Skip to content
Snippets Groups Projects
Commit 9d6095da authored by Thomas Lefebvre (thle)'s avatar Thomas Lefebvre (thle) Committed by Victor Feyens
Browse files

[FIX] sale_coupon: lost program information on line deletion


Versions:
---------

(- 14.0)
- 15.0
- saas-15.2

Steps to reproduce:
-------------------

1) Create two products:
    - one without tax (A)
    - one with tax (B)
2) Create a promotion program which
    applies a discount to the sale order
    and is triggered by a code
3) Add these two products to the ecommerce cart
4) Apply the promotion program
5) Remove the product with a tax (B)
6) Re-apply the promotion program

Issue:
------

The promotion program is applied twice.
The mechanism can be repeated to apply the promotion program
as many times as we want.

Cause:
------

When the product with tax is removed from the order (step 5), a call to
`_cart_update` > `recompute_coupon_lines` > `_update_existing_reward_lines`,
which will:
1) remove the reward line corresponding to the removed line
    which will 1a) remove the program from the order
    1b) remove all reward lines from this program from the order
2) recreate the other reward lines of the program that should be kept
    In our steps, the reward line representing the discount on product A

This results in a sale order holding the expected reward of the program,
but without being linked to this program anymore.

Solution:
---------

If for a given program, some lines are kept and some removed,
we have to make sure the program stays linked to the order,
making sure it cannot be applied multiple times.

opw-3267359

closes odoo/odoo#128542

