From 0eaecf062c3db72d8f1231c7027b696b13d5dc6e Mon Sep 17 00:00:00 2001 From: Patrick Hoste Date: Fri, 27 Aug 2021 12:23:19 +0000 Subject: [PATCH 01/10] [IMP] website_*_sale: add kwargs parameter to _website_product_id_change() method PURPOSE Before this commit we had to pass parameters through the context to get them to the _website_product_id_change() method. After this commit we can pass the parameters through the kwargs parameter. LINKS Task-2318655 Closes : odoo/odoo#71104 --- addons/website_event_sale/models/sale_order.py | 4 ++-- addons/website_sale/models/sale_order.py | 6 +++--- addons/website_sale_stock/models/sale_order.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/addons/website_event_sale/models/sale_order.py b/addons/website_event_sale/models/sale_order.py index 296738c6925..7978bfdff26 100644 --- a/addons/website_event_sale/models/sale_order.py +++ b/addons/website_event_sale/models/sale_order.py @@ -17,12 +17,12 @@ class SaleOrder(models.Model): domain.append(('event_ticket_id', '=', self.env.context.get("event_ticket_id"))) return self.env['sale.order.line'].sudo().search(domain) - def _website_product_id_change(self, order_id, product_id, qty=0): + def _website_product_id_change(self, order_id, product_id, qty=0, **kwargs): order = self.env['sale.order'].sudo().browse(order_id) if self._context.get('pricelist') != order.pricelist_id.id: self = self.with_context(pricelist=order.pricelist_id.id) - values = super(SaleOrder, self)._website_product_id_change(order_id, product_id, qty=qty) + values = super(SaleOrder, self)._website_product_id_change(order_id, product_id, qty=qty, **kwargs) event_ticket_id = None if self.env.context.get("event_ticket_id"): event_ticket_id = self.env.context.get("event_ticket_id") diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index f3d42707e73..bb217206519 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -102,7 +102,7 @@ class SaleOrder(models.Model): return self.env['sale.order.line'].sudo().search(domain) - def _website_product_id_change(self, order_id, product_id, qty=0): + def _website_product_id_change(self, order_id, product_id, qty=0, **kwargs): order = self.sudo().browse(order_id) product_context = dict(self.env.context) product_context.setdefault('lang', order.partner_id.lang) @@ -201,7 +201,7 @@ class SaleOrder(models.Model): product_id = product.id - values = self._website_product_id_change(self.id, product_id, qty=1) + values = self._website_product_id_change(self.id, product_id, qty=1, **kwargs) # add no_variant attributes that were not received for ptav in combination.filtered(lambda ptav: ptav.attribute_id.create_variant == 'no_variant' and ptav not in received_no_variant_values): @@ -260,7 +260,7 @@ class SaleOrder(models.Model): else: # update line no_variant_attributes_price_extra = [ptav.price_extra for ptav in order_line.product_no_variant_attribute_value_ids] - values = self.with_context(no_variant_attributes_price_extra=tuple(no_variant_attributes_price_extra))._website_product_id_change(self.id, product_id, qty=quantity) + values = self.with_context(no_variant_attributes_price_extra=tuple(no_variant_attributes_price_extra))._website_product_id_change(self.id, product_id, qty=quantity, **kwargs) order = self.sudo().browse(self.id) if self.pricelist_id.discount_policy == 'with_discount' and not self.env.context.get('fixed_price'): product_context.update({ diff --git a/addons/website_sale_stock/models/sale_order.py b/addons/website_sale_stock/models/sale_order.py index fda99c3f8b6..0a8e85905ae 100644 --- a/addons/website_sale_stock/models/sale_order.py +++ b/addons/website_sale_stock/models/sale_order.py @@ -35,8 +35,8 @@ class SaleOrder(models.Model): values['warning'] = self.warning_stock return values - def _website_product_id_change(self, order_id, product_id, qty=0): - res = super(SaleOrder, self)._website_product_id_change(order_id, product_id, qty=qty) + def _website_product_id_change(self, order_id, product_id, qty=0, **kwargs): + res = super(SaleOrder, self)._website_product_id_change(order_id, product_id, qty=qty, **kwargs) product = self.env['product.product'].browse(product_id) res['customer_lead'] = product.sale_delay return res From 73078e9cfcb445e66bafa9e953a267ae3b28b8d7 Mon Sep 17 00:00:00 2001 From: Patrick Hoste Date: Thu, 17 Dec 2020 13:38:07 +0000 Subject: [PATCH 02/10] [IMP] event: add setting option for event_booth module installation PURPOSE Adds the possibility to install the new module 'event_booth' through the event settings. LINKS Task-2318655 Closes : odoo/odoo#71104 --- addons/event/models/res_config_settings.py | 1 + addons/event/views/res_config_settings_views.xml | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/addons/event/models/res_config_settings.py b/addons/event/models/res_config_settings.py index 390ef942a20..5105365bdc6 100644 --- a/addons/event/models/res_config_settings.py +++ b/addons/event/models/res_config_settings.py @@ -16,6 +16,7 @@ class ResConfigSettings(models.TransientModel): module_website_event_questions = fields.Boolean("Registration Survey") module_event_barcode = fields.Boolean("Barcode") module_website_event_sale = fields.Boolean("Online Ticketing") + module_event_booth = fields.Boolean("Booth Management") @api.onchange('module_website_event_track') def _onchange_module_website_event_track(self): diff --git a/addons/event/views/res_config_settings_views.xml b/addons/event/views/res_config_settings_views.xml index 8882cd20c18..e186e10b852 100644 --- a/addons/event/views/res_config_settings_views.xml +++ b/addons/event/views/res_config_settings_views.xml @@ -61,6 +61,17 @@ +
+
+ +
+
+
+

Registration

From ac8fd6c1c5f083adcb7854bf4adfe8838b86df21 Mon Sep 17 00:00:00 2001 From: Patrick Hoste Date: Fri, 25 Jun 2021 15:57:15 +0000 Subject: [PATCH 03/10] [IMP] event: add is_finished field in event model PURPOSE The is_finished computed field was added in order to avoid displaying the booths on the registration page and warn the user with a message. LINKS Task-2318655 Closes : odoo/odoo#71104 --- addons/event/models/event_event.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/addons/event/models/event_event.py b/addons/event/models/event_event.py index 0a6a7f79dc6..bf8803c0680 100644 --- a/addons/event/models/event_event.py +++ b/addons/event/models/event_event.py @@ -187,6 +187,7 @@ class EventEvent(models.Model): date_end_located = fields.Char(string='End Date Located', compute='_compute_date_end_tz') is_ongoing = fields.Boolean('Is Ongoing', compute='_compute_is_ongoing', search='_search_is_ongoing') is_one_day = fields.Boolean(compute='_compute_field_is_one_day') + is_finished = fields.Boolean(compute='_compute_is_finished', search='_search_is_finished') # Location and communication address_id = fields.Many2one( 'res.partner', string='Venue', default=lambda self: self.env.company.partner_id.id, @@ -342,6 +343,30 @@ class EventEvent(models.Model): end_tz = fields.Datetime.context_timestamp(event, event.date_end) event.is_one_day = (begin_tz.date() == end_tz.date()) + @api.depends('date_end') + def _compute_is_finished(self): + for event in self: + if not event.date_end: + event.is_finished = False + continue + event = event._set_tz_context() + current_datetime = fields.Datetime.context_timestamp(event, fields.Datetime.now()) + datetime_end = fields.Datetime.context_timestamp(event, event.date_end) + event.is_finished = datetime_end <= current_datetime + + def _search_is_finished(self, operator, value): + if operator not in ['=', '!=']: + raise ValueError(_('This operator is not supported')) + if not isinstance(value, bool): + raise ValueError(_('Value should be True or False (not %s)'), value) + now = fields.Datetime.now() + if (operator == '=' and value) or (operator == '!=' and not value): + domain = [('date_end', '<=', now)] + else: + domain = [('date_end', '>', now)] + event_ids = self.env['event.event']._search(domain) + return [('id', 'in', event_ids)] + @api.depends('event_type_id') def _compute_date_tz(self): for event in self: From 3c5c3c257e755b3930da1388f6f84e06c925ed19 Mon Sep 17 00:00:00 2001 From: Patrick Hoste Date: Thu, 14 Jan 2021 10:39:24 +0000 Subject: [PATCH 04/10] [ADD] event_booth: add booths management for events PURPOSE This new module adds a booths management capabilities to the event module. The event manager will be able to manage booking to physical booths hosted at his event by first creating several booths categories depending for example on the booths size, the number of tables, extra services, ... He will then create the actual booths to offer to the final users. This module adds two main models : - Event Booth Category which is used to differenciate the booths type. - Event Booth which is the actual Booth that will be booked. The booking is done directly on the booth form view by setting a partner and his contact informations and by setting the booth state to unavailable. If the event manager want to book a booth for later use he can set the state to unavailable without setting a partner. The booking of a booth will be displayed in the chatter of the related event to ease the event manager overview The user manager can use event template to adds booths by default when setting the template to an event. LINKS Task-2318655 Closes : odoo/odoo#71104 --- addons/event_booth/__init__.py | 4 + addons/event_booth/__manifest__.py | 30 +++ .../data/event_booth_category_data.xml | 106 +++++++++ addons/event_booth/data/event_booth_demo.xml | 103 ++++++++ addons/event_booth/data/event_type_demo.xml | 25 ++ addons/event_booth/data/mail_data.xml | 10 + addons/event_booth/data/mail_templates.xml | 39 +++ addons/event_booth/models/__init__.py | 8 + addons/event_booth/models/event_booth.py | 117 +++++++++ .../models/event_booth_category.py | 17 ++ addons/event_booth/models/event_event.py | 90 +++++++ addons/event_booth/models/event_type.py | 12 + addons/event_booth/models/event_type_booth.py | 27 +++ .../event_booth/security/ir.model.access.csv | 9 + .../static/src/img/premium-booth.jpeg | Bin 0 -> 27446 bytes .../static/src/img/standard-booth.jpeg | Bin 0 -> 22229 bytes .../event_booth/static/src/img/vip-booth.jpeg | Bin 0 -> 28968 bytes addons/event_booth/tests/__init__.py | 5 + addons/event_booth/tests/common.py | 30 +++ .../tests/test_event_booth_internals.py | 56 +++++ .../event_booth/tests/test_event_internals.py | 91 +++++++ .../views/event_booth_category_views.xml | 65 +++++ .../event_booth/views/event_booth_views.xml | 224 ++++++++++++++++++ .../event_booth/views/event_event_views.xml | 37 +++ addons/event_booth/views/event_menus.xml | 16 ++ .../views/event_type_booth_views.xml | 85 +++++++ addons/event_booth/views/event_type_views.xml | 22 ++ 27 files changed, 1228 insertions(+) create mode 100644 addons/event_booth/__init__.py create mode 100644 addons/event_booth/__manifest__.py create mode 100644 addons/event_booth/data/event_booth_category_data.xml create mode 100644 addons/event_booth/data/event_booth_demo.xml create mode 100644 addons/event_booth/data/event_type_demo.xml create mode 100644 addons/event_booth/data/mail_data.xml create mode 100644 addons/event_booth/data/mail_templates.xml create mode 100644 addons/event_booth/models/__init__.py create mode 100644 addons/event_booth/models/event_booth.py create mode 100644 addons/event_booth/models/event_booth_category.py create mode 100644 addons/event_booth/models/event_event.py create mode 100644 addons/event_booth/models/event_type.py create mode 100644 addons/event_booth/models/event_type_booth.py create mode 100644 addons/event_booth/security/ir.model.access.csv create mode 100644 addons/event_booth/static/src/img/premium-booth.jpeg create mode 100644 addons/event_booth/static/src/img/standard-booth.jpeg create mode 100644 addons/event_booth/static/src/img/vip-booth.jpeg create mode 100644 addons/event_booth/tests/__init__.py create mode 100644 addons/event_booth/tests/common.py create mode 100644 addons/event_booth/tests/test_event_booth_internals.py create mode 100644 addons/event_booth/tests/test_event_internals.py create mode 100644 addons/event_booth/views/event_booth_category_views.xml create mode 100644 addons/event_booth/views/event_booth_views.xml create mode 100644 addons/event_booth/views/event_event_views.xml create mode 100644 addons/event_booth/views/event_menus.xml create mode 100644 addons/event_booth/views/event_type_booth_views.xml create mode 100644 addons/event_booth/views/event_type_views.xml diff --git a/addons/event_booth/__init__.py b/addons/event_booth/__init__.py new file mode 100644 index 00000000000..dc5e6b693d1 --- /dev/null +++ b/addons/event_booth/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/addons/event_booth/__manifest__.py b/addons/event_booth/__manifest__.py new file mode 100644 index 00000000000..22f4026f3e1 --- /dev/null +++ b/addons/event_booth/__manifest__.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': "Events Booths", + 'category': 'Marketing/Events', + 'version': '1.0', + 'summary': "Manage event booths", + 'description': """ +Create booths for your favorite event. + """, + 'depends': ['event'], + 'data': [ + 'security/ir.model.access.csv', + 'views/event_booth_category_views.xml', + 'views/event_type_booth_views.xml', + 'views/event_booth_views.xml', + 'views/event_type_views.xml', + 'views/event_event_views.xml', + 'views/event_menus.xml', + 'data/event_booth_category_data.xml', + 'data/mail_data.xml', + 'data/mail_templates.xml', + ], + 'demo': [ + 'data/event_booth_demo.xml', + 'data/event_type_demo.xml', + ], + 'license': 'LGPL-3', +} diff --git a/addons/event_booth/data/event_booth_category_data.xml b/addons/event_booth/data/event_booth_category_data.xml new file mode 100644 index 00000000000..e1815b49c31 --- /dev/null +++ b/addons/event_booth/data/event_booth_category_data.xml @@ -0,0 +1,106 @@ + + + + + Standard Booth + 1 + + +
+
+

Standard

+
+
    +
  • + 1 Branded Booth +
  • +
  • + 4m² +
  • +
  • + 46" display screen +
  • +
  • + 1 desk +
  • +
  • + Logo & link on website +
  • +
+
+
+
+ + + Premium Booth + 2 + + +
+
+

Premium

+
+
    +
  • + 1 Branded Booth +
  • +
  • + 4m² +
  • +
  • + 46" display screen +
  • +
  • + 1 desk +
  • +
  • + Logo & link on website +
  • +
  • + 50 words description on website +
  • +
  • + 10 + 1 passes +
  • +
+
+
+
+ + + VIP Booth + 3 + + +
+
+

VIP

+
+
    +
  • + 2 Branded Booth +
  • +
  • + 8m² +
  • +
  • + 2 x 46" display screens +
  • +
  • + 2 desks +
  • +
  • + Logo & link on website +
  • +
  • + 100 words description on website +
  • +
  • + 10 + 1 passes +
  • +
+
+
+
+ +
diff --git a/addons/event_booth/data/event_booth_demo.xml b/addons/event_booth/data/event_booth_demo.xml new file mode 100644 index 00000000000..36e5fd5bb88 --- /dev/null +++ b/addons/event_booth/data/event_booth_demo.xml @@ -0,0 +1,103 @@ + + + + + + Booth A1 + + + + + Booth A2 + + + + + Booth A3 + + + + + Premium Booth A4 + + + + + VIP Booth A5 + + + + + + + + + + Showbooth 1 + + + + + Showbooth 2 + + + + + Premium Showbooth 1 + + + + + Premium Showbooth 2 + + + + + + + Booth A1 + + + + + Booth A2 + + + + + Booth A3 + + + + + OpenWood Demonstrator 1 + + + + + OpenWood Demonstrator 2 + + + + + OpenWood Demonstrator 3 + + + + + Gold Booth 1 + + + + + Gold Booth 2 + + + + + Gold Booth 3 + + + + + diff --git a/addons/event_booth/data/event_type_demo.xml b/addons/event_booth/data/event_type_demo.xml new file mode 100644 index 00000000000..40d7ac10b36 --- /dev/null +++ b/addons/event_booth/data/event_type_demo.xml @@ -0,0 +1,25 @@ + + + + + Showbooth 1 + + + + + Showbooth 2 + + + + + Premium Showbooth 1 + + + + + Premium Showbooth 2 + + + + + diff --git a/addons/event_booth/data/mail_data.xml b/addons/event_booth/data/mail_data.xml new file mode 100644 index 00000000000..d46e2036395 --- /dev/null +++ b/addons/event_booth/data/mail_data.xml @@ -0,0 +1,10 @@ + + + + + Booth Booked + event.event + + + + diff --git a/addons/event_booth/data/mail_templates.xml b/addons/event_booth/data/mail_templates.xml new file mode 100644 index 00000000000..149ab0e7441 --- /dev/null +++ b/addons/event_booth/data/mail_templates.xml @@ -0,0 +1,39 @@ + + + + + + + + diff --git a/addons/event_booth/models/__init__.py b/addons/event_booth/models/__init__.py new file mode 100644 index 00000000000..dece3622c5a --- /dev/null +++ b/addons/event_booth/models/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import event_type_booth +from . import event_booth +from . import event_booth_category +from . import event_event +from . import event_type diff --git a/addons/event_booth/models/event_booth.py b/addons/event_booth/models/event_booth.py new file mode 100644 index 00000000000..7f6e6d6a07f --- /dev/null +++ b/addons/event_booth/models/event_booth.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models +from odoo.osv import expression + + +class EventBooth(models.Model): + _name = 'event.booth' + _description = 'Event Booth' + _inherit = [ + 'event.type.booth', + 'mail.thread', + 'mail.activity.mixin' + ] + + # owner + event_type_id = fields.Many2one(ondelete='set null', required=False) + event_id = fields.Many2one('event.event', string='Event', ondelete='cascade', required=True) + # customer + partner_id = fields.Many2one('res.partner', string='Renter', tracking=True, copy=False) + contact_name = fields.Char('Renter Name', compute='_compute_contact_name', readonly=False, store=True, copy=False) + contact_email = fields.Char('Renter Email', compute='_compute_contact_email', readonly=False, store=True, copy=False) + contact_mobile = fields.Char('Renter Mobile', compute='_compute_contact_mobile', readonly=False, store=True, copy=False) + contact_phone = fields.Char('Renter Phone', compute='_compute_contact_phone', readonly=False, store=True, copy=False) + # state + state = fields.Selection( + [('available', 'Available'), ('unavailable', 'Unavailable')], + string='Status', group_expand='_group_expand_states', + default='available', required=True, tracking=True, + help='Shows the availability of a Booth') + is_available = fields.Boolean(compute='_compute_is_available', search='_search_is_available') + + @api.depends('partner_id') + def _compute_contact_name(self): + for booth in self: + if not booth.contact_name: + booth.contact_name = booth.partner_id.name or False + + @api.depends('partner_id') + def _compute_contact_email(self): + for booth in self: + if not booth.contact_email: + booth.contact_email = booth.partner_id.email or False + + @api.depends('partner_id') + def _compute_contact_mobile(self): + for booth in self: + if not booth.contact_mobile: + booth.contact_mobile = booth.partner_id.mobile or False + + @api.depends('partner_id') + def _compute_contact_phone(self): + for booth in self: + if not booth.contact_phone: + booth.contact_phone = booth.partner_id.phone or False + + @api.depends('state') + def _compute_is_available(self): + for booth in self: + booth.is_available = booth.state == 'available' + + def _search_is_available(self, operator, operand): + negative = operator in expression.NEGATIVE_TERM_OPERATORS + if (negative and operand) or not operand: + return [('state', '=', 'unavailable')] + return [('state', '=', 'available')] + + def _group_expand_states(self, states, domain, order): + return [key for key, val in type(self).state.selection] + + @api.model_create_multi + def create(self, vals_list): + res = super(EventBooth, self.with_context(mail_create_nosubscribe=True)).create(vals_list) + unavailable_booths = res.filtered(lambda booth: not booth.is_available) + unavailable_booths._post_confirmation_message() + return res + + def write(self, vals): + to_confirm = self.filtered(lambda booth: booth.state == 'available') + wpartner = {} + if 'state' in vals or 'partner_id' in vals: + wpartner = dict( + (booth, booth.partner_id.ids) + for booth in self.filtered(lambda booth: booth.partner_id) + ) + + res = super(EventBooth, self).write(vals) + + if vals.get('state') == 'unavailable' or vals.get('partner_id'): + for booth in self: + booth.message_subscribe(booth.partner_id.ids) + for booth in self: + if wpartner.get(booth) and booth.partner_id.id not in wpartner[booth]: + booth.message_unsubscribe(wpartner[booth]) + + if vals.get('state') == 'unavailable': + to_confirm._action_post_confirm(vals) + + return res + + def _post_confirmation_message(self): + for booth in self: + booth.event_id.message_post_with_view( + 'event_booth.event_booth_booked_template', + values={ + 'booth': booth, + }, + subtype_id=self.env.ref('event_booth.mt_event_booth_booked').id, + ) + + def action_confirm(self, additional_values=None): + write_vals = dict({'state': 'unavailable'}, **additional_values or {}) + self.write(write_vals) + + def _action_post_confirm(self, write_vals): + self._post_confirmation_message() diff --git a/addons/event_booth/models/event_booth_category.py b/addons/event_booth/models/event_booth_category.py new file mode 100644 index 00000000000..64bda996c96 --- /dev/null +++ b/addons/event_booth/models/event_booth_category.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class EventBoothCategory(models.Model): + _name = 'event.booth.category' + _description = 'Event Booth Category' + _inherit = ['image.mixin'] + _order = 'sequence ASC' + + active = fields.Boolean(default=True) + name = fields.Char(string='Name', required=True, translate=True) + sequence = fields.Integer(string='Sequence', default=10) + description = fields.Html(string='Description', translate=True) + booth_ids = fields.One2many('event.booth', 'booth_category_id', string='Booths') diff --git a/addons/event_booth/models/event_event.py b/addons/event_booth/models/event_event.py new file mode 100644 index 00000000000..c9f5f8fa357 --- /dev/null +++ b/addons/event_booth/models/event_event.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models +from odoo import Command + + +class Event(models.Model): + _inherit = 'event.event' + + event_booth_ids = fields.One2many( + 'event.booth', 'event_id', string='Booths', copy=True, + compute='_compute_event_booth_ids', readonly=False, store=True) + event_booth_count = fields.Integer( + string='Total Booths', + compute='_compute_event_booth_count') + event_booth_count_available = fields.Integer( + string='Available Booths', + compute='_compute_event_booth_count') + event_booth_category_ids = fields.Many2many( + 'event.booth.category', compute='_compute_event_booth_category_ids') + event_booth_category_available_ids = fields.Many2many( + 'event.booth.category', compute='_compute_event_booth_category_available_ids', + help='Booth Category for which booths are still available. Used in frontend') + + @api.depends('event_type_id') + def _compute_event_booth_ids(self): + """ Update event configuration from its event type. Depends are set only + on event_type_id itself, not its sub fields. Purpose is to emulate an + onchange: if event type is changed, update event configuration. Changing + event type content itself should not trigger this method. + + When synchronizing booths: + + * lines that are available are removed; + * template lines are added; + """ + for event in self: + if not event.event_type_id and not event.event_booth_ids: + event.event_booth_ids = False + continue + + # booths to keep: those that are not available + booths_to_remove = event.event_booth_ids.filtered(lambda booth: booth.is_available) + command = [Command.unlink(booth.id) for booth in booths_to_remove] + if event.event_type_id.event_type_booth_ids: + command += [ + Command.create({ + attribute_name: line[attribute_name] if not isinstance(line[attribute_name], models.BaseModel) else line[attribute_name].id + for attribute_name in self.env['event.type.booth']._get_event_booth_fields_whitelist() + }) for line in event.event_type_id.event_type_booth_ids + ] + event.event_booth_ids = command + + def _get_booth_stat_count(self): + elements = self.env['event.booth'].sudo().read_group( + [('event_id', 'in', self.ids)], + ['event_id', 'state'], ['event_id', 'state'], lazy=False + ) + elements_total_count = dict() + elements_available_count = dict() + for element in elements: + event_id = element['event_id'][0] + if element['state'] == 'available': + elements_available_count[event_id] = element['__count'] + elements_total_count.setdefault(event_id, 0) + elements_total_count[event_id] += element['__count'] + return elements_available_count, elements_total_count + + @api.depends('event_booth_ids', 'event_booth_ids.state') + def _compute_event_booth_count(self): + if self.ids and all(bool(event.id) for event in self): # no new/onchange mode -> optimized + booths_available_count, booths_total_count = self._get_booth_stat_count() + for event in self: + event.event_booth_count_available = booths_available_count.get(event.id, 0) + event.event_booth_count = booths_total_count.get(event.id, 0) + else: + for event in self: + event.event_booth_count = len(event.event_booth_ids) + event.event_booth_count_available = len(event.event_booth_ids.filtered(lambda booth: booth.is_available)) + + @api.depends('event_booth_ids.booth_category_id') + def _compute_event_booth_category_ids(self): + for event in self: + event.event_booth_category_ids = event.event_booth_ids.mapped('booth_category_id') + + @api.depends('event_booth_ids.is_available') + def _compute_event_booth_category_available_ids(self): + for event in self: + event.event_booth_category_available_ids = event.event_booth_ids.filtered(lambda booth: booth.is_available).mapped('booth_category_id') diff --git a/addons/event_booth/models/event_type.py b/addons/event_booth/models/event_type.py new file mode 100644 index 00000000000..e557aa1f7a1 --- /dev/null +++ b/addons/event_booth/models/event_type.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class EventType(models.Model): + _inherit = 'event.type' + + event_type_booth_ids = fields.One2many( + 'event.type.booth', 'event_type_id', + string='Booths', readonly=False, store=True) diff --git a/addons/event_booth/models/event_type_booth.py b/addons/event_booth/models/event_type_booth.py new file mode 100644 index 00000000000..2c8fe636960 --- /dev/null +++ b/addons/event_booth/models/event_type_booth.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class EventBooth(models.Model): + _name = 'event.type.booth' + _description = 'Event Booth Template' + + def _get_default_booth_category(self): + """Assign booth category by default if only one exists""" + category_id = self.env['event.booth.category'].search([]) + if category_id and len(category_id) == 1: + return category_id + + name = fields.Char(string='Name', required=True, translate=True) + event_type_id = fields.Many2one( + 'event.type', string='Event Category', + ondelete='cascade', required=True) + booth_category_id = fields.Many2one( + 'event.booth.category', string='Booth Category', + default=_get_default_booth_category, ondelete='restrict', required=True) + + @api.model + def _get_event_booth_fields_whitelist(self): + return ['name', 'booth_category_id'] diff --git a/addons/event_booth/security/ir.model.access.csv b/addons/event_booth/security/ir.model.access.csv new file mode 100644 index 00000000000..e58a783cc84 --- /dev/null +++ b/addons/event_booth/security/ir.model.access.csv @@ -0,0 +1,9 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_event_booth_category,event.booth.category,model_event_booth_category,,0,0,0,0 +access_event_booth_category_desk,event.booth.category.desk,model_event_booth_category,event.group_event_registration_desk,1,0,0,0 +access_event_booth_category_manager,event.booth.category.manager,model_event_booth_category,event.group_event_manager,1,1,1,1 +access_event_booth_all,event.booth.public,model_event_booth,,0,0,0,0 +access_event_booth_user,event.booth.user,model_event_booth,event.group_event_registration_desk,1,0,0,0 +access_event_booth_manager,event.booth.manager,model_event_booth,event.group_event_manager,1,1,1,1 +access_event_type_booth_user,event.type.booth.user,model_event_type_booth,event.group_event_registration_desk,1,0,0,0 +access_event_type_booth_manager,event.type.booth.manager,model_event_type_booth,event.group_event_manager,1,1,1,1 diff --git a/addons/event_booth/static/src/img/premium-booth.jpeg b/addons/event_booth/static/src/img/premium-booth.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..45ba11eba1aa02d7c2e4e2bb08e213773c09645e GIT binary patch literal 27446 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 p^xm$%MuO9+rmBtCBPp?!+vlYhTnIFeWXEA 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 literal 0 HcmV?d00001 diff --git a/addons/event_booth/static/src/img/vip-booth.jpeg b/addons/event_booth/static/src/img/vip-booth.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..4e1e7c6ab20b7ba6cde86323c198eef5a524625c GIT binary patch literal 28968 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

', + }) + cls.event_booth_category_2 = cls.env['event.booth.category'].create({ + 'name': 'Premium', + 'description': '

Premium

', + }) + + cls.event_type_complex.write({ + 'event_type_booth_ids': [ + (5, 0), + (0, 0, + {'name': 'Standard 1', 'booth_category_id': cls.event_booth_category_1.id}), + (0, 0, + {'name': 'Premium 1', 'booth_category_id': cls.event_booth_category_2.id}), + ], + }) diff --git a/addons/event_booth/tests/test_event_booth_internals.py b/addons/event_booth/tests/test_event_booth_internals.py new file mode 100644 index 00000000000..529f1195536 --- /dev/null +++ b/addons/event_booth/tests/test_event_booth_internals.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime, timedelta + +from odoo.addons.event_booth.tests.common import TestEventBoothCommon +from odoo.fields import Datetime as FieldsDatetime +from odoo.tests.common import users + + +class TestEventData(TestEventBoothCommon): + + @users('user_eventmanager') + def test_event_booth_contact(self): + """ Test contact details computation """ + customer = self.env['res.partner'].browse(self.event_customer.ids) + category = self.env['event.booth.category'].browse(self.event_booth_category_1.ids) + self.assertTrue(all( + bool(customer[fname]) + for fname in ['name', 'email', 'country_id', 'phone'] + ) + ) + customer_email = customer.email + + event = self.env['event.event'].create({ + 'name': 'Event', + 'date_begin': FieldsDatetime.to_string(datetime.today() + timedelta(days=1)), + 'date_end': FieldsDatetime.to_string(datetime.today() + timedelta(days=15)), + 'event_type_id': False, + }) + self.assertEqual(event.event_booth_ids, self.env['event.booth']) + + booth = self.env['event.booth'].create({ + 'name': 'Test Booth', + 'booth_category_id': category.id, + 'event_id': event.id, + 'partner_id': customer.id, + }) + self.assertEqual(booth.contact_name, customer.name) + self.assertEqual(booth.contact_email, customer_email) + self.assertEqual(booth.contact_phone, customer.phone) + self.assertFalse(booth.contact_mobile, 'Data has no mobile') + + booth.write({ + 'contact_email': '"New Emails" + + + + event.booth.category.view.form + event.booth.category + +
+ + + +
+
+ + + + + + + + +
+
+
+
+ + + event.booth.category.view.tree + event.booth.category + + + + + + + + + + event.booth.category.view.search + event.booth.category + + + + + + + + + + Booth Category + event.booth.category + tree,form + +

+ Create a Booth Category +

+

