diff --git a/addons/event/__manifest__.py b/addons/event/__manifest__.py index ecba97514f5..e537a56152c 100644 --- a/addons/event/__manifest__.py +++ b/addons/event/__manifest__.py @@ -17,7 +17,7 @@ Key Features * Manage your Events and Registrations * Use emails to automatically confirm and send acknowledgments for any event registration """, - 'depends': ['base_setup', 'mail', 'phone_validation', 'portal', 'utm'], + 'depends': ['barcodes', 'base_setup', 'mail', 'phone_validation', 'portal', 'utm'], 'data': [ 'security/event_security.xml', 'security/ir.model.access.csv', @@ -30,6 +30,7 @@ Key Features 'views/event_stage_views.xml', 'report/event_event_templates.xml', 'report/event_event_reports.xml', + 'report/event_registration_report.xml', 'data/ir_cron_data.xml', 'data/mail_template_data.xml', 'data/event_data.xml', @@ -48,6 +49,7 @@ Key Features 'installable': True, 'assets': { 'web.assets_backend': [ + 'event/static/src/client_action/**/*', 'event/static/src/scss/event.scss', 'event/static/src/icon_selection_field/icon_selection_field.js', 'event/static/src/icon_selection_field/icon_selection_field.xml', diff --git a/addons/event/controllers/main.py b/addons/event/controllers/main.py index 72211d0d191..94d8ac41ce3 100644 --- a/addons/event/controllers/main.py +++ b/addons/event/controllers/main.py @@ -4,7 +4,7 @@ import json from werkzeug.exceptions import NotFound -from odoo import _ +from odoo import http, _ from odoo.http import Controller, request, route, content_disposition from odoo.tools import consteq @@ -75,3 +75,23 @@ class EventController(Controller): ('Content-Disposition', content_disposition(f'{report_name}.pdf')), ] return request.make_response(pdf, headers=pdfhttpheaders) + + @http.route(['/event/init_barcode_interface'], type='json', auth="user") + def init_barcode_interface(self, event_id): + event = request.env['event.event'].browse(event_id).exists() if event_id else False + if event: + return { + 'name': event.name, + 'country': event.address_id.country_id.name, + 'city': event.address_id.city, + 'company_name': event.company_id.name, + 'company_id': event.company_id.id + } + else: + return { + 'name': _('Registration Desk'), + 'country': False, + 'city': False, + 'company_name': request.env.company.name, + 'company_id': request.env.company.id + } diff --git a/addons/event/data/mail_template_data.xml b/addons/event/data/mail_template_data.xml index 5a8cbb2d781..ed340da5138 100644 --- a/addons/event/data/mail_template_data.xml +++ b/addons/event/data/mail_template_data.xml @@ -261,10 +261,17 @@ - - View Ticket - +
+ + View Ticket + +
+ +
+ QR Code +
+
diff --git a/addons/event/models/event_event.py b/addons/event/models/event_event.py index c6b565f3bd5..40f29276f66 100644 --- a/addons/event/models/event_event.py +++ b/addons/event/models/event_event.py @@ -124,6 +124,7 @@ class EventEvent(models.Model): user_id = fields.Many2one( 'res.users', string='Responsible', tracking=True, default=lambda self: self.env.user) + use_barcode = fields.Boolean(compute='_compute_use_barcode') company_id = fields.Many2one( 'res.company', string='Company', change_default=True, default=lambda self: self.env.company, @@ -236,6 +237,11 @@ class EventEvent(models.Model): compute='_compute_ticket_instructions', store=True, readonly=False, help="This information will be printed on your tickets.") + def _compute_use_barcode(self): + use_barcode = self.env['ir.config_parameter'].sudo().get_param('event.use_event_barcode') == 'True' + for record in self: + record.use_barcode = use_barcode + @api.depends('stage_id', 'kanban_state') def _compute_kanban_state_label(self): for event in self: diff --git a/addons/event/models/event_registration.py b/addons/event/models/event_registration.py index 4b6799407bf..fc3f002497d 100644 --- a/addons/event/models/event_registration.py +++ b/addons/event/models/event_registration.py @@ -1,11 +1,14 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import logging +import os from dateutil.relativedelta import relativedelta from odoo import _, api, fields, models, SUPERUSER_ID from odoo.tools import format_date, email_normalize, email_normalize_all from odoo.exceptions import AccessError, ValidationError +_logger = logging.getLogger(__name__) class EventRegistration(models.Model): @@ -14,12 +17,27 @@ class EventRegistration(models.Model): _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'id desc' + @api.model + def _get_random_barcode(self): + """Generate a string representation of a pseudo-random 8-byte number for barcode + generation. + + A decimal serialisation is longer than a hexadecimal one *but* it + generates a more compact barcode (Code128C rather than Code128A). + + Generate 8 bytes (64 bits) barcodes as 16 bytes barcodes are not + compatible with all scanners. + """ + return str(int.from_bytes(os.urandom(8), 'little')) + # event event_id = fields.Many2one( 'event.event', string='Event', required=True) event_ticket_id = fields.Many2one( 'event.event.ticket', string='Event Ticket', ondelete='restrict') active = fields.Boolean(default=True) + barcode = fields.Char(string='Barcode', default=lambda self: self._get_random_barcode(), readonly=True, copy=False) + # utm informations utm_campaign_id = fields.Many2one('utm.campaign', 'Campaign', index=True, ondelete='set null') utm_source_id = fields.Many2one('utm.source', 'Source', index=True, ondelete='set null') @@ -48,7 +66,9 @@ class EventRegistration(models.Model): ('draft', 'Unconfirmed'), ('cancel', 'Cancelled'), ('open', 'Confirmed'), ('done', 'Attended')], string='Status', default='draft', readonly=True, copy=False, tracking=6) - + _sql_constraints = [ + ('barcode_event_uniq', 'unique(barcode)', "Barcode should be unique") + ] @api.depends('partner_id') def _compute_name(self): for registration in self: @@ -116,6 +136,27 @@ class EventRegistration(models.Model): country = self.partner_id.country_id or self.event_id.country_id or self.env.company.country_id self.phone = self._phone_format(fname='phone', country=country) or self.phone + @api.model + def register_attendee(self, barcode, event_id): + attendee = self.search([('barcode', '=', barcode)], limit=1) + if not attendee: + return {'error': 'invalid_ticket'} + res = attendee._get_registration_summary() + if attendee.state == 'cancel': + status = 'canceled_registration' + elif not attendee.event_id.is_ongoing: + status = 'not_ongoing_event' + elif attendee.state != 'done': + if event_id and attendee.event_id.id != event_id: + status = 'need_manual_confirmation' + else: + attendee.action_set_done() + status = 'confirmed_registration' + else: + status = 'already_registered' + res.update({'status': status, 'event_id': event_id}) + return res + # ------------------------------------------------------------ # CRUD # ------------------------------------------------------------ diff --git a/addons/event/models/res_config_settings.py b/addons/event/models/res_config_settings.py index fdb24bf3bb2..7d7e92b2a34 100644 --- a/addons/event/models/res_config_settings.py +++ b/addons/event/models/res_config_settings.py @@ -24,7 +24,8 @@ class ResConfigSettings(models.TransientModel): module_website_event_track_live = fields.Boolean("Live Mode") module_website_event_track_quiz = fields.Boolean("Quiz on Tracks") module_website_event_exhibitor = fields.Boolean("Advanced Sponsors") - module_event_barcode = fields.Boolean("Barcode") + use_event_barcode = fields.Boolean(string="Use Event Barcode", help="Enable or Disable Event Barcode functionality.", config_parameter='event.use_event_barcode') + barcode_nomenclature_id = fields.Many2one('barcode.nomenclature', related='company_id.nomenclature_id', readonly=False) module_website_event_sale = fields.Boolean("Online Ticketing") module_event_booth = fields.Boolean("Booth Management") use_google_maps_static_api = fields.Boolean("Google Maps static API", default=_default_use_google_maps_static_api) diff --git a/addons/event/report/event_event_reports.xml b/addons/event/report/event_event_reports.xml index 35a92018485..4925680b0cd 100644 --- a/addons/event/report/event_event_reports.xml +++ b/addons/event/report/event_event_reports.xml @@ -92,5 +92,15 @@ event.event_registration_report_template_responsive_html_ticket event.event_registration_report_template_responsive_html_ticket - + + + Attendee List + event.event + qweb-pdf + event.event_event_attendee_list + event.event_event_attendee_list + + report + 'Attendee List - %s' % (object.name) + diff --git a/addons/event/report/event_event_templates.xml b/addons/event/report/event_event_templates.xml index 208e5d0cd63..9ffbd2b064e 100644 --- a/addons/event/report/event_event_templates.xml +++ b/addons/event/report/event_event_templates.xml @@ -15,9 +15,19 @@
-
-
- ID: + + + + + + + + +
+ QR Code +
+
+
@@ -27,9 +37,20 @@
-
-
- ID: + + + + + + + + +
+ QR Code +
+ +
+
@@ -136,7 +157,18 @@
-
+ +
+ +
+ QR Code +
+ + + Barcode + +
+
@@ -230,7 +262,18 @@
- + + QR Code + + + + + + + + + +
diff --git a/addons/event/report/event_registration_report.xml b/addons/event/report/event_registration_report.xml new file mode 100644 index 00000000000..a16ce04c6c8 --- /dev/null +++ b/addons/event/report/event_registration_report.xml @@ -0,0 +1,80 @@ + + + + + Attendee List + event.registration + qweb-pdf + event.event_registration_attendee_list + event.event_registration_attendee_list + + report + 'Attendee List' + + + + + + + + + + diff --git a/addons/event/static/src/client_action/event_barcode.js b/addons/event/static/src/client_action/event_barcode.js new file mode 100644 index 00000000000..9b78b282089 --- /dev/null +++ b/addons/event/static/src/client_action/event_barcode.js @@ -0,0 +1,111 @@ +/** @odoo-module **/ + +import { _t } from "@web/core/l10n/translation"; +import { BarcodeScanner } from "@barcodes/components/barcode_scanner"; +import { Component, onWillStart } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { useBus, useService } from "@web/core/utils/hooks"; +import { EventRegistrationSummaryDialog } from "./event_registration_summary_dialog"; + +export class EventScanView extends Component { + setup() { + this.actionService = useService("action"); + this.dialog = useService("dialog"); + this.notification = useService("notification"); + this.orm = useService("orm"); + this.rpc = useService("rpc"); + + const { default_event_id, active_model, active_id } = this.props.action.context; + this.eventId = default_event_id || (active_model === "event.event" && active_id); + this.isMultiEvent = !this.eventId; + + const barcode = useService("barcode"); + useBus(barcode.bus, "barcode_scanned", (ev) => this.onBarcodeScanned(ev.detail.barcode)); + + onWillStart(this.onWillStart); + } + + /** + * @override + * Fetch barcode init information. Notably eventId triggers mono- or multi- + * event mode (Registration Desk in multi event allow to manage attendees + * from several events and tickets without reloading / changing event in UX. + */ + async onWillStart() { + this.data = await this.rpc("/event/init_barcode_interface", { + event_id: this.eventId, + }); + } + + /** + * When scanning a barcode, call Registration.register_attendee() to get + * formatted registration information, notably its status or event-related + * information. Open a confirmation / choice Dialog to confirm attendee. + */ + async onBarcodeScanned(barcode) { + const result = await this.orm.call("event.registration", "register_attendee", [], { + barcode: barcode, + event_id: this.eventId, + }); + + if (result.error && result.error === "invalid_ticket") { + this.notification.add(_t("Invalid ticket"), { + title: _t("Warning"), + type: "danger", + }); + } else { + this.registrationId = result.id; + this.dialog.add(EventRegistrationSummaryDialog, { registration: result }); + } + } + + onClickSelectAttendee() { + if (this.isMultiEvent) { + this.actionService.doAction("event.event_registration_action"); + } else { + this.actionService.doAction("event.event_registration_action_kanban", { + additionalContext: { + active_id: this.eventId, + search_default_unconfirmed: true, + search_default_confirmed: true, + }, + }); + } + } + + onClickBackToEvents() { + if (this.isMultiEvent) { + // define action from scratch instead of using existing 'action_event_view' to avoid + // messing with menu bar + this.actionService.doAction({ + type: "ir.actions.act_window", + name: _t("Events"), + res_model: "event.event", + views: [ + [false, "kanban"], + [false, "calendar"], + [false, "list"], + [false, "gantt"], + [false, "form"], + [false, "pivot"], + [false, "graph"], + [false, "map"], + ], + target: "main", + }); + } else { + this.actionService.doAction({ + type: "ir.actions.act_window", + res_model: "event.event", + res_id: this.eventId, + views: [[false, "form"]], + target: "main", + }); + } + } +} + +EventScanView.template = "event.EventScanView"; +EventScanView.components = { BarcodeScanner }; + +registry.category("actions").add("event.event_barcode_scan_view", EventScanView); diff --git a/addons/event/static/src/client_action/event_barcode.scss b/addons/event/static/src/client_action/event_barcode.scss new file mode 100644 index 00000000000..db26c1bb5d3 --- /dev/null +++ b/addons/event/static/src/client_action/event_barcode.scss @@ -0,0 +1,43 @@ +.o_event_barcode_bg { + height: 100%; + display: flex; + flex-flow: column nowrap; + justify-content: center; + align-items: center; + + @include media-breakpoint-up(md) { + @include o-position-absolute(0, 0, 0, 0); + } +} +.o_event_barcode_main { + font-family: 'Lato', sans-serif; + width: 100%; + text-align: center; + padding: 2em; + img .o_event_barcode_image { + width: 115px; + height: 60px; + } + .o_event_barcode_company_image { + overflow: hidden; + margin: 1rem 0 2rem; + max-width: 200px; + max-height: 100px; + } + @include media-breakpoint-up(md) { + flex: 0 0 auto; + width: 550px; + border-radius: 10px; + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.6); + font-size: 1.2em; + padding: 3em; + } +} +.o_notification_manager { + .o_event_success { + color: white; + background-color: #5BC236; + } +} diff --git a/addons/event/static/src/client_action/event_barcode.xml b/addons/event/static/src/client_action/event_barcode.xml new file mode 100644 index 00000000000..eff7534a7a7 --- /dev/null +++ b/addons/event/static/src/client_action/event_barcode.xml @@ -0,0 +1,39 @@ + + + + + +
+
+ +
+

+

+ + - + + + +

+

Welcome to

+ Company Logo +
+
+
+ +
Scan a badge
+
+
+

or

+
+
+ +
+
+
+
+
+ +
diff --git a/addons/event/static/src/client_action/event_registration_summary_dialog.js b/addons/event/static/src/client_action/event_registration_summary_dialog.js new file mode 100644 index 00000000000..bfb7ee5fac2 --- /dev/null +++ b/addons/event/static/src/client_action/event_registration_summary_dialog.js @@ -0,0 +1,53 @@ +/** @odoo-module **/ + +import { _t } from "@web/core/l10n/translation"; +import { Component } from "@odoo/owl"; +import { Dialog } from "@web/core/dialog/dialog"; +import { useService } from "@web/core/utils/hooks"; + +export class EventRegistrationSummaryDialog extends Component { + setup() { + this.actionService = useService("action"); + this.notification = useService("notification"); + this.orm = useService("orm"); + } + + get registration() { + return this.props.registration; + } + + get needManualConfirmation() { + return this.registration.status === "need_manual_confirmation"; + } + + async onRegistrationConfirm() { + await this.orm.call("event.registration", "action_set_done", [this.registration.id]); + this.notification.add(_t("Registration confirmed")); + this.props.close(); + } + + onRegistrationPrintPdf() { + this.actionService.doAction({ + type: "ir.actions.report", + report_type: "qweb-pdf", + report_name: `event.event_registration_report_template_badge/${this.registration.id}`, + }); + } + + async onRegistrationView() { + await this.actionService.doAction({ + type: "ir.actions.act_window", + res_model: "event.registration", + res_id: this.registration.id, + views: [[false, "form"]], + target: "current", + }); + this.props.close(); + } +} +EventRegistrationSummaryDialog.template = "event.EventRegistrationSummaryDialog"; +EventRegistrationSummaryDialog.components = { Dialog }; +EventRegistrationSummaryDialog.props = { + close: Function, + registration: Object, +}; diff --git a/addons/event/static/src/client_action/event_registration_summary_dialog.xml b/addons/event/static/src/client_action/event_registration_summary_dialog.xml new file mode 100644 index 00000000000..2a5df67371e --- /dev/null +++ b/addons/event/static/src/client_action/event_registration_summary_dialog.xml @@ -0,0 +1,64 @@ + + + + + + +
+
+
+ Registration +
+ + + +
+
+
+
+ + + + + + + + + + +
Event
Company
Name
Ticket
Payment
Answers + +
+
+
+ + + + + + + +
+
+ +
diff --git a/addons/event/static/src/scss/event_foldable_badge_report.scss b/addons/event/static/src/scss/event_foldable_badge_report.scss new file mode 100644 index 00000000000..9b0709c4eea --- /dev/null +++ b/addons/event/static/src/scss/event_foldable_badge_report.scss @@ -0,0 +1,62 @@ +.o_event_foldable_badge_container { + .o_event_foldable_badge_top { + height: 148mm; + + &.o_event_foldable_badge_ticket { + border-left: 1px dashed black; + } + } + + .o_event_foldable_badge_font_small { + font-size: .8rem; + } + + .o_event_foldable_badge_bottom.o_event_foldable_badge_left { + border-top: 1px dashed black; + height: 148mm; + + p { + margin: 0px; // to match editor style (.o_field_html p) + } + } + + .o_event_foldable_badge_bottom.o_event_foldable_badge_right { + border-left: 1px dashed black; + border-top:1px dashed black; + height: 148mm; + background: white; + } + + .o_event_foldable_badge_ticket { + .o_event_foldable_badge_ticket_wrapper { + background-color: white; + border: solid 1px #939393; + margin: 0px 8px; + padding: 10px 3px; + font-size: 1.2rem; + box-shadow: -3px 3px 9px 0px rgba(0,0,0,0.51); + + .o_event_foldable_badge_ticket_wrapper_top { + min-height: 80mm; + } + + .o_event_foldable_badge_font_faded { + color: #939393; + } + + .o_event_foldable_badge_barcode { + min-height: 35mm; + } + } + } + + .o_event_foldable_badge_step { + position: absolute; + padding: 3px 9px; + top: 5mm; + left: 0mm; + border-radius: 50%; + background-color: black; + color: white; + } +} diff --git a/addons/event/static/src/scss/event_full_page_ticket_report.scss b/addons/event/static/src/scss/event_full_page_ticket_report.scss index fc6e255d792..778d48eb175 100644 --- a/addons/event/static/src/scss/event_full_page_ticket_report.scss +++ b/addons/event/static/src/scss/event_full_page_ticket_report.scss @@ -44,3 +44,14 @@ } } } + +.o_event_full_page_ticket_container .o_event_full_page_ticket_barcode { + border-left: dotted 2px #939393; + .o_event_barcode { + // Barcode image has fixed size. These values are hard-coded to + // have it aligned correctly in the pdf report. + margin: 28px 0 0 -28px; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); + } +} diff --git a/addons/event/views/event_event_views.xml b/addons/event/views/event_event_views.xml index 566bc9a90c7..85655d46376 100644 --- a/addons/event/views/event_event_views.xml +++ b/addons/event/views/event_event_views.xml @@ -1,5 +1,10 @@ + + Barcode Interface + event.event_barcode_scan_view + fullscreen + event.event.form @@ -7,6 +12,11 @@
+
@@ -27,6 +37,17 @@ help="Total Registrations for this Event"> + + +
@@ -303,4 +324,11 @@ + + diff --git a/addons/event/views/event_registration_views.xml b/addons/event/views/event_registration_views.xml index 47f78b7f97f..470c8acc5df 100644 --- a/addons/event/views/event_registration_views.xml +++ b/addons/event/views/event_registration_views.xml @@ -69,6 +69,7 @@ + diff --git a/addons/event/views/res_config_settings_views.xml b/addons/event/views/res_config_settings_views.xml index ad1f7d34607..5d9ab90c904 100644 --- a/addons/event/views/res_config_settings_views.xml +++ b/addons/event/views/res_config_settings_views.xml @@ -46,8 +46,12 @@ - - + + +
+
diff --git a/addons/web/static/src/webclient/clickbot/clickbot.js b/addons/web/static/src/webclient/clickbot/clickbot.js index 6933e818f53..73ed5f0ad72 100644 --- a/addons/web/static/src/webclient/clickbot/clickbot.js +++ b/addons/web/static/src/webclient/clickbot/clickbot.js @@ -11,7 +11,7 @@ "base.menu_theme_store", "base.menu_third_party", "account.menu_action_account_bank_journal_form", - "event_barcode.menu_event_registration_desk", // there's no way to come back from this menu + "event.menu_event_registration_desk", // there's no way to come back from this menu "hr_attendance.menu_hr_attendance_kiosk_no_user_mode", // same here "pos_adyen.menu_pos_adyen_account", "payment_odoo.menu_adyen_account", diff --git a/addons/website_event/views/event_templates_page_registration.xml b/addons/website_event/views/event_templates_page_registration.xml index 698975e5d81..fb95f566cf6 100644 --- a/addons/website_event/views/event_templates_page_registration.xml +++ b/addons/website_event/views/event_templates_page_registration.xml @@ -422,7 +422,7 @@ "widget": "contact", "fields": ["address"] }'/> -