feat(promote-customer-spec): Phase E — final removal of coating + treatment

DELETED entirely (model + view + ACL + data file + menu):
- fp.coating.config (configurator)
- fp.treatment (configurator + seeded data)
- fp.coating.thickness (configurator) — replaced by fp.recipe.thickness in Phase A
- fp.customer.price.list (configurator) — coating-keyed, no replacement

Field deletions:
- sale.order.x_fc_coating_config_id
- sale.order.line.x_fc_coating_config_id + x_fc_treatment_ids
- account.move.line.x_fc_coating_config_id
- fp.part.catalog.x_fc_default_coating_config_id + x_fc_default_treatment_ids
- fp.job.coating_config_id
- fp.pricing.rule.coating_config_id
- fp.quality.point.coating_config_ids
- fp.direct.order.line.coating_config_id + treatment_ids
- fp.sale.description.template.coating_config_id

Refactored:
- fp.quote.configurator.coating_config_id → recipe_id (now points at
  fusion.plating.process.node, the actual recipe). All compute, onchange,
  and matcher logic updated to use recipe directly. Quality inherit
  extends matcher with spec-tier scoring.
- fp.job._fp_create_certificates now reads spec from job.customer_spec_id
  and formats spec_reference as "code Rev rev". Same for thickness
  source — bake fields read from recipe_root (Phase A).
- fp.job.step.button_finish bake-window auto-spawn reads bake settings
  from recipe_root instead of coating.
- fp.certificate auto-fill spec_min_mils/max_mils from recipe (Phase A
  thickness fields) instead of coating.
- jobs/sale_order.py: job creation reads x_fc_customer_spec_id from
  line, drops coating refs and the legacy header-coating fallback.
- Wizards drop coating + treatment fields and refs.
- Configurator views drop x_fc_coating_config_id + x_fc_treatment_ids
  fields entirely. Quality inherits re-anchor on stable fields
  (x_fc_part_catalog_id, x_fc_internal_description, default_process_id,
  process_variant_id, substrate_material) so they keep working.
- Reports drop coating fallback elifs; print recipe / spec.
- Tablet payload drops coating_config_id from job.read fields.

Skipped (deferred to backlog):
- fusion_plating_bridge_mrp — module is uninstalled per Sub 11; source
  files retain coating refs but no runtime impact.
- fusion_plating_portal — circular dep (portal → quality → certs →
  portal). Customer-facing portal coating picker stays for now;
  promote-spec polish is a separate sub-project.

Verification: grep for "coating_config_id|fp.coating.config|
fp.treatment|fp.coating.thickness" in live (non-bridge_mrp,
non-portal, non-script, non-test) Python/XML/CSV returns 3 hits,
all in module / class docstrings explaining Phase E history.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-15 02:00:41 -04:00
parent e0eacc2530
commit d891002c84
54 changed files with 233 additions and 1283 deletions

View File

@@ -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)

View File

@@ -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',

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
-->
<odoo noupdate="1">
<!-- Pre-treatments -->
<record id="treatment_alkaline_clean" model="fp.treatment">
<field name="name">Alkaline Clean</field>
<field name="treatment_type">pre</field>
<field name="sequence">10</field>
<field name="default_duration_minutes">15</field>
</record>
<record id="treatment_acid_etch" model="fp.treatment">
<field name="name">Acid Etch</field>
<field name="treatment_type">pre</field>
<field name="sequence">20</field>
<field name="default_duration_minutes">10</field>
</record>
<record id="treatment_zincate" model="fp.treatment">
<field name="name">Zincate (Aluminium)</field>
<field name="treatment_type">pre</field>
<field name="sequence">30</field>
<field name="default_duration_minutes">5</field>
</record>
<record id="treatment_bead_blast" model="fp.treatment">
<field name="name">Bead Blast</field>
<field name="treatment_type">pre</field>
<field name="sequence">40</field>
<field name="default_duration_minutes">20</field>
</record>
<record id="treatment_degrease" model="fp.treatment">
<field name="name">Solvent Degrease</field>
<field name="treatment_type">pre</field>
<field name="sequence">50</field>
<field name="default_duration_minutes">10</field>
</record>
<!-- Post-treatments -->
<record id="treatment_bake" model="fp.treatment">
<field name="name">Hydrogen Embrittlement Bake</field>
<field name="treatment_type">post</field>
<field name="sequence">10</field>
<field name="default_duration_minutes">240</field>
</record>
<record id="treatment_passivate" model="fp.treatment">
<field name="name">Passivate</field>
<field name="treatment_type">post</field>
<field name="sequence">20</field>
<field name="default_duration_minutes">30</field>
</record>
<record id="treatment_chromate_seal" model="fp.treatment">
<field name="name">Chromate Seal</field>
<field name="treatment_type">post</field>
<field name="sequence">30</field>
<field name="default_duration_minutes">15</field>
</record>
</odoo>

