feat(jobs): Phase 6 — Process Tree OWL component for fp.job

Ports fusion_plating_shopfloor's process_tree.js to bind to fp.job
instead of mrp.production. Consumes the /fp/jobs/process_tree
JSON endpoint built in Phase 6 lean.

Renders the recipe tree as cards. Each operation card shows the
step state (pending/ready/in_progress/done/etc.) when there's a
matching fp.job.step. Click an operation card -> open the step
form. Click Back -> return to the job form.

New 'Process Tree' button on the fp.job form (manager-only)
launches the client action with job_id context.

Manifest 19.0.2.1.0 -> 19.0.2.2.0.

Part of: native job model migration (spec 2026-04-25)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-25 04:20:09 -04:00
parent 47a54eac8f
commit 034a6560ad
7 changed files with 778 additions and 1 deletions

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
{
'name': 'Fusion Plating — Native Jobs',
'version': '19.0.2.1.0',
'version': '19.0.2.2.0',
'category': 'Manufacturing/Plating',
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
'description': """
@@ -38,9 +38,18 @@ full design rationale and §6.2 of the implementation plan for task list.
'data': [
'security/ir.model.access.csv',
'views/res_config_settings_views.xml',
'views/job_process_tree_action.xml',
'views/fp_job_form_inherit.xml',
'report/report_fp_job_sticker.xml',
'report/report_fp_job_traveller.xml',
],
'assets': {
'web.assets_backend': [
'fusion_plating_jobs/static/src/scss/job_process_tree.scss',
'fusion_plating_jobs/static/src/js/job_process_tree.js',
'fusion_plating_jobs/static/src/xml/job_process_tree.xml',
],
},
'installable': True,
'application': False,
'auto_install': False,

View File

@@ -253,6 +253,26 @@ class FpJob(models.Model):
)
return True
# ------------------------------------------------------------------
# UI — Process Tree client action (Phase 6)
# ------------------------------------------------------------------
def action_open_process_tree(self):
"""Open the OWL process-tree visualization for this job.
Launches the fp_job_process_tree client action with job_id in
context. The component fetches /fp/jobs/process_tree and renders
the recipe -> sub_process -> operation hierarchy as cards with
per-step state badges.
"""
self.ensure_one()
return {
'type': 'ir.actions.client',
'tag': 'fp_job_process_tree',
'context': {'job_id': self.id},
'name': 'Process Tree — %s' % (self.name or ''),
'target': 'current',
}
# ------------------------------------------------------------------
# Lifecycle hooks (Tasks 2.6, 2.7, 2.8)
#

View File

@@ -0,0 +1,207 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating — Job Process Tree (horizontal hierarchical view, fp.job)
// Copyright 2026 Nexa Systems Inc. · License OPL-1
//
// Renders an fp.job's recipe (recipe → sub_process → operation) as a
// horizontal bracket tree, port of fusion_plating_shopfloor's process_tree.js
// rebound to fp.job + fp.job.step (instead of mrp.production + mrp.workorder).
//
// Action context:
// job_id — required; the fp.job whose recipe to render
// back_step_id — optional; if set, the back button returns to that step
// instead of the job form
//
// Endpoint: POST /fp/jobs/process_tree (fusion_plating_jobs/controllers)
// payload : { job_id: <int> }
// response : { job_name, partner, state, qty, recipe_name, progress_pct,
// tree: { id, name, node_type, sequence,
// step_id, step_state, step_assigned_user,
// duration_expected, duration_actual, children: [...] } }
// =============================================================================
import { Component, useState, onMounted } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { rpc } from "@web/core/network/rpc";
import { useService } from "@web/core/utils/hooks";
export class JobProcessTree extends Component {
static template = "fusion_plating_jobs.JobProcessTree";
static props = ["*"];
setup() {
this.notification = useService("notification");
this.action = useService("action");
this.state = useState({
jobName: "",
partner: "",
jobState: "",
qty: 0,
recipe: "",
progressPct: 0,
root: null,
loading: false,
});
onMounted(async () => {
await this.loadTree();
});
}
// ---- Action context -----------------------------------------------------
get _ctx() {
const a = this.props.action || {};
return { ...(a.context || {}), ...(a.params || {}) };
}
get jobId() { return this._ctx.job_id || null; }
get backStepId() { return this._ctx.back_step_id || null; }
get backLabel() {
return this.backStepId ? "Back to Step" : "Back to Job";
}
// ---- Data ---------------------------------------------------------------
async loadTree() {
const jobId = this.jobId;
if (!jobId) {
this.notification.add(
"No job specified for the process tree.",
{ type: "warning" },
);
return;
}
this.state.loading = true;
try {
const r = await rpc("/fp/jobs/process_tree", {
job_id: jobId,
});
if (r && !r.error) {
this.state.jobName = r.job_name || "";
this.state.partner = r.partner || "";
this.state.jobState = r.state || "";
this.state.qty = r.qty || 0;
this.state.recipe = r.recipe_name || "";
this.state.progressPct = r.progress_pct || 0;
this.state.root = r.tree || null;
} else if (r && r.error) {
this.notification.add(r.error, { type: "warning" });
}
} catch (err) {
this.notification.add(
`Failed to load process tree: ${err.message || err}`,
{ type: "danger" },
);
} finally {
this.state.loading = false;
}
}
// ---- Navigation ---------------------------------------------------------
onNodeClick(node) {
// Only operation cards with a matching fp.job.step are clickable —
// they open the underlying step form.
if (!node || !node.step_id) {
return;
}
this.action.doAction({
type: "ir.actions.act_window",
res_model: "fp.job.step",
res_id: node.step_id,
views: [[false, "form"]],
target: "current",
});
}
onBack() {
const stepId = this.backStepId;
if (stepId) {
this.action.doAction({
type: "ir.actions.act_window",
res_model: "fp.job.step",
res_id: parseInt(stepId, 10),
views: [[false, "form"]],
target: "current",
});
return;
}
// Default back: open the job form.
const jobId = this.jobId;
if (jobId) {
this.action.doAction({
type: "ir.actions.act_window",
res_model: "fp.job",
res_id: parseInt(jobId, 10),
views: [[false, "form"]],
target: "current",
});
return;
}
// Fallback — pop the stack.
this.action.doAction({ type: "ir.actions.act_window_close" });
}
// ---- Helpers ------------------------------------------------------------
/** Return the css class chain for a node card (state + node_type). */
getCardClass(node) {
const parts = ["o_fp_jpt_card"];
parts.push(`o_fp_jpt_type_${node.node_type || "unknown"}`);
if (node.step_state) {
parts.push(`o_fp_jpt_state_${node.step_state}`);
}
if (node.step_id) {
parts.push("o_fp_jpt_clickable");
}
if (this.isHighlight(node)) {
parts.push("o_fp_jpt_highlight");
}
return parts.join(" ");
}
/** Highlight steps that are live (ready / in_progress / paused). */
isHighlight(node) {
return node.step_state === "ready"
|| node.step_state === "in_progress"
|| node.step_state === "paused";
}
/** Friendly label for the step state badge. */
stateLabel(node) {
if (!node.step_state) return null;
const map = {
pending: "Pending",
ready: "Ready",
in_progress: "In Progress",
paused: "Paused",
done: "Done",
skipped: "Skipped",
cancelled: "Cancelled",
};
return map[node.step_state] || node.step_state;
}
/** Concise duration label: "actual / expected min" when available. */
durationLabel(node) {
const exp = node.duration_expected;
const act = node.duration_actual;
if (act && exp) return `${act.toFixed(0)}/${exp.toFixed(0)} min`;
if (exp) return `${exp.toFixed(0)} min`;
if (act) return `${act.toFixed(0)} min`;
return "";
}
nodeIcon(node) {
switch (node.node_type) {
case "recipe": return "fa-cubes";
case "sub_process": return "fa-folder";
case "operation": return "fa-cog";
case "step": return "fa-circle-o";
default: return "fa-square";
}
}
}
registry.category("actions").add("fp_job_process_tree", JobProcessTree);

View File

@@ -0,0 +1,384 @@
// =============================================================================
// Fusion Plating — Job Process Tree (horizontal hierarchical, v1, 2026-04)
// Copyright 2026 Nexa Systems Inc. · License OPL-1
//
// Parallel of fusion_plating_shopfloor's process_tree.scss, rebound to
// fp.job. Self-contained — does NOT pull in the shopfloor token partial,
// so this module stays free of the shopfloor dependency.
//
// Class prefix: .o_fp_jpt_* (Job Process Tree)
//
// Hierarchical bracket tree:
//
// [Recipe]──┬──[Sub-Process]──┬──[Operation]
// │ └──[Operation]
// ├──[Operation]
// └──[Operation]
//
// Each .o_fp_jpt_node is `display: flex` with:
// - the card on the left
// - .o_fp_jpt_children on the right (column of recursed children)
// Connectors are drawn entirely from CSS pseudo-elements:
// - vertical bus column on each child via ::after
// - horizontal stub from bus column to card via ::before
// - first/last children trim the vertical line so it stops at the card
// centre.
// =============================================================================
// Suppress hover transforms on touch devices so taps don't leave cards
// stuck in the hover state.
@media (hover: none) {
.o_fp_job_process_tree [class*="o_fp_jpt_"]:hover {
transform: none !important;
box-shadow: inherit !important;
}
}
// --- Connector geometry ------------------------------------------------------
// Tweaking these recalculates the whole bracket-tree layout.
$jpt-card-h : 44px; // nominal card height (centre stays at h/2)
$jpt-row-gap : 12px; // vertical gap between sibling children
$jpt-indent : 36px; // horizontal gap from parent → children
$jpt-stub : 28px; // horizontal connector segment length
$jpt-line-color : #6b7280; // connector colour
$jpt-line-width : 2px;
.o_fp_job_process_tree.o_fp_jpt_v1 {
height: 100%;
overflow: auto; // both axes — wide trees scroll horizontally
-webkit-overflow-scrolling: touch;
padding: 16px 24px;
display: flex;
flex-direction: column;
gap: 12px;
background-color: var(--o-action, #f7f7f8);
color: var(--bs-body-color, #1a1d21);
@media (max-width: 600px) { padding: 12px; gap: 12px; }
// -------------------------------------------------------------------------
// Header (compact strip)
// -------------------------------------------------------------------------
.o_fp_jpt_header {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
padding: 12px 16px;
background-color: var(--bs-body-bg, #ffffff);
border: 1px solid #d8dadd;
border-radius: 8px;
position: sticky;
top: 0;
z-index: 5;
}
.o_fp_jpt_back {
display: inline-flex;
align-items: center;
padding: 6px 12px;
border-radius: 999px;
background-color: var(--bs-tertiary-bg, #f1f3f5);
color: var(--bs-body-color, #1a1d21);
font-weight: 500;
font-size: 0.875rem;
border: 1px solid #d8dadd;
cursor: pointer;
transition: background-color 0.15s ease,
border-color 0.15s ease,
color 0.15s ease;
&:hover {
background-color: #e9ecef;
border-color: #c5c8cc;
}
}
.o_fp_jpt_title_block { flex: 1 1 auto; min-width: 0; }
.o_fp_jpt_title {
font-size: 1rem;
font-weight: 700;
margin: 0;
display: inline-flex; align-items: center; gap: 4px;
.o_fp_jpt_job_name { font-weight: 600; opacity: 0.8; }
}
.o_fp_jpt_subtitle {
margin-top: 2px;
font-size: 0.75rem;
opacity: 0.7;
display: flex; flex-wrap: wrap; align-items: center; gap: 2px;
.fa { margin-right: 2px; opacity: 0.7; }
}
// -------------------------------------------------------------------------
// Empty / loading
// -------------------------------------------------------------------------
.o_fp_jpt_empty {
text-align: center;
padding: 40px 24px;
opacity: 0.7;
background-color: var(--bs-body-bg, #ffffff);
border: 1px solid #d8dadd;
border-radius: 8px;
font-size: 0.875rem;
max-width: 520px;
> .fa { font-size: 1.75rem; margin-bottom: 8px; opacity: 0.6; }
}
// -------------------------------------------------------------------------
// Tree canvas — horizontally scrollable
// -------------------------------------------------------------------------
.o_fp_jpt_canvas {
padding: 12px 0;
min-width: max-content; // let cards push the canvas wider for scroll
}
// -------------------------------------------------------------------------
// Recursive node — flex row of [card | children-column]
// -------------------------------------------------------------------------
.o_fp_jpt_node {
display: flex;
align-items: flex-start;
position: relative;
}
// -------------------------------------------------------------------------
// Card (Steelhead-style: dark fill, rounded)
// -------------------------------------------------------------------------
.o_fp_jpt_card {
display: inline-flex;
align-items: center;
gap: 10px;
min-width: 220px;
max-width: 340px;
min-height: $jpt-card-h;
padding: 8px 12px;
background-color: #2b2f36; // dark slate
color: #f1f3f5;
border-radius: 6px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
font-size: 0.875rem;
line-height: 1.25;
flex: 0 0 auto;
position: relative;
z-index: 1; // sit above connector lines
transition: transform 0.1s ease,
box-shadow 0.15s ease,
background-color 0.15s ease;
&.o_fp_jpt_clickable {
cursor: pointer;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
background-color: #353a42;
}
}
// ---- Card type tints (subtle) -------------------------------------
&.o_fp_jpt_type_recipe {
background-color: #1f2329;
font-weight: 700;
}
&.o_fp_jpt_type_sub_process {
background-color: #262a31;
font-weight: 600;
}
&.o_fp_jpt_type_step {
background-color: #353a42;
font-size: 0.8rem;
min-height: 36px;
}
// ---- Live state highlight ----------------------------------------
&.o_fp_jpt_state_in_progress {
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);
}
&.o_fp_jpt_highlight.o_fp_jpt_state_ready {
background-color: #c0392b; // ready also red
color: #fff;
box-shadow: 0 0 0 1px rgba(192, 57, 43, .6),
0 4px 14px rgba(192, 57, 43, .35);
}
&.o_fp_jpt_state_paused {
background-color: #b5651d; // amber — paused
color: #fff;
}
&.o_fp_jpt_state_done {
background-color: #1e8449; // green for completed
color: #fff;
}
&.o_fp_jpt_state_skipped,
&.o_fp_jpt_state_cancelled { opacity: 0.55; }
}
.o_fp_jpt_card_icon {
flex: 0 0 auto;
width: 18px;
text-align: center;
opacity: 0.85;
font-size: 0.95em;
}
.o_fp_jpt_card_body {
flex: 1 1 auto;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.o_fp_jpt_card_title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.o_fp_jpt_card_meta {
font-size: 0.72rem;
opacity: 0.75;
display: flex;
flex-wrap: wrap;
gap: 2px 6px;
.fa { opacity: 0.8; }
}
.o_fp_jpt_card_right {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 6px;
}
.o_fp_jpt_card_open {
opacity: 0.55;
font-size: 0.85em;
}
// -------------------------------------------------------------------------
// State badge (right side of operation cards)
// -------------------------------------------------------------------------
.o_fp_jpt_state_badge {
display: inline-flex;
align-items: center;
padding: 1px 7px;
border-radius: 999px;
font-size: 0.65rem;
font-weight: 700;
line-height: 1.4;
white-space: nowrap;
text-transform: uppercase;
letter-spacing: 0.02em;
&.o_fp_jpt_state_badge_pending { background-color: rgba(255,255,255,.12); color: #c8ccd2; }
&.o_fp_jpt_state_badge_ready { background-color: rgba(255, 193, 7, .25); color: #ffd866; }
&.o_fp_jpt_state_badge_in_progress { background-color: rgba(13, 110, 253, .25); color: #6ea8fe; }
&.o_fp_jpt_state_badge_paused { background-color: rgba(255, 145, 0, .28); color: #ffb86b; }
&.o_fp_jpt_state_badge_done { background-color: rgba(25, 135, 84, .28); color: #75d4a4; }
&.o_fp_jpt_state_badge_skipped { background-color: rgba(108, 117, 125, .35); color: #d0d4d9; }
&.o_fp_jpt_state_badge_cancelled { background-color: rgba(220, 53, 69, .25); color: #f1aeb5; }
}
// -------------------------------------------------------------------------
// Children column (recursed nodes laid out vertically to the right)
//
// The ::before pseudo draws the horizontal connector that bridges the
// parent card's right edge → the bus column at left: 0 of this
// container.
// -------------------------------------------------------------------------
.o_fp_jpt_children {
display: flex;
flex-direction: column;
gap: $jpt-row-gap;
margin-left: $jpt-indent;
position: relative;
&::before {
content: "";
position: absolute;
left: -#{$jpt-indent};
top: calc(#{$jpt-card-h} / 2); // parent-card vertical centre
width: $jpt-indent;
height: $jpt-line-width;
background-color: $jpt-line-color;
z-index: 0;
}
}
// -------------------------------------------------------------------------
// Connector lines (bracket style, drawn from CSS only)
//
// Each child .o_fp_jpt_node owns its own connector segments:
// ::before → horizontal stub from the bus column → card centre
// ::after → vertical bus segment for this row
//
// First/last/single children trim the vertical so the bracket stops
// exactly at the card centre.
// -------------------------------------------------------------------------
.o_fp_jpt_children > .o_fp_jpt_node {
position: relative;
padding-left: $jpt-stub; // room for the horizontal stub
// -- horizontal stub from bus column → card --------------------------
&::before {
content: "";
position: absolute;
left: 0;
top: calc(#{$jpt-card-h} / 2); // align with card vertical centre
width: $jpt-stub;
height: $jpt-line-width;
background-color: $jpt-line-color;
z-index: 0;
}
// -- vertical bus segment (default: full row, top → bottom) ----------
&::after {
content: "";
position: absolute;
left: 0;
top: calc(-#{$jpt-row-gap} / 2); // bridge gap to sibling above
bottom: calc(-#{$jpt-row-gap} / 2); // bridge gap to sibling below
width: $jpt-line-width;
background-color: $jpt-line-color;
z-index: 0;
}
// First child — vertical only from card centre → bottom of row
&:first-child::after {
top: calc(#{$jpt-card-h} / 2);
}
// Last child — vertical only from top of row → card centre
&:last-child::after {
bottom: calc(100% - (#{$jpt-card-h} / 2));
}
// Only child — vertical only at the card centre point
&:first-child:last-child::after {
top: calc(#{$jpt-card-h} / 2);
bottom: calc(100% - (#{$jpt-card-h} / 2));
}
}
// -------------------------------------------------------------------------
// Pulse on live (in_progress / ready) cards
// -------------------------------------------------------------------------
@keyframes o_fp_jpt_pulse {
0%, 100% { box-shadow: 0 0 0 1px rgba(192, 57, 43, .55),
0 4px 14px rgba(192, 57, 43, .35); }
50% { box-shadow: 0 0 0 4px rgba(192, 57, 43, .25),
0 4px 18px rgba(192, 57, 43, .45); }
}
.o_fp_jpt_card.o_fp_jpt_state_in_progress,
.o_fp_jpt_card.o_fp_jpt_highlight.o_fp_jpt_state_ready {
animation: o_fp_jpt_pulse 2.4s ease-in-out infinite;
}
}

View File

@@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Job Process Tree — horizontal hierarchical view, fp.job edition.
Recursive template renders the recipe -> sub-process -> operation
hierarchy from the /fp/jobs/process_tree endpoint, with bracket
connectors between cards drawn from CSS.
-->
<templates xml:space="preserve">
<!-- =====================================================================
RECURSIVE NODE TEMPLATE
Expects a `node` set in the t-call context.
===================================================================== -->
<t t-name="fusion_plating_jobs.JobProcessNode">
<div class="o_fp_jpt_node">
<!-- The card itself -->
<div t-att-class="getCardClass(node)"
t-on-click="() => this.onNodeClick(node)">
<i t-attf-class="o_fp_jpt_card_icon fa #{ nodeIcon(node) }"/>
<div class="o_fp_jpt_card_body">
<div class="o_fp_jpt_card_title" t-esc="node.name"/>
<div class="o_fp_jpt_card_meta"
t-if="node.step_assigned_user or durationLabel(node)">
<span t-if="node.step_assigned_user">
<i class="fa fa-user me-1"/><t t-esc="node.step_assigned_user"/>
</span>
<span t-if="durationLabel(node)">
<t t-if="node.step_assigned_user"> · </t>
<i class="fa fa-clock-o me-1"/><t t-esc="durationLabel(node)"/>
</span>
</div>
</div>
<!-- Right-side: state badge / open icon -->
<div class="o_fp_jpt_card_right">
<span t-if="node.step_state"
t-attf-class="o_fp_jpt_state_badge o_fp_jpt_state_badge_#{ node.step_state }"
t-esc="stateLabel(node)"/>
<i class="o_fp_jpt_card_open fa fa-external-link"
t-if="node.step_id"/>
</div>
</div>
<!-- Children — recurse -->
<div class="o_fp_jpt_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_jobs.JobProcessNode">
<t t-set="node" t-value="child"/>
</t>
</t>
</div>
</div>
</t>
<!-- =====================================================================
ROOT TEMPLATE
===================================================================== -->
<t t-name="fusion_plating_jobs.JobProcessTree">
<div class="o_fp_job_process_tree o_fp_jpt_v1">
<!-- ========== HEADER ========== -->
<div class="o_fp_jpt_header">
<button class="o_fp_jpt_back"
t-on-click="onBack"
t-att-title="backLabel">
<i class="fa fa-arrow-left me-2"/>
<t t-esc="backLabel"/>
</button>
<div class="o_fp_jpt_title_block">
<h2 class="o_fp_jpt_title mb-0">
<i class="fa fa-sitemap me-2"/>Process
<span t-if="state.jobName" class="o_fp_jpt_job_name">
· <t t-esc="state.jobName"/>
</span>
</h2>
<div class="o_fp_jpt_subtitle">
<span t-if="state.partner">
<i class="fa fa-user me-1"/><t t-esc="state.partner"/>
</span>
<span t-if="state.jobState"> · <t t-esc="state.jobState"/></span>
<span t-if="state.qty"> · Qty <t t-esc="state.qty"/></span>
<span t-if="state.recipe"> · <i class="fa fa-flask me-1"/><t t-esc="state.recipe"/></span>
<span t-if="state.progressPct"> · <t t-esc="state.progressPct.toFixed(0)"/>%</span>
</div>
</div>
</div>
<!-- ========== LOADING ========== -->
<div class="o_fp_jpt_loading text-center py-4" t-if="state.loading">
<i class="fa fa-spinner fa-spin fa-2x"/>
<p class="mt-2 text-muted small">Loading process...</p>
</div>
<!-- ========== EMPTY ========== -->
<div class="o_fp_jpt_empty"
t-if="!state.loading and !jobId">
<i class="fa fa-exclamation-triangle"/>
<div>No job selected.</div>
</div>
<div class="o_fp_jpt_empty"
t-if="!state.loading and jobId and !state.root">
<i class="fa fa-sitemap"/>
<div>No recipe assigned to this job.</div>
</div>
<!-- ========== TREE ========== -->
<div class="o_fp_jpt_canvas" t-if="state.root">
<t t-call="fusion_plating_jobs.JobProcessNode">
<t t-set="node" t-value="state.root"/>
</t>
</div>
</div>
</t>
</templates>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!--
Adds a "Process Tree" button to the fp.job form header, launching
the fp_job_process_tree client action with job_id in context.
Hidden while the job is in draft (no recipe-derived steps yet).
-->
<record id="view_fp_job_form_jobs_inherit" model="ir.ui.view">
<field name="name">fp.job.form.jobs.inherit</field>
<field name="model">fp.job</field>
<field name="inherit_id" ref="fusion_plating.view_fp_job_form"/>
<field name="arch" type="xml">
<xpath expr="//header" position="inside">
<button name="action_open_process_tree" type="object"
string="Process Tree"
class="btn-secondary"
icon="fa-sitemap"
invisible="state == 'draft'"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!--
fp_job_process_tree client action — opened from the "Process Tree"
button on the fp.job form (action_open_process_tree on fp.job)
with {'job_id': self.id} in context.
-->
<record id="action_job_process_tree" model="ir.actions.client">
<field name="name">Job Process Tree</field>
<field name="tag">fp_job_process_tree</field>
</record>
</odoo>