import base64 import hashlib import hmac import io import qrcode from odoo import api, fields, models TICKET_REF_SEQUENCE_CODE = 'event_qr_ticketing.ticket_ref' class EventRegistration(models.Model): _inherit = 'event.registration' ticket_ref = fields.Char(string='Ticket Reference', readonly=True, copy=False, index=True) ticket_qr = fields.Binary(string='Ticket QR', compute='_compute_ticket_qr') checked_in = fields.Boolean(string='Checked In', default=False, copy=False, readonly=True) check_in_time = fields.Datetime(string='Check-in Time', readonly=True, copy=False) check_in_user_id = fields.Many2one('res.users', string='Checked In By', readonly=True, copy=False) _ticket_ref_uniq = models.Constraint('unique(ticket_ref)', 'Ticket reference must be unique.') @api.model_create_multi def create(self, vals_list): registrations = super().create(vals_list) for registration in registrations: if not registration.ticket_ref: registration.ticket_ref = registration._generate_ticket_ref() return registrations def _generate_ticket_ref(self): self.ensure_one() seq_number = self.env['ir.sequence'].next_by_code(TICKET_REF_SEQUENCE_CODE) or '1' return f"TIX-{self.event_id.id}-{seq_number}" def _get_signing_secret(self): secret = self.env['ir.config_parameter'].sudo().get_param('database.secret') return secret.encode() def _compute_ticket_token(self): """HMAC of event_id:registration_id:ticket_ref, signed with the per-database secret.""" self.ensure_one() payload = f"{self.event_id.id}:{self.id}:{self.ticket_ref}".encode() return hmac.new(self._get_signing_secret(), payload, hashlib.sha256).hexdigest()[:16] def _get_ticket_qr_value(self): self.ensure_one() return f"{self.ticket_ref}|{self._compute_ticket_token()}" def _compute_ticket_qr(self): for registration in self: if not registration.ticket_ref: registration.ticket_qr = False continue qr = qrcode.QRCode(box_size=6, border=2) qr.add_data(registration._get_ticket_qr_value()) qr.make(fit=True) image = qr.make_image(fill_color='black', back_color='white') buffer = io.BytesIO() image.save(buffer, format='PNG') registration.ticket_qr = base64.b64encode(buffer.getvalue()) @api.model def _verify_scanned_token(self, scanned_value): """Verify a scanned QR payload ('ticket_ref|token'). Returns (status, registration) where status is one of: 'ok', 'already', 'invalid' (bad/forged token), 'not_found'. """ parts = (scanned_value or '').split('|') if len(parts) != 2 or not parts[0]: return 'invalid', self.env['event.registration'] ticket_ref, token = parts registration = self.search([('ticket_ref', '=', ticket_ref)], limit=1) if not registration: return 'not_found', registration expected_token = registration._compute_ticket_token() if not hmac.compare_digest(expected_token, token): return 'invalid', registration if registration.checked_in: return 'already', registration return 'ok', registration @api.model def _lookup_by_ticket_ref(self, ticket_ref): """Manual entry lookup: staff-trusted, no signature check required.""" registration = self.search([('ticket_ref', '=', ticket_ref)], limit=1) if not registration: return 'not_found', registration if registration.checked_in: return 'already', registration return 'ok', registration def action_check_in(self): self.ensure_one() self.write({ 'checked_in': True, 'check_in_time': fields.Datetime.now(), 'check_in_user_id': self.env.user.id, }) if self.state != 'done': self.action_set_done() return True