Idempotent post-migrate that moves mid-flight in_progress jobs whose
recipe steps are all terminal into the appropriate new state:
- draft cert exists → awaiting_cert
- no cert required → awaiting_ship
done jobs left alone (historically completed, already shipped).
Card_state + mini_timeline_json recomputed for affected rows so the
plant kanban renders correctly on first page load.
Version bump 19.0.10.31.0 → 19.0.11.0.0 triggers the migration on -u.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TRIGGER_EVENTS extended with three new events:
- cert_awaiting_issuance — fires on in_progress → awaiting_cert
- cert_voided_re_notify — fires on awaiting_ship → awaiting_cert
regress (cert voided post-issue)
- job_shipped — fires on button_mark_shipped
_dispatch routes cert events through new internal-recipient resolver
(QM/Manager/Owner via all_group_ids, transitive per Rule 13l)
instead of the partner-based stream lookup. Other events unchanged.
Mail templates (fp_cert_authority_templates.xml): two new
mail.template records bound to fp.job. Amber accent bar for awaiting,
red accent bar for void-re-issue. Deep-link to
/odoo/action-...?tab=certificates so QM lands on the right tab.
Activity type (fp_activity_types_data.xml): mail.activity.type
activity_type_issue_coc — bound to fp.job, 1-day delay, certificate
icon.
fp.job helpers:
_fp_schedule_cert_activity: round-robin by oldest login_date,
idempotent on existing open activity, soft-fails if helpers
are missing.
_fp_resolve_cert_activities: auto-resolves on awaiting_ship,
soft-fails on per-activity exceptions.
Manifest bumps:
fusion_plating_notifications 19.0.6.6.1 → 19.0.7.0.0
fusion_plating_jobs: data list gains fp_activity_types_data.xml
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Counts endpoint: certificates block — open=draft, overdue=draft+>24h.
Falls back to {open:0, overdue:0} when fp.certificate isn't installed.
JS: TABS array gains the 6th entry. Existing data-driven OWL template
auto-renders both the header tile and the body panel. Tab opens the
fp.certificate kanban grouped by state, filtered to draft by default.
Deep-link: setup() reads action.context.params.tab. The
cert_awaiting_issuance notification email links to
/odoo/action-fp_quality_dashboard?tab=certificates and lands the QM
on the right tab automatically.
Template: 'Open across all 5' → 'Open across all <tabs.length>' so
it stays correct if more tabs are added later.
Manifest: fusion_plating_quality 19.0.6.6.6 → 19.0.7.0.0
(fusion_plating_certificates already in depends — no change needed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Controller (plant_kanban.py):
- Widen domain: state IN (confirmed, in_progress, awaiting_cert,
awaiting_ship). Done jobs still drop off.
- _resolve_card_area: state=awaiting_cert → 'inspection' column,
state=awaiting_ship → 'shipping' column. State drives column
regardless of recipe shape.
- _state_chip: 🏷️ Awaiting CoC (amber) + 📦 Ready to ship (green).
- _SORT_PRIORITY: awaiting_cert=3.5, awaiting_ship=8.5.
- KPI dict: awaiting_cert + awaiting_ship counts.
- Filter clauses for the two new chips.
Model (fp_job.py):
- _compute_card_state handles new states in BOTH branches: the
no-active-step early return (where awaiting_cert/ship cards
land — all steps terminal) AND the per-step branch (defensive).
- _compute_mini_timeline_json: awaiting_cert paints inspection
dot 'current'; awaiting_ship paints shipping dot 'current'.
All earlier dots show 'done'.
SCSS (_plant_tokens.scss + _plant_card.scss):
- New tokens for amber (cert) + green (ship), light + dark variants
via the existing $o-webclient-color-scheme compile-time branch.
- .state-awaiting_cert / .state-awaiting_ship modifier classes
match the existing border-left pattern.
XML (plant_kanban.xml):
- Two new KPI tiles + two new filter chips wired to the state
filter clauses.
Manifest: fusion_plating_shopfloor 19.0.33.2.0 → 19.0.34.0.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
action_issue gated to Manager/QM/Owner via Python AccessError +
view-level groups= on the Issue button (two-layer enforcement).
Manager bypass via fp_skip_cert_authority_gate=True context flag
with chatter audit.
action_issue post-callback calls job._fp_check_advance_after_cert_issue
so the job auto-advances awaiting_cert → awaiting_ship when every
required cert is issued.
write({'state':'voided'}) override calls
job._fp_check_regress_after_cert_void so a previously-issued cert
being voided slides the job back to awaiting_cert and re-notifies
the QM.
x_fc_age_hours non-stored Float drives the Quality Dashboard age
chip + overdue filter.
Version bump 19.0.7.9.3 → 19.0.8.0.0 (spec said 19.0.6.0.0 but
current is already higher; bumped to next major instead).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
button_mark_shipped: manual transition awaiting_ship → done. Does
not re-run the bake/qty/QC gates — those passed at the in_progress
→ awaiting_cert/ship transition. Just the 'yes, shipped' stamp.
Milestone cascade (_compute_next_milestone_action) extended to
recognize the two new states:
- awaiting_cert → 'issue_certs' button
- awaiting_ship → 'mark_shipped' button
Legacy state='done' branch preserved for historical jobs.
action_advance_next_milestone now dispatches 'mark_shipped' via
_action_mark_shipped_dispatch which routes:
awaiting_ship → button_mark_shipped (new path)
done + active delivery → _action_mark_active_delivery_delivered
(legacy, unchanged)
View: 'Mark Shipped' milestone button gated on Manager/Owner groups.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-super: when finishing the last open step on an in_progress job,
run the bake/qty/QC gates from button_mark_done so failures surface
as UserError on the click (per spec D12). Without this the
auto-advance would silently fail with no error path.
Post-super: trigger _fp_check_advance_post_shop so the state
auto-advances cleanly (in_progress → awaiting_cert / awaiting_ship).
Added _fp_check_finish_gates helper on fp.job and a
fp_check_gates_only context flag honored by button_mark_done so the
gate logic is single-sourced (DRY).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
_fp_check_advance_post_shop: in_progress + all steps terminal →
awaiting_cert (cert required) or awaiting_ship. Auto-spawns cert
+ delivery and fires notifications. Idempotent. Does NOT raise —
gate failures bubble up via fp.job.step.button_finish (Task 4).
_fp_check_advance_after_cert_issue: awaiting_cert → awaiting_ship
when every required cert is state=issued.
_fp_check_regress_after_cert_void: awaiting_ship → awaiting_cert
when a previously-issued cert is voided. Re-notifies QM.
hasattr guards on _fp_schedule_cert_activity + _fp_resolve_cert_activities
keep this safe during incremental rollout — those land in Task 20.
Test scaffolding added covering helper existence + idempotency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per spec docs/superpowers/specs/2026-05-25-post-shop-cert-shipping-job-states-design.md.
Selection extension only; transitions wired in subsequent tasks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trigger: WO-30058 (SO-30058) finished all recipe steps on entech and
disappeared from the Shop Floor kanban with a draft CoC and no QM
notification. Operators reported jobs feeling lost; risk that a job
could leave the building without paperwork.
Design (Approach A, approved):
- Two new fp.job.state values between in_progress and done:
awaiting_cert + awaiting_ship
- Auto-advance on last step finish; auto-advance on cert issue
- Plant kanban widens domain, renders the two states in the existing
Final inspection / Shipping columns
- 6th tab 'Certificates' on Quality Dashboard with kanban + filters
- ACL gate on fp.certificate.action_issue restricted to
Manager / QM / Owner (transitive via all_group_ids)
- Email + mail.activity notification to QM authority group
- Migration script backfills mid-flight jobs
Shipping label printing, BoL, carrier dispatch are explicitly
out of scope; awaiting_ship is a parking column with a manual
Mark Shipped button.
Self-review pass found and fixed:
- round-robin field ambiguity (last_activity_at vs login_date)
- unstated behavior for button_mark_done gates (now in step.finish)
- placeholder version inlined (19.0.11.0.0)
- dead reference replaced with inline body
Spec: docs/superpowers/specs/2026-05-25-post-shop-cert-shipping-job-states-design.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two layout polish fixes after persona-walk feedback on the new Plant
Kanban surface (`fp_plant_kanban`).
1. Columns now run full board height with visible borders
Was: `.col` had `background: $plant-bg` (= page bg, invisible) and
no border, so only the header card (`.o_fp_col_header`) drew any
outline. Empty columns (BAKING / DE-RACKING / SHIPPING) looked
unbounded — operators couldn't tell where one column ended and
another began.
Now: `.col` is the bordered white card (Trello / Asana style),
stretches full height via grid + flex. `.o_fp_col_header` drops
its standalone border / radius / background and is just a bottom-
divider band inside the column card.
2. Horizontal scrollbar pinned to viewport bottom
Was: `.o_fp_plant_kanban` was `min-height: 100vh` (block flow) +
`.board` had `min-height: 520px; overflow-x: auto`. Scrollbar
showed at the bottom of the .board element (~520px from top of
board), floating mid-page below the empty columns.
Now: parent is `height: 100vh; display: flex; flex-direction:
column`. Header is `flex: 0 0 auto`; `.board` is `flex: 1 1 auto;
min-height: 0` so it fills all remaining vertical and its
scrollbar sits at the viewport bottom.
`.col-scroll` switched from `max-height: calc(100vh - 260px)` to
`flex: 1 1 auto; min-height: 0` so it expands inside the now-full-
height column instead of being capped at a magic number.
Version: fusion_plating_shopfloor 19.0.33.1.13.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>