The purpose of this task is to reflect the coupon discount perfectly
from sale order to invoice. This issue is already fixed in 13.0 in
commit: bb5d999 but during refactor of coupon it was removed due to
module split.
So in this commit, improve the default invoicing policy of coupon product.
The default invoicing policy should always be by ordered quantity.
Otherwise, if the user changes the default invoicing policy to
"delivered quantity", coupon won't be applied to the invoice.
closes odoo/odoo#65845
Taskid: 2303244
X-original-commit: 98a3ca3274e9829666a773d5fe79f2e695524f51
Signed-off-by: Victor Feyens (vfe) <vfe@odoo.com>
169 lines
8.1 KiB
Python
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", 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
|
|
}
|