From bff53f529cf69b2beb2d5337fcd5893b05d5d5e9 Mon Sep 17 00:00:00 2001 From: prro-odoo Date: Wed, 4 Aug 2021 16:31:13 +0000 Subject: [PATCH] [FIX] sale_coupon: allow changing reward line quantity How to reproduce the problem: - Install the sale_coupon module - Sales -> Products -> Promotion Programs - Create a promotion with "Fixed Amount" discount, "Apply on current order", with Based on Products as "Name = Large Cabinet" - Create a SO with 3 Large Cabinet. - Modify the quantity on the new reward line: the price unit became 0. A client should be able to manually change the discount quantity. That was not possible before due to the fact that the reward products of a promotion are saved in the DB with a list price of 0. As the reward generation handles the job of computing the correct unit price on a reward SOL, that unit price is simply re-used when the quantity is changed. Also corrects the case where a Free Product is different from the Rule Product(s): with the promotion "Buy 1 Product A, get 1 free Product B", if the Sale Order has 3 Product A, for example, the promotion will offer only 1 Product B, while it should offer 3. opw-2522604 closes odoo/odoo#75143 X-original-commit: 97b78e8982f1e143350521bdee3c280618fb1846 Signed-off-by: Romain Derie Signed-off-by: prro-odoo --- addons/sale_coupon/models/sale_order.py | 10 ++- .../sale_coupon/tests/test_program_numbers.py | 64 +++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/addons/sale_coupon/models/sale_order.py b/addons/sale_coupon/models/sale_order.py index 623575fb72f..1dbe8566a5c 100644 --- a/addons/sale_coupon/models/sale_order.py +++ b/addons/sale_coupon/models/sale_order.py @@ -95,7 +95,8 @@ class SaleOrder(models.Model): order_total = sum(order_lines.mapped('price_total')) - (program.reward_product_quantity * program.reward_product_id.lst_price) reward_product_qty = min(reward_product_qty, order_total // program.rule_minimum_amount) else: - reward_product_qty = min(program.reward_product_quantity, total_qty) + program_in_order = max_product_qty // program.rule_min_quantity + reward_product_qty = min(program.reward_product_quantity * program_in_order, total_qty) reward_qty = min(int(int(max_product_qty / program.rule_min_quantity) * program.reward_product_quantity), reward_product_qty) # Take the default taxes on the reward product, mapped with the fiscal position @@ -512,6 +513,13 @@ class SaleOrderLine(models.Model): taxes = line.tax_id.filtered(lambda r: not line.company_id or r.company_id == line.company_id) line.tax_id = fpos.map_tax(taxes) + def _get_display_price(self, product): + # A product created from a promotion does not have a list_price. + # The price_unit of a reward order line is computed by the promotion, so it can be used directly + if self.is_reward_line: + return self.price_unit + return super()._get_display_price(product) + # Invalidation of `coupon.program.order_count` # `test_program_rules_validity_dates_and_uses`, # Overriding modified is quite hardcore as you need to know how works the cache and the invalidation system, diff --git a/addons/sale_coupon/tests/test_program_numbers.py b/addons/sale_coupon/tests/test_program_numbers.py index 745565d1341..5323a03a58b 100644 --- a/addons/sale_coupon/tests/test_program_numbers.py +++ b/addons/sale_coupon/tests/test_program_numbers.py @@ -1103,3 +1103,67 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon): self.assertEqual(order.amount_total, 500.0, 'The price must be 500.0 since two conference chairs are free and the user only bought one') self.assertEqual(order.order_line[2].price_total, -100.0, 'The last order line should apply a reduction of 100.0 since there is one conference chair that cost 100.0') + + def test_program_free_product_different_than_rule_product_with_multiple_application(self): + order = self.empty_order + + self.env['sale.order.line'].create({ + 'product_id': self.drawerBlack.id, + 'product_uom_qty': 2.0, + 'order_id': order.id, + }) + sol_B = self.env['sale.order.line'].create({ + 'product_id': self.largeMeetingTable.id, + 'product_uom_qty': 1.0, + 'order_id': order.id, + }) + + order.recompute_coupon_lines() + + self.assertEqual(len(order.order_line), 3, 'The order must contain 3 order lines: 1x for Black Drawer, 1x for Large Meeting Table and 1x for free Large Meeting Table') + self.assertEqual(order.amount_total, self.drawerBlack.list_price * 2, 'The price must be 50.0 since the Large Meeting Table is free: 2*25.00 (Black Drawer) + 1*40000.00 (Large Meeting Table) - 1*40000.00 (free Large Meeting Table)') + self.assertEqual(order.order_line.filtered(lambda x: x.is_reward_line).product_uom_qty, 1, "Only one free Large Meeting Table should be offered, as only one paid Large Meeting Table is in cart. You can't have more free product than paid product.") + + sol_B.product_uom_qty = 2 + + order.recompute_coupon_lines() + + self.assertEqual(len(order.order_line), 3, 'The order must contain 3 order lines: 1x for Black Drawer, 1x for Large Meeting Table and 1x for free Large Meeting Table') + self.assertEqual(order.amount_total, self.drawerBlack.list_price * 2, 'The price must be 50.0 since the 2 Large Meeting Table are free: 2*25.00 (Black Drawer) + 2*40000.00 (Large Meeting Table) - 2*40000.00 (free Large Meeting Table)') + self.assertEqual(order.order_line.filtered(lambda x: x.is_reward_line).product_uom_qty, 2, 'The 2 Large Meeting Table should be offered, as the promotion says 1 Black Drawer = 1 free Large Meeting Table and there are 2 Black Drawer') + + def test_program_modify_reward_line_qty(self): + order = self.empty_order + product_F = self.env['product.product'].create({ + 'name': 'Product F', + 'list_price': 100, + 'sale_ok': True, + 'taxes_id': [(6, 0, [])], + }) + self.env['coupon.program'].create({ + 'name': '1 Product F = 5$ discount', + 'promo_code_usage': 'no_code_needed', + 'reward_type': 'discount', + 'discount_type': 'fixed_amount', + 'discount_fixed_amount': 5, + 'rule_products_domain': "[('id', 'in', [%s])]" % (product_F.id), + 'active': True, + }) + + self.env['sale.order.line'].create({ + 'product_id': product_F.id, + 'product_uom_qty': 2.0, + 'order_id': order.id, + }) + + order.recompute_coupon_lines() + + self.assertEqual(len(order.order_line), 2, 'The order must contain 2 order lines: 1x Product F and 1x 5$ discount') + self.assertEqual(order.amount_total, 195.0, 'The price must be 195.0 since there is a 5$ discount and 2x Product F') + self.assertEqual(order.order_line.filtered(lambda x: x.is_reward_line).product_uom_qty, 1, 'The reward line should have a quantity of 1 since Fixed Amount discounts apply only once per Sale Order') + + order.order_line[1].product_uom_qty = 2 + + self.assertEqual(len(order.order_line), 2, 'The order must contain 2 order lines: 1x Product F and 1x 5$ discount') + self.assertEqual(order.amount_total, 190.0, 'The price must be 190.0 since there is now 2x 5$ discount and 2x Product F') + self.assertEqual(order.order_line.filtered(lambda x: x.is_reward_line).price_unit, -5, 'The discount unit price should still be -5 after the quantity was manually changed')