View File

@@ -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

View File

@@ -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.',

View File

@@ -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)

View File

@@ -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})

View File

@@ -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]

View File

@@ -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 = {

View File

@@ -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')],

View File

@@ -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
),
})],
}

View File

@@ -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'),

View File

@@ -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.'),
]

View File

@@ -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

View File

@@ -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 '

View File

@@ -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
1 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
2 access_fp_part_catalog_operator fp.part.catalog.operator model_fp_part_catalog fusion_plating.group_fusion_plating_operator 1 0 0 0
3 access_fp_part_catalog_estimator fp.part.catalog.estimator model_fp_part_catalog fusion_plating_configurator.group_fp_estimator 1 1 1 0
4 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
5 access_fp_pricing_rule_operator fp.pricing.rule.operator model_fp_pricing_rule fusion_plating.group_fusion_plating_operator 1 0 0 0
6 access_fp_pricing_rule_estimator fp.pricing.rule.estimator model_fp_pricing_rule fusion_plating_configurator.group_fp_estimator 1 1 1 0
7 access_fp_pricing_rule_manager fp.pricing.rule.manager model_fp_pricing_rule fusion_plating.group_fusion_plating_manager 1 1 1 1
29 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
30 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
31 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
32 access_fp_sale_desc_template_user fp.sale.description.template.user model_fp_sale_description_template base.group_user 1 0 0 0
33 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
34 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
39 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
40 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
41 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
42 access_fp_part_material_user fp.part.material.user model_fp_part_material base.group_user 1 0 0 0
43 access_fp_part_material_estimator fp.part.material.estimator model_fp_part_material fusion_plating_configurator.group_fp_estimator 1 1 1 0
44 access_fp_part_material_manager fp.part.material.manager model_fp_part_material fusion_plating.group_fusion_plating_manager 1 1 1 1

View File

