Compare commits
14 Commits
3cc93b8783
...
a8eacc94bc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8eacc94bc | ||
|
|
b79e3d5c2e | ||
|
|
be611876ad | ||
|
|
d07159b9b5 | ||
|
|
5d89e04f82 | ||
|
|
b6d101c9a2 | ||
|
|
0fe8a71c05 | ||
|
|
8b2cbd9085 | ||
|
|
d60a75a391 | ||
|
|
c30a61c93f | ||
|
|
f4c6dca171 | ||
|
|
87a649b63d | ||
|
|
7d8f30627f | ||
|
|
4fde4c7bd1 |
BIN
at_accounting-18.0.1.7.zip
Normal file
BIN
at_accounting-18.0.1.7.zip
Normal file
Binary file not shown.
668
docs/workflow-explorer/index.html
Normal file
668
docs/workflow-explorer/index.html
Normal file
@@ -0,0 +1,668 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Fusion Claims — Workflow Explorer</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.9.1/dist/mermaid.min.js"></script>
|
||||
<script src="./workflows.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0f1115;
|
||||
--panel: #161a22;
|
||||
--panel-2: #1d2330;
|
||||
--border: #2a3040;
|
||||
--text: #e6e9ef;
|
||||
--muted: #8a93a8;
|
||||
--accent: #4f8cff;
|
||||
--accent-soft: rgba(79,140,255,.15);
|
||||
--ok: #3fbf7f;
|
||||
--warn: #f4b400;
|
||||
--err: #ff5c6c;
|
||||
--entry: #9b5de5;
|
||||
--terminal: #5f6b80;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; background: var(--bg); color: var(--text); font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; height: 100%; }
|
||||
body { display: flex; min-height: 100vh; }
|
||||
aside {
|
||||
width: 280px; flex-shrink: 0;
|
||||
background: var(--panel);
|
||||
border-right: 1px solid var(--border);
|
||||
padding: 20px 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
aside h1 {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
margin: 0 20px 12px;
|
||||
}
|
||||
aside .subtitle {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
margin: 0 20px 20px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.wf-list { list-style: none; padding: 0; margin: 0; }
|
||||
.wf-list li {
|
||||
padding: 12px 20px;
|
||||
cursor: pointer;
|
||||
border-left: 3px solid transparent;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: background .15s;
|
||||
}
|
||||
.wf-list li:hover { background: var(--panel-2); }
|
||||
.wf-list li.active {
|
||||
background: var(--accent-soft);
|
||||
border-left-color: var(--accent);
|
||||
}
|
||||
.wf-list li .name { font-weight: 500; }
|
||||
.wf-list li .gap-badge {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
background: var(--err);
|
||||
color: #fff;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.wf-list li .gap-badge.zero { background: var(--ok); }
|
||||
main {
|
||||
flex: 1;
|
||||
padding: 32px 40px;
|
||||
overflow-y: auto;
|
||||
max-width: calc(100vw - 280px);
|
||||
}
|
||||
h2 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.field-name {
|
||||
color: var(--muted);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.stat {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
.stat .label {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .06em;
|
||||
color: var(--muted);
|
||||
}
|
||||
.stat .value {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.stat.ok .value { color: var(--ok); }
|
||||
.stat.err .value { color: var(--err); }
|
||||
.stat.warn .value { color: var(--warn); }
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.tabs button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--muted);
|
||||
font: inherit;
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.tabs button:hover { color: var(--text); }
|
||||
.tabs button.active {
|
||||
color: var(--accent);
|
||||
border-bottom-color: var(--accent);
|
||||
}
|
||||
.tab-panel { display: none; }
|
||||
.tab-panel.active { display: block; }
|
||||
|
||||
.mermaid-wrap {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.mermaid-wrap svg { max-width: 100%; height: auto; }
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
th, td {
|
||||
padding: 10px 14px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 13px;
|
||||
}
|
||||
th {
|
||||
background: var(--panel-2);
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .05em;
|
||||
color: var(--muted);
|
||||
}
|
||||
tbody tr:last-child td { border-bottom: none; }
|
||||
tbody tr:hover { background: var(--panel-2); }
|
||||
tbody tr.has-issue td:first-child { border-left: 3px solid var(--err); }
|
||||
code.state-key {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 12px;
|
||||
background: var(--panel-2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
color: var(--accent);
|
||||
}
|
||||
.pill {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .03em;
|
||||
}
|
||||
.pill.ok { background: rgba(63,191,127,.2); color: var(--ok); }
|
||||
.pill.warn { background: rgba(244,180,0,.2); color: var(--warn); }
|
||||
.pill.err { background: rgba(255,92,108,.2); color: var(--err); }
|
||||
.pill.entry { background: rgba(155,93,229,.2); color: var(--entry); }
|
||||
.pill.terminal { background: rgba(95,107,128,.35); color: #c2c9d9; }
|
||||
.count {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.tr-list { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 8px; }
|
||||
.tr-row {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 30px 200px 1fr 140px;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 13px;
|
||||
}
|
||||
.tr-row:last-child { border-bottom: none; }
|
||||
.tr-row:hover { background: var(--panel-2); }
|
||||
.tr-arrow { color: var(--muted); text-align: center; }
|
||||
.tr-trigger { color: var(--muted); font-family: ui-monospace, monospace; font-size: 12px; word-break: break-all; }
|
||||
.tr-trigger .file { color: #555; display: block; margin-top: 2px; }
|
||||
.tr-kind {
|
||||
text-align: right;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .04em;
|
||||
}
|
||||
.kind-wizard { color: #4f8cff; }
|
||||
.kind-action_method { color: #9b5de5; }
|
||||
.kind-cron { color: #f4b400; }
|
||||
.kind-auto_write { color: #3fbf7f; }
|
||||
.kind-ui_button { color: #ff5c6c; }
|
||||
|
||||
.gaps {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 4px solid var(--err);
|
||||
border-radius: 8px;
|
||||
padding: 20px 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.gaps.zero { border-left-color: var(--ok); }
|
||||
.gaps h3 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.gaps p { color: var(--muted); margin: 0; }
|
||||
.gaps ul { margin: 0; padding-left: 20px; }
|
||||
.gaps li { padding: 4px 0; font-size: 13px; }
|
||||
.gaps li code {
|
||||
font-family: ui-monospace, monospace;
|
||||
background: var(--panel-2);
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
color: var(--accent);
|
||||
font-size: 12px;
|
||||
}
|
||||
.gap-kind {
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
margin-right: 6px;
|
||||
letter-spacing: .05em;
|
||||
}
|
||||
.gap-kind.unreachable { background: rgba(244,180,0,.25); color: var(--warn); }
|
||||
.gap-kind.dead-end { background: rgba(255,92,108,.25); color: var(--err); }
|
||||
.gap-kind.missing-path { background: rgba(79,140,255,.25); color: var(--accent); }
|
||||
.gap-kind.hold-loss { background: rgba(155,93,229,.25); color: var(--entry); }
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.legend span { display: inline-flex; align-items: center; gap: 6px; }
|
||||
.legend .swatch { width: 10px; height: 10px; border-radius: 2px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<aside>
|
||||
<h1>Fusion Claims</h1>
|
||||
<p class="subtitle">Workflow Explorer — 5 parallel state machines on <code style="color:var(--accent)">sale.order</code>. Click a workflow to inspect.</p>
|
||||
<ul class="wf-list" id="wf-list"></ul>
|
||||
</aside>
|
||||
<main id="main">
|
||||
<div id="wf-content"></div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// ============================================================
|
||||
// DOM helpers — no innerHTML, all createElement / textContent
|
||||
// ============================================================
|
||||
function el(tag, opts, children) {
|
||||
const node = document.createElement(tag);
|
||||
if (opts) {
|
||||
if (opts.class) node.className = opts.class;
|
||||
if (opts.id) node.id = opts.id;
|
||||
if (opts.text != null) node.textContent = opts.text;
|
||||
if (opts.style) Object.assign(node.style, opts.style);
|
||||
if (opts.data) Object.entries(opts.data).forEach(([k,v]) => node.dataset[k] = v);
|
||||
if (opts.on) Object.entries(opts.on).forEach(([evt,fn]) => node.addEventListener(evt, fn));
|
||||
}
|
||||
if (children) {
|
||||
(Array.isArray(children) ? children : [children]).forEach(c => {
|
||||
if (c == null) return;
|
||||
node.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
|
||||
});
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
// Render a string that may contain <code>...</code> spans safely.
|
||||
// Splits on our own markers and builds real DOM nodes.
|
||||
function renderSafeInline(parent, text) {
|
||||
// Only recognise <code>...</code> — everything else is literal text.
|
||||
const parts = text.split(/(<code>[^<]*<\/code>)/);
|
||||
parts.forEach(part => {
|
||||
if (part.startsWith('<code>') && part.endsWith('</code>')) {
|
||||
const codeText = part.slice(6, -7);
|
||||
parent.appendChild(el('code', {text: codeText}));
|
||||
} else if (part) {
|
||||
parent.appendChild(document.createTextNode(part));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Gap analysis
|
||||
// ============================================================
|
||||
function analyseWorkflow(wf) {
|
||||
const stateKeys = wf.states.map(s => s.key);
|
||||
const inbound = new Map();
|
||||
const outbound = new Map();
|
||||
stateKeys.forEach(k => { inbound.set(k, []); outbound.set(k, []); });
|
||||
|
||||
wf.transitions.forEach(t => {
|
||||
if (t.to && inbound.has(t.to)) inbound.get(t.to).push(t);
|
||||
if (t.from && t.from !== '*' && outbound.has(t.from)) outbound.get(t.from).push(t);
|
||||
});
|
||||
|
||||
const wildcardInTo = new Set();
|
||||
wf.transitions.forEach(t => { if (t.from === '*') wildcardInTo.add(t.to); });
|
||||
|
||||
const terminal = new Set(wf.terminal || []);
|
||||
const gaps = [];
|
||||
const stateStatus = {};
|
||||
|
||||
stateKeys.forEach(key => {
|
||||
const label = wf.states.find(s => s.key === key).label;
|
||||
const isDefault = key === wf.default;
|
||||
const isTerminal = terminal.has(key);
|
||||
const hasInbound = inbound.get(key).length > 0 || wildcardInTo.has(key);
|
||||
const hasOutbound = outbound.get(key).length > 0 || wf.transitions.some(t => t.from === key);
|
||||
|
||||
let status = 'ok';
|
||||
const issues = [];
|
||||
|
||||
if (!isDefault && !hasInbound) {
|
||||
status = 'err';
|
||||
issues.push({kind: 'unreachable', msg: 'No code path sets this state. It will never be reached via normal workflow — only via manual DB edit or stale ORM context.'});
|
||||
}
|
||||
if (!isTerminal && !hasOutbound && !isDefault) {
|
||||
status = 'err';
|
||||
issues.push({kind: 'dead-end', msg: 'Once an order lands here, there is no action method or wizard to transition it out. Users will have to edit the record directly.'});
|
||||
}
|
||||
|
||||
stateStatus[key] = {
|
||||
status, issues, isDefault, isTerminal,
|
||||
inbound: inbound.get(key),
|
||||
outbound: wf.transitions.filter(t => t.from === key)
|
||||
};
|
||||
issues.forEach(iss => gaps.push({state: key, label, ...iss}));
|
||||
});
|
||||
|
||||
// Workflow-specific heuristics
|
||||
if (wf.field === 'x_fc_adp_application_status') {
|
||||
if (!wf.transitions.some(t => t.to === 'rejected')) {
|
||||
gaps.push({kind: 'missing-path', state: 'rejected', label: 'Rejected by ADP',
|
||||
msg: 'No transition writes <code>rejected</code>. The state is declared but nothing reaches it. An ADP rejection has nowhere to land.'});
|
||||
}
|
||||
if (!wf.transitions.some(t => t.from === 'rejected')) {
|
||||
gaps.push({kind: 'missing-path', state: 'rejected', label: 'Rejected by ADP',
|
||||
msg: 'No <code>action_resubmit_from_rejected</code> exists (only <code>action_resubmit_from_withdrawn</code>). A rejected application cannot be brought back into the workflow.'});
|
||||
}
|
||||
if (!wf.transitions.some(t => t.to === 'denied')) {
|
||||
gaps.push({kind: 'missing-path', state: 'denied', label: 'Application Denied',
|
||||
msg: 'No code path sets <code>denied</code>. Declared as a selection value but has no action method to assign it.'});
|
||||
}
|
||||
if (!wf.transitions.some(t => t.to === 'expired')) {
|
||||
gaps.push({kind: 'missing-path', state: 'expired', label: 'Application Expired',
|
||||
msg: 'No cron or method sets <code>expired</code>. Declared but unreachable — the ADP expiry logic was never implemented.'});
|
||||
}
|
||||
if (!wf.transitions.some(t => t.to === 'cancelled')) {
|
||||
gaps.push({kind: 'missing-path', state: 'cancelled', label: 'Cancelled',
|
||||
msg: 'No action method writes <code>cancelled</code> on the ADP workflow.'});
|
||||
}
|
||||
if (!wf.transitions.some(t => t.to === 'withdrawn')) {
|
||||
gaps.push({kind: 'missing-path', state: 'withdrawn', label: 'Withdrawn',
|
||||
msg: '<code>action_resubmit_from_withdrawn</code> exists (line 3667) but no method WRITES <code>withdrawn</code> in the first place. Dead end on entry.'});
|
||||
}
|
||||
if (!wf.transitions.some(t => t.to === 'needs_correction')) {
|
||||
gaps.push({kind: 'missing-path', state: 'needs_correction', label: 'Needs Correction',
|
||||
msg: 'The write() override at line 6017 handles <code>needs_correction</code> document-clearing logic, but no code path sets the state TO <code>needs_correction</code>. Only reachable via manual edit.'});
|
||||
}
|
||||
}
|
||||
|
||||
if (wf.field === 'x_fc_mod_status') {
|
||||
if (!wf.transitions.some(t => t.from === 'funding_denied')) {
|
||||
gaps.push({kind: 'dead-end', state: 'funding_denied', label: 'Denied',
|
||||
msg: 'No way to revive a denied MOD case. No resubmit, no cancellation path. Once denied, the order is stuck unless someone edits <code>x_fc_mod_status</code> directly.'});
|
||||
}
|
||||
}
|
||||
|
||||
if (['x_fc_sa_status', 'x_fc_odsp_std_status', 'x_fc_ow_status'].includes(wf.field)) {
|
||||
const resume = wf.transitions.find(t => t.from === 'on_hold');
|
||||
if (resume && resume.to === 'quotation') {
|
||||
gaps.push({kind: 'hold-loss', state: 'on_hold', label: 'On Hold',
|
||||
msg: '<code>action_odsp_resume</code> always resumes to <code>quotation</code>, losing all progress regardless of where the order was put on hold. An order held at <code>ready_delivery</code> is reset to the start.'});
|
||||
}
|
||||
if (!wf.transitions.some(t => t.from === 'denied')) {
|
||||
gaps.push({kind: 'dead-end', state: 'denied', label: 'Denied',
|
||||
msg: 'No path out of <code>denied</code>. Once set, the case is stuck.'});
|
||||
}
|
||||
}
|
||||
|
||||
return {gaps, stateStatus};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Mermaid flowchart builder — produces plain text, Mermaid parses it.
|
||||
// ============================================================
|
||||
function buildMermaid(wf, stateStatus) {
|
||||
const lines = ['flowchart LR'];
|
||||
wf.states.forEach(s => {
|
||||
const st = stateStatus[s.key];
|
||||
const safeLabel = s.label.replace(/"/g, '"');
|
||||
const shape = st.isTerminal ? `(("${safeLabel}"))` :
|
||||
st.isDefault ? `(["${safeLabel}"])` :
|
||||
`["${safeLabel}"]`;
|
||||
lines.push(` ${s.key}${shape}`);
|
||||
});
|
||||
const seen = new Set();
|
||||
wf.transitions.forEach(t => {
|
||||
if (t.from === '*') return;
|
||||
const key = `${t.from}->${t.to}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
lines.push(` ${t.from} --> ${t.to}`);
|
||||
});
|
||||
wf.states.forEach(s => {
|
||||
const st = stateStatus[s.key];
|
||||
let cls = 'ok';
|
||||
if (st.status === 'err') {
|
||||
if (st.issues.some(i => i.kind === 'unreachable')) cls = 'unreachable';
|
||||
else cls = 'deadend';
|
||||
} else if (st.isDefault) cls = 'entry';
|
||||
else if (st.isTerminal) cls = 'terminal';
|
||||
lines.push(` class ${s.key} ${cls}`);
|
||||
});
|
||||
lines.push(' classDef ok fill:#1d2330,stroke:#3fbf7f,color:#e6e9ef,stroke-width:1.5px');
|
||||
lines.push(' classDef entry fill:#2b1d40,stroke:#9b5de5,color:#e6e9ef,stroke-width:2.5px');
|
||||
lines.push(' classDef terminal fill:#1a2030,stroke:#5f6b80,color:#c2c9d9,stroke-width:1.5px');
|
||||
lines.push(' classDef unreachable fill:#2a2418,stroke:#f4b400,color:#f4b400,stroke-width:2px,stroke-dasharray:5 3');
|
||||
lines.push(' classDef deadend fill:#2a1820,stroke:#ff5c6c,color:#ff5c6c,stroke-width:2px');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Renderer — DOM-based, no innerHTML
|
||||
// ============================================================
|
||||
const wfData = window.WORKFLOWS_DATA;
|
||||
const wfKeys = Object.keys(wfData);
|
||||
let activeWf = wfKeys[0];
|
||||
let activeTab = 'flow';
|
||||
|
||||
function renderSidebar() {
|
||||
const list = document.getElementById('wf-list');
|
||||
while (list.firstChild) list.removeChild(list.firstChild);
|
||||
wfKeys.forEach(k => {
|
||||
const wf = wfData[k];
|
||||
const {gaps} = analyseWorkflow(wf);
|
||||
const li = el('li', {
|
||||
class: k === activeWf ? 'active' : '',
|
||||
on: {click: () => { activeWf = k; activeTab = 'flow'; renderSidebar(); renderContent(); }}
|
||||
}, [
|
||||
el('span', {class: 'name', text: wf.label}),
|
||||
el('span', {class: 'gap-badge' + (gaps.length === 0 ? ' zero' : ''), text: String(gaps.length)})
|
||||
]);
|
||||
list.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
function makeStat(label, value, cls) {
|
||||
return el('div', {class: 'stat' + (cls ? ' ' + cls : '')}, [
|
||||
el('div', {class: 'label', text: label}),
|
||||
el('div', {class: 'value', text: String(value)})
|
||||
]);
|
||||
}
|
||||
|
||||
function makeGapListItem(g) {
|
||||
const li = el('li');
|
||||
const kind = el('span', {class: 'gap-kind ' + g.kind, text: g.kind.replace('-', ' ')});
|
||||
li.appendChild(kind);
|
||||
const strong = el('strong', {text: g.label});
|
||||
li.appendChild(strong);
|
||||
li.appendChild(document.createTextNode(' — '));
|
||||
renderSafeInline(li, g.msg);
|
||||
return li;
|
||||
}
|
||||
|
||||
function renderContent() {
|
||||
const wf = wfData[activeWf];
|
||||
const {gaps, stateStatus} = analyseWorkflow(wf);
|
||||
const container = document.getElementById('wf-content');
|
||||
while (container.firstChild) container.removeChild(container.firstChild);
|
||||
|
||||
const wizardCount = wf.transitions.filter(t => t.kind === 'wizard').length;
|
||||
const cronCount = wf.transitions.filter(t => t.kind === 'cron').length;
|
||||
const autoCount = wf.transitions.filter(t => t.kind === 'auto_write').length;
|
||||
|
||||
container.appendChild(el('h2', {text: wf.label}));
|
||||
const fn = el('div', {class: 'field-name'});
|
||||
fn.appendChild(document.createTextNode(wf.field + ' · default: '));
|
||||
fn.appendChild(el('code', {text: wf.default}));
|
||||
container.appendChild(fn);
|
||||
|
||||
const stats = el('div', {class: 'stats'}, [
|
||||
makeStat('States', wf.states.length),
|
||||
makeStat('Transitions', wf.transitions.length),
|
||||
makeStat('Gaps', gaps.length, gaps.length === 0 ? 'ok' : 'err'),
|
||||
makeStat('Wizards', wizardCount),
|
||||
makeStat('Crons / Auto', cronCount + autoCount)
|
||||
]);
|
||||
container.appendChild(stats);
|
||||
|
||||
// Gaps panel
|
||||
const gapsBox = el('div', {class: 'gaps' + (gaps.length === 0 ? ' zero' : '')});
|
||||
gapsBox.appendChild(el('h3', {text: gaps.length === 0
|
||||
? '\u2713 No gaps detected'
|
||||
: '\u26A0 ' + gaps.length + ' gap' + (gaps.length === 1 ? '' : 's') + ' detected'}));
|
||||
if (gaps.length === 0) {
|
||||
gapsBox.appendChild(el('p', {text: 'This workflow has full coverage: every declared state is reachable, every non-terminal state has an exit, and all transitions are backed by code paths.'}));
|
||||
} else {
|
||||
const ul = el('ul');
|
||||
gaps.forEach(g => ul.appendChild(makeGapListItem(g)));
|
||||
gapsBox.appendChild(ul);
|
||||
}
|
||||
container.appendChild(gapsBox);
|
||||
|
||||
// Tabs
|
||||
const tabs = el('div', {class: 'tabs'});
|
||||
const tabDefs = [
|
||||
{key: 'flow', label: 'Flowchart'},
|
||||
{key: 'states', label: 'States (' + wf.states.length + ')'},
|
||||
{key: 'transitions', label: 'Transitions (' + wf.transitions.length + ')'}
|
||||
];
|
||||
tabDefs.forEach(t => {
|
||||
tabs.appendChild(el('button', {
|
||||
class: activeTab === t.key ? 'active' : '',
|
||||
text: t.label,
|
||||
on: {click: () => { activeTab = t.key; renderContent(); }}
|
||||
}));
|
||||
});
|
||||
container.appendChild(tabs);
|
||||
|
||||
// Flow tab
|
||||
if (activeTab === 'flow') {
|
||||
const legend = el('div', {class: 'legend'}, [
|
||||
el('span', null, [el('span', {class: 'swatch', style: {background: '#9b5de5'}}), 'Entry state']),
|
||||
el('span', null, [el('span', {class: 'swatch', style: {background: '#3fbf7f'}}), 'Healthy']),
|
||||
el('span', null, [el('span', {class: 'swatch', style: {background: '#f4b400'}}), 'Unreachable']),
|
||||
el('span', null, [el('span', {class: 'swatch', style: {background: '#ff5c6c'}}), 'Dead-end']),
|
||||
el('span', null, [el('span', {class: 'swatch', style: {background: '#5f6b80'}}), 'Terminal'])
|
||||
]);
|
||||
container.appendChild(legend);
|
||||
const wrap = el('div', {class: 'mermaid-wrap'});
|
||||
const mm = el('div', {class: 'mermaid', id: 'mermaid-' + activeWf});
|
||||
mm.textContent = buildMermaid(wf, stateStatus);
|
||||
wrap.appendChild(mm);
|
||||
container.appendChild(wrap);
|
||||
|
||||
// Render mermaid async
|
||||
mermaid.initialize({startOnLoad: false, theme: 'base', securityLevel: 'strict', themeVariables: {
|
||||
background: '#161a22', primaryColor: '#1d2330', primaryTextColor: '#e6e9ef',
|
||||
primaryBorderColor: '#3fbf7f', lineColor: '#4f8cff'
|
||||
}});
|
||||
const src = mm.textContent;
|
||||
const renderId = 'mm-svg-' + activeWf + '-' + Date.now();
|
||||
mermaid.render(renderId, src).then(result => {
|
||||
while (mm.firstChild) mm.removeChild(mm.firstChild);
|
||||
// mermaid.render returns an SVG string — parse via DOMParser, no innerHTML
|
||||
const doc = new DOMParser().parseFromString(result.svg, 'image/svg+xml');
|
||||
const svgNode = doc.documentElement;
|
||||
mm.appendChild(document.importNode(svgNode, true));
|
||||
}).catch(err => {
|
||||
while (mm.firstChild) mm.removeChild(mm.firstChild);
|
||||
const pre = el('pre', {style: {color: 'var(--err)', whiteSpace: 'pre-wrap'}});
|
||||
pre.textContent = 'Mermaid error: ' + err.message + '\n\n' + src;
|
||||
mm.appendChild(pre);
|
||||
});
|
||||
}
|
||||
|
||||
// States tab
|
||||
if (activeTab === 'states') {
|
||||
const table = el('table');
|
||||
const thead = el('thead');
|
||||
const headRow = el('tr');
|
||||
['State', 'Key', 'Status', 'In', 'Out'].forEach(h => headRow.appendChild(el('th', {text: h})));
|
||||
thead.appendChild(headRow);
|
||||
table.appendChild(thead);
|
||||
const tbody = el('tbody');
|
||||
wf.states.forEach(s => {
|
||||
const st = stateStatus[s.key];
|
||||
let pillClass = 'ok', pillLabel = 'Healthy';
|
||||
if (st.isDefault) { pillClass = 'entry'; pillLabel = 'Entry'; }
|
||||
else if (st.isTerminal) { pillClass = 'terminal'; pillLabel = 'Terminal'; }
|
||||
if (st.status === 'err') {
|
||||
if (st.issues.some(i => i.kind === 'unreachable')) { pillClass = 'warn'; pillLabel = 'Unreachable'; }
|
||||
else { pillClass = 'err'; pillLabel = 'Dead-end'; }
|
||||
}
|
||||
const tr = el('tr', {class: st.status === 'err' ? 'has-issue' : ''});
|
||||
tr.appendChild(el('td', null, [el('strong', {text: s.label})]));
|
||||
tr.appendChild(el('td', null, [el('code', {class: 'state-key', text: s.key})]));
|
||||
tr.appendChild(el('td', null, [el('span', {class: 'pill ' + pillClass, text: pillLabel})]));
|
||||
tr.appendChild(el('td', {class: 'count', text: String(st.inbound.length)}));
|
||||
tr.appendChild(el('td', {class: 'count', text: String(st.outbound.length)}));
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
table.appendChild(tbody);
|
||||
container.appendChild(table);
|
||||
}
|
||||
|
||||
// Transitions tab
|
||||
if (activeTab === 'transitions') {
|
||||
const list = el('div', {class: 'tr-list'});
|
||||
wf.transitions.forEach(t => {
|
||||
const row = el('div', {class: 'tr-row'});
|
||||
row.appendChild(el('div', null, [el('code', {class: 'state-key', text: t.from})]));
|
||||
row.appendChild(el('div', {class: 'tr-arrow', text: '\u2192'}));
|
||||
row.appendChild(el('div', null, [el('code', {class: 'state-key', text: t.to})]));
|
||||
const trig = el('div', {class: 'tr-trigger'});
|
||||
trig.appendChild(document.createTextNode(t.trigger));
|
||||
const fileLine = el('span', {class: 'file', text: t.file + (t.line ? ':' + t.line : '')});
|
||||
trig.appendChild(fileLine);
|
||||
row.appendChild(trig);
|
||||
const kind = el('div', {class: 'tr-kind'});
|
||||
kind.appendChild(el('span', {class: 'kind-' + t.kind, text: t.kind.replace('_', ' ')}));
|
||||
row.appendChild(kind);
|
||||
list.appendChild(row);
|
||||
});
|
||||
container.appendChild(list);
|
||||
}
|
||||
}
|
||||
|
||||
renderSidebar();
|
||||
renderContent();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
197
docs/workflow-explorer/workflows.js
Normal file
197
docs/workflow-explorer/workflows.js
Normal file
@@ -0,0 +1,197 @@
|
||||
// Workflow data extracted from fusion_claims/models/sale_order.py and wizard/*.py
|
||||
// Generated 2026-04-08. If the code changes, regenerate this file.
|
||||
|
||||
window.WORKFLOWS_DATA = {
|
||||
"adp_application": {
|
||||
"field": "x_fc_adp_application_status",
|
||||
"label": "ADP Application",
|
||||
"default": "quotation",
|
||||
"terminal": ["case_closed", "cancelled"],
|
||||
"states": [
|
||||
{"key": "quotation", "label": "Quotation Stage"},
|
||||
{"key": "assessment_scheduled", "label": "Assessment Scheduled"},
|
||||
{"key": "assessment_completed", "label": "Assessment Completed"},
|
||||
{"key": "waiting_for_application", "label": "Waiting for Application"},
|
||||
{"key": "application_received", "label": "Application Received"},
|
||||
{"key": "ready_submission", "label": "Ready for Submission"},
|
||||
{"key": "submitted", "label": "Application Submitted"},
|
||||
{"key": "accepted", "label": "Accepted by ADP"},
|
||||
{"key": "rejected", "label": "Rejected by ADP"},
|
||||
{"key": "resubmitted", "label": "Application Resubmitted"},
|
||||
{"key": "needs_correction", "label": "Needs Correction"},
|
||||
{"key": "approved", "label": "Application Approved"},
|
||||
{"key": "approved_deduction", "label": "Approved with Deduction"},
|
||||
{"key": "ready_delivery", "label": "Ready for Delivery"},
|
||||
{"key": "denied", "label": "Application Denied"},
|
||||
{"key": "withdrawn", "label": "Application Withdrawn"},
|
||||
{"key": "ready_bill", "label": "Ready to Bill"},
|
||||
{"key": "billed", "label": "Billed to ADP"},
|
||||
{"key": "case_closed", "label": "Case Closed"},
|
||||
{"key": "on_hold", "label": "On Hold"},
|
||||
{"key": "cancelled", "label": "Cancelled"},
|
||||
{"key": "expired", "label": "Application Expired"}
|
||||
],
|
||||
"transitions": [
|
||||
{"from": "quotation", "to": "assessment_scheduled", "trigger": "schedule_assessment_wizard.action_schedule", "file": "wizard/schedule_assessment_wizard.py", "line": 118, "kind": "wizard"},
|
||||
{"from": "assessment_scheduled", "to": "assessment_completed", "trigger": "assessment_completed_wizard.action_confirm", "file": "wizard/assessment_completed_wizard.py", "line": 105, "kind": "wizard"},
|
||||
{"from": "assessment_completed", "to": "waiting_for_application", "trigger": "auto-transition on status_email write()", "file": "models/sale_order.py", "line": 6017, "kind": "auto_write"},
|
||||
{"from": "assessment_completed", "to": "application_received", "trigger": "application_received_wizard.action_confirm", "file": "wizard/application_received_wizard.py", "line": 136, "kind": "wizard"},
|
||||
{"from": "waiting_for_application", "to": "application_received", "trigger": "application_received_wizard.action_confirm", "file": "wizard/application_received_wizard.py", "line": 136, "kind": "wizard"},
|
||||
{"from": "application_received", "to": "ready_submission", "trigger": "ready_for_submission_wizard.action_confirm", "file": "wizard/ready_for_submission_wizard.py", "line": 159, "kind": "wizard"},
|
||||
{"from": "ready_submission", "to": "submitted", "trigger": "submission_verification_wizard.action_confirm_submission", "file": "wizard/submission_verification_wizard.py", "line": 288, "kind": "wizard"},
|
||||
{"from": "needs_correction", "to": "resubmitted", "trigger": "submission_verification_wizard.action_confirm_submission", "file": "wizard/submission_verification_wizard.py", "line": 288, "kind": "wizard"},
|
||||
{"from": "submitted", "to": "accepted", "trigger": "action_mark_accepted", "file": "models/sale_order.py", "line": 3563, "kind": "action_method"},
|
||||
{"from": "resubmitted", "to": "accepted", "trigger": "action_mark_accepted", "file": "models/sale_order.py", "line": 3563, "kind": "action_method"},
|
||||
{"from": "submitted", "to": "approved", "trigger": "device_approval_wizard.action_confirm", "file": "wizard/device_approval_wizard.py", "line": 290, "kind": "wizard"},
|
||||
{"from": "resubmitted", "to": "approved", "trigger": "device_approval_wizard.action_confirm", "file": "wizard/device_approval_wizard.py", "line": 290, "kind": "wizard"},
|
||||
{"from": "accepted", "to": "approved", "trigger": "device_approval_wizard.action_confirm", "file": "wizard/device_approval_wizard.py", "line": 290, "kind": "wizard"},
|
||||
{"from": "submitted", "to": "approved_deduction", "trigger": "device_approval_wizard.action_confirm", "file": "wizard/device_approval_wizard.py", "line": 290, "kind": "wizard"},
|
||||
{"from": "resubmitted", "to": "approved_deduction", "trigger": "device_approval_wizard.action_confirm", "file": "wizard/device_approval_wizard.py", "line": 290, "kind": "wizard"},
|
||||
{"from": "accepted", "to": "approved_deduction", "trigger": "device_approval_wizard.action_confirm", "file": "wizard/device_approval_wizard.py", "line": 290, "kind": "wizard"},
|
||||
{"from": "approved", "to": "ready_delivery", "trigger": "ready_for_delivery_wizard.action_confirm", "file": "wizard/ready_for_delivery_wizard.py", "line": 108, "kind": "wizard"},
|
||||
{"from": "approved_deduction", "to": "ready_delivery", "trigger": "ready_for_delivery_wizard.action_confirm", "file": "wizard/ready_for_delivery_wizard.py", "line": 108, "kind": "wizard"},
|
||||
{"from": "*", "to": "ready_delivery", "trigger": "technician_task complete", "file": "models/technician_task.py", "line": 228, "kind": "auto_write"},
|
||||
{"from": "ready_delivery", "to": "approved", "trigger": "technician_task cancel", "file": "models/technician_task.py", "line": 327, "kind": "auto_write"},
|
||||
{"from": "ready_delivery", "to": "ready_bill", "trigger": "ready_to_bill_wizard.action_confirm", "file": "wizard/ready_to_bill_wizard.py", "line": null, "kind": "wizard"},
|
||||
{"from": "ready_bill", "to": "billed", "trigger": "adp_export_wizard.action_export", "file": "wizard/adp_export_wizard.py", "line": null, "kind": "wizard"},
|
||||
{"from": "billed", "to": "case_closed", "trigger": "_cron_auto_close_billed_cases", "file": "models/sale_order.py", "line": 6852, "kind": "cron"},
|
||||
{"from": "withdrawn", "to": "ready_submission", "trigger": "action_resubmit_from_withdrawn", "file": "models/sale_order.py", "line": 3667, "kind": "action_method"}
|
||||
]
|
||||
},
|
||||
"mod": {
|
||||
"field": "x_fc_mod_status",
|
||||
"label": "March of Dimes",
|
||||
"default": "need_to_schedule",
|
||||
"terminal": ["case_closed", "cancelled"],
|
||||
"states": [
|
||||
{"key": "need_to_schedule", "label": "Schedule Assessment"},
|
||||
{"key": "assessment_scheduled", "label": "Assessment Booked"},
|
||||
{"key": "assessment_completed", "label": "Assessment Done"},
|
||||
{"key": "processing_drawings", "label": "Processing Drawing"},
|
||||
{"key": "quote_submitted", "label": "Quote Sent"},
|
||||
{"key": "awaiting_funding", "label": "Awaiting Funding"},
|
||||
{"key": "funding_approved", "label": "Approved"},
|
||||
{"key": "funding_denied", "label": "Denied"},
|
||||
{"key": "contract_received", "label": "PCA Received"},
|
||||
{"key": "in_production", "label": "In Production"},
|
||||
{"key": "project_complete", "label": "Complete"},
|
||||
{"key": "pod_submitted", "label": "POD Sent"},
|
||||
{"key": "case_closed", "label": "Closed"},
|
||||
{"key": "on_hold", "label": "On Hold"},
|
||||
{"key": "cancelled", "label": "Cancelled"}
|
||||
],
|
||||
"transitions": [
|
||||
{"from": "need_to_schedule", "to": "assessment_scheduled", "trigger": "action_mod_schedule_assessment", "file": "models/sale_order.py", "line": 7018, "kind": "action_method"},
|
||||
{"from": "assessment_scheduled", "to": "assessment_completed", "trigger": "action_mod_complete_assessment", "file": "models/sale_order.py", "line": 7025, "kind": "action_method"},
|
||||
{"from": "assessment_completed", "to": "processing_drawings", "trigger": "action_mod_processing_drawing", "file": "models/sale_order.py", "line": 7035, "kind": "action_method"},
|
||||
{"from": "processing_drawings", "to": "quote_submitted", "trigger": "send_to_mod_wizard.action_send (quote)", "file": "wizard/send_to_mod_wizard.py", "line": 203, "kind": "wizard"},
|
||||
{"from": "quote_submitted", "to": "awaiting_funding", "trigger": "mod_awaiting_funding_wizard.action_confirm", "file": "wizard/mod_awaiting_funding_wizard.py", "line": 34, "kind": "wizard"},
|
||||
{"from": "awaiting_funding", "to": "funding_approved", "trigger": "mod_funding_approved_wizard.action_confirm", "file": "wizard/mod_funding_approved_wizard.py", "line": 48, "kind": "wizard"},
|
||||
{"from": "awaiting_funding", "to": "funding_denied", "trigger": "action_mod_funding_denied", "file": "models/sale_order.py", "line": 7076, "kind": "action_method"},
|
||||
{"from": "funding_approved", "to": "contract_received", "trigger": "mod_pca_received_wizard.action_confirm", "file": "wizard/mod_pca_received_wizard.py", "line": 143, "kind": "wizard"},
|
||||
{"from": "contract_received", "to": "in_production", "trigger": "action_mod_in_production", "file": "models/sale_order.py", "line": 7093, "kind": "action_method"},
|
||||
{"from": "in_production", "to": "project_complete", "trigger": "action_mod_project_complete", "file": "models/sale_order.py", "line": 7100, "kind": "action_method"},
|
||||
{"from": "project_complete", "to": "pod_submitted", "trigger": "send_to_mod_wizard.action_send (pod)", "file": "wizard/send_to_mod_wizard.py", "line": 221, "kind": "wizard"},
|
||||
{"from": "pod_submitted", "to": "case_closed", "trigger": "action_mod_close_case", "file": "models/sale_order.py", "line": 7123, "kind": "action_method"},
|
||||
{"from": "*", "to": "on_hold", "trigger": "action_mod_on_hold", "file": "models/sale_order.py", "line": 7129, "kind": "action_method"},
|
||||
{"from": "on_hold", "to": "in_production", "trigger": "action_mod_resume", "file": "models/sale_order.py", "line": 7134, "kind": "action_method"},
|
||||
{"from": "*", "to": "cancelled", "trigger": "action_cancel", "file": "models/sale_order.py", "line": 7142, "kind": "action_method"}
|
||||
]
|
||||
},
|
||||
"sa_mobility": {
|
||||
"field": "x_fc_sa_status",
|
||||
"label": "SA Mobility",
|
||||
"default": "quotation",
|
||||
"terminal": ["case_closed", "cancelled"],
|
||||
"states": [
|
||||
{"key": "quotation", "label": "Quotation"},
|
||||
{"key": "form_ready", "label": "SA Form Ready"},
|
||||
{"key": "submitted_to_sa", "label": "Submitted to SA Mobility"},
|
||||
{"key": "pre_approved", "label": "Pre-Approved"},
|
||||
{"key": "ready_delivery", "label": "Ready for Delivery"},
|
||||
{"key": "delivered", "label": "Delivered"},
|
||||
{"key": "pod_submitted", "label": "POD Submitted"},
|
||||
{"key": "payment_received", "label": "Payment Received"},
|
||||
{"key": "case_closed", "label": "Case Closed"},
|
||||
{"key": "on_hold", "label": "On Hold"},
|
||||
{"key": "cancelled", "label": "Cancelled"},
|
||||
{"key": "denied", "label": "Denied"}
|
||||
],
|
||||
"transitions": [
|
||||
{"from": "quotation", "to": "form_ready", "trigger": "odsp_sa_mobility_wizard.action_confirm", "file": "wizard/odsp_sa_mobility_wizard.py", "line": null, "kind": "wizard"},
|
||||
{"from": "form_ready", "to": "submitted_to_sa", "trigger": "odsp_submit_to_odsp_wizard.action_confirm", "file": "wizard/odsp_submit_to_odsp_wizard.py", "line": 105, "kind": "wizard"},
|
||||
{"from": "submitted_to_sa", "to": "pre_approved", "trigger": "odsp_pre_approved_wizard.action_confirm", "file": "wizard/odsp_pre_approved_wizard.py", "line": 68, "kind": "wizard"},
|
||||
{"from": "pre_approved", "to": "ready_delivery", "trigger": "odsp_ready_delivery_wizard.action_confirm", "file": "wizard/odsp_ready_delivery_wizard.py", "line": 170, "kind": "wizard"},
|
||||
{"from": "ready_delivery", "to": "delivered", "trigger": "_odsp_advance_status('delivered')", "file": "models/sale_order.py", "line": 1212, "kind": "auto_write"},
|
||||
{"from": "delivered", "to": "pod_submitted", "trigger": "_odsp_advance_status('pod_submitted')", "file": "models/sale_order.py", "line": 1225, "kind": "auto_write"},
|
||||
{"from": "pod_submitted", "to": "payment_received", "trigger": "invoice payment posted", "file": "models/account_move.py", "line": 59, "kind": "auto_write"},
|
||||
{"from": "payment_received", "to": "case_closed", "trigger": "_cron_auto_close_odsp_paid_cases", "file": "models/sale_order.py", "line": 6899, "kind": "cron"},
|
||||
{"from": "*", "to": "on_hold", "trigger": "action_odsp_on_hold", "file": "models/sale_order.py", "line": 1396, "kind": "action_method"},
|
||||
{"from": "on_hold", "to": "quotation", "trigger": "action_odsp_resume", "file": "models/sale_order.py", "line": 1401, "kind": "action_method"},
|
||||
{"from": "*", "to": "denied", "trigger": "action_odsp_denied", "file": "models/sale_order.py", "line": 1405, "kind": "action_method"},
|
||||
{"from": "*", "to": "cancelled", "trigger": "action_cancel", "file": "models/sale_order.py", "line": 1141, "kind": "action_method"}
|
||||
]
|
||||
},
|
||||
"odsp_standard": {
|
||||
"field": "x_fc_odsp_std_status",
|
||||
"label": "ODSP Standard",
|
||||
"default": "quotation",
|
||||
"terminal": ["case_closed", "cancelled"],
|
||||
"states": [
|
||||
{"key": "quotation", "label": "Quotation"},
|
||||
{"key": "submitted_to_odsp", "label": "Submitted to ODSP"},
|
||||
{"key": "pre_approved", "label": "Pre-Approved"},
|
||||
{"key": "ready_delivery", "label": "Ready for Delivery"},
|
||||
{"key": "delivered", "label": "Delivered"},
|
||||
{"key": "pod_submitted", "label": "POD Submitted"},
|
||||
{"key": "payment_received", "label": "Payment Received"},
|
||||
{"key": "case_closed", "label": "Case Closed"},
|
||||
{"key": "on_hold", "label": "On Hold"},
|
||||
{"key": "cancelled", "label": "Cancelled"},
|
||||
{"key": "denied", "label": "Denied"}
|
||||
],
|
||||
"transitions": [
|
||||
{"from": "quotation", "to": "submitted_to_odsp", "trigger": "odsp_submit_to_odsp_wizard.action_confirm", "file": "wizard/odsp_submit_to_odsp_wizard.py", "line": 105, "kind": "wizard"},
|
||||
{"from": "submitted_to_odsp", "to": "pre_approved", "trigger": "odsp_pre_approved_wizard.action_confirm", "file": "wizard/odsp_pre_approved_wizard.py", "line": 68, "kind": "wizard"},
|
||||
{"from": "pre_approved", "to": "ready_delivery", "trigger": "odsp_ready_delivery_wizard.action_confirm", "file": "wizard/odsp_ready_delivery_wizard.py", "line": 170, "kind": "wizard"},
|
||||
{"from": "ready_delivery", "to": "delivered", "trigger": "_odsp_advance_status('delivered')", "file": "models/sale_order.py", "line": 1215, "kind": "auto_write"},
|
||||
{"from": "delivered", "to": "pod_submitted", "trigger": "_odsp_advance_status('pod_submitted')", "file": "models/sale_order.py", "line": 1225, "kind": "auto_write"},
|
||||
{"from": "pod_submitted", "to": "payment_received", "trigger": "invoice payment posted", "file": "models/account_move.py", "line": 59, "kind": "auto_write"},
|
||||
{"from": "payment_received", "to": "case_closed", "trigger": "_cron_auto_close_odsp_paid_cases", "file": "models/sale_order.py", "line": 6899, "kind": "cron"},
|
||||
{"from": "*", "to": "on_hold", "trigger": "action_odsp_on_hold", "file": "models/sale_order.py", "line": 1396, "kind": "action_method"},
|
||||
{"from": "on_hold", "to": "quotation", "trigger": "action_odsp_resume", "file": "models/sale_order.py", "line": 1401, "kind": "action_method"},
|
||||
{"from": "*", "to": "denied", "trigger": "action_odsp_denied", "file": "models/sale_order.py", "line": 1405, "kind": "action_method"},
|
||||
{"from": "*", "to": "cancelled", "trigger": "action_cancel", "file": "models/sale_order.py", "line": 1141, "kind": "action_method"}
|
||||
]
|
||||
},
|
||||
"ontario_works": {
|
||||
"field": "x_fc_ow_status",
|
||||
"label": "Ontario Works",
|
||||
"default": "quotation",
|
||||
"terminal": ["case_closed", "cancelled"],
|
||||
"states": [
|
||||
{"key": "quotation", "label": "Quotation"},
|
||||
{"key": "documents_ready", "label": "Documents Ready"},
|
||||
{"key": "submitted_to_ow", "label": "Submitted to Ontario Works"},
|
||||
{"key": "payment_received", "label": "Payment Received"},
|
||||
{"key": "ready_delivery", "label": "Ready for Delivery"},
|
||||
{"key": "delivered", "label": "Delivered"},
|
||||
{"key": "case_closed", "label": "Case Closed"},
|
||||
{"key": "on_hold", "label": "On Hold"},
|
||||
{"key": "cancelled", "label": "Cancelled"},
|
||||
{"key": "denied", "label": "Denied"}
|
||||
],
|
||||
"transitions": [
|
||||
{"from": "quotation", "to": "documents_ready", "trigger": "odsp_discretionary_wizard.action_confirm (docs)", "file": "wizard/odsp_discretionary_wizard.py", "line": 245, "kind": "wizard"},
|
||||
{"from": "documents_ready", "to": "submitted_to_ow", "trigger": "odsp_discretionary_wizard.action_confirm (submit)", "file": "wizard/odsp_discretionary_wizard.py", "line": 260, "kind": "wizard"},
|
||||
{"from": "submitted_to_ow", "to": "payment_received", "trigger": "invoice payment posted", "file": "models/account_move.py", "line": 59, "kind": "auto_write"},
|
||||
{"from": "payment_received", "to": "ready_delivery", "trigger": "odsp_ready_delivery_wizard.action_confirm", "file": "wizard/odsp_ready_delivery_wizard.py", "line": 170, "kind": "wizard"},
|
||||
{"from": "ready_delivery", "to": "delivered", "trigger": "_odsp_advance_status('delivered')", "file": "models/sale_order.py", "line": 1217, "kind": "auto_write"},
|
||||
{"from": "delivered", "to": "case_closed", "trigger": "_cron_auto_close_odsp_paid_cases", "file": "models/sale_order.py", "line": 6899, "kind": "cron"},
|
||||
{"from": "*", "to": "on_hold", "trigger": "action_odsp_on_hold", "file": "models/sale_order.py", "line": 1396, "kind": "action_method"},
|
||||
{"from": "on_hold", "to": "quotation", "trigger": "action_odsp_resume", "file": "models/sale_order.py", "line": 1401, "kind": "action_method"},
|
||||
{"from": "*", "to": "denied", "trigger": "action_odsp_denied", "file": "models/sale_order.py", "line": 1405, "kind": "action_method"},
|
||||
{"from": "*", "to": "cancelled", "trigger": "action_cancel", "file": "models/sale_order.py", "line": 1141, "kind": "action_method"}
|
||||
]
|
||||
}
|
||||
};
|
||||
1197
entech-website-design.html
Normal file
1197
entech-website-design.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,244 @@
|
||||
# Existing Nickel Plating / Metal Finishing ERP Systems — What They Record
|
||||
|
||||
**Prepared:** 2026-04-09
|
||||
**Purpose:** Benchmark what the industry standard plating ERPs already capture, so `fusion-plating` can match the table stakes and differentiate where it counts.
|
||||
**Scope:** Commercial software used by actual metal finishing shops in North America. Focus on what they *track and retain*, not on marketing gloss.
|
||||
|
||||
---
|
||||
|
||||
## 1. The Landscape — Who's Actually in the Market
|
||||
|
||||
| Product | Focus | Notes |
|
||||
|---------|-------|-------|
|
||||
| **[Steelhead Technologies](https://gosteelhead.com/)** | Purpose-built for metal finishing (plating, anodizing, powder coat). | Most feature-complete plating-specific ERP. NADCAP Scanner is their flagship compliance tool. |
|
||||
| **[Lab Wizard Cloud](https://lab-wizard.com/en/lab-wizard-cloud/)** | Plating bath chemistry LIMS/SPC. | Specialized in titration, bath tracking, SPC. Often used alongside a general ERP. Free tier for small shops. |
|
||||
| **[ProShop ERP](https://proshoperp.com/)** | Shop-floor-first ERP with built-in QMS. Strong in aerospace / AS9100 / ITAR. | Paperless travelers, version-controlled work instructions. |
|
||||
| **[PROPLATE™](https://proplate.pro/)** | Barrel plating and general metal finishing. | Smaller vendor; limited public detail. |
|
||||
| **[Anoplex](https://www.anoplex.com/)** | Anodizing, plating, painting, NDT, passivation. | Cloud, solution control and chemical inventory. |
|
||||
| **[PHTPlus](https://phtplus.com/about/)** | Plating, heat treating, finishing services. | Job-shop centric. |
|
||||
| **[Fitfactory](https://www.fitfactory.com/metal-finishing-production-control)** | UK-based metal finishing production control. | NCR logging, chemical batch tracking for NADCAP. |
|
||||
| **[JobBOSS² + uniPoint](https://www.ecisolutions.com/blog/manufacturing/jobboss/how-jobboss-and-unipoint-support-iso-9001-and-as9100-certification/)** | General job shop ERP + bolted-on QMS. | Widely used, not plating-specific but common in shops that do plating. |
|
||||
| **[QT9 QMS / QT9 ERP](https://qt9software.com/as9100)** | Aerospace QMS first, ERP second. | Clean AS9100 workflows, customer portal. |
|
||||
| **[Global Shop Solutions](https://www.globalshopsolutions.com/)** | General manufacturing ERP with AS9100 modules. | Large installed base, not plating-first. |
|
||||
| **[Tudodesk](https://www.tudodesk.com/free-chrome-plating-shop-software)** | Chrome plating shop software. | Light-weight, SMB-focused, online quoting/billing. |
|
||||
| **[Varland Plating portal](https://varland.com/technology)** | Example of a shop-built customer portal. | Useful as a reference for what customers expect to see. |
|
||||
|
||||
**Takeaway:** There is **no** single Canadian-made, Toronto-compliance-aware, electroless-nickel-first ERP on the market. The closest competitors are US-based and cover the quality/NADCAP side well but treat **Ontario sewer, RPRA hazardous-waste, ECA air, and Toronto P2 Plan obligations** as something handled outside the ERP. This is the opening.
|
||||
|
||||
---
|
||||
|
||||
## 2. Feature Inventory — What They Record
|
||||
|
||||
Grouped by concern. "✓" = explicitly documented on the vendor page or a credible review; blank = unclear or not mentioned.
|
||||
|
||||
### 2.1 Bath / Tank Chemistry Management
|
||||
|
||||
| Data point / feature | Steelhead | Lab Wizard | Anoplex | ProShop | PROPLATE |
|
||||
|----------------------|-----------|------------|---------|---------|----------|
|
||||
| Titration-driven concentration calculation | | ✓ | ✓ | | |
|
||||
| Auto-calculated chemical addition (dosing) | | ✓ | ✓ | | |
|
||||
| Trend visualization (concentration vs time) | ✓ | ✓ | | | |
|
||||
| Out-of-spec alarm / email / channel alert | | ✓ | | | |
|
||||
| Scheduled analysis reminder (per bath) | | ✓ | | | |
|
||||
| Bath age / MTO (Metal Turnover) counter | | ✓ | | | |
|
||||
| Phosphorus / orthophosphite buildup tracking | | ✓ | | | |
|
||||
| Temperature / pH / agitation log | ✓ | ✓ | | | |
|
||||
| Filter / carbon treatment / cleanup log | | ✓ | | | |
|
||||
| Bath dump & makeup record | | ✓ | ✓ | | |
|
||||
| IoT / sensor input integration | | ✓ | | | |
|
||||
| SPC charts with CpK | | ✓ | | | |
|
||||
| Audit-ready chemistry reports | | ✓ | | ✓ | |
|
||||
|
||||
**Reference:** Electroless nickel bath life is measured in **Metal Turnovers (MTO)**: MTO = cumulative Ni replenished ÷ (initial Ni × bath volume). A 5 g/L bath replenished with 5 g/L of Ni = 1 MTO. Most mid-phos EN baths run **5–9 MTO** before dump; orthophosphite builds at roughly **24–30 g/L per MTO**, and phosphorus content in the deposit rises with bath age. Any serious EN plating system must track MTO, orthophosphite, and P-content bands. [Lab Wizard MTO article](https://lab-wizard.com/en/resources/knowledge/metal-turnovers/)
|
||||
|
||||
### 2.2 Production / Tank / Line Management
|
||||
|
||||
| Feature | Steelhead | ProShop | JobBOSS | PROPLATE | PHTPlus |
|
||||
|---------|-----------|---------|---------|----------|---------|
|
||||
| Tank QR code identification | ✓ | | | | |
|
||||
| Tank time tracking "to the second" | ✓ | | | | |
|
||||
| Multi-line capacity planning / scheduling | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| Recipe / process card per part number | ✓ | ✓ | ✓ | ✓ | |
|
||||
| Racking/packing instructions per part | ✓ | | | | |
|
||||
| Real-time part-in-process location | ✓ | ✓ | ✓ | | |
|
||||
| Mobile/tablet operator stations | ✓ | ✓ | | | |
|
||||
| Photo documentation at workstation | ✓ | ✓ | | | |
|
||||
| Paperless digital traveler | ✓ | ✓ | ✓ | ✓ | |
|
||||
| Rework tracking by line / part / finish | ✓ | ✓ | ✓ | | |
|
||||
|
||||
### 2.3 Quality & Certification Records
|
||||
|
||||
| Feature | Steelhead | ProShop | QT9 | JobBOSS | Fitfactory |
|
||||
|---------|-----------|---------|-----|---------|------------|
|
||||
| ISO 9001 compliance workflows | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| AS9100 Rev D aerospace workflows | ✓ | ✓ | ✓ | ✓ | |
|
||||
| NADCAP AC7108 traceability (plating) | ✓ | | | | ✓ |
|
||||
| NADCAP Scanner (cert digitization) | ✓ | | | | |
|
||||
| ITAR / CMMC controls | | ✓ | ✓ | | |
|
||||
| ISO 13485 (medical) | | ✓ | ✓ | | |
|
||||
| Certificate of Conformance auto-generation | ✓ | ✓ | ✓ | ✓ | |
|
||||
| First Article Inspection Report (FAIR) | | ✓ | ✓ | ✓ | |
|
||||
| Non-conformance / hold tag workflow | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| MRB (Material Review Board) routing | | ✓ | ✓ | ✓ | |
|
||||
| CAPA / corrective action log | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| Customer specification library | ✓ | ✓ | ✓ | ✓ | |
|
||||
| Document / revision control | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| Operator training tied to doc revision | | ✓ | ✓ | | |
|
||||
| Internal audit schedule + tracking | | ✓ | ✓ | ✓ | |
|
||||
| Management review agenda/minutes | | ✓ | ✓ | | |
|
||||
| Supplier approved-vendor list (AVL) | ✓ | ✓ | ✓ | ✓ | |
|
||||
| Supplier receiving inspection | ✓ | ✓ | ✓ | ✓ | |
|
||||
| Calibration register (NIST-traceable) | | ✓ | ✓ | ✓ | |
|
||||
| XRF thickness reading capture | | | | | ✓ |
|
||||
| Cross-reference to customer specs (AMS, ASTM, MIL) | ✓ | ✓ | ✓ | ✓ | |
|
||||
|
||||
### 2.4 Shop Floor / Operator Experience
|
||||
|
||||
| Feature | Steelhead | ProShop |
|
||||
|---------|-----------|---------|
|
||||
| Touch-screen tablets at each station | ✓ | ✓ |
|
||||
| Version-controlled setup sheets displayed live | | ✓ |
|
||||
| Tool list / fixture list at station | ✓ | ✓ |
|
||||
| Inspection plan linked to the operation | | ✓ |
|
||||
| Real-time shop-floor dashboard | ✓ | ✓ |
|
||||
| Operator badge sign-on, time-on-part | ✓ | ✓ |
|
||||
| Automatic hold if prior step incomplete | ✓ | ✓ |
|
||||
| Photo capture of defect / process step | ✓ | ✓ |
|
||||
|
||||
### 2.5 Customer Portal
|
||||
|
||||
| Feature | Steelhead | Varland | Tudodesk | QT9 |
|
||||
|---------|-----------|---------|----------|-----|
|
||||
| Near-real-time job status | ✓ | ✓ | ✓ | ✓ |
|
||||
| Quote request / approval online | ✓ | | ✓ | ✓ |
|
||||
| Upload drawings / specs | ✓ | | ✓ | |
|
||||
| Download Certificate of Conformance PDF | ✓ | ✓ | | ✓ |
|
||||
| Download packing lists / shipping PDF | ✓ | ✓ | ✓ | ✓ |
|
||||
| Invoice access | ✓ | | ✓ | ✓ |
|
||||
| Historical orders searchable | ✓ | ✓ (30 days) | ✓ | ✓ |
|
||||
|
||||
### 2.6 Environmental / Waste / Health & Safety
|
||||
|
||||
| Feature | Plating ERPs | Stand-alone EHS tools |
|
||||
|---------|--------------|----------------------|
|
||||
| SDS library with version + review date | ✗ mostly absent | ✓ (Chemical Safety, VelocityEHS, ERA-EHS) |
|
||||
| Chemical inventory by location | partial (Anoplex, Steelhead) | ✓ |
|
||||
| WHMIS training matrix | ✗ | ✓ (Cority, VelocityEHS) |
|
||||
| Exposure monitoring records (air sampling) | ✗ | ✓ |
|
||||
| Hazardous waste generation log | ✗ | ✓ (Wastelinq, IMEC, ERA, EHSTracks) |
|
||||
| Cradle-to-grave manifest tracking | ✗ | ✓ (Wastelinq, IMEC) |
|
||||
| Automated generator reporting | ✗ | partial |
|
||||
| Sewer discharge monitoring log | ✗ | partial |
|
||||
| Pollution Prevention Plan builder | ✗ | ✗ |
|
||||
| Spill incident register | ✗ | ✓ |
|
||||
| JHSC meeting / minutes | ✗ | partial |
|
||||
|
||||
**This is the biggest single gap in the industry.** Plating shops today run a plating ERP (Steelhead / ProShop / JobBOSS) for production, a bath-chemistry tool (Lab Wizard), and a *separate* EHS suite (ERA-EHS / VelocityEHS / Wastelinq) for environmental and waste compliance. Three logins, three sources of truth, and nothing in any of them is written against **Toronto Chapter 681** or **RPRA HWP Registry** specifically.
|
||||
|
||||
Vendors handling these separately:
|
||||
- Waste: [Wastelinq](https://wastelinq.com/), [IMEC Technologies](https://www.imectechnologies.com/hazardous-waste-management-software/), [ERA-EHS Waste](https://www.era-environmental.com/solutions/environmental/waste), [Cority Waste Management](https://www.cority.com/environmental-cloud/waste-management-software/), [VelocityEHS](https://www.ehs.com/solution/environmental-compliance/waste-management/), [Sphera](https://sphera.com/solutions/product-stewardship/hazardous-material-management-for-the-u-s-government/hazardous-waste-management-software/), [Chemical Safety](https://chemicalsafety.com/hazardous-waste-management-software/)
|
||||
|
||||
---
|
||||
|
||||
## 3. Process-Step Records That Electroless Nickel Specifically Needs
|
||||
|
||||
Beyond the general plating ERP fields, electroless nickel has a handful of step-specific data points that *must* be captured to satisfy Nadcap AC7108 and customer specs like AMS 2404 / ASTM B733 / MIL-C-26074:
|
||||
|
||||
| Step | Data captured per job / lot | Why |
|
||||
|------|------------------------------|-----|
|
||||
| **Pre-clean / degrease** | Time, temperature, bath ID | Contamination risk to EN bath |
|
||||
| **Acid activation / etch** | Time, concentration, temperature, bath ID | Major source of hydrogen absorption |
|
||||
| **Strike / Wood's nickel** (stainless, aluminium) | Current, time, bath ID | Adhesion requirement |
|
||||
| **EN plate** | Time-in, time-out, bath ID, MTO at plate, temperature, pH, Ni concentration, P concentration, target thickness, measured thickness | The core process — all of this is traceable for life-of-part. |
|
||||
| **Rinse cascade** | Number of rinses, conductivity readings (for zero-discharge shops) | Drag-out management |
|
||||
| **Passivation / chromate (optional)** | Bath ID, time, temperature | Corrosion spec compliance |
|
||||
| **Hydrogen embrittlement relief bake** | Time-in, time-out, temperature profile, oven ID, bake duration (typ. 4 h at 375°F / 190°C per AMS 2759/9), recorder chart | **Mandatory within 1–4 h of plate for high-strength steel**; non-compliance = customer reject, or worse, part failure in service |
|
||||
| **Thickness measurement** | XRF reading(s) per part/lot, instrument ID, calibration cert reference, P-content reading, operator | NIST-traceable, tamper-proof |
|
||||
| **Adhesion test** | Method (bend, heat-quench, grit-blast), result, operator | Per Nadcap AC7108 |
|
||||
| **Corrosion test** (if required) | Method (salt spray, CASS), duration, result | Per customer spec |
|
||||
| **Final inspection** | Visual, dimensional, operator, photo (optional) | CoC support |
|
||||
| **Certificate of Conformance** | Auto-generated, pulling every previous step | Customer deliverable |
|
||||
|
||||
Nadcap accreditation means *every* one of these is documented and traceable — [AC7108](https://www.newmethodplating.com/nadcap-accreditation-and-as7108-certification-for-chemical-processing/) explicitly audits against this. Existing plating ERPs cover most of it; **none** I found explicitly model the EN-specific fields (MTO, orthophosphite, P-content per MTO, bake time-window enforcement).
|
||||
|
||||
---
|
||||
|
||||
## 4. Gaps & Opportunities for `fusion-plating`
|
||||
|
||||
Stacking the compliance research from the companion document against this feature benchmark:
|
||||
|
||||
### 4.1 Gaps in the market
|
||||
1. **No unified production + environmental compliance system.** Plating shops juggle 2–3 platforms; `fusion-plating` can be the first to put production, bath chemistry, waste manifests, sewer monitoring, and P2 Plan data under one roof.
|
||||
2. **No Ontario/Toronto-native regulatory awareness.** Every existing system is built for US regs (EPA RCRA, OSHA) or UK regs. A module that speaks **Chapter 681 Table 1, RPRA HWP, O. Reg. 419/05, MECP ECA** out of the box is unique.
|
||||
3. **No automated Toronto P2 Plan builder.** Six-year cycles, year-3 updates, 12-heavy-metal inventory — this is a compliance workflow, not a document. It should be a feature, not a Word template.
|
||||
4. **No EN-specific chemistry engine.** MTO counter, orthophosphite projection, P-content prediction, bake-time-window enforcement (plate → bake must be ≤1 h) — none of the ERPs I examined model this natively.
|
||||
5. **Delegation-first UX.** The user said the owner likes to delegate. Existing systems are built around a scheduler assigning work. `fusion-plating` can flip this: owner drops the inquiry in, the system routes it (sales → engineer → scheduler → supervisor → operator → inspector → shipper) and the owner sees *status*, not *details*.
|
||||
|
||||
### 4.2 Table-stakes `fusion-plating` must match (or it looks cheap)
|
||||
- Paperless digital traveler
|
||||
- QR-coded tanks and jobs
|
||||
- Tablet shop-floor stations
|
||||
- Customer portal (quote, status, CoC download, invoices)
|
||||
- Certificate of Conformance auto-generation
|
||||
- Non-conformance + CAPA workflow
|
||||
- Document control with revision + trained-on matrix
|
||||
- Calibration register
|
||||
- AS9100 / NADCAP audit trail posture (traceability from raw material to shipped part)
|
||||
- Supplier AVL and receiving inspection
|
||||
|
||||
### 4.3 Differentiators `fusion-plating` should own
|
||||
- **Toronto Chapter 681 real-time discharge monitor** — log every lab report, trigger at 80 % of Table 1, force investigation before the next discharge.
|
||||
- **Automated P2 Plan document pack** — pulls 6 years of pollutant inventory, calculates trends, pre-writes year-3 and year-6 updates.
|
||||
- **RPRA HWP Registry handshake** — generator registration number, waste-stream profiles, manifest numbers, carrier approval expiry, annual report scheduling.
|
||||
- **ECA compliance calendar** — every ECA condition (source testing, annual report, equipment maintenance) on a calendar with responsible party, overdue escalation.
|
||||
- **EN bath chemistry engine** — MTO counter, orthophosphite projection, phosphorus content band forecast, automatic dump/makeup recommendation.
|
||||
- **Bake-window enforcer** — when a high-strength-steel part finishes plating, the system starts a **clock** and alarms if the part isn't in an oven within the customer-spec window.
|
||||
- **Ontario-aware training matrix** — WHMIS 2015 refresh flags, TDG 3-year recert, first-aid CPR renewal, JHSC member term tracking.
|
||||
- **Delegation inbox** — every new task, quality hold, complaint, inspector letter, or bath alarm lands in a "someone has to do this" inbox and auto-routes by role.
|
||||
|
||||
---
|
||||
|
||||
## 5. Sources
|
||||
|
||||
### Plating / metal finishing ERPs
|
||||
- [Steelhead Technologies — Compliance](https://gosteelhead.com/compliance)
|
||||
- [Steelhead — Plating & Anodizing Software](https://gosteelhead.com/plating-anodizing-software)
|
||||
- [Steelhead — Customer Portal](https://gosteelhead.com/customer-portal-0)
|
||||
- [Steelhead — Quality Management](https://gosteelhead.com/resource-library/metal-finishing-quality-management-software)
|
||||
- [Steelhead — NADCAP Scanner](https://www.pfonline.com/products/nadcap-scanner-provides-fully-digital-production-automated-certification-management)
|
||||
- [Lab Wizard Cloud](https://lab-wizard.com/en/lab-wizard-cloud/)
|
||||
- [Lab Wizard — Metal Turnovers (MTO) article](https://lab-wizard.com/en/resources/knowledge/metal-turnovers/)
|
||||
- [ProShop ERP — Quality Management System](https://proshoperp.com/product/quality-management-system/)
|
||||
- [ProShop ERP — Aerospace](https://proshoperp.com/industries/aerospace/)
|
||||
- [ProShop ERP — MES](https://proshoperp.com/product/mes/)
|
||||
- [PROPLATE™](https://proplate.pro/metal-finishing-erp-systems-and-job-shop-management-proplate-strategy-for-high-quality-scalable-operations/)
|
||||
- [Anoplex Software](https://www.anoplex.com/)
|
||||
- [PHTPlus](https://phtplus.com/about/)
|
||||
- [Fitfactory — Metal Finishing Production Control](https://www.fitfactory.com/metal-finishing-production-control)
|
||||
- [JobBOSS² + uniPoint for AS9100](https://www.ecisolutions.com/blog/manufacturing/jobboss/how-jobboss-and-unipoint-support-iso-9001-and-as9100-certification/)
|
||||
- [QT9 QMS — AS9100](https://qt9software.com/as9100)
|
||||
- [QT9 ERP — Customer Portal](https://qt9software.com/erp/customer-portal-software)
|
||||
- [Global Shop Solutions — AS9100](https://www.globalshopsolutions.com/blog/as9100-certification-doesnt-have-to-be-hard-let-erp-do-the-heavy-lifting)
|
||||
- [Tudodesk — Chrome Plating Shop Software](https://www.tudodesk.com/free-chrome-plating-shop-software)
|
||||
- [Varland Plating — Technology & Customer Portal](https://varland.com/technology)
|
||||
|
||||
### Stand-alone environmental / waste platforms (what plating ERPs don't cover)
|
||||
- [Wastelinq](https://wastelinq.com/)
|
||||
- [IMEC Hazardous Waste Management](https://www.imectechnologies.com/hazardous-waste-management-software/)
|
||||
- [Ecesis Waste Tracking](https://www.ecesis.net/Waste-Management-Software.aspx)
|
||||
- [ERA-EHS Waste Management](https://www.era-environmental.com/solutions/environmental/waste)
|
||||
- [Cority Waste Management](https://www.cority.com/environmental-cloud/waste-management-software/)
|
||||
- [VelocityEHS Waste Management](https://www.ehs.com/solution/environmental-compliance/waste-management/)
|
||||
- [Sphera Hazardous Waste Management](https://sphera.com/solutions/product-stewardship/hazardous-material-management-for-the-u-s-government/hazardous-waste-management-software/)
|
||||
- [Chemical Safety Software](https://chemicalsafety.com/hazardous-waste-management-software/)
|
||||
- [EHSTracks Waste Compliance](https://ehstracks.com/waste-compliance-software/)
|
||||
|
||||
### Process knowledge (EN-specific)
|
||||
- [Electroless Nickel Plating — Products Finishing overview](https://www.pfonline.com/articles/electroless-nickel-plating)
|
||||
- [Controlling Phosphorus Content — Products Finishing](https://www.pfonline.com/articles/controlling-phosphorus-content-in-electroless-nickel-phosphorus-coatings)
|
||||
- [Electroless Nickel-Phosphorus Plating — Wikipedia](https://en.wikipedia.org/wiki/Electroless_nickel-phosphorus_plating)
|
||||
- [Hydrogen Embrittlement & Electroplating — Sharretts Plating](https://www.sharrettsplating.com/blog/hydrogen-embrittlement-electroplating-what-you-need-to-know/)
|
||||
- [EN Thickness & P-Content via XRF — VRXRF](https://www.vrxrf.com/blog/electroless-nickel-thickness-phosphorus-content-detection-guide/)
|
||||
- [Measuring EN Plating with XRF — AZoM](https://www.azom.com/article.aspx?ArticleID=15524)
|
||||
- [National Electroless Nickel — Specifications & Properties](https://www.nationalelectrolessnickel.com/specs.htm)
|
||||
- [Nickel Institute — Nickel Plating Handbook (PDF)](https://nickelinstitute.org/media/lxxh1zwr/2023-nickelplatinghandbooka5_printablepdf.pdf)
|
||||
@@ -0,0 +1,258 @@
|
||||
# Electroless Nickel Plating — Canadian Compliance & Record-Keeping Reference
|
||||
|
||||
**Location:** Toronto, Ontario, Canada
|
||||
**Prepared:** 2026-04-09
|
||||
**Purpose:** Pre-engagement reference for scoping an Odoo module covering workflow, compliance, and order management for an electroless nickel plating shop.
|
||||
**Status:** Research document — verify jurisdiction-specific details with the client's environmental consultant and their current ECA before treating as authoritative.
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
An electroless nickel plating operation in Toronto sits at the intersection of **federal, provincial, municipal, and industry-voluntary** compliance regimes. The binding obligations that must be reflected in any operations system are:
|
||||
|
||||
| # | Obligation | Authority | Why it matters to the system |
|
||||
|---|------------|-----------|------------------------------|
|
||||
| 1 | **Environmental Compliance Approval (ECA — Air)** | Ontario MECP | Every plating line, drying oven and gas-fired source must be operated within the conditions on the facility's ECA — the system must track operating parameters, maintenance, and source testing. |
|
||||
| 2 | **Toronto Sewer Use By-law Ch. 681** — discharge limits + P2 Plan | City of Toronto | Nickel discharge cap **2 mg/L** to sanitary sewer, **0.08 mg/L** to storm sewer. Self-monitoring, sampling, and a six-year Pollution Prevention Plan are mandatory for metal finishers. |
|
||||
| 3 | **Ontario Reg. 347 hazardous waste generator registration** | Ontario MECP via RPRA HWP Registry | Spent plating baths, sludges, rinses and filter media are subject wastes — generator registration, manifesting and reporting run through RPRA's Hazardous Waste Program Registry (replaced HWIN in 2023). |
|
||||
| 4 | **CEPA Schedule 1 — nickel compounds** + NPRI reporting | Environment and Climate Change Canada | Oxidic, sulphidic and soluble inorganic nickel compounds are listed toxic substances. Releases and transfers are reportable to the National Pollutant Release Inventory annually if thresholds are met. |
|
||||
| 5 | **OHSA + Reg. 833 exposure limits, WHMIS 2015** | Ontario Ministry of Labour | Worker exposure to nickel aerosols, acids and reducing agents must be assessed, controlled, monitored, and documented. SDS library and training records are mandatory. |
|
||||
| 6 | **Transportation of Dangerous Goods (TDG)** | Transport Canada | Incoming acids, nickel salts, hypophosphite, and outgoing hazardous wastes need classified shipping documents, training certificates and means-of-containment records. |
|
||||
|
||||
Beyond law, customers will usually demand **ISO 9001** at minimum, and many will require **AS9100 / Nadcap AC7108** (aerospace electroless plating) or **CGP** (Controlled Goods Program, for defence work). These are not legally binding but functionally mandatory for the customer mix a Toronto shop typically serves.
|
||||
|
||||
---
|
||||
|
||||
## 2. Compliance Map by Jurisdiction
|
||||
|
||||
### 2.1 Federal (Canada)
|
||||
|
||||
#### 2.1.1 Canadian Environmental Protection Act (CEPA 1999)
|
||||
- **Nickel compounds on Schedule 1** (toxic substances): oxidic, sulphidic and soluble inorganic nickel compounds. Triggers risk-management obligations and may trigger pollution-prevention (P2) planning notices from ECCC.
|
||||
- **National Pollutant Release Inventory (NPRI)** — annual reporting for substances above thresholds. For Part 1A metals (nickel included), the standard threshold is **10 tonnes manufactured/processed/otherwise used at ≥1% concentration**, plus any alternate thresholds applicable to metal finishing. Smaller shops may still trigger the employee-hour threshold.
|
||||
- **Records:** facility activity logs, substance inventories, calculation methods, supporting documentation — retain **3 years**.
|
||||
|
||||
#### 2.1.2 Transportation of Dangerous Goods Act (TDG) and Regulations
|
||||
- Applies to incoming drums of nickel sulphate/sulphamate, acids, caustics, hypophosphite; outgoing hazardous waste shipments.
|
||||
- Shipping documents (consignor, classification, UN number, PG, quantity, emergency info), **means-of-containment selection and inspection**, TDG **training certificates** (valid 3 years by road), placarding.
|
||||
- **Records:** shipping documents retained for **2 years** (federal), training records for duration of employment + common practice is 5 years.
|
||||
|
||||
#### 2.1.3 Hazardous Products Act + Hazardous Products Regulations (WHMIS 2015 / GHS)
|
||||
- SDS library maintained current (each SDS must be no older than 3 years or confirmed current).
|
||||
- Container labels (supplier + workplace).
|
||||
- Worker training documented; refreshed when new products or processes are introduced.
|
||||
|
||||
#### 2.1.4 Controlled Goods Program (CGP) — optional, required only for defence/aerospace work
|
||||
- Registration with Public Services and Procurement Canada.
|
||||
- Security assessments of personnel with access to controlled goods.
|
||||
- Record-keeping of controlled technology and physical access.
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Provincial (Ontario)
|
||||
|
||||
#### 2.2.1 Environmental Compliance Approval — Air (Ontario EPA, R.S.O. 1990, c. E.19)
|
||||
- Electroless nickel plating facilities require an **ECA with Limited Operational Flexibility (air)** covering plating lines, drying ovens, and natural-gas-fired equipment.
|
||||
- Conditions typically include source-specific emission limits, equipment inventories, maintenance protocols, source testing schedules, ambient-air impact assessments (ESDM report), and record-keeping clauses.
|
||||
- Annual **Ontario Regulation 419/05** (Local Air Quality) compliance — emission summary and dispersion modelling report updates.
|
||||
|
||||
#### 2.2.2 Ontario Regulation 347 — General (Waste Management) / RPRA Hazardous Waste Program
|
||||
- Generator registration and annual update via the **RPRA HWP Registry** (replaced MECP HWIN in 2023).
|
||||
- Waste characterisation (ignitable, reactive, corrosive, leachate toxic, severely toxic, acute hazardous).
|
||||
- Manifest system for every off-site hazardous-waste shipment — carrier, receiver, waste class.
|
||||
- **Records:** generator reports and manifests retained per RPRA guidance (minimum **2 years**, longer if referenced by ECA or federal requirement).
|
||||
|
||||
#### 2.2.3 Ontario Water Resources Act + Environmental Protection Act (spills, releases)
|
||||
- Spill-reporting obligation: any spill that may cause an adverse effect must be reported immediately to the Spills Action Centre.
|
||||
- Site contingency plan required.
|
||||
|
||||
#### 2.2.4 Occupational Health and Safety Act + Regulation 833 (chemical/biological agents) + Reg. 851 (industrial establishments)
|
||||
- **Note:** Nickel is **not** one of the 11 designated substances under O. Reg. 490/09. It is regulated as a general hazardous chemical agent under **Reg. 833** — the Ontario Table / ACGIH 2017 TLVs apply.
|
||||
- Employers must limit exposure to or below the TWA and short-term limits, carry out exposure assessments, provide engineering controls and PPE, and document the program.
|
||||
- **JHSC** (Joint Health and Safety Committee) required ≥ 20 workers; minutes retained.
|
||||
- WSIB registration and premium reporting.
|
||||
- First-aid regulation (Reg. 1101): kits, trained first-aiders, log of injuries.
|
||||
|
||||
#### 2.2.5 Technical Standards and Safety Authority (TSSA)
|
||||
- Natural-gas-fired ovens, boilers, compressed-gas storage fall under TSSA — installation inspections, periodic maintenance, operator certifications.
|
||||
|
||||
#### 2.2.6 Fire Code (Ontario Reg. 213/07)
|
||||
- Hazardous chemical storage classification, ventilation, spill containment, fire-separation, inspection log.
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Municipal (City of Toronto)
|
||||
|
||||
#### 2.3.1 Sewer Use By-law — Toronto Municipal Code Chapter 681
|
||||
*Last amended 15 May 2023.*
|
||||
|
||||
**Prohibited discharges (§ 681-2A(3))** — absolute prohibition regardless of concentration:
|
||||
acute hazardous waste chemicals, combustible liquid, fuels, hauled waste (without permit), hazardous industrial waste, hazardous waste chemicals, pathological waste, PCBs, pesticides, reactive waste, severely toxic waste, ignitable waste, solid obstructing substances.
|
||||
|
||||
**Table 1 — Limits for Sanitary and Combined Sewers Discharge** (key parameters for a plating shop):
|
||||
|
||||
| Parameter | Limit | Unit |
|
||||
|-----------|-------|------|
|
||||
| pH | > 6.0 to < 11.5 | SU |
|
||||
| Temperature | < 60 | °C |
|
||||
| Biochemical oxygen demand (BOD) | 300 | mg/L |
|
||||
| Suspended solids (total) | 350 | mg/L |
|
||||
| Phosphorus (total) | 10 | mg/L |
|
||||
| Cyanide (total) | 2 | mg/L |
|
||||
| Fluoride | 10 | mg/L |
|
||||
| Oil & grease (mineral & synthetic) | 15 | mg/L |
|
||||
| Phenolics (4AAP) | 1.0 | mg/L |
|
||||
| **Nickel (total)** | **2** | **mg/L** |
|
||||
| Copper (total) | 2 | mg/L |
|
||||
| Chromium (total) | 4 | mg/L |
|
||||
| Chromium (hexavalent) | 2 | mg/L |
|
||||
| Zinc (total) | 2 | mg/L |
|
||||
| Lead (total) | 1 | mg/L |
|
||||
| Cadmium (total) | 0.7 | mg/L |
|
||||
| Cobalt (total) | 5 | mg/L |
|
||||
| Arsenic (total) | 1 | mg/L |
|
||||
| Mercury (total) | 0.01 | mg/L |
|
||||
| Molybdenum (total) | 5 | mg/L |
|
||||
| Aluminum (total) | 50 | mg/L |
|
||||
| Manganese (total) | 5 | mg/L |
|
||||
|
||||
**Table 2 — Storm Sewer Discharge** limits are **dramatically stricter** (e.g. Nickel 0.08 mg/L, Zinc 0.04 mg/L, Mercury 0.0004 mg/L). Plating rinse and spill water must be kept out of storm sewers.
|
||||
|
||||
**§ 681-3 Prohibition of dilution** — cannot dilute to meet a limit.
|
||||
|
||||
**§ 681-5 Pollution Prevention Plan (P2)**:
|
||||
- Required for every **subject sector industry** (metal finishing is listed) and for any industry discharging a **subject pollutant** (Table 3 — includes nickel, chromium, copper, zinc, cobalt, cadmium, arsenic, lead, molybdenum, mercury, selenium).
|
||||
- First plan due within **1 year** of commencing operations.
|
||||
- Full re-plan every **6 years**; update at **year 3**.
|
||||
- Plan contents: process description, subject-pollutant inventory, current quantities discharged, current reduction activities, options evaluation, **3- and 6-year reduction targets**, implementation schedule.
|
||||
- Non-compliance updates required within **90 days** of any new subject pollutant being discharged.
|
||||
|
||||
**§ 681-6 Agreements** — an **Industrial Waste Surcharge Agreement (IWSA)** or **Over-strength Agreement / Compliance Agreement** may be needed where a parameter cannot be met; subject to surcharge fees, self-monitoring and reporting.
|
||||
|
||||
**§ 681-8 Sampling and analytical requirements** — composite or grab sampling per City-approved Standard Methods; samples must be analysed by an **ISO/IEC 17025-accredited laboratory**.
|
||||
|
||||
**§ 681-13 Self-monitoring** — ongoing obligation to sample, record and report under the terms of any agreement or permit.
|
||||
|
||||
**§ 681-14.3 Document retention** — records referenced in the by-law must be retained per the by-law's own retention clause (verify current duration with City; historically **five years** is typical for sampling and P2 supporting records).
|
||||
|
||||
#### 2.3.2 Fire Services — hazardous-material inventory reporting, on-site emergency plan filed with Toronto Fire.
|
||||
|
||||
#### 2.3.3 Zoning & Building Permits — confirm current use permits, building compliance for process wastewater pre-treatment equipment.
|
||||
|
||||
---
|
||||
|
||||
### 2.4 Industry / Customer-Driven (Voluntary but Usually Mandatory in Practice)
|
||||
|
||||
| Standard | What it requires | Record obligations |
|
||||
|----------|------------------|--------------------|
|
||||
| **ISO 9001:2015** | Quality management system, document control, corrective actions, internal audits. | Controlled documents, CAPA, management review minutes, audit reports, training records. |
|
||||
| **ISO 14001:2015** | Environmental management system, aspects/impacts register, legal register, operational controls, emergency preparedness. | EMS manual, objectives & targets, non-conformance log, monitoring results. |
|
||||
| **AS9100 Rev D** | ISO 9001 + aerospace: risk, configuration management, counterfeit-part prevention, full traceability. | Certificate of Conformance per lot, full heat/batch traceability, calibration records. |
|
||||
| **Nadcap AC7108 (electroless plating)** | Audit criteria specific to EN plating — process control, bath chemistry monitoring, thickness testing, adhesion testing, hydrogen embrittlement relief, corrosion testing. | Process control charts, bath analysis logs, operator certification, customer specification cross-reference. |
|
||||
| **CGP (Controlled Goods Program)** | Personnel security screening, visitor control, physical security of controlled technology. | Security plan, personnel assessments, visitor logs, transfer records. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Record-Keeping Obligations — Consolidated Table
|
||||
|
||||
| Record type | Source regulation | Minimum retention | Must be stored/searchable in the system? |
|
||||
|-------------|-------------------|-------------------|--------------------------------------------|
|
||||
| ECA-required source monitoring & maintenance | Ontario ECA condition | Life of ECA + specified years (often 5) | **Yes** — auto-generate compliance reports |
|
||||
| Source-testing reports (stack tests) | ECA / Reg. 419/05 | 5 years | Yes |
|
||||
| Waste generator reports + manifests | Reg. 347 / RPRA | 2 years minimum | **Yes** |
|
||||
| Bath analysis / process control logs | Customer specs / Nadcap | 5–10 years (often life of part + 7) | **Yes** |
|
||||
| Calibration records (thickness gauges, pH meters, thermocouples, balances) | ISO 9001 / AS9100 / Nadcap | Life of equipment + 3–5 years | **Yes** |
|
||||
| Sewer self-monitoring samples & lab reports | By-law Ch. 681 | Per by-law (verify — typically 5 years) | **Yes** |
|
||||
| Pollution Prevention Plan + updates | By-law Ch. 681 § 681-5 | 6-year cycle kept permanently on file | Yes |
|
||||
| NPRI reports + calculation support | CEPA / NPRI | 3 years | Yes |
|
||||
| TDG shipping documents | TDG Regs | 2 years (road) | **Yes** |
|
||||
| TDG training certificates | TDG Regs | Duration of employment + 2 years | Yes |
|
||||
| WHMIS SDS library | HPR / WHMIS 2015 | Current + rolling 3-year window | **Yes** |
|
||||
| WHMIS training records | OHSA / HPR | Duration of employment; best practice 7 years | Yes |
|
||||
| Exposure monitoring (air sampling for nickel, acids) | OHSA / Reg. 833 | Minimum 1 year in Ontario; best practice 30 years for carcinogens | **Yes** |
|
||||
| JHSC minutes | OHSA | 1 year minimum; best practice permanent | Yes |
|
||||
| First-aid log & incident investigations | OHSA / Reg. 1101 | 5 years (WSIB) | **Yes** |
|
||||
| Preventive maintenance / equipment files | ISO 9001 | Life of equipment | **Yes** |
|
||||
| Certificates of Conformance / customer part traceability | AS9100 / Nadcap | Per customer contract — commonly 7–10 years or part life | **Yes** |
|
||||
| Internal audit reports | ISO 9001 / AS9100 | Typically 3 audit cycles | Yes |
|
||||
| Management review minutes | ISO 9001 | 3 years minimum | Yes |
|
||||
| Non-conformance / CAR / corrective action log | ISO 9001 / AS9100 | 5 years | **Yes** |
|
||||
| Supplier approval / material certifications | AS9100 | Per customer contract (7–10 years common) | **Yes** |
|
||||
|
||||
**Bold "Yes"** flags the records where the module has to actively capture, validate, and make retrievable the data at the point of work — not just store a file somewhere.
|
||||
|
||||
---
|
||||
|
||||
## 4. How Compliance Translates into Module Features
|
||||
|
||||
Grouping the above into operational concerns the Odoo module must address:
|
||||
|
||||
### 4.1 Environmental compliance surface
|
||||
- **Chemical inventory** tied to SDS library, expiry tracking, supplier certificates.
|
||||
- **Bath chemistry log** — scheduled titration, pH, temperature, nickel concentration, hypophosphite concentration, contaminant tracking, bath life and rejuvenation events.
|
||||
- **Waste generation log** — by waste stream, tied to manifests and RPRA registration.
|
||||
- **Sewer monitoring log** — grab and composite samples, accredited-lab reports, automatic flagging if any result approaches an 80 % trigger of Table 1 limits.
|
||||
- **Emission & energy log** (for ECA and NPRI) — consumption of nickel, hypophosphite, acids; calculation worksheets; annual NPRI roll-up.
|
||||
- **Spill / incident register** with Spills Action Centre reporting workflow.
|
||||
|
||||
### 4.2 Worker safety surface
|
||||
- **SDS library** with version control and review dates.
|
||||
- **Training matrix** per employee × competency (WHMIS, TDG, confined-space, spill response, process-specific). Auto-flag expiring certs.
|
||||
- **Exposure monitoring records** — air sampling, medical surveillance opt-in, hearing, respiratory fit-test.
|
||||
- **JHSC meeting register** and corrective actions.
|
||||
- **First-aid / injury / near-miss** register feeding WSIB claims.
|
||||
|
||||
### 4.3 Quality / customer surface
|
||||
- **Customer & part master** with specification references (e.g. AMS 2404, ASTM B733, MIL-C-26074, customer-internal specs).
|
||||
- **Router / process card** — every step, time, temperature, bath, concentration requirement, sign-off.
|
||||
- **Traceability** — job → lot → bath → operator → equipment → test results → certificate of conformance.
|
||||
- **Calibration register** with due-date alerts and out-of-tolerance impact assessment.
|
||||
- **Non-conformance / CAR** workflow with root cause, containment, corrective and preventive actions.
|
||||
- **Document control** (procedures, work instructions, forms) with revision history and trained-on-revision matrix.
|
||||
- **Internal audit schedule** and management-review dashboard.
|
||||
|
||||
### 4.4 Order processing surface (where "the team worries about the work, not the system")
|
||||
- **Quote → sales order → router → production → inspection → certificate → shipping → invoice** — one chain, one record.
|
||||
- **Delegation and task routing** — the boss can drop an incoming order into the system and it self-assigns: sales confirms, engineer scopes, scheduler slots, supervisor releases, operator logs, inspector releases, shipper labels.
|
||||
- **Customer portal** — drop drawings, approve quotes, see job status, download Certificates of Conformance.
|
||||
- **Shop-floor dashboards** on tablets — "what's next on my line", "what's parked waiting for first-piece inspection", "what's my bath chemistry telling me to do in 30 minutes".
|
||||
|
||||
---
|
||||
|
||||
## 5. Items to Confirm with the Client
|
||||
|
||||
When the owner meeting happens, these are the questions whose answers change the module scope:
|
||||
|
||||
1. **Scope of work** — aerospace / defence / automotive / electronics / general industrial? Determines whether AS9100 / Nadcap / CGP are in scope.
|
||||
2. **Existing ECA** — do they already hold one, when was it last updated, what are the specific conditions?
|
||||
3. **Sewer permit / IWSA status** — are they discharging under a Compliance Agreement or at Table 1 limits?
|
||||
4. **Waste volumes** — are they a Small Quantity Generator or Large? Drives RPRA reporting cadence.
|
||||
5. **Certifications held** — ISO 9001? AS9100? Nadcap? CGP? Audit calendar?
|
||||
6. **Existing systems** — is there an ERP today? Paper? Spreadsheets? What has to be migrated?
|
||||
7. **Payroll / HR** — will fusion_payroll play into this or is that separate?
|
||||
8. **Stakeholders** — owner, plant manager, quality manager, environment/EHS lead, sales, accounting. Who signs off on what?
|
||||
9. **Shop-floor reality** — are operators comfortable with tablets? Rugged kiosks? Barcode / RFID?
|
||||
10. **Integrations** — accounting (Odoo core?), customer EDI, shipping (Canada Post / courier APIs?), laboratory LIMS for bath analyses?
|
||||
|
||||
---
|
||||
|
||||
## 6. Sources
|
||||
|
||||
- [Ontario Environmental Compliance Approval guide](https://www.ontario.ca/page/environmental-compliance-approval)
|
||||
- [Guide to Applying for an Environmental Compliance Approval (PDF)](https://dr6j45jk9xcmk.cloudfront.net/documents/962/5-8-2-eca-guide-en.pdf)
|
||||
- [Toronto Municipal Code Chapter 681, Sewers (PDF, 15 May 2023)](https://www.toronto.ca/legdocs/municode/1184_681.pdf)
|
||||
- [Toronto Sewers By-law program page](https://www.toronto.ca/services-payments/water-environment/water-sewer-related-permits-and-bylaws/sewers-by-law/)
|
||||
- [Toronto Pollution Prevention (P2) Program](https://www.toronto.ca/services-payments/water-environment/water-sewer-related-permits-and-bylaws/sewers-by-law/pollution-prevention-p2-program/)
|
||||
- [Ontario Reg. 347 Registration Guidance Manual](https://www.ontario.ca/document/registration-guidance-manual-generators-liquid-industrial-and-hazardous-waste)
|
||||
- [RPRA Hazardous Waste Program Registry](https://rpra.ca/programs/hwp/)
|
||||
- [Guide to the Designated Substances Regulation (O. Reg. 490/09)](https://www.ontario.ca/document/guide-designated-substances-workplace/overview-regulation) — note: nickel is not designated
|
||||
- [Ontario OELs under Regulation 833](https://www.ontario.ca/page/current-occupational-exposure-limits-ontario-workplaces-under-regulation-833)
|
||||
- [CEPA Schedule 1 — oxidic, sulphidic, soluble inorganic nickel compounds](https://www.canada.ca/en/environment-climate-change/services/management-toxic-substances/list-canadian-environmental-protection-act/oxidic-sulphidic-soluble-inorganic-nickel.html)
|
||||
- [NPRI Substance list by threshold](https://www.canada.ca/en/environment-climate-change/services/national-pollutant-release-inventory/substances-list/threshold.html)
|
||||
- [CCOHS Transportation of Dangerous Goods overview](https://www.ccohs.ca/oshanswers/legisl/tdg/tdg_overview.html)
|
||||
- [Nadcap AC7108 Electroless Plating audit criteria — supplier references](https://www.newmethodplating.com/nadcap-accreditation-and-as7108-certification-for-chemical-processing/)
|
||||
|
||||
---
|
||||
|
||||
**This is a working reference. Verify all limits, thresholds and retention periods with an Ontario-licensed environmental consultant before incorporating them into a signed proposal or compliance undertaking.**
|
||||
121
fusion-plating/fusion_plating/README.md
Normal file
121
fusion-plating/fusion_plating/README.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Fusion Plating
|
||||
|
||||
**Core module of the Fusion Plating product family.**
|
||||
A configurable, multi-tenant capable ERP for plating and metal-finishing shops,
|
||||
built for Odoo 19 Community **and** Enterprise.
|
||||
|
||||
Copyright © 2026 Nexa Systems Inc.
|
||||
License: OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
---
|
||||
|
||||
## What this module is
|
||||
|
||||
`fusion_plating` is the **process-agnostic foundation** that every plating or
|
||||
metal-finishing shop needs, regardless of size, jurisdiction, process mix, or
|
||||
industry. It provides:
|
||||
|
||||
- **Facility** — physical sites with their own tanks, operators, capabilities
|
||||
- **Process Type** — extensible taxonomy (filled in by process packs)
|
||||
- **Work Center** — lines and stations inside a facility
|
||||
- **Tank** — physical vessel with QR code, state, bath history
|
||||
- **Bath** — the chemistry currently in a tank, with its own lifecycle
|
||||
- **Bath Parameter** — schema for chemistry readings
|
||||
- **Bath Log** — daily/per-shift chemistry readings with pass/warn/fail rollup
|
||||
- **Security** — Operator / Supervisor / Manager / Administrator roles
|
||||
- **Theme-aware UI** — respects Odoo light/dark mode with zero duplication
|
||||
|
||||
## What this module is **not**
|
||||
|
||||
This core intentionally ships with:
|
||||
|
||||
- **No process chemistry** — install `fusion_plating_process_en`, `_chrome`,
|
||||
`_anodize`, `_black_oxide` etc. to get actual process types and their
|
||||
bath parameter schemas.
|
||||
- **No regulatory data** — install `fusion_plating_compliance_<region>` to
|
||||
get jurisdiction-specific limits, forms, and reporting workflows.
|
||||
- **No industry specialisations** — install `fusion_plating_aerospace`,
|
||||
`_nuclear`, `_cgp` etc. for industry-specific QMS overlays.
|
||||
- **No client-specific strings** — everything is data-driven.
|
||||
|
||||
## Product family
|
||||
|
||||
| Module | Purpose | Status |
|
||||
| --- | --- | --- |
|
||||
| `fusion_plating` | Core (this module) | **MVP** |
|
||||
| `fusion_plating_quality` | QMS: NCR, CAPA, doc control, calibration, CoC | planned |
|
||||
| `fusion_plating_compliance` | Generic compliance framework | planned |
|
||||
| `fusion_plating_compliance_on` | Ontario regulatory pack | planned |
|
||||
| `fusion_plating_compliance_tor` | Toronto Ch. 681 municipal pack | planned |
|
||||
| `fusion_plating_safety` | SDS, WHMIS/TDG, JHSC, exposure | planned |
|
||||
| `fusion_plating_shopfloor` | Tablet operator stations, QR scanning, bake-window enforcer | planned |
|
||||
| `fusion_plating_portal` | Customer portal | planned |
|
||||
| `fusion_plating_process_en` | Electroless nickel — low/mid/high phos | planned |
|
||||
| `fusion_plating_process_chrome` | Chrome coating (hex & trivalent) | planned |
|
||||
| `fusion_plating_process_anodize` | Aluminum anodizing (Type II, III) | planned |
|
||||
| `fusion_plating_process_black_oxide` | Black oxidizing | planned |
|
||||
| `fusion_plating_aerospace` | AS9100 + Nadcap AC7108 | planned |
|
||||
| `fusion_plating_nuclear` | CSA N299, CNSC, NQA-1 | planned |
|
||||
| `fusion_plating_cgp` | Controlled Goods Program | planned |
|
||||
| `fusion_plating_logistics` | Pickup & delivery routing | planned |
|
||||
| `fusion_plating_culture` | Values / fundamentals framework | planned |
|
||||
| `fusion_plating_bridge_sign` | EE bridge: e-sign CoC acceptance | planned |
|
||||
| `fusion_plating_bridge_documents` | EE bridge: Documents workspace | planned |
|
||||
| `fusion_plating_bridge_quality` | EE bridge: native `quality` module | planned |
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Development
|
||||
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating --stop-after-init
|
||||
|
||||
# Production — after rsync to target server
|
||||
docker exec <odoo-container> odoo -d <db> -u fusion_plating --stop-after-init
|
||||
```
|
||||
|
||||
No external Python dependencies. Depends only on standard Odoo 19 Community
|
||||
base modules (`base`, `mail`, `contacts`, `product`, `stock`, `sale_management`,
|
||||
`purchase`, `hr`, `uom`).
|
||||
|
||||
## Design principles
|
||||
|
||||
1. **Works on both Odoo Community and Enterprise.** Never depends on
|
||||
`quality`, `documents`, `sign`, `studio`, or `mrp_plm`. EE-specific
|
||||
integrations live in separate `fusion_plating_bridge_*` modules.
|
||||
2. **No client-specific strings in core.** Configuration, not code.
|
||||
3. **Regions are data, not code.** Sewer limits, waste classes, reporting
|
||||
forms come from region packs.
|
||||
4. **Processes are plug-ins.** New process (copper, zinc, tin) = new
|
||||
`fusion_plating_process_*` module, core untouched.
|
||||
5. **Dashboards are configured, not coded.** Shops pick their own headline KPIs.
|
||||
6. **Theme-aware.** Uses Odoo/Bootstrap CSS variables. One source of truth
|
||||
for colours; Odoo's theme engine decides light vs dark.
|
||||
|
||||
## Security groups
|
||||
|
||||
| Group | Intended for |
|
||||
| --- | --- |
|
||||
| **Operator** | Shop-floor staff. Reads reference data, writes chemistry logs. |
|
||||
| **Supervisor** | Line supervisors. Manages baths, schedules jobs, reviews logs. |
|
||||
| **Manager** | Quality, EHS, plant manager, engineer. Full CRUD on configuration. |
|
||||
| **Administrator** | Owner, system admin. All manager rights + system settings. |
|
||||
|
||||
## Field naming convention
|
||||
|
||||
- New models use `fusion.plating.*` namespace.
|
||||
- Fields on our own models use simple names (no prefix).
|
||||
- Fields added to base Odoo models (`res.company`, `res.partner`,
|
||||
`product.template`, etc.) use the `x_fc_` prefix per the repo convention.
|
||||
|
||||
## Developer notes
|
||||
|
||||
- All models inheriting from `mail.thread` use the Odoo 19 chatter pattern.
|
||||
- Security follows the Odoo 19 `res.groups.privilege` pattern (module
|
||||
category → privilege → groups), not the legacy `category_id`-on-group
|
||||
pattern.
|
||||
- Sequence numbers use `ir.sequence` seeded in `data/fp_sequence_data.xml`.
|
||||
- SCSS uses `color-mix()` against CSS custom properties — never hardcodes
|
||||
hex values. See `static/src/scss/fusion_plating.scss` for the theming
|
||||
contract.
|
||||
- No `group expand="0"` in search views (Odoo 19 incompatibility).
|
||||
- No `category_id` or `users` field on `res.groups` (Odoo 19 incompatibility).
|
||||
6
fusion-plating/fusion_plating/__init__.py
Normal file
6
fusion-plating/fusion_plating/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from . import models
|
||||
106
fusion-plating/fusion_plating/__manifest__.py
Normal file
106
fusion-plating/fusion_plating/__manifest__.py
Normal file
@@ -0,0 +1,106 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating',
|
||||
'version': '19.0.1.0.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
||||
'description': """
|
||||
Fusion Plating — Core
|
||||
=====================
|
||||
|
||||
Part of the Fusion Plating product family by Nexa Systems Inc.
|
||||
|
||||
Fusion Plating is a configurable, multi-tenant capable ERP for plating and metal
|
||||
finishing shops. This core module provides the process-agnostic foundation that
|
||||
every shop needs regardless of size, process mix, jurisdiction, or industry.
|
||||
|
||||
The core ships intentionally empty of region-specific or process-specific
|
||||
content — that comes from add-on modules:
|
||||
|
||||
* fusion_plating_process_en — Electroless nickel plating
|
||||
* fusion_plating_process_chrome — Chrome coating (hex or trivalent)
|
||||
* fusion_plating_process_anodize — Aluminum anodizing (Type II, III)
|
||||
* fusion_plating_process_black_oxide — Black oxidizing
|
||||
* fusion_plating_quality — QMS (NCR, CAPA, calibration, CoC, doc control)
|
||||
* fusion_plating_compliance — Generic compliance framework
|
||||
* fusion_plating_compliance_on — Ontario regulatory pack
|
||||
* fusion_plating_compliance_tor — Toronto Ch. 681 municipal pack
|
||||
* fusion_plating_safety — SDS, WHMIS/TDG training, JHSC, exposure
|
||||
* fusion_plating_shopfloor — Tablet operator stations, QR scanning
|
||||
* fusion_plating_portal — Customer portal
|
||||
* fusion_plating_aerospace — AS9100 + Nadcap AC7108 pack
|
||||
* fusion_plating_nuclear — CSA N299, CNSC, NQA-1 pack
|
||||
* fusion_plating_cgp — Controlled Goods Program pack
|
||||
* fusion_plating_logistics — Pickup & delivery
|
||||
* fusion_plating_culture — Values / fundamentals framework
|
||||
|
||||
Core concepts
|
||||
-------------
|
||||
* Facility — a physical site with its own tanks, operators, compliance profile
|
||||
* Process Type — extensible taxonomy of finishing processes
|
||||
* Work Center — production line or station within a facility
|
||||
* Tank — physical vessel with QR code and state
|
||||
* Bath — the chemistry currently in a tank, with its own lifecycle
|
||||
* Bath Log — daily chemistry readings with pass/fail vs target
|
||||
* KPI — configurable headline metrics per shop
|
||||
* Delegation Inbox — single pane of "things waiting for someone"
|
||||
|
||||
Design principles
|
||||
-----------------
|
||||
1. No client-specific strings in core.
|
||||
2. No region-specific data in core.
|
||||
3. No process-specific chemistry in core.
|
||||
4. Works on both Odoo Community and Enterprise editions.
|
||||
5. Theme-aware: respects user light/dark mode preference.
|
||||
6. Multi-facility, multi-company, multi-currency capable.
|
||||
|
||||
Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
""",
|
||||
'author': 'Nexa Systems Inc.',
|
||||
'website': 'https://www.nexasystems.ca',
|
||||
'maintainer': 'Nexa Systems Inc.',
|
||||
'support': 'support@nexasystems.ca',
|
||||
'license': 'OPL-1',
|
||||
'price': 0.00,
|
||||
'currency': 'CAD',
|
||||
'depends': [
|
||||
'base',
|
||||
'mail',
|
||||
'contacts',
|
||||
'product',
|
||||
'stock',
|
||||
'sale_management',
|
||||
'purchase',
|
||||
'hr',
|
||||
'uom',
|
||||
],
|
||||
'data': [
|
||||
'security/fp_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/fp_sequence_data.xml',
|
||||
'data/fp_process_category_data.xml',
|
||||
'views/fp_process_type_views.xml',
|
||||
'views/fp_work_center_views.xml',
|
||||
'views/fp_tank_views.xml',
|
||||
'views/fp_bath_log_views.xml',
|
||||
'views/fp_facility_views.xml',
|
||||
'views/fp_bath_views.xml',
|
||||
'views/fp_menu.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'fusion_plating/static/src/scss/fusion_plating.scss',
|
||||
],
|
||||
},
|
||||
'demo': [
|
||||
'data/fp_demo_data.xml',
|
||||
],
|
||||
'images': ['static/description/icon.png'],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': True,
|
||||
}
|
||||
322
fusion-plating/fusion_plating/data/fp_demo_data.xml
Normal file
322
fusion-plating/fusion_plating/data/fp_demo_data.xml
Normal file
@@ -0,0 +1,322 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc. — DEMO DATA (temporary)
|
||||
Remove this file and its manifest entry before production release.
|
||||
-->
|
||||
<odoo noupdate="1">
|
||||
|
||||
<!-- ========== DEMO PARTNERS ========== -->
|
||||
<record id="demo_partner_aeroparts" model="res.partner">
|
||||
<field name="name">AeroParts Manufacturing Inc.</field>
|
||||
<field name="email">info@aeroparts.ca</field>
|
||||
<field name="phone">905-555-0101</field>
|
||||
<field name="city">Mississauga</field>
|
||||
<field name="country_id" ref="base.ca"/>
|
||||
<field name="company_type">company</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_partner_precision" model="res.partner">
|
||||
<field name="name">Precision MFG Ltd.</field>
|
||||
<field name="email">orders@precisionmfg.ca</field>
|
||||
<field name="phone">416-555-0202</field>
|
||||
<field name="city">Toronto</field>
|
||||
<field name="country_id" ref="base.ca"/>
|
||||
<field name="company_type">company</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_partner_opg" model="res.partner">
|
||||
<field name="name">Ontario Power Generation</field>
|
||||
<field name="email">procurement@opg.com</field>
|
||||
<field name="phone">905-555-0303</field>
|
||||
<field name="city">Pickering</field>
|
||||
<field name="country_id" ref="base.ca"/>
|
||||
<field name="company_type">company</field>
|
||||
</record>
|
||||
|
||||
<!-- ========== FACILITIES ========== -->
|
||||
<record id="demo_facility_main" model="fusion.plating.facility">
|
||||
<field name="name">Fusion Plating — Main Plant</field>
|
||||
<field name="code">FP-MAIN</field>
|
||||
<field name="sequence">10</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_facility_east" model="fusion.plating.facility">
|
||||
<field name="name">Fusion Plating — East Annex</field>
|
||||
<field name="code">FP-EAST</field>
|
||||
<field name="sequence">20</field>
|
||||
</record>
|
||||
|
||||
<!-- ========== WORK CENTRES ========== -->
|
||||
<record id="demo_wc_en_line" model="fusion.plating.work.center">
|
||||
<field name="name">EN Plating Line</field>
|
||||
<field name="code">WC-EN</field>
|
||||
<field name="facility_id" ref="demo_facility_main"/>
|
||||
<field name="capacity_per_day">80</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_wc_chrome_line" model="fusion.plating.work.center">
|
||||
<field name="name">Chrome Line</field>
|
||||
<field name="code">WC-CR</field>
|
||||
<field name="facility_id" ref="demo_facility_main"/>
|
||||
<field name="capacity_per_day">50</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_wc_anodize_line" model="fusion.plating.work.center">
|
||||
<field name="name">Anodize Line</field>
|
||||
<field name="code">WC-AN</field>
|
||||
<field name="facility_id" ref="demo_facility_main"/>
|
||||
<field name="capacity_per_day">120</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_wc_oxide_line" model="fusion.plating.work.center">
|
||||
<field name="name">Black Oxide Line</field>
|
||||
<field name="code">WC-BOX</field>
|
||||
<field name="facility_id" ref="demo_facility_east"/>
|
||||
<field name="capacity_per_day">60</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_wc_prep_line" model="fusion.plating.work.center">
|
||||
<field name="name">Prep & Clean Line</field>
|
||||
<field name="code">WC-PREP</field>
|
||||
<field name="facility_id" ref="demo_facility_main"/>
|
||||
<field name="capacity_per_day">200</field>
|
||||
</record>
|
||||
|
||||
<!-- ========== TANKS ========== -->
|
||||
<!-- EN Line -->
|
||||
<record id="demo_tank_en1" model="fusion.plating.tank">
|
||||
<field name="name">EN Tank 1 — Mid-Phos</field>
|
||||
<field name="code">T-EN-01</field>
|
||||
<field name="facility_id" ref="demo_facility_main"/>
|
||||
<field name="work_center_id" ref="demo_wc_en_line"/>
|
||||
<field name="current_process_id" ref="fusion_plating_process_en.ptype_en_mp"/>
|
||||
<field name="volume">800</field>
|
||||
<field name="volume_uom">l</field>
|
||||
<field name="material">polypro</field>
|
||||
<field name="heating_type">immersion</field>
|
||||
<field name="has_filtration" eval="True"/>
|
||||
<field name="state">in_use</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_tank_en2" model="fusion.plating.tank">
|
||||
<field name="name">EN Tank 2 — High-Phos</field>
|
||||
<field name="code">T-EN-02</field>
|
||||
<field name="facility_id" ref="demo_facility_main"/>
|
||||
<field name="work_center_id" ref="demo_wc_en_line"/>
|
||||
<field name="current_process_id" ref="fusion_plating_process_en.ptype_en_hp"/>
|
||||
<field name="volume">600</field>
|
||||
<field name="volume_uom">l</field>
|
||||
<field name="material">polypro</field>
|
||||
<field name="heating_type">immersion</field>
|
||||
<field name="has_filtration" eval="True"/>
|
||||
<field name="state">in_use</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_tank_en_strike" model="fusion.plating.tank">
|
||||
<field name="name">EN Strike Tank</field>
|
||||
<field name="code">T-EN-STK</field>
|
||||
<field name="facility_id" ref="demo_facility_main"/>
|
||||
<field name="work_center_id" ref="demo_wc_en_line"/>
|
||||
<field name="current_process_id" ref="fusion_plating_process_en.ptype_en_strike"/>
|
||||
<field name="volume">300</field>
|
||||
<field name="volume_uom">l</field>
|
||||
<field name="material">polypro</field>
|
||||
<field name="state">in_use</field>
|
||||
</record>
|
||||
|
||||
<!-- Chrome Line -->
|
||||
<record id="demo_tank_cr1" model="fusion.plating.tank">
|
||||
<field name="name">Hard Chrome Tank 1</field>
|
||||
<field name="code">T-CR-01</field>
|
||||
<field name="facility_id" ref="demo_facility_main"/>
|
||||
<field name="work_center_id" ref="demo_wc_chrome_line"/>
|
||||
<field name="current_process_id" ref="fusion_plating_process_chrome.ptype_cr_hard_hex"/>
|
||||
<field name="volume">1200</field>
|
||||
<field name="volume_uom">l</field>
|
||||
<field name="material">lined_steel</field>
|
||||
<field name="heating_type">immersion</field>
|
||||
<field name="has_rectifier" eval="True"/>
|
||||
<field name="has_filtration" eval="True"/>
|
||||
<field name="state">in_use</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_tank_cr2" model="fusion.plating.tank">
|
||||
<field name="name">Decorative Chrome Tank</field>
|
||||
<field name="code">T-CR-02</field>
|
||||
<field name="facility_id" ref="demo_facility_main"/>
|
||||
<field name="work_center_id" ref="demo_wc_chrome_line"/>
|
||||
<field name="current_process_id" ref="fusion_plating_process_chrome.ptype_cr_dec_hex"/>
|
||||
<field name="volume">500</field>
|
||||
<field name="volume_uom">l</field>
|
||||
<field name="material">lined_steel</field>
|
||||
<field name="heating_type">immersion</field>
|
||||
<field name="has_rectifier" eval="True"/>
|
||||
<field name="state">in_use</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_tank_cr_strike" model="fusion.plating.tank">
|
||||
<field name="name">Chrome Strike Tank</field>
|
||||
<field name="code">T-CR-STK</field>
|
||||
<field name="facility_id" ref="demo_facility_main"/>
|
||||
<field name="work_center_id" ref="demo_wc_chrome_line"/>
|
||||
<field name="current_process_id" ref="fusion_plating_process_chrome.ptype_cr_strike"/>
|
||||
<field name="volume">200</field>
|
||||
<field name="volume_uom">l</field>
|
||||
<field name="material">polypro</field>
|
||||
<field name="has_rectifier" eval="True"/>
|
||||
<field name="state">in_use</field>
|
||||
</record>
|
||||
|
||||
<!-- Anodize Line -->
|
||||
<record id="demo_tank_an1" model="fusion.plating.tank">
|
||||
<field name="name">Type II Sulfuric Anodize</field>
|
||||
<field name="code">T-AN-01</field>
|
||||
<field name="facility_id" ref="demo_facility_main"/>
|
||||
<field name="work_center_id" ref="demo_wc_anodize_line"/>
|
||||
<field name="current_process_id" ref="fusion_plating_process_anodize.ptype_an_type_ii"/>
|
||||
<field name="volume">2000</field>
|
||||
<field name="volume_uom">l</field>
|
||||
<field name="material">polypro</field>
|
||||
<field name="heating_type">jacket</field>
|
||||
<field name="has_rectifier" eval="True"/>
|
||||
<field name="has_filtration" eval="True"/>
|
||||
<field name="state">in_use</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_tank_an2" model="fusion.plating.tank">
|
||||
<field name="name">Type III Hardcoat Anodize</field>
|
||||
<field name="code">T-AN-02</field>
|
||||
<field name="facility_id" ref="demo_facility_main"/>
|
||||
<field name="work_center_id" ref="demo_wc_anodize_line"/>
|
||||
<field name="current_process_id" ref="fusion_plating_process_anodize.ptype_an_type_iii"/>
|
||||
<field name="volume">1500</field>
|
||||
<field name="volume_uom">l</field>
|
||||
<field name="material">polypro</field>
|
||||
<field name="heating_type">jacket</field>
|
||||
<field name="has_rectifier" eval="True"/>
|
||||
<field name="has_filtration" eval="True"/>
|
||||
<field name="state">in_use</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_tank_an_seal" model="fusion.plating.tank">
|
||||
<field name="name">Hot Water Seal Tank</field>
|
||||
<field name="code">T-AN-SEAL</field>
|
||||
<field name="facility_id" ref="demo_facility_main"/>
|
||||
<field name="work_center_id" ref="demo_wc_anodize_line"/>
|
||||
<field name="current_process_id" ref="fusion_plating_process_anodize.ptype_an_seal_hot"/>
|
||||
<field name="volume">1000</field>
|
||||
<field name="volume_uom">l</field>
|
||||
<field name="material">ss</field>
|
||||
<field name="heating_type">immersion</field>
|
||||
<field name="state">in_use</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_tank_an_dye" model="fusion.plating.tank">
|
||||
<field name="name">Dye Immersion Tank — Black</field>
|
||||
<field name="code">T-AN-DYE</field>
|
||||
<field name="facility_id" ref="demo_facility_main"/>
|
||||
<field name="work_center_id" ref="demo_wc_anodize_line"/>
|
||||
<field name="current_process_id" ref="fusion_plating_process_anodize.ptype_an_dye"/>
|
||||
<field name="volume">500</field>
|
||||
<field name="volume_uom">l</field>
|
||||
<field name="material">polypro</field>
|
||||
<field name="state">in_use</field>
|
||||
</record>
|
||||
|
||||
<!-- Black Oxide Line (East) -->
|
||||
<record id="demo_tank_box1" model="fusion.plating.tank">
|
||||
<field name="name">Hot Black Oxide Tank</field>
|
||||
<field name="code">T-BOX-01</field>
|
||||
<field name="facility_id" ref="demo_facility_east"/>
|
||||
<field name="work_center_id" ref="demo_wc_oxide_line"/>
|
||||
<field name="current_process_id" ref="fusion_plating_process_black_oxide.ptype_box_hot"/>
|
||||
<field name="volume">400</field>
|
||||
<field name="volume_uom">l</field>
|
||||
<field name="material">ss</field>
|
||||
<field name="heating_type">external</field>
|
||||
<field name="state">in_use</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_tank_box_seal" model="fusion.plating.tank">
|
||||
<field name="name">Sealing Oil Dip</field>
|
||||
<field name="code">T-BOX-SEAL</field>
|
||||
<field name="facility_id" ref="demo_facility_east"/>
|
||||
<field name="work_center_id" ref="demo_wc_oxide_line"/>
|
||||
<field name="current_process_id" ref="fusion_plating_process_black_oxide.ptype_box_seal_oil"/>
|
||||
<field name="volume">300</field>
|
||||
<field name="volume_uom">l</field>
|
||||
<field name="material">ss</field>
|
||||
<field name="state">in_use</field>
|
||||
</record>
|
||||
|
||||
<!-- Maintenance tank -->
|
||||
<record id="demo_tank_maint" model="fusion.plating.tank">
|
||||
<field name="name">Rinse Tank 3 (Down for Repair)</field>
|
||||
<field name="code">T-RN-03</field>
|
||||
<field name="facility_id" ref="demo_facility_main"/>
|
||||
<field name="work_center_id" ref="demo_wc_prep_line"/>
|
||||
<field name="volume">400</field>
|
||||
<field name="volume_uom">l</field>
|
||||
<field name="material">polypro</field>
|
||||
<field name="state">maintenance</field>
|
||||
</record>
|
||||
|
||||
<!-- ========== BATHS ========== -->
|
||||
<record id="demo_bath_en_mp" model="fusion.plating.bath">
|
||||
<field name="name">EN Mid-Phos Bath A</field>
|
||||
<field name="tank_id" ref="demo_tank_en1"/>
|
||||
<field name="facility_id" ref="demo_facility_main"/>
|
||||
<field name="process_type_id" ref="fusion_plating_process_en.ptype_en_mp"/>
|
||||
<field name="state">operational</field>
|
||||
<field name="makeup_date" eval="(DateTime.today() - timedelta(days=14)).strftime('%Y-%m-%d')"/>
|
||||
</record>
|
||||
|
||||
<record id="demo_bath_en_hp" model="fusion.plating.bath">
|
||||
<field name="name">EN High-Phos Bath B</field>
|
||||
<field name="tank_id" ref="demo_tank_en2"/>
|
||||
<field name="facility_id" ref="demo_facility_main"/>
|
||||
<field name="process_type_id" ref="fusion_plating_process_en.ptype_en_hp"/>
|
||||
<field name="state">operational</field>
|
||||
<field name="makeup_date" eval="(DateTime.today() - timedelta(days=30)).strftime('%Y-%m-%d')"/>
|
||||
</record>
|
||||
|
||||
<record id="demo_bath_cr_hard" model="fusion.plating.bath">
|
||||
<field name="name">Hard Chrome Bath 1</field>
|
||||
<field name="tank_id" ref="demo_tank_cr1"/>
|
||||
<field name="facility_id" ref="demo_facility_main"/>
|
||||
<field name="process_type_id" ref="fusion_plating_process_chrome.ptype_cr_hard_hex"/>
|
||||
<field name="state">operational</field>
|
||||
<field name="makeup_date" eval="(DateTime.today() - timedelta(days=60)).strftime('%Y-%m-%d')"/>
|
||||
</record>
|
||||
|
||||
<record id="demo_bath_an_typeii" model="fusion.plating.bath">
|
||||
<field name="name">Sulfuric Anodize Bath</field>
|
||||
<field name="tank_id" ref="demo_tank_an1"/>
|
||||
<field name="facility_id" ref="demo_facility_main"/>
|
||||
<field name="process_type_id" ref="fusion_plating_process_anodize.ptype_an_type_ii"/>
|
||||
<field name="state">operational</field>
|
||||
<field name="makeup_date" eval="(DateTime.today() - timedelta(days=7)).strftime('%Y-%m-%d')"/>
|
||||
</record>
|
||||
|
||||
<record id="demo_bath_box_hot" model="fusion.plating.bath">
|
||||
<field name="name">Hot Black Oxide Bath</field>
|
||||
<field name="tank_id" ref="demo_tank_box1"/>
|
||||
<field name="facility_id" ref="demo_facility_east"/>
|
||||
<field name="process_type_id" ref="fusion_plating_process_black_oxide.ptype_box_hot"/>
|
||||
<field name="state">operational</field>
|
||||
<field name="makeup_date" eval="(DateTime.today() - timedelta(days=45)).strftime('%Y-%m-%d')"/>
|
||||
</record>
|
||||
|
||||
<!-- Aged bath nearing dump -->
|
||||
<record id="demo_bath_cr_dec" model="fusion.plating.bath">
|
||||
<field name="name">Decorative Chrome Bath (aging)</field>
|
||||
<field name="tank_id" ref="demo_tank_cr2"/>
|
||||
<field name="facility_id" ref="demo_facility_main"/>
|
||||
<field name="process_type_id" ref="fusion_plating_process_chrome.ptype_cr_dec_hex"/>
|
||||
<field name="state">dump_scheduled</field>
|
||||
<field name="makeup_date" eval="(DateTime.today() - timedelta(days=180)).strftime('%Y-%m-%d')"/>
|
||||
<field name="dump_scheduled_date" eval="(DateTime.today() + timedelta(days=10)).strftime('%Y-%m-%d')"/>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,70 @@
|
||||
<?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.
|
||||
|
||||
Seed process categories. Categories are the one pinch of generic
|
||||
taxonomy core ships with — specific process types themselves are
|
||||
loaded by process packs (fusion_plating_process_en, etc.).
|
||||
-->
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="pcat_plating" model="fusion.plating.process.category">
|
||||
<field name="name">Plating</field>
|
||||
<field name="code">plating</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="description">Deposition of a metallic layer onto a substrate, either electrolytically or autocatalytically.</field>
|
||||
</record>
|
||||
|
||||
<record id="pcat_anodizing" model="fusion.plating.process.category">
|
||||
<field name="name">Anodizing</field>
|
||||
<field name="code">anodizing</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="description">Electrochemical conversion of a substrate surface into an oxide layer (typically aluminum).</field>
|
||||
</record>
|
||||
|
||||
<record id="pcat_coating" model="fusion.plating.process.category">
|
||||
<field name="name">Coating</field>
|
||||
<field name="code">coating</field>
|
||||
<field name="sequence">30</field>
|
||||
<field name="description">Non-metallic or hybrid surface coating (paint, powder, PTFE composite, etc.).</field>
|
||||
</record>
|
||||
|
||||
<record id="pcat_conversion" model="fusion.plating.process.category">
|
||||
<field name="name">Conversion Coating</field>
|
||||
<field name="code">conversion</field>
|
||||
<field name="sequence">40</field>
|
||||
<field name="description">Chemical reaction forming a protective film from the substrate itself (chromate, phosphate, black oxide).</field>
|
||||
</record>
|
||||
|
||||
<record id="pcat_prep" model="fusion.plating.process.category">
|
||||
<field name="name">Preparation</field>
|
||||
<field name="code">prep</field>
|
||||
<field name="sequence">50</field>
|
||||
<field name="description">Cleaning, degreasing, etching, activation — surface prep before the main finishing step.</field>
|
||||
</record>
|
||||
|
||||
<record id="pcat_strip" model="fusion.plating.process.category">
|
||||
<field name="name">Stripping</field>
|
||||
<field name="code">strip</field>
|
||||
<field name="sequence">60</field>
|
||||
<field name="description">Chemical or electrolytic removal of an existing coating.</field>
|
||||
</record>
|
||||
|
||||
<record id="pcat_post" model="fusion.plating.process.category">
|
||||
<field name="name">Post-Treatment</field>
|
||||
<field name="code">post</field>
|
||||
<field name="sequence">70</field>
|
||||
<field name="description">Sealing, dyeing, heat treatment, embrittlement relief, passivation.</field>
|
||||
</record>
|
||||
|
||||
<record id="pcat_other" model="fusion.plating.process.category">
|
||||
<field name="name">Other</field>
|
||||
<field name="code">other</field>
|
||||
<field name="sequence">100</field>
|
||||
<field name="description">Catch-all for processes that do not fit the standard categories.</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
26
fusion-plating/fusion_plating/data/fp_sequence_data.xml
Normal file
26
fusion-plating/fusion_plating/data/fp_sequence_data.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="seq_fp_bath" model="ir.sequence">
|
||||
<field name="name">Fusion Plating: Bath</field>
|
||||
<field name="code">fusion.plating.bath</field>
|
||||
<field name="prefix">BATH/%(year)s/</field>
|
||||
<field name="padding">5</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="seq_fp_bath_log" model="ir.sequence">
|
||||
<field name="name">Fusion Plating: Bath Log</field>
|
||||
<field name="code">fusion.plating.bath.log</field>
|
||||
<field name="prefix">BLOG/%(year)s%(month)s/</field>
|
||||
<field name="padding">6</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
15
fusion-plating/fusion_plating/models/__init__.py
Normal file
15
fusion-plating/fusion_plating/models/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from . import fp_process_category
|
||||
from . import fp_process_type
|
||||
from . import fp_facility
|
||||
from . import fp_work_center
|
||||
from . import fp_tank
|
||||
from . import fp_bath
|
||||
from . import fp_bath_log
|
||||
from . import fp_bath_log_line
|
||||
from . import fp_bath_parameter
|
||||
from . import res_company
|
||||
269
fusion-plating/fusion_plating/models/fp_bath.py
Normal file
269
fusion-plating/fusion_plating/models/fp_bath.py
Normal file
@@ -0,0 +1,269 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class FpBath(models.Model):
|
||||
"""A specific batch of chemistry in a tank.
|
||||
|
||||
Baths have their own lifecycle independent of the tank:
|
||||
|
||||
new → operational → under_review → dump_scheduled → dumped
|
||||
|
||||
Each bath carries:
|
||||
* its process type (which chemistry it runs)
|
||||
* per-bath target ranges (may override process defaults)
|
||||
* running MTO counter (set and maintained by the process pack)
|
||||
* chemistry log history (one2many to fusion.plating.bath.log)
|
||||
|
||||
Process packs (fusion_plating_process_en, etc.) add process-specific
|
||||
computed fields such as orthophosphite projection or P-content band
|
||||
without touching the generic bath model.
|
||||
"""
|
||||
_name = 'fusion.plating.bath'
|
||||
_description = 'Fusion Plating — Bath'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'state, makeup_date desc, id desc'
|
||||
_rec_name = 'display_name'
|
||||
|
||||
name = fields.Char(
|
||||
string='Reference',
|
||||
required=True,
|
||||
copy=False,
|
||||
default=lambda self: self._default_name(),
|
||||
tracking=True,
|
||||
)
|
||||
display_name = fields.Char(
|
||||
compute='_compute_display_name',
|
||||
store=True,
|
||||
)
|
||||
tank_id = fields.Many2one(
|
||||
'fusion.plating.tank',
|
||||
string='Tank',
|
||||
required=True,
|
||||
ondelete='restrict',
|
||||
tracking=True,
|
||||
)
|
||||
facility_id = fields.Many2one(
|
||||
'fusion.plating.facility',
|
||||
related='tank_id.facility_id',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
process_type_id = fields.Many2one(
|
||||
'fusion.plating.process.type',
|
||||
string='Process',
|
||||
required=True,
|
||||
ondelete='restrict',
|
||||
tracking=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
related='facility_id.company_id',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
# ----- Lifecycle ------------------------------------------------------
|
||||
state = fields.Selection(
|
||||
[
|
||||
('new', 'New'),
|
||||
('operational', 'Operational'),
|
||||
('under_review', 'Under Review'),
|
||||
('dump_scheduled', 'Dump Scheduled'),
|
||||
('dumped', 'Dumped'),
|
||||
],
|
||||
string='Status',
|
||||
default='new',
|
||||
tracking=True,
|
||||
required=True,
|
||||
)
|
||||
status_color = fields.Integer(
|
||||
string='Status Color',
|
||||
compute='_compute_status_color',
|
||||
help='Kanban colour index derived from state and chemistry health.',
|
||||
)
|
||||
makeup_date = fields.Datetime(
|
||||
string='Makeup Date',
|
||||
help='When this bath was made up (initial fresh charge).',
|
||||
tracking=True,
|
||||
)
|
||||
makeup_by_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Made Up By',
|
||||
tracking=True,
|
||||
)
|
||||
dump_scheduled_date = fields.Datetime(
|
||||
string='Dump Scheduled',
|
||||
tracking=True,
|
||||
)
|
||||
dumped_date = fields.Datetime(
|
||||
string='Dumped Date',
|
||||
tracking=True,
|
||||
)
|
||||
dump_reason = fields.Text(
|
||||
string='Dump Reason',
|
||||
)
|
||||
notes = fields.Html(
|
||||
string='Notes',
|
||||
)
|
||||
|
||||
# ----- Chemistry target ranges (per-bath; override process defaults) --
|
||||
target_line_ids = fields.One2many(
|
||||
'fusion.plating.bath.target',
|
||||
'bath_id',
|
||||
string='Target Parameters',
|
||||
copy=True,
|
||||
)
|
||||
|
||||
# ----- Logs -----------------------------------------------------------
|
||||
log_ids = fields.One2many(
|
||||
'fusion.plating.bath.log',
|
||||
'bath_id',
|
||||
string='Chemistry Logs',
|
||||
)
|
||||
log_count = fields.Integer(
|
||||
compute='_compute_log_count',
|
||||
)
|
||||
last_log_date = fields.Datetime(
|
||||
compute='_compute_last_log',
|
||||
store=True,
|
||||
)
|
||||
last_log_status = fields.Selection(
|
||||
[
|
||||
('ok', 'OK'),
|
||||
('warning', 'Warning'),
|
||||
('out_of_spec', 'Out of Spec'),
|
||||
],
|
||||
compute='_compute_last_log',
|
||||
store=True,
|
||||
)
|
||||
|
||||
# ----- Generic age / volume (process packs refine) --------------------
|
||||
mto_count = fields.Float(
|
||||
string='MTO',
|
||||
default=0.0,
|
||||
help='Metal Turnovers. Maintained by process packs that model '
|
||||
'replenishment (e.g. fusion_plating_process_en).',
|
||||
)
|
||||
volume = fields.Float(
|
||||
string='Volume',
|
||||
help='Working volume (defaults to tank volume on makeup).',
|
||||
)
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
# ==========================================================================
|
||||
# Defaults
|
||||
# ==========================================================================
|
||||
@api.model
|
||||
def _default_name(self):
|
||||
seq = self.env['ir.sequence'].next_by_code('fusion.plating.bath')
|
||||
return seq or '/'
|
||||
|
||||
# ==========================================================================
|
||||
# Computes
|
||||
# ==========================================================================
|
||||
@api.depends('name', 'process_type_id', 'tank_id')
|
||||
def _compute_display_name(self):
|
||||
for rec in self:
|
||||
parts = [rec.name or '']
|
||||
if rec.process_type_id:
|
||||
parts.append(f'({rec.process_type_id.code})')
|
||||
if rec.tank_id:
|
||||
parts.append(f'@ {rec.tank_id.code}')
|
||||
rec.display_name = ' '.join(p for p in parts if p)
|
||||
|
||||
def _compute_log_count(self):
|
||||
for rec in self:
|
||||
rec.log_count = len(rec.log_ids)
|
||||
|
||||
@api.depends('log_ids', 'log_ids.log_date', 'log_ids.status')
|
||||
def _compute_last_log(self):
|
||||
for rec in self:
|
||||
last = rec.log_ids.sorted('log_date', reverse=True)[:1]
|
||||
rec.last_log_date = last.log_date if last else False
|
||||
rec.last_log_status = last.status if last else False
|
||||
|
||||
@api.depends('state', 'last_log_status')
|
||||
def _compute_status_color(self):
|
||||
"""Kanban colour index — neutral palette that works in light + dark.
|
||||
|
||||
Uses Odoo's built-in color index rather than hex codes, so themes
|
||||
control the final rendering.
|
||||
"""
|
||||
# 0=no color, 4=green, 3=yellow, 2=orange, 1=red, 5=purple, 10=grey
|
||||
for rec in self:
|
||||
if rec.state == 'dumped':
|
||||
rec.status_color = 10 # grey
|
||||
elif rec.state == 'dump_scheduled':
|
||||
rec.status_color = 2 # orange
|
||||
elif rec.state == 'under_review':
|
||||
rec.status_color = 3 # yellow
|
||||
elif rec.state == 'new':
|
||||
rec.status_color = 5 # purple
|
||||
elif rec.last_log_status == 'out_of_spec':
|
||||
rec.status_color = 1 # red
|
||||
elif rec.last_log_status == 'warning':
|
||||
rec.status_color = 3 # yellow
|
||||
else:
|
||||
rec.status_color = 4 # green
|
||||
|
||||
# ==========================================================================
|
||||
# Actions
|
||||
# ==========================================================================
|
||||
def action_make_operational(self):
|
||||
self.write({'state': 'operational'})
|
||||
|
||||
def action_mark_under_review(self):
|
||||
self.write({'state': 'under_review'})
|
||||
|
||||
def action_schedule_dump(self):
|
||||
self.write({
|
||||
'state': 'dump_scheduled',
|
||||
'dump_scheduled_date': fields.Datetime.now(),
|
||||
})
|
||||
|
||||
def action_dump(self):
|
||||
self.write({
|
||||
'state': 'dumped',
|
||||
'dumped_date': fields.Datetime.now(),
|
||||
})
|
||||
|
||||
|
||||
class FpBathTarget(models.Model):
|
||||
"""Per-bath target range for a chemistry parameter."""
|
||||
_name = 'fusion.plating.bath.target'
|
||||
_description = 'Fusion Plating — Bath Target'
|
||||
_order = 'bath_id, sequence, parameter_id'
|
||||
|
||||
bath_id = fields.Many2one(
|
||||
'fusion.plating.bath',
|
||||
string='Bath',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
)
|
||||
parameter_id = fields.Many2one(
|
||||
'fusion.plating.bath.parameter',
|
||||
string='Parameter',
|
||||
required=True,
|
||||
ondelete='restrict',
|
||||
)
|
||||
sequence = fields.Integer(default=10)
|
||||
target_min = fields.Float(string='Min')
|
||||
target_max = fields.Float(string='Max')
|
||||
uom = fields.Char(
|
||||
related='parameter_id.uom',
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
'fp_bath_target_uniq',
|
||||
'unique(bath_id, parameter_id)',
|
||||
'Each parameter can only be defined once per bath.',
|
||||
),
|
||||
]
|
||||
144
fusion-plating/fusion_plating/models/fp_bath_log.py
Normal file
144
fusion-plating/fusion_plating/models/fp_bath_log.py
Normal file
@@ -0,0 +1,144 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class FpBathLog(models.Model):
|
||||
"""A daily / per-shift chemistry log for a bath.
|
||||
|
||||
One log record represents one sampling event: an operator walks to a
|
||||
tank, runs titrations or reads instruments, and enters the results.
|
||||
Each log has one or more lines (one per parameter).
|
||||
|
||||
Overall log status is rolled up from the lines:
|
||||
* ok — every line is within target
|
||||
* warning — at least one line is within warning tolerance
|
||||
* out_of_spec — at least one line is outside target
|
||||
"""
|
||||
_name = 'fusion.plating.bath.log'
|
||||
_description = 'Fusion Plating — Bath Chemistry Log'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'log_date desc, id desc'
|
||||
_rec_name = 'display_name'
|
||||
|
||||
name = fields.Char(
|
||||
string='Reference',
|
||||
required=True,
|
||||
copy=False,
|
||||
default=lambda self: self._default_name(),
|
||||
tracking=True,
|
||||
)
|
||||
display_name = fields.Char(
|
||||
compute='_compute_display_name',
|
||||
store=True,
|
||||
)
|
||||
bath_id = fields.Many2one(
|
||||
'fusion.plating.bath',
|
||||
string='Bath',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
tracking=True,
|
||||
)
|
||||
tank_id = fields.Many2one(
|
||||
related='bath_id.tank_id',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
facility_id = fields.Many2one(
|
||||
related='bath_id.facility_id',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
process_type_id = fields.Many2one(
|
||||
related='bath_id.process_type_id',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
related='bath_id.company_id',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
log_date = fields.Datetime(
|
||||
string='Logged At',
|
||||
default=fields.Datetime.now,
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
operator_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Operator',
|
||||
default=lambda self: self.env.user,
|
||||
tracking=True,
|
||||
)
|
||||
shift = fields.Selection(
|
||||
[
|
||||
('day', 'Day'),
|
||||
('evening', 'Evening'),
|
||||
('night', 'Night'),
|
||||
],
|
||||
string='Shift',
|
||||
)
|
||||
|
||||
line_ids = fields.One2many(
|
||||
'fusion.plating.bath.log.line',
|
||||
'log_id',
|
||||
string='Readings',
|
||||
copy=True,
|
||||
)
|
||||
|
||||
status = fields.Selection(
|
||||
[
|
||||
('ok', 'OK'),
|
||||
('warning', 'Warning'),
|
||||
('out_of_spec', 'Out of Spec'),
|
||||
],
|
||||
string='Status',
|
||||
compute='_compute_status',
|
||||
store=True,
|
||||
tracking=True,
|
||||
)
|
||||
status_color = fields.Integer(
|
||||
compute='_compute_status_color',
|
||||
)
|
||||
notes = fields.Text(
|
||||
string='Notes',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
@api.model
|
||||
def _default_name(self):
|
||||
seq = self.env['ir.sequence'].next_by_code('fusion.plating.bath.log')
|
||||
return seq or '/'
|
||||
|
||||
@api.depends('name', 'bath_id', 'log_date')
|
||||
def _compute_display_name(self):
|
||||
for rec in self:
|
||||
parts = []
|
||||
if rec.bath_id:
|
||||
parts.append(rec.bath_id.name)
|
||||
if rec.log_date:
|
||||
parts.append(fields.Datetime.to_string(rec.log_date))
|
||||
rec.display_name = ' — '.join(parts) if parts else rec.name
|
||||
|
||||
@api.depends('line_ids', 'line_ids.status')
|
||||
def _compute_status(self):
|
||||
for rec in self:
|
||||
statuses = set(rec.line_ids.mapped('status'))
|
||||
if 'out_of_spec' in statuses:
|
||||
rec.status = 'out_of_spec'
|
||||
elif 'warning' in statuses:
|
||||
rec.status = 'warning'
|
||||
else:
|
||||
rec.status = 'ok'
|
||||
|
||||
@api.depends('status')
|
||||
def _compute_status_color(self):
|
||||
# Kanban color indexes: 0 default, 1 red, 3 yellow, 4 green
|
||||
mapping = {'ok': 4, 'warning': 3, 'out_of_spec': 1}
|
||||
for rec in self:
|
||||
rec.status_color = mapping.get(rec.status, 0)
|
||||
114
fusion-plating/fusion_plating/models/fp_bath_log_line.py
Normal file
114
fusion-plating/fusion_plating/models/fp_bath_log_line.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class FpBathLogLine(models.Model):
|
||||
"""A single parameter reading on a bath log.
|
||||
|
||||
Each line = one titration result or one sensor reading. Target ranges
|
||||
are pulled from the bath's per-bath overrides if present, otherwise
|
||||
from the parameter's defaults on fusion.plating.bath.parameter.
|
||||
Status is computed per line (ok / warning / out_of_spec) and rolled
|
||||
up to the parent log.
|
||||
"""
|
||||
_name = 'fusion.plating.bath.log.line'
|
||||
_description = 'Fusion Plating — Bath Log Reading'
|
||||
_order = 'log_id, sequence, id'
|
||||
|
||||
log_id = fields.Many2one(
|
||||
'fusion.plating.bath.log',
|
||||
string='Log',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
index=True,
|
||||
)
|
||||
bath_id = fields.Many2one(
|
||||
related='log_id.bath_id',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
sequence = fields.Integer(
|
||||
string='Sequence',
|
||||
default=10,
|
||||
)
|
||||
parameter_id = fields.Many2one(
|
||||
'fusion.plating.bath.parameter',
|
||||
string='Parameter',
|
||||
required=True,
|
||||
ondelete='restrict',
|
||||
)
|
||||
parameter_code = fields.Char(
|
||||
related='parameter_id.code',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
uom = fields.Char(
|
||||
related='parameter_id.uom',
|
||||
readonly=True,
|
||||
)
|
||||
value = fields.Float(
|
||||
string='Value',
|
||||
required=True,
|
||||
)
|
||||
target_min = fields.Float(
|
||||
string='Target Min',
|
||||
compute='_compute_targets',
|
||||
store=True,
|
||||
)
|
||||
target_max = fields.Float(
|
||||
string='Target Max',
|
||||
compute='_compute_targets',
|
||||
store=True,
|
||||
)
|
||||
status = fields.Selection(
|
||||
[
|
||||
('ok', 'OK'),
|
||||
('warning', 'Warning'),
|
||||
('out_of_spec', 'Out of Spec'),
|
||||
],
|
||||
string='Status',
|
||||
compute='_compute_status',
|
||||
store=True,
|
||||
)
|
||||
notes = fields.Char(
|
||||
string='Notes',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
@api.depends('parameter_id', 'log_id.bath_id')
|
||||
def _compute_targets(self):
|
||||
"""Resolve target range: per-bath override first, parameter default second."""
|
||||
for rec in self:
|
||||
tmin = tmax = 0.0
|
||||
if rec.log_id.bath_id and rec.parameter_id:
|
||||
override = rec.log_id.bath_id.target_line_ids.filtered(
|
||||
lambda t: t.parameter_id.id == rec.parameter_id.id
|
||||
)[:1]
|
||||
if override:
|
||||
tmin, tmax = override.target_min, override.target_max
|
||||
else:
|
||||
tmin = rec.parameter_id.target_min
|
||||
tmax = rec.parameter_id.target_max
|
||||
rec.target_min = tmin
|
||||
rec.target_max = tmax
|
||||
|
||||
@api.depends('value', 'target_min', 'target_max', 'parameter_id.warning_tolerance')
|
||||
def _compute_status(self):
|
||||
for rec in self:
|
||||
if rec.target_min == 0.0 and rec.target_max == 0.0:
|
||||
rec.status = 'ok'
|
||||
continue
|
||||
v, lo, hi = rec.value, rec.target_min, rec.target_max
|
||||
if v < lo or v > hi:
|
||||
rec.status = 'out_of_spec'
|
||||
continue
|
||||
tol_pct = (rec.parameter_id.warning_tolerance or 0.0) / 100.0
|
||||
span = max(hi - lo, 1e-9)
|
||||
if tol_pct > 0 and (v - lo < span * tol_pct or hi - v < span * tol_pct):
|
||||
rec.status = 'warning'
|
||||
else:
|
||||
rec.status = 'ok'
|
||||
88
fusion-plating/fusion_plating/models/fp_bath_parameter.py
Normal file
88
fusion-plating/fusion_plating/models/fp_bath_parameter.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class FpBathParameter(models.Model):
|
||||
"""Definition of a bath chemistry parameter.
|
||||
|
||||
Parameters are process-agnostic at the schema level (e.g. "Temperature",
|
||||
"pH", "Nickel concentration"). Each process type references a set of
|
||||
parameters via fusion.plating.process.type.parameter_ids. Actual target
|
||||
ranges per bath are stored on fusion.plating.bath (per-bath overrides)
|
||||
or on the bath recipe.
|
||||
"""
|
||||
_name = 'fusion.plating.bath.parameter'
|
||||
_description = 'Fusion Plating — Bath Parameter'
|
||||
_order = 'sequence, name'
|
||||
|
||||
name = fields.Char(
|
||||
string='Parameter',
|
||||
required=True,
|
||||
translate=True,
|
||||
help='Display name (e.g. "Nickel Concentration", "pH").',
|
||||
)
|
||||
code = fields.Char(
|
||||
string='Code',
|
||||
required=True,
|
||||
help='Short code used in logs and exports (e.g. "Ni", "PH", "TEMP").',
|
||||
)
|
||||
sequence = fields.Integer(
|
||||
string='Sequence',
|
||||
default=10,
|
||||
)
|
||||
parameter_type = fields.Selection(
|
||||
[
|
||||
('concentration', 'Concentration'),
|
||||
('temperature', 'Temperature'),
|
||||
('ph', 'pH'),
|
||||
('conductivity', 'Conductivity'),
|
||||
('turbidity', 'Turbidity'),
|
||||
('ratio', 'Ratio'),
|
||||
('count', 'Count / Age'),
|
||||
('other', 'Other'),
|
||||
],
|
||||
string='Type',
|
||||
required=True,
|
||||
default='concentration',
|
||||
)
|
||||
uom = fields.Char(
|
||||
string='Unit',
|
||||
help='Display unit (e.g. "g/L", "°C", "pH", "MTO").',
|
||||
)
|
||||
target_min = fields.Float(
|
||||
string='Default Target Min',
|
||||
help='Default target minimum. Per-bath overrides are allowed.',
|
||||
)
|
||||
target_max = fields.Float(
|
||||
string='Default Target Max',
|
||||
help='Default target maximum. Per-bath overrides are allowed.',
|
||||
)
|
||||
warning_tolerance = fields.Float(
|
||||
string='Warning Tolerance %',
|
||||
default=10.0,
|
||||
help='Distance from target limit at which a reading is flagged as warning.',
|
||||
)
|
||||
decimals = fields.Integer(
|
||||
string='Decimals',
|
||||
default=2,
|
||||
)
|
||||
description = fields.Text(
|
||||
string='Description',
|
||||
translate=True,
|
||||
)
|
||||
active = fields.Boolean(
|
||||
string='Active',
|
||||
default=True,
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
'fp_bath_parameter_code_uniq',
|
||||
'unique(code)',
|
||||
'Bath parameter code must be unique.',
|
||||
),
|
||||
]
|
||||
102
fusion-plating/fusion_plating/models/fp_facility.py
Normal file
102
fusion-plating/fusion_plating/models/fp_facility.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class FpFacility(models.Model):
|
||||
"""A physical plating / finishing facility.
|
||||
|
||||
A company can operate 1..N facilities. Each facility has its own work
|
||||
centers, tanks, operators, regulatory profile (ECA, sewer permit, waste
|
||||
generator number), and capability footprint. Jobs are scheduled into
|
||||
a facility based on capability matching.
|
||||
|
||||
Compliance add-on modules (fusion_plating_compliance_*) extend this
|
||||
model with jurisdiction-specific fields via inheritance.
|
||||
"""
|
||||
_name = 'fusion.plating.facility'
|
||||
_description = 'Fusion Plating — Facility'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'sequence, name'
|
||||
|
||||
name = fields.Char(
|
||||
string='Facility',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
code = fields.Char(
|
||||
string='Code',
|
||||
required=True,
|
||||
tracking=True,
|
||||
help='Short facility code used in job numbers and reports.',
|
||||
)
|
||||
sequence = fields.Integer(
|
||||
string='Sequence',
|
||||
default=10,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
required=True,
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Address',
|
||||
help='Partner holding the facility postal address and contact details.',
|
||||
)
|
||||
active = fields.Boolean(
|
||||
string='Active',
|
||||
default=True,
|
||||
)
|
||||
|
||||
# ----- Capability -----------------------------------------------------
|
||||
capability_ids = fields.Many2many(
|
||||
'fusion.plating.process.type',
|
||||
'fp_facility_capability_rel',
|
||||
'facility_id',
|
||||
'process_type_id',
|
||||
string='Capabilities',
|
||||
help='Process types this facility can perform.',
|
||||
)
|
||||
|
||||
# ----- Child records --------------------------------------------------
|
||||
work_center_ids = fields.One2many(
|
||||
'fusion.plating.work.center',
|
||||
'facility_id',
|
||||
string='Work Centers',
|
||||
)
|
||||
tank_ids = fields.One2many(
|
||||
'fusion.plating.tank',
|
||||
'facility_id',
|
||||
string='Tanks',
|
||||
)
|
||||
work_center_count = fields.Integer(
|
||||
compute='_compute_counts',
|
||||
)
|
||||
tank_count = fields.Integer(
|
||||
compute='_compute_counts',
|
||||
)
|
||||
capability_count = fields.Integer(
|
||||
compute='_compute_counts',
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
'fp_facility_code_company_uniq',
|
||||
'unique(code, company_id)',
|
||||
'Facility code must be unique within a company.',
|
||||
),
|
||||
]
|
||||
|
||||
def _compute_counts(self):
|
||||
for rec in self:
|
||||
rec.work_center_count = len(rec.work_center_ids)
|
||||
rec.tank_count = len(rec.tank_ids)
|
||||
rec.capability_count = len(rec.capability_ids)
|
||||
|
||||
def name_get(self):
|
||||
return [(rec.id, f'{rec.name} [{rec.code}]') for rec in self]
|
||||
62
fusion-plating/fusion_plating/models/fp_process_category.py
Normal file
62
fusion-plating/fusion_plating/models/fp_process_category.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.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class FpProcessCategory(models.Model):
|
||||
"""High-level grouping of finishing process types.
|
||||
|
||||
Ships with a seed set (Plating, Anodizing, Coating, Conversion Coating,
|
||||
Stripping, Other). Process packs reference these categories when they
|
||||
load specific process types.
|
||||
"""
|
||||
_name = 'fusion.plating.process.category'
|
||||
_description = 'Fusion Plating — Process Category'
|
||||
_order = 'sequence, name'
|
||||
|
||||
name = fields.Char(
|
||||
string='Category',
|
||||
required=True,
|
||||
translate=True,
|
||||
)
|
||||
code = fields.Char(
|
||||
string='Code',
|
||||
required=True,
|
||||
help='Short identifier (e.g. "plating", "anodizing").',
|
||||
)
|
||||
sequence = fields.Integer(
|
||||
string='Sequence',
|
||||
default=10,
|
||||
)
|
||||
description = fields.Text(
|
||||
string='Description',
|
||||
translate=True,
|
||||
)
|
||||
active = fields.Boolean(
|
||||
string='Active',
|
||||
default=True,
|
||||
)
|
||||
process_type_ids = fields.One2many(
|
||||
'fusion.plating.process.type',
|
||||
'category_id',
|
||||
string='Process Types',
|
||||
)
|
||||
process_type_count = fields.Integer(
|
||||
string='Process Types',
|
||||
compute='_compute_process_type_count',
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
'fp_process_category_code_uniq',
|
||||
'unique(code)',
|
||||
'Process category code must be unique.',
|
||||
),
|
||||
]
|
||||
|
||||
def _compute_process_type_count(self):
|
||||
for rec in self:
|
||||
rec.process_type_count = len(rec.process_type_ids)
|
||||
92
fusion-plating/fusion_plating/models/fp_process_type.py
Normal file
92
fusion-plating/fusion_plating/models/fp_process_type.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class FpProcessType(models.Model):
|
||||
"""Extensible finishing process taxonomy.
|
||||
|
||||
Core ships this model empty. Process packs (fusion_plating_process_en,
|
||||
fusion_plating_process_chrome, etc.) load records via data XML with
|
||||
noupdate so shops and customisations are preserved across upgrades.
|
||||
|
||||
Each process type has a category (plating / anodizing / conversion / etc.),
|
||||
a reference to optional industry specs, and visual theming for the UI.
|
||||
Chemistry parameter schemas are defined on fusion.plating.bath.parameter
|
||||
and linked here via parameter_ids.
|
||||
"""
|
||||
_name = 'fusion.plating.process.type'
|
||||
_description = 'Fusion Plating — Process Type'
|
||||
_order = 'sequence, name'
|
||||
|
||||
name = fields.Char(
|
||||
string='Process',
|
||||
required=True,
|
||||
translate=True,
|
||||
help='Display name (e.g. "Electroless Nickel — Mid Phosphorus").',
|
||||
)
|
||||
code = fields.Char(
|
||||
string='Code',
|
||||
required=True,
|
||||
help='Short unique code (e.g. "EN_MID", "HARD_CR", "ANO_II").',
|
||||
)
|
||||
category_id = fields.Many2one(
|
||||
'fusion.plating.process.category',
|
||||
string='Category',
|
||||
required=True,
|
||||
ondelete='restrict',
|
||||
)
|
||||
sequence = fields.Integer(
|
||||
string='Sequence',
|
||||
default=10,
|
||||
)
|
||||
description = fields.Text(
|
||||
string='Description',
|
||||
translate=True,
|
||||
)
|
||||
active = fields.Boolean(
|
||||
string='Active',
|
||||
default=True,
|
||||
)
|
||||
|
||||
# ----- Visual theming (kept neutral so it adapts to both light/dark) ----
|
||||
# Uses Odoo's built-in kanban/list color index (0-11).
|
||||
color = fields.Integer(
|
||||
string='Color Index',
|
||||
default=0,
|
||||
help='Colour index used in kanban and list views.',
|
||||
)
|
||||
icon = fields.Char(
|
||||
string='Icon',
|
||||
help='Optional Font Awesome class (e.g. "fa-flask").',
|
||||
default='fa-flask',
|
||||
)
|
||||
|
||||
# ----- Chemistry & routing support ----------------------------------------
|
||||
parameter_ids = fields.Many2many(
|
||||
'fusion.plating.bath.parameter',
|
||||
'fp_process_type_parameter_rel',
|
||||
'process_type_id',
|
||||
'parameter_id',
|
||||
string='Bath Parameters',
|
||||
help='Chemistry parameters tracked for baths running this process.',
|
||||
)
|
||||
hazard_notes = fields.Text(
|
||||
string='Hazard Notes',
|
||||
translate=True,
|
||||
help='Process-level hazard awareness (e.g. Cr(VI) carcinogen, hypophosphite reducer).',
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
'fp_process_type_code_uniq',
|
||||
'unique(code)',
|
||||
'Process type code must be unique.',
|
||||
),
|
||||
]
|
||||
|
||||
def name_get(self):
|
||||
return [(rec.id, f'{rec.name} [{rec.code}]') for rec in self]
|
||||
170
fusion-plating/fusion_plating/models/fp_tank.py
Normal file
170
fusion-plating/fusion_plating/models/fp_tank.py
Normal file
@@ -0,0 +1,170 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class FpTank(models.Model):
|
||||
"""A physical vessel that holds a bath.
|
||||
|
||||
Tanks are long-lived assets. Baths come and go inside a tank. The
|
||||
separation lets a shop dump an exhausted bath without losing the
|
||||
tank's history, QR code, or equipment records.
|
||||
|
||||
Each tank carries a unique QR code for operator scanning at the
|
||||
shop-floor station.
|
||||
"""
|
||||
_name = 'fusion.plating.tank'
|
||||
_description = 'Fusion Plating — Tank'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'facility_id, work_center_id, sequence, code'
|
||||
|
||||
name = fields.Char(
|
||||
string='Tank',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
code = fields.Char(
|
||||
string='Code',
|
||||
required=True,
|
||||
tracking=True,
|
||||
help='Short unique tank identifier (e.g. "T-01", "EN-A1").',
|
||||
)
|
||||
qr_code = fields.Char(
|
||||
string='QR Code',
|
||||
help='Scannable identifier. Defaults to code, can be set to a longer URI.',
|
||||
)
|
||||
sequence = fields.Integer(
|
||||
string='Sequence',
|
||||
default=10,
|
||||
)
|
||||
active = fields.Boolean(
|
||||
string='Active',
|
||||
default=True,
|
||||
)
|
||||
facility_id = fields.Many2one(
|
||||
'fusion.plating.facility',
|
||||
string='Facility',
|
||||
required=True,
|
||||
ondelete='restrict',
|
||||
tracking=True,
|
||||
)
|
||||
work_center_id = fields.Many2one(
|
||||
'fusion.plating.work.center',
|
||||
string='Work Center',
|
||||
domain="[('facility_id','=',facility_id)]",
|
||||
ondelete='restrict',
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
# ----- Physical properties --------------------------------------------
|
||||
volume = fields.Float(
|
||||
string='Volume',
|
||||
help='Working volume.',
|
||||
)
|
||||
volume_uom = fields.Selection(
|
||||
[
|
||||
('l', 'Litres'),
|
||||
('gal_us', 'US gallons'),
|
||||
('gal_imp', 'Imperial gallons'),
|
||||
('m3', 'Cubic metres'),
|
||||
],
|
||||
string='Volume Unit',
|
||||
default='l',
|
||||
)
|
||||
material = fields.Selection(
|
||||
[
|
||||
('polypro', 'Polypropylene'),
|
||||
('pvc', 'PVC'),
|
||||
('pvdf', 'PVDF'),
|
||||
('ss', 'Stainless Steel'),
|
||||
('lined_steel', 'Lined Steel'),
|
||||
('glass', 'Glass'),
|
||||
('other', 'Other'),
|
||||
],
|
||||
string='Construction',
|
||||
)
|
||||
heating_type = fields.Selection(
|
||||
[
|
||||
('none', 'None'),
|
||||
('immersion', 'Immersion Heater'),
|
||||
('steam_coil', 'Steam Coil'),
|
||||
('jacket', 'Jacketed'),
|
||||
('external', 'External Heat Exchanger'),
|
||||
],
|
||||
string='Heating',
|
||||
default='none',
|
||||
)
|
||||
has_filtration = fields.Boolean(
|
||||
string='Has Filtration',
|
||||
)
|
||||
has_rectifier = fields.Boolean(
|
||||
string='Has Rectifier',
|
||||
help='Required for electrolytic processes (chrome, anodize, strike).',
|
||||
)
|
||||
|
||||
# ----- State ----------------------------------------------------------
|
||||
state = fields.Selection(
|
||||
[
|
||||
('empty', 'Empty'),
|
||||
('filled', 'Filled'),
|
||||
('in_use', 'In Use'),
|
||||
('draining', 'Draining'),
|
||||
('maintenance', 'Maintenance'),
|
||||
('out_of_service', 'Out of Service'),
|
||||
],
|
||||
string='Status',
|
||||
default='empty',
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
# ----- Relations ------------------------------------------------------
|
||||
bath_ids = fields.One2many(
|
||||
'fusion.plating.bath',
|
||||
'tank_id',
|
||||
string='Bath History',
|
||||
)
|
||||
current_bath_id = fields.Many2one(
|
||||
'fusion.plating.bath',
|
||||
string='Current Bath',
|
||||
compute='_compute_current_bath',
|
||||
store=True,
|
||||
)
|
||||
current_process_id = fields.Many2one(
|
||||
'fusion.plating.process.type',
|
||||
string='Current Process',
|
||||
related='current_bath_id.process_type_id',
|
||||
store=True,
|
||||
)
|
||||
bath_count = fields.Integer(
|
||||
compute='_compute_bath_count',
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
'fp_tank_code_facility_uniq',
|
||||
'unique(code, facility_id)',
|
||||
'Tank code must be unique within a facility.',
|
||||
),
|
||||
]
|
||||
|
||||
@api.depends('bath_ids', 'bath_ids.state')
|
||||
def _compute_current_bath(self):
|
||||
for rec in self:
|
||||
active = rec.bath_ids.filtered(
|
||||
lambda b: b.state in ('operational', 'under_review')
|
||||
)
|
||||
rec.current_bath_id = active[:1].id if active else False
|
||||
|
||||
def _compute_bath_count(self):
|
||||
for rec in self:
|
||||
rec.bath_count = len(rec.bath_ids)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if not vals.get('qr_code') and vals.get('code'):
|
||||
vals['qr_code'] = f"FP-TANK:{vals['code']}"
|
||||
return super().create(vals_list)
|
||||
72
fusion-plating/fusion_plating/models/fp_work_center.py
Normal file
72
fusion-plating/fusion_plating/models/fp_work_center.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class FpWorkCenter(models.Model):
|
||||
"""A production line or station inside a facility.
|
||||
|
||||
Examples: "Line 1 - EN", "Anodize Line", "Prep Bay", "Bake Station",
|
||||
"Inspection Booth", "Shipping Dock". Work centers group tanks and
|
||||
provide scheduling capacity.
|
||||
"""
|
||||
_name = 'fusion.plating.work.center'
|
||||
_description = 'Fusion Plating — Work Center'
|
||||
_order = 'facility_id, sequence, name'
|
||||
|
||||
name = fields.Char(
|
||||
string='Work Center',
|
||||
required=True,
|
||||
)
|
||||
code = fields.Char(
|
||||
string='Code',
|
||||
required=True,
|
||||
)
|
||||
facility_id = fields.Many2one(
|
||||
'fusion.plating.facility',
|
||||
string='Facility',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
)
|
||||
sequence = fields.Integer(
|
||||
string='Sequence',
|
||||
default=10,
|
||||
)
|
||||
active = fields.Boolean(
|
||||
string='Active',
|
||||
default=True,
|
||||
)
|
||||
supported_process_ids = fields.Many2many(
|
||||
'fusion.plating.process.type',
|
||||
'fp_work_center_process_rel',
|
||||
'work_center_id',
|
||||
'process_type_id',
|
||||
string='Supported Processes',
|
||||
)
|
||||
tank_ids = fields.One2many(
|
||||
'fusion.plating.tank',
|
||||
'work_center_id',
|
||||
string='Tanks',
|
||||
)
|
||||
tank_count = fields.Integer(
|
||||
compute='_compute_tank_count',
|
||||
)
|
||||
capacity_per_day = fields.Float(
|
||||
string='Capacity / Day',
|
||||
help='Theoretical throughput (parts, jobs, or square metres per day) — unit depends on shop.',
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
'fp_work_center_code_facility_uniq',
|
||||
'unique(code, facility_id)',
|
||||
'Work center code must be unique within a facility.',
|
||||
),
|
||||
]
|
||||
|
||||
def _compute_tank_count(self):
|
||||
for rec in self:
|
||||
rec.tank_count = len(rec.tank_ids)
|
||||
30
fusion-plating/fusion_plating/models/res_company.py
Normal file
30
fusion-plating/fusion_plating/models/res_company.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
# ----- Facility footprint for this legal entity ----------------------
|
||||
x_fc_facility_ids = fields.One2many(
|
||||
'fusion.plating.facility',
|
||||
'company_id',
|
||||
string='Plating Facilities',
|
||||
)
|
||||
x_fc_facility_count = fields.Integer(
|
||||
string='# Facilities',
|
||||
compute='_compute_x_fc_facility_count',
|
||||
)
|
||||
x_fc_default_facility_id = fields.Many2one(
|
||||
'fusion.plating.facility',
|
||||
string='Default Facility',
|
||||
help='Facility used when the context does not specify one (single-site shops).',
|
||||
)
|
||||
|
||||
def _compute_x_fc_facility_count(self):
|
||||
for rec in self:
|
||||
rec.x_fc_facility_count = len(rec.x_fc_facility_ids)
|
||||
85
fusion-plating/fusion_plating/security/fp_security.xml
Normal file
85
fusion-plating/fusion_plating/security/fp_security.xml
Normal file
@@ -0,0 +1,85 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- MODULE CATEGORY -->
|
||||
<!-- Odoo 19 organises privileges by ir.module.category. Without this, -->
|
||||
<!-- groups fall into the generic Extra Rights list in user settings. -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="module_category_fusion_plating" model="ir.module.category">
|
||||
<field name="name">Fusion Plating</field>
|
||||
<field name="sequence">46</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- PRIVILEGE (Odoo 19 res.groups.privilege) -->
|
||||
<!-- Groups must reference this privilege_id so they render under a -->
|
||||
<!-- "FUSION PLATING" section in user settings. -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="res_groups_privilege_fusion_plating" model="res.groups.privilege">
|
||||
<field name="name">Fusion Plating</field>
|
||||
<field name="sequence">46</field>
|
||||
<field name="category_id" ref="module_category_fusion_plating"/>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- OPERATOR (base shop-floor access) -->
|
||||
<!-- Reads most reference data, writes chemistry logs. -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="group_fusion_plating_operator" model="res.groups">
|
||||
<field name="name">Operator</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="privilege_id" ref="res_groups_privilege_fusion_plating"/>
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- SUPERVISOR (line supervisor, team lead) -->
|
||||
<!-- Can manage baths, schedule jobs, review logs. -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="group_fusion_plating_supervisor" model="res.groups">
|
||||
<field name="name">Supervisor</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="privilege_id" ref="res_groups_privilege_fusion_plating"/>
|
||||
<field name="implied_ids" eval="[(4, ref('group_fusion_plating_operator'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- MANAGER (quality, EHS, plant manager, engineer) -->
|
||||
<!-- Full CRUD on configuration objects. -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="group_fusion_plating_manager" model="res.groups">
|
||||
<field name="name">Manager</field>
|
||||
<field name="sequence">30</field>
|
||||
<field name="privilege_id" ref="res_groups_privilege_fusion_plating"/>
|
||||
<field name="implied_ids" eval="[(4, ref('group_fusion_plating_supervisor'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- ADMINISTRATOR (owner, super-admin) -->
|
||||
<!-- Everything a Manager can do, plus system-level settings. -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="group_fusion_plating_admin" model="res.groups">
|
||||
<field name="name">Administrator</field>
|
||||
<field name="sequence">40</field>
|
||||
<field name="privilege_id" ref="res_groups_privilege_fusion_plating"/>
|
||||
<field name="implied_ids" eval="[(4, ref('group_fusion_plating_manager'))]"/>
|
||||
<field name="user_ids" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- RECORD RULE — Multi-company isolation on facilities -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="fp_facility_company_rule" model="ir.rule">
|
||||
<field name="name">Fusion Plating: Facility — multi-company</field>
|
||||
<field name="model_id" ref="model_fusion_plating_facility"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
28
fusion-plating/fusion_plating/security/ir.model.access.csv
Normal file
28
fusion-plating/fusion_plating/security/ir.model.access.csv
Normal file
@@ -0,0 +1,28 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_fp_process_category_operator,fp.process.category.operator,model_fusion_plating_process_category,group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_process_category_manager,fp.process.category.manager,model_fusion_plating_process_category,group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_process_type_operator,fp.process.type.operator,model_fusion_plating_process_type,group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_process_type_manager,fp.process.type.manager,model_fusion_plating_process_type,group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_bath_parameter_operator,fp.bath.parameter.operator,model_fusion_plating_bath_parameter,group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_bath_parameter_manager,fp.bath.parameter.manager,model_fusion_plating_bath_parameter,group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_facility_operator,fp.facility.operator,model_fusion_plating_facility,group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_facility_supervisor,fp.facility.supervisor,model_fusion_plating_facility,group_fusion_plating_supervisor,1,0,0,0
|
||||
access_fp_facility_manager,fp.facility.manager,model_fusion_plating_facility,group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_work_center_operator,fp.work.center.operator,model_fusion_plating_work_center,group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_work_center_supervisor,fp.work.center.supervisor,model_fusion_plating_work_center,group_fusion_plating_supervisor,1,1,0,0
|
||||
access_fp_work_center_manager,fp.work.center.manager,model_fusion_plating_work_center,group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_tank_operator,fp.tank.operator,model_fusion_plating_tank,group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_tank_supervisor,fp.tank.supervisor,model_fusion_plating_tank,group_fusion_plating_supervisor,1,1,0,0
|
||||
access_fp_tank_manager,fp.tank.manager,model_fusion_plating_tank,group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_bath_operator,fp.bath.operator,model_fusion_plating_bath,group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_bath_supervisor,fp.bath.supervisor,model_fusion_plating_bath,group_fusion_plating_supervisor,1,1,1,0
|
||||
access_fp_bath_manager,fp.bath.manager,model_fusion_plating_bath,group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_bath_target_operator,fp.bath.target.operator,model_fusion_plating_bath_target,group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_bath_target_supervisor,fp.bath.target.supervisor,model_fusion_plating_bath_target,group_fusion_plating_supervisor,1,1,1,0
|
||||
access_fp_bath_target_manager,fp.bath.target.manager,model_fusion_plating_bath_target,group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_bath_log_operator,fp.bath.log.operator,model_fusion_plating_bath_log,group_fusion_plating_operator,1,1,1,0
|
||||
access_fp_bath_log_supervisor,fp.bath.log.supervisor,model_fusion_plating_bath_log,group_fusion_plating_supervisor,1,1,1,0
|
||||
access_fp_bath_log_manager,fp.bath.log.manager,model_fusion_plating_bath_log,group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_bath_log_line_operator,fp.bath.log.line.operator,model_fusion_plating_bath_log_line,group_fusion_plating_operator,1,1,1,0
|
||||
access_fp_bath_log_line_supervisor,fp.bath.log.line.supervisor,model_fusion_plating_bath_log_line,group_fusion_plating_supervisor,1,1,1,0
|
||||
access_fp_bath_log_line_manager,fp.bath.log.line.manager,model_fusion_plating_bath_log_line,group_fusion_plating_manager,1,1,1,1
|
||||
|
BIN
fusion-plating/fusion_plating/static/description/icon.png
Normal file
BIN
fusion-plating/fusion_plating/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,173 @@
|
||||
// =============================================================================
|
||||
// Fusion Plating — backend styles
|
||||
// Copyright 2026 Nexa Systems Inc.
|
||||
// License OPL-1 (Odoo Proprietary License v1.0)
|
||||
//
|
||||
// THEME AWARENESS
|
||||
// ---------------
|
||||
// This file NEVER hardcodes backgrounds or text colours. All surface colours
|
||||
// come from Odoo / Bootstrap CSS custom properties so the component renders
|
||||
// correctly in BOTH light and dark mode without any duplication:
|
||||
//
|
||||
// background: var(--bs-body-bg) // main surface
|
||||
// surface: var(--o-view-background-color) // view canvas
|
||||
// foreground: var(--bs-body-color) // main text
|
||||
// muted text: var(--bs-secondary-color)
|
||||
// border: var(--bs-border-color)
|
||||
// primary: var(--o-action) // Odoo action/brand
|
||||
//
|
||||
// Semantic status colours (green / amber / red) use `color-mix()` against the
|
||||
// Bootstrap theme token so a green badge is darker on light mode and brighter
|
||||
// on dark mode automatically — one rule, two looks.
|
||||
//
|
||||
// We never target `.o_dark`, `html.dark`, or `@media (prefers-color-scheme)`
|
||||
// to override colours. If you find yourself needing that, it's a smell — use
|
||||
// a variable instead.
|
||||
// =============================================================================
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Local helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
// `color-mix()` lets us tint a semantic colour against the surface, so the
|
||||
// result adapts to light or dark backgrounds automatically.
|
||||
@mixin fp-tint($color-var, $amount: 12%) {
|
||||
background-color: color-mix(in srgb, var(#{$color-var}) #{$amount}, transparent);
|
||||
color: var(#{$color-var});
|
||||
border: 1px solid color-mix(in srgb, var(#{$color-var}) 35%, transparent);
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Generic card surface used in kanban views (facility, tank, bath)
|
||||
// -----------------------------------------------------------------------------
|
||||
.o_fp_card {
|
||||
background-color: var(--o-view-background-color, var(--bs-body-bg));
|
||||
color: var(--bs-body-color);
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
transition: border-color 120ms ease, box-shadow 120ms ease;
|
||||
|
||||
&:hover {
|
||||
border-color: color-mix(in srgb, var(--o-action) 50%, var(--bs-border-color));
|
||||
box-shadow: 0 2px 8px color-mix(in srgb, var(--bs-body-color) 8%, transparent);
|
||||
}
|
||||
|
||||
.o_fp_card_title {
|
||||
color: var(--bs-body-color);
|
||||
font-size: 1rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.o_fp_card_stats {
|
||||
color: var(--bs-body-color);
|
||||
|
||||
.text-muted,
|
||||
.text-muted * {
|
||||
color: var(--bs-secondary-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Tank kanban — state badge theming
|
||||
// -----------------------------------------------------------------------------
|
||||
.o_fp_tank_kanban {
|
||||
|
||||
.o_fp_tank_card {
|
||||
// Let the left-border carry the state — subtle, theme-aware.
|
||||
border-left-width: 4px;
|
||||
|
||||
&[data-state="empty"],
|
||||
&[data-state="out_of_service"] {
|
||||
border-left-color: var(--bs-secondary-color);
|
||||
}
|
||||
&[data-state="filled"] {
|
||||
border-left-color: var(--bs-info, var(--o-action));
|
||||
}
|
||||
&[data-state="in_use"] {
|
||||
border-left-color: var(--bs-success);
|
||||
}
|
||||
&[data-state="draining"],
|
||||
&[data-state="maintenance"] {
|
||||
border-left-color: var(--bs-warning);
|
||||
}
|
||||
}
|
||||
|
||||
.o_fp_badge {
|
||||
padding: 2px 8px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
border-radius: 999px;
|
||||
|
||||
&[data-state="empty"],
|
||||
&[data-state="out_of_service"] {
|
||||
@include fp-tint(--bs-secondary-color);
|
||||
}
|
||||
&[data-state="filled"] {
|
||||
@include fp-tint(--bs-info);
|
||||
}
|
||||
&[data-state="in_use"] {
|
||||
@include fp-tint(--bs-success);
|
||||
}
|
||||
&[data-state="draining"],
|
||||
&[data-state="maintenance"] {
|
||||
@include fp-tint(--bs-warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Bath kanban — chemistry health dot
|
||||
// -----------------------------------------------------------------------------
|
||||
.o_fp_bath_kanban {
|
||||
|
||||
.o_fp_bath_card {
|
||||
// A single left-border tint conveys chemistry health without colouring
|
||||
// the entire card.
|
||||
border-left-width: 4px;
|
||||
border-left-color: var(--bs-success);
|
||||
|
||||
&[data-log-status="warning"] {
|
||||
border-left-color: var(--bs-warning);
|
||||
}
|
||||
&[data-log-status="out_of_spec"] {
|
||||
border-left-color: var(--bs-danger);
|
||||
}
|
||||
}
|
||||
|
||||
.o_fp_health_dot {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--bs-success);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--bs-success) 25%, transparent);
|
||||
|
||||
&[data-status="warning"] {
|
||||
background-color: var(--bs-warning);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--bs-warning) 25%, transparent);
|
||||
}
|
||||
&[data-status="out_of_spec"] {
|
||||
background-color: var(--bs-danger);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--bs-danger) 25%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Facility kanban — stat strip spacing
|
||||
// -----------------------------------------------------------------------------
|
||||
.o_fp_facility_kanban {
|
||||
|
||||
.o_fp_card_stats {
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed var(--bs-border-color);
|
||||
}
|
||||
}
|
||||
127
fusion-plating/fusion_plating/views/fp_bath_log_views.xml
Normal file
127
fusion-plating/fusion_plating/views/fp_bath_log_views.xml
Normal file
@@ -0,0 +1,127 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_bath_log_list" model="ir.ui.view">
|
||||
<field name="name">fp.bath.log.list</field>
|
||||
<field name="model">fusion.plating.bath.log</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Bath Logs"
|
||||
decoration-success="status == 'ok'"
|
||||
decoration-warning="status == 'warning'"
|
||||
decoration-danger="status == 'out_of_spec'">
|
||||
<field name="name"/>
|
||||
<field name="log_date"/>
|
||||
<field name="bath_id"/>
|
||||
<field name="tank_id" optional="show"/>
|
||||
<field name="process_type_id" optional="show"/>
|
||||
<field name="operator_id"/>
|
||||
<field name="shift" optional="hide"/>
|
||||
<field name="status" widget="badge"
|
||||
decoration-success="status == 'ok'"
|
||||
decoration-warning="status == 'warning'"
|
||||
decoration-danger="status == 'out_of_spec'"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_bath_log_form" model="ir.ui.view">
|
||||
<field name="name">fp.bath.log.form</field>
|
||||
<field name="model">fusion.plating.bath.log</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Bath Log">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1><field name="name" readonly="1"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="bath_id"/>
|
||||
<field name="tank_id" readonly="1"/>
|
||||
<field name="process_type_id" readonly="1"/>
|
||||
<field name="facility_id" readonly="1" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="log_date"/>
|
||||
<field name="operator_id"/>
|
||||
<field name="shift"/>
|
||||
<field name="status" readonly="1" widget="badge"
|
||||
decoration-success="status == 'ok'"
|
||||
decoration-warning="status == 'warning'"
|
||||
decoration-danger="status == 'out_of_spec'"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Readings">
|
||||
<field name="line_ids">
|
||||
<list editable="bottom"
|
||||
decoration-success="status == 'ok'"
|
||||
decoration-warning="status == 'warning'"
|
||||
decoration-danger="status == 'out_of_spec'">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="parameter_id"/>
|
||||
<field name="value"/>
|
||||
<field name="uom"/>
|
||||
<field name="target_min"/>
|
||||
<field name="target_max"/>
|
||||
<field name="status" widget="badge"
|
||||
decoration-success="status == 'ok'"
|
||||
decoration-warning="status == 'warning'"
|
||||
decoration-danger="status == 'out_of_spec'"/>
|
||||
<field name="notes"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Notes">
|
||||
<field name="notes"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_bath_log_search" model="ir.ui.view">
|
||||
<field name="name">fp.bath.log.search</field>
|
||||
<field name="model">fusion.plating.bath.log</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Bath Logs">
|
||||
<field name="name"/>
|
||||
<field name="bath_id"/>
|
||||
<field name="tank_id"/>
|
||||
<field name="process_type_id"/>
|
||||
<field name="operator_id"/>
|
||||
<separator/>
|
||||
<filter string="OK" name="ok" domain="[('status','=','ok')]"/>
|
||||
<filter string="Warning" name="warn" domain="[('status','=','warning')]"/>
|
||||
<filter string="Out of Spec" name="oos" domain="[('status','=','out_of_spec')]"/>
|
||||
<separator/>
|
||||
<filter string="Today" name="today"
|
||||
domain="[('log_date','>=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter string="This Week" name="week"
|
||||
domain="[('log_date','>=', (context_today() - relativedelta(days=7)).strftime('%Y-%m-%d'))]"/>
|
||||
<group>
|
||||
<filter string="Bath" name="group_bath" context="{'group_by':'bath_id'}"/>
|
||||
<filter string="Tank" name="group_tank" context="{'group_by':'tank_id'}"/>
|
||||
<filter string="Process" name="group_process" context="{'group_by':'process_type_id'}"/>
|
||||
<filter string="Operator" name="group_op" context="{'group_by':'operator_id'}"/>
|
||||
<filter string="Status" name="group_status" context="{'group_by':'status'}"/>
|
||||
<filter string="Day" name="group_day" context="{'group_by':'log_date:day'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fp_bath_log" model="ir.actions.act_window">
|
||||
<field name="name">Bath Logs</field>
|
||||
<field name="res_model">fusion.plating.bath.log</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_fp_bath_log_search"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
198
fusion-plating/fusion_plating/views/fp_bath_views.xml
Normal file
198
fusion-plating/fusion_plating/views/fp_bath_views.xml
Normal file
@@ -0,0 +1,198 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_bath_list" model="ir.ui.view">
|
||||
<field name="name">fp.bath.list</field>
|
||||
<field name="model">fusion.plating.bath</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Baths" decoration-muted="state == 'dumped'"
|
||||
decoration-warning="last_log_status == 'warning'"
|
||||
decoration-danger="last_log_status == 'out_of_spec'">
|
||||
<field name="name"/>
|
||||
<field name="tank_id"/>
|
||||
<field name="process_type_id"/>
|
||||
<field name="facility_id" groups="base.group_multi_company"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-success="state == 'operational'"
|
||||
decoration-info="state == 'new'"
|
||||
decoration-warning="state == 'under_review'"
|
||||
decoration-danger="state == 'dump_scheduled'"
|
||||
decoration-muted="state == 'dumped'"/>
|
||||
<field name="mto_count"/>
|
||||
<field name="last_log_date"/>
|
||||
<field name="last_log_status" widget="badge"
|
||||
decoration-success="last_log_status == 'ok'"
|
||||
decoration-warning="last_log_status == 'warning'"
|
||||
decoration-danger="last_log_status == 'out_of_spec'"/>
|
||||
<field name="makeup_date" optional="hide"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_bath_form" model="ir.ui.view">
|
||||
<field name="name">fp.bath.form</field>
|
||||
<field name="model">fusion.plating.bath</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Bath">
|
||||
<header>
|
||||
<button name="action_make_operational" string="Set Operational" type="object"
|
||||
class="oe_highlight" invisible="state != 'new'"/>
|
||||
<button name="action_mark_under_review" string="Flag for Review" type="object"
|
||||
invisible="state not in ('operational',)"/>
|
||||
<button name="action_schedule_dump" string="Schedule Dump" type="object"
|
||||
invisible="state not in ('operational','under_review')"/>
|
||||
<button name="action_dump" string="Dump" type="object"
|
||||
invisible="state != 'dump_scheduled'"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="new,operational,under_review,dump_scheduled,dumped"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="%(action_fp_bath_log)d" type="action" class="oe_stat_button" icon="fa-flask"
|
||||
context="{'search_default_bath_id': id}">
|
||||
<field name="log_count" widget="statinfo" string="Logs"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" readonly="state != 'new'"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="tank_id"/>
|
||||
<field name="process_type_id"/>
|
||||
<field name="facility_id" readonly="1"/>
|
||||
<field name="volume"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="makeup_date"/>
|
||||
<field name="makeup_by_id"/>
|
||||
<field name="mto_count" readonly="1"/>
|
||||
<field name="last_log_date" readonly="1"/>
|
||||
<field name="last_log_status" readonly="1" widget="badge"
|
||||
decoration-success="last_log_status == 'ok'"
|
||||
decoration-warning="last_log_status == 'warning'"
|
||||
decoration-danger="last_log_status == 'out_of_spec'"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Target Ranges">
|
||||
<field name="target_line_ids">
|
||||
<list editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="parameter_id"/>
|
||||
<field name="target_min"/>
|
||||
<field name="target_max"/>
|
||||
<field name="uom"/>
|
||||
</list>
|
||||
</field>
|
||||
<p class="text-muted mt-2">
|
||||
Per-bath target overrides. If empty, the parameter's default range is used.
|
||||
</p>
|
||||
</page>
|
||||
<page string="Chemistry Logs">
|
||||
<field name="log_ids" readonly="1">
|
||||
<list decoration-success="status == 'ok'"
|
||||
decoration-warning="status == 'warning'"
|
||||
decoration-danger="status == 'out_of_spec'">
|
||||
<field name="name"/>
|
||||
<field name="log_date"/>
|
||||
<field name="operator_id"/>
|
||||
<field name="shift"/>
|
||||
<field name="status"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Notes">
|
||||
<field name="notes"/>
|
||||
</page>
|
||||
<page string="Dump" invisible="state not in ('dump_scheduled','dumped')">
|
||||
<group>
|
||||
<field name="dump_scheduled_date"/>
|
||||
<field name="dumped_date"/>
|
||||
<field name="dump_reason"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_bath_kanban" model="ir.ui.view">
|
||||
<field name="name">fp.bath.kanban</field>
|
||||
<field name="model">fusion.plating.bath</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban default_group_by="state" class="o_fp_bath_kanban">
|
||||
<field name="id"/>
|
||||
<field name="name"/>
|
||||
<field name="tank_id"/>
|
||||
<field name="process_type_id"/>
|
||||
<field name="state"/>
|
||||
<field name="last_log_status"/>
|
||||
<field name="mto_count"/>
|
||||
<field name="status_color"/>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<div class="o_fp_card o_fp_bath_card"
|
||||
t-att-data-log-status="record.last_log_status.raw_value">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<strong class="o_fp_card_title"><field name="name"/></strong>
|
||||
<span class="o_fp_health_dot"
|
||||
t-att-data-status="record.last_log_status.raw_value or 'ok'"/>
|
||||
</div>
|
||||
<div class="small text-muted">
|
||||
<field name="process_type_id"/>
|
||||
</div>
|
||||
<div class="small"><i class="fa fa-flask me-1 text-muted"/><field name="tank_id"/></div>
|
||||
<div class="d-flex justify-content-between mt-2 small">
|
||||
<span class="text-muted">MTO</span>
|
||||
<span class="fw-bold"><field name="mto_count"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_bath_search" model="ir.ui.view">
|
||||
<field name="name">fp.bath.search</field>
|
||||
<field name="model">fusion.plating.bath</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Baths">
|
||||
<field name="name"/>
|
||||
<field name="tank_id"/>
|
||||
<field name="process_type_id"/>
|
||||
<field name="facility_id"/>
|
||||
<separator/>
|
||||
<filter string="Operational" name="operational" domain="[('state','=','operational')]"/>
|
||||
<filter string="Under Review" name="review" domain="[('state','=','under_review')]"/>
|
||||
<filter string="Out of Spec" name="oos" domain="[('last_log_status','=','out_of_spec')]"/>
|
||||
<filter string="Warning" name="warn" domain="[('last_log_status','=','warning')]"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
<group>
|
||||
<filter string="Facility" name="group_facility" context="{'group_by':'facility_id'}"/>
|
||||
<filter string="Process" name="group_process" context="{'group_by':'process_type_id'}"/>
|
||||
<filter string="Tank" name="group_tank" context="{'group_by':'tank_id'}"/>
|
||||
<filter string="Status" name="group_state" context="{'group_by':'state'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fp_bath" model="ir.actions.act_window">
|
||||
<field name="name">Baths</field>
|
||||
<field name="res_model">fusion.plating.bath</field>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
<field name="search_view_id" ref="view_fp_bath_search"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
156
fusion-plating/fusion_plating/views/fp_facility_views.xml
Normal file
156
fusion-plating/fusion_plating/views/fp_facility_views.xml
Normal file
@@ -0,0 +1,156 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_facility_list" model="ir.ui.view">
|
||||
<field name="name">fp.facility.list</field>
|
||||
<field name="model">fusion.plating.facility</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Facilities">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="work_center_count"/>
|
||||
<field name="tank_count"/>
|
||||
<field name="capability_count"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_facility_form" model="ir.ui.view">
|
||||
<field name="name">fp.facility.form</field>
|
||||
<field name="model">fusion.plating.facility</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Facility">
|
||||
<header/>
|
||||
<sheet>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="%(action_fp_tank)d" type="action" class="oe_stat_button" icon="fa-flask"
|
||||
context="{'search_default_facility_id': id}">
|
||||
<field name="tank_count" widget="statinfo" string="Tanks"/>
|
||||
</button>
|
||||
<button name="%(action_fp_work_center)d" type="action" class="oe_stat_button" icon="fa-cogs"
|
||||
context="{'search_default_facility_id': id}">
|
||||
<field name="work_center_count" widget="statinfo" string="Work Centers"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" placeholder="e.g. Site A — Mississauga"/></h1>
|
||||
<div class="text-muted">
|
||||
<field name="code" placeholder="SITE-A"/>
|
||||
</div>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="sequence"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Capabilities">
|
||||
<field name="capability_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||
<p class="text-muted mt-2">
|
||||
Process types this facility can perform. Install process packs
|
||||
(EN, chrome, anodize, black oxide) to populate the list.
|
||||
</p>
|
||||
</page>
|
||||
<page string="Work Centers">
|
||||
<field name="work_center_ids">
|
||||
<list>
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="tank_count"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Tanks">
|
||||
<field name="tank_ids">
|
||||
<list>
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="work_center_id"/>
|
||||
<field name="current_process_id"/>
|
||||
<field name="state"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_facility_kanban" model="ir.ui.view">
|
||||
<field name="name">fp.facility.kanban</field>
|
||||
<field name="model">fusion.plating.facility</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="o_fp_facility_kanban">
|
||||
<field name="id"/>
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="tank_count"/>
|
||||
<field name="work_center_count"/>
|
||||
<field name="capability_count"/>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<div class="o_fp_card">
|
||||
<div class="d-flex align-items-start justify-content-between">
|
||||
<div>
|
||||
<strong class="o_fp_card_title"><field name="name"/></strong>
|
||||
<div class="text-muted small"><field name="code"/></div>
|
||||
</div>
|
||||
<i class="fa fa-industry text-muted" aria-hidden="true"/>
|
||||
</div>
|
||||
<div class="d-flex gap-3 mt-3 o_fp_card_stats">
|
||||
<div class="text-center">
|
||||
<div class="fw-bold"><field name="work_center_count"/></div>
|
||||
<div class="small text-muted">Lines</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="fw-bold"><field name="tank_count"/></div>
|
||||
<div class="small text-muted">Tanks</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="fw-bold"><field name="capability_count"/></div>
|
||||
<div class="small text-muted">Processes</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fp_facility" model="ir.actions.act_window">
|
||||
<field name="name">Facilities</field>
|
||||
<field name="res_model">fusion.plating.facility</field>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first facility
|
||||
</p>
|
||||
<p>
|
||||
A facility is a physical site with its own tanks, work centers,
|
||||
operators, and regulatory profile. A single-site shop has one
|
||||
facility; a multi-site operator has several.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
77
fusion-plating/fusion_plating/views/fp_menu.xml
Normal file
77
fusion-plating/fusion_plating/views/fp_menu.xml
Normal file
@@ -0,0 +1,77 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- ===== ROOT APP MENU ===== -->
|
||||
<menuitem id="menu_fp_root"
|
||||
name="Plating"
|
||||
sequence="46"
|
||||
web_icon="fusion_plating,static/description/icon.png"
|
||||
groups="group_fusion_plating_operator"/>
|
||||
|
||||
<!-- ===== OPERATIONS ===== -->
|
||||
<menuitem id="menu_fp_operations"
|
||||
name="Operations"
|
||||
parent="menu_fp_root"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_fp_baths"
|
||||
name="Baths"
|
||||
parent="menu_fp_operations"
|
||||
action="action_fp_bath"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_fp_bath_logs"
|
||||
name="Chemistry Logs"
|
||||
parent="menu_fp_operations"
|
||||
action="action_fp_bath_log"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem id="menu_fp_tanks"
|
||||
name="Tanks"
|
||||
parent="menu_fp_operations"
|
||||
action="action_fp_tank"
|
||||
sequence="30"/>
|
||||
|
||||
<!-- ===== CONFIGURATION ===== -->
|
||||
<menuitem id="menu_fp_config"
|
||||
name="Configuration"
|
||||
parent="menu_fp_root"
|
||||
sequence="90"
|
||||
groups="group_fusion_plating_manager"/>
|
||||
|
||||
<menuitem id="menu_fp_facilities"
|
||||
name="Facilities"
|
||||
parent="menu_fp_config"
|
||||
action="action_fp_facility"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_fp_work_centers"
|
||||
name="Work Centers"
|
||||
parent="menu_fp_config"
|
||||
action="action_fp_work_center"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem id="menu_fp_process_categories"
|
||||
name="Process Categories"
|
||||
parent="menu_fp_config"
|
||||
action="action_fp_process_category"
|
||||
sequence="30"/>
|
||||
|
||||
<menuitem id="menu_fp_process_types"
|
||||
name="Process Types"
|
||||
parent="menu_fp_config"
|
||||
action="action_fp_process_type"
|
||||
sequence="40"/>
|
||||
|
||||
<menuitem id="menu_fp_bath_parameters"
|
||||
name="Bath Parameters"
|
||||
parent="menu_fp_config"
|
||||
action="action_fp_bath_parameter"
|
||||
sequence="50"/>
|
||||
|
||||
</odoo>
|
||||
240
fusion-plating/fusion_plating/views/fp_process_type_views.xml
Normal file
240
fusion-plating/fusion_plating/views/fp_process_type_views.xml
Normal file
@@ -0,0 +1,240 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- ===== Process Category ===== -->
|
||||
<record id="view_fp_process_category_list" model="ir.ui.view">
|
||||
<field name="name">fp.process.category.list</field>
|
||||
<field name="model">fusion.plating.process.category</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Process Categories">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="process_type_count"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_process_category_form" model="ir.ui.view">
|
||||
<field name="name">fp.process.category.form</field>
|
||||
<field name="model">fusion.plating.process.category</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Process Category">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" placeholder="e.g. Plating"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="code"/>
|
||||
<field name="sequence"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Description">
|
||||
<field name="description" placeholder="What this category represents..."/>
|
||||
</page>
|
||||
<page string="Process Types">
|
||||
<field name="process_type_ids">
|
||||
<list>
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fp_process_category" model="ir.actions.act_window">
|
||||
<field name="name">Process Categories</field>
|
||||
<field name="res_model">fusion.plating.process.category</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Define process categories
|
||||
</p>
|
||||
<p>
|
||||
Categories group related finishing processes (plating, anodizing,
|
||||
conversion coatings, etc.). Process packs reference these categories
|
||||
when they load specific process types.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Process Type ===== -->
|
||||
<record id="view_fp_process_type_list" model="ir.ui.view">
|
||||
<field name="name">fp.process.type.list</field>
|
||||
<field name="model">fusion.plating.process.type</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Process Types">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="category_id"/>
|
||||
<field name="icon" optional="hide"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_process_type_form" model="ir.ui.view">
|
||||
<field name="name">fp.process.type.form</field>
|
||||
<field name="model">fusion.plating.process.type</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Process Type">
|
||||
<sheet>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" placeholder="e.g. Electroless Nickel — Mid Phosphorus"/></h1>
|
||||
<div class="text-muted">
|
||||
<field name="code" placeholder="EN_MID"/>
|
||||
</div>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="category_id"/>
|
||||
<field name="sequence"/>
|
||||
<field name="icon"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="color" widget="color_picker"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Description">
|
||||
<field name="description" placeholder="Short description of the process..."/>
|
||||
</page>
|
||||
<page string="Bath Parameters">
|
||||
<field name="parameter_ids">
|
||||
<list>
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="parameter_type"/>
|
||||
<field name="uom"/>
|
||||
<field name="target_min"/>
|
||||
<field name="target_max"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Hazard Notes">
|
||||
<field name="hazard_notes"
|
||||
placeholder="Process-level hazard awareness (e.g. Cr(VI) carcinogen, hypophosphite reducer)..."/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_process_type_search" model="ir.ui.view">
|
||||
<field name="name">fp.process.type.search</field>
|
||||
<field name="model">fusion.plating.process.type</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Process Types">
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="category_id"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
<group>
|
||||
<filter string="Category" name="group_category" context="{'group_by':'category_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fp_process_type" model="ir.actions.act_window">
|
||||
<field name="name">Process Types</field>
|
||||
<field name="res_model">fusion.plating.process.type</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_fp_process_type_search"/>
|
||||
<field name="context">{'search_default_group_category': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No process types yet
|
||||
</p>
|
||||
<p>
|
||||
Install a process pack (EN, Chrome, Anodize, Black Oxide) to load
|
||||
pre-configured process types, or create your own.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Bath Parameter ===== -->
|
||||
<record id="view_fp_bath_parameter_list" model="ir.ui.view">
|
||||
<field name="name">fp.bath.parameter.list</field>
|
||||
<field name="model">fusion.plating.bath.parameter</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Bath Parameters">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="parameter_type"/>
|
||||
<field name="uom"/>
|
||||
<field name="target_min"/>
|
||||
<field name="target_max"/>
|
||||
<field name="warning_tolerance"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_bath_parameter_form" model="ir.ui.view">
|
||||
<field name="name">fp.bath.parameter.form</field>
|
||||
<field name="model">fusion.plating.bath.parameter</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Bath Parameter">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" placeholder="e.g. Nickel Concentration"/></h1>
|
||||
<div class="text-muted">
|
||||
<field name="code" placeholder="Ni"/>
|
||||
</div>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="parameter_type"/>
|
||||
<field name="uom"/>
|
||||
<field name="decimals"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="target_min"/>
|
||||
<field name="target_max"/>
|
||||
<field name="warning_tolerance"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Description">
|
||||
<field name="description" nolabel="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fp_bath_parameter" model="ir.actions.act_window">
|
||||
<field name="name">Bath Parameters</field>
|
||||
<field name="res_model">fusion.plating.bath.parameter</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
168
fusion-plating/fusion_plating/views/fp_tank_views.xml
Normal file
168
fusion-plating/fusion_plating/views/fp_tank_views.xml
Normal file
@@ -0,0 +1,168 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_tank_list" model="ir.ui.view">
|
||||
<field name="name">fp.tank.list</field>
|
||||
<field name="model">fusion.plating.tank</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Tanks">
|
||||
<field name="facility_id"/>
|
||||
<field name="work_center_id"/>
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="current_process_id"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-success="state == 'in_use'"
|
||||
decoration-info="state == 'filled'"
|
||||
decoration-warning="state in ('draining', 'maintenance')"
|
||||
decoration-muted="state in ('empty', 'out_of_service')"/>
|
||||
<field name="material" optional="hide"/>
|
||||
<field name="volume" optional="show"/>
|
||||
<field name="volume_uom" optional="show"/>
|
||||
<field name="active" widget="boolean_toggle" optional="hide"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_tank_form" model="ir.ui.view">
|
||||
<field name="name">fp.tank.form</field>
|
||||
<field name="model">fusion.plating.tank</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Tank">
|
||||
<header>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="empty,filled,in_use,draining,maintenance"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<widget name="web_ribbon" title="Out of Service" bg_color="text-bg-danger"
|
||||
invisible="state != 'out_of_service'"/>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" placeholder="e.g. EN Plating Tank A1"/></h1>
|
||||
<div class="text-muted">
|
||||
<field name="code" placeholder="T-01"/>
|
||||
</div>
|
||||
</div>
|
||||
<group>
|
||||
<group string="Location">
|
||||
<field name="facility_id"/>
|
||||
<field name="work_center_id"/>
|
||||
<field name="sequence"/>
|
||||
</group>
|
||||
<group string="Current Bath">
|
||||
<field name="current_bath_id" readonly="1"/>
|
||||
<field name="current_process_id" readonly="1"/>
|
||||
<field name="qr_code"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Physical">
|
||||
<group>
|
||||
<group>
|
||||
<field name="volume"/>
|
||||
<field name="volume_uom"/>
|
||||
<field name="material"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="heating_type"/>
|
||||
<field name="has_filtration"/>
|
||||
<field name="has_rectifier"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Bath History">
|
||||
<field name="bath_ids">
|
||||
<list decoration-muted="state == 'dumped'">
|
||||
<field name="name"/>
|
||||
<field name="process_type_id"/>
|
||||
<field name="state"/>
|
||||
<field name="makeup_date"/>
|
||||
<field name="mto_count"/>
|
||||
<field name="last_log_date"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_tank_kanban" model="ir.ui.view">
|
||||
<field name="name">fp.tank.kanban</field>
|
||||
<field name="model">fusion.plating.tank</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="o_fp_tank_kanban">
|
||||
<field name="id"/>
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="state"/>
|
||||
<field name="current_bath_id"/>
|
||||
<field name="current_process_id"/>
|
||||
<field name="facility_id"/>
|
||||
<field name="work_center_id"/>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<div class="o_fp_card o_fp_tank_card" t-att-data-state="record.state.raw_value">
|
||||
<div class="d-flex align-items-start justify-content-between">
|
||||
<div>
|
||||
<strong class="o_fp_card_title"><field name="code"/></strong>
|
||||
<div class="small text-muted"><field name="name"/></div>
|
||||
</div>
|
||||
<span class="badge o_fp_badge" t-att-data-state="record.state.raw_value">
|
||||
<field name="state"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 small">
|
||||
<div><i class="fa fa-flask me-1 text-muted"/><field name="current_process_id"/></div>
|
||||
<div class="text-muted"><field name="work_center_id"/></div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_tank_search" model="ir.ui.view">
|
||||
<field name="name">fp.tank.search</field>
|
||||
<field name="model">fusion.plating.tank</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Tanks">
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="qr_code"/>
|
||||
<field name="facility_id"/>
|
||||
<field name="work_center_id"/>
|
||||
<field name="current_process_id"/>
|
||||
<separator/>
|
||||
<filter string="In Use" name="in_use" domain="[('state','=','in_use')]"/>
|
||||
<filter string="Filled" name="filled" domain="[('state','=','filled')]"/>
|
||||
<filter string="Maintenance" name="maintenance" domain="[('state','=','maintenance')]"/>
|
||||
<filter string="Out of Service" name="out" domain="[('state','=','out_of_service')]"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
<group>
|
||||
<filter string="Facility" name="group_facility" context="{'group_by':'facility_id'}"/>
|
||||
<filter string="Work Center" name="group_wc" context="{'group_by':'work_center_id'}"/>
|
||||
<filter string="Process" name="group_process" context="{'group_by':'current_process_id'}"/>
|
||||
<filter string="Status" name="group_state" context="{'group_by':'state'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fp_tank" model="ir.actions.act_window">
|
||||
<field name="name">Tanks</field>
|
||||
<field name="res_model">fusion.plating.tank</field>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
<field name="search_view_id" ref="view_fp_tank_search"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
92
fusion-plating/fusion_plating/views/fp_work_center_views.xml
Normal file
92
fusion-plating/fusion_plating/views/fp_work_center_views.xml
Normal file
@@ -0,0 +1,92 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_work_center_list" model="ir.ui.view">
|
||||
<field name="name">fp.work.center.list</field>
|
||||
<field name="model">fusion.plating.work.center</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Work Centers">
|
||||
<field name="facility_id"/>
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="tank_count"/>
|
||||
<field name="capacity_per_day"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_work_center_form" model="ir.ui.view">
|
||||
<field name="name">fp.work.center.form</field>
|
||||
<field name="model">fusion.plating.work.center</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Work Center">
|
||||
<sheet>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" placeholder="e.g. Line 1 — EN Plating"/></h1>
|
||||
<div class="text-muted">
|
||||
<field name="code" placeholder="LINE-1"/>
|
||||
</div>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="facility_id"/>
|
||||
<field name="sequence"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="capacity_per_day"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Supported Processes">
|
||||
<field name="supported_process_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||
</page>
|
||||
<page string="Tanks">
|
||||
<field name="tank_ids">
|
||||
<list>
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="current_process_id"/>
|
||||
<field name="state"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_work_center_search" model="ir.ui.view">
|
||||
<field name="name">fp.work.center.search</field>
|
||||
<field name="model">fusion.plating.work.center</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Work Centers">
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="facility_id"/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
<group>
|
||||
<filter string="Facility" name="group_facility" context="{'group_by':'facility_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fp_work_center" model="ir.actions.act_window">
|
||||
<field name="name">Work Centers</field>
|
||||
<field name="res_model">fusion.plating.work.center</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_fp_work_center_search"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
46
fusion-plating/fusion_plating_aerospace/README.md
Normal file
46
fusion-plating/fusion_plating_aerospace/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Fusion Plating — Aerospace (AS9100 + Nadcap AC7108)
|
||||
|
||||
Part of the Fusion Plating product family by Nexa Systems Inc.
|
||||
|
||||
Industry pack that adds the aerospace-specific records a plating shop needs
|
||||
to support AS9100 Rev D certification and Nadcap AC7108 chemical-processing
|
||||
accreditation on top of the Fusion Plating QMS module.
|
||||
|
||||
## What this module adds
|
||||
|
||||
- **AS9100 Rev D clause catalog** — hierarchical, seeded with the main
|
||||
clauses 4 through 10 and the aerospace-critical 8.1.1 through 8.1.4
|
||||
family (Operational Risk Management, Configuration Management, Product
|
||||
Safety, Counterfeit Parts Prevention).
|
||||
- **Nadcap AC7108 audit records** — PRI-assigned auditor flag, merit and
|
||||
NCR counts, accreditation start/end dates, checklist selection covering
|
||||
AC7108 base and slash sheets 10/11/12/13/14.
|
||||
- **Counterfeit parts prevention log** — supports the AS9100 §8.1.4
|
||||
requirement and GIDEP reporting.
|
||||
- **Configuration management baseline** — records and tracks change
|
||||
history for configuration items (AS9100 §8.1.2).
|
||||
- **Risk register** — likelihood × impact scoring with automatic risk
|
||||
level classification (AS9100 §8.1.1).
|
||||
- **FAIR extension** — adds AS9102 form 1/2/3 attachments, drawing
|
||||
revision, and customer approval signature/date.
|
||||
- **Customer specification extension** — aerospace flag, AS9100 clause
|
||||
mapping, Nadcap requirement flag, PRI file code, customer approval
|
||||
requirement flag.
|
||||
|
||||
## Seed data
|
||||
|
||||
- ~25 AS9100 Rev D clauses.
|
||||
- Industry specs loaded with process-pack links: AMS 2404, ASTM B733,
|
||||
MIL-C-26074, MIL-A-8625, QQ-C-320, MIL-DTL-13924, AMS 2700, AMS 2759/9,
|
||||
AMS-QQ-P-416, BAC 5709, PRI AS7108.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Requires all four process packs (EN, chrome, anodize, black oxide) plus
|
||||
`fusion_plating_quality`. The process-pack dependency guarantees the
|
||||
seeded customer-spec process-type references always resolve.
|
||||
|
||||
## License
|
||||
|
||||
OPL-1 (Odoo Proprietary License v1.0). Copyright (c) 2026 Nexa Systems
|
||||
Inc. All rights reserved.
|
||||
6
fusion-plating/fusion_plating_aerospace/__init__.py
Normal file
6
fusion-plating/fusion_plating_aerospace/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from . import models
|
||||
93
fusion-plating/fusion_plating_aerospace/__manifest__.py
Normal file
93
fusion-plating/fusion_plating_aerospace/__manifest__.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Aerospace (AS9100 + Nadcap)',
|
||||
'version': '19.0.1.0.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Aerospace industry pack: AS9100 Rev D clause library, Nadcap AC7108 '
|
||||
'audits, counterfeit parts prevention, config management, risk register, '
|
||||
'FAIR/AS9102 extensions, industry spec library (AMS, ASTM, MIL, BAC).',
|
||||
'description': """
|
||||
Fusion Plating — Aerospace (AS9100 + Nadcap AC7108)
|
||||
====================================================
|
||||
|
||||
Part of the Fusion Plating product family by Nexa Systems Inc.
|
||||
|
||||
This industry pack layers aerospace-specific records, workflows and seed
|
||||
data on top of the Fusion Plating quality module (QMS). It is the
|
||||
baseline every AS9100-certified plating / metal finishing shop needs on
|
||||
top of the generic QMS.
|
||||
|
||||
Records added
|
||||
-------------
|
||||
* AS9100 Rev D clause catalog (hierarchical, seeded with the main
|
||||
clauses 4 through 10 plus the aerospace-critical 8.1.1–8.1.4 family)
|
||||
* Nadcap AC7108 audit records (PRI-assigned auditors, merit/NCR counts,
|
||||
accreditation start/end, checklist selection covering AC7108 base and
|
||||
slash sheets 10/11/12/13/14)
|
||||
* Counterfeit parts prevention log (AS9100 §8.1.4, GIDEP reporting)
|
||||
* Configuration management baseline (AS9100 §8.1.2)
|
||||
* Risk register with likelihood × impact scoring (AS9100 §8.1.1)
|
||||
* FAIR extension for AS9102 forms 1/2/3 and customer approval
|
||||
* Customer specification extension for aerospace flags, AS9100 clause
|
||||
mapping, Nadcap flag and PRI file code
|
||||
|
||||
Seed data
|
||||
---------
|
||||
* ~25 AS9100 Rev D clauses with a nested parent/child structure
|
||||
* Industry specs: AMS 2404, ASTM B733, MIL-C-26074, MIL-A-8625,
|
||||
QQ-C-320, MIL-DTL-13924, AMS 2700, AMS 2759/9, AMS-QQ-P-416,
|
||||
BAC 5709, PRI AS7108
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
Depends on all four process packs (EN, chrome, anodize, black oxide)
|
||||
plus the quality QMS module, so the seeded customer-spec ↔ process-type
|
||||
relationships always resolve.
|
||||
|
||||
Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
""",
|
||||
'author': 'Nexa Systems Inc.',
|
||||
'website': 'https://www.nexasystems.ca',
|
||||
'maintainer': 'Nexa Systems Inc.',
|
||||
'support': 'support@nexasystems.ca',
|
||||
'license': 'OPL-1',
|
||||
'price': 0.00,
|
||||
'currency': 'CAD',
|
||||
'depends': [
|
||||
'fusion_plating_quality',
|
||||
'fusion_plating_process_en',
|
||||
'fusion_plating_process_chrome',
|
||||
'fusion_plating_process_anodize',
|
||||
'fusion_plating_process_black_oxide',
|
||||
],
|
||||
'data': [
|
||||
'security/fp_aerospace_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/fp_sequence_data.xml',
|
||||
'data/fp_as9100_clause_data.xml',
|
||||
'data/fp_customer_spec_data.xml',
|
||||
'views/fp_as9100_clause_views.xml',
|
||||
'views/fp_nadcap_audit_views.xml',
|
||||
'views/fp_counterfeit_views.xml',
|
||||
'views/fp_config_item_views.xml',
|
||||
'views/fp_risk_views.xml',
|
||||
'views/fp_customer_spec_views.xml',
|
||||
'views/fp_fair_views.xml',
|
||||
'views/fp_menu.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/fp_demo_aerospace_data.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'fusion_plating_aerospace/static/src/scss/fusion_plating_aerospace.scss',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': False,
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
<?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.
|
||||
|
||||
Seed AS9100 Rev D clause hierarchy. Only the main top-level clauses
|
||||
(4–10) and the aerospace-critical 8.1.x family are included — a shop
|
||||
can extend this with the finer-grained sub-clauses via user data.
|
||||
-->
|
||||
<odoo noupdate="1">
|
||||
|
||||
<!-- ===== 4. CONTEXT OF THE ORGANIZATION ===== -->
|
||||
<record id="as9100_clause_4" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Context of the organization</field>
|
||||
<field name="code">4</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">leadership</field>
|
||||
<field name="description" type="html"><p>Understanding the organization and its context, interested parties, scope of the quality management system, and the QMS and its processes.</p></field>
|
||||
</record>
|
||||
|
||||
<!-- ===== 5. LEADERSHIP ===== -->
|
||||
<record id="as9100_clause_5" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Leadership</field>
|
||||
<field name="code">5</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">leadership</field>
|
||||
<field name="description" type="html"><p>Top-management leadership and commitment, quality policy, and organizational roles, responsibilities and authorities.</p></field>
|
||||
</record>
|
||||
|
||||
<!-- ===== 6. PLANNING ===== -->
|
||||
<record id="as9100_clause_6" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Planning</field>
|
||||
<field name="code">6</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">planning</field>
|
||||
<field name="description" type="html"><p>Actions to address risks and opportunities, quality objectives, and planning of changes.</p></field>
|
||||
</record>
|
||||
|
||||
<!-- ===== 7. SUPPORT ===== -->
|
||||
<record id="as9100_clause_7" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Support</field>
|
||||
<field name="code">7</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">support</field>
|
||||
<field name="description" type="html"><p>Resources, competence, awareness, communication, and documented information.</p></field>
|
||||
</record>
|
||||
|
||||
<!-- ===== 8. OPERATION ===== -->
|
||||
<record id="as9100_clause_8" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Operation</field>
|
||||
<field name="code">8</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">operation</field>
|
||||
<field name="description" type="html"><p>Operational planning and control of products and services, including design, external provision, production, release and nonconforming-output control.</p></field>
|
||||
</record>
|
||||
|
||||
<record id="as9100_clause_8_1" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Operational planning and control</field>
|
||||
<field name="code">8.1</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">operation</field>
|
||||
<field name="parent_id" ref="as9100_clause_8"/>
|
||||
<field name="description" type="html"><p>Planning, implementation and control of the processes needed to meet the requirements for the provision of products and services.</p></field>
|
||||
</record>
|
||||
|
||||
<record id="as9100_clause_8_1_1" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Operational risk management</field>
|
||||
<field name="code">8.1.1</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">operation</field>
|
||||
<field name="parent_id" ref="as9100_clause_8_1"/>
|
||||
<field name="description" type="html"><p>Establish, implement, and maintain a process for managing operational risks to the achievement of applicable requirements that includes assignment of responsibilities, definition of risk criteria, identification and assessment of risks, mitigation actions, and acceptance of residual risk.</p></field>
|
||||
</record>
|
||||
|
||||
<record id="as9100_clause_8_1_2" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Configuration management</field>
|
||||
<field name="code">8.1.2</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">operation</field>
|
||||
<field name="parent_id" ref="as9100_clause_8_1"/>
|
||||
<field name="description" type="html"><p>Establish a configuration-management process appropriate to the product with planning, identification, change control, status accounting and audits to ensure consistent product identification, traceability and control of physical and functional characteristics throughout the product life cycle.</p></field>
|
||||
</record>
|
||||
|
||||
<record id="as9100_clause_8_1_3" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Product safety</field>
|
||||
<field name="code">8.1.3</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">operation</field>
|
||||
<field name="parent_id" ref="as9100_clause_8_1"/>
|
||||
<field name="description" type="html"><p>Plan, implement and control the processes needed to assure product safety during the entire product life cycle, including hazard identification, risk assessment, management of identified risks, communication of risks to affected parties, and reporting and lessons learned.</p></field>
|
||||
</record>
|
||||
|
||||
<record id="as9100_clause_8_1_4" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Prevention of counterfeit parts</field>
|
||||
<field name="code">8.1.4</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">operation</field>
|
||||
<field name="parent_id" ref="as9100_clause_8_1"/>
|
||||
<field name="description" type="html"><p>Plan, implement and control processes, appropriate to the organization and the product, for the prevention of counterfeit or suspect-counterfeit part use and their inclusion in products delivered to the customer. Includes training, source controls, material and part verification, in-process control, quarantine, and reporting.</p></field>
|
||||
</record>
|
||||
|
||||
<record id="as9100_clause_8_2" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Requirements for products and services</field>
|
||||
<field name="code">8.2</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">operation</field>
|
||||
<field name="parent_id" ref="as9100_clause_8"/>
|
||||
<field name="description" type="html"><p>Customer communication, determination and review of requirements, and changes to requirements for products and services.</p></field>
|
||||
</record>
|
||||
|
||||
<record id="as9100_clause_8_2_3_1" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Customer communication</field>
|
||||
<field name="code">8.2.3.1</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">operation</field>
|
||||
<field name="parent_id" ref="as9100_clause_8_2"/>
|
||||
<field name="description" type="html"><p>Review of requirements for products and services — ensuring the organization's ability to meet requirements before committing to supply. Handling of enquiries, contracts or order handling, customer feedback including complaints, and specific requirements for contingency actions.</p></field>
|
||||
</record>
|
||||
|
||||
<record id="as9100_clause_8_3" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Design and development of products and services</field>
|
||||
<field name="code">8.3</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">operation</field>
|
||||
<field name="parent_id" ref="as9100_clause_8"/>
|
||||
<field name="description" type="html"><p>Design and development planning, inputs, controls, outputs and changes.</p></field>
|
||||
</record>
|
||||
|
||||
<record id="as9100_clause_8_4" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Control of externally provided processes, products and services</field>
|
||||
<field name="code">8.4</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">operation</field>
|
||||
<field name="parent_id" ref="as9100_clause_8"/>
|
||||
<field name="description" type="html"><p>Evaluation, selection, monitoring, and re-evaluation of external providers; type and extent of control; information for external providers.</p></field>
|
||||
</record>
|
||||
|
||||
<record id="as9100_clause_8_5" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Production and service provision</field>
|
||||
<field name="code">8.5</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">operation</field>
|
||||
<field name="parent_id" ref="as9100_clause_8"/>
|
||||
<field name="description" type="html"><p>Control of production and service provision, identification and traceability, property belonging to customers or external providers, preservation, post-delivery activities and control of changes.</p></field>
|
||||
</record>
|
||||
|
||||
<record id="as9100_clause_8_5_1_3" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Post-delivery support</field>
|
||||
<field name="code">8.5.1.3</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">operation</field>
|
||||
<field name="parent_id" ref="as9100_clause_8_5"/>
|
||||
<field name="description" type="html"><p>Determine and meet requirements for post-delivery activities associated with products and services, including collection and analysis of in-service data, actions to be taken as a result of product problems, control and updating of technical documentation, approval and control of repairs, and controls required for off-site work.</p></field>
|
||||
</record>
|
||||
|
||||
<record id="as9100_clause_8_6" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Release of products and services</field>
|
||||
<field name="code">8.6</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">operation</field>
|
||||
<field name="parent_id" ref="as9100_clause_8"/>
|
||||
<field name="description" type="html"><p>Implement planned arrangements, at appropriate stages, to verify that product and service requirements have been met, including First Article Inspection requirements.</p></field>
|
||||
</record>
|
||||
|
||||
<record id="as9100_clause_8_7" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Control of nonconforming outputs</field>
|
||||
<field name="code">8.7</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">operation</field>
|
||||
<field name="parent_id" ref="as9100_clause_8"/>
|
||||
<field name="description" type="html"><p>Ensure that nonconforming outputs are identified and controlled to prevent unintended use or delivery. Includes segregation, authority for disposition, and use of concessions.</p></field>
|
||||
</record>
|
||||
|
||||
<!-- ===== 9. PERFORMANCE EVALUATION ===== -->
|
||||
<record id="as9100_clause_9" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Performance evaluation</field>
|
||||
<field name="code">9</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">performance</field>
|
||||
<field name="description" type="html"><p>Monitoring, measurement, analysis and evaluation; internal audit; management review.</p></field>
|
||||
</record>
|
||||
|
||||
<record id="as9100_clause_9_2" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Internal audit</field>
|
||||
<field name="code">9.2</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">performance</field>
|
||||
<field name="parent_id" ref="as9100_clause_9"/>
|
||||
<field name="description" type="html"><p>Conduct internal audits at planned intervals to provide information on whether the QMS conforms to the organization's own requirements, the requirements of AS9100, and is effectively implemented and maintained.</p></field>
|
||||
</record>
|
||||
|
||||
<record id="as9100_clause_9_3" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Management review</field>
|
||||
<field name="code">9.3</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">performance</field>
|
||||
<field name="parent_id" ref="as9100_clause_9"/>
|
||||
<field name="description" type="html"><p>Top management reviews the organization's QMS at planned intervals to ensure its continuing suitability, adequacy, effectiveness and alignment with the strategic direction of the organization.</p></field>
|
||||
</record>
|
||||
|
||||
<!-- ===== 10. IMPROVEMENT ===== -->
|
||||
<record id="as9100_clause_10" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Improvement</field>
|
||||
<field name="code">10</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">improvement</field>
|
||||
<field name="description" type="html"><p>General; nonconformity and corrective action; continual improvement.</p></field>
|
||||
</record>
|
||||
|
||||
<record id="as9100_clause_10_2" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Nonconformity and corrective action</field>
|
||||
<field name="code">10.2</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">improvement</field>
|
||||
<field name="parent_id" ref="as9100_clause_10"/>
|
||||
<field name="description" type="html"><p>React to nonconformities, evaluate the need for action to eliminate the causes, implement any action needed, review the effectiveness of any corrective action taken, update risks and opportunities, and make changes to the QMS if necessary.</p></field>
|
||||
</record>
|
||||
|
||||
<record id="as9100_clause_10_3" model="fusion.plating.as9100.clause">
|
||||
<field name="name">Continual improvement</field>
|
||||
<field name="code">10.3</field>
|
||||
<field name="standard">as9100d</field>
|
||||
<field name="category">improvement</field>
|
||||
<field name="parent_id" ref="as9100_clause_10"/>
|
||||
<field name="description" type="html"><p>Continually improve the suitability, adequacy, and effectiveness of the QMS, including considering outputs of analysis and evaluation, and management review.</p></field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,151 @@
|
||||
<?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.
|
||||
|
||||
Seed aerospace industry specifications as fusion.plating.customer.spec
|
||||
records with spec_type='industry'. All four process packs (EN, chrome,
|
||||
anodize, black oxide) are listed as hard dependencies in the manifest
|
||||
so the ``ref()`` calls below always resolve at install time.
|
||||
-->
|
||||
<odoo noupdate="1">
|
||||
|
||||
<!-- ===== AMS 2404 — Electroless Nickel Plating ===== -->
|
||||
<record id="spec_ams_2404" model="fusion.plating.customer.spec">
|
||||
<field name="code">AMS 2404</field>
|
||||
<field name="name">Electroless Nickel Plating</field>
|
||||
<field name="spec_type">industry</field>
|
||||
<field name="x_fc_is_aerospace" eval="True"/>
|
||||
<field name="x_fc_nadcap_required" eval="True"/>
|
||||
<field name="x_fc_requires_first_article" eval="True"/>
|
||||
<field name="process_type_ids" eval="[(6, 0, [
|
||||
ref('fusion_plating_process_en.ptype_en_lp'),
|
||||
ref('fusion_plating_process_en.ptype_en_mp'),
|
||||
ref('fusion_plating_process_en.ptype_en_hp'),
|
||||
])]"/>
|
||||
</record>
|
||||
|
||||
<!-- ===== ASTM B733 — Autocatalytic (Electroless) Nickel-Phosphorus Coatings ===== -->
|
||||
<record id="spec_astm_b733" model="fusion.plating.customer.spec">
|
||||
<field name="code">ASTM B733</field>
|
||||
<field name="name">Autocatalytic (Electroless) Nickel-Phosphorus Coatings</field>
|
||||
<field name="spec_type">industry</field>
|
||||
<field name="x_fc_is_aerospace" eval="True"/>
|
||||
<field name="x_fc_requires_first_article" eval="True"/>
|
||||
<field name="process_type_ids" eval="[(6, 0, [
|
||||
ref('fusion_plating_process_en.ptype_en_lp'),
|
||||
ref('fusion_plating_process_en.ptype_en_mp'),
|
||||
ref('fusion_plating_process_en.ptype_en_hp'),
|
||||
])]"/>
|
||||
</record>
|
||||
|
||||
<!-- ===== MIL-C-26074 — Electroless Nickel Coatings ===== -->
|
||||
<record id="spec_mil_c_26074" model="fusion.plating.customer.spec">
|
||||
<field name="code">MIL-C-26074</field>
|
||||
<field name="name">Electroless Nickel Coatings</field>
|
||||
<field name="spec_type">industry</field>
|
||||
<field name="x_fc_is_aerospace" eval="True"/>
|
||||
<field name="x_fc_requires_first_article" eval="True"/>
|
||||
<field name="process_type_ids" eval="[(6, 0, [
|
||||
ref('fusion_plating_process_en.ptype_en_lp'),
|
||||
ref('fusion_plating_process_en.ptype_en_mp'),
|
||||
ref('fusion_plating_process_en.ptype_en_hp'),
|
||||
])]"/>
|
||||
</record>
|
||||
|
||||
<!-- ===== MIL-A-8625 — Anodic Coatings for Aluminum ===== -->
|
||||
<record id="spec_mil_a_8625" model="fusion.plating.customer.spec">
|
||||
<field name="code">MIL-A-8625</field>
|
||||
<field name="name">Anodic Coatings for Aluminum and Aluminum Alloys</field>
|
||||
<field name="spec_type">industry</field>
|
||||
<field name="x_fc_is_aerospace" eval="True"/>
|
||||
<field name="x_fc_nadcap_required" eval="True"/>
|
||||
<field name="x_fc_requires_first_article" eval="True"/>
|
||||
<field name="process_type_ids" eval="[(6, 0, [
|
||||
ref('fusion_plating_process_anodize.ptype_an_type_i'),
|
||||
ref('fusion_plating_process_anodize.ptype_an_type_ii'),
|
||||
ref('fusion_plating_process_anodize.ptype_an_type_ii_dye'),
|
||||
ref('fusion_plating_process_anodize.ptype_an_type_iii'),
|
||||
])]"/>
|
||||
</record>
|
||||
|
||||
<!-- ===== QQ-C-320 — Chromium Plating (Electrodeposited) ===== -->
|
||||
<record id="spec_qq_c_320" model="fusion.plating.customer.spec">
|
||||
<field name="code">QQ-C-320</field>
|
||||
<field name="name">Chromium Plating (Electrodeposited)</field>
|
||||
<field name="spec_type">industry</field>
|
||||
<field name="x_fc_is_aerospace" eval="True"/>
|
||||
<field name="x_fc_nadcap_required" eval="True"/>
|
||||
<field name="x_fc_requires_first_article" eval="True"/>
|
||||
<field name="process_type_ids" eval="[(6, 0, [
|
||||
ref('fusion_plating_process_chrome.ptype_cr_hard_hex'),
|
||||
ref('fusion_plating_process_chrome.ptype_cr_dec_hex'),
|
||||
])]"/>
|
||||
</record>
|
||||
|
||||
<!-- ===== MIL-DTL-13924 — Black Oxide Coating on Ferrous Metals ===== -->
|
||||
<record id="spec_mil_dtl_13924" model="fusion.plating.customer.spec">
|
||||
<field name="code">MIL-DTL-13924</field>
|
||||
<field name="name">Black Oxide Coating on Ferrous Metals</field>
|
||||
<field name="spec_type">industry</field>
|
||||
<field name="x_fc_is_aerospace" eval="True"/>
|
||||
<field name="process_type_ids" eval="[(6, 0, [
|
||||
ref('fusion_plating_process_black_oxide.ptype_box_hot'),
|
||||
ref('fusion_plating_process_black_oxide.ptype_box_mid'),
|
||||
ref('fusion_plating_process_black_oxide.ptype_box_rt'),
|
||||
])]"/>
|
||||
</record>
|
||||
|
||||
<!-- ===== AMS 2700 — Passivation of Corrosion-Resistant Steels ===== -->
|
||||
<record id="spec_ams_2700" model="fusion.plating.customer.spec">
|
||||
<field name="code">AMS 2700</field>
|
||||
<field name="name">Passivation of Corrosion-Resistant Steels</field>
|
||||
<field name="spec_type">industry</field>
|
||||
<field name="x_fc_is_aerospace" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- ===== AMS 2759/9 — Hydrogen Embrittlement Relief Bake ===== -->
|
||||
<record id="spec_ams_2759" model="fusion.plating.customer.spec">
|
||||
<field name="code">AMS 2759/9</field>
|
||||
<field name="name">Hydrogen Embrittlement Relief (Baking) of Steel Parts</field>
|
||||
<field name="spec_type">industry</field>
|
||||
<field name="x_fc_is_aerospace" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- ===== AMS-QQ-P-416 — Cadmium Plating (Electrodeposited) ===== -->
|
||||
<record id="spec_ams_qq_p_416" model="fusion.plating.customer.spec">
|
||||
<field name="code">AMS-QQ-P-416</field>
|
||||
<field name="name">Cadmium Plating (Electrodeposited)</field>
|
||||
<field name="spec_type">industry</field>
|
||||
<field name="x_fc_is_aerospace" eval="True"/>
|
||||
<field name="x_fc_nadcap_required" eval="True"/>
|
||||
<field name="x_fc_requires_first_article" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- ===== BAC 5709 — Boeing Electroless Nickel Plating ===== -->
|
||||
<record id="spec_bac_5709" model="fusion.plating.customer.spec">
|
||||
<field name="code">BAC 5709</field>
|
||||
<field name="name">Boeing — Electroless Nickel Plating</field>
|
||||
<field name="spec_type">industry</field>
|
||||
<field name="x_fc_is_aerospace" eval="True"/>
|
||||
<field name="x_fc_nadcap_required" eval="True"/>
|
||||
<field name="x_fc_requires_first_article" eval="True"/>
|
||||
<field name="x_fc_customer_approval_required" eval="True"/>
|
||||
<field name="process_type_ids" eval="[(6, 0, [
|
||||
ref('fusion_plating_process_en.ptype_en_mp'),
|
||||
ref('fusion_plating_process_en.ptype_en_hp'),
|
||||
])]"/>
|
||||
</record>
|
||||
|
||||
<!-- ===== PRI AS7108 — Nadcap Chemical Processing Accreditation ===== -->
|
||||
<record id="spec_pri_as7108" model="fusion.plating.customer.spec">
|
||||
<field name="code">PRI AS7108</field>
|
||||
<field name="name">Nadcap Chemical Processing Accreditation</field>
|
||||
<field name="spec_type">industry</field>
|
||||
<field name="x_fc_is_aerospace" eval="True"/>
|
||||
<field name="x_fc_nadcap_required" eval="True"/>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,150 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2026 Nexa Systems Inc. — Demo data for Fusion Plating Aerospace -->
|
||||
<odoo noupdate="1">
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════════
|
||||
Nadcap AC7108 Audits
|
||||
════════════════════════════════════════════════════════════════ -->
|
||||
|
||||
<!-- 1. Scheduled audit — electroless nickel checklist -->
|
||||
<record id="demo_nadcap_audit_scheduled" model="fusion.plating.nadcap.audit">
|
||||
<field name="name">NADCAP-DEMO-001</field>
|
||||
<field name="audit_date" eval="(DateTime.today() + timedelta(days=45)).strftime('%Y-%m-%d')"/>
|
||||
<field name="auditor_name">R. Patel</field>
|
||||
<field name="pri_auditor" eval="True"/>
|
||||
<field name="checklist">ac7108_10_electroless</field>
|
||||
<field name="state">scheduled</field>
|
||||
<field name="notes" type="html"><p>Scheduled electroless nickel slash-sheet audit for Q3 cycle.</p></field>
|
||||
</record>
|
||||
|
||||
<!-- 2. In-progress audit — chrome plating checklist -->
|
||||
<record id="demo_nadcap_audit_in_progress" model="fusion.plating.nadcap.audit">
|
||||
<field name="name">NADCAP-DEMO-002</field>
|
||||
<field name="audit_date" eval="(DateTime.today() - timedelta(days=3)).strftime('%Y-%m-%d')"/>
|
||||
<field name="auditor_name">J. Moreno</field>
|
||||
<field name="pri_auditor" eval="True"/>
|
||||
<field name="checklist">ac7108_12_chrome</field>
|
||||
<field name="state">in_progress</field>
|
||||
<field name="notes" type="html"><p>Chrome plating audit currently underway. Day 2 of on-site review.</p></field>
|
||||
</record>
|
||||
|
||||
<!-- 3. Closed audit — base checklist, accredited, 2 merits, 0 NCRs -->
|
||||
<record id="demo_nadcap_audit_closed" model="fusion.plating.nadcap.audit">
|
||||
<field name="name">NADCAP-DEMO-003</field>
|
||||
<field name="audit_date" eval="(DateTime.today() - timedelta(days=120)).strftime('%Y-%m-%d')"/>
|
||||
<field name="auditor_name">S. Whitfield</field>
|
||||
<field name="pri_auditor" eval="True"/>
|
||||
<field name="checklist">ac7108_base</field>
|
||||
<field name="result">accredited</field>
|
||||
<field name="merit_count">2</field>
|
||||
<field name="ncr_count">0</field>
|
||||
<field name="accreditation_start" eval="(DateTime.today() - timedelta(days=90)).strftime('%Y-%m-%d')"/>
|
||||
<field name="accreditation_end" eval="(DateTime.today() + timedelta(days=640)).strftime('%Y-%m-%d')"/>
|
||||
<field name="state">closed</field>
|
||||
<field name="notes" type="html"><p>Base checklist audit completed with full accreditation. Two merit observations noted for SPC controls and operator training programme.</p></field>
|
||||
</record>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════════
|
||||
Risk Register Entries
|
||||
════════════════════════════════════════════════════════════════ -->
|
||||
|
||||
<!-- 1. Operational risk — identified -->
|
||||
<record id="demo_risk_operational" model="fusion.plating.risk">
|
||||
<field name="name">RISK-DEMO-001</field>
|
||||
<field name="title">Tank temperature controller failure</field>
|
||||
<field name="description" type="html"><p>Single-point failure risk on Line 2 EN tank — temperature controller is end-of-life with no redundancy.</p></field>
|
||||
<field name="category">operational</field>
|
||||
<field name="likelihood">3</field>
|
||||
<field name="impact">4</field>
|
||||
<field name="state">identified</field>
|
||||
<field name="review_date" eval="(DateTime.today() + timedelta(days=30)).strftime('%Y-%m-%d')"/>
|
||||
</record>
|
||||
|
||||
<!-- 2. Supply chain risk — assessed -->
|
||||
<record id="demo_risk_supply_chain" model="fusion.plating.risk">
|
||||
<field name="name">RISK-DEMO-002</field>
|
||||
<field name="title">Single-source nickel sulphamate supply</field>
|
||||
<field name="description" type="html"><p>Only one approved vendor for nickel sulphamate concentrate. Lead time has increased to 12 weeks.</p></field>
|
||||
<field name="category">supply_chain</field>
|
||||
<field name="likelihood">2</field>
|
||||
<field name="impact">5</field>
|
||||
<field name="state">assessed</field>
|
||||
<field name="mitigation_plan" type="html"><p>Qualify secondary supplier (Atotech). Target completion by Q4.</p></field>
|
||||
<field name="review_date" eval="(DateTime.today() + timedelta(days=60)).strftime('%Y-%m-%d')"/>
|
||||
</record>
|
||||
|
||||
<!-- 3. Quality risk — treated -->
|
||||
<record id="demo_risk_quality" model="fusion.plating.risk">
|
||||
<field name="name">RISK-DEMO-003</field>
|
||||
<field name="title">Thickness measurement repeatability</field>
|
||||
<field name="description" type="html"><p>XRF gauge R&R study showed marginal repeatability on thin deposits (< 5 µm).</p></field>
|
||||
<field name="category">quality</field>
|
||||
<field name="likelihood">4</field>
|
||||
<field name="impact">3</field>
|
||||
<field name="state">treated</field>
|
||||
<field name="mitigation_plan" type="html"><p>Calibrated with new certified reference standards. Operator training refresher completed. Re-running R&R study next week.</p></field>
|
||||
<field name="review_date" eval="(DateTime.today() + timedelta(days=14)).strftime('%Y-%m-%d')"/>
|
||||
</record>
|
||||
|
||||
<!-- 4. Customer risk — monitored -->
|
||||
<record id="demo_risk_customer" model="fusion.plating.risk">
|
||||
<field name="name">RISK-DEMO-004</field>
|
||||
<field name="title">Prime OEM contract renewal uncertainty</field>
|
||||
<field name="description" type="html"><p>Largest aerospace customer contract expires in 6 months. RFQ response due next quarter.</p></field>
|
||||
<field name="category">customer</field>
|
||||
<field name="likelihood">2</field>
|
||||
<field name="impact">2</field>
|
||||
<field name="state">monitored</field>
|
||||
<field name="mitigation_plan" type="html"><p>Account review meetings scheduled monthly. Preparing competitive pricing and capacity expansion proposal.</p></field>
|
||||
<field name="review_date" eval="(DateTime.today() + timedelta(days=21)).strftime('%Y-%m-%d')"/>
|
||||
</record>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════════
|
||||
Configuration Management Items
|
||||
════════════════════════════════════════════════════════════════ -->
|
||||
|
||||
<!-- 1. EN plating procedure -->
|
||||
<record id="demo_config_item_en" model="fusion.plating.config.item">
|
||||
<field name="name">Electroless Nickel Plating Procedure</field>
|
||||
<field name="code">CFG-EN-001</field>
|
||||
<field name="baseline_revision">A</field>
|
||||
<field name="current_revision">C</field>
|
||||
<field name="approval_date" eval="(DateTime.today() - timedelta(days=60)).strftime('%Y-%m-%d')"/>
|
||||
<field name="change_history" type="html">
|
||||
<p><strong>Rev C</strong> — Updated rinse-water conductivity limits per customer spec.<br/>
|
||||
<strong>Rev B</strong> — Added mid-coat thickness checkpoint.<br/>
|
||||
<strong>Rev A</strong> — Initial baseline release.</p>
|
||||
</field>
|
||||
<field name="notes" type="html"><p>Covers AMS 2404 and MIL-C-26074 requirements for medium-phosphorus EN deposits.</p></field>
|
||||
</record>
|
||||
|
||||
<!-- 2. Chrome plating procedure -->
|
||||
<record id="demo_config_item_chrome" model="fusion.plating.config.item">
|
||||
<field name="name">Chrome Plating Procedure</field>
|
||||
<field name="code">CFG-CR-001</field>
|
||||
<field name="baseline_revision">A</field>
|
||||
<field name="current_revision">B</field>
|
||||
<field name="approval_date" eval="(DateTime.today() - timedelta(days=30)).strftime('%Y-%m-%d')"/>
|
||||
<field name="change_history" type="html">
|
||||
<p><strong>Rev B</strong> — Revised current-density parameters for thin hard chrome.<br/>
|
||||
<strong>Rev A</strong> — Initial baseline release.</p>
|
||||
</field>
|
||||
<field name="notes" type="html"><p>Covers QQ-C-320 and AMS 2460 requirements for hard chrome deposits on aerospace components.</p></field>
|
||||
</record>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════════
|
||||
Counterfeit Parts Prevention
|
||||
════════════════════════════════════════════════════════════════ -->
|
||||
|
||||
<record id="demo_counterfeit_investigation" model="fusion.plating.counterfeit.prevention">
|
||||
<field name="name">CFT-DEMO-001</field>
|
||||
<field name="incident_date" eval="(DateTime.today() - timedelta(days=10)).strftime('%Y-%m-%d')"/>
|
||||
<field name="part_number">NAS1149-C0363A</field>
|
||||
<field name="lot_serial">LOT-2026-04-A</field>
|
||||
<field name="detection_method">Certificate of conformance discrepancy flagged during receiving inspection</field>
|
||||
<field name="disposition">investigation</field>
|
||||
<field name="gidep_reported" eval="False"/>
|
||||
<field name="notes" type="html"><p>Supplier certificate references an outdated spec revision. Material quarantined pending metallurgical lab verification. GIDEP report to be filed if counterfeit is confirmed.</p></field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="seq_fp_nadcap_audit" model="ir.sequence">
|
||||
<field name="name">Fusion Plating: Nadcap Audit</field>
|
||||
<field name="code">fusion.plating.nadcap.audit</field>
|
||||
<field name="prefix">NADCAP/%(year)s/</field>
|
||||
<field name="padding">3</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="seq_fp_counterfeit" model="ir.sequence">
|
||||
<field name="name">Fusion Plating: Counterfeit Prevention</field>
|
||||
<field name="code">fusion.plating.counterfeit.prevention</field>
|
||||
<field name="prefix">CFT/%(year)s/</field>
|
||||
<field name="padding">4</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="seq_fp_risk" model="ir.sequence">
|
||||
<field name="name">Fusion Plating: Risk Register</field>
|
||||
<field name="code">fusion.plating.risk</field>
|
||||
<field name="prefix">RISK/%(year)s/</field>
|
||||
<field name="padding">3</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
12
fusion-plating/fusion_plating_aerospace/models/__init__.py
Normal file
12
fusion-plating/fusion_plating_aerospace/models/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from . import fp_as9100_clause
|
||||
from . import fp_nadcap_audit
|
||||
from . import fp_counterfeit_prevention
|
||||
from . import fp_config_item
|
||||
from . import fp_risk
|
||||
from . import fp_customer_spec
|
||||
from . import fp_fair
|
||||
@@ -0,0 +1,96 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class FpAs9100Clause(models.Model):
|
||||
"""AS9100 Rev D clause catalog.
|
||||
|
||||
A flat catalogue of clauses and sub-clauses from the AS9100 Rev D
|
||||
standard, plus related standards (ISO 9001:2015, etc.). Used by
|
||||
customer specifications and audit findings to pin a requirement to
|
||||
the specific paragraph of the standard it satisfies.
|
||||
"""
|
||||
_name = 'fusion.plating.as9100.clause'
|
||||
_description = 'Fusion Plating — AS9100 Clause'
|
||||
_order = 'standard, code, id'
|
||||
_parent_store = True
|
||||
_rec_name = 'display_name'
|
||||
|
||||
name = fields.Char(
|
||||
string='Name',
|
||||
required=True,
|
||||
translate=True,
|
||||
)
|
||||
display_name = fields.Char(
|
||||
compute='_compute_display_name',
|
||||
store=True,
|
||||
)
|
||||
code = fields.Char(
|
||||
string='Clause Code',
|
||||
required=True,
|
||||
help='Clause reference number, e.g. 8.1.2.',
|
||||
)
|
||||
parent_id = fields.Many2one(
|
||||
'fusion.plating.as9100.clause',
|
||||
string='Parent Clause',
|
||||
ondelete='cascade',
|
||||
index=True,
|
||||
)
|
||||
parent_path = fields.Char(index=True, unaccent=False)
|
||||
child_ids = fields.One2many(
|
||||
'fusion.plating.as9100.clause',
|
||||
'parent_id',
|
||||
string='Sub-clauses',
|
||||
)
|
||||
description = fields.Html(
|
||||
string='Description',
|
||||
translate=True,
|
||||
)
|
||||
standard = fields.Selection(
|
||||
[
|
||||
('as9100d', 'AS9100 Rev D'),
|
||||
('iso9001_2015', 'ISO 9001:2015'),
|
||||
('other', 'Other'),
|
||||
],
|
||||
string='Standard',
|
||||
default='as9100d',
|
||||
required=True,
|
||||
)
|
||||
category = fields.Selection(
|
||||
[
|
||||
('leadership', 'Leadership'),
|
||||
('planning', 'Planning'),
|
||||
('support', 'Support'),
|
||||
('operation', 'Operation'),
|
||||
('performance', 'Performance Evaluation'),
|
||||
('improvement', 'Improvement'),
|
||||
],
|
||||
string='Category',
|
||||
)
|
||||
notes = fields.Html(
|
||||
string='Notes',
|
||||
translate=True,
|
||||
)
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
'fp_as9100_clause_code_std_uniq',
|
||||
'unique(code, standard)',
|
||||
'A clause code must be unique per standard.',
|
||||
),
|
||||
]
|
||||
|
||||
@api.depends('code', 'name')
|
||||
def _compute_display_name(self):
|
||||
for rec in self:
|
||||
parts = []
|
||||
if rec.code:
|
||||
parts.append(rec.code)
|
||||
if rec.name:
|
||||
parts.append(rec.name)
|
||||
rec.display_name = ' — '.join(parts) if parts else ''
|
||||
@@ -0,0 +1,73 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class FpConfigItem(models.Model):
|
||||
"""Configuration management baseline item.
|
||||
|
||||
Tracks a configuration item (product, process, document, tooling,
|
||||
etc.) under formal configuration management control per AS9100
|
||||
§8.1.2. Each item has a baseline revision, the current in-use
|
||||
revision, and a change history log for traceability.
|
||||
"""
|
||||
_name = 'fusion.plating.config.item'
|
||||
_description = 'Fusion Plating — Configuration Management Item'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'name, id'
|
||||
|
||||
name = fields.Char(
|
||||
string='Name',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
code = fields.Char(
|
||||
string='Code',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
baseline_revision = fields.Char(
|
||||
string='Baseline Revision',
|
||||
tracking=True,
|
||||
help='The frozen baseline revision this item starts from.',
|
||||
)
|
||||
current_revision = fields.Char(
|
||||
string='Current Revision',
|
||||
tracking=True,
|
||||
help='The revision currently in effect — usually the most '
|
||||
'recently approved change.',
|
||||
)
|
||||
approved_by_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Approved By',
|
||||
tracking=True,
|
||||
)
|
||||
approval_date = fields.Date(
|
||||
string='Approval Date',
|
||||
tracking=True,
|
||||
)
|
||||
change_history = fields.Html(
|
||||
string='Change History',
|
||||
help='Manual log of baseline changes. Use dated entries to keep '
|
||||
'full traceability for auditors.',
|
||||
)
|
||||
notes = fields.Html(
|
||||
string='Notes',
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
'fp_config_item_code_uniq',
|
||||
'unique(code, company_id)',
|
||||
'Configuration item codes must be unique per company.',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class FpCounterfeitPrevention(models.Model):
|
||||
"""Counterfeit parts prevention incident log.
|
||||
|
||||
Records an incident where a suspected or confirmed counterfeit /
|
||||
fraudulent part, material, or raw stock was detected in the supply
|
||||
chain. Required by AS9100 §8.1.4 Counterfeit Parts Prevention.
|
||||
"""
|
||||
_name = 'fusion.plating.counterfeit.prevention'
|
||||
_description = 'Fusion Plating — Counterfeit Parts Prevention Log'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'incident_date desc, id desc'
|
||||
|
||||
name = fields.Char(
|
||||
string='Reference',
|
||||
required=True,
|
||||
copy=False,
|
||||
readonly=True,
|
||||
default=lambda self: self._default_name(),
|
||||
tracking=True,
|
||||
)
|
||||
incident_date = fields.Date(
|
||||
string='Incident Date',
|
||||
default=lambda self: fields.Date.context_today(self),
|
||||
tracking=True,
|
||||
)
|
||||
supplier_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Supplier',
|
||||
tracking=True,
|
||||
)
|
||||
part_number = fields.Char(
|
||||
string='Part Number',
|
||||
tracking=True,
|
||||
)
|
||||
lot_serial = fields.Char(
|
||||
string='Lot / Serial',
|
||||
tracking=True,
|
||||
)
|
||||
detection_method = fields.Char(
|
||||
string='Detection Method',
|
||||
help='How the counterfeit item was detected (receiving inspection, '
|
||||
'lab test, certificate discrepancy, etc.).',
|
||||
)
|
||||
disposition = fields.Selection(
|
||||
[
|
||||
('returned', 'Returned to Supplier'),
|
||||
('destroyed', 'Destroyed / Quarantined'),
|
||||
('investigation', 'Under Investigation'),
|
||||
],
|
||||
string='Disposition',
|
||||
default='investigation',
|
||||
tracking=True,
|
||||
)
|
||||
gidep_reported = fields.Boolean(
|
||||
string='GIDEP Reported',
|
||||
tracking=True,
|
||||
help='Reported to the Government Industry Data Exchange Program.',
|
||||
)
|
||||
notes = fields.Html(
|
||||
string='Notes',
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
@api.model
|
||||
def _default_name(self):
|
||||
seq = self.env['ir.sequence'].next_by_code('fusion.plating.counterfeit.prevention')
|
||||
return seq or '/'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if not vals.get('name') or vals.get('name') == '/':
|
||||
vals['name'] = self._default_name()
|
||||
return super().create(vals_list)
|
||||
@@ -0,0 +1,54 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class FpCustomerSpec(models.Model):
|
||||
"""Aerospace extension to the quality customer-spec model.
|
||||
|
||||
Adds the small set of aerospace-specific flags and linkages the
|
||||
quality module deliberately keeps out of its generic implementation.
|
||||
"""
|
||||
_inherit = 'fusion.plating.customer.spec'
|
||||
|
||||
x_fc_is_aerospace = fields.Boolean(
|
||||
string='Aerospace Spec',
|
||||
tracking=True,
|
||||
help='Check to mark this specification as aerospace-relevant so '
|
||||
'it shows up in aerospace filters and dashboards.',
|
||||
)
|
||||
x_fc_as9100_clause_ids = fields.Many2many(
|
||||
'fusion.plating.as9100.clause',
|
||||
'fp_customer_spec_as9100_clause_rel',
|
||||
'spec_id',
|
||||
'clause_id',
|
||||
string='Related AS9100 Clauses',
|
||||
help='AS9100 Rev D clauses this specification maps to.',
|
||||
)
|
||||
x_fc_nadcap_required = fields.Boolean(
|
||||
string='Nadcap Required',
|
||||
tracking=True,
|
||||
help='This specification can only be satisfied by a Nadcap-accredited '
|
||||
'supplier.',
|
||||
)
|
||||
x_fc_requires_first_article = fields.Boolean(
|
||||
string='Requires First Article',
|
||||
tracking=True,
|
||||
help='Running this specification on a new part number or revision '
|
||||
'requires a full FAIR on file.',
|
||||
)
|
||||
x_fc_pri_file_code = fields.Char(
|
||||
string='PRI File Code',
|
||||
tracking=True,
|
||||
help='Performance Review Institute file code assigned to this '
|
||||
'specification, when known.',
|
||||
)
|
||||
x_fc_customer_approval_required = fields.Boolean(
|
||||
string='Customer Approval Required',
|
||||
tracking=True,
|
||||
help='Each job against this spec requires explicit customer '
|
||||
'approval before shipping.',
|
||||
)
|
||||
54
fusion-plating/fusion_plating_aerospace/models/fp_fair.py
Normal file
54
fusion-plating/fusion_plating_aerospace/models/fp_fair.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class FpFair(models.Model):
|
||||
"""Aerospace extension to the quality FAIR model.
|
||||
|
||||
Adds the AS9102 form 1/2/3 attachment slots and the customer-approval
|
||||
signature block that aerospace customers routinely require on top of
|
||||
the generic first-article report.
|
||||
"""
|
||||
_inherit = 'fusion.plating.fair'
|
||||
|
||||
x_fc_as9102_form1 = fields.Binary(
|
||||
string='AS9102 Form 1 (Part Accountability)',
|
||||
help='Scanned / generated copy of AS9102 Form 1 — part '
|
||||
'accountability header.',
|
||||
)
|
||||
x_fc_as9102_form1_filename = fields.Char(string='Form 1 Filename')
|
||||
|
||||
x_fc_as9102_form2 = fields.Binary(
|
||||
string='AS9102 Form 2 (Product Accountability)',
|
||||
help='Scanned / generated copy of AS9102 Form 2 — product '
|
||||
'accountability, raw material / process / inspection records.',
|
||||
)
|
||||
x_fc_as9102_form2_filename = fields.Char(string='Form 2 Filename')
|
||||
|
||||
x_fc_as9102_form3 = fields.Binary(
|
||||
string='AS9102 Form 3 (Characteristic Accountability)',
|
||||
help='Scanned / generated copy of AS9102 Form 3 — characteristic '
|
||||
'accountability, verification, and compatibility evaluation.',
|
||||
)
|
||||
x_fc_as9102_form3_filename = fields.Char(string='Form 3 Filename')
|
||||
|
||||
x_fc_drawing_revision = fields.Char(
|
||||
string='Drawing Revision',
|
||||
tracking=True,
|
||||
help='Customer drawing revision this FAIR is against — must match '
|
||||
'the revision shipped to the customer.',
|
||||
)
|
||||
x_fc_customer_approval_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Customer Approver',
|
||||
tracking=True,
|
||||
help='User who recorded the customer-side approval of this FAIR.',
|
||||
)
|
||||
x_fc_customer_approval_date = fields.Date(
|
||||
string='Customer Approval Date',
|
||||
tracking=True,
|
||||
)
|
||||
@@ -0,0 +1,132 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class FpNadcapAudit(models.Model):
|
||||
"""Nadcap AC7108 chemical-processing audit record.
|
||||
|
||||
Tracks a single Nadcap audit against AC7108 (base checklist) and its
|
||||
slash sheets. This is deliberately kept separate from the generic
|
||||
fusion.plating.audit model in the quality module because Nadcap has
|
||||
its own accreditation lifecycle, PRI-assigned auditors, and
|
||||
merit/NCR tracking that don't map cleanly to the generic audit flow.
|
||||
"""
|
||||
_name = 'fusion.plating.nadcap.audit'
|
||||
_description = 'Fusion Plating — Nadcap AC7108 Audit'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'audit_date desc, id desc'
|
||||
|
||||
name = fields.Char(
|
||||
string='Reference',
|
||||
required=True,
|
||||
copy=False,
|
||||
readonly=True,
|
||||
default=lambda self: self._default_name(),
|
||||
tracking=True,
|
||||
)
|
||||
audit_date = fields.Date(
|
||||
string='Audit Date',
|
||||
tracking=True,
|
||||
)
|
||||
auditor_name = fields.Char(
|
||||
string='Auditor',
|
||||
tracking=True,
|
||||
help='Name of the individual auditor performing the assessment.',
|
||||
)
|
||||
pri_auditor = fields.Boolean(
|
||||
string='PRI-Assigned Auditor',
|
||||
tracking=True,
|
||||
help='Checked when the auditor was assigned by the Performance '
|
||||
'Review Institute rather than a self-nomination.',
|
||||
)
|
||||
checklist = fields.Selection(
|
||||
[
|
||||
('ac7108_base', 'AC7108 — Base Checklist'),
|
||||
('ac7108_10_electroless', 'AC7108/10 — Electroless Nickel'),
|
||||
('ac7108_11_brush', 'AC7108/11 — Brush Plating'),
|
||||
('ac7108_12_chrome', 'AC7108/12 — Chromium Plating'),
|
||||
('ac7108_13_anodize', 'AC7108/13 — Anodize'),
|
||||
('ac7108_14_conv', 'AC7108/14 — Conversion Coating'),
|
||||
],
|
||||
string='Checklist',
|
||||
default='ac7108_base',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
result = fields.Selection(
|
||||
[
|
||||
('accredited', 'Accredited'),
|
||||
('conditional', 'Conditional'),
|
||||
('failed', 'Failed'),
|
||||
],
|
||||
string='Result',
|
||||
tracking=True,
|
||||
)
|
||||
merit_count = fields.Integer(
|
||||
string='Merits',
|
||||
help='Count of merit-worthy observations recorded during the '
|
||||
'audit (positive findings).',
|
||||
)
|
||||
ncr_count = fields.Integer(
|
||||
string='NCRs',
|
||||
help='Non-conformance reports raised during the audit.',
|
||||
)
|
||||
audit_report_attachment = fields.Many2one(
|
||||
'ir.attachment',
|
||||
string='Audit Report',
|
||||
help='Final audit report PDF.',
|
||||
)
|
||||
accreditation_start = fields.Date(
|
||||
string='Accreditation Start',
|
||||
tracking=True,
|
||||
)
|
||||
accreditation_end = fields.Date(
|
||||
string='Accreditation End',
|
||||
tracking=True,
|
||||
)
|
||||
state = fields.Selection(
|
||||
[
|
||||
('scheduled', 'Scheduled'),
|
||||
('in_progress', 'In Progress'),
|
||||
('report_issued', 'Report Issued'),
|
||||
('closed', 'Closed'),
|
||||
],
|
||||
string='Status',
|
||||
default='scheduled',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
notes = fields.Html(
|
||||
string='Notes',
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
@api.model
|
||||
def _default_name(self):
|
||||
seq = self.env['ir.sequence'].next_by_code('fusion.plating.nadcap.audit')
|
||||
return seq or '/'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if not vals.get('name') or vals.get('name') == '/':
|
||||
vals['name'] = self._default_name()
|
||||
return super().create(vals_list)
|
||||
|
||||
def action_start(self):
|
||||
self.write({'state': 'in_progress'})
|
||||
|
||||
def action_issue_report(self):
|
||||
self.write({'state': 'report_issued'})
|
||||
|
||||
def action_close(self):
|
||||
self.write({'state': 'closed'})
|
||||
176
fusion-plating/fusion_plating_aerospace/models/fp_risk.py
Normal file
176
fusion-plating/fusion_plating_aerospace/models/fp_risk.py
Normal file
@@ -0,0 +1,176 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class FpRisk(models.Model):
|
||||
"""Risk register entry.
|
||||
|
||||
Implements the operational risk management requirement from AS9100
|
||||
Rev D §8.1.1. Each risk gets scored on a 1–5 likelihood and 1–5
|
||||
impact scale; the multiplicative score drives the level classification
|
||||
(low / medium / high / critical) used for escalation and reporting.
|
||||
"""
|
||||
_name = 'fusion.plating.risk'
|
||||
_description = 'Fusion Plating — Risk Register Entry'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'risk_score desc, id desc'
|
||||
|
||||
name = fields.Char(
|
||||
string='Reference',
|
||||
required=True,
|
||||
copy=False,
|
||||
readonly=True,
|
||||
default=lambda self: self._default_name(),
|
||||
tracking=True,
|
||||
)
|
||||
title = fields.Char(
|
||||
string='Title',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
description = fields.Html(
|
||||
string='Description',
|
||||
)
|
||||
category = fields.Selection(
|
||||
[
|
||||
('operational', 'Operational'),
|
||||
('supply_chain', 'Supply Chain'),
|
||||
('quality', 'Quality'),
|
||||
('safety', 'Safety'),
|
||||
('environmental', 'Environmental'),
|
||||
('financial', 'Financial'),
|
||||
('customer', 'Customer'),
|
||||
('regulatory', 'Regulatory'),
|
||||
],
|
||||
string='Category',
|
||||
default='operational',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
likelihood = fields.Selection(
|
||||
[
|
||||
('1', '1 — Very Low'),
|
||||
('2', '2 — Low'),
|
||||
('3', '3 — Medium'),
|
||||
('4', '4 — High'),
|
||||
('5', '5 — Very High'),
|
||||
],
|
||||
string='Likelihood',
|
||||
default='3',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
impact = fields.Selection(
|
||||
[
|
||||
('1', '1 — Very Low'),
|
||||
('2', '2 — Low'),
|
||||
('3', '3 — Medium'),
|
||||
('4', '4 — High'),
|
||||
('5', '5 — Very High'),
|
||||
],
|
||||
string='Impact',
|
||||
default='3',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
risk_score = fields.Integer(
|
||||
string='Score',
|
||||
compute='_compute_risk_score',
|
||||
store=True,
|
||||
help='Likelihood × Impact (1–25).',
|
||||
)
|
||||
risk_level = fields.Selection(
|
||||
[
|
||||
('low', 'Low'),
|
||||
('medium', 'Medium'),
|
||||
('high', 'High'),
|
||||
('critical', 'Critical'),
|
||||
],
|
||||
string='Level',
|
||||
compute='_compute_risk_level',
|
||||
store=True,
|
||||
tracking=True,
|
||||
)
|
||||
mitigation_plan = fields.Html(
|
||||
string='Mitigation Plan',
|
||||
)
|
||||
owner_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Owner',
|
||||
default=lambda self: self.env.user,
|
||||
tracking=True,
|
||||
)
|
||||
review_date = fields.Date(
|
||||
string='Next Review',
|
||||
tracking=True,
|
||||
)
|
||||
state = fields.Selection(
|
||||
[
|
||||
('identified', 'Identified'),
|
||||
('assessed', 'Assessed'),
|
||||
('treated', 'Treated'),
|
||||
('monitored', 'Monitored'),
|
||||
('closed', 'Closed'),
|
||||
],
|
||||
string='Status',
|
||||
default='identified',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
@api.model
|
||||
def _default_name(self):
|
||||
seq = self.env['ir.sequence'].next_by_code('fusion.plating.risk')
|
||||
return seq or '/'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if not vals.get('name') or vals.get('name') == '/':
|
||||
vals['name'] = self._default_name()
|
||||
return super().create(vals_list)
|
||||
|
||||
@api.depends('likelihood', 'impact')
|
||||
def _compute_risk_score(self):
|
||||
for rec in self:
|
||||
try:
|
||||
likely = int(rec.likelihood or 0)
|
||||
impact = int(rec.impact or 0)
|
||||
except (TypeError, ValueError):
|
||||
likely, impact = 0, 0
|
||||
rec.risk_score = likely * impact
|
||||
|
||||
@api.depends('risk_score')
|
||||
def _compute_risk_level(self):
|
||||
for rec in self:
|
||||
score = rec.risk_score or 0
|
||||
if score >= 20:
|
||||
rec.risk_level = 'critical'
|
||||
elif score >= 12:
|
||||
rec.risk_level = 'high'
|
||||
elif score >= 6:
|
||||
rec.risk_level = 'medium'
|
||||
else:
|
||||
rec.risk_level = 'low'
|
||||
|
||||
def action_assess(self):
|
||||
self.write({'state': 'assessed'})
|
||||
|
||||
def action_treat(self):
|
||||
self.write({'state': 'treated'})
|
||||
|
||||
def action_monitor(self):
|
||||
self.write({'state': 'monitored'})
|
||||
|
||||
def action_close(self):
|
||||
self.write({'state': 'closed'})
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!--
|
||||
This module reuses the core groups from fusion_plating:
|
||||
|
||||
fusion_plating.group_fusion_plating_operator
|
||||
fusion_plating.group_fusion_plating_supervisor
|
||||
fusion_plating.group_fusion_plating_manager
|
||||
fusion_plating.group_fusion_plating_admin
|
||||
|
||||
No new res.groups records are introduced here. All access control
|
||||
is expressed in security/ir.model.access.csv via those existing
|
||||
groups, so a single user role works across the core, the QMS and
|
||||
the aerospace pack.
|
||||
-->
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,16 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_fp_as9100_clause_operator,fp.as9100.clause.operator,model_fusion_plating_as9100_clause,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_as9100_clause_supervisor,fp.as9100.clause.supervisor,model_fusion_plating_as9100_clause,fusion_plating.group_fusion_plating_supervisor,1,0,0,0
|
||||
access_fp_as9100_clause_manager,fp.as9100.clause.manager,model_fusion_plating_as9100_clause,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_nadcap_audit_operator,fp.nadcap.audit.operator,model_fusion_plating_nadcap_audit,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_nadcap_audit_supervisor,fp.nadcap.audit.supervisor,model_fusion_plating_nadcap_audit,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
|
||||
access_fp_nadcap_audit_manager,fp.nadcap.audit.manager,model_fusion_plating_nadcap_audit,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_counterfeit_operator,fp.counterfeit.operator,model_fusion_plating_counterfeit_prevention,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_counterfeit_supervisor,fp.counterfeit.supervisor,model_fusion_plating_counterfeit_prevention,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
|
||||
access_fp_counterfeit_manager,fp.counterfeit.manager,model_fusion_plating_counterfeit_prevention,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_config_item_operator,fp.config.item.operator,model_fusion_plating_config_item,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_config_item_supervisor,fp.config.item.supervisor,model_fusion_plating_config_item,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
|
||||
access_fp_config_item_manager,fp.config.item.manager,model_fusion_plating_config_item,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_risk_operator,fp.risk.operator,model_fusion_plating_risk,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_risk_supervisor,fp.risk.supervisor,model_fusion_plating_risk,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
|
||||
access_fp_risk_manager,fp.risk.manager,model_fusion_plating_risk,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
|
@@ -0,0 +1,109 @@
|
||||
// =============================================================================
|
||||
// Fusion Plating — Aerospace pack styles
|
||||
// Copyright 2026 Nexa Systems Inc.
|
||||
// License OPL-1 (Odoo Proprietary License v1.0)
|
||||
//
|
||||
// THEME AWARENESS
|
||||
// ---------------
|
||||
// This file uses ONLY Bootstrap / Odoo CSS custom properties so it renders
|
||||
// correctly in both light and dark mode:
|
||||
//
|
||||
// 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)
|
||||
// brand: var(--o-action)
|
||||
//
|
||||
// Semantic status colours are tinted via `color-mix()` against the Bootstrap
|
||||
// theme tokens so a green badge adapts automatically between light and dark.
|
||||
// =============================================================================
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Local helper — tint a semantic colour against the surface.
|
||||
// -----------------------------------------------------------------------------
|
||||
@mixin fp-aero-tint($color-var, $amount: 12%) {
|
||||
background-color: color-mix(in srgb, var(#{$color-var}) #{$amount}, transparent);
|
||||
color: var(#{$color-var});
|
||||
border: 1px solid color-mix(in srgb, var(#{$color-var}) 35%, transparent);
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Risk register card — severity-tinted callout on the risk form
|
||||
// -----------------------------------------------------------------------------
|
||||
.o_fp_risk_card {
|
||||
background-color: var(--o-view-background-color, var(--bs-body-bg));
|
||||
color: var(--bs-body-color);
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-left-width: 4px;
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 12px;
|
||||
transition: border-color 120ms ease, box-shadow 120ms ease;
|
||||
|
||||
&[data-level="low"] {
|
||||
border-left-color: var(--bs-success);
|
||||
background-color: color-mix(in srgb, var(--bs-success) 6%, transparent);
|
||||
}
|
||||
|
||||
&[data-level="medium"] {
|
||||
border-left-color: var(--bs-info, var(--o-action));
|
||||
background-color: color-mix(in srgb, var(--bs-info, var(--o-action)) 6%, transparent);
|
||||
}
|
||||
|
||||
&[data-level="high"] {
|
||||
border-left-color: var(--bs-warning);
|
||||
background-color: color-mix(in srgb, var(--bs-warning) 8%, transparent);
|
||||
}
|
||||
|
||||
&[data-level="critical"] {
|
||||
border-left-color: var(--bs-danger);
|
||||
background-color: color-mix(in srgb, var(--bs-danger) 10%, transparent);
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--bs-danger) 20%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Nadcap badge — compact accreditation indicator on the audit form header
|
||||
// -----------------------------------------------------------------------------
|
||||
.o_fp_nadcap_badge {
|
||||
display: inline-block;
|
||||
padding: 3px 10px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
border-radius: 999px;
|
||||
@include fp-aero-tint(--o-action);
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// AS9100 clause tree — subtle hierarchy cueing for nested clause codes
|
||||
// -----------------------------------------------------------------------------
|
||||
.o_fp_as9100_tree {
|
||||
.o_data_row {
|
||||
// Tighten row padding a touch so long clause names stay legible
|
||||
// without dominating the screen.
|
||||
--fp-aero-row-pad: 6px;
|
||||
|
||||
> td {
|
||||
padding-top: var(--fp-aero-row-pad);
|
||||
padding-bottom: var(--fp-aero-row-pad);
|
||||
}
|
||||
}
|
||||
|
||||
// Monospace the clause code column for easy scanning of 8.1.2.3 style ids.
|
||||
td[name="code"] {
|
||||
font-family: var(--bs-font-monospace, monospace);
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
// Muted parent column — it's context, not the focus.
|
||||
td[name="parent_id"] {
|
||||
color: var(--bs-secondary-color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_as9100_clause_list" model="ir.ui.view">
|
||||
<field name="name">fp.as9100.clause.list</field>
|
||||
<field name="model">fusion.plating.as9100.clause</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="AS9100 Clauses" class="o_fp_as9100_tree">
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="standard" widget="badge"/>
|
||||
<field name="category"/>
|
||||
<field name="parent_id"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_as9100_clause_form" model="ir.ui.view">
|
||||
<field name="name">fp.as9100.clause.form</field>
|
||||
<field name="model">fusion.plating.as9100.clause</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="AS9100 Clause">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="code"/>
|
||||
<field name="standard"/>
|
||||
<field name="category"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="parent_id"/>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Description">
|
||||
<field name="description"/>
|
||||
</page>
|
||||
<page string="Sub-clauses">
|
||||
<field name="child_ids">
|
||||
<list>
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Notes">
|
||||
<field name="notes"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_as9100_clause_search" model="ir.ui.view">
|
||||
<field name="name">fp.as9100.clause.search</field>
|
||||
<field name="model">fusion.plating.as9100.clause</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="AS9100 Clauses">
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<separator/>
|
||||
<filter string="AS9100D" name="as9100d" domain="[('standard','=','as9100d')]"/>
|
||||
<filter string="ISO 9001:2015" name="iso9001" domain="[('standard','=','iso9001_2015')]"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
<group>
|
||||
<filter string="Standard" name="group_standard" context="{'group_by':'standard'}"/>
|
||||
<filter string="Category" name="group_category" context="{'group_by':'category'}"/>
|
||||
<filter string="Parent" name="group_parent" context="{'group_by':'parent_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fp_as9100_clause" model="ir.actions.act_window">
|
||||
<field name="name">AS9100 Clauses</field>
|
||||
<field name="res_model">fusion.plating.as9100.clause</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_fp_as9100_clause_search"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,84 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_config_item_list" model="ir.ui.view">
|
||||
<field name="name">fp.config.item.list</field>
|
||||
<field name="model">fusion.plating.config.item</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Configuration Items">
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="baseline_revision"/>
|
||||
<field name="current_revision"/>
|
||||
<field name="approved_by_id"/>
|
||||
<field name="approval_date"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_config_item_form" model="ir.ui.view">
|
||||
<field name="name">fp.config.item.form</field>
|
||||
<field name="model">fusion.plating.config.item</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Configuration Item">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="code"/>
|
||||
<field name="baseline_revision"/>
|
||||
<field name="current_revision"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="approved_by_id"/>
|
||||
<field name="approval_date"/>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Change History">
|
||||
<field name="change_history"/>
|
||||
</page>
|
||||
<page string="Notes">
|
||||
<field name="notes"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_config_item_search" model="ir.ui.view">
|
||||
<field name="name">fp.config.item.search</field>
|
||||
<field name="model">fusion.plating.config.item</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Configuration Items">
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="approved_by_id"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
<group>
|
||||
<filter string="Approver" name="group_approver" context="{'group_by':'approved_by_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fp_config_item" model="ir.actions.act_window">
|
||||
<field name="name">Configuration Items</field>
|
||||
<field name="res_model">fusion.plating.config.item</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_fp_config_item_search"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,92 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_counterfeit_list" model="ir.ui.view">
|
||||
<field name="name">fp.counterfeit.list</field>
|
||||
<field name="model">fusion.plating.counterfeit.prevention</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Counterfeit Parts Log">
|
||||
<field name="name"/>
|
||||
<field name="incident_date"/>
|
||||
<field name="supplier_id"/>
|
||||
<field name="part_number"/>
|
||||
<field name="lot_serial"/>
|
||||
<field name="disposition" widget="badge"
|
||||
decoration-warning="disposition == 'investigation'"
|
||||
decoration-danger="disposition == 'destroyed'"
|
||||
decoration-info="disposition == 'returned'"/>
|
||||
<field name="gidep_reported" widget="boolean"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_counterfeit_form" model="ir.ui.view">
|
||||
<field name="name">fp.counterfeit.form</field>
|
||||
<field name="model">fusion.plating.counterfeit.prevention</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Counterfeit Prevention Log">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" readonly="1"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="incident_date"/>
|
||||
<field name="supplier_id"/>
|
||||
<field name="part_number"/>
|
||||
<field name="lot_serial"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="detection_method"/>
|
||||
<field name="disposition"/>
|
||||
<field name="gidep_reported"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Notes">
|
||||
<field name="notes"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_counterfeit_search" model="ir.ui.view">
|
||||
<field name="name">fp.counterfeit.search</field>
|
||||
<field name="model">fusion.plating.counterfeit.prevention</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Counterfeit Incidents">
|
||||
<field name="name"/>
|
||||
<field name="supplier_id"/>
|
||||
<field name="part_number"/>
|
||||
<separator/>
|
||||
<filter string="Under Investigation" name="investigation" domain="[('disposition','=','investigation')]"/>
|
||||
<filter string="Returned" name="returned" domain="[('disposition','=','returned')]"/>
|
||||
<filter string="Destroyed" name="destroyed" domain="[('disposition','=','destroyed')]"/>
|
||||
<separator/>
|
||||
<filter string="GIDEP Reported" name="gidep" domain="[('gidep_reported','=',True)]"/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
<group>
|
||||
<filter string="Supplier" name="group_supplier" context="{'group_by':'supplier_id'}"/>
|
||||
<filter string="Disposition" name="group_disposition" context="{'group_by':'disposition'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fp_counterfeit" model="ir.actions.act_window">
|
||||
<field name="name">Counterfeit Parts Log</field>
|
||||
<field name="res_model">fusion.plating.counterfeit.prevention</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_fp_counterfeit_search"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,70 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!--
|
||||
Extend the quality customer-spec form with the aerospace flag
|
||||
block. Sits below the existing "Applicable Processes" group so
|
||||
aerospace-only users can flip the flag + link clauses without
|
||||
touching the base metadata.
|
||||
-->
|
||||
<record id="view_fp_customer_spec_form_inherit_aerospace" model="ir.ui.view">
|
||||
<field name="name">fp.customer.spec.form.inherit.aerospace</field>
|
||||
<field name="model">fusion.plating.customer.spec</field>
|
||||
<field name="inherit_id" ref="fusion_plating_quality.view_fp_customer_spec_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='applicable_processes']" position="after">
|
||||
<group string="Aerospace" name="aerospace">
|
||||
<group>
|
||||
<field name="x_fc_is_aerospace"/>
|
||||
<field name="x_fc_nadcap_required"/>
|
||||
<field name="x_fc_requires_first_article"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="x_fc_pri_file_code"/>
|
||||
<field name="x_fc_customer_approval_required"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="AS9100 Clauses" name="as9100_clauses">
|
||||
<field name="x_fc_as9100_clause_ids" widget="many2many_tags" nolabel="1"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Extend the list with a tiny aerospace indicator column. -->
|
||||
<record id="view_fp_customer_spec_list_inherit_aerospace" model="ir.ui.view">
|
||||
<field name="name">fp.customer.spec.list.inherit.aerospace</field>
|
||||
<field name="model">fusion.plating.customer.spec</field>
|
||||
<field name="inherit_id" ref="fusion_plating_quality.view_fp_customer_spec_list"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='spec_type']" position="after">
|
||||
<field name="x_fc_is_aerospace" string="Aero" optional="show"/>
|
||||
<field name="x_fc_nadcap_required" string="Nadcap" optional="hide"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Add aerospace filters to the spec search view. -->
|
||||
<record id="view_fp_customer_spec_search_inherit_aerospace" model="ir.ui.view">
|
||||
<field name="name">fp.customer.spec.search.inherit.aerospace</field>
|
||||
<field name="model">fusion.plating.customer.spec</field>
|
||||
<field name="inherit_id" ref="fusion_plating_quality.view_fp_customer_spec_search"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//filter[@name='internal']" position="after">
|
||||
<separator/>
|
||||
<filter string="Aerospace" name="aerospace"
|
||||
domain="[('x_fc_is_aerospace','=',True)]"/>
|
||||
<filter string="Nadcap Required" name="nadcap"
|
||||
domain="[('x_fc_nadcap_required','=',True)]"/>
|
||||
<filter string="FAI Required" name="fai_required"
|
||||
domain="[('x_fc_requires_first_article','=',True)]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,62 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!--
|
||||
Extend the quality FAIR form with the AS9102 form attachments
|
||||
and customer-approval signature block. Added as a notebook page
|
||||
so the base layout is untouched.
|
||||
-->
|
||||
<record id="view_fp_fair_form_inherit_aerospace" model="ir.ui.view">
|
||||
<field name="name">fp.fair.form.inherit.aerospace</field>
|
||||
<field name="model">fusion.plating.fair</field>
|
||||
<field name="inherit_id" ref="fusion_plating_quality.view_fp_fair_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="AS9102 Forms" name="as9102_forms">
|
||||
<group>
|
||||
<group string="Form 1 — Part Accountability">
|
||||
<field name="x_fc_as9102_form1"
|
||||
filename="x_fc_as9102_form1_filename"/>
|
||||
<field name="x_fc_as9102_form1_filename" invisible="1"/>
|
||||
</group>
|
||||
<group string="Form 2 — Product Accountability">
|
||||
<field name="x_fc_as9102_form2"
|
||||
filename="x_fc_as9102_form2_filename"/>
|
||||
<field name="x_fc_as9102_form2_filename" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group string="Form 3 — Characteristic Accountability">
|
||||
<field name="x_fc_as9102_form3"
|
||||
filename="x_fc_as9102_form3_filename"/>
|
||||
<field name="x_fc_as9102_form3_filename" invisible="1"/>
|
||||
</group>
|
||||
<group string="Drawing & Customer Approval">
|
||||
<field name="x_fc_drawing_revision"/>
|
||||
<field name="x_fc_customer_approval_id"/>
|
||||
<field name="x_fc_customer_approval_date"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Drawing revision column on the list for quick scanning. -->
|
||||
<record id="view_fp_fair_list_inherit_aerospace" model="ir.ui.view">
|
||||
<field name="name">fp.fair.list.inherit.aerospace</field>
|
||||
<field name="model">fusion.plating.fair</field>
|
||||
<field name="inherit_id" ref="fusion_plating_quality.view_fp_fair_list"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='part_revision']" position="after">
|
||||
<field name="x_fc_drawing_revision" string="Dwg Rev" optional="show"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
46
fusion-plating/fusion_plating_aerospace/views/fp_menu.xml
Normal file
46
fusion-plating/fusion_plating_aerospace/views/fp_menu.xml
Normal file
@@ -0,0 +1,46 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- ===== AEROSPACE (parent submenu under the Plating app) ===== -->
|
||||
<menuitem id="menu_fp_aerospace"
|
||||
name="Aerospace"
|
||||
parent="fusion_plating.menu_fp_root"
|
||||
sequence="60"
|
||||
groups="fusion_plating.group_fusion_plating_operator"/>
|
||||
|
||||
<menuitem id="menu_fp_aerospace_as9100"
|
||||
name="AS9100 Clauses"
|
||||
parent="menu_fp_aerospace"
|
||||
action="action_fp_as9100_clause"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_fp_aerospace_nadcap"
|
||||
name="Nadcap Audits"
|
||||
parent="menu_fp_aerospace"
|
||||
action="action_fp_nadcap_audit"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem id="menu_fp_aerospace_counterfeit"
|
||||
name="Counterfeit Log"
|
||||
parent="menu_fp_aerospace"
|
||||
action="action_fp_counterfeit"
|
||||
sequence="30"/>
|
||||
|
||||
<menuitem id="menu_fp_aerospace_config_items"
|
||||
name="Configuration Items"
|
||||
parent="menu_fp_aerospace"
|
||||
action="action_fp_config_item"
|
||||
sequence="40"/>
|
||||
|
||||
<menuitem id="menu_fp_aerospace_risk"
|
||||
name="Risk Register"
|
||||
parent="menu_fp_aerospace"
|
||||
action="action_fp_risk"
|
||||
sequence="50"/>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,125 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_nadcap_audit_list" model="ir.ui.view">
|
||||
<field name="name">fp.nadcap.audit.list</field>
|
||||
<field name="model">fusion.plating.nadcap.audit</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Nadcap Audits"
|
||||
decoration-success="result == 'accredited'"
|
||||
decoration-warning="result == 'conditional'"
|
||||
decoration-danger="result == 'failed'">
|
||||
<field name="name"/>
|
||||
<field name="audit_date"/>
|
||||
<field name="checklist"/>
|
||||
<field name="auditor_name"/>
|
||||
<field name="pri_auditor" widget="boolean"/>
|
||||
<field name="merit_count"/>
|
||||
<field name="ncr_count"/>
|
||||
<field name="accreditation_end"/>
|
||||
<field name="result" widget="badge"
|
||||
decoration-success="result == 'accredited'"
|
||||
decoration-warning="result == 'conditional'"
|
||||
decoration-danger="result == 'failed'"/>
|
||||
<field name="state" widget="badge"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_nadcap_audit_form" model="ir.ui.view">
|
||||
<field name="name">fp.nadcap.audit.form</field>
|
||||
<field name="model">fusion.plating.nadcap.audit</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Nadcap Audit">
|
||||
<header>
|
||||
<button name="action_start" string="Start Audit" type="object"
|
||||
class="oe_highlight" invisible="state != 'scheduled'"/>
|
||||
<button name="action_issue_report" string="Issue Report" type="object"
|
||||
class="oe_highlight" invisible="state != 'in_progress'"/>
|
||||
<button name="action_close" string="Close" type="object"
|
||||
invisible="state != 'report_issued'"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="scheduled,in_progress,report_issued,closed"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" readonly="1"/></h1>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mb-2" invisible="not checklist">
|
||||
<span class="o_fp_nadcap_badge">
|
||||
<field name="checklist"/>
|
||||
</span>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="audit_date"/>
|
||||
<field name="auditor_name"/>
|
||||
<field name="pri_auditor"/>
|
||||
<field name="checklist"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="result"/>
|
||||
<field name="merit_count"/>
|
||||
<field name="ncr_count"/>
|
||||
<field name="audit_report_attachment"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Accreditation">
|
||||
<group>
|
||||
<field name="accreditation_start"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="accreditation_end"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Notes">
|
||||
<field name="notes"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_nadcap_audit_search" model="ir.ui.view">
|
||||
<field name="name">fp.nadcap.audit.search</field>
|
||||
<field name="model">fusion.plating.nadcap.audit</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Nadcap Audits">
|
||||
<field name="name"/>
|
||||
<field name="auditor_name"/>
|
||||
<separator/>
|
||||
<filter string="Scheduled" name="scheduled" domain="[('state','=','scheduled')]"/>
|
||||
<filter string="In Progress" name="in_progress" domain="[('state','=','in_progress')]"/>
|
||||
<filter string="Closed" name="closed" domain="[('state','=','closed')]"/>
|
||||
<separator/>
|
||||
<filter string="Accredited" name="accredited" domain="[('result','=','accredited')]"/>
|
||||
<filter string="Failed" name="failed" domain="[('result','=','failed')]"/>
|
||||
<separator/>
|
||||
<filter string="PRI Auditor" name="pri_auditor" domain="[('pri_auditor','=',True)]"/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
<group>
|
||||
<filter string="Status" name="group_state" context="{'group_by':'state'}"/>
|
||||
<filter string="Checklist" name="group_checklist" context="{'group_by':'checklist'}"/>
|
||||
<filter string="Result" name="group_result" context="{'group_by':'result'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fp_nadcap_audit" model="ir.actions.act_window">
|
||||
<field name="name">Nadcap Audits</field>
|
||||
<field name="res_model">fusion.plating.nadcap.audit</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_fp_nadcap_audit_search"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
127
fusion-plating/fusion_plating_aerospace/views/fp_risk_views.xml
Normal file
127
fusion-plating/fusion_plating_aerospace/views/fp_risk_views.xml
Normal file
@@ -0,0 +1,127 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_risk_list" model="ir.ui.view">
|
||||
<field name="name">fp.risk.list</field>
|
||||
<field name="model">fusion.plating.risk</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Risk Register"
|
||||
decoration-success="risk_level == 'low'"
|
||||
decoration-info="risk_level == 'medium'"
|
||||
decoration-warning="risk_level == 'high'"
|
||||
decoration-danger="risk_level == 'critical'">
|
||||
<field name="name"/>
|
||||
<field name="title"/>
|
||||
<field name="category"/>
|
||||
<field name="likelihood"/>
|
||||
<field name="impact"/>
|
||||
<field name="risk_score"/>
|
||||
<field name="risk_level" widget="badge"
|
||||
decoration-success="risk_level == 'low'"
|
||||
decoration-info="risk_level == 'medium'"
|
||||
decoration-warning="risk_level == 'high'"
|
||||
decoration-danger="risk_level == 'critical'"/>
|
||||
<field name="owner_id"/>
|
||||
<field name="review_date"/>
|
||||
<field name="state" widget="badge"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_risk_form" model="ir.ui.view">
|
||||
<field name="name">fp.risk.form</field>
|
||||
<field name="model">fusion.plating.risk</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Risk">
|
||||
<header>
|
||||
<button name="action_assess" string="Assess" type="object"
|
||||
class="oe_highlight" invisible="state != 'identified'"/>
|
||||
<button name="action_treat" string="Treat" type="object"
|
||||
class="oe_highlight" invisible="state != 'assessed'"/>
|
||||
<button name="action_monitor" string="Monitor" type="object"
|
||||
invisible="state != 'treated'"/>
|
||||
<button name="action_close" string="Close" type="object"
|
||||
invisible="state not in ('monitored','treated')"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="identified,assessed,treated,monitored,closed"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="title"/>
|
||||
<h1><field name="title" placeholder="Short risk title..."/></h1>
|
||||
<div class="text-muted"><field name="name" readonly="1"/></div>
|
||||
</div>
|
||||
<div class="o_fp_risk_card">
|
||||
<group>
|
||||
<group>
|
||||
<field name="category"/>
|
||||
<field name="likelihood"/>
|
||||
<field name="impact"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="risk_score"/>
|
||||
<field name="risk_level" widget="badge"
|
||||
decoration-success="risk_level == 'low'"
|
||||
decoration-info="risk_level == 'medium'"
|
||||
decoration-warning="risk_level == 'high'"
|
||||
decoration-danger="risk_level == 'critical'"/>
|
||||
<field name="owner_id"/>
|
||||
<field name="review_date"/>
|
||||
</group>
|
||||
</group>
|
||||
</div>
|
||||
<notebook>
|
||||
<page string="Description">
|
||||
<field name="description"/>
|
||||
</page>
|
||||
<page string="Mitigation Plan">
|
||||
<field name="mitigation_plan"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_risk_search" model="ir.ui.view">
|
||||
<field name="name">fp.risk.search</field>
|
||||
<field name="model">fusion.plating.risk</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Risks">
|
||||
<field name="name"/>
|
||||
<field name="title"/>
|
||||
<field name="owner_id"/>
|
||||
<separator/>
|
||||
<filter string="Critical" name="critical" domain="[('risk_level','=','critical')]"/>
|
||||
<filter string="High" name="high" domain="[('risk_level','=','high')]"/>
|
||||
<filter string="Medium" name="medium" domain="[('risk_level','=','medium')]"/>
|
||||
<filter string="Low" name="low" domain="[('risk_level','=','low')]"/>
|
||||
<separator/>
|
||||
<filter string="Open" name="open" domain="[('state','not in',['closed'])]"/>
|
||||
<filter string="Closed" name="closed" domain="[('state','=','closed')]"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
<group>
|
||||
<filter string="Level" name="group_level" context="{'group_by':'risk_level'}"/>
|
||||
<filter string="Category" name="group_category" context="{'group_by':'category'}"/>
|
||||
<filter string="Owner" name="group_owner" context="{'group_by':'owner_id'}"/>
|
||||
<filter string="Status" name="group_state" context="{'group_by':'state'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fp_risk" model="ir.actions.act_window">
|
||||
<field name="name">Risk Register</field>
|
||||
<field name="res_model">fusion.plating.risk</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_fp_risk_search"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
BIN
fusion-plating/fusion_plating_batch/.DS_Store
vendored
Normal file
BIN
fusion-plating/fusion_plating_batch/.DS_Store
vendored
Normal file
Binary file not shown.
6
fusion-plating/fusion_plating_batch/__init__.py
Normal file
6
fusion-plating/fusion_plating_batch/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from . import models
|
||||
30
fusion-plating/fusion_plating_batch/__manifest__.py
Normal file
30
fusion-plating/fusion_plating_batch/__manifest__.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Batch Processing',
|
||||
'version': '19.0.1.0.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Group parts into rack or barrel loads for tank processing.',
|
||||
'author': 'Nexa Systems Inc.',
|
||||
'website': 'https://www.nexasystems.ca',
|
||||
'maintainer': 'Nexa Systems Inc.',
|
||||
'support': 'support@nexasystems.ca',
|
||||
'license': 'OPL-1',
|
||||
'depends': [
|
||||
'fusion_plating',
|
||||
],
|
||||
'data': [
|
||||
'security/fp_batch_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/fp_sequence_data.xml',
|
||||
'views/fp_batch_chemistry_views.xml',
|
||||
'views/fp_batch_views.xml',
|
||||
'views/fp_menu.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': False,
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,17 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="seq_fp_batch" model="ir.sequence">
|
||||
<field name="name">Fusion Plating: Batch</field>
|
||||
<field name="code">fusion.plating.batch</field>
|
||||
<field name="prefix">BATCH-</field>
|
||||
<field name="padding">4</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
7
fusion-plating/fusion_plating_batch/models/__init__.py
Normal file
7
fusion-plating/fusion_plating_batch/models/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from . import fp_batch
|
||||
from . import fp_batch_chemistry
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
141
fusion-plating/fusion_plating_batch/models/fp_batch.py
Normal file
141
fusion-plating/fusion_plating_batch/models/fp_batch.py
Normal file
@@ -0,0 +1,141 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class FpBatch(models.Model):
|
||||
"""A rack or barrel load of parts being processed through a tank.
|
||||
|
||||
Lifecycle:
|
||||
|
||||
draft → loading → in_process → unloading → complete
|
||||
↗
|
||||
(any non-complete state) → cancelled
|
||||
"""
|
||||
_name = 'fusion.plating.batch'
|
||||
_description = 'Plating Batch (Rack/Barrel Load)'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'create_date desc'
|
||||
|
||||
name = fields.Char(
|
||||
string='Batch Reference',
|
||||
required=True,
|
||||
copy=False,
|
||||
readonly=True,
|
||||
default=lambda self: self.env['ir.sequence'].next_by_code(
|
||||
'fusion.plating.batch') or '/',
|
||||
tracking=True,
|
||||
)
|
||||
facility_id = fields.Many2one(
|
||||
'fusion.plating.facility',
|
||||
string='Facility',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
bath_id = fields.Many2one(
|
||||
'fusion.plating.bath',
|
||||
string='Bath',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
tank_id = fields.Many2one(
|
||||
'fusion.plating.tank',
|
||||
string='Tank',
|
||||
related='bath_id.tank_id',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
process_type_id = fields.Many2one(
|
||||
related='bath_id.process_type_id',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
state = fields.Selection(
|
||||
selection=[
|
||||
('draft', 'Draft'),
|
||||
('loading', 'Loading'),
|
||||
('in_process', 'In Process'),
|
||||
('unloading', 'Unloading'),
|
||||
('complete', 'Complete'),
|
||||
('cancelled', 'Cancelled'),
|
||||
],
|
||||
string='Status',
|
||||
default='draft',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
rack_ref = fields.Char(string='Rack / Barrel Ref')
|
||||
part_count = fields.Integer(string='Part Count')
|
||||
start_time = fields.Datetime(string='Process Start', tracking=True)
|
||||
end_time = fields.Datetime(string='Process End', tracking=True)
|
||||
duration_minutes = fields.Float(
|
||||
string='Duration (min)',
|
||||
compute='_compute_duration',
|
||||
store=True,
|
||||
)
|
||||
chemistry_ids = fields.One2many(
|
||||
'fusion.plating.batch.chemistry',
|
||||
'batch_id',
|
||||
string='Chemistry Readings',
|
||||
)
|
||||
chemistry_count = fields.Integer(
|
||||
string='Readings',
|
||||
compute='_compute_chemistry_count',
|
||||
)
|
||||
operator_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Operator',
|
||||
default=lambda self: self.env.user,
|
||||
tracking=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
notes = fields.Html(string='Notes')
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Compute
|
||||
# -------------------------------------------------------------------------
|
||||
@api.depends('start_time', 'end_time')
|
||||
def _compute_duration(self):
|
||||
for rec in self:
|
||||
if rec.start_time and rec.end_time:
|
||||
delta = rec.end_time - rec.start_time
|
||||
rec.duration_minutes = delta.total_seconds() / 60.0
|
||||
else:
|
||||
rec.duration_minutes = 0.0
|
||||
|
||||
@api.depends('chemistry_ids')
|
||||
def _compute_chemistry_count(self):
|
||||
for rec in self:
|
||||
rec.chemistry_count = len(rec.chemistry_ids)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Actions
|
||||
# -------------------------------------------------------------------------
|
||||
def action_start_loading(self):
|
||||
self.write({'state': 'loading'})
|
||||
|
||||
def action_start_process(self):
|
||||
self.write({
|
||||
'state': 'in_process',
|
||||
'start_time': fields.Datetime.now(),
|
||||
})
|
||||
|
||||
def action_start_unloading(self):
|
||||
self.write({
|
||||
'state': 'unloading',
|
||||
'end_time': fields.Datetime.now(),
|
||||
})
|
||||
|
||||
def action_complete(self):
|
||||
self.write({'state': 'complete'})
|
||||
|
||||
def action_cancel(self):
|
||||
self.write({'state': 'cancelled'})
|
||||
@@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class FpBatchChemistry(models.Model):
|
||||
"""A single chemistry reading taken during batch processing."""
|
||||
_name = 'fusion.plating.batch.chemistry'
|
||||
_description = 'Batch Chemistry Reading'
|
||||
_order = 'reading_time desc, id desc'
|
||||
|
||||
batch_id = fields.Many2one(
|
||||
'fusion.plating.batch',
|
||||
string='Batch',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
)
|
||||
parameter_id = fields.Many2one(
|
||||
'fusion.plating.bath.parameter',
|
||||
string='Parameter',
|
||||
required=True,
|
||||
)
|
||||
value = fields.Float(string='Value', required=True)
|
||||
reading_time = fields.Datetime(
|
||||
string='Reading Time',
|
||||
default=fields.Datetime.now,
|
||||
)
|
||||
status = fields.Selection(
|
||||
selection=[
|
||||
('pass', 'Pass'),
|
||||
('warning', 'Warning'),
|
||||
('fail', 'Fail'),
|
||||
],
|
||||
string='Status',
|
||||
compute='_compute_status',
|
||||
store=True,
|
||||
)
|
||||
notes = fields.Char(string='Notes')
|
||||
|
||||
@api.depends('parameter_id', 'value')
|
||||
def _compute_status(self):
|
||||
"""Compare value against parameter target range.
|
||||
|
||||
Uses the parameter's default target range and warning tolerance.
|
||||
A reading within [target_min, target_max] is a pass. If it falls
|
||||
within the warning tolerance band outside that range, it is a
|
||||
warning. Otherwise it is a fail.
|
||||
"""
|
||||
for rec in self:
|
||||
if not rec.parameter_id:
|
||||
rec.status = 'pass'
|
||||
continue
|
||||
param = rec.parameter_id
|
||||
target_min = param.target_min
|
||||
target_max = param.target_max
|
||||
if not target_min and not target_max:
|
||||
rec.status = 'pass'
|
||||
continue
|
||||
# Value within target range = pass
|
||||
if target_min <= rec.value <= target_max:
|
||||
rec.status = 'pass'
|
||||
continue
|
||||
# Calculate warning band from tolerance %
|
||||
tolerance = (param.warning_tolerance or 0.0) / 100.0
|
||||
span = target_max - target_min if target_max != target_min else abs(target_max) or 1.0
|
||||
margin = span * tolerance
|
||||
warning_min = target_min - margin
|
||||
warning_max = target_max + margin
|
||||
if warning_min <= rec.value <= warning_max:
|
||||
rec.status = 'warning'
|
||||
else:
|
||||
rec.status = 'fail'
|
||||
@@ -0,0 +1,19 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- RECORD RULE — Multi-company isolation on batches -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="fp_batch_company_rule" model="ir.rule">
|
||||
<field name="name">Fusion Plating: Batch — multi-company</field>
|
||||
<field name="model_id" ref="model_fusion_plating_batch"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,7 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_fp_batch_operator,fp.batch.operator,model_fusion_plating_batch,fusion_plating.group_fusion_plating_operator,1,1,1,0
|
||||
access_fp_batch_supervisor,fp.batch.supervisor,model_fusion_plating_batch,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
|
||||
access_fp_batch_manager,fp.batch.manager,model_fusion_plating_batch,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_batch_chemistry_operator,fp.batch.chemistry.operator,model_fusion_plating_batch_chemistry,fusion_plating.group_fusion_plating_operator,1,1,1,0
|
||||
access_fp_batch_chemistry_supervisor,fp.batch.chemistry.supervisor,model_fusion_plating_batch_chemistry,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
|
||||
access_fp_batch_chemistry_manager,fp.batch.chemistry.manager,model_fusion_plating_batch_chemistry,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
|
@@ -0,0 +1,87 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- ===== LIST ===== -->
|
||||
<record id="view_fp_batch_chemistry_list" model="ir.ui.view">
|
||||
<field name="name">fp.batch.chemistry.list</field>
|
||||
<field name="model">fusion.plating.batch.chemistry</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Chemistry Readings"
|
||||
decoration-success="status == 'pass'"
|
||||
decoration-warning="status == 'warning'"
|
||||
decoration-danger="status == 'fail'">
|
||||
<field name="batch_id"/>
|
||||
<field name="parameter_id"/>
|
||||
<field name="value"/>
|
||||
<field name="reading_time"/>
|
||||
<field name="status" widget="badge"
|
||||
decoration-success="status == 'pass'"
|
||||
decoration-warning="status == 'warning'"
|
||||
decoration-danger="status == 'fail'"/>
|
||||
<field name="notes"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== FORM ===== -->
|
||||
<record id="view_fp_batch_chemistry_form" model="ir.ui.view">
|
||||
<field name="name">fp.batch.chemistry.form</field>
|
||||
<field name="model">fusion.plating.batch.chemistry</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Chemistry Reading">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="batch_id"/>
|
||||
<field name="parameter_id"/>
|
||||
<field name="value"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="reading_time"/>
|
||||
<field name="status" widget="badge"
|
||||
decoration-success="status == 'pass'"
|
||||
decoration-warning="status == 'warning'"
|
||||
decoration-danger="status == 'fail'"/>
|
||||
<field name="notes"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== SEARCH ===== -->
|
||||
<record id="view_fp_batch_chemistry_search" model="ir.ui.view">
|
||||
<field name="name">fp.batch.chemistry.search</field>
|
||||
<field name="model">fusion.plating.batch.chemistry</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Chemistry Readings">
|
||||
<field name="batch_id"/>
|
||||
<field name="parameter_id"/>
|
||||
<separator/>
|
||||
<filter string="Pass" name="pass" domain="[('status','=','pass')]"/>
|
||||
<filter string="Warning" name="warning" domain="[('status','=','warning')]"/>
|
||||
<filter string="Fail" name="fail" domain="[('status','=','fail')]"/>
|
||||
<group>
|
||||
<filter string="Batch" name="group_batch" context="{'group_by':'batch_id'}"/>
|
||||
<filter string="Parameter" name="group_parameter" context="{'group_by':'parameter_id'}"/>
|
||||
<filter string="Status" name="group_status" context="{'group_by':'status'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== ACTION ===== -->
|
||||
<record id="action_fp_batch_chemistry" model="ir.actions.act_window">
|
||||
<field name="name">Chemistry Readings</field>
|
||||
<field name="res_model">fusion.plating.batch.chemistry</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_fp_batch_chemistry_search"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
202
fusion-plating/fusion_plating_batch/views/fp_batch_views.xml
Normal file
202
fusion-plating/fusion_plating_batch/views/fp_batch_views.xml
Normal file
@@ -0,0 +1,202 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- ===== LIST ===== -->
|
||||
<record id="view_fp_batch_list" model="ir.ui.view">
|
||||
<field name="name">fp.batch.list</field>
|
||||
<field name="model">fusion.plating.batch</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Batches"
|
||||
decoration-muted="state == 'cancelled'"
|
||||
decoration-success="state == 'complete'">
|
||||
<field name="name"/>
|
||||
<field name="facility_id"/>
|
||||
<field name="bath_id"/>
|
||||
<field name="tank_id" optional="show"/>
|
||||
<field name="rack_ref" optional="show"/>
|
||||
<field name="part_count"/>
|
||||
<field name="operator_id" widget="many2one_avatar_user"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-info="state == 'draft'"
|
||||
decoration-warning="state == 'loading'"
|
||||
decoration-primary="state == 'in_process'"
|
||||
decoration-success="state in ('unloading','complete')"
|
||||
decoration-danger="state == 'cancelled'"/>
|
||||
<field name="duration_minutes" optional="show" widget="float_time"/>
|
||||
<field name="start_time" optional="hide"/>
|
||||
<field name="end_time" optional="hide"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== FORM ===== -->
|
||||
<record id="view_fp_batch_form" model="ir.ui.view">
|
||||
<field name="name">fp.batch.form</field>
|
||||
<field name="model">fusion.plating.batch</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Batch">
|
||||
<header>
|
||||
<button name="action_start_loading" string="Start Loading"
|
||||
type="object" class="oe_highlight"
|
||||
invisible="state != 'draft'"/>
|
||||
<button name="action_start_process" string="Start Process"
|
||||
type="object" class="oe_highlight"
|
||||
invisible="state != 'loading'"/>
|
||||
<button name="action_start_unloading" string="Unload"
|
||||
type="object" class="oe_highlight"
|
||||
invisible="state != 'in_process'"/>
|
||||
<button name="action_complete" string="Complete"
|
||||
type="object" class="oe_highlight"
|
||||
invisible="state != 'unloading'"/>
|
||||
<button name="action_cancel" string="Cancel"
|
||||
type="object"
|
||||
invisible="state in ('complete','cancelled')"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="draft,loading,in_process,unloading,complete"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="%(fusion_plating_batch.action_fp_batch_chemistry)d"
|
||||
type="action" class="oe_stat_button" icon="fa-flask"
|
||||
context="{'search_default_batch_id': id}">
|
||||
<field name="chemistry_count" widget="statinfo" string="Readings"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" readonly="1"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="facility_id"/>
|
||||
<field name="bath_id" domain="[('facility_id','=',facility_id)]"/>
|
||||
<field name="tank_id"/>
|
||||
<field name="process_type_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="rack_ref"/>
|
||||
<field name="part_count"/>
|
||||
<field name="operator_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="start_time"/>
|
||||
<field name="end_time"/>
|
||||
<field name="duration_minutes" widget="float_time"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Chemistry Readings">
|
||||
<field name="chemistry_ids">
|
||||
<list editable="bottom">
|
||||
<field name="parameter_id"/>
|
||||
<field name="value"/>
|
||||
<field name="reading_time"/>
|
||||
<field name="status" widget="badge"
|
||||
decoration-success="status == 'pass'"
|
||||
decoration-warning="status == 'warning'"
|
||||
decoration-danger="status == 'fail'"/>
|
||||
<field name="notes"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Notes">
|
||||
<field name="notes"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== KANBAN ===== -->
|
||||
<record id="view_fp_batch_kanban" model="ir.ui.view">
|
||||
<field name="name">fp.batch.kanban</field>
|
||||
<field name="model">fusion.plating.batch</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban default_group_by="state" class="o_fp_batch_kanban">
|
||||
<field name="id"/>
|
||||
<field name="name"/>
|
||||
<field name="facility_id"/>
|
||||
<field name="bath_id"/>
|
||||
<field name="rack_ref"/>
|
||||
<field name="part_count"/>
|
||||
<field name="state"/>
|
||||
<field name="operator_id"/>
|
||||
<field name="duration_minutes"/>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<div class="o_fp_card o_fp_batch_card">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<strong class="o_fp_card_title"><field name="name"/></strong>
|
||||
</div>
|
||||
<div class="small text-muted">
|
||||
<field name="bath_id"/>
|
||||
</div>
|
||||
<div class="small">
|
||||
<i class="fa fa-cubes me-1 text-muted"/>
|
||||
<field name="rack_ref"/>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mt-2 small">
|
||||
<span class="text-muted">Parts</span>
|
||||
<span class="fw-bold"><field name="part_count"/></span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between small">
|
||||
<span class="text-muted">Operator</span>
|
||||
<field name="operator_id" widget="many2one_avatar_user"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== SEARCH ===== -->
|
||||
<record id="view_fp_batch_search" model="ir.ui.view">
|
||||
<field name="name">fp.batch.search</field>
|
||||
<field name="model">fusion.plating.batch</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Batches">
|
||||
<field name="name"/>
|
||||
<field name="bath_id"/>
|
||||
<field name="facility_id"/>
|
||||
<field name="rack_ref"/>
|
||||
<field name="operator_id"/>
|
||||
<separator/>
|
||||
<filter string="Draft" name="draft" domain="[('state','=','draft')]"/>
|
||||
<filter string="Loading" name="loading" domain="[('state','=','loading')]"/>
|
||||
<filter string="In Process" name="in_process" domain="[('state','=','in_process')]"/>
|
||||
<filter string="Unloading" name="unloading" domain="[('state','=','unloading')]"/>
|
||||
<filter string="Complete" name="complete" domain="[('state','=','complete')]"/>
|
||||
<filter string="Cancelled" name="cancelled" domain="[('state','=','cancelled')]"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
<group>
|
||||
<filter string="Facility" name="group_facility" context="{'group_by':'facility_id'}"/>
|
||||
<filter string="Bath" name="group_bath" context="{'group_by':'bath_id'}"/>
|
||||
<filter string="Status" name="group_state" context="{'group_by':'state'}"/>
|
||||
<filter string="Operator" name="group_operator" context="{'group_by':'operator_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== ACTION ===== -->
|
||||
<record id="action_fp_batch" model="ir.actions.act_window">
|
||||
<field name="name">Batches</field>
|
||||
<field name="res_model">fusion.plating.batch</field>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
<field name="search_view_id" ref="view_fp_batch_search"/>
|
||||
<field name="context">{'search_default_draft': 1, 'search_default_loading': 1, 'search_default_in_process': 1, 'search_default_unloading': 1}</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
16
fusion-plating/fusion_plating_batch/views/fp_menu.xml
Normal file
16
fusion-plating/fusion_plating_batch/views/fp_menu.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- Batches menu item under Plating > Operations -->
|
||||
<menuitem id="menu_fp_batches"
|
||||
name="Batches"
|
||||
parent="fusion_plating.menu_fp_operations"
|
||||
action="action_fp_batch"
|
||||
sequence="15"/>
|
||||
|
||||
</odoo>
|
||||
64
fusion-plating/fusion_plating_bridge_documents/README.md
Normal file
64
fusion-plating/fusion_plating_bridge_documents/README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Fusion Plating — Documents Bridge (Enterprise)
|
||||
|
||||
Part of the Fusion Plating product family by Nexa Systems Inc.
|
||||
|
||||
## What this module does
|
||||
|
||||
When both `fusion_plating_quality` and Odoo Enterprise's `documents` module are
|
||||
installed, this bridge **auto-installs** and wires the two together so that
|
||||
every attachment dropped on a Fusion Plating quality record (NCR, CAPA, FAIR,
|
||||
or Controlled Document) is automatically mirrored into a dedicated Documents
|
||||
workspace with the right tag applied.
|
||||
|
||||
No manual file uploads, no duplicate copies to keep in sync — the bridge takes
|
||||
care of it on `ir.attachment.create()`.
|
||||
|
||||
## What it creates
|
||||
|
||||
* **Workspace**: `Plating — Quality` (a `documents.document` with `type='folder'`)
|
||||
* **Facet**: `Record Type` (a `documents.facet` scoped to the workspace)
|
||||
* **Tags**: `NCR`, `CAPA`, `FAIR`, `Doc Control` under that facet
|
||||
* **Smart button**: a `Documents` stat button on every NCR / CAPA / FAIR /
|
||||
Controlled Document form view, opening the filtered Documents kanban for
|
||||
that record
|
||||
|
||||
## How it works
|
||||
|
||||
1. A user attaches a file to an NCR (or any of the other supported records)
|
||||
via the chatter or a wizard.
|
||||
2. The bridge's `ir.attachment.create()` override inspects `res_model` and,
|
||||
if it matches one of the supported quality models, creates a mirror
|
||||
`documents.document` record:
|
||||
* Placed inside the `Plating — Quality` workspace
|
||||
* Tagged with the corresponding record type tag
|
||||
* Linked back to the original `ir.attachment` via `attachment_id`
|
||||
3. On the quality record form, the smart button reads a computed Many2many
|
||||
(`x_fc_document_ids`) that searches `documents.document` by the underlying
|
||||
attachment's `res_model` + `res_id` — no duplication, no storage overhead.
|
||||
|
||||
## Safety & robustness
|
||||
|
||||
* The bridge never blocks attachment creation. Any exception raised while
|
||||
creating the mirror `documents.document` record is caught and logged — the
|
||||
user's upload always succeeds.
|
||||
* All references to the workspace folder and tags use
|
||||
`env.ref(..., raise_if_not_found=False)`. If the data records are ever
|
||||
removed or renamed, the bridge degrades gracefully (no mirror created,
|
||||
logged warning).
|
||||
* The bridge never modifies `fusion_plating`, `fusion_plating_quality`, or
|
||||
the EE `documents` module. It is purely additive.
|
||||
|
||||
## Dependencies
|
||||
|
||||
* `fusion_plating_quality`
|
||||
* `documents` (Odoo Enterprise)
|
||||
|
||||
## Auto-install
|
||||
|
||||
`auto_install = True` — the bridge installs automatically whenever both
|
||||
dependencies are present in the same database, and stays dormant otherwise.
|
||||
|
||||
## Copyright
|
||||
|
||||
Copyright 2026 Nexa Systems Inc. All rights reserved.
|
||||
Licensed under OPL-1 (Odoo Proprietary License v1.0).
|
||||
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from . import models
|
||||
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Documents Bridge (EE)',
|
||||
'version': '19.0.1.0.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Enterprise bridge: auto-promotes Fusion Plating quality attachments '
|
||||
'(NCR, CAPA, FAIR, Doc Control) into Odoo EE Documents with a tagged '
|
||||
'workspace. Auto-installs when both modules are present.',
|
||||
'description': """
|
||||
Fusion Plating — Documents Bridge (Enterprise)
|
||||
==============================================
|
||||
|
||||
Part of the Fusion Plating product family by Nexa Systems Inc.
|
||||
|
||||
This bridge module connects the native Fusion Plating QMS (`fusion_plating_quality`)
|
||||
with the Odoo Enterprise `documents` module. When both modules are installed the
|
||||
bridge installs automatically and takes care of the plumbing so that every
|
||||
attachment dropped on an NCR, CAPA, FAIR, or Controlled Document record is
|
||||
promoted into a dedicated "Plating — Quality" workspace and tagged by record
|
||||
type for easy retrieval, review, and audit export.
|
||||
|
||||
What it does
|
||||
------------
|
||||
* Creates a dedicated Documents workspace: "Plating — Quality"
|
||||
* Creates a "Record Type" facet with four tags: NCR, CAPA, FAIR, Doc Control
|
||||
* Overrides `ir.attachment.create()` so attachments added to supported quality
|
||||
records are silently mirrored as `documents.document` records in the
|
||||
workspace and tagged with the appropriate record type
|
||||
* Adds a "Documents" smart button on each NCR, CAPA, FAIR, and Doc Control form
|
||||
view that opens the filtered Documents kanban for that record
|
||||
* Ships with `auto_install = True` so no manual install step is required — the
|
||||
bridge activates as soon as both pre-requisite modules are present
|
||||
|
||||
Why this module exists
|
||||
----------------------
|
||||
The Community-Edition-compatible `fusion_plating_quality` module intentionally
|
||||
does NOT depend on the Enterprise `documents` module. On Enterprise deployments
|
||||
this bridge provides the richer Documents-app experience (workspaces, tags,
|
||||
bulk download, preview, sharing) without ever touching the core or quality
|
||||
modules — both stay CE-safe and upgradable.
|
||||
|
||||
Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
""",
|
||||
'author': 'Nexa Systems Inc.',
|
||||
'website': 'https://www.nexasystems.ca',
|
||||
'maintainer': 'Nexa Systems Inc.',
|
||||
'support': 'support@nexasystems.ca',
|
||||
'license': 'OPL-1',
|
||||
'price': 0.00,
|
||||
'currency': 'CAD',
|
||||
'depends': [
|
||||
'fusion_plating_quality',
|
||||
'documents',
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/documents_folder_data.xml',
|
||||
'data/documents_tag_data.xml',
|
||||
'views/fp_ncr_views.xml',
|
||||
'views/fp_capa_views.xml',
|
||||
'views/fp_fair_views.xml',
|
||||
'views/fp_doc_control_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': True,
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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.
|
||||
|
||||
Workspace folder for Fusion Plating QMS attachments.
|
||||
|
||||
Odoo 19 EE unified the Documents data model: folders are simply
|
||||
``documents.document`` records with ``type = 'folder'`` and a null
|
||||
``folder_id`` (root) or a parent folder reference. This matches the
|
||||
Odoo 18.x / 19.x behaviour where the Documents kanban is driven by
|
||||
a single model rather than a separate ``documents.folder`` model.
|
||||
-->
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="documents_folder_plating_quality" model="documents.document">
|
||||
<field name="name">Plating — Quality</field>
|
||||
<field name="type">folder</field>
|
||||
<field name="folder_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,50 @@
|
||||
<?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.
|
||||
|
||||
Tags used by the bridge to categorise mirrored quality attachments.
|
||||
|
||||
Odoo 19 EE organises tags under a two-level hierarchy:
|
||||
documents.facet (a.k.a. "category" — scoped to a folder/workspace)
|
||||
└── documents.tag (individual tags, required to have a facet_id)
|
||||
|
||||
We create one facet called "Record Type" inside the Plating — Quality
|
||||
workspace, then four tags beneath it — one per supported quality
|
||||
record type.
|
||||
-->
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="documents_facet_record_type" model="documents.facet">
|
||||
<field name="name">Record Type</field>
|
||||
<field name="folder_id" ref="documents_folder_plating_quality"/>
|
||||
<field name="sequence">10</field>
|
||||
</record>
|
||||
|
||||
<record id="documents_tag_ncr" model="documents.tag">
|
||||
<field name="name">NCR</field>
|
||||
<field name="facet_id" ref="documents_facet_record_type"/>
|
||||
<field name="sequence">10</field>
|
||||
</record>
|
||||
|
||||
<record id="documents_tag_capa" model="documents.tag">
|
||||
<field name="name">CAPA</field>
|
||||
<field name="facet_id" ref="documents_facet_record_type"/>
|
||||
<field name="sequence">20</field>
|
||||
</record>
|
||||
|
||||
<record id="documents_tag_fair" model="documents.tag">
|
||||
<field name="name">FAIR</field>
|
||||
<field name="facet_id" ref="documents_facet_record_type"/>
|
||||
<field name="sequence">30</field>
|
||||
</record>
|
||||
|
||||
<record id="documents_tag_doc_control" model="documents.tag">
|
||||
<field name="name">Doc Control</field>
|
||||
<field name="facet_id" ref="documents_facet_record_type"/>
|
||||
<field name="sequence">40</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from . import ir_attachment
|
||||
from . import fp_ncr
|
||||
from . import fp_capa
|
||||
from . import fp_fair
|
||||
from . import fp_doc_control
|
||||
@@ -0,0 +1,65 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
_FOLDER_XMLID = 'fusion_plating_bridge_documents.documents_folder_plating_quality'
|
||||
|
||||
|
||||
class FpCapa(models.Model):
|
||||
"""Bridge extension: expose Documents workspace on CAPAs."""
|
||||
_inherit = 'fusion.plating.capa'
|
||||
|
||||
x_fc_document_ids = fields.Many2many(
|
||||
'documents.document',
|
||||
'fp_bridge_capa_document_rel',
|
||||
'capa_id',
|
||||
'document_id',
|
||||
string='Quality Documents',
|
||||
compute='_compute_x_fc_document_ids',
|
||||
store=False,
|
||||
help='Documents in the Plating — Quality workspace mirrored from '
|
||||
'attachments on this CAPA.',
|
||||
)
|
||||
x_fc_document_count = fields.Integer(
|
||||
string='# Documents',
|
||||
compute='_compute_x_fc_document_ids',
|
||||
store=False,
|
||||
)
|
||||
|
||||
@api.depends('message_attachment_count')
|
||||
def _compute_x_fc_document_ids(self):
|
||||
Document = self.env.get('documents.document') if 'documents.document' in self.env else None
|
||||
for rec in self:
|
||||
if not Document:
|
||||
rec.x_fc_document_ids = False
|
||||
rec.x_fc_document_count = 0
|
||||
continue
|
||||
docs = Document.sudo().search([
|
||||
('attachment_id.res_model', '=', 'fusion.plating.capa'),
|
||||
('attachment_id.res_id', '=', rec.id),
|
||||
])
|
||||
rec.x_fc_document_ids = docs
|
||||
rec.x_fc_document_count = len(docs)
|
||||
|
||||
def action_view_documents(self):
|
||||
self.ensure_one()
|
||||
folder_id = self._get_default_folder_id()
|
||||
ctx = {}
|
||||
if folder_id:
|
||||
ctx['default_folder_id'] = folder_id
|
||||
ctx['searchpanel_default_folder_id'] = folder_id
|
||||
return {
|
||||
'name': _('Quality Documents'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'documents.document',
|
||||
'view_mode': 'kanban,list,form',
|
||||
'domain': [('id', 'in', self.x_fc_document_ids.ids)],
|
||||
'context': ctx,
|
||||
}
|
||||
|
||||
def _get_default_folder_id(self):
|
||||
folder = self.env.ref(_FOLDER_XMLID, raise_if_not_found=False)
|
||||
return folder.id if folder else 0
|
||||
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
_FOLDER_XMLID = 'fusion_plating_bridge_documents.documents_folder_plating_quality'
|
||||
|
||||
|
||||
class FpDocControl(models.Model):
|
||||
"""Bridge extension: expose Documents workspace on Controlled Documents.
|
||||
|
||||
Doc Control already carries a native ``attachment_ids`` Many2many; the
|
||||
bridge additionally exposes the `documents.document` mirror so users can
|
||||
jump straight into the Documents app to use its preview, tag, share,
|
||||
and lock features.
|
||||
"""
|
||||
_inherit = 'fusion.plating.doc.control'
|
||||
|
||||
x_fc_document_ids = fields.Many2many(
|
||||
'documents.document',
|
||||
'fp_bridge_doc_control_document_rel',
|
||||
'doc_id',
|
||||
'document_id',
|
||||
string='Quality Documents',
|
||||
compute='_compute_x_fc_document_ids',
|
||||
store=False,
|
||||
help='Documents in the Plating — Quality workspace mirrored from '
|
||||
'attachments on this controlled document record.',
|
||||
)
|
||||
x_fc_document_count = fields.Integer(
|
||||
string='# Documents',
|
||||
compute='_compute_x_fc_document_ids',
|
||||
store=False,
|
||||
)
|
||||
|
||||
@api.depends('attachment_ids', 'message_attachment_count')
|
||||
def _compute_x_fc_document_ids(self):
|
||||
Document = self.env.get('documents.document') if 'documents.document' in self.env else None
|
||||
for rec in self:
|
||||
if not Document:
|
||||
rec.x_fc_document_ids = False
|
||||
rec.x_fc_document_count = 0
|
||||
continue
|
||||
# Pull in both chatter attachments (matched via res_model/res_id) and
|
||||
# any documents whose underlying ir.attachment is in the native
|
||||
# attachment_ids M2m on this controlled document record.
|
||||
native_attachment_ids = rec.attachment_ids.ids
|
||||
if native_attachment_ids:
|
||||
domain = [
|
||||
'|',
|
||||
'&', ('attachment_id.res_model', '=', 'fusion.plating.doc.control'),
|
||||
('attachment_id.res_id', '=', rec.id),
|
||||
('attachment_id', 'in', native_attachment_ids),
|
||||
]
|
||||
else:
|
||||
domain = [
|
||||
('attachment_id.res_model', '=', 'fusion.plating.doc.control'),
|
||||
('attachment_id.res_id', '=', rec.id),
|
||||
]
|
||||
docs = Document.sudo().search(domain)
|
||||
rec.x_fc_document_ids = docs
|
||||
rec.x_fc_document_count = len(docs)
|
||||
|
||||
def action_view_documents(self):
|
||||
self.ensure_one()
|
||||
folder_id = self._get_default_folder_id()
|
||||
ctx = {}
|
||||
if folder_id:
|
||||
ctx['default_folder_id'] = folder_id
|
||||
ctx['searchpanel_default_folder_id'] = folder_id
|
||||
return {
|
||||
'name': _('Quality Documents'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'documents.document',
|
||||
'view_mode': 'kanban,list,form',
|
||||
'domain': [('id', 'in', self.x_fc_document_ids.ids)],
|
||||
'context': ctx,
|
||||
}
|
||||
|
||||
def _get_default_folder_id(self):
|
||||
folder = self.env.ref(_FOLDER_XMLID, raise_if_not_found=False)
|
||||
return folder.id if folder else 0
|
||||
@@ -0,0 +1,65 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
_FOLDER_XMLID = 'fusion_plating_bridge_documents.documents_folder_plating_quality'
|
||||
|
||||
|
||||
class FpFair(models.Model):
|
||||
"""Bridge extension: expose Documents workspace on FAIRs."""
|
||||
_inherit = 'fusion.plating.fair'
|
||||
|
||||
x_fc_document_ids = fields.Many2many(
|
||||
'documents.document',
|
||||
'fp_bridge_fair_document_rel',
|
||||
'fair_id',
|
||||
'document_id',
|
||||
string='Quality Documents',
|
||||
compute='_compute_x_fc_document_ids',
|
||||
store=False,
|
||||
help='Documents in the Plating — Quality workspace mirrored from '
|
||||
'attachments on this FAIR.',
|
||||
)
|
||||
x_fc_document_count = fields.Integer(
|
||||
string='# Documents',
|
||||
compute='_compute_x_fc_document_ids',
|
||||
store=False,
|
||||
)
|
||||
|
||||
@api.depends('message_attachment_count')
|
||||
def _compute_x_fc_document_ids(self):
|
||||
Document = self.env.get('documents.document') if 'documents.document' in self.env else None
|
||||
for rec in self:
|
||||
if not Document:
|
||||
rec.x_fc_document_ids = False
|
||||
rec.x_fc_document_count = 0
|
||||
continue
|
||||
docs = Document.sudo().search([
|
||||
('attachment_id.res_model', '=', 'fusion.plating.fair'),
|
||||
('attachment_id.res_id', '=', rec.id),
|
||||
])
|
||||
rec.x_fc_document_ids = docs
|
||||
rec.x_fc_document_count = len(docs)
|
||||
|
||||
def action_view_documents(self):
|
||||
self.ensure_one()
|
||||
folder_id = self._get_default_folder_id()
|
||||
ctx = {}
|
||||
if folder_id:
|
||||
ctx['default_folder_id'] = folder_id
|
||||
ctx['searchpanel_default_folder_id'] = folder_id
|
||||
return {
|
||||
'name': _('Quality Documents'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'documents.document',
|
||||
'view_mode': 'kanban,list,form',
|
||||
'domain': [('id', 'in', self.x_fc_document_ids.ids)],
|
||||
'context': ctx,
|
||||
}
|
||||
|
||||
def _get_default_folder_id(self):
|
||||
folder = self.env.ref(_FOLDER_XMLID, raise_if_not_found=False)
|
||||
return folder.id if folder else 0
|
||||
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
_FOLDER_XMLID = 'fusion_plating_bridge_documents.documents_folder_plating_quality'
|
||||
|
||||
|
||||
class FpNcr(models.Model):
|
||||
"""Bridge extension: expose Documents workspace on NCRs.
|
||||
|
||||
Adds a reverse link to any `documents.document` records that were
|
||||
created by the bridge override on `ir.attachment`, plus a smart
|
||||
button action that opens the filtered Documents kanban for the
|
||||
current NCR.
|
||||
"""
|
||||
_inherit = 'fusion.plating.ncr'
|
||||
|
||||
x_fc_document_ids = fields.Many2many(
|
||||
'documents.document',
|
||||
'fp_bridge_ncr_document_rel',
|
||||
'ncr_id',
|
||||
'document_id',
|
||||
string='Quality Documents',
|
||||
compute='_compute_x_fc_document_ids',
|
||||
store=False,
|
||||
help='Documents in the Plating — Quality workspace mirrored from '
|
||||
'attachments on this NCR.',
|
||||
)
|
||||
x_fc_document_count = fields.Integer(
|
||||
string='# Documents',
|
||||
compute='_compute_x_fc_document_ids',
|
||||
store=False,
|
||||
)
|
||||
|
||||
@api.depends('message_attachment_count')
|
||||
def _compute_x_fc_document_ids(self):
|
||||
Document = self.env.get('documents.document') if 'documents.document' in self.env else None
|
||||
for rec in self:
|
||||
if not Document:
|
||||
rec.x_fc_document_ids = False
|
||||
rec.x_fc_document_count = 0
|
||||
continue
|
||||
docs = Document.sudo().search([
|
||||
('attachment_id.res_model', '=', 'fusion.plating.ncr'),
|
||||
('attachment_id.res_id', '=', rec.id),
|
||||
])
|
||||
rec.x_fc_document_ids = docs
|
||||
rec.x_fc_document_count = len(docs)
|
||||
|
||||
def action_view_documents(self):
|
||||
self.ensure_one()
|
||||
folder_id = self._get_default_folder_id()
|
||||
ctx = {}
|
||||
if folder_id:
|
||||
ctx['default_folder_id'] = folder_id
|
||||
ctx['searchpanel_default_folder_id'] = folder_id
|
||||
return {
|
||||
'name': _('Quality Documents'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'documents.document',
|
||||
'view_mode': 'kanban,list,form',
|
||||
'domain': [('id', 'in', self.x_fc_document_ids.ids)],
|
||||
'context': ctx,
|
||||
}
|
||||
|
||||
def _get_default_folder_id(self):
|
||||
folder = self.env.ref(_FOLDER_XMLID, raise_if_not_found=False)
|
||||
return folder.id if folder else 0
|
||||
@@ -0,0 +1,122 @@
|
||||
# -*- 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
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Map of quality-module res_model -> bridge tag XML id.
|
||||
# Kept module-level so both create() and write() can consult it cheaply.
|
||||
_QUALITY_MODELS_TO_TAG = {
|
||||
'fusion.plating.ncr': 'fusion_plating_bridge_documents.documents_tag_ncr',
|
||||
'fusion.plating.capa': 'fusion_plating_bridge_documents.documents_tag_capa',
|
||||
'fusion.plating.fair': 'fusion_plating_bridge_documents.documents_tag_fair',
|
||||
'fusion.plating.doc.control': 'fusion_plating_bridge_documents.documents_tag_doc_control',
|
||||
}
|
||||
|
||||
_FOLDER_XMLID = 'fusion_plating_bridge_documents.documents_folder_plating_quality'
|
||||
|
||||
|
||||
class IrAttachment(models.Model):
|
||||
"""Bridge ir.attachment with Odoo EE `documents.document`.
|
||||
|
||||
Whenever an attachment is created on one of the Fusion Plating QMS
|
||||
record types (NCR, CAPA, FAIR, Doc Control) we silently mirror it as
|
||||
a `documents.document` record inside the "Plating — Quality"
|
||||
workspace, tagged with the corresponding record type. The original
|
||||
`ir.attachment` record is untouched and continues to live on the
|
||||
quality record as before — the bridge is purely additive.
|
||||
|
||||
Design notes
|
||||
------------
|
||||
* We resolve the folder and tag XML ids via ``env.ref`` with
|
||||
``raise_if_not_found=False`` so that a partial install, a missing
|
||||
demo record, or a future schema change can never break attachment
|
||||
creation on a quality record — the worst case is that the
|
||||
`documents.document` mirror record isn't created and a line goes
|
||||
to the log.
|
||||
* The write is wrapped in a broad try/except for the same reason:
|
||||
user-facing attachment creation must never be blocked by a bridge
|
||||
failure.
|
||||
* We use ``sudo()`` on the `documents.document` create because the
|
||||
user uploading the attachment may not have write access to the
|
||||
Documents app — the bridge is a system-level convenience.
|
||||
"""
|
||||
_inherit = 'ir.attachment'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
attachments = super().create(vals_list)
|
||||
try:
|
||||
self._fusion_plating_bridge_promote_to_documents(attachments)
|
||||
except Exception: # pragma: no cover - defensive only
|
||||
_logger.exception(
|
||||
"Fusion Plating Documents bridge: failed to promote attachments %s",
|
||||
attachments.ids,
|
||||
)
|
||||
return attachments
|
||||
|
||||
def _fusion_plating_bridge_promote_to_documents(self, attachments):
|
||||
"""Create `documents.document` mirror records for quality attachments.
|
||||
|
||||
Silently skips if:
|
||||
- the documents module isn't in the registry (defensive, the
|
||||
manifest already depends on it but this module may be tested
|
||||
on CE)
|
||||
- the target folder hasn't been created yet
|
||||
- the attachment is not attached to a quality record
|
||||
"""
|
||||
if 'documents.document' not in self.env:
|
||||
return
|
||||
|
||||
folder = self.env.ref(_FOLDER_XMLID, raise_if_not_found=False)
|
||||
if not folder:
|
||||
_logger.warning(
|
||||
"Fusion Plating Documents bridge: target folder %s not found",
|
||||
_FOLDER_XMLID,
|
||||
)
|
||||
return
|
||||
|
||||
Document = self.env['documents.document'].sudo()
|
||||
|
||||
# Cache tag lookups across the batch so we don't hit env.ref per attachment.
|
||||
tag_cache = {}
|
||||
|
||||
for att in attachments:
|
||||
if att.res_model not in _QUALITY_MODELS_TO_TAG:
|
||||
continue
|
||||
# Skip attachments linked to a specific field (e.g. image_1920) —
|
||||
# those are UI artefacts, not user-uploaded docs.
|
||||
if att.res_field:
|
||||
continue
|
||||
# Skip records that have no concrete res_id (drafts being built).
|
||||
if not att.res_id:
|
||||
continue
|
||||
|
||||
tag_xmlid = _QUALITY_MODELS_TO_TAG[att.res_model]
|
||||
if tag_xmlid not in tag_cache:
|
||||
tag = self.env.ref(tag_xmlid, raise_if_not_found=False)
|
||||
tag_cache[tag_xmlid] = tag.id if tag else False
|
||||
tag_id = tag_cache[tag_xmlid]
|
||||
|
||||
doc_vals = {
|
||||
'attachment_id': att.id,
|
||||
'folder_id': folder.id,
|
||||
'name': att.name or 'Untitled',
|
||||
}
|
||||
if tag_id:
|
||||
doc_vals['tag_ids'] = [(4, tag_id)]
|
||||
|
||||
try:
|
||||
Document.create(doc_vals)
|
||||
except Exception: # pragma: no cover - defensive only
|
||||
_logger.exception(
|
||||
"Fusion Plating Documents bridge: could not create "
|
||||
"documents.document for attachment id=%s (res_model=%s, res_id=%s)",
|
||||
att.id, att.res_model, att.res_id,
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_documents_document_fp_operator,documents.document.fp.operator,documents.model_documents_document,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||
access_documents_document_fp_supervisor,documents.document.fp.supervisor,documents.model_documents_document,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
|
||||
access_documents_document_fp_manager,documents.document.fp.manager,documents.model_documents_document,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
access_documents_tag_fp_operator,documents.tag.fp.operator,documents.model_documents_tag,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||
access_documents_tag_fp_manager,documents.tag.fp.manager,documents.model_documents_tag,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
access_documents_facet_fp_operator,documents.facet.fp.operator,documents.model_documents_facet,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||
access_documents_facet_fp_manager,documents.facet.fp.manager,documents.model_documents_facet,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
|
@@ -0,0 +1,30 @@
|
||||
<?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.
|
||||
|
||||
The upstream fusion_plating_quality CAPA form does not ship with a
|
||||
button_box. We inject one at the top of the <sheet> so our stat button
|
||||
has somewhere to live. Adding via the sheet xpath keeps the core form
|
||||
untouched.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_capa_form_bridge_documents" model="ir.ui.view">
|
||||
<field name="name">fp.capa.form.bridge.documents</field>
|
||||
<field name="model">fusion.plating.capa</field>
|
||||
<field name="inherit_id" ref="fusion_plating_quality.view_fp_capa_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//sheet/div[hasclass('oe_title')]" position="before">
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_documents" type="object"
|
||||
class="oe_stat_button" icon="fa-folder-open">
|
||||
<field name="x_fc_document_count" widget="statinfo" string="Documents"/>
|
||||
</button>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?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.
|
||||
|
||||
The upstream fusion_plating_quality Doc Control form does not ship
|
||||
with a button_box. We inject one at the top of the <sheet> so our
|
||||
stat button has somewhere to live.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_doc_control_form_bridge_documents" model="ir.ui.view">
|
||||
<field name="name">fp.doc.control.form.bridge.documents</field>
|
||||
<field name="model">fusion.plating.doc.control</field>
|
||||
<field name="inherit_id" ref="fusion_plating_quality.view_fp_doc_control_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//sheet/div[hasclass('oe_title')]" position="before">
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_documents" type="object"
|
||||
class="oe_stat_button" icon="fa-folder-open">
|
||||
<field name="x_fc_document_count" widget="statinfo" string="Documents"/>
|
||||
</button>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?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.
|
||||
|
||||
The upstream fusion_plating_quality FAIR form does not ship with a
|
||||
button_box. We inject one at the top of the <sheet> so our stat button
|
||||
has somewhere to live.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_fair_form_bridge_documents" model="ir.ui.view">
|
||||
<field name="name">fp.fair.form.bridge.documents</field>
|
||||
<field name="model">fusion.plating.fair</field>
|
||||
<field name="inherit_id" ref="fusion_plating_quality.view_fp_fair_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//sheet/div[hasclass('oe_title')]" position="before">
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_documents" type="object"
|
||||
class="oe_stat_button" icon="fa-folder-open">
|
||||
<field name="x_fc_document_count" widget="statinfo" string="Documents"/>
|
||||
</button>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_ncr_form_bridge_documents" model="ir.ui.view">
|
||||
<field name="name">fp.ncr.form.bridge.documents</field>
|
||||
<field name="model">fusion.plating.ncr</field>
|
||||
<field name="inherit_id" ref="fusion_plating_quality.view_fp_ncr_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="action_view_documents" type="object"
|
||||
class="oe_stat_button" icon="fa-folder-open">
|
||||
<field name="x_fc_document_count" widget="statinfo" string="Documents"/>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
6
fusion-plating/fusion_plating_bridge_mrp/__init__.py
Normal file
6
fusion-plating/fusion_plating_bridge_mrp/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from . import models
|
||||
59
fusion-plating/fusion_plating_bridge_mrp/__manifest__.py
Normal file
59
fusion-plating/fusion_plating_bridge_mrp/__manifest__.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — MRP Bridge',
|
||||
'version': '19.0.1.0.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Bridge Fusion Plating facilities, baths and tanks to Odoo MRP work orders.',
|
||||
'description': """
|
||||
Fusion Plating — MRP Bridge
|
||||
============================
|
||||
|
||||
Part of the Fusion Plating product family by Nexa Systems Inc.
|
||||
|
||||
Links Fusion Plating infrastructure (facilities, work centres, baths, tanks)
|
||||
to Odoo's native MRP manufacturing orders and work orders so shops can:
|
||||
|
||||
* Assign a plating facility and FP work centre to an MRP work centre.
|
||||
* Tag each work order with the specific bath, tank, rack/fixture, target
|
||||
thickness, and dwell time for traceability.
|
||||
* Attach a customer specification and facility to a manufacturing order.
|
||||
* Create an MRP work centre directly from a Fusion Plating work centre.
|
||||
* Link a portal job to a manufacturing order for customer visibility.
|
||||
|
||||
Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
""",
|
||||
'author': 'Nexa Systems Inc.',
|
||||
'website': 'https://www.nexasystems.ca',
|
||||
'maintainer': 'Nexa Systems Inc.',
|
||||
'support': 'support@nexasystems.ca',
|
||||
'license': 'OPL-1',
|
||||
'price': 0.00,
|
||||
'currency': 'CAD',
|
||||
'depends': [
|
||||
'fusion_plating',
|
||||
'fusion_plating_portal',
|
||||
'fusion_plating_quality',
|
||||
'fusion_plating_logistics',
|
||||
'fusion_plating_batch',
|
||||
'mrp',
|
||||
'mrp_workorder',
|
||||
'mrp_account',
|
||||
'sale_mrp',
|
||||
'account',
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/mrp_workcenter_views.xml',
|
||||
'views/mrp_workorder_views.xml',
|
||||
'views/mrp_production_views.xml',
|
||||
'views/fp_quality_hold_views.xml',
|
||||
'views/fp_batch_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': False,
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
14
fusion-plating/fusion_plating_bridge_mrp/models/__init__.py
Normal file
14
fusion-plating/fusion_plating_bridge_mrp/models/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from . import mrp_workcenter
|
||||
from . import mrp_workorder
|
||||
from . import mrp_production
|
||||
from . import fp_work_center
|
||||
from . import fp_portal_job
|
||||
from . import fp_quality_hold
|
||||
from . import fp_delivery
|
||||
from . import fp_batch
|
||||
from . import account_move
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user