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:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Certificates',
|
||||
'version': '19.0.5.6.0',
|
||||
'version': '19.0.6.0.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Certificate registry for CoC, thickness reports, and quality documents.',
|
||||
'description': """
|
||||
|
||||
@@ -286,14 +286,27 @@ class FpCertificate(models.Model):
|
||||
def create(self, vals_list):
|
||||
SaleOrder = self.env['sale.order']
|
||||
for vals in vals_list:
|
||||
# Spec-limit auto-fill (existing behaviour, preserved).
|
||||
# Spec-limit auto-fill — sources thickness range from the
|
||||
# recipe (Phase A moved the thickness fields onto the
|
||||
# recipe root). Falls back gracefully when the SO has no
|
||||
# recipe-bearing line.
|
||||
already_set = vals.get('spec_min_mils') or vals.get('spec_max_mils')
|
||||
if not already_set and vals.get('sale_order_id'):
|
||||
so = SaleOrder.browse(vals['sale_order_id'])
|
||||
cfg = getattr(so, 'x_fc_coating_config_id', False)
|
||||
if cfg and cfg.thickness_uom == 'mils':
|
||||
vals.setdefault('spec_min_mils', cfg.thickness_min or 0.0)
|
||||
vals.setdefault('spec_max_mils', cfg.thickness_max or 0.0)
|
||||
# Look across order_line for the first recipe with a
|
||||
# populated thickness range.
|
||||
first_line = so.order_line[:1] if so.order_line else False
|
||||
recipe = (
|
||||
first_line.x_fc_process_variant_id
|
||||
if (first_line
|
||||
and 'x_fc_process_variant_id' in first_line._fields)
|
||||
else False
|
||||
)
|
||||
if (recipe
|
||||
and 'thickness_uom' in recipe._fields
|
||||
and recipe.thickness_uom == 'mils'):
|
||||
vals.setdefault('spec_min_mils', recipe.thickness_min or 0.0)
|
||||
vals.setdefault('spec_max_mils', recipe.thickness_max or 0.0)
|
||||
# Defer naming: let the record exist so the mixin can write
|
||||
# name via raw SQL, then fall back to the legacy sequence if
|
||||
# no parent SO is reachable.
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
{
|
||||
'name': 'Fusion Plating — Native Jobs',
|
||||
'version': '19.0.9.1.0',
|
||||
'version': '19.0.10.0.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
||||
'author': 'Nexa Systems Inc.',
|
||||
@@ -39,7 +39,7 @@ full design rationale and §6.2 of the implementation plan for task list.
|
||||
'fusion_plating', # fp.job, fp.job.step, fp.work.centre
|
||||
'fusion_plating_batch', # fusion.plating.batch (Phase 3)
|
||||
'fusion_plating_certificates', # fp.certificate, fp.thickness.reading
|
||||
'fusion_plating_configurator', # fp.part.catalog, fp.coating.config
|
||||
'fusion_plating_configurator', # fp.part.catalog
|
||||
'fusion_plating_kpi', # fusion.plating.kpi.value (Phase 4)
|
||||
'fusion_plating_logistics', # fusion.plating.delivery
|
||||
'fusion_plating_notifications', # fp.notification.template (Phase 4)
|
||||
|
||||
@@ -48,15 +48,12 @@ class FpJob(models.Model):
|
||||
string='Part',
|
||||
ondelete='restrict',
|
||||
)
|
||||
coating_config_id = fields.Many2one(
|
||||
'fp.coating.config',
|
||||
string='Coating Configuration',
|
||||
ondelete='restrict',
|
||||
)
|
||||
customer_spec_id = fields.Many2one(
|
||||
'fusion.plating.customer.spec',
|
||||
string='Customer Spec',
|
||||
string='Specification',
|
||||
ondelete='set null',
|
||||
help='Customer / industry spec the job ships under. Auto-filled '
|
||||
'from the SO line at job creation.',
|
||||
)
|
||||
portal_job_id = fields.Many2one(
|
||||
'fusion.plating.portal.job',
|
||||
@@ -996,29 +993,28 @@ class FpJob(models.Model):
|
||||
if node.estimated_duration:
|
||||
vals['dwell_time_minutes'] = node.estimated_duration
|
||||
|
||||
# Pull thickness target from the coating config when
|
||||
# this is a plating step (matched by node name keyword).
|
||||
coating = job.coating_config_id
|
||||
# Pull thickness target from the recipe root when this
|
||||
# is a plating step (matched by node name keyword).
|
||||
# Recipe-root carries thickness fields post-promote-spec.
|
||||
recipe_root = job.recipe_id
|
||||
name_l = (node.name or '').lower()
|
||||
is_plating_node = (
|
||||
'plat' in name_l or 'nickel' in name_l
|
||||
or 'chrome' in name_l or 'anodiz' in name_l
|
||||
)
|
||||
if coating and is_plating_node:
|
||||
if recipe_root and is_plating_node:
|
||||
if (
|
||||
'thickness_max' in coating._fields
|
||||
and coating.thickness_max
|
||||
'thickness_max' in recipe_root._fields
|
||||
and recipe_root.thickness_max
|
||||
):
|
||||
vals['thickness_target'] = coating.thickness_max
|
||||
vals['thickness_target'] = recipe_root.thickness_max
|
||||
if (
|
||||
'thickness_uom' in coating._fields
|
||||
and coating.thickness_uom
|
||||
'thickness_uom' in recipe_root._fields
|
||||
and recipe_root.thickness_uom
|
||||
):
|
||||
# fp.coating.config uses long-form uom names
|
||||
# (mils / microns / inches); fp.job.step uses
|
||||
# short codes (mil / um / inch). Map between
|
||||
# them. Unknown values fall through to the
|
||||
# step's default ('um').
|
||||
# Recipe uses long-form uom names (mils /
|
||||
# microns / inches); fp.job.step uses short
|
||||
# codes (mil / um / inch). Map between them.
|
||||
_UOM_MAP = {
|
||||
'mils': 'mil',
|
||||
'mil': 'mil',
|
||||
@@ -1029,7 +1025,7 @@ class FpJob(models.Model):
|
||||
'inch': 'inch',
|
||||
'in': 'inch',
|
||||
}
|
||||
mapped = _UOM_MAP.get(coating.thickness_uom)
|
||||
mapped = _UOM_MAP.get(recipe_root.thickness_uom)
|
||||
if mapped:
|
||||
vals['thickness_uom'] = mapped
|
||||
|
||||
@@ -1546,7 +1542,9 @@ class FpJob(models.Model):
|
||||
if not required:
|
||||
return
|
||||
has_job_link = 'x_fc_job_id' in Cert._fields
|
||||
coating = self.coating_config_id
|
||||
# Spec drives the cert spec_reference. The customer.spec was
|
||||
# auto-filled onto the job at confirm time (sale_order.py).
|
||||
spec = self.customer_spec_id
|
||||
for cert_type in sorted(required):
|
||||
# Idempotency per type.
|
||||
existing_dom = [('certificate_type', '=', cert_type)]
|
||||
@@ -1574,9 +1572,16 @@ class FpJob(models.Model):
|
||||
if 'sale_order_id' in Cert._fields and self.sale_order_id:
|
||||
vals['sale_order_id'] = self.sale_order_id.id
|
||||
# spec_reference is what action_issue blocks on.
|
||||
if coating and 'spec_reference' in Cert._fields \
|
||||
and getattr(coating, 'spec_reference', False):
|
||||
vals['spec_reference'] = coating.spec_reference
|
||||
# Format spec.code + revision for the cert text.
|
||||
if spec and 'spec_reference' in Cert._fields:
|
||||
ref = spec.code or ''
|
||||
if spec.revision:
|
||||
ref = (f'{ref} Rev {spec.revision}'
|
||||
if ref else f'Rev {spec.revision}')
|
||||
if ref:
|
||||
vals['spec_reference'] = ref
|
||||
if 'customer_spec_id' in Cert._fields:
|
||||
vals['customer_spec_id'] = spec.id
|
||||
if 'part_number' in Cert._fields and self.part_catalog_id:
|
||||
vals['part_number'] = (
|
||||
self.part_catalog_id.part_number or ''
|
||||
|
||||
@@ -474,8 +474,9 @@ class FpJobStep(models.Model):
|
||||
def button_finish(self):
|
||||
"""Override to:
|
||||
1) Auto-spawn a bake.window when a wet plating step finishes
|
||||
on a coating that requires hydrogen-embrittlement relief
|
||||
(AS9100 / Nadcap compliance);
|
||||
on a recipe that requires hydrogen-embrittlement relief
|
||||
(AS9100 / Nadcap compliance). Bake fields live on the
|
||||
recipe root post-promote-customer-spec.
|
||||
2) Post a chatter warning when duration_actual exceeds 1.5×
|
||||
duration_expected — silent overruns are a red flag for
|
||||
scheduling and costing.
|
||||
@@ -499,12 +500,11 @@ class FpJobStep(models.Model):
|
||||
'estimate too tight.'
|
||||
)) % (step.name, ratio, step.duration_expected,
|
||||
step.duration_actual))
|
||||
coating = step.job_id.coating_config_id \
|
||||
if 'coating_config_id' in step.job_id._fields else False
|
||||
if not coating:
|
||||
recipe_root = step.job_id.recipe_id
|
||||
if not recipe_root:
|
||||
continue
|
||||
requires = getattr(coating, 'requires_bake_relief', False)
|
||||
window_hrs = getattr(coating, 'bake_window_hours', 0.0)
|
||||
requires = getattr(recipe_root, 'requires_bake_relief', False)
|
||||
window_hrs = getattr(recipe_root, 'bake_window_hours', 0.0)
|
||||
if not requires or not window_hrs:
|
||||
continue
|
||||
# Trigger only on the actual plating-out step. We want
|
||||
|
||||
@@ -339,11 +339,8 @@ class SaleOrder(models.Model):
|
||||
1. line.x_fc_process_variant_id — Sarah explicitly picked a
|
||||
part-scoped variant on this order line. Always wins.
|
||||
2. part.default_process_id — part's flagged default
|
||||
variant. Customer-and-part-tuned recipe; must beat any
|
||||
generic coating template.
|
||||
3. coating.recipe_id — coating-config recipe
|
||||
(generic template fallback).
|
||||
4. part.recipe_id — legacy fallback.
|
||||
variant. Customer-and-part-tuned recipe.
|
||||
3. part.recipe_id — legacy fallback.
|
||||
Returns the recipe record or an empty recordset.
|
||||
"""
|
||||
Node = self.env['fusion.plating.process.node']
|
||||
@@ -352,11 +349,6 @@ class SaleOrder(models.Model):
|
||||
) or False
|
||||
if not part and 'x_fc_part_catalog_id' in self._fields:
|
||||
part = self.x_fc_part_catalog_id or False
|
||||
coating = (
|
||||
'x_fc_coating_config_id' in line._fields and line.x_fc_coating_config_id
|
||||
) or False
|
||||
if not coating and 'x_fc_coating_config_id' in self._fields:
|
||||
coating = self.x_fc_coating_config_id or False
|
||||
picked = (
|
||||
'x_fc_process_variant_id' in line._fields
|
||||
and line.x_fc_process_variant_id
|
||||
@@ -365,8 +357,6 @@ class SaleOrder(models.Model):
|
||||
return picked
|
||||
if part and 'default_process_id' in part._fields and part.default_process_id:
|
||||
return part.default_process_id
|
||||
if coating and 'recipe_id' in coating._fields and coating.recipe_id:
|
||||
return coating.recipe_id
|
||||
if part and 'recipe_id' in part._fields and part.recipe_id:
|
||||
return part.recipe_id
|
||||
return Node
|
||||
@@ -389,22 +379,22 @@ class SaleOrder(models.Model):
|
||||
if existing:
|
||||
return
|
||||
|
||||
# Find plating lines (those with a part_catalog_id or coating_config_id)
|
||||
# Find plating lines (those with a part_catalog_id or
|
||||
# customer_spec_id).
|
||||
plating_lines = self.order_line.filtered(
|
||||
lambda l: (
|
||||
('x_fc_part_catalog_id' in l._fields and l.x_fc_part_catalog_id)
|
||||
or ('x_fc_coating_config_id' in l._fields and l.x_fc_coating_config_id)
|
||||
or ('x_fc_customer_spec_id' in l._fields and l.x_fc_customer_spec_id)
|
||||
)
|
||||
)
|
||||
# Fallback: legacy/configurator SOs that carry part+coating on the
|
||||
# header but not on the line. Treat the entire order as one
|
||||
# plating line so the planner gets an fp.job to work against.
|
||||
# Fallback: SOs that carry part on the header but not on the
|
||||
# line. Treat the entire order as one plating job so the planner
|
||||
# gets an fp.job to work against.
|
||||
if not plating_lines and self.order_line and (
|
||||
('x_fc_part_catalog_id' in self._fields and self.x_fc_part_catalog_id)
|
||||
or ('x_fc_coating_config_id' in self._fields and self.x_fc_coating_config_id)
|
||||
'x_fc_part_catalog_id' in self._fields and self.x_fc_part_catalog_id
|
||||
):
|
||||
_logger.info(
|
||||
'SO %s: no line-level part/coating but header carries one — '
|
||||
'SO %s: no line-level part but header carries one — '
|
||||
'treating all lines as a single plating job.', self.name,
|
||||
)
|
||||
plating_lines = self.order_line
|
||||
@@ -412,13 +402,12 @@ class SaleOrder(models.Model):
|
||||
_logger.info('SO %s: no plating lines, skipping job creation.', self.name)
|
||||
return
|
||||
|
||||
# Group by (recipe, part, coating, thickness, serial). Lines that
|
||||
# share ALL FIVE collapse into one WO. Same compliance reasoning
|
||||
# as part_id + coating_id: bundling lines with different thicknesses
|
||||
# or different serials under one WO would carry the first line's
|
||||
# values onto the cert + sticker — silent mis-attestation. Sub 5
|
||||
# added thickness_id + serial_id; this extends the grouping logic
|
||||
# to honour them. No-recipe lines still get their own group each.
|
||||
# Group by (recipe, part, spec, thickness, serial). Lines that
|
||||
# share ALL FIVE collapse into one WO. Bundling lines with
|
||||
# different specs / thicknesses / serials under one WO would
|
||||
# carry the first line's values onto the cert + sticker —
|
||||
# silent mis-attestation. No-recipe lines still get their own
|
||||
# group each.
|
||||
groups = {}
|
||||
unrecipe_idx = 0
|
||||
for line in plating_lines:
|
||||
@@ -427,9 +416,9 @@ class SaleOrder(models.Model):
|
||||
'x_fc_part_catalog_id' in line._fields
|
||||
and line.x_fc_part_catalog_id.id
|
||||
) or False
|
||||
coating_id = (
|
||||
'x_fc_coating_config_id' in line._fields
|
||||
and line.x_fc_coating_config_id.id
|
||||
spec_id = (
|
||||
'x_fc_customer_spec_id' in line._fields
|
||||
and line.x_fc_customer_spec_id.id
|
||||
) or False
|
||||
thickness_id = (
|
||||
'x_fc_thickness_id' in line._fields
|
||||
@@ -440,7 +429,7 @@ class SaleOrder(models.Model):
|
||||
and line.x_fc_serial_id.id
|
||||
) or False
|
||||
if recipe:
|
||||
key = (recipe.id, part_id, coating_id, thickness_id, serial_id)
|
||||
key = (recipe.id, part_id, spec_id, thickness_id, serial_id)
|
||||
else:
|
||||
unrecipe_idx += 1
|
||||
key = ('no_recipe', unrecipe_idx)
|
||||
@@ -465,11 +454,6 @@ class SaleOrder(models.Model):
|
||||
and first_line.x_fc_part_catalog_id
|
||||
or False
|
||||
)
|
||||
coating = (
|
||||
'x_fc_coating_config_id' in first_line._fields
|
||||
and first_line.x_fc_coating_config_id
|
||||
or False
|
||||
)
|
||||
customer_spec = (
|
||||
'x_fc_customer_spec_id' in first_line._fields
|
||||
and first_line.x_fc_customer_spec_id
|
||||
@@ -477,8 +461,6 @@ class SaleOrder(models.Model):
|
||||
)
|
||||
if not part and 'x_fc_part_catalog_id' in self._fields:
|
||||
part = self.x_fc_part_catalog_id or False
|
||||
if not coating and 'x_fc_coating_config_id' in self._fields:
|
||||
coating = self.x_fc_coating_config_id or False
|
||||
recipe = self._fp_resolve_recipe_for_line(first_line)
|
||||
|
||||
vals = {
|
||||
@@ -492,8 +474,6 @@ class SaleOrder(models.Model):
|
||||
}
|
||||
if part:
|
||||
vals['part_catalog_id'] = part.id
|
||||
if coating:
|
||||
vals['coating_config_id'] = coating.id
|
||||
if customer_spec:
|
||||
vals['customer_spec_id'] = customer_spec.id
|
||||
if recipe:
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
<t t-set="_so" t-value="job.sale_order_id"/>
|
||||
<t t-set="_line" t-value="job.sale_order_line_ids[:1]"/>
|
||||
<t t-set="_part" t-value="('part_catalog_id' in job._fields and job.part_catalog_id) or False"/>
|
||||
<t t-set="_coating" t-value="('coating_config_id' in job._fields and job.coating_config_id) or False"/>
|
||||
<t t-set="_spec" t-value="('customer_spec_id' in job._fields and job.customer_spec_id) or False"/>
|
||||
<t t-set="_process" t-value="job.recipe_id or False"/>
|
||||
<t t-set="_due" t-value="job.date_deadline or False"/>
|
||||
@@ -99,7 +98,6 @@
|
||||
<t t-set="_so" t-value="job.sale_order_id"/>
|
||||
<t t-set="_line" t-value="job.sale_order_line_ids[:1]"/>
|
||||
<t t-set="_part" t-value="('part_catalog_id' in job._fields and job.part_catalog_id) or False"/>
|
||||
<t t-set="_coating" t-value="('coating_config_id' in job._fields and job.coating_config_id) or False"/>
|
||||
<t t-set="_spec" t-value="('customer_spec_id' in job._fields and job.customer_spec_id) or False"/>
|
||||
<t t-set="_process" t-value="job.recipe_id or False"/>
|
||||
<t t-set="_due" t-value="job.date_deadline or False"/>
|
||||
|
||||
@@ -203,9 +203,6 @@
|
||||
<t t-if="'customer_spec_id' in job._fields and job.customer_spec_id">
|
||||
<span t-esc="job.customer_spec_id.display_name"/>
|
||||
</t>
|
||||
<t t-elif="'coating_config_id' in job._fields and job.coating_config_id">
|
||||
<span t-esc="job.coating_config_id.name"/>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='product_id']" position="after">
|
||||
<field name="part_catalog_id" string="Part"/>
|
||||
<field name="coating_config_id" string="Coating"/>
|
||||
<field name="customer_spec_id" string="Specification"/>
|
||||
<field name="recipe_id" string="Process Recipe"/>
|
||||
</xpath>
|
||||
<!-- Show qty completed alongside total so the partial-qty
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Quality (QMS)',
|
||||
'version': '19.0.5.3.0',
|
||||
'version': '19.0.6.0.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Native QMS for plating shops: NCR, CAPA, calibration, AVL, FAIR, '
|
||||
'internal audits, customer specs, document control. CE + EE compatible.',
|
||||
|
||||
@@ -63,10 +63,6 @@ class FpQualityPoint(models.Model):
|
||||
'fp.part.catalog', 'fp_quality_point_part_rel',
|
||||
'point_id', 'part_id', string='Parts',
|
||||
)
|
||||
coating_config_ids = fields.Many2many(
|
||||
'fp.coating.config', 'fp_quality_point_coating_rel',
|
||||
'point_id', 'coating_id', string='Coatings',
|
||||
)
|
||||
customer_spec_ids = fields.Many2many(
|
||||
'fusion.plating.customer.spec',
|
||||
'fp_quality_point_spec_rel',
|
||||
@@ -119,7 +115,7 @@ class FpQualityPoint(models.Model):
|
||||
# ------------------------------------------------------------------
|
||||
# Matching + spawning
|
||||
# ------------------------------------------------------------------
|
||||
def _matches(self, partner=None, part=None, coating=None, step=None,
|
||||
def _matches(self, partner=None, part=None, step=None,
|
||||
customer_spec=None, recipe=None):
|
||||
"""Return True if this point's filters all pass against the supplied
|
||||
context. Empty filter == match anything.
|
||||
@@ -130,9 +126,6 @@ class FpQualityPoint(models.Model):
|
||||
if self.part_catalog_ids and (
|
||||
not part or part not in self.part_catalog_ids):
|
||||
return False
|
||||
if self.coating_config_ids and (
|
||||
not coating or coating not in self.coating_config_ids):
|
||||
return False
|
||||
if self.customer_spec_ids and (
|
||||
not customer_spec
|
||||
or customer_spec not in self.customer_spec_ids):
|
||||
@@ -146,7 +139,7 @@ class FpQualityPoint(models.Model):
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def _find_matching(self, trigger, partner=None, part=None, coating=None,
|
||||
def _find_matching(self, trigger, partner=None, part=None,
|
||||
step=None, customer_spec=None, recipe=None):
|
||||
"""Return active points whose trigger + filters match the context."""
|
||||
candidates = self.search([
|
||||
@@ -154,7 +147,7 @@ class FpQualityPoint(models.Model):
|
||||
('trigger_type', '=', trigger),
|
||||
])
|
||||
return candidates.filtered(lambda p: p._matches(
|
||||
partner=partner, part=part, coating=coating, step=step,
|
||||
partner=partner, part=part, step=step,
|
||||
customer_spec=customer_spec, recipe=recipe,
|
||||
))
|
||||
|
||||
|
||||
@@ -52,22 +52,16 @@ class SaleOrderPointHook(models.Model):
|
||||
# Walk lines for part / coating / spec context.
|
||||
parts = so.order_line.mapped('x_fc_part_catalog_id') \
|
||||
if 'x_fc_part_catalog_id' in so.order_line._fields else False
|
||||
coatings = so.order_line.mapped('x_fc_coating_config_id') \
|
||||
if 'x_fc_coating_config_id' in so.order_line._fields else False
|
||||
specs = so.order_line.mapped('x_fc_customer_spec_id') \
|
||||
if 'x_fc_customer_spec_id' in so.order_line._fields else False
|
||||
points = Point._find_matching(
|
||||
trigger='so_confirmed', partner=partner,
|
||||
)
|
||||
for point in points:
|
||||
# Filter by part / coating / spec intersection if the
|
||||
# point cares.
|
||||
# Filter by part / spec intersection if the point cares.
|
||||
if point.part_catalog_ids and parts and \
|
||||
not (point.part_catalog_ids & parts):
|
||||
continue
|
||||
if point.coating_config_ids and coatings and \
|
||||
not (point.coating_config_ids & coatings):
|
||||
continue
|
||||
if point.customer_spec_ids and specs and \
|
||||
not (point.customer_spec_ids & specs):
|
||||
continue
|
||||
@@ -85,12 +79,11 @@ class FpJobPointHook(models.Model):
|
||||
for job in self:
|
||||
partner = job.partner_id
|
||||
part = getattr(job, 'part_catalog_id', False) or False
|
||||
coating = getattr(job, 'coating_config_id', False) or False
|
||||
customer_spec = getattr(job, 'customer_spec_id', False) or False
|
||||
recipe = getattr(job, 'recipe_id', False) or False
|
||||
points = Point._find_matching(
|
||||
trigger='job_confirmed', partner=partner,
|
||||
part=part or None, coating=coating or None,
|
||||
part=part or None,
|
||||
customer_spec=customer_spec or None,
|
||||
recipe=recipe or None,
|
||||
)
|
||||
@@ -108,12 +101,11 @@ class FpJobPointHook(models.Model):
|
||||
continue
|
||||
partner = job.partner_id
|
||||
part = getattr(job, 'part_catalog_id', False) or False
|
||||
coating = getattr(job, 'coating_config_id', False) or False
|
||||
customer_spec = getattr(job, 'customer_spec_id', False) or False
|
||||
recipe = getattr(job, 'recipe_id', False) or False
|
||||
points = Point._find_matching(
|
||||
trigger='job_done', partner=partner,
|
||||
part=part or None, coating=coating or None,
|
||||
part=part or None,
|
||||
customer_spec=customer_spec or None,
|
||||
recipe=recipe or None,
|
||||
)
|
||||
@@ -137,12 +129,11 @@ class FpJobStepPointHook(models.Model):
|
||||
job = step.job_id
|
||||
partner = job.partner_id if job else False
|
||||
part = getattr(job, 'part_catalog_id', False) or False
|
||||
coating = getattr(job, 'coating_config_id', False) or False
|
||||
customer_spec = getattr(job, 'customer_spec_id', False) or False
|
||||
recipe = getattr(job, 'recipe_id', False) or False
|
||||
points = Point._find_matching(
|
||||
trigger='job_step_done', partner=partner,
|
||||
part=part or None, coating=coating or None, step=step,
|
||||
part=part or None, step=step,
|
||||
customer_spec=customer_spec or None,
|
||||
recipe=recipe or None,
|
||||
)
|
||||
|
||||
@@ -24,19 +24,9 @@ class FpQuoteConfigurator(models.Model):
|
||||
"""Extend the configurator's matcher to consider Spec + Recipe.
|
||||
|
||||
Spec match adds +8 (highest priority — explicit customer spec
|
||||
wins over chemistry / cert-level filters). Recipe adds +6.
|
||||
Falls through to the existing coating / material / cert scoring.
|
||||
wins over chemistry filters). Recipe adds +6. Material is +2.
|
||||
"""
|
||||
# Cache the recipe before super (super may overwrite via thickness
|
||||
# logic in some inherit chains).
|
||||
recipe = (
|
||||
self.coating_config_id.recipe_id
|
||||
if self.coating_config_id and self.coating_config_id.recipe_id
|
||||
else False
|
||||
)
|
||||
# Build the candidate rule set the same way super does — but
|
||||
# since super uses a private mechanism we re-implement to keep
|
||||
# the spec/recipe scoring inline with the rest.
|
||||
recipe = self.recipe_id or False
|
||||
builder_rules = (
|
||||
recipe.pricing_rule_ids
|
||||
if recipe else self.env['fp.pricing.rule']
|
||||
@@ -49,38 +39,25 @@ 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
|
||||
# NEW — spec wins biggest
|
||||
# Spec wins biggest
|
||||
if rule.customer_spec_id:
|
||||
if rule.customer_spec_id != self.customer_spec_id:
|
||||
continue
|
||||
score += 8
|
||||
# NEW — recipe is next
|
||||
# Recipe is next
|
||||
if rule.recipe_id:
|
||||
if rule.recipe_id != recipe:
|
||||
continue
|
||||
score += 6
|
||||
# Legacy — coating / material / cert
|
||||
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
|
||||
|
||||
@@ -15,17 +15,18 @@
|
||||
<field name="inherit_id"
|
||||
ref="fusion_plating_configurator.view_fp_direct_order_wizard_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Wizard line list (main editable rows) -->
|
||||
<xpath expr="//field[@name='line_ids']/list/field[@name='coating_config_id']"
|
||||
<!-- Wizard line list (main editable rows). Anchor on
|
||||
internal_description (stable, configurator-defined). -->
|
||||
<xpath expr="//field[@name='line_ids']/list/field[@name='internal_description']"
|
||||
position="after">
|
||||
<field name="customer_spec_id"
|
||||
string="Specification"
|
||||
options="{'no_quick_create': True}"
|
||||
optional="show"/>
|
||||
</xpath>
|
||||
<!-- Wizard line drawer / form view (the "expand line" panel) -->
|
||||
<xpath expr="//field[@name='line_ids']/form//field[@name='coating_config_id']"
|
||||
position="after">
|
||||
<!-- Wizard line drawer / form view -->
|
||||
<xpath expr="//field[@name='line_ids']/form//field[@name='process_variant_id']"
|
||||
position="before">
|
||||
<field name="customer_spec_id"
|
||||
string="Specification"
|
||||
options="{'no_quick_create': True}"/>
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
<field name="inherit_id"
|
||||
ref="fusion_plating_configurator.view_fp_part_catalog_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='x_fc_default_coating_config_id']"
|
||||
<!-- Anchor on default_process_id (stable, in core).
|
||||
Default Treatment block was removed in Phase E. -->
|
||||
<xpath expr="//field[@name='default_process_id']"
|
||||
position="after">
|
||||
<field name="x_fc_default_customer_spec_id"
|
||||
string="Default Specification"
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
<field name="inherit_id"
|
||||
ref="fusion_plating_configurator.view_fp_pricing_rule_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='coating_config_id']"
|
||||
position="after">
|
||||
<xpath expr="//field[@name='substrate_material']"
|
||||
position="before">
|
||||
<field name="customer_spec_id"
|
||||
options="{'no_quick_create': True}"/>
|
||||
<field name="recipe_id"
|
||||
|
||||
@@ -69,8 +69,6 @@
|
||||
placeholder="All specs if empty"/>
|
||||
<field name="recipe_ids" widget="many2many_tags"
|
||||
placeholder="All recipes if empty"/>
|
||||
<field name="coating_config_ids" widget="many2many_tags"
|
||||
placeholder="All coatings if empty"/>
|
||||
<field name="step_kind"
|
||||
invisible="trigger_type != 'job_step_done'"
|
||||
placeholder="Any step kind if empty"/>
|
||||
|
||||
@@ -17,15 +17,18 @@
|
||||
|
||||
<!-- Configurator's view_sale_order_form_fp inherits sale.view_order_form
|
||||
and adds Plating fields to the order_line tree. We inherit THAT
|
||||
view to add Specification right after Primary Treatment. -->
|
||||
view to add Specification next to Part Catalog. -->
|
||||
<record id="view_sale_order_form_quality_inherit" model="ir.ui.view">
|
||||
<field name="name">sale.order.form.quality.spec.inherit</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id"
|
||||
ref="fusion_plating_configurator.view_sale_order_form_fp"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Editable order_line tree (estimator's main grid) -->
|
||||
<xpath expr="//field[@name='order_line']/list/field[@name='x_fc_coating_config_id']"
|
||||
<!-- Editable order_line tree (estimator's main grid).
|
||||
Anchor on x_fc_internal_description because it's
|
||||
unique to the editable list (not in the read-only
|
||||
summary list at the form bottom). -->
|
||||
<xpath expr="//field[@name='x_fc_internal_description']"
|
||||
position="after">
|
||||
<field name="x_fc_customer_spec_id"
|
||||
string="Specification"
|
||||
|
||||
@@ -115,9 +115,6 @@
|
||||
<t t-if="so and so.x_fc_customer_spec_id">
|
||||
<span t-field="so.x_fc_customer_spec_id"/>
|
||||
</t>
|
||||
<t t-elif="so and so.x_fc_coating_config_id">
|
||||
<span t-field="so.x_fc_coating_config_id"/>
|
||||
</t>
|
||||
<t t-else="">—</t>
|
||||
</td>
|
||||
<th class="info-header">Recipe</th>
|
||||
|
||||
@@ -19,8 +19,7 @@
|
||||
* _mo — the mrp.production record (or False)
|
||||
* _so, _line — the originating sale order / line
|
||||
* _part — fp.part.catalog
|
||||
* _coating — fp.coating.config (legacy; removed in Phase E)
|
||||
* _spec — fusion.plating.customer.spec (the audit-tracked spec the cert prints)
|
||||
* _spec — fusion.plating.customer.spec (audit-tracked spec)
|
||||
* _process — the resolved fusion.plating.process.node tree
|
||||
* _due — datetime/date for "Due Date" row
|
||||
* _qty — float for "Qty" row
|
||||
@@ -48,11 +47,9 @@
|
||||
or (_so and _so.order_line[:1])
|
||||
or False"/>
|
||||
<t t-set="_part" t-value="_part or (_line and _line.x_fc_part_catalog_id) or False"/>
|
||||
<t t-set="_coating" t-value="_coating or (_line and _line.x_fc_coating_config_id) or False"/>
|
||||
<t t-set="_spec" t-value="_spec or (_line and _line.x_fc_customer_spec_id) or False"/>
|
||||
<t t-set="_process" t-value="_process
|
||||
or (_part and _part.default_process_id)
|
||||
or (_coating and _coating.recipe_id)
|
||||
or False"/>
|
||||
<t t-set="_due" t-value="_due
|
||||
or (_mo and (_mo.date_deadline or _mo.date_finished))
|
||||
@@ -470,7 +467,6 @@
|
||||
<t t-set="_so" t-value="so"/>
|
||||
<t t-set="_line" t-value="line"/>
|
||||
<t t-set="_part" t-value="line.x_fc_part_catalog_id"/>
|
||||
<t t-set="_coating" t-value="line.x_fc_coating_config_id"/>
|
||||
<t t-set="_spec" t-value="line.x_fc_customer_spec_id"/>
|
||||
<t t-set="_due" t-value="line.x_fc_part_deadline or so.commitment_date or False"/>
|
||||
<t t-set="_qty" t-value="line.product_uom_qty"/>
|
||||
@@ -501,7 +497,6 @@
|
||||
<t t-set="_so" t-value="so"/>
|
||||
<t t-set="_line" t-value="line"/>
|
||||
<t t-set="_part" t-value="line.x_fc_part_catalog_id"/>
|
||||
<t t-set="_coating" t-value="line.x_fc_coating_config_id"/>
|
||||
<t t-set="_spec" t-value="line.x_fc_customer_spec_id"/>
|
||||
<t t-set="_due" t-value="line.x_fc_part_deadline or so.commitment_date or False"/>
|
||||
<t t-set="_qty" t-value="line.product_uom_qty"/>
|
||||
|
||||
@@ -1142,7 +1142,7 @@ class FpShopfloorController(http.Controller):
|
||||
job_read_fields = [
|
||||
'name', 'origin', 'priority', 'partner_id', 'product_id',
|
||||
'qty', 'qty_done', 'date_planned_start', 'date_deadline',
|
||||
'part_catalog_id', 'coating_config_id',
|
||||
'part_catalog_id',
|
||||
]
|
||||
if 'customer_spec_id' in unique_jobs._fields:
|
||||
job_read_fields.append('customer_spec_id')
|
||||
@@ -1555,10 +1555,6 @@ class FpShopfloorController(http.Controller):
|
||||
job.part_catalog_id
|
||||
if 'part_catalog_id' in job._fields else False
|
||||
)
|
||||
coating = (
|
||||
job.coating_config_id
|
||||
if 'coating_config_id' in job._fields else False
|
||||
)
|
||||
# Specification (added by fusion_plating_quality)
|
||||
spec = (
|
||||
job.customer_spec_id
|
||||
@@ -1572,12 +1568,9 @@ class FpShopfloorController(http.Controller):
|
||||
getattr(part, 'part_number', '') or part.name or ''
|
||||
)
|
||||
part_revision = getattr(part, 'revision', '') or ''
|
||||
# coating_label kept blank — Phase E removed coating; downstream
|
||||
# tablet templates read spec_label instead.
|
||||
coating_label = ''
|
||||
if coating:
|
||||
spec_ref = getattr(coating, 'spec_reference', '') or ''
|
||||
coating_label = (
|
||||
f'{coating.name} · {spec_ref}' if spec_ref else coating.name
|
||||
)
|
||||
|
||||
# Customer logo + product image
|
||||
customer_logo_url = ''
|
||||
|
||||
Reference in New Issue
Block a user