diff --git a/event_sale_reservation/README.rst b/event_sale_reservation/README.rst new file mode 100644 index 000000000..8ba8e72e1 --- /dev/null +++ b/event_sale_reservation/README.rst @@ -0,0 +1,176 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +======================= +Sell event reservations +======================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:36881c01853b670ba930e05e772258702f94e0a4f25ccceea8ce95094f1dbf2c + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Mature-brightgreen.png + :target: https://odoo-community.org/page/development-status + :alt: Mature +.. |badge2| image:: https://img.shields.io/badge/license-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/19.0/event_sale_reservation + :alt: OCA/event +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/event-19-0/event-19-0-event_sale_reservation + :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=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the functionality of event_sale to support selling +reservations of events that still don't exist and to allow you to +schedule the creation of events based on how many reservations already +exist. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To make use of this module, a user needs these minimal permissions: + +- Sales / User: Own Documents Only +- Events / User + +Usage +===== + +To know how many reservations exist for a given event type: + +1. Go to *Events > Configuration > Event Templates* and pick or create + one. +2. There's a new smart button called *Reserved seats* with that count. +3. Click on it to get to the sales orders where the seats got reserved. + +But that counter will be probably zero when you install, so let's see +how to increase it. + +To create an event reservation product: + +1. Go to *Sales > Products > Products*. +2. Create one and set *Product Type* to *Event Reservation*. +3. Select one *Event type for reservations*. +4. Save. + +From now on, you can sell event reservations for that event type. To do +it: + +1. Go to *Sales > Orders > Quotations*. +2. Create one. +3. Set its basic info (customer, date...) and go to *Order lines* tab. +4. Click *Add a product*. +5. Select the event reservation product you created above. +6. Set its info (quantity, price...). +7. Save that line and the quotation. + +At this point, the reservation is not yet confirmed, so if you go to the +event type, the smart button will still count zero. + +To confirm those reservations: + +1. Go to the quotation you just created (if you are not there yet). +2. Click on *Confirm*. + +Now, if you go to the event type form, the smart button will indicate +how many reserved seats exist. + +If you want to convert those reservations into real event registrations: + +1. Go to the quotation you just created (if you are not there yet). +2. Click on *Register in event*. +3. In the wizard you see, set the *Event* and *Event Ticket* for all the + order lines you want to convert into registrations. +4. If there is any line you still don't want to convert, remove it from + the wizard. +5. Click on *Next*. +6. A new wizard will appear, where you will be able to specify the name, + email and phone of each one of the attendees. If you don't do it, + they will get that info from the sales order customer. +7. After that's done, click on *Apply*. + +At this point, the sales order lines will be modified to include the +ticket product instead of the reservation product, and the event +reservations have been created, linked to those lines. + +If the event is set to autoconfirmation, the registrations are +confirmed., otherwise, they are kept as draft until an invoice is +created for the sales order, and paid. But that is just upstream +``event_sale`` module in action. + +Known issues / Roadmap +====================== + +Some addons (event_registration_multi_qty + +event_sale_registration_multi_qty) makes totals wrong because they +depend currently on count and not sum of qtys; integrating with them +would require a glue module. + +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 to smash 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 `__: + + - Jairo Llopis + - Pilar Vargas + - Frederic Grall + +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. + +.. |maintainer-pilarvargas-tecnativa| image:: https://github.com/pilarvargas-tecnativa.png?size=40px + :target: https://github.com/pilarvargas-tecnativa + :alt: pilarvargas-tecnativa + +Current `maintainer `__: + +|maintainer-pilarvargas-tecnativa| + +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_sale_reservation/__init__.py b/event_sale_reservation/__init__.py new file mode 100644 index 000000000..8d855c85d --- /dev/null +++ b/event_sale_reservation/__init__.py @@ -0,0 +1,3 @@ +from . import models +from . import reports +from . import wizards diff --git a/event_sale_reservation/__manifest__.py b/event_sale_reservation/__manifest__.py new file mode 100644 index 000000000..2c3c96de4 --- /dev/null +++ b/event_sale_reservation/__manifest__.py @@ -0,0 +1,26 @@ +# Copyright 2021 Tecnativa - Jairo Llopis +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Sell event reservations", + "summary": "Allow selling event registrations before the event exists", + "version": "19.0.1.0.0", + "development_status": "Mature", + "category": "Marketing", + "website": "https://github.com/OCA/event", + "author": "Tecnativa, Odoo Community Association (OCA)", + "maintainers": ["pilarvargas-tecnativa"], + "license": "AGPL-3", + "application": False, + "installable": True, + "depends": [ + "event_sale", + ], + "data": [ + "reports/sale_report_view.xml", + "wizards/registration_editor_view.xml", + "views/event_type_view.xml", + "views/product_template_view.xml", + "views/sale_order_view.xml", + ], +} diff --git a/event_sale_reservation/exceptions.py b/event_sale_reservation/exceptions.py new file mode 100644 index 000000000..582465cd2 --- /dev/null +++ b/event_sale_reservation/exceptions.py @@ -0,0 +1,12 @@ +# Copyright 2021 Tecnativa - Jairo Llopis +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import exceptions + + +class TicketAndReservationError(exceptions.ValidationError): + pass + + +class ReservationWithoutEventTypeError(exceptions.ValidationError): + pass diff --git a/event_sale_reservation/i18n/es.po b/event_sale_reservation/i18n/es.po new file mode 100644 index 000000000..65ba8ee4c --- /dev/null +++ b/event_sale_reservation/i18n/es.po @@ -0,0 +1,229 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * event_sale_reservation +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-02-13 10:08+0000\n" +"PO-Revision-Date: 2023-10-29 23:37+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: \n" +"Language: es\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" +"X-Generator: Weblate 4.17\n" + +#. module: event_sale_reservation +#: model:ir.model.fields,help:event_sale_reservation.field_product_product__type +#: model:ir.model.fields,help:event_sale_reservation.field_product_template__type +msgid "" +"A storable product is a product for which you manage stock. The Inventory " +"app has to be installed.\n" +"A consumable product is a product for which stock is not managed.\n" +"A service is a non-material product you provide." +msgstr "" +"Un producto almacenable es un producto para el que se gestionan existencias. " +"La aplicación Inventario debe estar instalada.\n" +"Un producto consumible es un producto para el que no se gestionan " +"existencias.\n" +"Un servicio es un producto no material que usted proporciona." + +#. module: event_sale_reservation +#. odoo-python +#: code:addons/event_sale_reservation/models/sale_order.py:0 +#, python-format +msgid "Attendees" +msgstr "Asistentes" + +#. module: event_sale_reservation +#: model_terms:ir.ui.view,arch_db:event_sale_reservation.registration_editor_reservations_view_form +msgid "Cancel" +msgstr "Cancelar" + +#. module: event_sale_reservation +#: model_terms:ir.ui.view,arch_db:event_sale_reservation.registration_editor_reservations_view_form +msgid "Configure registrations" +msgstr "Configurar registros" + +#. module: event_sale_reservation +#: model_terms:ir.ui.view,arch_db:event_sale_reservation.registration_editor_reservations_view_form +msgid "Convert pending event reservations into registrations for" +msgstr "Convertir las reservas a eventos que estén pendientes de" + +#. module: event_sale_reservation +#: model:ir.model.fields,help:event_sale_reservation.field_sale_order_line__event_registration_count +msgid "Count of event registrations related to this sale order line" +msgstr "" +"Cuenta de registros a eventos relacionados con esta línea de pedido de venta" + +#. module: event_sale_reservation +#: model:ir.model,name:event_sale_reservation.model_registration_editor +msgid "Edit Attendee Details on Sales Confirmation" +msgstr "Editar detalles del asistente al confirmar la venta" + +#. module: event_sale_reservation +#: model:ir.model,name:event_sale_reservation.model_registration_editor_line +msgid "Edit Attendee Line on Sales Confirmation" +msgstr "Editar la línea del asistente al confirmar la venta" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_sale_order_line__event_registration_count +msgid "Event Registration Count" +msgstr "Cuenta de registros a eventos" + +#. module: event_sale_reservation +#: model:ir.model.fields.selection,name:event_sale_reservation.selection__product_template__type__event_reservation +msgid "Event Reservation" +msgstr "Reserva de Evento" + +#. module: event_sale_reservation +#: model_terms:ir.ui.view,arch_db:event_sale_reservation.view_order_product_search +msgid "Event Reservation Type" +msgstr "Categoría de evento de la reserva" + +#. module: event_sale_reservation +#: model:ir.model,name:event_sale_reservation.model_event_type +msgid "Event Template" +msgstr "Plantilla de producto" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_sale_order__event_registration_count +#: model:ir.model.fields,field_description:event_sale_reservation.field_sale_order_line__event_registration_ids +msgid "Event registrations" +msgstr "Registros a eventos" + +#. module: event_sale_reservation +#: model:ir.model.fields,help:event_sale_reservation.field_sale_order_line__event_registration_ids +msgid "Event registrations related to this sale order line" +msgstr "Registros a eventos relacionados con esta línea de pedido de venta" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_sale_report__event_reservation_type_id +msgid "Event reservation type" +msgstr "Categoría de evento de reserva" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_product_product__event_reservation_type_id +#: model:ir.model.fields,field_description:event_sale_reservation.field_product_template__event_reservation_type_id +#: model:ir.model.fields,field_description:event_sale_reservation.field_registration_editor_line__event_reservation_type_id +#: model:ir.model.fields,field_description:event_sale_reservation.field_sale_order_line__event_reservation_type_id +msgid "Event type for reservations" +msgstr "Categoría de evento para las reservas" + +#. module: event_sale_reservation +#: model_terms:ir.ui.view,arch_db:event_sale_reservation.registration_editor_reservations_view_form +msgid "" +"If there is any line from that order that you still do\n" +" not want to convert into real event registrations, you\n" +" can remove it from the list below. You will be able to\n" +" repeat this process later for those lines." +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model.fields,help:event_sale_reservation.field_sale_order__event_registration_count +msgid "Indicates how many event registrations are linked to this order." +msgstr "" +"Indica cuántas reservas a eventos están vinculadas a este pedido de venta." + +#. module: event_sale_reservation +#: model:ir.model.fields,help:event_sale_reservation.field_sale_order__event_reservations_pending +msgid "" +"Indicates how many event reservations are still not linked to any " +"registration." +msgstr "" +"Indica cuántas reservas a eventos todavía no están vinculadas a ningún " +"registro." + +#. module: event_sale_reservation +#: model_terms:ir.ui.view,arch_db:event_sale_reservation.registration_editor_reservations_view_form +msgid "Next" +msgstr "Siguiente" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_sale_order__event_reservations_pending +msgid "Pending event reservations" +msgstr "Reservas pendientes a eventos" + +#. module: event_sale_reservation +#: model:ir.model,name:event_sale_reservation.model_product_template +msgid "Product" +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_product_product__type +#: model:ir.model.fields,field_description:event_sale_reservation.field_product_template__type +msgid "Product Type" +msgstr "Tipo de producto" + +#. module: event_sale_reservation +#: model:ir.actions.act_window,name:event_sale_reservation.action_registration_editor_reservations +#: model_terms:ir.ui.view,arch_db:event_sale_reservation.view_sale_order_form_inherit_event +msgid "Register in event" +msgstr "Registrar en evento" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_event_type__reserved_sale_order_line_ids +msgid "Reserved sale order lines" +msgstr "Líneas reservadas para pedidos de venta" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_event_type__seats_reservation_total +msgid "Reserved seats" +msgstr "Plazas reservadas" + +#. module: event_sale_reservation +#: model:ir.model,name:event_sale_reservation.model_sale_report +msgid "Sales Analysis Report" +msgstr "Informe de análisis de ventas" + +#. module: event_sale_reservation +#: model:ir.model,name:event_sale_reservation.model_sale_order +msgid "Sales Order" +msgstr "Pedido de venta" + +#. module: event_sale_reservation +#: model:ir.model,name:event_sale_reservation.model_sale_order_line +msgid "Sales Order Line" +msgstr "Línea de pedido de venta" + +#. module: event_sale_reservation +#: model:ir.model.fields,help:event_sale_reservation.field_event_type__seats_reservation_total +msgid "Seats reserved for events of this type." +msgstr "Plazas reservadas para eventos de esta categoría." + +#. module: event_sale_reservation +#: model:ir.model.fields,help:event_sale_reservation.field_product_product__event_reservation_type_id +#: model:ir.model.fields,help:event_sale_reservation.field_product_template__event_reservation_type_id +#: model:ir.model.fields,help:event_sale_reservation.field_registration_editor_line__event_reservation_type_id +#: model:ir.model.fields,help:event_sale_reservation.field_sale_order_line__event_reservation_type_id +msgid "Type of events that can be reserved by buying this product" +msgstr "Categoría de eventos que se pueden reservar al comprar este producto" + +#. module: event_sale_reservation +#. odoo-python +#: code:addons/event_sale_reservation/models/product_template.py:0 +#, python-format +msgid "You must indicate event type for %(name)s." +msgstr "Debe indicar la categoría de evento para %(name)s." + +#~ msgid "" +#~ "If there is any line from that order that you still do\n" +#~ " not want to convert into real event " +#~ "registrations, you\n" +#~ " can remove it from the list below. You will be " +#~ "able to\n" +#~ " repeat this process later for those lines." +#~ msgstr "" +#~ "Si hay alguna línea de ese pedido que todavía\n" +#~ " no desee convertir en registros de evento " +#~ "reales,\n" +#~ " puede borrarla de la lista abajo. Podrá\n" +#~ " repetir este proceso más adelante para esas " +#~ "líneas." + +#~ msgid "Product Template" +#~ msgstr "Plantilla de producto" diff --git a/event_sale_reservation/i18n/event_sale_reservation.pot b/event_sale_reservation/i18n/event_sale_reservation.pot new file mode 100644 index 000000000..88d23ade6 --- /dev/null +++ b/event_sale_reservation/i18n/event_sale_reservation.pot @@ -0,0 +1,197 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * event_sale_reservation +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: event_sale_reservation +#: model:ir.model.fields,help:event_sale_reservation.field_product_product__type +#: model:ir.model.fields,help:event_sale_reservation.field_product_template__type +msgid "" +"A storable product is a product for which you manage stock. The Inventory app has to be installed.\n" +"A consumable product is a product for which stock is not managed.\n" +"A service is a non-material product you provide." +msgstr "" + +#. module: event_sale_reservation +#. odoo-python +#: code:addons/event_sale_reservation/models/sale_order.py:0 +#, python-format +msgid "Attendees" +msgstr "" + +#. module: event_sale_reservation +#: model_terms:ir.ui.view,arch_db:event_sale_reservation.registration_editor_reservations_view_form +msgid "Cancel" +msgstr "" + +#. module: event_sale_reservation +#: model_terms:ir.ui.view,arch_db:event_sale_reservation.registration_editor_reservations_view_form +msgid "Configure registrations" +msgstr "" + +#. module: event_sale_reservation +#: model_terms:ir.ui.view,arch_db:event_sale_reservation.registration_editor_reservations_view_form +msgid "Convert pending event reservations into registrations for" +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model.fields,help:event_sale_reservation.field_sale_order_line__event_registration_count +msgid "Count of event registrations related to this sale order line" +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model,name:event_sale_reservation.model_registration_editor +msgid "Edit Attendee Details on Sales Confirmation" +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model,name:event_sale_reservation.model_registration_editor_line +msgid "Edit Attendee Line on Sales Confirmation" +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_sale_order_line__event_registration_count +msgid "Event Registration Count" +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model.fields.selection,name:event_sale_reservation.selection__product_template__type__event_reservation +msgid "Event Reservation" +msgstr "" + +#. module: event_sale_reservation +#: model_terms:ir.ui.view,arch_db:event_sale_reservation.view_order_product_search +msgid "Event Reservation Type" +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model,name:event_sale_reservation.model_event_type +msgid "Event Template" +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_sale_order__event_registration_count +#: model:ir.model.fields,field_description:event_sale_reservation.field_sale_order_line__event_registration_ids +msgid "Event registrations" +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model.fields,help:event_sale_reservation.field_sale_order_line__event_registration_ids +msgid "Event registrations related to this sale order line" +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_sale_report__event_reservation_type_id +msgid "Event reservation type" +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_product_product__event_reservation_type_id +#: model:ir.model.fields,field_description:event_sale_reservation.field_product_template__event_reservation_type_id +#: model:ir.model.fields,field_description:event_sale_reservation.field_registration_editor_line__event_reservation_type_id +#: model:ir.model.fields,field_description:event_sale_reservation.field_sale_order_line__event_reservation_type_id +msgid "Event type for reservations" +msgstr "" + +#. module: event_sale_reservation +#: model_terms:ir.ui.view,arch_db:event_sale_reservation.registration_editor_reservations_view_form +msgid "" +"If there is any line from that order that you still do\n" +" not want to convert into real event registrations, you\n" +" can remove it from the list below. You will be able to\n" +" repeat this process later for those lines." +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model.fields,help:event_sale_reservation.field_sale_order__event_registration_count +msgid "Indicates how many event registrations are linked to this order." +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model.fields,help:event_sale_reservation.field_sale_order__event_reservations_pending +msgid "" +"Indicates how many event reservations are still not linked to any " +"registration." +msgstr "" + +#. module: event_sale_reservation +#: model_terms:ir.ui.view,arch_db:event_sale_reservation.registration_editor_reservations_view_form +msgid "Next" +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_sale_order__event_reservations_pending +msgid "Pending event reservations" +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model,name:event_sale_reservation.model_product_template +msgid "Product" +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_product_product__type +#: model:ir.model.fields,field_description:event_sale_reservation.field_product_template__type +msgid "Product Type" +msgstr "" + +#. module: event_sale_reservation +#: model:ir.actions.act_window,name:event_sale_reservation.action_registration_editor_reservations +#: model_terms:ir.ui.view,arch_db:event_sale_reservation.view_sale_order_form_inherit_event +msgid "Register in event" +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_event_type__reserved_sale_order_line_ids +msgid "Reserved sale order lines" +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_event_type__seats_reservation_total +msgid "Reserved seats" +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model,name:event_sale_reservation.model_sale_report +msgid "Sales Analysis Report" +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model,name:event_sale_reservation.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model,name:event_sale_reservation.model_sale_order_line +msgid "Sales Order Line" +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model.fields,help:event_sale_reservation.field_event_type__seats_reservation_total +msgid "Seats reserved for events of this type." +msgstr "" + +#. module: event_sale_reservation +#: model:ir.model.fields,help:event_sale_reservation.field_product_product__event_reservation_type_id +#: model:ir.model.fields,help:event_sale_reservation.field_product_template__event_reservation_type_id +#: model:ir.model.fields,help:event_sale_reservation.field_registration_editor_line__event_reservation_type_id +#: model:ir.model.fields,help:event_sale_reservation.field_sale_order_line__event_reservation_type_id +msgid "Type of events that can be reserved by buying this product" +msgstr "" + +#. module: event_sale_reservation +#. odoo-python +#: code:addons/event_sale_reservation/models/product_template.py:0 +#, python-format +msgid "You must indicate event type for %(name)s." +msgstr "" diff --git a/event_sale_reservation/i18n/it.po b/event_sale_reservation/i18n/it.po new file mode 100644 index 000000000..b65a2fca5 --- /dev/null +++ b/event_sale_reservation/i18n/it.po @@ -0,0 +1,234 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * event_sale_reservation +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-12-20 18:06+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.6.2\n" + +#. module: event_sale_reservation +#: model:ir.model.fields,help:event_sale_reservation.field_product_product__type +#: model:ir.model.fields,help:event_sale_reservation.field_product_template__type +msgid "" +"A storable product is a product for which you manage stock. The Inventory " +"app has to be installed.\n" +"A consumable product is a product for which stock is not managed.\n" +"A service is a non-material product you provide." +msgstr "" +"Un prodotto stoccabile è un prodotto per il quale si gestiscono le giacenze. " +"Deve essere installata l'app Magazzino.\n" +"Un prodotto consumabile è un prodotto per il quale non sono gestite le " +"giacenze.\n" +"Un servizio è un prodotto non materiale che viene fornito." + +#. module: event_sale_reservation +#. odoo-python +#: code:addons/event_sale_reservation/models/sale_order.py:0 +#, python-format +msgid "Attendees" +msgstr "Partecipanti" + +#. module: event_sale_reservation +#: model_terms:ir.ui.view,arch_db:event_sale_reservation.registration_editor_reservations_view_form +msgid "Cancel" +msgstr "Annulla" + +#. module: event_sale_reservation +#: model_terms:ir.ui.view,arch_db:event_sale_reservation.registration_editor_reservations_view_form +msgid "Configure registrations" +msgstr "Configura registrazioni" + +#. module: event_sale_reservation +#: model_terms:ir.ui.view,arch_db:event_sale_reservation.registration_editor_reservations_view_form +msgid "Convert pending event reservations into registrations for" +msgstr "Convertire registrazioni evento in attesa in una registrazione per" + +#. module: event_sale_reservation +#: model:ir.model.fields,help:event_sale_reservation.field_sale_order_line__event_registration_count +msgid "Count of event registrations related to this sale order line" +msgstr "" +"Conteggio delle registrazioni evento relative a questa riga ordine vendita" + +#. module: event_sale_reservation +#: model:ir.model,name:event_sale_reservation.model_registration_editor +msgid "Edit Attendee Details on Sales Confirmation" +msgstr "Modifica dettagli partecipante alla conferma di vendita" + +#. module: event_sale_reservation +#: model:ir.model,name:event_sale_reservation.model_registration_editor_line +msgid "Edit Attendee Line on Sales Confirmation" +msgstr "Modifica riga partecipante alla conferma di vendita" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_sale_order_line__event_registration_count +msgid "Event Registration Count" +msgstr "Conteggio iscrizione evento" + +#. module: event_sale_reservation +#: model:ir.model.fields.selection,name:event_sale_reservation.selection__product_template__type__event_reservation +msgid "Event Reservation" +msgstr "Prenotazione evento" + +#. module: event_sale_reservation +#: model_terms:ir.ui.view,arch_db:event_sale_reservation.view_order_product_search +msgid "Event Reservation Type" +msgstr "Tipo prenotazione evento" + +#. module: event_sale_reservation +#: model:ir.model,name:event_sale_reservation.model_event_type +msgid "Event Template" +msgstr "Modello evento" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_sale_order__event_registration_count +#: model:ir.model.fields,field_description:event_sale_reservation.field_sale_order_line__event_registration_ids +msgid "Event registrations" +msgstr "Registrazioni evento" + +#. module: event_sale_reservation +#: model:ir.model.fields,help:event_sale_reservation.field_sale_order_line__event_registration_ids +msgid "Event registrations related to this sale order line" +msgstr "Registrazioni evento relative a questa riga ordine vendita" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_sale_report__event_reservation_type_id +msgid "Event reservation type" +msgstr "Tipo prenotazione evento" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_product_product__event_reservation_type_id +#: model:ir.model.fields,field_description:event_sale_reservation.field_product_template__event_reservation_type_id +#: model:ir.model.fields,field_description:event_sale_reservation.field_registration_editor_line__event_reservation_type_id +#: model:ir.model.fields,field_description:event_sale_reservation.field_sale_order_line__event_reservation_type_id +msgid "Event type for reservations" +msgstr "Tipo evento per prenotazioni" + +#. module: event_sale_reservation +#: model_terms:ir.ui.view,arch_db:event_sale_reservation.registration_editor_reservations_view_form +msgid "" +"If there is any line from that order that you still do\n" +" not want to convert into real event registrations, you\n" +" can remove it from the list below. You will be able to\n" +" repeat this process later for those lines." +msgstr "" +"Se esiste una riga di quell'ordine di vendita che non si\n" +" vuole ancora convertire in una reale registrazione " +"evento,\n" +" la si può rimuovere dall'elenco seguente. Si potrà " +"ripetere\n" +" successivamente questo processo per quelle righe." + +#. module: event_sale_reservation +#: model:ir.model.fields,help:event_sale_reservation.field_sale_order__event_registration_count +msgid "Indicates how many event registrations are linked to this order." +msgstr "Indica quante registrazioni evento sono collegate a questo ordine." + +#. module: event_sale_reservation +#: model:ir.model.fields,help:event_sale_reservation.field_sale_order__event_reservations_pending +msgid "" +"Indicates how many event reservations are still not linked to any " +"registration." +msgstr "" +"Indica quante prenotazioni evento non sono ancora collegate ad una " +"registrazione." + +#. module: event_sale_reservation +#: model_terms:ir.ui.view,arch_db:event_sale_reservation.registration_editor_reservations_view_form +msgid "Next" +msgstr "Successiva" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_sale_order__event_reservations_pending +msgid "Pending event reservations" +msgstr "Prenotazioni evento in attesa" + +#. module: event_sale_reservation +#: model:ir.model,name:event_sale_reservation.model_product_template +msgid "Product" +msgstr "Prodotto" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_product_product__type +#: model:ir.model.fields,field_description:event_sale_reservation.field_product_template__type +msgid "Product Type" +msgstr "Tipologia prodotto" + +#. module: event_sale_reservation +#: model:ir.actions.act_window,name:event_sale_reservation.action_registration_editor_reservations +#: model_terms:ir.ui.view,arch_db:event_sale_reservation.view_sale_order_form_inherit_event +msgid "Register in event" +msgstr "Registrare in un evento" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_event_type__reserved_sale_order_line_ids +msgid "Reserved sale order lines" +msgstr "Righe ordine vendita prenotate" + +#. module: event_sale_reservation +#: model:ir.model.fields,field_description:event_sale_reservation.field_event_type__seats_reservation_total +msgid "Reserved seats" +msgstr "Posti prenotati" + +#. module: event_sale_reservation +#: model:ir.model,name:event_sale_reservation.model_sale_report +msgid "Sales Analysis Report" +msgstr "Resoconto analisi vendite" + +#. module: event_sale_reservation +#: model:ir.model,name:event_sale_reservation.model_sale_order +msgid "Sales Order" +msgstr "Ordine di vendita" + +#. module: event_sale_reservation +#: model:ir.model,name:event_sale_reservation.model_sale_order_line +msgid "Sales Order Line" +msgstr "Riga ordine di vendita" + +#. module: event_sale_reservation +#: model:ir.model.fields,help:event_sale_reservation.field_event_type__seats_reservation_total +msgid "Seats reserved for events of this type." +msgstr "Posti prenotati per eventi di questo tipo." + +#. module: event_sale_reservation +#: model:ir.model.fields,help:event_sale_reservation.field_product_product__event_reservation_type_id +#: model:ir.model.fields,help:event_sale_reservation.field_product_template__event_reservation_type_id +#: model:ir.model.fields,help:event_sale_reservation.field_registration_editor_line__event_reservation_type_id +#: model:ir.model.fields,help:event_sale_reservation.field_sale_order_line__event_reservation_type_id +msgid "Type of events that can be reserved by buying this product" +msgstr "" +"Tipi di eventi che possono essere prenotati acquistando questo prodotto" + +#. module: event_sale_reservation +#. odoo-python +#: code:addons/event_sale_reservation/models/product_template.py:0 +#, python-format +msgid "You must indicate event type for %(name)s." +msgstr "Bisogna indicare il tipo evento per %(name)s." + +#~ msgid "" +#~ "If there is any line from that order that you still do\n" +#~ " not want to convert into real event " +#~ "registrations, you\n" +#~ " can remove it from the list below. You will be " +#~ "able to\n" +#~ " repeat this process later for those lines." +#~ msgstr "" +#~ "Se esiste una riga di quell'ordine di vendita che non si\n" +#~ " vuole ancora convertire in una reale " +#~ "registrazione evento,\n" +#~ " la si può rimuovere dall'elenco seguente. Si " +#~ "potrà ripetere\n" +#~ " successivamente questo processo per quelle righe." + +#~ msgid "Product Template" +#~ msgstr "Modello prodotto" diff --git a/event_sale_reservation/models/__init__.py b/event_sale_reservation/models/__init__.py new file mode 100644 index 000000000..f3fcd22cf --- /dev/null +++ b/event_sale_reservation/models/__init__.py @@ -0,0 +1,4 @@ +from . import event_type +from . import product_template +from . import sale_order +from . import sale_order_line diff --git a/event_sale_reservation/models/event_type.py b/event_sale_reservation/models/event_type.py new file mode 100644 index 000000000..7170012ce --- /dev/null +++ b/event_sale_reservation/models/event_type.py @@ -0,0 +1,58 @@ +# Copyright 2021 Tecnativa - Jairo Llopis +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class EventType(models.Model): + _inherit = "event.type" + + reserved_sale_order_line_ids = fields.One2many( + string="Reserved sale order lines", + comodel_name="sale.order.line", + inverse_name="event_reservation_type_id", + ) + seats_reservation_total = fields.Integer( + string="Reserved seats", + compute="_compute_reservations_total", + store=True, + help="Seats reserved for events of this type.", + ) + + def _seats_reservation_domain(self): + """Domain to select sale.order.line with pending reservations.""" + return [ + ("event_reservation_type_id", "in", self.ids), + ("order_id.state", "in", ("sale", "done")), + ("product_id.type", "=", "event_reservation"), + ] + + @api.depends( + "reserved_sale_order_line_ids.event_registration_count", + "reserved_sale_order_line_ids.event_reservation_type_id", + "reserved_sale_order_line_ids.order_id.state", + "reserved_sale_order_line_ids.product_id.type", + "reserved_sale_order_line_ids.product_uom_qty", + ) + def _compute_reservations_total(self): + """Get how many reserved seats exist.""" + results = self.env["sale.order.line"].formatted_read_group( + domain=self._seats_reservation_domain(), + aggregates=["event_registration_count:sum", "product_uom_qty:sum"], + groupby=["event_reservation_type_id"], + ) + totals = {group["event_reservation_type_id"][0]: group for group in results} + for one in self: + totals_item = totals.get(one.id, {}) + one.seats_reservation_total = totals_item.get( + "product_uom_qty:sum", 0 + ) - totals_item.get("event_registration_count:sum", 0) + + def action_open_sale_orders(self): + """Display SO that include reservations.""" + sol = self.env["sale.order.line"].search( + self._seats_reservation_domain(), + ) + result = self.env["ir.actions.act_window"]._for_xml_id("sale.action_orders") + result["domain"] = [("order_line", "in", sol.ids)] + return result diff --git a/event_sale_reservation/models/product_template.py b/event_sale_reservation/models/product_template.py new file mode 100644 index 000000000..4c9adebd1 --- /dev/null +++ b/event_sale_reservation/models/product_template.py @@ -0,0 +1,44 @@ +# Copyright 2021 Tecnativa - Jairo Llopis +# Copyright 2023 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + +from ..exceptions import ReservationWithoutEventTypeError + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + type = fields.Selection( + selection_add=[ + ("event_reservation", "Event Reservation"), + ], + ondelete={"event_reservation": "set service"}, + ) + event_reservation_type_id = fields.Many2one( + comodel_name="event.type", + index=True, + string="Event type for reservations", + help="Type of events that can be reserved by buying this product", + ) + + def _detailed_type_mapping(self): + type_mapping = super()._detailed_type_mapping() + type_mapping["event_reservation"] = "service" + return type_mapping + + @api.constrains("type") + def _check_event_reservation(self): + """Event reservation products checks. + + - A product cannot be both an event ticket and an event reservation. + - An event reservation must have an event type attached. + """ + for one in self: + if one.type != "event_reservation": + continue + if not one.event_reservation_type_id: + raise ReservationWithoutEventTypeError( + self.env._("You must indicate event type for %(name)s.") + ) diff --git a/event_sale_reservation/models/sale_order.py b/event_sale_reservation/models/sale_order.py new file mode 100644 index 000000000..fee738364 --- /dev/null +++ b/event_sale_reservation/models/sale_order.py @@ -0,0 +1,51 @@ +# Copyright 2021 Tecnativa - Jairo Llopis +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + event_reservations_pending = fields.Integer( + compute="_compute_event_reservations_pending", + string="Pending event reservations", + help=( + "Indicates how many event reservations are still not linked to " + "any registration." + ), + ) + event_registration_count = fields.Integer( + compute="_compute_event_registration_count", + string="Event registrations", + help=("Indicates how many event registrations are linked to this order."), + ) + + @api.depends("order_line.product_uom_qty", "order_line.event_registration_count") + def _compute_event_reservations_pending(self): + """Know how many pending event reservations are linked to this SO.""" + for one in self: + reservation_lines = one.order_line.filtered( + lambda x: x.product_id.type == "event_reservation" + ) + reserved = sum(reservation_lines.mapped("product_uom_qty")) + registered = sum(reservation_lines.mapped("event_registration_count")) + one.event_reservations_pending = reserved - registered + + @api.depends("order_line.event_registration_ids") + def _compute_event_registration_count(self): + """Get registrations per SO.""" + for one in self: + one.event_registration_count = len( + one.mapped("order_line.event_registration_ids") + ) + + def action_open_event_registrations(self): + """Redirect user to event registrations related to this SO.""" + return { + "domain": [("sale_order_id", "in", self.ids)], + "name": self.env._("Attendees"), + "res_model": "event.registration", + "type": "ir.actions.act_window", + "view_mode": "list,form,calendar,graph", + } diff --git a/event_sale_reservation/models/sale_order_line.py b/event_sale_reservation/models/sale_order_line.py new file mode 100644 index 000000000..62900e892 --- /dev/null +++ b/event_sale_reservation/models/sale_order_line.py @@ -0,0 +1,32 @@ +# Copyright 2021 Tecnativa - Jairo Llopis +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + event_registration_ids = fields.One2many( + comodel_name="event.registration", + inverse_name="sale_order_line_id", + string="Event registrations", + help="Event registrations related to this sale order line", + ) + event_registration_count = fields.Integer( + compute="_compute_event_registration_count", + store=True, + help="Count of event registrations related to this sale order line", + ) + event_reservation_type_id = fields.Many2one( + index=True, + readonly=True, + related="product_id.event_reservation_type_id", + store=True, + ) + + @api.depends("event_registration_ids") + def _compute_event_registration_count(self): + """Get count of related event registrations.""" + for one in self: + one.event_registration_count = len(one.event_registration_ids) diff --git a/event_sale_reservation/pyproject.toml b/event_sale_reservation/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/event_sale_reservation/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/event_sale_reservation/readme/CONFIGURE.md b/event_sale_reservation/readme/CONFIGURE.md new file mode 100644 index 000000000..3e1ff454b --- /dev/null +++ b/event_sale_reservation/readme/CONFIGURE.md @@ -0,0 +1,4 @@ +To make use of this module, a user needs these minimal permissions: + +- Sales / User: Own Documents Only +- Events / User diff --git a/event_sale_reservation/readme/CONTRIBUTORS.md b/event_sale_reservation/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..248101947 --- /dev/null +++ b/event_sale_reservation/readme/CONTRIBUTORS.md @@ -0,0 +1,4 @@ +- [Tecnativa](https://www.tecnativa.com): + - Jairo Llopis + - Pilar Vargas + - Frederic Grall diff --git a/event_sale_reservation/readme/DESCRIPTION.md b/event_sale_reservation/readme/DESCRIPTION.md new file mode 100644 index 000000000..529ab77f3 --- /dev/null +++ b/event_sale_reservation/readme/DESCRIPTION.md @@ -0,0 +1,4 @@ +This module extends the functionality of event_sale to support selling +reservations of events that still don't exist and to allow you to +schedule the creation of events based on how many reservations already +exist. diff --git a/event_sale_reservation/readme/ROADMAP.md b/event_sale_reservation/readme/ROADMAP.md new file mode 100644 index 000000000..21378e686 --- /dev/null +++ b/event_sale_reservation/readme/ROADMAP.md @@ -0,0 +1,4 @@ +Some addons (event_registration_multi_qty + +event_sale_registration_multi_qty) makes totals wrong because they +depend currently on count and not sum of qtys; integrating with them +would require a glue module. diff --git a/event_sale_reservation/readme/USAGE.md b/event_sale_reservation/readme/USAGE.md new file mode 100644 index 000000000..0a3d7b742 --- /dev/null +++ b/event_sale_reservation/readme/USAGE.md @@ -0,0 +1,61 @@ +To know how many reservations exist for a given event type: + +1. Go to *Events \> Configuration \> Event Templates* and pick or + create one. +2. There's a new smart button called *Reserved seats* with that count. +3. Click on it to get to the sales orders where the seats got reserved. + +But that counter will be probably zero when you install, so let's see +how to increase it. + +To create an event reservation product: + +1. Go to *Sales \> Products \> Products*. +2. Create one and set *Product Type* to *Event Reservation*. +3. Select one *Event type for reservations*. +4. Save. + +From now on, you can sell event reservations for that event type. To do +it: + +1. Go to *Sales \> Orders \> Quotations*. +2. Create one. +3. Set its basic info (customer, date...) and go to *Order lines* tab. +4. Click *Add a product*. +5. Select the event reservation product you created above. +6. Set its info (quantity, price...). +7. Save that line and the quotation. + +At this point, the reservation is not yet confirmed, so if you go to the +event type, the smart button will still count zero. + +To confirm those reservations: + +1. Go to the quotation you just created (if you are not there yet). +2. Click on *Confirm*. + +Now, if you go to the event type form, the smart button will indicate +how many reserved seats exist. + +If you want to convert those reservations into real event registrations: + +1. Go to the quotation you just created (if you are not there yet). +2. Click on *Register in event*. +3. In the wizard you see, set the *Event* and *Event Ticket* for all + the order lines you want to convert into registrations. +4. If there is any line you still don't want to convert, remove it from + the wizard. +5. Click on *Next*. +6. A new wizard will appear, where you will be able to specify the + name, email and phone of each one of the attendees. If you don't do + it, they will get that info from the sales order customer. +7. After that's done, click on *Apply*. + +At this point, the sales order lines will be modified to include the +ticket product instead of the reservation product, and the event +reservations have been created, linked to those lines. + +If the event is set to autoconfirmation, the registrations are +confirmed., otherwise, they are kept as draft until an invoice is +created for the sales order, and paid. But that is just upstream +`event_sale` module in action. diff --git a/event_sale_reservation/reports/__init__.py b/event_sale_reservation/reports/__init__.py new file mode 100644 index 000000000..cd23411b8 --- /dev/null +++ b/event_sale_reservation/reports/__init__.py @@ -0,0 +1 @@ +from . import sale_report diff --git a/event_sale_reservation/reports/sale_report.py b/event_sale_reservation/reports/sale_report.py new file mode 100644 index 000000000..5a5a74a23 --- /dev/null +++ b/event_sale_reservation/reports/sale_report.py @@ -0,0 +1,24 @@ +# Copyright 2021 Tecnativa - Jairo Llopis +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class SaleReport(models.Model): + _inherit = "sale.report" + + event_reservation_type_id = fields.Many2one( + comodel_name="event.type", + readonly=True, + string="Event reservation type", + ) + + def _select_additional_fields(self): + res = super()._select_additional_fields() + res["event_reservation_type_id"] = "t.event_reservation_type_id" + return res + + def _group_by_sale(self): + res = super()._group_by_sale() + res += """, t.event_reservation_type_id""" + return res diff --git a/event_sale_reservation/reports/sale_report_view.xml b/event_sale_reservation/reports/sale_report_view.xml new file mode 100644 index 000000000..cd06a7b7b --- /dev/null +++ b/event_sale_reservation/reports/sale_report_view.xml @@ -0,0 +1,19 @@ + + + + + Event type search + sale.report + + + + + + + + diff --git a/event_sale_reservation/static/description/icon.png b/event_sale_reservation/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/event_sale_reservation/static/description/icon.png differ diff --git a/event_sale_reservation/static/description/index.html b/event_sale_reservation/static/description/index.html new file mode 100644 index 000000000..525ce4195 --- /dev/null +++ b/event_sale_reservation/static/description/index.html @@ -0,0 +1,517 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Sell event reservations

