Начальное наполнение

This commit is contained in:
parent 04882f6fa7
commit 3639b87a98
136 changed files with 292692 additions and 0 deletions

6
__init__.py Normal file
View File

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
from . import report

71
__manifest__.py Normal file
View File

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
{
'name': 'Events Organization',
'version': '1.8',
'website': 'https://www.odoo.com/app/events',
'category': 'Marketing/Events',
'summary': 'Trainings, Conferences, Meetings, Exhibitions, Registrations',
'description': """
Organization and management of Events.
======================================
The event module allows you to efficiently organize events and all related tasks: planning, registration tracking,
attendances, etc.
Key Features
------------
* Manage your Events and Registrations
* Use emails to automatically confirm and send acknowledgments for any event registration
""",
'depends': ['barcodes', 'base_setup', 'mail', 'phone_validation', 'portal', 'utm'],
'data': [
'security/event_security.xml',
'security/ir.model.access.csv',
'views/event_menu_views.xml',
'views/event_ticket_views.xml',
'views/event_mail_views.xml',
'views/event_registration_views.xml',
'views/event_type_views.xml',
'views/event_event_views.xml',
'views/event_stage_views.xml',
'report/event_event_templates.xml',
'report/event_event_reports.xml',
'report/event_registration_report.xml',
'data/ir_cron_data.xml',
'data/mail_template_data.xml',
'data/event_data.xml',
'views/res_config_settings_views.xml',
'views/event_templates.xml',
'views/res_partner_views.xml',
'views/event_tag_views.xml'
],
'demo': [
'data/res_users_demo.xml',
'data/res_partner_demo.xml',
'data/event_demo_misc.xml',
'data/event_demo.xml',
'data/event_registration_demo.xml',
],
'installable': True,
'assets': {
'web.assets_backend': [
'event/static/src/client_action/**/*',
'event/static/src/scss/event.scss',
'event/static/src/icon_selection_field/icon_selection_field.js',
'event/static/src/icon_selection_field/icon_selection_field.xml',
'event/static/src/js/tours/**/*',
],
'web.assets_frontend': [
'event/static/src/js/tours/**/*',
],
'web.report_assets_common': [
'/event/static/src/scss/event_badge_report.scss',
'/event/static/src/scss/event_full_page_ticket_report.scss',
'/event/static/src/scss/event_full_page_ticket_responsive_html_report.scss',
],
'web.report_assets_pdf': [
'/event/static/src/scss/event_full_page_ticket_report_pdf.scss',
],
},
'license': 'LGPL-3',
}

4
controllers/__init__.py Normal file
View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main

97
controllers/main.py Normal file
View File

@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from werkzeug.exceptions import NotFound
from odoo import http, _
from odoo.http import Controller, request, route, content_disposition
from odoo.tools import consteq
class EventController(Controller):
@route(['''/event/<model("event.event"):event>/ics'''], type='http', auth="public")
def event_ics_file(self, event, **kwargs):
lang = request.context.get('lang', request.env.user.lang)
if request.env.user._is_public():
lang = request.httprequest.cookies.get('frontend_lang')
event = event.with_context(lang=lang)
files = event._get_ics_file()
if not event.id in files:
return NotFound()
content = files[event.id]
return request.make_response(content, [
('Content-Type', 'application/octet-stream'),
('Content-Length', len(content)),
('Content-Disposition', content_disposition('%s.ics' % event.name))
])
@route(['/event/<int:event_id>/my_tickets'], type='http', auth='public')
def event_my_tickets(self, event_id, registration_ids, tickets_hash, badge_mode=False, responsive_html=False):
""" Returns a pdf response, containing all tickets for attendees in registration_ids for event_id.
Throw Forbidden if no registration is valid / hash is invalid / parameters are missing.
This route is used in links in emails to attendees, as well as in registration confirmation screens.
:param event: the id of prompted event. Only its attendees will be considered.
:param registration_ids: ids of event.registrations of which tickets are generated
:param tickets_hash: string hash used to access the tickets.
:param badge_mode: boolean, True to use template of foldable badge instead of full page ticket.
:param responsive_html: boolean, True if we want to see the a responsive html ticket.
"""
registration_ids = json.loads(registration_ids or '[]')
if not event_id or not tickets_hash or not registration_ids:
raise NotFound()
# We sudo the event in case of invitations sent before publishing it.
event_sudo = request.env['event.event'].browse(event_id).exists().sudo()
hash_truth = event_sudo and event_sudo._get_tickets_access_hash(registration_ids)
if not consteq(tickets_hash, hash_truth):
raise NotFound()
event_registrations_sudo = event_sudo.registration_ids.filtered(lambda reg: reg.id in registration_ids)
report_name_prefix = _("Ticket") if responsive_html else _("Badges") if badge_mode else _("Tickets")
report_name = f"{report_name_prefix} - {event_sudo.name} ({event_sudo.date_begin_located})"
if len(event_registrations_sudo) == 1:
report_name += f" - {event_registrations_sudo[0].name}"
# sudo is necessary for accesses in templates.
if responsive_html:
html = request.env['ir.actions.report'].sudo()._render_qweb_html(
'event.action_report_event_registration_responsive_html_ticket',
event_registrations_sudo.ids,
)[0]
return request.make_response(html)
pdf = request.env['ir.actions.report'].sudo()._render_qweb_pdf(
'event.action_report_event_registration_badge' if badge_mode else
'event.action_report_event_registration_full_page_ticket',
event_registrations_sudo.ids,
)[0]
pdfhttpheaders = [
('Content-Type', 'application/pdf'),
('Content-Length', len(pdf)),
('Content-Disposition', content_disposition(f'{report_name}.pdf')),
]
return request.make_response(pdf, headers=pdfhttpheaders)
@http.route(['/event/init_barcode_interface'], type='json', auth="user")
def init_barcode_interface(self, event_id):
event = request.env['event.event'].browse(event_id).exists() if event_id else False
if event:
return {
'name': event.name,
'country': event.address_id.country_id.name,
'city': event.address_id.city,
'company_name': event.company_id.name,
'company_id': event.company_id.id
}
else:
return {
'name': _('Registration Desk'),
'country': False,
'city': False,
'company_name': request.env.company.name,
'company_id': request.env.company.id
}

35
data/event_data.xml Normal file
View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Event stages -->
<record id="event_stage_new" model="event.stage">
<field name="name">New</field>
<field name="description">Freshly created</field>
<field name="sequence">1</field>
</record>
<record id="event_stage_booked" model="event.stage">
<field name="name">Booked</field>
<field name="description">The place has been reserved</field>
<field name="sequence">2</field>
</record>
<record id="event_stage_announced" model="event.stage">
<field name="name">Announced</field>
<field name="description">The event has been publicly announced</field>
<field name="sequence">3</field>
</record>
<record id="event_stage_done" model="event.stage">
<field name="name">Ended</field>
<field name="description">Fully ended</field>
<field name="sequence">5</field>
<field name="pipe_end" eval="True"/>
<field name="fold" eval="True"/>
</record>
<record id="event_stage_cancelled" model="event.stage">
<field name="name">Cancelled</field>
<field name="description">The event has been cancelled</field>
<field name="sequence">6</field>
<field name="pipe_end" eval="True"/>
<field name="fold" eval="True"/>
</record>
</data>
</odoo>

277
data/event_demo.xml Normal file
View File

@ -0,0 +1,277 @@
<?xml version="1.0"?>
<odoo>
<data noupdate="1">
<record id="base.user_demo" model="res.users">
<field name="groups_id" eval="[(3, ref('event.group_event_manager'))]"/>
</record>
</data>
<!-- Event -->
<record id="event.event_0" model="event.event">
<field name="name">Design Fair Los Angeles</field>
<field name="user_id" ref="base.user_demo"/>
<field name="date_begin" eval="(DateTime.now() + timedelta(days=10)).strftime('%Y-%m-%d 08:00:00')"/>
<field name="date_end" eval="(DateTime.now() + timedelta(days=14)).strftime('%Y-%m-%d 18:00:00')"/>
<field name="seats_limited">True</field>
<field name="seats_max">50</field>
<field name="address_id" ref="event.res_partner_location_2"/>
<field name="date_tz">US/Pacific</field>
<field name="event_type_id" ref="event_type_0"/>
<field name="stage_id" ref="event_stage_booked"/>
<field name="tag_ids" eval="[(4, ref('event.event_tag_category_1_tag_1')), (4, ref('event.event_tag_category_2_tag_1'))]"/>
<field name="ticket_instructions" type="html">
<div class="text-center fw-bold py-3">Important ticket information</div>
<ul>
<li>Please come <b>at least</b> 30 minutes before the beginning of the event.</li>
<li>Tickets can be printed or scanned directly from your phone.</li>
<li>If you don't have this ticket, you will <b>not</b> be allowed entry!</li>
</ul>
</field>
</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_datetime" eval="(DateTime.today() + timedelta(days=5)).strftime('%Y-%m-%d 00:00:00')"/>
<field name="end_sale_datetime" eval="(DateTime.today() + timedelta(days=10)).strftime('%Y-%m-%d 23:00:00')"/>
<field name="seats_max">0</field>
<field name="sequence">1</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="start_sale_datetime" eval="(DateTime.today() + timedelta(days=5)).strftime('%Y-%m-%d 00:00:00')"/>
<field name="end_sale_datetime" eval="(DateTime.today() + timedelta(days=10)).strftime('%Y-%m-%d 23:00:00')"/>
<field name="seats_max">50</field>
<field name="sequence">2</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="start_sale_datetime" eval="(DateTime.today() + timedelta(days=5)).strftime('%Y-%m-%d 00:00:00')"/>
<field name="end_sale_datetime" eval="(DateTime.today() + timedelta(days=10)).strftime('%Y-%m-%d 23:00:00')"/>
<field name="seats_max">10</field>
<field name="sequence">3</field>
</record>
<record id="event.event_1" model="event.event">
<field name="name">Great Reno Ballon Race</field>
<field name="user_id" ref="base.user_admin"/>
<field eval="(DateTime.today()+ timedelta(days=100)).strftime('%Y-%m-%d 20:15:00')" name="date_begin"/>
<field eval="(DateTime.today()+ timedelta(days=101)).strftime('%Y-%m-%d 00:30:00')" name="date_end"/>
<field name="event_type_id" ref="event_type_2"/>
<field name="address_id" ref="event.res_partner_location_0"/>
<field name="stage_id" ref="event_stage_booked"/>
<field name="kanban_state">blocked</field>
<field name="tag_ids" eval="[(4, ref('event.event_tag_category_1_tag_4')), (4, ref('event.event_tag_category_2_tag_3'))]"/>
</record>
<record id="message_event_1_0" model="mail.message">
<field name="model">event.event</field>
<field name="res_id" ref="event.event_1"/>
<field name="body" type="html"><p>Hello Marc Demo,<br/>
Our flight authorizations have been revoked due to insurance issues.<br/>
Could you take care of it as soon as possible?</p>
</field>
<field name="message_type">comment</field>
<field name="subtype_id" ref="mail.mt_comment"/>
<field name="author_id" ref="base.partner_admin"/>
</record>
<record id="message_event_1_1" model="mail.message">
<field name="model">event.event</field>
<field name="res_id" ref="event.event_1"/>
<field name="parent_id" ref="message_event_1_0"/>
<field name="body" type="html"><p>Hi Mitchell Admin,<br/>I will take care of it today!</p></field>
<field name="message_type">comment</field>
<field name="subtype_id" ref="mail.mt_comment"/>
<field name="author_id" ref="base.partner_demo"/>
</record>
<record id="message_event_1_2" model="mail.message">
<field name="model">event.event</field>
<field name="res_id" ref="event.event_1"/>
<field name="parent_id" ref="message_event_1_1"/>
<field name="body" type="html"><p>Great! This event will stay "blocked" until it is fixed.<br/>
Feel free to green it once everything is in order.</p>
</field>
<field name="message_type">comment</field>
<field name="subtype_id" ref="mail.mt_comment"/>
<field name="author_id" ref="base.partner_admin"/>
</record>
<record id="activity_event_1_0" model="mail.activity">
<field name="res_id" ref="event.event_1" />
<field name="res_model_id" ref="event.model_event_event"/>
<field name="activity_type_id" ref="mail.mail_activity_data_call"/>
<field name="summary">Call the local state house.</field>
<field name="date_deadline" eval="DateTime.today()"/>
<field name="create_uid" ref="base.user_demo"/>
<field name="user_id" ref="base.user_demo"/>
</record>
<record id="event_2" model="event.event">
<field name="name">Conference for Architects</field>
<field name="user_id" ref="base.user_admin"/>
<field eval="(DateTime.today()+ timedelta(days=5)).strftime('%Y-%m-%d 07:00:00')" name="date_begin"/>
<field eval="(DateTime.today()+ timedelta(days=5)).strftime('%Y-%m-%d 16:30:00')" name="date_end"/>
<field name="address_id" ref="event.res_partner_location_2"/>
<field name="seats_limited">True</field>
<field name="seats_max">200</field>
<field name="stage_id" ref="event_stage_booked"/>
<field name="tag_ids" eval="[(4, ref('event.event_tag_category_1_tag_4')), (4, ref('event.event_tag_category_2_tag_1'))]"/>
</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_datetime" eval="(DateTime.today() + timedelta(90)).strftime('%Y-%m-%d 23:00:00')"/>
<field name="seats_max">50</field>
<field name="sequence">4</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_datetime" eval="(DateTime.today() + timedelta(60)).strftime('%Y-%m-%d 23:00:00')"/>
<field name="seats_max">5</field>
<field name="sequence">5</field>
</record>
<record id="activity_event_2_0" model="mail.activity">
<field name="res_id" ref="event.event_2" />
<field name="res_model_id" ref="event.model_event_event"/>
<field name="activity_type_id" ref="mail.mail_activity_data_call"/>
<field name="summary">Call the caterer.</field>
<field name="date_deadline" eval="(DateTime.today() + relativedelta(days=3)).strftime('%Y-%m-%d %H:%M')"/>
<field name="create_uid" ref="base.user_admin"/>
<field name="user_id" ref="base.user_admin"/>
</record>
<record id="event_2_mail_0" model="event.mail">
<field name="event_id" ref="event.event_2"/>
<field name="template_ref" eval="'mail.template,%i' % ref('event.event_subscription')"/>
</record>
<record id="event.event_3" model="event.event">
<field name="name">Live Music Festival</field>
<field name="user_id" ref="base.user_demo"/>
<field name="date_begin" eval="(DateTime.today()+ timedelta(days=130)).strftime('%Y-%m-%d 20:15:00')"/>
<field name="date_end" eval="(DateTime.today()+ timedelta(days=133)).strftime('%Y-%m-%d 00:30:00')"/>
<field name="date_tz">Europe/London</field>
<field name="event_type_id" ref="event_type_0"/>
<field name="address_id" ref="event.res_partner_location_1"/>
<field name="stage_id" ref="event_stage_announced"/>
<field name="tag_ids" eval="[(4, ref('event.event_tag_category_1_tag_3')), (4, ref('event.event_tag_category_2_tag_2'))]"/>
</record>
<record id="event_3_ticket_0" model="event.event.ticket">
<field name="name">Standard</field>
<field name="event_id" ref="event.event_3"/>
<field name="end_sale_datetime" eval="(DateTime.today() + timedelta(days=20)).strftime('%Y-%m-%d 23:00:00')"/>
<field name="seats_max">1200</field>
<field name="sequence">6</field>
</record>
<record id="event_3_ticket_1" model="event.event.ticket">
<field name="name">VIP</field>
<field name="event_id" ref="event.event_3"/>
<field name="end_sale_datetime" eval="(DateTime.today() + timedelta(days=20)).strftime('%Y-%m-%d 23:00:00')"/>
<field name="seats_max">50</field>
<field name="sequence">7</field>
</record>
<record id="activity_event_3_0" model="mail.activity">
<field name="res_id" ref="event.event_3" />
<field name="res_model_id" ref="event.model_event_event"/>
<field name="activity_type_id" ref="mail.mail_activity_data_call"/>
<field name="summary">Prepare interview with local media.</field>
<field name="date_deadline" eval="DateTime.today().strftime('%Y-%m-%d %H:%M')"/>
<field name="create_uid" ref="base.user_admin"/>
<field name="user_id" ref="base.user_admin"/>
</record>
<record id="event_3_mail_0" model="event.mail">
<field name="event_id" ref="event.event_3"/>
<field name="template_ref" eval="'mail.template,%i' % ref('event.event_subscription')"/>
</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 name="date_begin" eval="(DateTime.today() - timedelta(days=5)).strftime('%Y-%m-%d 18:00:00')"/>
<field name="date_end" eval="(DateTime.today() - timedelta(days=5)).strftime('%Y-%m-%d 22:30:00')"/>
<field name="seats_limited">True</field>
<field name="seats_max">4</field>
<field name="address_id" ref="event.res_partner_location_2"/>
<field name="date_tz">US/Pacific</field>
<field name="event_type_id" ref="event_type_1"/>
<field name="stage_id" ref="event_stage_done"/>
<field name="kanban_state">done</field>
<field name="tag_ids" eval="[(4, ref('event.event_tag_category_1_tag_4')), (4, ref('event.event_tag_category_2_tag_1'))]"/>
</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_datetime" eval="(DateTime.today() - timedelta(30)).strftime('%Y-%m-%d 23:00:00')"/>
<field name="seats_max">4</field>
<field name="sequence">8</field>
</record>
<record id="activity_event_4_0" model="mail.activity">
<field name="res_id" ref="event.event_4" />
<field name="res_model_id" ref="event.model_event_event"/>
<field name="activity_type_id" ref="mail.mail_activity_data_call"/>
<field name="summary">Prepare after movie.</field>
<field name="date_deadline" eval="(DateTime.today() + relativedelta(days=3)).strftime('%Y-%m-%d %H:%M')"/>
<field name="create_uid" ref="base.user_admin"/>
<field name="user_id" ref="base.user_admin"/>
</record>
<record id="event.event_5" model="event.event">
<field name="name">Hockey Tournament</field>
<field name="user_id" ref="base.user_demo"/>
<field eval="(DateTime.today()+ timedelta(days=370)).strftime('%Y-%m-%d 09:00:00')" name="date_begin"/>
<field eval="(DateTime.today()+ timedelta(days=371)).strftime('%Y-%m-%d 17:00:00')" name="date_end"/>
<field name="event_type_id" ref="event_type_2"/>
<field name="address_id" ref="event.res_partner_location_1"/>
<field name="tag_ids" eval="[(6, 0, [ref('event.event_tag_category_1_tag_2'), ref('event.event_tag_category_2_tag_3')])]"/>
</record>
<record id="event.event_6" model="event.event">
<field name="name">An unpublished event</field>
<field name="user_id" ref="base.user_admin"/>
<field eval="(DateTime.today()+ timedelta(days=30)).strftime('%Y-%m-%d 09:30:00')" name="date_begin"/>
<field eval="(DateTime.today()+ timedelta(days=30)).strftime('%Y-%m-%d 17:30:00')" name="date_end"/>
<field name="event_type_id" ref="event_type_0"/>
<field name="address_id" ref="event.res_partner_location_1"/>
</record>
<record id="event.event_7" model="event.event">
<field name="name">OpenWood Collection Online Reveal</field>
<field name="date_tz">Europe/Brussels</field>
<field name="event_type_id" ref="event_type_0"/>
<field name="stage_id" ref="event.event_stage_booked"/>
<field name="user_id" ref="base.user_demo"/>
<field name="date_begin" eval="(DateTime.now() - timedelta(days=1)).strftime('%Y-%m-%d 05:00:00')"/>
<field name="date_end" eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d 15:00:00')"/>
<field name="address_id" eval="False"/>
<field name="tag_ids" eval="[(4, ref('event.event_tag_category_3_tag_1'))]"/>
<field name="description" type="html">
<div class="oe_structure">
<h5>The finest OpenWood furnitures are coming to your house in a brand new collection</h5>
<p>And this time, we go fully ONLINE! Meet us in our live streams from the comfort of your house.<br/>
Special discount codes will be handed out during the various streams, make sure to be there on time.</p>
<p class="mb-3">For any additional information, please contact us at <a href="mailto:events@idea.com">events@idea.com</a>.</p>
<div class="bg-light rounded-end border-start border-secondary p-3 mb-5" style="border-start-width: 3px !important;">
<p class="mb-1">This event is fully online and FREE, if you have paid for tickets, you should get a refund.<br/>
It will require a good Internet connection to get the best video quality.</p>
</div>
</div>
</field>
</record>
<record id="event_7_ticket_1" model="event.event.ticket">
<field name="name">Standard</field>
<field name="event_id" ref="event.event_7"/>
<field name="end_sale_datetime" eval="(DateTime.now() + timedelta(days=2)).strftime('%Y-%m-%d 15:00:00')"/>
<field name="sequence">9</field>
</record>
<record id="event_7_ticket_2" model="event.event.ticket">
<field name="name">VIP</field>
<field name="event_id" ref="event.event_7"/>
<field name="end_sale_datetime" eval="(DateTime.now() + timedelta(days=2)).strftime('%Y-%m-%d 15:00:00')"/>
<field name="seats_max">10</field>
<field name="sequence">10</field>
</record>
</odoo>

93
data/event_demo_misc.xml Normal file
View File

