Globally ease event registrations followup by improving the related search views and by allowing a quick access to statistics. Specifications: 1) A stat button has been added on event that go to attendee reporting filtered on the event 2.a) in the attendee reporting, the text search has been modified as follows: - order: first search on event - order + added: then search on responsible - order + added: then search on organizer - order: then search on participant - then other field already present 2.b) still in the attendee reporting, filter "archived" has been moved as the last filter 2.c) still in the attendee reporting, group by for campaign, medium and source have been added 3) link to confirmed and expected attendee in the kanban view box now redirect to the new stat view with the suitable filter Technical remarks: - view for a specific event launched from the event view uses a specific search view which doesn't include search on event (name, organizer, ...). Task 2761011 Part-of: odoo/odoo#84546
330 lines
15 KiB
Python
330 lines
15 KiB
Python
# -*- 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, SUPERUSER_ID
|
|
from odoo.tools import format_date
|
|
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)]})
|
|
active = fields.Boolean(default=True)
|
|
# 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', tracking=1)
|
|
name = fields.Char(
|
|
string='Attendee Name', index='trigram',
|
|
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_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)
|
|
event_organizer_id = fields.Many2one(string='Event Organizer', related='event_id.organizer_id', readonly=True)
|
|
event_user_id = fields.Many2one(string='Event Responsible', related='event_id.user_id', 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.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 = self.env.cr.now()
|
|
else:
|
|
registration.date_closed = False
|
|
|
|
@api.constrains('event_id', 'state')
|
|
def _check_seats_limit(self):
|
|
for registration in self:
|
|
event_id = registration.event_id
|
|
if event_id.seats_limited and event_id.seats_max and event_id.seats_available < (1 if registration.state == 'draft' and event_id.auto_confirm else 0):
|
|
raise ValidationError(_('There are no more available seats for %s. Raise the limit or remove some other confirmed registrations first.', event_id.name))
|
|
|
|
@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)
|
|
|
|
# auto_confirm if possible; if not automatically confirmed, call mail schedulers in case
|
|
# some were created already open
|
|
if registrations._check_auto_confirmation():
|
|
registrations.sudo().action_confirm()
|
|
elif not self.env.context.get('install_mode', False):
|
|
# running the scheduler for demo data can cause an issue where wkhtmltopdf runs during
|
|
# server start and hangs indefinitely, leading to serious crashes
|
|
# we currently avoid this by not running the scheduler, would be best to find the actual
|
|
# reason for this issue and fix it so we can remove this check
|
|
registrations._update_mail_schedulers()
|
|
|
|
return registrations
|
|
|
|
def write(self, vals):
|
|
pre_draft = self.env['event.registration']
|
|
if vals.get('state') == 'open':
|
|
pre_draft = self.filtered(lambda registration: registration.state == 'draft')
|
|
|
|
ret = super(EventRegistration, self).write(vals)
|
|
|
|
if vals.get('state') == 'open' and not self.env.context.get('install_mode', False):
|
|
# running the scheduler for demo data can cause an issue where wkhtmltopdf runs during
|
|
# server start and hangs indefinitely, leading to serious crashes
|
|
# we currently avoid this by not running the scheduler, would be best to find the actual
|
|
# reason for this issue and fix it so we can remove this check
|
|
pre_draft._update_mail_schedulers()
|
|
|
|
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 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',
|
|
default_email_layout_xmlid="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 _update_mail_schedulers(self):
|
|
""" Update schedulers to set them as running again, and cron to be called
|
|
as soon as possible. """
|
|
open_registrations = self.filtered(lambda registration: registration.state == 'open')
|
|
if not open_registrations:
|
|
return
|
|
|
|
onsubscribe_schedulers = self.env['event.mail'].sudo().search([
|
|
('event_id', 'in', open_registrations.event_id.ids),
|
|
('interval_type', '=', 'after_sub')
|
|
])
|
|
if not onsubscribe_schedulers:
|
|
return
|
|
|
|
onsubscribe_schedulers.update({'mail_done': False})
|
|
# we could simply call _create_missing_mail_registrations and let cron do their job
|
|
# but it currently leads to several delays. We therefore call execute until
|
|
# cron triggers are correctly used
|
|
onsubscribe_schedulers.with_user(SUPERUSER_ID).execute()
|
|
|
|
# ------------------------------------------------------------
|
|
# MAILING / GATEWAY
|
|
# ------------------------------------------------------------
|
|
|
|
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)
|
|
|
|
# ------------------------------------------------------------
|
|
# TOOLS
|
|
# ------------------------------------------------------------
|
|
|
|
def get_date_range_str(self, lang_code=False):
|
|
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_date(self.env, self.event_begin_date, lang_code=lang_code, date_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,
|
|
}
|