diff --git a/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py b/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py index 441ba5c7..faa19224 100644 --- a/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py +++ b/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py @@ -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--.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