Prevent to unlink tickets when having registrations by adding an ondelete restrict attribute. Otherwise billing information may be lost. Task ID 2244487 PR odoo/odoo#52998 FwdPort of odoo/odoo#52923 X-original-commit: 5322d76d378e4bd88a42dc0e4b01bb46c7b3546a
278 lines
12 KiB
Python
278 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.tools import format_datetime
|
|
from odoo.exceptions import AccessError, ValidationError
|
|
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
|
|
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_contact_info', readonly=False, store=True, tracking=10)
|
|
email = fields.Char(string='Email', compute='_compute_contact_info', readonly=False, store=True, tracking=11)
|
|
phone = fields.Char(string='Phone', compute='_compute_contact_info', readonly=False, store=True, tracking=12)
|
|
mobile = fields.Char(string='Mobile', compute='_compute_contact_info', 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_contact_info(self):
|
|
for registration in self:
|
|
if registration.partner_id:
|
|
partner_vals = self._synchronize_partner_values(registration.partner_id)
|
|
registration.update(
|
|
dict((fname, fvalue)
|
|
for fname, fvalue in partner_vals.items()
|
|
if fvalue and not (registration[fname] or registration._origin[fname])
|
|
)
|
|
)
|
|
|
|
@api.depends('state')
|
|
def _compute_date_closed(self):
|
|
for registration in self:
|
|
if registration.state == 'done' and not registration.date_closed:
|
|
registration.date_closed = fields.Datetime.now()
|
|
|
|
@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'))
|
|
|
|
# ------------------------------------------------------------
|
|
# 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.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
|
|
|
|
def _synchronize_partner_values(self, partner):
|
|
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 ['name', 'email', 'phone', 'mobile'] if contact[fname])
|
|
return {}
|
|
|
|
# ------------------------------------------------------------
|
|
# 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 ') + 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,
|
|
}
|