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)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Native Jobs',
|
'name': 'Fusion Plating — Native Jobs',
|
||||||
'version': '19.0.11.0.0',
|
'version': '19.0.11.1.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.',
|
||||||
|
|||||||
@@ -585,38 +585,90 @@ class FpJob(models.Model):
|
|||||||
def _resolve_required_cert_types(self):
|
def _resolve_required_cert_types(self):
|
||||||
"""Set of cert types this job must produce.
|
"""Set of cert types this job must produce.
|
||||||
|
|
||||||
Priority: part.certificate_requirement wins; 'inherit' falls
|
Three-step resolution (spec 2026-05-27 — see
|
||||||
back to partner-level send_coc / send_thickness_report flags.
|
docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md):
|
||||||
'none' returns empty (commercial customer, no paperwork).
|
|
||||||
Unknown requirement codes default to {'coc'} as a safety net.
|
|
||||||
|
|
||||||
Bundling rule (2026-05-18 — Entech workflow): when a CoC is
|
Step 1 — Start from partner + part flags. The existing logic,
|
||||||
wanted AND thickness is wanted, the thickness data is delivered
|
extended to read 3 new orphan-type partner toggles
|
||||||
as page 2 of the CoC PDF (see _fp_merge_thickness_into_pdf),
|
(Nadcap / Mill Test / Customer Specific).
|
||||||
so we return ONE cert ({'coc'}) instead of two. A standalone
|
|
||||||
thickness_report cert is only produced when thickness is wanted
|
Step 2 — Apply recipe suppression. Recipe-level requires_*
|
||||||
WITHOUT a CoC — a rare edge case kept for completeness.
|
Booleans on fusion.plating.process.node REMOVE cert
|
||||||
Action_issue's thickness-data gate enforces actual readings or
|
types from the set but never add them. This is the
|
||||||
a Fischerscope PDF on the merged CoC.
|
"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()
|
self.ensure_one()
|
||||||
|
# ---- Step 1 — partner + part baseline ----
|
||||||
req = (
|
req = (
|
||||||
self.part_catalog_id
|
self.part_catalog_id
|
||||||
and self.part_catalog_id.certificate_requirement
|
and self.part_catalog_id.certificate_requirement
|
||||||
) or 'inherit'
|
) or 'inherit'
|
||||||
if req == 'inherit':
|
if req == 'inherit':
|
||||||
want_coc = bool(self.partner_id.x_fc_send_coc)
|
wanted = set()
|
||||||
want_thickness = bool(self.partner_id.x_fc_send_thickness_report)
|
p = self.partner_id
|
||||||
if want_coc:
|
if p:
|
||||||
return {'coc'} # thickness gets merged in
|
if p.x_fc_send_coc:
|
||||||
if want_thickness:
|
wanted.add('coc')
|
||||||
return {'thickness_report'}
|
if p.x_fc_send_thickness_report:
|
||||||
return set()
|
wanted.add('thickness_report')
|
||||||
return {
|
# Three aerospace/defence partner toggles. Field guards
|
||||||
'none': set(),
|
# let this module load even if fusion_plating_certificates
|
||||||
'coc': {'coc'},
|
# is at an older version that pre-dates the new fields.
|
||||||
'coc_thickness': {'coc'}, # bundled — thickness on page 2
|
if ('x_fc_send_nadcap_cert' in p._fields
|
||||||
}.get(req, {'coc'})
|
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(
|
next_milestone_action = fields.Selection(
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in New Issue
Block a user