From a73f06af8a4a4915721842c95caa40670bc09b4f Mon Sep 17 00:00:00 2001 From: "Andrea Grazioso (agr-odoo)" Date: Wed, 21 Feb 2024 10:57:14 +0000 Subject: [PATCH] [FIX] account: global tax rounding error management Set Rounding Method to "Global rounding" Create a sale order having these line values: - Price Unit 54.45, tax 10% not included - Price Unit 600.00, tax 10% not included - Price Unit -500.00, tax 10% not included Tax total will be: Untaxed Amount 154.45 Taxes 15.44 Total 169.89 From the Sales Order create the Invoice Issue: Invoice tax totals will be Untaxed Amount 154.45 Taxes 15.45 Total 169.90 This occurs because when managing the rounding errors for global rounding the system 'force' a 1 cent difference on the negative line that do not need rounding Example: Line 1 - current diff: 0.0 - computed tax amount: 5.455 - tax amount + diff: 5.455, after rounding: 5.45 - stored diff for next line: -0.005 Line 2 - current diff: -0.005 - computed tax amount: 60.0 - tax amount + diff: 59.995, after rounding: 60.0 - stored diff for next line: -0.005 Line 3 - current diff: -0.005 - computed tax amount: -50.0 - tax amount + diff: -50.005, after rounding: -50.01 A solution is to not add the diff when the tax line is already rounded opw-3715229 closes odoo/odoo#156016 X-original-commit: bbca871841577203e44c98290ab0087ec94cf521 Signed-off-by: Laurent Smet (las) Signed-off-by: Andrea Grazioso (agr) Co-authored-by: smetl --- addons/account/models/account_tax.py | 58 ++++++++++++++----- .../account/tests/test_invoice_tax_totals.py | 14 +++++ 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/addons/account/models/account_tax.py b/addons/account/models/account_tax.py index 8416c0acc63..1bfaf59b9e8 100644 --- a/addons/account/models/account_tax.py +++ b/addons/account/models/account_tax.py @@ -1210,27 +1210,55 @@ class AccountTax(models.Model): tax_details['group_tax_details'].append(tax_values) if self.env.company.tax_calculation_rounding_method == 'round_globally': + # Aggregate all amounts according the tax lines grouping key. + comp_currency = self.env.company.currency_id amount_per_tax_repartition_line_id = defaultdict(lambda: { - 'delta_tax_amount': 0.0, - 'delta_tax_amount_currency': 0.0, + 'tax_amount': 0.0, + 'tax_amount_currency': 0.0, + 'tax_values_list': [], }) for base_line, to_update_vals, tax_values_list in to_process: - currency = base_line['currency'] or self.env.company.currency_id - comp_currency = self.env.company.currency_id + currency = base_line['currency'] or comp_currency for tax_values in tax_values_list: grouping_key = frozendict(self._get_generation_dict_from_base_line(base_line, tax_values)) - total_amounts = amount_per_tax_repartition_line_id[grouping_key] - tax_amount_currency_with_delta = tax_values['tax_amount_currency'] \ - + total_amounts['delta_tax_amount_currency'] - tax_amount_currency = currency.round(tax_amount_currency_with_delta) - tax_amount_with_delta = tax_values['tax_amount'] \ - + total_amounts['delta_tax_amount'] - tax_amount = comp_currency.round(tax_amount_with_delta) - tax_values['tax_amount_currency'] = tax_amount_currency - tax_values['tax_amount'] = tax_amount - total_amounts['delta_tax_amount_currency'] = tax_amount_currency_with_delta - tax_amount_currency - total_amounts['delta_tax_amount'] = tax_amount_with_delta - tax_amount + total_amounts['tax_amount_currency'] += tax_values['tax_amount_currency'] + total_amounts['tax_amount'] += tax_values['tax_amount'] + total_amounts['tax_values_list'].append(tax_values) + + # Round them like what the creation of tax lines would do. + for key, values in amount_per_tax_repartition_line_id.items(): + currency = self.env['res.currency'].browse(key['currency_id']) or comp_currency + values['tax_amount_rounded'] = comp_currency.round(values['tax_amount']) + values['tax_amount_currency_rounded'] = currency.round(values['tax_amount_currency']) + + # Dispatch the amount accross the tax values. + for key, values in amount_per_tax_repartition_line_id.items(): + foreign_currency = self.env['res.currency'].browse(key['currency_id']) or comp_currency + for currency, amount_field in ((comp_currency, 'tax_amount'), (foreign_currency, 'tax_amount_currency')): + raw_value = values[amount_field] + rounded_value = values[f'{amount_field}_rounded'] + diff = rounded_value - raw_value + abs_diff = abs(diff) + diff_sign = -1 if diff < 0 else 1 + tax_values_list = values['tax_values_list'] + nb_error = math.ceil(abs_diff / currency.rounding) + nb_cents_per_tax_values = math.floor(nb_error / len(tax_values_list)) + nb_extra_cent = nb_error % len(tax_values_list) + + for tax_values in tax_values_list: + if not abs_diff: + break + + nb_amount_curr_cent = nb_cents_per_tax_values + if nb_extra_cent: + nb_amount_curr_cent += 1 + nb_extra_cent -= 1 + + # We can have more than one cent to distribute on a single tax_values. + abs_delta_to_add = min(abs_diff, currency.rounding * nb_amount_curr_cent) + tax_values[amount_field] += diff_sign * abs_delta_to_add + abs_diff -= abs_delta_to_add grouping_key_generator = grouping_key_generator or default_grouping_key_generator diff --git a/addons/account/tests/test_invoice_tax_totals.py b/addons/account/tests/test_invoice_tax_totals.py index 004e96bd6c0..a2fcd1a4b52 100644 --- a/addons/account/tests/test_invoice_tax_totals.py +++ b/addons/account/tests/test_invoice_tax_totals.py @@ -35,6 +35,12 @@ class TestTaxTotals(AccountTestInvoicingCommon): 'sequence': 5 }) + cls.tax_10 = cls.env['account.tax'].create({ + 'name': "tax_10a", + 'amount_type': 'percent', + 'amount': 10.0, + }) + cls.tax_16 = cls.env['account.tax'].create({ 'name': "tax_16", 'amount_type': 'percent', @@ -733,6 +739,14 @@ class TestTaxTotals(AccountTestInvoicingCommon): run_case('round_per_line', lines, [16.60]) run_case('round_globally', lines, [16.59]) + lines = [ + (54.45, self.tax_10), + (600, self.tax_10), + (-500, self.tax_10), + ] + run_case('round_per_line', lines, [15.45]) + run_case('round_globally', lines, [15.45]) + def test_invoice_foreign_currency_tax_totals(self): self.env['res.currency.rate'].create({ 'name': '2018-01-01',