[IMP] (*_)loyalty: restrict programs to specific pricelist(s)
When specified, restrict the promotion to customers using said pricelists task-3049956 closes odoo/odoo#107656 Signed-off-by: Victor Feyens (vfe) <vfe@odoo.com> Co-authored-by: Lucas Vermulen (luve) <luve@odoo.com> Co-authored-by: Morgane Demesmaeker (edm) <edm@odoo.com>
This commit is contained in:
committed by
Victor Feyens
co-authored by
Morgane Demesmaeker
parent
bbe22683b5
commit
a3ee978f1b
@@ -21,6 +21,9 @@ class LoyaltyProgram(models.Model):
|
||||
currency_id = fields.Many2one('res.currency', 'Currency', compute='_compute_currency_id',
|
||||
readonly=False, required=True, store=True, precompute=True)
|
||||
currency_symbol = fields.Char(related='currency_id.symbol')
|
||||
pricelist_ids = fields.Many2many(
|
||||
'product.pricelist', string="Pricelist", domain="[('currency_id', '=', currency_id)]"
|
||||
)
|
||||
|
||||
total_order_count = fields.Integer("Total Order Count", compute="_compute_total_order_count")
|
||||
|
||||
@@ -100,6 +103,17 @@ class LoyaltyProgram(models.Model):
|
||||
'Max usage must be strictly positive if a limit is used.'),
|
||||
]
|
||||
|
||||
@api.constrains('currency_id', 'pricelist_ids')
|
||||
def _check_pricelist_currency(self):
|
||||
if any(
|
||||
pricelist.currency_id != program.currency_id
|
||||
for program in self
|
||||
for pricelist in program.pricelist_ids
|
||||
):
|
||||
raise UserError(_(
|
||||
"The loyalty program's currency must be the same as all it's pricelists ones."
|
||||
))
|
||||
|
||||
@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):
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
<field name="mail_template_id" attrs="{'invisible': [('program_type', 'not in', ('gift_card', 'ewallet'))]}"/>
|
||||
<field name="currency_id"/>
|
||||
<field name="currency_symbol" invisible="1"/>
|
||||
<field name="pricelist_ids" widget="many2many_tags" groups="product.group_product_pricelist"/>
|
||||
<field name="portal_point_name" attrs="{'invisible': [('program_type', 'in', ('loyalty', 'gift_card', 'ewallet'))]}" string="Points Unit" groups="base.group_no_one"/>
|
||||
<field name="portal_point_name" attrs="{'invisible': ['|', ('program_type', 'in', ('gift_card', 'ewallet')), ('program_type', '!=', 'loyalty')]}" string="Points Unit"/>
|
||||
<field name="portal_visible" invisible="1"/>
|
||||
|
||||
@@ -71,7 +71,7 @@ class PosConfig(models.Model):
|
||||
|
||||
return super()._check_before_creating_new_session()
|
||||
|
||||
def use_coupon_code(self, code, creation_date, partner_id):
|
||||
def use_coupon_code(self, code, creation_date, partner_id, pricelist_id):
|
||||
self.ensure_one()
|
||||
# Ordering by partner id to use the first assigned to the partner in case multiple coupons have the same code
|
||||
# it could happen with loyalty programs using a code
|
||||
@@ -79,7 +79,8 @@ class PosConfig(models.Model):
|
||||
coupon = self.env['loyalty.card'].search(
|
||||
[('program_id', 'in', self._get_program_ids().ids), ('partner_id', 'in', (False, partner_id)), ('code', '=', code)],
|
||||
order='partner_id, points desc', limit=1)
|
||||
if not coupon or not coupon.program_id.active:
|
||||
program = coupon.program_id
|
||||
if not coupon or not program.active:
|
||||
return {
|
||||
'successful': False,
|
||||
'payload': {
|
||||
@@ -91,17 +92,19 @@ class PosConfig(models.Model):
|
||||
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)
|
||||
or (program.date_to and program.date_to < today_date)
|
||||
or (program.limit_usage and program.total_order_count >= program.max_usage)
|
||||
):
|
||||
error_message = _("This coupon is expired (%s).", code)
|
||||
elif coupon.program_id.date_from and coupon.program_id.date_from > today_date:
|
||||
elif program.date_from and program.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)
|
||||
not program.reward_ids or
|
||||
not any(r.required_points <= coupon.points for r in program.reward_ids)
|
||||
):
|
||||
error_message = _("No reward can be claimed with this coupon.")
|
||||
elif program.pricelist_ids and pricelist_id not in program.pricelist_ids.ids:
|
||||
error_message = _("This coupon is not available with the current pricelist.")
|
||||
|
||||
if error_message:
|
||||
return {
|
||||
@@ -114,7 +117,7 @@ class PosConfig(models.Model):
|
||||
return {
|
||||
'successful': True,
|
||||
'payload': {
|
||||
'program_id': coupon.program_id.id,
|
||||
'program_id': program.id,
|
||||
'coupon_id': coupon.id,
|
||||
'coupon_partner_id': coupon.partner_id.id,
|
||||
'points': coupon.points,
|
||||
|
||||
@@ -21,8 +21,11 @@ class PosSession(models.Model):
|
||||
return {
|
||||
'search_params': {
|
||||
'domain': [('id', 'in', self.config_id._get_program_ids().ids)],
|
||||
'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'],
|
||||
'fields': [
|
||||
'name', 'trigger', 'applies_on', 'program_type', 'pricelist_ids', 'date_from',
|
||||
'date_to', 'limit_usage', 'max_usage', 'is_nominative', 'portal_visible',
|
||||
'portal_point_name', 'trigger_product_ids',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -163,6 +163,11 @@ patch(PosStore.prototype, "pos_loyalty.PosStore", {
|
||||
const result = [];
|
||||
for (const couponProgram of allCouponPrograms) {
|
||||
const program = this.program_by_id[couponProgram.program_id];
|
||||
if (program.pricelist_ids.length > 0
|
||||
&& (!order.pricelist || !program.pricelist_ids.includes(order.pricelist.id))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const points = order._getRealCouponPoints(couponProgram.coupon_id);
|
||||
const hasLine = order.orderlines.filter((line) => !line.is_reward_line).length > 0;
|
||||
for (const reward of program.rewards.filter(
|
||||
|
||||
@@ -301,7 +301,23 @@ patch(Order.prototype, "pos_loyalty.Order", {
|
||||
return orderLines[orderLines.length - 1];
|
||||
},
|
||||
set_pricelist(pricelist) {
|
||||
const oldPricelist = this.pricelist
|
||||
this._super(...arguments);
|
||||
if (this.couponPointChanges && oldPricelist !== pricelist) {
|
||||
// Remove couponPointChanges for cards in no longer available programs.
|
||||
// This makes sure that counting of points on loyalty and ewallet programs is updated after pricelist changes.
|
||||
const loyaltyProgramIds = new Set(
|
||||
this.pos.programs
|
||||
.filter((program) => program.pricelist_ids.length > 0
|
||||
&& (!pricelist || !program.pricelist_ids.includes(pricelist.id)))
|
||||
.map((program) => program.id)
|
||||
);
|
||||
for (const [key, pointChange] of Object.entries(this.couponPointChanges)) {
|
||||
if (loyaltyProgramIds.has(pointChange.program_id)) {
|
||||
delete this.couponPointChanges[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
this._updateRewards();
|
||||
},
|
||||
set_orderline_options(line, options) {
|
||||
@@ -701,6 +717,9 @@ patch(Order.prototype, "pos_loyalty.Order", {
|
||||
if (program.limit_usage && program.total_order_count >= program.max_usage) {
|
||||
return false;
|
||||
}
|
||||
if (program.pricelist_ids.length > 0 && (!this.pricelist || !program.pricelist_ids.includes(this.pricelist.id))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
@@ -929,6 +948,10 @@ patch(Order.prototype, "pos_loyalty.Order", {
|
||||
: 0;
|
||||
for (const couponProgram of allCouponPrograms) {
|
||||
const program = this.pos.program_by_id[couponProgram.program_id];
|
||||
if (program.pricelist_ids.length > 0
|
||||
&& (!this.pricelist || !program.pricelist_ids.includes(this.pricelist.id))) {
|
||||
continue;
|
||||
}
|
||||
if (program.trigger == "with_code") {
|
||||
// For coupon programs, the rules become conditions.
|
||||
// Points to purchase rewards will only come from the scanned coupon.
|
||||
@@ -1471,6 +1494,11 @@ patch(Order.prototype, "pos_loyalty.Order", {
|
||||
let claimableRewards = null;
|
||||
let coupon = null;
|
||||
if (rule) {
|
||||
let program_pricelists = rule.program_id.pricelist_ids;
|
||||
if (program_pricelists.length > 0
|
||||
&& (!this.pricelist || !program_pricelists.includes(this.pricelist.id))) {
|
||||
return _t("That promo code program requires a specific pricelist.");
|
||||
}
|
||||
if (this.codeActivatedProgramRules.includes(rule.id)) {
|
||||
return _t("That promo code program has already been activated.");
|
||||
}
|
||||
@@ -1481,11 +1509,11 @@ patch(Order.prototype, "pos_loyalty.Order", {
|
||||
if (this.codeActivatedCoupons.find((coupon) => coupon.code === code)) {
|
||||
return _t("That coupon code has already been scanned and activated.");
|
||||
}
|
||||
const customer = this.get_partner();
|
||||
const customerId = this.get_partner() ? this.get_partner().id : false;
|
||||
const { successful, payload } = await this.env.services.orm.call(
|
||||
"pos.config",
|
||||
"use_coupon_code",
|
||||
[[this.pos.config.id], code, this.creation_date, customer ? customer.id : false]
|
||||
[[this.pos.config.id], code, this.creation_date, customerId, this.pricelist ? this.pricelist.id : false]
|
||||
);
|
||||
if (successful) {
|
||||
// Allow rejecting a gift card that is not yet paid.
|
||||
|
||||
@@ -386,6 +386,7 @@ class SaleOrder(models.Model):
|
||||
today = fields.Date.context_today(self)
|
||||
return [('active', '=', True), ('sale_ok', '=', True),
|
||||
('company_id', 'in', (self.company_id.id, False)),
|
||||
'|', ('pricelist_ids', '=', False), ('pricelist_ids', 'in', [self.pricelist_id.id]),
|
||||
'|', ('date_from', '=', False), ('date_from', '<=', today),
|
||||
'|', ('date_to', '=', False), ('date_to', '>=', today)]
|
||||
|
||||
@@ -397,6 +398,8 @@ class SaleOrder(models.Model):
|
||||
today = fields.Date.context_today(self)
|
||||
return [('active', '=', True), ('program_id.sale_ok', '=', True),
|
||||
('company_id', 'in', (self.company_id.id, False)),
|
||||
'|', ('program_id.pricelist_ids', '=', False),
|
||||
('program_id.pricelist_ids', 'in', [self.pricelist_id.id]),
|
||||
'|', ('program_id.date_from', '=', False), ('program_id.date_from', '<=', today),
|
||||
'|', ('program_id.date_to', '=', False), ('program_id.date_to', '>=', today)]
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import tagged, new_test_user
|
||||
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
|
||||
from odoo.tools.float_utils import float_compare
|
||||
from odoo import Command
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests import tagged, new_test_user
|
||||
from odoo.tools.float_utils import float_compare
|
||||
|
||||
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestLoyalty(TestSaleCouponCommon):
|
||||
@@ -339,3 +342,133 @@ class TestLoyalty(TestSaleCouponCommon):
|
||||
order._update_programs_and_rewards()
|
||||
self._claim_reward(order, coupon_program)
|
||||
self.assertEqual(float_compare(order.amount_total, 218.7, precision_rounding=3), 0, "300 * 0.9 * 0.9 * 0.9 = 218.7")
|
||||
|
||||
def test_promotion_program_restricted_to_pricelists(self):
|
||||
self.env['product.pricelist'].search([]).action_archive()
|
||||
company_currency = self.env.company.currency_id
|
||||
pricelist_1, pricelist_2 = self.env['product.pricelist'].create([
|
||||
{'name': 'Basic company_currency pricelist', 'currency_id': company_currency.id},
|
||||
{'name': 'Other company_currency pricelist', 'currency_id': company_currency.id},
|
||||
])
|
||||
self.immediate_promotion_program.active = True
|
||||
order = self.empty_order.copy()
|
||||
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': 1.0,
|
||||
}),
|
||||
(0, False, {
|
||||
'product_id': self.product_B.id,
|
||||
'name': '2 Product B',
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 1.0,
|
||||
}),
|
||||
]})
|
||||
|
||||
applied_message = "The promo offer should have been applied."
|
||||
not_applied_message = "The promo offer should not have been applied because the order's " \
|
||||
"pricelist is not eligible to this promotion."
|
||||
|
||||
order.pricelist_id = self.env['product.pricelist']
|
||||
order._update_programs_and_rewards()
|
||||
self._claim_reward(order, self.immediate_promotion_program)
|
||||
self.assertEqual(len(order.order_line.ids), 3, applied_message)
|
||||
|
||||
order.pricelist_id = pricelist_1
|
||||
order._update_programs_and_rewards()
|
||||
self._claim_reward(order, self.immediate_promotion_program)
|
||||
self.assertEqual(len(order.order_line.ids), 3, applied_message)
|
||||
|
||||
self.immediate_promotion_program.pricelist_ids = [pricelist_1.id]
|
||||
order.pricelist_id = self.env['product.pricelist']
|
||||
order._update_programs_and_rewards()
|
||||
self._claim_reward(order, self.immediate_promotion_program)
|
||||
self.assertEqual(len(order.order_line.ids), 2, not_applied_message)
|
||||
|
||||
order.pricelist_id = pricelist_1
|
||||
order._update_programs_and_rewards()
|
||||
self._claim_reward(order, self.immediate_promotion_program)
|
||||
self.assertEqual(len(order.order_line.ids), 3, applied_message)
|
||||
|
||||
order.pricelist_id = pricelist_2
|
||||
order._update_programs_and_rewards()
|
||||
self._claim_reward(order, self.immediate_promotion_program)
|
||||
self.assertEqual(len(order.order_line.ids), 2, not_applied_message)
|
||||
|
||||
self.immediate_promotion_program.pricelist_ids = [pricelist_1.id, pricelist_2.id]
|
||||
order.pricelist_id = self.env['product.pricelist']
|
||||
order._update_programs_and_rewards()
|
||||
self._claim_reward(order, self.immediate_promotion_program)
|
||||
self.assertEqual(len(order.order_line.ids), 2, not_applied_message)
|
||||
|
||||
order.pricelist_id = pricelist_1
|
||||
order._update_programs_and_rewards()
|
||||
self._claim_reward(order, self.immediate_promotion_program)
|
||||
self.assertEqual(len(order.order_line.ids), 3, applied_message)
|
||||
|
||||
def test_coupon_program_restricted_to_pricelists(self):
|
||||
self.env['product.pricelist'].search([]).action_archive()
|
||||
company_currency = self.env.company.currency_id
|
||||
pricelist_1, pricelist_2 = self.env['product.pricelist'].create([
|
||||
{'name': 'Basic company_currency pricelist', 'currency_id': company_currency.id},
|
||||
{'name': 'Other company_currency pricelist', 'currency_id': company_currency.id},
|
||||
])
|
||||
|
||||
self.code_promotion_program.active = True
|
||||
self.env['loyalty.generate.wizard'].with_context(
|
||||
active_id=self.code_promotion_program.id
|
||||
).create({'coupon_qty': 7, 'points_granted': 1}).generate_coupons()
|
||||
coupons = self.code_promotion_program.coupon_ids
|
||||
|
||||
order_no_pricelist = self.empty_order.copy()
|
||||
order_no_pricelist.write({'pricelist_id': None, 'order_line': [
|
||||
(0, False, {
|
||||
'product_id': self.product_A.id,
|
||||
'name': '1 Product A',
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 1.0,
|
||||
}),
|
||||
]})
|
||||
order_pricelist_1 = order_no_pricelist.copy()
|
||||
order_pricelist_1.pricelist_id = pricelist_1
|
||||
order_pricelist_2 = order_no_pricelist.copy()
|
||||
order_pricelist_2.pricelist_id = pricelist_2
|
||||
|
||||
applied_message = "The coupon code should have been applied."
|
||||
not_applied_message = "The coupon code should not have been applied because the order's " \
|
||||
"pricelist is not eligible to this promotion."
|
||||
|
||||
order_0 = order_no_pricelist.copy()
|
||||
self._apply_promo_code(order_0, coupons[0].code)
|
||||
self.assertEqual(len(order_0.order_line.ids), 2, applied_message)
|
||||
|
||||
order_1 = order_pricelist_1.copy()
|
||||
self._apply_promo_code(order_1, coupons[1].code)
|
||||
self.assertEqual(len(order_1.order_line.ids), 2, applied_message)
|
||||
|
||||
self.code_promotion_program.pricelist_ids = [pricelist_1.id]
|
||||
order_2 = order_no_pricelist.copy()
|
||||
with self.assertRaises(ValidationError):
|
||||
self._apply_promo_code(order_2, coupons[2].code)
|
||||
self.assertEqual(len(order_2.order_line.ids), 1, not_applied_message)
|
||||
|
||||
order_3 = order_pricelist_1.copy()
|
||||
self._apply_promo_code(order_3, coupons[3].code)
|
||||
self.assertEqual(len(order_3.order_line.ids), 2, applied_message)
|
||||
|
||||
order_4 = order_pricelist_2.copy()
|
||||
with self.assertRaises(ValidationError):
|
||||
self._apply_promo_code(order_4, coupons[4].code)
|
||||
self.assertEqual(len(order_4.order_line.ids), 1, not_applied_message)
|
||||
|
||||
self.code_promotion_program.pricelist_ids = [pricelist_1.id, pricelist_2.id]
|
||||
order_5 = order_no_pricelist.copy()
|
||||
with self.assertRaises(ValidationError):
|
||||
self._apply_promo_code(order_5, coupons[5].code)
|
||||
self.assertEqual(len(order_5.order_line.ids), 1, not_applied_message)
|
||||
|
||||
order_6 = order_pricelist_1.copy()
|
||||
self._apply_promo_code(order_6, coupons[6].code)
|
||||
self.assertEqual(len(order_6.order_line.ids), 2, applied_message)
|
||||
|
||||
Reference in New Issue
Block a user