Skip to content

Commit 213a0f1

Browse files
committed
[IMP] account_invoice_tax: refactor tax override to use a Json field
Replace the old relational model approach with a lightweight `tax_override_data = fields.Json(copy=False)` on `account.move`. The field stores manually-set amounts keyed by tax id (string) with the structure `{"<tax_id>": {"amount": float, "rate": float}}`. Overrides are **only** persisted for fixed-amount taxes; percentage-based taxes are always recomputed automatically and are never stored here.
1 parent fa5caa8 commit 213a0f1

6 files changed

Lines changed: 228 additions & 30 deletions

File tree

account_invoice_tax/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
# Part of Odoo. See LICENSE file for full copyright and licensing details.
22
from . import wizards
3+
from . import models

account_invoice_tax/__manifest__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "Account Invoice Tax",
3-
"version": "18.0.1.0.0",
3+
"version": "18.0.1.1.0",
44
"author": "ADHOC SA",
55
"category": "Localization",
66
"depends": [
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import account_move
2+
from . import account_tax
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
from contextlib import contextmanager
2+
3+
from odoo import fields, models
4+
5+
6+
class AccountMove(models.Model):
7+
_inherit = "account.move"
8+
9+
# Stores manually-overridden tax amounts keyed by tax id (as string).
10+
# Structure: { "<tax_id>": {"amount": float, "amount_company_currency": float} }
11+
# Only fixed-amount taxes are stored here; percentage taxes are always
12+
# recomputed automatically.
13+
tax_override_data = fields.Json(
14+
copy=False,
15+
)
16+
17+
def sync_tax_override_from_tax_totals(self):
18+
"""Rebuild ``tax_override_data`` from the current ``tax_totals`` widget value.
19+
20+
Iterates every tax group in ``tax_totals`` and, for each fixed-amount
21+
tax found, stores the displayed amounts as an override so they survive
22+
future recomputations.
23+
24+
``tax_totals`` structure (relevant fields)::
25+
26+
{
27+
"subtotals": [{
28+
"tax_groups": [{
29+
"involved_tax_ids": [<tax_id>, ...],
30+
"tax_amount": <float>, # company currency
31+
"tax_amount_currency": <float>, # invoice currency
32+
}, ...]
33+
}, ...]
34+
}
35+
36+
``amount`` (invoice-currency) → ``tax_amount_currency`` when the
37+
invoice is in a foreign currency, otherwise ``tax_amount``.
38+
``amount_company_currency`` → ``tax_amount`` (always company currency),
39+
or 0.0 when the invoice is in company currency (field unused in that
40+
case, see ``_apply_tax_overrides``).
41+
"""
42+
for rec in self:
43+
tax_totals = rec.tax_totals
44+
if not tax_totals:
45+
continue
46+
47+
is_company_currency = rec.currency_id == rec.company_currency_id
48+
new_overrides = {}
49+
50+
for subtotal in tax_totals.get("subtotals", []):
51+
for tax_group in subtotal.get("tax_groups", []):
52+
tax_amount = tax_group.get("tax_amount", 0.0)
53+
tax_amount_currency = tax_group.get("tax_amount_currency", 0.0)
54+
55+
for tax_id in tax_group.get("involved_tax_ids", []):
56+
tax = rec.env["account.tax"].browse(tax_id)
57+
if not tax.exists() or tax.amount_type != "fixed":
58+
continue
59+
60+
if is_company_currency:
61+
amount = tax_amount
62+
amount_cc = 0.0
63+
else:
64+
amount = tax_amount_currency
65+
amount_cc = tax_amount
66+
67+
new_overrides[str(tax_id)] = {
68+
"amount": amount,
69+
"amount_company_currency": amount_cc,
70+
}
71+
72+
rec.tax_override_data = new_overrides or False
73+
74+
# ------------------------------------------------------------------
75+
# Tax-totals widget: inject override amounts so the widget reflects
76+
# the manually-set values instead of the recomputed ones.
77+
# ------------------------------------------------------------------
78+
79+
def _compute_tax_totals(self):
80+
for move in self:
81+
overrides = move.tax_override_data or {}
82+
if not overrides:
83+
super(AccountMove, move)._compute_tax_totals()
84+
continue
85+
86+
rate = getattr(move, "inverse_invoice_currency_rate", None) or 1.0
87+
tax_context = {
88+
"inverse_invoice_currency_rate": rate,
89+
"tax_context": {
90+
int(tax_id): {
91+
"fixed_amount": (vals["amount"]),
92+
"rate": (move.invoice_currency_rate or 1.0),
93+
}
94+
for tax_id, vals in overrides.items()
95+
},
96+
}
97+
super(AccountMove, move.with_context(**tax_context))._compute_tax_totals()
98+
99+
# ------------------------------------------------------------------
100+
# Re-apply overrides after every tax-line recomputation so that the
101+
# amounts set through the wizard survive any subsequent edits on the
102+
# invoice lines.
103+
# ------------------------------------------------------------------
104+
105+
@contextmanager
106+
def _sync_tax_lines(self, container):
107+
"""Restore manually-set tax amounts after the core recomputes tax lines."""
108+
with super()._sync_tax_lines(container):
109+
yield
110+
for move in container.get("records", self):
111+
move._apply_tax_overrides()
112+
113+
def _apply_tax_overrides(self):
114+
"""Re-write values from ``tax_override_data`` onto the matching tax lines.
115+
116+
Only overrides for fixed-amount taxes are applied; percentage-based
117+
taxes must always reflect their recomputed values.
118+
"""
119+
overrides = self.tax_override_data or {}
120+
if not overrides:
121+
return
122+
123+
move_currency = self.currency_id
124+
company_currency = self.company_currency_id
125+
not_company_currency = move_currency and move_currency != company_currency
126+
for line in self.line_ids.filtered(lambda l: l.tax_line_id and str(l.tax_line_id.id) in overrides):
127+
vals = overrides[str(line.tax_line_id.id)]
128+
amount = vals.get("amount", 0.0)
129+
amount_cc = amount / vals.get("rate", 1)
130+
debit = credit = debit_cc = credit_cc = 0.0
131+
if self.move_type in ("in_invoice", "in_receipt"):
132+
if amount > 0:
133+
debit, debit_cc = amount, amount_cc
134+
elif amount < 0:
135+
credit, credit_cc = -amount, -amount_cc
136+
else:
137+
if amount > 0:
138+
credit, credit_cc = amount, amount_cc
139+
elif amount < 0:
140+
debit, debit_cc = -amount, -amount_cc
141+
142+
line_vals = {
143+
"debit": debit_cc if not_company_currency else debit,
144+
"credit": credit_cc if not_company_currency else credit,
145+
"balance": ((amount_cc if not_company_currency else amount) * (1 if debit or debit_cc else -1)),
146+
}
147+
if not_company_currency and amount:
148+
line_vals["amount_currency"] = amount
149+
line.write(line_vals)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
from odoo import api, models
3+
4+
5+
class AccountTax(models.Model):
6+
_inherit = "account.tax"
7+
8+
@api.model
9+
def _get_tax_totals_summary(self, base_lines, currency, company, cash_rounding=None):
10+
res = super()._get_tax_totals_summary(base_lines, currency, company, cash_rounding=cash_rounding)
11+
# ``tax_context`` is injected by AccountMove._compute_tax_totals when
12+
# there are active tax overrides. Structure:
13+
# { tax_id (int): {'fixed_amount': float, 'rate': float}, ... }
14+
tax_context = self.env.context.get("tax_context")
15+
if not tax_context:
16+
return res
17+
18+
for tax_group in res.get("subtotals", [{}])[0].get("tax_groups", []):
19+
for involved_tax_id in tax_group.get("involved_tax_ids", []):
20+
override = tax_context.get(involved_tax_id)
21+
if not override:
22+
continue
23+
new_amount = override.get("fixed_amount", 0.0)
24+
rate = override.get("rate", 1.0)
25+
original_amount = tax_group.get("tax_amount", 0.0)
26+
original_currency_amount = tax_group.get("tax_amount_currency", 0.0)
27+
if not new_amount or new_amount == original_currency_amount:
28+
continue
29+
diff = new_amount - original_currency_amount
30+
currency_diff = new_amount / rate - original_amount
31+
tax_group.update(
32+
{
33+
"tax_amount": new_amount / rate,
34+
"tax_amount_currency": new_amount,
35+
}
36+
)
37+
res["tax_amount"] += diff
38+
res["total_amount"] += diff
39+
res["tax_amount_currency"] += currency_diff
40+
res["total_amount_currency"] += currency_diff
41+
42+
return res

account_invoice_tax/wizards/account_invoice_tax.py

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ class AccountInvoiceTax(models.TransientModel):
1010
move_id = fields.Many2one("account.move", required=True)
1111
tax_line_ids = fields.One2many("account.invoice.tax_line", "invoice_tax_id")
1212

13+
is_in_company_currency = fields.Boolean(compute="_compute_is_in_company_currency")
14+
1315
@api.model
1416
def default_get(self, fields):
1517
res = super().default_get(fields)
@@ -34,24 +36,15 @@ def default_get(self, fields):
3436

3537
def action_update_tax(self):
3638
move = self.move_id
37-
fixed_taxes_bu = {
38-
line.tax_line_id: {
39-
"amount_currency": line.amount_currency,
40-
"debit": line.debit,
41-
"credit": line.credit,
42-
}
43-
for line in self.move_id.line_ids.filtered(
44-
lambda x: x.tax_repartition_line_id.tax_id.amount_type == "fixed"
45-
)
46-
}
4739

4840
active_tax = self.tax_line_ids.mapped("tax_id")
4941
origin_tax = self.move_id.line_ids.filtered(lambda x: x.tax_line_id).mapped("tax_repartition_line_id.tax_id")
5042
to_remove_tax = origin_tax - active_tax
5143
to_add_tax = active_tax - origin_tax
44+
5245
container = {"records": move, "self": move}
5346

54-
# change tax list
47+
# --- 1. Update tax list on invoice lines ---
5548
with move.with_context(check_move_validity=False)._check_balanced(container):
5649
with move._sync_dynamic_lines(container):
5750
if to_remove_tax:
@@ -63,29 +56,36 @@ def action_update_tax(self):
6356
{"tax_ids": [Command.link(tax_id.id) for tax_id in to_add_tax]}
6457
)
6558

