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) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-04 19:24:26 -04:00
parent a86f20017d
commit 423f288507
3 changed files with 94 additions and 2 deletions

View File

@@ -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.",

View File

@@ -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(

View File

@@ -6,12 +6,28 @@
<field name="inherit_id" ref="stock.view_picking_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<field name="fusion_shipping_label_attachment_id" invisible="1"/>
<field name="fusion_commercial_invoice_attachment_id" invisible="1"/>
<button name="action_view_fusion_shipments" type="object"
class="oe_stat_button" icon="fa-plane"
invisible="fusion_shipment_count == 0">
<field name="fusion_shipment_count" widget="statinfo"
string="Shipments"/>
</button>
<button name="action_view_fusion_shipping_label" type="object"
class="oe_stat_button" icon="fa-print"
invisible="not fusion_shipping_label_attachment_id">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Shipping Label</span>
</div>
</button>
<button name="action_view_fusion_commercial_invoice" type="object"
class="oe_stat_button" icon="fa-file-text-o"
invisible="not fusion_commercial_invoice_attachment_id">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Commercial Invoice</span>
</div>
</button>
</xpath>
</field>
</record>