From c2646f59c4380afcc292f88724a9f0e7ed35da28 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Fri, 15 May 2026 18:55:42 -0400 Subject: [PATCH] feat(fusion_clock): NFC card enrollment wizard + employee form field Adds a tap-driven enrollment workflow so managers can pair NFC/RFID cards to employees using a USB HID reader at their desk: - New wizard model fusion.clock.nfc.enrollment.wizard with auto-focused Card UID field, employee picker, and reassignment warning if the card is already held by someone else. - Two actions: 'Enroll Card' (single) and 'Enroll & Next' (bulk). - Menu entry under Fusion Clock root, manager-gated. - Exposes x_fclk_nfc_card_uid on the Employee form Clock Settings section (next to Kiosk PIN) so it can be inspected/edited directly. - Bumps manifest to 19.0.3.1.0 for asset cache bust. Wizard reuses FusionClockNfcKiosk._normalize_uid so stored format matches what the kiosk /tap endpoint looks up later. Reassignment clears the UID from the previous holder and logs both events to the activity log under 'card_enrollment'. --- fusion_clock/__init__.py | 1 + fusion_clock/__manifest__.py | 4 +- fusion_clock/views/clock_menus.xml | 8 + fusion_clock/views/hr_employee_views.xml | 3 + fusion_clock/wizard/__init__.py | 5 + .../wizard/clock_nfc_enrollment_views.xml | 61 +++++++ .../wizard/clock_nfc_enrollment_wizard.py | 156 ++++++++++++++++++ 7 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 fusion_clock/wizard/__init__.py create mode 100644 fusion_clock/wizard/clock_nfc_enrollment_views.xml create mode 100644 fusion_clock/wizard/clock_nfc_enrollment_wizard.py diff --git a/fusion_clock/__init__.py b/fusion_clock/__init__.py index 1a132267..26d143b0 100644 --- a/fusion_clock/__init__.py +++ b/fusion_clock/__init__.py @@ -4,3 +4,4 @@ from . import models from . import controllers +from . import wizard diff --git a/fusion_clock/__manifest__.py b/fusion_clock/__manifest__.py index 50ecd741..1f3c47cb 100644 --- a/fusion_clock/__manifest__.py +++ b/fusion_clock/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Clock', - 'version': '19.0.3.0.0', + 'version': '19.0.3.1.0', 'category': 'Human Resources/Attendances', 'summary': 'Complete Employee T&A with Geofencing, Shifts, Penalties, Overtime, Kiosk, Dashboard & Payroll Export', 'description': """ @@ -77,6 +77,8 @@ Integrates natively with Odoo's hr.attendance module for full payroll compatibil 'views/portal_report_templates.xml', 'views/kiosk_templates.xml', 'views/kiosk_nfc_templates.xml', + # Wizards + 'wizard/clock_nfc_enrollment_views.xml', ], 'assets': { 'web.assets_frontend': [ diff --git a/fusion_clock/views/clock_menus.xml b/fusion_clock/views/clock_menus.xml index a987d824..675e2152 100644 --- a/fusion_clock/views/clock_menus.xml +++ b/fusion_clock/views/clock_menus.xml @@ -97,6 +97,14 @@ sequence="50" groups="group_fusion_clock_manager,group_fusion_clock_team_lead"/> + + + + diff --git a/fusion_clock/wizard/__init__.py b/fusion_clock/wizard/__init__.py new file mode 100644 index 00000000..d4fa3d08 --- /dev/null +++ b/fusion_clock/wizard/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) + +from . import clock_nfc_enrollment_wizard diff --git a/fusion_clock/wizard/clock_nfc_enrollment_views.xml b/fusion_clock/wizard/clock_nfc_enrollment_views.xml new file mode 100644 index 00000000..3a4ce266 --- /dev/null +++ b/fusion_clock/wizard/clock_nfc_enrollment_views.xml @@ -0,0 +1,61 @@ + + + + + + fusion.clock.nfc.enrollment.wizard.form + fusion.clock.nfc.enrollment.wizard + +
+ + + + + + + + + + +
+
+
+
+
+ + + + Enroll NFC Card + fusion.clock.nfc.enrollment.wizard + form + + new + + +
diff --git a/fusion_clock/wizard/clock_nfc_enrollment_wizard.py b/fusion_clock/wizard/clock_nfc_enrollment_wizard.py new file mode 100644 index 00000000..d34f20ce --- /dev/null +++ b/fusion_clock/wizard/clock_nfc_enrollment_wizard.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) + +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + +from ..controllers.clock_nfc_kiosk import FusionClockNfcKiosk + + +class FusionClockNfcEnrollmentWizard(models.TransientModel): + """Tap-driven NFC card enrollment for the fusion_clock kiosk. + + Flow: + 1. Manager opens this wizard. + 2. Card UID field is auto-focused. + 3. Manager taps an NFC card on the USB HID reader; the reader types the + UID into the focused field and hits Enter, which advances focus to + the Employee picker. + 4. Manager selects the employee. + 5. Manager clicks "Enroll Card" (closes wizard) or "Enroll & Next" + (resets wizard for the next card). + + The wizard reuses ``FusionClockNfcKiosk._normalize_uid`` so the stored + format matches whatever the kiosk's ``/tap`` endpoint will look up later. + """ + + _name = 'fusion.clock.nfc.enrollment.wizard' + _description = 'NFC Card Enrollment Wizard' + + card_uid = fields.Char( + string='Card UID', + required=True, + help='Tap an NFC card on the USB reader. Most HID readers type the ' + 'UID followed by Enter, which advances focus to the Employee ' + 'field below. You can also paste a UID manually.', + ) + normalized_uid = fields.Char( + string='Normalized UID', + compute='_compute_normalized_uid', + store=False, + help='UID after format normalization (uppercase, colon-separated hex). ' + 'This is what gets stored on the employee record.', + ) + employee_id = fields.Many2one( + 'hr.employee', + string='Employee', + domain=[('x_fclk_enable_clock', '=', True)], + ) + existing_employee_id = fields.Many2one( + 'hr.employee', + string='Currently Assigned To', + compute='_compute_existing_employee', + store=False, + ) + warning_message = fields.Char( + compute='_compute_existing_employee', + store=False, + ) + + @api.depends('card_uid') + def _compute_normalized_uid(self): + for wiz in self: + wiz.normalized_uid = FusionClockNfcKiosk._normalize_uid(wiz.card_uid) or '' + + @api.depends('normalized_uid', 'employee_id') + def _compute_existing_employee(self): + for wiz in self: + if not wiz.normalized_uid: + wiz.existing_employee_id = False + wiz.warning_message = '' + continue + existing = self.env['hr.employee'].sudo().search([ + ('x_fclk_nfc_card_uid', '=', wiz.normalized_uid), + ], limit=1) + if existing and existing != wiz.employee_id: + wiz.existing_employee_id = existing + wiz.warning_message = _( + "⚠ This card is currently assigned to %(name)s. " + "Enrolling will reassign it.", + name=existing.name, + ) + else: + wiz.existing_employee_id = False + wiz.warning_message = '' + + def action_enroll(self): + """Enroll the card to the selected employee and close the wizard.""" + self.ensure_one() + self._do_enroll() + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Card Enrolled'), + 'message': _("%(uid)s assigned to %(name)s.", + uid=self.normalized_uid, name=self.employee_id.name), + 'type': 'success', + 'sticky': False, + 'next': {'type': 'ir.actions.act_window_close'}, + }, + } + + def action_enroll_and_next(self): + """Enroll the card, then reopen the wizard cleared for the next card.""" + self.ensure_one() + self._do_enroll() + return { + 'type': 'ir.actions.act_window', + 'name': _('Enroll NFC Card'), + 'res_model': self._name, + 'view_mode': 'form', + 'target': 'new', + 'context': {}, # explicitly empty so no defaults persist + } + + def _do_enroll(self): + """Validate, then write the normalized UID to the employee record. + + Reassigns the card from any existing holder. Logs the event in the + activity log for audit. + """ + if not self.normalized_uid: + raise ValidationError(_( + "Card UID is empty or not valid hex. Tap the card again on " + "the reader." + )) + if not self.employee_id: + raise ValidationError(_("Please select an employee.")) + + # Reassignment: clear the UID from whoever currently holds it + if self.existing_employee_id and self.existing_employee_id != self.employee_id: + self.existing_employee_id.sudo().x_fclk_nfc_card_uid = False + self.env['fusion.clock.activity.log'].sudo().create({ + 'employee_id': self.existing_employee_id.id, + 'log_type': 'card_enrollment', + 'description': _( + "NFC card %(uid)s unassigned by %(user)s (reassigning to %(new)s)", + uid=self.normalized_uid, + user=self.env.user.name, + new=self.employee_id.name, + ), + 'source': 'nfc_kiosk', + }) + + self.employee_id.sudo().x_fclk_nfc_card_uid = self.normalized_uid + + self.env['fusion.clock.activity.log'].sudo().create({ + 'employee_id': self.employee_id.id, + 'log_type': 'card_enrollment', + 'description': _( + "NFC card %(uid)s enrolled by %(user)s", + uid=self.normalized_uid, user=self.env.user.name, + ), + 'source': 'nfc_kiosk', + })