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

@@ -384,6 +384,7 @@ Use only: `name`, `model_id`, `state`, `code` (or `function`/`model`), `interval
Applied to `.fp-report table.bordered`, `.fp-landscape table.bordered`, `.fp-report .totals-table`, and `.fp-report .sig-table`. If you need to add a new bordered table, follow the same longhand-border + background-clip template.
14b. **FP report signature source**: every FP report that prints a signer signature (WO Detail, CoC, CoC Chronological, future cert templates) reads from **`res.users.x_fc_signature_image`** — the "Plating Signature" the user uploads under Preferences → My Profile. Retired alternatives (2026-05-17): the HR Employee signature lookup (`user.employee_ids[:1].signature`) and the company-level Signature Override Image (`res.company.x_fc_coc_signature_override`). Both have been removed from all report templates and the company-level setting UI; the override column on `res.company` is kept for now (no migration) but is no longer read. **Don't re-introduce the HR-Employee or override patterns** — pick the signer user via whatever resolution chain the report needs (cert's `certified_by_id`, company's `x_fc_qa_manager_user_ids[:1]`, `job.manager_id`, `company.x_fc_owner_user_id`, etc.) then read `signer_user.x_fc_signature_image`.
14c. **CoC header + spec policy (client request 2026-05-28)**: the formal CoC reports (`report_coc_en` / `report_coc_fr`) render their OWN header (`coc_header` template in `report_coc.xml`) via `fusion_plating_reports.fp_external_layout_clean` — **NOT `web.external_layout`** — bound to `paperformat_fp_a4_portrait`. `coc_header` mirrors the Sale Order header (`report_fp_sale.xml`): company logo+address LEFT, **Nadcap accreditation logo CENTRE** (`company.x_fc_nadcap_logo`), document title + Code128 barcode of `doc.name` RIGHT. The old 3-logo accreditation strip (Nadcap/AS9100/CGP columns) was **removed** from `coc_body`; don't re-add it — Nadcap lives in the header now. The body wrappers (`.fp-coc`, `.fp-coc-chrono`) use `padding-top: 0` (no external_layout band to clear) and no longer carry an `<h1>` (title is in the header, rendered once by the EN/FR wrapper for BOTH body styles). **`spec_reference` is NO LONGER a hard gate** on `fp.certificate.action_issue` — the customer-facing description (`fp.certificate._fp_resolve_customer_facing_description()`, walks `x_fc_job_id → sale_order_id.order_line` matching the part, reuses `sale.order.line.fp_customer_description()`) is the cert's Process/spec text, with `process_description` (recipe name) as fallback; `spec_reference` still prints below when an estimator fills it. The Issue Certs wizard's thickness upload fields are **un-gated** (no longer `invisible="not needs_thickness"`) so a Fischerscope report can be attached to ANY cert and pulled onto the CoC (merge as page 2 for PDFs / inline readings table for .docx/RTF — both already render unconditionally on data presence). NB: the `process_description` / `certified_by_id` / `contact_partner_id`(+email) gates on `action_issue` are SEPARATE and still enforced.
14. **Sticker template — leave the CSS units alone**: `report_fp_wo_sticker_inner` is calibrated for **px units at paperformat dpi=300** on entech's wkhtmltopdf. Do NOT "modernise" it by converting px→mm or by bumping paperformat dpi — both have been tried (2026-05-16) and both collapsed the layout (tiny logo, tiny QR, body grid shorter than the body band, font sizes visually smaller despite using pt). The math suggests the conversions should be equivalent, but wkhtmltopdf's px↔mm↔dpi mapping doesn't follow the obvious model on this image. Trust the working geometry, change only what you came to change. **Barcode size cap**: Odoo core raises `ValueError("Barcode too large")` when `width * height > 1_200_000` OR `max(width, height) > 10000` (see `base/models/ir_actions_report.py::barcode`). Largest safe square is ~1095×1095 — we use 1000×1000 to stay clear of the ceiling. **Em-dash mojibake**: wkhtmltopdf's default font on entech mojibakes em-dash (—), en-dash (), smart quotes, and ellipsis into `â€"` etc. — strip them defensively for any free-text field that bleeds into the sticker (thickness, notes, line.name). The strip pattern is `.replace(u'—', '-').replace(u'', '-')...` in `report_fp_wo_sticker_inner`.
15. **Recipe editor parity**: Step-level UX features (image attachments, prompt editing, settings toggles, preview affordances, etc.) MUST be implemented in BOTH the **Simple Editor** (`fusion_plating/static/src/{js,xml,scss}/simple_recipe_editor.*` + `controllers/simple_recipe_controller.py`) AND the **Tree Editor** (`fusion_plating/static/src/{js,xml,scss}/recipe_tree_editor.*` + `controllers/recipe_controller.py`). Authors choose between editors per-recipe via `preferred_editor`; if a feature only lands in one, half the userbase silently misses it. Default assumption: most clients use the Simple Editor — when in doubt, ship Simple first, then port to Tree in the same change. Backend model + view changes (e.g. new fields on `fusion.plating.process.node`, new tabs on the node form) automatically reach both editors via the related model — only the editor-specific JS/XML/SCSS needs duplicating.
16. **HTTP controller route override = method name must match parent**: To override a route on an inherited controller (e.g. `portal.CustomerPortal.home()` at `/my/home`), the override method MUST share the parent's method name. Declaring a new method name with the same `@http.route()` URL does NOT override — Odoo registers BOTH handlers as siblings and the parent typically wins, silently. Pattern: `class FpCustomerPortal(CustomerPortal): @http.route() def home(self, **kw): ...`. Bit us 2026-05-17 in `fusion_plating_portal/controllers/portal.py``portal_my_home_dashboard()` failed to override stock `home()`; symptom was the rich FP dashboard never rendering at `/my/home` even though the template was active in DB.

