feat(plating): merge Fischerscope PDF into CoC as page 2+
When a QC uploaded the XDAL 600 report, the CoC PDF render pipeline now appends the Fischerscope PDF directly after the cert pages. This matches what aerospace / Nadcap auditors expect (and how Steelhead ships certs today) — a single PDF file carrying both the certificate declaration and the raw equipment report. Flow: * _fp_generate_cert_pdf renders the CoC via QWeb as before * _fp_merge_thickness_into_cert resolves the QC for the MO (preferring the passed one) and extracts its thickness_report_pdf_id bytes * PyPDF2.PdfMerger concatenates CoC then Fischerscope into a single PDF * Merged bytes replace pdf_content before the ir.attachment is written * Falls back to CoC-only (and logs a warning) if PyPDF2 is missing or either PDF fails to parse — never blocks MO completion Smoke test: synthetic Fischerscope + real QWeb CoC → 2-page merged PDF with page 1 CoC text and page 2 Fischerscope text, verified via PyPDF2 extract_text. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1233,6 +1233,15 @@ class MrpProduction(models.Model):
|
||||
force_report_rendering=True,
|
||||
)._render_qweb_pdf(report.report_name, [cert.id])
|
||||
|
||||
# Append the Fischerscope / XDAL 600 PDF as page 2+ of the CoC
|
||||
# when a QC uploaded one. Aerospace / Nadcap customers need the
|
||||
# raw equipment report in the same PDF as the cert — this is
|
||||
# how Steelhead does it and what auditors expect.
|
||||
if cert.certificate_type == 'coc':
|
||||
merged = self._fp_merge_thickness_into_cert(cert, pdf_content)
|
||||
if merged:
|
||||
pdf_content = merged
|
||||
|
||||
# Filename: CoC-<CustomerSlug>-<CertName>.pdf so the email
|
||||
# attachment doesn't just say CERT-00123.pdf to the customer.
|
||||
cust_name = cert.partner_id.name if cert.partner_id else ''
|
||||
@@ -1258,3 +1267,86 @@ class MrpProduction(models.Model):
|
||||
job.coc_attachment_id = att.id
|
||||
if delivery and not delivery.coc_attachment_id:
|
||||
delivery.coc_attachment_id = att.id
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# CoC + Fischerscope PDF merge
|
||||
# ------------------------------------------------------------------
|
||||
def _fp_merge_thickness_into_cert(self, cert, coc_pdf_bytes):
|
||||
"""Return a merged PDF: CoC first, Fischerscope report appended.
|
||||
|
||||
Returns None (so caller falls back to CoC-only) when:
|
||||
- no QC on the MO has a thickness_report_pdf_id, or
|
||||
- the Fischerscope attachment is missing / empty, or
|
||||
- PyPDF2 is not installed, or
|
||||
- either PDF fails to parse (corrupt / encrypted upload).
|
||||
|
||||
The uploaded PDF is treated as opaque — we don't try to normalise
|
||||
page size or re-render. WinFTM exports are US-letter portrait,
|
||||
which matches our CoC template. Mismatches will just show two
|
||||
sizes in a PDF reader, which is fine.
|
||||
"""
|
||||
import io
|
||||
import base64 as _b64
|
||||
if not cert or cert.certificate_type != 'coc':
|
||||
return None
|
||||
mo = cert.production_id
|
||||
if not mo:
|
||||
return None
|
||||
|
||||
QC = self.env.get('fusion.plating.quality.check')
|
||||
if QC is None:
|
||||
return None
|
||||
# Prefer the passed QC for this MO; if there isn't one yet (e.g.
|
||||
# cert issuance before QC is walked) fall through to any QC
|
||||
# that has a PDF uploaded.
|
||||
qc = QC.search([
|
||||
('production_id', '=', mo.id),
|
||||
('state', '=', 'passed'),
|
||||
('thickness_report_pdf_id', '!=', False),
|
||||
], order='completed_at desc', limit=1)
|
||||
if not qc:
|
||||
qc = QC.search([
|
||||
('production_id', '=', mo.id),
|
||||
('thickness_report_pdf_id', '!=', False),
|
||||
], order='create_date desc', limit=1)
|
||||
if not qc or not qc.thickness_report_pdf_id:
|
||||
return None
|
||||
|
||||
fischer_bytes = _b64.b64decode(qc.thickness_report_pdf_id.datas or b'')
|
||||
if not fischer_bytes:
|
||||
return None
|
||||
|
||||
try:
|
||||
from PyPDF2 import PdfMerger
|
||||
except ImportError:
|
||||
try:
|
||||
from pypdf import PdfMerger # newer name
|
||||
except ImportError:
|
||||
_logger.warning(
|
||||
'Neither PyPDF2 nor pypdf installed — cannot merge '
|
||||
'Fischerscope PDF into CoC %s. Attaching CoC only.',
|
||||
cert.name,
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
merger = PdfMerger()
|
||||
merger.append(io.BytesIO(coc_pdf_bytes))
|
||||
merger.append(io.BytesIO(fischer_bytes))
|
||||
out = io.BytesIO()
|
||||
merger.write(out)
|
||||
merger.close()
|
||||
merged = out.getvalue()
|
||||
except Exception:
|
||||
_logger.exception(
|
||||
'PDF merge failed for CoC %s — falling back to CoC-only. '
|
||||
'The Fischerscope PDF may be corrupt / encrypted / '
|
||||
'malformed.',
|
||||
cert.name,
|
||||
)
|
||||
return None
|
||||
|
||||
cert.message_post(body=_(
|
||||
'Fischerscope report from QC %s appended to CoC PDF.'
|
||||
) % qc.name)
|
||||
return merged
|
||||
|
||||
Reference in New Issue
Block a user