Files
Odoo-Modules/fusion_plating/CLAUDE.md
gsinghpal ab7ff3eea5 fix(portal): /my/orders 500 — QWeb t-value is Python not Jinja, |length is bitwise OR
orders|length in t-value parses as orders | length, not as a Jinja
length filter. orders is a sale.order recordset; the `length`
identifier resolves to None; Python evaluates
recordset | None and raises TypeError. Use len(orders) instead.

Also documents the gotcha in CLAUDE.md (rule 19) so future templates
don't reach for Jinja-style filters in t-value.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 00:13:33 -04:00

125 KiB
Raw Blame History

Fusion Plating — Claude Code Instructions

Project

Fusion Plating is a multi-module Odoo 19 ERP for electroless nickel plating and metal finishing shops. Built by Nexa Systems for EN Technologies (the client). Replaces Steelhead Software.

Recent Session Handoff — 2026-05-17 (Portal Redesign + Sub-A IA approved)

For the next Claude session. Portal dashboard + jobs + detail + configurator are LIVE on entech at fusion_plating_portal 19.0.3.7.0. Sub-A (Portal IA + Sidebar) is brainstormed, spec'd, planned, NOT yet executed — pick up there. Full handoff (live state, decisions, gotchas, how-to-deploy, what's deferred): docs/superpowers/handoffs/2026-05-17-portal-redesign-handoff.md. Approved plan ready to execute: docs/superpowers/plans/2026-05-17-portal-ia-sidebar-plan.md (11 tasks, 4 phases). Don't re-brainstorm; just execute.

Previous handoff (pre-portal redesign — superseded but kept for context)

All changes below are LIVE on entech (db admin on LXC 111 / pve-worker5). Local repo has uncommitted changes against base commit 9ebf89b; run git status to see them. All durable conventions added during this session are folded into the Critical Rules below (rules 6a, 14, 14a, 14b) — read those FIRST before changing reports / stickers / signatures / SO line tax fields.

Shipped this session