+ Booth categories are used to represent the different types of booths you rent (Premium Booth, Table and Chairs, ...) +

+
+
+ +
diff --git a/addons/event_booth/views/event_booth_views.xml b/addons/event_booth/views/event_booth_views.xml new file mode 100644 index 00000000000..3e55ceeb092 --- /dev/null +++ b/addons/event_booth/views/event_booth_views.xml @@ -0,0 +1,224 @@ + + + + + event.booth.view.form.from.event + event.booth + 32 + +
+
+ +
+ +
+
+
+ + + + + + + + + + + + + +
+ + + +
+ + + + + + event.booth.view.form + event.booth + + primary + 16 + + + + + + + + + event.booth.view.form.simple.from.event + event.booth + + primary + 48 + + + + + + + + event.booth.view.tree.from.event + event.booth + 32 + + + + + + + + + + + + + + + + event.booth.view.tree + event.booth + + primary + 16 + + + + + + + + + event.booth.view.kanban + event.booth + 32 + + + + + +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+ + + event.booth.view.kanban + event.booth + + primary + 16 + + + + + + + + + event.booth.view.form.quick_create + event.booth + +
+ + + + +
+
+
+ + + event.booth.view.search + event.booth + + + + + + + + + + + + + + + + + + Booths + event.booth + kanban,tree,form + [] + {} + +

+ Create a Booth +

+ Booths are the physical stands that you rent during your event. +

+
+
+ + + Booths + event.booth + kanban,tree,form + [('event_id', '=', active_id)] + {'default_event_id': active_id} + +

+ Create a Booth +

+ Booths are the physical stands that you rent during your event. +

+
+
+ + 1 + kanban + + + + + 2 + tree + + + + + 3 + form + + + + + diff --git a/addons/event_booth/views/event_event_views.xml b/addons/event_booth/views/event_event_views.xml new file mode 100644 index 00000000000..9116c78fe3d --- /dev/null +++ b/addons/event_booth/views/event_event_views.xml @@ -0,0 +1,37 @@ + + + + + event.event.view.form.inherit.event.booth + event.event + + + +
+ +
+ + + + + + +
+
+ +
diff --git a/addons/event_booth/views/event_menus.xml b/addons/event_booth/views/event_menus.xml new file mode 100644 index 00000000000..cce6ce4016c --- /dev/null +++ b/addons/event_booth/views/event_menus.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/addons/event_booth/views/event_type_booth_views.xml b/addons/event_booth/views/event_type_booth_views.xml new file mode 100644 index 00000000000..df5be278db5 --- /dev/null +++ b/addons/event_booth/views/event_type_booth_views.xml @@ -0,0 +1,85 @@ + + + + + event.type.booth.view.form.from.type + event.type.booth + +
+ +
+
+ + + +
+
+
+
+ + + event.type.booth.view.form + event.type.booth + + primary + 10 + + + + + + + + + event.type.booth.view.tree.from.type + event.type.booth + + + + + + + + + + event.type.booth.view.tree + event.type.booth + + primary + 10 + + + + + + + + + event.type.booth.view.search + event.type.booth + + + + + + + + + + + + Event Type Booths + event.type.booth + tree,form + +

+ Create a Type Booth +

+ Booths are the physical stands that you rent during your event. +

+
+
+ +
diff --git a/addons/event_booth/views/event_type_views.xml b/addons/event_booth/views/event_type_views.xml new file mode 100644 index 00000000000..c4d5cb490b2 --- /dev/null +++ b/addons/event_booth/views/event_type_views.xml @@ -0,0 +1,22 @@ + + + + + event.type.view.form.inherit.event.booth + event.type + + + + + + + + + + + From b3c1141f8b0853619d3da77fb75f6722347e881b Mon Sep 17 00:00:00 2001 From: Patrick Hoste Date: Tue, 19 Jan 2021 12:47:33 +0000 Subject: [PATCH 05/10] [ADD] event_booth_sale: add sale capabilities to event_booth PURPOSE This module allows to sell booths to partners by setting a product and a price to the booths categories. When selling booths on a sale order it will open the product configurator and ask the event, booth category and booths. It's only possible to select booths belonging to the same category for one sale order line. A new model is introduced by this module : - Booth Registration that allows multiple registrations to a booth. The booth will be set unavailable when the sale order linked to the registration is paid. All the others registrations will be cancelled LINKS Task-2318655 Closes : odoo/odoo#71104 --- addons/event_booth_sale/__init__.py | 5 + addons/event_booth_sale/__manifest__.py | 34 ++++ .../data/event_booth_category_data.xml | 19 +++ addons/event_booth_sale/data/product_data.xml | 15 ++ addons/event_booth_sale/models/__init__.py | 11 ++ .../event_booth_sale/models/account_move.py | 16 ++ addons/event_booth_sale/models/event_booth.py | 49 ++++++ .../models/event_booth_category.py | 84 ++++++++++ .../models/event_booth_registration.py | 78 +++++++++ .../models/event_type_booth.py | 16 ++ addons/event_booth_sale/models/product.py | 26 +++ addons/event_booth_sale/models/sale_order.py | 35 ++++ .../models/sale_order_line.py | 97 ++++++++++++ .../security/ir.model.access.csv | 5 + ...vent_booth_configurator_form_controller.js | 44 ++++++ .../js/event_booth_configurator_form_view.js | 21 +++ .../src/js/event_booth_configurator_widget.js | 149 ++++++++++++++++++ addons/event_booth_sale/tests/__init__.py | 4 + addons/event_booth_sale/tests/common.py | 24 +++ .../tests/test_event_internals.py | 95 +++++++++++ .../views/event_booth_category_views.xml | 32 ++++ .../views/event_booth_registration_views.xml | 46 ++++++ .../views/event_booth_views.xml | 63 ++++++++ .../views/event_type_booth_views.xml | 28 ++++ .../event_booth_sale/views/product_views.xml | 15 ++ .../views/sale_order_views.xml | 27 ++++ addons/event_booth_sale/wizard/__init__.py | 4 + .../wizard/event_booth_configurator.py | 34 ++++ .../wizard/event_booth_configurator_views.xml | 44 ++++++ 29 files changed, 1120 insertions(+) create mode 100644 addons/event_booth_sale/__init__.py create mode 100644 addons/event_booth_sale/__manifest__.py create mode 100644 addons/event_booth_sale/data/event_booth_category_data.xml create mode 100644 addons/event_booth_sale/data/product_data.xml create mode 100644 addons/event_booth_sale/models/__init__.py create mode 100644 addons/event_booth_sale/models/account_move.py create mode 100644 addons/event_booth_sale/models/event_booth.py create mode 100644 addons/event_booth_sale/models/event_booth_category.py create mode 100644 addons/event_booth_sale/models/event_booth_registration.py create mode 100644 addons/event_booth_sale/models/event_type_booth.py create mode 100644 addons/event_booth_sale/models/product.py create mode 100644 addons/event_booth_sale/models/sale_order.py create mode 100644 addons/event_booth_sale/models/sale_order_line.py create mode 100644 addons/event_booth_sale/security/ir.model.access.csv create mode 100644 addons/event_booth_sale/static/src/js/event_booth_configurator_form_controller.js create mode 100644 addons/event_booth_sale/static/src/js/event_booth_configurator_form_view.js create mode 100644 addons/event_booth_sale/static/src/js/event_booth_configurator_widget.js create mode 100644 addons/event_booth_sale/tests/__init__.py create mode 100644 addons/event_booth_sale/tests/common.py create mode 100644 addons/event_booth_sale/tests/test_event_internals.py create mode 100644 addons/event_booth_sale/views/event_booth_category_views.xml create mode 100644 addons/event_booth_sale/views/event_booth_registration_views.xml create mode 100644 addons/event_booth_sale/views/event_booth_views.xml create mode 100644 addons/event_booth_sale/views/event_type_booth_views.xml create mode 100644 addons/event_booth_sale/views/product_views.xml create mode 100644 addons/event_booth_sale/views/sale_order_views.xml create mode 100644 addons/event_booth_sale/wizard/__init__.py create mode 100644 addons/event_booth_sale/wizard/event_booth_configurator.py create mode 100644 addons/event_booth_sale/wizard/event_booth_configurator_views.xml diff --git a/addons/event_booth_sale/__init__.py b/addons/event_booth_sale/__init__.py new file mode 100644 index 00000000000..2ae6446f9dc --- /dev/null +++ b/addons/event_booth_sale/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models +from . import wizard diff --git a/addons/event_booth_sale/__manifest__.py b/addons/event_booth_sale/__manifest__.py new file mode 100644 index 00000000000..dc61102cce1 --- /dev/null +++ b/addons/event_booth_sale/__manifest__.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': "Events Booths Sales", + 'category': 'Marketing/Events', + 'version': '1.0', + 'summary': "Manage event booths sale", + 'description': """ +Sell your event booths and track payments on sale orders. + """, + 'depends': ['event_booth', 'event_sale'], + 'data': [ + 'security/ir.model.access.csv', + 'data/product_data.xml', + 'data/event_booth_category_data.xml', + 'views/product_views.xml', + 'views/sale_order_views.xml', + 'views/event_type_booth_views.xml', + 'views/event_booth_category_views.xml', + 'views/event_booth_registration_views.xml', + 'views/event_booth_views.xml', + 'wizard/event_booth_configurator_views.xml', + ], + 'auto_install': True, + 'assets': { + 'web.assets_backend': [ + 'event_booth_sale/static/src/js/event_booth_configurator_form_controller.js', + 'event_booth_sale/static/src/js/event_booth_configurator_form_view.js', + 'event_booth_sale/static/src/js/event_booth_configurator_widget.js', + ] + }, + 'license': 'LGPL-3', +} diff --git a/addons/event_booth_sale/data/event_booth_category_data.xml b/addons/event_booth_sale/data/event_booth_category_data.xml new file mode 100644 index 00000000000..dcbd5a75570 --- /dev/null +++ b/addons/event_booth_sale/data/event_booth_category_data.xml @@ -0,0 +1,19 @@ + + + + + + 100 + + + + + 500 + + + + + 1000 + + + diff --git a/addons/event_booth_sale/data/product_data.xml b/addons/event_booth_sale/data/product_data.xml new file mode 100644 index 00000000000..2a5f6befe94 --- /dev/null +++ b/addons/event_booth_sale/data/product_data.xml @@ -0,0 +1,15 @@ + + + + + Event Booth + + + 100.0 + + + order + service + + + diff --git a/addons/event_booth_sale/models/__init__.py b/addons/event_booth_sale/models/__init__.py new file mode 100644 index 00000000000..a5fe40c3823 --- /dev/null +++ b/addons/event_booth_sale/models/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import account_move +from . import product +from . import event_booth_registration +from . import event_booth +from . import event_booth_category +from . import event_type_booth +from . import sale_order +from . import sale_order_line diff --git a/addons/event_booth_sale/models/account_move.py b/addons/event_booth_sale/models/account_move.py new file mode 100644 index 00000000000..b3d7f10bdf1 --- /dev/null +++ b/addons/event_booth_sale/models/account_move.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class AccountMove(models.Model): + _inherit = 'account.move' + + def action_invoice_paid(self): + """ When an invoice linked to a sales order selling registrations is + paid, update booths accordingly as they are booked when invoice is paid. + """ + res = super(AccountMove, self).action_invoice_paid() + self.mapped('line_ids.sale_line_ids')._update_event_booths(set_paid=True) + return res diff --git a/addons/event_booth_sale/models/event_booth.py b/addons/event_booth_sale/models/event_booth.py new file mode 100644 index 00000000000..e03df7942a4 --- /dev/null +++ b/addons/event_booth_sale/models/event_booth.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + + +class EventBooth(models.Model): + _inherit = 'event.booth' + + # registrations + event_booth_registration_ids = fields.One2many('event.booth.registration', 'event_booth_id') + # sale information + sale_order_line_registration_ids = fields.Many2many( + 'sale.order.line', 'event_booth_registration', + 'event_booth_id', 'sale_order_line_id', string='SO Lines with reservations', + groups='sales_team.group_sale_salesman', copy=False) + sale_order_line_id = fields.Many2one( + 'sale.order.line', string='Final Sale Order Line', ondelete='set null', + readonly=True, states={'available': [('readonly', False)]}, + groups='sales_team.group_sale_salesman', copy=False) + sale_order_id = fields.Many2one( + related='sale_order_line_id.order_id', store='True', readonly=True, + groups='sales_team.group_sale_salesman') + is_paid = fields.Boolean('Is Paid', copy=False) + + @api.ondelete(at_uninstall=False) + def _unlink_except_linked_sale_order(self): + booth_with_so = self.filtered('sale_order_id') + if booth_with_so: + raise UserError(_( + 'You can\'t delete the following booths as they are linked to sales orders: ' + '%(booths)s', booths=', '.join(booth_with_so.mapped('name')))) + + def action_set_paid(self): + self.write({'is_paid': True}) + + def action_view_sale_order(self): + self.sale_order_id.ensure_one() + action = self.env['ir.actions.actions']._for_xml_id('sale.action_orders') + action['views'] = [(False, 'form')] + action['res_id'] = self.sale_order_id.id + return action + + def _get_booth_multiline_description(self): + return '%s : \n%s' % ( + self.event_id.display_name, + '\n'.join(['- %s' % booth.name for booth in self]) + ) diff --git a/addons/event_booth_sale/models/event_booth_category.py b/addons/event_booth_sale/models/event_booth_category.py new file mode 100644 index 00000000000..44fc51b9e83 --- /dev/null +++ b/addons/event_booth_sale/models/event_booth_category.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class EventBoothCategory(models.Model): + _inherit = 'event.booth.category' + + def _default_product_id(self): + return self.env.ref('event_booth_sale.product_product_event_booth', raise_if_not_found=False) + + product_id = fields.Many2one( + 'product.product', string='Product', required=True, + domain=[('is_event_booth', '=', True)], default=_default_product_id) + price = fields.Float(string='Price', compute='_compute_price', digits='Product Price', readonly=False, store=True) + price_reduce = fields.Float( + string='Price Reduce', compute='_compute_price_reduce', + compute_sudo=True, digits='Product Price') + image_1920 = fields.Image(compute='_compute_image_1920', readonly=False, store=True) + + @api.depends('product_id') + def _compute_image_1920(self): + for category in self: + category.image_1920 = category.image_1920 if category.image_1920 else category.product_id.image_1920 + + @api.depends('product_id') + def _compute_price(self): + """ By default price comes from category but can be changed by event + people as product may be shared accross various categories. """ + for category in self: + if category.product_id and category.product_id.list_price: + category.price = category.product_id.list_price + category.product_id.price_extra + + @api.depends('product_id', 'price') + def _compute_price_reduce(self): + for category in self: + product = category.product_id + list_price = product.list_price + product.price_extra + discount = (list_price - product.price) / list_price if list_price else 0.0 + category.price_reduce = (1.0 - discount) * category.price + + def _init_column(self, column_name): + """ Initialize product_id for existing columns when installing sale + bridge, to ensure required attribute is fulfilled. """ + if column_name != "product_id": + return super(EventBoothCategory, self)._init_column(column_name) + + # fetch void columns + self.env.cr.execute("SELECT id FROM %s WHERE product_id IS NULL" % self._table) + booth_category_ids = self.env.cr.fetchall() + if not booth_category_ids: + return + + # update existing columns + _logger.debug("Table '%s': setting default value of new column %s to unique values for each row", + self._table, column_name) + default_booth_product = self._default_product_id() + if default_booth_product: + product_id = default_booth_product.id + else: + product_id = self.env['product.product'].create({ + 'name': 'Generic Event Booth Product', + 'categ_id': self.env.ref('event_sale.product_category_events').id, + 'list_price': 100, + 'standard_price': 0, + 'is_event_booth': True, + 'type': 'service', + 'invoice_policy': 'order', + }).id + self.env['ir.model.data'].create({ + 'name': 'product_product_event_booth', + 'module': 'event_booth_sale', + 'model': 'product.product', + 'res_id': product_id, + }) + self.env.cr._obj.execute( + f'UPDATE {self._table} SET product_id = %s WHERE id IN %s;', + (product_id, tuple(booth_category_ids)) + ) diff --git a/addons/event_booth_sale/models/event_booth_registration.py b/addons/event_booth_sale/models/event_booth_registration.py new file mode 100644 index 00000000000..a08a4bfc845 --- /dev/null +++ b/addons/event_booth_sale/models/event_booth_registration.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ + + +class EventBoothRegistration(models.Model): + """event.booth.registrations are used to allow multiple partners to book the same booth. + Whenever a partner has paid his registration all the others linked to the booth will be deleted.""" + + _name = 'event.booth.registration' + _description = 'Event Booth Registration' + + sale_order_line_id = fields.Many2one('sale.order.line', string='Sale Order Line', required=True) + event_booth_id = fields.Many2one('event.booth', string='Booth', required=True) + partner_id = fields.Many2one( + 'res.partner', related='sale_order_line_id.order_partner_id', store=True) + contact_name = fields.Char(string='Contact Name', compute='_compute_contact_name', readonly=False, store=True) + contact_email = fields.Char(string='Contact Email', compute='_compute_contact_email', readonly=False, store=True) + contact_phone = fields.Char(string='Contact Phone', compute='_compute_contact_phone', readonly=False, store=True) + contact_mobile = fields.Char(string='Contact Mobile', compute='_compute_contact_mobile', readonly=False, store=True) + + _sql_constraints = [('unique_registration', 'unique(sale_order_line_id, event_booth_id)', + 'There can be only one registration for a booth by sale order line')] + + @api.depends('partner_id') + def _compute_contact_name(self): + for registration in self: + if not registration.contact_name: + registration.contact_name = registration.partner_id.name or False + + @api.depends('partner_id') + def _compute_contact_email(self): + for registration in self: + if not registration.contact_email: + registration.contact_email = registration.partner_id.email or False + + @api.depends('partner_id') + def _compute_contact_phone(self): + for registration in self: + if not registration.contact_phone: + registration.contact_phone = registration.partner_id.phone or False + + @api.depends('partner_id') + def _compute_contact_mobile(self): + for registration in self: + if not registration.contact_mobile: + registration.contact_mobile = registration.partner_id.mobile or False + + @api.model + def _get_fields_for_booth_confirmation(self): + return ['sale_order_line_id', 'partner_id', 'contact_name', 'contact_email', 'contact_phone', 'contact_mobile'] + + def action_confirm(self): + for registration in self: + values = { + field: registration[field].id if isinstance(registration[field], models.BaseModel) else registration[field] + for field in self._get_fields_for_booth_confirmation() + } + registration.event_booth_id.action_confirm(values) + self._cancel_pending_registrations() + + def _cancel_pending_registrations(self): + body = '