@ -0,0 +1,93 @@
<?xml version="1.0"?>
<odoo><data>
<!-- Event Type -->
<record id="event_type_0" model="event.type">
<field name="name">Exhibition</field>
<field name="sequence">3</field>
</record>
<record id="event_type_1" model="event.type">
<field name="name">Training</field>
<field name="sequence">4</field>
</record>
<record id="event_type_2" model="event.type">
<field name="name">Sport</field>
<field name="default_timezone">US/Pacific</field>
<field name="sequence">5</field>
</record>
<!-- Category and Tags -->
<record id="event_tag_category_1" model="event.tag.category">
<field name="name">Age</field>
<field name="sequence">3</field>
</record>
<record id="event_tag_category_2" model="event.tag.category">
<field name="name">Activity</field>
<field name="sequence">1</field>
</record>
<record id="event_tag_category_3" model="event.tag.category">
<field name="name">Type</field>
<field name="sequence">2</field>
</record>
<record id="event_tag_category_1_tag_1" model="event.tag">
<field name="name">5-10</field>
<field name="sequence">1</field>
<field name="category_id" ref="event_tag_category_1"/>
<field name="color">1</field>
</record>
<record id="event_tag_category_1_tag_2" model="event.tag">
<field name="name">10-14</field>
<field name="sequence">2</field>
<field name="category_id" ref="event_tag_category_1"/>
<field name="color">2</field>
</record>
<record id="event_tag_category_1_tag_3" model="event.tag">
<field name="name">15-18</field>
<field name="sequence">3</field>
<field name="category_id" ref="event_tag_category_1"/>
<field name="color">3</field>
</record>
<record id="event_tag_category_1_tag_4" model="event.tag">
<field name="name">18+</field>
<field name="sequence">4</field>
<field name="category_id" ref="event_tag_category_1"/>
<field name="color">4</field>
</record>
<record id="event_tag_category_2_tag_1" model="event.tag">
<field name="name">Culture</field>
<field name="sequence">10</field>
<field name="category_id" ref="event_tag_category_2"/>
<field name="color">5</field>
</record>
<record id="event_tag_category_2_tag_2" model="event.tag">
<field name="name">Music</field>
<field name="sequence">11</field>
<field name="category_id" ref="event_tag_category_2"/>
<field name="color">6</field>
</record>
<record id="event_tag_category_2_tag_3" model="event.tag">
<field name="name">Sport</field>
<field name="sequence">12</field>
<field name="category_id" ref="event_tag_category_2"/>
<field name="color">7</field>
</record>
<record id="event_tag_category_3_tag_1" model="event.tag">
<field name="name">Online</field>
<field name="sequence">20</field>
<field name="category_id" ref="event_tag_category_3"/>
<field name="color">8</field>
</record>
<record id="event_tag_category_3_tag_2" model="event.tag">
<field name="name">Conference</field>
<field name="sequence">21</field>
<field name="category_id" ref="event_tag_category_3"/>
<field name="color">9</field>
</record>
</data></odoo>

View File

@ -0,0 +1,166 @@
<?xml version="1.0"?>
<odoo><data>
<!-- Design fair -->
<record id="event_registration_0_0" model="event.registration">
<field name="create_date" eval="DateTime.now() - relativedelta(days=2)"/>
<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="create_date" eval="DateTime.now() - relativedelta(days=2)"/>
<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="create_date" eval="DateTime.now() - relativedelta(days=2)"/>
<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="email">tuck@test.example.com</field>
<field name="partner_id" eval="False"/>
</record>
<!-- Reno Ballon Race -->
<record id="event_registration_1_0" model="event.registration">
<field name="event_id" ref="event.event_1"/>
<field name="partner_id" ref="base.res_partner_address_1"/>
</record>
<record id="event_registration_1_1" model="event.registration">
<field name="event_id" ref="event.event_1"/>
<field name="partner_id" ref="base.res_partner_address_2"/>
</record>
<record id="event_registration_1_2" model="event.registration">
<field name="event_id" ref="event.event_1"/>
<field name="name">Piers Morgan</field>
<field name="email">piersm@test.example.com</field>
<field name="partner_id" eval="False"/>
</record>
<record id="event_registration_1_3" model="event.registration">
<field name="event_id" ref="event.event_1"/>
<field name="partner_id" ref="base.res_partner_address_3"/>
</record>
<record id="event_registration_1_4" model="event.registration">
<field name="event_id" ref="event.event_1"/>
<field name="partner_id" ref="base.res_partner_address_4"/>
</record>
<record id="event_registration_1_5" model="event.registration">
<field name="event_id" ref="event.event_1"/>
<field name="name">Nigel Woodfire</field>
<field name="email">nigelw@test.example.com</field>
<field name="partner_id" eval="False"/>
</record>
<!-- Conference for architects -->
<record id="event_registration_2_0" model="event.registration">
<field name="create_date" eval="DateTime.now() - relativedelta(days=0.5)"/>
<field name="event_id" ref="event.event_2"/>
<field name="event_ticket_id" ref="event.event_2_ticket_1"/>
<field name="partner_id" ref="base.res_partner_address_1"/>
</record>
<record id="event_registration_2_1" model="event.registration">
<field name="create_date" eval="DateTime.now() - relativedelta(days=0.5)"/>
<field name="event_id" ref="event.event_2"/>
<field name="event_ticket_id" ref="event.event_2_ticket_1"/>
<field name="partner_id" ref="base.res_partner_address_2"/>
</record>
<record id="event_registration_2_2" model="event.registration">
<field name="create_date" eval="DateTime.now() - relativedelta(days=0.5)"/>
<field name="event_id" ref="event.event_2"/>
<field name="event_ticket_id" ref="event.event_2_ticket_2"/>
<field name="name">Piers Morgan</field>
<field name="email">piersm@test.example.com</field>
<field name="partner_id" eval="False"/>
</record>
<record id="event_registration_2_3" model="event.registration">
<field name="create_date" eval="DateTime.now() - relativedelta(days=1)"/>
<field name="event_id" ref="event.event_2"/>
<field name="event_ticket_id" ref="event.event_2_ticket_1"/>
<field name="partner_id" ref="base.res_partner_address_3"/>
</record>
<record id="event_registration_2_4" model="event.registration">
<field name="create_date" eval="DateTime.now() - relativedelta(days=1)"/>
<field name="event_id" ref="event.event_2"/>
<field name="event_ticket_id" ref="event.event_2_ticket_1"/>
<field name="partner_id" ref="base.res_partner_address_4"/>
</record>
<!-- Live Music Festival -->
<record id="event_registration_3_0" model="event.registration">
<field name="event_id" ref="event.event_3"/>
<field name="partner_id" ref="base.res_partner_address_1"/>
</record>
<record id="event_registration_3_1" model="event.registration">
<field name="event_id" ref="event.event_3"/>
<field name="partner_id" ref="base.res_partner_address_2"/>
</record>
<record id="event_registration_3_2" model="event.registration">
<field name="event_id" ref="event.event_3"/>
<field name="name">Piers Morgan</field>
<field name="email">piersm@test.example.com</field>
<field name="partner_id" eval="False"/>
</record>
<record id="event_registration_3_3" model="event.registration">
<field name="event_id" ref="event.event_3"/>
<field name="partner_id" ref="base.res_partner_address_3"/>
</record>
<record id="event_registration_3_4" model="event.registration">
<field name="event_id" ref="event.event_3"/>
<field name="partner_id" ref="base.res_partner_address_4"/>
</record>
<record id="event_registration_3_5" model="event.registration">
<field name="event_id" ref="event.event_3"/>
<field name="name">Nigel Woodfire</field>
<field name="email">nigelw@test.example.com</field>
<field name="partner_id" eval="False"/>
</record>
<!-- Business Workshop -->
<record id="event_registration_4_0" model="event.registration">
<field name="create_date" eval="DateTime.now() - relativedelta(days=8)"/>
<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="create_date" eval="DateTime.now() - relativedelta(days=7)"/>
<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="create_date" eval="DateTime.now() - relativedelta(days=7)"/>
<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>
<!-- OpenWood Collection Online Reveal: Gemini (all) -->
<record id="event_registration_7_0" model="event.registration">
<field name="event_id" ref="event.event_7"/>
<field name="event_ticket_id" ref="event.event_7_ticket_1"/>
<field name="partner_id" ref="base.res_partner_address_5"/>
</record>
<record id="event_registration_7_1" model="event.registration">
<field name="event_id" ref="event.event_7"/>
<field name="event_ticket_id" ref="event.event_7_ticket_1"/>
<field name="partner_id" ref="base.res_partner_address_10"/>
</record>
<record id="event_registration_7_2" model="event.registration">
<field name="event_id" ref="event.event_7"/>
<field name="event_ticket_id" ref="event.event_7_ticket_2"/>
<field name="partner_id" ref="base.res_partner_address_11"/>
</record>
<record id="event_registration_7_3" model="event.registration">
<field name="event_id" ref="event.event_7"/>
<field name="event_ticket_id" ref="event.event_7_ticket_2"/>
<field name="partner_id" ref="base.res_partner_address_25"/>
</record>
<function model="event.registration"
name="action_set_done"
eval="[[ref('event_registration_4_0'), ref('event_registration_4_1')]]"
/>
</data></odoo>

15
data/ir_cron_data.xml Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data noupdate="1">
<!-- Event Mail Scheduler-->
<record model="ir.cron" forcecreate="True" id="event_mail_scheduler">
<field name="name">Event: Mail Scheduler</field>
<field name="model_id" ref="model_event_mail"/>
<field name="state">code</field>
<field name="code">model.schedule_communications(autocommit=True)</field>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">hours</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False" />
</record>
</data></odoo>

708
data/mail_template_data.xml Normal file
View File

