chore(plating): de-dash shipped code + intake-neutral customer emails
Replace em-dashes and en-dashes with hyphens across 789 shipped source files (py/xml/js/scss) so the delivered module reads as human-written; em-dashes had become a recognizable AI-generated tell. Internal .md dev notes are excluded. The WO-sticker mojibake strippers keep their dash search targets (now written — / –). No logic changes: comments and display strings only; validated with py_compile + lxml parse. Rewrite the 7 customer notification emails to be intake-neutral (ship-in / drop-off / pickup) and repair-aware, and fix the Shipped email documents line (packing slip vs bill of lading; certificate only when issued). Subjects use a hyphen separator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,12 +4,12 @@
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Shop Floor',
|
||||
'name': 'Fusion Plating - Shop Floor',
|
||||
'version': '19.0.37.2.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer.',
|
||||
'description': """
|
||||
Fusion Plating — Shop Floor
|
||||
Fusion Plating - Shop Floor
|
||||
===========================
|
||||
|
||||
Tablet / operator ergonomics layer for the Fusion Plating core. Adds:
|
||||
@@ -17,7 +17,7 @@ Tablet / operator ergonomics layer for the Fusion Plating core. Adds:
|
||||
* Tablet station registration with QR code scanning
|
||||
* Operator next-up queue (transient)
|
||||
* Bake-window enforcer for hydrogen embrittlement relief baking
|
||||
(high-strength steel — clock starts when the part exits plating;
|
||||
(high-strength steel - clock starts when the part exits plating;
|
||||
customer spec defines window before relief bake must begin)
|
||||
* Bake oven master with chart recorder reference
|
||||
* First-piece inspection gates per routing
|
||||
@@ -63,11 +63,11 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
# Tokens MUST load first — other SCSS files reference its mixins
|
||||
# Tokens MUST load first - other SCSS files reference its mixins
|
||||
# and variables directly (Odoo 19 forbids @import in custom SCSS,
|
||||
# so tokens are resolved via bundle concatenation order).
|
||||
'fusion_plating_shopfloor/static/src/scss/_fp_shopfloor_tokens.scss',
|
||||
# ---- Shared OWL services (Phase 1 — tablet redesign) ----
|
||||
# ---- Shared OWL services (Phase 1 - tablet redesign) ----
|
||||
# Registered ONCE in web.assets_backend; Odoo 19 auto-compiles
|
||||
# into BOTH bright and dark bundles via $o-webclient-color-scheme.
|
||||
'fusion_plating_shopfloor/static/src/scss/components/_workflow_chip.scss',
|
||||
@@ -95,7 +95,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
# /fp/tablet/lock_session and reloads the page so the
|
||||
# browser re-bootstraps under the kiosk.
|
||||
'fusion_plating_shopfloor/static/src/js/services/tablet_session_manager.js',
|
||||
# Phase 6.3 — fpRpc wrapper. MUST load before any consumer
|
||||
# Phase 6.3 - fpRpc wrapper. MUST load before any consumer
|
||||
# (job_workspace, plant_kanban, manager_dashboard,
|
||||
# hold_composer) so `import { fpRpc }` resolves.
|
||||
'fusion_plating_shopfloor/static/src/js/services/fp_rpc.js',
|
||||
@@ -105,7 +105,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
'fusion_plating_shopfloor/static/src/scss/components/_idle_warning.scss',
|
||||
'fusion_plating_shopfloor/static/src/xml/components/idle_warning.xml',
|
||||
'fusion_plating_shopfloor/static/src/js/components/idle_warning.js',
|
||||
# 2026-05-24 lock-screen redesign — tokens MUST precede tablet_lock.scss
|
||||
# 2026-05-24 lock-screen redesign - tokens MUST precede tablet_lock.scss
|
||||
# so the $lock-* vars are visible to the consumer (project rule 8).
|
||||
'fusion_plating_shopfloor/static/src/scss/_tablet_lock_tokens.scss',
|
||||
'fusion_plating_shopfloor/static/src/scss/tablet_lock.scss',
|
||||
@@ -113,12 +113,12 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
'fusion_plating_shopfloor/static/src/js/tablet_lock.js',
|
||||
'fusion_plating_shopfloor/static/src/xml/components/pin_setup.xml',
|
||||
'fusion_plating_shopfloor/static/src/js/components/pin_setup.js',
|
||||
# ---- Racking panel (multi-rack split, Phase 1 — 2026-06-03) ----
|
||||
# ---- Racking panel (multi-rack split, Phase 1 - 2026-06-03) ----
|
||||
# Loaded before job_workspace.js (which imports RackingPanel).
|
||||
'fusion_plating_shopfloor/static/src/scss/components/_racking_panel.scss',
|
||||
'fusion_plating_shopfloor/static/src/xml/components/racking_panel.xml',
|
||||
'fusion_plating_shopfloor/static/src/js/components/racking_panel.js',
|
||||
# ---- Job Workspace (Phase 1 — tablet redesign) ----
|
||||
# ---- Job Workspace (Phase 1 - tablet redesign) ----
|
||||
'fusion_plating_shopfloor/static/src/scss/job_workspace.scss',
|
||||
'fusion_plating_shopfloor/static/src/xml/job_workspace.xml',
|
||||
'fusion_plating_shopfloor/static/src/js/job_workspace.js',
|
||||
@@ -133,7 +133,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
'fusion_plating_shopfloor/static/src/xml/fp_finish_block_dialog.xml',
|
||||
'fusion_plating_shopfloor/static/src/js/fp_finish_block_dialog.js',
|
||||
# ---- Plant View Kanban (2026-05-23 redesign) ---------------
|
||||
# 2026-05-25 — fp_shopfloor_landing (Phase 3 component) retired.
|
||||
# 2026-05-25 - fp_shopfloor_landing (Phase 3 component) retired.
|
||||
# The inline QR scanner was ported into plant_kanban; every
|
||||
# other surface (kanban layout, mode toggle, search, drag-drop)
|
||||
# is now plant_kanban's exclusive domain.
|
||||
@@ -155,7 +155,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
'fusion_plating_shopfloor/static/src/xml/components/kpi_tile.xml',
|
||||
'fusion_plating_shopfloor/static/src/xml/components/filter_chip.xml',
|
||||
'fusion_plating_shopfloor/static/src/xml/plant_kanban.xml',
|
||||
# JS — leaf components first, then card (imports timeline),
|
||||
# JS - leaf components first, then card (imports timeline),
|
||||
# then top-level orchestrator (imports all).
|
||||
'fusion_plating_shopfloor/static/src/js/components/mini_timeline.js',
|
||||
'fusion_plating_shopfloor/static/src/js/components/plant_card.js',
|
||||
@@ -169,12 +169,12 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
'fusion_plating_shopfloor/static/src/scss/process_tree.scss',
|
||||
'fusion_plating_shopfloor/static/src/scss/manager_dashboard.scss',
|
||||
'fusion_plating_shopfloor/static/src/scss/fp_kanbans.scss',
|
||||
# ZXing-js (vendored) — primary QR decoder. Robust to the
|
||||
# ZXing-js (vendored) - primary QR decoder. Robust to the
|
||||
# perspective skew, motion blur, and glare that beat jsQR
|
||||
# on phone cameras. Same engine the iOS Camera app uses
|
||||
# under the hood. UMD bundle exposes `window.ZXing`.
|
||||
'fusion_plating_shopfloor/static/lib/zxing/zxing.min.js',
|
||||
# jsQR (vendored) — fallback decoder. Faster than ZXing but
|
||||
# jsQR (vendored) - fallback decoder. Faster than ZXing but
|
||||
# less tolerant; only used if ZXing fails to load.
|
||||
'fusion_plating_shopfloor/static/lib/jsQR/jsQR.js',
|
||||
# qr_scanner.js MUST load before its consumers so the
|
||||
@@ -189,7 +189,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
'fusion_plating_shopfloor/static/src/js/plant_overview.js',
|
||||
'fusion_plating_shopfloor/static/src/js/process_tree.js',
|
||||
'fusion_plating_shopfloor/static/src/js/manager_dashboard.js',
|
||||
# Sub 12b — Move Parts / Move Rack / Rack Parts / Stop Timer
|
||||
# Sub 12b - Move Parts / Move Rack / Rack Parts / Stop Timer
|
||||
'fusion_plating_shopfloor/static/src/scss/move_dialogs.scss',
|
||||
'fusion_plating_shopfloor/static/src/xml/move_parts_dialog.xml',
|
||||
'fusion_plating_shopfloor/static/src/xml/move_rack_dialog.xml',
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
Replaces the data path for both fp_shopfloor_tablet (legacy) and
|
||||
fp_plant_overview (legacy). Two modes:
|
||||
|
||||
station — paired station's work centre + Unassigned + next 1-2 WCs
|
||||
station - paired station's work centre + Unassigned + next 1-2 WCs
|
||||
in the recipe flow. The physical-station view.
|
||||
all_plant — every active work centre, sorted by recipe flow.
|
||||
all_plant - every active work centre, sorted by recipe flow.
|
||||
|
||||
The card payload shape matches the existing plant_overview cards so
|
||||
the front-end can share the KanbanCard component. Tapping a card opens
|
||||
@@ -72,7 +72,7 @@ class FpLandingController(http.Controller):
|
||||
if mode == 'station' and relevant_wcs:
|
||||
# In station mode, include the relevant WCs + Unassigned only.
|
||||
# The OR-of-three-leaves is what makes this filter "this WC,
|
||||
# the next 1-2 WCs, or Unassigned" — three branches OR'd.
|
||||
# the next 1-2 WCs, or Unassigned" - three branches OR'd.
|
||||
step_dom = step_dom + [
|
||||
'|', '|',
|
||||
('work_centre_id', 'in', relevant_wcs.ids),
|
||||
@@ -113,7 +113,7 @@ class FpLandingController(http.Controller):
|
||||
'cards': cards_by_wc[0],
|
||||
})
|
||||
|
||||
# ---- KPIs — 4 tech-relevant tiles --------------------------------
|
||||
# ---- KPIs - 4 tech-relevant tiles --------------------------------
|
||||
ready = sum(1 for s in steps if s.state == 'ready')
|
||||
running = sum(1 for s in steps if s.state == 'in_progress')
|
||||
|
||||
@@ -165,7 +165,7 @@ class FpLandingController(http.Controller):
|
||||
def _step_to_card(self, step):
|
||||
"""Build the kanban card payload for one fp.job.step.
|
||||
|
||||
Shape matches the KanbanCard OWL component (Phase 1 — P1.7).
|
||||
Shape matches the KanbanCard OWL component (Phase 1 - P1.7).
|
||||
"""
|
||||
job = step.job_id
|
||||
return {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"""JSON-RPC endpoints for the Manager Desk (client action).
|
||||
|
||||
Native fp.job / fp.job.step edition. Speaks fp.job/fp.job.step
|
||||
end-to-end — payload keys, variables, and RPC kwargs all use the
|
||||
end-to-end - payload keys, variables, and RPC kwargs all use the
|
||||
job/step vocabulary.
|
||||
|
||||
Manager Desk ergonomics:
|
||||
@@ -62,7 +62,7 @@ class FpManagerDashboardController(http.Controller):
|
||||
"""Manager-level view: unassigned jobs, in-progress jobs, team workload."""
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Overview snapshot — used on initial load + 8s auto-refresh
|
||||
# Overview snapshot - used on initial load + 8s auto-refresh
|
||||
# ------------------------------------------------------------------
|
||||
@http.route('/fp/manager/overview', type='jsonrpc', auth='user')
|
||||
def overview(self, facility_id=None, known_hash=None):
|
||||
@@ -255,7 +255,7 @@ class FpManagerDashboardController(http.Controller):
|
||||
ready_to_ship_jobs = Job.search_count([('state', '=', 'done')])
|
||||
|
||||
# ---- Compliance / floor-health KPIs ---------------------------
|
||||
# v19.0.24.3.0 — added the things a manager actually loses sleep
|
||||
# v19.0.24.3.0 - added the things a manager actually loses sleep
|
||||
# over: stale steps (S10/S16), missed bake windows (S15), open
|
||||
# holds (S17 + QC fail), pending QCs, predecessor-locked steps
|
||||
# (S14), and the cert pipeline. Without these the Manager Desk
|
||||
@@ -272,7 +272,7 @@ class FpManagerDashboardController(http.Controller):
|
||||
QC = env.get('fusion.plating.quality.check')
|
||||
Cert = env.get('fp.certificate')
|
||||
|
||||
# Stale steps — same domains the crons use
|
||||
# Stale steps - same domains the crons use
|
||||
stale_paused = Step.search_count([
|
||||
('state', '=', 'paused'),
|
||||
('write_date', '<=', stale_paused_threshold),
|
||||
@@ -282,7 +282,7 @@ class FpManagerDashboardController(http.Controller):
|
||||
('date_started', '<=', stale_inprogress_threshold),
|
||||
])
|
||||
|
||||
# Missed bake windows — compliance bomb
|
||||
# Missed bake windows - compliance bomb
|
||||
missed_bakes = BakeWindow.search_count([
|
||||
('state', '=', 'missed_window'),
|
||||
])
|
||||
@@ -295,7 +295,7 @@ class FpManagerDashboardController(http.Controller):
|
||||
('state', 'in', ('on_hold', 'under_review')),
|
||||
])
|
||||
|
||||
# Predecessor-locked steps — operators stuck waiting on others
|
||||
# Predecessor-locked steps - operators stuck waiting on others
|
||||
# Only count when the step is ready/paused AND has the lock flag
|
||||
# AND has at least one earlier-sequence step still open.
|
||||
pred_locked = 0
|
||||
@@ -463,7 +463,7 @@ class FpManagerDashboardController(http.Controller):
|
||||
if not step.exists():
|
||||
return {'ok': False, 'error': 'Step not found.'}
|
||||
user = env.user
|
||||
previous = step.assigned_user_id.name or '—'
|
||||
previous = step.assigned_user_id.name or '-'
|
||||
step.assigned_user_id = user.id
|
||||
step.message_post(
|
||||
body=Markup('Manager takeover: <b>%s</b> replaces %s.') % (
|
||||
@@ -473,7 +473,7 @@ class FpManagerDashboardController(http.Controller):
|
||||
return {'ok': True, 'user_name': user.name}
|
||||
|
||||
# ======================================================================
|
||||
# Phase 4 tablet redesign — 3 new tabs on the Manager Desk
|
||||
# Phase 4 tablet redesign - 3 new tabs on the Manager Desk
|
||||
# ======================================================================
|
||||
|
||||
@http.route('/fp/manager/funnel', type='jsonrpc', auth='user')
|
||||
@@ -494,7 +494,7 @@ class FpManagerDashboardController(http.Controller):
|
||||
job_dom.append(('facility_id', '=', int(facility_id)))
|
||||
jobs = Job.search(job_dom, order='priority desc, date_deadline asc')
|
||||
|
||||
# Group jobs by workflow_state_id (in-memory — list is bounded by
|
||||
# Group jobs by workflow_state_id (in-memory - list is bounded by
|
||||
# active job count, typically < 200)
|
||||
by_stage = {ws.id: [] for ws in all_states}
|
||||
for job in jobs:
|
||||
@@ -532,7 +532,7 @@ class FpManagerDashboardController(http.Controller):
|
||||
"""Approval Inbox: things waiting on a manager decision.
|
||||
|
||||
Four buckets: holds to release, certs to issue, recent scrap to
|
||||
acknowledge, override requests (deferred — empty for now).
|
||||
acknowledge, override requests (deferred - empty for now).
|
||||
"""
|
||||
env = request.env
|
||||
|
||||
@@ -553,7 +553,7 @@ class FpManagerDashboardController(http.Controller):
|
||||
h.hold_reason, h.hold_reason or '',
|
||||
),
|
||||
'qty': h.qty_on_hold or 0,
|
||||
'requested_by': h.operator_id.name or '—',
|
||||
'requested_by': h.operator_id.name or '-',
|
||||
'requested_at': fp_format(env, h.create_date) if h.create_date else '',
|
||||
}
|
||||
for h in holds
|
||||
@@ -603,7 +603,7 @@ class FpManagerDashboardController(http.Controller):
|
||||
),
|
||||
'scrap_qty': h.qty_on_hold or 0,
|
||||
'reason': h.description or '',
|
||||
'operator': h.operator_id.name or '—',
|
||||
'operator': h.operator_id.name or '-',
|
||||
'at': fp_format(env, h.create_date) if h.create_date else '',
|
||||
}
|
||||
for h in scrap_holds
|
||||
@@ -614,7 +614,7 @@ class FpManagerDashboardController(http.Controller):
|
||||
'holds_to_release': holds_to_release,
|
||||
'certs_to_issue': certs_to_issue,
|
||||
'scrap_to_review': scrap_to_review,
|
||||
'override_requests': [], # deferred — placeholder
|
||||
'override_requests': [], # deferred - placeholder
|
||||
}
|
||||
|
||||
@http.route('/fp/manager/at_risk', type='jsonrpc', auth='user')
|
||||
@@ -682,7 +682,7 @@ class FpManagerDashboardController(http.Controller):
|
||||
wcs = WC.search(wc_dom)
|
||||
bottlenecks = []
|
||||
for wc in wcs:
|
||||
# Skip work centres with zero queue — no signal
|
||||
# Skip work centres with zero queue - no signal
|
||||
if wc.bottleneck_score <= 0:
|
||||
continue
|
||||
bottlenecks.append({
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
"""Tablet endpoints for Sub 12b — Move Parts / Move Rack / Rack Parts /
|
||||
"""Tablet endpoints for Sub 12b - Move Parts / Move Rack / Rack Parts /
|
||||
Stop Timer dialogs.
|
||||
|
||||
All routes JSONRPC, auth='user'. Errors return {ok: False, error: msg}
|
||||
@@ -47,7 +47,7 @@ class FpTabletMoveController(http.Controller):
|
||||
"""Returns the list of allowed tanks for this step.
|
||||
|
||||
Pulls from the recipe node's tank_ids M2M (Sub 12a authored
|
||||
list). Falls back to [] if the recipe node has none — runtime
|
||||
list). Falls back to [] if the recipe node has none - runtime
|
||||
will hide the To Station selector.
|
||||
"""
|
||||
node = step.recipe_node_id
|
||||
@@ -104,7 +104,7 @@ class FpTabletMoveController(http.Controller):
|
||||
skip_rack = bool(
|
||||
ctx.get('fp_skip_rack_assignment') and is_manager)
|
||||
|
||||
# 1. Rack-required gate (soft block — operator may RACK PARTS)
|
||||
# 1. Rack-required gate (soft block - operator may RACK PARTS)
|
||||
if (to_step.requires_rack_assignment
|
||||
and not from_step.rack_id
|
||||
and not skip_rack):
|
||||
@@ -115,7 +115,7 @@ class FpTabletMoveController(http.Controller):
|
||||
'resolve_action': 'open_rack_parts_dialog',
|
||||
})
|
||||
|
||||
# 2. Predecessor lock (Sub 13 — recipe + per-step enforcement).
|
||||
# 2. Predecessor lock (Sub 13 - recipe + per-step enforcement).
|
||||
# Delegates to fp.job.step._fp_should_block_predecessors so the
|
||||
# tablet Move dialog and the backend button_start use the same
|
||||
# decision matrix (recipe.enforce_sequential, step.parallel_start,
|
||||
@@ -128,7 +128,7 @@ class FpTabletMoveController(http.Controller):
|
||||
# BETWEEN from_step and to_step blocks the move (you'd be skipping
|
||||
# an incomplete intermediate stage). The from_step itself is
|
||||
# in-progress BY DEFINITION when advancing partial parts out of
|
||||
# it — counting it (or any earlier step) as an "unfinished
|
||||
# it - counting it (or any earlier step) as an "unfinished
|
||||
# predecessor" blocked every partial advance to a not-yet-started
|
||||
# next step. Steps before from_step are irrelevant: the parts
|
||||
# being moved are physically at from_step, ready for the next
|
||||
@@ -157,7 +157,7 @@ class FpTabletMoveController(http.Controller):
|
||||
Step = request.env['fp.job.step']
|
||||
from_step = Step.browse(from_step_id)
|
||||
to_step = Step.browse(to_step_id)
|
||||
# Available-to-move = parts currently parked here (qty_at_step —
|
||||
# Available-to-move = parts currently parked here (qty_at_step -
|
||||
# the exact number the operator sees on the card). The old
|
||||
# qty_done − qty_scrapped read referenced step fields that don't
|
||||
# exist on fp.job.step (always 0), which is why the move path was
|
||||
@@ -194,7 +194,7 @@ class FpTabletMoveController(http.Controller):
|
||||
from_step = Step.browse(from_step_id)
|
||||
to_step = Step.browse(to_step_id)
|
||||
|
||||
# Hard-block re-check on commit (defence in depth — preview can
|
||||
# Hard-block re-check on commit (defence in depth - preview can
|
||||
# lie if state changed between preview and commit).
|
||||
blockers = self._blockers_for_move(from_step, to_step, qty)
|
||||
hard = [b for b in blockers if b['severity'] == 'hard']
|
||||
@@ -218,7 +218,7 @@ class FpTabletMoveController(http.Controller):
|
||||
for prompt_id, value in (prompt_values or {}).items():
|
||||
self._capture_prompt_value(move, int(prompt_id), value)
|
||||
|
||||
# S23 — required transition-input gate. Runs AFTER value capture
|
||||
# S23 - required transition-input gate. Runs AFTER value capture
|
||||
# so the operator gets credit for whatever they filled in. Raises
|
||||
# UserError if to_step.requires_transition_form=True and any
|
||||
# required transition_input prompt has no value. Rollback unwinds
|
||||
@@ -231,7 +231,7 @@ class FpTabletMoveController(http.Controller):
|
||||
|
||||
# Partial-flow "light up" (2026-06-02 partial-order handling).
|
||||
# A normal forward transfer that parks parts at the destination
|
||||
# makes that stage actionable — flip pending -> ready so the
|
||||
# makes that stage actionable - flip pending -> ready so the
|
||||
# receiving operator immediately sees a "Ready" card in their
|
||||
# column with zero action by anyone. Never downgrade a step that
|
||||
# is already past pending. Hold/scrap/rework/return route parts
|
||||
@@ -239,12 +239,12 @@ class FpTabletMoveController(http.Controller):
|
||||
# transfer_type == 'step'.
|
||||
if transfer_type == 'step' and to_step.state == 'pending':
|
||||
to_step.state = 'ready'
|
||||
# No auto-START — that begins the labour timer, which stays an
|
||||
# No auto-START - that begins the labour timer, which stays an
|
||||
# explicit operator tap (keeps cost accurate; avoids the S16
|
||||
# phantom-timer problem).
|
||||
|
||||
# Auto-finish the source when THIS forward move drained it to zero
|
||||
# parked parts — one fewer tap. Best-effort: swallows finish-gate
|
||||
# parked parts - one fewer tap. Best-effort: swallows finish-gate
|
||||
# failures so the move always stands. Restricted to 'step' moves:
|
||||
# a step drained by a HOLD still has unresolved held parts and
|
||||
# must not auto-finish.
|
||||
@@ -261,7 +261,7 @@ class FpTabletMoveController(http.Controller):
|
||||
if bypass_flags and request.env.user.has_group(
|
||||
'fusion_plating.group_fusion_plating_manager'):
|
||||
move.message_post(body=_(
|
||||
"Manager bypass activated by %s — flags: %s"
|
||||
"Manager bypass activated by %s - flags: %s"
|
||||
) % (request.env.user.name, ', '.join(bypass_flags)))
|
||||
|
||||
return {'move_id': move.id, 'move_name': move.name}
|
||||
@@ -342,14 +342,14 @@ class FpTabletMoveController(http.Controller):
|
||||
rack = Rack.browse(rack_id)
|
||||
to_step = Step.browse(to_step_id)
|
||||
|
||||
# S23 — pre-check: rack moves don't capture transition prompts
|
||||
# S23 - pre-check: rack moves don't capture transition prompts
|
||||
# (no per-move dialog), so if to_step.requires_transition_form
|
||||
# we must reject up-front and force the operator through Move
|
||||
# Parts (which has the form UI). Without this check, rack moves
|
||||
# silently bypass the audit gate that Move Parts enforces.
|
||||
if (to_step.requires_transition_form
|
||||
and not request.env.context.get('fp_skip_transition_form')):
|
||||
# Use the same model helper for consistency — build a dummy
|
||||
# Use the same model helper for consistency - build a dummy
|
||||
# in-memory move to compute "missing" set, then surface a
|
||||
# clear message that points operators at the right tool.
|
||||
recipe_node = to_step.recipe_node_id
|
||||
@@ -393,7 +393,7 @@ class FpTabletMoveController(http.Controller):
|
||||
batch.qty_at_step_finish = (batch.qty_at_step_finish or 0) + qty
|
||||
to_step.qty_at_step_start = (to_step.qty_at_step_start or 0) + qty
|
||||
moves.append(move.id)
|
||||
# Partial-flow "light up" — auto-finish the drained source
|
||||
# Partial-flow "light up" - auto-finish the drained source
|
||||
# batch (best-effort; see _fp_try_autofinish_on_drain).
|
||||
if transfer_type == 'step':
|
||||
batch._fp_try_autofinish_on_drain()
|
||||
|
||||
@@ -18,7 +18,7 @@ from odoo.http import request
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# Mirrors fusion_plating_jobs.models.fp_job._COLUMN_SEQUENCE exactly.
|
||||
# Keep these two in sync — the column order on the board IS the sequence.
|
||||
# Keep these two in sync - the column order on the board IS the sequence.
|
||||
_COLUMN_LABELS = [
|
||||
('receiving', _('Receiving')),
|
||||
('masking', _('Masking')),
|
||||
@@ -38,13 +38,13 @@ _SORT_PRIORITY = {
|
||||
'no_parts': 1,
|
||||
'bake_due': 2,
|
||||
'awaiting_signoff': 3,
|
||||
'awaiting_cert': 3.5, # spec 2026-05-25 — after awaiting_signoff
|
||||
'awaiting_cert': 3.5, # spec 2026-05-25 - after awaiting_signoff
|
||||
'awaiting_qc': 4,
|
||||
'ready_mine': 5,
|
||||
'running_mine': 6,
|
||||
'ready': 7,
|
||||
'running': 8,
|
||||
'awaiting_ship': 8.5, # spec 2026-05-25 — after running
|
||||
'awaiting_ship': 8.5, # spec 2026-05-25 - after running
|
||||
'idle_warning': 9,
|
||||
'predecessor_locked': 10,
|
||||
'contract_review': 11,
|
||||
@@ -67,7 +67,7 @@ class PlantKanbanController(http.Controller):
|
||||
else env['fp.work.centre'])
|
||||
paired_area = paired.area_kind if paired else None
|
||||
|
||||
# Base domain — in-flight jobs.
|
||||
# Base domain - in-flight jobs.
|
||||
# 2026-05-24 (spec 2026-05-24-shopfloor-live-step-fix-design.md
|
||||
# Defect 4 / Change 3): done + cancelled jobs drop off the live
|
||||
# board. They stay reachable via smart buttons, the Plating Jobs
|
||||
@@ -97,7 +97,7 @@ class PlantKanbanController(http.Controller):
|
||||
domain.append(('card_state', 'in', ('ready_mine', 'running_mine')))
|
||||
if filters.get('awaiting_qc'):
|
||||
domain.append(('card_state', '=', 'awaiting_qc'))
|
||||
# Spec 2026-05-25 — post-shop state filter chips
|
||||
# Spec 2026-05-25 - post-shop state filter chips
|
||||
if filters.get('awaiting_cert'):
|
||||
domain.append(('state', '=', 'awaiting_cert'))
|
||||
if filters.get('awaiting_ship'):
|
||||
@@ -114,7 +114,7 @@ class PlantKanbanController(http.Controller):
|
||||
# EVERY stage where it currently has parts (a "presence"), not just
|
||||
# the single active-step column. Cards are keyed by a composite
|
||||
# "{job_id}:{area}" so one job can appear in several columns. A job
|
||||
# whose parts are all at one stage produces exactly one presence —
|
||||
# whose parts are all at one stage produces exactly one presence -
|
||||
# byte-for-byte identical to the previous one-card-per-job board.
|
||||
cards = {}
|
||||
cards_by_area = {area: [] for area, _label in _COLUMN_LABELS}
|
||||
@@ -159,7 +159,7 @@ class PlantKanbanController(http.Controller):
|
||||
'awaiting_qc': sum(
|
||||
1 for j in jobs if j.card_state == 'awaiting_qc'
|
||||
),
|
||||
# Spec 2026-05-25 — post-shop state KPIs
|
||||
# Spec 2026-05-25 - post-shop state KPIs
|
||||
'awaiting_cert': sum(
|
||||
1 for j in jobs if j.state == 'awaiting_cert'
|
||||
),
|
||||
@@ -188,7 +188,7 @@ class PlantKanbanController(http.Controller):
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Pair the current user to a work centre (used by station-QR scan from
|
||||
# plant_kanban). 2026-05-25 — replaces the legacy
|
||||
# plant_kanban). 2026-05-25 - replaces the legacy
|
||||
# localStorage-based pairing that shopfloor_landing used; plant_kanban
|
||||
# reads the pairing from res.users.paired_work_centre_ids server-side,
|
||||
# so QR scans must persist there.
|
||||
@@ -230,7 +230,7 @@ def _resolve_card_area(job):
|
||||
"""Pick the column a card lives in.
|
||||
|
||||
Active-step area_kind wins, EXCEPT for no_parts cards which always
|
||||
land in Receiving regardless of active step — the receiver is who
|
||||
land in Receiving regardless of active step - the receiver is who
|
||||
needs to act, and they work the Receiving column. With the
|
||||
live-step priority chain (see fp.job._compute_active_step_id),
|
||||
active_step_id is False only when the job has NO steps at all
|
||||
@@ -242,7 +242,7 @@ def _resolve_card_area(job):
|
||||
and 2026-05-24-recipe-cleanup-design.md Change 6.
|
||||
"""
|
||||
# no_parts cards belong in Receiving regardless of where the
|
||||
# active step is — the receiver is who acts.
|
||||
# active step is - the receiver is who acts.
|
||||
if job.card_state == 'no_parts':
|
||||
return 'receiving'
|
||||
# 2026-05-25 (spec post-shop-cert-shipping-job-states): state drives
|
||||
@@ -255,7 +255,7 @@ def _resolve_card_area(job):
|
||||
return 'shipping'
|
||||
if job.active_step_id and job.active_step_id.area_kind:
|
||||
return job.active_step_id.area_kind
|
||||
# Orphan fallback — represents a data integrity issue, not a
|
||||
# Orphan fallback - represents a data integrity issue, not a
|
||||
# normal state. Cards here have NO steps assigned at all.
|
||||
return 'receiving'
|
||||
|
||||
@@ -266,7 +266,7 @@ def _job_presences(job):
|
||||
One entry per Shop Floor area where the job currently has parts parked
|
||||
OR an actionable (in_progress / paused / ready) step. This is what lets
|
||||
a split job appear in several columns at once. A job whose parts are
|
||||
all at one stage yields exactly ONE presence — byte-for-byte identical
|
||||
all at one stage yields exactly ONE presence - byte-for-byte identical
|
||||
to the previous one-card-per-job board.
|
||||
"""
|
||||
job = job.sudo()
|
||||
@@ -291,11 +291,11 @@ def _job_presences(job):
|
||||
presences = []
|
||||
for area, steps in by_area.items():
|
||||
qty_here = sum((s.qty_at_step or 0) for s in steps)
|
||||
# A stage shows ONLY where parts physically are (qty_here > 0 —
|
||||
# A stage shows ONLY where parts physically are (qty_here > 0 -
|
||||
# which includes the first-active step's qty_at_step seed) OR where
|
||||
# a step is actively being worked (in_progress / paused — e.g.
|
||||
# a step is actively being worked (in_progress / paused - e.g.
|
||||
# drained to zero but not yet finished). A merely `ready` / `pending`
|
||||
# step with NO parts is a FUTURE stage and must NOT show — otherwise
|
||||
# step with NO parts is a FUTURE stage and must NOT show - otherwise
|
||||
# the job appears in every not-yet-started step at once (these
|
||||
# recipes seed all downstream steps to `ready`, so 6 ready steps =
|
||||
# 6 phantom cards; bug on WO-30061). Strict sequential progress
|
||||
@@ -308,7 +308,7 @@ def _job_presences(job):
|
||||
presences.append((area, _pick_focus_step(steps), qty_here))
|
||||
|
||||
if not presences:
|
||||
# Nothing parked and nothing actionable — fall back to the single
|
||||
# Nothing parked and nothing actionable - fall back to the single
|
||||
# resolved column so the job never vanishes from the board.
|
||||
return [(_resolve_card_area(job), job.active_step_id, 0)]
|
||||
return presences
|
||||
@@ -354,7 +354,7 @@ def _render_presence(job, area, step, qty_here, is_primary, paired):
|
||||
SECONDARY presences derive a simpler state from their own focus step.
|
||||
|
||||
Sudo the job so cross-module reads (sale.order, fp.part.catalog,
|
||||
customer.spec) don't AccessError for low-privilege roles (Rule 13m) —
|
||||
customer.spec) don't AccessError for low-privilege roles (Rule 13m) -
|
||||
the output is denormalized display data; fp.job ACL gates visibility.
|
||||
"""
|
||||
job = job.sudo()
|
||||
@@ -376,7 +376,7 @@ def _render_presence(job, area, step, qty_here, is_primary, paired):
|
||||
card_state = (job.card_state if is_primary
|
||||
else _secondary_card_state(step, paired))
|
||||
|
||||
step_name = step.name if step else _('—')
|
||||
step_name = step.name if step else _('-')
|
||||
step_seq = step.sequence if step else 0
|
||||
step_total = len(job.step_ids)
|
||||
tank_label = ''
|
||||
@@ -401,7 +401,7 @@ def _render_presence(job, area, step, qty_here, is_primary, paired):
|
||||
|
||||
return {
|
||||
'job_id': job.id,
|
||||
# Composite identity — one job can have several presences.
|
||||
# Composite identity - one job can have several presences.
|
||||
'card_key': '%s:%s' % (job.id, area),
|
||||
'area_kind': area,
|
||||
'is_primary': is_primary,
|
||||
@@ -480,7 +480,7 @@ def _state_chip(card_state, step):
|
||||
return {'label': _('📦 Parts in transit'), 'kind': 'no_parts'}
|
||||
if card_state == 'contract_review':
|
||||
return {'label': _('📋 QA-005 review'), 'kind': 'paperwork'}
|
||||
# Spec 2026-05-25 — post-shop states
|
||||
# Spec 2026-05-25 - post-shop states
|
||||
if card_state == 'awaiting_cert':
|
||||
return {'label': _('🏷️ Awaiting CoC'), 'kind': 'awaiting_cert'}
|
||||
if card_state == 'awaiting_ship':
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
#
|
||||
# Multi-rack splitting at Racking — Phase 1 controller. Endpoints run as the
|
||||
# Multi-rack splitting at Racking - Phase 1 controller. Endpoints run as the
|
||||
# technician (request.env.user); the rack-load + division logic lives on
|
||||
# fp.rack.load (core + fusion_plating_jobs extension).
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ def _step_can_finish(step):
|
||||
class FpShopfloorController(http.Controller):
|
||||
"""JSON-RPC endpoints for the shop-floor tablet client.
|
||||
|
||||
NOTE — Odoo 19 requires `type='jsonrpc'`. The legacy `type='json'`
|
||||
NOTE - Odoo 19 requires `type='jsonrpc'`. The legacy `type='json'`
|
||||
decorator was removed.
|
||||
"""
|
||||
|
||||
@@ -367,7 +367,7 @@ class FpShopfloorController(http.Controller):
|
||||
if not _step_can_start(step):
|
||||
return {
|
||||
'ok': False,
|
||||
'error': f'Step is in state {step.state} — only ready/paused steps can start.',
|
||||
'error': f'Step is in state {step.state} - only ready/paused steps can start.',
|
||||
}
|
||||
try:
|
||||
step.button_start()
|
||||
@@ -384,7 +384,7 @@ class FpShopfloorController(http.Controller):
|
||||
"""Finish the timer on a fp.job.step.
|
||||
|
||||
finish=True calls button_finish(); other values are no-ops for
|
||||
now (button_pause is not yet implemented on fp.job.step — see
|
||||
now (button_pause is not yet implemented on fp.job.step - see
|
||||
fp_job_step.py).
|
||||
|
||||
button_finish() can raise UserError (e.g. required sign-off
|
||||
@@ -399,7 +399,7 @@ class FpShopfloorController(http.Controller):
|
||||
if not _step_can_finish(step):
|
||||
return {
|
||||
'ok': False,
|
||||
'error': f'Step is in state {step.state} — only in-progress steps can finish.',
|
||||
'error': f'Step is in state {step.state} - only in-progress steps can finish.',
|
||||
}
|
||||
try:
|
||||
step.button_finish()
|
||||
@@ -416,7 +416,7 @@ class FpShopfloorController(http.Controller):
|
||||
# ----------------------------------------------------------------------
|
||||
# Without these endpoints Carlos has to leave the tablet, navigate
|
||||
# to the back-office job form, and edit qty_done / qty_scrapped
|
||||
# there. Most operators won't bother — scrap goes unrecorded and
|
||||
# there. Most operators won't bother - scrap goes unrecorded and
|
||||
# the AS9100 trail breaks. These two endpoints let the tablet bump
|
||||
# both with a single tap. Scrap auto-spawns a hold via fp.job.write
|
||||
# (S17 hook, no extra wiring needed here).
|
||||
@@ -450,7 +450,7 @@ class FpShopfloorController(http.Controller):
|
||||
"""Increment job.qty_scrapped by `delta`. The S17 write-hook on
|
||||
fp.job auto-spawns a fusion.plating.quality.hold for the delta;
|
||||
the operator can edit the description on that hold later.
|
||||
`reason` is optional — passed through to the hold's description.
|
||||
`reason` is optional - passed through to the hold's description.
|
||||
"""
|
||||
env = request.env
|
||||
job = env['fp.job'].browse(int(job_id))
|
||||
@@ -476,7 +476,7 @@ class FpShopfloorController(http.Controller):
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Thickness reading — Fischerscope log entry from inspection station
|
||||
# Thickness reading - Fischerscope log entry from inspection station
|
||||
# ----------------------------------------------------------------------
|
||||
@http.route('/fp/shopfloor/log_thickness_reading', type='jsonrpc', auth='user')
|
||||
def log_thickness_reading(self, job_id=None, production_id=None,
|
||||
@@ -564,7 +564,7 @@ class FpShopfloorController(http.Controller):
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Quality hold — partial qty split
|
||||
# Quality hold - partial qty split
|
||||
# ----------------------------------------------------------------------
|
||||
@http.route('/fp/shopfloor/quality_hold', type='jsonrpc', auth='user')
|
||||
def quality_hold(self, step_id=None, workorder_id=None,
|
||||
@@ -634,13 +634,13 @@ class FpShopfloorController(http.Controller):
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Tablet Overview — one-shot dashboard payload
|
||||
# Tablet Overview - one-shot dashboard payload
|
||||
# ----------------------------------------------------------------------
|
||||
# DEPRECATED (Phase 3 tablet redesign — 2026-05-22).
|
||||
# DEPRECATED (Phase 3 tablet redesign - 2026-05-22).
|
||||
# New Shop Floor Landing client action (fp_shopfloor_landing) uses
|
||||
# /fp/landing/kanban. The Tablet Station menu now points at the new
|
||||
# surface. This endpoint stays live as long as the legacy
|
||||
# fp_shopfloor_tablet OWL component is still registered — it consumes
|
||||
# fp_shopfloor_tablet OWL component is still registered - it consumes
|
||||
# the rich payload (my_queue, active_wo, baths, bake_windows, holds,
|
||||
# pending_qcs, stations). Phase 5 cleanup will retire both the
|
||||
# legacy component and this endpoint together.
|
||||
@@ -652,7 +652,7 @@ class FpShopfloorController(http.Controller):
|
||||
fp_shopfloor_landing client action (Phase 3 tablet redesign).
|
||||
"""
|
||||
_logger.info(
|
||||
"DEPRECATED /fp/shopfloor/tablet_overview called by uid %s — "
|
||||
"DEPRECATED /fp/shopfloor/tablet_overview called by uid %s - "
|
||||
"Phase 5 cleanup will remove this endpoint.",
|
||||
request.env.uid,
|
||||
)
|
||||
@@ -691,7 +691,7 @@ class FpShopfloorController(http.Controller):
|
||||
steps_ready = len(my_steps.filtered(lambda s: s.state == 'ready'))
|
||||
steps_progress = len(my_steps.filtered(lambda s: s.state == 'in_progress'))
|
||||
|
||||
# KPI scoping — match the panels below so the operator never
|
||||
# KPI scoping - match the panels below so the operator never
|
||||
# sees "12 Quality Holds" on the strip but an empty list in the
|
||||
# panel. Operator-scoped where the panel is operator-scoped;
|
||||
# facility-scoped otherwise. The manager dashboard owns the
|
||||
@@ -717,7 +717,7 @@ class FpShopfloorController(http.Controller):
|
||||
]
|
||||
|
||||
# -- My Queue (top 8) --------------------------------------------
|
||||
# v19.0.24.3.0 — every step row now carries the recipe-author
|
||||
# v19.0.24.3.0 - every step row now carries the recipe-author
|
||||
# fields (instructions / thickness_target / dwell_time_minutes /
|
||||
# bake_setpoint_temp / requires_signoff) plus a S14 predecessor-
|
||||
# block flag so the tablet can:
|
||||
@@ -732,7 +732,7 @@ class FpShopfloorController(http.Controller):
|
||||
partner_name = job.partner_id.name or ''
|
||||
product_name = job.product_id.display_name if job.product_id else ''
|
||||
|
||||
# S14 — predecessor block. Same domain the model enforces.
|
||||
# S14 - predecessor block. Same domain the model enforces.
|
||||
predecessor_blocked = False
|
||||
blocked_by_name = ''
|
||||
if (getattr(step, 'requires_predecessor_done', False)
|
||||
@@ -765,14 +765,14 @@ class FpShopfloorController(http.Controller):
|
||||
'wo_name': step.name or '',
|
||||
'can_start': can_start,
|
||||
'can_finish': _step_can_finish(step),
|
||||
# S13 — recipe-author metadata so operator sees it inline
|
||||
# S13 - recipe-author metadata so operator sees it inline
|
||||
'instructions': step.instructions or '',
|
||||
'thickness_target': step.thickness_target or 0,
|
||||
'thickness_uom': step.thickness_uom or '',
|
||||
'dwell_time_minutes': step.dwell_time_minutes or 0,
|
||||
'bake_setpoint_temp': step.bake_setpoint_temp or 0,
|
||||
'requires_signoff': bool(getattr(step, 'requires_signoff', False)),
|
||||
# S14 — predecessor block reason
|
||||
# S14 - predecessor block reason
|
||||
'predecessor_blocked': predecessor_blocked,
|
||||
'blocked_by_name': blocked_by_name,
|
||||
# Sequence so the operator can see "Step 3 of 9"
|
||||
@@ -840,7 +840,7 @@ class FpShopfloorController(http.Controller):
|
||||
]
|
||||
|
||||
# -- Bake windows ------------------------------------------------
|
||||
# v19.0.24.3.0 — scope to the operator's own jobs first so Carlos
|
||||
# v19.0.24.3.0 - scope to the operator's own jobs first so Carlos
|
||||
# doesn't see 6 unrelated plant-wide bakes in his sidebar.
|
||||
# Fallback: facility-wide for managers / when the user has no
|
||||
# active steps. Order missed_window first so accountability
|
||||
@@ -871,7 +871,7 @@ class FpShopfloorController(http.Controller):
|
||||
]
|
||||
|
||||
# -- Quality holds -----------------------------------------------
|
||||
# v19.0.24.3.0 — scope holds to operator's jobs so Carlos's
|
||||
# v19.0.24.3.0 - scope holds to operator's jobs so Carlos's
|
||||
# sidebar isn't flooded with plant-wide HOLD-XXXX from other
|
||||
# crews. Falls back to all-open for managers.
|
||||
hold_states = ('on_hold', 'under_review')
|
||||
@@ -989,17 +989,17 @@ class FpShopfloorController(http.Controller):
|
||||
# ----------------------------------------------------------------------
|
||||
# Operator queue snapshot (legacy fusion.plating.operator.queue helper)
|
||||
# ----------------------------------------------------------------------
|
||||
# DEPRECATED (Phase 3 tablet redesign — 2026-05-22).
|
||||
# DEPRECATED (Phase 3 tablet redesign - 2026-05-22).
|
||||
# The new fp_shopfloor_landing component does NOT use this endpoint;
|
||||
# it uses /fp/landing/kanban which already filters per station. The
|
||||
# only remaining consumer is the legacy fp_shopfloor_tablet OWL
|
||||
# component (still registered, no menu pointing at it). Phase 5
|
||||
# cleanup will retire both this endpoint and the legacy component
|
||||
# together — no replacement, the kanban supersedes it entirely.
|
||||
# together - no replacement, the kanban supersedes it entirely.
|
||||
@http.route('/fp/shopfloor/queue', type='jsonrpc', auth='user')
|
||||
def queue(self, facility_id=None):
|
||||
_logger.info(
|
||||
"DEPRECATED /fp/shopfloor/queue called by uid %s — "
|
||||
"DEPRECATED /fp/shopfloor/queue called by uid %s - "
|
||||
"Phase 5 cleanup will remove this endpoint.",
|
||||
request.env.uid,
|
||||
)
|
||||
@@ -1063,7 +1063,7 @@ class FpShopfloorController(http.Controller):
|
||||
target_workcenter_id=None):
|
||||
"""Move a step card to a different work centre (drag & drop).
|
||||
|
||||
`source_model` is accepted for backward compatibility but ignored —
|
||||
`source_model` is accepted for backward compatibility but ignored -
|
||||
Plant Overview now only ever serves fp.job.step cards. A target
|
||||
of 0 / falsy clears the work centre.
|
||||
"""
|
||||
@@ -1093,7 +1093,7 @@ class FpShopfloorController(http.Controller):
|
||||
|
||||
return {'ok': True}
|
||||
|
||||
# DEPRECATED (Phase 3 tablet redesign — 2026-05-22).
|
||||
# DEPRECATED (Phase 3 tablet redesign - 2026-05-22).
|
||||
# The new fp_shopfloor_landing client action has an "All Plant" mode
|
||||
# that supersedes the standalone Plant Overview surface. Old endpoint
|
||||
# stays live for the move_card sibling endpoint and the legacy
|
||||
@@ -1106,10 +1106,10 @@ class FpShopfloorController(http.Controller):
|
||||
New consumers should use /fp/landing/kanban with mode='all_plant'
|
||||
via the fp_shopfloor_landing client action (Phase 3 tablet
|
||||
redesign). Note: /fp/shopfloor/plant_overview/move_card is NOT
|
||||
deprecated — the Landing component still uses it for drag-drop.
|
||||
deprecated - the Landing component still uses it for drag-drop.
|
||||
"""
|
||||
_logger.info(
|
||||
"DEPRECATED /fp/shopfloor/plant_overview called by uid %s — "
|
||||
"DEPRECATED /fp/shopfloor/plant_overview called by uid %s - "
|
||||
"Phase 5 cleanup will remove this endpoint.",
|
||||
request.env.uid,
|
||||
)
|
||||
@@ -1215,7 +1215,7 @@ class FpShopfloorController(http.Controller):
|
||||
# HOT first, then overdue, bake-risk, stuck, due-today, due-soon,
|
||||
# then FIFO. Score sums all those factors so equal-band cards
|
||||
# still rank by combined risk. Tiebreakers: deadline asc, then id
|
||||
# asc — so the oldest card with the same risk wins.
|
||||
# asc - so the oldest card with the same risk wins.
|
||||
FAR_FUTURE = '9999-12-31'
|
||||
for wc_id, cards in cards_by_wc.items():
|
||||
cards.sort(key=lambda c: (
|
||||
@@ -1239,8 +1239,8 @@ class FpShopfloorController(http.Controller):
|
||||
c['urgency_pulse'] = False
|
||||
# Flag every critical card so the template can apply a
|
||||
# plain class instead of relying on the `:has()` CSS
|
||||
# selector — `:has()` re-evaluates on every layout pass
|
||||
# and was the real reason 5–6 second freezes happened
|
||||
# selector - `:has()` re-evaluates on every layout pass
|
||||
# and was the real reason 5-6 second freezes happened
|
||||
# during drag-drop on a busy board (v19.0.24.11.0).
|
||||
c['is_urgent'] = c.get('urgency_band') in (
|
||||
'hot', 'overdue', 'bake_risk',
|
||||
@@ -1291,7 +1291,7 @@ class FpShopfloorController(http.Controller):
|
||||
'cards': cards_by_wc[0],
|
||||
})
|
||||
|
||||
# Sub 12b — Racks pane payload alongside the existing parts cards.
|
||||
# Sub 12b - Racks pane payload alongside the existing parts cards.
|
||||
# Filters to racks currently in active racking-state. Each entry
|
||||
# has the data the OWL plant overview's Racks pane renders:
|
||||
# tag chips, current node breadcrumb, part count, and the
|
||||
@@ -1348,7 +1348,7 @@ class FpShopfloorController(http.Controller):
|
||||
# each card from 7 signals and sort columns by score desc. The score
|
||||
# is internal; the supervisor sees a `urgency_band` chip explaining
|
||||
# WHY the card is where it is ("HOT", "OVERDUE 2d", "Bake by 14:30",
|
||||
# "Paused 28h", etc) — not just a coloured ranking.
|
||||
# "Paused 28h", etc) - not just a coloured ranking.
|
||||
#
|
||||
# Bands chosen so the most severe reason wins the chip (a HOT job
|
||||
# that's also overdue still shows "HOT"). Score adds them all so two
|
||||
@@ -1369,7 +1369,7 @@ class FpShopfloorController(http.Controller):
|
||||
"""Return a dict with urgency_score (int), urgency_band (str),
|
||||
urgency_label (chip text), urgency_icon (FontAwesome class),
|
||||
urgency_tone (chip color: danger/warning/info/muted), and
|
||||
urgency_pulse (bool — chip animates when critical).
|
||||
urgency_pulse (bool - chip animates when critical).
|
||||
|
||||
Higher score = sort earlier. Score is a sum so multiple mildly
|
||||
urgent factors aggregate (e.g. paused 12h + due in 30h together
|
||||
@@ -1418,7 +1418,7 @@ class FpShopfloorController(http.Controller):
|
||||
dd = int(delta_s / 86400)
|
||||
bands.append(('due_soon', f'Due in {dd}d', 'fa-clock-o', 'info', False))
|
||||
|
||||
# 3. Stuck — paused too long, or running past 1.5× planned
|
||||
# 3. Stuck - paused too long, or running past 1.5× planned
|
||||
if step.state == 'paused' and step.write_date:
|
||||
paused_s = (now - step.write_date).total_seconds()
|
||||
if paused_s > 24 * 3600:
|
||||
@@ -1436,7 +1436,7 @@ class FpShopfloorController(http.Controller):
|
||||
score += 200
|
||||
bands.append(('stuck', f'Overrun {ratio:.1f}x', 'fa-bolt', 'danger', True))
|
||||
|
||||
# 4. Bake-window risk — compliance bomb
|
||||
# 4. Bake-window risk - compliance bomb
|
||||
for bw in job_bakes:
|
||||
if bw.state == 'missed_window':
|
||||
score += 400
|
||||
@@ -1482,7 +1482,7 @@ class FpShopfloorController(http.Controller):
|
||||
queued_start=None, job_bakes=None, now=None):
|
||||
"""Convert one fp.job.step to a card dict for Plant Overview.
|
||||
|
||||
v19.0.24.7.0 — caller now precomputes `step_idx`,
|
||||
v19.0.24.7.0 - caller now precomputes `step_idx`,
|
||||
`job_step_count`, and `queued_start` once per job (see
|
||||
plant_overview()) and passes them in. Eliminates per-card
|
||||
re-iteration of job.step_ids; drag-and-drop reload is now
|
||||
@@ -1540,14 +1540,14 @@ class FpShopfloorController(http.Controller):
|
||||
env, step.write_date or step.date_started or job.write_date,
|
||||
)
|
||||
|
||||
# Tags from job priority — keep the existing HOT / Priority labels
|
||||
# Tags from job priority - keep the existing HOT / Priority labels
|
||||
tags = []
|
||||
if job.priority == 'rush':
|
||||
tags.append('HOT')
|
||||
elif job.priority == 'high':
|
||||
tags.append('Priority')
|
||||
|
||||
# Date display — the job's planned start (operator-readable)
|
||||
# Date display - the job's planned start (operator-readable)
|
||||
date_display = fp_format(
|
||||
env, step.date_started or job.date_planned_start, fmt='%-m/%-d',
|
||||
)
|
||||
@@ -1579,7 +1579,7 @@ class FpShopfloorController(http.Controller):
|
||||
getattr(part, 'part_number', '') or part.name or ''
|
||||
)
|
||||
part_revision = getattr(part, 'revision', '') or ''
|
||||
# coating_label kept blank — Phase E removed coating; downstream
|
||||
# coating_label kept blank - Phase E removed coating; downstream
|
||||
# tablet templates read spec_label instead.
|
||||
coating_label = ''
|
||||
|
||||
@@ -1626,7 +1626,7 @@ class FpShopfloorController(http.Controller):
|
||||
'timer_kind': timer_kind,
|
||||
'timer_started_at_iso': timer_started_at_iso,
|
||||
'timer_expected_minutes': step.duration_expected or 0,
|
||||
# 1-based step ordinal + total — replaces raw recipe sequence
|
||||
# 1-based step ordinal + total - replaces raw recipe sequence
|
||||
# in the card badge so the operator sees "step 4 of 9" not
|
||||
# "step 40" (v19.0.24.6.0). Both precomputed by caller.
|
||||
'step_index': step_idx,
|
||||
@@ -1729,7 +1729,7 @@ class FpShopfloorController(http.Controller):
|
||||
'duration_display': _dur_disp(step.duration_actual or 0),
|
||||
'duration_expected_display': _dur_disp(step.duration_expected or 0),
|
||||
'missing_for_release': '',
|
||||
# Operator-facing recipe guidance — without this the
|
||||
# Operator-facing recipe guidance - without this the
|
||||
# tablet UI has no way to surface bake setpoints,
|
||||
# masking patterns, dwell times, etc.
|
||||
'instructions': step.instructions or '',
|
||||
@@ -1813,7 +1813,7 @@ class FpShopfloorController(http.Controller):
|
||||
'children': [],
|
||||
})
|
||||
else:
|
||||
# No recipe — synth a root with steps as direct operation children.
|
||||
# No recipe - synth a root with steps as direct operation children.
|
||||
child_nodes = []
|
||||
for step in all_steps:
|
||||
step_data = _step_payload(step)
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
# Part of the Fusion Plating product family.
|
||||
"""JSON-RPC endpoints for the tablet PIN gate (Phase 6 tablet redesign).
|
||||
|
||||
POST /fp/tablet/tiles — list of tiles for the lock screen
|
||||
POST /fp/tablet/unlock — verify PIN + clear/increment failure counter
|
||||
POST /fp/tablet/set_pin — self-service set/change PIN
|
||||
POST /fp/tablet/reset_pin_for — manager-only reset of another user's PIN
|
||||
POST /fp/tablet/ping — bump server-side last-active timestamp
|
||||
POST /fp/tablet/tiles - list of tiles for the lock screen
|
||||
POST /fp/tablet/unlock - verify PIN + clear/increment failure counter
|
||||
POST /fp/tablet/set_pin - self-service set/change PIN
|
||||
POST /fp/tablet/reset_pin_for - manager-only reset of another user's PIN
|
||||
POST /fp/tablet/ping - bump server-side last-active timestamp
|
||||
|
||||
Spec: docs/superpowers/specs/2026-05-22-shopfloor-pin-gate-design.md
|
||||
"""
|
||||
@@ -67,7 +67,7 @@ def _initials_from(name):
|
||||
def _avatar_gradient_for(user_id):
|
||||
"""Deterministic gradient per user id.
|
||||
|
||||
Modulo the gradient list — same operator gets the same color
|
||||
Modulo the gradient list - same operator gets the same color
|
||||
across sessions so they learn to recognize their own tile. 8
|
||||
colors are enough for a small shop (10-15 ops) with at most 2
|
||||
color collisions on average.
|
||||
@@ -99,19 +99,19 @@ class FpTabletController(http.Controller):
|
||||
"""
|
||||
|
||||
# ======================================================================
|
||||
# /fp/tablet/set_pin — self-service set or change
|
||||
# /fp/tablet/set_pin - self-service set or change
|
||||
# ======================================================================
|
||||
@http.route('/fp/tablet/set_pin', type='jsonrpc', auth='user')
|
||||
def set_pin(self, new_pin, old_pin=None, reset_token=None, user_id=None):
|
||||
"""Set or change a tablet PIN. Three authorization paths:
|
||||
|
||||
1. old_pin provided — verify old PIN matches stored hash, then set
|
||||
new (existing behavior; user_id ignored — uses env.user).
|
||||
2. reset_token provided — verify HMAC-signed token from
|
||||
1. old_pin provided - verify old PIN matches stored hash, then set
|
||||
new (existing behavior; user_id ignored - uses env.user).
|
||||
2. reset_token provided - verify HMAC-signed token from
|
||||
/fp/tablet/verify_reset_code (Task 3). Token carries the
|
||||
target user_id; new PIN set on that user even though the
|
||||
browser session is still the kiosk. (Spec D16.)
|
||||
3. Neither — only allowed for users with NO existing PIN
|
||||
3. Neither - only allowed for users with NO existing PIN
|
||||
hash on env.user; same as the pre-redesign behavior.
|
||||
"""
|
||||
env = request.env
|
||||
@@ -156,17 +156,17 @@ class FpTabletController(http.Controller):
|
||||
return {'ok': True}
|
||||
|
||||
# ======================================================================
|
||||
# /fp/tablet/request_reset_code — self-service PIN reset (D1 + D2)
|
||||
# /fp/tablet/request_reset_code - self-service PIN reset (D1 + D2)
|
||||
# ======================================================================
|
||||
@http.route('/fp/tablet/request_reset_code',
|
||||
type='jsonrpc', auth='user')
|
||||
def request_reset_code(self, user_id):
|
||||
"""Generate + email a temporary 4-digit code to `user_id`.
|
||||
|
||||
Per spec D1 (create flow) and D2 (reset flow) — same backend,
|
||||
Per spec D1 (create flow) and D2 (reset flow) - same backend,
|
||||
triggered from either the no-PIN tile click or the 3-fail
|
||||
forgot button. Caller passes user_id (already known from the
|
||||
tile click), NOT login — matches the existing unlock_session
|
||||
tile click), NOT login - matches the existing unlock_session
|
||||
signature.
|
||||
|
||||
Returns:
|
||||
@@ -198,7 +198,7 @@ class FpTabletController(http.Controller):
|
||||
}
|
||||
if not (shop_branch_ids & set(target.all_group_ids.ids)):
|
||||
return {'ok': False, 'error': 'no_role'}
|
||||
# Resolve recipient email — MUST mirror the mail.template's
|
||||
# Resolve recipient email - MUST mirror the mail.template's
|
||||
# `{{ object.email or object.login }}` priority so the masked
|
||||
# address shown in the UI matches where the email actually
|
||||
# lands. (Pre-2026-05-25 these two were inverted: the controller
|
||||
@@ -225,7 +225,7 @@ class FpTabletController(http.Controller):
|
||||
requester_ip=request.httprequest.remote_addr or '',
|
||||
)
|
||||
except UserError as e:
|
||||
# Rate limit — parse the minutes hint out of the message
|
||||
# Rate limit - parse the minutes hint out of the message
|
||||
msg = str(e.args[0]) if e.args else str(e)
|
||||
wait_min = 60 # fallback
|
||||
import re
|
||||
@@ -239,7 +239,7 @@ class FpTabletController(http.Controller):
|
||||
}
|
||||
# Render + send IMMEDIATELY (force_send=True). The legacy
|
||||
# `force_send=False` queued the mail for the `Mail: Email Queue
|
||||
# Manager` cron — which runs every 1 HOUR on entech (not per
|
||||
# Manager` cron - which runs every 1 HOUR on entech (not per
|
||||
# minute), so a tech tapping "Send temporary PIN" could wait up
|
||||
# to 60 min for the code. PIN reset is an interactive flow; the
|
||||
# user is staring at the screen. Synchronous send adds ~1s of
|
||||
@@ -259,7 +259,7 @@ class FpTabletController(http.Controller):
|
||||
'tablet_pin_reset email dispatch failed for uid %s: %s',
|
||||
target.id, exc,
|
||||
)
|
||||
# Still return success — the code IS issued; user can
|
||||
# Still return success - the code IS issued; user can
|
||||
# request another if email truly failed. Audit captures it.
|
||||
# Audit
|
||||
write_event(env,
|
||||
@@ -270,7 +270,7 @@ class FpTabletController(http.Controller):
|
||||
return {'ok': True, 'masked_email': masked}
|
||||
|
||||
# ======================================================================
|
||||
# /fp/tablet/verify_reset_code — exchange code for reset_token
|
||||
# /fp/tablet/verify_reset_code - exchange code for reset_token
|
||||
# ======================================================================
|
||||
@http.route('/fp/tablet/verify_reset_code',
|
||||
type='jsonrpc', auth='user')
|
||||
@@ -299,7 +299,7 @@ class FpTabletController(http.Controller):
|
||||
return {'ok': False, 'error': 'no_active_code'}
|
||||
ok, err = active._verify_and_consume(str(code))
|
||||
if not ok:
|
||||
# Audit failures too — useful for forensics
|
||||
# Audit failures too - useful for forensics
|
||||
write_event(env,
|
||||
event_type='failed_unlock', # reuse existing label
|
||||
attempted_user_id=target.id,
|
||||
@@ -312,7 +312,7 @@ class FpTabletController(http.Controller):
|
||||
if err == 'wrong_code':
|
||||
resp['attempts_left'] = attempts_left
|
||||
return resp
|
||||
# Success — mint reset_token and audit
|
||||
# Success - mint reset_token and audit
|
||||
token = Reset.sudo()._sign_reset_token(target.id)
|
||||
write_event(env,
|
||||
event_type='pin_reset_code_verified',
|
||||
@@ -330,7 +330,7 @@ class FpTabletController(http.Controller):
|
||||
return f'{local[0]}***@{domain}'
|
||||
|
||||
# ======================================================================
|
||||
# /fp/tablet/reset_pin_for — manager-only
|
||||
# /fp/tablet/reset_pin_for - manager-only
|
||||
# ======================================================================
|
||||
@http.route('/fp/tablet/reset_pin_for', type='jsonrpc', auth='user')
|
||||
def reset_pin_for(self, user_id):
|
||||
@@ -352,7 +352,7 @@ class FpTabletController(http.Controller):
|
||||
return {'ok': True}
|
||||
|
||||
# ======================================================================
|
||||
# /fp/tablet/unlock_session — verify PIN + mint REAL Odoo session as tech
|
||||
# /fp/tablet/unlock_session - verify PIN + mint REAL Odoo session as tech
|
||||
# ======================================================================
|
||||
@http.route('/fp/tablet/unlock_session', type='jsonrpc', auth='user')
|
||||
def unlock_session(self, user_id, pin):
|
||||
@@ -408,7 +408,7 @@ class FpTabletController(http.Controller):
|
||||
# Attempt the real Odoo session swap via the custom auth manager.
|
||||
# session.authenticate validates credentials through _check_credentials,
|
||||
# issues a new sid, sets the cookie, returns the user dict.
|
||||
# NB: Odoo 19's Session.authenticate signature is (env, credential) —
|
||||
# NB: Odoo 19's Session.authenticate signature is (env, credential) -
|
||||
# passing request.db (a string) raises TypeError: 'str' object is not
|
||||
# callable on the internal env(user=None, su=False) reset. Pass
|
||||
# request.env instead.
|
||||
@@ -420,7 +420,7 @@ class FpTabletController(http.Controller):
|
||||
'pin': pin},
|
||||
)
|
||||
except AccessDenied:
|
||||
# Wrong PIN — increment failure counter
|
||||
# Wrong PIN - increment failure counter
|
||||
new_count = (target.x_fc_tablet_pin_failed_count or 0) + 1
|
||||
threshold = int(env['ir.config_parameter'].sudo().get_param(
|
||||
'fp.shopfloor.tablet_pin_fail_threshold', 5))
|
||||
@@ -464,14 +464,14 @@ class FpTabletController(http.Controller):
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# /fp/tablet/lock_session — destroy tech session + re-auth as kiosk
|
||||
# /fp/tablet/lock_session - destroy tech session + re-auth as kiosk
|
||||
# ======================================================================
|
||||
@http.route('/fp/tablet/lock_session', type='jsonrpc', auth='user')
|
||||
def lock_session(self, reason='manual'):
|
||||
"""Lock the tablet — destroy the tech's session and re-auth the
|
||||
"""Lock the tablet - destroy the tech's session and re-auth the
|
||||
browser as fp_tablet_kiosk. Audit-log the event.
|
||||
|
||||
`reason` is one of 'manual' / 'idle' / 'ceiling' — controls which
|
||||
`reason` is one of 'manual' / 'idle' / 'ceiling' - controls which
|
||||
event_type gets written. The corresponding event_type names:
|
||||
manual -> manual_lock
|
||||
idle -> idle_lock
|
||||
@@ -493,7 +493,7 @@ class FpTabletController(http.Controller):
|
||||
|
||||
# Find the matching open session_event so we can compute duration.
|
||||
# We look for the most recent unlock for this user without a
|
||||
# session_ended_at — that's the open one.
|
||||
# session_ended_at - that's the open one.
|
||||
SessionEvent = env['fp.tablet.session.event'].sudo()
|
||||
open_event = SessionEvent.search([
|
||||
('event_type', '=', 'unlock'),
|
||||
@@ -569,7 +569,7 @@ class FpTabletController(http.Controller):
|
||||
'needs_kiosk_relogin': True}
|
||||
|
||||
try:
|
||||
# Odoo 19 signature: (env, credential) — see unlock_session note.
|
||||
# Odoo 19 signature: (env, credential) - see unlock_session note.
|
||||
request.session.authenticate(
|
||||
request.env,
|
||||
{'type': 'password',
|
||||
@@ -585,20 +585,20 @@ class FpTabletController(http.Controller):
|
||||
return {'ok': True, 'locked_at': now.isoformat()}
|
||||
|
||||
# ======================================================================
|
||||
# /fp/tablet/tiles — lock-screen tile grid
|
||||
# /fp/tablet/tiles - lock-screen tile grid
|
||||
# ======================================================================
|
||||
@http.route('/fp/tablet/tiles', type='jsonrpc', auth='user')
|
||||
def tiles(self, station_id=None):
|
||||
env = request.env
|
||||
# Phase 1 permissions overhaul (2026-05-24): show everyone on the
|
||||
# SHOP branch — Technician, Shop Manager, Manager, Quality Manager,
|
||||
# SHOP branch - Technician, Shop Manager, Manager, Quality Manager,
|
||||
# Owner. Managers/Owners need to "chip in" on the floor occasionally.
|
||||
# Search-based query because res.groups.user_ids returns DIRECT
|
||||
# memberships only — implied groups (Owner → ... → Technician) don't
|
||||
# memberships only - implied groups (Owner → ... → Technician) don't
|
||||
# get stored in user.groups_id by Odoo's group-write propagation.
|
||||
# OR across the 5 shop-branch role group ids; sales-branch users
|
||||
# (Sales Rep / Sales Manager directly held without any shop-branch
|
||||
# role) are intentionally excluded — they don't operate the tablet.
|
||||
# role) are intentionally excluded - they don't operate the tablet.
|
||||
shop_branch_xmlids = (
|
||||
'fusion_plating.group_fp_technician',
|
||||
'fusion_plating.group_fp_shop_manager_v2',
|
||||
@@ -647,7 +647,7 @@ class FpTabletController(http.Controller):
|
||||
'avatar_url': f'/web/image/res.users/{u.id}/avatar_128',
|
||||
# has_photo lets the frontend skip the avatar img when
|
||||
# the user has no uploaded photo (avoids the 1×1 default
|
||||
# image flash). sudo-read of image_128 — the field is
|
||||
# image flash). sudo-read of image_128 - the field is
|
||||
# restricted to the user themselves otherwise.
|
||||
'has_photo': bool(u_sudo.image_128),
|
||||
'avatar_gradient': _avatar_gradient_for(u.id),
|
||||
@@ -665,7 +665,7 @@ class FpTabletController(http.Controller):
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
kiosk_uid = kiosk.id if kiosk else None
|
||||
# tz_name — resolved per fp_tz.fp_user_tz (kiosk user.tz → company
|
||||
# tz_name - resolved per fp_tz.fp_user_tz (kiosk user.tz → company
|
||||
# x_fc_default_tz → UTC). Frontend uses Intl.DateTimeFormat with
|
||||
# this tz so the lock-screen clock follows the FP regional
|
||||
# setting, not the iPad's system tz (caught 2026-05-25 when the
|
||||
@@ -680,12 +680,12 @@ class FpTabletController(http.Controller):
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# /fp/tablet/ping — heartbeat used by the OWL component on every action
|
||||
# /fp/tablet/ping - heartbeat used by the OWL component on every action
|
||||
# ======================================================================
|
||||
@http.route('/fp/tablet/ping', type='jsonrpc', auth='user')
|
||||
def ping(self):
|
||||
"""Lightweight heartbeat. Used by the OWL component to confirm
|
||||
the server-side session is alive. The session uid IS the tech
|
||||
post-Phase-G — no extra plumbing needed.
|
||||
post-Phase-G - no extra plumbing needed.
|
||||
"""
|
||||
return {'ok': True, 'server_time': fields.Datetime.now().isoformat()}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
#
|
||||
# /fp/tank/<id> — mobile-friendly tank status page. Linked from NFC
|
||||
# /fp/tank/<id> - mobile-friendly tank status page. Linked from NFC
|
||||
# tags on the physical tank. The operator taps the tag with a phone,
|
||||
# the tag's URL opens this page in their default browser.
|
||||
#
|
||||
@@ -44,7 +44,7 @@ class FpTankStatusController(http.Controller):
|
||||
('state', 'in', ('in_progress', 'paused')),
|
||||
], order='date_started desc', limit=1)
|
||||
|
||||
# Up to 5 ready steps for this tank — the operator's "what's
|
||||
# Up to 5 ready steps for this tank - the operator's "what's
|
||||
# coming next" signal.
|
||||
ready_steps = Step.search([
|
||||
('tank_id', '=', tank.id),
|
||||
|
||||
@@ -9,10 +9,10 @@ data (spec PDF, attachments, chatter) + action endpoints (hold, sign-off,
|
||||
milestone advance).
|
||||
|
||||
Endpoints:
|
||||
POST /fp/workspace/load — full payload for one fp.job
|
||||
POST /fp/workspace/hold — create quality.hold with photo
|
||||
POST /fp/workspace/sign_off — capture signature + finish step
|
||||
POST /fp/workspace/advance_milestone — fire next_milestone_action
|
||||
POST /fp/workspace/load - full payload for one fp.job
|
||||
POST /fp/workspace/hold - create quality.hold with photo
|
||||
POST /fp/workspace/sign_off - capture signature + finish step
|
||||
POST /fp/workspace/advance_milestone - fire next_milestone_action
|
||||
|
||||
Companion plan: docs/superpowers/plans/2026-05-22-shopfloor-tablet-redesign-plan.md
|
||||
"""
|
||||
@@ -31,7 +31,7 @@ class FpWorkspaceController(http.Controller):
|
||||
"""JSON-RPC endpoints for the JobWorkspace OWL client action."""
|
||||
|
||||
# ======================================================================
|
||||
# /fp/workspace/load — full workspace payload
|
||||
# /fp/workspace/load - full workspace payload
|
||||
# ======================================================================
|
||||
@http.route('/fp/workspace/load', type='jsonrpc', auth='user')
|
||||
def load(self, job_id):
|
||||
@@ -90,7 +90,7 @@ class FpWorkspaceController(http.Controller):
|
||||
# Drives the embedded rack-split panel inside this step's row.
|
||||
'is_racking': step.area_kind == 'racking',
|
||||
'state': step.state,
|
||||
# Partial-order handling — parts currently parked at this
|
||||
# Partial-order handling - parts currently parked at this
|
||||
# step. Drives the "Send to next" button visibility + the
|
||||
# per-step "N here" hint; the Move dialog pre-fills from the
|
||||
# same number via the preview endpoint.
|
||||
@@ -100,7 +100,7 @@ class FpWorkspaceController(http.Controller):
|
||||
'work_centre_name': step.work_centre_id.name or '',
|
||||
'duration_actual': step.duration_actual or 0,
|
||||
'duration_expected': step.duration_expected or 0,
|
||||
# fp_isoformat_utc — preserves UTC with explicit +00:00
|
||||
# fp_isoformat_utc - preserves UTC with explicit +00:00
|
||||
# offset so the JS timer parses it as UTC (not local wall
|
||||
# time). fp_format would convert to user tz first, then
|
||||
# the JS would re-interpret that wall time as UTC and
|
||||
@@ -301,20 +301,20 @@ class FpWorkspaceController(http.Controller):
|
||||
'required_certs': required_certs,
|
||||
'receivings': receivings_payload,
|
||||
'shipping': shipping_payload,
|
||||
# 2026-05-24 — is_manager surfaces to the JS so it can offer
|
||||
# 2026-05-24 - is_manager surfaces to the JS so it can offer
|
||||
# the manager-bypass affordance (e.g. on the required-inputs
|
||||
# gate dialog). Server-side endpoints re-check the group
|
||||
# before honouring any bypass param — this is for UI only.
|
||||
# before honouring any bypass param - this is for UI only.
|
||||
'is_manager': env.user.has_group(
|
||||
'fusion_plating.group_fusion_plating_manager',
|
||||
),
|
||||
# Note: the rack-split panel is gated per-step via each step's
|
||||
# 'is_racking' flag (area_kind == 'racking'), embedded in the
|
||||
# racking step's row — not a job-level panel.
|
||||
# racking step's row - not a job-level panel.
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# /fp/workspace/hold — create a quality.hold from HoldComposer
|
||||
# /fp/workspace/hold - create a quality.hold from HoldComposer
|
||||
# ======================================================================
|
||||
@http.route('/fp/workspace/hold', type='jsonrpc', auth='user')
|
||||
def hold(self, job_id, reason='other', qty_on_hold=1, description='',
|
||||
@@ -348,7 +348,7 @@ class FpWorkspaceController(http.Controller):
|
||||
return {'ok': False, 'error': str(exc)}
|
||||
|
||||
# Attach photo if provided (base64 string from the camera input).
|
||||
# Photo attach failure does NOT roll back the hold — log + continue.
|
||||
# Photo attach failure does NOT roll back the hold - log + continue.
|
||||
attachment_id = False
|
||||
if photo_data:
|
||||
try:
|
||||
@@ -379,13 +379,13 @@ class FpWorkspaceController(http.Controller):
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# /fp/workspace/finish_step — structured-error finish with manager bypass
|
||||
# /fp/workspace/finish_step - structured-error finish with manager bypass
|
||||
# ======================================================================
|
||||
# Wraps step.button_finish with structured response shape so the
|
||||
# frontend can render the FpFinishBlockDialog (Cancel / Record /
|
||||
# Bypass) instead of a generic toast. Manager bypass requires the
|
||||
# caller to set bypass_required_inputs=True AND the server confirms
|
||||
# the user has the fusion_plating_manager group — trusting the
|
||||
# the user has the fusion_plating_manager group - trusting the
|
||||
# context flag alone would let any operator bypass via raw RPC.
|
||||
|
||||
@http.route('/fp/workspace/finish_step', type='jsonrpc', auth='user')
|
||||
@@ -395,7 +395,7 @@ class FpWorkspaceController(http.Controller):
|
||||
if not step.exists():
|
||||
return {'ok': False, 'error': 'Step not found'}
|
||||
|
||||
# Server-side bypass authorization — can't trust the client.
|
||||
# Server-side bypass authorization - can't trust the client.
|
||||
is_manager = env.user.has_group(
|
||||
'fusion_plating.group_fusion_plating_manager',
|
||||
)
|
||||
@@ -450,7 +450,7 @@ class FpWorkspaceController(http.Controller):
|
||||
return {'ok': True}
|
||||
|
||||
# ======================================================================
|
||||
# /fp/workspace/sign_off — capture signature + finish step atomically
|
||||
# /fp/workspace/sign_off - capture signature + finish step atomically
|
||||
# ======================================================================
|
||||
@http.route('/fp/workspace/sign_off', type='jsonrpc', auth='user')
|
||||
def sign_off(self, step_id, signature_data_uri=None):
|
||||
@@ -477,7 +477,7 @@ class FpWorkspaceController(http.Controller):
|
||||
)
|
||||
return {'ok': False, 'error': 'Failed to save your signature.'}
|
||||
elif not user.x_fc_signature_image:
|
||||
# No drawing AND no saved signature — nothing to sign with.
|
||||
# No drawing AND no saved signature - nothing to sign with.
|
||||
return {
|
||||
'ok': False,
|
||||
'error': 'A signature is required. Draw one to continue.',
|
||||
@@ -493,7 +493,7 @@ class FpWorkspaceController(http.Controller):
|
||||
return {'ok': True, 'step_id': step.id, 'state': step.state}
|
||||
|
||||
# ======================================================================
|
||||
# /fp/workspace/advance_milestone — fire next_milestone_action
|
||||
# /fp/workspace/advance_milestone - fire next_milestone_action
|
||||
# ======================================================================
|
||||
@http.route('/fp/workspace/advance_milestone', type='jsonrpc', auth='user')
|
||||
def advance_milestone(self, job_id):
|
||||
@@ -504,7 +504,7 @@ class FpWorkspaceController(http.Controller):
|
||||
if not job.next_milestone_action:
|
||||
return {
|
||||
'ok': False,
|
||||
'error': 'No milestone advance available — finish all steps first.',
|
||||
'error': 'No milestone advance available - finish all steps first.',
|
||||
}
|
||||
try:
|
||||
job.action_advance_next_milestone()
|
||||
@@ -532,11 +532,11 @@ class FpWorkspaceController(http.Controller):
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# Receiving — pre-recipe box-count + damage log (Spec C1+C2 2026-05-24)
|
||||
# Receiving - pre-recipe box-count + damage log (Spec C1+C2 2026-05-24)
|
||||
# ======================================================================
|
||||
# Mirrors the backend fp.receiving form just enough for the receiver
|
||||
# persona to count boxes, log damage with photos, and close the
|
||||
# receiving from the tablet workspace. No new backend models — wraps
|
||||
# receiving from the tablet workspace. No new backend models - wraps
|
||||
# action_mark_counted / action_close and fp.receiving.damage CRUD.
|
||||
|
||||
@http.route('/fp/workspace/receiving_save_lines',
|
||||
@@ -554,7 +554,7 @@ class FpWorkspaceController(http.Controller):
|
||||
return {'ok': False, 'error': 'Receiving not found'}
|
||||
if rec.state not in ('draft', 'counted'):
|
||||
return {'ok': False, 'error': (
|
||||
'Receiving is %s — only Awaiting Parts / Counted are editable.'
|
||||
'Receiving is %s - only Awaiting Parts / Counted are editable.'
|
||||
) % rec.state}
|
||||
try:
|
||||
if box_count_in is not None:
|
||||
@@ -562,7 +562,7 @@ class FpWorkspaceController(http.Controller):
|
||||
for line_dict in (lines or []):
|
||||
line = env['fp.receiving.line'].browse(int(line_dict['id']))
|
||||
if not line.exists() or line.receiving_id.id != rec.id:
|
||||
continue # stale/foreign line id — skip silently
|
||||
continue # stale/foreign line id - skip silently
|
||||
line.write({
|
||||
'received_qty': int(line_dict.get('received_qty') or 0),
|
||||
'condition': line_dict.get('condition') or 'good',
|
||||
@@ -631,7 +631,7 @@ class FpWorkspaceController(http.Controller):
|
||||
'action_required': action_required or 'none',
|
||||
})
|
||||
# Attach photos (base64 from camera / file picker). Failure
|
||||
# on a single attach doesn't roll back the damage row —
|
||||
# on a single attach doesn't roll back the damage row -
|
||||
# operator can re-upload via the back office form if needed.
|
||||
photo_atts = env['ir.attachment']
|
||||
for p in (photos or []):
|
||||
@@ -676,11 +676,11 @@ class FpWorkspaceController(http.Controller):
|
||||
return {'ok': True}
|
||||
|
||||
# ======================================================================
|
||||
# Shipping — generate outbound label + mark shipped (2026-05-29)
|
||||
# Shipping - generate outbound label + mark shipped (2026-05-29)
|
||||
# ======================================================================
|
||||
# Spec D3/D4. mark_shipped runs as the technician (real chatter
|
||||
# attribution; the method has no group gate). generate_label sudo's
|
||||
# the carrier/stock/shipment machinery — technicians intentionally
|
||||
# the carrier/stock/shipment machinery - technicians intentionally
|
||||
# don't hold those ACLs. Both re-check the order-level "ship together"
|
||||
# gate server-side via fp.job._fp_order_ship_state.
|
||||
|
||||
@@ -724,7 +724,7 @@ class FpWorkspaceController(http.Controller):
|
||||
'error': 'Enter a non-zero weight before generating the label.'}
|
||||
# sudo: carrier write triggers delivery.carrier read; the actual
|
||||
# generate synthesizes a stock.picking + fusion.shipment + label
|
||||
# attachment — all privileged. The carrier choice came from the
|
||||
# attachment - all privileged. The carrier choice came from the
|
||||
# sudo'd options list in /load, so this is safe.
|
||||
rec = rec.sudo()
|
||||
vals = {'x_fc_weight': w,
|
||||
@@ -740,7 +740,7 @@ class FpWorkspaceController(http.Controller):
|
||||
_logger.exception("workspace/generate_label failed")
|
||||
return {'ok': False, 'error': 'Label generation failed: %s' % exc}
|
||||
# The model returns a manual-wizard action (no raise) on API
|
||||
# failure — so success is "a label landed on the shipment".
|
||||
# failure - so success is "a label landed on the shipment".
|
||||
shipment = rec.x_fc_outbound_shipment_id
|
||||
if not (shipment and shipment.label_attachment_id):
|
||||
return {'ok': False, 'error': (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc. — DEMO DATA (temporary)
|
||||
Copyright 2026 Nexa Systems Inc. - DEMO DATA (temporary)
|
||||
Remove this file and its manifest entry before production release.
|
||||
-->
|
||||
<odoo noupdate="1">
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
<!-- ========== BAKE OVENS ========== -->
|
||||
<record id="demo_oven_1" model="fusion.plating.bake.oven">
|
||||
<field name="name">Bake Oven A — EN Post-Plate</field>
|
||||
<field name="name">Bake Oven A - EN Post-Plate</field>
|
||||
<field name="code">OVEN-A</field>
|
||||
<field name="facility_id" ref="fusion_plating.demo_facility_main"/>
|
||||
<field name="work_center_id" ref="fusion_plating.demo_wc_en_line"/>
|
||||
@@ -55,7 +55,7 @@
|
||||
</record>
|
||||
|
||||
<record id="demo_oven_2" model="fusion.plating.bake.oven">
|
||||
<field name="name">Bake Oven B — Chrome Stress Relief</field>
|
||||
<field name="name">Bake Oven B - Chrome Stress Relief</field>
|
||||
<field name="code">OVEN-B</field>
|
||||
<field name="facility_id" ref="fusion_plating.demo_facility_main"/>
|
||||
<field name="work_center_id" ref="fusion_plating.demo_wc_chrome_line"/>
|
||||
@@ -67,7 +67,7 @@
|
||||
<!-- ========== BAKE WINDOWS ========== -->
|
||||
<record id="demo_bake_1" model="fusion.plating.bake.window">
|
||||
<field name="bath_id" ref="fusion_plating.demo_bath_en_mp"/>
|
||||
<field name="part_ref">P/N 4422-B — Hydraulic Cylinder Rod</field>
|
||||
<field name="part_ref">P/N 4422-B - Hydraulic Cylinder Rod</field>
|
||||
<field name="lot_ref">LOT-2026-0415</field>
|
||||
<field name="customer_ref">WO-8841</field>
|
||||
<field name="quantity">25</field>
|
||||
@@ -79,7 +79,7 @@
|
||||
|
||||
<record id="demo_bake_2" model="fusion.plating.bake.window">
|
||||
<field name="bath_id" ref="fusion_plating.demo_bath_cr_hard"/>
|
||||
<field name="part_ref">P/N 7810-A — Landing Gear Pin</field>
|
||||
<field name="part_ref">P/N 7810-A - Landing Gear Pin</field>
|
||||
<field name="lot_ref">LOT-2026-0413</field>
|
||||
<field name="customer_ref">WO-8835</field>
|
||||
<field name="quantity">6</field>
|
||||
@@ -94,7 +94,7 @@
|
||||
|
||||
<record id="demo_bake_3" model="fusion.plating.bake.window">
|
||||
<field name="bath_id" ref="fusion_plating.demo_bath_en_hp"/>
|
||||
<field name="part_ref">P/N 2290-D — Valve Body</field>
|
||||
<field name="part_ref">P/N 2290-D - Valve Body</field>
|
||||
<field name="lot_ref">LOT-2026-0410</field>
|
||||
<field name="customer_ref">WO-8820</field>
|
||||
<field name="quantity">12</field>
|
||||
@@ -110,14 +110,14 @@
|
||||
|
||||
<record id="demo_bake_4" model="fusion.plating.bake.window">
|
||||
<field name="bath_id" ref="fusion_plating.demo_bath_en_mp"/>
|
||||
<field name="part_ref">P/N 5500-E — Piston Rod</field>
|
||||
<field name="part_ref">P/N 5500-E - Piston Rod</field>
|
||||
<field name="lot_ref">LOT-2026-0408</field>
|
||||
<field name="customer_ref">WO-8810</field>
|
||||
<field name="quantity">8</field>
|
||||
<field name="plate_exit_time" eval="(DateTime.now() - timedelta(hours=10)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="window_hours">4.0</field>
|
||||
<field name="state">missed_window</field>
|
||||
<field name="notes" type="html"><p>Window missed — operator shift change, parts left on rack. Flagged for quality review.</p></field>
|
||||
<field name="notes" type="html"><p>Window missed - operator shift change, parts left on rack. Flagged for quality review.</p></field>
|
||||
</record>
|
||||
|
||||
<!-- First-piece gate demo records retired with the fp.first.piece.gate
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
Phase 6 tablet PIN gate — default knobs.
|
||||
Phase 6 tablet PIN gate - default knobs.
|
||||
All overridable via Settings → Technical → Parameters → System Parameters.
|
||||
-->
|
||||
<odoo noupdate="1">
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
<!-- ===== Mail template ============================================
|
||||
email_from is computed at send time (see _fp_resolve_from_header
|
||||
on res.users) so it ALWAYS matches the active mail server's
|
||||
from_filter — eliminates the "No mail server matches the
|
||||
from_filter - eliminates the "No mail server matches the
|
||||
from_filter" warning and the DMARC misalignment that warning
|
||||
signals. Without this alignment, M365 (nexasystems.ca's host)
|
||||
greylists cross-provider mail with mismatched From for 5–15 min
|
||||
before delivering. Subject dropped the 🔒 emoji — emojis in
|
||||
greylists cross-provider mail with mismatched From for 5-15 min
|
||||
before delivering. Subject dropped the 🔒 emoji - emojis in
|
||||
subject lines bump M365 spam scoring and slow delivery on
|
||||
cross-provider mail.
|
||||
-->
|
||||
@@ -50,7 +50,7 @@
|
||||
</div>
|
||||
<p style="margin: 16px 0; font-size: 13px; opacity: 0.65;">
|
||||
This code expires in 72 hours. If you didn't request it, ignore
|
||||
this email — no action needed. The previous PIN (if any) stays
|
||||
this email - no action needed. The previous PIN (if any) stays
|
||||
valid until you successfully complete the reset on the tablet.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tablet PIN session redesign — generate kiosk password on first deploy.
|
||||
"""Tablet PIN session redesign - generate kiosk password on first deploy.
|
||||
|
||||
Runs on every -u (post_init_hook only fires on fresh install per
|
||||
CLAUDE.md rule 13d). Idempotent: only writes the password if the
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
"""19.0.33.2.0 — Drop fp.first.piece.gate model + all dependents.
|
||||
"""19.0.33.2.0 - Drop fp.first.piece.gate model + all dependents.
|
||||
|
||||
The first-piece gate model was a skeleton: manual-create only, no
|
||||
enforcement gate, no FK to fp.job, 0 production rows on entech after
|
||||
months. Audit on 2026-05-25 concluded REMOVE.
|
||||
|
||||
Per Rule "Removing menus/records — Odoo does NOT auto-delete orphans":
|
||||
Per Rule "Removing menus/records - Odoo does NOT auto-delete orphans":
|
||||
deleting <menuitem> / <record> tags from XML does NOT remove the
|
||||
corresponding DB rows. We have to explicitly drop them here.
|
||||
|
||||
@@ -37,7 +37,7 @@ def migrate(cr, version):
|
||||
WHERE model = 'fusion.plating.first.piece.gate'
|
||||
""")
|
||||
|
||||
# ---- 3. Orphan ir.ui.menu — the menuitem was removed from
|
||||
# ---- 3. Orphan ir.ui.menu - the menuitem was removed from
|
||||
# fp_menu.xml; without explicit delete it'd linger ----------
|
||||
cr.execute("""
|
||||
DELETE FROM ir_ui_menu
|
||||
|
||||
@@ -14,7 +14,7 @@ class FpBakeOven(models.Model):
|
||||
to a bake window record by serial number.
|
||||
"""
|
||||
_name = 'fusion.plating.bake.oven'
|
||||
_description = 'Fusion Plating — Bake Oven'
|
||||
_description = 'Fusion Plating - Bake Oven'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'facility_id, code'
|
||||
|
||||
|
||||
@@ -15,14 +15,14 @@ class FpBakeWindow(models.Model):
|
||||
When a high-strength-steel part exits a plating tank, a clock starts.
|
||||
The customer / specification defines a window (typically 1 to 4 hours)
|
||||
inside which the relief bake MUST begin. Missing the window requires
|
||||
scrap or rework — there is no retroactive fix.
|
||||
scrap or rework - there is no retroactive fix.
|
||||
|
||||
This model is the headline differentiator of the shop-floor module.
|
||||
A cron job updates state every 5 minutes so the kanban board on the
|
||||
tablet always reflects current jeopardy.
|
||||
"""
|
||||
_name = 'fusion.plating.bake.window'
|
||||
_description = 'Fusion Plating — Bake Window'
|
||||
_description = 'Fusion Plating - Bake Window'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'bake_required_by, id desc'
|
||||
_rec_name = 'name'
|
||||
@@ -192,7 +192,7 @@ class FpBakeWindow(models.Model):
|
||||
@api.depends('state', 'plate_exit_time', 'window_hours', 'bake_required_by',
|
||||
'bake_start_time')
|
||||
def _compute_status_color(self):
|
||||
"""Kanban colour index — neutral palette that works in light + dark.
|
||||
"""Kanban colour index - neutral palette that works in light + dark.
|
||||
|
||||
0=no color, 1=red, 2=orange, 3=yellow, 4=green, 5=purple, 10=grey
|
||||
"""
|
||||
@@ -208,7 +208,7 @@ class FpBakeWindow(models.Model):
|
||||
rec.status_color = 5 # purple
|
||||
elif rec.state == 'awaiting_bake' and rec.bake_required_by:
|
||||
if now >= rec.bake_required_by:
|
||||
rec.status_color = 1 # red — missed
|
||||
rec.status_color = 1 # red - missed
|
||||
elif rec.plate_exit_time and rec.window_hours:
|
||||
elapsed = (now - rec.plate_exit_time).total_seconds()
|
||||
total = rec.window_hours * 3600.0
|
||||
@@ -229,7 +229,7 @@ class FpBakeWindow(models.Model):
|
||||
now = fields.Datetime.now()
|
||||
for rec in self:
|
||||
if rec.state in ('baked', 'scrapped'):
|
||||
rec.time_remaining_display = '—'
|
||||
rec.time_remaining_display = '-'
|
||||
continue
|
||||
if not rec.bake_required_by:
|
||||
rec.time_remaining_display = ''
|
||||
@@ -252,7 +252,7 @@ class FpBakeWindow(models.Model):
|
||||
|
||||
Hard guard: cannot start a bake on a missed_window record without
|
||||
manager override (context `fp_skip_missed_window=True`). AS9100 /
|
||||
Nadcap can't be retroactively documented — starting a bake after
|
||||
Nadcap can't be retroactively documented - starting a bake after
|
||||
the window means the parts are likely scrap. The override exists
|
||||
for the rare case the customer accepts a deviation in writing;
|
||||
every override posts to chatter so the audit trail is intact.
|
||||
@@ -267,13 +267,13 @@ class FpBakeWindow(models.Model):
|
||||
raise UserError(_(
|
||||
'Bake window %s has expired (required by %s). '
|
||||
'A manager must override via the "Force Start "'
|
||||
'(missed window)" action — the override is '
|
||||
'(missed window)" action - the override is '
|
||||
'logged on chatter for audit. Otherwise the '
|
||||
'parts must be scrapped.'
|
||||
) % (rec.name, rec.bake_required_by))
|
||||
rec.message_post(body=_(
|
||||
'MANAGER OVERRIDE: bake started after missed window. '
|
||||
'Window required by %s — actual start %s. Customer '
|
||||
'Window required by %s - actual start %s. Customer '
|
||||
'deviation must be on file.'
|
||||
) % (rec.bake_required_by, fields.Datetime.now()))
|
||||
rec.write({
|
||||
|
||||
@@ -14,7 +14,7 @@ class FpOperatorQueue(models.TransientModel):
|
||||
table that would drift from reality.
|
||||
"""
|
||||
_name = 'fusion.plating.operator.queue'
|
||||
_description = 'Fusion Plating — Operator Next-Up Queue'
|
||||
_description = 'Fusion Plating - Operator Next-Up Queue'
|
||||
_order = 'priority desc, due_at, id'
|
||||
|
||||
operator_id = fields.Many2one(
|
||||
@@ -71,7 +71,7 @@ class FpOperatorQueue(models.TransientModel):
|
||||
# Show two buckets, in this order:
|
||||
# 1) WOs explicitly assigned to this operator (their named tasks)
|
||||
# 2) WOs with NO assignment (open for any operator to grab)
|
||||
# Skip WOs assigned to OTHER operators — strict per-aerospace
|
||||
# Skip WOs assigned to OTHER operators - strict per-aerospace
|
||||
# accountability (no one should "borrow" someone else's job).
|
||||
MrpWO = self.env.get('mrp.workorder')
|
||||
if MrpWO is not None:
|
||||
|
||||
@@ -13,7 +13,7 @@ class FpShopfloorStation(models.Model):
|
||||
so an operator can pair their device to a work centre with a single tap.
|
||||
"""
|
||||
_name = 'fusion.plating.shopfloor.station'
|
||||
_description = 'Fusion Plating — Shop Floor Station'
|
||||
_description = 'Fusion Plating - Shop Floor Station'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'facility_id, work_center_id, code'
|
||||
|
||||
@@ -73,7 +73,7 @@ class FpShopfloorStation(models.Model):
|
||||
string='Notes',
|
||||
)
|
||||
|
||||
# Phase 6 tablet PIN gate — per-station roster + idle override.
|
||||
# Phase 6 tablet PIN gate - per-station roster + idle override.
|
||||
x_fc_authorised_user_ids = fields.Many2many(
|
||||
'res.users',
|
||||
relation='fp_shopfloor_station_authorised_user_rel',
|
||||
|
||||
@@ -23,7 +23,7 @@ from odoo.exceptions import UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# Code TTL — user-picked default per D4 (spec). Long enough for shift
|
||||
# Code TTL - user-picked default per D4 (spec). Long enough for shift
|
||||
# workers / weekend gaps; short enough that an old code in the inbox
|
||||
# isn't a long-lived risk.
|
||||
_CODE_TTL_HOURS = 72
|
||||
@@ -77,7 +77,7 @@ class FpTabletPinReset(models.Model):
|
||||
_sql_constraints = [
|
||||
# At most ONE active (used_at IS NULL) row per user. Forces the
|
||||
# "request new = invalidate old" behavior. Uses Postgres
|
||||
# EXCLUDE — partial unique index doesn't compose with the
|
||||
# EXCLUDE - partial unique index doesn't compose with the
|
||||
# other rows where used_at IS NOT NULL.
|
||||
('one_active_per_user',
|
||||
"EXCLUDE (user_id WITH =) WHERE (used_at IS NULL)",
|
||||
@@ -152,7 +152,7 @@ class FpTabletPinReset(models.Model):
|
||||
('user_id', '=', user.id),
|
||||
('used_at', '=', False),
|
||||
]).write({'used_at': fields.Datetime.now()})
|
||||
# Generate the code — 0000-9999, zero-padded.
|
||||
# Generate the code - 0000-9999, zero-padded.
|
||||
code = f"{secrets.randbelow(10000):04d}"
|
||||
rec = self.sudo().create({
|
||||
'user_id': user.id,
|
||||
@@ -211,7 +211,7 @@ class FpTabletPinReset(models.Model):
|
||||
)
|
||||
if not secret:
|
||||
raise UserError(_(
|
||||
'Cannot sign reset token — database.secret not set.'
|
||||
'Cannot sign reset token - database.secret not set.'
|
||||
))
|
||||
payload = {
|
||||
'user_id': int(user_id),
|
||||
@@ -271,7 +271,7 @@ class FpTabletPinReset(models.Model):
|
||||
|
||||
@api.model
|
||||
def _cron_purge_expired(self):
|
||||
"""Daily cron — delete used/expired rows > 7 days old.
|
||||
"""Daily cron - delete used/expired rows > 7 days old.
|
||||
Audit trail lives in fp.tablet.session.event, not here, so we
|
||||
can purge aggressively without losing forensics."""
|
||||
cutoff = fields.Datetime.now() - timedelta(days=7)
|
||||
|
||||
@@ -29,7 +29,7 @@ class FpTabletSessionEvent(models.Model):
|
||||
('ceiling_lock', '8-hour ceiling lock'),
|
||||
('force_lock', 'Force lock (cron, stale session)'),
|
||||
('admin_reset', 'Admin force-reset PIN'),
|
||||
# Spec 2026-05-25 — self-service PIN reset flow
|
||||
# Spec 2026-05-25 - self-service PIN reset flow
|
||||
('pin_reset_requested', 'PIN reset code requested (email sent)'),
|
||||
('pin_reset_code_verified', 'PIN reset code verified'),
|
||||
('pin_set_after_reset', 'New PIN set via email reset flow'),
|
||||
@@ -152,7 +152,7 @@ class FpTabletSessionEvent(models.Model):
|
||||
notes='Cron force-lock: session exceeded %d-hour ceiling' % ceiling_hours,
|
||||
)
|
||||
# Mark the original unlock event closed so it's not reprocessed
|
||||
# next tick. write() is blocked by the model override — use
|
||||
# next tick. write() is blocked by the model override - use
|
||||
# direct SQL bypass (this is the documented escape hatch for
|
||||
# the retention/cron path).
|
||||
self.env.cr.execute(
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"""Feature flags for fusion_plating_shopfloor.
|
||||
|
||||
Currently:
|
||||
- x_fc_shopfloor_layout — switches the Shop Floor client action
|
||||
- x_fc_shopfloor_layout - switches the Shop Floor client action
|
||||
between the legacy per-step kanban and the v2 plant-view kanban.
|
||||
Backed by ir.config_parameter so the landing-action resolver can
|
||||
read it cheaply on every action open without a recordset fetch.
|
||||
|
||||
@@ -88,7 +88,7 @@ class ResUsers(models.Model):
|
||||
"""Set or change this user's tablet PIN. Requires sudo OR self.
|
||||
|
||||
Caller is responsible for verifying the OLD pin separately if a
|
||||
hash already exists — this method just writes the new one.
|
||||
hash already exists - this method just writes the new one.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not pin or not pin.isdigit() or len(pin) != 4:
|
||||
@@ -182,7 +182,7 @@ class ResUsers(models.Model):
|
||||
so the standard auth chain returns a 401.
|
||||
|
||||
See docs/superpowers/specs/2026-05-24-tablet-pin-session-redesign-design.md
|
||||
Section 2 — Auth path.
|
||||
Section 2 - Auth path.
|
||||
"""
|
||||
if isinstance(credential, dict) and credential.get('type') == 'fp_tablet_pin':
|
||||
login = credential.get('login')
|
||||
@@ -192,7 +192,7 @@ class ResUsers(models.Model):
|
||||
user_sudo = self.sudo().search([('login', '=', login)], limit=1)
|
||||
if not user_sudo or not user_sudo.active:
|
||||
raise AccessDenied()
|
||||
# Must hold a shop-branch role (transitively — all_group_ids follows
|
||||
# Must hold a shop-branch role (transitively - all_group_ids follows
|
||||
# the implication chain so users who hold Owner directly still match
|
||||
# the Technician/Manager checks below). Matches has_group() semantics
|
||||
# and is futureproof against role-graph edits (CLAUDE.md rules 13l + 23).
|
||||
@@ -223,12 +223,12 @@ class ResUsers(models.Model):
|
||||
return super()._check_credentials(credential, env)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# _fp_resolve_from_header — used by mail.template email_from / reply_to
|
||||
# _fp_resolve_from_header - used by mail.template email_from / reply_to
|
||||
# ------------------------------------------------------------------
|
||||
# Picks the From address that matches the active outbound mail server's
|
||||
# from_filter, so the message goes out perfectly aligned for SPF +
|
||||
# DKIM + DMARC. Mismatched From triggers M365 greylisting (5–15 min
|
||||
# delivery delay) on cross-provider mail — the user feels this as
|
||||
# DKIM + DMARC. Mismatched From triggers M365 greylisting (5-15 min
|
||||
# delivery delay) on cross-provider mail - the user feels this as
|
||||
# "the email takes a while." Mail-server lookups need sudo; the kiosk
|
||||
# session calling the template has no read on ir.mail_server. Falls
|
||||
# back to res.company.email if no usable mail server is configured.
|
||||
@@ -239,12 +239,12 @@ class ResUsers(models.Model):
|
||||
order='sequence asc, id asc', limit=1)
|
||||
if srv and srv.from_filter and '@' in srv.from_filter:
|
||||
# from_filter can be 'user@domain' OR a domain like '*@domain' /
|
||||
# 'domain' — only the exact-address form is safe to use as From.
|
||||
# 'domain' - only the exact-address form is safe to use as From.
|
||||
ff = srv.from_filter.strip()
|
||||
if not ff.startswith('*') and ' ' not in ff:
|
||||
return ff
|
||||
if srv and srv.smtp_user and '@' in srv.smtp_user:
|
||||
return srv.smtp_user
|
||||
# Last-ditch fallback — preserves the legacy behaviour for any
|
||||
# Last-ditch fallback - preserves the legacy behaviour for any
|
||||
# environment that has no mail server configured.
|
||||
return self.company_id.email or self.email or ''
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tablet PIN self-service — entech smoke.
|
||||
"""Tablet PIN self-service - entech smoke.
|
||||
|
||||
Spec: docs/superpowers/specs/2026-05-25-tablet-pin-self-service-design.md
|
||||
Plan: docs/superpowers/plans/2026-05-25-tablet-pin-self-service-plan.md
|
||||
@@ -21,7 +21,7 @@ def _ok(cond, label):
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
# Pick a real user — first active shop-branch user with no PIN.
|
||||
# Pick a real user - first active shop-branch user with no PIN.
|
||||
gids = []
|
||||
for xmlid in (
|
||||
'fusion_plating.group_fp_technician',
|
||||
|
||||
@@ -6,7 +6,7 @@ Run via odoo-shell on entech.
|
||||
import logging
|
||||
_logger = logging.getLogger('bt_pin_send_debug')
|
||||
|
||||
# Garry (uid=2) — has gs@nexasystems.ca
|
||||
# Garry (uid=2) - has gs@nexasystems.ca
|
||||
u = env['res.users'].sudo().browse(2)
|
||||
print('user:', u.name, '| email:', u.email, '| login:', u.login)
|
||||
|
||||
|
||||
@@ -50,4 +50,4 @@ print(' TOTAL (Odoo-side): {:.3f}s'.format(t_total))
|
||||
|
||||
env.cr.commit()
|
||||
print()
|
||||
print('Watch gs@nexasystems.ca — measure wall-clock from now until it lands.')
|
||||
print('Watch gs@nexasystems.ca - measure wall-clock from now until it lands.')
|
||||
|
||||
@@ -11,24 +11,24 @@
|
||||
<odoo>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- RECORD RULE — Multi-company isolation on shop-floor stations -->
|
||||
<!-- RECORD RULE - Multi-company isolation on shop-floor stations -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="fp_shopfloor_station_company_rule" model="ir.rule">
|
||||
<field name="name">Fusion Plating: Shopfloor Station — multi-company</field>
|
||||
<field name="name">Fusion Plating: Shopfloor Station - multi-company</field>
|
||||
<field name="model_id" ref="model_fusion_plating_shopfloor_station"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|', ('facility_id.company_id', '=', False), ('facility_id.company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
|
||||
<record id="fp_bake_oven_company_rule" model="ir.rule">
|
||||
<field name="name">Fusion Plating: Bake Oven — multi-company</field>
|
||||
<field name="name">Fusion Plating: Bake Oven - multi-company</field>
|
||||
<field name="model_id" ref="model_fusion_plating_bake_oven"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|', ('facility_id.company_id', '=', False), ('facility_id.company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
|
||||
<record id="fp_bake_window_company_rule" model="ir.rule">
|
||||
<field name="name">Fusion Plating: Bake Window — multi-company</field>
|
||||
<field name="name">Fusion Plating: Bake Window - multi-company</field>
|
||||
<field name="model_id" ref="model_fusion_plating_bake_window"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
User reads via base.group_user (required for any auth='user' HTTP
|
||||
route to function). On top of that, this group grants explicit
|
||||
read on res.users (tile grid) and a NARROWED read on
|
||||
ir.config_parameter (whitelisted keys only — see ir.rule below).
|
||||
ir.config_parameter (whitelisted keys only - see ir.rule below).
|
||||
No write access to anything; no read on business records
|
||||
(fp.job, sale.order, fp.certificate, fp.part.catalog, etc.).
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</record>
|
||||
|
||||
<!-- I2 fix: Narrow the kiosk's ir.config_parameter read to keys that
|
||||
begin with fp.tablet. or fp.shopfloor. — prevents reading
|
||||
begin with fp.tablet. or fp.shopfloor. - prevents reading
|
||||
third-party secrets like fusion_tasks.vapid_private_key or
|
||||
arbitrary API keys stored in ICP. The CSV row that grants
|
||||
model-level read still needs this rule to scope the matches. -->
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — GateViz (shared OWL service)
|
||||
// Fusion Plating - GateViz (shared OWL service)
|
||||
//
|
||||
// "Can't start because…" explainer for fp.job.step blockers. Drives off
|
||||
// step.blocker_kind/reason from the backend compute. Used in:
|
||||
@@ -8,14 +8,14 @@
|
||||
// • Manager Plant Board "Needs Worker" cards (badge form)
|
||||
//
|
||||
// Props:
|
||||
// canStart : Boolean — when true, renders nothing
|
||||
// blockerKind : String — predecessor/contract_review/
|
||||
// canStart : Boolean - when true, renders nothing
|
||||
// blockerKind : String - predecessor/contract_review/
|
||||
// parts_not_received/racking_required/
|
||||
// manager_input/other
|
||||
// blockerReason : String — human-readable explanation
|
||||
// jumpTargetModel : String — optional model name for tap-to-jump
|
||||
// jumpTargetId : Number — optional record id for tap-to-jump
|
||||
// onJump : Function — called with {model, id} on Jump click
|
||||
// blockerReason : String - human-readable explanation
|
||||
// jumpTargetModel : String - optional model name for tap-to-jump
|
||||
// jumpTargetId : Number - optional record id for tap-to-jump
|
||||
// onJump : Function - called with {model, id} on Jump click
|
||||
// =============================================================================
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — HoldComposer (shared OWL service)
|
||||
// Fusion Plating - HoldComposer (shared OWL service)
|
||||
//
|
||||
// Modal form to create a fusion.plating.quality.hold with reason picker,
|
||||
// qty split, optional photo, description, and mark-for-scrap toggle.
|
||||
@@ -59,7 +59,7 @@ export class FpHoldComposer extends Component {
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
// Strip "data:...;base64," prefix — backend expects raw base64
|
||||
// Strip "data:...;base64," prefix - backend expects raw base64
|
||||
const dataUri = e.target.result;
|
||||
const base64 = dataUri.split(",", 2)[1] || "";
|
||||
this.state.photoDataUri = base64;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — FpIdleWarning (shared OWL service)
|
||||
// Fusion Plating - FpIdleWarning (shared OWL service)
|
||||
//
|
||||
// Yellow-border overlay + countdown toast shown during the last
|
||||
// (default 30) seconds before auto-lock. Any pointer/touch event on
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — KanbanCard (shared OWL service)
|
||||
// Fusion Plating - KanbanCard (shared OWL service)
|
||||
//
|
||||
// Standard WO/step card used on:
|
||||
// • Shop Floor Landing kanban (station + all-plant modes)
|
||||
@@ -18,7 +18,7 @@
|
||||
// showWorkflowChip : Boolean
|
||||
// showWorkcenter : Boolean
|
||||
// showAssignedTo : Boolean
|
||||
// onTap : Function(data) — called on card click
|
||||
// onTap : Function(data) - called on card click
|
||||
// =============================================================================
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
// =====================================================================
|
||||
// FpMiniTimeline — 9-step horizontal bar showing recipe journey.
|
||||
// FpMiniTimeline - 9-step horizontal bar showing recipe journey.
|
||||
// Consumes mini_timeline JSON from /fp/landing/plant_kanban.
|
||||
// Per project rule 20: no String()/Number() in templates; classFor()
|
||||
// and labelFor() do all the formatting in JS.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — FpPinPad (shared OWL service)
|
||||
// Fusion Plating - FpPinPad (shared OWL service)
|
||||
//
|
||||
// Numeric 4-digit PIN pad. Auto-submits on the 4th digit via onSubmit
|
||||
// callback. Used by FpTabletLock unlock flow AND FpPinSetup change flow.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — FpPinSetup (client action `fp_tablet_pin_setup`)
|
||||
// Fusion Plating - FpPinSetup (client action `fp_tablet_pin_setup`)
|
||||
//
|
||||
// Modal flow for setting OR changing the user's tablet PIN. Triggered
|
||||
// from res.users preferences via action_open_tablet_pin_setup. Three
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/** @odoo-module **/
|
||||
// =====================================================================
|
||||
// FpPlantCard — Variant C card for the plant-view kanban.
|
||||
// FpPlantCard - Variant C card for the plant-view kanban.
|
||||
// Renders the full job summary + 9-step mini-timeline. Tap opens the
|
||||
// Job Workspace.
|
||||
//
|
||||
// All formatting / class composition happens in JS — per project rule
|
||||
// All formatting / class composition happens in JS - per project rule
|
||||
// 20, OWL templates can't call String(), Number(), etc. as functions.
|
||||
// =====================================================================
|
||||
|
||||
@@ -61,7 +61,7 @@ export class FpPlantCard extends Component {
|
||||
const c = this.props.card;
|
||||
if (!c.job_id) return;
|
||||
// Open the workspace focused on THIS stage's step (partial-order
|
||||
// handling) — tapping the Baking card lands on the Baking step,
|
||||
// handling) - tapping the Baking card lands on the Baking step,
|
||||
// not the job's global active step. The workspace already accepts
|
||||
// focus_step_id (see the FP-STEP scan path in plant_kanban.js).
|
||||
this.action.doAction({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @odoo-module **/
|
||||
// Racking panel — split a WO's parts across multiple racks (Phase 1).
|
||||
// Racking panel - split a WO's parts across multiple racks (Phase 1).
|
||||
// Lives on the Job Workspace, shown when the WO is at the Racking step.
|
||||
import { Component, useState, onWillStart } from "@odoo/owl";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — SignatureConfirm
|
||||
// Fusion Plating - SignatureConfirm
|
||||
//
|
||||
// Confirm dialog shown when the operator already has a saved Plating
|
||||
// Signature: previews it + "Sign & Finish" (props.onConfirm) or "Use a
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — SignaturePad (shared OWL service)
|
||||
// Fusion Plating - SignaturePad (shared OWL service)
|
||||
//
|
||||
// Modal canvas signature capture. Returns dataURI via onSubmit; the caller
|
||||
// commits it (e.g. /fp/workspace/sign_off). Mounted via the dialog service:
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — WorkflowChip (shared OWL service)
|
||||
// Fusion Plating - WorkflowChip (shared OWL service)
|
||||
//
|
||||
// Renders an fp.job.workflow.state as a colored pill + optional next-action
|
||||
// hint. Used by KanbanCard, JobWorkspace header, Manager Funnel.
|
||||
//
|
||||
// Props:
|
||||
// state : { id, name, color } — required
|
||||
// nextActionLabel : string — optional
|
||||
// state : { id, name, color } - required
|
||||
// nextActionLabel : string - optional
|
||||
//
|
||||
// Color map mirrors the fp.job.workflow.state.color Selection
|
||||
// (grey/blue/cyan/yellow/orange/green/success/danger/purple).
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — FpDamageDialog
|
||||
// Fusion Plating - FpDamageDialog
|
||||
//
|
||||
// Tablet-friendly modal for logging damage during receiving. Captures:
|
||||
// - Severity (Cosmetic / Functional / Rejected) — pill picker
|
||||
// - Severity (Cosmetic / Functional / Rejected) - pill picker
|
||||
// - Description (required textarea)
|
||||
// - Action Required (None / Notify / Return / Proceed) — pill picker
|
||||
// - Photos — both camera capture (capture="environment") AND file picker
|
||||
// - Action Required (None / Notify / Return / Proceed) - pill picker
|
||||
// - Photos - both camera capture (capture="environment") AND file picker
|
||||
//
|
||||
// Wired from FpJobWorkspace via onAddDamage. POSTs to
|
||||
// /fp/workspace/damage_create on Save; caller refreshes after onCreated().
|
||||
@@ -86,7 +86,7 @@ export class FpDamageDialog extends Component {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
// strip the "data:image/jpeg;base64," prefix — backend wants raw base64
|
||||
// strip the "data:image/jpeg;base64," prefix - backend wants raw base64
|
||||
const dataUrl = reader.result || "";
|
||||
const idx = String(dataUrl).indexOf(",");
|
||||
resolve(idx >= 0 ? dataUrl.slice(idx + 1) : dataUrl);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — FpFinishBlockDialog
|
||||
// Fusion Plating - FpFinishBlockDialog
|
||||
//
|
||||
// Shown when /fp/workspace/finish_step returns ok=false with gate='required_inputs'.
|
||||
// Non-managers see: Cancel + Record Inputs.
|
||||
@@ -22,7 +22,7 @@ export class FpFinishBlockDialog extends Component {
|
||||
close: Function,
|
||||
stepName: String,
|
||||
// Server-classified gate. 'required_inputs' is the only one the
|
||||
// current Record/Bypass UI handles — other gates fall back to
|
||||
// current Record/Bypass UI handles - other gates fall back to
|
||||
// showing the message + a Cancel button only.
|
||||
gate: String,
|
||||
message: String,
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — Job Workspace (full-screen WO surface)
|
||||
// Fusion Plating - Job Workspace (full-screen WO surface)
|
||||
// Client action: fp_job_workspace
|
||||
//
|
||||
// Opens from: kanban tap (Landing — Phase 3), smart button (fp.job form),
|
||||
// Opens from: kanban tap (Landing - Phase 3), smart button (fp.job form),
|
||||
// QR scan (FP-JOB/FP-STEP), manager dashboard card tap (Phase 4).
|
||||
//
|
||||
// Layout (top-to-bottom):
|
||||
// sticky header — WO #, customer, part, qty/done, deadline, holds
|
||||
// sticky workflow bar — 9-stage milestone dots + Next-action button
|
||||
// sticky header - WO #, customer, part, qty/done, deadline, holds
|
||||
// sticky workflow bar - 9-stage milestone dots + Next-action button
|
||||
// scrollable main:
|
||||
// left/center — step list with active expansion + GateViz
|
||||
// right side panel — spec PDF link + attachments + chatter
|
||||
// sticky action rail — Hold · Note · Milestone advance · Issue Cert
|
||||
// left/center - step list with active expansion + GateViz
|
||||
// right side panel - spec PDF link + attachments + chatter
|
||||
// sticky action rail - Hold · Note · Milestone advance · Issue Cert
|
||||
//
|
||||
// Auto-refresh: every 15s.
|
||||
// =============================================================================
|
||||
@@ -53,7 +53,7 @@ export class FpJobWorkspace extends Component {
|
||||
data: null,
|
||||
jobId: null,
|
||||
focusStepId: null,
|
||||
// Reactive monotonic tick — bumped every 1s by _tickInterval so
|
||||
// Reactive monotonic tick - bumped every 1s by _tickInterval so
|
||||
// the active-step timer re-renders without an RPC. The template
|
||||
// reads tickNow and re-runs formatActiveStepElapsed each second.
|
||||
tickNow: Date.now(),
|
||||
@@ -68,7 +68,7 @@ export class FpJobWorkspace extends Component {
|
||||
this.state.focusStepId = params.focus_step_id || null;
|
||||
// After Hand Off + PIN relogin the action remounts without
|
||||
// params (action params aren't URL-encoded), so jobId is
|
||||
// null and refresh() exits early — workspace was stuck on
|
||||
// null and refresh() exits early - workspace was stuck on
|
||||
// "Loading Job Workspace…" indefinitely. Fall back to the
|
||||
// plant kanban so the operator lands somewhere usable.
|
||||
if (!this.state.jobId) {
|
||||
@@ -81,7 +81,7 @@ export class FpJobWorkspace extends Component {
|
||||
}
|
||||
await this.refresh();
|
||||
// If load failed (job no longer accessible, server error, etc.)
|
||||
// also redirect — leaving the user on a perma-loading screen
|
||||
// also redirect - leaving the user on a perma-loading screen
|
||||
// with no recovery path is worse than dropping them at the
|
||||
// kanban where they can re-enter.
|
||||
if (!this.state.data) {
|
||||
@@ -93,7 +93,7 @@ export class FpJobWorkspace extends Component {
|
||||
return;
|
||||
}
|
||||
this._refreshInterval = setInterval(() => this.refresh(), 15000);
|
||||
// 1s tick — pure client-side; no RPC. Drives the live timer
|
||||
// 1s tick - pure client-side; no RPC. Drives the live timer
|
||||
// on the active step's badge area.
|
||||
this._tickInterval = setInterval(() => {
|
||||
this.state.tickNow = Date.now();
|
||||
@@ -113,10 +113,10 @@ export class FpJobWorkspace extends Component {
|
||||
|
||||
formatActiveStepElapsed(step) {
|
||||
if (!step || !step.date_started_iso) return "";
|
||||
// Controller now sends fp_isoformat_utc — a proper ISO-8601 with
|
||||
// Controller now sends fp_isoformat_utc - a proper ISO-8601 with
|
||||
// explicit +00:00 offset. Parse directly. (Previously appended
|
||||
// "Z" to a fp_format string, which had been converted to user's
|
||||
// local tz, so the timer was offset by the tz delta — 4h on EDT.)
|
||||
// local tz, so the timer was offset by the tz delta - 4h on EDT.)
|
||||
const startedMs = Date.parse(step.date_started_iso);
|
||||
if (!startedMs || isNaN(startedMs)) return "";
|
||||
// touch state.tickNow so OWL re-evaluates this getter every tick
|
||||
@@ -151,7 +151,7 @@ export class FpJobWorkspace extends Component {
|
||||
// HTML-ESCAPES plain JS strings unless they're tagged with
|
||||
// markup() from @odoo/owl. Without this wrap the operator
|
||||
// sees literal `<p>` and `<b>` tags instead of formatted
|
||||
// text (caught 2026-05-23 — Notes panel showing raw HTML).
|
||||
// text (caught 2026-05-23 - Notes panel showing raw HTML).
|
||||
if (res.chatter && res.chatter.length) {
|
||||
for (const m of res.chatter) {
|
||||
if (m && typeof m.body === "string") {
|
||||
@@ -172,7 +172,7 @@ export class FpJobWorkspace extends Component {
|
||||
onBack() {
|
||||
// target: "main" CLEARS the breadcrumb stack (Odoo 19:
|
||||
// action.target === "main" => clearBreadcrumbs in action_service.js).
|
||||
// target: "current" was APPENDING — each kanban<->workspace switch
|
||||
// target: "current" was APPENDING - each kanban<->workspace switch
|
||||
// grew the /odoo/... URL, and lock/unlock window.location.reload()
|
||||
// preserved it, so the address bar ballooned. "main" keeps the URL a
|
||||
// single action. The plant kanban is the sole Shop Floor surface.
|
||||
@@ -250,7 +250,7 @@ export class FpJobWorkspace extends Component {
|
||||
if (step.override_excluded) return [];
|
||||
|
||||
const actions = [];
|
||||
// Partial-order handling — "Send to next →" advances parts parked
|
||||
// Partial-order handling - "Send to next →" advances parts parked
|
||||
// at this step to the next stage. Only shown when parts are here
|
||||
// AND a next stage exists. The destination name is on the button
|
||||
// so there's nothing to guess; qty defaults to all parked here.
|
||||
@@ -290,7 +290,7 @@ export class FpJobWorkspace extends Component {
|
||||
});
|
||||
return actions;
|
||||
}
|
||||
// state in ('pending', 'ready') — entry-point per kind.
|
||||
// state in ('pending', 'ready') - entry-point per kind.
|
||||
if (step.kind === "contract_review") {
|
||||
actions.push({ key: "open_contract_review", label: "Open QA-005 Form",
|
||||
icon: "fa fa-file-text-o", cssClass: "btn btn-primary" });
|
||||
@@ -306,7 +306,7 @@ export class FpJobWorkspace extends Component {
|
||||
icon: "fa fa-server", cssClass: "btn btn-primary" });
|
||||
return actions;
|
||||
}
|
||||
// Default — plain Start
|
||||
// Default - plain Start
|
||||
actions.push({ key: "start", label: "Start",
|
||||
icon: "fa fa-play", cssClass: "btn btn-primary" });
|
||||
return actions;
|
||||
@@ -374,13 +374,13 @@ export class FpJobWorkspace extends Component {
|
||||
onRedraw: () => this._openSignaturePad(step), // draw a new one
|
||||
});
|
||||
} else {
|
||||
// First time — draw once; the backend persists it to the
|
||||
// First time - draw once; the backend persists it to the
|
||||
// user's Plating Signature so later sign-offs are one-tap.
|
||||
this._openSignaturePad(step);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Plain finish — route through /fp/workspace/finish_step which
|
||||
// Plain finish - route through /fp/workspace/finish_step which
|
||||
// returns structured errors so we can show the FpFinishBlockDialog
|
||||
// for required-inputs failures (with manager bypass option).
|
||||
await this._callFinishStep(step, /* bypass */ false);
|
||||
@@ -427,7 +427,7 @@ export class FpJobWorkspace extends Component {
|
||||
await this.refresh();
|
||||
return;
|
||||
}
|
||||
// Structured-error branch — show block dialog for the
|
||||
// Structured-error branch - show block dialog for the
|
||||
// required_inputs gate (it has the rich Record / Bypass UX).
|
||||
// Other gates fall back to a plain notification.
|
||||
if (res && res.gate === "required_inputs") {
|
||||
@@ -525,11 +525,11 @@ export class FpJobWorkspace extends Component {
|
||||
}
|
||||
|
||||
// ---- Partial-order advance (2026-06-02) -------------------------------
|
||||
// "Send to next →" — moves parts parked at this step to the next stage.
|
||||
// "Send to next →" - moves parts parked at this step to the next stage.
|
||||
// The destination auto-readies server-side (move_controller), so the
|
||||
// receiving operator sees a Ready card immediately; the source
|
||||
// auto-finishes when it drains to zero. Pure client-side next-step
|
||||
// resolution off the loaded step list — no extra RPC.
|
||||
// resolution off the loaded step list - no extra RPC.
|
||||
|
||||
nextStepFor(step) {
|
||||
// The next stage parts flow into: lowest-sequence non-terminal step
|
||||
@@ -547,7 +547,7 @@ export class FpJobWorkspace extends Component {
|
||||
const nxt = this.nextStepFor(step);
|
||||
if (!nxt) {
|
||||
this.notification.add(
|
||||
"This is the last stage — parts finish here and close out at job completion.",
|
||||
"This is the last stage - parts finish here and close out at job completion.",
|
||||
{ type: "warning" },
|
||||
);
|
||||
return;
|
||||
@@ -632,7 +632,7 @@ export class FpJobWorkspace extends Component {
|
||||
}
|
||||
|
||||
async onReceivingClose(rcv) {
|
||||
// No confirmation — Mark Counted is already a deliberate prior
|
||||
// No confirmation - Mark Counted is already a deliberate prior
|
||||
// step, and the native browser confirm() popup looks out of place
|
||||
// on the tablet UI. If a receiver hits Close prematurely, an
|
||||
// admin can reset via fp.receiving.action_reset_to_counted from
|
||||
@@ -694,7 +694,7 @@ export class FpJobWorkspace extends Component {
|
||||
const text = window.prompt("Add a note to this WO:");
|
||||
if (!text) return;
|
||||
try {
|
||||
// ORM call for message_post — keeps chatter behaviour identical
|
||||
// ORM call for message_post - keeps chatter behaviour identical
|
||||
// to back-office posts (handles HTML escaping, subscribers, etc.)
|
||||
await rpc("/web/dataset/call_kw", {
|
||||
model: "fp.job",
|
||||
@@ -726,7 +726,7 @@ export class FpJobWorkspace extends Component {
|
||||
}
|
||||
|
||||
// ---- Shipping handlers (tablet receiving+shipping 2026-05-29) ----------
|
||||
// All coercion is JS-side (CLAUDE.md Rule 20 — templates only expose Math).
|
||||
// All coercion is JS-side (CLAUDE.md Rule 20 - templates only expose Math).
|
||||
onShipInput(field, ev) {
|
||||
const raw = ev.target.value;
|
||||
this.state.shipForm[field] =
|
||||
@@ -757,7 +757,7 @@ export class FpJobWorkspace extends Component {
|
||||
});
|
||||
if (res && res.ok) {
|
||||
this.notification.add(
|
||||
"Label generated — tracking " + (res.tracking_number || "n/a"),
|
||||
"Label generated - tracking " + (res.tracking_number || "n/a"),
|
||||
{ type: "success" },
|
||||
);
|
||||
await this.refresh();
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — Manager Desk (OWL client action)
|
||||
// Fusion Plating - Manager Desk (OWL client action)
|
||||
// Copyright 2026 Nexa Systems Inc.
|
||||
// License OPL-1 (Odoo Proprietary License v1.0)
|
||||
//
|
||||
// Manager-level view: assign workers, swap tanks, cover no-shows, drill
|
||||
// into detail when needed. Three columns: Needs a Worker / In Progress / Team.
|
||||
//
|
||||
// Native fp.job / fp.job.step edition. Speaks job/step end-to-end —
|
||||
// Native fp.job / fp.job.step edition. Speaks job/step end-to-end -
|
||||
// payload keys, variables, and RPC kwargs all use the job/step
|
||||
// vocabulary.
|
||||
// =============================================================================
|
||||
@@ -46,7 +46,7 @@ export class ManagerDashboard extends Component {
|
||||
// Defaults to false because lead-hand coverage often needs
|
||||
// off-roster names.
|
||||
hideOffShift: false,
|
||||
// Phase 4 tablet redesign — 4 sibling tabs.
|
||||
// Phase 4 tablet redesign - 4 sibling tabs.
|
||||
// funnel | inbox | plant_board | at_risk
|
||||
activeTab: "funnel",
|
||||
funnel: null, // /fp/manager/funnel payload
|
||||
@@ -103,7 +103,7 @@ export class ManagerDashboard extends Component {
|
||||
}
|
||||
this.state.lastUpdated = Date.now();
|
||||
} catch (err) {
|
||||
// Network / auth hiccup — surface it so the UI isn't a
|
||||
// Network / auth hiccup - surface it so the UI isn't a
|
||||
// permanent spinner.
|
||||
this.state.loadError = `Couldn't reach the server: ${err.message || err}`;
|
||||
} finally {
|
||||
@@ -123,7 +123,7 @@ export class ManagerDashboard extends Component {
|
||||
}
|
||||
// Dict slot
|
||||
if (source.kpis) target.kpis = source.kpis;
|
||||
// Arrays — replace whole so OWL's list diffing handles it cleanly
|
||||
// Arrays - replace whole so OWL's list diffing handles it cleanly
|
||||
for (const k of ["unassigned", "active", "team", "operators", "tanks"]) {
|
||||
if (Array.isArray(source[k])) target[k] = source[k];
|
||||
}
|
||||
@@ -168,10 +168,10 @@ export class ManagerDashboard extends Component {
|
||||
* Sort + filter the operator list for a specific step's dropdown.
|
||||
*
|
||||
* Buckets, top-down, each kept in original (alphabetical) order:
|
||||
* 1. Qualified for this role AND clocked in — primary picks
|
||||
* 2. Lead hands for this role AND clocked in — coverage picks
|
||||
* 3. Clocked in but NOT qualified — training mode
|
||||
* 4. Off-shift — greyed; only
|
||||
* 1. Qualified for this role AND clocked in - primary picks
|
||||
* 2. Lead hands for this role AND clocked in - coverage picks
|
||||
* 3. Clocked in but NOT qualified - training mode
|
||||
* 4. Off-shift - greyed; only
|
||||
* shown when hideOffShift is false
|
||||
*
|
||||
* Each option carries a `bucket` so the template can render a tiny
|
||||
@@ -199,7 +199,7 @@ export class ManagerDashboard extends Component {
|
||||
|
||||
/** Label that goes next to each option (after the name). */
|
||||
operatorBadge(op) {
|
||||
if (op.bucket === 1) return ""; // primary — no extra noise
|
||||
if (op.bucket === 1) return ""; // primary - no extra noise
|
||||
if (op.bucket === 2) return " · lead hand";
|
||||
if (op.bucket === 3) return " · training";
|
||||
return " · off-shift";
|
||||
@@ -288,7 +288,7 @@ export class ManagerDashboard extends Component {
|
||||
return ({'0': 'muted', '1': 'warning', '2': 'danger'})[p] || 'muted';
|
||||
}
|
||||
|
||||
// Open a list view of any model with an optional domain — used by
|
||||
// Open a list view of any model with an optional domain - used by
|
||||
// the new compliance KPI tiles (Missed Bakes / Open Holds / Stale
|
||||
// Steps / Locked / Pending QC / Draft Certs) so the manager can
|
||||
// drill in with one tap. v19.0.24.3.0.
|
||||
@@ -305,13 +305,13 @@ export class ManagerDashboard extends Component {
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
// Phase 4 tablet redesign — 4 sibling tabs
|
||||
// Phase 4 tablet redesign - 4 sibling tabs
|
||||
// ==================================================================
|
||||
|
||||
async setActiveTab(tab) {
|
||||
if (this.state.activeTab === tab) return;
|
||||
this.state.activeTab = tab;
|
||||
// Load the tab's data on first switch — subsequent ticks refresh
|
||||
// Load the tab's data on first switch - subsequent ticks refresh
|
||||
// via the auto-poll.
|
||||
await this.refreshActiveTab();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/** @odoo-module */
|
||||
/*
|
||||
* Sub 12b — Move Parts dialog (OWL).
|
||||
* Sub 12b - Move Parts dialog (OWL).
|
||||
*
|
||||
* Mirror of 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
|
||||
* hard-blocked OR required prompt blank - improvement over Steelhead's
|
||||
* silent disabled state (we show a tooltip listing blockers).
|
||||
*
|
||||
* Inline 'Resolve' button next to each blocker with a resolve_action.
|
||||
@@ -41,7 +41,7 @@ export class FpMovePartsDialog extends Component {
|
||||
blockers: [],
|
||||
committing: false,
|
||||
// Advanced fields (Transfer Type, To Location) stay collapsed
|
||||
// by default — the everyday flow is "advance all to the next
|
||||
// by default - the everyday flow is "advance all to the next
|
||||
// stage", which needs none of them. Keeps the dialog to a qty
|
||||
// confirm + SEND for the 95% case.
|
||||
showAdvanced: false,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module */
|
||||
/*
|
||||
* Sub 12b — Move Rack dialog (OWL).
|
||||
* Sub 12b - Move Rack dialog (OWL).
|
||||
*
|
||||
* Mirrors screens 11, 13, 14. Same shape as Move Parts but no
|
||||
* transition prompts (rack moves are rack-level). Title carries
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/** @odoo-module **/
|
||||
// =====================================================================
|
||||
// FpPlantKanban — sole Shop Floor OWL action (2026-05-25 onward).
|
||||
// FpPlantKanban - sole Shop Floor OWL action (2026-05-25 onward).
|
||||
// Mounts via the fp_plant_kanban client action. fp_shopfloor_landing
|
||||
// was retired the same day — its only unique feature (inline QR
|
||||
// was retired the same day - its only unique feature (inline QR
|
||||
// scanner) was ported here.
|
||||
//
|
||||
// Architecture:
|
||||
@@ -14,7 +14,7 @@
|
||||
// - Station pairing writes res.users.paired_work_centre_ids via
|
||||
// /fp/landing/pair_work_centre (replaces legacy localStorage
|
||||
// pairing)
|
||||
// - Per project rule 20, no String()/Number()/etc. in templates —
|
||||
// - Per project rule 20, no String()/Number()/etc. in templates -
|
||||
// all coercion happens here in JS-land.
|
||||
// =====================================================================
|
||||
|
||||
@@ -56,7 +56,7 @@ export class FpPlantKanban extends Component {
|
||||
loading: true,
|
||||
search: "",
|
||||
// QR scan drawer (text/wedge path). Camera path is owned by
|
||||
// the QrScanner component itself — it routes URLs internally.
|
||||
// the QrScanner component itself - it routes URLs internally.
|
||||
showScan: false,
|
||||
scanInput: "",
|
||||
});
|
||||
@@ -153,7 +153,7 @@ export class FpPlantKanban extends Component {
|
||||
|
||||
// ---- QR scan (text / wedge / manual paste path) -----------------------
|
||||
// Camera path is rendered by the inline <QrScanner/> component below
|
||||
// the header — it owns its own modal + decoder + URL routing. This
|
||||
// the header - it owns its own modal + decoder + URL routing. This
|
||||
// drawer is for FP-STATION:/FP-JOB:/FP-STEP: scanner-wedge codes typed
|
||||
// into the input.
|
||||
toggleScan() {
|
||||
@@ -214,7 +214,7 @@ export class FpPlantKanban extends Component {
|
||||
params: { job_id: res.id },
|
||||
target: "main",
|
||||
});
|
||||
return; // navigating away — skip the refresh
|
||||
return; // navigating away - skip the refresh
|
||||
} else if (res.model === "fp.job.step") {
|
||||
this.action.doAction({
|
||||
type: "ir.actions.client",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — Plant Overview Dashboard (OWL backend client action)
|
||||
// Fusion Plating - Plant Overview Dashboard (OWL backend client action)
|
||||
// Copyright 2026 Nexa Systems Inc.
|
||||
// License OPL-1 (Odoo Proprietary License v1.0)
|
||||
//
|
||||
@@ -26,10 +26,10 @@ import { QrScanner } from "./qr_scanner";
|
||||
import { FpMoveRackDialog } from "./move_rack_dialog";
|
||||
|
||||
// =============================================================================
|
||||
// TimerChip — per-card live elapsed-in-stage chip (v19.0.24.10.0)
|
||||
// TimerChip - per-card live elapsed-in-stage chip (v19.0.24.10.0)
|
||||
// =============================================================================
|
||||
// Old design read state.tickEpoch from inside the parent's getCardTimer(),
|
||||
// which forced OWL to mark the WHOLE component dirty every 5 seconds —
|
||||
// which forced OWL to mark the WHOLE component dirty every 5 seconds -
|
||||
// 389 cards re-rendering all at once, even though only the chip text
|
||||
// changes. That's what caused the "drop, wait 5s, card jumps back" feel
|
||||
// on a busy board: a tick fired mid-drop and froze the main thread.
|
||||
@@ -38,7 +38,7 @@ import { FpMoveRackDialog } from "./move_rack_dialog";
|
||||
// re-renders only itself, and has stable props from the parent. When a
|
||||
// card moves columns, OWL keeps the same TimerChip instance (matched by
|
||||
// t-key=card.id on the parent), so the interval keeps running across the
|
||||
// move — no remount, no flicker.
|
||||
// move - no remount, no flicker.
|
||||
class TimerChip extends Component {
|
||||
static template = "fusion_plating_shopfloor.TimerChip";
|
||||
static props = {
|
||||
@@ -50,7 +50,7 @@ class TimerChip extends Component {
|
||||
setup() {
|
||||
this.state = useState({ now: Date.now() });
|
||||
onMounted(() => {
|
||||
// 5s tick is plenty — the displayed text changes at minute
|
||||
// 5s tick is plenty - the displayed text changes at minute
|
||||
// resolution after the first 60s anyway.
|
||||
this._iv = setInterval(() => {
|
||||
this.state.now = Date.now();
|
||||
@@ -138,7 +138,7 @@ export class PlantOverview extends Component {
|
||||
this.state = useState({
|
||||
facilityName: "",
|
||||
columns: [],
|
||||
racks: [], // Sub 12b — Racks pane payload
|
||||
racks: [], // Sub 12b - Racks pane payload
|
||||
searchTerm: "",
|
||||
loading: false,
|
||||
lastRefresh: null,
|
||||
@@ -162,7 +162,7 @@ export class PlantOverview extends Component {
|
||||
await this.loadData();
|
||||
// Server data refresh every 30 seconds (catches changes from
|
||||
// other operators). Suppressed while a move is in flight or
|
||||
// for 30 s after the last drop — see _shouldSkipRefresh().
|
||||
// for 30 s after the last drop - see _shouldSkipRefresh().
|
||||
this._refreshInterval = setInterval(() => {
|
||||
if (!this._shouldSkipRefresh()) this.loadData();
|
||||
}, 30000);
|
||||
@@ -196,7 +196,7 @@ export class PlantOverview extends Component {
|
||||
this.state.facilityName = result.facility_name || "Plant 1";
|
||||
this.state.columns = result.columns || [];
|
||||
this.state.racks = result.racks || [];
|
||||
// Prefer the server-formatted timestamp — it's in the
|
||||
// Prefer the server-formatted timestamp - it's in the
|
||||
// FP-configured tz (fp_format → user.tz → company tz).
|
||||
// Browser tz fallback only kicks in if the endpoint
|
||||
// drops the field for some reason.
|
||||
@@ -216,7 +216,7 @@ export class PlantOverview extends Component {
|
||||
// ----- Search ------------------------------------------------------------
|
||||
//
|
||||
// Live search with a 200ms debounce. The user types, the cards update
|
||||
// as they go — no "press Enter" leap of faith. Debounce keeps us off
|
||||
// as they go - no "press Enter" leap of faith. Debounce keeps us off
|
||||
// the network on every keystroke when someone types fast.
|
||||
|
||||
onSearchInput(ev) {
|
||||
@@ -224,12 +224,12 @@ export class PlantOverview extends Component {
|
||||
this._debouncedSearch();
|
||||
}
|
||||
|
||||
// ===================================================== Sub 12b — racks
|
||||
// ===================================================== Sub 12b - racks
|
||||
|
||||
openMoveRackDialog(rackId, toStepId) {
|
||||
if (!toStepId) {
|
||||
this.notification.add(
|
||||
"No destination step available — rack is at the last "
|
||||
"No destination step available - rack is at the last "
|
||||
+ "step of its job. Use the rack form to manually pick "
|
||||
+ "a new step.",
|
||||
{ type: "warning" });
|
||||
@@ -247,7 +247,7 @@ export class PlantOverview extends Component {
|
||||
}
|
||||
|
||||
onSearchKey(ev) {
|
||||
// Enter still works — fires immediately, skipping the debounce.
|
||||
// Enter still works - fires immediately, skipping the debounce.
|
||||
if (ev.key === "Enter") {
|
||||
if (this._searchTimer) clearTimeout(this._searchTimer);
|
||||
this.loadData();
|
||||
@@ -288,7 +288,7 @@ export class PlantOverview extends Component {
|
||||
}
|
||||
|
||||
onCardDragStart(card, col, ev) {
|
||||
// Mark the kanban as actively dragging — CSS rule freezes all
|
||||
// Mark the kanban as actively dragging - CSS rule freezes all
|
||||
// animations + transitions on descendants. Without this the
|
||||
// browser was paint-locked fighting 27 chip pulses + transitions
|
||||
// during the drop, causing the 5+ second visual freeze.
|
||||
@@ -362,7 +362,7 @@ export class PlantOverview extends Component {
|
||||
const body = ev.currentTarget;
|
||||
if (body && !body.contains(ev.relatedTarget)) {
|
||||
body.classList.remove("o_fp_drop_target");
|
||||
// Only remove placeholder if we left the body entirely —
|
||||
// Only remove placeholder if we left the body entirely -
|
||||
// otherwise the child card enter fires dragleave on the body
|
||||
this._removePlaceholder();
|
||||
}
|
||||
@@ -370,7 +370,7 @@ export class PlantOverview extends Component {
|
||||
|
||||
async onColDrop(col, ev) {
|
||||
// Instrumentation (v19.0.24.11.0). Keeping these console.time
|
||||
// markers permanent — they cost ~0.01ms each, make every freeze
|
||||
// markers permanent - they cost ~0.01ms each, make every freeze
|
||||
// visible in DevTools, and let the user paste a real timing back
|
||||
// when something feels slow. Look in Console for "[fp drop] …".
|
||||
const _t0 = performance.now();
|
||||
@@ -435,7 +435,7 @@ export class PlantOverview extends Component {
|
||||
...sourceCards.slice(0, cardOriginalIdx),
|
||||
...sourceCards.slice(cardOriginalIdx + 1),
|
||||
];
|
||||
// New target array with the card on top — it just got
|
||||
// New target array with the card on top - it just got
|
||||
// moved, the supervisor's eye expects it there. Server
|
||||
// sort will re-position on the next refresh.
|
||||
this.state.columns[targetColIdx].cards = [
|
||||
@@ -449,7 +449,7 @@ export class PlantOverview extends Component {
|
||||
|
||||
// Force a paint frame BEFORE awaiting the RPC. Without this,
|
||||
// OWL's render is queued but the browser may not paint until
|
||||
// after the await rpc resolves — which means the user sees the
|
||||
// after the await rpc resolves - which means the user sees the
|
||||
// card "freeze" until the network roundtrip completes.
|
||||
// requestAnimationFrame schedules the callback right before the
|
||||
// next paint, so by the time we await, the card is on screen.
|
||||
@@ -491,7 +491,7 @@ export class PlantOverview extends Component {
|
||||
`Moved to ${col.work_center_name}`,
|
||||
{ type: "success" },
|
||||
);
|
||||
// Server confirmed — clear the dim. Locate the card in
|
||||
// Server confirmed - clear the dim. Locate the card in
|
||||
// its (now-target) column and rebuild the array WITHOUT
|
||||
// the _optimistic flag so OWL repaints at full opacity.
|
||||
if (movedCard && targetColIdx >= 0) {
|
||||
@@ -523,7 +523,7 @@ export class PlantOverview extends Component {
|
||||
console.timeEnd("[fp drop] total");
|
||||
const totalMs = (performance.now() - _t0).toFixed(0);
|
||||
if (totalMs > 200) {
|
||||
console.warn(`[fp drop] SLOW DROP: ${totalMs}ms — paste this in chat`);
|
||||
console.warn(`[fp drop] SLOW DROP: ${totalMs}ms - paste this in chat`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — Process Tree (horizontal hierarchical view)
|
||||
// Fusion Plating - Process Tree (horizontal hierarchical view)
|
||||
// Copyright 2026 Nexa Systems Inc. · License OPL-1
|
||||
//
|
||||
// Renders an fp.job's recipe (recipe → sub_process → operation → step) as a
|
||||
@@ -13,11 +13,11 @@
|
||||
// design is unchanged.
|
||||
//
|
||||
// Action context:
|
||||
// job_id — required; the fp.job whose recipe to render
|
||||
// production_id — legacy alias for job_id (still accepted)
|
||||
// back_step_id — optional; if set, the back button returns to
|
||||
// job_id - required; the fp.job whose recipe to render
|
||||
// production_id - legacy alias for job_id (still accepted)
|
||||
// back_step_id - optional; if set, the back button returns to
|
||||
// that step's form instead of Plant Overview
|
||||
// back_workorder_id — legacy alias for back_step_id
|
||||
// back_workorder_id - legacy alias for back_step_id
|
||||
// =============================================================================
|
||||
|
||||
import { Component, useState, onMounted } from "@odoo/owl";
|
||||
@@ -114,7 +114,7 @@ export class ProcessTree extends Component {
|
||||
// ---- Navigation ---------------------------------------------------------
|
||||
|
||||
onNodeClick(node) {
|
||||
// Operation cards with a matching fp.job.step are clickable —
|
||||
// Operation cards with a matching fp.job.step are clickable -
|
||||
// they open the underlying step form. node.workorder_id is the
|
||||
// legacy template key that now carries the step id.
|
||||
const stepId = node && (node.step_id || node.workorder_id);
|
||||
@@ -131,7 +131,7 @@ export class ProcessTree extends Component {
|
||||
}
|
||||
|
||||
onBack() {
|
||||
// Try action.restore() first — that pops the Process Tree
|
||||
// Try action.restore() first - that pops the Process Tree
|
||||
// off the breadcrumb stack and returns the user to the WO
|
||||
// (or Step) they came from. Without this, doAction pushes a
|
||||
// NEW act_window each time and the breadcrumb keeps growing
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — Reusable QR Scanner OWL Component
|
||||
// Fusion Plating - Reusable QR Scanner OWL Component
|
||||
// Copyright 2026 Nexa Systems Inc. · License OPL-1
|
||||
//
|
||||
// Decoder selection — strongest available wins:
|
||||
// Decoder selection - strongest available wins:
|
||||
// 1. Native BarcodeDetector API (Android Chrome, iOS Safari 17+, desktop
|
||||
// Chrome / Edge — fastest, hardware
|
||||
// Chrome / Edge - fastest, hardware
|
||||
// accelerated, no JS in the hot path)
|
||||
// 2. Vendored jsQR fallback (every other browser including iOS
|
||||
// Safari < 17 and the in-app webviews
|
||||
@@ -13,7 +13,7 @@
|
||||
// which is what we hit in practice on
|
||||
// phones today)
|
||||
// 3. Manual paste (last resort: HTTP origin or no camera
|
||||
// permission — typing the URL still
|
||||
// permission - typing the URL still
|
||||
// works)
|
||||
//
|
||||
// The component renders a single button. On click, opens a modal that
|
||||
@@ -24,7 +24,7 @@
|
||||
// fp.job form via the action service.
|
||||
//
|
||||
// Used by Manager Desk, Tablet Station, Plant Overview, and Process Tree
|
||||
// headers — see each component's `static components = { QrScanner }`.
|
||||
// headers - see each component's `static components = { QrScanner }`.
|
||||
// =============================================================================
|
||||
|
||||
import { Component, useState, useRef, onWillUnmount } from "@odoo/owl";
|
||||
@@ -54,7 +54,7 @@ export class QrScanner extends Component {
|
||||
error: null,
|
||||
manualUrl: "",
|
||||
detected: "", // last decoded value (for user feedback)
|
||||
// canScan / decoder are recomputed in open() — don't trust
|
||||
// canScan / decoder are recomputed in open() - don't trust
|
||||
// setup-time values because vendored libs may attach to
|
||||
// window asynchronously after the bundle finishes parsing.
|
||||
canScan: false,
|
||||
@@ -74,18 +74,18 @@ export class QrScanner extends Component {
|
||||
|
||||
/**
|
||||
* Check what decoder is available right now and update state. Run
|
||||
* at every open() — not just setup() — because a stale bundle in
|
||||
* at every open() - not just setup() - because a stale bundle in
|
||||
* the browser cache can flip results between page loads.
|
||||
*
|
||||
* Preference order:
|
||||
* 1. ZXing-js (window.ZXing.BrowserMultiFormatReader) — the most
|
||||
* 1. ZXing-js (window.ZXing.BrowserMultiFormatReader) - the most
|
||||
* tolerant; handles perspective skew, motion blur, and glare
|
||||
* that defeat jsQR on phone cameras. This is the default.
|
||||
* 2. Native BarcodeDetector — fast, hardware-backed, but only
|
||||
* 2. Native BarcodeDetector - fast, hardware-backed, but only
|
||||
* available on Android Chrome and iOS Safari 17+. Skipped
|
||||
* now that ZXing is the primary path; left as a code branch
|
||||
* in case ZXing fails to load.
|
||||
* 3. jsQR — kept as a last-resort JS fallback.
|
||||
* 3. jsQR - kept as a last-resort JS fallback.
|
||||
*/
|
||||
_detectCapabilities() {
|
||||
const hasZXing = typeof window !== "undefined"
|
||||
@@ -107,7 +107,7 @@ export class QrScanner extends Component {
|
||||
"Decoder: " + this.state.decoder +
|
||||
(hasNative ? " (native)" : "") +
|
||||
(!hasNative && hasJsQR ? " (jsQR)" : "") +
|
||||
(!this.state.canScan ? " — paste URL below" : "")
|
||||
(!this.state.canScan ? " - paste URL below" : "")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ export class QrScanner extends Component {
|
||||
|
||||
async _startCamera() {
|
||||
if (!this.state.canScan) {
|
||||
// No decoder at all — paste UI is the only path.
|
||||
// No decoder at all - paste UI is the only path.
|
||||
return;
|
||||
}
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
@@ -205,7 +205,7 @@ export class QrScanner extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode loop using the browser's BarcodeDetector. Cheapest path —
|
||||
* Decode loop using the browser's BarcodeDetector. Cheapest path -
|
||||
* the browser does the work off the JS thread. Only runs on
|
||||
* Android Chrome, iOS Safari 17+, and desktop Chrome / Edge.
|
||||
*/
|
||||
@@ -225,7 +225,7 @@ export class QrScanner extends Component {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Decode errors are noisy and recoverable — try the
|
||||
// Decode errors are noisy and recoverable - try the
|
||||
// next frame. Real failures (camera revoked, etc.)
|
||||
// surface via _startCamera's catch.
|
||||
}
|
||||
@@ -239,7 +239,7 @@ export class QrScanner extends Component {
|
||||
* <video> element (already wired to the getUserMedia stream)
|
||||
* straight into ZXing's continuous reader, which manages its
|
||||
* own per-frame timing and decode pipeline (HybridBinarizer +
|
||||
* perspective transform) — the same algorithm family the iOS
|
||||
* perspective transform) - the same algorithm family the iOS
|
||||
* Camera app uses internally.
|
||||
*
|
||||
* The vendored bundle exposes these instance methods on
|
||||
@@ -258,11 +258,11 @@ export class QrScanner extends Component {
|
||||
this.decodeLoopActive = true;
|
||||
const v = this.videoRef.el;
|
||||
if (!v) {
|
||||
this.state.statusLine = "Decoder: zxing — video element missing";
|
||||
this.state.statusLine = "Decoder: zxing - video element missing";
|
||||
return;
|
||||
}
|
||||
const Z = window.ZXing;
|
||||
// Pass hints via the constructor — assignment to .hints
|
||||
// Pass hints via the constructor - assignment to .hints
|
||||
// afterward doesn't work because decodeBitmap reads from
|
||||
// this._hints (set by MultiFormatReader.setHints during
|
||||
// construction). TRY_HARDER makes the QR finder more
|
||||
@@ -272,16 +272,16 @@ export class QrScanner extends Component {
|
||||
hints.set(ZXING_HINT_POSSIBLE_FORMATS, [Z.BarcodeFormat.QR_CODE]);
|
||||
}
|
||||
hints.set(ZXING_HINT_TRY_HARDER, true);
|
||||
// Second arg is timeBetweenScansMillis — drop from 500 default
|
||||
// Second arg is timeBetweenScansMillis - drop from 500 default
|
||||
// to 100 so we attempt ~10 decodes/sec instead of ~2.
|
||||
const reader = new Z.BrowserMultiFormatReader(hints, 100);
|
||||
this._zxingReader = reader;
|
||||
|
||||
// Live status — ZXing manages its own timing internally so we
|
||||
// Live status - ZXing manages its own timing internally so we
|
||||
// count callbacks instead of rAF ticks. Hits is what matters.
|
||||
let callbacks = 0;
|
||||
let lastStatus = 0;
|
||||
let lastResult = "—";
|
||||
let lastResult = "-";
|
||||
const refreshStatus = () => {
|
||||
const now = performance.now();
|
||||
if (now - lastStatus > 400) {
|
||||
@@ -310,7 +310,7 @@ export class QrScanner extends Component {
|
||||
lastResult = "no_code";
|
||||
} else if (name.indexOf("Checksum") >= 0 || name.indexOf("Format") >= 0) {
|
||||
// Found something QR-shaped but couldn't read it
|
||||
// (blurry / damaged) — keep trying next frame.
|
||||
// (blurry / damaged) - keep trying next frame.
|
||||
lastResult = "partial";
|
||||
} else {
|
||||
lastResult = "err:" + (err.message || name).slice(0, 40);
|
||||
@@ -332,7 +332,7 @@ export class QrScanner extends Component {
|
||||
*
|
||||
* Throttled to one decode per ~100ms to stay responsive without
|
||||
* pegging mid-range phones. Updates a live status line so the
|
||||
* operator can see exactly what the loop is doing — frames seen,
|
||||
* operator can see exactly what the loop is doing - frames seen,
|
||||
* decode attempts, video resolution. Critical for diagnosing
|
||||
* "scan does nothing" reports without round-tripping through
|
||||
* Safari Web Inspector.
|
||||
@@ -341,7 +341,7 @@ export class QrScanner extends Component {
|
||||
this.decodeLoopActive = true;
|
||||
const v = this.videoRef.el;
|
||||
if (!v) {
|
||||
this.state.statusLine = "Decoder: jsqr — video element missing";
|
||||
this.state.statusLine = "Decoder: jsqr - video element missing";
|
||||
return;
|
||||
}
|
||||
if (!this._canvas) {
|
||||
@@ -352,7 +352,7 @@ export class QrScanner extends Component {
|
||||
let attempts = 0;
|
||||
let lastDecode = 0;
|
||||
let lastStatus = 0;
|
||||
let lastResult = "—"; // "found" | "no_code" | "empty" | error msg
|
||||
let lastResult = "-"; // "found" | "no_code" | "empty" | error msg
|
||||
let firstNonZeroPixel = -1; // sanity check that drawImage works
|
||||
const MIN_INTERVAL_MS = 100;
|
||||
const STATUS_INTERVAL_MS = 500;
|
||||
@@ -369,7 +369,7 @@ export class QrScanner extends Component {
|
||||
try {
|
||||
const w = v.videoWidth;
|
||||
const h = v.videoHeight;
|
||||
// Use the native video resolution directly — no
|
||||
// Use the native video resolution directly - no
|
||||
// downscaling. jsQR's runtime cost is acceptable
|
||||
// even at 1080p, and downsampling can blur the
|
||||
// finder patterns just enough to defeat detection
|
||||
@@ -435,7 +435,7 @@ export class QrScanner extends Component {
|
||||
* (the <input type=file capture=environment> in the template
|
||||
* backs this).
|
||||
*
|
||||
* Works on every browser that supports file inputs — including
|
||||
* Works on every browser that supports file inputs - including
|
||||
* iOS Chrome / Safari, where the live-video decode path in ZXing
|
||||
* has been unreliable. iOS hands us a JPEG that's been autofocused
|
||||
* and properly exposed; we just need to run ONE decode on it
|
||||
@@ -500,7 +500,7 @@ export class QrScanner extends Component {
|
||||
}
|
||||
hints.set(ZXING_HINT_TRY_HARDER, true);
|
||||
const reader = new Z.BrowserMultiFormatReader(hints);
|
||||
// decodeFromImageElement / decodeFromCanvas — try the
|
||||
// decodeFromImageElement / decodeFromCanvas - try the
|
||||
// canvas-friendly path: build a luminance source +
|
||||
// binary bitmap manually and call MultiFormatReader.
|
||||
const luminance = new Z.HTMLCanvasElementLuminanceSource(canvas);
|
||||
@@ -512,7 +512,7 @@ export class QrScanner extends Component {
|
||||
const text = decoded && (decoded.getText ? decoded.getText() : decoded.text);
|
||||
if (text) return text;
|
||||
} catch (e) {
|
||||
// ZXing miss — fall through to jsQR.
|
||||
// ZXing miss - fall through to jsQR.
|
||||
}
|
||||
}
|
||||
if (typeof window.jsQR === "function") {
|
||||
@@ -534,7 +534,7 @@ export class QrScanner extends Component {
|
||||
* Route a decoded value to the right backend page.
|
||||
*
|
||||
* Stickers encode either /fp/job/<fp.job.id> (new) or
|
||||
* /fp/wo/<mrp.production.id|mrp.workorder.id> (legacy — still on
|
||||
* /fp/wo/<mrp.production.id|mrp.workorder.id> (legacy - still on
|
||||
* physical boxes from before the migration). Both URLs are
|
||||
* handled by server-side controllers (job_scan.py / wo_scan.py)
|
||||
* that resolve the correct record and redirect to its form.
|
||||
@@ -587,7 +587,7 @@ export class QrScanner extends Component {
|
||||
this._stopCamera();
|
||||
this.state.open = false;
|
||||
this.notification.add("Opening " + target, { type: "success" });
|
||||
// Full navigation — the server-side controller resolves the id
|
||||
// Full navigation - the server-side controller resolves the id
|
||||
// to the right record (works for both new fp.job stickers and
|
||||
// legacy mrp.production / mrp.workorder stickers).
|
||||
window.location.href = target;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module */
|
||||
/*
|
||||
* Sub 12b — Rack Parts sub-dialog (OWL).
|
||||
* Sub 12b - Rack Parts sub-dialog (OWL).
|
||||
*
|
||||
* Mirrors Steelhead screens 7-8. Searchable empty-rack picker,
|
||||
* QR-scan input, Unit + Amount fields. Save assigns the step → rack;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — Activity Tracker (shared OWL service)
|
||||
// Fusion Plating - Activity Tracker (shared OWL service)
|
||||
//
|
||||
// Watches the document for pointer/touch/keydown/visibility events and
|
||||
// tracks lastActiveAt. FpTabletLock reads getSecondsUntilLock() once per
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// fpRpc — thin wrapper around @web/core/network/rpc
|
||||
// fpRpc - thin wrapper around @web/core/network/rpc
|
||||
//
|
||||
// Post-Phase-G of the tablet PIN session redesign: this no longer
|
||||
// injects tablet_tech_id (the session uid IS the tech). Kept as a
|
||||
|
||||
@@ -87,7 +87,7 @@ export const tabletSessionManager = {
|
||||
await rpc("/fp/tablet/lock_session", { reason });
|
||||
} catch (e) {
|
||||
// Even if the RPC fails, force a reload to drop the
|
||||
// current session state — the cron will clean up.
|
||||
// current session state - the cron will clean up.
|
||||
console.warn("lock_session RPC failed; reloading anyway", e);
|
||||
}
|
||||
window.location.reload();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — Shop Floor Tablet (OWL backend client action)
|
||||
// Fusion Plating - Shop Floor Tablet (OWL backend client action)
|
||||
// Copyright 2026 Nexa Systems Inc.
|
||||
// License OPL-1 (Odoo Proprietary License v1.0)
|
||||
//
|
||||
@@ -35,7 +35,7 @@ export class ShopfloorTablet extends Component {
|
||||
this.dialog = useService("dialog");
|
||||
this.scanInput = useRef("scanInput");
|
||||
|
||||
// Sub 12b — listen for the rack-resolution custom event fired
|
||||
// Sub 12b - listen for the rack-resolution custom event fired
|
||||
// from inside FpMovePartsDialog when the operator hits the
|
||||
// 'Resolve' button on a rack-required blocker.
|
||||
this._onResolveRack = (ev) => {
|
||||
@@ -98,14 +98,14 @@ export class ShopfloorTablet extends Component {
|
||||
_tickElapsed() {
|
||||
const a = this.state.overview && this.state.overview.active_wo;
|
||||
if (!a || !a.date_started_iso) {
|
||||
this.state.activeElapsed = "—";
|
||||
this.state.activeElapsed = "-";
|
||||
return;
|
||||
}
|
||||
// Odoo gives "YYYY-MM-DD HH:MM:SS" in UTC; turn into ISO Z.
|
||||
const isoUtc = a.date_started_iso.replace(" ", "T") + "Z";
|
||||
const startMs = Date.parse(isoUtc);
|
||||
if (isNaN(startMs)) {
|
||||
this.state.activeElapsed = "—";
|
||||
this.state.activeElapsed = "-";
|
||||
return;
|
||||
}
|
||||
let s = Math.max(0, Math.floor((Date.now() - startMs) / 1000));
|
||||
@@ -131,7 +131,7 @@ export class ShopfloorTablet extends Component {
|
||||
this.state.overview = payload;
|
||||
}
|
||||
} catch (err) {
|
||||
// silent — next tick will retry
|
||||
// silent - next tick will retry
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ export class ShopfloorTablet extends Component {
|
||||
this.state.loading = false;
|
||||
return;
|
||||
} else {
|
||||
this.setMessage(`Scanned ${result.model} — ${result.name || ""}`, "info");
|
||||
this.setMessage(`Scanned ${result.model} - ${result.name || ""}`, "info");
|
||||
}
|
||||
} catch (err) {
|
||||
this.setMessage(`Scan error: ${err.message || err}`, "danger");
|
||||
@@ -252,7 +252,7 @@ export class ShopfloorTablet extends Component {
|
||||
try {
|
||||
const res = await rpc("/fp/shopfloor/start_wo", { workorder_id: woId });
|
||||
if (res && res.ok) {
|
||||
this.setMessage("Work order started — timer running.", "success");
|
||||
this.setMessage("Work order started - timer running.", "success");
|
||||
} else if (res && res.error) {
|
||||
this.setMessage(res.error, "danger");
|
||||
}
|
||||
@@ -279,8 +279,8 @@ export class ShopfloorTablet extends Component {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------- Qty / scrap
|
||||
// Bump qty_done from the tablet — operator confirms +1 part finished.
|
||||
// Also bump scrap with a confirm prompt — auto-spawns a Hold via S17.
|
||||
// Bump qty_done from the tablet - operator confirms +1 part finished.
|
||||
// Also bump scrap with a confirm prompt - auto-spawns a Hold via S17.
|
||||
async onBumpQtyDone(jobId) {
|
||||
if (!jobId) return;
|
||||
try {
|
||||
@@ -301,7 +301,7 @@ export class ShopfloorTablet extends Component {
|
||||
|
||||
async onBumpScrap(jobId) {
|
||||
if (!jobId) return;
|
||||
// Block-and-prompt — scrap is a quality event, force the operator
|
||||
// Block-and-prompt - scrap is a quality event, force the operator
|
||||
// to acknowledge and ideally type a brief reason.
|
||||
const reason = window.prompt(
|
||||
"Reason for scrap (e.g. 'dropped during de-rack', 'flash burn'):",
|
||||
@@ -328,7 +328,7 @@ export class ShopfloorTablet extends Component {
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
// Open a pending QC directly from the banner — same deep-link the
|
||||
// Open a pending QC directly from the banner - same deep-link the
|
||||
// FP-QC scan path uses.
|
||||
onOpenPendingQc(qcId) {
|
||||
if (!qcId) return;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module */
|
||||
/*
|
||||
* Sub 12b — Stop User Labor Timer dialog (OWL).
|
||||
* Sub 12b - Stop User Labor Timer dialog (OWL).
|
||||
*
|
||||
* Mirrors screen 10. Opens with state already at 'stopped' (server-side
|
||||
* flip on /labor_timer/stop), pre-fills billed_* from accrued. Operator
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — FpTabletLock (top-level wrapper)
|
||||
// Fusion Plating - FpTabletLock (top-level wrapper)
|
||||
//
|
||||
// Mounted by Landing / Workspace / Manager Dashboard as their outermost
|
||||
// element. Renders the lock screen (tile grid + PIN pad) when no tech
|
||||
@@ -43,7 +43,7 @@ export class FpTabletLock extends Component {
|
||||
selectedTileUserId: null,
|
||||
idleSecondsRemaining: null,
|
||||
loadingTiles: false,
|
||||
// 2026-05-24 redesign — clock + company branding
|
||||
// 2026-05-24 redesign - clock + company branding
|
||||
// Seeded synchronously so the first render shows real values
|
||||
// (no flash of empty content). tz=null on first render falls
|
||||
// back to browser tz; _loadTiles() then sets state.tz from
|
||||
@@ -61,12 +61,12 @@ export class FpTabletLock extends Component {
|
||||
// the kiosk (= locked).
|
||||
kioskUid: null,
|
||||
currentUid: null,
|
||||
// Spec 2026-05-25 — PIN self-service wizard states
|
||||
// 'pin' — default keypad (has-PIN user)
|
||||
// 'request_code' — "Send temp PIN" button screen
|
||||
// 'enter_temp_code' — 4-cell pad for emailed code
|
||||
// 'set_new_pin' — 4-cell pad — choose new PIN
|
||||
// 'confirm_new_pin' — 4-cell pad — confirm new PIN
|
||||
// Spec 2026-05-25 - PIN self-service wizard states
|
||||
// 'pin' - default keypad (has-PIN user)
|
||||
// 'request_code' - "Send temp PIN" button screen
|
||||
// 'enter_temp_code' - 4-cell pad for emailed code
|
||||
// 'set_new_pin' - 4-cell pad - choose new PIN
|
||||
// 'confirm_new_pin' - 4-cell pad - confirm new PIN
|
||||
mode: 'pin',
|
||||
failedAttempts: 0, // resets on tile re-select
|
||||
maskedEmail: '',
|
||||
@@ -81,7 +81,7 @@ export class FpTabletLock extends Component {
|
||||
onMounted(async () => {
|
||||
await this._loadTiles();
|
||||
this._tick = setInterval(() => this._checkIdle(), 1000);
|
||||
// Heartbeat ping every 60s — for forensic visibility. Only
|
||||
// Heartbeat ping every 60s - for forensic visibility. Only
|
||||
// ping while a tech is logged in; on the kiosk session this
|
||||
// is just noise.
|
||||
this._ping = setInterval(() => {
|
||||
@@ -90,7 +90,7 @@ export class FpTabletLock extends Component {
|
||||
rpc("/fp/tablet/ping", {}).catch(() => {});
|
||||
}
|
||||
}, 60000);
|
||||
// Clock tick — update visible HH:MM and date label every 60s.
|
||||
// Clock tick - update visible HH:MM and date label every 60s.
|
||||
// 60s is enough; the displayed precision is minute-level only.
|
||||
this._clockInterval = setInterval(() => {
|
||||
const now = new Date();
|
||||
@@ -117,7 +117,7 @@ export class FpTabletLock extends Component {
|
||||
|
||||
get isLocked() {
|
||||
// The browser session itself tells us whether a tech is
|
||||
// unlocked — current_uid != kiosk_uid means unlocked.
|
||||
// unlocked - current_uid != kiosk_uid means unlocked.
|
||||
return !this.state.currentUid
|
||||
|| this.state.currentUid === this.state.kioskUid;
|
||||
}
|
||||
@@ -125,14 +125,14 @@ export class FpTabletLock extends Component {
|
||||
async _loadTiles() {
|
||||
this.state.loadingTiles = true;
|
||||
try {
|
||||
// 2026-05-25 — the legacy fp_landing_station_id localStorage
|
||||
// 2026-05-25 - the legacy fp_landing_station_id localStorage
|
||||
// key (set by the now-deleted fp_shopfloor_landing component)
|
||||
// is purged on read. Sending its stale value to /fp/tablet/tiles
|
||||
// caused the kiosk session to AccessError on shopfloor.station
|
||||
// and return an empty tile list. plant_kanban pairs to
|
||||
// work_centre server-side (paired_work_centre_ids) so the
|
||||
// kiosk-rendered lock screen no longer needs per-tablet pairing
|
||||
// for tile scoping — all shop-branch users render.
|
||||
// for tile scoping - all shop-branch users render.
|
||||
try { localStorage.removeItem("fp_landing_station_id"); } catch {}
|
||||
try { localStorage.removeItem("fp_landing_mode"); } catch {}
|
||||
const res = await rpc("/fp/tablet/tiles", { station_id: null });
|
||||
@@ -156,7 +156,7 @@ export class FpTabletLock extends Component {
|
||||
}));
|
||||
}
|
||||
} catch (err) {
|
||||
// Quiet fail — tile grid stays empty; user gets prompted
|
||||
// Quiet fail - tile grid stays empty; user gets prompted
|
||||
} finally {
|
||||
this.state.loadingTiles = false;
|
||||
}
|
||||
@@ -186,7 +186,7 @@ export class FpTabletLock extends Component {
|
||||
onTileClick(userId) {
|
||||
this.state.selectedTileUserId = userId;
|
||||
this.state.failedAttempts = 0;
|
||||
// Spec D1 — if user has no PIN, jump straight to the
|
||||
// Spec D1 - if user has no PIN, jump straight to the
|
||||
// "Send temporary PIN" screen. Otherwise show the keypad.
|
||||
const tile = this._tileForUser(userId);
|
||||
this.state.mode = (tile && tile.has_pin) ? 'pin' : 'request_code';
|
||||
@@ -220,7 +220,7 @@ export class FpTabletLock extends Component {
|
||||
// navigate while we're tearing down.
|
||||
return { ok: true, reloading: true };
|
||||
}
|
||||
// Wrong PIN — bump client-side counter for "Forgot?" gating
|
||||
// Wrong PIN - bump client-side counter for "Forgot?" gating
|
||||
this.state.failedAttempts += 1;
|
||||
return {
|
||||
ok: false,
|
||||
@@ -251,7 +251,7 @@ export class FpTabletLock extends Component {
|
||||
|
||||
_formatTime(d, tz) {
|
||||
// 12-hour H:MM AM/PM in the FP-configured tz (falls back to
|
||||
// browser tz if tz is null — first paint before _loadTiles).
|
||||
// browser tz if tz is null - first paint before _loadTiles).
|
||||
// Per project rule 20 this MUST live in JS, not the template.
|
||||
// Hour is NOT zero-padded (1:05 PM, not 01:05 PM) to match
|
||||
// phone-clock idiom.
|
||||
@@ -275,7 +275,7 @@ export class FpTabletLock extends Component {
|
||||
|
||||
tileStyle(tile) {
|
||||
// Inline animation-delay so each tile's entrance staggers.
|
||||
// Returned as a string per project rule 20 — the template can't
|
||||
// Returned as a string per project rule 20 - the template can't
|
||||
// call String() inside t-att-style.
|
||||
return "animation-delay: " + tile.animDelay + "ms";
|
||||
}
|
||||
@@ -286,16 +286,16 @@ export class FpTabletLock extends Component {
|
||||
: "o_fp_lock_avatar";
|
||||
}
|
||||
|
||||
// ===== Spec 2026-05-25 — PIN self-service wizard handlers =====
|
||||
// ===== Spec 2026-05-25 - PIN self-service wizard handlers =====
|
||||
|
||||
/** "Forgot? Reset PIN via email" button click — from PIN entry screen
|
||||
/** "Forgot? Reset PIN via email" button click - from PIN entry screen
|
||||
* after 3 fails. */
|
||||
onForgotPinClick() {
|
||||
this.state.mode = 'request_code';
|
||||
this.state.statusMessage = '';
|
||||
}
|
||||
|
||||
/** "Send temporary PIN" button click — from request_code screen. */
|
||||
/** "Send temporary PIN" button click - from request_code screen. */
|
||||
async onSendCodeClick() {
|
||||
try {
|
||||
const res = await rpc("/fp/tablet/request_reset_code", {
|
||||
@@ -403,7 +403,7 @@ export class FpTabletLock extends Component {
|
||||
window.location.reload();
|
||||
return { ok: true, reloading: true };
|
||||
}
|
||||
// PIN set but unlock failed — user can tap their tile + enter
|
||||
// PIN set but unlock failed - user can tap their tile + enter
|
||||
// the new PIN manually
|
||||
this.state.statusMessage = 'PIN set. Tap your tile and enter the new PIN to log in.';
|
||||
this.onPinCancel();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// =============================================================================
|
||||
// Fusion Plating — Shop Floor Design System (v2, 2026-04)
|
||||
// Fusion Plating - Shop Floor Design System (v2, 2026-04)
|
||||
// Copyright 2026 Nexa Systems Inc. · License OPL-1
|
||||
//
|
||||
// Design philosophy:
|
||||
// * NO card borders — depth comes from elevation (shadow) only
|
||||
// * NO card borders - depth comes from elevation (shadow) only
|
||||
// * Generous whitespace, calm surfaces, one accent colour
|
||||
// * Semantic colours (success/warning/danger) reserved for STATUS — not
|
||||
// * Semantic colours (success/warning/danger) reserved for STATUS - not
|
||||
// decoration
|
||||
// * Type-first hierarchy: big headings + big numbers + small helpers
|
||||
// * Every value resolves from Odoo CSS custom properties, so light
|
||||
@@ -31,11 +31,11 @@ $fp-radius-lg : 20px;
|
||||
$fp-radius-xl : 28px;
|
||||
$fp-radius-pill: 999px;
|
||||
|
||||
// ---------- Surfaces — COMPILE-TIME branch on Odoo's dark scheme -------------
|
||||
// ---------- Surfaces - COMPILE-TIME branch on Odoo's dark scheme -------------
|
||||
//
|
||||
// Odoo 19 compiles TWO asset bundles: web.assets_backend (light) and
|
||||
// web.assets_web_dark (dark). The two bundles differ only in the value
|
||||
// of the SCSS variable $o-webclient-color-scheme — `bright` for light,
|
||||
// of the SCSS variable $o-webclient-color-scheme - `bright` for light,
|
||||
// `dark` for dark (defined in primary_variables.scss /
|
||||
// primary_variables.dark.scss in web_enterprise).
|
||||
//
|
||||
@@ -58,7 +58,7 @@ $_fp-ink-soft-hex : #4b5563;
|
||||
$_fp-ink-mute-hex : #6b7280;
|
||||
$_fp-ink-faint-hex : #9ca3af;
|
||||
|
||||
// Dark palette — engaged when the dark bundle is compiled
|
||||
// Dark palette - engaged when the dark bundle is compiled
|
||||
@if $o-webclient-color-scheme == dark {
|
||||
$_fp-page-hex : #1a1d21 !global;
|
||||
$_fp-card-hex : #22262d !global;
|
||||
@@ -71,7 +71,7 @@ $_fp-ink-faint-hex : #9ca3af;
|
||||
$_fp-ink-faint-hex : #5a606b !global;
|
||||
}
|
||||
|
||||
// Public tokens — CSS custom property fallback chain remains so a
|
||||
// Public tokens - CSS custom property fallback chain remains so a
|
||||
// deployment can still override via --fp-* without touching SCSS.
|
||||
$fp-page : var(--fp-page-bg, $_fp-page-hex);
|
||||
$fp-card : var(--fp-card-bg, $_fp-card-hex);
|
||||
@@ -83,7 +83,7 @@ $fp-ink-soft : var(--fp-ink-soft, $_fp-ink-soft-hex);
|
||||
$fp-ink-mute : var(--fp-ink-mute, $_fp-ink-mute-hex);
|
||||
$fp-ink-faint : var(--fp-ink-faint, $_fp-ink-faint-hex);
|
||||
|
||||
// Action colour — Odoo's primary. Same in both bundles (brand purple).
|
||||
// Action colour - Odoo's primary. Same in both bundles (brand purple).
|
||||
$fp-accent : var(--o-action, #714B67);
|
||||
|
||||
// ---------- Kind chip colours (domain semantic) ------------------------------
|
||||
@@ -117,7 +117,7 @@ $fp-kind-rack : var(--fp-kind-rack, $_fp-kind-rack-hex);
|
||||
$fp-kind-inspect : var(--fp-kind-inspect, $_fp-kind-inspect-hex);
|
||||
$fp-kind-other : var(--fp-kind-other, $_fp-kind-other-hex);
|
||||
|
||||
// ---------- Elevation — explicit rgba shadows --------------------------------
|
||||
// ---------- Elevation - explicit rgba shadows --------------------------------
|
||||
// Explicit rgba values (not color-mix) so they render identically across
|
||||
// browsers and themes. In dark mode the shadows still work against the
|
||||
// darker surfaces because they're translucent.
|
||||
@@ -131,7 +131,7 @@ $fp-elev-hover : 0 6px 12px rgba(0, 0, 0, 0.12),
|
||||
0 18px 36px rgba(0, 0, 0, 0.16);
|
||||
|
||||
// ---------- Semantic colour helpers ------------------------------------------
|
||||
// (Note: $fp-accent defined earlier with its fallback — not redefined here)
|
||||
// (Note: $fp-accent defined earlier with its fallback - not redefined here)
|
||||
$fp-ok : var(--bs-success, #28a745);
|
||||
$fp-warn : var(--bs-warning, #ffc107);
|
||||
$fp-bad : var(--bs-danger, #dc3545);
|
||||
@@ -143,7 +143,7 @@ $fp-info : var(--bs-info, #17a2b8);
|
||||
}
|
||||
|
||||
// ---------- Type scale ------------------------------------------------------
|
||||
// Shop-floor tablets are read from 18" — baseline bumped from Odoo default.
|
||||
// Shop-floor tablets are read from 18" - baseline bumped from Odoo default.
|
||||
$fp-text-xs : 0.75rem; // 12px small labels
|
||||
$fp-text-sm : 0.875rem; // 14px helper text
|
||||
$fp-text-base : 1rem; // 16px body
|
||||
@@ -169,20 +169,20 @@ $fp-dur : 200ms;
|
||||
$fp-dur-slow : 360ms;
|
||||
|
||||
// ---------- Touch ------------------------------------------------------------
|
||||
$fp-touch-min : 48px; // larger than Apple's 44px minimum — shop floor
|
||||
$fp-touch-min : 48px; // larger than Apple's 44px minimum - shop floor
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// Mixins
|
||||
// =============================================================================
|
||||
|
||||
// Focus ring — used on all interactive inputs/buttons
|
||||
// Focus ring - used on all interactive inputs/buttons
|
||||
@mixin fp-focus-ring {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, #{$fp-accent} 35%, transparent);
|
||||
}
|
||||
|
||||
// Card surface — shadow-based, no border
|
||||
// Card surface - shadow-based, no border
|
||||
@mixin fp-card($elev: $fp-elev-1) {
|
||||
background-color: $fp-card;
|
||||
border-radius: $fp-radius-lg;
|
||||
@@ -205,7 +205,7 @@ $fp-touch-min : 48px; // larger than Apple's 44px minimum — shop floor
|
||||
|
||||
// =============================================================================
|
||||
// Dark mode
|
||||
// No class-based override needed — Odoo 19 serves a separate compiled bundle
|
||||
// No class-based override needed - Odoo 19 serves a separate compiled bundle
|
||||
// for dark mode, and all --bs-* tokens are redefined inside it. Because our
|
||||
// $fp-* tokens fall through to --bs-* (see the surface definitions above),
|
||||
// dark mode Just Works.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// =====================================================================
|
||||
// Plant-view kanban — design tokens
|
||||
// Plant-view kanban - design tokens
|
||||
// MUST load BEFORE the component SCSS files. SCSS @import is forbidden
|
||||
// in custom Odoo 19 SCSS (project rule 8); the manifest concatenates
|
||||
// files in registration order, so this file's $vars are visible to
|
||||
@@ -34,7 +34,7 @@ $_plant-noparts-border-hex: #6c757d;
|
||||
$_plant-done-bg-hex: #f0f9f4;
|
||||
$_plant-done-border-hex: #28a745;
|
||||
|
||||
// Spec 2026-05-25 — post-shop states
|
||||
// Spec 2026-05-25 - post-shop states
|
||||
$_plant-awaiting-cert-bg-hex: #fff3cd;
|
||||
$_plant-awaiting-cert-border-hex: #ff9800;
|
||||
$_plant-awaiting-ship-bg-hex: #d1f1d4;
|
||||
@@ -58,7 +58,7 @@ $_plant-awaiting-ship-border-hex: #2e7d32;
|
||||
$_plant-noparts-bg-hex: #2d3138 !global;
|
||||
$_plant-done-bg-hex: #14281a !global;
|
||||
|
||||
// Spec 2026-05-25 — post-shop states (dark)
|
||||
// Spec 2026-05-25 - post-shop states (dark)
|
||||
$_plant-awaiting-cert-bg-hex: #3a2f15 !global;
|
||||
$_plant-awaiting-cert-border-hex: #ffb74d !global;
|
||||
$_plant-awaiting-ship-bg-hex: #1a2d1f !global;
|
||||
@@ -91,7 +91,7 @@ $plant-noparts-border: var(--fp-plant-noparts-border, $_plant-noparts-border-he
|
||||
$plant-done-bg: var(--fp-plant-done-bg, $_plant-done-bg-hex);
|
||||
$plant-done-border: var(--fp-plant-done-border, $_plant-done-border-hex);
|
||||
|
||||
// Spec 2026-05-25 — post-shop states
|
||||
// Spec 2026-05-25 - post-shop states
|
||||
$plant-awaiting-cert-bg: var(--fp-plant-awaiting-cert-bg, $_plant-awaiting-cert-bg-hex);
|
||||
$plant-awaiting-cert-border: var(--fp-plant-awaiting-cert-border, $_plant-awaiting-cert-border-hex);
|
||||
$plant-awaiting-ship-bg: var(--fp-plant-awaiting-ship-bg, $_plant-awaiting-ship-bg-hex);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// _column_header.scss — depends on _plant_tokens.scss
|
||||
// _column_header.scss - depends on _plant_tokens.scss
|
||||
|
||||
.o_fp_col_header {
|
||||
padding: 8px 10px;
|
||||
@@ -6,7 +6,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
// No own background or outer border — the parent .col carries them
|
||||
// No own background or outer border - the parent .col carries them
|
||||
// (full-height column-as-card layout). Just a bottom divider so the
|
||||
// header reads as a distinct band above the scrollable body.
|
||||
background: transparent;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// _filter_chip.scss — depends on _plant_tokens.scss
|
||||
// _filter_chip.scss - depends on _plant_tokens.scss
|
||||
// 2026-05-25: bigger touch target + gradient bg.
|
||||
|
||||
.o_fp_filter_chip {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// =============================================================================
|
||||
// GateViz — "this step can't start because..." explainer
|
||||
// GateViz - "this step can't start because..." explainer
|
||||
// Dark-mode aware via $o-webclient-color-scheme branch.
|
||||
// =============================================================================
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// =============================================================================
|
||||
// HoldComposer — modal hold-create form
|
||||
// HoldComposer - modal hold-create form
|
||||
// =============================================================================
|
||||
|
||||
.o_fp_hc {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// =============================================================================
|
||||
// FpIdleWarning — yellow-border countdown overlay before auto-lock
|
||||
// FpIdleWarning - yellow-border countdown overlay before auto-lock
|
||||
// =============================================================================
|
||||
|
||||
.o_fp_idle_warning_overlay {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// =============================================================================
|
||||
// KanbanCard — standard WO/step card for Landing + Manager surfaces
|
||||
// KanbanCard - standard WO/step card for Landing + Manager surfaces
|
||||
// Dark-mode aware via $o-webclient-color-scheme branch.
|
||||
// =============================================================================
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// _kpi_tile.scss — depends on _plant_tokens.scss
|
||||
// _kpi_tile.scss - depends on _plant_tokens.scss
|
||||
//
|
||||
// 2026-05-25: redesigned for the 8-tile row. Narrower individual width
|
||||
// (grid handles that), more vertical presence via padding + larger
|
||||
// typography, subtle 135deg gradients per kind that work in both light
|
||||
// and dark modes (gradient stops use $plant-* tokens — dark variants
|
||||
// and dark modes (gradient stops use $plant-* tokens - dark variants
|
||||
// flip automatically via the @if $o-webclient-color-scheme branch).
|
||||
|
||||
.o_fp_kpi_tile {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// _mini_timeline.scss — depends on _plant_tokens.scss
|
||||
// _mini_timeline.scss - depends on _plant_tokens.scss
|
||||
|
||||
.o_fp_mini_timeline {
|
||||
display: flex;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// =============================================================================
|
||||
// FpPinPad — numeric keypad for tablet lock screen + PIN setup
|
||||
// FpPinPad - numeric keypad for tablet lock screen + PIN setup
|
||||
// Dark-mode aware via $o-webclient-color-scheme branch.
|
||||
// =============================================================================
|
||||
|
||||
@@ -20,7 +20,7 @@ $_pin-dot-fill-hex: #1d1d1f;
|
||||
// Empty dot: dark gray that blends into the panel but is still
|
||||
// visible; Filled dot: bright white for strong contrast. Previous
|
||||
// version left empty at light gray (#d8dadd) and set filled to
|
||||
// off-white (#f5f5f7) — both light colours, indistinguishable from
|
||||
// off-white (#f5f5f7) - both light colours, indistinguishable from
|
||||
// each other on the dark panel so user couldn't see PIN progress.
|
||||
$_pin-dot-hex: #424245 !global;
|
||||
$_pin-dot-fill-hex: #ffffff !global;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// _plant_card.scss — depends on _plant_tokens.scss
|
||||
// _plant_card.scss - depends on _plant_tokens.scss
|
||||
|
||||
.o_fp_plant_card {
|
||||
background: $plant-card-bg;
|
||||
@@ -66,7 +66,7 @@
|
||||
border-left: 4px solid $plant-done-border;
|
||||
padding-left: 7px;
|
||||
}
|
||||
// Spec 2026-05-25 — post-shop states
|
||||
// Spec 2026-05-25 - post-shop states
|
||||
&.state-awaiting_cert {
|
||||
background: $plant-awaiting-cert-bg;
|
||||
border-left: 4px solid $plant-awaiting-cert-border;
|
||||
@@ -91,7 +91,7 @@
|
||||
.card-sub-em { color: $plant-text; font-weight: 600; }
|
||||
.card-meta { font-size: 11px; color: $plant-muted; }
|
||||
.card-step { font-size: 14px; font-weight: 600; color: $plant-text; margin-top: 2px; }
|
||||
// Partial-order handling — "20 of 50 here" per-stage count. The big
|
||||
// Partial-order handling - "20 of 50 here" per-stage count. The big
|
||||
// number pops so an operator scanning their column instantly sees how
|
||||
// many of a job's parts are at their station. Uses existing tokens so
|
||||
// dark mode is handled at compile time by _plant_tokens.scss.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Racking panel (Job Workspace) — split a WO across racks. Self-contained
|
||||
// Racking panel (Job Workspace) - split a WO across racks. Self-contained
|
||||
// tokens with a compile-time dark-mode branch (Odoo 19 compiles this file
|
||||
// into both web.assets_backend and web.assets_web_dark).
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// =============================================================================
|
||||
// SignaturePad — modal canvas signature capture
|
||||
// SignaturePad - modal canvas signature capture
|
||||
// Canvas stays light even in dark mode (signature legibility).
|
||||
// =============================================================================
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// =============================================================================
|
||||
// WorkflowChip — colored milestone pill
|
||||
// WorkflowChip - colored milestone pill
|
||||
// Dark-mode aware via $o-webclient-color-scheme branch (registered in BOTH
|
||||
// web.assets_backend AND web.assets_web_dark — see manifest).
|
||||
// web.assets_backend AND web.assets_web_dark - see manifest).
|
||||
// =============================================================================
|
||||
|
||||
$o-webclient-color-scheme: bright !default;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// =============================================================================
|
||||
// Fusion Plating — Shared kanban card style for menu pages
|
||||
// Fusion Plating - Shared kanban card style for menu pages
|
||||
// Copyright 2026 Nexa Systems Inc. · License OPL-1
|
||||
//
|
||||
// This file styles the standalone Bake Windows and First-Piece Gates kanban
|
||||
@@ -20,7 +20,7 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// Suppress Odoo's default .o_kanban_record chrome inside our kanbans.
|
||||
// The default wrapper paints its own background, border, padding, and
|
||||
// (annoyingly) box-shadow with sharper corners than our inner card —
|
||||
// (annoyingly) box-shadow with sharper corners than our inner card -
|
||||
// which makes a faint square ghost visible behind every card. By making
|
||||
// the wrapper transparent / borderless, only our .o_fp_kcard surface is
|
||||
// visible, so corner radii stay consistent across every state.
|
||||
@@ -39,7 +39,7 @@
|
||||
overflow: visible; // let our shadow paint outside the wrapper
|
||||
}
|
||||
|
||||
// Same treatment for the kanban group container — Odoo gives it a
|
||||
// Same treatment for the kanban group container - Odoo gives it a
|
||||
// subtle bg that looks misaligned next to the card surfaces.
|
||||
.o_kanban_group {
|
||||
background: transparent;
|
||||
@@ -76,7 +76,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Left state stripe — driven by data-state / data-result attribute on
|
||||
// Left state stripe - driven by data-state / data-result attribute on
|
||||
// the card. Default is the muted ink-faint colour; specific states
|
||||
// override below.
|
||||
&::before {
|
||||
@@ -105,7 +105,7 @@
|
||||
|
||||
// -- Big metric (time remaining etc.) ------------------------------
|
||||
// Used when the card has one number that matters more than the rest
|
||||
// (bake countdown, qty pending). Stays compact — this is a kanban,
|
||||
// (bake countdown, qty pending). Stays compact - this is a kanban,
|
||||
// not a billboard.
|
||||
.o_fp_kcard_metric {
|
||||
display: inline-flex; align-items: baseline; gap: $fp-space-1;
|
||||
@@ -124,7 +124,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// -- Meta line — small key/value pairs separated by mid-dots -------
|
||||
// -- Meta line - small key/value pairs separated by mid-dots -------
|
||||
.o_fp_kcard_meta {
|
||||
font-size: $fp-text-xs;
|
||||
color: $fp-ink-mute;
|
||||
@@ -135,7 +135,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// -- Footer — chip + secondary tags --------------------------------
|
||||
// -- Footer - chip + secondary tags --------------------------------
|
||||
.o_fp_kcard_footer {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
gap: $fp-space-2;
|
||||
@@ -163,7 +163,7 @@
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// Bake Windows — state-driven stripe + soft danger wash on missed jobs
|
||||
// Bake Windows - state-driven stripe + soft danger wash on missed jobs
|
||||
// =============================================================================
|
||||
.o_fp_bw_kanban {
|
||||
.o_fp_kcard {
|
||||
@@ -172,7 +172,7 @@
|
||||
&[data-state="baked"] { &::before { background-color: $fp-ok; } }
|
||||
&[data-state="missed_window"] {
|
||||
&::before { background-color: $fp-bad; }
|
||||
// Missed windows are an exception state — softly tint the
|
||||
// Missed windows are an exception state - softly tint the
|
||||
// whole card so it stands out in a sea of normal ones.
|
||||
background-color: fp-wash(--bs-danger, 6%);
|
||||
border-color: color-mix(in srgb, #{$fp-bad} 35%, #{$fp-border});
|
||||
@@ -186,7 +186,7 @@
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// First-Piece Gates — result-driven stripe (pending = warn, fail = bad)
|
||||
// First-Piece Gates - result-driven stripe (pending = warn, fail = bad)
|
||||
// =============================================================================
|
||||
.o_fp_fpg_kanban {
|
||||
.o_fp_kcard {
|
||||
@@ -199,7 +199,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Subtle "released" badge — visible only when the lot has been
|
||||
// Subtle "released" badge - visible only when the lot has been
|
||||
// released after a passing first-piece. Sits next to the result chip.
|
||||
.o_fp_fpg_released {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// =============================================================================
|
||||
// Fusion Plating — Tablet Station (Worker view)
|
||||
// Fusion Plating - Tablet Station (Worker view)
|
||||
// Copyright 2026 Nexa Systems Inc. · License OPL-1
|
||||
//
|
||||
// Shop-floor operators' home screen. Built fresh from the design system in
|
||||
// _fp_shopfloor_tokens.scss. No card borders — depth by shadow + tint.
|
||||
// _fp_shopfloor_tokens.scss. No card borders - depth by shadow + tint.
|
||||
// =============================================================================
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
flex-direction: column;
|
||||
gap: $fp-space-5;
|
||||
|
||||
// Tablet sweet spot — iPad landscape (1024) and portrait (768).
|
||||
// Tablet sweet spot - iPad landscape (1024) and portrait (768).
|
||||
// The goal is to fit Hero + KPIs + Active WO + the first row of
|
||||
// panels in a single 768-tall viewport.
|
||||
@media (max-width: 1180px) { padding: $fp-space-4 $fp-space-5; gap: $fp-space-4; }
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Hero — greeting + station chip + actions
|
||||
// Hero - greeting + station chip + actions
|
||||
// -------------------------------------------------------------------------
|
||||
.o_fp_tablet_header {
|
||||
display: grid;
|
||||
@@ -67,7 +67,7 @@
|
||||
color: $fp-ink;
|
||||
display: flex; align-items: center; gap: $fp-space-3;
|
||||
|
||||
// Smaller hero on tablet — saves ~16px of vertical space without
|
||||
// Smaller hero on tablet - saves ~16px of vertical space without
|
||||
// losing the page identity.
|
||||
@media (max-width: 1180px) { font-size: $fp-text-xl; gap: $fp-space-2; }
|
||||
}
|
||||
@@ -104,7 +104,7 @@
|
||||
min-width: 240px;
|
||||
min-height: $fp-touch-min;
|
||||
// Reserve room on the right so the custom chevron has breathing
|
||||
// space between itself and the rounded corner — the native arrow
|
||||
// space between itself and the rounded corner - the native arrow
|
||||
// hugs the edge in Odoo's frame, which looked cramped on iPad.
|
||||
padding: $fp-space-2 $fp-space-7 $fp-space-2 $fp-space-4;
|
||||
border: 1px solid #{$fp-border};
|
||||
@@ -199,7 +199,7 @@
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Flash message — inline banner
|
||||
// Flash message - inline banner
|
||||
// -------------------------------------------------------------------------
|
||||
.o_fp_tablet_message {
|
||||
display: flex; align-items: center; gap: $fp-space-3;
|
||||
@@ -225,14 +225,14 @@
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// KPI tiles — minimal, big number
|
||||
// KPI tiles - minimal, big number
|
||||
// -------------------------------------------------------------------------
|
||||
.o_fp_kpi_strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: $fp-space-4;
|
||||
|
||||
// iPad landscape (1024) — six 130px tiles + gaps fit on one row.
|
||||
// iPad landscape (1024) - six 130px tiles + gaps fit on one row.
|
||||
// Keeps the KPI strip a single line so the dashboard can stay above
|
||||
// the fold.
|
||||
@media (max-width: 1180px) {
|
||||
@@ -292,7 +292,7 @@
|
||||
font-weight: $fp-weight-medium;
|
||||
}
|
||||
|
||||
// Accent dot keyed to tone (top-right) — tiny, confident
|
||||
// Accent dot keyed to tone (top-right) - tiny, confident
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@@ -319,7 +319,7 @@
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Active WO banner — unmistakable when a job is running
|
||||
// Active WO banner - unmistakable when a job is running
|
||||
// -------------------------------------------------------------------------
|
||||
.o_fp_active_wo {
|
||||
display: flex;
|
||||
@@ -424,7 +424,7 @@
|
||||
display: inline-flex; align-items: center; gap: $fp-space-3;
|
||||
color: $fp-ink;
|
||||
|
||||
// Icon badge — rounded square with tinted background.
|
||||
// Icon badge - rounded square with tinted background.
|
||||
// Gives the panel head visual weight without being loud.
|
||||
> .fa {
|
||||
display: inline-flex;
|
||||
@@ -449,7 +449,7 @@
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Empty states — friendly, not boxed
|
||||
// Empty states - friendly, not boxed
|
||||
// -------------------------------------------------------------------------
|
||||
.o_fp_empty {
|
||||
padding: $fp-space-7 $fp-space-4;
|
||||
@@ -476,7 +476,7 @@
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Queue — flat list, big rows, no borders
|
||||
// Queue - flat list, big rows, no borders
|
||||
// -------------------------------------------------------------------------
|
||||
.o_fp_queue_list {
|
||||
list-style: none; margin: 0; padding: 0;
|
||||
@@ -701,7 +701,7 @@
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// S13/S14/S17/S19 — recipe chips, predecessor lock, live clock, scrap bar
|
||||
// S13/S14/S17/S19 - recipe chips, predecessor lock, live clock, scrap bar
|
||||
// -------------------------------------------------------------------------
|
||||
.o_fp_active_wo_left {
|
||||
display: flex; gap: $fp-space-3; align-items: flex-start;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// =============================================================================
|
||||
// JobWorkspace — full-screen WO surface
|
||||
// JobWorkspace - full-screen WO surface
|
||||
// Dark-mode aware via $o-webclient-color-scheme branch.
|
||||
// =============================================================================
|
||||
|
||||
@@ -66,7 +66,7 @@ $_ws-text-hex: #1d1d1f;
|
||||
}
|
||||
}
|
||||
|
||||
// Hand-Off button — gold gradient, prominent, matches plant kanban .handoff
|
||||
// Hand-Off button - gold gradient, prominent, matches plant kanban .handoff
|
||||
.o_fp_ws_handoff {
|
||||
padding: 0.55rem 1.1rem;
|
||||
font-size: 0.95rem;
|
||||
@@ -117,7 +117,7 @@ $_ws-text-hex: #1d1d1f;
|
||||
|
||||
// ---- WORKFLOW BAR ------------------------------------------------------
|
||||
// 2026-05-25: bigger step dots + labels + the Next button. Dots were
|
||||
// 14px with 0.65rem labels — too small to read at arm's length.
|
||||
// 14px with 0.65rem labels - too small to read at arm's length.
|
||||
.o_fp_ws_bar {
|
||||
background: $_ws-page-hex;
|
||||
border-bottom: 1px solid $_ws-border-hex;
|
||||
@@ -220,7 +220,7 @@ $_ws-text-hex: #1d1d1f;
|
||||
overflow: hidden;
|
||||
|
||||
// Single column on tablets/phones, and make MAIN itself the one scroll
|
||||
// container — the work (steps/receiving) sits at the top, Notes stack
|
||||
// container - the work (steps/receiving) sits at the top, Notes stack
|
||||
// below and scroll into view when needed. The old layout kept
|
||||
// overflow:hidden with two nested auto-height scroll panes, which
|
||||
// clipped the notes and broke scrolling on narrow screens.
|
||||
@@ -490,7 +490,7 @@ $_ws-text-hex: #1d1d1f;
|
||||
// ===== Phone optimization (2026-06-02) ==============================
|
||||
// Shrink the fixed chrome (header + workflow bar + rail) so the operator
|
||||
// sees the actual work (receiving / step cards) without scrolling. Notes
|
||||
// sit below the work in the single scroll column — present, scroll for more.
|
||||
// sit below the work in the single scroll column - present, scroll for more.
|
||||
@media (max-width: 600px) {
|
||||
.o_fp_ws_head {
|
||||
padding: 0.45rem 0.7rem;
|
||||
@@ -611,7 +611,7 @@ $_ws-text-hex: #1d1d1f;
|
||||
|
||||
.o_fp_ws_rcv {
|
||||
background: $_ws-card-hex;
|
||||
border: 2px solid #f1c40f; // amber — draws receiver's eye
|
||||
border: 2px solid #f1c40f; // amber - draws receiver's eye
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
@@ -1004,7 +1004,7 @@ $_ws-text-hex: #1d1d1f;
|
||||
|
||||
// NOTE: Odoo's backend CSS does NOT define --bs-body-color /
|
||||
// --bs-secondary-color / --bs-*-bg as custom properties (verified: 0
|
||||
// definitions in the compiled bundle — they're SCSS literals + two
|
||||
// definitions in the compiled bundle - they're SCSS literals + two
|
||||
// bundles + [data-bs-theme]). So var(--bs-body-color, #hex) ALWAYS
|
||||
// resolves to the dark #hex fallback, in light AND dark mode. The fix
|
||||
// for dialog text is to INHERIT the modal's theme-correct colour (the
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// =============================================================================
|
||||
// Fusion Plating — Manager Desk
|
||||
// Fusion Plating - Manager Desk
|
||||
// Copyright 2026 Nexa Systems Inc. · License OPL-1
|
||||
//
|
||||
// Shared tokens from _fp_shopfloor_tokens.scss (loaded first in the bundle).
|
||||
@@ -62,7 +62,7 @@
|
||||
display: flex; flex-wrap: wrap; gap: $fp-space-3; align-items: center;
|
||||
}
|
||||
|
||||
// Live indicator — calm dot that pulses during a fetch
|
||||
// Live indicator - calm dot that pulses during a fetch
|
||||
.o_fp_live_dot {
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 50%;
|
||||
@@ -135,7 +135,7 @@
|
||||
.o_fp_manager_head_actions {
|
||||
display: flex; gap: $fp-space-2;
|
||||
|
||||
// Secondary (default) button — plain card with border
|
||||
// Secondary (default) button - plain card with border
|
||||
.btn {
|
||||
min-height: $fp-touch-min;
|
||||
padding: 0 $fp-space-4;
|
||||
@@ -157,9 +157,9 @@
|
||||
&:active { transform: scale(0.97); }
|
||||
}
|
||||
|
||||
// Primary — filled with the accent (brand purple), white text. White
|
||||
// Primary - filled with the accent (brand purple), white text. White
|
||||
// is correct in BOTH light and dark bundles because $fp-accent is
|
||||
// the same hue in both — it doesn't flip with theme. Force
|
||||
// the same hue in both - it doesn't flip with theme. Force
|
||||
// specificity high enough to beat Bootstrap's .btn-primary which
|
||||
// loads later.
|
||||
.btn.btn-primary,
|
||||
@@ -183,7 +183,7 @@
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Flash message — reused styling
|
||||
// Flash message - reused styling
|
||||
// -------------------------------------------------------------------------
|
||||
.o_fp_tablet_message {
|
||||
display: flex; align-items: center; gap: $fp-space-3;
|
||||
@@ -209,7 +209,7 @@
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// KPI strip — same language as tablet
|
||||
// KPI strip - same language as tablet
|
||||
// -------------------------------------------------------------------------
|
||||
.o_fp_kpi_strip {
|
||||
display: grid;
|
||||
@@ -372,7 +372,7 @@
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// MO cards — NO borders, depth by shadow + surface tint
|
||||
// MO cards - NO borders, depth by shadow + surface tint
|
||||
// -------------------------------------------------------------------------
|
||||
.o_fp_mgr_card_list {
|
||||
display: flex; flex-direction: column; gap: $fp-space-3;
|
||||
@@ -395,7 +395,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Priority stripe (4px) on the left — only when priority is set
|
||||
// Priority stripe (4px) on the left - only when priority is set
|
||||
&[data-priority="2"] {
|
||||
background-color: color-mix(in srgb, #{$fp-bad} 4%, $fp-card);
|
||||
border-color: color-mix(in srgb, #{$fp-bad} 35%, #{$fp-border});
|
||||
@@ -433,7 +433,7 @@
|
||||
.o_fp_mgr_card_body {
|
||||
padding: $fp-space-3 $fp-space-4 $fp-space-4;
|
||||
display: flex; flex-direction: column; gap: $fp-space-2;
|
||||
// Subtle inset against the card surface — uses the soft surface
|
||||
// Subtle inset against the card surface - uses the soft surface
|
||||
// token so it tints correctly in both light and dark bundles.
|
||||
background-color: $fp-card-soft;
|
||||
}
|
||||
@@ -466,7 +466,7 @@
|
||||
gap: $fp-space-1;
|
||||
color: $fp-ink;
|
||||
|
||||
// Title row — kind badge + step name + sequence
|
||||
// Title row - kind badge + step name + sequence
|
||||
.o_fp_mgr_step_title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -476,7 +476,7 @@
|
||||
font-size: $fp-text-base;
|
||||
line-height: 1.25;
|
||||
}
|
||||
// Meta row — workcenter / role / set equipment
|
||||
// Meta row - workcenter / role / set equipment
|
||||
.o_fp_mgr_step_meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -486,7 +486,7 @@
|
||||
color: $fp-ink-mute;
|
||||
i { margin-right: 2px; }
|
||||
}
|
||||
// Chip row — what's still missing for the manager to set
|
||||
// Chip row - what's still missing for the manager to set
|
||||
.o_fp_mgr_step_needs {
|
||||
margin-top: 2px;
|
||||
}
|
||||
@@ -508,13 +508,13 @@
|
||||
.o_fp_mgr_picker {
|
||||
// box-sizing matters: native <select> defaults to content-box, so
|
||||
// `width: 100% + padding-right` overflows. border-box keeps the
|
||||
// picker inside its flex slot — the Tank dropdown was bleeding
|
||||
// picker inside its flex slot - the Tank dropdown was bleeding
|
||||
// past the card's right edge before this.
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 220px; // grows to fill, but stays usable
|
||||
min-width: 0; // lets flex actually shrink it
|
||||
min-height: 40px;
|
||||
// Custom chevron via SVG background — controls its position
|
||||
// Custom chevron via SVG background - controls its position
|
||||
// exactly (browsers crowd the native chevron right against the
|
||||
// edge). pe: none on chevron so the click still hits the select.
|
||||
appearance: none;
|
||||
@@ -600,7 +600,7 @@
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Team column — avatar + name + load
|
||||
// Team column - avatar + name + load
|
||||
// -------------------------------------------------------------------------
|
||||
.o_fp_team_grid {
|
||||
display: flex; flex-direction: column; gap: $fp-space-2;
|
||||
@@ -648,7 +648,7 @@
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Phase 4 tablet redesign — Manager dashboard sibling tabs
|
||||
// Phase 4 tablet redesign - Manager dashboard sibling tabs
|
||||
// =============================================================================
|
||||
|
||||
.o_fp_mgr_tabs {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Sub 12b — shared SCSS for Move Parts / Move Rack / Rack Parts /
|
||||
// Sub 12b - shared SCSS for Move Parts / Move Rack / Rack Parts /
|
||||
// Stop Timer dialogs. Tokens follow the existing fp_shopfloor pattern
|
||||
// with a dark-mode SCSS @if branch (CLAUDE.md rule for Odoo 19 dark
|
||||
// mode — runtime DOM class flips do not work in 19; we branch at SCSS
|
||||
// mode - runtime DOM class flips do not work in 19; we branch at SCSS
|
||||
// compile time on $o-webclient-color-scheme).
|
||||
|
||||
$o-webclient-color-scheme: bright !default;
|
||||
@@ -174,7 +174,7 @@ $fp-md-page: var(--fp-page-bg, #{$_fp_md_page_hex});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ Partial-order handling — easy-advance layout
|
||||
// ============================ Partial-order handling - easy-advance layout
|
||||
// "Send Parts Forward" dialog: destination banner + big-tap qty stepper
|
||||
// (no keyboard) + collapsed advanced fields. Reuses the $fp-md-* tokens so
|
||||
// dark mode is handled at compile time.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// plant_kanban.scss — depends on _plant_tokens.scss and the component partials
|
||||
// plant_kanban.scss - depends on _plant_tokens.scss and the component partials
|
||||
|
||||
.o_fp_plant_kanban {
|
||||
padding: 8px;
|
||||
background: $plant-bg;
|
||||
// Fill the Odoo action area (below the navbar) and own the scroll
|
||||
// internally — NOT 100vh. 100vh is taller than the available area by
|
||||
// internally - NOT 100vh. 100vh is taller than the available area by
|
||||
// the navbar height, so the board bottom + its horizontal scrollbar
|
||||
// overflowed off-screen and scrolling broke — badly on phones, where
|
||||
// overflowed off-screen and scrolling broke - badly on phones, where
|
||||
// Odoo also re-lays-out at the md breakpoint and the scroll gets lost
|
||||
// up the tree. height:100% + internal overflow is the same pattern
|
||||
// job_workspace / manager_dashboard / .o_fp_tablet use. flex column so
|
||||
@@ -39,7 +39,7 @@
|
||||
.floor-title { font-size: 16px; font-weight: 700; }
|
||||
.floor-controls { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
|
||||
|
||||
// 2026-05-25 — toolbar buttons bigger + gradients for visual weight
|
||||
// 2026-05-25 - toolbar buttons bigger + gradients for visual weight
|
||||
.station-picker {
|
||||
padding: 8px 14px;
|
||||
background: linear-gradient(135deg, $plant-mine-bg 0%, $plant-card-bg 100%);
|
||||
@@ -102,7 +102,7 @@
|
||||
color: #5e4400;
|
||||
font-weight: 700;
|
||||
}
|
||||
// Scan pair — matched look. "Scan QR" (camera, the primary way to
|
||||
// Scan pair - matched look. "Scan QR" (camera, the primary way to
|
||||
// scan a printed job sticker) is accent-filled so it stands out;
|
||||
// "Enter Code" (manual / hardware scanner-gun) is the accent-tinted
|
||||
// secondary. Matched FA icons (fa-qrcode / fa-keyboard-o), no emoji.
|
||||
@@ -122,7 +122,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 8 tiles — Work Orders, At My Station, Bakes Due, On Hold,
|
||||
// 8 tiles - Work Orders, At My Station, Bakes Due, On Hold,
|
||||
// Awaiting QC, Awaiting CoC, Ready to Ship, Overdue.
|
||||
.kpi-strip {
|
||||
display: grid;
|
||||
@@ -169,7 +169,7 @@
|
||||
// desktop ~6 columns visible; on a 1366px tablet ~4 visible with
|
||||
// smooth horizontal scroll. User explicitly accepted side-scrolling.
|
||||
//
|
||||
// flex: 1 + min-height: 0 — the board fills all remaining vertical
|
||||
// flex: 1 + min-height: 0 - the board fills all remaining vertical
|
||||
// space below the sticky header, so the horizontal scrollbar pins
|
||||
// to the viewport bottom (not mid-page where the board's natural
|
||||
// height would have ended).
|
||||
@@ -199,11 +199,11 @@
|
||||
}
|
||||
}
|
||||
// Each .col is now a proper bordered card that runs full board
|
||||
// height — same visual treatment as Trello / Asana columns. The
|
||||
// height - same visual treatment as Trello / Asana columns. The
|
||||
// header (.o_fp_col_header) sits at the top with a divider; the
|
||||
// scrollable body (.col-scroll) takes the rest. Previously .col
|
||||
// had bg = page-bg (invisible) so columns visually ended at the
|
||||
// header card — empty columns looked unbounded.
|
||||
// header card - empty columns looked unbounded.
|
||||
.col {
|
||||
background: $plant-card-bg;
|
||||
border: 1px solid $plant-card-border;
|
||||
@@ -220,7 +220,7 @@
|
||||
}
|
||||
.col-scroll {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0; // critical — without this the children
|
||||
min-height: 0; // critical - without this the children
|
||||
// push the column past its parent height
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
@@ -277,7 +277,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Responsive — phones / small screens (2026-06-02) ==============
|
||||
// ===== Responsive - phones / small screens (2026-06-02) ==============
|
||||
// Compact the header so the board keeps the screen, and make the toolbar
|
||||
// controls full-width + tappable (>=40px) on a phone.
|
||||
.o_fp_plant_kanban {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// =============================================================================
|
||||
// Fusion Plating — Plant Overview (Kanban)
|
||||
// Fusion Plating - Plant Overview (Kanban)
|
||||
// Copyright 2026 Nexa Systems Inc. · License OPL-1
|
||||
//
|
||||
// Kanban of work orders grouped by work centre. Cards have BOTH a
|
||||
@@ -91,7 +91,7 @@
|
||||
@include fp-focus-ring;
|
||||
border-color: $fp-accent;
|
||||
}
|
||||
// Tablet — keep it generously sized but cap so the toolbar
|
||||
// Tablet - keep it generously sized but cap so the toolbar
|
||||
// doesn't blow past the viewport.
|
||||
@media (max-width: 1180px) { width: 320px; min-height: 48px; }
|
||||
@media (max-width: 900px) { width: 100%; }
|
||||
@@ -157,7 +157,7 @@
|
||||
border-radius: $fp-radius-lg;
|
||||
box-shadow: $fp-elev-1;
|
||||
max-height: calc(100vh - 180px);
|
||||
// Keep overflow: hidden at ALL sizes — it's what clips the rounded
|
||||
// Keep overflow: hidden at ALL sizes - it's what clips the rounded
|
||||
// corners cleanly. On mobile we just lift the max-height so the
|
||||
// column sizes to content and doesn't need internal scroll.
|
||||
overflow: hidden;
|
||||
@@ -167,7 +167,7 @@
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
max-height: none;
|
||||
// overflow: hidden stays — corners stay rounded. Since the
|
||||
// overflow: hidden stays - corners stay rounded. Since the
|
||||
// content body no longer has overflow-y: auto on mobile (see
|
||||
// below), nothing is clipped and the parent page scrolls.
|
||||
}
|
||||
@@ -222,7 +222,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Insertion placeholder — a live DOM node inserted between cards as
|
||||
// Insertion placeholder - a live DOM node inserted between cards as
|
||||
// the cursor moves so the manager sees exactly where the drop will
|
||||
// slot in. 4px solid accent bar + small glow + smooth slide.
|
||||
.o_fp_po_drop_placeholder {
|
||||
@@ -253,7 +253,7 @@
|
||||
background-color: $fp-card;
|
||||
border: 1px solid #{$fp-border};
|
||||
border-radius: $fp-radius-md;
|
||||
// Clip children to the rounded corners — this is what makes the
|
||||
// Clip children to the rounded corners - this is what makes the
|
||||
// priority stripe (inside via ::before) curve with the card.
|
||||
// Shadows are painted outside the content box so they render fine.
|
||||
overflow: hidden;
|
||||
@@ -261,7 +261,7 @@
|
||||
margin-bottom: $fp-space-2;
|
||||
cursor: grab;
|
||||
box-shadow: $fp-elev-1;
|
||||
// Allow vertical page-scroll gestures on touch — without this
|
||||
// Allow vertical page-scroll gestures on touch - without this
|
||||
// a draggable card can swallow the touch and block scrolling.
|
||||
// Desktop drag (mousedown) still works.
|
||||
touch-action: pan-y;
|
||||
@@ -285,7 +285,7 @@
|
||||
box-shadow: $fp-elev-3;
|
||||
}
|
||||
|
||||
// Priority left bar — lives inside the card's overflow: hidden
|
||||
// Priority left bar - lives inside the card's overflow: hidden
|
||||
// so it gets clipped to the rounded corners automatically.
|
||||
&::before {
|
||||
content: "";
|
||||
@@ -307,7 +307,7 @@
|
||||
display: flex; align-items: center; gap: $fp-space-2;
|
||||
margin-bottom: $fp-space-1;
|
||||
|
||||
// Small customer avatar — 32px thumbnail. Just identifies the
|
||||
// Small customer avatar - 32px thumbnail. Just identifies the
|
||||
// customer at a glance; not a billboard.
|
||||
.o_fp_po_card_avatar {
|
||||
flex-shrink: 0;
|
||||
@@ -355,7 +355,7 @@
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
|
||||
// Parts progress bar — a real indicator of where the job is
|
||||
// Parts progress bar - a real indicator of where the job is
|
||||
.o_fp_po_card_parts {
|
||||
margin: $fp-space-2 0;
|
||||
}
|
||||
@@ -459,7 +459,7 @@ $_fp-urg-warn-bg-alpha: 0.20;
|
||||
}
|
||||
}
|
||||
|
||||
// HOT band gets the fattest treatment — solid red fill, white text.
|
||||
// HOT band gets the fattest treatment - solid red fill, white text.
|
||||
// Overrides the danger tone above so this band can't fade into the
|
||||
// other danger chips.
|
||||
.o_fp_po_card_urgency.o_fp_po_urg_hot {
|
||||
@@ -479,7 +479,7 @@ $_fp-urg-warn-bg-alpha: 0.20;
|
||||
// Replaces the always-identical "[FP-SERVICE] Plating Service" line with the
|
||||
// part number + coating spec the operator actually cares about. Both lines
|
||||
// rely on $fp-ink / $fp-ink-mute tokens so they flip cleanly between the
|
||||
// light and dark bundles — no hard-coded hex.
|
||||
// light and dark bundles - no hard-coded hex.
|
||||
.o_fp_po_card_part {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -524,7 +524,7 @@ $_fp-urg-warn-bg-alpha: 0.20;
|
||||
margin-bottom: $fp-space-2;
|
||||
}
|
||||
|
||||
// Step-ordinal badge — separator + total in mute tone (1-based "4/9").
|
||||
// Step-ordinal badge - separator + total in mute tone (1-based "4/9").
|
||||
.o_fp_po_card_step_total {
|
||||
font-weight: $fp-weight-medium;
|
||||
color: $fp-ink-faint;
|
||||
@@ -535,13 +535,13 @@ $_fp-urg-warn-bg-alpha: 0.20;
|
||||
// Live-ticking elapsed-in-stage label. JS getCardTimer() picks the tone
|
||||
// (muted/ok/warning/danger) and a `critical` flag that toggles the pulse
|
||||
// animation. Critical = step is overrun (>1.5× expected), paused >24h, or
|
||||
// queued >24h — any of those conditions need supervisor attention NOW.
|
||||
// queued >24h - any of those conditions need supervisor attention NOW.
|
||||
//
|
||||
// Light/dark mode: warning text needs different hex per bundle so it
|
||||
// stays legible against the translucent yellow tint. Other tones use
|
||||
// $fp-* tokens or --bs-* CSS vars which Odoo flips automatically.
|
||||
|
||||
$_fp-timer-warn-text-hex: #856404; // dark brown — readable on light card
|
||||
$_fp-timer-warn-text-hex: #856404; // dark brown - readable on light card
|
||||
$_fp-timer-warn-bg-alpha: 0.20;
|
||||
@if $o-webclient-color-scheme == dark {
|
||||
$_fp-timer-warn-text-hex: #ffda6a !global; // light yellow on dark card
|
||||
@@ -560,7 +560,7 @@ $_fp-timer-warn-bg-alpha: 0.20;
|
||||
|
||||
i { font-size: 11px; }
|
||||
|
||||
// Tones — backgrounds use rgba() with a low alpha so the underlying
|
||||
// Tones - backgrounds use rgba() with a low alpha so the underlying
|
||||
// card surface tints through; text uses the strong hue.
|
||||
&.o_fp_po_timer_muted {
|
||||
background: $fp-card-soft;
|
||||
@@ -599,12 +599,12 @@ $_fp-timer-warn-bg-alpha: 0.20;
|
||||
}
|
||||
}
|
||||
|
||||
// Critical card border (v19.0.24.11.0) — class-based, NOT `:has()`.
|
||||
// Critical card border (v19.0.24.11.0) - class-based, NOT `:has()`.
|
||||
// `:has()` re-evaluates on every layout pass; with 389 cards on screen
|
||||
// and the browser doing constant layout work during drag, that selector
|
||||
// was the actual reason drag-drop felt frozen for 5+ seconds. The
|
||||
// server now flags critical cards with `is_urgent=true` and the OWL
|
||||
// template adds `.o_fp_po_card_critical` directly — zero selector cost.
|
||||
// template adds `.o_fp_po_card_critical` directly - zero selector cost.
|
||||
.o_fp_po_card_critical {
|
||||
box-shadow: $fp-elev-2,
|
||||
0 0 0 2px rgba(220, 53, 69, 0.55),
|
||||
@@ -613,7 +613,7 @@ $_fp-timer-warn-bg-alpha: 0.20;
|
||||
|
||||
// While a drag is in progress, pause infinite keyframe animations on
|
||||
// the few cards that have them (chip pulses). We INTENTIONALLY do NOT
|
||||
// touch transitions here — the previous version used `* { transition:
|
||||
// touch transitions here - the previous version used `* { transition:
|
||||
// none !important }` which forced the browser to recalculate styles
|
||||
// on every descendant (~12,000 elements at 389 cards) on every drop,
|
||||
// and that style-recalc *was* the bottleneck the user was feeling
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// =============================================================================
|
||||
// Fusion Plating — Process Tree (horizontal hierarchical, v3, 2026-04)
|
||||
// Fusion Plating - Process Tree (horizontal hierarchical, v3, 2026-04)
|
||||
// Copyright 2026 Nexa Systems Inc. · License OPL-1
|
||||
//
|
||||
// Hierarchical bracket tree:
|
||||
@@ -45,7 +45,7 @@ $pt-line-width : 2px;
|
||||
background-color: $fp-page;
|
||||
color: $fp-ink;
|
||||
height: 100%;
|
||||
overflow: auto; // both axes — wide trees scroll horizontally
|
||||
overflow: auto; // both axes - wide trees scroll horizontally
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: $fp-space-4 $fp-space-5;
|
||||
display: flex;
|
||||
@@ -134,7 +134,7 @@ $pt-line-width : 2px;
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Tree canvas — horizontally scrollable
|
||||
// Tree canvas - horizontally scrollable
|
||||
// -------------------------------------------------------------------------
|
||||
.o_fp_pt_canvas {
|
||||
padding: $fp-space-3 0;
|
||||
@@ -143,7 +143,7 @@ $pt-line-width : 2px;
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Recursive node — flex row of [card | children-column]
|
||||
// Recursive node - flex row of [card | children-column]
|
||||
// -------------------------------------------------------------------------
|
||||
.o_fp_pt_node {
|
||||
display: flex;
|
||||
@@ -212,7 +212,7 @@ $pt-line-width : 2px;
|
||||
// ---- Live state highlight ----------------------------------------
|
||||
&.o_fp_pt_state_progress,
|
||||
&.o_fp_pt_highlight.o_fp_pt_state_progress {
|
||||
background-color: #c0392b; // warm red — active step
|
||||
background-color: #c0392b; // warm red - active step
|
||||
color: #fff;
|
||||
box-shadow: 0 0 0 1px rgba(192, 57, 43, .6),
|
||||
0 4px 14px rgba(192, 57, 43, .35);
|
||||
@@ -228,7 +228,7 @@ $pt-line-width : 2px;
|
||||
background-color: #1e8449; // green for completed slice
|
||||
color: #fff;
|
||||
}
|
||||
// Operation / step cards that have completed — green fill + subtle
|
||||
// Operation / step cards that have completed - green fill + subtle
|
||||
// pulsing glow so finished work pops against the still-pending dark
|
||||
// cards. Animation pauses on hover so the click target is steady.
|
||||
&.o_fp_pt_state_done.o_fp_pt_type_operation,
|
||||
@@ -399,15 +399,15 @@ $pt-line-width : 2px;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
// First child — vertical only from card centre → bottom of row
|
||||
// First child - vertical only from card centre → bottom of row
|
||||
&:first-child::after {
|
||||
top: calc(#{$pt-card-h} / 2);
|
||||
}
|
||||
// Last child — vertical only from top of row → card centre
|
||||
// Last child - vertical only from top of row → card centre
|
||||
&:last-child::after {
|
||||
bottom: calc(100% - (#{$pt-card-h} / 2));
|
||||
}
|
||||
// Only child — vertical only at the card centre point (just enough
|
||||
// Only child - vertical only at the card centre point (just enough
|
||||
// to render the elbow connecting to the parent stub)
|
||||
&:first-child:last-child::after {
|
||||
top: calc(#{$pt-card-h} / 2);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// =============================================================================
|
||||
// Fusion Plating — Reusable QR Scanner Modal
|
||||
// Fusion Plating - Reusable QR Scanner Modal
|
||||
// Copyright 2026 Nexa Systems Inc. · License OPL-1
|
||||
//
|
||||
// Mobile-first modal that overlays the page. The video element fills
|
||||
@@ -27,7 +27,7 @@
|
||||
border-radius: $fp-radius-lg;
|
||||
box-shadow: $fp-elev-3;
|
||||
// Wrap min() in #{...} so dart-sass doesn't try to compute it at
|
||||
// compile time (it can't combine 420px and 92vw — the clamp/min
|
||||
// compile time (it can't combine 420px and 92vw - the clamp/min
|
||||
// functions are CSS-runtime, not SCSS). Pass through verbatim.
|
||||
width: #{"min(420px, 92vw)"};
|
||||
max-width: 92vw;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// =====================================================================
|
||||
// FpTabletLock — lock screen with tile grid + PIN pad overlay
|
||||
// FpTabletLock - lock screen with tile grid + PIN pad overlay
|
||||
// 2026-05-24 redesign: hybrid Industrial Bold + Premium Glassmorphism
|
||||
// Spec: docs/superpowers/specs/2026-05-24-tablet-lock-screen-redesign-design.md
|
||||
// Depends on _tablet_lock_tokens.scss being loaded first.
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
.o_fp_lock_logo_frame {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
// Rectangle (wider than tall) — fits horizontal company logos
|
||||
// Rectangle (wider than tall) - fits horizontal company logos
|
||||
// (mark + name + tagline laid out left-to-right) without leaving
|
||||
// dead space top/bottom. Uniform 18px padding on all sides so the
|
||||
// image breathes evenly.
|
||||
@@ -232,9 +232,9 @@
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Spec 2026-05-25 — PIN self-service wizard screens
|
||||
// Spec 2026-05-25 - PIN self-service wizard screens
|
||||
// (request_code / enter_temp_code / set_new_pin / confirm_new_pin)
|
||||
// Reuses $lock-* tokens from _tablet_lock_tokens.scss — dark mode
|
||||
// Reuses $lock-* tokens from _tablet_lock_tokens.scss - dark mode
|
||||
// auto-flips via the existing $o-webclient-color-scheme branch.
|
||||
// =====================================================================
|
||||
|
||||
@@ -329,7 +329,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Responsive — phones / small screens (2026-06-02) ==============
|
||||
// ===== Responsive - phones / small screens (2026-06-02) ==============
|
||||
// The lock screen is position:fixed + overflow-y:auto, so it already
|
||||
// scrolls; it just needs the 5-up operator grid + chrome to step down
|
||||
// on narrow screens. Ordered descending max-width so the smaller query
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// =============================================================================
|
||||
// Fusion Plating — Tank Status (NFC tap-to-view)
|
||||
// Fusion Plating - Tank Status (NFC tap-to-view)
|
||||
// Copyright 2026 Nexa Systems Inc. · License OPL-1
|
||||
//
|
||||
// Mobile-first stylesheet for /fp/tank/<id>. Renders inside
|
||||
@@ -116,7 +116,7 @@
|
||||
padding: $fp-space-3 0;
|
||||
}
|
||||
|
||||
// State / status pills — use the same translucent-tint pattern as the
|
||||
// State / status pills - use the same translucent-tint pattern as the
|
||||
// other shop-floor surfaces so they read at a glance on a phone.
|
||||
.o_fp_state_badge {
|
||||
padding: 2px 8px;
|
||||
@@ -154,7 +154,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Bath chemistry grid — one cell per parameter reading.
|
||||
// Bath chemistry grid - one cell per parameter reading.
|
||||
.o_fp_tank_chem_grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="o_fp_gate_body">
|
||||
<div class="o_fp_gate_title">Can't start yet</div>
|
||||
<div class="o_fp_gate_reason">
|
||||
<t t-esc="props.blockerReason or 'Reason unknown — open the step in the back-office.'"/>
|
||||
<t t-esc="props.blockerReason or 'Reason unknown - open the step in the back-office.'"/>
|
||||
</div>
|
||||
</div>
|
||||
<button t-if="props.jumpTargetModel and props.jumpTargetId and props.onJump"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<div class="o_fp_pin_grid">
|
||||
<!-- IMPORTANT: digits MUST be string literals here.
|
||||
OWL templates only expose `Math` as a JS global —
|
||||
OWL templates only expose `Math` as a JS global -
|
||||
`String`, `Number`, `Array`, etc. are NOT in template
|
||||
scope. Calling `String(d)` throws "v2 is not a
|
||||
function" because the compiled template references
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<t t-name="fusion_plating_shopfloor.RackingPanel">
|
||||
<div class="o_fp_racking_panel" t-if="state.data">
|
||||
<div class="o_fp_rkp_head">
|
||||
<span class="o_fp_rkp_title">🧰 Racking — split across racks</span>
|
||||
<span class="o_fp_rkp_title">🧰 Racking - split across racks</span>
|
||||
<span t-att-class="'o_fp_rkp_unassigned' + (state.data.unassigned ? ' has' : '')">
|
||||
Unassigned: <t t-esc="state.data.unassigned"/> / <t t-esc="state.data.total"/>
|
||||
</span>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<strong t-esc="props.stepName"/>
|
||||
</div>
|
||||
|
||||
<!-- Orphan step (NULL recipe link) — different copy -->
|
||||
<!-- Orphan step (NULL recipe link) - different copy -->
|
||||
<t t-if="props.orphanStep">
|
||||
<div class="o_fp_finish_block_msg">
|
||||
This step has <strong>no recipe link</strong> (the source
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
<div class="o_fp_finish_block_action_note">
|
||||
<i class="fa fa-user-md me-1"/>
|
||||
Escalate to a manager — they can bypass with an
|
||||
Escalate to a manager - they can bypass with an
|
||||
audit-chatter entry.
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
<t t-if="state.data">
|
||||
|
||||
<!-- =========================================================
|
||||
STICKY HEADER — WO context, qty bumps, workflow chip
|
||||
STICKY HEADER - WO context, qty bumps, workflow chip
|
||||
========================================================= -->
|
||||
<header class="o_fp_ws_head">
|
||||
<div class="o_fp_ws_head_l">
|
||||
<button class="btn btn-link o_fp_ws_back" t-on-click="onBack">
|
||||
<i class="fa fa-arrow-left"/> Back
|
||||
</button>
|
||||
<!-- Phase 6.2 — Hand-Off: lock the tablet -->
|
||||
<!-- Phase 6.2 - Hand-Off: lock the tablet -->
|
||||
<button class="btn btn-warning o_fp_ws_handoff ms-2"
|
||||
t-on-click="handOff"
|
||||
title="Lock the tablet for the next operator">
|
||||
@@ -53,7 +53,7 @@
|
||||
</header>
|
||||
|
||||
<!-- =========================================================
|
||||
STICKY WORKFLOW BAR — milestone dots + Next button
|
||||
STICKY WORKFLOW BAR - milestone dots + Next button
|
||||
========================================================= -->
|
||||
<div class="o_fp_ws_bar">
|
||||
<div class="o_fp_ws_bar_line">
|
||||
@@ -73,7 +73,7 @@
|
||||
</div>
|
||||
|
||||
<!-- =========================================================
|
||||
MAIN — step list (left/center) + side panel (right)
|
||||
MAIN - step list (left/center) + side panel (right)
|
||||
========================================================= -->
|
||||
<div class="o_fp_ws_main">
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
<div class="o_fp_ws_rcv_line_part">
|
||||
<strong t-esc="ln.part_number or 'Part'"/>
|
||||
<span t-if="ln.description" class="text-muted">
|
||||
— <t t-esc="ln.description"/>
|
||||
- <t t-esc="ln.description"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="o_fp_ws_rcv_line_qty">
|
||||
@@ -248,7 +248,7 @@
|
||||
<t t-foreach="state.data.shipping.not_ready"
|
||||
t-as="nr" t-key="nr.wo_name">
|
||||
<span class="o_fp_chip o_fp_chip_warning">
|
||||
<t t-esc="nr.wo_name"/> — <t t-esc="nr.state_label"/>
|
||||
<t t-esc="nr.wo_name"/> - <t t-esc="nr.state_label"/>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
@@ -259,7 +259,7 @@
|
||||
<label>Carrier
|
||||
<select class="form-select"
|
||||
t-on-change="(ev) => this.onShipInput('carrier_id', ev)">
|
||||
<option value="">— pick carrier —</option>
|
||||
<option value="">- pick carrier -</option>
|
||||
<t t-foreach="state.data.shipping.carrier_options"
|
||||
t-as="c" t-key="c.id">
|
||||
<option t-att-value="c.id"
|
||||
@@ -341,7 +341,7 @@
|
||||
<!-- NON-TERMINAL: read-ahead detail (chips + instructions + opt-out + GateViz) -->
|
||||
<t t-if="!['done', 'skipped', 'cancelled'].includes(step.state)">
|
||||
<div class="o_fp_ws_step_detail">
|
||||
<!-- Multi-rack split — embedded in the Racking step's row. -->
|
||||
<!-- Multi-rack split - embedded in the Racking step's row. -->
|
||||
<RackingPanel t-if="step.is_racking" jobId="state.jobId"/>
|
||||
<!-- Recipe chips: visible on every non-done step so operator reads ahead -->
|
||||
<div class="o_fp_ws_step_chips"
|
||||
@@ -369,12 +369,12 @@
|
||||
<t t-esc="step.instructions"/>
|
||||
</div>
|
||||
|
||||
<!-- Masking reference(s) — attached at order entry; tap to enlarge -->
|
||||
<!-- Masking reference(s) - attached at order entry; tap to enlarge -->
|
||||
<div t-if="step.masking_refs and step.masking_refs.length"
|
||||
class="o_fp_ws_mask_refs">
|
||||
<div class="o_fp_ws_mask_refs_label">
|
||||
<i class="fa fa-paint-brush"/>
|
||||
Masking reference<t t-if="step.masking_refs.length > 1">s</t> — tap to enlarge
|
||||
Masking reference<t t-if="step.masking_refs.length > 1">s</t> - tap to enlarge
|
||||
</div>
|
||||
<div class="o_fp_ws_mask_refs_grid">
|
||||
<t t-foreach="step.masking_refs" t-as="ref" t-key="ref.id">
|
||||
@@ -467,7 +467,7 @@
|
||||
</div>
|
||||
|
||||
<!-- =========================================================
|
||||
STICKY ACTION RAIL — Hold · Note · Cert · Milestone
|
||||
STICKY ACTION RAIL - Hold · Note · Cert · Milestone
|
||||
========================================================= -->
|
||||
<footer class="o_fp_ws_rail">
|
||||
<button class="btn btn-warning" t-on-click="onCreateHold">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc. · License OPL-1
|
||||
Fusion Plating — Manager Desk
|
||||
Fusion Plating - Manager Desk
|
||||
Native fp.job / fp.job.step edition. Speaks job/step end-to-end.
|
||||
-->
|
||||
<templates xml:space="preserve">
|
||||
@@ -26,13 +26,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_fp_manager_head_actions">
|
||||
<!-- Presence chip — clocked-in workers vs roster.
|
||||
<!-- Presence chip - clocked-in workers vs roster.
|
||||
Tap to toggle whether off-shift names show in
|
||||
the worker dropdowns. -->
|
||||
<button class="btn o_fp_presence_chip"
|
||||
t-att-data-active="state.hideOffShift ? 'y' : 'n'"
|
||||
t-on-click="toggleOffShift"
|
||||
t-att-title="state.hideOffShift ? 'Showing only clocked-in workers — click to include off-shift' : 'Showing all workers — click to hide off-shift'"
|
||||
t-att-title="state.hideOffShift ? 'Showing only clocked-in workers - click to include off-shift' : 'Showing all workers - click to hide off-shift'"
|
||||
t-if="state.overview and state.overview.presence">
|
||||
<span class="o_fp_presence_dot"/>
|
||||
Present
|
||||
@@ -47,7 +47,7 @@
|
||||
<i t-att-class="'fa fa-refresh' + (state.isFetching ? ' fa-spin' : '')"/>
|
||||
</button>
|
||||
<QrScanner cssClass="'btn'"/>
|
||||
<!-- Phase 6.2 — Hand-Off: lock the tablet -->
|
||||
<!-- Phase 6.2 - Hand-Off: lock the tablet -->
|
||||
<button class="btn btn-warning"
|
||||
t-on-click="handOff"
|
||||
title="Lock the tablet for the next operator">
|
||||
@@ -98,7 +98,7 @@
|
||||
<div class="o_fp_kpi_value"><t t-esc="state.overview.kpis.pending_accept_sos"/></div>
|
||||
<div class="o_fp_kpi_label">Awaiting Assignment</div>
|
||||
</div>
|
||||
<!-- v19.0.24.3.0 — compliance + floor-health KPIs. -->
|
||||
<!-- v19.0.24.3.0 - compliance + floor-health KPIs. -->
|
||||
<!-- Hidden when 0 to keep the strip clean; light up -->
|
||||
<!-- when something needs attention. Click to drill. -->
|
||||
<div class="o_fp_kpi o_fp_kpi_danger"
|
||||
@@ -161,7 +161,7 @@
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Phase 4 tablet redesign — Pending Cert + At-Risk tiles -->
|
||||
<!-- Phase 4 tablet redesign - Pending Cert + At-Risk tiles -->
|
||||
<div class="o_fp_kpi o_fp_kpi_warning"
|
||||
t-if="state.inbox and state.inbox.certs_to_issue and state.inbox.certs_to_issue.length"
|
||||
t-on-click="() => this.setActiveTab('inbox')">
|
||||
@@ -279,7 +279,7 @@
|
||||
<div class="o_fp_mgr_step_actions">
|
||||
<select class="o_fp_mgr_picker"
|
||||
t-on-change="(ev) => this.onAssignWorker(step, ev.target.value)">
|
||||
<option value="">— Assign worker —</option>
|
||||
<option value="">- Assign worker -</option>
|
||||
<t t-foreach="operatorsForStep(step)" t-as="op" t-key="op.id">
|
||||
<option t-att-value="op.id"
|
||||
t-att-selected="step.assigned_user_id === op.id"
|
||||
@@ -293,7 +293,7 @@
|
||||
<select t-if="step.kind === 'wet'"
|
||||
class="o_fp_mgr_picker"
|
||||
t-on-change="(ev) => this.onAssignTank(step, ev.target.value)">
|
||||
<option value="">— Tank —</option>
|
||||
<option value="">- Tank -</option>
|
||||
<t t-foreach="state.overview.tanks" t-as="tnk" t-key="tnk.id">
|
||||
<option t-att-value="tnk.id"
|
||||
t-att-selected="step.tank_id === tnk.id">
|
||||
@@ -450,7 +450,7 @@
|
||||
<span t-if="stage.count > stage.jobs.length" class="o_fp_funnel_more">
|
||||
+<t t-esc="stage.count - stage.jobs.length"/> more
|
||||
</span>
|
||||
<span t-if="!stage.jobs.length" class="o_fp_funnel_empty">—</span>
|
||||
<span t-if="!stage.jobs.length" class="o_fp_funnel_empty">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<Dialog title.translate="Send Parts Forward" size="'md'">
|
||||
<div class="o_fp_move_dialog" t-if="!state.loading">
|
||||
|
||||
<!-- Destination banner — operator sees exactly where parts go,
|
||||
<!-- Destination banner - operator sees exactly where parts go,
|
||||
nothing to guess. -->
|
||||
<div class="o_fp_move_route">
|
||||
<span class="route-from" t-esc="state.fromStep.name"/>
|
||||
@@ -13,7 +13,7 @@
|
||||
<span class="route-to" t-esc="state.toStep.name"/>
|
||||
</div>
|
||||
|
||||
<!-- Qty stepper — no keyboard. Defaults to all parked here. -->
|
||||
<!-- Qty stepper - no keyboard. Defaults to all parked here. -->
|
||||
<div class="o_fp_move_qty">
|
||||
<label>How many to send?</label>
|
||||
<div class="o_fp_qty_stepper">
|
||||
@@ -29,7 +29,7 @@
|
||||
<span class="o_fp_qty_hint"><t t-esc="state.qtyAvailable"/> parked here</span>
|
||||
</div>
|
||||
|
||||
<!-- To Station (tank) — only when the recipe offers a choice -->
|
||||
<!-- To Station (tank) - only when the recipe offers a choice -->
|
||||
<div class="o_fp_move_field"
|
||||
t-if="state.toStep.tank_options and state.toStep.tank_options.length > 1">
|
||||
<label>To Station</label>
|
||||
@@ -40,7 +40,7 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Compliance prompts — only when the recipe author required
|
||||
<!-- Compliance prompts - only when the recipe author required
|
||||
them. Pickers/checkboxes, minimal free text. -->
|
||||
<div class="o_fp_compliance_prompts" t-if="state.transitionPrompts.length">
|
||||
<h5>Required before sending</h5>
|
||||
@@ -60,7 +60,7 @@
|
||||
type="datetime-local" t-model="state.promptValues[p.id]"/>
|
||||
<select t-elif="p.input_type === 'selection'"
|
||||
t-model="state.promptValues[p.id]">
|
||||
<option value="">— Select —</option>
|
||||
<option value="">- Select -</option>
|
||||
<t t-foreach="p.selection_options.split(',')"
|
||||
t-as="opt" t-key="opt_index">
|
||||
<option t-att-value="opt.trim()"><t t-esc="opt.trim()"/></option>
|
||||
@@ -71,7 +71,7 @@
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Blockers — inline resolve where possible -->
|
||||
<!-- Blockers - inline resolve where possible -->
|
||||
<div class="o_fp_blockers" t-if="state.blockers.length">
|
||||
<t t-foreach="state.blockers" t-as="b" t-key="b_index">
|
||||
<div class="o_fp_blocker_row"
|
||||
@@ -87,7 +87,7 @@
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- More options (advanced) — hold / scrap / rework / location.
|
||||
<!-- More options (advanced) - hold / scrap / rework / location.
|
||||
Collapsed by default so the everyday "advance all" flow is
|
||||
a qty confirm + SEND. -->
|
||||
<div class="o_fp_move_advanced_toggle">
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<!-- "Scan QR" = the QrScanner camera path (the
|
||||
primary way to scan a printed job sticker).
|
||||
The component renders its own fa-qrcode
|
||||
icon, so the label must be plain text — an
|
||||
icon, so the label must be plain text - an
|
||||
emoji here would double up the icon.
|
||||
"Enter Code" = the manual / hardware-scanner-
|
||||
gun text drawer (a wedge gun types the code;
|
||||
@@ -76,7 +76,7 @@
|
||||
kind="'qc'"
|
||||
active="!!state.filters.awaiting_qc"
|
||||
onClick="() => this.toggleFilter('awaiting_qc')"/>
|
||||
<!-- Spec 2026-05-25 — post-shop state tiles -->
|
||||
<!-- Spec 2026-05-25 - post-shop state tiles -->
|
||||
<FpKpiTile value="state.data.kpis.awaiting_cert"
|
||||
label="'Awaiting CoC'"
|
||||
kind="'warn'"
|
||||
@@ -121,7 +121,7 @@
|
||||
<FpFilterChip label="'Awaiting QC'"
|
||||
active="!!state.filters.awaiting_qc"
|
||||
onToggle="() => this.toggleFilter('awaiting_qc')"/>
|
||||
<!-- Spec 2026-05-25 — post-shop state chips -->
|
||||
<!-- Spec 2026-05-25 - post-shop state chips -->
|
||||
<FpFilterChip label="'Awaiting CoC'"
|
||||
active="!!state.filters.awaiting_cert"
|
||||
onToggle="() => this.toggleFilter('awaiting_cert')"/>
|
||||
@@ -140,7 +140,7 @@
|
||||
<t t-foreach="filteredCardIds(col)" t-as="card_id" t-key="card_id">
|
||||
<FpPlantCard card="state.data.cards[card_id]"/>
|
||||
</t>
|
||||
<div t-if="filteredCardIds(col).length === 0" class="col-empty">—</div>
|
||||
<div t-if="filteredCardIds(col).length === 0" class="col-empty">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<!-- Per-card timer chip (v19.0.24.10.0). Subcomponent so each chip -->
|
||||
<!-- has its own reactive ticker — a 5s tick re-renders only that -->
|
||||
<!-- has its own reactive ticker - a 5s tick re-renders only that -->
|
||||
<!-- chip instead of the entire 389-card board. -->
|
||||
<t t-name="fusion_plating_shopfloor.TimerChip">
|
||||
<div t-if="display.label"
|
||||
@@ -72,7 +72,7 @@
|
||||
<p class="mt-3 text-muted">No work centres with active orders found.</p>
|
||||
</div>
|
||||
|
||||
<!-- ========== Sub 12b — RACKS PANE ========== -->
|
||||
<!-- ========== Sub 12b - RACKS PANE ========== -->
|
||||
<!-- Top section above the work-centre columns. Shows racks
|
||||
currently in (loaded / in_use / awaiting_unrack) state
|
||||
with tag chips, part count, current node breadcrumb,
|
||||
@@ -167,7 +167,7 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Urgency chip (v19.0.24.8.0) — always -->
|
||||
<!-- Urgency chip (v19.0.24.8.0) - always -->
|
||||
<!-- visible. Explains WHY the card is at -->
|
||||
<!-- this position in the sort. Critical -->
|
||||
<!-- bands (HOT, OVERDUE, MISSED BAKE) -->
|
||||
@@ -230,7 +230,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Per-step timer (v19.0.24.10.0) -->
|
||||
<!-- TimerChip subcomponent — owns its -->
|
||||
<!-- TimerChip subcomponent - owns its -->
|
||||
<!-- own tick so a refresh re-renders one -->
|
||||
<!-- chip, not the whole board. -->
|
||||
<TimerChip
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
|
||||
Process Tree — horizontal hierarchical view.
|
||||
Process Tree - horizontal hierarchical view.
|
||||
Recursive template renders the recipe → sub-process → operation → step
|
||||
hierarchy with bracket connectors between cards. Active step pulses.
|
||||
-->
|
||||
@@ -65,7 +65,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Children — recurse -->
|
||||
<!-- Children - recurse -->
|
||||
<div class="o_fp_pt_children" t-if="node.children and node.children.length">
|
||||
<t t-foreach="node.children" t-as="child" t-key="child.id">
|
||||
<t t-call="fusion_plating_shopfloor.ProcessNode">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc. · License OPL-1
|
||||
Fusion Plating — Reusable QR Scanner template
|
||||
Fusion Plating - Reusable QR Scanner template
|
||||
|
||||
The video element is rendered whenever ANY decoder is available
|
||||
(state.canScan = native BarcodeDetector OR vendored jsQR). The
|
||||
@@ -41,7 +41,7 @@
|
||||
with capture=environment opens the iOS / Android
|
||||
camera UI directly and returns a JPEG when the
|
||||
user taps the shutter. We then run ONE decode
|
||||
on that high-quality still — far more reliable
|
||||
on that high-quality still - far more reliable
|
||||
on iOS than the live-video path. -->
|
||||
<div class="o_fp_qr_photo_row">
|
||||
<label class="btn btn-outline-secondary o_fp_qr_photo_btn">
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="o_fp_move_field">
|
||||
<label/>
|
||||
<select t-model.number="state.selectedRackId">
|
||||
<option value="">— Select empty rack —</option>
|
||||
<option value="">- Select empty rack -</option>
|
||||
<t t-foreach="state.racks" t-as="r" t-key="r.id">
|
||||
<option t-att-value="r.id">
|
||||
<t t-esc="r.name"/> (<t t-esc="r.rack_type"/>)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc. · License OPL-1
|
||||
Fusion Plating — Tablet Station (Worker view)
|
||||
Fusion Plating - Tablet Station (Worker view)
|
||||
Rebuilt 2026-04 with the shop-floor design system.
|
||||
-->
|
||||
<templates xml:space="preserve">
|
||||
@@ -31,7 +31,7 @@
|
||||
<select class="o_fp_station_picker"
|
||||
t-on-change="onPickStation"
|
||||
t-if="state.overview">
|
||||
<option value="">— Pick station —</option>
|
||||
<option value="">- Pick station -</option>
|
||||
<t t-foreach="state.overview.stations" t-as="s" t-key="s.id">
|
||||
<option t-att-value="s.id"
|
||||
t-att-selected="state.stationId === s.id">
|
||||
@@ -156,7 +156,7 @@
|
||||
</button>
|
||||
<button class="btn btn-outline-danger"
|
||||
t-on-click="() => this.onBumpScrap(this.state.overview.active_wo.mo_id)"
|
||||
title="Record one scrap part — auto-creates a Hold">
|
||||
title="Record one scrap part - auto-creates a Hold">
|
||||
<i class="fa fa-trash"/> Scrap
|
||||
</button>
|
||||
<button class="o_fp_big_button"
|
||||
@@ -178,7 +178,7 @@
|
||||
</div>
|
||||
<div t-if="!state.overview.my_queue.length" class="o_fp_empty">
|
||||
<i class="fa fa-check-circle text-success"/>
|
||||
<div>All caught up — nothing waiting on you.</div>
|
||||
<div>All caught up - nothing waiting on you.</div>
|
||||
</div>
|
||||
<ul class="o_fp_queue_list" t-if="state.overview.my_queue.length">
|
||||
<t t-foreach="state.overview.my_queue" t-as="row" t-key="row.id">
|
||||
@@ -203,15 +203,15 @@
|
||||
</t>
|
||||
</div>
|
||||
<div class="o_fp_queue_desc"><t t-esc="row.description"/></div>
|
||||
<!-- S14 — predecessor block notice. Replaces -->
|
||||
<!-- S14 - predecessor block notice. Replaces -->
|
||||
<!-- the green Start with a clear "wait for X". -->
|
||||
<div class="o_fp_queue_blocked_msg"
|
||||
t-if="row.predecessor_blocked">
|
||||
<i class="fa fa-lock"/>
|
||||
Awaiting <strong t-esc="row.blocked_by_name"/>
|
||||
— finish that step first
|
||||
- finish that step first
|
||||
</div>
|
||||
<!-- S13 — recipe-author chips inline -->
|
||||
<!-- S13 - recipe-author chips inline -->
|
||||
<div class="o_fp_queue_chips"
|
||||
t-if="row.thickness_target or row.dwell_time_minutes or row.bake_setpoint_temp or row.requires_signoff">
|
||||
<span class="o_fp_chip o_fp_chip_info"
|
||||
@@ -232,7 +232,7 @@
|
||||
Sign-off
|
||||
</span>
|
||||
</div>
|
||||
<!-- S13 — instructions snippet (first 120 chars) -->
|
||||
<!-- S13 - instructions snippet (first 120 chars) -->
|
||||
<div class="o_fp_queue_instructions"
|
||||
t-if="row.instructions">
|
||||
<t t-esc="row.instructions.length > 120 ? row.instructions.slice(0,120) + '…' : row.instructions"/>
|
||||
@@ -278,7 +278,7 @@
|
||||
<div class="o_fp_tile"
|
||||
t-on-click="() => this.openRecord('fusion.plating.bath', b.id)">
|
||||
<div class="o_fp_tile_title"><t t-esc="b.name"/></div>
|
||||
<div class="o_fp_tile_meta">Tank <t t-esc="b.tank || '—'"/></div>
|
||||
<div class="o_fp_tile_meta">Tank <t t-esc="b.tank || '-'"/></div>
|
||||
<div class="o_fp_tile_chips">
|
||||
<span t-att-class="'o_fp_chip o_fp_chip_' + stateBadge(b.state)">
|
||||
<t t-esc="b.state"/>
|
||||
@@ -308,7 +308,7 @@
|
||||
<div class="o_fp_bake_main">
|
||||
<div class="o_fp_bake_name">
|
||||
<t t-esc="bw.name"/>
|
||||
<span class="text-muted ms-1"> — <t t-esc="bw.part_ref"/></span>
|
||||
<span class="text-muted ms-1"> - <t t-esc="bw.part_ref"/></span>
|
||||
</div>
|
||||
<div class="o_fp_bake_meta">
|
||||
<t t-esc="bw.customer"/>
|
||||
@@ -344,13 +344,13 @@
|
||||
|
||||
<!-- First-piece gate panel retired with the fp.first.piece.gate
|
||||
model removal (19.0.33.2.0). The feature was never wired
|
||||
up — manual create, no enforcement, no rows in production. -->
|
||||
up - manual create, no enforcement, no rows in production. -->
|
||||
|
||||
<!-- ===== Pending QC banner (S19 follow-up) ===== -->
|
||||
<!-- Shows whenever Carlos's job has an open QC. Tap -->
|
||||
<!-- the QC name to deep-link straight into Lisa's -->
|
||||
<!-- mobile checklist. Without this Carlos doesn't -->
|
||||
<!-- know to call inspection — QC sits in draft. -->
|
||||
<!-- know to call inspection - QC sits in draft. -->
|
||||
<section class="o_fp_panel"
|
||||
t-if="state.overview.pending_qcs and state.overview.pending_qcs.length">
|
||||
<div class="o_fp_panel_head">
|
||||
@@ -366,7 +366,7 @@
|
||||
<div class="o_fp_bake_name">
|
||||
<t t-esc="qc.name"/>
|
||||
<span class="text-muted ms-1">
|
||||
— <t t-esc="qc.template_name"/>
|
||||
- <t t-esc="qc.template_name"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="o_fp_bake_meta">
|
||||
@@ -403,7 +403,7 @@
|
||||
<div class="o_fp_bake_main">
|
||||
<div class="o_fp_bake_name">
|
||||
<t t-esc="h.name"/>
|
||||
<span class="text-muted ms-1"> — <t t-esc="h.part_ref"/></span>
|
||||
<span class="text-muted ms-1"> - <t t-esc="h.part_ref"/></span>
|
||||
</div>
|
||||
<div class="o_fp_bake_meta">
|
||||
Qty <t t-esc="h.qty"/>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
</div>
|
||||
<div t-else="" class="o_fp_lock_pinwrap">
|
||||
|
||||
<!-- Mode: 'pin' — default keypad for users with PIN -->
|
||||
<!-- Mode: 'pin' - default keypad for users with PIN -->
|
||||
<t t-if="state.mode === 'pin'">
|
||||
<FpPinPad onSubmit.bind="unlock"
|
||||
title="_selectedTileName()"
|
||||
@@ -78,7 +78,7 @@
|
||||
</button>
|
||||
</t>
|
||||
|
||||
<!-- Mode: 'request_code' — Send Temp PIN screen -->
|
||||
<!-- Mode: 'request_code' - Send Temp PIN screen -->
|
||||
<t t-elif="state.mode === 'request_code'">
|
||||
<div class="o_fp_lock_wizard">
|
||||
<h3 t-esc="_selectedTileName()"/>
|
||||
@@ -100,7 +100,7 @@
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Mode: 'enter_temp_code' — 4-cell pad for emailed code -->
|
||||
<!-- Mode: 'enter_temp_code' - 4-cell pad for emailed code -->
|
||||
<t t-elif="state.mode === 'enter_temp_code'">
|
||||
<div class="o_fp_lock_wizard">
|
||||
<h3 t-esc="_selectedTileName()"/>
|
||||
@@ -124,7 +124,7 @@
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Mode: 'set_new_pin' — 4-cell pad to choose new PIN -->
|
||||
<!-- Mode: 'set_new_pin' - 4-cell pad to choose new PIN -->
|
||||
<t t-elif="state.mode === 'set_new_pin'">
|
||||
<FpPinPad onSubmit.bind="onNewPinSubmit"
|
||||
title="_selectedTileName()"
|
||||
@@ -132,7 +132,7 @@
|
||||
onCancel.bind="onPinCancel"/>
|
||||
</t>
|
||||
|
||||
<!-- Mode: 'confirm_new_pin' — 4-cell pad to confirm -->
|
||||
<!-- Mode: 'confirm_new_pin' - 4-cell pad to confirm -->
|
||||
<t t-elif="state.mode === 'confirm_new_pin'">
|
||||
<FpPinPad onSubmit.bind="onConfirmNewPinSubmit"
|
||||
title="_selectedTileName()"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc. — License OPL-1
|
||||
"""Plan task P3.1 — /fp/landing/kanban endpoint."""
|
||||
# Copyright 2026 Nexa Systems Inc. - License OPL-1
|
||||
"""Plan task P3.1 - /fp/landing/kanban endpoint."""
|
||||
import json
|
||||
|
||||
from odoo.tests.common import HttpCase, tagged
|
||||
|
||||
@@ -156,7 +156,7 @@ class TestSetPinViaResetToken(TransactionCase):
|
||||
# Verify the hash matches
|
||||
self.assertTrue(self.user.verify_tablet_pin('1234'))
|
||||
# Token still valid (single-use happens at set_pin endpoint
|
||||
# call site — model-level _sign / _verify is stateless)
|
||||
# call site - model-level _sign / _verify is stateless)
|
||||
uid = Reset._verify_reset_token(token)
|
||||
self.assertEqual(uid, self.user.id)
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
"""Tests for the tablet lock-screen payload helpers.
|
||||
|
||||
Covers the 3 module-level helpers added in 2026-05-24 redesign:
|
||||
- _initials_from(name) — letter-mark fallback for missing logo/photo
|
||||
- _avatar_gradient_for(uid) — deterministic per-user color gradient
|
||||
- _lock_company_payload(env) — company name + tagline + logo URL block
|
||||
- _initials_from(name) - letter-mark fallback for missing logo/photo
|
||||
- _avatar_gradient_for(uid) - deterministic per-user color gradient
|
||||
- _lock_company_payload(env) - company name + tagline + logo URL block
|
||||
|
||||
End-to-end test of the /fp/tablet/tiles endpoint payload shape:
|
||||
just verifies the helper output ends up in the response.
|
||||
@@ -52,7 +52,7 @@ class TestAvatarGradientFor(TransactionCase):
|
||||
)
|
||||
|
||||
def test_modulo_distribution(self):
|
||||
# Wrapping wraps cleanly — id 0 and id len(gradients) match
|
||||
# Wrapping wraps cleanly - id 0 and id len(gradients) match
|
||||
n = len(_AVATAR_GRADIENTS)
|
||||
self.assertEqual(_avatar_gradient_for(0), _avatar_gradient_for(n))
|
||||
self.assertEqual(_avatar_gradient_for(3), _avatar_gradient_for(n + 3))
|
||||
@@ -84,7 +84,7 @@ class TestLockCompanyPayload(TransactionCase):
|
||||
# brittle here because res.company.report_header is an HTML field in
|
||||
# Odoo 19: setting a plain string can come back wrapped in <p> tags
|
||||
# after sanitization. The helper's responsibility is just "use the
|
||||
# field's value when present, else fall back" — covered by
|
||||
# field's value when present, else fall back" - covered by
|
||||
# test_tagline_default_when_empty_report_header above.
|
||||
|
||||
def test_initials_match_company_name(self):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user