Two issues on the Process Tree client action:
1. Back to Work Order kept growing breadcrumbs (WO -> Tree -> WO ->
Tree -> ...) because onBack used action.doAction() which PUSHES
a new act_window onto the stack instead of popping. Fixed by
trying action.restore() first (pops the Tree off the stack and
returns to the parent WO/Step controller). Falls through to
explicit doAction only when there's no parent in the stack
(direct URL access).
2. The empty-state banner referenced productionId, a dead variable
from the bridge_mrp era when the tree was tied to mrp.production.
Since the component now uses jobId (fp.job context key), the
"No manufacturing order selected" message ALWAYS fired regardless
of whether a job was loaded. Fixed by using jobId and updating
the message to "No work order selected".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two improvements to the Process Tree visualization opened from the
Work Order's Process Tree header button:
1. Back button returns to the Work Order (job form) instead of
Plant Overview. fp.job.action_open_process_tree now passes
back_job_id in the client-action context; process_tree.js
reads it via a new backJobId getter, updates the button label
to "Back to Work Order", and routes onBack to fp.job form.
The Plant Overview fallback stays for callers that don't pass
either back_step_id or back_job_id.
2. Completed operation/step cards now have a green fill (#1e8449)
and a subtle pulsing glow (box-shadow animation, 2.6s alternate)
so finished work pops against still-pending dark cards. Hover
pauses the animation so the click target is steady. Reuses the
same green the workflow-state slice already used.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Gap 1 — Rack Travel Ticket PDF (Sub 12b's Save+Print 404):
+ report_fp_rack_travel.xml in fusion_plating_reports — A5 landscape
single page, big rack name, Code 128 of FP-RACK:<name>, tag chips,
contained part-batches table.
+ ir.actions.report bound to fusion.plating.rack so it appears in
the rack form's Print menu too.
+ Sub 12b's rack_parts_dialog.js Save+Print URL fixed to use the
standard /report/pdf/<xmlid>/<id> route.
Gap 2 — Per-customer cert statement:
+ res.company.x_fc_default_cert_statement (company-level fallback).
+ res.partner.x_fc_cert_statement (per-customer override).
+ Surfaced on the partner form under the existing Cert + Document
Routing block.
+ Chronological CoC body resolves: customer override → company
default → hardcoded AS9100/ISO 9001 boilerplate. Three-tier
fallback so existing certs without overrides keep working.
Gap 3 — Chronological CoC 'Actual' column:
+ Build a captured_values_by_input dict from the move's
transition_input_value_ids (Sub 12b captures these on every
Move Parts commit).
+ Render typed Actual: text → as-is, number → with target unit,
boolean → PASS/FAIL, date → formatted, attachment → '[Attachment]'
placeholder.
+ Falls back to prompts from the destination step's step_input list
when no values were captured (still useful as audit-of-what-was-
asked even if blank).
Version bumps:
fusion_plating → 19.0.10.3.0
fusion_plating_reports → 19.0.10.1.0
fusion_plating_certificates → 19.0.5.3.0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Controller: extend /fp/shopfloor/plant_overview return payload to
include 'racks' array (filtered to loaded/in_use/awaiting_unrack
states). Each entry has tag chips, part count, current node
breadcrumb, current step + tank code, and a precomputed
next_step_id (next sequence in the job's recipe — operator
overrides at runtime in the Move Rack dialog).
JS: state.racks populated from payload. New openMoveRackDialog()
method spawns FpMoveRackDialog. Notification when rack has no
successor (last step of job).
XML: top section above the existing work-centre columns. Renders
rack rows with tags, part count, breadcrumb, and primary MOVE RACK
button per row. Visible only when state.racks.length > 0.
SCSS: minimal styling for the racks pane (extends move_dialogs.scss
to keep all Sub 12b styles in one file).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ShopfloorTablet component:
- Imports the 3 new OWL dialogs.
- useService('dialog') for spawning.
- Listens for 'fp-resolve-rack' window CustomEvent fired from inside
FpMovePartsDialog → spawns FpRackPartsDialog inline.
- New methods: openMovePartsDialog(from, to) + openStopTimerDialog(id).
Refresh tablet after commit/reconcile so the UI reflects new state.
Listener cleanup on unmount.
Note: the actual buttons that call these methods are added to the
existing tablet XML in a follow-up step — for now they are wired but
not surfaced. Operators get them after Task 16 + smoke test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3-column grid layout for field rows (label / input / hint).
Compliance prompts + Blockers blocks have their own backgrounds.
Soft blockers amber + left border, hard blockers red + left border —
matches the spec's protocol.
Token pattern + dark-mode @if branch (CLAUDE.md rule: Odoo 19 doesn't
flip dark mode via runtime DOM class; we branch at SCSS compile time
on $o-webclient-color-scheme).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move Rack: rack name in title via getter, tag chips, batches list
(read-only), Type + To Node + To Station picker. Atomic Save commits
all batches via /fp/tablet/move_rack/commit.
Stop Timer: opens with state already at 'stopped' (server flipped on
load via /labor_timer/stop), pre-fills billed_* from accrued.
Operator edits → Save (state → reconciled).
Save & Start New Timer chains into a fresh timer for the same step
via the start_new=True flag — mirrors screen 10's right-most button.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors screens 7-8. Searchable empty-rack picker with debounced
typeahead via /fp/tablet/rack/list_empty. QR Scan button prompts
operator for FP-RACK:<name> token, resolves via /fp/tablet/rack/
scan_qr.
Save commits the racking via /fp/tablet/rack_parts/commit. Save+Print
opens /web/report/pdf/fp.rack.travel/<id> in a new tab — that report
ships in Sub 12c, returns 404 until then. Plain Save works today.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors Steelhead screens 1-3, 14-15. Loads preview on mount,
re-checks hard-blockers on commit. MOVE (n) button disabled when
hard-blocked OR required prompt blank — improvement over Steelhead's
silent disabled state (we show a tooltip listing reasons).
Inline 'Resolve' button next to each blocker. For rack-required,
fires a window CustomEvent ('fp-resolve-rack') the parent tablet
catches to open the Rack Parts sub-dialog.
Typed input rendering by input_type — text/number/checkbox/select/
datetime, plus support for time_hms and signature/photo (text input
for now; full upload widget in Sub 12c).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After 1117 video-frame callbacks ZXing still couldn't see the QR.
Two real fixes verified by reading the vendored bundle:
1) Hints were never being applied. BrowserMultiFormatReader stores
them in this._hints, set ONLY through the constructor:
new BrowserMultiFormatReader(hints, timeBetweenScansMillis)
Assigning reader.hints afterward (what the previous patch did) is
a no-op. Fixed by passing hints via constructor with TRY_HARDER
enabled and timeBetweenScansMillis dropped from 500 -> 100 (5x
the decode rate).
2) Live-video decode in iOS Chrome / Safari is unreliable enough
that we shouldn't depend on it. Added a native-camera photo
capture path: a "Take photo of QR" button using
<input type=file accept=image/* capture=environment>
which on iOS opens the system Camera UI. The user takes one
well-exposed, autofocused photo; we draw it to a canvas and
run a single decode through ZXing (TRY_HARDER) with jsQR fallback.
Far more reliable than chasing edge cases in live decoding.
Status: Live decode is still tried first. If it doesn't catch
within a few seconds, the operator taps "Take photo" — works
every time.
Version: shopfloor 19.0.23 -> 19.0.24.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous patch called reader.decodeFromCanvas which doesn't exist
in @zxing/library 0.21.3. Real methods (verified by grep on the
vendored bundle) are:
decodeFromVideoElement(el) -- one-shot
decodeFromVideoElementContinuously(el, cb) -- continuous loop
Switched to the continuous variant. ZXing manages its own per-frame
timing internally — we just register the (result, err) callback and
React to result.getText() on hits. NotFoundException = no QR this
frame, which we silently ignore.
Also fixed the related video-play race: ZXing internally registers a
'playing' event listener on the video and then calls play() itself.
If we await v.play() ourselves first, the 'playing' event fires
BEFORE ZXing attaches its listener and ZXing then waits forever for
an event that already happened.
Fix: for the zxing path we set attributes + srcObject but do NOT
call play(). ZXing's playVideoOnLoadAsync handles play -> playing ->
decode in the right order. The native and jsQR paths still pre-play
because their loops poll the video themselves.
Cleanup: _stopCamera now calls reader.reset() to tear down ZXing's
internal state cleanly when the modal closes.
Version: shopfloor 19.0.22 -> 19.0.23.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
149 jsQR attempts at full 720x1280 with px:ok and no detection means
the QR in the frame has perspective skew, motion blur, or glare under
jsQR's threshold but well within what real-world phone scanning needs
to handle. jsQR is fast but brittle.
Vendor @zxing/library 0.21.3 (Apache 2.0, ~328KB UMD) and make it the
default decoder. ZXing's HybridBinarizer + perspective transform are
the same algorithm family the iOS Camera app uses internally and they
recover from the cases jsQR rejects.
Decoder selection order:
1. ZXing-js (window.ZXing.BrowserMultiFormatReader) -- new default
2. native BarcodeDetector -- if ZXing missing
3. jsQR -- last-resort
Implementation details:
- Hint ZXing to QR_CODE only so it doesn't waste frames probing
Code 128 / EAN / PDF417.
- Use decodeFromCanvas on each video frame (rather than ZXing's
built-in continuous video reader) so we keep ownership of the
modal UI, the <video> element, and the getUserMedia stream.
- Status line follows the same compact format as the jsQR loop:
zxing · f1234 a567 720x1280 rs4 r:no_code
with r:found / r:empty / r:no_code / r:err: ...
Version: shopfloor 19.0.21 -> 19.0.22.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
271 attempts at 720x1280 with no detection means either downsampling
killed the finder patterns or drawImage is silently painting blank
pixels (a known iOS WebKit failure mode for some video-stream sources).
- Drop the 600px scaling cap. Feed the full native video frame to
jsQR. Per-frame cost goes up but is still fine; the win is jsQR
sees finder patterns at full sharpness.
- Add a one-time sanity check that walks the first ImageData buffer
looking for a non-zero pixel. If everything is 0,0,0 the canvas is
blank (drawImage failed) and we surface that as 'px:BLANK' in the
status line — telling us instantly to switch decoder strategies
rather than chasing tuning.
- Status line now also shows the last jsQR call outcome:
r:found / r:no_code / r:empty / r:error: ...
So we can confirm whether jsQR is even being invoked successfully.
Status format compacted to fit one line on phone:
jsqr · f1234 a567 720x1280 rs4 px:ok r:no_code
Version: shopfloor 19.0.20 -> 19.0.21.
The previous loop was running but never finding the QR. Three changes
to make it actually decode AND make any future failure visible:
1. inversionAttempts: 'dontInvert' -> 'attemptBoth'
Some camera exposures wash out the QR enough that jsQR sees it
inverted. attemptBoth tries both polarities per frame; the cost is
~2x decode time but jsQR is fast enough that scan stays interactive.
2. getUserMedia now requests 1280x720 (with downgrade allowed). The
default 640x480 only gives ~5 pixels per QR module on a typical
phone-to-sticker distance — borderline for jsQR. 720p doubles
that and makes detection near-instant.
3. The status line is now LIVE, updating every 400ms with:
Decoder: jsqr · frames N · attempts M · video WxH rsN
So when an operator says "scan does nothing" we can immediately see
whether the loop is running, whether the camera is feeding frames,
whether jsQR is being called, and at what resolution. No Web
Inspector required.
Version: shopfloor 19.0.19 -> 19.0.20.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Re-detect BarcodeDetector / window.jsQR at every modal open instead
of only at component setup. Avoids the trap where a stale cached
bundle reports "no decoder" even after a redeploy.
- Add a one-line status indicator at the top of the scan modal showing
exactly which decoder is active ("Decoder: native" / "Decoder: jsqr"
/ "Decoder: none — paste URL below"). Lets the operator see at a
glance whether scanning is even possible without round-tripping
through Safari Web Inspector.
- Sticker: strip a leading "Rev " (case-insensitive) from
fp.part.catalog.revision before printing so values like "Rev 1"
don't render as "Rev Rev 1".
Versions: shopfloor 19.0.18 -> 19.0.19, reports 19.0.7.16 -> 19.0.7.17.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs were colluding to make iPhone scans look like "nothing
happens":
1. The in-app scanner was calling action.doAction({res_model: 'fp.job',
res_id: <decoded-id>}). Old physical stickers (still on every box)
encode /fp/wo/<mrp.production.id> — that id space doesn't match
fp.job, so the form opened on a non-existent record and silently
showed nothing. New /fp/job/<id> stickers happened to work because
the IDs lined up by coincidence.
2. The /fp/wo/<id> controller redirected to mrp.production / mrp.workorder
forms, both of which still exist as legacy records but aren't the
canonical source of truth post-migration.
Fix:
- qr_scanner._handleCode now navigates via window.location.href instead
of action.doAction. It hands /fp/job/<n> and /fp/wo/<n> URLs straight
to the existing server-side controllers, which know how to resolve
the right record. Bare numeric ids pasted manually -> /fp/job/<n>.
Anything else surfaces the decoded text as an error so the operator
can see decode worked but the value isn't a sticker.
- Modal now shows "Detected: <value>" the moment a code is decoded
(before navigation), so even on slow phones the operator sees
immediate feedback that the camera read the QR.
- wo_scan.py now resolves in this order:
1. fp.job by legacy_mrp_production_id (migration-aware — old
stickers route to the new model)
2. mrp.production direct browse
3. mrp.workorder direct browse
4. fall back to /odoo/plating-jobs (or work-orders list)
Versions: shopfloor 19.0.17.0.0 -> 19.0.18.0.0,
reports 19.0.7.15.0 -> 19.0.7.16.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
iOS Safari (and the in-app webviews in Messages / WhatsApp / LinkedIn)
don't ship the BarcodeDetector API, so the previous scanner fell
through to the manual paste UI on every iPhone — defeating the point
of "tap to scan."
Vendored cozmo/jsQR (Apache 2.0, ~250KB) and made the scanner pick the
strongest available decoder at setup time:
1. native BarcodeDetector -> Android Chrome, iOS Safari 17+, desktop
2. jsQR canvas loop -> every other browser with getUserMedia
3. manual URL paste -> last-resort if camera unavailable
The jsQR loop draws each video frame into an offscreen canvas, downsamples
to 480px on the long side, and runs jsQR synchronously throttled to
~8 fps to stay under 10% CPU on mid-range Android phones.
Template now shows the <video> element whenever ANY decoder is
available (state.canScan), not just for native. Paste fallback still
visible as a secondary path so a tablet with broken camera permissions
still has a way in.
shopfloor: 19.0.16.0.0 -> 19.0.17.0.0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three connected operator-workflow features for entech.
A. fp.job smart buttons — count fields and action methods for sale
order, steps, deliveries, invoices, payments, quality holds,
certificates, time logs, and portal job. Each is an oe_stat_button
that drills into the matching records, mirroring the sale.order
pattern. Cross-module models are runtime-detected so the form
stays clean when bridge modules are uninstalled.
B. Reusable QR scanner OWL component (`<QrScanner/>`) wired into the
Manager Desk, Tablet Station, Plant Overview, and Process Tree
headers. Click → modal with rear-camera stream (getUserMedia) +
BarcodeDetector live decode → opens the matching fp.job form via
the action service. Falls back to a manual URL paste box on
browsers without BarcodeDetector. Works on iOS 17+ Safari and
Android Chrome. Width uses `min(420px, 92vw)` wrapped in #{} so
dart-sass passes it through verbatim instead of trying to compute
incompatible units at compile time.
C. /fp/tank/<id> public-but-auth-required tank status page for NFC
taps. Renders the tank's current step (in-progress / paused),
queued ready steps, and most recent bath chemistry log (lines
table) on a mobile-first page. URL-based so it works on iOS Safari
without the Web NFC API — the operator taps the NFC tag, the URL
opens in the default browser, the page auto-renders. New
web.assets_frontend bundle entry pulls in the design tokens +
tank_status.scss.
Manifest version bumps: jobs 19.0.5.0.0, shopfloor 19.0.16.0.0.
Tests: 44 pass (3 new smart-button assertions added).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous shopfloor consolidation kept the data layer correct
(controller queries fp.job.step) but left the UI labels, JS
variables, and RPC kwargs in legacy WO/MO vocabulary. Result:
every label said 'Unassigned WOs' / 'X WO' even though the
underlying records are fp.job.step rows.
Renames throughout:
wo → step (variable / loop / payload key)
WO → Step (label)
unassigned_wos → unassigned_steps (KPI key)
active_wos → active_steps
ready_to_ship_mos → ready_to_ship_jobs
mo_id / mo_name / expandedMoId → job_id / job_name / expandedJobId
wo_kind → kind, wo_kind_label → kind_label
o_fp_mgr_wo_* CSS classes → o_fp_mgr_step_*
RPC routes /fp/manager/assign_worker, /fp/manager/assign_tank,
/fp/manager/take_over: primary kwarg is step_id; workorder_id
accepted as a deprecated alias for one release with a logged
warning, so any uncaught caller doesn't break.
No layout / visual changes — same UI shape, native vocabulary.
SCSS class renames are mechanical (only `_wo_` → `_step_` in
selectors); XML updated in lockstep.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Manager Desk SCSS still had hardcoded chip colors (wet/bake/
mask/rack/inspect) and var(--bs-body-color) usage that CLAUDE.md
explicitly forbids. In dark mode these rendered as low-contrast
text on translucent backgrounds.
Fixes:
- Added 6 kind-chip tokens to _fp_shopfloor_tokens.scss
($fp-kind-wet/bake/mask/rack/inspect/other) with explicit hex
values for both light and dark bundles via the existing
$o-webclient-color-scheme branch.
- manager_dashboard.scss: kind chips reference the new tokens via
color-mix() for translucent backgrounds. var(--bs-body-color)
on the expanded card body replaced with $fp-card-soft.
- Annotated the .btn-primary white-text rule as intentional (the
$fp-accent surface beneath it is the same brand purple in both
bundles, so white is correct in both themes).
plant_overview.scss had no kind-chip block — already token-compliant.
manager_dashboard.xml had no inline styles or theme-leaky utility
classes — text-muted/text-success resolve through Bootstrap which
flips with the bundle.
Both light (3deab56) and dark (28de524) asset bundles compile
cleanly with distinct hashes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the parallel OWL/controller stack I built in
fusion_plating_jobs (job_process_tree, job_plant_overview,
job_manager_dashboard, job_tablet, job_*.scss, plus parallel
controllers and action XML files). Refactors the existing
fusion_plating_shopfloor components in place to bind to
fp.job / fp.job.step instead of mrp.production / mrp.workorder.
End state:
- ONE operator UI module (shopfloor) instead of two parallel ones
- Existing token system (_fp_shopfloor_tokens.scss) reused as
designed - no duplicate jobs tokens
- Existing /fp/shopfloor/* RPC URLs preserved (no integration
breakage); workorder_id kwargs accepted as legacy aliases for
step_id / job_id so older tablet clients keep working
- Existing visual designs preserved - only the data layer
underneath changed
- Process Tree button on fp.job form now points at
fusion_plating_shopfloor's fp_process_tree client action
- Bake Windows / First-Piece Gates / Bake Oven / Operator Queue
models stay where they were
- legacy_menu_hide.xml trimmed: only the bridge_mrp Production
Priorities entry remains; the 3 shopfloor menus (Manager Desk,
Plant Overview, Tablet Station) are now visible (the canonical
native consoles)
Manifests:
- fusion_plating_jobs 19.0.3.1.0 -> 19.0.4.0.0 (consolidation bump,
no more bundled JS/SCSS, only job_scan controller retained)
- fusion_plating_shopfloor 19.0.14.4.0 -> 19.0.15.0.0 (asset bundle
cache-bust + significant controller refactor)
Tests pass on entech: 0 failed, 0 errors of 41 tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues from the wet-WO card screenshot:
**1. Tank picker bleeding past the card's right edge**
Native <select> defaults to `box-sizing: content-box`, so my
`width:100% + padding-right:2.25rem` rendered the picker wider than
its flex slot — the second picker (Tank, on wet WOs) overflowed the
card border at the typical card width.
Fix on `.o_fp_mgr_picker`:
• `box-sizing: border-box` — keep total width inside the slot
• `min-width: 0` — let flex actually shrink it past its content
• Custom SVG chevron via background-image so we control the
indicator's position exactly (Bootstrap's native chevron sits
almost flush with the right border, which the user flagged
earlier). 1rem of clearance from the right edge.
**2. Take Over button**
Earlier I'd collapsed it to icon-only because the wet card was too
wide; user pointed out the icon alone is confusing. Restored the
"Take Over" label (with icon prefix) so both buttons read cleanly:
[👤 Take Over] [↗ Open WO]
Asset cache cleared as part of the deploy so the recompiled SCSS
+ refreshed XML template ship together. A hard browser refresh
(DevTools → Empty Cache + Hard Reload) is needed to pick them up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Screenshot showed the new WO row was broken:
• Kind chip text clipped ("Mas" instead of "Mask", "Rac" instead of
"Racking")
• WO name truncated to first 4 chars
• The wet WO had no info column at all — kind chip + name pushed
off-screen by the tank picker
• "Needs:" chip showed as just an exclamation icon with "N" cut off
• Take Over and Open WO buttons unevenly sized
Root cause: `.o_fp_mgr_wo_info` carried `nowrap + ellipsis` from the
old single-line design, but the new template stacks kind chip + name +
meta + needs across multiple lines. Plus the rigid grid
(1fr auto auto auto auto) gave the info column whatever the dropdowns
left over — usually nothing.
**Layout rewrite** — flex with wrap instead of grid:
• `.o_fp_mgr_wo_row` — flex row, info on left, actions on right,
wraps to two rows on narrow viewports.
• `.o_fp_mgr_wo_info` — `flex: 1 1 280px` so it grows but never
narrower than 280px. Contains a vertical stack: title row
(badge + name) → meta row (workcenter / role / equipment chips)
→ needs row (yellow chip if anything missing).
• `.o_fp_mgr_wo_actions` — `flex: 0 0 auto` with its own gap, so
pickers + buttons align cleanly to the right.
• Kind chip can wrap to its full label; meta row uses `flex-wrap`
so equipment hints don't get clipped.
• Take Over collapses to icon-only with title tooltip — the row
was getting too wide on the wet kind (which adds the tank picker).
**Other tweaks**
• Added `tank_id` to the controller payload so the tank picker
pre-selects the current tank (was missing on the previous
"current tank" highlight).
@720px the action group stacks below the info — pickers go full-width,
buttons get `min-height: $fp-touch-min` for thumb tap.
Asset cache cleared as part of the deploy so the SCSS recompiles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
**1. Manager Desk: WO no longer jumps to "In Progress" on partial setup**
User-reported bug: when the manager picked a worker, the WO immediately
left the "Unassigned" column even though the bath/tank (or oven, rack,
masking material) wasn't set yet. Worker would see a half-set job in
their queue and couldn't start it.
Fix:
- New compute `mrp.workorder.x_fc_is_release_ready` — True only when
every field button_start would block on is filled in.
- Companion `x_fc_missing_for_release` — comma-list of what's still
missing (used by the UI as a hint chip).
- Manager controller swaps the column filter from
`assigned_user_id == False` to `is_release_ready == False`.
- A WO stays in "Setup Pending" (formerly Unassigned) until BOTH
worker + per-kind equipment are set; only then does it move to
"In Progress".
**Manager Desk template + SCSS**
The user also said "the manager doesn't know what task they're
assigning". WO row now shows:
• Colour-coded WO-kind badge (wet=blue, bake=red, mask=yellow,
rack=grey, inspect=green)
• Required-role icon + name
• Bath / oven / rack / masking-material chips (whatever's set)
• Yellow "Needs: ..." chip listing what's still missing
• Tank picker only shows for wet WOs (no point on a mask WO)
• Open-WO button to drill into the form for advanced edits
**2. Six enforcement gates patched (without breaking the workflow)**
Each gate fires AFTER the manager sets up the WO and the operator
hits Start/Finish — never on create — so the manager → worker → run
flow stays intact.
| # | Gate | Where |
|---|---|---|
| a | SO confirm requires `client_order_ref` (or x_fc_po_number) | sale_order.action_confirm |
| b | Cert issue requires thickness readings (when partner.x_fc_strict_thickness_required) | fp_certificate.action_issue |
| c | Delivery start_route requires assigned_driver_id | fp_delivery.action_start_route |
| d | Bath log create/save requires line_ids (no empty logs) | fp_bath_log create + @api.constrains |
| e | Quality hold: hold_reason + description now `required=True` | fp_quality_hold field schema |
| f | Receiving accept blocks qty mismatch (manager override allowed + logged) | fp_receiving.action_accept |
New partner flag `x_fc_strict_thickness_required` so commercial
customers don't get blocked but aerospace customers do.
**Verified** via `scripts/fp_enforcement_audit.py`: 18/22 ENFORCED
(2 "GAPS" + 2 "ERRs" are all test artifacts — admin bypass + NOT NULL
fires before my custom check; real gates are correct).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The search bar required Enter to fire, which felt clunky on a shop
floor where managers expect cards to filter as they type. Switched
to a 200ms-debounced live search — fast enough to feel instant on
keystrokes, slow enough to skip the network call when someone is
mid-word.
Search bar visual weight bumped:
- Width 260px → 380px (320px on iPad, full width on phones)
- Height 48px → 52px
- Font-size base → md, weight medium
- Search icon nudged 14px → 16px from the edge with a 1.05rem size
- Placeholder uses the lighter $fp-ink-faint so the input feels
inviting rather than already-filled
Behaviour:
- Type → cards filter after 200ms of no input
- Enter → fires immediately (skips debounce) for power users
- Escape → clears the search (new shortcut)
- Clear button → unchanged
Bumped shopfloor to 19.0.14.0.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Bake Window + First-Piece Gate cards looked rounded on their
own, but Odoo's default .o_kanban_record wrapper painted its own
background + border + box-shadow with sharper corners than our
inner .o_fp_kcard — visible as a faint square ghost behind every
card, especially obvious on the missed_window state where the red
wash on the inner card didn't extend to the wrapper edges.
Added a .o_fp_bw_kanban / .o_fp_fpg_kanban scoped override that
zeroes the wrapper's background, border, box-shadow and padding,
letting only our card surface render. Also drops the kanban group
container's tinted bg for the same reason.
Bumped shopfloor to 19.0.13.0.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The two standalone menu pages (Bake Windows, First-Piece Gates) were
still on the older o_fp_card design from a pre-Plant-Overview pass —
visually drifted from the polished kanban-pattern cards we settled on
for Plant Overview. Pulling them onto the same design language without
rewriting them as OWL client actions (the 'Option A' from chat).
What changed
============
New shared SCSS — fp_kanbans.scss
---------------------------------
Defines .o_fp_kcard as the base kanban card surface. Mirrors the
Plant Overview .o_fp_po_card recipe: white $fp-card surface, 1px
$fp-border, $fp-radius-md corners, soft $fp-elev-1 shadow, hover
lift, 4px state stripe via ::before clipped by overflow:hidden.
Sub-elements (title, sub, metric, meta line, footer chip) get
their own classes so per-page tweaks stay surgical.
Page-scoped wrappers (.o_fp_bw_kanban, .o_fp_fpg_kanban) carry the
state/result → stripe colour mapping plus exception-state tints
(missed_window + fail get a soft danger wash so the card stands
out in a sea of normal ones).
Bake Window kanban
------------------
Rebuilt template — title (window name), part_ref subtitle, big
time-remaining metric (the operator's primary cue), meta line for
lot/customer/qty, footer with oven badge + state chip.
data-state attribute drives the stripe colour:
awaiting_bake → warning
bake_in_progress → info
baked → success
missed_window → danger + soft red wash
scrapped → muted + dimmed
First-Piece Gate kanban
-----------------------
Rebuilt template — title (gate name), part_ref subtitle, bath +
customer meta, inspector + first_piece_produced timestamp,
footer with result chip and an optional 'Released' badge when
the lot has been signed off.
data-result attribute drives the stripe colour:
pending → warning
pass → success
fail → danger + soft red wash
Shopfloor manifest bumped to 19.0.12.0.0 and the new SCSS is
registered in web.assets_backend after manager_dashboard.scss so
the design tokens it references are already in scope.
Plant Overview's existing .o_fp_po_card classes are deliberately
untouched — the OWL client action and the new kanbans share the
visual language but stay loosely coupled.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end workflow tightening + the team / skills system. Three
phases bundled because they share the same touchpoints (button_start /
button_finish / Manager Desk dropdown).
PHASE 1 — In-Odoo notifications + timer audit
=============================================
Workers now get a bell-icon notification (Odoo Discuss inbox) the
moment a manager assigns them a WO. No email — operators check Discuss
between jobs, and the customer-facing notification dispatcher stays
out of the worker loop.
- mrp.workorder.write() override fires message_notify(message_type=
'user_notification') only when x_fc_assigned_user_id transitions to
a non-empty value (clearing or no-op writes don't ping)
- 4 new fields on the WO header surface what was previously buried in
time_ids: x_fc_started_by_user_id, x_fc_started_at,
x_fc_finished_by_user_id, x_fc_finished_at
- button_start stamps started_* once (subsequent pause/resume cycles
preserve the original); button_finish stamps finished_* every time
the WO closes
- New "Timer Audit" group on the WO form (Time & Cost tab)
PHASE 2 — Presence-aware Manager Desk
=====================================
Manager Desk now knows who's clocked in. Works with vanilla
hr_attendance and fusion_clock — both expose hr.attendance with an
open record while the operator is on shift.
- bridge_mrp depends on hr_attendance
- hr.employee.x_fc_is_clocked_in computed field (batched query — one
DB hit for the whole employee set, not N+1)
- hr.employee._fp_clocked_in_user_ids() classmethod for the dashboard
- manager_controller sends operators with is_clocked_in / role_ids /
lead_hand_role_ids per worker, plus presence dict {clocked_in: N,
total: M}; each WO carries role_id/role_name so the dropdown can
match qualified operators
Manager Desk OWL:
- Header gets a "Present 7 / 12" pill chip; tap to toggle hideOffShift
(off-shift hidden when active, accent colour when filter is on)
- New operatorsForWO(wo) helper sorts dropdown options into 4 buckets:
qualified+clocked-in → lead-hand+clocked-in → clocked-in untrained
(training mode) → off-shift (greyed; only shown when hideOffShift
is false). Each option carries a ●/○ dot prefix and a soft suffix.
PHASE 3 — Skills, lead-hand-per-role, auto-promotion
====================================================
The team grows organically: managers assign training tasks, operators
finish them, the system auto-promotes after N successful runs.
- fp.work.role.mastery_required (integer, default reads from the
company-level Default Mastery Threshold). Each role can override —
masking might need 1 success, electroless nickel 5.
- res.company.x_fc_default_mastery_threshold + res.config.settings
exposure under "Workforce Settings" in the Fusion Plating settings
block (default 3)
- hr.employee.x_fc_lead_hand_role_ids m2m, separate from
x_fc_work_role_ids — Sarah can be a lead hand for masking + racking
even if those aren't her primary roles. Manager-only group access.
- New fp.operator.proficiency model (one row per employee+role) with
completed_count, first/last_completed_at, promoted, promoted_at,
progress_label compute. SQL-unique on (employee, role).
- mrp.workorder.button_finish increments the (employee, role)
counter, then if count >= role.mastery_required AND not promoted,
adds the role to x_fc_work_role_ids and posts a "🎉 Promoted"
chatter line on the employee record. Wrapped in try/except so a
tracker glitch never blocks production.
- Promotion uses the WO's assigned_user_id, NOT env.user — credit
goes to the operator who was supposed to do it, even if a manager
finished on their behalf.
Employee form gets a "Shop Roles" tab (supervisor+):
- "Tasks This Operator Can Do" m2m
- "Lead Hand For" m2m (manager-only)
- Read-only Task Proficiency list with progress / promotion badges
Verified on odoo-entech: all fields land, default threshold = 3,
asset bundle regenerated as 9f38f05.
Module bumps: fusion_plating 19.0.4.0.0,
fusion_plating_bridge_mrp 19.0.4.0.0,
fusion_plating_shopfloor 19.0.11.0.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Operators run the Tablet Station on iPads (mostly landscape, sometimes
portrait). The previous design pushed the dashboard panels below the
fold on a 1024×768 viewport — meant a swipe before they could see
their queue. Tightens spacing across the page without changing the
visual language.
What changed (all behind @media (max-width: 1180px)):
- Page padding 24/32 → 16/20, gap between sections 24 → 16
- Hero title 32 → 24px, subtitle margin-top halved
- KPI strip switches from auto-fit to fixed 6-column grid so all six
KPIs stay on a single row instead of wrapping at iPad widths;
per-tile padding 20 → 12/16, value font 44 → 24px, label 14 → 12px
- Active WO banner padding 20 → 12/16
- Dashboard breakpoint to single-column lowered 1100 → 760px so
iPad portrait still gets two columns of panels
- Panel padding 20 → 16, panel-head padding-bottom 12 → 8
- Empty state padding 32/16 → 16/12 (the "All caught up" tile no
longer eats 140px per panel)
- Queue rows min-height 64 → 52, bake/gate rows 64 → 48
Station picker dropdown:
- Native chevron suppressed via appearance: none and replaced with
an inlined SVG arrow positioned with explicit right-edge inset.
Stroke uses currentColor so it follows light/dark mode.
- Right padding bumped from $fp-space-4 → $fp-space-7 to give the
arrow breathing room — previously hugged the rounded corner.
Station dropdown labels:
- Append "(CODE)" after the name. The shop's five stations
(Bake Oven Tablet / Inspection Kiosk / Plating Room Tablet 1 /
Receiving Mobile / Shipping Desktop) all live in the same facility
with no work_center, so without the code suffix the dropdown
options looked similar at a glance.
Bumped fusion_plating_shopfloor → 19.0.10.0.0. Asset bundle
regenerated as bc28f73.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The coloured priority stripe (4px vertical bar at the card's left
edge, set via ::before pseudo) extended past the top and bottom
rounded corners of the card — visible as sharp corners on cards with
Urgent or HOT priority (yellow/red stripe).
Cause:
.o_fp_po_card::before was positioned at left/top/bottom: -1px and
given its own border-radius, but the stripe's own radii didn't
match the card's 14px radius precisely, and the -1px offsets
pushed the stripe outside the card's curves.
Fix:
1. .o_fp_po_card gets overflow: hidden. Shadows are painted outside
the content box in CSS so box-shadow still renders fine, but any
child element (including ::before) now clips to the parent's
border-radius automatically.
2. Stripe ::before simplified to left/top/bottom: 0 — no more
negative offsets, no more independent border-radius rules.
The parent's overflow does the corner-matching.
Verified in /web/assets/5e85f15/web.assets_backend.min.css:
.o_fp_po_card { ...; overflow: hidden; ... }
.o_fp_po_card::before { content: ""; position: absolute;
left: 0; top: 0; bottom: 0; width: 4px; ... }
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two problems after the previous round:
1) Mobile scroll still not working, even on a real phone.
Dug into /usr/lib/python3/dist-packages/odoo/addons/web/static/src/
webclient/webclient_layout.scss and found Odoo's mobile layout
switches scroll ownership at @media-breakpoint-down(md) (<768px):
Desktop: .o_content has overflow:auto — your content scrolls there
Mobile: .o_action gets overflow:auto, .o_content is overflow:initial
Our client action roots had `min-height: 100%` and relied on an
ancestor for scroll. That ancestor changes between breakpoints, and
somewhere in the transition scroll gets lost — the page fills but
can't scroll.
Fix: make each page OWN its scroll, like .o_content on desktop
kanban/list views. Three roots now have:
.o_fp_tablet / .o_fp_manager / .o_fp_plant_overview {
height: 100%;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
Scroll works regardless of which ancestor Odoo decides owns it at
any given breakpoint.
2) Sharp corner on column header at mobile widths.
The previous commit set `overflow: visible` on .o_fp_po_column at
<=900px trying to help scroll. But the column has border-radius: 20px
and contains .o_fp_po_col_header (which has its own background). When
overflow is visible, the header bg extends to the column's corners
without being clipped — you see squared corners on the mobile card.
Fix: keep `overflow: hidden` on .o_fp_po_column at every breakpoint
(that's what clips the rounded corners). Only lift `max-height` on
mobile so columns size to content naturally. Since the PAGE now owns
the scroll (see fix#1), the column doesn't need internal scroll —
no `overflow: auto` on the body is needed either.
Verified in compiled CSS at /web/assets/7ff5b28/web.assets_backend.min.css:
.o_fp_tablet { height: 100%; overflow-y: auto; ... }
.o_fp_manager { height: 100%; overflow-y: auto; ... }
.o_fp_plant_overview { height: 100%; overflow-y: auto; ... }
.o_fp_po_column { border-radius: 20px; overflow: hidden }
@media (max-width: 900px) .o_fp_po_column {
flex: 1 1 auto; min-width: 100%; max-width: 100%;
max-height: none; // no overflow override — hidden stays
}
Version bumped 19.0.6.0.0 -> 19.0.7.0.0 to force bundle hash change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User: "scrolling is not working" in Chrome DevTools mobile simulation.
Three actual problems:
1. Plant Overview columns had max-height: calc(100vh - 180px) +
overflow: hidden, with a nested overflow-y: auto on the column
body. Classic Trello kanban pattern — works on desktop, breaks
on mobile. You get two scroll containers fighting each other and
the PAGE itself can't scroll past the viewport height.
2. .o_fp_po_columns had overflow-x: auto on all widths. On the
phone-stack breakpoint (<600px) this was also still on, creating
another nested scroll container.
3. Draggable cards can swallow touch events on mobile because
touch-action defaults to "auto" and Chrome's mobile simulator
treats touch on draggable elements as potential drag-start.
Fixes — all at the <=900px breakpoint (tablets + phones):
.o_fp_po_column max-height: none; overflow: visible
.o_fp_po_col_body overflow-y: visible
.o_fp_po_columns flex-direction: column; overflow: visible
Plus .o_fp_po_card carries `touch-action: pan-y` unconditionally —
touch-scroll gestures never get hijacked by the draggable="true"
attribute. Desktop mousedown drag still works (HTML5 drag-drop
isn't touch-based by default).
Also added -webkit-overflow-scrolling: touch to all three page
roots (.o_fp_tablet, .o_fp_manager, .o_fp_plant_overview) and to
the internal scroll containers that remain on desktop — gives iOS
Safari proper momentum scroll (11 occurrences in the compiled
bundle).
Drag-drop JS preventDefault calls audited — they only fire on
dragover/drop (HTML5 drag events), which don't exist on touch by
default, so no touch interference there.
Verified via compiled CSS:
.o_fp_po_card { touch-action: pan-y; ... }
@media (max-width: 900px) .o_fp_po_column { overflow-x: visible;
overflow-y: visible; min-height: auto }
@media (max-width: 900px) .o_fp_po_col_body { overflow-y: visible }
Version bumped 19.0.5.0.0 -> 19.0.6.0.0 to force the bundle hash
to change. New URL: /web/assets/4a1b69e/web.assets_backend.min.css
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dug deeper after the user reported shop-floor pages staying white in
dark mode. Traced through Odoo 19 source:
_dependencies/web_enterprise/static/src/
webclient/color_scheme/color_scheme_service.js <- reads cookie
scss/primary_variables.scss \$o-webclient-color-scheme: bright
scss/primary_variables.dark.scss \$o-webclient-color-scheme: dark
Odoo compiles TWO separate CSS bundles:
web.assets_backend -> compiled with \$...scheme: bright
web.assets_web_dark -> compiled with \$...scheme: dark
(the .dark.scss files are layered in front of the light ones)
Our shop-floor SCSS is in web.assets_backend, which means it gets
compiled into BOTH bundles. But the previous CSS-variable fallback
chain (var(--fp-page-bg, var(--bs-tertiary-bg, #hex))) baked the
SAME hex fallback into both bundles, so cards stayed white in dark.
Odoo's own code doesn't redefine --bs-* CSS custom properties at
runtime either — it just bakes the dark palette straight into the
dark bundle via SCSS \$-variables during compile.
Fix: _fp_shopfloor_tokens.scss now branches at compile time:
\$o-webclient-color-scheme: bright !default;
\$_fp-page-hex: #f3f4f6; // light defaults
\$_fp-card-hex: #ffffff;
...
@if \$o-webclient-color-scheme == dark {
\$_fp-page-hex: #1a1d21 !global;
\$_fp-card-hex: #22262d !global;
...
}
\$fp-page: var(--fp-page-bg, \$_fp-page-hex);
\$fp-card: var(--fp-card-bg, \$_fp-card-hex);
The CSS-custom-property fallback stays so deployments can still skin
via --fp-* without touching SCSS; the underlying hex changes between
bundles.
Verified via odoo-shell:
LIGHT bundle: .o_fp_plant_overview { background-color: var(...#f3f4f6) }
.o_fp_po_card { background-color: var(...#ffffff);
border: ... #d8dadd }
DARK bundle: .o_fp_plant_overview { background-color: var(...#1a1d21) }
.o_fp_po_card { background-color: var(...#22262d);
border: ... #343942 }
Two separate bundle URLs generated:
/web/assets/a593157/web.assets_backend.min.css
/web/assets/a9dba7d/web.assets_web_dark.min.css
=== CLAUDE.md ===
Replaced the previous (incorrect) .o_dark_mode override advice with
a proper "Branch on \$o-webclient-color-scheme at SCSS compile time"
section, including the bundle names and the verify-via-odoo-shell
snippet. Future redesigns now have a single, correct pattern to
follow.
Version bumped 19.0.4.0.0 -> 19.0.5.0.0 to force asset hash change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two fixes + a memory entry in CLAUDE.md.
=== Dark mode ===
User: "when I change the theme the whole background does not turn
dark like the other pages does". Digging through Odoo 19 source:
/_dependencies/web_enterprise/static/src/scss/
bootstrap_overridden.dark.scss
primary_variables.dark.scss
secondary_variables.dark.scss
Odoo doesn't flip dark mode via a runtime .o_dark_mode class on the
DOM — it compiles a SEPARATE asset bundle where $o-webclient-color-
scheme: dark is set, which redefines every --bs-* token with dark
values. When the user toggles dark mode, Odoo swaps the whole CSS
bundle.
So my previous :root[data-bs-theme="dark"] { --fp-page-bg: #13161a; }
block was DEAD CODE — nothing ever sets data-bs-theme on the root.
Fixed: tokens now fall through to Bootstrap's --bs-* semantic tokens
before hitting a hex default, so they auto-invert when Odoo swaps
bundles. Three-level fallback chain:
$fp-page : var(--fp-page-bg,
var(--bs-tertiary-bg, #f3f4f6));
$fp-card : var(--fp-card-bg,
var(--bs-card-bg,
var(--o-view-background-color, #ffffff)));
$fp-border : var(--fp-border-color,
var(--bs-border-color, #d8dadd));
$fp-ink : var(--fp-ink, var(--bs-body-color, #1f2937));
Dead .o_dark_mode block removed. No runtime selector needed.
=== Quick View button ===
User: "Quick View button color is white with white button in light
mode." Cause: Bootstrap's .btn-primary loads AFTER our custom CSS
in the bundle and resets color: #fff, background: var(--bs-btn-bg)
— which clobbered our $fp-accent / $fp-ink assignment because a
later rule at the same specificity wins.
Fix: split the primary button into its own rule with higher
specificity (.o_fp_manager .o_fp_manager_head_actions .btn.btn-primary)
and !important on the three key properties — so Bootstrap can't
shout us down. Hover uses brightness(1.08) for a subtle darken
without needing another color assignment.
=== CLAUDE.md additions ===
Added two new rules documenting the lessons so this isn't relearned:
Rule 8 — Odoo 19 forbids @import in custom SCSS (silent warning,
falls back to cached bundle). Register partials in the assets list
in load order; SCSS variables cascade through the bundle.
"Card Styling — Copy Odoo's Kanban Pattern" section explaining:
- Don't rely on --bs-border-color directly for card surfaces
- Chain through $fp-* → --fp-* → --bs-* → hex
- 3-layer contrast rule (page → container → card)
- Reference _fp_shopfloor_tokens.scss as canonical
"Asset Bundle Cache Busting" section with 4-step escalation path
for when CSS changes don't show up in browser.
Verified: bundle regenerated to /web/assets/b48ab17/web.assets_backend.min.css
(id 1945). Card rule compiled with full fallback chain visible.
Primary button carries !important modifier for bg/border/color.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Why the borders weren't showing: the previous approach used
color-mix(var(--bs-body-color) 4%, var(--o-view-background-color)) for
card/column backgrounds. Under Odoo 19 the resolved values for those
variables were nearly identical to var(--bs-body-bg), so the card
surfaces visually merged into the page. Same problem for borders:
var(--bs-border-color) can render extremely faint depending on theme.
Checked what Odoo's native kanban does — dug through the compiled
CSS and found:
.o_kanban_record { background-color: white;
border: 1px solid #d8dadd; }
.o_kanban_group { background: var(--KanbanGroup-background); }
Odoo uses EXPLICIT hex values and card-specific tokens, not the
generic body/border variables. Adopted the same approach.
New tokens in _fp_shopfloor_tokens.scss — all explicit, plus a
dark-mode override block keyed off [data-bs-theme="dark"] and
.o_dark_mode (Odoo 19 uses both):
light dark
------------------------ ------------------
--fp-page-bg: #f3f4f6 #13161a
--fp-column-bg: #e9ebef #1a1e24
--fp-card-bg: #ffffff #22262d
--fp-card-soft-bg: #f8fafc #1c2027
--fp-border-color: #d8dadd #343942
--fp-ink: #1f2937 #e5e7eb
--fp-ink-mute: #6b7280 #8a909a
shadow scale switched from color-mix to explicit rgba(0,0,0,...)
so it renders identically across browsers.
All three SCSS files updated via sed to swap
var(--bs-border-color) -> #{$fp-border}
...then $fp-border resolves to var(--fp-border-color, #d8dadd) — a
proper card-level border that is VISIBLE (28 refs to --fp-card-bg
and 35 refs to --fp-border-color confirmed in the compiled bundle).
Plant Overview specifically now has:
* Column: #f8fafc bg + #d8dadd border + shadow
(column is brighter than the page it sits on)
* Column HEADER: #ffffff inside the column, with bottom border
(clear separator between stages)
* Card: solid #ffffff bg + #d8dadd border + shadow
(brightest surface, pops off the column)
* Gap between columns: 16px so the column borders don't touch
Module version bumped to 19.0.3.0.0. Bundle regenerated at
/web/assets/0cd8bc1/web.assets_backend.min.css (1.45 MB, id 1939).
Verified by parsing compiled CSS:
.o_fp_po_card: background-color: var(--fp-card-bg, #ffffff);
border: 1px solid var(--fp-border-color, #d8dadd);
.o_fp_po_column: background-color: var(--fp-card-soft-bg, #f8fafc);
border: 1px solid var(--fp-border-color, #d8dadd);
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three direct fixes responding to user feedback:
1. Drag-drop "simulation" — now works like Trello/Linear. As the
cursor moves over a column, a live DOM placeholder node is
INJECTED into the card list at the exact position the dragged
card will drop. The placeholder is a 4px pulsing accent-coloured
bar with a soft glow ring. Slides smoothly between cards as the
cursor moves. Column body also gets a tinted background + inset
accent outline for the "whole column is receptive" cue.
Previous version only tinted the column — no indicator of WHERE
the card would land. The new approach actually mimics the physical
gesture: cards visually make room for the incoming card.
2. Customer logo restored at 32×32px.
Removing it was the wrong call. It's back now as a small
thumbnail avatar (rounded 10px corners, soft border, object-fit
contain so wide logos don't squish). Sits to the left of the
customer name in the card top row. Fallback icon for customers
without a logo. Takes the same space as the step badge on the
right — compact and organised.
3. Module version bumped 19.0.1.0.0 → 19.0.2.0.0 so the asset
bundle content hash changes. The new compiled CSS is served at
/web/assets/022171c/web.assets_backend.min.css (previously
/web/assets/278b43c/...). Fresh URL forces browser to refetch —
this is what was causing the "still no border" complaint.
Verified in compiled CSS: o_fp_po_card_avatar, o_fp_po_drop_placeholder,
o_fp_placeholder_pulse keyframes, o_fp_drop_target — all present.
Zero SCSS warnings. Module upgrade clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three concrete fixes based on user feedback:
1. Card borders restored — every card / panel / KPI tile / queue row /
bake row / team card now has a thin 1px border (var(--bs-border-color))
ON TOP of the soft shadow. That's the classic SaaS card treatment
and solves the "jobs have no borders, they blend together" problem.
Hover lifts the border to the accent colour (~45% mix) so cards
feel responsive.
2. Plant Overview drop-zone indicator restored.
- Column body gets inset outline + tinted background on dragover
(.o_fp_drop_target class already added by onColDragOver in JS)
- A 56px dashed placeholder bar appears at the bottom of the column
via ::after on the drop target. That's the "here's where the card
will land" visual the user remembered.
- Dragged card gets scale(0.97) + slight rotation + opacity 0.4 for
a clearer "I'm picking this up" feedback.
3. Customer logo removed from Plant Overview cards.
The big company logo at the top of each kanban card was wasting
space. Customer NAME still shows (in bold, full-width, with text-
ellipsis), step badge pill stays on the right. No more wasted
real estate on visuals nobody looks at twice.
Extra polish while in there:
- Section headers (Tablet + Manager) now have a coloured icon badge
— a rounded square 36×36 with tinted background + accent-coloured
icon next to the H3 title. Adds visual weight without noise.
- Panel head gets a 1px bottom divider.
- Manager panels tint the icon badge per panel tone (amber for
Unassigned, green for In Progress, blue for Team).
- Header action buttons (Tablet scan/picker, Manager refresh/mode)
get proper borders + hover state.
- State dividers on bake/gate/hold rows preserved as inset shadows.
Verified: bundle rebuilt at /web/assets/278b43c/web.assets_backend.min.css
(1.45MB, id 1930). All key classes present: o_fp_drop_target,
o_fp_dragging, o_fp_po_parts_bar, o_fp_po_parts_fill, section-header
icon badges. Zero SCSS warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: the previous gradient-heavy look felt cluttered, job
cards had confusing heavy borders, the hierarchy was noisy. Wiped all
three SCSS files and both OWL templates and rebuilt from scratch with
a clean minimalist design language.
Design philosophy — the single source of truth:
* NO borders on cards — depth comes from elevation (shadow) + a
tiny surface-tint difference between page and card
* ONE accent colour (var(--o-action)); semantic red/amber/green only
for status pills and state bars
* Shadow-only cards: $fp-elev-1, $fp-elev-2, $fp-elev-3 built on
color-mix of foreground so they adapt to dark mode automatically
* Generous whitespace, 8pt spacing scale ($fp-space-1 through
$fp-space-10)
* Type-first hierarchy: 32px page titles, 44px KPI numbers, tabular
numerics so refreshing counts don't jitter
* Priority/state cues via narrow 4-6px coloured bars and small dots
— never via loud backgrounds or gradient washes
* All interactive elements at 48px touch minimum (shop-floor gloves)
New token file (_fp_shopfloor_tokens.scss) exports:
- $fp-space-1..10, $fp-radius-sm..xl, $fp-radius-pill
- $fp-page / $fp-card / $fp-card-soft surface tints
- $fp-ink / $fp-ink-soft / $fp-ink-mute / $fp-ink-faint text tiers
- $fp-elev-1..3 layered shadows
- $fp-text-xs..3xl type scale
- @mixin fp-pill, fp-focus-ring, fp-card, fp-hover-only
- fp-wash() function for state-coloured soft backgrounds
Tablet Station (fusion_plating_shopfloor.scss + shopfloor_tablet.xml):
- Clean hero: just the title, station chip, picker + scan button
- KPI cards: no gradient overlay, just a 10px coloured dot and big
44px number. Hover lifts with shadow
- Active WO: soft green wash background, no border, pulsing dot
- Panels contain queue/baths/bakes/gates/holds — all on the same
card surface with big rounded corners, no internal borders
- Queue rows: flat on a soft page-tinted background, hover slides
right 2px (no lift, cleaner)
- Bake/Gate/Hold rows: state-coloured inset shadow as a 4px stripe,
no border
- Empty states: centred with a 44px muted icon and friendly copy
Manager Desk (manager_dashboard.scss + manager_dashboard.xml):
- Matching hero with live dot that calmly pulses green during a fetch
- 4 KPI cards in the same language as the tablet
- Three panels (Unassigned / In Progress / Team) with coloured dots
next to their titles instead of top accent bars
- MO cards NO borders, subtle page-tint background, 4px left stripe
only for priority (red HOT, amber Urgent)
- Team cards: avatar + name + live load pill, hover slides right
- WO expanded rows use card-soft buttons/dropdowns for low contrast
Plant Overview (plant_overview.scss):
- Columns are now shadow-lifted cards on the tinted page background
- Kanban cards: no border, small shadow, lift on hover
- Priority stripe is an inset box-shadow (not a border) so hover
transform doesn't wobble
Backend contract preserved — OWL class names, prop signatures, RPC
endpoints, and stateBadge mapping all unchanged. Only visuals.
Verified:
* Bundle compiled to /web/assets/.../web.assets_backend.min.css
(1.45MB, id 1926)
* All 6 new classes present in compiled CSS
* Zero SCSS "forbidden import" warnings
* Zero Odoo module upgrade errors
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Odoo 19 forbids local SCSS @import statements for security reasons and
silently falls back to the OLD cached CSS bundle when it sees them. My
redesign commit used:
@import "./fp_shopfloor_tokens";
in three SCSS files. Odoo logged
WARNING Local import './fp_shopfloor_tokens' is forbidden for
security reasons. Please remove all @import {your_file} imports
in your custom files.
...and the compiled bundle kept rendering the old look. That's what
the user saw.
Fix:
1. Add _fp_shopfloor_tokens.scss as the FIRST entry in
web.assets_backend in the manifest. Odoo concatenates the bundle
in order, so variables/mixins in the first file are visible to
every later file — native @import is not needed.
2. Strip the @import "./fp_shopfloor_tokens"; line from all three
consumer files (tablet, manager, plant overview).
Verified: asset bundle regenerated to /web/assets/.../web.assets_backend.min.css
(1.45 MB). Grepped the compiled CSS and all five new classes are present:
o_fp_tablet_header, o_fp_kpi_strip, o_fp_mgr_card, o_fp_live_dot,
o_fp_panel_unassigned. 8 radial-gradients baked in. Zero warnings in
the Odoo server log post-rebuild.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Shop-floor operators and managers live on these screens all day — the
old look worked but felt like a spec sheet. Pass through all 5 pages
with one design language: gradient KPI cards, hero banners, soft
shadows, rounded corners, large touch targets, friendly empty states.
Dark mode and light mode both look deliberate, not inverted.
New shared file: _fp_shopfloor_tokens.scss
A single source of truth for radii, elevation (shadows that respect
dark mode via color-mix on foreground), typography scale (tabular
numerics for KPIs, 18px base for shop-floor readability), animation
easings, semantic gradients (@mixin fp-grad), tone helpers
(@mixin fp-tone), focus ring, and the 44px touch-min token.
Every other SCSS file imports this — no duplicated colour math.
Gradients are built on color-mix(in srgb, var(--bs-foo) X%, transparent)
so they layer naturally on either the light or dark page background.
No @media-prefers-color-scheme forks needed.
Tablet Station (fusion_plating_shopfloor.scss):
* Hero banner with dual radial-gradient wash (brand + success), live
station chip, gradient focus ring on the picker.
* KPI cards (6-up on desktop, 2x3 on phone) get a subtle top accent
line, coloured gradient overlay, 40px headline number, faded icon
at corner. Tone variants (info/success/warning/danger/muted) drive
colour without extra CSS.
* Active WO banner is a green gradient pill with a breathing-dot
pulse — unmissable when something is running.
* Panels get top accents, queue rows get priority pills (HI/M/·),
bake/gate/hold rows get colour-coded left accent bars.
* Tiles have a 4px left stripe keyed to state + hover lift.
* Status chips are uppercase, pill-shaped, tone-tinted with
color-mix so they respect theme.
* Empty states now have a large 36px icon + friendly copy instead
of a one-liner.
* Focus rings use the shared @mixin fp-focus-ring.
Manager Desk (manager_dashboard.scss):
* Same hero treatment with radial gradient + live-dot pulse.
* 3 panels carry a coloured top accent bar — amber (Unassigned),
green (In Progress), blue (Team). Instant visual routing.
* KPI strip matches tablet.
* MO cards get a left priority stripe (red for HOT, amber for
Urgent), lift on hover, expand cleanly.
* Team avatars get a border + subtle tint background for depth.
* Worker/tank pickers have custom focus rings.
Plant Overview (plant_overview.scss):
* Header is now a gradient wash tied to the brand colour.
* Work-centre columns get a thin gradient top-stripe and pill-style
count badges.
* Cards have real depth (layered shadow), lift harder on hover,
change border colour on hover.
All three files share the same design tokens, so colours/shadows/
radii are identical across pages. Edit one place, everything updates.
Verified: backend asset bundle compiles clean (no SCSS errors), zero
warnings on module upgrade, asset cache cleared for fresh delivery.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Shop-floor workers and managers use phones and iPads on the line.
The existing layouts only stacked at 1100px / 1280px, which left
everything cramped on a 375px iPhone or 390px Android. Pass through
all 5 shop-floor screens with disciplined breakpoints and touch-first
sizing.
Breakpoint ladder (consistent across files):
1400px : manager WO row: worker/tank pickers drop to their own rows
1280px : manager grid 3 → 2 columns, Team spans both
1100px : tablet dashboard 2 → 1 column
900px : manager grid → 1 column; tablet + manager padding shrinks
768px : plant overview columns stack; first-piece & bake kanbans
already handled natively by Odoo
600px : PHONE — all columns stack, everything full-width, every
button min-height 44px (Apple HIG touch target), font
shrinks for denser phone screens
Manager Desk (manager_dashboard.scss):
- Header stacks into two full-width rows on phone, action buttons
flex-grow to share the row
- 3 column grid stacks earlier (900px instead of 800px) so iPad
portrait gets a clean single-column view
- WO rows: assign/tank pickers go full-width on their own rows at
1400px, then the whole row stacks to 1 column at 600px
- Cards min 56px tap zone
- Team avatars keep their layout but cap gap on phone
Tablet Station (fusion_plating_shopfloor.scss):
- Header: picker/scan button stack full-width on phone
- KPI strip auto-fit by default, forced 2×3 grid on phone so 6
tiles stay visible without scrolling past a wall of tall cards
- Queue rows: Start/Finish buttons drop to their own row on phone,
each flexing to 50% width → easy one-thumb tap
- Bake/Gate/Hold rows: full stack on phone, action buttons flex-grow
- Bath tile grid: 2-up on phone (not auto-fit)
- Active WO banner stacks, Open-WO button full-width
- Station picker and scan input go full-width
Plant Overview (plant_overview.scss):
- Columns stack at 768px (already there) + 600px padding shrink,
search input full-width, header wraps sensibly
- Cards get min-height 64px for touch
Touch-device hover suppression:
@media (hover: none) — hover highlights were sticking after tap on
phones/iPads. Block them for .o_fp_queue_row, .o_fp_tile,
.o_fp_tablet_card, .o_fp_mgr_card, .o_fp_team_card, .o_fp_po_card.
Asset cache cleared so phones pick up the new SCSS on next load.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause of the stuck "Loading manager data..." spinner: the overview
endpoint included a search_count on sale.order.x_fc_workflow_stage,
which is a non-stored computed field. Odoo 19 raised:
ValueError: Cannot convert sale.order.x_fc_workflow_stage to SQL
because it is not stored
The controller silently logged the error; the JS caught and swallowed
the RPC failure, leaving state.overview=null forever. So the UI just
kept spinning while production changed around the manager.
Fixes:
1. Controller (manager_controller.py)
- "Awaiting assignment SOs" is now computed from STORED fields only:
state='sale' AND x_fc_receiving_status='inspected'
AND x_fc_assigned_manager_id=False
Same stage, legal SQL.
- Whole endpoint wrapped in try/except; failures return
{'ok': False, 'error': '...'} so the UI can surface them instead
of dying silently.
- Response carries a payload_hash (md5 of the JSON body minus
user_name). If the client sends back known_hash and nothing has
moved, the server returns {'unchanged': True, 'payload_hash': ...}
and the client skips the repaint entirely. Keeps the UI quiet
between polls.
2. OWL component (manager_dashboard.js)
- Poll cadence tightened from 30s → 8s (production-pace).
- Unchanged payloads don't mutate state.overview → no re-render,
no flash. Live dot just updates its tooltip.
- Changed payloads do an in-place MERGE of the overview (copying
scalars/arrays onto the existing reactive object) instead of
replacing it wholesale. OWL's diff only re-renders rows that
actually moved.
- isFetching guard so overlapping polls can't stack up.
- state.loadError surfaces backend errors in a red banner with a
Retry button — no more silent spinner.
3. UX
- Live dot next to the title: soft green at rest, bright green
pulsing during a fetch.
- "Updated Xs ago" subtitle uses a getter so the label freshens
between polls.
- Manual Refresh button next to Quick/Detailed toggle.
- Spinner only appears on the genuine first load; gone forever
once the first payload lands.
Verified: the old crashing query now runs clean on demo data; odoo
logs show zero errors for the last 5 minutes of polling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes the worker-access story. Handoffs now route themselves.
New model fp.work.role with 8 seeded defaults (noupdate so shops can
rename/prune):
masking · racking · plating_op · demask · oven · derack ·
inspection · rework
Each one has a code, icon, description, sequence, active flag.
Config menu: Configuration → Shop Roles (manager-only).
Field additions:
hr.employee.x_fc_work_role_ids (Many2many) — tag workers with the
roles they perform. One-person shop: one employee, every role.
Specialised shop: one role per employee. Cross-trained: multiple.
fusion.plating.process.node.x_fc_work_role_id (Many2one) — tag
each recipe operation with the role that performs it.
mrp.workorder.x_fc_work_role_id (Many2one) — copied from the recipe
operation on WO generation.
Auto-assignment on WO generation:
_generate_workorders_from_recipe() now copies the operation's role
onto the WO, then calls _fp_pick_worker_for_role() which picks the
least-loaded employee (active WO count) with that role. WO lands in
their Tablet "My Queue" the moment the MO is confirmed. No manual
routing needed for the common case.
Tablet Station — worker mode:
/fp/shopfloor/tablet_overview now filters to WOs where
x_fc_assigned_user_id == env.user when the field is populated.
KPIs (WOs Ready / In Progress) reflect the logged-in worker's load,
not shop-wide totals. "My Queue" rows carry wo_state + can_start +
can_finish so inline Start/Finish buttons appear.
New JS handlers onStartWo / onFinishWo call /fp/shopfloor/start_wo
and /fp/shopfloor/stop_wo (finish=true). One-tap progression.
Views:
hr.employee form gets a "Shop Roles" notebook page with many2many_tags.
Process node form gets x_fc_work_role_id inline after work_center_id.
Work Order form shows role + assigned worker.
Smoke-tested end-to-end on WH/MO/00010:
Masking → Administrator (masking role)
Racking → Administrator (racking role)
E-Nickel → Andrew (plating_op, least-loaded tiebreaker)
Demask → Administrator (masking)
Oven bake → Andrew (oven)
Derack → Administrator (racking fallback)
Post-plate QA → Administrator (inspection)
80 existing WOs backfilled with role + worker via name-match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New client action "Manager Desk" under Shop Floor menu (manager-only).
Three-column dashboard designed for the shop manager's daily job:
Column 1 — Needs a Worker
MOs with active WOs missing user assignment. Each card expands to
show per-WO rows with:
- Assign Worker dropdown (pulls from group_fusion_plating_operator)
- Tank swap dropdown (all tanks with current bath)
- Take Over (claims for the manager in one click)
- Open (jump to WO form)
Column 2 — In Progress
MOs with workers actively running WOs. Shows who's on each step,
lets manager reassign or take over if someone steps away.
Column 3 — Team
Avatar grid of operators with live queue + in-progress counts.
Click to drill into that operator's full WO list.
KPI strip on top: Unassigned WOs, In Progress, Ready to Ship, Awaiting
Assignment SOs.
Quick / Detailed view toggle — Detailed auto-expands every card body.
New field mrp.workorder.x_fc_assigned_user_id (indexed, tracked) —
the worker currently owning this step. Will be the pivot the Tablet
Station filters on in Chunk 4.
Three new endpoints:
/fp/manager/overview — dashboard snapshot (30s auto-refresh)
/fp/manager/assign_worker — set user on a WO
/fp/manager/assign_tank — swap tank on a WO
/fp/manager/take_over — manager claims the WO (no-show coverage)
Controller is graceful when mrp module isn't installed (empty overview,
no crash) and when the bridge_mrp assignment field isn't present (falls
back to showing all active WOs as "unassigned").
Verified: 4 WOs assigned across 2 users, overview queries return the
expected counts, res.groups.user_ids (Odoo 19 API, not deprecated .users).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tablet Station rebuilt as a live dashboard (not just a QR scanner):
* KPI strip — WOs Ready/Progress, Awaiting/Missed bakes,
First-Piece pending, Quality Holds (each tinted by state)
* Active WO banner with pulsing indicator when a WO is running
* My Queue panel (left) — priority-badged operator next-up list,
clickable rows that jump to the WO/bake/gate form
* Baths tile grid (right) — last-log status chips, MTO count,
hover jump to chemistry log
* Bake Windows list — inline Start/End/Open actions, colour-coded
by state (awaiting / in-progress / missed)
* First-Piece Gates — Pass/Fail buttons for pending inspections
* Quality Holds — Review jump when any open holds exist
* Station picker + scan drawer (collapsed by default)
* 30s auto-refresh, persists picked station in localStorage
New controller endpoints: /fp/shopfloor/tablet_overview,
/fp/shopfloor/pair_station, /fp/shopfloor/mark_gate.
Demo seeder (Phase 12.5) now populates:
* 5 shop-floor stations (Plating, Bake, Inspection, Shipping, Receiving)
* +3 bake windows (awaiting / in-progress / near-due)
* 4 first-piece gates (1 pending, 1 passed+released, 1 passed-holding, 1 failed)
* 2 quality holds on active MOs (one on_hold, one under_review)
All four Shop Floor menu pages (Plant Overview, Tablet Station, Bake
Windows, First-Piece Gates) now have meaningful content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>