@ -0,0 +1,708 @@
<?xml version="1.0"?>
<odoo>
<data noupdate="1">
<record id="event_registration_mail_template_badge" model="mail.template">
<field name="name">Event: Registration Badge</field>
<field name="model_id" ref="event.model_event_registration"/>
<field name="subject">Your badge for {{ object.event_id.name }}</field>
<field name="email_from">{{ (object.event_id.organizer_id.email_formatted or object.event_id.user_id.email_formatted or '') }}</field>
<field name="email_to">{{ (object.email and '"%s" &lt;%s&gt;' % (object.name, object.email) or object.partner_id.email_formatted or '') }}</field>
<field name="description">Sent automatically to someone after they registered to an event</field>
<field name="body_html" type="html">
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
<t t-set="date_begin" t-value="format_datetime(object.event_id.date_begin, tz='UTC', dt_format=&quot;yyyyMMdd'T'HHmmss'Z'&quot;)"/>
<t t-set="date_end" t-value="format_datetime(object.event_id.date_end, tz='UTC', dt_format=&quot;yyyyMMdd'T'HHmmss'Z'&quot;)"/>
<t t-set="is_online" t-value="'is_published' in object.event_id and object.event_id.is_published"/>
<t t-set="event_organizer" t-value="object.event_id.organizer_id"/>
<t t-set="event_address" t-value="object.event_id.address_id"/>
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
<tbody>
<!-- HEADER -->
<tr>
<td align="center" style="min-width: 590px;">
<table width="590" border="0" cellpadding="0" cellspacing="0" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="middle">
<span style="font-size: 10px;">Your registration</span><br/>
<span style="font-size: 20px; font-weight: bold;">
<t t-out="object.name or 'Guest'"/>
</span>
</td><td valign="middle" align="right">
<a t-attf-href="/event/{{ object.event_id.id }}/my_tickets?badge_mode=1&amp;registration_ids={{ object.ids }}&amp;tickets_hash={{ object.event_id._get_tickets_access_hash(object.ids) }}"
target="_blank" style="padding: 8px 12px; font-size: 12px; color: #FFFFFF; text-decoration: none !important; font-weight: 400; background-color: #875A7B; border: 0px solid #875A7B; border-radius:3px">
Download Badge
</a>
<t t-if="not object.company_id.uses_default_logo">
<img t-att-src="'/logo.png?company=%s' % object.company_id.id" style="padding: 0px; margin: 0px; height: auto; width: 80px;" t-att-alt="'%s' % object.company_id.name"/>
</t>
</td></tr>
<tr><td colspan="2" style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin:16px 0px 16px 0px;"/>
</td></tr>
</table>
</td>
</tr>
<!-- EVENT DESCRIPTION -->
<tr>
<td align="center" style="min-width: 590px;">
<table width="590" border="0" cellpadding="0" cellspacing="0" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="top" style="font-size: 14px;">
<div>
Hello <t t-out="object.name or 'Guest'"/>,<br/>
Please find attached your badge for
<t t-if="is_online">
<a t-att-href="object.event_id.website_url" style="color:#875A7B;text-decoration:none;" t-out="object.event_id.name or ''">OpenWood Collection Online Reveal</a>
</t>
<t t-else="">
<strong t-out="object.event_id.name or ''">OpenWood Collection Online Reveal</strong>.
</t>
</div>
<div>
<br />
<strong>Add this event to your calendar</strong>
<a t-attf-href="https://www.google.com/calendar/render?action=TEMPLATE&amp;text={{ object.event_id.name }}&amp;dates={{ date_begin }}/{{ date_end }}&amp;location={{ location }}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16" alt=""/> Google</a>
<a t-attf-href="/event/{{ slug(object.event_id) }}/ics" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16" alt=""/> iCal/Outlook</a>
<a t-attf-href="https://calendar.yahoo.com/?v=60&amp;view=d&amp;type=20&amp;title={{ object.event_id.name }}&amp;in_loc={{ location }}&amp;st={{ format_datetime(object.event_id.date_begin, tz='UTC', dt_format='yyyyMMdd\'T\'HHmmss') }}&amp;et={{ format_datetime(object.event_id.date_end, tz='UTC', dt_format='yyyyMMdd\'T\'HHmmss') }}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new">
<img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16" alt=""/> Yahoo
</a>
<br /><br />
</div>
<div>
See you soon,<br/>
<span style="color: #454748;">
-- <br/>
<t t-if="event_organizer">
<t t-out="event_organizer.name or ''">YourCompany</t>
</t>
<t t-else="">
The <t t-out="object.event_id.name or ''">OpenWood Collection Online Reveal</t> Team
</t>
</span>
</div>
</td></tr>
<tr><td style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</td></tr>
</table>
</td>
</tr>
<!-- DETAILS -->
<tr>
<td align="center" style="min-width: 590px;">
<table width="590" border="0" cellpadding="0" cellspacing="0" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="top" style="font-size: 14px;">
<table style="width:100%;">
<tr>
<td style="vertical-align:top;">
<img src="/web_editor/font_to_img/61555/rgb(81,81,102)/34" style="padding:4px;max-width:inherit;" height="34" alt=""/>
</td>
<td style="padding: 0px 10px 0px 10px;width:50%;line-height:20px;vertical-align:top;">
<div><t t-out="object.event_id.name or ''">OpenWood Collection Online Reveal</t></div>
<div><strong>From</strong> <t t-out="object.event_id.date_begin_located or ''">May 4, 2021, 7:00:00 AM</t></div>
<div><strong>To</strong> <t t-out="object.event_id.date_end_located or ''">May 6, 2021, 5:00:00 PM</t></div>
<div style="font-size:12px;color:#9e9e9e"><i>(<t t-out="object.event_id.date_tz or ''">Europe/Brussels</t>)</i></div>
</td>
<td style="vertical-align:top;">
<t t-if="event_address">
<img src="/web_editor/font_to_img/61505/rgb(81,81,102)/34" style="padding:4px;max-width:inherit;" height="34" alt=""/>
</t>
</td>
<td style="padding: 0px 10px 0px 10px;width:50%;vertical-align:top;">
<t t-if="event_address">
<t t-set="location" t-value="''"/>
<t t-if="object.event_id.address_id.name">
<div t-out="object.event_id.address_id.name">Teksa SpA</div>
</t>
<t t-if="object.event_id.address_id.street">
<div t-out="object.event_id.address_id.street">Puerto Madero 9710</div>
<t t-set="location" t-value="object.event_id.address_id.street"/>
</t>
<t t-if="object.event_id.address_id.street2">
<div t-out="object.event_id.address_id.street2">Of A15, Santiago (RM)</div>
<t t-set="location" t-valuef="{{location}}, {{object.event_id.address_id.street2}}"/>
</t>
<div>
<t t-if="object.event_id.address_id.city">
<t t-out="object.event_id.address_id.city">Pudahuel</t>,
<t t-set="location" t-valuef="{{location}}, {{object.event_id.address_id.city}}"/>
</t>
<t t-if="object.event_id.address_id.state_id.name">
<t t-out="object.event_id.address_id.state_id.name">C1</t>,
<t t-set="location" t-valuef="{{location}}, {{object.event_id.address_id.state_id.name}}"/>
</t>
<t t-if="object.event_id.address_id.zip">
<t t-out="object.event_id.address_id.zip">98450</t>
<t t-set="location" t-valuef="{{location}}, {{object.event_id.address_id.zip}}"/>
</t>
</div>
<t t-if="object.event_id.address_id.country_id.name">
<div t-out="object.event_id.address_id.country_id.name">Argentina</div>
<t t-set="location" t-valuef="{{location}}, {{object.event_id.address_id.country_id.name}}"/>
</t>
</t>
</td>
</tr>
</table>
</td></tr>
<tr><td style="text-align:center;">
<t t-if="event_organizer">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</t>
</td></tr>
<tr><td valign="top" style="font-size: 14px;">
<!-- CONTACT ORGANIZER -->
<t t-if="event_organizer">
<div>
<span style="font-weight:300;margin:10px 0px">Questions about this event?</span>
<div>Please contact the organizer:</div>
<ul>
<li><t t-out="event_organizer.name or ''">YourCompany</t></li>
<t t-if="event_organizer.email">
<li>Mail: <a t-attf-href="mailto:{{ event_organizer.email }}" style="text-decoration:none;color:#875A7B;" t-out="event_organizer.email">info@yourcompany.com</a></li>
</t>
<t t-if="event_organizer.phone">
<li>Phone: <t t-out="event_organizer.phone">+1 650-123-4567</t></li>
</t>
</ul>
</div>
</t>
</td></tr>
<tr><td style="text-align:center;">
<!-- CONTACT ORGANIZER SEPARATION -->
<t t-if="is_online or event_address">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</t>
</td></tr>
<tr><td valign="top" style="font-size: 14px;">
<!-- PWA MARKGETING -->
<t t-if="is_online">
<div>
<strong>Get the best mobile experience.</strong>
<a href="/event">Install our mobile app</a>
</div>
</t>
</td></tr>
<tr><td style="text-align:center;">
<!-- PWA MARKGETING SEPARATION-->
<t t-if="is_online and event_address">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</t>
</td></tr>
<tr><td valign="top" style="font-size: 14px;">
<!-- GOOGLE MAPS LINK -->
<t t-if="event_address and location">
<table style="width:100%;"><tr><td>
<div>
<i class="fa fa-map-marker"/>
<a t-attf-href="https://maps.google.com/maps?q={{ location }}" target="new">
<img t-if="event_address.static_map_url and event_address.static_map_url_is_valid"
t-att-src="event_address.static_map_url"
style="vertical-align:bottom; width: 100%;" alt="Google Maps"/>
<t t-else="">See location on Google Maps</t>
</a>
</div>
</td></tr></table>
</t>
</td></tr>
</table>
</td>
</tr>
</tbody>
</table>
</td></tr>
<!-- FOOTER BY -->
<tr><td align="center" style="min-width: 590px;">
<t t-if="object.company_id">
<table width="590" border="0" cellpadding="0" cellspacing="0" style="min-width: 590px; background-color: #F1F1F1; color: #454748; padding: 8px; border-collapse:separate;">
<tr><td style="text-align: center; font-size: 14px;">
Sent by <a target="_blank" t-attf-href="{{ object.company_id.website }}" style="color: #875A7B;" t-out="object.company_id.name or ''">YourCompany</a>
<t t-if="is_online">
<br />
Discover <a href="/event" style="color:#875A7B;">all our events</a>.
</t>
</td></tr>
</table>
</t>
</td></tr>
</table>
</field>
<field name="report_template_ids" eval="[(4, ref('event.action_report_event_registration_badge'))]"/>
<field name="lang">{{ object.event_id.lang or object.partner_id.lang }}</field>
<field name="auto_delete" eval="True"/>
</record>
<record id="event_subscription" model="mail.template">
<field name="name">Event: Registration Confirmation</field>
<field name="model_id" ref="event.model_event_registration"/>
<field name="subject">Your registration at {{ object.event_id.name }}</field>
<field name="email_from">{{ (object.event_id.organizer_id.email_formatted or object.event_id.user_id.email_formatted or '') }}</field>
<field name="email_to">{{ (object.email and '"%s" &lt;%s&gt;' % (object.name, object.email) or object.partner_id.email_formatted or '') }}</field>
<field name="description">Sent to attendees after registering to an event</field>
<field name="body_html" type="html">
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
<t t-set="date_begin" t-value="format_datetime(object.event_id.date_begin, tz='UTC', dt_format=&quot;yyyyMMdd'T'HHmmss'Z'&quot;)"/>
<t t-set="date_end" t-value="format_datetime(object.event_id.date_end, tz='UTC', dt_format=&quot;yyyyMMdd'T'HHmmss'Z'&quot;)"/>
<t t-set="is_online" t-value="'is_published' in object.event_id and object.event_id.is_published"/>
<t t-set="is_sale" t-value="'sale_order_id' in object and object.sale_order_id"/>
<t t-set="event_organizer" t-value="object.event_id.organizer_id"/>
<t t-set="event_address" t-value="object.event_id.address_id"/>
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
<tbody>
<!-- HEADER -->
<tr>
<td align="center" style="min-width: 590px;">
<table width="590" border="0" cellpadding="0" cellspacing="0" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="middle">
<span style="font-size: 10px;">Your registration</span><br/>
<span style="font-size: 20px; font-weight: bold;">
<t t-out="object.name or 'Guest'"/>
</span>
</td><td valign="middle" align="right">
<div style="margin-bottom: 5px;">
<a t-attf-href="/event/{{ object.event_id.id }}/my_tickets?registration_ids={{ object.ids }}&amp;tickets_hash={{ object.event_id._get_tickets_access_hash(object.ids) }}&amp;responsive_html=1"
target="_blank" style="padding: 8px 12px; font-size: 12px; color: #FFFFFF; text-decoration: none !important; font-weight: 400; background-color: #875A7B; border: 0px solid #875A7B; border-radius:3px">
View Ticket
</a>
</div>
<t t-if="object.barcode">
<div style="margin-bottom: 5px;">
<img t-attf-src="/report/barcode/QR/{{object.barcode}}?&amp;width=100&amp;height=100&amp;quiet=0" width="100" height="100" alt="QR Code"/>
</div>
</t>
<t t-if="not object.company_id.uses_default_logo">
<img t-att-src="'/logo.png?company=%s' % object.company_id.id" style="padding: 0px; margin: 0px; height: auto; width: 80px;" t-att-alt="'%s' % object.company_id.name"/>
</t>
</td></tr>
<tr><td colspan="2" style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin:16px 0px 16px 0px;"/>
</td></tr>
</table>
</td>
</tr>
<!-- EVENT DESCRIPTION -->
<tr>
<td align="center" style="min-width: 590px;">
<table width="590" border="0" cellpadding="0" cellspacing="0" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="top" style="font-size: 14px;">
<div>
Hello <t t-out="object.name or 'Guest'"/>,<br/>
We are happy to confirm your registration to the event
<t t-if="is_online">
<a t-att-href="object.event_id.website_url" style="color:#875A7B;text-decoration:none;" t-out="object.event_id.name or ''">OpenWood Collection Online Reveal</a>
</t>
<t t-else="">
<strong t-out="object.event_id.name or ''">OpenWood Collection Online Reveal</strong>
</t>.
<t t-if="object.partner_id and object.partner_id.name and object.partner_id.name != object.name">
This ticket was registered by <t t-out="object.partner_id.name"/>.
</t>
</div>
<div t-if="is_sale">
<br/>
The order for this ticket has reference <t t-out="object.sale_order_id.name"/>
and was placed on <t t-out="object.sale_order_id.date_order.date()"/>
<t t-if="object.sale_order_line_id.price_unit"> for an amount of
<t t-out="object.sale_order_line_id.price_unit" t-options="{'widget': 'monetary', 'display_currency': object.sale_order_line_id.currency_id}"/>
</t>.
</div>
<div>
<br />
<strong>Add this event to your calendar</strong>
<a t-attf-href="https://www.google.com/calendar/render?action=TEMPLATE&amp;text={{ object.event_id.name }}&amp;dates={{ date_begin }}/{{ date_end }}&amp;location={{ location }}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16" alt=""/> Google</a>
<a t-attf-href="/event/{{ slug(object.event_id) }}/ics" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16" alt=""/> iCal/Outlook</a>
<a t-attf-href="https://calendar.yahoo.com/?v=60&amp;view=d&amp;type=20&amp;title={{ object.event_id.name }}&amp;in_loc={{ location }}&amp;st={{ format_datetime(object.event_id.date_begin, tz='UTC', dt_format='yyyyMMdd\'T\'HHmmss') }}&amp;et={{ format_datetime(object.event_id.date_end, tz='UTC', dt_format='yyyyMMdd\'T\'HHmmss') }}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new">
<img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16" alt=""/> Yahoo
</a>
<br /><br />
</div>
<div>
See you soon,<br/>
<span style="color: #454748;">
-- <br/>
<t t-if="event_organizer">
<t t-out="event_organizer.name">YourCompany</t>
</t>
<t t-else="">
The <t t-out="object.event_id.name or ''">OpenWood Collection Online Reveal</t> Team
</t>
</span>
</div>
</td></tr>
<tr><td style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</td></tr>
</table>
</td>
</tr>
<!-- DETAILS -->
<tr>
<td align="center" style="min-width: 590px;">
<table width="590" border="0" cellpadding="0" cellspacing="0" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="top" style="font-size: 14px;">
<table style="width:100%;">
<tr>
<td style="vertical-align:top;">
<img src="/web_editor/font_to_img/61555/rgb(81,81,102)/34" style="padding:4px;max-width:inherit;" height="34" alt=""/>
</td>
<td style="padding: 0px 10px 0px 10px;width:50%;line-height:20px;vertical-align:top;">
<div><t t-out="object.event_id.name or ''">OpenWood Collection Online Reveal</t></div>
<div><strong>From</strong> <t t-out="object.event_id.date_begin_located or ''">May 4, 2021, 7:00:00 AM</t></div>
<div><strong>To</strong> <t t-out="object.event_id.date_end_located or ''">May 6, 2021, 5:00:00 PM</t></div>
<div style="font-size:12px;color:#9e9e9e"><i>(<t t-out="object.event_id.date_tz or ''">Europe/Brussels</t>)</i></div>
</td>
<td style="vertical-align:top;">
<t t-if="event_address">
<img src="/web_editor/font_to_img/61505/rgb(81,81,102)/34" style="padding:4px;max-width:inherit;" height="34" alt=""/>
</t>
</td>
<td style="padding: 0px 10px 0px 10px;width:50%;vertical-align:top;">
<t t-if="event_address">
<t t-set="location" t-value="''"/>
<t t-if="object.event_id.address_id.name">
<div t-out="object.event_id.address_id.name">Teksa SpA</div>
</t>
<t t-if="object.event_id.address_id.street">
<div t-out="object.event_id.address_id.street">Puerto Madero 9710</div>
<t t-set="location" t-value="object.event_id.address_id.street"/>
</t>
<t t-if="object.event_id.address_id.street2">
<div t-out="object.event_id.address_id.street2">Of A15, Santiago (RM)</div>
<t t-set="location" t-valuef="{{location}}, {{object.event_id.address_id.street2}}"/>
</t>
<div>
<t t-if="object.event_id.address_id.city">
<t t-out="object.event_id.address_id.city">Pudahuel</t>,
<t t-set="location" t-valuef="{{location}}, {{object.event_id.address_id.city}}"/>
</t>
<t t-if="object.event_id.address_id.state_id.name">
<t t-out="object.event_id.address_id.state_id.name">C1</t>,
<t t-set="location" t-valuef="{{location}}, {{object.event_id.address_id.state_id.name}}"/>
</t>
<t t-if="object.event_id.address_id.zip">
<t t-out="object.event_id.address_id.zip">98450</t>
<t t-set="location" t-valuef="{{location}}, {{object.event_id.address_id.zip}}"/>
</t>
</div>
<t t-if="object.event_id.address_id.country_id.name">
<div t-out="object.event_id.address_id.country_id.name">Argentina</div>
<t t-set="location" t-valuef="{{location}}, {{object.event_id.address_id.country_id.name}}"/>
</t>
</t>
</td>
</tr>
</table>
</td></tr>
<tr><td style="text-align:center;">
<t t-if="event_organizer">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</t>
</td></tr>
<tr><td valign="top" style="font-size: 14px;">
<!-- CONTACT ORGANIZER -->
<t t-if="event_organizer">
<div>
<span style="font-weight:300;margin:10px 0px">Questions about this event?</span>
<div>Please contact the organizer:</div>
<ul>
<li><t t-out="event_organizer.name or ''">YourCompany</t></li>
<t t-if="event_organizer.email">
<li>Mail: <a t-attf-href="mailto:{{ event_organizer.email }}" style="text-decoration:none;color:#875A7B;" t-out="event_organizer.email or ''">info@yourcompany.com</a></li>
</t>
<t t-if="event_organizer.phone">
<li>Phone: <t t-out="event_organizer.phone">+1 650-123-4567</t></li>
</t>
</ul>
</div>
</t>
</td></tr>
<tr><td style="text-align:center;">
<!-- CONTACT ORGANIZER SEPARATION -->
<t t-if="is_online or event_address">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</t>
</td></tr>
<tr><td valign="top" style="font-size: 14px;">
<!-- PWA MARKGETING -->
<t t-if="is_online">
<div>
<strong>Get the best mobile experience.</strong>
<a href="/event">Install our mobile app</a>
</div>
</t>
</td></tr>
<tr><td style="text-align:center;">
<!-- PWA MARKGETING SEPARATION-->
<t t-if="is_online and event_address">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</t>
</td></tr>
<tr><td valign="top" style="font-size: 14px;">
<!-- GOOGLE MAPS LINK -->
<t t-if="event_address and location">
<table style="width:100%;"><tr><td>
<div>
<i class="fa fa-map-marker"/>
<a t-attf-href="https://maps.google.com/maps?q={{ location }}" target="new">
<img t-if="event_address.static_map_url and event_address.static_map_url_is_valid"
t-att-src="event_address.static_map_url"
style="vertical-align:bottom; width: 100%;" alt="Google Maps"/>
<t t-else="">See location on Google Maps</t>
</a>
</div>
</td></tr></table>
</t>
</td></tr>
</table>
</td>
</tr>
</tbody>
</table>
</td></tr>
<!-- FOOTER BY -->
<tr><td align="center" style="min-width: 590px;">
<t t-if="object.company_id">
<table width="590" border="0" cellpadding="0" cellspacing="0" style="min-width: 590px; background-color: #F1F1F1; color: #454748; padding: 8px; border-collapse:separate;">
<tr><td style="text-align: center; font-size: 14px;">
Sent by <a target="_blank" t-attf-href="{{ object.company_id.website }}" style="color: #875A7B;" t-out="object.company_id.name or ''">YourCompany</a>
<t t-if="is_online">
<br />
Discover <a href="/event" style="color:#875A7B;">all our events</a>.
</t>
</td></tr>
</table>
</t>
</td></tr>
</table>
</field>
<field name="report_template_ids" eval="[(4, ref('event.action_report_event_registration_full_page_ticket'))]"/>
<field name="lang">{{ object.event_id.lang or object.partner_id.lang }}</field>
</record>
<record id="event_reminder" model="mail.template">
<field name="name">Event: Reminder</field>
<field name="model_id" ref="event.model_event_registration"/>
<field name="subject">{{ object.event_id.name }}: {{ object.get_date_range_str() }}</field>
<field name="email_from">{{ (object.event_id.organizer_id.email_formatted or object.event_id.user_id.email_formatted or '') }}</field>
<field name="email_to">{{ (object.email and '"%s" &lt;%s&gt;' % (object.name, object.email) or object.partner_id.email_formatted or '') }}</field>
<field name="description">Sent automatically to attendees if there is a reminder defined on the event</field>
<field name="body_html" type="html">
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
<t t-set="date_begin" t-value="format_datetime(object.event_id.date_begin, tz='UTC', dt_format=&quot;yyyyMMdd'T'HHmmss'Z'&quot;)"/>
<t t-set="date_end" t-value="format_datetime(object.event_id.date_end, tz='UTC', dt_format=&quot;yyyyMMdd'T'HHmmss'Z'&quot;)"/>
<t t-set="is_online" t-value="'is_published' in object.event_id and object.event_id.is_published"/>
<t t-set="event_organizer" t-value="object.event_id.organizer_id"/>
<t t-set="event_address" t-value="object.event_id.address_id"/>
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
<tbody>
<!-- HEADER -->
<tr>
<td align="center" style="min-width: 590px;">
<table width="590" border="0" cellpadding="0" cellspacing="0" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="middle">
<span style="font-size: 10px;">Your registration</span><br/>
<span style="font-size: 20px; font-weight: bold;" t-out="object.name or 'Guest'"/>
</td><td valign="middle" align="right">
<t t-if="is_online">
<a t-attf-href="{{ object.event_id.website_url }}"
style="padding: 8px 12px; font-size: 12px; color: #FFFFFF; text-decoration: none !important; font-weight: 400; background-color: #875A7B; border: 0px solid #875A7B; border-radius:3px">
View Event
</a>
</t>
<t t-elif="not object.company_id.uses_default_logo">
<img t-att-src="'/logo.png?company=%s' % object.company_id.id" style="padding: 0px; margin: 0px; height: auto; width: 80px;" t-att-alt="'%s' % object.company_id.name"/>
</t>
</td></tr>
<tr><td colspan="2" style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin:16px 0px 16px 0px;"/>
</td></tr>
</table>
</td>
</tr>
<!-- EVENT DESCRIPTION -->
<tr>
<td align="center" style="min-width: 590px;">
<table width="590" border="0" cellpadding="0" cellspacing="0" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="top" style="font-size: 14px;">
<div>
Hello <t t-out="object.name or 'Guest'"/>,<br/>
We are excited to remind you that the event
<t t-if="is_online">
<a t-att-href="object.event_id.website_url" style="color:#875A7B;text-decoration:none;" t-out="object.event_id.name or ''">OpenWood Collection Online Reveal</a>
</t>
<t t-else="">
<strong t-out="object.event_id.name or ''">OpenWood Collection Online Reveal</strong>
</t>
is starting <strong t-out="object.get_date_range_str() or ''">today</strong>.
</div>
<div>
<br />
<strong>Add this event to your calendar</strong>
<a t-attf-href="https://www.google.com/calendar/render?action=TEMPLATE&amp;text={{ object.event_id.name }}&amp;dates={{ date_begin }}/{{ date_end }}&amp;location={{ location }}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16" alt=""/> Google</a>
<a t-attf-href="/event/{{ slug(object.event_id) }}/ics" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16" alt=""/> iCal/Outlook</a>
<a t-attf-href="https://calendar.yahoo.com/?v=60&amp;view=d&amp;type=20&amp;title={{ object.event_id.name }}&amp;in_loc={{ location }}&amp;st={{ format_datetime(object.event_id.date_begin, tz='UTC', dt_format='yyyyMMdd\'T\'HHmmss') }}&amp;et={{ format_datetime(object.event_id.date_end, tz='UTC', dt_format='yyyyMMdd\'T\'HHmmss') }}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new">
<img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16" alt=""/> Yahoo
</a>
<br /><br />
</div>
<div>
We confirm your registration and hope to meet you there,<br/>
<span style="color: #454748;">
-- <br/>
<t t-if="event_organizer">
<t t-out="event_organizer.name or ''">YourCompany</t>
</t>
<t t-else="">
The <t t-out="object.event_id.name or ''">OpenWood Collection Online Reveal</t> Team
</t>
</span>
</div>
</td></tr>
<tr><td style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</td></tr>
</table>
</td>
</tr>
<!-- DETAILS -->
<tr>
<td align="center" style="min-width: 590px;">
<table width="590" border="0" cellpadding="0" cellspacing="0" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="top" style="font-size: 14px;">
<table style="width:100%;">
<tr>
<td style="vertical-align:top;">
<img src="/web_editor/font_to_img/61555/rgb(81,81,102)/34" style="padding:4px;max-width:inherit;" height="34" alt=""/>
</td>
<td style="padding: 0px 10px 0px 10px;width:50%;line-height:20px;vertical-align:top;">
<div><strong>From</strong> <t t-out="object.event_id.date_begin_located or ''">May 4, 2021, 7:00:00 AM</t></div>
<div><strong>To</strong> <t t-out="object.event_id.date_end_located or ''">May 6, 2021, 5:00:00 PM</t></div>
<div style="font-size:12px;color:#9e9e9e"><i><t t-out="object.event_id.date_tz or ''">Europe/Brussels</t></i></div>
</td>
<td style="vertical-align:top;">
<t t-if="event_address">
<img src="/web_editor/font_to_img/61505/rgb(81,81,102)/34" style="padding:4px;max-width:inherit;" height="34" alt=""/>
</t>
</td>
<td style="padding: 0px 10px 0px 10px;width:50%;vertical-align:top;">
<t t-if="event_address">
<t t-set="location" t-value="''"/>
<t t-if="object.event_id.address_id.name">
<div t-out="object.event_id.address_id.name or ''">Teksa SpA</div>
</t>
<t t-if="object.event_id.address_id.street">
<div t-out="object.event_id.address_id.street or ''">Puerto Madero 9710</div>
<t t-set="location" t-value="object.event_id.address_id.street"/>
</t>
<t t-if="object.event_id.address_id.street2">
<div t-out="object.event_id.address_id.street2 or ''">Of A15, Santiago (RM)</div>
<t t-set="location" t-value="'%s, %s' % (location, object.event_id.address_id.street2)"/>
</t>
<div>
<t t-if="object.event_id.address_id.city">
<t t-out="object.event_id.address_id.city or ''">Pudahuel</t>,
<t t-set="location" t-value="'%s, %s' % (location, object.event_id.address_id.city)"/>
</t>
<t t-if="object.event_id.address_id.state_id.name">
<t t-out="object.event_id.address_id.state_id.name or ''">C1</t>,
<t t-set="location" t-value="'%s, %s' % (location, object.event_id.address_id.state_id.name)"/>
</t>
<t t-if="object.event_id.address_id.zip">
<t t-out="object.event_id.address_id.zip or ''">98450</t>
<t t-set="location" t-value="'%s, %s' % (location, object.event_id.address_id.zip)"/>
</t>
</div>
<t t-if="object.event_id.address_id.country_id.name">
<div t-out="object.event_id.address_id.country_id.name or ''">Argentina</div>
<t t-set="location" t-value="'%s, %s' % (location, object.event_id.address_id.country_id.name)"/>
</t>
</t>
</td>
</tr>
</table>
</td></tr>
<tr><td style="text-align:center;">
<t t-if="event_organizer">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</t>
</td></tr>
<tr><td valign="top" style="font-size: 14px;">
<!-- CONTACT ORGANIZER -->
<t t-if="event_organizer">
<div>
<span style="font-weight:300;margin:10px 0px">Questions about this event?</span>
<div>Please contact the organizer:</div>
<ul>
<li t-out="event_organizer.name or ''">YourCompany</li>
<t t-if="event_organizer.email">
<li>Mail: <a t-attf-href="mailto:{{ event_organizer.email }}" style="text-decoration:none;color:#875A7B;" t-out="event_organizer.email or ''"></a></li>
</t>
<t t-if="event_organizer.phone">
<li>Phone: <t t-out="event_organizer.phone or ''"></t></li>
</t>
</ul>
</div>
</t>
</td></tr>
<tr><td style="text-align:center;">
<!-- CONTACT ORGANIZER SEPARATION -->
<hr t-if="is_online or event_address" width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</td></tr>
<tr><td valign="top" style="font-size: 14px;">
<!-- PWA MARKGETING -->
<div t-if="is_online">
<strong>Get the best mobile experience.</strong>
<a href="/event">Install our mobile app</a>
</div>
</td></tr>
<tr><td style="text-align:center;">
<!-- PWA MARKGETING SEPARATION-->
<hr t-if="is_online and event_address" width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</td></tr>
<tr><td valign="top" style="font-size: 14px;">
<!-- GOOGLE MAPS LINK -->
<table t-if="event_address and location" style="width:100%;"><tr><td>
<div>
<i class="fa fa-map-marker"/>
<a t-attf-href="https://maps.google.com/maps?q={{ location }}" target="new">
<img t-if="event_address.static_map_url and event_address.static_map_url_is_valid"
t-attf-src="{{ event_address.static_map_url }}"
style="vertical-align:bottom; width: 100%;" alt="Google Maps"/>
<span t-else="">See location on Google Maps</span>
</a>
</div>
</td></tr></table>
</td></tr>
</table>
</td>
</tr>
</tbody>
</table>
</td></tr>
<!-- FOOTER BY -->
<tr><td align="center" style="min-width: 590px;">
<table t-if="object.company_id" width="590" border="0" cellpadding="0" cellspacing="0" style="min-width: 590px; background-color: #F1F1F1; color: #454748; padding: 8px; border-collapse:separate;">
<tr><td style="text-align: center; font-size: 14px;">
Sent by <a target="_blank" t-attf-href="{{ object.company_id.website }}" style="color: #875A7B;" t-out="object.company_id.name or ''">YourCompany</a>
<t t-if="'website_url' in object.event_id and object.event_id.website_url">
<br />
Discover <a href="/event" style="color:#875A7B;">all our events</a>.
</t>
</td></tr>
</table>
</td></tr>
</table>
</field>
<field name="lang">{{ object.event_id.lang or object.partner_id.lang }}</field>
</record>
</data>
</odoo>

88
data/res_partner_demo.xml Normal file
View File

