'''
FedEx Shipping Module
v0.4
By Neum Schmickrath - www.pageworthy.com
You must have a Fedex account to use this module.
You may register at fedex.com
'''
from decimal import Decimal
from django.utils.translation import ugettext as _
from django.utils.safestring import mark_safe
from django.template import loader, Context
from django.core.cache import cache
from shipping.modules.base import BaseShipper
from shipping import signals
from livesettings import config_get_group
import urllib2
from xml.dom import minidom
import logging
log = logging.getLogger('fedex.shipper')
class Shipper(BaseShipper):
def __init__(self, cart=None, contact=None, service_type=None):
self._calculated = False
self.cart = cart
self.contact = contact
self.raw_results = '(not processed)'
if service_type:
self.service_type_code = service_type[0]
self.service_type_text = service_type[1]
else:
self.service_type_code = '99'
self.service_type_text = 'Uninitialized'
# Had to edit this so the shipping name did not error out for being more than 30 characters. Old code is commented out.
#self.id = u'FedEx-%s-%s' % (self.service_type_code, self.service_type_text)
self.id = u'%s' % (self.service_type_text)
#if cart or contact:
# self.calculate(cart, contact)
def __str__(self):
'''
This is mainly helpful for debugging purposes
'''
return 'FedEx'
def __unicode__(self):
'''
As is this.
'''
return 'FedEx'
def description(self):
'''
A basic description that will be displayed to the user when
selecting their shipping options
'''
return _('FedEx - %s' % self.service_type_text)
def cost(self):
'''
Complex calculations can be done here as long as the return
value is a decimal figure
'''
assert(self._calculated)
return(Decimal(self.charges))
def method(self):
'''
Describes the actual delivery service (Mail, FedEx, DHL, UPS, etc)
'''
return _('FedEx')
def expectedDelivery(self):
'''
Can be a plain string or complex calcuation
returning an actual date
'''
if self.delivery_days <> '1':
return _('%s business days' % self.delivery_days)
else:
return _('%s business day' % self.delivery_days)
def valid(self, order=None):
'''
Can do complex validation about whether or not this
option is valid. For example, may check to see if the
recipient is in an allowed country or location.
'''
return self.is_valid
def _check_for_error(self, response):
'''
Check XML response, see if it indicates an error.
Expects 'response' to already have been run through
minidom.parseString()
'''
if response.getElementsByTagName('Error'):
# we have an error!
error_code = response.getElementsByTagName('Error')[0].getElementsByTagName('Code')[0].firstChild.nodeValue
error_mesg = response.getElementsByTagName('Error')[0].getElementsByTagName('Message')[0].firstChild.nodeValue
log.info('Fedex Error: %s - Code: %s', error_mesg, error_code)
return (error_mesg, error_code)
else:
# all clear.
return False
def _process_request(self, connection, request):
'''
Post the data and return the XML response
'''
conn = urllib2.Request(url=connection, data=request)
f = urllib2.urlopen(conn)
all_results = f.read()
self.raw_response = all_results
return(minidom.parseString(all_results))
def calculate(self, cart, contact):
'''
Based on the chosen Fedex method, we will do our call(s)
to FedEx and see how much it will cost. We will also need
to store the results for further parsing and return via the
methods above.
'''
log.debug("Starting fedex calculations")
from satchmo_store.shop.models import Config
settings = config_get_group('shipping.modules.fedex')
verbose = settings.VERBOSE_LOG.value
self.delivery_days = _('3 - 4') #Default setting for ground delivery
shop_details = Config.objects.get_current()
self.packaging = ''
# FedEx Ground Home Delivery Packaging must be YOURPACKAGING only.
if self.service_type_code in ('FEDEXGROUND', 'GROUNDHOMEDELIVERY'):
self.packaging = 'YOURPACKAGING'
else:
self.packaging = settings.SHIPPING_PACKAGE.value
if verbose:
log.debug('Calculating fedex with type=%s, packaging=%s', self.service_type_code, self.packaging)
self.is_valid = False
error = False
if not settings.ACCOUNT.value:
log.warn("No fedex account found in settings")
return
if not settings.METER_NUMBER.value:
log.warn("No fedex meter number found in settings")
return
configuration = {
'account': settings.ACCOUNT.value,
'meter': settings.METER_NUMBER.value,
'packaging': self.packaging,
'ship_type': self.service_type_code,
'shop_details':shop_details,
}
if settings.LIVE.value:
connection = settings.CONNECTION.value
else:
connection = settings.CONNECTION_TEST.value
self.charges = 0
box_weight_units = "LB"
# FedEx requires that the price be formatted to 2 decimal points.
# e.g., 1.00, 10.40, 3.50
# They also require that the weight be one decimal point.
# e.g., 1.0, 2.3, 10.4
if settings.SINGLE_BOX.value:
if verbose:
log.debug("Using single-box method for fedex calculations.")
box_price = Decimal("0.00")
box_weight = Decimal("0.00")
for product in cart.get_shipment_list():
box_price += product.unit_price
if product.weight is None:
log.warn("No weight on product (skipping for ship calculations): %s", product)
else:
box_weight += product.weight
if product.weight_units and product.weight_units != "":
box_weight_units = product.weight_units
if box_weight < Decimal("0.1"):
log.debug("Total box weight too small, defaulting to 0.1")
box_weight = Decimal("0.1")
shippingdata = {
'config': configuration,
'box_price': '%.2f' % box_price,
'box_weight' : '%.1f' % box_weight,
'box_weight_units' : box_weight_units.upper(),
'contact': contact,
'shipping_address' : shop_details,
'shipping_phone' : shop_details.phone,
'shipping_country_code' : shop_details.country.iso2_code
}
signals.shipping_data_query.send(Shipper, shipper=self, cart=cart, shippingdata=shippingdata)
c = Context(shippingdata)
t = loader.get_template('shipping/fedex/request.xml')
request = t.render(c)
try:
response = self._process_request(connection, request)
error = self._check_for_error(response)
if verbose:
log.debug("Fedex request: %s", request)
log.debug("Fedex response: %s", self.raw_response)
if not error:
this_charge = float(response.documentElement.getElementsByTagName('NetCharge')[0].firstChild.nodeValue)
this_discount = float(response.documentElement.getElementsByTagName('EffectiveNetDiscount')[0].firstChild.nodeValue)
self.delivery_days = response.documentElement.getElementsByTagName('TimeInTransit')[0].firstChild.nodeValue
total_cost = this_charge + this_discount
self.charges += total_cost
except urllib2.URLError:
log.warn("Error opening url: %s", connection)
error = True
else:
# process each shippable separately
# I'm not certain why FedEx implemented their 'Web Service'
# this way. However, you can't give FedEx a list of boxes
# and get back a list of prices (as you can with UPS).
# Each box has to be a completely new transaction - that
# is, a separate POST to their server.
#
# So, to simulate this functionality, and return a total
# price, we have to loop through all of our items, and
# pray the customer isn't ordering a thousand boxes of bagels.
for product in cart.get_shipment_list():
c = Context({
'config': configuration,
'box_weight' : '%.1f' % (product.weight or 0.0),
'box_weight_units' : product.weight_units and product.weight_units.upper() or 'LB',
'box_price' : '%.2f' % product.unit_price,
'contact': contact,
})
t = loader.get_template('shipping/fedex/request.xml')
request = t.render(c)
response = self._process_request(connection, request)
error = self._check_for_error(response)
if verbose:
log.debug("Fedex request: %s", request)
log.debug("Fedex response: %s", self.raw_response)
if not error:
this_charge = float(response.documentElement.getElementsByTagName('NetCharge')[0].firstChild.nodeValue)
this_discount = float(response.documentElement.getElementsByTagName('EffectiveNetDiscount')[0].firstChild.nodeValue)
self.delivery_days = response.documentElement.getElementsByTagName('TimeInTransit')[0].firstChild.nodeValue
total_cost = this_charge + this_discount
self.charges += total_cost
else:
break
if not error:
self.charges = str(self.charges)
self.is_valid = True
self._calculated = True