This commit is contained in:
gsinghpal
2026-04-27 00:11:18 -04:00
parent d9f58b9851
commit f08f328688
116 changed files with 9891 additions and 359 deletions

View File

@@ -5,7 +5,7 @@
{
'name': 'Fusion Plating — Certificates',
'version': '19.0.5.0.0',
'version': '19.0.5.2.0',
'category': 'Manufacturing/Plating',
'summary': 'Certificate registry for CoC, thickness reports, and quality documents.',
'description': """

View File

@@ -3,9 +3,13 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
import logging
from odoo import api, fields, models, _
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class FpCertificate(models.Model):
"""Unified certificate registry.
@@ -307,8 +311,170 @@ class FpCertificate(models.Model):
'so': rec.sale_order_id.name if rec.sale_order_id else '?',
})
rec.state = 'issued'
# Generate the CoC PDF and attach it so action_send_to_customer
# has something to email. Without this the workflow goes:
# Issue → Send → opens composer with no attachment → operator
# closes confused. Best-effort: if the report renders, attach;
# if it fails, log + continue (cert is still issued).
try:
rec._fp_render_and_attach_pdf()
except Exception as e:
_logger.warning(
'Cert %s: PDF render failed: %s', rec.name, e,
)
rec.message_post(body=_('Certificate issued.'))
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
QC tablet operator) as page 2, and attach the result.
Without the merge, a customer who specs "CoC must include the
XRF report" gets two separate PDFs to chase down. AS9100 wants
the supporting evidence inline with the cert.
Tries the EN-language CoC report first, falls back to the
generic action_report_coc. Idempotent — skips if attachment_id
is already set. PDF merge is best-effort: corrupt Fischerscope
upload or missing pypdf falls back to CoC-only with a warning.
"""
import base64
import io
self.ensure_one()
if self.attachment_id:
return self.attachment_id
report = (
self.env.ref(
'fusion_plating_reports.action_report_coc_en',
raise_if_not_found=False,
)
or self.env.ref(
'fusion_plating_reports.action_report_coc',
raise_if_not_found=False,
)
)
if not report:
_logger.warning(
'Cert %s: no CoC report action found, cannot render PDF',
self.name,
)
return False
coc_pdf_bytes, _content_type = report._render_qweb_pdf(
report.report_name, res_ids=self.ids,
)
# Try to append the Fischerscope thickness-report PDF as page 2.
merged_bytes = self._fp_merge_thickness_into_pdf(coc_pdf_bytes)
final_pdf = merged_bytes or coc_pdf_bytes
att = self.env['ir.attachment'].sudo().create({
'name': '%s.pdf' % (self.name or 'certificate'),
'type': 'binary',
'datas': base64.b64encode(final_pdf),
'mimetype': 'application/pdf',
'res_model': self._name,
'res_id': self.id,
})
self.attachment_id = att.id
return att
def _fp_merge_thickness_into_pdf(self, coc_pdf_bytes):
"""Look up the linked QC check, find its thickness_report_pdf_id
(Fischerscope / XDAL 600 XRF export), and return a merged PDF
with the CoC first + Fischerscope appended as page 2+.
Returns None when:
- cert isn't a CoC, or
- no fp.job linked, or
- no fp.quality.check on the job has a PDF uploaded, or
- pypdf / PyPDF2 not installed, or
- either PDF fails to parse.
Caller falls back to CoC-only when None is returned.
"""
import io
import base64 as _b64
self.ensure_one()
if self.certificate_type != 'coc':
return None
# Find the linked job. fp.certificate has either x_fc_job_id
# (preferred — added by fusion_plating_jobs) or job_id (older).
job = False
if 'x_fc_job_id' in self._fields:
job = self.x_fc_job_id
if not job and 'job_id' in self._fields:
job = self.job_id
if not job:
return None
# Find a passed QC on this job with an uploaded Fischerscope PDF.
# Prefer state=passed; fall through to any with a PDF.
QC = self.env.get('fusion.plating.quality.check')
if QC is None:
return None
qc = QC.sudo().search([
('job_id', '=', job.id),
('state', '=', 'passed'),
('thickness_report_pdf_id', '!=', False),
], order='completed_at desc', limit=1)
if not qc:
qc = QC.sudo().search([
('job_id', '=', job.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
# Merge — pypdf is the modern name; PyPDF2 still works on older
# Odoo bundles. Either is fine.
try:
from pypdf import PdfWriter
writer_cls = PdfWriter
use_append = True
except ImportError:
try:
from PyPDF2 import PdfMerger
writer_cls = PdfMerger
use_append = False
except ImportError:
_logger.warning(
'Cert %s: neither pypdf nor PyPDF2 installed, '
'cannot append Fischerscope PDF to CoC.',
self.name,
)
return None
try:
if use_append:
# pypdf 3.x — PdfWriter.append() handles bytes/streams
writer = writer_cls()
writer.append(io.BytesIO(coc_pdf_bytes))
writer.append(io.BytesIO(fischer_bytes))
out = io.BytesIO()
writer.write(out)
merged = out.getvalue()
else:
# PyPDF2 — PdfMerger.append + write
merger = writer_cls()
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 cert %s — Fischerscope PDF may '
'be corrupt / encrypted / malformed. Falling back to '
'CoC-only.', self.name,
)
return None
self.message_post(body=_(
'Fischerscope thickness report from QC %s appended to CoC PDF.'
) % qc.name)
return merged
def action_void(self):
for rec in self:
if rec.state != 'issued':