Signed-off-by: default avatarVictor Feyens (vfe) <vfe@odoo.com>
Co-authored-by: default avatarVictor Feyens <vfe@odoo.com>
parent 4301489e
Branches
Tags
No related merge requests found
......@@ -424,6 +424,7 @@ class SaleOrder(models.Model):
self.ensure_one()
order = self
applied_programs = order._get_applied_programs_with_rewards_on_current_order()
programs_lost_that_should_be_kept = self.env['coupon.program']
for program in applied_programs.sorted(lambda ap: (ap.discount_type == 'fixed_amount', ap.discount_apply_on == 'on_order')):
values = order._get_reward_line_values(program)
lines = order.order_line.filtered(lambda line: line.product_id == program.discount_line_product_id)
......@@ -454,13 +455,36 @@ class SaleOrder(models.Model):
lines_to_add += [(0, False, value)]
# Case 3.
line_update = []
if lines_to_remove:
if lines_to_remove: # == if lines_to_keep
programs_lost_that_should_be_kept += program
line_update += [(3, line_id, 0) for line_id in lines_to_remove.ids]
line_update += lines_to_keep
line_update += lines_to_add
order.write({'order_line': line_update})
else:
update_line(order, lines, values[0]).unlink()
if programs_lost_that_should_be_kept:
# Some programs are lost when part of the targets of their rewards are removed
# Since they are still applied, we need to link them again to the order
# (even if it means recomputing somes reward lines with the same values)
#
# Example:
# Order with products A (Tax T) & B (no tax), program of x% discount
# will have two reward lines L1 & L2 (tax & no tax)
# If we remove product A, reward line L1 will be removed, and the program at the same time
# L2 is not removed, but since the program was removed from the order, it can be applied
# once again, leading to infinite discounts.
no_code_programs = programs_lost_that_should_be_kept.filtered(
lambda p: p.promo_code_usage == 'no_code_needed')
code_program = programs_lost_that_should_be_kept - no_code_programs
update_vals = {}
if code_program:
update_vals['code_promo_program_id'] = code_program.id
if no_code_programs:
update_vals['no_code_promo_program_ids'] = [
(4, no_code_program.id, 0) for no_code_program in no_code_programs]
if update_vals:
order.write(update_vals)
def _remove_invalid_reward_lines(self):
""" Find programs & coupons that are not applicable anymore.
......
......@@ -379,3 +379,68 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
}).process_coupon()
self.assertFalse(self.empty_order.applied_coupon_ids, 'No coupon should be linked to the order')
self.assertEqual(coupon.state, 'new', 'Coupon should be in a new state')
def test_delete_all_discount_lines(self):
"""
The goal is to ensure that all discount lines are deleted
when we need to update existing reward lines.
"""
program = self.env['coupon.program'].create({
'name': '50% Discount on order',
'promo_code_usage': 'code_needed',
'promo_code': 'test',
'reward_type': 'discount',
'discount_type': 'percentage',
'discount_percentage': 50,
'active': True,
'discount_apply_on': 'on_order',
})
product_with_tax, product_without_tax = self.env['product.product'].create([
{
'name': 'Product with tax',
'list_price': 100,
'sale_ok': True,
'taxes_id': [self.tax_10pc_excl.id],
},
{
'name': 'Product without tax',
'list_price': 50,
'sale_ok': True,
'taxes_id': [],
}
])
order = self.empty_order.copy()
order.write({'order_line': [
(0, False, {
'product_id': product_with_tax.id,
'name': '1 Product with tax',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
}),
(0, False, {
'product_id': product_without_tax.id,
'name': '1 Product without tax',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
self.assertEqual(order.amount_total, 160)
self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
'coupon_code': 'test'
}).process_coupon()
self.assertEqual(order.amount_total, 80)
self.assertEqual(order.code_promo_program_id, program)
self.assertEqual(len(order.order_line), 4, '2 products and 2 discount lines')
line_to_remove = order.order_line.filtered(lambda l: l.product_id == product_without_tax)
order.write({'order_line': [(3, line_to_remove.id, 0)]})
self.assertEqual(order.code_promo_program_id, program)
self.assertEqual(order.amount_total, 30)
order.recompute_coupon_lines()
self.assertEqual(order.amount_total, 55)
self.assertEqual(order.code_promo_program_id, program)
reward_lines = order.order_line.filtered(lambda l: l.is_reward_line)
self.assertTrue(reward_lines)
self.assertEqual(order.code_promo_program_id, program)
......@@ -57,3 +57,65 @@ class TestProgramWithoutCodeOperations(TestSaleCouponCommon):
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 1, "The promo reward should have been removed as the rules are not matched anymore")
self.assertEqual(order.order_line.product_id.id, self.product_B.id, "The wrong line has been removed")
def test_program_remains_linked_to_order_when_lines_are_removed(self):
"""
The goal is to ensure that all discount lines are deleted
when we need to update existing reward lines.
"""
program = self.env['coupon.program'].create({
'name': '50% Discount on order',
'promo_code_usage': 'no_code_needed',
'reward_type': 'discount',
'discount_type': 'percentage',
'discount_percentage': 50,
'active': True,
'discount_apply_on': 'on_order',
})
product_with_tax, product_without_tax = self.env['product.product'].create([
{
'name': 'Product with tax',
'list_price': 100,
'sale_ok': True,
'taxes_id': [self.tax_10pc_excl.id],
},
{
'name': 'Product without tax',
'list_price': 50,
'sale_ok': True,
'taxes_id': [],
}
])
order = self.empty_order.copy()
order.write({'order_line': [
(0, False, {
'product_id': product_with_tax.id,
'name': '1 Product with tax',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
}),
(0, False, {
'product_id': product_without_tax.id,
'name': '1 Product without tax',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
self.assertEqual(order.amount_total, 160)
order.recompute_coupon_lines()
self.assertEqual(order.amount_total, 80)
self.assertEqual(order.no_code_promo_program_ids, program)
self.assertEqual(len(order.order_line), 4, '2 products and 2 discount lines')
line_to_remove = order.order_line.filtered(lambda l: l.product_id == product_without_tax)
order.write({'order_line': [(3, line_to_remove.id, 0)]})
self.assertEqual(order.no_code_promo_program_ids, program)
self.assertEqual(order.amount_total, 30)
order.recompute_coupon_lines()
self.assertEqual(order.amount_total, 55)
self.assertEqual(order.no_code_promo_program_ids, program)
reward_lines = order.order_line.filtered(lambda l: l.is_reward_line)
self.assertTrue(reward_lines)
self.assertEqual(order.no_code_promo_program_ids, program)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment