From 6268e3eab161844716fdc3bc898e658415240b73 Mon Sep 17 00:00:00 2001 From: David Vidal Date: Mon, 17 Apr 2017 18:55:00 +0200 Subject: [PATCH 01/24] [ADD] event_session: New module --- event_session/README.rst | 45 ++ event_session/__init__.py | 6 + event_session/__manifest__.py | 23 + event_session/i18n/es.po | 521 ++++++++++++++++++ event_session/models/__init__.py | 5 + event_session/models/event.py | 50 ++ event_session/models/event_mail.py | 75 +++ event_session/models/event_session.py | 195 +++++++ event_session/reports/__init__.py | 3 + .../reports/report_event_registration.py | 40 ++ .../report_event_registration_view.xml | 31 ++ event_session/security/ir.model.access.csv | 3 + event_session/tests/__init__.py | 3 + event_session/tests/test_session.py | 165 ++++++ event_session/views/event_session_view.xml | 142 +++++ event_session/views/event_view.xml | 59 ++ event_session/wizards/__init__.py | 3 + event_session/wizards/wizard_event_session.py | 253 +++++++++ .../wizards/wizard_event_session_view.xml | 73 +++ 19 files changed, 1695 insertions(+) create mode 100644 event_session/README.rst create mode 100644 event_session/__init__.py create mode 100644 event_session/__manifest__.py create mode 100644 event_session/i18n/es.po create mode 100644 event_session/models/__init__.py create mode 100644 event_session/models/event.py create mode 100644 event_session/models/event_mail.py create mode 100644 event_session/models/event_session.py create mode 100644 event_session/reports/__init__.py create mode 100644 event_session/reports/report_event_registration.py create mode 100644 event_session/reports/report_event_registration_view.xml create mode 100644 event_session/security/ir.model.access.csv create mode 100644 event_session/tests/__init__.py create mode 100644 event_session/tests/test_session.py create mode 100644 event_session/views/event_session_view.xml create mode 100644 event_session/views/event_view.xml create mode 100644 event_session/wizards/__init__.py create mode 100644 event_session/wizards/wizard_event_session.py create mode 100644 event_session/wizards/wizard_event_session_view.xml diff --git a/event_session/README.rst b/event_session/README.rst new file mode 100644 index 000000000..46dde9416 --- /dev/null +++ b/event_session/README.rst @@ -0,0 +1,45 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + + +Create and assign sessions to events +==================================== + +This module allows to create sessions associated with events. + +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 +====================== + +* TODO: Seats and registrations management +* TODO: Improve hours constraints + +Credits +======= + +Contributors +------------ + +* Sergio Teruel +* David Vidal + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +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. diff --git a/event_session/__init__.py b/event_session/__init__.py new file mode 100644 index 000000000..c4e9a8c2f --- /dev/null +++ b/event_session/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import wizards +from . import reports +from . import tests diff --git a/event_session/__manifest__.py b/event_session/__manifest__.py new file mode 100644 index 000000000..b8431e40c --- /dev/null +++ b/event_session/__manifest__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 David Vidal +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Event Sessions', + 'version': '10.0.1.0.0', + 'author': 'Tecnativa, ' + 'Odoo Community Association (OCA)', + "license": "AGPL-3", + 'website': 'https://odoo-community.org/', + 'category': 'Marketing', + 'summary': 'Sessions in events', + 'depends': ['event', 'event_mail'], + 'data': [ + 'security/ir.model.access.csv', + 'views/event_session_view.xml', + 'views/event_view.xml', + 'wizards/wizard_event_session_view.xml', + 'reports/report_event_registration_view.xml', + ], + 'installable': True, +} diff --git a/event_session/i18n/es.po b/event_session/i18n/es.po new file mode 100644 index 000000000..8e3f890f0 --- /dev/null +++ b/event_session/i18n/es.po @@ -0,0 +1,521 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * event_session +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-04-28 15:18+0000\n" +"PO-Revision-Date: 2017-04-28 17:40+0200\n" +"Last-Translator: David \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Language: es\n" +"X-Generator: Poedit 1.8.7.1\n" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_active +msgid "Active" +msgstr "Activo" + +#. module: event_session +#: model:ir.model,name:event_session.model_event_registration +msgid "Attendee" +msgstr "Asistente" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_registration_ids +#: model:ir.ui.view,arch_db:event_session.view_event_session_form +msgid "Attendees" +msgstr "Asistentes" + +#. module: event_session +#: model:ir.ui.view,arch_db:event_session.view_event_session_form +msgid "Attendees on this session" +msgstr "Asistentes a esta sesión" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_seats_available +msgid "Available Seats" +msgstr "Plazas disponibles en sesión" + +#. module: event_session +#: model:ir.ui.view,arch_db:event_session.generator_view_form +msgid "Cancel" +msgstr "Cancelar" + +#. module: event_session +#: model:ir.model.fields,help:event_session.field_wizard_event_session_delete_existing_sessions +msgid "Check in order to delete every previous session for this event" +msgstr "Seleccionar para borrar todas las sesiones existentes en este evento." + +#. module: event_session +#: model:ir.model.fields,help:event_session.field_wizard_event_session_fridays +msgid "Create sessions on Fridays" +msgstr "Crear sesiones los Viernes" + +#. module: event_session +#: model:ir.model.fields,help:event_session.field_wizard_event_session_mondays +msgid "Create sessions on Mondays" +msgstr "Crear sesiones los Lunes" + +#. module: event_session +#: model:ir.model.fields,help:event_session.field_wizard_event_session_saturdays +msgid "Create sessions on Saturdays" +msgstr "Crear sesiones los Sábados" + +#. module: event_session +#: model:ir.model.fields,help:event_session.field_wizard_event_session_sundays +msgid "Create sessions on Sundays" +msgstr "Crear sesiones los Domingos" + +#. module: event_session +#: model:ir.model.fields,help:event_session.field_wizard_event_session_thursdays +msgid "Create sessions on Thursdays" +msgstr "Create sessions on Jueves" + +#. module: event_session +#: model:ir.model.fields,help:event_session.field_wizard_event_session_tuesdays +msgid "Create sessions on Tuesdays" +msgstr "Create sessions on Martes" + +#. module: event_session +#: model:ir.model.fields,help:event_session.field_wizard_event_session_wednesdays +msgid "Create sessions on Wednesdays" +msgstr "Create sessions on Miércoles" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_create_uid +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_create_uid +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_hours_create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_create_date +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_create_date +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_hours_create_date +msgid "Created on" +msgstr "Creado el" + +#. module: event_session +#: model:ir.ui.view,arch_db:event_session.generator_view_form +msgid "Dates and event data" +msgstr "Datos de evento y fechas" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_delete_existing_sessions +msgid "Delete existing sessions" +msgstr "Borrar sesiones existentes" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_display_name +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_display_name +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_hours_display_name +msgid "Display Name" +msgstr "Nombre a mostrar" + +#. module: event_session +#: model:ir.ui.view,arch_db:event_session.view_event_session_form +msgid "Email Schedule" +msgstr "Programación de Correo Electrónico" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_event_date_end +msgid "End Date" +msgstr "Fecha finalización" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_end_time +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_hours_end_time +msgid "End time" +msgstr "Hora de fin" + +#. module: event_session +#: code:addons/event_session/models/event_session.py:184 +#, python-format +msgid "Ending and starting time can't be the same!" +msgstr "La hora de comienzo y de fin no puede ser la misma" + +#. module: event_session +#: model:ir.model,name:event_session.model_event_event +#: model:ir.model.fields,field_description:event_session.field_event_session_event_id +#: model:ir.ui.view,arch_db:event_session.view_session_search +msgid "Event" +msgstr "Evento" + +#. module: event_session +#: model:ir.ui.view,arch_db:event_session.view_event_session_form +msgid "Event Session" +msgstr "Sesión de evento" + +#. module: event_session +#: model:ir.ui.view,arch_db:event_session.view_session_search +msgid "Event Sesssion" +msgstr "Sesión de evento" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_event_id +msgid "Event id" +msgstr "Evento" + +#. module: event_session +#: model:ir.model,name:event_session.model_event_session +msgid "Event session" +msgstr "Sessión de evento" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_fridays +msgid "Fridays" +msgstr "Viernes" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_seats_available_pc +msgid "Full %" +msgstr "% de aforo" + +#. module: event_session +#: model:ir.ui.view,arch_db:event_session.session_view_event_form +msgid "Generate Sessions" +msgstr "Generar sesiones" + +#. module: event_session +#: model:ir.actions.act_window,name:event_session.act_wizard_event_session +msgid "Generate Sessions Wizard" +msgstr "Asistente para generar sesiones" + +#. module: event_session +#: model:ir.ui.view,arch_db:event_session.generator_view_form +msgid "Generate sessions" +msgstr "Generar sesiones" + +#. module: event_session +#: model:ir.ui.view,arch_db:event_session.view_session_search +msgid "Group By" +msgstr "Agrupar por" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_session_hour_ids +#: model:ir.ui.view,arch_db:event_session.generator_view_form +msgid "Hours" +msgstr "Horas" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_id +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_hours_id +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_id +msgid "ID" +msgstr "ID" + +#. module: event_session +#: model:ir.model.fields,help:event_session.field_wizard_event_session_name +msgid "It will be generated according to given parameters" +msgstr "Se generarán de acuerdo con los parámetros dados" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session___last_update +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session___last_update +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_hours___last_update +msgid "Last Modified on" +msgstr "Última modificación en" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_write_uid +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_hours_write_uid +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_write_date +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_hours_write_date +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_write_date +msgid "Last Updated on" +msgstr "Última actualización el" + +#. module: event_session +#: selection:event.session,seats_availability:0 +msgid "Limited" +msgstr "Limitados" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_event_mail_ids +msgid "Mail Schedule" +msgstr "Programación de correo" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_event_mail_template_id +msgid "Mail Template" +msgstr "Plantilla de programación de correo" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_seats_availability +msgid "Maximum Attendees" +msgstr "Asistentes máximos" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_seats_max +msgid "Maximum seats" +msgstr "Plazas máximas de sesión" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_seats_min +msgid "Minimum seats" +msgstr "Plazas mínimas" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_mondays +msgid "Mondays" +msgstr "Lunes" + +#. module: event_session +#: code:addons/event_session/models/event_session.py:166 +#, python-format +msgid "No more available seats for this session." +msgstr "No hay más plazas disponibles para esta sesión." + +#. module: event_session +#: code:addons/event_session/models/event.py:50 +#, python-format +msgid "No more seats available for this event." +msgstr "No hay más plazas disponibles para esta sesión." + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_seats_expected +msgid "Number of Expected Attendees" +msgstr "Número previsto de asistentes" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_seats_used +msgid "Number of Participants" +msgstr "Número de asistentes" + +#. module: event_session +#: model:ir.ui.view,arch_db:event_session.view_event_session_form +msgid "Origin" +msgstr "Origen" + +#. module: event_session +#: model:ir.ui.view,arch_db:event_session.generator_view_form +msgid "Other options" +msgstr "Otras opciones" + +#. module: event_session +#: model:ir.ui.view,arch_db:event_session.view_event_session_form +msgid "Partner" +msgstr "Empresa" + +#. module: event_session +#: model:ir.model,name:event_session.model_event_mail_registration +msgid "Registration Mail Scheduler" +msgstr "Registro de Programador de Correo Electrónico" + +#. module: event_session +#: model:ir.ui.view,arch_db:event_session.view_event_session_form +msgid "Registrations" +msgstr "Registros" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_seats_reserved +msgid "Reserved Seats" +msgstr "Plazas reservadas" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_saturdays +msgid "Saturdays" +msgstr "Sábados" + +#. module: event_session +#: model:ir.ui.view,arch_db:event_session.generator_view_form +msgid "Schedule" +msgstr "Programar" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_mail_scheduler_template_session_id_4282 +#: model:ir.model.fields,field_description:event_session.field_event_mail_session_id +#: model:ir.model.fields,field_description:event_session.field_event_registration_session_id +#: model:ir.model.fields,field_description:event_session.field_event_session_name +#: model:ir.model.fields,field_description:event_session.field_report_event_registration_session_id +#: model:ir.ui.view,arch_db:event_session.view_event_session_calendar +#: model:ir.ui.view,arch_db:event_session.view_report_event_registration_search +#: model:ir.ui.view,arch_db:event_session.view_session_search +msgid "Session" +msgstr "Sesión" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_date +msgid "Session date" +msgstr "Fecha de sesión" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_date_end +msgid "Session date end" +msgstr "Fecha de fin de sesión" + +#. module: event_session +#: code:addons/event_session/models/event_session.py:175 +#, python-format +msgid "Session date is out of this event dates range" +msgstr "La fecha de la sesión está fuera del rango de fechas de este evento" + +#. module: event_session +#: model:ir.model.fields,help:event_session.field_event_session_end_time +msgid "Session end time" +msgstr "Fecha de fin" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_name +msgid "Session info" +msgstr "Información de la sesión" + +#. module: event_session +#: model:ir.model.fields,help:event_session.field_event_session_start_time +msgid "Session start time" +msgstr "Hora de comienzo de la sesión" + +#. module: event_session +#: model:ir.actions.act_window,name:event_session.act_event_session_event_form +#: model:ir.actions.act_window,name:event_session.act_event_session_form +#: model:ir.model.fields,field_description:event_session.field_event_event_session_ids +#: model:ir.ui.menu,name:event_session.event_session_menu +#: model:ir.ui.view,arch_db:event_session.view_event_form +#: model:ir.ui.view,arch_db:event_session.view_event_session_tree +msgid "Sessions" +msgstr "Sesiones" + +#. module: event_session +#: model:ir.ui.view,arch_db:event_session.view_event_form +msgid "Sessions availables for this event" +msgstr "Sesiones disponibles para este evento" + +#. module: event_session +#: model:ir.model.fields,help:event_session.field_wizard_event_session_event_date_begin +msgid "Set it up in the event configurationSessions will be generated from this date" +msgstr "Establécela en la configuración del evento. Se generarán sesiones a partir de esta fecha." + +#. module: event_session +#: model:ir.model.fields,help:event_session.field_wizard_event_session_event_date_end +#: model:ir.model.fields,help:event_session.field_wizard_event_session_event_date_tz +msgid "Set it up in the event configurationSessions will be generated up to this date" +msgstr "Establécela en la configuración del evento. Se generarán sesiones a hasta esta fecha." + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_event_date_begin +msgid "Start Date" +msgstr "Fecha de inicio" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_start_time +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_hours_start_time +msgid "Start time" +msgstr "Hora de incio" + +#. module: event_session +#: model:ir.ui.view,arch_db:event_session.view_event_session_form +msgid "State" +msgstr "Estado" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_sundays +msgid "Sundays" +msgstr "Domingos" + +#. module: event_session +#: code:addons/event_session/wizards/wizard_event_session.py:86 +#: code:addons/event_session/wizards/wizard_event_session.py:91 +#, python-format +msgid "There are overlapping hours!" +msgstr "¡Hay horarios superpuestos!" + +#. module: event_session +#: code:addons/event_session/wizards/wizard_event_session.py:233 +#, python-format +msgid "There are sessions with no duration!" +msgstr "¡Hay horarios con duración nula!" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_thursdays +msgid "Thursdays" +msgstr "Jueves" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_event_date_tz +msgid "Timezone" +msgstr "Zona horaria" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_event_sessions_count +#: model:ir.model.fields,field_description:event_session.field_event_registration_event_sessions_count +msgid "Total event sessions" +msgstr "Sesiones de evento totales" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_tuesdays +msgid "Tuesdays" +msgstr "Martes" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_event_session_seats_unconfirmed +msgid "Unconfirmed Seat Reservations" +msgstr "Reservas de plazas no confirmadas" + +#. module: event_session +#: selection:event.session,seats_availability:0 +msgid "Unlimited" +msgstr "Ilimitados" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_wednesdays +msgid "Wednesdays" +msgstr "Miércoles" + +#. module: event_session +#: model:ir.ui.view,arch_db:event_session.generator_view_form +msgid "Weekdays" +msgstr "Días de la semana" + +#. module: event_session +#: model:ir.model.fields,field_description:event_session.field_wizard_event_session_hours_wizard_event_session_id +msgid "Wizard event session id" +msgstr "Wizard event session id" + +#. module: event_session +#: code:addons/event_session/wizards/wizard_event_session.py:210 +#, python-format +msgid "You must select at least one weekday" +msgstr "Debes seleccionar al menos un día de la semana" + +#. module: event_session +#: code:addons/event_session/wizards/wizard_event_session.py:241 +#, python-format +msgid "You've entered invalid hours!" +msgstr "¡Las horas introducidas son erróneas!" + +#. module: event_session +#: model:ir.model,name:event_session.model_event_mail +msgid "event.mail" +msgstr "event.mail" + +#. module: event_session +#: model:ir.ui.view,arch_db:event_session.generator_view_form +msgid "or" +msgstr "o" + +#. module: event_session +#: model:ir.model,name:event_session.model_report_event_registration +msgid "report.event.registration" +msgstr "report.event.registration" + +#. module: event_session +#: model:ir.model,name:event_session.model_wizard_event_session +msgid "wizard.event.session" +msgstr "wizard.event.session" + +#. module: event_session +#: model:ir.model,name:event_session.model_wizard_event_session_hours +msgid "wizard.event.session.hours" +msgstr "wizard.event.session.hours" diff --git a/event_session/models/__init__.py b/event_session/models/__init__.py new file mode 100644 index 000000000..d01dffd0a --- /dev/null +++ b/event_session/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from . import event_session +from . import event +from . import event_mail diff --git a/event_session/models/event.py b/event_session/models/event.py new file mode 100644 index 000000000..d8fcb83fc --- /dev/null +++ b/event_session/models/event.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 David Vidal +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class EventEvent(models.Model): + _inherit = 'event.event' + + session_ids = fields.One2many( + comodel_name='event.session', + inverse_name='event_id', + string='Sessions', + ) + sessions_count = fields.Integer( + compute='_compute_sessions_count', + string='Total event sessions', + ) + + @api.multi + def _compute_sessions_count(self): + for event in self: + event.sessions_count = len(event.session_ids) + + +class EventRegistration(models.Model): + _inherit = 'event.registration' + + event_sessions_count = fields.Integer( + related='event_id.sessions_count', + readonly=True, + ) + session_id = fields.Many2one( + comodel_name='event.session', + string='Session', + ondelete='set null', + ) + + @api.multi + @api.constrains('event_id', 'session_id', 'state') + def _check_seats_limit(self): + for registration in self: + if (registration.session_id.seats_availability == 'limited' and + self.session_id.seats_max and + self.session_id.seats_available < + (1 if self.state == 'draft' else 0)): + raise ValidationError( + _('No more seats available for this event.')) diff --git a/event_session/models/event_mail.py b/event_session/models/event_mail.py new file mode 100644 index 000000000..3364f9eda --- /dev/null +++ b/event_session/models/event_mail.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 David Vidal +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import datetime +from odoo import api, fields, models, tools +import logging + +_logger = logging.getLogger(__name__) + +try: + from odoo.addons.event.models.event_mail import _INTERVALS +except ImportError: + _logger.debug('Can not import events module.') + + +class EventMailScheduler(models.Model): + _inherit = 'event.mail' + + session_id = fields.Many2one( + comodel_name='event.session', + string='Session', + ondelete='cascade', + ) + + @api.multi + def _compute_done(self): + super(EventMailScheduler, self)._compute_done() + for event_mail in self: + if (event_mail.session_id and + event_mail.interval_type not in + ['before_event', 'after_event']): + event_mail.done = ( + True if event_mail.event_id.sessions_count > 0 and + not event_mail.session_id else + len(event_mail.mail_registration_ids) == len( + event_mail.session_id.registration_ids) and + all(line.mail_sent for line in + event_mail.mail_registration_ids) + ) + + @api.multi + def _compute_scheduled_date(self): + super(EventMailScheduler, self)._compute_scheduled_date() + for event_mail in self: + if event_mail.event_id.state in ['confirm', 'done'] and \ + event_mail.session_id: + if event_mail.interval_type == 'before_event': + date, sign = event_mail.session_id.date, -1 + else: + date, sign = event_mail.session_id.date_end, 1 + event_mail.scheduled_date = datetime.strptime( + date, tools.DEFAULT_SERVER_DATETIME_FORMAT) + _INTERVALS[ + event_mail.interval_unit](sign * event_mail.interval_nbr) + + +class EventMailRegistration(models.Model): + _inherit = 'event.mail.registration' + + @api.multi + @api.depends('registration_id', 'scheduler_id.interval_unit', + 'scheduler_id.interval_type') + def _compute_scheduled_date(self): + super(EventMailRegistration, self)._compute_scheduled_date() + for event_mail_reg in self: + if (event_mail_reg.registration_id and + event_mail_reg.registration_id.session_id): + date_open = event_mail_reg.registration_id.session_id.date + date_open_datetime = date_open and datetime.strptime( + date_open, tools.DEFAULT_SERVER_DATETIME_FORMAT + ) or fields.datetime.now() + event_mail_reg.scheduled_date = ( + date_open_datetime + + _INTERVALS[event_mail_reg.scheduler_id.interval_unit]( + event_mail_reg.scheduler_id.interval_nbr)) diff --git a/event_session/models/event_session.py b/event_session/models/event_session.py new file mode 100644 index 000000000..caff1c608 --- /dev/null +++ b/event_session/models/event_session.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 David Vidal +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class EventSession(models.Model): + _name = 'event.session' + _description = 'Event session' + + name = fields.Char( + string='Session', + required=True, + ) + active = fields.Boolean( + default=True, + ) + event_id = fields.Many2one( + comodel_name='event.event', + string='Event', + ) + seats_min = fields.Integer( + string='Minimum seats', + ) + seats_max = fields.Integer( + string="Maximum seats", + ) + seats_availability = fields.Selection( + [('limited', 'Limited'), ('unlimited', 'Unlimited')], + 'Maximum Attendees', required=True, default='unlimited', + ) + seats_reserved = fields.Integer( + string='Reserved Seats', store=True, readonly=True, + compute='_compute_seats', + ) + seats_available = fields.Integer( + oldname='register_avail', string='Available Seats', + store=True, readonly=True, compute='_compute_seats') + seats_unconfirmed = fields.Integer( + oldname='register_prospect', string='Unconfirmed Seat Reservations', + store=True, readonly=True, compute='_compute_seats') + seats_used = fields.Integer( + oldname='register_attended', string='Number of Participants', + store=True, readonly=True, compute='_compute_seats') + seats_expected = fields.Integer( + string='Number of Expected Attendees', + readonly=True, compute='_compute_seats') + date = fields.Datetime( + string="Session date", + required=True, + ) + date_end = fields.Datetime( + string="Session date end", + required=True, + ) + start_time = fields.Float( + required=True, + help="Session start time", + ) + end_time = fields.Float( + required=True, + help="Session end time", + ) + registration_ids = fields.One2many( + comodel_name='event.registration', + inverse_name='session_id', + string='Attendees', + state={'done': [('readonly', True)]}, + ) + event_mail_ids = fields.One2many( + comodel_name='event.mail', + inverse_name='session_id', + string='Mail Schedule', + copy=True) + + @api.model + def _set_session_mail_ids(self, event_id): + return [(0, 0, { + 'event_id': event_id, + 'interval_unit': 'now', + 'interval_type': 'after_sub', + 'template_id': self.env.ref('event.event_subscription').id + }), (0, 0, { + 'event_id': event_id, + 'interval_nbr': 2, + 'interval_unit': 'days', + 'interval_type': 'before_event', + 'template_id': self.env.ref('event.event_reminder').id + }), (0, 0, { + 'event_id': event_id, + 'interval_nbr': 15, + 'interval_unit': 'days', + 'interval_type': 'before_event', + 'template_id': self.env.ref('event.event_reminder').id + })] + + @api.multi + def name_get(self): + """Redefine the name_get method to show the event name with the event + session. + """ + res = [] + for item in self: + res.append((item.id, "[%s] %s" % (item.event_id.name, item.name))) + return res + + @api.model + def create(self, vals): + if 'event_mail_ids' not in vals: + vals.update({ + 'event_mail_ids': self._set_session_mail_ids(vals['event_id']) + }) + return super(EventSession, self).create(vals) + + @api.multi + @api.depends('seats_max', 'registration_ids.state') + def _compute_seats(self): + """Determine reserved, available, reserved but unconfirmed and used + seats by session. + """ + # initialize fields to 0 + for session in self: + session.seats_unconfirmed = session.seats_reserved = \ + session.seats_used = session.seats_available = 0 + # aggregate registrations by event session and by state + if self.ids: + state_field = { + 'draft': 'seats_unconfirmed', + 'open': 'seats_reserved', + 'done': 'seats_used', + } + query = """ + SELECT session_id, state, count(session_id) + FROM event_registration + WHERE session_id IN %s AND state IN ('draft', 'open', 'done') + GROUP BY session_id, state """ + self._cr.execute(query, (tuple(self.ids),)) + for session_id, state, num in self._cr.fetchall(): + session = self.browse(session_id) + session[state_field[state]] += num + # compute seats_available + for session in self: + if session.seats_max > 0: + session.seats_available = session.seats_max - ( + session.seats_reserved + session.seats_used) + session.seats_expected = ( + session.seats_unconfirmed + session.seats_reserved + + session.seats_used) + + @api.onchange('event_id') + def onchage_event_selection(self): + self.seats_min = self.event_id.seats_min + self.seats_max = self.event_id.seats_max + self.seats_availability = self.event_id.seats_availability + self.date = self.event_id.date_begin_located + + @api.multi + @api.constrains('seats_max', 'seats_available') + def _check_seats_limit(self): + for session in self: + if (session.seats_availability == 'limited' and + session.seats_max and session.seats_available < 0): + raise ValidationError( + _('No more available seats for this session.')) + + @api.multi + @api.constrains('date') + def _check_out_of_event_date_range(self): + for session in self: + if self.event_id.date_end_located < session.date or \ + session.date < self.event_id.date_begin_located: + raise ValidationError( + _("Session date is out of this event dates range") + ) + + @api.multi + @api.constrains('start_time', 'end_time') + def _check_zero_duration(self): + for session in self: + if session.end_time == session.start_time: + raise ValidationError( + _("Ending and starting time can't be the same!") + ) + + @api.multi + def button_open_registration(self): + """Opens session registrations""" + self.ensure_one() + action = self.env.ref( + 'event.act_event_registration_from_event').read()[0] + action['domain'] = [('id', 'in', self.registration_ids.ids)] + action['context'] = {} + return action diff --git a/event_session/reports/__init__.py b/event_session/reports/__init__.py new file mode 100644 index 000000000..d544480a3 --- /dev/null +++ b/event_session/reports/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import report_event_registration diff --git a/event_session/reports/report_event_registration.py b/event_session/reports/report_event_registration.py new file mode 100644 index 000000000..d0005e96d --- /dev/null +++ b/event_session/reports/report_event_registration.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# © 2016 Sergio Teruel +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import models, fields + + +class ReportEventRegistration(models.Model): + """Events Analysis""" + _inherit = "report.event.registration" + + session_id = fields.Many2one( + comodel_name='event.session', + string='Session', + required=True, + readonly=True, + ) + session_seats_max = fields.Integer( + string="Maximum session seats", readonly=True, group_operator="avg") + session_seats_available = fields.Integer( + string='Available session Seats', readonly=True, group_operator="avg") + + def _select(self): + select_str = super(ReportEventRegistration, self)._select() + return select_str + """ + , MIN(r.session_id) AS session_id, + MIN(es.seats_max) AS session_seats_max, + MIN(es.seats_available) AS session_seats_available + """ + + def _from(self): + from_str = super(ReportEventRegistration, self)._from() + from_str += """ + LEFT JOIN event_session es ON r.session_id = es.id + """ + return from_str + + def _group_by(self): + group_by_str = super(ReportEventRegistration, self)._group_by() + return group_by_str + ", r.session_id" diff --git a/event_session/reports/report_event_registration_view.xml b/event_session/reports/report_event_registration_view.xml new file mode 100644 index 000000000..3dcc08389 --- /dev/null +++ b/event_session/reports/report_event_registration_view.xml @@ -0,0 +1,31 @@ + + + + + + report.event.registration + + + + + + + + + + + + report.event.registration + + + + + + + + + + + + diff --git a/event_session/security/ir.model.access.csv b/event_session/security/ir.model.access.csv new file mode 100644 index 000000000..26b364c5d --- /dev/null +++ b/event_session/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_event_session_user,event.session.user,event_session.model_event_session,event.group_event_user,1,0,0,0 +access_event_session_admin,event.session.admin,event_session.model_event_session,event.group_event_manager,1,1,1,1 diff --git a/event_session/tests/__init__.py b/event_session/tests/__init__.py new file mode 100644 index 000000000..e7f04fd77 --- /dev/null +++ b/event_session/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import test_session diff --git a/event_session/tests/test_session.py b/event_session/tests/test_session.py new file mode 100644 index 000000000..1cc3436e8 --- /dev/null +++ b/event_session/tests/test_session.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Tecnativa - David Vidal +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl-3.0). + +from odoo.tests import common +from odoo.exceptions import ValidationError +from datetime import datetime, timedelta + + +class EventSession(common.SavepointCase): + + @classmethod + def setUpClass(cls): + super(EventSession, cls).setUpClass() + cls.event = cls.env['event.event'].create({ + 'name': 'Test event', + 'date_begin': datetime.today(), + 'date_end': datetime.today() + timedelta(days=7), + 'seats_availability': 'limited', + 'seats_max': '5', + 'seats_min': '1', + }) + cls.session = cls.env['event.session'].create({ + 'name': 'Test session', + 'date': datetime.today() + timedelta(days=1), + 'date_end': datetime.today() + timedelta(days=1), + 'event_id': cls.event.id, + 'start_time': 20.0, + 'end_time': 21.5, + 'seats_availability': cls.event.seats_availability, + 'seats_max': cls.event.seats_max, + 'seats_min': cls.event.seats_min, + }) + cls.attendee = cls.env['event.registration'].create({ + 'name': 'Test attendee', + 'event_id': cls.event.id, + 'session_id': cls.session.id, + }) + cls.wizard = cls.env['wizard.event.session'].create({ + 'event_id': cls.event.id, + 'mondays': True, + 'tuesdays': True, + 'wednesdays': True, + 'thursdays': True, + 'fridays': True, + 'sundays': True, + 'saturdays': True, + 'delete_existing_sessions': False, + 'session_hour_ids': [ + (0, 0, {'start_time': 20.0, 'end_time': 21.0}), + ], + }) + cls.template = cls.env['event.mail.template'].create({ + 'name': 'Template test 01', + 'scheduler_template_ids': [(0, 0, { + 'interval_nbr': 15, + 'interval_unit': 'days', + 'interval_type': 'before_event', + 'template_id': cls.env.ref('event.event_reminder').id})], + }) + + def test_session_methods(self): + """ Session methods """ + self.assertEqual( + # name_get method + self.session.name_get()[0][1], + '[' + self.event.name + '] ' + self.session.name + ) + with self.assertRaises(ValidationError), self.cr.savepoint(): + # out of range begining date + self.session.update({ + 'date': datetime.strptime( + self.event.date_begin, '%Y-%m-%d %H:%M:%S' + ) - timedelta(days=1), + }) + with self.assertRaises(ValidationError), self.cr.savepoint(): + # out of range begining date + self.session.update({ + 'date': datetime.strptime( + self.event.date_end, '%Y-%m-%d %H:%M:%S' + ) + timedelta(days=1), + }) + with self.assertRaises(ValidationError), self.cr.savepoint(): + # zero duration + self.session.update({ + 'start_time': 20.0, + 'end_time': 20.0, + }) + # registrations button + res = self.session.button_open_registration() + attendees = self.env['event.registration'].search([ + ['session_id', '=', self.session.id] + ]) + self.assertEqual( + res['domain'], + [('id', 'in', attendees.ids)] + ) + # assign mail templates + self.session._set_session_mail_ids(self.event.id) + self.assertEqual(len(self.session.event_mail_ids), 3) + self.session._set_session_mail_ids(self.template) + self.assertEqual(len(self.session.event_mail_ids), 3) + + def test_session_seats(self): + """ Session seat """ + self.assertEqual( + self.event.seats_available, + self.session.seats_available) + self.assertEqual( + self.event.seats_unconfirmed, + self.session.seats_unconfirmed + ) + self.assertEqual( + self.event.seats_used, + self.session.seats_used + ) + with self.assertRaises(ValidationError), self.cr.savepoint(): + # check limit regs + for i in range(int(self.session.seats_available)+1): + self.env['event.registration'].create({ + 'name': 'Test Attendee', + 'event_id': self.event.id, + 'session_id': self.session.id, + }) + + def test_wizard(self): + """Test Session Generation Wizard""" + self.wizard.action_generate_sessions() + # delete previous sessions + self.wizard.update({'delete_existing_sessions': True}) + self.wizard.update({'event_mail_template_id': self.template}) + self.wizard.action_generate_sessions() + sessions = self.env['event.session'].search([ + ['event_id', '=', self.event.id] + ]) + for session in sessions: + self.assertTrue(session.event_mail_ids) + self.assertEqual(session.seats_max, self.event.seats_max) + self.assertEqual(session.seats_availability, + self.event.seats_availability) + with self.assertRaises(ValidationError), self.cr.savepoint(): + # session duration = 0 + self.wizard.update({'session_hour_ids': [ + (0, 0, {'start_time': 20.0, 'end_time': 20.0}), + ], + }) + with self.assertRaises(ValidationError), self.cr.savepoint(): + # schedules overlap + self.wizard.update({'session_hour_ids': [ + (0, 0, {'start_time': 20.0, 'end_time': 21.0}), + (0, 0, {'start_time': 20.5, 'end_time': 21.5}), + ], + }) + with self.assertRaises(ValidationError), self.cr.savepoint(): + # weekday not set + self.wizard.update({ + 'mondays': False, + 'tuesdays': False, + 'wednesdays': False, + 'thursdays': False, + 'fridays': False, + 'sundays': False, + 'saturdays': False, + }) + self.wizard.action_generate_sessions() diff --git a/event_session/views/event_session_view.xml b/event_session/views/event_session_view.xml new file mode 100644 index 000000000..9ce2bba16 --- /dev/null +++ b/event_session/views/event_session_view.xml @@ -0,0 +1,142 @@ + + + + + event.session.search + event.session + + + + + + + + + + + + + event.session.form + event.session + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + event.session.tree + event.session + + + + + + + + + + + + + + + + + + + event.session.calendar + event.session + + + + + + + + + + + event.session + form + Sessions + tree,form,calendar + + + + + + +
diff --git a/event_session/views/event_view.xml b/event_session/views/event_view.xml new file mode 100644 index 000000000..b1abcea9e --- /dev/null +++ b/event_session/views/event_view.xml @@ -0,0 +1,59 @@ + + + + + event.registration + + + + + + + + + + + event.registration + + + + + + + + + + + event.session + form + Sessions + tree,form,calendar + {'search_default_event_id': active_id, 'default_event_id': active_id} + + + + event.event + + + + + + + + + + + + + diff --git a/event_session/wizards/__init__.py b/event_session/wizards/__init__.py new file mode 100644 index 000000000..f23e87e25 --- /dev/null +++ b/event_session/wizards/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import wizard_event_session diff --git a/event_session/wizards/wizard_event_session.py b/event_session/wizards/wizard_event_session.py new file mode 100644 index 000000000..55c6447c2 --- /dev/null +++ b/event_session/wizards/wizard_event_session.py @@ -0,0 +1,253 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 David Vidal +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError +from datetime import datetime, timedelta +from time import strftime, strptime +from locale import setlocale, LC_ALL + + +class WizardEventSession(models.TransientModel): + _name = "wizard.event.session" + + name = fields.Char( + "Session info", + required=True, + help="It will be generated according to given parameters", + default='/', + ) + event_id = fields.Many2one( + comodel_name="event.event", + readonly=True, + default=lambda self: self.env.context["active_id"], + required=True, + ) + event_date_begin = fields.Datetime( + related="event_id.date_begin", + readonly=True, + help="Set it up in the event configuration" + "Sessions will be generated from this date", + ) + event_date_end = fields.Datetime( + related="event_id.date_end", + readonly=True, + help="Set it up in the event configuration" + "Sessions will be generated up to this date", + ) + event_date_tz = fields.Selection( + related="event_id.date_tz", + readonly=True, + help="Set it up in the event configuration" + "Sessions will be generated up to this date", + ) + mondays = fields.Boolean( + help="Create sessions on Mondays", + ) + tuesdays = fields.Boolean( + help="Create sessions on Tuesdays", + ) + wednesdays = fields.Boolean( + help="Create sessions on Wednesdays", + ) + thursdays = fields.Boolean( + help="Create sessions on Thursdays", + ) + fridays = fields.Boolean( + help="Create sessions on Fridays", + ) + saturdays = fields.Boolean( + help="Create sessions on Saturdays", + ) + sundays = fields.Boolean( + help="Create sessions on Sundays", + ) + delete_existing_sessions = fields.Boolean( + help="Check in order to delete every previous session for this event" + ) + session_hour_ids = fields.One2many( + comodel_name='wizard.event.session.hours', + inverse_name='wizard_event_session_id', + string='Hours', + ) + event_mail_template_id = fields.Many2one( + comodel_name='event.mail.template', + string='Mail Schedule Template', + ) + + @api.multi + @api.constrains('session_hour_ids') + def _avoid_overlapping_hours(self): + for hour_a in self.session_hour_ids: + for hour_b in self.session_hour_ids: + if hour_a != hour_b: + if hour_a.start_time == hour_b.start_time: + raise ValidationError( + _("There are overlapping hours!") + ) + elif hour_b.start_time < \ + hour_a.start_time < hour_b.end_time: + raise ValidationError( + _("There are overlapping hours!") + ) + + @api.multi + def weekdays(self): + return (self.mondays, + self.tuesdays, + self.wednesdays, + self.thursdays, + self.fridays, + self.saturdays, + self.sundays) + + @api.multi + def datetime_fields(self): + """Fields converted to Python's Datetime-based objects.""" + result = { + "event_start": fields.Datetime.from_string(self.event_date_begin), + "event_end": fields.Datetime.from_string(self.event_date_end), + "day_delta": timedelta(days=1), + } + return result + + @api.multi + def existing_sessions(self, date): + """Return existing sessions that match some criteria.""" + # Todo: Improve match + return self.env["event.session"].search( + [("event_id", "=", self.event_id.id), + ("date", "=", date), + ("start_time", "=", self.start_time)], + ) + + def _get_session_mail_template(self, mail_template): + vals = [(6, 0, [])] + if isinstance(mail_template, int): + mail_template = self.env['event.mail.template'].browse( + mail_template) + for scheduler in mail_template.scheduler_template_ids: + vals.append((0, 0, { + 'event_id': self.event_id.id, + 'interval_nbr': scheduler.interval_nbr, + 'interval_unit': scheduler.interval_unit, + 'interval_type': scheduler.interval_type, + 'template_id': scheduler.template_id.id, + })) + return vals + + @api.multi + def create_session(self, **values): + """Create a new session record with the provided values.""" + setlocale(LC_ALL, locale=(self.env.lang, 'UTF-8')) + data = { + "name": "{} {} - {}".format( + strftime('%A %d/%m/%y', + strptime(values["date"], "%Y-%m-%d %H:%M:%S")), + "%02d:%02d" % divmod(values["start_time"]*60, 60), + "%02d:%02d" % divmod(values["end_time"]*60, 60), + ).capitalize(), + "event_id": self.event_id.id, + "date_end": '%s : %s' % ( + strftime('%Y-%m-%d', strptime( + values["date"], "%Y-%m-%d %H:%M:%S")), + "%02d:%02d" % divmod(values["end_time"]*60, 60)), + "start_time": values["start_time"], + "end_time": values["end_time"], + "seats_min": self.event_id.seats_min, + "seats_max": self.event_id.seats_max, + "seats_availability": self.event_id.seats_availability, + } + mail_template = (self.event_mail_template_id or + self.event_id._default_event_mail_template_id()) + if mail_template: + data['event_mail_ids'] = self._get_session_mail_template( + mail_template) + else: + data['event_mail_ids'] = [] + + data.update(values) + return self.env["event.session"].create(data) + + @api.multi + def generate_sessions(self): + self.ensure_one() + counter = 0 + dt = self.datetime_fields() + weekdays = self.weekdays() + current = dt["event_start"] + while current <= dt["event_end"]: + for hour in self.session_hour_ids: + tm = hour.time_fields() + current_start = datetime.combine( + current.date(), + tm["start_time"].time() + ) + if (current_start >= dt["event_start"] and + weekdays[current.weekday()]): + current_end = datetime.combine( + current.date(), + tm["end_time"].time() + ) + if current_end <= dt["event_end"]: + current_start = \ + fields.Datetime.to_string(current_start) + # TODO: Check that no session exists with this data + self.create_session( + date=current_start, + start_time=hour.start_time, + end_time=hour.end_time, + ) + counter += 1 + # Next day + current += dt["day_delta"] + + @api.multi + def action_generate_sessions(self): + """Here's where magic is triggered""" + weekdays = self.weekdays() + if not any(weekdays): + raise ValidationError(_("You must select at least one weekday")) + if self.delete_existing_sessions: + self.event_id.session_ids.unlink() + self.generate_sessions() + + +class WizardEventSessionHours(models.TransientModel): + _name = "wizard.event.session.hours" + + wizard_event_session_id = fields.Many2one( + comodel_name='wizard.event.session' + ) + start_time = fields.Float(required=True) + end_time = fields.Float(required=True) + + # Todo: manage multiday sessions + + @api.multi + @api.constrains('start_time', 'end_time') + def _check_zero_duration(self): + for hour in self: + if hour.start_time == hour.end_time: + raise ValidationError( + _("There are sessions with no duration!") + ) + + @api.multi + @api.constrains('start_time', 'end_time') + def _check_hour_validity(self): + for hour in self: + if hour.start_time > 23.99 or hour.end_time > 23.99: + raise ValidationError( + _("You've entered invalid hours!") + ) + + @api.multi + def time_fields(self): + """Format hours""" + result = { + "start_time": datetime.min + timedelta(hours=self.start_time), + "end_time": datetime.min + timedelta(hours=self.end_time), + } + return result diff --git a/event_session/wizards/wizard_event_session_view.xml b/event_session/wizards/wizard_event_session_view.xml new file mode 100644 index 000000000..4a48085e3 --- /dev/null +++ b/event_session/wizards/wizard_event_session_view.xml @@ -0,0 +1,73 @@ + + + + + + Generate Sessions Wizard + wizard.event.session + form + form + new + + + + event.event + + + + + + {'readonly':[('sessions_count', '<', 1)]} + - + + event.event + + + + + + + diff --git a/event_session/wizards/__init__.py b/event_session/wizards/__init__.py index f23e87e25..5a6bb71ae 100644 --- a/event_session/wizards/__init__.py +++ b/event_session/wizards/__init__.py @@ -1,3 +1 @@ -# -*- coding: utf-8 -*- - from . import wizard_event_session diff --git a/event_session/wizards/wizard_event_session.py b/event_session/wizards/wizard_event_session.py index bdabd697a..312145c44 100644 --- a/event_session/wizards/wizard_event_session.py +++ b/event_session/wizards/wizard_event_session.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- # Copyright 2017 David Vidal # Copyright 2017 Tecnativa - Pedro M. Baeza -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import _, api, fields, models from odoo.exceptions import ValidationError @@ -119,15 +118,11 @@ def _prepare_session_values(self, date_begin, date_end): "event_id": self.event_id.id, "date_begin": fields.Datetime.to_string(date_begin), "date_end": fields.Datetime.to_string(date_end), - "seats_min": self.event_id.seats_min, - "seats_max": self.event_id.seats_max, - "seats_availability": self.event_id.seats_availability, } mail_template = ( self.event_mail_template_id or - self.env['ir.values'].get_default( - 'event.config.settings', 'event_mail_template_id')) - + self.env['ir.default'].get( + 'res.config.settings', 'event_mail_template_id')) if mail_template: template_values = \ self.env['event.session']._session_mails_from_template( From 9b16866d29a05398631aa41897f1522990da9134 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 20 Mar 2019 17:39:09 +0100 Subject: [PATCH 10/24] [12.0] event_session: Migration to 12.0 --- event_session/README.rst | 72 ++- event_session/__manifest__.py | 6 +- event_session/models/event_mail.py | 17 - event_session/readme/CONTRIBUTORS.rst | 6 + event_session/readme/DESCRIPTION.rst | 1 + event_session/readme/USAGE.rst | 5 + event_session/security/ir.model.access.csv | 1 - event_session/static/description/index.html | 434 ++++++++++++++++++ event_session/tests/test_session.py | 41 +- event_session/views/event_view.xml | 2 +- event_session/wizards/wizard_event_session.py | 4 +- 11 files changed, 518 insertions(+), 71 deletions(-) create mode 100644 event_session/readme/CONTRIBUTORS.rst create mode 100644 event_session/readme/DESCRIPTION.rst create mode 100644 event_session/readme/USAGE.rst create mode 100644 event_session/static/description/index.html diff --git a/event_session/README.rst b/event_session/README.rst index f9d80111c..587a26165 100644 --- a/event_session/README.rst +++ b/event_session/README.rst @@ -1,13 +1,37 @@ -.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html - :alt: License: AGPL-3 +============== +Event Sessions +============== -==================================== -Create and assign sessions to events -==================================== +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/12.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-12-0/event-12-0-event_session + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/199/12.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| This module allows to create sessions associated with events. +**Table of contents** + +.. contents:: + :local: + Usage ===== @@ -20,32 +44,44 @@ You can either: Bug Tracker =========== -Bugs are tracked on `GitHub Issues -`_. In case of trouble, please -check there if your issue has already been reported. If you spotted it first, -help us smash it by providing detailed and welcomed feedback. +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. Credits ======= +Authors +~~~~~~~ + +* Tecnativa + Contributors ------------- +~~~~~~~~~~~~ + +* `Tecnativa `__: + + * Sergio Teruel + * David Vidal -* Sergio Teruel -* David Vidal * Nikos Tsirintanis -Maintainer ----------- +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. .. image:: https://odoo-community.org/logo.png :alt: Odoo Community Association :target: https://odoo-community.org -This module is maintained by the OCA. - 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. -To contribute to this module, please visit https://odoo-community.org. +This module is part of the `OCA/event `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/event_session/__manifest__.py b/event_session/__manifest__.py index 98e245b5e..f6291a70a 100644 --- a/event_session/__manifest__.py +++ b/event_session/__manifest__.py @@ -1,16 +1,16 @@ -# Copyright 2017 David Vidal +# Copyright 2017-19 David Vidal # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). { 'name': 'Event Sessions', - 'version': '11.0.1.0.0', + 'version': '12.0.1.0.0', 'author': 'Tecnativa, ' 'Odoo Community Association (OCA)', "license": "AGPL-3", 'website': 'https://github.com/oca/event.git', 'category': 'Marketing', 'summary': 'Sessions in events', - 'depends': ['event'], + 'depends': ['event_mail'], 'data': [ 'security/ir.model.access.csv', 'security/event_session_security.xml', diff --git a/event_session/models/event_mail.py b/event_session/models/event_mail.py index 3366e920b..4e0f10f0b 100644 --- a/event_session/models/event_mail.py +++ b/event_session/models/event_mail.py @@ -89,20 +89,3 @@ def _compute_scheduled_date(self): date_open_datetime + _INTERVALS[event_mail_reg.scheduler_id.interval_unit]( event_mail_reg.scheduler_id.interval_nbr)) - - -class EventMailTemplate(models.Model): - _name = 'event.mail.template' - - @api.model - def _default_scheduler_template_ids(self): - return self.env['event.type'].with_context( - by_pass_config_template=True)._get_default_event_type_mail_ids() - - name = fields.Char() - scheduler_template_ids = fields.One2many( - comodel_name='event.mail', - inverse_name='event_mail_template_id', - string='Mail Schedule', - default=_default_scheduler_template_ids, - ) diff --git a/event_session/readme/CONTRIBUTORS.rst b/event_session/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..f8a18cec9 --- /dev/null +++ b/event_session/readme/CONTRIBUTORS.rst @@ -0,0 +1,6 @@ +* `Tecnativa `__: + + * Sergio Teruel + * David Vidal + +* Nikos Tsirintanis diff --git a/event_session/readme/DESCRIPTION.rst b/event_session/readme/DESCRIPTION.rst new file mode 100644 index 000000000..e148c1672 --- /dev/null +++ b/event_session/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module allows to create sessions associated with events. diff --git a/event_session/readme/USAGE.rst b/event_session/readme/USAGE.rst new file mode 100644 index 000000000..5c70c2d18 --- /dev/null +++ b/event_session/readme/USAGE.rst @@ -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. diff --git a/event_session/security/ir.model.access.csv b/event_session/security/ir.model.access.csv index 9ec0f59eb..26b364c5d 100644 --- a/event_session/security/ir.model.access.csv +++ b/event_session/security/ir.model.access.csv @@ -1,4 +1,3 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_event_session_user,event.session.user,event_session.model_event_session,event.group_event_user,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_template,access_event_mail_template,model_event_mail_template,base.group_user,1,0,0,0 diff --git a/event_session/static/description/index.html b/event_session/static/description/index.html new file mode 100644 index 000000000..51563719a --- /dev/null +++ b/event_session/static/description/index.html @@ -0,0 +1,434 @@ + + + + + + +Event Sessions + + + +
+

