changes
This commit is contained in:
@@ -0,0 +1,133 @@
|
|||||||
|
<h2>Recommended Hybrid: A + B's escape hatch</h2>
|
||||||
|
<p class="subtitle">Layout A's inline badge as default. Power users click "Show alternatives" on any line to reveal B's ranked panel for that line only.</p>
|
||||||
|
|
||||||
|
<div class="mockup">
|
||||||
|
<div class="mockup-header">Bank Reconciliation — Account: RBC Operating · 487 unreconciled</div>
|
||||||
|
<div class="mockup-body" style="padding:14px;font-family:-apple-system,sans-serif;font-size:13px;background:#f3f4f6">
|
||||||
|
|
||||||
|
<div style="background:#fff;border:1px solid #d8dadd;border-radius:8px;margin-bottom:10px;overflow:hidden">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 14px">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:600">Apr 12 — RBC e-transfer</div>
|
||||||
|
<div style="color:#666;font-size:12px;margin-top:2px">Cheque 4827 · Westin Plating Co · <strong>$1,847.50 CAD</strong></div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:8px;align-items:center">
|
||||||
|
<div style="background:#22c55e;color:#fff;padding:4px 10px;border-radius:14px;font-size:11px;font-weight:700;letter-spacing:0.3px">92% MATCH</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding:10px 14px;background:#f0fdf4;border-top:1px solid #d1fae5;display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div style="font-size:12px;color:#166534">
|
||||||
|
💡 <strong>INV/2026/00123</strong> — Westin Plating Co — $1,847.50
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:6px">
|
||||||
|
<button style="background:#22c55e;color:#fff;border:none;padding:5px 12px;border-radius:5px;font-size:11px;font-weight:600;cursor:pointer">Accept</button>
|
||||||
|
<button style="background:#fff;color:#666;border:1px solid #d8dadd;padding:5px 10px;border-radius:5px;font-size:11px;cursor:pointer">Reject</button>
|
||||||
|
<button style="background:transparent;color:#666;border:none;padding:5px 8px;font-size:11px;cursor:pointer;text-decoration:underline">Show 2 alternatives</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:#fff;border:1px solid #fde68a;border-radius:8px;margin-bottom:10px;overflow:hidden">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 14px">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:600">Apr 12 — RBC payment</div>
|
||||||
|
<div style="color:#666;font-size:12px;margin-top:2px">Cheque 4828 · partner unknown · <strong>$1,800.00 CAD</strong></div>
|
||||||
|
</div>
|
||||||
|
<div style="background:#f59e0b;color:#fff;padding:4px 10px;border-radius:14px;font-size:11px;font-weight:700">68% MATCH</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding:10px 14px;background:#fffbeb;border-top:1px solid #fde68a;display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div style="font-size:12px;color:#92400e">
|
||||||
|
💡 <strong>INV/2026/00098</strong> — Westin Plating Co — $1,800.00 · <em style="color:#a16207">amount matches but partner unconfirmed</em>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:6px">
|
||||||
|
<button style="background:#f59e0b;color:#fff;border:none;padding:5px 12px;border-radius:5px;font-size:11px;font-weight:600;cursor:pointer">Accept</button>
|
||||||
|
<button style="background:#fff;color:#666;border:1px solid #d8dadd;padding:5px 10px;border-radius:5px;font-size:11px;cursor:pointer">Reject</button>
|
||||||
|
<button style="background:transparent;color:#666;border:none;padding:5px 8px;font-size:11px;cursor:pointer;text-decoration:underline">Show 4 alternatives</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:#fff;border:1px solid #d8dadd;border-radius:8px;margin-bottom:10px;overflow:hidden">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 14px">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:600">Apr 11 — Visa adjustment</div>
|
||||||
|
<div style="color:#666;font-size:12px;margin-top:2px">Ref VSA-201 · Royal Bank fees · <strong>$89.99 CAD</strong></div>
|
||||||
|
</div>
|
||||||
|
<div style="background:#94a3b8;color:#fff;padding:4px 10px;border-radius:14px;font-size:11px;font-weight:700">NO MATCH</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding:8px 14px;background:#f8fafc;border-top:1px solid #e2e8f0;display:flex;gap:6px;justify-content:flex-end">
|
||||||
|
<button style="background:#fff;color:#666;border:1px solid #d8dadd;padding:5px 10px;border-radius:5px;font-size:11px;cursor:pointer">Reconcile manually</button>
|
||||||
|
<button style="background:#fff;color:#666;border:1px solid #d8dadd;padding:5px 10px;border-radius:5px;font-size:11px;cursor:pointer">Apply rule</button>
|
||||||
|
<button style="background:#fff;color:#666;border:1px solid #d8dadd;padding:5px 10px;border-radius:5px;font-size:11px;cursor:pointer">Write off</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:#fff;border:2px solid #22c55e;border-radius:8px;margin-bottom:10px;overflow:hidden">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 14px;background:#f0fdf4">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:600">Apr 11 — RBC bulk deposit</div>
|
||||||
|
<div style="color:#666;font-size:12px;margin-top:2px">Ref 9921-D · Westin Plating Co · <strong>$3,200.00 CAD</strong></div>
|
||||||
|
</div>
|
||||||
|
<div style="background:#22c55e;color:#fff;padding:4px 10px;border-radius:14px;font-size:11px;font-weight:700">98% MATCH (alternatives expanded)</div>
|
||||||
|
</div>
|
||||||
|
<div style="background:#f8fafc;padding:10px 14px;border-top:1px solid #d1fae5">
|
||||||
|
<div style="font-size:11px;color:#666;margin-bottom:8px;text-transform:uppercase;letter-spacing:0.4px;font-weight:600">AI suggestions, ranked</div>
|
||||||
|
<div style="background:#fff;border:1px solid #22c55e;border-radius:6px;padding:8px 10px;margin-bottom:5px;display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div>
|
||||||
|
<div style="font-size:12px;font-weight:600;color:#166534">98% — INV/2026/00145 — $3,200.00 · Westin Plating Co</div>
|
||||||
|
<div style="font-size:11px;color:#666;margin-top:2px">Exact amount + same partner + invoice date Apr 8 · 4 prior reconciles match this pattern</div>
|
||||||
|
</div>
|
||||||
|
<button style="background:#22c55e;color:#fff;border:none;padding:5px 14px;border-radius:5px;font-size:11px;font-weight:600;cursor:pointer">Accept</button>
|
||||||
|
</div>
|
||||||
|
<div style="background:#fff;border:1px solid #fde68a;border-radius:6px;padding:8px 10px;margin-bottom:5px;display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div>
|
||||||
|
<div style="font-size:12px;font-weight:600;color:#92400e">71% — INV/2026/00141 — $3,200.00 · Bramalea Lift Co</div>
|
||||||
|
<div style="font-size:11px;color:#666;margin-top:2px">Amount matches, partner is a different client</div>
|
||||||
|
</div>
|
||||||
|
<button style="background:#fff;color:#666;border:1px solid #d8dadd;padding:5px 12px;border-radius:5px;font-size:11px;cursor:pointer">Use this</button>
|
||||||
|
</div>
|
||||||
|
<div style="background:#fff;border:1px solid #d8dadd;border-radius:6px;padding:8px 10px;display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div>
|
||||||
|
<div style="font-size:12px;font-weight:600;color:#666">62% — INV/2026/00139 + INV/2026/00140 (combined) — Westin Plating Co</div>
|
||||||
|
<div style="font-size:11px;color:#666;margin-top:2px">Two invoices summing to $3,200.00</div>
|
||||||
|
</div>
|
||||||
|
<button style="background:#fff;color:#666;border:1px solid #d8dadd;padding:5px 12px;border-radius:5px;font-size:11px;cursor:pointer">Use this</button>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:6px"><button style="background:transparent;color:#666;border:none;padding:4px;font-size:11px;cursor:pointer;text-decoration:underline">Hide alternatives</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:#fff;border:1px solid #d8dadd;border-radius:8px;padding:8px 14px;text-align:center">
|
||||||
|
<button style="background:#22c55e;color:#fff;border:none;padding:8px 22px;border-radius:6px;font-size:12px;font-weight:700;cursor:pointer">Accept all 47 high-confidence (≥95%)</button>
|
||||||
|
<span style="color:#666;font-size:11px;margin-left:10px">·</span>
|
||||||
|
<span style="color:#666;font-size:11px;margin-left:8px">487 lines unreconciled · 47 ready to auto-accept · 134 need review · 306 no AI match</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="subtitle">Each line: confidence badge top-right, single suggestion strip below (Accept / Reject / Show alternatives). High-confidence lines have a green border for instant scanning. Bottom bar offers batch-accept of all ≥95% matches at once. The 4th line shows what "Show alternatives" reveals when expanded — B's ranked panel inline.</p>
|
||||||
|
|
||||||
|
<div class="options">
|
||||||
|
<div class="option" data-choice="approve" onclick="toggleSelect(this)">
|
||||||
|
<div class="letter">✓</div>
|
||||||
|
<div class="content">
|
||||||
|
<h3>Looks right — proceed with this hybrid</h3>
|
||||||
|
<p>I'll capture this as the default UI design in the spec. Specific colour choices and exact pixel spacing get refined during implementation.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="option" data-choice="adjust" onclick="toggleSelect(this)">
|
||||||
|
<div class="letter">✏</div>
|
||||||
|
<div class="content">
|
||||||
|
<h3>Mostly right but I want changes</h3>
|
||||||
|
<p>Tell me in the terminal what to adjust (positions, colours, button labels, missing actions, etc.).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="option" data-choice="back_to_pure_a" onclick="toggleSelect(this)">
|
||||||
|
<div class="letter">A</div>
|
||||||
|
<div class="content">
|
||||||
|
<h3>Just pure A, no alternatives panel</h3>
|
||||||
|
<p>Keep it simple — single suggestion per line, no expand. If user disagrees with AI they go to the manual reconcile dialog.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<h2>AI Suggestion Placement</h2>
|
||||||
|
<p class="subtitle">You picked "AI assistive" — now: how does the AI suggestion appear on each unreconciled bank line? Three layouts:</p>
|
||||||
|
|
||||||
|
<div class="cards" data-multiselect>
|
||||||
|
<div class="card" data-choice="badge_inline" onclick="toggleSelect(this)">
|
||||||
|
<div class="card-image">
|
||||||
|
<div class="mockup">
|
||||||
|
<div class="mockup-header">Layout A — Inline Badge</div>
|
||||||
|
<div class="mockup-body" style="padding:12px;font-family:monospace;font-size:13px;line-height:1.7">
|
||||||
|
<div style="border:1px solid #d8dadd;padding:10px;border-radius:6px;background:#fff">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:600">Apr 12 — RBC ETF deposit</div>
|
||||||
|
<div style="color:#666;font-size:12px">Cheque ref 4827 · $1,847.50 CAD</div>
|
||||||
|
</div>
|
||||||
|
<div style="background:#22c55e;color:#fff;padding:3px 8px;border-radius:12px;font-size:11px;font-weight:600">92% MATCH</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:8px;padding:6px;background:#f0fdf4;border-left:3px solid #22c55e;font-size:12px;color:#166534">
|
||||||
|
💡 Invoice <strong>INV/2026/00123</strong> — Westin Plating Co — $1,847.50 · <a href="#" style="color:#22c55e">Accept</a> · <a href="#" style="color:#666">Reject</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h3>A — Inline Badge + Suggestion Strip</h3>
|
||||||
|
<p>Confidence badge top-right of each line, suggestion strip just below. One-click Accept/Reject. Familiar Enterprise-style line layout, AI feels like a layer added on top.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" data-choice="side_panel" onclick="toggleSelect(this)">
|
||||||
|
<div class="card-image">
|
||||||
|
<div class="mockup">
|
||||||
|
<div class="mockup-header">Layout B — Side Panel</div>
|
||||||
|
<div class="mockup-body" style="padding:12px;font-family:monospace;font-size:12px">
|
||||||
|
<div style="display:flex;gap:8px;height:200px">
|
||||||
|
<div style="flex:1;border:1px solid #d8dadd;border-radius:6px;background:#fff;padding:8px">
|
||||||
|
<div style="font-weight:600;margin-bottom:6px">Bank lines</div>
|
||||||
|
<div style="background:#dbeafe;padding:6px;border-radius:4px;margin-bottom:4px;font-size:11px">Apr 12 RBC $1,847.50 ✓ selected</div>
|
||||||
|
<div style="padding:6px;font-size:11px;color:#666">Apr 12 RBC $245.00</div>
|
||||||
|
<div style="padding:6px;font-size:11px;color:#666">Apr 11 Visa $89.99</div>
|
||||||
|
<div style="padding:6px;font-size:11px;color:#666">Apr 11 RBC $3,200.00</div>
|
||||||
|
</div>
|
||||||
|
<div style="width:200px;border:1px solid #d8dadd;border-radius:6px;background:#f8fafc;padding:8px">
|
||||||
|
<div style="font-weight:600;font-size:11px;margin-bottom:6px">AI Suggestions</div>
|
||||||
|
<div style="padding:6px;background:#fff;border-radius:4px;margin-bottom:4px;font-size:10px">
|
||||||
|
<div style="color:#22c55e;font-weight:600">92% INV/2026/00123</div>
|
||||||
|
<div style="color:#666">Westin Plating $1,847.50</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding:6px;background:#fff;border-radius:4px;font-size:10px">
|
||||||
|
<div style="color:#f59e0b;font-weight:600">68% INV/2026/00098</div>
|
||||||
|
<div style="color:#666">Westin Plating $1,800.00</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h3>B — Dedicated Side Panel</h3>
|
||||||
|
<p>Bank lines on the left, AI suggestions panel on the right, updates as you select a line. Multiple ranked suggestions visible. More screen real estate for AI; line list stays clean.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" data-choice="hover_only" onclick="toggleSelect(this)">
|
||||||
|
<div class="card-image">
|
||||||
|
<div class="mockup">
|
||||||
|
<div class="mockup-header">Layout C — Hover Reveal</div>
|
||||||
|
<div class="mockup-body" style="padding:12px;font-family:monospace;font-size:13px;line-height:1.7">
|
||||||
|
<div style="border:1px solid #d8dadd;padding:10px;border-radius:6px;background:#fff;margin-bottom:6px">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:600">Apr 12 — RBC ETF deposit</div>
|
||||||
|
<div style="color:#666;font-size:12px">Cheque ref 4827 · $1,847.50 CAD</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;align-items:center;gap:6px">
|
||||||
|
<div style="width:8px;height:8px;background:#22c55e;border-radius:50%"></div>
|
||||||
|
<div style="color:#666;font-size:11px;font-style:italic">hover for AI</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="border:1px solid #22c55e;padding:10px;border-radius:6px;background:#f0fdf4;box-shadow:0 4px 12px rgba(0,0,0,0.08)">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:600">Apr 12 — RBC e-transfer</div>
|
||||||
|
<div style="color:#166534;font-size:12px">💡 92% match: INV/2026/00123 — $1,847.50 · <a href="#" style="color:#22c55e">Accept</a></div>
|
||||||
|
</div>
|
||||||
|
<div style="background:#22c55e;color:#fff;padding:3px 8px;border-radius:12px;font-size:11px">92%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h3>C — Hover-to-Reveal</h3>
|
||||||
|
<p>Just a confidence dot on each line. AI details appear on hover/click. Cleanest visual, most Enterprise-like density. Slowest discovery for new users.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="subtitle">Click your preferred option(s). I'll read your selection on the next turn. You can also describe in the terminal what you'd like changed.</p>
|
||||||
29
.superpowers/brainstorm/84408-1776602183/content/intro.html
Normal file
29
.superpowers/brainstorm/84408-1776602183/content/intro.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<h2>Phase 1 — Bank Reconciliation</h2>
|
||||||
|
<p class="subtitle">Brainstorming session for the next sub-module: <code>fusion_accounting_bank_rec</code></p>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>What we're designing</h3>
|
||||||
|
<p>A native bank-rec widget that replaces Odoo Enterprise's <code>account_accountant</code> bank reconciliation, using Odoo 19's frontend OWL architecture. It reads/writes the same <code>account.partial.reconcile</code> tables Community owns, so existing reconciliations are immune to Enterprise uninstall (verified empirically in Phase 0).</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>Reference material I've already scanned</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Roadmap design</strong> Section 4.3 — Phase 1 scope, exit criteria</li>
|
||||||
|
<li><strong>Enterprise V19 source</strong> at <code>RePackaged-Odoo/accounting/account_accountant/</code> — 17 OWL components in <code>static/src/components/bank_reconciliation/</code>, 1 service file (140 lines), 3 inherits on community models, 2 wizards</li>
|
||||||
|
<li><strong>Phase 0 BankRecAdapter</strong> — already present at <code>fusion_accounting_ai/services/data_adapters/bank_rec.py</code> with a stub <code>list_unreconciled_via_fusion()</code> waiting to be filled in</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>How this session works</h3>
|
||||||
|
<ol>
|
||||||
|
<li>I ask clarifying questions one at a time (terminal for scope/concept, this browser for layout/visual)</li>
|
||||||
|
<li>I propose 2-3 architectural approaches with tradeoffs</li>
|
||||||
|
<li>We work through the design section by section</li>
|
||||||
|
<li>I write the spec doc and you approve it</li>
|
||||||
|
<li>Then we transition to writing the implementation plan</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="subtitle">Continuing in terminal for the first question...</p>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh;flex-direction:column;gap:16px">
|
||||||
|
<p class="subtitle">UI layout approved ✓</p>
|
||||||
|
<p class="subtitle">Continuing in terminal — next sections are about file structure, reconcile engine algorithms, and migration. Browser will return for any further visual decisions.</p>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh;flex-direction:column;gap:16px">
|
||||||
|
<p class="subtitle">Spec approved ✓ — committed as <code>2d64f7e</code></p>
|
||||||
|
<p class="subtitle">Now writing the Phase 1 implementation plan in terminal. Browser session can be closed; the visual companion isn't needed for plan-writing.</p>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"reason":"idle timeout","timestamp":1776605003749}
|
||||||
12
.superpowers/brainstorm/84408-1776602183/state/server.log
Normal file
12
.superpowers/brainstorm/84408-1776602183/state/server.log
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{"type":"server-started","port":50540,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:50540","screen_dir":"/Users/gurpreet/Github/Odoo-Modules/.superpowers/brainstorm/84408-1776602183/content","state_dir":"/Users/gurpreet/Github/Odoo-Modules/.superpowers/brainstorm/84408-1776602183/state"}
|
||||||
|
{"type":"screen-added","file":"/Users/gurpreet/Github/Odoo-Modules/.superpowers/brainstorm/84408-1776602183/content/intro.html"}
|
||||||
|
{"type":"screen-added","file":"/Users/gurpreet/Github/Odoo-Modules/.superpowers/brainstorm/84408-1776602183/content/ai-badge-placement.html"}
|
||||||
|
{"type":"screen-added","file":"/Users/gurpreet/Github/Odoo-Modules/.superpowers/brainstorm/84408-1776602183/content/ai-badge-hybrid-v2.html"}
|
||||||
|
{"source":"user-event","type":"click","text":"✓\n \n Looks right — proceed with this hybrid\n I'll capture this as the default UI design in the spec. Specific colour choices and exact pixel spacing get refined during implementation.","choice":"approve","id":null,"timestamp":1776603091592}
|
||||||
|
{"source":"user-event","type":"click","text":"✓\n \n Looks right — proceed with this hybrid\n I'll capture this as the default UI design in the spec. Specific colour choices and exact pixel spacing get refined during implementation.","choice":"approve","id":null,"timestamp":1776603096458}
|
||||||
|
{"source":"user-event","type":"click","text":"✓\n \n Looks right — proceed with this hybrid\n I'll capture this as the default UI design in the spec. Specific colour choices and exact pixel spacing get refined during implementation.","choice":"approve","id":null,"timestamp":1776603097158}
|
||||||
|
{"source":"user-event","type":"click","text":"✓\n \n Looks right — proceed with this hybrid\n I'll capture this as the default UI design in the spec. Specific colour choices and exact pixel spacing get refined during implementation.","choice":"approve","id":null,"timestamp":1776603097583}
|
||||||
|
{"source":"user-event","type":"click","text":"✓\n \n Looks right — proceed with this hybrid\n I'll capture this as the default UI design in the spec. Specific colour choices and exact pixel spacing get refined during implementation.","choice":"approve","id":null,"timestamp":1776603097800}
|
||||||
|
{"source":"user-event","type":"click","text":"✓\n \n Looks right — proceed with this hybrid\n I'll capture this as the default UI design in the spec. Specific colour choices and exact pixel spacing get refined during implementation.","choice":"approve","id":null,"timestamp":1776603098691}
|
||||||
|
{"type":"screen-added","file":"/Users/gurpreet/Github/Odoo-Modules/.superpowers/brainstorm/84408-1776602183/content/waiting-1.html"}
|
||||||
|
{"type":"server-stopped","reason":"idle timeout"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
84418
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -96,6 +96,11 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
|||||||
'views/res_config_settings_views.xml',
|
'views/res_config_settings_views.xml',
|
||||||
'views/fp_menu.xml',
|
'views/fp_menu.xml',
|
||||||
'data/fp_recipe_enp_alum_basic.xml',
|
'data/fp_recipe_enp_alum_basic.xml',
|
||||||
|
'data/fp_recipe_enp_steel_basic.xml',
|
||||||
|
'data/fp_recipe_enp_sp.xml',
|
||||||
|
'data/fp_recipe_general_processing.xml',
|
||||||
|
'data/fp_recipe_anodize.xml',
|
||||||
|
'data/fp_recipe_chem_conversion.xml',
|
||||||
],
|
],
|
||||||
'post_init_hook': 'post_init_hook',
|
'post_init_hook': 'post_init_hook',
|
||||||
'assets': {
|
'assets': {
|
||||||
|
|||||||
386
fusion_plating/fusion_plating/data/fp_recipe_anodize.xml
Normal file
386
fusion_plating/fusion_plating/data/fp_recipe_anodize.xml
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2026 Nexa Systems Inc.
|
||||||
|
License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
|
Recipe: ANODIZE (Sulfuric Anodize — Type II)
|
||||||
|
Source: Client's Steelhead export (April 2026 transcription).
|
||||||
|
|
||||||
|
Anodize is an umbrella workflow covering pre-treatment, the wet
|
||||||
|
anodize line (alkaline clean → etch → deoxidize → sulfuric anodize
|
||||||
|
→ hot water seal), and post-treatment (dry, unrack, de-mask).
|
||||||
|
|
||||||
|
Tree:
|
||||||
|
ANODIZE (recipe)
|
||||||
|
Ready for Solvent Clean (1) (sub-process, auto)
|
||||||
|
Solvent Clean (1) (operation, customer-visible)
|
||||||
|
Blasting (sub-process, signoff, auto)
|
||||||
|
Ready For Blast (operation)
|
||||||
|
Blast (operation, customer-visible)
|
||||||
|
Masking (sub-process, signoff, auto)
|
||||||
|
Ready For Masking (operation)
|
||||||
|
Masking (operation, customer-visible)
|
||||||
|
Racking (sub-process, auto)
|
||||||
|
Ready for Racking (operation)
|
||||||
|
Racking (operation, customer-visible)
|
||||||
|
Anodize Line (sub-process, auto)
|
||||||
|
Ready For Anodize (operation)
|
||||||
|
Alkaline Clean (Tank A1) (operation, customer-visible)
|
||||||
|
Primary Rinse (Tank A2)
|
||||||
|
Secondary Rinse (Tank A4)
|
||||||
|
Etch (Tank A3) (operation, customer-visible)
|
||||||
|
Primary Rinse (Tank A4)
|
||||||
|
Secondary Rinse (Tank A6)
|
||||||
|
Deoxidize (Tank A5) (operation, customer-visible)
|
||||||
|
Primary Rinse (Tank A6)
|
||||||
|
Secondary Rinse (Tank A8)
|
||||||
|
Sulfuric Anodize (operation, auto)
|
||||||
|
Sulfuric Anodize Ramp (Tank A9)
|
||||||
|
Sulfuric Anodize (Tank A9) (operation, signoff, customer-visible)
|
||||||
|
Primary Rinse (Tank A8)
|
||||||
|
Secondary Rinse (Tank A12)
|
||||||
|
Hot Rinse (Tank A17)
|
||||||
|
Hot Water Seal (Tank A16) (operation, customer-visible)
|
||||||
|
Primary Rinse (Tank A12)
|
||||||
|
Hot Rinse (Tank A17) (customer-visible)
|
||||||
|
Anodize Dry (sub-process)
|
||||||
|
Unracking (sub-process, auto)
|
||||||
|
Ready for Unrack (operation)
|
||||||
|
Unracking (operation, customer-visible)
|
||||||
|
De-Masking (sub-process, signoff, auto)
|
||||||
|
Ready for De-Masking (operation)
|
||||||
|
De-Masking (operation, customer-visible)
|
||||||
|
-->
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="0">
|
||||||
|
|
||||||
|
<!-- ========================= ROOT ========================= -->
|
||||||
|
<record id="recipe_anodize" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Anodize</field>
|
||||||
|
<field name="code">ANODIZE</field>
|
||||||
|
<field name="node_type">recipe</field>
|
||||||
|
<field name="icon">fa-flask</field>
|
||||||
|
<field name="sequence">50</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 1. Ready for Solvent Clean (1) ========================= -->
|
||||||
|
<record id="anodize_ready_solvent_clean" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready for Solvent Clean (1)</field>
|
||||||
|
<field name="node_type">sub_process</field>
|
||||||
|
<field name="parent_id" ref="recipe_anodize"/>
|
||||||
|
<field name="icon">fa-shower</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_solvent_clean" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Solvent Clean (1)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="anodize_ready_solvent_clean"/>
|
||||||
|
<field name="icon">fa-shower</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 2. Blasting ========================= -->
|
||||||
|
<record id="anodize_blasting" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Blasting</field>
|
||||||
|
<field name="node_type">sub_process</field>
|
||||||
|
<field name="parent_id" ref="recipe_anodize"/>
|
||||||
|
<field name="icon">fa-bullseye</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_blasting_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready For Blast</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="anodize_blasting"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_blasting_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Blast</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="anodize_blasting"/>
|
||||||
|
<field name="icon">fa-bullseye</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 3. Masking ========================= -->
|
||||||
|
<record id="anodize_masking" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Masking</field>
|
||||||
|
<field name="node_type">sub_process</field>
|
||||||
|
<field name="parent_id" ref="recipe_anodize"/>
|
||||||
|
<field name="icon">fa-paint-brush</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_masking_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready For Masking</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="anodize_masking"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_masking_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Masking</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="anodize_masking"/>
|
||||||
|
<field name="icon">fa-paint-brush</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 4. Racking ========================= -->
|
||||||
|
<record id="anodize_racking" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Racking</field>
|
||||||
|
<field name="node_type">sub_process</field>
|
||||||
|
<field name="parent_id" ref="recipe_anodize"/>
|
||||||
|
<field name="icon">fa-th</field>
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_racking_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready for Racking</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="anodize_racking"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_racking_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Racking</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="anodize_racking"/>
|
||||||
|
<field name="icon">fa-th</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 5. Anodize Line (sub-process) ========================= -->
|
||||||
|
<record id="anodize_line" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Anodize Line</field>
|
||||||
|
<field name="node_type">sub_process</field>
|
||||||
|
<field name="parent_id" ref="recipe_anodize"/>
|
||||||
|
<field name="icon">fa-industry</field>
|
||||||
|
<field name="sequence">50</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 5a. Ready For Anodize -->
|
||||||
|
<record id="anodize_line_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready For Anodize</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="anodize_line"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 5b. Alkaline Clean (Tank A-1) -->
|
||||||
|
<record id="anodize_alkaline_clean" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Alkaline Clean (Tank A-1)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="anodize_line"/>
|
||||||
|
<field name="icon">fa-shower</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_alkaline_primary_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Primary Rinse (Tank A-2)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="anodize_alkaline_clean"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_alkaline_secondary_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Secondary Rinse (Tank A-4)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="anodize_alkaline_clean"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 5c. Etch (Tank A-3) -->
|
||||||
|
<record id="anodize_etch" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Etch (Tank A-3)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="anodize_line"/>
|
||||||
|
<field name="icon">fa-flask</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_etch_primary_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Primary Rinse (Tank A-4)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="anodize_etch"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_etch_secondary_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Secondary Rinse (Tank A-6)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="anodize_etch"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 5d. Deoxidize (Tank A-5) -->
|
||||||
|
<record id="anodize_deoxidize" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Deoxidize (Tank A-5)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="anodize_line"/>
|
||||||
|
<field name="icon">fa-flask</field>
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_deoxidize_primary_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Primary Rinse (Tank A-6)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="anodize_deoxidize"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_deoxidize_secondary_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Secondary Rinse (Tank A-8)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="anodize_deoxidize"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 5e. Sulfuric Anodize (operation container) -->
|
||||||
|
<record id="anodize_sulfuric" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Sulfuric Anodize</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="anodize_line"/>
|
||||||
|
<field name="icon">fa-bolt</field>
|
||||||
|
<field name="sequence">50</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_sulfuric_ramp" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Sulfuric Anodize Ramp (Tank A-9)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="anodize_sulfuric"/>
|
||||||
|
<field name="icon">fa-bolt</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_sulfuric_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Sulfuric Anodize (Tank A-9)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="anodize_sulfuric"/>
|
||||||
|
<field name="icon">fa-bolt</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_sulfuric_do_primary_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Primary Rinse (Tank A-8)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="anodize_sulfuric_do"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_sulfuric_do_secondary_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Secondary Rinse (Tank A-12)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="anodize_sulfuric_do"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_sulfuric_do_hot_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Hot Rinse (Tank A-17)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="anodize_sulfuric_do"/>
|
||||||
|
<field name="icon">fa-thermometer-half</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 5f. Hot Water Seal (Tank A-16) -->
|
||||||
|
<record id="anodize_hot_water_seal" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Hot Water Seal (Tank A-16)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="anodize_line"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">60</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_hot_water_seal_primary_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Primary Rinse (Tank A-12)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="anodize_hot_water_seal"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_hot_water_seal_hot_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Hot Rinse (Tank A-17)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="anodize_hot_water_seal"/>
|
||||||
|
<field name="icon">fa-thermometer-half</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 6. Anodize Dry ========================= -->
|
||||||
|
<record id="anodize_dry" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Anodize Dry</field>
|
||||||
|
<field name="node_type">sub_process</field>
|
||||||
|
<field name="parent_id" ref="recipe_anodize"/>
|
||||||
|
<field name="icon">fa-sun-o</field>
|
||||||
|
<field name="sequence">60</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 7. Unracking ========================= -->
|
||||||
|
<record id="anodize_unracking" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Unracking</field>
|
||||||
|
<field name="node_type">sub_process</field>
|
||||||
|
<field name="parent_id" ref="recipe_anodize"/>
|
||||||
|
<field name="icon">fa-th</field>
|
||||||
|
<field name="sequence">70</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_unracking_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready for Unrack</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="anodize_unracking"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_unracking_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Unracking</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="anodize_unracking"/>
|
||||||
|
<field name="icon">fa-th</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 8. De-Masking ========================= -->
|
||||||
|
<record id="anodize_demasking" model="fusion.plating.process.node">
|
||||||
|
<field name="name">De-Masking</field>
|
||||||
|
<field name="node_type">sub_process</field>
|
||||||
|
<field name="parent_id" ref="recipe_anodize"/>
|
||||||
|
<field name="icon">fa-eraser</field>
|
||||||
|
<field name="sequence">80</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_demasking_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready for De-Masking</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="anodize_demasking"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="anodize_demasking_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">De-Masking</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="anodize_demasking"/>
|
||||||
|
<field name="icon">fa-eraser</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
266
fusion_plating/fusion_plating/data/fp_recipe_chem_conversion.xml
Normal file
266
fusion_plating/fusion_plating/data/fp_recipe_chem_conversion.xml
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2026 Nexa Systems Inc.
|
||||||
|
License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
|
Recipe: CHEM_CONVERSION (Chemical Conversion Process - Trivalent Chromate)
|
||||||
|
Source: Client's Steelhead export (April 2026 transcription).
|
||||||
|
|
||||||
|
Chemical conversion coating for aluminium (MIL-DTL-5541 Type II /
|
||||||
|
Trivalent chromate). Optional Nickel Strip lane handles stripping
|
||||||
|
re-work parts that had previous EN plating.
|
||||||
|
|
||||||
|
Tree:
|
||||||
|
CHEM_CONVERSION (recipe)
|
||||||
|
Racking (sub-process, auto)
|
||||||
|
Ready for Racking (operation)
|
||||||
|
Racking (operation, customer-visible)
|
||||||
|
Chemical Conversion (sub-process, auto)
|
||||||
|
Ready For Chemical Conversion (operation)
|
||||||
|
Drying (operation)
|
||||||
|
Soak Clean (A-1) (operation, customer-visible)
|
||||||
|
Rinse (A-2)
|
||||||
|
Etch (A-3) (operation, customer-visible)
|
||||||
|
Rinse (A-2)
|
||||||
|
Rinse (A-4)
|
||||||
|
Desmutter (A-5) (operation, customer-visible)
|
||||||
|
Rinse (A-4)
|
||||||
|
Rinse (A-6)
|
||||||
|
Trivalent Chromate Conversion (A-14 / A) (operation, signoff, customer-visible)
|
||||||
|
Rinse (A-15)
|
||||||
|
Plug The Threaded Holes (sub-process, signoff)
|
||||||
|
Nickel Strip - Aluminum Line (sub-process, signoff, auto)
|
||||||
|
Strip Process - AL (operation, auto)
|
||||||
|
Nitric Acid (A-13 / SP-10)
|
||||||
|
Final Rinse (A-14 / SP-11)
|
||||||
|
Drying (operation)
|
||||||
|
Post Stripping Inspection (operation, auto)
|
||||||
|
Ready for Post Stripping Inspection
|
||||||
|
Post Stripping Inspection (customer-visible)
|
||||||
|
-->
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="0">
|
||||||
|
|
||||||
|
<!-- ========================= ROOT ========================= -->
|
||||||
|
<record id="recipe_chem_conversion" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Chemical Conversion Process</field>
|
||||||
|
<field name="code">CHEM_CONVERSION</field>
|
||||||
|
<field name="node_type">recipe</field>
|
||||||
|
<field name="icon">fa-flask</field>
|
||||||
|
<field name="sequence">60</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 1. Racking ========================= -->
|
||||||
|
<record id="cc_racking" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Racking</field>
|
||||||
|
<field name="node_type">sub_process</field>
|
||||||
|
<field name="parent_id" ref="recipe_chem_conversion"/>
|
||||||
|
<field name="icon">fa-th</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="cc_racking_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready for Racking</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="cc_racking"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="cc_racking_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Racking</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="cc_racking"/>
|
||||||
|
<field name="icon">fa-th</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 2. Chemical Conversion (sub-process) ========================= -->
|
||||||
|
<record id="cc_chem_conversion" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Chemical Conversion</field>
|
||||||
|
<field name="node_type">sub_process</field>
|
||||||
|
<field name="parent_id" ref="recipe_chem_conversion"/>
|
||||||
|
<field name="icon">fa-industry</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="cc_chem_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready For Chemical Conversion</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="cc_chem_conversion"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="cc_chem_drying" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Drying</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="cc_chem_conversion"/>
|
||||||
|
<field name="icon">fa-sun-o</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 2a. Soak Clean (A-1) -->
|
||||||
|
<record id="cc_soak_clean" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Soak Clean (A-1)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="cc_chem_conversion"/>
|
||||||
|
<field name="icon">fa-shower</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="cc_soak_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Rinse (A-2)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="cc_soak_clean"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 2b. Etch (A-3) -->
|
||||||
|
<record id="cc_etch" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Etch (A-3)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="cc_chem_conversion"/>
|
||||||
|
<field name="icon">fa-flask</field>
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="cc_etch_rinse1" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Rinse (A-2)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="cc_etch"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="cc_etch_rinse2" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Rinse (A-4)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="cc_etch"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 2c. Desmutter (A-5) -->
|
||||||
|
<record id="cc_desmutter" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Desmutter (A-5)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="cc_chem_conversion"/>
|
||||||
|
<field name="icon">fa-flask</field>
|
||||||
|
<field name="sequence">50</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="cc_desmutter_rinse1" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Rinse (A-4)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="cc_desmutter"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="cc_desmutter_rinse2" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Rinse (A-6)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="cc_desmutter"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 2d. Trivalent Chromate Conversion (A-14 / A) -->
|
||||||
|
<record id="cc_trivalent" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Trivalent Chromate Conversion (A-14 / A)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="cc_chem_conversion"/>
|
||||||
|
<field name="icon">fa-diamond</field>
|
||||||
|
<field name="sequence">60</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="cc_trivalent_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Rinse (A-15)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="cc_trivalent"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 3. Plug The Threaded Holes ========================= -->
|
||||||
|
<record id="cc_plug_threaded" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Plug The Threaded Holes</field>
|
||||||
|
<field name="node_type">sub_process</field>
|
||||||
|
<field name="parent_id" ref="recipe_chem_conversion"/>
|
||||||
|
<field name="icon">fa-shield</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 4. Nickel Strip - Aluminum Line ========================= -->
|
||||||
|
<record id="cc_ni_strip" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Nickel Strip - Aluminum Line</field>
|
||||||
|
<field name="node_type">sub_process</field>
|
||||||
|
<field name="parent_id" ref="recipe_chem_conversion"/>
|
||||||
|
<field name="icon">fa-eraser</field>
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 4a. Strip Process - AL -->
|
||||||
|
<record id="cc_ni_strip_process" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Strip Process - AL</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="cc_ni_strip"/>
|
||||||
|
<field name="icon">fa-flask</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="cc_ni_strip_nitric" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Nitric Acid (A-13 / SP-10)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="cc_ni_strip_process"/>
|
||||||
|
<field name="icon">fa-flask</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="cc_ni_strip_final_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Final Rinse (A-14 / SP-11)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="cc_ni_strip_process"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 4b. Drying -->
|
||||||
|
<record id="cc_ni_strip_drying" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Drying</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="cc_ni_strip"/>
|
||||||
|
<field name="icon">fa-sun-o</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 4c. Post Stripping Inspection -->
|
||||||
|
<record id="cc_ni_strip_inspect" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Post Stripping Inspection</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="cc_ni_strip"/>
|
||||||
|
<field name="icon">fa-search</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="cc_ni_strip_inspect_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready for Post Stripping Inspection</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="cc_ni_strip_inspect"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="cc_ni_strip_inspect_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Post Stripping Inspection</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="cc_ni_strip_inspect"/>
|
||||||
|
<field name="icon">fa-eye</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
577
fusion_plating/fusion_plating/data/fp_recipe_enp_sp.xml
Normal file
577
fusion_plating/fusion_plating/data/fp_recipe_enp_sp.xml
Normal file
@@ -0,0 +1,577 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2026 Nexa Systems Inc.
|
||||||
|
License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
|
Recipe: ENP-SP (Electroless Nickel Plating - Special Process)
|
||||||
|
Source: Client's Steelhead export (April 2026 transcription).
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Steelhead allows a node to appear at multiple positions in the
|
||||||
|
same recipe ("occurrence #2"). Our parent_id model is strict
|
||||||
|
single-parent so we duplicate the node — see "Oven baking"
|
||||||
|
appearing first as #1 and again later in the flow.
|
||||||
|
- The Electroless Nickel Plating sub-process holds the wet line;
|
||||||
|
everything inside it is a separate plating step (cleaner →
|
||||||
|
activation → strike → plate → drying).
|
||||||
|
|
||||||
|
Tree:
|
||||||
|
ENP-SP (recipe)
|
||||||
|
├── Oven baking (op, signoff, auto) — first oven cycle
|
||||||
|
│ ├── Ready for bake
|
||||||
|
│ └── Bake (customer-visible)
|
||||||
|
├── Adhesion Test Coupon (op, opt-out)
|
||||||
|
├── Blasting (op, signoff, auto)
|
||||||
|
│ ├── Ready For Blast
|
||||||
|
│ └── Blast (customer-visible)
|
||||||
|
├── Masking (op, signoff, auto)
|
||||||
|
│ ├── Ready For Masking
|
||||||
|
│ └── Masking (customer-visible)
|
||||||
|
├── Racking (op, auto)
|
||||||
|
│ ├── Ready for racking
|
||||||
|
│ └── Racking (customer-visible)
|
||||||
|
├── Ready For Plating (op)
|
||||||
|
├── Electroless Nickel Plating (sub-process, auto)
|
||||||
|
│ ├── Soak Clean (SP-1)
|
||||||
|
│ │ ├── ElectroClean (SP-1)
|
||||||
|
│ │ └── Rinse (SP-2)
|
||||||
|
│ ├── HCl Activation (SP-3)
|
||||||
|
│ │ └── Rinse (SP-6)
|
||||||
|
│ ├── Woods Nickel Strike (SP-5) (signoff)
|
||||||
|
│ │ └── Rinse (SP-6)
|
||||||
|
│ ├── E-Nickel Plate (Mid-Phos) (SP-7) (signoff)
|
||||||
|
│ │ └── Rinse (SP-11)
|
||||||
|
│ ├── E-Nickel Plate (Hi-Phos) (SP-8) (opt-out)
|
||||||
|
│ │ └── Rinse (SP-11)
|
||||||
|
│ └── Drying
|
||||||
|
├── Oven baking (op, signoff, auto) — second oven cycle (#2)
|
||||||
|
│ ├── Ready for bake
|
||||||
|
│ └── Bake (customer-visible)
|
||||||
|
├── De-racking (op, auto)
|
||||||
|
│ ├── Ready For DeRacking
|
||||||
|
│ └── DeRacking (customer-visible)
|
||||||
|
├── De-Masking (op, signoff, auto)
|
||||||
|
│ ├── Ready for De-Masking
|
||||||
|
│ └── De-Masking
|
||||||
|
├── Oven bake (Post de-rack) (op, opt-out, auto)
|
||||||
|
│ ├── Ready for bake
|
||||||
|
│ └── Bake (customer-visible)
|
||||||
|
├── Adhesion Testing (op, opt-out)
|
||||||
|
├── Post Plate Inspection (op, auto)
|
||||||
|
│ ├── Ready For Post Plate Inspection
|
||||||
|
│ └── Post Plate Inspection (customer-visible)
|
||||||
|
├── Salt Spray Masking (op, signoff, auto)
|
||||||
|
│ ├── Ready for Salt Spray Masking
|
||||||
|
│ └── Salt Spray Masking
|
||||||
|
├── Corrosion Testing (op, signoff, auto)
|
||||||
|
│ ├── Corrosion Testing
|
||||||
|
│ └── Corrosion Test Inspection
|
||||||
|
└── Lab Testing (op, signoff, auto)
|
||||||
|
├── Lab Testing
|
||||||
|
└── Lab Testing Results
|
||||||
|
-->
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="0">
|
||||||
|
|
||||||
|
<!-- ========================= ROOT ========================= -->
|
||||||
|
<record id="recipe_enp_sp" model="fusion.plating.process.node">
|
||||||
|
<field name="name">ENP-SP</field>
|
||||||
|
<field name="code">ENP_SP_BASIC</field>
|
||||||
|
<field name="node_type">recipe</field>
|
||||||
|
<field name="icon">fa-flask</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 1. Pre-plate Bake (Stress Relief) =========================
|
||||||
|
Done before plating on high-strength steel to relieve residual
|
||||||
|
machining/forming stresses (ASTM B850 / AMS 2759). -->
|
||||||
|
<record id="enp_sp_oven_baking_1" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Pre-plate Bake (Stress Relief)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_sp"/>
|
||||||
|
<field name="icon">fa-fire</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
<field name="is_manual">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_oven_baking_1_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready for bake</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_oven_baking_1"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_oven_baking_1_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Bake</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_oven_baking_1"/>
|
||||||
|
<field name="icon">fa-fire</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 2. Adhesion Test Coupon ========================= -->
|
||||||
|
<record id="enp_sp_adhesion_coupon" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Adhesion Test Coupon</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_sp"/>
|
||||||
|
<field name="icon">fa-cube</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="opt_in_out">opt_out</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 3. Blasting ========================= -->
|
||||||
|
<record id="enp_sp_blasting" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Blasting</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_sp"/>
|
||||||
|
<field name="icon">fa-bullseye</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
<field name="is_manual">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_blasting_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready For Blast</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_blasting"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_blasting_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Blast</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_blasting"/>
|
||||||
|
<field name="icon">fa-bullseye</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 4. Masking ========================= -->
|
||||||
|
<record id="enp_sp_masking" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Masking</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_sp"/>
|
||||||
|
<field name="icon">fa-paint-brush</field>
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
<field name="is_manual">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_masking_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready For Masking</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_masking"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_masking_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Masking</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_masking"/>
|
||||||
|
<field name="icon">fa-paint-brush</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 5. Racking ========================= -->
|
||||||
|
<record id="enp_sp_racking" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Racking</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_sp"/>
|
||||||
|
<field name="icon">fa-th</field>
|
||||||
|
<field name="sequence">50</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="is_manual">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_racking_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready for racking</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_racking"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_racking_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Racking</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_racking"/>
|
||||||
|
<field name="icon">fa-th</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 6. Ready For Plating ========================= -->
|
||||||
|
<record id="enp_sp_ready_plating" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready For Plating</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_sp"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">60</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 7. Electroless Nickel Plating (sub-process) ========================= -->
|
||||||
|
<record id="enp_sp_enp_line" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Electroless Nickel Plating</field>
|
||||||
|
<field name="node_type">sub_process</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_sp"/>
|
||||||
|
<field name="icon">fa-industry</field>
|
||||||
|
<field name="sequence">70</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 7a. Soak Clean (SP-1) -->
|
||||||
|
<record id="enp_sp_soak_clean" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Soak Clean (SP-1)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_enp_line"/>
|
||||||
|
<field name="icon">fa-shower</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_electroclean" model="fusion.plating.process.node">
|
||||||
|
<field name="name">ElectroClean (SP-1)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_soak_clean"/>
|
||||||
|
<field name="icon">fa-bolt</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_soak_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Rinse (SP-2)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_soak_clean"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 7b. HCl Activation (SP-3) -->
|
||||||
|
<record id="enp_sp_hcl_activation" model="fusion.plating.process.node">
|
||||||
|
<field name="name">HCl Activation (SP-3)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_enp_line"/>
|
||||||
|
<field name="icon">fa-flask</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_hcl_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Rinse (SP-6)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_hcl_activation"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 7c. Woods Nickel Strike (SP-5) -->
|
||||||
|
<record id="enp_sp_woods_strike" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Woods Nickel Strike (SP-5)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_enp_line"/>
|
||||||
|
<field name="icon">fa-bolt</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_woods_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Rinse (SP-6)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_woods_strike"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 7d. E-Nickel Plate (Mid-Phos) (SP-7) -->
|
||||||
|
<record id="enp_sp_enp_mid_phos" model="fusion.plating.process.node">
|
||||||
|
<field name="name">E-Nickel Plate (Mid-Phos) (SP-7)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_enp_line"/>
|
||||||
|
<field name="icon">fa-diamond</field>
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
<field name="is_manual">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_enp_mp_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Rinse (SP-11)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_enp_mid_phos"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 7e. E-Nickel Plate (Hi-Phos) (SP-8) — opt-out -->
|
||||||
|
<record id="enp_sp_enp_hi_phos" model="fusion.plating.process.node">
|
||||||
|
<field name="name">E-Nickel Plate (Hi-Phos) (SP-8)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_enp_line"/>
|
||||||
|
<field name="icon">fa-diamond</field>
|
||||||
|
<field name="sequence">50</field>
|
||||||
|
<field name="opt_in_out">opt_out</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
<field name="is_manual">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_enp_hp_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Rinse (SP-11)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_enp_hi_phos"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 7f. Drying -->
|
||||||
|
<record id="enp_sp_drying" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Drying</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_enp_line"/>
|
||||||
|
<field name="icon">fa-sun-o</field>
|
||||||
|
<field name="sequence">60</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 8. Post-plate Bake (H2 Embrittlement Relief) =========================
|
||||||
|
Drives out hydrogen absorbed during plating. Must START within
|
||||||
|
~4 hours of plate exit (ASTM B850 / AMS 2759). Parts still on
|
||||||
|
rack — de-rack happens after this bake. -->
|
||||||
|
<record id="enp_sp_oven_baking_2" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Post-plate Bake (H2 Embrittlement Relief)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_sp"/>
|
||||||
|
<field name="icon">fa-fire</field>
|
||||||
|
<field name="sequence">80</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
<field name="is_manual">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_oven_baking_2_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready for bake</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_oven_baking_2"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_oven_baking_2_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Bake</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_oven_baking_2"/>
|
||||||
|
<field name="icon">fa-fire</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 9. De-racking ========================= -->
|
||||||
|
<record id="enp_sp_deracking" model="fusion.plating.process.node">
|
||||||
|
<field name="name">De-racking</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_sp"/>
|
||||||
|
<field name="icon">fa-th</field>
|
||||||
|
<field name="sequence">90</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="is_manual">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_deracking_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready For DeRacking</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_deracking"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_deracking_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">DeRacking</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_deracking"/>
|
||||||
|
<field name="icon">fa-hand-paper-o</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 10. De-Masking ========================= -->
|
||||||
|
<record id="enp_sp_demasking" model="fusion.plating.process.node">
|
||||||
|
<field name="name">De-Masking</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_sp"/>
|
||||||
|
<field name="icon">fa-eraser</field>
|
||||||
|
<field name="sequence">100</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
<field name="is_manual">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_demasking_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready for De-Masking</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_demasking"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_demasking_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">De-Masking</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_demasking"/>
|
||||||
|
<field name="icon">fa-eraser</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 11. Oven bake (Post de-rack) ========================= -->
|
||||||
|
<record id="enp_sp_oven_post" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Oven bake (Post de-rack)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_sp"/>
|
||||||
|
<field name="icon">fa-fire</field>
|
||||||
|
<field name="sequence">110</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="opt_in_out">opt_out</field>
|
||||||
|
<field name="is_manual">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_oven_post_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready for bake</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_oven_post"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_oven_post_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Bake</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_oven_post"/>
|
||||||
|
<field name="icon">fa-fire</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 12. Adhesion Testing ========================= -->
|
||||||
|
<record id="enp_sp_adhesion_testing" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Adhesion Testing</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_sp"/>
|
||||||
|
<field name="icon">fa-check-circle</field>
|
||||||
|
<field name="sequence">120</field>
|
||||||
|
<field name="opt_in_out">opt_out</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 13. Post Plate Inspection ========================= -->
|
||||||
|
<record id="enp_sp_post_inspection" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Post Plate Inspection</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_sp"/>
|
||||||
|
<field name="icon">fa-search</field>
|
||||||
|
<field name="sequence">130</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_post_inspection_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready For Post Plate Inspection</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_post_inspection"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_post_inspection_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Post Plate Inspection</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_post_inspection"/>
|
||||||
|
<field name="icon">fa-search</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 14. Salt Spray Masking ========================= -->
|
||||||
|
<record id="enp_sp_salt_masking" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Salt Spray Masking</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_sp"/>
|
||||||
|
<field name="icon">fa-paint-brush</field>
|
||||||
|
<field name="sequence">140</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
<field name="is_manual">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_salt_masking_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready for Salt Spray Masking</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_salt_masking"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_salt_masking_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Salt Spray Masking</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_salt_masking"/>
|
||||||
|
<field name="icon">fa-paint-brush</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 15. Corrosion Testing ========================= -->
|
||||||
|
<record id="enp_sp_corrosion_testing" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Corrosion Testing</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_sp"/>
|
||||||
|
<field name="icon">fa-search</field>
|
||||||
|
<field name="sequence">150</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_corrosion_test_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Corrosion Testing</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_corrosion_testing"/>
|
||||||
|
<field name="icon">fa-flask</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_corrosion_test_inspect" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Corrosion Test Inspection</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_corrosion_testing"/>
|
||||||
|
<field name="icon">fa-search</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 16. Lab Testing ========================= -->
|
||||||
|
<record id="enp_sp_lab_testing" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Lab Testing</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_sp"/>
|
||||||
|
<field name="icon">fa-flask</field>
|
||||||
|
<field name="sequence">160</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_lab_test_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Lab Testing</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_lab_testing"/>
|
||||||
|
<field name="icon">fa-flask</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sp_lab_test_results" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Lab Testing Results</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sp_lab_testing"/>
|
||||||
|
<field name="icon">fa-search</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
469
fusion_plating/fusion_plating/data/fp_recipe_enp_steel_basic.xml
Normal file
469
fusion_plating/fusion_plating/data/fp_recipe_enp_steel_basic.xml
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2026 Nexa Systems Inc.
|
||||||
|
License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
|
Recipe: ENP-STEEL-BASIC (Electroless Nickel Plating - Steel Line)
|
||||||
|
Source: Client's Steelhead export (April 2026 transcription).
|
||||||
|
|
||||||
|
Tree:
|
||||||
|
ENP-STEEL-BASIC (recipe)
|
||||||
|
├── Blasting (op, signoff, auto)
|
||||||
|
│ ├── Ready For Blast
|
||||||
|
│ └── Blast (signoff, customer-visible)
|
||||||
|
├── Masking (op, signoff, auto)
|
||||||
|
│ ├── Ready For Masking
|
||||||
|
│ └── Masking (customer-visible)
|
||||||
|
├── Racking (op, auto)
|
||||||
|
│ ├── Ready for Racking
|
||||||
|
│ └── Racking (customer-visible)
|
||||||
|
├── Ready For Steel Line (op)
|
||||||
|
├── Steel Line (sub-process)
|
||||||
|
│ ├── Cleaner (op, auto)
|
||||||
|
│ │ ├── Soak Clean (S-3) (customer-visible)
|
||||||
|
│ │ ├── Electroclean (S-3)
|
||||||
|
│ │ └── Primary Rinse (S-4) (customer-visible)
|
||||||
|
│ ├── Acid Dip (S-5) (op, customer-visible)
|
||||||
|
│ │ ├── Primary Rinse (S-6) (customer-visible)
|
||||||
|
│ │ └── Secondary Rinse (S-8) (customer-visible)
|
||||||
|
│ ├── Nickel Strike (S-7 / SP-5) (op, signoff, customer-visible)
|
||||||
|
│ │ └── Rinse (S-8 / SP-6) (customer-visible)
|
||||||
|
│ ├── E-Nickel Plate (Mid Phos)(S-9) (op, opt-out, customer-visible)
|
||||||
|
│ │ ├── Primary Rinse (S-11) (customer-visible)
|
||||||
|
│ │ └── Hot Rinse (S-13) (customer-visible)
|
||||||
|
│ ├── E-Nickel Plate (S-10) (op, signoff, customer-visible)
|
||||||
|
│ │ ├── Primary Rinse (S-11) (customer-visible)
|
||||||
|
│ │ └── Hot Rinse (S-13) (customer-visible)
|
||||||
|
│ ├── Hot Water Porosity (A-15) (op, signoff, customer-visible)
|
||||||
|
│ └── Dry (op, customer-visible)
|
||||||
|
├── Oven baking (op, signoff, auto)
|
||||||
|
│ ├── Ready for bake
|
||||||
|
│ └── Bake (customer-visible)
|
||||||
|
├── De-racking (op, auto)
|
||||||
|
│ ├── Ready For DeRacking
|
||||||
|
│ └── DeRacking (customer-visible)
|
||||||
|
├── De-Masking (op, signoff, auto)
|
||||||
|
│ ├── Ready for De-Masking
|
||||||
|
│ └── De-Masking
|
||||||
|
├── Oven bake (Post de-rack) (op, opt-out, auto)
|
||||||
|
│ ├── Ready for bake
|
||||||
|
│ └── Bake (customer-visible)
|
||||||
|
└── Post Plate Inspection (op, auto)
|
||||||
|
├── Ready For Post Plate Inspection
|
||||||
|
└── Post Plate Inspection (customer-visible)
|
||||||
|
-->
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="0">
|
||||||
|
|
||||||
|
<!-- ========================= ROOT ========================= -->
|
||||||
|
<record id="recipe_enp_steel_basic" model="fusion.plating.process.node">
|
||||||
|
<field name="name">ENP-STEEL-BASIC</field>
|
||||||
|
<field name="code">ENP_STEEL_BASIC</field>
|
||||||
|
<field name="node_type">recipe</field>
|
||||||
|
<field name="icon">fa-flask</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 1. Blasting ========================= -->
|
||||||
|
<record id="enp_sb_blasting" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Blasting</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_steel_basic"/>
|
||||||
|
<field name="icon">fa-bullseye</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
<field name="is_manual">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_blasting_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready For Blast</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_blasting"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_blasting_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Blast</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_blasting"/>
|
||||||
|
<field name="icon">fa-bullseye</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 2. Masking ========================= -->
|
||||||
|
<record id="enp_sb_masking" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Masking</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_steel_basic"/>
|
||||||
|
<field name="icon">fa-paint-brush</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
<field name="is_manual">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_masking_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready For Masking</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_masking"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_masking_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Masking</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_masking"/>
|
||||||
|
<field name="icon">fa-paint-brush</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 3. Racking ========================= -->
|
||||||
|
<record id="enp_sb_racking" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Racking</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_steel_basic"/>
|
||||||
|
<field name="icon">fa-th</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="is_manual">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_racking_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready for Racking</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_racking"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_racking_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Racking</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_racking"/>
|
||||||
|
<field name="icon">fa-th</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 4. Ready For Steel Line ========================= -->
|
||||||
|
<record id="enp_sb_ready_steel_line" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready For Steel Line</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_steel_basic"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 5. Steel Line (sub-process) ========================= -->
|
||||||
|
<record id="enp_sb_steel_line" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Steel Line</field>
|
||||||
|
<field name="node_type">sub_process</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_steel_basic"/>
|
||||||
|
<field name="icon">fa-industry</field>
|
||||||
|
<field name="sequence">50</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 5a. Cleaner -->
|
||||||
|
<record id="enp_sb_sl_cleaner" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Cleaner</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_steel_line"/>
|
||||||
|
<field name="icon">fa-shower</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_sl_soak_clean" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Soak Clean (S-3)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_sl_cleaner"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_sl_electroclean" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Electroclean (S-3)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_sl_cleaner"/>
|
||||||
|
<field name="icon">fa-bolt</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_sl_primary_rinse_s4" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Primary Rinse (S-4)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_sl_cleaner"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 5b. Acid Dip (S-5) -->
|
||||||
|
<record id="enp_sb_sl_acid_dip" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Acid Dip (S-5)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_steel_line"/>
|
||||||
|
<field name="icon">fa-flask</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_sl_acid_rinse_s6" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Primary Rinse (S-6)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_sl_acid_dip"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_sl_acid_rinse_s8" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Secondary Rinse (S-8)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_sl_acid_dip"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 5c. Nickel Strike (S-7 / SP-5) -->
|
||||||
|
<record id="enp_sb_sl_nickel_strike" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Nickel Strike (S-7 / SP-5)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_steel_line"/>
|
||||||
|
<field name="icon">fa-bolt</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_sl_strike_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Rinse (S-8 / SP-6)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_sl_nickel_strike"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 5d. E-Nickel Plate (Mid Phos)(S-9) — opt-out (chosen per job) -->
|
||||||
|
<record id="enp_sb_sl_enp_mid_phos" model="fusion.plating.process.node">
|
||||||
|
<field name="name">E-Nickel Plate (Mid Phos)(S-9)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_steel_line"/>
|
||||||
|
<field name="icon">fa-diamond</field>
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
<field name="opt_in_out">opt_out</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
<field name="is_manual">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_sl_enp_mp_rinse_s11" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Primary Rinse (S-11)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_sl_enp_mid_phos"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_sl_enp_mp_hot_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Hot Rinse (S-13)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_sl_enp_mid_phos"/>
|
||||||
|
<field name="icon">fa-thermometer-half</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 5e. E-Nickel Plate (S-10) — primary plating -->
|
||||||
|
<record id="enp_sb_sl_enp_s10" model="fusion.plating.process.node">
|
||||||
|
<field name="name">E-Nickel Plate (S-10)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_steel_line"/>
|
||||||
|
<field name="icon">fa-diamond</field>
|
||||||
|
<field name="sequence">50</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
<field name="is_manual">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_sl_enp_s10_rinse_s11" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Primary Rinse (S-11)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_sl_enp_s10"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_sl_enp_s10_hot_rinse" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Hot Rinse (S-13)</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_sl_enp_s10"/>
|
||||||
|
<field name="icon">fa-thermometer-half</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 5f. Hot Water Porosity (A-15) -->
|
||||||
|
<record id="enp_sb_sl_hot_water_porosity" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Hot Water Porosity (A-15)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_steel_line"/>
|
||||||
|
<field name="icon">fa-tint</field>
|
||||||
|
<field name="sequence">60</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 5g. Dry -->
|
||||||
|
<record id="enp_sb_sl_dry" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Dry</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_steel_line"/>
|
||||||
|
<field name="icon">fa-sun-o</field>
|
||||||
|
<field name="sequence">70</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 6. Post-plate Bake (H2 Embrittlement Relief) =========================
|
||||||
|
Drives out hydrogen absorbed during plating. Must START within
|
||||||
|
~4 hours of plate exit (ASTM B850 / AMS 2759). Parts still on
|
||||||
|
rack — de-rack happens after this bake. -->
|
||||||
|
<record id="enp_sb_oven_baking" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Post-plate Bake (H2 Embrittlement Relief)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_steel_basic"/>
|
||||||
|
<field name="icon">fa-fire</field>
|
||||||
|
<field name="sequence">60</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
<field name="is_manual">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_oven_baking_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready for bake</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_oven_baking"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_oven_baking_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Bake</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_oven_baking"/>
|
||||||
|
<field name="icon">fa-fire</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 7. De-racking ========================= -->
|
||||||
|
<record id="enp_sb_deracking" model="fusion.plating.process.node">
|
||||||
|
<field name="name">De-racking</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_steel_basic"/>
|
||||||
|
<field name="icon">fa-th</field>
|
||||||
|
<field name="sequence">70</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="is_manual">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_deracking_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready For DeRacking</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_deracking"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_deracking_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">DeRacking</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_deracking"/>
|
||||||
|
<field name="icon">fa-hand-paper-o</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 8. De-Masking ========================= -->
|
||||||
|
<record id="enp_sb_demasking" model="fusion.plating.process.node">
|
||||||
|
<field name="name">De-Masking</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_steel_basic"/>
|
||||||
|
<field name="icon">fa-eraser</field>
|
||||||
|
<field name="sequence">80</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="requires_signoff">True</field>
|
||||||
|
<field name="is_manual">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_demasking_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready for De-Masking</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_demasking"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_demasking_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">De-Masking</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_demasking"/>
|
||||||
|
<field name="icon">fa-eraser</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 9. Oven bake (Post de-rack) ========================= -->
|
||||||
|
<record id="enp_sb_oven_post" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Oven bake (Post de-rack)</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_steel_basic"/>
|
||||||
|
<field name="icon">fa-fire</field>
|
||||||
|
<field name="sequence">90</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="opt_in_out">opt_out</field>
|
||||||
|
<field name="is_manual">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_oven_post_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready for bake</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_oven_post"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_oven_post_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Bake</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_oven_post"/>
|
||||||
|
<field name="icon">fa-fire</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 10. Post Plate Inspection ========================= -->
|
||||||
|
<record id="enp_sb_post_inspection" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Post Plate Inspection</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_enp_steel_basic"/>
|
||||||
|
<field name="icon">fa-search</field>
|
||||||
|
<field name="sequence">100</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_post_inspection_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready For Post Plate Inspection</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_post_inspection"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="enp_sb_post_inspection_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Post Plate Inspection</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="enp_sb_post_inspection"/>
|
||||||
|
<field name="icon">fa-search</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2026 Nexa Systems Inc.
|
||||||
|
License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
|
Recipe: GENERAL_PROCESSING (General Processing — common workflow umbrella)
|
||||||
|
Source: Client's Steelhead export (April 2026 transcription).
|
||||||
|
|
||||||
|
This is the "envelope" workflow that sits AROUND every job: contract
|
||||||
|
review (sales/quoting checkpoint), incoming inspection, scheduling,
|
||||||
|
final inspection, and shipping. Specific plating recipes (ENP-STEEL,
|
||||||
|
ENP-SP, ENP-ALUM, etc.) handle the technical processing in between.
|
||||||
|
|
||||||
|
Steelhead-only features that did NOT migrate (we don't have these
|
||||||
|
wired up yet — let me know if you want them):
|
||||||
|
- "Default Lead Time" on the recipe
|
||||||
|
- "Product" linkage from the recipe to a product/service record
|
||||||
|
- "Contract Review Users" — list of users who must approve
|
||||||
|
contract review before the job can advance
|
||||||
|
- Treatment Groups / "Use Price Builders" hook into pricing
|
||||||
|
|
||||||
|
Tree:
|
||||||
|
GENERAL_PROCESSING (recipe)
|
||||||
|
├── Contract Review (op, opt-out, customer-visible)
|
||||||
|
├── Incoming Inspection (op, auto)
|
||||||
|
│ ├── Ready for Incoming Inspection
|
||||||
|
│ └── Incoming Inspection (customer-visible)
|
||||||
|
├── Scheduling (op)
|
||||||
|
├── Final Inspection / Packaging (op, auto)
|
||||||
|
│ ├── Ready For Final Inspection / Packaging
|
||||||
|
│ └── Final Inspection / Packaging (customer-visible)
|
||||||
|
└── Shipping (op, auto)
|
||||||
|
├── Ready For Shipping
|
||||||
|
├── Packing Slip Created
|
||||||
|
└── Shipped (customer-visible)
|
||||||
|
-->
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="0">
|
||||||
|
|
||||||
|
<!-- ========================= ROOT ========================= -->
|
||||||
|
<record id="recipe_general_processing" model="fusion.plating.process.node">
|
||||||
|
<field name="name">General Processing</field>
|
||||||
|
<field name="code">GENERAL_PROCESSING</field>
|
||||||
|
<field name="node_type">recipe</field>
|
||||||
|
<field name="icon">fa-sitemap</field>
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 1. Contract Review ========================= -->
|
||||||
|
<!-- Sales / quoting checkpoint. Marked opt-out so it can be
|
||||||
|
skipped on repeat orders where contract terms haven't
|
||||||
|
changed (per Steelhead's setup). -->
|
||||||
|
<record id="gp_contract_review" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Contract Review</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_general_processing"/>
|
||||||
|
<field name="icon">fa-check-circle</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="opt_in_out">opt_out</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
<field name="is_manual">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 2. Incoming Inspection ========================= -->
|
||||||
|
<record id="gp_incoming_inspection" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Incoming Inspection</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_general_processing"/>
|
||||||
|
<field name="icon">fa-search</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="is_manual">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="gp_incoming_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready for Incoming Inspection</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="gp_incoming_inspection"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="gp_incoming_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Incoming Inspection</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="gp_incoming_inspection"/>
|
||||||
|
<field name="icon">fa-eye</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 3. Scheduling ========================= -->
|
||||||
|
<record id="gp_scheduling" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Scheduling</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_general_processing"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
<field name="is_manual">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 4. Final Inspection / Packaging ========================= -->
|
||||||
|
<record id="gp_final_inspection" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Final Inspection / Packaging</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_general_processing"/>
|
||||||
|
<field name="icon">fa-cube</field>
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="is_manual">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="gp_final_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready For Final Inspection / Packaging</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="gp_final_inspection"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="gp_final_do" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Final Inspection / Packaging</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="gp_final_inspection"/>
|
||||||
|
<field name="icon">fa-cube</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ========================= 5. Shipping ========================= -->
|
||||||
|
<!-- Steelhead shows a truck icon here; our icon list doesn't
|
||||||
|
include fa-truck so we fall back to fa-cube (the parts
|
||||||
|
leaving the door). -->
|
||||||
|
<record id="gp_shipping" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Shipping</field>
|
||||||
|
<field name="node_type">operation</field>
|
||||||
|
<field name="parent_id" ref="recipe_general_processing"/>
|
||||||
|
<field name="icon">fa-cube</field>
|
||||||
|
<field name="sequence">50</field>
|
||||||
|
<field name="auto_complete">True</field>
|
||||||
|
<field name="is_manual">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="gp_shipping_ready" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Ready For Shipping</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="gp_shipping"/>
|
||||||
|
<field name="icon">fa-clock-o</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="gp_shipping_packing_slip" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Packing Slip Created</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="gp_shipping"/>
|
||||||
|
<field name="icon">fa-cube</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="customer_visible">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="gp_shipping_shipped" model="fusion.plating.process.node">
|
||||||
|
<field name="name">Shipped</field>
|
||||||
|
<field name="node_type">step</field>
|
||||||
|
<field name="parent_id" ref="gp_shipping"/>
|
||||||
|
<field name="icon">fa-cube</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
<field name="customer_visible">True</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
@@ -199,6 +199,41 @@ class FpProcessNode(models.Model):
|
|||||||
tracking=True,
|
tracking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ---- Recipe-only fields (apply when node_type='recipe') -----------------
|
||||||
|
# These migrate Steelhead's recipe-level metadata: lead time, the
|
||||||
|
# product/service tied to this recipe, the contract review approver
|
||||||
|
# roster, and the pricing builders to apply when this recipe is on
|
||||||
|
# a quote. They're loose-coupled to keep non-recipe nodes clean.
|
||||||
|
|
||||||
|
default_lead_time = fields.Float(
|
||||||
|
string='Default Lead Time (days)',
|
||||||
|
digits=(8, 2),
|
||||||
|
help='When an MO is created using this recipe, '
|
||||||
|
'date_planned_finished is set to NOW + lead_time.',
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
product_id = fields.Many2one(
|
||||||
|
'product.product',
|
||||||
|
string='Service / Product',
|
||||||
|
ondelete='set null',
|
||||||
|
help='The plating service product this recipe sells. When the '
|
||||||
|
'product appears on a sale order, the resulting MO can '
|
||||||
|
'auto-pick this recipe.',
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
contract_review_user_ids = fields.Many2many(
|
||||||
|
'res.users',
|
||||||
|
relation='fp_process_node_contract_review_user_rel',
|
||||||
|
column1='node_id',
|
||||||
|
column2='user_id',
|
||||||
|
string='Contract Review Approvers',
|
||||||
|
help='Users authorised to sign off the Contract Review work order '
|
||||||
|
'on jobs running this recipe. Anyone outside this list will '
|
||||||
|
'be blocked from finishing the WO.',
|
||||||
|
)
|
||||||
|
# NB. `pricing_rule_ids` lives in fusion_plating_configurator
|
||||||
|
# (added there so this core module doesn't depend on the configurator).
|
||||||
|
|
||||||
# ---- Computed fields -----------------------------------------------------
|
# ---- Computed fields -----------------------------------------------------
|
||||||
|
|
||||||
display_name = fields.Char(
|
display_name = fields.Char(
|
||||||
@@ -270,6 +305,73 @@ class FpProcessNode(models.Model):
|
|||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('A process node cannot be its own ancestor.'))
|
_('A process node cannot be its own ancestor.'))
|
||||||
|
|
||||||
|
# ---- Version auto-bump ---------------------------------------------------
|
||||||
|
# Any meaningful edit / add / delete inside a recipe bumps the recipe
|
||||||
|
# root's `version` field by one. Lets shop managers see at a glance
|
||||||
|
# how stable a recipe is and (later) lets a job pin to a specific
|
||||||
|
# recipe revision so already-running MOs don't see mid-flight changes.
|
||||||
|
|
||||||
|
# Fields that don't represent a "meaningful" change — adjusting these
|
||||||
|
# alone does not bump the version. `version` itself is in the list to
|
||||||
|
# avoid an infinite write loop.
|
||||||
|
_FP_NON_VERSIONED_FIELDS = {
|
||||||
|
'version', 'write_date', 'write_uid',
|
||||||
|
'create_date', 'create_uid',
|
||||||
|
'parent_path', 'display_name', 'recipe_root_id', 'depth',
|
||||||
|
}
|
||||||
|
|
||||||
|
def _fp_bump_recipe_versions(self):
|
||||||
|
"""Increment `version` by 1 on the distinct recipe roots covering
|
||||||
|
the current recordset."""
|
||||||
|
roots = self.mapped('recipe_root_id')
|
||||||
|
# _compute_recipe_root_id falls back to self for nodes whose
|
||||||
|
# parent_path isn't yet stored — pick those up too.
|
||||||
|
for rec in self:
|
||||||
|
if not rec.recipe_root_id and rec.node_type == 'recipe':
|
||||||
|
roots |= rec
|
||||||
|
if not roots:
|
||||||
|
return
|
||||||
|
# Use a direct SQL update so we (a) skip our own write override
|
||||||
|
# and (b) avoid touching write_date / write_uid on the root,
|
||||||
|
# which would itself be a no-op-but-noisy chatter event.
|
||||||
|
self.env.cr.execute(
|
||||||
|
'UPDATE fusion_plating_process_node '
|
||||||
|
'SET version = COALESCE(version, 0) + 1 '
|
||||||
|
'WHERE id IN %s',
|
||||||
|
(tuple(roots.ids),),
|
||||||
|
)
|
||||||
|
roots.invalidate_recordset(['version'])
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
records = super().create(vals_list)
|
||||||
|
# Skip non-recipe roots — only count when the new node lives
|
||||||
|
# inside an existing recipe.
|
||||||
|
descendants = records.filtered(lambda r: r.node_type != 'recipe')
|
||||||
|
if descendants:
|
||||||
|
descendants._fp_bump_recipe_versions()
|
||||||
|
return records
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
meaningful = bool(set(vals.keys()) - self._FP_NON_VERSIONED_FIELDS)
|
||||||
|
res = super().write(vals)
|
||||||
|
if meaningful and self:
|
||||||
|
self._fp_bump_recipe_versions()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def unlink(self):
|
||||||
|
# Snapshot the affected recipe roots BEFORE delete, otherwise
|
||||||
|
# recipe_root_id becomes unreachable on the deleted records.
|
||||||
|
roots = self.mapped('recipe_root_id')
|
||||||
|
descendants = self.filtered(lambda r: r.node_type != 'recipe')
|
||||||
|
# Delete first so we don't bump the version of a recipe that's
|
||||||
|
# being removed entirely.
|
||||||
|
res = super().unlink()
|
||||||
|
survivors = roots.exists()
|
||||||
|
if descendants and survivors:
|
||||||
|
survivors._fp_bump_recipe_versions()
|
||||||
|
return res
|
||||||
|
|
||||||
# ---- Tree data for OWL component -----------------------------------------
|
# ---- Tree data for OWL component -----------------------------------------
|
||||||
|
|
||||||
def get_tree_data(self):
|
def get_tree_data(self):
|
||||||
|
|||||||
@@ -136,9 +136,16 @@ export class RecipeTreeEditor extends Component {
|
|||||||
if (result && result.ok) {
|
if (result && result.ok) {
|
||||||
this.state.recipe = result.recipe;
|
this.state.recipe = result.recipe;
|
||||||
this.state.tree = result.tree;
|
this.state.tree = result.tree;
|
||||||
// Auto-expand root node
|
// Auto-expand every node on first load so the full
|
||||||
if (result.tree) {
|
// hierarchy is visible. The horizontal bracket layout
|
||||||
this.state.expandedNodes[result.tree.id] = true;
|
// works best when everything is open by default;
|
||||||
|
// operators can still collapse individual branches.
|
||||||
|
if (result.tree && Object.keys(this.state.expandedNodes).length === 0) {
|
||||||
|
const expandAll = (n) => {
|
||||||
|
this.state.expandedNodes[n.id] = true;
|
||||||
|
for (const c of (n.children || [])) expandAll(c);
|
||||||
|
};
|
||||||
|
expandAll(result.tree);
|
||||||
}
|
}
|
||||||
// Refresh selected node data if panel is open
|
// Refresh selected node data if panel is open
|
||||||
if (this.state.selectedNodeId) {
|
if (this.state.selectedNodeId) {
|
||||||
|
|||||||
@@ -1,433 +1,623 @@
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Fusion Plating — Recipe Tree Editor
|
// Fusion Plating — Recipe Tree Editor (horizontal bracket-tree, v2, 2026-04)
|
||||||
// Copyright 2026 Nexa Systems Inc.
|
// Copyright 2026 Nexa Systems Inc. · License OPL-1
|
||||||
// License OPL-1 (Odoo Proprietary License v1.0)
|
|
||||||
//
|
//
|
||||||
// THEME AWARENESS
|
// Same Steelhead-style bracket layout as the read-only Process Tree, but
|
||||||
// ---------------
|
// with editable cards: hover reveals Add / Delete buttons, click opens
|
||||||
// All colours from CSS custom properties + SCSS $border-color.
|
// the side panel for full editing, drag-drop reorders.
|
||||||
// Works in both light and dark mode.
|
//
|
||||||
|
// Local tokens (this file lives in fusion_plating, not fusion_plating_shopfloor,
|
||||||
|
// so we can't share the shopfloor tokens; defining the few we need inline).
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
// ---- Root container ---------------------------------------------------------
|
// ---- Local tokens -----------------------------------------------------------
|
||||||
|
// Branch on dark mode at compile time (Odoo 19 compiles two bundles).
|
||||||
|
$o-webclient-color-scheme: bright !default;
|
||||||
|
|
||||||
.o_fp_recipe_editor {
|
$_re-page-hex : #f3f4f6;
|
||||||
|
$_re-card-hex : #ffffff;
|
||||||
|
$_re-soft-hex : #f1f3f5;
|
||||||
|
$_re-border-hex : #d8dadd;
|
||||||
|
$_re-ink-hex : #1f2937;
|
||||||
|
$_re-ink-mute : #6b7280;
|
||||||
|
@if $o-webclient-color-scheme == dark {
|
||||||
|
$_re-page-hex : #1a1d21 !global;
|
||||||
|
$_re-card-hex : #22262d !global;
|
||||||
|
$_re-soft-hex : #1c2027 !global;
|
||||||
|
$_re-border-hex : #343942 !global;
|
||||||
|
$_re-ink-hex : #e5e7eb !global;
|
||||||
|
$_re-ink-mute : #8a909a !global;
|
||||||
|
}
|
||||||
|
$re-page : var(--fp-page-bg, $_re-page-hex);
|
||||||
|
$re-card : var(--fp-card-bg, $_re-card-hex);
|
||||||
|
$re-soft : var(--fp-card-soft-bg, $_re-soft-hex);
|
||||||
|
$re-border : var(--fp-border-color, $_re-border-hex);
|
||||||
|
$re-ink : var(--fp-ink, $_re-ink-hex);
|
||||||
|
$re-mute : var(--fp-ink-mute, $_re-ink-mute);
|
||||||
|
$re-accent : var(--o-action, #714B67);
|
||||||
|
|
||||||
|
// Tree connector geometry
|
||||||
|
$re-card-h : 56px;
|
||||||
|
$re-row-gap : 14px;
|
||||||
|
$re-indent : 36px;
|
||||||
|
$re-stub : 28px;
|
||||||
|
$re-line : #6b7280;
|
||||||
|
$re-line-w : 2px;
|
||||||
|
|
||||||
|
|
||||||
|
@media (hover: none) {
|
||||||
|
.o_fp_recipe_editor [class*="o_fp_re_"]:hover {
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Editor shell
|
||||||
|
// =============================================================================
|
||||||
|
.o_fp_recipe_editor.o_fp_re_v2 {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
background: var(--o-view-background-color, var(--bs-body-bg));
|
background-color: $re-page;
|
||||||
}
|
color: $re-ink;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
|
"Inter", "Helvetica Neue", Arial, sans-serif;
|
||||||
|
|
||||||
// ---- Header -----------------------------------------------------------------
|
|
||||||
|
|
||||||
.o_fp_recipe_header {
|
// -------------------------------------------------------------------------
|
||||||
|
// Header
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
.o_fp_re_header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
gap: 16px;
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 12px 20px;
|
padding: 12px 20px;
|
||||||
background: var(--bs-body-bg);
|
background-color: $re-card;
|
||||||
border-bottom: 1px solid $border-color;
|
border-bottom: 1px solid #{$re-border};
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.o_fp_recipe_header_left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
}
|
||||||
|
.o_fp_re_back {
|
||||||
.o_fp_recipe_back_btn {
|
display: inline-flex;
|
||||||
text-decoration: none;
|
align-items: center;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background-color: $re-soft;
|
||||||
|
color: $re-ink;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
font-size: 0.875rem;
|
||||||
|
border: 1px solid #{$re-border};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s, border-color 0.2s;
|
||||||
|
|
||||||
.o_fp_recipe_title {
|
&:hover {
|
||||||
margin: 0;
|
background-color: color-mix(in srgb, #{$re-accent} 8%, #{$re-card});
|
||||||
font-size: 1.2rem;
|
border-color: color-mix(in srgb, #{$re-accent} 45%, #{$re-border});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.o_fp_re_header_title { flex: 1 1 auto; min-width: 0; }
|
||||||
|
.o_fp_re_h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--bs-body-color);
|
color: $re-ink;
|
||||||
}
|
display: inline-flex;
|
||||||
|
|
||||||
.o_fp_recipe_version_badge {
|
|
||||||
background: var(--bs-secondary-color);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_fp_recipe_header_right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 0;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
}
|
.o_fp_re_ver {
|
||||||
|
display: inline-block;
|
||||||
// ---- Body (tree + panel) layout ---------------------------------------------
|
background-color: $re-soft;
|
||||||
|
color: $re-mute;
|
||||||
.o_fp_recipe_body {
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
margin-left: 6px;
|
||||||
|
border: 1px solid #{$re-border};
|
||||||
|
}
|
||||||
|
.o_fp_re_subtitle {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: $re-mute;
|
||||||
|
}
|
||||||
|
.o_fp_re_header_actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.o_fp_re_btn_outline {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: transparent;
|
||||||
|
color: $re-ink;
|
||||||
|
border: 1px solid #{$re-border};
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s, border-color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $re-soft;
|
||||||
|
border-color: color-mix(in srgb, #{$re-accent} 45%, #{$re-border});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Body (canvas + side panel)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
.o_fp_re_body {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
.o_fp_re_canvas {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 24px 40px;
|
||||||
|
}
|
||||||
|
.o_fp_re_empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px 24px;
|
||||||
|
color: $re-mute;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
> .fa { font-size: 2rem; margin-bottom: 12px; opacity: 0.6; }
|
||||||
|
}
|
||||||
|
|
||||||
.o_fp_recipe_tree_area {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 24px 24px 24px 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Side panel -------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
// Recursive node row (card + children column)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
.o_fp_re_node {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.o_fp_recipe_panel {
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Card — dark Steelhead-style
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
.o_fp_re_card {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 240px;
|
||||||
|
max-width: 380px;
|
||||||
|
min-height: $re-card-h;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: #2b2f36;
|
||||||
|
color: #f1f3f5;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06),
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.08);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.12s, box-shadow 0.2s, background-color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06),
|
||||||
|
0 6px 14px rgba(0, 0, 0, 0.12);
|
||||||
|
background-color: #353a42;
|
||||||
|
.o_fp_re_actions { opacity: 1; }
|
||||||
|
.o_fp_re_handle { opacity: 0.6; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Type tints (subtle differences) -----------------------------
|
||||||
|
&.o_fp_re_type_recipe {
|
||||||
|
background-color: #1f2329;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
&.o_fp_re_type_sub_process {
|
||||||
|
background-color: #262a31;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
&.o_fp_re_type_step {
|
||||||
|
background-color: #353a42;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.o_fp_re_selected {
|
||||||
|
box-shadow: 0 0 0 2px #{$re-accent},
|
||||||
|
0 4px 12px rgba(113, 75, 103, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Drag visuals ------------------------------------------------
|
||||||
|
&.o_fp_recipe_drag_ghost {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
&.o_fp_recipe_drop_target {
|
||||||
|
box-shadow: 0 0 0 3px #{$re-accent};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_fp_re_handle {
|
||||||
|
opacity: 0;
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
font-size: 0.85em;
|
||||||
|
cursor: grab;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_fp_re_icon {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 18px;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0.85;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_fp_re_card_body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.o_fp_re_title {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.o_fp_re_meta {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2px 6px;
|
||||||
|
.fa { opacity: 0.85; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_fp_re_right {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Capability flags — small icon row inside the card
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
.o_fp_re_flags {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: rgba(255, 255, 255, 0.55);
|
||||||
|
i { opacity: 0.85; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Type pill
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
.o_fp_re_type_pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&.o_fp_re_type_pill_recipe { background-color: rgba(113, 75, 103, .35); color: #d9bfd1; }
|
||||||
|
&.o_fp_re_type_pill_sub_process { background-color: rgba(13, 110, 253, .25); color: #6ea8fe; }
|
||||||
|
&.o_fp_re_type_pill_operation { background-color: rgba(25, 135, 84, .28); color: #75d4a4; }
|
||||||
|
&.o_fp_re_type_pill_step { background-color: rgba(255, 255, 255, .12); color: #c8ccd2; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Hover-action buttons (Add / Delete) inside the card
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
.o_fp_re_actions {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 4px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
.o_fp_re_btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #f1f3f5;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
|
||||||
|
&:hover { background-color: rgba(255, 255, 255, 0.18); }
|
||||||
|
|
||||||
|
&.o_fp_re_btn_add:hover { background-color: rgba(40, 167, 69, 0.35); }
|
||||||
|
&.o_fp_re_btn_del:hover { background-color: rgba(220, 53, 69, 0.35); }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Children column (recursed nodes laid out vertically to the right)
|
||||||
|
//
|
||||||
|
// Connectors are drawn entirely from CSS pseudo-elements so the layout
|
||||||
|
// stays in pure flexbox. The horizontal stub bridging parent → bus
|
||||||
|
// column lives in .o_fp_re_children::before; per-child stubs and the
|
||||||
|
// vertical bus segments live on each child via ::before / ::after.
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
.o_fp_re_children {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $re-row-gap;
|
||||||
|
margin-left: $re-indent;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
// horizontal connector from parent card right edge → bus column
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: -#{$re-indent};
|
||||||
|
top: calc(#{$re-card-h} / 2);
|
||||||
|
width: $re-indent;
|
||||||
|
height: $re-line-w;
|
||||||
|
background-color: $re-line;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_fp_re_children > .o_fp_re_node {
|
||||||
|
position: relative;
|
||||||
|
padding-left: $re-stub;
|
||||||
|
|
||||||
|
// horizontal stub — bus column → child card
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: calc(#{$re-card-h} / 2);
|
||||||
|
width: $re-stub;
|
||||||
|
height: $re-line-w;
|
||||||
|
background-color: $re-line;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
// vertical bus segment (default: full row, top → bottom)
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: calc(-#{$re-row-gap} / 2);
|
||||||
|
bottom: calc(-#{$re-row-gap} / 2);
|
||||||
|
width: $re-line-w;
|
||||||
|
background-color: $re-line;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
&:first-child::after { top: calc(#{$re-card-h} / 2); }
|
||||||
|
&:last-child::after { bottom: calc(100% - (#{$re-card-h} / 2)); }
|
||||||
|
&:first-child:last-child::after {
|
||||||
|
top: calc(#{$re-card-h} / 2);
|
||||||
|
bottom: calc(100% - (#{$re-card-h} / 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Inline add-form card (a "ghost" card with input + dropdown + buttons)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
.o_fp_re_add_form .o_fp_re_card_add {
|
||||||
|
background-color: rgba(40, 167, 69, 0.18);
|
||||||
|
border: 1px dashed rgba(40, 167, 69, 0.5);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.o_fp_re_add_input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
|
||||||
|
&::placeholder { color: rgba(255, 255, 255, 0.45); }
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: rgba(40, 167, 69, 0.7);
|
||||||
|
box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.o_fp_re_add_row {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.o_fp_re_add_select {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
|
||||||
|
option { background-color: #2b2f36; color: #fff; }
|
||||||
|
}
|
||||||
|
.o_fp_re_btn_confirm { background-color: rgba(40, 167, 69, 0.45); }
|
||||||
|
.o_fp_re_btn_confirm:hover { background-color: rgba(40, 167, 69, 0.7); }
|
||||||
|
.o_fp_re_btn_cancel { background-color: rgba(255, 255, 255, 0.12); }
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Collapsed-children chip (shows when children are hidden)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
.o_fp_re_collapsed_chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 10px;
|
||||||
|
margin-left: $re-indent;
|
||||||
|
margin-top: 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background-color: $re-soft;
|
||||||
|
color: $re-mute;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #{$re-border};
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: -#{$re-indent};
|
||||||
|
top: 50%;
|
||||||
|
width: $re-indent;
|
||||||
|
height: $re-line-w;
|
||||||
|
background-color: $re-line;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: color-mix(in srgb, #{$re-accent} 8%, #{$re-soft});
|
||||||
|
border-color: color-mix(in srgb, #{$re-accent} 45%, #{$re-border});
|
||||||
|
color: $re-ink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Side panel
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
.o_fp_re_panel {
|
||||||
width: 0;
|
width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: width 0.2s ease;
|
transition: width 0.2s ease;
|
||||||
border-left: 1px solid $border-color;
|
background-color: $re-card;
|
||||||
background: var(--bs-body-bg);
|
border-left: 1px solid #{$re-border};
|
||||||
|
|
||||||
&.o_fp_recipe_panel_open {
|
&.o_fp_re_panel_open {
|
||||||
width: 340px;
|
width: 360px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.o_fp_recipe_panel_header {
|
.o_fp_re_panel_head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
border-bottom: 1px solid $border-color;
|
border-bottom: 1px solid #{$re-border};
|
||||||
|
|
||||||
h5 {
|
h5 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--bs-body-color);
|
color: $re-ink;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.o_fp_re_panel_body {
|
||||||
.o_fp_recipe_panel_body {
|
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
}
|
.o_fp_re_field {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
|
||||||
// ---- Connector lines --------------------------------------------------------
|
> label {
|
||||||
|
display: block;
|
||||||
.o_fp_recipe_connector {
|
|
||||||
width: 3px;
|
|
||||||
height: 16px;
|
|
||||||
background: $border-color;
|
|
||||||
margin-left: 22px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Node card --------------------------------------------------------------
|
|
||||||
|
|
||||||
.o_fp_recipe_node {
|
|
||||||
position: relative;
|
|
||||||
border-width: 1px;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: $border-color;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 10px 14px;
|
|
||||||
max-width: 520px;
|
|
||||||
cursor: pointer;
|
|
||||||
background: var(--bs-body-bg);
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
|
||||||
transition: box-shadow 0.15s, border-color 0.15s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
|
||||||
border-color: var(--o-action, var(--bs-primary));
|
|
||||||
}
|
|
||||||
|
|
||||||
&.o_fp_recipe_node_selected {
|
|
||||||
border-color: var(--o-action, var(--bs-primary));
|
|
||||||
box-shadow: 0 0 0 2px rgba(var(--bs-primary-rgb, 13, 110, 253), 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node type left accent
|
|
||||||
&.o_fp_recipe_node_recipe {
|
|
||||||
border-left: 5px solid var(--bs-primary);
|
|
||||||
}
|
|
||||||
&.o_fp_recipe_node_sub_process {
|
|
||||||
border-left: 5px solid var(--bs-info);
|
|
||||||
}
|
|
||||||
&.o_fp_recipe_node_operation {
|
|
||||||
border-left: 5px solid var(--bs-success);
|
|
||||||
}
|
|
||||||
&.o_fp_recipe_node_step {
|
|
||||||
border-left: 5px solid var(--bs-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drag states
|
|
||||||
&.o_fp_recipe_drag_ghost {
|
|
||||||
opacity: 0.35;
|
|
||||||
border-style: dashed;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.o_fp_recipe_drop_target {
|
|
||||||
border-color: var(--o-action, var(--bs-primary));
|
|
||||||
background: color-mix(in srgb, var(--o-action, var(--bs-primary)) 6%, var(--bs-body-bg));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Drag handle ------------------------------------------------------------
|
|
||||||
|
|
||||||
.o_fp_recipe_drag_handle {
|
|
||||||
position: absolute;
|
|
||||||
left: -20px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
color: var(--bs-secondary-color);
|
|
||||||
cursor: grab;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.15s;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
|
|
||||||
.o_fp_recipe_node:hover & {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Node header row --------------------------------------------------------
|
|
||||||
|
|
||||||
.o_fp_recipe_node_header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_fp_recipe_toggle_btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--bs-secondary-color);
|
|
||||||
cursor: pointer;
|
|
||||||
width: 20px;
|
|
||||||
text-align: center;
|
|
||||||
padding: 0;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--bs-body-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_fp_recipe_toggle_spacer {
|
|
||||||
width: 20px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_fp_recipe_node_icon {
|
|
||||||
color: var(--bs-secondary-color);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
width: 18px;
|
|
||||||
text-align: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_fp_recipe_node_name {
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--bs-body-color);
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_fp_recipe_node_badge {
|
|
||||||
font-size: 0.65rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.3px;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
&.o_fp_recipe_badge_recipe {
|
|
||||||
background: var(--bs-primary);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
&.o_fp_recipe_badge_sub {
|
|
||||||
background: var(--bs-info);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
&.o_fp_recipe_badge_op {
|
|
||||||
background: var(--bs-success);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
&.o_fp_recipe_badge_step {
|
|
||||||
background: var(--bs-secondary);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Node meta row ----------------------------------------------------------
|
|
||||||
|
|
||||||
.o_fp_recipe_node_meta {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
color: var(--bs-secondary-color);
|
color: $re-mute;
|
||||||
padding-left: 28px;
|
text-transform: uppercase;
|
||||||
margin-bottom: 2px;
|
letter-spacing: 0.04em;
|
||||||
}
|
margin-bottom: 4px;
|
||||||
|
|
||||||
.o_fp_recipe_node_wc,
|
|
||||||
.o_fp_recipe_node_duration {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_fp_recipe_node_icons {
|
|
||||||
display: inline-flex;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--bs-secondary-color);
|
|
||||||
|
|
||||||
i {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Node action buttons ----------------------------------------------------
|
|
||||||
|
|
||||||
.o_fp_recipe_node_actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
padding-left: 28px;
|
|
||||||
margin-top: 4px;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.15s;
|
|
||||||
|
|
||||||
.o_fp_recipe_node:hover & {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_fp_recipe_add_btn {
|
|
||||||
font-size: 0.72rem;
|
|
||||||
color: var(--bs-success);
|
|
||||||
border: 1px solid var(--bs-success);
|
|
||||||
padding: 1px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: transparent;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--bs-success);
|
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.o_fp_re_icon_picker {
|
||||||
.o_fp_recipe_delete_btn {
|
|
||||||
font-size: 0.72rem;
|
|
||||||
color: var(--bs-danger);
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 1px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: transparent;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--bs-danger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Add child form ---------------------------------------------------------
|
|
||||||
|
|
||||||
.o_fp_recipe_add_form {
|
|
||||||
padding-left: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_fp_recipe_add_card {
|
|
||||||
border: 1px dashed var(--bs-success);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 10px 14px;
|
|
||||||
max-width: 520px;
|
|
||||||
background: color-mix(in srgb, var(--bs-success) 4%, var(--bs-body-bg));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Children container (indentation) ---------------------------------------
|
|
||||||
|
|
||||||
.o_fp_recipe_children {
|
|
||||||
margin-left: 32px;
|
|
||||||
padding-top: 0;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
// Vertical guide line
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 22px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 16px;
|
|
||||||
width: 2px;
|
|
||||||
background: $border-color;
|
|
||||||
border-radius: 1px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Tracking section -------------------------------------------------------
|
|
||||||
|
|
||||||
.o_fp_recipe_tracking {
|
|
||||||
border-top: 1px solid $border-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Icon picker ------------------------------------------------------------
|
|
||||||
|
|
||||||
.o_fp_recipe_icon_picker {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
.o_fp_re_icon_btn {
|
||||||
.o_fp_recipe_icon_btn {
|
width: 32px;
|
||||||
width: 34px;
|
height: 32px;
|
||||||
height: 34px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: 1px solid $border-color;
|
border: 1px solid #{$re-border};
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: transparent;
|
background-color: transparent;
|
||||||
color: var(--bs-secondary-color);
|
color: $re-mute;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 0.12s, background-color 0.12s;
|
transition: border-color 0.15s, background-color 0.15s, color 0.15s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: var(--o-action, var(--bs-primary));
|
border-color: $re-accent;
|
||||||
color: var(--bs-body-color);
|
color: $re-ink;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background: var(--o-action, var(--bs-primary));
|
background-color: $re-accent;
|
||||||
border-color: var(--o-action, var(--bs-primary));
|
border-color: $re-accent;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.o_fp_re_tracking {
|
||||||
|
border-top: 1px solid #{$re-border};
|
||||||
|
margin-top: 14px;
|
||||||
|
padding-top: 12px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: $re-mute;
|
||||||
|
> div { margin-bottom: 4px; }
|
||||||
|
}
|
||||||
|
.o_fp_re_panel_actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.o_fp_re_btn_save {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8px 14px;
|
||||||
|
background-color: $re-accent;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: filter 0.15s;
|
||||||
|
|
||||||
// ---- Responsive -------------------------------------------------------------
|
&:hover { filter: brightness(1.1); }
|
||||||
|
&:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||||
@media (max-width: 768px) {
|
|
||||||
.o_fp_recipe_tree_area {
|
|
||||||
padding: 16px 12px 16px 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.o_fp_recipe_node {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_fp_recipe_panel.o_fp_recipe_panel_open {
|
// -------------------------------------------------------------------------
|
||||||
width: 280px;
|
// Responsive
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.o_fp_re_canvas { padding: 16px 20px; }
|
||||||
|
.o_fp_re_panel.o_fp_re_panel_open { width: 300px; }
|
||||||
|
.o_fp_re_card { min-width: 200px; max-width: 280px; }
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.o_fp_re_panel.o_fp_re_panel_open {
|
||||||
|
position: absolute;
|
||||||
|
right: 0; top: 0; bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 10;
|
||||||
|
box-shadow: -8px 0 20px rgba(0, 0, 0, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.o_fp_recipe_children {
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,41 +3,177 @@
|
|||||||
Copyright 2026 Nexa Systems Inc.
|
Copyright 2026 Nexa Systems Inc.
|
||||||
License OPL-1 (Odoo Proprietary License v1.0)
|
License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
Part of the Fusion Plating product family.
|
Part of the Fusion Plating product family.
|
||||||
|
|
||||||
|
Recipe Tree Editor — horizontal hierarchical layout (Steelhead-style).
|
||||||
|
Recursive template renders recipe → sub-process → operation → step
|
||||||
|
cards left→right with bracket connectors. Each card carries hover-
|
||||||
|
revealed Add / Delete buttons; a side panel slides in for editing
|
||||||
|
when a node is clicked.
|
||||||
-->
|
-->
|
||||||
<templates xml:space="preserve">
|
<templates xml:space="preserve">
|
||||||
|
|
||||||
|
<!-- ====================================================================
|
||||||
|
RECURSIVE NODE TEMPLATE
|
||||||
|
Expects: node, parentNode (or null), isFirst (bool)
|
||||||
|
==================================================================== -->
|
||||||
|
<t t-name="fusion_plating.RecipeTreeNode">
|
||||||
|
<div class="o_fp_re_node">
|
||||||
|
|
||||||
|
<!-- Card -->
|
||||||
|
<div t-att-class="'o_fp_re_card o_fp_re_type_' + node.node_type
|
||||||
|
+ (state.selectedNodeId === node.id ? ' o_fp_re_selected' : '')"
|
||||||
|
t-att-draggable="node.node_type !== 'recipe' ? 'true' : 'false'"
|
||||||
|
t-on-dragstart="(ev) => this.onNodeDragStart(node, parentNode, ev)"
|
||||||
|
t-on-dragend="(ev) => this.onNodeDragEnd(ev)"
|
||||||
|
t-on-dragover="(ev) => this.onNodeDragOver(node, ev)"
|
||||||
|
t-on-dragleave="(ev) => this.onNodeDragLeave(ev)"
|
||||||
|
t-on-drop="(ev) => this.onNodeDrop(node, parentNode, ev)"
|
||||||
|
t-on-click.stop="() => this.selectNode(node)">
|
||||||
|
|
||||||
|
<!-- Drag handle (left grip) -->
|
||||||
|
<i class="o_fp_re_handle fa fa-bars"
|
||||||
|
t-if="node.node_type !== 'recipe'"/>
|
||||||
|
|
||||||
|
<!-- Icon + name -->
|
||||||
|
<i t-attf-class="o_fp_re_icon fa #{ node.icon || 'fa-cog' }"/>
|
||||||
|
<div class="o_fp_re_card_body">
|
||||||
|
<div class="o_fp_re_title" t-esc="node.name"/>
|
||||||
|
<div class="o_fp_re_meta"
|
||||||
|
t-if="node.work_center or node.estimated_duration or node.input_count">
|
||||||
|
<span t-if="node.work_center">
|
||||||
|
<i class="fa fa-building me-1"/><t t-esc="node.work_center"/>
|
||||||
|
</span>
|
||||||
|
<span t-if="node.estimated_duration">
|
||||||
|
· <i class="fa fa-clock-o me-1"/><t t-esc="formatDuration(node.estimated_duration)"/>
|
||||||
|
</span>
|
||||||
|
<span t-if="node.input_count">
|
||||||
|
· <i class="fa fa-keyboard-o me-1"/><t t-esc="node.input_count"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right side: type pill + capability icons + actions -->
|
||||||
|
<div class="o_fp_re_right">
|
||||||
|
<!-- Capability flags as small icons -->
|
||||||
|
<span class="o_fp_re_flags">
|
||||||
|
<i class="fa fa-check-square" t-if="node.requires_signoff" title="Requires sign-off"/>
|
||||||
|
<i class="fa fa-eye" t-if="node.customer_visible" title="Customer visible"/>
|
||||||
|
<i class="fa fa-magic" t-if="node.auto_complete" title="Auto-complete"/>
|
||||||
|
<i class="fa fa-bolt" t-if="!node.is_manual" title="Automated"/>
|
||||||
|
</span>
|
||||||
|
<!-- Type pill -->
|
||||||
|
<span t-attf-class="o_fp_re_type_pill o_fp_re_type_pill_#{ node.node_type }"
|
||||||
|
t-esc="getNodeTypeMeta(node.node_type).label"/>
|
||||||
|
<!-- Hover-revealed actions -->
|
||||||
|
<span class="o_fp_re_actions">
|
||||||
|
<button class="o_fp_re_btn o_fp_re_btn_add"
|
||||||
|
t-on-click.stop="() => this.startAddChild(node.id)"
|
||||||
|
title="Add child step">
|
||||||
|
<i class="fa fa-plus"/>
|
||||||
|
</button>
|
||||||
|
<button class="o_fp_re_btn o_fp_re_btn_del"
|
||||||
|
t-if="node.node_type !== 'recipe'"
|
||||||
|
t-on-click.stop="() => this.deleteNode(node.id)"
|
||||||
|
title="Delete">
|
||||||
|
<i class="fa fa-trash"/>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Children + inline-add form -->
|
||||||
|
<div class="o_fp_re_children"
|
||||||
|
t-if="(node.children and node.children.length and isExpanded(node.id)) or state.addingTo === node.id">
|
||||||
|
<t t-foreach="node.children || []" t-as="child" t-key="child.id">
|
||||||
|
<t t-call="fusion_plating.RecipeTreeNode">
|
||||||
|
<t t-set="node" t-value="child"/>
|
||||||
|
<t t-set="parentNode" t-value="node"/>
|
||||||
|
<t t-set="isFirst" t-value="false"/>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Inline add form (sits as the last child of `node`) -->
|
||||||
|
<div class="o_fp_re_node o_fp_re_add_form"
|
||||||
|
t-if="state.addingTo === node.id">
|
||||||
|
<div class="o_fp_re_card o_fp_re_card_add">
|
||||||
|
<i class="o_fp_re_icon fa fa-plus-circle"/>
|
||||||
|
<div class="o_fp_re_card_body">
|
||||||
|
<input type="text" class="o_fp_re_add_input"
|
||||||
|
placeholder="New step name..."
|
||||||
|
autofocus="autofocus"
|
||||||
|
t-att-value="state.newNodeName"
|
||||||
|
t-on-input="(ev) => { state.newNodeName = ev.target.value; }"
|
||||||
|
t-on-keydown="onAddNameKey"
|
||||||
|
t-on-click.stop=""/>
|
||||||
|
<div class="o_fp_re_add_row">
|
||||||
|
<select class="o_fp_re_add_select"
|
||||||
|
t-on-change="(ev) => { state.newNodeType = ev.target.value; }"
|
||||||
|
t-on-click.stop="">
|
||||||
|
<t t-foreach="getNodeTypeOptions()" t-as="opt" t-key="opt.value">
|
||||||
|
<option t-att-value="opt.value"
|
||||||
|
t-att-selected="state.newNodeType === opt.value"
|
||||||
|
t-esc="opt.label"/>
|
||||||
|
</t>
|
||||||
|
</select>
|
||||||
|
<button class="o_fp_re_btn o_fp_re_btn_confirm"
|
||||||
|
t-on-click.stop="confirmAdd">
|
||||||
|
<i class="fa fa-check"/>
|
||||||
|
</button>
|
||||||
|
<button class="o_fp_re_btn o_fp_re_btn_cancel"
|
||||||
|
t-on-click.stop="cancelAdd">
|
||||||
|
<i class="fa fa-times"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Collapsed indicator: small chip showing # of hidden children -->
|
||||||
|
<div class="o_fp_re_collapsed_chip"
|
||||||
|
t-if="node.children and node.children.length and !isExpanded(node.id)"
|
||||||
|
t-on-click.stop="() => this.toggleExpand(node.id)">
|
||||||
|
<i class="fa fa-plus-square-o me-1"/>
|
||||||
|
<t t-esc="node.children.length"/> hidden
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ====================================================================
|
||||||
|
ROOT TEMPLATE
|
||||||
|
==================================================================== -->
|
||||||
<t t-name="fusion_plating.RecipeTreeEditor">
|
<t t-name="fusion_plating.RecipeTreeEditor">
|
||||||
<div class="o_fp_recipe_editor">
|
<div class="o_fp_recipe_editor o_fp_re_v2">
|
||||||
|
|
||||||
<!-- ========== HEADER ========== -->
|
<!-- ========== HEADER ========== -->
|
||||||
<div class="o_fp_recipe_header">
|
<div class="o_fp_re_header">
|
||||||
<div class="o_fp_recipe_header_left">
|
<button class="o_fp_re_back"
|
||||||
<button class="btn btn-link o_fp_recipe_back_btn"
|
t-on-click="onBackToList"
|
||||||
t-on-click="onBackToList" title="Back to list">
|
title="Back to recipes">
|
||||||
<i class="fa fa-arrow-left me-1"/> Recipes
|
<i class="fa fa-arrow-left me-2"/>Recipes
|
||||||
</button>
|
</button>
|
||||||
<h2 class="o_fp_recipe_title" t-if="state.recipe">
|
<div class="o_fp_re_header_title" t-if="state.recipe">
|
||||||
<i class="fa fa-flask me-2"/>
|
<h2 class="o_fp_re_h2 mb-0">
|
||||||
<t t-esc="state.recipe.name"/>
|
<i class="fa fa-flask me-2"/><t t-esc="state.recipe.name"/>
|
||||||
<span class="badge rounded-pill o_fp_recipe_version_badge ms-2"
|
<span t-if="state.recipe.version" class="o_fp_re_ver">
|
||||||
t-if="state.recipe.version">
|
|
||||||
v<t t-esc="state.recipe.version"/>
|
v<t t-esc="state.recipe.version"/>
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
<div class="o_fp_re_subtitle" t-if="state.recipe.process_type">
|
||||||
|
<i class="fa fa-tag me-1"/><t t-esc="state.recipe.process_type"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="o_fp_recipe_header_right" t-if="state.recipe">
|
</div>
|
||||||
<span class="text-muted small me-3" t-if="state.recipe.process_type">
|
<div class="o_fp_re_header_actions" t-if="state.recipe">
|
||||||
<i class="fa fa-tag me-1"/>
|
<button class="o_fp_re_btn_outline"
|
||||||
<t t-esc="state.recipe.process_type"/>
|
t-on-click="onDuplicate"
|
||||||
</span>
|
title="Duplicate recipe">
|
||||||
<button class="btn btn-sm btn-outline-secondary me-1"
|
<i class="fa fa-copy me-1"/>Duplicate
|
||||||
t-on-click="onDuplicate" title="Duplicate recipe">
|
|
||||||
<i class="fa fa-copy me-1"/> Duplicate
|
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-primary"
|
<button class="o_fp_re_btn_outline"
|
||||||
t-on-click="() => this.onOpenForm(state.recipe.id)"
|
t-on-click="() => this.onOpenForm(state.recipe.id)"
|
||||||
title="Edit in form view">
|
title="Edit in form view">
|
||||||
<i class="fa fa-pencil me-1"/> Form View
|
<i class="fa fa-pencil me-1"/>Form
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,17 +184,17 @@
|
|||||||
<p class="mt-2 text-muted">Loading recipe tree...</p>
|
<p class="mt-2 text-muted">Loading recipe tree...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ========== NO RECIPE ========== -->
|
<!-- ========== EMPTY ========== -->
|
||||||
<div class="text-center py-5" t-if="!state.loading and !_recipeId">
|
<div class="o_fp_re_empty" t-if="!state.loading and !_recipeId">
|
||||||
<i class="fa fa-exclamation-triangle fa-3x text-muted"/>
|
<i class="fa fa-exclamation-triangle"/>
|
||||||
<p class="mt-3 text-muted">No recipe selected.</p>
|
<div>No recipe selected.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ========== TREE + PANEL LAYOUT ========== -->
|
<!-- ========== BODY (canvas + side panel) ========== -->
|
||||||
<div class="o_fp_recipe_body" t-if="state.tree">
|
<div class="o_fp_re_body" t-if="state.tree">
|
||||||
|
|
||||||
<!-- Tree area -->
|
<!-- Tree canvas -->
|
||||||
<div class="o_fp_recipe_tree_area">
|
<div class="o_fp_re_canvas">
|
||||||
<t t-call="fusion_plating.RecipeTreeNode">
|
<t t-call="fusion_plating.RecipeTreeNode">
|
||||||
<t t-set="node" t-value="state.tree"/>
|
<t t-set="node" t-value="state.tree"/>
|
||||||
<t t-set="parentNode" t-value="null"/>
|
<t t-set="parentNode" t-value="null"/>
|
||||||
@@ -67,26 +203,29 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Side panel -->
|
<!-- Side panel -->
|
||||||
<div t-att-class="'o_fp_recipe_panel' + (state.showPanel ? ' o_fp_recipe_panel_open' : '')">
|
<div t-att-class="'o_fp_re_panel' + (state.showPanel ? ' o_fp_re_panel_open' : '')">
|
||||||
<t t-if="state.showPanel and state.selectedNode">
|
<t t-if="state.showPanel and state.selectedNode">
|
||||||
<div class="o_fp_recipe_panel_header">
|
<div class="o_fp_re_panel_head">
|
||||||
<h5>
|
<h5>
|
||||||
<i t-att-class="'fa ' + (state.selectedNode.icon || 'fa-cog') + ' me-2'"/>
|
<i t-attf-class="fa #{ state.selectedNode.icon || 'fa-cog' } me-2"/>
|
||||||
Edit Node
|
Edit Node
|
||||||
</h5>
|
</h5>
|
||||||
<button class="btn btn-sm btn-link" t-on-click="closePanel">
|
<button class="o_fp_re_btn o_fp_re_btn_cancel"
|
||||||
|
t-on-click="closePanel">
|
||||||
<i class="fa fa-times"/>
|
<i class="fa fa-times"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="o_fp_recipe_panel_body">
|
<div class="o_fp_re_panel_body">
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label fw-bold">Name</label>
|
<div class="o_fp_re_field">
|
||||||
|
<label>Name</label>
|
||||||
<input type="text" class="form-control"
|
<input type="text" class="form-control"
|
||||||
t-att-value="state.selectedNode.name"
|
t-att-value="state.selectedNode.name"
|
||||||
t-on-change="(ev) => { state.selectedNode.name = ev.target.value; }"/>
|
t-on-change="(ev) => { state.selectedNode.name = ev.target.value; }"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label fw-bold">Type</label>
|
<div class="o_fp_re_field">
|
||||||
|
<label>Type</label>
|
||||||
<select class="form-select"
|
<select class="form-select"
|
||||||
t-on-change="(ev) => { state.selectedNode.node_type = ev.target.value; }">
|
t-on-change="(ev) => { state.selectedNode.node_type = ev.target.value; }">
|
||||||
<option value="recipe"
|
<option value="recipe"
|
||||||
@@ -99,53 +238,57 @@
|
|||||||
t-att-selected="state.selectedNode.node_type === 'step'">Step</option>
|
t-att-selected="state.selectedNode.node_type === 'step'">Step</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label fw-bold">Icon</label>
|
<div class="o_fp_re_field">
|
||||||
<div class="o_fp_recipe_icon_picker">
|
<label>Icon</label>
|
||||||
|
<div class="o_fp_re_icon_picker">
|
||||||
<t t-foreach="getIconOptions()" t-as="ic" t-key="ic.value">
|
<t t-foreach="getIconOptions()" t-as="ic" t-key="ic.value">
|
||||||
<button t-att-class="'o_fp_recipe_icon_btn' + (state.selectedNode.icon === ic.value ? ' active' : '')"
|
<button t-att-class="'o_fp_re_icon_btn' + (state.selectedNode.icon === ic.value ? ' active' : '')"
|
||||||
t-on-click.stop="() => { state.selectedNode.icon = ic.value; }"
|
t-on-click.stop="() => { state.selectedNode.icon = ic.value; }"
|
||||||
t-att-title="ic.label">
|
t-att-title="ic.label">
|
||||||
<i t-att-class="'fa ' + ic.value"/>
|
<i t-attf-class="fa #{ ic.value }"/>
|
||||||
</button>
|
</button>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label fw-bold">Duration (min)</label>
|
<div class="o_fp_re_field">
|
||||||
|
<label>Estimated Duration (min)</label>
|
||||||
<input type="number" class="form-control" min="0" step="1"
|
<input type="number" class="form-control" min="0" step="1"
|
||||||
t-att-value="state.selectedNode.estimated_duration || 0"
|
t-att-value="state.selectedNode.estimated_duration || 0"
|
||||||
t-on-change="(ev) => { state.selectedNode.estimated_duration = parseFloat(ev.target.value) || 0; }"/>
|
t-on-change="(ev) => { state.selectedNode.estimated_duration = parseFloat(ev.target.value) || 0; }"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label fw-bold d-block">Flags</label>
|
<div class="o_fp_re_field">
|
||||||
|
<label>Flags</label>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input type="checkbox" class="form-check-input" id="fp_chk_manual"
|
<input type="checkbox" class="form-check-input" id="fp_re_chk_manual"
|
||||||
t-att-checked="state.selectedNode.is_manual"
|
t-att-checked="state.selectedNode.is_manual"
|
||||||
t-on-change="(ev) => { state.selectedNode.is_manual = ev.target.checked; }"/>
|
t-on-change="(ev) => { state.selectedNode.is_manual = ev.target.checked; }"/>
|
||||||
<label class="form-check-label" for="fp_chk_manual">Manual operation</label>
|
<label class="form-check-label" for="fp_re_chk_manual">Manual operation</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input type="checkbox" class="form-check-input" id="fp_chk_auto"
|
<input type="checkbox" class="form-check-input" id="fp_re_chk_auto"
|
||||||
t-att-checked="state.selectedNode.auto_complete"
|
t-att-checked="state.selectedNode.auto_complete"
|
||||||
t-on-change="(ev) => { state.selectedNode.auto_complete = ev.target.checked; }"/>
|
t-on-change="(ev) => { state.selectedNode.auto_complete = ev.target.checked; }"/>
|
||||||
<label class="form-check-label" for="fp_chk_auto">Auto-complete</label>
|
<label class="form-check-label" for="fp_re_chk_auto">Auto-complete</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input type="checkbox" class="form-check-input" id="fp_chk_signoff"
|
<input type="checkbox" class="form-check-input" id="fp_re_chk_signoff"
|
||||||
t-att-checked="state.selectedNode.requires_signoff"
|
t-att-checked="state.selectedNode.requires_signoff"
|
||||||
t-on-change="(ev) => { state.selectedNode.requires_signoff = ev.target.checked; }"/>
|
t-on-change="(ev) => { state.selectedNode.requires_signoff = ev.target.checked; }"/>
|
||||||
<label class="form-check-label" for="fp_chk_signoff">Requires sign-off</label>
|
<label class="form-check-label" for="fp_re_chk_signoff">Requires sign-off</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input type="checkbox" class="form-check-input" id="fp_chk_visible"
|
<input type="checkbox" class="form-check-input" id="fp_re_chk_visible"
|
||||||
t-att-checked="state.selectedNode.customer_visible"
|
t-att-checked="state.selectedNode.customer_visible"
|
||||||
t-on-change="(ev) => { state.selectedNode.customer_visible = ev.target.checked; }"/>
|
t-on-change="(ev) => { state.selectedNode.customer_visible = ev.target.checked; }"/>
|
||||||
<label class="form-check-label" for="fp_chk_visible">Customer visible</label>
|
<label class="form-check-label" for="fp_re_chk_visible">Customer visible</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label fw-bold">Opt In/Out</label>
|
<div class="o_fp_re_field">
|
||||||
|
<label>Opt In/Out</label>
|
||||||
<select class="form-select"
|
<select class="form-select"
|
||||||
t-on-change="(ev) => { state.selectedNode.opt_in_out = ev.target.value; }">
|
t-on-change="(ev) => { state.selectedNode.opt_in_out = ev.target.value; }">
|
||||||
<option value="disabled"
|
<option value="disabled"
|
||||||
@@ -156,30 +299,16 @@
|
|||||||
t-att-selected="state.selectedNode.opt_in_out === 'opt_out'">Opt-Out</option>
|
t-att-selected="state.selectedNode.opt_in_out === 'opt_out'">Opt-Out</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<!-- Info -->
|
|
||||||
<div class="text-muted small mb-2" t-if="state.selectedNode.work_center">
|
<div class="o_fp_re_tracking" t-if="state.selectedNode.create_date">
|
||||||
<i class="fa fa-building me-1"/>
|
<div>
|
||||||
<t t-esc="state.selectedNode.work_center"/>
|
|
||||||
</div>
|
|
||||||
<div class="text-muted small mb-2" t-if="state.selectedNode.process_type">
|
|
||||||
<i class="fa fa-tag me-1"/>
|
|
||||||
<t t-esc="state.selectedNode.process_type"/>
|
|
||||||
</div>
|
|
||||||
<div class="text-muted small mb-2"
|
|
||||||
t-if="state.selectedNode.input_count">
|
|
||||||
<i class="fa fa-keyboard-o me-1"/>
|
|
||||||
<t t-esc="state.selectedNode.input_count"/> operator input(s)
|
|
||||||
</div>
|
|
||||||
<!-- Tracking -->
|
|
||||||
<div class="o_fp_recipe_tracking mt-3 pt-3" t-if="state.selectedNode.create_date">
|
|
||||||
<div class="text-muted small mb-1">
|
|
||||||
<i class="fa fa-calendar-plus-o me-1"/>
|
<i class="fa fa-calendar-plus-o me-1"/>
|
||||||
Created <t t-esc="formatTimeAgo(state.selectedNode.create_date)"/>
|
Created <t t-esc="formatTimeAgo(state.selectedNode.create_date)"/>
|
||||||
<t t-if="state.selectedNode.create_uid_name">
|
<t t-if="state.selectedNode.create_uid_name">
|
||||||
by <strong t-esc="state.selectedNode.create_uid_name"/>
|
by <strong t-esc="state.selectedNode.create_uid_name"/>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-muted small" t-if="state.selectedNode.write_date">
|
<div t-if="state.selectedNode.write_date">
|
||||||
<i class="fa fa-pencil me-1"/>
|
<i class="fa fa-pencil me-1"/>
|
||||||
Updated <t t-esc="formatTimeAgo(state.selectedNode.write_date)"/>
|
Updated <t t-esc="formatTimeAgo(state.selectedNode.write_date)"/>
|
||||||
<t t-if="state.selectedNode.write_uid_name">
|
<t t-if="state.selectedNode.write_uid_name">
|
||||||
@@ -187,15 +316,15 @@
|
|||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Actions -->
|
|
||||||
<div class="d-flex gap-2 mt-4">
|
<div class="o_fp_re_panel_actions">
|
||||||
<button class="btn btn-primary flex-fill"
|
<button class="o_fp_re_btn_save"
|
||||||
t-on-click="saveNode"
|
t-on-click="saveNode"
|
||||||
t-att-disabled="state.saving">
|
t-att-disabled="state.saving">
|
||||||
<i t-att-class="state.saving ? 'fa fa-spinner fa-spin me-1' : 'fa fa-check me-1'"/>
|
<i t-att-class="state.saving ? 'fa fa-spinner fa-spin me-1' : 'fa fa-check me-1'"/>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-secondary"
|
<button class="o_fp_re_btn_outline"
|
||||||
t-on-click="() => this.onOpenForm(state.selectedNode.id)"
|
t-on-click="() => this.onOpenForm(state.selectedNode.id)"
|
||||||
title="Open full form">
|
title="Open full form">
|
||||||
<i class="fa fa-external-link"/>
|
<i class="fa fa-external-link"/>
|
||||||
@@ -208,127 +337,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
|
|
||||||
<!-- ========== RECURSIVE NODE TEMPLATE ========== -->
|
|
||||||
<t t-name="fusion_plating.RecipeTreeNode">
|
|
||||||
<!-- Connector line (skip for root) -->
|
|
||||||
<div class="o_fp_recipe_connector" t-if="!isFirst"/>
|
|
||||||
|
|
||||||
<!-- Node card -->
|
|
||||||
<div t-att-class="'o_fp_recipe_node'
|
|
||||||
+ (state.selectedNodeId === node.id ? ' o_fp_recipe_node_selected' : '')
|
|
||||||
+ ' o_fp_recipe_node_' + node.node_type"
|
|
||||||
t-att-draggable="node.node_type !== 'recipe' ? 'true' : 'false'"
|
|
||||||
t-on-dragstart="(ev) => this.onNodeDragStart(node, parentNode, ev)"
|
|
||||||
t-on-dragend="(ev) => this.onNodeDragEnd(ev)"
|
|
||||||
t-on-dragover="(ev) => this.onNodeDragOver(node, ev)"
|
|
||||||
t-on-dragleave="(ev) => this.onNodeDragLeave(ev)"
|
|
||||||
t-on-drop="(ev) => this.onNodeDrop(node, parentNode, ev)"
|
|
||||||
t-on-click.stop="() => this.selectNode(node)">
|
|
||||||
|
|
||||||
<!-- Drag handle (non-root only) -->
|
|
||||||
<span class="o_fp_recipe_drag_handle" t-if="node.node_type !== 'recipe'">
|
|
||||||
<i class="fa fa-grip-vertical"/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- Node header row -->
|
|
||||||
<div class="o_fp_recipe_node_header">
|
|
||||||
<!-- Expand/collapse toggle -->
|
|
||||||
<button class="o_fp_recipe_toggle_btn"
|
|
||||||
t-if="node.children and node.children.length"
|
|
||||||
t-on-click.stop="() => this.toggleExpand(node.id)">
|
|
||||||
<i t-att-class="isExpanded(node.id) ? 'fa fa-chevron-down' : 'fa fa-chevron-right'"/>
|
|
||||||
</button>
|
|
||||||
<span class="o_fp_recipe_toggle_spacer" t-else=""/>
|
|
||||||
|
|
||||||
<!-- Icon -->
|
|
||||||
<i t-att-class="'o_fp_recipe_node_icon fa ' + (node.icon || 'fa-cog')"/>
|
|
||||||
|
|
||||||
<!-- Name -->
|
|
||||||
<span class="o_fp_recipe_node_name">
|
|
||||||
<t t-esc="node.name"/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- Type badge -->
|
|
||||||
<span t-att-class="'badge o_fp_recipe_node_badge ' + getNodeTypeMeta(node.node_type).badgeClass">
|
|
||||||
<t t-esc="getNodeTypeMeta(node.node_type).label"/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Meta row: work centre, duration, capability icons -->
|
|
||||||
<div class="o_fp_recipe_node_meta">
|
|
||||||
<span class="o_fp_recipe_node_wc" t-if="node.work_center">
|
|
||||||
<i class="fa fa-building me-1"/>
|
|
||||||
<t t-esc="node.work_center"/>
|
|
||||||
</span>
|
|
||||||
<span class="o_fp_recipe_node_duration" t-if="node.estimated_duration">
|
|
||||||
<i class="fa fa-clock-o me-1"/>
|
|
||||||
<t t-esc="formatDuration(node.estimated_duration)"/>
|
|
||||||
</span>
|
|
||||||
<!-- Capability icons -->
|
|
||||||
<span class="o_fp_recipe_node_icons">
|
|
||||||
<i class="fa fa-hand-paper-o" t-if="node.is_manual" title="Manual"/>
|
|
||||||
<i class="fa fa-bolt" t-if="!node.is_manual" title="Automated"/>
|
|
||||||
<i class="fa fa-check-square" t-if="node.requires_signoff" title="Requires sign-off"/>
|
|
||||||
<i class="fa fa-eye" t-if="node.customer_visible" title="Customer visible"/>
|
|
||||||
<i class="fa fa-magic" t-if="node.auto_complete" title="Auto-complete"/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action buttons row -->
|
|
||||||
<div class="o_fp_recipe_node_actions">
|
|
||||||
<button class="btn btn-sm o_fp_recipe_add_btn"
|
|
||||||
t-on-click.stop="() => this.startAddChild(node.id)"
|
|
||||||
title="Add child step">
|
|
||||||
<i class="fa fa-plus me-1"/> Add Step
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm o_fp_recipe_delete_btn"
|
|
||||||
t-if="node.node_type !== 'recipe'"
|
|
||||||
t-on-click.stop="() => this.deleteNode(node.id)"
|
|
||||||
title="Delete">
|
|
||||||
<i class="fa fa-trash"/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Add child inline form -->
|
|
||||||
<div class="o_fp_recipe_add_form" t-if="state.addingTo === node.id">
|
|
||||||
<div class="o_fp_recipe_connector"/>
|
|
||||||
<div class="o_fp_recipe_add_card">
|
|
||||||
<input type="text" class="form-control form-control-sm mb-2"
|
|
||||||
placeholder="New step name..."
|
|
||||||
t-att-value="state.newNodeName"
|
|
||||||
t-on-input="(ev) => { state.newNodeName = ev.target.value; }"
|
|
||||||
t-on-keydown="onAddNameKey"/>
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<select class="form-select form-select-sm flex-shrink-1"
|
|
||||||
style="max-width: 140px;"
|
|
||||||
t-on-change="(ev) => { state.newNodeType = ev.target.value; }">
|
|
||||||
<t t-foreach="getNodeTypeOptions()" t-as="opt" t-key="opt.value">
|
|
||||||
<option t-att-value="opt.value"
|
|
||||||
t-att-selected="state.newNodeType === opt.value"
|
|
||||||
t-esc="opt.label"/>
|
|
||||||
</t>
|
|
||||||
</select>
|
|
||||||
<button class="btn btn-sm btn-primary" t-on-click="confirmAdd">
|
|
||||||
<i class="fa fa-check"/>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" t-on-click="cancelAdd">
|
|
||||||
<i class="fa fa-times"/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Children (recursive) -->
|
|
||||||
<div class="o_fp_recipe_children" t-if="node.children and node.children.length and isExpanded(node.id)">
|
|
||||||
<t t-foreach="node.children" t-as="child" t-key="child.id">
|
|
||||||
<t t-call="fusion_plating.RecipeTreeNode">
|
|
||||||
<t t-set="node" t-value="child"/>
|
|
||||||
<t t-set="parentNode" t-value="node"/>
|
|
||||||
<t t-set="isFirst" t-value="false"/>
|
|
||||||
</t>
|
|
||||||
</t>
|
|
||||||
</div>
|
|
||||||
</t>
|
|
||||||
|
|
||||||
</templates>
|
</templates>
|
||||||
|
|||||||
@@ -79,6 +79,24 @@
|
|||||||
<field name="active" invisible="True"/>
|
<field name="active" invisible="True"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
|
<!-- Recipe-only metadata (lead time, product link,
|
||||||
|
contract review approvers). Hidden on
|
||||||
|
operation/step nodes since those values are
|
||||||
|
only meaningful at the recipe root. -->
|
||||||
|
<group string="Recipe Settings"
|
||||||
|
invisible="node_type != 'recipe'">
|
||||||
|
<group>
|
||||||
|
<field name="default_lead_time"
|
||||||
|
widget="float_time"/>
|
||||||
|
<field name="product_id"
|
||||||
|
options="{'no_create': True}"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="contract_review_user_ids"
|
||||||
|
widget="many2many_tags"
|
||||||
|
options="{'no_create': True}"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<group string="Tracking">
|
<group string="Tracking">
|
||||||
<field name="create_date" string="Created"/>
|
<field name="create_date" string="Created"/>
|
||||||
|
|||||||
@@ -23,8 +23,6 @@
|
|||||||
<field name="code">model._fp_cron_auto_finish_completed_wos()</field>
|
<field name="code">model._fp_cron_auto_finish_completed_wos()</field>
|
||||||
<field name="interval_number">1</field>
|
<field name="interval_number">1</field>
|
||||||
<field name="interval_type">minutes</field>
|
<field name="interval_type">minutes</field>
|
||||||
<field name="numbercall">-1</field>
|
|
||||||
<field name="active" eval="True"/>
|
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
# Part of the Fusion Plating product family.
|
# Part of the Fusion Plating product family.
|
||||||
|
|
||||||
|
from markupsafe import Markup
|
||||||
|
|
||||||
from odoo import _, api, fields, models
|
from odoo import _, api, fields, models
|
||||||
|
|
||||||
|
|
||||||
@@ -191,6 +193,74 @@ class MrpWorkorder(models.Model):
|
|||||||
help='Wall-clock time the timer was closed for the last time.',
|
help='Wall-clock time the timer was closed for the last time.',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Recipe-node link + behaviour flags propagated from the recipe.
|
||||||
|
# _generate_workorders_from_recipe stores the link at WO creation;
|
||||||
|
# the related fields here let the start/finish gates and the
|
||||||
|
# auto-complete cron resolve flags in O(1) without joining by name.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
x_fc_recipe_node_id = fields.Many2one(
|
||||||
|
'fusion.plating.process.node',
|
||||||
|
string='Recipe Node',
|
||||||
|
readonly=True, copy=False, index=True,
|
||||||
|
help='The operation node in the recipe template that produced '
|
||||||
|
'this work order. Drives auto-complete, sign-off and '
|
||||||
|
'manual/automated behaviour.',
|
||||||
|
)
|
||||||
|
x_fc_requires_signoff = fields.Boolean(
|
||||||
|
related='x_fc_recipe_node_id.requires_signoff',
|
||||||
|
store=True, readonly=True,
|
||||||
|
help='Recipe says this is a quality hold point — finish is '
|
||||||
|
'blocked until an operator records a sign-off.',
|
||||||
|
)
|
||||||
|
x_fc_is_manual = fields.Boolean(
|
||||||
|
related='x_fc_recipe_node_id.is_manual',
|
||||||
|
store=True, readonly=True,
|
||||||
|
help='If false, this is an automated step — the worker '
|
||||||
|
'assignment gate is skipped on Start.',
|
||||||
|
)
|
||||||
|
x_fc_auto_complete = fields.Boolean(
|
||||||
|
related='x_fc_recipe_node_id.auto_complete',
|
||||||
|
store=True, readonly=True,
|
||||||
|
help='If true, the cron auto-finishes the WO once it has been '
|
||||||
|
'in Progress for at least its expected duration.',
|
||||||
|
)
|
||||||
|
x_fc_signoff_user_id = fields.Many2one(
|
||||||
|
'res.users', string='Signed Off By',
|
||||||
|
readonly=True, copy=False,
|
||||||
|
help='Operator who signed off on the quality hold point. '
|
||||||
|
'Required to finish a WO whose recipe sets requires_signoff.',
|
||||||
|
)
|
||||||
|
x_fc_signoff_date = fields.Datetime(
|
||||||
|
string='Signed Off At',
|
||||||
|
readonly=True, copy=False,
|
||||||
|
)
|
||||||
|
# Contract-review approver list lifted from the recipe root via the
|
||||||
|
# node link. Computed on the fly — we tried a `related=` field but
|
||||||
|
# Odoo's M2M-through-M2O-through-M2O related chain didn't populate
|
||||||
|
# reliably in tests. A small compute is more predictable.
|
||||||
|
x_fc_contract_review_user_ids = fields.Many2many(
|
||||||
|
'res.users',
|
||||||
|
relation='fp_wo_contract_review_user_rel',
|
||||||
|
column1='wo_id',
|
||||||
|
column2='user_id',
|
||||||
|
string='Contract Review Approvers',
|
||||||
|
compute='_compute_contract_review_approvers',
|
||||||
|
store=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('x_fc_recipe_node_id')
|
||||||
|
def _compute_contract_review_approvers(self):
|
||||||
|
for wo in self:
|
||||||
|
recipe = (
|
||||||
|
wo.x_fc_recipe_node_id.recipe_root_id
|
||||||
|
if wo.x_fc_recipe_node_id else False
|
||||||
|
)
|
||||||
|
wo.x_fc_contract_review_user_ids = (
|
||||||
|
recipe.contract_review_user_ids
|
||||||
|
if recipe else self.env['res.users']
|
||||||
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Workflow step tracking
|
# Workflow step tracking
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -362,13 +432,22 @@ class MrpWorkorder(models.Model):
|
|||||||
# Process tree action (opens OWL client action)
|
# Process tree action (opens OWL client action)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
def action_view_process_tree(self):
|
def action_view_process_tree(self):
|
||||||
"""Open the OWL process tree view for this MO's routing."""
|
"""Open the OWL process tree view for this MO's routing.
|
||||||
|
|
||||||
|
Passes `back_workorder_id` so the tree's "Back" button returns to
|
||||||
|
the WO the user came from instead of always jumping to Plant
|
||||||
|
Overview.
|
||||||
|
"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
return {
|
return {
|
||||||
'type': 'ir.actions.client',
|
'type': 'ir.actions.client',
|
||||||
'tag': 'fp_process_tree',
|
'tag': 'fp_process_tree',
|
||||||
'name': f'Process Tree — {self.production_id.name}',
|
'name': f'Process Tree — {self.production_id.name}',
|
||||||
'context': {'production_id': self.production_id.id},
|
'context': {
|
||||||
|
'production_id': self.production_id.id,
|
||||||
|
'back_workorder_id': self.id,
|
||||||
|
'back_workorder_name': self.display_name or self.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -629,7 +708,7 @@ class MrpWorkorder(models.Model):
|
|||||||
|
|
||||||
@api.depends('x_fc_assigned_user_id', 'x_fc_bath_id', 'x_fc_tank_id',
|
@api.depends('x_fc_assigned_user_id', 'x_fc_bath_id', 'x_fc_tank_id',
|
||||||
'x_fc_oven_id', 'x_fc_rack_id', 'x_fc_masking_material',
|
'x_fc_oven_id', 'x_fc_rack_id', 'x_fc_masking_material',
|
||||||
'x_fc_wo_kind')
|
'x_fc_wo_kind', 'x_fc_is_manual')
|
||||||
def _compute_is_release_ready(self):
|
def _compute_is_release_ready(self):
|
||||||
"""A WO is release-ready when the manager has set EVERY field
|
"""A WO is release-ready when the manager has set EVERY field
|
||||||
button_start would block on. Used by the Manager Desk to keep
|
button_start would block on. Used by the Manager Desk to keep
|
||||||
@@ -638,7 +717,9 @@ class MrpWorkorder(models.Model):
|
|||||||
"""
|
"""
|
||||||
for wo in self:
|
for wo in self:
|
||||||
missing = []
|
missing = []
|
||||||
if not wo.x_fc_assigned_user_id:
|
# Skip the operator requirement for automated steps so the
|
||||||
|
# Manager Desk doesn't park them in Setup Pending forever.
|
||||||
|
if not wo.x_fc_assigned_user_id and wo.x_fc_is_manual:
|
||||||
missing.append('Operator')
|
missing.append('Operator')
|
||||||
kind = wo.x_fc_wo_kind
|
kind = wo.x_fc_wo_kind
|
||||||
if kind == 'wet':
|
if kind == 'wet':
|
||||||
@@ -771,7 +852,11 @@ class MrpWorkorder(models.Model):
|
|||||||
from odoo.exceptions import UserError
|
from odoo.exceptions import UserError
|
||||||
for wo in self:
|
for wo in self:
|
||||||
missing = []
|
missing = []
|
||||||
if not wo.x_fc_assigned_user_id:
|
# Automated steps (recipe.is_manual=False) don't need a
|
||||||
|
# human operator — the equipment runs unattended (timed
|
||||||
|
# immersion, automated rinse, etc.). The kind-specific
|
||||||
|
# equipment checks below still apply.
|
||||||
|
if not wo.x_fc_assigned_user_id and wo.x_fc_is_manual:
|
||||||
missing.append(_('Assigned Operator'))
|
missing.append(_('Assigned Operator'))
|
||||||
kind = wo._fp_classify_kind()
|
kind = wo._fp_classify_kind()
|
||||||
if kind == 'wet':
|
if kind == 'wet':
|
||||||
@@ -847,17 +932,62 @@ class MrpWorkorder(models.Model):
|
|||||||
) % (employee.name, process_type.name))
|
) % (employee.name, process_type.name))
|
||||||
|
|
||||||
def _fp_check_required_fields_before_finish(self):
|
def _fp_check_required_fields_before_finish(self):
|
||||||
"""Block button_finish on bake WOs without the actual data
|
"""Block button_finish on:
|
||||||
Nadcap audits demand: setpoint temp, actual duration, and a
|
|
||||||
chart-recorder reference on the oven (so the printed chart
|
- bake WOs without setpoint temp / actual duration / chart-recorder
|
||||||
for this run can be retrieved).
|
ref (Nadcap requirement);
|
||||||
|
- any WO whose recipe node is `requires_signoff` and has no
|
||||||
|
sign-off recorded yet (quality hold point).
|
||||||
|
|
||||||
Run-time data (temp + duration) belongs at FINISH because
|
Run-time data (temp + duration) belongs at FINISH because
|
||||||
you don't know it until the bake is done. Chart-recorder ref
|
you don't know it until the bake is done.
|
||||||
is on the oven config — checked here as a defensive backstop.
|
|
||||||
"""
|
"""
|
||||||
from odoo.exceptions import UserError
|
from odoo.exceptions import UserError
|
||||||
for wo in self:
|
for wo in self:
|
||||||
|
# ---- Contract Review approver gate ---------------------------
|
||||||
|
# Only authorised users (per the recipe's
|
||||||
|
# contract_review_user_ids) can finish the Contract Review WO.
|
||||||
|
# Detected by the recipe-node name match — robust enough since
|
||||||
|
# this is a well-known operation in every recipe.
|
||||||
|
node = wo.x_fc_recipe_node_id
|
||||||
|
if (
|
||||||
|
node and (node.name or '').strip().lower() == 'contract review'
|
||||||
|
and wo.x_fc_contract_review_user_ids
|
||||||
|
and self.env.user not in wo.x_fc_contract_review_user_ids
|
||||||
|
and not self.env.user.has_group(
|
||||||
|
'fusion_plating.group_fusion_plating_manager'
|
||||||
|
)
|
||||||
|
):
|
||||||
|
allowed = ', '.join(
|
||||||
|
wo.x_fc_contract_review_user_ids.mapped('name')
|
||||||
|
) or '(none configured)'
|
||||||
|
raise UserError(_(
|
||||||
|
'Cannot finish Contract Review for "%(wo)s" — '
|
||||||
|
'this approval is restricted to: %(allowed)s.\n\n'
|
||||||
|
'You (%(user)s) are not on the approver list for '
|
||||||
|
'recipe "%(recipe)s". Ask one of the approvers to '
|
||||||
|
'sign off, or have a Plating Manager finish it on '
|
||||||
|
'their behalf.'
|
||||||
|
) % {
|
||||||
|
'wo': wo.display_name or wo.name,
|
||||||
|
'allowed': allowed,
|
||||||
|
'user': self.env.user.name,
|
||||||
|
'recipe': (node.recipe_root_id.name or '—'),
|
||||||
|
})
|
||||||
|
|
||||||
|
# ---- Quality hold point: requires sign-off -------------------
|
||||||
|
if wo.x_fc_requires_signoff and not wo.x_fc_signoff_user_id:
|
||||||
|
raise UserError(_(
|
||||||
|
'Cannot finish work order "%(wo)s" — recipe step '
|
||||||
|
'"%(node)s" is a quality hold point and requires '
|
||||||
|
'an operator sign-off first.\n\n'
|
||||||
|
'On the WO form: tap "Sign Off" before clicking '
|
||||||
|
'Finish. The sign-off captures who certified the '
|
||||||
|
'work and is recorded in the audit trail.'
|
||||||
|
) % {
|
||||||
|
'wo': wo.display_name or wo.name,
|
||||||
|
'node': (wo.x_fc_recipe_node_id.name or wo.name),
|
||||||
|
})
|
||||||
if wo._fp_classify_kind() != 'bake':
|
if wo._fp_classify_kind() != 'bake':
|
||||||
continue
|
continue
|
||||||
missing = []
|
missing = []
|
||||||
@@ -981,3 +1111,83 @@ class MrpWorkorder(models.Model):
|
|||||||
'within %s hours of plate exit.'
|
'within %s hours of plate exit.'
|
||||||
) % (coating.bake_window_hours or 4.0)
|
) % (coating.bake_window_hours or 4.0)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Sign-off (recipe quality hold point)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def action_signoff(self):
|
||||||
|
"""Capture the current user as the sign-off operator + timestamp.
|
||||||
|
|
||||||
|
The button only makes sense for WOs whose recipe step is marked
|
||||||
|
`requires_signoff`. The view hides the button otherwise.
|
||||||
|
"""
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
for wo in self:
|
||||||
|
if not wo.x_fc_requires_signoff:
|
||||||
|
raise UserError(_(
|
||||||
|
'Work order "%s" is not a quality hold point — '
|
||||||
|
'no sign-off required.'
|
||||||
|
) % (wo.display_name or wo.name))
|
||||||
|
wo.write({
|
||||||
|
'x_fc_signoff_user_id': self.env.user.id,
|
||||||
|
'x_fc_signoff_date': fields.Datetime.now(),
|
||||||
|
})
|
||||||
|
wo.message_post(
|
||||||
|
body=Markup(_(
|
||||||
|
'Quality hold point signed off by <b>%s</b>.'
|
||||||
|
)) % self.env.user.name,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Auto-complete cron
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@api.model
|
||||||
|
def _fp_cron_auto_finish_completed_wos(self):
|
||||||
|
"""Cron entry point — auto-finish WOs whose recipe step is marked
|
||||||
|
`auto_complete` once they've been in Progress for at least their
|
||||||
|
expected duration.
|
||||||
|
|
||||||
|
Used for fully-automated steps (timed immersion, automated rinse)
|
||||||
|
where the equipment runs unattended. Manual steps are unaffected.
|
||||||
|
|
||||||
|
Skips WOs that still have a sign-off requirement: those must be
|
||||||
|
finished by the operator after they've certified the work.
|
||||||
|
"""
|
||||||
|
candidates = self.search([
|
||||||
|
('state', '=', 'progress'),
|
||||||
|
('x_fc_auto_complete', '=', True),
|
||||||
|
('x_fc_started_at', '!=', False),
|
||||||
|
('duration_expected', '>', 0),
|
||||||
|
])
|
||||||
|
if not candidates:
|
||||||
|
return 0
|
||||||
|
now = fields.Datetime.now()
|
||||||
|
finished = 0
|
||||||
|
for wo in candidates:
|
||||||
|
if wo.x_fc_requires_signoff and not wo.x_fc_signoff_user_id:
|
||||||
|
# Quality hold trumps auto-complete — wait for the
|
||||||
|
# operator's sign-off before closing.
|
||||||
|
continue
|
||||||
|
elapsed_min = (now - wo.x_fc_started_at).total_seconds() / 60.0
|
||||||
|
if elapsed_min < (wo.duration_expected or 0):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
wo.with_user(
|
||||||
|
wo.x_fc_assigned_user_id or self.env.user
|
||||||
|
).button_finish()
|
||||||
|
wo.message_post(
|
||||||
|
body=Markup(_(
|
||||||
|
'Auto-finished by recipe (auto_complete) after '
|
||||||
|
'%.1f min — expected %.1f min.'
|
||||||
|
)) % (elapsed_min, wo.duration_expected),
|
||||||
|
subtype_xmlid='mail.mt_note',
|
||||||
|
)
|
||||||
|
finished += 1
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
import logging
|
||||||
|
logging.getLogger(__name__).warning(
|
||||||
|
'Auto-complete failed for WO %s (%s): %s',
|
||||||
|
wo.id, wo.display_name, exc,
|
||||||
|
)
|
||||||
|
return finished
|
||||||
|
|||||||
@@ -102,6 +102,25 @@
|
|||||||
decoration-muted="x_fc_wo_kind in ('mask', 'rack', 'inspect', 'other')"/>
|
decoration-muted="x_fc_wo_kind in ('mask', 'rack', 'inspect', 'other')"/>
|
||||||
<field name="x_fc_requires_bath" invisible="1"/>
|
<field name="x_fc_requires_bath" invisible="1"/>
|
||||||
<field name="x_fc_requires_oven" invisible="1"/>
|
<field name="x_fc_requires_oven" invisible="1"/>
|
||||||
|
<field name="x_fc_recipe_node_id" invisible="1"/>
|
||||||
|
<field name="x_fc_requires_signoff" invisible="1"/>
|
||||||
|
<field name="x_fc_is_manual" invisible="1"/>
|
||||||
|
<field name="x_fc_auto_complete" invisible="1"/>
|
||||||
|
<field name="x_fc_signoff_user_id" readonly="1"
|
||||||
|
invisible="not x_fc_requires_signoff"/>
|
||||||
|
<field name="x_fc_signoff_date" readonly="1"
|
||||||
|
invisible="not x_fc_requires_signoff"/>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<!-- ============================================================
|
||||||
|
SIGN OFF BUTTON — only visible when the recipe step
|
||||||
|
requires a sign-off and the WO is in progress.
|
||||||
|
============================================================ -->
|
||||||
|
<xpath expr="//header" position="inside">
|
||||||
|
<button name="action_signoff" type="object"
|
||||||
|
string="Sign Off" class="oe_highlight"
|
||||||
|
icon="fa-check-square-o"
|
||||||
|
invisible="not x_fc_requires_signoff or x_fc_signoff_user_id or state in ('done', 'cancel')"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
|
||||||
<!-- ============================================================
|
<!-- ============================================================
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
|
# Part of the Fusion Plating product family.
|
||||||
|
"""Add the configurator-side `pricing_rule_ids` field to process nodes.
|
||||||
|
|
||||||
|
Lives here (not in core fusion_plating) so the core module doesn't have
|
||||||
|
to depend on the configurator.
|
||||||
|
"""
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class FpProcessNode(models.Model):
|
||||||
|
_inherit = 'fusion.plating.process.node'
|
||||||
|
|
||||||
|
pricing_rule_ids = fields.Many2many(
|
||||||
|
'fp.pricing.rule',
|
||||||
|
relation='fp_process_node_pricing_rule_rel',
|
||||||
|
column1='node_id',
|
||||||
|
column2='rule_id',
|
||||||
|
string='Price Builders',
|
||||||
|
help='Pricing rules to apply when this recipe is selected on a '
|
||||||
|
'quotation (mirrors Steelhead "Use Price Builders").',
|
||||||
|
)
|
||||||
@@ -439,7 +439,25 @@ class FpQuoteConfigurator(models.Model):
|
|||||||
|
|
||||||
Scores rules by specificity -- most specific match wins.
|
Scores rules by specificity -- most specific match wins.
|
||||||
If no rule matches filters, returns None.
|
If no rule matches filters, returns None.
|
||||||
|
|
||||||
|
When the chosen coating config points at a recipe and that recipe
|
||||||
|
has `pricing_rule_ids` configured, the search is constrained to
|
||||||
|
those rules ("Use Price Builders" semantics). Otherwise the
|
||||||
|
whole active rule set is considered as before.
|
||||||
"""
|
"""
|
||||||
|
recipe = (
|
||||||
|
self.coating_config_id.recipe_id
|
||||||
|
if self.coating_config_id and self.coating_config_id.recipe_id
|
||||||
|
else False
|
||||||
|
)
|
||||||
|
builder_rules = (
|
||||||
|
recipe.pricing_rule_ids if recipe else self.env['fp.pricing.rule']
|
||||||
|
)
|
||||||
|
if builder_rules:
|
||||||
|
rules = builder_rules.filtered('active').sorted(
|
||||||
|
lambda r: (r.sequence, r.id)
|
||||||
|
)
|
||||||
|
else:
|
||||||
rules = self.env['fp.pricing.rule'].search(
|
rules = self.env['fp.pricing.rule'].search(
|
||||||
[('active', '=', True)], order='sequence, id'
|
[('active', '=', True)], order='sequence, id'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Notifications',
|
'name': 'Fusion Plating — Notifications',
|
||||||
'version': '19.0.4.0.0',
|
'version': '19.0.4.1.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Auto-email notifications at workflow milestones with configurable templates, PDF attachments, and audit log.',
|
'summary': 'Auto-email notifications at workflow milestones with configurable templates, PDF attachments, and audit log.',
|
||||||
'author': 'Nexa Systems Inc.',
|
'author': 'Nexa Systems Inc.',
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
'views/fp_notification_log_views.xml',
|
'views/fp_notification_log_views.xml',
|
||||||
'views/fp_notifications_menu.xml',
|
'views/fp_notifications_menu.xml',
|
||||||
],
|
],
|
||||||
|
'post_init_hook': 'post_init_hook',
|
||||||
'installable': True,
|
'installable': True,
|
||||||
'application': False,
|
'application': False,
|
||||||
'auto_install': False,
|
'auto_install': False,
|
||||||
|
|||||||
@@ -61,6 +61,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</field>
|
</field>
|
||||||
|
<field name="report_template_ids"
|
||||||
|
eval="[(6, 0, [ref('fusion_plating_reports.action_report_fp_sale_portrait')])]"/>
|
||||||
|
<field name="report_name">Quotation_{{ (object.name or '').replace('/','_') }}</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- ============================================================= -->
|
<!-- ============================================================= -->
|
||||||
@@ -118,6 +121,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</field>
|
</field>
|
||||||
|
<field name="report_template_ids"
|
||||||
|
eval="[(6, 0, [ref('fusion_plating_reports.action_report_fp_so_acknowledgement')])]"/>
|
||||||
|
<field name="report_name">Acknowledgement_{{ (object.name or '').replace('/','_') }}</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- ============================================================= -->
|
<!-- ============================================================= -->
|
||||||
@@ -342,6 +348,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</field>
|
</field>
|
||||||
|
<field name="report_template_ids"
|
||||||
|
eval="[(6, 0, [ref('fusion_plating_reports.action_report_fp_invoice_portrait')])]"/>
|
||||||
|
<field name="report_name">Invoice_{{ (object.name or '').replace('/','_') }}</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- ============================================================= -->
|
<!-- ============================================================= -->
|
||||||
|
|||||||
54
fusion_plating/fusion_plating_notifications/hooks.py
Normal file
54
fusion_plating/fusion_plating_notifications/hooks.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
|
# Part of the Fusion Plating product family.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def post_init_hook(env):
|
||||||
|
"""Force-populate report_template_ids on the mail templates.
|
||||||
|
|
||||||
|
The mail-template XML records are tagged noupdate="1" so
|
||||||
|
customer-edited templates aren't overwritten on module update.
|
||||||
|
That means report_template_ids added to the XML AFTER the
|
||||||
|
templates were first installed won't propagate via the usual
|
||||||
|
-u reload. This hook wires the reports onto the branded
|
||||||
|
templates on install/upgrade and is safe to re-run.
|
||||||
|
"""
|
||||||
|
_apply_report_template(
|
||||||
|
env,
|
||||||
|
'fusion_plating_notifications.fp_mail_template_quote_sent',
|
||||||
|
'fusion_plating_reports.action_report_fp_sale_portrait',
|
||||||
|
)
|
||||||
|
_apply_report_template(
|
||||||
|
env,
|
||||||
|
'fusion_plating_notifications.fp_mail_template_so_confirmed',
|
||||||
|
'fusion_plating_reports.action_report_fp_so_acknowledgement',
|
||||||
|
)
|
||||||
|
_apply_report_template(
|
||||||
|
env,
|
||||||
|
'fusion_plating_notifications.fp_mail_template_invoice_posted',
|
||||||
|
'fusion_plating_reports.action_report_fp_invoice_portrait',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_report_template(env, mail_template_xmlid, report_xmlid):
|
||||||
|
mail_template = env.ref(mail_template_xmlid, raise_if_not_found=False)
|
||||||
|
report = env.ref(report_xmlid, raise_if_not_found=False)
|
||||||
|
if not mail_template or not report:
|
||||||
|
_logger.warning(
|
||||||
|
'fusion_plating_notifications post_init: missing %s or %s',
|
||||||
|
mail_template_xmlid, report_xmlid,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if report.id not in mail_template.report_template_ids.ids:
|
||||||
|
mail_template.write({
|
||||||
|
'report_template_ids': [(4, report.id)],
|
||||||
|
})
|
||||||
|
_logger.info(
|
||||||
|
'fusion_plating_notifications: attached report %s to template %s',
|
||||||
|
report_xmlid, mail_template_xmlid,
|
||||||
|
)
|
||||||
@@ -9,6 +9,24 @@ from odoo import models
|
|||||||
class AccountMove(models.Model):
|
class AccountMove(models.Model):
|
||||||
_inherit = 'account.move'
|
_inherit = 'account.move'
|
||||||
|
|
||||||
|
def _get_mail_template(self):
|
||||||
|
"""Prefer the Fusion Plating-branded invoice template over Odoo default.
|
||||||
|
|
||||||
|
Called by the account.move.send wizard when the user clicks
|
||||||
|
"Send" on an invoice. Override returns our
|
||||||
|
fp_mail_template_invoice_posted (which attaches the branded
|
||||||
|
Invoice PDF) for out_invoice moves. Credit notes / self-billing
|
||||||
|
fall back to Odoo's built-in templates.
|
||||||
|
"""
|
||||||
|
if all(m.move_type == 'out_invoice' for m in self):
|
||||||
|
tpl = self.env.ref(
|
||||||
|
'fusion_plating_notifications.fp_mail_template_invoice_posted',
|
||||||
|
raise_if_not_found=False,
|
||||||
|
)
|
||||||
|
if tpl:
|
||||||
|
return tpl
|
||||||
|
return super()._get_mail_template()
|
||||||
|
|
||||||
def action_post(self):
|
def action_post(self):
|
||||||
res = super().action_post()
|
res = super().action_post()
|
||||||
Dispatch = self.env['fp.notification.template']
|
Dispatch = self.env['fp.notification.template']
|
||||||
|
|||||||
@@ -9,6 +9,40 @@ from odoo import models
|
|||||||
class SaleOrder(models.Model):
|
class SaleOrder(models.Model):
|
||||||
_inherit = 'sale.order'
|
_inherit = 'sale.order'
|
||||||
|
|
||||||
|
def _find_mail_template(self):
|
||||||
|
"""Prefer Fusion Plating-branded templates over Odoo defaults.
|
||||||
|
|
||||||
|
Called by sale.order.action_quotation_send (the "Send" button on
|
||||||
|
both quotations and confirmed orders) to resolve the template
|
||||||
|
the composer pre-selects.
|
||||||
|
|
||||||
|
Override returns:
|
||||||
|
- state in ('draft', 'sent') -> fp_mail_template_quote_sent
|
||||||
|
(attaches Quotation PDF)
|
||||||
|
- state == 'sale' or 'done' -> fp_mail_template_so_confirmed
|
||||||
|
(attaches Acknowledgement PDF)
|
||||||
|
|
||||||
|
Falls back to Odoo's default template if ours can't be resolved
|
||||||
|
(e.g. fusion_plating_reports not installed, or template record
|
||||||
|
missing).
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
ref = self.env.ref
|
||||||
|
if self.env.context.get('proforma'):
|
||||||
|
return super()._find_mail_template()
|
||||||
|
fp_tpl = False
|
||||||
|
if self.state in ('draft', 'sent'):
|
||||||
|
fp_tpl = ref(
|
||||||
|
'fusion_plating_notifications.fp_mail_template_quote_sent',
|
||||||
|
raise_if_not_found=False,
|
||||||
|
)
|
||||||
|
elif self.state in ('sale', 'done'):
|
||||||
|
fp_tpl = ref(
|
||||||
|
'fusion_plating_notifications.fp_mail_template_so_confirmed',
|
||||||
|
raise_if_not_found=False,
|
||||||
|
)
|
||||||
|
return fp_tpl or super()._find_mail_template()
|
||||||
|
|
||||||
def action_quotation_send(self):
|
def action_quotation_send(self):
|
||||||
"""Fire the quote_sent trigger when a quotation is emailed."""
|
"""Fire the quote_sent trigger when a quotation is emailed."""
|
||||||
res = super().action_quotation_send()
|
res = super().action_quotation_send()
|
||||||
|
|||||||
@@ -125,3 +125,42 @@ class FpPortalJob(models.Model):
|
|||||||
def _progress_percent(self):
|
def _progress_percent(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
return self._state_progress_map().get(self.state, 0)
|
return self._state_progress_map().get(self.state, 0)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Customer-visible process steps
|
||||||
|
#
|
||||||
|
# Walks the linked production's recipe tree and returns only the
|
||||||
|
# nodes the recipe author marked `customer_visible=True`. Used by
|
||||||
|
# the portal job page so internal QC / setup / handling steps stay
|
||||||
|
# hidden from the customer while the substantive process steps are
|
||||||
|
# surfaced.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def get_customer_visible_steps(self):
|
||||||
|
"""Return [{'name': str, 'icon': str, 'depth': int}] for portal display."""
|
||||||
|
self.ensure_one()
|
||||||
|
Production = self.env.get('mrp.production')
|
||||||
|
if Production is None:
|
||||||
|
return []
|
||||||
|
mo = Production.sudo().search(
|
||||||
|
[('x_fc_portal_job_id', '=', self.id)], limit=1,
|
||||||
|
)
|
||||||
|
if not mo or not mo.x_fc_recipe_id:
|
||||||
|
return []
|
||||||
|
|
||||||
|
result = []
|
||||||
|
def walk(node, depth):
|
||||||
|
for child in node.child_ids.sorted('sequence'):
|
||||||
|
if not child.customer_visible:
|
||||||
|
# Hidden node — and its sub-tree is also hidden,
|
||||||
|
# because if you're skipping the parent the kids
|
||||||
|
# never make sense in isolation.
|
||||||
|
continue
|
||||||
|
result.append({
|
||||||
|
'name': child.name,
|
||||||
|
'icon': child.icon or 'fa-cog',
|
||||||
|
'depth': depth,
|
||||||
|
'node_type': child.node_type,
|
||||||
|
})
|
||||||
|
walk(child, depth + 1)
|
||||||
|
walk(mo.x_fc_recipe_id, 0)
|
||||||
|
return result
|
||||||
|
|||||||
@@ -588,6 +588,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Process Steps — only the customer-visible recipe nodes
|
||||||
|
(recipe author marked customer_visible=True). -->
|
||||||
|
<t t-set="visible_steps" t-value="job.sudo().get_customer_visible_steps()"/>
|
||||||
|
<div class="mb-4" t-if="visible_steps">
|
||||||
|
<h6 class="text-muted small text-uppercase">Process Steps</h6>
|
||||||
|
<ol class="list-group list-group-numbered">
|
||||||
|
<li t-foreach="visible_steps" t-as="step"
|
||||||
|
class="list-group-item d-flex align-items-center"
|
||||||
|
t-attf-style="padding-left: #{ 12 + (step['depth'] * 18) }px;">
|
||||||
|
<i t-attf-class="fa #{ step['icon'] } me-2 text-muted"/>
|
||||||
|
<span t-out="step['name']"/>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h6 class="text-muted small text-uppercase">Documents</h6>
|
<h6 class="text-muted small text-uppercase">Documents</h6>
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
|
|||||||
@@ -1021,87 +1021,286 @@ class FpShopfloorController(http.Controller):
|
|||||||
def process_tree(self, production_id):
|
def process_tree(self, production_id):
|
||||||
"""Return routing tree for a manufacturing order.
|
"""Return routing tree for a manufacturing order.
|
||||||
|
|
||||||
Each node is an operation/work-order step. Children represent
|
Walks the MO's recipe tree (fusion.plating.process.node) and returns
|
||||||
sub-states (ready vs active) within that step.
|
a recursive nested structure:
|
||||||
"""
|
recipe → sub_process → operation → step
|
||||||
MrpWO = request.env.get('mrp.workorder')
|
For each `operation` node we look up the matching mrp.workorder by
|
||||||
if MrpWO is None:
|
name within this MO, then attach the WO state, qty progress, kind,
|
||||||
return {
|
equipment, and a synthetic state-child ("Ready for X" or "In X")
|
||||||
'production_name': '',
|
so the operator sees the live position in the flow.
|
||||||
'product_name': '',
|
|
||||||
'state': '',
|
|
||||||
'nodes': [],
|
|
||||||
}
|
|
||||||
|
|
||||||
MrpProduction = request.env['mrp.production']
|
If the MO has no recipe assigned we fall back to a flat list of
|
||||||
|
WOs as a single tier of operation nodes under a synthetic root.
|
||||||
|
"""
|
||||||
|
env = request.env
|
||||||
|
MrpWO = env.get('mrp.workorder')
|
||||||
|
MrpProduction = env['mrp.production']
|
||||||
production = MrpProduction.browse(int(production_id))
|
production = MrpProduction.browse(int(production_id))
|
||||||
if not production.exists():
|
if not production.exists():
|
||||||
raise UserError(f"Manufacturing order {production_id} not found")
|
raise UserError(f"Manufacturing order {production_id} not found")
|
||||||
|
|
||||||
work_orders = MrpWO.search(
|
# Customer
|
||||||
|
customer = ''
|
||||||
|
so_name = production.origin or ''
|
||||||
|
if production.x_fc_portal_job_id and production.x_fc_portal_job_id.partner_id:
|
||||||
|
customer = production.x_fc_portal_job_id.partner_id.name or ''
|
||||||
|
elif so_name:
|
||||||
|
so = env['sale.order'].search([('name', '=', so_name)], limit=1)
|
||||||
|
if so:
|
||||||
|
customer = so.partner_id.name or ''
|
||||||
|
|
||||||
|
product_qty = int(production.product_qty or 0)
|
||||||
|
recipe = production.x_fc_recipe_id
|
||||||
|
|
||||||
|
# Build a lookup so each operation node finds its matching WO by name.
|
||||||
|
# The bridge's _generate_workorders_from_recipe() copies node.name →
|
||||||
|
# wo.name, so this is a stable join key within one MO.
|
||||||
|
wos_by_name = {}
|
||||||
|
all_wos = MrpWO.browse([]) if MrpWO is not None else []
|
||||||
|
if MrpWO is not None:
|
||||||
|
all_wos = MrpWO.search(
|
||||||
[('production_id', '=', production.id)],
|
[('production_id', '=', production.id)],
|
||||||
order='sequence, id',
|
order='sequence, id',
|
||||||
)
|
)
|
||||||
|
for wo in all_wos:
|
||||||
|
key = (wo.name or '').strip()
|
||||||
|
if key and key not in wos_by_name:
|
||||||
|
wos_by_name[key] = wo
|
||||||
|
|
||||||
nodes = []
|
wo_kind_selection = (
|
||||||
for wo in work_orders:
|
dict(MrpWO._fields['x_fc_wo_kind'].selection)
|
||||||
|
if MrpWO is not None and 'x_fc_wo_kind' in MrpWO._fields else {}
|
||||||
|
)
|
||||||
|
masking_selection = (
|
||||||
|
dict(MrpWO._fields['x_fc_masking_material'].selection)
|
||||||
|
if MrpWO is not None and 'x_fc_masking_material' in MrpWO._fields else {}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _f(wo, name):
|
||||||
|
return wo[name] if wo and name in wo._fields else False
|
||||||
|
|
||||||
|
def _dur_disp(mins):
|
||||||
|
if mins >= 60:
|
||||||
|
return f'{mins / 60:.1f}h'
|
||||||
|
if mins > 0:
|
||||||
|
return f'{int(mins)}m'
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def _wo_payload(wo):
|
||||||
|
"""Manager-Desk style fields for one WO."""
|
||||||
qty_done = int(wo.qty_produced or 0)
|
qty_done = int(wo.qty_produced or 0)
|
||||||
qty_total = int(wo.qty_production or production.product_qty or 0)
|
qty_total = int(wo.qty_production or product_qty or 0)
|
||||||
|
wo_kind = _f(wo, 'x_fc_wo_kind') or 'other'
|
||||||
# Duration display
|
assigned = _f(wo, 'x_fc_assigned_user_id')
|
||||||
duration_mins = wo.duration or 0
|
bath = _f(wo, 'x_fc_bath_id')
|
||||||
if duration_mins >= 60:
|
tank = _f(wo, 'x_fc_tank_id')
|
||||||
duration_display = f'{duration_mins / 60:.1f}h'
|
oven = _f(wo, 'x_fc_oven_id')
|
||||||
elif duration_mins > 0:
|
rack = _f(wo, 'x_fc_rack_id')
|
||||||
duration_display = f'{int(duration_mins)}m'
|
masking = _f(wo, 'x_fc_masking_material')
|
||||||
else:
|
return {
|
||||||
duration_display = ''
|
|
||||||
|
|
||||||
# Build children — sub-state nodes
|
|
||||||
children = []
|
|
||||||
if wo.state in ('ready', 'waiting'):
|
|
||||||
children.append({
|
|
||||||
'id': f'{wo.id}_ready',
|
|
||||||
'name': f'Ready for {wo.workcenter_id.name or wo.name}',
|
|
||||||
'state': 'ready',
|
|
||||||
'qty_done': 0,
|
|
||||||
'qty_total': qty_total,
|
|
||||||
})
|
|
||||||
elif wo.state == 'progress':
|
|
||||||
children.append({
|
|
||||||
'id': f'{wo.id}_active',
|
|
||||||
'name': f'{wo.workcenter_id.name or wo.name}-ing',
|
|
||||||
'state': 'progress',
|
|
||||||
'qty_done': qty_done,
|
|
||||||
'qty_total': qty_total,
|
|
||||||
})
|
|
||||||
# Also show "remaining" child if partial
|
|
||||||
remaining = qty_total - qty_done
|
|
||||||
if remaining > 0:
|
|
||||||
children.append({
|
|
||||||
'id': f'{wo.id}_remaining',
|
|
||||||
'name': f'Ready for {wo.workcenter_id.name or wo.name}',
|
|
||||||
'state': 'ready',
|
|
||||||
'qty_done': 0,
|
|
||||||
'qty_total': remaining,
|
|
||||||
})
|
|
||||||
|
|
||||||
nodes.append({
|
|
||||||
'id': wo.id,
|
|
||||||
'workorder_id': wo.id,
|
'workorder_id': wo.id,
|
||||||
'sequence': wo.sequence or 0,
|
'wo_state': wo.state or '',
|
||||||
'name': wo.display_name or wo.name,
|
|
||||||
'work_center_name': wo.workcenter_id.name if wo.workcenter_id else '',
|
|
||||||
'state': wo.state or '',
|
|
||||||
'qty_done': qty_done,
|
'qty_done': qty_done,
|
||||||
'qty_total': qty_total,
|
'qty_total': qty_total,
|
||||||
'duration_display': duration_display,
|
'wo_kind': wo_kind,
|
||||||
'children': children,
|
'wo_kind_label': wo_kind_selection.get(wo_kind, ''),
|
||||||
|
'assigned_user_name': assigned.name if assigned else '',
|
||||||
|
'bath': bath.name if bath else '',
|
||||||
|
'tank': tank.name if tank else '',
|
||||||
|
'oven': oven.name if oven else '',
|
||||||
|
'rack': rack.name if rack else '',
|
||||||
|
'masking_material': (
|
||||||
|
masking_selection.get(masking, '') if masking else ''
|
||||||
|
),
|
||||||
|
'duration_display': _dur_disp(wo.duration or 0),
|
||||||
|
'duration_expected_display': _dur_disp(wo.duration_expected or 0),
|
||||||
|
'missing_for_release': _f(wo, 'x_fc_missing_for_release') or '',
|
||||||
|
}
|
||||||
|
|
||||||
|
def _step_state_for(step_node, wo):
|
||||||
|
"""Map a recipe step's state from the parent operation's WO.
|
||||||
|
|
||||||
|
The step nodes are templates ("Ready For Blast", "Blast",
|
||||||
|
"Bake", etc.). We push the operation's WO state down so the
|
||||||
|
step that represents the live position renders highlighted.
|
||||||
|
|
||||||
|
Convention: a step whose name contains "ready" represents the
|
||||||
|
queued/waiting phase; the other step represents the action
|
||||||
|
phase.
|
||||||
|
"""
|
||||||
|
if not wo:
|
||||||
|
return ''
|
||||||
|
step_name = (step_node.name or '').lower()
|
||||||
|
is_ready_step = 'ready' in step_name
|
||||||
|
wo_state = wo.state or ''
|
||||||
|
if wo_state == 'done':
|
||||||
|
return 'done'
|
||||||
|
if wo_state in ('ready', 'waiting'):
|
||||||
|
return 'ready' if is_ready_step else 'pending'
|
||||||
|
if wo_state == 'progress':
|
||||||
|
return 'progress' if not is_ready_step else 'done'
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def _step_qty_for(step_node, wo):
|
||||||
|
"""Live qty for a step — fed from the parent WO."""
|
||||||
|
if not wo or not wo.qty_production:
|
||||||
|
return (0, 0)
|
||||||
|
qty_done = int(wo.qty_produced or 0)
|
||||||
|
qty_total = int(wo.qty_production or 0)
|
||||||
|
step_name = (step_node.name or '').lower()
|
||||||
|
wo_state = wo.state or ''
|
||||||
|
if wo_state == 'done':
|
||||||
|
return (qty_total, qty_total)
|
||||||
|
if wo_state in ('ready', 'waiting'):
|
||||||
|
# Everything is queued
|
||||||
|
return (qty_total, qty_total) if 'ready' in step_name else (0, 0)
|
||||||
|
if wo_state == 'progress':
|
||||||
|
if 'ready' in step_name:
|
||||||
|
remaining = qty_total - qty_done
|
||||||
|
return (remaining, remaining) if remaining > 0 else (0, 0)
|
||||||
|
return (qty_done, qty_total)
|
||||||
|
return (0, 0)
|
||||||
|
|
||||||
|
# Track which WOs were attached to a recipe node — leftovers get
|
||||||
|
# pushed under the recipe root as orphan operations.
|
||||||
|
attached_wo_ids = set()
|
||||||
|
|
||||||
|
def _walk(node, parent_wo=None):
|
||||||
|
wo = wos_by_name.get((node.name or '').strip())
|
||||||
|
wo_data = {}
|
||||||
|
if node.node_type == 'operation' and wo:
|
||||||
|
attached_wo_ids.add(wo.id)
|
||||||
|
wo_data = _wo_payload(wo)
|
||||||
|
# If this node is a `step` whose parent operation has a WO,
|
||||||
|
# mirror the WO's state onto the step so the live phase
|
||||||
|
# ("Ready for X" or "X") renders highlighted.
|
||||||
|
step_state = ''
|
||||||
|
step_qty_done, step_qty_total = 0, 0
|
||||||
|
if node.node_type == 'step' and parent_wo:
|
||||||
|
step_state = _step_state_for(node, parent_wo)
|
||||||
|
step_qty_done, step_qty_total = _step_qty_for(node, parent_wo)
|
||||||
|
|
||||||
|
# Recurse — pass this operation's WO down so step children inherit
|
||||||
|
inherited_wo = wo if (node.node_type == 'operation' and wo) else parent_wo
|
||||||
|
children_payload = []
|
||||||
|
for child in node.child_ids.sorted('sequence'):
|
||||||
|
children_payload.append(_walk(child, inherited_wo))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': f'n_{node.id}',
|
||||||
|
'name': node.name or '',
|
||||||
|
'node_type': node.node_type,
|
||||||
|
'icon': node.icon or '',
|
||||||
|
'sequence': node.sequence or 0,
|
||||||
|
'workorder_id': wo_data.get('workorder_id'),
|
||||||
|
'wo_state': wo_data.get('wo_state', ''),
|
||||||
|
'state': wo_data.get('wo_state') or step_state or '',
|
||||||
|
'qty_done': wo_data.get('qty_done') or step_qty_done or 0,
|
||||||
|
'qty_total': wo_data.get('qty_total') or step_qty_total or 0,
|
||||||
|
'wo_kind': wo_data.get('wo_kind', ''),
|
||||||
|
'wo_kind_label': wo_data.get('wo_kind_label', ''),
|
||||||
|
'assigned_user_name': wo_data.get('assigned_user_name', ''),
|
||||||
|
'bath': wo_data.get('bath', ''),
|
||||||
|
'tank': wo_data.get('tank', ''),
|
||||||
|
'oven': wo_data.get('oven', ''),
|
||||||
|
'rack': wo_data.get('rack', ''),
|
||||||
|
'masking_material': wo_data.get('masking_material', ''),
|
||||||
|
'duration_display': wo_data.get('duration_display', ''),
|
||||||
|
'duration_expected_display': wo_data.get(
|
||||||
|
'duration_expected_display', ''),
|
||||||
|
'missing_for_release': wo_data.get('missing_for_release', ''),
|
||||||
|
'children': children_payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
if recipe:
|
||||||
|
root = _walk(recipe)
|
||||||
|
# Append orphan WOs (those not matched to any recipe node by name)
|
||||||
|
# so we don't lose them — these usually appear when the user
|
||||||
|
# adds ad-hoc WOs after generation.
|
||||||
|
for wo in all_wos:
|
||||||
|
if wo.id in attached_wo_ids:
|
||||||
|
continue
|
||||||
|
wo_data = _wo_payload(wo)
|
||||||
|
orphan = {
|
||||||
|
'id': f'wo_{wo.id}',
|
||||||
|
'name': wo.name or '',
|
||||||
|
'node_type': 'operation',
|
||||||
|
'icon': '',
|
||||||
|
'sequence': wo.sequence or 0,
|
||||||
|
'workorder_id': wo.id,
|
||||||
|
'wo_state': wo.state or '',
|
||||||
|
'state': wo.state or '',
|
||||||
|
'qty_done': wo_data['qty_done'],
|
||||||
|
'qty_total': wo_data['qty_total'],
|
||||||
|
'wo_kind': wo_data['wo_kind'],
|
||||||
|
'wo_kind_label': wo_data['wo_kind_label'],
|
||||||
|
'assigned_user_name': wo_data['assigned_user_name'],
|
||||||
|
'bath': wo_data['bath'],
|
||||||
|
'tank': wo_data['tank'],
|
||||||
|
'oven': wo_data['oven'],
|
||||||
|
'rack': wo_data['rack'],
|
||||||
|
'masking_material': wo_data['masking_material'],
|
||||||
|
'duration_display': wo_data['duration_display'],
|
||||||
|
'duration_expected_display': wo_data['duration_expected_display'],
|
||||||
|
'missing_for_release': wo_data['missing_for_release'],
|
||||||
|
'children': [],
|
||||||
|
}
|
||||||
|
root['children'].append(orphan)
|
||||||
|
else:
|
||||||
|
# No recipe — synth a root with WOs as direct operation children.
|
||||||
|
child_nodes = []
|
||||||
|
for wo in all_wos:
|
||||||
|
wo_data = _wo_payload(wo)
|
||||||
|
child_nodes.append({
|
||||||
|
'id': f'wo_{wo.id}',
|
||||||
|
'name': wo.name or '',
|
||||||
|
'node_type': 'operation',
|
||||||
|
'icon': '',
|
||||||
|
'sequence': wo.sequence or 0,
|
||||||
|
'workorder_id': wo.id,
|
||||||
|
'wo_state': wo.state or '',
|
||||||
|
'state': wo.state or '',
|
||||||
|
'qty_done': wo_data['qty_done'],
|
||||||
|
'qty_total': wo_data['qty_total'],
|
||||||
|
'wo_kind': wo_data['wo_kind'],
|
||||||
|
'wo_kind_label': wo_data['wo_kind_label'],
|
||||||
|
'assigned_user_name': wo_data['assigned_user_name'],
|
||||||
|
'bath': wo_data['bath'],
|
||||||
|
'tank': wo_data['tank'],
|
||||||
|
'oven': wo_data['oven'],
|
||||||
|
'rack': wo_data['rack'],
|
||||||
|
'masking_material': wo_data['masking_material'],
|
||||||
|
'duration_display': wo_data['duration_display'],
|
||||||
|
'duration_expected_display': wo_data['duration_expected_display'],
|
||||||
|
'missing_for_release': wo_data['missing_for_release'],
|
||||||
|
'children': [],
|
||||||
})
|
})
|
||||||
|
root = {
|
||||||
|
'id': 'root',
|
||||||
|
'name': production.product_id.display_name if production.product_id
|
||||||
|
else (production.name or 'Process'),
|
||||||
|
'node_type': 'recipe',
|
||||||
|
'icon': 'fa-sitemap',
|
||||||
|
'sequence': 0,
|
||||||
|
'children': child_nodes,
|
||||||
|
'workorder_id': None,
|
||||||
|
'state': production.state or '',
|
||||||
|
'wo_state': '',
|
||||||
|
'qty_done': 0, 'qty_total': 0,
|
||||||
|
'wo_kind': '', 'wo_kind_label': '',
|
||||||
|
'assigned_user_name': '', 'bath': '', 'tank': '', 'oven': '',
|
||||||
|
'rack': '', 'masking_material': '',
|
||||||
|
'duration_display': '', 'duration_expected_display': '',
|
||||||
|
'missing_for_release': '',
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'production_name': production.name or '',
|
'production_name': production.name or '',
|
||||||
'product_name': production.product_id.display_name if production.product_id else '',
|
'product_name': production.product_id.display_name if production.product_id else '',
|
||||||
'state': production.state or '',
|
'state': production.state or '',
|
||||||
'nodes': nodes,
|
'customer': customer,
|
||||||
|
'so_name': so_name,
|
||||||
|
'product_qty': product_qty,
|
||||||
|
'recipe': recipe.name if recipe else '',
|
||||||
|
'root': root,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
/** @odoo-module **/
|
/** @odoo-module **/
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Fusion Plating — Process Tree View (OWL backend client action)
|
// Fusion Plating — Process Tree (horizontal hierarchical view)
|
||||||
// Copyright 2026 Nexa Systems Inc.
|
// Copyright 2026 Nexa Systems Inc. · License OPL-1
|
||||||
// License OPL-1 (Odoo Proprietary License v1.0)
|
|
||||||
//
|
//
|
||||||
// Visual routing-step tree for a single manufacturing order showing progress
|
// Renders the MO's recipe (recipe → sub_process → operation → state) as a
|
||||||
// bars per work order.
|
// 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.
|
||||||
//
|
//
|
||||||
// Odoo 19 conventions:
|
// Action context:
|
||||||
// * Backend OWL component: `static template` + `static props = ["*"]`
|
// production_id — required; the MO whose recipe to render
|
||||||
// * RPC via standalone `rpc()` from @web/core/network/rpc
|
// back_workorder_id — optional; if set, the back button returns to
|
||||||
// * Registered under registry.category("actions") → "fp_process_tree"
|
// that WO instead of Plant Overview
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
import { Component, useState, onMounted } from "@odoo/owl";
|
import { Component, useState, onMounted } from "@odoo/owl";
|
||||||
@@ -30,9 +31,12 @@ export class ProcessTree extends Component {
|
|||||||
productionName: "",
|
productionName: "",
|
||||||
productName: "",
|
productName: "",
|
||||||
moState: "",
|
moState: "",
|
||||||
nodes: [],
|
customer: "",
|
||||||
|
soName: "",
|
||||||
|
productQty: 0,
|
||||||
|
recipe: "",
|
||||||
|
root: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
collapsed: {}, // node id → boolean
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -40,21 +44,20 @@ export class ProcessTree extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- Data loading ------------------------------------------------------
|
// ---- Action context -----------------------------------------------------
|
||||||
|
|
||||||
get productionId() {
|
get _ctx() {
|
||||||
// Client action may receive production_id via action context or params
|
const a = this.props.action || {};
|
||||||
const ctx = this.props.action && this.props.action.context;
|
return { ...(a.context || {}), ...(a.params || {}) };
|
||||||
if (ctx && ctx.production_id) {
|
|
||||||
return ctx.production_id;
|
|
||||||
}
|
}
|
||||||
const params = this.props.action && this.props.action.params;
|
get productionId() { return this._ctx.production_id || null; }
|
||||||
if (params && params.production_id) {
|
get backWorkorderId() { return this._ctx.back_workorder_id || null; }
|
||||||
return params.production_id;
|
get backLabel() {
|
||||||
}
|
return this.backWorkorderId ? "Back to Work Order" : "Plant Overview";
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Data ---------------------------------------------------------------
|
||||||
|
|
||||||
async loadTree() {
|
async loadTree() {
|
||||||
const prodId = this.productionId;
|
const prodId = this.productionId;
|
||||||
if (!prodId) {
|
if (!prodId) {
|
||||||
@@ -66,14 +69,18 @@ export class ProcessTree extends Component {
|
|||||||
}
|
}
|
||||||
this.state.loading = true;
|
this.state.loading = true;
|
||||||
try {
|
try {
|
||||||
const result = await rpc("/fp/shopfloor/process_tree", {
|
const r = await rpc("/fp/shopfloor/process_tree", {
|
||||||
production_id: prodId,
|
production_id: prodId,
|
||||||
});
|
});
|
||||||
if (result) {
|
if (r) {
|
||||||
this.state.productionName = result.production_name || "";
|
this.state.productionName = r.production_name || "";
|
||||||
this.state.productName = result.product_name || "";
|
this.state.productName = r.product_name || "";
|
||||||
this.state.moState = result.state || "";
|
this.state.moState = r.state || "";
|
||||||
this.state.nodes = result.nodes || [];
|
this.state.customer = r.customer || "";
|
||||||
|
this.state.soName = r.so_name || "";
|
||||||
|
this.state.productQty = r.product_qty || 0;
|
||||||
|
this.state.recipe = r.recipe || "";
|
||||||
|
this.state.root = r.root || null;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.notification.add(
|
this.notification.add(
|
||||||
@@ -85,20 +92,10 @@ export class ProcessTree extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- Collapse / expand -------------------------------------------------
|
// ---- Navigation ---------------------------------------------------------
|
||||||
|
|
||||||
toggleNode(nodeId) {
|
|
||||||
this.state.collapsed[nodeId] = !this.state.collapsed[nodeId];
|
|
||||||
}
|
|
||||||
|
|
||||||
isCollapsed(nodeId) {
|
|
||||||
return !!this.state.collapsed[nodeId];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- Navigation --------------------------------------------------------
|
|
||||||
|
|
||||||
onNodeClick(node) {
|
onNodeClick(node) {
|
||||||
if (!node.workorder_id) {
|
if (!node || !node.workorder_id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.action.doAction({
|
this.action.doAction({
|
||||||
@@ -110,54 +107,68 @@ export class ProcessTree extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onBackToOverview() {
|
onBack() {
|
||||||
|
const woId = this.backWorkorderId;
|
||||||
|
if (woId) {
|
||||||
|
this.action.doAction({
|
||||||
|
type: "ir.actions.act_window",
|
||||||
|
res_model: "mrp.workorder",
|
||||||
|
res_id: parseInt(woId, 10),
|
||||||
|
views: [[false, "form"]],
|
||||||
|
target: "current",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.action.doAction("fusion_plating_shopfloor.action_fp_plant_overview");
|
this.action.doAction("fusion_plating_shopfloor.action_fp_plant_overview");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- Helpers -----------------------------------------------------------
|
// ---- Helpers ------------------------------------------------------------
|
||||||
|
|
||||||
getProgressPct(node) {
|
/** Return the css class chain for a node card (state + node_type). */
|
||||||
if (!node.qty_total || node.qty_total === 0) {
|
getCardClass(node) {
|
||||||
return 0;
|
const parts = ["o_fp_pt_card"];
|
||||||
|
parts.push(`o_fp_pt_type_${node.node_type || "unknown"}`);
|
||||||
|
if (node.state) {
|
||||||
|
parts.push(`o_fp_pt_state_${node.state}`);
|
||||||
}
|
}
|
||||||
return Math.round((node.qty_done / node.qty_total) * 100);
|
if (node.workorder_id) {
|
||||||
|
parts.push("o_fp_pt_clickable");
|
||||||
|
}
|
||||||
|
if (this.isHighlight(node)) {
|
||||||
|
parts.push("o_fp_pt_highlight");
|
||||||
|
}
|
||||||
|
return parts.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
getProgressClass(node) {
|
/** A node should pulse-highlight if it is the live position of the MO. */
|
||||||
const pct = this.getProgressPct(node);
|
isHighlight(node) {
|
||||||
if (pct >= 100) {
|
return node.state === "ready"
|
||||||
return "o_fp_tree_progress_done";
|
|| node.state === "progress"
|
||||||
}
|
|| node.state === "waiting";
|
||||||
if (pct > 0) {
|
|
||||||
return "o_fp_tree_progress_active";
|
|
||||||
}
|
|
||||||
return "o_fp_tree_progress_empty";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getNodeStateLabel(state) {
|
getKindBadge(node) {
|
||||||
const map = {
|
if (!node.wo_kind) return null;
|
||||||
pending: "Pending",
|
return {
|
||||||
waiting: "Waiting",
|
cls: `o_fp_pt_kind o_fp_pt_kind_${node.wo_kind}`,
|
||||||
ready: "Ready",
|
label: node.wo_kind_label || node.wo_kind,
|
||||||
progress: "In Progress",
|
|
||||||
done: "Done",
|
|
||||||
cancel: "Cancelled",
|
|
||||||
};
|
};
|
||||||
return map[state] || state || "—";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getNodeStateClass(state) {
|
qtyLabel(node) {
|
||||||
switch (state) {
|
if (!node.qty_total) return "";
|
||||||
case "done":
|
return `${node.qty_done}/${node.qty_total}`;
|
||||||
return "o_fp_tree_state_done";
|
}
|
||||||
case "progress":
|
|
||||||
return "o_fp_tree_state_progress";
|
nodeIcon(node) {
|
||||||
case "ready":
|
if (node.icon) return node.icon;
|
||||||
return "o_fp_tree_state_ready";
|
switch (node.node_type) {
|
||||||
case "cancel":
|
case "recipe": return "fa-cubes";
|
||||||
return "o_fp_tree_state_cancel";
|
case "sub_process": return "fa-folder";
|
||||||
default:
|
case "operation": return "fa-cog";
|
||||||
return "o_fp_tree_state_pending";
|
case "step": return "fa-circle-o";
|
||||||
|
case "state": return "fa-circle";
|
||||||
|
default: return "fa-square";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,298 +1,398 @@
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Fusion Plating — Process Tree View
|
// Fusion Plating — Process Tree (horizontal hierarchical, v3, 2026-04)
|
||||||
// Copyright 2026 Nexa Systems Inc.
|
// Copyright 2026 Nexa Systems Inc. · License OPL-1
|
||||||
// License OPL-1 (Odoo Proprietary License v1.0)
|
|
||||||
//
|
//
|
||||||
// THEME AWARENESS
|
// Hierarchical bracket tree:
|
||||||
// ---------------
|
|
||||||
// All colours come from CSS custom properties (Bootstrap / Odoo tokens) so
|
|
||||||
// the tree view renders correctly in BOTH light and dark mode.
|
|
||||||
//
|
//
|
||||||
// background: var(--bs-body-bg)
|
// [Recipe]──┬──[Sub-Process]──┬──[Operation]──┬──[Ready for X]
|
||||||
// surface: var(--o-view-background-color)
|
// │ │ └──[X]
|
||||||
// foreground: var(--bs-body-color)
|
// │ └──[Operation]
|
||||||
// muted text: var(--bs-secondary-color)
|
// ├──[Operation]
|
||||||
// border: var(--bs-border-color)
|
// └──[Operation]
|
||||||
// primary: var(--o-action)
|
//
|
||||||
|
// Each .o_fp_pt_node is `display: flex` with:
|
||||||
|
// - the card on the left
|
||||||
|
// - .o_fp_pt_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.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
.o_fp_process_tree {
|
|
||||||
|
@media (hover: none) {
|
||||||
|
.o_fp_process_tree [class*="o_fp_pt_"]:hover {
|
||||||
|
transform: none !important;
|
||||||
|
box-shadow: inherit !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Connector geometry -------------------------------------------------------
|
||||||
|
// Tweaking these recalculates the whole bracket-tree layout.
|
||||||
|
$pt-card-h : 44px; // nominal card height (cards may be taller
|
||||||
|
// when meta line wraps; centre stays at h/2)
|
||||||
|
$pt-row-gap : 12px; // vertical gap between sibling children
|
||||||
|
$pt-indent : 36px; // horizontal gap from parent → children
|
||||||
|
$pt-stub : 28px; // horizontal connector segment length
|
||||||
|
$pt-line-color : #6b7280; // connector colour
|
||||||
|
$pt-line-width : 2px;
|
||||||
|
|
||||||
|
|
||||||
|
.o_fp_process_tree.o_fp_pt_v3 {
|
||||||
|
font-family: $fp-font-stack;
|
||||||
|
background-color: $fp-page;
|
||||||
|
color: $fp-ink;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto; // both axes — wide trees scroll horizontally
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
padding: $fp-space-4 $fp-space-5;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
gap: $fp-space-3;
|
||||||
min-height: 0;
|
|
||||||
background: var(--o-view-background-color, var(--bs-body-bg));
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Header -----------------------------------------------------------------
|
@media (max-width: 600px) { padding: $fp-space-3; gap: $fp-space-3; }
|
||||||
|
|
||||||
.o_fp_pt_header {
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Header (compact strip)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
.o_fp_pt_header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
gap: $fp-space-3;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 12px;
|
padding: $fp-space-3 $fp-space-4;
|
||||||
padding: 16px 24px;
|
background-color: $fp-card;
|
||||||
background: var(--bs-body-bg);
|
border-radius: $fp-radius-md;
|
||||||
border-bottom: 1px solid var(--bs-border-color);
|
box-shadow: $fp-elev-1;
|
||||||
box-shadow: 0 1px 3px color-mix(in srgb, var(--bs-body-color) 6%, transparent);
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
.o_fp_pt_header_left {
|
z-index: 5;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
.o_fp_pt_back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: $fp-radius-pill;
|
||||||
|
background-color: $fp-card-soft;
|
||||||
|
color: $fp-ink-soft;
|
||||||
|
font-weight: $fp-weight-medium;
|
||||||
|
font-size: $fp-text-sm;
|
||||||
|
border: 1px solid #{$fp-border};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color $fp-dur $fp-ease,
|
||||||
|
border-color $fp-dur $fp-ease,
|
||||||
|
color $fp-dur $fp-ease;
|
||||||
|
@include fp-hover-only {
|
||||||
|
&:hover {
|
||||||
|
background-color: color-mix(in srgb, #{$fp-accent} 8%, $fp-card);
|
||||||
|
border-color: color-mix(in srgb, #{$fp-accent} 45%, #{$fp-border});
|
||||||
|
color: $fp-ink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.o_fp_pt_title_block { flex: 1 1 auto; min-width: 0; }
|
||||||
.o_fp_pt_title {
|
.o_fp_pt_title {
|
||||||
font-size: 1.2rem;
|
font-size: $fp-text-md;
|
||||||
font-weight: 700;
|
font-weight: $fp-weight-bold;
|
||||||
color: var(--bs-body-color);
|
margin: 0;
|
||||||
|
color: $fp-ink;
|
||||||
|
display: inline-flex; align-items: center; gap: 4px;
|
||||||
|
.o_fp_pt_mo_name { color: $fp-ink-soft; font-weight: $fp-weight-semibold; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.o_fp_pt_subtitle {
|
.o_fp_pt_subtitle {
|
||||||
font-size: 0.85rem;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Tree container ---------------------------------------------------------
|
|
||||||
|
|
||||||
.o_fp_pt_tree {
|
|
||||||
padding: 24px;
|
|
||||||
padding-left: 48px;
|
|
||||||
overflow-y: auto;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Node wrapper -----------------------------------------------------------
|
|
||||||
|
|
||||||
.o_fp_pt_node_wrapper {
|
|
||||||
position: relative;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Connector line (vertical line between nodes) ---------------------------
|
|
||||||
|
|
||||||
.o_fp_pt_connector {
|
|
||||||
width: 3px;
|
|
||||||
height: 20px;
|
|
||||||
background: var(--bs-border-color);
|
|
||||||
margin-left: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Node box ---------------------------------------------------------------
|
|
||||||
|
|
||||||
.o_fp_pt_node {
|
|
||||||
background: var(--bs-secondary-bg);
|
|
||||||
color: var(--bs-body-color);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 14px 18px;
|
|
||||||
max-width: 440px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: box-shadow 0.15s, transform 0.1s;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: 0 3px 12px color-mix(in srgb, var(--bs-body-color) 15%, transparent);
|
|
||||||
transform: translateX(2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
// State colour accents (left border)
|
|
||||||
&.o_fp_tree_state_done {
|
|
||||||
border-left: 5px solid var(--bs-success);
|
|
||||||
}
|
|
||||||
&.o_fp_tree_state_progress {
|
|
||||||
border-left: 5px solid var(--bs-warning);
|
|
||||||
}
|
|
||||||
&.o_fp_tree_state_ready {
|
|
||||||
border-left: 5px solid var(--bs-primary);
|
|
||||||
}
|
|
||||||
&.o_fp_tree_state_cancel {
|
|
||||||
border-left: 5px solid var(--bs-secondary);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
&.o_fp_tree_state_pending {
|
|
||||||
border-left: 5px solid var(--bs-border-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_fp_pt_node_header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_fp_pt_node_name {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_fp_pt_node_seq {
|
|
||||||
color: var(--bs-secondary-color);
|
|
||||||
font-weight: 400;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_fp_pt_toggle_btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--bs-secondary-color);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 2px 6px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--bs-body-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_fp_pt_node_wc {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--bs-secondary-color) !important;
|
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
font-size: $fp-text-xs;
|
||||||
|
color: $fp-ink-mute;
|
||||||
// ---- State badges inside tree -----------------------------------------------
|
display: flex; flex-wrap: wrap; align-items: center; gap: 2px;
|
||||||
|
.fa { margin-right: 2px; opacity: 0.7; }
|
||||||
.o_fp_pt_node_state {
|
|
||||||
.badge {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 3px 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.o_fp_tree_state_done {
|
|
||||||
background: var(--bs-success) !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
.o_fp_tree_state_progress {
|
|
||||||
background: var(--bs-warning) !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
.o_fp_tree_state_ready {
|
|
||||||
background: var(--bs-primary) !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
.o_fp_tree_state_cancel {
|
|
||||||
background: var(--bs-secondary) !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
.o_fp_tree_state_pending {
|
|
||||||
background: var(--bs-tertiary-bg) !important;
|
|
||||||
color: var(--bs-secondary-color) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Progress bar -----------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
// Empty / loading
|
||||||
.o_fp_pt_bar {
|
// -------------------------------------------------------------------------
|
||||||
height: 8px;
|
.o_fp_pt_empty {
|
||||||
background: var(--bs-tertiary-bg);
|
text-align: center;
|
||||||
border-radius: 4px;
|
padding: $fp-space-7 $fp-space-5;
|
||||||
overflow: hidden;
|
color: $fp-ink-mute;
|
||||||
|
background-color: $fp-card;
|
||||||
&.o_fp_pt_bar_sm {
|
border-radius: $fp-radius-md;
|
||||||
height: 6px;
|
box-shadow: $fp-elev-1;
|
||||||
|
font-size: $fp-text-sm;
|
||||||
|
max-width: 520px;
|
||||||
|
> .fa { font-size: 1.75rem; margin-bottom: $fp-space-2; opacity: 0.6; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.o_fp_pt_bar_fill {
|
|
||||||
height: 100%;
|
// -------------------------------------------------------------------------
|
||||||
border-radius: 4px;
|
// Tree canvas — horizontally scrollable
|
||||||
transition: width 0.3s ease;
|
// -------------------------------------------------------------------------
|
||||||
|
.o_fp_pt_canvas {
|
||||||
|
padding: $fp-space-3 0;
|
||||||
|
min-width: max-content; // let cards push the canvas wider for scroll
|
||||||
}
|
}
|
||||||
|
|
||||||
&.o_fp_tree_progress_active .o_fp_pt_bar_fill {
|
|
||||||
background: var(--bs-warning);
|
|
||||||
}
|
|
||||||
&.o_fp_tree_progress_done .o_fp_pt_bar_fill {
|
|
||||||
background: var(--bs-success);
|
|
||||||
}
|
|
||||||
&.o_fp_tree_progress_empty .o_fp_pt_bar_fill {
|
|
||||||
background: var(--bs-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_fp_pt_bar_label {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--bs-secondary-color);
|
|
||||||
margin-top: 2px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_fp_pt_node_duration {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--bs-secondary-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Children (sub-state nodes) ---------------------------------------------
|
|
||||||
|
|
||||||
.o_fp_pt_children {
|
|
||||||
margin-left: 48px;
|
|
||||||
padding-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_fp_pt_child_connector {
|
|
||||||
width: 3px;
|
|
||||||
height: 12px;
|
|
||||||
background: var(--bs-border-color);
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_fp_pt_child_node {
|
|
||||||
background: var(--bs-tertiary-bg);
|
|
||||||
color: var(--bs-body-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 10px 14px;
|
|
||||||
max-width: 360px;
|
|
||||||
margin-bottom: 0;
|
|
||||||
|
|
||||||
&.o_fp_tree_state_progress {
|
|
||||||
border-left: 4px solid var(--bs-warning);
|
|
||||||
}
|
|
||||||
&.o_fp_tree_state_ready {
|
|
||||||
border-left: 4px solid var(--bs-primary);
|
|
||||||
}
|
|
||||||
&.o_fp_tree_state_done {
|
|
||||||
border-left: 4px solid var(--bs-success);
|
|
||||||
}
|
|
||||||
&.o_fp_tree_state_pending {
|
|
||||||
border-left: 4px solid var(--bs-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_fp_pt_child_name {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_fp_pt_child_progress {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.o_fp_pt_bar {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Responsive -------------------------------------------------------------
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.o_fp_pt_tree {
|
|
||||||
padding: 16px;
|
|
||||||
padding-left: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Recursive node — flex row of [card | children-column]
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
.o_fp_pt_node {
|
.o_fp_pt_node {
|
||||||
max-width: 100%;
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.o_fp_pt_child_node {
|
|
||||||
max-width: 100%;
|
// -------------------------------------------------------------------------
|
||||||
|
// Card (Steelhead-style: dark fill, rounded, fixed-ish width per row)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
.o_fp_pt_card {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 320px;
|
||||||
|
min-height: $pt-card-h;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: #2b2f36; // dark slate, matches Steelhead look
|
||||||
|
color: #f1f3f5;
|
||||||
|
border-radius: $fp-radius-sm;
|
||||||
|
box-shadow: $fp-elev-1;
|
||||||
|
font-size: $fp-text-sm;
|
||||||
|
line-height: 1.25;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1; // sit above connector lines
|
||||||
|
transition: transform $fp-dur-fast $fp-ease,
|
||||||
|
box-shadow $fp-dur $fp-ease,
|
||||||
|
background-color $fp-dur $fp-ease;
|
||||||
|
|
||||||
|
&.o_fp_pt_clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
@include fp-hover-only {
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: $fp-elev-2;
|
||||||
|
background-color: #34394221;
|
||||||
|
background-color: #353a42;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Card type tints (subtle) -------------------------------------
|
||||||
|
&.o_fp_pt_type_recipe {
|
||||||
|
background-color: #1f2329;
|
||||||
|
font-weight: $fp-weight-bold;
|
||||||
|
}
|
||||||
|
&.o_fp_pt_type_sub_process {
|
||||||
|
background-color: #262a31;
|
||||||
|
font-weight: $fp-weight-semibold;
|
||||||
|
}
|
||||||
|
&.o_fp_pt_type_state {
|
||||||
|
background-color: #3a3f47;
|
||||||
|
font-size: $fp-text-xs;
|
||||||
|
min-height: 36px;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
&.o_fp_pt_type_step {
|
||||||
|
background-color: #353a42;
|
||||||
|
font-size: $fp-text-xs;
|
||||||
|
min-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Live state highlight ----------------------------------------
|
||||||
|
&.o_fp_pt_state_progress,
|
||||||
|
&.o_fp_pt_highlight.o_fp_pt_state_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_pt_highlight.o_fp_pt_state_ready,
|
||||||
|
&.o_fp_pt_state_ready.o_fp_pt_type_state {
|
||||||
|
background-color: #c0392b; // ready-to-pickup 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_pt_state_done.o_fp_pt_type_state {
|
||||||
|
background-color: #1e8449; // green for completed slice
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
&.o_fp_pt_state_cancel { opacity: 0.55; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_fp_pt_card_icon {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 18px;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0.85;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_fp_pt_card_body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.o_fp_pt_card_title {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.o_fp_pt_card_meta {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
opacity: 0.75;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2px 6px;
|
||||||
|
|
||||||
|
.fa { opacity: 0.8; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_fp_pt_card_right {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_fp_pt_qty {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: $fp-weight-bold;
|
||||||
|
padding: 1px 8px;
|
||||||
|
border-radius: $fp-radius-pill;
|
||||||
|
background-color: rgba(255, 255, 255, 0.18);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_fp_pt_card_open {
|
||||||
|
opacity: 0.55;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Kind badge inside cards
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
.o_fp_pt_kind {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1px 7px;
|
||||||
|
border-radius: $fp-radius-pill;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: $fp-weight-bold;
|
||||||
|
line-height: 1.3;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&.o_fp_pt_kind_wet { background-color: rgba(13, 110, 253, .25); color: #6ea8fe; }
|
||||||
|
&.o_fp_pt_kind_bake { background-color: rgba(220, 53, 69, .25); color: #f1aeb5; }
|
||||||
|
&.o_fp_pt_kind_mask { background-color: rgba(255, 193, 7, .25); color: #ffd866; }
|
||||||
|
&.o_fp_pt_kind_rack { background-color: rgba(108, 117, 125, .35); color: #d0d4d9; }
|
||||||
|
&.o_fp_pt_kind_inspect { background-color: rgba(25, 135, 84, .28); color: #75d4a4; }
|
||||||
|
&.o_fp_pt_kind_other { background-color: rgba(255, 255, 255, .12); color: #c8ccd2; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 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. Without it the children look orphaned even though the
|
||||||
|
// bus column + per-child stubs are present.
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
.o_fp_pt_children {
|
.o_fp_pt_children {
|
||||||
margin-left: 24px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $pt-row-gap;
|
||||||
|
margin-left: $pt-indent;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: -#{$pt-indent};
|
||||||
|
top: calc(#{$pt-card-h} / 2); // parent-card vertical centre
|
||||||
|
width: $pt-indent;
|
||||||
|
height: $pt-line-width;
|
||||||
|
background-color: $pt-line-color;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Connector lines (bracket style, drawn from CSS only)
|
||||||
|
//
|
||||||
|
// Each child .o_fp_pt_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_pt_children > .o_fp_pt_node {
|
||||||
|
position: relative;
|
||||||
|
padding-left: $pt-stub; // room for the horizontal stub
|
||||||
|
|
||||||
|
// -- horizontal stub from bus column → card --------------------------
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: calc(#{$pt-card-h} / 2); // align with card vertical centre
|
||||||
|
width: $pt-stub;
|
||||||
|
height: $pt-line-width;
|
||||||
|
background-color: $pt-line-color;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- vertical bus segment (default: full row, top → bottom) ----------
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: calc(-#{$pt-row-gap} / 2); // bridge gap to sibling above
|
||||||
|
bottom: calc(-#{$pt-row-gap} / 2); // bridge gap to sibling below
|
||||||
|
width: $pt-line-width;
|
||||||
|
background-color: $pt-line-color;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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::after {
|
||||||
|
bottom: calc(100% - (#{$pt-card-h} / 2));
|
||||||
|
}
|
||||||
|
// 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);
|
||||||
|
bottom: calc(100% - (#{$pt-card-h} / 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Pulse on live (in-progress / ready) cards
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
@keyframes o_fp_pt_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_pt_card.o_fp_pt_state_progress,
|
||||||
|
.o_fp_pt_card.o_fp_pt_highlight.o_fp_pt_state_ready {
|
||||||
|
animation: o_fp_pt_pulse 2.4s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,148 +3,133 @@
|
|||||||
Copyright 2026 Nexa Systems Inc.
|
Copyright 2026 Nexa Systems Inc.
|
||||||
License OPL-1 (Odoo Proprietary License v1.0)
|
License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
Part of the Fusion Plating product family.
|
Part of the Fusion Plating product family.
|
||||||
|
|
||||||
|
Process Tree — horizontal hierarchical view.
|
||||||
|
Recursive template renders the recipe → sub-process → operation → step
|
||||||
|
hierarchy with bracket connectors between cards. Active step pulses.
|
||||||
-->
|
-->
|
||||||
<templates xml:space="preserve">
|
<templates xml:space="preserve">
|
||||||
|
|
||||||
|
<!-- =====================================================================
|
||||||
|
RECURSIVE NODE TEMPLATE
|
||||||
|
Expects a `node` set in the t-call context.
|
||||||
|
===================================================================== -->
|
||||||
|
<t t-name="fusion_plating_shopfloor.ProcessNode">
|
||||||
|
<div class="o_fp_pt_node">
|
||||||
|
|
||||||
|
<!-- The card itself -->
|
||||||
|
<div t-att-class="getCardClass(node)"
|
||||||
|
t-on-click="() => this.onNodeClick(node)">
|
||||||
|
<i t-attf-class="o_fp_pt_card_icon fa #{ nodeIcon(node) }"/>
|
||||||
|
<div class="o_fp_pt_card_body">
|
||||||
|
<div class="o_fp_pt_card_title" t-esc="node.name"/>
|
||||||
|
<div class="o_fp_pt_card_meta"
|
||||||
|
t-if="node.assigned_user_name or node.bath or node.tank or node.oven or node.rack or node.masking_material or node.duration_display or node.duration_expected_display">
|
||||||
|
<span t-if="node.assigned_user_name">
|
||||||
|
<i class="fa fa-user me-1"/><t t-esc="node.assigned_user_name"/>
|
||||||
|
</span>
|
||||||
|
<span t-if="node.bath">
|
||||||
|
· <i class="fa fa-flask me-1"/><t t-esc="node.bath"/>
|
||||||
|
</span>
|
||||||
|
<span t-if="node.tank">
|
||||||
|
· <i class="fa fa-tint me-1"/><t t-esc="node.tank"/>
|
||||||
|
</span>
|
||||||
|
<span t-if="node.oven">
|
||||||
|
· <i class="fa fa-fire me-1"/><t t-esc="node.oven"/>
|
||||||
|
</span>
|
||||||
|
<span t-if="node.rack">
|
||||||
|
· <i class="fa fa-th me-1"/><t t-esc="node.rack"/>
|
||||||
|
</span>
|
||||||
|
<span t-if="node.masking_material">
|
||||||
|
· <i class="fa fa-tag me-1"/><t t-esc="node.masking_material"/>
|
||||||
|
</span>
|
||||||
|
<span t-if="node.duration_display">
|
||||||
|
· <i class="fa fa-clock-o me-1"/><t t-esc="node.duration_display"/>
|
||||||
|
</span>
|
||||||
|
<span t-elif="node.duration_expected_display">
|
||||||
|
· <i class="fa fa-hourglass-half me-1"/><t t-esc="node.duration_expected_display"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right-side: kind badge / qty / open icon -->
|
||||||
|
<div class="o_fp_pt_card_right">
|
||||||
|
<span t-if="node.wo_kind"
|
||||||
|
t-attf-class="o_fp_pt_kind o_fp_pt_kind_#{ node.wo_kind }"
|
||||||
|
t-esc="node.wo_kind_label || node.wo_kind"/>
|
||||||
|
<span class="o_fp_pt_qty"
|
||||||
|
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"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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">
|
||||||
|
<t t-set="node" t-value="child"/>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- =====================================================================
|
||||||
|
ROOT TEMPLATE
|
||||||
|
===================================================================== -->
|
||||||
<t t-name="fusion_plating_shopfloor.ProcessTree">
|
<t t-name="fusion_plating_shopfloor.ProcessTree">
|
||||||
<div class="o_fp_process_tree">
|
<div class="o_fp_process_tree o_fp_pt_v3">
|
||||||
|
|
||||||
<!-- ========== HEADER ========== -->
|
<!-- ========== HEADER ========== -->
|
||||||
<div class="o_fp_pt_header">
|
<div class="o_fp_pt_header">
|
||||||
<div class="o_fp_pt_header_left">
|
<button class="o_fp_pt_back"
|
||||||
<button class="btn btn-outline-secondary btn-sm me-3"
|
t-on-click="onBack"
|
||||||
t-on-click="onBackToOverview"
|
t-att-title="backLabel">
|
||||||
title="Back to Plant Overview">
|
<i class="fa fa-arrow-left me-2"/>
|
||||||
<i class="fa fa-arrow-left me-1"/> Overview
|
<t t-esc="backLabel"/>
|
||||||
</button>
|
</button>
|
||||||
<div class="o_fp_pt_title_block">
|
<div class="o_fp_pt_title_block">
|
||||||
<h3 class="o_fp_pt_title mb-0">
|
<h2 class="o_fp_pt_title mb-0">
|
||||||
<i class="fa fa-sitemap me-2"/>
|
<i class="fa fa-sitemap me-2"/>Process
|
||||||
Process Tree
|
<span t-if="state.productionName" class="o_fp_pt_mo_name">
|
||||||
</h3>
|
· <t t-esc="state.productionName"/>
|
||||||
<span class="o_fp_pt_subtitle text-muted" t-if="state.productionName">
|
|
||||||
<t t-esc="state.productionName"/>
|
|
||||||
<t t-if="state.productName">
|
|
||||||
— <t t-esc="state.productName"/>
|
|
||||||
</t>
|
|
||||||
</span>
|
</span>
|
||||||
|
</h2>
|
||||||
|
<div class="o_fp_pt_subtitle">
|
||||||
|
<span t-if="state.soName"><t t-esc="state.soName"/></span>
|
||||||
|
<span t-if="state.customer"> · <i class="fa fa-user me-1"/><t t-esc="state.customer"/></span>
|
||||||
|
<span t-if="state.productName"> · <t t-esc="state.productName"/></span>
|
||||||
|
<span t-if="state.productQty"> · Qty <t t-esc="state.productQty"/></span>
|
||||||
|
<span t-if="state.recipe"> · <i class="fa fa-flask me-1"/><t t-esc="state.recipe"/></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="o_fp_pt_header_right" t-if="state.moState">
|
|
||||||
<span class="badge bg-secondary">
|
|
||||||
MO: <t t-esc="state.moState"/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ========== LOADING ========== -->
|
<!-- ========== LOADING ========== -->
|
||||||
<div class="o_fp_pt_loading text-center py-5" t-if="state.loading">
|
<div class="o_fp_pt_loading text-center py-4" t-if="state.loading">
|
||||||
<i class="fa fa-spinner fa-spin fa-2x"/>
|
<i class="fa fa-spinner fa-spin fa-2x"/>
|
||||||
<p class="mt-2 text-muted">Loading process tree...</p>
|
<p class="mt-2 text-muted small">Loading process...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ========== NO PRODUCTION ID ========== -->
|
<!-- ========== EMPTY ========== -->
|
||||||
<div class="o_fp_pt_empty text-center py-5"
|
<div class="o_fp_pt_empty"
|
||||||
t-if="!state.loading and !productionId">
|
t-if="!state.loading and !productionId">
|
||||||
<i class="fa fa-exclamation-triangle fa-3x text-warning"/>
|
<i class="fa fa-exclamation-triangle"/>
|
||||||
<p class="mt-3">No manufacturing order selected.
|
<div>No manufacturing order selected.</div>
|
||||||
Open this view from a production order to see its routing tree.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="o_fp_pt_empty"
|
||||||
<!-- ========== EMPTY TREE ========== -->
|
t-if="!state.loading and productionId and !state.root">
|
||||||
<div class="o_fp_pt_empty text-center py-5"
|
<i class="fa fa-sitemap"/>
|
||||||
t-if="!state.loading and productionId and !state.nodes.length">
|
<div>No process steps for this order.</div>
|
||||||
<i class="fa fa-sitemap fa-3x text-muted"/>
|
|
||||||
<p class="mt-3 text-muted">No routing steps found for this order.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ========== TREE ========== -->
|
<!-- ========== TREE ========== -->
|
||||||
<div class="o_fp_pt_tree" t-if="state.nodes.length">
|
<div class="o_fp_pt_canvas" t-if="state.root">
|
||||||
<t t-foreach="state.nodes" t-as="node" t-key="node.id">
|
<t t-call="fusion_plating_shopfloor.ProcessNode">
|
||||||
<div class="o_fp_pt_node_wrapper">
|
<t t-set="node" t-value="state.root"/>
|
||||||
|
|
||||||
<!-- Connecting line (not on first node) -->
|
|
||||||
<div class="o_fp_pt_connector" t-if="!node_first"/>
|
|
||||||
|
|
||||||
<!-- Node box -->
|
|
||||||
<div t-att-class="'o_fp_pt_node ' + getNodeStateClass(node.state)"
|
|
||||||
t-on-click="() => this.onNodeClick(node)">
|
|
||||||
|
|
||||||
<div class="o_fp_pt_node_header">
|
|
||||||
<div class="o_fp_pt_node_name">
|
|
||||||
<span class="o_fp_pt_node_seq"
|
|
||||||
t-if="node.sequence">
|
|
||||||
<t t-esc="node.sequence"/>.
|
|
||||||
</span>
|
|
||||||
<strong t-esc="node.name"/>
|
|
||||||
</div>
|
|
||||||
<button class="o_fp_pt_toggle_btn"
|
|
||||||
t-if="node.children and node.children.length"
|
|
||||||
t-on-click.stop="() => this.toggleNode(node.id)"
|
|
||||||
title="Expand / collapse">
|
|
||||||
<i t-att-class="isCollapsed(node.id) ? 'fa fa-chevron-right' : 'fa fa-chevron-down'"/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Work centre -->
|
|
||||||
<div class="o_fp_pt_node_wc text-muted"
|
|
||||||
t-if="node.work_center_name">
|
|
||||||
<i class="fa fa-cog me-1"/>
|
|
||||||
<t t-esc="node.work_center_name"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- State badge -->
|
|
||||||
<div class="o_fp_pt_node_state mt-1">
|
|
||||||
<span t-att-class="'badge ' + getNodeStateClass(node.state)">
|
|
||||||
<t t-esc="getNodeStateLabel(node.state)"/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Progress bar -->
|
|
||||||
<div class="o_fp_pt_node_progress mt-2"
|
|
||||||
t-if="node.qty_total">
|
|
||||||
<div t-att-class="'o_fp_pt_bar ' + getProgressClass(node)">
|
|
||||||
<div class="o_fp_pt_bar_fill"
|
|
||||||
t-att-style="'width:' + getProgressPct(node) + '%'"/>
|
|
||||||
</div>
|
|
||||||
<span class="o_fp_pt_bar_label">
|
|
||||||
<t t-esc="node.qty_done"/>/<t t-esc="node.qty_total"/>
|
|
||||||
(<t t-esc="getProgressPct(node)"/>%)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Duration -->
|
|
||||||
<div class="o_fp_pt_node_duration text-muted mt-1"
|
|
||||||
t-if="node.duration_display">
|
|
||||||
<i class="fa fa-clock-o me-1"/>
|
|
||||||
<t t-esc="node.duration_display"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Child nodes (sub-states: Ready for X, X-ing) -->
|
|
||||||
<div class="o_fp_pt_children"
|
|
||||||
t-if="node.children and node.children.length and !isCollapsed(node.id)">
|
|
||||||
<t t-foreach="node.children" t-as="child" t-key="child.id">
|
|
||||||
<div class="o_fp_pt_child_connector"/>
|
|
||||||
<div t-att-class="'o_fp_pt_child_node ' + getNodeStateClass(child.state)">
|
|
||||||
<div class="o_fp_pt_child_name">
|
|
||||||
<t t-esc="child.name"/>
|
|
||||||
</div>
|
|
||||||
<div class="o_fp_pt_child_progress"
|
|
||||||
t-if="child.qty_total">
|
|
||||||
<div t-att-class="'o_fp_pt_bar o_fp_pt_bar_sm ' + getProgressClass(child)">
|
|
||||||
<div class="o_fp_pt_bar_fill"
|
|
||||||
t-att-style="'width:' + getProgressPct(child) + '%'"/>
|
|
||||||
</div>
|
|
||||||
<span class="o_fp_pt_bar_label">
|
|
||||||
<t t-esc="child.qty_done"/>/<t t-esc="child.qty_total"/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</t>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
7
fusion_plating/scripts/fp_debug_mo.py
Normal file
7
fusion_plating/scripts/fp_debug_mo.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
env = env # noqa
|
||||||
|
mo49 = env['mrp.production'].browse(49)
|
||||||
|
print('id=49:', mo49.name, 'state=', mo49.state, 'company=', mo49.company_id.id)
|
||||||
|
mo47 = env['mrp.production'].browse(47)
|
||||||
|
print('id=47:', mo47.name, 'state=', mo47.state, 'company=', mo47.company_id.id)
|
||||||
|
res = env['mrp.production'].search([('state', '=', 'done')], order='id desc', limit=3)
|
||||||
|
for m in res: print('got:', m.id, m.name, m.state)
|
||||||
13
fusion_plating/scripts/fp_isolate.py
Normal file
13
fusion_plating/scripts/fp_isolate.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
env = env # noqa
|
||||||
|
# Same exact query the audit uses
|
||||||
|
print('attempt 1 (no sudo):')
|
||||||
|
mo = env['mrp.production'].search([('state', '=', 'done')], order='id desc', limit=1)
|
||||||
|
print(f' → {mo.name} (id {mo.id})')
|
||||||
|
|
||||||
|
print('attempt 2 (.sudo()):')
|
||||||
|
mo2 = env['mrp.production'].sudo().search([('state', '=', 'done')], order='id desc', limit=1)
|
||||||
|
print(f' → {mo2.name} (id {mo2.id})')
|
||||||
|
|
||||||
|
print('attempt 3 (read 5):')
|
||||||
|
mos = env['mrp.production'].sudo().search([('state', '=', 'done')], order='id desc', limit=5)
|
||||||
|
for m in mos: print(f' → {m.name} (id {m.id})')
|
||||||
BIN
fusion_plating/so_list.png
Normal file
BIN
fusion_plating/so_list.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
Reference in New Issue
Block a user