feat(promote-customer-spec): Phase A — recipe + spec foundation
- Add fp.recipe.thickness model (replaces fp.coating.thickness, scoped to recipe root)
- Add spec metadata + bake-relief fields to fusion.plating.process.node (recipe root):
phosphorus_level, thickness_min/max/uom, thickness_option_ids,
requires_bake_relief + bake_window_hours/temperature/duration
- Add recipe_ids M2M + print_on_cert to fusion.plating.customer.spec
- Add applicable_spec_ids reverse M2M as inherit in fusion_plating_quality
(avoids circular dep — core can't reference customer.spec which lives in quality)
- Surface new fields on recipe form ("Specification & Bake" notebook page)
- Surface recipe linkage on customer spec form
Pure additive. Foundation for Phases B-E.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ from . import fp_bath_log_line
|
||||
from . import fp_bath_parameter
|
||||
from . import fp_bath_replenishment_rule
|
||||
from . import fp_process_node
|
||||
from . import fp_recipe_thickness
|
||||
from . import fp_rack
|
||||
from . import fp_job
|
||||
from . import fp_job_step
|
||||
|
||||
@@ -336,6 +336,68 @@ class FpProcessNode(models.Model):
|
||||
# NB. `pricing_rule_ids` lives in fusion_plating_configurator
|
||||
# (added there so this core module doesn't depend on the configurator).
|
||||
|
||||
# ---- Spec-derived metadata (recipe-root only — Promote Customer Spec) ----
|
||||
# These were on fp.coating.config (since retired). They describe the
|
||||
# PROCESS the recipe runs, not the customer-facing specification —
|
||||
# specs live on fusion.plating.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.',
|
||||
)
|
||||
|
||||
# NB. `applicable_spec_ids` (reverse of customer.spec.recipe_ids) is
|
||||
# defined as an inherit in fusion_plating_quality (the module that
|
||||
# owns fusion.plating.customer.spec). Core can't reference it
|
||||
# directly without a dependency inversion.
|
||||
|
||||
# ---- Computed fields -----------------------------------------------------
|
||||
|
||||
display_name = fields.Char(
|
||||
|
||||
54
fusion_plating/fusion_plating/models/fp_recipe_thickness.py
Normal file
54
fusion_plating/fusion_plating/models/fp_recipe_thickness.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# -*- 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]
|
||||
Reference in New Issue
Block a user