# 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 `` 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 `
` 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 `
` 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 ``` 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. - `` — REQUIRED for the count-on-top label-below format. Don't use `string="Foo"` on the `