[MERGE] event[_sale]: use event tickets without event_sale
PURPOSE
Integration between event and eCommerce is required only when users handle the
entire selling process online. However users may require ticketing support
while managing payments outside of Odoo. Purpose of this commit is to support
tickets directly in event application without need of sales.
RATIONALE
Remove the need to have event_sale installed to manage basic multi ticket
event type. Integration with eCommerce is needed only when one wants to handle
the entire selling flow online, i.e. order, payment, ... Integration with
Sales is needed only when one wants to create sale orders linked to attendees.
Many event users do not need all of this. Their attendees pay through bank
transfers or they simply manage payments outside of Odoo while still
requiring tickets management.
See sub commits for more details. Here are a sample of main specifications
linked to this task.
SPECIFICATIONS: TICKET MOVE / SPLIT
Remove the need to have event_sale installed to manage basic ticketing on
events.
Move ticket model (event.event.ticket) directly into event, copying most
fields from event_sale. Only sale specific fields and behavior should be kept
in event_sale :
* keep product_id and price information in event_sale;
* keep sales analysis in event_sale;
We also split tickets model used for event type (event.type.ticket) and
events (event.event.ticket). Indeed previously to this commit both are
modeled in the same table, with the following issues :
* tickets on templates use only a subset of fields: name, seats availability,
product, price;
* a ticket has either an event_id, either an event_type_id, and there are
constraints to try to avoid having lost tickets. This leads to a strange
model where m2o fields are required only in some cases with a dual
behavior;
* tickets are not shared between event.type and event.event. They are copied
and having a single model is therefore not necessary;
We therefore choose to have a light model for event.type.ticket. It is linked
to event.type when configuring template tickets. They are copied in the
onchange copying event template configuration to the event itself, leading
to event.event.ticket creation.
Some tests are moved / completed accordingly.
Access rights are copied from website_event_sale to website_even concerning
ticket access for public / portal. Currently they are kept as they are with
some rewording as it is not the purpose of this commit to rewrite them.
SPECIFICATIONS: FRONTEND
In this commit we move frontend part of ticket support from website_event_sale
to website_event. Now eCommerce / event integration adds only payment
information when registering.
About tickets
* if there is no ticket -> generic registration allowed;
* if there is one ticket -> quick registration box;
* more than one ticket -> unfolding registration box with all available
tickets;
About price
* sale not installed -> no mention of price. A ticket without price is not
free. Its description allow to tell how to pay for example;
* a price is set: price is displayed;
* no price is set: FREE is displayed;
Most event frontend templates and controllers are therefore moved from
website_event_sale to website_event. Only part about pricing and sale order
creation is now located in website_event_sale.
Buy flow remains mainly untouched. Indeed this commit is mainly about moving
template to support tickets.
SPECIFICATIONS: improve ticket model and fields propagation
Remove date fields from event.type.ticket
Propagate seats definition from ticket template to tickets
Add a description on tickets to use in frontend
Improve naming of event tickets
LINKS
Task ID 2177281
Community PR #43488
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
@@ -21,6 +21,8 @@ Key Features
|
||||
'data': [
|
||||
'security/event_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/event_menu_views.xml',
|
||||
'views/event_ticket_views.xml',
|
||||
'views/event_views.xml',
|
||||
'views/event_stage_views.xml',
|
||||
'report/event_event_templates.xml',
|
||||
@@ -32,7 +34,9 @@ Key Features
|
||||
'views/res_partner_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/res_users_demo.xml',
|
||||
'data/event_demo.xml',
|
||||
'data/event_registration_demo.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
|
||||
@@ -27,6 +27,12 @@
|
||||
<field name="auto_confirm" eval="True"/>
|
||||
<field name="use_mail_schedule" eval="False"/>
|
||||
</record>
|
||||
<record id="event_type_data_ticket" model="event.type">
|
||||
<field name="name">Ticketing</field>
|
||||
<field name="auto_confirm" eval="False"/>
|
||||
<field name="use_mail_schedule" eval="True"/>
|
||||
<field name="use_ticket" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- Event stages -->
|
||||
<record id="event_stage_new" model="event.stage">
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="base.user_demo" model="res.users">
|
||||
<field name="groups_id" eval="[(4, ref('event.group_event_user'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- Event Type -->
|
||||
<record id="event_type_0" model="event.type">
|
||||
<field name="name">Exhibition</field>
|
||||
<field name="use_mail_schedule" eval="False"/>
|
||||
<field name="auto_confirm" eval="False"/>
|
||||
</record>
|
||||
<record id="event_type_1" model="event.type">
|
||||
<field name="name">Training</field>
|
||||
<field name="use_mail_schedule" eval="True"/>
|
||||
<field name="auto_confirm" eval="True"/>
|
||||
</record>
|
||||
<record id="event_type_2" model="event.type">
|
||||
<field name="name">Sport</field>
|
||||
@@ -22,15 +21,40 @@
|
||||
<!-- Event -->
|
||||
<record id="event_0" model="event.event">
|
||||
<field name="name">Design Fair Los Angeles</field>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d 08:00:00')" name="date_begin"/>
|
||||
<field eval="(DateTime.now() + timedelta(days=5)).strftime('%Y-%m-%d 18:00:00')" name="date_end"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="date_begin" eval="(DateTime.now() + timedelta(days=2)).strftime('%Y-%m-%d 08:00:00')"/>
|
||||
<field name="date_end" eval="(DateTime.now() + timedelta(days=6)).strftime('%Y-%m-%d 18:00:00')"/>
|
||||
<field name="seats_availability">limited</field>
|
||||
<field name="seats_max">500</field>
|
||||
<field name="seats_max">50</field>
|
||||
<field name="address_id" ref="base.res_partner_1"/>
|
||||
<field name="date_tz">US/Pacific</field>
|
||||
<field name="event_type_id" ref="event_type_0"/>
|
||||
<field name="auto_confirm" eval="False"/>
|
||||
<field name="twitter_hashtag">DesignFairLA</field>
|
||||
<field name="stage_id" ref="event_stage_booked"/>
|
||||
</record>
|
||||
<record id="event_0_ticket_0" model="event.event.ticket">
|
||||
<field name="name">Free</field>
|
||||
<field name="description">Free entrance, no food !</field>
|
||||
<field name="event_id" ref="event.event_0"/>
|
||||
<field name="start_sale_date" eval="(DateTime.today() - timedelta(days=5)).strftime('%Y-%m-%d')"/>
|
||||
<field name="end_sale_date" eval="(DateTime.today() + timedelta(days=5)).strftime('%Y-%m-%d')"/>
|
||||
<field name="seats_max">0</field>
|
||||
</record>
|
||||
<record id="event_0_ticket_1" model="event.event.ticket">
|
||||
<field name="name">Standard</field>
|
||||
<field name="description">For only 10, you gain access to catering. Yum yum.</field>
|
||||
<field name="event_id" ref="event.event_0"/>
|
||||
<field name="end_sale_date" eval="(DateTime.today() + timedelta(days=2)).strftime('%Y-%m-%d')"/>
|
||||
<field name="seats_max">50</field>
|
||||
</record>
|
||||
<record id="event_0_ticket_2" model="event.event.ticket">
|
||||
<field name="name">VIP</field>
|
||||
<field name="description">You are truly among the best.</field>
|
||||
<field name="event_id" ref="event.event_0"/>
|
||||
<field name="end_sale_date" eval="(DateTime.today() + timedelta(days=3)).strftime('%Y-%m-%d')"/>
|
||||
<field name="seats_max">10</field>
|
||||
</record>
|
||||
|
||||
<record id="event.event_1" model="event.event">
|
||||
<field name="name">Great Reno Ballon Race</field>
|
||||
@@ -54,6 +78,18 @@
|
||||
<field name="seats_max">200</field>
|
||||
<field name="stage_id" ref="event_stage_booked"/>
|
||||
</record>
|
||||
<record id="event_2_ticket_1" model="event.event.ticket">
|
||||
<field name="name">Standard</field>
|
||||
<field name="event_id" ref="event.event_2"/>
|
||||
<field name="end_sale_date" eval="(DateTime.today() + timedelta(90)).strftime('%Y-%m-%d')"/>
|
||||
<field name="seats_max">50</field>
|
||||
</record>
|
||||
<record id="event_2_ticket_2" model="event.event.ticket">
|
||||
<field name="name">VIP</field>
|
||||
<field name="event_id" ref="event.event_2"/>
|
||||
<field name="end_sale_date" eval="(DateTime.today() + timedelta(60)).strftime('%Y-%m-%d')"/>
|
||||
<field name="seats_max">5</field>
|
||||
</record>
|
||||
|
||||
<record id="event.event_3" model="event.event">
|
||||
<field name="name">Live Music Festival</field>
|
||||
@@ -66,15 +102,26 @@
|
||||
<field name="stage_id" ref="event_stage_announced"/>
|
||||
</record>
|
||||
|
||||
<!-- EVENT_4: very limited, intended to test seats reservation -->
|
||||
<record id="event.event_4" model="event.event">
|
||||
<field name="name">Business workshops</field>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field eval="(DateTime.today()+ timedelta(days=50)).strftime('%Y-%m-%d 18:00:00')" name="date_begin"/>
|
||||
<field eval="(DateTime.today()+ timedelta(days=50)).strftime('%Y-%m-%d 22:30:00')" name="date_end"/>
|
||||
<field name="event_type_id" ref="event_type_1"/>
|
||||
<field name="date_begin" eval="(DateTime.today()+ timedelta(days=50)).strftime('%Y-%m-%d 18:00:00')"/>
|
||||
<field name="date_end" eval="(DateTime.today()+ timedelta(days=50)).strftime('%Y-%m-%d 22:30:00')"/>
|
||||
<field name="seats_availability">limited</field>
|
||||
<field name="seats_max">4</field>
|
||||
<field name="address_id" ref="base.res_partner_4"/>
|
||||
<field name="date_tz">US/Pacific</field>
|
||||
<field name="event_type_id" ref="event_type_1"/>
|
||||
<field name="auto_confirm" eval="True"/>
|
||||
<field name="stage_id" ref="event_stage_announced"/>
|
||||
</record>
|
||||
<record id="event_4_ticket_0" model="event.event.ticket">
|
||||
<field name="name">General Admission</field>
|
||||
<field name="event_id" ref="event.event_4"/>
|
||||
<field name="end_sale_date" eval="(DateTime.today() + timedelta(30)).strftime('%Y-%m-%d')"/>
|
||||
<field name="seats_max">4</field>
|
||||
</record>
|
||||
|
||||
<record id="event.event_5" model="event.event">
|
||||
<field name="name">Hockey Tournament</field>
|
||||
@@ -94,13 +141,5 @@
|
||||
<field name="address_id" ref="base.res_partner_1"/>
|
||||
</record>
|
||||
|
||||
<!-- Attendee -->
|
||||
<record id="reg_2_0" model="event.registration">
|
||||
<field name="name">Camptocamp</field>
|
||||
<field name="email">odoo@camptocamp.com</field>
|
||||
<field name="phone">+41 21 619 10 04 </field>
|
||||
<field name="event_id" ref="event_2"/>
|
||||
<field name="partner_id" ref="base.res_partner_12"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo><data>
|
||||
|
||||
<record id="event_registration_0_0" model="event.registration">
|
||||
<field name="event_id" ref="event.event_0"/>
|
||||
<field name="event_ticket_id" ref="event.event_0_ticket_1"/>
|
||||
<field name="partner_id" ref="base.res_partner_address_1"/>
|
||||
</record>
|
||||
<record id="event_registration_0_1" model="event.registration">
|
||||
<field name="event_id" ref="event.event_0"/>
|
||||
<field name="event_ticket_id" ref="event.event_0_ticket_1"/>
|
||||
<field name="partner_id" ref="base.res_partner_address_2"/>
|
||||
</record>
|
||||
<record id="event_registration_0_2" model="event.registration">
|
||||
<field name="event_id" ref="event.event_0"/>
|
||||
<field name="event_ticket_id" ref="event.event_0_ticket_0"/>
|
||||
<field name="name">Tucker Carlson</field>
|
||||
<field name="name">tuck@test.example.com</field>
|
||||
<field name="partner_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="event_registration_4_0" model="event.registration">
|
||||
<field name="event_id" ref="event.event_4"/>
|
||||
<field name="event_ticket_id" ref="event.event_4_ticket_0"/>
|
||||
<field name="partner_id" ref="base.res_partner_address_7"/>
|
||||
</record>
|
||||
<record id="event_registration_4_1" model="event.registration">
|
||||
<field name="event_id" ref="event.event_4"/>
|
||||
<field name="event_ticket_id" ref="event.event_4_ticket_0"/>
|
||||
<field name="partner_id" ref="base.res_partner_address_13"/>
|
||||
</record>
|
||||
<record id="event_registration_4_2" model="event.registration">
|
||||
<field name="event_id" ref="event.event_4"/>
|
||||
<field name="event_ticket_id" ref="event.event_4_ticket_0"/>
|
||||
<field name="partner_id" ref="base.res_partner_address_14"/>
|
||||
</record>
|
||||
|
||||
<function model="event.registration"
|
||||
name="action_confirm"
|
||||
eval="[[ref('event_registration_0_0'), ref('event_registration_0_1'),
|
||||
ref('event_registration_4_0'), ref('event_registration_4_1'), ref('event_registration_4_2')]]"
|
||||
/>
|
||||
|
||||
</data></odoo>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<record id="base.user_demo" model="res.users">
|
||||
<field name="groups_id" eval="[(4, ref('event.group_event_user'))]"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -5,6 +5,7 @@ from . import event_event
|
||||
from . import event_mail
|
||||
from . import event_registration
|
||||
from . import event_stage
|
||||
from . import event_ticket
|
||||
from . import res_config_settings
|
||||
from . import res_partner
|
||||
from . import ir_autovacuum
|
||||
|
||||
@@ -41,6 +41,12 @@ class EventType(models.Model):
|
||||
|
||||
name = fields.Char('Event Category', required=True, translate=True)
|
||||
sequence = fields.Integer()
|
||||
# tickets
|
||||
use_ticket = fields.Boolean('Ticketing')
|
||||
event_type_ticket_ids = fields.One2many(
|
||||
'event.type.ticket', 'event_type_id',
|
||||
string='Tickets', compute='_compute_event_type_ticket_ids',
|
||||
readonly=False, store=True)
|
||||
# registration
|
||||
has_seats_limitation = fields.Boolean('Limited Seats')
|
||||
default_registration_min = fields.Integer(
|
||||
@@ -69,6 +75,16 @@ class EventType(models.Model):
|
||||
copy=False,
|
||||
default=lambda self: self._get_default_event_type_mail_ids())
|
||||
|
||||
@api.depends('use_ticket')
|
||||
def _compute_event_type_ticket_ids(self):
|
||||
for template in self:
|
||||
if not template.use_ticket:
|
||||
template.event_type_ticket_ids = [(5, 0)]
|
||||
elif not template.event_type_ticket_ids:
|
||||
template.event_type_ticket_ids = [(0, 0, {
|
||||
'name': _('Registration'),
|
||||
})]
|
||||
|
||||
@api.onchange('has_seats_limitation')
|
||||
def _onchange_has_seats_limitation(self):
|
||||
if not self.has_seats_limitation:
|
||||
@@ -143,6 +159,9 @@ class EventEvent(models.Model):
|
||||
auto_confirm = fields.Boolean(string='Autoconfirm Registrations')
|
||||
registration_ids = fields.One2many('event.registration', 'event_id', string='Attendees')
|
||||
event_registrations_open = fields.Boolean('Registration open', compute='_compute_event_registrations_open')
|
||||
event_ticket_ids = fields.One2many(
|
||||
'event.event.ticket', 'event_id', string='Event Ticket',
|
||||
copy=True)
|
||||
# Date fields
|
||||
date_tz = fields.Selection(
|
||||
_tz_get, string='Timezone', required=True,
|
||||
@@ -152,6 +171,7 @@ class EventEvent(models.Model):
|
||||
date_begin_located = fields.Char(string='Start Date Located', compute='_compute_date_begin_tz')
|
||||
date_end_located = fields.Char(string='End Date Located', compute='_compute_date_end_tz')
|
||||
is_one_day = fields.Boolean(compute='_compute_field_is_one_day')
|
||||
start_sale_date = fields.Date('Start sale date', compute='_compute_start_sale_date')
|
||||
# Location and communication
|
||||
is_online = fields.Boolean('Online Event')
|
||||
address_id = fields.Many2one(
|
||||
@@ -196,10 +216,12 @@ class EventEvent(models.Model):
|
||||
event.seats_available = event.seats_max - (event.seats_reserved + event.seats_used)
|
||||
event.seats_expected = event.seats_unconfirmed + event.seats_reserved + event.seats_used
|
||||
|
||||
@api.depends('date_end', 'seats_available', 'seats_availability')
|
||||
@api.depends('date_end', 'seats_available', 'seats_availability', 'event_ticket_ids.sale_available')
|
||||
def _compute_event_registrations_open(self):
|
||||
for event in self:
|
||||
event.event_registrations_open = event.date_end > fields.Datetime.now() and (event.seats_available or event.seats_availability == 'unlimited')
|
||||
event.event_registrations_open = (event.date_end > fields.Datetime.now()) and \
|
||||
(event.seats_available or event.seats_availability == 'unlimited') and \
|
||||
(not event.event_ticket_ids or any(ticket.sale_available for ticket in event.event_ticket_ids))
|
||||
|
||||
@api.depends('stage_id', 'kanban_state')
|
||||
def _compute_kanban_state_label(self):
|
||||
@@ -239,6 +261,12 @@ class EventEvent(models.Model):
|
||||
end_tz = fields.Datetime.context_timestamp(event, event.date_end)
|
||||
event.is_one_day = (begin_tz.date() == end_tz.date())
|
||||
|
||||
@api.depends('event_ticket_ids.start_sale_date')
|
||||
def _compute_start_sale_date(self):
|
||||
for event in self:
|
||||
start_dates = [ticket.start_sale_date for ticket in event.event_ticket_ids if ticket.start_sale_date]
|
||||
event.start_sale_date = min(start_dates) if start_dates else False
|
||||
|
||||
@api.onchange('is_online')
|
||||
def _onchange_is_online(self):
|
||||
if self.is_online:
|
||||
@@ -271,6 +299,18 @@ class EventEvent(models.Model):
|
||||
})
|
||||
for line in self.event_type_id.event_type_mail_ids]
|
||||
|
||||
# compute tickets information
|
||||
if self.event_type_id.use_ticket:
|
||||
all_ticket_values = []
|
||||
for ticket in self.event_type_id.event_type_ticket_ids:
|
||||
ticket_vals = dict(
|
||||
(attribute_name, ticket[attribute_name] if not isinstance(ticket[attribute_name], models.BaseModel) else ticket[attribute_name].id)
|
||||
for attribute_name in self.env['event.type.ticket']._get_event_ticket_fields_whitelist()
|
||||
)
|
||||
all_ticket_values.append(ticket_vals)
|
||||
|
||||
self.event_ticket_ids = [(5, 0, 0)] + [(0, 0, item) for item in all_ticket_values]
|
||||
|
||||
@api.constrains('seats_min', 'seats_max', 'seats_availability')
|
||||
def _check_seats_min_max(self):
|
||||
if any(event.seats_availability == 'limited' and event.seats_min > event.seats_max for event in self):
|
||||
|
||||
@@ -22,6 +22,9 @@ class EventRegistration(models.Model):
|
||||
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,
|
||||
states={'draft': [('readonly', False)]})
|
||||
# attendee
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner', string='Contact',
|
||||
@@ -43,58 +46,71 @@ class EventRegistration(models.Model):
|
||||
('open', 'Confirmed'), ('done', 'Attended')],
|
||||
string='Status', default='draft', readonly=True, copy=False, tracking=True)
|
||||
|
||||
@api.onchange('event_id')
|
||||
def _onchange_event_id(self):
|
||||
# We reset the ticket when keeping it would lead to an inconstitent state.
|
||||
if self.event_ticket_id and (not self.event_id or self.event_id != self.event_ticket_id.event_id):
|
||||
self.event_ticket_id = None
|
||||
|
||||
@api.constrains('event_id', 'state')
|
||||
def _check_seats_limit(self):
|
||||
for registration in self:
|
||||
if registration.event_id.seats_availability == '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.'))
|
||||
|
||||
def _check_auto_confirmation(self):
|
||||
if self._context.get('registration_force_draft'):
|
||||
return False
|
||||
if any(not registration.event_id.auto_confirm or
|
||||
(not registration.event_id.seats_available and registration.event_id.seats_availability == 'limited') for registration in self):
|
||||
return False
|
||||
return True
|
||||
@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.onchange('partner_id')
|
||||
def _onchange_partner(self):
|
||||
if self.partner_id:
|
||||
self.update(self._synchronize_partner_values(self.partner_id))
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# CRUD
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
# update missing pieces of information from partner
|
||||
if vals.get('partner_id'):
|
||||
partner_vals = self._synchronize_partner_values(
|
||||
self.env['res.partner'].browse(vals['partner_id'])
|
||||
)
|
||||
vals = dict(partner_vals, **vals)
|
||||
|
||||
registration = super(EventRegistration, self).create(vals)
|
||||
if registration._check_auto_confirmation():
|
||||
registration.sudo().action_confirm()
|
||||
|
||||
return registration
|
||||
|
||||
def write(self, vals):
|
||||
if vals.get('state') == 'done' and 'date_closed' not in vals:
|
||||
vals['date_closed'] = fields.Datetime.now()
|
||||
|
||||
ret = super(EventRegistration, self).write(vals)
|
||||
|
||||
# update missing pieces of information from partner
|
||||
if vals.get('partner_id'):
|
||||
partner_vals = self._synchronize_partner_values(
|
||||
self.env['res.partner'].browse(vals['partner_id'])
|
||||
)
|
||||
for registration in self:
|
||||
partner_info = dict((key, val) for key, val in partner_vals.items() if not registration[key])
|
||||
if partner_info:
|
||||
registration.write(partner_info)
|
||||
|
||||
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()
|
||||
elif vals.get('state') == 'done' and 'date_closed' not in vals:
|
||||
vals['date_closed'] = fields.Datetime.now()
|
||||
|
||||
return ret
|
||||
|
||||
@api.model
|
||||
def _prepare_attendee_values(self, registration):
|
||||
""" Method preparing the values to create new attendees based on a
|
||||
sales order line. It takes some registration data (dict-based) that are
|
||||
optional values coming from an external input like a web page. This method
|
||||
is meant to be inherited in various addons that sell events. """
|
||||
partner_id = registration.pop('partner_id', self.env.user.partner_id)
|
||||
event_id = registration.pop('event_id', False)
|
||||
data = {
|
||||
'name': registration.get('name', partner_id.name),
|
||||
'phone': registration.get('phone', partner_id.phone),
|
||||
'mobile': registration.get('mobile', partner_id.mobile),
|
||||
'email': registration.get('email', partner_id.email),
|
||||
'partner_id': partner_id.id,
|
||||
'event_id': event_id and event_id.id or False,
|
||||
}
|
||||
data.update({key: value for key, value in registration.items() if key in self._fields})
|
||||
return data
|
||||
|
||||
def name_get(self):
|
||||
""" Custom name_get implementation to better differentiate registrations
|
||||
linked to a given partner but with different name (one partner buying
|
||||
@@ -116,6 +132,26 @@ class EventRegistration(models.Model):
|
||||
ret_list.append((registration.id, name))
|
||||
return ret_list
|
||||
|
||||
def _check_auto_confirmation(self):
|
||||
if self._context.get('registration_force_draft'):
|
||||
return False
|
||||
if any(not registration.event_id.auto_confirm or
|
||||
(not registration.event_id.seats_available and registration.event_id.seats_availability == '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'})
|
||||
|
||||
@@ -129,17 +165,6 @@ class EventRegistration(models.Model):
|
||||
def action_cancel(self):
|
||||
self.write({'state': 'cancel'})
|
||||
|
||||
@api.onchange('partner_id')
|
||||
def _onchange_partner(self):
|
||||
if self.partner_id:
|
||||
contact_id = self.partner_id.address_get().get('contact', False)
|
||||
if contact_id:
|
||||
contact = self.env['res.partner'].browse(contact_id)
|
||||
self.name = contact.name or self.name
|
||||
self.email = contact.email or self.email
|
||||
self.phone = contact.phone or self.phone
|
||||
self.mobile = contact.mobile or self.mobile
|
||||
|
||||
def _message_get_suggested_recipients(self):
|
||||
recipients = super(EventRegistration, self)._message_get_suggested_recipients()
|
||||
public_users = self.env['res.users'].sudo()
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
# -*- 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_availability = fields.Selection([
|
||||
('limited', 'Limited'), ('unlimited', 'Unlimited')], string='Seats Limit',
|
||||
readonly=True, store=True, compute='_compute_seats_availability')
|
||||
seats_max = fields.Integer(
|
||||
string='Maximum Seats',
|
||||
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_availability(self):
|
||||
for ticket in self:
|
||||
ticket.seats_availability = 'limited' if ticket.seats_max else 'unlimited'
|
||||
|
||||
@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_date = fields.Date(string="Registration Start")
|
||||
end_sale_date = fields.Date(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')
|
||||
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_date', 'event_id.date_tz')
|
||||
def _compute_is_expired(self):
|
||||
for ticket in self:
|
||||
if ticket.end_sale_date:
|
||||
current_date = fields.Date.context_today(ticket.with_context(tz=ticket._get_ticket_tz()))
|
||||
ticket.is_expired = ticket.end_sale_date < current_date
|
||||
else:
|
||||
ticket.is_expired = False
|
||||
|
||||
@api.depends('start_sale_date', 'end_sale_date', 'event_id.date_tz')
|
||||
def _compute_sale_available(self):
|
||||
for ticket in self:
|
||||
current_date = fields.Date.context_today(ticket.with_context(tz=ticket._get_ticket_tz()))
|
||||
if (ticket.start_sale_date and ticket.start_sale_date > current_date) or \
|
||||
ticket.end_sale_date and ticket.end_sale_date < current_date:
|
||||
ticket.sale_available = False
|
||||
else:
|
||||
ticket.sale_available = True
|
||||
|
||||
@api.depends('seats_max', 'registration_ids.state')
|
||||
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
|
||||
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')
|
||||
GROUP BY event_ticket_id, state
|
||||
"""
|
||||
self.env['event.registration'].flush(['event_id', 'event_ticket_id', 'state'])
|
||||
self.env.cr.execute(query, (tuple(self.ids),))
|
||||
for event_ticket_id, state, num in self.env.cr.fetchall():
|
||||
ticket = self.browse(event_ticket_id)
|
||||
ticket[state_field[state]] += num
|
||||
# compute seats_available
|
||||
for ticket in self:
|
||||
if ticket.seats_max > 0:
|
||||
ticket.seats_available = ticket.seats_max - (ticket.seats_reserved + ticket.seats_used)
|
||||
|
||||
@api.constrains('start_sale_date', 'end_sale_date')
|
||||
def _constrains_dates_coherency(self):
|
||||
for ticket in self:
|
||||
if ticket.start_sale_date and ticket.end_sale_date and ticket.start_sale_date > ticket.end_sale_date:
|
||||
raise UserError(_('The stop date cannot be earlier than the start date.'))
|
||||
|
||||
@api.constrains('seats_available', 'seats_max')
|
||||
def _constrains_seats_available(self):
|
||||
if any(record.seats_max and record.seats_available < 0 for record in self):
|
||||
raise ValidationError(_('No more available seats for this ticket.'))
|
||||
|
||||
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 _get_ticket_tz(self):
|
||||
return self.event_id.date_tz or self.env.user.tz
|
||||
@@ -1,9 +1,13 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_event_type,event.type,model_event_type,event.group_event_user,1,0,0,0
|
||||
access_event_type_manager,event.type manager,model_event_type,event.group_event_manager,1,1,1,1
|
||||
access_event_type_ticket,event.type.ticket.user,model_event_type_ticket,event.group_event_user,1,0,0,0
|
||||
access_event_type_ticket_manager,event.type.ticket.manager,model_event_type_ticket,event.group_event_manager,1,1,1,1
|
||||
access_event_event_portal,event.event.portal,model_event_event,,1,0,0,0
|
||||
access_event_event_user,event.event.user,model_event_event,event.group_event_user,1,0,0,0
|
||||
access_event_event_manager,event.event.manager,model_event_event,event.group_event_manager,1,1,1,1
|
||||
access_event_event_ticket_user,event.event.ticket.user,model_event_event_ticket,event.group_event_user,1,0,0,0
|
||||
access_event_event_ticket_manager,event.event.ticket.manager,model_event_event_ticket,event.group_event_manager,1,1,1,1
|
||||
access_event_registration,event.registration,model_event_registration,event.group_event_user,1,1,1,1
|
||||
access_event_registration_employee,event.registration,model_event_registration,base.group_user,1,0,0,0
|
||||
access_event_registration_portal,event.registration,model_event_registration,base.group_portal,1,0,0,0
|
||||
|
||||
|
@@ -1,7 +1,8 @@
|
||||
.o_kanban_view.o_event_kanban_view {
|
||||
.o_kanban_record {
|
||||
min-height: 140px;
|
||||
}
|
||||
.o_kanban_content {
|
||||
min-height: 130px;
|
||||
|
||||
.o_event_fontweight_500 {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,13 @@ class TestEventCommon(common.SavepointCase):
|
||||
'default_timezone': 'Europe/Paris',
|
||||
'use_hashtag': True,
|
||||
'default_hashtag': 'amazing',
|
||||
'use_ticket': True,
|
||||
'event_type_ticket_ids': [(0, 0, {
|
||||
'name': 'First Ticket',
|
||||
}), (0, 0, {
|
||||
'name': 'Second Ticket',
|
||||
})
|
||||
],
|
||||
'use_mail_schedule': True,
|
||||
'event_type_mail_ids': [
|
||||
(0, 0, { # right at subscription
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import date, datetime, timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.addons.event.tests.common import TestEventCommon
|
||||
from odoo.fields import Datetime as FieldsDatetime
|
||||
from odoo import exceptions
|
||||
from odoo.fields import Datetime as FieldsDatetime, Date as FieldsDate
|
||||
from odoo.tests.common import users
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
@@ -149,6 +150,105 @@ class TestEventData(TestEventCommon):
|
||||
self.assertEqual(event.event_mail_ids.interval_type, 'before_event')
|
||||
self.assertEqual(event.event_mail_ids.template_id, self.env.ref('event.event_reminder'))
|
||||
|
||||
@users('user_eventmanager')
|
||||
def test_event_registrable(self):
|
||||
"""Test if `_compute_event_registrations_open` works properly."""
|
||||
event = self.event_0.with_user(self.env.user)
|
||||
self.assertTrue(event.event_registrations_open)
|
||||
|
||||
# ticket without dates boundaries -> ok
|
||||
ticket = self.env['event.event.ticket'].create({
|
||||
'name': 'TestTicket',
|
||||
'event_id': event.id,
|
||||
})
|
||||
self.assertTrue(event.event_registrations_open)
|
||||
|
||||
# even with valid tickets, date limits registrations
|
||||
event.write({
|
||||
'date_begin': datetime.now() - timedelta(days=3),
|
||||
'date_end': datetime.now() - timedelta(days=1),
|
||||
})
|
||||
self.assertFalse(event.event_registrations_open)
|
||||
|
||||
# no more seats available
|
||||
registration = self.env['event.registration'].create({
|
||||
'name': 'Albert Test',
|
||||
'event_id': event.id,
|
||||
})
|
||||
registration.action_confirm()
|
||||
event.write({
|
||||
'date_end': datetime.now() + timedelta(days=3),
|
||||
'seats_max': 1,
|
||||
'seats_availability': 'limited',
|
||||
})
|
||||
self.assertEqual(event.seats_available, 0)
|
||||
self.assertFalse(event.event_registrations_open)
|
||||
|
||||
# seats available are back
|
||||
registration.unlink()
|
||||
self.assertEqual(event.seats_available, 1)
|
||||
self.assertTrue(event.event_registrations_open)
|
||||
|
||||
# but tickets are expired
|
||||
ticket.write({'end_sale_date': datetime.now() - timedelta(days=2)})
|
||||
self.assertTrue(ticket.is_expired)
|
||||
self.assertFalse(event.event_registrations_open)
|
||||
|
||||
|
||||
class TestEventTicketData(TestEventCommon):
|
||||
|
||||
def setUp(self):
|
||||
super(TestEventTicketData, self).setUp()
|
||||
self.ticket_date_patcher = patch('odoo.addons.event.models.event_ticket.fields.Date', wraps=FieldsDate)
|
||||
self.ticket_date_patcher_mock = self.ticket_date_patcher.start()
|
||||
self.ticket_date_patcher_mock.context_today.return_value = date(2020, 1, 31)
|
||||
|
||||
def tearDown(self):
|
||||
super(TestEventTicketData, self).tearDown()
|
||||
self.ticket_date_patcher.stop()
|
||||
|
||||
@users('user_eventmanager')
|
||||
def test_event_ticket_fields(self):
|
||||
""" Test event ticket fields synchronization """
|
||||
event = self.event_0.with_user(self.env.user)
|
||||
event.write({
|
||||
'event_ticket_ids': [
|
||||
(5, 0),
|
||||
(0, 0, {
|
||||
'name': 'First Ticket',
|
||||
'seats_max': 30,
|
||||
}), (0, 0, { # limited in time, available (01/10 (start) < 01/31 (today) < 02/10 (end))
|
||||
'name': 'Second Ticket',
|
||||
'start_sale_date': date(2020, 1, 10),
|
||||
'end_sale_date': date(2020, 2, 10),
|
||||
})
|
||||
],
|
||||
})
|
||||
first_ticket = event.event_ticket_ids.filtered(lambda t: t.name == 'First Ticket')
|
||||
second_ticket = event.event_ticket_ids.filtered(lambda t: t.name == 'Second Ticket')
|
||||
|
||||
self.assertEqual(first_ticket.seats_availability, 'limited')
|
||||
self.assertTrue(first_ticket.sale_available)
|
||||
self.assertFalse(first_ticket.is_expired)
|
||||
|
||||
self.assertEqual(second_ticket.seats_availability, 'unlimited')
|
||||
self.assertTrue(second_ticket.sale_available)
|
||||
self.assertFalse(second_ticket.is_expired)
|
||||
# sale is ended
|
||||
second_ticket.write({'end_sale_date': date(2020, 1, 20)})
|
||||
self.assertFalse(second_ticket.sale_available)
|
||||
self.assertTrue(second_ticket.is_expired)
|
||||
# sale has not started
|
||||
second_ticket.write({
|
||||
'start_sale_date': date(2020, 2, 10),
|
||||
'end_sale_date': date(2020, 2, 20),
|
||||
})
|
||||
self.assertFalse(second_ticket.sale_available)
|
||||
self.assertFalse(second_ticket.is_expired)
|
||||
# incoherent dates are invalid
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
second_ticket.write({'end_sale_date': date(2020, 1, 20)})
|
||||
|
||||
|
||||
class TestEventTypeData(TestEventCommon):
|
||||
|
||||
@@ -161,11 +261,13 @@ class TestEventTypeData(TestEventCommon):
|
||||
'has_seats_limitation': True,
|
||||
'default_registration_min': 5,
|
||||
'default_registration_max': 30,
|
||||
'use_ticket': True,
|
||||
})
|
||||
event_type._onchange_has_seats_limitation()
|
||||
self.assertTrue(event_type.has_seats_limitation)
|
||||
self.assertEqual(event_type.default_registration_min, 5)
|
||||
self.assertEqual(event_type.default_registration_max, 30)
|
||||
self.assertEqual(event_type.event_type_ticket_ids.mapped('name'), ['Registration'])
|
||||
|
||||
# reset seats limitation
|
||||
event_type.write({'has_seats_limitation': False})
|
||||
@@ -173,3 +275,7 @@ class TestEventTypeData(TestEventCommon):
|
||||
self.assertFalse(event_type.has_seats_limitation)
|
||||
self.assertEqual(event_type.default_registration_min, 0)
|
||||
self.assertEqual(event_type.default_registration_max, 0)
|
||||
|
||||
# reset tickets
|
||||
event_type.write({'use_ticket': False})
|
||||
self.assertEqual(event_type.event_type_ticket_ids, self.env['event.type.ticket'])
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo><data>
|
||||
|
||||
<!-- MAIN MENU -->
|
||||
<menuitem name="Events"
|
||||
id="event_main_menu"
|
||||
sequence="65"
|
||||
groups="event.group_event_user"
|
||||
web_icon="event,static/description/icon.png"/>
|
||||
|
||||
<!-- HEADER: EVENTS -->
|
||||
<menuitem name="Events"
|
||||
id="menu_event_event"
|
||||
sequence="1"
|
||||
parent="event.event_main_menu"
|
||||
groups="event.group_event_user"/>
|
||||
|
||||
<!-- HEADER: REPORTING -->
|
||||
<menuitem name="Reporting"
|
||||
id="menu_reporting_events"
|
||||
sequence="50"
|
||||
parent="event_main_menu"
|
||||
groups="event.group_event_manager"/>
|
||||
|
||||
<!-- HEADER: CONFIGURATION -->
|
||||
<menuitem name="Configuration"
|
||||
id="menu_event_configuration"
|
||||
sequence="99"
|
||||
parent="event_main_menu"
|
||||
groups="event.group_event_user"/>
|
||||
<menuitem name="Event Categories"
|
||||
id="menu_event_type"
|
||||
sequence="1"
|
||||
parent="menu_event_configuration"/>
|
||||
<menuitem name="Mail Schedulers"
|
||||
id="menu_event_mail_schedulers"
|
||||
sequence="10"
|
||||
parent="menu_event_configuration"
|
||||
groups="base.group_no_one"/>
|
||||
|
||||
</data></odoo>
|
||||
@@ -0,0 +1,182 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo><data>
|
||||
|
||||
<!-- EVENT.TYPE.TICKET -->
|
||||
<record id="event_type_ticket_view_tree_from_type" model="ir.ui.view">
|
||||
<field name="name">event.type.ticket.view.tree.from.type</field>
|
||||
<field name="model">event.type.ticket</field>
|
||||
<field name="priority" eval="20"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Event Template Tickets" editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="description"/>
|
||||
<field name="seats_max"/>
|
||||
<field name="seats_availability"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="event_type_ticket_view_form_from_type" model="ir.ui.view">
|
||||
<field name="name">event.type.ticket.view.form.from.type</field>
|
||||
<field name="model">event.type.ticket</field>
|
||||
<field name="priority" eval="20"/>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Event Template Ticket">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="description"/>
|
||||
<field name="seats_availability"/>
|
||||
<field name="seats_max"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="event_type_ticket_view_tree" model="ir.ui.view">
|
||||
<field name="name">event.type.ticket.view.tree</field>
|
||||
<field name="model">event.type.ticket</field>
|
||||
<field name="inherit_id" ref="event_type_ticket_view_tree_from_type"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="priority" eval="10"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//tree" position="attributes">
|
||||
<attribute name="editable"></attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='name']" position="after">
|
||||
<field name="event_type_id"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="event_type_ticket_view_form" model="ir.ui.view">
|
||||
<field name="name">event.type.ticket.view.form</field>
|
||||
<field name="model">event.type.ticket</field>
|
||||
<field name="inherit_id" ref="event_type_ticket_view_form_from_type"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="priority" eval="10"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='name']" position="after">
|
||||
<field name="event_type_id"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- EVENT.TICKET -->
|
||||
<record id="event_event_ticket_view_tree_from_event" model="ir.ui.view">
|
||||
<field name="name">event.event.ticket.view.tree.from.event</field>
|
||||
<field name="model">event.event.ticket</field>
|
||||
<field name="priority" eval="20"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Tickets" editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="description"/>
|
||||
<field name="start_sale_date"/>
|
||||
<field name="end_sale_date"/>
|
||||
<field name="seats_max"/>
|
||||
<field name="seats_reserved"/>
|
||||
<field name="seats_unconfirmed"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="event_event_ticket_view_form_from_event" model="ir.ui.view">
|
||||
<field name="name">event.event.ticket.view.form.from.event</field>
|
||||
<field name="model">event.event.ticket</field>
|
||||
<field name="priority" eval="20"/>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Ticket">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="description"/>
|
||||
<field name="start_sale_date"/>
|
||||
<field name="end_sale_date"/>
|
||||
</group><group>
|
||||
<field name="seats_max"/>
|
||||
<field name="seats_reserved"/>
|
||||
<field name="seats_unconfirmed"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="event_event_ticket_view_kanban_from_event" model="ir.ui.view">
|
||||
<field name="name">event.event.ticket.view.kanban.from.event</field>
|
||||
<field name="model">event.event.ticket</field>
|
||||
<field name="priority" eval="20"/>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="o_kanban_mobile">
|
||||
<field name="name"/>
|
||||
<field name="seats_max"/>
|
||||
<field name="seats_reserved"/>
|
||||
<field name="seats_unconfirmed"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_card oe_kanban_global_click">
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<strong><t t-esc="record.name.value"/></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div><i>
|
||||
<t t-esc="record.seats_reserved.value"/> reserved + <t t-esc="record.seats_reserved.value"/> unconfirmed
|
||||
</i></div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="event_event_ticket_view_tree" model="ir.ui.view">
|
||||
<field name="name">event.event.ticket.view.tree</field>
|
||||
<field name="model">event.event.ticket</field>
|
||||
<field name="inherit_id" ref="event_event_ticket_view_tree_from_event"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="priority" eval="10"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//tree" position="attributes">
|
||||
<attribute name="editable"></attribute>
|
||||
</xpath>
|
||||
<field name="name" position="after">
|
||||
<field name="event_id"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="event_event_ticket_form_view" model="ir.ui.view">
|
||||
<field name="name">event.event.ticket.view.form</field>
|
||||
<field name="model">event.event.ticket</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Event's Ticket">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1><field name="name" placeholder="Event Name"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="event_id"/>
|
||||
<field name="seats_availability"/>
|
||||
<field name="seats_available"/>
|
||||
<field name="start_sale_date"/>
|
||||
<field name="end_sale_date"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="seats_max"/>
|
||||
<field name="seats_reserved"/>
|
||||
<field name="seats_unconfirmed"/>
|
||||
<field name="seats_used"/>
|
||||
<field name="is_expired"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</data></odoo>
|
||||
@@ -2,10 +2,6 @@
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- Main menu items -->
|
||||
<menuitem name="Events" id="event_main_menu" sequence="65" groups="event.group_event_user" web_icon="event,static/description/icon.png"/>
|
||||
<menuitem parent="event_main_menu" id="menu_reporting_events" sequence="99" groups="event.group_event_manager" name="Reporting"/>
|
||||
|
||||
<!-- EVENT.TYPE VIEWS -->
|
||||
<record model="ir.ui.view" id="view_event_type_form">
|
||||
<field name="name">event.type.form</field>
|
||||
@@ -111,6 +107,24 @@
|
||||
</div>
|
||||
<h2>Attendees</h2>
|
||||
<div class="row mt16 o_settings_container" name="event_type_attendees">
|
||||
<div class="col-12 o_setting_box" name="event_type_attendees_tickets">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="use_ticket"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="use_ticket"/>
|
||||
<div class="row mt16" attrs="{'invisible': [('use_ticket', '=', False)]}">
|
||||
<div class="col">
|
||||
<field name="event_type_ticket_ids"
|
||||
class="w-100"
|
||||
context="{
|
||||
'tree_view_ref': 'event.event_type_ticket_view_tree_from_type',
|
||||
'form_view_ref': 'event.event_type_ticket_view_form_from_type'
|
||||
}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box" name="event_type_attendees_auto_confirm">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="auto_confirm"/>
|
||||
@@ -158,9 +172,9 @@
|
||||
<field name="res_model">event.type</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_event_configuration" name="Configuration" parent="event_main_menu"
|
||||
sequence="100"/>
|
||||
<menuitem name="Event Categories" id="menu_event_type" action="action_event_type" parent="menu_event_configuration"/>
|
||||
<record id="menu_event_type" model="ir.ui.menu">
|
||||
<field name="action" ref="event.action_event_type"/>
|
||||
</record>
|
||||
|
||||
<!-- EVENT.REGISTRATION ACTIONS -->
|
||||
<record id="act_event_registration_from_event" model="ir.actions.act_window">
|
||||
@@ -263,6 +277,13 @@
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Tickets" name="tickets">
|
||||
<field name="event_ticket_ids" context="{
|
||||
'default_event_name': name,
|
||||
'tree_view_ref': 'event.event_event_ticket_view_tree_from_event',
|
||||
'form_view_ref': 'event.event_event_ticket_view_form_from_event',
|
||||
'kanban_view_ref': 'event.event_event_ticket_view_kanban_from_event'}" mode="tree,kanban"/>
|
||||
</page>
|
||||
<page string="Communication" name="event_communication">
|
||||
<field name="event_mail_ids">
|
||||
<tree string="Communication" editable="bottom">
|
||||
@@ -348,8 +369,8 @@
|
||||
<field name="activity_state"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="p-0 {{!selection_mode ? 'oe_kanban_color_' + kanban_getcolor(record.color.raw_value) : ''}} oe_kanban_card oe_kanban_global_click">
|
||||
<div class="o_kanban_content p-0 m-0 position-relative row d-flex">
|
||||
<div t-attf-class="d-flex flex-column p-0 {{!selection_mode ? 'oe_kanban_color_' + kanban_getcolor(record.color.raw_value) : ''}} oe_kanban_card oe_kanban_global_click">
|
||||
<div class="o_kanban_content p-0 m-0 position-relative row d-flex flex-fill">
|
||||
<div class="col-3 bg-primary p-2 text-center d-flex flex-column justify-content-center">
|
||||
<div t-esc="record.date_begin.raw_value.getDate()" class="o_event_fontsize_20"/>
|
||||
<div>
|
||||
@@ -475,7 +496,9 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem name="Events" id="menu_event_event" action="action_event_view" parent="event.event_main_menu" />
|
||||
<record id="event.menu_event_event" model="ir.ui.menu">
|
||||
<field name="action" ref="event.action_event_view"/>
|
||||
</record>
|
||||
<menuitem name="Events"
|
||||
id="event_event_menu_pivot_report"
|
||||
action="event_event_action_pivot"
|
||||
@@ -494,7 +517,10 @@
|
||||
<field name="partner_id"/>
|
||||
<field name="name"/>
|
||||
<field name="email" optional="hide"/>
|
||||
<field name="event_id" />
|
||||
<field name="phone" optional="hide"/>
|
||||
<field name="mobile" optional="hide"/>
|
||||
<field name="event_id"/>
|
||||
<field name="event_ticket_id"/>
|
||||
<field name="origin" optional="hide"/>
|
||||
<field name="state" readonly="0"/>
|
||||
<field name="company_id" groups="base.group_multi_company" optional="hide"/>
|
||||
@@ -531,6 +557,13 @@
|
||||
</group>
|
||||
<group string="Event Information" name="event">
|
||||
<field name="event_id" attrs="{'readonly': [('state', '!=', 'draft')]}" options="{'no_create': True}"/>
|
||||
<field name="event_ticket_id"
|
||||
domain="[
|
||||
('event_id', '=', event_id),
|
||||
'|', ('seats_availability', '=', 'unlimited'), ('seats_available', '>', 0)
|
||||
]"
|
||||
attrs="{'invisible': [('event_id', '=', False)]}"
|
||||
options="{'no_open': True, 'no_create': True}"/>
|
||||
<field name="date_open" groups="base.group_no_one"/>
|
||||
<field name="date_closed" groups="base.group_no_one"/>
|
||||
</group>
|
||||
@@ -643,6 +676,7 @@
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Partner" name="partner" domain="[]" context="{'group_by':'partner_id'}"/>
|
||||
<filter string="Event" name="group_event" domain="[]" context="{'group_by':'event_id'}"/>
|
||||
<filter string="Ticket Type" name ="group_event_ticket_id" domain="[]" context="{'group_by': 'event_ticket_id'}"/>
|
||||
<filter string="Status" name="status" domain="[]" context="{'group_by':'state'}"/>
|
||||
<filter string="Registration Date" name="createmonth" domain="[]" context="{'group_by': 'create_date:month'}"/>
|
||||
</group>
|
||||
@@ -726,6 +760,8 @@
|
||||
<field name="res_model">event.mail</field>
|
||||
</record>
|
||||
|
||||
<menuitem name="Mail Schedulers" id="menu_event_mail_schedulers" action="action_event_mail" parent="menu_event_configuration" groups="base.group_no_one"/>
|
||||
<record id="menu_event_mail_schedulers" model="ir.ui.menu">
|
||||
<field name="action" ref="event.action_event_mail"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -1,48 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="event_0_ticket_1" model="event.event.ticket">
|
||||
<field name="name">Standard</field>
|
||||
<field name="event_id" ref="event.event_0"/>
|
||||
<record id="event.event_0_ticket_0" model="event.event.ticket">
|
||||
<field name="product_id" ref="product_product_event"/>
|
||||
<field name="price">0</field>
|
||||
</record>
|
||||
<record id="event.event_0_ticket_1" model="event.event.ticket">
|
||||
<field name="product_id" ref="product_product_event"/>
|
||||
<field name="end_sale_date" eval="(DateTime.today() + timedelta(90)).strftime('%Y-%m-%d')"/>
|
||||
<field name="price">1000.0</field>
|
||||
<field name="seats_max">50</field>
|
||||
</record>
|
||||
|
||||
<record id="event_0_ticket_2" model="event.event.ticket">
|
||||
<field name="name">VIP</field>
|
||||
<field name="event_id" ref="event.event_0"/>
|
||||
<record id="event.event_0_ticket_2" model="event.event.ticket">
|
||||
<field name="product_id" ref="product_product_event"/>
|
||||
<field name="end_sale_date" eval="(DateTime.today() + timedelta(60)).strftime('%Y-%m-%d')"/>
|
||||
<field name="price">1500.0</field>
|
||||
<field name="seats_max">10</field>
|
||||
</record>
|
||||
|
||||
<record id="event_4_ticket_1" model="event.event.ticket">
|
||||
<field name="name">General Admission</field>
|
||||
<field name="event_id" ref="event.event_4"/>
|
||||
<record id="event.event_2_ticket_1" model="event.event.ticket">
|
||||
<field name="product_id" ref="product_product_event"/>
|
||||
<field name="price">1000.0</field>
|
||||
</record>
|
||||
<record id="event.event_2_ticket_2" model="event.event.ticket">
|
||||
<field name="product_id" ref="product_product_event"/>
|
||||
<field name="price">1500.0</field>
|
||||
</record>
|
||||
|
||||
<record id="event.event_4_ticket_0" model="event.event.ticket">
|
||||
<field name="product_id" ref="product_product_event"/>
|
||||
<field name="end_sale_date" eval="(DateTime.today() + timedelta(30)).strftime('%Y-%m-%d')"/>
|
||||
<field name="price">99.0</field>
|
||||
<field name="seats_max">100</field>
|
||||
</record>
|
||||
|
||||
<record id="event_2_ticket_1" model="event.event.ticket">
|
||||
<field name="name">Standard</field>
|
||||
<field name="event_id" ref="event.event_2"/>
|
||||
<field name="product_id" ref="product_product_event"/>
|
||||
<field name="end_sale_date" eval="(DateTime.today() + timedelta(90)).strftime('%Y-%m-%d')"/>
|
||||
<field name="price">1000.0</field>
|
||||
<field name="seats_max">50</field>
|
||||
</record>
|
||||
|
||||
<record id="event_2_ticket_2" model="event.event.ticket">
|
||||
<field name="name">VIP</field>
|
||||
<field name="event_id" ref="event.event_2"/>
|
||||
<field name="product_id" ref="product_product_event"/>
|
||||
<field name="end_sale_date" eval="(DateTime.today() + timedelta(60)).strftime('%Y-%m-%d')"/>
|
||||
<field name="price">1500.0</field>
|
||||
<field name="seats_max">5</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -13,17 +13,11 @@
|
||||
<field name="uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit"/>
|
||||
<field name="name">Event Registration</field>
|
||||
<field name="description_sale" eval="False"/>
|
||||
<field name="default_code">EVENT_REG</field>
|
||||
<field name="categ_id" ref="event_sale.product_category_events"/>
|
||||
<field name="type">service</field>
|
||||
</record>
|
||||
|
||||
<record id="event_type_data_sale" model="event.type">
|
||||
<field name="name">Ticketing</field>
|
||||
<field name="auto_confirm" eval="False"/>
|
||||
<field name="use_mail_schedule" eval="True"/>
|
||||
<field name="use_ticketing" eval="True"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
|
||||
@@ -1,42 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class EventType(models.Model):
|
||||
_inherit = 'event.type'
|
||||
|
||||
@api.model
|
||||
def _get_default_event_ticket_ids(self):
|
||||
product = self.env.ref('event_sale.product_product_event', raise_if_not_found=False)
|
||||
if not product:
|
||||
return False
|
||||
return [(0, 0, {
|
||||
'name': _('Registration'),
|
||||
'product_id': product.id,
|
||||
'price': 0,
|
||||
})]
|
||||
|
||||
use_ticketing = fields.Boolean('Ticketing')
|
||||
event_ticket_ids = fields.One2many(
|
||||
'event.event.ticket', 'event_type_id',
|
||||
string='Tickets', default=_get_default_event_ticket_ids)
|
||||
|
||||
@api.onchange('name')
|
||||
def _onchange_name(self):
|
||||
if self.name:
|
||||
self.event_ticket_ids.filtered(lambda ticket: ticket.name == _('Registration')).update({
|
||||
'name': _('Registration for %s') % self.name
|
||||
})
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class Event(models.Model):
|
||||
_inherit = 'event.event'
|
||||
|
||||
event_ticket_ids = fields.One2many(
|
||||
'event.event.ticket', 'event_id', string='Event Ticket',
|
||||
copy=True)
|
||||
sale_order_lines_ids = fields.One2many(
|
||||
'sale.order.line', 'event_id',
|
||||
string='All sale order lines pointing to this event')
|
||||
@@ -44,25 +14,6 @@ class Event(models.Model):
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency', string='Currency',
|
||||
related='company_id.currency_id', readonly=True)
|
||||
start_sale_date = fields.Date('Start sale date', compute='_compute_start_sale_date')
|
||||
|
||||
@api.onchange('event_type_id')
|
||||
def _onchange_type(self):
|
||||
super(Event, self)._onchange_type()
|
||||
if self.event_type_id.use_ticketing:
|
||||
self.event_ticket_ids = [(5, 0, 0)] + [
|
||||
(0, 0, {
|
||||
'name': self.name and _('Registration for %s') % self.name or ticket.name,
|
||||
'product_id': ticket.product_id.id,
|
||||
'price': ticket.price,
|
||||
})
|
||||
for ticket in self.event_type_id.event_ticket_ids]
|
||||
|
||||
@api.depends('event_ticket_ids.start_sale_date')
|
||||
def _compute_start_sale_date(self):
|
||||
for event in self:
|
||||
start_dates = [ticket.start_sale_date for ticket in event.event_ticket_ids if ticket.start_sale_date]
|
||||
event.start_sale_date = min(start_dates) if start_dates else False
|
||||
|
||||
@api.depends('company_id.currency_id',
|
||||
'sale_order_lines_ids.price_subtotal', 'sale_order_lines_ids.currency_id',
|
||||
@@ -78,12 +29,6 @@ class Event(models.Model):
|
||||
for sale_order_line_id in event.sale_order_lines_ids
|
||||
])
|
||||
|
||||
@api.depends('event_ticket_ids.sale_available')
|
||||
def _compute_event_registrations_open(self):
|
||||
non_open_events = self.filtered(lambda event: not any(event.event_ticket_ids.mapped('sale_available')))
|
||||
non_open_events.event_registrations_open = False
|
||||
super(Event, self - non_open_events)._compute_event_registrations_open()
|
||||
|
||||
def action_view_linked_orders(self):
|
||||
""" Redirects to the orders linked to the current events """
|
||||
sale_order_action = self.env.ref('sale.action_orders').read()[0]
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
|
||||
class EventRegistration(models.Model):
|
||||
_inherit = 'event.registration'
|
||||
|
||||
event_ticket_id = fields.Many2one('event.event.ticket', string='Event Ticket', readonly=True, states={'draft': [('readonly', False)]})
|
||||
# in addition to origin generic fields, add real relational fields to correctly
|
||||
# handle attendees linked to sales orders and their lines
|
||||
# TDE FIXME: maybe add an onchange on sale_order_id + origin
|
||||
@@ -19,18 +17,6 @@ class EventRegistration(models.Model):
|
||||
source_id = fields.Many2one('utm.source', 'Source', related="sale_order_id.source_id", store=True)
|
||||
medium_id = fields.Many2one('utm.medium', 'Medium', related="sale_order_id.medium_id", store=True)
|
||||
|
||||
@api.onchange('event_id')
|
||||
def _onchange_event_id(self):
|
||||
# We reset the ticket when keeping it would lead to an inconstitent state.
|
||||
if self.event_ticket_id and (not self.event_id or self.event_id != self.event_ticket_id.event_id):
|
||||
self.event_ticket_id = None
|
||||
|
||||
@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'))
|
||||
|
||||
def _check_auto_confirmation(self):
|
||||
res = super(EventRegistration, self)._check_auto_confirmation()
|
||||
if res:
|
||||
@@ -41,29 +27,38 @@ class EventRegistration(models.Model):
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
if vals.get('sale_order_line_id'):
|
||||
so_line_vals = self._synchronize_so_line_values(
|
||||
self.env['sale.order.line'].browse(vals['sale_order_line_id'])
|
||||
)
|
||||
vals.update(so_line_vals)
|
||||
res = super(EventRegistration, self).create(vals)
|
||||
if res.origin or res.sale_order_id:
|
||||
res.message_post_with_view('mail.message_origin_link',
|
||||
res.message_post_with_view(
|
||||
'mail.message_origin_link',
|
||||
values={'self': res, 'origin': res.sale_order_id},
|
||||
subtype_id=self.env.ref('mail.mt_note').id)
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _prepare_attendee_values(self, registration):
|
||||
""" Override to add sale related stuff """
|
||||
line_id = registration.get('sale_order_line_id')
|
||||
if line_id:
|
||||
registration.setdefault('partner_id', line_id.order_id.partner_id)
|
||||
att_data = super(EventRegistration, self)._prepare_attendee_values(registration)
|
||||
if line_id and line_id.event_ticket_id.sale_available:
|
||||
att_data.update({
|
||||
'event_id': line_id.event_id.id,
|
||||
'event_ticket_id': line_id.event_ticket_id.id,
|
||||
'origin': line_id.order_id.name,
|
||||
'sale_order_id': line_id.order_id.id,
|
||||
'sale_order_line_id': line_id.id,
|
||||
})
|
||||
return att_data
|
||||
def write(self, vals):
|
||||
if vals.get('sale_order_line_id'):
|
||||
so_line_vals = self._synchronize_so_line_values(
|
||||
self.env['sale.order.line'].browse(vals['sale_order_line_id'])
|
||||
)
|
||||
vals.update(so_line_vals)
|
||||
return super(EventRegistration, self).write(vals)
|
||||
|
||||
def _synchronize_so_line_values(self, so_line):
|
||||
if so_line:
|
||||
return {
|
||||
'partner_id': so_line.order_id.partner_id.id,
|
||||
'event_id': so_line.event_id.id,
|
||||
'event_ticket_id': so_line.event_ticket_id.id,
|
||||
'origin': so_line.order_id.name,
|
||||
'sale_order_id': so_line.order_id.id,
|
||||
'sale_order_line_id': so_line.id,
|
||||
}
|
||||
return {}
|
||||
|
||||
def summary(self):
|
||||
res = super(EventRegistration, self).summary()
|
||||
|
||||
@@ -1,145 +1,105 @@
|
||||
# -*- 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
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventTicket(models.Model):
|
||||
_name = 'event.event.ticket'
|
||||
_description = 'Event Ticket'
|
||||
class EventTemplateTicket(models.Model):
|
||||
_inherit = 'event.type.ticket'
|
||||
|
||||
def _default_product_id(self):
|
||||
return self.env.ref('event_sale.product_product_event', raise_if_not_found=False)
|
||||
|
||||
name = fields.Char(string='Name', required=True, translate=True)
|
||||
event_type_id = fields.Many2one('event.type', string='Event Category', ondelete='cascade')
|
||||
event_id = fields.Many2one('event.event', string="Event", ondelete='cascade')
|
||||
company_id = fields.Many2one('res.company', related='event_id.company_id')
|
||||
# product
|
||||
product_id = fields.Many2one('product.product', string='Product',
|
||||
required=True, domain=[("event_ok", "=", True)],
|
||||
product_id = fields.Many2one(
|
||||
'product.product', string='Product', required=True, domain=[("event_ok", "=", True)],
|
||||
default=_default_product_id)
|
||||
price = fields.Float(string='Price', digits='Product Price')
|
||||
price_reduce = fields.Float(string="Price Reduce", compute="_compute_price_reduce", digits='Product Price')
|
||||
price_reduce_taxinc = fields.Float(compute='_get_price_reduce_tax', string='Price Reduce Tax inc')
|
||||
# sale
|
||||
start_sale_date = fields.Date(string="Sales Start")
|
||||
end_sale_date = fields.Date(string="Sales End")
|
||||
is_expired = fields.Boolean(string='Is Expired', compute='_compute_is_expired')
|
||||
sale_available = fields.Boolean(string='Is Available', compute='_compute_sale_available')
|
||||
registration_ids = fields.One2many('event.registration', 'event_ticket_id', string='Registrations')
|
||||
# seats fields
|
||||
seats_availability = fields.Selection([('limited', 'Limited'), ('unlimited', 'Unlimited')],
|
||||
string='Available Seat', required=True, store=True, compute='_compute_seats', default="limited")
|
||||
seats_max = fields.Integer(string='Maximum Available Seats',
|
||||
help="Define the number of available tickets. If you have too much registrations you will "
|
||||
"not be able to sell tickets anymore. Set 0 to ignore this rule set as unlimited.")
|
||||
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 Seat Reservations', compute='_compute_seats', store=True)
|
||||
seats_used = fields.Integer(compute='_compute_seats', store=True)
|
||||
|
||||
def _compute_is_expired(self):
|
||||
for ticket in self:
|
||||
if ticket.end_sale_date:
|
||||
current_date = fields.Date.context_today(ticket.with_context(tz=ticket.event_id.date_tz))
|
||||
ticket.is_expired = ticket.end_sale_date < current_date
|
||||
else:
|
||||
ticket.is_expired = False
|
||||
|
||||
@api.depends('product_id.active', 'start_sale_date', 'end_sale_date')
|
||||
def _compute_sale_available(self):
|
||||
for ticket in self:
|
||||
current_date = fields.Date.context_today(ticket.with_context(tz=ticket.event_id.date_tz))
|
||||
if not ticket.product_id.active:
|
||||
ticket.sale_available = False
|
||||
elif ticket.start_sale_date and ticket.start_sale_date > current_date:
|
||||
ticket.sale_available = False
|
||||
elif ticket.end_sale_date and ticket.end_sale_date < current_date:
|
||||
ticket.sale_available = False
|
||||
else:
|
||||
ticket.sale_available = True
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_price_reduce(self):
|
||||
for record in self:
|
||||
product = record.product_id
|
||||
discount = product.lst_price and (product.lst_price - product.price) / product.lst_price or 0.0
|
||||
record.price_reduce = (1.0 - discount) * record.price
|
||||
|
||||
def _get_price_reduce_tax(self):
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id(self):
|
||||
self.price = self.product_id.list_price or 0
|
||||
if not self.description and self.product_id.description_sale:
|
||||
self.description = self.product_id.description_sale
|
||||
|
||||
def _init_column(self, column_name):
|
||||
if column_name != "product_id":
|
||||
return super(EventTemplateTicket, self)._init_column(column_name)
|
||||
|
||||
# fetch void columns
|
||||
self.env.cr.execute("SELECT id FROM %s WHERE product_id IS NULL" % self._table)
|
||||
ticket_type_ids = self.env.cr.fetchall()
|
||||
if not ticket_type_ids:
|
||||
return
|
||||
|
||||
# update existing columns
|
||||
_logger.debug("Table '%s': setting default value of new column %s to unique values for each row",
|
||||
self._table, column_name)
|
||||
default_event_product = self.env.ref('event_sale.product_product_event', raise_if_not_found=False)
|
||||
if default_event_product:
|
||||
product_id = default_event_product.id
|
||||
else:
|
||||
product_id = self.env['product.product'].create({
|
||||
'name': 'Generic Registration Product',
|
||||
'list_price': 0,
|
||||
'standard_price': 0,
|
||||
'type': 'service',
|
||||
'default_code': 'EVENT_REG',
|
||||
'type': 'service',
|
||||
}).id
|
||||
self.env['ir.model.data'].create({
|
||||
'name': 'product_product_event',
|
||||
'module': 'event_sale',
|
||||
'model': 'product.product',
|
||||
'res_id': product_id,
|
||||
})
|
||||
self.env.cr._obj.execute(
|
||||
f'UPDATE {self._table} SET product_id = %s WHERE id IN %s;',
|
||||
(product_id, tuple(ticket_type_ids))
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _get_event_ticket_fields_whitelist(self):
|
||||
""" Add sale specific fields to copy from template to ticket """
|
||||
return super(EventTemplateTicket, self)._get_event_ticket_fields_whitelist() + ['product_id', 'price']
|
||||
|
||||
|
||||
class EventTicket(models.Model):
|
||||
_inherit = 'event.event.ticket'
|
||||
|
||||
# product
|
||||
price_reduce_taxinc = fields.Float(string='Price Reduce Tax inc', compute='_compute_price_reduce_taxinc')
|
||||
|
||||
def _compute_price_reduce_taxinc(self):
|
||||
for record in self:
|
||||
# sudo necessary here since the field is most probably accessed through the website
|
||||
tax_ids = record.sudo().product_id.taxes_id.filtered(lambda r: r.company_id == record.event_id.company_id)
|
||||
taxes = tax_ids.compute_all(record.price_reduce, record.event_id.company_id.currency_id, 1.0, product=record.product_id)
|
||||
record.price_reduce_taxinc = taxes['total_included']
|
||||
|
||||
@api.depends('seats_max', 'registration_ids.state')
|
||||
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_availability = 'unlimited' if ticket.seats_max == 0 else 'limited'
|
||||
ticket.seats_unconfirmed = ticket.seats_reserved = ticket.seats_used = ticket.seats_available = 0
|
||||
# aggregate registrations by ticket and by state
|
||||
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')
|
||||
GROUP BY event_ticket_id, state
|
||||
"""
|
||||
self.env['event.registration'].flush(['event_id', 'event_ticket_id', 'state'])
|
||||
self.env.cr.execute(query, (tuple(self.ids),))
|
||||
for event_ticket_id, state, num in self.env.cr.fetchall():
|
||||
ticket = self.browse(event_ticket_id)
|
||||
ticket[state_field[state]] += num
|
||||
# compute seats_available
|
||||
for ticket in self:
|
||||
if ticket.seats_max > 0:
|
||||
ticket.seats_available = ticket.seats_max - (ticket.seats_reserved + ticket.seats_used)
|
||||
@api.depends('product_id.active')
|
||||
def _compute_sale_available(self):
|
||||
inactive_product_tickets = self.filtered(lambda ticket: not ticket.product_id.active)
|
||||
for ticket in inactive_product_tickets:
|
||||
ticket.sale_available = False
|
||||
super(EventTicket, self - inactive_product_tickets)._compute_sale_available()
|
||||
|
||||
@api.constrains('registration_ids', 'seats_max')
|
||||
def _check_seats_limit(self):
|
||||
for record in self:
|
||||
if record.seats_max and record.seats_available < 0:
|
||||
raise ValidationError(_('No more available seats for this ticket type.'))
|
||||
|
||||
@api.constrains('event_type_id', 'event_id')
|
||||
def _constrains_event(self):
|
||||
if any(ticket.event_type_id and ticket.event_id for ticket in self):
|
||||
raise UserError(_('Ticket cannot belong to both the event category and the event itself.'))
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id(self):
|
||||
self.price = self.product_id.list_price or 0
|
||||
|
||||
def _get_ticket_multiline_description_sale(self):
|
||||
""" Compute a multiline description of this ticket, in the context of sales.
|
||||
It will often be used as the default description of a sales order line referencing this ticket.
|
||||
|
||||
1. the first line is the ticket name
|
||||
2. the second line is the event name (if it exists, which should be the case with a normal workflow) or the product name (if it exists)
|
||||
|
||||
We decided to ignore entirely the product name and the product description_sale because they are considered to be replaced by the ticket name and event name.
|
||||
-> the workflow of creating a new event also does not lead to filling them correctly, as the product is created through the event interface
|
||||
"""
|
||||
|
||||
name = self.display_name
|
||||
|
||||
if self.event_id:
|
||||
name += '\n' + self.event_id.display_name
|
||||
elif self.product_id:
|
||||
name += '\n' + self.product_id.display_name
|
||||
|
||||
return name
|
||||
|
||||
@api.constrains('start_sale_date', 'end_sale_date')
|
||||
def _check_start_sale_date_and_end_sale_date(self):
|
||||
for ticket in self:
|
||||
if ticket.start_sale_date and ticket.end_sale_date and ticket.start_sale_date > ticket.end_sale_date:
|
||||
raise UserError(_('The stop date cannot be earlier than the start date.'))
|
||||
def _get_ticket_multiline_description(self):
|
||||
""" If people set a description on their product it has more priority
|
||||
than the ticket name itself for the SO description. """
|
||||
self.ensure_one()
|
||||
if self.product_id.description_sale:
|
||||
return '%s\n%s' % (self.product_id.description_sale, self.event_id.display_name)
|
||||
return super(EventTicket, self)._get_ticket_multiline_description()
|
||||
|
||||
@@ -60,13 +60,12 @@ class SaleOrderLine(models.Model):
|
||||
existing_registrations.filtered(lambda self: self.state == 'cancel').action_set_draft()
|
||||
|
||||
for count in range(int(so_line.product_uom_qty) - len(existing_registrations)):
|
||||
registration = {}
|
||||
registration_vals = {}
|
||||
if registration_data:
|
||||
registration = registration_data.pop()
|
||||
registration_vals = registration_data.pop()
|
||||
# TDE CHECK: auto confirmation
|
||||
registration['sale_order_line_id'] = so_line
|
||||
Registration.with_context(registration_force_draft=True).create(
|
||||
Registration._prepare_attendee_values(registration))
|
||||
registration_vals['sale_order_line_id'] = so_line.id
|
||||
Registration.with_context(registration_force_draft=True).create(registration_vals)
|
||||
return True
|
||||
|
||||
@api.onchange('product_id')
|
||||
@@ -103,7 +102,7 @@ class SaleOrderLine(models.Model):
|
||||
lang=self.order_id.partner_id.lang,
|
||||
)
|
||||
|
||||
return ticket._get_ticket_multiline_description_sale() + self._get_sale_order_line_multiline_description_variants()
|
||||
return ticket._get_ticket_multiline_description() + self._get_sale_order_line_multiline_description_variants()
|
||||
else:
|
||||
return super(SaleOrderLine, self).get_sale_order_line_multiline_description_sale(product)
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_event_event_ticket_user,event.event.ticket.user,event_sale.model_event_event_ticket,event.group_event_user,1,0,0,0
|
||||
access_event_event_ticket_admin,event.event.ticket.admin,event_sale.model_event_event_ticket,event.group_event_manager,1,1,1,1
|
||||
access_product_template_event_manager,product.template.event.manager,product.model_product_template,event.group_event_manager,1,1,1,1
|
||||
access_product_product_event_manager,product.product.event.manager,product.model_product_product,event.group_event_manager,1,1,1,1
|
||||
|
||||
|
@@ -12,6 +12,7 @@ class TestEventSaleCommon(TestEventCommon):
|
||||
|
||||
cls.event_product = cls.env['product.product'].create({
|
||||
'name': 'Test Registration Product',
|
||||
'description_sale': 'Mighty Description',
|
||||
'list_price': 10,
|
||||
'event_ok': True,
|
||||
'standard_price': 30.0,
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import date, datetime, timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.addons.event_sale.tests.common import TestEventSaleCommon
|
||||
from odoo.fields import Datetime as FieldsDatetime
|
||||
from odoo.fields import Datetime as FieldsDatetime, Date as FieldsDate
|
||||
from odoo.tests.common import users
|
||||
|
||||
|
||||
class EventSaleTest(TestEventSaleCommon):
|
||||
class TestEventData(TestEventSaleCommon):
|
||||
|
||||
@users('user_eventmanager')
|
||||
def test_event_configuration_from_type(self):
|
||||
@@ -17,11 +18,9 @@ class EventSaleTest(TestEventSaleCommon):
|
||||
event_type = self.event_type_complex.with_user(self.env.user)
|
||||
event_type.write({
|
||||
'use_mail_schedule': False,
|
||||
'use_ticketing': False,
|
||||
'use_ticket': False,
|
||||
})
|
||||
# Event type does not use tickets but data is kept for compatibility and avoid recreating them
|
||||
self.assertEqual(len(event_type.event_ticket_ids), 1)
|
||||
self.assertEqual(event_type.event_ticket_ids.name, 'Registration')
|
||||
self.assertEqual(event_type.event_type_ticket_ids, self.env['event.type.ticket'])
|
||||
|
||||
event = self.env['event.event'].create({
|
||||
'name': 'Event Update Type',
|
||||
@@ -33,53 +32,119 @@ class EventSaleTest(TestEventSaleCommon):
|
||||
self.assertEqual(event.event_ticket_ids, self.env['event.event.ticket'])
|
||||
|
||||
event_type.write({
|
||||
'use_ticketing': True,
|
||||
'event_ticket_ids': [(5, 0), (0, 0, {
|
||||
'use_ticket': True,
|
||||
'event_type_ticket_ids': [(5, 0), (0, 0, {
|
||||
'name': 'First Ticket',
|
||||
'product_id': self.event_product.id,
|
||||
'seats_max': 5,
|
||||
})]
|
||||
})
|
||||
event_type.event_ticket_ids._onchange_product_id()
|
||||
event_type.event_type_ticket_ids._onchange_product_id()
|
||||
self.assertEqual(event_type.event_type_ticket_ids.description, self.event_product.description_sale)
|
||||
|
||||
# synchronize event
|
||||
event._onchange_type()
|
||||
self.assertEqual(event.event_ticket_ids.name, 'Registration for %s' % event.name)
|
||||
self.assertEqual(event.event_ticket_ids.name, event.event_type_id.event_type_ticket_ids.name)
|
||||
self.assertEqual(event.event_ticket_ids.seats_availability, 'limited')
|
||||
self.assertEqual(event.event_ticket_ids.seats_max, 5)
|
||||
self.assertEqual(event.event_ticket_ids.product_id, self.event_product)
|
||||
self.assertEqual(event.event_ticket_ids.price, self.event_product.list_price)
|
||||
self.assertEqual(event.event_ticket_ids.description, self.event_product.description_sale)
|
||||
|
||||
def test_event_registrable(self):
|
||||
"""Test if `_compute_event_registrations_open` works properly."""
|
||||
test_event = self.env['event.event'].create({
|
||||
'name': 'TestEvent',
|
||||
'date_begin': datetime.now() - timedelta(days=5),
|
||||
'date_end': datetime.now() + timedelta(days=5),
|
||||
})
|
||||
test_event_ticket = self.env['event.event.ticket'].create({
|
||||
"""Test if `_compute_event_registrations_open` works properly with additional
|
||||
product active conditions compared to base tests (see event) """
|
||||
event = self.event_0.with_user(self.env.user)
|
||||
self.assertTrue(event.event_registrations_open)
|
||||
|
||||
# ticket without dates boundaries -> ok
|
||||
ticket = self.env['event.event.ticket'].create({
|
||||
'name': 'TestTicket',
|
||||
'event_id': test_event.id,
|
||||
'product_id': self.env['product.product'].search([], limit=1).id,
|
||||
'event_id': event.id,
|
||||
'product_id': self.event_product.id,
|
||||
})
|
||||
self.assertTrue(event.event_registrations_open)
|
||||
|
||||
self.assertEqual(test_event.event_registrations_open, True)
|
||||
# ticket has inactive product -> ko
|
||||
ticket.product_id.action_archive()
|
||||
self.assertFalse(event.event_registrations_open)
|
||||
|
||||
test_event.write({
|
||||
'date_begin': datetime.now() - timedelta(days=5),
|
||||
'date_end': datetime.now() - timedelta(days=1),
|
||||
# at least one valid ticket -> ok is back
|
||||
event_product = self.env['product.product'].create({'name': 'Test Registration Product New',})
|
||||
new_ticket = self.env['event.event.ticket'].create({
|
||||
'name': 'TestTicket 2',
|
||||
'event_id': event.id,
|
||||
'product_id': event_product.id,
|
||||
'end_sale_date': datetime.now() + timedelta(days=2),
|
||||
})
|
||||
self.assertTrue(new_ticket.sale_available)
|
||||
self.assertTrue(event.event_registrations_open)
|
||||
|
||||
test_event.date_end = datetime.now() - timedelta(days=1)
|
||||
self.assertEqual(test_event.event_registrations_open, False)
|
||||
|
||||
test_event.write({
|
||||
'date_begin': datetime.now() - timedelta(days=1),
|
||||
'date_end': datetime.now() + timedelta(days=5),
|
||||
class TestEventTicketData(TestEventSaleCommon):
|
||||
|
||||
def setUp(self):
|
||||
super(TestEventTicketData, self).setUp()
|
||||
self.ticket_date_patcher = patch('odoo.addons.event.models.event_ticket.fields.Date', wraps=FieldsDate)
|
||||
self.ticket_date_patcher_mock = self.ticket_date_patcher.start()
|
||||
self.ticket_date_patcher_mock.context_today.return_value = date(2020, 1, 31)
|
||||
|
||||
def tearDown(self):
|
||||
super(TestEventTicketData, self).tearDown()
|
||||
self.ticket_date_patcher.stop()
|
||||
|
||||
@users('user_eventmanager')
|
||||
def test_event_ticket_fields(self):
|
||||
""" Test event ticket fields synchronization """
|
||||
event = self.event_0.with_user(self.env.user)
|
||||
event.write({
|
||||
'event_ticket_ids': [
|
||||
(5, 0),
|
||||
(0, 0, {
|
||||
'name': 'First Ticket',
|
||||
'product_id': self.event_product.id,
|
||||
'seats_max': 30,
|
||||
}), (0, 0, { # limited in time, available (01/10 (start) < 01/31 (today) < 02/10 (end))
|
||||
'name': 'Second Ticket',
|
||||
'product_id': self.event_product.id,
|
||||
'start_sale_date': date(2020, 1, 10),
|
||||
'end_sale_date': date(2020, 2, 10),
|
||||
})
|
||||
],
|
||||
})
|
||||
first_ticket = event.event_ticket_ids.filtered(lambda t: t.name == 'First Ticket')
|
||||
first_ticket._onchange_product_id()
|
||||
second_ticket = event.event_ticket_ids.filtered(lambda t: t.name == 'Second Ticket')
|
||||
second_ticket._onchange_product_id()
|
||||
# force second ticket price, after calling the onchange
|
||||
second_ticket.write({'price': 8.0})
|
||||
|
||||
test_event.write({'event_ticket_ids': [(5, 0)]})
|
||||
self.assertEqual(test_event.event_registrations_open, False, 'cannot register if no tickets')
|
||||
# price coming from product
|
||||
self.assertEqual(first_ticket.price, self.event_product.list_price)
|
||||
self.assertEqual(second_ticket.price, 8.0)
|
||||
|
||||
test_event_ticket = self.env['event.event.ticket'].create({
|
||||
'name': 'TestTicket',
|
||||
'event_id': test_event.id,
|
||||
'product_id': self.env['product.product'].search([], limit=1).id,
|
||||
# default availability
|
||||
self.assertEqual(first_ticket.seats_availability, 'limited')
|
||||
self.assertTrue(first_ticket.sale_available)
|
||||
self.assertFalse(first_ticket.is_expired)
|
||||
self.assertEqual(second_ticket.seats_availability, 'unlimited')
|
||||
self.assertTrue(second_ticket.sale_available)
|
||||
self.assertFalse(second_ticket.is_expired)
|
||||
|
||||
# product archived
|
||||
self.event_product.action_archive()
|
||||
self.assertFalse(first_ticket.sale_available)
|
||||
self.assertFalse(second_ticket.sale_available)
|
||||
|
||||
# sale is ended
|
||||
self.event_product.action_unarchive()
|
||||
second_ticket.write({'end_sale_date': date(2020, 1, 20)})
|
||||
self.assertFalse(second_ticket.sale_available)
|
||||
self.assertTrue(second_ticket.is_expired)
|
||||
# sale has not started
|
||||
second_ticket.write({
|
||||
'start_sale_date': date(2020, 2, 10),
|
||||
'end_sale_date': date(2020, 2, 20),
|
||||
})
|
||||
test_event_ticket.product_id.active = False
|
||||
self.assertEqual(test_event.event_registrations_open, False, 'cannot register if product linked to the tickets are all archived')
|
||||
self.assertFalse(second_ticket.sale_available)
|
||||
self.assertFalse(second_ticket.is_expired)
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data>
|
||||
|
||||
<record id="view_event_registration_ticket_search" model="ir.ui.view">
|
||||
<field name="name">event.registration.search.inherit</field>
|
||||
<field name="model">event.registration</field>
|
||||
<field name="inherit_id" ref="event.view_registration_search" />
|
||||
<field name="arch" type="xml">
|
||||
<filter name="group_event" position="after">
|
||||
<filter string="Ticket Type" name ="tickettype" domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]" context="{'group_by':'event_ticket_id'}"/>
|
||||
</filter>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_event_registration_ticket_tree" model="ir.ui.view">
|
||||
<field name="name">event.registration.tree.inherit</field>
|
||||
<field name="model">event.registration</field>
|
||||
<field name="inherit_id" ref="event.view_event_registration_tree" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="event_id" position="after">
|
||||
<field name="event_ticket_id"/>
|
||||
<field name="origin"/>
|
||||
</field>
|
||||
</field>
|
||||
@@ -41,17 +29,6 @@
|
||||
<field name="model">event.registration</field>
|
||||
<field name="inherit_id" ref="event.view_event_registration_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="event_id" position="after">
|
||||
<field
|
||||
name="event_ticket_id"
|
||||
domain="[
|
||||
('event_id', '=', event_id),
|
||||
'|', ('seats_availability', '=', 'unlimited'), ('seats_available', '>', 0)
|
||||
]"
|
||||
attrs="{'invisible': [('event_id', '=', False)]}"
|
||||
options="{'no_open': True, 'no_create': True}"
|
||||
/>
|
||||
</field>
|
||||
<group name="event" position="after">
|
||||
<group string="Origin">
|
||||
<field name="sale_order_id"/>
|
||||
|
||||
@@ -1,106 +1,95 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<odoo><data>
|
||||
|
||||
<!-- EVENT.TYPE.TICKET -->
|
||||
<record id="event_type_ticket_view_tree_from_type" model="ir.ui.view">
|
||||
<field name="name">event.type.ticket.view.tree.inherit.sale</field>
|
||||
<field name="model">event.type.ticket</field>
|
||||
<field name="inherit_id" ref="event.event_type_ticket_view_tree_from_type"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="name" position="after">
|
||||
<field name="product_id"/>
|
||||
</field>
|
||||
<field name="description" position="after">
|
||||
<field name="price"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="event_type_ticket_view_form_from_type" model="ir.ui.view">
|
||||
<field name="name">event.type.ticket.view.form.inherit.sale</field>
|
||||
<field name="model">event.type.ticket</field>
|
||||
<field name="inherit_id" ref="event.event_type_ticket_view_form_from_type"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="name" position="after">
|
||||
<field name="product_id"/>
|
||||
</field>
|
||||
<field name="description" position="after">
|
||||
<field name="price"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- EVENT.TICKET -->
|
||||
<record id="event_event_ticket_view_tree_from_event" model="ir.ui.view">
|
||||
<field name="name">event.event.ticket.view.tree.from_event</field>
|
||||
<field name="name">event.event.ticket.view.tree.from.event.inherit.sale</field>
|
||||
<field name="model">event.event.ticket</field>
|
||||
<field name="priority" eval="20"/>
|
||||
<field name="inherit_id" ref="event.event_event_ticket_view_tree_from_event"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Tickets" editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="product_id" context="{'default_event_ok':1}"/>
|
||||
<field name="start_sale_date"/>
|
||||
<field name="end_sale_date"/>
|
||||
<field name="name" position="after">
|
||||
<field name="product_id" context="{'default_event_ok': 1}"/>
|
||||
</field>
|
||||
<field name="description" position="after">
|
||||
<field name="price"/>
|
||||
<field name="seats_max"/>
|
||||
<field name="seats_reserved" readonly="1"/>
|
||||
<field name="seats_unconfirmed" readonly="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="event_event_ticket_view_form_from_event" model="ir.ui.view">
|
||||
<field name="name">event.event.ticket.view.form.from.event</field>
|
||||
<field name="name">event.event.ticket.view.form.from.event.inherit.sale</field>
|
||||
<field name="model">event.event.ticket</field>
|
||||
<field name="priority" eval="20"/>
|
||||
<field name="inherit_id" ref="event.event_event_ticket_view_form_from_event"/>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Ticket">
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="product_id" context="{'default_event_ok':1}"/>
|
||||
<field name="end_sale_date"/>
|
||||
<field name="price"/>
|
||||
</group><group>
|
||||
<field name="seats_max"/>
|
||||
<field name="seats_reserved" readonly="1"/>
|
||||
<field name="seats_unconfirmed" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
</form>
|
||||
<field name="name" position="after">
|
||||
<field name="product_id" context="{'default_event_ok':1}"/>
|
||||
</field>
|
||||
<field name="description" position="after">
|
||||
<field name="price"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="event_event_ticket_view_kanban_from_event" model="ir.ui.view">
|
||||
<field name="name">event.event.ticket.view.kanban.from.event</field>
|
||||
<field name="model">event.event.ticket</field>
|
||||
<field name="priority" eval="20"/>
|
||||
<field name="inherit_id" ref="event.event_event_ticket_view_kanban_from_event"/>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="o_kanban_mobile">
|
||||
<field name="name" position="after">
|
||||
<field name="product_id"/>
|
||||
<field name="name"/>
|
||||
<field name="price"/>
|
||||
<field name="seats_max"/>
|
||||
<field name="seats_reserved"/>
|
||||
<field name="seats_unconfirmed"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_card oe_kanban_global_click">
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<strong><t t-esc="record.name.value"/></strong>
|
||||
</div>
|
||||
<div class="col-4 text-right"><strong> <t t-esc="record.price.value"/></strong></div>
|
||||
</div>
|
||||
<div t-esc="record.product_id.value"/>
|
||||
<div><i>
|
||||
<t t-esc="record.seats_reserved.value"/> reserved + <t t-esc="record.seats_reserved.value"/> unconfirmed
|
||||
</i></div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
<xpath expr="//div[hasclass('col-8')]" position="after">
|
||||
<div class="col-4 text-right"><strong> <t t-esc="record.price.value"/></strong></div>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('row')]" position="after">
|
||||
<div t-esc="record.product_id.value"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="event_event_ticket_view_form" model="ir.ui.view">
|
||||
<field name="name">event.event.ticket.view.form</field>
|
||||
<record id="event_event_ticket_form_view" model="ir.ui.view">
|
||||
<field name="name">event.event.ticket.view.form.inherit.sale</field>
|
||||
<field name="model">event.event.ticket</field>
|
||||
<field name="inherit_id" ref="event.event_event_ticket_form_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Event's Ticket">
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1><field name="name" placeholder="Event Name"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="event_id"/>
|
||||
<field name="seats_availability"/>
|
||||
<field name="seats_available"/>
|
||||
<field name="end_sale_date"/>
|
||||
<field name="price"/>
|
||||
<field name="price_reduce" groups="base.group_no_one"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="seats_max"/>
|
||||
<field name="seats_reserved"/>
|
||||
<field name="seats_unconfirmed"/>
|
||||
<field name="seats_used"/>
|
||||
<field name="product_id"/>
|
||||
<field name="is_expired"/>
|
||||
</group>
|
||||
</group>
|
||||
</form>
|
||||
<field name="end_sale_date" position="after">
|
||||
<field name="price"/>
|
||||
<field name="price_reduce" groups="base.group_no_one"/>
|
||||
</field>
|
||||
<field name="seats_used" position="after">
|
||||
<field name="product_id"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
</data></odoo>
|
||||
|
||||
|
||||
@@ -1,33 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="event_type_view_form_inherit_sale" model="ir.ui.view">
|
||||
<field name="name">event.type.view.form.inherit.sale</field>
|
||||
<field name="model">event.type</field>
|
||||
<field name="inherit_id" ref="event.view_event_type_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<div name="event_type_attendees_auto_confirm" position="after">
|
||||
<div class="col-12 col-lg-12 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="use_ticketing"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="use_ticketing"/>
|
||||
<div class="row mt16" attrs="{'invisible': [('use_ticketing', '=', False)]}">
|
||||
<div class="col-lg-9">
|
||||
<field name="event_ticket_ids">
|
||||
<tree string="Tickets" editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="product_id"/>
|
||||
<field name="price"/>
|
||||
</tree>
|
||||
</field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_event_form_inherit_ticket" model="ir.ui.view">
|
||||
<field name="name">event.form.inherit</field>
|
||||
@@ -49,15 +21,7 @@
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
<page name="event_communication" position="before">
|
||||
<page string="Tickets" name="tickets">
|
||||
<field name="event_ticket_ids" context="{
|
||||
'default_name': name,
|
||||
'tree_view_ref': 'event_sale.event_event_ticket_view_tree_from_event',
|
||||
'form_view_ref': 'event_sale.event_event_ticket_view_form_from_event',
|
||||
'kanban_view_ref': 'event_sale.event_event_ticket_view_kanban_from_event'}" mode="tree,kanban"/>
|
||||
</page>
|
||||
</page>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -254,25 +254,62 @@ class WebsiteEventController(http.Controller):
|
||||
"url": event.website_url})
|
||||
return request.env['ir.ui.view'].render_template("website_event.country_events_list", result)
|
||||
|
||||
def _process_tickets_details(self, data):
|
||||
nb_register = int(data.get('nb_register-0', 0))
|
||||
if nb_register:
|
||||
return [{'id': 0, 'name': 'Registration', 'quantity': nb_register, 'price': 0}]
|
||||
return []
|
||||
def _process_tickets_form(self, event, form_details):
|
||||
""" Process posted data about ticket order. Generic ticket are supported
|
||||
for event without tickets (generic registration).
|
||||
|
||||
:return: list of order per ticket: [{
|
||||
'id': if of ticket if any (0 if no ticket),
|
||||
'ticket': browse record of ticket if any (None if no ticket),
|
||||
'name': ticket name (or generic 'Registration' name if no ticket),
|
||||
'quantity': number of registrations for that ticket,
|
||||
}, {...}]
|
||||
"""
|
||||
ticket_order = {}
|
||||
for key, value in form_details.items():
|
||||
registration_items = key.split('nb_register-')
|
||||
if len(registration_items) != 2:
|
||||
continue
|
||||
ticket_order[int(registration_items[1])] = int(value)
|
||||
|
||||
ticket_dict = dict((ticket.id, ticket) for ticket in request.env['event.event.ticket'].search([
|
||||
('id', 'in', [tid for tid in ticket_order.keys() if tid]),
|
||||
('event_id', '=', event.id)
|
||||
]))
|
||||
|
||||
return [{
|
||||
'id': tid if ticket_dict.get(tid) else 0,
|
||||
'ticket': ticket_dict.get(tid),
|
||||
'name': ticket_dict[tid]['name'] if ticket_dict.get(tid) else _('Registration'),
|
||||
'quantity': count,
|
||||
} for tid, count in ticket_order.items() if count]
|
||||
|
||||
@http.route(['/event/<model("event.event"):event>/registration/new'], type='json', auth="public", methods=['POST'], website=True)
|
||||
def registration_new(self, event, **post):
|
||||
tickets = self._process_tickets_details(post)
|
||||
tickets = self._process_tickets_form(event, post)
|
||||
if not tickets:
|
||||
return False
|
||||
return request.env['ir.ui.view'].render_template("website_event.registration_attendee_details", {'tickets': tickets, 'event': event})
|
||||
|
||||
def _process_registration_details(self, details):
|
||||
''' Process data posted from the attendee details form. '''
|
||||
def _process_attendees_form(self, event, form_details):
|
||||
""" Process data posted from the attendee details form.
|
||||
|
||||
:param details: posted data from frontend registration form, like
|
||||
{'1-name': 'r', '1-email': 'r@r.com', '1-phone': '', '1-event_ticket_id': '1'}
|
||||
"""
|
||||
registration_fields = request.env['event.registration']._fields
|
||||
|
||||
registrations = {}
|
||||
global_values = {}
|
||||
for key, value in details.items():
|
||||
for key, value in form_details.items():
|
||||
counter, field_name = key.split('-', 1)
|
||||
if field_name not in registration_fields:
|
||||
continue
|
||||
elif isinstance(registration_fields[field_name], (fields.Many2one, fields.Integer)):
|
||||
value = int(value)
|
||||
else:
|
||||
value = value
|
||||
|
||||
if counter == '0':
|
||||
global_values[field_name] = value
|
||||
else:
|
||||
@@ -282,22 +319,28 @@ class WebsiteEventController(http.Controller):
|
||||
registration[key] = value
|
||||
return list(registrations.values())
|
||||
|
||||
def _create_attendees_from_registration_post(self, event, registration_data):
|
||||
attendees_sudo = request.env['event.registration'].sudo()
|
||||
|
||||
for registration_values in registration_data:
|
||||
registration_values['event_id'] = event.id
|
||||
if not registration_values.get('partner_id'):
|
||||
registration_values['partner_id'] = request.env.user.partner_id.id
|
||||
attendees_sudo += request.env['event.registration'].sudo().create(registration_values)
|
||||
|
||||
return attendees_sudo
|
||||
|
||||
@http.route(['''/event/<model("event.event"):event>/registration/confirm'''], type='http', auth="public", methods=['POST'], website=True)
|
||||
def registration_confirm(self, event, **post):
|
||||
if not event.can_access_from_current_website():
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
Attendees = request.env['event.registration']
|
||||
registrations = self._process_registration_details(post)
|
||||
|
||||
for registration in registrations:
|
||||
registration['event_id'] = event
|
||||
Attendees += Attendees.sudo().create(
|
||||
Attendees._prepare_attendee_values(registration))
|
||||
registrations = self._process_attendees_form(event, post)
|
||||
attendees = self._create_attendees_from_registration_post(event, registrations)
|
||||
|
||||
urls = event._get_event_resource_urls()
|
||||
return request.render("website_event.registration_complete", {
|
||||
'attendees': Attendees.sudo(),
|
||||
'attendees': attendees,
|
||||
'event': event,
|
||||
'google_url': urls.get('google_url'),
|
||||
'iCal_url': urls.get('iCal_url')
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_event_event_ticket_public" model="ir.rule">
|
||||
<field name="name">Event ticket: public/portal may read published</field>
|
||||
<field name="model_id" ref="event.model_event_event_ticket"/>
|
||||
<field name="domain_force">[('event_id.website_published', '=', True)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_public')), (4, ref('base.group_portal'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="event.group_event_manager" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('website.group_website_publisher'))]"/>
|
||||
</record>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_event_event_public,event.event.public,event.model_event_event,base.group_public,1,0,0,0
|
||||
access_event_event_portal,event.event.portal,event.model_event_event,base.group_portal,1,0,0,0
|
||||
access_event_type_public,event.type.public,event.model_event_type,,1,0,0,0
|
||||
access_event_type_public,event.type.public,event.model_event_type,,1,0,0,0
|
||||
access_event_event_ticket_public,event.event.ticket.public,event.model_event_event_ticket,base.group_public,1,0,0,0
|
||||
access_event_event_ticket_portal,event.event.ticket.portal,event.model_event_event_ticket,base.group_portal,1,0,0,0
|
||||
|
||||
|
@@ -269,3 +269,9 @@ $o-wevent-event-title-sizes-variants: (
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-open {
|
||||
> .modal-backdrop {
|
||||
z-index: $zindex-dropdown - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +209,18 @@
|
||||
</main>
|
||||
<!-- Footer -->
|
||||
<footer t-attf-class="small #{opt_events_list_cards and 'card-footer' or ((not opt_events_list_columns) and 'border-top mx-4 mt-auto pt-2' or 'border-top py-2')}">
|
||||
<div itemprop="price"><span content="0" class="font-weight-bold text-uppercase">Free</span></div>
|
||||
<t t-if="event.event_registrations_open">
|
||||
<span class="text-danger"
|
||||
t-if="event.seats_availability == 'limited' and not event.seats_available">
|
||||
Sold Out
|
||||
</span>
|
||||
<span class="text-muted"
|
||||
t-if="event.seats_availability == 'limited' and
|
||||
event.seats_available and
|
||||
(event.seats_available <5 or event.seats_available <= ((event.seats_max or 0) / 4))">
|
||||
<em>Only <t t-esc="event.seats_available"/> Remaining</em>
|
||||
</span>
|
||||
</t>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
@@ -505,22 +516,109 @@
|
||||
|
||||
<!-- Event - Registration -->
|
||||
<template id="registration_template">
|
||||
<form t-if="event.event_registrations_open" id="registration_form" t-attf-action="/event/#{slug(event)}/registration/new" method="post" itemscope="itemscope" itemprop="offers" itemtype="http://schema.org/AggregateOffer" class="mb-5">
|
||||
<div t-if="event.start_sale_date and event.start_sale_date > datetime.date.today() and not event.event_registrations_open"
|
||||
class="alert alert-info mb-5" role="status">
|
||||
<em >Ticket Sales starting on <t t-esc="event.start_sale_date"/></em>
|
||||
</div>
|
||||
<form t-if="event.event_registrations_open and (not event.event_ticket_ids or not all([ticket.is_expired for ticket in event.event_ticket_ids]))"
|
||||
id="registration_form"
|
||||
t-attf-action="/event/#{slug(event)}/registration/new" method="post"
|
||||
class="mb-5" itemscope="itemscope" itemprop="offers" itemtype="http://schema.org/AggregateOffer">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<div id="o_wevent_tickets" class="bg-white rounded shadow-sm">
|
||||
<t t-set="quantity">
|
||||
<select name="nb_register-0" class="custom-select w-auto">
|
||||
<t t-foreach="range(0, (event.seats_availability == 'unlimited' or event.seats_available > 9) and 10 or event.seats_available+1)" t-as="nb">
|
||||
<option t-esc="nb" t-att-selected="nb == 1 and 'selected'"/>
|
||||
</t>
|
||||
</select>
|
||||
</t>
|
||||
<t t-id="tickets" t-call="website_event.ticket">
|
||||
<t t-set="name">Registration</t>
|
||||
<t t-set="price">
|
||||
<span content="0" itemprop="price" class="font-weight-bold text-uppercase">Free</span>
|
||||
</t>
|
||||
|
||||
<t t-set="tickets" t-value="event.event_ticket_ids.filtered(lambda ticket: not ticket.is_expired)"/>
|
||||
<!-- If some tickets expired and there is only one type left, we keep the same layout -->
|
||||
<t t-if="len(event.event_ticket_ids) > 1">
|
||||
<div class="d-flex align-items-center py-2 pl-3 pr-2 border-bottom">
|
||||
<span t-if="not event.event_registrations_open" class="text-danger">
|
||||
<i class="fa fa-ban mr-2"/>Sold Out
|
||||
</span>
|
||||
<div class="ml-auto pr-3">
|
||||
<span t-if="event.is_participating" class="text-success ml-3">
|
||||
<i class="fa fa-check mr-2"/>Registered
|
||||
</span>
|
||||
</div>
|
||||
<a href="#" role="button" class="btn btn-primary o_wevent_registration_btn collapsed" data-toggle="collapse" data-target="#o_wevent_tickets_collapse">
|
||||
Tickets
|
||||
<span class="close">×</span>
|
||||
</a>
|
||||
</div>
|
||||
<div id="o_wevent_tickets_collapse" class="collapse">
|
||||
<div t-foreach="tickets" t-as="ticket" class="row px-3 mx-0 bg-light border-bottom">
|
||||
<div class="col-8 py-3" itemscope="itemscope" itemtype="http://schema.org/Offer">
|
||||
<h5 itemprop="name" t-field="ticket.name" class="my-0"/>
|
||||
<t t-if="ticket.description">
|
||||
<small t-field="ticket.description" class="text-muted py-2"/>
|
||||
<br/>
|
||||
</t>
|
||||
<small t-if="ticket.end_sale_date and ticket.sale_available and not ticket.is_expired" class="text-muted mr-3" itemprop="availabilityEnds">Sales end on <span itemprop="priceValidUntil" t-field="ticket.end_sale_date"/></small>
|
||||
<small t-if="ticket.start_sale_date and not ticket.sale_available and not ticket.is_expired" class="text-muted mr-3" itemprop="availabilityEnds">Sales start on <span itemprop="priceValidUntil" t-field="ticket.start_sale_date"/></small>
|
||||
</div>
|
||||
<div class="col-4 d-flex align-items-center">
|
||||
<div class="o_wevent_registration_multi_select"/>
|
||||
<div class="w-auto ml-auto">
|
||||
<select t-if="not ticket.is_expired and ticket.sale_available"
|
||||
t-attf-name="nb_register-#{ticket.id}"
|
||||
class="custom-select">
|
||||
<t t-set="seats_max_ticket" t-value="(ticket.seats_availability == 'unlimited' or ticket.seats_available > 9) and 10 or ticket.seats_available + 1"/>
|
||||
<t t-set="seats_max_event" t-value="(event.seats_availability == 'unlimited' or event.seats_available > 9) and 10 or event.seats_available + 1"/>
|
||||
<t t-set="seats_max" t-value="min(seats_max_ticket, seats_max_event)"/>
|
||||
<t t-foreach="range(0, seats_max)" t-as="nb">
|
||||
<option t-esc="nb" t-att-selected="len(ticket) == 0 and nb == 0 and 'selected'"/>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row no-gutters">
|
||||
<div class="col-md-4 offset-md-8 py-2 pl-md-0 pr-md-2">
|
||||
<button type="submit" class="btn btn-primary o_wait_lazy_js btn-block a-submit" t-att-disabled="not event.event_registrations_open or None" t-attf-id="#{event.id}">Register</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<div t-else="" class="o_wevent_registration_single">
|
||||
<div class="row p-2 pl-3">
|
||||
<div class="col-lg-8 d-flex flex-columns align-items-center" itemscope="itemscope" itemtype="http://schema.org/Offer">
|
||||
<h6 itemprop="name" class="my-0 pr-3 border-right">
|
||||
<span t-if="tickets" t-field="tickets.name"/>
|
||||
<span t-else="">Registration</span>
|
||||
</h6>
|
||||
|
||||
<small t-if="tickets.start_sale_date" class="text-muted ml-3" itemprop="availabilityEnds">
|
||||
Sales end on <span itemprop="priceValidUntil" t-field="tickets.end_sale_date"/>
|
||||
</small>
|
||||
<div class="ml-auto">
|
||||
<t t-if="event.event_registrations_open">
|
||||
<span class="font-weight-bold align-middle pr-2">Qty</span>
|
||||
<link itemprop="availability" content="http://schema.org/InStock"/>
|
||||
<select t-att-name="'nb_register-%s' % (tickets.id if tickets else 0)" class="w-auto custom-select">
|
||||
<t t-set="seats_max_ticket" t-value="(not tickets or tickets.seats_availability == 'unlimited' or tickets.seats_available > 9) and 10 or tickets.seats_available + 1"/>
|
||||
<t t-set="seats_max_event" t-value="(event.seats_availability == 'unlimited' or event.seats_available > 9) and 10 or event.seats_available + 1"/>
|
||||
<t t-set="seats_max" t-value="min(seats_max_ticket, seats_max_event) if tickets else seats_max_event"/>
|
||||
<t t-foreach="range(0, seats_max)" t-as="nb">
|
||||
<option t-esc="nb" t-att-selected="nb == 1 and 'selected'"/>
|
||||
</t>
|
||||
</select>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span itemprop="availability" content="http://schema.org/SoldOut" class="text-danger">
|
||||
<i class="fa fa-ban mr-2"/>Sold Out
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 pt-3 pt-lg-0 pl-2 pl-lg-0">
|
||||
<button type="submit" class="btn btn-primary o_wait_lazy_js btn-block a-submit" t-attf-id="#{event.id}">Register</button>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="tickets.description" class="row mx-0">
|
||||
<div class="col-12 bg-200 rounded-bottom">
|
||||
<p itemprop="description" t-field="tickets.description" class="small my-2"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div t-elif="request.env.user.has_group('event.group_event_manager')" class="alert alert-info mb-5 o_website_event_configuration" role="status">
|
||||
@@ -530,36 +628,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="ticket" name="Ticket offer template">
|
||||
<div class="row p-2 pl-3">
|
||||
<div class="col-lg-8 d-flex flex-columns align-items-center" itemscope="itemscope" itemtype="http://schema.org/Offer">
|
||||
<h6 t-raw="name" itemprop="name" class="my-0 pr-3"/>
|
||||
<div class="border-left border-right px-3"><t t-raw="price"/></div>
|
||||
<small t-raw="registration_end" class="text-muted ml-3" itemprop="availabilityEnds"/>
|
||||
<div class="ml-auto">
|
||||
<t t-if="event.event_registrations_open">
|
||||
<span class="font-weight-bold align-middle pr-2">Qty</span>
|
||||
<link itemprop="availability" content="http://schema.org/InStock"/>
|
||||
<t t-raw="quantity"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span itemprop="availability" content="http://schema.org/SoldOut" class="text-danger">
|
||||
<i class="fa fa-ban mr-2"/>Sold Out
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 pt-3 pt-lg-0 pl-2 pl-lg-0">
|
||||
<button type="submit" class="btn btn-primary o_wait_lazy_js btn-block a-submit" t-attf-id="#{event.id}">Register</button>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="description" class="row mx-0">
|
||||
<div class="col-12 bg-200 rounded-bottom">
|
||||
<p itemprop="description" t-raw="description" class="small my-2"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="registration_attendee_details" name="Registration Attendee Details">
|
||||
<div id="modal_attendees_registration" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
@@ -590,7 +658,7 @@
|
||||
<label>Phone <small>(Optional)</small></label>
|
||||
<input class="form-control" type="tel" t-attf-name="#{counter}-phone"/>
|
||||
</div>
|
||||
<input class="d-none" type="text" t-attf-name="#{counter}-ticket_id" t-attf-value="#{ticket['id']}"/>
|
||||
<input class="d-none" type="text" t-attf-name="#{counter}-event_ticket_id" t-attf-value="#{ticket['id']}"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
@@ -616,6 +684,7 @@
|
||||
<thead class="bg-secondary">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Ticket</th>
|
||||
<th>E-mail</th>
|
||||
<th>Phone</th>
|
||||
<th>Reference</th>
|
||||
@@ -625,6 +694,7 @@
|
||||
<t t-foreach="attendees" t-as="attendee">
|
||||
<tr>
|
||||
<td><t t-if="attendee.name" t-esc="attendee.name"/><t t-else="">N/A</t></td>
|
||||
<td><i class="fa fa-ticket"></i> <t t-if='attendee.event_ticket_id'><t t-esc="attendee.event_ticket_id.name"/></t><t t-if='not attendee.event_ticket_id'>N/A</t></td>
|
||||
<td><t t-if="attendee.email" t-esc="attendee.email"/><t t-else="">N/A</t></td>
|
||||
<td><t t-if="attendee.phone" t-esc="attendee.phone"/><t t-else="">N/A</t></td>
|
||||
<td><t t-esc="attendee.id"/></td>
|
||||
|
||||
@@ -12,5 +12,9 @@
|
||||
'report/report_event_question_view.xml',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'demo': [
|
||||
'data/event_demo.xml',
|
||||
'data/event_registration_demo.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ from odoo.addons.website_event.controllers.main import WebsiteEventController
|
||||
|
||||
class WebsiteEvent(WebsiteEventController):
|
||||
|
||||
def _process_registration_details(self, details):
|
||||
def _process_attendees_form(self, event, form_details):
|
||||
''' Process data posted from the attendee details form. '''
|
||||
registrations = super(WebsiteEvent, self)._process_registration_details(details)
|
||||
registrations = super(WebsiteEvent, self)._process_attendees_form(event, form_details)
|
||||
for registration in registrations:
|
||||
answer_ids = []
|
||||
for key, value in registration.items():
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data>
|
||||
<record id="event_0_question_0" model="event.question">
|
||||
<field name="title">Meal Type</field>
|
||||
<field name="is_individual" eval="False"/>
|
||||
<field name="event_id" ref="event.event_0"/>
|
||||
</record>
|
||||
<record id="event_0_question_0_answer_0" model="event.answer">
|
||||
<field name="name">Mixed</field>
|
||||
<field name="sequence">1</field>
|
||||
<field name="question_id" ref="website_event_questions.event_0_question_0"/>
|
||||
</record>
|
||||
<record id="event_0_question_0_answer_1" model="event.answer">
|
||||
<field name="name">Vegetarian</field>
|
||||
<field name="sequence">2</field>
|
||||
<field name="question_id" ref="website_event_questions.event_0_question_0"/>
|
||||
</record>
|
||||
<record id="event_0_question_0_answer_2" model="event.answer">
|
||||
<field name="name">Pastafarian</field>
|
||||
<field name="sequence">3</field>
|
||||
<field name="question_id" ref="website_event_questions.event_0_question_0"/>
|
||||
</record>
|
||||
</data></odoo>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data>
|
||||
<record id="event.event_registration_0_0" model="event.registration">
|
||||
<field name="answer_ids" eval="[
|
||||
(5, 0),
|
||||
(4, ref('website_event_questions.event_0_question_0_answer_0'))]"/>
|
||||
</record>
|
||||
<record id="event.event_registration_0_1" model="event.registration">
|
||||
<field name="answer_ids" eval="[
|
||||
(5, 0),
|
||||
(4, ref('website_event_questions.event_0_question_0_answer_1'))]"/>
|
||||
</record>
|
||||
<record id="event.event_registration_0_2" model="event.registration">
|
||||
<field name="answer_ids" eval="[
|
||||
(5, 0),
|
||||
(4, ref('website_event_questions.event_0_question_0_answer_2'))]"/>
|
||||
</record>
|
||||
</data></odoo>
|
||||
@@ -17,44 +17,46 @@ class WebsiteEventSaleController(WebsiteEventController):
|
||||
event = event.with_context(pricelist=pricelist.id)
|
||||
return super(WebsiteEventSaleController, self).event_register(event, **post)
|
||||
|
||||
def _process_tickets_details(self, data):
|
||||
ticket_post = {}
|
||||
for key, value in data.items():
|
||||
if not key.startswith('nb_register') or '-' not in key:
|
||||
continue
|
||||
items = key.split('-')
|
||||
if len(items) < 2:
|
||||
continue
|
||||
ticket_post[int(items[1])] = int(value)
|
||||
tickets = request.env['event.event.ticket'].browse(tuple(ticket_post))
|
||||
return [{'id': ticket.id, 'name': ticket.name, 'quantity': ticket_post[ticket.id], 'price': ticket.price} for ticket in tickets if ticket_post[ticket.id]]
|
||||
def _process_tickets_form(self, event, form_details):
|
||||
""" Add price information on ticket order """
|
||||
res = super(WebsiteEventSaleController, self)._process_tickets_form(event, form_details)
|
||||
for item in res:
|
||||
item['price'] = item['ticket']['price'] if item['ticket'] else 0
|
||||
return res
|
||||
|
||||
def _create_attendees_from_registration_post(self, event, registration_data):
|
||||
# we have at least one registration linked to a ticket -> sale mode activate
|
||||
if any(info.get('event_ticket_id') for info in registration_data):
|
||||
order = request.website.sale_get_order(force_create=1)
|
||||
|
||||
order_attendee_ids = set()
|
||||
for info in [r for r in registration_data if r.get('event_ticket_id')]:
|
||||
ticket = request.env['event.event.ticket'].sudo().browse(info['event_ticket_id'])
|
||||
cart_values = order.with_context(event_ticket_id=ticket.id, fixed_price=True)._cart_update(product_id=ticket.product_id.id, add_qty=1, registration_data=[info])
|
||||
order_attendee_ids |= set(cart_values.get('attendee_ids', []))
|
||||
|
||||
attendees_sudo = super(WebsiteEventSaleController, self)._create_attendees_from_registration_post(event, [r for r in registration_data if not r['event_ticket_id']])
|
||||
if order_attendee_ids:
|
||||
attendees_sudo |= request.env['event.registration'].sudo().browse(order_attendee_ids)
|
||||
|
||||
return attendees_sudo
|
||||
|
||||
@http.route()
|
||||
def registration_confirm(self, event, **post):
|
||||
order = request.website.sale_get_order(force_create=1)
|
||||
attendee_ids = set()
|
||||
res = super(WebsiteEventSaleController, self).registration_confirm(event, **post)
|
||||
|
||||
registrations = self._process_registration_details(post)
|
||||
for registration in registrations:
|
||||
ticket = request.env['event.event.ticket'].sudo().browse(int(registration['ticket_id']))
|
||||
cart_values = order.with_context(event_ticket_id=ticket.id, fixed_price=True)._cart_update(product_id=ticket.product_id.id, add_qty=1, registration_data=[registration])
|
||||
attendee_ids |= set(cart_values.get('attendee_ids', []))
|
||||
registrations = self._process_attendees_form(event, post)
|
||||
|
||||
# free tickets -> order with amount = 0: auto-confirm, no checkout
|
||||
if not order.amount_total:
|
||||
order.action_confirm() # tde notsure: email sending ?
|
||||
attendees = request.env['event.registration'].browse(list(attendee_ids)).sudo()
|
||||
# clean context and session, then redirect to the confirmation page
|
||||
request.website.sale_reset()
|
||||
urls = event._get_event_resource_urls()
|
||||
return request.render("website_event.registration_complete", {
|
||||
'attendees': attendees,
|
||||
'event': event,
|
||||
'google_url': urls.get('google_url'),
|
||||
'iCal_url': urls.get('iCal_url')
|
||||
})
|
||||
# we have at least one registration linked to a ticket -> sale mode activate
|
||||
if any(info['event_ticket_id'] for info in registrations):
|
||||
order = request.website.sale_get_order(force_create=False)
|
||||
if order.amount_total:
|
||||
return request.redirect("/shop/checkout")
|
||||
# free tickets -> order with amount = 0: auto-confirm, no checkout
|
||||
elif order:
|
||||
order.action_confirm() # tde notsure: email sending ?
|
||||
|
||||
return request.redirect("/shop/checkout")
|
||||
return res
|
||||
|
||||
def _add_event(self, event_name="New Event", context=None, **kwargs):
|
||||
product = request.env.ref('event_sale.product_product_event', raise_if_not_found=False)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="event_sale.event_type_data_sale" model="event.type">
|
||||
<record id="event.event_type_data_ticket" model="event.type">
|
||||
<field name="name">Sell Online</field>
|
||||
<field name="website_menu" eval="True"/>
|
||||
</record>
|
||||
|
||||
@@ -43,7 +43,7 @@ class SaleOrder(models.Model):
|
||||
values['price_unit'] = ticket.price
|
||||
else:
|
||||
values['price_unit'] = ticket.price_reduce
|
||||
values['name'] = ticket._get_ticket_multiline_description_sale()
|
||||
values['name'] = ticket._get_ticket_multiline_description()
|
||||
|
||||
# avoid writing related values that end up locking the product record
|
||||
values.pop('event_ok', None)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_event_event_ticket_public,event.event.ticket.public,event_sale.model_event_event_ticket,,1,0,0,0
|
||||
access_event_product_product_public,event.product.product.public,product.model_product_product,base.group_public,1,0,0,0
|
||||
access_event_product_template_public,event.product.template.public,product.model_product_template,base.group_public,1,0,0,0
|
||||
access_event_product_template_public,event.product.template.public,product.model_product_template,base.group_public,1,0,0,0
|
||||
|
||||
|
@@ -1,16 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="event_event_ticket_public" model="ir.rule">
|
||||
<field name="name">event ticket: Public</field>
|
||||
<field name="model_id" ref="event_sale.model_event_event_ticket"/>
|
||||
<field name="domain_force">[('event_id.website_published', '=', True)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_public')), (4, ref('base.group_portal'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="event_product_template_public" model="ir.rule">
|
||||
<field name="name">Product template linked to event: Public</field>
|
||||
<field name="model_id" ref="product.model_product_template"/>
|
||||
|
||||
@@ -1,130 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="index" inherit_id="website_event.events_list" name="Event's Ticket">
|
||||
<xpath expr="//div[@t-foreach='event_ids']//footer" position="inside">
|
||||
<t t-if="event.event_registrations_open and event.event_ticket_ids">
|
||||
<span t-if="event.seats_availability == 'limited' and not event.seats_available" class="text-danger">Sold Out</span>
|
||||
<span t-if="event.seats_availability == 'limited' and event.seats_available and event.seats_available <= ((event.seats_max or 0) / 4)" class="text-muted">
|
||||
<em>Only <t t-esc="event.seats_available"/> Remaining</em>
|
||||
</span>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="registration_template" inherit_id="website_event.registration_template">
|
||||
<xpath expr="//t[@t-id='tickets']" position="replace">
|
||||
<t t-set="tickets" t-value="event.event_ticket_ids.filtered(lambda ticket: not ticket.is_expired)"/>
|
||||
<t t-if="len(event.event_ticket_ids) > 1"> <!-- If some tickets expired and there is only one type left, we keep the same layout -->
|
||||
<div class="d-flex align-items-center py-2 pl-3 pr-2 border-bottom">
|
||||
<span t-if="not event.event_registrations_open" class="text-danger">
|
||||
<i class="fa fa-ban mr-2"/>Sold Out
|
||||
</span>
|
||||
<div class="ml-auto pr-3">
|
||||
<span t-if="event.is_participating" class="text-success ml-3">
|
||||
<i class="fa fa-check mr-2"/>Registered
|
||||
</span>
|
||||
</div>
|
||||
<a href="#" role="button" class="btn btn-primary o_wevent_registration_btn collapsed" data-toggle="collapse" data-target="#o_wevent_tickets_collapse">
|
||||
Tickets
|
||||
<span class="close">×</span>
|
||||
</a>
|
||||
</div>
|
||||
<div id="o_wevent_tickets_collapse" class="collapse">
|
||||
<div t-foreach="tickets" t-as="ticket" class="row mx-0 bg-light border-bottom">
|
||||
<div class="col-md-8 py-3" itemscope="itemscope" itemtype="http://schema.org/Offer">
|
||||
<h5 itemprop="name" t-field="ticket.name" class="my-0"/>
|
||||
<small t-if="ticket.end_sale_date and ticket.sale_available and not ticket.is_expired" class="text-muted mr-3" itemprop="availabilityEnds">Sales end on <span itemprop="priceValidUntil" t-field="ticket.end_sale_date"/></small>
|
||||
<small t-if="ticket.start_sale_date and not ticket.sale_available and not ticket.is_expired" class="text-muted mr-3" itemprop="availabilityEnds">Sales start on <span itemprop="priceValidUntil" t-field="ticket.start_sale_date"/></small>
|
||||
</div>
|
||||
<div class="col-md-4 py-3 pl-md-0">
|
||||
<div class="d-flex align-items-center">
|
||||
<t t-if="ticket.price or editable">
|
||||
<t t-if="(ticket.price-website.get_current_pricelist().currency_id._convert(ticket.price_reduce, event.company_id.sudo().currency_id, event.company_id, datetime.date.today())) > 1 and website.get_current_pricelist().discount_policy == 'without_discount'">
|
||||
<del class="text-danger mr-1" t-field="ticket.price" t-options="{'widget': 'monetary', 'from_currency': event.company_id.sudo().currency_id, 'display_currency': website.get_current_pricelist().currency_id}"/>
|
||||
</t>
|
||||
<span t-field="ticket.price_reduce" t-options="{'widget': 'monetary', 'display_currency': website.pricelist_id.currency_id}" groups="account.group_show_line_subtotals_tax_excluded"/>
|
||||
<span t-field="ticket.price_reduce_taxinc" t-options="{'widget': 'monetary', 'display_currency': website.pricelist_id.currency_id}" groups="account.group_show_line_subtotals_tax_included"/>
|
||||
<span itemprop="price" class="d-none" t-esc="ticket.price"/>
|
||||
<span itemprop="priceCurrency" class="d-none" t-esc="website.pricelist_id.currency_id.name"/>
|
||||
</t>
|
||||
<span t-if="not ticket.price and not editable" class="font-weight-bold text-uppercase">Free</span>
|
||||
<t t-if="not ticket.is_expired and ticket.sale_available">
|
||||
<select t-attf-name="nb_register-#{ticket.id}" class="w-auto ml-auto custom-select">
|
||||
<t t-set="seats_max_ticket" t-value="(ticket.seats_availability == 'unlimited' or ticket.seats_available > 9) and 10 or ticket.seats_available + 1"/>
|
||||
<t t-set="seats_max_event" t-value="(event.seats_availability == 'unlimited' or event.seats_available > 9) and 10 or event.seats_available + 1"/>
|
||||
<t t-set="seats_max" t-value="min(seats_max_ticket, seats_max_event)"/>
|
||||
<t t-foreach="range(0, seats_max)" t-as="nb">
|
||||
<option t-esc="nb" t-att-selected="len(ticket) == 0 and nb == 0 and 'selected'"/>
|
||||
</t>
|
||||
</select>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="ticket.product_id.description_sale" class="col-12">
|
||||
<p itemprop="description" t-field="ticket.product_id.description_sale" class="small py-2 border-top"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row no-gutters">
|
||||
<div class="col-md-4 offset-md-8 py-2 pl-md-0 pr-md-2">
|
||||
<button type="submit" class="btn btn-primary o_wait_lazy_js btn-block a-submit" t-att-disabled="not event.event_registrations_open or None" t-attf-id="#{event.id}">Register</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-call="website_event.ticket">
|
||||
<t t-set="name"><span t-field="tickets.name"/></t>
|
||||
<t t-set="price">
|
||||
<t t-if="tickets.price or editable">
|
||||
<t t-if="(tickets.price-website.get_current_pricelist().currency_id._convert(tickets.price_reduce, event.company_id.sudo().currency_id, event.company_id, datetime.date.today())) > 1 and website.get_current_pricelist().discount_policy == 'without_discount'">
|
||||
<del class="text-danger mr-1" t-field="tickets.price" t-options="{'widget': 'monetary', 'from_currency': event.company_id.sudo().currency_id, 'display_currency': website.get_current_pricelist().currency_id}"/>
|
||||
</t>
|
||||
<span t-field="tickets.price_reduce" t-options="{'widget': 'monetary', 'display_currency': website.pricelist_id.currency_id}" groups="account.group_show_line_subtotals_tax_excluded"/>
|
||||
<span t-field="tickets.price_reduce_taxinc" t-options="{'widget': 'monetary', 'display_currency': website.pricelist_id.currency_id}" groups="account.group_show_line_subtotals_tax_included"/>
|
||||
<span itemprop="price" class="d-none" t-esc="tickets.price"/>
|
||||
<span itemprop="priceCurrency" class="d-none" t-esc="website.pricelist_id.currency_id.name"/>
|
||||
</t>
|
||||
<span t-if="not tickets.price and not editable" class="font-weight-bold text-uppercase">Free</span>
|
||||
</t>
|
||||
<t t-if="tickets.product_id.description_sale" t-set="description">
|
||||
<span t-field="tickets.product_id.description_sale"/>
|
||||
</t>
|
||||
<t t-set="registration_end">
|
||||
<t t-if="tickets.start_sale_date">Sales end on <span itemprop="priceValidUntil" t-field="tickets.end_sale_date"/></t>
|
||||
</t>
|
||||
<t t-if="not tickets.is_expired and tickets.sale_available" t-set="quantity">
|
||||
<select t-attf-name="nb_register-#{tickets.id}" class="w-auto custom-select">
|
||||
<t t-set="seats_max_ticket" t-value="(tickets.seats_availability == 'unlimited' or tickets.seats_available > 9) and 10 or tickets.seats_available + 1"/>
|
||||
<t t-set="seats_max_event" t-value="(event.seats_availability == 'unlimited' or event.seats_available > 9) and 10 or event.seats_available + 1"/>
|
||||
<t t-set="seats_max" t-value="min(seats_max_ticket, seats_max_event)"/>
|
||||
<t t-foreach="range(0, seats_max)" t-as="nb">
|
||||
<option t-esc="nb" t-att-selected="len(tickets) == 1 and nb == 1 and 'selected'"/>
|
||||
</t>
|
||||
</select>
|
||||
</t>
|
||||
<!-- Add price information on tickets (multi tickets, aka in collapse) -->
|
||||
<xpath expr="//div[hasclass('o_wevent_registration_multi_select')]" position="inside">
|
||||
<t t-if="ticket.price">
|
||||
<t t-if="(ticket.price-website.get_current_pricelist().currency_id._convert(ticket.price_reduce, event.company_id.sudo().currency_id, event.company_id, datetime.date.today())) > 1 and website.get_current_pricelist().discount_policy == 'without_discount'">
|
||||
<del class="text-danger mr-1" t-field="ticket.price" t-options="{'widget': 'monetary', 'from_currency': event.company_id.sudo().currency_id, 'display_currency': website.get_current_pricelist().currency_id}"/>
|
||||
</t>
|
||||
<span t-field="ticket.price_reduce" t-options="{'widget': 'monetary', 'display_currency': website.pricelist_id.currency_id}" groups="account.group_show_line_subtotals_tax_excluded"/>
|
||||
<span t-field="ticket.price_reduce_taxinc" t-options="{'widget': 'monetary', 'display_currency': website.pricelist_id.currency_id}" groups="account.group_show_line_subtotals_tax_included"/>
|
||||
<span itemprop="price" class="d-none" t-esc="ticket.price"/>
|
||||
<span itemprop="priceCurrency" class="d-none" t-esc="website.pricelist_id.currency_id.name"/>
|
||||
</t>
|
||||
<span t-else="" class="font-weight-bold text-uppercase">Free</span>
|
||||
</xpath>
|
||||
<xpath expr="//form[@id='registration_form']" position="attributes">
|
||||
<attribute name="t-if">event.event_registrations_open and not all([ticket.is_expired for ticket in event.event_ticket_ids])</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//form[@id='registration_form']" position="before">
|
||||
<div t-if="event.start_sale_date and event.start_sale_date > datetime.date.today() and not event.event_registrations_open" class="alert alert-info mb-5" role="status">
|
||||
<em >Ticket Sales starting on <t t-esc="event.start_sale_date"/></em>
|
||||
<!-- Add price information on tickets (mono ticket, aka not in collapse) -->
|
||||
<xpath expr="//div[hasclass('o_wevent_registration_single')]//h6" position="after">
|
||||
<div class="border-right px-3">
|
||||
<t t-if="tickets.price">
|
||||
<t t-if="(tickets.price-website.get_current_pricelist().currency_id._convert(tickets.price_reduce, event.company_id.sudo().currency_id, event.company_id, datetime.date.today())) > 1 and website.get_current_pricelist().discount_policy == 'without_discount'">
|
||||
<del class="text-danger mr-1" t-field="tickets.price" t-options="{'widget': 'monetary', 'from_currency': event.company_id.sudo().currency_id, 'display_currency': website.get_current_pricelist().currency_id}"/>
|
||||
</t>
|
||||
<span t-field="tickets.price_reduce" t-options="{'widget': 'monetary', 'display_currency': website.pricelist_id.currency_id}" groups="account.group_show_line_subtotals_tax_excluded"/>
|
||||
<span t-field="tickets.price_reduce_taxinc" t-options="{'widget': 'monetary', 'display_currency': website.pricelist_id.currency_id}" groups="account.group_show_line_subtotals_tax_included"/>
|
||||
<span itemprop="price" class="d-none" t-esc="tickets.price"/>
|
||||
<span itemprop="priceCurrency" class="d-none" t-esc="website.pricelist_id.currency_id.name"/>
|
||||
</t>
|
||||
<span t-else="" class="font-weight-bold text-uppercase">Free</span>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
|
||||
<!-- Add the ticket name on the registration complete page. -->
|
||||
<template id="registration_complete_inherit_website_event_sale" inherit_id="website_event.registration_complete" name="Sale Registration Completed">
|
||||
<xpath expr="//thead/tr/th[1]" position="after">
|
||||
<th>Ticket</th>
|
||||
</xpath>
|
||||
<xpath expr="//tbody/t/tr/td[1]" position="after">
|
||||
<td><i class="fa fa-ticket"></i> <t t-if='attendee.event_ticket_id'><t t-esc="attendee.event_ticket_id.name"/></t><t t-if='not attendee.event_ticket_id'>N/A</t></td>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user