# 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 `
` 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 | | **`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, 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 `` 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 | ### 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 `