Files
Odoo-Modules/fusion_plating/fusion_plating_quality/models/fp_quality_point.py
gsinghpal d891002c84 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>
2026-05-15 02:00:41 -04:00

219 lines
7.9 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
#
# Sub 12 Phase C — trigger-based quality points.
#
# Replaces the old "customer.x_fc_requires_qc + customer.x_fc_qc_template_id"
# direct binding. Now an admin defines fp.quality.point rules with filters
# (partner / part / coating / step kind) and a trigger event; matching
# records spawn fusion.plating.quality.check rows from the chosen template.
import logging
from odoo import _, api, fields, models
_logger = logging.getLogger(__name__)
TRIGGER_TYPES = [
('manual', 'Manual'),
('so_confirmed', 'Sale Order Confirmed'),
('receiving_done', 'Receiving Closed'),
('job_confirmed', 'Job Confirmed'),
('job_step_done', 'Job Step Finished'),
('job_done', 'Job Completed'),
]
STEP_KINDS = [
('wet', 'Wet Process'),
('bake', 'Bake / Cure'),
('inspect', 'Inspection'),
('mask', 'Masking'),
('post', 'Post-Treatment'),
('other', 'Other'),
]
class FpQualityPoint(models.Model):
_name = 'fp.quality.point'
_description = 'Fusion Plating — Quality Point'
_inherit = ['mail.thread']
_order = 'sequence, name'
name = fields.Char(required=True, translate=True, tracking=True)
sequence = fields.Integer(default=10)
active = fields.Boolean(default=True, tracking=True)
description = fields.Text(translate=True)
trigger_type = fields.Selection(
TRIGGER_TYPES, string='Trigger', required=True,
default='job_confirmed', tracking=True,
help='When this point fires. "manual" never auto-fires.',
)
# ----- Filters (all optional; empty == match all) -----
partner_ids = fields.Many2many(
'res.partner', 'fp_quality_point_partner_rel',
'point_id', 'partner_id', string='Customers',
)
part_catalog_ids = fields.Many2many(
'fp.part.catalog', 'fp_quality_point_part_rel',
'point_id', 'part_id', string='Parts',
)
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')
template_id = fields.Many2one(
'fp.qc.checklist.template', string='Checklist Template',
required=True, ondelete='restrict',
)
assignee_user_id = fields.Many2one(
'res.users', string='Default Inspector',
help='If set, the auto-spawned QC check is pre-assigned here.',
)
team_id = fields.Many2one('fp.quality.team', string='Quality Team')
tag_ids = fields.Many2many(
'fp.quality.tag', 'fp_quality_point_tag_rel',
'point_id', 'tag_id', string='Tags',
)
# Stats
spawn_count = fields.Integer(
string='Checks Spawned', compute='_compute_spawn_count',
)
@api.depends('template_id')
def _compute_spawn_count(self):
Check = self.env['fusion.plating.quality.check']
for rec in self:
if not rec.template_id:
rec.spawn_count = 0
continue
rec.spawn_count = Check.search_count([
('template_id', '=', rec.template_id.id),
])
# ------------------------------------------------------------------
# Matching + spawning
# ------------------------------------------------------------------
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.
"""
self.ensure_one()
if self.partner_ids and (not partner or partner not in self.partner_ids):
return False
if self.part_catalog_ids and (
not part or part not in self.part_catalog_ids):
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) \
and step.kind != self.step_kind:
return False
return True
@api.model
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([
('active', '=', True),
('trigger_type', '=', trigger),
])
return candidates.filtered(lambda p: p._matches(
partner=partner, part=part, step=step,
customer_spec=customer_spec, recipe=recipe,
))
def _spawn_check_for(self, source, partner=None, job=None, step=None):
"""Create a fusion.plating.quality.check from this point's template.
Idempotent per (point, source): if a check already exists with the
same template_id and the same job/step binding, no new one is
created (returns the existing one).
"""
self.ensure_one()
Check = self.env['fusion.plating.quality.check']
if not self.template_id:
_logger.warning(
'fp.quality.point %s: no template_id set, skipping spawn.',
self.name,
)
return False
domain = [('template_id', '=', self.template_id.id)]
if job:
domain.append(('job_id', '=', job.id))
if step and 'step_id' in Check._fields:
domain.append(('step_id', '=', step.id))
existing = Check.search(domain, limit=1)
if existing:
return existing
vals = {
'template_id': self.template_id.id,
}
# Best-effort field bindings — survives schema variations.
if 'partner_id' in Check._fields and partner:
vals['partner_id'] = partner.id
if 'job_id' in Check._fields and job:
vals['job_id'] = job.id
if 'step_id' in Check._fields and step:
vals['step_id'] = step.id
if 'state' in Check._fields:
vals['state'] = 'pending'
if 'inspector_id' in Check._fields and self.assignee_user_id:
vals['inspector_id'] = self.assignee_user_id.id
if 'team_id' in Check._fields and self.team_id:
vals['team_id'] = self.team_id.id
if 'tag_ids' in Check._fields and self.tag_ids:
vals['tag_ids'] = [(6, 0, self.tag_ids.ids)]
try:
return Check.create(vals)
except Exception as e:
_logger.warning(
'fp.quality.point %s: spawn failed for %s%s',
self.name, source.display_name if source else '?', e,
)
return False
def action_spawn_manual(self):
"""Manual fire — present from the form view button. No source ctx."""
for rec in self:
rec._spawn_check_for(source=rec)
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Quality Point fired'),
'message': _('Spawned %s check(s).') % len(self),
'type': 'success',
},
}