# -*- coding: utf-8 -*-
# Unified Delivery Carrier for all shipping integrations
# Part of Fusion Shipping. See LICENSE file for full copyright and licensing details.

import string
import random
import base64
import logging
import time
import json
from json import JSONDecodeError

import pytz
from datetime import timedelta
from requests import request as http_request
import xml.etree.ElementTree as etree

from markupsafe import Markup

from odoo import api, models, fields, _, tools
from odoo.exceptions import UserError, ValidationError, RedirectWarning
from odoo.tools import pdf, float_repr
from odoo.tools.float_utils import float_round, json_float_round
from odoo.tools.misc import groupby
from odoo.tools.safe_eval import const_eval

# Canada Post
from odoo.addons.fusion_shipping.api.canada_post.response import Response

# UPS SOAP
from odoo.addons.fusion_shipping.api.ups.request import UPSRequest

# UPS REST
from odoo.addons.fusion_shipping.api.ups_rest.request import UPSRequest as UPSRestRequest

# FedEx SOAP
from odoo.addons.fusion_shipping.api.fedex.request import FedexRequest, _convert_curr_iso_fdx, _convert_curr_fdx_iso

# FedEx REST
from odoo.addons.fusion_shipping.api.fedex_rest.request import FedexRequest as FedexRestRequest

# DHL SOAP
from odoo.addons.fusion_shipping.api.dhl.request import DHLProvider

# DHL REST
from odoo.addons.fusion_shipping.api.dhl_rest.request import DHLProvider as DHLRestProvider

try:
    from odoo.tools.zeep.helpers import serialize_object
except ImportError:
    serialize_object = None


_logger = logging.getLogger(__name__)


# ── FedEx constants ──────────────────────────────────────────────────────────

FEDEX_STOCK_TYPE = [
    ('PAPER_4X6', 'PAPER_4X6'),
    ('PAPER_4X6.75', 'PAPER_4X6.75'),
    ('PAPER_4X8', 'PAPER_4X8'),
    ('PAPER_4X9', 'PAPER_4X9'),
    ('PAPER_7X4.75', 'PAPER_7X4.75'),
    ('PAPER_8.5X11_BOTTOM_HALF_LABEL', 'PAPER_8.5X11_BOTTOM_HALF_LABEL'),
    ('PAPER_8.5X11_TOP_HALF_LABEL', 'PAPER_8.5X11_TOP_HALF_LABEL'),
    ('PAPER_LETTER', 'PAPER_LETTER'),
    ('STOCK_4X6', 'STOCK_4X6'),
    ('STOCK_4X6.75', 'STOCK_4X6.75'),
    ('STOCK_4X6.75_LEADING_DOC_TAB', 'STOCK_4X6.75_LEADING_DOC_TAB'),
    ('STOCK_4X6.75_TRAILING_DOC_TAB', 'STOCK_4X6.75_TRAILING_DOC_TAB'),
    ('STOCK_4X8', 'STOCK_4X8'),
    ('STOCK_4X9', 'STOCK_4X9'),
    ('STOCK_4X9_LEADING_DOC_TAB', 'STOCK_4X9_LEADING_DOC_TAB'),
    ('STOCK_4X9_TRAILING_DOC_TAB', 'STOCK_4X9_TRAILING_DOC_TAB'),
]

FEDEX_SOAP_HELP_EXTRA_DATA = """The extra data in FedEx is organized like the inside of a json file.
This functionality is advanced/technical and should only be used if you know what you are doing.

Example of valid value: ```
"ShipmentDetails": {"Pieces": {"Piece": {"AdditionalInformation": "extra info"}}}
```

With the above example, the AdditionalInformation of each piece will be updated.
More info on https://www.fedex.com/en-us/developer/web-services/process.html#documentation"""

FEDEX_REST_HELP_EXTRA_DATA = """The extra data in FedEx is organized like the inside of a json file.
This functionality is advanced/technical and should only be used if you know what you are doing.
More info on https://www.developer.fedex.com"""

DHL_REST_HELP_EXTRA_DATA = """Extra data to be sent in the request. It should be JSON-formatted.
This functionality is advanced/technical and should only be used if you know what you are doing.

Example of a valid value: ```
"content": {"packages": {"description": "amazing package"}}
```

With the above example, the description of each package will be updated.
For more information, please refer to the DHL API documentation: https://developer.dhl.com/api-reference/dhl-express-mydhl-api.
"""


# ═══════════════════════════════════════════════════════════════════════════════
# UNIFIED DELIVERY CARRIER
# ═══════════════════════════════════════════════════════════════════════════════

