[MERGE] sale_coupon,point_of_sale: Merge Coupon, Loyalty Cards, eWallets models
This module is an unification of `coupon`, `gift_card` and an extraction of the program models of `pos_loyalty`. The goal of this module is to unify the creation of coupon, promotion, gift_card, loyalty programs etc.. into a single base module and share the same models. Managers will be able to create programs based on rules and rewards. Programs may apply on the current order (rules and rewards) or on future orders, where the rules must match the first order to get a reward on the second order or even be nominative and accumulate points over multiple orders. Every program is based on a point system. And each use of the program will result in the creation of a coupon (except for nominative programs which should be limited to one card per customer). A rule may filter on quantity, money spent and products and give points on the order, the amount of units paid or the amount of money spent. A reward could be a free product, a discount or (with an additional module) free shipping. Discounts can be percentage based, fixed, or based on the amount of points the card has. They can also be filtered on products or tags, to discount specific products. A new feature is also being able to select communication plan for these programs. The manager can define a template to be sent upon the creation of a new coupon/card or when reaching a certain amount of points. TaskId-2675382 Relates to: - Community: https://github.com/odoo/odoo/pull/82250 - Enterprise: https://github.com/odoo/enterprise/pull/23256 - Upgrade: https://github.com/odoo/upgrade/pull/3234 Original PRs: - coupon: https://github.com/odoo/odoo/pull/38266 - gift_card: https://github.com/odoo/odoo/pull/64195 - pos_loyalty commit: https://github.com/odoo/enterprise/commit/376f7ca4870e995438cd91b9289ace753320e32e - sale_coupon: https://github.com/odoo/enterprise/pull/13 - sale_gift_card: https://github.com/odoo/odoo/pull/64195 - website_sale_gift_card: https://github.com/odoo/odoo/pull/64195 - website_sale_coupon_delivery: https://github.com/odoo/odoo/issues/31857 - pos_coupon pr: https://github.com/odoo/odoo/pull/38382 - pos_gift_card pr: https://github.com/odoo/odoo/pull/75518 - sale_coupon_taxcloud: https://github.com/odoo/enterprise/commit/d08e5083fe89df231766bf57606515677405e1ad - sale_coupon_taxcloud_delivery: https://github.com/odoo/enterprise/commit/010926e28c08226531b21466ee092fe503aff497 - helpdesk_sale_coupon: https://github.com/odoo/enterprise/commit/4c81b37abb89ef748aa59cf193ed2f59ab974bc0 closes odoo/odoo#82250 Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
@@ -1,25 +0,0 @@
|
||||
# -*- 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/ir.model.access.csv',
|
||||
'security/coupon_security.xml',
|
||||
'views/coupon_views.xml',
|
||||
'views/coupon_program_views.xml',
|
||||
'report/coupon_report.xml',
|
||||
'report/coupon_report_templates.xml',
|
||||
'data/expire_coupon_cron.xml',
|
||||
],
|
||||
'demo': [
|
||||
'demo/coupon_demo.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="expire_coupon_cron" model="ir.cron">
|
||||
<field name="name">Coupon: expire coupon based on date</field>
|
||||
<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"/>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,60 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="product_product_10_percent_discount" model="product.product">
|
||||
<field name="name">10.0% discount on total amount</field>
|
||||
<field name="type">service</field>
|
||||
<field name="taxes_id" eval="False"/>
|
||||
<field name="supplier_taxes_id" eval="False"/>
|
||||
<field name="sale_ok" eval="False"/>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
<field name="default_code">10PERCENTDISC</field>
|
||||
<field name="categ_id" ref="product.product_category_3"/>
|
||||
</record>
|
||||
|
||||
<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>
|
||||
<field name="discount_apply_on">on_order</field>
|
||||
<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="coupon.product_product_10_percent_discount"/>
|
||||
<field name="validity_duration">0</field>
|
||||
</record>
|
||||
|
||||
<record id="product_product_free_large_cabinet" model="product.product">
|
||||
<field name="name">Free Product - Large Cabinet</field>
|
||||
<field name="type">service</field>
|
||||
<field name="taxes_id" eval="False"/>
|
||||
<field name="supplier_taxes_id" eval="False"/>
|
||||
<field name="sale_ok" eval="False"/>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
<field name="default_code">FREELARGECABINET</field>
|
||||
<field name="categ_id" ref="product.product_category_3"/>
|
||||
</record>
|
||||
|
||||
<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>
|
||||
<field name="reward_type">product</field>
|
||||
<field name="program_type">promotion_program</field>
|
||||
<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="coupon.product_product_free_large_cabinet"/>
|
||||
<field name="validity_duration">0</field>
|
||||
</record>
|
||||
|
||||
<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>
|
||||
<field name="discount_type">percentage</field>
|
||||
<field name="discount_percentage">10.0</field>
|
||||
<field name="program_type">coupon_program</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,8 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mail_compose_message
|
||||
from . import coupon
|
||||
from . import coupon_reward
|
||||
from . import coupon_rules
|
||||
from . import coupon_program
|
||||
@@ -1,117 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import random
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
class Coupon(models.Model):
|
||||
_name = 'coupon.coupon'
|
||||
_description = "Coupon"
|
||||
_rec_name = 'code'
|
||||
|
||||
@api.model
|
||||
def _generate_code(self):
|
||||
"""Generate a 20 char long pseudo-random string of digits for barcode
|
||||
generation.
|
||||
|
||||
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
|
||||
compatible with all scanners.
|
||||
"""
|
||||
return str(uuid4())[:22]
|
||||
|
||||
code = fields.Char(default=lambda self: self._generate_code(), required=True, readonly=True)
|
||||
expiration_date = fields.Date('Expiration Date', compute='_compute_expiration_date')
|
||||
state = fields.Selection([
|
||||
('reserved', 'Pending'),
|
||||
('new', 'Valid'),
|
||||
('sent', 'Sent'),
|
||||
('used', 'Used'),
|
||||
('expired', 'Expired'),
|
||||
('cancel', 'Cancelled')
|
||||
], required=True, default='new')
|
||||
partner_id = fields.Many2one('res.partner', "For Customer")
|
||||
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.')
|
||||
|
||||
_sql_constraints = [
|
||||
('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 _get_default_template(self):
|
||||
return False
|
||||
|
||||
def action_coupon_send(self):
|
||||
""" Open a window to compose an email, with the edi invoice template
|
||||
message loaded by default
|
||||
"""
|
||||
self.ensure_one()
|
||||
default_template = self._get_default_template()
|
||||
compose_form = self.env.ref('mail.email_compose_message_wizard_form', False)
|
||||
ctx = dict(
|
||||
default_model='coupon.coupon',
|
||||
default_res_id=self.id,
|
||||
default_use_template=bool(default_template),
|
||||
default_template_id=default_template and default_template.id,
|
||||
default_composition_mode='comment',
|
||||
default_email_layout_xmlid='mail.mail_notification_light',
|
||||
mark_coupon_as_sent=True,
|
||||
force_email=True,
|
||||
)
|
||||
return {
|
||||
'name': _('Compose Email'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'mail.compose.message',
|
||||
'views': [(compose_form.id, 'form')],
|
||||
'view_id': compose_form.id,
|
||||
'target': 'new',
|
||||
'context': ctx,
|
||||
}
|
||||
|
||||
def action_coupon_cancel(self):
|
||||
self.state = 'cancel'
|
||||
|
||||
def cron_expire_coupon(self):
|
||||
self._cr.execute("""
|
||||
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()""")
|
||||
|
||||
expired_ids = [res[0] for res in self._cr.fetchall()]
|
||||
self.browse(expired_ids).write({'state': 'expired'})
|
||||
|
||||
def _check_coupon_code(self, order_date, partner_id, **kwargs):
|
||||
""" Check the validity of this single coupon.
|
||||
:param order_date Date:
|
||||
:param partner_id int | boolean:
|
||||
"""
|
||||
self.ensure_one()
|
||||
message = {}
|
||||
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):
|
||||
message = {'error': _('This coupon is expired (%s).') % (self.code)}
|
||||
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.id != partner_id:
|
||||
message = {'error': _('Invalid partner.')}
|
||||
return message
|
||||
@@ -1,175 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
import ast
|
||||
|
||||
|
||||
class CouponProgram(models.Model):
|
||||
_name = 'coupon.program'
|
||||
_description = "Coupon Program"
|
||||
_inherits = {'coupon.rule': 'rule_id', 'coupon.reward': 'reward_id'}
|
||||
# We should apply 'discount' promotion first to avoid offering free product when we should not.
|
||||
# Eg: If the discount lower the SO total below the required threshold
|
||||
# Note: This is only revelant when programs have the same sequence (which they have by default)
|
||||
_order = "sequence, reward_type"
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
active = fields.Boolean('Active', default=True, help="A program is available for the customers when active")
|
||||
rule_id = fields.Many2one('coupon.rule', string="Coupon Rule", ondelete='restrict', required=True)
|
||||
reward_id = fields.Many2one('coupon.reward', string="Reward", ondelete='restrict', required=True, copy=False)
|
||||
sequence = fields.Integer(copy=False,
|
||||
help="Coupon program will be applied based on given sequence if multiple programs are " +
|
||||
"defined on same condition(For minimum amount)")
|
||||
maximum_use_number = fields.Integer(help="Maximum number of sales orders in which reward can be provided")
|
||||
program_type = fields.Selection([
|
||||
('promotion_program', 'Promotional Program'),
|
||||
('coupon_program', 'Coupon Program'),
|
||||
],
|
||||
help="""A promotional program can be either a limited promotional offer without code (applied automatically)
|
||||
or with a code (displayed on a magazine for example) that may generate a discount on the current
|
||||
order or create a coupon for a next order.
|
||||
|
||||
A coupon program generates coupons with a code that can be used to generate a discount on the current
|
||||
order or create a coupon for a next order.""")
|
||||
promo_code_usage = fields.Selection([
|
||||
('no_code_needed', 'Automatically Applied'),
|
||||
('code_needed', 'Use a code')],
|
||||
help="Automatically Applied - No code is required, if the program rules are met, the reward is applied (Except the global discount or the free shipping rewards which are not cumulative)\n" +
|
||||
"Use a code - If the program rules are met, a valid code is mandatory for the reward to be applied\n")
|
||||
promo_code = fields.Char('Promotion Code', copy=False,
|
||||
help="A promotion code is a code that is associated with a marketing discount. For example, a retailer might tell frequent customers to enter the promotion code 'THX001' to receive a 10%% discount on their whole order.")
|
||||
promo_applicability = fields.Selection([
|
||||
('on_current_order', 'Apply On Current Order'),
|
||||
('on_next_order', 'Send a Coupon')],
|
||||
default='on_current_order', string="Applicability")
|
||||
coupon_ids = fields.One2many('coupon.coupon', 'program_id', string="Generated Coupons", copy=False)
|
||||
coupon_count = fields.Integer(compute='_compute_coupon_count')
|
||||
company_id = fields.Many2one('res.company', string="Company", required=True, default=lambda self: self.env.company)
|
||||
currency_id = fields.Many2one(string="Currency", related='company_id.currency_id', readonly=True)
|
||||
validity_duration = fields.Integer(default=30,
|
||||
help="Validity duration for a coupon after its generation")
|
||||
total_order_count = fields.Integer("Total Order Count", compute="_compute_total_order_count")
|
||||
|
||||
@api.constrains('promo_code')
|
||||
def _check_promo_code_constraint(self):
|
||||
""" Program code must be unique """
|
||||
for program in self.filtered(lambda p: p.promo_code):
|
||||
domain = [('id', '!=', program.id), ('promo_code', '=', program.promo_code)]
|
||||
if self.search(domain):
|
||||
raise ValidationError(_('The program code must be unique!'))
|
||||
|
||||
@api.depends('coupon_ids')
|
||||
def _compute_coupon_count(self):
|
||||
coupon_data = self.env['coupon.coupon']._read_group([('program_id', 'in', self.ids)], ['program_id'], ['program_id'])
|
||||
mapped_data = dict([(m['program_id'][0], m['program_id_count']) for m in coupon_data])
|
||||
for program in self:
|
||||
program.coupon_count = mapped_data.get(program.id, 0)
|
||||
|
||||
@api.onchange('promo_code_usage')
|
||||
def _onchange_promo_code_usage(self):
|
||||
if self.promo_code_usage == 'no_code_needed':
|
||||
self.promo_code = False
|
||||
|
||||
@api.onchange('reward_product_id')
|
||||
def _onchange_reward_product_id(self):
|
||||
if self.reward_product_id:
|
||||
self.reward_product_uom_id = self.reward_product_id.uom_id
|
||||
|
||||
@api.onchange('discount_type')
|
||||
def _onchange_discount_type(self):
|
||||
if self.discount_type == 'fixed_amount':
|
||||
self.discount_apply_on = 'on_order'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
programs = super().create(vals_list)
|
||||
for program in programs:
|
||||
if program.discount_line_product_id:
|
||||
continue
|
||||
values = program._get_discount_product_values()
|
||||
discount_line_product_id = self.env['product.product'].create(values)
|
||||
program.write({'discount_line_product_id': discount_line_product_id.id})
|
||||
return programs
|
||||
|
||||
def write(self, vals):
|
||||
res = super(CouponProgram, self).write(vals)
|
||||
reward_fields = [
|
||||
'reward_type', 'reward_product_id', 'discount_type', 'discount_percentage',
|
||||
'discount_apply_on', 'discount_specific_product_ids', 'discount_fixed_amount'
|
||||
]
|
||||
if any(field in reward_fields for field in vals):
|
||||
self.mapped('discount_line_product_id').write({'name': self[0].reward_id.display_name})
|
||||
return res
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_active(self):
|
||||
if self.filtered('active'):
|
||||
raise UserError(_('You can not delete a program in active state'))
|
||||
|
||||
def unlink(self):
|
||||
# get reference to rule and reward
|
||||
rule = self.rule_id
|
||||
reward = self.reward_id
|
||||
# unlink the program
|
||||
super(CouponProgram, self).unlink()
|
||||
# then unlink the rule and reward
|
||||
rule.unlink()
|
||||
reward.unlink()
|
||||
return True
|
||||
|
||||
def toggle_active(self):
|
||||
super(CouponProgram, self).toggle_active()
|
||||
for program in self:
|
||||
program.discount_line_product_id.active = program.active
|
||||
coupons = self.filtered(lambda p: not p.active and p.promo_code_usage == 'code_needed').mapped('coupon_ids')
|
||||
coupons.filtered(lambda x: x.state != 'used').write({'state': 'expired'})
|
||||
|
||||
def _compute_program_amount(self, field, currency_to):
|
||||
self.ensure_one()
|
||||
return self.currency_id._convert(self[field], currency_to, self.company_id, fields.Date.today())
|
||||
|
||||
def _is_valid_partner(self, partner):
|
||||
if self.rule_partners_domain and self.rule_partners_domain != '[]':
|
||||
domain = ast.literal_eval(self.rule_partners_domain) + [('id', '=', partner.id)]
|
||||
return bool(self.env['res.partner'].search_count(domain))
|
||||
else:
|
||||
return True
|
||||
|
||||
def _get_valid_products(self, products):
|
||||
"""Get valid products for the program.
|
||||
|
||||
:param products: records of product.product
|
||||
:return: valid products recordset
|
||||
"""
|
||||
if self.rule_products_domain and self.rule_products_domain != "[]":
|
||||
domain = ast.literal_eval(self.rule_products_domain)
|
||||
return products.filtered_domain(domain)
|
||||
return products
|
||||
|
||||
def _generate_coupons(self, partner_id):
|
||||
'''Generate coupons that can be used in the next order for the given partner_id.'''
|
||||
generated_coupons = self.env['coupon.coupon']
|
||||
for program in self:
|
||||
generated_coupons |= self.env['coupon.coupon'].create({
|
||||
'program_id': program.id,
|
||||
'partner_id': partner_id,
|
||||
})
|
||||
return generated_coupons
|
||||
|
||||
def _compute_total_order_count(self):
|
||||
for program in self:
|
||||
program.total_order_count = 0
|
||||
|
||||
def _get_discount_product_values(self):
|
||||
return {
|
||||
'name': self.reward_id.display_name,
|
||||
'type': 'service',
|
||||
'taxes_id': False,
|
||||
'supplier_taxes_id': False,
|
||||
'sale_ok': False,
|
||||
'purchase_ok': False,
|
||||
'lst_price': 0, #Do not set a high value to avoid issue with coupon code
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class CouponReward(models.Model):
|
||||
_name = 'coupon.reward'
|
||||
_description = "Coupon Reward"
|
||||
_rec_name = 'reward_description'
|
||||
|
||||
# VFE FIXME multi company
|
||||
"""Rewards are not restricted to a company...
|
||||
You could have a reward_product_id limited to a specific company A.
|
||||
But still use this reward as reward of a program of company B...
|
||||
"""
|
||||
reward_description = fields.Char('Reward Description')
|
||||
reward_type = fields.Selection([
|
||||
('discount', 'Discount'),
|
||||
('product', 'Free Product'),
|
||||
], string='Reward Type', default='discount',
|
||||
help="Discount - Reward will be provided as discount.\n" +
|
||||
"Free Product - Free product will be provide as reward \n" +
|
||||
"Free Shipping - Free shipping will be provided as reward (Need delivery module)")
|
||||
# Product Reward
|
||||
reward_product_id = fields.Many2one('product.product', string="Free Product",
|
||||
help="Reward Product")
|
||||
reward_product_quantity = fields.Integer(string="Quantity", default=1, help="Reward product quantity")
|
||||
# Discount Reward
|
||||
discount_type = fields.Selection([
|
||||
('percentage', 'Percentage'),
|
||||
('fixed_amount', 'Fixed Amount')], default="percentage",
|
||||
help="Percentage - Entered percentage discount will be provided\n" +
|
||||
"Amount - Entered fixed amount discount will be provided")
|
||||
discount_percentage = fields.Float(string="Discount", default=10,
|
||||
help='The discount in percentage, between 1 and 100')
|
||||
discount_apply_on = fields.Selection([
|
||||
('on_order', 'On Order'),
|
||||
('cheapest_product', 'On Cheapest Product'),
|
||||
('specific_products', 'On Specific Products')], default="on_order",
|
||||
help="On Order - Discount on whole order\n" +
|
||||
"Cheapest product - Discount on cheapest product of the order\n" +
|
||||
"Specific products - Discount on selected specific products")
|
||||
discount_specific_product_ids = fields.Many2many('product.product', string="Products",
|
||||
help="Products that will be discounted if the discount is applied on specific products")
|
||||
discount_max_amount = fields.Float(default=0,
|
||||
help="Maximum amount of discount that can be provided")
|
||||
discount_fixed_amount = fields.Float(string="Fixed Amount", help='The discount in fixed amount')
|
||||
reward_product_uom_id = fields.Many2one(related='reward_product_id.product_tmpl_id.uom_id', string='Unit of Measure', readonly=True)
|
||||
discount_line_product_id = fields.Many2one('product.product', string='Reward Line Product', copy=False,
|
||||
help="Product used in the sales order to apply the discount. Each coupon program has its own reward product for reporting purpose")
|
||||
|
||||
@api.constrains('discount_percentage')
|
||||
def _check_discount_percentage(self):
|
||||
if self.filtered(lambda reward: reward.discount_type == 'percentage' and (reward.discount_percentage < 0 or reward.discount_percentage > 100)):
|
||||
raise ValidationError(_('Discount percentage should be between 1-100'))
|
||||
|
||||
def name_get(self):
|
||||
"""
|
||||
Returns a complete description of the reward
|
||||
"""
|
||||
result = []
|
||||
for reward in self:
|
||||
reward_string = ""
|
||||
if reward.reward_type == 'product':
|
||||
reward_string = _("Free Product - %s", reward.reward_product_id.name)
|
||||
elif reward.reward_type == 'discount':
|
||||
if reward.discount_type == 'percentage':
|
||||
reward_percentage = str(reward.discount_percentage)
|
||||
if reward.discount_apply_on == 'on_order':
|
||||
reward_string = _("%s%% discount on total amount", reward_percentage)
|
||||
elif reward.discount_apply_on == 'specific_products':
|
||||
if len(reward.discount_specific_product_ids) > 1:
|
||||
reward_string = _("%s%% discount on products", reward_percentage)
|
||||
else:
|
||||
reward_string = _(
|
||||
"%(percentage)s%% discount on %(product_name)s",
|
||||
percentage=reward_percentage,
|
||||
product_name=reward.discount_specific_product_ids.name
|
||||
)
|
||||
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['coupon.program'].search([('reward_id', '=', reward.id)])
|
||||
reward_string = _(
|
||||
"%(amount)s %(currency)s discount on total amount",
|
||||
amount=reward.discount_fixed_amount,
|
||||
currency=program.currency_id.name
|
||||
)
|
||||
result.append((reward.id, reward_string))
|
||||
return result
|
||||
@@ -1,38 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
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")
|
||||
rule_partners_domain = fields.Char(string="Based on Customers", help="Coupon program will work for selected customers only")
|
||||
rule_products_domain = fields.Char(string="Based on Products", default=[['sale_ok', '=', True]], help="On Purchase of selected product, reward will be given")
|
||||
rule_min_quantity = fields.Integer(string="Minimum Quantity", default=1,
|
||||
help="Minimum required product quantity to get the reward")
|
||||
rule_minimum_amount = fields.Float(default=0.0, help="Minimum required amount to get the reward")
|
||||
rule_minimum_amount_tax_inclusion = fields.Selection([
|
||||
('tax_included', 'Tax Included'),
|
||||
('tax_excluded', 'Tax Excluded')], default="tax_excluded")
|
||||
|
||||
@api.constrains('rule_date_to', 'rule_date_from')
|
||||
def _check_rule_date_from(self):
|
||||
if any(applicability for applicability in self
|
||||
if applicability.rule_date_to and applicability.rule_date_from
|
||||
and applicability.rule_date_to < applicability.rule_date_from):
|
||||
raise ValidationError(_('The start date must be before the end date'))
|
||||
|
||||
@api.constrains('rule_minimum_amount')
|
||||
def _check_rule_minimum_amount(self):
|
||||
if self.filtered(lambda applicability: applicability.rule_minimum_amount < 0):
|
||||
raise ValidationError(_('Minimum purchased amount should be greater than 0'))
|
||||
|
||||
@api.constrains('rule_min_quantity')
|
||||
def _check_rule_min_quantity(self):
|
||||
if not self.rule_min_quantity > 0:
|
||||
raise ValidationError(_('Minimum quantity should be greater than 0'))
|
||||
@@ -1,15 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class MailComposeMessage(models.TransientModel):
|
||||
_inherit = 'mail.compose.message'
|
||||
|
||||
def _action_send_mail(self, **kwargs):
|
||||
for wizard in self:
|
||||
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()._action_send_mail(**kwargs)
|
||||
@@ -1,19 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class CouponReport(models.AbstractModel):
|
||||
_name = 'report.coupon.report_coupon'
|
||||
_description = 'Sales Coupon Report'
|
||||
|
||||
@api.model
|
||||
def _get_report_values(self, docids, data=None):
|
||||
docs = self.env['coupon.coupon'].browse(docids)
|
||||
return {
|
||||
'doc_ids': docs.ids,
|
||||
'doc_model': 'coupon.coupon',
|
||||
'data': data,
|
||||
'docs': docs,
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="report_coupon_code" model="ir.actions.report">
|
||||
<field name="name">Coupon Code</field>
|
||||
<field name="model">coupon.coupon</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">coupon.report_coupon_i18n</field>
|
||||
<field name="report_file">coupon.report_coupon_i18n</field>
|
||||
<field name="binding_model_id" ref="model_coupon_coupon"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,101 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="report_coupon">
|
||||
<t t-call="web.internal_layout">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="page">
|
||||
<div class="row text-center">
|
||||
<div class="o_offer col-lg-12">
|
||||
<h4 t-if="o.partner_id.name">
|
||||
Congratulations
|
||||
<t t-esc="o.partner_id.name"/>,
|
||||
</h4>
|
||||
<t t-set="text">on your next order</t>
|
||||
<h4>Here is your reward from <t t-esc="o.program_id.company_id.name"/>.</h4>
|
||||
<t t-if="o.program_id.reward_type == 'discount'">
|
||||
<t t-set="trans_text">off %s</t>
|
||||
<t t-set="text" t-value="trans_text % text"/>
|
||||
<h1 t-if="o.program_id.discount_type == 'fixed_amount'" style="font-size: 55px; color: #875A7B">
|
||||
<strong><span t-field="o.program_id.discount_fixed_amount" t-options='{"widget": "monetary", "display_currency": o.program_id.currency_id}'/></strong>
|
||||
</h1>
|
||||
<h1 t-if="o.program_id.discount_type == 'percentage'" style="font-size: 55px; color: #875A7B">
|
||||
<strong><span t-field="o.program_id.discount_percentage"/> %</strong>
|
||||
</h1>
|
||||
<t t-if="o.program_id.discount_apply_on == 'specific_products'">
|
||||
<t t-if="len(o.program_id.discount_specific_product_ids) > 1">
|
||||
<t t-set="text">off on some products*</t>
|
||||
<t t-set="display_specific_products" t-value="True"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-set="trans_text">off on %s</t>
|
||||
<t t-set="text" t-value="trans_text % o.program_id.discount_specific_product_ids.name"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="o.program_id.discount_apply_on == 'cheapest_product'">
|
||||
<t t-set="text">off on the cheapest product</t>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="o.program_id.reward_type == 'product'">
|
||||
<strong style="font-size: 55px; color: #875A7B">
|
||||
<t t-esc="'get %s free %s' % (o.program_id.reward_product_quantity, o.program_id.reward_product_id.name)"/>
|
||||
</strong>
|
||||
</t>
|
||||
<t t-if="o.program_id.reward_type == 'free_shipping'">
|
||||
<strong style="font-size: 55px; color: #875A7B">get free shipping</strong>
|
||||
</t>
|
||||
<h1 class="font-weight-bold" style="font-size: 34px" t-esc="text"/>
|
||||
<br/>
|
||||
<h4 t-if="o.expiration_date">
|
||||
Use this promo code before
|
||||
<span t-field="o.expiration_date" t-options='{"format": "yyyy-MM-d"}'/>
|
||||
</h4>
|
||||
<h2 class="mt-4">
|
||||
<strong class="bg-light" t-esc="o.code"></strong>
|
||||
</h2>
|
||||
<h4 t-if="o.program_id.rule_min_quantity > 1">
|
||||
<span>Minimum purchase of</span>
|
||||
<strong t-esc="o.program_id.rule_min_quantity"/> <span>products</span>
|
||||
</h4>
|
||||
<h4 t-if="o.program_id.rule_minimum_amount">
|
||||
<span>Valid for purchase above</span>
|
||||
<strong t-esc="o.program_id.rule_minimum_amount" t-options="{'widget': 'monetary', 'display_currency': o.program_id.currency_id}"/>
|
||||
</h4>
|
||||
<p t-if="display_specific_products">
|
||||
<small>
|
||||
*Valid for following products: <t t-esc="', '.join(o.program_id.discount_specific_product_ids.mapped('name'))"/>
|
||||
</small>
|
||||
</p>
|
||||
<br/>
|
||||
<div t-field="o.code" t-options="{'widget': 'barcode', 'width': 600, 'height': 100}"/>
|
||||
<br/><br/>
|
||||
<h4>Thank you,</h4>
|
||||
<br/>
|
||||
<div class="mt32">
|
||||
<div class="text-center">
|
||||
<img alt="Logo" t-att-src="'/logo?company=%d' % (o.program_id.company_id)" t-att-alt="'%s' % (o.program_id.company_id.name)" style="border:0px solid transparent; height: 50; width: 200px;" height="50"/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-center d-inline-block">
|
||||
<span t-field="o.program_id.company_id.partner_id"
|
||||
t-options='{"widget": "contact", "fields": ["address", "email"], "no_marker": True}'/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="report_coupon_i18n">
|
||||
<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 or o.env.lang)"/>
|
||||
<t t-call="coupon.report_coupon" t-lang="o.partner_id.lang or o.env.lang"/>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?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,7 +0,0 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_coupon_coupon,access_coupon_coupon,model_coupon_coupon,base.group_user,0,0,0,0
|
||||
access_coupon_reward,access_coupon_reward,model_coupon_reward,base.group_user,0,0,0,0
|
||||
access_coupon_rule,access_coupon_rule,model_coupon_rule,base.group_user,0,0,0,0
|
||||
access_coupon_program,access_coupon_program,model_coupon_program,base.group_user,0,0,0,0
|
||||
access_coupon_generate_wizard,access_coupon_generate_wizard,model_coupon_generate_wizard,base.group_user,0,0,0,0
|
||||
|
||||
|
@@ -1,286 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Common form view between the coupon programs and the promotion programs -->
|
||||
<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" />
|
||||
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
|
||||
<div name="title" class="oe_left mw-100">
|
||||
</div>
|
||||
<group>
|
||||
<group name="conditions" string="Conditions">
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="program_type" invisible="1"/>
|
||||
<field name="rule_products_domain" placeholder="Select product" widget="domain" options="{'model': 'product.product', 'in_dialog': true}"/>
|
||||
<label string="Quantity" for="rule_min_quantity" attrs="{'invisible': [('rule_products_domain', '=', False)]}"/>
|
||||
<div attrs="{'invisible': [('rule_products_domain', '=',False)]}">
|
||||
<field name="rule_min_quantity" class="oe_inline"/>
|
||||
</div>
|
||||
<label string="Minimum Purchase Of" for="rule_minimum_amount" />
|
||||
<div name="rule_minimum_amount" class="o_row">
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="rule_minimum_amount" widget='monetary' options="{'currency_field': 'currency_id'}"/>
|
||||
<field name="rule_minimum_amount_tax_inclusion" required="1"/>
|
||||
</div>
|
||||
<field name="company_id" placeholder="Select company" groups="base.group_multi_company"></field>
|
||||
</group>
|
||||
<group name="validity" string="Validity"/>
|
||||
</group>
|
||||
<group string="Rewards">
|
||||
<group name='reward'>
|
||||
<field name="reward_type" string="Reward" widget="radio"/>
|
||||
<field name="discount_line_product_id" attrs="{'invisible': [('discount_line_product_id', '=', False)]}" readonly="True"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="reward_product_id" attrs="{'invisible': [('reward_type', 'in', ('discount', 'free_shipping'))], 'required': [('reward_type', '=', 'product')]}" placeholder="Select reward product"/>
|
||||
<label string="Quantity" for="reward_product_quantity" attrs="{'invisible': ['|', ('reward_type', 'in', ('discount', 'free_shipping')), ('reward_product_id', '=',False)]}"/>
|
||||
<div attrs="{'invisible': ['|', ('reward_type', 'in', ('discount', 'free_shipping')),('reward_product_id', '=',False)]}">
|
||||
<field name="reward_product_quantity" class="oe_inline"/>
|
||||
<field name="reward_product_uom_id" class="oe_inline"/>
|
||||
</div>
|
||||
<label string="Apply Discount" for="discount_type" attrs="{'invisible': [('reward_type', 'in', ('product', 'free_shipping'))]}"/>
|
||||
<div attrs="{'invisible': [('reward_type', 'in', ('product', 'free_shipping'))]}">
|
||||
<field name="discount_type" class="oe_inline" attrs="{'required': [('reward_type','=','discount')]}"/>
|
||||
<field name="discount_percentage" attrs="{'invisible': [('discount_type', '!=', 'percentage')],'required': [('discount_type', '=', 'percentage')]}" class="oe_inline"/>
|
||||
<span attrs="{'invisible': [('discount_type', '!=', 'percentage')],'required': [('discount_type', '=', 'percentage')]}" class="oe_inline">%</span>
|
||||
</div>
|
||||
<label string="Fixed Amount" for="discount_fixed_amount" attrs="{'invisible': ['|',('discount_type', '!=', 'fixed_amount'), ('reward_type', '!=', 'discount')]}" />
|
||||
<div attrs="{'invisible': ['|',('discount_type', '!=', 'fixed_amount'), ('reward_type', '!=', 'discount')]}">
|
||||
<field name="discount_fixed_amount" class="oe_inline" attrs="{'required':[('discount_type', '=', 'fixed_amount')]}" widget='monetary' options="{'currency_field': 'currency_id'}"/>
|
||||
</div>
|
||||
<field name="discount_apply_on" attrs="{'invisible':
|
||||
['|', ('reward_type', 'in', ('product', 'free_shipping')), ('discount_type', '!=', 'percentage')]}" widget="radio"/>
|
||||
<field name="discount_specific_product_ids" widget='many2many_tags' attrs="{'invisible': ['|', '|', ('discount_apply_on', '!=', 'specific_products'),('discount_type', '!=', 'percentage'), ('reward_type', 'in', ('product', 'free_shipping'))], 'required': [('reward_type', '=', 'discount'),('discount_apply_on', '=', 'specific_products'),('discount_type', '=', 'percentage')]}" placeholder="Select products"/>
|
||||
<label for="discount_max_amount" string="Max Discount Amount" attrs="{'invisible': ['|', ('reward_type', 'in', ('product', 'free_shipping')), ('discount_type', '!=', 'percentage')]}"/>
|
||||
<div attrs="{'invisible': ['|', ('reward_type', 'in', ('product', 'free_shipping')), ('discount_type', '!=', 'percentage')]}">
|
||||
<field name="discount_max_amount" class="oe_inline" widget='monetary' options="{'currency_field': 'currency_id'}"/>
|
||||
<span class="oe_grey"> if 0, no limit</span>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Coupon Program -->
|
||||
|
||||
<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="%(coupon.coupon_generate_action)d"
|
||||
string="Generate Coupon" attrs="{'invisible': [('active', '=', False)]}"/>
|
||||
<button type="action" name="%(coupon.coupon_generate_action)d"
|
||||
string="Generate Coupon" attrs="{'invisible': [('active', '=', True)]}" class="oe_highlight"/>
|
||||
</header>
|
||||
</xpath>
|
||||
<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>
|
||||
<xpath expr="//div[@name='title']" position="inside">
|
||||
<label for="name" string="Program Name"/>
|
||||
<h1><field name="name" class="oe_title" height="20px"/></h1>
|
||||
</xpath>
|
||||
<xpath expr="//group[@name='validity']" position="inside">
|
||||
<label for="validity_duration" string="Validity Duration"/>
|
||||
<div>
|
||||
<field name="validity_duration" class="oe_inline"/>
|
||||
<span class="o_form_label oe_inline"> Days</span> <span class="oe_grey">if 0, infinite use</span>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<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 sample="1">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="active"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<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"/>
|
||||
<filter string="Expired" name="expired" domain="[('rule_date_to', '<', datetime.datetime.now())]" help="Expired Programs"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<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" sample="1">
|
||||
<field name="name" />
|
||||
<field name="active"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_card oe_kanban_global_click">
|
||||
<div class="text-center oe_kanban_details">
|
||||
<strong><span><field name="name"/></span></strong>
|
||||
</div>
|
||||
<hr class="mt4 mb4"/>
|
||||
<div class="row">
|
||||
<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>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="coupon_program_action_coupon_program" model="ir.actions.act_window">
|
||||
<field name="name">Coupon Programs</field>
|
||||
<field name="res_model">coupon.program</field>
|
||||
<field name="view_mode">tree,kanban,form</field>
|
||||
<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('coupon_program_view_coupon_program_form')})]"/>
|
||||
<field name="domain">[('program_type','=', 'coupon_program')]</field>
|
||||
<field name="context">{
|
||||
'default_program_type': 'coupon_program',
|
||||
'promo_code_usage': 'code_needed',
|
||||
'search_default_opened': 1
|
||||
}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a new coupon program
|
||||
</p><p>
|
||||
Generate and share coupon codes with your customers to get discounts or free products.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Promotion Program -->
|
||||
|
||||
<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 for="name" string="Program Name"/>
|
||||
<h1><field name="name" class="oe_title o_text_overflow" placeholder="e.g. 10% Discount" height="20px"/></h1>
|
||||
</xpath>
|
||||
<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>
|
||||
<xpath expr="//group[@name='reward']" position="before">
|
||||
<field name="sequence" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='program_type']" position="after">
|
||||
<field name="rule_partners_domain" placeholder="Select customer" widget="domain" options="{'model': 'res.partner', 'in_dialog': true}"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='rule_minimum_amount']" position="after">
|
||||
<field name="promo_code_usage" widget="radio"/>
|
||||
<field name="promo_code" attrs="{'required': [('promo_code_usage', '=', 'code_needed')], 'invisible': [('promo_code_usage', '=', 'no_code_needed')]}"/>
|
||||
</xpath>
|
||||
<xpath expr="//group[@name='validity']" position="inside">
|
||||
<label string="Apply on First" for="maximum_use_number" class="oe_inline"/>
|
||||
<div>
|
||||
<field name="maximum_use_number" class="oe_inline"/>
|
||||
<span> Orders</span>
|
||||
<span class="oe_grey"> if 0, infinite use</span>
|
||||
</div>
|
||||
<field name="rule_date_from" class="oe_inline"/>
|
||||
<field name="rule_date_to" class="oe_inline"/>
|
||||
<label string="Coupon Validity" for="validity_duration" class="oe_inline" attrs="{'invisible': [('promo_applicability', '=', 'on_current_order')]}"/>
|
||||
<div attrs="{'invisible': [('promo_applicability', '=', 'on_current_order')]}">
|
||||
<field name="validity_duration" class="oe_inline"/>
|
||||
<span> Days</span>
|
||||
<span class="oe_grey"> if 0, coupon doesn't expire</span>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//group[@name='reward']" position="before">
|
||||
<group>
|
||||
<field name="promo_applicability" widget="radio"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<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"/>
|
||||
<field name="name"/>
|
||||
<field name="promo_code_usage"/>
|
||||
<field name="active"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<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"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="coupon_program_action_promo_program" model="ir.actions.act_window">
|
||||
<field name="name">Promotion Programs</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('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',
|
||||
'default_promo_code_usage': 'no_code_needed',
|
||||
'default_validity_duration': 0,
|
||||
'search_default_opened': 1
|
||||
}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a new promotion program
|
||||
</p><p>
|
||||
Build up promotion programs to attract more customers with discounts, free products, free delivery, etc.
|
||||
You can share promotion codes or grant the promotions automatically if some conditions are met.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -1,49 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<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"/>
|
||||
<button name="action_coupon_send" string="Send" type="object" icon="fa-paper-plane-o" attrs="{'invisible': [('state', 'not in', ['new', 'sent'])]}"/>
|
||||
<button name="action_coupon_cancel" string="Cancel" type="object" icon="fa-times" attrs="{'invisible': [('state', '!=', 'new')]}"/>
|
||||
<field name="state"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="coupon_action" model="ir.actions.act_window">
|
||||
<field name="name">Coupons</field>
|
||||
<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="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>
|
||||
<button name="action_coupon_send" type="object" string="Send by Email" class="oe_highlight" attrs="{'invisible': [('state', 'not in', ['new', 'sent'])]}"/>
|
||||
<button name="action_coupon_cancel" type="object" string="Cancel" class="oe_highlight" attrs="{'invisible': [('state', '!=', 'new')]}"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="new,sent,used,expired" context="{'state': state}"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="code"/>
|
||||
<field name="expiration_date"/>
|
||||
<field name="partner_id"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,4 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import coupon_generate
|
||||
@@ -1,48 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
import ast
|
||||
from odoo.osv import expression
|
||||
|
||||
|
||||
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([
|
||||
('nbr_coupon', 'Number of Coupons'),
|
||||
('nbr_customer', 'Number of Selected Customers')
|
||||
], default='nbr_coupon')
|
||||
partners_domain = fields.Char(string="Customer", default='[]')
|
||||
has_partner_email = fields.Boolean(compute='_compute_has_partner_email')
|
||||
template_id = fields.Many2one('mail.template', 'Use template', domain="[('model', '=', 'coupon.coupon')]")
|
||||
|
||||
def generate_coupon(self):
|
||||
"""Generates the number of coupons entered in wizard field nbr_coupons
|
||||
"""
|
||||
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['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['coupon.coupon'].create(vals)
|
||||
context = dict(lang=partner.lang)
|
||||
subject = _('%s, a coupon has been generated for you') % (partner.name)
|
||||
del context
|
||||
if self.template_id:
|
||||
email_values = {'email_from': self.env.user.email or '', 'subject': subject}
|
||||
self.template_id.send_mail(coupon.id, email_values=email_values, email_layout_xmlid='mail.mail_notification_light')
|
||||
|
||||
@api.depends('partners_domain')
|
||||
def _compute_has_partner_email(self):
|
||||
for record in self:
|
||||
domain = expression.AND([ast.literal_eval(record.partners_domain), [('email', '=', False)]])
|
||||
record.has_partner_email = self.env['res.partner'].search_count(domain) == 0
|
||||
@@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<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>
|
||||
<field name="has_partner_email" invisible="1"/>
|
||||
<field name="generation_type" widget="radio"/>
|
||||
<field name="partners_domain" attrs="{'invisible': [('generation_type', '!=', 'nbr_customer')]}" widget="domain" options="{'model': 'res.partner'}"/>
|
||||
<field name="nbr_coupons" attrs="{'invisible': [('generation_type', '!=', 'nbr_coupon')]}"/>
|
||||
<field name="template_id"
|
||||
options="{'no_create': True, 'no_edit': True}"
|
||||
context="{'default_model': 'coupon.coupon'}"
|
||||
attrs="{'invisible': [('generation_type', '!=', 'nbr_customer')]}" />
|
||||
<div class="text-muted" attrs="{'invisible': [('generation_type', '!=', 'nbr_customer')]}">
|
||||
Specify a mail template to send the generated coupons as email.
|
||||
</div>
|
||||
</group>
|
||||
<div role="alert" class="alert alert-warning" attrs="{'invisible': ['|', ('generation_type', '!=', 'nbr_customer'), ('has_partner_email', '=', True)]}">
|
||||
Some selected customers do not have an email address and will not receive the coupon.
|
||||
</div>
|
||||
<footer>
|
||||
<button name="generate_coupon" type="object" string="Generate" class="oe_highlight" data-hotkey="q"/>
|
||||
<button special="cancel" data-hotkey="z" string="Cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="coupon_generate_action" model="ir.actions.act_window">
|
||||
<field name="name">Number of Coupons To 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="coupon_generate_view_form"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -1,17 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': "Gift Card",
|
||||
'summary': "Use gift card",
|
||||
'description': """Integrate gift card mechanism""",
|
||||
'category': 'Sales/Sales',
|
||||
'version': '1.0',
|
||||
'depends': ['product'],
|
||||
'data': [
|
||||
'data/gift_card_data.xml',
|
||||
'views/views.xml',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- the product used to deduce on sale order -->
|
||||
<record id="pay_with_gift_card_product" model="product.product">
|
||||
<field name="name">Gift Card</field>
|
||||
<field name="list_price">0</field>
|
||||
<field name="detailed_type">service</field>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
<field name="sale_ok" eval="False"/>
|
||||
<field name="image_1920" type="base64" file="gift_card/static/img/gift_card.png"/>
|
||||
</record>
|
||||
|
||||
<!-- the product to sell to generate gift cards automatically -->
|
||||
<record id="gift_card_product_50" model="product.product">
|
||||
<field name="name">Gift Card</field>
|
||||
<field name="list_price">50</field>
|
||||
<field name="detailed_type">gift</field>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
<field name="image_1920" type="base64" file="gift_card/static/img/gift_card.png"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,179 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * gift_card
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~14.5\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-09-14 10:28+0000\n"
|
||||
"PO-Revision-Date: 2021-09-14 10:28+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.fields,help:gift_card.field_product_product__detailed_type
|
||||
#: model:ir.model.fields,help:gift_card.field_product_template__detailed_type
|
||||
msgid ""
|
||||
"A storable product is a product for which you manage stock. The Inventory app has to be installed.\n"
|
||||
"A consumable product is a product for which stock is not managed.\n"
|
||||
"A service is a non-material product you provide."
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.fields,field_description:gift_card.field_gift_card__balance
|
||||
msgid "Balance"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.fields,field_description:gift_card.field_gift_card__code
|
||||
msgid "Code"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.fields,field_description:gift_card.field_gift_card__company_id
|
||||
msgid "Company"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.fields,field_description:gift_card.field_gift_card__create_uid
|
||||
msgid "Created by"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.fields,field_description:gift_card.field_gift_card__create_date
|
||||
msgid "Created on"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.fields,field_description:gift_card.field_gift_card__currency_id
|
||||
msgid "Currency"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: code:addons/gift_card/models/product.py:0
|
||||
#, python-format
|
||||
msgid "Deleting the Gift Card Pay product is not allowed."
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.fields,field_description:gift_card.field_gift_card__display_name
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.fields.selection,name:gift_card.selection__gift_card__state__expired
|
||||
msgid "Expired"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.fields,field_description:gift_card.field_gift_card__expired_date
|
||||
msgid "Expired Date"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: code:addons/gift_card/models/gift_card.py:0
|
||||
#, python-format
|
||||
msgid "Gift #%s"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model,name:gift_card.model_gift_card
|
||||
#: model:ir.model.fields.selection,name:gift_card.selection__product_template__detailed_type__gift
|
||||
#: model:product.product,name:gift_card.gift_card_product_50
|
||||
#: model:product.product,name:gift_card.pay_with_gift_card_product
|
||||
#: model:product.template,name:gift_card.gift_card_product_50_product_template
|
||||
#: model:product.template,name:gift_card.pay_with_gift_card_product_product_template
|
||||
#: model_terms:ir.ui.view,arch_db:gift_card.gift_card_view_search
|
||||
#: model_terms:ir.ui.view,arch_db:gift_card.gift_card_view_tree
|
||||
msgid "Gift Card"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.actions.act_window,name:gift_card.gift_card_action
|
||||
msgid "Gift Cards"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.fields,field_description:gift_card.field_gift_card__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.fields,help:gift_card.field_gift_card__partner_id
|
||||
msgid "If empty, all users can use it"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.fields,field_description:gift_card.field_gift_card__initial_amount
|
||||
msgid "Initial Amount"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.fields,field_description:gift_card.field_gift_card____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.fields,field_description:gift_card.field_gift_card__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.fields,field_description:gift_card.field_gift_card__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.fields,field_description:gift_card.field_gift_card__name
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.fields,field_description:gift_card.field_gift_card__partner_id
|
||||
msgid "Partner"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model,name:gift_card.model_product_template
|
||||
msgid "Product Template"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.fields,field_description:gift_card.field_product_product__detailed_type
|
||||
#: model:ir.model.fields,field_description:gift_card.field_product_template__detailed_type
|
||||
msgid "Product Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.fields,field_description:gift_card.field_gift_card__state
|
||||
msgid "State"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.constraint,message:gift_card.constraint_gift_card_unique_gift_card_code
|
||||
msgid "The gift card code must be unique."
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.constraint,message:gift_card.constraint_gift_card_check_amount
|
||||
msgid "The initial amount must be positive."
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:product.product,uom_name:gift_card.gift_card_product_50
|
||||
#: model:product.product,uom_name:gift_card.pay_with_gift_card_product
|
||||
#: model:product.template,uom_name:gift_card.gift_card_product_50_product_template
|
||||
#: model:product.template,uom_name:gift_card.pay_with_gift_card_product_product_template
|
||||
msgid "Units"
|
||||
msgstr ""
|
||||
|
||||
#. module: gift_card
|
||||
#: model:ir.model.fields.selection,name:gift_card.selection__gift_card__state__valid
|
||||
#: model_terms:ir.ui.view,arch_db:gift_card.gift_card_view_search
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
@@ -1,58 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
class GiftCard(models.Model):
|
||||
_name = "gift.card"
|
||||
_description = "Gift Card"
|
||||
_order = 'id desc'
|
||||
_check_company_auto = True
|
||||
|
||||
@api.model
|
||||
def _generate_code(self):
|
||||
return '044' + str(uuid4())[4:-8][3:]
|
||||
|
||||
name = fields.Char(compute='_compute_name')
|
||||
code = fields.Char(default=lambda x: x._generate_code(), required=True, readonly=True, copy=False)
|
||||
partner_id = fields.Many2one('res.partner', help="If empty, all users can use it")
|
||||
company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
|
||||
currency_id = fields.Many2one('res.currency', readonly=True, related='company_id.currency_id')
|
||||
initial_amount = fields.Monetary(required=True, currency_field='currency_id')
|
||||
balance = fields.Monetary(compute="_compute_balance") # in company currency
|
||||
expired_date = fields.Date(default=lambda self: fields.Date.add(fields.Date.today(), years=1))
|
||||
state = fields.Selection(
|
||||
selection=[('valid', 'Valid'), ('expired', 'Expired')],
|
||||
default='valid',
|
||||
copy=False
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_gift_card_code', 'UNIQUE(code)', 'The gift card code must be unique.'),
|
||||
('check_amount', 'CHECK(initial_amount >= 0)', 'The initial amount must be positive.')
|
||||
]
|
||||
|
||||
def _compute_name(self):
|
||||
for record in self:
|
||||
record.name = _("Gift #%s", record.id)
|
||||
|
||||
@api.autovacuum
|
||||
def _gc_mark_expired_gift_card(self):
|
||||
self.env['gift.card'].search([
|
||||
'&', ('state', '=', 'valid'), ('expired_date', '<', fields.Date.today())
|
||||
]).write({'state': 'expired'})
|
||||
|
||||
def balance_converted(self, currency_id=False):
|
||||
# helper to convert the current balance in the currency provided
|
||||
return self.currency_id._convert(self.balance, currency_id, self.env.company, fields.Date.today())
|
||||
|
||||
def can_be_used(self):
|
||||
# expired state are computed once a day, so can be not synchro
|
||||
return self.state == 'valid' and self.balance > 0 and self.expired_date >= fields.Date.today()
|
||||
|
||||
@api.depends("initial_amount")
|
||||
def _compute_balance(self):
|
||||
for record in self:
|
||||
record.balance = record.initial_amount
|
||||
@@ -1,23 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = "product.template"
|
||||
|
||||
detailed_type = fields.Selection(selection_add=[
|
||||
('gift', 'Gift Card'),
|
||||
], ondelete={'gift': 'set service'})
|
||||
|
||||
def _detailed_type_mapping(self):
|
||||
type_mapping = super()._detailed_type_mapping()
|
||||
type_mapping['gift'] = 'service'
|
||||
return type_mapping
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_gift_card_product(self):
|
||||
if self.env.ref('gift_card.pay_with_gift_card_product').product_tmpl_id in self:
|
||||
raise UserError(_('Deleting the Gift Card Pay product is not allowed.'))
|
||||
@@ -1,2 +0,0 @@
|
||||
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_gift_card_all,Gift Card Program All,model_gift_card,,0,0,0,0
|
||||
|
@@ -1,62 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<!-- Actions -->
|
||||
<record id="gift_card_action" model="ir.actions.act_window">
|
||||
<field name="name">Gift Cards</field>
|
||||
<field name="res_model">gift.card</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{'search_default_valid': True}</field>
|
||||
</record>
|
||||
|
||||
<!-- view mode -->
|
||||
<record id="gift_card_view_tree" model="ir.ui.view">
|
||||
<field name="name">gift.card.tree</field>
|
||||
<field name="model">gift.card</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Gift Card">
|
||||
<field name="code"/>
|
||||
<field name="balance"/>
|
||||
<field name="state"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="gift_card_view_form" model="ir.ui.view">
|
||||
<field name="name">gift.card.form</field>
|
||||
<field name="model">gift.card</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<field name="state" widget="statusbar"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="create_date"/>
|
||||
<field name="expired_date"/>
|
||||
<field name="code"/>
|
||||
</group>
|
||||
<group name="gift_card">
|
||||
<field name="currency_id" attrs="{'invisible': True}"/>
|
||||
<field name="initial_amount"/>
|
||||
<field name="balance"/>
|
||||
<field name="partner_id"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Searching -->
|
||||
<record id="gift_card_view_search" model="ir.ui.view">
|
||||
<field name="name">gift.card.search</field>
|
||||
<field name="model">gift.card</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Gift Card">
|
||||
<field name="code"/>
|
||||
<filter name="valid" string="Valid" domain="[('state', '=', 'valid')]" />
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
{
|
||||
'name': 'Coupons & Loyalty',
|
||||
'summary': "Use discounts, gift card, eWallets and loyalty programs in different sales channels",
|
||||
'category': 'Sales',
|
||||
'version': '1.0',
|
||||
'depends': ['product'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'security/loyalty_security.xml',
|
||||
'report/loyalty_report_templates.xml',
|
||||
'report/loyalty_report.xml',
|
||||
'data/loyalty_data.xml',
|
||||
'data/mail_template_data.xml',
|
||||
'wizard/loyalty_generate_wizard_views.xml',
|
||||
'views/loyalty_card_views.xml',
|
||||
'views/loyalty_mail_views.xml',
|
||||
'views/loyalty_program_views.xml',
|
||||
'views/loyalty_reward_views.xml',
|
||||
'views/loyalty_rule_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/loyalty_demo.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'loyalty/static/src/js/loyalty_control_panel_widget.js',
|
||||
'loyalty/static/src/js/loyalty_views.js',
|
||||
'loyalty/static/src/js/loyalty_list_view.js',
|
||||
'loyalty/static/src/scss/loyalty.scss',
|
||||
],
|
||||
'web.assets_qweb': [
|
||||
'loyalty/static/src/xml/loyalty_templates.xml',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Basic product for gift card program -->
|
||||
<record id="gift_card_product_50" model="product.product">
|
||||
<field name="name">Gift Card</field>
|
||||
<field name="list_price">50</field>
|
||||
<field name="detailed_type">service</field>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
<field name="image_1920" type="base64" file="loyalty/static/img/gift_card.png"/>
|
||||
</record>
|
||||
<!-- Basic product for eWallet programs -->
|
||||
<record id="ewallet_product_50" model="product.product">
|
||||
<field name="name">Top-up eWallet</field>
|
||||
<field name="list_price">50</field>
|
||||
<field name="detailed_type">service</field>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- 10 percent with code -->
|
||||
<record id="10_percent_with_code" model="loyalty.program">
|
||||
<field name="name">Code for 10% on orders</field>
|
||||
<field name="program_type">promotion</field>
|
||||
<field name="trigger">with_code</field>
|
||||
</record>
|
||||
|
||||
<record id="10_percent_with_code_rule" model="loyalty.rule">
|
||||
<field name="mode">with_code</field>
|
||||
<field name="code">10pc</field>
|
||||
<field name="program_id" ref="loyalty.10_percent_with_code"/>
|
||||
</record>
|
||||
|
||||
<record id="10_percent_with_code_reward" model="loyalty.reward">
|
||||
<field name="reward_type">discount</field>
|
||||
<field name="discount">10</field>
|
||||
<field name="discount_mode">percent</field>
|
||||
<field name="discount_applicability">order</field>
|
||||
<field name="program_id" ref="loyalty.10_percent_with_code"/>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- 3 cabinet + 1 free -->
|
||||
<record id="3_cabinets_plus_1_free" model="loyalty.program">
|
||||
<field name="name">Buy 3 large cabinets, get one for free</field>
|
||||
<field name="program_type">promotion</field>
|
||||
<field name="trigger">auto</field>
|
||||
</record>
|
||||
|
||||
<record id="3_cabinets_plus_1_free_rule" model="loyalty.rule">
|
||||
<field name="minimum_qty">3</field>
|
||||
<field name="reward_point_mode">unit</field>
|
||||
<field name="reward_point_amount">1</field>
|
||||
<field name="product_ids" eval="[(4, ref('product.product_product_6'))]"/>
|
||||
<field name="program_id" ref="loyalty.3_cabinets_plus_1_free"/>
|
||||
</record>
|
||||
|
||||
<record id="3_cabinets_plus_1_free_reward" model="loyalty.reward">
|
||||
<field name="reward_type">product</field>
|
||||
<field name="reward_product_id" ref="product.product_product_6"/>
|
||||
<field name="required_points">3</field>
|
||||
<field name="program_id" ref="loyalty.3_cabinets_plus_1_free"/>
|
||||
</record>
|
||||
|
||||
<!-- 10 percent coupons -->
|
||||
<record id="10_percent_coupon" model="loyalty.program">
|
||||
<field name="name">10% Discount</field>
|
||||
<field name="applies_on">future</field>
|
||||
<field name="trigger">with_code</field>
|
||||
</record>
|
||||
|
||||
<record id="10_percent_coupon_rule" model="loyalty.rule">
|
||||
<field name="program_id" ref="loyalty.10_percent_coupon"/>
|
||||
</record>
|
||||
|
||||
<record id="10_percent_coupon_reward" model="loyalty.reward">
|
||||
<field name="reward_type">discount</field>
|
||||
<field name="discount">10</field>
|
||||
<field name="discount_mode">percent</field>
|
||||
<field name="discount_applicability">order</field>
|
||||
<field name="program_id" ref="loyalty.10_percent_coupon"/>
|
||||
</record>
|
||||
|
||||
<record id="10_percent_coupon_communication" model="loyalty.mail">
|
||||
<field name="trigger">create</field>
|
||||
<field name="mail_template_id" ref="loyalty.mail_template_loyalty_card"/>
|
||||
<field name="program_id" ref="loyalty.10_percent_coupon"/>
|
||||
</record>
|
||||
|
||||
<!-- Gift Cards -->
|
||||
<record id="gift_card_program" model="loyalty.program">
|
||||
<field name="name">Gift Cards</field>
|
||||
<field name="program_type">gift_card</field>
|
||||
<field name="applies_on">future</field>
|
||||
<field name="trigger">auto</field>
|
||||
</record>
|
||||
|
||||
<record id="gift_card_program_rule" model="loyalty.rule">
|
||||
<field name="reward_point_amount">1</field>
|
||||
<field name="reward_point_mode">money</field>
|
||||
<field name="reward_point_split">True</field>
|
||||
<field name="product_ids" eval="[(4, ref('loyalty.gift_card_product_50'))]"/>
|
||||
<field name="program_id" ref="loyalty.gift_card_program"/>
|
||||
</record>
|
||||
|
||||
<record id="gift_card_program_reward" model="loyalty.reward">
|
||||
<field name="reward_type">discount</field>
|
||||
<field name="discount_mode">per_point</field>
|
||||
<field name="discount">1</field>
|
||||
<field name="discount_applicability">order</field>
|
||||
<field name="required_points">1</field>
|
||||
<field name="program_id" ref="loyalty.gift_card_program"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="mail_template_gift_card" model="mail.template">
|
||||
<field name="name">Gift Card: Send by Email</field>
|
||||
<field name="model_id" ref="model_loyalty_card"/>
|
||||
<field name="subject">Your Gift Card at {{ object.company_id.name }}</field>
|
||||
<field name="partner_to">{{ object._get_mail_partner().id }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin:0px; font-size:24px; font-family:arial, 'helvetica neue', helvetica, sans-serif; line-height:36px; color:#333333; text-align: center">
|
||||
Here is your gift card!
|
||||
</div>
|
||||
<div style="padding-top:20px; padding-bottom:20px">
|
||||
<img src="/loyalty/static/img/gift_card.png" style="display:block; border:0; outline:none; text-decoration:none; margin:auto;" width="300"/>
|
||||
</div>
|
||||
<div style="padding:0; margin:0px; padding-top:35px; padding-bottom:35px; text-align:center;">
|
||||
<h3 style="margin:0px; line-height:48px; font-family:arial, 'helvetica neue', helvetica, sans-serif; font-size:40px; font-style:normal; font-weight:normal; color:#333333; text-align:center">
|
||||
<strong t-out="format_amount(object.points, object.currency_id) or ''">$ 150.00</strong></h3>
|
||||
</div>
|
||||
<div style="padding:0; margin:0px; padding-top:35px; padding-bottom:35px; background-color:#efefef; text-align:center;">
|
||||
<p style="margin:0px; font-size:14px;font-family:arial, 'helvetica neue', helvetica, sans-serif; line-height:21px; color:#333333">
|
||||
<strong>Gift Card Code</strong>
|
||||
</p>
|
||||
<p style="margin:0px; font-size:25px;font-family:arial, 'helvetica neue', helvetica, sans-serif; line-height:38px; color:#A9A9A9" t-out="object.code or ''">4f10-15d6-41b7-b04c-7b3e</p>
|
||||
</div>
|
||||
<div t-if="object.expiration_date" style="padding:0; margin:0px; padding-top:10px; padding-bottom:10px; text-align:center;">
|
||||
<h3 style="margin:0px; line-height:17px; font-family:arial, 'helvetica neue', helvetica, sans-serif; font-size:14px; font-style:normal; font-weight:normal; color:#A9A9A9; text-align:center">Card expires <t t-out="format_date(object.expiration_date) or ''">05/05/2021</t></h3>
|
||||
</div>
|
||||
<div style="padding:20px; margin:0px; text-align:center;">
|
||||
<span style="background-color:#999999; display:inline-block; width:auto; border-radius:5px;">
|
||||
<a t-attf-href="{{ object.get_base_url() }}/shop" target="_blank" style="text-decoration:none; font-family:arial, 'helvetica neue', helvetica, sans-serif; font-size:22px; color:#FFFFFF; border-style:solid; border-color:#999999; border-width:20px 30px; display:inline-block; background-color:#999999; border-radius:5px; font-weight:bold; font-style:normal; line-height:26px; width:auto; text-align:center">Use it right now!</a>
|
||||
</span>
|
||||
</div>
|
||||
</field>
|
||||
<field name="report_template" ref="loyalty.report_gift_card"/>
|
||||
<field name="report_name">Your Gift Card</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="mail_template_loyalty_card" model="mail.template">
|
||||
<field name="name">[Loyalty] Coupon: Send by Email</field>
|
||||
<field name="model_id" ref="loyalty.model_loyalty_card"/>
|
||||
<field name="subject">Your reward coupon from {{ object.program_id.company_id.name }} </field>
|
||||
<field name="email_from">{{ object.program_id.company_id.email }}</field>
|
||||
<field name="partner_to">{{ object._get_mail_partner().id }}</field>
|
||||
<field name="lang">{{ object._get_mail_partner().lang }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="width:100%; margin:0px auto;"><tbody>
|
||||
<tr>
|
||||
<td valign="top" style="text-align: center; font-size: 14px;">
|
||||
<t t-if="object._get_mail_partner().name">
|
||||
Congratulations <t t-out="object._get_mail_partner().name or ''">Brandon Freeman</t>,<br />
|
||||
</t>
|
||||
|
||||
Here is your reward from <t t-out="object.program_id.company_id.name or ''">YourCompany</t>.<br />
|
||||
|
||||
<t t-foreach="object.program_id.reward_ids" t-as="reward">
|
||||
<t t-if="reward.required_points <= object.points">
|
||||
<span style="font-size: 50px; color: #875A7B; font-weight: bold;" t-esc="reward.description">Reward Description</span>
|
||||
<br/>
|
||||
</t>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin-top: 16px">
|
||||
<td valign="top" style="text-align: center; font-size: 14px;">
|
||||
Use this promo code
|
||||
<t t-if="object.expiration_date">
|
||||
before <t t-out="object.expiration_date or ''">2021-06-16</t>
|
||||
</t>
|
||||
<p style="margin-top: 16px;">
|
||||
<strong style="padding: 16px 8px 16px 8px; border-radius: 3px; background-color: #F1F1F1;" t-out="object.code or ''">15637502648479132902</strong>
|
||||
</p>
|
||||
<t t-foreach="object.program_id.rule_ids" t-as="rule">
|
||||
<t t-if="rule.minimum_qty not in [0, 1]">
|
||||
<span style="font-size: 14px;">
|
||||
Minimum purchase of <t t-out="rule.minimum_qty or ''">10</t> products
|
||||
</span><br />
|
||||
</t>
|
||||
<t t-if="rule.minimum_amount != 0.00">
|
||||
<span style="font-size: 14px;">
|
||||
Valid for purchase above <t t-out="rule.company_id.currency_id.symbol or ''">$</t><t t-out="'%0.2f' % float(rule.minimum_amount) or ''">10.00</t>
|
||||
</span><br />
|
||||
</t>
|
||||
</t>
|
||||
<br/>
|
||||
Thank you,
|
||||
<t t-if="object._get_signature()">
|
||||
<br />
|
||||
<t t-out="object._get_signature() or ''">--<br/>Mitchell Admin</t>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</field>
|
||||
<field name="report_template" ref="loyalty.report_loyalty_card"/>
|
||||
<field name="report_name">Your Coupon Code</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import loyalty_card
|
||||
from . import loyalty_mail
|
||||
from . import loyalty_reward
|
||||
from . import loyalty_rule
|
||||
from . import loyalty_program
|
||||
from . import product_product
|
||||
@@ -0,0 +1,154 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
class LoyaltyCard(models.Model):
|
||||
_name = 'loyalty.card'
|
||||
_inherit = ['mail.thread']
|
||||
_description = 'Loyalty Coupon'
|
||||
_rec_name = 'code'
|
||||
|
||||
@api.model
|
||||
def _generate_code(self):
|
||||
"""
|
||||
Barcode identifiable codes.
|
||||
"""
|
||||
return '044' + str(uuid4())[4:-8][3:]
|
||||
|
||||
def name_get(self):
|
||||
return [(card.id, f'{card.program_id.name}: {card.code}') for card in self]
|
||||
|
||||
program_id = fields.Many2one('loyalty.program', ondelete='restrict')
|
||||
company_id = fields.Many2one(related='program_id.company_id', store=True)
|
||||
currency_id = fields.Many2one(related='program_id.currency_id')
|
||||
# Reserved for this partner if non-empty
|
||||
partner_id = fields.Many2one('res.partner', index=True)
|
||||
points = fields.Float(tracking=True)
|
||||
point_name = fields.Char(related='program_id.portal_point_name', readonly=True)
|
||||
|
||||
code = fields.Char(default=lambda self: self._generate_code(), required=True, readonly=True, index=True)
|
||||
expiration_date = fields.Date()
|
||||
|
||||
use_count = fields.Integer(compute='_compute_use_count')
|
||||
|
||||
_sql_constraints = [
|
||||
('card_code_unique', 'UNIQUE(code)', 'A coupon/loyalty card must have a unique code.')
|
||||
]
|
||||
|
||||
@api.constrains('code')
|
||||
def _contrains_code(self):
|
||||
# Prevent a coupon from having the same code a program
|
||||
if self.env['loyalty.rule'].search_count([('mode', '=', 'with_code'), ('code', 'in', self.mapped('code'))]):
|
||||
raise ValidationError(_('A trigger with the same code as one of your coupon already exists.'))
|
||||
|
||||
# Meant to be overriden
|
||||
def _compute_use_count(self):
|
||||
self.use_count = 0
|
||||
|
||||
def _get_default_template(self):
|
||||
self.ensure_one()
|
||||
return self.program_id.communication_plan_ids.filtered(lambda m: m.trigger == 'create').mail_template_id[:1]
|
||||
|
||||
def _get_mail_partner(self):
|
||||
self.ensure_one()
|
||||
return self.partner_id
|
||||
|
||||
def _get_signature(self):
|
||||
"""To be overriden"""
|
||||
self.ensure_one()
|
||||
return None
|
||||
|
||||
def action_coupon_send(self):
|
||||
""" Open a window to compose an email, with the default template returned by `_get_default_template`
|
||||
message loaded by default
|
||||
"""
|
||||
self.ensure_one()
|
||||
default_template = self._get_default_template()
|
||||
compose_form = self.env.ref('mail.email_compose_message_wizard_form', False)
|
||||
ctx = dict(
|
||||
default_model='loyalty.card',
|
||||
default_res_id=self.id,
|
||||
default_use_template=bool(default_template),
|
||||
default_template_id=default_template and default_template.id,
|
||||
default_composition_mode='comment',
|
||||
default_email_layout_xmlid='mail.mail_notification_light',
|
||||
mark_coupon_as_sent=True,
|
||||
force_email=True,
|
||||
)
|
||||
return {
|
||||
'name': _('Compose Email'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'mail.compose.message',
|
||||
'views': [(compose_form.id, 'form')],
|
||||
'view_id': compose_form.id,
|
||||
'target': 'new',
|
||||
'context': ctx,
|
||||
}
|
||||
|
||||
def _send_creation_communication(self):
|
||||
"""
|
||||
Sends the 'At Creation' communication plan if it exist for the given coupons.
|
||||
"""
|
||||
if self.env.context.get('loyalty_no_mail', False):
|
||||
return
|
||||
# Ideally one per program, but multiple is supported
|
||||
create_comm_per_program = dict()
|
||||
for program in self.program_id:
|
||||
create_comm_per_program[program] = program.communication_plan_ids.filtered(lambda c: c.trigger == 'create')
|
||||
for coupon in self:
|
||||
if not create_comm_per_program[coupon.program_id] or not coupon._get_mail_partner():
|
||||
continue
|
||||
for comm in create_comm_per_program[coupon.program_id]:
|
||||
comm.mail_template_id.send_mail(res_id=coupon.id, email_layout_xmlid='mail.mail_notification_light')
|
||||
|
||||
def _send_points_reach_communication(self, points_changes):
|
||||
"""
|
||||
Send the 'When Reaching' communicaton plans for the given coupons.
|
||||
|
||||
If a coupons passes multiple milestones we will only send the one with the highest target.
|
||||
"""
|
||||
if self.env.context.get('loyalty_no_mail', False):
|
||||
return
|
||||
milestones_per_program = dict()
|
||||
for program in self.program_id:
|
||||
milestones_per_program[program] = program.communication_plan_ids\
|
||||
.filtered(lambda c: c.trigger == 'points_reach')\
|
||||
.sorted('points', reverse=True)
|
||||
for coupon in self:
|
||||
if not coupon._get_mail_partner():
|
||||
continue
|
||||
coupon_change = points_changes[coupon]
|
||||
# Do nothing if coupon lost points or did not change
|
||||
if not milestones_per_program[coupon.program_id] or\
|
||||
not coupon.partner_id or\
|
||||
coupon_change['old'] >= coupon_change['new']:
|
||||
continue
|
||||
this_milestone = False
|
||||
for milestone in milestones_per_program[coupon.program_id]:
|
||||
if coupon_change['old'] < milestone.points and milestone.points <= coupon_change['new']:
|
||||
this_milestone = milestone
|
||||
break
|
||||
if not this_milestone:
|
||||
continue
|
||||
this_milestone.mail_template_id.send_mail(res_id=coupon.id, email_layout_xmlid='mail.mail_notification_light')
|
||||
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
res = super().create(vals_list)
|
||||
res._send_creation_communication()
|
||||
return res
|
||||
|
||||
def write(self, vals):
|
||||
if not self.env.context.get('loyalty_no_mail', False) and 'points' in vals:
|
||||
points_before = {coupon: coupon.points for coupon in self}
|
||||
res = super().write(vals)
|
||||
if not self.env.context.get('loyalty_no_mail', False) and 'points' in vals:
|
||||
points_changes = {coupon: {'old': points_before[coupon], 'new': coupon.points} for coupon in self}
|
||||
self._send_points_reach_communication(points_changes)
|
||||
return res
|
||||
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
# Allow promo programs to send mails upon certain triggers
|
||||
# Like : 'At creation' and 'When reaching X points'
|
||||
|
||||
class LoyaltyMail(models.Model):
|
||||
_name = 'loyalty.mail'
|
||||
_description = 'Loyalty Communication'
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
program_id = fields.Many2one('loyalty.program', required=True, ondelete='cascade')
|
||||
trigger = fields.Selection([
|
||||
('create', 'At Creation'),
|
||||
('points_reach', 'When Reaching')], string='When', required=True
|
||||
)
|
||||
points = fields.Float()
|
||||
mail_template_id = fields.Many2one('mail.template', string="Email Template", required=True, domain=[('model', '=', 'loyalty.card')])
|
||||
@@ -0,0 +1,383 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
class LoyaltyProgram(models.Model):
|
||||
_name = 'loyalty.program'
|
||||
_description = 'Loyalty Program'
|
||||
_order = 'sequence'
|
||||
_rec_name = 'name'
|
||||
|
||||
name = fields.Char('Program Name', required=True, translate=True)
|
||||
active = fields.Boolean(default=True)
|
||||
sequence = fields.Integer(copy=False)
|
||||
company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company)
|
||||
currency_id = fields.Many2one('res.currency', 'Currency', compute='_compute_currency_id',
|
||||
readonly=False, required=True, store=True, precompute=True)
|
||||
currency_symbol = fields.Char(related='currency_id.symbol')
|
||||
|
||||
total_order_count = fields.Integer("Total Order Count", compute="_compute_total_order_count")
|
||||
|
||||
rule_ids = fields.One2many('loyalty.rule', 'program_id', 'Triggers', copy=True,
|
||||
compute='_compute_from_program_type', readonly=False, store=True)
|
||||
reward_ids = fields.One2many('loyalty.reward', 'program_id', 'Rewards', copy=True,
|
||||
compute='_compute_from_program_type', readonly=False, store=True)
|
||||
communication_plan_ids = fields.One2many('loyalty.mail', 'program_id', copy=True,
|
||||
compute='_compute_from_program_type', readonly=False, store=True)
|
||||
coupon_ids = fields.One2many('loyalty.card', 'program_id')
|
||||
coupon_count = fields.Integer(compute='_compute_coupon_count')
|
||||
|
||||
program_type = fields.Selection([
|
||||
('coupons', 'Coupons'),
|
||||
('gift_card', 'Gift Card'),
|
||||
('loyalty', 'Loyalty Cards'),
|
||||
('promotion', 'Promotions'),
|
||||
('ewallet', 'eWallet')], default='promotion', required=True,
|
||||
)
|
||||
date_to = fields.Date(string='Validity')
|
||||
limit_usage = fields.Boolean(string='Limit Usage')
|
||||
max_usage = fields.Integer()
|
||||
# Dictates when the points can be used:
|
||||
# current: if the order gives enough points on that order, the reward may directly be claimed, points lost otherwise
|
||||
# future: if the order gives enough points on that order, a coupon is generated for a next order
|
||||
# both: points are accumulated on the coupon to claim rewards, the reward may directly be claimed
|
||||
applies_on = fields.Selection([
|
||||
('current', 'Current order'),
|
||||
('future', 'Future orders'),
|
||||
('both', 'Current & Future orders')], default='current', required=True,
|
||||
compute='_compute_from_program_type', readonly=False, store=True,
|
||||
)
|
||||
trigger = fields.Selection([
|
||||
('auto', 'Automatic'),
|
||||
('with_code', 'Use a code')],
|
||||
compute='_compute_from_program_type', readonly=False, store=True,
|
||||
help="""
|
||||
Automatic: Customers will be eligible for a reward automatically in their cart.
|
||||
Use a code: Customers will be eligible for a reward if they enter a code.
|
||||
"""
|
||||
)
|
||||
portal_visible = fields.Boolean(default=False,
|
||||
help="""
|
||||
Show in web portal, PoS customer ticket, eCommerce checkout, the number of points available and used by reward.
|
||||
""")
|
||||
portal_point_name = fields.Char(default='Points', translate=True,
|
||||
compute='_compute_from_program_type', readonly=False, store=True)
|
||||
is_nominative = fields.Boolean(compute='_compute_is_nominative')
|
||||
|
||||
_sql_constraints = [
|
||||
('check_max_usage', 'CHECK (limit_usage = False OR max_usage > 0)',
|
||||
'Max usage must be strictly positive if a limit is used.'),
|
||||
]
|
||||
|
||||
@api.constrains('reward_ids')
|
||||
def _constrains_reward_ids(self):
|
||||
if any(not program.reward_ids for program in self):
|
||||
raise ValidationError(_('A program must have at least one reward.'))
|
||||
|
||||
def _compute_total_order_count(self):
|
||||
self.total_order_count = 0
|
||||
|
||||
@api.depends('company_id')
|
||||
def _compute_currency_id(self):
|
||||
for program in self:
|
||||
program.currency_id = program.company_id.currency_id or program.currency_id
|
||||
|
||||
@api.depends('coupon_ids')
|
||||
def _compute_coupon_count(self):
|
||||
read_group_data = self.env['loyalty.card']._read_group([('program_id', 'in', self.ids)], ['program_id'], ['program_id'])
|
||||
count_per_program = {r['program_id'][0]: r['program_id_count'] for r in read_group_data}
|
||||
for program in self:
|
||||
program.coupon_count = count_per_program.get(program.id, 0)
|
||||
|
||||
@api.depends('program_type', 'applies_on')
|
||||
def _compute_is_nominative(self):
|
||||
for program in self:
|
||||
program.is_nominative = program.applies_on == 'both' or\
|
||||
(program.program_type == 'ewallet' and program.applies_on == 'future')
|
||||
|
||||
@api.model
|
||||
def _program_type_default_values(self):
|
||||
# All values to change when program_type changes
|
||||
# NOTE: any field used in `rule_ids`, `reward_ids` and `communication_plan_ids` MUST be present in the kanban view for it to work properly.
|
||||
return {
|
||||
'coupons': {
|
||||
'applies_on': 'current',
|
||||
'trigger': 'with_code',
|
||||
'portal_visible': False,
|
||||
'portal_point_name': _('Points'),
|
||||
# Coupons don't use rules by default
|
||||
'rule_ids': [(5, 0, 0)],
|
||||
'reward_ids': [(5, 0, 0), (0, 0, {
|
||||
'required_points': 1,
|
||||
'discount': 10,
|
||||
})],
|
||||
'communication_plan_ids': [(5, 0, 0), (0, 0, {
|
||||
'trigger': 'create',
|
||||
'mail_template_id': (self.env.ref('loyalty.mail_template_loyalty_card', raise_if_not_found=False) or self.env['mail.template']).id,
|
||||
})],
|
||||
},
|
||||
'promotion': {
|
||||
'applies_on': 'current',
|
||||
'trigger': 'auto',
|
||||
'portal_visible': False,
|
||||
'portal_point_name': _('Points'),
|
||||
'rule_ids': [(5, 0, 0), (0, 0, {
|
||||
'reward_point_amount': 1,
|
||||
'reward_point_mode': 'order',
|
||||
'minimum_amount': 50,
|
||||
})],
|
||||
'reward_ids': [(5, 0, 0), (0, 0, {
|
||||
'required_points': 1,
|
||||
'discount': 10,
|
||||
})],
|
||||
'communication_plan_ids': [(5, 0, 0)],
|
||||
},
|
||||
'gift_card': {
|
||||
'applies_on': 'future',
|
||||
'trigger': 'auto',
|
||||
'portal_visible': True,
|
||||
'portal_point_name': self.env.company.currency_id.symbol,
|
||||
'rule_ids': [(5, 0, 0), (0, 0, {
|
||||
'reward_point_amount': 1,
|
||||
'reward_point_mode': 'money',
|
||||
'reward_point_split': True,
|
||||
'product_ids': self.env.ref('loyalty.gift_card_product_50', raise_if_not_found=False),
|
||||
})],
|
||||
'reward_ids': [(5, 0, 0), (0, 0, {
|
||||
'reward_type': 'discount',
|
||||
'discount_mode': 'per_point',
|
||||
'discount': 1,
|
||||
'discount_applicability': 'order',
|
||||
'required_points': 1,
|
||||
'description': _('Pay With Gift Card'),
|
||||
})],
|
||||
'communication_plan_ids': [(5, 0, 0), (0, 0, {
|
||||
'trigger': 'create',
|
||||
'mail_template_id': (self.env.ref('loyalty.mail_template_gift_card', raise_if_not_found=False) or self.env['mail.template']).id,
|
||||
})],
|
||||
},
|
||||
'loyalty': {
|
||||
'applies_on': 'both',
|
||||
'trigger': 'auto',
|
||||
'portal_visible': True,
|
||||
'portal_point_name': _('Loyalty Points'),
|
||||
'rule_ids': [(5, 0, 0), (0, 0, {
|
||||
'reward_point_mode': 'money',
|
||||
})],
|
||||
'reward_ids': [(5, 0, 0), (0, 0, {
|
||||
'discount': 5,
|
||||
'required_points': 200,
|
||||
})],
|
||||
'communication_plan_ids': [(5, 0, 0)],
|
||||
},
|
||||
'ewallet': {
|
||||
'applies_on': 'future',
|
||||
'trigger': 'auto',
|
||||
'portal_visible': True,
|
||||
'portal_point_name': self.env.company.currency_id.symbol,
|
||||
'rule_ids': [(5, 0, 0), (0, 0, {
|
||||
'reward_point_amount': '1',
|
||||
'reward_point_mode': 'money',
|
||||
'product_ids': self.env.ref('loyalty.ewallet_product_50'),
|
||||
})],
|
||||
'reward_ids': [(5, 0, 0), (0, 0, {
|
||||
'reward_type': 'discount',
|
||||
'discount_mode': 'per_point',
|
||||
'discount': 1,
|
||||
'discount_applicability': 'order',
|
||||
'required_points': 1,
|
||||
})],
|
||||
'communication_plan_ids': [(5, 0, 0)],
|
||||
},
|
||||
}
|
||||
|
||||
@api.depends('program_type')
|
||||
def _compute_from_program_type(self):
|
||||
program_type_defaults = self._program_type_default_values()
|
||||
grouped_programs = defaultdict(lambda: self.env['loyalty.program'])
|
||||
for program in self:
|
||||
grouped_programs[program.program_type] |= program
|
||||
for program_type, programs in grouped_programs.items():
|
||||
if program_type in program_type_defaults:
|
||||
programs.write(program_type_defaults[program_type])
|
||||
|
||||
def _get_valid_products(self, products):
|
||||
'''
|
||||
Returns a dict containing the products that match per rule of the program
|
||||
'''
|
||||
rule_products = dict()
|
||||
for rule in self.rule_ids:
|
||||
domain = rule._get_valid_product_domain()
|
||||
if domain:
|
||||
rule_products[rule] = products.filtered_domain(domain)
|
||||
else:
|
||||
rule_products[rule] = products
|
||||
return rule_products
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_active(self):
|
||||
if any(program.active for program in self):
|
||||
raise UserError(_('You can not delete a program in an active state'))
|
||||
|
||||
def toggle_active(self):
|
||||
super().toggle_active()
|
||||
# Propagate active state to children
|
||||
for program in self:
|
||||
program.rule_ids.active = program.active
|
||||
program.reward_ids.active = program.active
|
||||
program.communication_plan_ids.active = program.active
|
||||
program.reward_ids.discount_line_product_id.active = program.active
|
||||
|
||||
@api.model
|
||||
def get_program_templates(self):
|
||||
'''
|
||||
Returns the templates to be used for promotional programs.
|
||||
'''
|
||||
return {
|
||||
'promo_code': {
|
||||
'title': _('Promo Code'),
|
||||
'description': _('Get a code to receive 10% discount on specific products'),
|
||||
'icon': 'promo_code',
|
||||
},
|
||||
'gift_card': {
|
||||
'title': _('Gift Card'),
|
||||
'description': _('Sell Gift Cards, that can be used to purchase products'),
|
||||
'icon': 'gift_card',
|
||||
},
|
||||
'loyalty': {
|
||||
'title': _('Loyalty Cards'),
|
||||
'description': _('Win points with each purchases, and use points to get gifts'),
|
||||
'icon': 'loyalty_cards',
|
||||
},
|
||||
'fidelity': {
|
||||
'title': _('Fidelity Cards'),
|
||||
'description': _('Buy 10 products, and get 10$ discount on the 11th one'),
|
||||
'icon': 'fidelity_cards',
|
||||
},
|
||||
'promotion': {
|
||||
'title': _('Promotional Program'),
|
||||
'description': _('Automatic promotion: 10% discount on orders higher than $50'),
|
||||
'icon': 'promotional_program',
|
||||
},
|
||||
'coupons': {
|
||||
'title': _('Coupons'),
|
||||
'description': _('Send unique coupons that give access to rewards'),
|
||||
'icon': 'coupons',
|
||||
},
|
||||
'buy_two_get_one': {
|
||||
'title': _('2+1 Free'),
|
||||
'description': _('Buy 2 products and get a third one for free'),
|
||||
'icon': '2_plus_1',
|
||||
},
|
||||
'ewallet': {
|
||||
'title': _('eWallet'),
|
||||
'description': _('Fill in your eWallet, and use it to pay future orders'),
|
||||
'icon': 'ewallet',
|
||||
},
|
||||
}
|
||||
|
||||
@api.model
|
||||
def create_from_template(self, template_id):
|
||||
'''
|
||||
Creates the program from the template id defined in `get_program_templates`.
|
||||
|
||||
Returns an action leading to that new record.
|
||||
'''
|
||||
template_values = self._get_template_values()
|
||||
if template_id not in template_values:
|
||||
return False
|
||||
program = self.create(template_values[template_id])
|
||||
action = self.env['ir.actions.act_window']._for_xml_id('loyalty.loyalty_program_action')
|
||||
action['view_type'] = 'form'
|
||||
action['res_id'] = program.id
|
||||
return action
|
||||
|
||||
@api.model
|
||||
def _get_template_values(self):
|
||||
'''
|
||||
Returns the values to create a program using the template keys defined above.
|
||||
'''
|
||||
program_type_defaults = self._program_type_default_values()
|
||||
# For programs that require a product get the first sellable.
|
||||
product = self.env['product.product'].search([('sale_ok', '=', True)], limit=1)
|
||||
return {
|
||||
'gift_card': {
|
||||
'name': _('Gift Card'),
|
||||
'program_type': 'gift_card',
|
||||
**program_type_defaults['gift_card']
|
||||
},
|
||||
'ewallet': {
|
||||
'name': _('eWallet'),
|
||||
'program_type': 'ewallet',
|
||||
**program_type_defaults['ewallet'],
|
||||
},
|
||||
'loyalty': {
|
||||
'name': _('Loyalty Cards'),
|
||||
'program_type': 'loyalty',
|
||||
**program_type_defaults['loyalty'],
|
||||
},
|
||||
'coupons': {
|
||||
'name': _('Coupons'),
|
||||
'program_type': 'coupons',
|
||||
**program_type_defaults['coupons'],
|
||||
},
|
||||
'promotion': {
|
||||
'name': _('Promotional Program'),
|
||||
'program_type': 'promotion',
|
||||
**program_type_defaults['promotion'],
|
||||
},
|
||||
'promo_code': {
|
||||
'name': _('Promo Code'),
|
||||
'program_type': 'promotion',
|
||||
'applies_on': 'current',
|
||||
'trigger': 'with_code',
|
||||
'rule_ids': [(0, 0, {
|
||||
'mode': 'with_code',
|
||||
'code': '10PERCENT_' + str(uuid4())[:4], # Require a random code in case a user creates multiple program using this template
|
||||
})],
|
||||
'reward_ids': [(0, 0, {
|
||||
'discount_applicability': 'specific',
|
||||
'discount_product_ids': product,
|
||||
'discount_mode': 'percent',
|
||||
'discount': 10,
|
||||
})]
|
||||
},
|
||||
'fidelity': {
|
||||
'name': _('Fidelity Cards'),
|
||||
'program_type': 'loyalty',
|
||||
'applies_on': 'both',
|
||||
'trigger': 'auto',
|
||||
'rule_ids': [(0, 0, {
|
||||
'reward_point_mode': 'unit',
|
||||
'product_ids': product,
|
||||
})],
|
||||
'reward_ids': [(0, 0, {
|
||||
'discount_mode': 'per_order',
|
||||
'required_points': 11,
|
||||
'discount_applicability': 'specific',
|
||||
'discount_product_ids': product,
|
||||
'discount': 10,
|
||||
})]
|
||||
},
|
||||
'buy_two_get_one': {
|
||||
'name': _('2+1 Free'),
|
||||
'program_type': 'promotion',
|
||||
'applies_on': 'current',
|
||||
'trigger': 'auto',
|
||||
'rule_ids': [(0, 0, {
|
||||
'reward_point_mode': 'unit',
|
||||
'product_ids': product,
|
||||
})],
|
||||
'reward_ids': [(0, 0, {
|
||||
'reward_type': 'product',
|
||||
'reward_product_id': product and product.id or False,
|
||||
'required_points': 2,
|
||||
})]
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import ast
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.osv import expression
|
||||
|
||||
class LoyaltyReward(models.Model):
|
||||
_name = 'loyalty.reward'
|
||||
_description = 'Loyalty Reward'
|
||||
_rec_name = 'description'
|
||||
_order = 'required_points asc'
|
||||
|
||||
def _get_discount_mode_select(self):
|
||||
# The value is provided in the loyalty program's view since we may not have a program_id yet
|
||||
# and makes sure to display the currency related to the program instead of the company's.
|
||||
symbol = self.env.context.get('currency_symbol', self.env.company.currency_id.symbol)
|
||||
return [
|
||||
('percent', '%'),
|
||||
('per_point', _('%s per point', symbol)),
|
||||
('per_order', _('%s per order', symbol))
|
||||
]
|
||||
|
||||
def name_get(self):
|
||||
return [(reward.id, '%s - %s' % (reward.program_id.name, reward.description)) for reward in self]
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
program_id = fields.Many2one('loyalty.program', required=True, ondelete='cascade')
|
||||
# Stored for security rules
|
||||
company_id = fields.Many2one(related='program_id.company_id', store=True)
|
||||
currency_id = fields.Many2one(related='program_id.currency_id')
|
||||
|
||||
description = fields.Char(compute='_compute_description', readonly=False, store=True, translate=True)
|
||||
|
||||
reward_type = fields.Selection([
|
||||
('product', 'Free Product'),
|
||||
('discount', 'Discount')],
|
||||
default='discount', required=True,
|
||||
)
|
||||
|
||||
# Discount rewards
|
||||
discount = fields.Float('Discount', default=10)
|
||||
discount_mode = fields.Selection(selection=_get_discount_mode_select, required=True, default='percent')
|
||||
discount_applicability = fields.Selection([
|
||||
('order', 'Order'),
|
||||
('cheapest', 'Cheapest Product'),
|
||||
('specific', 'Specific Products')], default='order',
|
||||
)
|
||||
discount_product_domain = fields.Char(default="[]")
|
||||
discount_product_ids = fields.Many2many('product.product', string="Discounted Products")
|
||||
discount_product_category_id = fields.Many2one('product.category', string="Discounted Prod. Categories")
|
||||
discount_product_tag_id = fields.Many2one('product.tag', string="Discounted Prod. Tag")
|
||||
all_discount_product_ids = fields.Many2many('product.product', compute='_compute_all_discount_product_ids')
|
||||
discount_max_amount = fields.Monetary('Max Discount', 'currency_id',
|
||||
help="This is the max amount this reward may discount, leave to 0 for no limit.")
|
||||
discount_line_product_id = fields.Many2one('product.product', copy=False, ondelete='restrict',
|
||||
help="Product used in the sales order to apply the discount. Each reward has its own product for reporting purpose")
|
||||
is_global_discount = fields.Boolean(compute='_compute_is_global_discount')
|
||||
|
||||
# Product rewards
|
||||
reward_product_id = fields.Many2one('product.product', string='Product')
|
||||
reward_product_tag_id = fields.Many2one('product.tag', string='Product Tag')
|
||||
multi_product = fields.Boolean(compute='_compute_multi_product')
|
||||
reward_product_ids = fields.Many2many(
|
||||
'product.product', string="Reward Products", compute='_compute_multi_product',
|
||||
help="These are the products that can be claimed with this rule.")
|
||||
reward_product_qty = fields.Integer(default=1)
|
||||
reward_product_uom_id = fields.Many2one('uom.uom', compute='_compute_reward_product_uom_id')
|
||||
|
||||
required_points = fields.Float('Points needed', default=1)
|
||||
point_name = fields.Char(related='program_id.portal_point_name', readonly=True)
|
||||
clear_wallet = fields.Boolean(default=False)
|
||||
|
||||
_sql_constraints = [
|
||||
('required_points_positive', 'CHECK (required_points > 0)',
|
||||
'The required points for a reward must be strictly positive.'),
|
||||
('product_qty_positive', "CHECK (reward_type != 'product' OR reward_product_qty > 0)",
|
||||
'The reward product quantity must be strictly positive.'),
|
||||
('discount_positive', "CHECK (reward_type != 'discount' OR discount > 0)",
|
||||
'The discount must be strictly positive.'),
|
||||
]
|
||||
|
||||
@api.depends('reward_product_id.product_tmpl_id.uom_id', 'reward_product_tag_id')
|
||||
def _compute_reward_product_uom_id(self):
|
||||
for reward in self:
|
||||
reward.reward_product_uom_id = reward.reward_product_ids.product_tmpl_id.uom_id[:1]
|
||||
|
||||
def _get_discount_product_domain(self):
|
||||
self.ensure_one()
|
||||
domain = []
|
||||
if self.discount_product_ids:
|
||||
domain = [('id', 'in', self.discount_product_ids.ids)]
|
||||
if self.discount_product_category_id:
|
||||
domain = expression.OR([domain, [('categ_id', 'child_of', self.discount_product_category_id.id)]])
|
||||
if self.discount_product_tag_id:
|
||||
domain = expression.OR([domain, [('all_product_tag_ids', 'in', self.discount_product_tag_id.id)]])
|
||||
if self.discount_product_domain and self.discount_product_domain != '[]':
|
||||
domain = expression.AND([domain, ast.literal_eval(self.discount_product_domain)])
|
||||
return domain
|
||||
|
||||
@api.depends('discount_product_ids', 'discount_product_category_id', 'discount_product_tag_id', 'discount_product_domain')
|
||||
def _compute_all_discount_product_ids(self):
|
||||
for reward in self:
|
||||
reward.all_discount_product_ids = self.env['product.product'].search(reward._get_discount_product_domain())
|
||||
|
||||
@api.depends('reward_product_id', 'reward_product_tag_id', 'reward_type')
|
||||
def _compute_multi_product(self):
|
||||
for reward in self:
|
||||
products = reward.reward_product_id + reward.reward_product_tag_id.product_ids
|
||||
reward.multi_product = reward.reward_type == 'product' and len(products) > 1
|
||||
reward.reward_product_ids = reward.reward_type == 'product' and products or self.env['product.product']
|
||||
|
||||
@api.depends('reward_type', 'reward_product_id', 'discount_mode',
|
||||
'discount', 'currency_id', 'discount_applicability', 'all_discount_product_ids')
|
||||
def _compute_description(self):
|
||||
for reward in self:
|
||||
reward_string = ""
|
||||
if reward.reward_type == 'product':
|
||||
products = reward.reward_product_ids
|
||||
if len(products) == 1:
|
||||
reward_string = _('Free Product - %s', reward.reward_product_id.name)
|
||||
else:
|
||||
reward_string = _('Free Product - [%s]', ', '.join(products.mapped('name')))
|
||||
elif reward.reward_type == 'discount':
|
||||
format_string = '%(amount)g %(symbol)s'
|
||||
if reward.currency_id.position == 'before':
|
||||
format_string = '%(symbol)s %(amount)g'
|
||||
formatted_amount = format_string % {'amount': reward.discount, 'symbol': reward.currency_id.symbol}
|
||||
if reward.discount_mode == 'percent':
|
||||
reward_string = _('%g%% on ', reward.discount)
|
||||
elif reward.discount_mode == 'per_point':
|
||||
reward_string = _('%s per point on ', formatted_amount)
|
||||
elif reward.discount_mode == 'per_order':
|
||||
reward_string = _('%s per order on ', formatted_amount)
|
||||
if reward.discount_applicability == 'order':
|
||||
reward_string += _('your order')
|
||||
elif reward.discount_applicability == 'cheapest':
|
||||
reward_string += _('the cheapest product')
|
||||
elif reward.discount_applicability == 'specific':
|
||||
if len(reward.all_discount_product_ids) == 1:
|
||||
reward_string += reward.all_discount_product_ids.name
|
||||
else:
|
||||
reward_string += _('specific products')
|
||||
if reward.discount_max_amount:
|
||||
format_string = '%(amount)g %(symbol)s'
|
||||
if reward.currency_id.position == 'before':
|
||||
format_string = '%(symbol)s %(amount)g'
|
||||
formatted_amount = format_string % {'amount': reward.discount_max_amount, 'symbol': reward.currency_id.symbol}
|
||||
reward_string += _(' (Max %s)', formatted_amount)
|
||||
reward.description = reward_string
|
||||
|
||||
@api.depends('reward_type', 'discount_applicability', 'discount_mode')
|
||||
def _compute_is_global_discount(self):
|
||||
for reward in self:
|
||||
reward.is_global_discount = reward.reward_type == 'discount' and\
|
||||
reward.discount_applicability == 'order' and\
|
||||
reward.discount_mode == 'percent'
|
||||
|
||||
def _create_missing_discount_line_products(self):
|
||||
# Make sure we create the product that will be used for our discounts
|
||||
rewards = self.filtered(lambda r: not r.discount_line_product_id)
|
||||
products = self.env['product.product'].create(rewards._get_discount_product_values())
|
||||
for reward, product in zip(rewards, products):
|
||||
reward.discount_line_product_id = product
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
res = super().create(vals_list)
|
||||
res._create_missing_discount_line_products()
|
||||
return res
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if 'description' in vals:
|
||||
self._create_missing_discount_line_products()
|
||||
# Keep the name of our discount product up to date
|
||||
for reward in self:
|
||||
reward.discount_line_product_id.write({'name': reward.description})
|
||||
return res
|
||||
|
||||
def unlink(self):
|
||||
programs = self.program_id
|
||||
res = super().unlink()
|
||||
# Not guaranteed to trigger the constraint
|
||||
programs._constrains_reward_ids()
|
||||
return res
|
||||
|
||||
def _get_discount_product_values(self):
|
||||
return [{
|
||||
'name': reward.description,
|
||||
'type': 'service',
|
||||
'sale_ok': False,
|
||||
'purchase_ok': False,
|
||||
'lst_price': 0,
|
||||
} for reward in self]
|
||||
@@ -0,0 +1,107 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import ast
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.osv import expression
|
||||
|
||||
class LoyaltyRule(models.Model):
|
||||
_name = 'loyalty.rule'
|
||||
_description = 'Loyalty Rule'
|
||||
|
||||
def _get_reward_point_mode_selection(self):
|
||||
# The value is provided in the loyalty program's view since we may not have a program_id yet
|
||||
# and makes sure to display the currency related to the program instead of the company's.
|
||||
symbol = self.env.context.get('currency_symbol', self.env.company.currency_id.symbol)
|
||||
return [
|
||||
('order', _('per order')),
|
||||
('money', _('per %s spent', symbol)),
|
||||
('unit', _('per unit paid')),
|
||||
]
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
program_id = fields.Many2one('loyalty.program', required=True, ondelete='cascade')
|
||||
# Stored for security rules
|
||||
company_id = fields.Many2one(related='program_id.company_id', store=True)
|
||||
currency_id = fields.Many2one(related='program_id.currency_id')
|
||||
|
||||
# Only for dev mode
|
||||
product_domain = fields.Char(default="[]")
|
||||
|
||||
product_ids = fields.Many2many('product.product', string='Products')
|
||||
product_category_id = fields.Many2one('product.category', string='Categories')
|
||||
product_tag_id = fields.Many2one('product.tag', string='Product Tag')
|
||||
|
||||
reward_point_amount = fields.Float(default=1, string="Reward")
|
||||
# Only used for program_id.applies_on == 'future'
|
||||
reward_point_split = fields.Boolean(string='Split per unit', default=False,
|
||||
help="Whether to separate reward coupons per matched unit, only applies to 'future' programs and trigger mode per money spent or unit paid..")
|
||||
reward_point_name = fields.Char(related='program_id.portal_point_name', readonly=True)
|
||||
reward_point_mode = fields.Selection(selection=_get_reward_point_mode_selection, required=True, default='order')
|
||||
|
||||
minimum_qty = fields.Integer('Minimum Quantity', default=1)
|
||||
minimum_amount = fields.Monetary('Minimum Purchase', 'currency_id')
|
||||
minimum_amount_tax_mode = fields.Selection([
|
||||
('incl', 'Included'),
|
||||
('excl', 'Excluded')], default='incl', required=True,
|
||||
)
|
||||
|
||||
mode = fields.Selection([
|
||||
('auto', 'Automatic'),
|
||||
('with_code', 'With a promotion code'),
|
||||
], string="Application", default="auto")
|
||||
code = fields.Char(string='Promotion Code', compute='_compute_code', store=True, readonly=False)
|
||||
|
||||
_sql_constraints = [
|
||||
('reward_point_amount_positive', 'CHECK (reward_point_amount > 0)', 'Rule points reward must be strictly positive.'),
|
||||
]
|
||||
|
||||
@api.constrains('reward_point_split')
|
||||
def _constraint_trigger_multi(self):
|
||||
# Prevent setting trigger multi in case of nominative programs, it does not make sense to allow this
|
||||
for rule in self:
|
||||
if rule.reward_point_split and (rule.program_id.applies_on == 'both' or rule.program_id.program_type == 'ewallet'):
|
||||
raise ValidationError(_('Split per unit is not allowed for Loyalty and eWallet programs.'))
|
||||
|
||||
@api.constrains('code')
|
||||
def _constrains_code(self):
|
||||
mapped_codes = self.filtered('code').mapped('code')
|
||||
# Program code must be unique
|
||||
if len(mapped_codes) != len(set(mapped_codes)) or\
|
||||
self.env['loyalty.rule'].search_count(
|
||||
[('mode', '=', 'with_code'), ('code', 'in', mapped_codes), ('id', 'not in', self.ids)]):
|
||||
raise ValidationError(_('The promo code must be unique.'))
|
||||
# Prevent coupons and programs from sharing a code
|
||||
if self.env['loyalty.card'].search_count([('code', 'in', mapped_codes)]):
|
||||
raise ValidationError(_('A coupon with the same code was found.'))
|
||||
|
||||
@api.depends('mode')
|
||||
def _compute_code(self):
|
||||
# Reset code when mode is set to auto
|
||||
for rule in self:
|
||||
if rule.mode == 'auto':
|
||||
rule.code = False
|
||||
|
||||
def _get_valid_product_domain(self):
|
||||
self.ensure_one()
|
||||
domain = []
|
||||
if self.product_ids:
|
||||
domain = [('id', 'in', self.product_ids.ids)]
|
||||
if self.product_category_id:
|
||||
domain = expression.OR([domain, [('categ_id', 'child_of', self.product_category_id.id)]])
|
||||
if self.product_tag_id:
|
||||
domain = expression.OR([domain, [('all_product_tag_ids', 'in', self.product_tag_id.id)]])
|
||||
if self.product_domain and self.product_domain != '[]':
|
||||
domain = expression.AND([domain, ast.literal_eval(self.product_domain)])
|
||||
return domain
|
||||
|
||||
def _get_valid_products(self):
|
||||
self.ensure_one()
|
||||
return self.env['product.product'].search(self._get_valid_product_domain())
|
||||
|
||||
def _compute_amount(self, currency_to):
|
||||
self.ensure_one()
|
||||
return self.currency_id._convert(
|
||||
self.minimum_amount, currency_to, self.company_id, fields.Date.today())
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
class ProductProduct(models.Model):
|
||||
_inherit = 'product.product'
|
||||
|
||||
def write(self, vals):
|
||||
if not vals.get('active', True) and any(product.active for product in self):
|
||||
# Prevent archiving products used for giving rewards
|
||||
rewards = self.env['loyalty.reward'].search([('discount_line_product_id', 'in', self.ids)])
|
||||
if rewards:
|
||||
raise ValidationError(_("This product may not be archived. It is being used for an active promotion program."))
|
||||
return super().write(vals)
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="report_loyalty_card" model="ir.actions.report">
|
||||
<field name="name">Coupon Code</field>
|
||||
<field name="model">loyalty.card</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">loyalty.loyalty_report_i18n</field>
|
||||
<field name="report_file">loyalty.loyalty_report_i18n</field>
|
||||
<field name="binding_model_id" ref="model_loyalty_card"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
<record id="report_gift_card" model="ir.actions.report">
|
||||
<field name="name">Gift Card</field>
|
||||
<field name="model">loyalty.card</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">loyalty.gift_card_report_i18n</field>
|
||||
<field name="report_file">loyalty.gift_card_report_i18n</field>
|
||||
<field name="binding_model_id" ref="model_loyalty_card"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="loyalty_report">
|
||||
<t t-call="web.internal_layout">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="page">
|
||||
<div class="row text-center">
|
||||
<div class="o_offer col-lg-12">
|
||||
<h4 t-if="o._get_mail_partner().name">
|
||||
Congratulations
|
||||
<t t-esc="o._get_mail_partner().name"/>,
|
||||
</h4>
|
||||
<t t-set="text">on your next order</t>
|
||||
<h4>Here is your reward from <t t-esc="o.program_id.company_id.name"/>.</h4>
|
||||
<t t-foreach="range(len(o.program_id.reward_ids))" t-as="reward_idx">
|
||||
<t t-set="reward" t-value="o.program_id.reward_ids[reward_idx]"/>
|
||||
<strong><t t-esc="reward.description"/></strong>
|
||||
<br/>
|
||||
<t t-if="reward_idx < (len(o.program_id.reward_ids) - 1)">
|
||||
<span class="text-center">OR</span>
|
||||
<br/>
|
||||
</t>
|
||||
</t>
|
||||
<h1 class="font-weight-bold" style="font-size: 34px" t-esc="text"/>
|
||||
<br/>
|
||||
<h4 t-if="o.expiration_date">
|
||||
Use this promo code before
|
||||
<span t-field="o.expiration_date" t-options='{"format": "yyyy-MM-d"}'/>
|
||||
</h4>
|
||||
<h2 class="mt-4">
|
||||
<strong class="bg-light" t-esc="o.code"></strong>
|
||||
</h2>
|
||||
<t t-set="rule" t-value="o.program_id.rule_ids[:1]"/>
|
||||
<h4 t-if="rule.minimum_qty > 1">
|
||||
<span>Minimum purchase of</span>
|
||||
<strong t-esc="rule.minimum_qty"/> <span>products</span>
|
||||
</h4>
|
||||
<h4 t-if="rule.minimum_amount">
|
||||
<span>Valid for purchase above</span>
|
||||
<strong t-esc="rule.minimum_amount" t-options="{'widget': 'monetary', 'display_currency': rule.currency_id}"/>
|
||||
</h4>
|
||||
<br/>
|
||||
<div t-field="o.code" t-options="{'widget': 'barcode', 'width': 600, 'height': 100}"/>
|
||||
<br/><br/>
|
||||
<h4>Thank you,</h4>
|
||||
<br/>
|
||||
<div class="mt32">
|
||||
<div class="text-center">
|
||||
<img alt="Logo" t-att-src="'/logo?company=%d' % (o.program_id.company_id)" t-att-alt="'%s' % (o.program_id.company_id.name)" style="border:0px solid transparent; height: 50; width: 200px;" height="50"/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-center d-inline-block">
|
||||
<span t-field="o.program_id.company_id.partner_id"
|
||||
t-options='{"widget": "contact", "fields": ["address", "email"], "no_marker": True}'/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="loyalty_report_i18n">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="o">
|
||||
<t t-set="o" t-value="o.with_context(lang=o._get_mail_partner().lang or o.env.lang)"/>
|
||||
<t t-call="loyalty.loyalty_report" t-lang="o._get_mail_partner().lang or o.env.lang"/>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="gift_card_report">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="o">
|
||||
<t t-call="web.external_layout">
|
||||
<div style="margin:0px; font-size:24px; font-family:arial, 'helvetica neue', helvetica, sans-serif; line-height:36px; color:#333333; text-align: center">
|
||||
Here is your gift card!
|
||||
</div>
|
||||
<div style="padding-top:20px; padding-bottom:20px">
|
||||
<img src="/loyalty/static/img/gift_card.png" style="display:block; border:0; outline:none; text-decoration:none; margin:auto;" width="300"/>
|
||||
</div>
|
||||
<div style="padding:0; margin:0px; padding-top:35px; padding-bottom:35px; text-align:center;">
|
||||
<h3 style="margin:0px; line-height:48px; font-family:arial, 'helvetica neue', helvetica, sans-serif; font-size:40px; font-style:normal; font-weight:normal; color:#333333; text-align:center">
|
||||
<strong><span t-esc="o.points" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/></strong>
|
||||
</h3>
|
||||
</div>
|
||||
<div style="padding:0; margin:0px; padding-top:35px; padding-bottom:35px; background-color:#efefef; text-align:center;">
|
||||
<p style="margin:0px; font-size:14px;font-family:arial, 'helvetica neue', helvetica, sans-serif; line-height:21px; color:#333333">
|
||||
<strong>Gift Card Code</strong>
|
||||
</p>
|
||||
<p style="margin:0px; font-size:25px;font-family:arial, 'helvetica neue', helvetica, sans-serif; line-height:38px; color:#A9A9A9">
|
||||
<span t-field="o.code"/>
|
||||
</p>
|
||||
</div>
|
||||
<div style="padding:0; margin:0px; padding-top:10px; padding-bottom:10px; text-align:center;">
|
||||
<h3 style="margin:0px; line-height:17px; font-family:arial, 'helvetica neue', helvetica, sans-serif; font-size:14px; font-style:normal; font-weight:normal; color:#A9A9A9; text-align:center">
|
||||
Card expires <span t-field="o.expiration_date"/>
|
||||
</h3>
|
||||
</div>
|
||||
<div style="padding:0; margin:0px; padding-top:10px; padding-bottom:10px; text-align:center;">
|
||||
<img t-att-src="'/report/barcode/Code128/'+o.code" style="width:200px;height:50px" alt="Barcode"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="gift_card_report_i18n">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="o">
|
||||
<t t-set="o" t-value="o.with_context(lang=o._get_mail_partner().lang or o.env.lang)"/>
|
||||
<t t-call="loyalty.gift_card_report" t-lang="o._get_mail_partner().lang or o.env.lang"/>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -0,0 +1,7 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_loyalty_card,access_loyalty_card,model_loyalty_card,base.group_user,0,0,0,0
|
||||
access_loyalty_mail,access_loyalty_mail,model_loyalty_mail,base.group_user,0,0,0,0
|
||||
access_loyalty_program,access_loyalty_program,model_loyalty_program,base.group_user,0,0,0,0
|
||||
access_loyalty_reward,access_loyalty_reward,model_loyalty_reward,base.group_user,0,0,0,0
|
||||
access_loyalty_rule,access_loyalty_rule,model_loyalty_rule,base.group_user,0,0,0,0
|
||||
access_loyalty_generate_wizard,access_loyalty_generate_wizard,model_loyalty_generate_wizard,base.group_user,0,0,0,0
|
||||
|
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="sale_loyalty_program_company_rule" model="ir.rule">
|
||||
<field name="name">Loyalty program multi company rule</field>
|
||||
<field name="model_id" ref="model_loyalty_program"/>
|
||||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_loyalty_card_company_rule" model="ir.rule">
|
||||
<field name="name">Loyalty card multi company rule</field>
|
||||
<field name="model_id" ref="model_loyalty_card"/>
|
||||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_loyalty_rule_company_rule" model="ir.rule">
|
||||
<field name="name">Loyalty rule multi company rule</field>
|
||||
<field name="model_id" ref="model_loyalty_rule"/>
|
||||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_loyalty_reward_company_rule" model="ir.rule">
|
||||
<field name="name">Loyalty reward multi company rule</field>
|
||||
<field name="model_id" ref="model_loyalty_reward"/>
|
||||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><g id="a"/><g id="b"/><g id="c"/><g id="d"/><g id="e"/><g id="f"/><g id="g"><g><polyline points="32.77 50.17 32.77 30.11 49.81 20.27 50.29 19.74 32.72 9.6 15.15 19.74 15.21 20.13 32.31 30.01 41.24 25.08 24.14 15.2 15.15 19.74 15.15 40.03 32.72 50.17 50.29 40.03 50.29 19.74" style="fill:none; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/><polyline points="67.34 50.36 67.34 30.29 84.38 20.46 84.85 19.93 67.28 9.78 49.71 19.93 49.77 20.32 66.87 30.19 75.8 25.26 58.7 15.39 49.71 19.93 49.71 40.21 67.28 50.36 84.85 40.21 84.85 19.93" style="fill:none; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/><polyline points="50.28 90.4 50.28 70.34 67.32 60.5 67.79 59.97 50.23 49.83 32.66 59.97 32.71 60.37 49.82 70.24 58.75 65.31 41.64 55.44 32.66 59.97 32.66 80.26 50.23 90.4 67.79 80.26 67.79 59.97" style="fill:none; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/></g></g><g id="h"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><g id="a"/><g id="b"/><g id="c"/><g id="d"/><g id="e"/><g id="f"><g><g><line x1="19.65" y1="42.53" x2="22.48" y2="45.36" style="fill:none; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/><line x1="28.86" y1="51.74" x2="51.19" y2="74.07" style="fill:none; stroke:#7c6576; stroke-dasharray:0 0 9.02 9.02; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/><line x1="54.38" y1="77.26" x2="57.21" y2="80.09" style="fill:none; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/></g><path d="M68.28,13.26c-4.89,5.04-4.85,13.09,.13,18.08s13.03,5.02,18.08,.13h.01l4.98,4.97c2.82,2.82,2.82,7.4,0,10.23l-44.8,44.8c-2.82,2.82-7.4,2.82-10.23,0l-4.85-4.85c4.89-5.04,4.85-13.09-.13-18.08s-13.03-5.02-18.08-.13l-4.85-4.85c-2.82-2.82-2.82-7.4,0-10.23L53.33,8.53c2.82-2.82,7.4-2.82,10.23,0l4.72,4.72" style="fill:none; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/></g></g><g id="g"/><g id="h"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><g id="a"/><g id="b"/><g id="c"/><g id="d"/><g id="e"/><g id="f"/><g id="g"/><g id="h"><g><g><path d="M94.82,38.97l-3.66-3.65h0l-2.05-2.06-2.83,2.83,2.05,2.05,3.66,3.66c.72,.72,1.12,1.62,1.24,2.55,.17,1.32-.23,2.69-1.24,3.7l-43.4,43.4c-1.01,1.01-2.39,1.41-3.7,1.24-.93-.12-1.84-.52-2.55-1.24l-3.66-3.66-21.51-21.51c-.83-.83-1.29-1.95-1.29-3.12s.46-2.29,1.29-3.13L60.57,16.63c.83-.83,1.95-1.29,3.13-1.29s2.29,.46,3.12,1.29l5.71,5.71,2.83-2.83-12.55-12.55c-1.65-1.65-3.85-2.56-6.19-2.56s-4.54,.91-6.19,2.56L7.49,49.9c-3.41,3.41-3.41,8.96,0,12.38l28.35,28.35h.01l3.65,3.66c1.64,1.64,3.8,2.46,5.95,2.46s4.31-.82,5.95-2.46l43.4-43.4c3.28-3.28,3.28-8.62,0-11.91ZM12.1,61.22l-1.78-1.78c-1.85-1.85-1.85-4.87,0-6.72L53.26,9.79c.93-.93,2.14-1.39,3.36-1.39s2.43,.46,3.36,1.39l1.78,1.78c-1.51,.35-2.89,1.11-4.02,2.23L14.34,57.21c-1.12,1.12-1.88,2.51-2.23,4.02Z" style="fill:#7c6576;"/><path d="M63.42,37.56c-.2,.48-.29,1-.29,1.51s.1,1.03,.29,1.51c.2,.48,.49,.93,.88,1.33l2.42,2.42c.3,.3,.64,.52,1,.71,.58,.3,1.2,.47,1.84,.47,.77,0,1.54-.22,2.2-.66,.22-.15,.43-.32,.63-.51l11.06-11.06,2.83-2.83,.77-.77c.39-.39,.68-.84,.88-1.33,.39-.96,.39-2.05,0-3.02-.2-.48-.49-.93-.88-1.33l-2.42-2.42c-.2-.2-.41-.37-.63-.51-.17-.11-.34-.21-.52-.29-.53-.25-1.11-.37-1.69-.37-1.03,0-2.05,.39-2.83,1.17l-.77,.77-2.83,2.83-11.06,11.06c-.39,.39-.68,.84-.88,1.33Zm4.15-.13c1-1,2.63-1,3.63,0,.48,.48,.75,1.13,.75,1.81,0,.69-.27,1.33-.75,1.82-.5,.5-1.16,.75-1.81,.75s-1.31-.25-1.81-.75c-1-1.01-1-2.63,0-3.63Z" style="fill:#7c6576;"/></g><g><path d="M50.19,83.55c-.51,0-1.02-.2-1.41-.59-1.48-1.49-3.91-1.49-5.39,0-.78,.78-2.05,.78-2.83,0s-.78-2.05,0-2.83c3.05-3.05,8-3.05,11.05,0,.78,.78,.78,2.05,0,2.83-.39,.39-.9,.59-1.41,.59Z" style="fill:#7c6576;"/><path d="M56.51,77.56c-.51,0-1.02-.2-1.41-.59-2.36-2.36-5.5-3.67-8.85-3.67s-6.48,1.3-8.85,3.67c-.78,.78-2.05,.78-2.83,0-.78-.78-.78-2.05,0-2.83,3.12-3.12,7.26-4.84,11.67-4.84s8.56,1.72,11.68,4.84c.78,.78,.78,2.05,0,2.83-.39,.39-.9,.59-1.41,.59Z" style="fill:#7c6576;"/><path d="M62.41,71.53c-.51,0-1.02-.2-1.41-.59-3.96-3.96-9.23-6.14-14.83-6.14s-10.87,2.18-14.83,6.14c-.78,.78-2.05,.78-2.83,0s-.78-2.05,0-2.83c4.72-4.72,10.99-7.31,17.66-7.31s12.94,2.6,17.66,7.31c.78,.78,.78,2.05,0,2.83-.39,.39-.9,.59-1.41,.59Z" style="fill:#7c6576;"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><g id="a"/><g id="b"/><g id="c"/><g id="d"><g><rect x="11.09" y="23.03" width="77.83" height="53.94" rx="7.23" ry="7.23" style="fill:none; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/><g><path d="M31,34.36l1.17,2.37c.12,.24,.35,.41,.61,.45l2.62,.38c.67,.1,.93,.92,.45,1.39l-1.89,1.85c-.19,.19-.28,.46-.23,.72l.45,2.61c.11,.67-.58,1.17-1.18,.86l-2.34-1.23c-.24-.12-.52-.12-.76,0l-2.34,1.23c-.6,.31-1.3-.19-1.18-.86l.45-2.61c.05-.26-.04-.53-.23-.72l-1.89-1.85c-.48-.47-.22-1.29,.45-1.39l2.62-.38c.27-.04,.49-.21,.61-.45l1.17-2.37c.3-.61,1.16-.61,1.46,0Z" style="fill:#7c6576; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/><path d="M51.01,34.36l1.17,2.37c.12,.24,.35,.41,.61,.45l2.62,.38c.67,.1,.93,.92,.45,1.39l-1.89,1.85c-.19,.19-.28,.46-.23,.72l.45,2.61c.11,.67-.58,1.17-1.18,.86l-2.34-1.23c-.24-.12-.52-.12-.76,0l-2.34,1.23c-.6,.31-1.3-.19-1.18-.86l.45-2.61c.05-.26-.04-.53-.23-.72l-1.89-1.85c-.48-.47-.22-1.29,.45-1.39l2.62-.38c.27-.04,.49-.21,.61-.45l1.17-2.37c.3-.61,1.16-.61,1.46,0Z" style="fill:#7c6576; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/><path d="M31,54.37l1.17,2.37c.12,.24,.35,.41,.61,.45l2.62,.38c.67,.1,.93,.92,.45,1.39l-1.89,1.85c-.19,.19-.28,.46-.23,.72l.45,2.61c.11,.67-.58,1.17-1.18,.86l-2.34-1.23c-.24-.12-.52-.12-.76,0l-2.34,1.23c-.6,.31-1.3-.19-1.18-.86l.45-2.61c.05-.26-.04-.53-.23-.72l-1.89-1.85c-.48-.47-.22-1.29,.45-1.39l2.62-.38c.27-.04,.49-.21,.61-.45l1.17-2.37c.3-.61,1.16-.61,1.46,0Z" style="fill:#7c6576; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/><path d="M51.01,54.37l1.17,2.37c.12,.24,.35,.41,.61,.45l2.62,.38c.67,.1,.93,.92,.45,1.39l-1.89,1.85c-.19,.19-.28,.46-.23,.72l.45,2.61c.11,.67-.58,1.17-1.18,.86l-2.34-1.23c-.24-.12-.52-.12-.76,0l-2.34,1.23c-.6,.31-1.3-.19-1.18-.86l.45-2.61c.05-.26-.04-.53-.23-.72l-1.89-1.85c-.48-.47-.22-1.29,.45-1.39l2.62-.38c.27-.04,.49-.21,.61-.45l1.17-2.37c.3-.61,1.16-.61,1.46,0Z" style="fill:#7c6576; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/><path d="M71.03,34.36l1.17,2.37c.12,.24,.35,.41,.61,.45l2.62,.38c.67,.1,.93,.92,.45,1.39l-1.89,1.85c-.19,.19-.28,.46-.23,.72l.45,2.61c.11,.67-.58,1.17-1.18,.86l-2.34-1.23c-.24-.12-.52-.12-.76,0l-2.34,1.23c-.6,.31-1.3-.19-1.18-.86l.45-2.61c.05-.26-.04-.53-.23-.72l-1.89-1.85c-.48-.47-.22-1.29,.45-1.39l2.62-.38c.27-.04,.49-.21,.61-.45l1.17-2.37c.3-.61,1.16-.61,1.46,0Z" style="fill:#7c6576; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/></g></g></g><g id="e"/><g id="f"/><g id="g"/><g id="h"/></svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><g id="a"/><g id="b"><path d="M81.68,21.03H18.32c-5.09,0-9.23,4.14-9.23,9.23v39.47c0,5.09,4.14,9.23,9.23,9.23h63.36c5.09,0,9.23-4.14,9.23-9.23V30.26c0-5.09-4.14-9.23-9.23-9.23Zm-35.04,53.94l40.28-29.08v5.41l-32.78,23.67h-7.49Zm-2.96-30.58l-.46-.16-3.37-1.14-.61-.21c.92-1.19,1.35-2.63,1.28-4.06-.02-.35-.06-.7-.13-1.05-.19-.85-.57-1.66-1.13-2.38l4.86-1.36c.07,.25,.19,.49,.36,.7,.01,.02,.03,.04,.04,.06,.76,.97,1.58,2.29,1.78,3.65,.05,.32,.07,.65,.04,.97v.02c-.12,1.08-.75,2.04-1.92,2.94-.62,.48-.89,1.27-.73,2.01Zm.5-14.53l-.53,.15,6.67-4.82c.07-.05,.12-.11,.18-.16h7.53l-9.93,7.17c-.1-.43-.28-.85-.56-1.22-.79-1.03-2.11-1.47-3.36-1.12Zm-25.86-4.83h25.39l-10.32,7.45-7.5-2.54c-1.45-.49-3.04-.06-4.05,1.09-.63,.71-.95,1.62-.94,2.52-1.36,1.32-2.18,2.83-2.44,4.51-.05,.16-.07,.33-.08,.51,0,.06,0,.12,0,.17-.08,1.3,.17,2.66,.76,4.03l-6.06,4.37V30.26c0-2.89,2.35-5.23,5.23-5.23Zm-5.23,27.04l7.67-5.53c.11,.26,.24,.52,.42,.75,.61,.79,1.53,1.24,2.5,1.24,.29,0,.58-.04,.86-.12l.61-.17v.54l-12.06,8.7v-5.41Zm11.52-7.84c-.04-.13-.08-.25-.15-.37-.06-.12-.13-.23-.21-.33-.67-.85-1.41-2-1.72-3.21-.12-.48-.18-.96-.14-1.44,0-.02,0-.04,0-.05,.12-1.08,.75-2.04,1.92-2.94,.62-.48,.89-1.27,.73-2.01l4.37,1.48,.07,.02c-.79,1.02-1.22,2.22-1.28,3.45-.04,.72,.06,1.44,.27,2.13,.2,.66,.52,1.29,.95,1.87,.01,.01,.02,.03,.03,.04l-.32,.09-4,1.12-.54,.15Zm4.54,6.59v-3.71l5.52-1.55,1.2,.41,3.35,1.13v15.19l-3.71-3.44c-.38-.36-.87-.53-1.36-.53s-.98,.18-1.37,.54l-3.64,3.41v-11.44Zm3.06-11.99c.04-.25,.12-.5,.24-.73,.1-.18,.21-.36,.37-.51,.33-.33,.74-.53,1.17-.6,.12-.02,.25-.04,.37-.04,.56,0,1.12,.21,1.55,.64,.35,.35,.55,.79,.61,1.24,.09,.66-.11,1.35-.61,1.85-.85,.85-2.24,.85-3.09,0-.5-.5-.7-1.19-.61-1.85Zm-19.12,30.91v-7.32l12.06-8.7v13.16c0,.8,.47,1.52,1.2,1.83,.26,.11,.53,.17,.8,.17,.5,0,.99-.19,1.37-.54l5.65-5.28,5.7,5.29c.58,.54,1.43,.68,2.16,.37,.73-.32,1.2-1.04,1.2-1.83v-18.45c1.34,.3,2.74-.14,3.66-1.19,.63-.71,.95-1.62,.94-2.53,1.36-1.32,2.18-2.83,2.44-4.51,.05-.16,.07-.33,.08-.51,0-.06,0-.11,0-.17,.07-1.16-.13-2.36-.58-3.58l15.03-10.85s.05-.05,.08-.07h16.83c2.89,0,5.23,2.35,5.23,5.23v10.69l-46.53,33.59c-.17,.12-.3,.26-.42,.42H18.32c-2.88,0-5.23-2.35-5.23-5.23Zm68.6,5.23h-20.72l25.95-18.73v13.5c0,2.89-2.35,5.23-5.23,5.23Z" style="fill:#7c6576;"/></g><g id="c"/><g id="d"/><g id="e"/><g id="f"/><g id="g"/><g id="h"/></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><g id="a"/><g id="b"/><g id="c"><g><path d="M69.02,26.08L21.21,73.89c-.78,.78-2.05,.78-2.83,0s-.78-2.05,0-2.83L66.19,23.25c.78-.78,2.05-.78,2.83,0s.78,2.05,0,2.83Z" style="fill:#7c6576;"/><path d="M63.69,20.76L15.89,68.56c-.78,.78-2.05,.78-2.83,0s-.78-2.05,0-2.83L60.86,17.93c.78-.78,2.05-.78,2.83,0s.78,2.05,0,2.83Z" style="fill:#7c6576;"/><path d="M47.77,75.37l-3.64,3.64c-3.03,3.03-7.95,3.03-10.98,0s-3.03-7.95,0-10.98l3.64-3.64c3.03-3.03,7.95-3.03,10.98,0s3.03,7.95,0,10.98Zm-11.79-4.51c-1.47,1.47-1.47,3.86,0,5.32s3.86,1.47,5.32,0l3.64-3.64c1.47-1.47,1.47-3.86,0-5.32s-3.86-1.47-5.32,0l-3.64,3.64Z" style="fill:#7c6576;"/><g><path d="M43.56,88.76c-1.76,1.76-4.62,1.76-6.38,0L12.07,63.64c-1.76-1.76-1.76-4.62,0-6.38L52.39,16.94c1.76-1.76,4.62-1.76,6.38,0l15.72,15.72,1.61,.55,2.63-1.96L61.6,14.11c-3.32-3.32-8.72-3.32-12.03,0L9.24,54.43c-3.32,3.32-3.32,8.72,0,12.03l25.12,25.12c3.32,3.32,8.72,3.32,12.03,0l28.37-28.37-2.34-3.31-28.85,28.85Z" style="fill:#7c6576;"/><path d="M60.44,56.01c-.22-.42-.37-.86-.48-1.31l-3.85,3.85c-.78,.78-.78,2.05,0,2.83s2.05,.78,2.83,0l3.26-3.26c-.72-.55-1.33-1.26-1.76-2.1Z" style="fill:#7c6576;"/><path d="M63.22,40.78l-12.44,12.44c-.78,.78-.78,2.05,0,2.83s2.05,.78,2.83,0l9.92-9.92,1.03-1.38-1.34-3.96Z" style="fill:#7c6576;"/><path d="M85.55,32.64c-.17-.09-.35-.15-.54-.19-.56-.12-1.18-.02-1.72,.38l-1.7,1.27-3.24,2.42-.64,.48c-.28,.21-.61,.35-.95,.4-.34,.05-.69,.03-1.02-.09l-6.59-2.24c-.42-.14-.84-.15-1.21-.05-.38,.1-.72,.3-.99,.57-.14,.14-.25,.29-.35,.45-.19,.33-.3,.72-.28,1.13,0,.14,.05,.29,.08,.44,.02,.06,.01,.12,.03,.18l1.38,4.06,.86,2.53c.06,.17,.09,.34,.11,.51,.04,.52-.1,1.04-.42,1.46l-.04,.05-4.13,5.53c-.88,1.18-.31,2.76,.93,3.26,.25,.1,.52,.17,.82,.17l6.96-.09c.53,0,1.04,.18,1.43,.52,.13,.11,.25,.24,.35,.39l.61,.86,2.34,3.31,1.07,1.51c.12,.17,.25,.3,.4,.42,.04,.04,.09,.07,.14,.1,.11,.07,.22,.14,.33,.19,.06,.02,.11,.05,.17,.07,.12,.04,.24,.07,.37,.09,.05,0,.1,.02,.15,.02,.17,.02,.35,.01,.52-.01,.02,0,.05-.01,.07-.02,.15-.03,.29-.07,.43-.13,.05-.02,.1-.05,.15-.07,.11-.06,.21-.12,.31-.19,.05-.04,.1-.07,.14-.11,.03-.03,.06-.05,.09-.07,.06-.06,.11-.14,.16-.21,.04-.05,.08-.09,.11-.14,.1-.16,.19-.33,.25-.53l2.07-6.65c.1-.34,.29-.64,.53-.88,.18-.18,.4-.32,.64-.42,.08-.04,.15-.08,.24-.11l6.65-2.07c.2-.06,.37-.15,.53-.25,.05-.03,.1-.07,.14-.11,.07-.05,.15-.1,.21-.16,.03-.03,.05-.06,.07-.09,.04-.05,.08-.09,.11-.14,.07-.1,.14-.2,.19-.31,.03-.05,.05-.1,.07-.15,.06-.14,.1-.28,.13-.43,0-.02,.01-.05,.02-.07,.03-.17,.03-.35,.01-.52,0-.05-.02-.1-.03-.15-.02-.12-.05-.25-.09-.37-.02-.06-.04-.11-.07-.17-.05-.11-.12-.22-.19-.33-.03-.05-.06-.1-.1-.14-.12-.14-.26-.28-.42-.4l-4.05-2.87-1.63-1.15c-.14-.1-.27-.22-.39-.35-.34-.39-.53-.9-.52-1.43l.03-2.37,.06-4.6c0-.44-.12-.84-.33-1.17s-.5-.59-.84-.77Z" style="fill:#7c6576;"/></g></g></g><g id="d"/><g id="e"/><g id="f"/><g id="g"/><g id="h"/></svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><g id="a"/><g id="b"/><g id="c"><g><rect x="9.79" y="23.51" width="77.83" height="53.94" rx="7.23" ry="7.23" style="fill:none; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/><line x1="86.26" y1="43.76" x2="11.14" y2="43.76" style="fill:none; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/><line x1="11.14" y1="35.4" x2="86.26" y2="35.4" style="fill:none; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/><g><line x1="69.26" y1="63.57" x2="50.61" y2="63.57" style="fill:none; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/><line x1="50.61" y1="55.2" x2="77.03" y2="55.2" style="fill:none; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/></g><rect x="18.71" y="52.98" width="18.53" height="12.81" rx="6.41" ry="6.41" style="fill:none; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/><g><path d="M86.23,86.49c-.71,0-1.4-.17-2.04-.51l-6.85-3.6c-.06-.03-.12-.04-.18-.04s-.12,.01-.18,.04l-6.85,3.6c-.64,.34-1.33,.51-2.04,.51-1.29,0-2.52-.57-3.36-1.57-.83-.98-1.18-2.28-.96-3.56l1.31-7.62c.02-.12-.02-.25-.11-.34l-5.54-5.4c-1.2-1.17-1.63-2.89-1.11-4.49,.52-1.6,1.88-2.74,3.54-2.98l7.66-1.11c.12-.02,.23-.1,.29-.21l3.42-6.94c.74-1.51,2.25-2.44,3.93-2.44h0c1.68,0,3.19,.94,3.93,2.44l3.42,6.94c.06,.11,.16,.19,.29,.21l7.65,1.11c1.66,.24,3.02,1.38,3.54,2.98,.52,1.6,.09,3.32-1.11,4.49l-5.54,5.4c-.09,.09-.13,.21-.11,.34l1.31,7.62c.22,1.28-.13,2.57-.96,3.56-.84,1-2.07,1.57-3.36,1.57Z" style="fill:#7c6576;"/><path d="M77.17,51.83c.85,0,1.7,.44,2.14,1.33l3.42,6.94c.35,.7,1.02,1.19,1.79,1.3l7.66,1.11c1.95,.28,2.73,2.68,1.32,4.06l-5.54,5.4c-.56,.55-.82,1.34-.69,2.11l1.31,7.62c.26,1.54-.96,2.79-2.35,2.79-.37,0-.74-.09-1.11-.28l-6.85-3.6c-.35-.18-.73-.27-1.11-.27s-.76,.09-1.11,.27l-6.85,3.6c-.36,.19-.74,.28-1.11,.28-1.39,0-2.61-1.25-2.35-2.79l1.31-7.62c.13-.77-.12-1.56-.69-2.11l-5.54-5.4c-1.41-1.38-.63-3.78,1.32-4.06l7.66-1.11c.78-.11,1.45-.6,1.79-1.3l3.42-6.94c.44-.89,1.29-1.33,2.14-1.33m0-4c-2.45,0-4.64,1.36-5.72,3.56l-3.05,6.17-6.81,.99c-2.42,.35-4.4,2.02-5.15,4.34-.76,2.33-.14,4.83,1.61,6.54l4.93,4.81-1.16,6.79c-.32,1.86,.19,3.75,1.4,5.18,1.22,1.45,3,2.28,4.89,2.28,1.02,0,2.05-.26,2.97-.74l6.09-3.2,6.09,3.2c.92,.48,1.95,.74,2.97,.74,1.88,0,3.67-.83,4.89-2.28,1.21-1.43,1.72-3.32,1.4-5.18l-1.16-6.79,4.93-4.81c1.75-1.71,2.37-4.21,1.61-6.54-.76-2.33-2.73-3.99-5.15-4.34l-6.81-.99-3.05-6.17c-1.08-2.19-3.28-3.56-5.72-3.56h0Z" style="fill:#fff;"/></g></g></g><g id="d"/><g id="e"/><g id="f"/><g id="g"/><g id="h"/></svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><g id="a"><g><path d="M53.33,8.53L8.53,53.33c-2.82,2.82-2.82,7.4,0,10.23l4.85,4.85c5.04-4.89,13.09-4.85,18.08,.13s5.02,13.03,.13,18.08l4.85,4.85c2.82,2.82,7.4,2.82,10.23,0l44.8-44.8c2.82-2.82,2.82-7.4,0-10.23L63.56,8.53c-2.82-2.82-7.4-2.82-10.23,0Z" style="fill:none; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/><path d="M31.59,86.62c4.89-5.04,4.85-13.09-.13-18.08s-13.03-5.02-18.08-.13" style="fill:none; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/><g><line x1="19.65" y1="42.53" x2="22.48" y2="45.36" style="fill:none; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/><line x1="28.86" y1="51.74" x2="51.19" y2="74.07" style="fill:none; stroke:#7c6576; stroke-dasharray:0 0 9.02 9.02; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/><line x1="54.38" y1="77.26" x2="57.21" y2="80.09" style="fill:none; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/></g><line x1="57.32" y1="59.75" x2="57.32" y2="25.61" style="fill:none; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/><circle cx="69.58" cy="42.13" r="4.93" style="fill:none; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/><circle cx="44.81" cy="42.92" r="4.93" style="fill:none; stroke:#7c6576; stroke-linecap:round; stroke-linejoin:round; stroke-width:4px;"/></g></g><g id="b"/><g id="c"/><g id="d"/><g id="e"/><g id="f"/><g id="g"/><g id="h"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><g id="a"/><g id="b"/><g id="c"/><g id="d"/><g id="e"><g><g><path d="M58.32,64.91c-.18,0-.36-.02-.53-.07l-27.73-7.62c-.87-.24-1.47-1.03-1.47-1.93v-19c0-.9,.6-1.69,1.47-1.93l27.73-7.62c.6-.16,1.25-.04,1.74,.34,.5,.38,.79,.97,.79,1.59V62.91c0,.62-.29,1.21-.79,1.59-.35,.27-.78,.41-1.21,.41Zm-25.73-11.15l23.73,6.52V31.29l-23.73,6.52v15.95Z" style="fill:#7c6576;"/><g><path d="M24.32,72.61c.25,.94,.85,1.72,1.7,2.21,.84,.49,1.82,.61,2.76,.36h0c.94-.25,1.72-.85,2.21-1.69,.48-.84,.61-1.82,.36-2.76l-3.64-13.53h-7.53l4.14,15.42Z" style="fill:none;"/><path d="M35.21,69.69l-3.46-12.87c.51-.36,.84-.95,.84-1.63v-18.82c0-1.1-.9-2-2-2h-11.19c-4.46,0-8.09,3.63-8.09,8.09v6.63c0,3.17,1.84,5.92,4.51,7.24,0,.02,0,.05,.02,.07l4.63,17.23c.53,1.97,1.79,3.62,3.56,4.63,1.17,.68,2.48,1.02,3.8,1.02,.67,0,1.33-.09,1.99-.26,1.97-.53,3.61-1.79,4.63-3.56s1.29-3.82,.76-5.79ZM15.3,42.47c0-2.26,1.84-4.09,4.09-4.09h9.19v14.82h-9.19c-1.33,0-2.51-.65-3.26-1.64-.52-.69-.83-1.53-.83-2.45v-6.63Zm15.68,31.01c-.49,.84-1.27,1.44-2.21,1.69h0c-.94,.25-1.92,.12-2.76-.36-.84-.49-1.44-1.27-1.7-2.21l-4.14-15.42h7.53l3.64,13.53c.25,.94,.12,1.92-.36,2.76Z" style="fill:#7c6576;"/></g><g><path d="M69.27,45.79c0-1.52-.97-2.81-2.33-3.31v6.62c1.35-.5,2.33-1.79,2.33-3.31Z" style="fill:none;"/><path d="M66.94,38.36v-12.33c0-2.94-2.39-5.33-5.33-5.33s-5.33,2.39-5.33,5.33v39.53c0,2.94,2.39,5.33,5.33,5.33s5.33-2.39,5.33-5.33v-12.33c3.58-.58,6.33-3.69,6.33-7.43s-2.75-6.85-6.33-7.43Zm-4,.44v26.76c0,.73-.6,1.33-1.33,1.33s-1.33-.6-1.33-1.33V26.02c0-.73,.6-1.33,1.33-1.33s1.33,.6,1.33,1.33v12.77Zm4,10.31v-6.62c1.35,.5,2.33,1.79,2.33,3.31s-.97,2.81-2.33,3.31Z" style="fill:#7c6576;"/></g></g><g><path d="M86.69,47.79h-6.51c-1.1,0-2-.9-2-2s.9-2,2-2h6.51c1.1,0,2,.9,2,2s-.9,2-2,2Z" style="fill:#7c6576;"/><path d="M84.2,64.37c-.51,0-1.02-.2-1.41-.59l-4.42-4.42c-.78-.78-.78-2.05,0-2.83s2.05-.78,2.83,0l4.42,4.42c.78,.78,.78,2.05,0,2.83-.39,.39-.9,.59-1.41,.59Z" style="fill:#7c6576;"/><path d="M79.78,35.63c-.51,0-1.02-.2-1.41-.59-.78-.78-.78-2.05,0-2.83l4.43-4.43c.78-.78,2.05-.78,2.83,0s.78,2.05,0,2.83l-4.43,4.43c-.39,.39-.9,.59-1.41,.59Z" style="fill:#7c6576;"/></g></g></g><g id="f"/><g id="g"/><g id="h"/></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,36 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import ControlPanelX2Many from 'web.ControlPanelX2Many';
|
||||
import { FieldOne2Many } from 'web.relational_fields';
|
||||
import fieldRegistry from 'web.field_registry';
|
||||
|
||||
export class LoyaltyControlPanelX2Many extends ControlPanelX2Many {
|
||||
}
|
||||
|
||||
LoyaltyControlPanelX2Many.props = Object.assign({}, ControlPanelX2Many.props, {
|
||||
label: String,
|
||||
});
|
||||
LoyaltyControlPanelX2Many.template = 'LoyaltyControlPanelX2Many';
|
||||
|
||||
export const LoyaltyFieldOne2Many = FieldOne2Many.extend({
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_getControlPanelComponent: function () {
|
||||
return LoyaltyControlPanelX2Many;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_getControlPanelContext: function () {
|
||||
const context = this._super.apply(this);
|
||||
Object.assign(context, {
|
||||
label: this.string,
|
||||
});
|
||||
return context;
|
||||
}
|
||||
});
|
||||
|
||||
fieldRegistry.add('loyalty_one2many', LoyaltyFieldOne2Many);
|
||||
@@ -0,0 +1,23 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { LoyaltyModelMixin, LoyaltyRendererMixin } from "@loyalty/js/loyalty_views";
|
||||
import ListModel from 'web.ListModel';
|
||||
import ListRenderer from 'web.ListRenderer';
|
||||
import ListView from 'web.ListView';
|
||||
import viewRegistry from 'web.view_registry';
|
||||
|
||||
export const LoyaltyListModel = ListModel.extend(LoyaltyModelMixin);
|
||||
export const LoyaltyListRenderer = ListRenderer.extend(LoyaltyRendererMixin, {
|
||||
events: _.extend({}, ListRenderer.prototype.events, {
|
||||
'click .loyalty-template': '_onTemplateClick',
|
||||
}),
|
||||
});
|
||||
|
||||
export const LoyaltyListView = ListView.extend({
|
||||
config: _.extend({}, ListView.prototype.config, {
|
||||
Model: LoyaltyListModel,
|
||||
Renderer: LoyaltyListRenderer,
|
||||
})
|
||||
});
|
||||
|
||||
viewRegistry.add('loyalty_program_list_view', LoyaltyListView);
|
||||
@@ -0,0 +1,61 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import session from 'web.session';
|
||||
import core from 'web.core';
|
||||
const qweb = core.qweb;
|
||||
|
||||
/*
|
||||
* This file provides the necessary mixins that can be used on views to change the behaviour of the help
|
||||
* display (no records) in order to provide templates to the user
|
||||
*/
|
||||
|
||||
export const LoyaltyModelMixin = {
|
||||
__load: async function () {
|
||||
const res = await this._super.apply(this, arguments);
|
||||
|
||||
this.loyaltyTemplateData = await this._rpc({
|
||||
model: 'loyalty.program',
|
||||
method: 'get_program_templates',
|
||||
context: session.user_context,
|
||||
});
|
||||
return res;
|
||||
},
|
||||
|
||||
__get: function () {
|
||||
const res = this._super.apply(this, arguments);
|
||||
if (res) {
|
||||
res.loyaltyTemplateData = this.loyaltyTemplateData;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
export const LoyaltyRendererMixin = {
|
||||
_renderNoContentHelper: function (context) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
if (!this.state.loyaltyTemplateData) {
|
||||
return;
|
||||
}
|
||||
const noContentHelper = this.$('.o_view_nocontent');
|
||||
if (noContentHelper) {
|
||||
noContentHelper.append(qweb.render('loyalty_program_helper', {
|
||||
templateData: this.state.loyaltyTemplateData,
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
_onTemplateClick: async function (ev) {
|
||||
ev.stopPropagation();
|
||||
const action = await this._rpc({
|
||||
model: 'loyalty.program',
|
||||
method: 'create_from_template',
|
||||
args: [ev.currentTarget.id || ''],
|
||||
context: session.user_context,
|
||||
});
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
this.do_action(action);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
.o_loyalty_kanban_inline {
|
||||
|
||||
width: 100% !important;
|
||||
|
||||
.o_kanban_record .o_kanban_tags .o_tag span {
|
||||
// Remove the small ball before the tags
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
background-color: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.o_loyalty_not_found {
|
||||
margin-top: -15vh;
|
||||
}
|
||||
|
||||
.loyalty-templates-container {
|
||||
pointer-events: auto;
|
||||
margin-top: -5vh;
|
||||
|
||||
.loyalty-template {
|
||||
&, * {
|
||||
transition: all .15s;
|
||||
}
|
||||
|
||||
cursor: pointer !important;
|
||||
|
||||
img {
|
||||
filter: invert(.5);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
* {
|
||||
color: #7C6576 !important;
|
||||
}
|
||||
|
||||
background-color: var(--o-color-4);
|
||||
box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.1), 0 2px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
border-color: #7C6576 !important;
|
||||
|
||||
img {
|
||||
filter: invert(0);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
background-color: var(--o-color-4) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loyalty-rule-form {
|
||||
// The base width for this field is 100px which is problematic for us.
|
||||
.o_field_widget.o_field_monetary.o_input > input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<t t-name="loyalty_program_helper">
|
||||
<div class="container">
|
||||
<div class="row loyalty-templates-container">
|
||||
<t t-foreach="Object.entries(templateData)" t-as="data">
|
||||
<t t-set="loyalty_el_icon" t-value="data[1].icon"/>
|
||||
<t t-set="loyalty_el_title" t-value="data[1].title"/>
|
||||
<div class="col-6 col-lg-3 py-4">
|
||||
<div class="card rounded p-3 d-flex align-items-stretch h-100 loyalty-template" t-att-id="data[0]">
|
||||
<div class="row m-0 w-100 h-100">
|
||||
<div class="col-lg-4 p-0">
|
||||
<div class="d-flex w-100 h-100 align-items-center justify-content-center display-3 p-3 text-muted">
|
||||
<img t-attf-src="/loyalty/static/img/{{loyalty_el_icon}}.svg" t-attf-alt="{{loyalty_el_title}}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-8 p-0">
|
||||
<div class="card-body d-flex flex-column text-center justify-content-center text-lg-left h-100">
|
||||
<h3 class="card-title" t-out="loyalty_el_title"/>
|
||||
<p class="card-text" t-out="data[1].description"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="LoyaltyControlPanelX2Many" owl="1">
|
||||
<div t-ref="controlPanel">
|
||||
<h4 class="d-inline"><t t-esc="props.label or ''"/></h4>
|
||||
<div class="o_cp_buttons float-right" role="toolbar" aria-label="Control panel buttons" t-ref="buttons">
|
||||
<t t-slot="buttons"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</odoo>
|
||||
@@ -1,4 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
from . import test_loyalty
|
||||
@@ -0,0 +1,98 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from psycopg2 import IntegrityError
|
||||
|
||||
from odoo.tests import tagged, TransactionCase
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestLoyalty(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.program = cls.env['loyalty.program'].create({
|
||||
'name': 'Test Program',
|
||||
'reward_ids': [(0, 0, {})],
|
||||
})
|
||||
|
||||
|
||||
def test_discount_product_unlink(self):
|
||||
# Test that we can not unlink dicount line product id
|
||||
with mute_logger('odoo.sql_db'):
|
||||
with self.assertRaises(IntegrityError):
|
||||
with self.cr.savepoint():
|
||||
self.program.reward_ids.discount_line_product_id.unlink()
|
||||
|
||||
def test_loyalty_mail(self):
|
||||
# Test basic loyalty_mail functionalities
|
||||
loyalty_card_model_id = self.env.ref('loyalty.model_loyalty_card')
|
||||
create_tmpl, fifty_tmpl, hundred_tmpl = self.env['mail.template'].create([
|
||||
{
|
||||
'name': 'CREATE',
|
||||
'model_id': loyalty_card_model_id.id,
|
||||
},
|
||||
{
|
||||
'name': '50 points',
|
||||
'model_id': loyalty_card_model_id.id,
|
||||
},
|
||||
{
|
||||
'name': '100 points',
|
||||
'model_id': loyalty_card_model_id.id,
|
||||
},
|
||||
])
|
||||
self.program.write({'communication_plan_ids': [
|
||||
(0, 0, {
|
||||
'program_id': self.program.id,
|
||||
'trigger': 'create',
|
||||
'mail_template_id': create_tmpl.id,
|
||||
}),
|
||||
(0, 0, {
|
||||
'program_id': self.program.id,
|
||||
'trigger': 'points_reach',
|
||||
'points': 50,
|
||||
'mail_template_id': fifty_tmpl.id,
|
||||
}),
|
||||
(0, 0, {
|
||||
'program_id': self.program.id,
|
||||
'trigger': 'points_reach',
|
||||
'points': 100,
|
||||
'mail_template_id': hundred_tmpl.id,
|
||||
}),
|
||||
]})
|
||||
|
||||
sent_mails = self.env['mail.template']
|
||||
|
||||
def mock_send_mail(self, *args, **kwargs):
|
||||
nonlocal sent_mails
|
||||
sent_mails |= self
|
||||
|
||||
partner = self.env['res.partner'].create({'name': 'Test Partner'})
|
||||
with patch('odoo.addons.mail.models.mail_template.MailTemplate.send_mail', new=mock_send_mail):
|
||||
# Send mail at creation
|
||||
coupon = self.env['loyalty.card'].create({
|
||||
'program_id': self.program.id,
|
||||
'partner_id': partner.id,
|
||||
'points': 0,
|
||||
})
|
||||
self.assertEqual(sent_mails, create_tmpl)
|
||||
sent_mails = self.env['mail.template']
|
||||
# 50 points mail
|
||||
coupon.points = 50
|
||||
self.assertEqual(sent_mails, fifty_tmpl)
|
||||
sent_mails = self.env['mail.template']
|
||||
# Check that it does not get sent again
|
||||
coupon.points = 99
|
||||
self.assertFalse(sent_mails)
|
||||
# 100 points mail
|
||||
coupon.points = 100
|
||||
self.assertEqual(sent_mails, hundred_tmpl)
|
||||
sent_mails = self.env['mail.template']
|
||||
# Reset and go straight to 100 points
|
||||
coupon.points = 0
|
||||
self.assertFalse(sent_mails)
|
||||
coupon.points = 100
|
||||
self.assertEqual(sent_mails, hundred_tmpl)
|
||||
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="loyalty_card_view_form" model="ir.ui.view">
|
||||
<field name="name">loyalty.card.view.form</field>
|
||||
<field name="model">loyalty.card</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="code"/>
|
||||
<field name="expiration_date"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="points"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="loyalty_card_view_tree" model="ir.ui.view">
|
||||
<field name="name">loyalty.card.view.tree</field>
|
||||
<field name="model">loyalty.card</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Coupons" create="false" edit="false" delete="false">
|
||||
<field name="code"/>
|
||||
<field name="points" string="Balance"/>
|
||||
<field name="expiration_date"/>
|
||||
<field name="program_id"/>
|
||||
<field name="partner_id"/>
|
||||
<button name="action_coupon_send" string="Send" type="object" icon="fa-paper-plane-o"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="loyalty_card_action" model="ir.actions.act_window">
|
||||
<field name="name">Coupons</field>
|
||||
<field name="res_model">loyalty.card</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[('program_id', '=', active_id)]</field>
|
||||
<field name="context">{'create': False}</field>
|
||||
<field name="help" type="html">
|
||||
<h1>No Coupons Found.</h1>
|
||||
<p>There haven't been any coupons generated yet.</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="loyalty_mail_view_tree" model="ir.ui.view">
|
||||
<field name="name">loyalty.mail.view.tree</field>
|
||||
<field name="model">loyalty.mail</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="bottom">
|
||||
<field name="trigger"/>
|
||||
<field name="points" string="Limit"
|
||||
attrs="{'required': [('trigger', '=', 'points_reach')], 'invisible': [('trigger', '!=', 'points_reach')]}"/>
|
||||
<field name="mail_template_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,147 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="loyalty_program_view_form" model="ir.ui.view">
|
||||
<field name="name">loyalty.program.view.form</field>
|
||||
<field name="model">loyalty.program</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Coupons & Loyalty">
|
||||
<header>
|
||||
<button name="%(loyalty_generate_wizard_action)d" string="Generate Coupons" class="btn-primary" type="action"
|
||||
attrs="{'invisible': [('program_type', '!=', 'coupons')]}"/>
|
||||
<button name="%(loyalty_generate_wizard_action)d" string="Generate Gift Cards" class="btn-primary" type="action"
|
||||
attrs="{'invisible': [('program_type', '!=', 'gift_card')]}"/>
|
||||
<button name="%(loyalty_generate_wizard_action)d" string="Generate eWallet" class="btn-primary" type="action"
|
||||
attrs="{'invisible': [('program_type', '!=', 'ewallet')]}" context="{'default_mode': 'selected'}"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button class="oe_stat_button" type="action" name="%(loyalty_card_action)d" icon="fa-tags">
|
||||
<div class="o_form_field o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="coupon_count"/>
|
||||
</span>
|
||||
<span class="o_stat_text" attrs="{'invisible': [('program_type', '!=', 'coupons')]}">Coupons</span>
|
||||
<span class="o_stat_text" attrs="{'invisible': [('program_type', '!=', 'gift_card')]}">Gift Cards</span>
|
||||
<span class="o_stat_text" attrs="{'invisible': [('program_type', '!=', 'ewallet')]}">eWallets</span>
|
||||
<span class="o_stat_text" attrs="{'invisible': [('program_type', '!=', 'loyalty')]}">Loyalty Cards</span>
|
||||
<span class="o_stat_text" attrs="{'invisible': [('program_type', '!=', 'promotion')]}">Promos</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<field name="active" invisible="1"/>
|
||||
<div class="oe_title">
|
||||
<label for="name" string="Program Name"/>
|
||||
<h1>
|
||||
<field name="name" placeholder="e.g. 10% discount on laptops"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<label for="program_type"/>
|
||||
<div>
|
||||
<field name="program_type" widget="selection"/>
|
||||
<p class="text-muted" attrs="{'invisible': [('program_type', '!=', 'coupons')]}" colspan="2">
|
||||
Generate & share coupon code manually.
|
||||
<br/>
|
||||
Rewards are provided when the user provides a coupon code in the eCommerce, Point of Sale or regular orders.
|
||||
<br/>
|
||||
Add triggers for constraints on coupon usage.
|
||||
</p>
|
||||
<p class="text-muted" attrs="{'invisible': [('program_type', '!=', 'loyalty')]}" colspan="2">
|
||||
Once a sale order is validated, the customers get Loyalty Points that can be used in the current order, or accumulated in future orders.
|
||||
</p>
|
||||
<p class="text-muted" attrs="{'invisible': [('program_type', '!=', 'gift_card')]}" colspan="2">
|
||||
Gift Cards are created and sent by email when the customer orders a product defined in your triggers.
|
||||
<br/>
|
||||
Then, Gift Cards can be used to pay orders.
|
||||
</p>
|
||||
<p class="text-muted" attrs="{'invisible': [('program_type', '!=', 'promotion')]}" colspan="2">
|
||||
Set up triggers based on a promotional code and/or products purchased, that give access to reward on the current order.
|
||||
</p>
|
||||
<p class="text-muted" attrs="{'invisible': [('program_type', '!=', 'ewallet')]}" colspan="2">
|
||||
Personal eWallet are created when the customer orders a product defined in the triggers.
|
||||
<br/>
|
||||
Then, eWallets are proposed during the checkout, to pay orders.
|
||||
</p>
|
||||
</div>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="currency_id"/>
|
||||
<field name="currency_symbol" invisible="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="date_to"/>
|
||||
<label for="limit_usage"/>
|
||||
<span>
|
||||
<field name="limit_usage" class="oe_inline"/>
|
||||
<span attrs="{'invisible': [('limit_usage', '=', False)]}"> to <field name="max_usage" class="oe_inline"/> usages</span>
|
||||
</span>
|
||||
<label for="portal_visible" string="Show points"/>
|
||||
<span>
|
||||
<field name="portal_visible" class="oe_inline"/>
|
||||
<span attrs="{'invisible': [('portal_visible', '=', False)]}"> as <field name="portal_point_name" class="oe_inline"/></span>
|
||||
</span>
|
||||
<field name="applies_on" widget="radio" groups="base.group_no_one"/>
|
||||
<field name="trigger" widget="radio" groups="base.group_no_one" attrs="{'invisible': [('applies_on', '!=', 'current')]}"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<notebook>
|
||||
<page string="Triggers & Rewards" name="rules_rewards">
|
||||
<group>
|
||||
<group>
|
||||
<field name="rule_ids" colspan="2" mode="kanban" nolabel="1" add-label="Add a trigger"
|
||||
class="o_loyalty_kanban_inline" widget="loyalty_one2many" context="{'currency_symbol': currency_symbol}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="reward_ids" colspan="2" mode="kanban" nolabel="1" add-label="Add a reward"
|
||||
class="o_loyalty_kanban_inline" widget="loyalty_one2many" context="{'currency_symbol': currency_symbol}"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Communications" name="communications" attrs="{'invisible': [('applies_on', '=', 'current')]}">
|
||||
<field name="communication_plan_ids" mode="tree"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="loyalty_program_view_tree" model="ir.ui.view">
|
||||
<field name="name">loyalty.program.view.tree</field>
|
||||
<field name="model">loyalty.program</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree js_class="loyalty_program_list_view">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="program_type"/>
|
||||
<field name="active"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="loyalty_program_view_search" model="ir.ui.view">
|
||||
<field name="name">loyalty.program.view.search</field>
|
||||
<field name="model">loyalty.program</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="loyalty_program_action" model="ir.actions.act_window">
|
||||
<field name="name">Promotions, Gift Card, Loyalty</field>
|
||||
<field name="res_model">loyalty.program</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<div class="o_loyalty_not_found container">
|
||||
<h1>No loyalty program found.</h1>
|
||||
<p class="lead font-weight-light">Create a new one from scratch, or use one of the templates below.</p>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,110 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="loyalty_reward_view_form" model="ir.ui.view">
|
||||
<field name="name">loyalty.reward.view.form</field>
|
||||
<field name="model">loyalty.reward</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="reward_type" widget="radio"/>
|
||||
</group>
|
||||
<group id="discount_group" attrs="{'invisible': [('reward_type', '!=', 'discount')]}">
|
||||
<label for="discount"/>
|
||||
<div class="o_row">
|
||||
<field name="discount" class="oe_edit_only col-2 oe_inline text-center pr-2"/>
|
||||
<field name="discount_mode" class="oe_edit_only col-4 oe_inline pr-2"/>
|
||||
<field name="discount" class="oe_read_only"/>
|
||||
<field name="discount_mode" class="oe_read_only"/>
|
||||
<span>on</span>
|
||||
</div>
|
||||
<!-- Align right -->
|
||||
<field name="all_discount_product_ids" invisible="1"/>
|
||||
<label for="discount_applicability" string=""/>
|
||||
<field name="discount_applicability" nolabel="1" widget="radio"/>
|
||||
<field name="discount_product_domain" groups="base.group_no_one" widget="domain" options="{'model': 'product.product', 'in_dialog': true}"/>
|
||||
<field name="discount_product_ids" widget="many2many_tags"
|
||||
attrs="{'invisible': [('discount_applicability', '!=', 'specific')]}"/>
|
||||
<field name="discount_product_category_id" attrs="{'invisible': [('discount_applicability', '!=', 'specific')]}"/>
|
||||
<field name="discount_product_tag_id" attrs="{'invisible': [('discount_applicability', '!=', 'specific')]}"/>
|
||||
<field name="discount_max_amount"/>
|
||||
</group>
|
||||
<group id="product_group" attrs="{'invisible': [('reward_type', '!=', 'product')]}">
|
||||
<field name="multi_product" invisible="1"/>
|
||||
<field name="reward_product_ids" invisible="1"/>
|
||||
<field name="reward_product_id" attrs="{'required': [('reward_type', '=', 'product'), ('reward_product_ids', '=', [])]}"/>
|
||||
<field name="reward_product_tag_id" attrs="{'required': [('reward_type', '=', 'product'), ('reward_product_ids', '=', [])]}"/>
|
||||
<label for="reward_product_qty"/>
|
||||
<div class="row m-0">
|
||||
<field name="reward_product_qty" class="oe_inline"/>
|
||||
<field name="reward_product_uom_id" class="oe_inline" attrs="{'invisible': [('multi_product', '=', True)]}"/>
|
||||
</div>
|
||||
</group>
|
||||
<group>
|
||||
<field name="required_points"/>
|
||||
<field name="clear_wallet"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="description"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="loyalty_reward_view_kanban" model="ir.ui.view">
|
||||
<field name="name">loyalty.reward.view.kanban</field>
|
||||
<field name="model">loyalty.reward</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban>
|
||||
<field name="program_id"/>
|
||||
<field name="company_id"/>
|
||||
<field name="currency_id"/>
|
||||
<field name="description"/>
|
||||
<field name="reward_type"/>
|
||||
<field name="discount"/>
|
||||
<field name="discount_mode"/>
|
||||
<field name="discount_applicability"/>
|
||||
<field name="discount_product_domain"/>
|
||||
<field name="discount_max_amount"/>
|
||||
<field name="discount_line_product_id"/>
|
||||
<field name="reward_product_id"/>
|
||||
<field name="reward_product_tag_id"/>
|
||||
<field name="multi_product"/>
|
||||
<field name="reward_product_qty"/>
|
||||
<field name="reward_product_uom_id"/>
|
||||
<field name="required_points"/>
|
||||
<field name="point_name"/>
|
||||
<field name="clear_wallet"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click_edit mx-0">
|
||||
<field name="description"/>
|
||||
<t t-if="record.reward_type.raw_value === 'discount' && record.discount_applicability.raw_value === 'specific'">
|
||||
<t t-if="record.discount_product_domain.raw_value && record.discount_product_domain.raw_value !== '[]'" groups="base.group_no_one">
|
||||
<a><i class="fa fa-search fa-fw" title="Product Domain"/> <field name="discount_product_domain"/></a>
|
||||
</t>
|
||||
<t t-if="record.discount_product_ids.raw_value.length > 0">
|
||||
<div class="d-flex"><i class="fa fa-cube fa-fw" title="Products"/> <field name="discount_product_ids" widget="many2many_tags" class="d-inline"/></div>
|
||||
</t>
|
||||
<t t-if="record.discount_product_category_id.raw_value">
|
||||
<a><i class="fa fa-cubes fa-fw" title="Product Categories"/> <field name="discount_product_category_id"/></a>
|
||||
<br/>
|
||||
</t>
|
||||
<t t-if="record.discount_product_tag_id.raw_value">
|
||||
<a><i class="fa fa-tags fa-fw" title="Product Tags"/> <field name="discount_product_tag_id"/></a>
|
||||
<br/>
|
||||
</t>
|
||||
</t>
|
||||
<p class="m-0">With <field name="required_points"/> <field name="point_name"/><t t-if="record.clear_wallet.raw_value"> (or more)</t></p>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,110 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="loyalty_rule_view_form" model="ir.ui.view">
|
||||
<field name="name">loyalty.rule.view.form</field>
|
||||
<field name="model">loyalty.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<form class="loyalty-rule-form">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="product_domain" groups="base.group_no_one" widget="domain" options="{'model': 'product.product', 'in_dialog': true}"/>
|
||||
<field name="product_ids" widget="many2many_tags"/>
|
||||
<field name="product_category_id"/>
|
||||
<field name="product_tag_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<label for="reward_point_amount"/>
|
||||
<span>
|
||||
<field name="reward_point_amount" class="oe_inline"/>
|
||||
<field name="reward_point_name" class="oe_inline"/>
|
||||
</span>
|
||||
<label for="reward_point_mode" string=""/>
|
||||
<field name="reward_point_mode" widget="radio" nolabel="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="minimum_qty"/>
|
||||
<label for="minimum_amount"/>
|
||||
<div class="o_row">
|
||||
<field name="minimum_amount" class="oe_inline pr-2"/>
|
||||
<span>tax</span>
|
||||
<field name="minimum_amount_tax_mode" class="oe_inline"/>
|
||||
</div>
|
||||
</group>
|
||||
<group>
|
||||
<field name="mode" widget="radio"/>
|
||||
<field name="code"
|
||||
attrs="{'invisible': [('mode', '!=', 'with_code')],
|
||||
'required': [('mode', '=', 'with_code')]}"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="loyalty_rule_view_kanban" model="ir.ui.view">
|
||||
<field name="name">loyalty.rule.view.kanban</field>
|
||||
<field name="model">loyalty.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban>
|
||||
<field name="program_id"/>
|
||||
<field name="company_id"/>
|
||||
<field name="currency_id"/>
|
||||
<field name="product_domain"/>
|
||||
<field name="product_ids"/>
|
||||
<field name="product_category_id"/>
|
||||
<field name="product_tag_id"/>
|
||||
<field name="reward_point_amount"/>
|
||||
<field name="reward_point_split"/>
|
||||
<field name="reward_point_name"/>
|
||||
<field name="reward_point_mode"/>
|
||||
<field name="minimum_qty"/>
|
||||
<field name="minimum_amount"/>
|
||||
<field name="minimum_amount_tax_mode"/>
|
||||
<field name="mode"/>
|
||||
<field name="code"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click_edit mx-0">
|
||||
<t t-if="record.product_domain.raw_value && record.product_domain.raw_value !== '[]'" groups="base.group_no_one">
|
||||
<a><i class="fa fa-search fa-fw" title="Product Domain"/> <field name="product_domain"/></a>
|
||||
</t>
|
||||
<t t-if="record.product_ids.raw_value.length > 0">
|
||||
<div class="d-flex"><i class="fa fa-cube fa-fw" title="Products"/> <field name="product_ids" widget="many2many_tags" class="d-inline"/></div>
|
||||
</t>
|
||||
<t t-if="record.product_category_id.raw_value">
|
||||
<a><i class="fa fa-cubes fa-fw" title="Product Categories"/> <field name="product_category_id"/></a>
|
||||
<br/>
|
||||
</t>
|
||||
<t t-if="record.product_tag_id.raw_value">
|
||||
<a><i class="fa fa-tags fa-fw" title="Product Tags"/> <field name="product_tag_id"/></a>
|
||||
<br/>
|
||||
</t>
|
||||
<t t-if="record.product_ids.raw_value.length === 0 && !record.product_category_id.raw_value && !record.product_tag_id.raw_value">
|
||||
<a><i class="fa fa-cube fa-fw" title="Products"/> All Products</a>
|
||||
<br/>
|
||||
</t>
|
||||
<a>
|
||||
<t t-if="record.minimum_qty.raw_value > 1 || record.minimum_amount.raw_value">
|
||||
<t t-set="qty" t-value="record.minimum_qty.raw_value"/>
|
||||
<t t-set="amt" t-value="record.minimum_amount.raw_value"/>
|
||||
<t t-set="tax_mode" t-value="record.minimum_amount_tax_mode.raw_value"/>
|
||||
<span>If<t t-if="qty > 1"> minimum <field name="minimum_qty"/> PCE</t>
|
||||
<t t-if="amt"><t t-if="qty > 1">and</t> minimum <field name="minimum_amount"/><t t-if="tax_mode === 'excl'"> (tax excluded)</t></t>
|
||||
</span>
|
||||
<br/>
|
||||
</t>
|
||||
</a>
|
||||
<t t-if="record.code.raw_value">
|
||||
<a><i class="fa fa-pencil-square-o fa-fw" title="Code"/>With Code: <field name="code"/></a>
|
||||
<br/>
|
||||
</t>
|
||||
<i class="fa fa-gift fa-fw" title="conditions"/> <span>Reward <field name="reward_point_amount"/> <field name="reward_point_name"/> <field name="reward_point_mode"/></span>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -1,4 +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 loyalty_generate_wizard
|
||||
@@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.osv import expression
|
||||
|
||||
class LoyaltyGenerateWizard(models.TransientModel):
|
||||
_name = 'loyalty.generate.wizard'
|
||||
_description = 'Generate Coupons'
|
||||
|
||||
program_id = fields.Many2one('loyalty.program', required=True, default=lambda self: self.env.context.get('active_id', False))
|
||||
program_type = fields.Selection(related='program_id.program_type')
|
||||
|
||||
mode = fields.Selection([
|
||||
('anonymous', 'Anonymous Customers'),
|
||||
('selected', 'Selected Customers')],
|
||||
string='For', required=True, default='anonymous'
|
||||
)
|
||||
|
||||
customer_ids = fields.Many2many('res.partner', string='Customers')
|
||||
customer_tag_ids = fields.Many2many('res.partner.category', string='Customer Tags')
|
||||
|
||||
coupon_qty = fields.Integer('Quantity',
|
||||
compute='_compute_coupon_qty', readonly=False, store=True)
|
||||
points_granted = fields.Float('Grant', required=True, default=1)
|
||||
points_name = fields.Char(related='program_id.portal_point_name', readonly=True)
|
||||
valid_until = fields.Date()
|
||||
|
||||
def _get_partners(self):
|
||||
self.ensure_one()
|
||||
if self.mode != 'selected':
|
||||
return self.env['res.partner']
|
||||
domain = []
|
||||
if self.customer_ids:
|
||||
domain = [('id', 'in', self.customer_ids.ids)]
|
||||
if self.customer_tag_ids:
|
||||
domain = expression.OR([domain, [('category_id', 'in', self.customer_tag_ids.ids)]])
|
||||
return self.env['res.partner'].search(domain)
|
||||
|
||||
@api.depends('customer_ids', 'customer_tag_ids', 'mode')
|
||||
def _compute_coupon_qty(self):
|
||||
for wizard in self:
|
||||
if wizard.mode == 'selected':
|
||||
wizard.coupon_qty = len(wizard._get_partners())
|
||||
else:
|
||||
wizard.coupon_qty = wizard.coupon_qty or 0
|
||||
|
||||
def _get_coupon_values(self, partner):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'program_id': self.program_id.id,
|
||||
'points': self.points_granted,
|
||||
'expiration_date': self.valid_until,
|
||||
'partner_id': partner.id if self.mode == 'selected' else False,
|
||||
}
|
||||
|
||||
def generate_coupons(self):
|
||||
if any(not wizard.program_id for wizard in self):
|
||||
raise ValidationError(_("Can not generate coupon, no program is set."))
|
||||
if any(wizard.coupon_qty <= 0 for wizard in self):
|
||||
raise ValidationError(_("Invalid quantity."))
|
||||
coupon_create_vals = []
|
||||
for wizard in self:
|
||||
customers = wizard._get_partners() or range(wizard.coupon_qty)
|
||||
for partner in customers:
|
||||
coupon_create_vals.append(wizard._get_coupon_values(partner))
|
||||
self.env['loyalty.card'].create(coupon_create_vals)
|
||||
return True
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="loyalty_generate_wizard_view_form" model="ir.ui.view">
|
||||
<field name="name">loyalty.generate.wizard.view.form</field>
|
||||
<field name="model">loyalty.generate.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Generate Coupons">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="program_id" invisible="1"/>
|
||||
<field name="program_type" invisible="1"/>
|
||||
<field name="mode" widget="radio" attrs="{'invisible': [('program_type', '=', 'ewallet')]}"/>
|
||||
<field name="customer_ids" widget="many2many_tags_avatar" attrs="{'invisible': [('mode', '=', 'anonymous')]}"/>
|
||||
<field name="customer_tag_ids" widget="many2many_tags" attrs="{'invisible': [('mode', '=', 'anonymous')]}" options="{'color_field': 'color'}"/>
|
||||
<separator/>
|
||||
<field name="coupon_qty" attrs="{'readonly': [('mode', '=', 'selected')], 'required': [('mode', '=', 'anonymous')]}"/>
|
||||
<label for="points_granted"/>
|
||||
<span>
|
||||
<field name="points_granted" class="oe_inline"/>
|
||||
<field name="points_name" class="oe_inline"/>
|
||||
</span>
|
||||
<field name="valid_until"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="generate_coupons" type="object" string="Generate Coupons" class="btn-primary" data-hotkey="q"/>
|
||||
<button special="cancel" data-hotkey="z" string="Cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="loyalty_generate_wizard_action" model="ir.actions.act_window">
|
||||
<field name="name">Generate Coupons</field>
|
||||
<field name="res_model">loyalty.generate.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
{
|
||||
'name': 'Coupons & Loyalty - Delivery',
|
||||
'summary': "Add a free shipping option to your rewards",
|
||||
'category': 'Sales',
|
||||
'version': '1.0',
|
||||
'depends': ['loyalty', 'delivery'],
|
||||
'data': [
|
||||
'data/loyalty_delivery_data.xml',
|
||||
'views/loyalty_reward_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
},
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="loyalty.gift_card_product_50" model="product.product">
|
||||
<field name="tracking">none</field>
|
||||
</record>
|
||||
<record id="loyalty.ewallet_product_50" model="product.product">
|
||||
<field name="tracking">none</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import gift_card
|
||||
from . import product
|
||||
from . import loyalty_program
|
||||
from . import loyalty_reward
|
||||
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, models
|
||||
|
||||
class LoyaltyProgram(models.Model):
|
||||
_inherit = 'loyalty.program'
|
||||
|
||||
@api.model
|
||||
def _program_type_default_values(self):
|
||||
res = super()._program_type_default_values()
|
||||
# Add a loyalty reward for free shipping
|
||||
res['loyalty']['reward_ids'].append((0, 0, {
|
||||
'reward_type': 'shipping',
|
||||
'required_points': 100,
|
||||
}))
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def get_program_templates(self):
|
||||
# Override 'promotion' template to say free shipping
|
||||
res = super().get_program_templates()
|
||||
res['promotion']['description'] = _("Automatic promotion: free shipping on orders higher than $50")
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _get_template_values(self):
|
||||
res = super()._get_template_values()
|
||||
res['promotion']['reward_ids'] = [(5, 0, 0), (0, 0, {
|
||||
'reward_type': 'shipping',
|
||||
})]
|
||||
return res
|
||||
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, fields, models
|
||||
|
||||
class LoyaltyReward(models.Model):
|
||||
_inherit = 'loyalty.reward'
|
||||
|
||||
reward_type = fields.Selection(
|
||||
selection_add=[('shipping', 'Free Shipping')],
|
||||
ondelete={'shipping': 'set default'})
|
||||
|
||||
def _compute_description(self):
|
||||
shipping_rewards = self.filtered(lambda r: r.reward_type == 'shipping')
|
||||
super(LoyaltyReward, self - shipping_rewards)._compute_description()
|
||||
shipping_rewards.description = _('Free shipping')
|
||||
for reward in shipping_rewards:
|
||||
if reward.discount_max_amount:
|
||||
format_string = '%(amount)g %(symbol)s'
|
||||
if reward.currency_id.position == 'before':
|
||||
format_string = '%(symbol)s %(amount)g'
|
||||
formatted_amount = format_string % {'amount': reward.discount_max_amount, 'symbol': reward.currency_id.symbol}
|
||||
reward.description += _(' (Max %s)', formatted_amount)
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="loyalty_reward_view_form_inherit_loyalty_delivery" model="ir.ui.view">
|
||||
<field name="name">loyalty.reward.view.form.inherit.loyalty.delivery</field>
|
||||
<field name="model">loyalty.reward</field>
|
||||
<field name="inherit_id" ref="loyalty.loyalty_reward_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@id='product_group']" position="after">
|
||||
<group id="shipping" attrs="{'invisible': [('reward_type', '!=', 'shipping')]}">
|
||||
<field name="discount_max_amount"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -11,9 +11,7 @@ class ResConfigSettings(models.TransientModel):
|
||||
module_pos_adyen = fields.Boolean(string="Adyen Payment Terminal", help="The transactions are processed by Adyen. Set your Adyen credentials on the related payment method.")
|
||||
module_pos_six = fields.Boolean(string="Six Payment Terminal", help="The transactions are processed by Six. Set the IP address of the terminal on the related payment method.")
|
||||
update_stock_quantities = fields.Selection(related="company_id.point_of_sale_update_stock_quantities", readonly=False)
|
||||
module_pos_coupon = fields.Boolean("Coupon and Promotion Programs", help="Allow the use of coupon and promotion programs in PoS.")
|
||||
account_default_pos_receivable_account_id = fields.Many2one(string='Default Account Receivable (PoS)', related='company_id.account_default_pos_receivable_account_id', readonly=False)
|
||||
module_pos_gift_card = fields.Boolean("Gift Cards", help="Allow the use of gift card")
|
||||
|
||||
def set_values(self):
|
||||
super(ResConfigSettings, self).set_values()
|
||||
|
||||
@@ -66,28 +66,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box" title="Manage promotion and coupon programs.">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="module_pos_coupon"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane" id="pos-coupon">
|
||||
<label for="module_pos_coupon" string="Coupons & Promotions"/>
|
||||
<div class="text-muted">
|
||||
Manage promotion & coupon programs
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box" title="Manage gift card.">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="module_pos_gift_card"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane" id="pos-gift-card">
|
||||
<label for="module_pos_gift_card" string="Gift card"/>
|
||||
<div class="text-muted">
|
||||
Manage gift card
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Payments</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
"name": "Point of Sale Coupons",
|
||||
"version": "1.0",
|
||||
"category": "Sales/Point Of Sale",
|
||||
"sequence": 6,
|
||||
"summary": "Use coupons in Point of Sale",
|
||||
"description": "",
|
||||
"depends": ["coupon", "point_of_sale"],
|
||||
"data": [
|
||||
"data/mail_template_data.xml",
|
||||
'data/default_barcode_patterns.xml',
|
||||
"security/ir.model.access.csv",
|
||||
"views/coupon_views.xml",
|
||||
"views/coupon_program_views.xml",
|
||||
"views/pos_config_views.xml",
|
||||
"views/res_config_settings_views.xml",
|
||||
],
|
||||
"demo": [
|
||||
"demo/pos_coupon_demo.xml",
|
||||
],
|
||||
"installable": True,
|
||||
'assets': {
|
||||
'point_of_sale.assets': [
|
||||
'pos_coupon/static/src/css/coupon.css',
|
||||
'pos_coupon/static/src/js/coupon.js',
|
||||
'pos_coupon/static/src/js/Orderline.js',
|
||||
'pos_coupon/static/src/js/PaymentScreen.js',
|
||||
'pos_coupon/static/src/js/ProductScreen.js',
|
||||
'pos_coupon/static/src/js/OrderWidget.js',
|
||||
'pos_coupon/static/src/js/ControlButtons/PromoCodeButton.js',
|
||||
'pos_coupon/static/src/js/ControlButtons/ResetProgramsButton.js',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'pos_coupon/static/src/js/tours/**/*',
|
||||
],
|
||||
'web.assets_qweb': [
|
||||
'pos_coupon/static/src/xml/**/*',
|
||||
],
|
||||
},
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="pos_coupon.mail_coupon_template" model="mail.template">
|
||||
<field name="name">[POS] Coupon: Send by Email</field>
|
||||
<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 }}</field>
|
||||
<field name="partner_to">{{ object.source_pos_order_id.partner_id.id or object.partner_id.id }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="width:100%; margin:0px auto;"><tbody>
|
||||
<tr><td valign="top" style="text-align: center; font-size: 14px;">
|
||||
<t t-if="object.partner_id.name">
|
||||
Congratulations <t t-out="object.partner_id.name or ''">Brandon Freeman</t>,<br />
|
||||
</t>
|
||||
|
||||
Here is your reward from <t t-out="object.program_id.company_id.name or ''">YourCompany</t>.<br />
|
||||
|
||||
<t t-if="object.program_id.reward_type == 'discount'">
|
||||
<t t-if="object.program_id.discount_type == 'fixed_amount'">
|
||||
<span style="font-size: 50px; color: #875A7B; font-weight: bold;" t-out="'%s' % format_amount(object.program_id.discount_fixed_amount, object.program_id.currency_id) or ''">$ 10.0</span><br />
|
||||
<strong style="font-size: 24px;">off on your next order</strong><br />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span style="font-size: 50px; color: #875A7B; font-weight: bold;">
|
||||
<t t-out="object.program_id.discount_percentage or ''">10.0</t> %
|
||||
</span>
|
||||
<t t-if="object.program_id.discount_apply_on == 'specific_products'">
|
||||
<br />
|
||||
<t t-if="len(object.program_id.discount_specific_product_ids) != 1">
|
||||
<t t-set="display_specific_products" t-value="True"/>
|
||||
<strong style="font-size: 24px;">
|
||||
on some products*
|
||||
</strong>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<strong style="font-size: 24px;" t-out="'on %s' % object.program_id.discount_specific_product_ids.name or ''">Chair floor protection</strong>
|
||||
</t>
|
||||
</t>
|
||||
<t t-elif="object.program_id.discount_apply_on == 'cheapest_product'">
|
||||
<br /><strong style="font-size: 24px;">
|
||||
off on the cheapest product
|
||||
</strong>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<br /><strong style="font-size: 24px;">
|
||||
off on your next order
|
||||
</strong>
|
||||
</t>
|
||||
<br />
|
||||
</t>
|
||||
</t>
|
||||
<t t-elif="object.program_id.reward_type == 'product'">
|
||||
<span style="font-size: 36px; color: #875A7B; font-weight: bold;" t-out="'get %s free %s' % (object.program_id.reward_product_quantity, object.program_id.reward_product_id.name) or ''">Chair floor protection</span><br />
|
||||
<strong style="font-size: 24px;">on your next order</strong><br />
|
||||
</t>
|
||||
<t t-elif="object.program_id.reward_type == 'free_shipping'">
|
||||
<span style="font-size: 36px; color: #875A7B; font-weight: bold;">
|
||||
get free shipping
|
||||
</span><br />
|
||||
<strong style="font-size: 24px;">on your next order</strong><br />
|
||||
</t>
|
||||
</td></tr>
|
||||
<tr style="margin-top: 16px"><td valign="top" style="text-align: center; font-size: 14px;">
|
||||
Use this promo code
|
||||
<t t-if="object.expiration_date">
|
||||
before <t t-out="object.expiration_date or ''">2021-06-05</t>
|
||||
</t>
|
||||
<p style="margin-top: 16px;">
|
||||
<strong style="padding: 16px 8px 16px 8px; border-radius: 3px; background-color: #F1F1F1;" t-out="object.code or ''">13996301932606901095</strong>
|
||||
</p>
|
||||
<t t-if="object.program_id.rule_min_quantity not in [0, 1]">
|
||||
<span style="font-size: 14px;">
|
||||
Minimum purchase of <t t-out="object.program_id.rule_min_quantity or ''">10</t> products
|
||||
</span><br />
|
||||
</t>
|
||||
<t t-if="object.program_id.rule_minimum_amount != 0.00">
|
||||
<span style="font-size: 14px;">
|
||||
Valid for purchase above <t t-out="object.program_id.company_id.currency_id.symbol or ''">$</t><t t-out="'%0.2f' % float(object.program_id.rule_minimum_amount) or ''">10.00</t>
|
||||
</span><br />
|
||||
</t>
|
||||
<t t-if="display_specific_products">
|
||||
<span>
|
||||
*Valid for following products: <t t-out="', '.join(object.program_id.discount_specific_product_ids.mapped('name')) or ''">Office Chair Black</t>
|
||||
</span><br />
|
||||
</t>
|
||||
<br/>
|
||||
Thank you,
|
||||
<t t-if="object.source_pos_order_id.user_id.signature">
|
||||
<br />
|
||||
<t t-out="object.source_pos_order_id.user_id.signature or ''">--<br/>Mitchell Admin</t>
|
||||
</t>
|
||||
</td></tr>
|
||||
</tbody></table>
|
||||
</field>
|
||||
<field name="report_template" ref="coupon.report_coupon_code"/>
|
||||
<field name="report_name">Your Coupon Code</field>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,41 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="15_pc_on_next_order" model="coupon.program">
|
||||
<field name="name">15% on next order</field>
|
||||
<field name="program_type">promotion_program</field>
|
||||
<field name="promo_applicability">on_next_order</field>
|
||||
<field name="promo_code_usage">no_code_needed</field>
|
||||
<field name="rule_minimum_amount">100</field>
|
||||
<field name="discount_apply_on">on_order</field>
|
||||
<field name="discount_type">percentage</field>
|
||||
<field name="discount_percentage">15.0</field>
|
||||
<field name="validity_duration">0</field>
|
||||
</record>
|
||||
|
||||
<record id="point_of_sale.pos_config_main" model="pos.config">
|
||||
<field name="use_coupon_programs">True</field>
|
||||
<field name="promo_program_ids" eval="[(6,0,[ref('coupon.3_cabinets_plus_1_free'),ref('coupon.10_percent_auto_applied'),ref('pos_coupon.15_pc_on_next_order')])]"/>
|
||||
<field name="coupon_program_ids" eval="[(6,0,[ref('coupon.10_percent_coupon')])]"/>
|
||||
</record>
|
||||
|
||||
<function name="create" model="coupon.generate.wizard">
|
||||
<value model="coupon.generate.wizard" eval="dict(
|
||||
obj().default_get(list(obj().fields_get())),
|
||||
**{
|
||||
'nbr_coupons': 10,
|
||||
}
|
||||
)"/>
|
||||
</function>
|
||||
|
||||
<!-- Create 10 coupons for the 10% coupon program based on the created record above -->
|
||||
<function name="generate_coupon" model="coupon.generate.wizard">
|
||||
<value model="coupon.generate.wizard" eval="obj().search([('nbr_coupons', '=', 10)]).id"/>
|
||||
<value name="context" eval="{'active_id': ref('coupon.10_percent_coupon')}" />
|
||||
</function>
|
||||
|
||||
<function name="unlink" model="coupon.generate.wizard">
|
||||
<value model="coupon.generate.wizard" eval="obj().search([('nbr_coupons', '=', 10)]).id"/>
|
||||
</function>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,451 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * pos_coupon
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~15.2\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-02-11 14:34+0000\n"
|
||||
"PO-Revision-Date: 2022-02-11 14:34+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:mail.template,body_html:pos_coupon.mail_coupon_template
|
||||
msgid ""
|
||||
"<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"width:100%; margin:0px auto;\"><tbody>\n"
|
||||
" <tr><td valign=\"top\" style=\"text-align: center; font-size: 14px;\">\n"
|
||||
" <t t-if=\"object.partner_id.name\">\n"
|
||||
" Congratulations <t t-out=\"object.partner_id.name or ''\">Brandon Freeman</t>,<br/>\n"
|
||||
" </t>\n"
|
||||
"\n"
|
||||
" Here is your reward from <t t-out=\"object.program_id.company_id.name or ''\">YourCompany</t>.<br/>\n"
|
||||
"\n"
|
||||
" <t t-if=\"object.program_id.reward_type == 'discount'\">\n"
|
||||
" <t t-if=\"object.program_id.discount_type == 'fixed_amount'\">\n"
|
||||
" <span style=\"font-size: 50px; color: #875A7B; font-weight: bold;\" t-out=\"'%s' % format_amount(object.program_id.discount_fixed_amount, object.program_id.currency_id) or ''\">$ 10.0</span><br/>\n"
|
||||
" <strong style=\"font-size: 24px;\">off on your next order</strong><br/>\n"
|
||||
" </t>\n"
|
||||
" <t t-else=\"\">\n"
|
||||
" <span style=\"font-size: 50px; color: #875A7B; font-weight: bold;\">\n"
|
||||
" <t t-out=\"object.program_id.discount_percentage or ''\">10.0</t> %\n"
|
||||
" </span>\n"
|
||||
" <t t-if=\"object.program_id.discount_apply_on == 'specific_products'\">\n"
|
||||
" <br/>\n"
|
||||
" <t t-if=\"len(object.program_id.discount_specific_product_ids) != 1\">\n"
|
||||
" <t t-set=\"display_specific_products\" t-value=\"True\"/>\n"
|
||||
" <strong style=\"font-size: 24px;\">\n"
|
||||
" on some products*\n"
|
||||
" </strong>\n"
|
||||
" </t>\n"
|
||||
" <t t-else=\"\">\n"
|
||||
" <strong style=\"font-size: 24px;\" t-out=\"'on %s' % object.program_id.discount_specific_product_ids.name or ''\">Chair floor protection</strong>\n"
|
||||
" </t>\n"
|
||||
" </t>\n"
|
||||
" <t t-elif=\"object.program_id.discount_apply_on == 'cheapest_product'\">\n"
|
||||
" <br/><strong style=\"font-size: 24px;\">\n"
|
||||
" off on the cheapest product\n"
|
||||
" </strong>\n"
|
||||
" </t>\n"
|
||||
" <t t-else=\"\">\n"
|
||||
" <br/><strong style=\"font-size: 24px;\">\n"
|
||||
" off on your next order\n"
|
||||
" </strong>\n"
|
||||
" </t>\n"
|
||||
" <br/>\n"
|
||||
" </t>\n"
|
||||
" </t>\n"
|
||||
" <t t-elif=\"object.program_id.reward_type == 'product'\">\n"
|
||||
" <span style=\"font-size: 36px; color: #875A7B; font-weight: bold;\" t-out=\"'get %s free %s' % (object.program_id.reward_product_quantity, object.program_id.reward_product_id.name) or ''\">Chair floor protection</span><br/>\n"
|
||||
" <strong style=\"font-size: 24px;\">on your next order</strong><br/>\n"
|
||||
" </t>\n"
|
||||
" <t t-elif=\"object.program_id.reward_type == 'free_shipping'\">\n"
|
||||
" <span style=\"font-size: 36px; color: #875A7B; font-weight: bold;\">\n"
|
||||
" get free shipping\n"
|
||||
" </span><br/>\n"
|
||||
" <strong style=\"font-size: 24px;\">on your next order</strong><br/>\n"
|
||||
" </t>\n"
|
||||
" </td></tr>\n"
|
||||
" <tr style=\"margin-top: 16px\"><td valign=\"top\" style=\"text-align: center; font-size: 14px;\">\n"
|
||||
" Use this promo code\n"
|
||||
" <t t-if=\"object.expiration_date\">\n"
|
||||
" before <t t-out=\"object.expiration_date or ''\">2021-06-05</t>\n"
|
||||
" </t>\n"
|
||||
" <p style=\"margin-top: 16px;\">\n"
|
||||
" <strong style=\"padding: 16px 8px 16px 8px; border-radius: 3px; background-color: #F1F1F1;\" t-out=\"object.code or ''\">13996301932606901095</strong>\n"
|
||||
" </p>\n"
|
||||
" <t t-if=\"object.program_id.rule_min_quantity not in [0, 1]\">\n"
|
||||
" <span style=\"font-size: 14px;\">\n"
|
||||
" Minimum purchase of <t t-out=\"object.program_id.rule_min_quantity or ''\">10</t> products\n"
|
||||
" </span><br/>\n"
|
||||
" </t>\n"
|
||||
" <t t-if=\"object.program_id.rule_minimum_amount != 0.00\">\n"
|
||||
" <span style=\"font-size: 14px;\">\n"
|
||||
" Valid for purchase above <t t-out=\"object.program_id.company_id.currency_id.symbol or ''\">$</t><t t-out=\"'%0.2f' % float(object.program_id.rule_minimum_amount) or ''\">10.00</t>\n"
|
||||
" </span><br/>\n"
|
||||
" </t>\n"
|
||||
" <t t-if=\"display_specific_products\">\n"
|
||||
" <span>\n"
|
||||
" *Valid for following products: <t t-out=\"', '.join(object.program_id.discount_specific_product_ids.mapped('name')) or ''\">Office Chair Black</t>\n"
|
||||
" </span><br/>\n"
|
||||
" </t>\n"
|
||||
" <br/>\n"
|
||||
" Thank you,\n"
|
||||
" <t t-if=\"object.source_pos_order_id.user_id.signature\">\n"
|
||||
" <br/>\n"
|
||||
" <t t-out=\"object.source_pos_order_id.user_id.signature or ''\">--<br/>Mitchell Admin</t>\n"
|
||||
" </t>\n"
|
||||
" </td></tr>\n"
|
||||
"</tbody></table>\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: code:addons/pos_coupon/models/coupon.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"A coupon from the same program has already been reserved for this order."
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,help:pos_coupon.field_coupon_program__promo_barcode
|
||||
msgid ""
|
||||
"A technical field used as an alternative to the promo_code. This is "
|
||||
"automatically generated when promo_code is changed."
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#. openerp-web
|
||||
#: code:addons/pos_coupon/static/src/xml/ActivePrograms.xml:0
|
||||
#, python-format
|
||||
msgid "Active Programs"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,field_description:pos_coupon.field_pos_order__applied_program_ids
|
||||
msgid "Applied Programs"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,field_description:pos_coupon.field_coupon_coupon__pos_order_id
|
||||
msgid "Applied on PoS Order"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#. openerp-web
|
||||
#: code:addons/pos_coupon/static/src/js/ProductScreen.js:0
|
||||
#, python-format
|
||||
msgid "Are you sure you want to deactivate %s in this order?"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#. openerp-web
|
||||
#: code:addons/pos_coupon/static/src/xml/OrderReceipt.xml:0
|
||||
#: model:ir.model.fields,field_description:pos_coupon.field_coupon_program__promo_barcode
|
||||
#, python-format
|
||||
msgid "Barcode"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model,name:pos_coupon.model_barcode_rule
|
||||
msgid "Barcode Rule"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,field_description:pos_coupon.field_pos_order__used_coupon_ids
|
||||
msgid "Consumed Coupons"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model,name:pos_coupon.model_coupon_coupon
|
||||
#: model:ir.model.fields,field_description:pos_coupon.field_pos_order_line__coupon_id
|
||||
#: model:ir.model.fields.selection,name:pos_coupon.selection__barcode_rule__type__coupon
|
||||
msgid "Coupon"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#. openerp-web
|
||||
#: code:addons/pos_coupon/static/src/xml/OrderReceipt.xml:0
|
||||
#, python-format
|
||||
msgid "Coupon Codes"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model,name:pos_coupon.model_coupon_program
|
||||
msgid "Coupon Program"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,field_description:pos_coupon.field_pos_config__coupon_program_ids
|
||||
#: model:ir.ui.menu,name:pos_coupon.menu_coupon_type_config
|
||||
#: model_terms:ir.ui.view,arch_db:pos_coupon.res_config_view_form_inherit_pos_coupon
|
||||
msgid "Coupon Programs"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,help:pos_coupon.field_pos_order_line__coupon_id
|
||||
msgid "Coupon that generated this reward."
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,field_description:pos_coupon.field_pos_config__use_coupon_programs
|
||||
msgid "Coupons & Promotions"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,field_description:pos_coupon.field_pos_config__program_ids
|
||||
msgid "Coupons and Promotions"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#. openerp-web
|
||||
#: code:addons/pos_coupon/static/src/js/ProductScreen.js:0
|
||||
#, python-format
|
||||
msgid "Deactivating program"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model_terms:ir.ui.view,arch_db:pos_coupon.pos_coupon_pos_config_view_form
|
||||
msgid "Define the coupon and promotion programs you can use in this PoS."
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#. openerp-web
|
||||
#: code:addons/pos_coupon/static/src/xml/ControlButtons/PromoCodeButton.xml:0
|
||||
#, python-format
|
||||
msgid "Enter Code"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#. openerp-web
|
||||
#: code:addons/pos_coupon/static/src/js/ControlButtons/PromoCodeButton.js:0
|
||||
#, python-format
|
||||
msgid "Enter Promotion or Coupon Code"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,help:pos_coupon.field_pos_order_line__is_program_reward
|
||||
msgid ""
|
||||
"Flag indicating that this order line is a result of coupon/promo program."
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,field_description:pos_coupon.field_pos_order__generated_coupon_ids
|
||||
msgid "Generated Coupons"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,field_description:pos_coupon.field_pos_order_line__is_program_reward
|
||||
msgid "Is reward line"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#. openerp-web
|
||||
#: code:addons/pos_coupon/static/src/js/ProductScreen.js:0
|
||||
#, python-format
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,help:pos_coupon.field_coupon_program__pos_order_line_ids
|
||||
msgid "Order lines where this program is applied."
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,field_description:pos_coupon.field_coupon_program__pos_order_count
|
||||
msgid "PoS Order Count"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,field_description:pos_coupon.field_coupon_program__pos_order_line_ids
|
||||
msgid "PoS Order Lines"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,field_description:pos_coupon.field_coupon_coupon__source_pos_order_id
|
||||
msgid "PoS Order Reference"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: code:addons/pos_coupon/models/coupon_program.py:0
|
||||
#, python-format
|
||||
msgid "PoS Orders"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model_terms:ir.ui.view,arch_db:pos_coupon.pos_coupon_program_view_coupon_program_form
|
||||
#: model_terms:ir.ui.view,arch_db:pos_coupon.pos_coupon_program_view_promo_program_form
|
||||
msgid "PoS Sales"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,help:pos_coupon.field_coupon_coupon__pos_order_id
|
||||
msgid "PoS order where this coupon is consumed/booked."
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,help:pos_coupon.field_coupon_coupon__source_pos_order_id
|
||||
msgid "PoS order where this coupon is generated."
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model,name:pos_coupon.model_pos_config
|
||||
msgid "Point of Sale Configuration"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model,name:pos_coupon.model_pos_order_line
|
||||
msgid "Point of Sale Order Lines"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model,name:pos_coupon.model_pos_order
|
||||
msgid "Point of Sale Orders"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model,name:pos_coupon.model_pos_session
|
||||
msgid "Point of Sale Session"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,field_description:pos_coupon.field_coupon_program__pos_config_ids
|
||||
msgid "Point of Sales"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,field_description:pos_coupon.field_coupon_program__pos_order_ids
|
||||
msgid "Pos Order"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,field_description:pos_coupon.field_pos_order_line__program_id
|
||||
msgid "Program"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: code:addons/pos_coupon/models/pos_config.py:0
|
||||
#, python-format
|
||||
msgid "Program: %(name)s (%(type)s), Reward Product: `%(reward_product)s`"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model_terms:ir.ui.view,arch_db:pos_coupon.pos_coupon_pos_config_view_form
|
||||
msgid "Promotion & coupon programs to use."
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,field_description:pos_coupon.field_pos_config__promo_program_ids
|
||||
#: model:ir.ui.menu,name:pos_coupon.menu_promotion_type_config
|
||||
#: model_terms:ir.ui.view,arch_db:pos_coupon.res_config_view_form_inherit_pos_coupon
|
||||
msgid "Promotion Programs"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,help:pos_coupon.field_pos_order_line__program_id
|
||||
msgid "Promotion/Coupon Program where this reward line is based."
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#. openerp-web
|
||||
#: code:addons/pos_coupon/static/src/xml/ControlButtons/ResetProgramsButton.xml:0
|
||||
#, python-format
|
||||
msgid "Reset Programs"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,help:pos_coupon.field_pos_order__applied_program_ids
|
||||
msgid ""
|
||||
"Technical field. This is set when the order is validated. We normally get "
|
||||
"this value thru the `program_id` of the reward lines."
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,help:pos_coupon.field_coupon_program__pos_order_ids
|
||||
msgid "The PoS orders where this program is applied."
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,help:pos_coupon.field_coupon_program__valid_partner_ids
|
||||
msgid "These are the partners that can avail this program."
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,help:pos_coupon.field_coupon_program__valid_product_ids
|
||||
msgid "These are the products that are valid in this program."
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: code:addons/pos_coupon/models/pos_config.py:0
|
||||
#, python-format
|
||||
msgid "This coupon is invalid (%s)."
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: code:addons/pos_coupon/models/pos_config.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"To continue, make the following reward products to be available in Point of "
|
||||
"Sale."
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,field_description:pos_coupon.field_barcode_rule__type
|
||||
msgid "Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,help:pos_coupon.field_pos_config__use_coupon_programs
|
||||
msgid "Use coupon and promotion programs in this PoS configuration."
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,field_description:pos_coupon.field_coupon_program__valid_partner_ids
|
||||
msgid "Valid Partner"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:ir.model.fields,field_description:pos_coupon.field_coupon_program__valid_product_ids
|
||||
msgid "Valid Product"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#. openerp-web
|
||||
#: code:addons/pos_coupon/static/src/xml/OrderReceipt.xml:0
|
||||
#, python-format
|
||||
msgid "Valid until:"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#. openerp-web
|
||||
#: code:addons/pos_coupon/static/src/js/ProductScreen.js:0
|
||||
#, python-format
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:mail.template,report_name:pos_coupon.mail_coupon_template
|
||||
msgid "Your Coupon Code"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:mail.template,subject:pos_coupon.mail_coupon_template
|
||||
msgid "Your reward coupon from {{ object.program_id.company_id.name }} "
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#: model:mail.template,name:pos_coupon.mail_coupon_template
|
||||
msgid "[POS] Coupon: Send by Email"
|
||||
msgstr ""
|
||||
|
||||
#. module: pos_coupon
|
||||
#. openerp-web
|
||||
#: code:addons/pos_coupon/static/src/xml/OrderReceipt.xml:0
|
||||
#, python-format
|
||||
msgid "no expiration"
|
||||
msgstr ""
|
||||
@@ -1,9 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import pos_config
|
||||
from . import pos_order
|
||||
from . import coupon
|
||||
from . import coupon_program
|
||||
from . import barcode_rule
|
||||
from . import pos_session
|
||||
@@ -1,15 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, fields
|
||||
from odoo.tools.translate import _
|
||||
|
||||
|
||||
class BarcodeRule(models.Model):
|
||||
_inherit = 'barcode.rule'
|
||||
|
||||
type = fields.Selection(selection_add=[
|
||||
('coupon', 'Coupon'),
|
||||
], ondelete={
|
||||
'coupon': 'set default',
|
||||
})
|
||||
@@ -1,42 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
# NOTE Use black to automatically format this code.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class Coupon(models.Model):
|
||||
_inherit = "coupon.coupon"
|
||||
|
||||
source_pos_order_id = fields.Many2one(
|
||||
"pos.order",
|
||||
string="PoS Order Reference",
|
||||
help="PoS order where this coupon is generated.",
|
||||
)
|
||||
pos_order_id = fields.Many2one(
|
||||
"pos.order",
|
||||
string="Applied on PoS Order",
|
||||
help="PoS order where this coupon is consumed/booked.",
|
||||
)
|
||||
|
||||
def _check_coupon_code(self, order_date, partner_id, **kwargs):
|
||||
if self.program_id.id in kwargs.get("reserved_program_ids", []):
|
||||
return {
|
||||
"error": _("A coupon from the same program has already been reserved for this order.")
|
||||
}
|
||||
return super()._check_coupon_code(order_date, partner_id, **kwargs)
|
||||
|
||||
def _get_default_template(self):
|
||||
if self.source_pos_order_id:
|
||||
return self.env.ref('pos_coupon.mail_coupon_template', False)
|
||||
return super()._get_default_template()
|
||||
|
||||
@api.model
|
||||
def _generate_code(self):
|
||||
"""
|
||||
Modify the generated barcode to be compatible with the default
|
||||
barcode rule in this module. See `data/default_barcode_patterns.xml`.
|
||||
"""
|
||||
code = super()._generate_code()
|
||||
return '043' + code[3:]
|
||||
@@ -1,86 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
# NOTE Use black to automatically format this code.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
import ast
|
||||
|
||||
class CouponProgram(models.Model):
|
||||
_inherit = "coupon.program"
|
||||
|
||||
pos_config_ids = fields.Many2many(
|
||||
"pos.config",
|
||||
string="Point of Sales",
|
||||
readonly=True,
|
||||
)
|
||||
pos_order_line_ids = fields.One2many(
|
||||
"pos.order.line",
|
||||
"program_id",
|
||||
string="PoS Order Lines",
|
||||
help="Order lines where this program is applied.",
|
||||
)
|
||||
promo_barcode = fields.Char(
|
||||
"Barcode",
|
||||
default=lambda self: self.env["coupon.coupon"]._generate_code(),
|
||||
help="A technical field used as an alternative to the promo_code. "
|
||||
"This is automatically generated when promo_code is changed.",
|
||||
)
|
||||
pos_order_ids = fields.Many2many(
|
||||
"pos.order", help="The PoS orders where this program is applied.",
|
||||
)
|
||||
pos_order_count = fields.Integer(
|
||||
"PoS Order Count", compute="_compute_pos_order_count"
|
||||
)
|
||||
valid_product_ids = fields.Many2many(
|
||||
"product.product",
|
||||
"Valid Products",
|
||||
compute="_compute_valid_product_ids",
|
||||
help="These are the products that are valid in this program.",
|
||||
)
|
||||
valid_partner_ids = fields.Many2many(
|
||||
"res.partner",
|
||||
"Valid Partners",
|
||||
compute="_compute_valid_partner_ids",
|
||||
help="These are the partners that can avail this program.",
|
||||
)
|
||||
|
||||
@api.depends("pos_order_ids")
|
||||
def _compute_pos_order_count(self):
|
||||
for program in self:
|
||||
program.pos_order_count = len(program.pos_order_ids)
|
||||
|
||||
def write(self, vals):
|
||||
if "promo_code" in vals:
|
||||
vals.update({"promo_barcode": self.env["coupon.coupon"]._generate_code()})
|
||||
return super(CouponProgram, self).write(vals)
|
||||
|
||||
def action_view_pos_orders(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"name": _("PoS Orders"),
|
||||
"view_mode": "tree,form",
|
||||
"res_model": "pos.order",
|
||||
"type": "ir.actions.act_window",
|
||||
"domain": [("id", "in", self.pos_order_ids.ids)],
|
||||
"context": dict(self._context, create=False),
|
||||
}
|
||||
|
||||
@api.depends("rule_products_domain")
|
||||
def _compute_valid_product_ids(self):
|
||||
for program in self:
|
||||
domain = ast.literal_eval(program.rule_products_domain) if program.rule_products_domain else []
|
||||
program.valid_product_ids = self.env["product.product"].search(domain).ids
|
||||
|
||||
@api.depends("rule_partners_domain")
|
||||
def _compute_valid_partner_ids(self):
|
||||
for program in self:
|
||||
domain = ast.literal_eval(program.rule_partners_domain) if program.rule_partners_domain else []
|
||||
program.valid_partner_ids = self.env["res.partner"].search(domain).ids
|
||||
|
||||
@api.depends('pos_order_ids')
|
||||
def _compute_total_order_count(self):
|
||||
super(CouponProgram, self)._compute_total_order_count()
|
||||
for program in self:
|
||||
program.total_order_count += len(program.pos_order_ids)
|
||||
@@ -1,104 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
# NOTE Use black to automatically format this code.
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class PosConfig(models.Model):
|
||||
_inherit = "pos.config"
|
||||
|
||||
use_coupon_programs = fields.Boolean(
|
||||
"Coupons & Promotions",
|
||||
help="Use coupon and promotion programs in this PoS configuration.",
|
||||
)
|
||||
coupon_program_ids = fields.Many2many(
|
||||
"coupon.program",
|
||||
string="Coupon Programs",
|
||||
compute="_filter_programs",
|
||||
inverse="_set_programs",
|
||||
)
|
||||
promo_program_ids = fields.Many2many(
|
||||
"coupon.program",
|
||||
string="Promotion Programs",
|
||||
compute="_filter_programs",
|
||||
inverse="_set_programs",
|
||||
)
|
||||
program_ids = fields.Many2many("coupon.program", string="Coupons and Promotions")
|
||||
|
||||
@api.depends("program_ids")
|
||||
def _filter_programs(self):
|
||||
for config in self:
|
||||
config.coupon_program_ids = config.program_ids.filtered(
|
||||
lambda program: program.program_type == "coupon_program"
|
||||
)
|
||||
config.promo_program_ids = config.program_ids.filtered(
|
||||
lambda program: program.program_type == "promotion_program"
|
||||
)
|
||||
|
||||
def _set_programs(self):
|
||||
for config in self:
|
||||
config.program_ids = config.coupon_program_ids | config.promo_program_ids
|
||||
|
||||
def open_session_cb(self):
|
||||
# Check validity of programs before opening a new session
|
||||
invalid_reward_products_msg = ""
|
||||
for program in self.program_ids:
|
||||
if (
|
||||
program.reward_product_id
|
||||
and not program.reward_product_id.available_in_pos
|
||||
):
|
||||
reward_product = program.reward_product_id
|
||||
invalid_reward_products_msg += "\n\t"
|
||||
invalid_reward_products_msg += _(
|
||||
"Program: %(name)s (%(type)s), Reward Product: `%(reward_product)s`",
|
||||
name=program.name,
|
||||
type=program.program_type,
|
||||
reward_product=reward_product.name,
|
||||
)
|
||||
|
||||
if invalid_reward_products_msg:
|
||||
intro = _(
|
||||
"To continue, make the following reward products to be available in Point of Sale."
|
||||
)
|
||||
raise UserError(f"{intro}\n{invalid_reward_products_msg}")
|
||||
|
||||
return super(PosConfig, self).open_session_cb()
|
||||
|
||||
def use_coupon_code(self, code, creation_date, partner_id, reserved_program_ids):
|
||||
coupon_to_check = self.env["coupon.coupon"].search(
|
||||
[("code", "=", code), ("program_id", "in", self.program_ids.ids)]
|
||||
)
|
||||
# If not unique, we only check the first coupon.
|
||||
coupon_to_check = coupon_to_check[:1]
|
||||
if not coupon_to_check:
|
||||
return {
|
||||
"successful": False,
|
||||
"payload": {
|
||||
"error_message": _("This coupon is invalid (%s).") % (code)
|
||||
},
|
||||
}
|
||||
message = coupon_to_check._check_coupon_code(
|
||||
fields.Date.from_string(creation_date[:11]),
|
||||
partner_id,
|
||||
reserved_program_ids=reserved_program_ids,
|
||||
)
|
||||
error_message = message.get("error", False)
|
||||
if error_message:
|
||||
return {
|
||||
"successful": False,
|
||||
"payload": {"error_message": error_message},
|
||||
}
|
||||
|
||||
coupon_to_check.write({"state": "used"})
|
||||
return {
|
||||
"successful": True,
|
||||
"payload": {
|
||||
"program_id": coupon_to_check.program_id.id,
|
||||
"coupon_id": coupon_to_check.id,
|
||||
},
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
# NOTE Use black to automatically format this code.
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class PosOrder(models.Model):
|
||||
_inherit = "pos.order"
|
||||
|
||||
applied_program_ids = fields.Many2many(
|
||||
"coupon.program",
|
||||
string="Applied Programs",
|
||||
help="Technical field. This is set when the order is validated. "
|
||||
"We normally get this value thru the `program_id` of the reward lines.",
|
||||
)
|
||||
used_coupon_ids = fields.One2many(
|
||||
"coupon.coupon", "pos_order_id", string="Consumed Coupons"
|
||||
)
|
||||
generated_coupon_ids = fields.One2many(
|
||||
"coupon.coupon", "source_pos_order_id", string="Generated Coupons"
|
||||
)
|
||||
|
||||
def validate_coupon_programs(
|
||||
self, program_ids_to_generate_coupons, unused_coupon_ids
|
||||
):
|
||||
"""This is called after create_from_ui is called. We set here fields
|
||||
that are used to link programs and coupons to the order.
|
||||
|
||||
We also return the generated coupons that can be used in the frontend
|
||||
to print the generated codes in the receipt.
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
program_ids_to_generate_coupons = program_ids_to_generate_coupons or []
|
||||
unused_coupon_ids = unused_coupon_ids or []
|
||||
|
||||
self.env["coupon.coupon"].browse(unused_coupon_ids).write({"state": "new"})
|
||||
self.sudo().write(
|
||||
{
|
||||
"applied_program_ids": [(4, i) for i in self.lines.program_id.ids],
|
||||
"used_coupon_ids": [(4, i) for i in self.lines.coupon_id.ids],
|
||||
"generated_coupon_ids": [
|
||||
(4, i)
|
||||
for i in (
|
||||
self.env["coupon.program"]
|
||||
.browse(program_ids_to_generate_coupons)
|
||||
.sudo()._generate_coupons(self.partner_id.id)
|
||||
).ids
|
||||
],
|
||||
}
|
||||
)
|
||||
return [
|
||||
{
|
||||
"code": coupon.code,
|
||||
"expiration_date": coupon.expiration_date,
|
||||
"program_name": coupon.program_id.name,
|
||||
}
|
||||
for coupon in self.generated_coupon_ids
|
||||
]
|
||||
|
||||
def _get_fields_for_order_line(self):
|
||||
fields = super(PosOrder, self)._get_fields_for_order_line()
|
||||
fields.extend({
|
||||
'is_program_reward',
|
||||
'coupon_id',
|
||||
'program_id',
|
||||
})
|
||||
return fields
|
||||
|
||||
class PosOrderLine(models.Model):
|
||||
_inherit = "pos.order.line"
|
||||
|
||||
is_program_reward = fields.Boolean(
|
||||
"Is reward line",
|
||||
help="Flag indicating that this order line is a result of coupon/promo program.",
|
||||
)
|
||||
program_id = fields.Many2one(
|
||||
"coupon.program",
|
||||
string="Program",
|
||||
help="Promotion/Coupon Program where this reward line is based.",
|
||||
)
|
||||
coupon_id = fields.Many2one(
|
||||
"coupon.coupon", string="Coupon", help="Coupon that generated this reward.",
|
||||
)
|
||||
@@ -1,37 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
from odoo.osv.expression import OR
|
||||
|
||||
|
||||
class PosSession(models.Model):
|
||||
_inherit = 'pos.session'
|
||||
|
||||
def _pos_ui_models_to_load(self):
|
||||
result = super()._pos_ui_models_to_load()
|
||||
if self.config_id.use_coupon_programs and len(self.config_id.program_ids) > 0:
|
||||
new_model = 'coupon.program'
|
||||
if new_model not in result:
|
||||
result.append(new_model)
|
||||
return result
|
||||
|
||||
def _loader_params_coupon_program(self):
|
||||
return {
|
||||
'search_params': {
|
||||
'domain': [('id', 'in', self.config_id.program_ids.ids), ('active', '=', True)],
|
||||
'fields': []
|
||||
},
|
||||
}
|
||||
|
||||
def _get_pos_ui_coupon_program(self, params):
|
||||
return self.env['coupon.program'].search_read(**params['search_params'])
|
||||
|
||||
def _loader_params_product_product(self):
|
||||
result = super(PosSession, self)._loader_params_product_product()
|
||||
if self.config_id.use_coupon_programs and len(self.config_id.program_ids) > 0:
|
||||
discount_product_ids = self.config_id.program_ids.mapped(lambda program: program.discount_line_product_id.id)
|
||||
reward_product_ids = self.config_id.program_ids.mapped(lambda program: program.reward_product_id.id)
|
||||
product_ids = [id for id in [*discount_product_ids, *reward_product_ids] if id]
|
||||
result['search_params']['domain'] = OR([result['search_params']['domain'], [('id', 'in', product_ids)]])
|
||||
return result
|
||||
@@ -1,10 +0,0 @@
|
||||
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_program_pos_user,Coupon Program (PoS User),coupon.model_coupon_program,point_of_sale.group_pos_user,1,0,0,0
|
||||
access_program_pos_manager,Coupon Program (PoS Manager),coupon.model_coupon_program,point_of_sale.group_pos_manager,1,1,1,1
|
||||
access_rule_pos_user,Coupon Rule (PoS User),coupon.model_coupon_rule,point_of_sale.group_pos_user,1,0,0,0
|
||||
access_rule_pos_manager,Coupon Rule (PoS Manager),coupon.model_coupon_rule,point_of_sale.group_pos_manager,1,1,1,0
|
||||
access_coupon_pos_user,Coupon (PoS User),coupon.model_coupon_coupon,point_of_sale.group_pos_user,1,0,0,0
|
||||
access_coupon_pos_manager,Coupon (PoS Manager),coupon.model_coupon_coupon,point_of_sale.group_pos_manager,1,1,1,0
|
||||
access_reward_pos_user,Coupon Reward (PoS User),coupon.model_coupon_reward,point_of_sale.group_pos_user,1,0,0,0
|
||||
access_reward_pos_manager,Coupon Reward (PoS Manager),coupon.model_coupon_reward,point_of_sale.group_pos_manager,1,1,1,0
|
||||
access_coupon_generate_wizard,Coupon Generation,coupon.model_coupon_generate_wizard,point_of_sale.group_pos_user,1,1,1,0
|
||||
|
@@ -1,29 +0,0 @@
|
||||
|
||||
.pos .order .active-programs {
|
||||
padding-top: 0.5em;
|
||||
font-weight: normal;
|
||||
font-size: 80%;
|
||||
text-align: right;
|
||||
margin-right: 15px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.pos .order .active-programs .title {
|
||||
padding-top: 0.8em;
|
||||
padding-bottom: 0.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pos .order .orderline.program-reward {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.pos-receipt .pos-coupon-rewards {
|
||||
text-align: center;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.pos-coupon-rewards .coupon-container {
|
||||
padding-top: 1em;
|
||||
font-size: 75%;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
odoo.define('pos_coupon.PromoCodeButton', function (require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const ProductScreen = require('point_of_sale.ProductScreen');
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
class PromoCodeButton extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
useListener('click', this.onClick);
|
||||
}
|
||||
async onClick() {
|
||||
const { confirmed, payload: code } = await this.showPopup('TextInputPopup', {
|
||||
title: this.env._t('Enter Promotion or Coupon Code'),
|
||||
startingValue: '',
|
||||
});
|
||||
if (confirmed && code !== '') {
|
||||
const order = this.env.pos.get_order();
|
||||
order.activateCode(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
PromoCodeButton.template = 'PromoCodeButton';
|
||||
|
||||
ProductScreen.addControlButton({
|
||||
component: PromoCodeButton,
|
||||
condition: function () {
|
||||
return this.env.pos.config.use_coupon_programs;
|
||||
},
|
||||
});
|
||||
|
||||
Registries.Component.add(PromoCodeButton);
|
||||
|
||||
return PromoCodeButton;
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
odoo.define('pos_coupon.ResetProgramsButton', function (require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const ProductScreen = require('point_of_sale.ProductScreen');
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
class ResetProgramsButton extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
useListener('click', this.onClick);
|
||||
}
|
||||
async onClick() {
|
||||
const order = this.env.pos.get_order();
|
||||
order.resetPrograms();
|
||||
}
|
||||
}
|
||||
ResetProgramsButton.template = 'ResetProgramsButton';
|
||||
|
||||
ProductScreen.addControlButton({
|
||||
component: ResetProgramsButton,
|
||||
condition: function () {
|
||||
return this.env.pos.config.use_coupon_programs;
|
||||
},
|
||||
});
|
||||
|
||||
Registries.Component.add(ResetProgramsButton);
|
||||
|
||||
return ResetProgramsButton;
|
||||
});
|
||||
@@ -1,65 +0,0 @@
|
||||
odoo.define('pos_coupon.OrderWidget', function (require) {
|
||||
'use strict';
|
||||
|
||||
const OrderWidget = require('point_of_sale.OrderWidget');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { onWillRender } = owl;
|
||||
|
||||
const PosCouponOrderWidget = (OrderWidget) =>
|
||||
class PosCouponOrderWidget extends OrderWidget {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.withRewardsPromoPrograms = null;
|
||||
this.withRewardsBookedCoupons = null;
|
||||
this.onNextOrderPromoPrograms = null;
|
||||
this.show = null;
|
||||
onWillRender(() => this.updateActivePrograms());
|
||||
}
|
||||
updateActivePrograms() {
|
||||
const order = this.env.pos.get_order();
|
||||
const unRewardedArray = order.rewardsContainer ? order.rewardsContainer.getUnawarded() : [];
|
||||
const nonGeneratingProgramIds = new Set(unRewardedArray.map(({ program }) => program.id));
|
||||
const nonGeneratingCouponIds = new Set(
|
||||
unRewardedArray.map(({ coupon_id }) => coupon_id).filter((coupon_id) => coupon_id)
|
||||
);
|
||||
const onNextOrderPromoPrograms = order.activePromoProgramIds
|
||||
.filter((program_id) => {
|
||||
const program = order.pos.coupon_programs_by_id[program_id];
|
||||
return program.promo_applicability === 'on_next_order' && (order.programIdsToGenerateCoupons || []).includes(program_id);
|
||||
})
|
||||
.map((program_id) => order.pos.coupon_programs_by_id[program_id]);
|
||||
const onCurrentOrderPromoProgramIds = order.activePromoProgramIds.filter((program_id) => {
|
||||
const program = order.pos.coupon_programs_by_id[program_id];
|
||||
return program.promo_applicability === 'on_current_order';
|
||||
});
|
||||
const withRewardsPromoPrograms = onCurrentOrderPromoProgramIds
|
||||
.filter((program_id) => !nonGeneratingProgramIds.has(program_id))
|
||||
.map((program_id) => {
|
||||
const program = order.pos.coupon_programs_by_id[program_id];
|
||||
return {
|
||||
name: program.name,
|
||||
promo_code: program.promo_code,
|
||||
};
|
||||
});
|
||||
const withRewardsBookedCoupons = Object.values(order.bookedCouponCodes)
|
||||
.filter((couponCode) => !nonGeneratingCouponIds.has(couponCode.coupon_id))
|
||||
.map((couponCode) => {
|
||||
let program = order.pos.coupon_programs_by_id[couponCode.program_id];
|
||||
return {
|
||||
program_name: program.name,
|
||||
coupon_code: couponCode.code,
|
||||
};
|
||||
});
|
||||
this.withRewardsPromoPrograms = withRewardsPromoPrograms;
|
||||
this.withRewardsBookedCoupons = withRewardsBookedCoupons;
|
||||
this.onNextOrderPromoPrograms = onNextOrderPromoPrograms;
|
||||
this.show = withRewardsPromoPrograms.length !== 0 ||
|
||||
withRewardsBookedCoupons.length !== 0 ||
|
||||
onNextOrderPromoPrograms.length !== 0;
|
||||
}
|
||||
};
|
||||
|
||||
Registries.Component.extend(OrderWidget, PosCouponOrderWidget);
|
||||
|
||||
return OrderWidget;
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
odoo.define('pos_coupon.Orderline', function (require) {
|
||||
'use strict';
|
||||
|
||||
const Orderline = require('point_of_sale.Orderline');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const PosCouponOrderline = (Orderline) =>
|
||||
class extends Orderline {
|
||||
get addedClasses() {
|
||||
return Object.assign({ 'program-reward': this.props.line.is_program_reward }, super.addedClasses);
|
||||
}
|
||||
};
|
||||
|
||||
Registries.Component.extend(Orderline, PosCouponOrderline);
|
||||
|
||||
return Orderline;
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
odoo.define('pos_coupon.PaymentScreen', function (require) {
|
||||
'use strict';
|
||||
|
||||
const PaymentScreen = require('point_of_sale.PaymentScreen');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const session = require('web.session');
|
||||
|
||||
const PosCouponPaymentScreen = (PaymentScreen) =>
|
||||
class extends PaymentScreen {
|
||||
async _postPushOrderResolve(order, server_ids) {
|
||||
const bookedCouponIds = new Set(
|
||||
Object.values(order.bookedCouponCodes)
|
||||
.map((couponCode) => couponCode.coupon_id)
|
||||
.filter((coupon_id) => coupon_id)
|
||||
);
|
||||
const usedCouponIds = order.orderlines
|
||||
.map((line) => line.coupon_id)
|
||||
.filter((coupon_id) => coupon_id);
|
||||
for (let coupon_id of usedCouponIds) {
|
||||
bookedCouponIds.delete(coupon_id);
|
||||
}
|
||||
const unusedCouponIds = [...bookedCouponIds.values()];
|
||||
order.generated_coupons = await this.rpc(
|
||||
{
|
||||
model: 'pos.order',
|
||||
method: 'validate_coupon_programs',
|
||||
args: [server_ids, order.programIdsToGenerateCoupons || [], unusedCouponIds],
|
||||
kwargs: { context: session.user_context },
|
||||
},
|
||||
{}
|
||||
);
|
||||
return super._postPushOrderResolve(order, server_ids);
|
||||
}
|
||||
};
|
||||
|
||||
Registries.Component.extend(PaymentScreen, PosCouponPaymentScreen);
|
||||
|
||||
return PaymentScreen;
|
||||
});
|
||||
@@ -1,90 +0,0 @@
|
||||
odoo.define('pos_coupon.ProductScreen', function (require) {
|
||||
'use strict';
|
||||
|
||||
const ProductScreen = require('point_of_sale.ProductScreen');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { useBarcodeReader } = require('point_of_sale.custom_hooks');
|
||||
|
||||
const PosCouponProductScreen = (ProductScreen) =>
|
||||
class extends ProductScreen {
|
||||
setup() {
|
||||
super.setup();
|
||||
useBarcodeReader({
|
||||
coupon: this._onCouponScan,
|
||||
});
|
||||
}
|
||||
_onCouponScan(code) {
|
||||
this.currentOrder.activateCode(code.base_code);
|
||||
}
|
||||
async _updateSelectedOrderline(event) {
|
||||
const selectedLine = this.currentOrder.get_selected_orderline();
|
||||
if (selectedLine && selectedLine.is_program_reward && event.detail.key === 'Backspace') {
|
||||
const program = this.env.pos.coupon_programs_by_id[selectedLine.program_id]
|
||||
const { confirmed } = await this.showPopup('ConfirmPopup', {
|
||||
title: this.env._t('Deactivating program'),
|
||||
body: _.str.sprintf(
|
||||
this.env._t('Are you sure you want to deactivate %s in this order?'),
|
||||
program.name
|
||||
),
|
||||
cancelText: this.env._t('No'),
|
||||
confirmText: this.env._t('Yes'),
|
||||
});
|
||||
if (confirmed) {
|
||||
event.detail.buffer = null;
|
||||
} else {
|
||||
return; // do nothing on the line
|
||||
}
|
||||
}
|
||||
return super._updateSelectedOrderline(...arguments);
|
||||
}
|
||||
/**
|
||||
* 1/ Perform the usual set value operation (super._setValue) if the line being modified
|
||||
* is not a reward line or if it is a reward line, the `val` being set is '' or 'remove' only.
|
||||
*
|
||||
* 2/ Update activated programs and coupons when removing a reward line.
|
||||
*
|
||||
* 3/ Trigger 'update-rewards' if the line being modified is a regular line or
|
||||
* if removing a reward line.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_setValue(val) {
|
||||
const selectedLine = this.currentOrder.get_selected_orderline();
|
||||
if (
|
||||
!selectedLine ||
|
||||
!selectedLine.is_program_reward ||
|
||||
(selectedLine.is_program_reward && ['', 'remove'].includes(val))
|
||||
) {
|
||||
super._setValue(val);
|
||||
}
|
||||
if (!selectedLine) return;
|
||||
if (selectedLine.is_program_reward && val === 'remove') {
|
||||
if (selectedLine.coupon_id) {
|
||||
const coupon_code = Object.values(selectedLine.order.bookedCouponCodes).find(
|
||||
(couponCode) => couponCode.coupon_id === selectedLine.coupon_id
|
||||
).code;
|
||||
delete selectedLine.order.bookedCouponCodes[coupon_code];
|
||||
selectedLine.order.resetCoupons([selectedLine.coupon_id]).then(() => {
|
||||
this.showNotification(`Coupon (${coupon_code}) has been deactivated.`);
|
||||
});
|
||||
} else if (selectedLine.program_id) {
|
||||
// remove program from active programs
|
||||
const index = selectedLine.order.activePromoProgramIds.indexOf(selectedLine.program_id);
|
||||
selectedLine.order.activePromoProgramIds.splice(index, 1);
|
||||
this.showNotification(
|
||||
`'${
|
||||
this.env.pos.coupon_programs_by_id[selectedLine.program_id].name
|
||||
}' program has been deactivated.`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!selectedLine.is_program_reward || (selectedLine.is_program_reward && val === 'remove')) {
|
||||
selectedLine.order._updateRewards();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Registries.Component.extend(ProductScreen, PosCouponProductScreen);
|
||||
|
||||
return ProductScreen;
|
||||
});
|
||||
@@ -1,77 +0,0 @@
|
||||
odoo.define('pos_coupon.tour.pos_coupon1', function (require) {
|
||||
'use strict';
|
||||
|
||||
// --- PoS Coupon Tour Basic Part 1 ---
|
||||
// Generate coupons for PosCouponTour2.
|
||||
|
||||
const { PosCoupon } = require('pos_coupon.tour.PosCouponTourMethods');
|
||||
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
|
||||
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
|
||||
var Tour = require('web_tour.tour');
|
||||
|
||||
startSteps();
|
||||
|
||||
ProductScreen.do.confirmOpeningPopup();
|
||||
ProductScreen.do.clickHomeCategory();
|
||||
|
||||
// basic order
|
||||
// just accept the automatically applied promo program
|
||||
// applied programs:
|
||||
// - on cheapest product
|
||||
ProductScreen.exec.addOrderline('Whiteboard Pen', '5');
|
||||
PosCoupon.check.hasRewardLine('90.0% discount on cheapest product', '-2.88');
|
||||
PosCoupon.do.selectRewardLine('on cheapest product');
|
||||
PosCoupon.check.orderTotalIs('13.12');
|
||||
PosCoupon.exec.finalizeOrder('Cash', '20');
|
||||
|
||||
// remove the reward from auto promo program
|
||||
// no applied programs
|
||||
ProductScreen.exec.addOrderline('Whiteboard Pen', '6');
|
||||
PosCoupon.check.hasRewardLine('on cheapest product', '-2.88');
|
||||
PosCoupon.check.orderTotalIs('16.32');
|
||||
PosCoupon.exec.removeRewardLine('90.0% discount on cheapest product');
|
||||
PosCoupon.check.orderTotalIs('19.2');
|
||||
PosCoupon.exec.finalizeOrder('Cash', '20');
|
||||
|
||||
// order with coupon code from coupon program
|
||||
// applied programs:
|
||||
// - coupon program
|
||||
ProductScreen.exec.addOrderline('Desk Organizer', '9');
|
||||
PosCoupon.check.hasRewardLine('on cheapest product', '-4.59');
|
||||
PosCoupon.exec.removeRewardLine('90.0% discount on cheapest product');
|
||||
PosCoupon.check.orderTotalIs('45.90');
|
||||
PosCoupon.do.enterCode('invalid_code');
|
||||
PosCoupon.do.enterCode('1234');
|
||||
PosCoupon.check.hasRewardLine('Free Product - Desk Organizer', '-15.30');
|
||||
PosCoupon.exec.finalizeOrder('Cash', '50');
|
||||
|
||||
// Use coupon but eventually remove the reward
|
||||
// applied programs:
|
||||
// - on cheapest product
|
||||
ProductScreen.exec.addOrderline('Letter Tray', '4');
|
||||
ProductScreen.exec.addOrderline('Desk Organizer', '9');
|
||||
PosCoupon.check.hasRewardLine('90.0% discount on cheapest product', '-4.75');
|
||||
PosCoupon.check.orderTotalIs('62.27');
|
||||
PosCoupon.do.enterCode('5678');
|
||||
PosCoupon.check.hasRewardLine('Free Product - Desk Organizer', '-15.30');
|
||||
PosCoupon.check.orderTotalIs('46.97');
|
||||
PosCoupon.exec.removeRewardLine('Free Product - Desk Organizer');
|
||||
PosCoupon.check.orderTotalIs('62.27');
|
||||
PosCoupon.exec.finalizeOrder('Cash', '90');
|
||||
|
||||
// specific product discount
|
||||
// applied programs:
|
||||
// - on cheapest product
|
||||
// - on specific products
|
||||
ProductScreen.exec.addOrderline('Magnetic Board', '10') // 1.98
|
||||
ProductScreen.exec.addOrderline('Desk Organizer', '3') // 5.1
|
||||
ProductScreen.exec.addOrderline('Letter Tray', '4') // 4.8 tax 10%
|
||||
PosCoupon.check.hasRewardLine('90.0% discount on cheapest product', '-1.78')
|
||||
PosCoupon.check.orderTotalIs('54.44')
|
||||
PosCoupon.do.enterCode('promocode')
|
||||
PosCoupon.check.hasRewardLine('50.0% discount on products', '-17.55')
|
||||
PosCoupon.check.orderTotalIs('36.89')
|
||||
PosCoupon.exec.finalizeOrder('Cash', '50')
|
||||
|
||||
Tour.register('PosCouponTour1', { test: true, url: '/pos/web' }, getSteps());
|
||||
});
|
||||
@@ -1,110 +0,0 @@
|
||||
odoo.define('pos_coupon.tour.pos_coupon2', function (require) {
|
||||
'use strict';
|
||||
|
||||
// --- PoS Coupon Tour Basic Part 2 ---
|
||||
// Using the coupons generated from PosCouponTour1.
|
||||
|
||||
const { PosCoupon } = require('pos_coupon.tour.PosCouponTourMethods');
|
||||
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
|
||||
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
|
||||
var Tour = require('web_tour.tour');
|
||||
|
||||
startSteps();
|
||||
|
||||
ProductScreen.do.clickHomeCategory();
|
||||
|
||||
// Cheapest product discount should be replaced by the global discount
|
||||
// because it's amount is lower.
|
||||
// Applied programs:
|
||||
// - global discount
|
||||
ProductScreen.exec.addOrderline('Desk Organizer', '10'); // 5.1
|
||||
PosCoupon.check.hasRewardLine('on cheapest product', '-4.59');
|
||||
ProductScreen.exec.addOrderline('Letter Tray', '4'); // 4.8 tax 10%
|
||||
PosCoupon.check.hasRewardLine('on cheapest product', '-4.75');
|
||||
PosCoupon.do.enterCode('123456');
|
||||
PosCoupon.check.hasRewardLine('10.0% discount on total amount', '-5.10');
|
||||
PosCoupon.check.hasRewardLine('10.0% discount on total amount', '-2.11');
|
||||
PosCoupon.check.orderTotalIs('64.91');
|
||||
PosCoupon.exec.finalizeOrder('Cash', '70');
|
||||
|
||||
// Use coupon from global discount but on cheapest discount prevails.
|
||||
// The global discount coupon should be consumed during the order as it is
|
||||
// activated in the order. But upon validation, the coupon should return
|
||||
// to new state.
|
||||
// Applied programs:
|
||||
// - on cheapest discount
|
||||
ProductScreen.exec.addOrderline('Small Shelf', '3'); // 2.83 per item
|
||||
PosCoupon.check.hasRewardLine('90.0% discount on cheapest product', '-2.55');
|
||||
PosCoupon.do.enterCode('345678');
|
||||
PosCoupon.check.hasRewardLine('90.0% discount on cheapest product', '-2.55');
|
||||
ProductScreen.exec.addOrderline('Desk Organizer', '9'); // 4.80 per item
|
||||
PosCoupon.check.hasRewardLine('10.0% discount on total amount', '-5.44');
|
||||
ProductScreen.do.pressNumpad('Backspace Backspace')
|
||||
PosCoupon.check.hasRewardLine('90.0% discount on cheapest product', '-2.55');
|
||||
ProductScreen.exec.addOrderline('Desk Pad', '1'); // 1.98
|
||||
PosCoupon.check.hasRewardLine('90.0% discount on cheapest product', '-1.78');
|
||||
PosCoupon.check.orderTotalIs('8.69');
|
||||
PosCoupon.exec.finalizeOrder('Cash', '10');
|
||||
|
||||
// Scanning coupon twice.
|
||||
// Also apply global discount on top of free product to check if the
|
||||
// calculated discount is correct.
|
||||
// Applied programs:
|
||||
// - coupon program (free product)
|
||||
// - global discount
|
||||
ProductScreen.exec.addOrderline('Desk Organizer', '11'); // 5.1 per item
|
||||
PosCoupon.check.hasRewardLine('90.0% discount on cheapest product', '-4.59');
|
||||
PosCoupon.check.orderTotalIs('51.51');
|
||||
// add global discount and the discount will be replaced
|
||||
PosCoupon.do.enterCode('345678');
|
||||
PosCoupon.check.hasRewardLine('10.0% discount on total amount', '-5.61');
|
||||
// add free product coupon (for qty=11, free=4)
|
||||
// the discount should change after having free products
|
||||
// it should go back to cheapest discount as it is higher
|
||||
PosCoupon.do.enterCode('5678');
|
||||
PosCoupon.check.hasRewardLine('Free Product - Desk Organizer', '-20.40');
|
||||
PosCoupon.check.hasRewardLine('90.0% discount on cheapest product', '-4.59');
|
||||
// set quantity to 18
|
||||
// should result to 'charged qty'=12, 'free qty'=6
|
||||
ProductScreen.do.pressNumpad('Backspace 8')
|
||||
PosCoupon.check.hasRewardLine('10.0% discount on total amount', '-6.12');
|
||||
PosCoupon.check.hasRewardLine('Free Product - Desk Organizer', '-30.60');
|
||||
// scan the code again and check notification
|
||||
PosCoupon.do.enterCode('5678');
|
||||
PosCoupon.check.orderTotalIs('55.08');
|
||||
PosCoupon.exec.finalizeOrder('Cash', '60');
|
||||
|
||||
// Specific products discount (with promocode) and free product (1357)
|
||||
// Applied programs:
|
||||
// - discount on specific products
|
||||
// - free product
|
||||
ProductScreen.exec.addOrderline('Desk Organizer', '6'); // 5.1 per item
|
||||
PosCoupon.check.hasRewardLine('on cheapest product', '-4.59');
|
||||
PosCoupon.exec.removeRewardLine('90.0% discount on cheapest product');
|
||||
PosCoupon.do.enterCode('promocode');
|
||||
PosCoupon.check.hasRewardLine('50.0% discount on products', '-15.30');
|
||||
PosCoupon.do.enterCode('1357');
|
||||
PosCoupon.check.hasRewardLine('Free Product - Desk Organizer', '-10.20');
|
||||
PosCoupon.check.hasRewardLine('50.0% discount on products', '-10.20');
|
||||
PosCoupon.check.orderTotalIs('10.20');
|
||||
PosCoupon.exec.finalizeOrder('Cash', '20');
|
||||
|
||||
// Check reset program
|
||||
// Enter two codes and reset the programs.
|
||||
// The codes should be checked afterwards. They should return to new.
|
||||
// Applied programs:
|
||||
// - cheapest product
|
||||
PosCoupon.do.enterCode('2468');
|
||||
PosCoupon.do.enterCode('098765');
|
||||
ProductScreen.exec.addOrderline('Monitor Stand', '6'); // 3.19 per item
|
||||
PosCoupon.check.hasRewardLine('90.0% discount on cheapest product', '-2.87');
|
||||
PosCoupon.check.orderTotalIs('16.27');
|
||||
PosCoupon.exec.removeRewardLine('90.0% discount on cheapest product');
|
||||
PosCoupon.check.hasRewardLine('10.0% discount on total amount', '-1.91');
|
||||
PosCoupon.do.resetActivePrograms();
|
||||
PosCoupon.check.hasRewardLine('90.0% discount on cheapest product', '-2.87');
|
||||
PosCoupon.check.orderTotalIs('16.27');
|
||||
PosCoupon.exec.finalizeOrder('Cash', '20');
|
||||
|
||||
Tour.register('PosCouponTour2', { test: true, url: '/pos/web' }, getSteps());
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="pos_coupon.OrderReceipt" t-inherit="point_of_sale.OrderReceipt" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//div[hasclass('before-footer')]" position="inside">
|
||||
<t t-if="receipt.generated_coupons and receipt.generated_coupons.length !== 0">
|
||||
<div class="pos-coupon-rewards">
|
||||
<div>------------------------</div>
|
||||
<br/>
|
||||
<div>
|
||||
Coupon Codes
|
||||
</div>
|
||||
<t t-foreach="receipt.generated_coupons" t-as="coupon_info" t-key="coupon_info.code">
|
||||
<div class="coupon-container">
|
||||
<div style="font-size: 110%;">
|
||||
<t t-esc="coupon_info['program_name']"/>
|
||||
</div>
|
||||
<div>
|
||||
<span>Valid until: </span>
|
||||
<t t-if="coupon_info['expiration_date']">
|
||||
<t t-esc="coupon_info['expiration_date']"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
no expiration
|
||||
</t>
|
||||
</div>
|
||||
<div>
|
||||
<img t-att-src="'/report/barcode/Code128/'+coupon_info['code']" style="width:200px;height:50px" alt="Barcode"/>
|
||||
</div>
|
||||
<div>
|
||||
<t t-esc="coupon_info['code']"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||