# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) import logging import re from odoo import http from odoo.http import request _logger = logging.getLogger(__name__) _UID_HEX_PATTERN = re.compile(r'^[0-9A-F]+$') class FusionClockNfcKiosk(http.Controller): """NFC tap-to-clock kiosk controller. Reuses FusionClockAPI helpers.""" @staticmethod def _normalize_uid(uid): """Normalize an NFC card UID to canonical hex (uppercase, colon-separated). Returns None if the input is empty or not valid hex. """ if not uid: return None cleaned = uid.strip().upper().replace('-', '').replace(':', '').replace(' ', '') if not cleaned or not _UID_HEX_PATTERN.match(cleaned): return None if len(cleaned) % 2 != 0: return None return ':'.join(cleaned[i:i+2] for i in range(0, len(cleaned), 2)) @http.route('/fusion_clock/kiosk/nfc', type='http', auth='user', website=True) def nfc_kiosk_page(self, **kw): """Render the NFC kiosk page for a wall-mounted tablet.""" user = request.env.user if not user.has_group('fusion_clock.group_fusion_clock_manager'): return request.redirect('/my') ICP = request.env['ir.config_parameter'].sudo() if ICP.get_param('fusion_clock.enable_nfc_kiosk', 'False') != 'True': return request.redirect('/my') company = request.env.company location = company.x_fclk_nfc_kiosk_location_id values = { 'page_name': 'nfc_kiosk', 'company_name': company.name, 'location_name': location.name if location else 'No location configured', 'location_configured': bool(location), 'photo_required': ICP.get_param('fusion_clock.nfc_photo_required', 'True') == 'True', 'debug_enabled': ICP.get_param('fusion_clock.nfc_kiosk_debug', 'False') == 'True', } return request.render('fusion_clock.nfc_kiosk_page', values) @staticmethod def _check_enroll_password(env, supplied): """Verify the enroll-mode password. Empty config = always-allow for managers.""" configured = env['ir.config_parameter'].sudo().get_param('fusion_clock.nfc_enroll_password', '') if not configured: return True return (supplied or '') == configured @http.route('/fusion_clock/kiosk/nfc/enroll', type='jsonrpc', auth='user', methods=['POST']) def nfc_enroll(self, employee_id=0, card_uid='', enroll_password='', **kw): """Bind an NFC card UID to an employee. Manager-gated, password-gated.""" user = request.env.user if not user.has_group('fusion_clock.group_fusion_clock_manager'): return {'error': 'access_denied'} if not self._check_enroll_password(request.env, enroll_password): return {'error': 'invalid_password'} normalized = self._normalize_uid(card_uid) if not normalized: return {'error': 'invalid_uid'} Employee = request.env['hr.employee'].sudo() target = Employee.browse(int(employee_id or 0)) if not target.exists(): return {'error': 'employee_not_found'} existing = Employee.search([ ('x_fclk_nfc_card_uid', '=', normalized), ('id', '!=', target.id), ], limit=1) if existing: return { 'error': 'card_already_assigned', 'existing_employee': existing.name, } target.x_fclk_nfc_card_uid = normalized # Activity log (uses 'card_enrollment' + 'nfc_kiosk' selections added in Task 2) request.env['fusion.clock.activity.log'].sudo().create({ 'employee_id': target.id, 'log_type': 'card_enrollment', 'description': f"NFC card {normalized} enrolled by {user.name}", 'source': 'nfc_kiosk', }) return { 'success': True, 'employee_name': target.name, 'card_uid': normalized, }