fix(certificates): honour recipe thickness suppression at cert issue
The recipe-cert-toggles feature (fb6cccc8) taught
fp.job._resolve_required_cert_types to suppress thickness for recipes
with requires_thickness_report=False (passivation, chemical conversion,
anodize seal-only). But the actual thickness-data ENFORCEMENT never got
the memo: both fp.certificate.action_issue's hard gate AND the
Issue-Certs wizard's readiness hint re-derived 'needs thickness' from
partner flags only and ignored the recipe. Result: a passivation CoC for
a thickness/strict customer could never be issued — the gate demanded
Fischerscope data the process physically cannot produce.
Consolidate the partner-flag + recipe-suppression logic into one
fp.certificate._fp_needs_thickness_data() helper and route both the gate
and the wizard through it, so the cert-type resolver and the issue-time
gate can never drift again. Add regression tests: passivation recipe
suppresses the issue gate even for strict-thickness customers; a normal
recipe still enforces (control, guards aerospace).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Certificates',
|
'name': 'Fusion Plating — Certificates',
|
||||||
'version': '19.0.9.3.0',
|
'version': '19.0.9.4.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Certificate registry for CoC, thickness reports, and quality documents.',
|
'summary': 'Certificate registry for CoC, thickness reports, and quality documents.',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -436,6 +436,47 @@ class FpCertificate(models.Model):
|
|||||||
rec.invalidate_recordset(['name'])
|
rec.invalidate_recordset(['name'])
|
||||||
return records
|
return records
|
||||||
|
|
||||||
|
def _fp_needs_thickness_data(self):
|
||||||
|
"""True when this cert MUST carry thickness data to be issued.
|
||||||
|
|
||||||
|
Single source of truth shared by action_issue (hard gate) and the
|
||||||
|
Issue-Certs wizard (readiness hint) so the two can never drift.
|
||||||
|
|
||||||
|
Partner side — the ceiling: a CoC needs thickness when the customer
|
||||||
|
is strict-thickness (aerospace / Nadcap) or opts into the
|
||||||
|
thickness-on-CoC bundle; a thickness_report cert always needs it.
|
||||||
|
|
||||||
|
Recipe side — suppress-only: a recipe whose requires_thickness_report
|
||||||
|
is False (passivation, chemical conversion, anodize seal-only — no
|
||||||
|
plating thickness physically exists) REMOVES the requirement even
|
||||||
|
when the customer asked. This mirrors Step 2 of
|
||||||
|
fp.job._resolve_required_cert_types so the cert-type resolver and
|
||||||
|
this issue-time gate agree. Without it, a passivation CoC for a
|
||||||
|
thickness customer can never be issued (the gate demands Fischerscope
|
||||||
|
data the process cannot produce). Field-existence guards keep this
|
||||||
|
safe when fusion_plating_jobs / fusion_plating are at an older
|
||||||
|
schema or not installed.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
partner = self.partner_id
|
||||||
|
needs = (
|
||||||
|
self.certificate_type == 'thickness_report'
|
||||||
|
or (self.certificate_type == 'coc' and partner and (
|
||||||
|
('x_fc_strict_thickness_required' in partner._fields
|
||||||
|
and partner.x_fc_strict_thickness_required)
|
||||||
|
or ('x_fc_send_thickness_report' in partner._fields
|
||||||
|
and partner.x_fc_send_thickness_report)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
if not needs:
|
||||||
|
return False
|
||||||
|
job = self.x_fc_job_id if 'x_fc_job_id' in self._fields else False
|
||||||
|
recipe = job.recipe_id if job else False
|
||||||
|
if (recipe and 'requires_thickness_report' in recipe._fields
|
||||||
|
and not recipe.requires_thickness_report):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
# ----- State actions ----------------------------------------------------
|
# ----- State actions ----------------------------------------------------
|
||||||
def action_issue(self):
|
def action_issue(self):
|
||||||
# ===== ACL guard (spec 2026-05-25 §ACL changes) ===============
|
# ===== ACL guard (spec 2026-05-25 §ACL changes) ===============
|
||||||
@@ -580,30 +621,18 @@ class FpCertificate(models.Model):
|
|||||||
'type': type_label,
|
'type': type_label,
|
||||||
'name': rec.name or rec.display_name,
|
'name': rec.name or rec.display_name,
|
||||||
})
|
})
|
||||||
# Thickness data requirement — unified gate covering both
|
# Thickness data requirement — _fp_needs_thickness_data is the
|
||||||
# cert types. A customer needs thickness data on the cert
|
# single source of truth (shared with the Issue-Certs wizard).
|
||||||
# when ANY of these is true:
|
# A customer needs thickness data when the cert is a
|
||||||
# 1. cert type is thickness_report (the cert IS the data)
|
# thickness_report, or it's a CoC and the partner is
|
||||||
# 2. partner.x_fc_strict_thickness_required (aerospace /
|
# strict-thickness / opts into the thickness-on-CoC bundle —
|
||||||
# Nadcap — always strict)
|
# UNLESS the job's recipe suppresses thickness
|
||||||
# 3. partner.x_fc_send_thickness_report (the bundling
|
# (requires_thickness_report=False: passivation, chemical
|
||||||
# rule — CoC carries thickness as page 2 by default
|
# conversion, anodize seal-only — no plating thickness exists).
|
||||||
# for these customers; see CLAUDE.md "CoC + thickness
|
# Acceptable data: logged readings on the cert OR a Fischerscope
|
||||||
# = ONE cert (page 2 merge)")
|
# PDF on the linked QC OR a cert-local Fischerscope upload.
|
||||||
# Acceptable data: logged readings on the cert OR a
|
|
||||||
# Fischerscope PDF on the linked QC OR a cert-local
|
|
||||||
# Fischerscope upload. Any one is enough.
|
|
||||||
partner = rec.partner_id
|
partner = rec.partner_id
|
||||||
needs_thickness = (
|
if rec._fp_needs_thickness_data():
|
||||||
rec.certificate_type == 'thickness_report'
|
|
||||||
or (rec.certificate_type == 'coc' and partner and (
|
|
||||||
('x_fc_strict_thickness_required' in partner._fields
|
|
||||||
and partner.x_fc_strict_thickness_required)
|
|
||||||
or ('x_fc_send_thickness_report' in partner._fields
|
|
||||||
and partner.x_fc_send_thickness_report)
|
|
||||||
))
|
|
||||||
)
|
|
||||||
if needs_thickness:
|
|
||||||
has_readings = bool(rec.thickness_reading_ids)
|
has_readings = bool(rec.thickness_reading_ids)
|
||||||
has_qc_fischer_pdf = bool(
|
has_qc_fischer_pdf = bool(
|
||||||
rec.x_fc_thickness_pdf_id
|
rec.x_fc_thickness_pdf_id
|
||||||
|
|||||||
@@ -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.12.1.6',
|
'version': '19.0.12.1.7',
|
||||||
'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.',
|
||||||
|
|||||||
@@ -141,3 +141,48 @@ class TestRecipeCertSuppression(TransactionCase):
|
|||||||
cert.with_context(
|
cert.with_context(
|
||||||
fp_skip_cert_authority_gate=True
|
fp_skip_cert_authority_gate=True
|
||||||
).action_issue()
|
).action_issue()
|
||||||
|
|
||||||
|
# ---- Test 7: passivation recipe also suppresses the ISSUE-TIME gate ----
|
||||||
|
def test_passivation_recipe_suppresses_thickness_issue_gate(self):
|
||||||
|
"""A passivation recipe (requires_thickness_report=False) must drop
|
||||||
|
the thickness-data requirement at issue time too — even for a
|
||||||
|
strict-thickness customer. Regression: the recipe-suppression
|
||||||
|
feature updated _resolve_required_cert_types but NOT the
|
||||||
|
action_issue / wizard thickness gate, so passivation CoCs could
|
||||||
|
never be issued (gate demanded Fischerscope data the process
|
||||||
|
cannot produce)."""
|
||||||
|
self.partner.x_fc_send_coc = True
|
||||||
|
self.partner.x_fc_send_thickness_report = True
|
||||||
|
self.partner.x_fc_strict_thickness_required = True
|
||||||
|
self.recipe.requires_thickness_report = False
|
||||||
|
part = self._make_part()
|
||||||
|
job = self._make_job(part_catalog_id=part.id)
|
||||||
|
cert = self.env['fp.certificate'].create({
|
||||||
|
'name': 'TEST-COC-PASSIVATION',
|
||||||
|
'certificate_type': 'coc',
|
||||||
|
'state': 'draft',
|
||||||
|
'partner_id': self.partner.id,
|
||||||
|
'x_fc_job_id': job.id,
|
||||||
|
})
|
||||||
|
# Recipe suppresses thickness -> the shared gate must NOT demand it.
|
||||||
|
self.assertFalse(cert._fp_needs_thickness_data())
|
||||||
|
|
||||||
|
# ---- Test 8: normal recipe still enforces thickness (control) ----
|
||||||
|
def test_normal_recipe_keeps_thickness_issue_gate(self):
|
||||||
|
"""Control for Test 7: a recipe that allows thickness
|
||||||
|
(requires_thickness_report default True) still demands thickness
|
||||||
|
data on the CoC for a thickness customer. Guards against
|
||||||
|
over-suppression weakening real aerospace enforcement."""
|
||||||
|
self.partner.x_fc_send_coc = True
|
||||||
|
self.partner.x_fc_send_thickness_report = True
|
||||||
|
# self.recipe.requires_thickness_report stays default True.
|
||||||
|
part = self._make_part()
|
||||||
|
job = self._make_job(part_catalog_id=part.id)
|
||||||
|
cert = self.env['fp.certificate'].create({
|
||||||
|
'name': 'TEST-COC-NORMAL',
|
||||||
|
'certificate_type': 'coc',
|
||||||
|
'state': 'draft',
|
||||||
|
'partner_id': self.partner.id,
|
||||||
|
'x_fc_job_id': job.id,
|
||||||
|
})
|
||||||
|
self.assertTrue(cert._fp_needs_thickness_data())
|
||||||
|
|||||||
@@ -461,17 +461,18 @@ class FpCertIssueWizardLine(models.TransientModel):
|
|||||||
|
|
||||||
@api.depends('cert_id.certificate_type',
|
@api.depends('cert_id.certificate_type',
|
||||||
'cert_id.partner_id.x_fc_send_thickness_report',
|
'cert_id.partner_id.x_fc_send_thickness_report',
|
||||||
'cert_id.partner_id.x_fc_strict_thickness_required')
|
'cert_id.partner_id.x_fc_strict_thickness_required',
|
||||||
|
'cert_id.x_fc_job_id.recipe_id.requires_thickness_report')
|
||||||
def _compute_needs_thickness(self):
|
def _compute_needs_thickness(self):
|
||||||
|
# Delegate to fp.certificate._fp_needs_thickness_data — the single
|
||||||
|
# source of truth shared with the action_issue hard gate — so the
|
||||||
|
# wizard's readiness hint and the gate can never drift. Honours
|
||||||
|
# recipe-level thickness suppression (passivation = no thickness
|
||||||
|
# even if the customer asked).
|
||||||
for ln in self:
|
for ln in self:
|
||||||
cert = ln.cert_id
|
|
||||||
partner = cert.partner_id
|
|
||||||
ln.needs_thickness = (
|
ln.needs_thickness = (
|
||||||
cert.certificate_type == 'thickness_report'
|
ln.cert_id._fp_needs_thickness_data()
|
||||||
or (cert.certificate_type == 'coc' and partner and (
|
if ln.cert_id else False
|
||||||
partner.x_fc_strict_thickness_required
|
|
||||||
or partner.x_fc_send_thickness_report
|
|
||||||
))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@api.depends('needs_thickness', 'fischer_file', 'reading_line_ids',
|
@api.depends('needs_thickness', 'fischer_file', 'reading_line_ids',
|
||||||
|
|||||||
Reference in New Issue
Block a user