# 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` |
| **Page-break-inside: avoid placement** | When a long QWeb report dumps content into multi-page PDFs via wkhtmltopdf, the company header (rendered as `--header-html`) can overlap body content if a page break lands mid-row in a table. **Apply `page-break-inside: avoid` to `
` elements** (and to wrapper `
`s that wrap whole logical sections like signature blocks), not to `
`. On entech wkhtmltopdf, `
`-level `page-break-inside` is unreliable when the table is long enough to definitely break; per-row is honoured. Pattern: keep individual readings/rows together so the wkhtmltopdf header zone never overlaps mid-row content. Wrap the larger logical block (cert thickness section, signature + certification statement) in `
` to keep it together when it fits and naturally wrap to a fresh page when it doesn't. | `fusion_plating_reports/report/report_coc.xml` |
| **`opacity` + `italic` muted text renders jagged on entech wkhtmltopdf** | The obvious pattern for a subtle footnote — `font-style: italic; opacity: 0.7;` (used by `.fp-coc .small-label`) — produces washed-out, jagged characters that look "broken" or "messed up" on the printed PDF. Visually it reads as garbled text even though the source is clean. **Use solid grey (`color: #555`) at normal weight instead** for muted secondary text. Same workaround applies to any `opacity`-driven greyed-out element bound for wkhtmltopdf. The existing `.small-label` class still exists for legacy callers but new code should prefer an explicit `color:` style. | `fusion_plating_reports` |
| **wkhtmltopdf header overlap — paperformat.margin_top, NOT body padding-top** | The wkhtmltopdf header zone is sized by `report.paperformat.margin_top` (and `header_spacing`). If the `web.external_layout` header (logo + address etc.) renders ~28mm tall but paperformat reserves only 8mm, page 2+ has the header bleeding over body content (the overlap shows up as the company logo printed *on top of* the signature, readings table, etc.). The anti-pattern is "fix" it by adding `padding-top: 50mm` to the body wrapper — this only pads page 1 (single one-shot padding) and does nothing for subsequent pages, while also wasting 50mm of usable space on page 1. **Right fix:** size `paperformat.margin_top` to the actual rendered header height, then drop body `padding-top` to a tiny visual gap (~5mm). Each report can have its own paperformat — `report_coc_en` / `report_coc_fr` use "Fusion Plating CoC" (id 13); the legacy `report_coc` uses "A4 Landscape (Fusion Plating)" (id 12). Update the right one and don't bleed changes across reports. | `fusion_plating_reports`, `report.paperformat` |
| **CoC + thickness = ONE cert (page 2 merge OR inline body)** | 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**. 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. **Two rendering paths exist for the thickness data in the CoC PDF:** (1) **Page-2 PDF merge** via `_fp_merge_thickness_into_pdf` — used when there's a real PDF source (operator uploaded a Fischerscope PDF, or QC has `thickness_report_pdf_id`). (2) **Inline readings table in the CoC body** — used when `thickness_reading_ids` is populated but there's no PDF source (e.g. RTF upload parsed to readings, manually typed readings). Lives in `report_coc.xml` between the parts table and the signature block, gated on `doc.thickness_reading_ids`. Both can coexist on a cert — PDF merges as page 2, readings render inline; usually only one path has data per cert. | `fusion_plating_jobs`, `fusion_plating_certificates`, `fusion_plating_reports` |
| **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 `
` without `` 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 |
| **Recordsets use `__slots__` — no transient attrs** | Odoo 19's `BaseModel` declares `__slots__ = ['env', '_ids', '_prefetch_ids']`, so `picking._my_stash = data` raises `AttributeError: 'stock.picking' object has no attribute '_my_stash'`. The error reads like a missing field but it's actually Python rejecting the assignment. Don't stash transient state on a recordset between method calls — pass it as a method arg, store on the caller's `self`, or use `env.context` for cross-frame plumbing. Caught here because `fp_receiving._fp_build_shipping_picking` tried to attach `_fp_outbound_packages` to the picking before handing off to `_fp_apply_shipping_result`; the catch-all `except Exception` swallowed it and surfaced the misleading "Carrier API call failed" wizard. | any code that wants to attach data to a recordset between calls |
| **labelary.com dependency for ZPL→PDF** | `fusion_plating_receiving` POSTs ZPL labels to `https://api.labelary.com/v1/printers/8dpmm/labels/4x6/0/` to get a PDF rasterization, so one FedEx ship call can populate both the PDF and ZPL smart buttons on the receiving form. **Privacy:** every outbound label's shipping address + tracking number leaves the network and hits labelary's servers (no payment data, but real customer info). **Operational:** anonymous tier is ~5 req/s; add an API key in the labelary helper if you ever ship more than that. PDF→ZPL is intentionally not attempted — that direction is impractical and FedEx's `/ship` endpoint only returns one format per shipment, so the carrier MUST be configured for ZPLII (not PDF) for the dual-format flow to work. Switching the carrier back to PDF will silently drop the ZPL button. | `fusion_plating_receiving/models/fp_receiving.py` (`_fp_apply_shipping_result`) |
| **FedEx ZPL ships with `^POI` — strip it** | FedEx's REST `/ship` endpoint returns ZPL with `^POI` (Print Orientation = Invert) baked in, which flips the label 180° on the printer. On a desktop direct-thermal like the Zebra ZD450 that prints upside-down for the operator, and labelary mirrors the inversion in the PDF preview. `_fp_apply_shipping_result` creates a `*-fixed.zpl` copy of the FedEx attachment with `^POI` removed and points the shipment + smart buttons at the cleaned copy; the original FedEx ZPL stays on the picking for audit. **Don't restore `^POI`** — both the PDF preview and the Zebra output need it stripped. If a future printer needs inverted orientation, configure the printer driver instead of putting `^POI` back. | `fusion_plating_receiving/models/fp_receiving.py` (`_fp_apply_shipping_result`) |
| **Per-shipment service override via `fp_service_type_override` context key** | Operator picks a FedEx service tier on `fp.receiving.x_fc_outbound_service_type` (Priority Overnight, 2Day, Ground, etc.). `action_generate_outbound_label` passes the chosen code through to `carrier.send_shipping` via `with_context(fp_service_type_override=…)`. `fusion_shipping.fusion_fedex_rest_send_shipping` reads the context key and overrides `srm.service_type` for that call only — carrier default is untouched. Empty/blank override falls back to `carrier.fedex_rest_service_type`. Only FedEx is wired up right now; mirroring this for Canada Post / UPS is a separate task. | `fusion_plating_receiving/models/fp_receiving.py` → `fusion_shipping/models/delivery_carrier.py` |
| **`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 `'` 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 `` so `-u ` does NOT reload them — users can edit templates in the UI and the module won't overwrite. To sync XML edits to existing records: temporarily flip the wrapper to ``, redeploy and `-u`, then revert (and `UPDATE ir_model_data SET noupdate=true ...` to restore protection). Alternatively, post-migration script or odoo shell write. (4) **`mail.template.report_name` was removed in Odoo 19** — the dynamic PDF-filename field now lives on `ir.actions.report.print_report_name` instead. Old `` entries in mail-template data files silently survive while protected by noupdate=1, but the moment you force-reload they error with `Invalid field 'report_name' in 'mail.template'`. Strip them or move the expression to the report action. | any code scripting `mail.template.body_html` |
| **`message_post(body=...)` HTML-escapes by default** | A plain `str` body with `` tags renders as literal `foo` text in chatter — operators see angle brackets, not bold. Wrap the template in `Markup(_('... %s ...'))` 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: %s')) % tracking)`. | any model posting HTML-formatted chatter |
| **OWL `t-out` escapes plain JS strings — wrap with `markup()`** | The JS-side analogue of the `message_post` markup gotcha. `t-out="state.html"` only renders unescaped HTML when the value is a `markup()`-tagged string from `@odoo/owl`; a plain string (e.g. straight off an RPC response) gets HTML-escaped and the user sees literal `
foo
` text. Caught here because `fp_record_inputs_dialog.js` was assigning `this.state.instructionsHtml = data.instructions_html` raw — recipe author's `
...
` rendered as visible tags in the operator dialog. **Fix:** `import { markup } from "@odoo/owl"` and wrap RPC-returned HTML: `this.state.html = markup(data.html || "")`. Same rule for any OWL component that ingests HTML from the server and pushes it through `t-out`. | any OWL component rendering server-returned HTML via `t-out` |
| **entech apt is broken — install new packages via `dpkg -i` bypass** | LXC 111's apt state has pre-existing breakage that blocks ANY `apt install`: `python3-lxml-html-clean` not installable on Bookworm but odoo's deb depends on it, `postgresql-15-pgvector` Breaks `postgresql-15-jit-llvm (< 19)`, `libglu1-mesa`/`libglx-mesa0` installed without their Mesa sub-deps (libopengl0, libdrm2, libxfixes3…), `postgresql-15` itself in `iF` half-configured state. Apt's global resolver refuses ALL installs until these are fixed. Workaround that worked for ImageMagick + libwmf: `apt-get download` the target debs into a tmp dir, then `dpkg -i *.deb` — dpkg only checks the direct deps of what you're installing, not the system-wide health. Use this pattern when entech needs new system packages; **don't try `apt --fix-broken install`** without coordinating with whoever owns the box — fixing pgvector/lxml-html-clean could cascade into Odoo or PostgreSQL changes. Installed this way: `imagemagick`, `imagemagick-6-common`, `imagemagick-6.q16`, `libmagickcore-6.q16-6`, `libmagickwand-6.q16-6`, `libwmf-0.2-7`, `libwmflite-0.2-7`, `libwmf-bin`, `libfftw3-double3`, `liblqr-1-0`, `hicolor-icon-theme` (2026-05-21, ~4 MB total). WMF→raster path: `wmf2svg input.wmf -o out.svg` writes a thin SVG referencing `out-N.png` side-files (libwmf unpacks raster blocks inside the metafile). ImageMagick's `convert` lacks the WMF delegate on Debian Bookworm — use wmf2svg for raster extraction, not `convert input.wmf out.png`. | any new system package install on entech LXC 111 |
| **Fischerscope XDAL 600 `.doc` files are actually RTF** | Helmut Fischer's XDAL 600 XRF software exports thickness reports with a `.doc` extension but the file contents are **RTF** (`{\\rtf1\\ansi…`), not Microsoft Word binary `.doc`. `file(1)` confirms: `Rich Text Format data, version 1`. python-docx will refuse to open it, and the filename-based dispatch (`endswith('.docx')`) silently skips parsing. **Don't reach for libreoffice/antiword.** Detect by **magic bytes** (`raw_bytes[:5] == b'{\\\\rtf'`) and route through `_fp_parse_fischerscope_rtf` instead — it strips RTF control words with regex and runs the same Fischerscope reading regex as the .docx path. The image data embedded as hex inside `{\\pict ...}` blocks must be stripped FIRST or the reading regex will choke on multi-MB image hex. | `fusion_plating_jobs/wizards/fp_cert_issue_wizard.py` |
| **entech apt — which conversion tools are available** | The host has pre-existing broken deps (`python3-lxml-html-clean` missing, `postgresql-15-pgvector` vs `postgresql-15-jit-llvm` conflict, various Mesa packages) that make new `apt install` calls fragile — they often abort partway through dep resolution. **Currently installed and usable:** `convert` (ImageMagick 6), `wmf2svg`, `wmf2eps` (libwmf-bin). **Not installed:** `libreoffice`, `unoconv`, `pandoc`, `wmf2png`. Don't assume the next `apt install` will go through — always run `which ` first and design the feature to soft-fail if the tool isn't there (see `_fp_extract_rtf_images` for the pattern: shell out, catch `FileNotFoundError`/`TimeoutExpired`, fall back to "no image" instead of crashing the cert flow). For WMF → PNG specifically: `wmf2svg` writes both SVG and a side-file `*-N.png` per embedded raster — use that, not `convert input.wmf` (no WMF delegate). For new tools: check pure-Python alternatives first (Pillow without backends, pypdf, openpyxl) before reaching for apt. | any feature wanting to convert docs/images server-side |
### 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 ``, NO `` wrapper for group-by filters. Use bare `` 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 `