diff --git a/fusion_plating/fusion_plating_certificates/__manifest__.py b/fusion_plating/fusion_plating_certificates/__manifest__.py index 9ba6bd6a..908d6d1a 100644 --- a/fusion_plating/fusion_plating_certificates/__manifest__.py +++ b/fusion_plating/fusion_plating_certificates/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Certificates', - 'version': '19.0.5.6.0', + 'version': '19.0.6.0.0', 'category': 'Manufacturing/Plating', 'summary': 'Certificate registry for CoC, thickness reports, and quality documents.', 'description': """ diff --git a/fusion_plating/fusion_plating_certificates/models/fp_certificate.py b/fusion_plating/fusion_plating_certificates/models/fp_certificate.py index 03e16a5a..141bb1d2 100644 --- a/fusion_plating/fusion_plating_certificates/models/fp_certificate.py +++ b/fusion_plating/fusion_plating_certificates/models/fp_certificate.py @@ -286,14 +286,27 @@ class FpCertificate(models.Model): def create(self, vals_list): SaleOrder = self.env['sale.order'] for vals in vals_list: - # Spec-limit auto-fill (existing behaviour, preserved). + # Spec-limit auto-fill — sources thickness range from the + # recipe (Phase A moved the thickness fields onto the + # recipe root). Falls back gracefully when the SO has no + # recipe-bearing line. already_set = vals.get('spec_min_mils') or vals.get('spec_max_mils') if not already_set and vals.get('sale_order_id'): so = SaleOrder.browse(vals['sale_order_id']) - cfg = getattr(so, 'x_fc_coating_config_id', False) - if cfg and cfg.thickness_uom == 'mils': - vals.setdefault('spec_min_mils', cfg.thickness_min or 0.0) - vals.setdefault('spec_max_mils', cfg.thickness_max or 0.0) + # Look across order_line for the first recipe with a + # populated thickness range. + first_line = so.order_line[:1] if so.order_line else False + recipe = ( + first_line.x_fc_process_variant_id + if (first_line + and 'x_fc_process_variant_id' in first_line._fields) + else False + ) + if (recipe + and 'thickness_uom' in recipe._fields + and recipe.thickness_uom == 'mils'): + vals.setdefault('spec_min_mils', recipe.thickness_min or 0.0) + vals.setdefault('spec_max_mils', recipe.thickness_max or 0.0) # Defer naming: let the record exist so the mixin can write # name via raw SQL, then fall back to the legacy sequence if # no parent SO is reachable. diff --git a/fusion_plating/fusion_plating_configurator/__init__.py b/fusion_plating/fusion_plating_configurator/__init__.py index a5c84cd0..39cece35 100644 --- a/fusion_plating/fusion_plating_configurator/__init__.py +++ b/fusion_plating/fusion_plating_configurator/__init__.py @@ -21,8 +21,6 @@ def _backfill_currency(env): return for model_name in ( 'fp.pricing.rule', - 'fp.treatment', - 'fp.customer.price.list', 'fp.quote.configurator', ): Model = env.get(model_name) diff --git a/fusion_plating/fusion_plating_configurator/__manifest__.py b/fusion_plating/fusion_plating_configurator/__manifest__.py index 935ec8f2..31a29b44 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.19.0.0', + 'version': '19.0.20.0.0', 'category': 'Manufacturing/Plating', 'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.', 'description': """ @@ -39,16 +39,11 @@ Provides: 'security/ir.model.access.csv', 'data/fp_configurator_sequence_data.xml', 'data/fp_sub5_sequence_data.xml', - 'data/fp_treatment_data.xml', 'data/fp_part_material_data.xml', - 'views/fp_treatment_views.xml', 'views/fp_part_material_views.xml', - 'views/fp_coating_thickness_views.xml', 'views/fp_part_catalog_views.xml', 'views/fp_process_node_part_scoped_views.xml', - 'views/fp_coating_config_views.xml', 'views/fp_pricing_rule_views.xml', - 'views/fp_customer_price_list_views.xml', 'views/fp_quote_configurator_views.xml', 'views/sale_order_views.xml', 'views/res_partner_views.xml', diff --git a/fusion_plating/fusion_plating_configurator/data/fp_treatment_data.xml b/fusion_plating/fusion_plating_configurator/data/fp_treatment_data.xml deleted file mode 100644 index 245356e9..00000000 --- a/fusion_plating/fusion_plating_configurator/data/fp_treatment_data.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - Alkaline Clean - pre - 10 - 15 - - - Acid Etch - pre - 20 - 10 - - - Zincate (Aluminium) - pre - 30 - 5 - - - Bead Blast - pre - 40 - 20 - - - Solvent Degrease - pre - 50 - 10 - - - - - Hydrogen Embrittlement Bake - post - 10 - 240 - - - Passivate - post - 20 - 30 - - - Chromate Seal - post - 30 - 15 - - - diff --git a/fusion_plating/fusion_plating_configurator/models/__init__.py b/fusion_plating/fusion_plating_configurator/models/__init__.py index 849d8a79..95789dbe 100644 --- a/fusion_plating/fusion_plating_configurator/models/__init__.py +++ b/fusion_plating/fusion_plating_configurator/models/__init__.py @@ -3,14 +3,10 @@ # License OPL-1 (Odoo Proprietary License v1.0) # Part of the Fusion Plating product family. -from . import fp_treatment from . import fp_part_material from . import fp_part_catalog -from . import fp_coating_thickness -from . import fp_coating_config from . import fp_pricing_complexity_surcharge from . import fp_pricing_rule -from . import fp_customer_price_list from . import fp_sale_description_template from . import fp_quote_configurator from . import fp_serial 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 86994560..85ed3424 100644 --- a/fusion_plating/fusion_plating_configurator/models/account_move_line.py +++ b/fusion_plating/fusion_plating_configurator/models/account_move_line.py @@ -70,8 +70,7 @@ class AccountMoveLine(models.Model): string='Thickness', help='Copied from sale.order.line for customer-facing invoice PDFs.', ) - # x_fc_customer_spec_id is added by fusion_plating_quality (where - # fusion.plating.customer.spec lives). + # x_fc_customer_spec_id added by fusion_plating_quality. x_fc_revision_snapshot = fields.Char( string='Revision (snapshot)', help='Revision letter from the source SO line.', diff --git a/fusion_plating/fusion_plating_configurator/models/fp_coating_config.py b/fusion_plating/fusion_plating_configurator/models/fp_coating_config.py deleted file mode 100644 index ef798622..00000000 --- a/fusion_plating/fusion_plating_configurator/models/fp_coating_config.py +++ /dev/null @@ -1,91 +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 fields, models - - -class FpCoatingConfig(models.Model): - """Coating configuration template. - - Defines a specific coating setup: process type, phosphorus level, - thickness range, spec reference, and required pre/post treatments. - Used by the configurator to drive pricing and recipe selection. - """ - _name = 'fp.coating.config' - _description = 'Fusion Plating — Coating Configuration' - _order = 'sequence, name' - - name = fields.Char(string='Configuration', required=True, help='e.g. "EN Mid-Phos AMS 2404"') - process_type_id = fields.Many2one( - 'fusion.plating.process.type', string='Process Type', required=True, ondelete='restrict', - ) - recipe_id = fields.Many2one( - 'fusion.plating.process.node', string='Default Recipe', - domain="[('node_type', '=', 'recipe')]", - help='Default recipe template for this coating configuration.', - ) - 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.', - ) - 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.coating.thickness', - 'coating_config_id', - string='Thickness Options', - help='Discrete thickness values the estimator can pick from when ' - 'this coating appears on a sale order line. Each value is ' - 'driven by the spec the coating is built against. Sub 5.', - ) - spec_reference = fields.Char(string='Spec Reference', help='e.g. "AMS 2404", "E499-303-00-005"') - certification_level = fields.Selection( - [('commercial', 'Commercial'), ('mil_spec', 'Mil-Spec'), - ('nadcap', 'Nadcap'), ('nuclear', 'Nuclear (CSA N299)')], - string='Certification Level', default='commercial', - ) - pre_treatment_ids = fields.Many2many( - 'fp.treatment', 'fp_coating_config_pre_treatment_rel', 'config_id', 'treatment_id', - string='Pre-Treatments', domain="[('treatment_type', '=', 'pre')]", - ) - post_treatment_ids = fields.Many2many( - 'fp.treatment', 'fp_coating_config_post_treatment_rel', 'config_id', 'treatment_id', - string='Post-Treatments', domain="[('treatment_type', '=', 'post')]", - ) - - # ---- Hydrogen embrittlement relief (AMS 2759/9) ---- - requires_bake_relief = fields.Boolean( - string='Requires Bake Relief', - help='Hydrogen embrittlement relief bake required (high-strength steel, ' - 'Rockwell C ≥ 31). When set, finishing the plating WO 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. Typically 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). Unit follows bake_temperature_uom.', - ) - bake_temperature_uom = fields.Selection( - [('F', '°F'), ('C', '°C')], - string='Temp Unit', - default=lambda self: self.env.company.x_fc_default_temp_uom or 'F', - ) - bake_duration_hours = fields.Float( - string='Bake Duration (hours)', default=23.0, - help='Minimum bake hold time at temperature. Typical: 23h.', - ) - - sequence = fields.Integer(string='Sequence', default=10) - description = fields.Text(string='Description') - active = fields.Boolean(string='Active', default=True) diff --git a/fusion_plating/fusion_plating_configurator/models/fp_coating_thickness.py b/fusion_plating/fusion_plating_configurator/models/fp_coating_thickness.py deleted file mode 100644 index 6a584151..00000000 --- a/fusion_plating/fusion_plating_configurator/models/fp_coating_thickness.py +++ /dev/null @@ -1,90 +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 FpCoatingThickness(models.Model): - """Allowed thickness option for a coating configuration. - - Each plating process (ENP Class 4, hard chrome 0.001", Type III - anodize, etc.) has its own set of valid thicknesses driven by the - spec it's built from. This child of `fp.coating.config` holds the - discrete options so the SO-line thickness dropdown can filter to - only what's actually achievable for the line's coating. - """ - _name = 'fp.coating.thickness' - _description = 'Coating Thickness Option' - _order = 'coating_config_id, sequence, value' - - coating_config_id = fields.Many2one( - 'fp.coating.config', - required=True, - ondelete='cascade', - ) - value = fields.Float( - string='Nominal', - digits=(10, 4), - required=True, - help='Target / nominal thickness value (the number printed on the cert). ' - 'Magnitude only — UoM lives in the next field.', - ) - # Hitting an exact thickness on plated parts is impossible — the spec - # is always "X mils ± tolerance" or a min/max range. These fields - # capture the acceptance band so QC can mark a reading pass/fail - # against real customer specs (e.g. AMS-2404 Class 4 = 0.001"–0.0015"). - # Both optional: leave blank for legacy single-value entries. - value_min = fields.Float( - string='Min', - digits=(10, 4), - help='Lower acceptance bound. Readings below this fail QC.', - ) - value_max = fields.Float( - string='Max', - digits=(10, 4), - help='Upper acceptance bound. Readings above this fail QC.', - ) - uom = fields.Selection( - [('mils', 'mils (0.001 in)'), - ('microns', 'microns (µm)'), - ('inches', 'inches'), - ('mm', 'mm')], - required=True, - default='mils', - ) - sequence = fields.Integer(default=10) - active = fields.Boolean(default=True) - display_name = fields.Char( - compute='_compute_display_name', - store=True, - ) - - @api.depends('value', 'value_min', 'value_max', 'uom') - def _compute_display_name(self): - uom_labels = dict(self._fields['uom'].selection) - for rec in self: - label = uom_labels.get(rec.uom, rec.uom or '') - # Strip the bracketed clarification for a tighter dropdown row. - if ' (' in label: - label = label.split(' (')[0] - # Range overrides single value when both bounds are set — - # operators see the real spec, not a phantom-precise nominal. - if rec.value_min and rec.value_max: - rec.display_name = ( - f'{rec.value_min:g}–{rec.value_max:g} {label}'.strip() - ) - elif rec.value: - rec.display_name = f'{rec.value:g} {label}'.strip() - else: - rec.display_name = label - - @api.constrains('value_min', 'value_max') - def _check_range(self): - for rec in self: - if rec.value_min and rec.value_max and rec.value_min > rec.value_max: - from odoo.exceptions import ValidationError - raise ValidationError(_( - 'Thickness Min (%(mn)s) cannot exceed Max (%(mx)s).' - ) % {'mn': rec.value_min, 'mx': rec.value_max}) diff --git a/fusion_plating/fusion_plating_configurator/models/fp_customer_price_list.py b/fusion_plating/fusion_plating_configurator/models/fp_customer_price_list.py deleted file mode 100644 index 0fbc2b09..00000000 --- a/fusion_plating/fusion_plating_configurator/models/fp_customer_price_list.py +++ /dev/null @@ -1,97 +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 FpCustomerPriceList(models.Model): - """Standing price per (customer, coating config). - - Repeat customers accept a negotiated price per coating — the configurator - and Direct Order wizard auto-fill `unit_price` from here before falling - back to the formula-based pricing engine. - - Optional effective_from / effective_to support annual contracts. - """ - _name = 'fp.customer.price.list' - _description = 'Fusion Plating — Customer Price List' - _inherit = ['mail.thread'] - _order = 'partner_id, coating_config_id, effective_from desc' - - name = fields.Char( - string='Reference', compute='_compute_name', store=True, - ) - partner_id = fields.Many2one( - 'res.partner', string='Customer', required=True, ondelete='cascade', - tracking=True, domain="[('customer_rank', '>', 0)]", - ) - coating_config_id = fields.Many2one( - 'fp.coating.config', string='Coating', required=True, ondelete='restrict', - tracking=True, - ) - unit_price = fields.Monetary( - string='Unit Price', required=True, currency_field='currency_id', - tracking=True, - ) - price_uom = fields.Selection( - [('per_part', 'per Part'), - ('per_sqin', 'per sq in'), - ('per_sqft', 'per sq ft'), - ('per_lb', 'per lb')], - string='Price Basis', default='per_part', required=True, - ) - currency_id = fields.Many2one( - 'res.currency', string='Currency', - required=True, default=lambda self: self.env.company.currency_id, - ) - effective_from = fields.Date( - string='Effective From', default=fields.Date.today, required=True, tracking=True, - ) - effective_to = fields.Date( - string='Effective To', - help='Blank = no expiry. Set for annual contract pricing.', - tracking=True, - ) - min_quantity = fields.Integer( - string='Minimum Qty', default=1, - help='Volume break — this price applies for orders of this size or larger.', - ) - notes = fields.Html(string='Notes') - active = fields.Boolean(default=True) - - _sql_constraints = [ - ('fp_price_list_unique', - 'unique(partner_id, coating_config_id, effective_from, min_quantity)', - 'A price entry already exists for this customer + coating + ' - 'effective date + quantity tier.'), - ] - - @api.depends('partner_id', 'coating_config_id', 'min_quantity', 'effective_from') - def _compute_name(self): - for rec in self: - parts = [] - if rec.partner_id: - parts.append(rec.partner_id.name) - if rec.coating_config_id: - parts.append(rec.coating_config_id.name) - if rec.min_quantity > 1: - parts.append(f'≥{rec.min_quantity}') - rec.name = ' / '.join(parts) if parts else '' - - @api.model - def _find_price(self, partner_id, coating_config_id, quantity=1, on_date=None): - """Return the best-matching active price list entry for this request.""" - if not (partner_id and coating_config_id): - return False - on_date = on_date or fields.Date.today() - candidates = self.search([ - ('partner_id', '=', partner_id), - ('coating_config_id', '=', coating_config_id), - ('active', '=', True), - ('effective_from', '<=', on_date), - '|', ('effective_to', '=', False), ('effective_to', '>=', on_date), - ('min_quantity', '<=', quantity), - ], order='min_quantity desc, effective_from desc') - return candidates[:1] 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 f2fb8d73..a233dd89 100644 --- a/fusion_plating/fusion_plating_configurator/models/fp_part_catalog.py +++ b/fusion_plating/fusion_plating_configurator/models/fp_part_catalog.py @@ -277,21 +277,8 @@ class FpPartCatalog(models.Model): rec.process_variant_count = len(variants) # ---- Direct-order defaults (Phase C — C4) ---- - x_fc_default_coating_config_id = fields.Many2one( - 'fp.coating.config', - string='Default Treatment', - help='Default coating applied when this part is dropped onto a ' - 'direct order line. Updated when "Save as Default" is ticked.', - ) - # x_fc_default_customer_spec_id is added by fusion_plating_quality - # (where fusion.plating.customer.spec lives). - x_fc_default_treatment_ids = fields.Many2many( - 'fp.treatment', - relation='fp_part_catalog_default_treatment_rel', - string='Default Additional Treatments', - help='Default additional treatments. Seeded when "Save as Default" ' - 'is ticked on a direct order line.', - ) + # x_fc_default_customer_spec_id added by fusion_plating_quality. + # Legacy default_coating_config_id + default_treatment_ids removed. # Substrate density mapping (g/cm³) for material weight calculation _SUBSTRATE_DENSITY = { diff --git a/fusion_plating/fusion_plating_configurator/models/fp_pricing_rule.py b/fusion_plating/fusion_plating_configurator/models/fp_pricing_rule.py index 370e4ac4..e643234b 100644 --- a/fusion_plating/fusion_plating_configurator/models/fp_pricing_rule.py +++ b/fusion_plating/fusion_plating_configurator/models/fp_pricing_rule.py @@ -18,8 +18,9 @@ class FpPricingRule(models.Model): _order = 'sequence, id' name = fields.Char(string='Rule Name', required=True) - coating_config_id = fields.Many2one('fp.coating.config', string='Coating Config', - help='Leave blank for a global rule.') + # coating_config_id removed. Spec + recipe match keys live on + # fusion_plating_quality.fp_pricing_rule_inherit. Material + + # cert_level (below) remain as generic filters. substrate_material = fields.Selection( [('aluminium', 'Aluminium'), ('steel', 'Steel'), ('stainless', 'Stainless Steel'), ('copper', 'Copper'), ('titanium', 'Titanium'), ('other', 'Other')], diff --git a/fusion_plating/fusion_plating_configurator/models/fp_quote_configurator.py b/fusion_plating/fusion_plating_configurator/models/fp_quote_configurator.py index 1960e1ac..3bef5f37 100644 --- a/fusion_plating/fusion_plating_configurator/models/fp_quote_configurator.py +++ b/fusion_plating/fusion_plating_configurator/models/fp_quote_configurator.py @@ -243,8 +243,15 @@ class FpQuoteConfigurator(models.Model): upload_po_file = fields.Binary(string='Upload PO', attachment=False) upload_po_filename = fields.Char(string='PO Filename') - coating_config_id = fields.Many2one( - 'fp.coating.config', string='Coating Configuration', required=True, + # Renamed from coating_config_id (Phase E — Promote Customer Spec). + # Now points at the recipe directly. The quote's specification + # (customer-facing audit ref) is added by quality inherit as + # customer_spec_id. + recipe_id = fields.Many2one( + 'fusion.plating.process.node', + string='Recipe', + required=True, + domain="[('node_type', '=', 'recipe'), ('parent_id', '=', False)]", ) quantity = fields.Integer(string='Quantity', default=1, required=True) batch_size = fields.Integer(string='Batch Size', help='Parts per rack or barrel load.') @@ -345,10 +352,10 @@ class FpQuoteConfigurator(models.Model): # Copy masking area too (for effective-area calculation) self.masking_area_sqin = cat.masking_area_sqin - @api.onchange('coating_config_id') - def _onchange_coating_config_id(self): - if self.coating_config_id: - self.thickness_requested = self.coating_config_id.thickness_min + @api.onchange('recipe_id') + def _onchange_recipe_id(self): + if self.recipe_id and self.recipe_id.thickness_min: + self.thickness_requested = self.recipe_id.thickness_min # ------------------------------------------------------------------------- # Price calculation @@ -358,11 +365,11 @@ class FpQuoteConfigurator(models.Model): 'masking_zones', 'complexity', 'substrate_material', 'quantity', 'batch_size', 'rush_order', 'shipping_fee', 'delivery_fee', - 'coating_config_id', 'coating_config_id.certification_level', + 'recipe_id', ) def _compute_price(self): for rec in self: - if not rec.coating_config_id or not rec.surface_area: + if not rec.recipe_id or not rec.surface_area: rec.calculated_price = 0 rec.price_breakdown_html = '' continue @@ -476,19 +483,17 @@ class FpQuoteConfigurator(models.Model): def _find_matching_rule(self): """Find the best pricing rule matching this configurator's filters. - Scores rules by specificity -- most specific match wins. + Scores rules by specificity — most specific match wins. If no rule matches filters, returns None. - When the chosen coating config points at a recipe and that recipe - has `pricing_rule_ids` configured, the search is constrained to - those rules ("Use Price Builders" semantics). Otherwise the - whole active rule set is considered as before. + When the chosen recipe has `pricing_rule_ids` configured, the + search is constrained to those rules ("Use Price Builders" + semantics). Otherwise the whole active rule set is considered. + + Spec-tier scoring is added by an inherit in + fusion_plating_quality (where customer.spec lives). """ - recipe = ( - self.coating_config_id.recipe_id - if self.coating_config_id and self.coating_config_id.recipe_id - else False - ) + recipe = self.recipe_id or False builder_rules = ( recipe.pricing_rule_ids if recipe else self.env['fp.pricing.rule'] ) @@ -500,27 +505,15 @@ class FpQuoteConfigurator(models.Model): rules = self.env['fp.pricing.rule'].search( [('active', '=', True)], order='sequence, id' ) - cert_level = ( - self.coating_config_id.certification_level - if self.coating_config_id else False - ) best = None best_score = -1 for rule in rules: score = 0 - if rule.coating_config_id: - if rule.coating_config_id != self.coating_config_id: - continue - score += 4 if rule.substrate_material: if rule.substrate_material != self.substrate_material: continue score += 2 - if rule.certification_level: - if rule.certification_level != cert_level: - continue - score += 1 if score > best_score: best_score = score best = rule @@ -569,9 +562,9 @@ class FpQuoteConfigurator(models.Model): raise UserError(_( 'Pick a part catalog entry before promoting this quote.' )) - if not self.coating_config_id: + if not self.recipe_id: raise UserError(_( - 'Pick a coating configuration before promoting this quote.' + 'Pick a recipe before promoting this quote.' )) existing_line = self.env['fp.direct.order.line'].search([ ('quote_id', '=', self.id), @@ -618,14 +611,13 @@ class FpQuoteConfigurator(models.Model): 'purchase_ok': False, }) - coating_name = self.coating_config_id.name if self.coating_config_id else '' + recipe_name = self.recipe_id.name if self.recipe_id else '' part_name = self.part_catalog_id.name if self.part_catalog_id else 'Custom Part' so_vals = { 'partner_id': self.partner_id.id, 'x_fc_configurator_id': self.id, 'x_fc_part_catalog_id': self.part_catalog_id.id if self.part_catalog_id else False, - 'x_fc_coating_config_id': self.coating_config_id.id, 'x_fc_rush_order': self.rush_order, 'x_fc_delivery_method': self.delivery_method, # Transfer RFQ / PO documents from configurator (if any) @@ -641,17 +633,19 @@ class FpQuoteConfigurator(models.Model): 'origin': self.name, 'order_line': [(0, 0, { 'product_id': product.id, - 'name': '%s — %s (x%d)' % (coating_name, part_name, self.quantity), + 'name': '%s — %s (x%d)' % (recipe_name, part_name, self.quantity), 'product_uom_qty': self.quantity, 'price_unit': price / self.quantity if self.quantity else price, - # Sub 11 fix — propagate part + coating to the LINE too. + # Propagate part + recipe to the LINE. # fusion_plating_jobs._fp_auto_create_job filters lines # by x_fc_part_catalog_id; without it, no fp.job spawns. + # Spec carry-over to SO line is handled by the quality + # inherit (sale_order_line_inherit.create override). 'x_fc_part_catalog_id': ( self.part_catalog_id.id if self.part_catalog_id else False ), - 'x_fc_coating_config_id': ( - self.coating_config_id.id if self.coating_config_id else False + 'x_fc_process_variant_id': ( + self.recipe_id.id if self.recipe_id else False ), })], } diff --git a/fusion_plating/fusion_plating_configurator/models/fp_sale_description_template.py b/fusion_plating/fusion_plating_configurator/models/fp_sale_description_template.py index b2d94f5e..d3c8f939 100644 --- a/fusion_plating/fusion_plating_configurator/models/fp_sale_description_template.py +++ b/fusion_plating/fusion_plating_configurator/models/fp_sale_description_template.py @@ -52,19 +52,14 @@ class FpSaleDescriptionTemplate(models.Model): 'part — it only appears in the picker when this part is on ' 'the order. Leave blank for generic fallback templates.', ) - # Related fields — surface the part's partner/coating for search & - # grouping without writing them twice. + # Related fields — surface the part's partner for search & grouping + # without writing it twice. partner_id = fields.Many2one( 'res.partner', string='Customer', related='part_catalog_id.partner_id', store=True, readonly=True, ) - # Keep the explicit coating slot for global templates that aren't - # part-specific but are still coating-specific. - coating_config_id = fields.Many2one( - 'fp.coating.config', string='Associated Coating', - ondelete='set null', - help='For generic (no-part) templates, restrict to one coating.', - ) + # coating_config_id removed; templates can be customer- or part- + # scoped. Spec-scoped templates are a future enhancement. tag = fields.Selection( [('standard', 'Standard'), ('masking', 'Masking / Selective'), diff --git a/fusion_plating/fusion_plating_configurator/models/fp_treatment.py b/fusion_plating/fusion_plating_configurator/models/fp_treatment.py deleted file mode 100644 index 33dad24c..00000000 --- a/fusion_plating/fusion_plating_configurator/models/fp_treatment.py +++ /dev/null @@ -1,52 +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 fields, models - - -class FpTreatment(models.Model): - """Pre- or post-treatment step (bead blast, zincate, bake, passivate, etc.). - - Used by coating configurations to specify which preparation and - finishing steps are required for a given process. - """ - _name = 'fp.treatment' - _description = 'Fusion Plating — Treatment' - _order = 'treatment_type, sequence, name' - - name = fields.Char( - string='Treatment', - required=True, - help='e.g. "Bead Blast", "Zincate", "Hydrogen Embrittlement Bake"', - ) - treatment_type = fields.Selection( - [('pre', 'Pre-Treatment'), ('post', 'Post-Treatment')], - string='Type', - required=True, - default='pre', - ) - sequence = fields.Integer(string='Sequence', default=10) - default_duration_minutes = fields.Float( - string='Default Duration (min)', - help='Estimated duration per application in minutes.', - ) - currency_id = fields.Many2one( - 'res.currency', - string='Currency', - required=True, - default=lambda self: self.env.company.currency_id, - ) - default_cost = fields.Monetary( - string='Default Cost', - currency_field='currency_id', - help='Default cost per application. Can be overridden on pricing rules.', - ) - description = fields.Text(string='Description') - active = fields.Boolean(string='Active', default=True) - - _sql_constraints = [ - ('fp_treatment_name_type_uniq', 'unique(name, treatment_type)', - 'Treatment name must be unique per type.'), - ] diff --git a/fusion_plating/fusion_plating_configurator/models/sale_order.py b/fusion_plating/fusion_plating_configurator/models/sale_order.py index 69b97981..077b161f 100644 --- a/fusion_plating/fusion_plating_configurator/models/sale_order.py +++ b/fusion_plating/fusion_plating_configurator/models/sale_order.py @@ -11,7 +11,8 @@ class SaleOrder(models.Model): x_fc_configurator_id = fields.Many2one('fp.quote.configurator', string='Configurator', copy=False) x_fc_part_catalog_id = fields.Many2one('fp.part.catalog', string='Part') - x_fc_coating_config_id = fields.Many2one('fp.coating.config', string='Coating Configuration') + # x_fc_coating_config_id removed; specs live on customer.spec via + # the line-level x_fc_customer_spec_id (added by quality inherit). x_fc_po_number = fields.Char(string='Customer PO #', tracking=True) x_fc_po_attachment_id = fields.Many2one( 'ir.attachment', string='PO Document', tracking=True, @@ -209,7 +210,7 @@ class SaleOrder(models.Model): for so in self: variants = [] for line in so.order_line: - if not (line.x_fc_part_catalog_id or line.x_fc_coating_config_id): + if not line.x_fc_part_catalog_id: continue # non-plating line variant = (line.x_fc_process_variant_id or line.x_fc_part_catalog_id.default_process_id) @@ -553,31 +554,16 @@ class SaleOrder(models.Model): @api.depends('order_line.price_subtotal', 'amount_untaxed') def _compute_margin(self): - """Margin = untaxed total − rolled-up cost from coating configs. + """Margin computation — stub. - x_fc_margin_percent is stored as a fraction (0.0 - 1.0) so the - widget='percentage' formats 100% as 100%, not 10000%. - - x_fc_margin_available is False when NO line has a costed coating - (i.e. fp.coating.config.unit_cost isn't populated anywhere). The - UI should render margin fields as "n/a" in that case rather than - showing a misleading 100%. + Pre-promote-customer-spec, this rolled up cost from + fp.coating.config.unit_cost. Coating Config is retired; cost + data on the recipe is a future enhancement (backlog). Until + then, margin is "not available" and the UI hides the fields. """ for rec in self: - has_cost_data = False - cost = 0.0 - for line in rec.order_line: - cc = line.x_fc_coating_config_id - if not cc: - continue - if 'unit_cost' not in cc._fields: - continue - if cc.unit_cost: - has_cost_data = True - cost_per_unit = cc.unit_cost or 0.0 - cost += cost_per_unit * (line.product_uom_qty or 0) - rec.x_fc_margin_available = has_cost_data - rec.x_fc_margin_amount = (rec.amount_untaxed or 0) - cost + rec.x_fc_margin_available = False + rec.x_fc_margin_amount = (rec.amount_untaxed or 0) rec.x_fc_margin_percent = ( (rec.x_fc_margin_amount / rec.amount_untaxed) if (rec.amount_untaxed and has_cost_data) else 0.0 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 50fe2a0a..38640416 100644 --- a/fusion_plating/fusion_plating_configurator/models/sale_order_line.py +++ b/fusion_plating/fusion_plating_configurator/models/sale_order_line.py @@ -59,15 +59,9 @@ class SaleOrderLine(models.Model): string='Description Template', help='Which template row populated this line. Informational.', ) - x_fc_coating_config_id = fields.Many2one( - 'fp.coating.config', string='Primary Treatment', - ) - # x_fc_customer_spec_id is added by fusion_plating_quality (where - # fusion.plating.customer.spec lives). Configurator can't reference - # it directly without a circular dep. - x_fc_treatment_ids = fields.Many2many( - 'fp.treatment', string='Additional Treatments', - ) + # Specification picker (x_fc_customer_spec_id) is added by + # fusion_plating_quality. Legacy x_fc_coating_config_id + + # x_fc_treatment_ids removed. x_fc_part_deadline = fields.Date( string='Part Deadline Override', help='Absolute-date manual override. When set, beats the days-offset ' diff --git a/fusion_plating/fusion_plating_configurator/security/ir.model.access.csv b/fusion_plating/fusion_plating_configurator/security/ir.model.access.csv index 2d49aafc..45ce88d3 100644 --- a/fusion_plating/fusion_plating_configurator/security/ir.model.access.csv +++ b/fusion_plating/fusion_plating_configurator/security/ir.model.access.csv @@ -1,13 +1,7 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_fp_treatment_operator,fp.treatment.operator,model_fp_treatment,fusion_plating.group_fusion_plating_operator,1,0,0,0 -access_fp_treatment_supervisor,fp.treatment.supervisor,model_fp_treatment,fusion_plating.group_fusion_plating_supervisor,1,1,0,0 -access_fp_treatment_manager,fp.treatment.manager,model_fp_treatment,fusion_plating.group_fusion_plating_manager,1,1,1,1 access_fp_part_catalog_operator,fp.part.catalog.operator,model_fp_part_catalog,fusion_plating.group_fusion_plating_operator,1,0,0,0 access_fp_part_catalog_estimator,fp.part.catalog.estimator,model_fp_part_catalog,fusion_plating_configurator.group_fp_estimator,1,1,1,0 access_fp_part_catalog_manager,fp.part.catalog.manager,model_fp_part_catalog,fusion_plating.group_fusion_plating_manager,1,1,1,1 -access_fp_coating_config_operator,fp.coating.config.operator,model_fp_coating_config,fusion_plating.group_fusion_plating_operator,1,0,0,0 -access_fp_coating_config_estimator,fp.coating.config.estimator,model_fp_coating_config,fusion_plating_configurator.group_fp_estimator,1,1,1,0 -access_fp_coating_config_manager,fp.coating.config.manager,model_fp_coating_config,fusion_plating.group_fusion_plating_manager,1,1,1,1 access_fp_pricing_rule_operator,fp.pricing.rule.operator,model_fp_pricing_rule,fusion_plating.group_fusion_plating_operator,1,0,0,0 access_fp_pricing_rule_estimator,fp.pricing.rule.estimator,model_fp_pricing_rule,fusion_plating_configurator.group_fp_estimator,1,1,1,0 access_fp_pricing_rule_manager,fp.pricing.rule.manager,model_fp_pricing_rule,fusion_plating.group_fusion_plating_manager,1,1,1,1 @@ -35,9 +29,6 @@ access_fp_sale_assembly_line_estimator,fp.sale.assembly.line.estimator,model_fp_ access_fp_sale_assembly_line_manager,fp.sale.assembly.line.manager,model_fp_sale_assembly_line,fusion_plating.group_fusion_plating_manager,1,1,1,1 access_fp_part_import_wizard_estimator,fp.part.catalog.import.wizard.estimator,model_fp_part_catalog_import_wizard,fusion_plating_configurator.group_fp_estimator,1,1,1,1 access_fp_part_import_wizard_manager,fp.part.catalog.import.wizard.manager,model_fp_part_catalog_import_wizard,fusion_plating.group_fusion_plating_manager,1,1,1,1 -access_fp_customer_price_list_operator,fp.customer.price.list.operator,model_fp_customer_price_list,fusion_plating.group_fusion_plating_operator,1,0,0,0 -access_fp_customer_price_list_estimator,fp.customer.price.list.estimator,model_fp_customer_price_list,fusion_plating_configurator.group_fp_estimator,1,1,1,0 -access_fp_customer_price_list_manager,fp.customer.price.list.manager,model_fp_customer_price_list,fusion_plating.group_fusion_plating_manager,1,1,1,1 access_fp_sale_desc_template_user,fp.sale.description.template.user,model_fp_sale_description_template,base.group_user,1,0,0,0 access_fp_sale_desc_template_estimator,fp.sale.description.template.estimator,model_fp_sale_description_template,fusion_plating_configurator.group_fp_estimator,1,1,1,0 access_fp_sale_desc_template_manager,fp.sale.description.template.manager,model_fp_sale_description_template,fusion_plating.group_fusion_plating_manager,1,1,1,1 @@ -48,9 +39,6 @@ access_fp_serial_bulk_add_estimator,fp.serial.bulk.add.estimator,model_fp_serial access_fp_serial_bulk_add_manager,fp.serial.bulk.add.manager,model_fp_serial_bulk_add_wizard,fusion_plating.group_fusion_plating_manager,1,1,1,1 access_fp_part_revision_bump_estimator,fp.part.revision.bump.estimator,model_fp_part_revision_bump_wizard,fusion_plating_configurator.group_fp_estimator,1,1,1,1 access_fp_part_revision_bump_manager,fp.part.revision.bump.manager,model_fp_part_revision_bump_wizard,fusion_plating.group_fusion_plating_manager,1,1,1,1 -access_fp_coating_thickness_user,fp.coating.thickness.user,model_fp_coating_thickness,base.group_user,1,0,0,0 -access_fp_coating_thickness_estimator,fp.coating.thickness.estimator,model_fp_coating_thickness,fusion_plating_configurator.group_fp_estimator,1,1,1,0 -access_fp_coating_thickness_manager,fp.coating.thickness.manager,model_fp_coating_thickness,fusion_plating.group_fusion_plating_manager,1,1,1,1 access_fp_part_material_user,fp.part.material.user,model_fp_part_material,base.group_user,1,0,0,0 access_fp_part_material_estimator,fp.part.material.estimator,model_fp_part_material,fusion_plating_configurator.group_fp_estimator,1,1,1,0 access_fp_part_material_manager,fp.part.material.manager,model_fp_part_material,fusion_plating.group_fusion_plating_manager,1,1,1,1 diff --git a/fusion_plating/fusion_plating_configurator/views/fp_coating_config_views.xml b/fusion_plating/fusion_plating_configurator/views/fp_coating_config_views.xml deleted file mode 100644 index d95c8787..00000000 --- a/fusion_plating/fusion_plating_configurator/views/fp_coating_config_views.xml +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - - fp.coating.config.list - fp.coating.config - - - - - - - - - - - - - - - - - - fp.coating.config.form - fp.coating.config - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Discrete thickness values the estimator can pick when - this coating appears on a sale order line. Each value - is driven by the spec this coating is built against - (e.g. AMS-2404 Class 4 → 0.0005″ / 0.001″ / 0.0015″). - Leave empty if no dropdown is needed for this coating. - - - - - - - - - - - - - - - - - - - - - - - - - fp.coating.config.search - fp.coating.config - - - - - - - - - - - - - - - - - - - - - - - - - - - Coating Configurations - fp.coating.config - list,form - - - - No coating configurations defined yet - - - Define coating setups with process type, phosphorus level, - thickness range, spec reference, and required treatments. - - - - - diff --git a/fusion_plating/fusion_plating_configurator/views/fp_coating_thickness_views.xml b/fusion_plating/fusion_plating_configurator/views/fp_coating_thickness_views.xml deleted file mode 100644 index d89aa353..00000000 --- a/fusion_plating/fusion_plating_configurator/views/fp_coating_thickness_views.xml +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - fp.coating.thickness.list - fp.coating.thickness - - - - - - - - - - - - - - - - fp.coating.thickness.form - fp.coating.thickness - - - - - - - - - - - - - - - - - - - Set Min/Max when the customer spec is a - range (e.g. AMS-2404 Class 4 = 0.001"–0.0015"). - QC readings outside the band fail. - - - - - - - - - - - - - - fp.coating.thickness.search - fp.coating.thickness - - - - - - - - - - - - - - - - - Coating Thicknesses - fp.coating.thickness - list,form - - - - diff --git a/fusion_plating/fusion_plating_configurator/views/fp_configurator_menu.xml b/fusion_plating/fusion_plating_configurator/views/fp_configurator_menu.xml index 13c6d4f8..aa7ebc6f 100644 --- a/fusion_plating/fusion_plating_configurator/views/fp_configurator_menu.xml +++ b/fusion_plating/fusion_plating_configurator/views/fp_configurator_menu.xml @@ -87,30 +87,12 @@ sequence="8" groups="group_fp_estimator"/> - - - - - - - - - - fp.customer.price.list.list - fp.customer.price.list - - - - - - - - - - - - - - - - - fp.customer.price.list.form - fp.customer.price.list - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - fp.customer.price.list.search - fp.customer.price.list - - - - - - - - - - - - - - - - - Customer Price Lists - fp.customer.price.list - list,form - - {'search_default_active': 1} - - - 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 a2409e70..69c04694 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 @@ -201,20 +201,13 @@ class="btn-link"/> - - - - - + - Seeds the treatment fields on new direct-order - lines for this part. Updated whenever "Save as - Default" is ticked while placing an order. + Set a Default Specification on this part + (under the section added by the Quality + module) so future direct-order lines + pre-fill it automatically. diff --git a/fusion_plating/fusion_plating_configurator/views/fp_pricing_rule_views.xml b/fusion_plating/fusion_plating_configurator/views/fp_pricing_rule_views.xml index 0e93a590..40ddfe95 100644 --- a/fusion_plating/fusion_plating_configurator/views/fp_pricing_rule_views.xml +++ b/fusion_plating/fusion_plating_configurator/views/fp_pricing_rule_views.xml @@ -14,7 +14,6 @@ - @@ -42,7 +41,6 @@ - @@ -104,7 +102,6 @@ - @@ -113,7 +110,6 @@ - diff --git a/fusion_plating/fusion_plating_configurator/views/fp_quote_configurator_views.xml b/fusion_plating/fusion_plating_configurator/views/fp_quote_configurator_views.xml index c70fb26b..218f1a86 100644 --- a/fusion_plating/fusion_plating_configurator/views/fp_quote_configurator_views.xml +++ b/fusion_plating/fusion_plating_configurator/views/fp_quote_configurator_views.xml @@ -129,7 +129,7 @@ - + - + @@ -350,14 +350,14 @@ - + - + diff --git a/fusion_plating/fusion_plating_configurator/views/fp_sale_description_template_views.xml b/fusion_plating/fusion_plating_configurator/views/fp_sale_description_template_views.xml index 65c4f10d..69009803 100644 --- a/fusion_plating/fusion_plating_configurator/views/fp_sale_description_template_views.xml +++ b/fusion_plating/fusion_plating_configurator/views/fp_sale_description_template_views.xml @@ -22,7 +22,6 @@ decoration-danger="tag == 'rework'" decoration-success="tag in ('aerospace','nuclear')"/> - @@ -46,9 +45,6 @@ - @@ -75,7 +71,6 @@ - diff --git a/fusion_plating/fusion_plating_configurator/views/fp_treatment_views.xml b/fusion_plating/fusion_plating_configurator/views/fp_treatment_views.xml deleted file mode 100644 index 859ed64f..00000000 --- a/fusion_plating/fusion_plating_configurator/views/fp_treatment_views.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - fp.treatment.list - fp.treatment - - - - - - - - - - - - - - - - fp.treatment.form - fp.treatment - - - - - - - - - - - - - - - - - - min - - - - - - - - - - - - - - - - - fp.treatment.search - fp.treatment - - - - - - - - - - - - - - - - - - Treatments - fp.treatment - list,form - - - - No treatments defined yet - - - Add pre-treatment steps (bead blast, zincate, acid etch) and - post-treatment steps (bake, passivate, chromate seal). - - - - - 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 3aaa455f..ee772e4a 100644 --- a/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml +++ b/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml @@ -106,7 +106,7 @@ so you can confirm an order has the right parts/coatings without scrolling pricing columns. The pre-Sub-12 SO- header singletons (x_fc_part_catalog_id / - x_fc_coating_config_id) only ever populated when the + x_fc_customer_spec_id) only ever populated when the order was built via the quote configurator — they're silent on direct orders, which is why they appeared empty after confirm. They still exist on the model @@ -118,7 +118,6 @@ readonly="1"> - @@ -251,7 +250,6 @@ - - - - @@ -373,7 +368,7 @@ - + Qty: @@ -399,7 +394,6 @@ - @@ -407,9 +401,6 @@ - - - Qty: diff --git a/fusion_plating/fusion_plating_configurator/wizard/fp_add_from_quote_wizard.py b/fusion_plating/fusion_plating_configurator/wizard/fp_add_from_quote_wizard.py index 4cc5c979..656fb774 100644 --- a/fusion_plating/fusion_plating_configurator/wizard/fp_add_from_quote_wizard.py +++ b/fusion_plating/fusion_plating_configurator/wizard/fp_add_from_quote_wizard.py @@ -43,14 +43,14 @@ class FpAddFromQuoteWizard(models.TransientModel): wizard = self.direct_order_wizard_id copied = 0 for q in self.quote_ids: - if not q.part_catalog_id or not q.coating_config_id: + if not q.part_catalog_id or not q.recipe_id: continue Line._create_from_quote(q, wizard) copied += 1 if not copied: raise UserError(_( - 'The selected quotes do not have both part and coating set, ' + 'The selected quotes do not have both part and recipe set, ' 'so nothing could be copied.' )) diff --git a/fusion_plating/fusion_plating_configurator/wizard/fp_add_from_quote_wizard_views.xml b/fusion_plating/fusion_plating_configurator/wizard/fp_add_from_quote_wizard_views.xml index ac3c6eb8..767a2653 100644 --- a/fusion_plating/fusion_plating_configurator/wizard/fp_add_from_quote_wizard_views.xml +++ b/fusion_plating/fusion_plating_configurator/wizard/fp_add_from_quote_wizard_views.xml @@ -22,7 +22,7 @@ - + diff --git a/fusion_plating/fusion_plating_configurator/wizard/fp_add_from_so_wizard.py b/fusion_plating/fusion_plating_configurator/wizard/fp_add_from_so_wizard.py index c82fa221..456ac133 100644 --- a/fusion_plating/fusion_plating_configurator/wizard/fp_add_from_so_wizard.py +++ b/fusion_plating/fusion_plating_configurator/wizard/fp_add_from_so_wizard.py @@ -53,14 +53,12 @@ class FpAddFromSoWizard(models.TransientModel): wizard = self.direct_order_wizard_id copied = 0 for src in self.source_line_ids: - if not src.x_fc_part_catalog_id or not src.x_fc_coating_config_id: - # Skip SO lines that predate the plating fields + if not src.x_fc_part_catalog_id: + # Skip non-plating SO lines continue Line.create({ 'wizard_id': wizard.id, 'part_catalog_id': src.x_fc_part_catalog_id.id, - 'coating_config_id': src.x_fc_coating_config_id.id, - 'treatment_ids': [(6, 0, src.x_fc_treatment_ids.ids)], 'quantity': int(src.product_uom_qty) or 1, 'unit_price': src.price_unit or 0.0, 'part_deadline': src.x_fc_part_deadline, diff --git a/fusion_plating/fusion_plating_configurator/wizard/fp_add_from_so_wizard_views.xml b/fusion_plating/fusion_plating_configurator/wizard/fp_add_from_so_wizard_views.xml index 985bf9ae..9f2a349a 100644 --- a/fusion_plating/fusion_plating_configurator/wizard/fp_add_from_so_wizard_views.xml +++ b/fusion_plating/fusion_plating_configurator/wizard/fp_add_from_so_wizard_views.xml @@ -27,7 +27,7 @@ - + diff --git a/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_line.py b/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_line.py index d2cfd1ea..bfeadc1d 100644 --- a/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_line.py +++ b/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_line.py @@ -51,22 +51,9 @@ class FpDirectOrderLine(models.Model): new_drawing_filename = fields.Char(string='Filename') revision_note = fields.Char(string='Revision Note') - # ---- Treatments ---- - coating_config_id = fields.Many2one( - 'fp.coating.config', - string='Primary Treatment', - help='Optional. Some orders are non-coating work (re-inspection, ' - 'rework, masking-only, etc.) and the operator picks the ' - 'workflow downstream — leaving this blank lets that path ' - 'through.', - ) - # customer_spec_id is added by fusion_plating_quality (where - # fusion.plating.customer.spec lives). - treatment_ids = fields.Many2many( - 'fp.treatment', - string='Additional Treatments', - help='Extra pre/post treatments applied to this line.', - ) + # Specification picker (customer_spec_id) added by + # fusion_plating_quality. Legacy coating_config_id + + # treatment_ids removed. # Sub 9 (polished 2026-04-28) — process variant per line. The picker # now lets the estimator pick ANY root recipe in the system: the # part's own variants, another customer's variants, or a template @@ -107,8 +94,7 @@ class FpDirectOrderLine(models.Model): ) @api.depends('process_variant_id', - 'part_catalog_id.default_process_id', - 'coating_config_id.recipe_id') + 'part_catalog_id.default_process_id') def _compute_effective_process(self): for rec in self: if rec.process_variant_id: @@ -122,12 +108,6 @@ class FpDirectOrderLine(models.Model): rec.effective_process_id = part_proc rec.effective_process_source = 'Part default' continue - cc_proc = (rec.coating_config_id.recipe_id - if rec.coating_config_id else False) - if cc_proc: - rec.effective_process_id = cc_proc - rec.effective_process_source = 'Coating default' - continue rec.effective_process_id = False rec.effective_process_source = False @@ -168,35 +148,26 @@ class FpDirectOrderLine(models.Model): if not rec.part_catalog_id: continue part = rec.part_catalog_id - has_default_coating = bool(getattr( - part, 'x_fc_default_coating_config_id', False)) - has_default_treatments = bool(getattr( - part, 'x_fc_default_treatment_ids', False)) - # Pre-fill default coating if the line is empty. - if not rec.coating_config_id and has_default_coating: - rec.coating_config_id = part.x_fc_default_coating_config_id - # Pre-fill default treatments if any are configured. - if not rec.treatment_ids and has_default_treatments: - rec.treatment_ids = [(6, 0, part.x_fc_default_treatment_ids.ids)] # Default-spec auto-fill is implemented by an inherit in # fusion_plating_quality (where customer_spec_id field lives). - # New-part auto-suggest: if neither default exists, this is + has_default_spec = bool(getattr( + part, 'x_fc_default_customer_spec_id', False)) + # New-part auto-suggest: if no default spec exists, this is # likely a first-time use of the part. Auto-tick the # push_to_defaults toggle so whatever Sarah picks becomes # the saved default — surface a warning popup so she knows. # `is_one_off` always wins (operator opted out of catalog # persistence), so don't auto-tick in that case. - if (not has_default_coating - and not has_default_treatments + if (not has_default_spec and not rec.is_one_off and not rec.push_to_defaults): rec.push_to_defaults = True warning = { 'title': _('First-Time Part — Defaults Will Be Saved'), 'message': _( - '%(part)s has no saved coating / treatments. ' - 'The coating + treatments you pick on this line ' - 'will be saved as the part\'s defaults so the ' + '%(part)s has no saved specification. ' + 'The specification you pick on this line will ' + 'be saved as the part\'s default so the ' 'next order auto-fills them. Untick "Save as ' 'Default" on the line if you don\'t want this.' ) % {'part': part.display_name or part.part_number or '(part)'}, @@ -269,11 +240,11 @@ class FpDirectOrderLine(models.Model): start_at_node_id = fields.Many2one( 'fusion.plating.process.node', string='Start at Node', - domain="[('id', 'child_of', coating_config_id and coating_config_id.recipe_id.id or 0)]", + domain="[('id', 'child_of', process_variant_id and process_variant_id.id or 0)]", help='For re-work jobs: pick the recipe step where this job should ' - 'begin. Pick a coating first — nodes are scoped to its ' - 'recipe tree. Skips earlier steps in the generated WO but ' - 'keeps later siblings and sub-processes.', + 'begin. Pick a recipe first — nodes are scoped to it. Skips ' + 'earlier steps in the generated WO but keeps later siblings ' + 'and sub-processes.', ) is_one_off = fields.Boolean( string='One-off Part', @@ -436,12 +407,11 @@ class FpDirectOrderLine(models.Model): for rec in self: rec.line_subtotal = (rec.quantity or 0) * (rec.unit_price or 0.0) - @api.depends('part_catalog_id', 'coating_config_id', 'unit_price', 'quantity') + @api.depends('part_catalog_id', 'unit_price', 'quantity') def _compute_is_missing_info(self): for rec in self: rec.is_missing_info = not ( rec.part_catalog_id - and rec.coating_config_id and rec.unit_price and rec.quantity ) @@ -499,14 +469,16 @@ class FpDirectOrderLine(models.Model): # ---- Onchange ---- @api.onchange('quote_id') def _onchange_quote_id(self): - """Auto-fill part, coating, and unit price from the linked quote.""" + """Auto-fill part and unit price from the linked quote. + + Spec carry-over from quote → wizard line is handled by an + inherit in fusion_plating_quality. + """ if not self.quote_id: return q = self.quote_id if q.part_catalog_id and not self.part_catalog_id: self.part_catalog_id = q.part_catalog_id - if q.coating_config_id and not self.coating_config_id: - self.coating_config_id = q.coating_config_id if not self.unit_price: final = q.estimator_override_price or q.calculated_price if final and q.quantity: @@ -514,13 +486,13 @@ class FpDirectOrderLine(models.Model): @api.onchange('part_catalog_id') def _onchange_part_defaults(self): - """When a part is picked, seed coating + treatments from its catalog defaults.""" + """Seed defaults when a part is picked. + + Spec auto-fill is handled by an inherit in fusion_plating_quality + (the customer_spec_id field lives there). + """ if not self.part_catalog_id: return - if not self.coating_config_id and self.part_catalog_id.x_fc_default_coating_config_id: - self.coating_config_id = self.part_catalog_id.x_fc_default_coating_config_id - if not self.treatment_ids and self.part_catalog_id.x_fc_default_treatment_ids: - self.treatment_ids = self.part_catalog_id.x_fc_default_treatment_ids # Seed default taxes from the FP-SERVICE product, fiscal-position # mapped from the customer. Only fills when the user hasn't set # taxes manually. @@ -543,21 +515,10 @@ class FpDirectOrderLine(models.Model): if taxes: self.tax_ids = [(6, 0, taxes.ids)] - @api.onchange('coating_config_id', 'quantity', 'part_catalog_id') - def _onchange_lookup_price(self): - """Auto-fill unit_price from customer price list when available.""" - if self.unit_price: - return - partner = self.wizard_id.partner_id - if not (partner and self.coating_config_id): - return - price = self.env['fp.customer.price.list']._find_price( - partner.id, - self.coating_config_id.id, - quantity=self.quantity or 1, - ) - if price: - self.unit_price = price.unit_price + # Auto-fill unit_price from a customer price list — extended in + # fusion_plating_quality (the spec field lives there). The base + # configurator wizard no longer triggers price lookup since + # coating_config_id is gone. @api.onchange('description_template_id') def _onchange_description_template(self): @@ -575,15 +536,14 @@ class FpDirectOrderLine(models.Model): if tpl.internal_description: self.internal_description = tpl.internal_description - @api.onchange('part_catalog_id', 'coating_config_id') + @api.onchange('part_catalog_id') def _onchange_suggest_template(self): """Offer a sensible default template — part-specific wins. Priority (first non-empty result wins): 1. This part's lowest-sequence active template 2. This customer's templates (no part) - 3. This coating's templates (no part) - 4. Don't auto-pick — user has to choose + 3. Don't auto-pick — user has to choose """ if self.description_template_id or self.line_description: return @@ -616,16 +576,6 @@ class FpDirectOrderLine(models.Model): _apply(match) return - if self.coating_config_id: - match = Template.search([ - ('active', '=', True), - ('part_catalog_id', '=', False), - ('partner_id', '=', False), - ('coating_config_id', '=', self.coating_config_id.id), - ], order='sequence', limit=1) - if match: - _apply(match) - # ---- Helpers ---- @api.model def _create_from_quote(self, quote, wizard): @@ -635,16 +585,17 @@ class FpDirectOrderLine(models.Model): the bulk "Add From Quotes" sub-wizard — keeps the field mapping in one place so the two flows can never drift. """ - if not quote.part_catalog_id or not quote.coating_config_id: + if not quote.part_catalog_id: raise UserError(_( - 'Quote %s has no part or coating set; cannot seed a line.' + 'Quote %s has no part set; cannot seed a line.' ) % (quote.name or quote.id)) final = quote.estimator_override_price or quote.calculated_price unit = (final / quote.quantity) if (final and quote.quantity) else 0.0 + # Spec carry-over from quote → wizard line is handled by an + # inherit in fusion_plating_quality (customer_spec_id field). return self.create({ 'wizard_id': wizard.id, 'part_catalog_id': quote.part_catalog_id.id, - 'coating_config_id': quote.coating_config_id.id, 'quantity': int(quote.quantity) or 1, 'unit_price': unit, 'quote_id': quote.id, diff --git a/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard.py b/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard.py index e950a28e..4136163d 100644 --- a/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard.py +++ b/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard.py @@ -550,12 +550,13 @@ class FpDirectOrderWizard(models.Model): for line in self.line_ids: part = line._get_or_bump_revision() resolved_parts[line.id] = part - # Build the line header. Primary treatment is optional now; - # when missing, drop it from the header rather than printing + # Build the line header. Specification is optional; when + # missing, drop it from the header rather than printing # "False - PartName Rev A". - treatment_label = line.coating_config_id.name or _('No coating') + spec = getattr(line, 'customer_spec_id', False) + spec_label = (spec.display_name if spec else '') or _('No spec') header = '%s - %s Rev %s (x%d)' % ( - treatment_label, + spec_label, part.name, part.revision, line.quantity, @@ -573,10 +574,9 @@ class FpDirectOrderWizard(models.Model): 'x_fc_part_catalog_id': part.id, 'x_fc_description_template_id': line.description_template_id.id or False, 'x_fc_internal_description': line.internal_description or False, - 'x_fc_coating_config_id': line.coating_config_id.id, - 'x_fc_treatment_ids': [(6, 0, line.treatment_ids.ids)], - # x_fc_customer_spec_id is added to vals by an extension - # of this method in fusion_plating_quality. + # x_fc_customer_spec_id is set on the resulting SO line + # by an extension in fusion_plating_quality (post-create + # patch — see fp_direct_order_line_inherit.py). 'x_fc_part_deadline': line.part_deadline, 'x_fc_part_deadline_offset_days': line.part_deadline_offset_days, 'x_fc_rush_order': line.rush_order, @@ -630,19 +630,9 @@ class FpDirectOrderWizard(models.Model): 'Quote won — promoted onto Direct Order %(doo)s, SO %(so)s.' ) % {'doo': self.name, 'so': so.name}) - # 6. Push-to-defaults (C4) — uses the resolved part cached - # during the build loop so rev-bumped lines write defaults to - # the NEW revision, not the pre-bump one. - for line in self.line_ids: - if not line.push_to_defaults or line.is_one_off: - continue - part = resolved_parts.get(line.id) or line.part_catalog_id - if not part: - continue - part.write({ - 'x_fc_default_coating_config_id': line.coating_config_id.id or False, - 'x_fc_default_treatment_ids': [(6, 0, line.treatment_ids.ids)], - }) + # 6. Push-to-defaults — Specification carry-over to the part's + # x_fc_default_customer_spec_id is handled by an inherit in + # fusion_plating_quality (the field lives there). so.message_post(body=_( 'Quotation created from PO %s with %d line(s). ' 'Review and confirm manually when ready.' diff --git a/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml b/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml index 3b4c0290..f9430fa2 100644 --- a/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml +++ b/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml @@ -154,8 +154,6 @@ optional="hide"/> - - - - - @@ -99,7 +98,6 @@ - diff --git a/fusion_plating/fusion_plating_jobs/report/report_fp_job_traveller.xml b/fusion_plating/fusion_plating_jobs/report/report_fp_job_traveller.xml index 35217114..d31895bb 100644 --- a/fusion_plating/fusion_plating_jobs/report/report_fp_job_traveller.xml +++ b/fusion_plating/fusion_plating_jobs/report/report_fp_job_traveller.xml @@ -203,9 +203,6 @@ - - - diff --git a/fusion_plating/fusion_plating_jobs/views/fp_job_form_inherit.xml b/fusion_plating/fusion_plating_jobs/views/fp_job_form_inherit.xml index 52a08b58..ea33d84c 100644 --- a/fusion_plating/fusion_plating_jobs/views/fp_job_form_inherit.xml +++ b/fusion_plating/fusion_plating_jobs/views/fp_job_form_inherit.xml @@ -95,7 +95,7 @@ - + - + - - + + diff --git a/fusion_plating/fusion_plating_quality/views/fp_part_catalog_views_inherit.xml b/fusion_plating/fusion_plating_quality/views/fp_part_catalog_views_inherit.xml index a85deb71..155fb6e2 100644 --- a/fusion_plating/fusion_plating_quality/views/fp_part_catalog_views_inherit.xml +++ b/fusion_plating/fusion_plating_quality/views/fp_part_catalog_views_inherit.xml @@ -16,7 +16,9 @@ - + - + - diff --git a/fusion_plating/fusion_plating_quality/views/sale_order_views_inherit.xml b/fusion_plating/fusion_plating_quality/views/sale_order_views_inherit.xml index b6080b18..7ccba17d 100644 --- a/fusion_plating/fusion_plating_quality/views/sale_order_views_inherit.xml +++ b/fusion_plating/fusion_plating_quality/views/sale_order_views_inherit.xml @@ -17,15 +17,18 @@ + view to add Specification next to Part Catalog. --> sale.order.form.quality.spec.inherit sale.order - - + - - - — Recipe 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 515fe6e7..52b624c0 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 @@ -19,8 +19,7 @@ * _mo — the mrp.production record (or False) * _so, _line — the originating sale order / line * _part — fp.part.catalog - * _coating — fp.coating.config (legacy; removed in Phase E) - * _spec — fusion.plating.customer.spec (the audit-tracked spec the cert prints) + * _spec — fusion.plating.customer.spec (audit-tracked spec) * _process — the resolved fusion.plating.process.node tree * _due — datetime/date for "Due Date" row * _qty — float for "Qty" row @@ -48,11 +47,9 @@ or (_so and _so.order_line[:1]) or False"/> - - @@ -501,7 +497,6 @@ - diff --git a/fusion_plating/fusion_plating_shopfloor/controllers/shopfloor_controller.py b/fusion_plating/fusion_plating_shopfloor/controllers/shopfloor_controller.py index 1111c721..edbd58d3 100644 --- a/fusion_plating/fusion_plating_shopfloor/controllers/shopfloor_controller.py +++ b/fusion_plating/fusion_plating_shopfloor/controllers/shopfloor_controller.py @@ -1142,7 +1142,7 @@ class FpShopfloorController(http.Controller): job_read_fields = [ 'name', 'origin', 'priority', 'partner_id', 'product_id', 'qty', 'qty_done', 'date_planned_start', 'date_deadline', - 'part_catalog_id', 'coating_config_id', + 'part_catalog_id', ] if 'customer_spec_id' in unique_jobs._fields: job_read_fields.append('customer_spec_id') @@ -1555,10 +1555,6 @@ class FpShopfloorController(http.Controller): job.part_catalog_id if 'part_catalog_id' in job._fields else False ) - coating = ( - job.coating_config_id - if 'coating_config_id' in job._fields else False - ) # Specification (added by fusion_plating_quality) spec = ( job.customer_spec_id @@ -1572,12 +1568,9 @@ class FpShopfloorController(http.Controller): getattr(part, 'part_number', '') or part.name or '' ) part_revision = getattr(part, 'revision', '') or '' + # coating_label kept blank — Phase E removed coating; downstream + # tablet templates read spec_label instead. coating_label = '' - if coating: - spec_ref = getattr(coating, 'spec_reference', '') or '' - coating_label = ( - f'{coating.name} · {spec_ref}' if spec_ref else coating.name - ) # Customer logo + product image customer_logo_url = ''
- Discrete thickness values the estimator can pick when - this coating appears on a sale order line. Each value - is driven by the spec this coating is built against - (e.g. AMS-2404 Class 4 → 0.0005″ / 0.001″ / 0.0015″). - Leave empty if no dropdown is needed for this coating. -
- No coating configurations defined yet -
- Define coating setups with process type, phosphorus level, - thickness range, spec reference, and required treatments. -
- Seeds the treatment fields on new direct-order - lines for this part. Updated whenever "Save as - Default" is ticked while placing an order. + Set a Default Specification on this part + (under the section added by the Quality + module) so future direct-order lines + pre-fill it automatically.
- No treatments defined yet -
- Add pre-treatment steps (bead blast, zincate, acid etch) and - post-treatment steps (bake, passivate, chromate seal). -