[REF] *_sale_coupon: create coupon module

This refactoring extracts parts of `sale_coupon` module to create a more
generic module called `coupon`. This new module can be extended in
different sales modules to implement functionalities specific to modules
that extend it.
This commit is contained in:
Joseph Caburnay
2020-05-08 14:27:29 +00:00
committed by Pierre Masereel
parent ef05ca2812
commit afad149eb8
48 changed files with 597 additions and 461 deletions
+6
View File
@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import report
from . import wizard
+23
View File
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': "Coupon",
'summary': "Use discount coupons in different sales channels.",
'description': """Integrate coupon mechanism in orders.""",
'category': 'Sales',
'version': '1.0',
'depends': ['account'],
'data': [
'wizard/coupon_generate_views.xml',
'security/coupon_security.xml',
'views/coupon_views.xml',
'views/coupon_program_views.xml',
'report/coupon_report.xml',
'report/coupon_report_templates.xml',
'data/coupon_email_data.xml',
],
'demo': [
'demo/coupon_demo.xml',
],
'installable': True,
}
+4 -4
View File
@@ -1,9 +1,9 @@
<?xml version="1.0" ?>
<odoo>
<data noupdate="1">
<record id="sale_coupon.mail_template_sale_coupon" model="mail.template">
<record id="coupon.mail_template_sale_coupon" model="mail.template">
<field name="name">Coupon: Send by Email</field>
<field name="model_id" ref="sale_coupon.model_sale_coupon"/>
<field name="model_id" ref="coupon.model_coupon_coupon"/>
<field name="subject">Your reward coupon from ${object.program_id.company_id.name} </field>
<field name="email_from">${object.program_id.company_id.email | safe}</field>
<field name="partner_to">${object.partner_id.id}</field>
@@ -103,7 +103,7 @@
<record id="expire_coupon_cron" model="ir.cron">
<field name="name">Coupon: expire coupon based on date</field>
<field name="model_id" ref="sale_coupon.model_sale_coupon"/>
<field name="model_id" ref="coupon.model_coupon_coupon"/>
<field name="state">code</field>
<field name="code">model.cron_expire_coupon()</field>
<field name="active" eval="True"/>
@@ -113,4 +113,4 @@
<field name="numbercall">-1</field>
</record>
</data>
</odoo>
</odoo>
+5 -7
View File
@@ -8,12 +8,11 @@
<field name="supplier_taxes_id" eval="False"/>
<field name="sale_ok" eval="False"/>
<field name="purchase_ok" eval="False"/>
<field name="invoice_policy">order</field>
<field name="default_code">10PERCENTDISC</field>
<field name="categ_id" ref="product.product_category_3"/>
</record>
<record id="10_percent_auto_applied" model="sale.coupon.program">
<record id="10_percent_auto_applied" model="coupon.program">
<field name="name">Code for 10% on orders</field>
<field name="promo_code_usage">code_needed</field>
<field name="promo_code">10pc</field>
@@ -21,7 +20,7 @@
<field name="discount_type">percentage</field>
<field name="discount_percentage">10.0</field>
<field name="program_type">promotion_program</field>
<field name="discount_line_product_id" ref="sale_coupon.product_product_10_percent_discount"/>
<field name="discount_line_product_id" ref="coupon.product_product_10_percent_discount"/>
<field name="validity_duration">0</field>
</record>
@@ -32,12 +31,11 @@
<field name="supplier_taxes_id" eval="False"/>
<field name="sale_ok" eval="False"/>
<field name="purchase_ok" eval="False"/>
<field name="invoice_policy">order</field>
<field name="default_code">FREELARGECABINET</field>
<field name="categ_id" ref="product.product_category_3"/>
</record>
<record id="3_cabinets_plus_1_free" model="sale.coupon.program">
<record id="3_cabinets_plus_1_free" model="coupon.program">
<field name="name">Buy 3 large cabinets, get one for free</field>
<field name="promo_code_usage">no_code_needed</field>
<field name="discount_apply_on">on_order</field>
@@ -46,11 +44,11 @@
<field name="reward_product_id" ref="product.product_product_6"></field>
<field name="rule_min_quantity">3</field>
<field name="rule_products_domain">[["name","ilike","large cabinet"]]</field>
<field name="discount_line_product_id" ref="sale_coupon.product_product_free_large_cabinet"/>
<field name="discount_line_product_id" ref="coupon.product_product_free_large_cabinet"/>
<field name="validity_duration">0</field>
</record>
<record id="10_percent_coupon" model="sale.coupon.program">
<record id="10_percent_coupon" model="coupon.program">
<field name="name">10% Discount</field>
<field name="promo_code_usage">code_needed</field>
<field name="discount_apply_on">on_order</field>
+4 -5
View File
@@ -2,8 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mail_compose_message
from . import sale_coupon
from . import sale_coupon_reward
from . import sale_coupon_rules
from . import sale_coupon_program
from . import sale_order
from . import coupon
from . import coupon_reward
from . import coupon_rules
from . import coupon_program
+10 -47
View File
@@ -6,9 +6,9 @@ from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
class SaleCoupon(models.Model):
_name = 'sale.coupon'
_description = "Sales Coupon"
class Coupon(models.Model):
_name = 'coupon.coupon'
_description = "Coupon"
_rec_name = 'code'
@api.model
@@ -19,7 +19,7 @@ class SaleCoupon(models.Model):
A decimal serialisation is longer than a hexadecimal one *but* it
generates a more compact barcode (Code128C rather than Code128A).
Generate 8 bytes (64 bits) barcodes as 16 bytes barcodes are not
Generate 8 bytes (64 bits) barcodes as 16 bytes barcodes are not
compatible with all scanners.
"""
return str(random.getrandbits(64))
@@ -35,11 +35,7 @@ class SaleCoupon(models.Model):
('cancel', 'Cancelled')
], required=True, default='new')
partner_id = fields.Many2one('res.partner', "For Customer")
program_id = fields.Many2one('sale.coupon.program', "Program")
order_id = fields.Many2one('sale.order', 'Order Reference', readonly=True,
help="The sales order from which coupon is generated")
sales_order_id = fields.Many2one('sale.order', 'Used in', readonly=True,
help="The sales order on which the coupon is applied")
program_id = fields.Many2one('coupon.program', "Program")
discount_line_product_id = fields.Many2one('product.product', related='program_id.discount_line_product_id', readonly=False,
help='Product used in the sales order to apply the discount.')
@@ -47,54 +43,21 @@ class SaleCoupon(models.Model):
('unique_coupon_code', 'unique(code)', 'The coupon code must be unique!'),
]
@api.depends('create_date', 'program_id.validity_duration')
def _compute_expiration_date(self):
self.expiration_date = 0
for coupon in self.filtered(lambda x: x.program_id.validity_duration > 0):
coupon.expiration_date = (coupon.create_date + relativedelta(days=coupon.program_id.validity_duration)).date()
def _check_coupon_code(self, order):
message = {}
applicable_programs = order._get_applicable_programs()
if self.state == 'used':
message = {'error': _('This coupon has already been used (%s).') % (self.code)}
elif self.state == 'reserved':
message = {'error': _('This coupon %s exists but the origin sales order is not validated yet.') % (self.code)}
elif self.state == 'cancel':
message = {'error': _('This coupon has been cancelled (%s).') % (self.code)}
elif self.state == 'expired' or (self.expiration_date and self.expiration_date < order.date_order.date()):
message = {'error': _('This coupon is expired (%s).') % (self.code)}
# Minimum requirement should not be checked if the coupon got generated by a promotion program (the requirement should have only be checked to generate the coupon)
elif self.program_id.program_type == 'coupon_program' and not self.program_id._filter_on_mimimum_amount(order):
message = {'error': _('A minimum of %s %s should be purchased to get the reward') % (self.program_id.rule_minimum_amount, self.program_id.currency_id.name)}
elif not self.program_id.active:
message = {'error': _('The coupon program for %s is in draft or closed state') % (self.code)}
elif self.partner_id and self.partner_id != order.partner_id:
message = {'error': _('Invalid partner.')}
elif self.program_id in order.applied_coupon_ids.mapped('program_id'):
message = {'error': _('A Coupon is already applied for the same reward')}
elif self.program_id._is_global_discount_program() and order._is_global_discount_already_applied():
message = {'error': _('Global discounts are not cumulable.')}
elif self.program_id.reward_type == 'product' and not order._is_reward_in_order_lines(self.program_id):
message = {'error': _('The reward products should be in the sales order lines to apply the discount.')}
elif not self.program_id._is_valid_partner(order.partner_id):
message = {'error': _("The customer doesn't have access to this reward.")}
# Product requirement should not be checked if the coupon got generated by a promotion program (the requirement should have only be checked to generate the coupon)
elif self.program_id.program_type == 'coupon_program' and not self.program_id._filter_programs_on_products(order):
message = {'error': _("You don't have the required product quantities on your sales order. All the products should be recorded on the sales order. (Example: You need to have 3 T-shirts on your sales order if the promotion is 'Buy 2, Get 1 Free').")}
else:
if self.program_id not in applicable_programs and self.program_id.promo_applicability == 'on_current_order':
message = {'error': _('At least one of the required conditions is not met to get the reward!')}
return message
def action_coupon_sent(self):
""" Open a window to compose an email, with the edi invoice template
message loaded by default
"""
self.ensure_one()
template = self.env.ref('sale_coupon.mail_template_sale_coupon', False)
template = self.env.ref('coupon.mail_template_sale_coupon', False)
compose_form = self.env.ref('mail.email_compose_message_wizard_form', False)
ctx = dict(
default_model='sale.coupon',
default_model='coupon.coupon',
default_res_id=self.id,
default_use_template=bool(template),
default_template_id=template.id,
@@ -119,8 +82,8 @@ class SaleCoupon(models.Model):
def cron_expire_coupon(self):
self._cr.execute("""
SELECT C.id FROM SALE_COUPON as C
INNER JOIN SALE_COUPON_PROGRAM as P ON C.program_id = P.id
SELECT C.id FROM COUPON_COUPON as C
INNER JOIN COUPON_PROGRAM as P ON C.program_id = P.id
WHERE C.STATE in ('reserved', 'new', 'sent')
AND P.validity_duration > 0
AND C.create_date + interval '1 day' * P.validity_duration < now()""")
+22 -187
View File
@@ -7,10 +7,10 @@ from odoo.exceptions import UserError, ValidationError
import ast
class SaleCouponProgram(models.Model):
_name = 'sale.coupon.program'
_description = "Sales Coupon Program"
_inherits = {'sale.coupon.rule': 'rule_id', 'sale.coupon.reward': 'reward_id'}
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)
@@ -18,8 +18,8 @@ class SaleCouponProgram(models.Model):
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('sale.coupon.rule', string="Coupon Rule", ondelete='restrict', required=True)
reward_id = fields.Many2one('sale.coupon.reward', string="Reward", ondelete='restrict', required=True, copy=False)
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)")
@@ -45,9 +45,8 @@ class SaleCouponProgram(models.Model):
('on_current_order', 'Apply On Current Order'),
('on_next_order', 'Send a Coupon')],
default='on_current_order', string="Applicability")
coupon_ids = fields.One2many('sale.coupon', 'program_id', string="Generated Coupons", copy=False)
coupon_ids = fields.One2many('coupon.coupon', 'program_id', string="Generated Coupons", copy=False)
coupon_count = fields.Integer(compute='_compute_coupon_count')
order_count = fields.Integer(compute='_compute_order_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,
@@ -61,16 +60,9 @@ class SaleCouponProgram(models.Model):
if self.search(domain):
raise ValidationError(_('The program code must be unique!'))
# The api.depends is handled in `def modified` of `sale_coupon/models/sale_order.py`
def _compute_order_count(self):
product_data = self.env['sale.order.line'].read_group([('product_id', 'in', self.mapped('discount_line_product_id').ids)], ['product_id'], ['product_id'])
mapped_data = dict([(m['product_id'][0], m['product_id_count']) for m in product_data])
for program in self:
program.order_count = mapped_data.get(program.discount_line_product_id.id, 0)
@api.depends('coupon_ids')
def _compute_coupon_count(self):
coupon_data = self.env['sale.coupon'].read_group([('program_id', 'in', self.ids)], ['program_id'], ['program_id'])
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)
@@ -87,7 +79,7 @@ class SaleCouponProgram(models.Model):
@api.model
def create(self, vals):
program = super(SaleCouponProgram, self).create(vals)
program = super(CouponProgram, self).create(vals)
if not vals.get('discount_line_product_id', False):
discount_line_product_id = self.env['product.product'].create({
'name': program.reward_id.display_name,
@@ -96,14 +88,13 @@ class SaleCouponProgram(models.Model):
'supplier_taxes_id': False,
'sale_ok': False,
'purchase_ok': False,
'invoice_policy': 'order',
'lst_price': 0, #Do not set a high value to avoid issue with coupon code
})
program.write({'discount_line_product_id': discount_line_product_id.id})
return program
def write(self, vals):
res = super(SaleCouponProgram, self).write(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'
@@ -113,184 +104,28 @@ class SaleCouponProgram(models.Model):
return res
def unlink(self):
for program in self.filtered(lambda x: x.active):
if self.filtered('active'):
raise UserError(_('You can not delete a program in active state'))
return super(SaleCouponProgram, self).unlink()
# 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(SaleCouponProgram, self).toggle_active()
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 action_view_sales_orders(self):
self.ensure_one()
orders = self.env['sale.order.line'].search([('product_id', '=', self.discount_line_product_id.id)]).mapped('order_id')
return {
'name': _('Sales Orders'),
'view_mode': 'tree,form',
'res_model': 'sale.order',
'type': 'ir.actions.act_window',
'domain': [('id', 'in', orders.ids), ('state', 'not in', ('draft', 'sent', 'cancel'))],
'context': dict(self._context, create=False)
}
def _is_global_discount_program(self):
self.ensure_one()
return self.promo_applicability == 'on_current_order' and \
self.reward_type == 'discount' and \
self.discount_type == 'percentage' and \
self.discount_apply_on == 'on_order'
def _keep_only_most_interesting_auto_applied_global_discount_program(self):
'''Given a record set of programs, remove the less interesting auto
applied global discount to keep only the most interesting one.
We should not take promo code programs into account as a 10% auto
applied is considered better than a 50% promo code, as the user might
not know about the promo code.
'''
programs = self.filtered(lambda p: p._is_global_discount_program() and p.promo_code_usage == 'no_code_needed')
if not programs: return self
most_interesting_program = max(programs, key=lambda p: p.discount_percentage)
# remove least interesting programs
return self - (programs - most_interesting_program)
def _check_promo_code(self, order, coupon_code):
message = {}
applicable_programs = order._get_applicable_programs()
if self.maximum_use_number != 0 and self.order_count >= self.maximum_use_number:
message = {'error': _('Promo code %s has been expired.') % (coupon_code)}
elif not self._filter_on_mimimum_amount(order):
message = {'error': _('A minimum of %s %s should be purchased to get the reward') % (self.rule_minimum_amount, self.currency_id.name)}
elif self.promo_code and self.promo_code == order.promo_code:
message = {'error': _('The promo code is already applied on this order')}
elif not self.promo_code and self in order.no_code_promo_program_ids:
message = {'error': _('The promotional offer is already applied on this order')}
elif not self.active:
message = {'error': _('Promo code is invalid')}
elif self.rule_date_from and self.rule_date_from > order.date_order or self.rule_date_to and order.date_order > self.rule_date_to:
message = {'error': _('Promo code is expired')}
elif order.promo_code and self.promo_code_usage == 'code_needed':
message = {'error': _('Promotionals codes are not cumulative.')}
elif self._is_global_discount_program() and order._is_global_discount_already_applied():
message = {'error': _('Global discounts are not cumulative.')}
elif self.promo_applicability == 'on_current_order' and self.reward_type == 'product' and not order._is_reward_in_order_lines(self):
message = {'error': _('The reward products should be in the sales order lines to apply the discount.')}
elif not self._is_valid_partner(order.partner_id):
message = {'error': _("The customer doesn't have access to this reward.")}
elif not self._filter_programs_on_products(order):
message = {'error': _("You don't have the required product quantities on your sales order. If the reward is same product quantity, please make sure that all the products are recorded on the sales order (Example: You need to have 3 T-shirts on your sales order if the promotion is 'Buy 2, Get 1 Free'.")}
else:
if self not in applicable_programs and self.promo_applicability == 'on_current_order':
message = {'error': _('At least one of the required conditions is not met to get the reward!')}
return message
def _compute_program_amount(self, field, currency_to):
self.ensure_one()
return self.currency_id._convert(getattr(self, field), currency_to, self.company_id, fields.Date.today())
@api.model
def _filter_on_mimimum_amount(self, order):
filtered_programs = self.env['sale.coupon.program']
no_effect_lines = order._get_no_effect_on_threshold_lines()
order_amount = {
'amount_untaxed' : order.amount_untaxed - sum([line.price_subtotal for line in no_effect_lines]),
'amount_tax' : order.amount_tax - sum([line.price_tax for line in no_effect_lines])
}
for program in self:
lines = order.order_line.filtered(lambda line:
program.reward_type == 'discount' and
(line.product_id == program.discount_line_product_id or
line.product_id == program.reward_id.discount_line_product_id
))
untaxed_amount = order_amount['amount_untaxed'] - sum([line.price_subtotal for line in lines])
tax_amount = order_amount['amount_tax'] - sum([line.price_tax for line in lines])
program_amount = program._compute_program_amount('rule_minimum_amount', order.currency_id)
if program.rule_minimum_amount_tax_inclusion == 'tax_included' and program_amount <= (untaxed_amount + tax_amount) or program.rule_minimum_amount_tax_inclusion == 'tax_excluded' and program_amount <= untaxed_amount:
filtered_programs |= program
return filtered_programs
@api.model
def _filter_on_validity_dates(self, order):
return self.filtered(lambda program:
program.rule_date_from and program.rule_date_to and
program.rule_date_from <= order.date_order and program.rule_date_to >= order.date_order or
not program.rule_date_from or not program.rule_date_to)
@api.model
def _filter_promo_programs_with_code(self, order):
'''Filter Promo program with code with a different promo_code if a promo_code is already ordered'''
return self.filtered(lambda program: program.promo_code_usage == 'code_needed' and program.promo_code != order.promo_code)
def _filter_unexpired_programs(self, order):
return self.filtered(lambda program: program.maximum_use_number == 0 or program.order_count <= program.maximum_use_number)
def _filter_programs_on_partners(self, order):
return self.filtered(lambda program: program._is_valid_partner(order.partner_id))
def _filter_programs_on_products(self, order):
"""
To get valid programs according to product list.
i.e Buy 1 imac + get 1 ipad mini free then check 1 imac is on cart or not
or Buy 1 coke + get 1 coke free then check 2 cokes are on cart or not
"""
order_lines = order.order_line.filtered(lambda line: line.product_id) - order._get_reward_lines()
products = order_lines.mapped('product_id')
products_qties = dict.fromkeys(products, 0)
for line in order_lines:
products_qties[line.product_id] += line.product_uom_qty
valid_programs = self.filtered(lambda program: not program.rule_products_domain)
for program in self - valid_programs:
valid_products = program._get_valid_products(products)
ordered_rule_products_qty = sum(products_qties[product] for product in valid_products)
# Avoid program if 1 ordered foo on a program '1 foo, 1 free foo'
if program.promo_applicability == 'on_current_order' and \
program._is_valid_product(program.reward_product_id) and program.reward_type == 'product':
ordered_rule_products_qty -= program.reward_product_quantity
if ordered_rule_products_qty >= program.rule_min_quantity:
valid_programs |= program
return valid_programs
def _filter_not_ordered_reward_programs(self, order):
"""
Returns the programs when the reward is actually in the order lines
"""
programs = self.env['sale.coupon.program']
for program in self:
if program.reward_type == 'product' and \
not order.order_line.filtered(lambda line: line.product_id == program.reward_product_id):
continue
elif program.reward_type == 'discount' and program.discount_apply_on == 'specific_products' and \
not order.order_line.filtered(lambda line: line.product_id in program.discount_specific_product_ids):
continue
programs |= program
return programs
@api.model
def _filter_programs_from_common_rules(self, order, next_order=False):
""" Return the programs if every conditions is met
:param bool next_order: is the reward given from a previous order
"""
programs = self
# Minimum requirement should not be checked if the coupon got generated by a promotion program (the requirement should have only be checked to generate the coupon)
if not next_order:
programs = programs and programs._filter_on_mimimum_amount(order)
programs = programs and programs._filter_on_validity_dates(order)
programs = programs and programs._filter_unexpired_programs(order)
programs = programs and programs._filter_programs_on_partners(order)
# Product requirement should not be checked if the coupon got generated by a promotion program (the requirement should have only be checked to generate the coupon)
if not next_order:
programs = programs and programs._filter_programs_on_products(order)
programs_curr_order = programs.filtered(lambda p: p.promo_applicability == 'on_current_order')
programs = programs.filtered(lambda p: p.promo_applicability == 'on_next_order')
if programs_curr_order:
# Checking if rewards are in the SO should not be performed for rewards on_next_order
programs += programs_curr_order._filter_not_ordered_reward_programs(order)
return programs
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:
+4 -4
View File
@@ -5,9 +5,9 @@ from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class SaleCouponReward(models.Model):
_name = 'sale.coupon.reward'
_description = "Sales Coupon Reward"
class CouponReward(models.Model):
_name = 'coupon.reward'
_description = "Coupon Reward"
_rec_name = 'reward_description'
# VFE FIXME multi company
@@ -78,7 +78,7 @@ class SaleCouponReward(models.Model):
elif reward.discount_apply_on == 'cheapest_product':
reward_string = _("%s%% discount on cheapest product" % (reward_percentage))
elif reward.discount_type == 'fixed_amount':
program = self.env['sale.coupon.program'].search([('reward_id', '=', reward.id)])
program = self.env['coupon.program'].search([('reward_id', '=', reward.id)])
reward_string = _("%s %s discount on total amount" % (str(reward.discount_fixed_amount), program.currency_id.name))
result.append((reward.id, reward_string))
return result
+3 -3
View File
@@ -5,9 +5,9 @@ from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class SaleCouponRule(models.Model):
_name = 'sale.coupon.rule'
_description = "Sales Coupon Rule"
class CouponRule(models.Model):
_name = 'coupon.rule'
_description = "Coupon Rule"
rule_date_from = fields.Datetime(string="Start Date", help="Coupon program start date")
rule_date_to = fields.Datetime(string="End Date", help="Coupon program end date")
@@ -9,7 +9,7 @@ class MailComposeMessage(models.TransientModel):
def send_mail(self, **kwargs):
for wizard in self:
if self._context.get('mark_coupon_as_sent') and wizard.model == 'sale.coupon' and wizard.partner_ids:
if self._context.get('mark_coupon_as_sent') and wizard.model == 'coupon.coupon' and wizard.partner_ids:
# Mark coupon as sent in sudo, as helpdesk users don't have the right to write on coupons
self.env[wizard.model].sudo().browse(wizard.res_id).state = 'sent'
return super().send_mail(**kwargs)
+1 -1
View File
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import sale_coupon_report
from . import coupon_report
+3 -3
View File
@@ -5,15 +5,15 @@ from odoo import api, models
class CouponReport(models.AbstractModel):
_name = 'report.sale_coupon.report_coupon'
_name = 'report.coupon.report_coupon'
_description = 'Sales Coupon Report'
@api.model
def _get_report_values(self, docids, data=None):
docs = self.env['sale.coupon'].browse(docids)
docs = self.env['coupon.coupon'].browse(docids)
return {
'doc_ids': docs.ids,
'doc_model': 'sale.coupon',
'doc_model': 'coupon.coupon',
'data': data,
'docs': docs,
}
+3 -3
View File
@@ -3,9 +3,9 @@
<data>
<report
id="report_coupon_code"
model="sale.coupon"
name="sale_coupon.report_coupon_i18n"
file="sale_coupon.report_coupon_i18n"
model="coupon.coupon"
name="coupon.report_coupon_i18n"
file="coupon.report_coupon_i18n"
report_type="qweb-pdf"
string="Coupon Code"/>
</data>
@@ -94,7 +94,7 @@
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-set="o" t-value="o.with_context(lang=o.partner_id.lang)"/>
<t t-call="sale_coupon.report_coupon" t-lang="o.partner_id.lang"/>
<t t-call="coupon.report_coupon" t-lang="o.partner_id.lang"/>
</t>
</t>
</template>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="sale_coupon_generate_rule" model="ir.rule">
<field name="name">Generate Sales Coupon Rule</field>
<field name="model_id" ref="model_coupon_generate_wizard"/>
<field name="domain_force">[('create_uid', '=', user.id)]</field>
</record>
</odoo>
@@ -1,11 +0,0 @@
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_program_salesman,salesman,model_sale_coupon_program,sales_team.group_sale_salesman,1,0,0,0
access_program_manager,program manager,model_sale_coupon_program,sales_team.group_sale_manager,1,1,1,1
access_applicability_salesman,salesman,model_sale_coupon_rule,sales_team.group_sale_salesman,1,0,0,0
access_applicability_manager,program manager,model_sale_coupon_rule,sales_team.group_sale_manager,1,1,1,0
access_coupon_salesman,salesman,model_sale_coupon,sales_team.group_sale_salesman,1,0,0,0
access_coupon_manager,program manager,model_sale_coupon,sales_team.group_sale_manager,1,1,1,0
access_reward_salesman,salesman,model_sale_coupon_reward,sales_team.group_sale_salesman,1,0,0,0
access_reward_manager,program manager,model_sale_coupon_reward,sales_team.group_sale_manager,1,1,1,0
access_sale_coupon_apply_code,access.sale.coupon.apply.code,model_sale_coupon_apply_code,sales_team.group_sale_salesman,1,1,1,0
access_sale_coupon_generate,access.sale.coupon.generate,model_sale_coupon_generate,sales_team.group_sale_salesman,1,1,1,0
1 id name model_id/id group_id/id perm_read perm_write perm_create perm_unlink
2 access_program_salesman salesman model_sale_coupon_program sales_team.group_sale_salesman 1 0 0 0
3 access_program_manager program manager model_sale_coupon_program sales_team.group_sale_manager 1 1 1 1
4 access_applicability_salesman salesman model_sale_coupon_rule sales_team.group_sale_salesman 1 0 0 0
5 access_applicability_manager program manager model_sale_coupon_rule sales_team.group_sale_manager 1 1 1 0
6 access_coupon_salesman salesman model_sale_coupon sales_team.group_sale_salesman 1 0 0 0
7 access_coupon_manager program manager model_sale_coupon sales_team.group_sale_manager 1 1 1 0
8 access_reward_salesman salesman model_sale_coupon_reward sales_team.group_sale_salesman 1 0 0 0
9 access_reward_manager program manager model_sale_coupon_reward sales_team.group_sale_manager 1 1 1 0
10 access_sale_coupon_apply_code access.sale.coupon.apply.code model_sale_coupon_apply_code sales_team.group_sale_salesman 1 1 1 0
11 access_sale_coupon_generate access.sale.coupon.generate model_sale_coupon_generate sales_team.group_sale_salesman 1 1 1 0
+46 -57
View File
@@ -2,17 +2,13 @@
<odoo>
<!-- Common form view between the coupon programs and the promotion programs -->
<record id="sale_coupon_program_view_form_common" model="ir.ui.view">
<field name="name">sale.coupon.program.common.form</field>
<field name="model">sale.coupon.program</field>
<record id="coupon_program_view_form_common" model="ir.ui.view">
<field name="name">coupon.program.common.form</field>
<field name="model">coupon.program</field>
<field name="arch" type="xml">
<form string="Coupon Program">
<sheet>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" type="object" icon="fa-usd" name="action_view_sales_orders">
<field name="order_count" string="Sales" widget="statinfo"/>
</button>
</div>
<div class="oe_button_box" name="button_box" />
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<div name="title" class="oe_left">
</div>
@@ -73,22 +69,23 @@
</record>
<!-- Coupon Program -->
<record id="sale_coupon_program_view_form" model="ir.ui.view">
<field name="name">sale.coupon.program.form</field>
<field name="model">sale.coupon.program</field>
<field name="inherit_id" ref="sale_coupon_program_view_form_common"/>
<record id="coupon_program_view_coupon_program_form" model="ir.ui.view">
<field name="name">coupon.program.form</field>
<field name="model">coupon.program</field>
<field name="inherit_id" ref="coupon_program_view_form_common"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//sheet" position="before">
<header>
<button type="action" name="%(sale_coupon.sale_coupon_generate_action)d"
<button type="action" name="%(coupon.coupon_generate_action)d"
string="Generate Coupon" attrs="{'invisible': [('active', '=', False)]}"/>
<button type="action" name="%(sale_coupon.sale_coupon_generate_action)d"
<button type="action" name="%(coupon.coupon_generate_action)d"
string="Generate Coupon" attrs="{'invisible': [('active', '=', True)]}" class="oe_highlight"/>
</header>
</xpath>
<xpath expr="//field[@name='order_count']/.." position="before">
<button class="oe_stat_button" type="action" icon="fa-ticket" name="%(sale_coupon.sale_coupon_action)d">
<xpath expr="//div[@name='button_box']" position="inside">
<button class="oe_stat_button" type="action" icon="fa-ticket" name="%(coupon.coupon_action)d">
<field name="coupon_count" string="Coupons" widget="statinfo"/>
</button>
</xpath>
@@ -106,9 +103,9 @@
</field>
</record>
<record id="sale_coupon_program_view_tree" model="ir.ui.view">
<field name="name">sale.coupon.program.tree</field>
<field name="model">sale.coupon.program</field>
<record id="coupon_program_view_tree" model="ir.ui.view">
<field name="name">coupon.program.tree</field>
<field name="model">coupon.program</field>
<field name="arch" type="xml">
<tree>
<field name="sequence" widget="handle"/>
@@ -119,9 +116,9 @@
</field>
</record>
<record id="sale_coupon_program_view_search" model="ir.ui.view">
<field name="name">sale.coupon.program.search</field>
<field name="model">sale.coupon.program</field>
<record id="coupon_program_view_search" model="ir.ui.view">
<field name="name">coupon.program.search</field>
<field name="model">coupon.program</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
@@ -133,9 +130,9 @@
</field>
</record>
<record id="view_sale_coupon_program_kanban" model="ir.ui.view">
<field name="name">sale.coupon.program.kanban</field>
<field name="model">sale.coupon.program</field>
<record id="view_coupon_program_kanban" model="ir.ui.view">
<field name="name">coupon.program.kanban</field>
<field name="model">coupon.program</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile">
<field name="name" />
@@ -148,12 +145,10 @@
</div>
<hr class="mt4 mb4"/>
<div class="row">
<div class="col-4 text-center"><strong>Coupons</strong></div>
<div class="col-4 text-center"><strong>Sales</strong></div>
<div class="col-4 text-center"><strong>Active</strong></div>
<div class="col-4 text-center"><field name="coupon_count"/></div>
<div class="col-4 text-center"><field name="order_count"/></div>
<div class="col-4 text-center">
<div class="col-4 text-center coupon-count-label"><strong>Coupons</strong></div>
<div class="col-4 text-center active-label"><strong>Active</strong></div>
<div class="col-4 text-center coupon-count-value"><field name="coupon_count"/></div>
<div class="col-4 text-center active-value">
<field name="active" widget="boolean"/>
</div>
</div>
@@ -164,15 +159,15 @@
</field>
</record>
<record id="sale_coupon_program_action_coupon_program" model="ir.actions.act_window">
<record id="coupon_program_action_coupon_program" model="ir.actions.act_window">
<field name="name">Coupon Programs</field>
<field name="res_model">sale.coupon.program</field>
<field name="res_model">coupon.program</field>
<field name="view_mode">tree,kanban,form</field>
<field name="search_view_id" ref="sale_coupon_program_view_search"/>
<field name="search_view_id" ref="coupon_program_view_search"/>
<field name="view_ids" eval="[
(5, 0, 0),
(0, 0, {'view_mode': 'tree'}),
(0, 0, {'view_mode': 'form', 'view_id': ref('sale_coupon_program_view_form')})]"/>
(0, 0, {'view_mode': 'form', 'view_id': ref('coupon_program_view_coupon_program_form')})]"/>
<field name="domain">[('program_type','=', 'coupon_program')]</field>
<field name="context">{
'default_program_type': 'coupon_program',
@@ -188,23 +183,20 @@
</field>
</record>
<menuitem id="menu_coupon_type_config" action="sale_coupon_program_action_coupon_program" parent="sale.product_menu_catalog" name="Coupon Programs" groups="sales_team.group_sale_manager" sequence="5"/>
<!-- Promotion Program -->
<record id="sale_coupon_program_view_promo_program_form" model="ir.ui.view">
<field name="name">sale.coupon.promotion.program.form</field>
<field name="model">sale.coupon.program</field>
<field name="inherit_id" ref="sale_coupon_program_view_form_common"/>
<record id="coupon_program_view_promo_program_form" model="ir.ui.view">
<field name="name">coupon.promotion.program.form</field>
<field name="model">coupon.program</field>
<field name="inherit_id" ref="coupon_program_view_form_common"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//div[@name='title']" position="inside">
<label class="oe_edit_only" for="name" string="Promotion Program Name"/>
<h1><field name="name" class="oe_title" placeholder="Promotion Program Name..." height="20px"/></h1>
</xpath>
<xpath expr="//field[@name='order_count']/.." position="before">
<button class="oe_stat_button" type="action" icon="fa-ticket" name="%(sale_coupon.sale_coupon_action)d" attrs="{'invisible': [('promo_applicability', '=', 'on_current_order')]}">
<xpath expr="//div[@name='button_box']" position="inside">
<button class="oe_stat_button" type="action" icon="fa-ticket" name="%(coupon.coupon_action)d" attrs="{'invisible': [('promo_applicability', '=', 'on_current_order')]}">
<field name="coupon_count" string="Coupons" widget="statinfo"/>
</button>
</xpath>
@@ -236,9 +228,9 @@
</field>
</record>
<record id="sale_coupon_program_view_promo_program_tree" model="ir.ui.view">
<field name="name">sale.coupon.promotion.program.tree</field>
<field name="model">sale.coupon.program</field>
<record id="coupon_program_view_promo_program_tree" model="ir.ui.view">
<field name="name">coupon.promotion.program.tree</field>
<field name="model">coupon.program</field>
<field name="arch" type="xml">
<tree>
<field name="sequence" widget="handle"/>
@@ -249,9 +241,9 @@
</field>
</record>
<record id="sale_coupon_program_view_promo_program_search" model="ir.ui.view">
<field name="name">sale.coupon.promotion.program.search</field>
<field name="model">sale.coupon.program</field>
<record id="coupon_program_view_promo_program_search" model="ir.ui.view">
<field name="name">coupon.promotion.program.search</field>
<field name="model">coupon.program</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
@@ -261,15 +253,15 @@
</field>
</record>
<record id="sale_coupon_program_action_promo_program" model="ir.actions.act_window">
<record id="coupon_program_action_promo_program" model="ir.actions.act_window">
<field name="name">Promotion Programs</field>
<field name="res_model">sale.coupon.program</field>
<field name="res_model">coupon.program</field>
<field name="view_mode">tree,kanban,form</field>
<field name="view_ids" eval="[
(5, 0, 0),
(0, 0, {'view_mode': 'tree'}),
(0, 0, {'view_mode': 'form', 'view_id': ref('sale_coupon_program_view_promo_program_form')})]"/>
<field name="search_view_id" ref="sale_coupon_program_view_promo_program_search"/>
(0, 0, {'view_mode': 'form', 'view_id': ref('coupon_program_view_promo_program_form')})]"/>
<field name="search_view_id" ref="coupon_program_view_promo_program_search"/>
<field name="domain">[('program_type', '=', 'promotion_program')]</field>
<field name="context">{
'default_program_type': 'promotion_program',
@@ -286,7 +278,4 @@
</p>
</field>
</record>
<menuitem action="sale_coupon_program_action_promo_program" id="menu_promotion_type_config" parent="sale.product_menu_catalog" name="Promotion Programs" groups="sales_team.group_sale_manager" sequence="4"/>
</odoo>
+9 -12
View File
@@ -1,32 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="sale_coupon_view_tree" model="ir.ui.view">
<field name="name">sale.coupon.tree</field>
<field name="model">sale.coupon</field>
<record id="coupon_view_tree" model="ir.ui.view">
<field name="name">coupon.coupon.tree</field>
<field name="model">coupon.coupon</field>
<field name="arch" type="xml">
<tree string="Coupons" create="false" edit="false" delete="false">
<field name="code"/>
<field name="expiration_date"/>
<field name="program_id"/>
<field name="partner_id"/>
<field name="order_id"/>
<field name="state"/>
</tree>
</field>
</record>
<record id="sale_coupon_action" model="ir.actions.act_window">
<record id="coupon_action" model="ir.actions.act_window">
<field name="name">Coupons</field>
<field name="res_model">sale.coupon</field>
<field name="view_id" ref="sale_coupon_view_tree"/>
<field name="res_model">coupon.coupon</field>
<field name="view_id" ref="coupon_view_tree"/>
<field name="domain">[('program_id', '=', active_id)]</field>
<field name="context">{}</field>
</record>
<record id="sale_coupon_view_form" model="ir.ui.view">
<field name="name">sale.coupon.form</field>
<field name="model">sale.coupon</field>
<record id="coupon_view_form" model="ir.ui.view">
<field name="name">coupon.coupon.form</field>
<field name="model">coupon.coupon</field>
<field name="arch" type="xml">
<form string="Coupons" create="false" edit="false" delete="false">
<header>
@@ -39,8 +38,6 @@
<field name="code"/>
<field name="expiration_date"/>
<field name="partner_id"/>
<field name="order_id"/>
<field name="sales_order_id"/>
</group>
</sheet>
</form>
+1 -2
View File
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import sale_coupon_apply_code
from . import sale_coupon_generate
from . import coupon_generate
+7 -7
View File
@@ -6,9 +6,9 @@ import ast
from odoo.osv import expression
class SaleCouponGenerate(models.TransientModel):
_name = 'sale.coupon.generate'
_description = 'Generate Sales Coupon'
class CouponGenerate(models.TransientModel):
_name = 'coupon.generate.wizard'
_description = 'Generate Coupon'
nbr_coupons = fields.Integer(string="Number of Coupons", help="Number of coupons", default=1)
generation_type = fields.Selection([
@@ -21,20 +21,20 @@ class SaleCouponGenerate(models.TransientModel):
def generate_coupon(self):
"""Generates the number of coupons entered in wizard field nbr_coupons
"""
program = self.env['sale.coupon.program'].browse(self.env.context.get('active_id'))
program = self.env['coupon.program'].browse(self.env.context.get('active_id'))
vals = {'program_id': program.id}
if self.generation_type == 'nbr_coupon' and self.nbr_coupons > 0:
for count in range(0, self.nbr_coupons):
self.env['sale.coupon'].create(vals)
self.env['coupon.coupon'].create(vals)
if self.generation_type == 'nbr_customer' and self.partners_domain:
for partner in self.env['res.partner'].search(ast.literal_eval(self.partners_domain)):
vals.update({'partner_id': partner.id, 'state': 'sent' if partner.email else 'new'})
coupon = self.env['sale.coupon'].create(vals)
coupon = self.env['coupon.coupon'].create(vals)
subject = '%s, a coupon has been generated for you' % (partner.name)
template = self.env.ref('sale_coupon.mail_template_sale_coupon', raise_if_not_found=False)
template = self.env.ref('coupon.mail_template_sale_coupon', raise_if_not_found=False)
if template:
email_values = {'email_to': partner.email, 'email_from': self.env.user.email or '', 'subject': subject}
template.send_mail(coupon.id, email_values=email_values, notif_layout='mail.mail_notification_light')
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="sale_coupon_generate_view_form" model="ir.ui.view">
<field name="name">sale.coupon.generate.form</field>
<field name="model">sale.coupon.generate</field>
<record id="coupon_generate_view_form" model="ir.ui.view">
<field name="name">coupon.generate.wizard.form</field>
<field name="model">coupon.generate.wizard</field>
<field name="arch" type="xml">
<form string="Generate Coupons">
<group>
@@ -22,11 +22,11 @@
</field>
</record>
<record id="sale_coupon_generate_action" model="ir.actions.act_window">
<record id="coupon_generate_action" model="ir.actions.act_window">
<field name="name">Number of Coupons To Generate</field>
<field name="res_model">sale.coupon.generate</field>
<field name="res_model">coupon.generate.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="view_id" ref="sale_coupon_generate_view_form"/>
<field name="view_id" ref="coupon_generate_view_form"/>
</record>
</odoo>
-1
View File
@@ -3,4 +3,3 @@
from . import models
from . import wizard
from . import report
+3 -10
View File
@@ -6,21 +6,14 @@
'description': """Integrate coupon mechanism in sales orders.""",
'category': 'Sales/Sales',
'version': '1.0',
'depends': ['sale'],
'depends': ['coupon', 'sale'],
'data': [
'security/sale_coupon_security.xml',
'security/ir.model.access.csv',
'wizard/sale_coupon_apply_code_views.xml',
'wizard/sale_coupon_generate_views.xml',
'views/sale_order_views.xml',
'views/sale_coupon_views.xml',
'views/sale_coupon_program_views.xml',
'views/coupon_views.xml',
'views/coupon_program_views.xml',
'views/res_config_settings_views.xml',
'report/sale_coupon_report.xml',
'report/sale_coupon_report_templates.xml',
'data/sale_coupon_email_data.xml',
],
'demo': [
'data/sale_coupon_demo.xml',
],
}
+6
View File
@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import coupon
from . import coupon_program
from . import sale_order
+47
View File
@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
class Coupon(models.Model):
_inherit = 'coupon.coupon'
order_id = fields.Many2one('sale.order', 'Order Reference', readonly=True,
help="The sales order from which coupon is generated")
sales_order_id = fields.Many2one('sale.order', 'Used in', readonly=True,
help="The sales order on which the coupon is applied")
def _check_coupon_code(self, order):
message = {}
applicable_programs = order._get_applicable_programs()
if self.state == 'used':
message = {'error': _('This coupon has already been used (%s).') % (self.code)}
elif self.state == 'reserved':
message = {'error': _('This coupon %s exists but the origin sales order is not validated yet.') % (self.code)}
elif self.state == 'cancel':
message = {'error': _('This coupon has been cancelled (%s).') % (self.code)}
elif self.state == 'expired' or (self.expiration_date and self.expiration_date < order.date_order.date()):
message = {'error': _('This coupon is expired (%s).') % (self.code)}
# Minimum requirement should not be checked if the coupon got generated by a promotion program (the requirement should have only be checked to generate the coupon)
elif self.program_id.program_type == 'coupon_program' and not self.program_id._filter_on_mimimum_amount(order):
message = {'error': _('A minimum of %s %s should be purchased to get the reward') % (self.program_id.rule_minimum_amount, self.program_id.currency_id.name)}
elif not self.program_id.active:
message = {'error': _('The coupon program for %s is in draft or closed state') % (self.code)}
elif self.partner_id and self.partner_id != order.partner_id:
message = {'error': _('Invalid partner.')}
elif self.program_id in order.applied_coupon_ids.mapped('program_id'):
message = {'error': _('A Coupon is already applied for the same reward')}
elif self.program_id._is_global_discount_program() and order._is_global_discount_already_applied():
message = {'error': _('Global discounts are not cumulable.')}
elif self.program_id.reward_type == 'product' and not order._is_reward_in_order_lines(self.program_id):
message = {'error': _('The reward products should be in the sales order lines to apply the discount.')}
elif not self.program_id._is_valid_partner(order.partner_id):
message = {'error': _("The customer doesn't have access to this reward.")}
# Product requirement should not be checked if the coupon got generated by a promotion program (the requirement should have only be checked to generate the coupon)
elif self.program_id.program_type == 'coupon_program' and not self.program_id._filter_programs_on_products(order):
message = {'error': _("You don't have the required product quantities on your sales order. All the products should be recorded on the sales order. (Example: You need to have 3 T-shirts on your sales order if the promotion is 'Buy 2, Get 1 Free').")}
else:
if self.program_id not in applicable_programs and self.program_id.promo_applicability == 'on_current_order':
message = {'error': _('At least one of the required conditions is not met to get the reward!')}
return message
+181
View File
@@ -0,0 +1,181 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
class CouponProgram(models.Model):
_inherit = 'coupon.program'
order_count = fields.Integer(compute='_compute_order_count')
# The api.depends is handled in `def modified` of `sale_coupon/models/sale_order.py`
def _compute_order_count(self):
product_data = self.env['sale.order.line'].read_group([('product_id', 'in', self.mapped('discount_line_product_id').ids)], ['product_id'], ['product_id'])
mapped_data = dict([(m['product_id'][0], m['product_id_count']) for m in product_data])
for program in self:
program.order_count = mapped_data.get(program.discount_line_product_id.id, 0)
def action_view_sales_orders(self):
self.ensure_one()
orders = self.env['sale.order.line'].search([('product_id', '=', self.discount_line_product_id.id)]).mapped('order_id')
return {
'name': _('Sales Orders'),
'view_mode': 'tree,form',
'res_model': 'sale.order',
'type': 'ir.actions.act_window',
'domain': [('id', 'in', orders.ids), ('state', 'not in', ('draft', 'sent', 'cancel'))],
'context': dict(self._context, create=False)
}
def _check_promo_code(self, order, coupon_code):
message = {}
applicable_programs = order._get_applicable_programs()
if self.maximum_use_number != 0 and self.order_count >= self.maximum_use_number:
message = {'error': _('Promo code %s has been expired.') % (coupon_code)}
elif not self._filter_on_mimimum_amount(order):
message = {'error': _('A minimum of %s %s should be purchased to get the reward') % (self.rule_minimum_amount, self.currency_id.name)}
elif self.promo_code and self.promo_code == order.promo_code:
message = {'error': _('The promo code is already applied on this order')}
elif not self.promo_code and self in order.no_code_promo_program_ids:
message = {'error': _('The promotional offer is already applied on this order')}
elif not self.active:
message = {'error': _('Promo code is invalid')}
elif self.rule_date_from and self.rule_date_from > order.date_order or self.rule_date_to and order.date_order > self.rule_date_to:
message = {'error': _('Promo code is expired')}
elif order.promo_code and self.promo_code_usage == 'code_needed':
message = {'error': _('Promotionals codes are not cumulative.')}
elif self._is_global_discount_program() and order._is_global_discount_already_applied():
message = {'error': _('Global discounts are not cumulative.')}
elif self.promo_applicability == 'on_current_order' and self.reward_type == 'product' and not order._is_reward_in_order_lines(self):
message = {'error': _('The reward products should be in the sales order lines to apply the discount.')}
elif not self._is_valid_partner(order.partner_id):
message = {'error': _("The customer doesn't have access to this reward.")}
elif not self._filter_programs_on_products(order):
message = {'error': _("You don't have the required product quantities on your sales order. If the reward is same product quantity, please make sure that all the products are recorded on the sales order (Example: You need to have 3 T-shirts on your sales order if the promotion is 'Buy 2, Get 1 Free'.")}
else:
if self not in applicable_programs and self.promo_applicability == 'on_current_order':
message = {'error': _('At least one of the required conditions is not met to get the reward!')}
return message
@api.model
def _filter_on_mimimum_amount(self, order):
filtered_programs = self.env['coupon.program']
no_effect_lines = order._get_no_effect_on_threshold_lines()
order_amount = {
'amount_untaxed' : order.amount_untaxed - sum([line.price_subtotal for line in no_effect_lines]),
'amount_tax' : order.amount_tax - sum([line.price_tax for line in no_effect_lines])
}
for program in self:
lines = order.order_line.filtered(lambda line:
program.reward_type == 'discount' and
(line.product_id == program.discount_line_product_id or
line.product_id == program.reward_id.discount_line_product_id
))
untaxed_amount = order_amount['amount_untaxed'] - sum([line.price_subtotal for line in lines])
tax_amount = order_amount['amount_tax'] - sum([line.price_tax for line in lines])
program_amount = program._compute_program_amount('rule_minimum_amount', order.currency_id)
if program.rule_minimum_amount_tax_inclusion == 'tax_included' and program_amount <= (untaxed_amount + tax_amount) or program.rule_minimum_amount_tax_inclusion == 'tax_excluded' and program_amount <= untaxed_amount:
filtered_programs |= program
return filtered_programs
@api.model
def _filter_on_validity_dates(self, order):
return self.filtered(lambda program:
program.rule_date_from and program.rule_date_to and
program.rule_date_from <= order.date_order and program.rule_date_to >= order.date_order or
not program.rule_date_from or not program.rule_date_to)
@api.model
def _filter_promo_programs_with_code(self, order):
'''Filter Promo program with code with a different promo_code if a promo_code is already ordered'''
return self.filtered(lambda program: program.promo_code_usage == 'code_needed' and program.promo_code != order.promo_code)
def _filter_unexpired_programs(self, order):
return self.filtered(lambda program: program.maximum_use_number == 0 or program.order_count <= program.maximum_use_number)
def _filter_programs_on_partners(self, order):
return self.filtered(lambda program: program._is_valid_partner(order.partner_id))
def _filter_programs_on_products(self, order):
"""
To get valid programs according to product list.
i.e Buy 1 imac + get 1 ipad mini free then check 1 imac is on cart or not
or Buy 1 coke + get 1 coke free then check 2 cokes are on cart or not
"""
order_lines = order.order_line.filtered(lambda line: line.product_id) - order._get_reward_lines()
products = order_lines.mapped('product_id')
products_qties = dict.fromkeys(products, 0)
for line in order_lines:
products_qties[line.product_id] += line.product_uom_qty
valid_programs = self.filtered(lambda program: not program.rule_products_domain)
for program in self - valid_programs:
valid_products = program._get_valid_products(products)
ordered_rule_products_qty = sum(products_qties[product] for product in valid_products)
# Avoid program if 1 ordered foo on a program '1 foo, 1 free foo'
if program.promo_applicability == 'on_current_order' and \
program._is_valid_product(program.reward_product_id) and program.reward_type == 'product':
ordered_rule_products_qty -= program.reward_product_quantity
if ordered_rule_products_qty >= program.rule_min_quantity:
valid_programs |= program
return valid_programs
def _filter_not_ordered_reward_programs(self, order):
"""
Returns the programs when the reward is actually in the order lines
"""
programs = self.env['coupon.program']
for program in self:
if program.reward_type == 'product' and \
not order.order_line.filtered(lambda line: line.product_id == program.reward_product_id):
continue
elif program.reward_type == 'discount' and program.discount_apply_on == 'specific_products' and \
not order.order_line.filtered(lambda line: line.product_id in program.discount_specific_product_ids):
continue
programs |= program
return programs
@api.model
def _filter_programs_from_common_rules(self, order, next_order=False):
""" Return the programs if every conditions is met
:param bool next_order: is the reward given from a previous order
"""
programs = self
# Minimum requirement should not be checked if the coupon got generated by a promotion program (the requirement should have only be checked to generate the coupon)
if not next_order:
programs = programs and programs._filter_on_mimimum_amount(order)
programs = programs and programs._filter_on_validity_dates(order)
programs = programs and programs._filter_unexpired_programs(order)
programs = programs and programs._filter_programs_on_partners(order)
# Product requirement should not be checked if the coupon got generated by a promotion program (the requirement should have only be checked to generate the coupon)
if not next_order:
programs = programs and programs._filter_programs_on_products(order)
programs_curr_order = programs.filtered(lambda p: p.promo_applicability == 'on_current_order')
programs = programs.filtered(lambda p: p.promo_applicability == 'on_next_order')
if programs_curr_order:
# Checking if rewards are in the SO should not be performed for rewards on_next_order
programs += programs_curr_order._filter_not_ordered_reward_programs(order)
return programs
def _is_global_discount_program(self):
self.ensure_one()
return self.promo_applicability == 'on_current_order' and \
self.reward_type == 'discount' and \
self.discount_type == 'percentage' and \
self.discount_apply_on == 'on_order'
def _keep_only_most_interesting_auto_applied_global_discount_program(self):
'''Given a record set of programs, remove the less interesting auto
applied global discount to keep only the most interesting one.
We should not take promo code programs into account as a 10% auto
applied is considered better than a 50% promo code, as the user might
not know about the promo code.
'''
programs = self.filtered(lambda p: p._is_global_discount_program() and p.promo_code_usage == 'no_code_needed')
if not programs: return self
most_interesting_program = max(programs, key=lambda p: p.discount_percentage)
# remove least interesting programs
return self - (programs - most_interesting_program)
+15 -15
View File
@@ -8,12 +8,12 @@ from odoo.tools.misc import formatLang
class SaleOrder(models.Model):
_inherit = "sale.order"
applied_coupon_ids = fields.One2many('sale.coupon', 'sales_order_id', string="Applied Coupons", copy=False)
generated_coupon_ids = fields.One2many('sale.coupon', 'order_id', string="Offered Coupons", copy=False)
applied_coupon_ids = fields.One2many('coupon.coupon', 'sales_order_id', string="Applied Coupons", copy=False)
generated_coupon_ids = fields.One2many('coupon.coupon', 'order_id', string="Offered Coupons", copy=False)
reward_amount = fields.Float(compute='_compute_reward_total')
no_code_promo_program_ids = fields.Many2many('sale.coupon.program', string="Applied Immediate Promo Programs",
no_code_promo_program_ids = fields.Many2many('coupon.program', string="Applied Immediate Promo Programs",
domain="[('promo_code_usage', '=', 'no_code_needed'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", copy=False)
code_promo_program_id = fields.Many2one('sale.coupon.program', string="Applied Promo Program",
code_promo_program_id = fields.Many2one('coupon.program', string="Applied Promo Program",
domain="[('promo_code_usage', '=', 'code_needed'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", copy=False)
promo_code = fields.Char(related='code_promo_program_id.promo_code', help="Applied program code", readonly=False)
@@ -105,7 +105,7 @@ class SaleOrder(models.Model):
def _get_paid_order_lines(self):
""" Returns the sale order lines that are not reward lines.
It will also return reward lines being free product lines. """
free_reward_product = self.env['sale.coupon.program'].search([('reward_type', '=', 'product')]).mapped('discount_line_product_id')
free_reward_product = self.env['coupon.program'].search([('reward_type', '=', 'product')]).mapped('discount_line_product_id')
return self.order_line.filtered(lambda x: not x.is_reward_line or x.product_id in free_reward_product)
def _get_reward_values_discount_fixed_amount(self, program):
@@ -156,7 +156,7 @@ class SaleOrder(models.Model):
elif program.discount_apply_on in ['specific_products', 'on_order']:
if program.discount_apply_on == 'specific_products':
# We should not exclude reward line that offer this product since we need to offer only the discount on the real paid product (regular product - free product)
free_product_lines = self.env['sale.coupon.program'].search([('reward_type', '=', 'product'), ('reward_product_id', 'in', program.discount_specific_product_ids.ids)]).mapped('discount_line_product_id')
free_product_lines = self.env['coupon.program'].search([('reward_type', '=', 'product'), ('reward_product_id', 'in', program.discount_specific_product_ids.ids)]).mapped('discount_line_product_id')
lines = lines.filtered(lambda x: x.product_id in (program.discount_specific_product_ids | free_product_lines))
for line in lines:
@@ -214,7 +214,7 @@ class SaleOrder(models.Model):
def _create_reward_coupon(self, program):
# if there is already a coupon that was set as expired, reactivate that one instead of creating a new one
coupon = self.env['sale.coupon'].search([
coupon = self.env['coupon.coupon'].search([
('program_id', '=', program.id),
('state', '=', 'expired'),
('partner_id', '=', self.partner_id.id),
@@ -224,7 +224,7 @@ class SaleOrder(models.Model):
if coupon:
coupon.write({'state': 'reserved'})
else:
coupon = self.env['sale.coupon'].create({
coupon = self.env['coupon.coupon'].create({
'program_id': program.id,
'state': 'reserved',
'partner_id': self.partner_id.id,
@@ -235,12 +235,12 @@ class SaleOrder(models.Model):
return coupon
def _send_reward_coupon_mail(self):
template = self.env.ref('sale_coupon.mail_template_sale_coupon', raise_if_not_found=False)
template = self.env.ref('coupon.mail_template_sale_coupon', raise_if_not_found=False)
if template:
for coupon in self.generated_coupon_ids:
self.message_post_with_template(
template.id, composition_mode='comment',
model='sale.coupon', res_id=coupon.id,
model='coupon.coupon', res_id=coupon.id,
email_layout_xmlid='mail.mail_notification_light',
)
@@ -250,7 +250,7 @@ class SaleOrder(models.Model):
param: order - The sale order for which method will get applicable programs.
"""
self.ensure_one()
programs = self.env['sale.coupon.program'].search([
programs = self.env['coupon.program'].search([
])._filter_programs_from_common_rules(self)
if self.promo_code:
programs._filter_promo_programs_with_code(self)
@@ -258,7 +258,7 @@ class SaleOrder(models.Model):
def _get_applicable_no_code_promo_program(self):
self.ensure_one()
programs = self.env['sale.coupon.program'].search([
programs = self.env['coupon.program'].search([
('promo_code_usage', '=', 'no_code_needed'),
])._filter_programs_from_common_rules(self)
return programs
@@ -402,7 +402,7 @@ class SaleOrderLine(models.Model):
line.order_id.applied_coupon_ids -= coupons_to_reactivate
# Remove the program from the order if the deleted line is the reward line of the program
# And delete the other lines from this program (It's the case when discount is split per different taxes)
related_program = self.env['sale.coupon.program'].search([('discount_line_product_id', '=', line.product_id.id)])
related_program = self.env['coupon.program'].search([('discount_line_product_id', '=', line.product_id.id)])
if related_program:
line.order_id.no_code_promo_program_ids -= related_program
line.order_id.code_promo_program_id -= related_program
@@ -422,7 +422,7 @@ class SaleOrderLine(models.Model):
taxes = line.tax_id.filtered(lambda r: not line.company_id or r.company_id == line.company_id)
line.tax_id = fpos.map_tax(taxes, line.product_id, line.order_id.partner_shipping_id)
# Invalidation of `sale.coupon.program.order_count`
# Invalidation of `coupon.program.order_count`
# `test_program_rules_validity_dates_and_uses`,
# Overriding modified is quite hardcore as you need to know how works the cache and the invalidation system,
# but at least the below works and should be efficient.
@@ -432,7 +432,7 @@ class SaleOrderLine(models.Model):
def modified(self, fnames, create=False):
super(SaleOrderLine, self).modified(fnames, create)
if 'product_id' in fnames:
Program = self.env['sale.coupon.program'].sudo()
Program = self.env['coupon.program'].sudo()
field_order_count = Program._fields['order_count']
programs = self.env.cache.get_records(Program, field_order_count)
if programs:
@@ -0,0 +1,11 @@
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_program_salesman,Coupon Program (Salesperson),coupon.model_coupon_program,sales_team.group_sale_salesman,1,0,0,0
access_program_manager,Coupon Program (Manager),coupon.model_coupon_program,sales_team.group_sale_manager,1,1,1,1
access_applicability_salesman,Coupon Rule (Salesperson),coupon.model_coupon_rule,sales_team.group_sale_salesman,1,0,0,0
access_applicability_manager,Coupon Rule (Manager),coupon.model_coupon_rule,sales_team.group_sale_manager,1,1,1,0
access_coupon_salesman,Coupon (Salesperson),coupon.model_coupon_coupon,sales_team.group_sale_salesman,1,0,0,0
access_coupon_manager,Coupon (Manager),coupon.model_coupon_coupon,sales_team.group_sale_manager,1,1,1,0
access_reward_salesman,Coupon Reward (Salesperson),coupon.model_coupon_reward,sales_team.group_sale_salesman,1,0,0,0
access_reward_manager,Coupon Reward (Manager),coupon.model_coupon_reward,sales_team.group_sale_manager,1,1,1,0
access_sale_coupon_apply_code,Apply Coupon,model_sale_coupon_apply_code,sales_team.group_sale_salesman,1,1,1,0
access_sale_coupon_generate,Coupon Generation,coupon.model_coupon_generate_wizard,sales_team.group_sale_salesman,1,1,1,0
1 id name model_id/id group_id/id perm_read perm_write perm_create perm_unlink
2 access_program_salesman Coupon Program (Salesperson) coupon.model_coupon_program sales_team.group_sale_salesman 1 0 0 0
3 access_program_manager Coupon Program (Manager) coupon.model_coupon_program sales_team.group_sale_manager 1 1 1 1
4 access_applicability_salesman Coupon Rule (Salesperson) coupon.model_coupon_rule sales_team.group_sale_salesman 1 0 0 0
5 access_applicability_manager Coupon Rule (Manager) coupon.model_coupon_rule sales_team.group_sale_manager 1 1 1 0
6 access_coupon_salesman Coupon (Salesperson) coupon.model_coupon_coupon sales_team.group_sale_salesman 1 0 0 0
7 access_coupon_manager Coupon (Manager) coupon.model_coupon_coupon sales_team.group_sale_manager 1 1 1 0
8 access_reward_salesman Coupon Reward (Salesperson) coupon.model_coupon_reward sales_team.group_sale_salesman 1 0 0 0
9 access_reward_manager Coupon Reward (Manager) coupon.model_coupon_reward sales_team.group_sale_manager 1 1 1 0
10 access_sale_coupon_apply_code Apply Coupon model_sale_coupon_apply_code sales_team.group_sale_salesman 1 1 1 0
11 access_sale_coupon_generate Coupon Generation coupon.model_coupon_generate_wizard sales_team.group_sale_salesman 1 1 1 0
@@ -6,10 +6,5 @@
<field name="model_id" ref="model_sale_coupon_apply_code"/>
<field name="domain_force">[('create_uid', '=', user.id)]</field>
</record>
<record id="sale_coupon_generate_rule" model="ir.rule">
<field name="name">Generate Sales Coupon Rule</field>
<field name="model_id" ref="model_sale_coupon_generate"/>
<field name="domain_force">[('create_uid', '=', user.id)]</field>
</record>
</odoo>
+4 -4
View File
@@ -16,7 +16,7 @@ class TestSaleCouponCommon(TestSaleProductAttributeValueCommon):
pricelist.currency_id = cls._setup_currency(cls.currency_ratio)
# Set all the existing programs to active=False to avoid interference
cls.env['sale.coupon.program'].search([]).write({'active': False})
cls.env['coupon.program'].search([]).write({'active': False})
# create partner for sale order.
cls.steve = cls.env['res.partner'].create({
@@ -70,7 +70,7 @@ class TestSaleCouponCommon(TestSaleProductAttributeValueCommon):
# Immediate Program By A + B: get B free
# No Conditions
cls.immediate_promotion_program = cls.env['sale.coupon.program'].create({
cls.immediate_promotion_program = cls.env['coupon.program'].create({
'name': 'Buy A + 1 B, 1 B are free',
'promo_code_usage': 'no_code_needed',
'reward_type': 'product',
@@ -79,7 +79,7 @@ class TestSaleCouponCommon(TestSaleProductAttributeValueCommon):
'active': True,
})
cls.code_promotion_program = cls.env['sale.coupon.program'].create({
cls.code_promotion_program = cls.env['coupon.program'].create({
'name': 'Buy 1 A + Enter code, 1 A is free',
'promo_code_usage': 'code_needed',
'reward_type': 'product',
@@ -88,7 +88,7 @@ class TestSaleCouponCommon(TestSaleProductAttributeValueCommon):
'active': True,
})
cls.code_promotion_program_with_discount = cls.env['sale.coupon.program'].create({
cls.code_promotion_program_with_discount = cls.env['coupon.program'].create({
'name': 'Buy 1 C + Enter code, 10 percent discount on C',
'promo_code_usage': 'code_needed',
'reward_type': 'discount',
@@ -46,7 +46,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
'partner_id': self.steve.id
})
self.p1 = self.env['sale.coupon.program'].create({
self.p1 = self.env['coupon.program'].create({
'name': 'Code for 10% on orders',
'promo_code_usage': 'code_needed',
'promo_code': 'test_10pc',
@@ -54,7 +54,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
'discount_percentage': 10.0,
'program_type': 'promotion_program',
})
self.p2 = self.env['sale.coupon.program'].create({
self.p2 = self.env['coupon.program'].create({
'name': 'Buy 3 cabinets, get one for free',
'promo_code_usage': 'no_code_needed',
'reward_type': 'product',
@@ -63,7 +63,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
'rule_min_quantity': 3,
'rule_products_domain': '[["name","ilike","large cabinet"]]',
})
self.p3 = self.env['sale.coupon.program'].create({
self.p3 = self.env['coupon.program'].create({
'name': 'Buy 1 drawer black, get a free Large Meeting Table',
'promo_code_usage': 'no_code_needed',
'reward_type': 'product',
@@ -155,7 +155,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
'amount': 15,
'price_include': True,
})
p_specific_product = self.env['sale.coupon.program'].create({
p_specific_product = self.env['coupon.program'].create({
'name': '20% reduction on Large Cabinet in cart',
'promo_code_usage': 'no_code_needed',
'reward_type': 'discount',
@@ -307,7 +307,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
# Create needed programs
self.p2.active = False
self.p_large_cabinet = self.env['sale.coupon.program'].create({
self.p_large_cabinet = self.env['coupon.program'].create({
'name': 'Buy 1 large cabinet, get one for free',
'promo_code_usage': 'no_code_needed',
'reward_type': 'product',
@@ -315,7 +315,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
'reward_product_id': self.largeCabinet.id,
'rule_products_domain': '[["name","ilike","large cabinet"]]',
})
self.p_conference_chair = self.env['sale.coupon.program'].create({
self.p_conference_chair = self.env['coupon.program'].create({
'name': 'Buy 1 chair, get one for free',
'promo_code_usage': 'no_code_needed',
'reward_type': 'product',
@@ -323,7 +323,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
'reward_product_id': self.conferenceChair.id,
'rule_products_domain': '[["name","ilike","conference chair"]]',
})
self.p_pedal_bin = self.env['sale.coupon.program'].create({
self.p_pedal_bin = self.env['coupon.program'].create({
'name': 'Buy 1 bin, get one for free',
'promo_code_usage': 'no_code_needed',
'reward_type': 'product',
@@ -391,7 +391,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
# -- End test inside the test
# Now we want to apply a 20% discount only on Large Cabinet
self.env['sale.coupon.program'].create({
self.env['coupon.program'].create({
'name': '20% reduction on Large Cabinet in cart',
'promo_code_usage': 'no_code_needed',
'reward_type': 'discount',
@@ -485,7 +485,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
def test_program_fixed_price(self):
# Check fixed amount discount
order = self.empty_order
fixed_amount_program = self.env['sale.coupon.program'].create({
fixed_amount_program = self.env['coupon.program'].create({
'name': '$249 discount',
'promo_code_usage': 'no_code_needed',
'program_type': 'promotion_program',
@@ -520,7 +520,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
def test_program_next_order(self):
order = self.empty_order
self.env['sale.coupon.program'].create({
self.env['coupon.program'].create({
'name': 'Free Pedal Bin if at least 1 article',
'promo_code_usage': 'no_code_needed',
'promo_applicability': 'on_next_order',
@@ -571,7 +571,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
'order_id': order.id,
})
self.assertEqual(order.amount_total, 165.0, "The order amount is not correct")
discount_coupon_program = self.env['sale.coupon.program'].create({
discount_coupon_program = self.env['coupon.program'].create({
'name': '$100 coupon',
'program_type': 'coupon_program',
'reward_type': 'discount',
@@ -581,7 +581,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
'discount_apply_on': 'on_order',
'rule_minimum_amount': 100.00,
})
self.env['sale.coupon.generate'].with_context(active_id=discount_coupon_program.id).create({}).generate_coupon()
self.env['coupon.generate.wizard'].with_context(active_id=discount_coupon_program.id).create({}).generate_coupon()
coupon = discount_coupon_program.coupon_ids[0]
self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
'coupon_code': coupon.code
@@ -604,7 +604,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
})
self.assertEqual(order.amount_total, 165.0, "The order amount is not correct")
discount_promotion_program = self.env['sale.coupon.program'].create({
discount_promotion_program = self.env['coupon.program'].create({
'name': '$100 promotion program',
'program_type': 'promotion_program',
'promo_code_usage': 'code_needed',
@@ -624,7 +624,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
order.recompute_coupon_lines()
self.assertEqual(order.amount_total, 65.0, "The promotion program should not be removed after recomputation")
discount_coupon_program = self.env['sale.coupon.program'].create({
discount_coupon_program = self.env['coupon.program'].create({
'name': '$100 coupon',
'program_type': 'coupon_program',
'reward_type': 'discount',
@@ -634,7 +634,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
'discount_apply_on': 'on_order',
'rule_minimum_amount': 100.00,
})
self.env['sale.coupon.generate'].with_context(active_id=discount_coupon_program.id).create({}).generate_coupon()
self.env['coupon.generate.wizard'].with_context(active_id=discount_coupon_program.id).create({}).generate_coupon()
coupon = discount_coupon_program.coupon_ids[0]
with self.assertRaises(UserError):
self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
@@ -656,7 +656,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
- Advanced: This discount must be split by different taxes
"""
order = self.empty_order
p_specific_products = self.env['sale.coupon.program'].create({
p_specific_products = self.env['coupon.program'].create({
'name': '20% reduction on Conference Chair and Drawer Black in cart',
'promo_code_usage': 'no_code_needed',
'reward_type': 'discount',
@@ -182,7 +182,7 @@ class TestProgramRules(TestSaleCouponCommon):
self.immediate_promotion_program.active = False # Avoid having this program to add rewards on this test
order = self.empty_order
program = self.env['sale.coupon.program'].create({
program = self.env['coupon.program'].create({
'name': 'Get 10% discount if buy at least 4 Product A and $320',
'program_type': 'coupon_program',
'reward_type': 'discount',
@@ -208,7 +208,7 @@ class TestProgramRules(TestSaleCouponCommon):
})
# Default value for coupon generate wizard is generate by quantity and generate only one coupon
self.env['sale.coupon.generate'].with_context(active_id=program.id).create({}).generate_coupon()
self.env['coupon.generate.wizard'].with_context(active_id=program.id).create({}).generate_coupon()
coupon = program.coupon_ids[0]
# Not enough amount since we only have 220 (100*2 + 5*4)
@@ -250,7 +250,7 @@ class TestProgramRules(TestSaleCouponCommon):
self.immediate_promotion_program.active = False # Avoid having this program to add rewards on this test
order = self.empty_order
program_5pc = self.env['sale.coupon.program'].create({
program_5pc = self.env['coupon.program'].create({
'name': 'Get 5% discount if buy at least 2 Product',
'program_type': 'promotion_program',
'reward_type': 'discount',
@@ -259,7 +259,7 @@ class TestProgramRules(TestSaleCouponCommon):
'rule_min_quantity': 2,
'promo_code_usage': 'no_code_needed',
})
program_10pc = self.env['sale.coupon.program'].create({
program_10pc = self.env['coupon.program'].create({
'name': 'Get 10% discount if buy at least 4 Product',
'program_type': 'promotion_program',
'reward_type': 'discount',
@@ -18,7 +18,7 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
# In this case, it will generate the coupon for every partner.
# Thus, we should ensure that if you leave the domain untouched, it generates a coupon for each partner
# as hinted on the screen ('Match all records (X records)')
self.env['sale.coupon.generate'].with_context(active_id=self.code_promotion_program.id).create({
self.env['coupon.generate.wizard'].with_context(active_id=self.code_promotion_program.id).create({
'generation_type': 'nbr_customer',
}).generate_coupon()
self.assertEqual(len(self.code_promotion_program.coupon_ids), len(self.env['res.partner'].search([])), "It should have generated a coupon for every partner")
@@ -28,7 +28,7 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
self.code_promotion_program.reward_type = 'discount'
self.env['sale.coupon.generate'].with_context(active_id=self.code_promotion_program.id).create({
self.env['coupon.generate.wizard'].with_context(active_id=self.code_promotion_program.id).create({
'generation_type': 'nbr_customer',
'partners_domain': "[('id', 'in', [%s])]" % (self.steve.id),
}).generate_coupon()
@@ -69,7 +69,7 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
def test_coupon_code_with_pricelist(self):
# Test case: Generate a coupon (10% discount) and apply it on an order with a specific pricelist (10% discount)
self.env['sale.coupon.generate'].with_context(active_id=self.code_promotion_program_with_discount.id).create({
self.env['coupon.generate.wizard'].with_context(active_id=self.code_promotion_program_with_discount.id).create({
'generation_type': 'nbr_coupon',
'nbr_coupons': 1,
}).generate_coupon()
@@ -127,7 +127,7 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
'promo_code': 'free_B_on_next_order',
})
# 2.
self.p1 = self.env['sale.coupon.program'].create({
self.p1 = self.env['coupon.program'].create({
'name': 'Code for 10% on next order',
'discount_type': 'percentage',
'discount_percentage': 10.0,
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Menu Items -->
<menuitem
id="menu_coupon_type_config"
action="coupon.coupon_program_action_coupon_program"
parent="sale.product_menu_catalog"
name="Coupon Programs"
groups="sales_team.group_sale_manager"
sequence="5"
/>
<menuitem
id="menu_promotion_type_config"
action="coupon.coupon_program_action_promo_program"
parent="sale.product_menu_catalog"
name="Promotion Programs"
groups="sales_team.group_sale_manager"
sequence="4"
/>
<!-- Form Views -->
<record id="sale_coupon_program_view_coupon_program_form" model="ir.ui.view">
<field name="name">coupon.program.form</field>
<field name="model">coupon.program</field>
<field name="inherit_id" ref="coupon.coupon_program_view_coupon_program_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='coupon_count']/.." position="before">
<button class="oe_stat_button" type="object" icon="fa-usd" name="action_view_sales_orders">
<field name="order_count" string="Sales" widget="statinfo"/>
</button>
</xpath>
</field>
</record>
<record id="sale_coupon_program_view_promo_program_form" model="ir.ui.view">
<field name="name">coupon.program.form</field>
<field name="model">coupon.program</field>
<field name="inherit_id" ref="coupon.coupon_program_view_promo_program_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='coupon_count']/.." position="before">
<button class="oe_stat_button" type="object" icon="fa-usd" name="action_view_sales_orders">
<field name="order_count" string="Sales" widget="statinfo"/>
</button>
</xpath>
</field>
</record>
<!-- Kanban Views -->
<record id="sale_coupon_view_coupon_program_kanban" model="ir.ui.view">
<field name="name">coupon.program.kanban</field>
<field name="model">coupon.program</field>
<field name="inherit_id" ref="coupon.view_coupon_program_kanban" />
<field name="arch" type="xml">
<xpath expr="//div[hasclass('coupon-count-label')]" position="after">
<div class="col-4 text-center">
<strong>Sales</strong>
</div>
</xpath>
<xpath expr="//div[hasclass('coupon-count-value')]" position="after">
<div class="col-4 text-center">
<field name="order_count"/>
</div>
</xpath>
</field>
</record>
</odoo>
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="sale_coupon_view_tree" model="ir.ui.view">
<field name="name">coupon.coupon.tree</field>
<field name="model">coupon.coupon</field>
<field name="inherit_id" ref="coupon.coupon_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="after">
<field name="order_id"/>
</xpath>
</field>
</record>
<record id="sale_coupon_view_form" model="ir.ui.view">
<field name="name">coupon.coupon.form</field>
<field name="model">coupon.coupon</field>
<field name="inherit_id" ref="coupon.coupon_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="after">
<field name="order_id"/>
<field name="sales_order_id"/>
</xpath>
</field>
</record>
</odoo>
@@ -9,10 +9,10 @@
<xpath expr="//div[@id='sale_coupon']" position="after">
<div class="content-group">
<div class="mt8" attrs="{'invisible': [('module_sale_coupon', '=', False)]}">
<button name="%(sale_coupon.sale_coupon_program_action_promo_program)d" icon="fa-arrow-right" type="action" string="Promotion Programs" class="btn-link"/>
<button name="%(coupon.coupon_program_action_promo_program)d" icon="fa-arrow-right" type="action" string="Promotion Programs" class="btn-link"/>
</div>
<div class="mt8" attrs="{'invisible': [('module_sale_coupon', '=', False)]}">
<button name="%(sale_coupon.sale_coupon_program_action_coupon_program)d" icon="fa-arrow-right" type="action" string="Coupon Programs" class="btn-link"/>
<button name="%(coupon.coupon_program_action_coupon_program)d" icon="fa-arrow-right" type="action" string="Coupon Programs" class="btn-link"/>
</div>
</div>
</xpath>
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import sale_coupon_apply_code
@@ -24,7 +24,7 @@ class SaleCouponApplyCode(models.TransientModel):
def apply_coupon(self, order, coupon_code):
error_status = {}
program = self.env['sale.coupon.program'].search([('promo_code', '=', coupon_code)])
program = self.env['coupon.program'].search([('promo_code', '=', coupon_code)])
if program:
error_status = program._check_promo_code(order, coupon_code)
if not error_status:
@@ -42,7 +42,7 @@ class SaleCouponApplyCode(models.TransientModel):
order._create_reward_line(program)
order.code_promo_program_id = program
else:
coupon = self.env['sale.coupon'].search([('code', '=', coupon_code)], limit=1)
coupon = self.env['coupon.coupon'].search([('code', '=', coupon_code)], limit=1)
if coupon:
error_status = coupon._check_coupon_code(order)
if not error_status:
@@ -5,10 +5,10 @@
from odoo import models, _
class SaleCoupon(models.Model):
_inherit = "sale.coupon"
class Coupon(models.Model):
_inherit = "coupon.coupon"
def _check_coupon_code(self, order):
if self.program_id.reward_type == 'free_shipping' and not order.order_line.filtered(lambda line: line.is_delivery):
return {'error': _('The shipping costs are not in the order lines.')}
return super(SaleCoupon, self)._check_coupon_code(order)
return super(Coupon, self)._check_coupon_code(order)
@@ -5,14 +5,14 @@
from odoo import models, _, api
class SaleCouponProgram(models.Model):
_inherit = "sale.coupon.program"
class CouponProgram(models.Model):
_inherit = "coupon.program"
def _filter_not_ordered_reward_programs(self, order):
"""
Returns the programs when the reward is actually in the order lines
"""
programs = super(SaleCouponProgram, self)._filter_not_ordered_reward_programs(order)
programs = super(CouponProgram, self)._filter_not_ordered_reward_programs(order)
# Do not filter on free delivery programs. As delivery_unset is called everywhere (which is
# rather stupid), the delivery line is unliked to be created again instead of writing on it to
# modify the price_unit. That way, the reward is unlink and is not set back again.
@@ -21,4 +21,4 @@ class SaleCouponProgram(models.Model):
def _check_promo_code(self, order, coupon_code):
if self.reward_type == 'free_shipping' and not order.order_line.filtered(lambda line: line.is_delivery):
return {'error': _('The shipping costs are not in the order lines.')}
return super(SaleCouponProgram, self)._check_promo_code(order, coupon_code)
return super(CouponProgram, self)._check_promo_code(order, coupon_code)
@@ -4,15 +4,15 @@
from odoo import fields, models, _
class SaleCouponReward(models.Model):
_inherit = 'sale.coupon.reward'
_description = "Sales Coupon Reward"
class CouponReward(models.Model):
_inherit = 'coupon.reward'
_description = "Coupon Reward"
reward_type = fields.Selection(selection_add=[('free_shipping', 'Free Shipping')])
def name_get(self):
result = []
reward_names = super(SaleCouponReward, self).name_get()
reward_names = super(CouponReward, self).name_get()
free_shipping_reward_ids = self.filtered(lambda reward: reward.reward_type == 'free_shipping').ids
for res in reward_names:
result.append((res[0], res[0] in free_shipping_reward_ids and _("Free Shipping") or res[1]))
@@ -9,13 +9,13 @@ class SaleOrder(models.Model):
def _get_no_effect_on_threshold_lines(self):
self.ensure_one()
# Do not count shipping and free shipping
free_delivery_product = self.env['sale.coupon.program'].search([('reward_type', '=', 'free_shipping')]).mapped('discount_line_product_id')
free_delivery_product = self.env['coupon.program'].search([('reward_type', '=', 'free_shipping')]).mapped('discount_line_product_id')
lines = self.order_line.filtered(lambda line: line.is_delivery or line.product_id in free_delivery_product)
return lines + super(SaleOrder, self)._get_no_effect_on_threshold_lines()
def _get_paid_order_lines(self):
""" Returns the taxes included sale order total amount without the rewards amount"""
free_reward_product = self.env['sale.coupon.program'].search([('reward_type', '=', 'product')]).mapped('discount_line_product_id')
free_reward_product = self.env['coupon.program'].search([('reward_type', '=', 'product')]).mapped('discount_line_product_id')
return self.order_line.filtered(lambda x: not (x.is_reward_line or x.is_delivery) or x.product_id in free_reward_product)
def _get_reward_line_values(self, program):
@@ -57,7 +57,7 @@ class TestSaleCouponProgramRules(TestSaleCouponCommon):
# Test case 1: The minimum amount is not reached, the reward should
# not be created
self.immediate_promotion_program.active = False
self.env['sale.coupon.program'].create({
self.env['coupon.program'].create({
'name': 'Free Shipping if at least 100 euros',
'promo_code_usage': 'no_code_needed',
'reward_type': 'free_shipping',
@@ -133,14 +133,14 @@ class TestSaleCouponProgramRules(TestSaleCouponCommon):
def test_shipping_cost(self):
# Free delivery should not be taken into account when checking for minimum required threshold
p_minimum_threshold_free_delivery = self.env['sale.coupon.program'].create({
p_minimum_threshold_free_delivery = self.env['coupon.program'].create({
'name': 'free shipping if > 872 tax exl',
'promo_code_usage': 'no_code_needed',
'reward_type': 'free_shipping',
'program_type': 'promotion_program',
'rule_minimum_amount': 872,
})
p_minimum_threshold_discount = self.env['sale.coupon.program'].create({
p_minimum_threshold_discount = self.env['coupon.program'].create({
'name': '10% reduction if > 872 tax exl',
'promo_code_usage': 'no_code_needed',
'reward_type': 'discount',
@@ -186,7 +186,7 @@ class TestSaleCouponProgramRules(TestSaleCouponCommon):
def test_shipping_cost_numbers(self):
# Free delivery should not be taken into account when checking for minimum required threshold
p_minimum_threshold_free_delivery = self.env['sale.coupon.program'].create({
p_minimum_threshold_free_delivery = self.env['coupon.program'].create({
'name': 'free shipping if > 872 tax exl',
'promo_code_usage': 'code_needed',
'promo_code': 'free_shipping',
@@ -194,7 +194,7 @@ class TestSaleCouponProgramRules(TestSaleCouponCommon):
'program_type': 'promotion_program',
'rule_minimum_amount': 872,
})
self.p2 = self.env['sale.coupon.program'].create({
self.p2 = self.env['coupon.program'].create({
'name': 'Buy 4 large cabinet, get one for free',
'promo_code_usage': 'no_code_needed',
'reward_type': 'product',
@@ -240,7 +240,7 @@ class TestSaleCouponProgramRules(TestSaleCouponCommon):
self.assertEqual(sum([line.price_total for line in order._get_no_effect_on_threshold_lines()]), 0)
self.assertEqual(order.amount_untaxed, 872.73)
p_specific_product = self.env['sale.coupon.program'].create({
p_specific_product = self.env['coupon.program'].create({
'name': '20% reduction on large cabinet in cart',
'promo_code_usage': 'no_code_needed',
'reward_type': 'discount',
@@ -5,9 +5,9 @@ from odoo import api, models, _
from odoo.exceptions import ValidationError
class SaleCouponProgram(models.Model):
_name = 'sale.coupon.program'
_inherit = ['sale.coupon.program', 'website.multi.mixin']
class CouponProgram(models.Model):
_name = 'coupon.program'
_inherit = ['coupon.program', 'website.multi.mixin']
@api.constrains('promo_code', 'website_id')
def _check_promo_code_constraint(self):
@@ -83,7 +83,7 @@ class SaleOrder(models.Model):
ICP = self.env['ir.config_parameter']
validity = ICP.get_param('website_sale_coupon.abandonned_coupon_validity', 4)
validity = fields.Datetime.to_string(fields.datetime.now() - timedelta(days=int(validity)))
coupon_to_reset = self.env['sale.coupon'].search([
coupon_to_reset = self.env['coupon.coupon'].search([
('state', '=', 'used'),
('sales_order_id.state', '=', 'draft'),
('sales_order_id.write_date', '<', validity),
@@ -60,7 +60,7 @@ class TestUi(TestSaleProductAttributeValueCommon, HttpCase):
'taxes_id': False,
})
self.env['sale.coupon.program'].create({
self.env['coupon.program'].create({
'name': "Buy 3 Small Cabinets, get one for free",
'promo_code_usage': 'no_code_needed',
'discount_apply_on': 'on_order',
@@ -72,7 +72,7 @@ class TestUi(TestSaleProductAttributeValueCommon, HttpCase):
'discount_line_product_id': free_large_cabinet.id
})
self.env['sale.coupon.program'].create({
self.env['coupon.program'].create({
'name': "Code for 10% on orders",
'promo_code_usage': 'code_needed',
'promo_code': 'testcode',
@@ -92,7 +92,7 @@ class TestWebsiteSaleCoupon(TransactionCase):
def setUp(self):
super(TestWebsiteSaleCoupon, self).setUp()
program = self.env['sale.coupon.program'].create({
program = self.env['coupon.program'].create({
'name': '10% TEST Discount',
'promo_code_usage': 'code_needed',
'discount_apply_on': 'on_order',
@@ -101,7 +101,7 @@ class TestWebsiteSaleCoupon(TransactionCase):
'program_type': 'coupon_program',
})
self.env['sale.coupon.generate'].with_context(active_id=program.id).create({}).generate_coupon()
self.env['coupon.generate.wizard'].with_context(active_id=program.id).create({}).generate_coupon()
self.coupon = program.coupon_ids[0]
self.steve = self.env['res.partner'].create({
@@ -9,10 +9,10 @@
<xpath expr="//div[@id='website_sale_coupon']" position="after">
<div class="content-group">
<div class="mt8" attrs="{'invisible': [('module_sale_coupon', '=', False)]}">
<button name="%(sale_coupon.sale_coupon_program_action_promo_program)d" icon="fa-arrow-right" type="action" string="Promotion Programs" class="btn-link"/>
<button name="%(coupon.coupon_program_action_promo_program)d" icon="fa-arrow-right" type="action" string="Promotion Programs" class="btn-link"/>
</div>
<div class="mt8" attrs="{'invisible': [('module_sale_coupon', '=', False)]}">
<button name="%(sale_coupon.sale_coupon_program_action_coupon_program)d" icon="fa-arrow-right" type="action" string="Coupon Programs" class="btn-link"/>
<button name="%(coupon.coupon_program_action_coupon_program)d" icon="fa-arrow-right" type="action" string="Coupon Programs" class="btn-link"/>
</div>
</div>
</xpath>
@@ -1,13 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem action="sale_coupon.sale_coupon_program_action_promo_program" id="menu_promotion_type_config" name="Promotion Programs" parent="website_sale.menu_catalog" groups="sales_team.group_sale_manager" sequence="50"/>
<menuitem id="menu_coupon_type_config" action="sale_coupon.sale_coupon_program_action_coupon_program" name="Coupon Programs" parent="website_sale.menu_catalog" groups="sales_team.group_sale_manager" sequence="51"/>
<menuitem action="coupon.coupon_program_action_promo_program" id="menu_promotion_type_config" name="Promotion Programs" parent="website_sale.menu_catalog" groups="sales_team.group_sale_manager" sequence="50"/>
<menuitem id="menu_coupon_type_config" action="coupon.coupon_program_action_coupon_program" name="Coupon Programs" parent="website_sale.menu_catalog" groups="sales_team.group_sale_manager" sequence="51"/>
<record model="ir.ui.view" id="sale_coupon_program_view_form_common_website">
<field name="name">sale.coupon.program.common.form</field>
<field name="model">sale.coupon.program</field>
<field name="inherit_id" ref="sale_coupon.sale_coupon_program_view_form_common"/>
<field name="name">coupon.program.common.form</field>
<field name="model">coupon.program</field>
<field name="inherit_id" ref="coupon.coupon_program_view_form_common"/>
<field name="arch" type="xml">
<group name="validity" position="inside">
<label for="website_id" groups="website.group_multi_website"/>
@@ -19,9 +19,9 @@
</record>
<record id="sale_coupon_program_view_tree_website" model="ir.ui.view">
<field name="name">sale.coupon.program.tree</field>
<field name="model">sale.coupon.program</field>
<field name="inherit_id" ref="sale_coupon.sale_coupon_program_view_tree"/>
<field name="name">coupon.program.tree</field>
<field name="model">coupon.program</field>
<field name="inherit_id" ref="coupon.coupon_program_view_tree"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="website_id" groups="website.group_multi_website"/>