diff --git a/examples/create_international_shipment.py b/examples/create_international_shipment.py new file mode 100644 index 0000000..a75d639 --- /dev/null +++ b/examples/create_international_shipment.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python +""" +This example shows how to create a shipment and generate a waybill as output. The variables populated below +represents the minimum required values. You will need to fill all of these, or +risk seeing a SchemaValidationError exception thrown. + +Near the bottom of the module, you'll see some different ways to handle the +label data that is returned with the reply. +""" +import logging +import binascii +import datetime +import sys + +from example_config import CONFIG_OBJ +from fedex.services.ship_service import FedexProcessInternationalShipmentRequest + +# What kind of file do you want this example to generate? +# Valid choices for this example are PDF, PNG +GENERATE_IMAGE_TYPE = 'PDF' + +# Un-comment to see the response from Fedex printed in stdout. +logging.basicConfig(stream=sys.stdout, level=logging.INFO) + +# This is the object that will be handling our shipment request. +# We're using the FedexConfig object from example_config.py in this dir. +customer_transaction_id = "*** ShipService Request v17 using Python ***" # Optional transaction_id +shipment = FedexProcessInternationalShipmentRequest(CONFIG_OBJ, customer_transaction_id=customer_transaction_id) + +# This is very generalized, top-level information. +# REGULAR_PICKUP, REQUEST_COURIER, DROP_BOX, BUSINESS_SERVICE_CENTER or STATION +shipment.RequestedShipment.DropoffType = 'BUSINESS_SERVICE_CENTER' + +# See page 355 in WS_ShipService.pdf for a full list. Here are the common ones: +# STANDARD_OVERNIGHT, PRIORITY_OVERNIGHT, FEDEX_GROUND, FEDEX_EXPRESS_SAVER, +# FEDEX_2_DAY, INTERNATIONAL_PRIORITY, SAME_DAY, INTERNATIONAL_ECONOMY +shipment.RequestedShipment.ServiceType = 'INTERNATIONAL_PRIORITY' + +# What kind of package this will be shipped in. +# FEDEX_BOX, FEDEX_PAK, FEDEX_TUBE, YOUR_PACKAGING, FEDEX_ENVELOPE +shipment.RequestedShipment.PackagingType = 'FEDEX_PAK' + +# Shipper contact info. +shipment.RequestedShipment.Shipper.Contact.PersonName = 'Sender Name' +shipment.RequestedShipment.Shipper.Contact.CompanyName = 'Some Company' +shipment.RequestedShipment.Shipper.Contact.PhoneNumber = '9012638716' + +# Shipper address. +shipment.RequestedShipment.Shipper.Address.StreetLines = ['Address Line 1'] +shipment.RequestedShipment.Shipper.Address.City = 'Herndon' +shipment.RequestedShipment.Shipper.Address.StateOrProvinceCode = 'VA' +shipment.RequestedShipment.Shipper.Address.PostalCode = '20171' +shipment.RequestedShipment.Shipper.Address.CountryCode = 'US' +shipment.RequestedShipment.Shipper.Address.Residential = True + +# Recipient contact info. +shipment.RequestedShipment.Recipient.Contact.PersonName = 'Recipient Name' +shipment.RequestedShipment.Recipient.Contact.CompanyName = 'Recipient Company' +shipment.RequestedShipment.Recipient.Contact.PhoneNumber = '00380445020911' + +# Recipient address +shipment.RequestedShipment.Recipient.Address.StreetLines = ['4, Horyva St.'] +shipment.RequestedShipment.Recipient.Address.City = 'Kyiv' +shipment.RequestedShipment.Recipient.Address.StateOrProvinceCode = '' +shipment.RequestedShipment.Recipient.Address.PostalCode = '04070' +shipment.RequestedShipment.Recipient.Address.CountryCode = 'UA' +# This is needed to ensure an accurate rate quote with the response. Use AddressValidation to get ResidentialStatus +shipment.RequestedShipment.Recipient.Address.Residential = True +shipment.RequestedShipment.EdtRequestType = 'NONE' + +# Senders account information +shipment.RequestedShipment.ShippingChargesPayment.Payor.ResponsibleParty.AccountNumber = CONFIG_OBJ.account_number + +# Who pays for the shipment? +# RECIPIENT, SENDER or THIRD_PARTY +shipment.RequestedShipment.ShippingChargesPayment.PaymentType = 'SENDER' + +# Specifies the label type to be returned. +# LABEL_DATA_ONLY or COMMON2D +shipment.RequestedShipment.LabelSpecification.LabelFormatType = 'COMMON2D' + +# Specifies which format the label file will be sent to you in. +# DPL, EPL2, PDF, PNG, ZPLII +shipment.RequestedShipment.LabelSpecification.ImageType = GENERATE_IMAGE_TYPE + +# To use doctab stocks, you must change ImageType above to one of the +# label printer formats (ZPLII, EPL2, DPL). +# See documentation for paper types, there quite a few. +shipment.RequestedShipment.LabelSpecification.LabelStockType = 'PAPER_7X4.75' + +# This indicates if the top or bottom of the label comes out of the +# printer first. +# BOTTOM_EDGE_OF_TEXT_FIRST or TOP_EDGE_OF_TEXT_FIRST +# Timestamp in YYYY-MM-DDThh:mm:ss format, e.g. 2002-05-30T09:00:00 +shipment.RequestedShipment.ShipTimestamp = datetime.datetime.now().replace(microsecond=0).isoformat() + +# BOTTOM_EDGE_OF_TEXT_FIRST, TOP_EDGE_OF_TEXT_FIRST +shipment.RequestedShipment.LabelSpecification.LabelPrintingOrientation = 'TOP_EDGE_OF_TEXT_FIRST' + +# Delete the flags we don't want. +# Can be SHIPPING_LABEL_FIRST, SHIPPING_LABEL_LAST or delete +if hasattr(shipment.RequestedShipment.LabelSpecification, 'LabelOrder'): + del shipment.RequestedShipment.LabelSpecification.LabelOrder # Delete, not using. + +# Create Weight, in pounds. +package1_weight = shipment.create_wsdl_object_of_type('Weight') +package1_weight.Value = 5.0 +package1_weight.Units = "LB" + +# international shipment options + +shipment.RequestedShipment.CustomsClearanceDetail.DutiesPayment.Payor.ResponsibleParty.AccountNumber = CONFIG_OBJ.account_number +shipment.RequestedShipment.CustomsClearanceDetail.DutiesPayment.PaymentType = 'SENDER' + +# TODO +# Implement Commercial Invoice and Purpose of Shipment (as a property of Commerial Invoice) +# Acceptable values: +# "GIFT", "NOT_SOLD", "PERSONAL_EFFECTS", "REPAIR_AND_RETURN", "SAMPLE","SOLD" +# shipment.RequestedShipment.CustomsClearanceDetail.CommercialInvoice.Purpose = "GIFT" + +quantity = 5 + +commodity = shipment.create_wsdl_object_of_type('Commodity') +commodity.Name = "Books" +commodity.NumberOfPieces = quantity +commodity.Description = "Books for a present" +commodity.CountryOfManufacture = "US" +commodity.Weight = package1_weight +commodity.Quantity = quantity +commodity.QuantityUnits = 'EA' # EACH - for items measured in units + +commodity.UnitPrice.Currency = "USD" +commodity.UnitPrice.Amount = 10 + +commodity.CustomsValue.Currency = "USD" +commodity.CustomsValue.Amount = quantity * commodity.UnitPrice.Amount + +# if you have more then one commodity in your shipment, CustomsClearanceDetail.CustomsValue.Amount +# shall be equal to the sum of commodity.CustomsValue.Amount for all commodities +shipment.RequestedShipment.CustomsClearanceDetail.CustomsValue.Amount = commodity.CustomsValue.Amount +shipment.RequestedShipment.CustomsClearanceDetail.CustomsValue.Currency = commodity.CustomsValue.Currency + +shipment.add_commodity(commodity) + +# Insured Value +# package1_insure = shipment.create_wsdl_object_of_type('Money') +# package1_insure.Currency = 'USD' +# package1_insure.Amount = 1.0 + +# Create PackageLineItem +package1 = shipment.create_wsdl_object_of_type('RequestedPackageLineItem') +# BAG, BARREL, BASKET, BOX, BUCKET, BUNDLE, CARTON, CASE, CONTAINER, ENVELOPE etc.. +package1.PhysicalPackaging = 'BOX' +package1.Weight = package1_weight + +# Add Insured and Total Insured values. +# package1.InsuredValue = package1_insure +# shipment.RequestedShipment.TotalInsuredValue = package1_insure + +# Add customer reference +# customer_reference = shipment.create_wsdl_object_of_type('CustomerReference') +# customer_reference.CustomerReferenceType="CUSTOMER_REFERENCE" +# customer_reference.Value = "your customer reference number" +# package1.CustomerReferences.append(customer_reference) + +# Add department number +# department_number = shipment.create_wsdl_object_of_type('CustomerReference') +# department_number.CustomerReferenceType="DEPARTMENT_NUMBER" +# department_number.Value = "your department number" +# package1.CustomerReferences.append(department_number) + +# Add invoice number +# invoice_number = shipment.create_wsdl_object_of_type('CustomerReference') +# invoice_number.CustomerReferenceType="INVOICE_NUMBER" +# invoice_number.Value = "your invoice number" +# package1.CustomerReferences.append(invoice_number) + +# Add a signature option for the package using SpecialServicesRequested or comment out. +# SpecialServiceTypes can be APPOINTMENT_DELIVERY, COD, DANGEROUS_GOODS, DRY_ICE, SIGNATURE_OPTION etc.. +package1.SpecialServicesRequested.SpecialServiceTypes = 'SIGNATURE_OPTION' +# SignatureOptionType can be ADULT, DIRECT, INDIRECT, NO_SIGNATURE_REQUIRED, SERVICE_DEFAULT +package1.SpecialServicesRequested.SignatureOptionDetail.OptionType = 'SERVICE_DEFAULT' + +# Un-comment this to see the other variables you may set on a package. +# print(package1) + +# 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(package1) + +# If you'd like to see some documentation on the ship service WSDL, un-comment +# this line. (Spammy). +# print(shipment.client) + +# Un-comment this to see your complete, ready-to-send request as it stands +# before it is actually sent. This is useful for seeing what values you can +# change. +# print(shipment.RequestedShipment) +# print(shipment.ClientDetail) +# print(shipment.TransactionDetail) + +# If you want to make sure that all of your entered details are valid, you +# can call this and parse it just like you would via send_request(). If +# shipment.response.HighestSeverity == "SUCCESS", your shipment is valid. +# print(shipment.send_validation_request()) + +# Fires off the request, sets the 'response' attribute on the object. +shipment.send_request() + +# This will show the reply to your shipment being sent. You can access the +# attributes through the response attribute on the request object. This is +# good to un-comment to see the variables returned by the Fedex reply. +# print(shipment.response) + +# This will convert the response to a python dict object. To +# make it easier to work with. Also see basic_sobject_to_dict, it's faster but lacks options. +# from fedex.tools.response_tools import sobject_to_dict +# response_dict = sobject_to_dict(shipment.response) +# response_dict['CompletedShipmentDetail']['CompletedPackageDetails'][0]['Label']['Parts'][0]['Image'] = '' +# print(response_dict) # Image is empty string for display purposes. + +# This will dump the response data dict to json. +# from fedex.tools.response_tools import sobject_to_json +# print(sobject_to_json(shipment.response)) + +# Here is the overall end result of the query. +print("HighestSeverity: {}".format(shipment.response.HighestSeverity)) + +# Getting the tracking number from the new shipment. +print("Tracking #: {}" + "".format(shipment.response.CompletedShipmentDetail.CompletedPackageDetails[0].TrackingIds[0].TrackingNumber)) + +# Net shipping costs. Only show if available. Sometimes sandbox will not include this in the response. +CompletedPackageDetails = shipment.response.CompletedShipmentDetail.CompletedPackageDetails[0] +if hasattr(CompletedPackageDetails, 'PackageRating'): + print("Net Shipping Cost (US$): {}" + "".format(CompletedPackageDetails.PackageRating.PackageRateDetails[0].NetCharge.Amount)) +else: + print('WARNING: Unable to get shipping rate.') + +# Get the label image in ASCII format from the reply. Note the list indices +# we're using. You'll need to adjust or iterate through these if your shipment +# has multiple packages. + +ascii_label_data = shipment.response.CompletedShipmentDetail.CompletedPackageDetails[0].Label.Parts[0].Image + +# Convert the ASCII data to binary. +label_binary_data = binascii.a2b_base64(ascii_label_data) + +""" +This is an example of how to dump a label to a local file. +""" +# This will be the file we write the label out to. +out_path = 'example_shipment_label.%s' % GENERATE_IMAGE_TYPE.lower() +print("Writing to file {}".format(out_path)) +out_file = open(out_path, 'wb') +out_file.write(label_binary_data) +out_file.close() + +""" +This is an example of how to print the label to a serial printer. This will not +work for all label printers, consult your printer's documentation for more +details on what formats it can accept. +""" +# Pipe the binary directly to the label printer. Works under Linux +# without requiring PySerial. This WILL NOT work on other platforms. +# label_printer = open("/dev/ttyS0", "w") +# label_printer.write(label_binary_data) +# label_printer.close() + +""" +This is a potential cross-platform solution using pySerial. This has not been +tested in a long time and may or may not work. For Windows, Mac, and other +platforms, you may want to go this route. +""" +# import serial +# label_printer = serial.Serial(0) +# print("SELECTED SERIAL PORT: "+ label_printer.portstr) +# label_printer.write(label_binary_data) +# label_printer.close() diff --git a/examples/example_config.py b/examples/example_config.py index 6fbcf51..d754843 100644 --- a/examples/example_config.py +++ b/examples/example_config.py @@ -12,9 +12,9 @@ from fedex.config import FedexConfig # Change these values to match your testing account/meter number. -CONFIG_OBJ = FedexConfig(key='xxxxxxxxxxx', - password='xxxxxxxxxxx', - account_number='xxxxxxxxxxx', - meter_number='xxxxxxxxxxx', - freight_account_number='xxxxxxxxxxx', +CONFIG_OBJ = FedexConfig(key='0uSKxCgw6AZANfZ5', + password='WFDeuKsHwGuplTgd7ESLK0FpB', + account_number='510087283', + meter_number='118747441', + freight_account_number='510087020', use_test_server=True) diff --git a/examples/international_rate_request.py b/examples/international_rate_request.py new file mode 100644 index 0000000..96a0c0a --- /dev/null +++ b/examples/international_rate_request.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python +""" +This example shows how to use the FedEx RateRequest service. +The variables populated below represents the minimum required values. +You will need to fill all of these, or risk seeing a SchemaValidationError +exception thrown by suds. + +TIP: Near the bottom of the module, see how to check the if the destination + is Out of Delivery Area (ODA). +""" +import logging +import sys + +from example_config import CONFIG_OBJ +from fedex.services.rate_service import FedexInternationalRateServiceRequest +from fedex.tools.conversion import sobject_to_dict + +# Un-comment to see the response from Fedex printed in stdout. +logging.basicConfig(stream=sys.stdout, level=logging.INFO) + +# This is the object that will be handling our request. +# We're using the FedexConfig object from example_config.py in this dir. +customer_transaction_id = "*** RateService Request v18 using Python ***" # Optional transaction_id +rate_request = FedexInternationalRateServiceRequest(CONFIG_OBJ, customer_transaction_id=customer_transaction_id) + +# If you wish to have transit data returned with your request you +# need to uncomment the following +# rate_request.ReturnTransitAndCommit = True + +# This is very generalized, top-level information. +# REGULAR_PICKUP, REQUEST_COURIER, DROP_BOX, BUSINESS_SERVICE_CENTER or STATION +rate_request.RequestedShipment.DropoffType = 'REGULAR_PICKUP' + +# See page 355 in WS_ShipService.pdf for a full list. Here are the common ones: +# STANDARD_OVERNIGHT, PRIORITY_OVERNIGHT, FEDEX_GROUND, FEDEX_EXPRESS_SAVER +# To receive rates for multiple ServiceTypes set to None. +rate_request.RequestedShipment.ServiceType = 'INTERNATIONAL_PRIORITY' + +# What kind of package this will be shipped in. +# FEDEX_BOX, FEDEX_PAK, FEDEX_TUBE, YOUR_PACKAGING +rate_request.RequestedShipment.PackagingType = 'YOUR_PACKAGING' + +# Shipper's address +rate_request.RequestedShipment.Shipper.Address.StreetLines = ['Address Line 1'] +rate_request.RequestedShipment.Shipper.Address.City = 'Herndon' +rate_request.RequestedShipment.Shipper.Address.StateOrProvinceCode = 'VA' +rate_request.RequestedShipment.Shipper.Address.PostalCode = '20171' +rate_request.RequestedShipment.Shipper.Address.CountryCode = 'US' +rate_request.RequestedShipment.Shipper.Address.Residential = False + +# Recipient address +rate_request.RequestedShipment.Recipient.Address.StreetLines = ['Address Line 1'] +rate_request.RequestedShipment.Recipient.Address.City = 'Kyiv' +rate_request.RequestedShipment.Recipient.Address.StateOrProvinceCode = '' +rate_request.RequestedShipment.Recipient.Address.PostalCode = '04070' +rate_request.RequestedShipment.Recipient.Address.CountryCode = 'UA' +rate_request.RequestedShipment.Recipient.Address.Residential = False +# This is needed to ensure an accurate rate quote with the response. +# rate_request.RequestedShipment.Recipient.Address.Residential = True +# include estimated duties and taxes in rate quote, can be ALL or NONE +rate_request.RequestedShipment.EdtRequestType = 'NONE' + +# International Shipment attributes + +rate_request.ReturnTransitAndCommit = True +rate_request.RequestedShipment.ServiceType = 'INTERNATIONAL_PRIORITY' +rate_request.RequestedShipment.RateRequestTypes = 'LIST' + +rate_request.RequestedShipment.CustomsClearanceDetail.CustomsValue.Amount = 100 +rate_request.RequestedShipment.CustomsClearanceDetail.CustomsValue.Currency = "USD" + +rate_request.RequestedShipment.CustomsClearanceDetail.DutiesPayment.Payor.ResponsibleParty.AccountNumber = CONFIG_OBJ.account_number +rate_request.RequestedShipment.CustomsClearanceDetail.DutiesPayment.PaymentType = 'SENDER' + +# Who pays for the rate_request? +# RECIPIENT, SENDER or THIRD_PARTY +rate_request.RequestedShipment.ShippingChargesPayment.PaymentType = 'SENDER' + +package1_weight = rate_request.create_wsdl_object_of_type('Weight') +# Weight, in LB. +package1_weight.Value = 1.0 +package1_weight.Units = "LB" + +package1 = rate_request.create_wsdl_object_of_type('RequestedPackageLineItem') +package1.Weight = package1_weight +# can be other values this is probably the most common +package1.PhysicalPackaging = 'BOX' +# Required, but according to FedEx docs: +# "Used only with PACKAGE_GROUPS, as a count of packages within a +# group of identical packages". In practice you can use this to get rates +# for a shipment with multiple packages of an identical package size/weight +# on rate request without creating multiple RequestedPackageLineItem elements. +# You can OPTIONALLY specify a package group: +# package1.GroupNumber = 0 # default is 0 +# The result will be found in RatedPackageDetail, with specified GroupNumber. +package1.GroupPackageCount = 1 +# Un-comment this to see the other variables you may set on a package. +# print(package1) + +# This adds the RequestedPackageLineItem WSDL object to the rate_request. It +# increments the package count and total weight of the rate_request for you. +rate_request.add_package(package1) + +# If you'd like to see some documentation on the ship service WSDL, un-comment +# this line. (Spammy). +# print(rate_request.client) + +# Un-comment this to see your complete, ready-to-send request as it stands +# before it is actually sent. This is useful for seeing what values you can +# change. +# print(rate_request.RequestedShipment) + +# Fires off the request, sets the 'response' attribute on the object. +rate_request.send_request() + +# This will show the reply to your rate_request being sent. You can access the +# attributes through the response attribute on the request object. This is +# good to un-comment to see the variables returned by the FedEx reply. +# print(rate_request.response) + +# This will convert the response to a python dict object. To +# make it easier to work with. +# from fedex.tools.conversion import basic_sobject_to_dict +# print(basic_sobject_to_dict(rate_request.response)) + +# This will dump the response data dict to json. +# from fedex.tools.conversion import sobject_to_json +# print(sobject_to_json(rate_request.response)) + +# Here is the overall end result of the query. +print("HighestSeverity: {}".format(rate_request.response.HighestSeverity)) + +# RateReplyDetails can contain rates for multiple ServiceTypes if ServiceType was set to None +for service in rate_request.response.RateReplyDetails: + for detail in service.RatedShipmentDetails: + for surcharge in detail.ShipmentRateDetail.Surcharges: + if surcharge.SurchargeType == 'OUT_OF_DELIVERY_AREA': + print("{}: ODA rate_request charge {}".format(service.ServiceType, surcharge.Amount.Amount)) + + for rate_detail in service.RatedShipmentDetails: + print("{}: Net FedEx Charge {} {}".format(service.ServiceType, + rate_detail.ShipmentRateDetail.TotalNetFedExCharge.Currency, + rate_detail.ShipmentRateDetail.TotalNetFedExCharge.Amount)) + +# Check for warnings, this is also logged by the base class. +if rate_request.response.HighestSeverity == 'NOTE': + for notification in rate_request.response.Notifications: + if notification.Severity == 'NOTE': + print(sobject_to_dict(notification)) diff --git a/examples/service_availability_request.py b/examples/service_availability_request.py index 5e17367..122ea81 100644 --- a/examples/service_availability_request.py +++ b/examples/service_availability_request.py @@ -25,8 +25,9 @@ avc_request.Origin.PostalCode = '29631' avc_request.Origin.CountryCode = 'US' + # Specify the destination postal code and country code. These fields are required. -avc_request.Destination.PostalCode = '27577' +avc_request.Destination.PostalCode = '20301' avc_request.Destination.CountryCode = 'US' # Can be set to FEDEX_TUBE, YOUR_PACKAGING, FEDEX_BOX etc.. Defaults to YOUR_PACKAGING if not set. diff --git a/fedex/services/rate_service.py b/fedex/services/rate_service.py index a4ca5f0..7605198 100644 --- a/fedex/services/rate_service.py +++ b/fedex/services/rate_service.py @@ -12,18 +12,18 @@ class FedexRateServiceRequest(FedexBaseService): """ - This class allows you to get the shipping charges for a particular address. - You will need to populate the data structures in self.RequestedShipment, + This class allows you to get the shipping charges for a particular address. + You will need to populate the data structures in self.RequestedShipment, then send the request. """ def __init__(self, config_obj, *args, **kwargs): """ - The optional keyword args detailed on L{FedexBaseService} + The optional keyword args detailed on L{FedexBaseService} apply here as well. @type config_obj: L{FedexConfig} - @param config_obj: A valid FedexConfig object. + @param config_obj: A valid FedexConfig object. """ self._config_obj = config_obj @@ -36,7 +36,7 @@ def __init__(self, config_obj, *args, **kwargs): """@ivar: Holds the RequestedShipment WSDL object including the shipper, recipient and shipt time.""" # Call the parent FedexBaseService class for basic setup work. super(FedexRateServiceRequest, self).__init__( - self._config_obj, 'RateService_v18.wsdl', *args, **kwargs) + self._config_obj, 'RateService_v18.wsdl', *args, **kwargs) self.ClientDetail.Region = config_obj.express_region_code """@ivar: Holds the express region code from the config object.""" @@ -93,25 +93,184 @@ def _prepare_wsdl_objects(self): def _assemble_and_send_request(self): """ Fires off the Fedex request. - - @warning: NEVER CALL THIS METHOD DIRECTLY. CALL send_request(), + + @warning: NEVER CALL THIS METHOD DIRECTLY. CALL send_request(), + WHICH RESIDES ON FedexBaseService AND IS INHERITED. + """ + + # Fire off the query. + return self.client.service.getRates( + WebAuthenticationDetail=self.WebAuthenticationDetail, + ClientDetail=self.ClientDetail, + TransactionDetail=self.TransactionDetail, + Version=self.VersionId, + RequestedShipment=self.RequestedShipment, + ReturnTransitAndCommit=self.ReturnTransitAndCommit) + + def add_package(self, package_item): + """ + Adds a package to the ship request. + + @type package_item: WSDL object, type of RequestedPackageLineItem + WSDL object. + @keyword package_item: A RequestedPackageLineItem, created by + calling create_wsdl_object_of_type('RequestedPackageLineItem') on + this ShipmentRequest object. See examples/create_shipment.py for + more details. + """ + + self.RequestedShipment.RequestedPackageLineItems.append(package_item) + package_weight = package_item.Weight.Value + self.RequestedShipment.TotalWeight.Value += package_weight + self.RequestedShipment.PackageCount += 1 + +# ################################################################### +# Rates Request Class for Fedex International Shipments ############# +# ################################################################### +class FedexInternationalRateServiceRequest(FedexBaseService): + """ + This class allows you to get the shipping charges for a particular address. + You will need to populate the data structures in self.RequestedShipment, + then send the request. + """ + + def __init__(self, config_obj, *args, **kwargs): + """ + The optional keyword args detailed on L{FedexBaseService} + apply here as well. + + @type config_obj: L{FedexConfig} + @param config_obj: A valid FedexConfig object. + """ + + self._config_obj = config_obj + + # Holds version info for the VersionId SOAP object. + self._version_info = {'service_id': 'crs', 'major': '18', + 'intermediate': '0', 'minor': '0'} + + self.RequestedShipment = None + """@ivar: Holds the RequestedShipment WSDL object including the shipper, recipient and shipt time.""" + # Call the parent FedexBaseService class for basic setup work. + super(FedexInternationalRateServiceRequest, self).__init__( + self._config_obj, 'RateService_v18.wsdl', *args, **kwargs) + self.ClientDetail.Region = config_obj.express_region_code + """@ivar: Holds the express region code from the config object.""" + + def _prepare_wsdl_objects(self): + """ + This is the data that will be used to create your shipment. Create + the data structure and get it ready for the WSDL request. + """ + + # Default behavior is to not request transit information + self.ReturnTransitAndCommit = False + + # This is the primary data structure for processShipment requests. + self.RequestedShipment = self.client.factory.create('RequestedShipment') + self.RequestedShipment.ShipTimestamp = datetime.datetime.now() + + # International options - CustomsClearanceDetail + customs_clearance_detail = self.client.factory.create('CustomsClearanceDetail') + + payor = self.client.factory.create('Payor') + # Grab the account number from the FedexConfig object by default. + # Assume US. + payor.ResponsibleParty = self.client.factory.create('Party') + payor.ResponsibleParty.Address = self.client.factory.create('Address') + payor.ResponsibleParty.Address.CountryCode = 'US' + + duty_payment= self.client.factory.create('Payment') + duty_payment.Payor = payor + duty_payment.PaymentType = 'SENDER' + customs_clearance_detail.DutiesPayment = duty_payment + + # Start with no commodities, user must add them. + customs_clearance_detail.Commodities = [] + + self.RequestedShipment.CustomsClearanceDetail = customs_clearance_detail + + + # Defaults for TotalWeight wsdl object. + total_weight = self.client.factory.create('Weight') + # Start at nothing. + total_weight.Value = 0.0 + # Default to pounds. + total_weight.Units = 'LB' + # This is the total weight of the entire shipment. Shipments may + # contain more than one package. + self.RequestedShipment.TotalWeight = total_weight + + # This is the top level data structure for Shipper information. + shipper = self.client.factory.create('Party') + shipper.Address = self.client.factory.create('Address') + shipper.Contact = self.client.factory.create('Contact') + + # Link the ShipperParty to our master data structure. + self.RequestedShipment.Shipper = shipper + + # This is the top level data structure for Recipient information. + recipient_party = self.client.factory.create('Party') + recipient_party.Contact = self.client.factory.create('Contact') + recipient_party.Address = self.client.factory.create('Address') + # Link the RecipientParty object to our master data structure. + self.RequestedShipment.Recipient = recipient_party + + # Make sender responsible for payment by default. + self.RequestedShipment.ShippingChargesPayment = self.create_wsdl_object_of_type('Payment') + self.RequestedShipment.ShippingChargesPayment.PaymentType = 'SENDER' + + # payor properties + payor = self.client.factory.create('Payor') + # Grab the account number from the FedexConfig object by default. + # Assume US. + payor.ResponsibleParty = self.client.factory.create('Party') + payor.ResponsibleParty.Address = self.client.factory.create('Address') + payor.ResponsibleParty.Address.CountryCode = 'US' + + # International options - CustomsClearanceDetail + customs_clearance_detail = self.client.factory.create('CustomsClearanceDetail') + + duty_payment = self.client.factory.create('Payment') + duty_payment.Payor = payor + duty_payment.PaymentType = 'SENDER' + customs_clearance_detail.DutiesPayment = duty_payment + + # Start with no commodities, user must add them. + customs_clearance_detail.Commodities = [] + + self.RequestedShipment.CustomsClearanceDetail = customs_clearance_detail + + # Start with no packages, user must add them. + self.RequestedShipment.PackageCount = 0 + self.RequestedShipment.RequestedPackageLineItems = [] + + # This is good to review if you'd like to see what the data structure + # looks like. + self.logger.debug(self.RequestedShipment) + + def _assemble_and_send_request(self): + """ + Fires off the Fedex request. + + @warning: NEVER CALL THIS METHOD DIRECTLY. CALL send_request(), WHICH RESIDES ON FedexBaseService AND IS INHERITED. """ # Fire off the query. return self.client.service.getRates( - WebAuthenticationDetail=self.WebAuthenticationDetail, - ClientDetail=self.ClientDetail, - TransactionDetail=self.TransactionDetail, - Version=self.VersionId, - RequestedShipment=self.RequestedShipment, - ReturnTransitAndCommit=self.ReturnTransitAndCommit) + WebAuthenticationDetail=self.WebAuthenticationDetail, + ClientDetail=self.ClientDetail, + TransactionDetail=self.TransactionDetail, + Version=self.VersionId, + RequestedShipment=self.RequestedShipment, + ReturnTransitAndCommit=self.ReturnTransitAndCommit) def add_package(self, package_item): """ Adds a package to the ship request. - - @type package_item: WSDL object, type of RequestedPackageLineItem + + @type package_item: WSDL object, type of RequestedPackageLineItem WSDL object. @keyword package_item: A RequestedPackageLineItem, created by calling create_wsdl_object_of_type('RequestedPackageLineItem') on diff --git a/fedex/services/ship_service.py b/fedex/services/ship_service.py index 43b9767..101c15a 100644 --- a/fedex/services/ship_service.py +++ b/fedex/services/ship_service.py @@ -20,11 +20,11 @@ class FedexProcessShipmentRequest(FedexBaseService): def __init__(self, config_obj, *args, **kwargs): """ - The optional keyword args detailed on L{FedexBaseService} + The optional keyword args detailed on L{FedexBaseService} apply here as well. @type config_obj: L{FedexConfig} - @param config_obj: A valid FedexConfig object. + @param config_obj: A valid FedexConfig object. """ self._config_obj = config_obj @@ -116,9 +116,9 @@ def send_validation_request(self): def _assemble_and_send_validation_request(self): """ Fires off the Fedex shipment validation request. - - @warning: NEVER CALL THIS METHOD DIRECTLY. CALL - send_validation_request(), WHICH RESIDES ON FedexBaseService + + @warning: NEVER CALL THIS METHOD DIRECTLY. CALL + send_validation_request(), WHICH RESIDES ON FedexBaseService AND IS INHERITED. """ @@ -133,8 +133,8 @@ def _assemble_and_send_validation_request(self): def _assemble_and_send_request(self): """ Fires off the Fedex request. - - @warning: NEVER CALL THIS METHOD DIRECTLY. CALL send_request(), + + @warning: NEVER CALL THIS METHOD DIRECTLY. CALL send_request(), WHICH RESIDES ON FedexBaseService AND IS INHERITED. """ @@ -149,8 +149,8 @@ def _assemble_and_send_request(self): def add_package(self, package_item): """ Adds a package to the ship request. - - @type package_item: WSDL object, type of RequestedPackageLineItem + + @type package_item: WSDL object, type of RequestedPackageLineItem WSDL object. @keyword package_item: A RequestedPackageLineItem, created by calling create_wsdl_object_of_type('RequestedPackageLineItem') on @@ -163,6 +163,193 @@ def add_package(self, package_item): self.RequestedShipment.TotalWeight.Value += package_weight self.RequestedShipment.PackageCount += 1 +# ################################################################### +# Fedex International Shipment Requrest Class ####################### +# ################################################################### +class FedexProcessInternationalShipmentRequest(FedexBaseService): + """ + This class allows you to process (create) a new International FedEx shipment. You will + need to populate the data structures in self.RequestedShipment, then + send the request. Label printing is supported and very configurable, + returning an ASCII representation with the response as well. + Note: The current limitation is limited to sending Commodity goods only. No document sending supported + """ + + def __init__(self, config_obj, *args, **kwargs): + """ + The optional keyword args detailed on L{FedexBaseService} + apply here as well. + + @type config_obj: L{FedexConfig} + @param config_obj: A valid FedexConfig object. + """ + + self._config_obj = config_obj + # Holds version info for the VersionId SOAP object. + self._version_info = { + 'service_id': 'ship', + 'major': '17', + 'intermediate': '0', + 'minor': '0' + } + self.RequestedShipment = None + """@ivar: Holds the RequestedShipment WSDL object.""" + # Call the parent FedexBaseService class for basic setup work. + super(FedexProcessInternationalShipmentRequest, self).__init__( + self._config_obj, 'ShipService_v17.wsdl', *args, **kwargs) + + def _prepare_wsdl_objects(self): + """ + This is the data that will be used to create your shipment. Create + the data structure and get it ready for the WSDL request. + """ + + # This is the primary data structure for processShipment requests. + self.RequestedShipment = self.client.factory.create('RequestedShipment') + self.RequestedShipment.ShipTimestamp = datetime.datetime.now() + # INTERNATIONAL PRIORITY SHIPMENT SERVICE ASSUMED BY DEFAULT + self.RequestedShipment.ServiceType = "INTERNATIONAL_PRIORITY" + + # Defaults for TotalWeight wsdl object. + total_weight = self.client.factory.create('Weight') + # Start at nothing. + total_weight.Value = 0.0 + # Default to pounds. + total_weight.Units = 'LB' + # This is the total weight of the entire shipment. Shipments may + # contain more than one package. + self.RequestedShipment.TotalWeight = total_weight + + # This is the top level data structure Shipper Party information. + shipper_party = self.client.factory.create('Party') + shipper_party.Address = self.client.factory.create('Address') + shipper_party.Contact = self.client.factory.create('Contact') + + # Link the Shipper Party to our master data structure. + self.RequestedShipment.Shipper = shipper_party + + # This is the top level data structure for RecipientParty information. + recipient_party = self.client.factory.create('Party') + recipient_party.Contact = self.client.factory.create('Contact') + recipient_party.Address = self.client.factory.create('Address') + + # Link the RecipientParty object to our master data structure. + self.RequestedShipment.Recipient = recipient_party + + payor = self.client.factory.create('Payor') + # Grab the account number from the FedexConfig object by default. + # Assume US. + payor.ResponsibleParty = self.client.factory.create('Party') + payor.ResponsibleParty.Address = self.client.factory.create('Address') + payor.ResponsibleParty.Address.CountryCode = 'US' + + # ShippingChargesPayment WSDL object default values. + shipping_charges_payment = self.client.factory.create('Payment') + shipping_charges_payment.Payor = payor + shipping_charges_payment.PaymentType = 'SENDER' + self.RequestedShipment.ShippingChargesPayment = shipping_charges_payment + + self.RequestedShipment.LabelSpecification = self.client.factory.create('LabelSpecification') + + # International options - CustomsClearanceDetail + customs_clearance_detail = self.client.factory.create('CustomsClearanceDetail') + + duty_payment= self.client.factory.create('Payment') + duty_payment.Payor = payor + duty_payment.PaymentType = 'SENDER' + customs_clearance_detail.DutiesPayment = duty_payment + + # Start with no commodities, user must add them. + customs_clearance_detail.Commodities = [] + + self.RequestedShipment.CustomsClearanceDetail = customs_clearance_detail + + # NONE, PREFERRED or LIST + self.RequestedShipment.RateRequestTypes = ['PREFERRED'] + + # Start with no packages, user must add them. + self.RequestedShipment.PackageCount = 0 + self.RequestedShipment.RequestedPackageLineItems = [] + + # This is good to review if you'd like to see what the data structure + # looks like. + self.logger.debug(self.RequestedShipment) + + def send_validation_request(self): + """ + This is very similar to just sending the shipment via the typical + send_request() function, but this doesn't create a shipment. It is + used to make sure "good" values are given by the user or the + application using the library. + """ + + self.send_request(send_function=self._assemble_and_send_validation_request) + + def _assemble_and_send_validation_request(self): + """ + Fires off the Fedex shipment validation request. + + @warning: NEVER CALL THIS METHOD DIRECTLY. CALL + send_validation_request(), WHICH RESIDES ON FedexBaseService + AND IS INHERITED. + """ + + # Fire off the query. + return self.client.service.validateShipment( + WebAuthenticationDetail=self.WebAuthenticationDetail, + ClientDetail=self.ClientDetail, + TransactionDetail=self.TransactionDetail, + Version=self.VersionId, + RequestedShipment=self.RequestedShipment) + + def _assemble_and_send_request(self): + """ + Fires off the Fedex request. + + @warning: NEVER CALL THIS METHOD DIRECTLY. CALL send_request(), + WHICH RESIDES ON FedexBaseService AND IS INHERITED. + """ + + # Fire off the query. + return self.client.service.processShipment( + WebAuthenticationDetail=self.WebAuthenticationDetail, + ClientDetail=self.ClientDetail, + TransactionDetail=self.TransactionDetail, + Version=self.VersionId, + RequestedShipment=self.RequestedShipment) + + def add_package(self, package_item): + """ + Adds a package to the ship request. + + @type package_item: WSDL object, type of RequestedPackageLineItem + WSDL object. + @keyword package_item: A RequestedPackageLineItem, created by + calling create_wsdl_object_of_type('RequestedPackageLineItem') on + this ShipmentRequest object. See examples/create_shipment.py for + more details. + """ + + self.RequestedShipment.RequestedPackageLineItems.append(package_item) + package_weight = package_item.Weight.Value + self.RequestedShipment.TotalWeight.Value += package_weight + self.RequestedShipment.PackageCount += 1 + + def add_commodity(self, commodity_item): + """ + Adds a commodity item to the ship request. + + @type commodity_item: WSDL object, type of Commodity - FIXIME + WSDL object. + @keyword commodity_item: A Commodity, created by + calling create_wsdl_object_of_type('Commodity') on + this ShipmentRequest object. See examples/create_international_shipment.py for + more details. + """ + + self.RequestedShipment.CustomsClearanceDetail.Commodities.append(commodity_item) + + class FedexDeleteShipmentRequest(FedexBaseService): """ diff --git a/tests/test_rate_service.py b/tests/test_rate_service.py index 2af0896..bc08da8 100644 --- a/tests/test_rate_service.py +++ b/tests/test_rate_service.py @@ -8,7 +8,7 @@ sys.path.insert(0, '..') from fedex.services.rate_service import FedexRateServiceRequest - +from fedex.services.rate_service import FedexInternationalRateServiceRequest # Common global config object for testing. from tests.common import get_fedex_config @@ -56,6 +56,47 @@ def test_rate(self): assert rate.response.HighestSeverity == 'SUCCESS', rate.response.Notifications[0].Message +def test_rate_international(self): + rate = FedexInternationalRateServiceRequest(CONFIG_OBJ) + + rate.RequestedShipment.DropoffType = 'REGULAR_PICKUP' + rate.RequestedShipment.ServiceType = 'FEDEX_GROUND' + rate.RequestedShipment.PackagingType = 'YOUR_PACKAGING' + + rate.RequestedShipment.Shipper.Address.StateOrProvinceCode = 'SC' + rate.RequestedShipment.Shipper.Address.PostalCode = '29631' + rate.RequestedShipment.Shipper.Address.CountryCode = 'US' + + rate.RequestedShipment.Recipient.Address.StateOrProvinceCode = '' + rate.RequestedShipment.Recipient.Address.PostalCode = '04070' + rate.RequestedShipment.Recipient.Address.CountryCode = 'UA' + + rate.ReturnTransitAndCommit = True + rate.RequestedShipment.ServiceType = 'INTERNATIONAL_PRIORITY' + rate.RequestedShipment.RateRequestTypes = 'LIST' + + rate.RequestedShipment.CustomsClearanceDetail.CustomsValue.Amount = 100 + rate.RequestedShipment.CustomsClearanceDetail.CustomsValue.Currency = "USD" + + rate.RequestedShipment.CustomsClearanceDetail.DutiesPayment.Payor.ResponsibleParty.AccountNumber = CONFIG_OBJ.account_number + rate.RequestedShipment.CustomsClearanceDetail.DutiesPayment.PaymentType = 'SENDER' + + rate.RequestedShipment.EdtRequestType = 'NONE' + rate.RequestedShipment.ShippingChargesPayment.PaymentType = 'SENDER' + + package1_weight = rate.create_wsdl_object_of_type('Weight') + package1_weight.Value = 1.0 + package1_weight.Units = "LB" + package1 = rate.create_wsdl_object_of_type('RequestedPackageLineItem') + package1.Weight = package1_weight + package1.PhysicalPackaging = 'BOX' + package1.GroupPackageCount = 1 + rate.add_package(package1) + + rate.send_request() + + assert rate.response.HighestSeverity == 'SUCCESS', rate.response.Notifications[0].Message + if __name__ == "__main__": logging.basicConfig(stream=sys.stdout, level=logging.INFO) unittest.main() diff --git a/tests/test_ship_service.py b/tests/test_ship_service.py index ac93dd2..c67bdcf 100644 --- a/tests/test_ship_service.py +++ b/tests/test_ship_service.py @@ -8,6 +8,7 @@ sys.path.insert(0, '..') from fedex.services.ship_service import FedexProcessShipmentRequest +from fedex.services.ship_service import FedexProcessInternationalShipmentRequest from fedex.services.ship_service import FedexDeleteShipmentRequest # Common global config object for testing. @@ -89,6 +90,98 @@ def test_create_delete_shipment(self): assert del_shipment.response + def test_create_delete_international_shipment(self): + shipment = FedexProcessInternationalShipmentRequest(CONFIG_OBJ) + + shipment.RequestedShipment.DropoffType = 'BUSINESS_SERVICE_CENTER' + shipment.RequestedShipment.ServiceType = 'INTERNATIONAL_PRIORITY' + shipment.RequestedShipment.PackagingType = 'FEDEX_BOX' + + shipment.RequestedShipment.Shipper.Contact.PersonName = 'Sender Name' + shipment.RequestedShipment.Shipper.Contact.PhoneNumber = '9012638716' + + shipment.RequestedShipment.Shipper.Address.StreetLines = ['Address Line 1'] + shipment.RequestedShipment.Shipper.Address.City = 'Herndon' + shipment.RequestedShipment.Shipper.Address.StateOrProvinceCode = 'VA' + shipment.RequestedShipment.Shipper.Address.PostalCode = '20171' + shipment.RequestedShipment.Shipper.Address.CountryCode = 'US' + + shipment.RequestedShipment.Recipient.Contact.PersonName = 'Recipient Name' + shipment.RequestedShipment.Recipient.Contact.PhoneNumber = '00380445010919' + + shipment.RequestedShipment.Recipient.Address.StreetLines = ['Address Line 1'] + shipment.RequestedShipment.Recipient.Address.City = 'Kyiv' + shipment.RequestedShipment.Recipient.Address.StateOrProvinceCode = '' + shipment.RequestedShipment.Recipient.Address.PostalCode = '04070' + shipment.RequestedShipment.Recipient.Address.CountryCode = 'UA' + shipment.RequestedShipment.EdtRequestType = 'NONE' + + shipment.RequestedShipment.ShippingChargesPayment.Payor.ResponsibleParty.AccountNumber \ + = CONFIG_OBJ.account_number + + shipment.RequestedShipment.ShippingChargesPayment.PaymentType = 'SENDER' + + # Create Weight, in pounds. + package1_weight = shipment.create_wsdl_object_of_type('Weight') + package1_weight.Value = 5.0 + package1_weight.Units = "LB" + + package1 = shipment.create_wsdl_object_of_type('RequestedPackageLineItem') + package1.PhysicalPackaging = 'ENVELOPE' + package1.Weight = package1_weight + shipment.add_package(package1) + + # international shipment options + + shipment.RequestedShipment.CustomsClearanceDetail.DutiesPayment.Payor.ResponsibleParty.AccountNumber = CONFIG_OBJ.account_number + shipment.RequestedShipment.CustomsClearanceDetail.DutiesPayment.PaymentType = 'SENDER' + + quantity = 5 + + commodity = shipment.create_wsdl_object_of_type('Commodity') + commodity.Name = "Books" + commodity.NumberOfPieces = quantity + commodity.Description = "Books for a present" + commodity.CountryOfManufacture = "US" + commodity.Weight = package1_weight + commodity.Quantity = quantity + commodity.QuantityUnits = 'EA' # EACH - for items measured in units + + commodity.UnitPrice.Currency = "USD" + commodity.UnitPrice.Amount = 10 + + commodity.CustomsValue.Currency = "USD" + commodity.CustomsValue.Amount = quantity * commodity.UnitPrice.Amount + + shipment.RequestedShipment.CustomsClearanceDetail.CustomsValue.Amount = commodity.CustomsValue.Amount + shipment.RequestedShipment.CustomsClearanceDetail.CustomsValue.Currency = commodity.CustomsValue.Currency + + shipment.add_commodity(commodity) + + shipment.RequestedShipment.LabelSpecification.LabelFormatType = 'COMMON2D' + shipment.RequestedShipment.LabelSpecification.ImageType = 'PNG' + shipment.RequestedShipment.LabelSpecification.LabelStockType = 'PAPER_7X4.75' + shipment.RequestedShipment.LabelSpecification.LabelPrintingOrientation = 'BOTTOM_EDGE_OF_TEXT_FIRST' + + # Use order if setting multiple labels or delete + del shipment.RequestedShipment.LabelSpecification.LabelOrder + + shipment.send_validation_request() + shipment.send_request() + + assert shipment.response + assert shipment.response.HighestSeverity in ['SUCCESS', 'WARNING'] + track_id = shipment.response.CompletedShipmentDetail.CompletedPackageDetails[0].TrackingIds[0].TrackingNumber + assert track_id + + del_shipment = FedexDeleteShipmentRequest(CONFIG_OBJ) + del_shipment.DeletionControlType = "DELETE_ALL_PACKAGES" + del_shipment.TrackingId.TrackingNumber = track_id + del_shipment.TrackingId.TrackingIdType = 'EXPRESS' + + del_shipment.send_request() + + assert del_shipment.response if __name__ == "__main__": logging.basicConfig(stream=sys.stdout, level=logging.INFO)