Files
Odoo-Modules/fusion_clock/wizard/clock_nfc_enrollment_wizard.py
gsinghpal c2646f59c4 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'.
2026-05-15 18:55:42 -04:00

157 lines
5.8 KiB
Python

# -*- 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',
})