[IMP] sale_coupon: allow discount on multiple specific products
Before this commit, a discount could only target the whole order, the cheapeast product or a specific product. With this commit, it becomes possible to select multiple specific products. This will ease some realistic ecommerce flows. For instance, one might create a promotion targetting all his bikes by simply adding all bikes products into the specific products field. This is easily done with the many2many widget on the form view. task-33737 closes odoo/odoo#35500 Signed-off-by: Jérémy Kersten (jke) <jke@openerp.com> Co-authored-by: Pratima Gupta <pgu@odoo.com> Co-authored-by: Romain Derie <rde@odoo.com>
This commit is contained in:
co-authored by
Pratima Gupta
parent
779e47caf3
commit
e87cec8c49
@@ -26,10 +26,18 @@
|
||||
<span style="font-size: 50px; color: #875A7B; font-weight: bold;">
|
||||
${object.program_id.discount_percentage} %
|
||||
</span>
|
||||
% if object.program_id.discount_apply_on == 'specific_product':
|
||||
<br /><strong style="font-size: 24px;">
|
||||
${'on %s' % object.program_id.discount_specific_product_id.name}
|
||||
</strong>
|
||||
% if object.program_id.discount_apply_on == 'specific_products'
|
||||
<br />
|
||||
% if len(object.program_id.discount_specific_product_ids) != 1
|
||||
% set display_specific_products = True
|
||||
<strong style="font-size: 24px;">
|
||||
on some products*
|
||||
</strong>
|
||||
% else
|
||||
<strong style="font-size: 24px;">
|
||||
${'on %s' % object.program_id.discount_specific_product_ids.name}
|
||||
</strong>
|
||||
% endif
|
||||
% elif object.program_id.discount_apply_on == 'cheapest_product':
|
||||
<br /><strong style="font-size: 24px;">
|
||||
off on the cheapest product
|
||||
@@ -73,6 +81,11 @@
|
||||
Valid for purchase above ${object.program_id.company_id.currency_id.symbol}${'%0.2f' % object.program_id.rule_minimum_amount |float}
|
||||
</span><br />
|
||||
% endif
|
||||
% if display_specific_products
|
||||
<span>
|
||||
*Valid for following products: ${', '.join(object.program_id.discount_specific_product_ids.mapped('name'))}
|
||||
</span><br />
|
||||
% endif
|
||||
<br/>
|
||||
Thank you,
|
||||
<br/>
|
||||
|
||||
@@ -110,7 +110,7 @@ class SaleCouponProgram(models.Model):
|
||||
res = super(SaleCouponProgram, self).write(vals)
|
||||
reward_fields = [
|
||||
'reward_type', 'reward_product_id', 'discount_type', 'discount_percentage',
|
||||
'discount_apply_on', 'discount_specific_product_id', 'discount_fixed_amount'
|
||||
'discount_apply_on', 'discount_specific_product_ids', 'discount_fixed_amount'
|
||||
]
|
||||
if any(field in reward_fields for field in vals):
|
||||
self.mapped('discount_line_product_id').write({'name': self[0].reward_id.display_name})
|
||||
@@ -259,8 +259,8 @@ class SaleCouponProgram(models.Model):
|
||||
if program.reward_type == 'product' and \
|
||||
not order.order_line.filtered(lambda line: line.product_id == program.reward_product_id):
|
||||
continue
|
||||
elif program.reward_type == 'discount' and program.discount_apply_on == 'specific_product' and \
|
||||
not order.order_line.filtered(lambda line: line.product_id == program.discount_specific_product_id):
|
||||
elif program.reward_type == 'discount' and program.discount_apply_on == 'specific_products' and \
|
||||
not order.order_line.filtered(lambda line: line.product_id in program.discount_specific_product_ids):
|
||||
continue
|
||||
programs |= program
|
||||
return programs
|
||||
|
||||
@@ -33,12 +33,12 @@ class SaleCouponReward(models.Model):
|
||||
discount_apply_on = fields.Selection([
|
||||
('on_order', 'On Order'),
|
||||
('cheapest_product', 'On Cheapest Product'),
|
||||
('specific_product', 'On Specific Product')], default="on_order",
|
||||
('specific_products', 'On Specific Products')], default="on_order",
|
||||
help="On Order - Discount on whole order\n" +
|
||||
"Cheapest product - Discount on cheapest product of the order\n" +
|
||||
"Specific product - Discount on selected specific product")
|
||||
discount_specific_product_id = fields.Many2one('product.product', string="Product",
|
||||
help="Product that will be discounted if the discount is applied on a specific product")
|
||||
"Specific products - Discount on selected specific products")
|
||||
discount_specific_product_ids = fields.Many2many('product.product', string="Products",
|
||||
help="Products that will be discounted if the discount is applied on specific products")
|
||||
discount_max_amount = fields.Float(default=0,
|
||||
help="Maximum amount of discount that can be provided")
|
||||
discount_fixed_amount = fields.Float(string="Fixed Amount", help='The discount in fixed amount')
|
||||
@@ -65,8 +65,11 @@ class SaleCouponReward(models.Model):
|
||||
reward_percentage = str(reward.discount_percentage)
|
||||
if reward.discount_apply_on == 'on_order':
|
||||
reward_string = _("%s%% discount on total amount" % (reward_percentage))
|
||||
elif reward.discount_apply_on == 'specific_product':
|
||||
reward_string = _("%s%% discount on %s" % (reward_percentage, reward.discount_specific_product_id.name))
|
||||
elif reward.discount_apply_on == 'specific_products':
|
||||
if len(reward.discount_specific_product_ids) > 1:
|
||||
reward_string = _("%s%% discount on products" % (reward_percentage))
|
||||
else:
|
||||
reward_string = _("%s%% discount on %s" % (reward_percentage, reward.discount_specific_product_ids.name))
|
||||
elif reward.discount_apply_on == 'cheapest_product':
|
||||
reward_string = _("%s%% discount on cheapest product" % (reward_percentage))
|
||||
elif reward.discount_type == 'fixed_amount':
|
||||
|
||||
@@ -155,11 +155,11 @@ class SaleOrder(models.Model):
|
||||
'is_reward_line': True,
|
||||
'tax_id': [(4, tax.id, False) for tax in taxes],
|
||||
}
|
||||
elif program.discount_apply_on in ['specific_product', 'on_order']:
|
||||
if program.discount_apply_on == 'specific_product':
|
||||
elif program.discount_apply_on in ['specific_products', 'on_order']:
|
||||
if program.discount_apply_on == 'specific_products':
|
||||
# We should not exclude reward line that offer this product since we need to offer only the discount on the real paid product (regular product - free product)
|
||||
free_product_lines = self.env['sale.coupon.program'].search([('reward_type', '=', 'product'), ('reward_product_id', '=', program.discount_specific_product_id.id)]).mapped('discount_line_product_id')
|
||||
lines = lines.filtered(lambda x: x.product_id == program.discount_specific_product_id or x.product_id in free_product_lines)
|
||||
free_product_lines = self.env['sale.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))
|
||||
|
||||
for line in lines:
|
||||
discount_line_amount = self._get_reward_values_discount_percentage_per_line(program, line)
|
||||
|
||||
@@ -19,8 +19,14 @@
|
||||
<h1 t-if="o.program_id.discount_type == 'percentage'" class="text-success">
|
||||
<strong><span t-field="o.program_id.discount_percentage"/>%</strong>
|
||||
</h1>
|
||||
<t t-if="o.program_id.discount_apply_on == 'specific_product'">
|
||||
<t t-set="text" t-value="'OFF ON %s' % (o.program_id.discount_specific_product_id.name.upper())"/>
|
||||
<t t-if="o.program_id.discount_apply_on == 'specific_products'">
|
||||
<t t-if="len(o.program_id.discount_specific_product_ids) > 1">
|
||||
<t t-set="text" t-value="'OFF ON SOME PRODUCTS*'"/>
|
||||
<t t-set="display_specific_products" t-value="True"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-set="text" t-value="'OFF ON %s' % o.program_id.discount_specific_product_ids.name.upper()"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="o.program_id.discount_apply_on == 'cheapest_product'">
|
||||
<t t-set="text" t-value="'%s' % ('OFF ON THE CHEAPEST PRODUCT')"/>
|
||||
@@ -49,6 +55,11 @@
|
||||
Valid for purchase above
|
||||
<strong t-esc="o.program_id.rule_minimum_amount" t-options="{'widget': 'monetary', 'display_currency': o.program_id.currency_id}"/>
|
||||
</h4>
|
||||
<p t-if="display_specific_products">
|
||||
<small>
|
||||
*Valid for following products: <t t-esc="', '.join(o.program_id.discount_specific_product_ids.mapped('name')).upper()"/>
|
||||
</small>
|
||||
</p>
|
||||
<br/>
|
||||
<img alt="Barcode" t-att-src="'/report/barcode/Code128/%s' % o.code"/>
|
||||
<br/>
|
||||
|
||||
@@ -142,8 +142,8 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
|
||||
'discount_type': 'percentage',
|
||||
'discount_percentage': 20.0,
|
||||
'rule_minimum_amount': 320.00,
|
||||
'discount_apply_on': 'specific_product',
|
||||
'discount_specific_product_id': self.largeCabinet.id,
|
||||
'discount_apply_on': 'specific_products',
|
||||
'discount_specific_product_ids': [(6, 0, [self.largeCabinet.id])],
|
||||
})
|
||||
order = self.empty_order
|
||||
self.largeCabinet.taxes_id = percent_tax
|
||||
@@ -377,8 +377,8 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
|
||||
'program_type': 'promotion_program',
|
||||
'discount_type': 'percentage',
|
||||
'discount_percentage': 20.0,
|
||||
'discount_apply_on': 'specific_product',
|
||||
'discount_specific_product_id': self.largeCabinet.id,
|
||||
'discount_apply_on': 'specific_products',
|
||||
'discount_specific_product_ids': [(6, 0, [self.largeCabinet.id])],
|
||||
})
|
||||
order.recompute_coupon_lines()
|
||||
# Note: we have 7 regular Large Cabinets and 3 free Large Cabinets. We should then discount only 4 really paid Large Cabinets
|
||||
@@ -537,3 +537,83 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
|
||||
generated_coupon = order.generated_coupon_ids
|
||||
self.assertEqual(len(generated_coupon), 1, "We should still have only 1 coupon as we now benefit again from the program but no need to create a new one (see next assert)")
|
||||
self.assertEqual(generated_coupon.state, 'reserved', "The coupon should be set back to reserved as we had already an expired one, no need to create a new one")
|
||||
|
||||
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
|
||||
- Advanced: This discount must be split by different taxes
|
||||
"""
|
||||
order = self.empty_order
|
||||
p_specific_products = self.env['sale.coupon.program'].create({
|
||||
'name': '20% reduction on Conference Chair and Drawer Black in cart',
|
||||
'promo_code_usage': 'no_code_needed',
|
||||
'reward_type': 'discount',
|
||||
'program_type': 'promotion_program',
|
||||
'discount_type': 'percentage',
|
||||
'discount_percentage': 25.0,
|
||||
'discount_apply_on': 'specific_products',
|
||||
'discount_specific_product_ids': [(6, 0, [self.conferenceChair.id, self.drawerBlack.id])],
|
||||
})
|
||||
|
||||
self.env['sale.order.line'].create({
|
||||
'product_id': self.conferenceChair.id,
|
||||
'name': 'Conference Chair',
|
||||
'product_uom_qty': 4.0,
|
||||
'order_id': order.id,
|
||||
})
|
||||
sol2 = self.env['sale.order.line'].create({
|
||||
'product_id': self.drawerBlack.id,
|
||||
'name': 'Drawer Black',
|
||||
'product_uom_qty': 2.0,
|
||||
'order_id': order.id,
|
||||
})
|
||||
|
||||
order.recompute_coupon_lines()
|
||||
self.assertEqual(len(order.order_line.ids), 3, "Conference Chair + Drawer Black + 20% discount line")
|
||||
# Name | Qty | price_unit | Tax | HTVA | TVAC | TVA |
|
||||
# --------------------------------------------------------------------------------
|
||||
# Conference Chair | 4 | 16.50 | / | 66.00 | 66.00 | 0.00
|
||||
# Drawer Black | 2 | 25.00 | / | 50.00 | 50.00 | 0.00
|
||||
# 25% discount | 1 | -29.00 | / | -29.00 | -29.00 | 0.00
|
||||
# --------------------------------------------------------------------------------
|
||||
# TOTAL | 87.00 | 87.00 | 0.00
|
||||
self.assertEqual(order.amount_total, 87.00, "Total should be 87.00, see above comment")
|
||||
|
||||
# remove Drawer Black case from promotion
|
||||
p_specific_products.discount_specific_product_ids = [(6, 0, [self.conferenceChair.id])]
|
||||
order.recompute_coupon_lines()
|
||||
self.assertEqual(len(order.order_line.ids), 3, "Should still be Conference Chair + Drawer Black + 20% discount line")
|
||||
# Name | Qty | price_unit | Tax | HTVA | TVAC | TVA |
|
||||
# --------------------------------------------------------------------------------
|
||||
# Conference Chair | 4 | 16.50 | / | 66.00 | 66.00 | 0.00
|
||||
# Drawer Black | 2 | 25.00 | / | 50.00 | 50.00 | 0.00
|
||||
# 25% discount | 1 | -16.50 | / | -16.50 | -16.50 | 0.00
|
||||
# --------------------------------------------------------------------------------
|
||||
# TOTAL | 99.50 | 99.50 | 0.00
|
||||
self.assertEqual(order.amount_total, 99.50, "The 12.50 discount from the drawer black should be gone")
|
||||
|
||||
# =========================================================================
|
||||
# PART 2: Same flow but with different taxes on products to ensure discount is split per VAT
|
||||
# Add back Drawer Black in promotion
|
||||
p_specific_products.discount_specific_product_ids = [(6, 0, [self.conferenceChair.id, self.drawerBlack.id])]
|
||||
|
||||
percent_tax = self.env['account.tax'].create({
|
||||
'name': "30% Tax",
|
||||
'amount_type': 'percent',
|
||||
'amount': 30,
|
||||
'price_include': True,
|
||||
})
|
||||
sol2.tax_id = percent_tax
|
||||
|
||||
order.recompute_coupon_lines()
|
||||
self.assertEqual(len(order.order_line.ids), 4, "Conference Chair + Drawer Black + 20% on no TVA product (Conference Chair) + 20% on 15% tva product (Drawer Black)")
|
||||
# Name | Qty | price_unit | Tax | HTVA | TVAC | TVA |
|
||||
# --------------------------------------------------------------------------------
|
||||
# Conference Chair | 4 | 16.50 | / | 66.00 | 66.00 | 0.00
|
||||
# Drawer Black | 2 | 25.00 | 30% incl | 38.46 | 50.00 | 11.54
|
||||
# 25% discount | 1 | -16.50 | / | -16.50 | -16.50 | 0.00
|
||||
# 25% discount | 1 | -12.50 | 30% incl | -9.62 | -12.50 | -2.88
|
||||
# --------------------------------------------------------------------------------
|
||||
# TOTAL | 78.34 | 87.00 | 8.66
|
||||
self.assertEqual(order.amount_total, 87.00, "Total untaxed should be as per above comment")
|
||||
self.assertEqual(order.amount_untaxed, 78.34, "Total with taxes should be as per above comment")
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</div>
|
||||
<field name="discount_apply_on" attrs="{'invisible':
|
||||
['|', ('reward_type', 'in', ('product', 'free_shipping')), ('discount_type', '!=', 'percentage')]}" widget="radio"/>
|
||||
<field name="discount_specific_product_id" attrs="{'invisible': ['|', '|', ('discount_apply_on', '!=', 'specific_product'),('discount_type', '!=', 'percentage'), ('reward_type', 'in', ('product', 'free_shipping'))], 'required': [('reward_type', '=', 'discount'),('discount_apply_on', '=', 'specific_product'),('discount_type', '=', 'percentage')]}" placeholder="Select product"/>
|
||||
<field name="discount_specific_product_ids" widget='many2many_tags' attrs="{'invisible': ['|', '|', ('discount_apply_on', '!=', 'specific_products'),('discount_type', '!=', 'percentage'), ('reward_type', 'in', ('product', 'free_shipping'))], 'required': [('reward_type', '=', 'discount'),('discount_apply_on', '=', 'specific_products'),('discount_type', '=', 'percentage')]}" placeholder="Select products"/>
|
||||
<label for="discount_max_amount" string="Max Discount Amount" attrs="{'invisible': ['|', ('reward_type', 'in', ('product', 'free_shipping')), ('discount_type', '!=', 'percentage')]}"/>
|
||||
<div attrs="{'invisible': ['|', ('reward_type', 'in', ('product', 'free_shipping')), ('discount_type', '!=', 'percentage')]}">
|
||||
<field name="discount_max_amount" class="oe_inline" widget='monetary' options="{'currency_field': 'currency_id'}"/>
|
||||
|
||||
Reference in New Issue
Block a user