A proper shared-device PIN kiosk for clients who don't want NFC: photo-tile grid (+search) -> tap -> PIN (or first-use create) -> optional master-gated selfie -> clock, in the NFC kiosk's dark glass + brand-gradient style. Built as an Odoo 19 Interaction; new pin_kiosk.scss (scoped); reworked clock_kiosk.py (search +avatar/has_pin, verify_pin needs_setup, set_pin, clock via kiosk location). Drops the redundant kiosk_pin_required (PIN always required); relabels the company kiosk location; adds a PIN-kiosk app icon. Opt-in via enable_kiosk (off by default). HttpCase tests added. Bump 19.0.4.0.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
131 lines
6.0 KiB
Python
131 lines
6.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
import json
|
|
from odoo.tests.common import HttpCase, tagged
|
|
|
|
|
|
@tagged('-at_install', 'post_install', 'fusion_clock')
|
|
class TestPinKioskIdentity(HttpCase):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.ICP = cls.env['ir.config_parameter'].sudo()
|
|
cls.ICP.set_param('fusion_clock.enable_kiosk', 'True')
|
|
cls.location = cls.env['fusion.clock.location'].create({
|
|
'name': 'PIN Plant', 'latitude': 43.65, 'longitude': -79.38, 'radius': 100,
|
|
})
|
|
cls.env.company.x_fclk_nfc_kiosk_location_id = cls.location.id
|
|
cls.env['res.users'].create({
|
|
'name': 'PIN Kiosk Op', 'login': 'pin-kiosk-op', 'password': 'kioskpass123',
|
|
'group_ids': [(4, cls.env.ref('fusion_clock.group_fusion_clock_manager').id)],
|
|
})
|
|
cls.withpin = cls.env['hr.employee'].create({
|
|
'name': 'Pat WithPin', 'x_fclk_enable_clock': True, 'x_fclk_kiosk_pin': '1234',
|
|
})
|
|
cls.nopin = cls.env['hr.employee'].create({
|
|
'name': 'Nora NoPin', 'x_fclk_enable_clock': True,
|
|
})
|
|
|
|
def _call(self, route, params):
|
|
self.authenticate('pin-kiosk-op', 'kioskpass123')
|
|
resp = self.url_open(route, data=json.dumps({
|
|
'jsonrpc': '2.0', 'method': 'call', 'params': params,
|
|
}), headers={'Content-Type': 'application/json'})
|
|
return resp.json().get('result', {})
|
|
|
|
def test_search_returns_avatar_and_has_pin(self):
|
|
res = self._call('/fusion_clock/kiosk/search', {'query': ''})
|
|
rows = {e['name']: e for e in res['employees']}
|
|
self.assertIn('Pat WithPin', rows)
|
|
self.assertTrue(rows['Pat WithPin']['has_pin'])
|
|
self.assertFalse(rows['Nora NoPin']['has_pin'])
|
|
self.assertIn('/web/image/hr.employee.public/', rows['Pat WithPin']['avatar_url'])
|
|
|
|
def test_verify_pin_correct(self):
|
|
res = self._call('/fusion_clock/kiosk/verify_pin', {'employee_id': self.withpin.id, 'pin': '1234'})
|
|
self.assertTrue(res.get('success'))
|
|
|
|
def test_verify_pin_incorrect(self):
|
|
res = self._call('/fusion_clock/kiosk/verify_pin', {'employee_id': self.withpin.id, 'pin': '9999'})
|
|
self.assertEqual(res.get('error'), 'invalid_pin')
|
|
|
|
def test_verify_pin_needs_setup(self):
|
|
res = self._call('/fusion_clock/kiosk/verify_pin', {'employee_id': self.nopin.id, 'pin': ''})
|
|
self.assertTrue(res.get('needs_setup'))
|
|
|
|
def test_set_pin_success_then_required(self):
|
|
res = self._call('/fusion_clock/kiosk/set_pin', {'employee_id': self.nopin.id, 'pin': '4321'})
|
|
self.assertTrue(res.get('success'))
|
|
self.assertEqual(self.nopin.x_fclk_kiosk_pin, '4321')
|
|
res2 = self._call('/fusion_clock/kiosk/set_pin', {'employee_id': self.nopin.id, 'pin': '0000'})
|
|
self.assertEqual(res2.get('error'), 'already_set')
|
|
|
|
def test_set_pin_rejects_bad_format(self):
|
|
res = self._call('/fusion_clock/kiosk/set_pin', {'employee_id': self.withpin.id, 'pin': '12'})
|
|
self.assertEqual(res.get('error'), 'bad_pin')
|
|
|
|
|
|
@tagged('-at_install', 'post_install', 'fusion_clock')
|
|
class TestPinKioskClock(HttpCase):
|
|
|
|
PNG = ('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwC'
|
|
'AAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=')
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.ICP = cls.env['ir.config_parameter'].sudo()
|
|
cls.ICP.set_param('fusion_clock.enable_kiosk', 'True')
|
|
cls.location = cls.env['fusion.clock.location'].create({
|
|
'name': 'PIN Plant 2', 'latitude': 43.65, 'longitude': -79.38, 'radius': 100,
|
|
})
|
|
cls.env.company.x_fclk_nfc_kiosk_location_id = cls.location.id
|
|
cls.env['res.users'].create({
|
|
'name': 'PIN Op2', 'login': 'pin-op2', 'password': 'kioskpass123',
|
|
'group_ids': [(4, cls.env.ref('fusion_clock.group_fusion_clock_manager').id)],
|
|
})
|
|
cls.emp = cls.env['hr.employee'].create({
|
|
'name': 'Quinn Clock', 'x_fclk_enable_clock': True, 'x_fclk_kiosk_pin': '1234',
|
|
})
|
|
|
|
def _clock(self, employee_id, photo_b64=''):
|
|
self.authenticate('pin-op2', 'kioskpass123')
|
|
resp = self.url_open('/fusion_clock/kiosk/clock', data=json.dumps({
|
|
'jsonrpc': '2.0', 'method': 'call',
|
|
'params': {'employee_id': employee_id, 'photo_b64': photo_b64},
|
|
}), headers={'Content-Type': 'application/json'})
|
|
return resp.json().get('result', {})
|
|
|
|
def _latest(self, emp_id):
|
|
return self.env['hr.attendance'].search(
|
|
[('employee_id', '=', emp_id)], order='check_in desc', limit=1)
|
|
|
|
def test_clock_in_uses_kiosk_location(self):
|
|
res = self._clock(self.emp.id)
|
|
self.assertTrue(res.get('success'))
|
|
self.assertEqual(res.get('action'), 'clock_in')
|
|
att = self._latest(self.emp.id)
|
|
self.assertEqual(att.x_fclk_clock_source, 'kiosk')
|
|
self.assertEqual(att.x_fclk_location_id, self.location)
|
|
|
|
def test_photo_off_stores_nothing(self):
|
|
self.ICP.set_param('fusion_clock.enable_photo_verification', 'False')
|
|
emp = self.env['hr.employee'].create({
|
|
'name': 'Quinn Off', 'x_fclk_enable_clock': True, 'x_fclk_kiosk_pin': '1234'})
|
|
self._clock(emp.id, self.PNG)
|
|
self.assertFalse(self._latest(emp.id).x_fclk_check_in_photo)
|
|
|
|
def test_photo_on_stores_selfie(self):
|
|
self.ICP.set_param('fusion_clock.enable_photo_verification', 'True')
|
|
emp = self.env['hr.employee'].create({
|
|
'name': 'Quinn On', 'x_fclk_enable_clock': True, 'x_fclk_kiosk_pin': '1234'})
|
|
self._clock(emp.id, self.PNG)
|
|
self.assertTrue(self._latest(emp.id).x_fclk_check_in_photo)
|
|
|
|
def test_no_location_configured(self):
|
|
self.env.company.x_fclk_nfc_kiosk_location_id = False
|
|
emp = self.env['hr.employee'].create({
|
|
'name': 'Quinn NoLoc', 'x_fclk_enable_clock': True, 'x_fclk_kiosk_pin': '1234'})
|
|
res = self._clock(emp.id)
|
|
self.assertEqual(res.get('error'), 'no_location_configured')
|