@ -0,0 +1,88 @@
<?xml version="1.0"?>
<odoo><data>
<!-- LOCATIONS -->
<record id="res_partner_location_0" model="res.partner">
<field name="name">Reno Airfield</field>
<field name="is_company">1</field>
<field name="street">1235 Columbia Hill Rd</field>
<field name="city">Reno</field>
<field name="state_id" ref='base.state_us_23'/>
<field name="zip">89508</field>
<field name="country_id" ref="base.us"/>
</record>
<record id="res_partner_location_1" model="res.partner">
<field name="name">Wembley Stadium</field>
<field name="is_company">1</field>
<field name="street">Wembley HA9 0WS</field>
<field name="city">London</field>
<field name="state_id" ref='base.state_uk117'/>
<field name="country_id" ref="base.uk"/>
</record>
<record id="res_partner_location_2" model="res.partner">
<field name="name">Los Angeles Convention Center</field>
<field name="is_company">1</field>
<field name="street">1201 S Figueroa St</field>
<field name="city">Los Angeles</field>
<field name="state_id" ref='base.state_us_5'/>
<field name="zip">90015</field>
<field name="country_id" ref="base.us"/>
</record>
<!-- SPONSORS / OTHER COUNTRIES -->
<record id="res_partner_event_1" model="res.partner">
<field name="name">Bloem GmbH</field>
<field name="is_company" eval="True"/>
<field name="image_1920" type="base64" file="event/static/src/img/partner_bloem.png"/>
<field name="street">Behrenstraße 55</field>
<field name="zip">10117</field>
<field name="city">Berlin</field>
<field name="country_id" ref="base.de"/>
<field name="phone">+49 30 12345678</field>
<field name="mobile">+49 30 87654321</field>
<field name="email">flower@example.com</field>
<field name="website">www.flower.example.com</field>
</record>
<record id="res_partner_event_2" model="res.partner">
<field name="name">OpenWood</field>
<field name="is_company" eval="True"/>
<field name="image_1920" type="base64" file="event/static/src/img/partner_open_wood.png"/>
<field name="street">Orval 1</field>
<field name="zip">6823</field>
<field name="city">Florenville</field>
<field name="country_id" ref="base.be"/>
<field name="phone">+32 987 65 43 21</field>
<field name="mobile">+32 987 65 43 21</field>
<field name="email">wow@example.com</field>
<field name="website">www.openwood.example.com</field>
</record>
<record id="res_partner_event_3" model="res.partner">
<field name="name">Tree Dealers SP</field>
<field name="is_company" eval="True"/>
<field name="image_1920" type="base64" file="event/static/src/img/partner_tree_dealers.png"/>
<field name="street">Place d'Youville, 995</field>
<field name="zip">QC G1R 3P1</field>
<field name="city">Ville de Quebec</field>
<field name="country_id" ref="base.ca"/>
<field name="phone">+1 418 123 4567</field>
<field name="mobile">+1 418 765 4321</field>
<field name="email">tree@example.com</field>
<field name="website">www.tree.example.com</field>
</record>
<record id="res_partner_event_4" model="res.partner">
<field name="name">Shangai Pterocarpus Furniture Co., Ltd.</field>
<field name="is_company" eval="True"/>
<field name="image_1920" type="base64" file="event/static/src/img/partner_pterocarpus.png"/>
<field name="street">68 Taicang Rd, Shi Men Er Lu Jie Dao, Huangpu Qu</field>
<field name="zip">200000</field>
<field name="city">Shanghai Shi</field>
<field name="country_id" ref="base.cn"/>
<field name="phone">+86 21 1234 5678</field>
<field name="mobile">+86 21 8765 4321</field>
<field name="email">ptero@example.com</field>
<field name="website">www.pterocarpus.example.com</field>
</record>
</data></odoo>

6
data/res_users_demo.xml Normal file
View File

@ -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>

3441
i18n/af.po Normal file

File diff suppressed because it is too large Load Diff

3437
i18n/am.po Normal file

File diff suppressed because it is too large Load Diff

4948
i18n/ar.po Normal file

File diff suppressed because it is too large Load Diff

3446
i18n/az.po Normal file

File diff suppressed because it is too large Load Diff

4150
i18n/bg.po Normal file

File diff suppressed because it is too large Load Diff

3444
i18n/bs.po Normal file

File diff suppressed because it is too large Load Diff

4332
i18n/ca.po Normal file

File diff suppressed because it is too large Load Diff

4189
i18n/cs.po Normal file

File diff suppressed because it is too large Load Diff

4256
i18n/da.po Normal file

File diff suppressed because it is too large Load Diff

5022
i18n/de.po Normal file

File diff suppressed because it is too large Load Diff

3444
i18n/el.po Normal file

File diff suppressed because it is too large Load Diff

3440
i18n/en_GB.po Normal file

File diff suppressed because it is too large Load Diff

5000
i18n/es.po Normal file

File diff suppressed because it is too large Load Diff

5008
i18n/es_419.po Normal file

File diff suppressed because it is too large Load Diff

3440
i18n/es_BO.po Normal file

File diff suppressed because it is too large Load Diff

3440
i18n/es_CL.po Normal file

File diff suppressed because it is too large Load Diff

3440
i18n/es_CO.po Normal file

File diff suppressed because it is too large Load Diff

3440
i18n/es_CR.po Normal file

File diff suppressed because it is too large Load Diff

3440
i18n/es_DO.po Normal file

File diff suppressed because it is too large Load Diff

3440
i18n/es_EC.po Normal file

File diff suppressed because it is too large Load Diff

3440
i18n/es_PE.po Normal file

File diff suppressed because it is too large Load Diff

3440
i18n/es_PY.po Normal file

File diff suppressed because it is too large Load Diff

3440
i18n/es_VE.po Normal file

File diff suppressed because it is too large Load Diff

4713
i18n/et.po Normal file

File diff suppressed because it is too large Load Diff

3440
i18n/eu.po Normal file

File diff suppressed because it is too large Load Diff

4117
i18n/event.pot Normal file

File diff suppressed because it is too large Load Diff

4148
i18n/fa.po Normal file

File diff suppressed because it is too large Load Diff

4315
i18n/fi.po Normal file

File diff suppressed because it is too large Load Diff

3440
i18n/fo.po Normal file

File diff suppressed because it is too large Load Diff

5007
i18n/fr.po Normal file

File diff suppressed because it is too large Load Diff

3439
i18n/fr_BE.po Normal file

File diff suppressed because it is too large Load Diff

3440
i18n/fr_CA.po Normal file

File diff suppressed because it is too large Load Diff

3440
i18n/gl.po Normal file

File diff suppressed because it is too large Load Diff

3445
i18n/gu.po Normal file

File diff suppressed because it is too large Load Diff

4238
i18n/he.po Normal file

File diff suppressed because it is too large Load Diff

3460
i18n/hr.po Normal file

File diff suppressed because it is too large Load Diff

4159
i18n/hu.po Normal file

File diff suppressed because it is too large Load Diff

4978
i18n/id.po Normal file

File diff suppressed because it is too large Load Diff

3437
i18n/is.po Normal file

File diff suppressed because it is too large Load Diff

4996
i18n/it.po Normal file

File diff suppressed because it is too large Load Diff

4855
i18n/ja.po Normal file

File diff suppressed because it is too large Load Diff

3440
i18n/ka.po Normal file

File diff suppressed because it is too large Load Diff

3440
i18n/kab.po Normal file

File diff suppressed because it is too large Load Diff

3444
i18n/km.po Normal file

File diff suppressed because it is too large Load Diff

4871
i18n/ko.po Normal file

File diff suppressed because it is too large Load Diff

3441
i18n/lb.po Normal file

File diff suppressed because it is too large Load Diff

3440
i18n/lo.po Normal file

File diff suppressed because it is too large Load Diff

4203
i18n/lt.po Normal file

File diff suppressed because it is too large Load Diff

4133
i18n/lv.po Normal file

File diff suppressed because it is too large Load Diff

3440
i18n/mk.po Normal file

File diff suppressed because it is too large Load Diff

3459
i18n/mn.po Normal file

File diff suppressed because it is too large Load Diff

3450
i18n/nb.po Normal file

File diff suppressed because it is too large Load Diff

3437
i18n/ne.po Normal file

File diff suppressed because it is too large Load Diff

4783
i18n/nl.po Normal file

File diff suppressed because one or more lines are too long

4310
i18n/pl.po Normal file

File diff suppressed because it is too large Load Diff

4312
i18n/pt.po Normal file

File diff suppressed because it is too large Load Diff

4991
i18n/pt_BR.po Normal file

File diff suppressed because it is too large Load Diff

3464
i18n/ro.po Normal file

File diff suppressed because it is too large Load Diff

5002
i18n/ru.po Normal file

File diff suppressed because it is too large Load Diff

4166
i18n/sk.po Normal file

File diff suppressed because it is too large Load Diff

4148
i18n/sl.po Normal file

File diff suppressed because it is too large Load Diff

3440
i18n/sq.po Normal file

File diff suppressed because it is too large Load Diff

4299
i18n/sr.po Normal file

File diff suppressed because it is too large Load Diff

3443
i18n/sr@latin.po Normal file

File diff suppressed because it is too large Load Diff

4206
i18n/sv.po Normal file

File diff suppressed because it is too large Load Diff

4938
i18n/th.po Normal file

File diff suppressed because it is too large Load Diff

4315
i18n/tr.po Normal file

File diff suppressed because it is too large Load Diff

4977
i18n/uk.po Normal file

File diff suppressed because it is too large Load Diff

4731
i18n/vi.po Normal file

File diff suppressed because it is too large Load Diff

4851
i18n/zh_CN.po Normal file

File diff suppressed because it is too large Load Diff

4836
i18n/zh_TW.po Normal file

File diff suppressed because it is too large Load Diff

12
models/__init__.py Normal file
View File

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import event_event
from . import event_mail
from . import event_registration
from . import event_stage
from . import event_tag
from . import event_ticket
from . import mail_template
from . import res_config_settings
from . import res_partner

690
models/event_event.py Normal file
View File