+ +

Mature License: AGPL-3 OCA/event Translate me on Weblate Try me on Runboat

+

This module extends the functionality of event_sale to support selling +reservations of events that still don’t exist and to allow you to +schedule the creation of events based on how many reservations already +exist.

+

Table of contents

+ +
+

Configuration

+

To make use of this module, a user needs these minimal permissions:

+
    +
  • Sales / User: Own Documents Only
  • +
  • Events / User
  • +
+
+
+

Usage

+

To know how many reservations exist for a given event type:

+
    +
  1. Go to Events > Configuration > Event Templates and pick or create +one.
  2. +
  3. There’s a new smart button called Reserved seats with that count.
  4. +
  5. Click on it to get to the sales orders where the seats got reserved.
  6. +
+

But that counter will be probably zero when you install, so let’s see +how to increase it.

+

To create an event reservation product:

+
    +
  1. Go to Sales > Products > Products.
  2. +
  3. Create one and set Product Type to Event Reservation.
  4. +
  5. Select one Event type for reservations.
  6. +
  7. Save.
  8. +
+

From now on, you can sell event reservations for that event type. To do +it:

+
    +
  1. Go to Sales > Orders > Quotations.
  2. +
  3. Create one.
  4. +
  5. Set its basic info (customer, date…) and go to Order lines tab.
  6. +
  7. Click Add a product.
  8. +
  9. Select the event reservation product you created above.
  10. +
  11. Set its info (quantity, price…).
  12. +
  13. Save that line and the quotation.
  14. +