@@ -1,143 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
-->
<odoo>
<!-- ===== Coating Configuration List View ===== -->
<record id="view_fp_coating_config_list" model="ir.ui.view">
<field name="name">fp.coating.config.list</field>
<field name="model">fp.coating.config</field>
<field name="arch" type="xml">
<list string="Coating Configurations" decoration-muted="not active">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="process_type_id"/>
<field name="phosphorus_level"/>
<field name="thickness_min"/>
<field name="thickness_max"/>
<field name="spec_reference"/>
<field name="certification_level"/>
<field name="active" widget="boolean_toggle"/>
</list>
</field>
</record>
<!-- ===== Coating Configuration Form View ===== -->
<record id="view_fp_coating_config_form" model="ir.ui.view">
<field name="name">fp.coating.config.form</field>
<field name="model">fp.coating.config</field>
<field name="arch" type="xml">
<form string="Coating Configuration">
<sheet>
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" placeholder="e.g. EN Mid-Phos AMS 2404"/></h1>
</div>
<group>
<group>
<field name="process_type_id"/>
<field name="recipe_id"/>
<field name="phosphorus_level"/>
<field name="certification_level"/>
<field name="sequence"/>
</group>
<group>
<field name="thickness_min"/>
<field name="thickness_max"/>
<field name="thickness_uom"/>
<field name="spec_reference"/>
</group>
</group>
<notebook>
<page string="Treatments" name="treatments">
<group>
<group string="Pre-Treatments">
<field name="pre_treatment_ids" widget="many2many_tags" nolabel="1"/>
</group>
<group string="Post-Treatments">
<field name="post_treatment_ids" widget="many2many_tags" nolabel="1"/>
</group>
</group>
</page>
<page string="Description" name="description">
<field name="description" placeholder="Detailed description of this coating configuration..."/>
</page>
<page string="Thickness Options" name="thickness_options">
<p class="text-muted">
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.
</p>
<field name="thickness_option_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="value" string="Nominal"/>
<field name="value_min" string="Min"/>
<field name="value_max" string="Max"/>
<field name="uom"/>
<field name="display_name" string="Display" readonly="1"/>
<field name="active" widget="boolean_toggle"/>
</list>
</field>
</page>
</notebook>
<group>
<field name="active" widget="boolean_toggle"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- ===== Coating Configuration Search View ===== -->
<record id="view_fp_coating_config_search" model="ir.ui.view">
<field name="name">fp.coating.config.search</field>
<field name="model">fp.coating.config</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="process_type_id"/>
<field name="spec_reference"/>
<separator/>
<filter string="Commercial" name="commercial" domain="[('certification_level','=','commercial')]"/>
<filter string="Mil-Spec" name="mil_spec" domain="[('certification_level','=','mil_spec')]"/>
<filter string="Nadcap" name="nadcap" domain="[('certification_level','=','nadcap')]"/>
<filter string="Nuclear" name="nuclear" domain="[('certification_level','=','nuclear')]"/>
<separator/>
<filter string="Low Phosphorus" name="low_phos" domain="[('phosphorus_level','=','low_phos')]"/>
<filter string="Mid Phosphorus" name="mid_phos" domain="[('phosphorus_level','=','mid_phos')]"/>
<filter string="High Phosphorus" name="high_phos" domain="[('phosphorus_level','=','high_phos')]"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<group>
<filter string="Process Type" name="group_process_type" context="{'group_by':'process_type_id'}"/>
<filter string="Certification Level" name="group_cert_level" context="{'group_by':'certification_level'}"/>
</group>
</search>
</field>
</record>
<!-- ===== Window Action ===== -->
<record id="action_fp_coating_config" model="ir.actions.act_window">
<field name="name">Coating Configurations</field>
<field name="res_model">fp.coating.config</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fp_coating_config_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No coating configurations defined yet
</p>
<p>
Define coating setups with process type, phosphorus level,
thickness range, spec reference, and required treatments.
</p>
</field>
</record>
</odoo>

View File