@ -0,0 +1,690 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import pytz
from datetime import timedelta
from odoo import _, api, Command, fields, models, tools
from odoo.addons.base.models.res_partner import _tz_get
from odoo.exceptions import UserError, ValidationError
from odoo.osv import expression
from odoo.tools import format_datetime, is_html_empty
from odoo.tools.misc import formatLang
from odoo.tools.translate import html_translate
_logger = logging.getLogger(__name__)
try:
import vobject
except ImportError:
_logger.warning("`vobject` Python module not found, iCal file generation disabled. Consider installing this module if you want to generate iCal files")
vobject = None
class EventType(models.Model):
_name = 'event.type'
_description = 'Event Template'
_order = 'sequence, id'
def _default_event_mail_type_ids(self):
return [(0, 0,
{'notification_type': 'mail',
'interval_nbr': 0,
'interval_unit': 'now',
'interval_type': 'after_sub',
'template_ref': 'mail.template, %i' % self.env.ref('event.event_subscription').id,
}),
(0, 0,
{'notification_type': 'mail',
'interval_nbr': 1,
'interval_unit': 'hours',
'interval_type': 'before_event',
'template_ref': 'mail.template, %i' % self.env.ref('event.event_reminder').id,
}),
(0, 0,
{'notification_type': 'mail',
'interval_nbr': 3,
'interval_unit': 'days',
'interval_type': 'before_event',
'template_ref': 'mail.template, %i' % self.env.ref('event.event_reminder').id,
})]
name = fields.Char('Event Template', required=True, translate=True)
note = fields.Html(string='Note')
sequence = fields.Integer(default=10)
# tickets
event_type_ticket_ids = fields.One2many('event.type.ticket', 'event_type_id', string='Tickets')
tag_ids = fields.Many2many('event.tag', string="Tags")
# registration
has_seats_limitation = fields.Boolean('Limited Seats')
seats_max = fields.Integer(
'Maximum Registrations', compute='_compute_seats_max',
readonly=False, store=True,
help="It will select this default maximum value when you choose this event")
default_timezone = fields.Selection(
_tz_get, string='Timezone', default=lambda self: self.env.user.tz or 'UTC')
# communication
event_type_mail_ids = fields.One2many(
'event.type.mail', 'event_type_id', string='Mail Schedule',
default=_default_event_mail_type_ids)
# ticket reports
ticket_instructions = fields.Html('Ticket Instructions', translate=True,
help="This information will be printed on your tickets.")
@api.depends('has_seats_limitation')
def _compute_seats_max(self):
for template in self:
if not template.has_seats_limitation:
template.seats_max = 0
class EventEvent(models.Model):
"""Event"""
_name = 'event.event'
_description = 'Event'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'date_begin, id'
@api.model
def default_get(self, fields_list):
result = super().default_get(fields_list)
if 'date_begin' in fields_list and 'date_begin' not in result:
now = fields.Datetime.now()
# Round the datetime to the nearest half hour (e.g. 08:17 => 08:30 and 08:37 => 09:00)
result['date_begin'] = now.replace(second=0, microsecond=0) + timedelta(minutes=-now.minute % 30)
if 'date_end' in fields_list and 'date_end' not in result and result.get('date_begin'):
result['date_end'] = result['date_begin'] + timedelta(days=1)
return result
def _get_default_stage_id(self):
return self.env['event.stage'].search([], limit=1)
def _default_description(self):
# avoid template branding with rendering_bundle=True
return self.env['ir.ui.view'].with_context(rendering_bundle=True) \
._render_template('event.event_default_descripton')
def _default_event_mail_ids(self):
return self.env['event.type']._default_event_mail_type_ids()
@api.model
def _lang_get(self):
return self.env['res.lang'].get_installed()
name = fields.Char(string='Event', translate=True, required=True)
note = fields.Html(string='Note', store=True, compute="_compute_note", readonly=False)
description = fields.Html(string='Description', translate=html_translate, sanitize_attributes=False, sanitize_form=False, default=_default_description)
active = fields.Boolean(default=True)
user_id = fields.Many2one(
'res.users', string='Responsible', tracking=True,
default=lambda self: self.env.user)
use_barcode = fields.Boolean(compute='_compute_use_barcode')
company_id = fields.Many2one(
'res.company', string='Company', change_default=True,
default=lambda self: self.env.company,
required=False)
organizer_id = fields.Many2one(
'res.partner', string='Organizer', tracking=True,
default=lambda self: self.env.company.partner_id,
check_company=True)
event_type_id = fields.Many2one('event.type', string='Template', ondelete='set null')
event_mail_ids = fields.One2many(
'event.mail', 'event_id', string='Mail Schedule', copy=True,
compute='_compute_event_mail_ids', readonly=False, store=True)
tag_ids = fields.Many2many(
'event.tag', string="Tags", readonly=False,
store=True, compute="_compute_tag_ids")
# properties
registration_properties_definition = fields.PropertiesDefinition('Registration Properties')
# Kanban fields
kanban_state = fields.Selection([('normal', 'In Progress'), ('done', 'Done'), ('blocked', 'Blocked')], default='normal', copy=False)
kanban_state_label = fields.Char(
string='Kanban State Label', compute='_compute_kanban_state_label',
store=True, tracking=True)
stage_id = fields.Many2one(
'event.stage', ondelete='restrict', default=_get_default_stage_id,
group_expand='_read_group_stage_ids', tracking=True, copy=False)
legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True)
legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True)
legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True)
# Seats and computation
seats_max = fields.Integer(
string='Maximum Attendees',
compute='_compute_seats_max', readonly=False, store=True,
help="For each event you can define a maximum registration of seats(number of attendees), above this numbers the registrations are not accepted.")
seats_limited = fields.Boolean('Limit Attendees', required=True, compute='_compute_seats_limited',
precompute=True, readonly=False, store=True)
seats_reserved = fields.Integer(
string='Number of Registrations',
store=False, readonly=True, compute='_compute_seats')
seats_available = fields.Integer(
string='Available Seats',
store=False, readonly=True, compute='_compute_seats')
seats_used = fields.Integer(
string='Number of Attendees',
store=False, readonly=True, compute='_compute_seats')
seats_taken = fields.Integer(
string='Number of Taken Seats',
store=False, readonly=True, compute='_compute_seats')
# Registration fields
registration_ids = fields.One2many('event.registration', 'event_id', string='Attendees')
event_ticket_ids = fields.One2many(
'event.event.ticket', 'event_id', string='Event Ticket', copy=True,
compute='_compute_event_ticket_ids', readonly=False, store=True)
event_registrations_started = fields.Boolean(
'Registrations started', compute='_compute_event_registrations_started',
help="registrations have started if the current datetime is after the earliest starting date of tickets."
)
event_registrations_open = fields.Boolean(
'Registration open', compute='_compute_event_registrations_open', compute_sudo=True,
help="Registrations are open if:\n"
"- the event is not ended\n"
"- there are seats available on event\n"
"- the tickets are sellable (if ticketing is used)")
event_registrations_sold_out = fields.Boolean(
'Sold Out', compute='_compute_event_registrations_sold_out', compute_sudo=True,
help='The event is sold out if no more seats are available on event. If ticketing is used and all tickets are sold out, the event will be sold out.')
start_sale_datetime = fields.Datetime(
'Start sale date', compute='_compute_start_sale_date',
help='If ticketing is used, contains the earliest starting sale date of tickets.')
# Date fields
date_tz = fields.Selection(
_tz_get, string='Timezone', required=True,
compute='_compute_date_tz', precompute=True, readonly=False, store=True)
date_begin = fields.Datetime(string='Start Date', required=True, tracking=True)
date_end = fields.Datetime(string='End Date', required=True, tracking=True)
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_ongoing = fields.Boolean('Is Ongoing', compute='_compute_is_ongoing', search='_search_is_ongoing')
is_one_day = fields.Boolean(compute='_compute_field_is_one_day')
is_finished = fields.Boolean(compute='_compute_is_finished', search='_search_is_finished')
# Location and communication
address_id = fields.Many2one(
'res.partner', string='Venue', default=lambda self: self.env.company.partner_id.id,
check_company=True,
tracking=True
)
address_search = fields.Many2one(
'res.partner', string='Address', compute='_compute_address_search', search='_search_address_search')
address_inline = fields.Char(
string='Venue (formatted for one line uses)', compute='_compute_address_inline',
compute_sudo=True)
country_id = fields.Many2one(
'res.country', 'Country', related='address_id.country_id', readonly=False, store=True)
lang = fields.Selection(_lang_get, string='Language',
help="All the communication emails sent to attendees will be translated in this language.")
# ticket reports
badge_format = fields.Selection(
string='Badge Dimension',
selection=[
('A4_french_fold', 'A4 foldable'),
('A6', 'A6'),
('four_per_sheet', '4 per sheet')
], default='A6', required=True)
badge_image = fields.Image('Badge Background', max_width=1024, max_height=1024)
ticket_instructions = fields.Html('Ticket Instructions', translate=True,
compute='_compute_ticket_instructions', store=True, readonly=False,
help="This information will be printed on your tickets.")
def _compute_use_barcode(self):
use_barcode = self.env['ir.config_parameter'].sudo().get_param('event.use_event_barcode') == 'True'
for record in self:
record.use_barcode = use_barcode
@api.depends('stage_id', 'kanban_state')
def _compute_kanban_state_label(self):
for event in self:
if event.kanban_state == 'normal':
event.kanban_state_label = event.stage_id.legend_normal
elif event.kanban_state == 'blocked':
event.kanban_state_label = event.stage_id.legend_blocked
else:
event.kanban_state_label = event.stage_id.legend_done
@api.depends('seats_max', 'registration_ids.state', 'registration_ids.active')
def _compute_seats(self):
""" Determine available, reserved, used and taken seats. """
# initialize fields to 0
for event in self:
event.seats_reserved = event.seats_used = event.seats_available = 0
# aggregate registrations by event and by state
state_field = {
'open': 'seats_reserved',
'done': 'seats_used',
}
base_vals = dict((fname, 0) for fname in state_field.values())
results = dict((event_id, dict(base_vals)) for event_id in self.ids)
if self.ids:
query = """ SELECT event_id, state, count(event_id)
FROM event_registration
WHERE event_id IN %s AND state IN ('open', 'done') AND active = true
GROUP BY event_id, state
"""
self.env['event.registration'].flush_model(['event_id', 'state', 'active'])
self._cr.execute(query, (tuple(self.ids),))
res = self._cr.fetchall()
for event_id, state, num in res:
results[event_id][state_field[state]] = num
# compute seats_available and expected
for event in self:
event.update(results.get(event._origin.id or event.id, base_vals))
if event.seats_max > 0:
event.seats_available = event.seats_max - (event.seats_reserved + event.seats_used)
event.seats_taken = event.seats_reserved + event.seats_used
@api.depends('date_tz', 'start_sale_datetime')
def _compute_event_registrations_started(self):
for event in self:
event = event._set_tz_context()
if event.start_sale_datetime:
current_datetime = fields.Datetime.context_timestamp(event, fields.Datetime.now())
start_sale_datetime = fields.Datetime.context_timestamp(event, event.start_sale_datetime)
event.event_registrations_started = (current_datetime >= start_sale_datetime)
else:
event.event_registrations_started = True
@api.depends('date_tz', 'event_registrations_started', 'date_end', 'seats_available', 'seats_limited', 'seats_max',
'event_ticket_ids.sale_available')
def _compute_event_registrations_open(self):
""" Compute whether people may take registrations for this event
* event.date_end -> if event is done, registrations are not open anymore;
* event.start_sale_datetime -> lowest start date of tickets (if any; start_sale_datetime
is False if no ticket are defined, see _compute_start_sale_date);
* any ticket is available for sale (seats available) if any;
* seats are unlimited or seats are available;
"""
for event in self:
event = event._set_tz_context()
current_datetime = fields.Datetime.context_timestamp(event, fields.Datetime.now())
date_end_tz = event.date_end.astimezone(pytz.timezone(event.date_tz or 'UTC')) if event.date_end else False
event.event_registrations_open = event.event_registrations_started and \
(date_end_tz >= current_datetime if date_end_tz else True) and \
(not event.seats_limited or not event.seats_max or event.seats_available) and \
(not event.event_ticket_ids or any(ticket.sale_available for ticket in event.event_ticket_ids))
@api.depends('event_ticket_ids.start_sale_datetime')
def _compute_start_sale_date(self):
""" Compute the start sale date of an event. Currently lowest starting sale
date of tickets if they are used, of False. """
for event in self:
start_dates = [ticket.start_sale_datetime for ticket in event.event_ticket_ids if not ticket.is_expired]
event.start_sale_datetime = min(start_dates) if start_dates and all(start_dates) else False
@api.depends('event_ticket_ids.sale_available', 'seats_available', 'seats_limited')
def _compute_event_registrations_sold_out(self):
"""Note that max seats limits for events and sum of limits for all its tickets may not be
equal to enable flexibility.
E.g. max 20 seats for ticket A, 20 seats for ticket B
* With max 20 seats for the event
* Without limit set on the event (=40, but the customer didn't explicitly write 40)
"""
for event in self:
event.event_registrations_sold_out = (
(event.seats_limited and event.seats_max and not event.seats_available)
or (event.event_ticket_ids and all(ticket.is_sold_out for ticket in event.event_ticket_ids))
)
@api.depends('date_tz', 'date_begin')
def _compute_date_begin_tz(self):
for event in self:
if event.date_begin:
event.date_begin_located = format_datetime(
self.env, event.date_begin, tz=event.date_tz, dt_format='medium')
else:
event.date_begin_located = False
@api.depends('date_tz', 'date_end')
def _compute_date_end_tz(self):
for event in self:
if event.date_end:
event.date_end_located = format_datetime(
self.env, event.date_end, tz=event.date_tz, dt_format='medium')
else:
event.date_end_located = False
@api.depends('date_begin', 'date_end')
def _compute_is_ongoing(self):
now = fields.Datetime.now()
for event in self:
event.is_ongoing = event.date_begin <= now < event.date_end
def _search_is_ongoing(self, operator, value):
if operator not in ['=', '!=']:
raise UserError(_('This operator is not supported'))
if not isinstance(value, bool):
raise UserError(_('Value should be True or False (not %s)', value))
now = fields.Datetime.now()
if (operator == '=' and value) or (operator == '!=' and not value):
domain = [('date_begin', '<=', now), ('date_end', '>', now)]
else:
domain = ['|', ('date_begin', '>', now), ('date_end', '<=', now)]
return domain
@api.depends('date_begin', 'date_end', 'date_tz')
def _compute_field_is_one_day(self):
for event in self:
# Need to localize because it could begin late and finish early in
# another timezone
event = event._set_tz_context()
begin_tz = fields.Datetime.context_timestamp(event, event.date_begin)
end_tz = fields.Datetime.context_timestamp(event, event.date_end)
event.is_one_day = (begin_tz.date() == end_tz.date())
@api.depends('date_end')
def _compute_is_finished(self):
for event in self:
if not event.date_end:
event.is_finished = False
continue
event = event._set_tz_context()
current_datetime = fields.Datetime.context_timestamp(event, fields.Datetime.now())
datetime_end = fields.Datetime.context_timestamp(event, event.date_end)
event.is_finished = datetime_end <= current_datetime
def _search_is_finished(self, operator, value):
if operator not in ['=', '!=']:
raise ValueError(_('This operator is not supported'))
if not isinstance(value, bool):
raise ValueError(_('Value should be True or False (not %s)'), value)
now = fields.Datetime.now()
if (operator == '=' and value) or (operator == '!=' and not value):
domain = [('date_end', '<=', now)]
else:
domain = [('date_end', '>', now)]
return domain
@api.depends('event_type_id')
def _compute_date_tz(self):
for event in self:
if event.event_type_id.default_timezone:
event.date_tz = event.event_type_id.default_timezone
if not event.date_tz:
event.date_tz = self.env.user.tz or 'UTC'
@api.depends('address_id')
def _compute_address_search(self):
for event in self:
event.address_search = event.address_id
def _search_address_search(self, operator, value):
if operator != 'ilike' or not isinstance(value, str):
raise NotImplementedError(_('Operation not supported.'))
return expression.OR([
[('address_id.name', 'ilike', value)],
[('address_id.street', 'ilike', value)],
[('address_id.street2', 'ilike', value)],
[('address_id.city', 'ilike', value)],
[('address_id.zip', 'ilike', value)],
[('address_id.state_id', 'ilike', value)],
[('address_id.country_id', 'ilike', value)],
])
# seats
@api.depends('event_type_id')
def _compute_seats_max(self):
""" Update event configuration from its event type. Depends are set only
on event_type_id itself, not its sub fields. Purpose is to emulate an
onchange: if event type is changed, update event configuration. Changing
event type content itself should not trigger this method. """
for event in self:
if not event.event_type_id:
event.seats_max = event.seats_max or 0
else:
event.seats_max = event.event_type_id.seats_max or 0
@api.depends('event_type_id')
def _compute_seats_limited(self):
""" Update event configuration from its event type. Depends are set only
on event_type_id itself, not its sub fields. Purpose is to emulate an
onchange: if event type is changed, update event configuration. Changing
event type content itself should not trigger this method. """
for event in self:
if event.event_type_id.has_seats_limitation != event.seats_limited:
event.seats_limited = event.event_type_id.has_seats_limitation
if not event.seats_limited:
event.seats_limited = False
@api.depends('event_type_id')
def _compute_event_mail_ids(self):
""" Update event configuration from its event type. Depends are set only
on event_type_id itself, not its sub fields. Purpose is to emulate an
onchange: if event type is changed, update event configuration. Changing
event type content itself should not trigger this method.
When synchronizing mails:
* lines that are not sent and have no registrations linked are remove;
* type lines are added;
"""
for event in self:
if not event.event_type_id and not event.event_mail_ids:
event.event_mail_ids = self._default_event_mail_ids()
continue
# lines to keep: those with already sent emails or registrations
mails_to_remove = event.event_mail_ids.filtered(
lambda mail: not(mail._origin.mail_done) and not(mail._origin.mail_registration_ids)
)
command = [Command.unlink(mail.id) for mail in mails_to_remove]
# lines to add: those which do not have the exact copy available in lines to keep
if event.event_type_id.event_type_mail_ids:
mails_to_keep_vals = {mail._prepare_event_mail_values() for mail in event.event_mail_ids - mails_to_remove}
for mail in event.event_type_id.event_type_mail_ids:
mail_values = mail._prepare_event_mail_values()
if mail_values not in mails_to_keep_vals:
command.append(Command.create(mail_values._asdict()))
if command:
event.event_mail_ids = command
@api.depends('event_type_id')
def _compute_tag_ids(self):
""" Update event configuration from its event type. Depends are set only
on event_type_id itself, not its sub fields. Purpose is to emulate an
onchange: if event type is changed, update event configuration. Changing
event type content itself should not trigger this method. """
for event in self:
if not event.tag_ids and event.event_type_id.tag_ids:
event.tag_ids = event.event_type_id.tag_ids
@api.depends('event_type_id')
def _compute_event_ticket_ids(self):
""" Update event configuration from its event type. Depends are set only
on event_type_id itself, not its sub fields. Purpose is to emulate an
onchange: if event type is changed, update event configuration. Changing
event type content itself should not trigger this method.
When synchronizing tickets:
* lines that have no registrations linked are remove;
* type lines are added;
Note that updating event_ticket_ids triggers _compute_start_sale_date
(start_sale_datetime computation) so ensure result to avoid cache miss.
"""
for event in self:
if not event.event_type_id and not event.event_ticket_ids:
event.event_ticket_ids = False
continue
# lines to keep: those with existing registrations
tickets_to_remove = event.event_ticket_ids.filtered(lambda ticket: not ticket._origin.registration_ids)
command = [Command.unlink(ticket.id) for ticket in tickets_to_remove]
if event.event_type_id.event_type_ticket_ids:
command += [
Command.create({
attribute_name: line[attribute_name] if not isinstance(line[attribute_name], models.BaseModel) else line[attribute_name].id
for attribute_name in self.env['event.type.ticket']._get_event_ticket_fields_whitelist()
}) for line in event.event_type_id.event_type_ticket_ids
]
event.event_ticket_ids = command
@api.depends('event_type_id')
def _compute_note(self):
for event in self:
if event.event_type_id and not is_html_empty(event.event_type_id.note):
event.note = event.event_type_id.note
@api.depends('event_type_id')
def _compute_ticket_instructions(self):
for event in self:
if is_html_empty(event.ticket_instructions) and not \
is_html_empty(event.event_type_id.ticket_instructions):
event.ticket_instructions = event.event_type_id.ticket_instructions
@api.depends('address_id')
def _compute_address_inline(self):
"""Use venue address if available, otherwise its name, finally ''. """
for event in self:
if (event.address_id.contact_address or '').strip():
event.address_inline = ', '.join(
frag.strip()
for frag in event.address_id.contact_address.split('\n') if frag.strip()
)
else:
event.address_inline = event.address_id.name or ''
@api.constrains('seats_max', 'seats_limited', 'registration_ids')
def _check_seats_availability(self, minimal_availability=0):
sold_out_events = []
for event in self:
if event.seats_limited and event.seats_max and event.seats_available < minimal_availability:
sold_out_events.append(
(_('- "%(event_name)s": Missing %(nb_too_many)i seats.',
event_name=event.name, nb_too_many=-event.seats_available)))
if sold_out_events:
raise ValidationError(_('There are not enough seats available for:')
+ '\n%s\n' % '\n'.join(sold_out_events))
@api.constrains('date_begin', 'date_end')
def _check_closing_date(self):
for event in self:
if event.date_end < event.date_begin:
raise ValidationError(_('The closing date cannot be earlier than the beginning date.'))
@api.model
def _read_group_stage_ids(self, stages, domain, order):
return self.env['event.stage'].search([])
@api.model_create_multi
def create(self, vals_list):
events = super(EventEvent, self).create(vals_list)
for res in events:
if res.organizer_id:
res.message_subscribe([res.organizer_id.id])
self.env.flush_all()
return events
def write(self, vals):
if 'stage_id' in vals and 'kanban_state' not in vals:
# reset kanban state when changing stage
vals['kanban_state'] = 'normal'
res = super(EventEvent, self).write(vals)
if vals.get('organizer_id'):
self.message_subscribe([vals['organizer_id']])
return res
@api.depends('event_registrations_sold_out', 'seats_limited', 'seats_max', 'seats_available')
@api.depends_context('name_with_seats_availability')
def _compute_display_name(self):
"""Adds ticket seats availability if requested by context."""
if not self.env.context.get('name_with_seats_availability'):
return super()._compute_display_name()
for event in self:
# event or its tickets are sold out
if event.event_registrations_sold_out:
name = _('%(event_name)s (Sold out)', event_name=event.name)
elif event.seats_limited and event.seats_max:
name = _(
'%(event_name)s (%(count)s seats remaining)',
event_name=event.name,
count=formatLang(self.env, event.seats_available, digits=0),
)
else:
name = event.name
event.display_name = name
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
self.ensure_one()
default = dict(default or {}, name=_("%s (copy)", self.name))
return super(EventEvent, self).copy(default)
@api.model
def _get_mail_message_access(self, res_ids, operation, model_name=None):
if (
operation == 'create'
and self.env.user.has_group('event.group_event_registration_desk')
and (not model_name or model_name == 'event.event')
):
# allow the registration desk users to post messages on Event
# can not be done with "_mail_post_access" otherwise public user will be
# able to post on published Event (see website_event)
return 'read'
return super(EventEvent, self)._get_mail_message_access(res_ids, operation, model_name)
def _set_tz_context(self):
self.ensure_one()
return self.with_context(tz=self.date_tz or 'UTC')
def action_set_done(self):
"""
Action which will move the events
into the first next (by sequence) stage defined as "Ended"
(if they are not already in an ended stage)
"""
first_ended_stage = self.env['event.stage'].search([('pipe_end', '=', True)], limit=1, order='sequence')
if first_ended_stage:
self.write({'stage_id': first_ended_stage.id})
def mail_attendees(self, template_id, force_send=False, filter_func=lambda self: self.state != 'cancel'):
for event in self:
for attendee in event.registration_ids.filtered(filter_func):
self.env['mail.template'].browse(template_id).send_mail(attendee.id, force_send=force_send)
def _get_ics_file(self):
""" Returns iCalendar file for the event invitation.
:returns a dict of .ics file content for each event
"""
result = {}
if not vobject:
return result
for event in self:
cal = vobject.iCalendar()
cal_event = cal.add('vevent')
cal_event.add('created').value = fields.Datetime.now().replace(tzinfo=pytz.timezone('UTC'))
cal_event.add('dtstart').value = event.date_begin.astimezone(pytz.timezone(event.date_tz))
cal_event.add('dtend').value = event.date_end.astimezone(pytz.timezone(event.date_tz))
cal_event.add('summary').value = event.name
if event.address_id:
cal_event.add('location').value = event.address_inline
result[event.id] = cal.serialize().encode('utf-8')
return result
def _get_tickets_access_hash(self, registration_ids):
""" Returns the ground truth hash for accessing the tickets in route /event/<int:event_id>/my_tickets.
The dl links are always made event-dependant, hence the method linked to the record in self.
"""
self.ensure_one()
return tools.hmac(self.env(su=True), 'event-registration-ticket-report-access', (self.id, sorted(registration_ids)))
@api.autovacuum
def _gc_mark_events_done(self):
""" move every ended events in the next 'ended stage' """
ended_events = self.env['event.event'].search([
('date_end', '<', fields.Datetime.now()),
('stage_id.pipe_end', '=', False),
])
if ended_events:
ended_events.action_set_done()

337
models/event_mail.py Normal file
View File

@ -0,0 +1,337 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import random
import threading
from collections import namedtuple
from datetime import datetime
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, tools
from odoo.tools import exception_to_unicode
from odoo.tools.translate import _
from odoo.exceptions import MissingError, ValidationError
_logger = logging.getLogger(__name__)
_INTERVALS = {
'hours': lambda interval: relativedelta(hours=interval),
'days': lambda interval: relativedelta(days=interval),
'weeks': lambda interval: relativedelta(days=7*interval),
'months': lambda interval: relativedelta(months=interval),
'now': lambda interval: relativedelta(hours=0),
}
class EventTypeMail(models.Model):
""" Template of event.mail to attach to event.type. Those will be copied
upon all events created in that type to ease event creation. """
_name = 'event.type.mail'
_description = 'Mail Scheduling on Event Category'
@api.model
def _selection_template_model(self):
return [('mail.template', 'Mail')]
event_type_id = fields.Many2one(
'event.type', string='Event Type',
ondelete='cascade', required=True)
notification_type = fields.Selection([('mail', 'Mail')], string='Send', default='mail', required=True)
interval_nbr = fields.Integer('Interval', default=1)
interval_unit = fields.Selection([
('now', 'Immediately'),
('hours', 'Hours'), ('days', 'Days'),
('weeks', 'Weeks'), ('months', 'Months')],
string='Unit', default='hours', required=True)
interval_type = fields.Selection([
('after_sub', 'After each registration'),
('before_event', 'Before the event'),
('after_event', 'After the event')],
string='Trigger', default="before_event", required=True)
template_model_id = fields.Many2one('ir.model', string='Template Model', compute='_compute_template_model_id', compute_sudo=True)
template_ref = fields.Reference(string='Template', selection='_selection_template_model', required=True)
@api.depends('notification_type')
def _compute_template_model_id(self):
mail_model = self.env['ir.model']._get('mail.template')
for mail in self:
mail.template_model_id = mail_model if mail.notification_type == 'mail' else False
def _prepare_event_mail_values(self):
self.ensure_one()
return namedtuple("MailValues", ['notification_type', 'interval_nbr', 'interval_unit', 'interval_type', 'template_ref'])(
self.notification_type,
self.interval_nbr,
self.interval_unit,
self.interval_type,
'%s,%i' % (self.template_ref._name, self.template_ref.id)
)
class EventMailScheduler(models.Model):
""" Event automated mailing. This model replaces all existing fields and
configuration allowing to send emails on events since Odoo 9. A cron exists
that periodically checks for mailing to run. """
_name = 'event.mail'
_rec_name = 'event_id'
_description = 'Event Automated Mailing'
@api.model
def _selection_template_model(self):
return [('mail.template', 'Mail')]
def _selection_template_model_get_mapping(self):
return {'mail': 'mail.template'}
@api.onchange('notification_type')
def set_template_ref_model(self):
mail_model = self.env['mail.template']
if self.notification_type == 'mail':
record = mail_model.search([('model', '=', 'event.registration')], limit=1)
self.template_ref = "{},{}".format('mail.template', record.id) if record else False
event_id = fields.Many2one('event.event', string='Event', required=True, ondelete='cascade')
sequence = fields.Integer('Display order')
notification_type = fields.Selection([('mail', 'Mail')], string='Send', default='mail', required=True)
interval_nbr = fields.Integer('Interval', default=1)
interval_unit = fields.Selection([
('now', 'Immediately'),
('hours', 'Hours'), ('days', 'Days'),
('weeks', 'Weeks'), ('months', 'Months')],
string='Unit', default='hours', required=True)
interval_type = fields.Selection([
('after_sub', 'After each registration'),
('before_event', 'Before the event'),
('after_event', 'After the event')],
string='Trigger ', default="before_event", required=True)
scheduled_date = fields.Datetime('Schedule Date', compute='_compute_scheduled_date', store=True)
# contact and status
mail_registration_ids = fields.One2many(
'event.mail.registration', 'scheduler_id',
help='Communication related to event registrations')
mail_done = fields.Boolean("Sent", copy=False, readonly=True)
mail_state = fields.Selection(
[('running', 'Running'), ('scheduled', 'Scheduled'), ('sent', 'Sent')],
string='Global communication Status', compute='_compute_mail_state')
mail_count_done = fields.Integer('# Sent', copy=False, readonly=True)
template_model_id = fields.Many2one('ir.model', string='Template Model', compute='_compute_template_model_id', compute_sudo=True)
template_ref = fields.Reference(string='Template', selection='_selection_template_model', required=True)
@api.depends('notification_type')
def _compute_template_model_id(self):
mail_model = self.env['ir.model']._get('mail.template')
for mail in self:
mail.template_model_id = mail_model if mail.notification_type == 'mail' else False
@api.depends('event_id.date_begin', 'event_id.date_end', 'interval_type', 'interval_unit', 'interval_nbr')
def _compute_scheduled_date(self):
for scheduler in self:
if scheduler.interval_type == 'after_sub':
date, sign = scheduler.event_id.create_date, 1
elif scheduler.interval_type == 'before_event':
date, sign = scheduler.event_id.date_begin, -1
else:
date, sign = scheduler.event_id.date_end, 1
scheduler.scheduled_date = date.replace(microsecond=0) + _INTERVALS[scheduler.interval_unit](sign * scheduler.interval_nbr) if date else False
@api.depends('interval_type', 'scheduled_date', 'mail_done')
def _compute_mail_state(self):
for scheduler in self:
# registrations based
if scheduler.interval_type == 'after_sub':
scheduler.mail_state = 'running'
# global event based
elif scheduler.mail_done:
scheduler.mail_state = 'sent'
elif scheduler.scheduled_date:
scheduler.mail_state = 'scheduled'
else:
scheduler.mail_state = 'running'
@api.constrains('notification_type', 'template_ref')
def _check_template_ref_model(self):
model_map = self._selection_template_model_get_mapping()
for record in self.filtered('template_ref'):
model = model_map[record.notification_type]
if record.template_ref._name != model:
raise ValidationError(_('The template which is referenced should be coming from %(model_name)s model.', model_name=model))
def execute(self):
for scheduler in self:
now = fields.Datetime.now()
if scheduler.interval_type == 'after_sub':
new_registrations = scheduler.event_id.registration_ids.filtered_domain(
[('state', 'not in', ('cancel', 'draft'))]
) - scheduler.mail_registration_ids.registration_id
scheduler._create_missing_mail_registrations(new_registrations)
# execute scheduler on registrations
scheduler.mail_registration_ids.execute()
total_sent = len(scheduler.mail_registration_ids.filtered(lambda reg: reg.mail_sent))
scheduler.update({
'mail_done': total_sent >= (scheduler.event_id.seats_reserved + scheduler.event_id.seats_used),
'mail_count_done': total_sent,
})
else:
# before or after event -> one shot email
if scheduler.mail_done or scheduler.notification_type != 'mail':
continue
# no template -> ill configured, skip and avoid crash
if not scheduler.template_ref:
continue
# do not send emails if the mailing was scheduled before the event but the event is over
if scheduler.scheduled_date <= now and (scheduler.interval_type != 'before_event' or scheduler.event_id.date_end > now):
scheduler.event_id.mail_attendees(scheduler.template_ref.id)
scheduler.update({
'mail_done': True,
'mail_count_done': scheduler.event_id.seats_reserved + scheduler.event_id.seats_used,
})
return True
def _create_missing_mail_registrations(self, registrations):
new = []
for scheduler in self:
new += [{
'registration_id': registration.id,
'scheduler_id': scheduler.id,
} for registration in registrations]
if new:
return self.env['event.mail.registration'].create(new)
return self.env['event.mail.registration']
def _prepare_event_mail_values(self):
self.ensure_one()
return namedtuple("MailValues", ['notification_type', 'interval_nbr', 'interval_unit', 'interval_type', 'template_ref'])(
self.notification_type,
self.interval_nbr,
self.interval_unit,
self.interval_type,
'%s,%i' % (self.template_ref._name, self.template_ref.id)
)
@api.model
def _warn_template_error(self, scheduler, exception):
# We warn ~ once by hour ~ instead of every 10 min if the interval unit is more than 'hours'.
if random.random() < 0.1666 or scheduler.interval_unit in ('now', 'hours'):
ex_s = exception_to_unicode(exception)
try:
event, template = scheduler.event_id, scheduler.template_ref
emails = list(set([event.organizer_id.email, event.user_id.email, template.write_uid.email]))
subject = _("WARNING: Event Scheduler Error for event: %s", event.name)
body = _("""Event Scheduler for:
- Event: %(event_name)s (%(event_id)s)
- Scheduled: %(date)s
- Template: %(template_name)s (%(template_id)s)
Failed with error:
- %(error)s
You receive this email because you are:
- the organizer of the event,
- or the responsible of the event,
- or the last writer of the template.
""",
event_name=event.name,
event_id=event.id,
date=scheduler.scheduled_date,
template_name=template.name,
template_id=template.id,
error=ex_s)
email = self.env['ir.mail_server'].build_email(
email_from=self.env.user.email,
email_to=emails,
subject=subject, body=body,
)
self.env['ir.mail_server'].send_email(email)
except Exception as e:
_logger.error("Exception while sending traceback by email: %s.\n Original Traceback:\n%s", e, exception)
pass
@api.model
def run(self, autocommit=False):
""" Backward compatible method, notably if crons are not updated when
migrating for some reason. """
return self.schedule_communications(autocommit=autocommit)
@api.model
def schedule_communications(self, autocommit=False):
schedulers = self.search([
('event_id.active', '=', True),
('mail_done', '=', False),
('scheduled_date', '<=', fields.Datetime.now())
])
for scheduler in schedulers:
try:
# Prevent a mega prefetch of the registration ids of all the events of all the schedulers
self.browse(scheduler.id).execute()
except Exception as e:
_logger.exception(e)
self.env.invalidate_all()
self._warn_template_error(scheduler, e)
else:
if autocommit and not getattr(threading.current_thread(), 'testing', False):
self.env.cr.commit()
return True
class EventMailRegistration(models.Model):
_name = 'event.mail.registration'
_description = 'Registration Mail Scheduler'
_rec_name = 'scheduler_id'
_order = 'scheduled_date DESC'
scheduler_id = fields.Many2one('event.mail', 'Mail Scheduler', required=True, ondelete='cascade')
registration_id = fields.Many2one('event.registration', 'Attendee', required=True, ondelete='cascade')
scheduled_date = fields.Datetime('Scheduled Time', compute='_compute_scheduled_date', store=True)
mail_sent = fields.Boolean('Mail Sent')
def execute(self):
now = fields.Datetime.now()
todo = self.filtered(lambda reg_mail:
not reg_mail.mail_sent and \
reg_mail.registration_id.state in ['open', 'done'] and \
(reg_mail.scheduled_date and reg_mail.scheduled_date <= now) and \
reg_mail.scheduler_id.notification_type == 'mail'
)
done = self.browse()
for reg_mail in todo:
organizer = reg_mail.scheduler_id.event_id.organizer_id
company = self.env.company
author = self.env.ref('base.user_root').partner_id
if organizer.email:
author = organizer
elif company.email:
author = company.partner_id
elif self.env.user.email:
author = self.env.user.partner_id
email_values = {
'author_id': author.id,
}
template = None
try:
template = reg_mail.scheduler_id.template_ref.exists()
except MissingError:
pass
if not template:
_logger.warning("Cannot process ticket %s, because Mail Scheduler %s has reference to non-existent template", reg_mail.registration_id, reg_mail.scheduler_id)
continue
if not template.email_from:
email_values['email_from'] = author.email_formatted
template.send_mail(reg_mail.registration_id.id, email_values=email_values)
done |= reg_mail
done.write({'mail_sent': True})
@api.depends('registration_id', 'scheduler_id.interval_unit', 'scheduler_id.interval_type')
def _compute_scheduled_date(self):
for mail in self:
if mail.registration_id:
mail.scheduled_date = mail.registration_id.create_date.replace(microsecond=0) + _INTERVALS[mail.scheduler_id.interval_unit](mail.scheduler_id.interval_nbr)
else:
mail.scheduled_date = False

