From 152ed86c3a7f2162893d664cf7510aa5e7a5a320 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Fri, 15 May 2026 08:54:40 -0400 Subject: [PATCH] =?UTF-8?q?feat(thickness):=20single=20Char=20range=20fiel?= =?UTF-8?q?d=20=E2=80=94=20drop=20fp.recipe.thickness=20picker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per client direction: every order is a thickness RANGE (e.g. "0.0005-0.0008 mils" or "5-10 mils"), never a single value. The old picker model (fp.recipe.thickness with a single 'value' Float) was modelling the wrong concept and overcrowding the order entry UI. Replaced with one free-text Char field that auto-fills from last-used or part default. DELETED entirely: - fp.recipe.thickness model (file + view + ACL + manifest entry) - recipe.thickness_option_ids One2many (the picker source) - "Thickness Options" inline list on the recipe form - sale.order.line.x_fc_thickness_id (M2O picker) - account.move.line.x_fc_thickness_id - fp.delivery.x_fc_thickness_id - fp.direct.order.line.thickness_id ADDED: - sale.order.line.x_fc_thickness_range (Char) — operator types range - account.move.line.x_fc_thickness_range — for invoice rendering - fp.delivery.x_fc_thickness_range — for packing slip - fp.direct.order.line.thickness_range — for the wizard - fp.part.catalog.x_fc_default_thickness_range — part default AUTO-FILL CHAIN (sale.order.line + wizard line): 1. Operator already typed → keep 2. Most recent SO line for (this part, this customer) with a non-empty thickness_range → copy that 3. part.x_fc_default_thickness_range → copy 4. Blank — operator types Implemented as both an @api.onchange (interactive) AND a create() override (programmatic — wizard, sale_mrp bridge, imports). Same logic in both paths. WIZARD push-to-defaults: when "Save as Default" toggle is ticked on a wizard line, persist the line's thickness_range to part.x_fc_default_thickness_range so future first-customer orders get a sensible starting point. REPORTS: customer_line_header.xml + report_fp_wo_sticker.xml now print the Char range as-typed (no display_name lookup needed). KEPT (admin documentation only — doesn't affect order entry): - recipe.thickness_min, thickness_max, thickness_uom on the recipe root: documents the recipe's CAPABILITY range. No UI gate; just for spec authors to record what the chemistry can produce. JOB GROUPING: fp.job auto-create groups SO lines by (recipe, part, spec, thickness, serial). Updated to key on the thickness_range Char (stripped) instead of the deleted thickness_id integer. DB cleanup: --update=base ran on the upgrade, dropping the fp_recipe_thickness table + the four x_fc_thickness_id columns. Existing data was already nulled in earlier dev work. Co-Authored-By: Claude Opus 4.7 (1M context) --- fusion_plating/fusion_plating/__manifest__.py | 3 +- .../fusion_plating/models/__init__.py | 1 - .../fusion_plating/models/fp_process_node.py | 11 +-- .../models/fp_recipe_thickness.py | 54 ------------ .../security/ir.model.access.csv | 3 - .../views/fp_process_node_views.xml | 20 ++--- .../views/fp_recipe_thickness_views.xml | 25 ------ .../__manifest__.py | 2 +- .../models/account_move_line.py | 5 +- .../models/fp_part_catalog.py | 8 ++ .../models/sale_order_line.py | 82 +++++++++++++++---- .../views/fp_part_catalog_views.xml | 13 ++- .../views/sale_order_views.xml | 9 +- .../wizard/fp_direct_order_line.py | 42 ++++++++-- .../wizard/fp_direct_order_wizard.py | 11 ++- .../wizard/fp_direct_order_wizard_views.xml | 7 +- .../fusion_plating_jobs/__manifest__.py | 2 +- .../fusion_plating_jobs/models/sale_order.py | 8 +- .../fusion_plating_logistics/__manifest__.py | 2 +- .../models/fp_delivery.py | 6 +- .../fusion_plating_reports/__manifest__.py | 2 +- .../report/customer_line_header.xml | 4 +- .../report/report_fp_wo_sticker.xml | 13 ++- 23 files changed, 164 insertions(+), 169 deletions(-) delete mode 100644 fusion_plating/fusion_plating/models/fp_recipe_thickness.py delete mode 100644 fusion_plating/fusion_plating/views/fp_recipe_thickness_views.xml diff --git a/fusion_plating/fusion_plating/__manifest__.py b/fusion_plating/fusion_plating/__manifest__.py index ad130ac5..a72b3d29 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.19.3.0', + 'version': '19.0.20.0.0', 'category': 'Manufacturing/Plating', 'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.', 'description': """ @@ -99,7 +99,6 @@ 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 8ba16e5e..2f6476c0 100644 --- a/fusion_plating/fusion_plating/models/__init__.py +++ b/fusion_plating/fusion_plating/models/__init__.py @@ -18,7 +18,6 @@ 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 e8543350..cd4627dc 100644 --- a/fusion_plating/fusion_plating/models/fp_process_node.py +++ b/fusion_plating/fusion_plating/models/fp_process_node.py @@ -357,13 +357,10 @@ class FpProcessNode(models.Model): [('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.', - ) + # thickness_option_ids removed — fp.recipe.thickness model deleted. + # Thickness on the SO line is now a free-text Char range (e.g. + # "0.0005-0.0008 mils") that auto-fills from last-used per + # (part, customer) or the part's x_fc_default_thickness_range. # ---- Bake relief — AMS 2759/9 hydrogen embrittlement (recipe root) ---- requires_bake_relief = fields.Boolean( diff --git a/fusion_plating/fusion_plating/models/fp_recipe_thickness.py b/fusion_plating/fusion_plating/models/fp_recipe_thickness.py deleted file mode 100644 index a3c3d1a5..00000000 --- a/fusion_plating/fusion_plating/models/fp_recipe_thickness.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- 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 c085819d..f8abd520 100644 --- a/fusion_plating/fusion_plating/security/ir.model.access.csv +++ b/fusion_plating/fusion_plating/security/ir.model.access.csv @@ -94,6 +94,3 @@ 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 68120e47..2dcf6e4a 100644 --- a/fusion_plating/fusion_plating/views/fp_process_node_views.xml +++ b/fusion_plating/fusion_plating/views/fp_process_node_views.xml @@ -255,18 +255,14 @@ - - - - - - - - - - - - + + diff --git a/fusion_plating/fusion_plating/views/fp_recipe_thickness_views.xml b/fusion_plating/fusion_plating/views/fp_recipe_thickness_views.xml deleted file mode 100644 index 1630bd93..00000000 --- a/fusion_plating/fusion_plating/views/fp_recipe_thickness_views.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - fp.recipe.thickness.list - fp.recipe.thickness - - - - - - - - - - - - - - diff --git a/fusion_plating/fusion_plating_configurator/__manifest__.py b/fusion_plating/fusion_plating_configurator/__manifest__.py index 5914d6c7..2dcf4ae3 100644 --- a/fusion_plating/fusion_plating_configurator/__manifest__.py +++ b/fusion_plating/fusion_plating_configurator/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Configurator', - 'version': '19.0.20.1.0', + 'version': '19.0.21.0.0', 'category': 'Manufacturing/Plating', 'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.', 'description': """ diff --git a/fusion_plating/fusion_plating_configurator/models/account_move_line.py b/fusion_plating/fusion_plating_configurator/models/account_move_line.py index 85ed3424..f7470408 100644 --- a/fusion_plating/fusion_plating_configurator/models/account_move_line.py +++ b/fusion_plating/fusion_plating_configurator/models/account_move_line.py @@ -65,10 +65,9 @@ class AccountMoveLine(models.Model): string='Job #', index=True, help='Copied from sale.order.line.', ) - x_fc_thickness_id = fields.Many2one( - 'fp.recipe.thickness', + x_fc_thickness_range = fields.Char( string='Thickness', - help='Copied from sale.order.line for customer-facing invoice PDFs.', + help='Carried from the SO line — prints on the invoice PDF.', ) # x_fc_customer_spec_id added by fusion_plating_quality. x_fc_revision_snapshot = fields.Char( diff --git a/fusion_plating/fusion_plating_configurator/models/fp_part_catalog.py b/fusion_plating/fusion_plating_configurator/models/fp_part_catalog.py index a233dd89..d18d22a0 100644 --- a/fusion_plating/fusion_plating_configurator/models/fp_part_catalog.py +++ b/fusion_plating/fusion_plating_configurator/models/fp_part_catalog.py @@ -279,6 +279,14 @@ class FpPartCatalog(models.Model): # ---- Direct-order defaults (Phase C — C4) ---- # x_fc_default_customer_spec_id added by fusion_plating_quality. # Legacy default_coating_config_id + default_treatment_ids removed. + x_fc_default_thickness_range = fields.Char( + string='Default Thickness', + help='Default thickness range as free text (e.g. "0.0005-0.0008 mils" ' + 'or "5-10 mils"). Pre-fills the thickness on new sale order ' + 'lines for this part — falls back when no recent order for ' + 'the same (part, customer) pair exists. Updated when the ' + 'wizard\'s "Save as Default" toggle is ticked.', + ) # Substrate density mapping (g/cm³) for material weight calculation _SUBSTRATE_DENSITY = { diff --git a/fusion_plating/fusion_plating_configurator/models/sale_order_line.py b/fusion_plating/fusion_plating_configurator/models/sale_order_line.py index 38640416..11585489 100644 --- a/fusion_plating/fusion_plating_configurator/models/sale_order_line.py +++ b/fusion_plating/fusion_plating_configurator/models/sale_order_line.py @@ -304,12 +304,13 @@ class SaleOrderLine(models.Model): help='Shop-floor reference for this line. Auto-sequenced on sale ' 'order confirmation; editable. Blank is allowed.', ) - x_fc_thickness_id = fields.Many2one( - 'fp.recipe.thickness', + x_fc_thickness_range = fields.Char( string='Thickness', - ondelete='set null', - domain="[('recipe_id', '=', x_fc_process_variant_id)]", - help="Target thickness. Options come from the line's recipe.", + help='Target thickness range as the operator types it, e.g. ' + '"0.0005-0.0008 mils" or "5-10 mils". Free-form text — ' + 'auto-fills from the last order for this (part, customer) ' + 'pair, falling back to the part\'s default range. Prints ' + 'verbatim on the cert, packing slip, and invoice.', ) x_fc_revision_snapshot = fields.Char( string='Revision (snapshot)', @@ -399,6 +400,32 @@ class SaleOrderLine(models.Model): part = Part.browse(vals['x_fc_part_catalog_id']).exists() if part and part.revision: vals['x_fc_revision_snapshot'] = part.revision + + # Auto-fill thickness range — same logic as the onchange but + # for programmatic creators (wizard, sale_mrp, imports). + # Resolution: explicit > last-used (part, partner) > part default. + if (not vals.get('x_fc_thickness_range') + and vals.get('x_fc_part_catalog_id')): + part = Part.browse(vals['x_fc_part_catalog_id']).exists() + if part: + # Need partner_id from the parent order + partner_id = False + if vals.get('order_id'): + order = self.env['sale.order'].browse(vals['order_id']).exists() + if order: + partner_id = order.partner_id.id + if partner_id: + recent = self.search([ + ('x_fc_part_catalog_id', '=', part.id), + ('order_id.partner_id', '=', partner_id), + ('x_fc_thickness_range', '!=', False), + ('x_fc_thickness_range', '!=', ''), + ], order='create_date desc', limit=1) + if recent: + vals['x_fc_thickness_range'] = recent.x_fc_thickness_range + if (not vals.get('x_fc_thickness_range') + and getattr(part, 'x_fc_default_thickness_range', None)): + vals['x_fc_thickness_range'] = part.x_fc_default_thickness_range lines = super().create(vals_list) lines._fp_apply_recipe_polish() return lines @@ -473,8 +500,8 @@ class SaleOrderLine(models.Model): vals['x_fc_serial_id'] = self.x_fc_serial_id.id if self.x_fc_job_number: vals['x_fc_job_number'] = self.x_fc_job_number - if self.x_fc_thickness_id: - vals['x_fc_thickness_id'] = self.x_fc_thickness_id.id + if self.x_fc_thickness_range: + vals['x_fc_thickness_range'] = self.x_fc_thickness_range if self.x_fc_revision_snapshot: vals['x_fc_revision_snapshot'] = self.x_fc_revision_snapshot # x_fc_customer_spec_id carry-over is handled by an @@ -576,18 +603,41 @@ class SaleOrderLine(models.Model): 'target': 'current', } - @api.onchange('x_fc_process_variant_id') - def _onchange_recipe_clears_thickness(self): - """Clear the thickness picker when recipe changes. + @api.onchange('x_fc_part_catalog_id') + def _onchange_part_default_thickness(self): + """Auto-fill thickness range from last-used or part default. - Thickness options are scoped to the recipe; a value carried over - from a previous recipe would fail its domain. + Resolution order (first match wins): + 1. Operator already typed a value → keep + 2. Most recent SO line for (this part, this customer) with a + non-empty thickness_range → copy that + 3. Part's x_fc_default_thickness_range → copy + 4. Blank — operator types """ 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 + if line.x_fc_thickness_range: + continue + if not line.x_fc_part_catalog_id: + continue + partner = line.order_id.partner_id + # 2. Last-used for (part, customer) + if partner: + recent = self.env['sale.order.line'].search([ + ('x_fc_part_catalog_id', '=', line.x_fc_part_catalog_id.id), + ('order_id.partner_id', '=', partner.id), + ('x_fc_thickness_range', '!=', False), + ('x_fc_thickness_range', '!=', ''), + ('id', '!=', line.id or 0), + ], order='create_date desc', limit=1) + if recent: + line.x_fc_thickness_range = recent.x_fc_thickness_range + continue + # 3. Part default + part_default = getattr( + line.x_fc_part_catalog_id, 'x_fc_default_thickness_range', None, + ) + if part_default: + line.x_fc_thickness_range = part_default def action_generate_serial(self): """Generate one new auto-sequenced serial and append it to the M2M. diff --git a/fusion_plating/fusion_plating_configurator/views/fp_part_catalog_views.xml b/fusion_plating/fusion_plating_configurator/views/fp_part_catalog_views.xml index 69c04694..cc729ce2 100644 --- a/fusion_plating/fusion_plating_configurator/views/fp_part_catalog_views.xml +++ b/fusion_plating/fusion_plating_configurator/views/fp_part_catalog_views.xml @@ -203,11 +203,16 @@ + + + +

- Set a Default Specification on this part - (under the section added by the Quality - module) so future direct-order lines - pre-fill it automatically. + Defaults pre-fill new direct-order lines + for this part. Thickness also auto-fills + from the most recent order for the same + (part, customer) pair when one exists.

diff --git a/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml b/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml index ee772e4a..efa61122 100644 --- a/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml +++ b/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml @@ -118,7 +118,7 @@ readonly="1"> - + @@ -260,11 +260,8 @@ widget="boolean_toggle" invisible="not x_fc_process_variant_id" optional="hide"/> - - Serial: - +
- Thickness: + Thickness:
diff --git a/fusion_plating/fusion_plating_reports/report/report_fp_wo_sticker.xml b/fusion_plating/fusion_plating_reports/report/report_fp_wo_sticker.xml index 52b624c0..3efdb032 100644 --- a/fusion_plating/fusion_plating_reports/report/report_fp_wo_sticker.xml +++ b/fusion_plating/fusion_plating_reports/report/report_fp_wo_sticker.xml @@ -87,14 +87,11 @@ - - - + + +