When an attendee's registration was archived, their seat was still considered taken, which could be problematic such as in cases of limited seat availability. Only non-archived registrations are now counted as seats. The same error as with regular registrations will be raised if there are not enough seats available to un-archive a registration. These ValidationError messages now show the name of the fully booked event. A few python tests are included to verify the impact of (un)archiving on seats availability for events and for event tickets. The appearence of archived registrations was also not different in form and kanban views, which is somewhat confusing and inconsistent with the aspect of archived records in Odoo. Actions buttons are not available on archived records. Filtering in the archived records needed to be simplified from a "Custom Filter" to a one-click feature, already available for many models. Task-2646298 closes odoo/odoo#77715 Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
166 lines
7.9 KiB
Python
166 lines
7.9 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.exceptions import ValidationError, UserError
|
|
|
|
|
|
class EventTemplateTicket(models.Model):
|
|
_name = 'event.type.ticket'
|
|
_description = 'Event Template Ticket'
|
|
|
|
# description
|
|
name = fields.Char(
|
|
string='Name', default=lambda self: _('Registration'),
|
|
required=True, translate=True)
|
|
description = fields.Text(
|
|
'Description', translate=True,
|
|
help="A description of the ticket that you want to communicate to your customers.")
|
|
event_type_id = fields.Many2one(
|
|
'event.type', string='Event Category', ondelete='cascade', required=True)
|
|
# seats
|
|
seats_limited = fields.Boolean(string='Limit Attendees', readonly=True, store=True,
|
|
compute='_compute_seats_limited')
|
|
seats_max = fields.Integer(
|
|
string='Maximum Attendees',
|
|
help="Define the number of available tickets. If you have too many registrations you will "
|
|
"not be able to sell tickets anymore. Set 0 to ignore this rule set as unlimited.")
|
|
|
|
@api.depends('seats_max')
|
|
def _compute_seats_limited(self):
|
|
for ticket in self:
|
|
ticket.seats_limited = ticket.seats_max
|
|
|
|
@api.model
|
|
def _get_event_ticket_fields_whitelist(self):
|
|
""" Whitelist of fields that are copied from event_type_ticket_ids to event_ticket_ids when
|
|
changing the event_type_id field of event.event """
|
|
return ['name', 'description', 'seats_max']
|
|
|
|
|
|
class EventTicket(models.Model):
|
|
""" Ticket model allowing to have differnt kind of registrations for a given
|
|
event. Ticket are based on ticket type as they share some common fields
|
|
and behavior. Those models come from <= v13 Odoo event.event.ticket that
|
|
modeled both concept: tickets for event templates, and tickets for events. """
|
|
_name = 'event.event.ticket'
|
|
_inherit = 'event.type.ticket'
|
|
_description = 'Event Ticket'
|
|
|
|
@api.model
|
|
def default_get(self, fields):
|
|
res = super(EventTicket, self).default_get(fields)
|
|
if 'name' in fields and (not res.get('name') or res['name'] == _('Registration')) and self.env.context.get('default_event_name'):
|
|
res['name'] = _('Registration for %s', self.env.context['default_event_name'])
|
|
return res
|
|
|
|
# description
|
|
event_type_id = fields.Many2one(ondelete='set null', required=False)
|
|
event_id = fields.Many2one(
|
|
'event.event', string="Event",
|
|
ondelete='cascade', required=True)
|
|
company_id = fields.Many2one('res.company', related='event_id.company_id')
|
|
# sale
|
|
start_sale_datetime = fields.Datetime(string="Registration Start")
|
|
end_sale_datetime = fields.Datetime(string="Registration End")
|
|
is_expired = fields.Boolean(string='Is Expired', compute='_compute_is_expired')
|
|
sale_available = fields.Boolean(string='Is Available', compute='_compute_sale_available', compute_sudo=True)
|
|
registration_ids = fields.One2many('event.registration', 'event_ticket_id', string='Registrations')
|
|
# seats
|
|
seats_reserved = fields.Integer(string='Reserved Seats', compute='_compute_seats', store=True)
|
|
seats_available = fields.Integer(string='Available Seats', compute='_compute_seats', store=True)
|
|
seats_unconfirmed = fields.Integer(string='Unconfirmed Seats', compute='_compute_seats', store=True)
|
|
seats_used = fields.Integer(string='Used Seats', compute='_compute_seats', store=True)
|
|
|
|
@api.depends('end_sale_datetime', 'event_id.date_tz')
|
|
def _compute_is_expired(self):
|
|
for ticket in self:
|
|
ticket = ticket._set_tz_context()
|
|
current_datetime = fields.Datetime.context_timestamp(ticket, fields.Datetime.now())
|
|
if ticket.end_sale_datetime:
|
|
end_sale_datetime = fields.Datetime.context_timestamp(ticket, ticket.end_sale_datetime)
|
|
ticket.is_expired = end_sale_datetime < current_datetime
|
|
else:
|
|
ticket.is_expired = False
|
|
|
|
@api.depends('is_expired', 'start_sale_datetime', 'event_id.date_tz', 'seats_available', 'seats_max')
|
|
def _compute_sale_available(self):
|
|
for ticket in self:
|
|
if not ticket.is_launched() or ticket.is_expired or (ticket.seats_max and ticket.seats_available <= 0):
|
|
ticket.sale_available = False
|
|
else:
|
|
ticket.sale_available = True
|
|
|
|
@api.depends('seats_max', 'registration_ids.state', 'registration_ids.active')
|
|
def _compute_seats(self):
|
|
""" Determine reserved, available, reserved but unconfirmed and used seats. """
|
|
# initialize fields to 0 + compute seats availability
|
|
for ticket in self:
|
|
ticket.seats_unconfirmed = ticket.seats_reserved = ticket.seats_used = ticket.seats_available = 0
|
|
# aggregate registrations by ticket and by state
|
|
results = {}
|
|
if self.ids:
|
|
state_field = {
|
|
'draft': 'seats_unconfirmed',
|
|
'open': 'seats_reserved',
|
|
'done': 'seats_used',
|
|
}
|
|
query = """ SELECT event_ticket_id, state, count(event_id)
|
|
FROM event_registration
|
|
WHERE event_ticket_id IN %s AND state IN ('draft', 'open', 'done') AND active = true
|
|
GROUP BY event_ticket_id, state
|
|
"""
|
|
self.env['event.registration'].flush(['event_id', 'event_ticket_id', 'state', 'active'])
|
|
self.env.cr.execute(query, (tuple(self.ids),))
|
|
for event_ticket_id, state, num in self.env.cr.fetchall():
|
|
results.setdefault(event_ticket_id, {})[state_field[state]] = num
|
|
|
|
# compute seats_available
|
|
for ticket in self:
|
|
ticket.update(results.get(ticket._origin.id or ticket.id, {}))
|
|
if ticket.seats_max > 0:
|
|
ticket.seats_available = ticket.seats_max - (ticket.seats_reserved + ticket.seats_used)
|
|
|
|
@api.constrains('start_sale_datetime', 'end_sale_datetime')
|
|
def _constrains_dates_coherency(self):
|
|
for ticket in self:
|
|
if ticket.start_sale_datetime and ticket.end_sale_datetime and ticket.start_sale_datetime > ticket.end_sale_datetime:
|
|
raise UserError(_('The stop date cannot be earlier than the start date.'))
|
|
|
|
@api.constrains('seats_available', 'seats_max')
|
|
def _constrains_seats_available(self):
|
|
for record in self:
|
|
if record.seats_max and record.seats_available < 0:
|
|
raise ValidationError(
|
|
_('No more available seats for the ticket %s (%s). '
|
|
'Raise the limit or remove some other confirmed registrations first.',
|
|
record.name, record.event_id.name))
|
|
|
|
def _get_ticket_multiline_description(self):
|
|
""" Compute a multiline description of this ticket. It is used when ticket
|
|
description are necessary without having to encode it manually, like sales
|
|
information. """
|
|
return '%s\n%s' % (self.display_name, self.event_id.display_name)
|
|
|
|
def _set_tz_context(self):
|
|
self.ensure_one()
|
|
return self.with_context(tz=self.event_id.date_tz or 'UTC')
|
|
|
|
def is_launched(self):
|
|
# TDE FIXME: in master, make a computed field, easier to use
|
|
self.ensure_one()
|
|
if self.start_sale_datetime:
|
|
ticket = self._set_tz_context()
|
|
current_datetime = fields.Datetime.context_timestamp(ticket, fields.Datetime.now())
|
|
start_sale_datetime = fields.Datetime.context_timestamp(ticket, ticket.start_sale_datetime)
|
|
return start_sale_datetime <= current_datetime
|
|
else:
|
|
return True
|
|
|
|
@api.ondelete(at_uninstall=False)
|
|
def _unlink_except_if_registrations(self):
|
|
if self.registration_ids:
|
|
raise UserError(_(
|
|
"The following tickets cannot be deleted while they have one or more registrations linked to them:\n- %s",
|
|
'\n- '.join(self.mapped('name'))))
|