View File

@@ -5,7 +5,7 @@
{
'name': 'Fusion Plating — Certificates',
'version': '19.0.9.0.0',
'version': '19.0.9.1.0',
'category': 'Manufacturing/Plating',
'summary': 'Certificate registry for CoC, thickness reports, and quality documents.',
'description': """

View File

@@ -496,16 +496,13 @@ class FpCertificate(models.Model):
and 'x_fc_owner_user_id' in rec.company_id._fields
and rec.company_id.x_fc_owner_user_id):
rec.certified_by_id = rec.company_id.x_fc_owner_user_id
# Spec reference is what the cert ATTESTS — without it the
# cert is just a piece of paper. AS9100 / Nadcap require
# naming the spec the work was performed to.
if not rec.spec_reference:
raise UserError(_(
'Cannot issue certificate "%(name)s" — no Spec '
'Reference set.\n\nFill the Spec Reference field '
'(e.g. "AMS 2404", "MIL-C-26074") so the cert '
'states which standard the work meets.'
) % {'name': rec.name or rec.display_name})
# Spec Reference is OPTIONAL (client request 2026-05-28).
# The customer-facing description now serves as the cert's
# spec / certificate information (see
# _fp_resolve_customer_facing_description + the CoC Process
# column). spec_reference still prints below the description
# when an estimator chooses to fill it, but it no longer
# blocks issuance.
# Process description (what was done to the parts). Without
# it the cert PDF just shows blank process text — customer
# has no idea what they paid for. Auto-filled from the
@@ -733,6 +730,53 @@ class FpCertificate(models.Model):
return
delivery.coc_attachment_id = self.attachment_id.id
def _fp_resolve_customer_facing_description(self):
"""Resolve the customer-facing description used as the cert's
spec / certificate information on the printed CoC.
Client request 2026-05-28: Spec Reference is no longer
mandatory; the customer-facing description (what the estimator
actually typed on the order) now carries the descriptive text.
Resolution — first non-empty wins:
1. The order line matching this cert's job/part — cleaned via
sale.order.line.fp_customer_description() (strips the
"[code] product" prefix Odoo re-prepends).
2. Any product line on the linked sale order.
Returns '' when no order context exists; the report template
then falls back to process_description. All cross-module field
access is guarded so the method stays safe even if the jobs /
configurator layers aren't installed.
"""
self.ensure_one()
job = self.x_fc_job_id if 'x_fc_job_id' in self._fields else False
so = self.sale_order_id or (
job.sale_order_id
if job and 'sale_order_id' in job._fields else False
)
if not so:
return ''
lines = so.order_line.filtered(lambda l: not l.display_type)
if not lines:
return ''
# Prefer the line whose part matches this cert's job.
part = (job.part_catalog_id
if job and 'part_catalog_id' in job._fields else False)
line = self.env['sale.order.line']
if part and 'x_fc_part_catalog_id' in lines._fields:
line = lines.filtered(
lambda l: l.x_fc_part_catalog_id == part
)[:1]
if not line:
line = lines[:1]
if not line:
return ''
if hasattr(line, 'fp_customer_description'):
desc = line.fp_customer_description()
else:
desc = line.name
return (desc or '').strip()
def _fp_render_and_attach_pdf(self):
"""Render the CoC PDF via the bound report action, OPTIONALLY
merge the Fischerscope thickness report PDF (uploaded by the

View File

@@ -55,13 +55,16 @@ class TestActionIssueGates(TransactionCase):
vals.update(kw)
return self.env['fp.certificate'].create(vals)
# ---- the existing gate still works (spec_reference) ----
# ---- spec_reference is OPTIONAL (client request 2026-05-28) ----
def test_blocks_on_missing_spec_reference(self):
def test_no_block_on_missing_spec_reference(self):
"""Spec Reference no longer gates issuance — the customer-facing
description now serves as the cert's spec/certificate info. A
cert with everything else present must issue even with a blank
spec_reference."""
cert = self._make_cert(spec_reference=False)
with self.assertRaises(UserError) as exc:
cert.action_issue()
self.assertIn('Spec Reference', str(exc.exception))
cert.action_issue()
self.assertEqual(cert.state, 'issued')
# ---- new gate: process_description ----
@@ -100,10 +103,12 @@ class TestActionIssueGates(TransactionCase):
cert.action_issue()
self.assertEqual(cert.state, 'issued')
# ---- order: spec_reference still wins (cheapest first) ----
# ---- order: process_description is now the first gate ----
def test_gate_order_spec_reference_first(self):
# Multiple missing → spec_reference message surfaces first.
def test_gate_order_process_description_first(self):
# spec_reference no longer gates, so with everything missing the
# process_description message surfaces first; the (removed) Spec
# Reference message must NOT appear.
cert = self._make_cert(
spec_reference=False,
process_description=False,
@@ -112,9 +117,8 @@ class TestActionIssueGates(TransactionCase):
)
with self.assertRaises(UserError) as exc:
cert.action_issue()
self.assertIn('Spec Reference', str(exc.exception))
# And NOT the process_description message (gate hit first).
self.assertNotIn('Process Description', str(exc.exception))
self.assertIn('Process Description', str(exc.exception))
self.assertNotIn('Spec Reference', str(exc.exception))
# ---- new gate: thickness_report cert needs thickness data ----

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
{
'name': 'Fusion Plating — Native Jobs',
'version': '19.0.11.1.0',
'version': '19.0.11.2.0',
'category': 'Manufacturing/Plating',
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
'author': 'Nexa Systems Inc.',

View File

@@ -48,20 +48,22 @@
<field name="needs_thickness" readonly="1"
widget="boolean_toggle"
string="Needs Thickness"/>
<!-- Upload column. Visible/required only when
the cert needs thickness data. Triggers
the @onchange-driven .docx parser. -->
<!-- Upload column. Available on EVERY cert so a
thickness report can be attached to any CoC
and pulled onto the printed cert (client
request 2026-05-28). needs_thickness still
drives the "Needs Thickness" badge + the
is_ready blocking gate below. Triggers the
@onchange-driven .docx/RTF parser. -->
<field name="fischer_filename" column_invisible="1"/>
<field name="fischer_file"
filename="fischer_filename"
widget="binary"
string="Fischerscope File (PDF or .docx)"
invisible="not needs_thickness"
readonly="not needs_thickness"/>
string="Fischerscope File (PDF or .docx)"/>
<field name="parsed_summary" readonly="1"
string="Parsed"
optional="show"
invisible="not needs_thickness or not parsed_summary"/>
invisible="not parsed_summary"/>
<field name="is_ready" widget="boolean_toggle"
readonly="1"
string="Ready"
@@ -86,15 +88,13 @@
widget="boolean_toggle"/>
</group>
</group>
<group string="Fischerscope File"
invisible="not needs_thickness">
<group string="Fischerscope File">
<field name="fischer_file"
filename="fischer_filename"/>
<field name="fischer_filename"
invisible="1"/>
</group>
<group string="Measurement Image (Optional)"
invisible="not needs_thickness">
<group string="Measurement Image (Optional)">
<field name="fischer_image_file"
filename="fischer_image_filename"
widget="image"
@@ -112,20 +112,17 @@
</group>
<div class="alert alert-info"
role="alert"
invisible="not needs_thickness or not parsed_summary">
invisible="not parsed_summary">
<i class="fa fa-check-circle me-1"/>
<field name="parsed_summary"
readonly="1" nolabel="1"/>
</div>
<separator string="Thickness Readings"
invisible="not needs_thickness"/>
<p class="text-muted small"
invisible="not needs_thickness">
<separator string="Thickness Readings"/>
<p class="text-muted small">
Auto-filled from the .docx upload above.
Edit/add rows manually as needed.
</p>
<field name="reading_line_ids"
invisible="not needs_thickness">
<field name="reading_line_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="nip_mils"/>

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">