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'.
157 lines
5.8 KiB
Python
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',
|
|
})
|