From 423f288507e38c4a9434132bd8b090d0f9b1f75f Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Thu, 4 Jun 2026 19:24:26 -0400 Subject: [PATCH] feat(fusion_shipping): Shipping Label + Commercial Invoice smart buttons on delivery order Carriers post the shipping label and (for international) the commercial invoice to the delivery order's chatter, where they were hard to find. Add two smart buttons on the stock.picking form that open the latest of each via the PDF preview dialog (fusion_pdf_preview when installed, else open in a new tab). Document discovery is carrier-agnostic (computed from the picking's attachments): labels match 'Label*'; invoices match '*CommercialInvoice*' (UPS/Canada Post) or 'ShippingDoc-*' (FedEx/DHL, _get_delivery_doc_prefix). Buttons hide when absent. Verified on entech: real FedEx picking resolved its label (invoice correctly none for a domestic ship); synthetic UPS names resolved label+invoice and the invoice button fired fusion_pdf_preview.open_attachment. Co-Authored-By: Claude Opus 4.8 (1M context) --- fusion_shipping/__manifest__.py | 2 +- fusion_shipping/models/stock_picking.py | 78 ++++++++++++++++++- fusion_shipping/views/stock_picking_views.xml | 16 ++++ 3 files changed, 94 insertions(+), 2 deletions(-) diff --git a/fusion_shipping/__manifest__.py b/fusion_shipping/__manifest__.py index f128c2c6..a521e739 100644 --- a/fusion_shipping/__manifest__.py +++ b/fusion_shipping/__manifest__.py @@ -1,6 +1,6 @@ { "name": "Fusion Shipping", - "version": "19.0.1.6.0", + "version": "19.0.1.7.0", "category": "Inventory/Delivery", "summary": "All-in-one shipping integration — Canada Post, UPS, FedEx, DHL Express. " "Live pricing, label generation, shipment tracking, and multi-package support.", diff --git a/fusion_shipping/models/stock_picking.py b/fusion_shipping/models/stock_picking.py index 11ca46f5..cf338752 100644 --- a/fusion_shipping/models/stock_picking.py +++ b/fusion_shipping/models/stock_picking.py @@ -1,4 +1,4 @@ -from odoo import models, fields +from odoo import models, fields, api, _ class StockPicking(models.Model): @@ -8,6 +8,20 @@ class StockPicking(models.Model): string='Shipments', compute='_compute_fusion_shipment_count', ) + fusion_shipping_label_attachment_id = fields.Many2one( + 'ir.attachment', + string='Shipping Label', + compute='_compute_fusion_shipping_docs', + help='Most recent shipping label generated for this delivery ' + '(any carrier).', + ) + fusion_commercial_invoice_attachment_id = fields.Many2one( + 'ir.attachment', + string='Commercial Invoice', + compute='_compute_fusion_shipping_docs', + help='Most recent commercial (customs) invoice generated for this ' + 'delivery (any carrier).', + ) def _compute_fusion_shipment_count(self): Shipment = self.env['fusion.shipment'] @@ -16,6 +30,68 @@ class StockPicking(models.Model): [('picking_id', '=', picking.id)] ) + @api.depends('message_ids.attachment_ids') + def _compute_fusion_shipping_docs(self): + """Find the latest shipping label + commercial invoice that the + carrier posted to this delivery's chatter, so they can be opened + from a smart button. + + Naming is carrier-agnostic (see the carriers' send_shipping): + - labels contain 'Label' (Label-, LabelUPS, LabelShipping-...) + - invoices contain 'CommercialInvoice' (UPS/Canada Post) or start + with 'ShippingDoc-' (FedEx/DHL, via _get_delivery_doc_prefix). + """ + Attachment = self.env['ir.attachment'] + for picking in self: + invoice = label = Attachment + if isinstance(picking.id, int): + atts = Attachment.search( + [('res_model', '=', 'stock.picking'), + ('res_id', '=', picking.id)], + order='id desc') + for att in atts: + name = att.name or '' + norm = name.lower().replace(' ', '').replace('-', '') + if 'commercialinvoice' in norm or name.startswith('ShippingDoc'): + invoice = invoice or att + elif 'label' in name.lower() and 'return' not in name.lower(): + label = label or att + if invoice and label: + break + picking.fusion_commercial_invoice_attachment_id = invoice.id + picking.fusion_shipping_label_attachment_id = label.id + + def _fusion_open_attachment(self, attachment): + """Open a shipping document for the operator. + + Routes PDFs through ir.attachment.action_fusion_preview (preview + dialog) when fusion_pdf_preview is installed, else opens the file + in a new tab. Mirrors fusion.shipment._action_open_attachment. + See CLAUDE.md "PDF Preview". + """ + self.ensure_one() + if not attachment: + return False + if hasattr(attachment, 'action_fusion_preview'): + return attachment.action_fusion_preview( + title=attachment.name or _('Shipping Document'), + model_name=self._name, + record_ids=self.id, + ) + return { + 'type': 'ir.actions.act_url', + 'url': '/web/content/%s?download=false' % attachment.id, + 'target': 'new', + } + + def action_view_fusion_shipping_label(self): + return self._fusion_open_attachment( + self.fusion_shipping_label_attachment_id) + + def action_view_fusion_commercial_invoice(self): + return self._fusion_open_attachment( + self.fusion_commercial_invoice_attachment_id) + def action_view_fusion_shipments(self): self.ensure_one() shipments = self.env['fusion.shipment'].search( diff --git a/fusion_shipping/views/stock_picking_views.xml b/fusion_shipping/views/stock_picking_views.xml index eb9f1739..9092e874 100644 --- a/fusion_shipping/views/stock_picking_views.xml +++ b/fusion_shipping/views/stock_picking_views.xml @@ -6,12 +6,28 @@ + + + +