Round together only taxable lines with the same taxes and sign

The rounding error must not be distributed between taxable lines having
different taxes otherwise the tax amount is not stable for the same base
amount.

Closes #13673
This commit is contained in:
Cédric Krier
2024-11-12 17:39:42 +01:00
parent 44b55e28f4
commit e9104be1ee
2 changed files with 86 additions and 28 deletions
+42 -28
View File
@@ -1,7 +1,7 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import datetime
from collections import namedtuple
from collections import defaultdict, namedtuple
from decimal import Decimal
from itertools import cycle, groupby
@@ -1192,8 +1192,13 @@ class _TaxKey(dict):
return hash(self._key())
_TaxableLine = namedtuple(
'_TaxableLine', ('taxes', 'unit_price', 'quantity', 'tax_date'))
class _TaxableLine(namedtuple(
'_TaxableLine',
('taxes', 'unit_price', 'quantity', 'tax_date'))):
@property
def _key(self):
base = self.unit_price * Decimal(str(self.quantity or 0))
return (tuple(self.taxes), base >= 0)
class TaxableMixin(object):
@@ -1266,34 +1271,43 @@ class TaxableMixin(object):
Tax = pool.get('account.tax')
Configuration = pool.get('account.configuration')
taxes = {}
all_taxes = {}
with Transaction().set_context(self._get_tax_context()):
config = Configuration(1)
tax_rounding = config.get_multivalue('tax_rounding')
taxable_lines = [_TaxableLine(*params)
for params in self.taxable_lines]
for line in taxable_lines:
assert all(t.company == self.company for t in line.taxes)
l_taxes = Tax.compute(Tax.browse(line.taxes), line.unit_price,
line.quantity, line.tax_date or self.tax_date)
current_taxes = {}
for tax in l_taxes:
taxline = self._compute_tax_line(**tax)
# Base must always be rounded per line as there will be one
# tax line per taxable_lines
if self.currency:
taxline['base'] = self.currency.round(taxline['base'])
if taxline not in taxes:
taxes[taxline] = taxline
else:
taxes[taxline]['base'] += taxline['base']
taxes[taxline]['amount'] += taxline['amount']
current_taxes[taxline] = taxes[taxline]
if tax_rounding == 'line':
self._round_taxes(current_taxes)
if tax_rounding == 'document':
self._round_taxes(taxes)
return taxes
taxable_lines = defaultdict(list)
for params in self.taxable_lines:
taxable_line = _TaxableLine(*params)
taxable_lines[taxable_line._key].append(taxable_line)
for grouped_taxable_lines in taxable_lines.values():
taxes = {}
for line in grouped_taxable_lines:
assert all(t.company == self.company for t in line.taxes)
l_taxes = Tax.compute(
Tax.browse(line.taxes), line.unit_price,
line.quantity, line.tax_date or self.tax_date)
current_taxes = {}
for tax in l_taxes:
taxline = self._compute_tax_line(**tax)
# Base must always be rounded per line as there will be
# one tax line per taxable_lines
if self.currency:
taxline['base'] = self.currency.round(
taxline['base'])
if taxline not in taxes:
taxes[taxline] = taxline
else:
taxes[taxline]['base'] += taxline['base']
taxes[taxline]['amount'] += taxline['amount']
current_taxes[taxline] = taxes[taxline]
if tax_rounding == 'line':
self._round_taxes(current_taxes)
if tax_rounding == 'document':
self._round_taxes(taxes)
if all_taxes.keys() & taxes.keys():
raise ValueError("Duplicate tax line keys")
all_taxes.update(taxes)
return all_taxes
class TaxLine(ModelSQL, ModelView):
+44
View File
@@ -1457,6 +1457,50 @@ class AccountTestCase(
self.assertEqual(taxline_3['base'], Decimal('-2.95'))
self.assertEqual(taxline_3['amount'], Decimal('-0.30'))
@with_transaction()
def test_taxable_mixin_tax_residual_rounding_per_same_taxes(self):
"""Test TaxableMixin for rounding with residual amount only for same
taxes"""
pool = Pool()
Account = pool.get('account.account')
Tax = pool.get('account.tax')
Configuration = pool.get('account.configuration')
currency = create_currency('cur')
company = create_company()
with set_company(company):
create_chart(company)
config = Configuration(1)
config.tax_rounding = 'document'
config.save()
tax_account, = Account.search([
('code', '=', '6.3.6'),
])
tax = Tax()
tax.name = tax.description = "Tax"
tax.type = 'percentage'
tax.rate = Decimal('.06')
tax.invoice_account = tax_account
tax.credit_note_account = tax_account
tax.save()
taxable = self.Taxable(
currency=currency,
taxable_lines=[
([tax], Decimal('11.2736'), 1.04, None),
([tax], Decimal('0.4529'), -1, None),
],
company=company)
taxline_1, taxline_2 = taxable._get_taxes()
self.assertEqual(taxline_1['base'], Decimal('11.72'))
self.assertEqual(taxline_1['amount'], Decimal('0.7'))
self.assertEqual(taxline_2['base'], Decimal('-0.45'))
self.assertEqual(taxline_2['amount'], Decimal('-0.03'))
@with_transaction()
def test_tax_compute_with_children_update_unit_price(self):
"Test tax compute with children taxes modifying unit_price"