+

At this point, the reservation is not yet confirmed, so if you go to the +event type, the smart button will still count zero.

+

To confirm those reservations:

+
    +
  1. Go to the quotation you just created (if you are not there yet).
  2. +
  3. Click on Confirm.
  4. +
+

Now, if you go to the event type form, the smart button will indicate +how many reserved seats exist.

+

If you want to convert those reservations into real event registrations:

+
    +
  1. Go to the quotation you just created (if you are not there yet).
  2. +
  3. Click on Register in event.
  4. +
  5. In the wizard you see, set the Event and Event Ticket for all the +order lines you want to convert into registrations.
  6. +
  7. If there is any line you still don’t want to convert, remove it from +the wizard.
  8. +
  9. Click on Next.
  10. +
  11. A new wizard will appear, where you will be able to specify the name, +email and phone of each one of the attendees. If you don’t do it, +they will get that info from the sales order customer.
  12. +
  13. After that’s done, click on Apply.
  14. +
+

At this point, the sales order lines will be modified to include the +ticket product instead of the reservation product, and the event +reservations have been created, linked to those lines.

+

If the event is set to autoconfirmation, the registrations are +confirmed., otherwise, they are kept as draft until an invoice is +created for the sales order, and paid. But that is just upstream +event_sale module in action.

