Skip to content

Commit

Permalink
[IMP] (pos_,sale_)loyalty: improve validity period
Browse files Browse the repository at this point in the history
currently it is only possible to define the end_date that
does not allow to prepare the program in advance.
Added the start_date property to the program model.
task-3305712

closes #121193

Signed-off-by: Victor Feyens (vfe) <vfe@odoo.com>
  • Loading branch information
vchu-odoo committed Jun 12, 2023
1 parent 500c7ab commit bce4aed
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 49 deletions.
16 changes: 15 additions & 1 deletion addons/loyalty/models/loyalty_program.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,14 @@ class LoyaltyProgram(models.Model):
('next_order_coupons', 'Next Order Coupons')],
default='promotion', required=True,
)
date_to = fields.Date(string='Validity')
date_from = fields.Date(
string="Start Date",
help="The start date is included in the validity period of this program",
)
date_to = fields.Date(
string="End date",
help="The end date is included in the validity period of this program",
)
limit_usage = fields.Boolean(string='Limit Usage')
max_usage = fields.Integer()
# Dictates when the points can be used:
Expand Down Expand Up @@ -93,6 +100,13 @@ class LoyaltyProgram(models.Model):
'Max usage must be strictly positive if a limit is used.'),
]

@api.constrains('date_from', 'date_to')
def _check_date_from_date_to(self):
if any(p.date_to and p.date_from and p.date_from > p.date_to for p in self):
raise UserError(_(
"The validity period's start date must be anterior or equal to its end date."
))

@api.constrains('reward_ids')
def _constrains_reward_ids(self):
if self.env.context.get('loyalty_skip_reward_check'):
Expand Down
1 change: 1 addition & 0 deletions addons/loyalty/views/loyalty_program_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
<field name="applies_on" string="Use points on" groups="base.group_no_one" widget="radio" readonly="1" force_save="1"/>
</group>
<group>
<field name="date_from" attrs="{'invisible': [('program_type', 'in', ('gift_card', 'ewallet'))]}"/>
<field name="date_to" attrs="{'invisible': [('program_type', 'in', ('gift_card', 'ewallet'))]}"/>
<label for="limit_usage" attrs="{'invisible': [('program_type', 'in', ('gift_card', 'ewallet'))]}"/>
<span attrs="{'invisible': [('program_type', 'in', ('gift_card', 'ewallet'))]}">
Expand Down
30 changes: 19 additions & 11 deletions addons/pos_loyalty/models/pos_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,22 +87,30 @@ def use_coupon_code(self, code, creation_date, partner_id):
},
}
check_date = fields.Date.from_string(creation_date[:11])
if (coupon.expiration_date and coupon.expiration_date < check_date) or\
(coupon.program_id.date_to and coupon.program_id.date_to < fields.Date.context_today(self)) or\
(coupon.program_id.limit_usage and coupon.program_id.total_order_count >= coupon.program_id.max_usage):
return {
'successful': False,
'payload': {
'error_message': _('This coupon is expired (%s).', code),
},
}
if not coupon.program_id.reward_ids or not any(reward.required_points <= coupon.points for reward in coupon.program_id.reward_ids):
today_date = fields.Date.context_today(self)
error_message = False
if (
(coupon.expiration_date and coupon.expiration_date < check_date)
or (coupon.program_id.date_to and coupon.program_id.date_to < today_date)
or (coupon.program_id.limit_usage and coupon.program_id.total_order_count >= coupon.program_id.max_usage)
):
error_message = _("This coupon is expired (%s).", code)
elif coupon.program_id.date_from and coupon.program_id.date_from > today_date:
error_message = _("This coupon is not yet valid (%s).", code)
elif (
not coupon.program_id.reward_ids or
not any(r.required_points <= coupon.points for r in coupon.program_id.reward_ids)
):
error_message = _("No reward can be claimed with this coupon.")

if error_message:
return {
'successful': False,
'payload': {
'error_message': _('No reward can be claimed with this coupon.'),
'error_message': error_message,
},
}

