summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2018-11-16 14:27:04 +0100
committerChristophe Simonis <chs@odoo.com>2018-11-16 14:27:04 +0100
commit5ac7557df2bb7dde5cd1067c58bdfab550eb19c8 (patch)
treecc228c0c415499b50941f9cca86ec593594b36d7
parent251be0905b0c362c489cb47becf62bf39d7c8599 (diff)
parent89ed115fe05ba22273e79ff6dbb1678f6d38a402 (diff)
[MERGE] forward port branch saas-11.3 up to a9f3cf76b93
-rw-r--r--models/sale_coupon_program.py12
-rw-r--r--models/sale_order.py54
-rw-r--r--tests/test_program_numbers.py41
-rw-r--r--tests/test_program_rules.py69
4 files changed, 161 insertions, 15 deletions
diff --git a/models/sale_coupon_program.py b/models/sale_coupon_program.py
index 544bca39b24..c20f3386ce9 100644
--- a/models/sale_coupon_program.py
+++ b/models/sale_coupon_program.py
@@ -229,10 +229,9 @@ class SaleCouponProgram(models.Model):
valid_products = program._get_valid_products(products)
ordered_rule_products_qty = sum(products_qties[product] for product in valid_products)
# Avoid program if 1 ordered foo on a program '1 foo, 1 free foo'
- if program._is_valid_product(program.reward_product_id) and program.reward_type == 'product':
- line = order.order_line.filtered(lambda line: line.product_id == program.reward_product_id)
+ if program.promo_applicability == 'on_current_order' and \
+ program._is_valid_product(program.reward_product_id) and program.reward_type == 'product':
ordered_rule_products_qty -= program.reward_product_quantity
- # needed_quantity = program.rule_min_quantity if self.
if ordered_rule_products_qty >= program.rule_min_quantity:
valid_programs |= program
return valid_programs
@@ -267,7 +266,12 @@ class SaleCouponProgram(models.Model):
# Product requirement should not be checked if the coupon got generated by a promotion program (the requirement should have only be checked to generate the coupon)
if not next_order:
programs = programs and programs._filter_programs_on_products(order)
- programs = programs and programs._filter_not_ordered_reward_programs(order)
+
+ programs_curr_order = programs.filtered(lambda p: p.promo_applicability == 'on_current_order')
+ programs = programs.filtered(lambda p: p.promo_applicability == 'on_next_order')
+ if programs_curr_order:
+ # Checking if rewards are in the SO should not be performed for rewards on_next_order
+ programs += programs_curr_order._filter_not_ordered_reward_programs(order)
return programs
def _is_valid_partner(self, partner):
diff --git a/models/sale_order.py b/models/sale_order.py
index 83cfb7f24ca..7702b93bb0c 100644
--- a/models/sale_order.py
+++ b/models/sale_order.py
@@ -217,13 +217,24 @@ class SaleOrder(models.Model):
self.write({'order_line': [(0, False, value) for value in self._get_reward_line_values(program)]})
def _create_reward_coupon(self, program):
- coupon = self.env['sale.coupon'].create({
- 'program_id': program.id,
- 'state': 'reserved',
- 'partner_id': self.partner_id.id,
- 'order_id': self.id,
- 'discount_line_product_id': program.discount_line_product_id.id
- })
+ # if there is already a coupon that was set as expired, reactivate that one instead of creating a new one
+ coupon = self.env['sale.coupon'].search([
+ ('program_id', '=', program.id),
+ ('state', '=', 'expired'),
+ ('partner_id', '=', self.partner_id.id),
+ ('order_id', '=', self.id),
+ ('discount_line_product_id', '=', program.discount_line_product_id.id),
+ ], limit=1)
+ if coupon:
+ coupon.write({'state': 'reserved'})
+ else:
+ coupon = self.env['sale.coupon'].create({
+ 'program_id': program.id,
+ 'state': 'reserved',
+ 'partner_id': self.partner_id.id,
+ 'order_id': self.id,
+ 'discount_line_product_id': program.discount_line_product_id.id
+ })
self.generated_coupon_ids |= coupon
return coupon
@@ -258,8 +269,17 @@ class SaleOrder(models.Model):
return programs
def _get_applied_coupon_program_coming_from_another_so(self):
+ # TODO: Remove me in master as no more used
+ pass
+
+ def _get_valid_applied_coupon_program(self):
self.ensure_one()
- programs = self.applied_coupon_ids.mapped('program_id')._filter_programs_from_common_rules(self, True)
+ # applied_coupon_ids's coupons might be coming from:
+ # * a coupon generated from a previous order that benefited from a promotion_program that rewarded the next sale order.
+ # In that case requirements to benefit from the program (Quantity and price) should not be checked anymore
+ # * a coupon_program, in that case the promo_applicability is always for the current order and everything should be checked (filtered)
+ programs = self.applied_coupon_ids.mapped('program_id').filtered(lambda p: p.promo_applicability == 'on_next_order')._filter_programs_from_common_rules(self, True)
+ programs += self.applied_coupon_ids.mapped('program_id').filtered(lambda p: p.promo_applicability == 'on_current_order')._filter_programs_from_common_rules(self)
return programs
def _create_new_no_code_promo_reward_lines(self):
@@ -334,18 +354,26 @@ class SaleOrder(models.Model):
self.ensure_one()
order = self
- applicable_programs = order._get_applicable_no_code_promo_program() + order._get_applicable_programs() + order._get_applied_coupon_program_coming_from_another_so()
- applied_programs = order._get_applied_programs_with_rewards_on_current_order()
+ applicable_programs = order._get_applicable_no_code_promo_program() + order._get_applicable_programs() + order._get_valid_applied_coupon_program()
+ applied_programs = order._get_applied_programs_with_rewards_on_current_order() + order._get_applied_programs_with_rewards_on_next_order()
programs_to_remove = applied_programs - applicable_programs
products_to_remove = programs_to_remove.mapped('discount_line_product_id')
# delete reward line coming from an archived coupon (it will never be updated/removed when recomputing the order)
invalid_lines = order.order_line.filtered(lambda line: line.is_reward_line and line.product_id.id not in (applied_programs).mapped('discount_line_product_id').ids)
+ # Invalid generated coupon for which we are not eligible anymore ('expired' since it is specific to this SO and we may again met the requirements)
self.generated_coupon_ids.filtered(lambda coupon: coupon.program_id.discount_line_product_id.id in products_to_remove.ids).write({'state': 'expired'})
+ # Reset applied coupons for which we are not eligible anymore ('valid' so it can be use on another )
+ coupons_to_remove = order.applied_coupon_ids.filtered(lambda coupon: coupon.program_id in programs_to_remove)
+ coupons_to_remove.write({'state': 'new'})
+
+ # Unbind promotion and coupon programs which requirements are not met anymore
order.no_code_promo_program_ids -= programs_to_remove
order.code_promo_program_id -= programs_to_remove
- order.applied_coupon_ids -= order.applied_coupon_ids.filtered(lambda coupon: coupon.program_id in programs_to_remove)
+ order.applied_coupon_ids -= coupons_to_remove
+
+ # Remove their reward lines
invalid_lines |= order.order_line.filtered(lambda line: line.product_id.id in products_to_remove.ids)
invalid_lines.unlink()
@@ -358,6 +386,10 @@ class SaleOrder(models.Model):
self.applied_coupon_ids.mapped('program_id') + \
self.code_promo_program_id.filtered(lambda p: p.promo_applicability == 'on_current_order')
+ def _get_applied_programs_with_rewards_on_next_order(self):
+ return self.no_code_promo_program_ids.filtered(lambda p: p.promo_applicability == 'on_next_order') + \
+ self.code_promo_program_id.filtered(lambda p: p.promo_applicability == 'on_next_order')
+
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
diff --git a/tests/test_program_numbers.py b/tests/test_program_numbers.py
index 656ddcaad6f..550b3b66447 100644
--- a/tests/test_program_numbers.py
+++ b/tests/test_program_numbers.py
@@ -490,3 +490,44 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
fixed_amount_program.write({'active': False}) # Check archived product will remove discount lines on recompute
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 1, "Archiving the program should remove the program reward line")
+
+ def test_program_next_order(self):
+ order = self.empty_order
+ self.env['sale.coupon.program'].create({
+ 'name': 'Free Keyboard if at least 1 article',
+ 'promo_code_usage': 'no_code_needed',
+ 'promo_applicability': 'on_next_order',
+ 'program_type': 'promotion_program',
+ 'reward_type': 'product',
+ 'reward_product_id': self.wirelessKeyboard.id,
+ 'rule_min_quantity': 2,
+ })
+ sol1 = self.env['sale.order.line'].create({
+ 'product_id': self.iPadMini.id,
+ 'name': 'iPad Mini',
+ 'product_uom_qty': 1.0,
+ 'order_id': order.id,
+ })
+ order.recompute_coupon_lines()
+ self.assertEqual(len(order.order_line.ids), 1, "Nothing should be added to the cart")
+ self.assertEqual(len(order.generated_coupon_ids), 0, "No coupon should have been generated yet")
+
+ sol1.product_uom_qty = 2
+ order.recompute_coupon_lines()
+ generated_coupon = order.generated_coupon_ids
+ self.assertEqual(len(order.order_line.ids), 1, "Nothing should be added to the cart (2)")
+ self.assertEqual(len(generated_coupon), 1, "A coupon should have been generated")
+ self.assertEqual(generated_coupon.state, 'reserved', "The coupon should be reserved")
+
+ sol1.product_uom_qty = 1
+ order.recompute_coupon_lines()
+ generated_coupon = order.generated_coupon_ids
+ self.assertEqual(len(order.order_line.ids), 1, "Nothing should be added to the cart (3)")
+ self.assertEqual(len(generated_coupon), 1, "No more coupon should have been generated and the existing one should not have been deleted")
+ self.assertEqual(generated_coupon.state, 'expired', "The coupon should have been set as expired as it is no more valid since we don't have the required quantity")
+
+ sol1.product_uom_qty = 2
+ order.recompute_coupon_lines()
+ 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")
diff --git a/tests/test_program_rules.py b/tests/test_program_rules.py
index 181b5adb07e..727f71bcadc 100644
--- a/tests/test_program_rules.py
+++ b/tests/test_program_rules.py
@@ -4,6 +4,7 @@
from datetime import datetime, timedelta
from openerp.addons.sale_coupon.tests.common import TestSaleCouponCommon
+from odoo.exceptions import UserError
from odoo.fields import Date
class TestProgramRules(TestSaleCouponCommon):
@@ -171,3 +172,71 @@ class TestProgramRules(TestSaleCouponCommon):
]})
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 2, "The promo offert shouldn't have been applied as the number of uses is exceeded")
+
+ def test_program_rules_coupon_qty_and_amount_remove_not_eligible(self):
+ ''' This test will:
+ * Check quantity and amount requirements works as expected (since it's slightly different from a promotion_program)
+ * Ensure that if a reward from a coupon_program was allowed and the conditions are not met anymore,
+ the reward will be removed on recompute.
+ '''
+ self.immediate_promotion_program.active = False # Avoid having this program to add rewards on this test
+ order = self.empty_order
+
+ program = self.env['sale.coupon.program'].create({
+ 'name': 'Get 10% discount if buy at least 4 Product A and $320',
+ 'program_type': 'coupon_program',
+ 'reward_type': 'discount',
+ 'discount_type': 'percentage',
+ 'discount_percentage': 10.0,
+ 'rule_products_domain': "[('id', 'in', [%s])]" % (self.product_A.id),
+ 'rule_min_quantity': 3,
+ 'rule_minimum_amount': 320.00,
+ })
+
+ sol1 = self.env['sale.order.line'].create({
+ 'product_id': self.product_A.id,
+ 'name': 'Product A',
+ 'product_uom_qty': 2.0,
+ 'order_id': order.id,
+ })
+
+ sol2 = self.env['sale.order.line'].create({
+ 'product_id': self.product_B.id,
+ 'name': 'Product B',
+ 'product_uom_qty': 4.0,
+ 'order_id': order.id,
+ })
+
+ # Default value for coupon generate wizard is generate by quantity and generate only one coupon
+ self.env['sale.coupon.generate'].with_context(active_id=program.id).create({}).generate_coupon()
+ coupon = program.coupon_ids[0]
+
+ # Not enough amount since we only have 220 (100*2 + 5*4)
+ with self.assertRaises(UserError):
+ self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
+ 'coupon_code': coupon.code
+ }).process_coupon()
+
+ sol2.product_uom_qty = 24
+
+ # Not enough qty since we only have 3 Product A (Amount is ok: 100*2 + 5*24 = 320)
+ with self.assertRaises(UserError):
+ self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
+ 'coupon_code': coupon.code
+ }).process_coupon()
+
+ sol1.product_uom_qty = 3
+
+ self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
+ 'coupon_code': coupon.code
+ }).process_coupon()
+ order.recompute_coupon_lines()
+
+ self.assertEqual(len(order.order_line.ids), 3, "The order should contains the Product A line, the Product B line and the discount line")
+ self.assertEqual(coupon.state, 'used', "The coupon should be set to Consumed as it has been used")
+
+ sol1.product_uom_qty = 2
+ order.recompute_coupon_lines()
+
+ self.assertEqual(len(order.order_line.ids), 2, "The discount line should have been removed as we don't meet the program requirements")
+ self.assertEqual(coupon.state, 'new', "The coupon should be reset to Valid as it's reward got removed")