refactor(shopfloor,jobs): consolidate operator UI into shopfloor

Removes the parallel OWL/controller stack I built in
fusion_plating_jobs (job_process_tree, job_plant_overview,
job_manager_dashboard, job_tablet, job_*.scss, plus parallel
controllers and action XML files). Refactors the existing
fusion_plating_shopfloor components in place to bind to
fp.job / fp.job.step instead of mrp.production / mrp.workorder.

End state:
- ONE operator UI module (shopfloor) instead of two parallel ones
- Existing token system (_fp_shopfloor_tokens.scss) reused as
  designed - no duplicate jobs tokens
- Existing /fp/shopfloor/* RPC URLs preserved (no integration
  breakage); workorder_id kwargs accepted as legacy aliases for
  step_id / job_id so older tablet clients keep working
- Existing visual designs preserved - only the data layer
  underneath changed
- Process Tree button on fp.job form now points at
  fusion_plating_shopfloor's fp_process_tree client action
- Bake Windows / First-Piece Gates / Bake Oven / Operator Queue
  models stay where they were
- legacy_menu_hide.xml trimmed: only the bridge_mrp Production
  Priorities entry remains; the 3 shopfloor menus (Manager Desk,
  Plant Overview, Tablet Station) are now visible (the canonical
  native consoles)

Manifests:
- fusion_plating_jobs 19.0.3.1.0 -> 19.0.4.0.0 (consolidation bump,
  no more bundled JS/SCSS, only job_scan controller retained)
- fusion_plating_shopfloor 19.0.14.4.0 -> 19.0.15.0.0 (asset bundle
  cache-bust + significant controller refactor)

Tests pass on entech: 0 failed, 0 errors of 41 tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-25 06:45:15 -04:00
parent 667654bd4e
commit 5df7d5e6cf
36 changed files with 891 additions and 5128 deletions

View File

@@ -6,6 +6,10 @@
//
// Manager-level view: assign workers, swap tanks, cover no-shows, drill
// into detail when needed. Three columns: Unassigned / In Progress / Team.
//
// Native fp.job / fp.job.step edition (consolidated 2026-04-24). The
// "wo" naming inside payloads is preserved so the existing XML template
// keeps rendering — those keys now carry fp.job.step rows under the hood.
// =============================================================================
import { Component, useState, onMounted, onWillUnmount } from "@odoo/owl";
@@ -244,11 +248,11 @@ export class ManagerDashboard extends Component {
this.action.doAction({
type: "ir.actions.act_window",
name: "Operator Queue",
res_model: "mrp.workorder",
res_model: "fp.job.step",
views: [[false, "list"], [false, "form"]],
domain: [
["x_fc_assigned_user_id", "=", userId],
["state", "in", ["ready", "progress", "waiting"]],
["assigned_user_id", "=", userId],
["state", "in", ["ready", "in_progress", "paused"]],
],
target: "current",
});

View File

@@ -4,8 +4,13 @@
// Copyright 2026 Nexa Systems Inc.
// License OPL-1 (Odoo Proprietary License v1.0)
//
// Steelhead-style multi-column kanban showing all active work orders grouped
// by work centre / station. Auto-refreshes every 30 s.
// Multi-column kanban showing all active fp.job.step rows grouped by
// fp.work.centre. Auto-refreshes every 30 s. Drag-drop between columns
// reassigns step.work_centre_id.
//
// Native fp.job / fp.job.step edition (consolidated 2026-04-24). The
// data layer underneath now points at fp.job.step (cards) / fp.work.centre
// (columns); the visual design and RPC URL paths are unchanged.
//
// Odoo 19 conventions:
// * Backend OWL component: `static template` + `static props = ["*"]`
@@ -133,7 +138,7 @@ export class PlantOverview extends Component {
onCardDragStart(card, col, ev) {
this._draggedCard = {
id: card.id,
source_model: card.source_model || "mrp.workorder",
source_model: card.source_model || "fp.job.step",
source_wc_id: col.work_center_id,
el: ev.target,
};
@@ -251,9 +256,10 @@ export class PlantOverview extends Component {
if (!card.id) {
return;
}
// Try opening the work order form if MRP is available, otherwise
// fall back to bake window or first-piece gate
const model = card.source_model || "mrp.workorder";
// Cards are fp.job.step rows. The model is overridable per-card
// so we keep working if a future card type joins the kanban
// (e.g. a quality hold drop-zone column).
const model = card.source_model || "fp.job.step";
this.action.doAction({
type: "ir.actions.act_window",
res_model: model,
@@ -281,14 +287,21 @@ export class PlantOverview extends Component {
getStateClass(state) {
switch (state) {
case "progress":
// Native fp.job.step states
case "in_progress":
return "o_fp_card_progress";
case "ready":
return "o_fp_card_ready";
case "paused":
return "o_fp_card_pending";
case "done":
return "o_fp_card_done";
case "pending":
return "o_fp_card_pending";
// Legacy MRP states still recognised so a server still
// serving the old payload renders cleanly.
case "progress":
return "o_fp_card_progress";
default:
return "";
}

View File

@@ -3,15 +3,21 @@
// Fusion Plating — Process Tree (horizontal hierarchical view)
// Copyright 2026 Nexa Systems Inc. · License OPL-1
//
// Renders the MO's recipe (recipe → sub_process → operation → state) as a
// horizontal bracket tree. Cards render dark, identical card style across
// Renders an fp.job's recipe (recipe → sub_process → operation → step) as a
// horizontal bracket tree. Cards render dark, identical card style across
// all depths; connector lines are drawn from CSS so the layout stays in
// pure flexbox.
//
// Native fp.job / fp.job.step edition (consolidated 2026-04-24). The data
// layer underneath now points at fp.job + fp.job.step, but the visual
// design is unchanged.
//
// Action context:
// production_id — required; the MO whose recipe to render
// back_workorder_idoptional; if set, the back button returns to
// that WO instead of Plant Overview
// 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
// =============================================================================
import { Component, useState, onMounted } from "@odoo/owl";
@@ -50,19 +56,25 @@ export class ProcessTree extends Component {
const a = this.props.action || {};
return { ...(a.context || {}), ...(a.params || {}) };
}
get productionId() { return this._ctx.production_id || null; }
get backWorkorderId() { return this._ctx.back_workorder_id || null; }
get jobId() {
// job_id is the canonical key; production_id is kept as an alias
// for legacy callers that still encode that name in their URLs.
return this._ctx.job_id || this._ctx.production_id || null;
}
get backStepId() {
return this._ctx.back_step_id || this._ctx.back_workorder_id || null;
}
get backLabel() {
return this.backWorkorderId ? "Back to Work Order" : "Plant Overview";
return this.backStepId ? "Back to Step" : "Plant Overview";
}
// ---- Data ---------------------------------------------------------------
async loadTree() {
const prodId = this.productionId;
if (!prodId) {
const jobId = this.jobId;
if (!jobId) {
this.notification.add(
"No manufacturing order specified for the process tree.",
"No job specified for the process tree.",
{ type: "warning" },
);
return;
@@ -70,7 +82,7 @@ export class ProcessTree extends Component {
this.state.loading = true;
try {
const r = await rpc("/fp/shopfloor/process_tree", {
production_id: prodId,
job_id: jobId,
});
if (r) {
this.state.productionName = r.production_name || "";
@@ -95,25 +107,29 @@ export class ProcessTree extends Component {
// ---- Navigation ---------------------------------------------------------
onNodeClick(node) {
if (!node || !node.workorder_id) {
// 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);
if (!stepId) {
return;
}
this.action.doAction({
type: "ir.actions.act_window",
res_model: "mrp.workorder",
res_id: node.workorder_id,
res_model: "fp.job.step",
res_id: stepId,
views: [[false, "form"]],
target: "current",
});
}
onBack() {
const woId = this.backWorkorderId;
if (woId) {
const stepId = this.backStepId;
if (stepId) {
this.action.doAction({
type: "ir.actions.act_window",
res_model: "mrp.workorder",
res_id: parseInt(woId, 10),
res_model: "fp.job.step",
res_id: parseInt(stepId, 10),
views: [[false, "form"]],
target: "current",
});
@@ -131,7 +147,9 @@ export class ProcessTree extends Component {
if (node.state) {
parts.push(`o_fp_pt_state_${node.state}`);
}
if (node.workorder_id) {
// step_id is the canonical clickable hint; workorder_id is the
// legacy alias. Either one means we have a real step to open.
if (node.step_id || node.workorder_id) {
parts.push("o_fp_pt_clickable");
}
if (this.isHighlight(node)) {
@@ -140,9 +158,13 @@ export class ProcessTree extends Component {
return parts.join(" ");
}
/** A node should pulse-highlight if it is the live position of the MO. */
/** Live-position highlight: ready / in_progress / paused. */
isHighlight(node) {
return node.state === "ready"
|| node.state === "in_progress"
|| node.state === "paused"
// Tolerate the legacy MRP states a node might still
// briefly carry on first render (progress/waiting).
|| node.state === "progress"
|| node.state === "waiting";
}

