fix(plating): CoC + invoice PDFs render full content

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-<Customer>-<CertName>.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) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-19 01:16:27 -04:00
parent 167c423bf5
commit 633427bcf8
7 changed files with 93 additions and 38 deletions

View File

@@ -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': """

View File

@@ -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-<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 ''
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',