1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216
|
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
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 different 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_used = fields.Integer(string='Used Seats', compute='_compute_seats', store=False)
seats_taken = fields.Integer(string="Taken 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.')
# reports
color = fields.Char('Color', default="#875A7B")
@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 available, reserved, used and taken seats. """
# initialize fields to 0 + compute seats availability
for ticket in self:
ticket.seats_reserved = ticket.seats_used = ticket.seats_available = 0
# aggregate registrations by ticket and by state
results = {}
if self.ids:
state_field = {
'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 ('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)
ticket.seats_taken = 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))
@api.depends('seats_max', 'seats_available')
@api.depends_context('name_with_seats_availability')
def _compute_display_name(self):
"""Adds ticket seats availability if requested by context."""
if not self.env.context.get('name_with_seats_availability'):
return super()._compute_display_name()
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),
)
ticket.display_name = 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')
@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'))))
def _get_ticket_printing_color(self):
self.ensure_one()
default_color = '#000000'
color_overrides_json = self.env['ir.config_parameter'].sudo().get_param('event.ticket_text_colors')
if color_overrides_json:
try:
color_overrides = json.loads(color_overrides_json)
return color_overrides.get(self.name, default_color)
except (json.JSONDecodeError, AttributeError):
pass
return default_color
|