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:
gsinghpal
2026-05-30 17:21:33 -04:00
parent 2a16f80d8d
commit 55898dd1d4
10 changed files with 1002 additions and 84 deletions

View File

@@ -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')