feat(reports): packing slip for local deliveries (fusion.plating.delivery)

The packing slip report only existed for stock.picking (Delivery Orders),
but this shop ships via fusion.plating.delivery and has no pickings — so
packing slips never rendered for their flow, and the prior auto-generate +
email-notification paths pointed the stock.picking report at a delivery
(wrong model -> blank PDF).

Add a delivery-native variant: report_fp_packing_slip_delivery_portrait +
action_report_fp_packing_slip_delivery_portrait (bound to
fusion.plating.delivery -> shows in the delivery Print menu), resolving the
SO + lines from the delivery job_ref (same pattern as the BoL report) and
reusing the shared styles / address / signoff bits + a sale.order.line
items table. Repoint _fp_generate_packing_slip (dispatch auto-gen) and the
notification attachment to the new report.

Verified on entech: real content (customer, PO, items, PS#) for DLV-30102 —
142KB PDF vs prior blank 12.8KB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-04 15:12:50 -04:00
parent e6bbf566ca
commit c97a0d985c
3 changed files with 212 additions and 2 deletions

View File

@@ -501,7 +501,7 @@ class FpDelivery(models.Model):
fusion_plating_reports.
"""
report_xmlid = (
'fusion_plating_reports.action_report_fp_packing_slip_portrait'
'fusion_plating_reports.action_report_fp_packing_slip_delivery_portrait'
)
report = self.env.ref(report_xmlid, raise_if_not_found=False)
if not report:

View File

@@ -377,7 +377,7 @@ class FpNotificationTemplate(models.Model):
# Packing slip — gated by customer preference (default True)
if self.attach_packing_list and delivery and _customer_wants('x_fc_send_packing_slip'):
att = _render_report(
'fusion_plating_reports.action_report_fp_packing_slip_portrait', delivery,
'fusion_plating_reports.action_report_fp_packing_slip_delivery_portrait', delivery,
)
if att:
ids.append(att)

View File

@@ -461,4 +461,214 @@
</t>
</template>
<!-- ============================================================= -->
<!-- LOCAL DELIVERY variant (fusion.plating.delivery) -->
<!-- The stock.picking templates above don't fit shops that ship -->
<!-- via fusion.plating.delivery (no picking). This variant -->
<!-- resolves the SO + lines from the delivery's job_ref (same -->
<!-- pattern as the BoL report) and reuses the shared styles / -->
<!-- address / signoff bits. Items come from the SO lines (vs -->
<!-- stock moves). -->
<!-- ============================================================= -->
<!-- Items table sourced from sale.order.line (not stock.move) -->
<template id="fp_packing_slip_items_lines">
<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="lines" t-as="line">
<t t-set="ordered_qty" t-value="line.product_uom_qty or 0.0"/>
<t t-set="wo_job" t-value="line.env['fp.job'].sudo().search([('sale_order_line_ids', 'in', line.ids)], limit=1)"/>
<t t-set="done_qty" t-value="(wo_job.qty_done if wo_job and wo_job.qty_done else ordered_qty)"/>
<t t-set="bo_qty" t-value="ordered_qty - done_qty if ordered_qty &gt; done_qty else 0.0"/>
<t t-set="proc_variant" t-value="(line.x_fc_process_variant_id if 'x_fc_process_variant_id' in line._fields else False)"/>
<t t-set="proc_label" t-value="(proc_variant.variant_label or proc_variant.name) if proc_variant else ((line.x_fc_part_catalog_id.default_process_id.variant_label or line.x_fc_part_catalog_id.default_process_id.name) if line.x_fc_part_catalog_id and line.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>
<template id="report_fp_packing_slip_delivery_portrait">
<t t-call="web.html_container">
<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"/>
<!-- Resolve SO + lines from the delivery's job_ref
(mirrors the BoL report's resolution). -->
<t t-set="_job" t-value="env['fp.job'].sudo().search([('name', '=', doc.job_ref)], limit=1) if doc.job_ref else env['fp.job']"/>
<t t-set="_so" t-value="_job.sale_order_id if _job else False"/>
<t t-set="_lines" t-value="_so.order_line.filtered(lambda l: l.product_id and not l.display_type and l.product_uom_qty &gt; 0) if _so else False"/>
<t t-set="bill_partner" t-value="(_so.partner_invoice_id if _so and _so.partner_invoice_id else (doc.partner_id.commercial_partner_id or doc.partner_id))"/>
<t t-set="ship_partner" t-value="doc.delivery_address_id or doc.partner_id"/>
<t t-set="has_carrier" t-value="'x_fc_carrier_id' in doc._fields and doc.x_fc_carrier_id"/>
<t t-set="ship_via" t-value="(doc.x_fc_carrier_id.name if has_carrier else (_so.x_fc_ship_via if _so and 'x_fc_ship_via' in _so._fields and _so.x_fc_ship_via else 'CUSTOMER PICKUP'))"/>
<t t-set="tracking_text" t-value="'Ready for pick up' if not has_carrier else '—'"/>
<t t-set="po_number" t-value="(_so.client_order_ref if _so and _so.client_order_ref else '')"/>
<t t-set="so_name_raw" t-value="_so.name if _so else (doc.name or '')"/>
<t t-set="ps_number" t-value="so_name_raw.rsplit('-', 1)[-1] if '-' in so_name_raw else so_name_raw"/>
<t t-set="ps_barcode_uri" t-value="doc.env['ir.actions.report'].sudo().barcode_data_uri('Code128', ps_number, 600, 100) if ps_number else False"/>
<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 fp-ps">
<div class="page">
<div class="fp-ps-titlebar">
<t t-if="ps_barcode_uri">
<div class="fp-ps-barcode">
<div class="fp-bc-wrap">
<img t-att-src="ps_barcode_uri" alt="Packing Slip Barcode"/>
<div class="fp-bc-label"><t t-esc="ps_number"/></div>
</div>
</div>
</t>
<span class="fp-ps-title-en">
Packing Slip<span class="fp-ps-title-num"># <t t-esc="ps_number"/></span>
</span>
<span class="fp-ps-title-fr">Bordereau d'expédition</span>
</div>
<table class="bordered fp-ps-addrtable">
<tbody>
<tr>
<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>
</td>
</tr>
</tbody>
</table>
<table class="bordered fp-ps-info-table">
<thead>
<tr>
<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><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>
<t t-if="_lines">
<t t-call="fusion_plating_reports.fp_packing_slip_items_lines">
<t t-set="lines" t-value="_lines"/>
</t>
</t>
<t t-else="">
<p style="margin-top: 10px; color: #555;">
No order lines are linked to this delivery
(job <span t-esc="doc.job_ref or doc.name"/>).
</p>
</t>
<t t-if="doc.notes">
<div style="margin-top: 10px;">
<strong>Notes:</strong>
<div t-field="doc.notes"/>
</div>
</t>
<t t-call="fusion_plating_reports.fp_packing_slip_signoff"/>
</div>
</div>
</t>
</t>
</t>
</template>
<record id="action_report_fp_packing_slip_delivery_portrait" model="ir.actions.report">
<field name="name">Packing Slip</field>
<field name="model">fusion.plating.delivery</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_packing_slip_delivery_portrait</field>
<field name="report_file">fusion_plating_reports.report_fp_packing_slip_delivery_portrait</field>
<field name="print_report_name">'Packing Slip - %s' % (object.name or '')</field>
<field name="binding_model_id" ref="fusion_plating_logistics.model_fusion_plating_delivery"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_portrait"/>
</record>
</odoo>