This commit is contained in:
gsinghpal
2026-05-21 04:47:45 -04:00
parent 3440e4b7c6
commit d6d6249857
10 changed files with 610 additions and 301 deletions

View File

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

View File

@@ -41,6 +41,34 @@
<field name="dpi">90</field>
</record>
<!-- ============================================================= -->
<!-- Compact A4 Portrait for customer-facing reports -->
<!-- (SO confirmation, quotation, invoice, packing slip, BoL). -->
<!-- Keeps the external_layout header band (logo + company addr) -->
<!-- but shrinks the reserved zone from Odoo's default ~40mm to -->
<!-- 22mm so the document title sits ~5mm under the logo instead -->
<!-- of 30mm. header_spacing kept at 3mm so the header HTML never -->
<!-- bleeds into body content on a page break. See CLAUDE.md row -->
<!-- "wkhtmltopdf header overlap" for the underlying mechanic. -->
<!-- ============================================================= -->
<record id="paperformat_fp_a4_portrait" model="report.paperformat">
<field name="name">Fusion Plating A4 Portrait (Compact)</field>
<field name="default" eval="False"/>
<field name="format">A4</field>
<field name="orientation">Portrait</field>
<!-- margin_top sized for the standard FP header (ENTECH logo +
2-line company address). Earlier 22mm clipped it — the logo
+ name + address actually need ~28mm. 32mm leaves a small
clean gap before the title. Tighter than Odoo's 40mm default. -->
<field name="margin_top">32</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">3</field>
<field name="dpi">90</field>
</record>
<!-- ============================================================= -->
<!-- 1. Certificate of Conformance (Portal Job) — Landscape -->
<!-- ============================================================= -->
@@ -266,6 +294,14 @@
<field name="print_report_name">(object.state in ('draft', 'sent') and 'Quotation - %s' % object.name) or 'Order - %s' % object.name</field>
<field name="binding_model_id" ref="sale.model_sale_order"/>
<field name="binding_type">report</field>
<!-- Uses Odoo's default paperformat so web.external_layout's
header/footer band gets its reserved space correctly (same
approach as report_coc_en / report_coc_fr). Title spacing
below the header is controlled by `padding-top` on the body
wrapper in report_fp_sale.xml — NOT by a custom paperformat,
since trimming the paperformat margin makes the header HTML
bleed into the body. See CLAUDE.md "wkhtmltopdf header
overlap" for the underlying mechanic. -->
</record>
<record id="action_report_fp_sale_landscape" model="ir.actions.report">

View File