66-
# set amount in the new created tax line. En este momento si queda balanceado y se ajusta la linea AP/AR
67-
container = {"records": move}
59+
# --- 2. Persist overrides in the JSON field so they survive recomputations ---
60+
self._save_overrides()
6861

69-
if move.move_type == "in_invoice":
70-
sign = 1
71-
else: # For refund
72-
sign = -1
62+
# --- 3. Apply overrides to the current tax lines ---
63+
container = {"records": move}
7364
with move._check_balanced(container):
7465
with move._sync_dynamic_lines(container):
75-
# restauramos todos los valores de impuestos fixed que se habrian recomputado
76-
# restaured = []
77-
for tax_line in move.line_ids.filtered(
78-
lambda x: x.tax_repartition_line_id.tax_id in fixed_taxes_bu
79-
and x.tax_repartition_line_id.tax_id.amount_type == "fixed"
80-
):
81-
tax_line.write(fixed_taxes_bu.get(tax_line.tax_line_id))
82-
for tax_line_id in self.tax_line_ids:
83-
# seteamos valor al impuesto segun lo que puso en el wizard
84-
line_with_tax = move.line_ids.filtered(lambda x: x.tax_line_id == tax_line_id.tax_id)
85-
line_with_tax.write({"amount_currency": tax_line_id.amount * sign})
66+
move._apply_tax_overrides()
67+
68+
def _save_overrides(self):
69+
"""Write wizard line amounts into ``tax_override_data`` on the move.
70+
71+
Only fixed-amount taxes are persisted as overrides. Percentage-based
72+
taxes are always recomputed automatically, so any stale entry for them
73+
is removed.
74+
"""
75+
new_overrides = {}
76+
move = self.move_id
77+
for wizard_line in self.tax_line_ids.filtered(lambda l: l.tax_id.amount_type == "fixed"):
78+
new_overrides[str(wizard_line.tax_id.id)] = {
79+
"amount": wizard_line.amount,
80+
"rate": self.move_id.invoice_currency_rate or 1.0,
81+
}
82+
83+
# Previous overrides are fully replaced – entries not present in
84+
# new_overrides (removed taxes or percentage-based taxes) are dropped.
85+
move.tax_override_data = new_overrides or False
8686

8787
def add_tax_and_new(self):
88-
self.add_tax()
88+
self.action_update_tax()
8989
return {
9090
"type": "ir.actions.act_window",
9191
"name": _("Edit tax lines"),
@@ -105,6 +105,10 @@ def check_analytic(self):
105105
% (", ".join(taxes.mapped(lambda x: "%s (%s)" % (x.name, x.id))))
106106
)
107107

108+
@api.depends("move_id")
109+
def _compute_is_in_company_currency(self):
110+
self.is_in_company_currency = self.move_id.currency_id == self.move_id.company_currency_id
111+
108112

109113
class AccountInvoiceTaxLine(models.TransientModel):
110114
_name = "account.invoice.tax_line"

0 commit comments

Comments
 (0)