feat(plating): CoC spec-optional + SO-style header + thickness for any cert

- Drop the hard spec_reference gate on fp.certificate.action_issue. The
  customer-facing description (_fp_resolve_customer_facing_description,
  walks job -> SO line, reuses fp_customer_description) now drives the CoC
  Process column; spec_reference prints only when an estimator fills it.
- CoC EN/FR reports swap web.external_layout for fp_external_layout_clean +
  paperformat_fp_a4_portrait. New shared coc_header (company logo + address
  left, Nadcap logo centre, title + Code128 barcode right) mirrors the Sale
  Order header. Removed the 3-logo Nadcap/AS9100/CGP accreditation strip and
  the body H1s; padding-top 0 on both body wrappers.
- Un-gate the Issue Certs wizard thickness upload (was invisible unless the
  customer was thickness-flagged) so a Fischerscope report can be attached to
  ANY cert; merge (page 2) + inline readings already render unconditionally.
- Update issue-gate tests, bump versions (certificates 19.0.9.1.0,
  reports 19.0.11.27.0, jobs 19.0.11.2.0), record CLAUDE.md rule 14c.

Deployed + render-verified on entech (CoC-30065, 223KB PDF, no QWeb errors).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-28 21:17:09 -04:00
parent fecd2415f6
commit 307afbf3c0
10 changed files with 209 additions and 110 deletions

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
{
'name': 'Fusion Plating — Reports',
'version': '19.0.11.26.31',
'version': '19.0.11.27.0',
'category': 'Manufacturing/Plating',
'summary': 'PDF reports for Fusion Plating: quote, SO, WO, packing, BoL, CoC, invoice, receipt, quality + compliance.',
'depends': [

View File

@@ -122,8 +122,10 @@
<!-- ============================================================= -->
<!-- Formal Certificate of Conformance — English -->
<!-- Uses Odoo's default paperformat so web.external_layout's -->
<!-- header/footer band gets its reserved space correctly. -->
<!-- Compact portrait paperformat (margin_top=8) — same as the SO -->
<!-- confirmation. The CoC now renders its own header (coc_header) -->
<!-- via fp_external_layout_clean, so it needs the top of the page, -->
<!-- not Odoo's ~40mm default header reservation. -->
<!-- ============================================================= -->
<record id="action_report_coc_en" model="ir.actions.report">
<field name="name">Certificate of Conformance (English)</field>
@@ -134,6 +136,7 @@
<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_a4_portrait"/>
</record>
<!-- ============================================================= -->
@@ -148,6 +151,7 @@
<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_a4_portrait"/>
</record>
<!-- ============================================================= -->

View File

@@ -22,7 +22,91 @@
<odoo>
<!-- ================================================================== -->
<!-- Shared CoC body — rendered inside web.external_layout -->
<!-- Shared CoC header — logo + Nadcap + title/barcode. Mirrors the -->
<!-- Sale Order header (report_fp_sale.xml fp-sale-header-row): company -->
<!-- logo + address LEFT, Nadcap accreditation logo CENTRE, document -->
<!-- title + Code128 barcode RIGHT. Rendered once by the EN/FR wrappers -->
<!-- above the body, in place of web.external_layout's company band. -->
<!-- ================================================================== -->
<template id="coc_header">
<t t-set="is_fr" t-value="LANG == 'fr'"/>
<t t-set="logo_uri" t-value="('data:image/png;base64,%s' % company.logo.decode()) if company.logo else False"/>
<t t-set="company_fax" t-value="company.partner_id.x_ff_fax_number if 'x_ff_fax_number' in company.partner_id._fields else False"/>
<t t-set="coc_barcode_uri" t-value="doc.env['ir.actions.report'].sudo().barcode_data_uri('Code128', doc.name, 600, 100) if doc.name else False"/>
<style>
/* Float-based 3-column header (avoid HTML tables — the global
bordered-table cascade bleeds borders onto nested tables on
entech wkhtmltopdf; see CLAUDE.md). Bottom border separates
header from body, replacing the old hr.heavy. */
.fp-coc-header-row { overflow: hidden; margin-bottom: 10px;
padding-bottom: 6px; border-bottom: 2px solid #000; }
.fp-coc-header-left { float: left; width: 38%; }
.fp-coc-header-mid { float: left; width: 24%; text-align: center; padding-top: 4px; }
.fp-coc-header-right { float: right; width: 38%; text-align: center; }
.fp-coc-logo { max-height: 50px; max-width: 280px; display: block; margin-bottom: 4px; }
.fp-coc-company-addr { font-size: 8.5pt; color: #222; line-height: 1.35; }
.fp-coc-company-addr div { margin: 0; }
.fp-coc-company-addr a { color: #2e6da4; text-decoration: none; }
.fp-coc-nadcap-logo { max-height: 45px; max-width: 115px; display: inline-block; }
.fp-coc-title { font-size: 18pt; font-weight: bold; color: #2e2e2e;
line-height: 1.1; display: block; }
/* Barcode: inline-block wrap so the cert-number label centres
under the bars. Explicit no-border (wkhtmltopdf frames
inline-data imgs on entech). */
.fp-coc-bc-wrap { display: inline-block; text-align: center; margin-top: 4px; }
.fp-coc-bc-wrap img { height: 48px; max-width: 240px; border: 0 !important; padding: 0; display: block; }
.fp-coc-bc-label { font-size: 14pt; font-weight: bold; color: #000;
margin-top: 6px; letter-spacing: 1.2px; }
</style>
<div class="fp-coc-header-row">
<div class="fp-coc-header-left">
<t t-if="logo_uri">
<img t-att-src="logo_uri" class="fp-coc-logo" alt="Logo"/>
</t>
<div class="fp-coc-company-addr">
<div>
<t t-if="company.partner_id.street"><span t-esc="company.partner_id.street"/></t>
<t t-if="company.partner_id.city"> | <span t-esc="company.partner_id.city"/></t>
<t t-if="company.partner_id.state_id"> | <span t-esc="company.partner_id.state_id.code or company.partner_id.state_id.name"/></t>
<t t-if="company.partner_id.zip"> | <span t-esc="company.partner_id.zip"/></t>
</div>
<div t-if="company.phone or company_fax">
<t t-if="company.phone">Tel: <span t-esc="company.phone"/></t>
<t t-if="company.phone and company_fax">&#160;&#160;&#160;</t>
<t t-if="company_fax">Fax: <span t-esc="company_fax"/></t>
</div>
<div t-if="company.partner_id.website">
<a t-att-href="company.partner_id.website"><span t-esc="company.partner_id.website"/></a>
</div>
</div>
</div>
<!-- Centre: NADCAP accreditation logo, base64-inlined from
company settings (wkhtmltopdf can't fetch over HTTP on
entech). Same source as the Sale Order header. -->
<div class="fp-coc-header-mid">
<t t-if="company.x_fc_nadcap_active and company.x_fc_nadcap_logo">
<img class="fp-coc-nadcap-logo"
t-att-src="'data:image/png;base64,%s' % company.x_fc_nadcap_logo.decode()"
alt="Nadcap Accredited"/>
</t>
</div>
<div class="fp-coc-header-right">
<span class="fp-coc-title">
<t t-if="not is_fr">Certificate of Conformance</t>
<t t-if="is_fr">Certificat de Conformité</t>
</span>
<t t-if="coc_barcode_uri">
<div class="fp-coc-bc-wrap">
<img t-att-src="coc_barcode_uri" alt="Cert Barcode"/>
<div class="fp-coc-bc-label"><span t-field="doc.name"/></div>
</div>
</t>
</div>
</div>
</template>
<!-- ================================================================== -->
<!-- Shared CoC body — rendered inside fp_external_layout_clean -->
<!-- ================================================================== -->
<template id="coc_body">
<t t-set="is_fr" t-value="LANG == 'fr'"/>
@@ -38,18 +122,12 @@
<t t-set="signer_name" t-value="(signer_user and signer_user.name) or ''"/>
<style>
/* padding-top history: original 50mm wasted too much
page-1 space; dropped to 5mm caused the title to
overlap the ENTECH header (the rendered header is
taller than paperformat margin_top reserves). 20mm
is the middle ground — title sits cleanly below the
header, still saves ~30mm vs the original 50mm so the
signature block fits on page 1. If the header logo /
address changes height, bump this in step with
paperformat.margin_top. See CLAUDE.md "wkhtmltopdf
header overlap". */
/* No web.external_layout header band anymore — the CoC
renders its own header (coc_header) above the body, the
same way the Sale Order does. So no top padding is
needed to clear an Odoo header zone. */
.fp-coc { font-family: Arial, sans-serif; font-size: 9pt; color: #000;
padding-top: 20mm; }
padding-top: 0; }
.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; }
@@ -61,20 +139,6 @@
.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 .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; }
@@ -116,40 +180,11 @@
<div class="fp-coc">
<!-- Title -->
<h1 t-if="not is_fr">Certificate of Conformance</h1>
<h1 t-if="is_fr">Certificat de Conformité</h1>
<!-- 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>
<hr class="heavy"/>
<!-- Title + accreditation logos moved to the shared
coc_header (rendered above this body by the EN/FR
wrapper). The 3-logo accreditation strip was dropped
per client request 2026-05-28; the Nadcap logo now
lives in the header, mirroring the Sale Order. -->
<!-- Customer block — 3 columns: address | contact | logo -->
<table class="bordered">
@@ -272,7 +307,13 @@
<tr>
<td class="text-center"><t t-esc="doc.part_number or '-'"/></td>
<td>
<t t-esc="doc.process_description or ''"/>
<!-- Customer-facing description is the cert's
spec / certificate info (client request
2026-05-28). Falls back to the recipe-
derived process_description. spec_reference,
now optional, still prints below when set. -->
<t t-set="cust_desc" t-value="doc._fp_resolve_customer_facing_description()"/>
<t t-esc="cust_desc or doc.process_description or ''"/>
<t t-if="doc.spec_reference">
<br/><em t-esc="doc.spec_reference"/>
</t>
@@ -562,8 +603,14 @@
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<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">
<!-- Custom SO-style header instead of web.external_layout's
company band. fp_external_layout_clean provides the
.article wrapper Odoo needs for correct UTF-8 dispatch
plus a minimal page-number footer, with NO auto header
div — coc_header renders the visible header instead. -->
<t t-call="fusion_plating_reports.fp_external_layout_clean">
<t t-set="LANG" t-value="'en'"/>
<t t-call="fusion_plating_reports.coc_header"/>
<div class="page">
<!-- Sub 12c — router picks chronological vs classic body -->
<t t-call="fusion_plating_reports.coc_body_router"/>
@@ -580,8 +627,10 @@
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<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">
<!-- Custom SO-style header (see report_coc_en). -->
<t t-call="fusion_plating_reports.fp_external_layout_clean">
<t t-set="LANG" t-value="'fr'"/>
<t t-call="fusion_plating_reports.coc_header"/>
<div class="page">
<!-- Sub 12c — router picks chronological vs classic body -->
<t t-call="fusion_plating_reports.coc_body_router"/>

View File

@@ -22,8 +22,7 @@
<t t-set="moves" t-value="(job and 'move_ids' in job._fields and job.move_ids.sorted('move_datetime')) or []"/>
<style>
.fp-coc-chrono { font-family: Arial, sans-serif; font-size: 9pt; color: #000; padding-top: 8mm; }
.fp-coc-chrono h1 { text-align: center; font-size: 18pt; margin: 0 0 6px 0; font-weight: bold; }
.fp-coc-chrono { font-family: Arial, sans-serif; font-size: 9pt; color: #000; padding-top: 0; }
.fp-coc-chrono h3 { font-size: 11pt; margin: 8px 0 2px 0; font-weight: bold; }
.fp-coc-chrono .fp-chrono-meta { font-size: 8.5pt; color: #444; margin-bottom: 4px; }
.fp-coc-chrono table.bordered,
@@ -38,7 +37,8 @@
<div class="fp-coc-chrono">
<h1>Certificate of Conformance</h1>
<!-- Title + Nadcap logo render in the shared coc_header above
this body (rendered once by the EN/FR wrapper). -->
<!-- Job header (compact) -->
<table class="bordered">