[MOV] event_barcode: merge into event

This PR moves event_barcode module from enterprise to community. It is
now integrated directly with the base 'event' module. Using it is optional
and activated directly from settings, instead of having to install a
new module.

This PR also allows organizers to use QR codes without needing a barcode.

Some improvements are made in registration flow, notably improving barcode
display and usage in checkout process and outgoing emails.

Task-3411827 (Move event barcode to community)
Task-3470321 (Improve registration flow)

closes odoo/odoo#135153

Related: odoo/enterprise#45598
Related: odoo/upgrade#5183
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Odoo's Mergebot
2023-10-05 16:49:01 +00:00
committed by Odoo Online
21 changed files with 647 additions and 21 deletions
+3 -1
View File
@@ -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',
+21 -1
View File
@@ -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
}
+11 -4
View File
@@ -261,10 +261,17 @@
<t t-out="object.name or 'Guest'"/>
</span>
</td><td valign="middle" align="right">
<a t-attf-href="/event/{{ object.event_id.id }}/my_tickets?registration_ids={{ object.ids }}&amp;tickets_hash={{ object.event_id._get_tickets_access_hash(object.ids) }}&amp;responsive_html=1"
target="_blank" style="padding: 8px 12px; font-size: 12px; color: #FFFFFF; text-decoration: none !important; font-weight: 400; background-color: #875A7B; border: 0px solid #875A7B; border-radius:3px">
View Ticket
</a>
<div style="margin-bottom: 5px;">
<a t-attf-href="/event/{{ object.event_id.id }}/my_tickets?registration_ids={{ object.ids }}&amp;tickets_hash={{ object.event_id._get_tickets_access_hash(object.ids) }}&amp;responsive_html=1"
target="_blank" style="padding: 8px 12px; font-size: 12px; color: #FFFFFF; text-decoration: none !important; font-weight: 400; background-color: #875A7B; border: 0px solid #875A7B; border-radius:3px">
View Ticket
</a>
</div>
<t t-if="object.barcode">
<div style="margin-bottom: 5px;">
<img t-attf-src="/report/barcode/QR/{{object.barcode}}?&amp;width=100&amp;height=100&amp;quiet=0" alt="QR Code"/>
</div>
</t>
<t t-if="not object.company_id.uses_default_logo">
<img t-att-src="'/logo.png?company=%s' % object.company_id.id" style="padding: 0px; margin: 0px; height: auto; width: 80px;" t-att-alt="'%s' % object.company_id.name"/>
</t>
+6
View File
@@ -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:
+42 -1
View File
@@ -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
# ------------------------------------------------------------
+2 -1
View File
@@ -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)
+11 -1
View File
@@ -92,5 +92,15 @@
<field name="report_name">event.event_registration_report_template_responsive_html_ticket</field>
<field name="report_file">event.event_registration_report_template_responsive_html_ticket</field>
</record>
<record id="action_report_event_event_attendee_list" model="ir.actions.report">
<field name="name">Attendee List</field>
<field name="model">event.event</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">event.event_event_attendee_list</field>
<field name="report_file">event.event_event_attendee_list</field>
<field name="binding_model_id" ref="event.model_event_event"/>
<field name="binding_type">report</field>
<field name="print_report_name">'Attendee List - %s' % (object.name)</field>
</record>
</odoo>
+51 -8
View File
@@ -15,9 +15,19 @@
<div class="page col-6 position-relative p-1">
<div class="oe_structure"/>
<div class="o_event_foldable_badge_back text-center">
<div class="o_event_foldable_badge_barcode_container_top"></div>
<div class="o_event_foldable_badge_barcode_id_top fs-4 mt-2">
ID: <span t-out="attendee.id if attendee else '0123456789'"/>
<t t-if="event.use_barcode">
<t t-if="attendee">
<span t-field="attendee.barcode" class="barcode ms-2" t-options="{'widget': 'barcode', 'width': 200, 'height': 84, 'quiet': 0, 'humanreadable': 1}"/>
</t>
<t t-elif="not attendee">
<span t-out="12345678901234567890" class="barcode ms-2" t-options="{'widget': 'barcode', 'width': 200, 'height': 84, 'quiet': 0, 'humanreadable': 1}"/>
</t>
</t>
<div class="o_event_foldable_badge_barcode_container_top">
<img t-attf-src="/report/barcode/QR/{{attendee.barcode if attendee else '12345678901234567890'}}?&amp;width=174&amp;height=174&amp;quiet=0" alt="QR Code"/>
</div>
<div class="fs-4 mt-2">
<span t-out="attendee.barcode if attendee else '12345678901234567890'"/>
</div>
</div>
</div>
@@ -27,9 +37,20 @@
<div class="o_event_foldable_badge_bottom_quarter page col-6 p-1">
<div class="oe_structure"/>
<div class="o_event_foldable_badge_back text-center">
<div class="o_event_foldable_badge_barcode_container_bottom"></div>
<div class="o_event_foldable_badge_barcode_id_bottom fs-4 mt-2">
ID: <span t-out="attendee.id if attendee else '0123456789'"/>
<t t-if="event.use_barcode">
<t t-if="attendee">
<span t-field="attendee.barcode" class="barcode ms-2" t-options="{'widget': 'barcode', 'width': 200, 'height': 84, 'quiet': 0, 'humanreadable': 1}"/>
</t>
<t t-elif="not attendee">
<span t-out="12345678901234567890" class="barcode ms-2" t-options="{'widget': 'barcode', 'width': 200, 'height': 84, 'quiet': 0, 'humanreadable': 1}"/>
</t>
</t>
<div class="o_event_foldable_badge_barcode_container_bottom">
<img t-attf-src="/report/barcode/QR/{{attendee.barcode if attendee else '12345678901234567890'}}?&amp;width=174&amp;height=174&amp;quiet=0" alt="QR Code"/>
</div>
<div class="fs-4 mt-2">
<span t-out="attendee.barcode if attendee else '12345678901234567890'"/>
</div>
</div>
</div>
@@ -136,7 +157,18 @@
<div t-if="not responsive_html" class="o_event_full_page_ticket_sponsors_container"></div>
</div>
<div class="o_event_full_page_ticket_barcode">
<div class="o_event_full_page_ticket_barcode_container px-2"></div>
<div class="o_event_full_page_ticket_barcode_container px-2">
<t t-if="not attendee or (attendee and attendee.barcode)">
<div class="pb-3">
<img t-attf-src="/report/barcode/QR/{{attendee.barcode if attendee and attendee.barcode else '12345678901234567890'}}?&amp;width=116&amp;height=116&amp;quiet=0" alt="QR Code"/>
</div>
<t t-if="event.use_barcode">
<img class="o_event_barcode" t-attf-src="/report/barcode/?barcode_type=Code128&amp;value={{attendee.barcode if attendee and attendee.barcode else '12345678901234567890'}}&amp;width=168&amp;height=84&amp;humanreadable=1&amp;quiet=0" alt="Barcode"/>
</t>
</t>
</div>
</div>
</div>
</div>
@@ -230,7 +262,18 @@
</div>
<div class="position-absolute bottom-0 w-100 text-center">
<img t-if="event.organizer_id.image_256" class="o_event_badge_logo text-center mb-2" t-att-src="image_data_uri(event.organizer_id.image_256)"/>
<span t-if="event.badge_format != 'A4_french_fold'" class="o_event_badge_barcode_container mb-2"></span>
<span t-if="event.badge_format != 'A4_french_fold'" class="o_event_badge_barcode_container mb-2">
<img t-att-class="'mb-2' + (' ms-5' if event.organizer_id.image_256 else '')" t-attf-src="/report/barcode/QR/{{attendee.barcode if attendee else '12345678901234567890'}}?&amp;width=116&amp;height=116&amp;quiet=0" alt="QR Code"/>
<t t-if="event.use_barcode">
<t t-if="attendee">
<span t-field="attendee.barcode" class="barcode ms-2" t-options="{'widget': 'barcode', 'width': 200, 'height': 84, 'quiet': 0, 'humanreadable': 1}"/>
</t>
<t t-elif="not attendee">
<span t-out="12345678901234567890" class="barcode ms-2" t-options="{'widget': 'barcode', 'width': 200, 'height': 84, 'quiet': 0, 'humanreadable': 1}"/>
</t>
</t>
</span>
<t t-set="first_ticket" t-value="event.event_ticket_ids[0] if event.event_ticket_ids else None"/>
<t t-set="ticket" t-value="attendee.event_ticket_id if attendee else first_ticket"/>
<div t-if="ticket" class="text-center w-100" t-attf-style="background-color: {{ticket.color or '#875A7B'}};">
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="action_report_event_registration_attendee_list" model="ir.actions.report">
<field name="name">Attendee List</field>
<field name="model">event.registration</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">event.event_registration_attendee_list</field>
<field name="report_file">event.event_registration_attendee_list</field>
<field name="binding_model_id" ref="event.model_event_registration"/>
<field name="binding_type">report</field>
<field name="print_report_name">'Attendee List'</field>
</record>
</data>
<template id="attendee_list">
<h1>Attendee list</h1>
<div class="row">
<div class="col-7">
<div t-out="event.name"/>
</div>
<div class="col-5">
<span t-out="event.date_begin"/>
<i class="fa fa-arrow-right"/>
<span t-out="event.date_end"/>
</div>
</div>
<table class="table mt-3" style="page-break-after:always;">
<thead>
<tr class="text-start">
<th>Name</th>
<th>Company</th>
<th>Ticket type</th>
<th>Phone number</th>
<th></th>
</tr>
</thead>
<tbody>
<tr t-foreach="attendees" t-as="attendee">
<td><t t-out="attendee.name"/></td>
<td><t t-out="attendee.company_name"/></td>
<td><t t-out="attendee.event_ticket_id.name"/></td>
<td><t t-out="attendee.phone"/></td>
<td class="text-center">
<t t-if="attendee.barcode">
<img t-attf-src="/report/barcode/QR/{{ attendee.barcode }}?&amp;width=87&amp;height=87&amp;quiet=0" alt="QR Code"/>
</t>
</td>
</tr>
</tbody>
</table>
</template>
<template id="event_registration_attendee_list">
<t t-call="web.html_container">
<t t-call="web.internal_layout">
<t t-foreach="docs.grouped('event_id').items()" t-as="group">
<t t-call="event.attendee_list">
<t t-set="event" t-value="group[0].with_context(tz=group[0].date_tz)"/>
<t t-set="attendees" t-value="group[1]"/>
</t>
</t>
</t>
</t>
</template>
<template id="event_event_attendee_list">
<t t-call="web.html_container">
<t t-call="web.internal_layout">
<t t-foreach="docs" t-as="event">
<t t-call="event.attendee_list">
<t t-set="event" t-value="event.with_context(tz=event.date_tz)"/>
<t t-set="attendees" t-value="event.registration_ids"/>
</t>
</t>
</t>
</t>
</template>
</odoo>
@@ -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);
@@ -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;
}
}
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="event.EventScanView">
<div class="o_event_barcode_bg o_home_menu_background">
<div class="o_event_barcode_main bg-view">
<a href="#" class="o_event_previous_menu float-start"><i class="oi oi-chevron-left" t-on-click.prevent="() => this.onClickBackToEvents()"></i></a>
<div class="text-center">
<h1 t-out="data.name"/>
<p>
<t t-if="data.city and data.country">
<t t-out="data.city"/> - <t t-out="data.country"/>
</t>
<t t-if="data.city and !data.country" t-out="data.city"/>
<t t-if="data.country and !data.city" t-out="data.country"/>
</p>
<h2><small>Welcome to</small> <t t-out="data.company_name"/></h2>
<img t-if="data.company_id" t-attf-src="/web/image/res.company/{{data.company_id}}/logo_web" alt="Company Logo" class="o_event_barcode_company_image"/>
</div>
<div class="row">
<div class="col-sm-5 mt16">
<BarcodeScanner onBarcodeScanned="(ev) => this.onBarcodeScanned(ev)"/>
<h5 class="mt8 mb0 text-muted">Scan a badge</h5>
</div>
<div class="col-sm-2 mt32">
<h4 class="mt0 mb8"><i>or</i></h4>
</div>
<div class="col-sm-5 mt16">
<button class="o_event_select_attendee btn btn-primary mb16" t-on-click="() => this.onClickSelectAttendee()">
<div class="mb16 mt16">Select Attendee</div>
</button>
</div>
</div>
</div>
</div>
</t>
</templates>
@@ -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,
};
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="event.EventRegistrationSummaryDialog" >
<Dialog size="'md'" title="'Registration Summary'">
<div class="row">
<div class="col-lg-8 offset-lg-2">
<div class="text-center mb-3" t-if="registration.partner_id">
<img t-attf-src="/web/image/res.partner/{{registration.partner_id}}/avatar_128" alt="Registration" class="o_image_64_cover"/>
</div>
<div t-if="registration.status === 'confirmed_registration'" class="alert alert-success text-center" role="alert">
<t t-out="registration.name"/> is successfully registered
</div>
<div t-else="" class="alert alert-warning text-center" role="alert">
<t t-if="registration.status === 'need_manual_confirmation'">
<span>This ticket is for another event<br/>
Confirm attendance for <t t-out="registration.name"/>?</span>
</t>
<t t-elif="registration.status === 'not_ongoing_event'">
<span>This ticket is not for an ongoing event</span>
</t>
<t t-elif="registration.status === 'canceled_registration'">
<span>Canceled registration</span>
</t>
<t t-elif="registration.status == 'already_registered'">
<t t-out="registration.name"/><span> is already registered</span>
</t>
</div>
<div t-if="registration.has_to_pay" class="alert alert-danger text-center" role="alert">
The registration must be paid
</div>
</div>
</div>
<div id="registration_information" class="row">
<div class="col-lg-12">
<table class="table table-striped">
<tr><td>Event</td><td><t t-out="registration.event_display_name"/></td></tr>
<tr t-if="!registration.event_id and registration.company_name"><td>Company</td><td><t t-out="registration.company_name"/></td></tr>
<tr><td>Name</td><td><t t-out="registration.name"/></td></tr>
<tr t-if="registration.ticket_name"><td>Ticket</td><td><t t-out="registration.ticket_name"/></td></tr>
<tr t-if="registration.payment_status_value"><td>Payment</td><td><t t-out="registration.payment_status_value"/></td></tr>
<tr t-if="registration.registration_answers">
<td>Answers</td>
<td>
<span t-foreach="registration.registration_answers" t-as="registration_answer" t-key="registration_answer_index"
t-attf-class="o_tag o_tag_badge_text o_tag_color_#{registration_answer_index % 10} badge rounded-pill p-1 me-1 mb-1"
t-out="registration_answer"/>
</td>
</tr>
</table>
</div>
</div>
<t t-set-slot="footer">
<button t-if="needManualConfirmation" class="btn btn-primary" t-on-click="() => this.onRegistrationConfirm()">Confirm</button>
<button class="btn" t-att-class="needManualConfirmation ? 'btn-secondary' : 'btn-primary'" t-on-click="() => this.props.close()">Close</button>
<button class="btn btn-secondary" t-on-click="() => this.onRegistrationPrintPdf()">Print</button>
<button class="btn btn-secondary" t-on-click="() => this.onRegistrationView()">View</button>
</t>
</Dialog>
</t>
</templates>
@@ -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;
}
}
@@ -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);
}
}
+28
View File
@@ -1,5 +1,10 @@
<?xml version="1.0"?>
<odoo><data>
<record id="event_barcode_action_main_view" model="ir.actions.client">
<field name="name">Barcode Interface</field>
<field name="tag">event.event_barcode_scan_view</field>
<field name="target">fullscreen</field>
</record>
<record model="ir.ui.view" id="view_event_form">
<field name="name">event.event.form</field>
@@ -7,6 +12,11 @@
<field name="arch" type="xml">
<form string="Events" class="o_event_form_view">
<header>
<button name="%(event_barcode_action_main_view)d"
type="action"
context="{'default_event_id': active_id}">
Registration Desk
</button>
<field name="stage_id" widget="statusbar" options="{'clickable': '1'}"/>
</header>
<sheet>
@@ -27,6 +37,17 @@
help="Total Registrations for this Event">
<field name="seats_expected" widget="statinfo" string="Attendees"/>
</button>
<button name="%(event_barcode_action_main_view)d"
type="action"
class="oe_stat_button"
icon="fa-mobile"
context="{'default_event_id': active_id}">
<div class="o_stat_info">
<span class="o_stat_text">Registration Desk</span>
</div>
</button>
</div>
<field name="active" invisible="1"/>
<field name="company_id" invisible="1"/>
@@ -303,4 +324,11 @@
<field name="action" ref="event.action_event_view"/>
</record>
<!-- EVENT.EVENT HEADER: REGISTRATION DESK MENU -->
<menuitem name="Registration Desk"
id="menu_event_registration_desk"
sequence="30"
action="event.event_barcode_action_main_view"
parent="event.event_main_menu"
groups="event.group_event_registration_desk"/>
</data></odoo>
@@ -69,6 +69,7 @@
<group string="Event Information" name="event">
<field class="text-break" name="event_id" readonly="state != 'draft'"
context="{'name_with_seats_availability': True}" options="{'no_create': True}"/>
<field name="barcode" groups="base.group_no_one"/>
<field name="event_ticket_id" invisible="not event_id" readonly="state != 'draft'"
context="{'name_with_seats_availability': True}" options="{'no_open': True, 'no_create': True}"
domain="[('event_id', '=', event_id)]"/>
@@ -46,8 +46,12 @@
</setting>
</block>
<block title="Attendance" name="attendance_setting_container">
<setting id="event_barcode" company_dependent="1" help="Scan badges to confirm attendances">
<field name="module_event_barcode" widget="upgrade_boolean"/>
<setting id="event" company_dependent="1" help="Enable barcode scanning">
<field name="use_event_barcode"/>
<div class="content-group row mt16" invisible="use_event_barcode==False">
<label for="barcode_nomenclature_id" string="Barcode Nomenclature" class="col-lg-3 o_light_label"/>
<field name="barcode_nomenclature_id" required="use_event_barcode==True"/>
</div>
</setting>
</block>
</app>
@@ -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",
@@ -422,7 +422,7 @@
"widget": "contact",
"fields": ["address"]
}'/>
<div itemprop="location" t-field="event.address_id" t-options='{
<div itemprop="contactInfo" t-field="event.organizer_id" t-options='{
"widget": "contact",
"fields": ["phone", "mobile", "email"]
}'/>