feat(plating): CoC spec-optional + SO-style header + thickness for any cert

- Drop the hard spec_reference gate on fp.certificate.action_issue. The
  customer-facing description (_fp_resolve_customer_facing_description,
  walks job -> SO line, reuses fp_customer_description) now drives the CoC
  Process column; spec_reference prints only when an estimator fills it.
- CoC EN/FR reports swap web.external_layout for fp_external_layout_clean +
  paperformat_fp_a4_portrait. New shared coc_header (company logo + address
  left, Nadcap logo centre, title + Code128 barcode right) mirrors the Sale
  Order header. Removed the 3-logo Nadcap/AS9100/CGP accreditation strip and
  the body H1s; padding-top 0 on both body wrappers.
- Un-gate the Issue Certs wizard thickness upload (was invisible unless the
  customer was thickness-flagged) so a Fischerscope report can be attached to
  ANY cert; merge (page 2) + inline readings already render unconditionally.
- Update issue-gate tests, bump versions (certificates 19.0.9.1.0,
  reports 19.0.11.27.0, jobs 19.0.11.2.0), record CLAUDE.md rule 14c.

Deployed + render-verified on entech (CoC-30065, 223KB PDF, no QWeb errors).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-28 21:17:09 -04:00
parent fecd2415f6
commit 307afbf3c0
10 changed files with 209 additions and 110 deletions

View File

@@ -496,16 +496,13 @@ class FpCertificate(models.Model):
and 'x_fc_owner_user_id' in rec.company_id._fields
and rec.company_id.x_fc_owner_user_id):
rec.certified_by_id = rec.company_id.x_fc_owner_user_id
# Spec reference is what the cert ATTESTS — without it the
# cert is just a piece of paper. AS9100 / Nadcap require
# naming the spec the work was performed to.
if not rec.spec_reference:
raise UserError(_(
'Cannot issue certificate "%(name)s" — no Spec '
'Reference set.\n\nFill the Spec Reference field '
'(e.g. "AMS 2404", "MIL-C-26074") so the cert '
'states which standard the work meets.'
) % {'name': rec.name or rec.display_name})
# Spec Reference is OPTIONAL (client request 2026-05-28).
# The customer-facing description now serves as the cert's
# spec / certificate information (see
# _fp_resolve_customer_facing_description + the CoC Process
# column). spec_reference still prints below the description
# when an estimator chooses to fill it, but it no longer
# blocks issuance.
# Process description (what was done to the parts). Without
# it the cert PDF just shows blank process text — customer
# has no idea what they paid for. Auto-filled from the
@@ -733,6 +730,53 @@ class FpCertificate(models.Model):
return
delivery.coc_attachment_id = self.attachment_id.id
def _fp_resolve_customer_facing_description(self):
"""Resolve the customer-facing description used as the cert's
spec / certificate information on the printed CoC.
Client request 2026-05-28: Spec Reference is no longer
mandatory; the customer-facing description (what the estimator
actually typed on the order) now carries the descriptive text.
Resolution — first non-empty wins:
1. The order line matching this cert's job/part — cleaned via
sale.order.line.fp_customer_description() (strips the
"[code] product" prefix Odoo re-prepends).
2. Any product line on the linked sale order.
Returns '' when no order context exists; the report template
then falls back to process_description. All cross-module field
access is guarded so the method stays safe even if the jobs /
configurator layers aren't installed.
"""
self.ensure_one()
job = self.x_fc_job_id if 'x_fc_job_id' in self._fields else False
so = self.sale_order_id or (
job.sale_order_id
if job and 'sale_order_id' in job._fields else False
)
if not so:
return ''
lines = so.order_line.filtered(lambda l: not l.display_type)
if not lines:
return ''
# Prefer the line whose part matches this cert's job.
part = (job.part_catalog_id
if job and 'part_catalog_id' in job._fields else False)
line = self.env['sale.order.line']
if part and 'x_fc_part_catalog_id' in lines._fields:
line = lines.filtered(
lambda l: l.x_fc_part_catalog_id == part
)[:1]
if not line:
line = lines[:1]
if not line:
return ''
if hasattr(line, 'fp_customer_description'):
desc = line.fp_customer_description()
else:
desc = line.name
return (desc or '').strip()
def _fp_render_and_attach_pdf(self):
"""Render the CoC PDF via the bound report action, OPTIONALLY
merge the Fischerscope thickness report PDF (uploaded by the