diff --git a/fusion_plating/fusion_plating/__manifest__.py b/fusion_plating/fusion_plating/__manifest__.py index c7feda08..8b4b26a3 100644 --- a/fusion_plating/fusion_plating/__manifest__.py +++ b/fusion_plating/fusion_plating/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating', - 'version': '19.0.18.15.16', + 'version': '19.0.19.0.0', 'category': 'Manufacturing/Plating', 'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.', 'description': """ @@ -99,6 +99,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved. 'views/fp_facility_views.xml', 'views/fp_bath_views.xml', 'views/fp_process_node_views.xml', + 'views/fp_recipe_thickness_views.xml', # Sub 14b — fp.step.kind catalog. MUST load before # fp_step_template_data.xml (templates reference kinds via # kind_id) AND before fp_step_template_views.xml (the form diff --git a/fusion_plating/fusion_plating/models/__init__.py b/fusion_plating/fusion_plating/models/__init__.py index 2f6476c0..8ba16e5e 100644 --- a/fusion_plating/fusion_plating/models/__init__.py +++ b/fusion_plating/fusion_plating/models/__init__.py @@ -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 diff --git a/fusion_plating/fusion_plating/models/fp_process_node.py b/fusion_plating/fusion_plating/models/fp_process_node.py index be509735..64ea164a 100644 --- a/fusion_plating/fusion_plating/models/fp_process_node.py +++ b/fusion_plating/fusion_plating/models/fp_process_node.py @@ -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( diff --git a/fusion_plating/fusion_plating/models/fp_recipe_thickness.py b/fusion_plating/fusion_plating/models/fp_recipe_thickness.py new file mode 100644 index 00000000..a3c3d1a5 --- /dev/null +++ b/fusion_plating/fusion_plating/models/fp_recipe_thickness.py @@ -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] diff --git a/fusion_plating/fusion_plating/security/ir.model.access.csv b/fusion_plating/fusion_plating/security/ir.model.access.csv index f8abd520..c085819d 100644 --- a/fusion_plating/fusion_plating/security/ir.model.access.csv +++ b/fusion_plating/fusion_plating/security/ir.model.access.csv @@ -94,3 +94,6 @@ access_fp_job_step_move_manager,fp.job.step.move.manager,model_fp_job_step_move, access_fp_job_step_move_input_value_operator,fp.job.step.move.input.value.operator,model_fp_job_step_move_input_value,group_fusion_plating_operator,1,1,1,0 access_fp_job_step_move_input_value_supervisor,fp.job.step.move.input.value.supervisor,model_fp_job_step_move_input_value,group_fusion_plating_supervisor,1,1,1,0 access_fp_job_step_move_input_value_manager,fp.job.step.move.input.value.manager,model_fp_job_step_move_input_value,group_fusion_plating_manager,1,1,1,1 +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,group_fusion_plating_supervisor,1,1,1,0 +access_fp_recipe_thickness_manager,fp.recipe.thickness.manager,model_fp_recipe_thickness,group_fusion_plating_manager,1,1,1,1 diff --git a/fusion_plating/fusion_plating/views/fp_process_node_views.xml b/fusion_plating/fusion_plating/views/fp_process_node_views.xml index 6204712a..6d976735 100644 --- a/fusion_plating/fusion_plating/views/fp_process_node_views.xml +++ b/fusion_plating/fusion_plating/views/fp_process_node_views.xml @@ -226,6 +226,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fusion_plating/fusion_plating/views/fp_recipe_thickness_views.xml b/fusion_plating/fusion_plating/views/fp_recipe_thickness_views.xml new file mode 100644 index 00000000..1630bd93 --- /dev/null +++ b/fusion_plating/fusion_plating/views/fp_recipe_thickness_views.xml @@ -0,0 +1,25 @@ + + + + + + fp.recipe.thickness.list + fp.recipe.thickness + + + + + + + + + + + + + + diff --git a/fusion_plating/fusion_plating_quality/__manifest__.py b/fusion_plating/fusion_plating_quality/__manifest__.py index 2ca3ccb3..f034de0e 100644 --- a/fusion_plating/fusion_plating_quality/__manifest__.py +++ b/fusion_plating/fusion_plating_quality/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Quality (QMS)', - 'version': '19.0.4.14.0', + 'version': '19.0.5.0.0', 'category': 'Manufacturing/Plating', 'summary': 'Native QMS for plating shops: NCR, CAPA, calibration, AVL, FAIR, ' 'internal audits, customer specs, document control. CE + EE compatible.', @@ -90,6 +90,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved. 'views/fp_calibration_views.xml', 'views/fp_avl_views.xml', 'views/fp_customer_spec_views.xml', + 'views/fp_process_node_inherit_views.xml', 'views/fp_audit_views.xml', 'views/fp_fair_views.xml', 'views/fp_doc_control_views.xml', diff --git a/fusion_plating/fusion_plating_quality/models/__init__.py b/fusion_plating/fusion_plating_quality/models/__init__.py index c7322d38..f5e358d8 100644 --- a/fusion_plating/fusion_plating_quality/models/__init__.py +++ b/fusion_plating/fusion_plating_quality/models/__init__.py @@ -9,6 +9,7 @@ from . import fp_calibration from . import fp_calibration_event from . import fp_avl from . import fp_customer_spec +from . import fp_process_node_inherit from . import fp_audit from . import fp_fair from . import fp_doc_control diff --git a/fusion_plating/fusion_plating_quality/models/fp_customer_spec.py b/fusion_plating/fusion_plating_quality/models/fp_customer_spec.py index cd6ff587..f5b4057b 100644 --- a/fusion_plating/fusion_plating_quality/models/fp_customer_spec.py +++ b/fusion_plating/fusion_plating_quality/models/fp_customer_spec.py @@ -74,6 +74,22 @@ class FpCustomerSpec(models.Model): notes = fields.Html( string='Notes', ) + 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.', + ) company_id = fields.Many2one( 'res.company', string='Company', diff --git a/fusion_plating/fusion_plating_quality/models/fp_process_node_inherit.py b/fusion_plating/fusion_plating_quality/models/fp_process_node_inherit.py new file mode 100644 index 00000000..5563224f --- /dev/null +++ b/fusion_plating/fusion_plating_quality/models/fp_process_node_inherit.py @@ -0,0 +1,26 @@ +# -*- 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 fields, models + + +class FusionPlatingProcessNode(models.Model): + """Add the reverse M2M from recipe → applicable specifications. + + The forward M2M lives on fusion.plating.customer.spec.recipe_ids. + Defined here (in the quality module) because customer.spec is owned + by quality and core can't reference it without a circular dep. + """ + _inherit = 'fusion.plating.process.node' + + 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.', + ) diff --git a/fusion_plating/fusion_plating_quality/views/fp_customer_spec_views.xml b/fusion_plating/fusion_plating_quality/views/fp_customer_spec_views.xml index 65e575de..a832e40a 100644 --- a/fusion_plating/fusion_plating_quality/views/fp_customer_spec_views.xml +++ b/fusion_plating/fusion_plating_quality/views/fp_customer_spec_views.xml @@ -50,6 +50,13 @@ + + + + + + diff --git a/fusion_plating/fusion_plating_quality/views/fp_process_node_inherit_views.xml b/fusion_plating/fusion_plating_quality/views/fp_process_node_inherit_views.xml new file mode 100644 index 00000000..0308f419 --- /dev/null +++ b/fusion_plating/fusion_plating_quality/views/fp_process_node_inherit_views.xml @@ -0,0 +1,29 @@ + + + + + + fusion.plating.process.node.form.quality.inherit + fusion.plating.process.node + + + + + + + + + + +