Four fixes shipped together — all surfaced during tablet UX walkthrough
on entech.
1. sale.order ACL on step completion
Technicians hit "Access Denied... sale.order" when starting/finishing
any step. _fp_check_receiving_gate + the serial-promotion helpers +
_fp_resolve_contract_review_part read step.job_id.sale_order_id (and
sale_order_line_ids) without sudo. Per Rule 13m, denormalized cross-
module reads in tablet controllers must sudo() the source recordset.
2. Workspace stuck on "Loading Job Workspace…" after Hand Off + relogin
Action params aren't URL-encoded, so the workspace remounts with
jobId=null. refresh() exited early, state.data stayed null, "Loading"
shown indefinitely. onMounted now redirects to the plant kanban
when jobId is null or the initial load returns no data.
3. 4-hour timer offset on active steps
workspace_controller used fp_format() to serialize date_started —
which converts naive UTC to user tz wall time first. JS then
appended 'Z' and parsed as UTC, so timer was offset by the user's
tz (4h on EDT). Switched to fp_isoformat_utc() (proper +00:00 ISO)
and dropped the Z-append in formatActiveStepElapsed +
isActiveStepOvertime.
4. Lock-screen clock follows FP regional setting
tablet_lock.js used d.getHours() / d.toLocaleDateString() — browser
tz. Now /fp/tablet/tiles returns tz_name (fp_user_tz resolution:
user.tz → company.x_fc_default_tz → UTC) and the formatters use
Intl.DateTimeFormat with the explicit timeZone option. plant_overview
now consumes server_time (already fp_format'd) instead of toLocaleTime
String. Same chain Odoo backend uses, so PDF / view / tablet all
agree on what time it is.
Versions: fusion_plating_jobs 19.0.10.30.0,
fusion_plating_shopfloor 19.0.33.1.12.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per user request: technicians on the tablet should only see Discuss,
To-do, Plating, AI, Maintenance, Time Off. Every other top-level app
menu (Calendar, Contacts, CRM, Sales, Dashboards, RC, Faxes, Field
Service, Fusion Clock, Invoicing, Accounting, Project, Timesheets,
Planning, Shipping, Website, Purchase, Inventory, Sign, HR, Payroll,
Attendances, Recruitment, Expenses, IoT, Link Tracker, Apps) is now
restricted to a new group_fp_office_user.
Architecture:
- New group_fp_office_user (security/fp_menu_visibility.xml) — a
marker group that controls back-office menu visibility.
- Owner / Manager / Quality Manager / Shop Manager / Sales Rep all
imply office_user via implied_ids — they see everything they did
before.
- Pure Technicians do NOT imply office_user — they see only the
tablet-friendly menus.
- A "!technician" filter would have hit managers too (because Manager
→ ... → Technician via implication), so office_user is the inverse
pattern that gets the right scoping.
Implementation:
- post_init_hook + migrations/19.0.21.4.0/post-migrate.py both call
_fp_apply_office_user_menu_visibility(env) which iterates a curated
list of menu xmlids and sets group_ids = [office_user] on each.
- Uses env.ref(..., raise_if_not_found=False) so menus from
uninstalled modules silently skip — no hard depends added.
- ir.ui.menu uses `group_ids` in Odoo 19 (was groups_id pre-18 — same
rename pattern as res.users; CLAUDE.md Rule 13c).
- Settings / Apps / Tests left untouched (already admin-restricted).
- Some menus (Field Service) end up with office_user OR their original
group — that's correct behavior: Plating Techs have neither so still
don't see them; explicit Field Technicians keep access.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds [-] / [+] buttons around every numeric input in the Record Inputs
dialog (single-value, dual-entry, and pass_fail+range branches). Tap
to increment / decrement by the recipe-author-derived step size
(stepFor() already computes this from target_min/target_max precision,
falling back to input-type defaults).
- Decrement clamps at 0 (typical qty/time/temp on a plating floor
doesn't go negative; if needed, operator can still tap the input
and type a negative value)
- Increment uses _stepRound() to avoid floating-point fuzz on decimals
- Center-aligned monospace-ish input between the buttons for clarity
- inputmode='decimal' (or 'numeric' for time fields) hint so when the
operator does tap the input, the iPad shows a number keypad instead
of the full keyboard
Touches single-value, dual-entry (min/max), and pass_fail+range. Other
multi-field widgets (multi-point thickness, bath chemistry panel) still
use plain inputs — separate request if they need steppers too.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Native browser confirm popups look out of place in the tablet UI.
Mark Counted is already a deliberate prior step, so requiring a
second confirmation on Close Receiving was just friction. If a
receiver hits Close prematurely, action_reset_to_counted on
fp.receiving from the back office is the recovery path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs:
1. Gate silently passed when step.recipe_node_id was NULL — happened
to every WO-30057 step after this morning's clone delete (the FK
ON DELETE SET NULL wiped the link). _fp_missing_required_step_inputs
returned an empty recordset when node was None, so the gate had
nothing to fail on and button_finish succeeded with zero audit.
Fix: _fp_check_step_inputs_complete now treats NULL recipe_node_id
as an explicit "no recipe link" hard block. Operator can't finish;
manager bypass posts chatter audit.
2. No tablet UI for the manager bypass. The gate's bypass was a
Python context flag — invisible from the JS layer, so managers
were stuck behind the same hard error as operators.
Fix: new /fp/workspace/finish_step endpoint returns structured
errors (gate type, missing_prompts list, bypass_available bool).
Server-side enforces manager group when bypass=True (can't trust
the client). New FpFinishBlockDialog OWL modal renders:
- Non-manager: Cancel + Record Inputs
- Manager: Cancel + Record Inputs + ⚠ Bypass & Finish (audit)
JobWorkspace.onFinishStep routes plain finishes through the new
endpoint; signature-required steps still go through /fp/workspace/sign_off
(separate gate). Added is_manager to /fp/workspace/load payload so
the JS knows which dialog variant to render.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure client-side tick — 1s setInterval bumps state.tickNow which the
template reads via formatActiveStepElapsed(step). No RPC per tick.
Reads step.date_started_iso (UTC) from the existing payload, parses
to ms, displays elapsed since.
- Green pill (#d1fae5 bg, monospace tabular-nums) on the ACTIVE badge
- Flips red (#fee2e2 + pulse animation) when elapsed > 1.5x
duration_expected — visual cue for the operator that the step is
running long against the recipe target
Cleanup interval on onWillUnmount alongside the existing 15s refresh
interval.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the receiver workflow to the Job Workspace tablet view (was the
gap behind WO-30057 sitting in Receiving with no way to advance).
Receivers no longer need to go to the backend form.
Workspace card (renders above the step list when fp.receiving in
state draft/counted on the linked SO):
- Draft state: numeric box-count input + per-line received_qty /
condition picker (good/damaged/mixed) + Damage Log panel + Mark
Counted button. Autosaves on input blur.
- Counted state: read-only summary (boxes, parts, who/when) +
Damage Log still editable + Close Receiving button.
- Closed: card disappears, recipe takes over.
New FpDamageDialog OWL modal:
- Severity pill picker (Cosmetic / Functional / Rejected) with
color-coded active state
- Required description textarea
- Action Required pill picker (None / Notify / Return / As-Is)
- Photo capture: both "Take Photo" (input capture="environment"
triggers tablet camera) AND "Upload" (file picker fallback).
Multi-photo with preview grid + per-photo remove.
5 new endpoints on workspace_controller.py:
- receiving_save_lines (autosave box_count_in + per-line qty/cond)
- receiving_mark_counted (wraps action_mark_counted)
- receiving_close (wraps action_close)
- damage_create (creates fp.receiving.damage + attaches base64 photos)
- damage_delete (removes a damage row)
No model changes — wraps existing fp.receiving actions and damage
CRUD. C3 (outbound shipping carrier/label) is a separate spec.
Spec: in-conversation brainstorm (C1+C2) following the 2026-05-24
workspace step actions spec; no standalone doc since scope is small.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fix: in the Job Workspace tablet view, the Start button was buried
inside a parent t-if that required the step to already be in_progress
or blocked. So ready/paused steps showed no buttons at all -
operators couldn't advance the WO from this screen (the reason the
user couldn't complete anything on WO-30057).
Template restructure (job_workspace.xml):
- Always-visible line 1 (icon + step# + name + ACTIVE/PAUSED badge + meta)
- Non-terminal detail panel (chips + instructions + opt-out + GateViz)
visible on every non-done step so operator reads ahead
- Action row dispatched per-kind via getStepActions() helper
Per-kind action dispatcher (job_workspace.js):
- in_progress -> Record Inputs, Pause, Finish (or Finish & Sign Off)
- paused -> Resume, Record Inputs, Finish
- contract_review (ready) -> Open QA-005 Form
- gating (ready) -> Mark Passed (1-click start+finish)
- requires_rack_assignment -> Start (Assign Rack) - opens FpRackPartsDialog
- else (ready) -> Start
5 new handlers: onPauseStep / onResumeStep / onMarkPassed /
onOpenContractReview / onStartWithRack. Pause and Resume use ORM RPC
(button_pause/button_resume) since no HTTP endpoint exists.
New model method (fp.job.step.action_mark_gating_passed):
- 1-click pass for gating steps - does button_start + button_finish
in one transaction, posts chatter "Gate X marked passed by Y"
- Raises UserError if called on a non-gating step (defensive)
- Bypasses S21 required-inputs gate (gating steps have no inputs)
Controller: workspace_controller.py adds requires_rack_assignment to
the step payload so the JS dispatcher can route correctly.
Spec: docs/superpowers/specs/2026-05-24-workspace-step-actions-design.md
Sub-B (Record Inputs tablet polish: inputmode/prefill/date pickers/
signature pad/camera) is brainstormed but deferred.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root causes documented:
1. Recipe 3620 ENP-ALUM-BASIC had duplicate sequences (Contract
Review + Masking both at seq 10; Incoming Inspection + Racking
both at seq 20). Clones inherited the ambiguity and resolved by
id ordering, putting Masking before Incoming Inspection — which
meant new jobs went straight to Plating column after the
contract-review auto-complete.
2. 24 per-part clone recipes accumulated, all carrying the broken
ordering.
3. ~10 kind=other stragglers across the base recipes (Blasting,
Adhesion Test Coupon, Strip Process, Chemical Conversion etc.)
4. Recipe duplication had no kind safety net.
Implementation shipped in commits referenced from the plan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-up fixes caught during the entech deploy of recipe cleanup:
1. RESOLVER_KIND_TO_ACTIVE_KIND was missing a self-pass entry for
'wet_process'. The new aliases added in 19.0.21.3.0 (Chemical
Conversion, Trivalent Chromate Conversion, Strip Process - AL,
Plug The Threaded Holes via mask) directly return 'wet_process'
from the resolver — without the passthrough they didn't translate
to any active kind and stayed as 'other'. Added 'wet_process':
'wet_process' so the migration's Phase 2 backfill catches them.
2. Migration 19.0.10.26.0 Phase 3 was using batch unlink
(clone_recipes.unlink()) which tripped a PostgreSQL FK cascade
ordering bug on entech ("insert or update on parent_id violates
FK ..." during the CASCADE chain). Rewrote Phase 3 to delete one
clone at a time with SAVEPOINT per clone — slower but immune to
the batching bug, and one failed clone doesn't roll back the
whole transaction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migration 19.0.10.26.0/post-migrate.py runs in 5 phases:
1. Resequence recipe 3620 ENP-ALUM-BASIC ops to fix the duplicate-
sequence bug (Contract Review=10, Incoming Inspection=20,
Masking=30, Racking=40, then the rest). Also delete the empty
duplicate ENP-Alum Line sub_process (id 4056).
2. Backfill kind on all kind=other nodes via the extended resolver
from fusion_plating 19.0.21.3.0
3. Delete all per-part clone recipes (name contains em-dash)
4. Recompute fp.job.step.area_kind on all steps
5. Recompute fp.job.active_step_id + card_state on in-flight jobs
Plant kanban: no_parts cards now always land in the Receiving column
regardless of active_step area_kind. The receiver works Receiving;
that's where the card belongs when parts haven't arrived.
Spec: docs/superpowers/specs/2026-05-24-recipe-cleanup-design.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolver (fp_resolve_step_kind) extensions:
- New aliases: blasting/bead blast/media blast variants, adhesion
testing, corrosion testing, lab testing, strip process, chemical
conversion, trivalent chromate, plug the threaded holes, air dry,
desmut, soak clean, cleaner, nickel strike/strip
- Parenthetical suffix stripping - "Masking (If Required)" resolves
through "masking", "Incoming Inspection (Standard)" through
"incoming inspection", "Trivalent Chromate Conversion (A-14 / A)"
through "trivalent chromate conversion"
- New RESOLVER_KIND_TO_ACTIVE_KIND map translates the resolver's
vocabulary (cleaning/electroclean/etch/rinse/strike/dry/wbf_test
-> wet_process) so the resolver output lands on active kinds only
Auto-classify hook on fusion.plating.process.node:
- _fp_autoclassify_kind() upgrades kind_id when current is 'other'
AND name resolves via the resolver. Idempotent - never overrides
a non-'other' kind. Skip via context flag fp_skip_kind_autoclassify
- Wired into create() and write() (only fires when name or kind_id
changed on write)
- Side-effects: recipe duplication via copy() auto-corrects newly
copied nodes; Simple/Tree editor authoring auto-classifies as soon
as the name is saved
Spec: docs/superpowers/specs/2026-05-24-recipe-cleanup-design.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Required because fp.job.card_state (stored) has @api.depends including
active_step_id.area_kind. When step.area_kind changes, Odoo's trigger
chain searches fp.job by active_step_id — non-stored fields can't be
queried in WHERE clauses, raising ValueError("Cannot convert ... to
SQL because it is not stored").
Caught during entech deploy of 19.0.10.25.0/post-migrate.py Phase 3
(steps._compute_area_kind() failed on first run). store=True makes
the column searchable and the trigger chain works.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- fp.step.kind.area_kind: drop tracking=True (model doesn't inherit
mail.thread; tracking was a no-op emitting a startup warning).
- Migration 19.0.10.25.0: anchor the De-Masking ILIKE so it doesn't
wildcard-match "Ready For De-Masking" (which the earlier "Ready %"
rule already routes to gating). Also drop the cur_code='mask' filter
so the 4 De-Masking nodes still classified as 'other' get picked up
on fresh re-runs too.
Direct SQL applied the 4-row fix on entech (post-migrate doesn't
re-run for already-applied versions); this commit keeps fresh
installs and any future re-runs consistent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fix the Shop Floor plant kanban so cards land in the right column:
- fp.job._compute_active_step_id walks priority chain
(in_progress > paused > ready > pending), not just in_progress
- fp.job._compute_card_state edge case respects job.state='done'
(no more bogus 'contract_review' label on done jobs)
- fp.job.step._compute_area_kind reads kind.area_kind directly;
legacy _STEP_KIND_TO_AREA dict removed (50+ lines deleted)
- /fp/landing/plant_kanban filters out done/cancelled jobs from
the live board
Migration 19.0.10.25.0 backfills template metadata (codes,
descriptions, icons, kind_id) on 30 unfinished library templates
and repoints recipe nodes for 6 unambiguous name patterns
(Blasting -> blast, Ready For X -> gating, De-Masking -> demask,
Scheduling -> gating, Nickel Strip -> wet_process,
Pre-Meas/Check Sulfamate -> inspect).
Battle test bt_s24_between_steps.py covers between-step routing,
paused step lifecycle, and done-job board filter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add required area_kind Selection to fp.step.kind so each kind
self-declares which plant-view column its steps belong in. Replaces
the hardcoded _STEP_KIND_TO_AREA dict (removed in fp_job_step.py
in the follow-up commit).
- New `blast` kind for the Blasting column (sequence=35)
- 26 existing kind records seeded with area_kind in XML
- Pre-migrate 19.0.21.2.0 seeds existing rows BEFORE NOT NULL hits
the schema; also activates derack/demask/gating that were
deactivated in 19.0.20.6.0 but are needed for the full taxonomy
- Step Kind form + list views surface area_kind (badge + chip)
- Step Kind search adds Group By Shop Floor Column
- Simple Editor kind picker shows "Masking — Masking column"
suffix so authors see the routing at pick time
- Add Hot Water Porosity Test (A-15) + Final Inspection / Packaging
templates (used by 7+3 recipe nodes that previously had no
library entry)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Odoo 19's Session.authenticate(env, credential) takes an Environment as
the first arg, not a db-name string. Passing request.db triggered
TypeError: 'str' object is not callable on the internal
env(user=None, su=False) reset.
Fixes the "Odoo Server Error" dialog operators saw when trying to PIN
unlock from the tablet. Same fix applies to lock_session (which was
silently masked by its broad except Exception).
Bumps fusion_plating_shopfloor to 19.0.33.1.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure code change (no DB schema), but bumping the patch version
keeps repo manifest aligned with the deployed state so the next
-u doesn't no-op due to version match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The kiosk_login in /fp/tablet/lock_session was hardcoded to the
data XML's original value ('fp_tablet_kiosk@enplating.local'). The
data record is noupdate='1', so admins can (and on entech, did)
rename the kiosk user on the form for memorability — the rename
persists through -u, but the hardcoded string in the controller
silently breaks the re-auth-as-kiosk path.
Fix: resolve the kiosk login dynamically via env.ref of the xmlid
'fusion_plating_shopfloor.user_fp_tablet_kiosk'. Robust against any
future rename. CLAUDE.md updated to make 'identify by xmlid, never
by login string' an explicit convention for the tablet flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tablet PIN session redesign Phase G removed all tablet_tech_id
plumbing. CLAUDE.md still documented the old session-pool + kwarg
flow which would mislead future-Claude. Updated to describe the
new per-tech-session attribution + kiosk re-auth flow, plus the
gotcha about keeping ir.config_parameter['fp.tablet.kiosk_password']
in sync with the actual user-record password.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records the legacy-tablet-flow-removed state. Triggers -u so the
module's installed version reflects the post-cleanup code (the
ir_module_module row shows 19.0.33.1.0 after deploy).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
Session swap makes attribution automatic via request.env.user — the
tablet_tech_id plumbing is dead code after the kiosk + per-tech-session
architecture lands. Removed kwarg from 3 endpoints in
manager_controller, 11 in shopfloor_controller, 3 in
workspace_controller. _tablet_audit.env_for_tablet_tech import gone
from all 3 files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owner-only smart button on res.users form. Click opens the audit log
filtered to that user (both user_id and attempted_user_id, so
failed unlock attempts against a tile show up too).
Compute is non-stored: search_count on the audit model per user on
demand. Sudo'd because the audit model has Owner-only ACL — the
compute fires for the form-viewing user (Owner) who would see the
results anyway via the menu.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plating > Configuration > Tablet Audit Log. Read-only list with
decoration (green=unlock, red=failed, warning=ceiling/force,
muted=manual/idle). Form shows full forensic detail incl. ip/ua.
Owner-only via groups=fusion_plating.group_fp_owner on the menu.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Important I1: tablet_session_manager.beginSession() now calls
_removeListeners() (and clears any pending _tickHandle) defensively
at start. Prevents DOM listener leak on dev hot-reload or any path
that re-bootstraps without a clean endSession() first.
Important I2: tablet_lock._checkIdle() early-returns in session_swap
mode. The tablet_session_manager owns idle tracking there (5s poll,
calls /fp/tablet/lock_session directly). Was previously dormant by
accident because session_swap never populates the legacy techStore;
explicit guard makes the decoupling intentional.
Minor M5: session_swap unlock success now resets selectedTileUserId
before window.location.reload(), matching the legacy path''s
cleanup pattern. Cosmetic before reload kicks in.
Minor M9: New test_tiles_bootstrap_fields with 3 HttpCase tests
asserting /fp/tablet/tiles returns tablet_session_mode, kiosk_uid,
and current_uid. The OWL lock screen branches on all three — a
contract regression would silently break session_swap.
Minor M10: Added inline comment near _sessionModeCache declaration
in fp_rpc.js explaining the page-reload-invalidates-cache lifecycle.
Deferred (for future polish, NOT in this commit):
- I3 (_getSessionMode ACL gap for tech users — functionally correct,
just suboptimal; cache fallback to ''legacy'' kicks in)
- M6 (wrapper component Hand-Off buttons no-op in session_swap)
- M7 (hardcoded idle/ceiling thresholds — server-configurable later)
- M8 (timer divergence vs activity_tracker — unify later)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When tablet_session_mode='session_swap', the server attributes every
write via request.env.user — there's no need to pass tablet_tech_id
in the RPC params. Caches the mode lookup at module level so we don't
round-trip on every RPC.
Legacy mode unchanged — fpRpc still injects tablet_tech_id from
techStore.currentTechId.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When ir.config_parameter[fp.shopfloor.tablet_session_mode]='session_swap',
PIN submit calls /fp/tablet/unlock_session and reloads the page; the
new session manager service kicks in on next mount. handOff() calls
lockBack('manual') which destroys the tech session server-side and
re-auths as kiosk.
Legacy mode unchanged — same /fp/tablet/unlock + techStore flow.
The feature flag, kiosk_uid, and current_uid arrive via the existing
/fp/tablet/tiles bootstrap response (Task D0).
Adds a tablet_lock-owned Hand-Off button visible only in session_swap
mode (in legacy mode wrapper components own their own buttons that hit
techStore.lock(); session_swap renders our own button so the manual
hand-off goes through lockBack() + page reload).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tracks idle + ceiling timers for an unlocked tech session. Fires
/fp/tablet/lock_session when either trips, then reloads the page so
the browser re-bootstraps under the fresh kiosk session.
Defaults: 10min idle, 8hr ceiling, 5s tick interval. Listens for
click/touchstart/keydown/mousemove as activity signals.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
OWL lock screen needs to know (a) the active session mode (legacy or
session_swap) to branch between endpoints, and (b) the kiosk uid to
determine 'is the current browser session the kiosk?' Both come from
the bootstrap response so no extra round-trips on every render.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Important 1: lock_session now closes the original unlock event's
session_ended_at via the same parameterized-SQL bypass pattern used
by the force-lock cron. Without this, every Hand-Off click became
a duplicate force_lock event 8 hours later (cron saw the unlock still
open and re-processed).
Important 2: test_unlock_lock_session_endpoints setUp now
unconditionally overrides the kiosk password (was gated on
'if not get_param(...)' which broke on entech where the post-migrate
hook already generated a random password — tests failed against the
real value). HttpCase rolls back per test so no persistence.
Minor 4: _cron_force_lock_stale_sessions now routes the force_lock
create through write_event helper for consistency (single audit-write
path; helper captures acting_uid/ip/ua uniformly).
Minor 5: Hoisted local imports inside method bodies to top-of-file
in tablet_controller.py (AccessDenied, _tablet_session_audit) and
fp_tablet_session_event.py (timedelta, write_event).
Minor 6: New test_force_lock_cron.py with 3 tests: stale session
emits force_lock + closes original; recent session unaffected;
already-closed session not re-processed. Would have caught
Important 1 if it had existed during Phase C review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every 5 minutes, find active unlock events past 8-hour ceiling and
mark them force-locked. SQL bypass of the model's read-only ACL is
the only path that can update existing rows (no Python write() works
because the model override blocks even sudo writes without the
explicit fp_tablet_audit_admin_write context flag).
Ceiling configurable via ir.config_parameter[fp.tablet.session_ceiling_hours].
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Writes lock event (manual/idle/ceiling) with duration computed from
the open unlock event. Then logout + re-authenticate as kiosk via
the password stored in ir.config_parameter['fp.tablet.kiosk_password'].
Falls back to 'needs_kiosk_relogin' if the kiosk password is missing
(sysadmin must log in manually). Logs every event for forensic
review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PIN verify -> request.session.authenticate(type=fp_tablet_pin) -> new
session sid, cookie swap, audit event written. Failed attempts also
written to audit log (failed_unlock, failure_reason=wrong_pin or
locked_out or no_pin_set or user_inactive).
OLD /fp/tablet/unlock stays alive during the 1-week overlap window
per spec Section 5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single source for sha256(session sid), ua trim, ip/acting_uid capture
from request. Used by unlock_session, lock_session, and force-lock cron.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
C1: Add placeholder fp_tablet_cron.xml + fp_tablet_session_event_views.xml
so the module is installable now (real content lands in Phase C task C4
and Phase E task E1 respectively).
I1: test_tablet_pin_auth_manager now passes {} (not self.env) as the
env arg to _check_credentials — matches what request.session.authenticate
provides and what the base implementation expects.
I2: Auth manager role check now uses user_sudo.all_group_ids (transitive)
instead of group_ids (direct) per CLAUDE.md rules 13l + 23. Owner users
who hold Owner directly still match all 5 shop-branch xmlids via the
implication chain.
I3: fp.tablet.session.event gains Python-layer write() + unlink()
overrides that always raise AccessError unless the explicit
fp_tablet_audit_admin_write / fp_tablet_audit_admin_purge context flag
is set. Closes the gap between the model's append-only docstring and
its actual enforcement (ACL-only previously).
M1: Hoisted 'from odoo.exceptions import AccessDenied' to top-of-file
imports next to existing UserError import.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8 tests: correct/wrong/missing PIN, missing/unknown login, inactive
user, no shop-branch role, and pass-through of other credential types.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Validates PIN hash + shop-branch role membership when the credential
type is fp_tablet_pin. Goes through Odoo's standard _check_credentials
chain so future 2FA / IP-gate modules layer cleanly on top.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-review findings on Phase A (Tablet PIN Session Redesign):
I1: Security XML comment now honestly describes the kiosk as Internal
User + explicit reads, not 'near-zero ACL'. base.group_user is kept
(required for auth='user' HTTP routes to function) but the comment
no longer overstates how locked-down the kiosk is.
I2: New ir.rule scopes the kiosk's ir.config_parameter read to keys
matching 'fp.tablet.%' or 'fp.shopfloor.%'. Combined with the
existing model-level read ACL, kiosk can no longer enumerate
third-party secrets (e.g. fusion_tasks.vapid_private_key) or
arbitrary API keys stored in ICP.
I3: post-migrate docstring now advises sysadmins to unlink the
plaintext ICP password row after kiosk tablets are paired, to
minimise plaintext-in-backups risk. Rotation procedure documented.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generates a random kiosk password on first deploy, stores in
ir.config_parameter for sysadmin retrieval. Idempotent — re-runs
on subsequent -u leave the password alone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Kiosk holds the tablet session when no tech is PIN-unlocked.
Password is auto-generated by the post-migrate hook (Task A5).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>