Skip to content
Closed
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
28 changes: 28 additions & 0 deletions account_avatax_oca/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,31 @@ def _compute_all_tax_new(self):
if not hasattr(AccountMoveLine, "_compute_all_tax_origin"):
AccountMoveLine._compute_all_tax_origin = AccountMoveLine._compute_all_tax
AccountMoveLine._compute_all_tax = _compute_all_tax_new

@api.depends("quantity", "discount", "price_unit", "tax_ids", "currency_id")
def _compute_totals_new(self):
for line in self:
if line.display_type != "product":
line.price_total = line.price_subtotal = False
# Compute 'price_subtotal'.
line_discount_price_unit = line.price_unit * (1 - (line.discount / 100.0))
subtotal = line.quantity * line_discount_price_unit

# Compute 'price_total'.
if line.tax_ids:
taxes_res = line.tax_ids.with_context(current_aml=line.id).compute_all(
line_discount_price_unit,
quantity=line.quantity,
currency=line.currency_id,
product=line.product_id,
partner=line.partner_id,
is_refund=line.is_refund,
)
line.price_subtotal = taxes_res["total_excluded"]
line.price_total = taxes_res["total_included"]
else:
line.price_total = line.price_subtotal = subtotal

if not hasattr(AccountMoveLine, "_compute_totals_origin"):
AccountMoveLine._compute_totals_origin = AccountMoveLine._compute_totals
AccountMoveLine._compute_totals = _compute_totals_new
118 changes: 63 additions & 55 deletions account_avatax_oca/models/account_move.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,62 +238,70 @@ def _avatax_compute_tax(self, commit=False):
avatax_config.unvoid_transaction(self.name, doc_type)
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 self.state != "draft":
return tax_result
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 not tax_result_line:
continue
new_taxes = Tax
if avatax_config.breakdown_all_taxes:
details = tax_result_line["details"]
for detail in details:
fixed = detail.get("unitOfBasis") == "FlatAmount"
rate = detail["rate"] if fixed else detail["rate"] * 100
tax_group_name = detail["taxName"].removesuffix(" TAX")
tax_name_display = "%s %s" % (
tax_group_name,
("$ %.4g" if fixed else "%.4g%%") % round(rate, 4),
)
tax = Tax.get_avalara_tax(rate, doc_type, tax_name=tax_name_display)
tax, line = self.update_tax_details(tax, line, tax_result_line)
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
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: 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})
new_taxes |= tax
else:
taxable_amt = tax_result_line.get("taxableAmount", 0.0)
tax_calc_amt = tax_result_line.get("taxCalculated") or 0.0
if taxable_amt:
rate = round((tax_calc_amt / taxable_amt) * 100, 4)
tax = Tax.get_avalara_tax(rate, doc_type)
new_taxes |= tax
if new_taxes and new_taxes not in line.tax_ids:
line_taxes = line.tax_ids.filtered(lambda x: not x.is_avatax)
taxes_to_set[line.id] = line_taxes | new_taxes
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
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, taxes in taxes_to_set.items():
line = self.invoice_line_ids.filtered(lambda l: l.id == line_id)
line.write({"tax_ids": [(6, 0, [])]})
line.with_context(avatax_invoice=self, check_move_validity=False).write(
{"tax_ids": taxes.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})
return tax_result

# Same as v13
Expand Down
104 changes: 86 additions & 18 deletions account_avatax_oca/models/account_tax.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import ast
from math import copysign

from odoo import _, api, exceptions, fields, models
from odoo.tools.float_utils import float_compare
from odoo.tools.float_utils import float_compare, float_round


class AccountTax(models.Model):
Expand All @@ -12,8 +13,8 @@ class AccountTax(models.Model):
is_avatax = fields.Boolean()

