mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-22 12:51:58 +02:00
19.0 vanilla
This commit is contained in:
parent
d1963a3c3a
commit
2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions
|
|
@ -1,10 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import loyalty_card
|
||||
from . import loyalty_history
|
||||
from . import loyalty_mail
|
||||
from . import loyalty_program
|
||||
from . import loyalty_reward
|
||||
from . import loyalty_rule
|
||||
from . import loyalty_program
|
||||
from . import product_pricelist
|
||||
from . import product_product
|
||||
from . import product_template
|
||||
from . import res_partner
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from uuid import uuid4
|
||||
|
|
@ -11,7 +10,7 @@ from odoo.tools import format_amount
|
|||
class LoyaltyCard(models.Model):
|
||||
_name = 'loyalty.card'
|
||||
_inherit = ['mail.thread']
|
||||
_description = 'Loyalty Coupon'
|
||||
_description = "Loyalty Coupon"
|
||||
_rec_name = 'code'
|
||||
|
||||
@api.model
|
||||
|
|
@ -19,35 +18,50 @@ class LoyaltyCard(models.Model):
|
|||
"""
|
||||
Barcode identifiable codes.
|
||||
"""
|
||||
return '044' + str(uuid4())[7:-18]
|
||||
return "044" + str(uuid4())[7:-18]
|
||||
|
||||
def name_get(self):
|
||||
return [(card.id, f'{card.program_id.name}: {card.code}') for card in self]
|
||||
@api.depends('program_id', 'code')
|
||||
def _compute_display_name(self):
|
||||
for card in self:
|
||||
card.display_name = f"{card.program_id.name}: {card.code}"
|
||||
|
||||
program_id = fields.Many2one('loyalty.program', ondelete='restrict', default=lambda self: self.env.context.get('active_id', None))
|
||||
program_id = fields.Many2one(
|
||||
comodel_name='loyalty.program',
|
||||
ondelete='restrict',
|
||||
index='btree_not_null',
|
||||
default=lambda self: self.env.context.get('active_id', None),
|
||||
)
|
||||
program_type = fields.Selection(related='program_id.program_type')
|
||||
company_id = fields.Many2one(related='program_id.company_id', store=True)
|
||||
# TODO probably isn't useful to store this company_id anymore
|
||||
company_id = fields.Many2one(related='program_id.company_id', store=True, precompute=True)
|
||||
currency_id = fields.Many2one(related='program_id.currency_id')
|
||||
# Reserved for this partner if non-empty
|
||||
partner_id = fields.Many2one('res.partner', index=True)
|
||||
partner_id = fields.Many2one(comodel_name='res.partner', index=True)
|
||||
points = fields.Float(tracking=True)
|
||||
point_name = fields.Char(related='program_id.portal_point_name', readonly=True)
|
||||
points_display = fields.Char(compute='_compute_points_display')
|
||||
|
||||
code = fields.Char(default=lambda self: self._generate_code(), required=True)
|
||||
code = fields.Char(required=True, default=lambda self: self._generate_code())
|
||||
expiration_date = fields.Date()
|
||||
|
||||
use_count = fields.Integer(compute='_compute_use_count')
|
||||
active = fields.Boolean(default=True)
|
||||
history_ids = fields.One2many(
|
||||
comodel_name='loyalty.history',
|
||||
inverse_name='card_id',
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
('card_code_unique', 'UNIQUE(code)', 'A coupon/loyalty card must have a unique code.')
|
||||
]
|
||||
_card_code_unique = models.Constraint(
|
||||
'UNIQUE(code)',
|
||||
"A coupon/loyalty card must have a unique code.",
|
||||
)
|
||||
|
||||
@api.constrains('code')
|
||||
def _contrains_code(self):
|
||||
# Prevent a coupon from having the same code a program
|
||||
if self.env['loyalty.rule'].search_count([('mode', '=', 'with_code'), ('code', 'in', self.mapped('code'))]):
|
||||
raise ValidationError(_('A trigger with the same code as one of your coupon already exists.'))
|
||||
raise ValidationError(_("A trigger with the same code as one of your coupon already exists."))
|
||||
|
||||
@api.depends('points', 'point_name')
|
||||
def _compute_points_display(self):
|
||||
|
|
@ -62,7 +76,7 @@ class LoyaltyCard(models.Model):
|
|||
|
||||
def _format_points(self, points):
|
||||
self.ensure_one()
|
||||
if self.point_name == self.program_id.currency_id.symbol:
|
||||
if self.program_id.currency_id and self.point_name == self.program_id.currency_id.symbol:
|
||||
return format_amount(self.env, points, self.program_id.currency_id)
|
||||
if points == int(points):
|
||||
return f"{int(points)} {self.point_name or ''}"
|
||||
|
|
@ -76,10 +90,6 @@ class LoyaltyCard(models.Model):
|
|||
self.ensure_one()
|
||||
return self.program_id.communication_plan_ids.filtered(lambda m: m.trigger == 'create').mail_template_id[:1]
|
||||
|
||||
def _get_mail_partner(self):
|
||||
self.ensure_one()
|
||||
return self.partner_id
|
||||
|
||||
def _get_mail_author(self):
|
||||
self.ensure_one()
|
||||
return (
|
||||
|
|
@ -103,16 +113,14 @@ class LoyaltyCard(models.Model):
|
|||
compose_form = self.env.ref('mail.email_compose_message_wizard_form', False)
|
||||
ctx = dict(
|
||||
default_model='loyalty.card',
|
||||
default_res_id=self.id,
|
||||
default_use_template=bool(default_template),
|
||||
default_res_ids=self.ids,
|
||||
default_template_id=default_template and default_template.id,
|
||||
default_composition_mode='comment',
|
||||
default_email_layout_xmlid='mail.mail_notification_light',
|
||||
mark_coupon_as_sent=True,
|
||||
force_email=True,
|
||||
)
|
||||
return {
|
||||
'name': _('Compose Email'),
|
||||
'name': _("Compose Email"),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'mail.compose.message',
|
||||
|
|
@ -133,7 +141,7 @@ class LoyaltyCard(models.Model):
|
|||
for program in self.program_id:
|
||||
create_comm_per_program[program] = program.communication_plan_ids.filtered(lambda c: c.trigger == 'create')
|
||||
for coupon in self:
|
||||
if not create_comm_per_program[coupon.program_id] or not coupon._get_mail_partner():
|
||||
if not create_comm_per_program[coupon.program_id] or not coupon._mail_get_customer():
|
||||
continue
|
||||
for comm in create_comm_per_program[coupon.program_id]:
|
||||
mail_template = comm.mail_template_id
|
||||
|
|
@ -163,7 +171,7 @@ class LoyaltyCard(models.Model):
|
|||
.filtered(lambda c: c.trigger == 'points_reach')\
|
||||
.sorted('points', reverse=True)
|
||||
for coupon in self:
|
||||
if not coupon._get_mail_partner():
|
||||
if not coupon._mail_get_customer():
|
||||
continue
|
||||
coupon_change = points_changes[coupon]
|
||||
# Do nothing if coupon lost points or did not change
|
||||
|
|
@ -195,3 +203,15 @@ class LoyaltyCard(models.Model):
|
|||
points_changes = {coupon: {'old': points_before[coupon], 'new': coupon.points} for coupon in self}
|
||||
self._send_points_reach_communication(points_changes)
|
||||
return res
|
||||
|
||||
def action_loyalty_update_balance(self):
|
||||
return {
|
||||
'name': _("Update Balance"),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'loyalty.card.update.balance',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_card_id': self.id,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class LoyaltyHistory(models.Model):
|
||||
_name = 'loyalty.history'
|
||||
_description = "History for Loyalty cards and Ewallets"
|
||||
_order = 'id desc'
|
||||
|
||||
card_id = fields.Many2one(comodel_name='loyalty.card', required=True, index=True, ondelete='cascade')
|
||||
company_id = fields.Many2one(related='card_id.company_id')
|
||||
|
||||
description = fields.Text(required=True)
|
||||
|
||||
issued = fields.Float()
|
||||
used = fields.Float()
|
||||
|
||||
order_model = fields.Char(readonly=True)
|
||||
order_id = fields.Many2oneReference(model_field='order_model', readonly=True)
|
||||
|
||||
def _get_order_portal_url(self):
|
||||
self.ensure_one()
|
||||
return False
|
||||
|
||||
def _get_order_description(self):
|
||||
self.ensure_one()
|
||||
return self.env[self.order_model].browse(self.order_id).display_name
|
||||
|
|
@ -1,20 +1,31 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
# Allow promo programs to send mails upon certain triggers
|
||||
# Like : 'At creation' and 'When reaching X points'
|
||||
|
||||
|
||||
class LoyaltyMail(models.Model):
|
||||
_name = 'loyalty.mail'
|
||||
_description = 'Loyalty Communication'
|
||||
_description = "Loyalty Communication"
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
program_id = fields.Many2one('loyalty.program', required=True, ondelete='cascade')
|
||||
trigger = fields.Selection([
|
||||
('create', 'At Creation'),
|
||||
('points_reach', 'When Reaching')], string='When', required=True
|
||||
program_id = fields.Many2one(comodel_name='loyalty.program', ondelete='cascade', required=True, index=True)
|
||||
trigger = fields.Selection(
|
||||
string="When",
|
||||
selection=[
|
||||
('create', "At Creation"),
|
||||
('points_reach', "When Reaching")
|
||||
],
|
||||
required=True,
|
||||
)
|
||||
points = fields.Float()
|
||||
mail_template_id = fields.Many2one('mail.template', string="Email Template", required=True, domain=[('model', '=', 'loyalty.card')])
|
||||
mail_template_id = fields.Many2one(
|
||||
string="Email Template",
|
||||
comodel_name='mail.template',
|
||||
ondelete='cascade',
|
||||
domain=[('model', '=', 'loyalty.card')],
|
||||
required=True,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,112 +1,208 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
from uuid import uuid4
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
class LoyaltyProgram(models.Model):
|
||||
_name = 'loyalty.program'
|
||||
_description = 'Loyalty Program'
|
||||
_description = "Loyalty Program"
|
||||
_order = 'sequence'
|
||||
_rec_name = 'name'
|
||||
|
||||
name = fields.Char('Program Name', required=True, translate=True)
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
defaults = super().default_get(fields)
|
||||
program_type = defaults.get('program_type')
|
||||
if program_type:
|
||||
program_default_values = self._program_type_default_values()
|
||||
if program_type in program_default_values:
|
||||
default_values = program_default_values[program_type]
|
||||
defaults.update({k: v for k, v in default_values.items() if k in fields})
|
||||
return defaults
|
||||
|
||||
name = fields.Char(string="Program Name", translate=True, required=True)
|
||||
active = fields.Boolean(default=True)
|
||||
sequence = fields.Integer(copy=False)
|
||||
company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company)
|
||||
currency_id = fields.Many2one('res.currency', 'Currency', compute='_compute_currency_id',
|
||||
readonly=False, required=True, store=True, precompute=True)
|
||||
company_id = fields.Many2one(
|
||||
string="Company", comodel_name='res.company', default=lambda self: self.env.company
|
||||
)
|
||||
currency_id = fields.Many2one(
|
||||
string="Currency",
|
||||
comodel_name='res.currency',
|
||||
compute='_compute_currency_id',
|
||||
precompute=True,
|
||||
store=True,
|
||||
readonly=False,
|
||||
required=True,
|
||||
)
|
||||
currency_symbol = fields.Char(related='currency_id.symbol')
|
||||
pricelist_ids = fields.Many2many(
|
||||
string="Pricelist",
|
||||
help="This program is specific to this pricelist set.",
|
||||
comodel_name='product.pricelist',
|
||||
domain="[('currency_id', '=', currency_id)]",
|
||||
)
|
||||
|
||||
total_order_count = fields.Integer("Total Order Count", compute="_compute_total_order_count")
|
||||
total_order_count = fields.Integer(
|
||||
string="Total Order Count", compute='_compute_total_order_count'
|
||||
)
|
||||
|
||||
rule_ids = fields.One2many('loyalty.rule', 'program_id', 'Conditional rules', copy=True,
|
||||
compute='_compute_from_program_type', readonly=False, store=True)
|
||||
reward_ids = fields.One2many('loyalty.reward', 'program_id', 'Rewards', copy=True,
|
||||
compute='_compute_from_program_type', readonly=False, store=True)
|
||||
communication_plan_ids = fields.One2many('loyalty.mail', 'program_id', copy=True,
|
||||
compute='_compute_from_program_type', readonly=False, store=True)
|
||||
rule_ids = fields.One2many(
|
||||
string="Conditional rules",
|
||||
comodel_name='loyalty.rule',
|
||||
inverse_name='program_id',
|
||||
compute='_compute_from_program_type',
|
||||
store=True,
|
||||
readonly=False,
|
||||
copy=True,
|
||||
)
|
||||
reward_ids = fields.One2many(
|
||||
string="Rewards",
|
||||
comodel_name='loyalty.reward',
|
||||
inverse_name='program_id',
|
||||
compute='_compute_from_program_type',
|
||||
store=True,
|
||||
readonly=False,
|
||||
copy=True,
|
||||
)
|
||||
communication_plan_ids = fields.One2many(
|
||||
comodel_name='loyalty.mail',
|
||||
inverse_name='program_id',
|
||||
compute='_compute_from_program_type',
|
||||
store=True,
|
||||
readonly=False,
|
||||
copy=True,
|
||||
)
|
||||
|
||||
# These fields are used for the simplified view of gift_card and ewallet
|
||||
mail_template_id = fields.Many2one('mail.template', compute='_compute_mail_template_id', inverse='_inverse_mail_template_id', string="Email template", readonly=False)
|
||||
mail_template_id = fields.Many2one(
|
||||
string="Email template",
|
||||
comodel_name='mail.template',
|
||||
compute='_compute_mail_template_id',
|
||||
inverse='_inverse_mail_template_id',
|
||||
readonly=False,
|
||||
)
|
||||
trigger_product_ids = fields.Many2many(related='rule_ids.product_ids', readonly=False)
|
||||
|
||||
coupon_ids = fields.One2many('loyalty.card', 'program_id')
|
||||
coupon_ids = fields.One2many(comodel_name='loyalty.card', inverse_name='program_id')
|
||||
coupon_count = fields.Integer(compute='_compute_coupon_count')
|
||||
coupon_count_display = fields.Char(compute='_compute_coupon_count_display', string="Items")
|
||||
coupon_count_display = fields.Char(string="Items", compute='_compute_coupon_count_display')
|
||||
|
||||
program_type = fields.Selection([
|
||||
('coupons', 'Coupons'),
|
||||
('gift_card', 'Gift Card'),
|
||||
('loyalty', 'Loyalty Cards'),
|
||||
('promotion', 'Promotions'),
|
||||
('ewallet', 'eWallet'),
|
||||
('promo_code', 'Discount Code'),
|
||||
('buy_x_get_y', 'Buy X Get Y'),
|
||||
('next_order_coupons', 'Next Order Coupons')],
|
||||
default='promotion', required=True,
|
||||
program_type = fields.Selection(
|
||||
selection=[
|
||||
('coupons', "Coupons"),
|
||||
('gift_card', "Gift Card"),
|
||||
('loyalty', "Loyalty Cards"),
|
||||
('promotion', "Promotions"),
|
||||
('ewallet', "eWallet"),
|
||||
('promo_code', "Discount Code"),
|
||||
('buy_x_get_y', "Buy X Get Y"),
|
||||
('next_order_coupons', "Next Order Coupons"),
|
||||
],
|
||||
required=True,
|
||||
default='promotion',
|
||||
)
|
||||
date_to = fields.Date(string='Validity')
|
||||
limit_usage = fields.Boolean(string='Limit Usage')
|
||||
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:
|
||||
# current: if the order gives enough points on that order, the reward may directly be claimed, points lost otherwise
|
||||
# future: if the order gives enough points on that order, a coupon is generated for a next order
|
||||
# both: points are accumulated on the coupon to claim rewards, the reward may directly be claimed
|
||||
applies_on = fields.Selection([
|
||||
('current', 'Current order'),
|
||||
('future', 'Future orders'),
|
||||
('both', 'Current & Future orders')], default='current', required=True,
|
||||
compute='_compute_from_program_type', readonly=False, store=True,
|
||||
applies_on = fields.Selection(
|
||||
selection=[
|
||||
('current', "Current order"),
|
||||
('future', "Future orders"),
|
||||
('both', "Current & Future orders"),
|
||||
],
|
||||
compute='_compute_from_program_type',
|
||||
store=True,
|
||||
readonly=False,
|
||||
required=True,
|
||||
default='current',
|
||||
)
|
||||
trigger = fields.Selection([
|
||||
('auto', 'Automatic'),
|
||||
('with_code', 'Use a code')],
|
||||
compute='_compute_from_program_type', readonly=False, store=True,
|
||||
trigger = fields.Selection(
|
||||
help="""
|
||||
Automatic: Customers will be eligible for a reward automatically in their cart.
|
||||
Use a code: Customers will be eligible for a reward if they enter a code.
|
||||
"""
|
||||
""",
|
||||
selection=[('auto', "Automatic"), ('with_code', "Use a code")],
|
||||
compute='_compute_from_program_type',
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
portal_visible = fields.Boolean(default=False,
|
||||
portal_visible = fields.Boolean(
|
||||
help="""
|
||||
Show in web portal, PoS customer ticket, eCommerce checkout, the number of points available and used by reward.
|
||||
""")
|
||||
portal_point_name = fields.Char(default='Points', translate=True,
|
||||
compute='_compute_portal_point_name', readonly=False, store=True)
|
||||
Show in web portal, PoS customer ticket, eCommerce checkout, the number of points available
|
||||
and used by reward.
|
||||
""",
|
||||
default=False,
|
||||
)
|
||||
portal_point_name = fields.Char(
|
||||
translate=True,
|
||||
compute='_compute_portal_point_name',
|
||||
store=True,
|
||||
readonly=False,
|
||||
default='Points',
|
||||
)
|
||||
is_nominative = fields.Boolean(compute='_compute_is_nominative')
|
||||
is_payment_program = fields.Boolean(compute='_compute_is_payment_program')
|
||||
|
||||
payment_program_discount_product_id = fields.Many2one(
|
||||
'product.product',
|
||||
string='Discount Product',
|
||||
string="Discount Product",
|
||||
help="Product used in the sales order to apply the discount.",
|
||||
comodel_name='product.product',
|
||||
compute='_compute_payment_program_discount_product_id',
|
||||
readonly=True,
|
||||
help="Product used in the sales order to apply the discount."
|
||||
)
|
||||
|
||||
# Technical field used for a label
|
||||
available_on = fields.Boolean("Available On", store=False,
|
||||
help="""
|
||||
Manage where your program should be available for use.
|
||||
"""
|
||||
available_on = fields.Boolean(
|
||||
string="Available On",
|
||||
help="Manage where your program should be available for use.",
|
||||
store=False,
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
('check_max_usage', 'CHECK (limit_usage = False OR max_usage > 0)',
|
||||
'Max usage must be strictly positive if a limit is used.'),
|
||||
]
|
||||
_check_max_usage = models.Constraint(
|
||||
'CHECK (limit_usage = False OR max_usage > 0)',
|
||||
"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):
|
||||
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'):
|
||||
return
|
||||
if any(not program.reward_ids for program in self):
|
||||
raise ValidationError(_('A program must have at least one reward.'))
|
||||
raise ValidationError(_("A program must have at least one reward."))
|
||||
|
||||
def _compute_total_order_count(self):
|
||||
self.total_order_count = 0
|
||||
|
|
@ -117,14 +213,14 @@ class LoyaltyProgram(models.Model):
|
|||
for program in self:
|
||||
program.coupon_count_display = "%i %s" % (program.coupon_count or 0, program_items_name[program.program_type] or '')
|
||||
|
||||
@api.depends("communication_plan_ids.mail_template_id")
|
||||
@api.depends('communication_plan_ids.mail_template_id')
|
||||
def _compute_mail_template_id(self):
|
||||
for program in self:
|
||||
program.mail_template_id = program.communication_plan_ids.mail_template_id[:1]
|
||||
|
||||
def _inverse_mail_template_id(self):
|
||||
for program in self:
|
||||
if program.program_type not in ("gift_card", "ewallet"):
|
||||
if program.program_type not in ('gift_card', 'ewallet'):
|
||||
continue
|
||||
if not program.mail_template_id:
|
||||
program.communication_plan_ids = [(5, 0, 0)]
|
||||
|
|
@ -147,8 +243,8 @@ class LoyaltyProgram(models.Model):
|
|||
|
||||
@api.depends('coupon_ids')
|
||||
def _compute_coupon_count(self):
|
||||
read_group_data = self.env['loyalty.card']._read_group([('program_id', 'in', self.ids)], ['program_id'], ['program_id'])
|
||||
count_per_program = {r['program_id'][0]: r['program_id_count'] for r in read_group_data}
|
||||
read_group_data = self.env['loyalty.card']._read_group([('program_id', 'in', self.ids)], ['program_id'], ['__count'])
|
||||
count_per_program = {program.id: count for program, count in read_group_data}
|
||||
for program in self:
|
||||
program.coupon_count = count_per_program.get(program.id, 0)
|
||||
|
||||
|
|
@ -156,7 +252,7 @@ class LoyaltyProgram(models.Model):
|
|||
def _compute_is_nominative(self):
|
||||
for program in self:
|
||||
program.is_nominative = program.applies_on == 'both' or\
|
||||
(program.program_type == 'ewallet' and program.applies_on == 'future')
|
||||
(program.program_type in ('ewallet', 'loyalty') and program.applies_on == 'future')
|
||||
|
||||
@api.depends('program_type')
|
||||
def _compute_is_payment_program(self):
|
||||
|
|
@ -174,30 +270,27 @@ class LoyaltyProgram(models.Model):
|
|||
@api.model
|
||||
def _program_items_name(self):
|
||||
return {
|
||||
'coupons': _('Coupons'),
|
||||
'promotion': _('Promos'),
|
||||
'gift_card': _('Gift Cards'),
|
||||
'loyalty': _('Loyalty Cards'),
|
||||
'ewallet': _('eWallets'),
|
||||
'promo_code': _('Discounts'),
|
||||
'buy_x_get_y': _('Promos'),
|
||||
'next_order_coupons': _('Coupons'),
|
||||
'coupons': _("Coupons"),
|
||||
'promotion': _("Promos"),
|
||||
'gift_card': _("Gift Cards"),
|
||||
'loyalty': _("Loyalty Cards"),
|
||||
'ewallet': _("eWallets"),
|
||||
'promo_code': _("Discounts"),
|
||||
'buy_x_get_y': _("Promos"),
|
||||
'next_order_coupons': _("Coupons"),
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _program_type_default_values(self):
|
||||
# All values to change when program_type changes
|
||||
# NOTE: any field used in `rule_ids`, `reward_ids` and `communication_plan_ids` MUST be present in the kanban view for it to work properly.
|
||||
first_sale_product = self.env['product.product'].search([
|
||||
'|', ('company_id', '=', False), ('company_id', '=', self.company_id.id),
|
||||
('sale_ok', '=', True)
|
||||
], limit=1)
|
||||
first_sale_product = self.env['product.product'].search([('company_id', 'in', [False, self.env.company.id]), ('sale_ok', '=', True)], limit=1)
|
||||
return {
|
||||
'coupons': {
|
||||
'applies_on': 'current',
|
||||
'trigger': 'with_code',
|
||||
'portal_visible': False,
|
||||
'portal_point_name': _('Coupon point(s)'),
|
||||
'portal_point_name': _("Coupon point(s)"),
|
||||
'rule_ids': [(5, 0, 0)],
|
||||
'reward_ids': [(5, 0, 0), (0, 0, {
|
||||
'required_points': 1,
|
||||
|
|
@ -212,7 +305,7 @@ class LoyaltyProgram(models.Model):
|
|||
'applies_on': 'current',
|
||||
'trigger': 'auto',
|
||||
'portal_visible': False,
|
||||
'portal_point_name': _('Promo point(s)'),
|
||||
'portal_point_name': _("Promo point(s)"),
|
||||
'rule_ids': [(5, 0, 0), (0, 0, {
|
||||
'reward_point_amount': 1,
|
||||
'reward_point_mode': 'order',
|
||||
|
|
@ -243,7 +336,7 @@ class LoyaltyProgram(models.Model):
|
|||
'discount': 1,
|
||||
'discount_applicability': 'order',
|
||||
'required_points': 1,
|
||||
'description': _('Gift Card'),
|
||||
'description': _("Gift Card"),
|
||||
})],
|
||||
'communication_plan_ids': [(5, 0, 0), (0, 0, {
|
||||
'trigger': 'create',
|
||||
|
|
@ -254,7 +347,7 @@ class LoyaltyProgram(models.Model):
|
|||
'applies_on': 'both',
|
||||
'trigger': 'auto',
|
||||
'portal_visible': True,
|
||||
'portal_point_name': _('Loyalty point(s)'),
|
||||
'portal_point_name': _("Loyalty point(s)"),
|
||||
'rule_ids': [(5, 0, 0), (0, 0, {
|
||||
'reward_point_mode': 'money',
|
||||
})],
|
||||
|
|
@ -272,6 +365,7 @@ class LoyaltyProgram(models.Model):
|
|||
'rule_ids': [(5, 0, 0), (0, 0, {
|
||||
'reward_point_amount': '1',
|
||||
'reward_point_mode': 'money',
|
||||
'reward_point_split': False,
|
||||
'product_ids': self.env.ref('loyalty.ewallet_product_50', raise_if_not_found=False),
|
||||
})],
|
||||
'reward_ids': [(5, 0, 0), (0, 0, {
|
||||
|
|
@ -280,7 +374,7 @@ class LoyaltyProgram(models.Model):
|
|||
'discount': 1,
|
||||
'discount_applicability': 'order',
|
||||
'required_points': 1,
|
||||
'description': _('eWallet'),
|
||||
'description': _("eWallet"),
|
||||
})],
|
||||
'communication_plan_ids': [(5, 0, 0)],
|
||||
},
|
||||
|
|
@ -288,7 +382,7 @@ class LoyaltyProgram(models.Model):
|
|||
'applies_on': 'current',
|
||||
'trigger': 'with_code',
|
||||
'portal_visible': False,
|
||||
'portal_point_name': _('Discount point(s)'),
|
||||
'portal_point_name': _("Discount point(s)"),
|
||||
'rule_ids': [(5, 0, 0), (0, 0, {
|
||||
'mode': 'with_code',
|
||||
'code': 'PROMO_CODE_' + str(uuid4())[:4], # We should try not to trigger any unicity constraint
|
||||
|
|
@ -306,7 +400,7 @@ class LoyaltyProgram(models.Model):
|
|||
'applies_on': 'current',
|
||||
'trigger': 'auto',
|
||||
'portal_visible': False,
|
||||
'portal_point_name': _('Credit(s)'),
|
||||
'portal_point_name': _("Credit(s)"),
|
||||
'rule_ids': [(5, 0, 0), (0, 0, {
|
||||
'reward_point_mode': 'unit',
|
||||
'product_ids': first_sale_product,
|
||||
|
|
@ -323,7 +417,7 @@ class LoyaltyProgram(models.Model):
|
|||
'applies_on': 'future',
|
||||
'trigger': 'auto',
|
||||
'portal_visible': True,
|
||||
'portal_point_name': _('Coupon point(s)'),
|
||||
'portal_point_name': _("Coupon point(s)"),
|
||||
'rule_ids': [(5, 0, 0), (0, 0, {
|
||||
'minimum_amount': 100,
|
||||
'minimum_qty': 0,
|
||||
|
|
@ -354,7 +448,7 @@ class LoyaltyProgram(models.Model):
|
|||
if program_type in program_type_defaults:
|
||||
programs.write(program_type_defaults[program_type])
|
||||
|
||||
@api.depends("currency_id", "program_type")
|
||||
@api.depends('currency_id', 'program_type')
|
||||
def _compute_portal_point_name(self):
|
||||
for program in self:
|
||||
if program.program_type not in ('ewallet', 'gift_card'):
|
||||
|
|
@ -370,7 +464,7 @@ class LoyaltyProgram(models.Model):
|
|||
domain = rule._get_valid_product_domain()
|
||||
if domain:
|
||||
rule_products[rule] = products.filtered_domain(domain)
|
||||
elif not domain and rule.program_type != "gift_card":
|
||||
elif not domain and rule.program_type != 'gift_card':
|
||||
rule_products[rule] = products
|
||||
else:
|
||||
continue
|
||||
|
|
@ -378,7 +472,7 @@ class LoyaltyProgram(models.Model):
|
|||
|
||||
def action_open_loyalty_cards(self):
|
||||
self.ensure_one()
|
||||
action = self.env['ir.actions.act_window']._for_xml_id("loyalty.loyalty_card_action")
|
||||
action = self.env['ir.actions.act_window']._for_xml_id('loyalty.loyalty_card_action')
|
||||
action['name'] = self._program_items_name()[self.program_type]
|
||||
action['display_name'] = action['name']
|
||||
action['context'] = {
|
||||
|
|
@ -393,17 +487,7 @@ class LoyaltyProgram(models.Model):
|
|||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_active(self):
|
||||
if any(program.active for program in self):
|
||||
raise UserError(_('You can not delete a program in an active state'))
|
||||
|
||||
def toggle_active(self):
|
||||
res = super().toggle_active()
|
||||
# Propagate active state to children
|
||||
for program in self.with_context(active_test=False):
|
||||
program.rule_ids.active = program.active
|
||||
program.reward_ids.active = program.active
|
||||
program.communication_plan_ids.active = program.active
|
||||
program.reward_ids.with_context(active_test=True).discount_line_product_id.active = program.active
|
||||
return res
|
||||
raise UserError(_("You can not delete a program in an active state"))
|
||||
|
||||
def write(self, vals):
|
||||
# There is an issue when we change the program type, since we clear the rewards and create new ones.
|
||||
|
|
@ -416,14 +500,24 @@ class LoyaltyProgram(models.Model):
|
|||
#`loyalty.reward`.
|
||||
if 'program_type' in vals:
|
||||
self = self.with_context(program_type=vals['program_type'])
|
||||
return super().write(vals)
|
||||
res = super().write(vals)
|
||||
else:
|
||||
for program in self:
|
||||
program = program.with_context(program_type=program.program_type)
|
||||
super(LoyaltyProgram, program).write(vals)
|
||||
return True
|
||||
res = True
|
||||
else:
|
||||
return super().write(vals)
|
||||
res = super().write(vals)
|
||||
|
||||
# Propagate active state to children
|
||||
if 'active' in vals:
|
||||
for program in self.with_context(active_test=False):
|
||||
program.rule_ids.active = program.active
|
||||
program.reward_ids.active = program.active
|
||||
program.communication_plan_ids.active = program.active
|
||||
program.reward_ids.with_context(active_test=True).discount_line_product_id.active = program.active
|
||||
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def get_program_templates(self):
|
||||
|
|
@ -435,57 +529,49 @@ class LoyaltyProgram(models.Model):
|
|||
return {
|
||||
'gift_card': {
|
||||
'title': _("Gift Card"),
|
||||
'description': _("Sell Gift Cards, that can be used to purchase products."),
|
||||
'description': _("Sell Gift Cards, that allows to purchase products"),
|
||||
'icon': 'gift_card',
|
||||
},
|
||||
'ewallet': {
|
||||
'title': _("eWallet"),
|
||||
'description': _("Fill in your eWallet, and use it to pay future orders."),
|
||||
'description': _("Fill in your eWallet, to pay future orders"),
|
||||
'icon': 'ewallet',
|
||||
},
|
||||
}
|
||||
return {
|
||||
'promotion': {
|
||||
'title': _("Promotion Program"),
|
||||
'description': _(
|
||||
"Define promotions to apply automatically on your customers' orders."
|
||||
),
|
||||
'title': _("Promotional Program"),
|
||||
'description': _("Automatic promo: 10% off on orders higher than $50"),
|
||||
'icon': 'promotional_program',
|
||||
},
|
||||
'promo_code': {
|
||||
'title': _("Discount Code"),
|
||||
'description': _(
|
||||
"Share a discount code with your customers to create a purchase incentive."
|
||||
),
|
||||
'title': _("Promo Code"),
|
||||
'description': _("Get 10% off on some products, with a code"),
|
||||
'icon': 'promo_code',
|
||||
},
|
||||
'buy_x_get_y': {
|
||||
'title': _("Buy X Get Y"),
|
||||
'description': _(
|
||||
"Offer Y to your customers if they are buying X; for example, 2+1 free."
|
||||
),
|
||||
'description': _("Buy 2 products and get a third one for free"),
|
||||
'icon': '2_plus_1',
|
||||
},
|
||||
'next_order_coupons': {
|
||||
'title': _("Next Order Coupons"),
|
||||
'description': _(
|
||||
"Reward your customers for a purchase with a coupon to use on their next order."
|
||||
),
|
||||
'title': _("Next Order Coupon"),
|
||||
'description': _("Send a coupon after an order, valid for next purchase"),
|
||||
'icon': 'coupons',
|
||||
},
|
||||
'loyalty': {
|
||||
'title': _("Loyalty Cards"),
|
||||
'description': _("Win points with each purchase, and use points to get gifts."),
|
||||
'title': _("Loyalty Card"),
|
||||
'description': _("Win points with each purchase, and claim gifts"),
|
||||
'icon': 'loyalty_cards',
|
||||
},
|
||||
'coupons': {
|
||||
'title': _("Coupons"),
|
||||
'description': _("Generate and share unique coupons with your customers."),
|
||||
'title': _("Coupon"),
|
||||
'description': _("Generate and share unique coupons with your customers"),
|
||||
'icon': 'coupons',
|
||||
},
|
||||
'fidelity': {
|
||||
'title': _("Fidelity Cards"),
|
||||
'description': _("Buy 10 products, and get 10$ discount on the 11th one."),
|
||||
'title': _("Fidelity Card"),
|
||||
'description': _("Buy 10 products to get 10$ off on the 11th one"),
|
||||
'icon': 'fidelity_cards',
|
||||
},
|
||||
}
|
||||
|
|
@ -523,47 +609,47 @@ class LoyaltyProgram(models.Model):
|
|||
product = self.env['product.product'].search([('sale_ok', '=', True)], limit=1)
|
||||
return {
|
||||
'gift_card': {
|
||||
'name': _('Gift Card'),
|
||||
'name': _("Gift Card"),
|
||||
'program_type': 'gift_card',
|
||||
**program_type_defaults['gift_card']
|
||||
},
|
||||
'ewallet': {
|
||||
'name': _('eWallet'),
|
||||
'name': _("eWallet"),
|
||||
'program_type': 'ewallet',
|
||||
**program_type_defaults['ewallet'],
|
||||
},
|
||||
'loyalty': {
|
||||
'name': _('Loyalty Cards'),
|
||||
'name': _("Loyalty Cards"),
|
||||
'program_type': 'loyalty',
|
||||
**program_type_defaults['loyalty'],
|
||||
},
|
||||
'coupons': {
|
||||
'name': _('Coupons'),
|
||||
'name': _("Coupons"),
|
||||
'program_type': 'coupons',
|
||||
**program_type_defaults['coupons'],
|
||||
},
|
||||
'promotion': {
|
||||
'name': _('Promotional Program'),
|
||||
'name': _("Promotional Program"),
|
||||
'program_type': 'promotion',
|
||||
**program_type_defaults['promotion'],
|
||||
},
|
||||
'promo_code': {
|
||||
'name': _('Discount code'),
|
||||
'name': _("Discount code"),
|
||||
'program_type': 'promo_code',
|
||||
**program_type_defaults['promo_code'],
|
||||
},
|
||||
'buy_x_get_y': {
|
||||
'name': _('2+1 Free'),
|
||||
'name': _("2+1 Free"),
|
||||
'program_type': 'buy_x_get_y',
|
||||
**program_type_defaults['buy_x_get_y'],
|
||||
},
|
||||
'next_order_coupons': {
|
||||
'name': _('Next Order Coupons'),
|
||||
'name': _("Next Order Coupons"),
|
||||
'program_type': 'next_order_coupons',
|
||||
**program_type_defaults['next_order_coupons'],
|
||||
},
|
||||
'fidelity': {
|
||||
'name': _('Fidelity Cards'),
|
||||
'name': _("Fidelity Cards"),
|
||||
'program_type': 'loyalty',
|
||||
'applies_on': 'both',
|
||||
'trigger': 'auto',
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import ast
|
||||
import json
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.osv import expression
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.fields import Domain
|
||||
|
||||
|
||||
class LoyaltyReward(models.Model):
|
||||
_name = 'loyalty.reward'
|
||||
_description = 'Loyalty Reward'
|
||||
_description = "Loyalty Reward"
|
||||
_rec_name = 'description'
|
||||
_order = 'required_points asc'
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
def default_get(self, fields):
|
||||
# Try to copy the values of the program types default's
|
||||
result = super().default_get(fields_list)
|
||||
result = super().default_get(fields)
|
||||
if 'program_type' in self.env.context:
|
||||
program_type = self.env.context['program_type']
|
||||
program_default_values = self.env['loyalty.program']._program_type_default_values()
|
||||
|
|
@ -25,7 +25,7 @@ class LoyaltyReward(models.Model):
|
|||
len(program_default_values[program_type]['reward_ids']) == 2 and\
|
||||
isinstance(program_default_values[program_type]['reward_ids'][1][2], dict):
|
||||
result.update({
|
||||
k: v for k, v in program_default_values[program_type]['reward_ids'][1][2].items() if k in fields_list
|
||||
k: v for k, v in program_default_values[program_type]['reward_ids'][1][2].items() if k in fields
|
||||
})
|
||||
return result
|
||||
|
||||
|
|
@ -34,73 +34,116 @@ class LoyaltyReward(models.Model):
|
|||
# and makes sure to display the currency related to the program instead of the company's.
|
||||
symbol = self.env.context.get('currency_symbol', self.env.company.currency_id.symbol)
|
||||
return [
|
||||
('percent', '%'),
|
||||
('per_point', _('%s per point', symbol)),
|
||||
('per_order', _('%s per order', symbol))
|
||||
('percent', "%"),
|
||||
('per_order', symbol),
|
||||
('per_point', _("%s per point", symbol)),
|
||||
]
|
||||
|
||||
def name_get(self):
|
||||
return [(reward.id, '%s - %s' % (reward.program_id.name, reward.description)) for reward in self]
|
||||
@api.depends('program_id', 'description')
|
||||
def _compute_display_name(self):
|
||||
for reward in self:
|
||||
reward.display_name = f"{reward.program_id.name} - {reward.description}"
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
program_id = fields.Many2one('loyalty.program', required=True, ondelete='cascade')
|
||||
program_type = fields.Selection(related="program_id.program_type")
|
||||
program_id = fields.Many2one(comodel_name='loyalty.program', ondelete='cascade', required=True, index=True)
|
||||
program_type = fields.Selection(related='program_id.program_type')
|
||||
# Stored for security rules
|
||||
company_id = fields.Many2one(related='program_id.company_id', store=True)
|
||||
currency_id = fields.Many2one(related='program_id.currency_id')
|
||||
|
||||
description = fields.Char(compute='_compute_description', readonly=False, store=True, translate=True)
|
||||
description = fields.Char(
|
||||
translate=True,
|
||||
compute='_compute_description',
|
||||
precompute=True,
|
||||
store=True,
|
||||
readonly=False,
|
||||
required=True,
|
||||
)
|
||||
|
||||
reward_type = fields.Selection([
|
||||
('product', 'Free Product'),
|
||||
('discount', 'Discount')],
|
||||
default='discount', required=True,
|
||||
reward_type = fields.Selection(
|
||||
selection=[
|
||||
('product', "Free Product"),
|
||||
('discount', "Discount"),
|
||||
],
|
||||
required=True,
|
||||
default='discount',
|
||||
)
|
||||
user_has_debug = fields.Boolean(compute='_compute_user_has_debug')
|
||||
|
||||
# Discount rewards
|
||||
discount = fields.Float('Discount', default=10)
|
||||
discount_mode = fields.Selection(selection=_get_discount_mode_select, required=True, default='percent')
|
||||
discount_applicability = fields.Selection([
|
||||
('order', 'Order'),
|
||||
('cheapest', 'Cheapest Product'),
|
||||
('specific', 'Specific Products')], default='order',
|
||||
discount = fields.Float(string="Discount", default=10)
|
||||
discount_mode = fields.Selection(
|
||||
selection=_get_discount_mode_select, required=True, default='percent'
|
||||
)
|
||||
discount_applicability = fields.Selection(
|
||||
selection=[
|
||||
('order', "Order"),
|
||||
('cheapest', "Cheapest Product"),
|
||||
('specific', "Specific Products"),
|
||||
],
|
||||
default='order',
|
||||
)
|
||||
discount_product_domain = fields.Char(default="[]")
|
||||
discount_product_ids = fields.Many2many('product.product', string="Discounted Products")
|
||||
discount_product_category_id = fields.Many2one('product.category', string="Discounted Prod. Categories")
|
||||
discount_product_tag_id = fields.Many2one('product.tag', string="Discounted Prod. Tag")
|
||||
all_discount_product_ids = fields.Many2many('product.product', compute='_compute_all_discount_product_ids')
|
||||
discount_product_ids = fields.Many2many(
|
||||
string="Discounted Products", comodel_name='product.product'
|
||||
)
|
||||
discount_product_category_id = fields.Many2one(
|
||||
string="Discounted Prod. Categories", comodel_name='product.category'
|
||||
)
|
||||
discount_product_tag_id = fields.Many2one(
|
||||
string="Discounted Prod. Tag", comodel_name='product.tag'
|
||||
)
|
||||
all_discount_product_ids = fields.Many2many(
|
||||
comodel_name='product.product', compute='_compute_all_discount_product_ids'
|
||||
)
|
||||
reward_product_domain = fields.Char(compute='_compute_reward_product_domain', store=False)
|
||||
discount_max_amount = fields.Monetary('Max Discount', 'currency_id',
|
||||
help="This is the max amount this reward may discount, leave to 0 for no limit.")
|
||||
discount_line_product_id = fields.Many2one('product.product', copy=False, ondelete='restrict',
|
||||
help="Product used in the sales order to apply the discount. Each reward has its own product for reporting purpose")
|
||||
discount_max_amount = fields.Monetary(
|
||||
string="Max Discount",
|
||||
help="This is the max amount this reward may discount, leave to 0 for no limit.",
|
||||
)
|
||||
discount_line_product_id = fields.Many2one(
|
||||
help="Product used in the sales order to apply the discount. Each reward has its own"
|
||||
" product for reporting purpose",
|
||||
comodel_name='product.product',
|
||||
ondelete='restrict',
|
||||
copy=False,
|
||||
)
|
||||
is_global_discount = fields.Boolean(compute='_compute_is_global_discount')
|
||||
|
||||
# Product rewards
|
||||
reward_product_id = fields.Many2one('product.product', string='Product')
|
||||
reward_product_tag_id = fields.Many2one('product.tag', string='Product Tag')
|
||||
reward_product_id = fields.Many2one(
|
||||
string="Product", comodel_name='product.product', domain=[('type', '!=', 'combo')]
|
||||
)
|
||||
reward_product_tag_id = fields.Many2one(string="Product Tag", comodel_name='product.tag')
|
||||
multi_product = fields.Boolean(compute='_compute_multi_product')
|
||||
reward_product_ids = fields.Many2many(
|
||||
'product.product', string="Reward Products", compute='_compute_multi_product',
|
||||
string="Reward Products",
|
||||
help="These are the products that can be claimed with this rule.",
|
||||
comodel_name='product.product',
|
||||
compute='_compute_multi_product',
|
||||
search='_search_reward_product_ids',
|
||||
help="These are the products that can be claimed with this rule.")
|
||||
)
|
||||
reward_product_qty = fields.Integer(default=1)
|
||||
reward_product_uom_id = fields.Many2one('uom.uom', compute='_compute_reward_product_uom_id')
|
||||
reward_product_uom_id = fields.Many2one(
|
||||
comodel_name='uom.uom', compute='_compute_reward_product_uom_id'
|
||||
)
|
||||
|
||||
required_points = fields.Float('Points needed', default=1)
|
||||
required_points = fields.Float(string="Points needed", default=1)
|
||||
point_name = fields.Char(related='program_id.portal_point_name', readonly=True)
|
||||
clear_wallet = fields.Boolean(default=False)
|
||||
|
||||
_sql_constraints = [
|
||||
('required_points_positive', 'CHECK (required_points > 0)',
|
||||
'The required points for a reward must be strictly positive.'),
|
||||
('product_qty_positive', "CHECK (reward_type != 'product' OR reward_product_qty > 0)",
|
||||
'The reward product quantity must be strictly positive.'),
|
||||
('discount_positive', "CHECK (reward_type != 'discount' OR discount > 0)",
|
||||
'The discount must be strictly positive.'),
|
||||
]
|
||||
_required_points_positive = models.Constraint(
|
||||
'CHECK (required_points > 0)',
|
||||
"The required points for a reward must be strictly positive.",
|
||||
)
|
||||
_product_qty_positive = models.Constraint(
|
||||
"CHECK (reward_type != 'product' OR reward_product_qty > 0)",
|
||||
"The reward product quantity must be strictly positive.",
|
||||
)
|
||||
_discount_positive = models.Constraint(
|
||||
"CHECK (reward_type != 'discount' OR discount > 0)",
|
||||
"The discount must be strictly positive.",
|
||||
)
|
||||
|
||||
@api.depends('reward_product_id.product_tmpl_id.uom_id', 'reward_product_tag_id')
|
||||
def _compute_reward_product_uom_id(self):
|
||||
|
|
@ -116,17 +159,18 @@ class LoyaltyReward(models.Model):
|
|||
|
||||
def _get_discount_product_domain(self):
|
||||
self.ensure_one()
|
||||
domain = []
|
||||
constrains = []
|
||||
if self.discount_product_ids:
|
||||
domain = [('id', 'in', self.discount_product_ids.ids)]
|
||||
constrains.append([('id', 'in', self.discount_product_ids.ids)])
|
||||
if self.discount_product_category_id:
|
||||
product_category_ids = self._find_all_category_children(self.discount_product_category_id, [])
|
||||
product_category_ids.append(self.discount_product_category_id.id)
|
||||
domain = expression.OR([domain, [('categ_id', 'in', product_category_ids)]])
|
||||
constrains.append([('categ_id', 'in', product_category_ids)])
|
||||
if self.discount_product_tag_id:
|
||||
domain = expression.OR([domain, [('all_product_tag_ids', 'in', self.discount_product_tag_id.id)]])
|
||||
constrains.append([('all_product_tag_ids', 'in', self.discount_product_tag_id.id)])
|
||||
domain = Domain.OR(constrains) if constrains else Domain.TRUE
|
||||
if self.discount_product_domain and self.discount_product_domain != '[]':
|
||||
domain = expression.AND([domain, ast.literal_eval(self.discount_product_domain)])
|
||||
domain &= Domain(ast.literal_eval(self.discount_product_domain))
|
||||
return domain
|
||||
|
||||
@api.model
|
||||
|
|
@ -152,7 +196,7 @@ class LoyaltyReward(models.Model):
|
|||
if compute_all_discount_product == 'enabled':
|
||||
reward.reward_product_domain = "null"
|
||||
else:
|
||||
reward.reward_product_domain = json.dumps(reward._get_discount_product_domain())
|
||||
reward.reward_product_domain = json.dumps(list(reward._get_discount_product_domain()))
|
||||
|
||||
@api.depends('discount_product_ids', 'discount_product_category_id', 'discount_product_tag_id', 'discount_product_domain')
|
||||
def _compute_all_discount_product_ids(self):
|
||||
|
|
@ -166,13 +210,15 @@ class LoyaltyReward(models.Model):
|
|||
@api.depends('reward_product_id', 'reward_product_tag_id', 'reward_type')
|
||||
def _compute_multi_product(self):
|
||||
for reward in self:
|
||||
products = reward.reward_product_id + reward.reward_product_tag_id.product_ids
|
||||
products = reward.reward_product_id + reward.reward_product_tag_id.product_ids.filtered(
|
||||
lambda product: product.type != 'combo'
|
||||
)
|
||||
reward.multi_product = reward.reward_type == 'product' and len(products) > 1
|
||||
reward.reward_product_ids = reward.reward_type == 'product' and products or self.env['product.product']
|
||||
|
||||
def _search_reward_product_ids(self, operator, value):
|
||||
if operator not in ('=', '!=', 'in'):
|
||||
raise NotImplementedError(_("Unsupported search operator"))
|
||||
if operator != 'in':
|
||||
return NotImplemented
|
||||
return [
|
||||
'&', ('reward_type', '=', 'product'),
|
||||
'|', ('reward_product_id', operator, value),
|
||||
|
|
@ -191,57 +237,58 @@ class LoyaltyReward(models.Model):
|
|||
elif reward.reward_type == 'product':
|
||||
products = reward.reward_product_ids
|
||||
if len(products) == 0:
|
||||
reward_string = _('Free Product')
|
||||
reward_string = _("Free Product")
|
||||
elif len(products) == 1:
|
||||
reward_string = _('Free Product - %s', reward.reward_product_id.with_context(display_default_code=False).display_name)
|
||||
reward_string = _("Free Product - %s", reward.reward_product_id.with_context(display_default_code=False).display_name)
|
||||
else:
|
||||
reward_string = _('Free Product - [%s]', ', '.join(products._origin.with_context(display_default_code=False).mapped('display_name')))
|
||||
reward_string = _("Free Product - [%s]", ', '.join(products.with_context(display_default_code=False).mapped('display_name')))
|
||||
elif reward.reward_type == 'discount':
|
||||
format_string = '%(amount)g %(symbol)s'
|
||||
format_string = "%(amount)g %(symbol)s"
|
||||
if reward.currency_id.position == 'before':
|
||||
format_string = '%(symbol)s %(amount)g'
|
||||
format_string = "%(symbol)s %(amount)g"
|
||||
formatted_amount = format_string % {'amount': reward.discount, 'symbol': reward.currency_id.symbol}
|
||||
if reward.discount_mode == 'percent':
|
||||
reward_string = _('%g%% on ', reward.discount)
|
||||
reward_string = _("%g%% on ", reward.discount)
|
||||
elif reward.discount_mode == 'per_point':
|
||||
reward_string = _('%s per point on ', formatted_amount)
|
||||
reward_string = _("%s per point on ", formatted_amount)
|
||||
elif reward.discount_mode == 'per_order':
|
||||
reward_string = _('%s per order on ', formatted_amount)
|
||||
reward_string = _("%s on ", formatted_amount)
|
||||
if reward.discount_applicability == 'order':
|
||||
reward_string += _('your order')
|
||||
reward_string += _("your order")
|
||||
elif reward.discount_applicability == 'cheapest':
|
||||
reward_string += _('the cheapest product')
|
||||
reward_string += _("the cheapest product")
|
||||
elif reward.discount_applicability == 'specific':
|
||||
product_available = self.env['product.product'].search(reward._get_discount_product_domain(), limit=2)
|
||||
if len(product_available) == 1:
|
||||
reward_string += product_available.with_context(display_default_code=False).display_name
|
||||
else:
|
||||
reward_string += _('specific products')
|
||||
reward_string += _("specific products")
|
||||
if reward.discount_max_amount:
|
||||
format_string = '%(amount)g %(symbol)s'
|
||||
format_string = "%(amount)g %(symbol)s"
|
||||
if reward.currency_id.position == 'before':
|
||||
format_string = '%(symbol)s %(amount)g'
|
||||
format_string = "%(symbol)s %(amount)g"
|
||||
formatted_amount = format_string % {'amount': reward.discount_max_amount, 'symbol': reward.currency_id.symbol}
|
||||
reward_string += _(' (Max %s)', formatted_amount)
|
||||
reward_string += _(" (Max %s)", formatted_amount)
|
||||
reward.description = reward_string
|
||||
|
||||
@api.depends('reward_type', 'discount_applicability', 'discount_mode')
|
||||
def _compute_is_global_discount(self):
|
||||
for reward in self:
|
||||
reward.is_global_discount = reward.reward_type == 'discount' and\
|
||||
reward.discount_applicability == 'order' and\
|
||||
reward.discount_mode == 'percent'
|
||||
reward.is_global_discount = (
|
||||
reward.reward_type == 'discount'
|
||||
and reward.discount_applicability == 'order'
|
||||
and reward.discount_mode in ['per_order', 'percent']
|
||||
)
|
||||
|
||||
@api.depends_context('uid')
|
||||
@api.depends("reward_type")
|
||||
@api.depends('reward_type')
|
||||
def _compute_user_has_debug(self):
|
||||
self.user_has_debug = self.user_has_groups('base.group_no_one')
|
||||
self.user_has_debug = self.env.user.has_group('base.group_no_one')
|
||||
|
||||
@api.onchange('description')
|
||||
def _ensure_reward_has_description(self):
|
||||
for reward in self:
|
||||
if not reward.description:
|
||||
raise UserError(_("The reward description field cannot be empty."))
|
||||
@api.constrains('reward_product_id')
|
||||
def _check_reward_product_id_no_combo(self):
|
||||
if any(reward.reward_product_id.type == 'combo' for reward in self):
|
||||
raise ValidationError(_("A reward product can't be of type \"combo\"."))
|
||||
|
||||
def _create_missing_discount_line_products(self):
|
||||
# Make sure we create the product that will be used for our discounts
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import ast
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.osv import expression
|
||||
from odoo.fields import Domain
|
||||
|
||||
|
||||
class LoyaltyRule(models.Model):
|
||||
_name = 'loyalty.rule'
|
||||
_description = 'Loyalty Rule'
|
||||
_description = "Loyalty Rule"
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
def default_get(self, fields):
|
||||
# Try to copy the values of the program types default's
|
||||
result = super().default_get(fields_list)
|
||||
result = super().default_get(fields)
|
||||
if 'program_type' in self.env.context:
|
||||
program_type = self.env.context['program_type']
|
||||
program_default_values = self.env['loyalty.program']._program_type_default_values()
|
||||
|
|
@ -22,7 +22,7 @@ class LoyaltyRule(models.Model):
|
|||
len(program_default_values[program_type]['rule_ids']) == 2 and\
|
||||
isinstance(program_default_values[program_type]['rule_ids'][1][2], dict):
|
||||
result.update({
|
||||
k: v for k, v in program_default_values[program_type]['rule_ids'][1][2].items() if k in fields_list
|
||||
k: v for k, v in program_default_values[program_type]['rule_ids'][1][2].items() if k in fields
|
||||
})
|
||||
return result
|
||||
|
||||
|
|
@ -31,14 +31,14 @@ class LoyaltyRule(models.Model):
|
|||
# and makes sure to display the currency related to the program instead of the company's.
|
||||
symbol = self.env.context.get('currency_symbol', self.env.company.currency_id.symbol)
|
||||
return [
|
||||
('order', _('per order')),
|
||||
('money', _('per %s spent', symbol)),
|
||||
('unit', _('per unit paid')),
|
||||
('order', _("per order")),
|
||||
('money', _("per %s spent", symbol)),
|
||||
('unit', _("per unit paid")),
|
||||
]
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
program_id = fields.Many2one('loyalty.program', required=True, ondelete='cascade')
|
||||
program_type = fields.Selection(related="program_id.program_type")
|
||||
program_id = fields.Many2one(comodel_name='loyalty.program', ondelete='cascade', required=True, index=True)
|
||||
program_type = fields.Selection(related='program_id.program_type')
|
||||
# Stored for security rules
|
||||
company_id = fields.Many2one(related='program_id.company_id', store=True)
|
||||
currency_id = fields.Many2one(related='program_id.currency_id')
|
||||
|
|
@ -47,52 +47,74 @@ class LoyaltyRule(models.Model):
|
|||
user_has_debug = fields.Boolean(compute='_compute_user_has_debug')
|
||||
product_domain = fields.Char(default="[]")
|
||||
|
||||
product_ids = fields.Many2many('product.product', string='Products')
|
||||
product_category_id = fields.Many2one('product.category', string='Categories')
|
||||
product_tag_id = fields.Many2one('product.tag', string='Product Tag')
|
||||
product_ids = fields.Many2many(string="Products", comodel_name='product.product')
|
||||
product_category_id = fields.Many2one(string="Categories", comodel_name='product.category')
|
||||
product_tag_id = fields.Many2one(string="Product Tag", comodel_name='product.tag')
|
||||
|
||||
reward_point_amount = fields.Float(default=1, string="Reward")
|
||||
reward_point_amount = fields.Float(string="Reward", default=1)
|
||||
# Only used for program_id.applies_on == 'future'
|
||||
reward_point_split = fields.Boolean(string='Split per unit', default=False,
|
||||
help="Whether to separate reward coupons per matched unit, only applies to 'future' programs and trigger mode per money spent or unit paid..")
|
||||
reward_point_split = fields.Boolean(
|
||||
string="Split per unit",
|
||||
help="Whether to separate reward coupons per matched unit, only applies to 'future' programs and trigger mode per money spent or unit paid...",
|
||||
default=False,
|
||||
)
|
||||
reward_point_name = fields.Char(related='program_id.portal_point_name', readonly=True)
|
||||
reward_point_mode = fields.Selection(selection=_get_reward_point_mode_selection, required=True, default='order')
|
||||
|
||||
minimum_qty = fields.Integer('Minimum Quantity', default=1)
|
||||
minimum_amount = fields.Monetary('Minimum Purchase', 'currency_id')
|
||||
minimum_amount_tax_mode = fields.Selection([
|
||||
('incl', 'Included'),
|
||||
('excl', 'Excluded')], default='incl', required=True,
|
||||
reward_point_mode = fields.Selection(
|
||||
selection=_get_reward_point_mode_selection, required=True, default='order'
|
||||
)
|
||||
|
||||
mode = fields.Selection([
|
||||
('auto', 'Automatic'),
|
||||
('with_code', 'With a promotion code'),
|
||||
], string="Application", compute='_compute_mode', store=True, readonly=False)
|
||||
code = fields.Char(string='Discount code', compute='_compute_code', store=True, readonly=False)
|
||||
minimum_qty = fields.Integer(string="Minimum Quantity", default=1)
|
||||
minimum_amount = fields.Monetary(string="Minimum Purchase")
|
||||
minimum_amount_tax_mode = fields.Selection(
|
||||
selection=[
|
||||
('incl', "tax included"),
|
||||
('excl', "tax excluded"),
|
||||
],
|
||||
required=True,
|
||||
default='incl',
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
('reward_point_amount_positive', 'CHECK (reward_point_amount > 0)', 'Rule points reward must be strictly positive.'),
|
||||
]
|
||||
mode = fields.Selection(
|
||||
string="Application",
|
||||
selection=[
|
||||
('auto', "Automatic"),
|
||||
('with_code', "With a promotion code"),
|
||||
],
|
||||
compute='_compute_mode',
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
code = fields.Char(string="Discount code", compute='_compute_code', store=True, readonly=False)
|
||||
|
||||
_reward_point_amount_positive = models.Constraint(
|
||||
'CHECK (reward_point_amount > 0)',
|
||||
"Rule points reward must be strictly positive.",
|
||||
)
|
||||
|
||||
@api.constrains('reward_point_split')
|
||||
def _constraint_trigger_multi(self):
|
||||
# Prevent setting trigger multi in case of nominative programs, it does not make sense to allow this
|
||||
for rule in self:
|
||||
if rule.reward_point_split and (rule.program_id.applies_on == 'both' or rule.program_id.program_type == 'ewallet'):
|
||||
raise ValidationError(_('Split per unit is not allowed for Loyalty and eWallet programs.'))
|
||||
raise ValidationError(_("Split per unit is not allowed for Loyalty and eWallet programs."))
|
||||
|
||||
@api.constrains('code')
|
||||
@api.constrains('code', 'active')
|
||||
def _constrains_code(self):
|
||||
mapped_codes = self.filtered('code').mapped('code')
|
||||
mapped_codes = self.filtered(lambda r: r.code and r.active).mapped('code')
|
||||
# Program code must be unique
|
||||
if len(mapped_codes) != len(set(mapped_codes)) or\
|
||||
self.env['loyalty.rule'].search_count(
|
||||
[('mode', '=', 'with_code'), ('code', 'in', mapped_codes), ('id', 'not in', self.ids)]):
|
||||
raise ValidationError(_('The promo code must be unique.'))
|
||||
self.env['loyalty.rule'].search_count([
|
||||
('mode', '=', 'with_code'),
|
||||
('code', 'in', mapped_codes),
|
||||
('id', 'not in', self.ids),
|
||||
('active', '=', True),
|
||||
]):
|
||||
raise ValidationError(_("The promo code must be unique."))
|
||||
# Prevent coupons and programs from sharing a code
|
||||
if self.env['loyalty.card'].search_count([('code', 'in', mapped_codes)]):
|
||||
raise ValidationError(_('A coupon with the same code was found.'))
|
||||
if self.env['loyalty.card'].search_count([
|
||||
('code', 'in', mapped_codes), ('active', '=', True)
|
||||
]):
|
||||
raise ValidationError(_("A coupon with the same code was found."))
|
||||
|
||||
@api.depends('mode')
|
||||
def _compute_code(self):
|
||||
|
|
@ -110,21 +132,22 @@ class LoyaltyRule(models.Model):
|
|||
rule.mode = 'auto'
|
||||
|
||||
@api.depends_context('uid')
|
||||
@api.depends("mode")
|
||||
@api.depends('mode')
|
||||
def _compute_user_has_debug(self):
|
||||
self.user_has_debug = self.user_has_groups('base.group_no_one')
|
||||
self.user_has_debug = self.env.user.has_group('base.group_no_one')
|
||||
|
||||
def _get_valid_product_domain(self):
|
||||
self.ensure_one()
|
||||
domain = []
|
||||
constrains = []
|
||||
if self.product_ids:
|
||||
domain = [('id', 'in', self.product_ids.ids)]
|
||||
constrains.append([('id', 'in', self.product_ids.ids)])
|
||||
if self.product_category_id:
|
||||
domain = expression.OR([domain, [('categ_id', 'child_of', self.product_category_id.id)]])
|
||||
constrains.append([('categ_id', 'child_of', self.product_category_id.id)])
|
||||
if self.product_tag_id:
|
||||
domain = expression.OR([domain, [('all_product_tag_ids', 'in', self.product_tag_id.id)]])
|
||||
constrains.append([('all_product_tag_ids', 'in', self.product_tag_id.id)])
|
||||
domain = Domain.OR(constrains) if constrains else Domain.TRUE
|
||||
if self.product_domain and self.product_domain != '[]':
|
||||
domain = expression.AND([domain, ast.literal_eval(self.product_domain)])
|
||||
domain &= Domain(ast.literal_eval(self.product_domain))
|
||||
return domain
|
||||
|
||||
def _get_valid_products(self):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
from odoo import _, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ProductPricelist(models.Model):
|
||||
_inherit = 'product.pricelist'
|
||||
|
||||
def action_archive(self):
|
||||
loyalty_programs = self.env['loyalty.program'].sudo().search([
|
||||
('active', '=', True),
|
||||
('pricelist_ids', 'in', self.ids)
|
||||
])
|
||||
if loyalty_programs:
|
||||
raise UserError(_(
|
||||
"This pricelist may not be archived. "
|
||||
"It is being used for active promotion programs: %s",
|
||||
', '.join(loyalty_programs.mapped('name'))
|
||||
))
|
||||
return super().action_archive()
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, models
|
||||
|
|
|
|||
|
|
@ -1,13 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
|
||||
from odoo import _, api, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import file_open
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
""" Override of `product` to set a default image for gift cards. """
|
||||
templates = super().create(vals_list)
|
||||
if templates and self.env.context.get('loyalty_is_gift_card_product'):
|
||||
with file_open('loyalty/static/img/gift_card.png', 'rb') as f:
|
||||
gift_card_placeholder = base64.b64encode(f.read())
|
||||
templates.image_1920 = gift_card_placeholder
|
||||
return templates
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_loyalty_products(self):
|
||||
product_data = [
|
||||
|
|
|
|||
41
odoo-bringout-oca-ocb-loyalty/loyalty/models/res_partner.py
Normal file
41
odoo-bringout-oca-ocb-loyalty/loyalty/models/res_partner.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
loyalty_card_count = fields.Integer(
|
||||
string="Active loyalty cards",
|
||||
compute='_compute_count_active_cards',
|
||||
compute_sudo=True,
|
||||
groups='base.group_user')
|
||||
|
||||
def _compute_count_active_cards(self):
|
||||
loyalty_groups = self.env['loyalty.card']._read_group(
|
||||
domain=[
|
||||
'|', ('company_id', '=', False), ('company_id', 'in', self.env.companies.ids),
|
||||
('partner_id', 'in', self.with_context(active_test=False)._search([('id', 'child_of', self.ids)])),
|
||||
('points', '>', '0'),
|
||||
('program_id.active', '=', True),
|
||||
'|',
|
||||
('expiration_date', '>=', fields.Date().context_today(self)),
|
||||
('expiration_date', '=', False),
|
||||
],
|
||||
groupby=['partner_id'],
|
||||
aggregates=['__count'],
|
||||
)
|
||||
self.loyalty_card_count = 0
|
||||
for partner, count in loyalty_groups:
|
||||
while partner:
|
||||
if partner in self:
|
||||
partner.loyalty_card_count += count
|
||||
partner = partner.parent_id
|
||||
|
||||
def action_view_loyalty_cards(self):
|
||||
action = self.env['ir.actions.act_window']._for_xml_id('loyalty.loyalty_card_action')
|
||||
all_child = self.with_context(active_test=False).search([('id', 'child_of', self.ids)])
|
||||
action['domain'] = [('partner_id', 'in', all_child.ids)]
|
||||
action['context'] = {'search_default_active' : True, 'create': False}
|
||||
return action
|
||||
Loading…
Add table
Add a link
Reference in a new issue