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:
+42
-28
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user