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:
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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 "";
|
||||
}
|
||||
|
||||
@@ -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_id — optional; 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";
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user