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
348 lines
15 KiB
Python
348 lines
15 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import babel.dates
|
|
import re
|
|
import werkzeug
|
|
from werkzeug.datastructures import OrderedMultiDict
|
|
|
|
from datetime import datetime, timedelta
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from odoo import fields, http, _
|
|
from odoo.addons.http_routing.models.ir_http import slug
|
|
from odoo.addons.website.controllers.main import QueryURL
|
|
from odoo.http import request
|
|
from odoo.tools.misc import get_lang
|
|
|
|
|
|
class WebsiteEventController(http.Controller):
|
|
|
|
def sitemap_event(env, rule, qs):
|
|
if not qs or qs.lower() in '/events':
|
|
yield {'loc': '/events'}
|
|
|
|
@http.route(['/event', '/event/page/<int:page>', '/events', '/events/page/<int:page>'], type='http', auth="public", website=True, sitemap=sitemap_event)
|
|
def events(self, page=1, **searches):
|
|
Event = request.env['event.event']
|
|
EventType = request.env['event.type']
|
|
|
|
searches.setdefault('search', '')
|
|
searches.setdefault('date', 'all')
|
|
searches.setdefault('type', 'all')
|
|
searches.setdefault('country', 'all')
|
|
|
|
website = request.website
|
|
|
|
def sdn(date):
|
|
return fields.Datetime.to_string(date.replace(hour=23, minute=59, second=59))
|
|
|
|
def sd(date):
|
|
return fields.Datetime.to_string(date)
|
|
today = datetime.today()
|
|
dates = [
|
|
['all', _('Next Events'), [("date_end", ">", sd(today))], 0],
|
|
['today', _('Today'), [
|
|
("date_end", ">", sd(today)),
|
|
("date_begin", "<", sdn(today))],
|
|
0],
|
|
['week', _('This Week'), [
|
|
("date_end", ">=", sd(today + relativedelta(days=-today.weekday()))),
|
|
("date_begin", "<", sdn(today + relativedelta(days=6-today.weekday())))],
|
|
0],
|
|
['nextweek', _('Next Week'), [
|
|
("date_end", ">=", sd(today + relativedelta(days=7-today.weekday()))),
|
|
("date_begin", "<", sdn(today + relativedelta(days=13-today.weekday())))],
|
|
0],
|
|
['month', _('This month'), [
|
|
("date_end", ">=", sd(today.replace(day=1))),
|
|
("date_begin", "<", (today.replace(day=1) + relativedelta(months=1)).strftime('%Y-%m-%d 00:00:00'))],
|
|
0],
|
|
['nextmonth', _('Next month'), [
|
|
("date_end", ">=", sd(today.replace(day=1) + relativedelta(months=1))),
|
|
("date_begin", "<", (today.replace(day=1) + relativedelta(months=2)).strftime('%Y-%m-%d 00:00:00'))],
|
|
0],
|
|
['old', _('Past Events'), [
|
|
("date_end", "<", today.strftime('%Y-%m-%d 00:00:00'))],
|
|
0],
|
|
]
|
|
|
|
# search domains
|
|
domain_search = {'website_specific': website.website_domain()}
|
|
|
|
if searches['search']:
|
|
domain_search['search'] = [('name', 'ilike', searches['search'])]
|
|
|
|
current_date = None
|
|
current_type = None
|
|
current_country = None
|
|
for date in dates:
|
|
if searches["date"] == date[0]:
|
|
domain_search["date"] = date[2]
|
|
if date[0] != 'all':
|
|
current_date = date[1]
|
|
|
|
if searches["type"] != 'all':
|
|
current_type = EventType.browse(int(searches['type']))
|
|
domain_search["type"] = [("event_type_id", "=", int(searches["type"]))]
|
|
|
|
if searches["country"] != 'all' and searches["country"] != 'online':
|
|
current_country = request.env['res.country'].browse(int(searches['country']))
|
|
domain_search["country"] = ['|', ("country_id", "=", int(searches["country"])), ("country_id", "=", False)]
|
|
elif searches["country"] == 'online':
|
|
domain_search["country"] = [("country_id", "=", False)]
|
|
|
|
def dom_without(without):
|
|
domain = []
|
|
for key, search in domain_search.items():
|
|
if key != without:
|
|
domain += search
|
|
return domain
|
|
|
|
# count by domains without self search
|
|
for date in dates:
|
|
if date[0] != 'old':
|
|
date[3] = Event.search_count(dom_without('date') + date[2])
|
|
|
|
domain = dom_without('type')
|
|
types = Event.read_group(domain, ["id", "event_type_id"], groupby=["event_type_id"], orderby="event_type_id")
|
|
types.insert(0, {
|
|
'event_type_id_count': sum([int(type['event_type_id_count']) for type in types]),
|
|
'event_type_id': ("all", _("All Categories"))
|
|
})
|
|
|
|
domain = dom_without('country')
|
|
countries = Event.read_group(domain, ["id", "country_id"], groupby="country_id", orderby="country_id")
|
|
countries.insert(0, {
|
|
'country_id_count': sum([int(country['country_id_count']) for country in countries]),
|
|
'country_id': ("all", _("All Countries"))
|
|
})
|
|
|
|
step = 12 # Number of events per page
|
|
event_count = Event.search_count(dom_without("none"))
|
|
pager = website.pager(
|
|
url="/event",
|
|
url_args=searches,
|
|
total=event_count,
|
|
page=page,
|
|
step=step,
|
|
scope=5)
|
|
|
|
order = 'date_begin'
|
|
if searches.get('date', 'all') == 'old':
|
|
order = 'date_begin desc'
|
|
if searches["country"] != 'all': # if we are looking for a specific country
|
|
order = 'is_online, ' + order # show physical events first
|
|
order = 'is_published desc, ' + order
|
|
events = Event.search(dom_without("none"), limit=step, offset=pager['offset'], order=order)
|
|
|
|
keep = QueryURL('/event', **{key: value for key, value in searches.items() if (key == 'search' or value != 'all')})
|
|
|
|
values = {
|
|
'current_date': current_date,
|
|
'current_country': current_country,
|
|
'current_type': current_type,
|
|
'event_ids': events, # event_ids used in website_event_track so we keep name as it is
|
|
'dates': dates,
|
|
'types': types,
|
|
'countries': countries,
|
|
'pager': pager,
|
|
'searches': searches,
|
|
'keep': keep,
|
|
}
|
|
|
|
if searches['date'] == 'old':
|
|
# the only way to display this content is to set date=old so it must be canonical
|
|
values['canonical_params'] = OrderedMultiDict([('date', 'old')])
|
|
|
|
return request.render("website_event.index", values)
|
|
|
|
@http.route(['''/event/<model("event.event"):event>/page/<path:page>'''], type='http', auth="public", website=True, sitemap=False)
|
|
def event_page(self, event, page, **post):
|
|
if not event.can_access_from_current_website():
|
|
raise werkzeug.exceptions.NotFound()
|
|
|
|
values = {
|
|
'event': event,
|
|
}
|
|
|
|
if '.' not in page:
|
|
page = 'website_event.%s' % page
|
|
|
|
try:
|
|
# Every event page view should have its own SEO.
|
|
values['seo_object'] = request.website.get_template(page)
|
|
values['main_object'] = event
|
|
except ValueError:
|
|
# page not found
|
|
values['path'] = re.sub(r"^website_event\.", '', page)
|
|
values['from_template'] = 'website_event.default_page' # .strip('website_event.')
|
|
page = 'website.%s' % (request.website.is_publisher() and 'page_404' or '404')
|
|
|
|
return request.render(page, values)
|
|
|
|
@http.route(['''/event/<model("event.event"):event>'''], type='http', auth="public", website=True, sitemap=True)
|
|
def event(self, event, **post):
|
|
if not event.can_access_from_current_website():
|
|
raise werkzeug.exceptions.NotFound()
|
|
|
|
if event.menu_id and event.menu_id.child_id:
|
|
target_url = event.menu_id.child_id[0].url
|
|
else:
|
|
target_url = '/event/%s/register' % str(event.id)
|
|
if post.get('enable_editor') == '1':
|
|
target_url += '?enable_editor=1'
|
|
return request.redirect(target_url)
|
|
|
|
@http.route(['''/event/<model("event.event"):event>/register'''], type='http', auth="public", website=True, sitemap=False)
|
|
def event_register(self, event, **post):
|
|
if not event.can_access_from_current_website():
|
|
raise werkzeug.exceptions.NotFound()
|
|
|
|
urls = event._get_event_resource_urls()
|
|
values = {
|
|
'event': event,
|
|
'main_object': event,
|
|
'range': range,
|
|
'registrable': event.sudo().event_registrations_open,
|
|
'google_url': urls.get('google_url'),
|
|
'iCal_url': urls.get('iCal_url'),
|
|
}
|
|
return request.render("website_event.event_description_full", values)
|
|
|
|
@http.route('/event/add_event', type='json', auth="user", methods=['POST'], website=True)
|
|
def add_event(self, event_name="New Event", **kwargs):
|
|
event = self._add_event(event_name, request.context)
|
|
return "/event/%s/register?enable_editor=1" % slug(event)
|
|
|
|
def _add_event(self, event_name=None, context=None, **kwargs):
|
|
if not event_name:
|
|
event_name = _("New Event")
|
|
date_begin = datetime.today() + timedelta(days=(14))
|
|
vals = {
|
|
'name': event_name,
|
|
'date_begin': fields.Date.to_string(date_begin),
|
|
'date_end': fields.Date.to_string((date_begin + timedelta(days=(1)))),
|
|
'seats_available': 1000,
|
|
'website_id': request.website.id,
|
|
}
|
|
return request.env['event.event'].with_context(context or {}).create(vals)
|
|
|
|
def get_formated_date(self, event):
|
|
start_date = fields.Datetime.from_string(event.date_begin).date()
|
|
end_date = fields.Datetime.from_string(event.date_end).date()
|
|
month = babel.dates.get_month_names('abbreviated', locale=get_lang(event.env).code)[start_date.month]
|
|
return ('%s %s%s') % (month, start_date.strftime("%e"), (end_date != start_date and ("-" + end_date.strftime("%e")) or ""))
|
|
|
|
@http.route('/event/get_country_event_list', type='json', auth='public', website=True)
|
|
def get_country_events(self, **post):
|
|
Event = request.env['event.event']
|
|
country_code = request.session['geoip'].get('country_code')
|
|
result = {'events': [], 'country': False}
|
|
events = None
|
|
domain = request.website.website_domain()
|
|
if country_code:
|
|
country = request.env['res.country'].search([('code', '=', country_code)], limit=1)
|
|
events = Event.search(domain + ['|', ('address_id', '=', None), ('country_id.code', '=', country_code), ('date_begin', '>=', '%s 00:00:00' % fields.Date.today())], order="date_begin")
|
|
if not events:
|
|
events = Event.search(domain + [('date_begin', '>=', '%s 00:00:00' % fields.Date.today())], order="date_begin")
|
|
for event in events:
|
|
if country_code and event.country_id.code == country_code:
|
|
result['country'] = country
|
|
result['events'].append({
|
|
"date": self.get_formated_date(event),
|
|
"event": event,
|
|
"url": event.website_url})
|
|
return request.env['ir.ui.view'].render_template("website_event.country_events_list", result)
|
|
|
|
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_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_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 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:
|
|
registrations.setdefault(counter, dict())[field_name] = value
|
|
for key, value in global_values.items():
|
|
for registration in registrations.values():
|
|
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()
|
|
|
|
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,
|
|
'event': event,
|
|
'google_url': urls.get('google_url'),
|
|
'iCal_url': urls.get('iCal_url')
|
|
})
|