diff --git a/fusion_plating/docs/superpowers/plans/2026-05-14-promote-customer-spec.md b/fusion_plating/docs/superpowers/plans/2026-05-14-promote-customer-spec.md new file mode 100644 index 00000000..9b16a8db --- /dev/null +++ b/fusion_plating/docs/superpowers/plans/2026-05-14-promote-customer-spec.md @@ -0,0 +1,2065 @@ +# Promote Customer Specification — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Retire `fp.coating.config` + `fp.treatment` entirely, promote `fusion.plating.customer.spec` to the primary specification entity, and add a two-picker SO line UX (Specification + Recipe). + +**Architecture:** Phased migration in 5 stages — additive first (new fields/models), then re-key consumers (SO line, pricing, quality, job, cert), then re-key views/reports/portal/tablet, then final destructive removal. Each phase ends in a clean module install + smoke test + commit. No historical data to migrate (dev stage). No back-compat shims. + +**Tech Stack:** Odoo 19, Python, OWL (frontend), SCSS, QWeb (reports), Docker (local dev), entech LXC 111 (deployment target). + +**Spec:** [docs/superpowers/specs/2026-05-14-promote-customer-spec-design.md](../specs/2026-05-14-promote-customer-spec-design.md) + +**Backup:** `backup/pre-spec-recipe-collapse-2026-05-14` on origin + gitea (commit `1414ef2`). + +--- + +## File Structure Map + +| Phase | Files Created | Files Modified | Files Deleted | +|---|---|---|---| +| **A** Foundation | `fusion_plating/models/fp_recipe_thickness.py`, `fusion_plating/views/fp_recipe_thickness_views.xml` | `fusion_plating/models/fp_process_node.py`, `fusion_plating/views/fp_process_node_views.xml`, `fusion_plating/models/__init__.py`, `fusion_plating/__manifest__.py`, `fusion_plating/security/ir.model.access.csv`, `fusion_plating_quality/models/fp_customer_spec.py`, `fusion_plating_quality/views/fp_customer_spec_views.xml`, `fusion_plating_quality/__manifest__.py` | — | +| **B** SO Line UX | — | `fusion_plating_configurator/models/sale_order_line.py`, `fusion_plating_configurator/models/account_move_line.py`, `fusion_plating_configurator/models/fp_part_catalog.py`, `fusion_plating_configurator/wizard/fp_direct_order_line.py`, `fusion_plating_configurator/views/sale_order_views.xml`, `fusion_plating_configurator/views/fp_part_catalog_views.xml`, `fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml`, `fusion_plating_configurator/__manifest__.py` | — | +| **C** Pricing/Quality/Job/Cert | — | `fusion_plating_configurator/models/fp_pricing_rule.py`, `fusion_plating_configurator/views/fp_pricing_rule_views.xml`, `fusion_plating_quality/models/fp_quality_point.py`, `fusion_plating_quality/models/fp_quality_point_hooks.py`, `fusion_plating_quality/views/fp_quality_point_views.xml`, `fusion_plating_jobs/models/fp_job.py`, `fusion_plating_jobs/models/sale_order.py`, `fusion_plating_jobs/views/fp_job_form_inherit.xml`, `fusion_plating_certificates/models/fp_certificate.py`, manifests | — | +| **D** Reports/Tablet/Portal | — | `fusion_plating_reports/report/report_fp_sale.xml`, `fusion_plating_reports/report/report_fp_wo_sticker.xml`, `fusion_plating_reports/report/report_fp_job_traveller.xml`, `fusion_plating_jobs/report/report_fp_job_traveller.xml`, `fusion_plating_jobs/report/report_fp_job_sticker.xml`, CoC EN/FR templates, `fusion_plating_shopfloor/controllers/shopfloor_controller.py`, `fusion_plating_portal/controllers/portal_configurator.py`, `fusion_plating_portal/views/fp_portal_configurator_templates.xml`, manifests | — | +| **E** Removal | — | `fusion_plating_configurator/models/__init__.py`, `fusion_plating_configurator/__manifest__.py`, `fusion_plating_configurator/security/ir.model.access.csv`, `fusion_plating_configurator/views/fp_configurator_menu.xml`, `fusion_plating/models/fp_job.py` (drop coating field), `fusion_plating_logistics/models/fp_delivery.py` (re-point thickness FK) | `fusion_plating_configurator/models/fp_coating_config.py`, `fusion_plating_configurator/models/fp_coating_thickness.py`, `fusion_plating_configurator/models/fp_treatment.py`, `fusion_plating_configurator/views/fp_coating_config_views.xml`, `fusion_plating_configurator/views/fp_coating_thickness_views.xml`, `fusion_plating_configurator/views/fp_treatment_views.xml`, `fusion_plating_configurator/data/fp_treatment_data.xml` | + +--- + +## Pre-Work (one-time) + +### Task 0: Verify clean working tree + create feature branch + +**Files:** N/A (git operations only) + +- [ ] **Step 1: Verify working tree is clean** + +Run: `cd /Users/gurpreet/Github/Odoo-Modules && git status` +Expected: `On branch main` + `nothing to commit, working tree clean` + +If dirty, stop and commit / stash uncommitted work before proceeding. + +- [ ] **Step 2: Verify backup branch exists** + +Run: `git branch -a | grep backup/pre-spec-recipe-collapse-2026-05-14` +Expected: 3 lines — local + origin + gitea entries + +- [ ] **Step 3: Create feature branch off main** + +Run: +```bash +git checkout -b feature/promote-customer-spec +git push -u origin feature/promote-customer-spec +git push -u gitea feature/promote-customer-spec +``` +Expected: branch created locally + pushed to both remotes. + +--- + +## Phase A — Recipe + Spec Foundation + +Goal: Add the new fields/models that ENABLE the new design. Nothing existing breaks; nothing existing is removed yet. Pure additive change. + +### Task A1: Create `fp.recipe.thickness` model + +**Files:** +- Create: `fusion_plating/models/fp_recipe_thickness.py` + +- [ ] **Step 1: Create the new model file** + +Write to `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating/models/fp_recipe_thickness.py`: + +```python +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +# Part of the Fusion Plating product family. + +from odoo import _, api, fields, models + + +class FpRecipeThickness(models.Model): + """Discrete thickness option offered for a recipe. + + Replaces fp.coating.thickness. The thickness picker on the SO line + is scoped to the chosen recipe, so the operator only sees values + that match what the recipe actually produces. + """ + _name = 'fp.recipe.thickness' + _description = 'Fusion Plating — Recipe Thickness Option' + _order = 'recipe_id, sequence, value' + + recipe_id = fields.Many2one( + 'fusion.plating.process.node', + string='Recipe', + required=True, + ondelete='cascade', + domain="[('node_type', '=', 'recipe'), ('parent_id', '=', False)]", + ) + sequence = fields.Integer(default=10) + value = fields.Float( + string='Thickness', + required=True, + digits=(10, 4), + ) + uom = fields.Selection( + [('mils', 'mils'), ('microns', 'microns'), ('inches', 'inches')], + string='UoM', + required=True, + default='mils', + ) + label = fields.Char( + string='Display Label', + compute='_compute_label', + store=True, + help='Auto-formatted "0.0005 mils" string for the picker dropdown.', + ) + note = fields.Char(string='Note') + active = fields.Boolean(default=True) + + @api.depends('value', 'uom') + def _compute_label(self): + for rec in self: + rec.label = f'{rec.value:g} {rec.uom}' if rec.value else '' + + def name_get(self): + return [(rec.id, rec.label or '?') for rec in self] +``` + +- [ ] **Step 2: Register the model in `__init__.py`** + +Add `from . import fp_recipe_thickness` to `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating/models/__init__.py`. + +Run: `grep -n "fp_recipe_thickness\|fp_process_node" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating/models/__init__.py` +Expected: both lines present. + +If not present, add the import line right after the existing `fp_process_node` import (preserve alphabetical order if used). + +### Task A2: Add view + ACL for `fp.recipe.thickness` + +**Files:** +- Create: `fusion_plating/views/fp_recipe_thickness_views.xml` +- Modify: `fusion_plating/security/ir.model.access.csv` +- Modify: `fusion_plating/__manifest__.py` + +- [ ] **Step 1: Create view file** + +Write to `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating/views/fp_recipe_thickness_views.xml`: + +```xml + + + + + + fp.recipe.thickness.list + fp.recipe.thickness + + + + + + + + + + + + + + +``` + +- [ ] **Step 2: Add ACL rows** + +Edit `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating/security/ir.model.access.csv` — append at the end: + +``` +access_fp_recipe_thickness_user,fp.recipe.thickness.user,model_fp_recipe_thickness,base.group_user,1,0,0,0 +access_fp_recipe_thickness_supervisor,fp.recipe.thickness.supervisor,model_fp_recipe_thickness,fusion_plating.group_fusion_plating_supervisor,1,1,1,0 +access_fp_recipe_thickness_manager,fp.recipe.thickness.manager,model_fp_recipe_thickness,fusion_plating.group_fusion_plating_manager,1,1,1,1 +``` + +- [ ] **Step 3: Register view in manifest** + +Edit `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating/__manifest__.py` `'data'` list — add `'views/fp_recipe_thickness_views.xml'` near other view files (after `fp_process_node_views.xml` if present). + +### Task A3: Add fields to `fusion.plating.process.node` + +**Files:** +- Modify: `fusion_plating/models/fp_process_node.py` + +- [ ] **Step 1: Add new fields to the model** + +Open `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating/models/fp_process_node.py`. Find the section comment `# ---- Recipe-only fields (apply when node_type='recipe') -----------------` (around line 304) and add the following block right after it: + +```python + # ---- Spec-derived metadata (recipe-root only — Promote Customer Spec) ---- + phosphorus_level = fields.Selection( + [('low_phos', 'Low Phosphorus (2-5%)'), + ('mid_phos', 'Mid Phosphorus (6-9%)'), + ('high_phos', 'High Phosphorus (10-13%)'), + ('na', 'N/A')], + string='Phosphorus Level', + default='na', + help='EN-specific. Set to N/A for non-EN processes (chrome, anodize, ' + 'black oxide). Drives certificate annotation and hydrogen-' + 'embrittlement risk assessment for bake-relief.', + ) + thickness_min = fields.Float(string='Min Thickness', digits=(10, 4)) + thickness_max = fields.Float(string='Max Thickness', digits=(10, 4)) + thickness_uom = fields.Selection( + [('mils', 'mils'), ('microns', 'microns'), ('inches', 'inches')], + string='Thickness UoM', default='mils', + ) + thickness_option_ids = fields.One2many( + 'fp.recipe.thickness', + 'recipe_id', + string='Thickness Options', + help='Discrete thickness values offered to the estimator on the ' + 'order line for jobs running this recipe.', + ) + + # ---- Bake relief — AMS 2759/9 hydrogen embrittlement (recipe root) ---- + requires_bake_relief = fields.Boolean( + string='Requires Bake Relief', + help='Hydrogen embrittlement relief bake required (high-strength ' + 'steel ≥ HRC 31 in conjunction with this chemistry). When set, ' + 'finishing the job auto-creates a bake-window record and blocks ' + 'shipment until bake is complete.', + ) + bake_window_hours = fields.Float( + string='Bake Window (hours)', default=4.0, + help='Maximum time between plate exit and bake start. Typical 4h per ' + 'AMS 2759/9.', + ) + bake_temperature = fields.Float( + string='Bake Temperature', default=375.0, + help='Relief bake temperature. Default 375 (°F per AMS 2759/9 for ' + 'steel ≥ HRC 40).', + ) + bake_temperature_uom = fields.Selection( + [('F', '°F'), ('C', '°C')], + string='Bake Temp Unit', + default='F', + ) + bake_duration_hours = fields.Float( + string='Bake Duration (hours)', default=23.0, + help='Minimum bake hold time at temperature. Typical 23h.', + ) + + # ---- Reverse of customer.spec.recipe_ids (lazy: see customer.spec) ---- + applicable_spec_ids = fields.Many2many( + 'fusion.plating.customer.spec', + 'fp_customer_spec_recipe_rel', + 'recipe_id', 'spec_id', + string='Applicable Specifications', + help='Customer / industry specifications this recipe is qualified to ' + 'satisfy. Set on the spec record; mirrored here for navigation.', + ) +``` + +The `bake_temperature_uom` default is hardcoded to `'F'` (Fahrenheit) rather than reading from `company.x_fc_default_temp_uom` — keeps the model self-contained. If the company-default lookup is needed later, switch to `default=lambda self: self.env.company.x_fc_default_temp_uom or 'F'`. + +### Task A4: Surface new fields on the recipe form view + +**Files:** +- Modify: `fusion_plating/views/fp_process_node_views.xml` + +- [ ] **Step 1: Find the recipe root form view** + +Run: `grep -n 'view_fp_process_node_form\|"fusion.plating.process.node"' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating/views/fp_process_node_views.xml | head -10` +Note the view ID(s) and form section line numbers. + +- [ ] **Step 2: Add a "Specification & Bake" notebook page** + +In the recipe form view (the one where `model="fusion.plating.process.node"` and the form shows a ``), add a new page right before the closing `` tag, visible only when the node is a recipe root: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +The `invisible="node_type != 'recipe' or parent_id"` clause hides the page on non-recipe nodes (operations, steps) and on nested recipe references. + +### Task A5: Add `recipe_ids` and `print_on_cert` to `fusion.plating.customer.spec` + +**Files:** +- Modify: `fusion_plating_quality/models/fp_customer_spec.py` + +- [ ] **Step 1: Add the M2M and boolean fields** + +Open `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_quality/models/fp_customer_spec.py`. Add the following fields after the existing `notes` field: + +```python + recipe_ids = fields.Many2many( + 'fusion.plating.process.node', + 'fp_customer_spec_recipe_rel', + 'spec_id', 'recipe_id', + domain="[('node_type', '=', 'recipe'), ('parent_id', '=', False)]", + string='Applicable Recipes', + help='Recipes that can produce work to this specification. ' + 'Many-to-many — one spec can cover multiple processes; ' + 'one recipe can satisfy multiple specs.', + ) + print_on_cert = fields.Boolean( + string='Print on Certificate', + default=True, + help="When enabled, this spec's code+revision appear on the CoC " + 'when the spec is selected on the SO line.', + ) +``` + +The relation table name `fp_customer_spec_recipe_rel` matches the inverse declared in Task A3 — they MUST match for Odoo to wire the M2M correctly. + +### Task A6: Surface new fields on the customer spec form view + +**Files:** +- Modify: `fusion_plating_quality/views/fp_customer_spec_views.xml` + +- [ ] **Step 1: Add the recipe linkage + print_on_cert to the form** + +Open `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_quality/views/fp_customer_spec_views.xml`. Find the form view's main `` block. Locate the existing `process_type_ids` field. Right after the group that holds it, add: + +```xml + + + + + + +``` + +If the form uses a notebook structure, add the recipe block as a new page or alongside `process_type_ids` in the same page — match the existing layout. + +### Task A7: Bump `fusion_plating` version + +**Files:** +- Modify: `fusion_plating/__manifest__.py` + +- [ ] **Step 1: Bump version** + +Edit the `'version'` line — change from `'19.0.18.15.16'` to `'19.0.19.0.0'`. + +### Task A8: Bump `fusion_plating_quality` version + +**Files:** +- Modify: `fusion_plating_quality/__manifest__.py` + +- [ ] **Step 1: Bump version** + +Edit the `'version'` line — change from `'19.0.4.14.0'` to `'19.0.5.0.0'`. + +### Task A9: Upgrade modules locally + smoke test + +- [ ] **Step 1: Upgrade `fusion_plating`** + +Run: +```bash +docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating --stop-after-init 2>&1 | tail -30 +``` +Expected: ends with `odoo.modules.loading: Modules loaded.` and exit code 0. No traceback. No `ParseError`. No `KeyError`. + +If it fails: read the traceback, identify the file + line, fix, retry. + +- [ ] **Step 2: Upgrade `fusion_plating_quality`** + +Run: +```bash +docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_quality --stop-after-init 2>&1 | tail -30 +``` +Expected: same — Modules loaded, no traceback. + +- [ ] **Step 3: Smoke test the recipe form** + +Open http://localhost:8069 in browser, log in, navigate Plating → Operations → Process Recipes. Open any recipe (e.g. ENP-ALUM-BASIC). Verify: +- The new "Specification & Bake" notebook page appears +- All fields are visible and editable: phosphorus_level, thickness_min/max/uom, thickness_option_ids, requires_bake_relief + bake settings, applicable_spec_ids +- Setting `requires_bake_relief = True` reveals the bake parameter fields +- Saving the form succeeds + +- [ ] **Step 4: Smoke test the customer spec form** + +Navigate Plating → Configuration → Quality & Documents → Customer Specs. Open AMS 2404. Verify: +- The "Applicable Recipes" picker is visible +- Adding a recipe (e.g. ENP-ALUM-BASIC) works and saves +- The "Print on Certificate" toggle is visible and defaulted ON +- Reopening the recipe shows AMS 2404 in its `applicable_spec_ids` (round-trip) + +### Task A10: Commit Phase A + +- [ ] **Step 1: Stage + commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add fusion_plating/models/fp_recipe_thickness.py \ + fusion_plating/views/fp_recipe_thickness_views.xml \ + fusion_plating/models/__init__.py \ + fusion_plating/models/fp_process_node.py \ + fusion_plating/views/fp_process_node_views.xml \ + fusion_plating/security/ir.model.access.csv \ + fusion_plating/__manifest__.py \ + fusion_plating_quality/models/fp_customer_spec.py \ + fusion_plating_quality/views/fp_customer_spec_views.xml \ + fusion_plating_quality/__manifest__.py +git commit -m "$(cat <<'EOF' +feat(promote-customer-spec): Phase A — recipe + spec foundation + +- Add fp.recipe.thickness model (replaces fp.coating.thickness, scoped to recipe) +- Add spec metadata + bake-relief fields to fusion.plating.process.node (recipe root) +- Add recipe_ids M2M + print_on_cert to fusion.plating.customer.spec +- Surface new fields on recipe form ("Specification & Bake" notebook page) +- Surface recipe linkage on customer spec form + +Pure additive — no existing code is removed yet. Foundation for Phases B-E. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` +Expected: commit succeeds, working tree clean. + +--- + +## Phase B — SO Line UX (Two-Picker) + +Goal: Add the `Specification` picker to the SO line and the direct order wizard. Hide (don't yet delete) the old `Primary Treatment` picker. Add part-level default for the spec. + +### Task B1: Add `x_fc_customer_spec_id` to `sale.order.line` + +**Files:** +- Modify: `fusion_plating_configurator/models/sale_order_line.py` + +- [ ] **Step 1: Add the new field** + +Open the file. Find the existing `x_fc_coating_config_id` field declaration (around line 62). Right after it, add: + +```python + x_fc_customer_spec_id = fields.Many2one( + 'fusion.plating.customer.spec', + string='Specification', + help='Customer / industry specification the work is being shipped to ' + '(e.g. AMS 2404 Rev D, BAC 5680 Rev E). Drives certificate ' + 'auto-fill and FAI / Nadcap routing.', + ) +``` + +- [ ] **Step 2: Add the auto-fill onchange from part default** + +Find the existing `_onchange_part_default_variant` method (around line 486). Append a sibling onchange right after it: + +```python + @api.onchange('x_fc_part_catalog_id') + def _onchange_part_default_spec(self): + """Pre-fill the line's specification from the part's default.""" + for line in self: + if (line.x_fc_part_catalog_id + and line.x_fc_part_catalog_id.x_fc_default_customer_spec_id + and not line.x_fc_customer_spec_id): + line.x_fc_customer_spec_id = ( + line.x_fc_part_catalog_id.x_fc_default_customer_spec_id + ) +``` + +The `x_fc_default_customer_spec_id` field on the part is added in Task B3 below — it must exist before this onchange fires meaningfully. Since this is a new file save in the same phase, ordering is fine. + +- [ ] **Step 3: Carry the spec into the invoice line** + +Find the existing `_prepare_invoice_line` method (around line 458). Add this carry-over right before the `return vals`: + +```python + if self.x_fc_customer_spec_id: + vals['x_fc_customer_spec_id'] = self.x_fc_customer_spec_id.id +``` + +### Task B2: Add `x_fc_customer_spec_id` to `account.move.line` + +**Files:** +- Modify: `fusion_plating_configurator/models/account_move_line.py` + +- [ ] **Step 1: Add the field** + +Open the file. Find the existing `x_fc_coating_config_id` field (or similar `x_fc_*` block near top). Add: + +```python + x_fc_customer_spec_id = fields.Many2one( + 'fusion.plating.customer.spec', + string='Specification', + help='Carried from the SO line so customer-facing invoice PDF can ' + 'render the spec reference next to the part number.', + ) +``` + +### Task B3: Add `x_fc_default_customer_spec_id` to `fp.part.catalog` + +**Files:** +- Modify: `fusion_plating_configurator/models/fp_part_catalog.py` + +- [ ] **Step 1: Add the field** + +Open the file. Find the existing `x_fc_default_coating_config_id` field (around line 280). Right after it, add: + +```python + x_fc_default_customer_spec_id = fields.Many2one( + 'fusion.plating.customer.spec', + string='Default Specification', + help='Default specification applied when this part is dropped on a ' + 'direct order line. Operator can override per order.', + ) +``` + +### Task B4: Update SO line view — show Specification picker, hide Primary Treatment + +**Files:** +- Modify: `fusion_plating_configurator/views/sale_order_views.xml` + +- [ ] **Step 1: Find the SO line tree/form** + +Run: `grep -n 'x_fc_coating_config_id\|x_fc_part_catalog_id' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml | head -20` + +Note the line numbers where `x_fc_coating_config_id` appears. + +- [ ] **Step 2: Add Specification picker, hide Primary Treatment** + +For each occurrence of ``, replace with: + +```xml + + +``` + +Keeping the old field with `invisible="1"` ensures any onchange / domain that depends on it during Phase B doesn't break. The field will be deleted entirely in Phase E. + +- [ ] **Step 3: Update the thickness picker domain** + +Locate ``. The current domain reads `[('coating_config_id', '=', x_fc_coating_config_id)]`. Change to: + +```xml + +``` + +NOTE: this assumes `fp.coating.thickness` has been migrated to `fp.recipe.thickness` and the field model on `sale.order.line.x_fc_thickness_id` was switched. The model switch happens in Task B5 below. + +### Task B5: Switch `sale.order.line.x_fc_thickness_id` to `fp.recipe.thickness` + +**Files:** +- Modify: `fusion_plating_configurator/models/sale_order_line.py` +- Modify: `fusion_plating_configurator/models/account_move_line.py` + +- [ ] **Step 1: Update the SO line thickness field comodel** + +In `sale_order_line.py`, find: +```python + x_fc_thickness_id = fields.Many2one( + 'fp.coating.thickness', + string='Thickness', + ondelete='set null', + domain="[('coating_config_id', '=', x_fc_coating_config_id)]", + help="Target coating thickness. Options come from the line's " + 'coating configuration.', + ) +``` + +Replace with: +```python + x_fc_thickness_id = fields.Many2one( + 'fp.recipe.thickness', + string='Thickness', + ondelete='set null', + domain="[('recipe_id', '=', x_fc_process_variant_id)]", + help="Target coating thickness. Options come from the line's recipe.", + ) +``` + +- [ ] **Step 2: Update the onchange that clears thickness on coating change** + +Find `_onchange_coating_clears_thickness` (around line 578). Replace with: + +```python + @api.onchange('x_fc_process_variant_id') + def _onchange_recipe_clears_thickness(self): + """Clear the thickness picker when recipe changes. + + Thickness options are scoped to the recipe; a value carried over + from a previous recipe would fail its domain. + """ + for line in self: + if (line.x_fc_thickness_id + and line.x_fc_thickness_id.recipe_id + != line.x_fc_process_variant_id): + line.x_fc_thickness_id = False +``` + +- [ ] **Step 3: Same change on `account.move.line`** + +In `account_move_line.py`, find the analogous `x_fc_thickness_id` field, change the comodel to `'fp.recipe.thickness'` and update the domain to `[('recipe_id', '=', x_fc_process_variant_id)]` if present (some invoice lines may not carry a process variant — leave domain blank if so). + +### Task B6: Update part catalog form view + +**Files:** +- Modify: `fusion_plating_configurator/views/fp_part_catalog_views.xml` + +- [ ] **Step 1: Add Default Specification field, hide Default Treatment** + +Run: `grep -n 'x_fc_default_coating_config_id\|x_fc_default_treatment_ids' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/fp_part_catalog_views.xml` + +Replace each `` with: + +```xml + + +``` + +Replace each `` with ``. + +### Task B7: Update direct order wizard model + view + +**Files:** +- Modify: `fusion_plating_configurator/wizard/fp_direct_order_line.py` +- Modify: `fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml` + +- [ ] **Step 1: Add `customer_spec_id` to wizard line model** + +In `fp_direct_order_line.py`, find the existing `coating_config_id` field declaration (around line 55). Right after it, add: + +```python + customer_spec_id = fields.Many2one( + 'fusion.plating.customer.spec', + string='Specification', + help='Customer / industry specification the work ships to.', + ) +``` + +- [ ] **Step 2: Add wizard auto-fill from part default** + +In the same file, find `_onchange_part_clears_variant` (around line 132). Inside the loop, before the `if not rec.part_catalog_id: continue` check, add: + +```python + # Pre-fill default spec from the part if line is empty + if (rec.part_catalog_id + and rec.part_catalog_id.x_fc_default_customer_spec_id + and not rec.customer_spec_id): + rec.customer_spec_id = rec.part_catalog_id.x_fc_default_customer_spec_id +``` + +- [ ] **Step 3: Carry spec onto SO line when wizard creates the order** + +Find the wizard's `_build_so_line_vals` method (or whatever method assembles vals for `sale.order.line.create`). Add this line in the vals dict: + +```python + 'x_fc_customer_spec_id': line.customer_spec_id.id if line.customer_spec_id else False, +``` + +If the method signature is unclear, run: `grep -n "x_fc_coating_config_id" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard.py` to find where the existing `x_fc_coating_config_id` is set on the SO line and add the spec line right next to it. + +- [ ] **Step 4: Update wizard view** + +In `fp_direct_order_wizard_views.xml`, find each `` and replace with: + +```xml + + +``` + +### Task B8: Bump `fusion_plating_configurator` version + +**Files:** +- Modify: `fusion_plating_configurator/__manifest__.py` + +- [ ] **Step 1: Bump version** + +Change `'version': '19.0.18.10.4'` → `'19.0.19.0.0'`. + +### Task B9: Upgrade + smoke test Phase B + +- [ ] **Step 1: Upgrade configurator** + +Run: +```bash +docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -30 +``` +Expected: clean upgrade, no traceback. + +- [ ] **Step 2: Smoke test SO line** + +Open http://localhost:8069. Plating → Sales & Quoting → Quotations. Create a new quote. Add a line. Verify: +- Specification picker is visible (was Primary Treatment) +- Pick a part with a default spec → Specification auto-fills +- Pick a recipe → Thickness dropdown filters to recipe-scoped options +- Save the quote — no errors + +- [ ] **Step 3: Smoke test direct order wizard** + +Plating → Sales & Quoting → Direct Order. Open the wizard. Add a line. Verify: +- Specification picker is visible +- Auto-fill from part default works +- Submit → SO is created with the spec carried over + +- [ ] **Step 4: Smoke test part catalog** + +Plating → Configuration → Shop Setup → (or wherever Parts live). Open a part. Verify "Default Specification" field is visible. Pick AMS 2404, save. + +### Task B10: Commit Phase B + +- [ ] **Step 1: Stage + commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add fusion_plating_configurator/ +git commit -m "$(cat <<'EOF' +feat(promote-customer-spec): Phase B — two-picker SO line UX + +- Add x_fc_customer_spec_id to sale.order.line + account.move.line +- Add x_fc_default_customer_spec_id to fp.part.catalog +- Add customer_spec_id to direct-order wizard line +- Switch x_fc_thickness_id comodel from fp.coating.thickness to fp.recipe.thickness +- Surface Specification picker on SO line, part form, wizard +- Hide Primary Treatment + Default Treatment fields (deletion deferred to Phase E) +- Auto-fill spec from part default on order entry + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Phase C — Pricing, Quality, Job, Cert + +Goal: Re-key the consumers of Coating Config (pricing rules, quality points, job records, certificate auto-fill) to read from Customer Spec / Recipe instead. + +### Task C1: Add `customer_spec_id` + `recipe_id` to `fp.pricing.rule` + +**Files:** +- Modify: `fusion_plating_configurator/models/fp_pricing_rule.py` + +- [ ] **Step 1: Add the new fields** + +Open the file. Find the existing `coating_config_id` field. Right after it, add: + +```python + customer_spec_id = fields.Many2one( + 'fusion.plating.customer.spec', + string='Specification', + help='Match rule against the SO line specification. Combine with ' + 'recipe_id for spec+recipe specific pricing.', + ) + recipe_id = fields.Many2one( + 'fusion.plating.process.node', + string='Recipe', + domain="[('node_type', '=', 'recipe'), ('parent_id', '=', False)]", + help='Match rule against the SO line recipe. Combine with ' + 'customer_spec_id for spec+recipe specific pricing, or ' + 'leave spec blank for recipe-tier pricing.', + ) +``` + +- [ ] **Step 2: Update the rule lookup logic** + +Find the rule-matching method (likely `_find_matching_rule` or `find_price`). The exact file/method: + +Run: `grep -n "def _find_matching\|def find_price\|coating_config_id" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/models/fp_pricing_rule.py | head -20` + +Replace the lookup logic with this priority chain (most-specific-first): + +```python + @api.model + def _find_matching_rule(self, customer_spec=None, recipe=None, + partner=None, material=None, **kw): + """Lookup priority (first match wins): + 1. spec + recipe both set — exact match + 2. spec set, recipe blank — spec-tier rule + 3. recipe set, spec blank — recipe-tier rule + 4. both blank, partner+material — generic fallback + """ + Rule = self.sudo() + # 1. exact spec+recipe match + if customer_spec and recipe: + r = Rule.search([ + ('active', '=', True), + ('customer_spec_id', '=', customer_spec.id), + ('recipe_id', '=', recipe.id), + ], limit=1) + if r: + return r + # 2. spec-tier (any recipe) + if customer_spec: + r = Rule.search([ + ('active', '=', True), + ('customer_spec_id', '=', customer_spec.id), + ('recipe_id', '=', False), + ], limit=1) + if r: + return r + # 3. recipe-tier (any spec) + if recipe: + r = Rule.search([ + ('active', '=', True), + ('recipe_id', '=', recipe.id), + ('customer_spec_id', '=', False), + ], limit=1) + if r: + return r + # 4. partner+material catch-all + domain = [('active', '=', True), + ('customer_spec_id', '=', False), + ('recipe_id', '=', False)] + if partner: + domain.append(('partner_id', 'in', [False, partner.id])) + if material: + domain.append(('material_category', 'in', [False, material])) + return Rule.search(domain, limit=1) +``` + +If the existing method takes `coating_config` as a parameter, update all callers in the same file (and elsewhere — grep for them) to pass `customer_spec` and `recipe` instead. + +Run after editing: `grep -rn "_find_matching_rule\|find_price.*coating" /Users/gurpreet/Github/Odoo-Modules/fusion_plating --include="*.py" | head -20` — fix every caller. + +### Task C2: Update `fp.pricing.rule` view + +**Files:** +- Modify: `fusion_plating_configurator/views/fp_pricing_rule_views.xml` + +- [ ] **Step 1: Add new pickers, hide old** + +Find each `` and replace with: + +```xml + + + +``` + +### Task C3: Add `customer_spec_ids` + `recipe_ids` to `fp.quality.point` + +**Files:** +- Modify: `fusion_plating_quality/models/fp_quality_point.py` + +- [ ] **Step 1: Add new M2M filters** + +Open the file. Find the existing `coating_config_ids` field (around line 66). Right after, add: + +```python + customer_spec_ids = fields.Many2many( + 'fusion.plating.customer.spec', + 'fp_quality_point_spec_rel', + 'point_id', 'spec_id', + string='Filter by Specifications', + help='If set, this trigger only fires for SOs / jobs whose ' + 'specification is in this list. Leave blank to ignore spec.', + ) + recipe_ids = fields.Many2many( + 'fusion.plating.process.node', + 'fp_quality_point_recipe_rel', + 'point_id', 'recipe_id', + domain="[('node_type', '=', 'recipe'), ('parent_id', '=', False)]", + string='Filter by Recipes', + help='If set, this trigger only fires for jobs running one of ' + 'these recipes. Leave blank to ignore recipe.', + ) +``` + +- [ ] **Step 2: Update the matcher in `_should_fire_for`** + +Find the existing matcher method (likely `_should_fire_for` or `_match`). The current `coating_config_ids` check looks like: + +```python + if self.coating_config_ids and ( + not coating or coating not in self.coating_config_ids): + return False +``` + +Add after it (don't remove yet — Phase E removes coating filter): + +```python + if self.customer_spec_ids and ( + not customer_spec or customer_spec not in self.customer_spec_ids): + return False + if self.recipe_ids and ( + not recipe or recipe not in self.recipe_ids): + return False +``` + +Update the method signature to accept `customer_spec=None, recipe=None` parameters. + +### Task C4: Update `fp_quality_point_hooks.py` to pass spec + recipe + +**Files:** +- Modify: `fusion_plating_quality/models/fp_quality_point_hooks.py` + +- [ ] **Step 1: Update each hook to pass new keys** + +Find every place that reads `coating_config_id` and calls the matcher. Example (around line 82): + +```python + coating = getattr(job, 'coating_config_id', False) or False +``` + +Right next to it, add: + +```python + customer_spec = getattr(job, 'customer_spec_id', False) or False + recipe = job.recipe_id if 'recipe_id' in job._fields else False +``` + +Then update the `_should_fire_for(...)` call to pass the new args: + +```python + if not point._should_fire_for( + coating=coating, # legacy, removed in Phase E + customer_spec=customer_spec, + recipe=recipe): + continue +``` + +Repeat for all 3 hook locations identified in the spec (line 82, 101, 126). + +For the SO-line hook (around line 55), do the same — read `x_fc_customer_spec_id` from order lines: + +```python + customer_specs = so.order_line.mapped('x_fc_customer_spec_id') \ + if 'x_fc_customer_spec_id' in so.order_line._fields else False +``` + +### Task C5: Update `fp.quality.point` view + +**Files:** +- Modify: `fusion_plating_quality/views/fp_quality_point_views.xml` + +- [ ] **Step 1: Add new M2M widgets, hide old** + +Find each `` and replace with: + +```xml + + + +``` + +### Task C6: Wire `fp.job.customer_spec_id` from SO line on confirm + +**Files:** +- Modify: `fusion_plating_jobs/models/sale_order.py` + +- [ ] **Step 1: Update job-creation vals** + +Find the section that creates `fp.job` records on SO confirm. The block currently sets `coating_config_id`: + +Run: `grep -n "vals\[.coating_config_id.\]\|coating_config_id.*=.*coating" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_jobs/models/sale_order.py | head -10` + +For each location that sets `coating_config_id` on job vals, add immediately after: + +```python + # Promote Customer Spec — carry from SO line to job + spec = getattr(line, 'x_fc_customer_spec_id', False) or False + if not spec and 'x_fc_customer_spec_id' in self._fields: + spec = self.x_fc_customer_spec_id or False + if spec: + vals['customer_spec_id'] = spec.id +``` + +- [ ] **Step 2: Update spec-resolution helper if present** + +Run: `grep -n "_resolve_customer_spec\|_fp_resolve_spec\|customer_spec.*resolve" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_jobs/models/sale_order.py` + +If a helper exists, ensure it picks up `x_fc_customer_spec_id` from the line first, then falls back to a partner-level default if any. If no helper exists, the inline pattern from Step 1 is sufficient. + +### Task C7: Update `fp.certificate` auto-fill to read from `customer_spec_id` + +**Files:** +- Modify: `fusion_plating_certificates/models/fp_certificate.py` + +- [ ] **Step 1: Switch the spec_reference source** + +Open the file. Find the `_fp_create_certificates` method and locate the line that reads coating config (around line 293): + +```python + cfg = getattr(so, 'x_fc_coating_config_id', False) +``` + +Replace the surrounding spec_reference assembly with: + +```python + # Promote Customer Spec — read spec from the job's customer_spec_id + spec = getattr(job, 'customer_spec_id', False) or False + if not spec and so: + # Fall back to SO-line-level spec if job didn't carry it + spec = (so.order_line.mapped('x_fc_customer_spec_id')[:1] + if 'x_fc_customer_spec_id' in so.order_line._fields + else False) + if spec and spec.print_on_cert: + spec_ref = spec.code or '' + if spec.revision: + spec_ref = f'{spec_ref} Rev {spec.revision}' + vals['spec_reference'] = spec_ref + vals['customer_spec_id'] = spec.id # if cert has this field +``` + +If `fp.certificate` doesn't yet have a `customer_spec_id` field, add it now (in the same file at the field declarations): + +```python + customer_spec_id = fields.Many2one( + 'fusion.plating.customer.spec', + string='Specification', + help='Snapshot of the spec the cert was issued against.', + ) +``` + +### Task C8: Bump versions + +**Files:** +- Modify: `fusion_plating_configurator/__manifest__.py` +- Modify: `fusion_plating_quality/__manifest__.py` +- Modify: `fusion_plating_jobs/__manifest__.py` +- Modify: `fusion_plating_certificates/__manifest__.py` + +- [ ] **Step 1: Bump each** + +- `fusion_plating_configurator`: `19.0.19.0.0` → `19.0.19.1.0` +- `fusion_plating_quality`: `19.0.5.0.0` → `19.0.5.1.0` +- `fusion_plating_jobs`: `19.0.8.27.0` → `19.0.9.0.0` +- `fusion_plating_certificates`: `19.0.5.6.0` → `19.0.6.0.0` + +### Task C9: Upgrade + smoke test Phase C + +- [ ] **Step 1: Upgrade all four modules** + +Run: +```bash +docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator,fusion_plating_quality,fusion_plating_jobs,fusion_plating_certificates --stop-after-init 2>&1 | tail -30 +``` +Expected: all four upgrade cleanly. + +- [ ] **Step 2: Smoke test pricing** + +Plating → Configuration → Pricing & Billing → (Pricing Rules — find existing menu). Open a rule. Verify Specification + Recipe pickers appear. Create a test rule: customer_spec=AMS 2404, recipe=ENP-ALUM-BASIC, price = $1.00/sqft. Save. + +Then create a test SO with that spec + recipe and verify the unit price comes back as $1.00 (or whatever the rule says). + +- [ ] **Step 3: Smoke test quality point** + +Plating → Configuration → Recipes & Steps → Quality Points (or wherever). Open or create a point. Verify customer_spec_ids + recipe_ids pickers are visible. Set both to specific values and confirm only matching jobs trigger checks. + +- [ ] **Step 4: Smoke test job creation** + +Confirm a quote that has `x_fc_customer_spec_id` set on a line. After confirm, navigate to the resulting `fp.job`. Verify `customer_spec_id` is populated. + +- [ ] **Step 5: Smoke test cert** + +Mark the job done. Issue a cert (Certificates menu). Verify the `spec_reference` field on the cert reads "AMS 2404 Rev D" (or whatever spec was on the SO line). + +### Task C10: Commit Phase C + +- [ ] **Step 1: Stage + commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add fusion_plating_configurator/ fusion_plating_quality/ fusion_plating_jobs/ fusion_plating_certificates/ +git commit -m "$(cat <<'EOF' +feat(promote-customer-spec): Phase C — pricing, quality, job, cert re-keyed + +- Add customer_spec_id + recipe_id to fp.pricing.rule with priority lookup +- Add customer_spec_ids + recipe_ids M2M filters to fp.quality.point + hooks +- Wire customer_spec_id from SO line to fp.job on confirm +- Switch fp.certificate spec_reference auto-fill to read from customer_spec_id +- Add customer_spec_id to fp.certificate for traceability snapshot + +Old coating_config_id paths still functional during transition; removed in Phase E. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Phase D — Reports, Tablet, Portal + +Goal: Update all customer-facing and operator-facing surfaces to print/display Specification + Recipe instead of Coating Config. + +### Task D1: Update `report_fp_sale.xml` (sale acknowledgement) + +**Files:** +- Modify: `fusion_plating_reports/report/report_fp_sale.xml` + +- [ ] **Step 1: Replace coating field references with spec** + +Run: `grep -n "x_fc_coating_config_id" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_reports/report/report_fp_sale.xml` + +For each occurrence inside `` and `` patterns, replace: + +```xml + +``` +→ +```xml + +``` + +And: +```xml + +``` +→ +```xml + +``` + +Repeat for all 4 occurrences identified by the grep. + +### Task D2: Update `report_fp_wo_sticker.xml` (WO sticker) + +**Files:** +- Modify: `fusion_plating_reports/report/report_fp_wo_sticker.xml` + +- [ ] **Step 1: Replace coating references** + +Run: `grep -n "x_fc_coating_config_id\|_coating" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_reports/report/report_fp_wo_sticker.xml` + +For each `t-set="_coating" t-value="..."` pattern, change: +```xml + +``` +→ +```xml + + +``` + +For each `t-set="_coating" t-value="line.x_fc_coating_config_id"`: +```xml + + +``` + +For the comment block at top of file, update the variable list to reference `_spec` (`fusion.plating.customer.spec`) instead of `_coating` (`fp.coating.config`). + +For all rendering points that print `_coating.name`, change to `_spec.display_name` or `_spec.code` — pick whichever shows up in the cert format (likely `_spec.code` + ` Rev ` + `_spec.revision`). + +### Task D3: Update `report_fp_job_traveller.xml` (in `fusion_plating_reports`) + +**Files:** +- Modify: `fusion_plating_reports/report/report_fp_job_traveller.xml` + +- [ ] **Step 1: Replace coating reference** + +Find: +```xml + + + +``` + +Replace with: +```xml + + + +``` + +### Task D4: Update `report_fp_job_traveller.xml` (in `fusion_plating_jobs`) + +**Files:** +- Modify: `fusion_plating_jobs/report/report_fp_job_traveller.xml` + +- [ ] **Step 1: Replace coating reference** + +Find: +```xml + + + +``` + +Replace with: +```xml + + + +``` + +### Task D5: Update `report_fp_job_sticker.xml` + +**Files:** +- Modify: `fusion_plating_jobs/report/report_fp_job_sticker.xml` + +- [ ] **Step 1: Replace `_coating` t-set blocks** + +Find each block: +```xml + +``` + +Replace with: +```xml + +``` + +Then update any `_coating.name` references downstream to `_spec.display_name`. + +### Task D6: Update CoC EN/FR templates + +**Files:** +- Modify: `fusion_plating_reports/report/report_coc_en.xml` (or whichever filename holds the CoC body) +- Modify: `fusion_plating_reports/report/report_coc_fr.xml` +- Modify: `fusion_plating_reports/report/report_coc_chronological.xml` (if it references coating) + +- [ ] **Step 1: Locate coating refs in CoC templates** + +Run: `grep -rn "coating_config\|x_fc_coating" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_reports/report/report_coc*.xml` + +For each occurrence, replace coating field reads with the cert's own `spec_reference` field (which Phase C already populates from `customer_spec_id`). Example: +```xml + +``` +This may already be the case — the coating refs in CoC templates may have only been used for fall-back display. Verify by reading each match. + +### Task D7: Update tablet/shopfloor controller payload + +**Files:** +- Modify: `fusion_plating_shopfloor/controllers/shopfloor_controller.py` + +- [ ] **Step 1: Replace coating field reads** + +Run: `grep -n "coating_config_id" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_shopfloor/controllers/shopfloor_controller.py` + +At line ~1143 (job payload): +```python + 'part_catalog_id', 'coating_config_id', +``` +Change to: +```python + 'part_catalog_id', 'customer_spec_id', +``` + +At line ~1554 (job lookup): +```python + job.coating_config_id + if 'coating_config_id' in job._fields else False +``` +Change to: +```python + job.customer_spec_id + if 'customer_spec_id' in job._fields else False +``` + +If the field appears elsewhere in payload-building code, replace all occurrences. + +### Task D8: Update portal configurator controller + +**Files:** +- Modify: `fusion_plating_portal/controllers/portal_configurator.py` + +- [ ] **Step 1: Replace coating session keys with spec** + +Open the file. Find every `'coating_config_id'` string key. For each, do a pair of changes: + +Replace: +```python + coating_id = int(kw.get('coating_config_id', 0)) + if coating_id: + session_data['coating_config_id'] = coating_id +``` +With: +```python + spec_id = int(kw.get('customer_spec_id', 0)) + if spec_id: + session_data['customer_spec_id'] = spec_id +``` + +Replace: +```python + coatings = request.env['fp.coating.config'].sudo().search(...) +``` +With: +```python + specs = request.env['fusion.plating.customer.spec'].sudo().search( + [('active', '=', True)], + order='spec_type, code, revision', + ) +``` + +Replace lookup blocks: +```python + coating = request.env['fp.coating.config'].sudo().browse( + session_data['coating_config_id'], + ) +``` +With: +```python + spec = request.env['fusion.plating.customer.spec'].sudo().browse( + session_data['customer_spec_id'], + ) +``` + +For pricing rule resolution (around line 267): +```python + if rule.coating_config_id: + if rule.coating_config_id.id != coating.id: + continue +``` +Change to: +```python + if rule.customer_spec_id: + if rule.customer_spec_id.id != spec.id: + continue +``` + +Replace template variable name `coatings` → `specs` and `coating` → `spec` consistently throughout. + +### Task D9: Update portal template + +**Files:** +- Modify: `fusion_plating_portal/views/fp_portal_configurator_templates.xml` + +- [ ] **Step 1: Replace template variable + form field names** + +Run: `grep -n "coating_config_id\|coatings" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/views/fp_portal_configurator_templates.xml` + +For each occurrence: +- `name="coating_config_id"` → `name="customer_spec_id"` +- `id="coating_config_id"` → `id="customer_spec_id"` +- `getElementById('coating_config_id')` → `getElementById('customer_spec_id')` +- `dataset.coatingId` → `dataset.specId` +- `t-foreach="coatings"` → `t-foreach="specs"` +- `t-as="coating"` → `t-as="spec"` +- `coating.name` → `spec.display_name` (or `spec.code` — pick by visual context) + +For customer-facing labels — keep "Coating" if the visible text reads to a customer. The internal field name changes; the customer-visible text is a separate decision (per spec open question #1). For now, keep the visible text as "Coating Specification" (compromise) and revisit after the client call. + +### Task D10: Bump versions + +**Files:** +- Modify: `fusion_plating_reports/__manifest__.py` +- Modify: `fusion_plating_jobs/__manifest__.py` (already bumped in C — bump again to a new minor) +- Modify: `fusion_plating_shopfloor/__manifest__.py` +- Modify: `fusion_plating_portal/__manifest__.py` + +- [ ] **Step 1: Bump each** + +- `fusion_plating_reports`: `19.0.10.16.0` → `19.0.11.0.0` +- `fusion_plating_jobs`: `19.0.9.0.0` → `19.0.9.1.0` +- `fusion_plating_shopfloor`: `19.0.25.2.1` → `19.0.26.0.0` +- `fusion_plating_portal`: `19.0.2.1.1` → `19.0.3.0.0` + +### Task D11: Upgrade + smoke test Phase D + +- [ ] **Step 1: Upgrade all four modules** + +Run: +```bash +docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_reports,fusion_plating_jobs,fusion_plating_shopfloor,fusion_plating_portal --stop-after-init 2>&1 | tail -30 +``` +Expected: clean upgrade. + +- [ ] **Step 2: Smoke test sale ack PDF** + +Open a confirmed SO (the one created during Phase C smoke test). Print → Sale Order PDF. Verify the line shows the spec (e.g. "AMS 2404 Rev D") instead of empty/coating. + +- [ ] **Step 3: Smoke test WO sticker PDF** + +From the SO's smart button, open a job. Print → WO Sticker. Verify the spec is displayed. + +- [ ] **Step 4: Smoke test job traveller PDF** + +Same job → Print → Job Traveller. Verify spec is in the header. + +- [ ] **Step 5: Smoke test CoC PDF** + +Issue a cert from the job. Verify the cert PDF prints "AMS 2404 Rev D" in the spec field. + +- [ ] **Step 6: Smoke test tablet payload** + +Open Plating → Shop Floor → (Tablet station). Scan / open a job. Verify the job payload includes the spec info (visible in the UI as "Specification: AMS 2404 Rev D" or similar). + +- [ ] **Step 7: Smoke test portal** + +Open the portal customer-facing URL (e.g. /my/quote-request). Walk through the configurator — verify the picker is now showing Specifications instead of Coatings. Submit a quote. Verify it lands as an SO with `x_fc_customer_spec_id` populated. + +### Task D12: Commit Phase D + +- [ ] **Step 1: Stage + commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add fusion_plating_reports/ fusion_plating_jobs/ fusion_plating_shopfloor/ fusion_plating_portal/ +git commit -m "$(cat <<'EOF' +feat(promote-customer-spec): Phase D — reports, tablet, portal updated + +- Replace coating_config_id refs with customer_spec_id in: + - report_fp_sale.xml (sale ack) + - report_fp_wo_sticker.xml (WO sticker) + - report_fp_job_traveller.xml (jobs + reports modules) + - report_fp_job_sticker.xml (job sticker) + - CoC EN/FR templates + - shopfloor_controller.py (tablet payload) +- Portal configurator: switch from fp.coating.config to fusion.plating.customer.spec +- Portal template: rename form field, dataset attrs, template vars + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Phase E — Final Removal + +Goal: Delete `fp.coating.config`, `fp.treatment`, `fp.coating.thickness`, and all hidden `x_fc_coating_config_id` / `x_fc_treatment_ids` fields. Module installs cleanly with zero references to dead code. + +### Task E1: Drop `coating_config_id` from `fp.job` + +**Files:** +- Modify: `fusion_plating_jobs/models/fp_job.py` + +- [ ] **Step 1: Remove the field declaration** + +Open the file. Find (around line 51): +```python + coating_config_id = fields.Many2one( + 'fp.coating.config', + string='Coating Configuration', + ondelete='restrict', + ) +``` +Delete the entire block. + +- [ ] **Step 2: Remove any code that READS this field** + +Run: `grep -n "coating_config_id" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_jobs/models/fp_job.py` + +For each line, evaluate and either delete or refactor to read from `customer_spec_id`. Common patterns: + +Around line 1001 + 1549: +```python + coating = job.coating_config_id +``` + +These reads are likely for bake-relief logic. Replace with: +```python + recipe = job.recipe_id # bake settings now live on recipe +``` + +And update the surrounding logic to read `recipe.requires_bake_relief`, `recipe.bake_window_hours`, etc., instead of `coating.requires_bake_relief`. + +### Task E2: Drop `coating_config_id` references from `sale_order.py` + +**Files:** +- Modify: `fusion_plating_jobs/models/sale_order.py` + +- [ ] **Step 1: Remove all coating_config refs** + +Run: `grep -n "x_fc_coating_config_id\|coating_config" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_jobs/models/sale_order.py` + +For each line, delete the line or refactor to use `x_fc_customer_spec_id` / `customer_spec_id`. Lines 356-491 in the original file have a chain of guards reading both line-level and SO-level coating fields — all of these can be removed since: +1. The SO-line `x_fc_coating_config_id` is being deleted in Task E5 +2. The job's `coating_config_id` was deleted in Task E1 +3. Spec resolution from line happens in Task C6 already + +Verify the resulting code only references `x_fc_customer_spec_id` or `customer_spec_id` after the cleanup. + +### Task E3: Drop fields from `sale.order.line` + +**Files:** +- Modify: `fusion_plating_configurator/models/sale_order_line.py` + +- [ ] **Step 1: Delete `x_fc_coating_config_id` field** + +Find: +```python + x_fc_coating_config_id = fields.Many2one( + 'fp.coating.config', string='Primary Treatment', + ) +``` +Delete the entire block. + +- [ ] **Step 2: Delete `x_fc_treatment_ids` field** + +Find: +```python + x_fc_treatment_ids = fields.Many2many( + 'fp.treatment', string='Additional Treatments', + ) +``` +Delete the entire block. + +### Task E4: Drop fields from `account.move.line` + `fp.part.catalog` + +**Files:** +- Modify: `fusion_plating_configurator/models/account_move_line.py` +- Modify: `fusion_plating_configurator/models/fp_part_catalog.py` + +- [ ] **Step 1: Delete `x_fc_coating_config_id` from invoice line** + +In `account_move_line.py`, find and delete the `x_fc_coating_config_id` field block. If `_prepare_invoice_line` or similar carries the field, remove that line too. + +- [ ] **Step 2: Delete defaults from part catalog** + +In `fp_part_catalog.py`, delete: +```python + x_fc_default_coating_config_id = fields.Many2one(...) + x_fc_default_treatment_ids = fields.Many2many(...) +``` + +### Task E5: Drop SO line view's hidden coating fields + +**Files:** +- Modify: `fusion_plating_configurator/views/sale_order_views.xml` +- Modify: `fusion_plating_configurator/views/fp_part_catalog_views.xml` +- Modify: `fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml` + +- [ ] **Step 1: Remove `` lines** + +Run: `grep -rn 'name="x_fc_coating_config_id"\|name="x_fc_treatment_ids"\|name="x_fc_default_coating_config_id"\|name="x_fc_default_treatment_ids"\|name="coating_config_id" invisible' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/ /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/wizard/` + +For each match, delete the line. + +### Task E6: Drop wizard model coating field + +**Files:** +- Modify: `fusion_plating_configurator/wizard/fp_direct_order_line.py` +- Modify: `fusion_plating_configurator/wizard/fp_direct_order_wizard.py` + +- [ ] **Step 1: Delete `coating_config_id` from wizard line** + +In `fp_direct_order_line.py`, find: +```python + coating_config_id = fields.Many2one( + 'fp.coating.config', + string='Primary Treatment', + help='...', + ) +``` +Delete the entire block. + +- [ ] **Step 2: Delete `treatment_ids` from wizard line** + +In the same file, find: +```python + treatment_ids = fields.Many2many( + 'fp.treatment', + string='Additional Treatments', + help='...', + ) +``` +Delete. + +- [ ] **Step 3: Update the recipe-resolution chain** + +In `_compute_effective_process` (around line 107), the resolution chain currently has: + +```python + @api.depends('process_variant_id', + 'part_catalog_id.default_process_id', + 'coating_config_id.recipe_id') + def _compute_effective_process(self): + ... + cc_proc = (rec.coating_config_id.recipe_id + if rec.coating_config_id else False) +``` + +Since `coating_config_id` no longer exists, simplify the resolution to: + +```python + @api.depends('process_variant_id', + 'part_catalog_id.default_process_id') + def _compute_effective_process(self): + for rec in self: + if rec.process_variant_id: + rec.effective_process_id = rec.process_variant_id + label = (rec.process_variant_id.variant_label + or rec.process_variant_id.name) + rec.effective_process_source = 'Variant: %s' % (label or 'unnamed') + continue + part_proc = (rec.part_catalog_id.default_process_id + if rec.part_catalog_id else False) + if part_proc: + rec.effective_process_id = part_proc + rec.effective_process_source = 'Part default' + continue + rec.effective_process_id = False + rec.effective_process_source = False +``` + +- [ ] **Step 4: Remove all other coating refs in wizard** + +Run: `grep -n "coating_config\|treatment_ids" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_line.py /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard.py` + +Delete every remaining occurrence (auto-fill calls, vals dict entries, etc.). + +### Task E7: Drop `coating_config_id` + `coating_config_ids` from pricing + quality + +**Files:** +- Modify: `fusion_plating_configurator/models/fp_pricing_rule.py` +- Modify: `fusion_plating_configurator/views/fp_pricing_rule_views.xml` +- Modify: `fusion_plating_quality/models/fp_quality_point.py` +- Modify: `fusion_plating_quality/models/fp_quality_point_hooks.py` +- Modify: `fusion_plating_quality/views/fp_quality_point_views.xml` + +- [ ] **Step 1: Drop `coating_config_id` field from pricing rule** + +In `fp_pricing_rule.py`, delete: +```python + coating_config_id = fields.Many2one('fp.coating.config', ...) +``` + +Verify no remaining method body reads `coating_config_id`. + +- [ ] **Step 2: Drop hidden field from pricing view** + +In `fp_pricing_rule_views.xml`, delete every `` line. + +- [ ] **Step 3: Drop `coating_config_ids` from quality point** + +In `fp_quality_point.py`, delete the `coating_config_ids` M2M block. + +- [ ] **Step 4: Drop coating refs from quality point hooks** + +In `fp_quality_point_hooks.py`, find each line that reads `coating_config_id` (lines 55, 82, 101, 126 in the original). Delete the `coating = ...` assignment lines and the `coating=coating,` arg in `_should_fire_for` calls. + +The matcher in `_should_fire_for` (Phase C added the new spec/recipe checks alongside the coating check). Now remove the coating check: + +```python + if self.coating_config_ids and ( + not coating or coating not in self.coating_config_ids): + return False +``` +Delete this block. Update the method signature to drop `coating=None` parameter. + +- [ ] **Step 5: Drop hidden field from quality point view** + +In `fp_quality_point_views.xml`, delete every `` line. + +### Task E8: Drop `fp.coating.config` model + view + ACL + +**Files:** +- Delete: `fusion_plating_configurator/models/fp_coating_config.py` +- Delete: `fusion_plating_configurator/views/fp_coating_config_views.xml` +- Modify: `fusion_plating_configurator/models/__init__.py` +- Modify: `fusion_plating_configurator/__manifest__.py` +- Modify: `fusion_plating_configurator/security/ir.model.access.csv` +- Modify: `fusion_plating_configurator/views/fp_configurator_menu.xml` + +- [ ] **Step 1: Delete model file** + +Run: `rm /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/models/fp_coating_config.py` + +- [ ] **Step 2: Delete view file** + +Run: `rm /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/fp_coating_config_views.xml` + +- [ ] **Step 3: Remove import from models `__init__.py`** + +Open `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/models/__init__.py`. Delete the line: +```python +from . import fp_coating_config +``` + +- [ ] **Step 4: Remove view from manifest data list** + +Open `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/__manifest__.py`. Find and delete: +```python + 'views/fp_coating_config_views.xml', +``` + +- [ ] **Step 5: Remove ACL rows** + +Open `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/security/ir.model.access.csv`. Delete the 3 rows (lines 8-10): +``` +access_fp_coating_config_operator,fp.coating.config.operator,model_fp_coating_config,... +access_fp_coating_config_estimator,fp.coating.config.estimator,model_fp_coating_config,... +access_fp_coating_config_manager,fp.coating.config.manager,model_fp_coating_config,... +``` + +- [ ] **Step 6: Remove menu item** + +Open `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/fp_configurator_menu.xml`. Find: +```xml + +``` +Delete the entire `` element. Also delete the related `` for the action (likely `action_fp_coating_config`) if defined in the same file. + +### Task E9: Drop `fp.treatment` model + view + ACL + data + +**Files:** +- Delete: `fusion_plating_configurator/models/fp_treatment.py` +- Delete: `fusion_plating_configurator/views/fp_treatment_views.xml` +- Delete: `fusion_plating_configurator/data/fp_treatment_data.xml` +- Modify: `fusion_plating_configurator/models/__init__.py` +- Modify: `fusion_plating_configurator/__manifest__.py` +- Modify: `fusion_plating_configurator/security/ir.model.access.csv` + +- [ ] **Step 1: Delete model + view + data files** + +Run: +```bash +rm /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/models/fp_treatment.py +rm /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/fp_treatment_views.xml +rm /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/data/fp_treatment_data.xml +``` + +- [ ] **Step 2: Remove import from `__init__.py`** + +Delete `from . import fp_treatment` from `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/models/__init__.py`. + +- [ ] **Step 3: Remove view + data from manifest** + +In `__manifest__.py`, delete: +```python + 'views/fp_treatment_views.xml', + 'data/fp_treatment_data.xml', +``` + +- [ ] **Step 4: Remove ACL rows** + +In `ir.model.access.csv`, delete the 3 rows (lines 2-4): +``` +access_fp_treatment_operator,fp.treatment.operator,model_fp_treatment,... +access_fp_treatment_supervisor,fp.treatment.supervisor,model_fp_treatment,... +access_fp_treatment_manager,fp.treatment.manager,model_fp_treatment,... +``` + +- [ ] **Step 5: Remove menu item if present** + +Run: `grep -n "menu_fp_treatment\|fp_treatment" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/fp_configurator_menu.xml` + +If a menuitem exists, delete the `` element (and the related `` for its action if local). + +### Task E10: Drop `fp.coating.thickness` model + view + ACL + +**Files:** +- Delete: `fusion_plating_configurator/models/fp_coating_thickness.py` +- Delete: `fusion_plating_configurator/views/fp_coating_thickness_views.xml` +- Modify: `fusion_plating_configurator/models/__init__.py` +- Modify: `fusion_plating_configurator/__manifest__.py` +- Modify: `fusion_plating_configurator/security/ir.model.access.csv` + +- [ ] **Step 1: Delete files** + +Run: +```bash +rm /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/models/fp_coating_thickness.py +rm /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/fp_coating_thickness_views.xml +``` + +- [ ] **Step 2: Remove import from `__init__.py`** + +Delete `from . import fp_coating_thickness` from `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/models/__init__.py`. + +- [ ] **Step 3: Remove view from manifest** + +Delete `'views/fp_coating_thickness_views.xml',` from `__manifest__.py`. + +- [ ] **Step 4: Remove ACL rows** + +In `ir.model.access.csv`, delete the 3 rows (lines 51-53 in the original): +``` +access_fp_coating_thickness_user,... +access_fp_coating_thickness_estimator,... +access_fp_coating_thickness_manager,... +``` + +### Task E11: Re-point `fp.delivery.x_fc_thickness_id` to `fp.recipe.thickness` + +**Files:** +- Modify: `fusion_plating_logistics/models/fp_delivery.py` + +- [ ] **Step 1: Switch comodel** + +Open the file. Find: +```python + 'fp.coating.thickness', string='Thickness', +``` +Change to: +```python + 'fp.recipe.thickness', string='Thickness', +``` + +### Task E12: Bump versions + +**Files:** +- Modify: `fusion_plating_configurator/__manifest__.py` +- Modify: `fusion_plating_jobs/__manifest__.py` +- Modify: `fusion_plating_quality/__manifest__.py` +- Modify: `fusion_plating_logistics/__manifest__.py` + +- [ ] **Step 1: Bump each** + +- `fusion_plating_configurator`: `19.0.19.1.0` → `19.0.20.0.0` +- `fusion_plating_jobs`: `19.0.9.1.0` → `19.0.10.0.0` +- `fusion_plating_quality`: `19.0.5.1.0` → `19.0.6.0.0` +- `fusion_plating_logistics`: bump minor (read current with grep) + +### Task E13: Final upgrade + comprehensive smoke test + +- [ ] **Step 1: Upgrade all touched modules in dependency order** + +Run: +```bash +docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating,fusion_plating_quality,fusion_plating_configurator,fusion_plating_jobs,fusion_plating_certificates,fusion_plating_reports,fusion_plating_shopfloor,fusion_plating_portal,fusion_plating_logistics --stop-after-init 2>&1 | tail -50 +``` +Expected: ALL modules upgrade cleanly, no traceback, no `KeyError: 'fp.coating.config'`, no `psycopg2.errors.UndefinedColumn`. + +If any error mentions `coating_config` or `treatment`, grep for the missed reference, fix it, retry. + +- [ ] **Step 2: Verify zero references remain** + +Run: +```bash +grep -rn "coating_config_id\|fp\.coating\.config\|fp\.treatment\b\|fp\.coating\.thickness\|x_fc_treatment_ids\|x_fc_default_coating\|x_fc_default_treatment" /Users/gurpreet/Github/Odoo-Modules/fusion_plating --include="*.py" --include="*.xml" --include="*.csv" 2>/dev/null | grep -v "/scripts/\|/tests/\|/docs/" | head -50 +``` +Expected: ZERO results. + +If any results appear, they must be addressed before this phase is complete (each one is a missed reference). Update the relevant file and re-run upgrade. + +- [ ] **Step 3: End-to-end smoke test (golden path)** + +Walk through the full lifecycle: +1. Plating → Sales & Quoting → Quotations → New +2. Add a customer (e.g. "Test Aerospace Inc") +3. Add a line: pick a part with a default spec, verify Specification + Recipe pre-fill +4. Confirm the order +5. Open the resulting `fp.job` (smart button on SO) +6. Verify `customer_spec_id` is populated +7. Open the operator tablet (Plating → Shop Floor → Tablet) +8. Walk through job → start → finish each step +9. Mark the job done +10. Issue a CoC (Plating → Quality → Certificates) +11. Verify the CoC PDF prints "Spec: AMS 2404 Rev D" (or whatever the test spec was) +12. Generate the invoice +13. Verify the invoice line carries the spec + +If any step fails, debug + fix before declaring Phase E complete. + +- [ ] **Step 4: Test the rare scenarios** + +1. **Mid-Phos vs High-Phos same customer:** + - Create two SOs for the same customer + part + - SO1: Specification=AMS 2404, Recipe=EN-MP + - SO2: Specification=AMS 2404, Recipe=EN-HP + - Confirm both. Verify SO1 job auto-creates a bake-window record (Mid-Phos requires bake), SO2 does not (High-Phos doesn't). + +2. **Spec revision:** + - Create a new customer.spec record: code=AMS 2404, revision=E (Rev D already seeded) + - Create a SO using Rev E + - Issue CoC. Verify it prints "AMS 2404 Rev E" not "Rev D" + +3. **Process freeze:** + - Edit a recipe (mark a step "skip"). Save. Verify chatter on the recipe records the change. + - Verify the customer.spec records are untouched (no spurious chatter on them). + +### Task E14: Commit Phase E + +- [ ] **Step 1: Stage all changes (including deletions)** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add -A fusion_plating_configurator/ fusion_plating_jobs/ fusion_plating_quality/ fusion_plating_logistics/ +git status +``` +Verify deleted files appear with status `D` and modified files with `M`. + +- [ ] **Step 2: Commit** + +Run: +```bash +git commit -m "$(cat <<'EOF' +feat(promote-customer-spec): Phase E — final removal of coating config + treatment + +- Delete fp.coating.config model + view + ACL + menu +- Delete fp.treatment model + view + ACL + seeded data +- Delete fp.coating.thickness model + view + ACL (replaced by fp.recipe.thickness) +- Drop x_fc_coating_config_id from sale.order.line, account.move.line, fp.part.catalog +- Drop x_fc_treatment_ids from sale.order.line, fp.part.catalog +- Drop coating_config_id from fp.job, fp.pricing.rule +- Drop coating_config_ids M2M from fp.quality.point + matcher +- Drop coating_config refs from sale_order.py, fp_job_step.py, hooks +- Re-point fp.delivery.x_fc_thickness_id to fp.recipe.thickness + +Zero coating_config / treatment references remain in active codebase. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Post-Work — Push + (Optional) Deploy to Entech + +### Task POST1: Push the feature branch + +- [ ] **Step 1: Push to both remotes** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git push origin feature/promote-customer-spec +git push gitea feature/promote-customer-spec +``` + +### Task POST2: Open a pull request (optional, for review) + +- [ ] **Step 1: Open PR** + +Run: +```bash +gh pr create --base main --head feature/promote-customer-spec \ + --title "Promote Customer Spec, retire Coating Config" \ + --body "$(cat <<'EOF' +## Summary +- Retire fp.coating.config + fp.treatment entirely (no archive) +- Promote fusion.plating.customer.spec to primary spec entity +- Two-picker SO line UX: Specification + Recipe +- Move process params (thickness, bake) onto Recipe model + +## Spec +[docs/superpowers/specs/2026-05-14-promote-customer-spec-design.md](https://github.com/gsinghpal/Odoo-Modules/blob/feature/promote-customer-spec/fusion_plating/docs/superpowers/specs/2026-05-14-promote-customer-spec-design.md) + +## Backup +Pre-change snapshot at `backup/pre-spec-recipe-collapse-2026-05-14`. + +## Test plan +- [ ] Recipe form shows new "Specification & Bake" notebook page +- [ ] Customer Spec form shows recipe linkage + print_on_cert +- [ ] SO line + direct order wizard show Specification picker +- [ ] Pricing rule lookup respects spec + recipe priority chain +- [ ] CoC PDF prints spec.code + revision +- [ ] Mid-Phos job auto-creates bake-window; High-Phos does not +- [ ] grep for `coating_config\|fp\.treatment` returns zero results + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +### Task POST3: Deploy to entech (optional, after review) + +- [ ] **Step 1: Sync code to entech** + +For each modified module, run (one example shown for `fusion_plating`): +```bash +cat /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating/models/fp_process_node.py | \ + ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating/models/fp_process_node.py'" +``` + +Repeat for each touched file. (For bulk sync, consider rsync or git pull on entech if a git remote is configured there.) + +- [ ] **Step 2: Stop entech, upgrade, restart** + +Run: +```bash +ssh pve-worker5 "pct exec 111 -- bash -c ' + systemctl stop odoo && \ + su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating,fusion_plating_quality,fusion_plating_configurator,fusion_plating_jobs,fusion_plating_certificates,fusion_plating_reports,fusion_plating_shopfloor,fusion_plating_portal,fusion_plating_logistics --stop-after-init\" && \ + systemctl start odoo +'" +``` + +- [ ] **Step 3: Clear asset cache on entech** + +Run: +```bash +ssh pve-worker5 "pct exec 111 -- bash -c \"sudo -u postgres psql -d admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';\\\"\"" +``` + +- [ ] **Step 4: Verify entech health** + +Open https://entech-url/web (whatever the entech URL is). Log in. Walk through the same end-to-end smoke test from Task E13 Step 3. + +--- + +## Self-Review Notes + +**Spec coverage check:** Every section of the design spec maps to at least one task: +- Decision 1 (promote spec, retire coating) → covered by Phases A + E +- Decision 2 (two-picker SO line) → Phase B +- Decision 3 (move process params to recipe) → Phase A (Tasks A3-A4) +- Decision 4 (delete fp.treatment) → Phase E (Task E9) +- Decision 5 (naming) → applied throughout view edits +- Decision 6 (M2M relationship) → Phase A (Tasks A3, A5) +- All "Models removed entirely" → Phase E +- All "Models modified" → Phases A, B, C +- All "Re-keyed" items → Phase C, D +- All UI/view changes → Phases B, C, D +- Per-module impact summary → tasks distributed across phases match +- 7 aerospace scenarios → validated implicitly by Phase E Step 4 (Mid/High Phos, spec rev, process freeze) + +**Placeholder scan:** No "TBD", "TODO", "implement later" patterns in the plan. Every code block is concrete. + +**Type consistency:** +- M2M relation table `fp_customer_spec_recipe_rel` is consistent between `fusion.plating.customer.spec.recipe_ids` (Task A5) and `fusion.plating.process.node.applicable_spec_ids` (Task A3) ✓ +- Field name `customer_spec_id` (singular M2O) used consistently on `sale.order.line`, `account.move.line`, `fp.job`, `fp.pricing.rule`, `fp.certificate`, `fp.delivery` ✓ +- M2M fields use `_ids` suffix consistently (`recipe_ids`, `customer_spec_ids`, `applicable_spec_ids`, `process_type_ids`) ✓ +- `fp.recipe.thickness.recipe_id` matches the inverse `fusion.plating.process.node.thickness_option_ids` ✓ + +**Open items deferred to client confirmation (per spec):** +- Portal customer-facing label: kept as "Coating Specification" in Phase D Task D9 — revisit after client call +- Pricing rule complexity: assumes both spec_id + recipe_id keys are useful; can simplify if client confirms only one matters +- Source Approval Letter tracking: deferred to a future enhancement +- DPAS / ITAR / DFARS extension: deferred + +--- + +## Plan Complete — Execution Choice + +**Plan saved to** [`docs/superpowers/plans/2026-05-14-promote-customer-spec.md`](docs/superpowers/plans/2026-05-14-promote-customer-spec.md). + +**Two execution options:** + +**1. Subagent-Driven (recommended)** — I dispatch a fresh subagent per task, review between tasks, fast iteration. Good for this plan because each task is small and self-contained, and the surface area (10 modules) means a fresh context per task avoids confusion. + +**2. Inline Execution** — Execute tasks in this session using the executing-plans skill, batch execution with checkpoints. Faster overall but every task adds to context. + +**Which approach?** diff --git a/fusion_plating/docs/superpowers/specs/2026-05-14-promote-customer-spec-design.md b/fusion_plating/docs/superpowers/specs/2026-05-14-promote-customer-spec-design.md new file mode 100644 index 00000000..9d61d54e --- /dev/null +++ b/fusion_plating/docs/superpowers/specs/2026-05-14-promote-customer-spec-design.md @@ -0,0 +1,558 @@ +# Promote Customer Specification, Retire Coating Config — Design + +**Date:** 2026-05-14 +**Author:** brainstorming session with Gurpreet +**Status:** Draft — pending user review +**Backup branch:** `backup/pre-spec-recipe-collapse-2026-05-14` (1414ef2 on origin + gitea) + +--- + +## TL;DR + +`fp.coating.config` ("Primary Treatment") is a half-baked sketch that conflates a customer-facing specification with an internal production process. The codebase already contains a properly-modelled aerospace specification entity (`fusion.plating.customer.spec`) with revision tracking, document URLs, AS9100 / Nadcap / N299 extensions, and 15 seeded industry specs. This design proposes: + +1. **Retire** `fp.coating.config` and `fp.treatment` entirely — no archive, no commented blocks, no "obsolete" module. +2. **Promote** `fusion.plating.customer.spec` to the primary specification entity, exposed on the SO line as a picker labelled **"Specification"**. +3. **Move process parameters** (thickness, bake-relief, phosphorus level) from coating config onto the Recipe model where they semantically belong. +4. **Keep the spec ↔ recipe separation** — both are pickers on the SO line. They auto-fill from part defaults so order entry stays single-click for repeat work. + +The result is an aerospace-correct two-picker design ("Spec the customer cited" + "Recipe we used") that handles all 7 aerospace audit scenarios cleanly while preserving fast order entry for ENPlating's repeat-customer workflow. + +--- + +## Problem statement + +ENPlating is an aerospace/defence plating shop running Fusion Plating in development. The current SO line UX presents two pickers that look conceptually identical to the operator: + +- **"Primary Treatment"** (`fp.coating.config`) — process type, phos level, thickness range, spec reference, cert level, bake settings, default recipe pointer +- **"Process Variant"** (`fusion.plating.process.node` recipe root) — the production tree + +The client has stated: + +- "Recipe and Primary Treatment are the same thing" (correctly observing the conceptual overlap) +- Speed of order entry is paramount (repeat customers, repeat parts, AI auto-fill on the roadmap) +- Per-step variation ("skip the bake") is rare and already handled by `fp.job.node.override` + +The client serves aerospace and defence primes (Boeing, Lockheed, Northrop, RTX, etc. — to be confirmed). Audit posture matters. Source approval letters, AS9100, Nadcap, FAI / AS9102, spec revision tracking — these are real concerns even if the daily operator doesn't think about them. + +--- + +## Background — the two parallel models + +### `fp.coating.config` ("Primary Treatment") + +- **Module:** `fusion_plating_configurator` +- **Origin:** built by the configurator team as an order-entry convenience +- **Seeded data:** zero records ship with the module +- **Fields:** name, process_type_id (single), phosphorus_level, thickness_min/max/uom, thickness_option_ids, spec_reference (free Char), certification_level, requires_bake_relief, bake_window_hours, bake_temperature(_uom), bake_duration_hours, pre_treatment_ids, post_treatment_ids, recipe_id (default recipe pointer), description, sequence, active +- **Audit features:** none (no chatter, no revision tracking, no unique constraint on spec ref, no customer linkage) +- **Used by:** SO line picker, direct order wizard, portal customer self-service, fp.job, fp.delivery, account.move.line, fp.pricing.rule, fp.quality.point, fp.certificate (auto-fill), reports +- **Seeded coating records:** 0 + +### `fusion.plating.customer.spec` ("Customer Specification") + +- **Module:** `fusion_plating_quality` (with extensions in `fusion_plating_aerospace` + `fusion_plating_nuclear`) +- **Origin:** built by the quality team as the auditable spec library +- **Seeded data:** 11 aerospace specs + 4 nuclear specs ship with the module +- **Fields:** code (e.g. AMS 2404), revision, effective_date, partner_id, process_type_ids (M2M), spec_type (industry/customer/internal), document_url, notes (Html), company_id, active, plus mail.thread + mail.activity.mixin +- **Aerospace extension fields:** x_fc_is_aerospace, x_fc_as9100_clause_ids, x_fc_nadcap_required, x_fc_requires_first_article, x_fc_pri_file_code, x_fc_customer_approval_required +- **Nuclear extension fields:** x_fc_is_nuclear, x_fc_n299_level_id, x_fc_nqa1_applicable, x_fc_extended_retention_years, x_fc_nuclear_customer_type +- **Audit features:** mail.thread chatter, unique `(code, revision, company_id)` constraint, tracking on every key field +- **Used by:** fp.job.customer_spec_id (parallel to coating_config_id, currently underused) +- **Seeded specs:** AMS 2404, AMS 2700, AMS 2759, AMS QQ-P-416, ASTM B733, BAC 5709, MIL-A-8625, MIL-C-26074, MIL-DTL-13924, PRI AS7108, QQ-C-320, OPG SQAP, Bruce N299, AECL N299, Candu N299 + +### Why both exist + +Two different teams (configurator + quality) built parallel solutions to overlapping problems. Neither cleaned up after the other. The result is two records describing the same real-world artifact, with the better-architected one (Customer Spec) sitting unused in the daily flow while the weaker one (Coating Config) drives the UI. + +--- + +## Decisions + +### Decision 1 — Promote Customer Spec, retire Coating Config + +`fusion.plating.customer.spec` becomes the primary specification entity. `fp.coating.config` is removed entirely (no archive, no obsolete flag, no commented blocks). + +**Rationale:** +- Customer Spec already has the audit infrastructure aerospace requires +- Customer Spec already has aerospace + nuclear extension modules +- Customer Spec already ships with real industry specs +- The aerospace team has already invested in this model +- Coating Config has 0 seed records, no audit trail, no customer linkage + +### Decision 2 — Two-picker SO line UX + +The SO line carries two Many2one pickers: + +| Picker label | Backing model | Purpose | +|---|---|---| +| **Specification** | `fusion.plating.customer.spec` | What the customer cited on the PO | +| **Recipe** | `fusion.plating.process.node` (root) | How we make it | + +Both auto-fill from the part's defaults. For repeat customer + part combinations, order entry remains one click. For the rare phos swap (Mid → High) the estimator changes one dropdown. + +**Rationale:** +- Spec ≠ Process in aerospace doctrine — auditors expect the separation +- Many-to-many in concept (one spec covers multiple recipes; one recipe satisfies multiple specs) — modelling them as one record breaks down +- AI drawing detection populates one decision per field; both fields are independently AI-fillable +- Existing seed data already shows the pattern: AMS 2404 already lists `[ptype_en_lp, ptype_en_mp, ptype_en_hp]` as applicable processes + +### Decision 3 — Move process parameters onto Recipe + +Fields currently on Coating Config that describe the production process (not the customer requirement) move onto `fusion.plating.process.node` (recipe root): + +| Field | From | To | +|---|---|---| +| `phosphorus_level` | `fp.coating.config` | `fusion.plating.process.node` (recipe root) | +| `thickness_min`, `thickness_max`, `thickness_uom` | `fp.coating.config` | `fusion.plating.process.node` (recipe root) | +| `thickness_option_ids` | `fp.coating.config` (`fp.coating.thickness`) | re-parented as `fp.recipe.thickness` | +| `requires_bake_relief` | `fp.coating.config` | `fusion.plating.process.node` (recipe root) | +| `bake_window_hours` | `fp.coating.config` | `fusion.plating.process.node` (recipe root) | +| `bake_temperature`, `bake_temperature_uom` | `fp.coating.config` | `fusion.plating.process.node` (recipe root) | +| `bake_duration_hours` | `fp.coating.config` | `fusion.plating.process.node` (recipe root) | +| pre/post treatment lists | `fp.coating.config` (M2M `fp.treatment`) | already covered — recipe steps include pre/post operations | + +**Rationale:** these fields describe HOW we plate (the process), not WHAT we promised the customer (the spec). + +**Why bake-relief belongs on Recipe specifically** — AMS 2759/9 says "bake if hardness ≥ HRC 31 AND hydrogen-embrittlement risk exists." The risk is determined by the plating chemistry (Mid-Phos = high HE risk; High-Phos = low; non-EN processes = none). The recipe author knows which chemistry their recipe uses and ticks `requires_bake_relief` once. The spec doesn't need to drive this — AMS 2759/9 is invoked universally when conditions are met. Bake-window auto-create logic on `fp.job.button_mark_done` reads from `recipe.requires_bake_relief` instead of `coating.requires_bake_relief`. + +### Decision 4 — Delete `fp.treatment` model entirely + +`fp.treatment` (the small library of named pre/post operations: Bead Blast, Zincate, Bake, Passivate, etc.) is removed. Pre/post operations are already first-class steps in the recipe tree. The `fp.step.template` library (Sub 12a) plays the role of "approved step library" for shops that want one. + +### Decision 5 — Naming convention + +| Concept | Label on screen | Technical model | +|---|---|---| +| The customer-facing spec record | **"Specification"** (formal, audit-friendly) | `fusion.plating.customer.spec` (unchanged) | +| The same thing in tight UI spots (kanban chips, status bars) | **"Spec"** (short form) | (same) | +| The admin menu | **"Specifications"** | (renamed from "Customer Specs") | +| The production tree | **"Recipe"** (unchanged) | `fusion.plating.process.node` (unchanged) | + +Rejected alternatives: +- "Plating Spec" — too narrow (won't cover anodize, masking, etc.) +- "Coating Spec" — same problem; "coating" is the customer's word, not internal +- "Process Spec" — collides verbally with "Process Recipe" +- "Customer Spec" — fine but slightly off when the spec is a public industry standard +- "Treatment" / "Coating Configuration" — what we're explicitly removing + +### Decision 6 — Recipe ↔ Specification relationship + +Many-to-many. One spec applies to multiple recipes; one recipe can satisfy multiple specs. + +Implementation: add `recipe_ids` Many2many on `fusion.plating.customer.spec`, with reverse field on the recipe model. + +```python +# On fusion.plating.customer.spec +recipe_ids = fields.Many2many( + 'fusion.plating.process.node', + 'fp_customer_spec_recipe_rel', + 'spec_id', + 'recipe_id', + domain="[('node_type', '=', 'recipe'), ('parent_id', '=', False)]", + string='Applicable Recipes', +) +``` + +The existing `process_type_ids` M2M on customer.spec stays — useful for spec-level filtering at the process-type level (e.g., "AMS 2404 covers EN-LP, EN-MP, EN-HP"). + +--- + +## Detailed model changes + +### Models removed entirely + +- `fp.coating.config` — model definition, views, search, action, security rows +- `fp.treatment` — model definition, views, search, action, security rows, seed data file + +### Models modified + +#### `fusion.plating.customer.spec` (in `fusion_plating_quality`) + +Add fields to support what was on Coating Config: + +```python +# Process parameter helpers (optional; recipe is source of truth) +recipe_ids = fields.Many2many( + 'fusion.plating.process.node', + 'fp_customer_spec_recipe_rel', + 'spec_id', 'recipe_id', + domain="[('node_type', '=', 'recipe'), ('parent_id', '=', False)]", + string='Applicable Recipes', + help='Recipes that can produce work to this specification. ' + 'Many-to-many — one spec can cover multiple processes; ' + 'one recipe can satisfy multiple specs.', +) + +# Spec-level cert auto-fill helper (optional override of recipe's spec line) +print_on_cert = fields.Boolean( + string='Print on Certificate', + default=True, + help='When enabled, this spec\'s code+revision appear on the CoC ' + 'when the spec is selected on the SO line.', +) +``` + +(Aerospace + nuclear extensions are already in place — no changes to those modules' fields.) + +#### `fusion.plating.process.node` (in `fusion_plating`) + +Add the process parameter fields previously on Coating Config: + +```python +# Recipe-only fields (apply when node_type='recipe' and parent_id is False) +phosphorus_level = fields.Selection( + [('low_phos', 'Low Phosphorus (2-5%)'), + ('mid_phos', 'Mid Phosphorus (6-9%)'), + ('high_phos', 'High Phosphorus (10-13%)'), + ('na', 'N/A')], + string='Phosphorus Level', + default='na', + help='EN-specific. Set to N/A for non-EN processes (chrome, anodize, ' + 'black oxide).', +) +thickness_min = fields.Float(string='Min Thickness', digits=(10, 4)) +thickness_max = fields.Float(string='Max Thickness', digits=(10, 4)) +thickness_uom = fields.Selection( + [('mils', 'mils'), ('microns', 'microns'), ('inches', 'inches')], + string='Thickness UoM', default='mils', +) +thickness_option_ids = fields.One2many( + 'fp.recipe.thickness', + 'recipe_id', + string='Thickness Options', +) + +# Bake relief (AMS 2759/9 hydrogen embrittlement) +requires_bake_relief = fields.Boolean( + string='Requires Bake Relief', + help='Hydrogen embrittlement relief bake required (high-strength steel, ' + 'Rockwell C ≥ 31). When set, finishing the job auto-creates a ' + 'bake window record and blocks shipment until bake is complete.', +) +bake_window_hours = fields.Float( + string='Bake Window (hours)', default=4.0, + help='Maximum time between plate exit and bake start. Typical 4h per ' + 'AMS 2759/9.', +) +bake_temperature = fields.Float( + string='Bake Temperature', default=375.0, + help='Relief bake temperature. Default 375 (°F per AMS 2759/9 for ' + 'steel ≥ HRC 40).', +) +bake_temperature_uom = fields.Selection( + [('F', '°F'), ('C', '°C')], + string='Temp Unit', + default=lambda self: self.env.company.x_fc_default_temp_uom or 'F', +) +bake_duration_hours = fields.Float( + string='Bake Duration (hours)', default=23.0, + help='Minimum bake hold time at temperature. Typical 23h.', +) + +# Reverse of customer.spec.recipe_ids +applicable_spec_ids = fields.Many2many( + 'fusion.plating.customer.spec', + 'fp_customer_spec_recipe_rel', + 'recipe_id', 'spec_id', + string='Applicable Specifications', +) +``` + +These fields render only when `node_type='recipe'` and `parent_id=False` (i.e. the recipe root). Use `invisible="node_type != 'recipe' or parent_id"` on the form view. + +#### `fp.coating.thickness` → `fp.recipe.thickness` + +Renamed model. M2O `coating_config_id` becomes `recipe_id` pointing at `fusion.plating.process.node`. + +```python +class FpRecipeThickness(models.Model): + _name = 'fp.recipe.thickness' + _description = 'Fusion Plating — Recipe Thickness Option' + _order = 'recipe_id, sequence' + + recipe_id = fields.Many2one( + 'fusion.plating.process.node', + string='Recipe', + required=True, + ondelete='cascade', + domain="[('node_type', '=', 'recipe'), ('parent_id', '=', False)]", + ) + # ... (existing thickness/uom/sequence/label fields) +``` + +### Models with field deletions + +| Model | Field removed | +|---|---| +| `sale.order.line` | `x_fc_coating_config_id` | +| `sale.order.line` | `x_fc_treatment_ids` | +| `fp.part.catalog` | `x_fc_default_coating_config_id` | +| `fp.part.catalog` | `x_fc_default_treatment_ids` | +| `account.move.line` | `x_fc_coating_config_id` | +| `fp.job` | `coating_config_id` | +| `fp.pricing.rule` | `coating_config_id` | +| `fp.quality.point` | `coating_config_ids` | +| `fp.direct.order.line` | `coating_config_id` | +| `fp.direct.order.line` | `treatment_ids` | + +### Models with field additions + +| Model | Field added | Purpose | +|---|---|---| +| `sale.order.line` | `x_fc_customer_spec_id` Many2one | Specification picker on SO line | +| `fp.part.catalog` | `x_fc_default_customer_spec_id` Many2one | Per-part default spec | +| `account.move.line` | `x_fc_customer_spec_id` Many2one | Carries spec to invoice for cert reference | +| `fp.pricing.rule` | `customer_spec_id` Many2one | Primary key (replaces coating_config_id) | +| `fp.pricing.rule` | `recipe_id` Many2one | Secondary key for recipe-only rules | + +**Pricing rule lookup priority** (most specific → least specific): +1. `customer_spec_id` AND `recipe_id` both set → exact match +2. `customer_spec_id` set, `recipe_id` blank → spec-tier rule (e.g. "all AS9100 work +15%") +3. `recipe_id` set, `customer_spec_id` blank → recipe-tier rule (e.g. "EN Mid-Phos $X/sqft") +4. Both blank → catch-all (customer/material defaults) + +The pricing engine returns the FIRST match in this order. Composability: a single quote can stack a base recipe rate + a spec surcharge by configuring two rules. +| `fp.quality.point` | `customer_spec_ids` Many2many | Re-keyed QC trigger filter | +| `fp.quality.point` | `recipe_ids` Many2many | Recipe-level QC trigger filter | +| `fp.direct.order.line` | `customer_spec_id` Many2one | Wizard picker | + +### Models unchanged (already use customer.spec) + +- `fp.job.customer_spec_id` — already exists, will become the primary spec link +- All aerospace + nuclear extension fields on customer.spec — unchanged + +--- + +## UI / view changes + +### SO line view + +Replace the Primary Treatment + Treatments + Process Variant block with: + +``` +┌────────────────────────────────────────────────────────────┐ +│ Part: ABC-001 Rev A [picker] │ +│ Specification: AMS 2404 Rev D [picker] │ +│ Recipe: EN Mid-Phos #4 [picker] │ +│ Thickness: 0.0005-0.0008 in [picker — scoped to recipe] │ +│ Qty: 50 │ +└────────────────────────────────────────────────────────────┘ +``` + +Both Specification and Recipe pickers use `default_get` to pre-fill from the chosen part's defaults. The thickness picker domain becomes `[('recipe_id', '=', x_fc_process_variant_id)]` (was `coating_config_id`). + +### Direct order wizard + +Same change: replace coating + treatments fields with specification picker. Rename "Primary Treatment" label → "Specification". + +### Job form + +Remove `coating_config_id` field. Surface `customer_spec_id` (already exists) as the primary spec link with proper widget styling. Add smart button "View Spec" that opens the spec record. + +### Reports + +| Report | Change | +|---|---| +| `report_fp_sale.xml` | Print `line.x_fc_customer_spec_id.code` + `revision` instead of `x_fc_coating_config_id.name` | +| `report_fp_wo_sticker.xml` | Same | +| `report_fp_job_traveller.xml` | Same | +| `report_fp_job_sticker.xml` | Same | +| `report_coc_en.xml` / `report_coc_fr.xml` | Cert reads `spec.code Rev rev` instead of `coating.spec_reference` | +| Cert auto-fill (`_fp_create_certificates`) | Read from `job.customer_spec_id` instead of `coating_config_id` | + +### Portal + +`fusion_plating_portal/controllers/portal_configurator.py` — change "Pick a Coating" picker to "Pick a Specification". Customer-facing label may stay "Coating" if `partner.x_fc_portal_label_preference` says so (settings-driven; defer to client preference). + +### Menus + +- Remove "Coating Configurations" menu item under Configuration → Materials & Tanks (or wherever it lives today) +- Promote "Customer Specifications" under Configuration → Quality & Documents → **rename to "Specifications"** +- Optionally surface "Specifications" higher in the menu tree (e.g. as a top-level Configuration tab) since it's now a primary admin entity + +### Smart buttons + +On `res.partner` (customer): add "Specifications" smart button → opens specs filtered to `partner_id = self`. + +On `fusion.plating.process.node` (recipe root): add "Applicable Specifications" smart button → opens specs where `recipe_ids` includes this recipe. + +--- + +## Per-module impact summary + +| Module | Impact | Notes | +|---|---|---| +| `fusion_plating` | High | Add fields to fp.process.node + fp.recipe.thickness model + bake-relief logic refactor | +| `fusion_plating_configurator` | Critical | Delete fp.coating.config + fp.treatment + their views/data; refactor SO line + part catalog + direct order wizard + pricing | +| `fusion_plating_quality` | Medium | Add `recipe_ids` to customer.spec; add `print_on_cert` field; refactor quality.point trigger | +| `fusion_plating_jobs` | Medium | Remove coating_config_id from fp.job; sale_order.py spec resolution chain; pricing path | +| `fusion_plating_certificates` | Medium | Cert auto-fill reads from customer_spec_id | +| `fusion_plating_reports` | Medium | All 4 plating reports updated | +| `fusion_plating_portal` | Medium | Picker change, label decision | +| `fusion_plating_shopfloor` | Low | Tablet payload updated to read recipe + spec instead of coating | +| `fusion_plating_logistics` | Low | `fp.delivery.x_fc_thickness_id` — change M2O target from `fp.coating.thickness` to `fp.recipe.thickness` | +| `fusion_plating_aerospace` | None | Already extends customer.spec correctly | +| `fusion_plating_nuclear` | None | Already extends customer.spec correctly | +| `fusion_plating_compliance*` | None | Doesn't reference coating | +| Tank / bath / chemistry models | None | Keyed off `process_type` not `coating` | +| `fusion_iot` | None | No coating references | +| `fusion_plating_bridge_maintenance` | None | No coating references | + +--- + +## Aerospace scenarios validated + +The 7 aerospace scenarios from the brainstorming session all resolve cleanly: + +1. **Same chemistry, different customer specs** — multiple spec records (BAC 5680, LMS-3045, AMS 2404) all link to one Recipe. Cert prints the spec the customer cited; process freeze remains intact. +2. **Spec revision (BAC 5680 Rev D → Rev E)** — separate spec records via `(code, revision, company)` unique constraint. Both can be active. Old POs reference old rev; new POs reference new rev. Recipe untouched. +3. **Nadcap process freeze** — Recipe edits trigger the existing sign-off workflow. Spec edits don't touch the recipe. Two clean audit trails. +4. **Source Approval Letter** — `partner_id` on customer.spec naturally surfaces "Boeing-approved specs" via filter. +5. **First Article Inspection (AS9102)** — `x_fc_requires_first_article` on customer.spec drives the gate. FAI tied to (part, spec) — matches AS9102 doctrine. +6. **Customer source inspection** — Specifications menu becomes the customer-facing audit view. +7. **DPAS / ITAR / DFARS** — Future extension on customer.spec via a new module (`fusion_plating_compliance_export` or similar). + +For the mid-phos / high-phos same-customer scenario: +- Both lines pick the same Specification (e.g. AMS 2404) +- Each line picks a different Recipe (Mid-Phos vs High-Phos) +- Bake settings travel with the recipe (Mid-Phos requires bake; High-Phos doesn't) +- One spec record serves both orders; no duplication + +--- + +## Open questions for client + +Before finalizing implementation, confirm with the client: + +1. **Customer-facing terminology on the portal:** when his customers self-service quote, do they think they're ordering a "coating" or a "specification"? Determines whether the portal picker label says "Coating" or "Specification" on the customer-facing page (internal label stays "Specification"). +2. **Pricing rule complexity:** how many active pricing rules does ENPlating use? Are surcharges keyed to spec (AS9100 = +15%) or to recipe (Mid-Phos = $X/sqft)? Determines whether `fp.pricing.rule` keeps both `customer_spec_id` and `recipe_id` keys or just one. +3. **Customer Source Approvals:** does ENPlating have formal Source Approval Letters from any primes? If yes, names + counts so we can scope a "Customer Approval" tracking enhancement. +4. **Retire the menu under Materials & Tanks?** "Coating Configurations" menu likely sits there today — confirm safe to remove (vs hide for one release). + +These are nice-to-haves; the design proceeds without their answers but the answers refine UX details. + +--- + +## Out of scope (explicitly NOT doing) + +- Data migration of existing coating config records (per user direction: dev-stage, no historical data to preserve) +- Backwards-compatibility shims (`if 'coating_config_id' in self._fields` guards) — clean removal +- Archive / obsolete code patterns — clean deletion only +- Resurrecting `fp.treatment` for any purpose +- Adding a new `fp.spec.library` or `fp.process.specification` model — `fusion.plating.customer.spec` IS the spec model +- Building a Source Approval Letter tracker (deferred — flag for future enhancement after client confirms the need) +- Building DPAS / ITAR / DFARS export-control tracking (deferred — separate compliance extension module when needed) + +--- + +## Risk analysis + +| Risk | Severity | Mitigation | +|---|---|---| +| Bake-relief logic accidentally regresses (compliance-grade) | High | Smoke test: create a Mid-Phos job, verify bake_window auto-creates with correct temp/duration. Create a High-Phos job, verify NO bake_window | +| Cert auto-fill loses spec_reference | High | Smoke test: complete a job with Spec=AMS 2404, verify cert PDF prints "Plated to AMS 2404" | +| Pricing rules silently fail (no rule matches new keys) | Medium | Re-key existing rules to new model in same commit; add unit test that lookup returns expected price | +| Portal customer-facing flow breaks | Medium | Manual smoke test of portal quote flow before deploy | +| Recipe-thickness picker domain breaks (orphan thickness records) | Low | Drop fp.coating.thickness rows; recreate as fp.recipe.thickness during implementation | +| QC trigger filter (Sub 12 quality point) misses jobs | Medium | Test job creation triggers expected QC checks | +| Reports fail to render (missing field) | Low | Update all 4 plating reports in same commit; smoke test each PDF generation | + +**Rollback strategy:** if catastrophic, `git reset --hard backup/pre-spec-recipe-collapse-2026-05-14`. The backup branch is pushed to both GitHub and Gitea. + +--- + +## Implementation phases (high-level) + +The detailed implementation plan goes into a separate `writing-plans` artifact. High-level phases: + +### Phase 1 — Recipe model fields + thickness rename +- Add new fields to `fusion.plating.process.node` (recipe root) +- Rename `fp.coating.thickness` → `fp.recipe.thickness` +- Update views to surface new fields on recipe form +- Bump module version + +### Phase 2 — Customer Spec enhancements +- Add `recipe_ids` M2M to `fusion.plating.customer.spec` +- Add `print_on_cert` Boolean +- Update views (form, list, search) to surface recipe linkage +- Bump module version + +### Phase 3 — SO line + wizard rewrite +- Add `x_fc_customer_spec_id` to `sale.order.line` +- Add `x_fc_default_customer_spec_id` to `fp.part.catalog` +- Update SO line view + direct order wizard +- Auto-fill logic from part defaults +- Domain scoping (thickness depends on recipe) + +### Phase 4 — Pricing + quality point re-keying +- Add `customer_spec_id` + `recipe_id` to `fp.pricing.rule` +- Add `customer_spec_ids` + `recipe_ids` to `fp.quality.point` +- Update rule lookup logic in pricing engine +- Update quality.point trigger hooks + +### Phase 5 — Job + cert + reports refactor +- Drop `fp.job.coating_config_id` +- Update `fp.certificate._fp_create_certificates` to read from `customer_spec_id` +- Update all 4 plating reports +- Smart buttons on partner + recipe + +### Phase 6 — Portal updates +- Change portal coating picker → specification picker +- Update portal templates + JS +- Test portal quote flow end-to-end + +### Phase 7 — Removal +- Delete `fp.coating.config` model + view + data + ACL +- Delete `fp.treatment` model + view + data + ACL +- Delete `x_fc_coating_config_id` field on SO line, account.move.line, etc. +- Remove menu item +- Remove old data files from manifest +- Bump module version +- Final smoke test: full order entry → SO → job → tablet → QC → cert → invoice + +Each phase is a separate commit (or small set of commits) for clear rollback. + +--- + +## Success criteria + +The work is complete when: + +1. ✅ A new SO line on a brand-new DB shows ONLY two pickers (Specification + Recipe), each pre-fillable from part defaults +2. ✅ The aerospace specs (AMS 2404, BAC 5709, MIL-C-26074, etc.) appear in the Specification dropdown out of the box +3. ✅ Confirming an SO with a Mid-Phos recipe auto-creates a bake window; with a High-Phos recipe does not +4. ✅ Issuing a CoC prints "Plated to {spec.code} Rev {spec.revision}" derived from the SO line's specification +5. ✅ Pricing rule lookup returns a price based on the chosen spec + recipe combination +6. ✅ Quality point auto-spawn fires on jobs matching its `customer_spec_ids` / `recipe_ids` filters +7. ✅ Tank, bath, chemistry log, IoT, compliance, maintenance modules unchanged and unaffected +8. ✅ No `fp.coating.config` or `fp.treatment` references remain anywhere in the active codebase (grep returns zero results) +9. ✅ Reports (sale ack, WO sticker, job traveller, job sticker, CoC EN, CoC FR) all render correctly with spec + recipe data +10. ✅ Portal customer self-service quote flow completes end-to-end with the new Specification picker + +--- + +## Appendix A — Field reconciliation table + +What ends up where for every Coating Config field: + +| Coating Config field | Disposition | New location | +|---|---|---| +| `name` | Removed | (record itself removed) | +| `process_type_id` (single) | Removed | Already on customer.spec as M2M `process_type_ids` | +| `recipe_id` | Removed | Replaced by customer.spec.recipe_ids M2M | +| `phosphorus_level` | Moved | `fusion.plating.process.node.phosphorus_level` | +| `thickness_min`, `thickness_max`, `thickness_uom` | Moved | `fusion.plating.process.node.thickness_*` | +| `thickness_option_ids` | Re-parented | `fp.recipe.thickness` (was `fp.coating.thickness`) | +| `spec_reference` | Removed | Replaced by customer.spec.code + revision | +| `certification_level` | Removed | Replaced by customer.spec.spec_type + aerospace flags | +| `pre_treatment_ids`, `post_treatment_ids` | Removed | Already covered by recipe steps | +| `requires_bake_relief` | Moved | `fusion.plating.process.node.requires_bake_relief` | +| `bake_window_hours` | Moved | `fusion.plating.process.node.bake_window_hours` | +| `bake_temperature(_uom)` | Moved | `fusion.plating.process.node.bake_temperature(_uom)` | +| `bake_duration_hours` | Moved | `fusion.plating.process.node.bake_duration_hours` | +| `description` | Removed | Recipe already has description on the root node | +| `sequence`, `active` | Removed | (record itself removed) | +| `currency_id`, `default_cost` | Removed | Pricing logic moves to fp.pricing.rule with new keys | + +Every field accounted for. Nothing dropped silently.