# -*- 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())[7:-18] @api.depends('program_id', 'code') def _compute_display_name(self): for card in self: card.display_name = f'{card.program_id.name}: {card.code}' program_id = fields.Many2one('loyalty.program', ondelete='restrict', default=lambda self: self.env.context.get('active_id', None)) program_type = fields.Selection(related='program_id.program_type') 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) points_display = fields.Char(compute='_compute_points_display') code = fields.Char(default=lambda self: self._generate_code(), required=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.')) @api.depends('points', 'point_name') def _compute_points_display(self): for card in self: card.points_display = "%.2f %s" % (card.points or 0, card.point_name or '') # 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 _has_source_order(self): return False 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_ids=self.ids, default_template_id=default_template and default_template.id, default_composition_mode='comment', default_email_layout_xmlid='mail.mail_notification_light', 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, force_send=False): """ Sends the 'At Creation' communication plan if it exist for the given coupons. """ if self.env.context.get('loyalty_no_mail', False) or self.env.context.get('action_no_send_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, force_send=force_send, 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