# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import binascii
import io
import PIL.PdfImagePlugin   # activate PDF support in PIL
from PIL import Image
import logging
import os
import re

from odoo.tools.zeep import Client, Plugin
from odoo.tools.zeep.exceptions import Fault
from odoo.tools.zeep.wsdl.utils import etree_to_string

from odoo.tools import _, LazyTranslate
from odoo.tools.float_utils import float_repr
from odoo.exceptions import UserError

_lt = LazyTranslate(__name__)
_logger = logging.getLogger(__name__)
# uncomment to enable logging of SOAP requests and responses
# logging.getLogger('zeep.transports').setLevel(logging.DEBUG)


UPS_ERROR_MAP = {
    '110002': _lt("Please provide at least one item to ship."),
    '110208': _lt("Please set a valid country in the recipient address."),
    '110308': _lt("Please set a valid country in the warehouse address."),
    '110548': _lt("A shipment cannot have a KGS/IN or LBS/CM as its unit of measurements. Configure it from the delivery method."),
    '111057': _lt("This measurement system is not valid for the selected country. Please switch from LBS/IN to KGS/CM (or vice versa). Configure it from the delivery method."),
    '111091': _lt("The selected service is not possible from your warehouse to the recipient address, please choose another service."),
    '111100': _lt("The selected service is invalid from the requested warehouse, please choose another service."),
    '111107': _lt("Please provide a valid zip code in the warehouse address."),
    '111210': _lt("The selected service is invalid to the recipient address, please choose another service."),
    '111212': _lt("Please provide a valid package type available for service and selected locations."),
    '111500': _lt("The selected service is not valid with the selected packaging."),
    '112111': _lt("Please provide a valid shipper number/Carrier Account."),
    '113020': _lt("Please provide a valid zip code in the warehouse address."),
    '113021': _lt("Please provide a valid zip code in the recipient address."),
    '120031': _lt("Exceeds Total Number of allowed pieces per World Wide Express Shipment."),
    '120100': _lt("Please provide a valid shipper number/Carrier Account."),
    '120102': _lt("Please provide a valid street in shipper's address."),
    '120105': _lt("Please provide a valid city in the shipper's address."),
    '120106': _lt("Please provide a valid state in the shipper's address."),
    '120107': _lt("Please provide a valid zip code in the shipper's address."),
    '120108': _lt("Please provide a valid country in the shipper's address."),
    '120109': _lt("Please provide a valid shipper phone number."),
    '120113': _lt("Shipper number must contain alphanumeric characters only."),
    '120114': _lt("Shipper phone extension cannot exceed the length of 4."),
    '120115': _lt("Shipper Phone must be at least 10 alphanumeric characters."),
    '120116': _lt("Shipper phone extension must contain only numbers."),
    '120122': _lt("Please provide a valid shipper Number/Carrier Account."),
    '120124': _lt("The requested service is unavailable between the selected locations."),
    '120202': _lt("Please provide a valid street in the recipient address."),
    '120205': _lt("Please provide a valid city in the recipient address."),
    '120206': _lt("Please provide a valid state in the recipient address."),
    '120207': _lt("Please provide a valid zipcode in the recipient address."),
    '120208': _lt("Please provide a valid Country in recipient's address."),
    '120209': _lt("Please provide a valid phone number for the recipient."),
    '120212': _lt("Recipient PhoneExtension cannot exceed the length of 4."),
    '120213': _lt("Recipient Phone must be at least 10 alphanumeric characters."),
    '120214': _lt("Recipient PhoneExtension must contain only numbers."),
    '120302': _lt("Please provide a valid street in the warehouse address."),
    '120305': _lt("Please provide a valid City in the warehouse address."),
    '120306': _lt("Please provide a valid State in the warehouse address."),
    '120307': _lt("Please provide a valid Zip in the warehouse address."),
    '120308': _lt("Please provide a valid Country in the warehouse address."),
    '120309': _lt("Please provide a valid warehouse Phone Number"),
    '120312': _lt("Warehouse PhoneExtension cannot exceed the length of 4."),
    '120313': _lt("Warehouse Phone must be at least 10 alphanumeric characters."),
    '120314': _lt("Warehouse Phone must contain only numbers."),
    '120412': _lt("Please provide a valid shipper Number/Carrier Account."),
    '121057': _lt("This measurement system is not valid for the selected country. Please switch from LBS/IN to KGS/CM (or vice versa). Configure it from delivery method"),
    '121210': _lt("The requested service is unavailable between the selected locations."),
    '128089': _lt("Access License number is Invalid. Provide a valid number (Length should be 0-35 alphanumeric characters)"),
    '190001': _lt("Cancel shipment not available at this time , Please try again Later."),
    '190100': _lt("Provided Tracking Ref. Number is invalid."),
    '190109': _lt("Provided Tracking Ref. Number is invalid."),
    '250001': _lt("Access License number is invalid for this provider.Please re-license."),
    '250002': _lt("Username/Password is invalid for this delivery provider."),
    '250003': _lt("Access License number is invalid for this delivery provider."),
    '250004': _lt("Username/Password is invalid for this delivery provider."),
    '250006': _lt("The maximum number of user access attempts was exceeded. So please try again later"),
    '250007': _lt("The UserId is currently locked out; please try again in 24 hours."),
    '250009': _lt("Provided Access License Number not found in the UPS database"),
    '250038': _lt("Please provide a valid shipper number/Carrier Account."),
    '250047': _lt("Access License number is revoked contact UPS to get access."),
    '250052': _lt("Authorization system is currently unavailable , try again later."),
    '250053': _lt("UPS Server Not Found"),
    '9120200': _lt("Please provide at least one item to ship")
}


