summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorRomain Derie <rde@odoo.com>2019-07-29 16:00:04 +0000
committerRomain Derie <rde@odoo.com>2019-08-13 09:21:52 +0000
commite87cec8c4960f8286226fd15ec0fae4e4612b47f (patch)
treeeacf35e9e448082c9b3a18f0b8308e71a551ff08 /addons
parent779e47caf368fa9d488eb307219bd3342809bf06 (diff)
[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>
Diffstat (limited to 'addons')
-rw-r--r--addons/sale_coupon/data/sale_coupon_email_data.xml21
-rw-r--r--addons/sale_coupon/models/sale_coupon_program.py6
-rw-r--r--addons/sale_coupon/models/sale_coupon_reward.py15
-rw-r--r--addons/sale_coupon/models/sale_order.py8
-rw-r--r--addons/sale_coupon/report/sale_coupon_report_templates.xml15
-rw-r--r--addons/sale_coupon/tests/test_program_numbers.py88
-rw-r--r--addons/sale_coupon/views/sale_coupon_program_views.xml2
7 files changed, 131 insertions, 24 deletions
diff --git a/addons/sale_coupon/data/sale_coupon_email_data.xml b/addons/sale_coupon/data/sale_coupon_email_data.xml
index afe32281dce..9d385b9272c 100644
--- a/addons/sale_coupon/data/sale_coupon_email_data.xml
+++ b/addons/sale_coupon/data/sale_coupon_email_data.xml
@@ -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/>
diff --git a/addons/sale_coupon/models/sale_coupon_program.py b/addons/sale_coupon/models/sale_coupon_program.py
index 8ce426ee98c..bbdb4807070 100644
--- a/addons/sale_coupon/models/sale_coupon_program.py
+++ b/addons/sale_coupon/models/sale_coupon_program.py
@@ -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
diff --git a/addons/sale_coupon/models/sale_coupon_reward.py b/addons/sale_coupon/models/sale_coupon_reward.py
index e7af055055d..57450c1d17d 100644
--- a/addons/sale_coupon/models/sale_coupon_reward.py
+++ b/addons/sale_coupon/models/sale_coupon_reward.py
@@ -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':
diff --git a/addons/sale_coupon/models/sale_order.py b/addons/sale_coupon/models/sale_order.py
index 01183d35cbe..8a7a8913795 100644
--- a/addons/sale_coupon/models/sale_order.py
+++ b/addons/sale_coupon/models/sale_order.py
@@ -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)
diff --git a/addons/sale_coupon/report/sale_coupon_report_templates.xml b/addons/sale_coupon/report/sale_coupon_report_templates.xml
index 10f6fe32591..1bdd96ad56d 100644
--- a/addons/sale_coupon/report/sale_coupon_report_templates.xml
+++ b/addons/sale_coupon/report/sale_coupon_report_templates.xml
@@ -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/>
diff --git a/addons/sale_coupon/tests/test_program_numbers.py b/addons/sale_coupon/tests/test_program_numbers.py
index 9edc5d06989..31dc651118b 100644
--- a/addons/sale_coupon/tests/test_program_numbers.py
+++ b/addons/sale_coupon/tests/test_program_numbers.py
@@ -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")
diff --git a/addons/sale_coupon/views/sale_coupon_program_views.xml b/addons/sale_coupon/views/sale_coupon_program_views.xml
index ffd15643321..f6c72b23e4d 100644
--- a/addons/sale_coupon/views/sale_coupon_program_views.xml
+++ b/addons/sale_coupon/views/sale_coupon_program_views.xml
@@ -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'}"/>