forked from Mapan/odoo17e
574 lines
27 KiB
Python
574 lines
27 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
import json
|
|
from json import JSONDecodeError
|
|
|
|
import requests
|
|
from requests import RequestException
|
|
|
|
from odoo import _
|
|
from odoo.exceptions import ValidationError, UserError
|
|
from odoo.tools import float_repr
|
|
|
|
TEST_BASE_URL = "https://apis-sandbox.fedex.com"
|
|
PROD_BASE_URL = "https://apis.fedex.com"
|
|
|
|
# Why using standardized ISO codes? It's way more fun to use made up codes...
|
|
# https://developer.fedex.com/api/en-us/guides/api-reference.html#currencycodes
|
|
FEDEX_CURR_MATCH = {
|
|
'XCD': 'ECD',
|
|
'MXN': 'NMP',
|
|
'KYD': 'CID',
|
|
'CHF': 'SFR',
|
|
'DOP': 'RDD',
|
|
'JPY': 'JYE',
|
|
'KRW': 'WON',
|
|
'SGD': 'SID',
|
|
'CLP': 'CHP',
|
|
'JMD': 'JAD',
|
|
'KWD': 'KUD',
|
|
'AED': 'DHS',
|
|
'TWD': 'NTD',
|
|
'ARS': 'ARN',
|
|
'VES': 'VEF',
|
|
# 'LVL': 'EUR',
|
|
# 'UYU': 'UYP',
|
|
# 'GBP': 'UKL',
|
|
# 'IDR': 'RPA',
|
|
}
|
|
|
|
|
|
class FedexRequest:
|
|
def __init__(self, carrier):
|
|
super_carrier = carrier.sudo()
|
|
self.base_url = PROD_BASE_URL if super_carrier.prod_environment else TEST_BASE_URL
|
|
self.access_token = super_carrier.fedex_rest_access_token
|
|
self.client_id = super_carrier.fedex_rest_developer_key
|
|
self.client_secret = super_carrier.fedex_rest_developer_password
|
|
self.account_number = super_carrier.fedex_rest_account_number
|
|
self.weight_units = super_carrier.fedex_rest_weight_unit
|
|
self.vat_override = super_carrier.fedex_rest_override_shipper_vat
|
|
self.email_notifications = super_carrier.fedex_rest_email_notifications
|
|
self.documentation_type = super_carrier.fedex_rest_documentation_type
|
|
self.insurance = super_carrier.shipping_insurance
|
|
self.check_residential = super_carrier.fedex_rest_residential_address
|
|
self.dropoff_type = super_carrier.fedex_rest_droppoff_type
|
|
self.service_type = super_carrier.fedex_rest_service_type
|
|
self.label_stock = super_carrier.fedex_rest_label_stock_type
|
|
self.label_file = super_carrier.fedex_rest_label_file_type
|
|
self.duty_payment = super_carrier.fedex_rest_duty_payment
|
|
self.make_return = super_carrier.return_label_on_delivery
|
|
self.debug_logger = super_carrier.log_xml
|
|
self.carrier = super_carrier
|
|
self.session = requests.Session()
|
|
|
|
def _send_fedex_request(self, url, data, method='POST'):
|
|
new_token = False
|
|
if not self.access_token:
|
|
self.access_token = self._get_new_access_token()
|
|
self.carrier.fedex_rest_access_token = self.access_token
|
|
new_token = True
|
|
|
|
def _request_call():
|
|
try:
|
|
response = self.session.request(method, self.base_url + url, json=data, headers={
|
|
'Content-Type': "application/json",
|
|
'Authorization': "Bearer " + self.access_token
|
|
}, timeout=15
|
|
)
|
|
self.debug_logger("%s %s\n%s\n\n%s" % (
|
|
response.request.method,
|
|
response.request.url,
|
|
'\n'.join([f'{k}: {v}' for k, v in response.request.headers.items()]),
|
|
response.request.body.decode('utf-8')
|
|
), 'fedex_rest_request')
|
|
self.debug_logger("%s %s\n%s\n\n%s" % (
|
|
response.status_code,
|
|
response.reason,
|
|
'\n'.join([f'{k}: {v}' for k, v in response.headers.items()]),
|
|
response.text
|
|
), 'fedex_rest_response')
|
|
except RequestException:
|
|
raise ValidationError(_('Something went wrong, please try again later!!')) from None
|
|
return response
|
|
|
|
res = _request_call()
|
|
if res.status_code == 401 and not new_token:
|
|
self.access_token = self._get_new_access_token()
|
|
self.carrier.fedex_rest_access_token = self.access_token
|
|
res = _request_call()
|
|
|
|
try:
|
|
response_data = res.json()
|
|
except JSONDecodeError:
|
|
raise ValidationError(_('Could not decode response')) from None
|
|
if not res.ok:
|
|
raise ValidationError(self._process_errors(response_data))
|
|
if 'output' not in response_data:
|
|
raise ValidationError(_('Could not decode response'))
|
|
|
|
return response_data['output']
|
|
|
|
def _process_errors(self, res_body):
|
|
err_msgs = []
|
|
for err in res_body.get('errors', []):
|
|
err_msgs.append(f"{err['message']} ({err['code']})")
|
|
return ','.join(err_msgs)
|
|
|
|
def _process_alerts(self, response):
|
|
messages = []
|
|
alerts = response.get('alerts', [])
|
|
if 'rateReplyDetails' in response:
|
|
alerts += response['rateReplyDetails'][0].get('customerMessages', [])
|
|
for alert in alerts:
|
|
messages.append(f"{alert['message']} ({alert['code']})")
|
|
|
|
return '\n'.join(messages)
|
|
|
|
def _get_new_access_token(self):
|
|
if not self.client_id or not self.client_secret:
|
|
raise ValidationError(_('You must setup a client ID and secret on the carrier first'))
|
|
try:
|
|
response = self.session.post(
|
|
self.base_url + "/oauth/token",
|
|
f"grant_type=client_credentials&client_id={self.client_id}&client_secret={self.client_secret}",
|
|
headers={'Content-Type': "application/x-www-form-urlencoded"},
|
|
timeout=15
|
|
)
|
|
response_data = response.json()
|
|
except RequestException:
|
|
raise ValidationError(_('Something went wrong, please try again later!!')) from None
|
|
except JSONDecodeError:
|
|
raise ValidationError(_('Could not decode response')) from None
|
|
if not response.ok:
|
|
raise ValidationError(self._process_errors(response_data))
|
|
if 'access_token' not in response_data:
|
|
raise ValidationError(_('Could not decode response'))
|
|
|
|
return response_data['access_token']
|
|
|
|
def _get_location_from_partner(self, partner, check_residential=False):
|
|
res = {'countryCode': partner.country_id.code}
|
|
if partner.city:
|
|
res['city'] = partner.city
|
|
if partner.zip:
|
|
res['postalCode'] = partner.zip
|
|
if partner.state_id:
|
|
res['stateOrProvinceCode'] = partner.state_id.code
|
|
if check_residential:
|
|
setting = self.check_residential
|
|
if setting == 'always' or (setting == 'check' and self._check_residential_address({**res, 'streetLines': [partner.street, partner.street2]})):
|
|
res['residential'] = True
|
|
return res
|
|
|
|
def _check_residential_address(self, address):
|
|
if not address['streetLines'][1]:
|
|
del address['streetLines'][1]
|
|
result = self._send_fedex_request('/address/v1/addresses/resolve', {
|
|
'addressesToValidate': [{'address': address}]
|
|
})
|
|
return result['resolvedAddresses'][0]['classification'] != 'BUSINESS' # We assume residential until proven otherwise
|
|
|
|
def _get_address_from_partner(self, partner, check_residential=False):
|
|
res = self._get_location_from_partner(partner, check_residential)
|
|
res['streetLines'] = [partner.street]
|
|
if partner.street2:
|
|
res['streetLines'].append(partner.street2)
|
|
return res
|
|
|
|
def _get_contact_from_partner(self, partner, company_partner=False):
|
|
res = {'phoneNumber': partner.phone or partner.mobile}
|
|
if company_partner and not res['phoneNumber']:
|
|
# Fallback to phone on the company if none on the WH
|
|
res['phoneNumber'] = company_partner.phone or company_partner.mobile
|
|
if company_partner:
|
|
# Always put the name of the company, if the partner is a WH
|
|
res['companyName'] = partner.name
|
|
elif partner.is_company:
|
|
res['companyName'] = partner.name
|
|
else:
|
|
res['personName'] = partner.name
|
|
if partner.parent_id:
|
|
res['companyName'] = partner.parent_id.name
|
|
if partner.email:
|
|
res['emailAddress'] = partner.email
|
|
elif company_partner and company_partner.email:
|
|
res['emailAddress'] = company_partner.email
|
|
return res
|
|
|
|
def _get_package_info(self, package):
|
|
res = {
|
|
'weight': {
|
|
'units': self.weight_units,
|
|
'value': self.carrier._fedex_rest_convert_weight(package.weight)
|
|
},
|
|
}
|
|
if int(package.dimension['length']) or int(package.dimension['width']) or int(package.dimension['height']):
|
|
# FedEx will raise a warning when mixing imperial and metric units (MIXED.MEASURING.UNITS.INCLUDED).
|
|
# So we force the dimension unit based on the selected weight unit on the delivery method.
|
|
res['dimensions'] = {
|
|
'units': 'IN' if self.weight_units == 'LB' else 'CM',
|
|
'length': int(package.dimension['length']),
|
|
'width': int(package.dimension['width']),
|
|
'height': int(package.dimension['height']),
|
|
}
|
|
if self.insurance:
|
|
res['declaredValue'] = {
|
|
'amount': float_repr(package.total_cost * self.insurance / 100, 2),
|
|
'currency': _convert_curr_iso_fdx(package.currency_id.name),
|
|
}
|
|
return res
|
|
|
|
def _get_detailed_package_info(self, package, customPackaging, order_no=False):
|
|
res = self._get_package_info(package)
|
|
if customPackaging:
|
|
res['subPackagingType'] = 'PACKAGE'
|
|
description = ', '.join([c.product_id.name for c in package.commodities])
|
|
res['itemDescription'] = description
|
|
res['itemDescriptionForClearance'] = description
|
|
if order_no:
|
|
res['customerReferences'] = [{
|
|
'customerReferenceType': 'P_O_NUMBER',
|
|
'value': order_no
|
|
}]
|
|
return res
|
|
|
|
def _get_commodities_info(self, commodity, currency):
|
|
res = {
|
|
'description': commodity.product_id.name,
|
|
'customsValue': ({'amount': commodity.monetary_value * commodity.qty, 'currency': currency}),
|
|
'unitPrice': ({'amount': commodity.monetary_value, 'currency': currency}),
|
|
'countryOfManufacture': commodity.country_of_origin,
|
|
'weight': {
|
|
'units': self.weight_units,
|
|
'value': self.carrier._fedex_rest_convert_weight(commodity.product_id.weight),
|
|
},
|
|
'quantity': commodity.qty,
|
|
'quantityUnits': commodity.product_id.uom_id.fedex_code,
|
|
'numberOfPieces': 1,
|
|
}
|
|
if commodity.product_id.hs_code:
|
|
res['harmonizedCode'] = commodity.product_id.hs_code
|
|
return res
|
|
|
|
def _get_tins_from_partner(self, partner, custom_vat=False):
|
|
res = []
|
|
if custom_vat:
|
|
res.append({
|
|
'number': self.vat_override,
|
|
'tinType': 'BUSINESS_UNION'
|
|
})
|
|
if partner.vat and partner.is_company:
|
|
res.append({'number': partner.vat, 'tinType': 'BUSINESS_NATIONAL'})
|
|
elif partner.parent_id and partner.parent_id.vat and partner.parent_id.is_company:
|
|
res.append({'number': partner.parent_id.vat, 'tinType': 'BUSINESS_NATIONAL'})
|
|
return res
|
|
|
|
def _get_shipping_price(self, ship_from, ship_to, packages, currency):
|
|
fedex_currency = _convert_curr_iso_fdx(currency)
|
|
request_data = {
|
|
'accountNumber': {'value': self.account_number},
|
|
'requestedShipment': {
|
|
'rateRequestType': ['PREFERRED'],
|
|
'preferredCurrency': fedex_currency,
|
|
'pickupType': self.dropoff_type,
|
|
'serviceType': self.service_type,
|
|
'packagingType': packages[0].packaging_type,
|
|
'shipper': {'address': self._get_location_from_partner(ship_from)},
|
|
'recipient': {'address': self._get_location_from_partner(ship_to, True)},
|
|
'requestedPackageLineItems': [self._get_package_info(p) for p in packages],
|
|
'customsClearanceDetail': {
|
|
'commercialInvoice': {'shipmentPurpose': 'SOLD'},
|
|
'commodities': [self._get_commodities_info(c, fedex_currency) for pkg in packages for c in pkg.commodities],
|
|
'freightOnValue': 'CARRIER_RISK' if self.insurance == 100 else 'OWN_RISK',
|
|
'dutiesPayment': {'paymentType': 'SENDER'} # Only allowed value...
|
|
}
|
|
}
|
|
}
|
|
self._add_extra_data_to_request(request_data, 'rate')
|
|
res = self._send_fedex_request("/rate/v1/rates/quotes", request_data)
|
|
try:
|
|
rate = next(filter(lambda d: d['currency'] == fedex_currency, res['rateReplyDetails'][0]['ratedShipmentDetails']), {})
|
|
if rate.get('totalNetChargeWithDutiesAndTaxes', 0):
|
|
price = rate['totalNetChargeWithDutiesAndTaxes']
|
|
else:
|
|
price = rate['totalNetCharge']
|
|
except KeyError:
|
|
raise ValidationError(_('Could not decode response')) from None
|
|
|
|
return {
|
|
'price': price,
|
|
'alert_message': self._process_alerts(res),
|
|
}
|
|
|
|
def _ship_package(self, ship_from_wh, ship_from_company, ship_to, sold_to, packages, currency, order_no, customer_ref, picking_no, incoterms, freight_charge):
|
|
fedex_currency = _convert_curr_iso_fdx(currency)
|
|
package_type = packages[0].packaging_type
|
|
request_data = {
|
|
'accountNumber': {'value': self.account_number},
|
|
'labelResponseOptions': 'LABEL',
|
|
'requestedShipment': {
|
|
'rateRequestType': ['PREFERRED'],
|
|
'preferredCurrency': fedex_currency,
|
|
'pickupType': self.dropoff_type,
|
|
'serviceType': self.service_type,
|
|
'packagingType': package_type,
|
|
'shippingChargesPayment': {'paymentType': 'SENDER'},
|
|
'labelSpecification': {'labelStockType': self.label_stock, 'imageType': self.label_file},
|
|
'shipper': {
|
|
'address': self._get_address_from_partner(ship_from_wh),
|
|
'contact': self._get_contact_from_partner(ship_from_wh, ship_from_company),
|
|
'tins': self._get_tins_from_partner(ship_from_company, self.vat_override),
|
|
},
|
|
'recipients': [{
|
|
'address': self._get_address_from_partner(ship_to, True),
|
|
'contact': self._get_contact_from_partner(ship_to),
|
|
'tins': self._get_tins_from_partner(ship_to),
|
|
}],
|
|
'requestedPackageLineItems': [self._get_detailed_package_info(p, package_type == 'YOUR_PACKAGING', order_no) for p in packages],
|
|
'customsClearanceDetail': {
|
|
'dutiesPayment': {'paymentType': self.duty_payment},
|
|
'commodities': [self._get_commodities_info(c, fedex_currency) for pkg in packages for c in pkg.commodities],
|
|
'commercialInvoice': {
|
|
'shipmentPurpose': 'SOLD',
|
|
'originatorName': ship_from_company.name,
|
|
'comments': ['', picking_no], # First one is special instructions
|
|
},
|
|
}
|
|
}
|
|
}
|
|
if freight_charge:
|
|
request_data['requestedShipment']['customsClearanceDetail']['commercialInvoice']['freightCharge'] = {
|
|
'amount': freight_charge,
|
|
'currency': fedex_currency,
|
|
}
|
|
if incoterms:
|
|
request_data['requestedShipment']['customsClearanceDetail']['commercialInvoice']['termsOfSale'] = incoterms
|
|
if customer_ref:
|
|
request_data['requestedShipment']['customsClearanceDetail']['commercialInvoice']['customerReferences'] = [{
|
|
'customerReferenceType': 'CUSTOMER_REFERENCE',
|
|
'value': customer_ref,
|
|
}]
|
|
if request_data['requestedShipment']['shipper']['address']['countryCode'] == 'IN' and request_data['requestedShipment']['recipients'][0]['address']['countryCode'] == 'IN':
|
|
request_data['requestedShipment']['customsClearanceDetail']['freightOnValue'] = 'CARRIER_RISK' if self.insurance == 100 else 'OWN_RISK'
|
|
if sold_to and sold_to != ship_to:
|
|
request_data['requestedShipment']['soldTo'] = {
|
|
'address': self._get_address_from_partner(sold_to),
|
|
'contact': self._get_contact_from_partner(sold_to),
|
|
'tins': self._get_tins_from_partner(sold_to),
|
|
}
|
|
if ship_to.vat or ship_to.parent_id.vat:
|
|
request_data['requestedShipment']['customsClearanceDetail']['recipientCustomsId'] = {
|
|
'type': 'COMPANY',
|
|
'value': ship_to.vat or ship_to.parent_id.vat,
|
|
}
|
|
if self.email_notifications and ship_to.email:
|
|
request_data['requestedShipment']['emailNotificationDetail'] = {
|
|
'aggregationType': 'PER_PACKAGE',
|
|
'emailNotificationRecipients': [{
|
|
'emailNotificationRecipientType': 'RECIPIENT',
|
|
'emailAddress': ship_to.email,
|
|
'name': ship_to.name,
|
|
'notificationFormatType': 'HTML',
|
|
'notificationType': 'EMAIL',
|
|
'notificationEventType': ['ON_DELIVERY', 'ON_EXCEPTION', 'ON_SHIPMENT', 'ON_TENDER', 'ON_ESTIMATED_DELIVERY']
|
|
}]
|
|
}
|
|
if self.documentation_type != 'none':
|
|
request_data['requestedShipment']['shippingDocumentSpecification'] = {
|
|
'shippingDocumentTypes': ['COMMERCIAL_INVOICE'],
|
|
'commercialInvoiceDetail': {
|
|
'documentFormat': {'stockType': 'PAPER_LETTER', 'docType': 'PDF'}
|
|
}
|
|
}
|
|
if self.documentation_type == 'etd':
|
|
request_data['requestedShipment']['shipmentSpecialServices'] = {
|
|
"specialServiceTypes": [
|
|
"ELECTRONIC_TRADE_DOCUMENTS"
|
|
],
|
|
"etdDetail": {
|
|
"requestedDocumentTypes": [
|
|
"COMMERCIAL_INVOICE"
|
|
]
|
|
}
|
|
}
|
|
if self.make_return:
|
|
request_data['requestedShipment']['customsClearanceDetail']['customsOption'] = {'type': 'COURTESY_RETURN_LABEL'}
|
|
|
|
self._add_extra_data_to_request(request_data, 'ship')
|
|
res = self._send_fedex_request("/ship/v1/shipments", request_data)
|
|
|
|
try:
|
|
shipment = res['transactionShipments'][0]
|
|
details = shipment['completedShipmentDetail']
|
|
pieces = shipment['pieceResponses']
|
|
# Sometimes the shipment might be created but no pricing calculated, we just set to 0.
|
|
price = self._decode_pricing(details['shipmentRating']) if 'shipmentRating' in details else 0.0
|
|
except KeyError:
|
|
raise ValidationError(_('Could not decode response')) from None
|
|
|
|
return {
|
|
'service_info': f"{details.get('carrierCode', '')} > {details.get('serviceDescription', {}).get('description', '')} > {details.get('packagingDescription', '')}",
|
|
'tracking_numbers': ','.join([
|
|
t.get('trackingNumber', '')
|
|
for pkg in details.get('completedPackageDetails', [])
|
|
for t in pkg.get('trackingIds', [])
|
|
]),
|
|
'labels': [
|
|
(
|
|
p.get('trackingNumber', ''),
|
|
next(filter(lambda d: d.get('contentType', '') == 'LABEL', p.get('packageDocuments', {})), {}).get('encodedLabel')
|
|
)
|
|
for p in pieces
|
|
],
|
|
'price': price,
|
|
'documents': ', '.join([
|
|
f"{d.get('minimumCopiesRequired')}x {d.get('type', '')}"
|
|
for d in details.get('documentRequirements', {}).get('generationDetails', {})
|
|
if d.get('minimumCopiesRequired', 0)
|
|
]),
|
|
'alert_message': self._process_alerts(shipment),
|
|
'invoice': next(filter(
|
|
lambda d: d.get('contentType', '') == 'COMMERCIAL_INVOICE',
|
|
shipment.get('shipmentDocuments', {})
|
|
), {}).get('encodedLabel', ''),
|
|
'date': shipment.get('shipDatestamp', ''),
|
|
}
|
|
|
|
def _return_package(self, ship_from, ship_to_company, ship_to_wh, packages, currency, tracking, date):
|
|
fedex_currency = _convert_curr_iso_fdx(currency)
|
|
package_type = packages[0].packaging_type
|
|
request_data = {
|
|
'accountNumber': {'value': self.account_number},
|
|
'labelResponseOptions': 'LABEL',
|
|
'requestedShipment': {
|
|
'rateRequestType': ['PREFERRED'],
|
|
'preferredCurrency': fedex_currency,
|
|
'pickupType': self.dropoff_type,
|
|
'serviceType': self.service_type,
|
|
'packagingType': package_type,
|
|
'shippingChargesPayment': {'paymentType': 'SENDER'},
|
|
'shipmentSpecialServices': {
|
|
'specialServiceTypes': ['RETURN_SHIPMENT'],
|
|
'returnShipmentDetail': {
|
|
'returnType': 'PRINT_RETURN_LABEL',
|
|
'returnAssociationDetail': {'trackingNumber': tracking, 'shipDatestamp': date},
|
|
}
|
|
},
|
|
'labelSpecification': {'labelStockType': self.label_stock, 'imageType': self.label_file},
|
|
'shipper': {
|
|
'address': self._get_address_from_partner(ship_from),
|
|
'contact': self._get_contact_from_partner(ship_from),
|
|
'tins': self._get_tins_from_partner(ship_from),
|
|
},
|
|
'recipients': [{
|
|
'address': self._get_address_from_partner(ship_to_wh),
|
|
'contact': self._get_contact_from_partner(ship_to_wh, ship_to_company),
|
|
'tins': self._get_tins_from_partner(ship_to_company, self.vat_override),
|
|
}],
|
|
'requestedPackageLineItems': [self._get_detailed_package_info(p, package_type == 'YOUR_PACKAGING') for p in packages],
|
|
'customsClearanceDetail': {
|
|
'dutiesPayment': {'paymentType': 'SENDER'}, # Only allowed value for returns
|
|
'commodities': [self._get_commodities_info(c, fedex_currency) for pkg in packages for c in pkg.commodities],
|
|
'customsOption': {'type': 'REJECTED'},
|
|
}
|
|
}
|
|
}
|
|
if request_data['requestedShipment']['shipper']['address']['countryCode'] == 'IN' and request_data['requestedShipment']['recipients'][0]['address']['countryCode'] == 'IN':
|
|
request_data['requestedShipment']['customsClearanceDetail']['freightOnValue'] = 'CARRIER_RISK' if self.insurance == 100 else 'OWN_RISK'
|
|
if self.vat_override or ship_to_company.vat:
|
|
request_data['requestedShipment']['customsClearanceDetail']['recipientCustomsId'] = {
|
|
'type': 'COMPANY',
|
|
'value': self.vat_override or ship_to_company.vat,
|
|
}
|
|
|
|
self._add_extra_data_to_request(request_data, 'return')
|
|
res = self._send_fedex_request("/ship/v1/shipments", request_data)
|
|
|
|
try:
|
|
shipment = res['transactionShipments'][0]
|
|
details = shipment['completedShipmentDetail']
|
|
pieces = shipment['pieceResponses']
|
|
except KeyError:
|
|
raise ValidationError(_('Could not decode response')) from None
|
|
|
|
return {
|
|
'tracking_numbers': ','.join([
|
|
t.get('trackingNumber', '')
|
|
for pkg in details.get('completedPackageDetails', [])
|
|
for t in pkg.get('trackingIds', [])
|
|
]),
|
|
'labels': [
|
|
(
|
|
p.get('trackingNumber', ''),
|
|
next(filter(lambda d: d.get('contentType', '') == 'LABEL', p.get('packageDocuments', {})), {}).get('encodedLabel')
|
|
)
|
|
for p in pieces
|
|
],
|
|
'documents': ', '.join([
|
|
f"{d.get('minimumCopiesRequired')}x {d.get('type', '')}"
|
|
for d in details.get('documentRequirements', {}).get('generationDetails', {})
|
|
if d.get('minimumCopiesRequired', 0)
|
|
]),
|
|
'alert_message': self._process_alerts(shipment),
|
|
}
|
|
|
|
def _decode_pricing(self, rating_result):
|
|
actual = next(filter(lambda d: d['rateType'] == rating_result['actualRateType'], rating_result['shipmentRateDetails']), {})
|
|
if actual.get('totalNetChargeWithDutiesAndTaxes', False):
|
|
return actual['totalNetChargeWithDutiesAndTaxes']
|
|
return actual['totalNetCharge']
|
|
|
|
def cancel_shipment(self, tracking_nr):
|
|
res = self._send_fedex_request('/ship/v1/shipments/cancel', {
|
|
'accountNumber': {'value': self.account_number},
|
|
'deletionControl': 'DELETE_ALL_PACKAGES', # Cancel the entire shipment, not only the individual package.
|
|
'trackingNumber': tracking_nr,
|
|
}, 'PUT')
|
|
if not res.get('cancelledShipment', False):
|
|
return {
|
|
'delete_success': False,
|
|
'errors_message': res.get('message', 'Cancel shipment failed. Reason unknown.'),
|
|
}
|
|
return {
|
|
'delete_success': True,
|
|
'alert_message': self._process_alerts(res),
|
|
}
|
|
|
|
def _add_extra_data_to_request(self, request, request_type):
|
|
"""Adds the extra data to the request.
|
|
When there are multiple items in a list, they will all be affected by
|
|
the change.
|
|
"""
|
|
extra_data_input = {
|
|
'rate': self.carrier.fedex_rest_extra_data_rate_request,
|
|
'ship': self.carrier.fedex_rest_extra_data_ship_request,
|
|
'return': self.carrier.fedex_rest_extra_data_return_request,
|
|
}.get(request_type) or ''
|
|
try:
|
|
extra_data = json.loads('{' + extra_data_input + '}')
|
|
except SyntaxError:
|
|
raise UserError(_('Invalid syntax for FedEx extra data.')) from None
|
|
|
|
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 _convert_curr_fdx_iso(code):
|
|
curr_match = {v: k for k, v in FEDEX_CURR_MATCH.items()}
|
|
return curr_match.get(code, code)
|
|
|
|
|
|
def _convert_curr_iso_fdx(code):
|
|
return FEDEX_CURR_MATCH.get(code, code)
|