changes
This commit is contained in:
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"}
|
||||
]
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user