return {
'successful': True,
'payload': {
Expand Down
2 changes: 1 addition & 1 deletion addons/pos_loyalty/models/pos_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def _loader_params_loyalty_program(self):
return {
'search_params': {
'domain': [('id', 'in', self.config_id._get_program_ids().ids)],
'fields': ['name', 'trigger', 'applies_on', 'program_type', 'date_to',
'fields': ['name', 'trigger', 'applies_on', 'program_type', 'date_from', 'date_to',
'limit_usage', 'max_usage', 'is_nominative', 'portal_visible', 'portal_point_name', 'trigger_product_ids'],
},
}
Expand Down
8 changes: 7 additions & 1 deletion addons/pos_loyalty/static/src/js/Loyalty.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ patch(PosGlobalState.prototype, "pos_loyalty.PosGlobalState", {

for (const program of this.programs) {
this.program_by_id[program.id] = program;
if (program.date_from) {
program.date_from = new Date(program.date_from);
}
if (program.date_to) {
program.date_to = new Date(program.date_to);
}
Expand Down Expand Up @@ -868,7 +871,10 @@ patch(Order.prototype, "pos_loyalty.Order", {
if (program.is_nominative && !this.get_partner()) {
return false;
}
if (program.date_to && program.date_to <= new Date()) {
if (program.date_from && program.date_from > new Date()) {
return false;
}
if (program.date_to && program.date_to < new Date()) {
return false;
}
if (program.limit_usage && program.total_order_count >= program.max_usage) {
Expand Down
11 changes: 7 additions & 4 deletions addons/sale_loyalty/models/sale_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,18 +381,22 @@ def _get_program_domain(self):
Returns the base domain that all programs have to comply to.
"""
self.ensure_one()
today = fields.Date.context_today(self)
return [('active', '=', True), ('sale_ok', '=', True),
('company_id', 'in', (self.company_id.id, False)),
'|', ('date_to', '=', False), ('date_to', '>=', fields.Date.context_today(self))]
'|', ('date_from', '=', False), ('date_from', '<=', today),
'|', ('date_to', '=', False), ('date_to', '>=', today)]

def _get_trigger_domain(self):
"""
Returns the base domain that all triggers have to comply to.
"""
self.ensure_one()
today = fields.Date.context_today(self)
return [('active', '=', True), ('program_id.sale_ok', '=', True),
('company_id', 'in', (self.company_id.id, False)),
'|', ('program_id.date_to', '=', False), ('program_id.date_to', '>=', fields.Date.context_today(self))]
'|', ('program_id.date_from', '=', False), ('program_id.date_from', '<=', today),
'|', ('program_id.date_to', '=', False), ('program_id.date_to', '>=', today)]

def _get_applicable_program_points(self, domain=None):
"""
Expand Down Expand Up @@ -991,8 +995,7 @@ def _try_apply_code(self, code):

if not program or not program.active:
return {'error': _('This code is invalid (%s).', code), 'not_found': True}
elif (program.limit_usage and program.total_order_count >= program.max_usage) or\
(program.date_to and program.date_to < fields.Date.context_today(self)):
elif (program.limit_usage and program.total_order_count >= program.max_usage):
return {'error': _('This code is expired (%s).', code)}

# Rule will count the next time the points are updated
Expand Down
102 changes: 71 additions & 31 deletions addons/sale_loyalty/tests/test_program_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from datetime import date, timedelta

from odoo import Command
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
from odoo.exceptions import ValidationError

Expand Down Expand Up @@ -206,62 +207,100 @@ def test_program_rules_promotion_use_best(self):
self.assertEqual(len(discounts), 1, "The order should contains the Product A line and a discount")
self.assertTrue('Discount: 10% on your order' in discounts.pop(), "The discount should be a 10% discount")

def test_program_rules_validity_dates_and_uses(self):
# Test case: Based on the validity dates and the number of allowed uses

self.immediate_promotion_program.write({
'date_to': date.today() - timedelta(days=2),
'limit_usage': True,
'max_usage': 1,
})

def test_program_rules_validity_dates(self):
# Test date_to (no date_from)
today = date.today()
past_day = today - timedelta(days=2)
future_day = today + timedelta(days=2)
self.immediate_promotion_program.write({'date_to': past_day})
order = self.empty_order
order.write({'order_line': [
(0, False, {
Command.create({
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
}),
(0, False, {
Command.create({
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
self._auto_rewards(order, self.immediate_promotion_program)
self.assertEqual(len(order.order_line.ids), 2, "The promo offer shouldn't have been applied we're not between the validity dates")
msg = "The promo shouldn't have been applied as it is expired."
self.assertEqual(len(order.order_line.ids), 2, msg)

self.immediate_promotion_program.write({'date_to': future_day})
self._auto_rewards(order, self.immediate_promotion_program)
msg = "The promo should have been applied we're between the validity dates."
self.assertEqual(len(order.order_line.ids), 3, msg)

# Test date_from (no date_to)
self.immediate_promotion_program.write({
'date_to': date.today() + timedelta(days=2),
'date_from': future_day, 'date_to': False,
})
order = self.env['sale.order'].create({'partner_id': self.steve.id})
self._auto_rewards(order, self.immediate_promotion_program)
msg = "The promo shouldn't have been applied as it is not active yet."
self.assertEqual(len(order.order_line.ids), 2, msg)

self.immediate_promotion_program.write({'date_from': past_day})
self._auto_rewards(order, self.immediate_promotion_program)
msg = "The promo should have been applied we're between the validity dates."
self.assertEqual(len(order.order_line.ids), 3, msg)

# Test date_from and date_to
self.immediate_promotion_program.write({'date_from': past_day, 'date_to': future_day})
self._auto_rewards(order, self.immediate_promotion_program)
msg = "The promo should have been applied as we're between the validity dates"
self.assertEqual(len(order.order_line.ids), 3, msg)

self.immediate_promotion_program.write({
'date_from': today + timedelta(days=1),
'date_to': future_day,
})
self._auto_rewards(order, self.immediate_promotion_program)
msg = "The promo offer shouldn't have been applied as it is not active yet."
self.assertEqual(len(order.order_line.ids), 2, msg)

self.immediate_promotion_program.write({
'date_from': past_day,
'date_to': today - timedelta(days=1),
})
self._auto_rewards(order, self.immediate_promotion_program)
msg = "The promo offer shouldn't have been applied as it is expired."
self.assertEqual(len(order.order_line.ids), 2, msg)

self.immediate_promotion_program.write({'date_from': today, 'date_to': today})
self._auto_rewards(order, self.immediate_promotion_program)
msg = "The promo should have been applied as today is a valid starting and ending date."
self.assertEqual(len(order.order_line.ids), 3, msg)

def test_program_rules_number_of_uses(self):
# Test case: Based on the number of allowed uses
self.immediate_promotion_program.write({
'limit_usage': True,
'max_usage': 1,
})
order = self.empty_order
order.write({'order_line': [
(0, False, {
Command.create({
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 10.0,
}),
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
self._auto_rewards(order, self.immediate_promotion_program)
self.assertEqual(len(order.order_line.ids), 3, "The promo offer should have been applied as we're between the validity dates")
order = self.env['sale.order'].create({'partner_id': self.env['res.partner'].create({'name': 'My Partner'}).id})
self.assertEqual(len(order.order_line.ids), 2, "The promo offer should have been applied")

order = self.env['sale.order'].create({
'partner_id': self.env['res.partner'].create({'name': 'My Partner'}).id
})

order.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 10.0,
}),
(0, False, {
Command.create({
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
Expand All @@ -271,4 +310,5 @@ def test_program_rules_validity_dates_and_uses(self):
# Invalidate total_order_count
self.immediate_promotion_program.invalidate_recordset(['order_count', 'total_order_count'])
self._auto_rewards(order, self.immediate_promotion_program)
self.assertEqual(len(order.order_line.ids), 2, "The promo offer shouldn't have been applied as the number of uses is exceeded")
msg = "The promo offer shouldn't have been applied as the number of uses is exceeded"
self.assertEqual(len(order.order_line.ids), 1, msg)

0 comments on commit bce4aed

Please sign in to comment.