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

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 &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:

    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 = 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:

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_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:

    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.019.0.19.1.0

  • fusion_plating_quality: 19.0.5.0.019.0.5.1.0

  • fusion_plating_jobs: 19.0.8.27.019.0.9.0.0

  • fusion_plating_certificates: 19.0.5.6.019.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 _coating t-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 coatingsspecs and coatingspec 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.coatingIddataset.specId
  • t-foreach="coatings"t-foreach="specs"
  • t-as="coating"t-as="spec"
  • coating.namespec.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.019.0.11.0.0

  • fusion_plating_jobs: 19.0.9.0.019.0.9.1.0

  • fusion_plating_shopfloor: 19.0.25.2.119.0.26.0.0

  • fusion_plating_portal: 19.0.2.1.119.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:

  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:

    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:

    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:

    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:

    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:

    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_id field 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_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:

        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.019.0.20.0.0

  • fusion_plating_jobs: 19.0.9.1.019.0.10.0.0

  • fusion_plating_quality: 19.0.5.1.019.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:

  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:

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_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.

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?