1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
|
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
import ast
class CouponProgram(models.Model):
_name = 'coupon.program'
_description = "Coupon Program"
_inherits = {'coupon.rule': 'rule_id', 'coupon.reward': 'reward_id'}
# We should apply 'discount' promotion first to avoid offering free product when we should not.
# Eg: If the discount lower the SO total below the required threshold
# Note: This is only revelant when programs have the same sequence (which they have by default)
_order = "sequence, reward_type"
name = fields.Char(required=True, translate=True)
active = fields.Boolean('Active', default=True, help="A program is available for the customers when active")
rule_id = fields.Many2one('coupon.rule', string="Coupon Rule", ondelete='restrict', required=True)
reward_id = fields.Many2one('coupon.reward', string="Reward", ondelete='restrict', required=True, copy=False)
sequence = fields.Integer(copy=False,
help="Coupon program will be applied based on given sequence if multiple programs are " +
"defined on same condition(For minimum amount)")
maximum_use_number = fields.Integer(help="Maximum number of sales orders in which reward can be provided")
program_type = fields.Selection([
('promotion_program', 'Promotional Program'),
('coupon_program', 'Coupon Program'),
],
help="""A promotional program can be either a limited promotional offer without code (applied automatically)
or with a code (displayed on a magazine for example) that may generate a discount on the current
order or create a coupon for a next order.
A coupon program generates coupons with a code that can be used to generate a discount on the current
order or create a coupon for a next order.""")
promo_code_usage = fields.Selection([
('no_code_needed', 'Automatically Applied'),
('code_needed', 'Use a code')],
help="Automatically Applied - No code is required, if the program rules are met, the reward is applied (Except the global discount or the free shipping rewards which are not cumulative)\n" +
"Use a code - If the program rules are met, a valid code is mandatory for the reward to be applied\n")
promo_code = fields.Char('Promotion Code', copy=False,
help="A promotion code is a code that is associated with a marketing discount. For example, a retailer might tell frequent customers to enter the promotion code 'THX001' to receive a 10%% discount on their whole order.")
promo_applicability = fields.Selection([
('on_current_order', 'Apply On Current Order'),
('on_next_order', 'Send a Coupon')],
default='on_current_order', string="Applicability")
coupon_ids = fields.One2many('coupon.coupon', 'program_id', string="Generated Coupons", copy=False)
coupon_count = fields.Integer(compute='_compute_coupon_count')
company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.company)
currency_id = fields.Many2one(string="Currency", related='company_id.currency_id', readonly=True)
validity_duration = fields.Integer(default=30,
help="Validity duration for a coupon after its generation")
@api.constrains('promo_code')
def _check_promo_code_constraint(self):
""" Program code must be unique """
for program in self.filtered(lambda p: p.promo_code):
domain = [('id', '!=', program.id), ('promo_code', '=', program.promo_code)]
if self.search(domain):
raise ValidationError(_('The program code must be unique!'))
@api.depends('coupon_ids')
def _compute_coupon_count(self):
coupon_data = self.env['coupon.coupon'].read_group([('program_id', 'in', self.ids)], ['program_id'], ['program_id'])
mapped_data = dict([(m['program_id'][0], m['program_id_count']) for m in coupon_data])
for program in self:
program.coupon_count = mapped_data.get(program.id, 0)
@api.onchange('promo_code_usage')
def _onchange_promo_code_usage(self):
if self.promo_code_usage == 'no_code_needed':
self.promo_code = False
@api.onchange('reward_product_id')
def _onchange_reward_product_id(self):
if self.reward_product_id:
self.reward_product_uom_id = self.reward_product_id.uom_id
@api.onchange('discount_type')
def _onchange_discount_type(self):
if self.discount_type == 'fixed_amount':
self.discount_apply_on = 'on_order'
@api.model
def create(self, vals):
program = super(CouponProgram, self).create(vals)
if not vals.get('discount_line_product_id', False):
values = program._get_discount_product_values()
discount_line_product_id = self.env['product.product'].create(values)
program.write({'discount_line_product_id': discount_line_product_id.id})
return program
def write(self, vals):
res = super(CouponProgram, self).write(vals)
reward_fields = [
'reward_type', 'reward_product_id', 'discount_type', 'discount_percentage',
'discount_apply_on', 'discount_specific_product_ids', 'discount_fixed_amount'
]
if any(field in reward_fields for field in vals):
self.mapped('discount_line_product_id').write({'name': self[0].reward_id.display_name})
return res
def unlink(self):
if self.filtered('active'):
raise UserError(_('You can not delete a program in active state'))
# get reference to rule and reward
rule = self.rule_id
reward = self.reward_id
# unlink the program
super(CouponProgram, self).unlink()
# then unlink the rule and reward
rule.unlink()
reward.unlink()
return True
def toggle_active(self):
super(CouponProgram, self).toggle_active()
for program in self:
program.discount_line_product_id.active = program.active
coupons = self.filtered(lambda p: not p.active and p.promo_code_usage == 'code_needed').mapped('coupon_ids')
coupons.filtered(lambda x: x.state != 'used').write({'state': 'expired'})
def _compute_program_amount(self, field, currency_to):
self.ensure_one()
return self.currency_id._convert(self[field], currency_to, self.company_id, fields.Date.today())
def _is_valid_partner(self, partner):
if self.rule_partners_domain and self.rule_partners_domain != '[]':
domain = ast.literal_eval(self.rule_partners_domain) + [('id', '=', partner.id)]
return bool(self.env['res.partner'].search_count(domain))
else:
return True
def _is_valid_product(self, product):
# NOTE: if you override this method, think of also overriding _get_valid_products
# we also encourage the use of _get_valid_products as its execution is faster
if self.rule_products_domain:
domain = ast.literal_eval(self.rule_products_domain) + [('id', '=', product.id)]
return bool(self.env['product.product'].search_count(domain))
else:
return True
def _get_valid_products(self, products):
if self.rule_products_domain:
domain = ast.literal_eval(self.rule_products_domain)
return products.filtered_domain(domain)
return products
def _get_discount_product_values(self):
return {
'name': self.reward_id.display_name,
'type': 'service',
'taxes_id': False,
'supplier_taxes_id': False,
'sale_ok': False,
'purchase_ok': False,
'lst_price': 0, #Do not set a high value to avoid issue with coupon code
}
|