[REF][MOV] website_event(_sale): support tickets in website_event frontend
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.
SPECIFICATIONS
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.
LINKS
Task ID 2177281
Community PR #43488
This commit is contained in:
@@ -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,23 +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_vals in registrations:
|
||||
registration_vals['event_id'] = event.id
|
||||
if not registration_vals.get('partner_id'):
|
||||
registration_vals['partner_id'] = request.env.user.partner_id.id
|
||||
Attendees += Attendees.sudo().create(registration_vals)
|
||||
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')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -17,50 +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_registration_details(self, details):
|
||||
res = super(WebsiteEventSaleController, self)._process_registration_details(details)
|
||||
for registration_vals in res:
|
||||
registration_vals['event_ticket_id'] = int(registration_vals.pop('ticket_id')) if registration_vals.get('ticket_id') else False
|
||||
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['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=[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,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