Skip to content

Commit 0a32783

Browse files
committed
[IMP] Overrides as Json
1 parent b03ef9b commit 0a32783

8 files changed

Lines changed: 211 additions & 188 deletions

File tree

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.2.0",
3+
"version": "18.0.1.3.0",
44
"author": "ADHOC SA",
55
"category": "Localization",
66
"depends": [

account_invoice_tax/migrations/18.0.1.2.0/pre-migrate.py

Lines changed: 0 additions & 77 deletions
This file was deleted.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
"""
3+
Migration 18.0.1.2.0 → 18.0.1.3.0
4+
====================================
5+
The ``account.move.tax.override`` model has been replaced by a plain JSON
6+
field ``tax_override_data`` on ``account.move``.
7+
8+
This pre-migration script reads every row from the old
9+
``account_move_tax_override`` table and serialises the data into the new
10+
``tax_override_data`` column **before** the ORM updates the schema, so that
11+
no override data is lost during the upgrade.
12+
13+
JSON structure written to ``tax_override_data``:
14+
{ "<tax_id>": {"amount": <float>, "amount_company_currency": <float>} }
15+
"""
16+
17+
import json
18+
import logging
19+
20+
_logger = logging.getLogger(__name__)
21+
22+
23+
def migrate(cr, version):
24+
if not version:
25+
return
26+
27+
# Check that the source table still exists
28+
cr.execute(
29+
"""
30+
SELECT EXISTS (
31+
SELECT 1 FROM information_schema.tables
32+
WHERE table_name = 'account_move_tax_override'
33+
)
34+
"""
35+
)
36+
if not cr.fetchone()[0]:
37+
_logger.info(
38+
"account_move_tax_override table not found – skipping data migration."
39+
)
40+
return
41+
42+
# Add the new column if it does not exist yet (the ORM will add it during
43+
# the module update, but we need it available right now in pre-migrate).
44+
cr.execute(
45+
"""
46+
ALTER TABLE account_move
47+
ADD COLUMN IF NOT EXISTS tax_override_data jsonb
48+
"""
49+
)
50+
51+
# Read all override rows grouped by move
52+
cr.execute(
53+
"""
54+
SELECT move_id, tax_id, amount, amount_company_currency
55+
FROM account_move_tax_override
56+
ORDER BY move_id
57+
"""
58+
)
59+
rows = cr.fetchall()
60+
if not rows:
61+
_logger.info("No rows found in account_move_tax_override – nothing to migrate.")
62+
return
63+
64+
# Group by move_id
65+
by_move: dict[int, dict] = {}
66+
for move_id, tax_id, amount, amount_company_currency in rows:
67+
by_move.setdefault(move_id, {})[str(tax_id)] = {
68+
"amount": float(amount or 0),
69+
"amount_company_currency": float(amount_company_currency or 0),
70+
}
71+
72+
# Write into tax_override_data – merge with any value already present
73+
for move_id, override_dict in by_move.items():
74+
cr.execute(
75+
"""
76+
UPDATE account_move
77+
SET tax_override_data = COALESCE(tax_override_data, '{}'::jsonb)
78+
|| %s::jsonb
79+
WHERE id = %s
80+
""",
81+
(json.dumps(override_dict), move_id),
82+
)
83+
84+
_logger.info(
85+
"Migrated tax override data for %d move(s) into tax_override_data.",
86+
len(by_move),
87+
)
88+
89+
# Drop the old table now that data has been moved
90+
cr.execute("DROP TABLE IF EXISTS account_move_tax_override")
91+
_logger.info("Dropped table account_move_tax_override.")
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
1-
from . import account_move_tax_override
21
from . import account_move
32
from . import account_tax

account_invoice_tax/models/account_move.py

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,54 @@
44
class AccountMove(models.Model):
55
_inherit = "account.move"
66

7-
tax_override_ids = fields.One2many(
8-
"account.move.tax.override",
9-
"move_id",
10-
string="Tax Overrides",
7+
# Stores manually-overridden tax amounts keyed by tax id (as string).
8+
# Structure: { "<tax_id>": {"amount": float, "amount_company_currency": float} }
9+
# Only fixed-amount taxes are stored here; percentage taxes are always
10+
# recomputed automatically.
11+
tax_override_data = fields.Json(
12+
string="Tax Override Data",
1113
copy=False,
14+
default=dict,
1215
)
1316

17+
# ------------------------------------------------------------------
18+
# Helpers to read/write the JSON field in a structured way
19+
# ------------------------------------------------------------------
20+
21+
def _get_tax_overrides(self):
22+
"""Return the override dict keyed by integer tax id.
23+
24+
Structure: { tax_id (int): {"amount": float, "amount_company_currency": float} }
25+
"""
26+
self.ensure_one()
27+
raw = self.tax_override_data or {}
28+
result = {}
29+
for str_id, vals in raw.items():
30+
try:
31+
result[int(str_id)] = vals
32+
except (ValueError, TypeError):
33+
continue
34+
return result
35+
36+
def _set_tax_overrides(self, overrides_by_tax_id):
37+
"""Persist the override dict to the JSON field.
38+
39+
:param overrides_by_tax_id: dict mapping integer tax_id →
40+
{"amount": float, "amount_company_currency": float}
41+
"""
42+
self.ensure_one()
43+
self.tax_override_data = {
44+
str(tax_id): vals for tax_id, vals in overrides_by_tax_id.items()
45+
}
46+
1447
# ------------------------------------------------------------------
1548
# Tax-totals widget: inject override amounts so the widget reflects
1649
# the manually-set values instead of the recomputed ones.
1750
# ------------------------------------------------------------------
1851

1952
def _compute_tax_totals(self):
2053
for move in self:
21-
# Only fixed-amount tax overrides affect the totals widget;
22-
# percentage taxes are always recomputed.
23-
overrides = {o.tax_id.id: o for o in move.tax_override_ids if o.tax_id.amount_type == "fixed"}
54+
overrides = move._get_tax_overrides()
2455
if not overrides:
2556
super(AccountMove, move)._compute_tax_totals()
2657
continue
@@ -35,14 +66,22 @@ def _compute_tax_totals(self):
3566
# is not stored (always 0); use `amount` directly with rate=1.0.
3667
# If the invoice is in a foreign currency, use amount_company_currency
3768
# as fixed_amount and derive the rate from both fields.
38-
"fixed_amount": (override.amount if is_company_currency else override.amount_company_currency),
69+
"fixed_amount": (
70+
vals["amount"]
71+
if is_company_currency
72+
else vals["amount_company_currency"]
73+
),
3974
"rate": (
4075
1.0
4176
if is_company_currency
42-
else (override.amount_company_currency / override.amount if override.amount else 1.0)
77+
else (
78+
vals["amount_company_currency"] / vals["amount"]
79+
if vals.get("amount")
80+
else 1.0
81+
)
4382
),
4483
}
45-
for tax_id, override in overrides.items()
84+
for tax_id, vals in overrides.items()
4685
},
4786
}
4887
super(AccountMove, move.with_context(**tax_context))._compute_tax_totals()
@@ -67,14 +106,46 @@ def _recompute_tax_lines(
67106
move._apply_tax_overrides()
68107

69108
def _apply_tax_overrides(self):
70-
"""Re-write values from ``tax_override_ids`` onto the matching tax lines.
109+
"""Re-write values from ``tax_override_data`` onto the matching tax lines.
71110
72111
Only overrides for fixed-amount taxes are applied; percentage-based
73112
taxes must always reflect their recomputed values.
74113
"""
75-
if not self.tax_override_ids:
114+
overrides = self._get_tax_overrides()
115+
if not overrides:
76116
return
77-
overrides = {o.tax_id: o for o in self.tax_override_ids if o.tax_id.amount_type == "fixed"}
78-
for line in self.line_ids.filtered(lambda l: l.tax_line_id in overrides):
79-
override = overrides[line.tax_line_id]
80-
line.write(override._get_line_values(self))
117+
118+
move_currency = self.currency_id
119+
company_currency = self.company_currency_id
120+
not_company_currency = move_currency and move_currency != company_currency
121+
122+
for line in self.line_ids.filtered(
123+
lambda l: l.tax_line_id and l.tax_line_id.id in overrides
124+
):
125+
vals = overrides[line.tax_line_id.id]
126+
amount = vals.get("amount", 0.0)
127+
amount_cc = vals.get("amount_company_currency", 0.0)
128+
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": (
145+
(amount_cc if not_company_currency else amount)
146+
* (1 if debit or debit_cc else -1)
147+
),
148+
}
149+
if not_company_currency and amount:
150+
line_vals["amount_currency"] = amount
151+
line.write(line_vals)

account_invoice_tax/models/account_move_tax_override.py

Lines changed: 0 additions & 64 deletions
This file was deleted.
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
22
access_account_invoice_tax,access_account_invoice_tax,account_invoice_tax.model_account_invoice_tax,account.group_account_invoice,1,1,1,0
33
access_account_invoice_tax_line,access_account_invoice_tax_line,account_invoice_tax.model_account_invoice_tax_line,account.group_account_invoice,1,1,1,1
4-
access_account_move_tax_override,access_account_move_tax_override,account_invoice_tax.model_account_move_tax_override,account.group_account_invoice,1,1,1,1

0 commit comments

Comments
 (0)