Skip to content

Commit 5576f61

Browse files
committed
[WIP]
1 parent f3ab400 commit 5576f61

9 files changed

Lines changed: 368 additions & 138 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.1.0",
3+
"version": "18.0.1.2.0",
44
"author": "ADHOC SA",
55
"category": "Localization",
66
"depends": [
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
"""
3+
Migration 18.0.1.1.0 → 18.0.1.2.0
4+
====================================
5+
The fields ``tax_fixed_amount`` and ``tax_fixed_amount_in_currency`` on
6+
``account.move.line`` have been replaced by the new model
7+
``account.move.tax.override`` which persists override values on the move
8+
itself so they survive tax-line recomputations.
9+
10+
This pre-migration script reads any existing override values from the old
11+
columns and inserts them into the new table **before** the ORM updates the
12+
schema, ensuring no data is lost.
13+
"""
14+
import logging
15+
16+
_logger = logging.getLogger(__name__)
17+
18+
19+
def migrate(cr, version):
20+
if not version:
21+
return
22+
23+
# Check that the old columns still exist (they will during pre-migration)
24+
cr.execute(
25+
"""
26+
SELECT column_name
27+
FROM information_schema.columns
28+
WHERE table_name = 'account_move_line'
29+
AND column_name IN ('tax_fixed_amount', 'tax_fixed_amount_in_currency')
30+
"""
31+
)
32+
existing_cols = {row[0] for row in cr.fetchall()}
33+
if not existing_cols:
34+
_logger.info("Old tax_fixed_amount columns not found, skipping data migration.")
35+
return
36+
37+
# Ensure the destination table exists (it may not yet if the module was
38+
# never installed with the new code). We create it manually here; the ORM
39+
# will reconcile it during the update.
40+
cr.execute(
41+
"""
42+
CREATE TABLE IF NOT EXISTS account_move_tax_override (
43+
id SERIAL PRIMARY KEY,
44+
move_id INTEGER NOT NULL REFERENCES account_move(id) ON DELETE CASCADE,
45+
tax_id INTEGER NOT NULL REFERENCES account_tax(id) ON DELETE CASCADE,
46+
amount NUMERIC,
47+
amount_company_currency NUMERIC,
48+
create_uid INTEGER,
49+
create_date TIMESTAMP,
50+
write_uid INTEGER,
51+
write_date TIMESTAMP
52+
)
53+
"""
54+
)
55+
56+
# Migrate rows that have a non-zero override stored in the old columns.
57+
cr.execute(
58+
"""
59+
INSERT INTO account_move_tax_override
60+
(move_id, tax_id, amount, amount_company_currency,
61+
create_uid, create_date, write_uid, write_date)
62+
SELECT aml.move_id,
63+
aml.tax_line_id,
64+
COALESCE(aml.tax_fixed_amount_in_currency, 0),
65+
COALESCE(aml.tax_fixed_amount, 0),
66+
1, NOW(), 1, NOW()
67+
FROM account_move_line aml
68+
WHERE aml.tax_line_id IS NOT NULL
69+
AND (aml.tax_fixed_amount <> 0 OR aml.tax_fixed_amount_in_currency <> 0)
70+
ON CONFLICT DO NOTHING
71+
"""
72+
)
73+
74+
cr.execute("SELECT COUNT(*) FROM account_move_tax_override")
75+
count = cr.fetchone()[0]
76+
_logger.info("Migrated %d tax override record(s) to account_move_tax_override.", count)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from . import account_move_tax_override
12
from . import account_move
23
from . import account_move_line
34
from . import account_tax
Lines changed: 89 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,96 @@
1-
from odoo import models, fields, api
1+
from odoo import api, fields, models
22

33

44
class AccountMove(models.Model):
5-
65
_inherit = "account.move"
76

7+
tax_override_ids = fields.One2many(
8+
"account.move.tax.override",
9+
"move_id",
10+
string="Tax Overrides",
11+
copy=False,
12+
)
13+
14+
# ------------------------------------------------------------------
15+
# Tax-totals widget: inject override amounts so the widget reflects
16+
# the manually-set values instead of the recomputed ones.
17+
# ------------------------------------------------------------------
18+
819
def _compute_tax_totals(self):
920
for move in self:
10-
tax_context = {'inverse_invoice_currency_rate': move.inverse_invoice_currency_rate}
11-
for line in move.line_ids.filtered(lambda x: x.tax_fixed_amount_in_currency):
12-
#tax_context[line.tax_line_id.id] = line.tax_fixed_amount_in_currency
13-
tax_context[line.tax_line_id.id] = {
14-
'fixed_amount':line.tax_fixed_amount_in_currency,
15-
'rate': line.tax_fixed_amount / line.tax_fixed_amount_in_currency
16-
}
17-
super(AccountMove, move.with_context(tax_context=tax_context))._compute_tax_totals()
18-
19-
# def button_draft(self):
20-
# for move in self:
21-
# tax_context = {'inverse_invoice_currency_rate': move.inverse_invoice_currency_rate}
22-
# for line in move.line_ids.filtered(lambda x: x.tax_fixed_amount_in_currency):
23-
# tax_context[line.tax_line_id.id] = line.tax_fixed_amount_in_currency
24-
# super(AccountMove, move.with_context(tax_context=tax_context)).button_draft()
21+
# Only fixed-amount tax overrides affect the totals widget;
22+
# percentage taxes are always recomputed.
23+
overrides = {
24+
o.tax_id.id: o
25+
for o in move.tax_override_ids
26+
if o.tax_id.amount_type == "fixed"
27+
}
28+
if not overrides:
29+
super(AccountMove, move)._compute_tax_totals()
30+
continue
31+
32+
rate = getattr(move, "inverse_invoice_currency_rate", None) or 1.0
33+
is_company_currency = move.currency_id == move.company_currency_id
34+
tax_context = {
35+
"inverse_invoice_currency_rate": rate,
36+
"tax_context": {
37+
tax_id: {
38+
# If the invoice is in company currency, amount_company_currency
39+
# is not stored (always 0); use `amount` directly with rate=1.0.
40+
# If the invoice is in a foreign currency, use amount_company_currency
41+
# as fixed_amount and derive the rate from both fields.
42+
"fixed_amount": (
43+
override.amount
44+
if is_company_currency
45+
else override.amount_company_currency
46+
),
47+
"rate": (
48+
1.0
49+
if is_company_currency
50+
else (
51+
override.amount_company_currency / override.amount
52+
if override.amount
53+
else 1.0
54+
)
55+
),
56+
}
57+
for tax_id, override in overrides.items()
58+
},
59+
}
60+
super(AccountMove, move.with_context(**tax_context))._compute_tax_totals()
61+
62+
# ------------------------------------------------------------------
63+
# Re-apply overrides after every tax-line recomputation so that the
64+
# amounts set through the wizard survive any subsequent edits on the
65+
# invoice lines.
66+
# ------------------------------------------------------------------
67+
68+
def _recompute_tax_lines(
69+
self,
70+
recompute_tax_base_amount=False,
71+
tax_rep_lines_to_recompute=None,
72+
):
73+
"""Restore manually-set tax amounts after each recomputation."""
74+
super()._recompute_tax_lines(
75+
recompute_tax_base_amount=recompute_tax_base_amount,
76+
tax_rep_lines_to_recompute=tax_rep_lines_to_recompute,
77+
)
78+
for move in self:
79+
move._apply_tax_overrides()
80+
81+
def _apply_tax_overrides(self):
82+
"""Re-write values from ``tax_override_ids`` onto the matching tax lines.
83+
84+
Only overrides for fixed-amount taxes are applied; percentage-based
85+
taxes must always reflect their recomputed values.
86+
"""
87+
if not self.tax_override_ids:
88+
return
89+
overrides = {
90+
o.tax_id: o
91+
for o in self.tax_override_ids
92+
if o.tax_id.amount_type == "fixed"
93+
}
94+
for line in self.line_ids.filtered(lambda l: l.tax_line_id in overrides):
95+
override = overrides[line.tax_line_id]
96+
line.write(override._get_line_values(self))
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
from odoo import models, fields
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
from odoo import models
23

34

45
class AccountMoveLine(models.Model):
56
_inherit = "account.move.line"
6-
7-
tax_fixed_amount = fields.Float()
8-
tax_fixed_amount_in_currency = fields.Float()
7+
# tax_fixed_amount / tax_fixed_amount_in_currency were removed in favour of
8+
# the ``account.move.tax.override`` model, which persists override values
9+
# on the move itself so they survive tax-line recomputations.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
from odoo import fields, models
3+
4+
5+
class AccountMoveTaxOverride(models.Model):
6+
"""Stores manually-overridden tax amounts for a given invoice.
7+
8+
These values survive recomputations of ``account.move.line`` tax lines
9+
because they are kept on the move itself, not on the (volatile) journal
10+
item. After every recomputation the override amounts are re-applied to
11+
the corresponding tax line.
12+
"""
13+
14+
_name = "account.move.tax.override"
15+
_description = "Account Move Tax Override"
16+
17+
move_id = fields.Many2one(
18+
"account.move",
19+
required=True,
20+
ondelete="cascade",
21+
index=True,
22+
)
23+
tax_id = fields.Many2one(
24+
"account.tax",
25+
required=True,
26+
ondelete="cascade",
27+
)
28+
# Amount expressed in the invoice currency (amount_currency on the line)
29+
amount = fields.Float(string="Amount (invoice currency)", digits="Account")
30+
# Amount expressed in the company currency (debit/credit on the line)
31+
amount_company_currency = fields.Float(
32+
string="Amount (company currency)", digits="Account"
33+
)
34+
35+
def _get_line_values(self, move):
36+
"""Return a dict suitable for ``account.move.line.write()``."""
37+
self.ensure_one()
38+
amount = self.amount
39+
amount_cc = self.amount_company_currency
40+
41+
move_currency = move.currency_id
42+
company_currency = move.company_currency_id
43+
not_company_currency = move_currency and move_currency != company_currency
44+
45+
debit = credit = debit_cc = credit_cc = 0.0
46+
47+
if move.move_type in ("in_invoice", "in_receipt"):
48+
if amount > 0:
49+
debit, debit_cc = amount, amount_cc
50+
elif amount < 0:
51+
credit, credit_cc = -amount, -amount_cc
52+
else: # out_invoice, refunds, etc.
53+
if amount > 0:
54+
credit, credit_cc = amount, amount_cc
55+
elif amount < 0:
56+
debit, debit_cc = -amount, -amount_cc
57+
58+
values = {
59+
"debit": debit_cc if not_company_currency else debit,
60+
"credit": credit_cc if not_company_currency else credit,
61+
"balance": (
62+
(amount_cc if not_company_currency else amount) * (1 if debit or debit_cc else -1)
63+
),
64+
}
65+
if not_company_currency and amount:
66+
values["amount_currency"] = amount
67+
68+
return values
Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,42 @@
1-
from odoo import models, fields, api
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
from odoo import api, models
23

3-
class AccountTax(models.Model):
44

5+
class AccountTax(models.Model):
56
_inherit = "account.tax"
67

78
@api.model
89
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-
if 'tax_context' in self.env.context:
11-
rate = self.env.context.get('inverse_invoice_currency_rate', 1)
12-
for tax_groups in res['subtotals'][0]['tax_groups']:
13-
for involved_tax_id in tax_groups['involved_tax_ids']:
14-
amount = self.env.context['tax_context'].get(involved_tax_id, {}).get('fixed_amount', 0.0)
15-
rate = self.env.context['tax_context'].get(involved_tax_id, {}).get('rate', 1.0)
16-
original_amount = tax_groups.get('tax_amount', {})
17-
if amount and amount != original_amount:
18-
tax_groups.update({
19-
'tax_amount': amount,
20-
'tax_amount_currency': amount
21-
})
22-
res['tax_amount'] += amount - original_amount
23-
res['total_amount'] += amount - original_amount
24-
res['tax_amount_currency'] += (amount - original_amount) * rate
25-
res['total_amount_currency'] += (amount - original_amount) * rate
10+
res = super()._get_tax_totals_summary(
11+
base_lines, currency, company, cash_rounding=cash_rounding
12+
)
13+
# ``tax_context`` is injected by AccountMove._compute_tax_totals when
14+
# there are active tax overrides. Structure:
15+
# { tax_id (int): {'fixed_amount': float, 'rate': float}, ... }
16+
tax_context = self.env.context.get("tax_context")
17+
if not tax_context:
18+
return res
19+
20+
for tax_group in res.get("subtotals", [{}])[0].get("tax_groups", []):
21+
for involved_tax_id in tax_group.get("involved_tax_ids", []):
22+
override = tax_context.get(involved_tax_id)
23+
if not override:
24+
continue
25+
new_amount = override.get("fixed_amount", 0.0)
26+
rate = override.get("rate", 1.0)
27+
original_amount = tax_group.get("tax_amount", 0.0)
28+
if not new_amount or new_amount == original_amount:
29+
continue
30+
diff = new_amount - original_amount
31+
tax_group.update(
32+
{
33+
"tax_amount": new_amount,
34+
"tax_amount_currency": new_amount,
35+
}
36+
)
37+
res["tax_amount"] += diff
38+
res["total_amount"] += diff
39+
res["tax_amount_currency"] += diff * rate
40+
res["total_amount_currency"] += diff * rate
2641

2742
return res
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
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)