137 lines
5.5 KiB
Python
137 lines
5.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
|
"""Phase C — extend fusion.shipment with dimension fields.
|
|
|
|
fusion_shipping's native model has `weight` but no length/width/height.
|
|
The plating workflow needs all four captured at receiving time so the
|
|
shipment record carries everything the carrier API would want. Added
|
|
here (not in fusion_shipping) to keep the upstream module untouched.
|
|
"""
|
|
import logging
|
|
|
|
from odoo import api, fields, models
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class FusionShipment(models.Model):
|
|
_inherit = 'fusion.shipment'
|
|
|
|
x_fc_length = fields.Float(string='Length', digits=(10, 2))
|
|
x_fc_width = fields.Float(string='Width', digits=(10, 2))
|
|
x_fc_height = fields.Float(string='Height', digits=(10, 2))
|
|
x_fc_dim_uom = fields.Selection(
|
|
[('in', 'in'), ('cm', 'cm')],
|
|
string='Dim UoM', default='in',
|
|
)
|
|
x_fc_weight_uom = fields.Selection(
|
|
[('lb', 'lb'), ('kg', 'kg')],
|
|
string='Weight UoM', default='lb',
|
|
)
|
|
|
|
# Multi-piece label storage. label_attachment_id remains the
|
|
# primary (first box) for backward-compat; this M2M holds the full
|
|
# set so the operator can download any box's label individually.
|
|
x_fc_label_attachment_ids = fields.Many2many(
|
|
'ir.attachment',
|
|
'fusion_shipment_label_attachment_rel',
|
|
'shipment_id', 'attachment_id',
|
|
string='All Labels',
|
|
copy=False,
|
|
)
|
|
|
|
# Phase C — resolved carrier tracking URL with the tracking number
|
|
# substituted into the carrier.tracking_url template. Used by the
|
|
# shipment_labeled email template and any other place that needs a
|
|
# working clickable tracking link. Single source of truth so both
|
|
# email + portal stay consistent.
|
|
x_fc_tracking_url = fields.Char(
|
|
string='Tracking URL (resolved)',
|
|
compute='_compute_x_fc_tracking_url',
|
|
help='carrier.tracking_url with <shipmenttrackingnumber> replaced '
|
|
'by tracking_number. Empty when the carrier has no URL '
|
|
'template or there is no tracking number yet.',
|
|
)
|
|
|
|
@api.depends('carrier_id.tracking_url', 'tracking_number')
|
|
def _compute_x_fc_tracking_url(self):
|
|
for rec in self:
|
|
tpl = (rec.carrier_id.tracking_url or '') if rec.carrier_id else ''
|
|
tn = rec.tracking_number or ''
|
|
if not tpl or not tn:
|
|
rec.x_fc_tracking_url = ''
|
|
continue
|
|
placeholder = '<shipmenttrackingnumber>'
|
|
if placeholder in tpl:
|
|
rec.x_fc_tracking_url = tpl.replace(placeholder, tn)
|
|
else:
|
|
rec.x_fc_tracking_url = tpl + tn
|
|
|
|
def write(self, vals):
|
|
"""Sync the carrier tracking number + label to the customer
|
|
portal job whenever they land on the shipment. The portal_job
|
|
currently shows `delivery.name` as 'tracking' — wrong; the
|
|
customer wants the carrier's actual tracking number so the
|
|
clickable link goes to FedEx/UPS/etc."""
|
|
res = super().write(vals)
|
|
sync_keys = {'tracking_number', 'label_attachment_id', 'status'}
|
|
if not sync_keys & set(vals.keys()):
|
|
return res
|
|
for ship in self:
|
|
try:
|
|
ship._fp_sync_to_portal_job()
|
|
except Exception as e:
|
|
_logger.warning(
|
|
'Shipment %s: portal-job sync failed: %s',
|
|
ship.name, e,
|
|
)
|
|
return res
|
|
|
|
def _fp_sync_to_portal_job(self):
|
|
"""Walk shipment → SO → fp.job → fusion.plating.portal.job
|
|
and push the carrier tracking number + label + delivery's
|
|
packing slip onto the customer-facing record.
|
|
"""
|
|
self.ensure_one()
|
|
if not self.sale_order_id:
|
|
return
|
|
Job = self.env.get('fp.job')
|
|
if Job is None:
|
|
return
|
|
jobs = Job.sudo().search(
|
|
[('sale_order_id', '=', self.sale_order_id.id)],
|
|
)
|
|
if not jobs:
|
|
return
|
|
for job in jobs:
|
|
portal = job.portal_job_id
|
|
if not portal:
|
|
continue
|
|
vals = {}
|
|
if self.tracking_number and portal.tracking_ref != self.tracking_number:
|
|
vals['tracking_ref'] = self.tracking_number
|
|
# Packing slip lives on the linked fp.delivery, not the
|
|
# shipment. Walk it lazily here so a packing-slip generated
|
|
# earlier on the delivery also lands on the portal job.
|
|
delivery = job.delivery_id
|
|
if (delivery
|
|
and 'packing_list_attachment_id' in delivery._fields
|
|
and delivery.packing_list_attachment_id
|
|
and portal.packing_list_attachment_id !=
|
|
delivery.packing_list_attachment_id):
|
|
vals['packing_list_attachment_id'] = (
|
|
delivery.packing_list_attachment_id.id
|
|
)
|
|
# Once a tracking number exists, the parts have been picked
|
|
# by the carrier (or are about to be) — advance the portal
|
|
# state to 'shipped' so the customer sees their order is
|
|
# on its way. The 'delivered' status flips when FedEx
|
|
# tracking reports the delivery.
|
|
if self.tracking_number and portal.state in (
|
|
'received', 'in_progress', 'ready_to_ship',
|
|
):
|
|
vals['state'] = 'shipped'
|
|
if vals:
|
|
portal.sudo().write(vals)
|