+
+
+

Known issues / Roadmap

+

Some addons (event_registration_multi_qty + +event_sale_registration_multi_qty) makes totals wrong because they +depend currently on count and not sum of qtys; integrating with them +would require a glue module.

+
+
+

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 to smash 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:
      +
    • Jairo Llopis
    • +
    • Pilar Vargas
    • +
    • Frederic Grall
    • +
    +
  • +
+
+
+

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.

+

Current maintainer:

+

pilarvargas-tecnativa

+

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_sale_reservation/tests/__init__.py b/event_sale_reservation/tests/__init__.py new file mode 100644 index 000000000..c755c1872 --- /dev/null +++ b/event_sale_reservation/tests/__init__.py @@ -0,0 +1 @@ +from . import test_event_sale diff --git a/event_sale_reservation/tests/test_event_sale.py b/event_sale_reservation/tests/test_event_sale.py new file mode 100644 index 000000000..0d3400dd2 --- /dev/null +++ b/event_sale_reservation/tests/test_event_sale.py @@ -0,0 +1,161 @@ +# Copyright 2021 Tecnativa - Jairo Llopis +# Copyright 2023 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from datetime import datetime, timedelta + +from odoo import Command +from odoo.tests import Form + +from odoo.addons.base.tests.common import BaseCommon + +from ..exceptions import ReservationWithoutEventTypeError + + +class EventSaleCase(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + qtys = (1, 10, 100) + cls.event_types = cls.env["event.type"].create( + [{"name": f"event type {num}"} for num in range(3)] + ) + cls.products = cls.env["product.product"].create( + [ + { + "type": "event_reservation", + "event_reservation_type_id": cls.event_types[num].id, + "list_price": num, + "name": f"product reservation for event type {num}", + } + for num in range(3) + ] + ) + cls.events = cls.env["event.event"].create( + [ + { + "date_begin": datetime.now(), + "date_end": datetime.now() + timedelta(days=1), + "event_ticket_ids": [ + Command.create( + { + "name": f"ticket {num}", + "product_id": cls.products[num].id, + }, + ) + ], + "event_type_id": cls.event_types[num].id, + "name": f"event {num}", + } + for num in range(3) + ] + ) + cls.customers = cls.env["res.partner"].create( + [ + { + "email": f"{num}@example.com", + "name": f"customer {num}", + "phone": num, + } + for num in range(3) + ] + ) + cls.orders = cls.env["sale.order"].create( + [ + { + "order_line": [ + Command.create( + { + "product_id": cls.products[num].id, + "product_uom_qty": qtys[num], + }, + ), + ], + "partner_id": cls.customers[num].id, + } + for num in range(3) + ] + ) + + def wizard_reservation_to_registration(self, order): + """Generate a wizard to register reservations.""" + return Form( + self.env["registration.editor"].with_context( + active_id=order.id, + active_ids=order.ids, + active_model=order._name, + registering_reservations=True, + ), + view="event_sale_reservation.registration_editor_reservations_view_form", + ) + + def wizard_step_2(self, wizard1): + """Generate a step 2 wizard from the step 1 wizard.""" + action = wizard1.action_convert_to_registration() + # Verify that the action type is open a window + self.assertEqual(action["type"], "ir.actions.act_window") + # Verify the model and context of the wizard + self.assertEqual(action["res_model"], wizard1._name) + self.assertIn("skip_event_sale_registration_multi_qty", action["context"]) + self.assertFalse(action["context"]["registering_reservations"]) + # Get form from 2nd chained action + return Form( + self.env[action["res_model"]].with_context(**action["context"]), + view=action["view_id"], + ) + + def test_wrong_product_reservation_without_type(self): + """Event reservation products require the type.""" + with self.assertRaises(ReservationWithoutEventTypeError): + self.env["product.product"].create( + { + "type": "event_reservation", + "list_price": 10, + "name": "event reservation without event type fails", + } + ) + + def test_event_type_open_orders(self): + """Test the smart button that opens orders from an event type.""" + self.orders.action_confirm() + groups = zip(self.event_types, self.orders, (1, 10, 100), strict=True) + for ev_type, so, reservations in groups: + self.assertEqual(ev_type.seats_reservation_total, reservations) + action = ev_type.action_open_sale_orders() + found_so = self.env[action["res_model"]].search(action["domain"]) + self.assertEqual(found_so, so) + + def test_sale_workflow(self): + # Start: orders are draft, all is pending, nothing is reserved + self.orders.invalidate_recordset(["event_reservations_pending"]) + self.assertEqual(self.orders.mapped("event_reservations_pending"), [1, 10, 100]) + self.assertEqual(self.event_types.mapped("seats_reservation_total"), [0, 0, 0]) + # Confirm orders: all is pending, all is reserved + self.orders.action_confirm() + self.orders.invalidate_recordset(["event_reservations_pending"]) + self.assertEqual(self.orders.mapped("event_reservations_pending"), [1, 10, 100]) + self.assertEqual( + self.event_types.mapped("seats_reservation_total"), [1, 10, 100] + ) + # Register 2nd SO to event using wizard + wiz1 = self.wizard_reservation_to_registration(self.orders[1]) + self.assertEqual(len(wiz1.event_registration_ids), 1) + wiz1_line = wiz1.event_registration_ids.edit(0) + wiz1_line.event_id = self.events[1] + wiz1_line.event_ticket_id = self.events[1].event_ticket_ids + wiz1_line.save() + # Click "Next": opens 2nd wizard to configure registrations + wiz2 = self.wizard_step_2(wiz1.save()) + self.assertEqual(len(wiz2.event_registration_ids), 10) + for num in range(len(wiz2.event_registration_ids)): + wiz2_line = wiz2.event_registration_ids.edit(num) + wiz2_line.name = f"name {num}" + wiz2_line.email = f"{num}@example.com" + wiz2_line.phone = f"phone {num}" + wiz2_line.save() + wiz2.save().action_make_registration() + # 1st and 3rd SO are pending and reserved + self.orders.invalidate_recordset(["event_reservations_pending"]) + self.assertEqual(self.orders.mapped("event_reservations_pending"), [1, 0, 100]) + self.assertEqual( + self.event_types.mapped("seats_reservation_total"), [1, 0, 100] + ) diff --git a/event_sale_reservation/views/event_type_view.xml b/event_sale_reservation/views/event_type_view.xml new file mode 100644 index 000000000..c8a566043 --- /dev/null +++ b/event_sale_reservation/views/event_type_view.xml @@ -0,0 +1,45 @@ + + + + + Link to reservations from type + event.type + + +
+ +
+ + +
+
+
+
+ + Rerservations info from event category tree + event.type + + + + + + + +
diff --git a/event_sale_reservation/views/product_template_view.xml b/event_sale_reservation/views/product_template_view.xml new file mode 100644 index 000000000..03f4d3840 --- /dev/null +++ b/event_sale_reservation/views/product_template_view.xml @@ -0,0 +1,20 @@ + + + + + Allow products that are event registrations + product.template + + + + + + + + diff --git a/event_sale_reservation/views/sale_order_view.xml b/event_sale_reservation/views/sale_order_view.xml new file mode 100644 index 000000000..a6fe5302a --- /dev/null +++ b/event_sale_reservation/views/sale_order_view.xml @@ -0,0 +1,39 @@ + + + + + Event reservation management + sale.order + + + +
+ +
+ + + +
+
+
diff --git a/event_sale_reservation/wizards/__init__.py b/event_sale_reservation/wizards/__init__.py new file mode 100644 index 000000000..750cb451f --- /dev/null +++ b/event_sale_reservation/wizards/__init__.py @@ -0,0 +1,2 @@ +from . import registration_editor +from . import registration_editor_line diff --git a/event_sale_reservation/wizards/registration_editor.py b/event_sale_reservation/wizards/registration_editor.py new file mode 100644 index 000000000..b29f3fafe --- /dev/null +++ b/event_sale_reservation/wizards/registration_editor.py @@ -0,0 +1,70 @@ +# Copyright 2021 Tecnativa - Jairo Llopis +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class RegistrationEditor(models.TransientModel): + _inherit = "registration.editor" + + @api.model + def default_get(self, fields): + """Get data needed to register reservations.""" + result = super().default_get(fields) + if self.env.context.get("registering_reservations"): + order = self.env["sale.order"].browse(result["sale_order_id"]) + result["event_registration_ids"] = [] + for sol in order.order_line: + if sol.product_id.type != "event_reservation": + continue + sol_type = sol.event_reservation_type_id + result["event_registration_ids"] += [ + ( + 0, + 0, + { + "sale_order_line_id": sol.id, + "event_reservation_type_id": sol_type.id, + }, + ) + ] + return result + + def action_convert_to_registration(self): + """Convert reservations to registrations. + We use the skip_event_sale_registration_multi_qty context to "skip" the + operation of the event_sale_registration_multi_qty addon because they are + "incompatible with each other".""" + # Modify SO lines to be tickets instead of reservations + for line in self.event_registration_ids: + # HACK: Force product_updatable = True to be able to change the product_id + # when converting reservations into registrations. + # Overwriting _compute_product_updatable() and controlling this specific + # case there is not feasible, given that other modules also overwrite + # that compute and change the value, which would result in our logic being + # overridden. + line.sale_order_line_id.product_updatable = True + line.sale_order_line_id.write( + { + "event_id": line.event_id.id, + "event_ticket_id": line.event_ticket_id.id, + "product_id": line.event_ticket_id.product_id.id, + "price_unit": line.sale_order_line_id.price_unit, + } + ) + line.sale_order_line_id._compute_product_updatable() + # Close current wizard and reopen normally to configure registrations + upstream_view = self.env.ref("event_sale.view_event_registration_editor_form") + return { + "type": "ir.actions.act_window", + "context": dict( + self.env.context, + registering_reservations=False, + skip_event_sale_registration_multi_qty=True, + ), + "res_model": self._name, + "target": "new", + "view_id": upstream_view.id, + "view_mode": "form", + "views": [[upstream_view.id, "form"]], + } diff --git a/event_sale_reservation/wizards/registration_editor_line.py b/event_sale_reservation/wizards/registration_editor_line.py new file mode 100644 index 000000000..b661fc28e --- /dev/null +++ b/event_sale_reservation/wizards/registration_editor_line.py @@ -0,0 +1,13 @@ +# Copyright 2021 Tecnativa - Jairo Llopis +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class RegistrationEditorLine(models.TransientModel): + _inherit = "registration.editor.line" + + event_reservation_type_id = fields.Many2one( + related="sale_order_line_id.event_reservation_type_id", + readonly=True, + ) diff --git a/event_sale_reservation/wizards/registration_editor_view.xml b/event_sale_reservation/wizards/registration_editor_view.xml new file mode 100644 index 000000000..44ff4437d --- /dev/null +++ b/event_sale_reservation/wizards/registration_editor_view.xml @@ -0,0 +1,59 @@ + + + + + + Convert reservation into registrations + registration.editor + +
+

+ Convert pending event reservations into registrations for + +

+

+ If there is any line from that order that you still do + not want to convert into real event registrations, you + can remove it from the list below. You will be able to + repeat this process later for those lines. +

+ + + + + + + + +
+
+
+
+
+ + Register in event + ir.actions.act_window + registration.editor + form + + new + {'registering_reservations': True} + +