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',
|
'name': 'Fusion Plating — Certificates',
|
||||||
'version': '19.0.5.6.0',
|
'version': '19.0.6.0.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Certificate registry for CoC, thickness reports, and quality documents.',
|
'summary': 'Certificate registry for CoC, thickness reports, and quality documents.',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -286,14 +286,27 @@ class FpCertificate(models.Model):
|
|||||||
def create(self, vals_list):
|
def create(self, vals_list):
|
||||||
SaleOrder = self.env['sale.order']
|
SaleOrder = self.env['sale.order']
|
||||||
for vals in vals_list:
|
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')
|
already_set = vals.get('spec_min_mils') or vals.get('spec_max_mils')
|
||||||
if not already_set and vals.get('sale_order_id'):
|
if not already_set and vals.get('sale_order_id'):
|
||||||
so = SaleOrder.browse(vals['sale_order_id'])
|
so = SaleOrder.browse(vals['sale_order_id'])
|
||||||
cfg = getattr(so, 'x_fc_coating_config_id', False)
|
# Look across order_line for the first recipe with a
|
||||||
if cfg and cfg.thickness_uom == 'mils':
|
# populated thickness range.
|
||||||
vals.setdefault('spec_min_mils', cfg.thickness_min or 0.0)
|
first_line = so.order_line[:1] if so.order_line else False
|
||||||
vals.setdefault('spec_max_mils', cfg.thickness_max or 0.0)
|
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
|
# Defer naming: let the record exist so the mixin can write
|
||||||
# name via raw SQL, then fall back to the legacy sequence if
|
# name via raw SQL, then fall back to the legacy sequence if
|
||||||
# no parent SO is reachable.
|
# no parent SO is reachable.
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ def _backfill_currency(env):
|
|||||||
return
|
return
|
||||||
for model_name in (
|
for model_name in (
|
||||||
'fp.pricing.rule',
|
'fp.pricing.rule',
|
||||||
'fp.treatment',
|
|
||||||
'fp.customer.price.list',
|
|
||||||
'fp.quote.configurator',
|
'fp.quote.configurator',
|
||||||
):
|
):
|
||||||
Model = env.get(model_name)
|
Model = env.get(model_name)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Configurator',
|
'name': 'Fusion Plating — Configurator',
|
||||||
'version': '19.0.19.0.0',
|
'version': '19.0.20.0.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.',
|
'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.',
|
||||||
'description': """
|
'description': """
|
||||||
@@ -39,16 +39,11 @@ Provides:
|
|||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'data/fp_configurator_sequence_data.xml',
|
'data/fp_configurator_sequence_data.xml',
|
||||||
'data/fp_sub5_sequence_data.xml',
|
'data/fp_sub5_sequence_data.xml',
|
||||||
'data/fp_treatment_data.xml',
|
|
||||||
'data/fp_part_material_data.xml',
|
'data/fp_part_material_data.xml',
|
||||||
'views/fp_treatment_views.xml',
|
|
||||||
'views/fp_part_material_views.xml',
|
'views/fp_part_material_views.xml',
|
||||||
'views/fp_coating_thickness_views.xml',
|
|
||||||
'views/fp_part_catalog_views.xml',
|
'views/fp_part_catalog_views.xml',
|
||||||
'views/fp_process_node_part_scoped_views.xml',
|
'views/fp_process_node_part_scoped_views.xml',
|
||||||
'views/fp_coating_config_views.xml',
|
|
||||||
'views/fp_pricing_rule_views.xml',
|
'views/fp_pricing_rule_views.xml',
|
||||||
'views/fp_customer_price_list_views.xml',
|
|
||||||
'views/fp_quote_configurator_views.xml',
|
'views/fp_quote_configurator_views.xml',
|
||||||
'views/sale_order_views.xml',
|
'views/sale_order_views.xml',
|
||||||
'views/res_partner_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)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
# Part of the Fusion Plating product family.
|
# Part of the Fusion Plating product family.
|
||||||
|
|
||||||
from . import fp_treatment
|
|
||||||
from . import fp_part_material
|
from . import fp_part_material
|
||||||
from . import fp_part_catalog
|
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_complexity_surcharge
|
||||||
from . import fp_pricing_rule
|
from . import fp_pricing_rule
|
||||||
from . import fp_customer_price_list
|
|
||||||
from . import fp_sale_description_template
|
from . import fp_sale_description_template
|
||||||
from . import fp_quote_configurator
|
from . import fp_quote_configurator
|
||||||
from . import fp_serial
|
from . import fp_serial
|
||||||
|
|||||||
@@ -70,8 +70,7 @@ class AccountMoveLine(models.Model):
|
|||||||
string='Thickness',
|
string='Thickness',
|
||||||
help='Copied from sale.order.line for customer-facing invoice PDFs.',
|
help='Copied from sale.order.line for customer-facing invoice PDFs.',
|
||||||
)
|
)
|
||||||
# x_fc_customer_spec_id is added by fusion_plating_quality (where
|
# x_fc_customer_spec_id added by fusion_plating_quality.
|
||||||
# fusion.plating.customer.spec lives).
|
|
||||||
x_fc_revision_snapshot = fields.Char(
|
x_fc_revision_snapshot = fields.Char(
|
||||||
string='Revision (snapshot)',
|
string='Revision (snapshot)',
|
||||||
help='Revision letter from the source SO line.',
|
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)
|
rec.process_variant_count = len(variants)
|
||||||
|
|
||||||
# ---- Direct-order defaults (Phase C — C4) ----
|
# ---- Direct-order defaults (Phase C — C4) ----
|
||||||
x_fc_default_coating_config_id = fields.Many2one(
|
# x_fc_default_customer_spec_id added by fusion_plating_quality.
|
||||||
'fp.coating.config',
|
# Legacy default_coating_config_id + default_treatment_ids removed.
|
||||||
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.',
|
|
||||||
)
|
|
||||||
|
|
||||||
# Substrate density mapping (g/cm³) for material weight calculation
|
# Substrate density mapping (g/cm³) for material weight calculation
|
||||||
_SUBSTRATE_DENSITY = {
|
_SUBSTRATE_DENSITY = {
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ class FpPricingRule(models.Model):
|
|||||||
_order = 'sequence, id'
|
_order = 'sequence, id'
|
||||||
|
|
||||||
name = fields.Char(string='Rule Name', required=True)
|
name = fields.Char(string='Rule Name', required=True)
|
||||||
coating_config_id = fields.Many2one('fp.coating.config', string='Coating Config',
|
# coating_config_id removed. Spec + recipe match keys live on
|
||||||
help='Leave blank for a global rule.')
|
# fusion_plating_quality.fp_pricing_rule_inherit. Material +
|
||||||
|
# cert_level (below) remain as generic filters.
|
||||||
substrate_material = fields.Selection(
|
substrate_material = fields.Selection(
|
||||||
[('aluminium', 'Aluminium'), ('steel', 'Steel'), ('stainless', 'Stainless Steel'),
|
[('aluminium', 'Aluminium'), ('steel', 'Steel'), ('stainless', 'Stainless Steel'),
|
||||||
('copper', 'Copper'), ('titanium', 'Titanium'), ('other', 'Other')],
|
('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_file = fields.Binary(string='Upload PO', attachment=False)
|
||||||
upload_po_filename = fields.Char(string='PO Filename')
|
upload_po_filename = fields.Char(string='PO Filename')
|
||||||
|
|
||||||
coating_config_id = fields.Many2one(
|
# Renamed from coating_config_id (Phase E — Promote Customer Spec).
|
||||||
'fp.coating.config', string='Coating Configuration', required=True,
|
# 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)
|
quantity = fields.Integer(string='Quantity', default=1, required=True)
|
||||||
batch_size = fields.Integer(string='Batch Size', help='Parts per rack or barrel load.')
|
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)
|
# Copy masking area too (for effective-area calculation)
|
||||||
self.masking_area_sqin = cat.masking_area_sqin
|
self.masking_area_sqin = cat.masking_area_sqin
|
||||||
|
|
||||||
@api.onchange('coating_config_id')
|
@api.onchange('recipe_id')
|
||||||
def _onchange_coating_config_id(self):
|
def _onchange_recipe_id(self):
|
||||||
if self.coating_config_id:
|
if self.recipe_id and self.recipe_id.thickness_min:
|
||||||
self.thickness_requested = self.coating_config_id.thickness_min
|
self.thickness_requested = self.recipe_id.thickness_min
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# Price calculation
|
# Price calculation
|
||||||
@@ -358,11 +365,11 @@ class FpQuoteConfigurator(models.Model):
|
|||||||
'masking_zones', 'complexity', 'substrate_material',
|
'masking_zones', 'complexity', 'substrate_material',
|
||||||
'quantity', 'batch_size', 'rush_order',
|
'quantity', 'batch_size', 'rush_order',
|
||||||
'shipping_fee', 'delivery_fee',
|
'shipping_fee', 'delivery_fee',
|
||||||
'coating_config_id', 'coating_config_id.certification_level',
|
'recipe_id',
|
||||||
)
|
)
|
||||||
def _compute_price(self):
|
def _compute_price(self):
|
||||||
for rec in 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.calculated_price = 0
|
||||||
rec.price_breakdown_html = ''
|
rec.price_breakdown_html = ''
|
||||||
continue
|
continue
|
||||||
@@ -476,19 +483,17 @@ class FpQuoteConfigurator(models.Model):
|
|||||||
def _find_matching_rule(self):
|
def _find_matching_rule(self):
|
||||||
"""Find the best pricing rule matching this configurator's filters.
|
"""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.
|
If no rule matches filters, returns None.
|
||||||
|
|
||||||
When the chosen coating config points at a recipe and that recipe
|
When the chosen recipe has `pricing_rule_ids` configured, the
|
||||||
has `pricing_rule_ids` configured, the search is constrained to
|
search is constrained to those rules ("Use Price Builders"
|
||||||
those rules ("Use Price Builders" semantics). Otherwise the
|
semantics). Otherwise the whole active rule set is considered.
|
||||||
whole active rule set is considered as before.
|
|
||||||
|
Spec-tier scoring is added by an inherit in
|
||||||
|
fusion_plating_quality (where customer.spec lives).
|
||||||
"""
|
"""
|
||||||
recipe = (
|
recipe = self.recipe_id or False
|
||||||
self.coating_config_id.recipe_id
|
|
||||||
if self.coating_config_id and self.coating_config_id.recipe_id
|
|
||||||
else False
|
|
||||||
)
|
|
||||||
builder_rules = (
|
builder_rules = (
|
||||||
recipe.pricing_rule_ids if recipe else self.env['fp.pricing.rule']
|
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(
|
rules = self.env['fp.pricing.rule'].search(
|
||||||
[('active', '=', True)], order='sequence, id'
|
[('active', '=', True)], order='sequence, id'
|
||||||
)
|
)
|
||||||
cert_level = (
|
|
||||||
self.coating_config_id.certification_level
|
|
||||||
if self.coating_config_id else False
|
|
||||||
)
|
|
||||||
|
|
||||||
best = None
|
best = None
|
||||||
best_score = -1
|
best_score = -1
|
||||||
for rule in rules:
|
for rule in rules:
|
||||||
score = 0
|
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:
|
||||||
if rule.substrate_material != self.substrate_material:
|
if rule.substrate_material != self.substrate_material:
|
||||||
continue
|
continue
|
||||||
score += 2
|
score += 2
|
||||||
if rule.certification_level:
|
|
||||||
if rule.certification_level != cert_level:
|
|
||||||
continue
|
|
||||||
score += 1
|
|
||||||
if score > best_score:
|
if score > best_score:
|
||||||
best_score = score
|
best_score = score
|
||||||
best = rule
|
best = rule
|
||||||
@@ -569,9 +562,9 @@ class FpQuoteConfigurator(models.Model):
|
|||||||
raise UserError(_(
|
raise UserError(_(
|
||||||
'Pick a part catalog entry before promoting this quote.'
|
'Pick a part catalog entry before promoting this quote.'
|
||||||
))
|
))
|
||||||
if not self.coating_config_id:
|
if not self.recipe_id:
|
||||||
raise UserError(_(
|
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([
|
existing_line = self.env['fp.direct.order.line'].search([
|
||||||
('quote_id', '=', self.id),
|
('quote_id', '=', self.id),
|
||||||
@@ -618,14 +611,13 @@ class FpQuoteConfigurator(models.Model):
|
|||||||
'purchase_ok': False,
|
'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'
|
part_name = self.part_catalog_id.name if self.part_catalog_id else 'Custom Part'
|
||||||
|
|
||||||
so_vals = {
|
so_vals = {
|
||||||
'partner_id': self.partner_id.id,
|
'partner_id': self.partner_id.id,
|
||||||
'x_fc_configurator_id': self.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_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_rush_order': self.rush_order,
|
||||||
'x_fc_delivery_method': self.delivery_method,
|
'x_fc_delivery_method': self.delivery_method,
|
||||||
# Transfer RFQ / PO documents from configurator (if any)
|
# Transfer RFQ / PO documents from configurator (if any)
|
||||||
@@ -641,17 +633,19 @@ class FpQuoteConfigurator(models.Model):
|
|||||||
'origin': self.name,
|
'origin': self.name,
|
||||||
'order_line': [(0, 0, {
|
'order_line': [(0, 0, {
|
||||||
'product_id': product.id,
|
'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,
|
'product_uom_qty': self.quantity,
|
||||||
'price_unit': price / self.quantity if self.quantity else price,
|
'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
|
# fusion_plating_jobs._fp_auto_create_job filters lines
|
||||||
# by x_fc_part_catalog_id; without it, no fp.job spawns.
|
# 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': (
|
'x_fc_part_catalog_id': (
|
||||||
self.part_catalog_id.id if self.part_catalog_id else False
|
self.part_catalog_id.id if self.part_catalog_id else False
|
||||||
),
|
),
|
||||||
'x_fc_coating_config_id': (
|
'x_fc_process_variant_id': (
|
||||||
self.coating_config_id.id if self.coating_config_id else False
|
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 '
|
'part — it only appears in the picker when this part is on '
|
||||||
'the order. Leave blank for generic fallback templates.',
|
'the order. Leave blank for generic fallback templates.',
|
||||||
)
|
)
|
||||||
# Related fields — surface the part's partner/coating for search &
|
# Related fields — surface the part's partner for search & grouping
|
||||||
# grouping without writing them twice.
|
# without writing it twice.
|
||||||
partner_id = fields.Many2one(
|
partner_id = fields.Many2one(
|
||||||
'res.partner', string='Customer',
|
'res.partner', string='Customer',
|
||||||
related='part_catalog_id.partner_id', store=True, readonly=True,
|
related='part_catalog_id.partner_id', store=True, readonly=True,
|
||||||
)
|
)
|
||||||
# Keep the explicit coating slot for global templates that aren't
|
# coating_config_id removed; templates can be customer- or part-
|
||||||
# part-specific but are still coating-specific.
|
# scoped. Spec-scoped templates are a future enhancement.
|
||||||
coating_config_id = fields.Many2one(
|
|
||||||
'fp.coating.config', string='Associated Coating',
|
|
||||||
ondelete='set null',
|
|
||||||
help='For generic (no-part) templates, restrict to one coating.',
|
|
||||||
)
|
|
||||||
tag = fields.Selection(
|
tag = fields.Selection(
|
||||||
[('standard', 'Standard'),
|
[('standard', 'Standard'),
|
||||||
('masking', 'Masking / Selective'),
|
('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_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_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_number = fields.Char(string='Customer PO #', tracking=True)
|
||||||
x_fc_po_attachment_id = fields.Many2one(
|
x_fc_po_attachment_id = fields.Many2one(
|
||||||
'ir.attachment', string='PO Document', tracking=True,
|
'ir.attachment', string='PO Document', tracking=True,
|
||||||
@@ -209,7 +210,7 @@ class SaleOrder(models.Model):
|
|||||||
for so in self:
|
for so in self:
|
||||||
variants = []
|
variants = []
|
||||||
for line in so.order_line:
|
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
|
continue # non-plating line
|
||||||
variant = (line.x_fc_process_variant_id
|
variant = (line.x_fc_process_variant_id
|
||||||
or line.x_fc_part_catalog_id.default_process_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')
|
@api.depends('order_line.price_subtotal', 'amount_untaxed')
|
||||||
def _compute_margin(self):
|
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
|
Pre-promote-customer-spec, this rolled up cost from
|
||||||
widget='percentage' formats 100% as 100%, not 10000%.
|
fp.coating.config.unit_cost. Coating Config is retired; cost
|
||||||
|
data on the recipe is a future enhancement (backlog). Until
|
||||||
x_fc_margin_available is False when NO line has a costed coating
|
then, margin is "not available" and the UI hides the fields.
|
||||||
(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%.
|
|
||||||
"""
|
"""
|
||||||
for rec in self:
|
for rec in self:
|
||||||
has_cost_data = False
|
rec.x_fc_margin_available = False
|
||||||
cost = 0.0
|
rec.x_fc_margin_amount = (rec.amount_untaxed or 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_percent = (
|
rec.x_fc_margin_percent = (
|
||||||
(rec.x_fc_margin_amount / rec.amount_untaxed)
|
(rec.x_fc_margin_amount / rec.amount_untaxed)
|
||||||
if (rec.amount_untaxed and has_cost_data) else 0.0
|
if (rec.amount_untaxed and has_cost_data) else 0.0
|
||||||
|
|||||||
@@ -59,15 +59,9 @@ class SaleOrderLine(models.Model):
|
|||||||
string='Description Template',
|
string='Description Template',
|
||||||
help='Which template row populated this line. Informational.',
|
help='Which template row populated this line. Informational.',
|
||||||
)
|
)
|
||||||
x_fc_coating_config_id = fields.Many2one(
|
# Specification picker (x_fc_customer_spec_id) is added by
|
||||||
'fp.coating.config', string='Primary Treatment',
|
# fusion_plating_quality. Legacy x_fc_coating_config_id +
|
||||||
)
|
# x_fc_treatment_ids removed.
|
||||||
# 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',
|
|
||||||
)
|
|
||||||
x_fc_part_deadline = fields.Date(
|
x_fc_part_deadline = fields.Date(
|
||||||
string='Part Deadline Override',
|
string='Part Deadline Override',
|
||||||
help='Absolute-date manual override. When set, beats the days-offset '
|
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
|
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_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_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_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_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_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
|
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_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_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_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_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_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
|
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_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_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_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_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_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
|
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"
|
sequence="8"
|
||||||
groups="group_fp_estimator"/>
|
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"
|
<menuitem id="menu_fp_pricing_rules"
|
||||||
name="Pricing Rules"
|
name="Pricing Rules"
|
||||||
parent="menu_fp_configurator"
|
parent="menu_fp_configurator"
|
||||||
action="action_fp_pricing_rule"
|
action="action_fp_pricing_rule"
|
||||||
sequence="30"/>
|
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"
|
<menuitem id="menu_fp_part_materials"
|
||||||
name="Materials"
|
name="Materials"
|
||||||
parent="menu_fp_configurator"
|
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"/>
|
class="btn-link"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
<separator string="Default Treatments" class="mt-4"/>
|
<!-- Default Specification picker added by
|
||||||
<group>
|
fusion_plating_quality view inherit. -->
|
||||||
<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>
|
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
Seeds the treatment fields on new direct-order
|
Set a Default Specification on this part
|
||||||
lines for this part. Updated whenever "Save as
|
(under the section added by the Quality
|
||||||
Default" is ticked while placing an order.
|
module) so future direct-order lines
|
||||||
|
pre-fill it automatically.
|
||||||
</p>
|
</p>
|
||||||
</page>
|
</page>
|
||||||
<page string="Dimensions & Complexity" name="dimensions">
|
<page string="Dimensions & Complexity" name="dimensions">
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
<list string="Pricing Rules" decoration-muted="not active">
|
<list string="Pricing Rules" decoration-muted="not active">
|
||||||
<field name="sequence" widget="handle"/>
|
<field name="sequence" widget="handle"/>
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="coating_config_id"/>
|
|
||||||
<field name="substrate_material"/>
|
<field name="substrate_material"/>
|
||||||
<field name="certification_level"/>
|
<field name="certification_level"/>
|
||||||
<field name="pricing_method"/>
|
<field name="pricing_method"/>
|
||||||
@@ -42,7 +41,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<group string="Filters">
|
<group string="Filters">
|
||||||
<group>
|
<group>
|
||||||
<field name="coating_config_id"/>
|
|
||||||
<field name="substrate_material"/>
|
<field name="substrate_material"/>
|
||||||
<field name="certification_level"/>
|
<field name="certification_level"/>
|
||||||
</group>
|
</group>
|
||||||
@@ -104,7 +102,6 @@
|
|||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<search>
|
<search>
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="coating_config_id"/>
|
|
||||||
<separator/>
|
<separator/>
|
||||||
<filter string="Per Square Inch" name="per_sqin" domain="[('pricing_method','=','per_sqin')]"/>
|
<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')]"/>
|
<filter string="Per Square Foot" name="per_sqft" domain="[('pricing_method','=','per_sqft')]"/>
|
||||||
@@ -113,7 +110,6 @@
|
|||||||
<separator/>
|
<separator/>
|
||||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||||
<group>
|
<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'}"/>
|
<filter string="Pricing Method" name="group_pricing_method" context="{'group_by':'pricing_method'}"/>
|
||||||
</group>
|
</group>
|
||||||
</search>
|
</search>
|
||||||
|
|||||||
@@ -129,7 +129,7 @@
|
|||||||
<group string="Customer & Part">
|
<group string="Customer & Part">
|
||||||
<field name="partner_id"/>
|
<field name="partner_id"/>
|
||||||
<field name="part_catalog_id"/>
|
<field name="part_catalog_id"/>
|
||||||
<field name="coating_config_id"/>
|
<field name="recipe_id"/>
|
||||||
<!-- 3D File: upload before, filename + clear button after -->
|
<!-- 3D File: upload before, filename + clear button after -->
|
||||||
<field name="upload_3d_file" filename="upload_3d_filename"
|
<field name="upload_3d_file" filename="upload_3d_filename"
|
||||||
invisible="state != 'draft' or model_attachment_id"
|
invisible="state != 'draft' or model_attachment_id"
|
||||||
@@ -325,7 +325,7 @@
|
|||||||
<field name="create_date" string="Date"/>
|
<field name="create_date" string="Date"/>
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="partner_id"/>
|
<field name="partner_id"/>
|
||||||
<field name="coating_config_id"/>
|
<field name="recipe_id"/>
|
||||||
<field name="surface_area"/>
|
<field name="surface_area"/>
|
||||||
<field name="quantity"/>
|
<field name="quantity"/>
|
||||||
<field name="currency_id" column_invisible="1"/>
|
<field name="currency_id" column_invisible="1"/>
|
||||||
@@ -350,14 +350,14 @@
|
|||||||
<search>
|
<search>
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="partner_id"/>
|
<field name="partner_id"/>
|
||||||
<field name="coating_config_id"/>
|
<field name="recipe_id"/>
|
||||||
<separator/>
|
<separator/>
|
||||||
<filter string="Draft" name="draft" domain="[('state', '=', 'draft')]"/>
|
<filter string="Draft" name="draft" domain="[('state', '=', 'draft')]"/>
|
||||||
<filter string="Confirmed" name="confirmed" domain="[('state', '=', 'confirmed')]"/>
|
<filter string="Confirmed" name="confirmed" domain="[('state', '=', 'confirmed')]"/>
|
||||||
<filter string="Cancelled" name="cancelled" domain="[('state', '=', 'cancelled')]"/>
|
<filter string="Cancelled" name="cancelled" domain="[('state', '=', 'cancelled')]"/>
|
||||||
<group>
|
<group>
|
||||||
<filter string="Customer" name="group_customer" context="{'group_by': 'partner_id'}"/>
|
<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'}"/>
|
<filter string="Status" name="group_state" context="{'group_by': 'state'}"/>
|
||||||
</group>
|
</group>
|
||||||
</search>
|
</search>
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
decoration-danger="tag == 'rework'"
|
decoration-danger="tag == 'rework'"
|
||||||
decoration-success="tag in ('aerospace','nuclear')"/>
|
decoration-success="tag in ('aerospace','nuclear')"/>
|
||||||
<field name="partner_id" optional="show"/>
|
<field name="partner_id" optional="show"/>
|
||||||
<field name="coating_config_id" optional="hide"/>
|
|
||||||
<field name="usage_count" string="Used"/>
|
<field name="usage_count" string="Used"/>
|
||||||
<field name="active" widget="boolean_toggle"/>
|
<field name="active" widget="boolean_toggle"/>
|
||||||
</list>
|
</list>
|
||||||
@@ -46,9 +45,6 @@
|
|||||||
<field name="tag"/>
|
<field name="tag"/>
|
||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="coating_config_id"
|
|
||||||
help="Only used for generic (no-part) templates."
|
|
||||||
invisible="part_catalog_id"/>
|
|
||||||
<field name="sequence"/>
|
<field name="sequence"/>
|
||||||
<field name="usage_count" readonly="1"/>
|
<field name="usage_count" readonly="1"/>
|
||||||
<field name="active" widget="boolean_toggle"/>
|
<field name="active" widget="boolean_toggle"/>
|
||||||
@@ -75,7 +71,6 @@
|
|||||||
<field name="internal_description"/>
|
<field name="internal_description"/>
|
||||||
<field name="customer_facing_description"/>
|
<field name="customer_facing_description"/>
|
||||||
<field name="part_catalog_id"/>
|
<field name="part_catalog_id"/>
|
||||||
<field name="coating_config_id"/>
|
|
||||||
<field name="partner_id"/>
|
<field name="partner_id"/>
|
||||||
<field name="tag"/>
|
<field name="tag"/>
|
||||||
<filter name="active" string="Active" domain="[('active','=',True)]"/>
|
<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
|
so you can confirm an order has the right parts/coatings
|
||||||
without scrolling pricing columns. The pre-Sub-12 SO-
|
without scrolling pricing columns. The pre-Sub-12 SO-
|
||||||
header singletons (x_fc_part_catalog_id /
|
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
|
order was built via the quote configurator — they're
|
||||||
silent on direct orders, which is why they appeared
|
silent on direct orders, which is why they appeared
|
||||||
empty after confirm. They still exist on the model
|
empty after confirm. They still exist on the model
|
||||||
@@ -118,7 +118,6 @@
|
|||||||
readonly="1">
|
readonly="1">
|
||||||
<list create="false" delete="false" edit="false">
|
<list create="false" delete="false" edit="false">
|
||||||
<field name="x_fc_part_catalog_id"/>
|
<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_thickness_id" optional="show"/>
|
||||||
<field name="x_fc_process_variant_id" optional="show"
|
<field name="x_fc_process_variant_id" optional="show"
|
||||||
string="Process"/>
|
string="Process"/>
|
||||||
@@ -251,7 +250,6 @@
|
|||||||
<field name="x_fc_internal_description"
|
<field name="x_fc_internal_description"
|
||||||
placeholder="Shop-floor workflow instructions (prints on WO / traveler)"
|
placeholder="Shop-floor workflow instructions (prints on WO / traveler)"
|
||||||
optional="hide"/>
|
optional="hide"/>
|
||||||
<field name="x_fc_coating_config_id" optional="show"/>
|
|
||||||
<field name="x_fc_process_variant_id"
|
<field name="x_fc_process_variant_id"
|
||||||
string="Process / Recipe"
|
string="Process / Recipe"
|
||||||
options="{'no_quick_create': True}"
|
options="{'no_quick_create': True}"
|
||||||
@@ -290,7 +288,6 @@
|
|||||||
<field name="x_fc_revision_snapshot"
|
<field name="x_fc_revision_snapshot"
|
||||||
readonly="1"
|
readonly="1"
|
||||||
optional="hide"/>
|
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" string="Part Deadline Override" optional="hide"/>
|
||||||
<field name="x_fc_part_deadline_offset_days" string="Days Offset" 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"
|
<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_wo_completion" optional="show"/>
|
||||||
<field name="x_fc_planned_start_date" optional="hide"/>
|
<field name="x_fc_planned_start_date" optional="hide"/>
|
||||||
<field name="x_fc_part_catalog_id" 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="amount_total" sum="Total"/>
|
||||||
<field name="x_fc_invoiced_amount" sum="Invoiced" optional="hide"
|
<field name="x_fc_invoiced_amount" sum="Invoiced" optional="hide"
|
||||||
widget="monetary"
|
widget="monetary"
|
||||||
@@ -363,7 +359,6 @@
|
|||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<kanban default_group_by="x_fc_part_catalog_id" records_draggable="0">
|
<kanban default_group_by="x_fc_part_catalog_id" records_draggable="0">
|
||||||
<field name="x_fc_part_catalog_id"/>
|
<field name="x_fc_part_catalog_id"/>
|
||||||
<field name="x_fc_coating_config_id"/>
|
|
||||||
<field name="product_uom_qty"/>
|
<field name="product_uom_qty"/>
|
||||||
<field name="qty_delivered"/>
|
<field name="qty_delivered"/>
|
||||||
<field name="x_fc_wo_group_tag"/>
|
<field name="x_fc_wo_group_tag"/>
|
||||||
@@ -373,7 +368,7 @@
|
|||||||
<t t-name="card">
|
<t t-name="card">
|
||||||
<div class="o_kanban_card_content">
|
<div class="o_kanban_card_content">
|
||||||
<div class="o_kanban_record_title">
|
<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>
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
Qty: <field name="product_uom_qty"/>
|
Qty: <field name="product_uom_qty"/>
|
||||||
@@ -399,7 +394,6 @@
|
|||||||
<kanban default_group_by="x_fc_wo_group_tag" records_draggable="0">
|
<kanban default_group_by="x_fc_wo_group_tag" records_draggable="0">
|
||||||
<field name="x_fc_wo_group_tag"/>
|
<field name="x_fc_wo_group_tag"/>
|
||||||
<field name="x_fc_part_catalog_id"/>
|
<field name="x_fc_part_catalog_id"/>
|
||||||
<field name="x_fc_coating_config_id"/>
|
|
||||||
<field name="product_uom_qty"/>
|
<field name="product_uom_qty"/>
|
||||||
<templates>
|
<templates>
|
||||||
<t t-name="card">
|
<t t-name="card">
|
||||||
@@ -407,9 +401,6 @@
|
|||||||
<div>
|
<div>
|
||||||
<strong><field name="x_fc_part_catalog_id"/></strong>
|
<strong><field name="x_fc_part_catalog_id"/></strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-muted">
|
|
||||||
<field name="x_fc_coating_config_id"/>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
Qty: <field name="product_uom_qty"/>
|
Qty: <field name="product_uom_qty"/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -43,14 +43,14 @@ class FpAddFromQuoteWizard(models.TransientModel):
|
|||||||
wizard = self.direct_order_wizard_id
|
wizard = self.direct_order_wizard_id
|
||||||
copied = 0
|
copied = 0
|
||||||
for q in self.quote_ids:
|
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
|
continue
|
||||||
Line._create_from_quote(q, wizard)
|
Line._create_from_quote(q, wizard)
|
||||||
copied += 1
|
copied += 1
|
||||||
|
|
||||||
if not copied:
|
if not copied:
|
||||||
raise UserError(_(
|
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.'
|
'so nothing could be copied.'
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<list>
|
<list>
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="part_catalog_id"/>
|
<field name="part_catalog_id"/>
|
||||||
<field name="coating_config_id"/>
|
<field name="recipe_id"/>
|
||||||
<field name="quantity"/>
|
<field name="quantity"/>
|
||||||
<field name="calculated_price" widget="monetary"/>
|
<field name="calculated_price" widget="monetary"/>
|
||||||
<field name="estimator_override_price" widget="monetary"/>
|
<field name="estimator_override_price" widget="monetary"/>
|
||||||
|
|||||||
@@ -53,14 +53,12 @@ class FpAddFromSoWizard(models.TransientModel):
|
|||||||
wizard = self.direct_order_wizard_id
|
wizard = self.direct_order_wizard_id
|
||||||
copied = 0
|
copied = 0
|
||||||
for src in self.source_line_ids:
|
for src in self.source_line_ids:
|
||||||
if not src.x_fc_part_catalog_id or not src.x_fc_coating_config_id:
|
if not src.x_fc_part_catalog_id:
|
||||||
# Skip SO lines that predate the plating fields
|
# Skip non-plating SO lines
|
||||||
continue
|
continue
|
||||||
Line.create({
|
Line.create({
|
||||||
'wizard_id': wizard.id,
|
'wizard_id': wizard.id,
|
||||||
'part_catalog_id': src.x_fc_part_catalog_id.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,
|
'quantity': int(src.product_uom_qty) or 1,
|
||||||
'unit_price': src.price_unit or 0.0,
|
'unit_price': src.price_unit or 0.0,
|
||||||
'part_deadline': src.x_fc_part_deadline,
|
'part_deadline': src.x_fc_part_deadline,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<list>
|
<list>
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="x_fc_part_catalog_id"/>
|
<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="product_uom_qty"/>
|
||||||
<field name="price_unit"/>
|
<field name="price_unit"/>
|
||||||
<field name="x_fc_part_deadline"/>
|
<field name="x_fc_part_deadline"/>
|
||||||
|
|||||||
@@ -51,22 +51,9 @@ class FpDirectOrderLine(models.Model):
|
|||||||
new_drawing_filename = fields.Char(string='Filename')
|
new_drawing_filename = fields.Char(string='Filename')
|
||||||
revision_note = fields.Char(string='Revision Note')
|
revision_note = fields.Char(string='Revision Note')
|
||||||
|
|
||||||
# ---- Treatments ----
|
# Specification picker (customer_spec_id) added by
|
||||||
coating_config_id = fields.Many2one(
|
# fusion_plating_quality. Legacy coating_config_id +
|
||||||
'fp.coating.config',
|
# treatment_ids removed.
|
||||||
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.',
|
|
||||||
)
|
|
||||||
# Sub 9 (polished 2026-04-28) — process variant per line. The picker
|
# Sub 9 (polished 2026-04-28) — process variant per line. The picker
|
||||||
# now lets the estimator pick ANY root recipe in the system: the
|
# now lets the estimator pick ANY root recipe in the system: the
|
||||||
# part's own variants, another customer's variants, or a template
|
# part's own variants, another customer's variants, or a template
|
||||||
@@ -107,8 +94,7 @@ class FpDirectOrderLine(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@api.depends('process_variant_id',
|
@api.depends('process_variant_id',
|
||||||
'part_catalog_id.default_process_id',
|
'part_catalog_id.default_process_id')
|
||||||
'coating_config_id.recipe_id')
|
|
||||||
def _compute_effective_process(self):
|
def _compute_effective_process(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
if rec.process_variant_id:
|
if rec.process_variant_id:
|
||||||
@@ -122,12 +108,6 @@ class FpDirectOrderLine(models.Model):
|
|||||||
rec.effective_process_id = part_proc
|
rec.effective_process_id = part_proc
|
||||||
rec.effective_process_source = 'Part default'
|
rec.effective_process_source = 'Part default'
|
||||||
continue
|
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_id = False
|
||||||
rec.effective_process_source = False
|
rec.effective_process_source = False
|
||||||
|
|
||||||
@@ -168,35 +148,26 @@ class FpDirectOrderLine(models.Model):
|
|||||||
if not rec.part_catalog_id:
|
if not rec.part_catalog_id:
|
||||||
continue
|
continue
|
||||||
part = rec.part_catalog_id
|
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
|
# Default-spec auto-fill is implemented by an inherit in
|
||||||
# fusion_plating_quality (where customer_spec_id field lives).
|
# 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
|
# likely a first-time use of the part. Auto-tick the
|
||||||
# push_to_defaults toggle so whatever Sarah picks becomes
|
# push_to_defaults toggle so whatever Sarah picks becomes
|
||||||
# the saved default — surface a warning popup so she knows.
|
# the saved default — surface a warning popup so she knows.
|
||||||
# `is_one_off` always wins (operator opted out of catalog
|
# `is_one_off` always wins (operator opted out of catalog
|
||||||
# persistence), so don't auto-tick in that case.
|
# persistence), so don't auto-tick in that case.
|
||||||
if (not has_default_coating
|
if (not has_default_spec
|
||||||
and not has_default_treatments
|
|
||||||
and not rec.is_one_off
|
and not rec.is_one_off
|
||||||
and not rec.push_to_defaults):
|
and not rec.push_to_defaults):
|
||||||
rec.push_to_defaults = True
|
rec.push_to_defaults = True
|
||||||
warning = {
|
warning = {
|
||||||
'title': _('First-Time Part — Defaults Will Be Saved'),
|
'title': _('First-Time Part — Defaults Will Be Saved'),
|
||||||
'message': _(
|
'message': _(
|
||||||
'%(part)s has no saved coating / treatments. '
|
'%(part)s has no saved specification. '
|
||||||
'The coating + treatments you pick on this line '
|
'The specification you pick on this line will '
|
||||||
'will be saved as the part\'s defaults so the '
|
'be saved as the part\'s default so the '
|
||||||
'next order auto-fills them. Untick "Save as '
|
'next order auto-fills them. Untick "Save as '
|
||||||
'Default" on the line if you don\'t want this.'
|
'Default" on the line if you don\'t want this.'
|
||||||
) % {'part': part.display_name or part.part_number or '(part)'},
|
) % {'part': part.display_name or part.part_number or '(part)'},
|
||||||
@@ -269,11 +240,11 @@ class FpDirectOrderLine(models.Model):
|
|||||||
start_at_node_id = fields.Many2one(
|
start_at_node_id = fields.Many2one(
|
||||||
'fusion.plating.process.node',
|
'fusion.plating.process.node',
|
||||||
string='Start at 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 '
|
help='For re-work jobs: pick the recipe step where this job should '
|
||||||
'begin. Pick a coating first — nodes are scoped to its '
|
'begin. Pick a recipe first — nodes are scoped to it. Skips '
|
||||||
'recipe tree. Skips earlier steps in the generated WO but '
|
'earlier steps in the generated WO but keeps later siblings '
|
||||||
'keeps later siblings and sub-processes.',
|
'and sub-processes.',
|
||||||
)
|
)
|
||||||
is_one_off = fields.Boolean(
|
is_one_off = fields.Boolean(
|
||||||
string='One-off Part',
|
string='One-off Part',
|
||||||
@@ -436,12 +407,11 @@ class FpDirectOrderLine(models.Model):
|
|||||||
for rec in self:
|
for rec in self:
|
||||||
rec.line_subtotal = (rec.quantity or 0) * (rec.unit_price or 0.0)
|
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):
|
def _compute_is_missing_info(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
rec.is_missing_info = not (
|
rec.is_missing_info = not (
|
||||||
rec.part_catalog_id
|
rec.part_catalog_id
|
||||||
and rec.coating_config_id
|
|
||||||
and rec.unit_price
|
and rec.unit_price
|
||||||
and rec.quantity
|
and rec.quantity
|
||||||
)
|
)
|
||||||
@@ -499,14 +469,16 @@ class FpDirectOrderLine(models.Model):
|
|||||||
# ---- Onchange ----
|
# ---- Onchange ----
|
||||||
@api.onchange('quote_id')
|
@api.onchange('quote_id')
|
||||||
def _onchange_quote_id(self):
|
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:
|
if not self.quote_id:
|
||||||
return
|
return
|
||||||
q = self.quote_id
|
q = self.quote_id
|
||||||
if q.part_catalog_id and not self.part_catalog_id:
|
if q.part_catalog_id and not self.part_catalog_id:
|
||||||
self.part_catalog_id = q.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:
|
if not self.unit_price:
|
||||||
final = q.estimator_override_price or q.calculated_price
|
final = q.estimator_override_price or q.calculated_price
|
||||||
if final and q.quantity:
|
if final and q.quantity:
|
||||||
@@ -514,13 +486,13 @@ class FpDirectOrderLine(models.Model):
|
|||||||
|
|
||||||
@api.onchange('part_catalog_id')
|
@api.onchange('part_catalog_id')
|
||||||
def _onchange_part_defaults(self):
|
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:
|
if not self.part_catalog_id:
|
||||||
return
|
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
|
# Seed default taxes from the FP-SERVICE product, fiscal-position
|
||||||
# mapped from the customer. Only fills when the user hasn't set
|
# mapped from the customer. Only fills when the user hasn't set
|
||||||
# taxes manually.
|
# taxes manually.
|
||||||
@@ -543,21 +515,10 @@ class FpDirectOrderLine(models.Model):
|
|||||||
if taxes:
|
if taxes:
|
||||||
self.tax_ids = [(6, 0, taxes.ids)]
|
self.tax_ids = [(6, 0, taxes.ids)]
|
||||||
|
|
||||||
@api.onchange('coating_config_id', 'quantity', 'part_catalog_id')
|
# Auto-fill unit_price from a customer price list — extended in
|
||||||
def _onchange_lookup_price(self):
|
# fusion_plating_quality (the spec field lives there). The base
|
||||||
"""Auto-fill unit_price from customer price list when available."""
|
# configurator wizard no longer triggers price lookup since
|
||||||
if self.unit_price:
|
# coating_config_id is gone.
|
||||||
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
|
|
||||||
|
|
||||||
@api.onchange('description_template_id')
|
@api.onchange('description_template_id')
|
||||||
def _onchange_description_template(self):
|
def _onchange_description_template(self):
|
||||||
@@ -575,15 +536,14 @@ class FpDirectOrderLine(models.Model):
|
|||||||
if tpl.internal_description:
|
if tpl.internal_description:
|
||||||
self.internal_description = 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):
|
def _onchange_suggest_template(self):
|
||||||
"""Offer a sensible default template — part-specific wins.
|
"""Offer a sensible default template — part-specific wins.
|
||||||
|
|
||||||
Priority (first non-empty result wins):
|
Priority (first non-empty result wins):
|
||||||
1. This part's lowest-sequence active template
|
1. This part's lowest-sequence active template
|
||||||
2. This customer's templates (no part)
|
2. This customer's templates (no part)
|
||||||
3. This coating's templates (no part)
|
3. Don't auto-pick — user has to choose
|
||||||
4. Don't auto-pick — user has to choose
|
|
||||||
"""
|
"""
|
||||||
if self.description_template_id or self.line_description:
|
if self.description_template_id or self.line_description:
|
||||||
return
|
return
|
||||||
@@ -616,16 +576,6 @@ class FpDirectOrderLine(models.Model):
|
|||||||
_apply(match)
|
_apply(match)
|
||||||
return
|
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 ----
|
# ---- Helpers ----
|
||||||
@api.model
|
@api.model
|
||||||
def _create_from_quote(self, quote, wizard):
|
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
|
the bulk "Add From Quotes" sub-wizard — keeps the field mapping
|
||||||
in one place so the two flows can never drift.
|
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(_(
|
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))
|
) % (quote.name or quote.id))
|
||||||
final = quote.estimator_override_price or quote.calculated_price
|
final = quote.estimator_override_price or quote.calculated_price
|
||||||
unit = (final / quote.quantity) if (final and quote.quantity) else 0.0
|
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({
|
return self.create({
|
||||||
'wizard_id': wizard.id,
|
'wizard_id': wizard.id,
|
||||||
'part_catalog_id': quote.part_catalog_id.id,
|
'part_catalog_id': quote.part_catalog_id.id,
|
||||||
'coating_config_id': quote.coating_config_id.id,
|
|
||||||
'quantity': int(quote.quantity) or 1,
|
'quantity': int(quote.quantity) or 1,
|
||||||
'unit_price': unit,
|
'unit_price': unit,
|
||||||
'quote_id': quote.id,
|
'quote_id': quote.id,
|
||||||
|
|||||||
@@ -550,12 +550,13 @@ class FpDirectOrderWizard(models.Model):
|
|||||||
for line in self.line_ids:
|
for line in self.line_ids:
|
||||||
part = line._get_or_bump_revision()
|
part = line._get_or_bump_revision()
|
||||||
resolved_parts[line.id] = part
|
resolved_parts[line.id] = part
|
||||||
# Build the line header. Primary treatment is optional now;
|
# Build the line header. Specification is optional; when
|
||||||
# when missing, drop it from the header rather than printing
|
# missing, drop it from the header rather than printing
|
||||||
# "False - PartName Rev A".
|
# "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)' % (
|
header = '%s - %s Rev %s (x%d)' % (
|
||||||
treatment_label,
|
spec_label,
|
||||||
part.name,
|
part.name,
|
||||||
part.revision,
|
part.revision,
|
||||||
line.quantity,
|
line.quantity,
|
||||||
@@ -573,10 +574,9 @@ class FpDirectOrderWizard(models.Model):
|
|||||||
'x_fc_part_catalog_id': part.id,
|
'x_fc_part_catalog_id': part.id,
|
||||||
'x_fc_description_template_id': line.description_template_id.id or False,
|
'x_fc_description_template_id': line.description_template_id.id or False,
|
||||||
'x_fc_internal_description': line.internal_description or False,
|
'x_fc_internal_description': line.internal_description or False,
|
||||||
'x_fc_coating_config_id': line.coating_config_id.id,
|
# x_fc_customer_spec_id is set on the resulting SO line
|
||||||
'x_fc_treatment_ids': [(6, 0, line.treatment_ids.ids)],
|
# by an extension in fusion_plating_quality (post-create
|
||||||
# x_fc_customer_spec_id is added to vals by an extension
|
# patch — see fp_direct_order_line_inherit.py).
|
||||||
# of this method in fusion_plating_quality.
|
|
||||||
'x_fc_part_deadline': line.part_deadline,
|
'x_fc_part_deadline': line.part_deadline,
|
||||||
'x_fc_part_deadline_offset_days': line.part_deadline_offset_days,
|
'x_fc_part_deadline_offset_days': line.part_deadline_offset_days,
|
||||||
'x_fc_rush_order': line.rush_order,
|
'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.'
|
'Quote won — promoted onto Direct Order %(doo)s, SO %(so)s.'
|
||||||
) % {'doo': self.name, 'so': so.name})
|
) % {'doo': self.name, 'so': so.name})
|
||||||
|
|
||||||
# 6. Push-to-defaults (C4) — uses the resolved part cached
|
# 6. Push-to-defaults — Specification carry-over to the part's
|
||||||
# during the build loop so rev-bumped lines write defaults to
|
# x_fc_default_customer_spec_id is handled by an inherit in
|
||||||
# the NEW revision, not the pre-bump one.
|
# fusion_plating_quality (the field lives there).
|
||||||
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)],
|
|
||||||
})
|
|
||||||
so.message_post(body=_(
|
so.message_post(body=_(
|
||||||
'Quotation created from PO %s with %d line(s). '
|
'Quotation created from PO %s with %d line(s). '
|
||||||
'Review and confirm manually when ready.'
|
'Review and confirm manually when ready.'
|
||||||
|
|||||||
@@ -154,8 +154,6 @@
|
|||||||
optional="hide"/>
|
optional="hide"/>
|
||||||
<field name="internal_description"
|
<field name="internal_description"
|
||||||
optional="hide"/>
|
optional="hide"/>
|
||||||
<field name="coating_config_id"
|
|
||||||
optional="show"/>
|
|
||||||
<field name="process_variant_id"
|
<field name="process_variant_id"
|
||||||
string="Process / Recipe"
|
string="Process / Recipe"
|
||||||
options="{'no_quick_create': True}"
|
options="{'no_quick_create': True}"
|
||||||
@@ -194,9 +192,6 @@
|
|||||||
class="btn-link"
|
class="btn-link"
|
||||||
invisible="not part_catalog_id or serial_count > 0"/>
|
invisible="not part_catalog_id or serial_count > 0"/>
|
||||||
<field name="job_number" optional="hide"/>
|
<field name="job_number" optional="hide"/>
|
||||||
<field name="treatment_ids"
|
|
||||||
widget="many2many_tags"
|
|
||||||
invisible="1"/>
|
|
||||||
<field name="quantity"
|
<field name="quantity"
|
||||||
optional="show"/>
|
optional="show"/>
|
||||||
<field name="unit_price"
|
<field name="unit_price"
|
||||||
@@ -239,9 +234,6 @@
|
|||||||
invisible="not part_catalog_id"/>
|
invisible="not part_catalog_id"/>
|
||||||
<field name="part_revision"
|
<field name="part_revision"
|
||||||
invisible="not part_catalog_id"/>
|
invisible="not part_catalog_id"/>
|
||||||
<field name="coating_config_id"/>
|
|
||||||
<field name="treatment_ids"
|
|
||||||
widget="many2many_tags"/>
|
|
||||||
<field name="process_variant_id"
|
<field name="process_variant_id"
|
||||||
string="Process / Recipe"
|
string="Process / Recipe"
|
||||||
options="{'no_quick_create': True}"
|
options="{'no_quick_create': True}"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Native Jobs',
|
'name': 'Fusion Plating — Native Jobs',
|
||||||
'version': '19.0.9.1.0',
|
'version': '19.0.10.0.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
||||||
'author': 'Nexa Systems Inc.',
|
'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', # fp.job, fp.job.step, fp.work.centre
|
||||||
'fusion_plating_batch', # fusion.plating.batch (Phase 3)
|
'fusion_plating_batch', # fusion.plating.batch (Phase 3)
|
||||||
'fusion_plating_certificates', # fp.certificate, fp.thickness.reading
|
'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_kpi', # fusion.plating.kpi.value (Phase 4)
|
||||||
'fusion_plating_logistics', # fusion.plating.delivery
|
'fusion_plating_logistics', # fusion.plating.delivery
|
||||||
'fusion_plating_notifications', # fp.notification.template (Phase 4)
|
'fusion_plating_notifications', # fp.notification.template (Phase 4)
|
||||||
|
|||||||
@@ -48,15 +48,12 @@ class FpJob(models.Model):
|
|||||||
string='Part',
|
string='Part',
|
||||||
ondelete='restrict',
|
ondelete='restrict',
|
||||||
)
|
)
|
||||||
coating_config_id = fields.Many2one(
|
|
||||||
'fp.coating.config',
|
|
||||||
string='Coating Configuration',
|
|
||||||
ondelete='restrict',
|
|
||||||
)
|
|
||||||
customer_spec_id = fields.Many2one(
|
customer_spec_id = fields.Many2one(
|
||||||
'fusion.plating.customer.spec',
|
'fusion.plating.customer.spec',
|
||||||
string='Customer Spec',
|
string='Specification',
|
||||||
ondelete='set null',
|
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(
|
portal_job_id = fields.Many2one(
|
||||||
'fusion.plating.portal.job',
|
'fusion.plating.portal.job',
|
||||||
@@ -996,29 +993,28 @@ class FpJob(models.Model):
|
|||||||
if node.estimated_duration:
|
if node.estimated_duration:
|
||||||
vals['dwell_time_minutes'] = node.estimated_duration
|
vals['dwell_time_minutes'] = node.estimated_duration
|
||||||
|
|
||||||
# Pull thickness target from the coating config when
|
# Pull thickness target from the recipe root when this
|
||||||
# this is a plating step (matched by node name keyword).
|
# is a plating step (matched by node name keyword).
|
||||||
coating = job.coating_config_id
|
# Recipe-root carries thickness fields post-promote-spec.
|
||||||
|
recipe_root = job.recipe_id
|
||||||
name_l = (node.name or '').lower()
|
name_l = (node.name or '').lower()
|
||||||
is_plating_node = (
|
is_plating_node = (
|
||||||
'plat' in name_l or 'nickel' in name_l
|
'plat' in name_l or 'nickel' in name_l
|
||||||
or 'chrome' in name_l or 'anodiz' 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 (
|
if (
|
||||||
'thickness_max' in coating._fields
|
'thickness_max' in recipe_root._fields
|
||||||
and coating.thickness_max
|
and recipe_root.thickness_max
|
||||||
):
|
):
|
||||||
vals['thickness_target'] = coating.thickness_max
|
vals['thickness_target'] = recipe_root.thickness_max
|
||||||
if (
|
if (
|
||||||
'thickness_uom' in coating._fields
|
'thickness_uom' in recipe_root._fields
|
||||||
and coating.thickness_uom
|
and recipe_root.thickness_uom
|
||||||
):
|
):
|
||||||
# fp.coating.config uses long-form uom names
|
# Recipe uses long-form uom names (mils /
|
||||||
# (mils / microns / inches); fp.job.step uses
|
# microns / inches); fp.job.step uses short
|
||||||
# short codes (mil / um / inch). Map between
|
# codes (mil / um / inch). Map between them.
|
||||||
# them. Unknown values fall through to the
|
|
||||||
# step's default ('um').
|
|
||||||
_UOM_MAP = {
|
_UOM_MAP = {
|
||||||
'mils': 'mil',
|
'mils': 'mil',
|
||||||
'mil': 'mil',
|
'mil': 'mil',
|
||||||
@@ -1029,7 +1025,7 @@ class FpJob(models.Model):
|
|||||||
'inch': 'inch',
|
'inch': 'inch',
|
||||||
'in': 'inch',
|
'in': 'inch',
|
||||||
}
|
}
|
||||||
mapped = _UOM_MAP.get(coating.thickness_uom)
|
mapped = _UOM_MAP.get(recipe_root.thickness_uom)
|
||||||
if mapped:
|
if mapped:
|
||||||
vals['thickness_uom'] = mapped
|
vals['thickness_uom'] = mapped
|
||||||
|
|
||||||
@@ -1546,7 +1542,9 @@ class FpJob(models.Model):
|
|||||||
if not required:
|
if not required:
|
||||||
return
|
return
|
||||||
has_job_link = 'x_fc_job_id' in Cert._fields
|
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):
|
for cert_type in sorted(required):
|
||||||
# Idempotency per type.
|
# Idempotency per type.
|
||||||
existing_dom = [('certificate_type', '=', cert_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:
|
if 'sale_order_id' in Cert._fields and self.sale_order_id:
|
||||||
vals['sale_order_id'] = self.sale_order_id.id
|
vals['sale_order_id'] = self.sale_order_id.id
|
||||||
# spec_reference is what action_issue blocks on.
|
# spec_reference is what action_issue blocks on.
|
||||||
if coating and 'spec_reference' in Cert._fields \
|
# Format spec.code + revision for the cert text.
|
||||||
and getattr(coating, 'spec_reference', False):
|
if spec and 'spec_reference' in Cert._fields:
|
||||||
vals['spec_reference'] = coating.spec_reference
|
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:
|
if 'part_number' in Cert._fields and self.part_catalog_id:
|
||||||
vals['part_number'] = (
|
vals['part_number'] = (
|
||||||
self.part_catalog_id.part_number or ''
|
self.part_catalog_id.part_number or ''
|
||||||
|
|||||||
@@ -474,8 +474,9 @@ class FpJobStep(models.Model):
|
|||||||
def button_finish(self):
|
def button_finish(self):
|
||||||
"""Override to:
|
"""Override to:
|
||||||
1) Auto-spawn a bake.window when a wet plating step finishes
|
1) Auto-spawn a bake.window when a wet plating step finishes
|
||||||
on a coating that requires hydrogen-embrittlement relief
|
on a recipe that requires hydrogen-embrittlement relief
|
||||||
(AS9100 / Nadcap compliance);
|
(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×
|
2) Post a chatter warning when duration_actual exceeds 1.5×
|
||||||
duration_expected — silent overruns are a red flag for
|
duration_expected — silent overruns are a red flag for
|
||||||
scheduling and costing.
|
scheduling and costing.
|
||||||
@@ -499,12 +500,11 @@ class FpJobStep(models.Model):
|
|||||||
'estimate too tight.'
|
'estimate too tight.'
|
||||||
)) % (step.name, ratio, step.duration_expected,
|
)) % (step.name, ratio, step.duration_expected,
|
||||||
step.duration_actual))
|
step.duration_actual))
|
||||||
coating = step.job_id.coating_config_id \
|
recipe_root = step.job_id.recipe_id
|
||||||
if 'coating_config_id' in step.job_id._fields else False
|
if not recipe_root:
|
||||||
if not coating:
|
|
||||||
continue
|
continue
|
||||||
requires = getattr(coating, 'requires_bake_relief', False)
|
requires = getattr(recipe_root, 'requires_bake_relief', False)
|
||||||
window_hrs = getattr(coating, 'bake_window_hours', 0.0)
|
window_hrs = getattr(recipe_root, 'bake_window_hours', 0.0)
|
||||||
if not requires or not window_hrs:
|
if not requires or not window_hrs:
|
||||||
continue
|
continue
|
||||||
# Trigger only on the actual plating-out step. We want
|
# 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
|
1. line.x_fc_process_variant_id — Sarah explicitly picked a
|
||||||
part-scoped variant on this order line. Always wins.
|
part-scoped variant on this order line. Always wins.
|
||||||
2. part.default_process_id — part's flagged default
|
2. part.default_process_id — part's flagged default
|
||||||
variant. Customer-and-part-tuned recipe; must beat any
|
variant. Customer-and-part-tuned recipe.
|
||||||
generic coating template.
|
3. part.recipe_id — legacy fallback.
|
||||||
3. coating.recipe_id — coating-config recipe
|
|
||||||
(generic template fallback).
|
|
||||||
4. part.recipe_id — legacy fallback.
|
|
||||||
Returns the recipe record or an empty recordset.
|
Returns the recipe record or an empty recordset.
|
||||||
"""
|
"""
|
||||||
Node = self.env['fusion.plating.process.node']
|
Node = self.env['fusion.plating.process.node']
|
||||||
@@ -352,11 +349,6 @@ class SaleOrder(models.Model):
|
|||||||
) or False
|
) or False
|
||||||
if not part and 'x_fc_part_catalog_id' in self._fields:
|
if not part and 'x_fc_part_catalog_id' in self._fields:
|
||||||
part = self.x_fc_part_catalog_id or False
|
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 = (
|
picked = (
|
||||||
'x_fc_process_variant_id' in line._fields
|
'x_fc_process_variant_id' in line._fields
|
||||||
and line.x_fc_process_variant_id
|
and line.x_fc_process_variant_id
|
||||||
@@ -365,8 +357,6 @@ class SaleOrder(models.Model):
|
|||||||
return picked
|
return picked
|
||||||
if part and 'default_process_id' in part._fields and part.default_process_id:
|
if part and 'default_process_id' in part._fields and part.default_process_id:
|
||||||
return 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:
|
if part and 'recipe_id' in part._fields and part.recipe_id:
|
||||||
return part.recipe_id
|
return part.recipe_id
|
||||||
return Node
|
return Node
|
||||||
@@ -389,22 +379,22 @@ class SaleOrder(models.Model):
|
|||||||
if existing:
|
if existing:
|
||||||
return
|
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(
|
plating_lines = self.order_line.filtered(
|
||||||
lambda l: (
|
lambda l: (
|
||||||
('x_fc_part_catalog_id' in l._fields and l.x_fc_part_catalog_id)
|
('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
|
# Fallback: SOs that carry part on the header but not on the
|
||||||
# header but not on the line. Treat the entire order as one
|
# line. Treat the entire order as one plating job so the planner
|
||||||
# plating line so the planner gets an fp.job to work against.
|
# gets an fp.job to work against.
|
||||||
if not plating_lines and self.order_line and (
|
if not plating_lines and self.order_line and (
|
||||||
('x_fc_part_catalog_id' in self._fields and self.x_fc_part_catalog_id)
|
'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)
|
|
||||||
):
|
):
|
||||||
_logger.info(
|
_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,
|
'treating all lines as a single plating job.', self.name,
|
||||||
)
|
)
|
||||||
plating_lines = self.order_line
|
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)
|
_logger.info('SO %s: no plating lines, skipping job creation.', self.name)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Group by (recipe, part, coating, thickness, serial). Lines that
|
# Group by (recipe, part, spec, thickness, serial). Lines that
|
||||||
# share ALL FIVE collapse into one WO. Same compliance reasoning
|
# share ALL FIVE collapse into one WO. Bundling lines with
|
||||||
# as part_id + coating_id: bundling lines with different thicknesses
|
# different specs / thicknesses / serials under one WO would
|
||||||
# or different serials under one WO would carry the first line's
|
# carry the first line's values onto the cert + sticker —
|
||||||
# values onto the cert + sticker — silent mis-attestation. Sub 5
|
# silent mis-attestation. No-recipe lines still get their own
|
||||||
# added thickness_id + serial_id; this extends the grouping logic
|
# group each.
|
||||||
# to honour them. No-recipe lines still get their own group each.
|
|
||||||
groups = {}
|
groups = {}
|
||||||
unrecipe_idx = 0
|
unrecipe_idx = 0
|
||||||
for line in plating_lines:
|
for line in plating_lines:
|
||||||
@@ -427,9 +416,9 @@ class SaleOrder(models.Model):
|
|||||||
'x_fc_part_catalog_id' in line._fields
|
'x_fc_part_catalog_id' in line._fields
|
||||||
and line.x_fc_part_catalog_id.id
|
and line.x_fc_part_catalog_id.id
|
||||||
) or False
|
) or False
|
||||||
coating_id = (
|
spec_id = (
|
||||||
'x_fc_coating_config_id' in line._fields
|
'x_fc_customer_spec_id' in line._fields
|
||||||
and line.x_fc_coating_config_id.id
|
and line.x_fc_customer_spec_id.id
|
||||||
) or False
|
) or False
|
||||||
thickness_id = (
|
thickness_id = (
|
||||||
'x_fc_thickness_id' in line._fields
|
'x_fc_thickness_id' in line._fields
|
||||||
@@ -440,7 +429,7 @@ class SaleOrder(models.Model):
|
|||||||
and line.x_fc_serial_id.id
|
and line.x_fc_serial_id.id
|
||||||
) or False
|
) or False
|
||||||
if recipe:
|
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:
|
else:
|
||||||
unrecipe_idx += 1
|
unrecipe_idx += 1
|
||||||
key = ('no_recipe', unrecipe_idx)
|
key = ('no_recipe', unrecipe_idx)
|
||||||
@@ -465,11 +454,6 @@ class SaleOrder(models.Model):
|
|||||||
and first_line.x_fc_part_catalog_id
|
and first_line.x_fc_part_catalog_id
|
||||||
or False
|
or False
|
||||||
)
|
)
|
||||||
coating = (
|
|
||||||
'x_fc_coating_config_id' in first_line._fields
|
|
||||||
and first_line.x_fc_coating_config_id
|
|
||||||
or False
|
|
||||||
)
|
|
||||||
customer_spec = (
|
customer_spec = (
|
||||||
'x_fc_customer_spec_id' in first_line._fields
|
'x_fc_customer_spec_id' in first_line._fields
|
||||||
and first_line.x_fc_customer_spec_id
|
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:
|
if not part and 'x_fc_part_catalog_id' in self._fields:
|
||||||
part = self.x_fc_part_catalog_id or False
|
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)
|
recipe = self._fp_resolve_recipe_for_line(first_line)
|
||||||
|
|
||||||
vals = {
|
vals = {
|
||||||
@@ -492,8 +474,6 @@ class SaleOrder(models.Model):
|
|||||||
}
|
}
|
||||||
if part:
|
if part:
|
||||||
vals['part_catalog_id'] = part.id
|
vals['part_catalog_id'] = part.id
|
||||||
if coating:
|
|
||||||
vals['coating_config_id'] = coating.id
|
|
||||||
if customer_spec:
|
if customer_spec:
|
||||||
vals['customer_spec_id'] = customer_spec.id
|
vals['customer_spec_id'] = customer_spec.id
|
||||||
if recipe:
|
if recipe:
|
||||||
|
|||||||
@@ -56,7 +56,6 @@
|
|||||||
<t t-set="_so" t-value="job.sale_order_id"/>
|
<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="_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="_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="_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="_process" t-value="job.recipe_id or False"/>
|
||||||
<t t-set="_due" t-value="job.date_deadline 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="_so" t-value="job.sale_order_id"/>
|
||||||
<t t-set="_line" t-value="job.sale_order_line_ids[:1]"/>
|
<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="_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="_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="_process" t-value="job.recipe_id or False"/>
|
||||||
<t t-set="_due" t-value="job.date_deadline 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">
|
<t t-if="'customer_spec_id' in job._fields and job.customer_spec_id">
|
||||||
<span t-esc="job.customer_spec_id.display_name"/>
|
<span t-esc="job.customer_spec_id.display_name"/>
|
||||||
</t>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//field[@name='product_id']" position="after">
|
<xpath expr="//field[@name='product_id']" position="after">
|
||||||
<field name="part_catalog_id" string="Part"/>
|
<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"/>
|
<field name="recipe_id" string="Process Recipe"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
<!-- Show qty completed alongside total so the partial-qty
|
<!-- Show qty completed alongside total so the partial-qty
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Quality (QMS)',
|
'name': 'Fusion Plating — Quality (QMS)',
|
||||||
'version': '19.0.5.3.0',
|
'version': '19.0.6.0.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Native QMS for plating shops: NCR, CAPA, calibration, AVL, FAIR, '
|
'summary': 'Native QMS for plating shops: NCR, CAPA, calibration, AVL, FAIR, '
|
||||||
'internal audits, customer specs, document control. CE + EE compatible.',
|
'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',
|
'fp.part.catalog', 'fp_quality_point_part_rel',
|
||||||
'point_id', 'part_id', string='Parts',
|
'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(
|
customer_spec_ids = fields.Many2many(
|
||||||
'fusion.plating.customer.spec',
|
'fusion.plating.customer.spec',
|
||||||
'fp_quality_point_spec_rel',
|
'fp_quality_point_spec_rel',
|
||||||
@@ -119,7 +115,7 @@ class FpQualityPoint(models.Model):
|
|||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Matching + spawning
|
# 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):
|
customer_spec=None, recipe=None):
|
||||||
"""Return True if this point's filters all pass against the supplied
|
"""Return True if this point's filters all pass against the supplied
|
||||||
context. Empty filter == match anything.
|
context. Empty filter == match anything.
|
||||||
@@ -130,9 +126,6 @@ class FpQualityPoint(models.Model):
|
|||||||
if self.part_catalog_ids and (
|
if self.part_catalog_ids and (
|
||||||
not part or part not in self.part_catalog_ids):
|
not part or part not in self.part_catalog_ids):
|
||||||
return False
|
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 (
|
if self.customer_spec_ids and (
|
||||||
not customer_spec
|
not customer_spec
|
||||||
or customer_spec not in self.customer_spec_ids):
|
or customer_spec not in self.customer_spec_ids):
|
||||||
@@ -146,7 +139,7 @@ class FpQualityPoint(models.Model):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
@api.model
|
@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):
|
step=None, customer_spec=None, recipe=None):
|
||||||
"""Return active points whose trigger + filters match the context."""
|
"""Return active points whose trigger + filters match the context."""
|
||||||
candidates = self.search([
|
candidates = self.search([
|
||||||
@@ -154,7 +147,7 @@ class FpQualityPoint(models.Model):
|
|||||||
('trigger_type', '=', trigger),
|
('trigger_type', '=', trigger),
|
||||||
])
|
])
|
||||||
return candidates.filtered(lambda p: p._matches(
|
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,
|
customer_spec=customer_spec, recipe=recipe,
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|||||||
@@ -52,22 +52,16 @@ class SaleOrderPointHook(models.Model):
|
|||||||
# Walk lines for part / coating / spec context.
|
# Walk lines for part / coating / spec context.
|
||||||
parts = so.order_line.mapped('x_fc_part_catalog_id') \
|
parts = so.order_line.mapped('x_fc_part_catalog_id') \
|
||||||
if 'x_fc_part_catalog_id' in so.order_line._fields else False
|
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') \
|
specs = so.order_line.mapped('x_fc_customer_spec_id') \
|
||||||
if 'x_fc_customer_spec_id' in so.order_line._fields else False
|
if 'x_fc_customer_spec_id' in so.order_line._fields else False
|
||||||
points = Point._find_matching(
|
points = Point._find_matching(
|
||||||
trigger='so_confirmed', partner=partner,
|
trigger='so_confirmed', partner=partner,
|
||||||
)
|
)
|
||||||
for point in points:
|
for point in points:
|
||||||
# Filter by part / coating / spec intersection if the
|
# Filter by part / spec intersection if the point cares.
|
||||||
# point cares.
|
|
||||||
if point.part_catalog_ids and parts and \
|
if point.part_catalog_ids and parts and \
|
||||||
not (point.part_catalog_ids & parts):
|
not (point.part_catalog_ids & parts):
|
||||||
continue
|
continue
|
||||||
if point.coating_config_ids and coatings and \
|
|
||||||
not (point.coating_config_ids & coatings):
|
|
||||||
continue
|
|
||||||
if point.customer_spec_ids and specs and \
|
if point.customer_spec_ids and specs and \
|
||||||
not (point.customer_spec_ids & specs):
|
not (point.customer_spec_ids & specs):
|
||||||
continue
|
continue
|
||||||
@@ -85,12 +79,11 @@ class FpJobPointHook(models.Model):
|
|||||||
for job in self:
|
for job in self:
|
||||||
partner = job.partner_id
|
partner = job.partner_id
|
||||||
part = getattr(job, 'part_catalog_id', False) or 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
|
customer_spec = getattr(job, 'customer_spec_id', False) or False
|
||||||
recipe = getattr(job, 'recipe_id', False) or False
|
recipe = getattr(job, 'recipe_id', False) or False
|
||||||
points = Point._find_matching(
|
points = Point._find_matching(
|
||||||
trigger='job_confirmed', partner=partner,
|
trigger='job_confirmed', partner=partner,
|
||||||
part=part or None, coating=coating or None,
|
part=part or None,
|
||||||
customer_spec=customer_spec or None,
|
customer_spec=customer_spec or None,
|
||||||
recipe=recipe or None,
|
recipe=recipe or None,
|
||||||
)
|
)
|
||||||
@@ -108,12 +101,11 @@ class FpJobPointHook(models.Model):
|
|||||||
continue
|
continue
|
||||||
partner = job.partner_id
|
partner = job.partner_id
|
||||||
part = getattr(job, 'part_catalog_id', False) or 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
|
customer_spec = getattr(job, 'customer_spec_id', False) or False
|
||||||
recipe = getattr(job, 'recipe_id', False) or False
|
recipe = getattr(job, 'recipe_id', False) or False
|
||||||
points = Point._find_matching(
|
points = Point._find_matching(
|
||||||
trigger='job_done', partner=partner,
|
trigger='job_done', partner=partner,
|
||||||
part=part or None, coating=coating or None,
|
part=part or None,
|
||||||
customer_spec=customer_spec or None,
|
customer_spec=customer_spec or None,
|
||||||
recipe=recipe or None,
|
recipe=recipe or None,
|
||||||
)
|
)
|
||||||
@@ -137,12 +129,11 @@ class FpJobStepPointHook(models.Model):
|
|||||||
job = step.job_id
|
job = step.job_id
|
||||||
partner = job.partner_id if job else False
|
partner = job.partner_id if job else False
|
||||||
part = getattr(job, 'part_catalog_id', False) or 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
|
customer_spec = getattr(job, 'customer_spec_id', False) or False
|
||||||
recipe = getattr(job, 'recipe_id', False) or False
|
recipe = getattr(job, 'recipe_id', False) or False
|
||||||
points = Point._find_matching(
|
points = Point._find_matching(
|
||||||
trigger='job_step_done', partner=partner,
|
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,
|
customer_spec=customer_spec or None,
|
||||||
recipe=recipe or None,
|
recipe=recipe or None,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,19 +24,9 @@ class FpQuoteConfigurator(models.Model):
|
|||||||
"""Extend the configurator's matcher to consider Spec + Recipe.
|
"""Extend the configurator's matcher to consider Spec + Recipe.
|
||||||
|
|
||||||
Spec match adds +8 (highest priority — explicit customer spec
|
Spec match adds +8 (highest priority — explicit customer spec
|
||||||
wins over chemistry / cert-level filters). Recipe adds +6.
|
wins over chemistry filters). Recipe adds +6. Material is +2.
|
||||||
Falls through to the existing coating / material / cert scoring.
|
|
||||||
"""
|
"""
|
||||||
# Cache the recipe before super (super may overwrite via thickness
|
recipe = self.recipe_id or False
|
||||||
# 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.
|
|
||||||
builder_rules = (
|
builder_rules = (
|
||||||
recipe.pricing_rule_ids
|
recipe.pricing_rule_ids
|
||||||
if recipe else self.env['fp.pricing.rule']
|
if recipe else self.env['fp.pricing.rule']
|
||||||
@@ -49,38 +39,25 @@ class FpQuoteConfigurator(models.Model):
|
|||||||
rules = self.env['fp.pricing.rule'].search(
|
rules = self.env['fp.pricing.rule'].search(
|
||||||
[('active', '=', True)], order='sequence, id'
|
[('active', '=', True)], order='sequence, id'
|
||||||
)
|
)
|
||||||
cert_level = (
|
|
||||||
self.coating_config_id.certification_level
|
|
||||||
if self.coating_config_id else False
|
|
||||||
)
|
|
||||||
|
|
||||||
best = None
|
best = None
|
||||||
best_score = -1
|
best_score = -1
|
||||||
for rule in rules:
|
for rule in rules:
|
||||||
score = 0
|
score = 0
|
||||||
# NEW — spec wins biggest
|
# Spec wins biggest
|
||||||
if rule.customer_spec_id:
|
if rule.customer_spec_id:
|
||||||
if rule.customer_spec_id != self.customer_spec_id:
|
if rule.customer_spec_id != self.customer_spec_id:
|
||||||
continue
|
continue
|
||||||
score += 8
|
score += 8
|
||||||
# NEW — recipe is next
|
# Recipe is next
|
||||||
if rule.recipe_id:
|
if rule.recipe_id:
|
||||||
if rule.recipe_id != recipe:
|
if rule.recipe_id != recipe:
|
||||||
continue
|
continue
|
||||||
score += 6
|
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:
|
||||||
if rule.substrate_material != self.substrate_material:
|
if rule.substrate_material != self.substrate_material:
|
||||||
continue
|
continue
|
||||||
score += 2
|
score += 2
|
||||||
if rule.certification_level:
|
|
||||||
if rule.certification_level != cert_level:
|
|
||||||
continue
|
|
||||||
score += 1
|
|
||||||
if score > best_score:
|
if score > best_score:
|
||||||
best_score = score
|
best_score = score
|
||||||
best = rule
|
best = rule
|
||||||
|
|||||||
@@ -15,17 +15,18 @@
|
|||||||
<field name="inherit_id"
|
<field name="inherit_id"
|
||||||
ref="fusion_plating_configurator.view_fp_direct_order_wizard_form"/>
|
ref="fusion_plating_configurator.view_fp_direct_order_wizard_form"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<!-- Wizard line list (main editable rows) -->
|
<!-- Wizard line list (main editable rows). Anchor on
|
||||||
<xpath expr="//field[@name='line_ids']/list/field[@name='coating_config_id']"
|
internal_description (stable, configurator-defined). -->
|
||||||
|
<xpath expr="//field[@name='line_ids']/list/field[@name='internal_description']"
|
||||||
position="after">
|
position="after">
|
||||||
<field name="customer_spec_id"
|
<field name="customer_spec_id"
|
||||||
string="Specification"
|
string="Specification"
|
||||||
options="{'no_quick_create': True}"
|
options="{'no_quick_create': True}"
|
||||||
optional="show"/>
|
optional="show"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
<!-- Wizard line drawer / form view (the "expand line" panel) -->
|
<!-- Wizard line drawer / form view -->
|
||||||
<xpath expr="//field[@name='line_ids']/form//field[@name='coating_config_id']"
|
<xpath expr="//field[@name='line_ids']/form//field[@name='process_variant_id']"
|
||||||
position="after">
|
position="before">
|
||||||
<field name="customer_spec_id"
|
<field name="customer_spec_id"
|
||||||
string="Specification"
|
string="Specification"
|
||||||
options="{'no_quick_create': True}"/>
|
options="{'no_quick_create': True}"/>
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
<field name="inherit_id"
|
<field name="inherit_id"
|
||||||
ref="fusion_plating_configurator.view_fp_part_catalog_form"/>
|
ref="fusion_plating_configurator.view_fp_part_catalog_form"/>
|
||||||
<field name="arch" type="xml">
|
<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">
|
position="after">
|
||||||
<field name="x_fc_default_customer_spec_id"
|
<field name="x_fc_default_customer_spec_id"
|
||||||
string="Default Specification"
|
string="Default Specification"
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
<field name="inherit_id"
|
<field name="inherit_id"
|
||||||
ref="fusion_plating_configurator.view_fp_pricing_rule_form"/>
|
ref="fusion_plating_configurator.view_fp_pricing_rule_form"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//field[@name='coating_config_id']"
|
<xpath expr="//field[@name='substrate_material']"
|
||||||
position="after">
|
position="before">
|
||||||
<field name="customer_spec_id"
|
<field name="customer_spec_id"
|
||||||
options="{'no_quick_create': True}"/>
|
options="{'no_quick_create': True}"/>
|
||||||
<field name="recipe_id"
|
<field name="recipe_id"
|
||||||
|
|||||||
@@ -69,8 +69,6 @@
|
|||||||
placeholder="All specs if empty"/>
|
placeholder="All specs if empty"/>
|
||||||
<field name="recipe_ids" widget="many2many_tags"
|
<field name="recipe_ids" widget="many2many_tags"
|
||||||
placeholder="All recipes if empty"/>
|
placeholder="All recipes if empty"/>
|
||||||
<field name="coating_config_ids" widget="many2many_tags"
|
|
||||||
placeholder="All coatings if empty"/>
|
|
||||||
<field name="step_kind"
|
<field name="step_kind"
|
||||||
invisible="trigger_type != 'job_step_done'"
|
invisible="trigger_type != 'job_step_done'"
|
||||||
placeholder="Any step kind if empty"/>
|
placeholder="Any step kind if empty"/>
|
||||||
|
|||||||
@@ -17,15 +17,18 @@
|
|||||||
|
|
||||||
<!-- Configurator's view_sale_order_form_fp inherits sale.view_order_form
|
<!-- Configurator's view_sale_order_form_fp inherits sale.view_order_form
|
||||||
and adds Plating fields to the order_line tree. We inherit THAT
|
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">
|
<record id="view_sale_order_form_quality_inherit" model="ir.ui.view">
|
||||||
<field name="name">sale.order.form.quality.spec.inherit</field>
|
<field name="name">sale.order.form.quality.spec.inherit</field>
|
||||||
<field name="model">sale.order</field>
|
<field name="model">sale.order</field>
|
||||||
<field name="inherit_id"
|
<field name="inherit_id"
|
||||||
ref="fusion_plating_configurator.view_sale_order_form_fp"/>
|
ref="fusion_plating_configurator.view_sale_order_form_fp"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<!-- Editable order_line tree (estimator's main grid) -->
|
<!-- Editable order_line tree (estimator's main grid).
|
||||||
<xpath expr="//field[@name='order_line']/list/field[@name='x_fc_coating_config_id']"
|
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">
|
position="after">
|
||||||
<field name="x_fc_customer_spec_id"
|
<field name="x_fc_customer_spec_id"
|
||||||
string="Specification"
|
string="Specification"
|
||||||
|
|||||||
@@ -115,9 +115,6 @@
|
|||||||
<t t-if="so and so.x_fc_customer_spec_id">
|
<t t-if="so and so.x_fc_customer_spec_id">
|
||||||
<span t-field="so.x_fc_customer_spec_id"/>
|
<span t-field="so.x_fc_customer_spec_id"/>
|
||||||
</t>
|
</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>
|
<t t-else="">—</t>
|
||||||
</td>
|
</td>
|
||||||
<th class="info-header">Recipe</th>
|
<th class="info-header">Recipe</th>
|
||||||
|
|||||||
@@ -19,8 +19,7 @@
|
|||||||
* _mo — the mrp.production record (or False)
|
* _mo — the mrp.production record (or False)
|
||||||
* _so, _line — the originating sale order / line
|
* _so, _line — the originating sale order / line
|
||||||
* _part — fp.part.catalog
|
* _part — fp.part.catalog
|
||||||
* _coating — fp.coating.config (legacy; removed in Phase E)
|
* _spec — fusion.plating.customer.spec (audit-tracked spec)
|
||||||
* _spec — fusion.plating.customer.spec (the audit-tracked spec the cert prints)
|
|
||||||
* _process — the resolved fusion.plating.process.node tree
|
* _process — the resolved fusion.plating.process.node tree
|
||||||
* _due — datetime/date for "Due Date" row
|
* _due — datetime/date for "Due Date" row
|
||||||
* _qty — float for "Qty" row
|
* _qty — float for "Qty" row
|
||||||
@@ -48,11 +47,9 @@
|
|||||||
or (_so and _so.order_line[:1])
|
or (_so and _so.order_line[:1])
|
||||||
or False"/>
|
or False"/>
|
||||||
<t t-set="_part" t-value="_part or (_line and _line.x_fc_part_catalog_id) 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="_spec" t-value="_spec or (_line and _line.x_fc_customer_spec_id) or False"/>
|
||||||
<t t-set="_process" t-value="_process
|
<t t-set="_process" t-value="_process
|
||||||
or (_part and _part.default_process_id)
|
or (_part and _part.default_process_id)
|
||||||
or (_coating and _coating.recipe_id)
|
|
||||||
or False"/>
|
or False"/>
|
||||||
<t t-set="_due" t-value="_due
|
<t t-set="_due" t-value="_due
|
||||||
or (_mo and (_mo.date_deadline or _mo.date_finished))
|
or (_mo and (_mo.date_deadline or _mo.date_finished))
|
||||||
@@ -470,7 +467,6 @@
|
|||||||
<t t-set="_so" t-value="so"/>
|
<t t-set="_so" t-value="so"/>
|
||||||
<t t-set="_line" t-value="line"/>
|
<t t-set="_line" t-value="line"/>
|
||||||
<t t-set="_part" t-value="line.x_fc_part_catalog_id"/>
|
<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="_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="_due" t-value="line.x_fc_part_deadline or so.commitment_date or False"/>
|
||||||
<t t-set="_qty" t-value="line.product_uom_qty"/>
|
<t t-set="_qty" t-value="line.product_uom_qty"/>
|
||||||
@@ -501,7 +497,6 @@
|
|||||||
<t t-set="_so" t-value="so"/>
|
<t t-set="_so" t-value="so"/>
|
||||||
<t t-set="_line" t-value="line"/>
|
<t t-set="_line" t-value="line"/>
|
||||||
<t t-set="_part" t-value="line.x_fc_part_catalog_id"/>
|
<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="_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="_due" t-value="line.x_fc_part_deadline or so.commitment_date or False"/>
|
||||||
<t t-set="_qty" t-value="line.product_uom_qty"/>
|
<t t-set="_qty" t-value="line.product_uom_qty"/>
|
||||||
|
|||||||
@@ -1142,7 +1142,7 @@ class FpShopfloorController(http.Controller):
|
|||||||
job_read_fields = [
|
job_read_fields = [
|
||||||
'name', 'origin', 'priority', 'partner_id', 'product_id',
|
'name', 'origin', 'priority', 'partner_id', 'product_id',
|
||||||
'qty', 'qty_done', 'date_planned_start', 'date_deadline',
|
'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:
|
if 'customer_spec_id' in unique_jobs._fields:
|
||||||
job_read_fields.append('customer_spec_id')
|
job_read_fields.append('customer_spec_id')
|
||||||
@@ -1555,10 +1555,6 @@ class FpShopfloorController(http.Controller):
|
|||||||
job.part_catalog_id
|
job.part_catalog_id
|
||||||
if 'part_catalog_id' in job._fields else False
|
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)
|
# Specification (added by fusion_plating_quality)
|
||||||
spec = (
|
spec = (
|
||||||
job.customer_spec_id
|
job.customer_spec_id
|
||||||
@@ -1572,12 +1568,9 @@ class FpShopfloorController(http.Controller):
|
|||||||
getattr(part, 'part_number', '') or part.name or ''
|
getattr(part, 'part_number', '') or part.name or ''
|
||||||
)
|
)
|
||||||
part_revision = getattr(part, 'revision', '') or ''
|
part_revision = getattr(part, 'revision', '') or ''
|
||||||
|
# coating_label kept blank — Phase E removed coating; downstream
|
||||||
|
# tablet templates read spec_label instead.
|
||||||
coating_label = ''
|
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 + product image
|
||||||
customer_logo_url = ''
|
customer_logo_url = ''
|
||||||
|
|||||||
Reference in New Issue
Block a user