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:
@@ -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': """
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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': [
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
<t t-elif="line.display_type == 'line_note'">
|
||||
<tr class="note-row"><td colspan="7"><span t-field="line.name"/></td></tr>
|
||||
</t>
|
||||
<t t-elif="not line.display_type">
|
||||
<t t-elif="not line.display_type or line.display_type == 'product'">
|
||||
<tr>
|
||||
<td class="text-center"><span t-esc="line.product_id.default_code or ''"/></td>
|
||||
<td>
|
||||
@@ -290,7 +290,7 @@
|
||||
<t t-elif="line.display_type == 'line_note'">
|
||||
<tr class="note-row"><td t-att-colspan="col_count"><span t-field="line.name"/></td></tr>
|
||||
</t>
|
||||
<t t-elif="not line.display_type">
|
||||
<t t-elif="not line.display_type or line.display_type == 'product'">
|
||||
<tr>
|
||||
<td class="text-center"><span t-esc="line.product_id.default_code or ''"/></td>
|
||||
<td>
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
<t t-elif="line.display_type == 'line_note'">
|
||||
<tr class="note-row"><td colspan="7"><span t-field="line.name"/></td></tr>
|
||||
</t>
|
||||
<t t-elif="not line.display_type">
|
||||
<t t-elif="not line.display_type or line.display_type == 'product'">
|
||||
<tr>
|
||||
<td class="text-center"><span t-esc="line.product_id.default_code or ''"/></td>
|
||||
<td>
|
||||
@@ -351,7 +351,7 @@
|
||||
<t t-elif="line.display_type == 'line_note'">
|
||||
<tr class="note-row"><td t-att-colspan="col_count"><span t-field="line.name"/></td></tr>
|
||||
</t>
|
||||
<t t-elif="not line.display_type">
|
||||
<t t-elif="not line.display_type or line.display_type == 'product'">
|
||||
<tr>
|
||||
<td class="text-center"><span t-esc="line.product_id.default_code or ''"/></td>
|
||||
<td>
|
||||
|
||||
Reference in New Issue
Block a user