- fusion_claims: separated field service logic, updated controllers/views - fusion_tasks: updated task views and map integration - fusion_authorizer_portal: added page 11 signing, schedule booking, migrations - fusion_shipping: new standalone shipping module (Canada Post, FedEx, DHL, Purolator) - fusion_ltc_management: new standalone LTC management module
748 lines
26 KiB
Python
748 lines
26 KiB
Python
import base64
|
|
import logging
|
|
from datetime import datetime as dt_mod
|
|
from lxml import etree
|
|
from requests import request
|
|
from odoo import models, fields, api, _
|
|
from odoo.exceptions import ValidationError
|
|
from odoo.addons.fusion_shipping.api.canada_post.response import Response
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class FusionShipment(models.Model):
|
|
_name = 'fusion.shipment'
|
|
_description = 'Fusion Shipment'
|
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
|
_order = 'shipment_date desc, id desc'
|
|
_rec_name = 'name'
|
|
|
|
name = fields.Char(
|
|
string='Reference',
|
|
required=True,
|
|
readonly=True,
|
|
default=lambda self: _('New'),
|
|
copy=False,
|
|
)
|
|
carrier_type = fields.Selection(
|
|
[
|
|
('canada_post', 'Canada Post'),
|
|
('ups', 'UPS'),
|
|
('ups_rest', 'UPS (REST)'),
|
|
('fedex', 'FedEx'),
|
|
('fedex_rest', 'FedEx (REST)'),
|
|
('dhl', 'DHL Express'),
|
|
('dhl_rest', 'DHL Express (REST)'),
|
|
],
|
|
string='Carrier Type',
|
|
readonly=True,
|
|
tracking=True,
|
|
help='The shipping carrier used for this shipment.',
|
|
)
|
|
tracking_number = fields.Char(
|
|
string='Tracking Number',
|
|
index=True,
|
|
readonly=True,
|
|
copy=False,
|
|
tracking=True,
|
|
)
|
|
shipment_id = fields.Char(
|
|
string='CP Shipment ID',
|
|
readonly=True,
|
|
copy=False,
|
|
help='Canada Post internal shipment identifier',
|
|
)
|
|
sale_order_id = fields.Many2one(
|
|
'sale.order',
|
|
string='Sale Order',
|
|
index=True,
|
|
ondelete='set null',
|
|
tracking=True,
|
|
)
|
|
picking_id = fields.Many2one(
|
|
'stock.picking',
|
|
string='Transfer',
|
|
index=True,
|
|
ondelete='set null',
|
|
tracking=True,
|
|
)
|
|
carrier_id = fields.Many2one(
|
|
'delivery.carrier',
|
|
string='Carrier',
|
|
ondelete='set null',
|
|
)
|
|
shipment_date = fields.Datetime(
|
|
string='Shipment Date',
|
|
default=fields.Datetime.now,
|
|
tracking=True,
|
|
)
|
|
status = fields.Selection(
|
|
[
|
|
('draft', 'Draft'),
|
|
('confirmed', 'Confirmed'),
|
|
('shipped', 'Shipped'),
|
|
('delivered', 'Delivered'),
|
|
('returned', 'Returned'),
|
|
('cancelled', 'Cancelled'),
|
|
],
|
|
string='Status',
|
|
default='confirmed',
|
|
tracking=True,
|
|
)
|
|
# Label attachments (Many2one for storage)
|
|
label_attachment_id = fields.Many2one(
|
|
'ir.attachment',
|
|
string='Printable Label (4x6)',
|
|
ondelete='set null',
|
|
help='The thermal printer label (4x6 format)',
|
|
)
|
|
full_label_attachment_id = fields.Many2one(
|
|
'ir.attachment',
|
|
string='Full Label (8.5x11)',
|
|
ondelete='set null',
|
|
help='Full page label with instructions and receipt',
|
|
)
|
|
receipt_attachment_id = fields.Many2one(
|
|
'ir.attachment',
|
|
string='Receipt',
|
|
ondelete='set null',
|
|
)
|
|
commercial_invoice_attachment_id = fields.Many2one(
|
|
'ir.attachment',
|
|
string='Commercial Invoice',
|
|
ondelete='set null',
|
|
)
|
|
# Return label
|
|
return_tracking_number = fields.Char(
|
|
string='Return Tracking Number',
|
|
readonly=True,
|
|
copy=False,
|
|
)
|
|
return_label_attachment_id = fields.Many2one(
|
|
'ir.attachment',
|
|
string='Return Label',
|
|
ondelete='set null',
|
|
)
|
|
# Shipment details
|
|
shipping_cost = fields.Monetary(
|
|
string='Shipping Cost',
|
|
currency_field='currency_id',
|
|
readonly=True,
|
|
)
|
|
service_type = fields.Char(
|
|
string='Service Type',
|
|
readonly=True,
|
|
)
|
|
weight = fields.Float(
|
|
string='Weight',
|
|
digits='Stock Weight',
|
|
readonly=True,
|
|
)
|
|
package_name = fields.Char(
|
|
string='Package',
|
|
readonly=True,
|
|
help='Name of the package this shipment covers',
|
|
)
|
|
# Address fields (computed)
|
|
sender_name = fields.Char(
|
|
string='Sender',
|
|
compute='_compute_sender_fields',
|
|
store=True,
|
|
)
|
|
sender_address = fields.Char(
|
|
string='Sender Address',
|
|
compute='_compute_sender_fields',
|
|
store=True,
|
|
)
|
|
recipient_name = fields.Char(
|
|
string='Recipient',
|
|
compute='_compute_recipient_fields',
|
|
store=True,
|
|
)
|
|
recipient_address = fields.Char(
|
|
string='Recipient Address',
|
|
compute='_compute_recipient_fields',
|
|
store=True,
|
|
)
|
|
company_id = fields.Many2one(
|
|
'res.company',
|
|
string='Company',
|
|
default=lambda self: self.env.company,
|
|
)
|
|
currency_id = fields.Many2one(
|
|
'res.currency',
|
|
related='company_id.currency_id',
|
|
store=True,
|
|
)
|
|
# Tracking history
|
|
tracking_event_ids = fields.One2many(
|
|
'fusion.tracking.event',
|
|
'shipment_id',
|
|
string='Tracking Events',
|
|
)
|
|
tracking_event_count = fields.Integer(
|
|
string='Tracking Events',
|
|
compute='_compute_tracking_event_count',
|
|
)
|
|
last_tracking_update = fields.Datetime(
|
|
string='Last Tracking Update',
|
|
readonly=True,
|
|
)
|
|
delivery_date = fields.Datetime(
|
|
string='Delivery Date',
|
|
readonly=True,
|
|
)
|
|
|
|
def _compute_tracking_event_count(self):
|
|
for rec in self:
|
|
rec.tracking_event_count = len(rec.tracking_event_ids)
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
for vals in vals_list:
|
|
if vals.get('name', _('New')) == _('New'):
|
|
vals['name'] = self.env['ir.sequence'].next_by_code(
|
|
'fusion.shipment'
|
|
) or _('New')
|
|
return super().create(vals_list)
|
|
|
|
@api.depends('picking_id', 'picking_id.picking_type_id.warehouse_id.partner_id')
|
|
def _compute_sender_fields(self):
|
|
for rec in self:
|
|
partner = (
|
|
rec.picking_id.picking_type_id.warehouse_id.partner_id
|
|
if rec.picking_id
|
|
else False
|
|
)
|
|
if partner:
|
|
rec.sender_name = partner.name or ''
|
|
parts = filter(None, [
|
|
partner.street,
|
|
partner.city,
|
|
partner.state_id.name if partner.state_id else '',
|
|
partner.zip,
|
|
])
|
|
rec.sender_address = ', '.join(parts)
|
|
else:
|
|
rec.sender_name = ''
|
|
rec.sender_address = ''
|
|
|
|
@api.depends('picking_id', 'picking_id.partner_id')
|
|
def _compute_recipient_fields(self):
|
|
for rec in self:
|
|
partner = rec.picking_id.partner_id if rec.picking_id else False
|
|
if partner:
|
|
rec.recipient_name = partner.name or ''
|
|
parts = filter(None, [
|
|
partner.street,
|
|
partner.city,
|
|
partner.state_id.name if partner.state_id else '',
|
|
partner.zip,
|
|
])
|
|
rec.recipient_address = ', '.join(parts)
|
|
else:
|
|
rec.recipient_name = ''
|
|
rec.recipient_address = ''
|
|
|
|
def action_open_sale_order(self):
|
|
self.ensure_one()
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': 'Sale Order',
|
|
'res_model': 'sale.order',
|
|
'res_id': self.sale_order_id.id,
|
|
'view_mode': 'form',
|
|
'target': 'current',
|
|
}
|
|
|
|
def action_open_picking(self):
|
|
self.ensure_one()
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': 'Transfer',
|
|
'res_model': 'stock.picking',
|
|
'res_id': self.picking_id.id,
|
|
'view_mode': 'form',
|
|
'target': 'current',
|
|
}
|
|
|
|
def _action_open_attachment(self, attachment):
|
|
"""Open an attachment PDF in the browser viewer (new tab)."""
|
|
self.ensure_one()
|
|
if not attachment:
|
|
return False
|
|
return {
|
|
'type': 'ir.actions.act_url',
|
|
'url': '/web/content/%s?download=false' % attachment.id,
|
|
'target': 'new',
|
|
}
|
|
|
|
def action_view_label(self):
|
|
return self._action_open_attachment(self.label_attachment_id)
|
|
|
|
def action_view_full_label(self):
|
|
return self._action_open_attachment(self.full_label_attachment_id)
|
|
|
|
def action_view_receipt(self):
|
|
return self._action_open_attachment(self.receipt_attachment_id)
|
|
|
|
def action_view_commercial_invoice(self):
|
|
return self._action_open_attachment(self.commercial_invoice_attachment_id)
|
|
|
|
# ── Tracking ──────────────────────────────────────────────
|
|
|
|
def action_refresh_tracking(self):
|
|
"""Fetch latest tracking events from Canada Post VIS API."""
|
|
self.ensure_one()
|
|
if not self.tracking_number:
|
|
raise ValidationError(
|
|
_("No tracking number available for this shipment."))
|
|
carrier = self.carrier_id
|
|
if not carrier:
|
|
raise ValidationError(
|
|
_("No carrier linked to this shipment."))
|
|
|
|
# VIS tracking uses /vis/ path, not /rs/
|
|
if carrier.prod_environment:
|
|
base = "https://soa-gw.canadapost.ca"
|
|
else:
|
|
base = "https://ct.soa-gw.canadapost.ca"
|
|
|
|
url = "%s/vis/track/pin/%s/detail" % (base, self.tracking_number)
|
|
headers = {
|
|
'Accept': 'application/vnd.cpc.track-v2+xml',
|
|
'Accept-language': 'en-CA',
|
|
}
|
|
|
|
try:
|
|
resp = request(
|
|
method='GET', url=url, headers=headers,
|
|
auth=(carrier.username, carrier.password))
|
|
_logger.info("Tracking API %s → %s", url, resp.status_code)
|
|
|
|
if resp.status_code == 404:
|
|
# "No Pin History" — normal for new/sandbox shipments
|
|
self.last_tracking_update = fields.Datetime.now()
|
|
self.message_post(
|
|
body=_("No tracking history available yet for %s.") %
|
|
self.tracking_number)
|
|
return
|
|
|
|
if resp.status_code != 200:
|
|
raise ValidationError(
|
|
_("Canada Post tracking error: %s %s\n%s") % (
|
|
resp.status_code, resp.reason,
|
|
resp.text[:500] if resp.text else ''))
|
|
api_resp = Response(resp)
|
|
result = api_resp.dict()
|
|
except ValidationError:
|
|
raise
|
|
except Exception as e:
|
|
raise ValidationError(
|
|
_("Failed to fetch tracking: %s") % str(e))
|
|
|
|
self._process_tracking_events(result)
|
|
|
|
def _process_tracking_events(self, result):
|
|
"""Parse CP tracking detail response and store events."""
|
|
detail = result.get('tracking-detail', result)
|
|
sig_events = detail.get('significant-events', {})
|
|
occurrences = sig_events.get('occurrence', [])
|
|
|
|
# Single event returns dict, multiple returns list
|
|
if isinstance(occurrences, dict):
|
|
occurrences = [occurrences]
|
|
|
|
# Replace existing events
|
|
self.tracking_event_ids.unlink()
|
|
|
|
vals_list = []
|
|
for occ in occurrences:
|
|
event_date_str = occ.get('event-date', '')
|
|
event_time_str = occ.get('event-time', '')
|
|
|
|
# Build combined datetime for sorting
|
|
event_datetime = False
|
|
if event_date_str:
|
|
try:
|
|
if event_time_str:
|
|
event_datetime = dt_mod.strptime(
|
|
'%s %s' % (event_date_str, event_time_str),
|
|
'%Y-%m-%d %H:%M:%S')
|
|
else:
|
|
event_datetime = dt_mod.strptime(
|
|
event_date_str, '%Y-%m-%d')
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
vals_list.append({
|
|
'shipment_id': self.id,
|
|
'event_date': event_date_str or False,
|
|
'event_time': event_time_str or '',
|
|
'event_datetime': event_datetime,
|
|
'event_description': occ.get('event-description', ''),
|
|
'event_type': occ.get('event-type', ''),
|
|
'event_site': occ.get('event-site', ''),
|
|
'event_province': occ.get('event-province', ''),
|
|
'signatory_name': occ.get('signatory-name', ''),
|
|
})
|
|
|
|
if vals_list:
|
|
self.env['fusion.tracking.event'].create(vals_list)
|
|
|
|
self.last_tracking_update = fields.Datetime.now()
|
|
self._update_status_from_tracking(detail)
|
|
self.message_post(
|
|
body=_("Tracking refreshed: %d events loaded.") % len(vals_list))
|
|
|
|
def _update_status_from_tracking(self, detail):
|
|
"""Auto-update shipment status based on tracking data."""
|
|
if self.status == 'cancelled':
|
|
return
|
|
|
|
delivered_date = detail.get('actual-delivery-date', '')
|
|
if delivered_date:
|
|
if self._has_return_events():
|
|
self.status = 'returned'
|
|
else:
|
|
self.status = 'delivered'
|
|
try:
|
|
self.delivery_date = dt_mod.strptime(
|
|
delivered_date, '%Y-%m-%d')
|
|
except (ValueError, TypeError):
|
|
self.delivery_date = fields.Datetime.now()
|
|
elif self._has_return_events():
|
|
self.status = 'returned'
|
|
elif self.status == 'confirmed' and self.tracking_event_ids:
|
|
self.status = 'shipped'
|
|
|
|
def _has_return_events(self):
|
|
"""Check if any tracking events indicate a return/RTS."""
|
|
RETURN_TYPES = {'RTS', 'RETURN', 'RTS_LABEL_PROC'}
|
|
for event in self.tracking_event_ids:
|
|
if event.event_type in RETURN_TYPES:
|
|
return True
|
|
desc = (event.event_description or '').lower()
|
|
if 'return to sender' in desc or 'item returned' in desc:
|
|
return True
|
|
return False
|
|
|
|
# ── Void & Reissue ─────────────────────────────────────
|
|
|
|
def action_void_shipment(self):
|
|
"""Void this shipment via Canada Post API (DELETE endpoint)."""
|
|
self.ensure_one()
|
|
if self.status == 'cancelled':
|
|
raise ValidationError(
|
|
_("This shipment is already cancelled."))
|
|
if not self.shipment_id:
|
|
raise ValidationError(
|
|
_("No CP shipment ID — cannot void."))
|
|
|
|
carrier = self.carrier_id
|
|
if not carrier:
|
|
raise ValidationError(
|
|
_("No carrier linked to this shipment."))
|
|
|
|
if carrier.prod_environment:
|
|
base = "https://soa-gw.canadapost.ca"
|
|
else:
|
|
base = "https://ct.soa-gw.canadapost.ca"
|
|
customer = carrier.customer_number
|
|
|
|
if carrier.fusion_cp_type == 'commercial':
|
|
url = "%s/rs/%s/%s/shipment/%s" % (
|
|
base, customer, customer, self.shipment_id)
|
|
accept_header = 'application/vnd.cpc.shipment-v8+xml'
|
|
else:
|
|
url = "%s/rs/%s/ncshipment/%s" % (
|
|
base, customer, self.shipment_id)
|
|
accept_header = 'application/vnd.cpc.ncshipment-v4+xml'
|
|
|
|
try:
|
|
resp = request(
|
|
method='DELETE', url=url,
|
|
headers={
|
|
'Accept': accept_header,
|
|
'Accept-language': 'en-CA',
|
|
},
|
|
auth=(carrier.username, carrier.password))
|
|
_logger.info(
|
|
"Void Shipment %s → %s", url, resp.status_code)
|
|
|
|
if resp.status_code == 204:
|
|
self.status = 'cancelled'
|
|
self.message_post(
|
|
body=_("Shipment voided successfully via "
|
|
"Canada Post API."))
|
|
else:
|
|
# Parse CP error XML for a clean message
|
|
error_msg = self._parse_cp_error_response(resp)
|
|
except ValidationError:
|
|
raise
|
|
except Exception as e:
|
|
raise ValidationError(
|
|
_("Failed to void shipment: %s") % str(e))
|
|
|
|
def _parse_cp_error_response(self, resp):
|
|
"""Parse a Canada Post error XML response and raise a clean
|
|
ValidationError with the human-readable description.
|
|
|
|
If the response cannot be parsed, falls back to the raw text.
|
|
"""
|
|
description = ''
|
|
try:
|
|
root = etree.fromstring(resp.content)
|
|
# Strip namespace for easy tag matching
|
|
ns = {'cp': root.nsmap.get(None, '')}
|
|
if ns['cp']:
|
|
msgs = root.findall('.//cp:message', ns)
|
|
else:
|
|
msgs = root.findall('.//message')
|
|
parts = []
|
|
for msg in msgs:
|
|
desc_el = msg.find(
|
|
'cp:description', ns) if ns['cp'] else msg.find(
|
|
'description')
|
|
if desc_el is not None and desc_el.text:
|
|
parts.append(desc_el.text.strip())
|
|
if parts:
|
|
description = '\n'.join(parts)
|
|
except Exception:
|
|
description = ''
|
|
|
|
if not description:
|
|
description = (resp.text[:500]
|
|
if resp.text else 'Unknown error')
|
|
|
|
raise ValidationError(
|
|
_("Canada Post: %s") % description)
|
|
|
|
def action_reissue_shipment(self):
|
|
"""Void current shipment and create a new one."""
|
|
self.ensure_one()
|
|
if self.status != 'cancelled':
|
|
self.action_void_shipment()
|
|
if not self.picking_id:
|
|
raise ValidationError(
|
|
_("No transfer linked — cannot reissue."))
|
|
picking = self.picking_id
|
|
carrier = picking.carrier_id or self.carrier_id
|
|
if not carrier:
|
|
raise ValidationError(
|
|
_("No carrier found for reissue."))
|
|
result = carrier.send_shipping(picking)
|
|
if result:
|
|
picking.carrier_tracking_ref = result[0].get(
|
|
'tracking_number', '')
|
|
self.message_post(
|
|
body=_("Shipment reissued. See new shipment record."))
|
|
|
|
def action_track_on_carrier(self):
|
|
"""Open the carrier's tracking website in a new tab."""
|
|
self.ensure_one()
|
|
if not self.tracking_number:
|
|
raise ValidationError(
|
|
_("No tracking number available for this shipment."))
|
|
# Default tracking URLs per carrier type
|
|
DEFAULT_TRACKING = {
|
|
'canada_post': 'https://www.canadapost.ca/trackweb/en#/resultList?searchFor=',
|
|
'ups': 'https://www.ups.com/track?tracknum=',
|
|
'ups_rest': 'https://www.ups.com/track?tracknum=',
|
|
'fedex': 'https://www.fedex.com/wtrk/track/?trknbr=',
|
|
'fedex_rest': 'https://www.fedex.com/wtrk/track/?trknbr=',
|
|
'dhl': 'https://www.dhl.com/en/express/tracking.html?AWB=',
|
|
'dhl_rest': 'https://www.dhl.com/en/express/tracking.html?AWB=',
|
|
}
|
|
base_link = ''
|
|
if self.carrier_id and self.carrier_id.tracking_link:
|
|
base_link = self.carrier_id.tracking_link
|
|
elif self.carrier_type:
|
|
base_link = DEFAULT_TRACKING.get(self.carrier_type, '')
|
|
if not base_link:
|
|
base_link = 'https://www.google.com/search?q='
|
|
return {
|
|
'type': 'ir.actions.act_url',
|
|
'url': '%s%s' % (base_link, self.tracking_number),
|
|
'target': 'new',
|
|
}
|
|
|
|
# ── Cron ─────────────────────────────────────────────────
|
|
|
|
@api.model
|
|
def _cron_refresh_tracking(self):
|
|
"""Auto-refresh tracking for all active (non-terminal) shipments."""
|
|
shipments = self.search([
|
|
('status', 'in', ('confirmed', 'shipped')),
|
|
('tracking_number', '!=', False),
|
|
])
|
|
_logger.info(
|
|
"Cron: refreshing tracking for %d shipments", len(shipments))
|
|
for shipment in shipments:
|
|
try:
|
|
shipment.action_refresh_tracking()
|
|
self.env.cr.commit()
|
|
except Exception as e:
|
|
self.env.cr.rollback()
|
|
_logger.warning(
|
|
"Cron: tracking refresh failed for %s: %s",
|
|
shipment.name, str(e))
|
|
|
|
# ── Return Labels ────────────────────────────────────────
|
|
|
|
def action_view_return_label(self):
|
|
return self._action_open_attachment(self.return_label_attachment_id)
|
|
|
|
def action_create_return_label(self):
|
|
"""Create a prepaid return label via Canada Post Authorized
|
|
Returns API. Bill-on-scan: charged only when the customer
|
|
uses the label at a post office.
|
|
"""
|
|
self.ensure_one()
|
|
if self.return_tracking_number:
|
|
raise ValidationError(
|
|
_("A return label has already been created "
|
|
"for this shipment."))
|
|
if not self.carrier_id:
|
|
raise ValidationError(
|
|
_("No carrier linked to this shipment."))
|
|
if not self.picking_id:
|
|
raise ValidationError(
|
|
_("No transfer linked to this shipment."))
|
|
|
|
carrier = self.carrier_id
|
|
customer = carrier.customer_number
|
|
|
|
warehouse_partner = (
|
|
self.picking_id.picking_type_id.warehouse_id.partner_id)
|
|
if not warehouse_partner:
|
|
raise ValidationError(
|
|
_("No warehouse address found. Please configure "
|
|
"a contact on the warehouse."))
|
|
|
|
if carrier.prod_environment:
|
|
base = "https://soa-gw.canadapost.ca"
|
|
else:
|
|
base = "https://ct.soa-gw.canadapost.ca"
|
|
|
|
url = "%s/rs/%s/%s/authorizedreturn" % (base, customer, customer)
|
|
|
|
xml = self._build_return_label_xml(carrier, warehouse_partner)
|
|
headers = {
|
|
'Content-Type': 'application/vnd.cpc.authreturn-v2+xml',
|
|
'Accept': 'application/vnd.cpc.authreturn-v2+xml',
|
|
'Accept-language': 'en-CA',
|
|
}
|
|
|
|
try:
|
|
resp = request(
|
|
method='POST', url=url, data=xml,
|
|
headers=headers,
|
|
auth=(carrier.username, carrier.password))
|
|
_logger.info(
|
|
"Return Label API %s → %s", url, resp.status_code)
|
|
|
|
if resp.status_code not in (200, 201):
|
|
self._parse_cp_error_response(resp)
|
|
|
|
self._process_return_label_response(resp, carrier)
|
|
|
|
except ValidationError:
|
|
raise
|
|
except Exception as e:
|
|
raise ValidationError(
|
|
_("Failed to create return label: %s") % str(e))
|
|
|
|
def _build_return_label_xml(self, carrier, return_to_partner):
|
|
"""Build Authorized Return XML.
|
|
|
|
return_to_partner = warehouse address (where the return goes).
|
|
"""
|
|
ns = "http://www.canadapost.ca/ws/authreturn-v2"
|
|
root = etree.Element("authorized-return", xmlns=ns)
|
|
|
|
etree.SubElement(root, "service-code").text = "DOM.EP"
|
|
|
|
# Receiver — warehouse where the item is returned to
|
|
receiver = etree.SubElement(root, "receiver")
|
|
rec_name = etree.SubElement(receiver, "name")
|
|
rec_name.text = (return_to_partner.name or '')[:44]
|
|
rec_company = etree.SubElement(receiver, "company")
|
|
rec_company.text = (
|
|
return_to_partner.commercial_company_name
|
|
or return_to_partner.name or '')[:44]
|
|
rec_addr = etree.SubElement(receiver, "domestic-address")
|
|
etree.SubElement(rec_addr, "address-line-1").text = (
|
|
return_to_partner.street or '')[:44]
|
|
if return_to_partner.street2:
|
|
etree.SubElement(rec_addr, "address-line-2").text = (
|
|
return_to_partner.street2)[:44]
|
|
etree.SubElement(rec_addr, "city").text = (
|
|
return_to_partner.city or '')[:40]
|
|
etree.SubElement(rec_addr, "province").text = (
|
|
return_to_partner.state_id.code or '')
|
|
etree.SubElement(rec_addr, "postal-code").text = (
|
|
(return_to_partner.zip or '').replace(' ', ''))
|
|
|
|
# Parcel characteristics
|
|
parcel = etree.SubElement(root, "parcel-characteristics")
|
|
etree.SubElement(parcel, "weight").text = str(
|
|
max(self.weight or 0.5, 0.1))
|
|
|
|
# Print preferences
|
|
prefs = etree.SubElement(root, "print-preferences")
|
|
etree.SubElement(prefs, "output-format").text = (
|
|
carrier.fusion_cp_output_format or '8.5x11')
|
|
|
|
return etree.tostring(
|
|
root, xml_declaration=True, encoding='UTF-8')
|
|
|
|
def _process_return_label_response(self, resp, carrier):
|
|
"""Parse return label response, download label PDF,
|
|
store on shipment.
|
|
"""
|
|
api_resp = Response(resp)
|
|
result = api_resp.dict()
|
|
|
|
auth_return = result.get('authorized-return', result)
|
|
tracking_pin = auth_return.get('tracking-pin', '')
|
|
self.return_tracking_number = tracking_pin
|
|
|
|
# Get label artifact URL from links
|
|
links = auth_return.get('links', {})
|
|
link_list = links.get('link', [])
|
|
if isinstance(link_list, dict):
|
|
link_list = [link_list]
|
|
|
|
label_url = ''
|
|
for link in link_list:
|
|
if link.get('@rel') == 'labelDetails':
|
|
label_url = link.get('@href', '')
|
|
break
|
|
|
|
if label_url:
|
|
label_resp = request(
|
|
method='GET', url=label_url,
|
|
headers={
|
|
'Accept': 'application/pdf',
|
|
'Accept-language': 'en-CA',
|
|
},
|
|
auth=(carrier.username, carrier.password))
|
|
if label_resp.status_code == 200:
|
|
attachment = self.env['ir.attachment'].create({
|
|
'name': 'Return-Label-%s.pdf' % tracking_pin,
|
|
'type': 'binary',
|
|
'datas': base64.b64encode(
|
|
label_resp.content).decode(),
|
|
'res_model': self._name,
|
|
'res_id': self.id,
|
|
'mimetype': 'application/pdf',
|
|
})
|
|
self.return_label_attachment_id = attachment
|
|
|
|
self.message_post(
|
|
body=_("Return label created. Tracking: %s") % tracking_pin,
|
|
attachment_ids=(
|
|
self.return_label_attachment_id.ids
|
|
if self.return_label_attachment_id else []))
|