mirror of
https://github.com/bringout/oca-mrp.git
synced 2026-04-22 00:12:01 +02:00
Initial commit: OCA Mrp packages (117 packages)
This commit is contained in:
commit
277e84fd7a
4403 changed files with 395154 additions and 0 deletions
44
odoo-bringout-oca-event-event_session/README.md
Normal file
44
odoo-bringout-oca-event-event_session/README.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Event Sessions
|
||||
|
||||
Odoo addon: event_session
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-event-event_session
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- event
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: Event Sessions
|
||||
- **Version**: 16.0.1.4.1
|
||||
- **Category**: Marketing
|
||||
- **License**: AGPL-3
|
||||
- **Installable**: False
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/event](https://github.com/OCA/event) branch 16.0, addon `event_session`.
|
||||
|
||||
## License
|
||||
|
||||
This package maintains the original AGPL-3 license from the upstream Odoo project.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Overview: doc/OVERVIEW.md
|
||||
- Architecture: doc/ARCHITECTURE.md
|
||||
- Models: doc/MODELS.md
|
||||
- Controllers: doc/CONTROLLERS.md
|
||||
- Wizards: doc/WIZARDS.md
|
||||
- Install: doc/INSTALL.md
|
||||
- Usage: doc/USAGE.md
|
||||
- Configuration: doc/CONFIGURATION.md
|
||||
- Dependencies: doc/DEPENDENCIES.md
|
||||
- Troubleshooting: doc/TROUBLESHOOTING.md
|
||||
- FAQ: doc/FAQ.md
|
||||
32
odoo-bringout-oca-event-event_session/doc/ARCHITECTURE.md
Normal file
32
odoo-bringout-oca-event-event_session/doc/ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U[Users] -->|HTTP| V[Views and QWeb Templates]
|
||||
V --> C[Controllers]
|
||||
V --> W[Wizards – Transient Models]
|
||||
C --> M[Models and ORM]
|
||||
W --> M
|
||||
M --> R[Reports]
|
||||
DX[Data XML] --> M
|
||||
S[Security – ACLs and Groups] -. enforces .-> M
|
||||
|
||||
subgraph Event_session Module - event_session
|
||||
direction LR
|
||||
M:::layer
|
||||
W:::layer
|
||||
C:::layer
|
||||
V:::layer
|
||||
R:::layer
|
||||
S:::layer
|
||||
DX:::layer
|
||||
end
|
||||
|
||||
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
|
||||
```
|
||||
|
||||
Notes
|
||||
- Views include tree/form/kanban templates and report templates.
|
||||
- Controllers provide website/portal routes when present.
|
||||
- Wizards are UI flows implemented with `models.TransientModel`.
|
||||
- Data XML loads data/demo records; Security defines groups and access.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Configuration
|
||||
|
||||
Refer to Odoo settings for event_session. Configure related models, access rights, and options as needed.
|
||||
17
odoo-bringout-oca-event-event_session/doc/CONTROLLERS.md
Normal file
17
odoo-bringout-oca-event-event_session/doc/CONTROLLERS.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Controllers
|
||||
|
||||
HTTP routes provided by this module.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User/Client
|
||||
participant C as Module Controllers
|
||||
participant O as ORM/Views
|
||||
|
||||
U->>C: HTTP GET/POST (routes)
|
||||
C->>O: ORM operations, render templates
|
||||
O-->>U: HTML/JSON/PDF
|
||||
```
|
||||
|
||||
Notes
|
||||
- See files in controllers/ for route definitions.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Dependencies
|
||||
|
||||
This addon depends on:
|
||||
|
||||
- [event](../../odoo-bringout-oca-ocb-event)
|
||||
4
odoo-bringout-oca-event-event_session/doc/FAQ.md
Normal file
4
odoo-bringout-oca-event-event_session/doc/FAQ.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# FAQ
|
||||
|
||||
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
|
||||
- Q: How to enable? A: Start server with --addon event_session or install in UI.
|
||||
7
odoo-bringout-oca-event-event_session/doc/INSTALL.md
Normal file
7
odoo-bringout-oca-event-event_session/doc/INSTALL.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Install
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-event-event_session"
|
||||
# or
|
||||
uv pip install odoo-bringout-oca-event-event_session"
|
||||
```
|
||||
19
odoo-bringout-oca-event-event_session/doc/MODELS.md
Normal file
19
odoo-bringout-oca-event-event_session/doc/MODELS.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Models
|
||||
|
||||
Detected core models and extensions in event_session.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class event_mail_session
|
||||
class event_session
|
||||
class event_session_timeslot
|
||||
class event_event
|
||||
class event_mail
|
||||
class event_mail_registration
|
||||
class event_registration
|
||||
class event_type
|
||||
```
|
||||
|
||||
Notes
|
||||
- Classes show model technical names; fields omitted for brevity.
|
||||
- Items listed under _inherit are extensions of existing models.
|
||||
6
odoo-bringout-oca-event-event_session/doc/OVERVIEW.md
Normal file
6
odoo-bringout-oca-event-event_session/doc/OVERVIEW.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Overview
|
||||
|
||||
Packaged Odoo addon: event_session. Provides features documented in upstream Odoo 16 under this addon.
|
||||
|
||||
- Source: OCA/OCB 16.0, addon event_session
|
||||
- License: LGPL-3
|
||||
3
odoo-bringout-oca-event-event_session/doc/REPORTS.md
Normal file
3
odoo-bringout-oca-event-event_session/doc/REPORTS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Reports
|
||||
|
||||
This module does not define custom reports.
|
||||
41
odoo-bringout-oca-event-event_session/doc/SECURITY.md
Normal file
41
odoo-bringout-oca-event-event_session/doc/SECURITY.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Security
|
||||
|
||||
Access control and security definitions in event_session.
|
||||
|
||||
## Access Control Lists (ACLs)
|
||||
|
||||
Model access permissions defined in:
|
||||
- **[ir.model.access.csv](../event_session/security/ir.model.access.csv)**
|
||||
- 7 model access rules
|
||||
|
||||
## Record Rules
|
||||
|
||||
Row-level security rules defined in:
|
||||
|
||||
## Security Groups & Configuration
|
||||
|
||||
Security groups and permissions defined in:
|
||||
- **[security.xml](../event_session/security/security.xml)**
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Security Layers"
|
||||
A[Users] --> B[Groups]
|
||||
B --> C[Access Control Lists]
|
||||
C --> D[Models]
|
||||
B --> E[Record Rules]
|
||||
E --> F[Individual Records]
|
||||
end
|
||||
```
|
||||
|
||||
Security files overview:
|
||||
- **[ir.model.access.csv](../event_session/security/ir.model.access.csv)**
|
||||
- Model access permissions (CRUD rights)
|
||||
- **[security.xml](../event_session/security/security.xml)**
|
||||
- Security groups, categories, and XML-based rules
|
||||
|
||||
Notes
|
||||
- Access Control Lists define which groups can access which models
|
||||
- Record Rules provide row-level security (filter records by user/group)
|
||||
- Security groups organize users and define permission sets
|
||||
- All security is enforced at the ORM level by Odoo
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Troubleshooting
|
||||
|
||||
- Ensure Python and Odoo environment matches repo guidance.
|
||||
- Check database connectivity and logs if startup fails.
|
||||
- Validate that dependent addons listed in DEPENDENCIES.md are installed.
|
||||
7
odoo-bringout-oca-event-event_session/doc/USAGE.md
Normal file
7
odoo-bringout-oca-event-event_session/doc/USAGE.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Usage
|
||||
|
||||
Start Odoo including this addon (from repo root):
|
||||
|
||||
```bash
|
||||
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon event_session
|
||||
```
|
||||
3
odoo-bringout-oca-event-event_session/doc/WIZARDS.md
Normal file
3
odoo-bringout-oca-event-event_session/doc/WIZARDS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Wizards
|
||||
|
||||
This module does not include UI wizards.
|
||||
111
odoo-bringout-oca-event-event_session/event_session/README.rst
Normal file
111
odoo-bringout-oca-event-event_session/event_session/README.rst
Normal 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.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from . import controllers
|
||||
from . import models
|
||||
from . import wizards
|
||||
|
|
@ -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"],
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import main
|
||||
|
|
@ -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),
|
||||
],
|
||||
)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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" <%s>' % (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" <%s>' % (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="yyyyMMdd'T'HHmmss'Z'")"
|
||||
/>
|
||||
<t
|
||||
t-set="date_end"
|
||||
t-value="format_datetime(object.session_id.date_end, tz='UTC', dt_format="yyyyMMdd'T'HHmmss'Z'")"
|
||||
/>
|
||||
<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&text={{ object.session_id.name }}&dates={{ date_begin }}/{{ date_end }}&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&view=d&type=20&title={{ object.session_id.name }}&in_loc={{ location }}&st={{ format_datetime(object.session_id.date_begin, tz='UTC', dt_format='yyyyMMdd\'T\'HHmmss') }}&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&size=598x200&maptype=roadmap&format=png&visual_refresh=true&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" <%s>' % (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="yyyyMMdd'T'HHmmss'Z'")"
|
||||
/>
|
||||
<t
|
||||
t-set="date_end"
|
||||
t-value="format_datetime(object.session_id.date_end, tz='UTC', dt_format="yyyyMMdd'T'HHmmss'Z'")"
|
||||
/>
|
||||
<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&text={{ object.session_id.name }}&dates={{ date_begin }}/{{ date_end }}&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&view=d&type=20&title={{ object.session_id.name }}&in_loc={{ location }}&st={{ format_datetime(object.session_id.date_begin, tz='UTC', dt_format='yyyyMMdd\'T\'HHmmss') }}&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&size=598x200&maptype=roadmap&format=png&visual_refresh=true&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>
|
||||
|
|
@ -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>
|
||||
1963
odoo-bringout-oca-event-event_session/event_session/i18n/bs.po
Normal file
1963
odoo-bringout-oca-event-event_session/event_session/i18n/bs.po
Normal file
File diff suppressed because it is too large
Load diff
3122
odoo-bringout-oca-event-event_session/event_session/i18n/es.po
Normal file
3122
odoo-bringout-oca-event-event_session/event_session/i18n/es.po
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
4249
odoo-bringout-oca-event-event_session/event_session/i18n/fr.po
Normal file
4249
odoo-bringout-oca-event-event_session/event_session/i18n/fr.po
Normal file
File diff suppressed because it is too large
Load diff
2890
odoo-bringout-oca-event-event_session/event_session/i18n/it.po
Normal file
2890
odoo-bringout-oca-event-event_session/event_session/i18n/it.po
Normal file
File diff suppressed because it is too large
Load diff
2433
odoo-bringout-oca-event-event_session/event_session/i18n/nl.po
Normal file
2433
odoo-bringout-oca-event-event_session/event_session/i18n/nl.po
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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",
|
||||
)
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
This module allows to create sessions associated with events.
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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 |
|
|
@ -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&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 > 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 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 .</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 <<a class="reference external" href="mailto:ntsirintanis@therp.nl">ntsirintanis@therp.nl</a>></p>
|
||||
</li>
|
||||
<li><p class="first">David Alonso <<a class="reference external" href="mailto:david.alonso@solvos.es">david.alonso@solvos.es</a>></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 <<a class="reference external" href="mailto:ivan.todorovich@gmail.com">ivan.todorovich@gmail.com</a>></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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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"))
|
||||
|
|
@ -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)
|
||||
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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', '>=', 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', '<', 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', '>', 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import wizard_event_session
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
42
odoo-bringout-oca-event-event_session/pyproject.toml
Normal file
42
odoo-bringout-oca-event-event_session/pyproject.toml
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
[project]
|
||||
name = "odoo-bringout-oca-event-event_session"
|
||||
version = "16.0.0"
|
||||
description = "Event Sessions - Sessions in events"
|
||||
authors = [
|
||||
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
|
||||
]
|
||||
dependencies = [
|
||||
"odoo-bringout-oca-ocb-event>=16.0.0",
|
||||
"requests>=2.25.1"
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">= 3.11"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Office/Business",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://github.com/bringout/0"
|
||||
repository = "https://github.com/bringout/0"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.metadata]
|
||||
allow-direct-references = true
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["event_session"]
|
||||
|
||||
[tool.rye]
|
||||
managed = true
|
||||
dev-dependencies = [
|
||||
"pytest>=8.4.1",
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue