From 0d2018ad7ecd8126334dbc56f828d6c9eb3fbba7 Mon Sep 17 00:00:00 2001 From: David Vidal Date: Wed, 19 Apr 2017 10:19:38 +0200 Subject: [PATCH] [10.0][WIP] event_sale_session: New module --- event_sale_session/README.rst | 39 +++++ event_sale_session/__init__.py | 4 + event_sale_session/__manifest__.py | 26 ++++ event_sale_session/i18n/es.po | 139 ++++++++++++++++++ event_sale_session/models/__init__.py | 7 + event_sale_session/models/account_invoice.py | 35 +++++ event_sale_session/models/event_event.py | 38 +++++ .../models/event_registration.py | 17 +++ event_sale_session/models/event_session.py | 27 ++++ event_sale_session/models/sale_order.py | 99 +++++++++++++ .../security/ir.model.access.csv | 3 + event_sale_session/tests/__init__.py | 3 + .../tests/test_event_sale_session.py | 84 +++++++++++ .../views/event_session_view.xml | 29 ++++ event_sale_session/views/event_view.xml | 21 +++ event_sale_session/views/sale_order_views.xml | 26 ++++ event_sale_session/wizard/__init__.py | 3 + .../wizard/event_edit_registration.py | 40 +++++ .../wizard/event_edit_registration.xml | 15 ++ 19 files changed, 655 insertions(+) create mode 100644 event_sale_session/README.rst create mode 100644 event_sale_session/__init__.py create mode 100644 event_sale_session/__manifest__.py create mode 100644 event_sale_session/i18n/es.po create mode 100644 event_sale_session/models/__init__.py create mode 100644 event_sale_session/models/account_invoice.py create mode 100644 event_sale_session/models/event_event.py create mode 100644 event_sale_session/models/event_registration.py create mode 100644 event_sale_session/models/event_session.py create mode 100644 event_sale_session/models/sale_order.py create mode 100644 event_sale_session/security/ir.model.access.csv create mode 100644 event_sale_session/tests/__init__.py create mode 100644 event_sale_session/tests/test_event_sale_session.py create mode 100644 event_sale_session/views/event_session_view.xml create mode 100644 event_sale_session/views/event_view.xml create mode 100644 event_sale_session/views/sale_order_views.xml create mode 100644 event_sale_session/wizard/__init__.py create mode 100644 event_sale_session/wizard/event_edit_registration.py create mode 100644 event_sale_session/wizard/event_edit_registration.xml diff --git a/event_sale_session/README.rst b/event_sale_session/README.rst new file mode 100644 index 000000000..fe2c7acb6 --- /dev/null +++ b/event_sale_session/README.rst @@ -0,0 +1,39 @@ +.. 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 + + +Sell tickets associated to sessions +=================================== + +Adds the option of choosing a session when selling an event ticket + +Usage +===== + + +Known issues / Roadmap +====================== + + +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. \ No newline at end of file diff --git a/event_sale_session/__init__.py b/event_sale_session/__init__.py new file mode 100644 index 000000000..35e7c9600 --- /dev/null +++ b/event_sale_session/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import wizard diff --git a/event_sale_session/__manifest__.py b/event_sale_session/__manifest__.py new file mode 100644 index 000000000..de4a8e433 --- /dev/null +++ b/event_sale_session/__manifest__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 David Vidal +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Event Sale 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 sales in events', + 'depends': [ + 'event_sale', + 'event_session', + ], + 'data': [ + 'security/ir.model.access.csv', + 'views/sale_order_views.xml', + 'views/event_view.xml', + 'views/event_session_view.xml', + 'wizard/event_edit_registration.xml', + ], + 'installable': True, +} diff --git a/event_sale_session/i18n/es.po b/event_sale_session/i18n/es.po new file mode 100644 index 000000000..cb7e4231d --- /dev/null +++ b/event_sale_session/i18n/es.po @@ -0,0 +1,139 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * event_sale_session +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-06-14 11:31+0000\n" +"PO-Revision-Date: 2017-06-14 14:10+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: \n" +"Language: es\n" +"X-Generator: Poedit 1.8.7.1\n" + +#. module: event_sale_session +#: model:ir.model,name:event_sale_session.model_event_registration +msgid "Attendee" +msgstr "Asistentes" + +#. module: event_sale_session +#: model:ir.model.fields,field_description:event_sale_session.field_sale_order_line_registration_ids +#: model:ir.model.fields,field_description:event_sale_session.field_sale_order_registration_ids +msgid "Attendees" +msgstr "Asistentes" + +#. module: event_sale_session +#: model:ir.model.fields,field_description:event_sale_session.field_sale_order_line_event_session_seats_available +msgid "Available Seats" +msgstr "Plazas disponibles" + +#. module: event_sale_session +#: model:ir.model,name:event_sale_session.model_event_event +#: model:ir.model.fields,field_description:event_sale_session.field_sale_order_event_ids +msgid "Event" +msgstr "Evento" + +#. module: event_sale_session +#: model:ir.model,name:event_sale_session.model_event_session +msgid "Event session" +msgstr "Sesión" + +#. module: event_sale_session +#: model:ir.model,name:event_sale_session.model_account_invoice +msgid "Invoice" +msgstr "Factura" + +#. module: event_sale_session +#: code:addons/event_sale_session/models/sale_order.py:73 +#, python-format +msgid "Not enough seats. Change quanty or session" +msgstr "No hay plazas suficientes. Cambia la cantidad o escoge otra sesión." + +#. module: event_sale_session +#: model:ir.actions.act_window,name:event_sale_session.act_event_session_unconfirmed_qty +msgid "Orders" +msgstr "Pedidos" + +#. module: event_sale_session +#: model:ir.model,name:event_sale_session.model_sale_order +msgid "Sales Order" +msgstr "Pedido de venta" + +#. module: event_sale_session +#: model:ir.model,name:event_sale_session.model_sale_order_line +msgid "Sales Order Line" +msgstr "Línea de pedido de venta" + +#. module: event_sale_session +#: model:ir.model.fields,field_description:event_sale_session.field_event_event_order_line_ids +#: model:ir.model.fields,field_description:event_sale_session.field_event_session_order_line_ids +msgid "Sales Order Lines" +msgstr "Líneas de pedido de venta" + +#. module: event_sale_session +#: model:ir.model.fields,field_description:event_sale_session.field_sale_order_line_event_session_seats_availability +msgid "Seats Availavility" +msgstr "Disponibilidad de plazas" + +#. module: event_sale_session +#: model:ir.model.fields,field_description:event_sale_session.field_registration_editor_line_session_id +#: model:ir.model.fields,field_description:event_sale_session.field_sale_order_line_session_id +msgid "Session" +msgstr "Sesión" + +#. module: event_sale_session +#: code:addons/event_sale_session/models/sale_order.py:63 +#, python-format +msgid "" +"There are sessions with no available seats!\n" +"Edit them so you can save the sale order" +msgstr "" +"Estás intentando reservar en una sesión sin plazas.\n" +"Edítal para que puedas guardar tu pedido." + +#. module: event_sale_session +#: model:ir.ui.view,arch_db:event_sale_session.view_event_session_form +msgid "Ticket" +msgstr "Ticket" + +#. module: event_sale_session +#: model:ir.model.fields,field_description:event_sale_session.field_sale_order_line_event_sessions_count +msgid "Total event sessions" +msgstr "Sesiones de evento totales" + +#. module: event_sale_session +#: model:ir.ui.view,arch_db:event_sale_session.view_event_session_tree +msgid "Total unconfirmed seats in orders" +msgstr "Total plazas de pedidos no confirmados" + +#. module: event_sale_session +#: model:ir.model.fields,field_description:event_sale_session.field_event_event_unconfirmed_qty +#: model:ir.model.fields,field_description:event_sale_session.field_event_session_unconfirmed_qty +msgid "Unconfirmed Qty" +msgstr "Cantidad sin asignar" + +#. module: event_sale_session +#: model:ir.ui.view,arch_db:event_sale_session.view_event_form +msgid "Unconfirmed order seats" +msgstr "Plazas en pedidos sin confirmar" + +#. module: event_sale_session +#: model:ir.ui.view,arch_db:event_sale_session.view_event_form +msgid "Unconfirmed orders seats" +msgstr "Plazas en pedidos sin confirmar" + +#. module: event_sale_session +#: model:ir.model,name:event_sale_session.model_registration_editor +msgid "registration.editor" +msgstr "registration.editor" + +#. module: event_sale_session +#: model:ir.model,name:event_sale_session.model_registration_editor_line +msgid "registration.editor.line" +msgstr "registration.editor.line" diff --git a/event_sale_session/models/__init__.py b/event_sale_session/models/__init__.py new file mode 100644 index 000000000..213c935a2 --- /dev/null +++ b/event_sale_session/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +from . import account_invoice +from . import event_event +from . import event_registration +from . import event_session +from . import sale_order diff --git a/event_sale_session/models/account_invoice.py b/event_sale_session/models/account_invoice.py new file mode 100644 index 000000000..b14e0f72f --- /dev/null +++ b/event_sale_session/models/account_invoice.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Sergio Teruel +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class AccountInvoice(models.Model): + _inherit = "account.invoice" + + @api.multi + def action_cancel(self): + res = super(AccountInvoice, self).action_cancel() + if res and not self.env.context.get('is_merge', False): + self.mapped( + 'invoice_line_ids.sale_line_ids.registration_ids').filtered( + lambda x: x.state not in ['done', 'draft'] + ).do_draft() + return res + + @api.multi + def unlink(self): + registrations = self.mapped( + 'invoice_line_ids.sale_line_ids.registration_ids').filtered( + lambda x: x.state not in ['done', 'draft']) + res = super(AccountInvoice, self).unlink() + if res: + registrations.filtered( + lambda x: x.state not in ['done', 'draft']).do_draft() + + @api.multi + def action_invoice_draft(self): + res = super(AccountInvoice, self).action_invoice_draft() + if res: + self._confirm_attendees() diff --git a/event_sale_session/models/event_event.py b/event_sale_session/models/event_event.py new file mode 100644 index 000000000..6a405f207 --- /dev/null +++ b/event_sale_session/models/event_event.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models + + +class EventSession(models.Model): + _inherit = 'event.event' + + order_line_ids = fields.One2many( + comodel_name='sale.order.line', + inverse_name='event_id', + string='Sales Order Lines' + ) + unconfirmed_qty = fields.Integer( + string='Unconfirmed Qty', + compute='_compute_unconfirmed_qty', + store=True, + ) + + @api.depends('order_line_ids', 'order_line_ids.product_uom_qty', + 'order_line_ids.order_id.state') + @api.multi + def _compute_unconfirmed_qty(self): + for event in self: + event.unconfirmed_qty = int(sum(event.order_line_ids.filtered( + lambda x: x.order_id.state in ('draft', 'sent') + ).mapped('product_uom_qty'))) + + @api.multi + def button_open_unconfirmed_event_order(self): + action = self.env.ref('sale.action_quotations').read()[0] + sales = self.env[ + 'sale.order.line'].search( + [('event_id','=',self.id)]).mapped('order_id').ids + action['domain'] = [('id', 'in' , sales), + ('state','in',('draft','sent'))] + action['context'] = {} + return action diff --git a/event_sale_session/models/event_registration.py b/event_sale_session/models/event_registration.py new file mode 100644 index 000000000..2ad935256 --- /dev/null +++ b/event_sale_session/models/event_registration.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 David Vidal +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class EventRegistration(models.Model): + _inherit = 'event.registration' + + @api.model + def _prepare_attendee_values(self, registration): + data = super(EventRegistration, self)._prepare_attendee_values( + registration) + session_id = registration['sale_order_line_id'].session_id.id + data.update({'session_id': session_id}) + return data diff --git a/event_sale_session/models/event_session.py b/event_sale_session/models/event_session.py new file mode 100644 index 000000000..b7c945504 --- /dev/null +++ b/event_sale_session/models/event_session.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models + + +class EventSession(models.Model): + _inherit = 'event.session' + + order_line_ids = fields.One2many( + comodel_name='sale.order.line', + inverse_name='session_id', + string='Sales Order Lines' + ) + unconfirmed_qty = fields.Integer( + string='Unconfirmed Qty', + compute='_compute_unconfirmed_qty', + store=True, + ) + + @api.depends('order_line_ids', 'order_line_ids.product_uom_qty', + 'order_line_ids.order_id.state') + @api.multi + def _compute_unconfirmed_qty(self): + for session in self: + session.unconfirmed_qty = int(sum(session.order_line_ids.filtered( + lambda x: x.order_id.state in ('draft', 'sent') + ).mapped('product_uom_qty'))) diff --git a/event_sale_session/models/sale_order.py b/event_sale_session/models/sale_order.py new file mode 100644 index 000000000..73c34bab1 --- /dev/null +++ b/event_sale_session/models/sale_order.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- + +from odoo import _, api, fields, models +from odoo import exceptions + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + registration_ids = fields.One2many( + comodel_name='event.registration', + inverse_name='sale_order_id', + string='Attendees', + readonly=True, + ) + event_ids = fields.Many2many( + comodel_name="event.event", + string='Event', + compute='_compute_event_ids', + readonly=True, + ) + + @api.multi + @api.depends('order_line.event_id') + def _compute_event_ids(self): + for sale in self: + sale.event_ids = sale.order_line.mapped('event_id') + +class SaleOrderLine(models.Model): + + _inherit = 'sale.order.line' + + session_id = fields.Many2one( + comodel_name='event.session', + string='Session', + ) + event_sessions_count = fields.Integer( + comodel_name='event.session', + related='event_id.sessions_count', + readonly=True, + ) + event_session_seats_available = fields.Integer( + related='session_id.seats_available', + string='Available Seats', + readonly=True, + ) + event_session_seats_availability = fields.Selection( + related='session_id.seats_availability', + string='Seats Availavility', + readonly=True, + ) + registration_ids = fields.One2many( + comodel_name='event.registration', + inverse_name='sale_order_line_id', + string='Attendees', + readonly=True, + ) + + @api.multi + def write(self, values): + super(SaleOrderLine, self).write(values) + for line in self: + if not line._session_seats_available(): + raise exceptions.ValidationError(_( + "There are sessions with no available seats!\n" + "Edit them so you can save the sale order")) + + @api.onchange( + 'product_uom_qty', 'event_id', 'session_id', 'event_ticket_id') + def product_uom_change(self): + super(SaleOrderLine, self).product_uom_change() + if self.session_id: + if not self._session_seats_available(): + raise exceptions.UserError(_( + "Not enough seats. Change quanty or session")) + + @api.multi + @api.onchange('event_id', 'session_id', 'event_ticket_id') + def event_id_change(self): + for so_line in self: + so_line.name = so_line._set_order_line_description() + if self.event_sessions_count == 1: + so_line.session_id = self.event_id.session_ids[0] + + def _session_seats_available(self): + self.ensure_one() + if self.session_id and self.session_id.seats_availability == 'limited': + seats = self.event_session_seats_available - self.product_uom_qty + return True if seats > 0 else False + else: + return True + + def _set_order_line_description(self): + description = self.event_id.name or self.product_id.name + if self.session_id: + description += ' - %s' % self.session_id.name or '' + if self.event_ticket_id: + description += ' - %s' % self.event_ticket_id.name or '' + return description diff --git a/event_sale_session/security/ir.model.access.csv b/event_sale_session/security/ir.model.access.csv new file mode 100644 index 000000000..8a4959087 --- /dev/null +++ b/event_sale_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_event_ticket_user,event.event.ticket.user,event_sale.model_event_event_ticket,event.group_event_user,1,0,0,0 +access_event_event_ticket_admin,event.event.ticket.admin,event_sale.model_event_event_ticket,event.group_event_manager,1,1,1,1 diff --git a/event_sale_session/tests/__init__.py b/event_sale_session/tests/__init__.py new file mode 100644 index 000000000..46f0b8bb1 --- /dev/null +++ b/event_sale_session/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import test_event_sale_session diff --git a/event_sale_session/tests/test_event_sale_session.py b/event_sale_session/tests/test_event_sale_session.py new file mode 100644 index 000000000..749720b43 --- /dev/null +++ b/event_sale_session/tests/test_event_sale_session.py @@ -0,0 +1,84 @@ +# -*- 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 + + +class EventSaleSession(common.SavepointCase): + + @classmethod + def setUpClass(cls): + super(EventSaleSession, cls).setUpClass() + cls.product_category = cls.env['product.category'].create({ + 'name': 'test_cat', + }) + cls.product = cls.env['product.product'].create({ + 'name': 'Test product event', + 'type': 'service', + 'event_ok': True, + 'lst_price': 10.0, + 'categ_id': cls.product_category.id, + }) + cls.event = cls.env['event.event'].create({ + 'name': 'Test event', + 'date_begin': '2017-05-26 20:00:00', + 'date_end': '2017-05-30 22:00:00', + 'seats_availability': 'limited', + 'seats_max': '100', + 'seats_min': '1', + 'event_ticket_ids': [ + (0, 0, {'product_id': cls.product.id, 'name': 'test1'}), + (0, 0, {'product_id': cls.product.id, + 'name': 'test2', 'price': 8.0, + }), + ], + }) + cls.session = cls.env['event.session'].create({ + 'name': 'Test session', + 'date_begin': '2017-05-26 20:00:00', + 'date_end': '2017-05-26 22:00:00', + 'event_id': cls.event.id, + 'seats_availability': cls.event.seats_availability, + 'seats_max': cls.event.seats_max, + 'seats_min': cls.event.seats_min, + }) + cls.session_alt = cls.env['event.session'].create({ + 'name': 'Test session', + 'date_begin': '2017-05-27 20:00:00', + 'date_end': '2017-05-27 22:00:00', + 'event_id': cls.event.id, + 'seats_availability': cls.event.seats_availability, + 'seats_max': cls.event.seats_max, + 'seats_min': cls.event.seats_min, + }) + cls.partner = cls.env['res.partner'].create({ + 'name': 'Test partner', + }) + + def test_sale(self): + """ sell event with session """ + sale = self.env['sale.order'].create({ + 'partner_id': self.partner.id, + 'order_line': [ + (0, 0, { + 'product_id': self.product.id, + 'event_id': self.event.id, + 'session_id': self.session.id, + 'product_uom_qty': 5.0, + 'event_ticket_id': self.event.event_ticket_ids[0].id,}), + ]}) + self.assertEqual(self.session.unconfirmed_qty, 5) + self.assertEqual(self.event.unconfirmed_qty, 5) + sale.action_confirm() + self.assertEqual(self.session.unconfirmed_qty, 0) + self.assertEqual(self.event.unconfirmed_qty, 0) + regs = self.env['event.registration'].search([ + ('sale_order_id', '=', sale.id) + ]) + self.assertTrue(len(regs) > 0) + for reg in regs: + self.assertEqual(reg.event_id.id, self.event.id) + self.assertEqual(reg.session_id.id, self.session.id) + self.assertEqual(reg.partner_id.id, self.partner.id) + self.assertEqual(reg.name, self.partner.name) diff --git a/event_sale_session/views/event_session_view.xml b/event_sale_session/views/event_session_view.xml new file mode 100644 index 000000000..556d8d2d0 --- /dev/null +++ b/event_sale_session/views/event_session_view.xml @@ -0,0 +1,29 @@ + + + + + event.session + + + + + + + + + + event.session + + + + + + + + + diff --git a/event_sale_session/views/event_view.xml b/event_sale_session/views/event_view.xml new file mode 100644 index 000000000..fd73b12d4 --- /dev/null +++ b/event_sale_session/views/event_view.xml @@ -0,0 +1,21 @@ + + + + + event.event + + + + + + + + + diff --git a/event_sale_session/views/sale_order_views.xml b/event_sale_session/views/sale_order_views.xml new file mode 100644 index 000000000..ec78d7b3c --- /dev/null +++ b/event_sale_session/views/sale_order_views.xml @@ -0,0 +1,26 @@ + + + + + sale.order.form.inherit + sale.order + + + + + + + + + + + + diff --git a/event_sale_session/wizard/__init__.py b/event_sale_session/wizard/__init__.py new file mode 100644 index 000000000..d3c281de0 --- /dev/null +++ b/event_sale_session/wizard/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import event_edit_registration diff --git a/event_sale_session/wizard/event_edit_registration.py b/event_sale_session/wizard/event_edit_registration.py new file mode 100644 index 000000000..965cfdffd --- /dev/null +++ b/event_sale_session/wizard/event_edit_registration.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api + + +class RegistrationEditor(models.TransientModel): + _inherit = "registration.editor" + + @api.model + def default_get(self, fields): + res = super(RegistrationEditor, self).default_get(fields) + vals = [(6, 0, [])] + so_line = self.env['sale.order.line'] + for registration in res['event_registration_ids'][1:]: + if so_line.id != registration[2]['sale_order_line_id']: + so_line = self.env['sale.order.line'].browse( + registration[2]['sale_order_line_id'] + ) + vals.append((0, 0, dict(registration[2], + session_id=so_line.session_id.id)),) + res['event_registration_ids'] = vals + return res + + +class RegistrationEditorLine(models.TransientModel): + """Event Registration""" + _inherit = "registration.editor.line" + + session_id = fields.Many2one( + comodel_name='event.session', + string='Session', + ) + + @api.multi + def get_registration_data(self): + res = super(RegistrationEditorLine, self).get_registration_data() + res.update({ + 'session_id': self.sale_order_line_id.session_id.id, + }) + return res diff --git a/event_sale_session/wizard/event_edit_registration.xml b/event_sale_session/wizard/event_edit_registration.xml new file mode 100644 index 000000000..47e874af9 --- /dev/null +++ b/event_sale_session/wizard/event_edit_registration.xml @@ -0,0 +1,15 @@ + + + + + registration.editor.form + registration.editor + + + + + + + + +