@api.model
def _get_avalara_tax_domain(self, tax_rate, doc_type):
return [
def _get_avalara_tax_domain(self, tax_rate, doc_type, tax_name=None):
domain = [
("amount", "=", tax_rate),
("is_avatax", "=", True),
(
Expand All @@ -22,19 +23,22 @@ def _get_avalara_tax_domain(self, tax_rate, doc_type):
self.env.company.id,
),
]
if tax_name:
domain.append(("name", "=", tax_name))
return domain

@api.model
def _get_avalara_tax_name(self, tax_rate, doc_type=None):
return _("{}%*").format(str(tax_rate))

@api.model
def get_avalara_tax(self, tax_rate, doc_type):
domain = self._get_avalara_tax_domain(tax_rate, doc_type)
def get_avalara_tax(self, tax_rate, doc_type, tax_name=None):
domain = self._get_avalara_tax_domain(tax_rate, doc_type, tax_name)
tax = self.with_context(active_test=False).search(domain, limit=1)
if tax and not tax.active:
tax.active = True
if not tax:
domain = self._get_avalara_tax_domain(0, doc_type)
domain = self._get_avalara_tax_domain(0, doc_type, "")
tax_template = self.search(domain, limit=1)
if not tax_template:
raise exceptions.UserError(
Expand All @@ -45,13 +49,16 @@ def get_avalara_tax(self, tax_rate, doc_type):
# check the data for your existing Avatax taxes.
vals = {
"amount": tax_rate,
"name": self._get_avalara_tax_name(tax_rate, doc_type),
"name": tax_name
if tax_name
else self._get_avalara_tax_name(tax_rate, doc_type),
}
tax = tax_template.sudo().copy(default=vals)
# Odoo core does not use the name set in default dict
tax.name = vals.get("name")
return tax

# flake8: noqa: C901
def compute_all(
self,
price_unit,
Expand All @@ -78,11 +85,11 @@ def compute_all(
partner,
is_refund,
handle_price_include,
include_caba_tags,
fixed_multiplicator,
include_caba_tags=include_caba_tags,
fixed_multiplicator=fixed_multiplicator,
)
avatax_invoice = self.env.context.get("avatax_invoice")
current_aml = False
current_aml = self.env["account.move.line"]
if "current_aml" in self.env.context:
current_aml = self.env["account.move.line"].browse(
self.env.context.get("current_aml")
Expand All @@ -92,15 +99,17 @@ def compute_all(
and current_aml.account_type != "asset_receivable"
):
avatax_invoice = False
if not avatax_invoice and current_aml:
avatax_invoice = current_aml.move_id
if avatax_invoice:
# Find the Avatax amount in the invoice Lines
# Looks up the line for the current product, price_unit, and quantity
# Note that the price_unit used must consider discount
base = res["total_excluded"]
total_excluded = res["total_excluded"]
digits = 6
avatax_amount = None
if current_aml:
avatax_amount = copysign(current_aml.avatax_amt_line, base)
avatax_amount = copysign(current_aml.avatax_amt_line, total_excluded)
else:
for line in avatax_invoice.invoice_line_ids:
price_unit = line.currency_id._convert(
Expand All @@ -113,19 +122,78 @@ def compute_all(
line.product_id == product
and float_compare(line.quantity, quantity, digits) == 0
):
avatax_amount = copysign(line.avatax_amt_line, base)
avatax_amount = copysign(line.avatax_amt_line, total_excluded)
current_aml = line
break
if avatax_amount is None:
avatax_amount = 0.0
raise exceptions.UserError(
_(
"Incorrect retrieval of Avatax amount for Invoice %(avatax_invoice)s:"
" product %(product.display_name)s, price_unit %(-price_unit)f"
" , quantity %(quantity)f"
)
)
for tax_item in res["taxes"]:
if tax_item["amount"] != 0:
tax_item["amount"] = avatax_amount
res["total_included"] = base + avatax_amount
response = ast.literal_eval(avatax_invoice.avatax_response_log or "{}")
response_lines = {
int(l["lineNumber"]): l for l in response.get("lines", [])
}
doc_type = avatax_invoice._get_avatax_doc_type()
avatax_ids = self.search([("is_avatax", "=", True)]).ids
line = current_aml
line_result = response_lines.get(line.id)
if not line_result:
return res
relevant_tax_ids = [
x
for x in res["taxes"]
if x["id"] in line.tax_ids.ids and x["id"] in avatax_ids
]
if not relevant_tax_ids:
return res
company = self.env.company if not self else self[0].company_id
currency = currency or company.currency_id
precision = currency.rounding
round_tax = company.tax_calculation_rounding_method != "round_globally"
if "round" in self.env.context:
round_tax = bool(self.env.context["round"])
if not round_tax:
precision *= 1e-5
base = price_unit * quantity
if self._context.get("round_base", True):
base = currency.round(base)
sign = 1
if currency.is_zero(base):
sign = -1 if fixed_multiplicator < 0 else 1
elif base < 0:
sign = -1
avatax_config = avatax_invoice.company_id.get_avatax_config_company()
if not avatax_config:
return res
if not avatax_config.breakdown_all_taxes:
for tax_item in res["taxes"]:
if tax_item["amount"] != 0:
tax_item["amount"] = avatax_amount
else:
for detail in line_result.get("details", []):
fixed = detail.get("unitOfBasis") == "FlatAmount"
rate = detail["rate"] if fixed else detail["rate"] * 100
tax_group_name = detail.get("taxName", "").removesuffix(" TAX")
tax_name_display = "%s %s" % (
tax_group_name,
("$ %.4g" if fixed else "%.4g%%") % round(rate, 4),
)
tax = self.get_avalara_tax(
rate, doc_type, tax_name=tax_name_display
)
for tax_item in relevant_tax_ids:
if tax_item["id"] == tax.id:
tax_item["amount"] = (
float_round(detail["tax"], precision_rounding=precision)
* sign
)
tax_item["base"] = float_round(
sign * detail["taxableAmount"],
precision_rounding=precision,
)
res["total_included"] = total_excluded + avatax_amount
return res
5 changes: 5 additions & 0 deletions account_avatax_oca/models/avalara_salestax.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ def _get_avatax_supported_countries(self):
help="Uncheck the this field to show exemption fields on SO/Invoice form view. "
"Also, it will show Tax based on shipping address button",
)
breakdown_all_taxes = fields.Boolean(
default=False, # option to avoid disturbing current users who do not want to change
help="If checked, taxes will be shown as separate detailed lines instead of "
"one aggregated line.",
)
# TODO: add option to Display Prices with Tax Included
# Enabled the tax inclusive flag in the GetTax Request.

Expand Down
1 change: 1 addition & 0 deletions account_avatax_oca/views/avalara_salestax_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<field name="company_partner_id" />
<field name="disable_tax_calculation" />
<field name="disable_address_validation" />
<field name="breakdown_all_taxes" />
</group>
</group>
<notebook>
Expand Down
Loading
Loading