Skip to content

Commit 4add77c

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 4add77c

6 files changed

Lines changed: 220 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: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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+
tax_context = {
87+
"tax_context": {
88+
int(tax_id): {
89+
"fixed_amount": (vals["amount"]),
90+
"rate": (move.invoice_currency_rate or 1.0),
91+
}
92+
for tax_id, vals in overrides.items()
93+
},
94+
}
95+
super(AccountMove, move.with_context(**tax_context))._compute_tax_totals()
96+
97+
# ------------------------------------------------------------------
98+
# Re-apply overrides after every tax-line recomputation so that the
99+
# amounts set through the wizard survive any subsequent edits on the
100+
# invoice lines.
101+
# ------------------------------------------------------------------
102+
103+
@contextmanager
104+
def _sync_tax_lines(self, container):
105+
"""Restore manually-set tax amounts after the core recomputes tax lines."""
106+
with super()._sync_tax_lines(container):
107+
yield
108+
for move in container.get("records", self):
109+
move._apply_tax_overrides()
110+
111+
def _apply_tax_overrides(self):
112+
"""Re-write values from ``tax_override_data`` onto the matching tax lines.
113+
114+
Only overrides for fixed-amount taxes are applied; percentage-based
115+
taxes must always reflect their recomputed values.
116+
"""
117+
overrides = self.tax_override_data or {}
118+
if not overrides:
119+
return
120+
121+
move_currency = self.currency_id
122+
company_currency = self.company_currency_id
123+
not_company_currency = move_currency and move_currency != company_currency
124+
for line in self.line_ids.filtered(lambda l: l.tax_line_id and str(l.tax_line_id.id) in overrides):
125+
vals = overrides[str(line.tax_line_id.id)]
126+
rate = line.move_id.invoice_currency_rate or 1.0
127+
amount = vals.get("amount", 0.0)
128+
amount_cc = amount / rate
129+
debit = credit = debit_cc = credit_cc = 0.0
130+
if self.move_type in ("in_invoice", "in_receipt"):
131+
if amount > 0:
132+
debit, debit_cc = amount, amount_cc
133+
elif amount < 0:
134+
credit, credit_cc = -amount, -amount_cc
135+
else:
136+
if amount > 0:
137+
credit, credit_cc = amount, amount_cc
138+
elif amount < 0:
139+
debit, debit_cc = -amount, -amount_cc
140+
141+
line_vals = {
142+
"debit": debit_cc if not_company_currency else debit,
143+
"credit": credit_cc if not_company_currency else credit,
144+
"balance": ((amount_cc if not_company_currency else amount) * (1 if debit or debit_cc else -1)),
145+
}
146+
if not_company_currency and amount:
147+
line_vals["amount_currency"] = amount
148+
line.write(line_vals)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from odoo import api, models
2+
3+
4+
class AccountTax(models.Model):
5+
_inherit = "account.tax"
6+
7+
@api.model
8+
def _get_tax_totals_summary(self, base_lines, currency, company, cash_rounding=None):
9+
res = super()._get_tax_totals_summary(base_lines, currency, company, cash_rounding=cash_rounding)
10+
# ``tax_context`` is injected by AccountMove._compute_tax_totals when
11+
# there are active tax overrides. Structure:
12+
# { tax_id (int): {'fixed_amount': float, 'rate': float}, ... }
13+
tax_context = self.env.context.get("tax_context")
14+
if not tax_context:
15+
return res
16+
17+
for tax_group in res.get("subtotals", [{}])[0].get("tax_groups", []):
18+
for involved_tax_id in tax_group.get("involved_tax_ids", []):
19+
override = tax_context.get(involved_tax_id)
20+
if not override:
21+
continue
22+
new_amount = override.get("fixed_amount", 0.0)
23+
rate = override.get("rate", 1.0)
24+
original_amount = tax_group.get("tax_amount", 0.0)
25+
original_currency_amount = tax_group.get("tax_amount_currency", 0.0)
26+
if not new_amount or new_amount == original_currency_amount:
27+
continue
28+
diff = new_amount - original_currency_amount
29+
currency_diff = new_amount / rate - original_amount
30+
tax_group.update(
31+
{
32+
"tax_amount": new_amount / rate,
33+
"tax_amount_currency": new_amount,
34+
}
35+
)
36+
res["tax_amount"] += diff
37+
res["total_amount"] += diff
38+
res["tax_amount_currency"] += currency_diff
39+
res["total_amount_currency"] += currency_diff
40+
41+
return res

account_invoice_tax/wizards/account_invoice_tax.py

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -34,24 +34,15 @@ def default_get(self, fields):
3434

3535
def action_update_tax(self):
3636
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-
}
4737

4838
active_tax = self.tax_line_ids.mapped("tax_id")
4939
origin_tax = self.move_id.line_ids.filtered(lambda x: x.tax_line_id).mapped("tax_repartition_line_id.tax_id")
5040
to_remove_tax = origin_tax - active_tax
5141
to_add_tax = active_tax - origin_tax
42+
5243
container = {"records": move, "self": move}
5344

54-
# change tax list
45+
# --- 1. Update tax list on invoice lines ---
5546
with move.with_context(check_move_validity=False)._check_balanced(container):
5647
with move._sync_dynamic_lines(container):
5748
if to_remove_tax:
@@ -63,29 +54,36 @@ def action_update_tax(self):
6354
{"tax_ids": [Command.link(tax_id.id) for tax_id in to_add_tax]}
6455
)
6556

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}
57+
# --- 2. Persist overrides in the JSON field so they survive recomputations ---
58+
self._save_overrides()
6859

69-
if move.move_type == "in_invoice":
70-
sign = 1
71-
else: # For refund
72-
sign = -1
60+
# --- 3. Apply overrides to the current tax lines ---
61+
container = {"records": move}
7362
with move._check_balanced(container):
7463
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})
64+
move._apply_tax_overrides()
65+
66+
def _save_overrides(self):
67+
"""Write wizard line amounts into ``tax_override_data`` on the move.
68+
69+
Only fixed-amount taxes are persisted as overrides. Percentage-based
70+
taxes are always recomputed automatically, so any stale entry for them
71+
is removed.
72+
"""
73+
new_overrides = {}
74+
move = self.move_id
75+
for wizard_line in self.tax_line_ids.filtered(lambda l: l.tax_id.amount_type == "fixed"):
76+
new_overrides[str(wizard_line.tax_id.id)] = {
77+
"amount": wizard_line.amount,
78+
"rate": self.move_id.invoice_currency_rate or 1.0,
79+
}
80+
81+
# Previous overrides are fully replaced – entries not present in
82+
# new_overrides (removed taxes or percentage-based taxes) are dropped.
83+
move.tax_override_data = new_overrides or False
8684

8785
def add_tax_and_new(self):
88-
self.add_tax()
86+
self.action_update_tax()
8987
return {
9088
"type": "ir.actions.act_window",
9189
"name": _("Edit tax lines"),

0 commit comments

Comments
 (0)