View File

@@ -4,6 +4,11 @@
// Copyright 2026 Nexa Systems Inc.
// License OPL-1 (Odoo Proprietary License v1.0)
//
// Native fp.job / fp.job.step edition (consolidated 2026-04-24). Start /
// Finish buttons drive fp.job.step.button_start / button_finish through
// the existing /fp/shopfloor/start_wo / stop_wo URLs (now internally
// step-bound). The visual design is unchanged.
//
// Odoo 19 conventions:
// * Backend OWL component using `static template` + `static props = ["*"]`.
// * RPC via standalone `rpc()` from @web/core/network/rpc.

View File

@@ -191,7 +191,7 @@
<i class="fa fa-user me-1"/>Take Over
</button>
<button class="btn o_fp_mgr_btn"
t-on-click="() => this.openRecord('mrp.workorder', wo.id)">
t-on-click="() => this.openRecord('fp.job.step', wo.id)">
<i class="fa fa-external-link me-1"/>Open WO
</button>
</div>
@@ -250,7 +250,7 @@
</t>
</span>
</div>
<span t-att-class="'o_fp_chip o_fp_chip_' + (wo.state === 'progress' ? 'success' : 'info')">
<span t-att-class="'o_fp_chip o_fp_chip_' + (wo.state === 'in_progress' || wo.state === 'progress' ? 'success' : 'info')">
<t t-esc="wo.state"/>
</span>
<button class="btn"
@@ -258,7 +258,7 @@
Take Over
</button>
<button class="btn"
t-on-click="() => this.openRecord('mrp.workorder', wo.id)">
t-on-click="() => this.openRecord('fp.job.step', wo.id)">
Open
</button>
</div>

View File

@@ -61,7 +61,7 @@
t-if="node.qty_total"
t-esc="qtyLabel(node)"/>
<i class="o_fp_pt_card_open fa fa-external-link"
t-if="node.workorder_id"/>
t-if="node.step_id or node.workorder_id"/>
</div>
</div>

View File

@@ -89,7 +89,7 @@
Active: <strong t-esc="state.overview.active_wo.name"/>
</div>
<div class="o_fp_active_wo_meta">
MO <t t-esc="state.overview.active_wo.mo_name"/>
Job <t t-esc="state.overview.active_wo.mo_name"/>
· <t t-esc="state.overview.active_wo.product_name"/>
· Qty <t t-esc="state.overview.active_wo.qty_done"/>/<t t-esc="state.overview.active_wo.qty_total"/>
<t t-if="state.overview.active_wo.workcenter"> @ <t t-esc="state.overview.active_wo.workcenter"/></t>
@@ -97,8 +97,8 @@
</div>
</div>
<button class="o_fp_big_button"
t-on-click="() => openRecord('mrp.workorder', state.overview.active_wo.id)">
Open WO
t-on-click="() => openRecord('fp.job.step', state.overview.active_wo.id)">
Open Step
</button>
</div>