Initial commit: Core packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:45 +02:00
commit 12c29a983b
9512 changed files with 8379910 additions and 0 deletions

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

View file

@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
{
'name': 'Events Organization',
'version': '1.7',
'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': ['base_setup', 'mail', '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',
'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/scss/event.scss',
'event/static/src/icon_selection_field/icon_selection_field.js',
'event/static/src/icon_selection_field/icon_selection_field.xml',
],
'web.assets_common': [
'event/static/src/js/tours/**/*',
],
'web.assets_frontend': [
'event/static/src/js/tours/**/*',
],
'web.report_assets_common': [
'/event/static/src/scss/event_foldable_badge_report.scss',
'/event/static/src/scss/event_full_page_ticket_report.scss',
],
'web.report_assets_pdf': [
'/event/static/src/scss/event_full_page_ticket_report_pdf.scss',
],
},
'license': 'LGPL-3',
}

View file

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

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from werkzeug.exceptions import NotFound
from odoo.http import Controller, request, route, content_disposition
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))
])

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Event Categories -->
<record id="event_type_data_ticket" model="event.type">
<field name="name">Ticketing</field>
<field name="auto_confirm" eval="False"/>
</record>
<record id="event_type_data_conference" model="event.type">
<field name="name">Conference</field>
<field name="auto_confirm" eval="True"/>
</record>
<!-- 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>

View file

@ -0,0 +1,265 @@
<?xml version="1.0"?>
<odoo><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">America/Los_Angeles</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>
</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>
</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>
</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="event_type_id" ref="event_type_data_conference"/>
<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>
</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>
</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>
</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>
</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">America/Los_Angeles</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>
</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="auto_confirm" eval="True"/>
<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')"/>
</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>
</record>
</data></odoo>

View file

@ -0,0 +1,96 @@
<?xml version="1.0"?>
<odoo><data>
<!-- Event Type -->
<record id="event_type_0" model="event.type">
<field name="name">Exhibition</field>
<field name="auto_confirm" eval="False"/>
</record>
<record id="event_type_1" model="event.type">
<field name="name">Training</field>
<field name="auto_confirm" eval="False"/>
</record>
<record id="event_type_2" model="event.type">
<field name="name">Sport</field>
<field name="auto_confirm" eval="False"/>
<field name="default_timezone">America/Los_Angeles</field>
</record>
<record id="event_type_data_conference" model="event.type">
<field name="default_timezone">Europe/Brussels</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,175 @@
<?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_confirm"
context="{'install_mode' : True}"
eval="[[ref('event_registration_0_0'), ref('event_registration_0_1'),
ref('event_registration_1_0'), ref('event_registration_1_1'), ref('event_registration_1_2'),
ref('event_registration_2_0'), ref('event_registration_2_1'), ref('event_registration_2_2'), ref('event_registration_2_3'),
ref('event_registration_4_2')]]"
/>
<function model="event.registration"
name="action_set_done"
eval="[[ref('event_registration_4_0'), ref('event_registration_4_1')]]"
/>
</data></odoo>

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>

View file

