Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions account_avatax_oca/data/avalara_salestax_data.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</record>
<record id="avatax_tax_group" model="account.tax.group">
<field name="name">AvaTax</field>
<field name="country_id" ref="base.us" />
</record>
<!-- Used as template for automatic Tax records created -->
<record id="avatax" model="account.tax">
Expand Down
112 changes: 28 additions & 84 deletions account_avatax_oca/models/account_move.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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",
)
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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 (
Expand All @@ -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}
Expand Down Expand Up @@ -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
Expand Down
30 changes: 26 additions & 4 deletions account_avatax_oca/models/avalara_salestax.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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!"),
),
]

Expand Down Expand Up @@ -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
10 changes: 7 additions & 3 deletions account_avatax_oca/models/partner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down
27 changes: 3 additions & 24 deletions account_avatax_oca/models/res_company.py
Original file line number Diff line number Diff line change
@@ -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")
7 changes: 1 addition & 6 deletions account_avatax_oca/tests/test_avatax.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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(
{
Expand All @@ -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(
Expand Down Expand Up @@ -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"}
Expand Down
Loading
Loading