Initial commit: OCA Mrp packages (117 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:05 +02:00
commit 277e84fd7a
4403 changed files with 395154 additions and 0 deletions

View file

@ -0,0 +1,111 @@
==============
Event Sessions
==============
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:5ae24a9cf7c47a24afbf736b5ff4ad048079df0156a0482bc0c18e63d3210404
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fevent-lightgray.png?logo=github
:target: https://github.com/OCA/event/tree/16.0/event_session
:alt: OCA/event
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/event-16-0/event-16-0-event_session
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/event&target_branch=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This module allows to create sessions associated with events.
**Table of contents**
.. contents::
:local:
Usage
=====
You can either:
* Go to Events > Sessions and create some sessions associated with an event.
* Go to an event and use the sessions wizard to create all your event sessions
according to a given schedule.
Known issues / Roadmap
======================
* In the sessions form view, for now is possible to modify multiple sessions
at the same time. This can be a bit weird for the user without having the
"SAVE" button, as it's difficult to know when the record is going to be saved
exactly. This feature is inspired by a core feature from recurring Calendar Events.
And it seems that Odoo hasn't handle this dissaperance of the "SAVE" button .
With this in mind, where propossed thre solutions:
A. Keep it as-is
B. Deprecate/ remove this feature
C. Find a better way, in terms of UX
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/event/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/event/issues/new?body=module:%20event_session%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Tecnativa
Contributors
~~~~~~~~~~~~
* `Tecnativa <https://www.tecnativa.com>`__:
* Sergio Teruel
* David Vidal
* Carlos Roca
* Stefan Ungureanu
* Nikos Tsirintanis <ntsirintanis@therp.nl>
* David Alonso <david.alonso@solvos.es>
* `Moka Tourisme <https://www.mokatourisme.fr>`_
* Iván Todorovich <ivan.todorovich@gmail.com>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/event <https://github.com/OCA/event/tree/16.0/event_session>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View file

@ -0,0 +1,3 @@
from . import controllers
from . import models
from . import wizards

View file

@ -0,0 +1,26 @@
# Copyright 2017-19 David Vidal<david.vidal@tecnativa.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Event Sessions",
"summary": "Sessions in events",
"version": "16.0.1.4.1",
"author": "Tecnativa, Odoo Community Association (OCA)",
"license": "AGPL-3",
"website": "https://github.com/OCA/event",
"category": "Marketing",
"depends": ["event"],
"data": [
"data/event_session_timeslot.xml",
"data/mail_template.xml",
"security/ir.model.access.csv",
"security/security.xml",
"views/event_event.xml",
"views/event_registration.xml",
"views/event_session.xml",
"views/event_type.xml",
"reports/event_report_templates.xml",
"wizards/wizard_event_session.xml",
],
"demo": ["demo/event_session.xml"],
}

View file

@ -0,0 +1 @@
from . import main

View file

@ -0,0 +1,30 @@
# Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr).
# @author Iván Todorovich <ivan.todorovich@gmail.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from werkzeug.exceptions import NotFound
from odoo.http import Controller, content_disposition, request, route
class EventSessionController(Controller):
@route(
"""/event/session/<model("event.session"):event_session>/ics""",
type="http",
auth="public",
)
def event_session_ics_file(self, event_session, **kwargs):
"""Similar to core :meth:`~event_ics_file` for event.event"""
files = event_session._get_ics_file()
if event_session.id not in files: # pragma: no cover
return NotFound()
content = files[event_session.id]
disposition = content_disposition(f"{event_session.name}.ics")
return request.make_response(
content,
[
("Content-Type", "application/octet-stream"),
("Content-Length", len(content)),
("Content-Disposition", disposition),
],
)

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr)
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo noupdate="1">
<record id="timeslot_10_00" model="event.session.timeslot">
<field name="time">10</field>
</record>
<record id="timeslot_11_00" model="event.session.timeslot">
<field name="time">11</field>
</record>
<record id="timeslot_12_00" model="event.session.timeslot">
<field name="time">12</field>
</record>
<record id="timeslot_13_00" model="event.session.timeslot">
<field name="time">13</field>
</record>
<record id="timeslot_14_00" model="event.session.timeslot">
<field name="time">14</field>
</record>
<record id="timeslot_15_00" model="event.session.timeslot">
<field name="time">15</field>
</record>
<record id="timeslot_16_00" model="event.session.timeslot">
<field name="time">16</field>
</record>
<record id="timeslot_17_00" model="event.session.timeslot">
<field name="time">17</field>
</record>
<record id="timeslot_18_00" model="event.session.timeslot">
<field name="time">18</field>
</record>
<record id="timeslot_19_00" model="event.session.timeslot">
<field name="time">19</field>
</record>
<record id="timeslot_20_00" model="event.session.timeslot">
<field name="time">20</field>
</record>
<record id="timeslot_21_00" model="event.session.timeslot">
<field name="time">21</field>
</record>
<record id="timeslot_22_00" model="event.session.timeslot">
<field name="time">22</field>
</record>
</odoo>

View file

