refactor(coc): use web.external_layout for header/footer + 3-column bordered accreditations

Per feedback, dropped the custom company-contact header and paperformat
in favour of Odoo's standard web.external_layout. This gives the CoC:
  - Company-branded header (logo, name, address, phone, email, tax id)
    matching whichever layout variant the company picked in
    Settings → General → Document Layout (Standard / Boxed / Clean /
    Striped). Automatically themed with company.primary_color.
  - Consistent page X/Y footer + "Printed on" timestamp.
  - Correct header_spacing so the letterhead band lines up with the
    default paperformat.

Our body now owns:
  - Centred "Certificate of Conformance" / "Certificat de Conformité"
  - 3-column bordered accreditation table — one logo per cell (Nadcap,
    AS9100D/ISO 9001, CGP) with equal 33.33% widths and #000 borders,
    2.8cm cell height so logos centre vertically
  - Optional customer logo (res.partner.image_1920) right-aligned
    below the accreditation row
  - Customer info block (name, address, contact, email, phone)
  - Certification info table (date, generated-by, WO#)
  - Quantities table (part, process, PO, shipped, NC qty, job no)
  - Signature image + bordered cert statement
  - "Fusion Plating by Nexa Systems" brand note

Template plumbing:
- Explicit `<t t-set="company" t-value="doc.sale_order_id.company_id
  or doc.production_id.company_id or env.company"/>` in the EN/FR
  wrappers because QWeb's t-call scoping doesn't expose variables set
  inside external_layout to the body we pass through. Without this,
  coc_body's `company.x_fc_owner_user_id` raises KeyError.
- Removed paperformat_fp_coc from the report actions (now uses the
  default paperformat, which is designed for external_layout's
  reserved header_spacing).

Verified: 332KB PDF, 1 page, all 5 images embedded, Amphenol logo on
right side of accreditation row, signature renders, company header
band at top, page footer at bottom.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-17 01:59:54 -04:00
parent 66f7f6c644
commit 44a980c468
2 changed files with 117 additions and 142 deletions

View File

@@ -70,6 +70,8 @@
<!-- ============================================================= -->
<!-- Formal Certificate of Conformance — English -->
<!-- Uses Odoo's default paperformat so web.external_layout's -->
<!-- header/footer band gets its reserved space correctly. -->
<!-- ============================================================= -->
<record id="action_report_coc_en" model="ir.actions.report">
<field name="name">Certificate of Conformance (English)</field>
@@ -80,7 +82,6 @@
<field name="print_report_name">'CoC EN - %s' % object.name</field>
<field name="binding_model_id" ref="fusion_plating_certificates.model_fp_certificate"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_coc"/>
</record>
<!-- ============================================================= -->
@@ -95,7 +96,6 @@
<field name="print_report_name">'CoC FR - %s' % object.name</field>
<field name="binding_model_id" ref="fusion_plating_certificates.model_fp_certificate"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_coc"/>
</record>
<!-- ============================================================= -->

View File

@@ -4,21 +4,25 @@
License OPL-1 (Odoo Proprietary License v1.0)
Fusion Plating — Certificate of Conformance
Four variants:
- report_coc_en English, portrait, ENTECH-style formal cert (primary)
- report_coc_fr French, portrait, mirror of EN
- report_coc_portrait Legacy portrait (kept for existing bindings)
- report_coc Legacy landscape (kept for existing bindings)
Design note:
The EN + FR CoCs wrap their body in web.external_layout. That gives
us Odoo's standard company-branded header (logo, name, address,
phone, email, tax id) and footer (page X/Y, printed-on) for free,
using whichever layout variant the company has chosen in
Settings → General → Document Layout. Our body just renders the
CoC-specific content: title, accreditation badges, customer block,
details table, signature, and certification statement.
Settings sourced from res.company (Settings → Fusion Plating):
- x_fc_owner_user_id.employee_ids[:1].signature Default signer image
- x_fc_coc_signature_override Optional override image
- x_fc_{nadcap,as9100,cgp}_logo + _active Accreditation badges
Variants:
- report_coc_en English formal cert (primary)
- report_coc_fr French mirror
- report_coc, report_coc_portrait
Legacy portal-job-bound variants (unchanged)
-->
<odoo>
<!-- ================================================================== -->
<!-- Shared CoC body macro (English / French switched via LANG) -->
<!-- Shared CoC body — rendered inside web.external_layout -->
<!-- ================================================================== -->
<template id="coc_body">
<t t-set="is_fr" t-value="LANG == 'fr'"/>
@@ -33,95 +37,91 @@
<t t-set="signer_name" t-value="doc.certified_by_id.name or (company.x_fc_owner_user_id.name if company.x_fc_owner_user_id else '')"/>
<style>
/* Kill all Odoo-inherited body/main/page spacing */
html, body { margin: 0 !important; padding: 0 !important; }
main, .article, .page, .o_report_layout_boxed {
margin: 0 !important; padding: 0 !important;
page-break-after: auto !important;
min-height: 0 !important;
}
header.o_company_header, footer.o_company_footer { display: none !important; }
.fp-coc { font-family: Arial, sans-serif; font-size: 9pt; color: #000;
margin: 0; padding: 0; }
.fp-coc h1 { text-align: center; font-size: 20pt; margin: 0 0 6px 0;
font-weight: bold; page-break-before: avoid; }
.fp-coc hr.heavy { border: 0; border-top: 2px solid #000; margin: 4px 0; }
.fp-coc table { width: 100%; border-collapse: collapse; }
.fp-coc { font-family: Arial, sans-serif; font-size: 9pt; color: #000; }
.fp-coc h1 { text-align: center; font-size: 20pt; margin: 0 0 10px 0; font-weight: bold; }
.fp-coc hr.heavy { border: 0; border-top: 2px solid #000; margin: 6px 0; }
.fp-coc table { width: 100%; border-collapse: collapse; margin-bottom: 6px; }
.fp-coc table.bordered,
.fp-coc table.bordered th,
.fp-coc table.bordered td { border: 1px solid #000; }
.fp-coc th { background-color: #ededed; font-weight: bold;
padding: 4px 6px; font-size: 8.5pt; text-align: center; }
.fp-coc td { padding: 4px 6px; vertical-align: top; font-size: 8.5pt; }
padding: 5px 8px; font-size: 8.5pt; text-align: center; }
.fp-coc td { padding: 5px 8px; vertical-align: top; font-size: 8.5pt; }
.fp-coc .text-center { text-align: center; }
.fp-coc .text-end { text-align: right; }
.fp-coc .hdr-company { font-size: 8pt; line-height: 1.35; padding: 4px; }
.fp-coc .hdr-company strong { font-size: 10pt; }
.fp-coc .cert-statement-box { border: 1px solid #000; padding: 8px; font-size: 8pt; }
.fp-coc .cert-statement-box h4 { margin: 0 0 4px 0; font-size: 9pt; font-weight: bold; }
.fp-coc .signature-img { max-height: 1.8cm; max-width: 6.5cm; }
.fp-coc .accreditations { text-align: center; vertical-align: middle; padding: 4px; }
.fp-coc .accreditations img { max-height: 1.8cm; margin: 0 3px; vertical-align: middle; }
.fp-coc .logo-box { text-align: right; vertical-align: middle; padding: 4px; }
.fp-coc .logo-box img { max-height: 2cm; max-width: 3.5cm; }
.fp-coc .customer-logo-box img { max-height: 1.6cm; max-width: 3cm; }
.fp-coc .fp-footer-brand { font-size: 7.5pt; color: #666; text-align: center;
margin-top: 8px; }
.fp-coc .accreditation-table { margin: 8px 0; }
.fp-coc .accreditation-table td.accreditation-cell {
width: 33.33%;
text-align: center;
vertical-align: middle;
padding: 10px;
border: 1px solid #000;
height: 2.8cm;
}
.fp-coc .accreditation-table img {
max-height: 2cm;
max-width: 95%;
vertical-align: middle;
}
.fp-coc .customer-logo { max-height: 1.8cm; max-width: 3.5cm; }
.fp-coc .cert-statement-box { border: 1px solid #000; padding: 10px; font-size: 8.5pt; }
.fp-coc .cert-statement-box h4 { margin: 0 0 6px 0; font-size: 9.5pt; font-weight: bold; }
.fp-coc .signature-img { max-height: 2.2cm; max-width: 7cm; }
.fp-coc .small-label { font-size: 7.5pt; opacity: 0.7; }
.fp-coc .brand-note { font-size: 7.5pt; color: #888; text-align: center;
margin-top: 10px; font-style: italic; }
</style>
<div class="fp-coc">
<!-- ============================= -->
<!-- HEADER -->
<!-- ============================= -->
<!-- Title -->
<h1 t-if="not is_fr">Certificate of Conformance</h1>
<h1 t-if="is_fr">Certificat de Conformité</h1>
<table>
<tr>
<td style="width: 33%;" class="hdr-company">
<strong t-field="company.name"/><br/>
<t t-if="company.email"><t t-esc="company.email"/><br/></t>
<t t-if="company.phone"><t t-esc="company.phone"/><br/></t>
<t t-if="company.partner_id.street"><t t-esc="company.partner_id.street"/><br/></t>
<t t-if="company.partner_id.street2"><t t-esc="company.partner_id.street2"/><br/></t>
<span t-if="company.partner_id.city"><t t-esc="company.partner_id.city"/></span>
<span t-if="company.partner_id.state_id">, <t t-esc="company.partner_id.state_id.name"/></span>
<span t-if="company.partner_id.zip"> <t t-esc="company.partner_id.zip"/></span><br/>
<t t-if="company.partner_id.country_id"><t t-esc="company.partner_id.country_id.name.upper()"/></t>
</td>
<td style="width: 34%;" class="accreditations">
<t t-if="company.x_fc_nadcap_active and company.x_fc_nadcap_logo">
<img t-att-src="'data:image/png;base64,%s' % company.x_fc_nadcap_logo.decode()"
alt="Nadcap Accredited"/>
</t>
<t t-if="company.x_fc_as9100_active and company.x_fc_as9100_logo">
<img t-att-src="'data:image/png;base64,%s' % company.x_fc_as9100_logo.decode()"
alt="AS9100 / ISO 9001"/>
</t>
<t t-if="company.x_fc_cgp_active and company.x_fc_cgp_logo">
<img t-att-src="'data:image/png;base64,%s' % company.x_fc_cgp_logo.decode()"
alt="Controlled Goods Program"/>
</t>
</td>
<td style="width: 33%;" class="logo-box">
<img t-if="company.logo"
t-att-src="'data:image/png;base64,%s' % company.logo.decode()"
alt=""/>
</td>
</tr>
</table>
<!-- Accreditations — 3 bordered columns, one logo per column -->
<t t-set="nadcap_on" t-value="company.x_fc_nadcap_active and company.x_fc_nadcap_logo"/>
<t t-set="as9100_on" t-value="company.x_fc_as9100_active and company.x_fc_as9100_logo"/>
<t t-set="cgp_on" t-value="company.x_fc_cgp_active and company.x_fc_cgp_logo"/>
<t t-if="nadcap_on or as9100_on or cgp_on">
<table class="bordered accreditation-table">
<tr>
<td class="accreditation-cell">
<t t-if="nadcap_on">
<img t-att-src="'data:image/png;base64,%s' % company.x_fc_nadcap_logo.decode()"
alt="Nadcap Accredited"/>
</t>
</td>
<td class="accreditation-cell">
<t t-if="as9100_on">
<img t-att-src="'data:image/png;base64,%s' % company.x_fc_as9100_logo.decode()"
alt="AS9100 / ISO 9001"/>
</t>
</td>
<td class="accreditation-cell">
<t t-if="cgp_on">
<img t-att-src="'data:image/png;base64,%s' % company.x_fc_cgp_logo.decode()"
alt="Controlled Goods Program"/>
</t>
</td>
</tr>
</table>
</t>
<!-- Customer logo (separate, right-aligned) -->
<t t-if="doc.partner_id.image_1920">
<div style="text-align: right; margin-top: 6px;">
<img class="customer-logo"
t-att-src="'data:image/png;base64,%s' % doc.partner_id.image_1920.decode()"
alt=""/>
</div>
</t>
<hr class="heavy"/>
<!-- ============================= -->
<!-- CUSTOMER BLOCK -->
<!-- ============================= -->
<table style="margin-top: 6px;">
<!-- Customer block -->
<table>
<tr>
<td style="width: 40%; vertical-align: top;">
<td style="width: 50%; vertical-align: top;">
<div>
<strong t-if="not is_fr">Customer Name: </strong>
<strong t-if="is_fr">Nom du client : </strong>
@@ -138,7 +138,7 @@
<span t-if="doc.partner_id.zip"> <t t-esc="doc.partner_id.zip"/></span>
</div>
</td>
<td style="width: 40%; vertical-align: top;">
<td style="width: 50%; vertical-align: top;">
<t t-set="contact" t-value="doc.contact_partner_id or doc.partner_id"/>
<div>
<strong t-if="not is_fr">Contact Name: </strong>
@@ -156,20 +156,13 @@
<t t-esc="contact.phone or '-'"/>
</div>
</td>
<td style="width: 20%; vertical-align: top;" class="logo-box">
<img t-if="doc.partner_id.image_1920"
t-att-src="'data:image/png;base64,%s' % doc.partner_id.image_1920.decode()"
alt=""/>
</td>
</tr>
</table>
<hr class="heavy"/>
<!-- ============================= -->
<!-- CERTIFICATION INFO -->
<!-- ============================= -->
<table class="bordered" style="margin-top: 10px;">
<!-- Certification info table -->
<table class="bordered" style="margin-top: 8px;">
<thead>
<tr>
<th style="width: 33%;">
@@ -201,10 +194,8 @@
</tbody>
</table>
<!-- ============================= -->
<!-- LINE ITEM / QUANTITIES TABLE -->
<!-- ============================= -->
<div class="text-end small-label" style="margin-top: 12px;">
<!-- Quantities / line item table -->
<div class="text-end small-label" style="margin-top: 8px;">
<strong t-if="not is_fr">Quantities</strong>
<strong t-if="is_fr">Quantités</strong>
</div>
@@ -239,54 +230,42 @@
</thead>
<tbody>
<tr>
<td class="text-center">
<t t-esc="doc.part_number or '-'"/>
</td>
<td class="text-center"><t t-esc="doc.part_number or '-'"/></td>
<td>
<t t-esc="doc.process_description or ''"/>
<t t-if="doc.spec_reference">
<br/><em t-esc="doc.spec_reference"/>
</t>
</td>
<td class="text-center">
<t t-esc="doc.po_number or '-'"/>
</td>
<td class="text-center">
<t t-esc="doc.quantity_shipped or 0"/>
</td>
<td class="text-center">
<t t-esc="doc.nc_quantity or 0"/>
</td>
<td class="text-center">
<t t-esc="doc.customer_job_no or '-'"/>
</td>
<td class="text-center"><t t-esc="doc.po_number or '-'"/></td>
<td class="text-center"><t t-esc="doc.quantity_shipped or 0"/></td>
<td class="text-center"><t t-esc="doc.nc_quantity or 0"/></td>
<td class="text-center"><t t-esc="doc.customer_job_no or '-'"/></td>
</tr>
</tbody>
</table>
<!-- ============================= -->
<!-- SIGNATURE + STATEMENT -->
<!-- ============================= -->
<table style="margin-top: 28px;">
<!-- Signature + certification statement -->
<table style="margin-top: 18px;">
<tr>
<td style="width: 50%; vertical-align: top;">
<div>
<strong t-if="not is_fr">Certified By:</strong>
<strong t-if="is_fr">Certifié par :</strong>
</div>
<div style="min-height: 3cm; margin-top: 8px;">
<div style="min-height: 2.5cm; margin-top: 6px;">
<img t-if="signature_img"
class="signature-img"
t-att-src="'data:image/png;base64,%s' % signature_img.decode()"
alt=""/>
</div>
<div style="margin-top: 6px; border-top: 1px solid #000; padding-top: 4px;">
<div style="margin-top: 4px; border-top: 1px solid #000; padding-top: 4px;">
<strong t-if="not is_fr">Name: </strong>
<strong t-if="is_fr">Nom : </strong>
<t t-esc="signer_name or ''"/>
</div>
</td>
<td style="width: 50%; vertical-align: top; padding-left: 16px;">
<td style="width: 50%; vertical-align: top; padding-left: 14px;">
<div class="cert-statement-box">
<h4 t-if="not is_fr">Certification Statement</h4>
<h4 t-if="is_fr">Énoncé de conformité</h4>
@@ -311,36 +290,28 @@
</tr>
</table>
<!-- ============================= -->
<!-- FOOTER -->
<!-- ============================= -->
<div class="fp-footer-brand">
<t t-if="not is_fr">
Cert Created At:
<span t-esc="context_timestamp(doc.create_date).strftime('%Y-%m-%d %H:%M') if doc.create_date else ''"/>
· Fusion Plating by Nexa Systems
</t>
<t t-if="is_fr">
Certificat créé le :
<span t-esc="context_timestamp(doc.create_date).strftime('%Y-%m-%d %H:%M') if doc.create_date else ''"/>
· Fusion Plating par Nexa Systems
</t>
<!-- Brand note (small, above Odoo's page footer) -->
<div class="brand-note">
<t t-if="not is_fr">Fusion Plating by Nexa Systems</t>
<t t-if="is_fr">Fusion Plating par Nexa Systems</t>
</div>
</div>
</template>
<!-- ================================================================== -->
<!-- English CoC — renders directly in html_container with NO wrapper -->
<!-- templates, so Odoo's `.article` and `.page` styles (which force -->
<!-- page-break-after + reserved header padding) don't apply. -->
<!-- English CoC — wrapped in web.external_layout -->
<!-- ================================================================== -->
<template id="report_coc_en">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-set="company" t-value="(doc.portal_job_id.company_id if doc.portal_job_id else False) or (doc.sale_order_id.company_id if doc.sale_order_id else False) or env.company"/>
<t t-set="LANG" t-value="'en'"/>
<t t-call="fusion_plating_reports.coc_body"/>
<t t-set="company" t-value="(doc.sale_order_id.company_id if doc.sale_order_id else False) or (doc.production_id.company_id if doc.production_id else False) or env.company"/>
<t t-call="web.external_layout">
<t t-set="LANG" t-value="'en'"/>
<div class="page">
<t t-call="fusion_plating_reports.coc_body"/>
</div>
</t>
</t>
</t>
</template>
@@ -351,9 +322,13 @@
<template id="report_coc_fr">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-set="company" t-value="(doc.portal_job_id.company_id if doc.portal_job_id else False) or (doc.sale_order_id.company_id if doc.sale_order_id else False) or env.company"/>
<t t-set="LANG" t-value="'fr'"/>
<t t-call="fusion_plating_reports.coc_body"/>
<t t-set="company" t-value="(doc.sale_order_id.company_id if doc.sale_order_id else False) or (doc.production_id.company_id if doc.production_id else False) or env.company"/>
<t t-call="web.external_layout">
<t t-set="LANG" t-value="'fr'"/>
<div class="page">
<t t-call="fusion_plating_reports.coc_body"/>
</div>
</t>
</t>
</t>
</template>