@@ -1,94 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Standalone views for fp.coating.thickness so SO-line m2o pickers
can offer "Create and edit..." — the inline-on-coating-config
editor was the only way to add thicknesses pre-Sub-12d.
-->
<odoo>
<record id="view_fp_coating_thickness_list" model="ir.ui.view">
<field name="name">fp.coating.thickness.list</field>
<field name="model">fp.coating.thickness</field>
<field name="arch" type="xml">
<list string="Coating Thicknesses" decoration-muted="not active">
<field name="sequence" widget="handle"/>
<field name="coating_config_id"/>
<field name="value" string="Nominal"/>
<field name="value_min" string="Min" optional="show"/>
<field name="value_max" string="Max" optional="show"/>
<field name="uom"/>
<field name="display_name" string="Label"/>
<field name="active" widget="boolean_toggle"/>
</list>
</field>
</record>
<record id="view_fp_coating_thickness_form" model="ir.ui.view">
<field name="name">fp.coating.thickness.form</field>
<field name="model">fp.coating.thickness</field>
<field name="arch" type="xml">
<form string="Coating Thickness">
<sheet>
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
<div class="oe_title">
<label for="display_name" string="Thickness"/>
<h2><field name="display_name" readonly="1" placeholder="Auto-generated from value + UoM"/></h2>
</div>
<group>
<group string="Spec">
<field name="coating_config_id"
options="{'no_create_edit': True}"/>
<field name="value" string="Nominal"/>
<field name="uom"/>
</group>
<group string="Acceptance Band (optional)">
<field name="value_min" string="Min"/>
<field name="value_max" string="Max"/>
<div colspan="2" class="text-muted">
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.
</div>
</group>
<group>
<field name="sequence"/>
<field name="active" widget="boolean_toggle"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_fp_coating_thickness_search" model="ir.ui.view">
<field name="name">fp.coating.thickness.search</field>
<field name="model">fp.coating.thickness</field>
<field name="arch" type="xml">
<search>
<field name="coating_config_id"/>
<field name="display_name"/>
<field name="uom"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<group>
<filter string="Coating" name="group_coating"
context="{'group_by':'coating_config_id'}"/>
<filter string="UoM" name="group_uom"
context="{'group_by':'uom'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_coating_thickness" model="ir.actions.act_window">
<field name="name">Coating Thicknesses</field>
<field name="res_model">fp.coating.thickness</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fp_coating_thickness_search"/>
</record>
</odoo>

View File

@@ -87,30 +87,12 @@
sequence="8"
groups="group_fp_estimator"/>
<menuitem id="menu_fp_coating_configs"
name="Coating Configurations"
parent="menu_fp_configurator"
action="action_fp_coating_config"
sequence="20"/>
<menuitem id="menu_fp_pricing_rules"
name="Pricing Rules"
parent="menu_fp_configurator"
action="action_fp_pricing_rule"
sequence="30"/>
<menuitem id="menu_fp_customer_price_lists"
name="Customer Price Lists"
parent="menu_fp_configurator"
action="action_fp_customer_price_list"
sequence="35"/>
<menuitem id="menu_fp_treatments"
name="Treatments"
parent="menu_fp_configurator"
action="action_fp_treatment"
sequence="40"/>
<menuitem id="menu_fp_part_materials"
name="Materials"
parent="menu_fp_configurator"

View File

@@ -1,86 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_fp_customer_price_list_list" model="ir.ui.view">
<field name="name">fp.customer.price.list.list</field>
<field name="model">fp.customer.price.list</field>
<field name="arch" type="xml">
<list editable="bottom">
<field name="partner_id"/>
<field name="coating_config_id"/>
<field name="currency_id" column_invisible="True"/>
<field name="unit_price" widget="monetary"
options="{'currency_field': 'currency_id'}" sum="Total"/>
<field name="price_uom"/>
<field name="min_quantity"/>
<field name="effective_from"/>
<field name="effective_to"/>
<field name="active" widget="boolean_toggle"/>
</list>
</field>
</record>
<record id="view_fp_customer_price_list_form" model="ir.ui.view">
<field name="name">fp.customer.price.list.form</field>
<field name="model">fp.customer.price.list</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_title">
<h2><field name="name" readonly="1"/></h2>
</div>
<group>
<group>
<field name="partner_id"/>
<field name="coating_config_id"/>
<field name="currency_id"/>
<field name="unit_price" widget="monetary"
options="{'currency_field': 'currency_id'}"/>
<field name="price_uom"/>
</group>
<group>
<field name="effective_from"/>
<field name="effective_to"/>
<field name="min_quantity"/>
<field name="active" widget="boolean_toggle"/>
</group>
</group>
<separator string="Notes"/>
<field name="notes" colspan="2"/>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_customer_price_list_search" model="ir.ui.view">
<field name="name">fp.customer.price.list.search</field>
<field name="model">fp.customer.price.list</field>
<field name="arch" type="xml">
<search>
<field name="partner_id"/>
<field name="coating_config_id"/>
<filter name="active" string="Active"
domain="[('active', '=', True)]"/>
<filter name="expired" string="Expired"
domain="[('effective_to', '&lt;', context_today().strftime('%Y-%m-%d'))]"/>
<separator/>
<group>
<filter name="group_customer" string="Customer"
context="{'group_by': 'partner_id'}"/>
<filter name="group_coating" string="Coating"
context="{'group_by': 'coating_config_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_customer_price_list" model="ir.actions.act_window">
<field name="name">Customer Price Lists</field>
<field name="res_model">fp.customer.price.list</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fp_customer_price_list_search"/>
<field name="context">{'search_default_active': 1}</field>
</record>
</odoo>