class DeliveryCarrier(models.Model):
    _inherit = 'delivery.carrier'

    # ── delivery_type with ALL carrier options ────────────────────────────────

    delivery_type = fields.Selection(selection_add=[
        ('fusion_canada_post', 'Canada Post'),
        ('fusion_ups', 'UPS (Legacy)'),
        ('fusion_ups_rest', 'UPS'),
        ('fusion_fedex', 'FedEx (Legacy)'),
        ('fusion_fedex_rest', 'FedEx'),
        ('fusion_dhl', 'DHL Express (Legacy)'),
        ('fusion_dhl_rest', 'DHL Express'),
    ], ondelete={
        'fusion_canada_post': lambda recs: recs.write({'delivery_type': 'fixed', 'fixed_price': 0}),
        'fusion_ups': lambda recs: recs.write({'delivery_type': 'fixed', 'fixed_price': 0}),
        'fusion_ups_rest': lambda recs: recs.write({'delivery_type': 'fixed', 'fixed_price': 0}),
        'fusion_fedex': lambda recs: recs.write({'delivery_type': 'fixed', 'fixed_price': 0}),
        'fusion_fedex_rest': lambda recs: recs.write({'delivery_type': 'fixed', 'fixed_price': 0}),
        'fusion_dhl': lambda recs: recs.write({'delivery_type': 'fixed', 'fixed_price': 0}),
        'fusion_dhl_rest': lambda recs: recs.write({'delivery_type': 'fixed', 'fixed_price': 0}),
    })

    # ══════════════════════════════════════════════════════════════════════════
    # CANADA POST FIELDS
    # ══════════════════════════════════════════════════════════════════════════

    option_code = fields.Selection([
        ('SO', 'Signature Required'),
        ('COV', 'Coverage (Insurance)'),
        ('COD', 'Collect on Delivery'),
        ('PA18', 'Proof of Age – 18+'),
        ('PA19', 'Proof of Age – 19+'),
        ('HFP', 'Card for Pickup'),
        ('DNS', 'Do Not Safe Drop'),
        ('LAD', 'Leave at Door'),
    ], string="Shipping Option",
       help="Optional delivery option applied to all shipments using this carrier.")
    service_type = fields.Selection([
        ('DOM.RP', 'Regular Parcel'),
        ('DOM.EP', 'Expedited Parcel'),
        ('DOM.XP', 'Xpresspost'),
        ('DOM.PC', 'Priority'),
        ('USA.XP', 'Xpresspost USA'),
        ('USA.EP', 'Expedited Parcel USA'),
        ('INT.IP.SURF', 'International Parcel – Surface'),
        ('INT.PW.PARCEL', 'Priority Worldwide Parcel'),
        ('INT.XP', 'Xpresspost International'),
    ], string="Default Service",
       help="Default Canada Post service. Users can select a different service when adding shipping to a sale order.")
    product_packaging_id = fields.Many2one('stock.package.type', string="Default Package Type",
        help="Optional default package type. Dimensions will pre-fill in the shipping wizard but can be overridden per shipment.")

    reason_for_export = fields.Selection([
        ('DOC', 'Document'),
        ('SAM', 'Commercial Sample'),
        ('REP', 'Repair or Warranty'),
        ('SOG', 'Sale of Goods'),
        ('OTH', 'Other'),
    ], string="Reason for Export", default="SOG",
       help="Required for international and cross-border shipments.")

    username = fields.Char("Username", copy=False, help="API key from Canada Post Developer Program.")
    password = fields.Char("Password", copy=False, help="API password from Canada Post Developer Program.")
    customer_number = fields.Char("Customer Number", copy=False, help="Your Canada Post customer number (mailed-by number).")
    tracking_link = fields.Char(string="Tracking Link",
        help="Base URL for tracking shipments. Leave blank to use the default Canada Post tracking page.", size=256)

    fusion_cp_type = fields.Selection(selection=[('commercial', 'Contract Shipping'), ('counter', 'Non-Contract Shipping')],
        string="Customer Type", required=False)
    fusion_cp_contract_id = fields.Char(string="Contract ID")
    fusion_cp_payment_method = fields.Selection([('CreditCard', 'Credit Card'), ('Account', 'Account')],
        string="Payment Method", default='CreditCard',
        help="How Canada Post charges for shipments on this account.")
    fusion_cp_output_format = fields.Selection(
        [('4x6', '4x6 Thermal Label'), ('8.5x11', '8.5x11 Full Page')],
        string="Label Output Format",
        default='4x6',
        help="Label format returned by Canada Post. "
             "4x6 is for thermal label printers, "
             "8.5x11 is the full page format.",
    )
    fusion_cp_dimension_unit = fields.Selection(
        [('cm', 'Centimeters (cm)'), ('in', 'Inches (in)')],
        string="Package Dimension Unit",
        default='cm',
        help="Unit of measurement for package dimensions (length, width, height). "
             "Choose the unit you use when entering dimensions on your package types. "
             "Dimensions will be automatically converted to centimeters for the Canada Post API.",
    )

    def _default_uom_in_delive(self):
        weight_uom_id = self.env.ref('uom.product_uom_kgm', raise_if_not_found=False)
        if not weight_uom_id:
            uom_categ_id = self.env.ref('uom.product_uom_categ_kgm').id
            weight_uom_id = self.env['uom.uom'].search([('category_id', '=', uom_categ_id), ('factor', '=', 1)], limit=1)
        return weight_uom_id

    weight_uom_id = fields.Many2one('uom.uom', string='API Weight Unit',
        help="Weight unit expected by the Canada Post API (kg). Used internally for converting weights before sending requests.",
        default=_default_uom_in_delive)

    # ══════════════════════════════════════════════════════════════════════════
    # UPS SOAP (Legacy) FIELDS
    # ══════════════════════════════════════════════════════════════════════════

    def _get_ups_service_types(self):
        return [
            ('03', 'UPS Ground'),
            ('11', 'UPS Standard'),
            ('01', 'UPS Next Day'),
            ('14', 'UPS Next Day AM'),
            ('13', 'UPS Next Day Air Saver'),
            ('02', 'UPS 2nd Day'),
            ('59', 'UPS 2nd Day AM'),
            ('12', 'UPS 3-day Select'),
            ('65', 'UPS Saver'),
            ('07', 'UPS Worldwide Express'),
            ('08', 'UPS Worldwide Expedited'),
            ('54', 'UPS Worldwide Express Plus'),
            ('96', 'UPS Worldwide Express Freight')
        ]

    ups_username = fields.Char(string='UPS Username', groups="base.group_system")
    ups_passwd = fields.Char(string='UPS Password', groups="base.group_system")
    ups_shipper_number = fields.Char(string='UPS Shipper Number', groups="base.group_system")
    ups_access_number = fields.Char(string='UPS Access Key', groups="base.group_system")
    ups_default_package_type_id = fields.Many2one('stock.package.type', string='UPS Legacy Package Type')
    ups_default_service_type = fields.Selection(_get_ups_service_types, string="UPS Service Type", default='03')
    ups_duty_payment = fields.Selection([('SENDER', 'Sender'), ('RECIPIENT', 'Recipient')], required=True, default="RECIPIENT")
    ups_package_weight_unit = fields.Selection([('LBS', 'Pounds'), ('KGS', 'Kilograms')], default='LBS')
    ups_package_dimension_unit = fields.Selection([('IN', 'Inches'), ('CM', 'Centimeters')], string="Package Size Unit", default='IN')
    ups_label_file_type = fields.Selection([('GIF', 'PDF'),
                                            ('ZPL', 'ZPL'),
                                            ('EPL', 'EPL'),
                                            ('SPL', 'SPL')],
                                           string="UPS Label File Type", default='GIF')
    ups_bill_my_account = fields.Boolean(string='Bill My Account',
                                         help="If checked, ecommerce users will be prompted their UPS account number\n"
                                              "and delivery fees will be charged on it.")
    ups_saturday_delivery = fields.Boolean(string='UPS Saturday Delivery',
        help='This value added service will allow you to ship the package on saturday also.')
    ups_cod_funds_code = fields.Selection(selection=[
        ('0', "Check, Cashier's Check or MoneyOrder"),
        ('8', "Cashier's Check or MoneyOrder"),
        ], string='COD Funding Option', default='0')

    # ══════════════════════════════════════════════════════════════════════════
    # UPS REST FIELDS
    # ══════════════════════════════════════════════════════════════════════════

    ups_client_id = fields.Char(string='UPS Client ID', groups="base.group_system")
    ups_client_secret = fields.Char(string='UPS Client Secret', groups="base.group_system")
    ups_access_token = fields.Char(string='UPS Access Token', groups="base.group_system")
    ups_default_packaging_id = fields.Many2one('stock.package.type', string='UPS Package Type')
    ups_require_signature = fields.Boolean("Require Signature")

    # ══════════════════════════════════════════════════════════════════════════
    # FEDEX SOAP (Legacy) FIELDS
    # ══════════════════════════════════════════════════════════════════════════

    fedex_developer_key = fields.Char(string="Developer Key", groups="base.group_system")
    fedex_developer_password = fields.Char(string="Password", groups="base.group_system")
    fedex_account_number = fields.Char(string="FedEx Legacy Account Number", groups="base.group_system")
    fedex_meter_number = fields.Char(string="Meter Number", groups="base.group_system")
    fedex_droppoff_type = fields.Selection([('BUSINESS_SERVICE_CENTER', 'BUSINESS_SERVICE_CENTER'),
                                            ('DROP_BOX', 'DROP_BOX'),
                                            ('REGULAR_PICKUP', 'REGULAR_PICKUP'),
                                            ('REQUEST_COURIER', 'REQUEST_COURIER'),
                                            ('STATION', 'STATION')],
                                           string="Fedex Drop-Off Type",
                                           default='REGULAR_PICKUP')
    fedex_default_package_type_id = fields.Many2one('stock.package.type', string="Fedex Package Type")
    fedex_service_type = fields.Selection([('INTERNATIONAL_ECONOMY', 'INTERNATIONAL_ECONOMY'),
                                           ('INTERNATIONAL_PRIORITY', 'INTERNATIONAL_PRIORITY'),
                                           ('FEDEX_INTERNATIONAL_PRIORITY', 'FEDEX_INTERNATIONAL_PRIORITY'),
                                           ('FEDEX_INTERNATIONAL_PRIORITY_EXPRESS', 'FEDEX_INTERNATIONAL_PRIORITY_EXPRESS'),
                                           ('FEDEX_GROUND', 'FEDEX_GROUND'),
                                           ('FEDEX_2_DAY', 'FEDEX_2_DAY'),
                                           ('FEDEX_2_DAY_AM', 'FEDEX_2_DAY_AM'),
                                           ('FEDEX_3_DAY_FREIGHT', 'FEDEX_3_DAY_FREIGHT'),
                                           ('FIRST_OVERNIGHT', 'FIRST_OVERNIGHT'),
                                           ('PRIORITY_OVERNIGHT', 'PRIORITY_OVERNIGHT'),
                                           ('STANDARD_OVERNIGHT', 'STANDARD_OVERNIGHT'),
                                           ('FEDEX_NEXT_DAY_EARLY_MORNING', 'FEDEX_NEXT_DAY_EARLY_MORNING'),
                                           ('FEDEX_NEXT_DAY_MID_MORNING', 'FEDEX_NEXT_DAY_MID_MORNING'),
                                           ('FEDEX_NEXT_DAY_AFTERNOON', 'FEDEX_NEXT_DAY_AFTERNOON'),
                                           ('FEDEX_NEXT_DAY_END_OF_DAY', 'FEDEX_NEXT_DAY_END_OF_DAY'),
                                           ('FEDEX_EXPRESS_SAVER', 'FEDEX_EXPRESS_SAVER'),
                                           ('FEDEX_REGIONAL_ECONOMY', 'FEDEX_REGIONAL_ECONOMY'),
                                           ('FEDEX_FIRST', 'FEDEX_FIRST'),
                                           ('FEDEX_PRIORITY_EXPRESS', 'FEDEX_PRIORITY_EXPRESS'),
                                           ('FEDEX_PRIORITY', 'FEDEX_PRIORITY'),
                                           ('FEDEX_PRIORITY_EXPRESS_FREIGHT', 'FEDEX_PRIORITY_EXPRESS_FREIGHT'),
                                           ('FEDEX_PRIORITY_FREIGHT', 'FEDEX_PRIORITY_FREIGHT'),
                                           ('FEDEX_ECONOMY_SELECT', 'FEDEX_ECONOMY_SELECT'),
                                           ('FEDEX_INTERNATIONAL_CONNECT_PLUS', 'FEDEX_INTERNATIONAL_CONNECT_PLUS'),
                                           ],
                                          default='FEDEX_INTERNATIONAL_PRIORITY')
    fedex_duty_payment = fields.Selection([('SENDER', 'Sender'), ('RECIPIENT', 'Recipient')], required=True, default="SENDER")
    fedex_weight_unit = fields.Selection([('LB', 'LB'), ('KG', 'KG')], default='LB')
    fedex_label_stock_type = fields.Selection(FEDEX_STOCK_TYPE, string='Label Type', default='PAPER_LETTER')
    fedex_label_file_type = fields.Selection([('PDF', 'PDF'),
                                              ('EPL2', 'EPL2'),
                                              ('PNG', 'PNG'),
                                              ('ZPLII', 'ZPLII')],
                                             default='PDF', string="FEDEX Label File Type")
    fedex_document_stock_type = fields.Selection(FEDEX_STOCK_TYPE, string='Commercial Invoice Type', default='PAPER_LETTER')
    fedex_saturday_delivery = fields.Boolean(string="FedEx Saturday Delivery",
        help="Special service: Saturday Delivery, can be requested on following days. "
             "Thursday: FEDEX_2_DAY. Friday: PRIORITY_OVERNIGHT, FIRST_OVERNIGHT, INTERNATIONAL_PRIORITY (To Select Countries)")
    fedex_extra_data_rate_request = fields.Text('Extra data for rate (legacy)', help=FEDEX_SOAP_HELP_EXTRA_DATA)
    fedex_extra_data_ship_request = fields.Text('Extra data for ship (legacy)', help=FEDEX_SOAP_HELP_EXTRA_DATA)
    fedex_extra_data_return_request = fields.Text('Extra data for return (legacy)', help=FEDEX_SOAP_HELP_EXTRA_DATA)

    # ══════════════════════════════════════════════════════════════════════════
    # FEDEX REST FIELDS
    # ══════════════════════════════════════════════════════════════════════════

    fedex_rest_developer_key = fields.Char(string="API Key", groups="base.group_system")
    fedex_rest_developer_password = fields.Char(string="Secret Key", groups="base.group_system")
    fedex_rest_account_number = fields.Char(string="FedEx Account Number", groups="base.group_system")
    fedex_rest_access_token = fields.Char(string="FedEx Access Token", groups="base.group_system")
    fedex_rest_droppoff_type = fields.Selection([('CONTACT_FEDEX_TO_SCHEDULE', 'Contact FedEx for pickup'),
                                                 ('DROPOFF_AT_FEDEX_LOCATION', 'Drop off at FedEx location'),
                                                 ('USE_SCHEDULED_PICKUP', 'Part of regular scheduled pickup')],
                                                string="FedEx Drop-Off Type",
                                                default='USE_SCHEDULED_PICKUP')
    fedex_rest_default_package_type_id = fields.Many2one('stock.package.type', string="FedEx Package Type")
    fedex_rest_service_type = fields.Selection([
        ('EUROPE_FIRST_INTERNATIONAL_PRIORITY', 'FedEx Europe First'),
        ('FEDEX_1_DAY_FREIGHT', 'FedEx 1Day Freight'),
        ('FEDEX_2_DAY', 'FedEx 2Day'),
        ('FEDEX_2_DAY_AM', 'FedEx 2Day AM'),
        ('FEDEX_2_DAY_FREIGHT', 'FedEx 2Day Freight'),
        ('FEDEX_3_DAY_FREIGHT', 'FedEx 3Day Freight'),
        ('FEDEX_ECONOMY', 'FedEx Economy'),
        ('FEDEX_ECONOMY_FREIGHT', 'FedEx Economy Freight'),
        ('FEDEX_ECONOMY_SELECT', 'FedEx Economy (Only U.K.)'),
        ('FEDEX_EXPRESS_SAVER', 'FedEx Express Saver'),
        ('FEDEX_FIRST', 'FedEx First'),
        ('FEDEX_FIRST_FREIGHT', 'FedEx First Overnight Freight'),
        ('FEDEX_GROUND', 'FedEx International Ground and FedEx Domestic Ground'),
        ('FEDEX_INTERNATIONAL_CONNECT_PLUS', 'FedEx International Connect Plus'),
        ('FEDEX_INTERNATIONAL_DEFERRED_FREIGHT', 'FedEx International Deferred Freight'),
        ('FEDEX_INTERNATIONAL_PRIORITY', 'FedEx International Priority'),
        ('FEDEX_INTERNATIONAL_PRIORITY_EXPRESS', 'FedEx International Priority Express'),
        ('FEDEX_PRIORITY', 'FedEx Priority'),
        ('FEDEX_PRIORITY_EXPRESS', 'FedEx Priority Express'),
        ('FEDEX_PRIORITY_EXPRESS_FREIGHT', 'FedEx Priority Express Freight'),
        ('FEDEX_PRIORITY_FREIGHT', 'FedEx Priority Freight'),
        ('FEDEX_REGIONAL_ECONOMY', 'FedEx Regional Economy'),
        ('FEDEX_REGIONAL_ECONOMY_FREIGHT', 'FedEx Regional Economy Freight'),
        ('FIRST_OVERNIGHT', 'FedEx First Overnight'),
        ('GROUND_HOME_DELIVERY', 'FedEx Home Delivery'),
        ('INTERNATIONAL_DISTRIBUTION_FREIGHT', 'FedEx International Priority DirectDistribution Freight'),
        ('INTERNATIONAL_ECONOMY', 'FedEx International Economy'),
        ('INTERNATIONAL_ECONOMY_DISTRIBUTION', 'FedEx International Economy DirectDistribution'),
        ('INTERNATIONAL_ECONOMY_FREIGHT', 'FedEx International Economy Freight'),
        ('INTERNATIONAL_FIRST', 'FedEx International First'),
        ('INTERNATIONAL_PRIORITY_DISTRIBUTION', 'FedEx International Priority DirectDistribution'),
        ('INTERNATIONAL_PRIORITY_FREIGHT', 'FedEx International Priority Freight'),
        ('INTL_GROUND_DISTRIBUTION', 'International Ground Distribution (IGD)'),
        ('PRIORITY_OVERNIGHT', 'FedEx Priority Overnight'),
        ('SAME_DAY', 'FedEx SameDay'),
        ('SAME_DAY_CITY', 'FedEx SameDay City'),
        ('SMART_POST', 'FedEx Ground Economy (Formerly known as FedEx SmartPost)'),
        ('STANDARD_OVERNIGHT', 'FedEx Standard Overnight'),
        ('TRANSBORDER_DISTRIBUTION', 'Transborder distribution'),
    ], default='FEDEX_INTERNATIONAL_PRIORITY', string='FedEx Service Type')
    fedex_rest_duty_payment = fields.Selection([('SENDER', 'Sender'), ('RECIPIENT', 'Recipient')], required=True, default="SENDER")
    fedex_rest_weight_unit = fields.Selection([('LB', 'Pounds'), ('KG', 'Kilograms')], default='LB')
    fedex_rest_label_stock_type = fields.Selection(FEDEX_STOCK_TYPE, string='Label Size', default='PAPER_LETTER')
    fedex_rest_label_file_type = fields.Selection([('PDF', 'PDF'),
                                                   ('EPL2', 'EPL2'),
                                                   ('PNG', 'PNG'),
                                                   ('ZPLII', 'ZPLII')],
                                                  default='PDF', string="Label File Type")
    fedex_rest_extra_data_rate_request = fields.Text('Extra data for rate', help=FEDEX_REST_HELP_EXTRA_DATA)
    fedex_rest_extra_data_ship_request = fields.Text('Extra data for ship', help=FEDEX_REST_HELP_EXTRA_DATA)
    fedex_rest_extra_data_return_request = fields.Text('Extra data for return', help=FEDEX_REST_HELP_EXTRA_DATA)
    fedex_rest_override_shipper_vat = fields.Char('Union tax id (EORI/IOSS)',
        help='Will be provided to Fedex as primary company tax identifier of type BUSINESS_UNION to put on the generated invoice. '
             'Use this when you need to use an IOSS or EORI number in addition to the national tax number. '
             'When not provided the regular tax id on the company will be used with type BUSINESS_NATIONAL.')
    fedex_rest_email_notifications = fields.Boolean('Email Notifications',
        help='When enabled, the customer will receive email notifications from FedEx about this shipment (when an email address is configured on the customer)')
    fedex_rest_documentation_type = fields.Selection(
        [('none', 'No'), ('invoice', 'Print PDF'), ('etd', 'Electronic Trade Documents')], 'Generate invoice', default="none", required=True,
        help='For international shipments (or some intra-country shipments), a commercial invoice might be required for customs clearance. '
             'This commercial invoice can be generated by FedEx based on shipment data and returned as PDF for printing and attaching to the shipment or manual electronic submission to FedEx. '
             'It can also be submitted directly as ETD information to FedEx upon shipment validation.')
    fedex_rest_residential_address = fields.Selection(
        [('never', 'Never'), ('always', 'Always'), ('check', 'Check using FedEx Address API')],
        'Residential delivery', default='never', required=True,
        help='Determines whether to mark the recipient address as residential (to correctly calculate any possible surcharges). '
             'Please note: when retrieving this information using the FedEx Address API, we assume that the address is residential unless it is marked explicitly as a BUSINESS address.')

    # ══════════════════════════════════════════════════════════════════════════
    # DHL SOAP (Legacy) FIELDS
    # ══════════════════════════════════════════════════════════════════════════

    dhl_SiteID = fields.Char(string="DHL SiteID", groups="base.group_system")
    dhl_password = fields.Char(string="DHL Password", groups="base.group_system")
    dhl_account_number = fields.Char(string="DHL Account Number", groups="base.group_system")
    dhl_package_dimension_unit = fields.Selection([('I', 'Inches'), ('C', 'Centimeters')],
        default='C', string='Package Dimension Unit')
    dhl_package_weight_unit = fields.Selection([('L', 'Pounds'), ('K', 'Kilograms')],
        default='K', string="Package Weight Unit")
    dhl_default_package_type_id = fields.Many2one('stock.package.type', string='DHL Legacy Package Type')
    dhl_region_code = fields.Selection([('AP', 'Asia Pacific'), ('AM', 'America'), ('EU', 'Europe')],
        default='AM', string='Region')
    dhl_product_code = fields.Selection([('0', '0 - Logistics Services'),
                                         ('1', '1 - Domestic Express 12:00'),
                                         ('2', '2 - B2C'),
                                         ('3', '3 - B2C'),
                                         ('4', '4 - Jetline'),
                                         ('5', '5 - Sprintline'),
                                         ('6', '6 - Secureline'),
                                         ('7', '7 - Express Easy'),
                                         ('8', '8 - Express Easy'),
                                         ('9', '9 - Europack'),
                                         ('A', 'A - Auto Reversals'),
                                         ('B', 'B - Break Bulk Express'),
                                         ('C', 'C - Medical Express'),
                                         ('D', 'D - Express Worldwide'),
                                         ('E', 'E - Express 9:00'),
                                         ('F', 'F - Freight Worldwide'),
                                         ('G', 'G - Domestic Economy Select'),
                                         ('H', 'H - Economy Select'),
                                         ('I', 'I - Break Bulk Economy'),
                                         ('J', 'J - Jumbo Box'),
                                         ('K', 'K - Express 9:00'),
                                         ('L', 'L - Express 10:30'),
                                         ('M', 'M - Express 10:30'),
                                         ('N', 'N - Domestic Express'),
                                         ('O', 'O - DOM Express 10:30'),
                                         ('P', 'P - Express Worldwide'),
                                         ('Q', 'Q - Medical Express'),
                                         ('R', 'R - GlobalMail Business'),
                                         ('S', 'S - Same Day'),
                                         ('T', 'T - Express 12:00'),
                                         ('U', 'U - Express Worldwide'),
                                         ('V', 'V - Europack'),
                                         ('W', 'W - Economy Select'),
                                         ('X', 'X - Express Envelope'),
                                         ('Y', 'Y - Express 12:00'),
                                         ('Z', 'Z - Destination Charges'),
                                         ],
                                        default='D',
                                        string='DHL Product')
    dhl_dutiable = fields.Boolean(string="Dutiable Material", help="Check this if your package is dutiable.")
    dhl_duty_payment = fields.Selection([('S', 'Sender'), ('R', 'Recipient')], required=True, default="S")
    dhl_label_image_format = fields.Selection([
        ('EPL2', 'EPL2'),
        ('PDF', 'PDF'),
        ('ZPL2', 'ZPL2'),
    ], string="Label Image Format", default='PDF')
    dhl_label_template = fields.Selection([
        ('8X4_A4_PDF', '8X4_A4_PDF'),
        ('8X4_thermal', '8X4_thermal'),
        ('8X4_A4_TC_PDF', '8X4_A4_TC_PDF'),
        ('6X4_thermal', '6X4_thermal'),
        ('6X4_A4_PDF', '6X4_A4_PDF'),
        ('8X4_CI_PDF', '8X4_CI_PDF'),
        ('8X4_CI_thermal', '8X4_CI_thermal'),
        ('8X4_RU_A4_PDF', '8X4_RU_A4_PDF'),
        ('6X4_PDF', '6X4_PDF'),
        ('8X4_PDF', '8X4_PDF')
    ], string="Label Template", default='8X4_A4_PDF')
    dhl_custom_data_request = fields.Text(
        'Custom data for DHL requests,',
        help="The custom data in DHL is organized like the inside of a json file. "
             "There are 3 possible keys: 'rate', 'ship', 'return', to which you can add your custom data. "
             "More info on https://xmlportal.dhl.com/")

    # ══════════════════════════════════════════════════════════════════════════
    # DHL REST FIELDS
    # ══════════════════════════════════════════════════════════════════════════

    dhl_api_key = fields.Char(string="DHL API Key", groups="base.group_system")
    dhl_api_secret = fields.Char(string="DHL API Secret", groups="base.group_system")
    dhl_unit_system = fields.Selection([('imperial', 'Imperial'), ('metric', 'Metric')],
        default='metric', string='Unit System')
    dhl_extra_data_rate_request = fields.Text('Extra data for rate requests', help=DHL_REST_HELP_EXTRA_DATA)
    dhl_extra_data_ship_request = fields.Text('Extra data for ship requests', help=DHL_REST_HELP_EXTRA_DATA)
    dhl_extra_data_return_request = fields.Text('Extra data for return requests', help=DHL_REST_HELP_EXTRA_DATA)

    # ══════════════════════════════════════════════════════════════════════════
    # COMBINED COMPUTE METHODS
    # ══════════════════════════════════════════════════════════════════════════

    def _compute_can_generate_return(self):
        super()._compute_can_generate_return()
        for carrier in self:
            if carrier.delivery_type in ('fusion_canada_post', 'fusion_ups', 'fusion_ups_rest',
                                         'fusion_fedex', 'fusion_fedex_rest',
                                         'fusion_dhl', 'fusion_dhl_rest'):
                carrier.can_generate_return = True

    def _compute_supports_shipping_insurance(self):
        super()._compute_supports_shipping_insurance()
        for carrier in self:
            if carrier.delivery_type in ('fusion_ups', 'fusion_ups_rest',
                                         'fusion_fedex', 'fusion_fedex_rest',
                                         'fusion_dhl', 'fusion_dhl_rest'):
                carrier.supports_shipping_insurance = True

    # ══════════════════════════════════════════════════════════════════════════
    # CANADA POST METHODS
    # ══════════════════════════════════════════════════════════════════════════

    @api.model
    def get_fusion_cp_url(self):
        if self.prod_environment:
            return "https://soa-gw.canadapost.ca/rs/"
        else:
            return "https://ct.soa-gw.canadapost.ca/rs/"

    def validating_address(self, partner_id):
        missing = []
        if not partner_id.street:
            missing.append('Street')
        if not partner_id.city:
            missing.append('City')
        if not partner_id.state_id:
            missing.append('State')
        if not partner_id.country_id:
            missing.append('Country')
        if not partner_id.zip:
            missing.append('ZIP')
        return missing

    def check_required_value_to_ship(self, order):
        order.ensure_one()
        if order.order_line:
            for order_line in order.order_line.filtered(
                    lambda line: not line.is_delivery
                    and not line.display_type
                    and line.product_id.type == 'consu'):
                if order_line.product_id and not order_line.product_id.weight:
                    return _("Product '%s' has no weight defined. Please set the weight before shipping.") % order_line.product_id.name

            # validating customer address
            missing_value = self.validating_address(order.partner_shipping_id)
            if missing_value:
                fields_str = ", ".join(missing_value)
                return (_("Incomplete customer address. Missing field(s): %s") % fields_str)

            # validation shipper address
            missing_value = self.validating_address(order.warehouse_id.partner_id)
            if missing_value:
                fields_str = ", ".join(missing_value)
                return (_("Incomplete warehouse address. Missing field(s): %s") % fields_str)
        return False

    def convert_weight(self, from_uom_unit, to_uom_unit, weight):
        if not from_uom_unit:
            from_uom_unit = self.env[
                "product.template"
            ]._get_weight_uom_id_from_ir_config_parameter()
        return from_uom_unit._compute_quantity(weight, to_uom_unit)

    def check_max_weight(self, order, shipment_weight):
        for order_line in order.order_line:
            if order_line.product_id and order_line.product_id.weight > shipment_weight:
                return (_("Product weight exceeds the maximum allowed weight for this package type."))
        return False

    def fusion_canada_post_rate_shipment(self, order):
        # check the address validation
        check_value = self.check_required_value_to_ship(order)
        # check the product weight is appropriate to maximum weight.
        if check_value:
            return {'success': False, 'price': 0.0, 'error_message': check_value, 'warning_message': False}

        shipper_address = order.warehouse_id.partner_id or order.company_id.partner_id
        recipient_address = order.partner_shipping_id or order.partner_id
        carrier_ctx = self.env.context
        weight = carrier_ctx.get("order_weight", 0.0) or order._get_estimated_weight() or 0.0
        total_weight = self.convert_weight(
            order.company_id and order.company_id.weight_unit_of_measurement_id,
            self.weight_uom_id, weight)
        declared_value = round(order.amount_untaxed, 2)
        declared_currency = order.currency_id.name

        # Get package dimensions from context or SO fields or default package type
        package_info = self.env.context.get('cp_package_info')

        if not package_info and order.fusion_cp_package_length and order.fusion_cp_package_width and order.fusion_cp_package_height:
            package_info = {
                'length': round(self._fusion_cp_convert_dimension_to_cm(
                    order.fusion_cp_package_length), 1),
                'width': round(self._fusion_cp_convert_dimension_to_cm(
                    order.fusion_cp_package_width), 1),
                'height': round(self._fusion_cp_convert_dimension_to_cm(
                    order.fusion_cp_package_height), 1),
            }
        elif self.product_packaging_id:
            package_info = self.get_fusion_cp_parcel(
                self.product_packaging_id)

        price = 0.0
        rate_dict = self.fusion_canada_post_get_shipping_rate(shipper_address, recipient_address, total_weight,
                                                   picking_bulk_weight=False, packages=False,
                                                   declared_value=declared_value, declared_currency=declared_currency,
                                                   company_id=order.company_id,
                                                   package_info=package_info)
        _logger.info("Rate Response Data  : %s" % (rate_dict))
        if rate_dict.get('messages', False):
            return {'success': False, 'price': price, 'error_message': rate_dict['messages']['message']['description'],
                    'warning_message': False}
        if rate_dict.get('price-quotes', False) and rate_dict.get('price-quotes').get('price-quote', False):
            cnt = 0
            quotes = rate_dict['price-quotes']['price-quote']
            if isinstance(quotes, dict):
                quotes = [quotes]
            for quote in quotes:
                if quote['service-code'] == self.service_type:
                    price = quote['price-details']['due']
                    cnt += 1
            if cnt == 0:
                return {'success': False, 'price': 0.0,
                        'error_message': _("Service '%s' is not available for this route. "
                                           "Please select a different service or adjust the destination.") % (self.service_type),
                        'warning_message': False}

        return {'success': True, 'price': float(price), 'error_message': False, 'warning_message': False}

    def fusion_canada_post_get_shipping_rate(self, shipper_address, recipient_address, total_weight,
                                             picking_bulk_weight=False, packages=False,
                                             declared_value=False, declared_currency=False,
                                             company_id=False, package_info=None):
        result = {}
        if self.fusion_cp_type == 'commercial':
            service_root = etree.Element("mailing-scenario")
            service_root.attrib["xmlns"] = "http://www.canadapost.ca/ws/ship/rate-v4"
            etree.SubElement(service_root, "customer-number").text = self.customer_number
            contract = etree.SubElement(service_root, "contract-id")
            contract.text = "%s" % (self.fusion_cp_contract_id or '')
        else:
            service_root = etree.Element("mailing-scenario")
            service_root.attrib["xmlns"] = "http://www.canadapost.ca/ws/ship/rate-v3"
            etree.SubElement(service_root, "customer-number").text = self.customer_number

        parcel = etree.SubElement(service_root, "parcel-characteristics")
        etree.SubElement(parcel, "weight").text = "%s" % round(total_weight, 3)

        # Include dimensions if provided (improves cubing/volumetric accuracy)
        if package_info:
            dims = etree.SubElement(parcel, "dimensions")
            etree.SubElement(dims, "length").text = str(
                package_info.get('length', 1))
            etree.SubElement(dims, "width").text = str(
                package_info.get('width', 1))
            etree.SubElement(dims, "height").text = str(
                package_info.get('height', 1))

        etree.SubElement(service_root, "origin-postal-code").text = "%s" % (shipper_address.zip.replace(" ", "").upper())

        # Determine destination type from recipient country (not service_type)
        destination = etree.SubElement(service_root, "destination")
        country_code = recipient_address.country_id and recipient_address.country_id.code or ''
        if country_code == 'CA':
            domestic = etree.SubElement(destination, "domestic")
            etree.SubElement(domestic, "postal-code").text = "%s" % (recipient_address.zip.replace(" ", "").upper())
        elif country_code == 'US':
            united_states = etree.SubElement(destination, "united-states")
            etree.SubElement(united_states, "zip-code").text = "%s" % (recipient_address.zip.upper())
        else:
            international = etree.SubElement(destination, "international")
            etree.SubElement(international, "country-code").text = "%s" % (country_code)

        url = '%sship/price' % (self.get_fusion_cp_url())

        base_data = etree.tostring(service_root).decode('utf-8')

        if self.fusion_cp_type == 'commercial':
            headers = {"Accept": "application/vnd.cpc.ship.rate-v4+xml", "Content-Type": "application/vnd.cpc.ship.rate-v4+xml"}
        else:
            headers = {"Accept": "application/vnd.cpc.ship.rate-v3+xml", "Content-Type": "application/vnd.cpc.ship.rate-v3+xml"}
        _logger.info("Rate Request Data  : %s" % (base_data))
        try:
            response_body = http_request(method='POST', url=url, data=base_data, headers=headers, auth=(self.username, self.password))
            api = Response(response_body)
            result = api.dict()
            _logger.info("Rate Response Data  : %s" % (result))
        except Exception as e:
            result['error_message'] = e.message
            return result
        return result

    def fusion_canada_post_rate_shipment_all(self, order):
        """Return ALL available Canada Post service rates for the order."""
        check_value = self.check_required_value_to_ship(order)
        if check_value:
            return {'success': False, 'error_message': check_value}

        shipper_address = order.warehouse_id.partner_id or order.company_id.partner_id
        recipient_address = order.partner_shipping_id or order.partner_id
        carrier_ctx = self.env.context
        weight = carrier_ctx.get("order_weight", 0.0) or order._get_estimated_weight() or 0.0
        total_weight = self.convert_weight(
            order.company_id and order.company_id.weight_unit_of_measurement_id,
            self.weight_uom_id, weight)
        declared_value = round(order.amount_untaxed, 2)
        declared_currency = order.currency_id.name

        # Get package dimensions from context (set by wizard)
        package_info = self.env.context.get('cp_package_info')

        rate_dict = self.fusion_canada_post_get_shipping_rate(
            shipper_address, recipient_address, total_weight,
            picking_bulk_weight=False, packages=False,
            declared_value=declared_value, declared_currency=declared_currency,
            company_id=order.company_id,
            package_info=package_info)

        if rate_dict.get('messages', False):
            return {
                'success': False,
                'error_message': rate_dict['messages']['message']['description'],
            }

        rates = []
        if rate_dict.get('price-quotes') and rate_dict['price-quotes'].get('price-quote'):
            quotes = rate_dict['price-quotes']['price-quote']
            if isinstance(quotes, dict):
                quotes = [quotes]
            for quote in quotes:
                expected_delivery = ''
                service_standard = quote.get('service-standard', {})
                if isinstance(service_standard, dict):
                    expected_delivery = service_standard.get(
                        'expected-delivery-date', '')
                rates.append({
                    'service_code': quote.get('service-code', ''),
                    'service_name': quote.get('service-name', ''),
                    'price': float(quote.get('price-details', {}).get('due', 0)),
                    'expected_delivery': expected_delivery,
                })

        if not rates:
            return {
                'success': False,
                'error_message': _("No shipping prices available for this destination."),
            }

        return rates

    def get_group_id(self):
        size = 15
        chars = string.ascii_uppercase
        return ''.join(random.choice(chars) for _ in range(size))

    def fusion_canada_post_send_shipping(self, pickings):
        response = []
        for picking in pickings:
            # Determine service code (order-level selection or carrier default)
            service_code = self.service_type
            if picking.sale_id and picking.sale_id.fusion_cp_service_code:
                service_code = picking.sale_id.fusion_cp_service_code

            sale_order = picking.sale_id
            so_packages = (
                sale_order.fusion_cp_package_ids.sorted('sequence')
                if sale_order else False)

            from_unit = (picking.company_id
                         and picking.company_id
                         .weight_unit_of_measurement_id or "")

            all_tracking = []
            all_results = []
            total_price = 0.0

            if so_packages:
                for so_pkg in so_packages:
                    pkg_weight = round(self.convert_weight(
                        from_unit, self.weight_uom_id,
                        so_pkg.weight), 2)

                    package_info = {
                        'length': round(
                            self._fusion_cp_convert_dimension_to_cm(
                                so_pkg.package_length), 1),
                        'width': round(
                            self._fusion_cp_convert_dimension_to_cm(
                                so_pkg.package_width), 1),
                        'height': round(
                            self._fusion_cp_convert_dimension_to_cm(
                                so_pkg.package_height), 1),
                    }

                    svc = so_pkg.service_code or service_code

                    self._fusion_cp_validate_package(
                        pkg_weight, package_info)

                    result = self._fusion_cp_create_single_shipment(
                        picking, pkg_weight, package_info, svc)

                    self._fusion_cp_record_shipment(
                        result, picking, sale_order, svc, pkg_weight,
                        pkg_name="Package %d" % (
                            so_packages.ids.index(so_pkg.id) + 1))

                    all_tracking.append(
                        result.get('tracking_number', ''))
                    total_price += result.get('exact_price', 0.0)
                    all_results.append(result)

            else:
                # Fall back to physical packages from the picking
                packages = self._get_packages_from_picking(
                    picking, self.product_packaging_id)

                for idx, pkg in enumerate(packages):
                    pkg_weight = round(self.convert_weight(
                        from_unit, self.weight_uom_id,
                        pkg.weight), 2)

                    # Dimensions: legacy SO fields -> package type -> default
                    package_info = None

                    if sale_order:
                        if (sale_order.fusion_cp_package_length
                                and sale_order.fusion_cp_package_width
                                and sale_order.fusion_cp_package_height):
                            package_info = {
                                'length': round(
                                    self._fusion_cp_convert_dimension_to_cm(
                                        sale_order.fusion_cp_package_length
                                    ), 1),
                                'width': round(
                                    self._fusion_cp_convert_dimension_to_cm(
                                        sale_order.fusion_cp_package_width
                                    ), 1),
                                'height': round(
                                    self._fusion_cp_convert_dimension_to_cm(
                                        sale_order.fusion_cp_package_height
                                    ), 1),
                            }

                    if not package_info:
                        pkg_type = (
                            pkg.package_type
                            if hasattr(pkg, 'package_type')
                            and pkg.package_type
                            else self.product_packaging_id)
                        if pkg_type:
                            package_info = self.get_fusion_cp_parcel(
                                pkg_type)
                        else:
                            raise ValidationError(_(
                                "No package dimensions available for "
                                "'%s'. Please set dimensions when "
                                "adding shipping to the sale order.")
                                % (pkg.name or 'package'))

                    self._fusion_cp_validate_package(
                        pkg_weight, package_info)

                    result = self._fusion_cp_create_single_shipment(
                        picking, pkg_weight, package_info,
                        service_code)

                    self._fusion_cp_record_shipment(
                        result, picking, sale_order, service_code,
                        pkg_weight,
                        pkg_name=(pkg.name
                                  if hasattr(pkg, 'name') else ''))

                    all_tracking.append(
                        result.get('tracking_number', ''))
                    total_price += result.get('exact_price', 0.0)
                    all_results.append(result)

            # Post combined chatter message with ALL labels
            self._fusion_cp_post_shipping_documents(
                picking, all_results)

            shipping_data = {
                'exact_price': total_price,
                'tracking_number': ','.join(
                    filter(None, all_tracking)),
            }
            response.append(shipping_data)
        return response

    def _fusion_cp_create_single_shipment(
            self, picking, pkg_weight, package_info, service_code):
        """Create one CP shipment for a single package."""
        destination_address = picking.partner_id
        sender_address = (picking.picking_type_id
                          and picking.picking_type_id.warehouse_id
                          and picking.picking_type_id.warehouse_id.partner_id)

        # --- Build XML ---
        if self.fusion_cp_type == 'commercial':
            root_node = etree.Element("shipment")
            root_node.attrib["xmlns"] = (
                "http://www.canadapost.ca/ws/shipment-v8")
            etree.SubElement(
                root_node, "transmit-shipment").text = "true"
            etree.SubElement(
                root_node, "provide-receipt-info").text = "true"
        else:
            root_node = etree.Element("non-contract-shipment")
            root_node.attrib["xmlns"] = (
                "http://www.canadapost.ca/ws/ncshipment-v4")

        etree.SubElement(
            root_node, "requested-shipping-point"
        ).text = "%s" % (sender_address.zip.replace(" ", "").upper() or "")

        delivery_spec_node = etree.SubElement(root_node, "delivery-spec")
        etree.SubElement(
            delivery_spec_node, "service-code").text = "%s" % service_code

        # Sender
        sender_node = etree.SubElement(delivery_spec_node, "sender")
        etree.SubElement(sender_node, "company").text = sender_address.name
        etree.SubElement(
            sender_node, "contact-phone"
        ).text = "%s" % (sender_address.phone or "")
        address_details = etree.SubElement(sender_node, "address-details")
        etree.SubElement(
            address_details, "address-line-1"
        ).text = sender_address.street or ""
        etree.SubElement(
            address_details, "city").text = sender_address.city or ""
        etree.SubElement(
            address_details, "prov-state"
        ).text = "%s" % (sender_address.state_id
                         and sender_address.state_id.code or "")
        if self.fusion_cp_type == 'commercial':
            etree.SubElement(
                address_details, "country-code"
            ).text = "%s" % (sender_address.country_id
                             and sender_address.country_id.code or "")
        etree.SubElement(
            address_details, "postal-zip-code"
        ).text = "%s" % (sender_address.zip.replace(" ", "").upper() or "")

        # Destination
        destination_node = etree.SubElement(
            delivery_spec_node, "destination")
        etree.SubElement(
            destination_node, "name").text = destination_address.name
        etree.SubElement(
            destination_node, "company").text = destination_address.name
        etree.SubElement(
            destination_node, "client-voice-number"
        ).text = destination_address.phone
        dest_addr = etree.SubElement(
            destination_node, "address-details")
        etree.SubElement(
            dest_addr, "address-line-1"
        ).text = destination_address.street or ""
        etree.SubElement(
            dest_addr, "city").text = destination_address.city or ""
        etree.SubElement(
            dest_addr, "prov-state"
        ).text = "%s" % (destination_address.state_id
                         and destination_address.state_id.code or "")
        etree.SubElement(
            dest_addr, "country-code"
        ).text = "%s" % (destination_address.country_id
                         and destination_address.country_id.code or "")
        etree.SubElement(
            dest_addr, "postal-zip-code"
        ).text = "%s" % (destination_address.zip.replace(
            " ", "").upper() or "")

        # Options
        if (self.option_code
                and self.service_type not in [
                    'USA.XP', 'USA.EP', 'INT.IP.SURF',
                    'INT.PW.PARCEL', 'INT.XP']):
            options = etree.SubElement(delivery_spec_node, "options")
            option = etree.SubElement(options, "option")
            etree.SubElement(
                option, "option-code").text = str(self.option_code or "")
            if self.option_code in ['PA18', 'PA19']:
                option_pa = etree.SubElement(options, "option")
                etree.SubElement(option_pa, "option-code").text = 'SO'
            if self.option_code in ('COD', 'COV'):
                etree.SubElement(
                    option, "option-amount"
                ).text = str(picking.sale_id.amount_total or "")
                etree.SubElement(
                    option, "option-qualifier-1").text = "true"
        else:
            options = etree.SubElement(delivery_spec_node, "options")
            option_usa = etree.SubElement(options, "option")
            etree.SubElement(option_usa, "option-code").text = 'RASE'

        # Parcel characteristics
        parcel_chars = etree.SubElement(
            delivery_spec_node, "parcel-characteristics")
        etree.SubElement(
            parcel_chars, "weight").text = "%s" % pkg_weight
        dimensions = etree.SubElement(parcel_chars, "dimensions")
        etree.SubElement(
            dimensions, "length"
        ).text = "%s" % package_info.get('length', 1)
        etree.SubElement(
            dimensions, "width"
        ).text = "%s" % package_info.get('width', 1)
        etree.SubElement(
            dimensions, "height"
        ).text = "%s" % package_info.get('height', 1)

        # Preferences
        preferences = etree.SubElement(delivery_spec_node, "preferences")
        etree.SubElement(
            preferences, "show-packing-instructions").text = "true"
        if self.fusion_cp_type != 'commercial':
            etree.SubElement(
                preferences, "output-format"
            ).text = self.fusion_cp_output_format or "4x6"
        else:
            print_prefs = etree.SubElement(
                delivery_spec_node, "print-preferences")
            etree.SubElement(
                print_prefs, "output-format"
            ).text = self.fusion_cp_output_format or "4x6"

        # Customs
        customs = etree.SubElement(delivery_spec_node, "customs")
        etree.SubElement(
            customs, "currency"
        ).text = str(picking.sale_id.currency_id.name)
        rate = 1.0
        if picking.sale_id.currency_id.rate:
            rate = round(picking.sale_id.currency_id.rate, 2)
        etree.SubElement(
            customs, "conversion-from-cad").text = str(rate or '')
        etree.SubElement(
            customs, "reason-for-export"
        ).text = "%s" % self.reason_for_export

        sku_list = etree.SubElement(customs, "sku-list")
        for move_line in picking.move_ids:
            item = etree.SubElement(sku_list, "item")
            etree.SubElement(
                item, "customs-description"
            ).text = str(move_line.product_id.name)
            etree.SubElement(
                item, "unit-weight").text = str(move_line.weight)
            etree.SubElement(
                item, "customs-value-per-unit"
            ).text = str(move_line.product_id.lst_price)
            etree.SubElement(
                item, "customs-number-of-units"
            ).text = str(int(move_line.product_uom_qty))

        # Settlement info (commercial only)
        if self.fusion_cp_type == 'commercial':
            settlement_info = etree.SubElement(
                delivery_spec_node, "settlement-info")
            etree.SubElement(
                settlement_info, "contract-id"
            ).text = self.fusion_cp_contract_id
            etree.SubElement(
                settlement_info, "intended-method-of-payment"
            ).text = self.fusion_cp_payment_method

        # --- Make API call ---
        api_url = self.get_fusion_cp_url()
        if self.fusion_cp_type == 'commercial':
            url = "%s%s/%s/shipment" % (
                api_url, self.customer_number, self.customer_number)
        else:
            url = "%s%s/ncshipment" % (api_url, self.customer_number)

        base_data = etree.tostring(root_node).decode('utf-8')

        if self.fusion_cp_type == 'commercial':
            headers = {
                "Accept": "application/vnd.cpc.shipment-v8+xml",
                "Content-Type": "application/vnd.cpc.shipment-v8+xml",
                "Accept-language": "en-CA",
            }
        else:
            headers = {
                "Accept": "application/vnd.cpc.ncshipment-v4+xml",
                "Content-Type": "application/vnd.cpc.ncshipment-v4+xml",
                "Accept-language": "en-CA",
            }

        _logger.info("Create Shipment Request Data: %s", base_data)
        response_body = http_request(
            method='POST', url=url, data=base_data,
            headers=headers, auth=(self.username, self.password))

        if response_body.status_code == 200:
            api = Response(response_body)
            result = api.dict()
            _logger.info("Create Shipment Response Data: %s", result)
        else:
            error_code = "%s" % response_body.status_code
            error_message = response_body.reason
            message = error_code + " " + error_message
            api = Response(response_body)
            result = api.dict()
            if result['messages']['message']['description']:
                raise ValidationError(
                    _("Canada Post: %s")
                    % result['messages']['message']['description'])
            else:
                raise ValidationError(
                    _("Canada Post shipment request failed (%s).\n%s")
                    % (message, response_body.text[:500]))

        # --- Extract shipment ID ---
        if self.fusion_cp_type == 'commercial':
            if (not result['shipment-info']['shipment-id']
                    or not result['shipment-info']['links']['link']):
                raise RedirectWarning(
                    "ShipmentRequest Fail \n More Information \n %s"
                    % result)
            shipment_id = str(result['shipment-info']['shipment-id'])
        else:
            if (not result['non-contract-shipment-info']['shipment-id']
                    or not result['non-contract-shipment-info']
                    ['links']['link']):
                raise RedirectWarning(
                    "ShipmentRequest Fail \n More Information \n %s"
                    % result)
            shipment_id = str(
                result['non-contract-shipment-info']['shipment-id'])

        # --- Extract artifact URLs ---
        label_url = ""
        receipt_link_url = ""
        commercial_invoice_url = ""
        commercial_invoice = False
        links = (result['shipment-info']['links']['link']
                 if self.fusion_cp_type == 'commercial'
                 else result['non-contract-shipment-info']
                 ['links']['link'])
        if isinstance(links, dict):
            links = [links]
        for link in links:
            if link['_rel'] == 'label':
                label_url = link['_href']
            elif link['_rel'] == 'receipt':
                receipt_link_url = link['_href']
            elif link['_rel'] == 'commercialInvoice':
                commercial_invoice_url = link['_href']
                commercial_invoice = True

        # --- Download artifacts ---
        pdf_headers = {'Accept': 'application/pdf'}
        label_attachment = False
        receipt_attachment = False
        full_label_attachment = False
        ci_attachment = False
        label_content = b''
        receipt_pdf_content = b''

        try:
            if label_url:
                label_resp = http_request(
                    method='GET', url=label_url, headers=pdf_headers,
                    auth=(self.username, self.password))
                _logger.info(
                    "Label Response Status: %s", label_resp.status_code)
                if label_resp.status_code == 200:
                    label_content = label_resp.content
                    label_attachment = self.env['ir.attachment'].create({
                        'name': 'Label-%s.pdf' % shipment_id,
                        'type': 'binary',
                        'datas': base64.b64encode(label_content),
                        'res_model': 'stock.picking',
                        'res_id': picking.id,
                        'mimetype': 'application/pdf',
                    })

            if not receipt_link_url:
                if self.fusion_cp_type == 'commercial':
                    receipt_link_url = (
                        "%s%s/%s/shipment/%s/receipt"
                        % (self.get_fusion_cp_url(),
                           self.customer_number,
                           self.customer_number, shipment_id))
                else:
                    receipt_link_url = (
                        "%s%s/ncshipment/%s/receipt"
                        % (self.get_fusion_cp_url(),
                           self.customer_number, shipment_id))

            if receipt_link_url:
                receipt_pdf_resp = http_request(
                    method='GET', url=receipt_link_url,
                    headers=pdf_headers,
                    auth=(self.username, self.password))
                if receipt_pdf_resp.status_code == 200:
                    receipt_pdf_content = receipt_pdf_resp.content
                    receipt_attachment = (
                        self.env['ir.attachment'].create({
                            'name': 'Receipt-%s.pdf' % shipment_id,
                            'type': 'binary',
                            'datas': base64.b64encode(
                                receipt_pdf_content),
                            'res_model': 'stock.picking',
                            'res_id': picking.id,
                            'mimetype': 'application/pdf',
                        }))

            if label_content and receipt_pdf_content:
                try:
                    from odoo.tools import pdf as pdf_tools
                    full_content = pdf_tools.merge_pdf(
                        [label_content, receipt_pdf_content])
                except Exception:
                    full_content = label_content
                full_label_attachment = (
                    self.env['ir.attachment'].create({
                        'name': 'FullLabel-%s.pdf' % shipment_id,
                        'type': 'binary',
                        'datas': base64.b64encode(full_content),
                        'res_model': 'stock.picking',
                        'res_id': picking.id,
                        'mimetype': 'application/pdf',
                    }))

            if commercial_invoice and commercial_invoice_url:
                ci_resp = http_request(
                    method='GET', url=commercial_invoice_url,
                    headers=pdf_headers,
                    auth=(self.username, self.password))
                if ci_resp.status_code == 200:
                    ci_attachment = self.env['ir.attachment'].create({
                        'name': 'CommercialInvoice-%s.pdf' % shipment_id,
                        'type': 'binary',
                        'datas': base64.b64encode(ci_resp.content),
                        'res_model': 'stock.picking',
                        'res_id': picking.id,
                        'mimetype': 'application/pdf',
                    })

        except Exception as e:
            _logger.error(
                "Error downloading Canada Post artifacts: %s", e)
            picking.message_post(
                body=_('Error downloading shipping documents '
                       'for shipment %s: %s') % (shipment_id, str(e)))

        # --- Fetch receipt XML for pricing info ---
        if self.fusion_cp_type == 'commercial':
            receipt_xml_headers = {
                "Accept": "application/vnd.cpc.shipment-v8+xml",
                "Accept-language": "en-CA",
            }
            url_receipt = "%s%s/%s/shipment/%s/receipt" % (
                self.get_fusion_cp_url(), self.customer_number,
                self.customer_number, shipment_id)
        else:
            receipt_xml_headers = {
                "Accept": "application/vnd.cpc.ncshipment-v4+xml",
                "Accept-language": "en-CA",
            }
            url_receipt = "%s%s/ncshipment/%s/receipt" % (
                self.get_fusion_cp_url(), self.customer_number,
                shipment_id)

        result_receipt = {}
        try:
            receipt_response = http_request(
                method='GET', url=url_receipt,
                headers=receipt_xml_headers,
                auth=(self.username, self.password))
            if receipt_response.status_code == 200:
                api_receipt = Response(receipt_response)
                result_receipt = api_receipt.dict()
                _logger.info("Receipt XML Data: %s", result_receipt)
            else:
                _logger.warning(
                    "Receipt fetch: %s %s",
                    receipt_response.status_code,
                    receipt_response.reason)
        except Exception as e:
            _logger.warning("Error fetching receipt XML: %s", e)

        # Extract pricing and tracking
        extra_price = 0.0
        tracking_pin = False
        if self.fusion_cp_type == 'commercial':
            if result_receipt:
                try:
                    extra_price = float(
                        result_receipt.get('shipment-receipt', {})
                        .get('cc-receipt-details', {})
                        .get('charge-amount', 0.0))
                except (TypeError, ValueError):
                    extra_price = 0.0
            tracking_pin = result['shipment-info'].get(
                'tracking-pin', False)
        else:
            if result_receipt:
                try:
                    extra_price = float(
                        result_receipt
                        .get('non-contract-shipment-receipt', {})
                        .get('cc-receipt-details', {})
                        .get('charge-amount', 0.0))
                except (TypeError, ValueError):
                    extra_price = 0.0
            tracking_pin = (
                result['non-contract-shipment-info']
                .get('tracking-pin', False))

        if not tracking_pin:
            _logger.info(
                "This service does not provide tracking. Service: %s",
                service_code)

        return {
            'tracking_number': tracking_pin or '',
            'shipment_id': shipment_id,
            'exact_price': float(extra_price) or 0.0,
            'label_attachment': label_attachment,
            'full_label_attachment': full_label_attachment,
            'receipt_attachment': receipt_attachment,
            'ci_attachment': ci_attachment,
            'label_content': label_content,
            'receipt_content': receipt_pdf_content,
        }

    def _fusion_cp_post_shipping_documents(self, picking, all_results):
        """Post a single chatter message with combined shipping documents."""
        from odoo.tools import pdf as pdf_tools

        shipment_ids = [
            r.get('shipment_id', '') for r in all_results
            if r.get('shipment_id')]

        # Merge all label PDFs
        label_pages = [
            r['label_content'] for r in all_results
            if r.get('label_content')]
        receipt_pages = [
            r['receipt_content'] for r in all_results
            if r.get('receipt_content')]

        attach_ids = []

        if label_pages:
            try:
                if len(label_pages) > 1:
                    merged = pdf_tools.merge_pdf(label_pages)
                else:
                    merged = label_pages[0]
                att = self.env['ir.attachment'].create({
                    'name': 'Labels-All.pdf',
                    'type': 'binary',
                    'datas': base64.b64encode(merged),
                    'res_model': 'stock.picking',
                    'res_id': picking.id,
                    'mimetype': 'application/pdf',
                })
                attach_ids.append(att.id)
            except Exception as e:
                _logger.warning(
                    "Could not merge label PDFs: %s", e)
                for r in all_results:
                    if r.get('label_attachment'):
                        attach_ids.append(
                            r['label_attachment'].id)

        if receipt_pages:
            try:
                if len(receipt_pages) > 1:
                    merged_rcpt = pdf_tools.merge_pdf(receipt_pages)
                else:
                    merged_rcpt = receipt_pages[0]
                att_r = self.env['ir.attachment'].create({
                    'name': 'Receipts-All.pdf',
                    'type': 'binary',
                    'datas': base64.b64encode(merged_rcpt),
                    'res_model': 'stock.picking',
                    'res_id': picking.id,
                    'mimetype': 'application/pdf',
                })
                attach_ids.append(att_r.id)
            except Exception as e:
                _logger.warning(
                    "Could not merge receipt PDFs: %s", e)
                for r in all_results:
                    if r.get('receipt_attachment'):
                        attach_ids.append(
                            r['receipt_attachment'].id)

        # Commercial invoices (one per shipment, not merged)
        for r in all_results:
            if r.get('ci_attachment'):
                attach_ids.append(r['ci_attachment'].id)

        # Post to chatter
        if len(shipment_ids) == 1:
            body = _('Canada Post shipment %s created.') % (
                shipment_ids[0])
        elif shipment_ids:
            body = _('Canada Post -- %d shipments created: %s') % (
                len(shipment_ids), ', '.join(shipment_ids))
        else:
            body = _('Canada Post shipment created.')

        if attach_ids:
            picking.message_post(
                body=body, attachment_ids=attach_ids)
        else:
            picking.message_post(body=body)

    def _fusion_cp_record_shipment(
            self, result, picking, sale_order, service_code,
            pkg_weight, pkg_name=''):
        """Create a fusion.cp.shipment record from shipment result dict."""
        shipment_vals = {
            'tracking_number': result.get('tracking_number', ''),
            'shipment_id': result.get('shipment_id', ''),
            'carrier_id': self.id,
            'sale_order_id': (sale_order.id if sale_order else False),
            'picking_id': picking.id,
            'service_type': service_code,
            'shipment_date': fields.Datetime.now(),
            'shipping_cost': result.get('exact_price', 0.0),
            'weight': pkg_weight,
            'status': 'confirmed',
            'company_id': picking.company_id.id,
            'package_name': pkg_name,
        }

        # Attach label / receipt / CI documents
        if result.get('label_attachment'):
            shipment_vals['label_attachment_id'] = (
                result['label_attachment'].id)
        if result.get('full_label_attachment'):
            shipment_vals['full_label_attachment_id'] = (
                result['full_label_attachment'].id)
        if result.get('receipt_attachment'):
            shipment_vals['receipt_attachment_id'] = (
                result['receipt_attachment'].id)
        if result.get('ci_attachment'):
            shipment_vals['commercial_invoice_attachment_id'] = (
                result['ci_attachment'].id)

        self.env['fusion.cp.shipment'].create(shipment_vals)

    def fusion_canada_post_get_tracking_link(self, picking):
        link = picking.carrier_id.tracking_link or 'https://www.canadapost.ca/trackweb/en#/resultList?searchFor='
        res = '%s %s' % (link, picking.carrier_tracking_ref)
        return res

    def fusion_canada_post_cancel_shipment(self, picking):
        """Cancel all CP shipments linked to this picking via void API."""
        shipments = self.env['fusion.cp.shipment'].search([
            ('picking_id', '=', picking.id),
            ('status', '!=', 'cancelled'),
        ])
        for shipment in shipments:
            try:
                shipment.action_void_shipment()
            except ValidationError as e:
                _logger.warning(
                    "Failed to void CP shipment %s: %s",
                    shipment.name, e)
                raise

    def _fusion_cp_validate_package(self, weight_kg, package_info):
        """Validate package dimensions and weight against Canada Post limits."""
        errors = []
        length_cm = float(package_info.get('length', 0))
        width_cm = float(package_info.get('width', 0))
        height_cm = float(package_info.get('height', 0))

        # Sort dimensions to find longest and second longest
        dims = sorted([length_cm, width_cm, height_cm], reverse=True)

        # Determine user's preferred display unit
        dim_unit = self.fusion_cp_dimension_unit or 'cm'
        if dim_unit == 'in':
            unit_label = 'in'
            max_longest = 39.4  # 100 cm
            max_second = 29.9   # 76 cm
            factor = 1 / 2.54   # cm -> inches
        else:
            unit_label = 'cm'
            max_longest = 100
            max_second = 76
            factor = 1

        if weight_kg > 30:
            errors.append(
                _("Weight %.2f kg exceeds Canada Post maximum of 30 kg.")
                % weight_kg)
        if dims[0] > 100:
            errors.append(
                _("Longest dimension %.1f %s exceeds Canada Post "
                  "maximum of %.1f %s.") % (
                    dims[0] * factor, unit_label,
                    max_longest, unit_label))
        if dims[1] > 76:
            errors.append(
                _("Second longest dimension %.1f %s exceeds Canada Post "
                  "maximum of %.1f %s.") % (
                    dims[1] * factor, unit_label,
                    max_second, unit_label))

        if errors:
            raise ValidationError('\n'.join(errors))

        # Volumetric weight warning (CP cubing)
        if length_cm and width_cm and height_cm:
            vol_weight = (length_cm * width_cm * height_cm) / 5000
            if vol_weight > weight_kg:
                _logger.warning(
                    "CP cubing: volumetric weight %.2f kg > actual %.2f kg "
                    "for package (%.1f x %.1f x %.1f cm). Canada Post will "
                    "bill at the higher volumetric weight.",
                    vol_weight, weight_kg, length_cm, width_cm, height_cm)

    def get_fusion_cp_parcel(self, package):
        """Get package dimensions in cm for Canada Post API."""
        packaging_length = self.sudo()._fusion_cp_convert_dimension_to_cm(
            package.packaging_length)
        width = self.sudo()._fusion_cp_convert_dimension_to_cm(
            package.width)
        height = self.sudo()._fusion_cp_convert_dimension_to_cm(
            package.height)
        return {
            "length": round(packaging_length, 1),
            "width": round(width, 1),
            "height": round(height, 1),
        }

    def _fusion_cp_convert_dimension_to_cm(self, dimension):
        """Convert a dimension value to centimeters using the carrier's configured dimension unit."""
        if not dimension:
            return 0.0
        dim_unit = self.fusion_cp_dimension_unit or 'cm'
        if dim_unit == 'cm':
            return dimension
        # inches -> cm
        target_uom = self.env.ref("uom.product_uom_cm")
        from_uom = self.env.ref("uom.product_uom_inch")
        return from_uom._compute_quantity(dimension, target_uom)

    # ══════════════════════════════════════════════════════════════════════════
    # UPS SOAP (Legacy) METHODS
    # ══════════════════════════════════════════════════════════════════════════

    @api.onchange('ups_default_service_type')
    def on_change_service_type(self):
        self.allow_cash_on_delivery = False
        self.ups_saturday_delivery = False

    def fusion_ups_rate_shipment(self, order):
        superself = self.sudo()
        srm = UPSRequest(self.log_xml, superself.ups_username, superself.ups_passwd, superself.ups_shipper_number, superself.ups_access_number, self.prod_environment)
        ResCurrency = self.env['res.currency']

        packages = self._get_packages_from_order(order, self.ups_default_package_type_id)

        shipment_info = {
            'total_qty': sum(line.product_uom_qty for line in order.order_line.filtered(lambda line: not line.is_delivery and not line.display_type))
        }

        if self.allow_cash_on_delivery:
            cod_info = {
                'currency': order.partner_id.country_id.currency_id.name,
                'monetary_value': order.amount_total,
                'funds_code': self.ups_cod_funds_code,
            }
        else:
            cod_info = None

        check_value = srm.check_required_value(order.company_id.partner_id, order.warehouse_id.partner_id, order.partner_shipping_id, order=order)
        if check_value:
            return {'success': False,
                    'price': 0.0,
                    'error_message': check_value,
                    'warning_message': False}

        ups_service_type = self.ups_default_service_type
        result = srm.get_shipping_price(
            carrier=self,
            shipment_info=shipment_info, packages=packages, shipper=order.company_id.partner_id, ship_from=order.warehouse_id.partner_id,
            ship_to=order.partner_shipping_id, service_type=ups_service_type,
            saturday_delivery=self.ups_saturday_delivery, cod_info=cod_info)

        if result.get('error_message'):
            return {'success': False,
                    'price': 0.0,
                    'error_message': _('Error:\n%s', result['error_message']),
                    'warning_message': False}

        if order.currency_id.name == result['currency_code']:
            price = float(result['price'])
        else:
            quote_currency = ResCurrency.search([('name', '=', result['currency_code'])], limit=1)
            price = quote_currency._convert(
                float(result['price']), order.currency_id, order.company_id, order.date_order or fields.Date.today())

        if self.ups_bill_my_account and order.partner_ups_carrier_account:
            # Don't show delivery amount, if ups bill my account option is true
            price = 0.0

        return {'success': True,
                'price': price,
                'error_message': False,
                'warning_message': False}

    def fusion_ups_send_shipping(self, pickings):
        res = []
        superself = self.sudo()
        srm = UPSRequest(self.log_xml, superself.ups_username, superself.ups_passwd, superself.ups_shipper_number, superself.ups_access_number, self.prod_environment)
        ResCurrency = self.env['res.currency']
        for picking in pickings:
            packages = self._get_packages_from_picking(picking, self.ups_default_package_type_id)

            terms_of_shipment = picking.company_id.incoterm_id
            if picking.sale_id and picking.sale_id.incoterm:
                terms_of_shipment = picking.sale_id.incoterm

            shipment_info = {
                'require_invoice': picking._should_generate_commercial_invoice(),
                'invoice_date': fields.Date.today().strftime('%Y%m%d'),
                'description': picking.origin or picking.name,
                'total_qty': sum(sml.quantity for sml in picking.move_line_ids),
                'ilt_monetary_value': '%d' % sum(sml.sale_price for sml in picking.move_line_ids),
                'itl_currency_code': self.env.company.currency_id.name,
                'phone': picking.partner_id.phone or picking.sale_id.partner_id.phone,
                'terms_of_shipment': terms_of_shipment.code if terms_of_shipment else None,
                'purchase_order_number': picking.sale_id.name if picking.sale_id else None,
            }
            if picking.sale_id and picking.sale_id.carrier_id != picking.carrier_id:
                ups_service_type = picking.carrier_id.ups_default_service_type or self.ups_default_service_type
            else:
                ups_service_type = self.ups_default_service_type
            ups_carrier_account = False
            if self.ups_bill_my_account:
                ups_carrier_account = picking.partner_id.with_company(picking.company_id).property_ups_carrier_account

            if picking.carrier_id.allow_cash_on_delivery:
                cod_info = {
                    'currency': picking.partner_id.country_id.currency_id.name,
                    'monetary_value': picking.sale_id.amount_total,
                    'funds_code': self.ups_cod_funds_code,
                }
            else:
                cod_info = None

            check_value = srm.check_required_value(picking.company_id.partner_id, picking.picking_type_id.warehouse_id.partner_id, picking.partner_id, picking=picking)
            if check_value:
                raise UserError(check_value)

            srm.send_shipping(
                carrier=picking.carrier_id, shipment_info=shipment_info, packages=packages, shipper=picking.company_id.partner_id, ship_from=picking.picking_type_id.warehouse_id.partner_id,
                ship_to=picking.partner_id, service_type=ups_service_type, duty_payment=picking.carrier_id.ups_duty_payment,
                label_file_type=self.ups_label_file_type, ups_carrier_account=ups_carrier_account, saturday_delivery=picking.carrier_id.ups_saturday_delivery,
                cod_info=cod_info)
            result = srm.process_shipment()
            if result.get('error_message'):
                raise UserError(result['error_message'].__str__())

            order = picking.sale_id
            company = order.company_id or picking.company_id or self.env.company
            currency_order = picking.sale_id.currency_id
            if not currency_order:
                currency_order = picking.company_id.currency_id

            if currency_order.name == result['currency_code']:
                price = float(result['price'])
            else:
                quote_currency = ResCurrency.search([('name', '=', result['currency_code'])], limit=1)
                price = quote_currency._convert(
                    float(result['price']), currency_order, company, order.date_order or fields.Date.today())

            package_labels = []
            for track_number, label_binary_data in result.get('label_binary_data').items():
                package_labels = package_labels + [(track_number, label_binary_data)]

            carrier_tracking_ref = "+".join([pl[0] for pl in package_labels])
            logmessage = _("Shipment created into UPS") + Markup("<br/><b>") + \
                         _("Tracking Numbers:") + Markup("</b> ") + carrier_tracking_ref + \
                         Markup("<br/><b>") + _("Packages:") + Markup("</b> ") + \
                         ','.join([p.name for p in packages if p.name])
            if self.ups_label_file_type != 'GIF':
                attachments = [('%s-%s.%s' % (self._get_delivery_label_prefix(), pl[0], self.ups_label_file_type), pl[1]) for pl in package_labels]
            if self.ups_label_file_type == 'GIF':
                attachments = [('%s.pdf' % (self._get_delivery_label_prefix()), pdf.merge_pdf([pl[1] for pl in package_labels]))]
            if 'invoice_binary_data' in result:
                attachments.append(('%s-CommercialInvoice.pdf' % (self._get_delivery_doc_prefix()), result['invoice_binary_data']))
            if picking.sale_id:
                for pick in picking.sale_id.picking_ids:
                    pick.message_post(body=logmessage, attachments=attachments)
            else:
                picking.message_post(body=logmessage, attachments=attachments)
            shipping_data = {
                'exact_price': price,
                'tracking_number': carrier_tracking_ref}
            res = res + [shipping_data]
            if self.return_label_on_delivery:
                self.fusion_ups_get_return_label(picking)
        return res

    def fusion_ups_get_return_label(self, picking, tracking_number=None, origin_date=None):
        res = []
        superself = self.sudo()
        srm = UPSRequest(self.log_xml, superself.ups_username, superself.ups_passwd, superself.ups_shipper_number, superself.ups_access_number, self.prod_environment)
        ResCurrency = self.env['res.currency']

        packages = self._get_packages_from_picking(picking, self.ups_default_package_type_id)

        invoice_line_total = 0
        for move in picking.move_ids:
            invoice_line_total += picking.company_id.currency_id.round(move.product_id.lst_price * move.product_qty)

        shipment_info = {
            'is_return': True,
            'invoice_date': fields.Date.today().strftime('%Y%m%d'),
            'description': picking.origin or picking.name,
            'total_qty': sum(sml.quantity for sml in picking.move_line_ids),
            'ilt_monetary_value': '%d' % invoice_line_total,
            'itl_currency_code': self.env.company.currency_id.name,
            'phone': picking.partner_id.phone or picking.sale_id.partner_id.phone,
        }
        if picking.sale_id and picking.sale_id.carrier_id != picking.carrier_id:
            ups_service_type = picking.carrier_id.ups_default_service_type or self.ups_default_service_type
        else:
            ups_service_type = self.ups_default_service_type
        ups_carrier_account = False
        if self.ups_bill_my_account:
            ups_carrier_account = picking.partner_id.with_company(picking.company_id).property_ups_carrier_account

        if picking.carrier_id.allow_cash_on_delivery:
            cod_info = {
                'currency': picking.partner_id.country_id.currency_id.name,
                'monetary_value': picking.sale_id.amount_total,
                'funds_code': self.ups_cod_funds_code,
            }
        else:
            cod_info = None

        check_value = srm.check_required_value(picking.partner_id, picking.partner_id, picking.picking_type_id.warehouse_id.partner_id)
        if check_value:
            raise UserError(check_value)

        srm.send_shipping(
            carrier=picking.carrier_id, shipment_info=shipment_info, packages=packages, shipper=picking.company_id.partner_id, ship_from=picking.partner_id,
            ship_to=picking.picking_type_id.warehouse_id.partner_id, service_type=ups_service_type, duty_payment='RECIPIENT', label_file_type=self.ups_label_file_type, ups_carrier_account=ups_carrier_account,
            saturday_delivery=picking.carrier_id.ups_saturday_delivery, cod_info=cod_info)
        srm.return_label()
        result = srm.process_shipment()
        if result.get('error_message'):
            raise UserError(result['error_message'].__str__())

        order = picking.sale_id
        company = order.company_id or picking.company_id or self.env.company
        currency_order = picking.sale_id.currency_id
        if not currency_order:
            currency_order = picking.company_id.currency_id

        if currency_order.name == result['currency_code']:
            price = float(result['price'])
        else:
            quote_currency = ResCurrency.search([('name', '=', result['currency_code'])], limit=1)
            price = quote_currency._convert(
                float(result['price']), currency_order, company, order.date_order or fields.Date.today())

        package_labels = []
        for track_number, label_binary_data in result.get('label_binary_data').items():
            package_labels = package_labels + [(track_number, label_binary_data)]

        carrier_tracking_ref = "+".join([pl[0] for pl in package_labels])
        logmessage = _("Return label generated") + Markup("<br/><b>") + \
                     _("Tracking Numbers:") + Markup("</b> ") + carrier_tracking_ref + \
                     Markup("<br/><b>") + _("Packages:") + Markup("</b> ") + \
                     ','.join([p.name for p in packages if p.name])
        if self.ups_label_file_type != 'GIF':
            attachments = [('%s-%s-%s.%s' % (self.get_return_label_prefix(), pl[0], index, self.ups_label_file_type), pl[1]) for index, pl in enumerate(package_labels)]
        if self.ups_label_file_type == 'GIF':
            attachments = [('%s-%s-%s.%s' % (self.get_return_label_prefix(), package_labels[0][0], 1, 'pdf'), pdf.merge_pdf([pl[1] for pl in package_labels]))]
        picking.message_post(body=logmessage, attachments=attachments)
        shipping_data = {
            'exact_price': price,
            'tracking_number': carrier_tracking_ref}
        res = res + [shipping_data]
        return res

    def fusion_ups_get_tracking_link(self, picking):
        return 'http://wwwapps.ups.com/WebTracking/track?track=yes&trackNums=%s' % picking.carrier_tracking_ref.replace('+', '%0D%0A')

    def fusion_ups_cancel_shipment(self, picking):
        tracking_ref = picking.carrier_tracking_ref
        if not self.prod_environment:
            tracking_ref = "1ZISDE016691676846"  # used for testing purpose

        superself = self.sudo()
        srm = UPSRequest(self.log_xml, superself.ups_username, superself.ups_passwd, superself.ups_shipper_number, superself.ups_access_number, self.prod_environment)
        result = srm.cancel_shipment(tracking_ref.partition('+')[0])

        if result.get('error_message'):
            raise UserError(result['error_message'].__str__())
        else:
            picking.message_post(body=_(u'Shipment #%s has been cancelled', picking.carrier_tracking_ref))
            picking.write({'carrier_tracking_ref': '',
                           'carrier_price': 0.0})

    def _ups_get_default_custom_package_code(self):
        return '02'

    def _ups_convert_weight(self, weight, unit='KGS'):
        weight_uom_id = self.env['product.template']._get_weight_uom_id_from_ir_config_parameter()
        if unit == 'KGS':
            return weight_uom_id._compute_quantity(weight, self.env.ref('uom.product_uom_kgm'), round=False)
        elif unit == 'LBS':
            return weight_uom_id._compute_quantity(weight, self.env.ref('uom.product_uom_lb'), round=False)
        else:
            raise ValueError

    # ══════════════════════════════════════════════════════════════════════════
    # UPS REST METHODS
    # ══════════════════════════════════════════════════════════════════════════

    def fusion_ups_rest_rate_shipment(self, order):
        ups = UPSRestRequest(self)
        packages = self._get_packages_from_order(order, self.ups_default_packaging_id)

        if self.allow_cash_on_delivery:
            cod_info = {
                'currency': order.partner_id.country_id.currency_id.name,
                'monetary_value': order.amount_total,
                'funds_code': self.ups_cod_funds_code,
            }
        else:
            cod_info = None

        check_value = ups._check_required_value(order=order)
        if check_value:
            return {'success': False,
                    'price': 0.0,
                    'error_message': check_value,
                    'warning_message': False}

        total_qty = sum([line.product_uom_qty for line in order.order_line.filtered(lambda line: not line.is_delivery and not line.display_type)])

        result = ups._get_shipping_price(order.company_id.partner_id, order.warehouse_id.partner_id,
                                         order.partner_shipping_id, total_qty, packages, self, cod_info=cod_info)

        if result.get('error_message'):
            return {'success': False,
                    'price': 0.0,
                    'error_message': _('Error:\n%s', result['error_message']),
                    'warning_message': False}

        if order.currency_id.name == result['currency_code']:
            price = float(result['price'])
        else:
            quote_currency = self.env['res.currency'].search([('name', '=', result['currency_code'])], limit=1)
            price = quote_currency._convert(
                float(result['price']), order.currency_id, order.company_id, order.date_order or fields.Date.today())

        if self.ups_bill_my_account and order.partner_ups_carrier_account:
            price = 0.0

        return {'success': True,
                'price': price,
                'error_message': False,
                'warning_message': result.get('alert_message', False)}

    def _ups_rest_prepare_shipping_data(self, picking):
        packages = self._get_packages_from_picking(picking, self.ups_default_packaging_id)

        terms_of_shipment = picking.company_id.incoterm_id
        if picking.sale_id and picking.sale_id.incoterm:
            terms_of_shipment = picking.sale_id.incoterm

        currency_code = self.env.company.currency_id.name
        if picking.sale_id:
            currency_code = picking.sale_id.currency_id.name

        shipment_info = {
            'require_invoice': picking._should_generate_commercial_invoice(),
            'invoice_date': fields.Date.today().strftime('%Y%m%d'),
            'description': picking.origin or picking.name,
            'total_qty': sum(sml.quantity for sml in picking.move_line_ids),
            'ilt_monetary_value': '%d' % sum(sml.sale_price for sml in picking.move_line_ids),
            'itl_currency_code': currency_code,
            'phone': picking.partner_id.phone or picking.sale_id.partner_id.phone,
            'terms_of_shipment': terms_of_shipment.code if terms_of_shipment else None,
            'purchase_order_number': picking.sale_id.name if picking.sale_id else None,
            'reference_number': [
                {'Code': 'BM', 'Value': picking.name},
            ],
        }
        if picking.sale_id and picking.sale_id.carrier_id != picking.carrier_id:
            ups_service_type = picking.carrier_id.ups_default_service_type or self.ups_default_service_type
        else:
            ups_service_type = self.ups_default_service_type
        ups_carrier_account = False
        if self.ups_bill_my_account:
            ups_carrier_account = picking.partner_id.with_company(picking.company_id).property_ups_carrier_account

        if picking.carrier_id.allow_cash_on_delivery:
            cod_info = {
                'currency': picking.partner_id.country_id.currency_id.name,
                'monetary_value': picking.sale_id.amount_total,
                'funds_code': self.ups_cod_funds_code,
            }
        else:
            cod_info = None
        return packages, shipment_info, ups_service_type, ups_carrier_account, cod_info

    def fusion_ups_rest_send_shipping(self, pickings):
        res = []
        ups = UPSRestRequest(self)
        for picking in pickings:
            packages, shipment_info, ups_service_type, ups_carrier_account, cod_info = self._ups_rest_prepare_shipping_data(picking)

            check_value = ups._check_required_value(picking=picking)
            if check_value:
                raise UserError(check_value)

            result = ups._send_shipping(
                shipment_info=shipment_info, packages=packages, carrier=self, shipper=picking.company_id.partner_id, ship_from=picking.picking_type_id.warehouse_id.partner_id,
                ship_to=picking.partner_id, service_type=ups_service_type, duty_payment=picking.carrier_id.ups_duty_payment,
                saturday_delivery=picking.carrier_id.ups_saturday_delivery, cod_info=cod_info,
                label_file_type=self.ups_label_file_type, ups_carrier_account=ups_carrier_account)

            order = picking.sale_id
            company = order.company_id or picking.company_id or self.env.company
            currency_order = picking.sale_id.currency_id
            if not currency_order:
                currency_order = picking.company_id.currency_id

            if currency_order.name == result['currency_code']:
                price = float(result['price'])
            else:
                quote_currency = self.env['res.currency'].search([('name', '=', result['currency_code'])], limit=1)
                price = quote_currency._convert(
                    float(result['price']), currency_order, company, order.date_order or fields.Date.today())

            package_labels = result.get('label_binary_data', [])

            carrier_tracking_ref = "+".join([pl[0] for pl in package_labels])
            logmessage = _("Shipment created into UPS<br/>"
                           "<b>Tracking Numbers:</b> %(tracking_numbers)s<br/>"
                           "<b>Packages:</b> %(packages)s",
                           tracking_numbers=carrier_tracking_ref,
                           packages=[p.name for p in packages if p.name])
            if self.ups_label_file_type != 'GIF':
                attachments = [('LabelUPS-%s.%s' % (pl[0], self.ups_label_file_type), pl[1]) for pl in package_labels]
            else:
                attachments = [('LabelUPS.pdf', pdf.merge_pdf([pl[1] for pl in package_labels]))]
            if result.get('invoice_binary_data'):
                attachments.append(('UPSCommercialInvoice.pdf', result['invoice_binary_data']))
            picking.message_post(body=logmessage, attachments=attachments)
            shipping_data = {
                'exact_price': price,
                'tracking_number': carrier_tracking_ref}
            res = res + [shipping_data]
            if self.return_label_on_delivery:
                try:
                    self.fusion_ups_rest_get_return_label(picking)
                except (UserError, ValidationError) as err:
                    try:
                        ups._cancel_shipping(result['tracking_ref'])
                    except ValidationError:
                        pass
                    raise UserError(err)
        return res

    def fusion_ups_rest_get_return_label(self, picking, tracking_number=None, origin_date=None):
        res = []
        ups = UPSRestRequest(self)
        packages, shipment_info, ups_service_type, ups_carrier_account, cod_info = self._ups_rest_prepare_shipping_data(picking)

        check_value = ups._check_required_value(picking=picking, is_return=True)
        if check_value:
            raise UserError(check_value)

        result = ups._send_shipping(
            shipment_info=shipment_info, packages=packages, carrier=self, shipper=picking.company_id.partner_id, ship_from=picking.partner_id,
            ship_to=picking.picking_type_id.warehouse_id.partner_id, service_type=ups_service_type, duty_payment='RECIPIENT', saturday_delivery=picking.carrier_id.ups_saturday_delivery,
            cod_info=cod_info, label_file_type=self.ups_label_file_type, ups_carrier_account=ups_carrier_account, is_return=True)

        order = picking.sale_id
        company = order.company_id or picking.company_id or self.env.company
        currency_order = picking.sale_id.currency_id
        if not currency_order:
            currency_order = picking.company_id.currency_id

        if currency_order.name == result['currency_code']:
            price = float(result['price'])
        else:
            quote_currency = self.env['res.currency'].search([('name', '=', result['currency_code'])], limit=1)
            price = quote_currency._convert(
                float(result['price']), currency_order, company, order.date_order or fields.Date.today())

        package_labels = []
        for track_number, label_binary_data in result.get('label_binary_data'):
            package_labels = package_labels + [(track_number, label_binary_data)]

        carrier_tracking_ref = "+".join([pl[0] for pl in package_labels])
        logmessage = _("Return label generated<br/>"
                        "<b>Tracking Numbers:</b> %(tracking_numbers)s<br/>"
                        "<b>Packages:</b> %(packages)s",
                        tracking_numbers=carrier_tracking_ref,
                        packages=[p.name for p in packages if p.name])
        if self.ups_label_file_type != 'GIF':
            attachments = [('%s-%s-%s.%s' % (self.get_return_label_prefix(), pl[0], index, self.ups_label_file_type), pl[1]) for index, pl in enumerate(package_labels)]
        else:
            attachments = [('%s-%s-%s.%s' % (self.get_return_label_prefix(), package_labels[0][0], 1, 'pdf'), pdf.merge_pdf([pl[1] for pl in package_labels]))]
        picking.message_post(body=logmessage, attachments=attachments)
        shipping_data = {
            'exact_price': price,
            'tracking_number': carrier_tracking_ref}
        res = res + [shipping_data]
        return res

    def fusion_ups_rest_get_tracking_link(self, picking):
        return 'http://wwwapps.ups.com/WebTracking/track?track=yes&trackNums=%s' % picking.carrier_tracking_ref

    def fusion_ups_rest_cancel_shipment(self, picking):
        ups = UPSRestRequest(self)
        ups._cancel_shipping(picking.carrier_tracking_ref.partition('+')[0])
        picking.message_post(body=_(u'Shipment #%s has been cancelled', picking.carrier_tracking_ref))
        picking.write({'carrier_tracking_ref': '',
                        'carrier_price': 0.0})

    def _get_delivery_type(self):
        """Override of delivery to return the ups delivery type for UPS REST."""
        res = super()._get_delivery_type()
        if self.delivery_type != 'fusion_ups_rest':
            return res
        return 'ups'

    # ══════════════════════════════════════════════════════════════════════════
    # FEDEX SOAP (Legacy) METHODS
    # ══════════════════════════════════════════════════════════════════════════

    @api.onchange('fedex_service_type')
    def on_change_fedex_service_type(self):
        self.fedex_saturday_delivery = False

    def fusion_fedex_rate_shipment(self, order):
        is_india = order.partner_shipping_id.country_id.code == 'IN' and order.company_id.partner_id.country_id.code == 'IN'

        order_currency = order.currency_id
        superself = self.sudo()

        # Authentication stuff
        srm = FedexRequest(self.log_xml, request_type="rating", prod_environment=self.prod_environment)
        srm.web_authentication_detail(superself.fedex_developer_key, superself.fedex_developer_password)
        srm.client_detail(superself.fedex_account_number, superself.fedex_meter_number)

        # Build basic rating request and set addresses
        srm.transaction_detail(order.name)
        srm.shipment_request(
            self.fedex_droppoff_type,
            self.fedex_service_type,
            self.fedex_default_package_type_id.shipper_package_code,
            self.fedex_weight_unit,
            self.fedex_saturday_delivery,
        )

        srm.set_currency(_convert_curr_iso_fdx(order_currency.name))
        srm.set_shipper(order.company_id.partner_id, order.warehouse_id.partner_id)
        srm.set_recipient(order.partner_shipping_id)

        packages = self._get_packages_from_order(order, self.fedex_default_package_type_id)

        for sequence, package in enumerate(packages, 1):
            srm.add_package(
                self,
                package,
                _convert_curr_iso_fdx(package.company_id.currency_id.name),
                sequence_number=sequence,
                mode='rating'
            )

        weight_value = self._fedex_convert_weight(order._get_estimated_weight(), self.fedex_weight_unit)
        srm.set_master_package(weight_value, 1)

        # Commodities for customs declaration (international shipping)
        if 'INTERNATIONAL' in self.fedex_service_type or self.fedex_service_type == 'FEDEX_REGIONAL_ECONOMY' or is_india:
            commodities = self._get_commodities_from_order(order)
            for commodity in commodities:
                srm.commodities(self, commodity, _convert_curr_iso_fdx(order_currency.name))

            total_commodities_amount = sum(c.monetary_value * c.qty for c in commodities)
            srm.customs_value(_convert_curr_iso_fdx(order_currency.name), total_commodities_amount, "NON_DOCUMENTS")
            srm.duties_payment(order.warehouse_id.partner_id, superself.fedex_account_number, superself.fedex_duty_payment)

        # Prepare the request
        self._fedex_update_srm(srm, 'rate', order=order)
        del srm.ClientDetail['Region']
        request = serialize_object(dict(WebAuthenticationDetail=srm.WebAuthenticationDetail,
                                        ClientDetail=srm.ClientDetail,
                                        TransactionDetail=srm.TransactionDetail,
                                        VersionId=srm.VersionId,
                                        RequestedShipment=srm.RequestedShipment))
        self._fedex_add_extra_data_to_request(request, 'rate')
        response = srm.rate(request)

        warnings = response.get('warnings_message')
        if warnings:
            _logger.info(warnings)

        if response.get('errors_message'):
            return {'success': False,
                    'price': 0.0,
                    'error_message': _('Error:\n%s', response['errors_message']),
                    'warning_message': False}

        price = self._get_request_price(response['price'], order, order_currency)
        return {'success': True,
                'price': price,
                'error_message': False,
                'warning_message': _('Warning:\n%s', warnings) if warnings else False}

    def fusion_fedex_send_shipping(self, pickings):
        res = []

        for picking in pickings:
            order_currency = picking.sale_id.currency_id or picking.company_id.currency_id

            srm = FedexRequest(self.log_xml, request_type="shipping", prod_environment=self.prod_environment)
            superself = self.sudo()
            srm.web_authentication_detail(superself.fedex_developer_key, superself.fedex_developer_password)
            srm.client_detail(superself.fedex_account_number, superself.fedex_meter_number)

            srm.transaction_detail(picking.id)

            packages = picking.move_line_ids.result_package_id
            package_type = packages and packages[0].package_type_id.shipper_package_code or self.fedex_default_package_type_id.shipper_package_code
            srm.shipment_request(self.fedex_droppoff_type, self.fedex_service_type, package_type, self.fedex_weight_unit, self.fedex_saturday_delivery)
            srm.set_currency(_convert_curr_iso_fdx(order_currency.name))
            srm.set_shipper(picking.company_id.partner_id, picking.picking_type_id.warehouse_id.partner_id)
            srm.set_recipient(picking.partner_id)

            srm.shipping_charges_payment(superself.fedex_account_number)

            srm.shipment_label('COMMON2D', self.fedex_label_file_type, self.fedex_label_stock_type, 'TOP_EDGE_OF_TEXT_FIRST', 'SHIPPING_LABEL_FIRST')

            order = picking.sale_id

            net_weight = self._fedex_convert_weight(picking.shipping_weight, self.fedex_weight_unit)

            # Commodities for customs declaration (international shipping)
            if 'INTERNATIONAL' in self.fedex_service_type or self.fedex_service_type == 'FEDEX_REGIONAL_ECONOMY' or (picking.partner_id.country_id.code == 'IN' and picking.picking_type_id.warehouse_id.partner_id.country_id.code == 'IN'):

                commodities = self._get_commodities_from_stock_move_lines(picking.move_line_ids)
                for commodity in commodities:
                    srm.commodities(self, commodity, _convert_curr_iso_fdx(order_currency.name))

                total_commodities_amount = sum(c.monetary_value * c.qty for c in commodities)
                srm.customs_value(_convert_curr_iso_fdx(order_currency.name), total_commodities_amount, "NON_DOCUMENTS")
                srm.duties_payment(order.warehouse_id.partner_id, superself.fedex_account_number, superself.fedex_duty_payment)

                send_etd = superself.env['ir.config_parameter'].get_param("delivery_fedex.send_etd")
                srm.commercial_invoice(self.fedex_document_stock_type, send_etd)

            package_count = len(picking.move_line_ids.result_package_id) or 1

            # For india picking courier is not accepted without this details in label.
            po_number = order.display_name or False
            dept_number = False
            if picking.partner_id.country_id.code == 'IN' and picking.picking_type_id.warehouse_id.partner_id.country_id.code == 'IN':
                po_number = 'B2B' if picking.partner_id.commercial_partner_id.is_company else 'B2C'
                dept_number = 'BILL D/T: SENDER'

            packages = self._get_packages_from_picking(picking, self.fedex_default_package_type_id)

            master_tracking_id = False
            package_labels = []
            carrier_tracking_refs = []
            lognote_pickings = picking.sale_id.picking_ids if picking.sale_id else picking

            for sequence, package in enumerate(packages, start=1):

                srm.add_package(
                    self,
                    package,
                    _convert_curr_iso_fdx(package.company_id.currency_id.name),
                    sequence_number=sequence,
                    po_number=po_number,
                    dept_number=dept_number,
                    reference=picking.display_name,
                )
                srm.set_master_package(net_weight, len(packages), master_tracking_id=master_tracking_id)

                # Prepare the request
                self._fedex_update_srm(srm, 'ship', picking=picking)
                request = serialize_object(dict(WebAuthenticationDetail=srm.WebAuthenticationDetail,
                                                ClientDetail=srm.ClientDetail,
                                                TransactionDetail=srm.TransactionDetail,
                                                VersionId=srm.VersionId,
                                                RequestedShipment=srm.RequestedShipment))
                self._fedex_add_extra_data_to_request(request, 'ship')
                response = srm.process_shipment(request)

                warnings = response.get('warnings_message')
                if warnings:
                    _logger.info(warnings)

                if response.get('errors_message'):
                    raise UserError(response['errors_message'])

                package_name = package.name or 'package-' + str(sequence)
                package_labels.append((package_name, srm.get_label()))
                carrier_tracking_refs.append(response['tracking_number'])

                # First package
                if sequence == 1:
                    master_tracking_id = response['master_tracking_id']

                # Last package
                if sequence == package_count:

                    carrier_price = self._get_request_price(response['price'], order, order_currency)

                    logmessage = _(
                        "Shipment created into Fedex %(line_break)s"
                        "%(bold_start)s Tracking Numbers: %(bold_end)s %(tracking_numbers)s %(line_break)s"
                        "%(bold_start)s Packages: %(bold_end)s %(packages)s",
                        tracking_numbers=carrier_tracking_refs,
                        packages=[pl[0] for pl in package_labels],
                        line_break=Markup("<br>"),
                        bold_start=Markup("<b>"),
                        bold_end=Markup("</b>"),
                    )
                    if self.fedex_label_file_type != 'PDF':
                        attachments = [('%s-%s.%s' % (self._get_delivery_label_prefix(), pl[0], self.fedex_label_file_type), pl[1]) for pl in package_labels]
                    if self.fedex_label_file_type == 'PDF':
                        attachments = [('%s.pdf' % (self._get_delivery_label_prefix()), pdf.merge_pdf([pl[1] for pl in package_labels]))]
                    for pick in lognote_pickings:
                        pick.message_post(body=logmessage, attachments=attachments)
                    shipping_data = {'exact_price': carrier_price,
                                     'tracking_number': ','.join(carrier_tracking_refs)}
                    res = res + [shipping_data]

            if self.return_label_on_delivery:
                self.get_return_label(picking, tracking_number=response['tracking_number'], origin_date=response['date'])
            commercial_invoice = srm.get_document()
            if commercial_invoice:
                fedex_documents = [('%s.pdf' % self._get_delivery_doc_prefix(), commercial_invoice)]
                for pick in lognote_pickings:
                    pick.message_post(body=_('Fedex Documents'), attachments=fedex_documents)
        return res

    def fusion_fedex_get_return_label(self, picking, tracking_number=None, origin_date=None):
        srm = FedexRequest(self.log_xml, request_type="shipping", prod_environment=self.prod_environment)
        superself = self.sudo()
        srm.web_authentication_detail(superself.fedex_developer_key, superself.fedex_developer_password)
        srm.client_detail(superself.fedex_account_number, superself.fedex_meter_number)

        srm.transaction_detail(picking.id)

        packages = picking.move_line_ids.result_package_id
        package_type = packages and packages[0].package_type_id.shipper_package_code or self.fedex_default_package_type_id.shipper_package_code
        srm.shipment_request(self.fedex_droppoff_type, self.fedex_service_type, package_type, self.fedex_weight_unit, self.fedex_saturday_delivery)
        srm.set_currency(_convert_curr_iso_fdx(picking.company_id.currency_id.name))
        srm.set_shipper(picking.partner_id, picking.partner_id)
        srm.set_recipient(picking.company_id.partner_id)

        srm.shipping_charges_payment(superself.fedex_account_number)

        srm.shipment_label('COMMON2D', self.fedex_label_file_type, self.fedex_label_stock_type, 'TOP_EDGE_OF_TEXT_FIRST', 'SHIPPING_LABEL_FIRST')
        if picking.is_return_picking:
            net_weight = self._fedex_convert_weight(picking._get_estimated_weight(), self.fedex_weight_unit)
        else:
            net_weight = self._fedex_convert_weight(picking.shipping_weight, self.fedex_weight_unit)
        package_type = packages[:1].package_type_id or picking.carrier_id.fedex_default_package_type_id
        order = picking.sale_id
        po_number = order.display_name or False
        dept_number = False
        packages = self._get_packages_from_picking(picking, self.fedex_default_package_type_id)
        for pkg in packages:
            srm.add_package(self, pkg, _convert_curr_iso_fdx(pkg.company_id.currency_id.name), reference=picking.display_name, po_number=po_number, dept_number=dept_number)
        srm.set_master_package(net_weight, 1)
        if 'INTERNATIONAL' in self.fedex_service_type or self.fedex_service_type == 'FEDEX_REGIONAL_ECONOMY' or (picking.partner_id.country_id.code == 'IN' and picking.picking_type_id.warehouse_id.partner_id.country_id.code == 'IN'):

            order_currency = picking.sale_id.currency_id or picking.company_id.currency_id

            commodities = [com for pack in packages for com in pack.commodities]
            for commodity in commodities:
                srm.commodities(self, commodity, _convert_curr_iso_fdx(order_currency.name))

            total_commodities_amount = sum(com.monetary_value * com.qty for com in commodities)
            srm.customs_value(_convert_curr_iso_fdx(order_currency.name), total_commodities_amount, "NON_DOCUMENTS")
            srm.duties_payment(order.warehouse_id.partner_id, superself.fedex_account_number, superself.fedex_duty_payment)

            srm.customs_value(_convert_curr_iso_fdx(order_currency.name), total_commodities_amount, "NON_DOCUMENTS")
            # We consider that returns are always paid by the company creating the label
            srm.duties_payment(picking.picking_type_id.warehouse_id.partner_id, superself.fedex_account_number, 'SENDER')
        srm.return_label(tracking_number, origin_date)

        # Prepare the request
        self._fedex_update_srm(srm, 'return', picking=picking)
        request = serialize_object(dict(WebAuthenticationDetail=srm.WebAuthenticationDetail,
                                        ClientDetail=srm.ClientDetail,
                                        TransactionDetail=srm.TransactionDetail,
                                        VersionId=srm.VersionId,
                                        RequestedShipment=srm.RequestedShipment))
        self._fedex_add_extra_data_to_request(request, 'return')
        response = srm.process_shipment(request)
        if not response.get('errors_message'):
            fedex_labels = [('%s-%s-%s.%s' % (self.get_return_label_prefix(), response['tracking_number'], index, self.fedex_label_file_type), label)
                            for index, label in enumerate(srm._get_labels(self.fedex_label_file_type))]
            picking.message_post(body=_('Return Label'), attachments=fedex_labels)
        else:
            raise UserError(response['errors_message'])

    def fusion_fedex_get_tracking_link(self, picking):
        return 'https://www.fedex.com/apps/fedextrack/?action=track&trackingnumber=%s' % picking.carrier_tracking_ref

    def fusion_fedex_cancel_shipment(self, picking):
        request = FedexRequest(self.log_xml, request_type="shipping", prod_environment=self.prod_environment)
        superself = self.sudo()
        request.web_authentication_detail(superself.fedex_developer_key, superself.fedex_developer_password)
        request.client_detail(superself.fedex_account_number, superself.fedex_meter_number)
        request.transaction_detail(picking.id)

        master_tracking_id = picking.carrier_tracking_ref.split(',')[0]
        request.set_deletion_details(master_tracking_id)
        serialized_request = serialize_object(dict(WebAuthenticationDetail=request.WebAuthenticationDetail,
                                                   ClientDetail=request.ClientDetail,
                                                   TransactionDetail=request.TransactionDetail,
                                                   VersionId=request.VersionId,
                                                   TrackingId=request.TrackingId,
                                                   DeletionControl=request.DeletionControl))
        result = request.delete_shipment(serialized_request)

        warnings = result.get('warnings_message')
        if warnings:
            _logger.info(warnings)

        if result.get('delete_success') and not result.get('errors_message'):
            picking.message_post(body=_(u'Shipment #%s has been cancelled', master_tracking_id))
            picking.write({'carrier_tracking_ref': '',
                           'carrier_price': 0.0})
        else:
            raise UserError(result['errors_message'])

    def _get_request_price(self, req_price, order, order_currency=None):
        """Extract price info in target currency, converting if necessary"""
        if not order_currency:
            order_currency = order.currency_id
        company = order.company_id or self.env.user.company_id
        fdx_currency = _convert_curr_iso_fdx(order_currency.name)
        if fdx_currency in req_price:
            return req_price[fdx_currency]
        _logger.info("Preferred currency has not been found in FedEx response")
        fdx_currency = _convert_curr_iso_fdx(company.currency_id.name)
        if fdx_currency in req_price:
            return company.currency_id._convert(
                req_price[fdx_currency], order_currency, company, order.date_order or fields.Date.today())
        currency_codes = list(req_price.keys())
        currency_codes += [_convert_curr_fdx_iso(c) for c in currency_codes]
        currency_instances = self.env['res.currency'].search([('name', 'in', currency_codes)])
        currency_by_name = {c.name: c for c in currency_instances}
        for fdx_currency in req_price:
            if fdx_currency in currency_by_name:
                return currency_by_name[fdx_currency]._convert(
                    req_price[fdx_currency], order_currency, company, order.date_order or fields.Date.today())
        _logger.info("No known currency has not been found in FedEx response")
        return 0.0

    def _fedex_add_extra_data_to_request(self, request, request_type):
        """Adds the extra data to the request (FedEx SOAP legacy)."""
        extra_data_input = {
            'rate': self.fedex_extra_data_rate_request,
            'ship': self.fedex_extra_data_ship_request,
            'return': self.fedex_extra_data_return_request,
        }.get(request_type) or ''
        try:
            extra_data = const_eval('{' + extra_data_input + '}')
        except SyntaxError:
            raise UserError(_('Invalid syntax for FedEx extra data.'))

        def extra_data_to_request(request, extra_data):
            """recursive function that adds extra data to the current request."""
            for key, new_value in extra_data.items():
                request[key] = current_value = request.get(key)
                if isinstance(current_value, list):
                    for item in current_value:
                        extra_data_to_request(item, new_value)
                elif isinstance(new_value, dict) and isinstance(current_value, dict):
                    extra_data_to_request(current_value, new_value)
                else:
                    request[key] = new_value

        extra_data_to_request(request, extra_data)

    def _fedex_get_default_custom_package_code(self):
        return 'YOUR_PACKAGING'

    def _fedex_convert_weight(self, weight, unit='KG'):
        if unit == 'KG':
            convert_to = 'uom.product_uom_kgm'
        elif unit == 'LB':
            convert_to = 'uom.product_uom_lb'
        else:
            raise ValueError

        weight_uom_id = self.env['product.template']._get_weight_uom_id_from_ir_config_parameter()
        new_value = weight_uom_id._compute_quantity(weight, self.env.ref(convert_to), round=False)

        if weight > 0.0:
            new_value = max(new_value, 0.01)

        return float_repr(new_value, 10)

    def _fedex_update_srm(self, srm, request_type, order=None, picking=None):
        """ Hook to introduce new custom behaviors in the Fedex request. """
        return srm

    # ══════════════════════════════════════════════════════════════════════════
    # FEDEX REST METHODS
    # ══════════════════════════════════════════════════════════════════════════

    def fusion_fedex_rest_rate_shipment(self, order):
        srm = FedexRestRequest(self)
        try:
            response = srm._get_shipping_price(
                ship_from=order.warehouse_id.partner_id,
                ship_to=order.partner_shipping_id,
                packages=self._get_packages_from_order(order, self.fedex_rest_default_package_type_id),
                currency=order.currency_id.name
            )
        except ValidationError as err:
            return {'success': False,
                    'price': 0.0,
                    'error_message': _('Error(s) from FedEx:\n%s', err),
                    'warning_message': False}

        warnings = response.get('alert_message')
        if warnings:
            _logger.info(warnings)

        return {'success': True,
                'price': response.get('price'),
                'error_message': False,
                'warning_message': _('Warning(s) from FedEx:\n%s', warnings) if warnings else False}

    def fusion_fedex_rest_send_shipping(self, pickings):
        res = []
        srm = FedexRestRequest(self)
        for picking in pickings:
            packages = self._get_packages_from_picking(picking, self.fedex_rest_default_package_type_id)
            response = srm._ship_package(
                ship_from_wh=picking.picking_type_id.warehouse_id.partner_id,
                ship_from_company=picking.company_id.partner_id,
                ship_to=picking.partner_id,
                sold_to=picking.sale_id.partner_invoice_id,
                packages=packages,
                currency=picking.sale_id.currency_id.name or picking.company_id.currency_id.name,
                order_no=picking.sale_id.name,
                customer_ref=picking.sale_id.client_order_ref,
                picking_no=picking.name,
                incoterms=picking.sale_id.incoterm.code,
                freight_charge=picking.sale_id.order_line.filtered(lambda sol: sol.is_delivery and sol.product_id == self.product_id).price_total,
            )

            warnings = response.get('alert_message')
            if warnings:
                _logger.info(warnings)

            logmessage = (_("Shipment created into Fedex") + Markup("<br/>") +
                          response.get('service_info') + Markup("<br/><b>") +
                         _("Tracking Numbers:") + Markup("</b> ") + response.get('tracking_numbers') + Markup("<br/><b>") +
                         _("Packages:") + Markup("</b> ") + ','.join([p.name for p in packages if p.name]))

            if response.get('documents'):
                logmessage += Markup("<br/><b>") + _("Required documents:") + Markup("</b> ") + response.get('documents')

            attachments = [
                ('%s-%s.%s' % (self._get_delivery_label_prefix(), nr, self.fedex_rest_label_file_type), base64.b64decode(label))
                for nr, label in response.get('labels')
            ]
            if response.get('invoice'):
                attachments.append(('%s.pdf' % self._get_delivery_doc_prefix(), base64.b64decode(response.get('invoice'))))

            lognote_pickings = picking
            if picking.sale_id:
                lognote_pickings |= picking.sale_id.picking_ids.filtered(lambda p: p.state not in ('done', 'cancel'))

            for pick in lognote_pickings:
                pick.message_post(body=logmessage, attachments=attachments)

            res.append({'exact_price': response.get('price'), 'tracking_number': response.get('tracking_numbers')})

            if self.return_label_on_delivery:
                if len(packages) > 1:
                    picking.message_post(body=_("Automated return label generation is not supported by FedEx for multi-package shipments. Please generate the return labels manually."))
                else:
                    self.get_return_label(picking, tracking_number=response.get('tracking_numbers').split(',')[0], origin_date=response.get('date'))

        return res

    def fusion_fedex_rest_get_return_label(self, picking, tracking_number=None, origin_date=None):
        srm = FedexRestRequest(self)
        response = srm._return_package(
            ship_from=picking.partner_id,
            ship_to_company=picking.company_id.partner_id,
            ship_to_wh=picking.picking_type_id.warehouse_id.partner_id,
            packages=self._get_packages_from_picking(picking, self.fedex_rest_default_package_type_id),
            currency=picking.sale_id.currency_id.name or picking.company_id.currency_id.name,
            tracking=tracking_number,
            date=origin_date
        )

        warnings = response.get('alert_message')
        if warnings:
            _logger.info(warnings)

        logmessage = (_("Return Label") + Markup("<br/><b>") +
                      _("Tracking Numbers:") + Markup("</b> ") + response.get('tracking_numbers') + Markup("<br/>"))

        if response.get('documents'):
            logmessage += Markup("<b>") + _("Required documents:") + Markup("</b> ") + response.get('documents')

        fedex_labels = [('%s-%s.%s' % (self.get_return_label_prefix(), nr, self.fedex_rest_label_file_type), base64.b64decode(label))
                        for nr, label in response.get('labels')]
        picking.message_post(body=logmessage, attachments=fedex_labels)

    def fusion_fedex_rest_get_tracking_link(self, picking):
        return 'https://www.fedex.com/wtrk/track/?trknbr=%s' % picking.carrier_tracking_ref

    def fusion_fedex_rest_cancel_shipment(self, picking):
        master_tracking = picking.carrier_tracking_ref.split(',')[0]
        request = FedexRestRequest(self)
        result = request.cancel_shipment(master_tracking)

        warnings = result.get('warnings_message')
        if warnings:
            _logger.info(warnings)

        if result.get('delete_success') and not result.get('errors_message'):
            picking.message_post(body=_('Shipment %s has been cancelled', picking.carrier_tracking_ref))
            picking.write({'carrier_tracking_ref': '', 'carrier_price': 0.0})
        else:
            raise UserError(result['errors_message'])

    def _fedex_rest_convert_weight(self, weight):
        if self.fedex_rest_weight_unit == 'KG':
            convert_to = 'uom.product_uom_kgm'
        elif self.fedex_rest_weight_unit == 'LB':
            convert_to = 'uom.product_uom_lb'
        else:
            raise ValueError

        weight_uom_id = self.env['product.template']._get_weight_uom_id_from_ir_config_parameter()
        new_value = weight_uom_id._compute_quantity(weight, self.env.ref(convert_to), round=False)

        if weight > 0.0:
            new_value = max(new_value, 0.01)

        return float_repr(new_value, 10)

    # ══════════════════════════════════════════════════════════════════════════
    # DHL SOAP (Legacy) METHODS
    # ══════════════════════════════════════════════════════════════════════════

    def fusion_dhl_rate_shipment(self, order):
        res = self._dhl_soap_rate_shipment_vals(order=order)
        return res

    def _dhl_soap_rate_shipment_vals(self, order=False, picking=False):
        if picking:
            warehouse_partner_id = picking.picking_type_id.warehouse_id.partner_id
            currency_id = picking.sale_id.currency_id or picking.company_id.currency_id
            destination_partner_id = picking.partner_id
            total_value = sum(sml.sale_price for sml in picking.move_line_ids)
        else:
            warehouse_partner_id = order.warehouse_id.partner_id
            currency_id = order.currency_id or order.company_id.currency_id
            total_value = sum(line.price_reduce_taxinc * line.product_uom_qty for line in order.order_line.filtered(lambda l: l.product_id.type == 'consu' and not l.display_type))
            destination_partner_id = order.partner_shipping_id

        rating_request = {}
        srm = DHLProvider(self.log_xml, request_type="rate", prod_environment=self.prod_environment)
        check_value = srm.check_required_value(self, destination_partner_id, warehouse_partner_id, order=order, picking=picking)
        if check_value:
            return {'success': False,
                    'price': 0.0,
                    'error_message': check_value,
                    'warning_message': False}
        site_id = self.sudo().dhl_SiteID
        password = self.sudo().dhl_password
        rating_request['Request'] = srm._set_request(site_id, password)
        rating_request['From'] = srm._set_dct_from(warehouse_partner_id)
        if picking:
            packages = self._get_packages_from_picking(picking, self.dhl_default_package_type_id)
        else:
            packages = self._get_packages_from_order(order, self.dhl_default_package_type_id)
        rating_request['BkgDetails'] = srm._set_dct_bkg_details(self, packages)
        rating_request['To'] = srm._set_dct_to(destination_partner_id)
        if self.dhl_dutiable:
            rating_request['Dutiable'] = srm._set_dct_dutiable(total_value, currency_id.name)
        real_rating_request = {}
        real_rating_request['GetQuote'] = rating_request
        real_rating_request['schemaVersion'] = 2.0
        self._dhl_soap_add_custom_data_to_request(rating_request, 'rate')
        response = srm._process_rating(real_rating_request)

        available_product_code = []
        shipping_charge = False
        qtd_shp = response.findall('GetQuoteResponse/BkgDetails/QtdShp')
        if qtd_shp:
            for q in qtd_shp:
                charge = q.find('ShippingCharge').text
                global_product_code = q.find('GlobalProductCode').text
                if global_product_code == self.dhl_product_code and charge:
                    shipping_charge = charge
                    shipping_currency = q.find('CurrencyCode')
                    shipping_currency = None if shipping_currency is None else shipping_currency.text
                    break
                else:
                    available_product_code.append(global_product_code)
        else:
            condition = response.find('GetQuoteResponse/Note/Condition')
            if condition:
                condition_code = condition.find('ConditionCode').text
                if condition_code == '410301':
                    return {
                        'success': False,
                        'price': 0.0,
                        'error_message': "%s.\n%s" % (condition.find('ConditionData').text, _("Hint: The destination may not require the dutiable option.")),
                        'warning_message': False,
                    }
                elif condition_code in ['420504', '420505', '420506', '410304'] or\
                        response.find('GetQuoteResponse/Note/ActionStatus').text == "Failure":
                    return {
                        'success': False,
                        'price': 0.0,
                        'error_message': "%s." % (condition.find('ConditionData').text),
                        'warning_message': False,
                    }
        if shipping_charge:
            if order:
                order_currency = order.currency_id
            else:
                order_currency = picking.sale_id.currency_id or picking.company_id.currency_id
            if shipping_currency is None or order_currency.name == shipping_currency:
                price = float(shipping_charge)
            else:
                quote_currency = self.env['res.currency'].search([('name', '=', shipping_currency)], limit=1)
                price = quote_currency._convert(float(shipping_charge), order_currency, (order or picking).company_id, order.date_order if order else fields.Date.today())
            return {'success': True,
                    'price': price,
                    'error_message': False,
                    'warning_message': False}

        if available_product_code:
            return {'success': False,
                    'price': 0.0,
                    'error_message': _(
                        "There is no price available for this shipping, you should rather try with the DHL product %s",
                        available_product_code[0]),
                    'warning_message': False}

    def fusion_dhl_send_shipping(self, pickings):
        res = []
        for picking in pickings:
            shipment_request = {}
            srm = DHLProvider(self.log_xml, request_type="ship", prod_environment=self.prod_environment)
            site_id = self.sudo().dhl_SiteID
            password = self.sudo().dhl_password
            account_number = self.sudo().dhl_account_number
            shipment_request['Request'] = srm._set_request(site_id, password)
            shipment_request['RegionCode'] = srm._set_region_code(self.dhl_region_code)
            shipment_request['RequestedPickupTime'] = srm._set_requested_pickup_time(True)
            shipment_request['Billing'] = srm._set_billing(account_number, "S", self.dhl_duty_payment, self.dhl_dutiable)
            shipment_request['Consignee'] = srm._set_consignee(picking.partner_id)
            shipment_request['Shipper'] = srm._set_shipper(account_number, picking.company_id.partner_id, picking.picking_type_id.warehouse_id.partner_id)
            shipment_request['Reference'] = {
                'ReferenceID': picking.sale_id.name if picking.sale_id else picking.name,
                'ReferenceType': 'CU'
            }
            total_value, currency_name = self._dhl_soap_calculate_value(picking)
            if self.dhl_dutiable:
                incoterm = picking.sale_id.incoterm or self.env.company.incoterm_id
                shipment_request['Dutiable'] = srm._set_dutiable(total_value, currency_name, incoterm)
            if picking._should_generate_commercial_invoice():
                shipment_request['UseDHLInvoice'] = 'Y'
                shipment_request['DHLInvoiceType'] = 'CMI'
                shipment_request['ExportDeclaration'] = srm._set_export_declaration(self, picking)
            shipment_request['ShipmentDetails'] = srm._set_shipment_details(picking)
            shipment_request['LabelImageFormat'] = srm._set_label_image_format(self.dhl_label_image_format)
            shipment_request['Label'] = srm._set_label(self.dhl_label_template)
            shipment_request['schemaVersion'] = 10.0
            shipment_request['LanguageCode'] = 'en'
            if picking.carrier_id.shipping_insurance:
                shipment_request['SpecialService'] = []
                shipment_request['SpecialService'].append(srm._set_insurance(shipment_request['ShipmentDetails']))
            self._dhl_soap_add_custom_data_to_request(shipment_request, 'ship')
            dhl_response = srm._process_shipment(shipment_request)
            traking_number = dhl_response.AirwayBillNumber
            logmessage = Markup(_("Shipment created into DHL <br/> <b>Tracking Number: </b>%s")) % (traking_number)
            dhl_labels = [('%s-%s.%s' % (self._get_delivery_label_prefix(), traking_number, self.dhl_label_image_format), dhl_response.LabelImage[0].OutputImage)]
            dhl_cmi = [('%s-%s.%s' % (self._get_delivery_doc_prefix(), mlabel.DocName, mlabel.DocFormat), mlabel.DocImageVal) for mlabel in dhl_response.LabelImage[0].MultiLabels.MultiLabel] if dhl_response.LabelImage[0].MultiLabels else None
            lognote_pickings = picking
            if picking.sale_id:
                lognote_pickings |= picking.sale_id.picking_ids.filtered(lambda p: p.state not in ('done', 'cancel'))
            for pick in lognote_pickings:
                pick.message_post(body=logmessage, attachments=dhl_labels)
                if dhl_cmi:
                    pick.message_post(body=_("DHL Documents"), attachments=dhl_cmi)
            shipping_data = {
                'exact_price': 0,
                'tracking_number': traking_number,
            }
            rate = self._dhl_soap_rate_shipment_vals(picking=picking)
            shipping_data['exact_price'] = rate['price']
            if self.return_label_on_delivery:
                self.get_return_label(picking)
            res = res + [shipping_data]

        return res

    def fusion_dhl_get_return_label(self, picking, tracking_number=None, origin_date=None):
        shipment_request = {}
        srm = DHLProvider(self.log_xml, request_type="ship", prod_environment=self.prod_environment)
        site_id = self.sudo().dhl_SiteID
        password = self.sudo().dhl_password
        account_number = self.sudo().dhl_account_number
        shipment_request['Request'] = srm._set_request(site_id, password)
        shipment_request['RegionCode'] = srm._set_region_code(self.dhl_region_code)
        shipment_request['RequestedPickupTime'] = srm._set_requested_pickup_time(True)
        shipment_request['Billing'] = srm._set_billing(account_number, "S", "S", self.dhl_dutiable)
        shipment_request['Consignee'] = srm._set_consignee(picking.picking_type_id.warehouse_id.partner_id)
        shipment_request['Shipper'] = srm._set_shipper(account_number, picking.partner_id, picking.partner_id)
        total_value, currency_name = self._dhl_soap_calculate_value(picking)
        if self.dhl_dutiable:
            incoterm = picking.sale_id.incoterm or self.env.company.incoterm_id
            shipment_request['Dutiable'] = srm._set_dutiable(total_value, currency_name, incoterm)
        if picking._should_generate_commercial_invoice():
            shipment_request['UseDHLInvoice'] = 'Y'
            shipment_request['DHLInvoiceType'] = 'CMI'
            shipment_request['ExportDeclaration'] = srm._set_export_declaration(self, picking, is_return=True)
        shipment_request['ShipmentDetails'] = srm._set_shipment_details(picking)
        shipment_request['LabelImageFormat'] = srm._set_label_image_format(self.dhl_label_image_format)
        shipment_request['Label'] = srm._set_label(self.dhl_label_template)
        shipment_request['SpecialService'] = []
        shipment_request['SpecialService'].append(srm._set_return())
        shipment_request['schemaVersion'] = 10.0
        shipment_request['LanguageCode'] = 'en'
        self._dhl_soap_add_custom_data_to_request(shipment_request, 'return')
        dhl_response = srm._process_shipment(shipment_request)
        traking_number = dhl_response.AirwayBillNumber
        logmessage = Markup(_("Shipment created into DHL <br/> <b>Tracking Number: </b>%s")) % (traking_number)
        dhl_labels = [('%s-%s-%s.%s' % (self.get_return_label_prefix(), traking_number, 1, self.dhl_label_image_format), dhl_response.LabelImage[0].OutputImage)]
        dhl_cmi = [('%s-Return-%s.%s' % (self._get_delivery_doc_prefix(), mlabel.DocName, mlabel.DocFormat), mlabel.DocImageVal) for mlabel in dhl_response.LabelImage[0].MultiLabels.MultiLabel] if dhl_response.LabelImage[0].MultiLabels else None
        lognote_pickings = picking.sale_id.picking_ids if picking.sale_id else picking
        for pick in lognote_pickings:
            pick.message_post(body=logmessage, attachments=dhl_labels)
            if dhl_cmi:
                pick.message_post(body=_("DHL Documents"), attachments=dhl_cmi)
        shipping_data = {
            'exact_price': 0,
            'tracking_number': traking_number,
        }
        return shipping_data

    def fusion_dhl_get_tracking_link(self, picking):
        return 'http://www.dhl.com/en/express/tracking.html?AWB=%s' % picking.carrier_tracking_ref

    def fusion_dhl_cancel_shipment(self, picking):
        # Obviously you need a pick up date to delete SHIPMENT by DHL. So you can't do it if you didn't schedule a pick-up.
        picking.message_post(body=_(u"You can't cancel DHL shipping without pickup date."))
        picking.write({'carrier_tracking_ref': '',
                       'carrier_price': 0.0})

    def _dhl_convert_weight(self, weight, unit):
        weight_uom_id = self.env['product.template']._get_weight_uom_id_from_ir_config_parameter()
        if unit == 'L':
            weight = weight_uom_id._compute_quantity(weight, self.env.ref('uom.product_uom_lb'), round=False)
        else:
            weight = weight_uom_id._compute_quantity(weight, self.env.ref('uom.product_uom_kgm'), round=False)
        return float_repr(weight, 3)

    def _dhl_soap_add_custom_data_to_request(self, request, request_type):
        """Adds the custom data to the request (DHL SOAP legacy)."""
        if not self.dhl_custom_data_request:
            return
        try:
            custom_data = const_eval('{%s}' % self.dhl_custom_data_request).get(request_type, {})
        except SyntaxError:
            raise UserError(_('Invalid syntax for DHL custom data.'))

        def extra_data_to_request(request, custom_data):
            """recursive function that adds custom data to the current request."""
            for key, new_value in custom_data.items():
                request[key] = current_value = serialize_object(request.get(key, {})) or None
                if isinstance(current_value, list):
                    for item in current_value:
                        extra_data_to_request(item, new_value)
                elif isinstance(new_value, dict) and isinstance(current_value, dict):
                    extra_data_to_request(current_value, new_value)
                else:
                    request[key] = new_value

        extra_data_to_request(request, custom_data)

    def _dhl_soap_calculate_value(self, picking):
        sale_order = picking.sale_id
        if sale_order:
            total_value = sum(line.price_reduce_taxinc * line.product_uom_qty for line in
                              sale_order.order_line.filtered(
                                  lambda l: l.product_id.type == 'consu' and not l.display_type))
            currency_name = picking.sale_id.currency_id.name
        else:
            total_value = sum([line.product_id.lst_price * line.product_qty for line in picking.move_ids])
            currency_name = picking.company_id.currency_id.name
        return total_value, currency_name

    # ══════════════════════════════════════════════════════════════════════════
    # DHL REST METHODS
    # ══════════════════════════════════════════════════════════════════════════

    def fusion_dhl_rest_rate_shipment(self, order):
        res = self._dhl_rest_rate_shipment_vals(order=order)
        return res

    def _dhl_rest_get_order_packages(self, order):
        self.ensure_one()
        total_weight = order._get_estimated_weight()
        total_weight = self._dhl_rest_convert_weight(total_weight)
        if total_weight == 0.0:
            weight_uom_name = self.env['product.template']._get_weight_uom_name_from_ir_config_parameter()
            raise UserError(_("The package cannot be created because the total weight of the products in the picking is 0.0 %s", weight_uom_name))

        max_weight = self.dhl_default_package_type_id.max_weight or total_weight + 1
        number_of_packages = int(total_weight / max_weight)
        last_package_weight = total_weight % max_weight

        weights = [max_weight] * number_of_packages + ([last_package_weight] if last_package_weight else [])
        return [{
            'weight': weight,
            'dimension': {
                'length': self.dhl_default_package_type_id.packaging_length,
                'width': self.dhl_default_package_type_id.width,
                'height': self.dhl_default_package_type_id.height,
            },
        } for weight in weights]

    def _dhl_rest_get_commodities_from_stock_move_lines(self, move_lines):
        commodities = []

        product_lines = move_lines.filtered(lambda line: line.product_id.type in ['product', 'consu'])
        for product, lines in groupby(product_lines, lambda x: x.product_id):
            unit_quantity = sum(
                line.product_uom_id._compute_quantity(
                    line.quantity,
                    product.uom_id)
                for line in lines)
            rounded_qty = max(1, float_round(unit_quantity, precision_digits=0))
            country_of_origin = lines[0].picking_id.picking_type_id.warehouse_id.partner_id.country_id.code
            unit_price = sum(line.sale_price for line in lines) / rounded_qty
            commodities.append({
                'product_id': product,
                'qty': rounded_qty,
                'monetary_value': unit_price,
                'country_of_origin': country_of_origin
            })

        return commodities

    def _dhl_rest_get_picking_packages(self, picking):
        self.ensure_one()
        packages = []

        if picking.is_return_picking:
            commodities = self._dhl_rest_get_commodities_from_stock_move_lines(picking.move_line_ids)
            weight = picking._get_estimated_weight()
            packages.append({
                'commodities': commodities,
                'weight': weight,
                'dimension': {
                    'length': self.dhl_default_package_type_id.packaging_length,
                    'width': self.dhl_default_package_type_id.width,
                    'height': self.dhl_default_package_type_id.height,
                },
            })
            return packages

        # Create all packages.
        for package in picking.move_line_ids.result_package_id:
            move_lines = picking.move_line_ids.filtered(lambda ml: ml.result_package_id == package)
            commodities = self._dhl_rest_get_commodities_from_stock_move_lines(move_lines)
            packages.append({
                'commodities': commodities,
                'weight': package.shipping_weight or package.weight,
                'dimension': {
                    'length': package.package_type_id.packaging_length,
                    'width': package.package_type_id.width,
                    'height': package.package_type_id.height,
                },
                'name': package.name,
            })

        # Create one package: either everything is in pack or nothing is.
        if picking.weight_bulk:
            commodities = self._dhl_rest_get_commodities_from_stock_move_lines(picking.move_line_ids)
            packages.append({
                'commodities': commodities,
                'weight': picking.weight_bulk,
                'dimension': {
                    'length': self.dhl_default_package_type_id.packaging_length,
                    'width': self.dhl_default_package_type_id.width,
                    'height': self.dhl_default_package_type_id.height,
                },
                'name': 'Bulk Content'
            })
        elif not packages:
            raise UserError(_(
                "The package cannot be created because the total weight of the "
                "products in the picking is 0.0 %s",
                picking.weight_uom_name
            ))
        return packages

    def _convert_to_utc_string(self, datetime_object):
        return datetime_object.astimezone(tz=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S GMT+00:00')

    def _dhl_rest_rate_shipment_vals(self, order=False, picking=False):
        if picking:
            warehouse_partner_id = picking.picking_type_id.warehouse_id.partner_id
            currency_id = picking.sale_id.currency_id or picking.company_id.currency_id
            destination_partner_id = picking.partner_id
            total_value = sum(sml.sale_price for sml in picking.move_line_ids)
            planned_date = picking.scheduled_date + timedelta(hours=1)
        else:
            warehouse_partner_id = order.warehouse_id.partner_id
            currency_id = order.currency_id or order.company_id.currency_id
            total_value = sum(line.price_reduce_taxinc * line.product_uom_qty for line in order.order_line.filtered(lambda l: l.product_id.type in ('consu', 'product') and not l.display_type))
            destination_partner_id = order.partner_shipping_id
            if hasattr(order, 'website_id') and order.website_id:
                planned_date = fields.Datetime.now() + timedelta(hours=1)
            else:
                planned_date = order.date_order + timedelta(hours=1)

        rating_request = {}
        account_number = self.sudo().dhl_account_number
        srm = DHLRestProvider(self)
        check_value = srm._check_required_value(self, destination_partner_id, warehouse_partner_id, order=order, picking=picking)
        if check_value:
            return {'success': False,
                    'price': 0.0,
                    'error_message': check_value,
                    'warning_message': False}
        rating_request['customerDetails'] = {
            'shipperDetails': srm._get_from_vals(warehouse_partner_id),
            'receiverDetails': srm._get_to_vals(destination_partner_id)
        }
        if picking:
            packages = self._dhl_rest_get_picking_packages(picking)
        else:
            packages = self._dhl_rest_get_order_packages(order)
        rating_request['packages'] = srm._get_package_vals(self, packages)
        rating_request['isCustomsDeclarable'] = self.dhl_dutiable
        if self.dhl_dutiable:
            rating_request['monetaryAmount'] = srm._get_dutiable_vals(total_value, currency_id.name)
        rating_request['unitOfMeasurement'] = self.dhl_unit_system
        if planned_date <= fields.Datetime.now():
            raise UserError(_("The planned date for the shipment must be in the future."))
        rating_request['plannedShippingDateAndTime'] = self._convert_to_utc_string(planned_date)
        rating_request['accounts'] = srm._get_billing_vals(account_number, "shipper")
        self._dhl_rest_add_extra_data_to_request(rating_request, 'rate')
        rating_request['productsAndServices'] = [{
            'productCode': self.dhl_product_code,
            'valueAddedServices': [],
        }]
        if self.supports_shipping_insurance and self.shipping_insurance:
            rating_request['productsAndServices'][0]['valueAddedServices'].append(srm._get_insurance_vals(self.shipping_insurance, total_value, currency_id.name))

        response = srm._get_rates(rating_request)

        available_product_code = []
        shipping_charge = False
        products = response['products']
        for product in products:
            charge = [price for price in product['totalPrice'] if price['currencyType'] == 'BILLC']
            global_product_code = product['productCode']
            if global_product_code == self.dhl_product_code and charge:
                shipping_charge = charge[0]['price']
                shipping_currency = charge[0]['priceCurrency']
                break
            else:
                available_product_code.append(global_product_code)
        if shipping_charge:
            if order:
                order_currency = order.currency_id
            else:
                order_currency = picking.sale_id.currency_id or picking.company_id.currency_id
            if shipping_currency is None or order_currency.name == shipping_currency:
                price = float(shipping_charge)
            else:
                quote_currency = self.env['res.currency'].search([('name', '=', shipping_currency)], limit=1)
                price = quote_currency._convert(float(shipping_charge), order_currency, (order or picking).company_id, order.date_order if order else fields.Date.today())
            if self.supports_shipping_insurance and self.shipping_insurance:
                for product in products:
                    services = []
                    for price_breakdown in product['detailedPriceBreakdown']:
                        services.extend([service['serviceCode'] for service in price_breakdown['breakdown'] if 'serviceCode' in service])
                    if 'II' not in services:
                        return {'success': False,
                                'price': 0.0,
                                'error_message': _("Shipment insurance is not available between the origin and destination. You should try with another DHL product, or select a delivery method with no insurance."),
                                'warning_message': False}

            return {'success': True,
                    'price': price,
                    'error_message': False,
                    'warning_message': False}

        if available_product_code:
            return {'success': False,
                    'price': 0.0,
                    'error_message': _(
                        "There is no price available for this shipping, you should rather try with the DHL product %s",
                        available_product_code[0]),
                    'warning_message': False}

    def fusion_dhl_rest_send_shipping(self, pickings):
        res = []
        for picking in pickings:
            shipment_request = {}
            srm = DHLRestProvider(self)
            account_number = self.sudo().dhl_account_number
            planned_date = picking.scheduled_date
            if planned_date <= fields.Datetime.now():
                raise UserError(_("The planned date for the shipment must be in the future."))
            shipment_request['plannedShippingDateAndTime'] = self._convert_to_utc_string(planned_date)
            shipment_request['pickup'] = {'isRequested': True}
            shipment_request['accounts'] = srm._get_billing_vals(account_number, "shipper")
            shipment_request['customerDetails'] = {}
            shipment_request['customerDetails']['receiverDetails'] = srm._get_consignee_vals(picking.partner_id)
            shipment_request['customerDetails']['shipperDetails'] = srm._get_shipper_vals(picking.company_id.partner_id, picking.picking_type_id.warehouse_id.partner_id)
            shipment_request['productCode'] = self.dhl_product_code
            shipment_request['customerReferences'] = [{
                'value': picking.sale_id.name if picking.sale_id else picking.name,
                'typeCode': 'CU'
            }]
            shipment_request['content'] = {}
            shipment_request['content']['description'] = picking.sale_id.name if picking.sale_id else picking.name
            shipment_request['content']['unitOfMeasurement'] = self.dhl_unit_system
            incoterm = picking.sale_id.incoterm or self.env.company.incoterm_id
            shipment_request['content']['incoterm'] = incoterm.code or 'EXW'
            total_value, currency_name = self._dhl_rest_calculate_value(picking)
            shipment_request['content']['isCustomsDeclarable'] = self.dhl_dutiable
            if self.dhl_dutiable:
                shipment_request['content']['declaredValue'] = total_value
                shipment_request['content']['declaredValueCurrency'] = currency_name
            if picking._should_generate_commercial_invoice():
                shipment_request['content']['exportDeclaration'] = srm._get_export_declaration_vals(self, picking)
                shipment_request['content']['declaredValueCurrency'] = currency_name
            shipment_request['content']['packages'] = srm._get_shipment_vals(picking)
            shipment_request['outputImageProperties'] = {}
            shipment_request['outputImageProperties']['imageOptions'] = [{
                'typeCode': 'label',
                'templateName': self.dhl_label_template,
            }]
            if self.supports_shipping_insurance and self.shipping_insurance:
                shipment_request['valueAddedServices'] = [srm._get_insurance_vals(self.shipping_insurance, total_value, currency_name)]
            self._dhl_rest_add_extra_data_to_request(shipment_request, 'ship')
            dhl_response = srm._send_shipment(shipment_request)
            tracking_number = dhl_response['shipmentTrackingNumber']
            logmessage = Markup("%s<br/><b>%s:</b> %s") % (_("Shipment created into DHL"), _("Tracking Number"), tracking_number)
            dhl_labels = [
                (
                    'LabelShipping-DHL-{}.{}'.format(tracking_number, document['imageFormat']),
                    base64.b64decode(document['content'])
                )
                for document in dhl_response['documents'] if document['typeCode'] == 'label'
            ]
            other_documents = [
                (
                    'ShippingDoc-DHL-{}.{}'.format(document['packageReferenceNumber'], document['imageFormat']),
                    base64.b64decode(document['content'])
                )
                for document in dhl_response['documents'] if document['typeCode'] != 'label'
            ]
            lognote_pickings = picking.sale_id.picking_ids if picking.sale_id else picking
            for pick in lognote_pickings:
                pick.message_post(body=logmessage, attachments=dhl_labels)
                if other_documents:
                    pick.message_post(body=_("DHL Documents"), attachments=other_documents)
            shipping_data = {
                'exact_price': 0,
                'tracking_number': tracking_number,
            }
            rate = self._dhl_rest_rate_shipment_vals(picking=picking)
            shipping_data['exact_price'] = rate['price']
            if self.return_label_on_delivery:
                self.get_return_label(picking)
            res = res + [shipping_data]

        return res

    def fusion_dhl_rest_get_return_label(self, picking, tracking_number=None, origin_date=None):
        shipment_request = {}
        srm = DHLRestProvider(self)
        account_number = self.sudo().dhl_account_number
        planned_date = picking.scheduled_date
        if planned_date <= fields.Datetime.now():
            raise UserError(_("The planned date for the shipment must be in the future."))
        shipment_request['plannedShippingDateAndTime'] = self._convert_to_utc_string(planned_date)
        shipment_request['pickup'] = {'isRequested': False}
        shipment_request['accounts'] = srm._get_billing_vals(account_number, "shipper")
        shipment_request['customerDetails'] = {
            'shipperDetails': srm._get_shipper_vals(picking.partner_id, picking.partner_id),
            'receiverDetails': srm._get_consignee_vals(picking.picking_type_id.warehouse_id.partner_id)
        }
        shipment_request['productCode'] = self.dhl_product_code
        shipment_request['content'] = {
            'description': picking.sale_id.name if picking.sale_id else picking.name,
            'unitOfMeasurement': self.dhl_unit_system,
            'incoterm': picking.sale_id.incoterm or self.env.company.incoterm_id.code or 'EXW',
            'isCustomsDeclarable': self.dhl_dutiable,
            'packages': srm._get_shipment_vals(picking)
        }
        total_value, currency_name = self._dhl_rest_calculate_value(picking)
        if self.dhl_dutiable:
            shipment_request['content']['declaredValue'] = total_value
            shipment_request['content']['declaredValueCurrency'] = currency_name
        if picking._should_generate_commercial_invoice():
            shipment_request['content']['exportDeclaration'] = srm._get_export_declaration_vals(self, picking)
            shipment_request['content']['declaredValueCurrency'] = currency_name
        shipment_request['outputImageProperties'] = {
            'imageOptions': [{
                'typeCode': 'label',
                'templateName': self.dhl_label_template,
            }]
        }
        shipment_request['valueAddedServices'] = [{'serviceCode': 'PV'}]
        self._dhl_rest_add_extra_data_to_request(shipment_request, 'return')
        dhl_response = srm._send_shipment(shipment_request)
        tracking_number = dhl_response['shipmentTrackingNumber']
        logmessage = Markup("%s<br/><b>%s:</b> %s") % (_("Shipment created into DHL"), _("Tracking Number"), tracking_number)
        dhl_labels = [
            (
                'LabelReturn-DHL-{}.{}'.format(tracking_number, document['imageFormat']),
                base64.b64decode(document['content'])
            )
            for document in dhl_response['documents'] if document['typeCode'] == 'label'
        ]
        other_documents = [
            (
                'ShippingDoc-DHL-{}.{}'.format(document['packageReferenceNumber'], document['imageFormat']),
                base64.b64decode(document['content'])
            )
            for document in dhl_response['documents'] if document['typeCode'] != 'label'
        ]
        lognote_pickings = picking.sale_id.picking_ids if picking.sale_id else picking
        for pick in lognote_pickings:
            pick.message_post(body=logmessage, attachments=dhl_labels)
            if other_documents:
                pick.message_post(body=_("DHL Documents"), attachments=other_documents)
        shipping_data = {
            'exact_price': 0,
            'tracking_number': tracking_number,
        }
        return shipping_data

    def fusion_dhl_rest_get_tracking_link(self, picking):
        return 'http://www.dhl.com/en/express/tracking.html?AWB=%s' % picking.carrier_tracking_ref

    def fusion_dhl_rest_cancel_shipment(self, picking):
        # Obviously you need a pick up date to delete SHIPMENT by DHL. So you can't do it if you didn't schedule a pick-up.
        picking.message_post(body=_("You can't cancel DHL shipping without pickup date."))
        picking.write({'carrier_tracking_ref': '', 'carrier_price': 0.0})

    def _dhl_rest_convert_weight(self, weight):
        weight_uom_id = self.env['product.template']._get_weight_uom_id_from_ir_config_parameter()
        unit = self.dhl_unit_system
        if unit == 'imperial':
            weight = weight_uom_id._compute_quantity(weight, self.env.ref('uom.product_uom_lb'), round=False)
        else:
            weight = weight_uom_id._compute_quantity(weight, self.env.ref('uom.product_uom_kgm'), round=False)
        # float_round doesn't work here, for example float_round(0.7000000000000001, 3) = 0.7000000000000001
        return json_float_round(weight, 3)

    def _dhl_rest_calculate_value(self, picking):
        sale_order = picking.sale_id
        if sale_order:
            total_value = sum(
                line.price_reduce_taxinc * line.product_uom_qty
                for line in sale_order.order_line
                if not line.display_type
                if line.product_id.type in ['product', 'consu']
            )
            currency_name = picking.sale_id.currency_id.name
        else:
            total_value = sum(line.product_id.lst_price * line.product_qty for line in picking.move_ids)
            currency_name = picking.company_id.currency_id.name
        return total_value, currency_name

    def _dhl_rest_add_extra_data_to_request(self, request, request_type):
        """Adds the extra data to the request (DHL REST)."""
        extra_data_input = {
            'rate': self.dhl_extra_data_rate_request,
            'ship': self.dhl_extra_data_ship_request,
            'return': self.dhl_extra_data_return_request,
        }.get(request_type) or ''
        try:
            extra_data = json.loads('{' + extra_data_input + '}')
        except JSONDecodeError:
            raise UserError(_("Invalid syntax for DHL extra data."))

        def extra_data_to_request(request, extra_data):
            """recursive function that adds extra data to the current request"""
            for key, new_value in extra_data.items():
                current_value = request.get(key)
                if isinstance(current_value, list):
                    for item in current_value:
                        extra_data_to_request(item, new_value)
                elif isinstance(new_value, dict) and isinstance(current_value, dict):
                    extra_data_to_request(current_value, new_value)
                else:
                    request[key] = new_value

        extra_data_to_request(request, extra_data)
