[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:
Romain Derie
2019-08-13 09:21:52 +00:00
co-authored by Pratima Gupta
parent 779e47caf3
commit e87cec8c49
7 changed files with 131 additions and 24 deletions
@@ -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':
+4 -4
View File
@@ -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'}"/>