@@ -3,11 +3,150 @@
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Fusion Plating — Packing Slip / Shipping Confirmation (Portrait + Landscape).
Binds to stock.picking. Shows parts, quantities, lot/serial tracking,
and a receiver sign-off.
Binds to stock.picking. Bill-To / Ship-To boxes, bilingual column
headers, Received-By signature block and a QR code for scan-to-sign.
-->
<odoo>
<!-- ============================================================= -->
<!-- Shared bits -->
<!-- ============================================================= -->
<template id="fp_packing_slip_styles">
<style>
.fp-ps-addrtable td { vertical-align: top; padding: 8px 10px; font-size: 10pt; }
.fp-ps-addrtable .fp-ps-addr-label { font-weight: bold; font-size: 9pt; color: #333; text-transform: uppercase; margin-bottom: 4px; }
.fp-ps-info-table th { background-color: #eaeaea; }
.fp-ps-info-table td { text-align: center; font-size: 11pt; padding: 8px; }
.fp-ps-items-table th { font-size: 8.5pt; line-height: 1.1; padding: 4px 4px; }
.fp-ps-items-table th .fp-fr { display: block; font-weight: normal; color: #555; font-size: 7.5pt; }
.fp-ps-items-table td { font-size: 9.5pt; padding: 5px 5px; }
.fp-ps-num { text-align: center; }
.fp-ps-sig-table td { padding: 10px 12px; vertical-align: top; }
.fp-ps-sig-line { border-bottom: 1px solid #000; min-height: 38px; margin-top: 4px; }
.fp-ps-sig-label { font-weight: bold; font-size: 9pt; text-transform: uppercase; color: #333; }
.fp-ps-sig-sub { font-size: 8pt; color: #666; }
.fp-ps-qr-box { text-align: center; padding: 6px; }
.fp-ps-qr-box img { width: 110px; height: 110px; display: inline-block; }
.fp-ps-qr-caption { font-size: 9pt; color: #333; margin-top: 4px; line-height: 1.2; }
.fp-ps-qr-caption .fp-fr { display: block; color: #666; font-size: 8pt; }
</style>
</template>
<!-- Address box content (shared by portrait + landscape) -->
<template id="fp_packing_slip_addr_block">
<div class="fp-ps-addr-label" t-esc="label"/>
<strong><span t-esc="partner.name or ''"/></strong>
<div t-field="partner"
t-options="{'widget': 'contact', 'fields': ['address', 'phone', 'email'], 'no_marker': True}"/>
</template>
<!-- Items table (shared markup; only widths change between layouts) -->
<template id="fp_packing_slip_items">
<table class="bordered fp-ps-items-table">
<thead>
<tr>
<th t-att-style="w_ordered or 'width: 8%;'">
Ordered<span class="fp-fr">Comm.</span>
</th>
<th t-att-style="w_shipped or 'width: 8%;'">
Shipped<span class="fp-fr">EXP</span>
</th>
<th t-att-style="w_bo or 'width: 8%;'">
B/O<span class="fp-fr">À venir</span>
</th>
<th class="text-start" t-att-style="w_part or 'width: 17%;'">
Part Number<span class="fp-fr">N° de pièce</span>
</th>
<th t-att-style="w_po or 'width: 11%;'">
PO<span class="fp-fr">B/C</span>
</th>
<th t-att-style="w_wo or 'width: 11%;'">
WO<span class="fp-fr">B/T</span>
</th>
<th t-att-style="w_process or 'width: 14%;'">
Process<span class="fp-fr">Procédé</span>
</th>
<th class="text-start" t-att-style="w_desc or 'width: 23%;'">
Description
</th>
</tr>
</thead>
<tbody>
<t t-foreach="doc.move_ids_without_package" t-as="move">
<t t-set="line" t-value="move.sale_line_id or move"/>
<t t-set="ordered_qty" t-value="move.product_uom_qty or 0.0"/>
<t t-set="done_qty" t-value="move.quantity or 0.0"/>
<t t-set="bo_qty" t-value="ordered_qty - done_qty if ordered_qty &gt; done_qty else 0.0"/>
<t t-set="wo_job" t-value="doc.env['fp.job'].search([('sale_order_line_ids', 'in', move.sale_line_id.ids)], limit=1) if move.sale_line_id else doc.env['fp.job']"/>
<t t-set="proc_variant" t-value="(move.sale_line_id.x_fc_process_variant_id if move.sale_line_id and 'x_fc_process_variant_id' in move.sale_line_id._fields else False)"/>
<t t-set="proc_label" t-value="(proc_variant.variant_label or proc_variant.name) if proc_variant else ((move.sale_line_id.x_fc_part_catalog_id.default_process_id.variant_label or move.sale_line_id.x_fc_part_catalog_id.default_process_id.name) if move.sale_line_id and move.sale_line_id.x_fc_part_catalog_id and move.sale_line_id.x_fc_part_catalog_id.default_process_id else '')"/>
<tr>
<td class="fp-ps-num">
<span t-esc="int(ordered_qty) if ordered_qty == int(ordered_qty) else ordered_qty"/>
</td>
<td class="fp-ps-num">
<span t-esc="int(done_qty) if done_qty == int(done_qty) else done_qty"/>
</td>
<td class="fp-ps-num">
<span t-esc="int(bo_qty) if bo_qty == int(bo_qty) else bo_qty"/>
</td>
<td>
<t t-call="fusion_plating_reports.customer_line_part_number"/>
</td>
<td class="fp-ps-num">
<span t-esc="po_number or '-'"/>
</td>
<td class="fp-ps-num">
<t t-if="wo_job"><span t-esc="wo_job.name"/></t>
<t t-else="">-</t>
</td>
<td class="fp-ps-num">
<span t-esc="proc_label or '-'"/>
</td>
<td>
<t t-call="fusion_plating_reports.customer_line_description"/>
</td>
</tr>
</t>
</tbody>
</table>
</template>
<!-- Signature + QR strip (shared) -->
<template id="fp_packing_slip_signoff">
<table class="bordered fp-ps-sig-table" style="margin-top: 14px;">
<tbody>
<tr>
<td style="width: 38%;">
<div class="fp-ps-sig-label">
Received By
<span style="font-weight: normal; color: #666; font-size: 8pt;"> / Reçu par</span>
</div>
<div class="fp-ps-sig-line"/>
<div class="fp-ps-sig-sub">Print name &amp; signature</div>
</td>
<td style="width: 32%;">
<div class="fp-ps-sig-label">
Received Date
<span style="font-weight: normal; color: #666; font-size: 8pt;"> / Date de réception</span>
</div>
<div class="fp-ps-sig-line"/>
<div class="fp-ps-sig-sub">YYYY-MM-DD</div>
</td>
<td style="width: 30%;" class="fp-ps-qr-box">
<t t-if="qr_uri">
<img t-att-src="qr_uri" alt="QR Code"/>
</t>
<div class="fp-ps-qr-caption">
Scan the QR Code to Sign
<span class="fp-fr">Scannez le code QR pour signer</span>
</div>
</td>
</tr>
</tbody>
</table>
</template>
<!-- ============================================================= -->
<!-- PORTRAIT -->
<!-- ============================================================= -->
@@ -16,6 +155,25 @@
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_portrait_styles"/>
<t t-call="fusion_plating_reports.fp_packing_slip_styles"/>
<!-- =========================================
Pre-compute fields from the picking chain.
doc → stock.picking, doc.sale_id → SO,
partner_invoice_id → bill-to (falls back
to commercial_partner). carrier presence
decides "Ready for pick up" vs tracking ref.
========================================= -->
<t t-set="bill_partner" t-value="(doc.sale_id.partner_invoice_id if doc.sale_id and doc.sale_id.partner_invoice_id else (doc.partner_id.commercial_partner_id or doc.partner_id))"/>
<t t-set="ship_partner" t-value="doc.partner_id"/>
<t t-set="has_carrier" t-value="'carrier_id' in doc._fields and doc.carrier_id"/>
<t t-set="ship_via" t-value="(doc.carrier_id.name if has_carrier else (doc.sale_id.x_fc_ship_via if doc.sale_id and 'x_fc_ship_via' in doc.sale_id._fields and doc.sale_id.x_fc_ship_via else 'CUSTOMER PICKUP'))"/>
<t t-set="tracking_ref" t-value="doc.carrier_tracking_ref if 'carrier_tracking_ref' in doc._fields and doc.carrier_tracking_ref else False"/>
<t t-set="tracking_text" t-value="tracking_ref if tracking_ref else ('Ready for pick up' if not has_carrier else '—')"/>
<t t-set="po_number" t-value="(doc.sale_id.client_order_ref if doc.sale_id and doc.sale_id.client_order_ref else '')"/>
<t t-set="qr_payload" t-value="doc.name or ''"/>
<t t-set="qr_uri" t-value="doc.env['ir.actions.report'].sudo().barcode_data_uri('QR', qr_payload, 220, 220) if qr_payload else False"/>
<div class="fp-report">
<div class="page">
@@ -24,92 +182,67 @@
<span t-field="doc.name"/>
</h4>
<!-- From / To -->
<table class="bordered">
<thead>
<tr>
<th style="width: 50%;">FROM</th>
<th style="width: 50%;">SHIP TO</th>
</tr>
</thead>
<!-- Bill To / Ship To -->
<table class="bordered fp-ps-addrtable">
<tbody>
<tr>
<td style="height: 80px;">
<strong><span t-field="doc.company_id.name"/></strong><br/>
<div t-field="doc.company_id.partner_id"
t-options="{'widget': 'contact', 'fields': ['address', 'phone', 'email'], 'no_marker': True}"/>
</td>
<td style="height: 80px;">
<strong><span t-field="doc.partner_id.name"/></strong><br/>
<div t-field="doc.partner_id"
t-options="{'widget': 'contact', 'fields': ['address', 'phone'], 'no_marker': True}"/>
</td>
</tr>
</tbody>
</table>
<!-- Shipment info -->
<table class="bordered">
<thead>
<tr>
<th class="info-header" style="width: 25%;">SHIP DATE</th>
<th class="info-header" style="width: 25%;">SOURCE</th>
<th class="info-header" style="width: 25%;">OPERATION</th>
<th class="info-header" style="width: 25%;">CARRIER</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-field="doc.scheduled_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center"><span t-esc="doc.origin or '-'"/></td>
<td class="text-center"><span t-field="doc.picking_type_id"/></td>
<td class="text-center">
<t t-if="'carrier_id' in doc._fields and doc.carrier_id">
<span t-field="doc.carrier_id"/>
<td style="width: 50%;">
<t t-call="fusion_plating_reports.fp_packing_slip_addr_block">
<t t-set="label" t-value="'Bill To:'"/>
<t t-set="partner" t-value="bill_partner"/>
</t>
</td>
<td style="width: 50%;">
<t t-call="fusion_plating_reports.fp_packing_slip_addr_block">
<t t-set="label" t-value="'Ship To:'"/>
<t t-set="partner" t-value="ship_partner"/>
</t>
<t t-else="">-</t>
</td>
</tr>
</tbody>
</table>
<!-- Products -->
<table class="bordered">
<!-- Ship details -->
<table class="bordered fp-ps-info-table">
<thead>
<tr>
<th class="text-start" style="width: 22%;">PART NUMBER</th>
<th class="text-start" style="width: 34%;">DESCRIPTION</th>
<th style="width: 12%;">QTY</th>
<th style="width: 10%;">UOM</th>
<th style="width: 22%;">LOT / SERIAL</th>
<th style="width: 33%;">
Ship Via<span class="fp-fr" style="display:block; font-weight:normal; color:#555; font-size:8pt;">Mode d'expédition</span>
</th>
<th style="width: 33%;">
Shipping Date<span class="fp-fr" style="display:block; font-weight:normal; color:#555; font-size:8pt;">Date d'expédition</span>
</th>
<th style="width: 34%;">
Tracking #<span class="fp-fr" style="display:block; font-weight:normal; color:#555; font-size:8pt;">N° de suivi</span>
</th>
</tr>
</thead>
<tbody>
<t t-foreach="doc.move_ids_without_package" t-as="move">
<tr>
<t t-set="line" t-value="move.sale_line_id or move"/>
<td>
<t t-call="fusion_plating_reports.customer_line_part_number"/>
</td>
<td>
<t t-call="fusion_plating_reports.customer_line_description"/>
</td>
<td class="text-center">
<span t-esc="int(move.quantity) if move.quantity == int(move.quantity) else move.quantity"/>
</td>
<td class="text-center"><span t-field="move.product_uom"/></td>
<td>
<t t-foreach="move.move_line_ids" t-as="ml">
<t t-if="ml.lot_id">
<span t-field="ml.lot_id.name"/><br/>
</t>
</t>
</td>
</tr>
</t>
<tr>
<td><span t-esc="ship_via"/></td>
<td>
<t t-if="doc.scheduled_date">
<span t-field="doc.scheduled_date" t-options="{'widget': 'date'}"/>
</t>
<t t-else=""></t>
</td>
<td><span t-esc="tracking_text"/></td>
</tr>
</tbody>
</table>
<!-- Items -->
<t t-call="fusion_plating_reports.fp_packing_slip_items">
<t t-set="w_ordered" t-value="'width: 8%;'"/>
<t t-set="w_shipped" t-value="'width: 8%;'"/>
<t t-set="w_bo" t-value="'width: 8%;'"/>
<t t-set="w_part" t-value="'width: 17%;'"/>
<t t-set="w_po" t-value="'width: 11%;'"/>
<t t-set="w_wo" t-value="'width: 11%;'"/>
<t t-set="w_process" t-value="'width: 14%;'"/>
<t t-set="w_desc" t-value="'width: 23%;'"/>
</t>
<!-- Notes -->
<t t-if="doc.note">
<div style="margin-top: 10px;">
@@ -118,21 +251,8 @@
</div>
</t>
<!-- Sign off -->
<div class="row" style="margin-top: 30px;">
<div class="col-6">
<div class="sig-box">
<div class="sig-line"/>
<div class="small-muted">Shipper (Signature / Date)</div>
</div>
</div>
<div class="col-6">
<div class="sig-box">
<div class="sig-line"/>
<div class="small-muted">Receiver (Signature / Date)</div>
</div>
</div>
</div>
<!-- Sign-off + QR -->
<t t-call="fusion_plating_reports.fp_packing_slip_signoff"/>
</div>
</div>
@@ -149,6 +269,18 @@
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<t t-call="fusion_plating_reports.fp_packing_slip_styles"/>
<t t-set="bill_partner" t-value="(doc.sale_id.partner_invoice_id if doc.sale_id and doc.sale_id.partner_invoice_id else (doc.partner_id.commercial_partner_id or doc.partner_id))"/>
<t t-set="ship_partner" t-value="doc.partner_id"/>
<t t-set="has_carrier" t-value="'carrier_id' in doc._fields and doc.carrier_id"/>
<t t-set="ship_via" t-value="(doc.carrier_id.name if has_carrier else (doc.sale_id.x_fc_ship_via if doc.sale_id and 'x_fc_ship_via' in doc.sale_id._fields and doc.sale_id.x_fc_ship_via else 'CUSTOMER PICKUP'))"/>
<t t-set="tracking_ref" t-value="doc.carrier_tracking_ref if 'carrier_tracking_ref' in doc._fields and doc.carrier_tracking_ref else False"/>
<t t-set="tracking_text" t-value="tracking_ref if tracking_ref else ('Ready for pick up' if not has_carrier else '—')"/>
<t t-set="po_number" t-value="(doc.sale_id.client_order_ref if doc.sale_id and doc.sale_id.client_order_ref else '')"/>
<t t-set="qr_payload" t-value="doc.name or ''"/>
<t t-set="qr_uri" t-value="doc.env['ir.actions.report'].sudo().barcode_data_uri('QR', qr_payload, 220, 220) if qr_payload else False"/>
<div class="fp-landscape">
<div class="page">
@@ -157,104 +289,67 @@
<span t-field="doc.name"/>
</h2>
<!-- From / To -->
<table class="bordered">
<thead>
<tr>
<th style="width: 50%;">FROM</th>
<th style="width: 50%;">SHIP TO</th>
</tr>
</thead>
<!-- Bill To / Ship To -->
<table class="bordered fp-ps-addrtable">
<tbody>
<tr>
<td style="height: 80px; font-size: 12pt;">
<strong><span t-field="doc.company_id.name"/></strong><br/>
<div t-field="doc.company_id.partner_id"
t-options="{'widget': 'contact', 'fields': ['address', 'phone', 'email'], 'no_marker': True}"/>
<td style="width: 50%;">
<t t-call="fusion_plating_reports.fp_packing_slip_addr_block">
<t t-set="label" t-value="'Bill To:'"/>
<t t-set="partner" t-value="bill_partner"/>
</t>
</td>
<td style="height: 80px; font-size: 12pt;">
<strong><span t-field="doc.partner_id.name"/></strong><br/>
<div t-field="doc.partner_id"
t-options="{'widget': 'contact', 'fields': ['address', 'phone'], 'no_marker': True}"/>
<td style="width: 50%;">
<t t-call="fusion_plating_reports.fp_packing_slip_addr_block">
<t t-set="label" t-value="'Ship To:'"/>
<t t-set="partner" t-value="ship_partner"/>
</t>
</td>
</tr>
</tbody>
</table>
<!-- Shipment info -->
<table class="bordered info-table">
<!-- Ship details -->
<table class="bordered fp-ps-info-table">
<thead>
<tr>
<th>SHIP DATE</th>
<th>SOURCE</th>
<th>OPERATION</th>
<th>CARRIER</th>
<th>TRACKING REF</th>
<th style="width: 33%;">
Ship Via<span class="fp-fr" style="display:block; font-weight:normal; color:#555; font-size:8pt;">Mode d'expédition</span>
</th>
<th style="width: 33%;">
Shipping Date<span class="fp-fr" style="display:block; font-weight:normal; color:#555; font-size:8pt;">Date d'expédition</span>
</th>
<th style="width: 34%;">
Tracking #<span class="fp-fr" style="display:block; font-weight:normal; color:#555; font-size:8pt;">N° de suivi</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-field="doc.scheduled_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center"><span t-esc="doc.origin or '-'"/></td>
<td class="text-center"><span t-field="doc.picking_type_id"/></td>
<td class="text-center">
<t t-if="'carrier_id' in doc._fields and doc.carrier_id">
<span t-field="doc.carrier_id"/>
<td><span t-esc="ship_via"/></td>
<td>
<t t-if="doc.scheduled_date">
<span t-field="doc.scheduled_date" t-options="{'widget': 'date'}"/>
</t>
<t t-else="">-</t>
</td>
<td class="text-center">
<t t-if="'carrier_tracking_ref' in doc._fields">
<span t-esc="doc.carrier_tracking_ref or '-'"/>
</t>
<t t-else="">-</t>
<t t-else=""></t>
</td>
<td><span t-esc="tracking_text"/></td>
</tr>
</tbody>
</table>
<!-- Products -->
<table class="bordered">
<thead>
<tr>
<th class="text-start" style="width: 18%;">PART NUMBER</th>
<th class="text-start" style="width: 26%;">DESCRIPTION</th>
<th style="width: 10%;">ORDERED</th>
<th style="width: 10%;">DONE</th>
<th style="width: 8%;">UOM</th>
<th style="width: 14%;">LOT / SERIAL</th>
<th style="width: 14%;">NOTES</th>
</tr>
</thead>
<tbody>
<t t-foreach="doc.move_ids_without_package" t-as="move">
<tr>
<t t-set="line" t-value="move.sale_line_id or move"/>
<td>
<t t-call="fusion_plating_reports.customer_line_part_number"/>
</td>
<td>
<t t-call="fusion_plating_reports.customer_line_description"/>
</td>
<td class="text-center">
<span t-esc="int(move.product_uom_qty) if move.product_uom_qty == int(move.product_uom_qty) else move.product_uom_qty"/>
</td>
<td class="text-center">
<span t-esc="int(move.quantity) if move.quantity == int(move.quantity) else move.quantity"/>
</td>
<td class="text-center"><span t-field="move.product_uom"/></td>
<td>
<t t-foreach="move.move_line_ids" t-as="ml">
<t t-if="ml.lot_id">
<span t-field="ml.lot_id.name"/><br/>
</t>
</t>
</td>
<td/>
</tr>
</t>
</tbody>
</table>
<!-- Items: landscape gets a touch more breathing room on
the description / part columns. -->
<t t-call="fusion_plating_reports.fp_packing_slip_items">
<t t-set="w_ordered" t-value="'width: 7%;'"/>
<t t-set="w_shipped" t-value="'width: 7%;'"/>
<t t-set="w_bo" t-value="'width: 7%;'"/>
<t t-set="w_part" t-value="'width: 16%;'"/>
<t t-set="w_po" t-value="'width: 10%;'"/>
<t t-set="w_wo" t-value="'width: 10%;'"/>
<t t-set="w_process" t-value="'width: 13%;'"/>
<t t-set="w_desc" t-value="'width: 30%;'"/>
</t>
<!-- Notes -->
<t t-if="doc.note">
@@ -264,21 +359,8 @@
</div>
</t>
<!-- Sign off -->
<div class="row" style="margin-top: 30px;">
<div class="col-6">
<div class="sig-box">
<div class="sig-line"/>
<div class="small-muted">Shipper (Signature / Date)</div>
</div>
</div>
<div class="col-6">
<div class="sig-box">
<div class="sig-line"/>
<div class="small-muted">Receiver (Signature / Date)</div>
</div>
</div>
</div>
<!-- Sign-off + QR -->
<t t-call="fusion_plating_reports.fp_packing_slip_signoff"/>
</div>
</div>

View File

@@ -11,28 +11,99 @@
<!-- ============================================================= -->
<!-- PORTRAIT -->
<!-- ============================================================= -->
<!-- Shared bilingual-label snippet. CSS class `.fp-bl` does the
two-line render: English on top, French underneath in a lighter
italic. Stored next to the report's own scss-style block so it
doesn't drift when the same idiom propagates to other reports.
Title sizing: the previous attempt at "compact" (negative
margin-top) pushed the title up INTO the wkhtmltopdf header zone
(the company logo band) and clipped the top of the H1 glyphs.
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. -->
<template id="fp_sale_bilingual_styles">
<style>
/* Inline bilingual: English bold, then a faint slash, then
French italic-grey. Sits on one line where room allows
and wraps to two naturally if the cell is narrow. Apply
this everywhere except super-narrow cells (QTY, UOM)
where the cell is physically too tight even for the
shortest French word — those use the stacked variant
below. */
.fp-bl-en { font-weight: bold; }
.fp-bl-sep { color: #999; margin: 0 3px; font-weight: normal; }
.fp-bl-fr { font-weight: normal; font-style: italic; color: #555; }
/* Stacked variant for narrow cells — EN on top line, FR
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; }
/* Kill the extra top padding Odoo's `.page` class adds
(1cm by default). The paperformat already reserves
header room — `.page` padding compounds on top of it
and was the source of the giant gap. Keep left/right/
bottom at 1cm so the content isn't flush to the edges. */
.fp-report.fp-sale .page { padding-top: 0 !important; }
/* 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",
so the only reliable fix is to avoid the table element. */
.fp-sale-titlebar { margin: 0 0 8px 0; padding: 0; overflow: hidden; }
.fp-sale-title { font-size: 14pt; line-height: 1.2; color: #2e2e2e; font-weight: bold; }
.fp-sale-title .fp-bl-fr { font-size: 10pt; }
.fp-sale-barcode { float: right; text-align: right; margin-left: 12px; }
.fp-sale-barcode img { height: 34px; max-width: 220px; }
.fp-sale-barcode .fp-bc-label { font-size: 8pt; color: #555; margin-top: 2px; }
</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"/>
<div class="fp-report">
<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 ''"/>
<div class="fp-report fp-sale">
<div class="page">
<!-- Title -->
<h4>
<span t-if="doc.state in ['draft','sent']">Quotation # </span>
<span t-else="">Sales Order # </span>
<span t-field="doc.name"/>
</h4>
<!-- Title bar: bilingual title on the left,
Code128 barcode floated right. NO <table>
— see CLAUDE.md "wkhtmltopdf header
overlap" §2 for why a table here leaks
borders even with `border:0 !important`. -->
<div class="fp-sale-titlebar">
<t t-if="barcode_uri">
<div class="fp-sale-barcode">
<img t-att-src="barcode_uri" alt="Order Barcode"/>
<div class="fp-bc-label"><span t-field="doc.name"/></div>
</div>
</t>
<div class="fp-sale-title">
<span class="fp-bl-en"><t t-esc="title_en"/></span><span class="fp-bl-sep">/</span><span class="fp-bl-fr"><t t-esc="title_fr"/></span>
<span> # </span><span t-field="doc.name"/>
</div>
</div>
<!-- Billing / Shipping -->
<!-- Billing / Shipping (wide cells — inline) -->
<table class="bordered">
<thead>
<tr>
<th style="width: 50%;">BILLING ADDRESS</th>
<th style="width: 50%;">SHIPPING 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">Shipping Address</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Adresse d'expédition</span>
</th>
</tr>
</thead>
<tbody>
@@ -49,115 +120,144 @@
</tbody>
</table>
<!-- Order info -->
<!-- Row 1: 5 narrow cells (20% each) — stacked
so the French label doesn't overflow into
the next column. -->
<table class="bordered">
<thead>
<tr>
<th class="info-header" style="width: 20%;">ORDER DATE</th>
<th class="info-header" style="width: 20%;">EXPIRATION</th>
<th class="info-header" style="width: 20%;">SALESPERSON</th>
<th class="info-header" style="width: 20%;">CUSTOMER PO #</th>
<th class="info-header" style="width: 20%;">RUSH</th>
<th class="info-header" style="width: 20%;">
<span class="fp-bl-en-stk">Order Date</span>
<span class="fp-bl-fr-stk">Date de commande</span>
</th>
<th class="info-header" style="width: 20%;">
<span class="fp-bl-en-stk">Delivery Date</span>
<span class="fp-bl-fr-stk">Date de livraison</span>
</th>
<th class="info-header" style="width: 20%;">
<span class="fp-bl-en-stk">Salesperson</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">Lead Time</span>
<span class="fp-bl-fr-stk">Délai</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-field="doc.date_order" t-options="{'widget': 'date'}"/></td>
<td class="text-center"><span t-field="doc.validity_date"/></td>
<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">
<span t-if="doc.x_fc_rush_order" class="status-warning">RUSH</span>
<span t-else="">Standard</span>
<t t-if="doc.commitment_date">
<span t-field="doc.commitment_date" t-options="{'widget': 'date'}"/>
</t>
<t t-else=""></t>
</td>
<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>
</t>
<t t-else="">Standard</t>
</td>
</tr>
</tbody>
</table>
<!-- Plating info -->
<t t-if="doc.x_fc_part_catalog_id or doc.x_fc_customer_spec_id or doc.x_fc_delivery_method">
<!-- Row 2: 3 wider cells (33% each) — inline. -->
<t t-if="doc.x_fc_customer_job_number or spec_label or delivery_method_label">
<table class="bordered">
<thead>
<tr>
<th class="info-header" style="width: 34%;">PART</th>
<th class="info-header" style="width: 33%;">SPECIFICATION</th>
<th class="info-header" style="width: 33%;">DELIVERY METHOD</th>
<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-field="doc.x_fc_part_catalog_id"/></td>
<td class="text-center"><span t-field="doc.x_fc_customer_spec_id"/></td>
<td class="text-center">
<t t-set="dm" t-value="dict(doc._fields['x_fc_delivery_method'].selection).get(doc.x_fc_delivery_method, '-')"/>
<span t-esc="dm"/>
</td>
<td class="text-center"><span t-esc="doc.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>
<!-- Scheduling + customer job reference -->
<t t-if="doc.x_fc_customer_job_number or doc.x_fc_planned_start_date or doc.commitment_date or doc.x_fc_ship_via">
<table class="bordered">
<thead>
<tr>
<th class="info-header" style="width: 25%;">CUSTOMER JOB #</th>
<th class="info-header" style="width: 25%;">PLANNED START</th>
<th class="info-header" style="width: 25%;">CUSTOMER DEADLINE</th>
<th class="info-header" style="width: 25%;">SHIP VIA</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-esc="doc.x_fc_customer_job_number or '-'"/></td>
<td class="text-center"><span t-field="doc.x_fc_planned_start_date"/></td>
<td class="text-center"><span t-field="doc.commitment_date"/></td>
<td class="text-center"><span t-esc="doc.x_fc_ship_via or '-'"/></td>
</tr>
</tbody>
</table>
</t>
<!-- Blanket / block-partial callout (confirmed-order shipping flags) -->
<!-- Blanket / block-partial callout -->
<t t-if="doc.x_fc_is_blanket_order or doc.x_fc_block_partial_shipments">
<div class="highlight-box">
<t t-if="doc.x_fc_is_blanket_order">
<strong>Blanket Order.</strong>
<strong>Blanket Order / Commande ouverte.</strong>
Parts will be released in quantities over time.
</t>
<t t-if="doc.x_fc_block_partial_shipments">
<strong>Partial shipments blocked.</strong>
<strong>Partial shipments blocked / Expéditions partielles bloquées.</strong>
The order ships as one complete batch.
</t>
</div>
</t>
<!-- Order lines -->
<!-- Order lines. Taxes column dropped — taxes
summarized in the totals block below; per-line
tax labels were noise on a single-tax-region
plating order. The part-number cell appends
the catalog `name` (Part Name) after the
revision so customers see PN + Rev + Name. -->
<table class="bordered">
<thead>
<tr>
<th class="text-start" style="width: 20%;">PART NUMBER</th>
<th class="text-start" style="width: 30%;">DESCRIPTION</th>
<th style="width: 8%;">QTY</th>
<th style="width: 8%;">UOM</th>
<th style="width: 12%;">UNIT PRICE</th>
<th style="width: 10%;">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.order_line" 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"/>
<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>
</td>
<td>
<t t-call="fusion_plating_reports.customer_line_description"/>
@@ -169,9 +269,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>
@@ -184,37 +281,38 @@
<!-- Terms + Totals -->
<div class="row" style="margin-top: 15px;">
<div class="col-6">
<t t-if="doc.payment_term_id.note">
<strong>Payment Terms:</strong><br/>
<span t-field="doc.payment_term_id.note"/>
</t>
<t t-if="doc.x_fc_invoice_strategy">
<div style="margin-top: 10px;">
<strong>Invoice Strategy: </strong>
<t t-set="inv_strat" t-value="dict(doc._fields['x_fc_invoice_strategy'].selection).get(doc.x_fc_invoice_strategy, '-')"/>
<span t-esc="inv_strat"/>
<t t-if="doc.x_fc_invoice_strategy == 'deposit' and doc.x_fc_deposit_percent">
(<span t-esc="doc.x_fc_deposit_percent"/>%)
</t>
</div>
<t t-if="doc.payment_term_id">
<strong>Payment Terms / Modalités de paiement:</strong><br/>
<t t-if="doc.payment_term_id.note">
<span t-field="doc.payment_term_id.note"/>
</t>
<t t-else="">
<span t-field="doc.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>
@@ -226,7 +324,7 @@
<!-- External (customer-visible) notes -->
<t t-if="doc.x_fc_external_note">
<div style="margin-top: 15px;">
<strong>Notes:</strong>
<strong>Notes / Remarques:</strong>
<div t-field="doc.x_fc_external_note"/>
</div>
</t>
@@ -234,7 +332,7 @@
<!-- Terms and Conditions -->
<t t-if="doc.note">
<div style="margin-top: 15px;">
<strong>Terms and Conditions:</strong>
<strong>Terms and Conditions / Conditions générales:</strong>
<div t-field="doc.note"/>
</div>
</t>