From bbad356f135bc8f5a59577ac15ca6b6316291db4 Mon Sep 17 00:00:00 2001 From: William Braeckman Date: Wed, 27 Apr 2022 14:21:29 +0000 Subject: [PATCH] [FIX] sale_loyalty,pos_loyalty: fix discountable on specific products Prior to this commit the discountable amount on specific products was not computed correctly when using fixed price discounts as it did not take the taxes into account. They will now be taken into account and result in the correct discount. TaskId-2834863 closes odoo/odoo#91202 X-original-commit: 669f4cca94af0c022318f6cb508c1c197aae0846 Signed-off-by: Yannick Tivisse (yti) Signed-off-by: William Braeckman (wbr) --- addons/pos_loyalty/static/src/js/Loyalty.js | 16 ++- addons/sale_loyalty/models/sale_order.py | 15 ++- .../tests/test_program_numbers.py | 100 ++++++++++++++++++ 3 files changed, 122 insertions(+), 9 deletions(-) diff --git a/addons/pos_loyalty/static/src/js/Loyalty.js b/addons/pos_loyalty/static/src/js/Loyalty.js index 6ed8ee893b6..258b364b88c 100644 --- a/addons/pos_loyalty/static/src/js/Loyalty.js +++ b/addons/pos_loyalty/static/src/js/Loyalty.js @@ -976,14 +976,18 @@ const PosLoyaltyOrder = (Order) => class PosLoyaltyOrder extends Order { } } else { const nonCommonLines = discountedLines.filter((line) => !linesToDiscount.includes(line)); - let discountedAmount = Math.abs(lines.reduce((sum, line) => sum + line.get_price_with_tax(), 0)); + const discountedAmounts = lines.reduce((map, line) => { + map[line.get_taxes().map((t) => t.id)]; + return map; + }, {}); const process = (line) => { - if (!discountedAmount || line.reward_id) { + const key = line.get_taxes().map((t) => t.id); + if (!discountedAmounts[key] || line.reward_id) { return; } const remaining = remainingAmountPerLine[line.cid]; - const consumed = Math.min(remaining, discountedAmount); - discountedAmount -= consumed; + const consumed = Math.min(remaining, discountedAmounts[key]); + discountedAmounts[key] -= consumed; remainingAmountPerLine[line.cid] -= consumed; } nonCommonLines.forEach(process); @@ -1071,6 +1075,10 @@ const PosLoyaltyOrder = (Order) => class PosLoyaltyOrder extends Order { } const discountFactor = discountable ? Math.min(1, (maxDiscount / discountable)) : 1; const result = Object.entries(discountablePerTax).reduce((lst, entry) => { + // Ignore 0 price lines + if (!entry[1]) { + return; + } const taxIds = entry[0] === '' ? [] : entry[0].split(',').map((str) => parseInt(str)); lst.push({ product: discountProduct, diff --git a/addons/sale_loyalty/models/sale_order.py b/addons/sale_loyalty/models/sale_order.py index 1b46b4b6d52..6acb431ca4d 100644 --- a/addons/sale_loyalty/models/sale_order.py +++ b/addons/sale_loyalty/models/sale_order.py @@ -241,16 +241,21 @@ class SaleOrder(models.Model): common_lines = discounted_lines & lines_to_discount if line_reward.discount_mode == 'percent': for line in discounted_lines: - remaining_amount_per_line[line] *= (1 - line_reward.discount/100) + if line_reward.discount_applicability == 'cheapest': + remaining_amount_per_line[line] *= (1 - line_reward.discount / 100 / line.product_uom_qty) + else: + remaining_amount_per_line[line] *= (1 - line_reward.discount / 100) else: non_common_lines = discounted_lines - lines_to_discount - discounted_amount = abs(sum(lines.mapped('price_total'))) + # Fixed prices are per tax + discounted_amounts = {line.tax_id: abs(line.price_total) for line in lines} for line in itertools.chain(non_common_lines, common_lines): + discounted_amount = discounted_amounts[line.tax_id] if discounted_amount == 0: - break + continue remaining = remaining_amount_per_line[line] consumed = min(remaining, discounted_amount) - discounted_amount -= consumed + discounted_amounts[line.tax_id] -= consumed remaining_amount_per_line[line] -= consumed discountable = 0 @@ -331,7 +336,7 @@ class SaleOrder(models.Model): 'points_cost': 0, 'reward_identifier_code': reward_code, 'tax_id': [(Command.CLEAR, 0, 0)] + [(Command.LINK, tax.id, False) for tax in mapped_taxes[tax]] - } for tax, price in discountable_per_tax.items()} + } for tax, price in discountable_per_tax.items() if price} # We only assign the point cost to one line to avoid counting the cost multiple times if reward_dict: reward_dict[next(iter(reward_dict))]['points_cost'] = point_cost diff --git a/addons/sale_loyalty/tests/test_program_numbers.py b/addons/sale_loyalty/tests/test_program_numbers.py index c5c6f66fbd3..67fccd3cf95 100644 --- a/addons/sale_loyalty/tests/test_program_numbers.py +++ b/addons/sale_loyalty/tests/test_program_numbers.py @@ -1461,3 +1461,103 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon): self.assertEqual(order.amount_total, 18, "The total should be 9$.") self._auto_rewards(order, programs) self.assertEqual(order.amount_total, 9, "The total should be 9$.") + + def test_specific_discount_multiple_taxes(self): + # Check the following setup + # Product A 10$ 10% tva excl + # Product B 10$ 20% tva excl + # Program A -100% on product A + # Program B -5$ fixed on both products + # Applying both programs in a different order should result in a different + # outcome since discountable amounts are computed per tax + # Applying program A before B should yield a better final price + tax_10pc_excl, tax_20pc_excl = self.env['account.tax'].create([ + { + 'name': "10% Tax excl", + 'amount_type': 'percent', + 'amount': 10, + 'type_tax_use': 'sale', + }, + { + 'name': "20% Tax excl", + 'amount_type': 'percent', + 'amount': 20, + 'type_tax_use': 'sale', + }, + ]) + product_a, product_b = self.env['product.product'].create([ + { + 'name': 'Product A', + 'list_price': 10, + 'sale_ok': True, + 'taxes_id': [(6, 0, [tax_10pc_excl.id])], + }, + { + 'name': 'Product B', + 'list_price': 10, + 'sale_ok': True, + 'taxes_id': [(6, 0, [tax_20pc_excl.id])], + }, + ]) + program_a, program_b = self.env['loyalty.program'].create([ + { + 'name': '-100% on A', + 'trigger': 'auto', + 'program_type': 'promotion', + 'applies_on': 'current', + 'rule_ids': [(0, 0, { + })], + 'reward_ids': [(0, 0, { + 'reward_type': 'discount', + 'discount': 100, + 'discount_mode': 'percent', + 'discount_applicability': 'specific', + 'discount_product_ids': product_a, + 'required_points': 1, + })], + }, + { + 'name': '-5 USD on [A, B]', + 'trigger': 'auto', + 'program_type': 'promotion', + 'applies_on': 'current', + 'rule_ids': [(0, 0, { + })], + 'reward_ids': [(0, 0, { + 'reward_type': 'discount', + 'discount': 5, + 'discount_mode': 'per_point', + 'discount_applicability': 'specific', + 'discount_product_ids': product_a | product_b, + 'required_points': 1, + })], + }, + ]) + + order = self.empty_order + self.env['sale.order.line'].create([ + { + 'product_id': product_a.id, + 'name': 'Product A', + 'product_uom_qty': 1, + 'order_id': order.id, + }, + { + 'product_id': product_b.id, + 'name': 'Product B', + 'product_uom_qty': 1, + 'order_id': order.id, + }, + ]) + self._auto_rewards(order, program_a) + self.assertEqual(order.amount_total, 12, 'Total should be 12$') + self._auto_rewards(order, program_b) + self.assertAlmostEqual(order.amount_total, 7, 0, 'Total should be 7$') + # Now the order way around + order.order_line.filtered('reward_id').unlink() + self._auto_rewards(order, program_b) + self.assertAlmostEqual(order.amount_total, 18, 0, 'Total should be 18$') + self._auto_rewards(order, program_a) + # We essentially create a discount of -100% off of an already discounted product + # (11 - 2.4) = 8.6$ discount ~ + self.assertAlmostEqual(order.amount_total, 9.4, 1, 'Total should be 9.4$')