Files
Odoo-Modules/fusion_plating/docs/superpowers/plans/2026-05-14-promote-customer-spec.md
gsinghpal 13fd0712d9 docs(fusion_plating): add Promote Customer Spec design + implementation plan
- Spec: retire fp.coating.config + fp.treatment, promote fusion.plating.customer.spec
- Two-picker SO line UX (Specification + Recipe), aerospace-correct audit posture
- Plan: 5 phases (foundation, SO line, pricing/quality/job/cert, reports/tablet/portal, removal)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:23:22 -04:00

2066 lines
80 KiB
Markdown

# 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
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
-->
<odoo>
<record id="view_fp_recipe_thickness_list" model="ir.ui.view">
<field name="name">fp.recipe.thickness.list</field>
<field name="model">fp.recipe.thickness</field>
<field name="arch" type="xml">
<list string="Thickness Options" decoration-muted="not active" editable="bottom">
<field name="sequence" widget="handle"/>
<field name="recipe_id"/>
<field name="value"/>
<field name="uom"/>
<field name="label" string="Display"/>
<field name="note" optional="hide"/>
<field name="active" optional="hide"/>
</list>
</field>
</record>
</odoo>
```
- [ ] **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 `<notebook>`), add a new page right before the closing `</notebook>` tag, visible only when the node is a recipe root:
```xml
<page name="spec_metadata" string="Specification &amp; Bake"
invisible="node_type != 'recipe' or parent_id">
<group>
<group string="Spec Metadata">
<field name="phosphorus_level"/>
<field name="thickness_min"/>
<field name="thickness_max"/>
<field name="thickness_uom"/>
</group>
<group string="Bake Relief (AMS 2759/9)">
<field name="requires_bake_relief"/>
<field name="bake_window_hours" invisible="not requires_bake_relief"/>
<field name="bake_temperature" invisible="not requires_bake_relief"/>
<field name="bake_temperature_uom" invisible="not requires_bake_relief"/>
<field name="bake_duration_hours" invisible="not requires_bake_relief"/>
</group>
</group>
<group string="Thickness Options">
<field name="thickness_option_ids" nolabel="1">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="value"/>
<field name="uom"/>
<field name="label" readonly="1"/>
<field name="note" optional="hide"/>
<field name="active" optional="hide"/>
</list>
</field>
</group>
<group string="Applicable Specifications">
<field name="applicable_spec_ids" nolabel="1" widget="many2many_tags"
options="{'no_create': True}"/>
</group>
</page>
```
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 `<sheet>` block. Locate the existing `process_type_ids` field. Right after the group that holds it, add:
```xml
<group string="Applicable Recipes">
<field name="recipe_ids" nolabel="1" widget="many2many_tags"
options="{'no_create_edit': True}"/>
</group>
<group>
<field name="print_on_cert"/>
</group>
```
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) <noreply@anthropic.com>
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 `<field name="x_fc_coating_config_id" .../>`, replace with:
```xml
<field name="x_fc_customer_spec_id"
options="{'no_create_edit': True}"/>
<field name="x_fc_coating_config_id" invisible="1"/>
```
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 `<field name="x_fc_thickness_id" .../>`. The current domain reads `[('coating_config_id', '=', x_fc_coating_config_id)]`. Change to:
```xml
<field name="x_fc_thickness_id"
domain="[('recipe_id', '=', x_fc_process_variant_id)]"
options="{'no_create_edit': True}"/>
```
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 `<field name="x_fc_default_coating_config_id" .../>` with:
```xml
<field name="x_fc_default_customer_spec_id"
options="{'no_create_edit': True}"/>
<field name="x_fc_default_coating_config_id" invisible="1"/>
```
Replace each `<field name="x_fc_default_treatment_ids" .../>` with `<field name="x_fc_default_treatment_ids" invisible="1"/>`.
### 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 `<field name="coating_config_id" .../>` and replace with:
```xml
<field name="customer_spec_id"
options="{'no_create_edit': True}"/>
<field name="coating_config_id" invisible="1"/>
```
### 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) <noreply@anthropic.com>
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 `<field name="coating_config_id" .../>` and replace with:
```xml
<field name="customer_spec_id"
options="{'no_create_edit': True}"/>
<field name="recipe_id"
options="{'no_create_edit': True}"/>
<field name="coating_config_id" invisible="1"/>
```
### 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 `<field name="coating_config_ids" .../>` and replace with:
```xml
<field name="customer_spec_ids" widget="many2many_tags"
options="{'no_create_edit': True}"/>
<field name="recipe_ids" widget="many2many_tags"
options="{'no_create_edit': True}"/>
<field name="coating_config_ids" invisible="1"/>
```
### 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) <noreply@anthropic.com>
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 `<t-if>` and `<span t-field>` patterns, replace:
```xml
<t t-if="doc.x_fc_part_catalog_id or doc.x_fc_coating_config_id or doc.x_fc_delivery_method">
```
```xml
<t t-if="doc.x_fc_part_catalog_id or doc.x_fc_customer_spec_id or doc.x_fc_delivery_method">
```
And:
```xml
<td class="text-center"><span t-field="doc.x_fc_coating_config_id"/></td>
```
```xml
<td class="text-center"><span t-field="doc.x_fc_customer_spec_id"/></td>
```
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
<t t-set="_coating" t-value="_coating or (_line and _line.x_fc_coating_config_id) or False"/>
```
```xml
<t t-set="_spec" t-value="_spec or (_line and _line.x_fc_customer_spec_id) or False"/>
<t t-set="_coating" t-value="_spec"/><!-- transitional alias; remove in Phase E -->
```
For each `t-set="_coating" t-value="line.x_fc_coating_config_id"`:
```xml
<t t-set="_spec" t-value="line.x_fc_customer_spec_id"/>
<t t-set="_coating" t-value="_spec"/>
```
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
<t t-if="so and so.x_fc_coating_config_id">
<span t-field="so.x_fc_coating_config_id"/>
</t>
```
Replace with:
```xml
<t t-if="so and so.x_fc_customer_spec_id">
<span t-field="so.x_fc_customer_spec_id"/>
</t>
```
### 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
<t t-if="'coating_config_id' in job._fields and job.coating_config_id">
<span t-esc="job.coating_config_id.name"/>
</t>
```
Replace with:
```xml
<t t-if="'customer_spec_id' in job._fields and job.customer_spec_id">
<span t-esc="job.customer_spec_id.display_name"/>
</t>
```
### 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
<t t-set="_coating" t-value="('coating_config_id' in job._fields and job.coating_config_id) or False"/>
```
Replace with:
```xml
<t t-set="_spec" t-value="('customer_spec_id' in job._fields and job.customer_spec_id) or False"/>
```
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
<span t-esc="cert.spec_reference"/>
```
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) <noreply@anthropic.com>
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 `<field name="x_fc_coating_config_id" invisible="1"/>` 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 `<field name="coating_config_id" invisible="1"/>` 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 `<field name="coating_config_ids" invisible="1"/>` 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
<menuitem id="menu_fp_coating_configs" .../>
```
Delete the entire `<menuitem>` element. Also delete the related `<record>` 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 `<menuitem>` element (and the related `<record>` 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) <noreply@anthropic.com>
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?**