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
-
+
+
+
+
+
+
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:
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -27,9 +37,20 @@
-
-
- ID:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -136,7 +157,18 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
@@ -230,7 +262,18 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
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'
+
+
+
+
+ Attendee list
+
+
+
+
+ Name
+ Company
+ Ticket type
+ Phone number
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ is successfully registered
+
+
+
+ This ticket is for another event
+ Confirm attendance for ?
+
+
+ This ticket is not for an ongoing event
+
+
+ Canceled registration
+
+
+ is already registered
+
+
+
+ The registration must be paid
+
+
+
+
+
+
+ Confirm
+ Close
+ Print
+ View
+
+
+
+
+
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 @@
@@ -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"]
}'/>
-