Skip to content
Closed
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
1 change: 1 addition & 0 deletions account_invoice_tax/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import wizards
from . import models
6 changes: 6 additions & 0 deletions account_invoice_tax/__manifest__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
{
"name": "Account Invoice Tax",
<<<<<<< 24dad28271a7be0f910c94eae5e492a680c86b6a
"version": "19.0.1.0.0",
||||||| fa5caa84911bca5b06138eb1bc5dc3d52a23458c
"version": "18.0.1.0.0",
=======
"version": "18.0.1.2.0",
>>>>>>> 9d1cd66e35c3227decb6be584534c1605bd1c831
"author": "ADHOC SA",
"category": "Localization",
"depends": [
Expand Down
43 changes: 43 additions & 0 deletions account_invoice_tax/migrations/18.0.1.2.0/post-migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import logging
from datetime import date

from dateutil.relativedelta import relativedelta
from odoo import SUPERUSER_ID, api

_logger = logging.getLogger(__name__)


def migrate(cr, version):
"""Populate tax_override_data for vendor bills that have fixed-amount tax
lines but whose override data was never stored.

Targets invoices of type in_invoice / in_refund / in_receipt created in
the last month that:
- have at least one tax line linked to a fixed-amount tax, and
- have tax_override_data IS NULL (never set).

Processes up to BATCH_LIMIT records.
"""
env = api.Environment(cr, SUPERUSER_ID, {})

month_start = date.today() - relativedelta(months=3)

moves = env["account.move"].search(
[
("move_type", "in", ["in_invoice", "in_refund", "in_receipt"]),
("invoice_date", ">=", month_start),
("tax_override_data", "=", False),
("line_ids.tax_line_id.amount_type", "=", "fixed"),
],
limit=1000,
order="id desc",
)

if not moves:
_logger.info("account_invoice_tax migration: no invoices to process.")
return

_logger.info("account_invoice_tax migration: processing %d invoice(s).", len(moves))
moves.sync_tax_override_from_tax_totals()

_logger.info("account_invoice_tax migration: done.")
2 changes: 2 additions & 0 deletions account_invoice_tax/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import account_move
from . import account_tax
145 changes: 145 additions & 0 deletions account_invoice_tax/models/account_move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
from contextlib import contextmanager

from odoo import fields, models


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

# Stores manually-overridden tax amounts keyed by tax id (as string).
# Structure: { "<tax_id>": {"amount": float, "rate": float} }
# Only fixed-amount taxes are stored here; percentage taxes are always
# recomputed automatically.
tax_override_data = fields.Json()

def sync_tax_override_from_tax_totals(self):
"""Rebuild ``tax_override_data`` from the current ``tax_totals`` widget value.

Iterates every tax group in ``tax_totals`` and, for each fixed-amount
tax found, stores the displayed amounts as an override so they survive
future recomputations.

``tax_totals`` structure (relevant fields)::

{
"subtotals": [{
"tax_groups": [{
"involved_tax_ids": [<tax_id>, ...],
"tax_amount": <float>, # company currency
}, ...]
}, ...]
}

``amount`` (invoice-currency) → ``tax_amount_currency`` when the
invoice is in a foreign currency, otherwise ``tax_amount``.
``amount_company_currency`` → ``tax_amount`` (always company currency),
or 0.0 when the invoice is in company currency (field unused in that
case, see ``_apply_tax_overrides``).
"""
for rec in self.filtered(lambda m: m.move_type in ("in_invoice", "in_refund", "in_receipt")):
tax_totals = rec.tax_totals
if not tax_totals:
continue

is_company_currency = rec.currency_id == rec.company_currency_id
new_overrides = {}

for subtotal in tax_totals.get("subtotals", []):
for tax_group in subtotal.get("tax_groups", []):
tax_amount = tax_group.get("tax_amount", 0.0)
tax_amount_currency = tax_group.get("tax_amount_currency", 0.0)

for tax_id in tax_group.get("involved_tax_ids", []):
tax = rec.env["account.tax"].browse(tax_id)
if not tax.exists() or tax.amount_type != "fixed":
continue

if is_company_currency:
amount = tax_amount
rate = 1
else:
amount = tax_amount_currency
rate = rec.invoice_currency_rate or 1.0

new_overrides[str(tax_id)] = {
"amount": amount,
"rate": rate,
}

rec.tax_override_data = new_overrides or False

# ------------------------------------------------------------------
# Tax-totals widget: inject override amounts so the widget reflects
# the manually-set values instead of the recomputed ones.
# ------------------------------------------------------------------

def _compute_tax_totals(self):
for move in self:
overrides = move.tax_override_data or {}
if not overrides:
super(AccountMove, move)._compute_tax_totals()
continue

tax_context = {
"tax_context": {
int(tax_id): {
"fixed_amount": (vals["amount"]),
"rate": (move.invoice_currency_rate or 1.0),
}
for tax_id, vals in overrides.items()
},
}
super(AccountMove, move.with_context(**tax_context))._compute_tax_totals()

# ------------------------------------------------------------------
# Re-apply overrides after every tax-line recomputation so that the
# amounts set through the wizard survive any subsequent edits on the
# invoice lines.
# ------------------------------------------------------------------

@contextmanager
def _sync_tax_lines(self, container):
"""Restore manually-set tax amounts after the core recomputes tax lines."""
with super()._sync_tax_lines(container):
yield
for move in container.get("records", self):
move._apply_tax_overrides()

def _apply_tax_overrides(self):
"""Re-write values from ``tax_override_data`` onto the matching tax lines.

Only overrides for fixed-amount taxes are applied; percentage-based
taxes must always reflect their recomputed values.
"""
overrides = self.tax_override_data or {}
if not overrides:
return

move_currency = self.currency_id
company_currency = self.company_currency_id
not_company_currency = move_currency and move_currency != company_currency
for line in self.line_ids.filtered(lambda l: l.tax_line_id and str(l.tax_line_id.id) in overrides):
vals = overrides[str(line.tax_line_id.id)]
rate = line.move_id.invoice_currency_rate or 1.0
amount = vals.get("amount", 0.0)
amount_cc = amount / rate
debit = credit = debit_cc = credit_cc = 0.0
if self.move_type in ("in_invoice", "in_receipt"):
if amount > 0:
debit, debit_cc = amount, amount_cc
elif amount < 0:
credit, credit_cc = -amount, -amount_cc
else:
if amount > 0:
credit, credit_cc = amount, amount_cc
elif amount < 0:
debit, debit_cc = -amount, -amount_cc

line_vals = {
"debit": debit_cc if not_company_currency else debit,
"credit": credit_cc if not_company_currency else credit,
"balance": ((amount_cc if not_company_currency else amount) * (1 if debit or debit_cc else -1)),
}
if not_company_currency and amount:
line_vals["amount_currency"] = amount
line.write(line_vals)
38 changes: 38 additions & 0 deletions account_invoice_tax/models/account_tax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from odoo import api, models


class AccountTax(models.Model):
_inherit = "account.tax"

@api.model
def _get_tax_totals_summary(self, base_lines, currency, company, cash_rounding=None):
res = super()._get_tax_totals_summary(base_lines, currency, company, cash_rounding=cash_rounding)
# ``tax_context`` is injected by AccountMove._compute_tax_totals when
# there are active tax overrides. Structure:
# { tax_id (int): {'fixed_amount': float, 'rate': float}, ... }
if tax_context := self.env.context.get("tax_context"):
for tax_group in res.get("subtotals", [{}])[0].get("tax_groups", []):
for involved_tax_id in tax_group.get("involved_tax_ids", []):
override = tax_context.get(involved_tax_id)
if not override:
continue
new_amount = override.get("fixed_amount", 0.0)
rate = override.get("rate", 1.0)
original_amount = tax_group.get("tax_amount", 0.0)
original_currency_amount = tax_group.get("tax_amount_currency", 0.0)
if not new_amount or new_amount == original_currency_amount:
continue
diff = new_amount - original_currency_amount
currency_diff = new_amount / rate - original_amount
tax_group.update(
{
"tax_amount": new_amount / rate,
"tax_amount_currency": new_amount,
}
)
res["tax_amount"] += diff
res["total_amount"] += diff
res["tax_amount_currency"] += currency_diff
res["total_amount_currency"] += currency_diff

return res
68 changes: 39 additions & 29 deletions account_invoice_tax/wizards/account_invoice_tax.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command, _, api, fields, models
from odoo import Command, api, fields, models
from odoo.exceptions import UserError


Expand Down Expand Up @@ -34,24 +34,14 @@ def default_get(self, fields):

def action_update_tax(self):
move = self.move_id
fixed_taxes_bu = {
line.tax_line_id: {
"amount_currency": line.amount_currency,
"debit": line.debit,
"credit": line.credit,
}
for line in self.move_id.line_ids.filtered(
lambda x: x.tax_repartition_line_id.tax_id.amount_type == "fixed"
)
}

active_tax = self.tax_line_ids.mapped("tax_id")
origin_tax = self.move_id.line_ids.filtered(lambda x: x.tax_line_id).mapped("tax_repartition_line_id.tax_id")
to_remove_tax = origin_tax - active_tax
to_add_tax = active_tax - origin_tax
container = {"records": move, "self": move}

# change tax list
# --- 1. Update tax list on invoice lines ---
with move.with_context(check_move_validity=False)._check_balanced(container):
with move._sync_dynamic_lines(container):
if to_remove_tax:
Expand All @@ -63,27 +53,16 @@ def action_update_tax(self):
{"tax_ids": [Command.link(tax_id.id) for tax_id in to_add_tax]}
)

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

if move.move_type == "in_invoice":
sign = 1
else: # For refund
sign = -1
# --- 3. Apply overrides to the current tax lines ---
container = {"records": move}
with move._check_balanced(container):
with move._sync_dynamic_lines(container):
# restauramos todos los valores de impuestos fixed que se habrian recomputado
# restaured = []
for tax_line in move.line_ids.filtered(
lambda x: x.tax_repartition_line_id.tax_id in fixed_taxes_bu
and x.tax_repartition_line_id.tax_id.amount_type == "fixed"
):
tax_line.write(fixed_taxes_bu.get(tax_line.tax_line_id))
for tax_line_id in self.tax_line_ids:
# seteamos valor al impuesto segun lo que puso en el wizard
line_with_tax = move.line_ids.filtered(lambda x: x.tax_line_id == tax_line_id.tax_id)
line_with_tax.write({"amount_currency": tax_line_id.amount * sign})
move._apply_tax_overrides()

<<<<<<< 24dad28271a7be0f910c94eae5e492a680c86b6a
def add_tax_and_new(self):
self.add_tax()
return {
Expand All @@ -94,6 +73,37 @@ def add_tax_and_new(self):
"view_mode": "form",
"context": self.env.context,
}
||||||| fa5caa84911bca5b06138eb1bc5dc3d52a23458c
def add_tax_and_new(self):
self.add_tax()
return {
"type": "ir.actions.act_window",
"name": _("Edit tax lines"),
"res_model": self._name,
"target": "new",
"view_mode": "form",
"context": self._context,
}
=======
def _save_overrides(self):
"""Write wizard line amounts into ``tax_override_data`` on the move.

Only fixed-amount taxes are persisted as overrides. Percentage-based
taxes are always recomputed automatically, so any stale entry for them
is removed.
"""
new_overrides = {}
move = self.move_id
for wizard_line in self.tax_line_ids.filtered(lambda l: l.tax_id.amount_type == "fixed"):
new_overrides[str(wizard_line.tax_id.id)] = {
"amount": wizard_line.amount,
"rate": self.move_id.invoice_currency_rate or 1.0,
}

# Previous overrides are fully replaced – entries not present in
# new_overrides (removed taxes or percentage-based taxes) are dropped.
move.tax_override_data = new_overrides or False
>>>>>>> 9d1cd66e35c3227decb6be584534c1605bd1c831

@api.constrains("tax_line_ids")
@api.onchange("tax_line_ids")
Expand Down
Loading