diff --git a/modules/account/CHANGELOG b/modules/account/CHANGELOG index 5c03eea08f..17a9c7a22e 100644 --- a/modules/account/CHANGELOG +++ b/modules/account/CHANGELOG @@ -1,3 +1,4 @@ +* Accrue and allocate rounding errors when there are multiple taxes * Use match pattern to find journal * Display general ledger information on account form diff --git a/modules/account/tax.py b/modules/account/tax.py index 888417c9ad..29b6898995 100644 --- a/modules/account/tax.py +++ b/modules/account/tax.py @@ -3,7 +3,7 @@ import datetime from collections import namedtuple from decimal import Decimal -from itertools import groupby +from itertools import cycle, groupby from sql import Literal from sql.aggregate import Sum @@ -1248,8 +1248,23 @@ class TaxableMixin(object): def _round_taxes(self, taxes): if not self.currency: return + + remainder = 0 for taxline in taxes.values(): - taxline['amount'] = self.currency.round(taxline['amount']) + rounded_amount = self.currency.round(taxline['amount']) + remainder += rounded_amount - taxline['amount'] + taxline['amount'] = rounded_amount + + # We need to compensate the rounding we did + remainder = self.currency.round(remainder, opposite=True) + if abs(remainder) >= self.currency.rounding: + offset_amount = self.currency.rounding.copy_sign(remainder) + + for tax in cycle(taxes.values()): + tax['amount'] -= offset_amount + remainder -= offset_amount + if abs(remainder) < self.currency.rounding: + break @fields.depends('company', methods=['_get_tax_context', '_round_taxes']) def _get_taxes(self): @@ -1267,6 +1282,7 @@ class TaxableMixin(object): 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 @@ -1278,8 +1294,9 @@ class TaxableMixin(object): else: taxes[taxline]['base'] += taxline['base'] taxes[taxline]['amount'] += taxline['amount'] + current_taxes[taxline] = taxes[taxline] if tax_rounding == 'line': - self._round_taxes(taxes) + self._round_taxes(current_taxes) if tax_rounding == 'document': self._round_taxes(taxes) return taxes diff --git a/modules/account/tests/test_module.py b/modules/account/tests/test_module.py index 4236db2463..ad8a645dd1 100644 --- a/modules/account/tests/test_module.py +++ b/modules/account/tests/test_module.py @@ -1153,6 +1153,108 @@ class AccountTestCase( self.assertEqual(tax['base'], Decimal('100.00')) self.assertEqual(tax['amount'], Decimal('20.02')) + @with_transaction() + def test_taxable_mixin_tax_residual_rounding(self): + "Test TaxableMixin for rounding with residual amount" + 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 = 'line' + config.save() + + tax_account, = Account.search([ + ('name', '=', 'Main Tax'), + ]) + + tax1 = Tax() + tax1.name = tax1.description = "Tax 1" + tax1.type = 'percentage' + tax1.rate = Decimal('.1') + tax1.invoice_account = tax_account + tax1.credit_note_account = tax_account + tax1.save() + tax2 = Tax() + tax2.name = tax2.description = "Tax 2" + tax2.type = 'percentage' + tax2.rate = Decimal('.1') + tax2.invoice_account = tax_account + tax2.credit_note_account = tax_account + tax2.save() + + taxable = self.Taxable( + currency=currency, + taxable_lines=[ + ([tax1, tax2], Decimal('1.0417'), 1, None), + ], + company=company) + + taxline_1, taxline_2 = taxable._get_taxes() + self.assertEqual(taxline_1['base'], Decimal('1.04')) + self.assertEqual(taxline_1['amount'], Decimal('.11')) + self.assertEqual(taxline_2['base'], Decimal('1.04')) + self.assertEqual(taxline_2['amount'], Decimal('.10')) + + @with_transaction() + def test_taxable_mixin_tax_residual_rounding_negative_residual(self): + "Test TaxableMixin for rounding with negative residual amount" + 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 = 'line' + config.save() + + tax_account, = Account.search([ + ('name', '=', 'Main Tax'), + ]) + + tax1 = Tax() + tax1.name = tax1.description = "Tax 1" + tax1.type = 'percentage' + tax1.rate = Decimal('.1') + tax1.invoice_account = tax_account + tax1.credit_note_account = tax_account + tax1.save() + tax2 = Tax() + tax2.name = tax2.description = "Tax 2" + tax2.type = 'percentage' + tax2.rate = Decimal('.1') + tax2.invoice_account = tax_account + tax2.credit_note_account = tax_account + tax2.save() + + # -2.95 is the unit price of -3.54 with 20% tax included + taxable = self.Taxable( + currency=currency, + taxable_lines=[ + ([tax1], Decimal('30.00'), 1, None), + ([tax1, tax2], Decimal('-2.95'), 1, None), + ], + company=company) + + taxline_1, taxline_2, taxline_3 = taxable._get_taxes() + self.assertEqual(taxline_1['base'], Decimal('30.00')) + self.assertEqual(taxline_1['amount'], Decimal('3.00')) + self.assertEqual(taxline_2['base'], Decimal('-2.95')) + self.assertEqual(taxline_2['amount'], Decimal('-0.29')) + self.assertEqual(taxline_3['base'], Decimal('-2.95')) + self.assertEqual(taxline_3['amount'], Decimal('-0.30')) + @with_transaction() def test_tax_compute_with_children_update_unit_price(self): "Test tax compute with children taxes modifying unit_price"