Area What Module @ version
Sticker — multi-line PO When SO has ≥2 part-bearing lines, SO External / SO Internal / FP Job External / FP Job Internal stickers print ONE consolidated sticker with Part #: "Multiple Line Items" and Qty = sum. fusion_plating_reports 19.0.11.x, fusion_plating_jobs 19.0.10.x
Sticker — em-dash mojibake _notes_content strips em-dash/en-dash/smart-quotes/ellipsis before rendering (entech wkhtmltopdf font mangles them). Same defensive pattern as thickness. See rule 14. fusion_plating_reports
Sticker — font weight + QR res .fp-sticker-strong set to font-weight 400 (only labels and WO# header stay bold). QR bumped 600→1000 (under Odoo's 1.2M-pixel barcode cap). px units + dpi=300 are calibrated — don't touch, see rule 14. fusion_plating_reports
Portal SO — Part # column New leading column on sale.sale_order_portal_content showing line.x_fc_part_catalog_id.part_number. NEW file fusion_plating_portal/views/fp_sale_order_portal.xml. fusion_plating_portal 19.0.2.2.0
Direct order "False Rev" bug fp_direct_order_wizard.py header used part.name (NULL on many parts) → printed literal "False". Now prefers part.part_number and suppresses " Rev X" when revision is blank. Cleaned up 6 existing SO lines on entech with a one-shot SQL UPDATE. fusion_plating_configurator 19.0.21.x
Direct order — remember last entered New _fp_seed_from_last_so_line on wizard line + spec carry-over inherit. On part_catalog_id onchange, prefills process_variant_id, unit_price, tax_ids, customer_spec_id from the most recent SO line for (part, customer). Spec also auto-pushes to part default after first order. "First-Time Part" warning suppressed when history exists. fusion_plating_configurator + fusion_plating_quality
Direct order — recipe filter + search Process / Recipe picker domain narrowed to templates + this part's variants + customer-used recipes (cap 500). New compute recipe_choice_ids on wizard line. Recipe _rec_names_search = ['name', 'code'] so typing the code matches. New recipe auto-set as part default if part has no other variants. fusion_plating_configurator + fusion_plating
Contract Review flow Rule 3: forced redirect to QA-005 form on part create via bus.bus notification (new JS service contract_review_redirect.js). Rule 4: WO contract-review step auto-completes at fp.job creation if the part already has a complete review; new "Contract Review (QA-005)" block on Print WO Detail showing Reviewer / Initials / Date / Status. fusion_plating_quality 19.0.6.x, fusion_plating_jobs
QA Manager (renamed from QA Signers) "QA Manager" settings field (under Contract Review block) now ALSO drives the WO Detail "Certified By" signer. Resolution: company.x_fc_qa_manager_user_ids[:1]job.manager_idcompany.x_fc_owner_user_id. fusion_plating_quality
Signature unification All FP reports (WO Detail, CoC, CoC Chronological) now read signatures from a single source: signer_user.x_fc_signature_image (Plating Signature). Retired: HR Employee signature lookup AND res.company.x_fc_coc_signature_override (UI removed; column kept, no migration). See rule 14b. fusion_plating_certificates, fusion_plating_reports, fusion_plating_jobs
Report palette overhaul Green res.company.primary_color → hardcoded neutral palette: #c1c1c1 header backgrounds, #1d1f1e th text, #2e2e2e h2/h4 titles (bumped to 20pt portrait / 22pt landscape). Grand Total row also #c1c1c1. Work Order Detail blue #1a4d80 retired in favour of the same palette. Title format now "Type # Number" (Quotation # …, Sales Order # …, Invoice # …, Packing Slip # …, Work Order Traveller # …). See rule 14a. fusion_plating_reports 19.0.11.14.0, fusion_plating_jobs 19.0.10.8.0
Report border rendering After two failed attempts (px→mm conversion + dpi bump; then border-collapse: separate single-side-per-cell), settled on border-collapse: collapse + longhand borders + background-clip: padding-box. Verticals are a hair softer than horizontals on entech wkhtmltopdf — accepted as the lesser evil vs misaligned tables. See rule 14a, last paragraph. Don't retry the single-side pattern. fusion_plating_reports

Pending — IN PROGRESS when this session ended

Customer Portal Dashboard — user asked for a "professional-looking customer dashboard listing all jobs". Brainstorming had just started (superpowers:brainstorming skill loaded). User interrupted to spawn a fresh session.

Key context already gathered:

  • Existing /my/jobs route at fusion_plating_portal/controllers/portal.py:506-554 renders a card-list view of fusion.plating.portal.job records grouped by partner.
  • Existing template fusion_plating_portal.portal_my_jobs at views/fp_portal_templates.xml:431-497 uses Bootstrap cards with a segmented progress bar (Receiving / In Progress / Shipping).
  • The customer toggle / scope: portal users see jobs where partner_id is child_of their commercial_partner_id.
  • Portal user count on entech: ZERO (SELECT count(*) FROM res_users WHERE share=true AND active=true → 0). To preview as a real customer, either log in as admin and hit /my/home directly, OR grant portal access from a customer partner record.
  • All portal URLs: /my/home, /my/jobs, /my/quote_requests, /my/configurator, /my/purchase_orders, /my/fp_invoices, /my/deliveries, /my/certifications.
  • Base URL on entech: https://enplating.com.
  • The user's design intent is "professional" — the existing implementation is functional but probably looks bland. No specific design direction has been picked yet.

Suggested first move for the fresh session: re-enter the brainstorming flow (Skill superpowers:brainstorming), offer the visual companion if it would help mock options, then ask 1-2 clarifying questions about (a) which page is the "dashboard" (replace /my/jobs vs new page) and (b) what info the customer most wants to see at a glance (status, ETA, last touch, current step, etc.).

Tangential items raised but NOT actioned

  1. Plating Departments dashboard — customer wants AMPHENOL / ENP STEEL MP/HP / LGPS 1104/1108 / PERIODIC TESTING as draggable/sortable accordion sections with jobs grouped. Investigated: most of the underlying capability already exists in the fusion_plating_shopfloor Plant Overview (drag-between-columns, search, refresh). Three options floated in the chat (repurpose fp.work.centre vs new fp.plating.department model vs group by existing dimension). User said "lets worry about this after confirming with client" — DEFERRED.

  2. Selection-type prompt has no UI for options in Simple Recipe Editor — confirmed bug: author can pick "Selection" as input_type but there's no field in the editor row to enter selection_options, so at runtime the dropdown is empty and falls through to a text input. Fix is ~10 lines of XML (add a textarea column conditionally visible when inp.input_type === 'selection'). User said "lets worry about this after confirming with client" — DEFERRED.

  3. Report title format on quality reports — the customer-facing sweep (Sale, Invoice, Packing, BoL, Receipt, Work Order) got the Type # Number format. The other 16 quality reports (NCR, CAPA, FAIR, audit, bath log, etc.) all picked up the new colour palette automatically but their title formats were NOT updated. The user said they'd let me know if they wanted them too. Currently NO follow-up requested.

  4. Reports still on per-customer hardcoded colours — the palette in rule 14a was applied per the client's request. If a future shop wants different branding, the fp_primary variable is still computed in report_base_styles.xml and per-report templates can opt back into it. Don't revert the base styles without confirming.

Module Structure (30 modules)

fusion_plating/                     — Core: facilities, process types, tanks, baths, chemistry, recipes
fusion_plating_batch/               — Rack/barrel batch tracking (FpBatch, FpBatchChemistry)
fusion_plating_kpi/                 — KPI definitions, daily auto-compute, dashboard views
fusion_plating_configurator/        — Quotation configurator, pricing engine, part catalog, 3D viewer
fusion_plating_receiving/           — Parts receiving, inspection, damage logging
fusion_plating_invoicing/           — Invoice strategies (deposit/progress/net/COD), account holds
fusion_plating_certificates/        — Certificate registry (CoC, thickness reports), Fischerscope data
fusion_plating_notifications/       — Auto-email engine, notification templates, audit log
fusion_plating_shopfloor/           — Tablet UI, plant overview kanban, process tree visualization
fusion_plating_portal/              — Customer portal + self-service configurator wizard
fusion_plating_reports/             — PDF reports (WO margin, discharge sample, CoC, etc.)
fusion_plating_compliance/          — Compliance framework, jurisdictions
fusion_plating_compliance_on/       — Ontario compliance reference data (data-only, no menus)
fusion_plating_compliance_tor/      — Toronto bylaw discharge limits (data-only, no menus)
fusion_plating_aerospace/           — AS9100 / Nadcap
fusion_plating_nuclear/             — CSA N299 / CNSC
fusion_plating_cgp/                 — Controlled Goods Program
fusion_plating_safety/              — SDS, WHMIS, JHSC
fusion_plating_quality/             — QMS (NCR, CAPA, calibration)
fusion_plating_logistics/           — Pickup & delivery, chain of custody
fusion_plating_culture/             — Values / fundamentals (⚠️ RETIRED — do NOT auto-install)
fusion_plating_bridge_mrp/          — MRP integration (recipe→WO, portal job, work order priorities)
fusion_plating_bridge_sign/         — Digital signatures
fusion_plating_bridge_quality/      — Quality bridge
fusion_plating_bridge_documents/    — Odoo Documents integration (NCR, CAPA, FAIR, Doc Control)
fusion_plating_process_en/          — Electroless nickel process pack
fusion_plating_process_chrome/      — Chrome process pack
fusion_plating_process_anodize/     — Anodizing process pack
fusion_plating_process_black_oxide/ — Black oxide process pack
fusion_tasks/                       — Local delivery dispatch (GPS, maps, driver scheduling)

Menu Structure (Plating App)

Updated 2026-04-28 — Phase 1/2/3 menu reorg consolidated 17 top-levels down to 6 (operator-visible). Industry verticals (Safety/Aerospace/Nuclear/CGP) moved INSIDE a new Compliance hub. Configuration regrouped into 7 themed folders. See the "Phase 1 / 2 / 3 — Menu reorganization" section near the bottom of this file for the full record.

The Plating app (menu_fp_root, seq 46) opens via the landing-page resolver (action_fp_resolve_plating_landing) — user override → company default → Sale Orders fallback.

Top-level menus (manager view):

Seq Menu Module(s) Visibility
5 Sales & Quoting fusion_plating_configurator + portal estimator + supervisor
8 Configurator fusion_plating_configurator estimator
12 Shop Floor fusion_plating_shopfloor operator
15 Receiving & Shipping fusion_plating_receiving + logistics receiving role
18 Operations fusion_plating (core) open (children gate per-action)
30 Quality fusion_plating_quality + certificates operator
50 Compliance (hub) fusion_plating + 5 vertical modules supervisor+
85 KPIs fusion_plating_kpi supervisor+
90 Configuration fusion_plating + many manager-only

Children re-parented in Phase 1:

  • Operations now contains: Process Recipes, Baths, Chemistry Logs, Tanks, Racks & Fixtures, Maintenance (was top-level), Move Log (was top-level, supervisor+), Labor History (was top-level), Replenishment Suggestions (supervisor+).
  • Quality now contains: Holds, NCRs, CAPAs, RMAs, FAIR, Audits, Doc Control, Certificates (was top-level).
  • Compliance hub now contains: General, Safety / WHMIS, Aerospace (AS9100 / Nadcap), Nuclear (CSA N299 / CNSC), Controlled Goods (CGP).

Configuration's 7 themed folders (manager-only by inheritance from menu_fp_config):

  1. Shop Setup — Facilities, Production Lines (was "Work Centers"), Routing Stations (was "Work Centres"), Process Categories, Process Types, Bake Ovens, Shopfloor Stations, Vehicles
  2. Recipes & Steps — Step Library, QC Checklist Templates, Quality Points
  3. Materials & Tanks — Bath Parameters, Replenishment Rules, Chemicals, Rack Tags, Calibration Equipment, Calibration Events
  4. Workforce — Operator Certifications, Shop Roles, Training Types, Quality Teams
  5. Quality & Documents — Customer Specs, Approved Vendor List, Quality Tags / Reasons / Stages, N299 Levels, Notification Templates, Notification Log
  6. Pricing & Billing — Invoice Strategy Defaults, Account Holds
  7. Reference Data — Value Sets, Value Rotations Plus Settings (sequence 1, sibling above the 7 folders).

Field Service (fusion_tasks) still has its own standalone root app (seq 45). Same task actions also accessible under Plating → Receiving & Shipping.

Culture (seq 80) — RETIRED, uninstalled on entech; the menu still defines itself in repo but doesn't appear on the live system.

Key rules:

  • Sales menu unified in fusion_plating_configurator. Portal adds Quote Requests + Portal Jobs as children. Do NOT create a separate Sales menu in portal.
  • New top-level menus should be a LAST resort. Most new functionality belongs as a child of one of the 6 existing top-levels. Adding to Configuration goes into the right themed folder.
  • When adding a new bucket folder to Configuration, define it in fusion_plating/views/fp_menu.xml near the top (Odoo's data loader is strictly sequential — every parent xmlid must be defined before any child references it).

Retired / Do-Not-Install Modules

These modules have source code in this repo but are intentionally NOT installed on entech (the client's live Odoo). Do not:

  • Include them in any -i or -u sequence that installs modules automatically.
  • Add them as a depends target from any other Fusion Plating module.
  • Re-sync their folders to /mnt/extra-addons/custom/ on entech.
  • Recommend installing them to the user without explicit confirmation.
Module State on entech Retired because What to do if revisiting
fusion_plating_culture state=uninstalled, dir removed from entech disk Soft people-ops feature (peer kudos / "Fundamental of the Week"); zero data entered; not a client priority. Top-level "Culture" menu confused operators. Ask the client whether they want it before reinstalling. If yes: re-sync folder + -i fusion_plating_culture + seed a value set.
fusion_plating_sensors deleted entirely (not in repo anymore) Duplicated fusion_plating_iot's scope but with no working alerting logic. Its valuables (sensor_type taxonomy, dashboard, location flexibility) were ported into fusion_iot/fusion_plating_iot/. N/A — gone. Any new sensor work goes in fusion_iot/fusion_plating_iot/.

Critical Rules — Odoo 19

  1. NEVER code from memory — Read reference files from the server first.

  2. Backend OWL: static template, static props = ["*"], standalone rpc() from @web/core/network/rpc. NOT useService("rpc").

  3. HTTP routes: type="jsonrpc" — NOT type="json" (deprecated in Odoo 19).

  4. Search views: NO group expand="0", NO string attribute on <search>, NO <group string="..."> wrapper for group-by filters. Use bare <group> for group-by.

  5. res.config.settings: Only boolean/integer/float/char/selection/many2one/datetime. NO Date fields.

  6. res.groups: Use privilege_id (NOT category_id). user_ids is OK but the deprecated users alias is NOT. Always include sequence field. 6a. sale.order.line tax field: It's tax_ids (Many2many) in Odoo 19 — the legacy tax_id (Many2one in earlier versions) was removed. Reading line.tax_id raises AttributeError: 'sale.order.line' object has no attribute 'tax_id'. Same pattern likely on related sale models — verify against /usr/lib/python3/dist-packages/odoo/addons/sale/models/sale_order_line.py before assuming legacy names.

  7. Field params: parent_path does NOT accept unaccent parameter in Odoo 19.

  8. SCSS borders: Use $border-color (SCSS variable) for card borders, NOT color-mix() in border shorthand — the SCSS compiler drops it. color-mix() works fine in background-color, box-shadow, etc.

  9. Theme awareness: All colours must use CSS custom properties (var(--bs-body-bg), var(--bs-body-color), var(--bs-border-color), var(--bs-secondary-color), var(--o-action)). NO hardcoded hex. See fusion_plating_shopfloor.scss as the gold standard.

  10. XML comments: No double-hyphens (--) inside <!-- --> comments — invalid XML, causes lxml parse error.

  11. XML data ordering: Window actions must be defined BEFORE <menuitem> elements that reference them in the same file.

  12. Module install on new modules: Use --update=base alongside -i MODULE to ensure Odoo rescans the addons path and finds the new module directory.

  13. Implied group cascade: implied_ids on res.groups does NOT reliably propagate to users on module install. Always include user_ids to explicitly assign admin, or fix via SQL post-install. 14a. FP report palette + border rendering: fusion_plating_reports/report/report_base_styles.xml uses #c1c1c1 for section-header backgrounds and #1d1f1e (th text on grey) / #4e4e4e (h2/h4 on white) — NOT res.company.primary_color. Per-customer request (2026-05-17) the FP reports stopped following the company brand colour so every shop gets the same neutral look. The fp_primary template variable is still computed in the styles block so per-report templates can opt back in if needed, but the default .fp-report / .fp-landscape rules use the hardcoded greys. Don't "fix" this back to fp_primary without confirming.

    Border-rendering gotcha (entech wkhtmltopdf): with the standard border-collapse: collapse + border: 1px solid #000 pattern, vertical borders can render slightly softer than horizontal borders because of how wkhtmltopdf rounds sub-pixels in its collapse-adjudication. Cells with a background-color also paint over the border edge unless clipped. Mitigations in place:

    • Longhand border declarations: border-width: 1px; border-style: solid; border-color: #000; (shorthand border: was producing mixed weights on header vs data cells)
    • background-clip: padding-box; on every th/td so the cell background never bleeds into the border edge
    • box-sizing: border-box; so width math stays predictable
    • Keep border-collapse: collapse — this is the right call even though it leaves the verticals a hair softer. Tried border-collapse: separate with the single-side-per-cell pattern (table draws top+left, cells draw right+bottom only) — it produced perfectly uniform line weights but separate-mode column widths drift between tables with different column counts, so stacked tables stop aligning at the outer edges. The misalignment is visibly worse than the soft verticals. Don't try it again.

    Applied to .fp-report table.bordered, .fp-landscape table.bordered, .fp-report .totals-table, and .fp-report .sig-table. If you need to add a new bordered table, follow the same longhand-border + background-clip template.

14b. FP report signature source: every FP report that prints a signer signature (WO Detail, CoC, CoC Chronological, future cert templates) reads from res.users.x_fc_signature_image — the "Plating Signature" the user uploads under Preferences → My Profile. Retired alternatives (2026-05-17): the HR Employee signature lookup (user.employee_ids[:1].signature) and the company-level Signature Override Image (res.company.x_fc_coc_signature_override). Both have been removed from all report templates and the company-level setting UI; the override column on res.company is kept for now (no migration) but is no longer read. Don't re-introduce the HR-Employee or override patterns — pick the signer user via whatever resolution chain the report needs (cert's certified_by_id, company's x_fc_qa_manager_user_ids[:1], job.manager_id, company.x_fc_owner_user_id, etc.) then read signer_user.x_fc_signature_image. 14. Sticker template — leave the CSS units alone: report_fp_wo_sticker_inner is calibrated for px units at paperformat dpi=300 on entech's wkhtmltopdf. Do NOT "modernise" it by converting px→mm or by bumping paperformat dpi — both have been tried (2026-05-16) and both collapsed the layout (tiny logo, tiny QR, body grid shorter than the body band, font sizes visually smaller despite using pt). The math suggests the conversions should be equivalent, but wkhtmltopdf's px↔mm↔dpi mapping doesn't follow the obvious model on this image. Trust the working geometry, change only what you came to change. Barcode size cap: Odoo core raises ValueError("Barcode too large") when width * height > 1_200_000 OR max(width, height) > 10000 (see base/models/ir_actions_report.py::barcode). Largest safe square is ~1095×1095 — we use 1000×1000 to stay clear of the ceiling. Em-dash mojibake: wkhtmltopdf's default font on entech mojibakes em-dash (—), en-dash (), smart quotes, and ellipsis into â€" etc. — strip them defensively for any free-text field that bleeds into the sticker (thickness, notes, line.name). The strip pattern is .replace(u'—', '-').replace(u'', '-')... in report_fp_wo_sticker_inner. 15. Recipe editor parity: Step-level UX features (image attachments, prompt editing, settings toggles, preview affordances, etc.) MUST be implemented in BOTH the Simple Editor (fusion_plating/static/src/{js,xml,scss}/simple_recipe_editor.* + controllers/simple_recipe_controller.py) AND the Tree Editor (fusion_plating/static/src/{js,xml,scss}/recipe_tree_editor.* + controllers/recipe_controller.py). Authors choose between editors per-recipe via preferred_editor; if a feature only lands in one, half the userbase silently misses it. Default assumption: most clients use the Simple Editor — when in doubt, ship Simple first, then port to Tree in the same change. Backend model + view changes (e.g. new fields on fusion.plating.process.node, new tabs on the node form) automatically reach both editors via the related model — only the editor-specific JS/XML/SCSS needs duplicating. 16. HTTP controller route override = method name must match parent: To override a route on an inherited controller (e.g. portal.CustomerPortal.home() at /my/home), the override method MUST share the parent's method name. Declaring a new method name with the same @http.route() URL does NOT override — Odoo registers BOTH handlers as siblings and the parent typically wins, silently. Pattern: class FpCustomerPortal(CustomerPortal): @http.route() def home(self, **kw): .... Bit us 2026-05-17 in fusion_plating_portal/controllers/portal.pyportal_my_home_dashboard() failed to override stock home(); symptom was the rich FP dashboard never rendering at /my/home even though the template was active in DB. 17. Test scaffolding — creating account.move in tests: Two custom gates block direct invoice creation in tests: - fusion_plating_jobs blocks all out_invoice/out_refund/out_receipt creates unless context.get('fp_from_so_invoice') is set or invoice_origin matches an SO name. Bypass: self.env['account.move'].with_context(fp_from_so_invoice=True).create(...). - fusion_plating_invoicing blocks action_post() when invoice_payment_term_id is unset. Bypass: pass 'invoice_payment_term_id': self.env.ref('account.account_payment_term_immediate').id in the create vals. Both are test-data scaffolding; neither weakens assertions and neither must appear in production code paths. 18. Portal list pages — no pagination, 500-record cap: All FP portal list routes (quote requests, jobs, certifications, deliveries) load up to 500 records and rely on client-side JS filtering. Do NOT re-add portal_pager to these routes. The fp_portal_list_controls macro + fp_portal_list_search.js handle filtering, counting, and the sort dropdown. Hidden <td class="d-none"> cells inside each row carry extra searchable text (part number, customer PO, contact) that isn't displayed but is matched by the JS. 19. QWeb t-value is Python, not Jinja: t-value="orders|length" does NOT call a filter — Python parses | as bitwise/recordset OR, so on a non-empty recordset it tries recordset | length_var and raises TypeError: unsupported operand types in: sale.order(…) | None (when length is undefined) or returns a merged recordset (when length happens to be another recordset). Use len(orders) or bool(orders) or (orders and orders[0]) or False — explicit Python. Same trap applies to |default, |first, |join, etc. — none of these Jinja filters exist in QWeb. Bit us 2026-05-18 on fp_sale_order_portal.xml injecting result_total into the list-controls macro.

Naming

  • New custom models (post-2026-04): fp.* prefix (e.g. fp.part.catalog, fp.certificate)
  • Existing custom models: Keep fusion.plating.* (e.g. fusion.plating.portal.job, fusion.plating.delivery)
  • New fields on standard Odoo models: x_fc_* prefix
  • Legacy fields: x_studio_*
  • Canadian English for all user-facing text
  • SCSS class prefix: o_fp_* (shopfloor: o_fp_po_*, o_fp_pt_*; recipes: o_fp_recipe_*)
  • Monetary fields: always pair with currency_id field on the same model

Smart Buttons — Anatomy + Conventions

Smart buttons sit in the <div class="oe_button_box" name="button_box"> at the top of a form view. Every smart button MUST follow this canonical pattern so the row stays visually consistent — icon on top, count in the middle, label on the bottom.

Canonical button shape

<button name="action_view_holds"            <!-- method on the underlying model -->
        type="object"
        class="oe_stat_button"               <!-- mandatory — drives the box styling -->
        icon="fa-hand-paper-o"               <!-- Font Awesome 4.x class, always fa-* -->
        invisible="x_fc_hold_count == 0">    <!-- optional; see "Conditional visibility" -->
    <field name="x_fc_hold_count" widget="statinfo" string="Holds"/>
</button>

What each piece does:

  • name= — the Python method called on click (an action_view_X returning a window action dict).
  • class="oe_stat_button" — REQUIRED. Without it the button doesn't get the stat-box styling and renders as a plain action button.
  • icon= — Font Awesome 4 (fa-cogs, fa-truck, fa-list-alt, fa-th-large, etc.). Pick one that telegraphs the target model.
  • <field widget="statinfo"> — REQUIRED for the count-on-top label-below format. Don't use string="Foo" on the <button> itself when you want a count — that produces a label-only button (the empty BOM Items issue we fixed in v19.0.17.6.0).

Don'ts (every one of these is a real bug we shipped + reverted)

  • Don't use string="Label" on <button> if the button has a meaningful count — you get a plain Label button with no number. Use the <field widget="statinfo"> form instead.
  • Don't anchor smart-button xpath to a model that may not exist (e.g. //button[@name='action_view_mrp_production']mrp.production is gone post-Sub 11). Anchor to a stable button this same view adds (e.g. action_view_pickings) or to //div[hasclass('oe_button_box')] directly.
  • Don't add a smart button that always shows zero because the underlying field/model is gone (the dead Work Orders button we removed in 19.0.17.4.0). If the count is structurally zero, drop the button entirely.
  • Don't compute counts via env.get('model')Environment in Odoo 19 has no get. Use 'model.name' in self.env then self.env['model.name'] (see Critical Rules — Odoo 19).
  • Don't put the same data behind two different buttons. "Plating Jobs" and "Work Orders" were both fp.job lookups — we kept Plating Jobs and dropped Work Orders.

Conditional visibility

If a button is only meaningful for some SOs (e.g. BOM Items is noise on a single-part SO; By Job Group is noise on an SO with no group tags), HIDE it conditionally rather than letting it render as 0 Foo:

invisible="x_fc_distinct_part_count < 2"        <!-- BOM Items: 2+ parts -->
invisible="not x_fc_has_wo_group_tag"           <!-- By Job Group: at least one tag -->
invisible="x_fc_ncr_count == 0"                 <!-- NCRs: only when there are open ones -->

Add the supporting boolean / count as a stored or non-stored compute on the model. Group multiple visibility helpers in ONE compute method to keep the _compute_smart_button_visibility chain cheap (one pass over order_line).

Ordering / placement

  • Always-visible meaningful buttons go first — they're the workflow signals an operator scans for first (Receiving, Plating Jobs, Holds, Checks).
  • NCRs / RMAs sit in the middle — visible only when present (so they pop only when there's actual quality work).
  • Conditional / multi-lens analytical buttons go LAST (BOM Items, By Job Group). They overflow into the More ▾ dropdown when the row is full, which is fine — they're the "I'm zooming into a complex SO" tools, not the daily-driver buttons.

To add a button at the end of the row regardless of where the inherited view positions things, use a second xpath:

<xpath expr="//div[hasclass('oe_button_box')]" position="inside">
    <button .../>
</xpath>

position="inside" appends to the end of the button box.

Action method shape

def action_view_holds(self):
    self.ensure_one()
    return {
        'name': _('Holds'),
        'type': 'ir.actions.act_window',
        'res_model': 'fusion.plating.quality.hold',
        'view_mode': 'list,form',                 # always 'list,form' or 'kanban,list,form'
        'domain': [('job_id', '=', self.id)],     # filter to this record's data
        'context': {'default_job_id': self.id},   # so the Create button pre-fills
    }

Always include a context with default_* keys for the Create button on the empty-list state — otherwise the operator hits Create on an empty list and gets a blank form with no link back to the source record.

Smart-button row checklist before merge

  • Uses class="oe_stat_button" and widget="statinfo" if it shows a count
  • Has an icon= (FA 4 class)
  • Has an invisible= clause if the count is structurally zero in some scenarios
  • Action method returns a window action with view_mode, domain, and context.default_*
  • Conditional/analytical buttons are pushed to the end of the button box via a second position="inside" xpath
  • No two buttons surface the same underlying records (no MRP/native duplicates)

Process Recipe System (NEW — v19.0.2.x)

Model: fusion.plating.process.node (in fusion_plating core)

  • Hierarchical tree with _parent_store = True
  • Node types: recipe, sub_process, operation, step
  • Companion model: fusion.plating.process.node.input (operator inputs)
  • icon is a Selection field (24 curated plating icons), NOT a Char
  • Auto-icon: JS guessIcon(name) maps keywords → icons when adding nodes
  • OWL tree editor: registered as fp_recipe_tree_editor client action
  • Controller: fusion_plating/controllers/recipe_controller.py (7 endpoints)
  • SCSS: fusion_plating/static/src/scss/recipe_tree_editor.scss

Recipe Endpoints

POST /fp/recipe/tree              — full nested tree for OWL editor
POST /fp/recipe/node/create       — add child node
POST /fp/recipe/node/write        — update fields
POST /fp/recipe/node/unlink       — delete + cascade
POST /fp/recipe/node/reorder      — bulk sequence update
POST /fp/recipe/node/move         — change parent_id
POST /fp/recipe/duplicate         — deep-copy recipe

Steelhead Features Status

Feature Status
Hierarchical process tree Done
Node types (recipe/sub/op/step) Done
Auto-complete flag Done
Customer visible flag Done
Manual/automated flag Done
Requires sign-off Done
Opt In/Out (disabled/opt-in/opt-out) Done
Icon picker Done
Time tracking (created/updated with seconds) Done
Operator inputs Done
Description (rich text) Done
File attachments (via mail.thread) Done
OWL tree editor with drag-drop Done
Tags Not yet
Dashboard Transitions Not yet
Treatment Groups / Choices Not yet
Go To Node Options Not yet
Spec Fields Not yet

Client Recipes Created

  • ENP-ALUM-BASIC — Electroless Nickel Plating Aluminium Basic (9 operations, 15 steps). Data file: fusion_plating/data/fp_recipe_enp_alum_basic.xml

Plant Overview Dashboard

  • OWL client action: fp_plant_overview in fusion_plating_shopfloor
  • Kanban columns = work centres, cards = active mrp.workorder records
  • Drag & drop between columns (writes workcenter_id on the work order)
  • Endpoint: POST /fp/shopfloor/plant_overview
  • Move endpoint: POST /fp/shopfloor/plant_overview/move_card
  • Auto-refreshes every 30s

Deployment

odoo-entech (LXC 111 on pve-worker5)

  • Type: Native Odoo (apt package, NOT Docker)
  • IP: 10.200.1.26
  • DB: admin (PostgreSQL local, user odoo)
  • Config: /etc/odoo/odoo.conf
  • Addons: /mnt/extra-addons/custom/ (fusion_plating modules live here)
  • Service: systemctl {start|stop|restart} odoo
  • Update command:
    ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u MODULE_NAME --stop-after-init\" && systemctl start odoo'"
    
  • Copy files: cat LOCAL_FILE | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/REMOTE_PATH'"
  • IMPORTANT: Must pass -c /etc/odoo/odoo.conf or Odoo won't find the repackaged enterprise addons

odoo-trial (VM 316 on pve-worker1)

  • Type: Docker (container odoo-trial-app, db odoo-trial-db)
  • DB: trial (user odoo)
  • Host addons path: /opt/odoo/custom-addons/ → mounts as /mnt/extra-addons/ in Docker
  • Docker network: odoo_odoo-network
  • Copy files (base64 pipe through qm guest exec):
    B64=$(base64 -w0 "LOCAL_FILE")
    ssh pve-worker1 "qm guest exec 316 -- bash -c 'echo $B64 | base64 -d > /opt/odoo/custom-addons/REMOTE_PATH'"
    
  • Clear asset cache (required after SCSS/JS changes):
    ssh pve-worker1 "qm guest exec 316 -- bash -c \"docker exec odoo-trial-db psql -U odoo -d trial -c \\\"DELETE FROM ir_attachment WHERE url LIKE '%/web/assets/%';\\\"\""
    
  • Update command:
    ssh pve-worker1 "qm guest exec 316 -- bash -c 'docker stop odoo-trial-app && docker run --rm --network odoo_odoo-network -v odoo_odoo-data:/var/lib/odoo -v /opt/odoo/custom-addons:/mnt/extra-addons -v /opt/odoo/enterprise-addons:/mnt/enterprise-addons -v /opt/odoo/odoo.conf:/etc/odoo/odoo.conf odoo:19 odoo -d trial -u MODULE_NAME --stop-after-init && docker start odoo-trial-app'"
    

Git Push

cd K:/Github/Odoo-Modules/fusion-plating && git push origin main

Pushes to both GitHub and Gitea (nexasystems.ca) via multiple remotes.

Supabase Knowledge Base

Project: nexasystems (id: ikvdlqkbqsitabxidvnq)

  • fusionapps.decisions — past architecture decisions
  • fusionapps.issues — known issues and fixes
  • fusionapps.code_snippets — reference code
  • fusionapps.quick_commands — deployment and admin commands
  • fusionapps.vm_registry — VM inventory
  • fusionapps.proxmox_nodes — cluster node specs

End-to-End Business Workflow

Full Lifecycle (What Exists Today)

┌─ QUOTATION ──────────────────────────────────────────────────────┐
│ 1. Customer submits RFQ on portal                     [DONE]     │
│    → FpQuoteRequest (state: new → under_review → quoted)         │
│    → Model: fusion_plating_portal/models/fp_quote_request.py     │
│                                                                   │
│ 2. Customer accepts → "Create Sale Order" button       [DONE]    │
│    → action_create_sale_order() creates SO with lines            │
│    → Links SO origin back to RFQ ref                             │
│                                                                   │
│ 3. SO confirmed → MRP creates Manufacturing Order      [DONE]    │
│    → Standard Odoo sale_mrp flow                                 │
└──────────────────────────────────────────────────────────────────┘

┌─ MANUFACTURING ──────────────────────────────────────────────────┐
│ 4. MO confirmed → Portal Job auto-created              [DONE]    │
│    → MrpProduction.action_confirm() override                     │
│    → Creates FpPortalJob (state: in_progress)                    │
│    → Links via x_fc_portal_job_id                                │
│                                                                   │
│ 5. Planner assigns recipe + configures steps           [DONE]    │
│    → x_fc_recipe_id set on MO                                    │
│    → Opens fp.recipe.config.wizard for opt-in/out                │
│    → Creates fusion.plating.job.node.override records            │
│                                                                   │
│ 6. Work orders generated from recipe                 [DONE]      │
│    → _generate_workorders_from_recipe() in bridge_mrp            │
│    → One WO per operation node, steps become WO instructions     │
│    → Respects opt-in/out overrides from job.node.override        │
│                                                                   │
│ 7. Operators execute WOs on shopfloor                  [DONE]    │
│    → Plant Overview kanban (drag between work centres)           │
│    → Batch chemistry tracking (FpBatch + FpBatchChemistry)       │
│    → Quality holds (FpQualityHold → FpNcr → FpCapa)             │
│                                                                   │
│ 8. MO marked done → Portal job ready_to_ship           [DONE]    │
│    → MrpProduction.button_mark_done() override                   │
│    → Auto-creates FpDelivery (draft)                             │
└──────────────────────────────────────────────────────────────────┘

┌─ SHIPPING & INVOICING ───────────────────────────────────────────┐
│ 9. CoC report generated                                [DONE]    │
│    → report_coc.xml (PDF with job info, certification, sig)      │
│    → Attached to delivery + portal job                           │
│                                                                   │
│ 10. Delivery scheduled & executed                      [DONE]    │
│     → FpDelivery: draft → scheduled → en_route → delivered       │
│     → Chain of custody auto-logged (FpChainOfCustody)            │
│     → Proof of delivery captured (FpProofOfDelivery)             │
│     → Routes with stops (FpRoute + FpRouteStop)                  │
│                                                                   │
│ 11. Delivery marked → Portal job shipped               [DONE]    │
│     → FpDelivery.action_mark_delivered() override                │
│     → Sets actual_ship_date + tracking_ref on portal job         │
│                                                                   │
│ 12. Account hold check before invoicing              [DONE]      │
│     → x_fc_account_hold on res.partner (fusion_plating_invoicing)│
│     → Blocks SO confirm, invoice post, shipping for non-managers │
│                                                                   │
│ 13. Invoice posted → Portal job complete               [DONE]    │
│     → AccountMove.action_post() override                         │
│     → Sets invoice_ref on portal job, state → complete           │
│                                                                   │
│ 14. Auto-email with CoC + Invoice + Tracking         [DONE]      │
│     → fusion_plating_notifications module                        │
│     → fp.notification.template (configurable per trigger event)  │
│     → fp.notification.log (audit trail)                          │
└──────────────────────────────────────────────────────────────────┘

┌─ CUSTOMER PORTAL ────────────────────────────────────────────────┐
│ 15. Customer sees on portal                            [DONE]    │
│     → Job progress bar (received → complete)                     │
│     → CoC download, invoice access, tracking ref                 │
│     → Quote request history                                      │
└──────────────────────────────────────────────────────────────────┘

Per-Job Recipe Overrides (v19.0.2.0.0 bridge_mrp)

  • x_fc_recipe_id on mrp.production → links MO to recipe
  • fusion.plating.job.node.override → per-job opt-in/out decisions
  • fp.recipe.config.wizard → checklist wizard for planner
  • "Overrides" stat button on MO form
  • Located in fusion_plating_bridge_mrp

All Gaps Resolved (2026-04-12/13)

Gap Resolution Module
Recipe → Work Orders _generate_workorders_from_recipe() — one WO per operation, steps become instructions fusion_plating_bridge_mrp v2.1.0
Account Hold Check x_fc_account_hold on res.partner, blocks SO/invoice/shipping for non-managers fusion_plating_invoicing
Auto-Email Package fp.notification.template + fp.notification.log with hooks on SO confirm, receiving, invoice fusion_plating_notifications
Quotation Configurator Part catalog, coating configs, pricing engine, 3D STL viewer, portal wizard fusion_plating_configurator
Parts Receiving Receiving records, inspection, damage logging, SO auto-create, MRP soft gate fusion_plating_receiving
Certificate Registry Unified fp.certificate with thickness readings, CoC/thickness/Nadcap types fusion_plating_certificates
Local Delivery Forked fusion_tasks with GPS/maps, stripped of claims/sync, delivery-specific fields fusion_tasks

Architectural Decisions Made

  1. Recipe → WO: One WO per operation node, child step nodes become numbered instructions in WO description
  2. Account hold: Manual flag on res.partner (auto from aging is roadmap)
  3. Email triggers: SO confirmed, parts received, invoice posted (configurable per trigger)
  4. Configurator: Custom build with formula-based pricing, estimator override, portal self-service wizard
  5. Model naming: New models use fp.* prefix, existing keep fusion.plating.*
  6. Security groups: Role-based (Estimator, Receiving, Accounting, Shop Manager) layered on existing privilege hierarchy (Operator→Supervisor→Manager→Admin)

Key Models Quick Reference

Model Module Purpose
fusion.plating.process.node fusion_plating Recipe tree (template)
fusion.plating.process.node.input fusion_plating Operator input definitions
fusion.plating.job.node.override fusion_plating_bridge_mrp Per-job opt-in/out
fp.part.catalog fusion_plating_configurator Customer part library (geometry, material)
fp.coating.config fusion_plating_configurator Coating configuration templates
fp.treatment fusion_plating_configurator Pre/post treatment steps
fp.pricing.rule fusion_plating_configurator Formula-based pricing engine
fp.pricing.complexity.surcharge fusion_plating_configurator Complexity surcharge lines
fp.quote.configurator fusion_plating_configurator Configurator session + price calc
fp.receiving fusion_plating_receiving Parts receiving record
fp.receiving.line fusion_plating_receiving Per-part receiving detail
fp.receiving.damage fusion_plating_receiving Damage log entry
fp.invoice.strategy.default fusion_plating_invoicing Customer-level invoice strategy
fp.certificate fusion_plating_certificates Certificate registry (CoC, thickness, etc.)
fp.thickness.reading fusion_plating_certificates Fischerscope measurement data
fp.notification.template fusion_plating_notifications Configurable email notification
fp.notification.log fusion_plating_notifications Email audit trail
fusion.plating.quote.request fusion_plating_portal Customer RFQ
fusion.plating.portal.job fusion_plating_portal Portal-facing job tracker
fusion.plating.customer.spec fusion_plating_quality Spec library
fusion.plating.quality.hold fusion_plating_quality Parts on hold
fusion.plating.ncr fusion_plating_quality Non-conformance reports
fusion.plating.capa fusion_plating_quality Corrective actions
fusion.plating.batch fusion_plating_batch Rack/barrel batch tracking
fusion.plating.kpi fusion_plating_kpi KPI definition (OTD, yield, throughput, etc.)
fusion.plating.kpi.value fusion_plating_kpi KPI daily value (auto-computed or manual)
fusion.plating.delivery fusion_plating_logistics Delivery with chain of custody
fusion.plating.pickup.request fusion_plating_logistics Customer pickup requests
fusion.plating.route fusion_plating_logistics Driver routes with stops
fusion.technician.task fusion_tasks Local delivery task (GPS, maps)
fusion.technician.location fusion_tasks Driver GPS tracking

Repackaged Enterprise Modules

See K:\Github\RePackaged-Odoo\CLAUDE.md for full details. Key points:

  • Odoo 19 enterprise modules repackaged for community edition
  • All OEEL-1 licenses changed to LGPL-3
  • Phone-home/telemetry gutted
  • web_enterprise and mail_enterprise are installed on odoo-entech
  • Addons path includes: _dependencies, accounting, inventory_manufacturing, hr, sales, ai, fusion_backend, custom, website

Fine-Tuning Initiative (Started 2026-04-21)

System-wide UX gap closure. Running PLAN → SPEC → IMPLEMENT per sub-project so we don't rewrite code as new requirements surface. Each sub-project has its own design doc in docs/superpowers/specs/ and its own implementation plan before any code lands.

Sub-Project Roadmap

# Sub-project Status Gaps
1 Direct Order Wizard fix (no auto-confirm/auto-email) Shipped 2026-04-22 (commit afd8bae+) Gap 1
2 Part Data Model Overhaul (part#/rev required, dual descriptions, per-part cert requirement, SKU→Part Number on customer docs) Shipped 2026-04-22 (commits 868b418..afd8bae) 2b, 2c, 2d, 4
3 Default Process + Composer per part (reuse recipe tree) Shipped 2026-04-22 (commits ce07daa..f059348) 2e, 2f
4 Contract Review (optional, per-part, settings-driven QA roster, QA-005 1:1 PDF) Shipped 2026-04-22 2i
5 Order-line fields (fp.serial registry, auto job#, coating-scoped thickness dropdown, revision picker) Shipped 2026-04-22 5, 6, Q2
6 Contact Profiles & Communication Routing (per-contact flags + per-location routing + global contact; single resolver helper) Shipped 2026-04-22 client transcript A/B/C
7 IoT tuning (per-sensor polling interval + ingest rate-limit; entech seeded with 25 tanks / 50 sensors) Shipped 2026-04-22 client transcript D
8 Receiving / Inspection / QC flow restructure (fp.receiving = box count only; new fp.racking.inspection per MO; WO soft gate; delivery box-parity warning) Shipped 2026-04-22 client transcript E
9 Process variants per part + persistent draft order wizard + tax per line + payment terms wired + chatter + nicer breadcrumbs across plating models Shipped 2026-04-26 various wizard/UX
10 Quote → Direct Order promotion (won quotes consolidate onto a single PO instead of spawning standalone 1-line SOs) Shipped 2026-04-26 redundancy concern
11 MRP cutout — bridge_mrp deletion + MRP module uninstall (7-phase migration: relocate models, swap inherits, drop legacy FK columns, uninstall mrp + 10 cascade modules) Shipped 2026-04-26 bridge_mrp removal
12 Native Quality — full Odoo quality_control replacement + RMA + integration polish In flight (planned) quality dependency removal
First-off / last-off QC Deferred client transcript F
VEC machine auto-ingest (Word-format thickness report from network-connected XRF; different machine from Fischerscope) Deferred client transcript G
RMA customer portal submission Deferred (Sub 12 phase 2) follow-on to Sub 12

Sub 2 Locked Decisions (2026-04-21)

Q Decision
Q1 — Cert requirement precedence Part wins; partner is fallback. New selection certificate_requirement on fp.part.catalog: inherit / none / coc / coc_thickness. Default inherit preserves current behaviour for existing records.
Q2 — Revision handling Keep existing chain (parent_part_id, is_latest_revision, revision_ids). Out-of-scope for Sub 2. The "revision picker at order entry" moves to Sub 5.
Q3 — Required-field flip Strict + backfill. On upgrade: part_number = name if empty; revision = 'A' if empty. Then required=True for both. name becomes optional.
Q4 — Descriptions shape Split fp.sale.description.template.description into internal_description + customer_facing_description. Repeater on the part's Descriptions tab gains two columns. Old description column dropped in migration.
Q5 — SKU vs Part Number Use fp.part.catalog.part_number directly as the source of truth. Don't sync to default_code. Customer-facing reports print part_number; internal reports keep showing default_code (service code). Odoo-native screens untouched.
Q6 — Description required at order entry Both required. SO line carries name (customer-facing, already Odoo standard) + new x_fc_internal_description (ops workflow). Both required before save.

Sub 2 Defensive Measures (Prevent Rework When Later Subs Land)

  1. Single-source cert resolution functionmrp.production._fp_resolve_cert_requirement(self) returns (want_coc, want_thickness). Every caller (cert cascade, QC gate, notification routing) goes through this. When Sub 6 restructures partner-level flags into location / contact permissions, one function updates — no call-site hunt.
  2. Shared QWeb line-header macrofusion_plating_reports.customer_line_header renders part_number + revision + customer-facing description with fallback to product name for non-part lines. All 4 customer-facing reports (SO, invoice, packing slip, BoL) call the macro. Sub 5's revision picker updates the macro once, all reports follow.
  3. Isolated migration — Sub 2's post_init_hook is idempotent (NULL/empty checks). Safe to re-run. Doesn't fight Sub 3/4/5/6 migrations.
  4. Additive SO line fieldsx_fc_internal_description, x_fc_description_template_id sit alongside future Sub 5 fields (x_fc_serial_number, x_fc_job_number, x_fc_thickness, x_fc_revision_snapshot) with zero touchpoints.
  5. Clean removal of old description column — migrated then dropped. Not kept as deprecated. One clean break now beats two migrations later.

Sub 6 Preview — Contact Profiles & Communication Routing (client transcript A/B/C)

  • Sub-contacts under res.partner with per-contact permissions: certs / QC / quotes+SO / invoices.
  • Multiple delivery locations per customer; each location has its own notification list.
  • Global contact (company-level + location-level) gets all communications.
  • Will restructure or augment the partner-level x_fc_send_coc / x_fc_send_thickness_report flags that Sub 2 currently falls back to. Sub 2's _fp_resolve_cert_requirement is the update point.

Sub 7 Preview — IoT Tuning (client transcript D)

  • 610 active tanks (of ~2025 total) need continuous monitoring.
  • Polling interval: 30 minutes acceptable, 15 minutes ideal. Configurable per tank.
  • Temperature, pH, nickel concentration — all on automated controller (existing fusion_plating_iot module).
  • Work scope: ensure per-sensor interval field exists + defaults + seed 610 tank.sensor records.

Sub 8 Preview — Receiving / Inspection / QC Restructure (client transcript E)

Current flow (wrong): Direct order → receiving entry → receiver inspects on arrival. Correct flow:

  1. Customer ships parts in boxes. Receiver counts boxes (does NOT inspect individual parts).
  2. Boxes sit in staging until racking.
  3. Racking crew opens boxes, inspects each part as they load racks (inspection ≠ receiving).
  4. Parts go through plating process.
  5. Post-plate QC on machine (thickness / depth / coating thickness) — existing QC gate (Phase 13 work).
  6. Pack back into the SAME boxes they arrived in. Same qty out as in.

Implication: The current fusion_plating_receiving module conflates receiving + inspection. Sub 8 splits them. Racking-time inspection becomes its own record, linked to WOs not to receiving.

Deferred Items (Future)

  • First-off / last-off QC — first and last part of each batch get full QC inspection; middle parts sampled. Not priority.
  • VEC machine auto-ingest — different from Fischerscope. Exports a Word doc (picture + data) named workorder_PO.docx to a network share. Plan: auto-scan the share, parse, attach to QC as thickness_report. Defer until core flow is solid.

Client-Confirmed Operational Thresholds

  • Tank polling: 1530 min, half-hour acceptable
  • Active tanks: 610 (not all 2025)
  • Boxes round-trip: parts ship back in the same boxes they arrived in, same quantity per box

How to Resume This Work in a Fresh Session

  1. Read this section (Fine-Tuning Initiative).
  2. Check the sub-project status table — which sub is in flight.
  3. Read the corresponding spec in docs/superpowers/specs/YYYY-MM-DD-sub<N>-*-design.md.
  4. Read the implementation plan if one exists.
  5. Continue from the next un-checked step.

Sub 11 — MRP Cutout (shipped 2026-04-26)

The Odoo mrp module + 10 cascade dependents have been uninstalled. fusion_plating_bridge_mrp is gone. The plating shop runs entirely on fp.job / fp.job.step. Document this so a fresh session doesn't try to re-add MRP refs.

Final state

  • 0 rows in mrp_production, mrp_workorder, mrp_workcenter
  • 205+ fp.job rows, 1,800+ fp.job.step rows in production
  • 0 custom-table FKs to MRP
  • Modules uninstalled: mrp, mrp_workorder, mrp_account, sale_mrp, purchase_mrp, quality_mrp, quality_mrp_workorder, project_mrp*, fusion_manufacturing, fusion_plating_bridge_mrp

Where things ended up after Sub 11

Model / asset Old home New home
fp.work.role, fp.operator.proficiency, hr.employee shop-roles, fusion.plating.process.node.x_fc_work_role_id fusion_plating_bridge_mrp fusion_plating (core)
fp.qc.checklist.template (+line) fusion_plating_bridge_mrp fusion_plating_quality
fusion.plating.quality.check (+line) fusion_plating_bridge_mrp fusion_plating_quality
fp.thickness.reading.quality_check_id link + auto_extracted fusion_plating_bridge_mrp fusion_plating_quality
res.partner.x_fc_requires_qc + x_fc_qc_template_id fusion_plating_bridge_mrp fusion_plating_quality
fp.job.consumption fusion_plating_bridge_mrp fusion_plating_jobs
sale.order.x_fc_workflow_stage + x_fc_assigned_manager_id + workflow buttons fusion_plating_bridge_mrp fusion_plating_jobs
QC tablet OWL (fp_qc_checklist.js/.xml/.scss) + /fp/qc/* controller fusion_plating_bridge_mrp fusion_plating_quality
Production Priorities kanban fusion_plating_bridge_mrp (mrp.workorder) fusion_plating_jobs (fp.job.step)

Hard rules going forward

  1. Never re-introduce 'mrp' as a manifest dep. Use fp.job for jobs, fp.job.step for operations.
  2. x_fc_job_id is the canonical job link, not production_id. Drop legacy MO refs as you find them.
  3. fusion_plating_quality depends on fusion_plating_shopfloor for SCSS tokens ($fp-page, $fp-card, $fp-accent). Don't strip that dep — the QC tablet bundle breaks without it.
  4. The QC tablet OWL template namespace is fusion_plating_quality.FpQcChecklist (was fusion_plating_bridge_mrp.FpQcChecklist). Don't rename back.

Sub 12 — Native Quality Module (in flight, ~4 working days)

Goal: Build a complete native quality stack matching Odoo quality_control functionality plus plating-specific extensions (RMA, CAPA effectiveness, holds, 8D reports), with zero dependency on Odoo's quality / quality_control. After Sub 12 lands, those modules + fusion_plating_bridge_quality get uninstalled.

Module choice

Enrich fusion_plating_quality — no new modules. Existing module already owns NCR / CAPA / Hold / Check / Calibration / AVL / FAIR / Audit / Doc Control / Customer Spec / Contract Review.

Locked decisions (don't re-ask in fresh session)

Q Decision
RMA portal submission Deferred to phase 2. Internal-only RMA in Sub 12.
8D format Full 8D (D1D8 sections in the combined NCR + CAPA PDF).
Quality Dashboard 5 tabs (Holds / Checks / NCRs / CAPAs / RMAs) in one client action with a summary header that totals open + overdue across all five.
Auto-NCR + auto-Hold on RMA receive Automatic, with a manager-only "skip this RMA's auto-spawn" toggle on the RMA record.
Auto-CAPA on NCR closure Automatic when severity in (high, critical), with a manager-only override on the NCR.
Quality team model Build a dedicated fp.quality.team rather than reusing res.groups. Teams need their own kanban grouping + per-team escalation chains, which groups don't model well.
Stage model vs. state field on NCR Both. Keep the existing state Selection (used by code paths). Add a parallel stage_id Many2one to fp.quality.alert.stage for the kanban draggable view. Computed bidirectional sync (stage ↔ state).
Trigger-based quality.point Build a new fp.quality.point model. Trigger types: manual, receiving_done, job_step_done, job_done. Existing fp.qc.checklist.template STAYS — it's the template a point fires; the point is the trigger rule.
RMA back-link to original SO line Required field. Always carry the original SO line so cert / part / coating context follows the return.
Module choice (one or many) Single module — enrich fusion_plating_quality.

Phase A — RMA model (~1 day)

File: fusion_plating_quality/models/fp_rma.py

Model: fusion.plating.rma

Field Type Notes
name Char Sequence RMA/YYYY/NNNN
partner_id M2O res.partner Required
sale_order_id M2O sale.order The original order being returned
sale_order_line_ids M2M sale.order.line Specific lines being returned (subset of the SO)
original_job_ids M2O fp.job (compute from SO lines) For navigation only
state Selection draft / authorised / shipped_to_us / received / triaged / resolving / resolved / closed / cancelled
trigger_source Selection customer_complaint / qc_fail_post_ship / inspection_post_delivery / other
severity Selection low / medium / high / critical
complaint_description Html What the customer reported
triage_findings Html What we found on inspection
resolution_type Selection replace / rework / refund / scrap
resolution_notes Html Free-form notes on the chosen path
replacement_job_id M2O fp.job When replace/rework — the new job created
refund_invoice_id M2O account.move When refund — the credit note
inbound_receiving_id M2O fp.receiving The receiving record auto-created when carrier delivers
inbound_picking_id M2O stock.picking Optional — if a stock.picking is also created
linked_ncr_ids O2M fusion.plating.ncr (inverse rma_id) NCRs spawned from this RMA
linked_capa_ids O2M fusion.plating.capa (related via NCRs) Read-only roll-up
linked_hold_ids O2M fusion.plating.quality.hold (inverse rma_id) Holds placed on returned parts
qty_returned Integer Total units customer is returning
qty_received Integer Counted on receipt
customer_tracking Char Customer's outbound tracking #
our_tracking Char Our return-to-shop tracking #
carrier_id M2O delivery.carrier Optional
qr_code Binary (compute) QR encoding /fp/rma/<id> for the authorisation PDF
auto_spawn_ncr Boolean Default True. Manager can toggle off before saving.
auto_spawn_hold Boolean Default True.
tag_ids M2M fp.quality.tag (Sub 12 Phase B)
reason_id M2O fp.quality.reason (Sub 12 Phase B)
team_id M2O fp.quality.team (Sub 12 Phase B)
chatter mail.thread mandatory

Lifecycle hooks

  • action_authorise: state draft → authorised. Generate the RMA authorisation PDF + email link/QR to customer (using fp.notification.template if installed; falls back to standard mail.template).
  • action_mark_shipped_to_us: customer-driven; updates state when carrier scan logged.
  • On fp.receiving create with rma_id set: state → received. If auto_spawn_ncr, create an fusion.plating.ncr pre-filled (description, severity, customer, parent SO line). If auto_spawn_hold, create fusion.plating.quality.hold for the returned qty.
  • action_triage_complete: state → triaged. Requires resolution_type set.
  • action_resolve: state → resolved. Triggers resolution-specific actions:
    • replace → spawn new fp.job cloned from original
    • rework → spawn new fp.job referencing the returned units (linked to inbound fp.receiving)
    • refund → open account.move.refund wizard, link result to refund_invoice_id
    • scrap → create fp.job.consumption row tagged 'rma_scrap' + post chatter
  • action_close: state → closed. Locks editing.
  • action_cancel: any state → cancelled (manager only).

Smart buttons

RMA form gets buttons to: original SO, original Jobs, inbound Receiving, replacement Job, refund Invoice, NCRs (count), CAPAs (count), Holds (count). Per-target button visibility based on resolution_type / state.

Sequence

Create ir.sequence fp.rma with prefix RMA/%(year)s/, padding 4. Data file fp_rma_sequence.xml.

Reports

fusion_plating_reports/report/report_fp_rma_authorisation.xml — single-page customer-facing PDF with QR code. Branded "EN Technologies".

Phase B — Categorisation & kanban infra (~half day)

Files: fusion_plating_quality/models/fp_quality_tag.py, fp_quality_reason.py, fp_quality_team.py, fp_quality_alert_stage.py

fp.quality.tag

  • name (Char, required, translate)
  • color (Integer, kanban color)
  • active (Boolean)
  • Reused by NCR / CAPA / Hold / RMA / Check via M2M tag_ids

fp.quality.reason

  • name, description, category (selection: process / supplier / equipment / human / material / other)
  • Curated reason library so root-cause classification is consistent

fp.quality.team

  • name, lead_user_id (M2O res.users), member_ids (M2M res.users)
  • escalation_user_id (manager who gets notified on missed deadlines)
  • Used by NCR / RMA — primary owner team

fp.quality.alert.stage

  • name, sequence, fold (Boolean — collapsed-by-default in kanban)
  • Default stages seeded: New / Investigating / Containment / Disposition / Awaiting Sign-off / Closed / Cancelled
  • Add stage_id = fields.Many2one('fp.quality.alert.stage') to fusion.plating.ncr AND fusion.plating.rma. Map state ↔ stage_id via _inverse_* so legacy code paths keep working.

Apply tag/reason/team M2M/M2O fields to: NCR, CAPA, Hold, Check, RMA

Each model gets tag_ids, reason_id, team_id. NCR + RMA additionally get stage_id.

Phase C — Trigger-based quality points (~half day)

File: fusion_plating_quality/models/fp_quality_point.py

fp.quality.point

  • name, active, description
  • trigger_type (Selection): manual / receiving_done / job_confirmed / job_step_done / job_done / so_confirmed
  • Filters (any combination): partner_ids (M2M), part_catalog_ids (M2M), coating_config_ids (M2M), step_kind (Selection — wet/bake/inspect/etc.)
  • template_id (M2O fp.qc.checklist.template) — required, the checks to spawn
  • assignee_user_id (M2O res.users) — optional default inspector
  • Fires _spawn_check_for(<source_record>) which creates a fusion.plating.quality.check from the template + binds it to the source via job_id or step_id.

Hooks (already partly in place — extend)

  • fp.receiving.write hook (existing): when state flips to closed, walk all fp.quality.point with trigger receiving_done matching the receiving's partner/parts → spawn checks.
  • fp.job.action_confirm hook (existing — currently calls _fp_create_qc_check_if_needed): replace with quality.point lookup. Keep the existing partner-template fallback as a default point seeded by fp_qc_data.xml.
  • fp.job.button_mark_done: trigger job_done points.
  • fp.job.step.button_finish: trigger job_step_done points.

Phase D — Integration polish (~1 day)

  1. fp.job form smart-button row: add Holds, Checks, NCRs, CAPAs, RMAs buttons with badge counts. Always-visible (zero is OK).
  2. sale.order form smart-button row: same five, rolled up across all linked jobs.
  3. res.partner form: customer-level "Quality History" smart button that opens a kanban filtered to that partner across all 5 record types.
  4. One-click cross-creation:
    • Hold form → Open NCR button — pre-fills NCR with hold's part / customer / quantity / linked job.
    • NCR form → Spawn CAPA button — visible when state ∈ {disposition, closed} and severity ≥ medium.
    • CAPA form → Verify Effectiveness button — schedules a follow-up check on the originating NCR.
  5. Unified Quality Dashboard (fp_quality_dashboard client action):
    • 5 tabs: Holds / Checks / NCRs / CAPAs / RMAs
    • Each tab is a kanban grouped by stage_id (NCRs/RMAs) or state (Holds/Checks/CAPAs)
    • Header summary card: open count + overdue count across all 5 types
    • Filters: my team / my customer / overdue / high-severity
    • Menu: Plating → Quality → Dashboard
  6. CAPA closure-loop linkage: when CAPA effectiveness verification fails, auto-spawn a new NCR linked back to the original. Closes the loop "we said we fixed it but it happened again."

Phase E — Reports (~half day)

Files: fusion_plating_reports/report/report_fp_rma_authorisation.xml, report_fp_8d.xml, report_fp_quality_monthly.xml

  1. RMA Authorisation PDF: single-page customer-facing. Header with our logo + customer info, RMA number, parts listed (table), return-to address, QR code linking to /fp/rma/<id> for status tracking, carrier instructions.
  2. 8D Report (NCR + CAPA combined):
    • D1: Team (from team_id + member_ids)
    • D2: Problem description (NCR description + scope)
    • D3: Containment (NCR containment narrative)
    • D4: Root cause analysis (CAPA root_cause + reason_id)
    • D5: Permanent corrective action (CAPA action_plan)
    • D6: Implement & verify (CAPA implementation_date + verification_evidence)
    • D7: Prevent recurrence (CAPA preventive_actions)
    • D8: Congratulate the team (CAPA closure notes + team sign-offs)
    • Auto-renders when both NCR and CAPA exist; degraded mode if CAPA missing.
  3. Monthly Quality Summary (fp_quality_monthly report):
    • Counts by record type / severity / customer / month
    • Overdue ageing buckets
    • CAPA effectiveness rate (verified / total closed)
    • Repeat-customer-issue flag (>2 NCRs same customer in 90 days)
    • Run via cron monthly + on-demand from dashboard.

Phase F — Test + verify (~half day)

End-to-end smoke flow on a fresh DB:

  1. Customer reports issue → create RMA → authorise → email PDF
  2. Customer ships → carrier delivers → fp.receiving auto-created → RMA receives → NCR + Hold auto-spawn
  3. QA triages NCR → finds root cause → spawns CAPA (auto via severity rule)
  4. CAPA assigned to engineering → action plan written → implemented → effectiveness check scheduled
  5. Effectiveness verified → CAPA closes → NCR closes → RMA resolves (rework path) → replacement job created from original → ships → CoC issued → invoice
  6. Run 8D report on the closed NCR/CAPA pair
  7. Verify dashboard counts update at every state transition
  8. Confirm legacy NCR/CAPA/Hold/Check forms still work (no regressions)
  9. ACL drilldown: operator sees what they should, supervisor more, manager all

Phase G — Drop Odoo quality cascade (~30 min)

Pre-conditions: Phases AF all merged + smoke-tested.

  1. Strip the three custom fields from fusion.plating.ncr (x_fc_quality_alert_id, x_fc_quality_alert_synced, x_fc_auto_sync — added by bridge_quality)
  2. Remove fusion_plating_bridge_quality from /mnt/extra-addons/custom/
  3. SQL: UPDATE ir_module_module SET state='to remove' WHERE name IN ('fusion_plating_bridge_quality', 'quality_control', 'quality') AND state='installed';
  4. Restart odoo → cascade uninstall fires
  5. ALTER TABLE drop the three NCR columns
  6. (Optional) move /mnt/extra-addons/inventory_manufacturing/quality{,_control}/ out of the path so they can't auto-reinstall

Server / deployment notes (entech)

  • LXC 111 on pve-worker5, native odoo (apt), DB admin, addons path /mnt/extra-addons/custom/
  • Update flow:
    ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && \
      su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin \
        -u fusion_plating_quality --stop-after-init\" && systemctl start odoo'"
    
  • File copy:
    cat LOCAL | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > REMOTE'"
    
  • Asset cache bust: DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';
  • Always bump module version in __manifest__.py for migrations to fire (current fusion_plating_quality: 19.0.3.0.0; bump to 19.0.4.0.0 for Sub 12).

Build order (executable checklist for fresh session)

  1. Read this Sub 12 section in full + the Sub 11 section above (for context on what's already native).
  2. Bump fusion_plating_quality/__manifest__.py version to 19.0.4.0.0.
  3. Phase A — RMA: create fp_rma.py model + fp_rma_views.xml + fp_rma_sequence.xml + ACL rows + add to __manifest__.py data list.
  4. Phase A migration: not needed (new model, fresh table).
  5. Phase B — categorisation: create the 4 small models + their views + ACL. Add tag_ids / reason_id / team_id M2M/M2O to NCR, CAPA, Hold, Check, RMA. Add stage_id to NCR + RMA.
  6. Phase B data: seed default stages + a few starter tags/reasons/teams in fp_quality_categorisation_data.xml.
  7. Phase C — fp.quality.point model + view + ACL + the 4 trigger hooks (in fp.receiving, fp.job.action_confirm, fp.job.button_mark_done, fp.job.step.button_finish).
  8. Phase D — smart buttons on fp.job, sale.order, res.partner. Cross-creation buttons. Dashboard client action.
  9. Phase E — three QWeb reports.
  10. Phase F — manual smoke test + ACL drilldown + screenshot the dashboard.
  11. Deploy each phase as it lands (don't batch — easier to roll back). Bump version each time.
  12. Phase G runs LAST, only after confirmation that AF work end-to-end.

Things to NOT do

  • Don't add 'quality' or 'quality_control' to any manifest dep. They will be uninstalled by Phase G.
  • Don't import from odoo.addons.quality.*. Use only native models.
  • Don't put RMA in a new module. It belongs in fusion_plating_quality.
  • Don't break the existing QC tablet OWL. Its template namespace is fusion_plating_quality.FpQcChecklist, endpoints are /fp/qc/*, and fusion_plating_quality depends on fusion_plating_shopfloor for SCSS tokens.
  • Don't re-introduce production_id references anywhere. Use job_id / x_fc_job_id. MRP is gone.
  • Don't forget rma_id inverse field on NCR + Hold — those One2many fields on RMA need an inverse Many2one on the linked model.

Status check before starting (run this first in the fresh session)

-- Should show 4: NCR, CAPA, Hold, Check (Sub 12 adds RMA = 5)
SELECT model FROM ir_model WHERE model LIKE 'fusion.plating.%' AND model SIMILAR TO '%(ncr|capa|hold|check|rma)%';

-- Should show 'fusion_plating_quality_bridge_quality_control' state — likely 'installed' until Phase G
SELECT name, state FROM ir_module_module WHERE name LIKE 'quality%' OR name LIKE 'fusion_plating_bridge_quality';

-- Confirm MRP is gone (Sub 11)
SELECT name, state FROM ir_module_module WHERE name = 'mrp';  -- expect 'uninstalled'

-- Live row counts so you know what survives
SELECT 'ncr' AS m, count(*) FROM fusion_plating_ncr
UNION ALL SELECT 'capa', count(*) FROM fusion_plating_capa
UNION ALL SELECT 'hold', count(*) FROM fusion_plating_quality_hold
UNION ALL SELECT 'check', count(*) FROM fusion_plating_quality_check;

Contract Review — Policy B (shipped 2026-04-28)

The fp.contract.review model (QA-005) was originally shipped as "always optional, never blocks anything" (Sub 4). Audit 2026-04-28 revealed three integration holes:

  1. The Simple Recipe Editor library had no Contract Review step template, so authors couldn't drop QA-005 into a recipe at all.
  2. Adding a node literally named "Contract Review" to a recipe did nothing — no auto-create, no operator routing, no gate.
  3. The pre-Sub-11 contract_review_user_ids approver list on fp.process.node was dead — mrp.workorder.button_finish used to gate on it, but fp.job.step never picked up the gate.

Policy B (chosen 2026-04-28) — Contract Review is REQUIRED on a per-customer basis (partner.x_fc_contract_review_required), soft elsewhere. Recipe-side enforcement closes the post-Sub-11 hole.

What's wired

Trigger Behaviour
fp.step.template.default_kind = 'contract_review' New kind in the Simple Editor library. Auto-seeds 3 inputs: Reviewer Initials / Date Reviewed / QA-005 Approved (pass_fail).
Library seeders (_STARTER_KIND_BY_NAME, _seed_minimal_library) "Contract Review" is the FIRST entry in the minimal library. Authors drag-drop it into recipes from the Simple Editor sidebar.
fp.job.step.button_start on a Contract Review step Auto-creates fp.contract.review for the linked part if missing, returns an act_window pointing at the QA-005 form. Operator gets routed straight to the form without hunting for the smart button on the part.
fp.job.step.button_finish on a Contract Review step Blocks unless fp.contract.review.state == 'complete' AND current user is on recipe.contract_review_user_ids (when configured). Manager bypass: fp_skip_contract_review_gate=True in context.
Step detection _fp_is_contract_review_step() matches case-insensitive name == "contract review" / "qa-005" OR recipe_node_id.source_template_id.default_kind == 'contract_review' (simple-editor library entry).

What stays optional (NOT enforced)

  • Customers without x_fc_contract_review_required=True get the soft banner only — no step-level block. The customer-flag gate is the ONLY enforcement trigger.
  • Adding a Contract Review node to a recipe for a customer that doesn't require it is purely documentary; nothing fires.

Why the part-side banner stays

The part-form banner ("New part created. Please complete the Contract Review (QA-005) if applicable.") is independent of the recipe step. It nudges QA before any job is started — an early-detection mechanism distinct from the in-flight step gate. Both can fire on the same part (banner first, then step gate later); one resolution clears both.

Manager bypass examples

# Skip the step-level gate from a privileged caller (script / shell)
step.with_context(fp_skip_contract_review_gate=True).button_finish()

Files touched

  • fusion_plating/models/fp_step_template.py — added contract_review kind + 3 default inputs.
  • fusion_plating/models/fp_process_node.pyalso added contract_review to default_kind Selection here. Easy to miss: the node and the template have separate Selection fields and they must stay in lockstep.
  • fusion_plating/__init__.py — added "Contract Review" / "QA-005" to _STARTER_KIND_BY_NAME + first entry in _seed_minimal_library, exposed fp_resolve_step_kind() helper.
  • fusion_plating_jobs/models/fp_job_step.py — added _fp_is_contract_review_step, _fp_resolve_contract_review_part, _fp_open_contract_review, _fp_check_contract_review_complete; hooked into button_start (auto-open form) + button_finish (gate). Sub 11's contract_review_user_ids field on fp.process.node is now wired again.

Bugs caught during the persona walkthrough (2026-04-28, fixed 12.4.1)

A scripted "brand-new estimator builds a recipe from scratch" walk (/tmp/fp_recipe_walkthrough.py on entech) surfaced 7 real gaps; all fixed in 19.0.12.4.1. The walk is preserved as a smoke test — re-runnable on any DB to verify the library is healthy.

# Bug Fix
1 _seed_step_library_if_empty skips when the library is non-empty, so existing DBs got NO Contract Review template after Policy B shipped. Migration 19.0.12.4.1/post-migrate.py — backfills the template if missing.
2 fp.process.node.default_kind Selection didn't include contract_review, so dropping the template into a recipe blew up with ValueError. The kind is on TWO models (template + node) and they drifted. Added contract_review to the node's Selection too.
3 The library had only racking populated as a kind (1/16). 12 of 14 templates landed with default_kind = NULL because the original seeder used a brittle case-sensitive lookup. Migration backfills default_kind via the new fp_resolve_step_kind() helper.
4 _STARTER_KIND_BY_NAME lookup was hyphen / -ing / case sensitive — "E-Nickel Plating" didn't match 'e-nickel plate', "DeRacking" didn't match 'de-racking', "Ready For Masking" didn't map to gating. Expanded the lookup with 30+ alias entries + a "Ready for X → gating" prefix rule in fp_resolve_step_kind().
5 The library was missing the canonical names a fresh estimator would type from scratch (Soak Clean, Rinse, Etch, Acid Dip, Desmut, Zincate, Drying, Inspection, Shipping, Water Break Test). The ENP-ALUM-BASIC seed included only the names from that one recipe. Migration adds 13 canonical missing entries (Soak Clean, Electroclean, Rinse, Etch, Desmut, Zincate, Acid Dip, HCl Activation, Water Break Test, Drying, Inspection, Final Inspection, Shipping, Contract Review).
6 _seed_minimal_library (the fresh-DB fallback path) had only 15 entries, didn't include Contract Review, and used English names that don't match the 30+ aliases. Added "Contract Review" as the first entry. Library is now bigger, but fp_resolve_step_kind() is the canonical way authors will get coverage.
7 DEFAULT_INPUTS_BY_KIND in fp_step_template.py still had free-text target_unit values ('HH:MM', '°F', 'sec', 'in', 'each') left over from before the 19.0.12.1.0 UoM cleanup. action_seed_default_inputs() blew up with Wrong value for target_unit: 'HH:MM' when called against the new Selection-typed column. Translated to selection keys: 'sec' → 's', '°F' → 'f', 'in' → 'in', 'each' → 'each', 'min' → 'min'. Format-only strings ('HH:MM') dropped — they're not units.

The walkthrough script is checked into context at /tmp/fp_recipe_walkthrough.py (rerun via odoo shell) and is the recommended smoke test before any future library / step-template changes ship.


Record Inputs Wizard — ad-hoc rows (shipped 2026-04-28)

The backend Record Inputs button on the job-form Steps tab opened an empty wizard when the recipe step had no step_input prompts authored — operator had no way to log anything. Fixed by:

  • Making node_input_id optional on fp.job.step.input.wizard.line. Authored prompts still show pre-filled + readonly; ad-hoc rows are fully editable (operator types the prompt label + value).
  • View now shows a helpful empty-state hint and an Add a line button.
  • Commit step requires every ad-hoc row to have a Prompt label, then serialises it into value_text of the resulting fp.job.step.move.input.value (format Prompt: value [unit]) so the chronological CoC report still renders the captured data.

Files: fusion_plating_jobs/wizards/fp_job_step_input_wizard.py + fp_job_step_input_wizard_views.xml.


Battle Tests — Real-World Operator Scenario Coverage

Persona-driven shop-floor scenarios that surfaced bugs / workflow holes. Every scenario has:

  • A test script in fusion_plating_quality/scripts/bt_s*.py you can re-run end-to-end on entech (or any DB)
  • A fix shipped at a specific module version
  • A description of how a real operator would trip the gap and what the system now does

How to re-run any scenario

# From a fresh shell, point at the entech DB:
ssh pve-worker5 "pct exec 111 -- bash -c 'echo \"exec(open(\\\"/mnt/extra-addons/custom/fusion_plating_quality/scripts/bt_sN_NAME.py\\\").read())\" | su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\"'"

Each script is self-contained — builds a fresh SO + job, walks the scenario, asserts the fix is in effect.

Scenario index

ID Persona / Scenario Gap before Fix shipped Module version Test script
S1 Carlos forgot to click Start; realizes 2h later date_started readonly + no way to back-date action_recompute_duration_from_timelogs on fp.job.step re-sums after timelog edits fusion_plating_jobs 19.0.6.10.0 bt_s2_* (covered with S2)
S2 Carlos finished step physically; forgot Finish; went home (12h ghost) Same as S1 Same fix — supervisor edits the timelog row, clicks Recompute Duration fusion_plating_jobs 19.0.6.10.0 battle_test_v2.py Fix 4
S3 Two operators tap Start on same step ✓ already blocked correctly n/a battle_test.py
S4 Out-of-order step finish (intentional for parallel tanks) Allowed by design (parallel work). Use S14 for opt-in serial n/a battle_test.py
S5 Manager takes over a stuck step (operator on vacation) ✓ reassign + finish work — added audit in S9 See S9 battle_test.py
S6 Bake window expired; operator wants to start anyway Silently allowed → no audit action_start_bake blocks missed_window; manager-only action_force_start_missed overrides + posts chatter audit fusion_plating_shopfloor 19.0.24.1.0 battle_test_v2.py Fix 1
S7 Step ran 12× expected duration Silent Chatter warning posted on the job at 1.5×+ overrun fusion_plating_jobs 19.0.6.10.0 battle_test_v2.py Fix 2
S8 Job closed with qty_done=0 despite qty=5 Silent — invoiced for parts that may not exist button_mark_done blocks until qty_done + qty_scrapped == qty. Manager bypass fp_skip_qty_reconcile=True fusion_plating_jobs 19.0.6.10.0 battle_test_v2.py Fix 3
S9 Bob takes over Carlos's in_progress step Silent reassignment (only step's own chatter logged) write() override on fp.job.step posts to JOB chatter when assigned_user_id changes on active state fusion_plating_jobs 19.0.6.11.0 bt_s9_reassign.py
S10 Operator paused for lunch, never resumed → 14 stale-paused steps in prod No alert / cron / activity Daily cron _cron_nudge_stale_paused (24h threshold) — schedules mail.activity on parent job for the manager. Idempotent fusion_plating_jobs 19.0.6.12.0 bt_s10_stale_paused.py
S11 Rectifier dies mid-plating → operator has no abort+retry path Only options: cancel (kills step) or pause+writetank+start (no audit) New action_abort_for_retry(reason, new_tank_id) — closes timelog, swaps tank, posts chatter, resets to ready fusion_plating_jobs 19.0.6.13.0 bt_s11_verify.py
S12 Sarah edits SO line qty 5→8 mid-job Silent — Carlos plates 5, invoice ships 8 sale.order.line.write posts warning to job chatter; new action_sync_qty_from_so button on job for explicit propagation fusion_plating_jobs 19.0.6.14.0 bt_s12_verify.py
S13 Recipe author wrote detailed step instructions; operator never sees them on tablet Tablet payload omitted instructions/thickness_target/dwell_time_minutes/bake_setpoint_temp/requires_signoff All 5 fields added to /fp/shopfloor/scan response AND _step_payload for tablet_overview fusion_plating_shopfloor 19.0.24.2.0 bt_s13_verify.py
S14 No way to enforce serial-required steps (e.g. acid etch → plating) Out-of-order start always allowed New requires_predecessor_done Boolean on fusion.plating.process.node → related on fp.job.stepbutton_start blocks if any earlier-sequence step isn't done/skipped/cancelled. Manager bypass fp_skip_predecessor_check=True fusion_plating 19.0.9.2.0, fusion_plating_jobs 19.0.6.15.0 bt_s14_verify.py
S15 Job marked done but bake.window still awaiting_bake Compliance bomb — parts ship without bake record button_mark_done blocks if any linked fusion.plating.bake.window is awaiting_bake or bake_in_progress. Manager bypass fp_skip_bake_gate=True for documented customer deviation fusion_plating_jobs 19.0.6.16.0 bt_s15_bake_close.py
S16 45 phantom in_progress steps in DB (operator clocked Start, never moved) No alert / cron / activity Hourly cron _cron_nudge_stale_in_progress (8h threshold) — sister to S10 cron fusion_plating_jobs 19.0.6.17.0 bt_s16_phantom_inprogress.py
S17 Operator drops parts, bumps qty_scrapped 0→2 Silent — no AS9100 disposition record fp.job.write hook auto-spawns fusion.plating.quality.hold for the scrap delta. Operator updates description with cause fusion_plating_jobs 19.0.6.18.0 bt_s17_scrap_ncr.py
S18 CoC issuance broken in 4 places — operator can't actually email a cert (a) auto-spawn left every useful field blank → Issue blocked on missing spec_reference; (b) Issue button never generated PDF → attachment_id stayed empty; (c) Send to Customer opened email composer with no attachment; (d) auto-spawn had no idempotency → dupes on button_mark_done retry _fp_create_certificates now pre-fills spec_reference (from coating), part_number, quantity_shipped (qty scrap), po_number, customer_job_no, process_description, entech_wo_number, sale_order_id. Idempotency check skips dupes. action_issue now renders the EN CoC PDF via new _fp_render_and_attach_pdf and sets attachment_id so Send to Customer attaches it automatically. Smart button "Certificates" already on the job form (visible when count > 0) so Tom finds the cert from the job he just closed fusion_plating_certificates 19.0.5.1.0, fusion_plating_jobs 19.0.6.19.0 bt_s18_cert_flow.py
S19 Lisa uploads Fischerscope X-Ray thickness PDF to QC; CoC ships without it as page 2 — and even after the back-end merge worked, operators couldn't see in the cert form whether the merge would happen Existing merge logic lived in uninstalled fusion_plating_bridge_mrp (keyed off mrp.production — gone with Sub 11). Post-Sub-11 cert path rendered CoC only; Fischerscope PDF stayed orphaned on the QC record. Even after Phase 1 fix shipped, the cert form had zero indicator that a thickness PDF was on file or had been merged → user reported "I did not see anything in the certification issue" Phase 1 (back-end merge): Ported merge to fp.certificate._fp_merge_thickness_into_pdf. New _fp_render_and_attach_pdf wraps cert PDF generation: renders the CoC via QWeb, then looks up the linked fusion.plating.quality.check (x_fc_job_id → fp.job → QC), finds the most recent passed QC with thickness_report_pdf_id, merges via pypdf.PdfWriter.append() (PyPDF2 PdfMerger fallback), posts chatter audit Fischerscope thickness report from QC <name> appended to CoC PDF.. Hooked into action_issue so the multi-page PDF lands on attachment_id automatically. Phase 2 (UI surface): Added 3 computed fields on fp.certificate (in fusion_plating_jobs): x_fc_thickness_qc_id (linked QC), x_fc_thickness_pdf_id (Fischerscope PDF), x_fc_thickness_status (none / pending / merged). Cert form now shows: (1) coloured banner above the title — blue "Will Append on Issue" / green "Merged" / amber "No PDF — operator action required"; (2) two new smart buttons (Plating Job, Fischerscope status); (3) new "Thickness Report (Fischerscope)" notebook tab with clickable PDF preview + step-by-step instructions when none uploaded fusion_plating_certificates 19.0.5.2.0, fusion_plating_jobs 19.0.6.20.0 bt_s19_fischer_merge.py (asserts both pre-Issue pending + post-Issue merged status flips)
S20 Tablet Station UX hardening — three real-world UX gaps surfaced during a persona walk on the Tablet + Manager Desk client actions (a) Scrap reason dropped: /fp/shopfloor/bump_qty_scrapped accepted operator's typed reason via window.prompt, passed it through context as fp_scrap_reason — but fp.job.write never read it, so the auto-spawned Hold's description had the generic "OPERATOR: replace this text with the actual reason" placeholder instead of what Carlos typed. Audit trail lost what just happened on the floor. (b) KPI/panel mismatch: tablet KPI strip showed plant-wide totals ("Quality Holds: 12") but the Holds panel below was scoped to the operator's own jobs (might show 0). Operator stares at a big red 12, scrolls down, sees nothing — confused/distrustful. (c) UserError stack-trace leak: when start_wo hit an S14 predecessor lock (or any other button_start-side guard), the raw UserError propagated through the JSON-RPC handler and operator got a Python stack-trace dialog instead of the nice setMessage("...", "danger") flash. Same hole on stop_wo, start_bake, end_bake, mark_gate, bump_qty_done, bump_qty_scrapped. (a) fp.job.write now reads self.env.context.get('fp_scrap_reason') and prepends Operator reason: <text> to the Hold description so the audit row captures what the operator actually typed. (b) Tablet KPI strip now reuses my_job_ids_for_kpi (the operator's own steps) for awaiting_bakes, bake_in_progress, missed, open_holds — same scope as the panels below, so the strip never lies. Manager dashboard keeps its own plant-wide KPI set. (c) Wrapped every action endpoint in try: ... except UserError as e: return {'ok': False, 'error': str(e.args[0])} — operator now gets the clean setMessage flash with the real guard text ("Step 'X' requires predecessors done first…") instead of a stack-trace popup. fusion_plating_jobs 19.0.6.22.0, fusion_plating_shopfloor 19.0.24.4.0 persona walk via sim_tablet_actions.py + sim_reverify.py (asserts: typed reason ends up in hold.description, KPI=panel for holds, start_wo returns {ok:False, error:"..."} for locked step)
S20 Tablet usability pass — operators were squinting at the tablet, scanning back-and-forth between recipe binders and the screen because the tablet showed step names but no targets, no live timer, no predecessor visibility. QC fail left parts in limbo with no Hold record. Manager Desk showed feel-good KPIs but hid the compliance bombs (missed bakes, stale steps, locked steps, holds, pending QC missing PDF) Tablet My Queue rows had no instructions, thickness_target, dwell_time_minutes, bake_setpoint_temp, requires_signoff — operators kept scanning the QR code just to read the bake temperature. Steps with requires_predecessor_done=True (S14) showed a green Start that always failed with a UserError. Active step "duration" was a stale number that only refreshed every 30s. Holds and bake windows showed plant-wide noise from other crews. No banner alerted Carlos when his job had a pending QC (Lisa was not called → QC sat for hours). No way to bump qty_done or scrap from the tablet → S17 hold auto-spawn never fired because operators didn't update the field. action_fail on QC marked the check failed but spawned no Hold — AS9100 disposition trail broken. Manager Desk KPIs were missing 7 compliance metrics: stale paused/in-progress steps (cron data), missed bake windows, open holds, predecessor-locked steps, pending QCs, QCs missing Fischerscope PDF, draft cert pipeline Carlos's Shopfloor Tablet — every queue row now carries the recipe-author fields (instructions snippet, thickness target chip, dwell-time chip, bake-temp chip, sign-off badge) so operators read the targets inline. Predecessor-blocked steps render with a 🔒 lock icon, an "Awaiting [step name]" notice, and a disabled Locked button (no more Start-then-fail). Active step now shows a live ticking HH:MM:SS clock (1s interval, computed from date_started_iso JS-side; flips to red on >1.5× planned duration) plus +1 Done and Scrap buttons that hit two new endpoints (/fp/shopfloor/bump_qty_done, /fp/shopfloor/bump_qty_scrapped — scrap prompts for reason and S17 auto-spawns the Hold). New Pending QC banner lists open QCs for my jobs with line-progress + Fischerscope-PDF status badge, and a tap deep-links into Lisa's mobile QC checklist. Holds and bake windows are now scoped to my jobs first (fall back to facility-wide for managers). QC checklistaction_fail now auto-creates a fusion.plating.quality.hold with hold_reason='qc_failure' (new selection value), populated description listing the failed checks, idempotent on retry. Manager Desk — 7 new clickable compliance KPI tiles: Missed Bakes (S15), Open Holds (S17 + QC fail), Stale Steps (S10/S16 cron data), Locked Steps (S14), Pending QC + "X need PDF" (S19 + missing-Fischerscope), Draft Certs + "Y today" (cert pipeline). Each tile drills into a list filtered to the relevant exception fusion_plating_shopfloor 19.0.24.3.0, fusion_plating_quality 19.0.4.8.0 sim_tablet_walk.py, sim_timer_pred_test.py, sim_qc_fail_hold.py, sim_manager_qc_fail.py (one-off persona walkthroughs)

Manager-bypass context flags

When you need to override a guard (documented customer deviation, emergency rework, etc.), set the context key on the call. All bypasses post to chatter with the user name for audit:

Flag Skips
fp_skip_step_gate=True step-completion check on button_mark_done (S5/S8 era)
fp_skip_qc_gate=True QC checklist requirement on button_mark_done
fp_skip_qty_reconcile=True qty_done + qty_scrapped == qty check on button_mark_done
fp_skip_bake_gate=True bake.window pending check on button_mark_done (S15)
fp_skip_predecessor_check=True requires_predecessor_done check on button_start (S14)
fp_skip_missed_window=True missed_window block on bake.window.action_start_bake (S6)

Daily / hourly crons added by battle tests

Cron Schedule What it does
Fusion Plating: Nudge stale paused steps daily 24h threshold, schedules activity on job for stale paused steps
Fusion Plating: Nudge stale in-progress steps hourly 8h threshold, sister cron for in_progress (phantom-time guard)
Fusion Plating: Update Bake Window states every 5 min (pre-existing) flips awaiting_bake → missed_window past required_by

Open scenarios — flagged for next session

  • S21 — Operator clocks two steps simultaneously across different jobs (multi-tasking conflict)
  • S22 — Bath chemistry drift mid-step — operator measures bath while plating, value out of spec; no alert on the step
  • S23 — Wrong recipe attached — Carlos sees mismatch with the part he's holding; recovery path?
  • S24 — Customer orders 100 parts spread across 3 jobs; one job's recipe gets edited — does it propagate to siblings?
  • S25 — Hold-aging cron + 3-day escalation (flagged in original audit, not yet built)
  • S26 — Calibration + permit-expiry cron (flagged in original audit, not yet built)
  • S27 — FAIR detection on first-shipment to a new customer/part combo (flagged in original audit, not yet built)

Tablet UI / persona-coverage gaps (S20 audit follow-ups)

The S20 walkthrough mapped 6 OWL apps (fp_shopfloor_tablet, fp_plant_overview, fp_process_tree, fp_manager_dashboard, fp_qc_checklist, fp_quality_dashboard) and surfaced these missing pieces. Each is a separate scenario for a future session:

  • S28 — Bake Oven Operator dedicated tablet. HE-bake operators currently work from Carlos's tablet (Bake Windows panel). Real shops have a separate oven station; needs: oven-scoped queue (ovens they're certified on), countdown to bake_required_by, one-tap Start/End, photo of chart recorder, daily history. /fp/shopfloor/start_bake + end_bake already exist — only a focused OWL action + menu item needed.
  • S29 — Tank-side chemistry logger. /fp/shopfloor/log_chemistry endpoint exists in shopfloor controller but has no UI calling it. Plating tech walks the line, takes Hull Cell + concentration readings, has nowhere to log them on the tablet. Needs a "Log Bath Reading" action that hits the existing endpoint.
  • S30 — Receiving dock tablet. Mike works from desktop list/form. A tablet-friendly view at the dock would let him scan PO QR → counted → staged → closed without typing. Existing fp.receiving state machine + actions are tablet-ready; only OWL view missing.
  • S31 — Maintenance technician mobile work-orders. fusion_plating_bridge_maintenance shows kanban / list views but no tablet UI. Maintenance walks to broken equipment with a phone — needs "My Open Work Orders" mobile view with photo + start/finish + parts checkout.
  • S32 — Shipping/Logistics tablet. Tom uses cert form + delivery list. A "Today's Shipments" tablet would let him scan job QR → pull cert → mark delivered. The cert PDF + Send-to-Customer flow is already built in S18 — only a packaging/dispatch view is missing.
  • S33 — Operator landing page after clock-in. When Carlos clocks in, the system has no "where do I go?" prompt. Should auto-route to Tablet Station with their station pre-paired (currently relies on manual scan or last-localStorage value).

Where the test scripts live

K:/Github/Odoo-Modules/fusion_plating/fusion_plating_quality/scripts/

  • battle_test.py — original S1S8 (mixed, some not-bug scenarios)
  • battle_test_v2.py — re-verify of S6/S7/S8/S2 fixes
  • bt_s9_reassign.py through bt_s17_scrap_ncr.py — one script per scenario
  • bt_s18_cert_flow.py — full CSR→operator→QC→shipper cert issuance + Send to Customer
  • bt_s19_fischer_merge.py — uploads fake Fischerscope PDF to QC, asserts CoC + thickness merged into 2-page output
  • step_internal_full.py — full pause/resume/skip/bake-spawn walk

To re-test the whole battle suite after a future change, run each bt_s*.py in sequence and confirm green.


Sub 12a / 12b / 12c — Simple Recipe Editor + Tablet Move/Rack/Timer + Reports (shipped 2026-04-27/28)

Three sequential sub-projects implementing Steelhead-replacement features for clients who prefer a simpler UX over the existing tree editor. All shipped on entech.

Spec: docs/superpowers/specs/2026-04-27-sub12-simple-recipe-editor-design.md (full design) Steelhead screen inventory: docs/superpowers/specs/2026-04-27-simple-recipe-editor-steelhead-screens.md (24 screens)

Sub 12a — Simple Recipe Editor + Step Library (versions: fusion_plating 19.0.10.0.0)

New models:

  • fp.step.template — reusable step library; tank_ids, target ranges (time/temp/voltage/viscosity), default_kind selection (15 kinds), input_template_ids + transition_input_ids, _seed_default_inputs() helper.
  • fp.step.template.input — operation-measurement definitions (during step). 11 input_types: text, number, boolean, selection, date, signature, time_hms, time_seconds, temperature, thickness, pass_fail.
  • fp.step.template.transition.input — compliance prompts fired on move-out. 9 input_types incl. photo, location_picker, customer_wo. compliance_tag selection (none/as9100/nadcap/cgp/nuclear).

Additive fields on fusion.plating.process.node (zero impact on tree editor):

  • is_template Boolean (recipe-level — appears in Import Starter dropdown).
  • source_template_id M2O fp.step.template (snapshot trace; no live coupling).
  • tank_ids M2M to fusion.plating.tank (via new join table fp_node_tank_rel).
  • material_callout, time_min/max_target, time_unit, temp_min/max_target, temp_unit, voltage_target, viscosity_target.
  • requires_rack_assignment, requires_transition_form, default_kind, preferred_editor (tree/simple/auto).

Additive fields on fusion.plating.process.node.input:

  • kind Selection (step_input / transition_input, default step_input).
  • target_min, target_max, target_unit, compliance_tag.
  • 9 new typed input_type values appended (existing values preserved).

Settings: res.company.x_fc_default_recipe_editor (tree/simple).

OWL client action: fp_simple_recipe_editor — flat 2-pane drag-drop layout. Library on right, Selected on left. HTML5 drag-drop with two distinct dataTransfer types (application/x-fp-step vs application/x-fp-library) so the drop handler knows whether to reorder or snapshot-copy. Drop-position simulator (commit 3098fcf): green dashed reservation line snaps above/below each row based on cursor Y vs row midpoint, with ghost-preview chip showing dragged step's icon + name. 80ms transition glides between slots.

11 JSONRPC routes under /fp/simple_recipe/...:

  • load, library/{list,create,write,delete}, step/{insert,write,remove,reorder}, template/{list,import}.
  • Library + template imports SNAPSHOT-COPY fields (Q4 = A locked) — editing a library template later does NOT mutate recipes already built.
  • library/delete is soft when any node references the template via source_template_id.

Recipe form integration: 2 header buttons (Open Tree Editor / Open Simple Editor), is_template + preferred_editor fields, new "Step Authoring" notebook page for step/operation nodes.

_resolve_preferred_editor() + action_open_recipe_with_preferred_editor() — per-recipe preferred_editor wins; auto falls back to company default; final fallback tree.

Menu: Plating → Configuration → Step Library (later moved to Configuration → Recipes & Steps in Phase 2).

post_init_hook: backfills kind='step_input' on existing process.node.input rows; seeds 1318 starter library templates from ENP-ALUM-BASIC recipe (idempotent — won't re-seed).

Naming gotcha: _seed_default_inputs was originally underscore-prefixed which Odoo 19 rejects when called from a view button — renamed to action_seed_default_inputs (commit 5494684). Public name required for any method called from XML buttons.

Sub 12b — Move Parts / Move Rack / Rack Parts / Stop Timer dialogs (versions: fusion_plating 19.0.10.1.0, fusion_plating_shopfloor 19.0.25.0.0)

Decisions adjusted from the original spec:

  • fusion.plating.rack already existed (wear-tracking model with state selection). Sub 12b adds an ORTHOGONAL racking_state field for the load lifecycle. The two states coexist — a rack can be wear-active AND racking-loaded simultaneously.
  • fp.labor.timer was NOT created. Instead, the existing fp.job.step.timelog (used by S1/S2 battle tests) is extended with a state machine. Single source of truth for labor; preserves S1/S2 paths.
  • fp.job.step.rack_id already existed and is reused as the "current rack on this step" pointer (no new current_rack_id).

New models:

  • fp.rack.tag — M2M tag registry (Rush / Hold for QC / Damaged / Customer Sample seeded by post_init_hook).
  • fp.job.step.move — chain-of-custody log, one row per Move Parts/Rack commit. FP/MOVE/YYYY/NNNN sequence. Carries from/to step + tank, transfer_type (step/hold/scrap/rework/split/return), qty_moved, to_location, photo_evidence_id, customer_wo_count, rack_id, moved_by_user_id.
  • fp.job.step.move.input.value — captured transition prompt values per move. Typed dispatch on input_type → correct value_text/number/boolean/date/attachment column.

Extended fusion.plating.rack:

  • racking_state (empty/loading/loaded/in_use/awaiting_unrack/out_of_service) — orthogonal to existing wear state.
  • tag_ids M2M, capacity_count (soft warn), notes.
  • current_job_step_id, current_tank_id, current_part_count (computes that walk fp.job.step.move history).

Extended fp.job.step:

  • requires_rack_assignment, requires_transition_form (related from recipe_node_id).
  • move_ids (O2M from_step_id), incoming_move_ids (O2M to_step_id).
  • is_racked (compute, stored, depends rack_id) — drives tablet rack-vs-parts greyed-button guard.
  • qty_at_step_start, qty_at_step_finish.

Extended fp.job: qty_received, qty_visual_inspection_rejects, qty_rework, special_requirements, active_timer_ids (filtered O2M), move_ids.

Extended fp.job.step.timelog with persistent state machine:

  • state Selection (running/paused/stopped/reconciled, default running — preserves S1/S2).
  • last_paused_at, total_paused_seconds, accrued_seconds (compute).
  • billed_hrs/min/sec, billed_total_seconds, billed_pct (compute).
  • product_id (split-by-product reconciliation), notes.
  • job_id (related, indexed) for fast O2M from fp.job.active_timer_ids.

12 tablet controller endpoints in fusion_plating_shopfloor/controllers/move_controller.py:

  • Move Parts: /preview, /commit
  • Move Rack: /preview, /commit
  • Rack Parts: /commit
  • Rack picker: /rack/list_empty, /rack/scan_qr
  • Persistent labor timer: /labor_timer/{start,pause,resume,stop,reconcile}

Manager-bypass context flags (consistent with existing fp_skip_* protocol): fp_skip_predecessor_check, fp_skip_rack_assignment, fp_skip_transition_form. All bypasses post to chatter on the move record naming the user + which flags fired. Manager group check enforced.

_safe() wrapper: UserError → JSONRPC-friendly {ok: False, error: msg} so OWL components show a flash without crashing.

4 OWL dialogs (in fusion_plating_shopfloor/static/src/js/):

  • move_parts_dialog.js — mirror of Steelhead screens 1-3, 14-15. System-derived top section (Part Count / From Node / To Node / Transfer Type / To Station / To Location with camera button). Compliance Prompts section renders authored transition_input_ids. Blockers section (NEW pattern, our improvement over Steelhead): each blocker has inline Resolve button. Soft (amber + button enabled) vs hard (amber + button disabled with tooltip listing reasons). MOVE button greys out when blocked.
  • move_rack_dialog.js — atomic multi-batch move. Rack name in title, tag chips, batches list, Type + To Node + To Station picker.
  • rack_parts_dialog.js — searchable empty-rack picker, QR Scan input, Unit + Amount fields. Save / Save+Print (the latter opens /report/pdf/fusion_plating_reports.action_report_fp_rack_travel/<id> — gap closed in Sub 12c+ commit 7d3b8f1).
  • stop_timer_dialog.js — opens with state already at stopped (server flips on load), pre-fills billed_* from accrued. Cancel / Save / Save & Start New Timer (chains into a fresh timer for the same step).

Custom event protocol: fp-resolve-rack window CustomEvent fired from Move Parts dialog when operator clicks Resolve on a rack-required blocker → tablet listens → spawns Rack Parts sub-dialog inline. Cleanup on unmount.

Shopfloor tablet (shopfloor_tablet.js): wired Move Parts + Stop Timer button handlers; dialog service injected; rack-resolve event listener with cleanup on onWillUnmount.

Plant overview (plant_overview.js + XML): new top "Racks" pane shows racks in (loaded/in_use/awaiting_unrack) state with tag chips, current_part_count, breadcrumb (current node + tank code), MOVE RACK button per row. Backend /fp/shopfloor/plant_overview extended to return racks array alongside the existing parts/batches.

Operator UX rule: fp.job.step.is_racked drives the tablet's MOVE PARTS button grey-out. Operator MUST go through MOVE RACK when batch is racked — enforced by disabled button state, not error message.

post_init_hook: seeds 4 starter rack tags (idempotent).

Deploy gotcha: to_step_id was originally required=True, ondelete='set null' — Odoo 19 disallows that combination. Switched to ondelete='restrict' (commit e718a47). Audit-safety bonus: destination steps can't be unlinked while move-log rows reference them.

Sub 12c — Reports + Labor History screen (versions: fusion_plating 19.0.10.2.0, fusion_plating_jobs 19.0.7.0.0, fusion_plating_reports 19.0.10.0.0, fusion_plating_certificates 19.0.5.3.0)

Re-scoped from the original 18-task plan to 5 tasks after auditing existing artifacts: report_coc_en / report_coc_fr already had Nadcap / AS9100 / CGP infrastructure built into fusion_plating_reports. company.x_fc_nadcap_logo etc. already existed.

Operator Traveller v2 (fusion_plating_jobs/report/report_fp_job_traveller.xml):

  • A4 landscape paper-style (matching Amphenol screens 16-18), replaces the minimal portrait template.
  • Header: company logo + Code 128 barcode + WO# + Date In + Due Date + Type + Order# + PO# + WO-Generated-By + customer block.
  • Item Information: Part# / Rev / Mat / Catg / S/N + Item-Name + Qty Rec / VIS INSP / Rework / Special Requirements / Stamp-Date.
  • Process-Sheet header: recipe name + category + spec/info.
  • Routing table (11 cols): Step / Tank / Operation+Actuals / Instruction / Unit / Material / Voltage / Time(min) / Temp / Stamp / Date.
  • Targets pulled from recipe-node fields when present (Sub 12a authored), 'N/A' otherwise.
  • Defensive QWeb — every cross-module field guarded via 'X in record._fields'.
  • New paperformat paperformat_fp_traveller_landscape.

Chronological CoC body (fusion_plating_reports/report/report_coc_chronological.xml):

  • New coc_body_chronological template walks fp.job.step.move records ordered by move_datetime.
  • Per-move heading <step.name> (<tank.code>) + "Moved By / Time / Qty" meta line.
  • 5-column measurement sub-table (Name / Description / Target / Actual / Recorded By) when destination step has captured inputs OR move has captured transition_input_value_ids.
  • Actual column (gap-fix commit 7d3b8f1): builds captured_values_by_input dict from mv.transition_input_value_ids, renders typed values (text as-is, number with target unit, boolean as PASS/FAIL, datetime formatted, attachment placeholder).
  • New router template coc_body_router picks chronological vs classic body via fp.certificate.body_style field.
  • Both English + French CoC actions (report_coc_en, report_coc_fr) rerouted through the router. Existing certs default to classic so no regressions.

fp.certificate.body_style Selection (classic/chronological), default classic. Surfaced on cert form alongside certified_by_id.

Per-customer cert statement (gap-fix 7d3b8f1): 3-tier resolution.

  • res.partner.x_fc_cert_statement Text (per-customer override, surfaced on partner form under Cert + Document Routing block).
  • res.company.x_fc_default_cert_statement Text (company-level fallback).
  • Hardcoded AS9100 / ISO 9001 boilerplate as final fallback.

Rack Travel Ticket PDF (gap-fix 7d3b8f1) in fusion_plating_reports/report/report_fp_rack_travel.xml:

  • A5 landscape, 28pt rack name, Code 128 barcode of FP-RACK:<name>, tag chips, contained-batches table (qty / part number / WO / customer / current step).
  • Bound to fusion.plating.rack model — appears in the rack form's Print menu.
  • Closes Sub 12b's Save+Print 404 placeholder.

Labor History screen (fusion_plating/views/fp_job_step_timelog_views.xml):

  • Plating → Operations → Labor History (sequence 64).
  • List view colour-coded by state, with billed_pct progressbar.
  • 8 search filters (My Timers default, Today, Running, Paused, Pending Reconciliation, Reconciled) + Group-by Operator/Job/Date.
  • Form view: identity readonly, billed_hrs/min/sec editable for supervisors+ until state=reconciled. create=false (timers are runtime-produced via tablet).
  • ACL rows for fp.job.step.timelog: operator (rwc, no unlink), supervisor (rwc, no unlink), manager (full).

Other sub-12 era ergonomics shipped in this session

  • Tank model (commit cfe776b): code → "Tank Number", name → "Tank Name". Header buttons for state transitions (Mark Empty/Filled/In Use/Draining/Maintenance/Out of Service) with chatter audit logging.
  • Plating app default landing screen (commit cfe776b): menu_fp_root.actionaction_fp_sale_orders (later replaced by Phase 1 resolver server action).
  • WO label (commit cfe776b): SO smart-button "Plating Jobs" → "WO".
  • Drop-position simulator in Simple Recipe Editor (commit 3098fcf): green dashed reservation line + ghost chip showing exactly where the drop will land. Snaps above/below row midpoint based on cursor Y. 80ms transition.

Phase 1 / 2 / 3 — Menu reorganization (shipped 2026-04-28)

Customer feedback: "too many top-level menus" + "configuration is unorganized". Three-phase reshuffle reduces 17 top-levels to 6 (operator-visible), groups the flat 36-entry Configuration into 7 themed folders, and tightens role-based visibility.

Phase 1 — Top-level consolidation + landing-page resolver (fusion_plating 19.0.11.0.0, commit 0ad382e)

New top-level structure (manager view):

🏭 Plating  (action = landing resolver — see below)
├── 📊 KPIs                            [seq 85, supervisor+]
├── 💰 Sales & Quoting                 (Sales + Configurator)
├── 🔧 Operations                      [seq 18]
│   ├── Process Recipes, Baths, Chemistry Logs, Tanks, Racks
│   ├── Replenishment Suggestions      [Phase 3: supervisor+]
│   ├── Maintenance                    [Phase 1: re-parented from top]
│   ├── Move Log                       [Phase 1+3: re-parented + supervisor+]
│   └── Labor History                  [Phase 1: re-parented from top]
├── 📦 Receiving & Shipping
├── ✅ Quality
│   └── Certificates                   [Phase 1: re-parented from top]
├── 📋 Compliance                      [seq 50, supervisor+]
│   ├── General                        ← was top-level Compliance
│   ├── Safety / WHMIS                 ← was top-level Safety
│   ├── Aerospace (AS9100 / Nadcap)    ← was top-level
│   ├── Nuclear (CSA N299 / CNSC)      ← was top-level
│   └── Controlled Goods (CGP)         ← was top-level
└── ⚙ Configuration                    [seq 90, manager-only]

Re-parented (no XML id changes — bookmarks still work):

  • fusion_plating_compliance.menu_fp_compliance_rootmenu_fp_compliance_hub (renamed 'General')
  • fusion_plating_safety.menu_fp_safety_rootmenu_fp_compliance_hub (renamed 'Safety / WHMIS')
  • fusion_plating_aerospace.menu_fp_aerospacemenu_fp_compliance_hub (renamed 'Aerospace (AS9100 / Nadcap)')
  • fusion_plating_nuclear.menu_fp_nuclearmenu_fp_compliance_hub (renamed 'Nuclear (CSA N299 / CNSC)')
  • fusion_plating_cgp.menu_fp_cgpmenu_fp_compliance_hub (renamed 'Controlled Goods (CGP)')
  • fusion_plating_certificates.menu_fp_certificatesfusion_plating_quality.menu_fp_quality
  • fusion_plating_bridge_maintenance.menu_fp_maintenancefusion_plating.menu_fp_operations
  • fusion_plating.menu_fp_job_step_move (Move Log) → menu_fp_operations
  • fusion_plating.menu_fp_job_step_timelog (Labor History) → menu_fp_operations

Landing-page resolver (fusion_plating/data/fp_landing_data.xml):

  • ir.actions.server named action_fp_resolve_plating_landing. Code in the action: user override → company default → Sale Orders fallback.
  • menu_fp_root rewired to call this server action.
  • New fields:
    • ir.actions.act_window.x_fc_pickable_landing — Boolean tag for curated picklist.
    • res.company.x_fc_default_landing_action_id — admin sets fallback.
    • res.users.x_fc_plating_landing_action_id — per-user override.
  • UI surfaces in fusion_plating/views/fp_landing_views.xml:
    • User Profile / Preferences → Fusion Plating tab (per-user dropdown).
    • Settings → Fusion Plating → Plating Landing Page block (company default).
  • fusion_plating_configurator's earlier menu_fp_root override (action_fp_sale_orders direct) was removed — core's resolver now owns the routing.
  • Pickable list is curated via inline <field name="x_fc_pickable_landing" eval="True"/> on action records — currently flagged: action_fp_sale_orders, action_fp_quotations, action_fp_process_recipe. Add more by tagging the relevant act_window record at its source.

Phase 2 — Configuration sub-folder grouping (fusion_plating 19.0.11.1.0, commits 3641b78 + 62c1315 + 4671541)

7 themed folders + Settings sibling:

⚙ Configuration  [manager-only]
├── ⚡ Settings                         (sequence 1, sibling)
├── 🏢 Shop Setup                       (10)
│   ├── Facilities, Production Lines, Routing Stations,
│   ├── Process Categories, Process Types,
│   └── Bake Ovens, Shopfloor Stations, Vehicles
├── 📜 Recipes & Steps                  (20)
│   └── Step Library, QC Checklist Templates, Quality Points
├── 🧪 Materials & Tanks                (30)
│   ├── Bath Parameters, Replenishment Rules, Chemicals,
│   └── Rack Tags, Calibration Equipment, Calibration Events
├── 👥 Workforce                        (40)
│   └── Operator Certifications, Shop Roles, Training Types, Quality Teams
├── 📝 Quality & Documents              (50)
│   ├── Customer Specs, Approved Vendor List,
│   ├── Quality Tags, Quality Reasons, Quality Stages, N299 Levels,
│   └── Notification Templates, Notification Log
├── 💵 Pricing & Billing                (60)
│   └── Invoice Strategy Defaults, Account Holds
└── 🔁 Reference Data                   (70)
    └── Value Sets, Value Rotations

The 7 bucket folders are defined in fusion_plating/views/fp_menu.xml. Touched 11 module XML files to re-parent existing children:

  • fusion_plating_invoicing → Pricing & Billing
  • fusion_plating_notifications → Quality & Documents
  • fusion_plating_safety → Workforce + Materials & Tanks
  • fusion_plating_shopfloor → Shop Setup
  • fusion_plating_logistics → Shop Setup (Vehicles)
  • fusion_plating_culture → Reference Data
  • fusion_plating_nuclear → Quality & Documents (N299 Levels)
  • fusion_plating_quality → Materials & Tanks (Calibration), Quality & Documents (Specs/AVL/Tags/Reasons/Stages), Workforce (Quality Teams), Recipes & Steps (Quality Points + QC Templates)

Critical load-order rule (caught by entech upgrade 62c1315 + 4671541):

  • Every parent menuitem MUST be defined before any child references it by xmlid. Odoo's data loader is strictly sequential — within a single XML file AND across the manifest's data list.
  • fp_menu.xml was reorganized so its declaration order is: Root → Configuration + 7 buckets → Compliance hub → Operations parent → all children.
  • The manifest's data list was reordered to load views/fp_menu.xml BEFORE any view file that references the bucket xmlids (e.g. fp_rack_tag_views.xml, downstream module views).
  • Lesson for future menu reshuffles: when adding a new bucket folder, define it in fp_menu.xml near the top, AND make sure that file loads early in the manifest data list.

Phase 3 — Tightened group-gating (fusion_plating 19.0.11.2.0, fusion_plating_kpi 19.0.1.1.0, commit 5f6c7af)

Three targeted gates so operators no longer see admin/audit views:

  • menu_fp_dashboard (KPIs) → groups="fusion_plating.group_fusion_plating_supervisor". Operators don't need dashboards.
  • menu_fp_job_step_move (Move Log) → supervisor+. Operators see their own moves on the tablet; this top-level menu is the audit-of-everyone-else view.
  • menu_fp_replenishment_suggestions → supervisor+. Purchasing decision, not operator concern.

Net effect by role:

Top-level Operator Supervisor Manager
Sales / Configurator ✓ (if estimator)
Shop Floor
Operations
Receiving & Shipping ✓ (if receiving)
Quality
KPIs
Compliance (hub)
Configuration

Operator now sees ~5 top-level menus instead of the previous ~10.

Production Line / Routing Station rename (commit afcd128, fusion_plating 19.0.11.3.0)

Two distinct entities were both labelled "Work Centre" / "Work Centers" — only the US/UK spelling differentiated them. Renamed by purpose:

Model Old display New display What it is
fusion.plating.work.center Work Centers Production Lines Physical shop-layout grouping that owns tanks. Has tank_ids, supported_process_ids, capacity_per_day.
fp.work.centre Work Centres Routing Stations Per-job-step routing entity (post-Sub-11 mrp.workcenter replacement). Has kind (wet_line/bake/mask/rack/inspect), cost_per_hour, default_bath_id, default_tank_id.

Conceptually a Production Line CONTAINS many Routing Stations.

Model IDs unchanged (12 + 9 cross-refs preserved). Updated: _description on both models, string= on name fields, list/form/search view strings, act_window names, menu items, doc comments.


Naming convention recap (Plating menu hierarchy as of 2026-04-28)

When adding a new menu, default to one of these 6 top-level homes:

  • Sales & Quoting — quote/order workflows, customers, parts catalog
  • Operations — recipes, baths, tanks, racks, jobs, move log, labor, maintenance
  • Receiving & Shipping — inbound/outbound logistics
  • Quality — holds, NCRs, CAPAs, certificates, FAIR, audits, doc control
  • Compliance (hub) — General / Safety / Aerospace / Nuclear / CGP
  • Configuration (manager-only) — Settings + 7 themed folders

Avoid creating a new TOP-LEVEL menu under menu_fp_root unless it's a genuinely new domain. Most new functionality belongs as a child of an existing top-level.

When adding a new admin config, drop it into the right Configuration folder:

  • Equipment / physical infrastructure → Shop Setup
  • Recipe authoring → Recipes & Steps
  • Chemicals, baths, calibration → Materials & Tanks
  • People, roles, training → Workforce
  • Specs, vendors, quality categorisation, customer notifications → Quality & Documents
  • Pricing rules, account holds → Pricing & Billing
  • Generic value lists → Reference Data

Don't add new top-level Configuration entries (siblings of the 7 folders) unless absolutely necessary — Settings is the only one allowed.