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 @@
+
+
+
+
+
+
+
+ Create Booths and manage their reservations
+
+
+
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
+
+
+
+
+
+ -
+ 1 Branded Booth
+
+ -
+ 4m²
+
+ -
+ 46" display screen
+
+ -
+ 1 desk
+
+ -
+ Logo & link on website
+
+
+
+
+
+
+
+ Premium Booth
+ 2
+
+
+
+
+
+ -
+ 1 Branded Booth
+
+ -
+ 4m²
+
+ -
+ 46" display screen
+
+ -
+ 1 desk
+
+ -
+ Logo & link on website
+
+ -
+ 50 words description on website
+
+ -
+ 10 + 1 passes
+
+
+
+
+
+
+
+ VIP Booth
+ 3
+
+
+
+
+
+ -
+ 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 @@
+
+
+
+
+
+
+
+
+ Booth
+
+
+
+
+
+
+
+
+
+ -
+ Name:
+
+
+ -
+ Email:
+
+
+ -
+ Mobile:
+
+
+ -
+ Phone:
+
+
+
+
+
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:
' % {
+ '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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Booth Registration completed!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Get A Booth
+
+ This event is finished. It's no longer possible to book a booth.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+ 'd-none' if booth_category.use_sponsor else ''
+
+
+ False if booth_category.use_sponsor else True
+
+
+ False if booth_category.use_sponsor else True
+
+
+
+
diff --git a/addons/website_event_booth_exhibitor/views/event_booth_templates.xml b/addons/website_event_booth_exhibitor/views/event_booth_templates.xml
new file mode 100644
index 00000000000..000703198fb
--- /dev/null
+++ b/addons/website_event_booth_exhibitor/views/event_booth_templates.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+ 'true' if booth_category.use_sponsor else 'false'
+
+
+
+
diff --git a/addons/website_event_booth_exhibitor/views/event_booth_views.xml b/addons/website_event_booth_exhibitor/views/event_booth_views.xml
new file mode 100644
index 00000000000..61fb201f174
--- /dev/null
+++ b/addons/website_event_booth_exhibitor/views/event_booth_views.xml
@@ -0,0 +1,30 @@
+
+
+
+
+ event.booth.view.form.inherit.website.event.booth.exhibitor
+ event.booth
+
+ 5
+
+
+
+
+
+
+
+
+
+ event.booth.view.search.inherit.website.event.booth.exhibitor
+ event.booth
+
+
+
+
+
+
+
+
+
diff --git a/addons/website_event_booth_exhibitor/views/mail_templates.xml b/addons/website_event_booth_exhibitor/views/mail_templates.xml
new file mode 100644
index 00000000000..22aa160b3a2
--- /dev/null
+++ b/addons/website_event_booth_exhibitor/views/mail_templates.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
diff --git a/addons/website_event_booth_sale/__init__.py b/addons/website_event_booth_sale/__init__.py
new file mode 100644
index 00000000000..7d34c7c054a
--- /dev/null
+++ b/addons/website_event_booth_sale/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import controllers
+from . import models
diff --git a/addons/website_event_booth_sale/__manifest__.py b/addons/website_event_booth_sale/__manifest__.py
new file mode 100644
index 00000000000..1e75fe9edcd
--- /dev/null
+++ b/addons/website_event_booth_sale/__manifest__.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+
+{
+ 'name': 'Online Event Booth Sale',
+ 'category': 'Marketing/Events',
+ 'version': '1.0',
+ 'summary': 'Events, sell your booths online',
+ 'description': """
+Use the e-commerce to sell your event booths.
+ """,
+ 'depends': ['event_booth_sale', 'website_event_booth', 'website_sale'],
+ 'data': [
+ 'views/event_booth_templates.xml',
+ ],
+ 'demo': [],
+ 'auto_install': True,
+ 'assets': {
+ 'web.assets_frontend': [
+ '/website_event_booth_sale/static/src/js/booth_register.js',
+ ]
+ },
+ 'license': 'LGPL-3',
+}
diff --git a/addons/website_event_booth_sale/controllers/__init__.py b/addons/website_event_booth_sale/controllers/__init__.py
new file mode 100644
index 00000000000..1e9f6c7c1c1
--- /dev/null
+++ b/addons/website_event_booth_sale/controllers/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import event_booth
diff --git a/addons/website_event_booth_sale/controllers/event_booth.py b/addons/website_event_booth_sale/controllers/event_booth.py
new file mode 100644
index 00000000000..70e76f35e6e
--- /dev/null
+++ b/addons/website_event_booth_sale/controllers/event_booth.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import werkzeug
+
+from odoo import http
+from odoo.http import request
+from odoo.addons.website_event.controllers.main import WebsiteEventController
+
+
+class WebsiteEventBoothController(WebsiteEventController):
+
+ @http.route()
+ def event_booth_main(self, event):
+ pricelist = request.website.get_current_pricelist()
+ if pricelist:
+ event = event.with_context(pricelist=pricelist.id)
+ return super(WebsiteEventBoothController, self).event_booth_main(event)
+
+ @http.route()
+ def event_booth_registration_confirm(self, event, booth_category_id, event_booth_ids, **kwargs):
+ booths = self._get_requested_booths(event, event_booth_ids)
+ booth_category = request.env['event.booth.category'].sudo().browse(int(booth_category_id))
+ order = request.website.sale_get_order(force_create=1)
+ order._cart_update(
+ product_id=booth_category.product_id.id,
+ set_qty=1,
+ event_booth_pending_ids=booths.ids,
+ registration_values=self._prepare_booth_registration_values(event, kwargs),
+ )
+ if order.amount_total:
+ return request.redirect('/shop/checkout')
+ elif order:
+ order.action_confirm()
+ request.website.sale_reset()
+
+ return request.redirect(('/event/%s/booth/success?' % event.id) + werkzeug.urls.url_encode({
+ 'booths': ','.join([str(id) for id in booths.ids]),
+ }))
diff --git a/addons/website_event_booth_sale/models/__init__.py b/addons/website_event_booth_sale/models/__init__.py
new file mode 100644
index 00000000000..220f9c129d7
--- /dev/null
+++ b/addons/website_event_booth_sale/models/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import sale_order
+from . import website
diff --git a/addons/website_event_booth_sale/models/sale_order.py b/addons/website_event_booth_sale/models/sale_order.py
new file mode 100644
index 00000000000..7fb7a8c429b
--- /dev/null
+++ b/addons/website_event_booth_sale/models/sale_order.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import Command
+from odoo import models, _
+
+
+class SaleOrder(models.Model):
+ _inherit = 'sale.order'
+
+ def _cart_find_product_line(self, product_id=None, line_id=None, **kwargs):
+ """Check if there is another sale order line which already contains the requested event_booth_pending_ids
+ to overwrite it with the newly requested booths to avoid having multiple so_line related to the same booths"""
+ self.ensure_one()
+ lines = super(SaleOrder, self)._cart_find_product_line(product_id, line_id)
+ if line_id:
+ return lines
+ event_booth_pending_ids = kwargs.get('event_booth_pending_ids')
+ if event_booth_pending_ids:
+ lines = lines.filtered(
+ lambda line: any(booth.id in event_booth_pending_ids for booth in line.event_booth_pending_ids)
+ )
+ return lines
+
+ def _website_product_id_change(self, order_id, product_id, qty=0, **kwargs):
+ values = super(SaleOrder, self)._website_product_id_change(order_id, product_id, qty=qty, **kwargs)
+ event_booth_pending_ids = kwargs.get('event_booth_pending_ids')
+ if event_booth_pending_ids:
+ order_line = self.env['sale.order.line'].sudo().search([
+ ('id', 'in', self.order_line.ids),
+ ('event_booth_pending_ids', 'in', event_booth_pending_ids)])
+ booths = self.env['event.booth'].browse(event_booth_pending_ids)
+ new_registration_ids = [Command.create({
+ 'event_booth_id': booth.id,
+ **kwargs.get('registration_values'),
+ }) for booth in booths]
+ if order_line:
+ event_booth_registration_ids = [Command.delete(reg.id)
+ for reg in order_line.event_booth_registration_ids] + new_registration_ids
+ else:
+ event_booth_registration_ids = new_registration_ids
+
+ values.update(
+ event_id=booths.event_id.id,
+ event_booth_registration_ids=event_booth_registration_ids,
+ name=booths._get_booth_multiline_description,
+ )
+
+ return values
+
+ def _cart_update(self, product_id=None, line_id=None, add_qty=0, set_qty=0, **kwargs):
+ values = {}
+ product = self.env['product.product'].browse(product_id)
+ if product.is_event_booth and set_qty > 1:
+ set_qty = 1
+ values['warning'] = _('Sorry, you can\'t modify quantity to an Event Booth product.')
+ values.update(super(SaleOrder, self)._cart_update(product_id, line_id, add_qty, set_qty, **kwargs))
+ return values
diff --git a/addons/website_event_booth_sale/models/website.py b/addons/website_event_booth_sale/models/website.py
new file mode 100644
index 00000000000..b4beddb52a7
--- /dev/null
+++ b/addons/website_event_booth_sale/models/website.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models
+
+
+class Website(models.Model):
+ _inherit = 'website'
+
+ def sale_product_domain(self):
+ return ['&'] + super(Website, self).sale_product_domain() + [('is_event_booth', '=', False)]
diff --git a/addons/website_event_booth_sale/static/src/js/booth_register.js b/addons/website_event_booth_sale/static/src/js/booth_register.js
new file mode 100644
index 00000000000..2f9a09d9e43
--- /dev/null
+++ b/addons/website_event_booth_sale/static/src/js/booth_register.js
@@ -0,0 +1,43 @@
+odoo.define('website_event_booth_sale.booth_registration', function (require) {
+'use strict';
+
+const BoothRegistration = require('website_event_booth.booth_registration');
+
+/**
+ * This class changes the displayed price after selecting the requested booths.
+ */
+BoothRegistration.include({
+
+ //--------------------------------------------------------------------------
+ // Overrides
+ //--------------------------------------------------------------------------
+
+ _onChangeBoothType(ev) {
+ this.categoryPrice = parseFloat($(ev.currentTarget).data('price-reduce'));
+ return this._super.apply(this, arguments);
+ },
+
+ /**
+ * Updates the displayed total price after selecting the requested booths
+ * @param boothCount
+ * @private
+ */
+ _updateUiAfterBoothChange(boothCount) {
+ this._super.apply(this, arguments);
+ let $elem = this.$('.o_wbooth_booth_total_price');
+ $elem.toggleClass('d-none', !boothCount || !this.categoryPrice);
+ this._updatePrice(boothCount);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ _updatePrice(boothsCount) {
+ let $elem = this.$('.o_wbooth_booth_total_price .oe_currency_value');
+ $elem.text(boothsCount * this.categoryPrice);
+ },
+
+});
+
+});
diff --git a/addons/website_event_booth_sale/views/event_booth_templates.xml b/addons/website_event_booth_sale/views/event_booth_templates.xml
new file mode 100644
index 00000000000..addc378e73f
--- /dev/null
+++ b/addons/website_event_booth_sale/views/event_booth_templates.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ Free
+
+
+ booth_category.price_reduce or '0'
+
+
+
+
+
+
+
diff --git a/addons/website_event_booth_sale_exhibitor/__init__.py b/addons/website_event_booth_sale_exhibitor/__init__.py
new file mode 100644
index 00000000000..dc5e6b693d1
--- /dev/null
+++ b/addons/website_event_booth_sale_exhibitor/__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/website_event_booth_sale_exhibitor/__manifest__.py b/addons/website_event_booth_sale_exhibitor/__manifest__.py
new file mode 100644
index 00000000000..df6e3cbd234
--- /dev/null
+++ b/addons/website_event_booth_sale_exhibitor/__manifest__.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+
+{
+ 'name': 'Booths Sale/Exhibitors Bridge',
+ 'category': 'Marketing/Events',
+ 'version': '1.0',
+ 'summary': 'Bridge module between website_event_booth_exhibitor and website_event_booth_sale.',
+ 'description': """
+ """,
+ 'depends': ['website_event_exhibitor', 'website_event_booth_sale'],
+ 'data': [],
+ 'auto_install': True,
+ 'assets': {
+ 'web.assets_tests': [
+ 'website_event_booth_sale_exhibitor/static/tests/tours/website_event_booth_sale_exhibitor.js',
+ ],
+ },
+ 'license': 'LGPL-3',
+}
diff --git a/addons/website_event_booth_sale_exhibitor/models/__init__.py b/addons/website_event_booth_sale_exhibitor/models/__init__.py
new file mode 100644
index 00000000000..d18cfd5aad1
--- /dev/null
+++ b/addons/website_event_booth_sale_exhibitor/models/__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_registration
diff --git a/addons/website_event_booth_sale_exhibitor/models/event_booth_registration.py b/addons/website_event_booth_sale_exhibitor/models/event_booth_registration.py
new file mode 100644
index 00000000000..575be3eb836
--- /dev/null
+++ b/addons/website_event_booth_sale_exhibitor/models/event_booth_registration.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class EventBoothRegistration(models.Model):
+ _inherit = 'event.booth.registration'
+
+ sponsor_name = fields.Char(string='Sponsor Name')
+ sponsor_email = fields.Char(string='Sponsor Email')
+ sponsor_mobile = fields.Char(string='Sponsor Mobile')
+ sponsor_phone = fields.Char(string='Sponsor Phone')
+ sponsor_subtitle = fields.Char(string='Sponsor Slogan')
+ sponsor_website_description = fields.Html(string='Sponsor Description')
+ sponsor_image_512 = fields.Image(string='Sponsor Logo')
+
+ def _get_fields_for_booth_confirmation(self):
+ return super(EventBoothRegistration, self)._get_fields_for_booth_confirmation() + \
+ ['sponsor_name', 'sponsor_email', 'sponsor_mobile', 'sponsor_phone', 'sponsor_subtitle',
+ 'sponsor_website_description', 'sponsor_image_512']
diff --git a/addons/website_event_booth_sale_exhibitor/static/tests/tours/website_event_booth_sale_exhibitor.js b/addons/website_event_booth_sale_exhibitor/static/tests/tours/website_event_booth_sale_exhibitor.js
new file mode 100644
index 00000000000..2ba0ac780df
--- /dev/null
+++ b/addons/website_event_booth_sale_exhibitor/static/tests/tours/website_event_booth_sale_exhibitor.js
@@ -0,0 +1,25 @@
+odoo.define("website_event_booth_sale_exhibitor.tour", function (require) {
+ "use strict";
+
+ var FinalSteps = require('website_event_booth_exhibitor.tour_steps');
+
+ FinalSteps.include({
+
+ _getSteps: function () {
+ return [{
+ content: 'Confirm your order',
+ trigger: '.btn-primary[href="/shop/confirm_order"]',
+ run: 'click',
+ }, {
+ content: 'Pay your order',
+ trigger: '.btn-primary[name="o_payment_submit_button"]',
+ run: 'click',
+ }, {
+ trigger: 'h3:contains("Please use the following transfer details")',
+ run: function () {},
+ }]
+ }
+
+ });
+
+});
\ No newline at end of file
diff --git a/addons/website_event_sale/models/sale_order.py b/addons/website_event_sale/models/sale_order.py
index 296738c6925..7978bfdff26 100644
--- a/addons/website_event_sale/models/sale_order.py
+++ b/addons/website_event_sale/models/sale_order.py
@@ -17,12 +17,12 @@ class SaleOrder(models.Model):
domain.append(('event_ticket_id', '=', self.env.context.get("event_ticket_id")))
return self.env['sale.order.line'].sudo().search(domain)
- def _website_product_id_change(self, order_id, product_id, qty=0):
+ def _website_product_id_change(self, order_id, product_id, qty=0, **kwargs):
order = self.env['sale.order'].sudo().browse(order_id)
if self._context.get('pricelist') != order.pricelist_id.id:
self = self.with_context(pricelist=order.pricelist_id.id)
- values = super(SaleOrder, self)._website_product_id_change(order_id, product_id, qty=qty)
+ values = super(SaleOrder, self)._website_product_id_change(order_id, product_id, qty=qty, **kwargs)
event_ticket_id = None
if self.env.context.get("event_ticket_id"):
event_ticket_id = self.env.context.get("event_ticket_id")
diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py
index f3d42707e73..bb217206519 100644
--- a/addons/website_sale/models/sale_order.py
+++ b/addons/website_sale/models/sale_order.py
@@ -102,7 +102,7 @@ class SaleOrder(models.Model):
return self.env['sale.order.line'].sudo().search(domain)
- def _website_product_id_change(self, order_id, product_id, qty=0):
+ def _website_product_id_change(self, order_id, product_id, qty=0, **kwargs):
order = self.sudo().browse(order_id)
product_context = dict(self.env.context)
product_context.setdefault('lang', order.partner_id.lang)
@@ -201,7 +201,7 @@ class SaleOrder(models.Model):
product_id = product.id
- values = self._website_product_id_change(self.id, product_id, qty=1)
+ values = self._website_product_id_change(self.id, product_id, qty=1, **kwargs)
# add no_variant attributes that were not received
for ptav in combination.filtered(lambda ptav: ptav.attribute_id.create_variant == 'no_variant' and ptav not in received_no_variant_values):
@@ -260,7 +260,7 @@ class SaleOrder(models.Model):
else:
# update line
no_variant_attributes_price_extra = [ptav.price_extra for ptav in order_line.product_no_variant_attribute_value_ids]
- values = self.with_context(no_variant_attributes_price_extra=tuple(no_variant_attributes_price_extra))._website_product_id_change(self.id, product_id, qty=quantity)
+ values = self.with_context(no_variant_attributes_price_extra=tuple(no_variant_attributes_price_extra))._website_product_id_change(self.id, product_id, qty=quantity, **kwargs)
order = self.sudo().browse(self.id)
if self.pricelist_id.discount_policy == 'with_discount' and not self.env.context.get('fixed_price'):
product_context.update({
diff --git a/addons/website_sale_stock/models/sale_order.py b/addons/website_sale_stock/models/sale_order.py
index fda99c3f8b6..0a8e85905ae 100644
--- a/addons/website_sale_stock/models/sale_order.py
+++ b/addons/website_sale_stock/models/sale_order.py
@@ -35,8 +35,8 @@ class SaleOrder(models.Model):
values['warning'] = self.warning_stock
return values
- def _website_product_id_change(self, order_id, product_id, qty=0):
- res = super(SaleOrder, self)._website_product_id_change(order_id, product_id, qty=qty)
+ def _website_product_id_change(self, order_id, product_id, qty=0, **kwargs):
+ res = super(SaleOrder, self)._website_product_id_change(order_id, product_id, qty=qty, **kwargs)
product = self.env['product.product'].browse(product_id)
res['customer_lead'] = product.sale_delay
return res