From 64937078cd981698a6fb99ed9b6f53fc9385441b Mon Sep 17 00:00:00 2001 From: Demesmaeker Date: Thu, 25 May 2023 14:42:54 +0000 Subject: [PATCH] [FIX] sale_loyalty: wrong discount amount computation with negative amount product Currently, when using a promotion with a reward available only on specific products and having a maximum amount allowed, when adding another product with a negative amount, the discount could go above the maximum discount allowed. This was caused by the calculated discount amount that was overridden by the total amount of the order. This new amount was then used to calculate the discounted factor. So, instead of having: discount_amount = product_price * min(1, (max_discount / discountable)) When the total_amount of the order was under this discountable, we had: discount_amount = product_price * min(1, (max_discount / total_amount)) Instead, we should calculate the discount amount normally but limit it to the amount_total. opw-3217369 closes odoo/odoo#123361 X-original-commit: 977f726b67f48d4b858b1a5fe1d558a64186d332 Signed-off-by: Victor Feyens (vfe) Signed-off-by: Morgane Demesmaeker --- addons/sale_loyalty/models/sale_order.py | 4 +- addons/sale_loyalty/tests/test_loyalty.py | 67 +++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/addons/sale_loyalty/models/sale_order.py b/addons/sale_loyalty/models/sale_order.py index 53ffaf84908..366c0a69911 100644 --- a/addons/sale_loyalty/models/sale_order.py +++ b/addons/sale_loyalty/models/sale_order.py @@ -301,8 +301,6 @@ class SaleOrder(models.Model): discountable, discountable_per_tax = self._discountable_specific(reward) elif reward_applies_on == 'cheapest': discountable, discountable_per_tax = self._discountable_cheapest(reward) - # Discountable should never surpass the order's current total amount - discountable = min(self.amount_total, discountable) if not discountable: if not reward.program_id.is_payment_program and any(line.reward_id.program_id.is_payment_program for line in self.order_line): return [{ @@ -320,6 +318,8 @@ class SaleOrder(models.Model): }] raise UserError(_('There is nothing to discount')) max_discount = reward.currency_id._convert(reward.discount_max_amount, self.currency_id, self.company_id, fields.Date.today()) or float('inf') + # discount should never surpass the order's current total amount + max_discount = min(self.amount_total, max_discount) if reward.discount_mode == 'per_point': max_discount = min(max_discount, reward.currency_id._convert(reward.discount * self._get_real_points_for_coupon(coupon), diff --git a/addons/sale_loyalty/tests/test_loyalty.py b/addons/sale_loyalty/tests/test_loyalty.py index a7a9d708a98..a93cca75308 100644 --- a/addons/sale_loyalty/tests/test_loyalty.py +++ b/addons/sale_loyalty/tests/test_loyalty.py @@ -222,6 +222,73 @@ class TestLoyalty(TestSaleCouponCommon): self.assertEqual(order.amount_tax, 15.0) self.assertEqual(order.reward_amount, -215.0) + def test_discount_max_amount_on_specific_product(self): + product_a = self.product_A + product_b = self.product_B + product_a.write({'taxes_id': [Command.set(self.tax_20pc_excl.ids)]}) + product_b.write({'list_price': -20, 'taxes_id': [Command.set(self.tax_20pc_excl.ids)]}) + + self.env['loyalty.program'].search([]).write({'active': False}) + promotion = self.env['loyalty.program'].create({ + 'name': '10% Discount', + 'program_type': 'promotion', + 'trigger': 'auto', + 'rule_ids': [Command.create({'reward_point_amount': 1, 'reward_point_mode': 'unit'})], + 'reward_ids': [Command.create({ + 'discount': 10.0, + 'discount_max_amount': 9, + 'discount_applicability': 'specific', + 'discount_product_ids': [product_a.id], + })], + }) + + order = self.env['sale.order'].create({ + 'partner_id': self.partner_a.id, + 'order_line': [Command.create({'product_id': product_a.id})], + }) + self.assertEqual(order.reward_amount, 0) + + self._auto_rewards(order, promotion) + reward_amount_tax_included = sum(l.price_total for l in order.order_line if l.reward_id) + msg = "Max discount amount reached, the reward amount should be the max amount value." + self.assertEqual(reward_amount_tax_included, -9, msg) + + order.order_line = [Command.clear(), Command.create({'product_id': product_b.id})] + self._auto_rewards(order, promotion) + reward_amount_tax_included = sum(l.price_total for l in order.order_line if l.reward_id) + msg = "This product is not eligible to the discount." + self.assertEqual(reward_amount_tax_included, 0, msg=msg) + + order.order_line = [ + Command.clear(), + Command.create({'product_id': product_a.id}), # price_total = 120 + Command.create({'product_id': product_b.id}), # price_total = -20 + ] + self._auto_rewards(order, promotion) + reward_amount_tax_included = sum(l.price_total for l in order.order_line if l.reward_id) + msg = "Reward amount above the max amount, the reward should be the max amount value." + self.assertEqual(reward_amount_tax_included, -9, msg) + + order.order_line = [ + Command.clear(), + Command.create({'product_id': product_a.id}), # price_total = 120 + Command.create({'product_id': product_b.id, 'price_unit': -95}), # price_total = -114 + ] + self._auto_rewards(order, promotion) + reward_amount_tax_included = sum(l.price_total for l in order.order_line if l.reward_id) + msg = "Reward amount should never surpass the order's current total amount." + self.assertEqual(reward_amount_tax_included, -6, msg) + + order.order_line = [ + Command.clear(), + Command.create({'product_id': product_a.id, 'price_unit': 50}), # price_total = 60 + Command.create({'product_id': product_b.id, 'price_unit': -5}), # price_total = -6 + ] + self._auto_rewards(order, promotion) + reward_amount_tax_included = sum(l.price_total for l in order.order_line if l.reward_id) + msg = "Reward amount should be the percentage one if under the max amount discount." + self.assertEqual(reward_amount_tax_included, -6, msg) + def test_multiple_discount_specific(self): """ Check the discount calculation if it is based on the remaining amount