diff --git a/addons/website_event_booth/__init__.py b/addons/website_event_booth/__init__.py new file mode 100644 index 00000000000..7d34c7c054a --- /dev/null +++ b/addons/website_event_booth/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import controllers +from . import models diff --git a/addons/website_event_booth/__manifest__.py b/addons/website_event_booth/__manifest__.py new file mode 100644 index 00000000000..77d04ff55ca --- /dev/null +++ b/addons/website_event_booth/__manifest__.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Online Event Booths', + 'category': 'Marketing/Events', + 'version': '1.0', + 'summary': 'Events, display your booths on your website', + 'description': """ +Display your booths on your website for the users to register. + """, + 'depends': ['website_event', 'event_booth'], + 'data': [ + 'security/ir.model.access.csv', + 'security/event_booth_security.xml', + 'views/event_type_views.xml', + 'views/event_event_views.xml', + 'views/event_booth_registration_templates.xml', + 'views/event_booth_templates.xml', + ], + 'demo': [ + 'data/event_demo.xml', + ], + 'auto_install': True, + 'assets': { + 'web.assets_frontend': [ + '/website_event_booth/static/src/js/booth_register.js', + '/website_event_booth/static/src/scss/website_event_booth.scss', + ] + }, + 'license': 'LGPL-3', +} diff --git a/addons/website_event_booth/controllers/__init__.py b/addons/website_event_booth/controllers/__init__.py new file mode 100644 index 00000000000..1e9f6c7c1c1 --- /dev/null +++ b/addons/website_event_booth/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import event_booth diff --git a/addons/website_event_booth/controllers/event_booth.py b/addons/website_event_booth/controllers/event_booth.py new file mode 100644 index 00000000000..2aa7fdf38ab --- /dev/null +++ b/addons/website_event_booth/controllers/event_booth.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import json +import werkzeug +from werkzeug.exceptions import Forbidden, NotFound + +from odoo import exceptions, http, _ +from odoo.http import request +from odoo.addons.website_event.controllers.main import WebsiteEventController + + +class WebsiteEventBoothController(WebsiteEventController): + + @http.route('/event//booth', type='http', auth='public', website=True, sitemap=True) + def event_booth_main(self, event): + try: + event.check_access_rights('read') + event.check_access_rule('read') + except exceptions.AccessError: + raise Forbidden() + + event_sudo = event.sudo() + values = { + 'event': event_sudo, + 'event_booths': event_sudo.event_booth_ids, + 'available_booth_category_ids': event_sudo.event_booth_category_available_ids, + 'main_object': event, + } + return request.render('website_event_booth.event_booth_registration', values) + + @http.route('/event//booth/register', + type='http', auth='public', methods=['POST'], website=True, sitemap=False) + def event_booth_register(self, event, booth_category_id): + event_booth_ids = request.httprequest.form.getlist('event_booth_ids') + + return request.redirect(('/event/%s/booth/register_form?' % event.id) + werkzeug.urls.url_encode({ + 'booth_ids': ','.join(event_booth_ids), + 'booth_category_id': int(booth_category_id), + })) + + @http.route('/event//booth/register_form', + type='http', auth='public', methods=['GET'], website=True, sitemap=False) + def event_booth_contact_form(self, event, booth_ids=None, booth_category_id=None): + if not booth_ids or not booth_category_id: + raise NotFound() + + booth_category = request.env['event.booth.category'].sudo().browse(int(booth_category_id)) + event_booths = request.env['event.booth'].sudo().browse([int(booth_id) for booth_id in booth_ids.split(',')]) + default_contact = {} + if not request.env.user._is_public(): + default_contact = { + 'name': request.env.user.partner_id.name, + 'email': request.env.user.partner_id.email, + 'phone': request.env.user.partner_id.phone, + 'mobile': request.env.user.partner_id.mobile, + } + else: + visitor = request.env['website.visitor']._get_visitor_from_request() + if visitor.email: + default_contact = { + 'name': visitor.name, + 'email': visitor.email, + 'mobile': visitor.mobile, + } + return request.render( + 'website_event_booth.event_booth_registration_details', + {'event': event.sudo(), + 'default_contact': default_contact, + 'booth_category': booth_category, + 'event_booths': event_booths, + } + ) + + def _get_requested_booths(self, event, event_booth_ids): + booth_ids = json.loads(event_booth_ids) + booths = request.env['event.booth'].sudo().search([ + ('event_id', '=', event.id), + ('state', '=', 'available'), + ('id', 'in', booth_ids) + ]) + if booth_ids != booths.ids: + raise Forbidden(_('Booth registration failed. Please try again.')) + if len(booths.booth_category_id) != 1: + raise Forbidden(_('Booths should belong to the same category.')) + return booths + + @http.route('/event//booth/confirm', + type='http', auth='public', methods=['POST'], website=True, sitemap=False) + def event_booth_registration_confirm(self, event, booth_category_id, event_booth_ids, **kwargs): + booths = self._get_requested_booths(event, event_booth_ids) + + booth_values = self._prepare_booth_registration_values(event, kwargs) + booths.action_confirm(booth_values) + + return request.redirect(('/event/%s/booth/success?' % event.id) + werkzeug.urls.url_encode({ + 'booths': ','.join([str(id) for id in booths.ids]), + })) + + # This will be removed soon + @http.route('/event//booth/success', + type='http', auth='public', methods=['GET'], website=True, sitemap=False) + def event_booth_registration_complete(self, event, booths): + booth_ids = request.env['event.booth'].sudo().search([ + ('event_id', '=', event.id), + ('state', '=', 'unavailable'), + ('id', 'in', [int(id) for id in booths.split(',')]), + ]) + if len(booth_ids.mapped('partner_id')) > 1: + raise NotFound() + event_sudo = event.sudo() + return request.render( + 'website_event_booth.event_booth_registration_complete', + {'event': event, + 'event_booths': event_sudo.event_booth_ids, + 'main_object': event, + 'contact_name': booth_ids[0].contact_name or booth_ids.partner_id.name, + 'contact_email': booth_ids[0].contact_email or booth_ids.partner_id.email, + 'contact_mobile': booth_ids[0].contact_mobile or booth_ids.partner_id.mobile, + 'contact_phone': booth_ids[0].contact_phone or booth_ids.partner_id.phone, + } + ) + + def _prepare_booth_registration_values(self, event, kwargs): + return self._prepare_booth_registration_partner_values(event, kwargs) + + def _prepare_booth_registration_partner_values(self, event, kwargs): + if request.env.user._is_public(): + contact_email = kwargs['contact_email'] + partner = request.env['res.partner'].sudo().find_or_create(contact_email) + if not partner.name and kwargs.get('contact_name'): + partner.name = kwargs['contact_name'] + if not partner.phone and kwargs.get('contact_phone'): + partner.phone = kwargs['contact_phone'] + if not partner.mobile and kwargs.get('contact_mobile'): + partner.mobile = kwargs['contact_mobile'] + else: + partner = request.env.user.partner_id + return { + 'partner_id': partner.id, + 'contact_name': kwargs.get('contact_name') or partner.name, + 'contact_email': kwargs.get('contact_email') or partner.email, + 'contact_mobile': kwargs.get('contact_mobile') or partner.mobile, + 'contact_phone': kwargs.get('contact_phone') or partner.phone, + } + + @http.route('/event/booth/check_availability', type='json', auth='public', methods=['POST']) + def check_booths_availability(self, event_booth_ids=None): + if not event_booth_ids: + return {} + booths = request.env['event.booth'].sudo().browse(event_booth_ids) + return { + 'unavailable_booths': booths.filtered(lambda booth: not booth.is_available).ids + } + + @http.route(['/event/booth_category/get_available_booths'], type='json', auth='public') + def get_booth_category_available_booths(self, event_id, booth_category_id): + booth_ids = request.env['event.booth'].sudo().search([ + ('event_id', '=', int(event_id)), + ('booth_category_id', '=', int(booth_category_id)), + ('state', '=', 'available') + ]) + + return [ + {'id': booth.id, 'name': booth.name} + for booth in booth_ids + ] diff --git a/addons/website_event_booth/data/event_demo.xml b/addons/website_event_booth/data/event_demo.xml new file mode 100644 index 00000000000..d2a2cb3645b --- /dev/null +++ b/addons/website_event_booth/data/event_demo.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/addons/website_event_booth/models/__init__.py b/addons/website_event_booth/models/__init__.py new file mode 100644 index 00000000000..883289b766b --- /dev/null +++ b/addons/website_event_booth/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import event_type +from . import event_event +from . import website_event_menu diff --git a/addons/website_event_booth/models/event_event.py b/addons/website_event_booth/models/event_event.py new file mode 100644 index 00000000000..24d9b34af1b --- /dev/null +++ b/addons/website_event_booth/models/event_event.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ +from odoo.addons.http_routing.models.ir_http import slug + + +class Event(models.Model): + _inherit = 'event.event' + + exhibition_map = fields.Image(string='Exhibition Map', max_width=1024, max_height=1024) + # frontend menu management + booth_menu = fields.Boolean( + string='Booth Register', compute='_compute_booth_menu', + readonly=False, store=True) + booth_menu_ids = fields.One2many( + 'website.event.menu', 'event_id', string='Event Booths Menus', + domain=[('menu_type', '=', 'booth')]) + + @api.depends('event_type_id', 'website_menu') + def _compute_booth_menu(self): + for event in self: + if event.event_type_id and event.event_type_id != event._origin.event_type_id: + event.booth_menu = event.event_type_id.booth_menu + elif event.website_menu and (event.website_menu != event._origin.website_menu or not event.booth_menu): + event.booth_menu = True + elif not event.website_menu: + event.booth_menu = False + + # ------------------------------------------------------------ + # WEBSITE MENU MANAGEMENT + # ------------------------------------------------------------ + + def toggle_booth_menu(self, val): + self.booth_menu = val + + def _get_menu_update_fields(self): + return super(Event, self)._get_menu_update_fields() + ['booth_menu'] + + def _update_website_menus(self, menus_update_by_field=None): + super(Event, self)._update_website_menus(menus_update_by_field=menus_update_by_field) + for event in self: + if event.menu_id and (not menus_update_by_field or event in menus_update_by_field.get('booth_menu')): + event._update_website_menu_entry('booth_menu', 'booth_menu_ids', 'booth') + + def _get_menu_type_field_matching(self): + res = super(Event, self)._get_menu_type_field_matching() + res['booth'] = 'booth_menu' + return res + + def _get_website_menu_entries(self): + self.ensure_one() + return super(Event, self)._get_website_menu_entries() + [ + (_('Get A Booth'), '/event/%s/booth' % slug(self), False, 90, 'booth') + ] diff --git a/addons/website_event_booth/models/event_type.py b/addons/website_event_booth/models/event_type.py new file mode 100644 index 00000000000..6f7808d5d5d --- /dev/null +++ b/addons/website_event_booth/models/event_type.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class EventType(models.Model): + _inherit = 'event.type' + + booth_menu = fields.Boolean( + string='Booths on Website', compute='_compute_booth_menu', + readonly=False, store=True) + + @api.depends('website_menu') + def _compute_booth_menu(self): + for event_type in self: + event_type.booth_menu = event_type.website_menu diff --git a/addons/website_event_booth/models/website_event_menu.py b/addons/website_event_booth/models/website_event_menu.py new file mode 100644 index 00000000000..384cdc78ab2 --- /dev/null +++ b/addons/website_event_booth/models/website_event_menu.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class EventMenu(models.Model): + _inherit = "website.event.menu" + + menu_type = fields.Selection( + selection_add=[('booth', 'Event Booth Menus')], ondelete={'booth': 'cascade'}) diff --git a/addons/website_event_booth/security/event_booth_security.xml b/addons/website_event_booth/security/event_booth_security.xml new file mode 100644 index 00000000000..c3498357f40 --- /dev/null +++ b/addons/website_event_booth/security/event_booth_security.xml @@ -0,0 +1,15 @@ + + + + + Event Booth: public/portal: published read + + [('event_id.website_published', '=', True)] + + + + + + + + diff --git a/addons/website_event_booth/security/ir.model.access.csv b/addons/website_event_booth/security/ir.model.access.csv new file mode 100644 index 00000000000..23e4820a76e --- /dev/null +++ b/addons/website_event_booth/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_event_booth_all,event.booth.public,event_booth.model_event_booth,,1,0,0,0 diff --git a/addons/website_event_booth/static/src/img/exhibition-map.gif b/addons/website_event_booth/static/src/img/exhibition-map.gif new file mode 100644 index 00000000000..ff618d99af2 Binary files /dev/null and b/addons/website_event_booth/static/src/img/exhibition-map.gif differ diff --git a/addons/website_event_booth/static/src/js/booth_register.js b/addons/website_event_booth/static/src/js/booth_register.js new file mode 100644 index 00000000000..043bec67f08 --- /dev/null +++ b/addons/website_event_booth/static/src/js/booth_register.js @@ -0,0 +1,137 @@ +odoo.define('website_event_booth.booth_registration', function (require) { +'use strict'; + +var dom = require('web.dom'); +var publicWidget = require('web.public.widget'); + +publicWidget.registry.boothRegistration = publicWidget.Widget.extend({ + selector: '.o_wbooth_registration', + events: { + 'change input[name="booth_category_id"]': '_onChangeBoothType', + 'change .custom-checkbox > input[type="checkbox"]': '_onChangeBooth', + 'click .o_wbooth_registration_submit': '_onSubmitClick', + }, + + start() { + this.eventId = parseInt(this.$el.data('event-id')); + this.activeType = false; + this.boothCache = {}; + return this._super.apply(this, arguments).then(() => { + this.$('input[name="booth_category_id"]:enabled:first').click(); + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _check_booths_availability(eventBoothIds) { + const self = this; + return this._rpc({ + route: "/event/booth/check_availability", + params: { + event_booth_ids: eventBoothIds, + }, + }).then(function (result) { + if (result.unavailable_booths.length) { + self.$('input[name="event_booth_ids"]').each(function (i, el) { + if (result.unavailable_booths.includes(parseInt(el.value))) { + $(el).closest('.custom-checkbox').addClass('text-danger'); + } + }); + self.$('.o_wbooth_unavailable_booth_alert').removeClass('d-none'); + return Promise.resolve(false); + } + return Promise.resolve(true); + }) + }, + + _countSelectedBooths() { + return this.$('.custom-checkbox > input[type="checkbox"]:checked').length; + }, + + _fillBooths() { + var $boothElem = this.$('.o_wbooth_booths'); + $boothElem.empty(); + $.each(this.boothCache[this.activeType], function (key, booth) { + let $checkbox = dom.renderCheckbox({ + text: booth.name, + prop: { + name: 'event_booth_ids', + value: booth.id + } + }); + $boothElem.append($checkbox); + }); + }, + + _showBoothCategoryDescription() { + this.$('.o_wbooth_booth_description').addClass('d-none'); + this.$('#o_wbooth_booth_description_' + this.activeType).removeClass('d-none'); + }, + + _updateUiAfterBoothCategoryChange() { + this._fillBooths(); + this._showBoothCategoryDescription(); + this._updateUiAfterBoothChange(this._countSelectedBooths()); + }, + + _updateUiAfterBoothChange(boothCount) { + let $button = this.$('button.o_wbooth_registration_submit'); + $button.attr('disabled', !boothCount); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + _onChangeBooth(ev) { + $(ev.currentTarget).closest('.custom-checkbox').removeClass('text-danger'); + this._updateUiAfterBoothChange(this._countSelectedBooths()); + }, + + /** + * Load all the booths related to the chosen booth category and + * add them to a local dictionary to avoid making rpc each time the + * user change the booth category. + * + * Then the selection input will be filled with the fetched booth values. + * + * @param ev + * @private + */ + _onChangeBoothType(ev) { + ev.preventDefault(); + this.activeType = parseInt(ev.currentTarget.value); + if (this.boothCache[this.activeType] === undefined) { + var self = this; + this._rpc({ + route: '/event/booth_category/get_available_booths', + params: { + event_id: this.eventId, + booth_category_id: this.activeType, + }, + }).then(function (result) { + self.boothCache[self.activeType] = result; + self._updateUiAfterBoothCategoryChange(); + }); + } else { + this._updateUiAfterBoothCategoryChange(); + } + }, + + async _onSubmitClick(ev) { + ev.preventDefault(); + let $form = this.$('.o_wbooth_registration_form'); + let event_booth_ids = this.$('input[name=event_booth_ids]:checked').map(function () { + return parseInt($(this).val()); + }).get(); + if (await this._check_booths_availability(event_booth_ids)) { + $form.submit(); + } + }, + +}); + +return publicWidget.registry.boothRegistration; +}); diff --git a/addons/website_event_booth/static/src/scss/website_event_booth.scss b/addons/website_event_booth/static/src/scss/website_event_booth.scss new file mode 100644 index 00000000000..569310991d5 --- /dev/null +++ b/addons/website_event_booth/static/src/scss/website_event_booth.scss @@ -0,0 +1,69 @@ +label.o_wbooth_category_unavailable { + opacity: 0.5; + overflow: hidden; +} + +#o_wbooth_contact_details_form { + + .col-form-label { + width: 200px; + } + +} + +.o_wbooth_registration_form label { + + position: relative; + + & > input { + visibility: hidden; + position: absolute; + } + + & > input + div { + padding: 10px; + border: 5px solid #e9ecef; + } + + &:not(.o_wbooth_category_unavailable) > input + div { + cursor: pointer; + } + + &:not(.o_wbooth_category_unavailable):hover > input + div { + border-color: #6c757d; + } + + &:hover > input + div:before { + color: #6c757d; + } + + & > input:checked + div, + &:hover > input:checked + div { + border-color: $o-enterprise-primary-color; + } + + & > input:checked + div:before, + &:hover > input:checked + div:before { + color: $o-enterprise-primary-color; + } + + & > input:checked + div:before, + &:not(.o_wbooth_category_unavailable):hover > input + div:before { + content: "\f058"; + font-family: "FontAwesome"; + width: 40px; + height: 40px; + position: absolute; + line-height: 40px; + top: -16px; + right: -16px; + background-color: white; + font-size: 2em; + text-align: center; + } + + & img { + min-height: 250px; + } + +} diff --git a/addons/website_event_booth/views/event_booth_registration_templates.xml b/addons/website_event_booth/views/event_booth_registration_templates.xml new file mode 100644 index 00000000000..3774f9be108 --- /dev/null +++ b/addons/website_event_booth/views/event_booth_registration_templates.xml @@ -0,0 +1,91 @@ + + + + + + + + \ No newline at end of file diff --git a/addons/website_event_booth/views/event_booth_templates.xml b/addons/website_event_booth/views/event_booth_templates.xml new file mode 100644 index 00000000000..313f6c96b78 --- /dev/null +++ b/addons/website_event_booth/views/event_booth_templates.xml @@ -0,0 +1,124 @@ + + + + + + + + diff --git a/addons/website_event_booth/views/event_event_views.xml b/addons/website_event_booth/views/event_event_views.xml new file mode 100644 index 00000000000..454ecea017a --- /dev/null +++ b/addons/website_event_booth/views/event_event_views.xml @@ -0,0 +1,19 @@ + + + + + event.event.view.form.inherit.website.event.booth + event.event + + + + + + + + + + diff --git a/addons/website_event_booth/views/event_type_views.xml b/addons/website_event_booth/views/event_type_views.xml new file mode 100644 index 00000000000..14c02d21dcc --- /dev/null +++ b/addons/website_event_booth/views/event_type_views.xml @@ -0,0 +1,18 @@ + + + + + event.type.view.form.inherit.website.event.booth + event.type + + + + + + + + + +