diff --git a/account_avatax_oca/data/avalara_salestax_data.xml b/account_avatax_oca/data/avalara_salestax_data.xml
index 8645f89ea..50e8df5e8 100644
--- a/account_avatax_oca/data/avalara_salestax_data.xml
+++ b/account_avatax_oca/data/avalara_salestax_data.xml
@@ -7,6 +7,7 @@
AvaTax
+
diff --git a/account_avatax_oca/models/account_move.py b/account_avatax_oca/models/account_move.py
index 52996c5ce..77092901d 100644
--- a/account_avatax_oca/models/account_move.py
+++ b/account_avatax_oca/models/account_move.py
@@ -2,7 +2,6 @@
from odoo import api, fields, models
from odoo.exceptions import UserError
-from odoo.tools import float_compare
_logger = logging.getLogger(__name__)
@@ -116,14 +115,13 @@ def onchange_warehouse_id(self):
@api.model
@api.depends("company_id")
def _compute_hide_exemption(self):
- avatax_config = self.env.company.get_avatax_config_company()
for inv in self:
+ avatax_config = inv.company_id.avatax_configuration_id
inv.hide_exemption = avatax_config.hide_exemption
hide_exemption = fields.Boolean(
"Hide Exemption & Tax Based on shipping address",
compute=_compute_hide_exemption, # For past transactions visibility
- default=lambda self: self.env.company.get_avatax_config_company,
help="Uncheck the this field to show exemption fields on SO/Invoice form view. "
"Also, it will show Tax based on shipping address button",
)
@@ -164,7 +162,7 @@ def get_origin_tax_date(self):
# Same as v12
def _get_avatax_doc_type(self, commit=True):
self.ensure_one()
- avatax_config = self.company_id.get_avatax_config_company()
+ avatax_config = self.company_id.avatax_configuration_id
if avatax_config.disable_tax_reporting:
commit = False
if "refund" in self.move_type:
@@ -187,15 +185,15 @@ def _avatax_prepare_lines(self, doc_type=None):
]
return [x for x in lines if x]
- # Same as v12
def _avatax_compute_tax(self, commit=False):
"""Contact REST API and recompute taxes for a Sale Order"""
# Override to handle lines with split taxes (e.g. TN)
self and self.ensure_one()
- avatax_config = self.company_id.get_avatax_config_company()
- if not avatax_config:
+ avatax_config = self.company_id.avatax_configuration_id
+ if not (avatax_config and self.state == "draft"):
# Skip Avatax computation if no configuration is found
return
+
doc_type = self._get_avatax_doc_type(commit=commit)
tax_date = self.get_origin_tax_date() or self.invoice_date
taxable_lines = self._avatax_prepare_lines(doc_type)
@@ -236,69 +234,15 @@ def _avatax_compute_tax(self, commit=False):
avatax_config.commit_transaction(self.name, doc_type)
return tax_result
- if self.state == "draft":
- Tax = self.env["account.tax"]
- tax_result_lines = {int(x["lineNumber"]): x for x in tax_result["lines"]}
- taxes_to_set = {}
- for line in self.invoice_line_ids:
- tax_result_line = tax_result_lines.get(line.id)
- if tax_result_line:
- # rate = tax_result_line.get("rate", 0.0)
- tax_calculation = 0.0
- if tax_result_line["taxableAmount"]:
- tax_calculation = (
- tax_result_line["taxCalculated"]
- / tax_result_line["taxableAmount"]
- )
- rate = round(tax_calculation * 100, 4)
- tax = Tax.get_avalara_tax(rate, doc_type)
- if tax and tax not in line.tax_ids:
- line_taxes = line.tax_ids.filtered(lambda x: not x.is_avatax)
- taxes_to_set[line.id] = line_taxes | tax
- line.avatax_amt_line = tax_result_line["tax"]
- self.with_context(check_move_validity=False).avatax_amount = tax_result[
- "totalTax"
- ]
- container = {"records": self}
-
- # Set Taxes on lines in a way that properly triggers onchanges
- # This same approach is also used by the official account_taxcloud connector
-
- # for index, taxes in taxes_to_set:
- # # Access the invoice line by index
- # line = self.invoice_line_ids[index]
- # # Update the tax_ids field
- # line.write({"tax_ids": [(6, 0, [tax.id for tax in taxes])]})
-
- with (
- self.with_context(
- avatax_invoice=self, check_move_validity=False
- )._sync_dynamic_lines(container),
- self.line_ids.mapped("move_id")._check_balanced(container),
- ):
- for line_id in taxes_to_set.keys():
- line = self.invoice_line_ids.filtered(
- lambda x, line_id=line_id: x.id == line_id
- )
- line.write({"tax_ids": [(6, 0, [])]})
- line.with_context(
- avatax_invoice=self, check_move_validity=False
- ).write({"tax_ids": taxes_to_set.get(line_id).ids})
- # After taxes are changed is needed to force compute taxes again,
- # in 16 version change of tax doesn't trigger compute of taxes
- # on header for unknown reason
- self._compute_amount()
- if float_compare(
- self.amount_untaxed + max(self.amount_tax, abs(self.avatax_amount)),
- self.amount_residual,
- precision_rounding=self.currency_id.rounding or 0.001,
- ):
- taxes_data = {
- iline.id: iline.tax_ids for iline in self.invoice_line_ids
- }
- self.invoice_line_ids.write({"tax_ids": [(6, 0, [])]})
- for line in self.invoice_line_ids:
- line.write({"tax_ids": taxes_data[line.id].ids})
+ tax_result_lines = avatax_config.get_avatax_line_tax(tax_result)
+ for line in self.invoice_line_ids:
+ line_tax = tax_result_lines.get(line.id, {})
+ tax = line_tax.get("tax_id")
+ if tax and tax not in line.tax_ids:
+ line.tax_ids = line.tax_ids.filtered(lambda x: not x.is_avatax) | tax
+ line.avatax_amt_line = line_tax.get("tax_amount", 0.0)
+ self.avatax_amount = tax_result.get("totalTax", 0.0)
+
return tax_result
# Same as v13
@@ -318,7 +262,7 @@ def avatax_compute_taxes(self, commit=False):
def avatax_commit_taxes(self):
for invoice in self:
- avatax_config = invoice.company_id.get_avatax_config_company()
+ avatax_config = invoice.company_id.avatax_configuration_id
if not avatax_config.disable_tax_reporting:
doc_type = invoice._get_avatax_doc_type()
avatax_config.commit_transaction(invoice.name, doc_type)
@@ -334,7 +278,7 @@ def is_avatax_calculated(self):
def _post(self, soft=True):
for invoice in self:
if invoice.is_avatax_calculated():
- avatax_config = self.company_id.get_avatax_config_company()
+ avatax_config = invoice.company_id.avatax_configuration_id
if avatax_config and avatax_config.force_address_validation:
for addr in [self.partner_id, self.partner_shipping_id]:
if not addr.date_validation:
@@ -389,7 +333,7 @@ def button_draft(self):
)
res = super().button_draft()
for invoice in posted_invoices:
- avatax_config = invoice.company_id.get_avatax_config_company()
+ avatax_config = invoice.company_id.avatax_configuration_id
if avatax_config:
doc_type = invoice._get_avatax_doc_type()
avatax_config.void_transaction(invoice.name, doc_type)
@@ -403,7 +347,7 @@ def button_draft(self):
"partner_id",
)
def onchange_avatax_calculation(self):
- avatax_config = self.env.company.get_avatax_config_company()
+ avatax_config = self.company_id.avatax_configuration_id
self.calculate_tax_on_save = False
if avatax_config.invoice_calculate_tax:
if (
@@ -425,29 +369,29 @@ def onchange_avatax_calculation(self):
def write(self, vals):
result = super().write(vals)
- avatax_config = self.env.company.get_avatax_config_company()
- for record in self:
+ for move in self:
+ avatax_config = move.company_id.avatax_configuration_id
if (
avatax_config.invoice_calculate_tax
- and record.calculate_tax_on_save
- and record.state == "draft"
- and not self._context.get("skip_second_write", False)
+ and move.calculate_tax_on_save
+ and move.state == "draft"
+ and not self.env.context.get("skip_second_write", False)
):
- record.with_context(skip_second_write=True).write(
+ move.with_context(skip_second_write=True).write(
{"calculate_tax_on_save": False}
)
- record.avatax_compute_taxes()
+ move.avatax_compute_taxes()
return result
@api.model_create_multi
def create(self, vals_list):
moves = super().create(vals_list)
- avatax_config = self.env.company.get_avatax_config_company()
for move in moves:
+ avatax_config = move.company_id.avatax_configuration_id
if (
avatax_config.invoice_calculate_tax
and move.calculate_tax_on_save
- and not self._context.get("skip_second_write", False)
+ and not self.env.context.get("skip_second_write", False)
):
move.with_context(skip_second_write=True).write(
{"calculate_tax_on_save": False}
@@ -503,7 +447,7 @@ def _avatax_prepare_line(self, sign=1, doc_type=None):
line = self
res = {}
# Add UPC to product item code
- avatax_config = line.company_id.get_avatax_config_company()
+ avatax_config = line.company_id.avatax_configuration_id
product = line.product_id
if product.barcode and avatax_config.upc_enable:
item_code = "UPC:%d" % product.barcode
diff --git a/account_avatax_oca/models/avalara_salestax.py b/account_avatax_oca/models/avalara_salestax.py
index 2b8a00105..227149f78 100644
--- a/account_avatax_oca/models/avalara_salestax.py
+++ b/account_avatax_oca/models/avalara_salestax.py
@@ -1,6 +1,6 @@
import logging
-from odoo import api, fields, models
+from odoo import _, api, fields, models
from odoo.exceptions import UserError
from .avatax_rest_api import AvaTaxRESTService
@@ -157,13 +157,13 @@ def _get_avatax_supported_countries(self):
_sql_constraints = [
(
"code_company_uniq",
- "unique (company_code)",
- "Avalara setting is already available for this company code",
+ "unique (company_id)",
+ _("Only one Avatax configuration per company allowed."),
),
(
"account_number_company_uniq",
"unique (account_number, company_id)",
- "The account number must be unique per company!",
+ _("The account number must be unique per company!"),
),
]
@@ -337,3 +337,25 @@ def ping(self):
client = AvaTaxRESTService(config=self)
client.ping()
return True
+
+ #
+ # AVATAX RESPONSE PARSING HELPERS
+ #
+
+ def get_avatax_line_tax(self, avatax_response):
+ """
+ Receives an Avatax API response JSON-like object
+ Returns a dict mapping line database IDs to the tax amount and ID:
+ {123: {"taxable": 200.0, "tax_amount": 20.0, "tax_id": account.tax(12)}, ...}
+ """
+ res = {}
+ for line in avatax_response.get("lines", []):
+ taxable = line.get("taxableAmount", 0.0)
+ tax_amount = line.get("tax", 0.0)
+ rate = round(sum(x["rate"] for x in line.get("details", [])) * 100, 4)
+ real_rate = round(tax_amount * 100 / taxable if taxable else 0.0, 4)
+ Tax = self.env["account.tax"].sudo().with_company(self.company_id)
+ tax = Tax.get_avalara_tax(real_rate, display_rate=rate)
+ line_id = int(line["lineNumber"])
+ res[line_id] = {"taxable": taxable, "tax_amount": tax_amount, "tax_id": tax}
+ return res
diff --git a/account_avatax_oca/models/partner.py b/account_avatax_oca/models/partner.py
index 6a837fbb4..8031e2abf 100644
--- a/account_avatax_oca/models/partner.py
+++ b/account_avatax_oca/models/partner.py
@@ -156,7 +156,9 @@ def get_valid_address_vals(self, validation_on_save=False):
partner.display_name,
)
return False
- avatax_config = self.env.company.get_avatax_config_company()
+ company = self.company_id or self.env.company
+ avatax_config = company.avatax_configuration_id
+
# Skip automatic validation for countries not supported by Avatax
supported_countries = [x.code for x in avatax_config.country_ids]
country_code = partner.country_id.code
@@ -213,8 +215,9 @@ def button_avatax_validate_address(self):
@api.model_create_multi
def create(self, vals_list):
partners = super().create(vals_list)
- avatax_config = self.env.company.get_avatax_config_company()
for partner in partners:
+ company = partner.company_id or self.env.company
+ avatax_config = company.avatax_configuration_id
# Auto populate customer code, if not provided
if not partner.customer_code:
partner.generate_cust_code()
@@ -231,7 +234,8 @@ def write(self, vals):
x in vals for x in address_fields
):
partner = self.with_context(avatax_writing=True)
- avatax_config = self.env.company.get_avatax_config_company()
+ company = partner.company_id or self.env.company
+ avatax_config = company.avatax_configuration_id
if avatax_config.validation_on_save:
partner.multi_address_validation(validation_on_save=True)
partner.validated_on_save = True
diff --git a/account_avatax_oca/models/res_company.py b/account_avatax_oca/models/res_company.py
index 9127e3ac8..c41fd799b 100644
--- a/account_avatax_oca/models/res_company.py
+++ b/account_avatax_oca/models/res_company.py
@@ -1,29 +1,8 @@
-import logging
-
-from odoo import models
-
-_LOGGER = logging.getLogger(__name__)
+from odoo import fields, models
class Company(models.Model):
_inherit = "res.company"
- def get_avatax_config_company(self):
- """Returns the AvaTax configuration for the Company"""
- if self:
- self.ensure_one()
- AvataxConfig = self.env["avalara.salestax"]
- res = AvataxConfig.search(
- [("company_id", "=", self.id), ("disable_tax_calculation", "=", False)]
- )
- if len(res) > 1:
- _LOGGER.warning(
- self.env._("Company %s has too many Avatax configurations!"),
- self.display_name,
- )
- if len(res) < 1:
- _LOGGER.warning(
- self.env._("Company %s has no Avatax configuration."),
- self.display_name,
- )
- return res and res[0]
+ # Although it is a One2many field, is is ensured to be one or zero records
+ avatax_configuration_id = fields.One2many("avalara.salestax", "company_id")
diff --git a/account_avatax_oca/tests/test_avatax.py b/account_avatax_oca/tests/test_avatax.py
index 01211b673..5b9485882 100644
--- a/account_avatax_oca/tests/test_avatax.py
+++ b/account_avatax_oca/tests/test_avatax.py
@@ -76,9 +76,6 @@ def test_101_moves_onchange(self):
self.invoice.action_post()
self.invoice.button_draft()
- @patch(
- "odoo.addons.account_avatax_oca.models.res_company.Company.get_avatax_config_company"
- )
@patch(
"odoo.addons.account_avatax_oca.models.avalara_salestax.AvalaraSalestax.create_transaction" # noqa: B950
)
@@ -89,7 +86,6 @@ def test_avatax_compute_tax(
self,
mock_void_transaction,
mock_create_transaction,
- mock_get_avatax_config_company,
):
avatax_config = self.env["avalara.salestax"].create(
{
@@ -98,9 +94,9 @@ def test_avatax_compute_tax(
"company_code": "DEFAULT2",
"disable_tax_calculation": False,
"invoice_calculate_tax": False,
+ "company_id": self.env.company.id,
}
)
- mock_get_avatax_config_company.return_value = avatax_config
# Force empty taxes to check only avatax taxes
self.invoice.invoice_line_ids.write(
@@ -165,7 +161,6 @@ def test_avatax_compute_tax(
self.invoice.amount_tax + self.invoice.amount_untaxed,
self.invoice.amount_residual,
)
- mock_get_avatax_config_company.assert_called()
mock_create_transaction.assert_called()
mock_void_transaction.return_value = {"status": "success"}
diff --git a/account_avatax_sale_oca/models/sale_order.py b/account_avatax_sale_oca/models/sale_order.py
index b0a916d23..d26166b07 100644
--- a/account_avatax_sale_oca/models/sale_order.py
+++ b/account_avatax_sale_oca/models/sale_order.py
@@ -1,5 +1,9 @@
+import logging
+
from odoo import api, fields, models
+_logger = logging.getLogger(__name__)
+
class SaleOrder(models.Model):
_inherit = "sale.order"
@@ -16,15 +20,14 @@ def _compute_tax_totals(self):
@api.model
@api.depends("company_id", "partner_id", "partner_invoice_id", "state")
def _compute_hide_exemption(self):
- avatax_config = self.env.company.get_avatax_config_company()
for order in self:
+ avatax_config = order.company_id.avatax_configuration_id
order.hide_exemption = avatax_config.hide_exemption
hide_exemption = fields.Boolean(
"Hide Exemption & Tax Based on shipping address",
compute="_compute_hide_exemption", # For past transactions visibility
- default=lambda self: self.env.company.get_avatax_config_company,
- help="Uncheck this field to show exemption fields on SO/Invoice form view. "
+ help="Uncheck the this field to show exemption fields on SO/Invoice form view. "
"Also, it will show Tax based on shipping address button",
)
tax_amount = fields.Monetary(string="AvaTax")
@@ -172,16 +175,21 @@ def _avatax_prepare_lines(self, order_lines, doc_type=None):
def _avatax_compute_tax(self):
"""Contact REST API and recompute taxes for a Sale Order"""
- # Override to handle lines with split taxes (e.g. TN)
self and self.ensure_one()
- doc_type = self._get_avatax_doc_type()
- Tax = self.env["account.tax"]
- avatax_config = self.company_id.get_avatax_config_company()
+ if self.state == "cancel" or self.locked:
+ # Exit early if no tax is to be recalculated
+ _logger.warning(
+ "Order %s is locked or cancelled, can't recompute tax", self.name
+ )
+ return False
+
+ avatax_config = self.company_id.avatax_configuration_id
if not avatax_config:
return False
partner = self.partner_id
if avatax_config.use_partner_invoice_id:
partner = self.partner_invoice_id
+ doc_type = self._get_avatax_doc_type()
taxable_lines = self._avatax_prepare_lines(self.order_line)
tax_result = avatax_config.create_transaction(
self.date_order,
@@ -197,30 +205,13 @@ def _avatax_compute_tax(self):
currency_id=self.currency_id,
log_to_record=self,
)
- tax_result_lines = {int(x["lineNumber"]): x for x in tax_result["lines"]}
+ tax_result_lines = avatax_config.get_avatax_line_tax(tax_result)
for line in self.order_line:
- tax_result_line = tax_result_lines.get(line.id)
- if tax_result_line:
- # Should we check the rate with the tax amount?
- # tax_amount = tax_result_line["taxCalculated"]
- # rate = round(tax_amount / line.price_subtotal * 100, 2)
- # rate = tax_result_line["rate"]
- tax_calculation = 0.0
- if tax_result_line["taxableAmount"]:
- tax_calculation = (
- tax_result_line["taxCalculated"]
- / tax_result_line["taxableAmount"]
- )
- rate = round(tax_calculation * 100, 4)
- tax = Tax.get_avalara_tax(rate, doc_type)
- if tax not in line.tax_id:
- line_taxes = (
- tax
- if avatax_config.override_line_taxes
- else tax | line.tax_id.filtered(lambda x: not x.is_avatax)
- )
- line.tax_id = line_taxes
- line.tax_amt = tax_result_line["tax"]
+ line_tax = tax_result_lines.get(line.id, {})
+ tax = line_tax.get("tax_id")
+ if tax and tax not in line.tax_id:
+ line.tax_id = line.tax_id.filtered(lambda x: not x.is_avatax) | tax
+ line.tax_amt = line_tax.get("tax_amount", 0.0)
self.tax_amount = tax_result.get("totalTax")
return True
@@ -235,15 +226,16 @@ def avalara_compute_taxes(self):
return True
def action_confirm(self):
- avatax_config = self.company_id.get_avatax_config_company()
- if avatax_config and avatax_config.force_address_validation:
- for addr in [self.partner_id, self.partner_shipping_id]:
- if not addr.date_validation:
- # The Confirm action will be interrupted
- # if the address is not validated
- return addr.button_avatax_validate_address()
- if avatax_config:
- self.avalara_compute_taxes()
+ for order in self:
+ avatax_config = order.company_id.avatax_configuration_id
+ if avatax_config and avatax_config.force_address_validation:
+ for addr in [order.partner_id, order.partner_shipping_id]:
+ if not addr.date_validation:
+ # The Confirm action will be interrupted
+ # if the address is not validated
+ return addr.button_avatax_validate_address()
+ if avatax_config:
+ order.avalara_compute_taxes()
return super().action_confirm()
@api.onchange(
@@ -253,7 +245,7 @@ def action_confirm(self):
"partner_id",
)
def onchange_avatax_calculation(self):
- avatax_config = self.env.company.get_avatax_config_company()
+ avatax_config = self.company_id.avatax_configuration_id
self.calculate_tax_on_save = False
if avatax_config.sale_calculate_tax:
if (
@@ -276,37 +268,33 @@ def onchange_avatax_calculation(self):
@api.model_create_multi
def create(self, vals_list):
sales = super().create(vals_list)
- avatax_config = self.env.company.get_avatax_config_company()
for sale in sales:
+ avatax_config = sale.company_id.avatax_configuration_id
if (
avatax_config.sale_calculate_tax
and sale.calculate_tax_on_save
- and not self._context.get("skip_second_write", False)
+ and not self.env.context.get("skip_second_write", False)
):
sale.with_context(skip_second_write=True).write(
- {
- "calculate_tax_on_save": False,
- }
+ {"calculate_tax_on_save": False}
)
sale.avalara_compute_taxes()
return sales
def write(self, vals):
result = super().write(vals)
- avatax_config = self.env.company.get_avatax_config_company()
- for record in self:
+ for sale in self:
+ avatax_config = sale.company_id.avatax_configuration_id
if (
avatax_config.sale_calculate_tax
- and record.calculate_tax_on_save
- and record.state != "done"
- and not self._context.get("skip_second_write", False)
+ and sale.calculate_tax_on_save
+ and sale.state != "done"
+ and not self.env.context.get("skip_second_write", False)
):
- record.with_context(skip_second_write=True).write(
- {
- "calculate_tax_on_save": False,
- }
+ sale.with_context(skip_second_write=True).write(
+ {"calculate_tax_on_save": False}
)
- record.avalara_compute_taxes()
+ sale.avalara_compute_taxes()
return result
@@ -323,7 +311,7 @@ def _avatax_prepare_line(self, sign=1, doc_type=None):
line = self
res = {}
# Add UPC to product item code
- avatax_config = line.company_id.get_avatax_config_company()
+ avatax_config = line.company_id.avatax_configuration_id
product = line.product_id
if product.barcode and avatax_config.upc_enable:
item_code = "UPC:%d" % product.barcode
diff --git a/account_avatax_sale_oca/views/sale_order_view.xml b/account_avatax_sale_oca/views/sale_order_view.xml
index cbc213042..f7789d9ba 100644
--- a/account_avatax_sale_oca/views/sale_order_view.xml
+++ b/account_avatax_sale_oca/views/sale_order_view.xml
@@ -13,7 +13,7 @@
name="avalara_compute_taxes"
type="object"
string="Compute Taxes"
- invisible="is_avatax == False or state in ('done', 'cancel')"
+ invisible="is_avatax == False or state in ('done', 'cancel') or locked"
/>