feat(fusion_clock): NFC card enrollment endpoint
Adds /fusion_clock/kiosk/nfc/enroll (jsonrpc, auth=user) that validates the enroll password, normalises the card UID, checks for duplicate assignments, writes x_fclk_nfc_card_uid, and creates a card_enrollment activity log entry. 4 new tests; 21 total passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,3 +51,56 @@ class FusionClockNfcKiosk(http.Controller):
|
||||
'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,
|
||||
}
|
||||
|
||||
@@ -79,3 +79,75 @@ class TestUidNormalization(TransactionCase):
|
||||
|
||||
def test_odd_length_returns_none(self):
|
||||
self.assertIsNone(FusionClockNfcKiosk._normalize_uid('04A2B562C18'))
|
||||
|
||||
|
||||
import json
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'fusion_clock')
|
||||
class TestEnrollEndpoint(HttpCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.ICP = cls.env['ir.config_parameter'].sudo()
|
||||
cls.ICP.set_param('fusion_clock.enable_nfc_kiosk', 'True')
|
||||
cls.ICP.set_param('fusion_clock.nfc_enroll_password', '1234')
|
||||
cls.kiosk_user = cls.env['res.users'].create({
|
||||
'name': 'Enroll Kiosk User',
|
||||
'login': 'nfc-kiosk-enroll',
|
||||
'password': 'kioskpass123',
|
||||
'group_ids': [(4, cls.env.ref('fusion_clock.group_fusion_clock_manager').id)],
|
||||
})
|
||||
cls.alice = cls.env['hr.employee'].create({'name': 'Alice E', 'x_fclk_enable_clock': True})
|
||||
cls.bob = cls.env['hr.employee'].create({'name': 'Bob E', 'x_fclk_enable_clock': True})
|
||||
|
||||
def _call(self, payload):
|
||||
self.authenticate('nfc-kiosk-enroll', 'kioskpass123')
|
||||
response = self.url_open(
|
||||
'/fusion_clock/kiosk/nfc/enroll',
|
||||
data=json.dumps({'jsonrpc': '2.0', 'method': 'call', 'params': payload}),
|
||||
headers={'Content-Type': 'application/json'},
|
||||
)
|
||||
return response.json().get('result', {})
|
||||
|
||||
def test_enroll_success(self):
|
||||
result = self._call({
|
||||
'employee_id': self.alice.id,
|
||||
'card_uid': '04:a2:b5:62:c1:80',
|
||||
'enroll_password': '1234',
|
||||
})
|
||||
self.assertTrue(result.get('success'))
|
||||
self.assertEqual(result.get('card_uid'), '04:A2:B5:62:C1:80')
|
||||
self.alice.invalidate_recordset()
|
||||
self.assertEqual(self.alice.x_fclk_nfc_card_uid, '04:A2:B5:62:C1:80')
|
||||
|
||||
def test_enroll_wrong_password(self):
|
||||
result = self._call({
|
||||
'employee_id': self.alice.id,
|
||||
'card_uid': '04:A2:B5:62:C1:81',
|
||||
'enroll_password': 'wrong',
|
||||
})
|
||||
self.assertEqual(result.get('error'), 'invalid_password')
|
||||
self.alice.invalidate_recordset()
|
||||
self.assertFalse(self.alice.x_fclk_nfc_card_uid)
|
||||
|
||||
def test_enroll_card_already_assigned(self):
|
||||
self.alice.x_fclk_nfc_card_uid = '04:A2:B5:62:C1:82'
|
||||
result = self._call({
|
||||
'employee_id': self.bob.id,
|
||||
'card_uid': '04:A2:B5:62:C1:82',
|
||||
'enroll_password': '1234',
|
||||
})
|
||||
self.assertEqual(result.get('error'), 'card_already_assigned')
|
||||
self.assertEqual(result.get('existing_employee'), 'Alice E')
|
||||
self.bob.invalidate_recordset()
|
||||
self.assertFalse(self.bob.x_fclk_nfc_card_uid)
|
||||
|
||||
def test_enroll_invalid_uid(self):
|
||||
result = self._call({
|
||||
'employee_id': self.alice.id,
|
||||
'card_uid': 'not-a-uid',
|
||||
'enroll_password': '1234',
|
||||
})
|
||||
self.assertEqual(result.get('error'), 'invalid_uid')
|
||||
|
||||
Reference in New Issue
Block a user