[FIX] sale_coupon: Avoid discounting more than order amount
- Create a promotion of discount 10% with coupon (A) - Create a promotion of fixed discount (50$) with coupon (B) - Create a sale order of 50$, apply A and then B Order will have a negative total. This is due to the check done on the order amount, which needs to be checked without the current program, to allow correct stacking of promotions opw-2410666 closes odoo/odoo#64848 X-original-commit: 22598c88638aaa2570c88f5bdf01d185abadd086 Signed-off-by: agr-odoo <agr-odoo@users.noreply.github.com>
This commit is contained in:
@@ -116,8 +116,13 @@ class SaleOrder(models.Model):
|
||||
free_reward_product = self.env['coupon.program'].search([('reward_type', '=', 'product')]).mapped('discount_line_product_id')
|
||||
return self.order_line.filtered(lambda x: not x.is_reward_line or x.product_id in free_reward_product)
|
||||
|
||||
def _get_base_order_lines(self, program):
|
||||
""" Returns the sale order lines not linked to the given program.
|
||||
"""
|
||||
return self.order_line.filtered(lambda x: not (x.is_reward_line and x.product_id == program.discount_line_product_id))
|
||||
|
||||
def _get_reward_values_discount_fixed_amount(self, program):
|
||||
total_amount = sum(self._get_paid_order_lines().mapped('price_total'))
|
||||
total_amount = sum(self._get_base_order_lines(program).mapped('price_total'))
|
||||
fixed_amount = program._compute_program_amount('discount_fixed_amount', self.currency_id)
|
||||
if total_amount < fixed_amount:
|
||||
return total_amount
|
||||
@@ -145,17 +150,18 @@ class SaleOrder(models.Model):
|
||||
}]
|
||||
reward_dict = {}
|
||||
lines = self._get_paid_order_lines()
|
||||
amount_total = sum(self._get_base_order_lines(program).mapped('price_subtotal'))
|
||||
if program.discount_apply_on == 'cheapest_product':
|
||||
line = self._get_cheapest_line()
|
||||
if line:
|
||||
discount_line_amount = line.price_reduce * (program.discount_percentage / 100)
|
||||
discount_line_amount = min(line.price_reduce * (program.discount_percentage / 100), amount_total)
|
||||
if discount_line_amount:
|
||||
taxes = self.fiscal_position_id.map_tax(line.tax_id)
|
||||
|
||||
reward_dict[line.tax_id] = {
|
||||
'name': _("Discount: %s", program.name),
|
||||
'product_id': program.discount_line_product_id.id,
|
||||
'price_unit': - discount_line_amount,
|
||||
'price_unit': - discount_line_amount if discount_line_amount > 0 else 0,
|
||||
'product_uom_qty': 1.0,
|
||||
'product_uom': program.discount_line_product_id.uom_id.id,
|
||||
'is_reward_line': True,
|
||||
@@ -167,8 +173,10 @@ class SaleOrder(models.Model):
|
||||
free_product_lines = self.env['coupon.program'].search([('reward_type', '=', 'product'), ('reward_product_id', 'in', program.discount_specific_product_ids.ids)]).mapped('discount_line_product_id')
|
||||
lines = lines.filtered(lambda x: x.product_id in (program.discount_specific_product_ids | free_product_lines))
|
||||
|
||||
# when processing lines we should not discount more than the order remaining total
|
||||
currently_discounted_amount = 0
|
||||
for line in lines:
|
||||
discount_line_amount = self._get_reward_values_discount_percentage_per_line(program, line)
|
||||
discount_line_amount = min(self._get_reward_values_discount_percentage_per_line(program, line), amount_total - currently_discounted_amount)
|
||||
|
||||
if discount_line_amount:
|
||||
|
||||
@@ -184,12 +192,13 @@ class SaleOrder(models.Model):
|
||||
taxes=", ".join(taxes.mapped('name')),
|
||||
),
|
||||
'product_id': program.discount_line_product_id.id,
|
||||
'price_unit': - discount_line_amount,
|
||||
'price_unit': - discount_line_amount if discount_line_amount > 0 else 0,
|
||||
'product_uom_qty': 1.0,
|
||||
'product_uom': program.discount_line_product_id.uom_id.id,
|
||||
'is_reward_line': True,
|
||||
'tax_id': [(4, tax.id, False) for tax in taxes],
|
||||
}
|
||||
currently_discounted_amount += discount_line_amount
|
||||
|
||||
# If there is a max amount for discount, we might have to limit some discount lines or completely remove some lines
|
||||
max_amount = program._compute_program_amount('discount_max_amount', self.currency_id)
|
||||
|
||||
@@ -640,6 +640,182 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
|
||||
order.recompute_coupon_lines()
|
||||
self.assertEqual(order.amount_total, 82.5, "The promotion programs should have been removed from the order to avoid negative amount")
|
||||
|
||||
def test_coupon_and_coupon_discount_fixed_amount_tax_excl(self):
|
||||
""" Ensure multiple coupon can cohexists without making
|
||||
the order go below 0
|
||||
* Have an order of 300 (3 lines: 1 tax excl 15%, 2 notax)
|
||||
* Apply a coupon A of 10% discount, unconditioned
|
||||
* Apply a coupon B of 288.5 discount, unconditioned
|
||||
* Order should not go below 0
|
||||
* Even applying the coupon in reverse order should yield same result
|
||||
"""
|
||||
|
||||
coupon_program = self.env['coupon.program'].create({
|
||||
'name': '$288.5 coupon',
|
||||
'program_type': 'coupon_program',
|
||||
'reward_type': 'discount',
|
||||
'discount_type': 'fixed_amount',
|
||||
'discount_fixed_amount': 288.5,
|
||||
'active': True,
|
||||
'discount_apply_on': 'on_order',
|
||||
})
|
||||
|
||||
order = self.empty_order
|
||||
orderline = self.env['sale.order.line'].create([
|
||||
{
|
||||
'product_id': self.conferenceChair.id,
|
||||
'name': 'Conference Chair',
|
||||
'product_uom_qty': 1.0,
|
||||
'price_unit': 100.0,
|
||||
'order_id': order.id,
|
||||
'tax_id': [(6, 0, (self.tax_15pc_excl.id,))],
|
||||
},
|
||||
{
|
||||
'product_id': self.pedalBin.id,
|
||||
'name': 'Computer Case',
|
||||
'product_uom_qty': 1.0,
|
||||
'price_unit': 100.0,
|
||||
'order_id': order.id,
|
||||
'tax_id': [(6, 0, [])],
|
||||
},
|
||||
{
|
||||
'product_id': self.product_A.id,
|
||||
'name': 'Computer Case',
|
||||
'product_uom_qty': 1.0,
|
||||
'price_unit': 100.0,
|
||||
'order_id': order.id,
|
||||
'tax_id': [(6, 0, [])],
|
||||
},
|
||||
])
|
||||
|
||||
self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
|
||||
'coupon_code': 'test_10pc'
|
||||
}).process_coupon()
|
||||
self.assertEqual(order.amount_total, 283.5, "The promotion program should be correctly applied")
|
||||
|
||||
self.env['coupon.generate.wizard'].with_context(active_id=coupon_program.id).create({
|
||||
'generation_type': 'nbr_coupon',
|
||||
'nbr_coupons': 1,
|
||||
}).generate_coupon()
|
||||
coupon = coupon_program.coupon_ids
|
||||
self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
|
||||
'coupon_code': coupon.code
|
||||
}).process_coupon()
|
||||
order.recompute_coupon_lines()
|
||||
#TODO fix numbers
|
||||
# Need an in-depth inspection on the behavior with
|
||||
# - multiple product with different VAT +
|
||||
# - a fixed amount (greater than remaining amount to pay) +
|
||||
# - discount amount
|
||||
# And user should be able to swap the promotion order with a meaningful result.
|
||||
self.assertEqual(order.amount_tax, 13.5)
|
||||
self.assertEqual(order.amount_untaxed, 0.0, "The untaxed amount should not go below 0")
|
||||
self.assertEqual(order.amount_total, 13.5, "The promotion program should not make the order total go below 0")
|
||||
|
||||
order.order_line[3:].unlink() #remove all coupon
|
||||
|
||||
order.recompute_coupon_lines()
|
||||
self.assertEqual(len(order.order_line), 3, "The promotion program should be removed")
|
||||
self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
|
||||
'coupon_code': coupon.code
|
||||
}).process_coupon()
|
||||
self.assertEqual(order.amount_total, 26.5, "The promotion program should be correctly applied")
|
||||
order.recompute_coupon_lines()
|
||||
self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
|
||||
'coupon_code': 'test_10pc'
|
||||
}).process_coupon()
|
||||
order.recompute_coupon_lines()
|
||||
#TODO fix numbers
|
||||
self.assertEqual(order.amount_tax, 13.5)
|
||||
self.assertEqual(order.amount_untaxed, 0.0)
|
||||
self.assertEqual(order.amount_total, 13.5, "The promotion program should not make the order total go below 0be altered after recomputation")
|
||||
|
||||
def test_coupon_and_coupon_discount_fixed_amount_tax_incl(self):
|
||||
""" Ensure multiple coupon can cohexists without making
|
||||
the order go below 0
|
||||
* Have an order of 300 (3 lines: 1 tax incl 10%, 2 notax)
|
||||
* Apply a coupon A of 10% discount, unconditioned
|
||||
* Apply a coupon B of 290 discount, unconditioned
|
||||
* Order should not go below 0
|
||||
* Even applying the coupon in reverse order should yield same result
|
||||
"""
|
||||
|
||||
coupon_program = self.env['coupon.program'].create({
|
||||
'name': '$290 coupon',
|
||||
'program_type': 'coupon_program',
|
||||
'reward_type': 'discount',
|
||||
'discount_type': 'fixed_amount',
|
||||
'discount_fixed_amount': 290,
|
||||
'active': True,
|
||||
'discount_apply_on': 'on_order',
|
||||
})
|
||||
|
||||
order = self.empty_order
|
||||
orderline = self.env['sale.order.line'].create([
|
||||
{
|
||||
'product_id': self.conferenceChair.id,
|
||||
'name': 'Conference Chair',
|
||||
'product_uom_qty': 1.0,
|
||||
'price_unit': 100.0,
|
||||
'order_id': order.id,
|
||||
'tax_id': [(6, 0, (self.tax_10pc_incl.id,))],
|
||||
},
|
||||
{
|
||||
'product_id': self.pedalBin.id,
|
||||
'name': 'Computer Case',
|
||||
'product_uom_qty': 1.0,
|
||||
'price_unit': 100.0,
|
||||
'order_id': order.id,
|
||||
'tax_id': [(6, 0, [])],
|
||||
},
|
||||
{
|
||||
'product_id': self.product_A.id,
|
||||
'name': 'Computer Case',
|
||||
'product_uom_qty': 1.0,
|
||||
'price_unit': 100.0,
|
||||
'order_id': order.id,
|
||||
'tax_id': [(6, 0, [])],
|
||||
},
|
||||
])
|
||||
|
||||
self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
|
||||
'coupon_code': 'test_10pc'
|
||||
}).process_coupon()
|
||||
self.assertEqual(order.amount_total, 270.0, "The promotion program should be correctly applied")
|
||||
|
||||
self.env['coupon.generate.wizard'].with_context(active_id=coupon_program.id).create({
|
||||
'generation_type': 'nbr_coupon',
|
||||
'nbr_coupons': 1,
|
||||
}).generate_coupon()
|
||||
coupon = coupon_program.coupon_ids
|
||||
self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
|
||||
'coupon_code': coupon.code
|
||||
}).process_coupon()
|
||||
self.assertEqual(order.amount_total, 0.0, "The promotion program should not make the order total go below 0")
|
||||
order.recompute_coupon_lines()
|
||||
#TODO fix numbers
|
||||
self.assertEqual(order.amount_total, 9.09, "The promotion program should not be altered after recomputation")
|
||||
self.assertEqual(order.amount_tax, 8.18)
|
||||
self.assertEqual(order.amount_untaxed, 0.91)
|
||||
|
||||
order.order_line[3:].unlink() #remove all coupon
|
||||
|
||||
order.recompute_coupon_lines()
|
||||
self.assertEqual(len(order.order_line), 3, "The promotion program should be removed")
|
||||
self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
|
||||
'coupon_code': coupon.code
|
||||
}).process_coupon()
|
||||
self.assertEqual(order.amount_total, 10.0, "The promotion program should be correctly applied")
|
||||
order.recompute_coupon_lines()
|
||||
self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
|
||||
'coupon_code': 'test_10pc'
|
||||
}).process_coupon()
|
||||
order.recompute_coupon_lines()
|
||||
#TODO fix numbers
|
||||
self.assertEqual(order.amount_tax, 9.01)
|
||||
self.assertEqual(order.amount_untaxed, 0.08)
|
||||
self.assertEqual(order.amount_total, 9.09, "The promotion program should not be altered after recomputation")
|
||||
|
||||
def test_program_discount_on_multiple_specific_products(self):
|
||||
""" Ensure a discount on multiple specific products is correctly computed.
|
||||
- Simple: Discount must be applied on all the products set on the promotion
|
||||
|
||||
Reference in New Issue
Block a user