Files
odoo_source/addons/event/models/event_ticket.py
T
Noe Antoine a340443177 [IMP] event{_sale}: add sequence to tickets (and templates)
When the price and the event are the same, nothing allows
ordering tickets in event_sale. Therefore, add a sequence
number and handle widget to the tickets (in data as well).
Also do it for ticket templates. Also, order the tickets
(on events and templates) by name before id.

Since event type tickets are not linked to any event, the
order does not include the event_id. However, event event
tickets are, and if they appear in the same list / reporting
someday, it makes sense to order them by event_id first.

In event_sale, we make sure the price is also in the order.
As the price is in the copied fields on the event when
using an event template, we add the price on the event
type tickets ordering to align with the event event tickets.

Add default value to sequence and sequence number to data
for event.type model. They were sequenced but demo data had
none, hence not really ordered.

We also add the id in the event.event _order value, otherwise
when ordering on the event (for instance ordering on event_id
for event.event.tickets), the order is random for events
having the same date_begin. We prevent this from happening.

Also make description optional hide on event types.

Task-2997391

closes odoo/odoo#101384

Signed-off-by: David Beguin (dbe) <dbe@odoo.com>
2022-11-17 14:56:42 +01:00

201 lines
9.7 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
from odoo.tools.misc import formatLang
class EventTemplateTicket(models.Model):
_name = 'event.type.ticket'
_description = 'Event Template Ticket'
_order = 'sequence, name, id'
sequence = fields.Integer('Sequence', default=10)
# 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 ['sequence', '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'
_order = "event_id, sequence, name, id"
@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_launched = fields.Boolean(string='Are sales launched', compute='_compute_is_launched')
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,
help='Whether it is possible to sell these tickets')
registration_ids = fields.One2many('event.registration', 'event_ticket_id', string='Registrations')
# seats
seats_reserved = fields.Integer(string='Reserved Seats', compute='_compute_seats', store=False)
seats_available = fields.Integer(string='Available Seats', compute='_compute_seats', store=False)
seats_unconfirmed = fields.Integer(string='Unconfirmed Seats', compute='_compute_seats', store=False)
seats_used = fields.Integer(string='Used Seats', compute='_compute_seats', store=False)
is_sold_out = fields.Boolean(
'Sold Out', compute='_compute_is_sold_out', help='Whether seats are not available for this ticket.')
@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('start_sale_datetime', 'event_id.date_tz')
def _compute_is_launched(self):
now = fields.Datetime.now()
for ticket in self:
if not ticket.start_sale_datetime:
ticket.is_launched = True
else:
ticket = ticket._set_tz_context()
current_datetime = fields.Datetime.context_timestamp(ticket, now)
start_sale_datetime = fields.Datetime.context_timestamp(ticket, ticket.start_sale_datetime)
ticket.is_launched = start_sale_datetime <= current_datetime
@api.depends('is_expired', 'start_sale_datetime', 'event_id.date_tz', 'seats_available', 'seats_max')
def _compute_sale_available(self):
for ticket in self:
ticket.sale_available = ticket.is_launched and not ticket.is_expired and not ticket.is_sold_out
@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_model(['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.depends('seats_limited', 'seats_available')
def _compute_is_sold_out(self):
for ticket in self:
ticket.is_sold_out = ticket.seats_limited and not ticket.seats_available
@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. '
'Please check ticket %(ticket_name)s', ticket_name=ticket.name))
@api.constrains('registration_ids', 'seats_max')
def _check_seats_availability(self, minimal_availability=0):
sold_out_tickets = []
for ticket in self:
if ticket.seats_max and ticket.seats_available < minimal_availability:
sold_out_tickets.append((_(
'- the ticket "%(ticket_name)s" (%(event_name)s): Missing %(nb_too_many)i seats.',
ticket_name=ticket.name, event_name=ticket.event_id.name, nb_too_many=-ticket.seats_available)))
if sold_out_tickets:
raise ValidationError(_('There are not enough seats available for:')
+ '\n%s\n' % '\n'.join(sold_out_tickets))
def name_get(self):
"""Adds ticket seats availability if requested by context."""
if not self.env.context.get('name_with_seats_availability'):
return super().name_get()
res = []
for ticket in self:
if not ticket.seats_max:
name = ticket.name
elif not ticket.seats_available:
name = _('%(ticket_name)s (Sold out)', ticket_name=ticket.name)
else:
name = _(
'%(ticket_name)s (%(count)s seats remaining)',
ticket_name=ticket.name,
count=formatLang(self.env, ticket.seats_available, digits=0),
)
res.append((ticket.id, name))
return res
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')
@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'))))