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
457 changes: 457 additions & 0 deletions account_avatax_oca_line_grouping/README.rst

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions account_avatax_oca_line_grouping/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
19 changes: 19 additions & 0 deletions account_avatax_oca_line_grouping/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "Avalara Avatax Line Grouping",
"version": "16.0.1.0.0",
"category": "Accounting",
"summary": "Group document lines as a single line for Avatax computation",
"author": "Binhex, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/account-fiscal-rule",
"license": "AGPL-3",
"depends": [
"account_avatax_oca",
"account_avatax_sale_oca",
],
"data": [
"views/avalara_salestax_view.xml",
],
"demo": [],
"installable": True,
"application": False,
}
6 changes: 6 additions & 0 deletions account_avatax_oca_line_grouping/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from . import res_company
from . import avalara_salestax
from . import account_move
from . import account_move_line
from . import sale_order
from . import sale_order_line
183 changes: 183 additions & 0 deletions account_avatax_oca_line_grouping/models/account_move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import logging

from odoo import fields, models
from odoo.tools import float_compare, float_round

_logger = logging.getLogger(__name__)


class AccountMove(models.Model):
_inherit = "account.move"

def _avatax_compute_tax(self, commit=False): # noqa: C901
"""Contact REST API and recompute taxes for an Invoice."""
self and self.ensure_one()
avatax_config = self.company_id.get_avatax_config_company()
if not avatax_config:
# 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)

tax_result = avatax_config.create_transaction(
self.invoice_date or fields.Date.today(),
self.name,
doc_type,
(
self.so_partner_id
if self.so_partner_id and avatax_config.use_so_partner_id
else self.partner_id
),
self.warehouse_id.partner_id or self.company_id.partner_id,
self.tax_address_id or self.partner_id,
taxable_lines,
self.user_id,
self.exemption_code or None,
self.exemption_code_id.code or None,
commit,
tax_date,
# TODO: can we report self.invoice_doc_no?
self.name if self.move_type == "out_refund" else "",
self.location_code or "",
is_override=self.move_type == "out_refund",
currency_id=self.currency_id,
ignore_error=300 if commit else None,
log_to_record=self,
)

# If committing, and document exists, try unvoiding it
# Error number 300 = GetTaxError, Expected Saved|Posted
if commit and tax_result.get("number") == 300:
_logger.info(
"Document %s (%s) already exists in Avatax. "
"Should be a voided transaction. "
"Unvoiding and re-committing.",
self.name,
doc_type,
)
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 = {}

if avatax_config.avatax_group_lines and len(tax_result_lines) == 1:
single_result_line = list(tax_result_lines.values())[0]

total_tax = (
single_result_line.get("taxCalculated")
or single_result_line.get("tax")
or 0.0
)

inv_lines = self.invoice_line_ids.filtered(lambda l: not l.display_type)
if inv_lines and total_tax:

def _line_base(line):
amount = line._get_avatax_amount()
if line.quantity < 0:
amount = -amount
return amount

total_base = sum(_line_base(line) for line in inv_lines)

if total_base:
tax_calculation = 0.0
if single_result_line.get("taxableAmount"):
tax_calculation = (
single_result_line["taxCalculated"]
/ single_result_line["taxableAmount"]
)
rate = round(tax_calculation * 100, 4)
tax = Tax.get_avalara_tax(rate, doc_type)

remaining_tax = total_tax
currency = self.currency_id
line_count = len(inv_lines)

for idx, line in enumerate(inv_lines, start=1):
base = _line_base(line)

if idx == line_count:
line_tax = remaining_tax
else:
share = base / total_base if total_base else 0.0
line_tax = float_round(
total_tax * share,
precision_rounding=currency.rounding,
)
remaining_tax -= line_tax

tax_line, line_obj = self.update_tax_details(
tax, line, single_result_line
)

if tax_line:
base_taxes = line.tax_ids.filtered(
lambda x: not x.is_avatax
)
if avatax_config.override_line_taxes:
taxes_to_set[line.id] = tax_line
else:
taxes_to_set[line.id] = base_taxes | tax_line

line.avatax_amt_line = line_tax
else:
for line in self.invoice_line_ids:
tax_result_line = tax_result_lines.get(line.id)
if tax_result_line:
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)
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}

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})

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
65 changes: 65 additions & 0 deletions account_avatax_oca_line_grouping/models/account_move_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from odoo import models


class AccountMoveLine(models.Model):
_inherit = "account.move.line"

def _avatax_prepare_line(self, sign=1, doc_type=None):
"""
Prepare invoice line data for Avatax.

When company.avatax_group_lines is enabled, only the first invoice line
of the move will produce a dict, aggregating the amounts of all lines.
All other lines will return {} so they are ignored in the Avatax payload.
"""
move = self.move_id
company = move.company_id

if company and getattr(company, "avatax_group_lines", False):
first_line = move.invoice_line_ids[:1]
if not first_line or self != first_line[0]:
return {}

total_amount = 0.0
for line in move.invoice_line_ids:
amount = sign * line._get_avatax_amount()
if hasattr(line, "quantity") and line.quantity < 0:
amount = -amount
total_amount += amount

line = first_line[0]
product = line.product_id

item_code = None
tax_code = None
upc_enable = False
avatax_config = None

if hasattr(company, "get_avatax_config_company"):
avatax_config = company.get_avatax_config_company()
if avatax_config:
upc_enable = bool(getattr(avatax_config, "upc_enable", False))

if product:
if product.barcode and upc_enable:
item_code = "UPC:%s" % product.barcode
else:
item_code = product.default_code or "ID:%d" % product.id
tax_code = product.applicable_tax_code_id.name

if not item_code:
item_code = "DOC:%s" % (move.name or move.ref or move.id)
description = line.name or move.name or "Combined Items"

return {
"qty": 1,
"itemcode": item_code,
"description": description,
"amount": total_amount,
"tax_code": tax_code,
"id": line,
"account_id": line.account_id.id,
"tax_id": line.tax_ids,
}

return super()._avatax_prepare_line(sign=sign, doc_type=doc_type)
15 changes: 15 additions & 0 deletions account_avatax_oca_line_grouping/models/avalara_salestax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from odoo import fields, models


class AvalaraSalestax(models.Model):
_inherit = "avalara.salestax"

avatax_group_lines = fields.Boolean(
string="Group document lines for Avatax",
related="company_id.avatax_group_lines",
readonly=False,
help=(
"If enabled on the company, Avatax will receive a single aggregated "
"line per document instead of one line per order/invoice line."
),
)
14 changes: 14 additions & 0 deletions account_avatax_oca_line_grouping/models/res_company.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from odoo import fields, models


class ResCompany(models.Model):
_inherit = "res.company"

avatax_group_lines = fields.Boolean(
string="Group document lines for Avatax",
help=(
"If enabled, all lines of a Sales Order or Invoice will be sent to "
"Avatax as a single aggregated line for tax computation."
),
default=False,
)
Loading