%(message)s:

    %(booth_names)s

' % { + 'message': _('Your order has been cancelled because the following booths have been reserved'), + 'booth_names': ''.join('
  • %s
  • ' % booth.display_name for booth in self.event_booth_id) + } + other_registrations = self.search([ + ('event_booth_id', 'in', self.event_booth_id.ids), + ('id', 'not in', self.ids) + ]) + for order in other_registrations.sale_order_line_id.order_id: + order.sudo().message_post( + body=body, + partner_ids=order.user_id.partner_id.ids, + ) + order.sudo().action_cancel() + other_registrations.unlink() diff --git a/addons/event_booth_sale/models/event_type_booth.py b/addons/event_booth_sale/models/event_type_booth.py new file mode 100644 index 00000000000..c5318e29611 --- /dev/null +++ b/addons/event_booth_sale/models/event_type_booth.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class EventTypeBooth(models.Model): + _inherit = 'event.type.booth' + + product_id = fields.Many2one(related='booth_category_id.product_id') + price = fields.Float(related='booth_category_id.price') + + @api.model + def _get_event_booth_fields_whitelist(self): + res = super(EventTypeBooth, self)._get_event_booth_fields_whitelist() + return res + ['product_id', 'price'] diff --git a/addons/event_booth_sale/models/product.py b/addons/event_booth_sale/models/product.py new file mode 100644 index 00000000000..62f4a8ac0b1 --- /dev/null +++ b/addons/event_booth_sale/models/product.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + is_event_booth = fields.Boolean(string='Is an Event Booth') + + @api.onchange('is_event_booth') + def _onchange_is_event_booth(self): + if self.is_event_booth: + self.type = 'service' + self.invoice_policy = 'order' + + +class Product(models.Model): + _inherit = 'product.product' + + @api.onchange('is_event_booth') + def _onchange_is_event_booth(self): + if self.is_event_booth: + self.type = 'service' + self.invoice_policy = 'order' diff --git a/addons/event_booth_sale/models/sale_order.py b/addons/event_booth_sale/models/sale_order.py new file mode 100644 index 00000000000..1569533272c --- /dev/null +++ b/addons/event_booth_sale/models/sale_order.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + event_booth_ids = fields.One2many('event.booth', 'sale_order_id', string='Booths') + event_booth_count = fields.Integer(string='Booth Count', compute='_compute_event_booth_count') + + @api.depends('event_booth_ids') + def _compute_event_booth_count(self): + if self.ids: + slot_data = self.env['event.booth'].read_group( + [('sale_order_id', 'in', self.ids)], + ['sale_order_id'], ['sale_order_id'] + ) + slot_mapped = dict((data['sale_order_id'][0], data['sale_order_id_count']) for data in slot_data) + else: + slot_mapped = dict() + for so in self: + so.event_booth_count = slot_mapped.get(so.id, 0) + + def action_confirm(self): + res = super(SaleOrder, self).action_confirm() + for so in self: + so.order_line._update_event_booths() + return res + + def action_view_booth_list(self): + action = self.env['ir.actions.act_window']._for_xml_id('event_booth.event_booth_action') + action['domain'] = [('sale_order_id', 'in', self.ids)] + return action diff --git a/addons/event_booth_sale/models/sale_order_line.py b/addons/event_booth_sale/models/sale_order_line.py new file mode 100644 index 00000000000..9255c61d2ea --- /dev/null +++ b/addons/event_booth_sale/models/sale_order_line.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + event_booth_category_id = fields.Many2one('event.booth.category', string='Booths Category', ondelete='set null') + event_booth_pending_ids = fields.Many2many( + 'event.booth', string='Pending Booths', search='_search_event_booth_pending_ids', + compute='_compute_event_booth_pending_ids', inverse='_inverse_event_booth_pending_ids', + help='Used to create registration when providing the desired event booth.' + ) + event_booth_registration_ids = fields.One2many( + 'event.booth.registration', 'sale_order_line_id', string='Confirmed Registration') + event_booth_ids = fields.One2many('event.booth', 'sale_order_line_id', string='Confirmed Booths') + is_event_booth = fields.Boolean(related='product_id.is_event_booth', readonly=True) + + @api.depends('event_booth_ids') + def _compute_name_short(self): + wbooth = self.filtered(lambda line: line.event_booth_pending_ids) + for record in wbooth: + record.name_short = record.event_booth_pending_ids.event_id.name + super(SaleOrderLine, self - wbooth)._compute_name_short() + + @api.depends('event_booth_registration_ids') + def _compute_event_booth_pending_ids(self): + for so_line in self: + so_line.event_booth_pending_ids = so_line.event_booth_registration_ids.event_booth_id + + def _inverse_event_booth_pending_ids(self): + for so_line in self: + self.env['event.booth.registration'].create([{ + 'event_booth_id': booth.id, + 'sale_order_line_id': so_line.id, + 'partner_id': so_line.order_id.partner_id.id + } for booth in so_line.event_booth_pending_ids]) + + def _search_event_booth_pending_ids(self, operator, value): + return [('event_booth_registration_ids.event_booth_id', operator, value)] + + @api.constrains('event_booth_registration_ids') + def _check_event_booth_registration_ids(self): + if len(self.event_booth_registration_ids.event_booth_id.event_id) > 1: + raise ValidationError(_('Registrations from the same Order Line must belong to a single event.')) + + @api.onchange('product_id') + def _onchange_product_id_booth(self): + """We reset the event when the selected product doesn't belong to any pending booths.""" + if self.event_id and (not self.product_id or self.product_id not in self.event_booth_pending_ids.product_id): + self.event_id = None + + @api.onchange('event_id') + def _onchange_event_id_booth(self): + """We reset the pending booths when the event changes to avoid inconsistent state.""" + if self.event_booth_pending_ids and (not self.event_id or self.event_id != self.event_booth_pending_ids.event_id): + self.event_booth_pending_ids = None + + @api.onchange('event_booth_pending_ids') + def _onchange_event_booth_pending_ids(self): + # trigger the description computation + self.product_id_change() + + def _update_event_booths(self, set_paid=False): + for so_line in self.filtered('is_event_booth'): + if so_line.event_booth_pending_ids and not so_line.event_booth_ids: + unavailable = so_line.event_booth_pending_ids.filtered(lambda booth: not booth.is_available) + if unavailable: + raise ValidationError( + _('The following booths are unavailable, please remove them to continue : %(booth_names)s', + booth_names=''.join('\n\t- %s' % booth.display_name for booth in unavailable))) + so_line.event_booth_registration_ids.action_confirm() + if so_line.event_booth_ids and set_paid: + so_line.event_booth_ids.action_set_paid() + return True + + def get_sale_order_line_multiline_description_sale(self, product): + if self.event_booth_pending_ids: + booths = self.event_booth_pending_ids.with_context( + lang=self.order_id.partner_id.lang, + ) + return booths._get_booth_multiline_description() + return super(SaleOrderLine, self).get_sale_order_line_multiline_description_sale(product) + + def _get_display_price(self, product): + if self.event_booth_pending_ids and self.event_id: + company = self.event_id.company_id or self.env.company + currency = company.currency_id + total_price = sum([booth.price for booth in self.event_booth_pending_ids]) + return currency._convert( + total_price, self.order_id.currency_id, + self.order_id.company_id or self.env.company.id, + self.order_id.date_order or fields.Date.today()) + return super(SaleOrderLine, self)._get_display_price(product) diff --git a/addons/event_booth_sale/security/ir.model.access.csv b/addons/event_booth_sale/security/ir.model.access.csv new file mode 100644 index 00000000000..e375afb8eb3 --- /dev/null +++ b/addons/event_booth_sale/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_event_booth_registration_salesman,access.event.booth.registration.salesman,model_event_booth_registration,sales_team.group_sale_salesman,1,1,1,1 +access_event_booth_registration_event_desk,access.event.booth.registration.event.desk,model_event_booth_registration,event.group_event_registration_desk,1,0,0,0 +access_event_booth_registration_event_user,access.event.booth.registration.event.user,model_event_booth_registration,event.group_event_user,1,1,1,1 +access_event_booth_configurator_salesman,access.event.booth.configurator.salesman,model_event_booth_configurator,sales_team.group_sale_salesman,1,1,1,0 diff --git a/addons/event_booth_sale/static/src/js/event_booth_configurator_form_controller.js b/addons/event_booth_sale/static/src/js/event_booth_configurator_form_controller.js new file mode 100644 index 00000000000..393fc7fdc9a --- /dev/null +++ b/addons/event_booth_sale/static/src/js/event_booth_configurator_form_controller.js @@ -0,0 +1,44 @@ +odoo.define('event_booth_sale.event_booth_configurator_form_controller', function (require) { +'use strict'; + +const FormController = require('web.FormController'); +/** + * This controller is overridden to allow configuring sale_order_lines through a popup + * window when a product with 'is_event_booth' is selected. + * + * This allows keeping an editable list view for sales order and remove the noise of + * those 3 fields ('event_id', 'event_booth_category_id' and 'event_booth_ids') + */ +const EventBoothConfiguratorFormController = FormController.extend({ + + //-------------------------------------------------------------------------- + // Overrides + //-------------------------------------------------------------------------- + + saveRecord: function () { + const self = this; + return this._super.apply(this, arguments).then(function () { + const state = self.model.get(self.handle, {raw: true}); + self.do_action({ + type: 'ir.actions.act_window_close', + infos: { + eventBoothConfiguration: { + event_id: {id: state.data.event_id}, + event_booth_category_id: {id: state.data.event_booth_category_id}, + event_booth_pending_ids: { + operation: 'MULTI', + commands: [{ + operation: 'REPLACE_WITH', + ids: state.data.event_booth_ids, + }], + } + } + } + }); + }); + } +}); + +return EventBoothConfiguratorFormController; + +}); diff --git a/addons/event_booth_sale/static/src/js/event_booth_configurator_form_view.js b/addons/event_booth_sale/static/src/js/event_booth_configurator_form_view.js new file mode 100644 index 00000000000..b1f7d849cbf --- /dev/null +++ b/addons/event_booth_sale/static/src/js/event_booth_configurator_form_view.js @@ -0,0 +1,21 @@ +odoo.define('event_booth_sale.event_booth_configurator_form_view', function (require) { +'use strict'; + +const EventBoothConfiguratorFormController = require('event_booth_sale.event_booth_configurator_form_controller'); +const FormView = require('web.FormView'); +const viewRegistry = require('web.view_registry'); + +/** + * @see EventBoothConfiguratorFormController for more information. + */ +const EventBoothConfiguratorFormView = FormView.extend({ + config: _.extend({}, FormView.prototype.config, { + Controller: EventBoothConfiguratorFormController, + }), +}); + +viewRegistry.add('event_booth_configurator_form', EventBoothConfiguratorFormView); + +return EventBoothConfiguratorFormView; + +}); diff --git a/addons/event_booth_sale/static/src/js/event_booth_configurator_widget.js b/addons/event_booth_sale/static/src/js/event_booth_configurator_widget.js new file mode 100644 index 00000000000..ce6595a740b --- /dev/null +++ b/addons/event_booth_sale/static/src/js/event_booth_configurator_widget.js @@ -0,0 +1,149 @@ +odoo.define('event_booth_sale.product_configurator', function (require) { +const ProductConfiguratorWidget = require('sale.product_configurator'); + +/** + * Extension of the ProductConfiguratorWidget to support event booth configuration. + * It opens when an event booth product_product is set. + * + * The event booth information include: + * - event_id + * - event_booth_category_id + * - event_booth_ids + * + */ +ProductConfiguratorWidget.include({ + + //-------------------------------------------------------------------------- + // Overrides + //-------------------------------------------------------------------------- + + /** + * @returns {boolean} + * + * @override + * @private + */ + _isConfigurableLine: function () { + return this.recordData.is_event_booth || this._super.apply(this, arguments); + }, + + /** + * Opens the event booth configurator in 'edit' mode + * + * @override + * @private + */ + _onEditLineConfiguration: function () { + if (this.recordData.is_event_booth) { + const defaultValues = { + default_product_id: this.recordData.product_id.data.id, + default_sale_order_line_id: this.recordData.id || null + }; + if (this.recordData.event_id) { + defaultValues.default_event_id = this.recordData.event_id.data.id; + } + if (this.recordData.event_booth_category_id) { + defaultValues.default_event_booth_category_id = this.recordData.event_booth_category_id.data.id; + } + if (this.recordData.event_booth_pending_ids) { + defaultValues.default_event_booth_ids = this.recordData.event_booth_pending_ids.res_ids; + } + this._openEventBoothConfigurator(defaultValues, this.dataPointID); + } else { + this._super.apply(this, arguments); + } + }, + + /** + * @param {integer} productId: product.product ID + * @param {String} dataPointId + * @returns {Promise} stopPropagation true if a suitable configurator has been found. + * + * @override + * @private + */ + _onProductChange: function (productId, dataPointId) { + return this._super.apply(this, arguments).then((stopPropagation) => { + if (stopPropagation) { + return Promise.resolve(true); + } else { + return this._isProductEventBooth(productId, dataPointId); + } + }); + }, + + //-------------------------------------------------------------------------- + // Private + //------------------------------------------------------------------------- + + /** + * This method will check if the productId needs a configuration or not. + * + * @param {integer} productId: product.product ID + * @param {String} dataPointId + * @returns {Promise} stopPropagation true if the product is an event booth. + * + * @private + */ + _isProductEventBooth: function (productId, dataPointId) { + const self = this; + return this._rpc({ + model: 'product.product', + method: 'read', + args: [productId, ['is_event_booth']], + }).then(function (result) { + if (result && result[0].is_event_booth) { + self._openEventBoothConfigurator({ + default_product_id: productId + }, dataPointId); + return Promise.resolve(true); + } + return Promise.resolve(false); + }); + }, + + /** + * Opens the event booth configurator allowing to configure the SO line with its informations. + * + * When the window is closed, configured values are used to trigger a 'field_changed' + * event to modify the current SO line. + * + * If the window is closed without providing the required values 'event_id', 'event_booth_category_id' + * and 'event_booth_pending_ids', the product_id field is cleaned. + * + * @param {Object} additionalContext: various "default_" values added to context when calling + * configuration wizard model; + * @param dataPointId + * + * @private + */ + _openEventBoothConfigurator: function (additionalContext, dataPointId) { + const self = this; + this.do_action('event_booth_sale.event_booth_configurator_action', { + additional_context: additionalContext, + on_close: function (result) { + if (result && !result.special) { + self.trigger_up('field_changed', { + changes: result.eventBoothConfiguration, + dataPointID: dataPointId, + onSuccess: function () { + self._onLineConfigured(); + } + }); + } else if (!self.recordData.event_id || !self.recordData.event_booth_pending_ids) { + self.trigger_up('field_changed', { + changes: { + product_id: false, + name: '', + }, + dataPointID: dataPointId, + }); + } + } + }); + }, +}); + +return ProductConfiguratorWidget; + +}); diff --git a/addons/event_booth_sale/tests/__init__.py b/addons/event_booth_sale/tests/__init__.py new file mode 100644 index 00000000000..93a5521bfb0 --- /dev/null +++ b/addons/event_booth_sale/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_event_internals diff --git a/addons/event_booth_sale/tests/common.py b/addons/event_booth_sale/tests/common.py new file mode 100644 index 00000000000..15930921621 --- /dev/null +++ b/addons/event_booth_sale/tests/common.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.event_booth.tests.common import TestEventBoothCommon +from odoo.addons.event_sale.tests.common import TestEventSaleCommon + + +class TestEventBoothSaleCommon(TestEventBoothCommon, TestEventSaleCommon): + + @classmethod + def setUpClass(cls): + super(TestEventBoothSaleCommon, cls).setUpClass() + + cls.event_booth_product = cls.env['product.product'].create({ + 'name': 'Test Booth Product', + 'description_sale': 'Mighty Booth Description', + 'list_price': 20, + 'is_event_booth': True, + 'standard_price': 60.0, + 'type': 'service', + }) + (cls.event_booth_category_1 + cls.event_booth_category_2).write({ + 'product_id': cls.event_booth_product.id, + }) diff --git a/addons/event_booth_sale/tests/test_event_internals.py b/addons/event_booth_sale/tests/test_event_internals.py new file mode 100644 index 00000000000..e745e2eafff --- /dev/null +++ b/addons/event_booth_sale/tests/test_event_internals.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime, timedelta + +from odoo import Command +from odoo.addons.event_booth_sale.tests.common import TestEventBoothSaleCommon +from odoo.fields import Datetime as FieldsDatetime +from odoo.tests.common import users, Form + + +class TestEventData(TestEventBoothSaleCommon): + + @users('user_eventmanager') + def test_event_configuration_booths_from_type(self): + """ Test data computation (related to booths) of event coming from its event.type template. """ + # setup test records + event_type_nobooth = self.env['event.type'].create({ + 'name': 'No booth', + }) + event_type_wbooths = self.env['event.type'].create({ + 'name': 'Using booths', + 'event_type_booth_ids': [ + Command.clear(), + Command.create({ + 'name': 'Standard Booth', + 'booth_category_id': self.event_booth_category_1.id, + }), + Command.create({ + 'name': 'Premium Booth', + 'booth_category_id': self.event_booth_category_2.id, + }) + ] + }) + + # no booth by default as no booths on type + event = self.env['event.event'].create({ + 'name': 'Event', + 'date_begin': FieldsDatetime.to_string(datetime.today() + timedelta(days=1)), + 'date_end': FieldsDatetime.to_string(datetime.today() + timedelta(days=15)), + 'event_type_id': event_type_nobooth.id + }) + self.assertEqual(event.event_booth_ids, self.env['event.booth']) + + # manually create booths: ok + event.write({ + 'event_booth_ids': [ + Command.create({ + 'name': 'Custom Standard Booth 1', + 'booth_category_id': self.event_booth_category_1.id, + }), + Command.create({ + 'name': 'Custom Standard Booth 2', + 'booth_category_id': self.event_booth_category_1.id, + }) + ] + }) + self.assertEqual(event.event_booth_count, 2) + self.assertEqual(event.event_booth_count_available, 2) + self.assertEqual(event.event_booth_ids.product_id, self.event_booth_product) + + # updating partner is independent from availability + event.event_booth_ids[1].write({'partner_id': self.event_customer.id}) + self.assertEqual(event.event_booth_count, 2) + self.assertEqual(event.event_booth_count_available, 2) + self.assertEqual(event.event_booth_ids[1].message_partner_ids, self.event_customer) + + # one booth is sold + event.event_booth_ids[1].write({'state': 'unavailable'}) + self.assertEqual(event.event_booth_count, 2) + self.assertEqual(event.event_booth_count_available, 1) + + # partner is reset: booth still unavailable but follower removed + event.event_booth_ids[1].write({'partner_id': False}) + self.assertEqual(event.event_booth_count, 2) + self.assertEqual(event.event_booth_count_available, 1) + self.assertEqual(event.event_booth_ids[1].message_partner_ids, self.env['res.partner']) + + # add group or the test will fail + self.user_eventmanager.write({'groups_id': [ + (4, self.env.ref('sales_team.group_sale_salesman').id), + ]}) + + # change event type to one using booths: include event type booths and keep reserved booths + event_form = Form(event) + event_form.event_type_id = event_type_wbooths + self.assertEqual(event_form.event_booth_count, 3) + self.assertEqual( + set(r['name'] for r in event_form.event_booth_ids._records), + set(('Custom Standard Booth 2', 'Standard Booth', 'Premium Booth')), + 'Should keep booths with reservation, remove unused ones and add type ones' + ) + event_form.save() + self.assertEqual(event.event_booth_count_available, 2) + self.assertEqual(event.event_booth_category_ids, self.event_booth_category_1 + self.event_booth_category_2) diff --git a/addons/event_booth_sale/views/event_booth_category_views.xml b/addons/event_booth_sale/views/event_booth_category_views.xml new file mode 100644 index 00000000000..5c88fa178d5 --- /dev/null +++ b/addons/event_booth_sale/views/event_booth_category_views.xml @@ -0,0 +1,32 @@ + + + + + event.booth.category.view.form.inherit.sale + event.booth.category + + + + + + + + + + + + + + event.booth.category.view.tree.inherit.sale + event.booth.category + + 3 + + + + + + + + + diff --git a/addons/event_booth_sale/views/event_booth_registration_views.xml b/addons/event_booth_sale/views/event_booth_registration_views.xml new file mode 100644 index 00000000000..84c5d6948fa --- /dev/null +++ b/addons/event_booth_sale/views/event_booth_registration_views.xml @@ -0,0 +1,46 @@ + + + + + event.booth.registration.view.form + event.booth.registration + +
    + +
    +

    +
    + + + + + + + + + + + + +
    +
    +
    +
    + + + event.booth.registration.view.tree + event.booth.registration + + + + + + + + + + + + + +
    diff --git a/addons/event_booth_sale/views/event_booth_views.xml b/addons/event_booth_sale/views/event_booth_views.xml new file mode 100644 index 00000000000..a1f47fcfcf7 --- /dev/null +++ b/addons/event_booth_sale/views/event_booth_views.xml @@ -0,0 +1,63 @@ + + + + + event.booth.view.form.inherit.sale + event.booth + + 10 + +
    + +
    +
    + + +
    + + + + + + + + + + + + + + + + + + +
    + + + event.booth.view.tree.inherit.sale + event.booth + + + + + + + + + + event.booth.view.search.inherit.sale + event.booth + + + + + + + + +
    diff --git a/addons/event_booth_sale/views/event_type_booth_views.xml b/addons/event_booth_sale/views/event_type_booth_views.xml new file mode 100644 index 00000000000..00a20ed3fa6 --- /dev/null +++ b/addons/event_booth_sale/views/event_type_booth_views.xml @@ -0,0 +1,28 @@ + + + + + event.booth.view.form.from.type.inherit.sale + event.type.booth + + + + + + + + + + + event.booth.view.tree.from.type.inherit.sale + event.type.booth + + + + + + + + + + diff --git a/addons/event_booth_sale/views/product_views.xml b/addons/event_booth_sale/views/product_views.xml new file mode 100644 index 00000000000..4eb911c4725 --- /dev/null +++ b/addons/event_booth_sale/views/product_views.xml @@ -0,0 +1,15 @@ + + + + + product.template.view.form.inherit.event.booth.sale + product.template + + + + + + + + + diff --git a/addons/event_booth_sale/views/sale_order_views.xml b/addons/event_booth_sale/views/sale_order_views.xml new file mode 100644 index 00000000000..2137ec9b6c1 --- /dev/null +++ b/addons/event_booth_sale/views/sale_order_views.xml @@ -0,0 +1,27 @@ + + + + + sale.order.view.form.inherit.event.booth.sale + sale.order + + + + + + + + + + + + + {'readonly': [('is_event_booth', '=', True)]} + + + + + diff --git a/addons/event_booth_sale/wizard/__init__.py b/addons/event_booth_sale/wizard/__init__.py new file mode 100644 index 00000000000..c17705e493b --- /dev/null +++ b/addons/event_booth_sale/wizard/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import event_booth_configurator diff --git a/addons/event_booth_sale/wizard/event_booth_configurator.py b/addons/event_booth_sale/wizard/event_booth_configurator.py new file mode 100644 index 00000000000..c53986305ce --- /dev/null +++ b/addons/event_booth_sale/wizard/event_booth_configurator.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models, fields, _ +from odoo.exceptions import ValidationError + + +class EventBoothConfigurator(models.TransientModel): + _name = 'event.booth.configurator' + _description = 'Event Booth Configurator' + + product_id = fields.Many2one('product.product', string='Product', readonly=True) + sale_order_line_id = fields.Many2one('sale.order.line', string='Sale Order Line', readonly=True) + event_id = fields.Many2one('event.event', string='Event', required=True) + event_booth_category_available_ids = fields.Many2many(related='event_id.event_booth_category_available_ids', readonly=True) + event_booth_category_id = fields.Many2one( + 'event.booth.category', string='Booth Category', required=True, + compute='_compute_event_booth_category_id', readonly=False, store=True) + event_booth_ids = fields.Many2many( + 'event.booth', string='Booth', required=True, + compute='_compute_event_booth_ids', readonly=False, store=True) + + @api.depends('event_id') + def _compute_event_booth_category_id(self): + self.event_booth_category_id = False + + @api.depends('event_id', 'event_booth_category_id') + def _compute_event_booth_ids(self): + self.event_booth_ids = False + + @api.constrains('event_booth_ids') + def _check_if_no_booth_ids(self): + if any(not wizard.event_booth_ids for wizard in self): + raise ValidationError(_('You have to select at least one booth.')) diff --git a/addons/event_booth_sale/wizard/event_booth_configurator_views.xml b/addons/event_booth_sale/wizard/event_booth_configurator_views.xml new file mode 100644 index 00000000000..0dee2313286 --- /dev/null +++ b/addons/event_booth_sale/wizard/event_booth_configurator_views.xml @@ -0,0 +1,44 @@ + + + + + event.booth.configurator.view.form + event.booth.configurator + +
    + + + + + + + +
    +
    +
    +
    +
    + + + Configure an event booth + event.booth.configurator + form + new + + +
    From 60092b78e07ee65a9631961568914eeaf1dc1e5f Mon Sep 17 00:00:00 2001 From: Patrick Hoste Date: Wed, 21 Oct 2020 15:19:02 +0000 Subject: [PATCH 06/10] [ADD] website_event_booth: add frontend page for booths reservation PURPOSE This module enables the Booths registration on the frontend by adding a dedicated page on the related event. The user will have to first select a booth category and then select one or more booths to book. He will then be asked to provide his contact informations. The booths will directly be set to unavailable... First come, first served. LINKS Task-2318655 Closes : odoo/odoo#71104 --- addons/website_event_booth/__init__.py | 5 + addons/website_event_booth/__manifest__.py | 32 ++++ .../controllers/__init__.py | 4 + .../controllers/event_booth.py | 167 ++++++++++++++++++ .../website_event_booth/data/event_demo.xml | 15 ++ addons/website_event_booth/models/__init__.py | 6 + .../website_event_booth/models/event_event.py | 55 ++++++ .../website_event_booth/models/event_type.py | 17 ++ .../models/website_event_menu.py | 11 ++ .../security/event_booth_security.xml | 15 ++ .../security/ir.model.access.csv | 2 + .../static/src/img/exhibition-map.gif | Bin 0 -> 65375 bytes .../static/src/js/booth_register.js | 137 ++++++++++++++ .../static/src/scss/website_event_booth.scss | 69 ++++++++ .../event_booth_registration_templates.xml | 91 ++++++++++ .../views/event_booth_templates.xml | 124 +++++++++++++ .../views/event_event_views.xml | 19 ++ .../views/event_type_views.xml | 18 ++ 18 files changed, 787 insertions(+) create mode 100644 addons/website_event_booth/__init__.py create mode 100644 addons/website_event_booth/__manifest__.py create mode 100644 addons/website_event_booth/controllers/__init__.py create mode 100644 addons/website_event_booth/controllers/event_booth.py create mode 100644 addons/website_event_booth/data/event_demo.xml create mode 100644 addons/website_event_booth/models/__init__.py create mode 100644 addons/website_event_booth/models/event_event.py create mode 100644 addons/website_event_booth/models/event_type.py create mode 100644 addons/website_event_booth/models/website_event_menu.py create mode 100644 addons/website_event_booth/security/event_booth_security.xml create mode 100644 addons/website_event_booth/security/ir.model.access.csv create mode 100644 addons/website_event_booth/static/src/img/exhibition-map.gif create mode 100644 addons/website_event_booth/static/src/js/booth_register.js create mode 100644 addons/website_event_booth/static/src/scss/website_event_booth.scss create mode 100644 addons/website_event_booth/views/event_booth_registration_templates.xml create mode 100644 addons/website_event_booth/views/event_booth_templates.xml create mode 100644 addons/website_event_booth/views/event_event_views.xml create mode 100644 addons/website_event_booth/views/event_type_views.xml diff --git a/addons/website_event_booth/__init__.py b/addons/website_event_booth/__init__.py new file mode 100644 index 00000000000..7d34c7c054a --- /dev/null +++ b/addons/website_event_booth/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import controllers +from . import models diff --git a/addons/website_event_booth/__manifest__.py b/addons/website_event_booth/__manifest__.py new file mode 100644 index 00000000000..77d04ff55ca --- /dev/null +++ b/addons/website_event_booth/__manifest__.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Online Event Booths', + 'category': 'Marketing/Events', + 'version': '1.0', + 'summary': 'Events, display your booths on your website', + 'description': """ +Display your booths on your website for the users to register. + """, + 'depends': ['website_event', 'event_booth'], + 'data': [ + 'security/ir.model.access.csv', + 'security/event_booth_security.xml', + 'views/event_type_views.xml', + 'views/event_event_views.xml', + 'views/event_booth_registration_templates.xml', + 'views/event_booth_templates.xml', + ], + 'demo': [ + 'data/event_demo.xml', + ], + 'auto_install': True, + 'assets': { + 'web.assets_frontend': [ + '/website_event_booth/static/src/js/booth_register.js', + '/website_event_booth/static/src/scss/website_event_booth.scss', + ] + }, + 'license': 'LGPL-3', +} diff --git a/addons/website_event_booth/controllers/__init__.py b/addons/website_event_booth/controllers/__init__.py new file mode 100644 index 00000000000..1e9f6c7c1c1 --- /dev/null +++ b/addons/website_event_booth/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import event_booth diff --git a/addons/website_event_booth/controllers/event_booth.py b/addons/website_event_booth/controllers/event_booth.py new file mode 100644 index 00000000000..2aa7fdf38ab --- /dev/null +++ b/addons/website_event_booth/controllers/event_booth.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import json +import werkzeug +from werkzeug.exceptions import Forbidden, NotFound + +from odoo import exceptions, http, _ +from odoo.http import request +from odoo.addons.website_event.controllers.main import WebsiteEventController + + +class WebsiteEventBoothController(WebsiteEventController): + + @http.route('/event//booth', type='http', auth='public', website=True, sitemap=True) + def event_booth_main(self, event): + try: + event.check_access_rights('read') + event.check_access_rule('read') + except exceptions.AccessError: + raise Forbidden() + + event_sudo = event.sudo() + values = { + 'event': event_sudo, + 'event_booths': event_sudo.event_booth_ids, + 'available_booth_category_ids': event_sudo.event_booth_category_available_ids, + 'main_object': event, + } + return request.render('website_event_booth.event_booth_registration', values) + + @http.route('/event//booth/register', + type='http', auth='public', methods=['POST'], website=True, sitemap=False) + def event_booth_register(self, event, booth_category_id): + event_booth_ids = request.httprequest.form.getlist('event_booth_ids') + + return request.redirect(('/event/%s/booth/register_form?' % event.id) + werkzeug.urls.url_encode({ + 'booth_ids': ','.join(event_booth_ids), + 'booth_category_id': int(booth_category_id), + })) + + @http.route('/event//booth/register_form', + type='http', auth='public', methods=['GET'], website=True, sitemap=False) + def event_booth_contact_form(self, event, booth_ids=None, booth_category_id=None): + if not booth_ids or not booth_category_id: + raise NotFound() + + booth_category = request.env['event.booth.category'].sudo().browse(int(booth_category_id)) + event_booths = request.env['event.booth'].sudo().browse([int(booth_id) for booth_id in booth_ids.split(',')]) + default_contact = {} + if not request.env.user._is_public(): + default_contact = { + 'name': request.env.user.partner_id.name, + 'email': request.env.user.partner_id.email, + 'phone': request.env.user.partner_id.phone, + 'mobile': request.env.user.partner_id.mobile, + } + else: + visitor = request.env['website.visitor']._get_visitor_from_request() + if visitor.email: + default_contact = { + 'name': visitor.name, + 'email': visitor.email, + 'mobile': visitor.mobile, + } + return request.render( + 'website_event_booth.event_booth_registration_details', + {'event': event.sudo(), + 'default_contact': default_contact, + 'booth_category': booth_category, + 'event_booths': event_booths, + } + ) + + def _get_requested_booths(self, event, event_booth_ids): + booth_ids = json.loads(event_booth_ids) + booths = request.env['event.booth'].sudo().search([ + ('event_id', '=', event.id), + ('state', '=', 'available'), + ('id', 'in', booth_ids) + ]) + if booth_ids != booths.ids: + raise Forbidden(_('Booth registration failed. Please try again.')) + if len(booths.booth_category_id) != 1: + raise Forbidden(_('Booths should belong to the same category.')) + return booths + + @http.route('/event//booth/confirm', + type='http', auth='public', methods=['POST'], website=True, sitemap=False) + def event_booth_registration_confirm(self, event, booth_category_id, event_booth_ids, **kwargs): + booths = self._get_requested_booths(event, event_booth_ids) + + booth_values = self._prepare_booth_registration_values(event, kwargs) + booths.action_confirm(booth_values) + + return request.redirect(('/event/%s/booth/success?' % event.id) + werkzeug.urls.url_encode({ + 'booths': ','.join([str(id) for id in booths.ids]), + })) + + # This will be removed soon + @http.route('/event//booth/success', + type='http', auth='public', methods=['GET'], website=True, sitemap=False) + def event_booth_registration_complete(self, event, booths): + booth_ids = request.env['event.booth'].sudo().search([ + ('event_id', '=', event.id), + ('state', '=', 'unavailable'), + ('id', 'in', [int(id) for id in booths.split(',')]), + ]) + if len(booth_ids.mapped('partner_id')) > 1: + raise NotFound() + event_sudo = event.sudo() + return request.render( + 'website_event_booth.event_booth_registration_complete', + {'event': event, + 'event_booths': event_sudo.event_booth_ids, + 'main_object': event, + 'contact_name': booth_ids[0].contact_name or booth_ids.partner_id.name, + 'contact_email': booth_ids[0].contact_email or booth_ids.partner_id.email, + 'contact_mobile': booth_ids[0].contact_mobile or booth_ids.partner_id.mobile, + 'contact_phone': booth_ids[0].contact_phone or booth_ids.partner_id.phone, + } + ) + + def _prepare_booth_registration_values(self, event, kwargs): + return self._prepare_booth_registration_partner_values(event, kwargs) + + def _prepare_booth_registration_partner_values(self, event, kwargs): + if request.env.user._is_public(): + contact_email = kwargs['contact_email'] + partner = request.env['res.partner'].sudo().find_or_create(contact_email) + if not partner.name and kwargs.get('contact_name'): + partner.name = kwargs['contact_name'] + if not partner.phone and kwargs.get('contact_phone'): + partner.phone = kwargs['contact_phone'] + if not partner.mobile and kwargs.get('contact_mobile'): + partner.mobile = kwargs['contact_mobile'] + else: + partner = request.env.user.partner_id + return { + 'partner_id': partner.id, + 'contact_name': kwargs.get('contact_name') or partner.name, + 'contact_email': kwargs.get('contact_email') or partner.email, + 'contact_mobile': kwargs.get('contact_mobile') or partner.mobile, + 'contact_phone': kwargs.get('contact_phone') or partner.phone, + } + + @http.route('/event/booth/check_availability', type='json', auth='public', methods=['POST']) + def check_booths_availability(self, event_booth_ids=None): + if not event_booth_ids: + return {} + booths = request.env['event.booth'].sudo().browse(event_booth_ids) + return { + 'unavailable_booths': booths.filtered(lambda booth: not booth.is_available).ids + } + + @http.route(['/event/booth_category/get_available_booths'], type='json', auth='public') + def get_booth_category_available_booths(self, event_id, booth_category_id): + booth_ids = request.env['event.booth'].sudo().search([ + ('event_id', '=', int(event_id)), + ('booth_category_id', '=', int(booth_category_id)), + ('state', '=', 'available') + ]) + + return [ + {'id': booth.id, 'name': booth.name} + for booth in booth_ids + ] diff --git a/addons/website_event_booth/data/event_demo.xml b/addons/website_event_booth/data/event_demo.xml new file mode 100644 index 00000000000..d2a2cb3645b --- /dev/null +++ b/addons/website_event_booth/data/event_demo.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/addons/website_event_booth/models/__init__.py b/addons/website_event_booth/models/__init__.py new file mode 100644 index 00000000000..883289b766b --- /dev/null +++ b/addons/website_event_booth/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import event_type +from . import event_event +from . import website_event_menu diff --git a/addons/website_event_booth/models/event_event.py b/addons/website_event_booth/models/event_event.py new file mode 100644 index 00000000000..24d9b34af1b --- /dev/null +++ b/addons/website_event_booth/models/event_event.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ +from odoo.addons.http_routing.models.ir_http import slug + + +class Event(models.Model): + _inherit = 'event.event' + + exhibition_map = fields.Image(string='Exhibition Map', max_width=1024, max_height=1024) + # frontend menu management + booth_menu = fields.Boolean( + string='Booth Register', compute='_compute_booth_menu', + readonly=False, store=True) + booth_menu_ids = fields.One2many( + 'website.event.menu', 'event_id', string='Event Booths Menus', + domain=[('menu_type', '=', 'booth')]) + + @api.depends('event_type_id', 'website_menu') + def _compute_booth_menu(self): + for event in self: + if event.event_type_id and event.event_type_id != event._origin.event_type_id: + event.booth_menu = event.event_type_id.booth_menu + elif event.website_menu and (event.website_menu != event._origin.website_menu or not event.booth_menu): + event.booth_menu = True + elif not event.website_menu: + event.booth_menu = False + + # ------------------------------------------------------------ + # WEBSITE MENU MANAGEMENT + # ------------------------------------------------------------ + + def toggle_booth_menu(self, val): + self.booth_menu = val + + def _get_menu_update_fields(self): + return super(Event, self)._get_menu_update_fields() + ['booth_menu'] + + def _update_website_menus(self, menus_update_by_field=None): + super(Event, self)._update_website_menus(menus_update_by_field=menus_update_by_field) + for event in self: + if event.menu_id and (not menus_update_by_field or event in menus_update_by_field.get('booth_menu')): + event._update_website_menu_entry('booth_menu', 'booth_menu_ids', 'booth') + + def _get_menu_type_field_matching(self): + res = super(Event, self)._get_menu_type_field_matching() + res['booth'] = 'booth_menu' + return res + + def _get_website_menu_entries(self): + self.ensure_one() + return super(Event, self)._get_website_menu_entries() + [ + (_('Get A Booth'), '/event/%s/booth' % slug(self), False, 90, 'booth') + ] diff --git a/addons/website_event_booth/models/event_type.py b/addons/website_event_booth/models/event_type.py new file mode 100644 index 00000000000..6f7808d5d5d --- /dev/null +++ b/addons/website_event_booth/models/event_type.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class EventType(models.Model): + _inherit = 'event.type' + + booth_menu = fields.Boolean( + string='Booths on Website', compute='_compute_booth_menu', + readonly=False, store=True) + + @api.depends('website_menu') + def _compute_booth_menu(self): + for event_type in self: + event_type.booth_menu = event_type.website_menu diff --git a/addons/website_event_booth/models/website_event_menu.py b/addons/website_event_booth/models/website_event_menu.py new file mode 100644 index 00000000000..384cdc78ab2 --- /dev/null +++ b/addons/website_event_booth/models/website_event_menu.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class EventMenu(models.Model): + _inherit = "website.event.menu" + + menu_type = fields.Selection( + selection_add=[('booth', 'Event Booth Menus')], ondelete={'booth': 'cascade'}) diff --git a/addons/website_event_booth/security/event_booth_security.xml b/addons/website_event_booth/security/event_booth_security.xml new file mode 100644 index 00000000000..c3498357f40 --- /dev/null +++ b/addons/website_event_booth/security/event_booth_security.xml @@ -0,0 +1,15 @@ + + + + + Event Booth: public/portal: published read + + [('event_id.website_published', '=', True)] + + + + + + + + diff --git a/addons/website_event_booth/security/ir.model.access.csv b/addons/website_event_booth/security/ir.model.access.csv new file mode 100644 index 00000000000..23e4820a76e --- /dev/null +++ b/addons/website_event_booth/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_event_booth_all,event.booth.public,event_booth.model_event_booth,,1,0,0,0 diff --git a/addons/website_event_booth/static/src/img/exhibition-map.gif b/addons/website_event_booth/static/src/img/exhibition-map.gif new file mode 100644 index 0000000000000000000000000000000000000000..ff618d99af20bd27322ebfb3a2ce21e6b77c5fa4 GIT binary patch literal 65375 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 input[type="checkbox"]': '_onChangeBooth', + 'click .o_wbooth_registration_submit': '_onSubmitClick', + }, + + start() { + this.eventId = parseInt(this.$el.data('event-id')); + this.activeType = false; + this.boothCache = {}; + return this._super.apply(this, arguments).then(() => { + this.$('input[name="booth_category_id"]:enabled:first').click(); + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _check_booths_availability(eventBoothIds) { + const self = this; + return this._rpc({ + route: "/event/booth/check_availability", + params: { + event_booth_ids: eventBoothIds, + }, + }).then(function (result) { + if (result.unavailable_booths.length) { + self.$('input[name="event_booth_ids"]').each(function (i, el) { + if (result.unavailable_booths.includes(parseInt(el.value))) { + $(el).closest('.custom-checkbox').addClass('text-danger'); + } + }); + self.$('.o_wbooth_unavailable_booth_alert').removeClass('d-none'); + return Promise.resolve(false); + } + return Promise.resolve(true); + }) + }, + + _countSelectedBooths() { + return this.$('.custom-checkbox > input[type="checkbox"]:checked').length; + }, + + _fillBooths() { + var $boothElem = this.$('.o_wbooth_booths'); + $boothElem.empty(); + $.each(this.boothCache[this.activeType], function (key, booth) { + let $checkbox = dom.renderCheckbox({ + text: booth.name, + prop: { + name: 'event_booth_ids', + value: booth.id + } + }); + $boothElem.append($checkbox); + }); + }, + + _showBoothCategoryDescription() { + this.$('.o_wbooth_booth_description').addClass('d-none'); + this.$('#o_wbooth_booth_description_' + this.activeType).removeClass('d-none'); + }, + + _updateUiAfterBoothCategoryChange() { + this._fillBooths(); + this._showBoothCategoryDescription(); + this._updateUiAfterBoothChange(this._countSelectedBooths()); + }, + + _updateUiAfterBoothChange(boothCount) { + let $button = this.$('button.o_wbooth_registration_submit'); + $button.attr('disabled', !boothCount); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + _onChangeBooth(ev) { + $(ev.currentTarget).closest('.custom-checkbox').removeClass('text-danger'); + this._updateUiAfterBoothChange(this._countSelectedBooths()); + }, + + /** + * Load all the booths related to the chosen booth category and + * add them to a local dictionary to avoid making rpc each time the + * user change the booth category. + * + * Then the selection input will be filled with the fetched booth values. + * + * @param ev + * @private + */ + _onChangeBoothType(ev) { + ev.preventDefault(); + this.activeType = parseInt(ev.currentTarget.value); + if (this.boothCache[this.activeType] === undefined) { + var self = this; + this._rpc({ + route: '/event/booth_category/get_available_booths', + params: { + event_id: this.eventId, + booth_category_id: this.activeType, + }, + }).then(function (result) { + self.boothCache[self.activeType] = result; + self._updateUiAfterBoothCategoryChange(); + }); + } else { + this._updateUiAfterBoothCategoryChange(); + } + }, + + async _onSubmitClick(ev) { + ev.preventDefault(); + let $form = this.$('.o_wbooth_registration_form'); + let event_booth_ids = this.$('input[name=event_booth_ids]:checked').map(function () { + return parseInt($(this).val()); + }).get(); + if (await this._check_booths_availability(event_booth_ids)) { + $form.submit(); + } + }, + +}); + +return publicWidget.registry.boothRegistration; +}); diff --git a/addons/website_event_booth/static/src/scss/website_event_booth.scss b/addons/website_event_booth/static/src/scss/website_event_booth.scss new file mode 100644 index 00000000000..569310991d5 --- /dev/null +++ b/addons/website_event_booth/static/src/scss/website_event_booth.scss @@ -0,0 +1,69 @@ +label.o_wbooth_category_unavailable { + opacity: 0.5; + overflow: hidden; +} + +#o_wbooth_contact_details_form { + + .col-form-label { + width: 200px; + } + +} + +.o_wbooth_registration_form label { + + position: relative; + + & > input { + visibility: hidden; + position: absolute; + } + + & > input + div { + padding: 10px; + border: 5px solid #e9ecef; + } + + &:not(.o_wbooth_category_unavailable) > input + div { + cursor: pointer; + } + + &:not(.o_wbooth_category_unavailable):hover > input + div { + border-color: #6c757d; + } + + &:hover > input + div:before { + color: #6c757d; + } + + & > input:checked + div, + &:hover > input:checked + div { + border-color: $o-enterprise-primary-color; + } + + & > input:checked + div:before, + &:hover > input:checked + div:before { + color: $o-enterprise-primary-color; + } + + & > input:checked + div:before, + &:not(.o_wbooth_category_unavailable):hover > input + div:before { + content: "\f058"; + font-family: "FontAwesome"; + width: 40px; + height: 40px; + position: absolute; + line-height: 40px; + top: -16px; + right: -16px; + background-color: white; + font-size: 2em; + text-align: center; + } + + & img { + min-height: 250px; + } + +} diff --git a/addons/website_event_booth/views/event_booth_registration_templates.xml b/addons/website_event_booth/views/event_booth_registration_templates.xml new file mode 100644 index 00000000000..3774f9be108 --- /dev/null +++ b/addons/website_event_booth/views/event_booth_registration_templates.xml @@ -0,0 +1,91 @@ + + + + + + + + \ No newline at end of file diff --git a/addons/website_event_booth/views/event_booth_templates.xml b/addons/website_event_booth/views/event_booth_templates.xml new file mode 100644 index 00000000000..313f6c96b78 --- /dev/null +++ b/addons/website_event_booth/views/event_booth_templates.xml @@ -0,0 +1,124 @@ + + + + + + + + diff --git a/addons/website_event_booth/views/event_event_views.xml b/addons/website_event_booth/views/event_event_views.xml new file mode 100644 index 00000000000..454ecea017a --- /dev/null +++ b/addons/website_event_booth/views/event_event_views.xml @@ -0,0 +1,19 @@ + + + + + event.event.view.form.inherit.website.event.booth + event.event + + + + + + + + + + diff --git a/addons/website_event_booth/views/event_type_views.xml b/addons/website_event_booth/views/event_type_views.xml new file mode 100644 index 00000000000..14c02d21dcc --- /dev/null +++ b/addons/website_event_booth/views/event_type_views.xml @@ -0,0 +1,18 @@ + + + + + event.type.view.form.inherit.website.event.booth + event.type + + + + + + + + + + From 37c0c37bb1c7f120e92ee4ba42b9ce19e1956528 Mon Sep 17 00:00:00 2001 From: Patrick Hoste Date: Wed, 27 Jan 2021 10:03:31 +0000 Subject: [PATCH 07/10] [ADD] website_event_booth_sale: add e-commerce capabilities to frontend booths reservation PURPOSE This module integrates the e-commerce module which allows the event manager to sell booths online. The flow is the same as before but after the booths selection they will be added to the user cart and he will be redirected to the e-commerce flow. LINKS Task-2318655 Closes : odoo/odoo#71104 --- addons/website_event_booth_sale/__init__.py | 5 ++ .../website_event_booth_sale/__manifest__.py | 23 ++++++++ .../controllers/__init__.py | 4 ++ .../controllers/event_booth.py | 39 +++++++++++++ .../models/__init__.py | 5 ++ .../models/sale_order.py | 58 +++++++++++++++++++ .../models/website.py | 11 ++++ .../static/src/js/booth_register.js | 43 ++++++++++++++ .../views/event_booth_templates.xml | 27 +++++++++ 9 files changed, 215 insertions(+) create mode 100644 addons/website_event_booth_sale/__init__.py create mode 100644 addons/website_event_booth_sale/__manifest__.py create mode 100644 addons/website_event_booth_sale/controllers/__init__.py create mode 100644 addons/website_event_booth_sale/controllers/event_booth.py create mode 100644 addons/website_event_booth_sale/models/__init__.py create mode 100644 addons/website_event_booth_sale/models/sale_order.py create mode 100644 addons/website_event_booth_sale/models/website.py create mode 100644 addons/website_event_booth_sale/static/src/js/booth_register.js create mode 100644 addons/website_event_booth_sale/views/event_booth_templates.xml diff --git a/addons/website_event_booth_sale/__init__.py b/addons/website_event_booth_sale/__init__.py new file mode 100644 index 00000000000..7d34c7c054a --- /dev/null +++ b/addons/website_event_booth_sale/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import controllers +from . import models diff --git a/addons/website_event_booth_sale/__manifest__.py b/addons/website_event_booth_sale/__manifest__.py new file mode 100644 index 00000000000..1e75fe9edcd --- /dev/null +++ b/addons/website_event_booth_sale/__manifest__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +{ + 'name': 'Online Event Booth Sale', + 'category': 'Marketing/Events', + 'version': '1.0', + 'summary': 'Events, sell your booths online', + 'description': """ +Use the e-commerce to sell your event booths. + """, + 'depends': ['event_booth_sale', 'website_event_booth', 'website_sale'], + 'data': [ + 'views/event_booth_templates.xml', + ], + 'demo': [], + 'auto_install': True, + 'assets': { + 'web.assets_frontend': [ + '/website_event_booth_sale/static/src/js/booth_register.js', + ] + }, + 'license': 'LGPL-3', +} diff --git a/addons/website_event_booth_sale/controllers/__init__.py b/addons/website_event_booth_sale/controllers/__init__.py new file mode 100644 index 00000000000..1e9f6c7c1c1 --- /dev/null +++ b/addons/website_event_booth_sale/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import event_booth diff --git a/addons/website_event_booth_sale/controllers/event_booth.py b/addons/website_event_booth_sale/controllers/event_booth.py new file mode 100644 index 00000000000..70e76f35e6e --- /dev/null +++ b/addons/website_event_booth_sale/controllers/event_booth.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import werkzeug + +from odoo import http +from odoo.http import request +from odoo.addons.website_event.controllers.main import WebsiteEventController + + +class WebsiteEventBoothController(WebsiteEventController): + + @http.route() + def event_booth_main(self, event): + pricelist = request.website.get_current_pricelist() + if pricelist: + event = event.with_context(pricelist=pricelist.id) + return super(WebsiteEventBoothController, self).event_booth_main(event) + + @http.route() + def event_booth_registration_confirm(self, event, booth_category_id, event_booth_ids, **kwargs): + booths = self._get_requested_booths(event, event_booth_ids) + booth_category = request.env['event.booth.category'].sudo().browse(int(booth_category_id)) + order = request.website.sale_get_order(force_create=1) + order._cart_update( + product_id=booth_category.product_id.id, + set_qty=1, + event_booth_pending_ids=booths.ids, + registration_values=self._prepare_booth_registration_values(event, kwargs), + ) + if order.amount_total: + return request.redirect('/shop/checkout') + elif order: + order.action_confirm() + request.website.sale_reset() + + return request.redirect(('/event/%s/booth/success?' % event.id) + werkzeug.urls.url_encode({ + 'booths': ','.join([str(id) for id in booths.ids]), + })) diff --git a/addons/website_event_booth_sale/models/__init__.py b/addons/website_event_booth_sale/models/__init__.py new file mode 100644 index 00000000000..220f9c129d7 --- /dev/null +++ b/addons/website_event_booth_sale/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import sale_order +from . import website diff --git a/addons/website_event_booth_sale/models/sale_order.py b/addons/website_event_booth_sale/models/sale_order.py new file mode 100644 index 00000000000..7fb7a8c429b --- /dev/null +++ b/addons/website_event_booth_sale/models/sale_order.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import Command +from odoo import models, _ + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + def _cart_find_product_line(self, product_id=None, line_id=None, **kwargs): + """Check if there is another sale order line which already contains the requested event_booth_pending_ids + to overwrite it with the newly requested booths to avoid having multiple so_line related to the same booths""" + self.ensure_one() + lines = super(SaleOrder, self)._cart_find_product_line(product_id, line_id) + if line_id: + return lines + event_booth_pending_ids = kwargs.get('event_booth_pending_ids') + if event_booth_pending_ids: + lines = lines.filtered( + lambda line: any(booth.id in event_booth_pending_ids for booth in line.event_booth_pending_ids) + ) + return lines + + def _website_product_id_change(self, order_id, product_id, qty=0, **kwargs): + values = super(SaleOrder, self)._website_product_id_change(order_id, product_id, qty=qty, **kwargs) + event_booth_pending_ids = kwargs.get('event_booth_pending_ids') + if event_booth_pending_ids: + order_line = self.env['sale.order.line'].sudo().search([ + ('id', 'in', self.order_line.ids), + ('event_booth_pending_ids', 'in', event_booth_pending_ids)]) + booths = self.env['event.booth'].browse(event_booth_pending_ids) + new_registration_ids = [Command.create({ + 'event_booth_id': booth.id, + **kwargs.get('registration_values'), + }) for booth in booths] + if order_line: + event_booth_registration_ids = [Command.delete(reg.id) + for reg in order_line.event_booth_registration_ids] + new_registration_ids + else: + event_booth_registration_ids = new_registration_ids + + values.update( + event_id=booths.event_id.id, + event_booth_registration_ids=event_booth_registration_ids, + name=booths._get_booth_multiline_description, + ) + + return values + + def _cart_update(self, product_id=None, line_id=None, add_qty=0, set_qty=0, **kwargs): + values = {} + product = self.env['product.product'].browse(product_id) + if product.is_event_booth and set_qty > 1: + set_qty = 1 + values['warning'] = _('Sorry, you can\'t modify quantity to an Event Booth product.') + values.update(super(SaleOrder, self)._cart_update(product_id, line_id, add_qty, set_qty, **kwargs)) + return values diff --git a/addons/website_event_booth_sale/models/website.py b/addons/website_event_booth_sale/models/website.py new file mode 100644 index 00000000000..b4beddb52a7 --- /dev/null +++ b/addons/website_event_booth_sale/models/website.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class Website(models.Model): + _inherit = 'website' + + def sale_product_domain(self): + return ['&'] + super(Website, self).sale_product_domain() + [('is_event_booth', '=', False)] diff --git a/addons/website_event_booth_sale/static/src/js/booth_register.js b/addons/website_event_booth_sale/static/src/js/booth_register.js new file mode 100644 index 00000000000..2f9a09d9e43 --- /dev/null +++ b/addons/website_event_booth_sale/static/src/js/booth_register.js @@ -0,0 +1,43 @@ +odoo.define('website_event_booth_sale.booth_registration', function (require) { +'use strict'; + +const BoothRegistration = require('website_event_booth.booth_registration'); + +/** + * This class changes the displayed price after selecting the requested booths. + */ +BoothRegistration.include({ + + //-------------------------------------------------------------------------- + // Overrides + //-------------------------------------------------------------------------- + + _onChangeBoothType(ev) { + this.categoryPrice = parseFloat($(ev.currentTarget).data('price-reduce')); + return this._super.apply(this, arguments); + }, + + /** + * Updates the displayed total price after selecting the requested booths + * @param boothCount + * @private + */ + _updateUiAfterBoothChange(boothCount) { + this._super.apply(this, arguments); + let $elem = this.$('.o_wbooth_booth_total_price'); + $elem.toggleClass('d-none', !boothCount || !this.categoryPrice); + this._updatePrice(boothCount); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _updatePrice(boothsCount) { + let $elem = this.$('.o_wbooth_booth_total_price .oe_currency_value'); + $elem.text(boothsCount * this.categoryPrice); + }, + +}); + +}); diff --git a/addons/website_event_booth_sale/views/event_booth_templates.xml b/addons/website_event_booth_sale/views/event_booth_templates.xml new file mode 100644 index 00000000000..addc378e73f --- /dev/null +++ b/addons/website_event_booth_sale/views/event_booth_templates.xml @@ -0,0 +1,27 @@ + + + + + + From a4244e0dfd07d0345a1406cc9b7dc9bbf4ed5fda Mon Sep 17 00:00:00 2001 From: Patrick Hoste Date: Thu, 29 Apr 2021 13:32:36 +0000 Subject: [PATCH 08/10] [ADD] website_event_booth_exhibitor: allow sponsor creation with booths reservation PURPOSE This module integrates the Exhibitor module which allows the event manager to automatically create a sponsor after a booth registration. To enable that feature an option has to be set on the booth category. The event manager can also choose what kind of sponsor will be created at the registration. The frontend flow changes a bit to allow the user to provide his sponsor informations. LINKS Task-2318655 Closes : odoo/odoo#71104 --- .../website_event_booth_exhibitor/__init__.py | 5 ++ .../__manifest__.py | 32 ++++++++ .../controllers/__init__.py | 4 + .../controllers/event_booth.py | 41 ++++++++++ .../data/event_booth_category_data.xml | 16 ++++ .../models/__init__.py | 5 ++ .../models/event_booth.py | 54 +++++++++++++ .../models/event_booth_category.py | 24 ++++++ .../static/src/js/booth_sponsor_details.js | 29 +++++++ .../tours/website_event_booth_exhibitor.js | 62 +++++++++++++++ .../tests/__init__.py | 4 + .../tests/test_wevent_booth_exhibitor.py | 17 +++++ .../views/event_booth_category_views.xml | 51 +++++++++++++ .../event_booth_registration_templates.xml | 75 +++++++++++++++++++ .../views/event_booth_templates.xml | 10 +++ .../views/event_booth_views.xml | 30 ++++++++ .../views/mail_templates.xml | 13 ++++ 17 files changed, 472 insertions(+) create mode 100644 addons/website_event_booth_exhibitor/__init__.py create mode 100644 addons/website_event_booth_exhibitor/__manifest__.py create mode 100644 addons/website_event_booth_exhibitor/controllers/__init__.py create mode 100644 addons/website_event_booth_exhibitor/controllers/event_booth.py create mode 100644 addons/website_event_booth_exhibitor/data/event_booth_category_data.xml create mode 100644 addons/website_event_booth_exhibitor/models/__init__.py create mode 100644 addons/website_event_booth_exhibitor/models/event_booth.py create mode 100644 addons/website_event_booth_exhibitor/models/event_booth_category.py create mode 100644 addons/website_event_booth_exhibitor/static/src/js/booth_sponsor_details.js create mode 100644 addons/website_event_booth_exhibitor/static/tests/tours/website_event_booth_exhibitor.js create mode 100644 addons/website_event_booth_exhibitor/tests/__init__.py create mode 100644 addons/website_event_booth_exhibitor/tests/test_wevent_booth_exhibitor.py create mode 100644 addons/website_event_booth_exhibitor/views/event_booth_category_views.xml create mode 100644 addons/website_event_booth_exhibitor/views/event_booth_registration_templates.xml create mode 100644 addons/website_event_booth_exhibitor/views/event_booth_templates.xml create mode 100644 addons/website_event_booth_exhibitor/views/event_booth_views.xml create mode 100644 addons/website_event_booth_exhibitor/views/mail_templates.xml diff --git a/addons/website_event_booth_exhibitor/__init__.py b/addons/website_event_booth_exhibitor/__init__.py new file mode 100644 index 00000000000..7d34c7c054a --- /dev/null +++ b/addons/website_event_booth_exhibitor/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import controllers +from . import models diff --git a/addons/website_event_booth_exhibitor/__manifest__.py b/addons/website_event_booth_exhibitor/__manifest__.py new file mode 100644 index 00000000000..52ce1a08f43 --- /dev/null +++ b/addons/website_event_booth_exhibitor/__manifest__.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +{ + 'name': 'Booths/Exhibitors Bridge', + 'category': 'Marketing/Events', + 'version': '1.0', + 'summary': 'Event Booths, automatically create a sponsor.', + 'description': """ +Automatically create a sponsor when renting a booth. + """, + 'depends': ['website_event_exhibitor', 'website_event_booth'], + 'data': [ + 'data/event_booth_category_data.xml', + + 'views/event_booth_category_views.xml', + 'views/event_booth_views.xml', + + 'views/event_booth_registration_templates.xml', + 'views/event_booth_templates.xml', + 'views/mail_templates.xml' + ], + 'auto_install': True, + 'assets': { + 'web.assets_frontend': [ + '/website_event_booth_exhibitor/static/src/js/booth_sponsor_details.js', + ], + 'web.assets_tests': [ + 'website_event_booth_exhibitor/static/tests/tours/website_event_booth_exhibitor.js', + ], + }, + 'license': 'LGPL-3', +} diff --git a/addons/website_event_booth_exhibitor/controllers/__init__.py b/addons/website_event_booth_exhibitor/controllers/__init__.py new file mode 100644 index 00000000000..1e9f6c7c1c1 --- /dev/null +++ b/addons/website_event_booth_exhibitor/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import event_booth diff --git a/addons/website_event_booth_exhibitor/controllers/event_booth.py b/addons/website_event_booth_exhibitor/controllers/event_booth.py new file mode 100644 index 00000000000..e43b53aa8b2 --- /dev/null +++ b/addons/website_event_booth_exhibitor/controllers/event_booth.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 + +from odoo.addons.website_event.controllers.main import WebsiteEventController +from odoo.tools import plaintext2html + + +class WebsiteEventBoothController(WebsiteEventController): + + def _prepare_booth_registration_values(self, event, kwargs): + booth_values = super(WebsiteEventBoothController, self)._prepare_booth_registration_values(event, kwargs) + if not booth_values.get('contact_email'): + booth_values['contact_email'] = kwargs.get('sponsor_email') + if not booth_values.get('contact_name'): + booth_values['contact_name'] = kwargs.get('sponsor_name') + if not booth_values.get('contact_mobile'): + booth_values['contact_mobile'] = kwargs.get('sponsor_mobile') + if not booth_values.get('contact_phone'): + booth_values['contact_phone'] = kwargs.get('sponsor_phone') + + booth_values.update(**self._prepare_booth_registration_sponsor_values(event, booth_values, kwargs)) + return booth_values + + def _prepare_booth_registration_partner_values(self, event, kwargs): + if not kwargs.get('contact_email') and kwargs.get('sponsor_email'): + kwargs['contact_email'] = kwargs['sponsor_email'] + return super(WebsiteEventBoothController, self)._prepare_booth_registration_partner_values(event, kwargs) + + def _prepare_booth_registration_sponsor_values(self, event, booth_values, kwargs): + sponsor_values = { + 'sponsor_name': kwargs.get('sponsor_name') or booth_values.get('contact_name'), + 'sponsor_email': kwargs.get('sponsor_email') or booth_values.get('contact_email'), + 'sponsor_mobile': kwargs.get('sponsor_mobile') or booth_values.get('contact_mobile'), + 'sponsor_phone': kwargs.get('sponsor_phone') or booth_values.get('contact_phone'), + 'sponsor_subtitle': kwargs.get('sponsor_slogan'), + 'sponsor_website_description': plaintext2html(kwargs.get('sponsor_description')) if kwargs.get('sponsor_description') else '', + 'sponsor_image_512': base64.b64encode(kwargs['sponsor_image'].read()) if kwargs.get('sponsor_image') else False, + } + return sponsor_values diff --git a/addons/website_event_booth_exhibitor/data/event_booth_category_data.xml b/addons/website_event_booth_exhibitor/data/event_booth_category_data.xml new file mode 100644 index 00000000000..01deefe94d7 --- /dev/null +++ b/addons/website_event_booth_exhibitor/data/event_booth_category_data.xml @@ -0,0 +1,16 @@ + + + + + + + exhibitor + + + + + + online + + + diff --git a/addons/website_event_booth_exhibitor/models/__init__.py b/addons/website_event_booth_exhibitor/models/__init__.py new file mode 100644 index 00000000000..81b1903ec93 --- /dev/null +++ b/addons/website_event_booth_exhibitor/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import event_booth +from . import event_booth_category diff --git a/addons/website_event_booth_exhibitor/models/event_booth.py b/addons/website_event_booth_exhibitor/models/event_booth.py new file mode 100644 index 00000000000..8e9d7f99381 --- /dev/null +++ b/addons/website_event_booth_exhibitor/models/event_booth.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class EventBooth(models.Model): + _inherit = 'event.booth' + + use_sponsor = fields.Boolean(related='booth_category_id.use_sponsor') + sponsor_type_id = fields.Many2one(related='booth_category_id.sponsor_type_id') + sponsor_id = fields.Many2one('event.sponsor', string='Sponsor', copy=False) + sponsor_name = fields.Char(string='Sponsor Name', related='sponsor_id.name') + sponsor_email = fields.Char(string='Sponsor Email', related='sponsor_id.email') + sponsor_mobile = fields.Char(string='Sponsor Mobile', related='sponsor_id.mobile') + sponsor_phone = fields.Char(string='Sponsor Phone', related='sponsor_id.phone') + sponsor_subtitle = fields.Char(string='Sponsor Slogan', related='sponsor_id.subtitle') + sponsor_website_description = fields.Html(string='Sponsor Description', related='sponsor_id.website_description') + sponsor_image_512 = fields.Image(string='Sponsor Logo', related='sponsor_id.image_512') + + def action_view_sponsor(self): + action = self.env['ir.actions.act_window']._for_xml_id('website_event_exhibitor.event_sponsor_action') + action['views'] = [(False, 'form')] + action['res_id'] = self.sponsor_id.id + return action + + def _get_or_create_sponsor(self, vals): + self.ensure_one() + sponsor_id = self.env['event.sponsor'].sudo().search([ + ('partner_id', '=', self.partner_id.id), + ('sponsor_type_id', '=', self.sponsor_type_id.id), + ('exhibitor_type', '=', self.booth_category_id.exhibitor_type), + ('event_id', '=', self.event_id.id), + ], limit=1) + if not sponsor_id: + values = { + 'event_id': self.event_id.id, + 'sponsor_type_id': self.sponsor_type_id.id, + 'exhibitor_type': self.booth_category_id.exhibitor_type, + 'partner_id': self.partner_id.id, + **{key.partition('sponsor_')[2]: value for key, value in vals.items() if key.startswith('sponsor_')}, + } + if self.booth_category_id.exhibitor_type == 'online': + values.update({ + 'room_name': 'odoo-exhibitor-%s' % self.partner_id.name, + }) + sponsor_id = self.env['event.sponsor'].sudo().create(values) + return sponsor_id.id + + def _action_post_confirm(self, write_vals): + for booth in self: + if booth.use_sponsor and booth.partner_id: + booth.sponsor_id = booth._get_or_create_sponsor(write_vals) + super(EventBooth, self)._action_post_confirm(write_vals) diff --git a/addons/website_event_booth_exhibitor/models/event_booth_category.py b/addons/website_event_booth_exhibitor/models/event_booth_category.py new file mode 100644 index 00000000000..e7e818f4d27 --- /dev/null +++ b/addons/website_event_booth_exhibitor/models/event_booth_category.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class EventBoothCategory(models.Model): + _inherit = 'event.booth.category' + + @api.model + def _get_exhibitor_type(self): + return self.env['event.sponsor']._fields['exhibitor_type'].selection + + use_sponsor = fields.Boolean(string='Create Sponsor', help="If set, when booking a booth a sponsor will be created for the user") + sponsor_type_id = fields.Many2one('event.sponsor.type', string='Sponsor Level') + exhibitor_type = fields.Selection(_get_exhibitor_type, string='Sponsor Type') + + @api.onchange('use_sponsor') + def _onchange_use_sponsor(self): + if self.use_sponsor: + if not self.sponsor_type_id: + self.sponsor_type_id = self.env['event.sponsor.type'].search([], order="sequence desc", limit=1).id + if not self.exhibitor_type: + self.exhibitor_type = self._get_exhibitor_type()[0][0] diff --git a/addons/website_event_booth_exhibitor/static/src/js/booth_sponsor_details.js b/addons/website_event_booth_exhibitor/static/src/js/booth_sponsor_details.js new file mode 100644 index 00000000000..df5d359a68f --- /dev/null +++ b/addons/website_event_booth_exhibitor/static/src/js/booth_sponsor_details.js @@ -0,0 +1,29 @@ +odoo.define('website_event_booth_exhibitor.booth_sponsor_details', function (require) { +'use strict'; + +var publicWidget = require('web.public.widget'); + +publicWidget.registry.boothSponsorDetails = publicWidget.Widget.extend({ + selector: '#o_wbooth_contact_details_form', + events: { + 'click input[id="contact_details"]': '_onClickContactDetails', + }, + + //-------------------------------------------------------------------------- + // Handler + //-------------------------------------------------------------------------- + + _onClickContactDetails(ev) { + this.useContactDetails = ev.currentTarget.checked; + this.$('#o_wbooth_contact_details').toggleClass('d-none', !this.useContactDetails); + this.$('label[for="sponsor_name"] > .mandatory_mark, label[for="sponsor_email"] > .mandatory_mark').toggleClass('d-none', this.useContactDetails); + this.$('input[name="contact_name"], input[name="contact_email"]').attr('required', this.useContactDetails); + }, + +}); + + return { + boothSponsorDetails: publicWidget.registry.boothSponsorDetails, + }; + +}); diff --git a/addons/website_event_booth_exhibitor/static/tests/tours/website_event_booth_exhibitor.js b/addons/website_event_booth_exhibitor/static/tests/tours/website_event_booth_exhibitor.js new file mode 100644 index 00000000000..4b46b399976 --- /dev/null +++ b/addons/website_event_booth_exhibitor/static/tests/tours/website_event_booth_exhibitor.js @@ -0,0 +1,62 @@ +odoo.define("website_event_booth_exhibitor.tour_steps", function (require) { + "use strict"; + + var core = require('web.core'); + + var FinalSteps = core.Class.extend({ + + _getSteps: function () { + return [{ + trigger: 'h3:contains("Booth Registration completed!")', + run: function() {}, + }]; + }, + + }); + + return FinalSteps; + +}); +odoo.define("website_event_booth_exhibitor.tour", function (require) { + "use strict"; + + var tour = require("web_tour.tour"); + var FinalSteps = require('website_event_booth_exhibitor.tour_steps'); + + + tour.register("webooth_exhibitor_register", { + test: true, + url: "/event", + }, [{ + content: 'Go on "Online Reveal" page', + trigger: 'a[href*="/event"]:contains("Online Reveal"):first', + }, { + content: 'Browse Booths', + trigger: 'a:contains("Get A Booth")', + }, { + content: 'Choose Premium Booths', + trigger: 'img[title="Premium Booth"]', + }, { + content: 'Choose Booth', + trigger: 'div:contains("OpenWood Demonstrator 2") input', + }, { + content: "Validate attendees details", + trigger: 'button:contains("Book my Booths")', + run: 'click', + }, { + content: "Fill booth details", + trigger: 'form[id="o_wbooth_contact_details_form"]', + run: function () { + $("input[name='sponsor_name']").val("Patrick Sponsor"); + $("input[name='sponsor_email']").val("patrick.sponssor@test.example.com"); + $("input[name='sponsor_phone']").val("+32456001122"); + $("input[name='sponsor_slogan']").val("Patrick is Your Sponsor"); + $("textarea[name='sponsor_description']").text("Really eager to meet you !"); + }, + }, { + content: "Validate booth details", + extra_trigger: "input[name='sponsor_name'], input[name='sponsor_email'], input[name='sponsor_phone']", + trigger: 'button:contains("Book my Booths")', + run: 'click', + }, ...new FinalSteps()._getSteps()].filter(Boolean)); +}); diff --git a/addons/website_event_booth_exhibitor/tests/__init__.py b/addons/website_event_booth_exhibitor/tests/__init__.py new file mode 100644 index 00000000000..7358a417c50 --- /dev/null +++ b/addons/website_event_booth_exhibitor/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_wevent_booth_exhibitor diff --git a/addons/website_event_booth_exhibitor/tests/test_wevent_booth_exhibitor.py b/addons/website_event_booth_exhibitor/tests/test_wevent_booth_exhibitor.py new file mode 100644 index 00000000000..49729e762be --- /dev/null +++ b/addons/website_event_booth_exhibitor/tests/test_wevent_booth_exhibitor.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.base.tests.common import HttpCaseWithUserDemo, HttpCaseWithUserPortal +from odoo.tests import tagged + + +@tagged('post_install', '-at_install') +class TestWEventBoothExhibitorCommon(HttpCaseWithUserDemo, HttpCaseWithUserPortal): + + def test_register(self): + self.browser_js( + '/event', + 'odoo.__DEBUG__.services["web_tour.tour"].run("webooth_exhibitor_register")', + 'odoo.__DEBUG__.services["web_tour.tour"].tours.webooth_exhibitor_register.ready', + login='admin' + ) diff --git a/addons/website_event_booth_exhibitor/views/event_booth_category_views.xml b/addons/website_event_booth_exhibitor/views/event_booth_category_views.xml new file mode 100644 index 00000000000..e33cda8a57f --- /dev/null +++ b/addons/website_event_booth_exhibitor/views/event_booth_category_views.xml @@ -0,0 +1,51 @@ + + + + + event.booth.category.view.form.inherit.website.event.booth.exhibitor + event.booth.category + + + + + + + + + + + + + + + event.booth.category.view.tree.inherit.website.event.booth.exhibitor + event.booth.category + + 5 + + + + + + + + + + + event.booth.category.view.search.inherit.website.event.booth.exhibitor + event.booth.category + + + + + + + + + + + + + + + diff --git a/addons/website_event_booth_exhibitor/views/event_booth_registration_templates.xml b/addons/website_event_booth_exhibitor/views/event_booth_registration_templates.xml new file mode 100644 index 00000000000..a39eafe0b00 --- /dev/null +++ b/addons/website_event_booth_exhibitor/views/event_booth_registration_templates.xml @@ -0,0 +1,75 @@ + + + +