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:
@@ -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>
|
||||
|
||||
<!-- ============================================================= -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user