Compare commits
2 Commits
8217bb0ff6
...
f09bef9083
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f09bef9083 | ||
|
|
54e56ed0e6 |
@@ -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/fp_menu.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',
|
||||
'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,
|
||||
)
|
||||
|
||||
# ---- 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 -----------------------------------------------------
|
||||
|
||||
display_name = fields.Char(
|
||||
@@ -270,6 +305,73 @@ class FpProcessNode(models.Model):
|
||||
raise ValidationError(
|
||||
_('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 -----------------------------------------
|
||||
|
||||
def get_tree_data(self):
|
||||
|
||||
@@ -136,9 +136,16 @@ export class RecipeTreeEditor extends Component {
|
||||
if (result && result.ok) {
|
||||
this.state.recipe = result.recipe;
|
||||
this.state.tree = result.tree;
|
||||
// Auto-expand root node
|
||||
if (result.tree) {
|
||||
this.state.expandedNodes[result.tree.id] = true;
|
||||
// Auto-expand every node on first load so the full
|
||||
// hierarchy is visible. The horizontal bracket layout
|
||||
// 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
|
||||
if (this.state.selectedNodeId) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,41 +3,177 @@
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
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">
|
||||
|
||||
<!-- ====================================================================
|
||||
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">
|
||||
<div class="o_fp_recipe_editor">
|
||||
<div class="o_fp_recipe_editor o_fp_re_v2">
|
||||
|
||||
<!-- ========== HEADER ========== -->
|
||||
<div class="o_fp_recipe_header">
|
||||
<div class="o_fp_recipe_header_left">
|
||||
<button class="btn btn-link o_fp_recipe_back_btn"
|
||||
t-on-click="onBackToList" title="Back to list">
|
||||
<i class="fa fa-arrow-left me-1"/> Recipes
|
||||
</button>
|
||||
<h2 class="o_fp_recipe_title" t-if="state.recipe">
|
||||
<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"
|
||||
t-if="state.recipe.version">
|
||||
<div class="o_fp_re_header">
|
||||
<button class="o_fp_re_back"
|
||||
t-on-click="onBackToList"
|
||||
title="Back to recipes">
|
||||
<i class="fa fa-arrow-left me-2"/>Recipes
|
||||
</button>
|
||||
<div class="o_fp_re_header_title" t-if="state.recipe">
|
||||
<h2 class="o_fp_re_h2 mb-0">
|
||||
<i class="fa fa-flask me-2"/><t t-esc="state.recipe.name"/>
|
||||
<span t-if="state.recipe.version" class="o_fp_re_ver">
|
||||
v<t t-esc="state.recipe.version"/>
|
||||
</span>
|
||||
</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">
|
||||
<span class="text-muted small me-3" t-if="state.recipe.process_type">
|
||||
<i class="fa fa-tag me-1"/>
|
||||
<t t-esc="state.recipe.process_type"/>
|
||||
</span>
|
||||
<button class="btn btn-sm btn-outline-secondary me-1"
|
||||
t-on-click="onDuplicate" title="Duplicate recipe">
|
||||
<i class="fa fa-copy me-1"/> Duplicate
|
||||
<div class="o_fp_re_header_actions" t-if="state.recipe">
|
||||
<button class="o_fp_re_btn_outline"
|
||||
t-on-click="onDuplicate"
|
||||
title="Duplicate recipe">
|
||||
<i class="fa fa-copy me-1"/>Duplicate
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
<button class="o_fp_re_btn_outline"
|
||||
t-on-click="() => this.onOpenForm(state.recipe.id)"
|
||||
title="Edit in form view">
|
||||
<i class="fa fa-pencil me-1"/> Form View
|
||||
<i class="fa fa-pencil me-1"/>Form
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,17 +184,17 @@
|
||||
<p class="mt-2 text-muted">Loading recipe tree...</p>
|
||||
</div>
|
||||
|
||||
<!-- ========== NO RECIPE ========== -->
|
||||
<div class="text-center py-5" t-if="!state.loading and !_recipeId">
|
||||
<i class="fa fa-exclamation-triangle fa-3x text-muted"/>
|
||||
<p class="mt-3 text-muted">No recipe selected.</p>
|
||||
<!-- ========== EMPTY ========== -->
|
||||
<div class="o_fp_re_empty" t-if="!state.loading and !_recipeId">
|
||||
<i class="fa fa-exclamation-triangle"/>
|
||||
<div>No recipe selected.</div>
|
||||
</div>
|
||||
|
||||
<!-- ========== TREE + PANEL LAYOUT ========== -->
|
||||
<div class="o_fp_recipe_body" t-if="state.tree">
|
||||
<!-- ========== BODY (canvas + side panel) ========== -->
|
||||
<div class="o_fp_re_body" t-if="state.tree">
|
||||
|
||||
<!-- Tree area -->
|
||||
<div class="o_fp_recipe_tree_area">
|
||||
<!-- Tree canvas -->
|
||||
<div class="o_fp_re_canvas">
|
||||
<t t-call="fusion_plating.RecipeTreeNode">
|
||||
<t t-set="node" t-value="state.tree"/>
|
||||
<t t-set="parentNode" t-value="null"/>
|
||||
@@ -67,26 +203,29 @@
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<div class="o_fp_recipe_panel_header">
|
||||
<div class="o_fp_re_panel_head">
|
||||
<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
|
||||
</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"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="o_fp_recipe_panel_body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Name</label>
|
||||
<div class="o_fp_re_panel_body">
|
||||
|
||||
<div class="o_fp_re_field">
|
||||
<label>Name</label>
|
||||
<input type="text" class="form-control"
|
||||
t-att-value="state.selectedNode.name"
|
||||
t-on-change="(ev) => { state.selectedNode.name = ev.target.value; }"/>
|
||||
</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"
|
||||
t-on-change="(ev) => { state.selectedNode.node_type = ev.target.value; }">
|
||||
<option value="recipe"
|
||||
@@ -99,53 +238,57 @@
|
||||
t-att-selected="state.selectedNode.node_type === 'step'">Step</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Icon</label>
|
||||
<div class="o_fp_recipe_icon_picker">
|
||||
|
||||
<div class="o_fp_re_field">
|
||||
<label>Icon</label>
|
||||
<div class="o_fp_re_icon_picker">
|
||||
<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-att-title="ic.label">
|
||||
<i t-att-class="'fa ' + ic.value"/>
|
||||
<i t-attf-class="fa #{ ic.value }"/>
|
||||
</button>
|
||||
</t>
|
||||
</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"
|
||||
t-att-value="state.selectedNode.estimated_duration || 0"
|
||||
t-on-change="(ev) => { state.selectedNode.estimated_duration = parseFloat(ev.target.value) || 0; }"/>
|
||||
</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">
|
||||
<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-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 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-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 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-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 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-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 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"
|
||||
t-on-change="(ev) => { state.selectedNode.opt_in_out = ev.target.value; }">
|
||||
<option value="disabled"
|
||||
@@ -156,30 +299,16 @@
|
||||
t-att-selected="state.selectedNode.opt_in_out === 'opt_out'">Opt-Out</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Info -->
|
||||
<div class="text-muted small mb-2" t-if="state.selectedNode.work_center">
|
||||
<i class="fa fa-building me-1"/>
|
||||
<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">
|
||||
|
||||
<div class="o_fp_re_tracking" t-if="state.selectedNode.create_date">
|
||||
<div>
|
||||
<i class="fa fa-calendar-plus-o me-1"/>
|
||||
Created <t t-esc="formatTimeAgo(state.selectedNode.create_date)"/>
|
||||
<t t-if="state.selectedNode.create_uid_name">
|
||||
by <strong t-esc="state.selectedNode.create_uid_name"/>
|
||||
</t>
|
||||
</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"/>
|
||||
Updated <t t-esc="formatTimeAgo(state.selectedNode.write_date)"/>
|
||||
<t t-if="state.selectedNode.write_uid_name">
|
||||
@@ -187,15 +316,15 @@
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Actions -->
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<button class="btn btn-primary flex-fill"
|
||||
|
||||
<div class="o_fp_re_panel_actions">
|
||||
<button class="o_fp_re_btn_save"
|
||||
t-on-click="saveNode"
|
||||
t-att-disabled="state.saving">
|
||||
<i t-att-class="state.saving ? 'fa fa-spinner fa-spin me-1' : 'fa fa-check me-1'"/>
|
||||
Save
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary"
|
||||
<button class="o_fp_re_btn_outline"
|
||||
t-on-click="() => this.onOpenForm(state.selectedNode.id)"
|
||||
title="Open full form">
|
||||
<i class="fa fa-external-link"/>
|
||||
@@ -208,127 +337,4 @@
|
||||
</div>
|
||||
</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>
|
||||
|
||||
@@ -79,6 +79,24 @@
|
||||
<field name="active" invisible="True"/>
|
||||
</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 string="Tracking">
|
||||
<field name="create_date" string="Created"/>
|
||||
|
||||
@@ -23,8 +23,6 @@
|
||||
<field name="code">model._fp_cron_auto_finish_completed_wos()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">minutes</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="active" eval="True"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from markupsafe import Markup
|
||||
|
||||
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.',
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 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
|
||||
# ------------------------------------------------------------------
|
||||
@@ -362,13 +432,22 @@ class MrpWorkorder(models.Model):
|
||||
# Process tree action (opens OWL client action)
|
||||
# ------------------------------------------------------------------
|
||||
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()
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'fp_process_tree',
|
||||
'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',
|
||||
'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):
|
||||
"""A WO is release-ready when the manager has set EVERY field
|
||||
button_start would block on. Used by the Manager Desk to keep
|
||||
@@ -638,7 +717,9 @@ class MrpWorkorder(models.Model):
|
||||
"""
|
||||
for wo in self:
|
||||
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')
|
||||
kind = wo.x_fc_wo_kind
|
||||
if kind == 'wet':
|
||||
@@ -771,7 +852,11 @@ class MrpWorkorder(models.Model):
|
||||
from odoo.exceptions import UserError
|
||||
for wo in self:
|
||||
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'))
|
||||
kind = wo._fp_classify_kind()
|
||||
if kind == 'wet':
|
||||
@@ -847,17 +932,62 @@ class MrpWorkorder(models.Model):
|
||||
) % (employee.name, process_type.name))
|
||||
|
||||
def _fp_check_required_fields_before_finish(self):
|
||||
"""Block button_finish on bake WOs without the actual data
|
||||
Nadcap audits demand: setpoint temp, actual duration, and a
|
||||
chart-recorder reference on the oven (so the printed chart
|
||||
for this run can be retrieved).
|
||||
"""Block button_finish on:
|
||||
|
||||
- bake WOs without setpoint temp / actual duration / chart-recorder
|
||||
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
|
||||
you don't know it until the bake is done. Chart-recorder ref
|
||||
is on the oven config — checked here as a defensive backstop.
|
||||
you don't know it until the bake is done.
|
||||
"""
|
||||
from odoo.exceptions import UserError
|
||||
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':
|
||||
continue
|
||||
missing = []
|
||||
@@ -981,3 +1111,83 @@ class MrpWorkorder(models.Model):
|
||||
'within %s hours of plate exit.'
|
||||
) % (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')"/>
|
||||
<field name="x_fc_requires_bath" 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>
|
||||
|
||||
<!-- ============================================================
|
||||
|
||||
@@ -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,10 +439,28 @@ class FpQuoteConfigurator(models.Model):
|
||||
|
||||
Scores rules by specificity -- most specific match wins.
|
||||
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.
|
||||
"""
|
||||
rules = self.env['fp.pricing.rule'].search(
|
||||
[('active', '=', True)], order='sequence, id'
|
||||
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(
|
||||
[('active', '=', True)], order='sequence, id'
|
||||
)
|
||||
cert_level = (
|
||||
self.coating_config_id.certification_level
|
||||
if self.coating_config_id else False
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Notifications',
|
||||
'version': '19.0.4.0.0',
|
||||
'version': '19.0.4.1.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Auto-email notifications at workflow milestones with configurable templates, PDF attachments, and audit log.',
|
||||
'author': 'Nexa Systems Inc.',
|
||||
@@ -35,6 +35,7 @@
|
||||
'views/fp_notification_log_views.xml',
|
||||
'views/fp_notifications_menu.xml',
|
||||
],
|
||||
'post_init_hook': 'post_init_hook',
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': False,
|
||||
|
||||
@@ -61,6 +61,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- ============================================================= -->
|
||||
@@ -118,6 +121,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</field>
|
||||
<field name="report_template_ids"
|
||||
eval="[(6, 0, [ref('fusion_plating_reports.action_report_fp_sale_portrait')])]"/>
|
||||
<field name="report_name">SalesOrder_{{ (object.name or '').replace('/','_') }}</field>
|
||||
</record>
|
||||
|
||||
<!-- ============================================================= -->
|
||||
@@ -342,6 +348,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- ============================================================= -->
|
||||
|
||||
62
fusion_plating/fusion_plating_notifications/hooks.py
Normal file
62
fusion_plating/fusion_plating_notifications/hooks.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# -*- 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_sale_portrait',
|
||||
)
|
||||
_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):
|
||||
"""Replace the template's report_template_ids with exactly [report].
|
||||
|
||||
We use `set` semantics (replace all) rather than `add` so that old
|
||||
attachments from previous refactors get cleaned up — e.g. when the
|
||||
Acknowledgement report was consolidated into the Sales Order report,
|
||||
the now-stale Acknowledgement reference gets removed here.
|
||||
"""
|
||||
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
|
||||
current_ids = set(mail_template.report_template_ids.ids)
|
||||
if current_ids != {report.id}:
|
||||
mail_template.write({
|
||||
'report_template_ids': [(6, 0, [report.id])],
|
||||
})
|
||||
_logger.info(
|
||||
'fusion_plating_notifications: set report %s on template %s',
|
||||
report_xmlid, mail_template_xmlid,
|
||||
)
|
||||
@@ -9,6 +9,24 @@ from odoo import models
|
||||
class AccountMove(models.Model):
|
||||
_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):
|
||||
res = super().action_post()
|
||||
Dispatch = self.env['fp.notification.template']
|
||||
|
||||
@@ -9,6 +9,40 @@ from odoo import models
|
||||
class SaleOrder(models.Model):
|
||||
_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):
|
||||
"""Fire the quote_sent trigger when a quotation is emailed."""
|
||||
res = super().action_quotation_send()
|
||||
|
||||
@@ -125,3 +125,42 @@ class FpPortalJob(models.Model):
|
||||
def _progress_percent(self):
|
||||
self.ensure_one()
|
||||
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>
|
||||
|
||||
<!-- 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">
|
||||
<h6 class="text-muted small text-uppercase">Documents</h6>
|
||||
<div class="list-group">
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
'report/report_wo_margin.xml',
|
||||
# Quote-to-cash reports (portrait + landscape)
|
||||
'report/report_fp_sale.xml',
|
||||
'report/report_fp_so_acknowledgement.xml',
|
||||
'report/report_fp_work_order.xml',
|
||||
'report/report_fp_job_traveller.xml',
|
||||
'report/report_fp_packing_slip.xml',
|
||||
|
||||
@@ -97,6 +97,42 @@
|
||||
</table>
|
||||
</t>
|
||||
|
||||
<!-- Scheduling + customer job reference -->
|
||||
<t t-if="doc.x_fc_customer_job_number or doc.x_fc_planned_start_date or doc.commitment_date or doc.x_fc_ship_via">
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="info-header" style="width: 25%;">CUSTOMER JOB #</th>
|
||||
<th class="info-header" style="width: 25%;">PLANNED START</th>
|
||||
<th class="info-header" style="width: 25%;">CUSTOMER DEADLINE</th>
|
||||
<th class="info-header" style="width: 25%;">SHIP VIA</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center"><span t-esc="doc.x_fc_customer_job_number or '-'"/></td>
|
||||
<td class="text-center"><span t-field="doc.x_fc_planned_start_date"/></td>
|
||||
<td class="text-center"><span t-field="doc.commitment_date"/></td>
|
||||
<td class="text-center"><span t-esc="doc.x_fc_ship_via or '-'"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
|
||||
<!-- Blanket / block-partial callout (confirmed-order shipping flags) -->
|
||||
<t t-if="doc.x_fc_is_blanket_order or doc.x_fc_block_partial_shipments">
|
||||
<div class="highlight-box">
|
||||
<t t-if="doc.x_fc_is_blanket_order">
|
||||
<strong>Blanket Order.</strong>
|
||||
Parts will be released in quantities over time.
|
||||
</t>
|
||||
<t t-if="doc.x_fc_block_partial_shipments">
|
||||
<strong>Partial shipments blocked.</strong>
|
||||
The order ships as one complete batch.
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Order lines -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
@@ -189,6 +225,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- External (customer-visible) notes -->
|
||||
<t t-if="doc.x_fc_external_note">
|
||||
<div style="margin-top: 15px;">
|
||||
<strong>Notes:</strong>
|
||||
<div t-field="doc.x_fc_external_note"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Terms and Conditions -->
|
||||
<t t-if="doc.note">
|
||||
<div style="margin-top: 15px;">
|
||||
@@ -327,6 +371,42 @@
|
||||
</table>
|
||||
</t>
|
||||
|
||||
<!-- Scheduling + customer job reference -->
|
||||
<t t-if="doc.x_fc_customer_job_number or doc.x_fc_planned_start_date or doc.commitment_date or doc.x_fc_ship_via">
|
||||
<table class="bordered info-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>CUSTOMER JOB #</th>
|
||||
<th>PLANNED START</th>
|
||||
<th>CUSTOMER DEADLINE</th>
|
||||
<th>SHIP VIA</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center"><span t-esc="doc.x_fc_customer_job_number or '-'"/></td>
|
||||
<td class="text-center"><span t-field="doc.x_fc_planned_start_date"/></td>
|
||||
<td class="text-center"><span t-field="doc.commitment_date"/></td>
|
||||
<td class="text-center"><span t-esc="doc.x_fc_ship_via or '-'"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
|
||||
<!-- Blanket / block-partial callout -->
|
||||
<t t-if="doc.x_fc_is_blanket_order or doc.x_fc_block_partial_shipments">
|
||||
<div class="highlight-box">
|
||||
<t t-if="doc.x_fc_is_blanket_order">
|
||||
<strong>Blanket Order.</strong>
|
||||
Parts will be released in quantities over time.
|
||||
</t>
|
||||
<t t-if="doc.x_fc_block_partial_shipments">
|
||||
<strong>Partial shipments blocked.</strong>
|
||||
The order ships as one complete batch.
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Order lines — hide discount column unless at least one line has a discount -->
|
||||
<t t-set="has_discount" t-value="any(l.discount for l in doc.order_line)"/>
|
||||
<t t-set="col_count" t-value="8 if has_discount else 7"/>
|
||||
@@ -426,6 +506,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- External (customer-visible) notes -->
|
||||
<t t-if="doc.x_fc_external_note">
|
||||
<div style="margin-top: 15px;">
|
||||
<strong>Notes:</strong>
|
||||
<div t-field="doc.x_fc_external_note"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Terms and Conditions -->
|
||||
<t t-if="doc.note">
|
||||
<div style="margin-top: 15px;">
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
|
||||
Sales Order Acknowledgement (Phase D7) — customer-facing
|
||||
confirmation sent shortly after action_confirm. Styled to match
|
||||
the rest of the Fusion Plating report family (portrait; bordered
|
||||
tables; company primary-colour header; totals-table footer;
|
||||
sig-box signature pair).
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="action_report_fp_so_acknowledgement" model="ir.actions.report">
|
||||
<field name="name">Sales Order Acknowledgement</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_plating_reports.report_fp_so_acknowledgement_doc</field>
|
||||
<field name="report_file">fusion_plating_reports.report_fp_so_acknowledgement_doc</field>
|
||||
<field name="binding_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="binding_type">report</field>
|
||||
<field name="print_report_name">'Acknowledgement - %s' % object.name</field>
|
||||
</record>
|
||||
|
||||
<template id="report_fp_so_acknowledgement_doc">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
|
||||
<t t-call="fusion_plating_reports.fp_portrait_styles"/>
|
||||
<div class="fp-report">
|
||||
<div class="page">
|
||||
|
||||
<!-- Title -->
|
||||
<h4>
|
||||
<span>Sales Order Acknowledgement </span>
|
||||
<span t-field="doc.name"/>
|
||||
</h4>
|
||||
|
||||
<!-- Billing / Shipping -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50%;">BILLING ADDRESS</th>
|
||||
<th style="width: 50%;">SHIPPING ADDRESS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="height: 70px;">
|
||||
<div t-field="doc.partner_invoice_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone', 'email'], 'no_marker': True}"/>
|
||||
</td>
|
||||
<td style="height: 70px;">
|
||||
<div t-field="doc.partner_shipping_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone'], 'no_marker': True}"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- References -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="info-header" style="width: 25%;">CUSTOMER PO #</th>
|
||||
<th class="info-header" style="width: 25%;">CUSTOMER JOB #</th>
|
||||
<th class="info-header" style="width: 25%;">ORDER DATE</th>
|
||||
<th class="info-header" style="width: 25%;">SALESPERSON</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-esc="doc.x_fc_po_number or '-'"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="doc.x_fc_customer_job_number or '-'"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.date_order"
|
||||
t-options="{'widget': 'date'}"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.user_id"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Scheduling -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="info-header" style="width: 25%;">PLANNED START</th>
|
||||
<th class="info-header" style="width: 25%;">INTERNAL DEADLINE</th>
|
||||
<th class="info-header" style="width: 25%;">CUSTOMER DEADLINE</th>
|
||||
<th class="info-header" style="width: 25%;">SHIP VIA</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.x_fc_planned_start_date"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.x_fc_internal_deadline"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.commitment_date"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="doc.x_fc_ship_via or '-'"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Blanket / Block Partial callout -->
|
||||
<t t-if="doc.x_fc_is_blanket_order or doc.x_fc_block_partial_shipments">
|
||||
<div class="highlight-box">
|
||||
<t t-if="doc.x_fc_is_blanket_order">
|
||||
<strong>Blanket Order.</strong>
|
||||
Parts will be released in quantities over time.
|
||||
</t>
|
||||
<t t-if="doc.x_fc_block_partial_shipments">
|
||||
<strong>Partial shipments blocked.</strong>
|
||||
The order ships as one complete batch.
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Order lines -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 14%;">PART</th>
|
||||
<th class="text-start" style="width: 36%;">DESCRIPTION</th>
|
||||
<th style="width: 18%;">TREATMENT</th>
|
||||
<th style="width: 8%;">QTY</th>
|
||||
<th style="width: 12%;">UNIT PRICE</th>
|
||||
<th style="width: 12%;">SUBTOTAL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="doc.order_line.filtered(lambda l: not l.x_fc_archived and (not l.display_type or l.display_type in ('line_section', 'line_note', 'product')))"
|
||||
t-as="line">
|
||||
<t t-if="line.display_type == 'line_section'">
|
||||
<tr class="section-row">
|
||||
<td colspan="6"><strong t-field="line.name"/></td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-elif="line.display_type == 'line_note'">
|
||||
<tr class="note-row">
|
||||
<td colspan="6"><span t-field="line.name"/></td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-esc="line.x_fc_part_catalog_id.part_number or '-'"/>
|
||||
</td>
|
||||
<td>
|
||||
<t t-set="clean_name" t-value="line.name"/>
|
||||
<t t-if="line.name and '] ' in line.name">
|
||||
<t t-set="clean_name" t-value="line.name.split('] ', 1)[1]"/>
|
||||
</t>
|
||||
<span t-esc="clean_name"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="line.x_fc_coating_config_id"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="int(line.product_uom_qty) if line.product_uom_qty == int(line.product_uom_qty) else line.product_uom_qty"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-field="line.price_unit"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-field="line.price_subtotal"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Terms + Totals -->
|
||||
<div class="row" style="margin-top: 15px;">
|
||||
<div class="col-6">
|
||||
<t t-if="doc.x_fc_invoice_strategy">
|
||||
<strong>Invoice Strategy: </strong>
|
||||
<t t-set="inv_strat" t-value="dict(doc._fields['x_fc_invoice_strategy'].selection).get(doc.x_fc_invoice_strategy, '-')"/>
|
||||
<span t-esc="inv_strat"/>
|
||||
<t t-if="doc.x_fc_invoice_strategy == 'deposit' and doc.x_fc_deposit_percent">
|
||||
(<span t-esc="doc.x_fc_deposit_percent"/>%)
|
||||
</t>
|
||||
<br/>
|
||||
</t>
|
||||
<t t-if="doc.payment_term_id.note">
|
||||
<strong>Payment Terms:</strong><br/>
|
||||
<span t-field="doc.payment_term_id.note"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="col-6" style="text-align: right;">
|
||||
<table class="totals-table" style="width: auto; margin-left: auto;">
|
||||
<tr>
|
||||
<td style="min-width: 150px;">Subtotal</td>
|
||||
<td class="text-end" style="min-width: 110px;">
|
||||
<span t-field="doc.amount_untaxed"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Taxes</td>
|
||||
<td class="text-end">
|
||||
<span t-field="doc.amount_tax"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="background-color: #eaf2f8;">
|
||||
<td><strong>Grand Total</strong></td>
|
||||
<td class="text-end"><strong>
|
||||
<span t-field="doc.amount_total"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</strong></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- External (customer-visible) notes -->
|
||||
<t t-if="doc.x_fc_external_note">
|
||||
<div style="margin-top: 15px;">
|
||||
<strong>Notes:</strong>
|
||||
<div t-field="doc.x_fc_external_note"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Signature block -->
|
||||
<div class="row" style="margin-top: 25px;">
|
||||
<div class="col-6">
|
||||
<div class="sig-box">
|
||||
<div class="sig-line"/>
|
||||
<div class="small-muted">Customer Acceptance (Signature / Date)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="sig-box">
|
||||
<t t-if="doc.signature">
|
||||
<img t-att-src="image_data_uri(doc.signature)"
|
||||
style="max-height: 3cm; max-width: 8cm;"/><br/>
|
||||
<span t-field="doc.signed_by"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="sig-line"/>
|
||||
</t>
|
||||
<div class="small-muted">Authorized Representative</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
@@ -1021,87 +1021,286 @@ class FpShopfloorController(http.Controller):
|
||||
def process_tree(self, production_id):
|
||||
"""Return routing tree for a manufacturing order.
|
||||
|
||||
Each node is an operation/work-order step. Children represent
|
||||
sub-states (ready vs active) within that step.
|
||||
"""
|
||||
MrpWO = request.env.get('mrp.workorder')
|
||||
if MrpWO is None:
|
||||
return {
|
||||
'production_name': '',
|
||||
'product_name': '',
|
||||
'state': '',
|
||||
'nodes': [],
|
||||
}
|
||||
Walks the MO's recipe tree (fusion.plating.process.node) and returns
|
||||
a recursive nested structure:
|
||||
recipe → sub_process → operation → step
|
||||
For each `operation` node we look up the matching mrp.workorder by
|
||||
name within this MO, then attach the WO state, qty progress, kind,
|
||||
equipment, and a synthetic state-child ("Ready for X" or "In X")
|
||||
so the operator sees the live position in the flow.
|
||||
|
||||
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))
|
||||
if not production.exists():
|
||||
raise UserError(f"Manufacturing order {production_id} not found")
|
||||
|
||||
work_orders = MrpWO.search(
|
||||
[('production_id', '=', production.id)],
|
||||
order='sequence, id',
|
||||
# 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)],
|
||||
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
|
||||
|
||||
wo_kind_selection = (
|
||||
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 {}
|
||||
)
|
||||
|
||||
nodes = []
|
||||
for wo in work_orders:
|
||||
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_total = int(wo.qty_production or production.product_qty or 0)
|
||||
|
||||
# Duration display
|
||||
duration_mins = wo.duration or 0
|
||||
if duration_mins >= 60:
|
||||
duration_display = f'{duration_mins / 60:.1f}h'
|
||||
elif duration_mins > 0:
|
||||
duration_display = f'{int(duration_mins)}m'
|
||||
else:
|
||||
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,
|
||||
qty_total = int(wo.qty_production or product_qty or 0)
|
||||
wo_kind = _f(wo, 'x_fc_wo_kind') or 'other'
|
||||
assigned = _f(wo, 'x_fc_assigned_user_id')
|
||||
bath = _f(wo, 'x_fc_bath_id')
|
||||
tank = _f(wo, 'x_fc_tank_id')
|
||||
oven = _f(wo, 'x_fc_oven_id')
|
||||
rack = _f(wo, 'x_fc_rack_id')
|
||||
masking = _f(wo, 'x_fc_masking_material')
|
||||
return {
|
||||
'workorder_id': wo.id,
|
||||
'sequence': wo.sequence or 0,
|
||||
'name': wo.display_name or wo.name,
|
||||
'work_center_name': wo.workcenter_id.name if wo.workcenter_id else '',
|
||||
'state': wo.state or '',
|
||||
'wo_state': wo.state or '',
|
||||
'qty_done': qty_done,
|
||||
'qty_total': qty_total,
|
||||
'duration_display': duration_display,
|
||||
'children': children,
|
||||
})
|
||||
'wo_kind': wo_kind,
|
||||
'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 {
|
||||
'production_name': production.name or '',
|
||||
'product_name': production.product_id.display_name if production.product_id else '',
|
||||
'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 **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — Process Tree View (OWL backend client action)
|
||||
// Copyright 2026 Nexa Systems Inc.
|
||||
// License OPL-1 (Odoo Proprietary License v1.0)
|
||||
// Fusion Plating — Process Tree (horizontal hierarchical view)
|
||||
// Copyright 2026 Nexa Systems Inc. · License OPL-1
|
||||
//
|
||||
// Visual routing-step tree for a single manufacturing order showing progress
|
||||
// bars per work order.
|
||||
// Renders the MO's recipe (recipe → sub_process → operation → state) as a
|
||||
// horizontal bracket tree. Cards render dark, identical card style across
|
||||
// all depths; connector lines are drawn from CSS so the layout stays in
|
||||
// pure flexbox.
|
||||
//
|
||||
// Odoo 19 conventions:
|
||||
// * Backend OWL component: `static template` + `static props = ["*"]`
|
||||
// * RPC via standalone `rpc()` from @web/core/network/rpc
|
||||
// * Registered under registry.category("actions") → "fp_process_tree"
|
||||
// Action context:
|
||||
// production_id — required; the MO whose recipe to render
|
||||
// back_workorder_id — optional; if set, the back button returns to
|
||||
// that WO instead of Plant Overview
|
||||
// =============================================================================
|
||||
|
||||
import { Component, useState, onMounted } from "@odoo/owl";
|
||||
@@ -30,9 +31,12 @@ export class ProcessTree extends Component {
|
||||
productionName: "",
|
||||
productName: "",
|
||||
moState: "",
|
||||
nodes: [],
|
||||
customer: "",
|
||||
soName: "",
|
||||
productQty: 0,
|
||||
recipe: "",
|
||||
root: null,
|
||||
loading: false,
|
||||
collapsed: {}, // node id → boolean
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -40,20 +44,19 @@ export class ProcessTree extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
// ----- Data loading ------------------------------------------------------
|
||||
// ---- Action context -----------------------------------------------------
|
||||
|
||||
get productionId() {
|
||||
// Client action may receive production_id via action context or params
|
||||
const ctx = this.props.action && this.props.action.context;
|
||||
if (ctx && ctx.production_id) {
|
||||
return ctx.production_id;
|
||||
}
|
||||
const params = this.props.action && this.props.action.params;
|
||||
if (params && params.production_id) {
|
||||
return params.production_id;
|
||||
}
|
||||
return null;
|
||||
get _ctx() {
|
||||
const a = this.props.action || {};
|
||||
return { ...(a.context || {}), ...(a.params || {}) };
|
||||
}
|
||||
get productionId() { return this._ctx.production_id || null; }
|
||||
get backWorkorderId() { return this._ctx.back_workorder_id || null; }
|
||||
get backLabel() {
|
||||
return this.backWorkorderId ? "Back to Work Order" : "Plant Overview";
|
||||
}
|
||||
|
||||
// ---- Data ---------------------------------------------------------------
|
||||
|
||||
async loadTree() {
|
||||
const prodId = this.productionId;
|
||||
@@ -66,14 +69,18 @@ export class ProcessTree extends Component {
|
||||
}
|
||||
this.state.loading = true;
|
||||
try {
|
||||
const result = await rpc("/fp/shopfloor/process_tree", {
|
||||
const r = await rpc("/fp/shopfloor/process_tree", {
|
||||
production_id: prodId,
|
||||
});
|
||||
if (result) {
|
||||
this.state.productionName = result.production_name || "";
|
||||
this.state.productName = result.product_name || "";
|
||||
this.state.moState = result.state || "";
|
||||
this.state.nodes = result.nodes || [];
|
||||
if (r) {
|
||||
this.state.productionName = r.production_name || "";
|
||||
this.state.productName = r.product_name || "";
|
||||
this.state.moState = r.state || "";
|
||||
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) {
|
||||
this.notification.add(
|
||||
@@ -85,20 +92,10 @@ export class ProcessTree extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Collapse / expand -------------------------------------------------
|
||||
|
||||
toggleNode(nodeId) {
|
||||
this.state.collapsed[nodeId] = !this.state.collapsed[nodeId];
|
||||
}
|
||||
|
||||
isCollapsed(nodeId) {
|
||||
return !!this.state.collapsed[nodeId];
|
||||
}
|
||||
|
||||
// ----- Navigation --------------------------------------------------------
|
||||
// ---- Navigation ---------------------------------------------------------
|
||||
|
||||
onNodeClick(node) {
|
||||
if (!node.workorder_id) {
|
||||
if (!node || !node.workorder_id) {
|
||||
return;
|
||||
}
|
||||
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");
|
||||
}
|
||||
|
||||
// ----- Helpers -----------------------------------------------------------
|
||||
// ---- Helpers ------------------------------------------------------------
|
||||
|
||||
getProgressPct(node) {
|
||||
if (!node.qty_total || node.qty_total === 0) {
|
||||
return 0;
|
||||
/** Return the css class chain for a node card (state + node_type). */
|
||||
getCardClass(node) {
|
||||
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) {
|
||||
const pct = this.getProgressPct(node);
|
||||
if (pct >= 100) {
|
||||
return "o_fp_tree_progress_done";
|
||||
}
|
||||
if (pct > 0) {
|
||||
return "o_fp_tree_progress_active";
|
||||
}
|
||||
return "o_fp_tree_progress_empty";
|
||||
/** A node should pulse-highlight if it is the live position of the MO. */
|
||||
isHighlight(node) {
|
||||
return node.state === "ready"
|
||||
|| node.state === "progress"
|
||||
|| node.state === "waiting";
|
||||
}
|
||||
|
||||
getNodeStateLabel(state) {
|
||||
const map = {
|
||||
pending: "Pending",
|
||||
waiting: "Waiting",
|
||||
ready: "Ready",
|
||||
progress: "In Progress",
|
||||
done: "Done",
|
||||
cancel: "Cancelled",
|
||||
getKindBadge(node) {
|
||||
if (!node.wo_kind) return null;
|
||||
return {
|
||||
cls: `o_fp_pt_kind o_fp_pt_kind_${node.wo_kind}`,
|
||||
label: node.wo_kind_label || node.wo_kind,
|
||||
};
|
||||
return map[state] || state || "—";
|
||||
}
|
||||
|
||||
getNodeStateClass(state) {
|
||||
switch (state) {
|
||||
case "done":
|
||||
return "o_fp_tree_state_done";
|
||||
case "progress":
|
||||
return "o_fp_tree_state_progress";
|
||||
case "ready":
|
||||
return "o_fp_tree_state_ready";
|
||||
case "cancel":
|
||||
return "o_fp_tree_state_cancel";
|
||||
default:
|
||||
return "o_fp_tree_state_pending";
|
||||
qtyLabel(node) {
|
||||
if (!node.qty_total) return "";
|
||||
return `${node.qty_done}/${node.qty_total}`;
|
||||
}
|
||||
|
||||
nodeIcon(node) {
|
||||
if (node.icon) return node.icon;
|
||||
switch (node.node_type) {
|
||||
case "recipe": return "fa-cubes";
|
||||
case "sub_process": return "fa-folder";
|
||||
case "operation": return "fa-cog";
|
||||
case "step": return "fa-circle-o";
|
||||
case "state": return "fa-circle";
|
||||
default: return "fa-square";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,298 +1,398 @@
|
||||
// =============================================================================
|
||||
// Fusion Plating — Process Tree View
|
||||
// Copyright 2026 Nexa Systems Inc.
|
||||
// License OPL-1 (Odoo Proprietary License v1.0)
|
||||
// Fusion Plating — Process Tree (horizontal hierarchical, v3, 2026-04)
|
||||
// Copyright 2026 Nexa Systems Inc. · License OPL-1
|
||||
//
|
||||
// THEME AWARENESS
|
||||
// ---------------
|
||||
// All colours come from CSS custom properties (Bootstrap / Odoo tokens) so
|
||||
// the tree view renders correctly in BOTH light and dark mode.
|
||||
// Hierarchical bracket tree:
|
||||
//
|
||||
// background: var(--bs-body-bg)
|
||||
// surface: var(--o-view-background-color)
|
||||
// foreground: var(--bs-body-color)
|
||||
// muted text: var(--bs-secondary-color)
|
||||
// border: var(--bs-border-color)
|
||||
// primary: var(--o-action)
|
||||
// [Recipe]──┬──[Sub-Process]──┬──[Operation]──┬──[Ready for X]
|
||||
// │ │ └──[X]
|
||||
// │ └──[Operation]
|
||||
// ├──[Operation]
|
||||
// └──[Operation]
|
||||
//
|
||||
// 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;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
background: var(--o-view-background-color, var(--bs-body-bg));
|
||||
padding: 0;
|
||||
}
|
||||
gap: $fp-space-3;
|
||||
|
||||
// ---- Header -----------------------------------------------------------------
|
||||
@media (max-width: 600px) { padding: $fp-space-3; gap: $fp-space-3; }
|
||||
|
||||
.o_fp_pt_header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
background: var(--bs-body-bg);
|
||||
border-bottom: 1px solid var(--bs-border-color);
|
||||
box-shadow: 0 1px 3px color-mix(in srgb, var(--bs-body-color) 6%, transparent);
|
||||
|
||||
.o_fp_pt_header_left {
|
||||
// -------------------------------------------------------------------------
|
||||
// Header (compact strip)
|
||||
// -------------------------------------------------------------------------
|
||||
.o_fp_pt_header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $fp-space-3;
|
||||
flex-wrap: wrap;
|
||||
padding: $fp-space-3 $fp-space-4;
|
||||
background-color: $fp-card;
|
||||
border-radius: $fp-radius-md;
|
||||
box-shadow: $fp-elev-1;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.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 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--bs-body-color);
|
||||
font-size: $fp-text-md;
|
||||
font-weight: $fp-weight-bold;
|
||||
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 {
|
||||
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);
|
||||
margin-top: 2px;
|
||||
font-size: $fp-text-xs;
|
||||
color: $fp-ink-mute;
|
||||
display: flex; flex-wrap: wrap; align-items: center; gap: 2px;
|
||||
.fa { margin-right: 2px; opacity: 0.7; }
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// ---- State badges inside tree -----------------------------------------------
|
||||
|
||||
.o_fp_pt_node_state {
|
||||
.badge {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
padding: 3px 8px;
|
||||
// -------------------------------------------------------------------------
|
||||
// Empty / loading
|
||||
// -------------------------------------------------------------------------
|
||||
.o_fp_pt_empty {
|
||||
text-align: center;
|
||||
padding: $fp-space-7 $fp-space-5;
|
||||
color: $fp-ink-mute;
|
||||
background-color: $fp-card;
|
||||
border-radius: $fp-radius-md;
|
||||
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_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 -----------------------------------------------------------
|
||||
|
||||
.o_fp_pt_bar {
|
||||
height: 8px;
|
||||
background: var(--bs-tertiary-bg);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
&.o_fp_pt_bar_sm {
|
||||
height: 6px;
|
||||
// -------------------------------------------------------------------------
|
||||
// Tree canvas — horizontally scrollable
|
||||
// -------------------------------------------------------------------------
|
||||
.o_fp_pt_canvas {
|
||||
padding: $fp-space-3 0;
|
||||
min-width: max-content; // let cards push the canvas wider for scroll
|
||||
}
|
||||
|
||||
.o_fp_pt_bar_fill {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
&.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 {
|
||||
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 {
|
||||
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.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
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">
|
||||
|
||||
<t t-name="fusion_plating_shopfloor.ProcessTree">
|
||||
<div class="o_fp_process_tree">
|
||||
<!-- =====================================================================
|
||||
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">
|
||||
|
||||
<!-- ========== HEADER ========== -->
|
||||
<div class="o_fp_pt_header">
|
||||
<div class="o_fp_pt_header_left">
|
||||
<button class="btn btn-outline-secondary btn-sm me-3"
|
||||
t-on-click="onBackToOverview"
|
||||
title="Back to Plant Overview">
|
||||
<i class="fa fa-arrow-left me-1"/> Overview
|
||||
</button>
|
||||
<div class="o_fp_pt_title_block">
|
||||
<h3 class="o_fp_pt_title mb-0">
|
||||
<i class="fa fa-sitemap me-2"/>
|
||||
Process Tree
|
||||
</h3>
|
||||
<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>
|
||||
<!-- 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>
|
||||
<div class="o_fp_pt_header_right" t-if="state.moState">
|
||||
<span class="badge bg-secondary">
|
||||
MO: <t t-esc="state.moState"/>
|
||||
</span>
|
||||
|
||||
<!-- 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">
|
||||
<div class="o_fp_process_tree o_fp_pt_v3">
|
||||
|
||||
<!-- ========== HEADER ========== -->
|
||||
<div class="o_fp_pt_header">
|
||||
<button class="o_fp_pt_back"
|
||||
t-on-click="onBack"
|
||||
t-att-title="backLabel">
|
||||
<i class="fa fa-arrow-left me-2"/>
|
||||
<t t-esc="backLabel"/>
|
||||
</button>
|
||||
<div class="o_fp_pt_title_block">
|
||||
<h2 class="o_fp_pt_title mb-0">
|
||||
<i class="fa fa-sitemap me-2"/>Process
|
||||
<span t-if="state.productionName" class="o_fp_pt_mo_name">
|
||||
· <t t-esc="state.productionName"/>
|
||||
</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>
|
||||
|
||||
<!-- ========== 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"/>
|
||||
<p class="mt-2 text-muted">Loading process tree...</p>
|
||||
<p class="mt-2 text-muted small">Loading process...</p>
|
||||
</div>
|
||||
|
||||
<!-- ========== NO PRODUCTION ID ========== -->
|
||||
<div class="o_fp_pt_empty text-center py-5"
|
||||
<!-- ========== EMPTY ========== -->
|
||||
<div class="o_fp_pt_empty"
|
||||
t-if="!state.loading and !productionId">
|
||||
<i class="fa fa-exclamation-triangle fa-3x text-warning"/>
|
||||
<p class="mt-3">No manufacturing order selected.
|
||||
Open this view from a production order to see its routing tree.</p>
|
||||
<i class="fa fa-exclamation-triangle"/>
|
||||
<div>No manufacturing order selected.</div>
|
||||
</div>
|
||||
|
||||
<!-- ========== EMPTY TREE ========== -->
|
||||
<div class="o_fp_pt_empty text-center py-5"
|
||||
t-if="!state.loading and productionId and !state.nodes.length">
|
||||
<i class="fa fa-sitemap fa-3x text-muted"/>
|
||||
<p class="mt-3 text-muted">No routing steps found for this order.</p>
|
||||
<div class="o_fp_pt_empty"
|
||||
t-if="!state.loading and productionId and !state.root">
|
||||
<i class="fa fa-sitemap"/>
|
||||
<div>No process steps for this order.</div>
|
||||
</div>
|
||||
|
||||
<!-- ========== TREE ========== -->
|
||||
<div class="o_fp_pt_tree" t-if="state.nodes.length">
|
||||
<t t-foreach="state.nodes" t-as="node" t-key="node.id">
|
||||
<div class="o_fp_pt_node_wrapper">
|
||||
|
||||
<!-- 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>
|
||||
<div class="o_fp_pt_canvas" t-if="state.root">
|
||||
<t t-call="fusion_plating_shopfloor.ProcessNode">
|
||||
<t t-set="node" t-value="state.root"/>
|
||||
</t>
|
||||
</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