From eec0b80756557ecd543a50f25b8f63f94e23de7c Mon Sep 17 00:00:00 2001 From: Laurent Smet Date: Mon, 28 Oct 2019 09:15:52 +0000 Subject: [PATCH] [FIX] account: Fix computation of taxes with round_globally. -github issue: 14673 closes odoo/odoo#44312 -task: 2007300 X-original-commit: 6d3f3c749dbccc875f67c17ee95086df494044f6 Signed-off-by: Quentin De Paoli (qdp) --- addons/account/models/account.py | 38 ++++++-- addons/account/tests/test_tax.py | 145 +++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 7 deletions(-) diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 555abe1d12e..45f207f8bb4 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -1819,10 +1819,8 @@ class AccountTax(models.Model): # precision of the currency. # The context key 'round' allows to force the standard behavior. round_tax = False if company.tax_calculation_rounding_method == 'round_globally' else True - round_total = True if 'round' in self.env.context: round_tax = bool(self.env.context['round']) - round_total = bool(self.env.context['round']) if not round_tax: prec += 5 @@ -1850,7 +1848,33 @@ class AccountTax(models.Model): # (145 - 15) / (1.0 + 30%) * 90% = 130 / 1.3 * 90% = 90 return (base_amount - fixed_amount) / (1.0 + percent_amount / 100.0) * (100 - division_amount) / 100 - base = round(price_unit * quantity, prec) + # The first/last base must absolutely be rounded to work in round globally. + # Indeed, the sum of all taxes ('taxes' key in the result dictionary) must be strictly equals to + # 'price_included' - 'price_excluded' whatever the rounding method. + # + # Example using the global rounding without any decimals: + # Suppose two invoice lines: 27000 and 10920, both having a 19% price included tax. + # + # Line 1 Line 2 + # ----------------------------------------------------------------------- + # total_included: 27000 10920 + # tax: 27000 / 1.19 = 4310.924 10920 / 1.19 = 1743.529 + # total_excluded: 22689.076 9176.471 + # + # If the rounding of the total_excluded isn't made at the end, it could lead to some rounding issues + # when summing the tax amounts, e.g. on invoices. + # In that case: + # - amount_untaxed will be 22689 + 9176 = 31865 + # - amount_tax will be 4310.924 + 1743.529 = 6054.453 ~ 6054 + # - amount_total will be 31865 + 6054 = 37919 != 37920 = 27000 + 10920 + # + # By performing a rounding at the end to compute the price_excluded amount, the amount_tax will be strictly + # equals to 'price_included' - 'price_excluded' after rounding and then: + # Line 1: sum(taxes) = 27000 - 22689 = 4311 + # Line 2: sum(taxes) = 10920 - 2176 = 8744 + # amount_tax = 4311 + 8744 = 13055 + # amount_total = 31865 + 13055 = 37920 + base = currency.round(price_unit * quantity) # For the computation of move lines, we could have a negative base value. # In this case, compute all with positive values and negate them at the end. @@ -1898,7 +1922,7 @@ class AccountTax(models.Model): store_included_tax_total = False i -= 1 - total_excluded = recompute_base(base, incl_fixed_amount, incl_percent_amount, incl_division_amount) + total_excluded = currency.round(recompute_base(base, incl_fixed_amount, incl_percent_amount, incl_division_amount)) # 5) Iterate the taxes in the sequence order to compute missing tax amounts. # Start the computation of accumulated amounts at the total_excluded value. @@ -1983,9 +2007,9 @@ class AccountTax(models.Model): return { 'base_tags': taxes.mapped(is_refund and 'refund_repartition_line_ids' or 'invoice_repartition_line_ids').filtered(lambda x: x.repartition_type == 'base').mapped('tag_ids').ids, 'taxes': taxes_vals, - 'total_excluded': sign * (currency.round(total_excluded) if round_total else total_excluded), - 'total_included': sign * (currency.round(total_included) if round_total else total_included), - 'total_void': sign * (currency.round(total_void) if round_total else total_void), + 'total_excluded': sign * total_excluded, + 'total_included': sign * currency.round(total_included), + 'total_void': sign * currency.round(total_void), } @api.model diff --git a/addons/account/tests/test_tax.py b/addons/account/tests/test_tax.py index 47e645351bb..be857511d29 100644 --- a/addons/account/tests/test_tax.py +++ b/addons/account/tests/test_tax.py @@ -107,6 +107,13 @@ class TestTaxCommon(AccountTestUsersCommon): }), ], }) + + cls.tax_19_percent = cls.tax_model.create({ + 'name': "test_rounding_methods_1", + 'amount_type': 'percent', + 'amount': 19, + }) + cls.bank_journal = cls.env['account.journal'].search([('type', '=', 'bank'), ('company_id', '=', cls.account_manager.company_id.id)])[0] cls.bank_account = cls.bank_journal.default_debit_account_id cls.expense_account = cls.env['account.account'].search([('user_type_id.type', '=', 'payable')], limit=1) #Should be done by onchange later @@ -649,3 +656,141 @@ class TestTax(TestTaxCommon): ], tax.compute_all(-1.0) ) + + def test_rounding_tax_excluded_round_per_line(self): + ''' Test the rounding of a 19% price excluded tax in an invoice having 22689 and 9176 as lines. + The decimal precision is set to zero. + The computation must be similar to round(22689 * 0.19) + round(9176 * 0.19). + ''' + self.tax_19_percent.company_id.currency_id.rounding = 1.0 + self.tax_19_percent.company_id.tax_calculation_rounding_method = 'round_per_line' + + res1 = self.tax_19_percent.compute_all(22689) + self._check_compute_all_results( + 27000, # 'total_included' + 22689, # 'total_excluded' + [ + # base, amount + # --------------- + (22689, 4311), + # --------------- + ], + res1 + ) + + res2 = self.tax_19_percent.compute_all(9176) + self._check_compute_all_results( + 10919, # 'total_included' + 9176, # 'total_excluded' + [ + # base , amount + # --------------- + (9176, 1743), + # --------------- + ], + res2 + ) + + def test_rounding_tax_excluded_round_globally(self): + ''' Test the rounding of a 19% price excluded tax in an invoice having 22689 and 9176 as lines. + The decimal precision is set to zero. + The computation must be similar to round((22689 + 9176) * 0.19). + ''' + self.tax_19_percent.company_id.currency_id.rounding = 1.0 + self.tax_19_percent.company_id.tax_calculation_rounding_method = 'round_globally' + + res1 = self.tax_19_percent.compute_all(22689) + self._check_compute_all_results( + 27000, # 'total_included' + 22689, # 'total_excluded' + [ + # base, amount + # --------------- + (22689, 4310.91), + # --------------- + ], + res1 + ) + + res2 = self.tax_19_percent.compute_all(9176) + self._check_compute_all_results( + 10919, # 'total_included' + 9176, # 'total_excluded' + [ + # base , amount + # --------------- + (9176, 1743.44), + # --------------- + ], + res2 + ) + + def test_rounding_tax_included_round_per_line(self): + ''' Test the rounding of a 19% price included tax in an invoice having 27000 and 10920 as lines. + The decimal precision is set to zero. + The computation must be similar to round(27000 / 1.19) + round(10920 / 1.19). + ''' + self.tax_19_percent.price_include = True + self.tax_19_percent.company_id.currency_id.rounding = 1.0 + self.tax_19_percent.company_id.tax_calculation_rounding_method = 'round_per_line' + + res1 = self.tax_19_percent.compute_all(27000) + self._check_compute_all_results( + 27000, # 'total_included' + 22689, # 'total_excluded' + [ + # base , amount + # --------------- + (22689, 4311), + # --------------- + ], + res1 + ) + + res2 = self.tax_19_percent.compute_all(10920) + self._check_compute_all_results( + 10920, # 'total_included' + 9176, # 'total_excluded' + [ + # base , amount + # --------------- + (9176, 1744), + # --------------- + ], + res2 + ) + + def test_rounding_tax_included_round_globally(self): + ''' Test the rounding of a 19% price included tax in an invoice having 27000 and 10920 as lines. + The decimal precision is set to zero. + The computation must be similar to round((27000 + 10920) / 1.19). + ''' + self.tax_19_percent.price_include = True + self.tax_19_percent.company_id.currency_id.rounding = 1.0 + self.tax_19_percent.company_id.tax_calculation_rounding_method = 'round_globally' + + res1 = self.tax_19_percent.compute_all(27000) + self._check_compute_all_results( + 27000, # 'total_included' + 22689, # 'total_excluded' + [ + # base , amount + # --------------- + (22689, 4311), + # --------------- + ], + res1 + ) + + res2 = self.tax_19_percent.compute_all(10920) + self._check_compute_all_results( + 10920, # 'total_included' + 9176, # 'total_excluded' + [ + # base , amount + # --------------- + (9176, 1744), + # --------------- + ], + res2 + )