Repository URL to install this package:
|
Version:
0.4.139 ▾
|
lib-py-b2b
/
fedex_api.py
|
|---|
from os import environ
from datetime import datetime
from dateutil.parser import isoparse
from typing import Optional
from .carrier import CarrierIntegration
from .address import Address, AddressValidationResult, ValidationAddressType
from .fulfillment import Fulfillment
from .fulfillment_status import FulfillmentStatus
from .errors import LabelPrintError, AddressValidationServiceError
from .orders import Orders
from .print import PrintRequest, Printer
from .fedex import FedexServiceType, FedexSignatureOptionType, FedexSpecialServiceType, \
FedexPhysicalPackaging, FedexLabelPrintOrientation, FedexLabelStockType, \
FedexLabelImageType, FedexLabelType, FedexPaymentType, FedexPackaging, \
FedexDropOffType, FedexCustomerReferenceType, FedexTrackingIdType, FedexTransitTime
from fedex.services.ship_service import FedexProcessShipmentRequest, FedexDeleteShipmentRequest
from fedex.services.rate_service import FedexRateServiceRequest
from fedex.services.track_service import FedexTrackRequest
from fedex.base_service import FedexError, FedexFailure
from fedex.config import FedexConfig
from fedex.services.address_validation_service import FedexAddressValidationRequest
from fedex.services.availability_commitment_service import FedexAvailabilityCommitmentRequest
from fedex.tools.conversion import basic_sobject_to_dict
import logging
from aws_xray_sdk.core import xray_recorder
log_level = logging.getLevelName(environ['LOG_LEVEL']) if 'LOG_LEVEL' in environ else logging.INFO
logging.basicConfig(format='%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s',
datefmt='%d-%m-%Y:%H:%M:%S',
level=log_level)
logger = logging.getLogger('lib-b2b-fedex_api')
"""
FedEx Test System Access Confirmation
Contact Info License Authorization Confirmation
Congratulations! Your Test System Access information is confirmed.
There are two parts to the FedEx Authentication process. There is a security code and a
test key.
Please retain the following information in a secured environment. You will need
this information to run your Web Services in the FedEx test environment.
A confirmation email will be sent to you with your Test Security Code and directions for
testing your FedEx Web Service application in the FedEx Test environment.
Required for All Web Services
Developer Test Key: ehwbQ1vaYKdewC1b
Required for FedEx Web Services for Intra Country Shipping in US and Global
Test Account Number: 510087500
Test Meter Number: 119116198
Required for FedEx Web Services for Office and Print
Test FedEx Office Integrator ID: 123
Test Client Product ID: TEST
Test Client Product Version: 9999
"""
"""
case id: 47110201
Account No: 678421081
Supported Web Services: FedEx Web Services for Shipping
Authentication Key: 9Bf2qvscdTbiLYxf
Meter Number: 114350995
Production Account Information
Production URL: https://ws.fedex.com:443/web-services
Password: 4WMSKaSnZGz0YVBYbR4QlHOQQ
FedEx Shipping Account Number: XXXXXX081
FedEx Web Services Meter Number: 114350995
"""
"""
Case id: 47110787
Account No: 105970960
Supported Web Services: FedEx Web Services for Shipping
Authentication Key: bL1AC5ALnaX7AXqK
Meter Number: 114352627
Production URL: https://ws.fedex.com:443/web-services
Password: b3GArrgYwbkZxerBXbh8YJBGR
FedEx Shipping Account Number: XXXXXX960
FedEx Web Services Meter Number: 114352627
"""
"""
Case id: 47110663
Account No: 898601919
Supported Web Services: FedEx Web Services for Shipping
Authentication Key: SunYT31WTo22lkOM
Meter Number: 114352689
Production URL: https://ws.fedex.com:443/web-services
Password: YjIwFY68cOpJu46jodljoxnGf
FedEx Shipping Account Number: XXXXXX919
FedEx Web Services Meter Number: 114352689
"""
class FedexAPI(CarrierIntegration):
def __init__(self, profile):
super().__init__(profile)
self.fedex_config = FedexConfig(key=self.profile.fulfillment_config.fedex_api_config.key,
password=self.profile.fulfillment_config.fedex_api_config.password,
account_number=self.profile.fulfillment_config.fedex_api_config.account_number,
meter_number=self.profile.fulfillment_config.fedex_api_config.meter_number,
use_test_server=self.profile.fulfillment_config.fedex_api_config.use_test_mode)
@xray_recorder.capture()
def track_shipment(self, fulfillment):
results = []
_fulfillment = Fulfillment.for_(fulfillment)
if 'tracking_numbers' in _fulfillment:
for tn in _fulfillment['tracking_numbers']:
track = FedexTrackRequest(self.fedex_config)
track.SelectionDetails.PackageIdentifier.Type = 'TRACKING_NUMBER_OR_DOORTAG'
track.SelectionDetails.PackageIdentifier.Value = tn
track.send_request()
from fedex.tools.conversion import sobject_to_dict
results.append(sobject_to_dict(track.response))
from fedex.tools.conversion import sobject_to_json
print(sobject_to_json(track.response))
return results
@xray_recorder.capture()
def get_shipping_rates(self, fulfillment):
rate = FedexRateServiceRequest(self.fedex_config)
_fulfillment = Fulfillment.for_(fulfillment)
recipient_address = Orders.for_(_fulfillment['order_id']).ship_to
rate.RequestedShipment.DropoffType = FedexDropOffType.REQUEST_COURIER.value if self.profile.fulfillment_config.fedex_api_config.request_pickup else FedexDropOffType.REGULAR_PICKUP.value
rate.RequestedShipment.ServiceType = FedexServiceType(_fulfillment.get('service_type', 92)).name
rate.RequestedShipment.PackagingType = FedexPackaging.YOUR_PACKAGING.value
sender = self.profile.sender_address
rate.RequestedShipment.Shipper = self.__convert_to_fedex_address(address=sender, fedex_party=rate.RequestedShipment.Shipper)
rate.RequestedShipment.Recipient = self.__convert_to_fedex_address(address=recipient_address, fedex_party=rate.RequestedShipment.Recipient)
rate.RequestedShipment.EdtRequestType = 'NONE'
rate.RequestedShipment.ShippingChargesPayment.Payor.ResponsibleParty.AccountNumber = self.profile.fulfillment_config.fedex_api_config.account_number
rate.RequestedShipment.ShippingChargesPayment.PaymentType = FedexPaymentType.SENDER.value
weight = rate.create_wsdl_object_of_type('Weight')
weight.Value = _fulfillment.get('weight', 0)
weight.Units = self.profile.fulfillment_config.get_weight_type(_fulfillment.get('weight_um', 'LB')).name
package_line_item = rate.create_wsdl_object_of_type('RequestedPackageLineItem')
package_line_item.PhysicalPackaging = FedexPhysicalPackaging(
self.profile.fulfillment_config.fedex_api_config.physical_packaging).value if self.profile.fulfillment_config.fedex_api_config.physical_packaging else FedexPhysicalPackaging.BOX.value
package_line_item.Weight = weight
package_line_item.GroupPackageCount = 1
rate.add_package(package_line_item)
rate.send_request()
from fedex.tools.conversion import sobject_to_dict
response_dict = sobject_to_dict(rate.response)
return response_dict['RateReplyDetails'][0]
@xray_recorder.capture()
def remove_shipment(self, fulfillment):
_fulfillment = Fulfillment.for_(fulfillment)
if 'tracking_numbers' in _fulfillment:
for tn in _fulfillment['tracking_numbers']:
del_shipment = FedexDeleteShipmentRequest(self.fedex_config)
del_shipment.DeletionControlType = "DELETE_ALL_PACKAGES"
del_shipment.TrackingId.TrackingNumber = tn
del_shipment.TrackingId.TrackingIdType = FedexTrackingIdType.for_(FedexServiceType(_fulfillment.get('service_type', 92)))
del_shipment.send_request()
if del_shipment.response.HighestSeverity == "SUCCESS":
logger.info(f"Successfully deleted fedex shipment for fulfillment {_fulfillment['id']}")
logger.debug(del_shipment.response)
@xray_recorder.capture()
def generate_label_image(self, fulfillment):
_fulfillment = Fulfillment.for_(fulfillment)
if 'labels' in _fulfillment:
label_images = self.__convert_label_to_png(_fulfillment['id'], _fulfillment['labels'])
return label_images
else:
raise LabelPrintError(
f"Unable to convert labels because they don't exist on fulfillment {_fulfillment['id']}")
@xray_recorder.capture()
def __convert_label_to_png(self, fullfillment_id, labels):
"""
I'm a little nervoud about sending this data to this online service. Would like to find a better way to generate an image for ZPL.
:param fullfillment_id:
:param labels:
:return:
"""
import requests
import base64
label_images = []
for label in labels:
for image in label['parts']:
zpl = base64.b64decode(image)
# adjust print density (8dpmm), label width (4 inches), label height (6 inches), and label index (0) as necessary
url = 'http://api.labelary.com/v1/printers/12dpmm/labels/4x6/0/'
files = {'file': zpl}
# headers = {'Accept': 'application/pdf'} # omit this line to get PNG images back
response = requests.post(url, files=files)
if response.status_code == 200:
response.raw.decode_content = True
encoded_image = base64.b64encode(response.content)
label_images.append(encoded_image)
else:
logger.error(response.text)
return label_images
@xray_recorder.capture()
def __convert_to_fedex_address(self, address: Address, fedex_party):
fedex_party.Address.StreetLines = []
fedex_party.Address.StreetLines.append(address.address1)
if address.address2:
fedex_party.Address.StreetLines.append(address.address2)
fedex_party.Address.City = address.city
fedex_party.Address.StateOrProvinceCode = address.state
fedex_party.Address.PostalCode = address.postalCode
fedex_party.Address.CountryCode = address.country
fedex_party.Address.Residential = False
if address.company:
fedex_party.Contact.CompanyName = address.company
else:
fedex_party.Contact.PersonName = address.name
fedex_party.Contact.PhoneNumber = address.phone
return fedex_party
@xray_recorder.capture()
def __convert_from_fedex_address(self, address_dict: dict) -> Address:
street_lines = address_dict.get('StreetLines', [])
address1 = street_lines[0] if len(street_lines) > 0 else None
address2 = street_lines[1] if len(street_lines) > 1 else None
_address = Address(address1=address1,
address2=address2,
city=address_dict.get('City', None),
state=address_dict.get('StateOrProvinceCode', None),
postalCode=address_dict.get('PostalCode', None),
country=address_dict.get('CountryCode', None),
company=address_dict.get('CompanyName', None),
phone=address_dict.get('PhoneNumber', None),
name=address_dict.get('PersonName', None))
return _address
@xray_recorder.capture()
def validate_address(self, address: Address):
avs_request = FedexAddressValidationRequest(self.fedex_config)
address1 = self.__convert_to_fedex_address(address, avs_request.create_wsdl_object_of_type('AddressToValidate'))
avs_request.add_address(address1)
try:
avs_request.send_request()
except (FedexError, FedexFailure) as fe:
logger.error("Problem validating address with Fedex API. " + str(fe))
raise AddressValidationServiceError(str(fe)) from fe
address_results = avs_request.response.AddressResults[0]
attributes = {k.Name: True if k.Value == 'true' else False for k in address_results.Attributes}
fedex_transit_time = self.get_estimated_transit_time(address)
effective_address = self.__convert_from_fedex_address(basic_sobject_to_dict(address_results.EffectiveAddress))
return AddressValidationResult(
original_address=address,
effective_address=effective_address,
address_type=ValidationAddressType(address_results.State),
residential=address_results.Classification == 'RESIDENTIAL',
attributes=attributes,
estimated_transit_time=fedex_transit_time.value
)
def get_estimated_transit_time(self, address: Address) -> Optional[FedexTransitTime]:
acr_request = FedexAvailabilityCommitmentRequest(self.fedex_config)
sender: Address = self.profile.sender_address
acr_request.Origin.PostalCode = sender.postalCode
acr_request.Origin.CountryCode = "US" if sender.country == "USA" else sender.country
acr_request.Destination.PostalCode = address.postalCode
acr_request.Destination.CountryCode = "US" if address.country == "USA" else address.country
acr_request.CarrierCode = 'FDXG'
try:
acr_request.send_request()
except (FedexError, FedexFailure) as fe:
logger.error("Problem checking service availability for address with Fedex API. " + str(fe))
raise AddressValidationServiceError(str(fe)) from fe
if acr_request.response.Options:
_options = {FedexServiceType.for_(o.Service):FedexTransitTime.for_(o.TransitTime) for o in acr_request.response.Options}
return _options[FedexServiceType.FEDEX_GROUND]
else:
return None
@xray_recorder.capture()
def create_shipment(self, fulfillment, validate_address=False, validate_request=False):
_fulfillment = Fulfillment.for_(fulfillment)
recipient_address = Orders.for_(_fulfillment['order_id']).ship_to
xray_recorder.begin_subsegment('validate_address')
_profile_validate_address = self.profile.fulfillment_config.fedex_api_config.validate_address
effective_address = recipient_address
is_residential = True
# Moving this to earlier in the process, no longer makes sense to do the validation at this step
# if validate_address or _profile_validate_address:
# try:
# validation_result = self.validate_address(recipient_address)
# effective_address = validation_result.effective_address
# is_residential = validation_result.residential
# # merge in any attributes that don't come back from the address validation.
# effective_address.merge(recipient_address)
# except AddressValidationServiceError as avse:
# logger.error(avse)
xray_recorder.end_subsegment()
xray_recorder.begin_subsegment('create_api_request')
# shipment_uid = f"{order.get_shipment_id()}-{i}"
# shipment = FedexProcessShipmentRequest(self.fedex_config, customer_transaction_id=shipment_uid)
shipment = FedexProcessShipmentRequest(self.fedex_config)
# MARK: -- Create Requested Shipment --
shipment.RequestedShipment.DropoffType = FedexDropOffType.REQUEST_COURIER.value if self.profile.fulfillment_config.fedex_api_config.request_pickup else FedexDropOffType.REGULAR_PICKUP.value
shipment.RequestedShipment.ServiceType = FedexServiceType(_fulfillment.get('service_type', 92)).name
shipment.RequestedShipment.PackagingType = FedexPackaging.YOUR_PACKAGING.value
xray_recorder.end_subsegment()
xray_recorder.begin_subsegment('set_addresses')
sender = self.profile.sender_address
shipment.RequestedShipment.Shipper = self.__convert_to_fedex_address(address=sender,
fedex_party=shipment.RequestedShipment.Shipper)
shipment.RequestedShipment.Recipient = self.__convert_to_fedex_address(address=effective_address,
fedex_party=shipment.RequestedShipment.Recipient)
# This is needed to ensure an accurate rate quote with the response.
# Use AddressValidation to get ResidentialStatus
shipment.RequestedShipment.Recipient.Address.Residential = is_residential
# Include Estimated Duties and Taxes for International Shipments
shipment.RequestedShipment.EdtRequestType = 'NONE'
xray_recorder.end_subsegment()
# Senders account information
xray_recorder.begin_subsegment('configure_request')
shipment.RequestedShipment.ShippingChargesPayment.Payor.ResponsibleParty.AccountNumber = self.profile.fulfillment_config.fedex_api_config.account_number
shipment.RequestedShipment.ShippingChargesPayment.PaymentType = FedexPaymentType.SENDER.value
shipment.RequestedShipment.LabelSpecification.LabelFormatType = FedexLabelType.COMMON2D.value
shipment.RequestedShipment.LabelSpecification.ImageType = FedexLabelImageType.ZPLII.value
# To use doctab stocks, you must change ImageType above to one of the label printer formats (ZPLII, EPL2, DPL).
shipment.RequestedShipment.LabelSpecification.LabelStockType = FedexLabelStockType(
self.profile.fulfillment_config.fedex_api_config.label_type).value if self.profile.fulfillment_config.fedex_api_config.label_type else FedexLabelStockType.STOCK_4X6.value
# Timestamp in YYYY-MM-DDThh:mm:ss format, e.g. 2002-05-30T09:00:00
ship_date = _fulfillment['ship_date'] if 'ship_date' in _fulfillment else datetime.now().isoformat()
iso_ship_date = isoparse(ship_date).replace(microsecond=0).isoformat()
shipment.RequestedShipment.ShipTimestamp = iso_ship_date
shipment.RequestedShipment.LabelSpecification.LabelPrintingOrientation = FedexLabelPrintOrientation(
self.profile.fulfillment_config.fedex_api_config.label_orientation).value if self.profile.fulfillment_config.fedex_api_config.label_orientation else FedexLabelPrintOrientation.BOTTOM_EDGE_OF_TEXT_FIRST.value
# Delete the flags we don't want.
if hasattr(shipment.RequestedShipment.LabelSpecification, 'LabelOrder'):
del shipment.RequestedShipment.LabelSpecification.LabelOrder # Delete, not using.
xray_recorder.end_subsegment()
xray_recorder.begin_subsegment('create_package_line')
# Create Weight
weight = shipment.create_wsdl_object_of_type('Weight')
weight.Value = float(_fulfillment.get('weight', 0))
weight.Units = self.profile.fulfillment_config.get_weight_type(_fulfillment.get('weight_um', 'LB')).name
# Create PackageLineItem
package_line_item = shipment.create_wsdl_object_of_type('RequestedPackageLineItem')
# BAG, BARREL, BASKET, BOX, BUCKET, BUNDLE, CARTON, CASE, CONTAINER, ENVELOPE etc..
package_line_item.PhysicalPackaging = FedexPhysicalPackaging(
self.profile.fulfillment_config.fedex_api_config.physical_packaging).value if self.profile.fulfillment_config.fedex_api_config.physical_packaging else FedexPhysicalPackaging.BOX.value
package_line_item.Weight = weight
# Insured Value
# insure = shipment.create_wsdl_object_of_type('Money')
# insure.Currency = 'USD'
# insure.Amount = 1.0
# Add Insured and Total Insured values.
# package_line_item.InsuredValue = insure
# shipment.RequestedShipment.TotalInsuredValue = insure
xray_recorder.end_subsegment()
xray_recorder.begin_subsegment('configure_references')
# Add customer reference
customer_reference = shipment.create_wsdl_object_of_type('CustomerReference')
customer_reference.CustomerReferenceType = "CUSTOMER_REFERENCE"
customer_reference.Value = _fulfillment.get('container_id', 'Unknown Container')
package_line_item.CustomerReferences.append(customer_reference)
# Add department number
# department_number = shipment.create_wsdl_object_of_type('CustomerReference')
# department_number.CustomerReferenceType = FedexCustomerReferenceType.DEPARTMENT_NUMBER.value
# department_number.Value = self.profile.department
# package_line_item.CustomerReferences.append(department_number)
# Add invoice number
if 'glovia_order' in _fulfillment:
invoice_number = shipment.create_wsdl_object_of_type('CustomerReference')
invoice_number.CustomerReferenceType = FedexCustomerReferenceType.INVOICE_NUMBER.value
invoice_number.Value = _fulfillment.get('glovia_order', 'Unknown Orders')
package_line_item.CustomerReferences.append(invoice_number)
# Add po number
po_number = shipment.create_wsdl_object_of_type('CustomerReference')
po_number.CustomerReferenceType = FedexCustomerReferenceType.P_O_NUMBER.value
po_number.Value = _fulfillment.get('purchase_order', 'Unknown PO')
package_line_item.CustomerReferences.append(po_number)
if 'signature_required' in _fulfillment:
package_line_item.SpecialServicesRequested.SpecialServiceTypes = FedexSpecialServiceType.SIGNATURE_OPTION.value
package_line_item.SpecialServicesRequested.SignatureOptionDetail.OptionType = FedexSignatureOptionType.SERVICE_DEFAULT.value
# This adds the RequestedPackageLineItem WSDL object to the shipment. It
# increments the package count and total weight of the shipment for you.
shipment.add_package(package_line_item)
xray_recorder.end_subsegment()
xray_recorder.begin_subsegment('send_request')
logger.debug("RequestedShipment...")
logger.debug(shipment.RequestedShipment)
logger.debug("ClientDetail...")
logger.debug(shipment.ClientDetail)
logger.debug("TransactionDetail...")
logger.debug(shipment.TransactionDetail)
_profile_validate_request = self.profile.fulfillment_config.fedex_api_config.verify_request
if validate_request or _profile_validate_request:
xray_recorder.begin_subsegment('validate_request')
shipment.send_validation_request()
xray_recorder.end_subsegment()
if shipment.response.HighestSeverity == "SUCCESS":
shipment.send_request()
else:
shipment.send_request()
xray_recorder.end_subsegment()
xray_recorder.begin_subsegment('process_response')
logger.debug("Shipment Response...")
logger.debug(shipment.response)
from fedex.tools.conversion import sobject_to_dict
response_dict = sobject_to_dict(shipment.response)
# response_dict['CompletedShipmentDetail']['CompletedPackageDetails'][0]['Label']['Parts'][0]['Image'] = ''
# This will dump the response data dict to json.
# from fedex.tools.conversion import sobject_to_json
# response_json = sobject_to_json(shipment.response)
logger.debug(f"HighestSeverity: {shipment.response.HighestSeverity}")
shipment_data = {
'tracking_numbers': [],
'carrier_code': '',
'carrier_data': response_dict,
'transit_time': '',
'labels': [],
'net_shipping_cost': 0.00,
'saturday_delivery': False
}
# NOTE: -------------------------------------------------------
# This is built on the assumption that we do not have
# multi part shipments. One Shipment = One Box.
# -------------------------------------------------------------
csd = response_dict['CompletedShipmentDetail']
shipment_data['transit_time'] = csd['OperationalDetail']['TransitTime']
shipment_data['carrier_code'] = csd['CarrierCode']
if 'OperationalDetail' in csd and 'DeliveryEligibilities' in csd['OperationalDetail']:
shipment_data['saturday_delivery'] = (
'SATURDAY_DELIVERY' in csd['OperationalDetail']['DeliveryEligibilities'])
for cpd in csd['CompletedPackageDetails']:
for tid in cpd['TrackingIds']:
shipment_data['tracking_numbers'].append(tid['TrackingNumber'])
label = cpd['Label']
shipment_data['labels'].append({
'copies': label['CopiesToPrint'],
'parts': [i['Image'] for i in label['Parts']]
})
if 'ShipmentRating' in csd and 'ShipmentRateDetails' in csd['ShipmentRating']:
for srd in csd.get('ShipmentRating', {}).get('ShipmentRateDetails', []):
shipment_data['ship_zone'] = srd.get('RatedZone')
shipment_data['net_shipping_cost'] += srd.get('TotalNetCharge', {}).get('Amount', 0.0)
xray_recorder.end_subsegment()
xray_recorder.begin_subsegment('update_fulfillment')
Fulfillment.update_for_shipment(fulfillment=_fulfillment['id'],
status=FulfillmentStatus.NOT_SENT,
tracking_numbers=shipment_data['tracking_numbers'],
ship_dates=[iso_ship_date],
net_shipping_cost=shipment_data['net_shipping_cost'],
labels=shipment_data['labels'],
carrier_code=shipment_data['carrier_code'],
carrier_data=shipment_data['carrier_data'],
transit_time=shipment_data['transit_time']
)
xray_recorder.end_subsegment()
xray_recorder.begin_subsegment('create_print_request')
if self.profile.fulfillment_config.fedex_api_config.print_immediately:
print_request = PrintRequest.create(printer=Printer.for_(_fulfillment['customer_edi_id']),
fulfillment=_fulfillment)
logger.info(f"Requested print for fulfillment {_fulfillment['id']} with request {print_request}")
xray_recorder.end_subsegment()