View File

@@ -201,20 +201,13 @@
class="btn-link"/>
</list>
</field>
<separator string="Default Treatments" class="mt-4"/>
<group>
<field name="x_fc_default_coating_config_id"
string="Default Treatment"
options="{'no_create_edit': True}"/>
<field name="x_fc_default_treatment_ids"
string="Default Additional Treatments"
widget="many2many_tags"
options="{'no_create_edit': True}"/>
</group>
<!-- Default Specification picker added by
fusion_plating_quality view inherit. -->
<p class="text-muted">
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.
</p>
</page>
<page string="Dimensions &amp; Complexity" name="dimensions">

View File

@@ -14,7 +14,6 @@
<list string="Pricing Rules" decoration-muted="not active">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="coating_config_id"/>
<field name="substrate_material"/>
<field name="certification_level"/>
<field name="pricing_method"/>
@@ -42,7 +41,6 @@
</div>
<group string="Filters">
<group>
<field name="coating_config_id"/>
<field name="substrate_material"/>
<field name="certification_level"/>
</group>
@@ -104,7 +102,6 @@
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="coating_config_id"/>
<separator/>
<filter string="Per Square Inch" name="per_sqin" domain="[('pricing_method','=','per_sqin')]"/>
<filter string="Per Square Foot" name="per_sqft" domain="[('pricing_method','=','per_sqft')]"/>
@@ -113,7 +110,6 @@
<separator/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<group>
<filter string="Coating Config" name="group_coating_config" context="{'group_by':'coating_config_id'}"/>
<filter string="Pricing Method" name="group_pricing_method" context="{'group_by':'pricing_method'}"/>
</group>
</search>

View File

@@ -129,7 +129,7 @@
<group string="Customer &amp; Part">
<field name="partner_id"/>
<field name="part_catalog_id"/>
<field name="coating_config_id"/>
<field name="recipe_id"/>
<!-- 3D File: upload before, filename + clear button after -->
<field name="upload_3d_file" filename="upload_3d_filename"
invisible="state != 'draft' or model_attachment_id"
@@ -325,7 +325,7 @@
<field name="create_date" string="Date"/>
<field name="name"/>
<field name="partner_id"/>
<field name="coating_config_id"/>
<field name="recipe_id"/>
<field name="surface_area"/>
<field name="quantity"/>
<field name="currency_id" column_invisible="1"/>
@@ -350,14 +350,14 @@
<search>
<field name="name"/>
<field name="partner_id"/>
<field name="coating_config_id"/>
<field name="recipe_id"/>
<separator/>
<filter string="Draft" name="draft" domain="[('state', '=', 'draft')]"/>
<filter string="Confirmed" name="confirmed" domain="[('state', '=', 'confirmed')]"/>
<filter string="Cancelled" name="cancelled" domain="[('state', '=', 'cancelled')]"/>
<group>
<filter string="Customer" name="group_customer" context="{'group_by': 'partner_id'}"/>
<filter string="Coating Config" name="group_coating" context="{'group_by': 'coating_config_id'}"/>
<filter string="Recipe" name="group_recipe" context="{'group_by': 'recipe_id'}"/>
<filter string="Status" name="group_state" context="{'group_by': 'state'}"/>
</group>
</search>

View File

