- 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>
80 KiB
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
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:
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:
# -*- 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 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:
# ---- 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:
<page name="spec_metadata" string="Specification & 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:
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:
<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:
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:
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 = Truereveals 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:
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:
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:
@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:
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:
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:
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:
<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:
<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:
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:
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:
@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:
<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_idto wizard line model
In fp_direct_order_line.py, find the existing coating_config_id field declaration (around line 55). Right after it, add:
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:
# 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:
'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:
<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:
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:
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:
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):
@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:
<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:
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:
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):
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):
coating = getattr(job, 'coating_config_id', False) or False
Right next to it, add:
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:
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:
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:
<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:
# 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):
cfg = getattr(so, 'x_fc_coating_config_id', False)
Replace the surrounding spec_reference assembly with:
# 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):
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:
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:
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:
<t t-if="doc.x_fc_part_catalog_id or doc.x_fc_coating_config_id or doc.x_fc_delivery_method">
→
<t t-if="doc.x_fc_part_catalog_id or doc.x_fc_customer_spec_id or doc.x_fc_delivery_method">
And:
<td class="text-center"><span t-field="doc.x_fc_coating_config_id"/></td>
→
<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:
<t t-set="_coating" t-value="_coating or (_line and _line.x_fc_coating_config_id) or False"/>
→
<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":
<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:
<t t-if="so and so.x_fc_coating_config_id">
<span t-field="so.x_fc_coating_config_id"/>
</t>
Replace with:
<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:
<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:
<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
_coatingt-set blocks
Find each block:
<t t-set="_coating" t-value="('coating_config_id' in job._fields and job.coating_config_id) or False"/>
Replace with:
<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:
<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):
'part_catalog_id', 'coating_config_id',
Change to:
'part_catalog_id', 'customer_spec_id',
At line ~1554 (job lookup):
job.coating_config_id
if 'coating_config_id' in job._fields else False
Change to:
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:
coating_id = int(kw.get('coating_config_id', 0))
if coating_id:
session_data['coating_config_id'] = coating_id
With:
spec_id = int(kw.get('customer_spec_id', 0))
if spec_id:
session_data['customer_spec_id'] = spec_id
Replace:
coatings = request.env['fp.coating.config'].sudo().search(...)
With:
specs = request.env['fusion.plating.customer.spec'].sudo().search(
[('active', '=', True)],
order='spec_type, code, revision',
)
Replace lookup blocks:
coating = request.env['fp.coating.config'].sudo().browse(
session_data['coating_config_id'],
)
With:
spec = request.env['fusion.plating.customer.spec'].sudo().browse(
session_data['customer_spec_id'],
)
For pricing rule resolution (around line 267):
if rule.coating_config_id:
if rule.coating_config_id.id != coating.id:
continue
Change to:
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.specIdt-foreach="coatings"→t-foreach="specs"t-as="coating"→t-as="spec"coating.name→spec.display_name(orspec.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:
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:
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):
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:
coating = job.coating_config_id
These reads are likely for bake-relief logic. Replace with:
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:
- The SO-line
x_fc_coating_config_idis being deleted in Task E5 - The job's
coating_config_idwas deleted in Task E1 - 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_idfield
Find:
x_fc_coating_config_id = fields.Many2one(
'fp.coating.config', string='Primary Treatment',
)
Delete the entire block.
- Step 2: Delete
x_fc_treatment_idsfield
Find:
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_idfrom 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:
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_idfrom wizard line
In fp_direct_order_line.py, find:
coating_config_id = fields.Many2one(
'fp.coating.config',
string='Primary Treatment',
help='...',
)
Delete the entire block.
- Step 2: Delete
treatment_idsfrom wizard line
In the same file, find:
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:
@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:
@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_idfield from pricing rule
In fp_pricing_rule.py, delete:
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_idsfrom 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:
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:
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:
'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:
<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:
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:
'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:
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:
'fp.coating.thickness', string='Thickness',
Change to:
'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:
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:
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:
- Plating → Sales & Quoting → Quotations → New
- Add a customer (e.g. "Test Aerospace Inc")
- Add a line: pick a part with a default spec, verify Specification + Recipe pre-fill
- Confirm the order
- Open the resulting
fp.job(smart button on SO) - Verify
customer_spec_idis populated - Open the operator tablet (Plating → Shop Floor → Tablet)
- Walk through job → start → finish each step
- Mark the job done
- Issue a CoC (Plating → Quality → Certificates)
- Verify the CoC PDF prints "Spec: AMS 2404 Rev D" (or whatever the test spec was)
- Generate the invoice
- Verify the invoice line carries the spec
If any step fails, debug + fix before declaring Phase E complete.
- Step 4: Test the rare scenarios
-
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).
-
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"
-
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:
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:
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:
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:
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):
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:
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:
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_relis consistent betweenfusion.plating.customer.spec.recipe_ids(Task A5) andfusion.plating.process.node.applicable_spec_ids(Task A3) ✓ - Field name
customer_spec_id(singular M2O) used consistently onsale.order.line,account.move.line,fp.job,fp.pricing.rule,fp.certificate,fp.delivery✓ - M2M fields use
_idssuffix consistently (recipe_ids,customer_spec_ids,applicable_spec_ids,process_type_ids) ✓ fp.recipe.thickness.recipe_idmatches the inversefusion.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.
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?