From 8a443222a116e8b9a94a4c8fc11798824abbc484 Mon Sep 17 00:00:00 2001 From: Laurent Smet Date: Mon, 15 Jan 2024 12:54:49 +0000 Subject: [PATCH] [FIX] account: Fix indian taxes computation. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For the indian case, when facing two percent price-included taxes having the same percentage, both need to produce the same tax amounts. task_id: 3410529 closes odoo/odoo#150971 X-original-commit: 235347db954495b8c0a1bf3bfd0cd408aec7ef0f Signed-off-by: de Wouters de Bouchout Jean-BenoƮt (jbw) Signed-off-by: Laurent Smet (las) --- addons/account/models/account_tax.py | 55 ++++++++----- addons/account/tests/test_tax.py | 17 +++- .../static/src/app/store/pos_store.js | 79 +++++++++++-------- 3 files changed, 97 insertions(+), 54 deletions(-) diff --git a/addons/account/models/account_tax.py b/addons/account/models/account_tax.py index 7423dc78d39..4e9397b7506 100644 --- a/addons/account/models/account_tax.py +++ b/addons/account/models/account_tax.py @@ -721,19 +721,29 @@ class AccountTax(models.Model): # || tax_3 | .. | | # || ... | .. | .. | # ---------------------------- - def recompute_base(base_amount, fixed_amount, percent_amount, division_amount): - # Recompute the new base amount based on included fixed/percent amounts and the current base amount. - # Example: - # tax | amount | type | price_include | - # ----------------------------------------------- - # tax_1 | 10% | percent | t - # tax_2 | 15 | fix | t - # tax_3 | 20% | percent | t - # tax_4 | 10% | division | t - # ----------------------------------------------- + def recompute_base(base_amount, incl_tax_amounts): + """ Recompute the new base amount based on included fixed/percent amounts and the current base amount. """ + fixed_amount = incl_tax_amounts['fixed_amount'] + division_amount = sum(tax_factor for _i, tax_factor in incl_tax_amounts['division_taxes']) + percent_amount = sum(tax_factor for _i, tax_factor in incl_tax_amounts['percent_taxes']) + + if company.country_code == 'IN': + # For the indian case, when facing two percent price-included taxes having the same percentage, + # both need to produce the same tax amounts. To do that, the tax amount of those taxes are computed + # directly during the first traveling in reversed order. + total_percentage = sum(tax_factor for _i, tax_factor in incl_tax_amounts['percent_taxes']) + for i, tax_factor in incl_tax_amounts['percent_taxes']: + tax_amount = float_round(base * tax_factor / (100 + total_percentage), precision_rounding=prec) + cached_tax_amounts[i] = tax_amount + fixed_amount += tax_amount + percent_amount = 0.0 + + incl_tax_amounts.update({ + 'percent_taxes': [], + 'division_taxes': [], + 'fixed_amount': 0.0, + }) - # if base_amount = 145, the new base is computed as: - # (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 # The first/last base must absolutely be rounded to work in round globally. @@ -780,7 +790,11 @@ class AccountTax(models.Model): i = len(taxes) - 1 store_included_tax_total = True # Keep track of the accumulated included fixed/percent amount. - incl_fixed_amount = incl_percent_amount = incl_division_amount = 0 + incl_tax_amounts = { + 'percent_taxes': [], + 'division_taxes': [], + 'fixed_amount': 0.0, + } # Store the tax amounts we compute while searching for the total_excluded cached_tax_amounts = {} if handle_price_include: @@ -793,20 +807,19 @@ class AccountTax(models.Model): sum_repartition_factor = sum(tax_repartition_lines.mapped("factor")) if tax.include_base_amount: - base = recompute_base(base, incl_fixed_amount, incl_percent_amount, incl_division_amount) - incl_fixed_amount = incl_percent_amount = incl_division_amount = 0 + base = recompute_base(base, incl_tax_amounts) store_included_tax_total = True if tax.price_include or self._context.get('force_price_include'): if tax.amount_type == 'percent': - incl_percent_amount += tax.amount * sum_repartition_factor + incl_tax_amounts['percent_taxes'].append((i, tax.amount * sum_repartition_factor)) elif tax.amount_type == 'division': - incl_division_amount += tax.amount * sum_repartition_factor + incl_tax_amounts['division_taxes'].append((i, tax.amount * sum_repartition_factor)) elif tax.amount_type == 'fixed': - incl_fixed_amount += abs(quantity) * tax.amount * sum_repartition_factor * abs(fixed_multiplicator) + incl_tax_amounts['fixed_amount'] = abs(quantity) * tax.amount * sum_repartition_factor * abs(fixed_multiplicator) else: # tax.amount_type == other (python) tax_amount = tax._compute_amount(base, sign * price_unit, quantity, product, partner, fixed_multiplicator) * sum_repartition_factor - incl_fixed_amount += tax_amount + incl_tax_amounts['fixed_amount'] += tax_amount # Avoid unecessary re-computation cached_tax_amounts[i] = tax_amount # In case of a zero tax, do not store the base amount since the tax amount will @@ -819,7 +832,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 = recompute_base(base, incl_tax_amounts) if self._context.get('round_base', True): total_excluded = currency.round(total_excluded) @@ -855,6 +868,8 @@ class AccountTax(models.Model): # We know the total to reach for that tax, so we make a substraction to avoid any rounding issues tax_amount = total_included_checkpoints[i] - (base + cumulated_tax_included_amount) cumulated_tax_included_amount = 0 + elif price_include and i in cached_tax_amounts: + tax_amount = cached_tax_amounts[i] else: tax_amount = tax.with_context(force_price_include=False)._compute_amount( tax_base_amount, sign * price_unit, quantity, product, partner, fixed_multiplicator) diff --git a/addons/account/tests/test_tax.py b/addons/account/tests/test_tax.py index 0b043c5efe7..76a621eb4c8 100644 --- a/addons/account/tests/test_tax.py +++ b/addons/account/tests/test_tax.py @@ -220,7 +220,7 @@ class TestTax(TestTaxCommon): res = self.group_tax_percent.with_context({'force_price_include':True}).compute_all(100.0) self._check_compute_all_results( 100, # 'total_included' - 83.33, # 'total_excluded' + 83.33, # 'total_excluded' [ # base , amount | seq | amount | incl | incl_base # --------------------------------------------------- @@ -231,6 +231,21 @@ class TestTax(TestTaxCommon): res ) + self.env.company.country_id = self.env.ref('base.in') + res = self.group_tax_percent.with_context({'force_price_include':True}).compute_all(100.0) + self._check_compute_all_results( + 100, # 'total_included' + 83.34, # 'total_excluded' + [ + # base , amount | seq | amount | incl | incl_base + # --------------------------------------------------- + (83.34, 8.33), # | 1 | 10% | | + (83.34, 8.33), # | 2 | 10% | | + # --------------------------------------------------- + ], + res + ) + def test_tax_percent_division(self): self.division_tax.price_include = True self.division_tax.include_base_amount = True diff --git a/addons/point_of_sale/static/src/app/store/pos_store.js b/addons/point_of_sale/static/src/app/store/pos_store.js index 959f4e296ec..a968cb7c4e8 100644 --- a/addons/point_of_sale/static/src/app/store/pos_store.js +++ b/addons/point_of_sale/static/src/app/store/pos_store.js @@ -1436,7 +1436,8 @@ export class PosStore extends Reactive { // 2) Deal with the rounding methods - var round_tax = this.company.tax_calculation_rounding_method != "round_globally"; + const company = this.company; + var round_tax = company.tax_calculation_rounding_method != 'round_globally'; var initial_currency_rounding = currency_rounding; if (!round_tax) { @@ -1444,13 +1445,34 @@ export class PosStore extends Reactive { } // 3) Iterate the taxes in the reversed sequence order to retrieve the initial base of the computation. - var recompute_base = function (base_amount, fixed_amount, percent_amount, division_amount) { - return ( - (((base_amount - fixed_amount) / (1.0 + percent_amount / 100.0)) * - (100 - division_amount)) / - 100 - ); - }; + var recompute_base = function(base_amount, incl_tax_amounts){ + let fixed_amount = incl_tax_amounts.fixed_amount; + let division_amount = 0.0; + for(const [, tax_factor] of incl_tax_amounts.division_taxes){ + division_amount += tax_factor; + } + let percent_amount = 0.0; + for(const [, tax_factor] of incl_tax_amounts.percent_taxes){ + percent_amount += tax_factor; + } + + if(company.country && company.country.code === "IN"){ + for(const [i, tax_factor] of incl_tax_amounts.percent_taxes){ + const tax_amount = round_pr(base_amount * tax_factor / (100 + percent_amount), currency_rounding); + cached_tax_amounts[i] = tax_amount; + fixed_amount += tax_amount; + } + percent_amount = 0.0; + } + + Object.assign(incl_tax_amounts, { + percent_taxes: [], + division_taxes: [], + fixed_amount: 0.0, + }); + + return (base_amount - fixed_amount) / (1.0 + percent_amount / 100.0) * (100 - division_amount) / 100; + } var base = round_pr(price_unit * quantity, initial_currency_rounding); @@ -1464,36 +1486,29 @@ export class PosStore extends Reactive { var i = taxes.length - 1; var store_included_tax_total = true; - var incl_fixed_amount = 0.0; - var incl_percent_amount = 0.0; - var incl_division_amount = 0.0; + const incl_tax_amounts = { + percent_taxes: [], + division_taxes: [], + fixed_amount: 0.0, + } var cached_tax_amounts = {}; if (handle_price_include) { taxes.reverse().forEach(function (tax) { if (tax.include_base_amount) { - base = recompute_base( - base, - incl_fixed_amount, - incl_percent_amount, - incl_division_amount - ); - incl_fixed_amount = 0.0; - incl_percent_amount = 0.0; - incl_division_amount = 0.0; + base = recompute_base(base, incl_tax_amounts); store_included_tax_total = true; } if (tax.price_include) { if (tax.amount_type === "percent") { - incl_percent_amount += tax.amount * tax.sum_repartition_factor; + incl_tax_amounts.percent_taxes.push([i, tax.amount * tax.sum_repartition_factor]); } else if (tax.amount_type === "division") { - incl_division_amount += tax.amount * tax.sum_repartition_factor; + incl_tax_amounts.division_taxes.push([i, tax.amount * tax.sum_repartition_factor]); } else if (tax.amount_type === "fixed") { - incl_fixed_amount += - Math.abs(quantity) * tax.amount * tax.sum_repartition_factor; + incl_tax_amounts.fixed_amount += Math.abs(quantity) * tax.amount * tax.sum_repartition_factor; } else { var tax_amount = self._compute_all(tax, base, quantity); - incl_fixed_amount += tax_amount; + incl_tax_amounts.fixed_amount += tax_amount; cached_tax_amounts[i] = tax_amount; } if (store_included_tax_total) { @@ -1505,10 +1520,7 @@ export class PosStore extends Reactive { }); } - var total_excluded = round_pr( - recompute_base(base, incl_fixed_amount, incl_percent_amount, incl_division_amount), - initial_currency_rounding - ); + var total_excluded = round_pr(recompute_base(base, incl_tax_amounts), initial_currency_rounding); var total_included = total_excluded; // 4) Iterate the taxes in the sequence order to fill missing base/amount values. @@ -1533,11 +1545,12 @@ export class PosStore extends Reactive { total_included_checkpoints[i] !== undefined && tax.sum_repartition_factor != 0 ) { - var tax_amount = - total_included_checkpoints[i] - (base + cumulated_tax_included_amount); + var tax_amount = total_included_checkpoints[i] - (base + cumulated_tax_included_amount); cumulated_tax_included_amount = 0; - } else { - tax_amount = self._compute_all(tax, tax_base_amount, quantity, true); + }else if(tax.price_include && cached_tax_amounts.hasOwnProperty(i)){ + var tax_amount = cached_tax_amounts[i]; + }else{ + var tax_amount = self._compute_all(tax, tax_base_amount, quantity, true); } tax_amount = round_pr(tax_amount, currency_rounding);