From 633427bcf8ea502ca94ab9e8ee886f06cf22633b Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 01:16:27 -0400 Subject: [PATCH] fix(plating): CoC + invoice PDFs render full content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three reported PDF bugs from the customer-facing email package: 1. Invoice body was empty — Odoo 19 sets display_type='product' on regular invoice/SO lines (was empty string in 18.0). Both report_fp_invoice.xml and report_fp_sale.xml only matched `not line.display_type`, so every product line was skipped. Fixed both portrait + landscape variants to also match display_type == 'product'. 2. CoC PDF was a bare 30 KB header — _fp_generate_cert_pdf was rendering action_report_coc, which is bound to portal_job and has minimal content. Rewrote to use the rich fp.certificate-bound report (action_report_coc_en / action_report_coc_fr based on cert.partner_id.lang) and slugged the filename to CoC--.pdf so the email attachment reads nicely instead of CERT-00123.pdf. 3. Thickness cert was an exact duplicate of the CoC — the CoC template already embeds thickness readings. Skip thickness cert creation entirely when the customer also wants CoC; only create a standalone thickness cert when the customer opted out of CoC. Also: dispatcher in fp_notification_template now prefers portal_job.coc_attachment_id (the rich one we just generated) and falls back to rendering action_report_coc_en against fp.certificate by partner.lang — never the bare portal-job report. Versions bumped: bridge_mrp 19.0.6.0.0, notifications 19.0.4.0.0, reports 19.0.4.0.0. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../fusion_plating_bridge_mrp/__manifest__.py | 4 +- .../models/mrp_production.py | 63 ++++++++++++++----- .../__manifest__.py | 2 +- .../models/fp_notification_template.py | 52 ++++++++++----- .../fusion_plating_reports/__manifest__.py | 2 +- .../report/report_fp_invoice.xml | 4 +- .../report/report_fp_sale.xml | 4 +- 7 files changed, 93 insertions(+), 38 deletions(-) diff --git a/fusion_plating/fusion_plating_bridge_mrp/__manifest__.py b/fusion_plating/fusion_plating_bridge_mrp/__manifest__.py index 93539d9b..801e9ca1 100644 --- a/fusion_plating/fusion_plating_bridge_mrp/__manifest__.py +++ b/fusion_plating/fusion_plating_bridge_mrp/__manifest__.py @@ -4,8 +4,8 @@ # Part of the Fusion Plating product family. { - 'name': 'Fusion Plating — MRP Bridge', - 'version': '19.0.5.0.0', + "name": "Fusion Plating — MRP Bridge", + 'version': '19.0.6.0.0', 'category': 'Manufacturing/Plating', 'summary': 'Bridge Fusion Plating facilities, baths and tanks to Odoo MRP work orders.', 'description': """ 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 8452d975..da53329f 100644 --- a/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py +++ b/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py @@ -598,8 +598,13 @@ class MrpProduction(models.Model): if not coc_cert: coc_cert = Certificate.create({**base_vals, 'certificate_type': 'coc'}) + # Skip thickness cert when CoC also wanted — the CoC + # template already embeds thickness readings, so creating + # a separate thickness cert just produces a duplicate PDF. + # Only create a standalone thickness cert when the customer + # has explicitly opted OUT of CoC and only wants thickness. thickness_cert = False - if want_thickness: + if want_thickness and not want_coc: thickness_cert = Certificate.search( [('production_id', '=', mo.id), ('certificate_type', '=', 'thickness_report')], limit=1, @@ -610,11 +615,21 @@ class MrpProduction(models.Model): 'certificate_type': 'thickness_report', }) - # Render PDFs and stash on the cert + portal job + delivery - # so the operator doesn't need to open each cert and click - # "Generate". Errors here never block MO completion. + # Issue + render PDFs and stash on the cert + portal job + + # delivery. The cert moves out of draft so chatter + DB + # state are honest. Errors never block MO completion. for cert in (coc_cert, thickness_cert): - if cert and not cert.attachment_id: + if not cert: + continue + if cert.state == 'draft': + try: + cert.action_issue() + except Exception: + import logging + logging.getLogger(__name__).exception( + 'Cert auto-issue failed for %s', cert.name, + ) + if not cert.attachment_id: try: self._fp_generate_cert_pdf(cert, job, delivery) except Exception: @@ -676,30 +691,46 @@ class MrpProduction(models.Model): """Render a fp.certificate to PDF and attach it to the cert, the portal job, and the delivery (so the customer-facing portal and the shipping email both find it without an extra step). + + Uses the rich fp.certificate-bound report (action_report_coc_en + or action_report_coc_fr based on partner lang). The older + action_report_coc is portal-job bound and produces a bare header + — don't use it here. """ - report_xmlid = ( - 'fusion_plating_reports.action_report_coc' - if cert.certificate_type == 'coc' - else 'fusion_plating_reports.action_report_thickness' + # Pick the report variant by the customer's preferred language. + lang = (cert.partner_id.lang or '').lower() if cert.partner_id else '' + is_fr = lang.startswith('fr') + report = self.env.ref( + 'fusion_plating_reports.action_report_coc_fr' + if is_fr + else 'fusion_plating_reports.action_report_coc_en', + raise_if_not_found=False, ) - report = self.env.ref(report_xmlid, raise_if_not_found=False) - if not report and cert.certificate_type == 'thickness_report': - # Fall back to the CoC layout for thickness if a dedicated - # thickness report isn't installed — better an attachment - # with thickness data baked in than nothing at all. + if not report: + # Last-resort fallback to the EN variant if FR is missing. report = self.env.ref( - 'fusion_plating_reports.action_report_coc', + 'fusion_plating_reports.action_report_coc_en', raise_if_not_found=False, ) if not report: return # reports module not available import base64 + import re + pdf_content, _ext = report.with_context( force_report_rendering=True, )._render_qweb_pdf(report.report_name, [cert.id]) + + # 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 '' + cust_slug = re.sub(r'[^A-Za-z0-9]+', '_', cust_name).strip('_') or 'Customer' + prefix = 'CoC' if cert.certificate_type == 'coc' else 'Thickness' + filename = f'{prefix}-{cust_slug}-{cert.name}.pdf' + att = self.env['ir.attachment'].create({ - 'name': f'{cert.name}.pdf', + 'name': filename, 'type': 'binary', 'datas': base64.b64encode(pdf_content), 'res_model': 'fp.certificate', diff --git a/fusion_plating/fusion_plating_notifications/__manifest__.py b/fusion_plating/fusion_plating_notifications/__manifest__.py index d1264a02..2a8be43b 100644 --- a/fusion_plating/fusion_plating_notifications/__manifest__.py +++ b/fusion_plating/fusion_plating_notifications/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Notifications', - 'version': '19.0.3.0.0', + 'version': '19.0.4.0.0', 'category': 'Manufacturing/Plating', 'summary': 'Auto-email notifications at workflow milestones with configurable templates, PDF attachments, and audit log.', 'author': 'Nexa Systems Inc.', diff --git a/fusion_plating/fusion_plating_notifications/models/fp_notification_template.py b/fusion_plating/fusion_plating_notifications/models/fp_notification_template.py index 768f709e..41e6381c 100644 --- a/fusion_plating/fusion_plating_notifications/models/fp_notification_template.py +++ b/fusion_plating/fusion_plating_notifications/models/fp_notification_template.py @@ -218,25 +218,49 @@ class FpNotificationTemplate(models.Model): ) if att: ids.append(att) - # CoC — gated by customer preference (x_fc_send_coc, default True) + # CoC — gated by customer preference (x_fc_send_coc, default True). + # Prefer the rich PDF that mrp_production.button_mark_done already + # rendered against the fp.certificate (signatures, accreditation + # logos, thickness data). The legacy action_report_coc bound to + # fusion.plating.portal.job is only a header table; never use it + # when a real cert PDF exists. if self.attach_coc and portal_job and _customer_wants('x_fc_send_coc'): - att = _render_report( - 'fusion_plating_reports.action_report_coc', portal_job, - ) - if att: - ids.append(att) - # Thickness report — gated by customer preference. Today the CoC - # template embeds thickness readings, so when a customer wants - # thickness-only we fall back to the CoC report attachment with - # a distinct filename. A standalone thickness-only template is - # TBD (not part of this chunk). + if portal_job.coc_attachment_id: + ids.append(portal_job.coc_attachment_id.id) + else: + # No pre-rendered cert (older job or cert-gen failed). + # Render the rich cert report against the most recent + # CoC fp.certificate, falling back to the bare portal_job + # template only if no cert exists at all. + Cert = self.env.get('fp.certificate') + cert = False + if Cert is not None and production: + cert = Cert.search([ + ('production_id', '=', production.id), + ('certificate_type', '=', 'coc'), + ], order='id desc', limit=1) + if cert: + lang = (cert.partner_id.lang or '').lower() + cert_xmlid = ( + 'fusion_plating_reports.action_report_coc_fr' + if lang.startswith('fr') + else 'fusion_plating_reports.action_report_coc_en' + ) + att = _render_report(cert_xmlid, cert) + else: + att = _render_report( + 'fusion_plating_reports.action_report_coc', portal_job, + ) + if att: + ids.append(att) + # Thickness report — only attach when the customer opted OUT of + # CoC and ONLY wants thickness. The CoC PDF already embeds + # thickness data so attaching both would be a duplicate. if (self.attach_thickness_report and portal_job and _customer_wants('x_fc_send_thickness_report') and not (self.attach_coc and _customer_wants('x_fc_send_coc'))): - # Avoid double-attaching the same PDF when both are wanted — - # the CoC already carries the thickness data. att = _render_report( - 'fusion_plating_reports.action_report_coc', portal_job, + 'fusion_plating_reports.action_report_coc_en', portal_job, ) if att: ids.append(att) diff --git a/fusion_plating/fusion_plating_reports/__manifest__.py b/fusion_plating/fusion_plating_reports/__manifest__.py index 7fb1e2df..571adc29 100644 --- a/fusion_plating/fusion_plating_reports/__manifest__.py +++ b/fusion_plating/fusion_plating_reports/__manifest__.py @@ -3,7 +3,7 @@ # License OPL-1 (Odoo Proprietary License v1.0) { 'name': 'Fusion Plating — Reports', - 'version': '19.0.3.0.0', + 'version': '19.0.4.0.0', 'category': 'Manufacturing/Plating', 'summary': 'PDF reports for Fusion Plating: quote, SO, WO, packing, BoL, CoC, invoice, receipt, quality + compliance.', 'depends': [ diff --git a/fusion_plating/fusion_plating_reports/report/report_fp_invoice.xml b/fusion_plating/fusion_plating_reports/report/report_fp_invoice.xml index 463837cd..7e3eaaa2 100644 --- a/fusion_plating/fusion_plating_reports/report/report_fp_invoice.xml +++ b/fusion_plating/fusion_plating_reports/report/report_fp_invoice.xml @@ -97,7 +97,7 @@ - + @@ -290,7 +290,7 @@ - + diff --git a/fusion_plating/fusion_plating_reports/report/report_fp_sale.xml b/fusion_plating/fusion_plating_reports/report/report_fp_sale.xml index 03d6ae71..e1bb8231 100644 --- a/fusion_plating/fusion_plating_reports/report/report_fp_sale.xml +++ b/fusion_plating/fusion_plating_reports/report/report_fp_sale.xml @@ -118,7 +118,7 @@ - + @@ -351,7 +351,7 @@ - +