View File

@ -0,0 +1,412 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import os
from dateutil.relativedelta import relativedelta
from odoo import _, api, fields, models, SUPERUSER_ID
from odoo.tools import format_date, email_normalize, email_normalize_all
from odoo.exceptions import AccessError, ValidationError
_logger = logging.getLogger(__name__)
class EventRegistration(models.Model):
_name = 'event.registration'
_description = 'Event Registration'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'id desc'
@api.model
def _get_random_barcode(self):
"""Generate a string representation of a pseudo-random 8-byte number for barcode
generation.
A decimal serialisation is longer than a hexadecimal one *but* it
generates a more compact barcode (Code128C rather than Code128A).
Generate 8 bytes (64 bits) barcodes as 16 bytes barcodes are not
compatible with all scanners.
"""
return str(int.from_bytes(os.urandom(8), 'little'))
# event
event_id = fields.Many2one(
'event.event', string='Event', required=True)
event_ticket_id = fields.Many2one(
'event.event.ticket', string='Event Ticket', ondelete='restrict')
active = fields.Boolean(default=True)
barcode = fields.Char(string='Barcode', default=lambda self: self._get_random_barcode(), readonly=True, copy=False)
# utm informations
utm_campaign_id = fields.Many2one('utm.campaign', 'Campaign', index=True, ondelete='set null')
utm_source_id = fields.Many2one('utm.source', 'Source', index=True, ondelete='set null')
utm_medium_id = fields.Many2one('utm.medium', 'Medium', index=True, ondelete='set null')
# attendee
partner_id = fields.Many2one('res.partner', string='Booked by', tracking=1)
name = fields.Char(
string='Attendee Name', index='trigram',
compute='_compute_name', readonly=False, store=True, tracking=2)
email = fields.Char(string='Email', compute='_compute_email', readonly=False, store=True, tracking=3)
phone = fields.Char(string='Phone', compute='_compute_phone', readonly=False, store=True, tracking=4)
company_name = fields.Char(
string='Company Name', compute='_compute_company_name', readonly=False, store=True, tracking=5)
# organization
date_closed = fields.Datetime(
string='Attended Date', compute='_compute_date_closed',
readonly=False, store=True)
event_begin_date = fields.Datetime(string="Event Start Date", related='event_id.date_begin', readonly=True)
event_end_date = fields.Datetime(string="Event End Date", related='event_id.date_end', readonly=True)
event_organizer_id = fields.Many2one(string='Event Organizer', related='event_id.organizer_id', readonly=True)
event_user_id = fields.Many2one(string='Event Responsible', related='event_id.user_id', readonly=True)
company_id = fields.Many2one(
'res.company', string='Company', related='event_id.company_id',
store=True, readonly=False)
state = fields.Selection([
('draft', 'Unconfirmed'),
('open', 'Registered'),
('done', 'Attended'),
('cancel', 'Cancelled')],
string='Status', default='open',
readonly=True, copy=False, tracking=6,
help='Unconfirmed: registrations in a pending state waiting for an action (specific case, notably with sale status)\n'
'Registered: registrations considered taken by a client\n'
'Attended: registrations for which the attendee attended the event\n'
'Cancelled: registrations cancelled manually')
# properties
registration_properties = fields.Properties(
'Properties', definition='event_id.registration_properties_definition', copy=True)
_sql_constraints = [
('barcode_event_uniq', 'unique(barcode)', "Barcode should be unique")
]
@api.constrains('state', 'event_id', 'event_ticket_id')
def _check_seats_availability(self):
registrations_confirmed = self.filtered(lambda registration: registration.state in ('open', 'done'))
registrations_confirmed.event_id._check_seats_availability()
registrations_confirmed.event_ticket_id._check_seats_availability()
def default_get(self, fields):
ret_vals = super().default_get(fields)
utm_mixin_fields = ("campaign_id", "medium_id", "source_id")
utm_fields = ("utm_campaign_id", "utm_medium_id", "utm_source_id")
if not any(field in utm_fields for field in fields):
return ret_vals
utm_mixin_defaults = self.env['utm.mixin'].default_get(utm_mixin_fields)
for (mixin_field, field) in zip(utm_mixin_fields, utm_fields):
if field in fields and utm_mixin_defaults.get(mixin_field):
ret_vals[field] = utm_mixin_defaults[mixin_field]
return ret_vals
@api.depends('partner_id')
def _compute_name(self):
for registration in self:
if not registration.name and registration.partner_id:
registration.name = registration._synchronize_partner_values(
registration.partner_id,
fnames={'name'},
).get('name') or False
@api.depends('partner_id')
def _compute_email(self):
for registration in self:
if not registration.email and registration.partner_id:
registration.email = registration._synchronize_partner_values(
registration.partner_id,
fnames={'email'},
).get('email') or False
@api.depends('partner_id')
def _compute_phone(self):
for registration in self:
if not registration.phone and registration.partner_id:
partner_values = registration._synchronize_partner_values(
registration.partner_id,
fnames={'phone', 'mobile'},
)
registration.phone = partner_values.get('phone') or partner_values.get('mobile') or False
@api.depends('partner_id')
def _compute_company_name(self):
for registration in self:
if not registration.company_name and registration.partner_id:
registration.company_name = registration._synchronize_partner_values(
registration.partner_id,
fnames={'company_name'},
).get('company_name') or False
@api.depends('state')
def _compute_date_closed(self):
for registration in self:
if not registration.date_closed:
if registration.state == 'done':
registration.date_closed = self.env.cr.now()
else:
registration.date_closed = False
@api.constrains('event_id', 'event_ticket_id')
def _check_event_ticket(self):
if any(registration.event_id != registration.event_ticket_id.event_id for registration in self if registration.event_ticket_id):
raise ValidationError(_('Invalid event / ticket choice'))
def _synchronize_partner_values(self, partner, fnames=None):
if fnames is None:
fnames = {'name', 'email', 'phone', 'mobile'}
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 fnames if contact[fname])
return {}
@api.onchange('phone', 'event_id', 'partner_id')
def _onchange_phone_validation(self):
if self.phone:
country = self.partner_id.country_id or self.event_id.country_id or self.env.company.country_id
self.phone = self._phone_format(fname='phone', country=country) or self.phone
@api.model
def register_attendee(self, barcode, event_id):
attendee = self.search([('barcode', '=', barcode)], limit=1)
if not attendee:
return {'error': 'invalid_ticket'}
res = attendee._get_registration_summary()
if attendee.state == 'cancel':
status = 'canceled_registration'
elif attendee.event_id.is_finished:
status = 'not_ongoing_event'
elif attendee.state != 'done':
if event_id and attendee.event_id.id != event_id:
status = 'need_manual_confirmation'
else:
attendee.action_set_done()
status = 'confirmed_registration'
else:
status = 'already_registered'
res.update({'status': status, 'event_id': event_id})
return res
# ------------------------------------------------------------
# CRUD
# ------------------------------------------------------------
@api.model_create_multi
def create(self, vals_list):
# format numbers: prefetch side records, then try to format according to country
all_partner_ids = set(values['partner_id'] for values in vals_list if values.get('partner_id'))
all_event_ids = set(values['event_id'] for values in vals_list if values.get('event_id'))
for values in vals_list:
if not values.get('phone'):
continue
related_country = self.env['res.country']
if values.get('partner_id'):
related_country = self.env['res.partner'].with_prefetch(all_partner_ids).browse(values['partner_id']).country_id
if not related_country and values.get('event_id'):
related_country = self.env['event.event'].with_prefetch(all_event_ids).browse(values['event_id']).country_id
if not related_country:
related_country = self.env.company.country_id
values['phone'] = self._phone_format(number=values['phone'], country=related_country) or values['phone']
registrations = super(EventRegistration, self).create(vals_list)
if not self.env.context.get('install_mode', False):
# running the scheduler for demo data can cause an issue where wkhtmltopdf runs during
# server start and hangs indefinitely, leading to serious crashes
# we currently avoid this by not running the scheduler, would be best to find the actual
# reason for this issue and fix it so we can remove this check
registrations._update_mail_schedulers()
return registrations
def write(self, vals):
confirming = vals.get('state') in {'open', 'done'}
to_confirm = (self.filtered(lambda registration: registration.state in {'draft', 'cancel'})
if confirming else None)
ret = super(EventRegistration, self).write(vals)
if confirming and not self.env.context.get('install_mode', False):
# running the scheduler for demo data can cause an issue where wkhtmltopdf runs
# during server start and hangs indefinitely, leading to serious crashes we
# currently avoid this by not running the scheduler, would be best to find the
# actual reason for this issue and fix it so we can remove this check
to_confirm._update_mail_schedulers()
return ret
def _compute_display_name(self):
""" Custom display_name in case a registration is nott linked to an attendee
"""
for registration in self:
registration.display_name = registration.name or f"#{registration.id}"
def toggle_active(self):
pre_inactive = self - self.filtered(self._active_name)
super().toggle_active()
# Necessary triggers as changing registration states cannot be used as triggers for the
# Event(Ticket) models constraints.
if pre_inactive:
pre_inactive.event_id._check_seats_availability()
pre_inactive.event_ticket_id._check_seats_availability()
# ------------------------------------------------------------
# ACTIONS / BUSINESS
# ------------------------------------------------------------
def action_set_previous_state(self):
self.filtered(lambda reg: reg.state == 'open').action_set_draft()
self.filtered(lambda reg: reg.state == 'done').action_confirm()
def action_set_draft(self):
self.write({'state': 'draft'})
def action_confirm(self):
self.write({'state': 'open'})
def action_set_done(self):
""" Close Registration """
self.write({'state': 'done'})
def action_cancel(self):
self.write({'state': 'cancel'})
def action_send_badge_email(self):
""" Open a window to compose an email, with the template - 'event_badge'
message loaded by default
"""
self.ensure_one()
template = self.env.ref('event.event_registration_mail_template_badge', raise_if_not_found=False)
compose_form = self.env.ref('mail.email_compose_message_wizard_form')
ctx = dict(
default_model='event.registration',
default_res_ids=self.ids,
default_template_id=template.id if template else False,
default_composition_mode='comment',
default_email_layout_xmlid="mail.mail_notification_light",
)
return {
'name': _('Compose Email'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'mail.compose.message',
'views': [(compose_form.id, 'form')],
'view_id': compose_form.id,
'target': 'new',
'context': ctx,
}
def _update_mail_schedulers(self):
""" Update schedulers to set them as running again, and cron to be called
as soon as possible. """
open_registrations = self.filtered(lambda registration: registration.state == 'open')
if not open_registrations:
return
onsubscribe_schedulers = self.env['event.mail'].sudo().search([
('event_id', 'in', open_registrations.event_id.ids),
('interval_type', '=', 'after_sub')
])
if not onsubscribe_schedulers:
return
onsubscribe_schedulers.update({'mail_done': False})
# we could simply call _create_missing_mail_registrations and let cron do their job
# but it currently leads to several delays. We therefore call execute until
# cron triggers are correctly used
onsubscribe_schedulers.with_user(SUPERUSER_ID).execute()
# ------------------------------------------------------------
# MAILING / GATEWAY
# ------------------------------------------------------------
def _message_compute_subject(self):
if self.name:
return _(
"%(event_name)s - Registration for %(attendee_name)s",
event_name=self.event_id.name,
attendee_name=self.name,
)
return _(
"%(event_name)s - Registration #%(registration_id)s",
event_name=self.event_id.name,
registration_id=self.id,
)
def _message_get_suggested_recipients(self):
recipients = super(EventRegistration, self)._message_get_suggested_recipients()
public_users = self.env['res.users'].sudo()
public_groups = self.env.ref("base.group_public", raise_if_not_found=False)
if public_groups:
public_users = public_groups.sudo().with_context(active_test=False).mapped("users")
try:
for attendee in self:
is_public = attendee.sudo().with_context(active_test=False).partner_id.user_ids in public_users if public_users else False
if attendee.partner_id and not is_public:
attendee._message_add_suggested_recipient(recipients, partner=attendee.partner_id, reason=_('Customer'))
elif attendee.email:
attendee._message_add_suggested_recipient(recipients, email=attendee.email, reason=_('Customer Email'))
except AccessError: # no read access rights -> ignore suggested recipients
pass
return recipients
def _message_get_default_recipients(self):
# Prioritize registration email over partner_id, which may be shared when a single
# partner booked multiple seats
return {r.id:
{
'partner_ids': [],
'email_to': ','.join(email_normalize_all(r.email)) or r.email,
'email_cc': False,
} for r in self
}
def _message_post_after_hook(self, message, msg_vals):
if self.email and not self.partner_id:
# we consider that posting a message with a specified recipient (not a follower, a specific one)
# on a document without customer means that it was created through the chatter using
# suggested recipients. This heuristic allows to avoid ugly hacks in JS.
email_normalized = email_normalize(self.email)
new_partner = message.partner_ids.filtered(
lambda partner: partner.email == self.email or (email_normalized and partner.email_normalized == email_normalized)
)
if new_partner:
if new_partner[0].email_normalized:
email_domain = ('email', 'in', [new_partner[0].email, new_partner[0].email_normalized])
else:
email_domain = ('email', '=', new_partner[0].email)
self.search([
('partner_id', '=', False), email_domain, ('state', 'not in', ['cancel']),
]).write({'partner_id': new_partner[0].id})
return super(EventRegistration, self)._message_post_after_hook(message, msg_vals)
# ------------------------------------------------------------
# TOOLS
# ------------------------------------------------------------
def get_date_range_str(self, lang_code=False):
self.ensure_one()
today = fields.Datetime.now()
event_date = self.event_begin_date
diff = (event_date.date() - today.date())
if diff.days <= 0:
return _('today')
elif diff.days == 1:
return _('tomorrow')
elif (diff.days < 7):
return _('in %d days', diff.days)
elif (diff.days < 14):
return _('next week')
elif event_date.month == (today + relativedelta(months=+1)).month:
return _('next month')
else:
return _('on %(date)s', date=format_date(self.env, self.event_begin_date, lang_code=lang_code, date_format='medium'))
def _get_registration_summary(self):
self.ensure_one()
return {
'id': self.id,
'name': self.name,
'partner_id': self.partner_id.id,
'ticket_name': self.event_ticket_id.name or _('None'),
'event_id': self.event_id.id,
'event_display_name': self.event_id.display_name,
'company_name': self.event_id.company_id and self.event_id.company_id.name or False,
}

27
models/event_stage.py Normal file
View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, fields, models
class EventStage(models.Model):
_name = 'event.stage'
_description = 'Event Stage'
_order = 'sequence, name'
name = fields.Char(string='Stage Name', required=True, translate=True)
description = fields.Text(string='Stage description', translate=True)
sequence = fields.Integer('Sequence', default=1)
fold = fields.Boolean(string='Folded in Kanban', default=False)
pipe_end = fields.Boolean(
string='End Stage', default=False,
help='Events will automatically be moved into this stage when they are finished. The event moved into this stage will automatically be set as green.')
legend_blocked = fields.Char(
'Red Kanban Label', default=lambda s: _('Blocked'), translate=True, prefetch='legend', required=True,
help='Override the default value displayed for the blocked state for kanban selection.')
legend_done = fields.Char(
'Green Kanban Label', default=lambda s: _('Ready for Next Stage'), translate=True, prefetch='legend', required=True,
help='Override the default value displayed for the done state for kanban selection.')
legend_normal = fields.Char(
'Grey Kanban Label', default=lambda s: _('In Progress'), translate=True, prefetch='legend', required=True,
help='Override the default value displayed for the normal state for kanban selection.')

41
models/event_tag.py Normal file
View File

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from random import randint
from odoo import api, fields, models
class EventTagCategory(models.Model):
_name = "event.tag.category"
_description = "Event Tag Category"
_order = "sequence"
def _default_sequence(self):
"""
Here we use a _default method instead of ordering on 'sequence, id' to
prevent adding a new related stored field in the 'event.tag' model that
would hold the category id.
"""
return (self.search([], order="sequence desc", limit=1).sequence or 0) + 1
name = fields.Char("Name", required=True, translate=True)
sequence = fields.Integer('Sequence', default=_default_sequence)
tag_ids = fields.One2many('event.tag', 'category_id', string="Tags")
class EventTag(models.Model):
_name = "event.tag"
_description = "Event Tag"
_order = "category_sequence, sequence, id"
def _default_color(self):
return randint(1, 11)
name = fields.Char("Name", required=True, translate=True)
sequence = fields.Integer('Sequence', default=0)
category_id = fields.Many2one("event.tag.category", string="Category", required=True, ondelete='cascade')
category_sequence = fields.Integer(related='category_id.sequence', string='Category Sequence', store=True)
color = fields.Integer(
string='Color Index', default=lambda self: self._default_color(),
help='Tag color. No color means no display in kanban or front-end, to distinguish internal tags from public categorization tags.')

202
models/event_ticket.py Normal file
View File

@ -0,0 +1,202 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError, UserError
from odoo.tools.misc import formatLang
class EventTemplateTicket(models.Model):
_name = 'event.type.ticket'
_description = 'Event Template Ticket'
_order = 'sequence, name, id'
sequence = fields.Integer('Sequence', default=10)
# description
name = fields.Char(
string='Name', default=lambda self: _('Registration'),
required=True, translate=True)
description = fields.Text(
'Description', translate=True,
help="A description of the ticket that you want to communicate to your customers.")
event_type_id = fields.Many2one(
'event.type', string='Event Category', ondelete='cascade', required=True)
# seats
seats_limited = fields.Boolean(string='Limit Attendees', readonly=True, store=True,
compute='_compute_seats_limited')
seats_max = fields.Integer(
string='Maximum Attendees',
help="Define the number of available tickets. If you have too many registrations you will "
"not be able to sell tickets anymore. Set 0 to ignore this rule set as unlimited.")
@api.depends('seats_max')
def _compute_seats_limited(self):
for ticket in self:
ticket.seats_limited = ticket.seats_max
@api.model
def _get_event_ticket_fields_whitelist(self):
""" Whitelist of fields that are copied from event_type_ticket_ids to event_ticket_ids when
changing the event_type_id field of event.event """
return ['sequence', 'name', 'description', 'seats_max']
class EventTicket(models.Model):
""" Ticket model allowing to have different kind of registrations for a given
event. Ticket are based on ticket type as they share some common fields
and behavior. Those models come from <= v13 Odoo event.event.ticket that
modeled both concept: tickets for event templates, and tickets for events. """
_name = 'event.event.ticket'
_inherit = 'event.type.ticket'
_description = 'Event Ticket'
_order = "event_id, sequence, name, id"
@api.model
def default_get(self, fields):
res = super(EventTicket, self).default_get(fields)
if 'name' in fields and (not res.get('name') or res['name'] == _('Registration')) and self.env.context.get('default_event_name'):
res['name'] = _('Registration for %s', self.env.context['default_event_name'])
return res
# description
event_type_id = fields.Many2one(ondelete='set null', required=False)
event_id = fields.Many2one(
'event.event', string="Event",
ondelete='cascade', required=True)
company_id = fields.Many2one('res.company', related='event_id.company_id')
# sale
start_sale_datetime = fields.Datetime(string="Registration Start")
end_sale_datetime = fields.Datetime(string="Registration End")
is_launched = fields.Boolean(string='Are sales launched', compute='_compute_is_launched')
is_expired = fields.Boolean(string='Is Expired', compute='_compute_is_expired')
sale_available = fields.Boolean(
string='Is Available', compute='_compute_sale_available', compute_sudo=True,
help='Whether it is possible to sell these tickets')
registration_ids = fields.One2many('event.registration', 'event_ticket_id', string='Registrations')
# seats
seats_reserved = fields.Integer(string='Reserved Seats', compute='_compute_seats', store=False)
seats_available = fields.Integer(string='Available Seats', compute='_compute_seats', store=False)
seats_used = fields.Integer(string='Used Seats', compute='_compute_seats', store=False)
seats_taken = fields.Integer(string="Taken Seats", compute="_compute_seats", store=False)
is_sold_out = fields.Boolean(
'Sold Out', compute='_compute_is_sold_out', help='Whether seats are not available for this ticket.')
# reports
color = fields.Char('Color', default="#875A7B")
@api.depends('end_sale_datetime', 'event_id.date_tz')
def _compute_is_expired(self):
for ticket in self:
ticket = ticket._set_tz_context()
current_datetime = fields.Datetime.context_timestamp(ticket, fields.Datetime.now())
if ticket.end_sale_datetime:
end_sale_datetime = fields.Datetime.context_timestamp(ticket, ticket.end_sale_datetime)
ticket.is_expired = end_sale_datetime < current_datetime
else:
ticket.is_expired = False
@api.depends('start_sale_datetime', 'event_id.date_tz')
def _compute_is_launched(self):
now = fields.Datetime.now()
for ticket in self:
if not ticket.start_sale_datetime:
ticket.is_launched = True
else:
ticket = ticket._set_tz_context()
current_datetime = fields.Datetime.context_timestamp(ticket, now)
start_sale_datetime = fields.Datetime.context_timestamp(ticket, ticket.start_sale_datetime)
ticket.is_launched = start_sale_datetime <= current_datetime
@api.depends('is_expired', 'start_sale_datetime', 'event_id.date_tz', 'seats_available', 'seats_max')
def _compute_sale_available(self):
for ticket in self:
ticket.sale_available = ticket.is_launched and not ticket.is_expired and not ticket.is_sold_out
@api.depends('seats_max', 'registration_ids.state', 'registration_ids.active')
def _compute_seats(self):
""" Determine available, reserved, used and taken seats. """
# initialize fields to 0 + compute seats availability
for ticket in self:
ticket.seats_reserved = ticket.seats_used = ticket.seats_available = 0
# aggregate registrations by ticket and by state
results = {}
if self.ids:
state_field = {
'open': 'seats_reserved',
'done': 'seats_used',
}
query = """ SELECT event_ticket_id, state, count(event_id)
FROM event_registration
WHERE event_ticket_id IN %s AND state IN ('open', 'done') AND active = true
GROUP BY event_ticket_id, state
"""
self.env['event.registration'].flush_model(['event_id', 'event_ticket_id', 'state', 'active'])
self.env.cr.execute(query, (tuple(self.ids),))
for event_ticket_id, state, num in self.env.cr.fetchall():
results.setdefault(event_ticket_id, {})[state_field[state]] = num
# compute seats_available
for ticket in self:
ticket.update(results.get(ticket._origin.id or ticket.id, {}))
if ticket.seats_max > 0:
ticket.seats_available = ticket.seats_max - (ticket.seats_reserved + ticket.seats_used)
ticket.seats_taken = ticket.seats_reserved + ticket.seats_used
@api.depends('seats_limited', 'seats_available')
def _compute_is_sold_out(self):
for ticket in self:
ticket.is_sold_out = ticket.seats_limited and not ticket.seats_available
@api.constrains('start_sale_datetime', 'end_sale_datetime')
def _constrains_dates_coherency(self):
for ticket in self:
if ticket.start_sale_datetime and ticket.end_sale_datetime and ticket.start_sale_datetime > ticket.end_sale_datetime:
raise UserError(_('The stop date cannot be earlier than the start date. '
'Please check ticket %(ticket_name)s', ticket_name=ticket.name))
@api.constrains('registration_ids', 'seats_max')
def _check_seats_availability(self, minimal_availability=0):
sold_out_tickets = []
for ticket in self:
if ticket.seats_max and ticket.seats_available < minimal_availability:
sold_out_tickets.append((_(
'- the ticket "%(ticket_name)s" (%(event_name)s): Missing %(nb_too_many)i seats.',
ticket_name=ticket.name, event_name=ticket.event_id.name, nb_too_many=-ticket.seats_available)))
if sold_out_tickets:
raise ValidationError(_('There are not enough seats available for:')
+ '\n%s\n' % '\n'.join(sold_out_tickets))
@api.depends('seats_max', 'seats_available')
@api.depends_context('name_with_seats_availability')
def _compute_display_name(self):
"""Adds ticket seats availability if requested by context."""
if not self.env.context.get('name_with_seats_availability'):
return super()._compute_display_name()
for ticket in self:
if not ticket.seats_max:
name = ticket.name
elif not ticket.seats_available:
name = _('%(ticket_name)s (Sold out)', ticket_name=ticket.name)
else:
name = _(
'%(ticket_name)s (%(count)s seats remaining)',
ticket_name=ticket.name,
count=formatLang(self.env, ticket.seats_available, digits=0),
)
ticket.display_name = name
def _get_ticket_multiline_description(self):
""" Compute a multiline description of this ticket. It is used when ticket
description are necessary without having to encode it manually, like sales
information. """
return '%s\n%s' % (self.display_name, self.event_id.display_name)
def _set_tz_context(self):
self.ensure_one()
return self.with_context(tz=self.event_id.date_tz or 'UTC')
@api.ondelete(at_uninstall=False)
def _unlink_except_if_registrations(self):
if self.registration_ids:
raise UserError(_(
"The following tickets cannot be deleted while they have one or more registrations linked to them:\n- %s",
'\n- '.join(self.mapped('name'))))

22
models/mail_template.py Normal file
View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
from odoo.osv import expression
class MailTemplate(models.Model):
_inherit = 'mail.template'
@api.model
def _name_search(self, name, domain=None, operator='ilike', limit=None, order=None):
"""Context-based hack to filter reference field in a m2o search box to emulate a domain the ORM currently does not support.
As we can not specify a domain on a reference field, we added a context
key `filter_template_on_event` on the template reference field. If this
key is set, we add our domain in the `domain` in the `_name_search`
method to filtrate the mail templates.
"""
if self.env.context.get('filter_template_on_event'):
domain = expression.AND([[('model', '=', 'event.registration')], domain])
return super()._name_search(name, domain, operator, limit, order)

View File

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import binascii
from odoo import _, api, exceptions, fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
def _default_use_google_maps_static_api(self):
api_key = self.env['ir.config_parameter'].sudo().get_param('google_maps.signed_static_api_key')
api_secret = self.env['ir.config_parameter'].sudo().get_param('google_maps.signed_static_api_secret')
return bool(api_key and api_secret)
google_maps_static_api_key = fields.Char("Google Maps API key", compute="_compute_maps_static_api_key",
readonly=False, store=True, config_parameter='google_maps.signed_static_api_key')
google_maps_static_api_secret = fields.Char("Google Maps API secret", compute="_compute_maps_static_api_secret",
readonly=False, store=True, config_parameter='google_maps.signed_static_api_secret')
module_event_sale = fields.Boolean("Tickets")
module_website_event_meet = fields.Boolean("Discussion Rooms")
module_website_event_track = fields.Boolean("Tracks and Agenda")
module_website_event_track_live = fields.Boolean("Live Mode")
module_website_event_track_quiz = fields.Boolean("Quiz on Tracks")
module_website_event_exhibitor = fields.Boolean("Advanced Sponsors")
use_event_barcode = fields.Boolean(string="Use Event Barcode", help="Enable or Disable Event Barcode functionality.", config_parameter='event.use_event_barcode')
barcode_nomenclature_id = fields.Many2one('barcode.nomenclature', related='company_id.nomenclature_id', readonly=False)
module_website_event_sale = fields.Boolean("Online Ticketing")
module_event_booth = fields.Boolean("Booth Management")
use_google_maps_static_api = fields.Boolean("Google Maps static API", default=_default_use_google_maps_static_api)
@api.depends('use_google_maps_static_api')
def _compute_maps_static_api_key(self):
"""Clear API key on disabling google maps."""
for config in self:
if not config.use_google_maps_static_api:
config.google_maps_static_api_key = ''
@api.depends('use_google_maps_static_api')
def _compute_maps_static_api_secret(self):
"""Clear API secret on disabling google maps."""
for config in self:
if not config.use_google_maps_static_api:
config.google_maps_static_api_secret = ''
@api.onchange('module_website_event_track')
def _onchange_module_website_event_track(self):
""" Reset sub-modules, otherwise you may have track to False but still
have track_live or track_quiz to True, meaning track will come back due
to dependencies of modules. """
for config in self:
if not config.module_website_event_track:
config.module_website_event_track_live = False
config.module_website_event_track_quiz = False
def _check_google_maps_static_api_secret(self):
for config in self:
if config.google_maps_static_api_secret:
try:
base64.urlsafe_b64decode(config.google_maps_static_api_secret)
except binascii.Error:
raise exceptions.UserError(_("Please enter a valid base64 secret"))
@api.model_create_multi
def create(self, vals_list):
configs = super().create(vals_list)
configs._check_google_maps_static_api_secret()
return configs
def write(self, vals):
configs = super().write(vals)
if vals.get('google_maps_static_api_secret'):
configs._check_google_maps_static_api_secret()
return configs

87
models/res_partner.py Normal file
View File

@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import binascii
import hmac
import requests
import werkzeug.urls
from odoo import api, fields, models
class ResPartner(models.Model):
_inherit = 'res.partner'
event_count = fields.Integer(
'# Events', compute='_compute_event_count', groups='event.group_event_registration_desk')
static_map_url = fields.Char(compute="_compute_static_map_url")
static_map_url_is_valid = fields.Boolean(compute="_compute_static_map_url_is_valid")
def _compute_event_count(self):
self.event_count = 0
for partner in self:
partner.event_count = self.env['event.event'].search_count([('registration_ids.partner_id', 'child_of', partner.ids)])
@api.depends('zip', 'city', 'country_id', 'street')
def _compute_static_map_url(self):
for partner in self:
partner.static_map_url = partner._google_map_signed_img(zoom=13, width=598, height=200)
@api.depends('static_map_url')
def _compute_static_map_url_is_valid(self):
"""Compute whether the link is valid.
This should only remain valid for a relatively short time.
Here, for the duration it is in cache.
"""
session = requests.Session()
for partner in self:
url = partner.static_map_url
if not url:
partner.static_map_url_is_valid = False
continue
is_valid = False
# If the response isn't strictly successful, assume invalid url
try:
res = session.get(url, timeout=2)
if res.ok and not res.headers.get('X-Staticmap-API-Warning'):
is_valid = True
except requests.exceptions.RequestException:
pass
partner.static_map_url_is_valid = is_valid
def action_event_view(self):
action = self.env["ir.actions.actions"]._for_xml_id("event.action_event_view")
action['context'] = {}
action['domain'] = [('registration_ids.partner_id', 'child_of', self.ids)]
return action
def _google_map_signed_img(self, zoom=13, width=298, height=298):
"""Create a signed static image URL for the location of this partner."""
GOOGLE_MAPS_STATIC_API_KEY = self.env['ir.config_parameter'].sudo().get_param('google_maps.signed_static_api_key')
GOOGLE_MAPS_STATIC_API_SECRET = self.env['ir.config_parameter'].sudo().get_param('google_maps.signed_static_api_secret')
if not GOOGLE_MAPS_STATIC_API_KEY or not GOOGLE_MAPS_STATIC_API_SECRET:
return None
# generate signature as per https://developers.google.com/maps/documentation/maps-static/digital-signature#server-side-signing
location_string = f"{self.street}, {self.city} {self.zip}, {self.country_id and self.country_id.display_name or ''}"
params = {
'center': location_string,
'markers': f'size:mid|{location_string}',
'size': f"{width}x{height}",
'zoom': zoom,
'sensor': "false",
'key': GOOGLE_MAPS_STATIC_API_KEY,
}
unsigned_path = '/maps/api/staticmap?' + werkzeug.urls.url_encode(params)
try:
api_secret_bytes = base64.urlsafe_b64decode(GOOGLE_MAPS_STATIC_API_SECRET + "====")
except binascii.Error:
return None
url_signature_bytes = hmac.digest(api_secret_bytes, unsigned_path.encode(), 'sha1')
params['signature'] = base64.urlsafe_b64encode(url_signature_bytes)
return 'https://maps.googleapis.com/maps/api/staticmap?' + werkzeug.urls.url_encode(params)

2
report/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

View File

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="paperformat_event_badge" model="report.paperformat">
<field name="name">Custom Paperformat for the Event Badge report</field>
<field name="default" eval="False"/>
<field name="disable_shrinking" eval="True"/>
<field name="format">A4</field>
<field name="page_height">0</field>
<field name="page_width">0</field>
<field name="orientation">Portrait</field>
<field name="margin_top">0</field>
<field name="margin_bottom">0</field>
<field name="margin_left">0</field>
<field name="margin_right">0</field>
<field name="dpi">96</field>
</record>
<record id="paperformat_event_full_page_ticket" model="report.paperformat">
<field name="name">Custom Paperformat for the Event Full Page Ticket report</field>
<field name="default" eval="False"/>
<field name="disable_shrinking" eval="True"/>
<field name="format">A4</field>
<field name="page_height">0</field>
<field name="page_width">0</field>
<field name="orientation">Portrait</field>
<field name="margin_top">0</field>
<field name="margin_bottom">8</field>
<field name="margin_left">0</field>
<field name="margin_right">0</field>
<field name="dpi">96</field>
</record>
<!-- The "Full Page Ticket", as opposed to the (a6) badge that only contains the bare minimum
(attendee name + barcode), gives all the information of the ticket in a portrait A4 format.
It allows to add some information in the ticket_instructions field and, when printed, functions
as an "official" ticket that the attendee can show to the registration desk where all
the information are available (event name, organizer, SO reference, barcode, footer with
sponsors, ...). -->
<record id="action_report_event_registration_full_page_ticket" model="ir.actions.report">
<field name="name">Full Page Ticket</field>
<field name="model">event.registration</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">event.event_registration_report_template_full_page_ticket</field>
<field name="report_file">event.event_registration_report_template_full_page_ticket</field>
<field name="print_report_name">'Full Page Ticket - %s - %s' % ((object.event_id.name or 'Event').replace('/',''), (object.name or '').replace('/',''))</field>
<field name="binding_model_id" ref="model_event_registration"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_event_full_page_ticket"/>
</record>
<record id="action_report_event_event_full_page_ticket" model="ir.actions.report">
<field name="name">Full Page Ticket Example</field>
<field name="model">event.event</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">event.event_event_report_template_full_page_ticket</field>
<field name="report_file">event.event_event_report_template_full_page_ticket</field>
<field name="print_report_name">'Full Page Ticket - %s' % (object.name or 'Event').replace('/','')</field>
<field name="binding_model_id" ref="model_event_event"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_event_full_page_ticket"/>
</record>
<record id="action_report_event_registration_badge" model="ir.actions.report">
<field name="name">Badge</field>
<field name="model">event.registration</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">event.event_registration_report_template_badge</field>
<field name="report_file">event.event_registration_report_template_badge</field>
<field name="print_report_name">'Badge - %s - %s' % ((object.event_id.name or 'Event').replace('/',''), (object.name or '').replace('/',''))</field>
<field name="binding_model_id" ref="model_event_registration"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_event_badge"/>
</record>
<record id="action_report_event_event_badge" model="ir.actions.report">
<field name="name">Badge Example</field>
<field name="model">event.event</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">event.event_event_report_template_badge</field>
<field name="report_file">event.event_event_report_template_badge</field>
<field name="print_report_name">'Badge - %s' % (object.name or 'Event').replace('/','')</field>
<field name="binding_model_id" ref="model_event_event"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_event_badge"/>
</record>
<record id="action_report_event_registration_responsive_html_ticket" model="ir.actions.report">
<field name="name">Responsive Html Full Page Ticket</field>
<field name="model">event.registration</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">event.event_registration_report_template_responsive_html_ticket</field>
<field name="report_file">event.event_registration_report_template_responsive_html_ticket</field>
</record>
<record id="action_report_event_event_attendee_list" model="ir.actions.report">
<field name="name">Attendee List</field>
<field name="model">event.event</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">event.event_event_attendee_list</field>
<field name="report_file">event.event_event_attendee_list</field>
<field name="binding_model_id" ref="event.model_event_event"/>
<field name="binding_type">report</field>
<field name="print_report_name">'Attendee List - %s' % (object.name)</field>
</record>
</odoo>

View File

@ -0,0 +1,312 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- EVENT A4 FOLDABLE BADGE (A4_french_fold) -->
<template id="event_report_template_foldable_badge">
<div class="o_event_foldable_badge_container o_event_badge_report_page_break">
<div class="row">
<!-- Front -->
<div class="page col-6 o_event_badge_height pt-2">
<div class="oe_structure"/>
<t t-call="event.event_report_template_badge_card"/>
</div>
<!-- Back -->
<div class="page col-6 position-relative p-1">
<div class="oe_structure"/>
<div class="o_event_foldable_badge_back text-center">
<t t-if="event.use_barcode">
<t t-if="attendee">
<span t-field="attendee.barcode" class="barcode ms-2" t-options="{'widget': 'barcode', 'width': 200, 'height': 84, 'quiet': 0, 'humanreadable': 1}"/>
</t>
<t t-elif="not attendee">
<span t-out="12345678901234567890" class="barcode ms-2" t-options="{'widget': 'barcode', 'width': 200, 'height': 84, 'quiet': 0, 'humanreadable': 1}"/>
</t>
</t>
<div class="o_event_foldable_badge_barcode_container_top">
<img t-attf-src="/report/barcode/QR/{{attendee.barcode if attendee else '12345678901234567890'}}?&amp;width=174&amp;height=174&amp;quiet=0" alt="QR Code"/>
</div>
<div class="fs-4 mt-2">
<span t-out="attendee.barcode if attendee else '12345678901234567890'"/>
</div>
</div>
</div>
</div>
<div class="row o_event_foldable_badge_bottom_row">
<!-- Reversed Back -->
<div class="o_event_foldable_badge_bottom_quarter page col-6 p-1">
<div class="oe_structure"/>
<div class="o_event_foldable_badge_back text-center">
<t t-if="event.use_barcode">
<t t-if="attendee">
<span t-field="attendee.barcode" class="barcode ms-2" t-options="{'widget': 'barcode', 'width': 200, 'height': 84, 'quiet': 0, 'humanreadable': 1}"/>
</t>
<t t-elif="not attendee">
<span t-out="12345678901234567890" class="barcode ms-2" t-options="{'widget': 'barcode', 'width': 200, 'height': 84, 'quiet': 0, 'humanreadable': 1}"/>
</t>
</t>
<div class="o_event_foldable_badge_barcode_container_bottom">
<img t-attf-src="/report/barcode/QR/{{attendee.barcode if attendee else '12345678901234567890'}}?&amp;width=174&amp;height=174&amp;quiet=0" alt="QR Code"/>
</div>
<div class="fs-4 mt-2">
<span t-out="attendee.barcode if attendee else '12345678901234567890'"/>
</div>
</div>
</div>
<!-- Reversed Front -->
<div class="o_event_foldable_badge_bottom_quarter page col-6 pt-2">
<div class="oe_structure"/>
<t t-call="event.event_report_template_badge_card"/>
</div>
</div>
</div>
</template>
<!-- EVENT A6 BADGE -->
<template id="event_report_template_a6_badge">
<div class="o_event_badge_report_page_break">
<div class="row">
<div class="o_event_badge_height page col-6 pt-2">
<div class="oe_structure"/>
<t t-call="event.event_report_template_badge_card"/>
</div>
</div>
</div>
</template>
<!-- EVENT A4 4 PER SHEET -->
<template id="event_report_template_four_per_sheet_badge">
<t t-if="example_badge" t-foreach="[(0, 1), (2, 3)]" t-as="indices_pair">
<div t-att-class="'row' + (' o_event_badge_report_page_break' if indices_pair_index else '')">
<div t-foreach="indices_pair" t-as="attendee_number" class="col-6 o_event_badge_height pt-2">
<t t-call="event.event_report_template_badge_card"/>
</div>
</div>
</t>
<t t-if="not example_badge" t-foreach="[attendees[n:n+2] for n in range(0, len(attendees), 2)] if attendees else []" t-as="attendee_pair">
<t t-set="do_page_break" t-value="attendee_pair_index > 0 and attendee_pair_index % 2 or (attendee_pair_index + 1) * 2 >= len(attendees)"/>
<div t-att-class="'row' + (' o_event_badge_report_page_break' if do_page_break else '')">
<div t-foreach="attendee_pair" t-as="attendee" class="col-6 o_event_badge_height pt-2">
<t t-call="event.event_report_template_badge_card"/>
</div>
</div>
</t>
</template>
<!-- EVENT REGISTRATION BADGE - REDIRECTING TO EVENT FORMAT BADGE ABOVE -->
<template id="event_registration_report_template_badge">
<t t-call="web.basic_layout">
<t t-foreach="docs.grouped('event_id').items()" t-as="attendees_per_event">
<t t-set="event" t-value="attendees_per_event[0]"/>
<t t-set="attendees" t-value="attendees_per_event[1]"/>
<t t-if="event.badge_format != 'four_per_sheet'" t-foreach="attendees" t-as="attendee">
<t t-if="event.badge_format == 'A4_french_fold'" t-call="event.event_report_template_foldable_badge"/>
<t t-else="" t-call="event.event_report_template_a6_badge"/>
</t>
<t t-if="event.badge_format == 'four_per_sheet'" t-call="event.event_report_template_four_per_sheet_badge"/>
</t>
</t>
</template>
<!-- EVENT EVENT BADGE - EXAMPLE BADGE - ATTENDEE NOT SET -->
<template id="event_event_report_template_badge">
<t t-call="web.basic_layout">
<t t-foreach="docs" t-as="event">
<t t-set="example_badge" t-value="True"/>
<t t-if="event.badge_format == 'A4_french_fold'" t-call="event.event_report_template_foldable_badge"/>
<t t-elif="event.badge_format == 'four_per_sheet'" t-call="event.event_report_template_four_per_sheet_badge"/>
<t t-else="" t-call="event.event_report_template_a6_badge"/>
</t>
</t>
</template>
<!-- EVENT FULL PAGE TICKET -->
<template id="event_report_template_full_page_ticket">
<div class="row page">
<div t-attf-class="o_event_full_page_ticket_container page w-100 #{'o_event_full_page_ticket_responsive_html' if responsive_html else 'o_event_full_page_ticket'}">
<div class="o_event_full_page_ticket_wrapper">
<div class="o_event_full_page_ticket_details d-flex">
<div class="o_event_full_page_left_details ps-3 pt-4 pb-3 pe-2">
<span itemprop="startDate" t-field="event.date_begin"
t-options='{"widget": "datetime", "date_only": True}'
class="fw-bold"/>
<span itemprop="startDateTime" t-field="event.date_begin"
class="fw-bold"
t-options='{"widget": "datetime", "time_only": True, "hide_seconds": True, "tz_name": event.date_tz}'/>
<h2 class="o_event_full_page_ticket_event_name fw-bold pt-3 pb-2" t-field="event.name"/>
<div t-if="event.address_id">
<t t-set="use_map_marker" t-value="False"/>
<t t-call="event.event_report_template_formatted_event_address"/>
</div>
<div t-if="not responsive_html" t-field="event.ticket_instructions" class="o_event_full_page_extra_instructions pt-3"></div>
<div class="row pt-3 g-0">
<h5 class="col-6" t-if="attendee" t-field="attendee.name"></h5>
<h5 class="col-6" t-elif="not attendee"><span>John Doe</span></h5>
<t t-set="first_ticket" t-value="event.event_ticket_ids[0] if event.event_ticket_ids else None"/>
<div class="col-6 text-end">
<div t-if="attendee" class="o_event_full_page_ticket_font_faded o_event_full_page_ticket_type pe-4" t-field="attendee.event_ticket_id"/>
<div t-elif="first_ticket" t-out="first_ticket.name" class="o_event_full_page_ticket_font_faded pe-4"/>
</div>
</div>
<div t-if="not responsive_html" class="o_event_full_page_ticket_sponsors_container"></div>
</div>
<div class="o_event_full_page_ticket_barcode">
<div class="o_event_full_page_ticket_barcode_container px-2">
<t t-if="not attendee or (attendee and attendee.barcode)">
<div class="pb-3">
<img t-attf-src="/report/barcode/QR/{{attendee.barcode if attendee and attendee.barcode else '12345678901234567890'}}?&amp;width=116&amp;height=116&amp;quiet=0" alt="QR Code"/>
</div>
<t t-if="event.use_barcode">
<img class="o_event_barcode" t-attf-src="/report/barcode/?barcode_type=Code128&amp;value={{attendee.barcode if attendee and attendee.barcode else '12345678901234567890'}}&amp;width=168&amp;height=84&amp;humanreadable=1&amp;quiet=0" alt="Barcode"/>
</t>
</t>
</div>
</div>
</div>
</div>
<div class="page oe_structure"/>
</div>
</div>
</template>
<template id="event_report_full_page_ticket_layout">
<!-- Inspired from "external_layout_standard" to get a repeated footer element. -->
<div class="article"
t-att-data-oe-model="main_object and main_object._name"
t-att-data-oe-id="main_object and main_object.id"
t-att-data-oe-lang="main_object and main_object.env.context.get('lang')">
<main>
<t t-out="0"/>
</main>
</div>
<div class="oe_structure"></div>
<div class="row footer o_event_full_page_ticket_footer d-block">
<div class="o_event_full_page_ticket_powered_by bg-odoo text-white text-center p-2 w-100">
<span t-if="event.organizer_id">
<span class="fw-bold" t-field="event.organizer_id.name">Marc Demo</span>
<span t-if="event.organizer_id.phone" class="ps-3 fa fa-phone"/>
<span t-if="event.organizer_id.phone" t-field="event.organizer_id.phone">+123456789</span>
<span t-if="event.organizer_id.email_normalized" class="ps-3 fa fa-envelope"/>
<span t-if="event.organizer_id.email_normalized" t-field="event.organizer_id.email_normalized">organizer@email.com</span>
<span t-if="event.organizer_id.website" class="ps-3 fa fa-globe"/>
<span t-if="event.organizer_id.website" t-field="event.organizer_id.website">https://www.example.com</span>
</span>
<t t-else="">
<span t-out="event.name">Odoo Community Days</span> <!-- Force some content to avoid messing the layout -->
</t>
</div>
</div>
<div class="oe_structure"></div>
</template>
<template id="event_registration_report_template_full_page_ticket">
<t t-foreach="docs" t-as="attendee">
<t t-call="web.html_container">
<t t-set="event" t-value="attendee.event_id._set_tz_context()"/>
<t t-set="main_object" t-value="attendee"/>
<t t-set="responsive_html" t-value="False"/>
<t t-call="event.event_report_full_page_ticket_layout">
<t t-call="event.event_report_template_full_page_ticket"/>
</t>
</t>
</t>
</template>
<template id="event_event_report_template_full_page_ticket">
<t t-foreach="docs" t-as="event">
<t t-call="web.html_container">
<t t-set="event" t-value="event._set_tz_context()"/>
<t t-set="main_object" t-value="event"/>
<t t-set="responsive_html" t-value="False"/>
<t t-call="event.event_report_full_page_ticket_layout">
<t t-call="event.event_report_template_full_page_ticket"/>
</t>
</t>
</t>
</template>
<!-- EVENT RESPONSIVE HTML TICKET : Responsive web page -->
<template id="event_registration_report_template_responsive_html_ticket">
<t t-foreach="docs" t-as="attendee">
<t t-call="web.html_container">
<t t-set="event" t-value="attendee.event_id._set_tz_context()"/>
<t t-set="main_object" t-value="attendee"/>
<t t-set="responsive_html" t-value="True"/>
<t t-call="event.event_report_full_page_ticket_layout">
<t t-call="event.event_report_template_full_page_ticket"/>
</t>
</t>
</t>
</template>
<!-- EVENT BADGE CARD (tool template used in all badge templates)-->
<template id="event_report_template_badge_card">
<t t-set="badge_image_url" t-value="image_data_uri(event.badge_image) if event.badge_image else ''"/>
<div class="o_event_badge_ticket_wrapper" t-attf-style="background-image: url({{badge_image_url}});">
<div class="position-relative h-100">
<h3 class="fw-bold text-center" t-field="event.name"/>
<div class="text-center mt-5 py-2">
<h2 class="mb-0" t-if="attendee" t-field="attendee.name"/>
<h2 class="mb-0" t-elif="not attendee"><span>John Doe</span> <span t-if="attendee_number" t-out="attendee_number + 1"/></h2>
<h4 t-if="attendee and attendee.company_name" class="o_event_badge_font_faded" t-field="attendee.company_name"/>
<h4 t-elif="not attendee"><span class="o_event_badge_font_faded">My Placeholder Company</span></h4>
</div>
<div class="position-absolute bottom-0 w-100 text-center">
<img t-if="event.organizer_id.image_256" class="o_event_badge_logo text-center mb-2" t-att-src="image_data_uri(event.organizer_id.image_256)"/>
<span t-if="event.badge_format != 'A4_french_fold'" class="o_event_badge_barcode_container mb-2">
<img t-att-class="'mb-2' + (' ms-5' if event.organizer_id.image_256 else '')" t-attf-src="/report/barcode/QR/{{attendee.barcode if attendee else '12345678901234567890'}}?&amp;width=116&amp;height=116&amp;quiet=0" alt="QR Code"/>
<t t-if="event.use_barcode">
<t t-if="attendee">
<span t-field="attendee.barcode" class="barcode ms-2" t-options="{'widget': 'barcode', 'width': 200, 'height': 84, 'quiet': 0, 'humanreadable': 1}"/>
</t>
<t t-elif="not attendee">
<span t-out="12345678901234567890" class="barcode ms-2" t-options="{'widget': 'barcode', 'width': 200, 'height': 84, 'quiet': 0, 'humanreadable': 1}"/>
</t>
</t>
</span>
<t t-set="first_ticket" t-value="event.event_ticket_ids[0] if event.event_ticket_ids else None"/>
<t t-set="ticket" t-value="attendee.event_ticket_id if attendee else first_ticket"/>
<div t-if="ticket" class="text-center w-100" t-attf-style="background-color: {{ticket.color or '#875A7B'}};">
<div class="p-3 fs-3" t-out="ticket.name"/>
</div>
</div>
</div>
</div>
</template>
<!-- MISC -->
<template id="event_report_template_formatted_event_address">
<!-- Small utility template to display "Venue" as:
fa-map-marker PartnerName
RestOfAddress -->
<span t-if="use_map_marker" class="fa fa-map-marker"/>
<t t-if="event.address_id.contact_address.strip()">
<t t-set="address_bits" t-value="event.address_id.contact_address.split('\n')"/>
<span t-if="address_bits" t-out="address_bits[0]">Rue de la Paix 123</span>
<t t-if="len(address_bits) > 1">
<br/>
</t>
<t t-set="remaining_bits" t-value="address_bits[1:]"/>
<t t-foreach="remaining_bits" t-as="address_bit">
<t t-if="address_bit and address_bit.strip()">
<span class="text-muted" t-out="address_bit">Rue de la Paix 123</span>
</t>
</t>
</t>
<span t-else="" t-out="event.address_id.name">1000 Brussels</span>
</template>
</odoo>

View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="action_report_event_registration_attendee_list" model="ir.actions.report">
<field name="name">Attendee List</field>
<field name="model">event.registration</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">event.event_registration_attendee_list</field>
<field name="report_file">event.event_registration_attendee_list</field>
<field name="binding_model_id" ref="event.model_event_registration"/>
<field name="binding_type">report</field>
<field name="print_report_name">'Attendee List'</field>
</record>
</data>
<template id="attendee_list">
<h1>Attendee list</h1>
<div class="row">
<div class="col-7">
<div t-out="event.name"/>
</div>
<div class="col-5">
<span t-out="event.date_begin"/>
<i class="fa fa-arrow-right"/>
<span t-out="event.date_end"/>
</div>
</div>
<table class="table mt-3" style="page-break-after:always;">
<thead>
<tr class="text-start">
<th>Name</th>
<th>Company</th>
<th>Ticket type</th>
<th>Phone number</th>
<th></th>
</tr>
</thead>
<tbody>
<tr t-foreach="attendees" t-as="attendee">
<td><t t-out="attendee.name"/></td>
<td><t t-out="attendee.company_name"/></td>
<td><t t-out="attendee.event_ticket_id.name"/></td>
<td><t t-out="attendee.phone"/></td>
<td class="text-center">
<t t-if="attendee.barcode">
<img t-attf-src="/report/barcode/QR/{{ attendee.barcode }}?&amp;width=87&amp;height=87&amp;quiet=0" alt="QR Code"/>
</t>
</td>
</tr>
</tbody>
</table>
</template>
<template id="event_registration_attendee_list">
<t t-call="web.html_container">
<t t-call="web.internal_layout">
<t t-foreach="docs.grouped('event_id').items()" t-as="group">
<t t-call="event.attendee_list">
<t t-set="event" t-value="group[0].with_context(tz=group[0].date_tz)"/>
<t t-set="attendees" t-value="group[1]"/>
</t>
</t>
</t>
</t>
</template>
<template id="event_event_attendee_list">
<t t-call="web.html_container">
<t t-call="web.internal_layout">
<t t-foreach="docs" t-as="event">
<t t-call="event.attendee_list">
<t t-set="event" t-value="event.with_context(tz=event.date_tz)"/>
<t t-set="attendees" t-value="event.registration_ids"/>
</t>
</t>
</t>
</t>
</template>
</odoo>

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<record model="ir.module.category" id="base.module_category_marketing_events">
<field name="description">Helps you manage your Events.</field>
<field name="sequence">18</field>
</record>
<record id="group_event_registration_desk" model="res.groups">
<field name="name">Registration Desk</field>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
<field name="category_id" ref="base.module_category_marketing_events"/>
</record>
<record id="group_event_user" model="res.groups">
<field name="name">User</field>
<field name="implied_ids" eval="[(4, ref('group_event_registration_desk'))]"/>
<field name="category_id" ref="base.module_category_marketing_events"/>
</record>
<record id="group_event_manager" model="res.groups">
<field name="name">Administrator</field>
<field name="category_id" ref="base.module_category_marketing_events"/>
<field name="implied_ids" eval="[(4, ref('group_event_user')), (4, ref('mail.group_mail_template_editor'))]"/>
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record>
</data>
<data noupdate="1">
<record id="base.default_user" model="res.users">
<field name="groups_id" eval="[(4,ref('event.group_event_manager'))]"/>
</record>
<!-- Multi - Company Rules -->
<record model="ir.rule" id="event_event_company_rule">
<field name="name">Event: multi-company</field>
<field name="model_id" ref="model_event_event"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record model="ir.rule" id="event_registration_company_rule">
<field name="name">Event/Registration: multi-company</field>
<field name="model_id" ref="model_event_registration"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="ir_rule_event_event_ticket_company" model="ir.rule">
<field name="name">Event/Ticket: multi-company</field>
<field name="model_id" ref="model_event_event_ticket"/>
<field name="domain_force">['|', ('event_id.company_id', '=', False), ('event_id.company_id', 'in', company_ids)]</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,28 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_event_type_registration,event.type.registration,model_event_type,event.group_event_registration_desk,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_registration,event.type.ticket.registration,model_event_type_ticket,event.group_event_registration_desk,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_registration,event.event.registration,model_event_event,event.group_event_registration_desk,1,0,0,0
access_event_event_user,event.event.user,model_event_event,event.group_event_user,1,1,1,0
access_event_event_manager,event.event.manager,model_event_event,event.group_event_manager,1,1,1,1
access_event_event_ticket,event.event.ticket,model_event_event_ticket,,0,0,0,0
access_event_event_ticket_registration,event.event.ticket.registration,model_event_event_ticket,event.group_event_registration_desk,1,0,0,0
access_event_event_ticket_user,event.event.ticket.user,model_event_event_ticket,event.group_event_user,1,1,1,1
access_event_registration,event.registration,model_event_registration,,0,0,0,0
access_event_registration_registration,event.registration.registration,model_event_registration,event.group_event_registration_desk,1,1,1,0
access_event_registration_manager,event.registration.manager,model_event_registration,event.group_event_manager,1,1,1,1
access_event_mail_registration,event.mail.registration,model_event_mail,event.group_event_registration_desk,1,0,0,0
access_event_mail_user,event.mail.user,model_event_mail,event.group_event_user,1,1,1,1
access_event_mail_registration_registration,event.mail.registration.registration,model_event_mail_registration,event.group_event_registration_desk,1,0,0,0
access_event_mail_registration_manager,event.mail.registration.manager,model_event_mail_registration,event.group_event_manager,1,1,1,1
access_event_type_mail_registration,event.type.mail.registration,model_event_type_mail,event.group_event_registration_desk,1,0,0,0
access_event_type_mail_manager,event.type.mail.manager,model_event_type_mail,event.group_event_manager,1,1,1,1
access_event_stage_registration,event.stage.registration,model_event_stage,event.group_event_registration_desk,1,0,0,0
access_event_stage_manager,event.stage.manager,model_event_stage,event.group_event_manager,1,1,1,1
access_event_tag_category_registration,event.tag.category.registration,model_event_tag_category,event.group_event_registration_desk,1,0,0,0
access_event_tag_category_user,event.tag.category.user,model_event_tag_category,event.group_event_user,1,1,1,1
access_event_tag,event.tag,model_event_tag,,0,0,0,0
access_event_tag_registration,event.tag.user,model_event_tag,event.group_event_registration_desk,1,0,0,0
access_event_tag_user,event.tag.user,model_event_tag,event.group_event_user,1,1,1,0
access_event_tag_manager,event.tag.manager,model_event_tag,event.group_event_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_event_type_registration event.type.registration model_event_type event.group_event_registration_desk 1 0 0 0
3 access_event_type_manager event.type.manager model_event_type event.group_event_manager 1 1 1 1
4 access_event_type_ticket_registration event.type.ticket.registration model_event_type_ticket event.group_event_registration_desk 1 0 0 0
5 access_event_type_ticket_manager event.type.ticket.manager model_event_type_ticket event.group_event_manager 1 1 1 1
6 access_event_event_registration event.event.registration model_event_event event.group_event_registration_desk 1 0 0 0
7 access_event_event_user event.event.user model_event_event event.group_event_user 1 1 1 0
8 access_event_event_manager event.event.manager model_event_event event.group_event_manager 1 1 1 1
9 access_event_event_ticket event.event.ticket model_event_event_ticket 0 0 0 0
10 access_event_event_ticket_registration event.event.ticket.registration model_event_event_ticket event.group_event_registration_desk 1 0 0 0
11 access_event_event_ticket_user event.event.ticket.user model_event_event_ticket event.group_event_user 1 1 1 1
12 access_event_registration event.registration model_event_registration 0 0 0 0
13 access_event_registration_registration event.registration.registration model_event_registration event.group_event_registration_desk 1 1 1 0
14 access_event_registration_manager event.registration.manager model_event_registration event.group_event_manager 1 1 1 1
15 access_event_mail_registration event.mail.registration model_event_mail event.group_event_registration_desk 1 0 0 0
16 access_event_mail_user event.mail.user model_event_mail event.group_event_user 1 1 1 1
17 access_event_mail_registration_registration event.mail.registration.registration model_event_mail_registration event.group_event_registration_desk 1 0 0 0
18 access_event_mail_registration_manager event.mail.registration.manager model_event_mail_registration event.group_event_manager 1 1 1 1
19 access_event_type_mail_registration event.type.mail.registration model_event_type_mail event.group_event_registration_desk 1 0 0 0
20 access_event_type_mail_manager event.type.mail.manager model_event_type_mail event.group_event_manager 1 1 1 1
21 access_event_stage_registration event.stage.registration model_event_stage event.group_event_registration_desk 1 0 0 0
22 access_event_stage_manager event.stage.manager model_event_stage event.group_event_manager 1 1 1 1
23 access_event_tag_category_registration event.tag.category.registration model_event_tag_category event.group_event_registration_desk 1 0 0 0
24 access_event_tag_category_user event.tag.category.user model_event_tag_category event.group_event_user 1 1 1 1
25 access_event_tag event.tag model_event_tag 0 0 0 0
26 access_event_tag_registration event.tag.user model_event_tag event.group_event_registration_desk 1 0 0 0
27 access_event_tag_user event.tag.user model_event_tag event.group_event_user 1 1 1 0
28 access_event_tag_manager event.tag.manager model_event_tag event.group_event_manager 1 1 1 1

BIN
static/description/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Some files were not shown because too many files have changed in this diff Show More