diff --git a/fusion_plating/fusion_plating_jobs/__manifest__.py b/fusion_plating/fusion_plating_jobs/__manifest__.py index b1ad4661..20aae879 100644 --- a/fusion_plating/fusion_plating_jobs/__manifest__.py +++ b/fusion_plating/fusion_plating_jobs/__manifest__.py @@ -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.', diff --git a/fusion_plating/fusion_plating_jobs/models/fp_job.py b/fusion_plating/fusion_plating_jobs/models/fp_job.py index b59d365e..b8534ad5 100644 --- a/fusion_plating/fusion_plating_jobs/models/fp_job.py +++ b/fusion_plating/fusion_plating_jobs/models/fp_job.py @@ -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( [