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

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

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

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

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

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

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

View File

@@ -63,10 +63,6 @@ class FpQualityPoint(models.Model):
'fp.part.catalog', 'fp_quality_point_part_rel',
'point_id', 'part_id', string='Parts',
)
coating_config_ids = fields.Many2many(
'fp.coating.config', 'fp_quality_point_coating_rel',
'point_id', 'coating_id', string='Coatings',
)
customer_spec_ids = fields.Many2many(
'fusion.plating.customer.spec',
'fp_quality_point_spec_rel',
@@ -119,7 +115,7 @@ class FpQualityPoint(models.Model):
# ------------------------------------------------------------------
# Matching + spawning
# ------------------------------------------------------------------
def _matches(self, partner=None, part=None, coating=None, step=None,
def _matches(self, partner=None, part=None, step=None,
customer_spec=None, recipe=None):
"""Return True if this point's filters all pass against the supplied
context. Empty filter == match anything.
@@ -130,9 +126,6 @@ class FpQualityPoint(models.Model):
if self.part_catalog_ids and (
not part or part not in self.part_catalog_ids):
return False
if self.coating_config_ids and (
not coating or coating not in self.coating_config_ids):
return False
if self.customer_spec_ids and (
not customer_spec
or customer_spec not in self.customer_spec_ids):
@@ -146,7 +139,7 @@ class FpQualityPoint(models.Model):
return True
@api.model
def _find_matching(self, trigger, partner=None, part=None, coating=None,
def _find_matching(self, trigger, partner=None, part=None,
step=None, customer_spec=None, recipe=None):
"""Return active points whose trigger + filters match the context."""
candidates = self.search([
@@ -154,7 +147,7 @@ class FpQualityPoint(models.Model):
('trigger_type', '=', trigger),
])
return candidates.filtered(lambda p: p._matches(
partner=partner, part=part, coating=coating, step=step,
partner=partner, part=part, step=step,
customer_spec=customer_spec, recipe=recipe,
))

View File

@@ -52,22 +52,16 @@ class SaleOrderPointHook(models.Model):
# Walk lines for part / coating / spec context.
parts = so.order_line.mapped('x_fc_part_catalog_id') \
if 'x_fc_part_catalog_id' in so.order_line._fields else False
coatings = so.order_line.mapped('x_fc_coating_config_id') \
if 'x_fc_coating_config_id' in so.order_line._fields else False
specs = so.order_line.mapped('x_fc_customer_spec_id') \
if 'x_fc_customer_spec_id' in so.order_line._fields else False
points = Point._find_matching(
trigger='so_confirmed', partner=partner,
)
for point in points:
# Filter by part / coating / spec intersection if the
# point cares.
# Filter by part / spec intersection if the point cares.
if point.part_catalog_ids and parts and \
not (point.part_catalog_ids & parts):
continue
if point.coating_config_ids and coatings and \
not (point.coating_config_ids & coatings):
continue
if point.customer_spec_ids and specs and \
not (point.customer_spec_ids & specs):
continue
@@ -85,12 +79,11 @@ class FpJobPointHook(models.Model):
for job in self:
partner = job.partner_id
part = getattr(job, 'part_catalog_id', False) or False
coating = getattr(job, 'coating_config_id', False) or False
customer_spec = getattr(job, 'customer_spec_id', False) or False
recipe = getattr(job, 'recipe_id', False) or False
points = Point._find_matching(
trigger='job_confirmed', partner=partner,
part=part or None, coating=coating or None,
part=part or None,
customer_spec=customer_spec or None,
recipe=recipe or None,
)
@@ -108,12 +101,11 @@ class FpJobPointHook(models.Model):
continue
partner = job.partner_id
part = getattr(job, 'part_catalog_id', False) or False
coating = getattr(job, 'coating_config_id', False) or False
customer_spec = getattr(job, 'customer_spec_id', False) or False
recipe = getattr(job, 'recipe_id', False) or False
points = Point._find_matching(
trigger='job_done', partner=partner,
part=part or None, coating=coating or None,
part=part or None,
customer_spec=customer_spec or None,
recipe=recipe or None,
)
@@ -137,12 +129,11 @@ class FpJobStepPointHook(models.Model):
job = step.job_id
partner = job.partner_id if job else False
part = getattr(job, 'part_catalog_id', False) or False
coating = getattr(job, 'coating_config_id', False) or False
customer_spec = getattr(job, 'customer_spec_id', False) or False
recipe = getattr(job, 'recipe_id', False) or False
points = Point._find_matching(
trigger='job_step_done', partner=partner,
part=part or None, coating=coating or None, step=step,
part=part or None, step=step,
customer_spec=customer_spec or None,
recipe=recipe or None,
)

View File

@@ -24,19 +24,9 @@ class FpQuoteConfigurator(models.Model):
"""Extend the configurator's matcher to consider Spec + Recipe.
Spec match adds +8 (highest priority — explicit customer spec
wins over chemistry / cert-level filters). Recipe adds +6.
Falls through to the existing coating / material / cert scoring.
wins over chemistry filters). Recipe adds +6. Material is +2.
"""
# Cache the recipe before super (super may overwrite via thickness
# logic in some inherit chains).
recipe = (
self.coating_config_id.recipe_id
if self.coating_config_id and self.coating_config_id.recipe_id
else False
)
# Build the candidate rule set the same way super does — but
# since super uses a private mechanism we re-implement to keep
# the spec/recipe scoring inline with the rest.
recipe = self.recipe_id or False
builder_rules = (
recipe.pricing_rule_ids
if recipe else self.env['fp.pricing.rule']
@@ -49,38 +39,25 @@ class FpQuoteConfigurator(models.Model):
rules = self.env['fp.pricing.rule'].search(
[('active', '=', True)], order='sequence, id'
)
cert_level = (
self.coating_config_id.certification_level
if self.coating_config_id else False
)
best = None
best_score = -1
for rule in rules:
score = 0
# NEW — spec wins biggest
# Spec wins biggest
if rule.customer_spec_id:
if rule.customer_spec_id != self.customer_spec_id:
continue
score += 8
# NEW — recipe is next
# Recipe is next
if rule.recipe_id:
if rule.recipe_id != recipe:
continue
score += 6
# Legacy — coating / material / cert
if rule.coating_config_id:
if rule.coating_config_id != self.coating_config_id:
continue
score += 4
if rule.substrate_material:
if rule.substrate_material != self.substrate_material:
continue
score += 2
if rule.certification_level:
if rule.certification_level != cert_level:
continue
score += 1
if score > best_score:
best_score = score
best = rule