feat(promote-customer-spec): Phase C — pricing, quality, job, cert re-keyed
Pricing:
- Quality inherit on fp.pricing.rule adds customer_spec_id + recipe_id
- Quality inherit on fp.quote.configurator adds customer_spec_id field
+ extends _find_matching_rule with priority chain:
spec (+8) > recipe (+6) > coating (+4) > material (+2) > cert (+1)
- View inherit surfaces both new pickers on the rule form
Quality points:
- fp.quality.point now has customer_spec_ids + recipe_ids M2M filters
- Matcher (_matches + _find_matching) accepts new args
- Hook overrides on SO confirm + job confirm/done + step finish
pass spec/recipe context through to the matcher
- View surfaces both new M2M widgets
Job:
- jobs/sale_order.py wires x_fc_customer_spec_id from SO line to
fp.job.customer_spec_id on action_confirm
Cert:
- Quality inherit on fp.certificate adds customer_spec_id field +
create() override auto-fills spec_reference from spec.code+revision
Resolution priority: explicit spec_reference > cert.customer_spec_id
> SO line spec (with print_on_cert) > legacy coating fallback
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.8.27.0',
|
'version': '19.0.9.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.',
|
||||||
|
|||||||
@@ -470,6 +470,11 @@ class SaleOrder(models.Model):
|
|||||||
and first_line.x_fc_coating_config_id
|
and first_line.x_fc_coating_config_id
|
||||||
or False
|
or False
|
||||||
)
|
)
|
||||||
|
customer_spec = (
|
||||||
|
'x_fc_customer_spec_id' in first_line._fields
|
||||||
|
and first_line.x_fc_customer_spec_id
|
||||||
|
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
|
||||||
if not coating and 'x_fc_coating_config_id' in self._fields:
|
if not coating and 'x_fc_coating_config_id' in self._fields:
|
||||||
@@ -489,6 +494,8 @@ class SaleOrder(models.Model):
|
|||||||
vals['part_catalog_id'] = part.id
|
vals['part_catalog_id'] = part.id
|
||||||
if coating:
|
if coating:
|
||||||
vals['coating_config_id'] = coating.id
|
vals['coating_config_id'] = coating.id
|
||||||
|
if customer_spec:
|
||||||
|
vals['customer_spec_id'] = customer_spec.id
|
||||||
if recipe:
|
if recipe:
|
||||||
vals['recipe_id'] = recipe.id
|
vals['recipe_id'] = recipe.id
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Quality (QMS)',
|
'name': 'Fusion Plating — Quality (QMS)',
|
||||||
'version': '19.0.5.1.0',
|
'version': '19.0.5.2.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.',
|
||||||
@@ -94,6 +94,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
|||||||
'views/sale_order_views_inherit.xml',
|
'views/sale_order_views_inherit.xml',
|
||||||
'views/fp_part_catalog_views_inherit.xml',
|
'views/fp_part_catalog_views_inherit.xml',
|
||||||
'views/fp_direct_order_wizard_views_inherit.xml',
|
'views/fp_direct_order_wizard_views_inherit.xml',
|
||||||
|
'views/fp_pricing_rule_views_inherit.xml',
|
||||||
'views/fp_audit_views.xml',
|
'views/fp_audit_views.xml',
|
||||||
'views/fp_fair_views.xml',
|
'views/fp_fair_views.xml',
|
||||||
'views/fp_doc_control_views.xml',
|
'views/fp_doc_control_views.xml',
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ from . import fp_process_node_inherit
|
|||||||
from . import sale_order_line_inherit
|
from . import sale_order_line_inherit
|
||||||
from . import account_move_line_inherit
|
from . import account_move_line_inherit
|
||||||
from . import fp_direct_order_line_inherit
|
from . import fp_direct_order_line_inherit
|
||||||
|
from . import fp_pricing_rule_inherit
|
||||||
|
from . import fp_quote_configurator_inherit
|
||||||
|
from . import fp_certificate_inherit
|
||||||
from . import fp_audit
|
from . import fp_audit
|
||||||
from . import fp_fair
|
from . import fp_fair
|
||||||
from . import fp_doc_control
|
from . import fp_doc_control
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# -*- 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 FpCertificate(models.Model):
|
||||||
|
"""Add Specification linkage + auto-fill spec_reference from it.
|
||||||
|
|
||||||
|
Lives in fusion_plating_quality because customer.spec lives here.
|
||||||
|
Quality already depends on certificates, so the inverse direction
|
||||||
|
works.
|
||||||
|
"""
|
||||||
|
_inherit = 'fp.certificate'
|
||||||
|
|
||||||
|
customer_spec_id = fields.Many2one(
|
||||||
|
'fusion.plating.customer.spec',
|
||||||
|
string='Specification',
|
||||||
|
help='Snapshot of the specification the cert was issued against. '
|
||||||
|
'Drives the spec_reference printed on the CoC.',
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
"""Auto-fill spec_reference from the SO line's customer_spec_id.
|
||||||
|
|
||||||
|
Resolution order (first match wins):
|
||||||
|
1. Explicit spec_reference passed in vals.
|
||||||
|
2. customer_spec_id (this field) → format "code Rev rev".
|
||||||
|
3. SO line x_fc_customer_spec_id (with print_on_cert=True).
|
||||||
|
4. Existing legacy fall-back lives in the parent module
|
||||||
|
(reads x_fc_coating_config_id.spec_reference). Untouched.
|
||||||
|
"""
|
||||||
|
SaleOrder = self.env['sale.order']
|
||||||
|
for vals in vals_list:
|
||||||
|
if vals.get('spec_reference'):
|
||||||
|
continue
|
||||||
|
spec = False
|
||||||
|
# 2. Explicit spec on the cert.
|
||||||
|
if vals.get('customer_spec_id'):
|
||||||
|
spec = self.env['fusion.plating.customer.spec'].browse(
|
||||||
|
vals['customer_spec_id'],
|
||||||
|
).exists()
|
||||||
|
# 3. SO line's spec.
|
||||||
|
if not spec and vals.get('sale_order_id'):
|
||||||
|
so = SaleOrder.browse(vals['sale_order_id'])
|
||||||
|
if 'x_fc_customer_spec_id' in so.order_line._fields:
|
||||||
|
spec = so.order_line.mapped(
|
||||||
|
'x_fc_customer_spec_id',
|
||||||
|
).filtered('print_on_cert')[:1]
|
||||||
|
if spec and not vals.get('customer_spec_id'):
|
||||||
|
vals['customer_spec_id'] = spec.id
|
||||||
|
if spec:
|
||||||
|
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
|
||||||
|
return super().create(vals_list)
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# -*- 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 FpPricingRule(models.Model):
|
||||||
|
"""Add Specification + Recipe match keys to the pricing rule.
|
||||||
|
|
||||||
|
Lives in fusion_plating_quality because fusion.plating.customer.spec
|
||||||
|
lives here. Rules can now match on:
|
||||||
|
- customer_spec_id (most specific — e.g. "AMS 2404 surcharge")
|
||||||
|
- recipe_id (recipe-tier — e.g. "EN Mid-Phos $X/sqft")
|
||||||
|
- both blank (fallback — material/cert-level matching)
|
||||||
|
|
||||||
|
The configurator's matcher is extended in fp_quote_configurator_inherit.
|
||||||
|
"""
|
||||||
|
_inherit = 'fp.pricing.rule'
|
||||||
|
|
||||||
|
customer_spec_id = fields.Many2one(
|
||||||
|
'fusion.plating.customer.spec',
|
||||||
|
string='Specification',
|
||||||
|
help='Match rule against the SO line specification. Combine with '
|
||||||
|
'recipe_id for spec+recipe specific pricing, or leave recipe '
|
||||||
|
'blank for spec-tier pricing.',
|
||||||
|
)
|
||||||
|
recipe_id = fields.Many2one(
|
||||||
|
'fusion.plating.process.node',
|
||||||
|
string='Recipe',
|
||||||
|
domain="[('node_type', '=', 'recipe'), ('parent_id', '=', False)]",
|
||||||
|
help='Match rule against the SO line recipe. Combine with '
|
||||||
|
'customer_spec_id for spec+recipe specific pricing, or '
|
||||||
|
'leave spec blank for recipe-tier pricing.',
|
||||||
|
)
|
||||||
@@ -67,6 +67,23 @@ class FpQualityPoint(models.Model):
|
|||||||
'fp.coating.config', 'fp_quality_point_coating_rel',
|
'fp.coating.config', 'fp_quality_point_coating_rel',
|
||||||
'point_id', 'coating_id', string='Coatings',
|
'point_id', 'coating_id', string='Coatings',
|
||||||
)
|
)
|
||||||
|
customer_spec_ids = fields.Many2many(
|
||||||
|
'fusion.plating.customer.spec',
|
||||||
|
'fp_quality_point_spec_rel',
|
||||||
|
'point_id', 'spec_id',
|
||||||
|
string='Specifications',
|
||||||
|
help='If set, this trigger only fires for SOs / jobs whose '
|
||||||
|
'specification is in this list. Leave blank to ignore spec.',
|
||||||
|
)
|
||||||
|
recipe_ids = fields.Many2many(
|
||||||
|
'fusion.plating.process.node',
|
||||||
|
'fp_quality_point_recipe_rel',
|
||||||
|
'point_id', 'recipe_id',
|
||||||
|
domain="[('node_type', '=', 'recipe'), ('parent_id', '=', False)]",
|
||||||
|
string='Recipes',
|
||||||
|
help='If set, this trigger only fires for jobs running one of '
|
||||||
|
'these recipes. Leave blank to ignore recipe.',
|
||||||
|
)
|
||||||
step_kind = fields.Selection(STEP_KINDS, string='Step Kind')
|
step_kind = fields.Selection(STEP_KINDS, string='Step Kind')
|
||||||
|
|
||||||
template_id = fields.Many2one(
|
template_id = fields.Many2one(
|
||||||
@@ -102,7 +119,8 @@ 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, coating=None, step=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.
|
||||||
"""
|
"""
|
||||||
@@ -115,6 +133,13 @@ class FpQualityPoint(models.Model):
|
|||||||
if self.coating_config_ids and (
|
if self.coating_config_ids and (
|
||||||
not coating or coating not in self.coating_config_ids):
|
not coating or coating not in self.coating_config_ids):
|
||||||
return False
|
return False
|
||||||
|
if self.customer_spec_ids and (
|
||||||
|
not customer_spec
|
||||||
|
or customer_spec not in self.customer_spec_ids):
|
||||||
|
return False
|
||||||
|
if self.recipe_ids and (
|
||||||
|
not recipe or recipe not in self.recipe_ids):
|
||||||
|
return False
|
||||||
if self.step_kind and step and getattr(step, 'kind', None) \
|
if self.step_kind and step and getattr(step, 'kind', None) \
|
||||||
and step.kind != self.step_kind:
|
and step.kind != self.step_kind:
|
||||||
return False
|
return False
|
||||||
@@ -122,7 +147,7 @@ class FpQualityPoint(models.Model):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _find_matching(self, trigger, partner=None, part=None, coating=None,
|
def _find_matching(self, trigger, partner=None, part=None, coating=None,
|
||||||
step=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([
|
||||||
('active', '=', True),
|
('active', '=', True),
|
||||||
@@ -130,6 +155,7 @@ class FpQualityPoint(models.Model):
|
|||||||
])
|
])
|
||||||
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, coating=coating, step=step,
|
||||||
|
customer_spec=customer_spec, recipe=recipe,
|
||||||
))
|
))
|
||||||
|
|
||||||
def _spawn_check_for(self, source, partner=None, job=None, step=None):
|
def _spawn_check_for(self, source, partner=None, job=None, step=None):
|
||||||
|
|||||||
@@ -49,22 +49,28 @@ class SaleOrderPointHook(models.Model):
|
|||||||
Point = self.env['fp.quality.point']
|
Point = self.env['fp.quality.point']
|
||||||
for so in self:
|
for so in self:
|
||||||
partner = so.partner_id
|
partner = so.partner_id
|
||||||
# Walk lines for part / coating 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') \
|
coatings = so.order_line.mapped('x_fc_coating_config_id') \
|
||||||
if 'x_fc_coating_config_id' in so.order_line._fields else False
|
if 'x_fc_coating_config_id' in so.order_line._fields else False
|
||||||
|
specs = so.order_line.mapped('x_fc_customer_spec_id') \
|
||||||
|
if 'x_fc_customer_spec_id' in so.order_line._fields else False
|
||||||
points = Point._find_matching(
|
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 intersection if the point cares.
|
# Filter by part / coating / spec intersection if the
|
||||||
|
# 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 \
|
if point.coating_config_ids and coatings and \
|
||||||
not (point.coating_config_ids & coatings):
|
not (point.coating_config_ids & coatings):
|
||||||
continue
|
continue
|
||||||
|
if point.customer_spec_ids and specs and \
|
||||||
|
not (point.customer_spec_ids & specs):
|
||||||
|
continue
|
||||||
point._spawn_check_for(source=so, partner=partner)
|
point._spawn_check_for(source=so, partner=partner)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -80,9 +86,13 @@ class FpJobPointHook(models.Model):
|
|||||||
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
|
coating = getattr(job, 'coating_config_id', False) or False
|
||||||
|
customer_spec = getattr(job, 'customer_spec_id', False) or False
|
||||||
|
recipe = getattr(job, 'recipe_id', False) or False
|
||||||
points = Point._find_matching(
|
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, coating=coating or None,
|
||||||
|
customer_spec=customer_spec or None,
|
||||||
|
recipe=recipe or None,
|
||||||
)
|
)
|
||||||
for point in points:
|
for point in points:
|
||||||
point._spawn_check_for(
|
point._spawn_check_for(
|
||||||
@@ -99,9 +109,13 @@ class FpJobPointHook(models.Model):
|
|||||||
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
|
coating = getattr(job, 'coating_config_id', False) or False
|
||||||
|
customer_spec = getattr(job, 'customer_spec_id', False) or False
|
||||||
|
recipe = getattr(job, 'recipe_id', False) or False
|
||||||
points = Point._find_matching(
|
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, coating=coating or None,
|
||||||
|
customer_spec=customer_spec or None,
|
||||||
|
recipe=recipe or None,
|
||||||
)
|
)
|
||||||
for point in points:
|
for point in points:
|
||||||
point._spawn_check_for(
|
point._spawn_check_for(
|
||||||
@@ -124,9 +138,13 @@ class FpJobStepPointHook(models.Model):
|
|||||||
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
|
coating = getattr(job, 'coating_config_id', False) or False
|
||||||
|
customer_spec = getattr(job, 'customer_spec_id', False) or False
|
||||||
|
recipe = getattr(job, 'recipe_id', False) or False
|
||||||
points = Point._find_matching(
|
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, coating=coating or None, step=step,
|
||||||
|
customer_spec=customer_spec or None,
|
||||||
|
recipe=recipe or None,
|
||||||
)
|
)
|
||||||
for point in points:
|
for point in points:
|
||||||
point._spawn_check_for(
|
point._spawn_check_for(
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# -*- 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 FpQuoteConfigurator(models.Model):
|
||||||
|
"""Add Specification field + extend the pricing rule matcher.
|
||||||
|
|
||||||
|
Lives in fusion_plating_quality because customer.spec lives here.
|
||||||
|
"""
|
||||||
|
_inherit = 'fp.quote.configurator'
|
||||||
|
|
||||||
|
customer_spec_id = fields.Many2one(
|
||||||
|
'fusion.plating.customer.spec',
|
||||||
|
string='Specification',
|
||||||
|
help='Customer / industry spec the quote is built against. '
|
||||||
|
'Drives pricing rule lookup and certificate auto-fill.',
|
||||||
|
)
|
||||||
|
|
||||||
|
def _find_matching_rule(self):
|
||||||
|
"""Extend the configurator's matcher to consider Spec + Recipe.
|
||||||
|
|
||||||
|
Spec match adds +8 (highest priority — explicit customer spec
|
||||||
|
wins over chemistry / cert-level filters). Recipe adds +6.
|
||||||
|
Falls through to the existing coating / material / cert scoring.
|
||||||
|
"""
|
||||||
|
# Cache the recipe before super (super may overwrite via thickness
|
||||||
|
# logic in some inherit chains).
|
||||||
|
recipe = (
|
||||||
|
self.coating_config_id.recipe_id
|
||||||
|
if self.coating_config_id and self.coating_config_id.recipe_id
|
||||||
|
else False
|
||||||
|
)
|
||||||
|
# Build the candidate rule set the same way super does — but
|
||||||
|
# since super uses a private mechanism we re-implement to keep
|
||||||
|
# the spec/recipe scoring inline with the rest.
|
||||||
|
builder_rules = (
|
||||||
|
recipe.pricing_rule_ids
|
||||||
|
if recipe else self.env['fp.pricing.rule']
|
||||||
|
)
|
||||||
|
if builder_rules:
|
||||||
|
rules = builder_rules.filtered('active').sorted(
|
||||||
|
lambda r: (r.sequence, r.id)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
rules = self.env['fp.pricing.rule'].search(
|
||||||
|
[('active', '=', True)], order='sequence, id'
|
||||||
|
)
|
||||||
|
cert_level = (
|
||||||
|
self.coating_config_id.certification_level
|
||||||
|
if self.coating_config_id else False
|
||||||
|
)
|
||||||
|
|
||||||
|
best = None
|
||||||
|
best_score = -1
|
||||||
|
for rule in rules:
|
||||||
|
score = 0
|
||||||
|
# NEW — spec wins biggest
|
||||||
|
if rule.customer_spec_id:
|
||||||
|
if rule.customer_spec_id != self.customer_spec_id:
|
||||||
|
continue
|
||||||
|
score += 8
|
||||||
|
# NEW — recipe is next
|
||||||
|
if rule.recipe_id:
|
||||||
|
if rule.recipe_id != recipe:
|
||||||
|
continue
|
||||||
|
score += 6
|
||||||
|
# Legacy — coating / material / cert
|
||||||
|
if rule.coating_config_id:
|
||||||
|
if rule.coating_config_id != self.coating_config_id:
|
||||||
|
continue
|
||||||
|
score += 4
|
||||||
|
if rule.substrate_material:
|
||||||
|
if rule.substrate_material != self.substrate_material:
|
||||||
|
continue
|
||||||
|
score += 2
|
||||||
|
if rule.certification_level:
|
||||||
|
if rule.certification_level != cert_level:
|
||||||
|
continue
|
||||||
|
score += 1
|
||||||
|
if score > best_score:
|
||||||
|
best_score = score
|
||||||
|
best = rule
|
||||||
|
return best
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?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.
|
||||||
|
|
||||||
|
Adds Specification + Recipe pickers to the pricing rule form.
|
||||||
|
Both fields live on this module's inherit (the customer.spec model
|
||||||
|
lives here).
|
||||||
|
-->
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_fp_pricing_rule_form_quality_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">fp.pricing.rule.form.quality.spec.inherit</field>
|
||||||
|
<field name="model">fp.pricing.rule</field>
|
||||||
|
<field name="inherit_id"
|
||||||
|
ref="fusion_plating_configurator.view_fp_pricing_rule_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='coating_config_id']"
|
||||||
|
position="after">
|
||||||
|
<field name="customer_spec_id"
|
||||||
|
options="{'no_quick_create': True}"/>
|
||||||
|
<field name="recipe_id"
|
||||||
|
options="{'no_quick_create': True}"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
@@ -65,6 +65,10 @@
|
|||||||
placeholder="All parts if empty"/>
|
placeholder="All parts if empty"/>
|
||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
|
<field name="customer_spec_ids" widget="many2many_tags"
|
||||||
|
placeholder="All specs if empty"/>
|
||||||
|
<field name="recipe_ids" widget="many2many_tags"
|
||||||
|
placeholder="All recipes if empty"/>
|
||||||
<field name="coating_config_ids" widget="many2many_tags"
|
<field name="coating_config_ids" widget="many2many_tags"
|
||||||
placeholder="All coatings if empty"/>
|
placeholder="All coatings if empty"/>
|
||||||
<field name="step_kind"
|
<field name="step_kind"
|
||||||
|
|||||||
Reference in New Issue
Block a user