Event Sessions

+ + +

Beta License: AGPL-3 OCA/event Translate me on Weblate Try me on Runbot

+

This module allows to create sessions associated with events.

+

Table of contents

+ +
+

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.
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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 project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/event_session/tests/test_session.py b/event_session/tests/test_session.py index 315c124f0..8f593ce8a 100644 --- a/event_session/tests/test_session.py +++ b/event_session/tests/test_session.py @@ -1,9 +1,9 @@ -# Copyright 2017 Tecnativa - David Vidal +# Copyright 2017-19 Tecnativa - David Vidal # Copyright 2017 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl-3.0). from dateutil.relativedelta import relativedelta -from odoo import _, fields +from odoo import _ from odoo.tests import common from odoo.exceptions import ValidationError @@ -214,36 +214,17 @@ def test_wizard(self): def test_event_mail_compute_scheduled_date(self): self.assertFalse(self.scheduler.scheduled_date) self.scheduler.event_id.update({'state': 'confirm'}) - date = fields.Datetime.from_string( - self.scheduler.session_id.create_date - ) + relativedelta(hours=+1) - self.assertEqual( - self.scheduler.scheduled_date, - fields.Datetime.to_string(date) - ) + date = self.scheduler.session_id.create_date + relativedelta(hours=+1) + self.assertEqual(self.scheduler.scheduled_date, date) self.scheduler.update({'interval_type': 'before_event'}) - date = fields.Datetime.from_string( - self.scheduler.session_id.date_begin - ) + relativedelta(hours=-1) - self.assertEqual( - self.scheduler.scheduled_date, - fields.Datetime.to_string(date) - ) + date = self.scheduler.session_id.date_begin + relativedelta(hours=-1) + self.assertEqual(self.scheduler.scheduled_date, date) self.scheduler.update({'interval_type': 'after_event'}) - date = fields.Datetime.from_string( - self.scheduler.session_id.date_end - ) + relativedelta(hours=+1) - self.assertEqual( - self.scheduler.scheduled_date, - fields.Datetime.to_string(date) - ) + date = self.scheduler.session_id.date_end + relativedelta(hours=+1) + self.assertEqual(self.scheduler.scheduled_date, date) def test_event_mail_registration_compute_scheduled_date(self): self.scheduler.update({'interval_unit': 'days'}) - date = fields.Datetime.from_string( - self.mail_registration.registration_id.date_open - ) + relativedelta(days=+1) - self.assertEqual( - self.mail_registration.scheduled_date, - fields.Datetime.to_string(date) - ) + date = (self.mail_registration.registration_id.date_open + + relativedelta(days=+1)) + self.assertEqual(self.mail_registration.scheduled_date, date) diff --git a/event_session/views/event_view.xml b/event_session/views/event_view.xml index 39add992e..a015ffcbe 100644 --- a/event_session/views/event_view.xml +++ b/event_session/views/event_view.xml @@ -31,7 +31,7 @@ event.session form Sessions - tree,form,calendar + tree,form,calendar,pivot {'search_default_event_id': active_id, 'default_event_id': active_id} diff --git a/event_session/wizards/wizard_event_session.py b/event_session/wizards/wizard_event_session.py index 312145c44..ea37bf440 100644 --- a/event_session/wizards/wizard_event_session.py +++ b/event_session/wizards/wizard_event_session.py @@ -10,6 +10,7 @@ class WizardEventSession(models.TransientModel): _name = "wizard.event.session" + _description = "Wizard for ease sessions creation" name = fields.Char( "Session info", @@ -71,7 +72,7 @@ class WizardEventSession(models.TransientModel): ) event_mail_template_id = fields.Many2one( comodel_name='event.mail.template', - string='Mail Schedule Template', + string='Mail Schedule', ) @api.multi @@ -181,6 +182,7 @@ def action_generate_sessions(self): class WizardEventSessionHours(models.TransientModel): _name = "wizard.event.session.hours" + _description = "Hours in wich the sessions will run" wizard_event_session_id = fields.Many2one( comodel_name='wizard.event.session' From c110dd991a4cbb0021a549a92b8af8ad041fbca9 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 10 May 2019 13:48:21 +0000 Subject: [PATCH 11/24] [ADD] icon.png --- event_session/static/description/icon.png | Bin 0 -> 9455 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 event_session/static/description/icon.png diff --git a/event_session/static/description/icon.png b/event_session/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z z?n?eq1XP>p-IM$Z^C;2L3itnbJZAip*Zo0aw2bs8@(s^~*8T9go!%dHcAz2lM;`yp zD=7&xjFV$S&5uDaiScyD?B-i1ze`+CoRtz`Wn+Zl&#s4&}MO{@N!ufrzjG$B79)Y2d3tBk&)TxUTw@QS0TEL_?njX|@vq?Uz(nBFK5Pq7*xj#u*R&i|?7+6# z+|r_n#SW&LXhtheZdah{ZVoqwyT{D>MC3nkFF#N)xLi{p7J1jXlmVeb;cP5?e(=f# zuT7fvjSbjS781v?7{)-X3*?>tq?)Yd)~|1{BDS(pqC zC}~H#WXlkUW*H5CDOo<)#x7%RY)A;ShGhI5s*#cRDA8YgqG(HeKDx+#(ZQ?386dv! zlXCO)w91~Vw4AmOcATuV653fa9R$fyK8ul%rG z-wfS zihugoZyr38Im?Zuh6@RcF~t1anQu7>#lPpb#}4cOA!EM11`%f*07RqOVkmX{p~KJ9 z^zP;K#|)$`^Rb{rnHGH{~>1(fawV0*Z#)}M`m8-?ZJV<+e}s9wE# z)l&az?w^5{)`S(%MRzxdNqrs1n*-=jS^_jqE*5XDrA0+VE`5^*p3CuM<&dZEeCjoz zR;uu_H9ZPZV|fQq`Cyw4nscrVwi!fE6ciMmX$!_hN7uF;jjKG)d2@aC4ropY)8etW=xJvni)8eHi`H$%#zn^WJ5NLc-rqk|u&&4Z6fD_m&JfSI1Bvb?b<*n&sfl0^t z=HnmRl`XrFvMKB%9}>PaA`m-fK6a0(8=qPkWS5bb4=v?XcWi&hRY?O5HdulRi4?fN zlsJ*N-0Qw+Yic@s0(2uy%F@ib;GjXt01Fmx5XbRo6+n|pP(&nodMoap^z{~q ziEeaUT@Mxe3vJSfI6?uLND(CNr=#^W<1b}jzW58bIfyWTDle$mmS(|x-0|2UlX+9k zQ^EX7Nw}?EzVoBfT(-LT|=9N@^hcn-_p&sqG z&*oVs2JSU+N4ZD`FhCAWaS;>|wH2G*Id|?pa#@>tyxX`+4HyIArWDvVrX)2WAOQff z0qyHu&-S@i^MS-+j--!pr4fPBj~_8({~e1bfcl0wI1kaoN>mJL6KUPQm5N7lB(ui1 zE-o%kq)&djzWJ}ob<-GfDlkB;F31j-VHKvQUGQ3sp`CwyGJk_i!y^sD0fqC@$9|jO zOqN!r!8-p==F@ZVP=U$qSpY(gQ0)59P1&t@y?5rvg<}E+GB}26NYPp4f2YFQrQtot5mn3wu_qprZ=>Ig-$ zbW26Ws~IgY>}^5w`vTB(G`PTZaDiGBo5o(tp)qli|NeV( z@H_=R8V39rt5J5YB2Ky?4eJJ#b`_iBe2ot~6%7mLt5t8Vwi^Jy7|jWXqa3amOIoRb zOr}WVFP--DsS`1WpN%~)t3R!arKF^Q$e12KEqU36AWwnCBICpH4XCsfnyrHr>$I$4 z!DpKX$OKLWarN7nv@!uIA+~RNO)l$$w}p(;b>mx8pwYvu;dD_unryX_NhT8*Tj>BTrTTL&!?O+%Rv;b?B??gSzdp?6Uug9{ zd@V08Z$BdI?fpoCS$)t4mg4rT8Q_I}h`0d-vYZ^|dOB*Q^S|xqTV*vIg?@fVFSmMpaw0qtTRbx} z({Pg?#{2`sc9)M5N$*N|4;^t$+QP?#mov zGVC@I*lBVrOU-%2y!7%)fAKjpEFsgQc4{amtiHb95KQEwvf<(3T<9-Zm$xIew#P22 zc2Ix|App^>v6(3L_MCU0d3W##AB0M~3D00EWoKZqsJYT(#@w$Y_H7G22M~ApVFTRHMI_3be)Lkn#0F*V8Pq zc}`Cjy$bE;FJ6H7p=0y#R>`}-m4(0F>%@P|?7fx{=R^uFdISRnZ2W_xQhD{YuR3t< z{6yxu=4~JkeA;|(J6_nv#>Nvs&FuLA&PW^he@t(UwFFE8)|a!R{`E`K`i^ZnyE4$k z;(749Ix|oi$c3QbEJ3b~D_kQsPz~fIUKym($a_7dJ?o+40*OLl^{=&oq$<#Q(yyrp z{J-FAniyAw9tPbe&IhQ|a`DqFTVQGQ&Gq3!C2==4x{6EJwiPZ8zub-iXoUtkJiG{} zPaR&}_fn8_z~(=;5lD-aPWD3z8PZS@AaUiomF!G8I}Mf>e~0g#BelA-5#`cj;O5>N Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 From ae2f3310852760af3b2ef1c0a5df90aec8dc9f90 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 29 Jul 2019 02:53:26 +0000 Subject: [PATCH 12/24] [UPD] README.rst --- event_session/static/description/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/event_session/static/description/index.html b/event_session/static/description/index.html index 51563719a..f0ed08b90 100644 --- a/event_session/static/description/index.html +++ b/event_session/static/description/index.html @@ -3,7 +3,7 @@ - + Event Sessions