class LogPlugin(Plugin):
    """ Small plugin for zeep that catches out/ingoing XML requests and logs them"""
    def __init__(self, debug_logger):
        self.debug_logger = debug_logger

    def egress(self, envelope, http_headers, operation, binding_options):
        self.debug_logger(etree_to_string(envelope).decode(), 'ups_request')
        return envelope, http_headers

    def ingress(self, envelope, http_headers, operation):
        self.debug_logger(etree_to_string(envelope).decode(), 'ups_response')
        return envelope, http_headers


class FixRequestNamespacePlug(Plugin):
    def __init__(self, root):
        self.root = root

    def marshalled(self, context):
        context.envelope = context.envelope.prune()


class UPSRequest():
    def __init__(self, debug_logger, username, password, shipper_number, access_number, prod_environment):
        self.debug_logger = debug_logger
        # Product and Testing url
        self.endurl = "https://onlinetools.ups.com/webservices/"
        if not prod_environment:
            self.endurl = "https://wwwcie.ups.com/webservices/"

        # Basic detail require to authenticate
        self.username = username
        self.password = password
        self.shipper_number = shipper_number
        self.access_number = access_number

        self.rate_wsdl = '../wsdl/RateWS.wsdl'
        self.ship_wsdl = '../wsdl/Ship.wsdl'
        self.void_wsdl = '../wsdl/Void.wsdl'
        self.ns = {'err': "http://www.ups.com/XMLSchema/XOLTWS/Error/v1.1"}

    def _add_security_header(self, client, api):
        # set the detail which require to authenticate
        user_token = {'Username': self.username, 'Password': self.password}
        access_token = {'AccessLicenseNumber': self.access_number}
        security = client._Client__obj.get_element('ns0:UPSSecurity')(UsernameToken=user_token, ServiceAccessToken=access_token)
        client._Client__obj.set_default_soapheaders([security])

    def _set_service(self, client, api):
        service = client.create_service(
            next(iter(client._Client__obj.wsdl.bindings)),
            '%s%s' % (self.endurl, api))
        return service

    def _set_client(self, wsdl, api, root):
        wsdl_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), wsdl)
        client = Client(wsdl_path, plugins=[FixRequestNamespacePlug(root), LogPlugin(self.debug_logger)])
        self.factory_ns2 = client.type_factory('ns2')
        self.factory_ns3 = client.type_factory('ns3')
        # ns4 only exists for Ship API - we only use it for the invoice
        self.factory_ns4 = client.type_factory('ns4') if api == 'Ship' else self.factory_ns3
        self._add_security_header(client, api)
        return client

    def _clean_phone_number(self, phone):
        return re.sub('[^0-9]','', phone)

    def check_required_value(self, shipper, ship_from, ship_to, order=False, picking=False):
        required_field = {'city': 'City', 'country_id': 'Country', 'phone': 'Phone'}
        # Check required field for shipper
        res = [required_field[field] for field in required_field if not shipper[field]]
        if shipper.country_id.code in ('US', 'CA', 'IE') and not shipper.state_id.code:
            res.append('State')
        if not shipper.street and not shipper.street2:
            res.append('Street')
        if shipper.country_id.code != 'HK' and not shipper.zip:
            res.append('ZIP code')
        if res:
            return _("The address of your company is missing or wrong.\n(Missing field(s) : %s)", ",".join(res))
        if len(self._clean_phone_number(shipper.phone)) < 10:
            return str(UPS_ERROR_MAP.get('120115'))
        # Check required field for warehouse address
        res = [required_field[field] for field in required_field if not ship_from[field]]
        if ship_from.country_id.code in ('US', 'CA', 'IE') and not ship_from.state_id.code:
            res.append('State')
        if not ship_from.street and not ship_from.street2:
            res.append('Street')
        if ship_from.country_id.code != 'HK' and not ship_from.zip:
            res.append('ZIP code')
        if res:
            return _("The address of your warehouse is missing or wrong.\n(Missing field(s) : %s)", ",".join(res))
        if len(self._clean_phone_number(ship_from.phone)) < 10:
            return str(UPS_ERROR_MAP.get('120313'))
        # Check required field for recipient address
        res = [required_field[field] for field in required_field if field != 'phone' and not ship_to[field]]
        if ship_to.country_id.code in ('US', 'CA', 'IE') and not ship_to.state_id.code:
            res.append('State')
        # The street isn't required if we compute the rate with a partial delivery address in the
        # express checkout flow.
        if not ship_to.street and not ship_to.street2 and not ship_to.env.context.get(
            'express_checkout_partial_delivery_address', False
        ):
            res.append('Street')
        if ship_to.country_id.code != 'HK' and not ship_to.zip:
            res.append('ZIP code')
        if len(ship_to.street or '') > 35 or len(ship_to.street2 or '') > 35:
            return _("UPS address lines can only contain a maximum of 35 characters. You can split the contacts addresses on multiple lines to try to avoid this limitation.")
        if picking and not order:
            order = picking.sale_id
        phone = ship_to.phone
        if order and not phone:
            phone = order.partner_id.phone
        if order:
            if not order.order_line:
                return _("Please provide at least one item to ship.")
            error_lines = order.order_line._get_invalid_delivery_weight_lines()
            if error_lines:
                return _("The estimated shipping price cannot be computed because the weight is missing for the following product(s): \n %s", ", ".join(error_lines.product_id.mapped('name')))
        if picking:
            if not all(ml.result_package_id or ml.product_id.weight for ml in picking.move_line_ids):
                return _("The delivery cannot be done because the weight of your product is missing.")
            packages_without_weight = picking.move_line_ids.mapped('result_package_id').filtered(lambda p: not p.shipping_weight)
            if packages_without_weight:
                return _('Packages %s do not have a positive shipping weight.', ', '.join(packages_without_weight.mapped('display_name')))
        # The phone isn't required if we compute the rate with a partial delivery address in the
        # express checkout flow.
        if not phone and not ship_to.env.context.get(
            'express_checkout_partial_delivery_address', False
        ):
            res.append('Phone')
        if res:
            return _("The recipient address is missing or wrong.\n(Missing field(s) : %s)", ",".join(res))
        # The phone isn't required if we compute the rate with a partial delivery address in the
        # express checkout flow.
        if not ship_to.env.context.get(
            'express_checkout_partial_delivery_address', False
        ) and len(self._clean_phone_number(phone)) < 10:
            return str(UPS_ERROR_MAP.get('120213'))
        return False

    def get_error_message(self, error_code, description):
        result = {}
        result['error_message'] = str(UPS_ERROR_MAP.get(error_code))
        if result['error_message'] == "None":
            result['error_message'] = description
        return result

    def save_label(self, image64, label_file_type='GIF'):
        img_decoded = base64.decodebytes(image64.encode('utf-8'))
        if label_file_type == 'GIF':
            # Label format is GIF, so need to rotate and convert as PDF
            image_string = io.BytesIO(img_decoded)
            im = Image.open(image_string)
            label_result = io.BytesIO()
            im.save(label_result, 'pdf')
            return label_result.getvalue()
        else:
            return img_decoded

    def set_package_detail(self, carrier, client, packages, ship_from, ship_to, cod_info, request_type):
        Packages = []
        if request_type == "rating":
            MeasurementType = self.factory_ns2.CodeDescriptionType
        elif request_type == "shipping":
            MeasurementType = self.factory_ns2.ShipUnitOfMeasurementType
        for p in packages:
            package = self.factory_ns2.PackageType()
            if hasattr(package, 'Packaging'):
                package.Packaging = self.factory_ns2.PackagingType()
                package.Packaging.Code = p.packaging_type or ''
            elif hasattr(package, 'PackagingType'):
                package.PackagingType = self.factory_ns2.CodeDescriptionType()
                package.PackagingType.Code = p.packaging_type or ''

            package.Dimensions = self.factory_ns2.DimensionsType()
            package.Dimensions.UnitOfMeasurement = MeasurementType()
            package.Dimensions.UnitOfMeasurement.Code = carrier.ups_package_dimension_unit
            package.Dimensions.Length = p.dimension['length']
            package.Dimensions.Width = p.dimension['width']
            package.Dimensions.Height = p.dimension['height']

            package.PackageServiceOptions = self.factory_ns2.PackageServiceOptionsType()
            if cod_info:
                package.PackageServiceOptions.COD = self.factory_ns2.CODType()
                package.PackageServiceOptions.COD.CODFundsCode = str(cod_info['funds_code'])
                package.PackageServiceOptions.COD.CODAmount = self.factory_ns2.CODAmountType() if request_type == 'rating' else self.factory_ns2.CurrencyMonetaryType()
                package.PackageServiceOptions.COD.CODAmount.MonetaryValue = cod_info['monetary_value']
                package.PackageServiceOptions.COD.CODAmount.CurrencyCode = cod_info['currency']

            if p.currency_id:
                package.PackageServiceOptions.DeclaredValue = self.factory_ns2.InsuredValueType() if request_type == 'rating' else self.factory_ns2.PackageDeclaredValueType()
                package.PackageServiceOptions.DeclaredValue.CurrencyCode = p.currency_id.name
                package.PackageServiceOptions.DeclaredValue.MonetaryValue = float_repr(p.total_cost * carrier.shipping_insurance / 100, 2)
                if request_type == "shipping":
                    package.PackageServiceOptions.DeclaredValue.Type = self.factory_ns2.DeclaredValueType()
                    package.PackageServiceOptions.DeclaredValue.Type.Code = '01'  # EVS

            package.PackageWeight = self.factory_ns2.PackageWeightType()
            package.PackageWeight.UnitOfMeasurement = MeasurementType()
            package.PackageWeight.UnitOfMeasurement.Code = carrier.ups_package_weight_unit
            package.PackageWeight.Weight = carrier._ups_convert_weight(p.weight, carrier.ups_package_weight_unit)

            # Package and shipment reference text is only allowed for shipments within
            # the USA and within Puerto Rico. This is a UPS limitation.
            if (p.name and not ' ' in p.name and ship_from.country_id.code in ('US') and ship_to.country_id.code in ('US')):
                reference_number = self.factory_ns2.ReferenceNumberType()
                reference_number.Code = 'PM'
                reference_number.Value = p.name
                reference_number.BarCodeIndicator = p.name
                package.ReferenceNumber = reference_number

            Packages.append(package)
        return Packages

    def set_invoice(self, shipment_info, commodities, ship_to):

        invoice_products = []
        for commodity in commodities:
            uom_type = self.factory_ns4.UnitOfMeasurementType()
            uom_type.Code = 'PC' if commodity.qty == 1 else 'PCS'

            unit_type = self.factory_ns4.UnitType()
            unit_type.Number = int(commodity.qty)
            unit_type.Value = float_repr(commodity.monetary_value, 2)
            unit_type.UnitOfMeasurement = uom_type

            product = self.factory_ns4.ProductType()
            # split the name of the product to maximum 3 substrings of length 35
            name = commodity.product_id.name
            product.Description = [line for line in [name[35 * i:35 * (i + 1)] for i in range(3)] if line]
            product.Unit = unit_type
            product.OriginCountryCode = commodity.country_of_origin
            product.CommodityCode = commodity.product_id.hs_code or ''

            invoice_products.append(product)

        address_sold_to = self.factory_ns4.AddressType()
        address_sold_to.AddressLine = [line for line in (ship_to.street, ship_to.street2) if line]
        address_sold_to.City = ship_to.city or ''
        address_sold_to.PostalCode = ship_to.zip or ''
        address_sold_to.CountryCode = ship_to.country_id.code or ''
        if ship_to.country_id.code in ('US', 'CA', 'IE'):
            address_sold_to.StateProvinceCode = ship_to.state_id.code or ''

        sold_to = self.factory_ns4.SoldToType()
        if len(ship_to.commercial_partner_id.name) > 35:
            raise UserError(_('The name of the customer should be no more than 35 characters.'))
        sold_to.Name = ship_to.commercial_partner_id.name
        sold_to.AttentionName = ship_to.name
        sold_to.Address = address_sold_to

        contact = self.factory_ns4.ContactType()
        contact.SoldTo = sold_to

        invoice = self.factory_ns4.InternationalFormType()
        invoice.FormType = '01'  # Invoice
        invoice.Product = invoice_products
        invoice.CurrencyCode = shipment_info.get('itl_currency_code')
        invoice.InvoiceDate = shipment_info.get('invoice_date')
        invoice.ReasonForExport = 'RETURN' if shipment_info.get('is_return', False) else 'SALE'
        invoice.Contacts = contact
        return invoice

    def get_shipping_price(self, carrier, shipment_info, packages, shipper, ship_from, ship_to, service_type, saturday_delivery, cod_info):
        client = self._set_client(self.rate_wsdl, 'Rate', 'RateRequest')
        service = self._set_service(client, 'Rate')
        request = self.factory_ns3.RequestType()
        request.RequestOption = 'Rate'

        classification = self.factory_ns2.CodeDescriptionType()
        classification.Code = '00'  # Get rates for the shipper account
        classification.Description = 'Get rates for the shipper account'

        request_type = "rating"
        shipment = self.factory_ns2.ShipmentType()

        for package in self.set_package_detail(carrier, client, packages, ship_from, ship_to, cod_info, request_type):
            shipment.Package.append(package)

        shipment.Shipper = self.factory_ns2.ShipperType()
        shipment.Shipper.Name = shipper.name or ''
        shipment.Shipper.Address = self.factory_ns2.AddressType()
        shipment.Shipper.Address.AddressLine = [shipper.street or '', shipper.street2 or '']
        shipment.Shipper.Address.City = shipper.city or ''
        shipment.Shipper.Address.PostalCode = shipper.zip or ''
        shipment.Shipper.Address.CountryCode = shipper.country_id.code or ''
        if shipper.country_id.code in ('US', 'CA', 'IE'):
            shipment.Shipper.Address.StateProvinceCode = shipper.state_id.code or ''
        shipment.Shipper.ShipperNumber = self.shipper_number or ''
        # shipment.Shipper.Phone.Number = shipper.phone or ''

        shipment.ShipFrom = self.factory_ns2.ShipFromType()
        shipment.ShipFrom.Name = ship_from.name or ''
        shipment.ShipFrom.Address = self.factory_ns2.AddressType()
        shipment.ShipFrom.Address.AddressLine = [ship_from.street or '', ship_from.street2 or '']
        shipment.ShipFrom.Address.City = ship_from.city or ''
        shipment.ShipFrom.Address.PostalCode = ship_from.zip or ''
        shipment.ShipFrom.Address.CountryCode = ship_from.country_id.code or ''
        if ship_from.country_id.code in ('US', 'CA', 'IE'):
            shipment.ShipFrom.Address.StateProvinceCode = ship_from.state_id.code or ''
        # shipment.ShipFrom.Phone.Number = ship_from.phone or ''

        shipment.ShipTo = self.factory_ns2.ShipToType()
        shipment.ShipTo.Name = ship_to.name or ''
        shipment.ShipTo.Address = self.factory_ns2.AddressType()
        shipment.ShipTo.Address.AddressLine = [ship_to.street or '', ship_to.street2 or '']
        shipment.ShipTo.Address.City = ship_to.city or ''
        shipment.ShipTo.Address.PostalCode = ship_to.zip or ''
        shipment.ShipTo.Address.CountryCode = ship_to.country_id.code or ''
        if ship_to.country_id.code in ('US', 'CA', 'IE'):
            shipment.ShipTo.Address.StateProvinceCode = ship_to.state_id.code or ''
        # shipment.ShipTo.Phone.Number = ship_to.phone or ''
        if not ship_to.commercial_partner_id.is_company:
            shipment.ShipTo.Address.ResidentialAddressIndicator = None

        shipment.Service = self.factory_ns2.CodeDescriptionType()
        shipment.Service.Code = service_type or ''
        shipment.Service.Description = 'Service Code'
        if service_type == "96":
            shipment.NumOfPieces = int(shipment_info.get('total_qty'))

        if saturday_delivery:
            shipment.ShipmentServiceOptions = self.factory_ns2.ShipmentServiceOptionsType()
            shipment.ShipmentServiceOptions.SaturdayDeliveryIndicator = saturday_delivery
        else:
            shipment.ShipmentServiceOptions = ''

        shipment.ShipmentRatingOptions = self.factory_ns2.ShipmentRatingOptionsType()
        shipment.ShipmentRatingOptions.NegotiatedRatesIndicator = 1

        try:
            # Get rate using for provided detail
            response = service.ProcessRate(Request=request, CustomerClassification=classification, Shipment=shipment)

            # Check if ProcessRate is not success then return reason for that
            if response.Response.ResponseStatus.Code != "1":
                return self.get_error_message(response.Response.ResponseStatus.Code, response.Response.ResponseStatus.Description)

            rate = response.RatedShipment[0]
            charge = rate.TotalCharges

            # Some users are qualified to receive negotiated rates
            if 'NegotiatedRateCharges' in rate and rate.NegotiatedRateCharges and rate.NegotiatedRateCharges.TotalCharge.MonetaryValue:
                charge = rate.NegotiatedRateCharges.TotalCharge

            return {
                'currency_code': charge.CurrencyCode,
                'price': charge.MonetaryValue,
            }

        except Fault as e:
            code = e.detail.xpath("//err:PrimaryErrorCode/err:Code", namespaces=self.ns)[0].text
            description = e.detail.xpath("//err:PrimaryErrorCode/err:Description", namespaces=self.ns)[0].text
            return self.get_error_message(code, description)
        except IOError as e:
            return self.get_error_message('0', 'UPS Server Not Found:\n%s' % e)

    def send_shipping(self, carrier, shipment_info, packages, shipper, ship_from, ship_to, service_type, saturday_delivery, duty_payment, cod_info=None, label_file_type='GIF', ups_carrier_account=False):
        client = self._set_client(self.ship_wsdl, 'Ship', 'ShipmentRequest')
        request = self.factory_ns3.RequestType()
        request.RequestOption = 'nonvalidate'

        request_type = "shipping"
        label = self.factory_ns2.LabelSpecificationType()
        label.LabelImageFormat = self.factory_ns2.LabelImageFormatType()
        label.LabelImageFormat.Code = label_file_type
        label.LabelImageFormat.Description = label_file_type
        if label_file_type != 'GIF':
            label.LabelStockSize = self.factory_ns2.LabelStockSizeType()
            label.LabelStockSize.Height = '6'
            label.LabelStockSize.Width = '4'

        shipment = self.factory_ns2.ShipmentType()
        shipment.Description = shipment_info.get('description')

        for package in self.set_package_detail(carrier, client, packages, ship_from, ship_to, cod_info, request_type):
            shipment.Package.append(package)

        shipment.Shipper = self.factory_ns2.ShipperType()
        shipment.Shipper.Address = self.factory_ns2.ShipAddressType()
        shipment.Shipper.AttentionName = (shipper.name or '')[:35]
        shipment.Shipper.Name = (shipper.parent_id.name or shipper.name or '')[:35]
        shipment.Shipper.Address.AddressLine = [l for l in [shipper.street or '', shipper.street2 or ''] if l]
        shipment.Shipper.Address.City = shipper.city or ''
        shipment.Shipper.Address.PostalCode = shipper.zip or ''
        shipment.Shipper.Address.CountryCode = shipper.country_id.code or ''
        if shipper.country_id.code in ('US', 'CA', 'IE'):
            shipment.Shipper.Address.StateProvinceCode = shipper.state_id.code or ''
        shipment.Shipper.ShipperNumber = self.shipper_number or ''
        shipment.Shipper.Phone = self.factory_ns2.ShipPhoneType()
        shipment.Shipper.Phone.Number = self._clean_phone_number(shipper.phone)
        shipment.Shipper.EMailAddress = shipper.email or ''

        shipment.ShipFrom = self.factory_ns2.ShipFromType()
        shipment.ShipFrom.Address = self.factory_ns2.ShipAddressType()
        shipment.ShipFrom.AttentionName = (ship_from.name or '')[:35]
        shipment.ShipFrom.Name = (ship_from.parent_id.name or ship_from.name or '')[:35]
        shipment.ShipFrom.Address.AddressLine = [l for l in [ship_from.street or '', ship_from.street2 or ''] if l]
        shipment.ShipFrom.Address.City = ship_from.city or ''
        shipment.ShipFrom.Address.PostalCode = ship_from.zip or ''
        shipment.ShipFrom.Address.CountryCode = ship_from.country_id.code or ''
        if ship_from.country_id.code in ('US', 'CA', 'IE'):
            shipment.ShipFrom.Address.StateProvinceCode = ship_from.state_id.code or ''
        shipment.ShipFrom.Phone = self.factory_ns2.ShipPhoneType()
        shipment.ShipFrom.Phone.Number = self._clean_phone_number(ship_from.phone)
        shipment.ShipFrom.EMailAddress = ship_from.email or ''

        shipment.ShipTo = self.factory_ns2.ShipToType()
        shipment.ShipTo.Address = self.factory_ns2.ShipToAddressType()
        shipment.ShipTo.AttentionName = (ship_to.name or '')[:35]
        shipment.ShipTo.Name = (ship_to.parent_id.name or ship_to.name or '')[:35]
        shipment.ShipTo.Address.AddressLine = [l for l in [ship_to.street or '', ship_to.street2 or ''] if l]
        shipment.ShipTo.Address.City = ship_to.city or ''
        shipment.ShipTo.Address.PostalCode = ship_to.zip or ''
        shipment.ShipTo.Address.CountryCode = ship_to.country_id.code or ''
        if ship_to.country_id.code in ('US', 'CA', 'IE'):
            shipment.ShipTo.Address.StateProvinceCode = ship_to.state_id.code or ''
        shipment.ShipTo.Phone = self.factory_ns2.ShipPhoneType()
        shipment.ShipTo.Phone.Number = self._clean_phone_number(shipment_info['phone'])
        shipment.ShipTo.EMailAddress = ship_to.email or ''
        if not ship_to.commercial_partner_id.is_company:
            shipment.ShipTo.Address.ResidentialAddressIndicator = None

        shipment.Service = self.factory_ns2.ServiceType()
        shipment.Service.Code = service_type or ''
        shipment.Service.Description = 'Service Code'
        if service_type == "96":
            shipment.NumOfPiecesInShipment = int(shipment_info.get('total_qty'))
        shipment.ShipmentRatingOptions = self.factory_ns2.RateInfoType()
        shipment.ShipmentRatingOptions.NegotiatedRatesIndicator = 1

        # Shipments from US to CA or PR require extra info
        if ship_from.country_id.code == 'US' and ship_to.country_id.code in ['CA', 'PR']:
            shipment.InvoiceLineTotal = self.factory_ns2.CurrencyMonetaryType()
            shipment.InvoiceLineTotal.CurrencyCode = shipment_info.get('itl_currency_code')
            shipment.InvoiceLineTotal.MonetaryValue = shipment_info.get('ilt_monetary_value')

        # set the default method for payment using shipper account
        payment_info = self.factory_ns2.PaymentInfoType()
        shipcharge = self.factory_ns2.ShipmentChargeType()
        shipcharge.Type = '01'

        # Bill Recevier 'Bill My Account'
        if ups_carrier_account:
            shipcharge.BillReceiver = self.factory_ns2.BillReceiverType()
            shipcharge.BillReceiver.Address = self.factory_ns2.BillReceiverAddressType()
            shipcharge.BillReceiver.AccountNumber = ups_carrier_account
            shipcharge.BillReceiver.Address.PostalCode = ship_to.zip
        else:
            shipcharge.BillShipper = self.factory_ns2.BillShipperType()
            shipcharge.BillShipper.AccountNumber = self.shipper_number or ''

        payment_info.ShipmentCharge = [shipcharge]

        if duty_payment == 'SENDER':
            duty_charge = self.factory_ns2.ShipmentChargeType()
            duty_charge.Type = '02'
            duty_charge.BillShipper = self.factory_ns2.BillShipperType()
            duty_charge.BillShipper.AccountNumber = self.shipper_number or ''
            payment_info.ShipmentCharge.append(duty_charge)

        shipment.PaymentInformation = payment_info

        sso = self.factory_ns2.ShipmentServiceOptionsType()
        if shipment_info.get('require_invoice'):
            sso.InternationalForms = self.set_invoice(shipment_info, [c for pkg in packages for c in pkg.commodities], ship_to)
            sso.InternationalForms.TermsOfShipment = shipment_info.get('terms_of_shipment')
            sso.InternationalForms.PurchaseOrderNumber = shipment_info.get('purchase_order_number')
        if saturday_delivery:
            sso.SaturdayDeliveryIndicator = saturday_delivery
        shipment.ShipmentServiceOptions = sso

        self.shipment = shipment
        self.label = label
        self.request = request
        self.label_file_type = label_file_type

    def return_label(self):
        return_service = self.factory_ns2.ReturnServiceType()
        return_service.Code = "9"
        self.shipment.ReturnService = return_service
        for p in self.shipment.Package:
            p.Description = "Return of courtesy"


    def process_shipment(self):
        client = self._set_client(self.ship_wsdl, 'Ship', 'ShipmentRequest')
        service = self._set_service(client, 'Ship')
        try:
            response = service.ProcessShipment(
                Request=self.request, Shipment=self.shipment,
                LabelSpecification=self.label)

            # Check if shipment is not success then return reason for that
            if response.Response.ResponseStatus.Code != "1":
                return self.get_error_message(response.Response.ResponseStatus.Code, response.Response.ResponseStatus.Description)

            result = {}
            result['label_binary_data'] = {}
            for package in response.ShipmentResults.PackageResults:
                result['label_binary_data'][package.TrackingNumber] = self.save_label(package.ShippingLabel.GraphicImage, label_file_type=self.label_file_type)
            if response.ShipmentResults.Form:
                result['invoice_binary_data'] = self.save_label(response.ShipmentResults.Form.Image.GraphicImage, label_file_type='pdf')  # only pdf supported currently
            result['tracking_ref'] = response.ShipmentResults.ShipmentIdentificationNumber
            result['currency_code'] = response.ShipmentResults.ShipmentCharges.TotalCharges.CurrencyCode

            # Some users are qualified to receive negotiated rates
            negotiated_rate = 'NegotiatedRateCharges' in response.ShipmentResults and response.ShipmentResults.NegotiatedRateCharges and response.ShipmentResults.NegotiatedRateCharges.TotalCharge.MonetaryValue or None

            result['price'] = negotiated_rate or response.ShipmentResults.ShipmentCharges.TotalCharges.MonetaryValue
            return result

        except Fault as e:
            code = e.detail.xpath("//err:PrimaryErrorCode/err:Code", namespaces=self.ns)[0].text
            description = e.detail.xpath("//err:PrimaryErrorCode/err:Description", namespaces=self.ns)[0].text
            return self.get_error_message(code, description)
        except IOError as e:
            return self.get_error_message('0', 'UPS Server Not Found:\n%s' % e)

    def cancel_shipment(self, tracking_number):
        client = self._set_client(self.void_wsdl, 'Void', 'VoidShipmentRequest')
        service = self._set_service(client, 'Void')

        request = self.factory_ns3.RequestType()
        request.TransactionReference = self.factory_ns3.TransactionReferenceType()
        request.TransactionReference.CustomerContext = "Cancle shipment"
        voidshipment = {'ShipmentIdentificationNumber': tracking_number or ''}

        result = {}
        try:
            response = service.ProcessVoid(
                Request=request, VoidShipment=voidshipment
            )
            if response.Response.ResponseStatus.Code == "1":
                return result
            return self.get_error_message(response.Response.ResponseStatus.Code, response.Response.ResponseStatus.Description)

        except Fault as e:
            code = e.detail.xpath("//err:PrimaryErrorCode/err:Code", namespaces=self.ns)[0].text
            description = e.detail.xpath("//err:PrimaryErrorCode/err:Description", namespaces=self.ns)[0].text
            return self.get_error_message(code, description)
        except IOError as e:
            return self.get_error_message('0', 'UPS Server Not Found:\n%s' % e)
