feat(fusion_clock): NFC kiosk — enrollment, manager page, sounds, lock, profile photos
Kiosk work across this session (19.0.3.6.0 -> 19.0.3.10.0): - Program-from-unknown-tap: amber prompt -> Manager PIN -> pick/create employee -> binds the captured UID (no re-tap). Reassign moves a card between employees. - Manager page (gear, when unlocked): search employees + tag status; assign/re-tag, clear tag, archive employee, + new employee. Server-gated by the enroll password. - Screen lock: kiosk starts locked (tap-only); Unlock -> Manager PIN, Lock button; PIN remembered for the session so the gear never re-prompts. - Sounds: pleasant + loud sine chimes (rising in / descending out) + a low "denied" tone for wrong/unknown taps. Gated by fusion_clock.enable_sounds. - Guided profile-photo capture for employees with no picture (clock-in or enroll): live camera + oval face guide -> capture -> preview -> save to hr.employee. - PIN no longer re-renders per digit; centered result card; 12h time; clock-out shows "Worked Xh Ym this shift"; modern clock idle icon; faster animations/result timers; session keep-alive so the kiosk login never expires. - New endpoints: create_employee, clear_tag, delete_employee (archive), verify_pin, save_profile_photo; enroll gains force-reassign. - Docs: fusion_clock is now developed in Claude Code (dropped Cursor references). Spec/plan under fusion_clock/docs/superpowers/. Deployed live on entech (odoo-entech / LXC 111 on pve-worker5), v19.0.3.10.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -411,3 +411,46 @@ class TestEmployeeSearch(HttpCase):
|
||||
self.assertIn('employees', result)
|
||||
names = [e['name'] for e in result['employees']]
|
||||
self.assertIn('Searchable Steve', names)
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'fusion_clock')
|
||||
class TestCreateEmployeeEndpoint(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': 'Create Kiosk User',
|
||||
'login': 'nfc-kiosk-create',
|
||||
'password': 'kioskpass123',
|
||||
'group_ids': [(4, cls.env.ref('fusion_clock.group_fusion_clock_manager').id)],
|
||||
})
|
||||
|
||||
def _call(self, payload):
|
||||
self.authenticate('nfc-kiosk-create', 'kioskpass123')
|
||||
response = self.url_open(
|
||||
'/fusion_clock/kiosk/nfc/create_employee',
|
||||
data=json.dumps({'jsonrpc': '2.0', 'method': 'call', 'params': payload}),
|
||||
headers={'Content-Type': 'application/json'},
|
||||
)
|
||||
return response.json().get('result', {})
|
||||
|
||||
def test_create_success_clock_enabled(self):
|
||||
result = self._call({'name': 'Newhire Nancy', 'enroll_password': '1234'})
|
||||
self.assertIn('employee_id', result)
|
||||
emp = self.env['hr.employee'].browse(result['employee_id'])
|
||||
self.assertTrue(emp.exists())
|
||||
self.assertEqual(emp.name, 'Newhire Nancy')
|
||||
self.assertTrue(emp.x_fclk_enable_clock)
|
||||
|
||||
def test_create_wrong_password(self):
|
||||
result = self._call({'name': 'Should Not Exist', 'enroll_password': 'wrong'})
|
||||
self.assertEqual(result.get('error'), 'invalid_password')
|
||||
self.assertFalse(self.env['hr.employee'].search([('name', '=', 'Should Not Exist')]))
|
||||
|
||||
def test_create_invalid_name(self):
|
||||
result = self._call({'name': 'X', 'enroll_password': '1234'})
|
||||
self.assertEqual(result.get('error'), 'invalid_name')
|
||||
|
||||
Reference in New Issue
Block a user