feat(fusion_plating_shopfloor): /fp/tablet/tiles + /fp/tablet/ping endpoints (P6.1.4-P6.1.5)
Tiles returns the lock-screen grid: operator-group users, sorted clocked-in-first then alphabetical, with avatar URL + has_pin flag. Honours station.x_fc_authorised_user_ids when non-empty (Phase 6.1.6 adds that field). Ping is a lightweight ack used by FpTabletLock as a heartbeat — logs current_tech_id at DEBUG for forensic visibility. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -147,3 +147,65 @@ class FpTabletController(http.Controller):
|
||||
'error': _('Incorrect PIN.'),
|
||||
'attempts_remaining': threshold - new_count,
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# /fp/tablet/tiles — lock-screen tile grid
|
||||
# ======================================================================
|
||||
@http.route('/fp/tablet/tiles', type='jsonrpc', auth='user')
|
||||
def tiles(self, station_id=None):
|
||||
env = request.env
|
||||
op_group = env.ref(
|
||||
'fusion_plating.group_fusion_plating_operator',
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
if not op_group:
|
||||
return {'ok': False, 'error': 'operator group missing'}
|
||||
|
||||
# Determine candidate users — station roster wins if non-empty
|
||||
users = op_group.user_ids
|
||||
if station_id:
|
||||
Station = env['fusion.plating.shopfloor.station']
|
||||
station = Station.browse(int(station_id))
|
||||
if (station.exists()
|
||||
and 'x_fc_authorised_user_ids' in station._fields
|
||||
and station.x_fc_authorised_user_ids):
|
||||
users = station.x_fc_authorised_user_ids
|
||||
|
||||
# has_pin needs sudo-read on the hash field
|
||||
clocked_ids = set()
|
||||
if 'hr.employee' in env and hasattr(
|
||||
env['hr.employee'], '_fp_clocked_in_user_ids',
|
||||
):
|
||||
clocked_ids = env['hr.employee']._fp_clocked_in_user_ids() or set()
|
||||
|
||||
users_sorted = users.sorted('name')
|
||||
users_sudo = users_sorted.sudo()
|
||||
tiles = []
|
||||
for u, u_sudo in zip(users_sorted, users_sudo):
|
||||
tiles.append({
|
||||
'user_id': u.id,
|
||||
'name': u.name,
|
||||
'avatar_url': f'/web/image/res.users/{u.id}/avatar_128',
|
||||
'is_clocked_in': u.id in clocked_ids,
|
||||
'has_pin': bool(u_sudo.x_fc_tablet_pin_hash),
|
||||
})
|
||||
# Clocked-in first, then alphabetical within bucket
|
||||
tiles.sort(key=lambda t: (not t['is_clocked_in'], t['name']))
|
||||
return {'ok': True, 'tiles': tiles}
|
||||
|
||||
# ======================================================================
|
||||
# /fp/tablet/ping — heartbeat used by the OWL component on every action
|
||||
# ======================================================================
|
||||
@http.route('/fp/tablet/ping', type='jsonrpc', auth='user')
|
||||
def ping(self, current_tech_id=None):
|
||||
"""Lightweight heartbeat. Used by the OWL component to confirm
|
||||
the server-side session is alive AND to log the tech-of-record
|
||||
every few minutes so the server has forensic visibility into
|
||||
which tech was 'driving' the tablet at any moment.
|
||||
"""
|
||||
if current_tech_id:
|
||||
_logger.debug(
|
||||
"Tablet ping: session uid %s carrying tablet_tech_id=%s",
|
||||
request.env.uid, current_tech_id,
|
||||
)
|
||||
return {'ok': True, 'server_time': fields.Datetime.now().isoformat()}
|
||||
|
||||
@@ -181,3 +181,36 @@ class TestTabletUnlock(HttpCase):
|
||||
res = self._unlock('1234')
|
||||
self.assertFalse(res['ok'])
|
||||
self.assertTrue(res.get('needs_setup'))
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'fp_shopfloor', 'fp_tablet_pin')
|
||||
class TestTabletTiles(HttpCase):
|
||||
"""P6.1.4 — /fp/tablet/tiles endpoint."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.authenticate("admin", "admin")
|
||||
self.op_group = self.env.ref('fusion_plating.group_fusion_plating_operator')
|
||||
self.alice = self.env['res.users'].create({
|
||||
'name': 'Alice Tile', 'login': 'alice_tile@example.com',
|
||||
'group_ids': [(6, 0, [self.op_group.id])],
|
||||
})
|
||||
self.bob = self.env['res.users'].create({
|
||||
'name': 'Bob Tile', 'login': 'bob_tile@example.com',
|
||||
'group_ids': [(6, 0, [self.op_group.id])],
|
||||
})
|
||||
self.alice.sudo().set_tablet_pin('1111')
|
||||
|
||||
def test_tiles_returns_all_operators_without_station(self):
|
||||
res = _rpc(self, '/fp/tablet/tiles')
|
||||
self.assertTrue(res['ok'])
|
||||
names = [t['name'] for t in res['tiles']]
|
||||
self.assertIn('Alice Tile', names)
|
||||
self.assertIn('Bob Tile', names)
|
||||
|
||||
def test_tile_has_pin_flag(self):
|
||||
res = _rpc(self, '/fp/tablet/tiles')
|
||||
alice_tile = next(t for t in res['tiles'] if t['name'] == 'Alice Tile')
|
||||
bob_tile = next(t for t in res['tiles'] if t['name'] == 'Bob Tile')
|
||||
self.assertTrue(alice_tile['has_pin'])
|
||||
self.assertFalse(bob_tile['has_pin'])
|
||||
|
||||
Reference in New Issue
Block a user