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 `