This commit is contained in:
gsinghpal
2026-05-21 21:00:10 -04:00
parent d022e529d9
commit d127e19b45
11 changed files with 699 additions and 148 deletions

View File

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

View File

@@ -90,7 +90,12 @@
<t t-set="_desc"
t-value="line.fp_customer_description() if _has_helper else line.name"/>
<span t-esc="_desc" style="white-space: pre-line;"/>
<t t-if="'x_fc_serial_id' in line._fields and line.x_fc_serial_id">
<!-- Serial line is suppressed when the calling template sets
`fp_no_serial_in_desc=True` in `line.env.context`. SO
portrait does this because it shows the serial in its own
column in the part-number cell — otherwise it'd be
duplicated. Invoice / packing slip still display it here. -->
<t t-if="'x_fc_serial_id' in line._fields and line.x_fc_serial_id and not line.env.context.get('fp_no_serial_in_desc')">
<br/>
<small>Serial: <span t-esc="line.x_fc_serial_id.name"/></small>
</t>

View File

@@ -73,6 +73,26 @@
<field name="dpi">90</field>
</record>
<!-- ============================================================= -->
<!-- Compact A4 Landscape for customer-facing landscape reports. -->
<!-- Same shape as `paperformat_fp_a4_portrait` (margin_top=8, -->
<!-- header_spacing=0) just rotated. Bind alongside the portrait -->
<!-- compact for any report that has a landscape variant. -->
<!-- ============================================================= -->
<record id="paperformat_fp_a4_landscape_compact" model="report.paperformat">
<field name="name">Fusion Plating A4 Landscape (Compact)</field>
<field name="default" eval="False"/>
<field name="format">A4</field>
<field name="orientation">Landscape</field>
<field name="margin_top">8</field>
<field name="margin_bottom">15</field>
<field name="margin_left">10</field>
<field name="margin_right">10</field>
<field name="header_line" eval="False"/>
<field name="header_spacing">0</field>
<field name="dpi">90</field>
</record>
<!-- ============================================================= -->
<!-- 1. Certificate of Conformance (Portal Job) — Landscape -->
<!-- ============================================================= -->
@@ -451,6 +471,11 @@
<field name="binding_model_id" ref="account.model_account_move"/>
<field name="binding_type">report</field>
<field name="is_invoice_report" eval="True"/>
<!-- Same compact paperformat as the SO confirmation so the
inline custom header sits at the top of the page (not 40mm
down under Odoo's default margin). See CLAUDE.md
"wkhtmltopdf header overlap — the CoC pattern". -->
<field name="paperformat_id" ref="paperformat_fp_a4_portrait"/>
</record>
<record id="action_report_fp_invoice_landscape" model="ir.actions.report">
@@ -462,7 +487,10 @@
<field name="print_report_name">'Invoice - %s' % (object.name or '')</field>
<field name="binding_model_id" ref="account.model_account_move"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
<!-- Compact landscape paperformat (same shape as the portrait
compact, just rotated) so the inline header lands at the
top with no auto-margin gap. -->
<field name="paperformat_id" ref="paperformat_fp_a4_landscape_compact"/>
</record>
<!-- ============================================================= -->

View File

@@ -14,26 +14,92 @@
<template id="report_fp_invoice_portrait">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
<t t-call="fusion_plating_reports.fp_portrait_styles"/>
<div class="fp-report">
<div class="page">
<t t-set="form_code" t-value="'FRM-007'"/>
<t t-call="fusion_plating_reports.fp_external_layout_clean">
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
<t t-set="company" t-value="doc.company_id or env.company"/>
<t t-call="fusion_plating_reports.fp_portrait_styles"/>
<t t-call="fusion_plating_reports.fp_sale_bilingual_styles"/>
<h4>
<span t-if="doc.move_type == 'out_invoice' and doc.state == 'posted'">Invoice # </span>
<span t-elif="doc.move_type == 'out_invoice' and doc.state == 'draft'">Draft Invoice # </span>
<span t-elif="doc.move_type == 'out_refund'">Credit Note # </span>
<span t-elif="doc.move_type == 'in_invoice'">Vendor Bill # </span>
<span t-field="doc.name"/>
</h4>
<!-- Compute helpers -->
<t t-set="title_en" t-value="
'Credit Note' if doc.move_type == 'out_refund'
else 'Vendor Bill' if doc.move_type == 'in_invoice'
else 'Draft Invoice' if (doc.move_type == 'out_invoice' and doc.state == 'draft')
else 'Invoice'"/>
<t t-set="title_fr" t-value="
'Note de crédit' if doc.move_type == 'out_refund'
else 'Facture fournisseur' if doc.move_type == 'in_invoice'
else 'Facture brouillon' if (doc.move_type == 'out_invoice' and doc.state == 'draft')
else 'Facture'"/>
<t t-set="barcode_uri" t-value="doc.env['ir.actions.report'].sudo().barcode_data_uri('Code128', doc.name, 600, 100) if doc.name and doc.name != '/' else False"/>
<!-- Pull FP-specific fields from the source SO when invoice_origin
names one. Falls back to an empty recordset on manual
invoices so the t-if guards in the markup stay clean. -->
<t t-set="source_so" t-value="doc.env['sale.order'].search([('name', '=', doc.invoice_origin)], limit=1) if doc.invoice_origin else doc.env['sale.order'].browse()"/>
<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="po_number" t-value="(source_so.x_fc_po_number if source_so else False) or doc.invoice_origin or ''"/>
<t t-set="spec_label" t-value="(source_so.x_fc_customer_spec_id.display_name or source_so.x_fc_customer_spec_id.name) if source_so and source_so.x_fc_customer_spec_id else ''"/>
<t t-set="delivery_method_label" t-value="dict(source_so._fields['x_fc_delivery_method'].selection).get(source_so.x_fc_delivery_method, '') if source_so and source_so.x_fc_delivery_method else ''"/>
<div class="fp-report fp-sale">
<!-- Inline 3-column header: logo+address LEFT,
NADCAP MIDDLE, title+barcode RIGHT.
Mirrors SO confirmation exactly. -->
<div class="fp-sale-header-row">
<div class="fp-sale-header-left">
<t t-if="logo_uri">
<img t-att-src="logo_uri" class="fp-sale-logo" alt="Logo"/>
</t>
<div class="fp-sale-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>
<div class="fp-sale-header-mid">
<t t-if="company.x_fc_nadcap_active and company.x_fc_nadcap_logo">
<img class="fp-nadcap-logo"
t-att-src="'data:image/png;base64,%s' % company.x_fc_nadcap_logo.decode()"
alt="Nadcap Accredited"/>
</t>
</div>
<div class="fp-sale-header-right">
<span class="fp-sale-title-en"><t t-esc="title_en"/></span>
<span class="fp-sale-title-fr"><t t-esc="title_fr"/></span>
<t t-if="barcode_uri">
<div class="fp-bc-wrap" style="margin-top: 4px;">
<img t-att-src="barcode_uri" alt="Invoice Barcode"/>
<div class="fp-bc-label"><span t-field="doc.name"/></div>
</div>
</t>
</div>
</div>
<div class="page">
<!-- Billing / Shipping -->
<table class="bordered">
<thead>
<tr>
<th style="width: 50%;">BILLING ADDRESS</th>
<th style="width: 50%;">DELIVERY ADDRESS</th>
<th style="width: 50%;">
<span class="fp-bl-en">Billing Address</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Adresse de facturation</span>
</th>
<th style="width: 50%;">
<span class="fp-bl-en">Delivery Address</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Adresse de livraison</span>
</th>
</tr>
</thead>
<tbody>
@@ -56,53 +122,131 @@
</tbody>
</table>
<!-- Invoice info -->
<!-- Row 1: Invoice Date | Due Date | Sales Rep | Customer PO # | Payment Ref -->
<table class="bordered">
<thead>
<tr>
<th class="info-header" style="width: 25%;">INVOICE DATE</th>
<th class="info-header" style="width: 25%;">DUE DATE</th>
<th class="info-header" style="width: 25%;">SOURCE</th>
<th class="info-header" style="width: 25%;">SALES REP</th>
<th class="info-header" style="width: 20%;">
<span class="fp-bl-en-stk">Invoice Date</span>
<span class="fp-bl-fr-stk">Date de facture</span>
</th>
<th class="info-header" style="width: 20%;">
<span class="fp-bl-en-stk">Due Date</span>
<span class="fp-bl-fr-stk">Date d'échéance</span>
</th>
<th class="info-header" style="width: 20%;">
<span class="fp-bl-en-stk">Sales Rep</span>
<span class="fp-bl-fr-stk">Vendeur</span>
</th>
<th class="info-header" style="width: 20%;">
<span class="fp-bl-en-stk">Customer PO #</span>
<span class="fp-bl-fr-stk">N° de B/C client</span>
</th>
<th class="info-header" style="width: 20%;">
<span class="fp-bl-en-stk">Payment Ref</span>
<span class="fp-bl-fr-stk">Réf. paiement</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-field="doc.invoice_date"/></td>
<td class="text-center"><span t-field="doc.invoice_date_due"/></td>
<td class="text-center"><span t-field="doc.invoice_origin"/></td>
<td class="text-center"><span t-field="doc.invoice_user_id"/></td>
<td class="text-center"><span t-esc="po_number or '—'"/></td>
<td class="text-center"><span t-esc="doc.payment_reference or '—'"/></td>
</tr>
</tbody>
</table>
<!-- Lines -->
<!-- Row 2: Customer Job # | Specification | Delivery Method (pulled from source SO; hidden on manual invoices). -->
<t t-if="source_so and (source_so.x_fc_customer_job_number or spec_label or delivery_method_label)">
<table class="bordered">
<thead>
<tr>
<th class="info-header" style="width: 34%;">
<span class="fp-bl-en">Customer Job #</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">N° de travail client</span>
</th>
<th class="info-header" style="width: 33%;">
<span class="fp-bl-en">Specification</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Spécification</span>
</th>
<th class="info-header" style="width: 33%;">
<span class="fp-bl-en">Delivery Method</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Méthode de livraison</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-esc="source_so.x_fc_customer_job_number or '—'"/></td>
<td class="text-center"><span t-esc="spec_label or '—'"/></td>
<td class="text-center"><span t-esc="delivery_method_label or '—'"/></td>
</tr>
</tbody>
</table>
</t>
<!-- Lines (taxes column dropped; summarized in totals) -->
<table class="bordered">
<thead>
<tr>
<th class="text-start" style="width: 20%;">PART NUMBER</th>
<th class="text-start" style="width: 32%;">DESCRIPTION</th>
<th style="width: 8%;">QTY</th>
<th style="width: 8%;">UOM</th>
<th style="width: 12%;">UNIT PRICE</th>
<th style="width: 8%;">TAXES</th>
<th style="width: 12%;">AMOUNT</th>
<th class="text-start" style="width: 24%;">
<span class="fp-bl-en">Part Number</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">N° de pièce</span>
</th>
<th class="text-start" style="width: 38%;">
<span class="fp-bl-en">Description</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Description</span>
</th>
<th style="width: 8%;">
<span class="fp-bl-en-stk">Qty</span>
<span class="fp-bl-fr-stk">Qté</span>
</th>
<th style="width: 8%;">
<span class="fp-bl-en-stk">UOM</span>
<span class="fp-bl-fr-stk">UDM</span>
</th>
<th style="width: 11%;">
<span class="fp-bl-en-stk">Unit Price</span>
<span class="fp-bl-fr-stk">Prix unitaire</span>
</th>
<th style="width: 11%;">
<span class="fp-bl-en-stk">Amount</span>
<span class="fp-bl-fr-stk">Montant</span>
</th>
</tr>
</thead>
<tbody>
<t t-foreach="doc.invoice_line_ids" t-as="line">
<t t-if="line.display_type == 'line_section'">
<tr class="section-row"><td colspan="7"><strong t-field="line.name"/></td></tr>
<tr class="section-row"><td colspan="6"><strong t-field="line.name"/></td></tr>
</t>
<t t-elif="line.display_type == 'line_note'">
<tr class="note-row"><td colspan="7"><span t-field="line.name"/></td></tr>
<tr class="note-row"><td colspan="6"><span t-field="line.name"/></td></tr>
</t>
<t t-elif="not line.display_type or line.display_type == 'product'">
<tr>
<td>
<t t-call="fusion_plating_reports.customer_line_part_number"/>
<!-- Three stacked lines: Part #, Name, S/N -->
<div>
<strong>Part #:</strong>
<t t-call="fusion_plating_reports.customer_line_part_number"/>
</div>
<div>
<strong>Name:</strong>
<t t-if="line.x_fc_part_catalog_id and line.x_fc_part_catalog_id.name">
<span t-esc="line.x_fc_part_catalog_id.name"/>
</t>
<t t-else=""></t>
</div>
<div>
<strong>S/N:</strong>
<t t-if="'x_fc_serial_id' in line._fields and line.x_fc_serial_id">
<span t-esc="line.x_fc_serial_id.name"/>
</t>
<t t-else=""></t>
</div>
</td>
<td>
<!-- Suppress duplicate serial in the description column -->
<t t-set="line" t-value="line.with_context(fp_no_serial_in_desc=True)"/>
<t t-call="fusion_plating_reports.customer_line_description"/>
</td>
<td class="text-center">
@@ -112,9 +256,6 @@
<td class="text-end">
<span t-field="line.price_unit" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
<td class="text-center">
<t t-esc="', '.join([(tax.invoice_label or tax.name) for tax in line.tax_ids]) or '-'"/>
</td>
<td class="text-end">
<span t-field="line.price_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
@@ -127,40 +268,47 @@
<!-- Terms + Totals -->
<div class="row" style="margin-top: 15px;">
<div class="col-6">
<t t-if="doc.invoice_payment_term_id.note">
<strong>Payment Terms:</strong><br/>
<span t-field="doc.invoice_payment_term_id.note"/>
</t>
<t t-if="doc.payment_reference">
<div style="margin-top: 10px;">
<strong>Payment Reference:</strong>
<span t-field="doc.payment_reference"/>
</div>
<t t-if="doc.invoice_payment_term_id">
<strong>Payment Terms / Modalités de paiement:</strong><br/>
<t t-if="doc.invoice_payment_term_id.note">
<span t-field="doc.invoice_payment_term_id.note"/>
</t>
<t t-else="">
<span t-field="doc.invoice_payment_term_id.name"/>
</t>
</t>
</div>
<div class="col-6" style="text-align: right;">
<table class="totals-table" style="width: auto; margin-left: auto;">
<tr>
<td style="min-width: 150px;">Subtotal</td>
<td style="min-width: 150px;">
<span class="fp-bl-en">Subtotal</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Sous-total</span>
</td>
<td class="text-end" style="min-width: 110px;">
<span t-field="doc.amount_untaxed" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
<tr>
<td>Taxes</td>
<td>
<span class="fp-bl-en">Taxes</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Taxes</span>
</td>
<td class="text-end">
<span t-field="doc.amount_tax" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
<tr style="background-color: #c1c1c1;">
<td><strong>Grand Total</strong></td>
<td>
<span class="fp-bl-en">Grand Total</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Total général</span>
</td>
<td class="text-end"><strong>
<span t-field="doc.amount_total" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</strong></td>
</tr>
<t t-if="doc.amount_residual and doc.amount_residual != doc.amount_total">
<tr>
<td><strong>Amount Due</strong></td>
<td>
<strong><span class="fp-bl-en">Amount Due</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Montant dû</span></strong>
</td>
<td class="text-end"><strong>
<span t-field="doc.amount_residual" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</strong></td>
@@ -173,14 +321,14 @@
<!-- Paid stamp -->
<t t-if="doc.payment_state in ('paid', 'in_payment')">
<div style="margin-top: 15px; text-align: center;">
<span class="paid-stamp">PAID</span>
<span class="paid-stamp">PAID / PAYÉ</span>
</div>
</t>
<!-- Notes -->
<t t-if="doc.narration">
<div style="margin-top: 15px;">
<strong>Notes:</strong>
<strong>Notes / Remarques:</strong>
<div t-field="doc.narration"/>
</div>
</t>
@@ -198,26 +346,87 @@
<template id="report_fp_invoice_landscape">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<div class="fp-landscape">
<div class="page">
<t t-set="form_code" t-value="'FRM-007'"/>
<t t-call="fusion_plating_reports.fp_external_layout_clean">
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
<t t-set="company" t-value="doc.company_id or env.company"/>
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<t t-call="fusion_plating_reports.fp_sale_bilingual_styles"/>
<h2 style="text-align: left;">
<span t-if="doc.move_type == 'out_invoice' and doc.state == 'posted'">Invoice # </span>
<span t-elif="doc.move_type == 'out_invoice' and doc.state == 'draft'">Draft Invoice # </span>
<span t-elif="doc.move_type == 'out_refund'">Credit Note # </span>
<span t-elif="doc.move_type == 'in_invoice'">Vendor Bill # </span>
<span t-field="doc.name"/>
</h2>
<!-- Same compute helpers as portrait -->
<t t-set="title_en" t-value="
'Credit Note' if doc.move_type == 'out_refund'
else 'Vendor Bill' if doc.move_type == 'in_invoice'
else 'Draft Invoice' if (doc.move_type == 'out_invoice' and doc.state == 'draft')
else 'Invoice'"/>
<t t-set="title_fr" t-value="
'Note de crédit' if doc.move_type == 'out_refund'
else 'Facture fournisseur' if doc.move_type == 'in_invoice'
else 'Facture brouillon' if (doc.move_type == 'out_invoice' and doc.state == 'draft')
else 'Facture'"/>
<t t-set="barcode_uri" t-value="doc.env['ir.actions.report'].sudo().barcode_data_uri('Code128', doc.name, 600, 100) if doc.name and doc.name != '/' else False"/>
<t t-set="source_so" t-value="doc.env['sale.order'].search([('name', '=', doc.invoice_origin)], limit=1) if doc.invoice_origin else doc.env['sale.order'].browse()"/>
<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="po_number" t-value="(source_so.x_fc_po_number if source_so else False) or doc.invoice_origin or ''"/>
<t t-set="spec_label" t-value="(source_so.x_fc_customer_spec_id.display_name or source_so.x_fc_customer_spec_id.name) if source_so and source_so.x_fc_customer_spec_id else ''"/>
<t t-set="delivery_method_label" t-value="dict(source_so._fields['x_fc_delivery_method'].selection).get(source_so.x_fc_delivery_method, '') if source_so and source_so.x_fc_delivery_method else ''"/>
<div class="fp-landscape fp-sale">
<!-- 3-column inline header — same as portrait -->
<div class="fp-sale-header-row">
<div class="fp-sale-header-left">
<t t-if="logo_uri">
<img t-att-src="logo_uri" class="fp-sale-logo" alt="Logo"/>
</t>
<div class="fp-sale-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>
<div class="fp-sale-header-mid">
<t t-if="company.x_fc_nadcap_active and company.x_fc_nadcap_logo">
<img class="fp-nadcap-logo"
t-att-src="'data:image/png;base64,%s' % company.x_fc_nadcap_logo.decode()"
alt="Nadcap Accredited"/>
</t>
</div>
<div class="fp-sale-header-right">
<span class="fp-sale-title-en"><t t-esc="title_en"/></span>
<span class="fp-sale-title-fr"><t t-esc="title_fr"/></span>
<t t-if="barcode_uri">
<div class="fp-bc-wrap" style="margin-top: 4px;">
<img t-att-src="barcode_uri" alt="Invoice Barcode"/>
<div class="fp-bc-label"><span t-field="doc.name"/></div>
</div>
</t>
</div>
</div>
<div class="page">
<!-- Billing / Shipping -->
<table class="bordered">
<thead>
<tr>
<th style="width: 50%;">BILLING ADDRESS</th>
<th style="width: 50%;">DELIVERY ADDRESS</th>
<th style="width: 50%;">
<span class="fp-bl-en">Billing Address</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Adresse de facturation</span>
</th>
<th style="width: 50%;">
<span class="fp-bl-en">Delivery Address</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Adresse de livraison</span>
</th>
</tr>
</thead>
<tbody>
@@ -240,44 +449,106 @@
</tbody>
</table>
<!-- Invoice info (wide) -->
<!-- Invoice info row (wide, 6 cols on landscape) -->
<table class="bordered info-table">
<thead>
<tr>
<th>INVOICE DATE</th>
<th>DUE DATE</th>
<th>SOURCE</th>
<th>SALES REP</th>
<th>PAYMENT REF</th>
<th>CURRENCY</th>
<th>
<span class="fp-bl-en-stk">Invoice Date</span>
<span class="fp-bl-fr-stk">Date de facture</span>
</th>
<th>
<span class="fp-bl-en-stk">Due Date</span>
<span class="fp-bl-fr-stk">Date d'échéance</span>
</th>
<th>
<span class="fp-bl-en-stk">Sales Rep</span>
<span class="fp-bl-fr-stk">Vendeur</span>
</th>
<th>
<span class="fp-bl-en-stk">Customer PO #</span>
<span class="fp-bl-fr-stk">N° de B/C client</span>
</th>
<th>
<span class="fp-bl-en-stk">Payment Ref</span>
<span class="fp-bl-fr-stk">Réf. paiement</span>
</th>
<th>
<span class="fp-bl-en-stk">Currency</span>
<span class="fp-bl-fr-stk">Devise</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-field="doc.invoice_date"/></td>
<td class="text-center"><span t-field="doc.invoice_date_due"/></td>
<td class="text-center"><span t-field="doc.invoice_origin"/></td>
<td class="text-center"><span t-field="doc.invoice_user_id"/></td>
<td class="text-center"><span t-esc="doc.payment_reference or '-'"/></td>
<td class="text-center"><span t-esc="po_number or ''"/></td>
<td class="text-center"><span t-esc="doc.payment_reference or '—'"/></td>
<td class="text-center"><span t-field="doc.currency_id.name"/></td>
</tr>
</tbody>
</table>
<!-- Lines — hide discount column unless at least one line has a discount -->
<!-- Source SO row (wider, 3 cols, inline). Hidden on manual invoices. -->
<t t-if="source_so and (source_so.x_fc_customer_job_number or spec_label or delivery_method_label)">
<table class="bordered info-table">
<thead>
<tr>
<th>
<span class="fp-bl-en">Customer Job #</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">N° de travail client</span>
</th>
<th>
<span class="fp-bl-en">Specification</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Spécification</span>
</th>
<th>
<span class="fp-bl-en">Delivery Method</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Méthode de livraison</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-esc="source_so.x_fc_customer_job_number or '—'"/></td>
<td class="text-center"><span t-esc="spec_label or '—'"/></td>
<td class="text-center"><span t-esc="delivery_method_label or '—'"/></td>
</tr>
</tbody>
</table>
</t>
<!-- Lines (taxes column dropped; discount column conditional) -->
<t t-set="has_discount" t-value="any(l.discount for l in doc.invoice_line_ids)"/>
<t t-set="col_count" t-value="8 if has_discount else 7"/>
<t t-set="col_count" t-value="7 if has_discount else 6"/>
<table class="bordered">
<thead>
<tr>
<th class="text-start" style="width: 18%;">PART NUMBER</th>
<th class="text-start" style="width: 24%;">DESCRIPTION</th>
<th style="width: 8%;">QTY</th>
<th style="width: 8%;">UOM</th>
<th style="width: 12%;">UNIT PRICE</th>
<th t-if="has_discount" style="width: 10%;">DISCOUNT</th>
<th style="width: 10%;">TAXES</th>
<th style="width: 10%;">AMOUNT</th>
<th class="text-start" style="width: 22%;">
<span class="fp-bl-en">Part Number</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">N° de pièce</span>
</th>
<th class="text-start" style="width: 30%;">
<span class="fp-bl-en">Description</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Description</span>
</th>
<th style="width: 7%;">
<span class="fp-bl-en-stk">Qty</span>
<span class="fp-bl-fr-stk">Qté</span>
</th>
<th style="width: 7%;">
<span class="fp-bl-en-stk">UOM</span>
<span class="fp-bl-fr-stk">UDM</span>
</th>
<th style="width: 12%;">
<span class="fp-bl-en-stk">Unit Price</span>
<span class="fp-bl-fr-stk">Prix unitaire</span>
</th>
<th t-if="has_discount" style="width: 10%;">
<span class="fp-bl-en-stk">Discount</span>
<span class="fp-bl-fr-stk">Remise</span>
</th>
<th style="width: 12%;">
<span class="fp-bl-en-stk">Amount</span>
<span class="fp-bl-fr-stk">Montant</span>
</th>
</tr>
</thead>
<tbody>
@@ -291,9 +562,28 @@
<t t-elif="not line.display_type or line.display_type == 'product'">
<tr>
<td>
<t t-call="fusion_plating_reports.customer_line_part_number"/>
<!-- Three stacked lines: Part #, Name, S/N -->
<div>
<strong>Part #:</strong>
<t t-call="fusion_plating_reports.customer_line_part_number"/>
</div>
<div>
<strong>Name:</strong>
<t t-if="line.x_fc_part_catalog_id and line.x_fc_part_catalog_id.name">
<span t-esc="line.x_fc_part_catalog_id.name"/>
</t>
<t t-else=""></t>
</div>
<div>
<strong>S/N:</strong>
<t t-if="'x_fc_serial_id' in line._fields and line.x_fc_serial_id">
<span t-esc="line.x_fc_serial_id.name"/>
</t>
<t t-else=""></t>
</div>
</td>
<td>
<t t-set="line" t-value="line.with_context(fp_no_serial_in_desc=True)"/>
<t t-call="fusion_plating_reports.customer_line_description"/>
</td>
<td class="text-center">
@@ -305,10 +595,7 @@
</td>
<td t-if="has_discount" class="text-center">
<t t-if="line.discount"><span t-esc="line.discount"/>%</t>
<t t-else="">-</t>
</td>
<td class="text-center">
<t t-esc="', '.join([(tax.invoice_label or tax.name) for tax in line.tax_ids]) or '-'"/>
<t t-else=""></t>
</td>
<td class="text-end">
<span t-field="line.price_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
@@ -322,34 +609,47 @@
<!-- Terms + Totals -->
<div class="row" style="margin-top: 15px;">
<div class="col-7">
<t t-if="doc.invoice_payment_term_id.note">
<strong>Payment Terms:</strong><br/>
<span t-field="doc.invoice_payment_term_id.note"/>
<t t-if="doc.invoice_payment_term_id">
<strong>Payment Terms / Modalités de paiement:</strong><br/>
<t t-if="doc.invoice_payment_term_id.note">
<span t-field="doc.invoice_payment_term_id.note"/>
</t>
<t t-else="">
<span t-field="doc.invoice_payment_term_id.name"/>
</t>
</t>
</div>
<div class="col-5" style="text-align: right;">
<table class="totals-table" style="width: auto; margin-left: auto;">
<tr>
<td style="min-width: 200px;">Subtotal</td>
<td style="min-width: 200px;">
<span class="fp-bl-en">Subtotal</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Sous-total</span>
</td>
<td class="text-end" style="min-width: 150px;">
<span t-field="doc.amount_untaxed" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
<tr>
<td>Taxes</td>
<td>
<span class="fp-bl-en">Taxes</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Taxes</span>
</td>
<td class="text-end">
<span t-field="doc.amount_tax" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
<tr style="background-color: #c1c1c1;">
<td><strong>Grand Total</strong></td>
<td>
<span class="fp-bl-en">Grand Total</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Total général</span>
</td>
<td class="text-end"><strong>
<span t-field="doc.amount_total" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</strong></td>
</tr>
<t t-if="doc.amount_residual and doc.amount_residual != doc.amount_total">
<tr>
<td><strong>Amount Due</strong></td>
<td>
<strong><span class="fp-bl-en">Amount Due</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Montant dû</span></strong>
</td>
<td class="text-end"><strong>
<span t-field="doc.amount_residual" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</strong></td>
@@ -362,7 +662,7 @@
<!-- Paid stamp -->
<t t-if="doc.payment_state in ('paid', 'in_payment')">
<div style="margin-top: 15px; text-align: center;">
<span class="paid-stamp">PAID</span>
<span class="paid-stamp">PAID / PAYÉ</span>
</div>
</t>

View File

@@ -22,6 +22,46 @@
External_layout already places the page body at the bottom of
the reserved margin-top — don't fight that. Use a small positive
gap and shrink the title text instead. -->
<!-- Custom minimal layout — same .article wrapper that Odoo's
report pipeline expects (so UTF-8 charset handling works
correctly via the standard processing path), but with NO
auto company .header div. Includes a minimal .footer div
that ONLY carries the wkhtmltopdf page-number placeholders
(`<span class="page"/> / <span class="topage"/>`) — those
only get filled in when the .footer div is extracted into
wkhtmltopdf's footer-html stream. The .footer is otherwise
empty so no boilerplate company info shows. -->
<template id="fp_external_layout_clean">
<t t-if="not o" t-set="o" t-value="doc"/>
<t t-if="not company">
<t t-if="company_id">
<t t-set="company" t-value="company_id"/>
</t>
<t t-elif="o and 'company_id' in o and o.company_id.sudo()">
<t t-set="company" t-value="o.company_id.sudo()"/>
</t>
<t t-else="else">
<t t-set="company" t-value="res_company"/>
</t>
</t>
<div class="article o_report_layout_standard"
t-att-data-oe-model="o and o._name"
t-att-data-oe-id="o and o.id"
t-att-data-oe-lang="o and o.env.context.get('lang')">
<t t-out="0"/>
</div>
<div class="footer">
<div style="font-size: 9pt; color: #666; overflow: hidden;">
<!-- Internal form code (e.g. "FRM-006") on the left.
Each report sets `form_code` via t-set BEFORE the
t-call to this layout; reports that don't set it
leave the left side blank. -->
<span style="float: left;" t-if="form_code"><t t-esc="form_code"/></span>
<span style="float: right; white-space: nowrap; padding-left: 10px;" t-if="report_type == 'pdf'">Page <span class="page"/> / <span class="topage"/></span>
</div>
</div>
</template>
<template id="fp_sale_bilingual_styles">
<style>
/* Inline bilingual: English bold, then a faint slash, then
@@ -38,13 +78,58 @@
below in italic-grey. */
.fp-bl-en-stk { display: block; font-weight: bold; }
.fp-bl-fr-stk { display: block; font-weight: normal; font-style: italic; color: #555; font-size: 80%; margin-top: 1px; }
/* Match the CoC pattern exactly: tiny paperformat margin_top
(8mm) lets the header HTML overflow into the body area,
and this 20mm padding-top on the wrapper clears it. See
CLAUDE.md "wkhtmltopdf header overlap" — the alternative
"size margin_top to the header height" approach has zero
slack and breaks any time the header HTML grows. */
.fp-report.fp-sale { padding-top: 20mm; }
/* This template uses fp_external_layout_clean (sibling
template in this file) instead of web.external_layout.
That gives us the `.article` wrapper Odoo's report
renderer needs for proper UTF-8 dispatch, WITHOUT the
`.header` / `.footer` divs that wkhtmltopdf would
extract into page-margin streams. So the body owns the
entire visible header (logo + address LEFT, title +
barcode RIGHT) and no auto company band shows up. See
CLAUDE.md "Custom-header reports need .article wrapper". */
.fp-report.fp-sale { padding-top: 0; }
/* Custom inline header: 2-column flex-via-floats. Left has
the company logo + address + phone + URL; right has the
document title (bilingual stack) + Code128 barcode. */
.fp-sale-header-row { overflow: hidden; margin-bottom: 14px; padding-bottom: 6px; }
.fp-sale-header-left { float: left; width: 38%; }
/* Middle column: NADCAP accreditation logo (pulled from
company settings) above a small "25 Years in Business"
medallion. Conditional on company.x_fc_nadcap_active so
non-Nadcap shops only see the badge. */
.fp-sale-header-mid { float: left; width: 24%; text-align: center; padding-top: 4px; }
.fp-nadcap-logo { max-height: 45px; max-width: 115px; display: inline-block; }
/* 25-Years-in-Business medallion: tasteful gold-on-cream
bordered badge. Picks a dark muted gold (#8a6d2c) so it
reads as "anniversary keepsake" rather than gaudy. */
.fp-25-badge {
display: inline-block;
border: 1.5px solid #c8a55b;
background-color: #faf5e8;
padding: 5px 12px;
border-radius: 4px;
margin-top: 6px;
text-align: center;
line-height: 1.05;
}
.fp-25-badge .fp-25-num { font-size: 22pt; font-weight: bold; color: #8a6d2c; }
.fp-25-badge .fp-25-lbl { font-size: 7pt; font-weight: bold; color: #8a6d2c; letter-spacing: 1.2px; margin-top: 1px; }
.fp-25-badge .fp-25-sub { font-size: 6.5pt; color: #8a6d2c; font-style: italic; margin-top: 1px; }
/* Right column: title (English bold + French italic) + barcode
+ SO number all centered as a stacked block. Width reduced
to 38% to share row with the new middle NADCAP+25Years cell. */
.fp-sale-header-right { float: right; width: 38%; text-align: center; }
.fp-sale-logo { max-height: 50px; max-width: 280px; display: block; margin-bottom: 4px; }
.fp-sale-company-addr { font-size: 8.5pt; color: #222; line-height: 1.35; }
.fp-sale-company-addr div { margin: 0; }
.fp-sale-company-addr a { color: #2e6da4; text-decoration: none; }
/* Inline footer line — phone | email | website | tax id.
One-time render at the bottom of page 1 (multi-page SO
reports are rare; if we ever need it, switch to
wkhtmltopdf --footer-html). */
.fp-sale-customfooter { text-align: center; font-size: 8pt; color: #666; margin-top: 24px; padding-top: 8px; border-top: 1px solid #ccc; }
/* Title bar uses float-based div layout, NOT an HTML table —
the global ".fp-report table" rule was applying borders
to every nested table even with "border: 0 !important",
@@ -65,48 +150,83 @@
.fp-sale-barcode { float: right; margin-left: 12px; }
.fp-bc-wrap { display: inline-block; text-align: center; }
.fp-bc-wrap img { height: 48px; max-width: 240px; border: 0 !important; padding: 0; display: block; }
.fp-bc-wrap .fp-bc-label { font-size: 10pt; color: #333; margin-top: 6px; letter-spacing: 0.5px; }
.fp-bc-wrap .fp-bc-label { font-size: 16pt; font-weight: bold; color: #000; margin-top: 6px; letter-spacing: 1.5px; }
</style>
</template>
<template id="report_fp_sale_portrait">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
<t t-call="fusion_plating_reports.fp_portrait_styles"/>
<t t-call="fusion_plating_reports.fp_sale_bilingual_styles"/>
<!-- Internal form code rendered on the footer left side
by fp_external_layout_clean (see that template). -->
<t t-set="form_code" t-value="'FRM-006'"/>
<t t-call="fusion_plating_reports.fp_external_layout_clean">
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
<t t-set="company" t-value="doc.company_id or env.company"/>
<t t-call="fusion_plating_reports.fp_portrait_styles"/>
<t t-call="fusion_plating_reports.fp_sale_bilingual_styles"/>
<!-- Compute helpers -->
<t t-set="is_quote" t-value="doc.state in ('draft', 'sent')"/>
<t t-set="title_en" t-value="'Quotation' if is_quote else 'Order Confirmation'"/>
<t t-set="title_fr" t-value="'Devis' if is_quote else 'Confirmation de commande'"/>
<t t-set="barcode_uri" t-value="doc.env['ir.actions.report'].sudo().barcode_data_uri('Code128', doc.name, 600, 100) if doc.name else False"/>
<t t-set="spec_label" t-value="(doc.x_fc_customer_spec_id.display_name or doc.x_fc_customer_spec_id.name) if doc.x_fc_customer_spec_id else ''"/>
<t t-set="delivery_method_label" t-value="dict(doc._fields['x_fc_delivery_method'].selection).get(doc.x_fc_delivery_method, '') if 'x_fc_delivery_method' in doc._fields and doc.x_fc_delivery_method else ''"/>
<!-- Compute helpers -->
<t t-set="is_quote" t-value="doc.state in ('draft', 'sent')"/>
<t t-set="title_en" t-value="'Quotation' if is_quote else 'Order Confirmation'"/>
<t t-set="title_fr" t-value="'Devis' if is_quote else 'Confirmation de commande'"/>
<t t-set="barcode_uri" t-value="doc.env['ir.actions.report'].sudo().barcode_data_uri('Code128', doc.name, 600, 100) if doc.name else False"/>
<t t-set="spec_label" t-value="(doc.x_fc_customer_spec_id.display_name or doc.x_fc_customer_spec_id.name) if doc.x_fc_customer_spec_id else ''"/>
<t t-set="delivery_method_label" t-value="dict(doc._fields['x_fc_delivery_method'].selection).get(doc.x_fc_delivery_method, '') if 'x_fc_delivery_method' in doc._fields and doc.x_fc_delivery_method else ''"/>
<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"/>
<div class="fp-report fp-sale">
<div class="page">
<!-- Title bar: stacked English/French title
on the left, Code128 barcode floated
right. NO HTML table — see CLAUDE.md
"wkhtmltopdf header overlap" §2 for why
a table here leaks borders. -->
<div class="fp-sale-titlebar">
<t t-if="barcode_uri">
<div class="fp-sale-barcode">
<div class="fp-bc-wrap">
<img t-att-src="barcode_uri" alt="Order Barcode"/>
<div class="fp-bc-label"><span t-field="doc.name"/></div>
</div>
</div>
</t>
<span class="fp-sale-title-en">
<t t-esc="title_en"/><span class="fp-sale-title-num"># <span t-field="doc.name"/></span>
</span>
<span class="fp-sale-title-fr"><t t-esc="title_fr"/></span>
<div class="fp-report fp-sale">
<!-- Inline header (drops web.external_layout for this
report — see CSS comment for context). Left: logo
+ address + tel/fax + URL. Right: bilingual title
+ Code128 barcode of the order number. -->
<div class="fp-sale-header-row">
<div class="fp-sale-header-left">
<t t-if="logo_uri">
<img t-att-src="logo_uri" class="fp-sale-logo" alt="Logo"/>
</t>
<div class="fp-sale-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>
<!-- Middle column: NADCAP logo only. Base64-inlined
from `company.x_fc_nadcap_logo` so wkhtmltopdf
doesn't have to fetch over HTTP (network calls
fail on entech). -->
<div class="fp-sale-header-mid">
<t t-if="company.x_fc_nadcap_active and company.x_fc_nadcap_logo">
<img class="fp-nadcap-logo"
t-att-src="'data:image/png;base64,%s' % company.x_fc_nadcap_logo.decode()"
alt="Nadcap Accredited"/>
</t>
</div>
<div class="fp-sale-header-right">
<span class="fp-sale-title-en"><t t-esc="title_en"/></span>
<span class="fp-sale-title-fr"><t t-esc="title_fr"/></span>
<t t-if="barcode_uri">
<div class="fp-bc-wrap" style="margin-top: 4px;">
<img t-att-src="barcode_uri" alt="Order Barcode"/>
<div class="fp-bc-label"><span t-field="doc.name"/></div>
</div>
</t>
</div>
</div>
<div class="page">
<!-- Billing / Shipping (wide cells — inline) -->
<table class="bordered">
@@ -174,10 +294,18 @@
<td class="text-center"><span t-field="doc.user_id"/></td>
<td class="text-center"><span t-esc="doc.x_fc_po_number or '—'"/></td>
<td class="text-center">
<t t-if="doc.x_fc_rush_order">
<span class="status-warning">Rush / Urgent</span>
<!-- Lead Time renders from the
computed display string on
sale.order. Rush stays
highlighted; everything
else (range / single value
/ Standard) is plain text. -->
<t t-if="doc.x_fc_lead_time_display == 'Rush'">
<span class="status-warning">Rush</span>
</t>
<t t-else="">
<span t-esc="doc.x_fc_lead_time_display"/>
</t>
<t t-else="">Standard</t>
</td>
</tr>
</tbody>
@@ -267,13 +395,34 @@
<t t-elif="not line.display_type or line.display_type == 'product'">
<tr>
<td>
<t t-call="fusion_plating_reports.customer_line_part_number"/>
<t t-if="line.x_fc_part_catalog_id and line.x_fc_part_catalog_id.name">
<span> - </span>
<span t-esc="line.x_fc_part_catalog_id.name"/>
</t>
<!-- Three stacked lines:
1. Part Number (catalog part_number + revision)
2. Name (catalog name, falls back to em-dash)
3. Serial Number (m2m serials joined, or em-dash) -->
<div>
<strong>Part #:</strong>
<t t-call="fusion_plating_reports.customer_line_part_number"/>
</div>
<div>
<strong>Name:</strong>
<t t-if="line.x_fc_part_catalog_id and line.x_fc_part_catalog_id.name">
<span t-esc="line.x_fc_part_catalog_id.name"/>
</t>
<t t-else=""></t>
</div>
<div>
<strong>S/N:</strong>
<t t-if="line.x_fc_serial_ids">
<span t-esc="', '.join(line.x_fc_serial_ids.mapped('name'))"/>
</t>
<t t-else=""></t>
</div>
</td>
<td>
<!-- Rebind `line` with fp_no_serial_in_desc=True so the
shared description macro skips its Serial line — we
already render S/N in the part-number cell above. -->
<t t-set="line" t-value="line.with_context(fp_no_serial_in_desc=True)"/>
<t t-call="fusion_plating_reports.customer_line_description"/>
</td>
<td class="text-center">