@ -0,0 +1,480 @@
<?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">
<div>
Dear <t t-out="object.name or ''">Oscar Morgan</t>,<br/>
Thank you for your inquiry.<br/>
Here is your badge for the event <t t-out="object.event_id.name or ''">OpenWood Collection Online Reveal</t>.<br/>
If you have any questions, please let us know.
<br/><br/>
Thank you,
<t t-if="object.event_id.user_id.signature">
<br />
<t t-out="object.event_id.user_id.signature or ''">--<br/>Mitchell Admin</t>
</t>
</div></field>
<field name="report_template" ref="action_report_event_registration_foldable_badge"/>
<field name="report_name">Foldable Badge - {{ (object.event_id.name or 'Event').replace('/','_') }}</field>
<field name="lang">{{ 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="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 ''">Oscar Morgan</t>
</span>
</td><td valign="middle" align="right">
<t t-if="is_online">
<a t-att-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-else="">
<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 ''">Oscar Morgan</t>,<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>
for attendee <t t-out="object.name or ''">Oscar Morgan</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><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 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 or ''">+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">
See location on Google Maps
</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" ref="action_report_event_registration_full_page_ticket"/>
<field name="report_name">Full Page Ticket - {{ (object.event_id.name or 'Event').replace('/','') }}</field>
<field name="lang">{{ 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 ''">Oscar Morgan</span>
</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-else="">
<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 ''">Oscar Morgan</t>,<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" 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">
See location on Google Maps
</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.partner_id.lang }}</field>
</record>
</data>
</odoo>

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>

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>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

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

View file

@ -0,0 +1,684 @@
# -*- 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
from odoo.addons.base.models.res_partner import _tz_get
from odoo.tools import format_datetime, is_html_empty
from odoo.exceptions import UserError, ValidationError
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()
# 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")
auto_confirm = fields.Boolean(
'Automatically Confirm Registrations', default=True,
help="Events and registrations will automatically be confirmed "
"upon creation, easing the flow for simple events.")
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'
@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()
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)
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,
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
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")
# 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_unconfirmed = fields.Integer(
string='Unconfirmed Registrations',
store=False, readonly=True, compute='_compute_seats')
seats_used = fields.Integer(
string='Number of Attendees',
store=False, readonly=True, compute='_compute_seats')
seats_expected = fields.Integer(
string='Number of Expected Attendees',
store=False, readonly=True, compute='_compute_seats')
# Registration fields
auto_confirm = fields.Boolean(
string='Autoconfirmation', compute='_compute_auto_confirm', readonly=False, store=True,
help='Autoconfirm Registrations. Registrations will automatically be confirmed upon creation.')
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='Display Timezone', required=True,
compute='_compute_date_tz', precompute=True, readonly=False, store=True,
help="Indicates the timezone in which the event dates/times will be displayed on the website.")
date_begin = fields.Datetime(string='Start Date', required=True, tracking=True,
help="When the event is scheduled to take place (expressed in your local timezone on the form view).")
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,
tracking=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
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)
# ticket reports
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.")
@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 reserved, available, reserved but unconfirmed and used seats. """
# initialize fields to 0
for event in self:
event.seats_unconfirmed = event.seats_reserved = event.seats_used = event.seats_available = 0
# aggregate registrations by event and by state
state_field = {
'draft': 'seats_unconfirmed',
'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 ('draft', '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_expected = event.seats_unconfirmed + 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)]
event_ids = self.env['event.event']._search(domain)
return [('id', 'in', event_ids)]
@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)]
event_ids = self.env['event.event']._search(domain)
return [('id', 'in', event_ids)]
@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.'))
address_ids = self.env['res.partner']._search([
'|', '|', '|', '|', '|',
('street', 'ilike', value),
('street2', 'ilike', value),
('city', 'ilike', value),
('zip', 'ilike', value),
('state_id', 'ilike', value),
('country_id', 'ilike', value),
])
return [('address_id', 'in', address_ids)]
# 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_auto_confirm(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:
event.auto_confirm = event.event_type_id.auto_confirm
@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
def name_get(self):
"""Adds ticket seats availability if requested by context."""
if not self.env.context.get('name_with_seats_availability'):
return super().name_get()
res = []
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
res.append((event.id, name))
return res
@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
@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()

View file

@ -0,0 +1,338 @@
# -*- 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)
# Mail is sent to all attendees (unconfirmed as well), so count all attendees
scheduler.update({
'mail_done': True,
'mail_count_done': scheduler.event_id.seats_expected,
})
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,402 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from dateutil.relativedelta import relativedelta
import pytz
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
# phone_validation is not officially in the depends of event, but we would like
# to have the formatting available in event, not in event_sms -> do a conditional
# import just to be sure
try:
from odoo.addons.phone_validation.tools.phone_validation import phone_format
except ImportError:
def phone_format(number, country_code, country_phone_code, force_format='INTERNATIONAL', raise_exception=True):
return number
class EventRegistration(models.Model):
_name = 'event.registration'
_description = 'Event Registration'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'id desc'
# event
event_id = fields.Many2one(
'event.event', string='Event', required=True,
readonly=True, states={'draft': [('readonly', False)]})
event_ticket_id = fields.Many2one(
'event.event.ticket', string='Event Ticket', readonly=True, ondelete='restrict',
states={'draft': [('readonly', False)]})
active = fields.Boolean(default=True)
# 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=10)
email = fields.Char(string='Email', compute='_compute_email', readonly=False, store=True, tracking=11)
phone = fields.Char(string='Phone', compute='_compute_phone', readonly=False, store=True, tracking=12)
mobile = fields.Char(string='Mobile', compute='_compute_mobile', readonly=False, store=True, tracking=13)
# 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=True, states={'draft': [('readonly', False)]})
state = fields.Selection([
('draft', 'Unconfirmed'), ('cancel', 'Cancelled'),
('open', 'Confirmed'), ('done', 'Attended')],
string='Status', default='draft', readonly=True, copy=False, tracking=True)
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:
registration.phone = registration._synchronize_partner_values(
registration.partner_id,
fnames=['phone']
).get('phone') or False
@api.depends('partner_id')
def _compute_mobile(self):
for registration in self:
if not registration.mobile and registration.partner_id:
registration.mobile = registration._synchronize_partner_values(
registration.partner_id,
fnames=['mobile']
).get('mobile') 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(self.phone, country)
@api.onchange('mobile', 'event_id', 'partner_id')
def _onchange_mobile_validation(self):
if self.mobile:
country = self.partner_id.country_id or self.event_id.country_id or self.env.company.country_id
self.mobile = self._phone_format(self.mobile, country)
# ------------------------------------------------------------
# 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') and not values.get('mobile'):
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
for fname in {'mobile', 'phone'}:
if values.get(fname):
values[fname] = self._phone_format(values[fname], related_country)
registrations = super(EventRegistration, self).create(vals_list)
# auto_confirm if possible; if not automatically confirmed, call mail schedulers in case
# some were created already open
if registrations._check_auto_confirmation():
registrations.sudo().action_confirm()
elif 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)
# As these Event(Ticket) methods are model constraints, it is not necessary to call them
# explicitly when creating new registrations. However, it is necessary to trigger them here
# as changes in registration states cannot be used as constraints triggers.
if confirming:
to_confirm.event_id._check_seats_availability()
to_confirm.event_ticket_id._check_seats_availability()
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
to_confirm._update_mail_schedulers()
return ret
def name_get(self):
""" Custom name_get implementation to better differentiate registrations
linked to a given partner but with different name (one partner buying
several registrations)
* name, partner_id has no name -> take name
* partner_id has name, name void or same -> take partner name
* both have name: partner + name
"""
ret_list = []
for registration in self:
if registration.partner_id.name:
if registration.name and registration.name != registration.partner_id.name:
name = '%s, %s' % (registration.partner_id.name, registration.name)
else:
name = registration.partner_id.name
else:
name = registration.name
ret_list.append((registration.id, name))
return ret_list
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()
def _check_auto_confirmation(self):
""" Checks that all registrations are for `auto-confirm` events. """
return all(event.auto_confirm for event in self.event_id)
def _phone_format(self, number, country):
""" Call phone_validation formatting tool function. Returns original
number in case formatting cannot be done (no country, wrong info, ...) """
if not number or not country:
return number
new_number = phone_format(
number,
country.code,
country.phone_code,
force_format='E164',
raise_exception=False,
)
return new_number if new_number else number
# ------------------------------------------------------------
# ACTIONS / BUSINESS
# ------------------------------------------------------------
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_id=self.id,
default_use_template=bool(template),
default_template_id=template and template.id,
default_composition_mode='comment',
default_email_layout_xmlid="mail.mail_notification_light",
name_with_seats_availability=False,
)
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_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_tz = pytz.utc.localize(fields.Datetime.now()).astimezone(pytz.timezone(self.event_id.date_tz))
event_date_tz = pytz.utc.localize(self.event_begin_date).astimezone(pytz.timezone(self.event_id.date_tz))
diff = (event_date_tz.date() - today_tz.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_tz.month == (today_tz + 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,
}

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.')

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.')

View file

@ -0,0 +1,200 @@
# -*- 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'
# 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 ['name', 'description', 'seats_max']
class EventTicket(models.Model):
""" Ticket model allowing to have differnt kind of registrations for a given
event. Ticket are based on ticket type as they share some common fields
and behavior. Those models come from <= v13 Odoo event.event.ticket that
modeled both concept: tickets for event templates, and tickets for events. """
_name = 'event.event.ticket'
_inherit = 'event.type.ticket'
_description = 'Event Ticket'
@api.model
def default_get(self, fields):
res = super(EventTicket, self).default_get(fields)
if 'name' in fields and (not res.get('name') or res['name'] == _('Registration')) and self.env.context.get('default_event_name'):
res['name'] = _('Registration for %s', self.env.context['default_event_name'])
return res
# description
event_type_id = fields.Many2one(ondelete='set null', required=False)
event_id = fields.Many2one(
'event.event', string="Event",
ondelete='cascade', required=True)
company_id = fields.Many2one('res.company', related='event_id.company_id')
# sale
start_sale_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_unconfirmed = fields.Integer(string='Unconfirmed Seats', compute='_compute_seats', store=False)
seats_used = fields.Integer(string='Used 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.')
@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 reserved, available, reserved but unconfirmed and used seats. """
# initialize fields to 0 + compute seats availability
for ticket in self:
ticket.seats_unconfirmed = ticket.seats_reserved = ticket.seats_used = ticket.seats_available = 0
# aggregate registrations by ticket and by state
results = {}
if self.ids:
state_field = {
'draft': 'seats_unconfirmed',
'open': 'seats_reserved',
'done': 'seats_used',
}
query = """ SELECT event_ticket_id, state, count(event_id)
FROM event_registration
WHERE event_ticket_id IN %s AND state IN ('draft', 'open', 'done') 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)
@api.depends('seats_limited', 'seats_available', 'event_id.event_registrations_sold_out')
def _compute_is_sold_out(self):
for ticket in self:
ticket.is_sold_out = (
(ticket.seats_limited and not ticket.seats_available)
or ticket.event_id.event_registrations_sold_out
)
@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))
def name_get(self):
"""Adds ticket seats availability if requested by context."""
if not self.env.context.get('name_with_seats_availability'):
return super().name_get()
res = []
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),
)
res.append((ticket.id, name))
return res
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'))))

View file

@ -0,0 +1,29 @@
# -*- 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, args=None, operator='ilike', limit=100, name_get_uid=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 `args` in the `_name_search`
method to filtrate the mail templates.
"""
if self.env.context.get('filter_template_on_event'):
args = expression.AND([[('model', '=', 'event.registration')], args])
return super(MailTemplate, self)._name_search(name, args, operator, limit, name_get_uid)
def unlink(self):
res = super().unlink()
domain = ('template_ref', 'in', [f"{template._name},{template.id}" for template in self])
self.env['event.mail'].sudo().search([domain]).unlink()
self.env['event.type.mail'].sudo().search([domain]).unlink()
return res

View file

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
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")
module_website_event_questions = fields.Boolean("Registration Survey")
module_event_barcode = fields.Boolean("Barcode")
module_website_event_sale = fields.Boolean("Online Ticketing")
module_event_booth = fields.Boolean("Booth Management")
@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

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