@ -0,0 +1,875 @@
<?xml version="1.0" ?>
<!--
Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr)
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo noupdate="1">
<!--
What is this? Why not using core templates?
Core event templates will display information related to the event itself,
and would completely ignore the session.
This information is very important because it often contains the date and
place the participant should attend.
-->
<record id="event_session_registration_mail_template_badge" model="mail.template">
<field name="name">Event Session: Registration Badge</field>
<field name="model_id" ref="event.model_event_registration" />
<field name="subject">Your badge for {{ object.session_id.name }}</field>
<field
name="email_from"
>{{ (object.session_id.organizer_id.email_formatted or object.session_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="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.session_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.session_id.user_id.signature">
<br />
<t t-out="object.session_id.user_id.signature or ''">--<br />Mitchell Admin</t>
</t>
</div></field>
<field
name="report_template"
ref="event.action_report_event_registration_foldable_badge"
/>
<field
name="report_name"
>Foldable Badge - {{ (object.session_id.name or 'Event').replace('/','_') }}</field>
<field name="lang">{{ object.partner_id.lang }}</field>
<field name="auto_delete" eval="True" />
</record>
<record id="event_session_subscription" model="mail.template">
<field name="name">Event Session: Registration</field>
<field name="model_id" ref="event.model_event_registration" />
<field name="subject">Your registration at {{ object.session_id.name }}</field>
<field
name="email_from"
>{{ (object.session_id.organizer_id.email_formatted or object.session_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="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.session_id.date_begin, tz='UTC', dt_format=&quot;yyyyMMdd'T'HHmmss'Z'&quot;)"
/>
<t
t-set="date_end"
t-value="format_datetime(object.session_id.date_end, tz='UTC', dt_format=&quot;yyyyMMdd'T'HHmmss'Z'&quot;)"
/>
<t
t-set="is_online"
t-value="'is_published' in object.session_id and object.session_id.is_published"
/>
<t t-set="event_organizer" t-value="object.session_id.organizer_id" />
<t t-set="event_address" t-value="object.session_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.session_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.session_id.website_url"
style="color:#875A7B;text-decoration:none;"
t-out="object.session_id.name or ''"
>OpenWood Collection Online Reveal</a>
</t>
<t t-else="">
<strong
t-out="object.session_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.session_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/session/{{ slug(object.session_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.session_id.name }}&amp;in_loc={{ location }}&amp;st={{ format_datetime(object.session_id.date_begin, tz='UTC', dt_format='yyyyMMdd\'T\'HHmmss') }}&amp;et={{ format_datetime(object.session_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.session_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.session_id.date_begin_located or ''"
>May 4, 2021, 7:00:00 AM</t></div>
<div><strong>To</strong> <t
t-out="object.session_id.date_end_located or ''"
>May 6, 2021, 5:00:00 PM</t></div>
<div style="font-size:12px;color:#9e9e9e"><i><strong
>TZ</strong> <t
t-out="object.session_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.session_id.address_id.name">
<div
t-out="object.session_id.address_id.name or ''"
>Teksa SpA</div>
</t>
<t t-if="object.session_id.address_id.street">
<div
t-out="object.session_id.address_id.street or ''"
>Puerto Madero 9710</div>
<t
t-set="location"
t-value="object.session_id.address_id.street"
/>
</t>
<t t-if="object.session_id.address_id.street2">
<div
t-out="object.session_id.address_id.street2 or ''"
>Of A15, Santiago (RM)</div>
<t
t-set="location"
t-value="'%s, %s' % (location, object.session_id.address_id.street2)"
/>
</t>
<div>
<t t-if="object.session_id.address_id.city">
<t
t-out="object.session_id.address_id.city or ''"
>Pudahuel</t>,
<t
t-set="location"
t-value="'%s, %s' % (location, object.session_id.address_id.city)"
/>
</t>
<t
t-if="object.session_id.address_id.state_id.name"
>
<t
t-out="object.session_id.address_id.state_id.name or ''"
>C1</t>,
<t
t-set="location"
t-value="'%s, %s' % (location, object.session_id.address_id.state_id.name)"
/>
</t>
<t t-if="object.session_id.address_id.zip">
<t
t-out="object.session_id.address_id.zip or ''"
>98450</t>
<t
t-set="location"
t-value="'%s, %s' % (location, object.session_id.address_id.zip)"
/>
</t>
</div>
<t
t-if="object.session_id.address_id.country_id.name"
>
<div
t-out="object.session_id.address_id.country_id.name or ''"
>Argentina</div>
<t
t-set="location"
t-value="'%s, %s' % (location, object.session_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">
<table style="width:100%;"><tr><td>
<div>
<a
t-attf-href="https://maps.google.com/maps?q={{ location }}"
target="new"
>
<img
t-attf-src="http://maps.googleapis.com/maps/api/staticmap?autoscale=1&amp;size=598x200&amp;maptype=roadmap&amp;format=png&amp;visual_refresh=true&amp;markers=size:mid%7Ccolor:0xa5117d%7Clabel:%7C{{ location }}"
style="vertical-align:bottom; width: 100%;"
alt="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="event.action_report_event_registration_full_page_ticket"
/>
<field
name="report_name"
>Full Page Ticket - {{ (object.session_id.name or 'Event').replace('/','') }}</field>
<field name="lang">{{ object.partner_id.lang }}</field>
</record>
<record id="event_session_reminder" model="mail.template">
<field name="name">Event Session: Reminder</field>
<field name="model_id" ref="event.model_event_registration" />
<field
name="subject"
>{{ object.session_id.name }}: {{ object.get_date_range_str() }}</field>
<field
name="email_from"
>{{ (object.session_id.organizer_id.email_formatted or object.session_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="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.session_id.date_begin, tz='UTC', dt_format=&quot;yyyyMMdd'T'HHmmss'Z'&quot;)"
/>
<t
t-set="date_end"
t-value="format_datetime(object.session_id.date_end, tz='UTC', dt_format=&quot;yyyyMMdd'T'HHmmss'Z'&quot;)"
/>
<t
t-set="is_online"
t-value="'is_published' in object.session_id and object.session_id.is_published"
/>
<t t-set="event_organizer" t-value="object.session_id.organizer_id" />
<t t-set="event_address" t-value="object.session_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.session_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.session_id.website_url"
style="color:#875A7B;text-decoration:none;"
t-out="object.session_id.name or ''"
>OpenWood Collection Online Reveal</a>
</t>
<t t-else="">
<strong
t-out="object.session_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.session_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/session/{{ slug(object.session_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.session_id.name }}&amp;in_loc={{ location }}&amp;st={{ format_datetime(object.session_id.date_begin, tz='UTC', dt_format='yyyyMMdd\'T\'HHmmss') }}&amp;et={{ format_datetime(object.session_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.session_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.session_id.date_begin_located or ''"
>May 4, 2021, 7:00:00 AM</t></div>
<div><strong>To</strong> <t
t-out="object.session_id.date_end_located or ''"
>May 6, 2021, 5:00:00 PM</t></div>
<div style="font-size:12px;color:#9e9e9e"><i><strong
>TZ</strong> <t
t-out="object.session_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.session_id.address_id.name">
<div
t-out="object.session_id.address_id.name or ''"
>Teksa SpA</div>
</t>
<t t-if="object.session_id.address_id.street">
<div
t-out="object.session_id.address_id.street or ''"
>Puerto Madero 9710</div>
<t
t-set="location"
t-value="object.session_id.address_id.street"
/>
</t>
<t t-if="object.session_id.address_id.street2">
<div
t-out="object.session_id.address_id.street2 or ''"
>Of A15, Santiago (RM)</div>
<t
t-set="location"
t-value="'%s, %s' % (location, object.session_id.address_id.street2)"
/>
</t>
<div>
<t t-if="object.session_id.address_id.city">
<t
t-out="object.session_id.address_id.city or ''"
>Pudahuel</t>,
<t
t-set="location"
t-value="'%s, %s' % (location, object.session_id.address_id.city)"
/>
</t>
<t
t-if="object.session_id.address_id.state_id.name"
>
<t
t-out="object.session_id.address_id.state_id.name or ''"
>C1</t>,
<t
t-set="location"
t-value="'%s, %s' % (location, object.session_id.address_id.state_id.name)"
/>
</t>
<t t-if="object.session_id.address_id.zip">
<t
t-out="object.session_id.address_id.zip or ''"
>98450</t>
<t
t-set="location"
t-value="'%s, %s' % (location, object.session_id.address_id.zip)"
/>
</t>
</div>
<t
t-if="object.session_id.address_id.country_id.name"
>
<div
t-out="object.session_id.address_id.country_id.name or ''"
>Argentina</div>
<t
t-set="location"
t-value="'%s, %s' % (location, object.session_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 ''"
/></li>
</t>
<t t-if="event_organizer.phone">
<li>Phone: <t
t-out="event_organizer.phone or ''"
/></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>
<a
t-attf-href="https://maps.google.com/maps?q={{ location }}"
target="new"
>
<img
t-attf-src="http://maps.googleapis.com/maps/api/staticmap?autoscale=1&amp;size=598x200&amp;maptype=roadmap&amp;format=png&amp;visual_refresh=true&amp;markers=size:mid%7Ccolor:0xa5117d%7Clabel:%7C{{ location }}"
style="vertical-align:bottom; width: 100%;"
alt="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.session_id and object.session_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>
</odoo>

View file

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr)
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="res_partner_location_theater" model="res.partner">
<field name="name">Grand Theater</field>
<field name="is_company">1</field>
<field name="street">Cinema St. 100</field>
<field name="city">Los Angeles</field>
<field name="state_id" ref="base.state_us_5" />
<field name="country_id" ref="base.us" />
<field name="zip">90015</field>
</record>
<record id="event_type_theater" model="event.type">
<field name="name">Theater</field>
<field name="auto_confirm" eval="False" />
<field name="use_sessions" eval="True" />
<field
name="event_type_mail_ids"
eval="
[
(0, 0, {
'notification_type': 'mail',
'interval_nbr': 0,
'interval_unit': 'now',
'interval_type': 'after_sub',
'template_ref': 'mail.template, %i' % ref('event_session_subscription'),
}),
(0, 0, {
'notification_type': 'mail',
'interval_nbr': 1,
'interval_unit': 'hours',
'interval_type': 'before_event',
'template_ref': 'mail.template, %i' % ref('event_session_reminder'),
}),
(0, 0, {
'notification_type': 'mail',
'interval_nbr': 3,
'interval_unit': 'days',
'interval_type': 'before_event',
'template_ref': 'mail.template, %i' % ref('event_session_reminder'),
})
]
"
/>
</record>
<record id="event_tag_movie" model="event.tag">
<field name="name">Movie</field>
<field name="sequence">12</field>
<field name="category_id" ref="event.event_tag_category_2" />
<field name="color">7</field>
</record>
<record id="event_event_007" model="event.event">
<field name="name">007: No Time to Die</field>
<field name="user_id" ref="base.user_demo" />
<field name="use_sessions" eval="True" />
<field name="seats_limited">True</field>
<field name="seats_max">50</field>
<field name="address_id" ref="res_partner_location_theater" />
<field name="date_tz">Europe/Brussels</field>
<field name="event_type_id" ref="event_type_theater" />
<field name="stage_id" ref="event.event_stage_booked" />
<field name="tag_ids" eval="[(4, ref('event_tag_movie'))]" />
</record>
<record id="event_session_007_1_16_00" model="event.session">
<field name="event_id" ref="event_event_007" />
<field
name="date_begin"
eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d 16:00:00')"
/>
<field
name="date_end"
eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d 18:00:00')"
/>
</record>
<record id="event_session_007_1_18_00" model="event.session">
<field name="event_id" ref="event_event_007" />
<field
name="date_begin"
eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d 19:00:00')"
/>
<field
name="date_end"
eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d 21:00:00')"
/>
</record>
<record id="event_session_007_1_20_00" model="event.session">
<field name="event_id" ref="event_event_007" />
<field
name="date_begin"
eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d 20:00:00')"
/>
<field
name="date_end"
eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d 22:00:00')"
/>
</record>
<record id="event_session_007_2_16_00" model="event.session">
<field name="event_id" ref="event_event_007" />
<field
name="date_begin"
eval="(DateTime.now() + timedelta(days=2)).strftime('%Y-%m-%d 16:00:00')"
/>
<field
name="date_end"
eval="(DateTime.now() + timedelta(days=2)).strftime('%Y-%m-%d 18:00:00')"
/>
</record>
<record id="event_session_007_2_18_00" model="event.session">
<field name="event_id" ref="event_event_007" />
<field
name="date_begin"
eval="(DateTime.now() + timedelta(days=2)).strftime('%Y-%m-%d 19:00:00')"
/>
<field
name="date_end"
eval="(DateTime.now() + timedelta(days=2)).strftime('%Y-%m-%d 21:00:00')"
/>
</record>
<record id="event_session_007_2_20_00" model="event.session">
<field name="event_id" ref="event_event_007" />
<field
name="date_begin"
eval="(DateTime.now() + timedelta(days=2)).strftime('%Y-%m-%d 20:00:00')"
/>
<field
name="date_end"
eval="(DateTime.now() + timedelta(days=2)).strftime('%Y-%m-%d 22:00:00')"
/>
</record>
<record id="event_session_007_3_20_00" model="event.session">
<field name="event_id" ref="event_event_007" />
<field
name="date_begin"
eval="(DateTime.now() + timedelta(days=3)).strftime('%Y-%m-%d 20:00:00')"
/>
<field
name="date_end"
eval="(DateTime.now() + timedelta(days=3)).strftime('%Y-%m-%d 22:00:00')"
/>
</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

View file

@ -0,0 +1,8 @@
from . import event_event
from . import event_mail_registration
from . import event_mail_session
from . import event_mail
from . import event_registration
from . import event_session
from . import event_session_timeslot
from . import event_type

View file

@ -0,0 +1,140 @@
# Copyright 2017 David Vidal<david.vidal@tecnativa.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class EventEvent(models.Model):
_inherit = "event.event"
use_sessions = fields.Boolean(
string="Event Sessions",
help="Manage multiple sessions per event",
compute="_compute_use_sessions",
store=True,
readonly=False,
)
session_ids = fields.One2many(
comodel_name="event.session",
inverse_name="event_id",
string="Sessions",
)
session_count = fields.Integer(
string="Sessions Count",
compute="_compute_session_count",
)
date_begin = fields.Datetime(
compute="_compute_date_begin",
store=True,
readonly=False,
)
date_end = fields.Datetime(
compute="_compute_date_end",
store=True,
readonly=False,
)
@api.depends("event_type_id")
def _compute_use_sessions(self):
for rec in self:
rec.use_sessions = rec.event_type_id.use_sessions
@api.onchange("use_sessions")
def _onchange_use_sessions(self):
"""
Automatically fill date_begin and date_end if it's a use_session event.
These fields are required but computed from sessions anyway.
"""
if self.use_sessions and not self.date_begin:
self.date_begin = fields.Datetime.now()
if self.use_sessions and not self.date_end:
self.date_end = fields.Datetime.now()
@api.depends("session_ids")
def _compute_session_count(self):
groups = self.env["event.session"].read_group(
domain=[("event_id", "in", self.ids)],
fields=["event_id"],
groupby=["event_id"],
)
result = {g["event_id"][0]: g["event_id_count"] for g in groups}
for rec in self:
rec.session_count = result.get(rec.id, 0)
@api.depends("use_sessions", "session_ids.date_begin")
def _compute_date_begin(self):
session_records = self.filtered("use_sessions")
regular_records = self - session_records
# This is a core field. Play nice with other modules.
# It is also why we compute date_begin and date_end separately.
if hasattr(super(), "_compute_date_begin"): # pragma: no cover
super(EventEvent, regular_records)._compute_date_begin()
if not session_records: # pragma: no cover
return
groups = self.env["event.session"].read_group(
domain=[("event_id", "in", session_records.ids)],
fields=["event_id", "date_begin:min"],
groupby=["event_id"],
)
data = {d["event_id"][0]: d["date_begin"] for d in groups}
for rec in session_records:
if data.get(rec.id):
rec.date_begin = data.get(rec.id)
@api.depends("use_sessions", "session_ids.date_end")
def _compute_date_end(self):
session_records = self.filtered("use_sessions")
regular_records = self - session_records
# This is a core field. Play nice with other modules.
# It is also why we compute date_begin and date_end separately.
if hasattr(super(), "_compute_date_end"): # pragma: no cover
super(EventEvent, regular_records)._compute_date_end()
if not session_records: # pragma: no cover
return
groups = self.env["event.session"].read_group(
domain=[("event_id", "in", session_records.ids)],
fields=["event_id", "date_end:max"],
groupby=["event_id"],
)
data = {d["event_id"][0]: d["date_end"] for d in groups}
for rec in session_records:
if data.get(rec.id):
rec.date_end = data.get(rec.id)
def _check_seats_availability(self, minimal_availability=0): # pragma: no cover
# OVERRIDE to ignore this constraint for event with sessions
# Seat availability is checked on each session, not here.
session_records = self.filtered("use_sessions")
regular_records = self - session_records
return super(EventEvent, regular_records)._check_seats_availability(
minimal_availability=minimal_availability
)
@api.model_create_multi
def create(self, vals_list):
# OVERRIDE to automatically fill date_begin and date_end if they're
# missing and it's a use_session event.
# These fields are required but computed from sessions anyway.
for vals in vals_list:
if vals.get("use_sessions"):
vals["date_begin"] = fields.Datetime.now()
vals["date_end"] = fields.Datetime.now()
return super().create(vals_list)
def write(self, vals):
# OVERRIDE to prevent the switch of use_sessions if the event has registrations
# and to automatically subscribe the organizer to sessions, if it changes.
if "use_sessions" in vals:
if any(
rec.use_sessions != vals["use_sessions"] and rec.registration_ids
for rec in self
):
raise ValidationError(
_("You can't enable/disable sessions on events with registrations.")
)
if not vals["use_sessions"]:
self.with_context(active_test=False).session_ids.unlink()
if vals.get("organizer_id"):
self.session_ids.message_subscribe([vals["organizer_id"]])
return super().write(vals)

View file

@ -0,0 +1,59 @@
# Copyright 2017 David Vidal<david.vidal@tecnativa.com>
# Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import logging
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
class EventMail(models.Model):
_inherit = "event.mail"
use_sessions = fields.Boolean(
related="event_id.use_sessions",
)
session_scheduler_ids = fields.One2many(
comodel_name="event.mail.session",
inverse_name="scheduler_id",
string="Session Mails",
)
@api.depends("event_id.use_sessions")
def _compute_scheduled_date(self):
# OVERRIDE to handle event session mail schedulers.
# We set scheduled_date to False because it doesn't make sense for sessions,
# as we use them only as "templates" to be copied/synced to the sessions as
# `event.mail.session` records. Their scheduled_dates are then computed from
# the dates of the related session.
# By doing it, we get the additional benefit of having them automatically
# ignored by the scheduled_date domain leaf of the core's mail scheduler cron.
session_records = self.filtered("use_sessions")
session_records.scheduled_date = False
regular_records = self - session_records
return super(EventMail, regular_records)._compute_scheduled_date()
@api.model
def schedule_communications(self, autocommit=False):
# OVERRIDE to also process session mail schedulers
res = super().schedule_communications(autocommit=autocommit)
self.env["event.mail.session"].schedule_communications(autocommit=autocommit)
return res
def execute(self): # pragma: no cover
# OVERRIDE. Just in case, prevent execution of schedulers linked to event.event
# that are using sessions. They manage that through event.mail.session.
# This should never happen because they always have scheduled_date = False.
session_records = self.filtered("use_sessions")
regular_records = self - session_records
if session_records: # pragma: no cover
_logger.error("Trying to execute event.mail linked to a session event.")
return super(EventMail, regular_records).execute()
def _prepare_session_mail_scheduler_vals(self, session):
return {
"scheduler_id": self.id,
"session_id": session.id,
}

View file

@ -0,0 +1,35 @@
# Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr).
# @author Iván Todorovich <ivan.todorovich@gmail.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
from odoo.addons.event.models.event_mail import _INTERVALS
class EventMailRegistration(models.Model):
_inherit = "event.mail.registration"
session_scheduler_id = fields.Many2one(
comodel_name="event.mail.session",
string="Session Mail",
ondelete="cascade",
)
@api.depends(
"session_scheduler_id.interval_unit",
"session_scheduler_id.interval_type",
)
def _compute_scheduled_date(self):
# OVERRIDE to handle session mail registrations
session_records = self.filtered("session_scheduler_id")
regular_records = self - session_records
for rec in session_records:
if rec.registration_id:
date_open = rec.registration_id.create_date or fields.Datetime.now()
scheduler = rec.session_scheduler_id
delta = _INTERVALS[scheduler.interval_unit](scheduler.interval_nbr)
rec.scheduled_date = date_open + delta
else: # pragma: no cover
rec.scheduled_date = False
return super(EventMailRegistration, regular_records)._compute_scheduled_date()

View file

@ -0,0 +1,164 @@
# Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr).
# @author Iván Todorovich <ivan.todorovich@gmail.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import logging
import threading
from odoo import api, fields, models
from odoo.addons.event.models.event_mail import _INTERVALS
_logger = logging.getLogger(__name__)
class EventMailSession(models.Model):
_name = "event.mail.session"
_inherits = {"event.mail": "scheduler_id"}
_description = "Event Session Automated Mailing"
scheduler_id = fields.Many2one(
comodel_name="event.mail",
string="Event Mail Scheduler",
ondelete="cascade",
auto_join=True,
required=True,
)
session_id = fields.Many2one(
comodel_name="event.session",
string="Session",
ondelete="cascade",
required=True,
)
mail_registration_ids = fields.One2many(
comodel_name="event.mail.registration",
inverse_name="session_scheduler_id",
)
scheduled_date = fields.Datetime(
compute="_compute_scheduled_date",
store=True,
)
mail_done = fields.Boolean("Sent", copy=False, readonly=True)
mail_count_done = fields.Integer("# Sent", copy=False, readonly=True)
@api.depends(
"session_id",
"session_id.date_begin",
"session_id.date_end",
"scheduler_id",
"interval_type",
"interval_unit",
"interval_nbr",
)
def _compute_scheduled_date(self):
"""
Similar to core's :meth:`event.models.event_mail._compute_scheduled_date`,
only here we take values from the `event.session` instead.
"""
for scheduler in self:
if scheduler.interval_type == "after_sub":
date, sign = scheduler.session_id.create_date, 1
elif scheduler.interval_type == "before_event":
date, sign = scheduler.session_id.date_begin, -1
else:
date, sign = scheduler.session_id.date_end, 1
delta = _INTERVALS[scheduler.interval_unit](sign * scheduler.interval_nbr)
scheduler.scheduled_date = date + delta if date else False
def _get_new_event_registrations(self):
registrations = self.session_id.registration_ids.filtered_domain(
[("state", "not in", ("cancel", "draft"))]
)
return registrations - self.mail_registration_ids.registration_id
def _prepare_mail_registration_vals(self, registration):
self.ensure_one()
return {
"registration_id": registration.id,
"scheduler_id": self.scheduler_id.id,
"session_scheduler_id": self.id,
}
def _create_missing_mail_registrations(self, registrations):
vals_list = []
for scheduler in self:
vals_list += [
scheduler._prepare_mail_registration_vals(registration)
for registration in registrations
]
if vals_list:
return self.env["event.mail.registration"].create(vals_list)
return self.env["event.mail.registration"]
def execute(self):
"""
Similar to core's :meth:`event.models.event_mail.execute`, only here we
take values from the `event.session` instead.
"""
for scheduler in self:
now = fields.Datetime.now()
if scheduler.interval_type == "after_sub":
new_registrations = self._get_new_event_registrations()
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.session_id.seats_reserved
+ scheduler.session_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 # pragma: no cover
# no template -> ill configured, skip and avoid crash
if not scheduler.template_ref: # pragma: no cover
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.session_id.date_end > now
):
scheduler.session_id.mail_attendees(scheduler.template_ref.id)
scheduler.update(
{
"mail_done": True,
"mail_count_done": scheduler.session_id.seats_reserved
+ scheduler.session_id.seats_used,
}
)
return True
@api.model
def schedule_communications(self, autocommit=False):
"""
Similar to core's :meth:`event.models.event_mail.schedule_communications`.
"""
schedulers = self.search(
[("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: # pragma: no cover
_logger.exception(e)
self.invalidate_cache()
self.env["event.mail"]._warn_template_error(scheduler, e)
else:
if autocommit and not getattr(
threading.currentThread(), "testing", False
): # pragma: no cover
self.env.cr.commit() # pylint: disable=invalid-commit
return True

View file

@ -0,0 +1,80 @@
# Copyright 2017 David Vidal<david.vidal@tecnativa.com>
# Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import SUPERUSER_ID, _, api, fields, models
from odoo.exceptions import ValidationError
class EventRegistration(models.Model):
_inherit = "event.registration"
use_sessions = fields.Boolean(
related="event_id.use_sessions",
)
session_id = fields.Many2one(
comodel_name="event.session",
string="Session",
ondelete="restrict",
)
# NOTE: Originally these fields are related to event_id.
# We make them computed to get the date from the session if needed.
event_begin_date = fields.Datetime(
related=None, compute="_compute_event_begin_date"
)
event_end_date = fields.Datetime(related=None, compute="_compute_event_end_date")
@api.depends("event_id.date_begin", "session_id.date_begin", "use_sessions")
def _compute_event_begin_date(self):
for rec in self:
if rec.use_sessions:
rec.event_begin_date = rec.session_id.date_begin
else:
rec.event_begin_date = rec.event_id.date_begin
@api.depends("event_id.date_end", "session_id.date_end", "use_sessions")
def _compute_event_end_date(self):
for rec in self:
if rec.use_sessions:
rec.event_end_date = rec.session_id.date_end
else:
rec.event_end_date = rec.event_id.date_end
@api.constrains("session_id")
def _check_seats_limit(self):
# Needed to check if the registration can be created
# when we try to save it.
session_records = self.filtered("session_id")
for rec in session_records:
session = rec.session_id
if (
session.seats_limited
and session.seats_max
and session.seats_available < (1 if rec.state == "draft" else 0)
):
raise ValidationError(_("No more seats available for this session."))
def _update_mail_schedulers(self):
# OVERRIDE to handle sessions' mail scheduler, not event ones.
session_records = self.filtered("session_id")
regular_records = self - session_records
res = super(EventRegistration, regular_records)._update_mail_schedulers()
# Similar to super, only we find the schedulers linked to the session
open_registrations = session_records.filtered(lambda r: r.state == "open")
if not open_registrations:
return res
onsubscribe_schedulers = (
self.env["event.mail.session"]
.sudo()
.search(
[
("session_id", "in", open_registrations.session_id.ids),
("interval_type", "=", "after_sub"),
]
)
)
if not onsubscribe_schedulers:
return res
onsubscribe_schedulers.mail_done = False
onsubscribe_schedulers.with_user(SUPERUSER_ID).execute()
return res

View file

@ -0,0 +1,550 @@
# Copyright 2017 David Vidal<david.vidal@tecnativa.com>
# Copyright 2017 Tecnativa - Pedro M. Baeza
# Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr).
# @author Iván Todorovich <ivan.todorovich@gmail.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from collections import defaultdict
import pytz
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools import format_datetime
from odoo.addons.event.models.event_event import vobject
class EventSession(models.Model):
_name = "event.session"
_inherits = {"event.event": "event_id"}
_inherit = ["mail.thread", "mail.activity.mixin"]
_description = "Event session"
_order = "date_begin"
active = fields.Boolean(
default=True,
)
event_id = fields.Many2one(
comodel_name="event.event",
string="Parent Event",
domain=[("use_sessions", "=", True)],
ondelete="cascade",
auto_join=True,
index=True,
required=True,
)
date_begin = fields.Datetime(
string="Start Date",
required=True,
)
date_end = fields.Datetime(
string="End Date",
required=True,
)
date_begin_located = fields.Char(
string="Start Date Located",
compute="_compute_date_begin_located",
)
date_end_located = fields.Char(
string="End Date Located",
compute="_compute_date_end_located",
)
is_ongoing = fields.Boolean(
compute="_compute_is_ongoing",
search="_search_is_ongoing",
)
is_finished = fields.Boolean(
compute="_compute_is_finished",
search="_search_is_finished",
)
is_one_day = fields.Boolean(
compute="_compute_is_one_day",
)
registration_ids = fields.One2many(
comodel_name="event.registration",
inverse_name="session_id",
string="Attendees",
)
seats_reserved = fields.Integer(
string="Reserved Seats",
compute="_compute_seats",
store=True,
)
seats_available = fields.Integer(
string="Available Seats",
compute="_compute_seats_available",
store=True,
)
seats_unconfirmed = fields.Integer(
string="Unconfirmed Seat Reservations",
compute="_compute_seats",
store=True,
)
seats_used = fields.Integer(
string="Number of Participants",
compute="_compute_seats",
store=True,
)
seats_expected = fields.Integer(
string="Number of Expected Attendees",
compute="_compute_seats_expected",
compute_sudo=True,
)
seats_available_unexpected = fields.Integer(
string="Number of seats non allocated by an attendee of any kind",
compute="_compute_seats_available_unexpected",
compute_sudo=True,
)
event_registrations_open = fields.Boolean(
string="Registration open",
compute="_compute_event_registrations_open",
compute_sudo=True,
)
event_registrations_sold_out = fields.Boolean(
string="Sold Out",
compute="_compute_event_registrations_sold_out",
compute_sudo=True,
)
event_mail_ids = fields.One2many(
comodel_name="event.mail.session",
inverse_name="session_id",
string="Mail Schedule",
compute="_compute_event_mail_ids",
store=True,
)
stage_id = fields.Many2one(
comodel_name="event.stage",
default=lambda self: self.env["event.event"]._get_default_stage_id(),
group_expand="_read_group_stage_ids",
tracking=True,
copy=False,
ondelete="restrict",
)
kanban_state = fields.Selection(
selection=lambda self: self.env["event.event"]
._fields["kanban_state"]
.selection,
default="normal",
copy=False,
)
kanban_state_label = fields.Char(
compute="_compute_kanban_state_label",
store=True,
tracking=True,
)
session_update = fields.Selection(
[
("this", "This session"),
("subsequent", "This and following event sessions"),
("all", "All event sessions"),
],
help="Choose what to do with other event sessions",
default="this",
store=False,
)
session_update_message = fields.Text(
compute="_compute_session_update_message",
)
def onchange(self, values, field_name, field_onchange):
# OVERRIDE to workaround this issue: https://github.com/odoo/odoo/pull/91373
# This can/should be removed if a FIX is merged on odoo core
first_call = not field_name
res = super().onchange(values, field_name, field_onchange)
if (
first_call
and "default_event_id" in self.env.context
and "event_id" in res["value"]
and not res["value"]["event_id"]
):
res["value"]["event_id"] = (
self.env["event.event"]
.browse(self.env.context["default_event_id"])
.name_get()[0]
)
return res
@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("date_begin_located", "date_tz")
def _compute_display_name(self):
with_event_name = self.env.context.get("with_event_name", True)
for rec in self:
name = f"{rec.event_id.name}, " if with_event_name else ""
name += rec.date_begin_located
if rec.date_tz != self.env.user.tz:
name += f" ({rec.date_tz})"
rec.display_name = name
def name_get(self):
return [(rec.id, rec.display_name) for rec in self]
@api.model
def _map_registration_state_to_seats_fields(self):
return {
"draft": "seats_unconfirmed",
"open": "seats_reserved",
"done": "seats_used",
}
@api.depends("seats_max", "registration_ids.state")
def _compute_seats(self):
"""Determine reserved, available, reserved but unconfirmed and used seats."""
# Aggregate registrations by session and by state
state_field = self._map_registration_state_to_seats_fields()
results = defaultdict(lambda: defaultdict(lambda: 0))
if self.ids:
query = """
SELECT session_id, state, count(session_id)
FROM event_registration
WHERE session_id IN %s
AND state IN %s
GROUP BY session_id, state
"""
self.env["event.registration"].flush_model(
["session_id", "state", "active"]
)
self.env.cr.execute(query, (tuple(self.ids), tuple(state_field.keys())))
for session_id, state, num in self.env.cr.fetchall():
results[session_id][state_field[state]] = num
# Compute seats
for rec in self:
rec.update(
{
fname: results[rec._origin.id or rec.id][fname]
for fname in state_field.values()
}
)
@api.depends("seats_unconfirmed", "seats_reserved", "seats_used", "seats_max")
def _compute_seats_available(self):
for rec in self:
rec.seats_available = (
rec.seats_max - (rec.seats_reserved + rec.seats_used)
if rec.seats_max > 0
else 0
)
@api.depends("seats_unconfirmed", "seats_reserved", "seats_used")
def _compute_seats_expected(self):
for rec in self:
rec.seats_expected = (
rec.seats_unconfirmed + rec.seats_reserved + rec.seats_used
)
@api.depends("seats_max", "seats_expected")
def _compute_seats_available_unexpected(self):
"""How many non allocated free seats we've got?"""
for rec in self:
rec.seats_available_unexpected = rec.seats_max - rec.seats_expected
@api.depends("date_tz", "date_begin")
def _compute_date_begin_located(self):
for rec in self:
if rec.date_begin:
rec.date_begin_located = format_datetime(
self.env,
rec.date_begin,
tz=rec.date_tz,
dt_format="medium",
)
else: # pragma: no cover
rec.date_begin_located = False
@api.depends("date_tz", "date_end")
def _compute_date_end_located(self):
for rec in self:
if rec.date_end:
rec.date_end_located = format_datetime(
self.env,
rec.date_end,
tz=rec.date_tz,
dt_format="medium",
)
else: # pragma: no cover
rec.date_end_located = False
def _set_tz_context(self):
"""Similar to core's :meth:`event_event._set_tz_context`"""
return self.with_context(**self.event_id._set_tz_context().env.context)
@api.depends("date_begin", "date_end")
def _compute_is_ongoing(self):
"""Similar to core's :meth:`event_event._compute_is_ongoing`"""
now = fields.Datetime.now()
for rec in self:
rec.is_ongoing = rec.date_begin <= now < rec.date_end
def _search_is_ongoing(self, operator, value):
"""Similar to core's :meth:`event_event._search_is_ongoing`"""
if operator not in ["=", "!="]: # pragma: no cover
raise ValueError(_("This operator is not supported"))
if not isinstance(value, bool): # pragma: no cover
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_begin", "<=", now), ("date_end", ">", now)]
else:
domain = ["|", ("date_begin", ">", now), ("date_end", "<=", now)]
return domain
@api.depends("date_begin", "date_end", "date_tz")
def _compute_is_one_day(self):
"""Similar to core's :meth:`event_event._compute_is_one_day`"""
for rec in self:
rec = rec._set_tz_context()
begin_tz = fields.Datetime.context_timestamp(rec, rec.date_begin)
end_tz = fields.Datetime.context_timestamp(rec, rec.date_end)
rec.is_one_day = begin_tz.date() == end_tz.date()
@api.depends("date_end")
def _compute_is_finished(self):
"""Similar to core's :meth:`event_event._compute_is_finished`"""
now = fields.Datetime.now()
for rec in self:
rec.is_finished = rec.date_end and rec.date_end <= now
def _search_is_finished(self, operator, value):
"""Similar to core's :meth:`event_event._search_is_finished`"""
if operator not in ["=", "!="]: # pragma: no cover
raise ValueError(_("This operator is not supported"))
if not isinstance(value, bool): # pragma: no cover
raise ValueError(_("Value should be True or False (not %s)", value))
now = fields.Datetime.now()
if (operator == "=" and value) or (operator == "!=" and not value):
domain = [("date_end", "<=", now)]
else:
domain = [("date_end", ">", now)]
return domain
@api.depends(
"date_tz",
"date_end",
"event_registrations_started",
"seats_available",
"seats_limited",
"event_ticket_ids.sale_available",
)
def _compute_event_registrations_open(self):
"""Similar to core's :meth:`event_event._compute_event_registrations_open`"""
now = fields.Datetime.now()
for rec in self:
rec.event_registrations_open = (
rec.event_registrations_started
and (not rec.date_end or rec.date_end >= now)
and (not rec.seats_limited or not rec.seats_max or rec.seats_available)
and (
not rec.event_ticket_ids
or any(ticket.sale_available for ticket in rec.event_ticket_ids)
)
)
@api.depends(
"event_ticket_ids.seats_available",
"seats_limited",
"seats_available",
)
def _compute_event_registrations_sold_out(self):
"""Similar to core's :meth:`event_event._compute_event_registrations_sold_out`"""
for rec in self:
rec.event_registrations_sold_out = (
rec.seats_limited and rec.seats_max and not rec.seats_available
) or (
rec.event_ticket_ids
and all(ticket.is_sold_out for ticket in rec.event_ticket_ids)
)
@api.depends("event_id.event_mail_ids")
def _compute_event_mail_ids(self):
"""Compute event mail ids from its parent event
The email schedulers for sessions are used to track their independent states,
but the management is done directly from the parent event.event.
This method takes care of synchronizing the session's schedulers with those
of their parent events.
"""
for rec in self:
existing_schedulers = rec.event_mail_ids.scheduler_id
event_schedulers = rec.event_id.event_mail_ids
# Unlink the ones no-longer in sync
to_unlink = rec.event_mail_ids.filtered(
lambda r: r.scheduler_id not in event_schedulers
)
if to_unlink:
rec.event_mail_ids = [
fields.Command.unlink(scheduler.id) for scheduler in to_unlink
]
# Create missing ones
to_create = event_schedulers - existing_schedulers
if to_create:
rec.event_mail_ids = [
fields.Command.create(
scheduler._prepare_session_mail_scheduler_vals(rec)
)
for scheduler in to_create
]
# Force recomputation of scheduled date
rec.event_mail_ids._compute_scheduled_date()
@api.model
def _read_group_stage_ids(self, stages, domain, order): # pragma: no cover
return self.env["event.event"]._read_group_stage_ids(stages, domain, order)
@api.constrains("seats_max", "seats_available", "seats_limited")
def _check_seats_availability(self, minimal_availability=0):
sold_out_events = []
for session in self:
if (
session.seats_limited
and session.seats_max
and session.seats_available < minimal_availability
):
sold_out_events.append(
_(
'- "%(event_name)s": Missing %(nb_too_many)i seats.',
event_name=session.name,
nb_too_many=-session.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 rec in self:
if rec.date_end <= rec.date_begin:
raise ValidationError(
_("The closing date cannot be earlier than the beginning date.")
)
def mail_attendees(
self,
template_id,
force_send=False,
filter_func=lambda self: self.state != "cancel",
):
"""Mail session attendees
Similar to core's :meth:`event.models.event.mail_attendees`, but here we take
only the session's attendees into account.
"""
template = self.env["mail.template"].browse(template_id)
for rec in self:
for attendee in rec.registration_ids.filtered(filter_func):
template.send_mail(attendee.id, force_send=force_send)
def action_open_registrations(self):
"""Open session registrations"""
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id(
"event.act_event_registration_from_event"
)
action["domain"] = [("id", "in", self.registration_ids.ids)]
action["context"] = {
"default_event_id": self.event_id.id,
"default_session_id": self.id,
}
return action
def action_set_done(self):
"""Similar to core's :meth:`event_event.action_set_done`"""
first_ended_stage = self.env["event.stage"].search(
[("pipe_end", "=", True)], limit=1, order="sequence"
)
if first_ended_stage:
self.stage_id = first_ended_stage
def _get_ics_file(self):
"""Similar to core's :meth:`event_event._get_ics_file`"""
result = {}
if not vobject: # pragma: no cover
return result
for rec 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 = fields.Datetime.from_string(
rec.date_begin
).replace(tzinfo=pytz.timezone("UTC"))
cal_event.add("dtend").value = fields.Datetime.from_string(
rec.date_end
).replace(tzinfo=pytz.timezone("UTC"))
cal_event.add("summary").value = rec.name
if rec.address_id:
cal_event.add("location").value = rec.sudo().address_id.contact_address
result[rec.id] = cal.serialize().encode("utf-8")
return result
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
# Subscribe the organizer to sessions. Similar to core's behaviour for events.
for rec in records:
if rec.organizer_id:
rec.message_subscribe([rec.organizer_id.id])
return records
@api.model
def _session_update_fields(self):
"""List of fields that could be synced with session_update"""
return ["active"]
def _compute_session_update_message(self):
"""Human readable list of fields that could be synced with session_update"""
fnames = self._session_update_fields()
fdescs = map(lambda fname: self._fields[fname].string, fnames)
self.session_update_message = "\n".join(map(lambda s: f"* {s}", fdescs))
def _sync_session_update(self, vals):
"""Handles write on multiple sessions at once from the UX"""
update = vals.pop("session_update", "this")
if update not in ("subsequent", "all"):
return
if len(self) > 1:
raise ValidationError(
_("You cannot use session_update when writing on recordsets")
)
to_sync = self._session_update_fields()
to_sync_vals = {k: v for k, v in vals.items() if k in to_sync}
if not to_sync_vals:
return
domain = [("event_id", "=", self.event_id.id)]
if update == "subsequent":
domain.append(("date_begin", ">", self.date_begin))
records = self.search(domain)
records.write(to_sync_vals)
def write(self, vals):
# OVERRIDE to apply session_update mechanism
self._sync_session_update(vals)
return super().write(vals)
@api.autovacuum
def _gc_mark_events_done(self):
"""Move every ended sessions in the next 'ended stage'
Similar to core's :meth:`event_event._gc_mark_events_done`
"""
ended = self.search(
[
("date_end", "<", fields.Datetime.now()),
("stage_id.pipe_end", "=", False),
]
)
if ended:
ended.action_set_done()

View file

@ -0,0 +1,51 @@
# Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr).
# @author Iván Todorovich <ivan.todorovich@gmail.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import time
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools.misc import format_duration
def time_as_float_time(tm):
hours, minutes = tm.tm_hour, tm.tm_min
return hours + (minutes / 60)
class EventSessionTimeslot(models.Model):
_name = "event.session.timeslot"
_description = "Event Session Timeslot"
_order = "time"
_rec_name = "time"
_sql_constraints = [
("unique_time", "UNIQUE(time)", "The timeslot has to be unique"),
(
"valid_time",
"CHECK(time >= 0 AND time <= 24)",
"Time has to be between 0:00 and 23:59",
),
]
time = fields.Float(required=True)
def name_get(self):
return [(rec.id, format_duration(rec.time)) for rec in self]
@api.model
def name_create(self, name):
try:
tm = time.strptime(name.strip(), "%H:%M")
except ValueError as e:
raise ValidationError(
_("The timeslot has to be defined in HH:MM format")
) from e
vals = {"time": time_as_float_time(tm)}
return self.create(vals).name_get()[0]
def _prepare_session_extra_vals(self):
"""Hook to prepare values to apply on sessions created from this timeslot"""
self.ensure_one()
return {}

View file

@ -0,0 +1,14 @@
# Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr).
# @author Iván Todorovich <ivan.todorovich@gmail.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class EventType(models.Model):
_inherit = "event.type"
use_sessions = fields.Boolean(
string="Event Sessions",
help="Manage multiple sessions per event",
)

View file

@ -0,0 +1,13 @@
* `Tecnativa <https://www.tecnativa.com>`__:
* Sergio Teruel
* David Vidal
* Carlos Roca
* Stefan Ungureanu
* Nikos Tsirintanis <ntsirintanis@therp.nl>
* David Alonso <david.alonso@solvos.es>
* `Moka Tourisme <https://www.mokatourisme.fr>`_
* Iván Todorovich <ivan.todorovich@gmail.com>

View file

@ -0,0 +1 @@
This module allows to create sessions associated with events.

View file

@ -0,0 +1,10 @@
* In the sessions form view, for now is possible to modify multiple sessions
at the same time. This can be a bit weird for the user without having the
"SAVE" button, as it's difficult to know when the record is going to be saved
exactly. This feature is inspired by a core feature from recurring Calendar Events.
And it seems that Odoo hasn't handle this dissaperance of the "SAVE" button .
With this in mind, where propossed thre solutions:
A. Keep it as-is
B. Deprecate/ remove this feature
C. Find a better way, in terms of UX

View file

@ -0,0 +1,5 @@
You can either:
* Go to Events > Sessions and create some sessions associated with an event.
* Go to an event and use the sessions wizard to create all your event sessions
according to a given schedule.

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr)
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo>
<!--
What is this?
Core Odoo's event registration report templates are wrapped with a loop
that iterates all records. This loop sets the `event` variable that gets
passed onto the single-page report for rendering.
See `event.event_registration_report_template_foldable_badge`, for example.
We're leveraging that, and the fact that sessions inherit from events the
same way than product.product inherits from product.template, so all event
fields are accessible from the session, except for those that are specific
to the session itself (date_begin, etc..)
-->
<template
id="event_registration_report_template_foldable_badge"
inherit_id="event.event_registration_report_template_foldable_badge"
>
<xpath expr="//t[@t-foreach='docs']//t[@t-set='event']" position="after">
<t
t-if="attendee.session_id"
t-set="event"
t-value="attendee.session_id._set_tz_context()"
/>
</xpath>
</template>
<template
id="event_registration_report_template_full_page_ticket"
inherit_id="event.event_registration_report_template_full_page_ticket"
>
<xpath expr="//t[@t-foreach='docs']//t[@t-set='event']" position="after">
<t
t-if="attendee.session_id"
t-set="event"
t-value="attendee.session_id._set_tz_context()"
/>
</xpath>
</template>
</odoo>

View file

@ -0,0 +1,8 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_event_session_registration,event.session.registration,event_session.model_event_session,event.group_event_registration_desk,1,0,0,0
access_event_session_admin,event.session.admin,event_session.model_event_session,event.group_event_manager,1,1,1,1
access_event_mail_session_registration,event.mail.session.registration,model_event_mail_session,event.group_event_registration_desk,1,0,0,0
access_event_mail_session_user,event.mail.session.user,model_event_mail_session,event.group_event_user,1,1,1,1
access_event_session_timeslot_registration,event.session.timeslot,model_event_session_timeslot,event.group_event_registration_desk,1,0,0,0
access_event_session_timeslot_user,event.session.timeslot,model_event_session_timeslot,event.group_event_user,1,1,1,1
access_wizard_event_session,wizard.event.session,model_wizard_event_session,event.group_event_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_event_session_registration event.session.registration event_session.model_event_session event.group_event_registration_desk 1 0 0 0
3 access_event_session_admin event.session.admin event_session.model_event_session event.group_event_manager 1 1 1 1
4 access_event_mail_session_registration event.mail.session.registration model_event_mail_session event.group_event_registration_desk 1 0 0 0
5 access_event_mail_session_user event.mail.session.user model_event_mail_session event.group_event_user 1 1 1 1
6 access_event_session_timeslot_registration event.session.timeslot model_event_session_timeslot event.group_event_registration_desk 1 0 0 0
7 access_event_session_timeslot_user event.session.timeslot model_event_session_timeslot event.group_event_user 1 1 1 1
8 access_wizard_event_session wizard.event.session model_wizard_event_session event.group_event_user 1 1 1 1

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record model="ir.rule" id="event_session_company_rule">
<field name="name">Event Session: multi-company</field>
<field name="model_id" ref="model_event_session" />
<field name="domain_force">
['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]
</field>
</record>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -0,0 +1,469 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Event Sessions</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="event-sessions">
<h1 class="title">Event Sessions</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:5ae24a9cf7c47a24afbf736b5ff4ad048079df0156a0482bc0c18e63d3210404
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/event/tree/16.0/event_session"><img alt="OCA/event" src="https://img.shields.io/badge/github-OCA%2Fevent-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/event-16-0/event-16-0-event_session"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/event&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module allows to create sessions associated with events.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-2">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-6">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-7">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
<p>You can either:</p>
<ul class="simple">
<li>Go to Events &gt; Sessions and create some sessions associated with an event.</li>
<li>Go to an event and use the sessions wizard to create all your event sessions
according to a given schedule.</li>
</ul>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#toc-entry-2">Known issues / Roadmap</a></h1>
<ul>
<li><p class="first">In the sessions form view, for now is possible to modify multiple sessions
at the same time. This can be a bit weird for the user without having the
“SAVE” button, as its difficult to know when the record is going to be saved
exactly. This feature is inspired by a core feature from recurring Calendar Events.
And it seems that Odoo hasnt handle this dissaperance of the “SAVE” button .</p>
<dl class="docutils">
<dt>With this in mind, where propossed thre solutions:</dt>
<dd><ol class="first last upperalpha simple">
<li>Keep it as-is</li>
<li>Deprecate/ remove this feature</li>
<li>Find a better way, in terms of UX</li>
</ol>
</dd>
</dl>
</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-3">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/event/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/event/issues/new?body=module:%20event_session%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-4">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-5">Authors</a></h2>
<ul class="simple">
<li>Tecnativa</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-6">Contributors</a></h2>
<ul>
<li><p class="first"><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:</p>
<ul class="simple">
<li>Sergio Teruel</li>
<li>David Vidal</li>
<li>Carlos Roca</li>
<li>Stefan Ungureanu</li>
</ul>
</li>
<li><p class="first">Nikos Tsirintanis &lt;<a class="reference external" href="mailto:ntsirintanis&#64;therp.nl">ntsirintanis&#64;therp.nl</a>&gt;</p>
</li>
<li><p class="first">David Alonso &lt;<a class="reference external" href="mailto:david.alonso&#64;solvos.es">david.alonso&#64;solvos.es</a>&gt;</p>
</li>
<li><p class="first"><a class="reference external" href="https://www.mokatourisme.fr">Moka Tourisme</a></p>
<blockquote>
<ul class="simple">
<li>Iván Todorovich &lt;<a class="reference external" href="mailto:ivan.todorovich&#64;gmail.com">ivan.todorovich&#64;gmail.com</a>&gt;</li>
</ul>
</blockquote>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/event/tree/16.0/event_session">OCA/event</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,4 @@
from . import test_event_session
from . import test_event_session_ics
from . import test_event_session_mail
from . import test_event_session_wizard

View file

@ -0,0 +1,28 @@
# Copyright 2022 Moka Tourisme (https://www.mokatourisme.fr).
# @author Iván Todorovich <ivan.todorovich@gmail.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields
from odoo.tests import TransactionCase
class CommonEventSessionCase(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
cls.stage_new = cls.env.ref("event.event_stage_new")
cls.stage_done = cls.env.ref("event.event_stage_done")
def assertSessionDates(self, sessions, expected):
for session, date in zip(sessions, expected):
local_date = fields.Datetime.context_timestamp(
session._set_tz_context(), session.date_begin
)
local_date_str = fields.Datetime.to_string(local_date)
self.assertEqual(local_date_str, date)
def _wizard_generate_sessions(self, vals):
wizard = self.env["wizard.event.session"].create(vals)
sessions_domain = wizard.action_create_sessions()["domain"]
return self.env["event.session"].search(sessions_domain)

View file

@ -0,0 +1,415 @@
# Copyright 2017-19 Tecnativa - David Vidal
# Copyright 2017 Tecnativa - Pedro M. Baeza
# Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl-3.0).
from datetime import timedelta
from freezegun import freeze_time
from odoo import fields
from odoo.exceptions import ValidationError
from odoo.tests.common import Form
from odoo.tools import mute_logger
from .common import CommonEventSessionCase
class TestEventSession(CommonEventSessionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.event = cls.env["event.event"].create(
{
"name": "Test event",
"use_sessions": True,
"seats_limited": True,
"seats_max": 5,
}
)
cls.session = cls.env["event.session"].create(
{
"date_begin": "2017-05-26 20:00:00",
"date_end": "2017-05-26 21:00:00",
"event_id": cls.event.id,
}
)
def test_session_name_get(self):
# Case 1: Same tz than user
name = self.session.name_get()[0][1]
self.assertEqual(name, "Test event, May 26, 2017, 10:00:00 PM")
# Case 2: Different timezone
self.event.date_tz = "UTC"
name = self.session.name_get()[0][1]
self.assertEqual(name, "Test event, May 26, 2017, 8:00:00 PM (UTC)")
def test_check_dates(self):
with self.assertRaisesRegex(
ValidationError,
"The closing date cannot be earlier than the beginning date",
):
self.session.date_end = "2017-05-26 19:00:00"
def test_open_registrations(self):
domain = self.session.action_open_registrations()["domain"]
attendees = self.env["event.registration"].search(domain)
self.assertEqual(attendees, self.session.registration_ids)
def test_event_registration_event_begin_end_dates(self):
"""Test that the date_begin and date_end are set to the session's"""
# Case 1: Even with sessions
registration = self.env["event.registration"].create(
{
"name": "Test attendee",
"event_id": self.event.id,
"session_id": self.session.id,
}
)
self.assertEqual(registration.event_begin_date, self.session.date_begin)
self.assertEqual(registration.event_end_date, self.session.date_end)
# Case 2: Regular events
event = self.env.ref("event.event_0")
registration = self.env["event.registration"].create(
{
"name": "Test attendee",
"event_id": event.id,
}
)
self.assertEqual(registration.event_begin_date, event.date_begin)
self.assertEqual(registration.event_end_date, event.date_end)
def test_event_session_dates_located(self):
self.session.date_tz = "Europe/Paris"
self.assertEqual(self.session.date_begin_located, "May 26, 2017, 10:00:00 PM")
self.assertEqual(self.session.date_end_located, "May 26, 2017, 11:00:00 PM")
self.session.date_tz = "US/Pacific"
self.assertEqual(self.session.date_begin_located, "May 26, 2017, 1:00:00 PM")
self.assertEqual(self.session.date_end_located, "May 26, 2017, 2:00:00 PM")
def test_event_event_sync_from_event_type(self):
"""Test that the event.type fields are synced to the event.event"""
event_type = self.env["event.type"].create(
{
"name": "Test event type",
"use_sessions": True,
}
)
event = self.env["event.event"].create(
{
"name": "Test event",
"event_type_id": event_type.id,
"date_begin": self.event.date_begin,
"date_end": self.event.date_end,
}
)
self.assertEqual(event.use_sessions, True)
def test_event_session_form(self):
# Test workaround for this Odoo bug: https://github.com/odoo/odoo/pull/91373
session_form = Form(
self.env["event.session"].with_context(
default_event_id=self.event.id,
)
)
self.assertEqual(session_form.event_id, self.event)
self.assertEqual(session_form.name, self.event.name)
def test_event_event_use_sessions_switch(self):
# Case 1: We can't change an event to use_sessions after registrations
event = self.env["event.event"].create(
{
"name": "Test event",
"date_begin": self.event.date_begin,
"date_end": self.event.date_end,
}
)
self.env["event.registration"].create(
{
"event_id": event.id,
"name": "Test attendee",
}
)
msg = "You can't enable/disable sessions on events with registrations."
with self.assertRaisesRegex(ValidationError, msg):
event.use_sessions = True
# Case 2: We can change it back, if we have no registrations
# In fact event.sessions are removed when doing so
self.event.use_sessions = False
self.assertFalse(self.session.exists())
@mute_logger("odoo.models.unlink")
def test_event_event_sessions_count(self):
"""Test that the sessions count is computed correctly"""
self.assertEqual(self.event.session_count, 1)
self.session.unlink()
self.assertEqual(self.event.session_count, 0)
def test_event_message_subscribe_organizer(self):
"""Test that the organizer is subscribed to the sessions"""
organizer = self.env["res.partner"].create({"name": "Test organizer"})
# Case 1: Updating the event's organizer
self.event.organizer_id = organizer
self.assertIn(organizer, self.session.message_partner_ids)
# Case 2: Creating new sessions
session = self.env["event.session"].create(
{
"date_begin": "2017-05-27 20:00:00",
"date_end": "2017-05-27 21:00:00",
"event_id": self.event.id,
}
)
self.assertIn(organizer, session.message_partner_ids)
def test_session_seats(self):
"""Test event session seats constraints"""
self.assertEqual(self.event.seats_unconfirmed, self.session.seats_unconfirmed)
self.assertEqual(self.event.seats_used, self.session.seats_used)
vals = {
"name": "Test Attendee",
"event_id": self.event.id,
"session_id": self.session.id,
"state": "open",
}
# Fill the event session with attendees
self.env["event.registration"].create([vals] * self.session.seats_available)
# Try to create another one
with self.assertRaisesRegex(
ValidationError, r"There are not enough seats available for:"
), self.cr.savepoint():
self.env["event.registration"].create(vals)
# Attempt to create a draft registration on a full session
with self.assertRaisesRegex(
ValidationError, "No more seats available for this session."
), self.cr.savepoint():
self.env["event.registration"].create(dict(vals, state="draft"))
# Temporarily allow to create a draft registration and attempt to confirm it
self.event.seats_limited = False
registration = self.env["event.registration"].create(dict(vals, state="draft"))
self.event.seats_limited = True
with self.assertRaisesRegex(
ValidationError, r"There are not enough seats available for:"
), self.cr.savepoint():
registration.action_confirm()
registration.flush_recordset()
def test_event_seats(self):
"""Test that event.event seats constraints do not apply to sessions"""
# Case: Event has a limit of 5 seats, but it should apply per-session
self.event.seats_max = 5
self.event.seats_limited = True
# Fill session with attendees
vals = {
"name": "Test Attendee",
"event_id": self.event.id,
"session_id": self.session.id,
"state": "open",
}
self.assertFalse(self.session.event_registrations_sold_out)
self.env["event.registration"].create([vals] * 5)
self.assertTrue(self.session.event_registrations_sold_out)
# Create a second session and fill it too
session2 = self.session.copy({})
vals["session_id"] = session2.id
self.env["event.registration"].create([vals] * 5)
# Now attempt to move one registration to another session
with self.assertRaisesRegex(
ValidationError, r"There are not enough seats available for:"
), self.cr.savepoint():
self.session.registration_ids[0].session_id = session2
# Attempt to decrease the event seats limit below the existing registrations
with self.assertRaisesRegex(
ValidationError, r"There are not enough seats available for:"
), self.cr.savepoint():
self.event.seats_max = 2
self.event.flush_recordset()
def test_session_seats_count(self):
session_1, session_2 = self.env["event.session"].create(
[
{
"event_id": self.event.id,
"date_begin": fields.Datetime.now(),
"date_end": fields.Datetime.now() + timedelta(hours=1),
},
{
"event_id": self.event.id,
"date_begin": fields.Datetime.now() + timedelta(days=1),
"date_end": fields.Datetime.now() + timedelta(days=1, hours=1),
},
]
)
attendee_1, attendee_2, attendee_3 = self.env["event.registration"].create(
[
{
"name": "S1: First Atendee",
"event_id": self.event.id,
"session_id": session_1.id,
},
{
"name": "S1: Second Atendee",
"event_id": self.event.id,
"session_id": session_1.id,
},
{
"name": "S2: First Atendee",
"event_id": self.event.id,
"session_id": session_2.id,
},
]
)
self.assertEqual(session_1.seats_unconfirmed, 2)
self.assertEqual(session_1.seats_reserved, 0)
self.assertEqual(session_1.seats_expected, 2)
self.assertEqual(session_1.seats_available_unexpected, 3)
self.assertEqual(session_2.seats_unconfirmed, 1)
self.assertEqual(session_2.seats_reserved, 0)
self.assertEqual(session_2.seats_expected, 1)
self.assertEqual(session_2.seats_available_unexpected, 4)
self.assertEqual(self.event.seats_unconfirmed, 3)
self.assertEqual(self.event.seats_reserved, 0)
self.assertEqual(self.event.seats_expected, 3)
attendee_1.action_confirm()
self.assertEqual(session_1.seats_unconfirmed, 1)
self.assertEqual(session_1.seats_reserved, 1)
self.assertEqual(session_2.seats_unconfirmed, 1)
self.assertEqual(session_2.seats_reserved, 0)
self.assertEqual(self.event.seats_unconfirmed, 2)
self.assertEqual(self.event.seats_reserved, 1)
attendee_2.action_confirm()
self.assertEqual(session_1.seats_unconfirmed, 0)
self.assertEqual(session_1.seats_reserved, 2)
self.assertEqual(session_2.seats_unconfirmed, 1)
self.assertEqual(session_2.seats_reserved, 0)
self.assertEqual(self.event.seats_unconfirmed, 1)
self.assertEqual(self.event.seats_reserved, 2)
attendee_3.action_confirm()
self.assertEqual(session_1.seats_unconfirmed, 0)
self.assertEqual(session_1.seats_reserved, 2)
self.assertEqual(session_2.seats_unconfirmed, 0)
self.assertEqual(session_2.seats_reserved, 1)
self.assertEqual(self.event.seats_unconfirmed, 0)
self.assertEqual(self.event.seats_reserved, 3)
def test_event_session_is_ongoing(self):
# Case 1: Session is ongoing
session = self.env["event.session"].create(
{
"event_id": self.event.id,
"date_begin": fields.Datetime.now() - timedelta(hours=1),
"date_end": fields.Datetime.now() + timedelta(hours=1),
}
)
ongoing = self.env["event.session"].search([("is_ongoing", "=", True)])
not_ongoing = self.env["event.session"].search([("is_ongoing", "=", False)])
self.assertTrue(session.is_ongoing)
self.assertIn(session, ongoing)
self.assertNotIn(session, not_ongoing)
# Case 2: It isn't
session.write(
{
"date_begin": fields.Datetime.now() + timedelta(days=1),
"date_end": fields.Datetime.now() + timedelta(days=1, hours=1),
}
)
ongoing = self.env["event.session"].search([("is_ongoing", "=", True)])
not_ongoing = self.env["event.session"].search([("is_ongoing", "=", False)])
self.assertFalse(session.is_ongoing)
self.assertIn(session, not_ongoing)
self.assertNotIn(session, ongoing)
def test_event_session_is_finished(self):
# Case 1: Session is finished
session = self.env["event.session"].create(
{
"event_id": self.event.id,
"date_begin": fields.Datetime.now() - timedelta(hours=2),
"date_end": fields.Datetime.now() - timedelta(hours=1),
}
)
finished = self.env["event.session"].search([("is_finished", "=", True)])
not_finished = self.env["event.session"].search([("is_finished", "=", False)])
self.assertTrue(session.is_finished)
self.assertIn(session, finished)
self.assertNotIn(session, not_finished)
# Case 2: It isn't
session.write(
{
"date_begin": fields.Datetime.now() + timedelta(days=1),
"date_end": fields.Datetime.now() + timedelta(days=1, hours=1),
}
)
finished = self.env["event.session"].search([("is_finished", "=", True)])
not_finished = self.env["event.session"].search([("is_finished", "=", False)])
self.assertFalse(session.is_finished)
self.assertIn(session, not_finished)
self.assertNotIn(session, finished)
def test_event_session_registrations_open(self):
with freeze_time("2017-05-26 20:30:00"):
self.session.invalidate_recordset(["event_registrations_open"])
self.assertTrue(self.session.event_registrations_open)
with freeze_time("2017-05-30 20:00:00"):
self.session.invalidate_recordset(["event_registrations_open"])
self.assertFalse(self.session.event_registrations_open)
def test_event_session_action_set_done(self):
self.assertEqual(self.session.stage_id, self.stage_new)
self.session.action_set_done()
self.assertEqual(self.session.stage_id, self.stage_done)
def test_event_session_gc(self):
self.assertEqual(self.session.stage_id, self.stage_new)
with freeze_time("2017-05-26 20:30:00"):
self.env["event.session"]._gc_mark_events_done()
self.assertEqual(self.session.stage_id, self.stage_new, "Not done yet")
with freeze_time("2017-05-27 20:00:00"):
self.env["event.session"]._gc_mark_events_done()
self.assertEqual(self.session.stage_id, self.stage_done, "Done")
def test_event_session_update_multi(self):
"""Test the session series update"""
sessions = self.env["event.session"].create(
[
{
"event_id": self.event.id,
"date_begin": "2017-05-20 20:00:00",
"date_end": "2017-05-20 21:00:00",
},
{
"event_id": self.event.id,
"date_begin": "2017-05-21 20:00:00",
"date_end": "2017-05-21 21:00:00",
},
{
"event_id": self.event.id,
"date_begin": "2017-05-22 20:00:00",
"date_end": "2017-05-22 21:00:00",
},
{
"event_id": self.event.id,
"date_begin": "2017-05-23 20:00:00",
"date_end": "2017-05-23 21:00:00",
},
]
)
sessions = sessions.with_context(active_test=False)
session1, session2, session3, session4 = sessions
# Case 1: Archive session 1
session1.write({"active": False, "session_update": "this"})
self.assertFalse(session1.active)
self.assertTrue(session2.active)
self.assertTrue(session3.active)
self.assertTrue(session4.active)
# Case 2: Archive all
session2.write({"active": False, "session_update": "all"})
self.assertFalse(session1.active)
self.assertFalse(session2.active)
self.assertFalse(session3.active)
self.assertFalse(session4.active)
# Case 3: Unarchive starting from session 3
session3.write({"active": True, "session_update": "subsequent"})
self.assertFalse(session1.active)
self.assertFalse(session2.active)
self.assertTrue(session3.active)
self.assertTrue(session4.active)

View file

@ -0,0 +1,22 @@
# Copyright 2022 Moka Tourisme (https://www.mokatourisme.fr).
# @author Iván Todorovich <ivan.todorovich@gmail.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo.tests import HttpCase, tagged
@tagged("-at_install", "post_install")
class TestEventSessionICS(HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
cls.event_session = cls.env.ref("event_session.event_session_007_1_16_00")
cls.event = cls.event_session.event_id
def test_event_session_ics_file(self):
self.authenticate("admin", "admin")
res = self.url_open(f"/event/session/{self.event_session.id}/ics")
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers["Content-Type"], "application/octet-stream")
self.assertTrue(res.content.startswith(b"BEGIN:VCALENDAR"))

View file

@ -0,0 +1,147 @@
# Copyright 2017-19 Tecnativa - David Vidal
# Copyright 2017 Tecnativa - Pedro M. Baeza
# Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl-3.0).
from datetime import timedelta
from freezegun import freeze_time
from odoo.tools import mute_logger
from .common import CommonEventSessionCase
class TestEventSession(CommonEventSessionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.mail_template_reminder = cls.env.ref("event_session.event_session_reminder")
cls.mail_template_badge = cls.env.ref(
"event_session.event_session_registration_mail_template_badge"
)
cls.event = cls.env["event.event"].create(
{
"name": "Test event",
"use_sessions": True,
"event_mail_ids": [
(0, 0, vals)
for vals in [
{
"interval_nbr": 15,
"interval_unit": "days",
"interval_type": "before_event",
"template_ref": f"mail.template,{cls.mail_template_reminder.id}",
},
{
"interval_nbr": 0,
"interval_unit": "hours",
"interval_type": "after_sub",
"template_ref": f"mail.template,{cls.mail_template_badge.id}",
},
]
],
}
)
cls.session = cls.env["event.session"].create(
{
"date_begin": "2017-05-26 20:00:00",
"date_end": "2017-05-26 21:00:00",
"event_id": cls.event.id,
}
)
cls.registration = cls.env["event.registration"].create(
{
"name": "Test Attendee",
"event_id": cls.event.id,
"session_id": cls.session.id,
}
)
cls.registration.action_confirm()
@mute_logger("odoo.models.unlink")
def test_event_mail_sync_from_event(self):
self.assertEqual(len(self.session.event_mail_ids), 2)
# Case 1: Remove from event, removes from sessions
self.event.event_mail_ids[0].unlink()
self.assertEqual(len(self.session.event_mail_ids), 1)
# Case 2: Add a new template
event_mail = self.env["event.mail"].create(
{
"event_id": self.event.id,
"interval_nbr": 5,
"interval_unit": "days",
"interval_type": "before_event",
"template_ref": f"mail.template,{self.mail_template_reminder.id}",
}
)
session_mail = self.session.event_mail_ids.filtered(
lambda r: r.scheduler_id == event_mail
)
self.assertTrue(session_mail)
self.assertEqual(event_mail.interval_nbr, session_mail.interval_nbr)
self.assertEqual(event_mail.interval_unit, session_mail.interval_unit)
self.assertEqual(event_mail.interval_type, session_mail.interval_type)
self.assertEqual(event_mail.template_ref, session_mail.template_ref)
def test_event_mail_compute_scheduled_date(self):
event_mail = self.event.event_mail_ids.filtered(
lambda m: m.interval_type == "before_event"
)
session_mail = self.session.event_mail_ids.filtered(
lambda m: m.scheduler_id == event_mail
)
# Case 1: 15 days before event
event_mail.interval_nbr = 10
expected = self.session.date_begin - timedelta(days=10)
self.assertEqual(session_mail.scheduled_date, expected)
self.assertFalse(event_mail.scheduled_date)
# Case 2: 2 days after event
event_mail.interval_nbr = 2
event_mail.interval_type = "after_event"
expected = self.session.date_end + timedelta(days=2)
self.assertEqual(session_mail.scheduled_date, expected)
self.assertFalse(event_mail.scheduled_date)
# Case 3: after sub
event_mail.interval_nbr = 0
event_mail.interval_type = "after_sub"
self.assertEqual(session_mail.scheduled_date, self.session.create_date)
self.assertFalse(event_mail.scheduled_date)
def test_event_mail_registration_compute_scheduled_date(self):
session_mail = self.session.event_mail_ids.filtered(
lambda m: m.interval_type == "after_sub"
)
self.env["event.registration"].create(
{
"name": "Test Attendee",
"event_id": self.event.id,
"session_id": self.session.id,
"state": "open",
}
)
mail_registration = session_mail._create_missing_mail_registrations(
session_mail._get_new_event_registrations()
)
expected = mail_registration.registration_id.create_date
self.assertEqual(mail_registration.scheduled_date, expected)
@freeze_time("2017-05-16")
def test_event_mail_session_scheduler(self):
before_mail = self.session.event_mail_ids.filtered(
lambda m: m.interval_type == "before_event"
)
self.assertFalse(before_mail.mail_done)
self.env["event.mail"].schedule_communications()
self.assertTrue(before_mail.mail_done)
@freeze_time("2017-06-01")
def test_event_mail_session_scheduler_before_event_ignore_old(self):
"""Test that we do not send emails if the mailing was scheduled before the event
but the event is over"""
before_mail = self.session.event_mail_ids.filtered(
lambda m: m.interval_type == "before_event"
)
self.assertFalse(before_mail.mail_done)
self.env["event.mail"].schedule_communications()
self.assertFalse(before_mail.mail_done)

View file

@ -0,0 +1,235 @@
# Copyright 2017-19 Tecnativa - David Vidal
# Copyright 2017 Tecnativa - Pedro M. Baeza
# Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl-3.0).
from odoo import fields
from odoo.exceptions import ValidationError
from .common import CommonEventSessionCase
class TestEventSessionCreateWizard(CommonEventSessionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.event = cls.env["event.event"].create(
{"name": "Test Event", "use_sessions": True}
)
cls.timeslot_16_00 = cls.env.ref("event_session.timeslot_16_00")
cls.timeslot_20_00 = cls.env.ref("event_session.timeslot_20_00")
def test_timeslot_name_create(self):
Timeslot = self.env["event.session.timeslot"]
# Case 1: Simple case
timeslot_id, __ = Timeslot.name_create("23:00")
timeslot = Timeslot.browse(timeslot_id)
self.assertEqual(timeslot.time, 23.00)
# Case 2: float case
timeslot_id, __ = Timeslot.name_create("23:30")
timeslot = Timeslot.browse(timeslot_id)
self.assertEqual(timeslot.time, 23.50)
# Case 3: invalid
msg = "The timeslot has to be defined in HH:MM format"
with self.assertRaisesRegex(ValidationError, msg):
Timeslot.name_create("25:30")
# Case 4: invalid
msg = "The timeslot has to be defined in HH:MM format"
with self.assertRaisesRegex(ValidationError, msg):
Timeslot.name_create("22:70")
def test_wizard_default_values(self):
self.env["event.session"].create(
[
{
"date_begin": "2017-05-26 20:00:00",
"date_end": "2017-05-26 21:00:00",
"event_id": self.event.id,
},
{
"date_begin": "2017-05-27 20:00:00",
"date_end": "2017-05-27 22:00:00",
"event_id": self.event.id,
},
]
)
wizard = self.env["wizard.event.session"].new(
{
"event_id": self.event.id,
}
)
self.assertEqual(wizard.start, fields.Date.to_date("2017-05-28"))
self.assertEqual(wizard.duration, 2.0)
def test_check_duration(self):
with self.assertRaisesRegex(ValidationError, "Duration is required"):
self._wizard_generate_sessions(
{
"event_id": self.event.id,
"rrule_type": "weekly",
"mon": True,
"timeslot_ids": [(6, 0, self.timeslot_16_00.ids)],
"duration": 0.0,
"start": "2022-01-01",
"until": "2022-01-31",
}
)
def test_check_interval(self):
with self.assertRaisesRegex(ValidationError, "The interval cannot be negative"):
self._wizard_generate_sessions(
{
"event_id": self.event.id,
"rrule_type": "weekly",
"mon": True,
"timeslot_ids": [(6, 0, self.timeslot_16_00.ids)],
"duration": 1.0,
"interval": -1,
"start": "2022-01-01",
"until": "2022-01-31",
}
)
def test_session_create_wizard_weekly_01(self):
"""Mondays at 16:00 and 20:00, for whole Jan 2022
January 2022
03
10
17
24
31
"""
self.assertSessionDates(
self._wizard_generate_sessions(
{
"event_id": self.event.id,
"rrule_type": "weekly",
"mon": True,
"tue": False,
"wed": False,
"thu": False,
"fri": False,
"sun": False,
"sat": False,
"timeslot_ids": [
(6, 0, (self.timeslot_16_00 | self.timeslot_20_00).ids)
],
"duration": 1.0,
"start": "2022-01-01",
"until": "2022-01-31",
}
),
[
"2022-01-03 16:00:00",
"2022-01-03 20:00:00",
"2022-01-10 16:00:00",
"2022-01-10 20:00:00",
"2022-01-17 16:00:00",
"2022-01-17 20:00:00",
"2022-01-24 16:00:00",
"2022-01-24 20:00:00",
"2022-01-31 16:00:00",
"2022-01-31 20:00:00",
],
)
def test_session_create_wizard_weekly_02(self):
"""Mondays, Wednesdays and Fridays at 20:00, every 2 weeks for a Feb 2022
February 2022
02 04
14 16 18
28
"""
self.assertSessionDates(
self._wizard_generate_sessions(
{
"event_id": self.event.id,
"rrule_type": "weekly",
"interval": 2,
"mon": True,
"tue": False,
"wed": True,
"thu": False,
"fri": True,
"sun": False,
"sat": False,
"timeslot_ids": [(6, 0, self.timeslot_20_00.ids)],
"duration": 2.0,
"start": "2022-02-01",
"until": "2022-02-28",
}
),
[
"2022-02-02 20:00:00",
"2022-02-04 20:00:00",
"2022-02-14 20:00:00",
"2022-02-16 20:00:00",
"2022-02-18 20:00:00",
"2022-02-28 20:00:00",
],
)
def test_session_create_wizard_monthly_by_day(self):
"""Last sunday of each month at 16:00, from March 2022 to May 2022"""
self.assertSessionDates(
self._wizard_generate_sessions(
{
"event_id": self.event.id,
"rrule_type": "monthly",
"month_by": "day",
"byday": "-1",
"weekday": "SUN",
"timeslot_ids": [(6, 0, self.timeslot_16_00.ids)],
"duration": 1.0,
"start": "2022-03-01",
"until": "2022-05-31",
}
),
[
"2022-03-27 16:00:00",
"2022-04-24 16:00:00",
"2022-05-29 16:00:00",
],
)
def test_session_create_wizard_monthly_by_date(self):
"""The 15th of every month, from March 2022 to May 2022"""
self.assertSessionDates(
self._wizard_generate_sessions(
{
"event_id": self.event.id,
"rrule_type": "monthly",
"month_by": "date",
"day": "15",
"timeslot_ids": [(6, 0, self.timeslot_16_00.ids)],
"duration": 1.0,
"start": "2022-03-01",
"until": "2022-05-31",
}
),
[
"2022-03-15 16:00:00",
"2022-04-15 16:00:00",
"2022-05-15 16:00:00",
],
)

View file

@ -0,0 +1,119 @@
<?xml version="1.0" ?>
<odoo>
<record id="act_event_session_event_form" model="ir.actions.act_window">
<field name="res_model">event.session</field>
<field name="name">Sessions</field>
<field name="view_mode">kanban,tree,form,calendar,pivot</field>
<field name="context">
{
'search_default_event_id': active_id,
'default_event_id': active_id,
}
</field>
</record>
<record id="view_event_form" model="ir.ui.view">
<field name="model">event.event</field>
<field name="inherit_id" ref="event.view_event_form" />
<field name="priority">5</field>
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button
name="%(act_event_session_event_form)d"
type="action"
class="oe_stat_button"
icon="fa-calendar"
help="Sessions available for this event"
attrs="{'invisible': [('use_sessions', '=', False)]}"
>
<field name="session_count" widget="statinfo" string="Sessions" />
</button>
</div>
<label for="date_begin" position="before">
<field name="use_sessions" />
</label>
<label for="date_begin" position="attributes">
<attribute name="attrs">
{
'invisible': [('use_sessions', '=', True)],
}
</attribute>
</label>
<xpath
expr="//field[@name='date_begin']/parent::div[hasclass('o_row')]"
position="attributes"
>
<attribute name="attrs">
{
'invisible': [('use_sessions', '=', True)],
}
</attribute>
</xpath>
<!-- Hide not relevant columns in event.mail -->
<xpath
expr="//field[@name='event_mail_ids']/tree/field[@name='scheduled_date']"
position="attributes"
>
<attribute name="attrs">
{
'column_invisible': [('parent.use_sessions', '=', True)],
}
</attribute>
</xpath>
<xpath
expr="//field[@name='event_mail_ids']/tree/field[@name='mail_count_done']"
position="attributes"
>
<attribute name="attrs">
{
'column_invisible': [('parent.use_sessions', '=', True)],
}
</attribute>
</xpath>
<xpath
expr="//field[@name='event_mail_ids']/tree/field[@name='mail_state']"
position="attributes"
>
<attribute name="attrs">
{
'column_invisible': [('parent.use_sessions', '=', True)],
}
</attribute>
</xpath>
</field>
</record>
<record id="view_event_kanban" model="ir.ui.view">
<field name="model">event.event</field>
<field name="inherit_id" ref="event.view_event_kanban" />
<field name="arch" type="xml">
<templates position="before">
<field name="use_sessions" />
<field name="session_count" />
</templates>
<div class="o_kanban_record_bottom" position="before">
<h5
name="sessions"
class="o_event_fontsize_11 p-0"
attrs="{'invisible': [('use_sessions', '=', False)]}"
>
<a name="%(act_event_session_event_form)d" type="action">
<t t-esc="record.session_count.raw_value" /> Sessions
</a>
</h5>
</div>
</field>
</record>
<record id="view_event_search" model="ir.ui.view">
<field name="model">event.event</field>
<field name="inherit_id" ref="event.view_event_search" />
<field name="arch" type="xml">
<filter name="filter_inactive" position="before">
<filter
string="With sessions"
name="sessions"
domain="[('use_sessions', '=', True)]"
/>
<separator />
</filter>
</field>
</record>
</odoo>

View file

@ -0,0 +1,78 @@
<?xml version="1.0" ?>
<odoo>
<record id="view_event_registration_form" model="ir.ui.view">
<field name="model">event.registration</field>
<field name="inherit_id" ref="event.view_event_registration_form" />
<field name="arch" type="xml">
<field name="event_id" position="after">
<field name="use_sessions" invisible="1" />
<field
name="session_id"
domain="[('event_id', '=', event_id)]"
attrs="{'required': [('use_sessions', '=', True)], 'invisible': [('use_sessions', '!=', True)]}"
options="{'no_create': True}"
/>
</field>
</field>
</record>
<record id="view_event_registration_tree" model="ir.ui.view">
<field name="model">event.registration</field>
<field name="inherit_id" ref="event.view_event_registration_tree" />
<field name="arch" type="xml">
<field name="event_id" position="after">
<field name="session_id" optional="show" />
</field>
</field>
</record>
<record id="event_registration_view_kanban" model="ir.ui.view">
<field name="model">event.registration</field>
<field name="inherit_id" ref="event.event_registration_view_kanban" />
<field name="arch" type="xml">
<field name="event_id" position="after">
<field name="use_sessions" invisible="1" />
<field
name="session_id"
class="o_text_overflow"
invisible="context.get('default_session_id')"
attrs="{'required': [('use_sessions', '=', True)], 'invisible': [('use_sessions', '!=', True)]}"
/>
</field>
</field>
</record>
<record id="view_event_registration_calendar" model="ir.ui.view">
<field name="model">event.registration</field>
<field name="inherit_id" ref="event.view_event_registration_calendar" />
<field name="arch" type="xml">
<field name="event_id" position="after">
<field name="session_id" filters="1" />
</field>
</field>
</record>
<record model="ir.ui.view" id="view_event_registration_pivot">
<field name="model">event.registration</field>
<field name="inherit_id" ref="event.view_event_registration_pivot" />
<field name="arch" type="xml">
<field name="event_id" position="after">
<field name="session_id" type="row" />
</field>
</field>
</record>
<record model="ir.ui.view" id="view_event_registration_graph">
<field name="model">event.registration</field>
<field name="inherit_id" ref="event.view_event_registration_graph" />
<field name="arch" type="xml">
<field name="event_id" position="after">
<field name="session_id" />
</field>
</field>
</record>
<record model="ir.ui.view" id="view_registration_search">
<field name="model">event.registration</field>
<field name="inherit_id" ref="event.view_registration_search" />
<field name="arch" type="xml">
<field name="event_id" position="after">
<field name="session_id" />
</field>
</field>
</record>
</odoo>

View file

@ -0,0 +1,527 @@
<?xml version="1.0" ?>
<odoo>
<record
id="act_event_registration_from_event_session"
model="ir.actions.act_window"
>
<field name="res_model">event.registration</field>
<field name="name">Attendees</field>
<field name="view_mode">kanban,tree,form,calendar,graph</field>
<field name="domain">[('session_id', '=', active_id)]</field>
<field name="context">{'default_session_id': active_id}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No Attendees yet!
</p><p>
Wait until Attendees register to your Event or create their registrations manually.
</p>
</field>
</record>
<record model="ir.ui.view" id="view_event_session_search">
<field name="model">event.session</field>
<field name="arch" type="xml">
<search>
<field
name="name"
string="Session"
filter_domain="[('name', 'ilike', self), ('date_begin_located', 'ilike', self)]"
/>
<field name="event_id" />
<field name="event_type_id" />
<field name="user_id" />
<field name="company_id" groups="base.group_multi_company" />
<filter
string="My Events"
name="myevents"
help="My Events"
domain="[('user_id', '=', uid)]"
/>
<filter
string="Unread Messages"
name="message_needaction"
domain="[('message_needaction', '=', True)]"
/>
<separator />
<filter
string="Upcoming/Running"
name="upcoming"
domain="[('date_end', '&gt;=', datetime.datetime.combine(context_today(), datetime.time(0,0,0)))]"
help="Upcoming events from today"
/>
<separator />
<filter string="Start Date" name="start_date" date="date_begin" />
<separator />
<filter
string="Archived"
name="inactive"
domain="[('active', '=', False)]"
/>
<separator />
<filter
invisible="1"
string="Late Activities"
name="activities_overdue"
domain="[('my_activity_date_deadline', '&lt;', context_today().strftime('%Y-%m-%d'))]"
help="Show all records which has next action date is before today"
/>
<filter
invisible="1"
string="Today Activities"
name="activities_today"
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"
/>
<filter
invisible="1"
string="Future Activities"
name="activities_upcoming_all"
domain="[('my_activity_date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))]"
/>
<group expand="0" string="Group By">
<filter
string="Event"
name="group_event"
domain="[]"
context="{'group_by':'event_id'}"
/>
<filter
string="Responsible"
name="responsible"
context="{'group_by': 'user_id'}"
/>
<filter
string="Template"
name="event_type_id"
context="{'group_by': 'event_type_id'}"
/>
<filter
string="Start Date"
name="date_begin"
domain="[]"
context="{'group_by': 'date_begin'}"
/>
</group>
</search>
</field>
</record>
<record id="view_event_session_form" model="ir.ui.view">
<field name="model">event.session</field>
<field name="arch" type="xml">
<form>
<header>
<field
name="stage_id"
widget="statusbar"
options="{'clickable': '1'}"
/>
</header>
<div
attrs="{'invisible': ['|', ('id', '=', False), ('session_update_message', 'in', [False, ''])]}"
class="alert alert-info oe_edit_only"
role="status"
>
<field name="id" invisible="1" />
<p>Edit sessions</p>
<field name="session_update" widget="radio" class="o_light_label" />
<p attrs="{'invisible': [('session_update', '=', 'this')]}">
Applies to the following fields:
<field name="session_update_message" />
</p>
</div>
<sheet>
<div class="oe_button_box" name="button_box">
<button
name="action_open_registrations"
type="object"
context="{'search_default_expected': True}"
class="oe_stat_button"
icon="fa-users"
help="Total Registrations for this Session"
>
<field
name="seats_expected"
widget="statinfo"
string="Attendees"
/>
</button>
</div>
<field name="active" invisible="1" />
<widget
name="web_ribbon"
title="Archived"
bg_color="bg-danger"
attrs="{'invisible': [('active', '=', True)]}"
/>
<field
name="kanban_state"
widget="state_selection"
class="ml-auto float-right"
/>
<div class="oe_title">
<h1><field
class="o_text_overflow"
name="name"
readonly="1"
/></h1>
</div>
<group>
<group name="left">
<field
name="event_id"
invisible="context.get('default_event_id')"
/>
<label for="date_begin" string="Date" />
<div class="o_row">
<field
name="date_begin"
widget="daterange"
nolabel="1"
class="oe_inline"
options="{'related_end_date': 'date_end'}"
/>
<i
class="fa fa-long-arrow-right mx-2"
aria-label="Arrow icon"
title="Arrow"
/>
<field
name="date_end"
widget="daterange"
nolabel="1"
class="oe_inline"
options="{'related_start_date': 'date_begin'}"
/>
</div>
<field name="date_tz" readonly="1" />
<field
name="company_id"
groups="base.group_multi_company"
widget="selection"
readonly="1"
/>
</group>
<group name="right">
</group>
</group>
<notebook>
<page string="Communication" name="event_communication">
<field name="event_mail_ids" options="{'no_open': True}">
<tree editable="bottom">
<field name="sequence" widget="handle" />
<field name="notification_type" />
<field name="template_model_id" invisible="1" />
<field
name="template_ref"
options="{'model_field': 'template_model_id', 'no_quick_create': True}"
context="{'filter_template_on_event': True, 'default_model': 'event.registration'}"
/>
<field
name="interval_nbr"
attrs="{'readonly':[('interval_unit','=','now')]}"
/>
<field name="interval_unit" />
<field name="interval_type" />
<field
name="scheduled_date"
groups="base.group_no_one"
/>
<field name="mail_count_done" />
<field
name="mail_state"
widget="icon_selection"
string=" "
options="{'sent': 'fa fa-check', 'scheduled': 'fa fa-hourglass-half', 'running': 'fa fa-cogs'}"
/>
</tree>
</field>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" groups="base.group_user" />
<field name="activity_ids" />
<field name="message_ids" />
</div>
</form>
</field>
</record>
<record id="view_event_session_tree" model="ir.ui.view">
<field name="model">event.session</field>
<field name="arch" type="xml">
<tree>
<field name="event_id" optional="show" />
<field name="date_begin" />
<field name="date_end" />
<field name="date_tz" optional="hide" />
<field name="seats_limited" optional="hide" />
<field
name="seats_expected"
string="Expected Attendees"
sum="Total"
readonly="1"
/>
<field name="seats_used" sum="Total" readonly="1" />
<field
name="seats_max"
string="Maximum Seats"
sum="Total"
readonly="1"
optional="hide"
/>
<field
name="seats_available_unexpected"
string="Not allocated seats"
sum="Available seats not expected"
attrs="{'invisible': [('seats_limited','=',False)]}"
/>
<field name="seats_reserved" sum="Total" readonly="1" optional="hide" />
<field
name="seats_unconfirmed"
string="Unconfirmed Seats"
sum="Total"
readonly="1"
optional="hide"
/>
<field name="message_needaction" invisible="1" readonly="1" />
<field
name="activity_exception_decoration"
widget="activity_exception"
readonly="1"
/>
</tree>
</field>
</record>
<record id="view_event_session_form_quick_create" model="ir.ui.view">
<field name="model">event.session</field>
<field name="priority">1000</field>
<field name="arch" type="xml">
<form>
<group>
<label for="date_begin" string="Date" />
<div class="o_row">
<field
name="date_begin"
widget="daterange"
options="{'related_end_date': 'date_end'}"
/>
<i
class="fa fa-long-arrow-right mx-2"
aria-label="Arrow icon"
title="Arrow"
/>
<field
name="date_end"
widget="daterange"
options="{'related_start_date': 'date_begin'}"
/>
</div>
</group>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_event_session_kanban">
<field name="model">event.session</field>
<field name="arch" type="xml">
<kanban
class="o_event_kanban_view"
on_create="quick_create"
quick_create_view="event_session.view_event_session_form_quick_create"
sample="1"
>
<field name="user_id" />
<field name="name" />
<field name="address_id" />
<field name="date_begin" />
<field name="date_end" />
<field name="auto_confirm" />
<field name="seats_unconfirmed" />
<field name="seats_reserved" />
<field name="seats_used" />
<field name="seats_expected" />
<field name="activity_ids" />
<field name="activity_state" />
<templates>
<t t-name="kanban-box">
<div
t-attf-class="d-flex flex-column p-0 oe_kanban_card oe_kanban_global_click"
>
<div
class="o_kanban_content p-0 m-0 position-relative row d-flex flex-fill"
>
<div
class="col-3 text-bg-primary p-2 text-center d-flex flex-column justify-content-center"
>
<div
t-esc="luxon.DateTime.fromISO(record.date_begin.raw_value).toFormat('d')"
class="o_event_fontsize_20"
/>
<div>
<t
t-esc="luxon.DateTime.fromISO(record.date_begin.raw_value).toFormat('MMM yyyy')"
/>
</div>
<div><t
t-esc="luxon.DateTime.fromISO(record.date_begin.raw_value).toFormat('t')"
/></div>
<div
t-if="record.date_begin.raw_value !== record.date_end.raw_value"
>
<i
class="fa fa-arrow-right o_event_fontsize_09"
title="End date"
/>
<t
t-esc="luxon.DateTime.fromISO(record.date_end.raw_value).toFormat('d MMM')"
/>
</div>
</div>
<div
class="col-9 py-2 px-3 d-flex flex-column justify-content-between pt-3"
>
<div>
<div
class="o_kanban_record_title o_text_overflow"
t-att-title="record.name.value"
>
<field name="name" />
</div>
<div t-if="record.address_id.value"><i
class="fa fa-map-marker"
title="Location"
/> <span
class="o_text_overflow o_event_kanban_location"
t-esc="record.address_id.value"
/></div>
</div>
<h5 class="o_event_fontsize_11 p-0">
<a
name="%(act_event_registration_from_event_session)d"
type="action"
context="{'search_default_expected': True}"
>
<t
t-esc="record.seats_expected.raw_value"
/> Expected attendees
</a>
<t
t-set="total_seats"
t-value="record.seats_reserved.raw_value + record.seats_used.raw_value"
/>
<div
class="pt-2 pt-md-0"
t-if="total_seats > 0 and ! record.auto_confirm.raw_value"
><br />
<a
class="pl-2"
name="%(act_event_registration_from_event_session)d"
type="action"
context="{'search_default_confirmed': True}"
>
<i
class="fa fa-level-up fa-rotate-90"
title="Confirmed"
/><span class="pl-2"><t
t-esc="total_seats"
/> Confirmed</span>
</a>
</div>
</h5>
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_left">
<field
name="activity_ids"
widget="kanban_activity"
/>
</div>
<div class="oe_kanban_bottom_right">
<field
name="kanban_state"
widget="state_selection"
/>
<field
name="user_id"
widget="many2one_avatar_user"
/>
</div>
</div>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record model="ir.ui.view" id="view_event_session_calendar">
<field name="model">event.session</field>
<field name="arch" type="xml">
<calendar
date_start="date_begin"
date_stop="date_end"
mode="month"
color="event_type_id"
quick_add="False"
>
<field
name="date_begin"
widget="daterange"
options="{'related_end_date': 'date_end'}"
/>
<field
name="date_end"
widget="daterange"
options="{'related_start_date': 'date_begin'}"
/>
<field
name="event_id"
filters="1"
invisible="context.get('default_event_id')"
/>
<field name="event_type_id" filters="1" invisible="1" />
</calendar>
</field>
</record>
<record id="view_event_session_pivot" model="ir.ui.view">
<field name="model">event.session</field>
<field eval="4" name="priority" />
<field name="arch" type="xml">
<pivot sample="1">
<field name="event_id" type="row" />
<field name="date_begin" type="col" />
<field name="seats_reserved" type="measure" />
</pivot>
</field>
</record>
<record id="action_event_session" model="ir.actions.act_window">
<field name="res_model">event.session</field>
<field name="name">Sessions</field>
<field name="view_mode">kanban,tree,form,calendar,pivot,graph</field>
</record>
<record id="action_event_session_pivot" model="ir.actions.act_window">
<field name="res_model">event.session</field>
<field name="name">Sessions Analysis</field>
<field name="view_mode">pivot,graph</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No Sessions data yet!
</p><p>
Use this report to compare or aggregate sessions performances.
</p>
</field>
</record>
<menuitem
id="event_session_menu"
name="Sessions"
sequence="2"
parent="event.event_main_menu"
action="action_event_session"
groups="event.group_event_registration_desk"
/>
<menuitem
id="event_session_menu_report"
name="Sessions"
sequence="3"
parent="event.menu_reporting_events"
action="action_event_session_pivot"
groups="event.group_event_user"
/>
</odoo>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_event_type_form" model="ir.ui.view">
<field name="model">event.type</field>
<field name="inherit_id" ref="event.view_event_type_form" />
<field name="arch" type="xml">
<xpath
expr="//field[@name='has_seats_limitation']/parent::div"
position="before"
>
<div colspan="2" class="o_checkbox_optional_field">
<label for="use_sessions" />
<field name="use_sessions" class="w-100" />
</div>
</xpath>
</field>
</record>
</odoo>

View file

@ -0,0 +1 @@
from . import wizard_event_session

View file

@ -0,0 +1,273 @@
# Copyright 2017 David Vidal<david.vidal@tecnativa.com>
# Copyright 2017 Tecnativa - Pedro M. Baeza
# Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr).
# @author Iván Todorovich <ivan.todorovich@gmail.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from datetime import datetime, time, timedelta
import pytz
from dateutil import rrule
from dateutil.relativedelta import relativedelta
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
SELECT_FREQ_TO_RRULE = {
"daily": rrule.DAILY,
"weekly": rrule.WEEKLY,
"monthly": rrule.MONTHLY,
"yearly": rrule.YEARLY,
}
RRULE_WEEKDAYS = {
"SUN": "SU",
"MON": "MO",
"TUE": "TU",
"WED": "WE",
"THU": "TH",
"FRI": "FR",
"SAT": "SA",
}
def freq_to_rrule(freq):
return SELECT_FREQ_TO_RRULE[freq]
def float_time_to_hours_and_minutes(float_time):
# Round to 2 decimals to avoid hours like 1:60
# It'd be rounded to 2:00
float_time = round(float_time, 2)
hours = int(float_time)
minutes = round((float_time - hours) * 60)
return (hours, minutes)
def float_time_as_timedelta(float_time):
hours, minutes = float_time_to_hours_and_minutes(float_time)
return timedelta(hours=hours, minutes=minutes)
def float_time_as_time(float_time):
hours, minutes = float_time_to_hours_and_minutes(float_time)
return time(hour=hours, minute=minutes)
class WizardEventSession(models.TransientModel):
_name = "wizard.event.session"
_description = "Wizard for ease sessions creation"
event_id = fields.Many2one(
comodel_name="event.event",
default=lambda self: self.env.context["active_id"],
ondelete="cascade",
required=True,
readonly=True,
)
date_tz = fields.Selection(
related="event_id.date_tz",
help="Set it up in the event configuration"
"Sessions will be generated up to this date",
)
duration = fields.Float(
compute="_compute_duration",
readonly=False,
store=True,
required=True,
help="Duration of the sessions in hours",
)
timeslot_ids = fields.Many2many(
comodel_name="event.session.timeslot",
string="Time slots",
required=True,
)
# rrule fields
interval = fields.Integer(default=1, required=True)
rrule_type = fields.Selection(
[("weekly", "Weeks"), ("monthly", "Months")],
string="Recurrence",
default="weekly",
required=True,
)
mon = fields.Boolean()
tue = fields.Boolean()
wed = fields.Boolean()
thu = fields.Boolean()
fri = fields.Boolean()
sat = fields.Boolean()
sun = fields.Boolean()
month_by = fields.Selection(
[("date", "Date of month"), ("day", "Day of month")],
default="date",
)
day = fields.Integer(default=1)
weekday = fields.Selection(
[
("MON", "Monday"),
("TUE", "Tuesday"),
("WED", "Wednesday"),
("THU", "Thursday"),
("FRI", "Friday"),
("SAT", "Saturday"),
("SUN", "Sunday"),
],
)
byday = fields.Selection(
[
("1", "First"),
("2", "Second"),
("3", "Third"),
("4", "Fourth"),
("-1", "Last"),
],
string="By day",
)
start = fields.Date(
compute="_compute_start",
readonly=False,
required=True,
store=True,
)
until = fields.Date(required=True)
@api.depends("event_id")
def _compute_start(self):
# Suggest to create sessions from the date of the last session
# Usually the user wants to add new ones.
for rec in self:
rec.start = rec.event_id.date_end.date() + timedelta(days=1)
@api.depends("event_id")
def _compute_duration(self):
# Suggest to create sessions with the same duration than the
# last existing session
for rec in self:
if rec.event_id.session_ids:
session = rec.event_id.session_ids[-1]
delta = session.date_end - session.date_begin
rec.duration = round(delta.total_seconds() / 3600, 2)
@api.constrains("duration")
def _check_duration(self):
if any(rec.duration <= 0 for rec in self):
raise ValidationError(_("Duration is required."))
@api.constrains("interval")
def _check_interval(self):
if any(rec.interval <= 0 for rec in self):
raise ValidationError(_("The interval cannot be negative."))
def _get_lang_week_start(self):
lang = self.env["res.lang"]._lang_get(self.env.user.lang)
week_start = int(lang.week_start)
# lang.week_start ranges from '1' to '7'
# rrule expects an int from 0 to 6
return rrule.weekday(week_start - 1)
def _get_week_days(self):
return tuple(
rrule.weekday(weekday_index)
for weekday_index, weekday in {
rrule.MO.weekday: self.mon,
rrule.TU.weekday: self.tue,
rrule.WE.weekday: self.wed,
rrule.TH.weekday: self.thu,
rrule.FR.weekday: self.fri,
rrule.SA.weekday: self.sat,
rrule.SU.weekday: self.sun,
}.items()
if weekday
)
def _get_rrule(self, dtstart=None):
"""Builds the rrule from fields"""
self.ensure_one()
freq = self.rrule_type
rrule_params = dict(
dtstart=dtstart,
until=datetime.combine(self.until, datetime.max.time()),
interval=self.interval,
)
if freq == "monthly" and self.month_by == "date":
rrule_params["bymonthday"] = self.day
elif freq == "monthly" and self.month_by == "day":
rrule_params["byweekday"] = getattr(rrule, RRULE_WEEKDAYS[self.weekday])(
int(self.byday)
)
elif freq == "weekly":
weekdays = self._get_week_days()
if not weekdays: # pragma: no cover
raise ValidationError(
_("You have to choose at least one day in the week")
)
rrule_params["byweekday"] = weekdays
rrule_params["wkst"] = self._get_lang_week_start()
return rrule.rrule(freq_to_rrule(freq), **rrule_params)
def _get_start_of_period(self):
self.ensure_one()
dtstart = datetime.combine(self.start, datetime.min.time())
if self.rrule_type == "monthly":
return dtstart - relativedelta(day=1)
return dtstart
def _get_occurrences(self):
self.ensure_one()
dtstart = self._get_start_of_period()
occurences = self._get_rrule(dtstart=dtstart)
return list(occurences)
def _get_ranges(self):
"""Generate ranges from the rrule
:return: list of tuples (start_dt, end_dt, extra_vals)
"""
self.ensure_one()
res = []
ocurrences = self._get_occurrences()
duration = float_time_as_timedelta(self.duration)
timezone = pytz.timezone(self.date_tz)
timeslot_times = [float_time_as_time(t.time) for t in self.timeslot_ids]
for dtstart in ocurrences:
for tslot, ttime in zip(self.timeslot_ids, timeslot_times):
start = datetime.combine(dtstart, ttime)
start_utc = (
timezone.localize(start, is_dst=False)
.astimezone(pytz.utc)
.replace(tzinfo=None)
)
extra_vals = tslot._prepare_session_extra_vals()
res.append((start_utc, start_utc + duration, extra_vals))
return res
def _prepare_session_vals(self, date_begin, date_end):
self.ensure_one()
return {
"event_id": self.event_id.id,
"date_begin": date_begin,
"date_end": date_end,
}
def _create_sessions(self):
"""Create sessions"""
self.ensure_one()
session_vals = []
for date_begin, date_end, extra_vals in self._get_ranges():
vals = self._prepare_session_vals(date_begin, date_end)
vals.update(extra_vals)
session_vals.append(vals)
return self.env["event.session"].create(session_vals)
def action_create_sessions(self):
self.ensure_one()
sessions = self._create_sessions()
action = self.env["ir.actions.act_window"]._for_xml_id(
"event_session.act_event_session_event_form"
)
action["domain"] = [("id", "in", sessions.ids)]
action["context"] = {
"default_event_id": self.event_id.id,
"search_default_event_id": self.event_id.id,
}
return action

View file

@ -0,0 +1,115 @@
<?xml version="1.0" ?>
<odoo>
<record id="act_wizard_event_session" model="ir.actions.act_window">
<field name="name">Create Sessions</field>
<field name="res_model">wizard.event.session</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<record id="view_event_form_create_sessions" model="ir.ui.view">
<field name="model">event.event</field>
<field name="inherit_id" ref="event.view_event_form" />
<field name="arch" type="xml">
<header position="inside">
<button
string="Create Sessions"
name="%(act_wizard_event_session)d"
class="oe_highlight"
attrs="{'invisible': [('use_sessions', '=', False)]}"
type="action"
/>
</header>
</field>
</record>
<record id="view_wizard_event_session_form" model="ir.ui.view">
<field name="model">wizard.event.session</field>
<field name="arch" type="xml">
<form>
<group name="schedule" string="Schedule">
<group name="rrule">
<label string="From" for="start" />
<div class="o_row">
<field
name="start"
widget="daterange"
nolabel="1"
class="oe_inline"
options="{'related_end_date': 'until'}"
/>
<i
class="fa fa-long-arrow-right mx-2"
aria-label="Arrow icon"
title="Arrow"
/>
<field
name="until"
widget="daterange"
nolabel="1"
class="oe_inline"
options="{'related_start_date': 'start'}"
/>
</div>
<label for="interval" string="Repeat Every" />
<div class="o_col">
<div class="o_row">
<field name="interval" />
<field name="rrule_type" />
</div>
<widget
name="week_days"
attrs="{'invisible': [('rrule_type', '!=', 'weekly')]}"
/>
</div>
<label
string="Day of Month"
for="month_by"
attrs="{'invisible': [('rrule_type', '!=', 'monthly')]}"
/>
<div
class="o_row"
attrs="{'invisible': [('rrule_type', '!=', 'monthly')]}"
>
<field name="month_by" />
<field
name="day"
attrs="{'required': [('month_by', '=', 'date'), ('rrule_type', '=', 'monthly')],
'invisible': [('month_by', '!=', 'date')]}"
/>
<field
name="byday"
string="The"
attrs="{'required': [('month_by', '=', 'day'), ('rrule_type', '=', 'monthly')],
'invisible': [('month_by', '!=', 'day')]}"
/>
<field
name="weekday"
nolabel="1"
attrs="{'required': [('month_by', '=', 'day'), ('rrule_type', '=', 'monthly')],
'invisible': [('month_by', '!=', 'day')]}"
/>
</div>
</group>
<group name="time">
<field name="event_id" invisible="1" />
<field name="date_tz" />
<field
name="timeslot_ids"
string="At"
widget="many2many_tags"
/>
<field name="duration" widget="float_time" />
</group>
</group>
<footer>
<button
name="action_create_sessions"
type="object"
string="Create Sessions"
class="oe_highlight"
/>
<button special="cancel" string="Cancel" />
</footer>
</form>
</field>
</record>
</odoo>