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:
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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)
|
||||
@@ -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})
|
||||
@@ -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]
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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')],
|
||||
|
||||
@@ -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
|
||||
),
|
||||
})],
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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.'),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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 '
|
||||
|
||||
@@ -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,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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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', '<', 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>
|
||||
@@ -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 & Complexity" name="dimensions">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
<group string="Customer & 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>
|
||||
|
||||
@@ -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)]"/>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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.'
|
||||
))
|
||||
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.'
|
||||
|
||||
@@ -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 > 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}"
|
||||
|
||||
Reference in New Issue
Block a user