cleanup(shopfloor): session_swap is the only tablet flow
Frontend cleanup completing Phase G of the tablet PIN session
redesign:
- tablet_lock.js: removed sessionMode branching (no legacy path).
unlock() always calls /fp/tablet/unlock_session + reloads.
handOff() always calls tabletSessionManager.lockBack('manual').
isLocked uses currentUid vs kioskUid exclusively. _checkIdle
still drives the warning UI via activity_tracker; the actual
lock RPC is owned by tablet_session_manager.
- fp_rpc.js: simplified to a thin async pass-through around @web/core
network rpc. tech_store-based tablet_tech_id injection is gone
(the session uid IS the tech).
- tech_store.js: DELETED (replaced by per-session backend attribution
+ tablet_session_manager for lock state). Removed from manifest.
- Wrapper components (shopfloor_landing, job_workspace,
manager_dashboard, plant_kanban): swapped useService('fp_shopfloor_tech_store')
for useService('fp_tablet_session_manager'); techStore.lock() ->
tabletSessionManager.lockBack('manual'). plant_kanban's defensive
try/catch on the tech_store lookup is no longer needed.
- tablet_lock.xml: Hand-Off button no longer gated on sessionMode;
always rendered.
- Tests: removed legacy TestTabletUnlock class from test_tablet_pin.py
(covered the deleted /fp/tablet/unlock route). Dropped session_mode
assertion from test_tiles_bootstrap_fields.py (the return key is
gone post-Phase-G). kiosk_uid + current_uid assertions retained.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -86,13 +86,11 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
'fusion_plating_shopfloor/static/src/scss/components/_kanban_card.scss',
|
||||
'fusion_plating_shopfloor/static/src/xml/components/kanban_card.xml',
|
||||
'fusion_plating_shopfloor/static/src/js/components/kanban_card.js',
|
||||
# ---- Phase 6.2 tablet PIN gate ----
|
||||
'fusion_plating_shopfloor/static/src/js/services/tech_store.js',
|
||||
# ---- Tablet PIN gate ----
|
||||
'fusion_plating_shopfloor/static/src/js/services/activity_tracker.js',
|
||||
# Phase D — tablet session manager (idle + ceiling timer).
|
||||
# Used by tablet_lock when fp.shopfloor.tablet_session_mode
|
||||
# ='session_swap' to call /fp/tablet/lock_session and reload
|
||||
# the page so the browser re-bootstraps under the kiosk.
|
||||
# Tablet session manager (idle + ceiling timer). Calls
|
||||
# /fp/tablet/lock_session and reloads the page so the
|
||||
# browser re-bootstraps under the kiosk.
|
||||
'fusion_plating_shopfloor/static/src/js/services/tablet_session_manager.js',
|
||||
# Phase 6.3 — fpRpc wrapper. MUST load before any consumer
|
||||
# (job_workspace, shopfloor_landing, manager_dashboard,
|
||||
|
||||
@@ -37,7 +37,7 @@ export class FpJobWorkspace extends Component {
|
||||
this.notification = useService("notification");
|
||||
this.action = useService("action");
|
||||
this.dialog = useService("dialog");
|
||||
this.techStore = useService("fp_shopfloor_tech_store");
|
||||
this.tabletSessionManager = useService("fp_tablet_session_manager");
|
||||
|
||||
this.state = useState({
|
||||
data: null,
|
||||
@@ -105,7 +105,7 @@ export class FpJobWorkspace extends Component {
|
||||
|
||||
// ---- Hand-Off (Phase 6.2) ---------------------------------------------
|
||||
handOff() {
|
||||
this.techStore.lock();
|
||||
this.tabletSessionManager.lockBack("manual");
|
||||
}
|
||||
|
||||
onJumpToBlocker({ model, id }) {
|
||||
|
||||
@@ -28,7 +28,7 @@ export class ManagerDashboard extends Component {
|
||||
setup() {
|
||||
this.notification = useService("notification");
|
||||
this.action = useService("action");
|
||||
this.techStore = useService("fp_shopfloor_tech_store");
|
||||
this.tabletSessionManager = useService("fp_tablet_session_manager");
|
||||
|
||||
this.state = useState({
|
||||
overview: null,
|
||||
@@ -153,7 +153,7 @@ export class ManagerDashboard extends Component {
|
||||
|
||||
// ---- Hand-Off (Phase 6.2) ---------------------------------------------
|
||||
handOff() {
|
||||
this.techStore.lock();
|
||||
this.tabletSessionManager.lockBack("manual");
|
||||
}
|
||||
|
||||
toggleCard(jobId) {
|
||||
|
||||
@@ -40,12 +40,7 @@ export class FpPlantKanban extends Component {
|
||||
setup() {
|
||||
this.notification = useService("notification");
|
||||
this.action = useService("action");
|
||||
// techStore may not be registered until first PIN unlock; guard with try.
|
||||
try {
|
||||
this.techStore = useService("fp_shopfloor_tech_store");
|
||||
} catch {
|
||||
this.techStore = null;
|
||||
}
|
||||
this.tabletSessionManager = useService("fp_tablet_session_manager");
|
||||
|
||||
this.state = useState({
|
||||
mode: "station",
|
||||
@@ -142,9 +137,7 @@ export class FpPlantKanban extends Component {
|
||||
}
|
||||
|
||||
onHandOff() {
|
||||
if (this.techStore && this.techStore.lock) {
|
||||
this.techStore.lock();
|
||||
}
|
||||
this.tabletSessionManager.lockBack("manual");
|
||||
}
|
||||
|
||||
onScanQr() {
|
||||
|
||||
@@ -1,74 +1,15 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — fpRpc() wrapper
|
||||
// fpRpc — thin wrapper around @web/core/network/rpc
|
||||
//
|
||||
// Drop-in replacement for the standard `rpc()` import. Automatically
|
||||
// injects the current tablet_tech_id from the tech_store into every
|
||||
// call, so server-side endpoints can attribute the action to the right
|
||||
// user via env.with_user() (see env_for_tablet_tech in
|
||||
// controllers/_tablet_audit.py).
|
||||
//
|
||||
// USE for any RPC that WRITES (start step, finish step, hold create,
|
||||
// sign-off, milestone advance). For read-only loads (kanban, workspace
|
||||
// load, manager funnel), plain rpc() is fine.
|
||||
//
|
||||
// Example:
|
||||
// import { fpRpc } from "../services/fp_rpc";
|
||||
// await fpRpc("/fp/shopfloor/start_wo", { workorder_id: stepId });
|
||||
//
|
||||
// Phase D — in `session_swap` mode, the tablet operates on a REAL Odoo
|
||||
// session minted by /fp/tablet/unlock_session whose uid IS the tech.
|
||||
// In that mode tablet_tech_id is redundant; the server attributes the
|
||||
// write to request.env.user directly. We cache the mode at module
|
||||
// level (refresh on every page load, exactly when the session may
|
||||
// have flipped).
|
||||
// Post-Phase-G of the tablet PIN session redesign: this no longer
|
||||
// injects tablet_tech_id (the session uid IS the tech). Kept as a
|
||||
// thin pass-through for backwards compatibility with callers that
|
||||
// import fpRpc; a future cleanup could remove the wrapper entirely
|
||||
// and update callers to use `rpc` directly.
|
||||
// =============================================================================
|
||||
|
||||
import { rpc as baseRpc } from "@web/core/network/rpc";
|
||||
|
||||
// Cached once per page load. Invalidated naturally by window.location.reload()
|
||||
// after every lock/unlock (the JS bundle reinitializes, cache resets to null).
|
||||
let _sessionModeCache = null; // 'legacy' | 'session_swap' | null (unknown)
|
||||
|
||||
async function _getSessionMode() {
|
||||
if (_sessionModeCache !== null) return _sessionModeCache;
|
||||
try {
|
||||
const res = await baseRpc("/web/dataset/call_kw", {
|
||||
model: "ir.config_parameter",
|
||||
method: "get_param",
|
||||
args: ["fp.shopfloor.tablet_session_mode", "legacy"],
|
||||
kwargs: {},
|
||||
});
|
||||
_sessionModeCache = res || "legacy";
|
||||
} catch (e) {
|
||||
// If the lookup fails (network blip, ACL change), fail SAFE
|
||||
// to legacy — that keeps tablet_tech_id injection on so the
|
||||
// server-side audit attribution still works.
|
||||
_sessionModeCache = "legacy";
|
||||
}
|
||||
return _sessionModeCache;
|
||||
}
|
||||
|
||||
function _getTechStore() {
|
||||
// Lazy-resolve via the global debug API — avoids circular service init
|
||||
try {
|
||||
const env = odoo.__WOWL_DEBUG__?.root?.env;
|
||||
if (env && env.services && env.services.fp_shopfloor_tech_store) {
|
||||
return env.services.fp_shopfloor_tech_store;
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
|
||||
export async function fpRpc(url, params = {}) {
|
||||
const mode = await _getSessionMode();
|
||||
if (mode !== "session_swap") {
|
||||
const techStore = _getTechStore();
|
||||
if (techStore && techStore.currentTechId) {
|
||||
params = { ...params, tablet_tech_id: techStore.currentTechId };
|
||||
}
|
||||
}
|
||||
return baseRpc(url, params);
|
||||
return rpc(url, params);
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — Tech Store (shared OWL service)
|
||||
//
|
||||
// Holds the "current tech of record" for the locked tablet. Set by
|
||||
// FpTabletLock on successful PIN unlock; cleared on auto-lock / Hand-Off.
|
||||
// Other components read currentTechId via useService("fp_shopfloor_tech_store")
|
||||
// and pass it through fpRpc() so server actions credit the right user.
|
||||
// =============================================================================
|
||||
|
||||
import { reactive } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
export const fpShopfloorTechStore = {
|
||||
start() {
|
||||
const state = reactive({
|
||||
currentTechId: null,
|
||||
currentTechName: "",
|
||||
lockedAt: null,
|
||||
});
|
||||
return {
|
||||
get currentTechId() { return state.currentTechId; },
|
||||
get currentTechName() { return state.currentTechName; },
|
||||
get isLocked() { return !state.currentTechId; },
|
||||
setTech(id, name) {
|
||||
state.currentTechId = id;
|
||||
state.currentTechName = name;
|
||||
state.lockedAt = null;
|
||||
},
|
||||
lock() {
|
||||
state.currentTechId = null;
|
||||
state.currentTechName = "";
|
||||
state.lockedAt = Date.now();
|
||||
},
|
||||
state, // exposed for OWL reactive subscriptions
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry
|
||||
.category("services")
|
||||
.add("fp_shopfloor_tech_store", fpShopfloorTechStore);
|
||||
@@ -38,7 +38,7 @@ export class FpShopfloorLanding extends Component {
|
||||
setup() {
|
||||
this.notification = useService("notification");
|
||||
this.action = useService("action");
|
||||
this.techStore = useService("fp_shopfloor_tech_store");
|
||||
this.tabletSessionManager = useService("fp_tablet_session_manager");
|
||||
|
||||
this.state = useState({
|
||||
mode: localStorage.getItem(LS_MODE) || "all_plant",
|
||||
@@ -126,7 +126,7 @@ export class FpShopfloorLanding extends Component {
|
||||
// ---- Hand-Off (Phase 6.2) ---------------------------------------------
|
||||
handOff() {
|
||||
// Tech walking away: lock the tablet so the next operator must PIN in
|
||||
this.techStore.lock();
|
||||
this.tabletSessionManager.lockBack("manual");
|
||||
}
|
||||
|
||||
// ---- Search ------------------------------------------------------------
|
||||
|
||||
@@ -31,11 +31,11 @@ export class FpTabletLock extends Component {
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.techStore = useService("fp_shopfloor_tech_store");
|
||||
this.activity = useService("fp_shopfloor_activity");
|
||||
this.notification = useService("notification");
|
||||
// Phase D: idle + ceiling timer for session_swap mode. Started
|
||||
// once tiles bootstrap shows we're already on a tech session.
|
||||
// Post-Phase-G: session_swap is the only flow. The tablet
|
||||
// session manager owns the actual idle-lock RPC; activity
|
||||
// tracker drives the warning countdown UI.
|
||||
this.tabletSessionManager = useService("fp_tablet_session_manager");
|
||||
|
||||
this.state = useState({
|
||||
@@ -49,8 +49,9 @@ export class FpTabletLock extends Component {
|
||||
clockText: this._formatTime(new Date()),
|
||||
dateText: this._formatDate(new Date()),
|
||||
company: null,
|
||||
// Phase D — feature flag + kiosk identity from bootstrap
|
||||
sessionMode: "legacy", // 'legacy' or 'session_swap'
|
||||
// Kiosk identity from bootstrap so we can tell when the
|
||||
// current browser session belongs to a tech (= unlocked) vs.
|
||||
// the kiosk (= locked).
|
||||
kioskUid: null,
|
||||
currentUid: null,
|
||||
});
|
||||
@@ -58,11 +59,13 @@ export class FpTabletLock extends Component {
|
||||
onMounted(async () => {
|
||||
await this._loadTiles();
|
||||
this._tick = setInterval(() => this._checkIdle(), 1000);
|
||||
// Heartbeat ping every 60s — for forensic visibility
|
||||
// Heartbeat ping every 60s — for forensic visibility. Only
|
||||
// ping while a tech is logged in; on the kiosk session this
|
||||
// is just noise.
|
||||
this._ping = setInterval(() => {
|
||||
if (this.techStore.currentTechId) {
|
||||
rpc("/fp/tablet/ping", { current_tech_id: this.techStore.currentTechId })
|
||||
.catch(() => {});
|
||||
if (this.state.currentUid
|
||||
&& this.state.currentUid !== this.state.kioskUid) {
|
||||
rpc("/fp/tablet/ping", {}).catch(() => {});
|
||||
}
|
||||
}, 60000);
|
||||
// Clock tick — update visible HH:MM and date label every 60s.
|
||||
@@ -72,12 +75,11 @@ export class FpTabletLock extends Component {
|
||||
this.state.clockText = this._formatTime(now);
|
||||
this.state.dateText = this._formatDate(now);
|
||||
}, 60000);
|
||||
// Session-swap mode: if we're already on a TECH session (uid
|
||||
// != kiosk), start the idle/ceiling timer immediately. This
|
||||
// handles the case where the page was reloaded after
|
||||
// unlock_session minted the tech's session.
|
||||
if (this.state.sessionMode === "session_swap"
|
||||
&& this.state.currentUid
|
||||
// If we're already on a TECH session (uid != kiosk), start
|
||||
// the idle/ceiling timer immediately. This handles the case
|
||||
// where the page was reloaded after unlock_session minted
|
||||
// the tech's session.
|
||||
if (this.state.currentUid
|
||||
&& this.state.currentUid !== this.state.kioskUid) {
|
||||
this.tabletSessionManager.beginSession();
|
||||
}
|
||||
@@ -92,14 +94,10 @@ export class FpTabletLock extends Component {
|
||||
}
|
||||
|
||||
get isLocked() {
|
||||
// SESSION-SWAP MODE: the BROWSER session itself tells us whether
|
||||
// a tech is unlocked — current_uid != kiosk_uid means unlocked.
|
||||
// LEGACY MODE: defer to the techStore client-side flag.
|
||||
if (this.state.sessionMode === "session_swap") {
|
||||
return !this.state.currentUid
|
||||
|| this.state.currentUid === this.state.kioskUid;
|
||||
}
|
||||
return this.techStore.isLocked;
|
||||
// The browser session itself tells us whether a tech is
|
||||
// unlocked — current_uid != kiosk_uid means unlocked.
|
||||
return !this.state.currentUid
|
||||
|| this.state.currentUid === this.state.kioskUid;
|
||||
}
|
||||
|
||||
async _loadTiles() {
|
||||
@@ -109,9 +107,6 @@ export class FpTabletLock extends Component {
|
||||
const res = await rpc("/fp/tablet/tiles", { station_id: stationId });
|
||||
if (res && res.ok) {
|
||||
this.state.company = res.company || null;
|
||||
// Phase D — capture session_mode + kiosk/current uids so
|
||||
// unlock() / isLocked / handOff can branch on mode.
|
||||
this.state.sessionMode = res.tablet_session_mode || "legacy";
|
||||
this.state.kioskUid = res.kiosk_uid || null;
|
||||
this.state.currentUid = res.current_uid || null;
|
||||
// Decorate each tile with an animation-delay (50ms staggered,
|
||||
@@ -130,14 +125,12 @@ export class FpTabletLock extends Component {
|
||||
}
|
||||
|
||||
_checkIdle() {
|
||||
// In session_swap mode, the tablet_session_manager owns the idle
|
||||
// timer (it polls every 5s and calls /fp/tablet/lock_session
|
||||
// directly). Skip this legacy 1s-poll path to avoid two parallel
|
||||
// idle systems competing on the same tech session.
|
||||
if (this.state.sessionMode === "session_swap") {
|
||||
return;
|
||||
}
|
||||
if (!this.techStore.currentTechId) {
|
||||
// Activity tracker drives the warning countdown UI; the actual
|
||||
// idle-lock RPC is owned by tablet_session_manager (it polls
|
||||
// every 5s and calls /fp/tablet/lock_session directly). This
|
||||
// path just updates the visible "auto-lock in N seconds" banner.
|
||||
if (!this.state.currentUid
|
||||
|| this.state.currentUid === this.state.kioskUid) {
|
||||
this.state.idleSecondsRemaining = null;
|
||||
return;
|
||||
}
|
||||
@@ -163,36 +156,19 @@ export class FpTabletLock extends Component {
|
||||
|
||||
async unlock(pin) {
|
||||
try {
|
||||
// SESSION-SWAP MODE: call the new endpoint, then reload the
|
||||
// page so the browser re-bootstraps under the tech's session.
|
||||
if (this.state.sessionMode === "session_swap") {
|
||||
const res = await rpc("/fp/tablet/unlock_session", {
|
||||
user_id: this.state.selectedTileUserId,
|
||||
pin,
|
||||
});
|
||||
if (res && res.ok) {
|
||||
// Cookie has swapped. Reload so OWL/services re-init
|
||||
// under the new (tech) session. The session manager
|
||||
// (Task D1) picks up on the next page load.
|
||||
// Match the legacy path's cleanup before reload kicks in.
|
||||
this.state.selectedTileUserId = null;
|
||||
window.location.reload();
|
||||
// Return a pending state so the caller doesn't try to
|
||||
// navigate while we're tearing down.
|
||||
return { ok: true, reloading: true };
|
||||
}
|
||||
return { ok: false, error: (res && res.error) || "Unlock failed" };
|
||||
}
|
||||
// LEGACY MODE: existing /fp/tablet/unlock path
|
||||
const res = await rpc("/fp/tablet/unlock", {
|
||||
const res = await rpc("/fp/tablet/unlock_session", {
|
||||
user_id: this.state.selectedTileUserId,
|
||||
pin,
|
||||
});
|
||||
if (res && res.ok) {
|
||||
this.techStore.setTech(res.current_tech_id, res.current_tech_name);
|
||||
this.activity.bump();
|
||||
// Cookie has swapped. Reload so OWL/services re-init
|
||||
// under the new (tech) session. The session manager
|
||||
// picks up on the next page load.
|
||||
this.state.selectedTileUserId = null;
|
||||
return { ok: true };
|
||||
window.location.reload();
|
||||
// Return a pending state so the caller doesn't try to
|
||||
// navigate while we're tearing down.
|
||||
return { ok: true, reloading: true };
|
||||
}
|
||||
return { ok: false, error: (res && res.error) || "Unlock failed" };
|
||||
} catch (err) {
|
||||
@@ -205,17 +181,9 @@ export class FpTabletLock extends Component {
|
||||
}
|
||||
|
||||
handOff() {
|
||||
// SESSION-SWAP MODE: the server destroys the tech session, then
|
||||
// we reload to re-bootstrap as the kiosk.
|
||||
if (this.state.sessionMode === "session_swap") {
|
||||
this.tabletSessionManager.lockBack("manual");
|
||||
return;
|
||||
}
|
||||
// LEGACY MODE: client-side state flip only.
|
||||
this.techStore.lock();
|
||||
this.state.selectedTileUserId = null;
|
||||
this.state.idleSecondsRemaining = null;
|
||||
this._loadTiles();
|
||||
// Server destroys the tech session, then we reload to
|
||||
// re-bootstrap as the kiosk.
|
||||
this.tabletSessionManager.lockBack("manual");
|
||||
}
|
||||
|
||||
// === 2026-05-24 redesign helpers =====================================
|
||||
|
||||
@@ -75,16 +75,10 @@
|
||||
<t t-slot="default"/>
|
||||
<FpIdleWarning t-if="state.idleSecondsRemaining !== null"
|
||||
secondsRemaining="state.idleSecondsRemaining"/>
|
||||
<!-- Phase D: floating Hand-Off button for session_swap mode.
|
||||
In legacy mode the wrapper components (landing/workspace/
|
||||
manager) own their own Hand-Off buttons that hit
|
||||
techStore.lock(). In session_swap mode those become
|
||||
no-ops (techStore isn't the source of truth anymore),
|
||||
so we render a tablet_lock-owned button that calls
|
||||
our own handOff() which routes through the session
|
||||
manager + page reload. -->
|
||||
<button t-if="state.sessionMode === 'session_swap'"
|
||||
class="o_fp_lock_handoff_btn btn btn-warning"
|
||||
<!-- Floating Hand-Off button. Post-Phase-G session_swap is
|
||||
the only flow; the button routes through the tablet
|
||||
session manager + page reload. -->
|
||||
<button class="o_fp_lock_handoff_btn btn btn-warning"
|
||||
t-on-click="handOff"
|
||||
title="Lock the tablet for the next operator">
|
||||
<i class="fa fa-lock"/> Hand Off
|
||||
|
||||
@@ -126,63 +126,6 @@ class TestTabletResetPinFor(HttpCase):
|
||||
self.assertFalse(self.target.sudo().x_fc_tablet_pin_hash)
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'fp_shopfloor', 'fp_tablet_pin')
|
||||
class TestTabletUnlock(HttpCase):
|
||||
"""P6.1.3 — /fp/tablet/unlock endpoint + lockout."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.authenticate("admin", "admin")
|
||||
self.target = self.env['res.users'].create({
|
||||
'name': 'Unlock Target', 'login': 'unlock@example.com',
|
||||
})
|
||||
self.target.sudo().set_tablet_pin('1234')
|
||||
|
||||
def _unlock(self, pin):
|
||||
return _rpc(self, '/fp/tablet/unlock',
|
||||
user_id=self.target.id, pin=pin)
|
||||
|
||||
def test_unlock_correct_pin(self):
|
||||
res = self._unlock('1234')
|
||||
self.assertTrue(res['ok'])
|
||||
self.assertEqual(res['current_tech_id'], self.target.id)
|
||||
self.assertEqual(res['current_tech_name'], 'Unlock Target')
|
||||
|
||||
def test_unlock_correct_pin_resets_fail_counter(self):
|
||||
self._unlock('0000') # fail once
|
||||
self._unlock('1234') # succeed
|
||||
self.target.invalidate_recordset(['x_fc_tablet_pin_failed_count'])
|
||||
self.assertEqual(self.target.sudo().x_fc_tablet_pin_failed_count, 0)
|
||||
|
||||
def test_unlock_wrong_pin_increments_counter(self):
|
||||
self._unlock('0000')
|
||||
self.target.invalidate_recordset(['x_fc_tablet_pin_failed_count'])
|
||||
self.assertEqual(self.target.sudo().x_fc_tablet_pin_failed_count, 1)
|
||||
|
||||
def test_lockout_after_5_fails(self):
|
||||
for _ in range(5):
|
||||
self._unlock('0000')
|
||||
res = self._unlock('0000') # 6th
|
||||
self.assertFalse(res['ok'])
|
||||
self.assertIn('locked', res['error'].lower())
|
||||
self.target.invalidate_recordset(['x_fc_tablet_locked_until'])
|
||||
self.assertTrue(self.target.sudo().x_fc_tablet_locked_until)
|
||||
|
||||
def test_lockout_blocks_even_correct_pin(self):
|
||||
for _ in range(5):
|
||||
self._unlock('0000')
|
||||
# Even the correct PIN now rejected
|
||||
res = self._unlock('1234')
|
||||
self.assertFalse(res['ok'])
|
||||
self.assertIn('locked', res['error'].lower())
|
||||
|
||||
def test_unlock_no_pin_set(self):
|
||||
self.target.clear_tablet_pin()
|
||||
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."""
|
||||
|
||||
@@ -3,10 +3,11 @@ from odoo.tests.common import HttpCase, tagged
|
||||
|
||||
@tagged('-at_install', 'post_install', 'fp_tablet')
|
||||
class TestTilesBootstrapFields(HttpCase):
|
||||
"""Phase D added 3 bootstrap fields to /fp/tablet/tiles:
|
||||
tablet_session_mode, kiosk_uid, current_uid. The OWL lock screen
|
||||
branches on these, so a contract regression would silently
|
||||
break the session-swap flow."""
|
||||
"""Post-Phase-G /fp/tablet/tiles returns kiosk_uid + current_uid so
|
||||
the OWL lock screen can detect whether the current browser session
|
||||
belongs to the kiosk (locked) or a tech (unlocked). A contract
|
||||
regression on either field would silently break the session-swap
|
||||
flow."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@@ -25,15 +26,6 @@ class TestTilesBootstrapFields(HttpCase):
|
||||
headers={'Content-Type': 'application/json'},
|
||||
).json()
|
||||
|
||||
def test_tiles_returns_session_mode(self):
|
||||
self.authenticate('fp_tablet_kiosk@enplating.local', 'tiles_test_pwd')
|
||||
resp = self._jsonrpc('/fp/tablet/tiles', {})
|
||||
result = resp.get('result', {})
|
||||
self.assertIn('tablet_session_mode', result,
|
||||
'tiles bootstrap must include tablet_session_mode for OWL branching')
|
||||
# Default value is 'legacy' until rollout flips the flag
|
||||
self.assertIn(result['tablet_session_mode'], ('legacy', 'session_swap'))
|
||||
|
||||
def test_tiles_returns_kiosk_uid(self):
|
||||
self.authenticate('fp_tablet_kiosk@enplating.local', 'tiles_test_pwd')
|
||||
resp = self._jsonrpc('/fp/tablet/tiles', {})
|
||||
|
||||
Reference in New Issue
Block a user