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$')