summaryrefslogtreecommitdiff
path: root/addons/coupon/models
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/coupon/models
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/coupon/models')
-rw-r--r--addons/coupon/models/__init__.py8
-rw-r--r--addons/coupon/models/coupon.py92
-rw-r--r--addons/coupon/models/coupon_program.py159
-rw-r--r--addons/coupon/models/coupon_reward.py92
-rw-r--r--addons/coupon/models/coupon_rules.py38
-rw-r--r--addons/coupon/models/mail_compose_message.py15
6 files changed, 404 insertions, 0 deletions
diff --git a/addons/coupon/models/__init__.py b/addons/coupon/models/__init__.py
new file mode 100644
index 00000000..14d4dcf9
--- /dev/null
+++ b/addons/coupon/models/__init__.py
@@ -0,0 +1,8 @@
+# -*- 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
diff --git a/addons/coupon/models/coupon.py b/addons/coupon/models/coupon.py
new file mode 100644
index 00000000..5f023906
--- /dev/null
+++ b/addons/coupon/models/coupon.py
@@ -0,0 +1,92 @@
+# -*- 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, _
+
+
+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(random.getrandbits(64))
+
+ code = fields.Char(default=_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 action_coupon_sent(self):
+ """ Open a window to compose an email, with the edi invoice template
+ message loaded by default
+ """
+ self.ensure_one()
+ template = self.env.ref('coupon.mail_template_sale_coupon', False)
+ 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(template),
+ default_template_id=template.id,
+ default_composition_mode='comment',
+ custom_layout='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'})
diff --git a/addons/coupon/models/coupon_program.py b/addons/coupon/models/coupon_program.py
new file mode 100644
index 00000000..afcb3da6
--- /dev/null
+++ b/addons/coupon/models/coupon_program.py
@@ -0,0 +1,159 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError, ValidationError
+
+import ast
+
+
+class CouponProgram(models.Model):
+ _name = 'coupon.program'
+ _description = "Coupon Program"
+ _inherits = {'coupon.rule': 'rule_id', 'coupon.reward': 'reward_id'}
+ # We should apply 'discount' promotion first to avoid offering free product when we should not.
+ # Eg: If the discount lower the SO total below the required threshold
+ # Note: This is only revelant when programs have the same sequence (which they have by default)
+ _order = "sequence, reward_type"
+
+ name = fields.Char(required=True, translate=True)
+ active = fields.Boolean('Active', default=True, help="A program is available for the customers when active")
+ rule_id = fields.Many2one('coupon.rule', string="Coupon Rule", ondelete='restrict', required=True)
+ reward_id = fields.Many2one('coupon.reward', string="Reward", ondelete='restrict', required=True, copy=False)
+ sequence = fields.Integer(copy=False,
+ help="Coupon program will be applied based on given sequence if multiple programs are " +
+ "defined on same condition(For minimum amount)")
+ maximum_use_number = fields.Integer(help="Maximum number of sales orders in which reward can be provided")
+ program_type = fields.Selection([
+ ('promotion_program', 'Promotional Program'),
+ ('coupon_program', 'Coupon Program'),
+ ],
+ help="""A promotional program can be either a limited promotional offer without code (applied automatically)
+ or with a code (displayed on a magazine for example) that may generate a discount on the current
+ order or create a coupon for a next order.
+
+ A coupon program generates coupons with a code that can be used to generate a discount on the current
+ order or create a coupon for a next order.""")
+ promo_code_usage = fields.Selection([
+ ('no_code_needed', 'Automatically Applied'),
+ ('code_needed', 'Use a code')],
+ help="Automatically Applied - No code is required, if the program rules are met, the reward is applied (Except the global discount or the free shipping rewards which are not cumulative)\n" +
+ "Use a code - If the program rules are met, a valid code is mandatory for the reward to be applied\n")
+ promo_code = fields.Char('Promotion Code', copy=False,
+ help="A promotion code is a code that is associated with a marketing discount. For example, a retailer might tell frequent customers to enter the promotion code 'THX001' to receive a 10%% discount on their whole order.")
+ promo_applicability = fields.Selection([
+ ('on_current_order', 'Apply On Current Order'),
+ ('on_next_order', 'Send a Coupon')],
+ default='on_current_order', string="Applicability")
+ coupon_ids = fields.One2many('coupon.coupon', 'program_id', string="Generated Coupons", copy=False)
+ coupon_count = fields.Integer(compute='_compute_coupon_count')
+ company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.company)
+ currency_id = fields.Many2one(string="Currency", related='company_id.currency_id', readonly=True)
+ validity_duration = fields.Integer(default=30,
+ help="Validity duration for a coupon after its generation")
+
+ @api.constrains('promo_code')
+ def _check_promo_code_constraint(self):
+ """ Program code must be unique """
+ for program in self.filtered(lambda p: p.promo_code):
+ domain = [('id', '!=', program.id), ('promo_code', '=', program.promo_code)]
+ if self.search(domain):
+ raise ValidationError(_('The program code must be unique!'))
+
+ @api.depends('coupon_ids')
+ def _compute_coupon_count(self):
+ coupon_data = self.env['coupon.coupon'].read_group([('program_id', 'in', self.ids)], ['program_id'], ['program_id'])
+ mapped_data = dict([(m['program_id'][0], m['program_id_count']) for m in coupon_data])
+ for program in self:
+ program.coupon_count = mapped_data.get(program.id, 0)
+
+ @api.onchange('promo_code_usage')
+ def _onchange_promo_code_usage(self):
+ if self.promo_code_usage == 'no_code_needed':
+ self.promo_code = False
+
+ @api.onchange('reward_product_id')
+ def _onchange_reward_product_id(self):
+ if self.reward_product_id:
+ self.reward_product_uom_id = self.reward_product_id.uom_id
+
+ @api.onchange('discount_type')
+ def _onchange_discount_type(self):
+ if self.discount_type == 'fixed_amount':
+ self.discount_apply_on = 'on_order'
+
+ @api.model
+ def create(self, vals):
+ program = super(CouponProgram, self).create(vals)
+ if not vals.get('discount_line_product_id', False):
+ values = program._get_discount_product_values()
+ discount_line_product_id = self.env['product.product'].create(values)
+ program.write({'discount_line_product_id': discount_line_product_id.id})
+ return program
+
+ def write(self, vals):
+ res = super(CouponProgram, self).write(vals)
+ reward_fields = [
+ 'reward_type', 'reward_product_id', 'discount_type', 'discount_percentage',
+ 'discount_apply_on', 'discount_specific_product_ids', 'discount_fixed_amount'
+ ]
+ if any(field in reward_fields for field in vals):
+ self.mapped('discount_line_product_id').write({'name': self[0].reward_id.display_name})
+ return res
+
+ def unlink(self):
+ if self.filtered('active'):
+ raise UserError(_('You can not delete a program in active state'))
+ # get reference to rule and reward
+ rule = self.rule_id
+ reward = self.reward_id
+ # unlink the program
+ super(CouponProgram, self).unlink()
+ # then unlink the rule and reward
+ rule.unlink()
+ reward.unlink()
+ return True
+
+ def toggle_active(self):
+ super(CouponProgram, self).toggle_active()
+ for program in self:
+ program.discount_line_product_id.active = program.active
+ coupons = self.filtered(lambda p: not p.active and p.promo_code_usage == 'code_needed').mapped('coupon_ids')
+ coupons.filtered(lambda x: x.state != 'used').write({'state': 'expired'})
+
+ def _compute_program_amount(self, field, currency_to):
+ self.ensure_one()
+ return self.currency_id._convert(self[field], currency_to, self.company_id, fields.Date.today())
+
+ def _is_valid_partner(self, partner):
+ if self.rule_partners_domain and self.rule_partners_domain != '[]':
+ domain = ast.literal_eval(self.rule_partners_domain) + [('id', '=', partner.id)]
+ return bool(self.env['res.partner'].search_count(domain))
+ else:
+ return True
+
+ def _is_valid_product(self, product):
+ # NOTE: if you override this method, think of also overriding _get_valid_products
+ # we also encourage the use of _get_valid_products as its execution is faster
+ if self.rule_products_domain:
+ domain = ast.literal_eval(self.rule_products_domain) + [('id', '=', product.id)]
+ return bool(self.env['product.product'].search_count(domain))
+ else:
+ return True
+
+ def _get_valid_products(self, products):
+ if self.rule_products_domain:
+ domain = ast.literal_eval(self.rule_products_domain)
+ return products.filtered_domain(domain)
+ return products
+
+ def _get_discount_product_values(self):
+ return {
+ 'name': self.reward_id.display_name,
+ 'type': 'service',
+ 'taxes_id': False,
+ 'supplier_taxes_id': False,
+ 'sale_ok': False,
+ 'purchase_ok': False,
+ 'lst_price': 0, #Do not set a high value to avoid issue with coupon code
+ }
diff --git a/addons/coupon/models/coupon_reward.py b/addons/coupon/models/coupon_reward.py
new file mode 100644
index 00000000..d5ad92a9
--- /dev/null
+++ b/addons/coupon/models/coupon_reward.py
@@ -0,0 +1,92 @@
+# -*- 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
diff --git a/addons/coupon/models/coupon_rules.py b/addons/coupon/models/coupon_rules.py
new file mode 100644
index 00000000..1412d683
--- /dev/null
+++ b/addons/coupon/models/coupon_rules.py
@@ -0,0 +1,38 @@
+# -*- 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'))
diff --git a/addons/coupon/models/mail_compose_message.py b/addons/coupon/models/mail_compose_message.py
new file mode 100644
index 00000000..a819777a
--- /dev/null
+++ b/addons/coupon/models/mail_compose_message.py
@@ -0,0 +1,15 @@
+# -*- 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 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().send_mail(**kwargs)