Files
Odoo-Modules/fusion_plating/CLAUDE.md
gsinghpal 091f98e1f9 changes
2026-05-18 22:33:23 -04:00

1418 lines
128 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`](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`](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_id``company.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` |
| **CoC + thickness = ONE cert (page 2 merge)** | When a customer has both `x_fc_send_coc` and `x_fc_send_thickness_report` on (or part has `certificate_requirement='coc_thickness'`), `_resolve_required_cert_types` returns **`{'coc'}` only** — the thickness data is delivered as page 2 of the CoC PDF via `_fp_merge_thickness_into_pdf`, not as a separate `thickness_report` cert. Standalone `thickness_report` certs are only created when CoC is OFF and thickness is ON (rare). The earlier "two certs" behavior was a bug — don't restore it. | `fusion_plating_jobs`, `fusion_plating_certificates` |
| **Smart-button "create or view" pattern** | For a smart button that toggles between "create" and "view" states, use **one** idempotent button with `widget="statinfo"`, not two sibling buttons gated by mutually-exclusive `invisible` expressions. Custom `<div class="o_stat_info">` without `<span class="o_stat_value">` renders awkwardly in Odoo 19 (numbers + label expected); `statinfo` handles the standard structure automatically. The action method itself should branch on whether the linked record exists (create-then-open or just open). | any module with smart buttons |
| **stock.move.name removed** | Odoo 19 dropped the `name` field on `stock.move`. Passing `name` in a create dict raises `ValueError: Invalid field 'name' on model 'stock.move'`. Use `description_picking` instead (the operator-facing line label on the picking). The DB column is gone too — `name` doesn't exist as a stored field. | any code that builds stock.move records |
| **`mail.template.body_html` is `Markup` + jsonb** | Two gotchas: (1) `tpl.body_html` returns a `markupsafe.Markup` object. `Markup.replace(old, new)` *escapes both args* — quotes in `old` become `&#39;` so the literal pre-escape string never matches. **Cast to `str(tpl.body_html)` before calling `.replace`**. (2) The DB column is `jsonb` (translatable). Direct `UPDATE ... SET body_html = '...'` SQL fails with `invalid input syntax for type json`; either use ORM `tpl.write({'body_html': ...})` or wrap raw SQL with `jsonb_build_object('en_US', ...)`. (3) Mail-template XML data files typically use `<odoo noupdate="1">` so `-u <module>` does NOT reload them — users can edit templates in the UI and the module won't overwrite. To sync XML edits to existing records, write a one-shot post-migration or update via `odoo shell`. | any code scripting `mail.template.body_html` |
| **`message_post(body=...)` HTML-escapes by default** | A plain `str` body with `<b>` tags renders as literal `<b>foo</b>` text in chatter — operators see angle brackets, not bold. Wrap the template in `Markup(_('... <b>%s</b> ...'))` and use `%`/`format_map` for substitutions; markupsafe escapes the substituted values automatically so user input still can't inject HTML. Pattern: `self.message_post(body=Markup(_('Tracking: <b>%s</b>')) % tracking)`. | any model posting HTML-formatted chatter |
### 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.py``portal_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
```xml
<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`:
```xml
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:
```xml
<xpath expr="//div[hasclass('oe_button_box')]" position="inside">
<button .../>
</xpath>
```
`position="inside"` appends to the end of the button box.
### Action method shape
```python
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**:
```bash
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):
```bash
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):
```bash
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**:
```bash
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
```bash
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 function** — `mrp.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 macro** — `fusion_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 fields** — `x_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:
```bash
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:
```bash
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)
```sql
-- 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
```python
# 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.py` — **also 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
```bash
# 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.step` → `button_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 checklist** — `action_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](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](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.action` → `action_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_root` → `menu_fp_compliance_hub` (renamed 'General')
- `fusion_plating_safety.menu_fp_safety_root` → `menu_fp_compliance_hub` (renamed 'Safety / WHMIS')
- `fusion_plating_aerospace.menu_fp_aerospace` → `menu_fp_compliance_hub` (renamed 'Aerospace (AS9100 / Nadcap)')
- `fusion_plating_nuclear.menu_fp_nuclear` → `menu_fp_compliance_hub` (renamed 'Nuclear (CSA N299 / CNSC)')
- `fusion_plating_cgp.menu_fp_cgp` → `menu_fp_compliance_hub` (renamed 'Controlled Goods (CGP)')
- `fusion_plating_certificates.menu_fp_certificates` → `fusion_plating_quality.menu_fp_quality`
- `fusion_plating_bridge_maintenance.menu_fp_maintenance` → `fusion_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.