feat(jobs): three-step cert resolver with recipe suppression
Rewrites fp.job._resolve_required_cert_types as a documented three-step
pipeline:
Step 1 — partner + part flags (extended to read 3 new orphan-type
partner toggles: x_fc_send_nadcap_cert / x_fc_send_mill_test
/ x_fc_send_customer_specific)
Step 2 — recipe-level requires_* Booleans STRIP cert types from
the wanted set (suppress-only — never adds)
Step 3 — CoC + thickness bundling preserved (thickness collapses
into CoC PDF as page 2)
Field-existence guards on partner/recipe attribute reads keep the
resolver robust if the certificates / plating module schemas drift.
Recipe is suppress-only per Q1 locked decision: customer/part is the
ceiling, recipe can only remove. Test 3 (test_recipe_cannot_add_certs_
customer_didnt_want) is the explicit regression guard.
Sub: docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md
Task: T4. Makes the 5 resolver tests from T3 pass.
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)
|
||||
{
|
||||
'name': 'Fusion Plating — Native Jobs',
|
||||
'version': '19.0.11.0.0',
|
||||
'version': '19.0.11.1.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
||||
'author': 'Nexa Systems Inc.',
|
||||
|
||||
@@ -585,38 +585,90 @@ class FpJob(models.Model):
|
||||
def _resolve_required_cert_types(self):
|
||||
"""Set of cert types this job must produce.
|
||||
|
||||
Priority: part.certificate_requirement wins; 'inherit' falls
|
||||
back to partner-level send_coc / send_thickness_report flags.
|
||||
'none' returns empty (commercial customer, no paperwork).
|
||||
Unknown requirement codes default to {'coc'} as a safety net.
|
||||
Three-step resolution (spec 2026-05-27 — see
|
||||
docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md):
|
||||
|
||||
Bundling rule (2026-05-18 — Entech workflow): when a CoC is
|
||||
wanted AND thickness is wanted, the thickness data is delivered
|
||||
as page 2 of the CoC PDF (see _fp_merge_thickness_into_pdf),
|
||||
so we return ONE cert ({'coc'}) instead of two. A standalone
|
||||
thickness_report cert is only produced when thickness is wanted
|
||||
WITHOUT a CoC — a rare edge case kept for completeness.
|
||||
Action_issue's thickness-data gate enforces actual readings or
|
||||
a Fischerscope PDF on the merged CoC.
|
||||
Step 1 — Start from partner + part flags. The existing logic,
|
||||
extended to read 3 new orphan-type partner toggles
|
||||
(Nadcap / Mill Test / Customer Specific).
|
||||
|
||||
Step 2 — Apply recipe suppression. Recipe-level requires_*
|
||||
Booleans on fusion.plating.process.node REMOVE cert
|
||||
types from the set but never add them. This is the
|
||||
"passivation = no thickness even if customer asked"
|
||||
case. Locked decision Q1: recipe suppresses only.
|
||||
|
||||
Step 3 — Bundling rule preserved. When CoC AND thickness are
|
||||
both in the set, thickness collapses into the CoC PDF
|
||||
as page 2 (see _fp_merge_thickness_into_pdf). The
|
||||
returned set holds just {'coc'} in that case.
|
||||
|
||||
Field-existence guards on partner / recipe attribute reads
|
||||
defend against installs where fusion_plating_certificates or
|
||||
fusion_plating's latest schema bump hasn't landed yet —
|
||||
matches the defensive pattern used elsewhere in this file.
|
||||
"""
|
||||
self.ensure_one()
|
||||
# ---- Step 1 — partner + part baseline ----
|
||||
req = (
|
||||
self.part_catalog_id
|
||||
and self.part_catalog_id.certificate_requirement
|
||||
) or 'inherit'
|
||||
if req == 'inherit':
|
||||
want_coc = bool(self.partner_id.x_fc_send_coc)
|
||||
want_thickness = bool(self.partner_id.x_fc_send_thickness_report)
|
||||
if want_coc:
|
||||
return {'coc'} # thickness gets merged in
|
||||
if want_thickness:
|
||||
return {'thickness_report'}
|
||||
return set()
|
||||
return {
|
||||
'none': set(),
|
||||
'coc': {'coc'},
|
||||
'coc_thickness': {'coc'}, # bundled — thickness on page 2
|
||||
}.get(req, {'coc'})
|
||||
wanted = set()
|
||||
p = self.partner_id
|
||||
if p:
|
||||
if p.x_fc_send_coc:
|
||||
wanted.add('coc')
|
||||
if p.x_fc_send_thickness_report:
|
||||
wanted.add('thickness_report')
|
||||
# Three aerospace/defence partner toggles. Field guards
|
||||
# let this module load even if fusion_plating_certificates
|
||||
# is at an older version that pre-dates the new fields.
|
||||
if ('x_fc_send_nadcap_cert' in p._fields
|
||||
and p.x_fc_send_nadcap_cert):
|
||||
wanted.add('nadcap_cert')
|
||||
if ('x_fc_send_mill_test' in p._fields
|
||||
and p.x_fc_send_mill_test):
|
||||
wanted.add('mill_test')
|
||||
if ('x_fc_send_customer_specific' in p._fields
|
||||
and p.x_fc_send_customer_specific):
|
||||
wanted.add('customer_specific')
|
||||
else:
|
||||
wanted = {
|
||||
'none': set(),
|
||||
'coc': {'coc'},
|
||||
'coc_thickness': {'coc', 'thickness_report'},
|
||||
}.get(req, {'coc'})
|
||||
|
||||
# ---- Step 2 — recipe suppression (suppress-only) ----
|
||||
recipe = self.recipe_id
|
||||
if recipe:
|
||||
if ('requires_coc' in recipe._fields
|
||||
and not recipe.requires_coc):
|
||||
wanted.discard('coc')
|
||||
if ('requires_thickness_report' in recipe._fields
|
||||
and not recipe.requires_thickness_report):
|
||||
wanted.discard('thickness_report')
|
||||
if ('requires_nadcap_cert' in recipe._fields
|
||||
and not recipe.requires_nadcap_cert):
|
||||
wanted.discard('nadcap_cert')
|
||||
if ('requires_mill_test' in recipe._fields
|
||||
and not recipe.requires_mill_test):
|
||||
wanted.discard('mill_test')
|
||||
if ('requires_customer_specific' in recipe._fields
|
||||
and not recipe.requires_customer_specific):
|
||||
wanted.discard('customer_specific')
|
||||
|
||||
# ---- Step 3 — CoC + thickness bundling ----
|
||||
# Thickness data is merged as page 2 of the CoC PDF by
|
||||
# _fp_merge_thickness_into_pdf, so we return ONE cert
|
||||
# instead of two. action_issue's thickness-data gate enforces
|
||||
# actual readings or a Fischerscope PDF on the merged CoC.
|
||||
if 'coc' in wanted and 'thickness_report' in wanted:
|
||||
wanted.discard('thickness_report')
|
||||
|
||||
return wanted
|
||||
|
||||
next_milestone_action = fields.Selection(
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user