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: 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

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 00000000000..45ba11eba1a Binary files /dev/null and b/addons/event_booth/static/src/img/premium-booth.jpeg differ diff --git a/addons/event_booth/static/src/img/standard-booth.jpeg b/addons/event_booth/static/src/img/standard-booth.jpeg new file mode 100644 index 00000000000..e3b42b3e340 Binary files /dev/null and b/addons/event_booth/static/src/img/standard-booth.jpeg differ 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 00000000000..4e1e7c6ab20 Binary files /dev/null and b/addons/event_booth/static/src/img/vip-booth.jpeg differ diff --git a/addons/event_booth/tests/__init__.py b/addons/event_booth/tests/__init__.py new file mode 100644 index 00000000000..63ce8ea1199 --- /dev/null +++ b/addons/event_booth/tests/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_event_booth_internals +from . import test_event_internals diff --git a/addons/event_booth/tests/common.py b/addons/event_booth/tests/common.py new file mode 100644 index 00000000000..b378449ad6a --- /dev/null +++ b/addons/event_booth/tests/common.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.event.tests.common import TestEventCommon + + +class TestEventBoothCommon(TestEventCommon): + + @classmethod + def setUpClass(cls): + super(TestEventBoothCommon, cls).setUpClass() + + cls.event_booth_category_1 = cls.env['event.booth.category'].create({ + 'name': 'Standard', + 'description': '

Standard

', + }) + 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 + + + + + + + + + + + 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 + + +
    diff --git a/addons/test_event_full/__manifest__.py b/addons/test_event_full/__manifest__.py index 7d3dcf222e7..a474fbd6cdf 100644 --- a/addons/test_event_full/__manifest__.py +++ b/addons/test_event_full/__manifest__.py @@ -12,16 +12,16 @@ automatic lead generation, full Online support, ... """, 'depends': [ 'event', + 'event_booth', 'event_crm', 'event_sale', + 'website_event_booth_sale_exhibitor', 'website_event_crm_questions', 'website_event_exhibitor', 'website_event_questions', - 'website_event_questions', 'website_event_meet', 'website_event_sale', 'website_event_track', - 'website_event_exhibitor', 'website_event_track_live', 'website_event_track_quiz', ], 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 00000000000..ff618d99af2 Binary files /dev/null and b/addons/website_event_booth/static/src/img/exhibition-map.gif differ diff --git a/addons/website_event_booth/static/src/js/booth_register.js b/addons/website_event_booth/static/src/js/booth_register.js new file mode 100644 index 00000000000..043bec67f08 --- /dev/null +++ b/addons/website_event_booth/static/src/js/booth_register.js @@ -0,0 +1,137 @@ +odoo.define('website_event_booth.booth_registration', function (require) { +'use strict'; + +var dom = require('web.dom'); +var publicWidget = require('web.public.widget'); + +publicWidget.registry.boothRegistration = publicWidget.Widget.extend({ + selector: '.o_wbooth_registration', + events: { + 'change input[name="booth_category_id"]': '_onChangeBoothType', + 'change .custom-checkbox > 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 + + + + + + + + + + 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 @@ + + + +