Files
odoo_source/addons/coupon/models/coupon_program.py
T
Nicolas Lempereur ec0fc57462 [FIX] sale_coupon: prevent program w/o company
The company is used to compute currency of:

- discount_fixed_amount
- discount_max_amount
- rule_minimum_amount

If currently we don't set a company, we have an error when using the
company.

opw-2458950

note: forward-port in master set the company field as required setting
it as required in the view.

closes odoo/odoo#66231

X-original-commit: d260dddcdeddecd2bc0649d948f5e0cc30607e09
Signed-off-by: Nicolas Lempereur (nle) <nle@odoo.com>
2021-02-16 11:22:56 +00:00

169 lines
8.1 KiB
Python

# -*- 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", required=True, 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")
total_order_count = fields.Integer("Total Order Count", compute="_compute_total_order_count")
@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
@api.ondelete(at_uninstall=False)
def _unlink_except_active(self):
if self.filtered('active'):
raise UserError(_('You can not delete a program in active state'))
def unlink(self):
# 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 _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 _generate_coupons(self, partner_id):
'''Generate coupons that can be used in the next order for the given partner_id.'''
generated_coupons = self.env['coupon.coupon']
for program in self:
generated_coupons |= self.env['coupon.coupon'].create({
'program_id': program.id,
'partner_id': partner_id,
})
return generated_coupons
def _compute_total_order_count(self):
for program in self:
program.total_order_count = 0
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
}