# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from dateutil.relativedelta import relativedelta from odoo import _, api, fields, models from odoo.tools import format_datetime from odoo.exceptions import AccessError, ValidationError class EventRegistration(models.Model): _name = 'event.registration' _description = 'Event Registration' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'id desc' # event event_id = fields.Many2one( 'event.event', string='Event', required=True, readonly=True, states={'draft': [('readonly', False)]}) event_ticket_id = fields.Many2one( 'event.event.ticket', string='Event Ticket', readonly=True, ondelete='restrict', states={'draft': [('readonly', False)]}) # utm informations utm_campaign_id = fields.Many2one('utm.campaign', 'Campaign', index=True, ondelete='set null') utm_source_id = fields.Many2one('utm.source', 'Source', index=True, ondelete='set null') utm_medium_id = fields.Many2one('utm.medium', 'Medium', index=True, ondelete='set null') # attendee partner_id = fields.Many2one( 'res.partner', string='Booked by', states={'done': [('readonly', True)]}) name = fields.Char( string='Attendee Name', index=True, compute='_compute_name', readonly=False, store=True, tracking=10) email = fields.Char(string='Email', compute='_compute_email', readonly=False, store=True, tracking=11) phone = fields.Char(string='Phone', compute='_compute_phone', readonly=False, store=True, tracking=12) mobile = fields.Char(string='Mobile', compute='_compute_mobile', readonly=False, store=True, tracking=13) # organization date_open = fields.Datetime(string='Registration Date', readonly=True, default=lambda self: fields.Datetime.now()) # weird crash is directly now date_closed = fields.Datetime( string='Attended Date', compute='_compute_date_closed', readonly=False, store=True) event_begin_date = fields.Datetime(string="Event Start Date", related='event_id.date_begin', readonly=True) event_end_date = fields.Datetime(string="Event End Date", related='event_id.date_end', readonly=True) company_id = fields.Many2one( 'res.company', string='Company', related='event_id.company_id', store=True, readonly=True, states={'draft': [('readonly', False)]}) state = fields.Selection([ ('draft', 'Unconfirmed'), ('cancel', 'Cancelled'), ('open', 'Confirmed'), ('done', 'Attended')], string='Status', default='draft', readonly=True, copy=False, tracking=True) @api.onchange('partner_id') def _onchange_partner_id(self): """ Keep an explicit onchange on partner_id. Rationale : if user explicitly changes the partner in interface, he want to update the whole customer information. If partner_id is updated in code (e.g. updating your personal information after having registered in website_event_sale) fields with a value should not be reset as we don't know which one is the right one. In other words * computed fields based on partner_id should only update missing information. Indeed automated code cannot decide which information is more accurate; * interface should allow to update all customer related information at once. We consider event users really want to update all fields related to the partner; """ for registration in self: if registration.partner_id: registration.update(registration._synchronize_partner_values(registration.partner_id)) @api.depends('partner_id') def _compute_name(self): for registration in self: if not registration.name and registration.partner_id: registration.name = registration._synchronize_partner_values( registration.partner_id, fnames=['name'] ).get('name') or False @api.depends('partner_id') def _compute_email(self): for registration in self: if not registration.email and registration.partner_id: registration.email = registration._synchronize_partner_values( registration.partner_id, fnames=['email'] ).get('email') or False @api.depends('partner_id') def _compute_phone(self): for registration in self: if not registration.phone and registration.partner_id: registration.phone = registration._synchronize_partner_values( registration.partner_id, fnames=['phone'] ).get('phone') or False @api.depends('partner_id') def _compute_mobile(self): for registration in self: if not registration.mobile and registration.partner_id: registration.mobile = registration._synchronize_partner_values( registration.partner_id, fnames=['mobile'] ).get('mobile') or False @api.depends('state') def _compute_date_closed(self): for registration in self: if not registration.date_closed: if registration.state == 'done': registration.date_closed = fields.Datetime.now() else: registration.date_closed = False @api.constrains('event_id', 'state') def _check_seats_limit(self): for registration in self: if registration.event_id.seats_limited and registration.event_id.seats_max and registration.event_id.seats_available < (1 if registration.state == 'draft' else 0): raise ValidationError(_('No more seats available for this event.')) @api.constrains('event_ticket_id', 'state') def _check_ticket_seats_limit(self): for record in self: if record.event_ticket_id.seats_max and record.event_ticket_id.seats_available < 0: raise ValidationError(_('No more available seats for this ticket')) @api.constrains('event_id', 'event_ticket_id') def _check_event_ticket(self): if any(registration.event_id != registration.event_ticket_id.event_id for registration in self if registration.event_ticket_id): raise ValidationError(_('Invalid event / ticket choice')) def _synchronize_partner_values(self, partner, fnames=None): if fnames is None: fnames = ['name', 'email', 'phone', 'mobile'] if partner: contact_id = partner.address_get().get('contact', False) if contact_id: contact = self.env['res.partner'].browse(contact_id) return dict((fname, contact[fname]) for fname in fnames if contact[fname]) return {} # ------------------------------------------------------------ # CRUD # ------------------------------------------------------------ @api.model_create_multi def create(self, vals_list): registrations = super(EventRegistration, self).create(vals_list) if registrations._check_auto_confirmation(): registrations.sudo().action_confirm() return registrations def write(self, vals): ret = super(EventRegistration, self).write(vals) if vals.get('state') == 'open': # auto-trigger after_sub (on subscribe) mail schedulers, if needed onsubscribe_schedulers = self.mapped('event_id.event_mail_ids').filtered(lambda s: s.interval_type == 'after_sub') onsubscribe_schedulers.sudo().execute() return ret def name_get(self): """ Custom name_get implementation to better differentiate registrations linked to a given partner but with different name (one partner buying several registrations) * name, partner_id has no name -> take name * partner_id has name, name void or same -> take partner name * both have name: partner + name """ ret_list = [] for registration in self: if registration.partner_id.name: if registration.name and registration.name != registration.partner_id.name: name = '%s, %s' % (registration.partner_id.name, registration.name) else: name = registration.partner_id.name else: name = registration.name ret_list.append((registration.id, name)) return ret_list def _check_auto_confirmation(self): if any(not registration.event_id.auto_confirm or (not registration.event_id.seats_available and registration.event_id.seats_limited) for registration in self): return False return True # ------------------------------------------------------------ # ACTIONS / BUSINESS # ------------------------------------------------------------ def action_set_draft(self): self.write({'state': 'draft'}) def action_confirm(self): self.write({'state': 'open'}) def action_set_done(self): """ Close Registration """ self.write({'state': 'done'}) def action_cancel(self): self.write({'state': 'cancel'}) def _message_get_suggested_recipients(self): recipients = super(EventRegistration, self)._message_get_suggested_recipients() public_users = self.env['res.users'].sudo() public_groups = self.env.ref("base.group_public", raise_if_not_found=False) if public_groups: public_users = public_groups.sudo().with_context(active_test=False).mapped("users") try: for attendee in self: is_public = attendee.sudo().with_context(active_test=False).partner_id.user_ids in public_users if public_users else False if attendee.partner_id and not is_public: attendee._message_add_suggested_recipient(recipients, partner=attendee.partner_id, reason=_('Customer')) elif attendee.email: attendee._message_add_suggested_recipient(recipients, email=attendee.email, reason=_('Customer Email')) except AccessError: # no read access rights -> ignore suggested recipients pass return recipients def _message_get_default_recipients(self): # Prioritize registration email over partner_id, which may be shared when a single # partner booked multiple seats return {r.id: { 'partner_ids': [], 'email_to': r.email, 'email_cc': False} for r in self} def _message_post_after_hook(self, message, msg_vals): if self.email and not self.partner_id: # we consider that posting a message with a specified recipient (not a follower, a specific one) # on a document without customer means that it was created through the chatter using # suggested recipients. This heuristic allows to avoid ugly hacks in JS. new_partner = message.partner_ids.filtered(lambda partner: partner.email == self.email) if new_partner: self.search([ ('partner_id', '=', False), ('email', '=', new_partner.email), ('state', 'not in', ['cancel']), ]).write({'partner_id': new_partner.id}) return super(EventRegistration, self)._message_post_after_hook(message, msg_vals) def action_send_badge_email(self): """ Open a window to compose an email, with the template - 'event_badge' message loaded by default """ self.ensure_one() template = self.env.ref('event.event_registration_mail_template_badge') compose_form = self.env.ref('mail.email_compose_message_wizard_form') ctx = dict( default_model='event.registration', 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", ) 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 get_date_range_str(self): self.ensure_one() today = fields.Datetime.now() event_date = self.event_begin_date diff = (event_date.date() - today.date()) if diff.days <= 0: return _('today') elif diff.days == 1: return _('tomorrow') elif (diff.days < 7): return _('in %d days') % (diff.days, ) elif (diff.days < 14): return _('next week') elif event_date.month == (today + relativedelta(months=+1)).month: return _('next month') else: return _('on %(date)s', date=format_datetime(self.env, self.event_begin_date, tz=self.event_id.date_tz, dt_format='medium')) def _get_registration_summary(self): self.ensure_one() return { 'id': self.id, 'name': self.name, 'partner_id': self.partner_id.id, 'ticket_name': self.event_ticket_id.name or _('None'), 'event_id': self.event_id.id, 'event_display_name': self.event_id.display_name, 'company_name': self.event_id.company_id and self.event_id.company_id.name or False, }