cleanup(shopfloor): remove legacy /fp/tablet/unlock + _tablet_audit helper
Session-swap is now the only flow. Legacy /fp/tablet/unlock endpoint deleted. _tablet_audit.py (env_for_tablet_tech helper) deleted with its last caller gone. /fp/tablet/ping no longer takes current_tech_id (session uid IS the tech). /fp/tablet/tiles drops tablet_session_mode return key (kiosk_uid + current_uid retained for OWL isLocked logic). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,42 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
"""Helper for audit-credit propagation (Phase 6.3 tablet redesign).
|
||||
|
||||
Controllers that accept an optional `tablet_tech_id` kwarg use this
|
||||
helper to switch their `env` to the tech-of-record before performing
|
||||
writes. The result: chatter posts + create_uid/write_uid carry the
|
||||
unlocked tech's identity, not the tablet's persistent session user.
|
||||
"""
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def env_for_tablet_tech(env, tablet_tech_id):
|
||||
"""Return an env scoped to `tablet_tech_id` if it's a valid user;
|
||||
otherwise return the original env unchanged.
|
||||
|
||||
Validation: the user must exist and be active. We deliberately do
|
||||
NOT cross-check that they actually unlocked recently — the OWL
|
||||
component is the source of truth for "who's at the tablet right
|
||||
now", and the only path that produces a tablet_tech_id is a
|
||||
successful /fp/tablet/unlock followed by an active session in the
|
||||
OWL tech_store.
|
||||
"""
|
||||
if not tablet_tech_id:
|
||||
return env
|
||||
try:
|
||||
tech_id = int(tablet_tech_id)
|
||||
except (TypeError, ValueError):
|
||||
return env
|
||||
User = env['res.users'].sudo()
|
||||
tech = User.browse(tech_id)
|
||||
if not tech.exists() or not tech.active:
|
||||
_logger.warning(
|
||||
"tablet_tech_id %s invalid (not found or inactive); "
|
||||
"falling back to session uid %s",
|
||||
tablet_tech_id, env.uid,
|
||||
)
|
||||
return env
|
||||
return env(user=tech_id)
|
||||
@@ -141,78 +141,6 @@ class FpTabletController(http.Controller):
|
||||
)
|
||||
return {'ok': True}
|
||||
|
||||
# ======================================================================
|
||||
# /fp/tablet/unlock — verify PIN + manage failure counter / lockout
|
||||
# ======================================================================
|
||||
@http.route('/fp/tablet/unlock', type='jsonrpc', auth='user')
|
||||
def unlock(self, user_id, pin):
|
||||
env = request.env
|
||||
Users = env['res.users'].sudo() # need sudo to read hash field
|
||||
target = Users.browse(int(user_id))
|
||||
if not target.exists():
|
||||
return {'ok': False, 'error': _('User not found.')}
|
||||
|
||||
# No PIN set yet — caller must set one first
|
||||
if not target.x_fc_tablet_pin_hash:
|
||||
return {
|
||||
'ok': False,
|
||||
'error': _('No PIN set. Set one in Preferences first.'),
|
||||
'needs_setup': True,
|
||||
}
|
||||
|
||||
# Currently locked out?
|
||||
now = fields.Datetime.now()
|
||||
if target.x_fc_tablet_locked_until and target.x_fc_tablet_locked_until > now:
|
||||
return {
|
||||
'ok': False,
|
||||
'error': _('Account locked. Try again in a few minutes.'),
|
||||
'locked_until': target.x_fc_tablet_locked_until.isoformat(),
|
||||
}
|
||||
|
||||
if target.verify_tablet_pin(pin):
|
||||
# Reset failure state on success
|
||||
target.write({
|
||||
'x_fc_tablet_pin_failed_count': 0,
|
||||
'x_fc_tablet_locked_until': False,
|
||||
})
|
||||
_logger.info(
|
||||
"Tablet unlocked by uid %s (session uid %s)",
|
||||
target.id, env.uid,
|
||||
)
|
||||
return {
|
||||
'ok': True,
|
||||
'current_tech_id': target.id,
|
||||
'current_tech_name': target.name,
|
||||
}
|
||||
|
||||
# Wrong PIN — increment and check threshold
|
||||
new_count = (target.x_fc_tablet_pin_failed_count or 0) + 1
|
||||
threshold = int(env['ir.config_parameter'].sudo().get_param(
|
||||
'fp.shopfloor.tablet_pin_fail_threshold', 5,
|
||||
))
|
||||
lockout_min = int(env['ir.config_parameter'].sudo().get_param(
|
||||
'fp.shopfloor.tablet_pin_fail_lockout_minutes', 5,
|
||||
))
|
||||
vals = {'x_fc_tablet_pin_failed_count': new_count}
|
||||
if new_count >= threshold:
|
||||
vals['x_fc_tablet_locked_until'] = now + timedelta(minutes=lockout_min)
|
||||
target.write(vals)
|
||||
_logger.warning(
|
||||
"Tablet PIN failure for uid %s (count=%d, locked=%s)",
|
||||
target.id, new_count, bool(vals.get('x_fc_tablet_locked_until')),
|
||||
)
|
||||
if vals.get('x_fc_tablet_locked_until'):
|
||||
return {
|
||||
'ok': False,
|
||||
'error': _('Too many failed attempts. Locked for %d minutes.') % lockout_min,
|
||||
'locked_until': vals['x_fc_tablet_locked_until'].isoformat(),
|
||||
}
|
||||
return {
|
||||
'ok': False,
|
||||
'error': _('Incorrect PIN.'),
|
||||
'attempts_remaining': threshold - new_count,
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# /fp/tablet/unlock_session — verify PIN + mint REAL Odoo session as tech
|
||||
# ======================================================================
|
||||
@@ -498,12 +426,10 @@ class FpTabletController(http.Controller):
|
||||
})
|
||||
# Clocked-in first, then alphabetical within bucket
|
||||
tiles.sort(key=lambda t: (not t['is_clocked_in'], t['name']))
|
||||
# Phase D bootstrap: lock screen needs the feature flag + kiosk
|
||||
# uid so it knows whether to call /unlock (legacy) or
|
||||
# /unlock_session (new), and whether the current session is
|
||||
# the kiosk's (= no tech logged in).
|
||||
ICP = env['ir.config_parameter'].sudo()
|
||||
session_mode = ICP.get_param('fp.shopfloor.tablet_session_mode', 'legacy')
|
||||
# Lock screen needs the kiosk uid + current session uid so it can
|
||||
# tell whether a tech is currently logged in (current_uid !=
|
||||
# kiosk_uid) or the tablet is back on the kiosk (current_uid ==
|
||||
# kiosk_uid). Post-Phase-G session_swap is the only flow.
|
||||
kiosk = env.ref(
|
||||
'fusion_plating_shopfloor.user_fp_tablet_kiosk',
|
||||
raise_if_not_found=False,
|
||||
@@ -513,7 +439,6 @@ class FpTabletController(http.Controller):
|
||||
'ok': True,
|
||||
'company': _lock_company_payload(request.env),
|
||||
'tiles': tiles,
|
||||
'tablet_session_mode': session_mode,
|
||||
'kiosk_uid': kiosk_uid,
|
||||
'current_uid': request.env.uid,
|
||||
}
|
||||
@@ -522,15 +447,9 @@ class FpTabletController(http.Controller):
|
||||
# /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):
|
||||
def ping(self):
|
||||
"""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.
|
||||
the server-side session is alive. The session uid IS the tech
|
||||
post-Phase-G — no extra plumbing needed.
|
||||
"""
|
||||
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()}
|
||||
|
||||
Reference in New Issue
Block a user