@@ -22,7 +22,6 @@
decoration-danger="tag == 'rework'"
decoration-success="tag in ('aerospace','nuclear')"/>
<field name="partner_id" optional="show"/>
<field name="coating_config_id" optional="hide"/>
<field name="usage_count" string="Used"/>
<field name="active" widget="boolean_toggle"/>
</list>
@@ -46,9 +45,6 @@
<field name="tag"/>
</group>
<group>
<field name="coating_config_id"
help="Only used for generic (no-part) templates."
invisible="part_catalog_id"/>
<field name="sequence"/>
<field name="usage_count" readonly="1"/>
<field name="active" widget="boolean_toggle"/>
@@ -75,7 +71,6 @@
<field name="internal_description"/>
<field name="customer_facing_description"/>
<field name="part_catalog_id"/>
<field name="coating_config_id"/>
<field name="partner_id"/>
<field name="tag"/>
<filter name="active" string="Active" domain="[('active','=',True)]"/>

View File

@@ -1,100 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
-->
<odoo>
<!-- ===== Treatment List View ===== -->
<record id="view_fp_treatment_list" model="ir.ui.view">
<field name="name">fp.treatment.list</field>
<field name="model">fp.treatment</field>
<field name="arch" type="xml">
<list string="Treatments">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="treatment_type"/>
<field name="default_duration_minutes" string="Duration (min)"/>
<field name="currency_id" column_invisible="1"/>
<field name="default_cost" widget="monetary"
options="{'currency_field': 'currency_id'}" sum="Total"/>
<field name="active" widget="boolean_toggle"/>
</list>
</field>
</record>
<!-- ===== Treatment Form View ===== -->
<record id="view_fp_treatment_form" model="ir.ui.view">
<field name="name">fp.treatment.form</field>
<field name="model">fp.treatment</field>
<field name="arch" type="xml">
<form string="Treatment">
<sheet>
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" placeholder="e.g. Bead Blast"/></h1>
</div>
<group>
<group>
<field name="treatment_type"/>
<field name="sequence"/>
</group>
<group>
<label for="default_duration_minutes"/>
<div class="o_row">
<field name="default_duration_minutes" nolabel="1" class="oe_inline"/>
<span class="ms-1">min</span>
</div>
<field name="currency_id"/>
<field name="default_cost" widget="monetary"
options="{'currency_field': 'currency_id'}"/>
</group>
</group>
<field name="description" placeholder="Description of this treatment step..."/>
<group>
<field name="active" widget="boolean_toggle"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- ===== Treatment Search View ===== -->
<record id="view_fp_treatment_search" model="ir.ui.view">
<field name="name">fp.treatment.search</field>
<field name="model">fp.treatment</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<separator/>
<filter string="Pre-Treatment" name="pre" domain="[('treatment_type','=','pre')]"/>
<filter string="Post-Treatment" name="post" domain="[('treatment_type','=','post')]"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<group>
<filter string="Type" name="group_type" context="{'group_by':'treatment_type'}"/>
</group>
</search>
</field>
</record>
<!-- ===== Window Action ===== -->
<record id="action_fp_treatment" model="ir.actions.act_window">
<field name="name">Treatments</field>
<field name="res_model">fp.treatment</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fp_treatment_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No treatments defined yet
</p>
<p>
Add pre-treatment steps (bead blast, zincate, acid etch) and
post-treatment steps (bake, passivate, chromate seal).
</p>
</field>
</record>
</odoo>

View File

