From a11bca18100972f71f41a526be3de818ba5acdba Mon Sep 17 00:00:00 2001 From: Ethan Vincent Date: Thu, 21 Mar 2024 13:21:37 +0100 Subject: [PATCH] [FIX] sale_loyalty: skip paid calc if ewallet type **Current behavior:** Creating an ewallet loyalty program and removing the default value for `trigger_product_ids` without adding another in its place will result in any ewallet created using that program to not have its balance deducted after being used in a sale order. **Expected behavior:** The default product should not be required here, nor should any product, seeing as there is no `required` constraint on this field nor any related ones. **Steps to reproduce:** 1. Create an ewallet loyalty program and remove the Top-up ewallet product 2. Create an ewallet, give it some balance, and use it in an order 3. Check the balance of the wallet to see the erroneous behavior **Cause of the issue:** In the _program_check_compute_points() method of `sale.order` in `sale_loyalty`, the conditional block: `if not products_per_rule.get(rule): continue` will normally prevent rules belonging to ewallet program types from going further in the method because they have the default ewallet top-up product in their domain. When the continue is not reached, they will reach this line: `amount_paid = sum(max(0, line.price_total) for line in order_lines if line.product_id in rule_products)` which will end up offsetting the actual subtraction of a SOLs `points_cost` from an ewallet `loyalty.card`'s balance. **Fix:** Add another check prior to the calculation of order points which prevents ewallet coupons without any top-up products from reaching the problematic code. If an ewallet program doesn't have any `trigger_product_ids`, it shouldn't ever need to perform such calculations. opw-3756134 closes odoo/odoo#159957 X-original-commit: 695b3a687a3d160451339a675aab8986682389eb Signed-off-by: Vincent Ethan --- addons/sale_loyalty/models/sale_order.py | 3 + addons/sale_loyalty/tests/test_loyalty.py | 133 +++++++++++++++------- 2 files changed, 92 insertions(+), 44 deletions(-) diff --git a/addons/sale_loyalty/models/sale_order.py b/addons/sale_loyalty/models/sale_order.py index 4e777948027..4890ba628ef 100644 --- a/addons/sale_loyalty/models/sale_order.py +++ b/addons/sale_loyalty/models/sale_order.py @@ -881,6 +881,9 @@ class SaleOrder(models.Model): rule_points = [] program_result = result.setdefault(program, dict()) for rule in program.rule_ids: + # prevent bottomless ewallet spending + if program.program_type == 'ewallet' and not program.trigger_product_ids: + break if rule.mode == 'with_code' and rule not in self.code_enabled_rule_ids: continue code_matched = True diff --git a/addons/sale_loyalty/tests/test_loyalty.py b/addons/sale_loyalty/tests/test_loyalty.py index 1c737648ddc..12fe7e03d43 100644 --- a/addons/sale_loyalty/tests/test_loyalty.py +++ b/addons/sale_loyalty/tests/test_loyalty.py @@ -26,40 +26,52 @@ class TestLoyalty(TestSaleCouponCommon): 'taxes_id': [(6, 0, [])], }) + cls.ewallet_program = cls.env['loyalty.program'].create({ + 'name': 'eWallet Program', + 'program_type': 'ewallet', + 'trigger': 'auto', + 'applies_on': 'future', + 'reward_ids': [Command.create({ + 'reward_type': 'discount', + 'discount_mode': 'per_point', + 'discount': 1, + })], + 'rule_ids': [Command.create({ + 'reward_point_amount': '1', + 'reward_point_mode': 'money', + 'product_ids': cls.env.ref('loyalty.ewallet_product_50'), + })], + 'trigger_product_ids': cls.env.ref('loyalty.ewallet_product_50'), + }) + + cls.ewallet = cls.env['loyalty.card'].create({ + 'program_id': cls.ewallet_program.id, + 'partner_id': cls.partner_a.id, + 'points': 10, + }) + cls.ewallet_program.coupon_ids = [Command.set([cls.ewallet.id])] + cls.user_salemanager = new_test_user(cls.env, login='user_salemanager', groups='sales_team.group_sale_manager') def test_nominative_programs(self): - loyalty_program, ewallet_program = self.env['loyalty.program'].create([ - { - 'name': 'Loyalty Program', - 'program_type': 'loyalty', - 'trigger': 'auto', - 'applies_on': 'both', - 'rule_ids': [(0, 0, { - 'reward_point_mode': 'unit', - 'reward_point_amount': 1, - 'product_ids': [self.product_a.id], - })], - 'reward_ids': [(0, 0, { - 'reward_type': 'discount', - 'discount': 1.5, - 'discount_mode': 'per_point', - 'discount_applicability': 'order', - 'required_points': 3, - })], - }, - { - 'name': 'eWallet Program', - 'program_type': 'ewallet', - 'applies_on': 'future', - 'trigger': 'auto', - 'rule_ids': [(0, 0, { - 'reward_point_mode': 'money', - 'reward_point_amount': 10, - })], - 'reward_ids': [(0, 0, {})], - } - ]) + loyalty_program = self.env['loyalty.program'].create({ + 'name': 'Loyalty Program', + 'program_type': 'loyalty', + 'trigger': 'auto', + 'applies_on': 'both', + 'rule_ids': [(0, 0, { + 'reward_point_mode': 'unit', + 'reward_point_amount': 1, + 'product_ids': [self.product_a.id], + })], + 'reward_ids': [(0, 0, { + 'reward_type': 'discount', + 'discount': 1.5, + 'discount_mode': 'per_point', + 'discount_applicability': 'order', + 'required_points': 3, + })], + }) order = self.env['sale.order'].create({ 'partner_id': self.partner_a.id, @@ -68,18 +80,13 @@ class TestLoyalty(TestSaleCouponCommon): claimable_rewards = order._get_claimable_rewards() # Should be empty since we do not have any coupon created yet self.assertFalse(claimable_rewards, "No program should be applicable") - loyalty_card, ewallet_coupon = self.env['loyalty.card'].create([ - { - 'program_id': loyalty_program.id, - 'partner_id': self.partner_a.id, - 'points': 10, - }, - { - 'program_id': ewallet_program.id, - 'partner_id': self.partner_a.id, - 'points': 0, - }, - ]) + loyalty_card = self.env['loyalty.card'].create({ + 'program_id': loyalty_program.id, + 'partner_id': self.partner_a.id, + 'points': 10, + }) + self.ewallet.points = 0 + order.write({ 'order_line': [(0, 0, { 'product_id': self.product_a.id, @@ -96,7 +103,7 @@ class TestLoyalty(TestSaleCouponCommon): "Can only use a whole number of required points", ) self.assertEqual(vals[0]['points_cost'], 9, "Use maximum available points for the reward") - ewallet_coupon.points = 50 + self.ewallet.points = 50 order._update_programs_and_rewards() claimable_rewards = order._get_claimable_rewards() self.assertEqual(len(claimable_rewards), 2, "Now that the ewallet has some points they should both be applicable.") @@ -605,3 +612,41 @@ class TestLoyalty(TestSaleCouponCommon): self._claim_reward(order, loyalty_program) msg = "100% discount on order should reduce total amount to 0" self.assertEqual(order.amount_total, 0, msg=msg) + + def test_ewallet_program_without_trigger_product(self): + self.ewallet_program.trigger_product_ids = [Command.clear()] + self.ewallet.points = 1000 + + order = self.env['sale.order'].create({ + 'partner_id': self.partner_a.id, + 'order_line': [Command.create({ + 'product_id': self.product_a.id, + 'points_cost': 100, + 'product_uom_qty': 1, + })], + }) + order._update_programs_and_rewards() + self._claim_reward(order, self.ewallet_program, coupon=self.ewallet) + order.action_confirm() + + self.assertEqual(self.ewallet.points, 900) + + def test_ewallet_applied_ewallet_topup_in_order(self): + self.ewallet.points = 10 + + order = self.env['sale.order'].create({ + 'partner_id': self.partner_a.id, + 'order_line': [Command.create({ + 'product_id': self.product_a.id, + 'points_cost': 100, + 'product_uom_qty': 1, + }), Command.create({ + 'product_id': self.env.ref('loyalty.ewallet_product_50').id, + 'product_uom_qty': 1, + })], + }) + order._update_programs_and_rewards() + self._claim_reward(order, self.ewallet_program, coupon=self.ewallet) + order.action_confirm() + + self.assertEqual(self.ewallet.points, 50)