@@ -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">
<list create="false" delete="false" edit="false">
<field name="x_fc_part_catalog_id"/>
<field name="x_fc_coating_config_id"/>
<field name="x_fc_thickness_id" optional="show"/>
<field name="x_fc_process_variant_id" optional="show"
string="Process"/>
@@ -251,7 +250,6 @@
<field name="x_fc_internal_description"
placeholder="Shop-floor workflow instructions (prints on WO / traveler)"
optional="hide"/>
<field name="x_fc_coating_config_id" optional="show"/>
<field name="x_fc_process_variant_id"
string="Process / Recipe"
options="{'no_quick_create': True}"
@@ -290,7 +288,6 @@
<field name="x_fc_revision_snapshot"
readonly="1"
optional="hide"/>
<field name="x_fc_treatment_ids" widget="many2many_tags" invisible="1"/>
<field name="x_fc_part_deadline" string="Part Deadline Override" optional="hide"/>
<field name="x_fc_part_deadline_offset_days" string="Days Offset" optional="hide"/>
<field name="x_fc_effective_part_deadline" string="Effective Deadline"
@@ -335,7 +332,6 @@
<field name="x_fc_wo_completion" optional="show"/>
<field name="x_fc_planned_start_date" optional="hide"/>
<field name="x_fc_part_catalog_id" optional="hide"/>
<field name="x_fc_coating_config_id" optional="hide"/>
<field name="amount_total" sum="Total"/>
<field name="x_fc_invoiced_amount" sum="Invoiced" optional="hide"
widget="monetary"
@@ -363,7 +359,6 @@
<field name="arch" type="xml">
<kanban default_group_by="x_fc_part_catalog_id" records_draggable="0">
<field name="x_fc_part_catalog_id"/>
<field name="x_fc_coating_config_id"/>
<field name="product_uom_qty"/>
<field name="qty_delivered"/>
<field name="x_fc_wo_group_tag"/>
@@ -373,7 +368,7 @@
<t t-name="card">
<div class="o_kanban_card_content">
<div class="o_kanban_record_title">
<strong><field name="x_fc_coating_config_id"/></strong>
<strong><field name="x_fc_part_catalog_id"/></strong>
</div>
<div class="text-muted">
Qty: <field name="product_uom_qty"/>
@@ -399,7 +394,6 @@
<kanban default_group_by="x_fc_wo_group_tag" records_draggable="0">
<field name="x_fc_wo_group_tag"/>
<field name="x_fc_part_catalog_id"/>
<field name="x_fc_coating_config_id"/>
<field name="product_uom_qty"/>
<templates>
<t t-name="card">
@@ -407,9 +401,6 @@
<div>
<strong><field name="x_fc_part_catalog_id"/></strong>
</div>
<div class="text-muted">
<field name="x_fc_coating_config_id"/>
</div>
<div>
Qty: <field name="product_uom_qty"/>
</div>

View File

@@ -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.'
))

View File

@@ -22,7 +22,7 @@
<list>
<field name="name"/>
<field name="part_catalog_id"/>
<field name="coating_config_id"/>
<field name="recipe_id"/>
<field name="quantity"/>
<field name="calculated_price" widget="monetary"/>
<field name="estimator_override_price" widget="monetary"/>

View File

@@ -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,

View File

@@ -27,7 +27,7 @@
<list>
<field name="name"/>
<field name="x_fc_part_catalog_id"/>
<field name="x_fc_coating_config_id"/>
<field name="x_fc_part_deadline" optional="hide"/>
<field name="product_uom_qty"/>
<field name="price_unit"/>
<field name="x_fc_part_deadline"/>

View File

@@ -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,

View File

@@ -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.'

View File

@@ -154,8 +154,6 @@
optional="hide"/>
<field name="internal_description"
optional="hide"/>
<field name="coating_config_id"
optional="show"/>
<field name="process_variant_id"
string="Process / Recipe"
options="{'no_quick_create': True}"
@@ -194,9 +192,6 @@
class="btn-link"
invisible="not part_catalog_id or serial_count &gt; 0"/>
<field name="job_number" optional="hide"/>
<field name="treatment_ids"
widget="many2many_tags"
invisible="1"/>
<field name="quantity"
optional="show"/>
<field name="unit_price"
@@ -239,9 +234,6 @@
invisible="not part_catalog_id"/>
<field name="part_revision"
invisible="not part_catalog_id"/>
<field name="coating_config_id"/>
<field name="treatment_ids"
widget="many2many_tags"/>
<field name="process_variant_id"
string="Process / Recipe"
options="{'no_quick_create': True}"