Commits

Bruce Kroeze committed d472293

adding UPS time_to_transit as an option for UPS shipper

  • Participants
  • Parent commits 25dee90

Comments (0)

Files changed (5)

File satchmo/apps/product/modules/custom/models.py

     def translated_name(self, language_code=None):
         return lookup_translation(self, 'name', language_code)
 
+    def __unicode__(self):
+        return u"CustomText: %s" % self.name
+
     class Meta:
         ordering = ('sort_order',)
         unique_together = ('slug', 'products')

File satchmo/apps/satchmo_store/shop/views/cart.py

             else:
                 price_change = zero
             data = { 'name' : customfield.translated_name(),
-                     'value' : formdata["custom_%s" % customfield.slug],
+                     'value' : formdata.get("custom_%s" % customfield.slug, ''),
                      'sort_order': customfield.sort_order,
                      'price_change': price_change }
             details.append(data)

File satchmo/apps/shipping/modules/ups/config.py

         description=_("UPS XML Access Key"),
         help_text=_("XML Access Key Provided by UPS"),
         default=u""),
-    
+
     StringValue(SHIPPING_GROUP,
         'USER_ID',
         description=_("UPS User ID"),
         description=_("UPS Account Number"),
         help_text=_("UPS Account Number."),
         default=u""),
-    
+
     StringValue(SHIPPING_GROUP,
         'USER_PASSWORD',
         description=_("UPS User Password"),
         help_text=_("User password provided by UPS site."),
         default=u""),
-    
+
     MultipleStringValue(SHIPPING_GROUP,
         'UPS_SHIPPING_CHOICES',
         description=_("UPS Shipping Choices Available to customers. These are valid domestic codes only."),
         help_text=_("Use just one box and ship by weight?  If no then every item will be sent in its own box."),
         default=True),
 
+    BooleanValue(SHIPPING_GROUP,
+        'TIME_IN_TRANSIT',
+        description=_("Time in Transit?"),
+        help_text=_("Use the UPS Time In Transit API? It is slower but delivery dates are more accurate."),
+        default=False),
+
     StringValue(SHIPPING_GROUP,
         'PICKUP_TYPE',
         description=_("UPS Pickup option."),

File satchmo/apps/shipping/modules/ups/shipper.py

 """
 
 from decimal import Decimal
-from django.core.cache import cache
 from django.template import Context, loader
 from django.utils.translation import ugettext as _
 from livesettings import config_get_group, config_value
+from keyedcache import cache_key, cache_get, cache_set, NotCachedError
 from shipping import signals
 from shipping.modules.base import BaseShipper
+import datetime
 import logging
 import urllib2
 
     from elementtree.ElementTree import fromstring, tostring
 
 log = logging.getLogger('ups.shipper')
+
+# map the codes returned by the time in transit api to the ones in the shipping api (sigh)
+# even worse is that the published codes in their API don't match what we actually get.
+#
+# reference:
+# 1DA=UPS Next Day Air
+# 1DAS=UPS Next Day Air (Saturday Delivery)
+# 1DM=UPS Next Day Air Early A.M.
+# 1DMS=UPS Next Day Air Early A.M. (Saturday Delivery)
+# 1DP=UPS Next Day Air Saver
+# 2DA=UPS 2nd Day Air
+# 3DS=UPS 3 Day Select
+# GND=UPS Ground
+
+TRANSIT_CODE_MAP = {
+    '1DA' : '01',
+    '2DA' : '02',
+    'GND' : '03',
+    '3DS' : '12',
+    '1DP' : '13',
+    '1DM' : '14',
+    '2DM' : '59'  #guessing, I've never seen this code come back
+    }
+
 class Shipper(BaseShipper):
-    
+
     def __init__(self, cart=None, contact=None, service_type=None):
         self._calculated = False
         self.cart = cart
         self.contact = contact
-        if service_type:        
+        if service_type:
             self.service_type_code = service_type[0]
             self.service_type_text = service_type[1]
         else:
         self.raw = "NO DATA"
         #if cart or contact:
         #    self.calculate(cart, contact)
-    
+
     def __str__(self):
         """
         This is mainly helpful for debugging purposes
         """
         return "UPS"
-        
+
     def description(self):
         """
         A basic description that will be displayed to the user when selecting their shipping options
             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.
         all_results = f.read()
         self.raw = all_results
         return(fromstring(all_results))
-        
+
     def calculate(self, cart, contact):
         """
         Based on the chosen UPS method, we will do our call to UPS and see how much it will
         methods above
         """
         from satchmo_store.shop.models import Config
-        
+
         settings =  config_get_group('shipping.modules.ups')
         self.delivery_days = _("3 - 4") #Default setting for ground delivery
         shop_details = Config.objects.get_current()
             'ship_type': self.service_type_code,
             'shop_details':shop_details,
         }
-        
+
         shippingdata = {
             'single_box': False,
             'config': configuration,
             'shipping_phone' : shop_details.phone,
             'shipping_country_code' : shop_details.country.iso2_code
             }
-            
+
         if settings.SINGLE_BOX.value:
             log.debug("Using single-box method for ups calculations.")
 
             shippingdata['box_weight'] = '%.1f' % box_weight
             shippingdata['box_weight_units'] = box_weight_units.upper()
 
+        total_weight = 0
+        for product in cart.get_shipment_list():
+            try:
+                total_weight += product.smart_attr('weight')
+            except TypeError:
+                pass
+
         signals.shipping_data_query.send(Shipper, shipper=self, cart=cart, shippingdata=shippingdata)
         c = Context(shippingdata)
         t = loader.get_template('shipping/ups/request.xml')
             connection = settings.CONNECTION.value
         else:
             connection = settings.CONNECTION_TEST.value
-        cache_key_response = "ups-cart-%s-response" % int(cart.id)
-        cache_key_request = "ups-cart-%s-request" % int(cart.id)
-        last_request = cache.get(cache_key_request)
-        tree = cache.get(cache_key_response)
 
-        if (last_request != request) or tree is None:
-            self.verbose_log("Requesting from UPS [%s]\n%s", cache_key_request, request)
-            cache.set(cache_key_request, request, 60)
+        cachekey = cache_key(
+            'UPS_SHIP',
+            #service_type = self.service_type_code,
+            weight = str(total_weight),
+            country = shop_details.country.iso2_code,
+            zipcode = contact.shipping_address.postal_code)
+
+        try:
+            tree = cache_get(cachekey)
+        except NotCachedError:
+            tree = None
+
+        if tree is not None:
+            self.verbose_log('Got UPS info from cache [%s]', cachekey)
+        else:
+            self.verbose_log("Requesting from UPS [%s]\n%s", cachekey, request)
+            cache_set(cachekey, value=request, length=600)
             tree = self._process_request(connection, request)
-            self.verbose_log("Got from UPS [%s]:\n%s", cache_key_response, self.raw)
-            needs_cache = True
-        else:
-            needs_cache = False
+            self.verbose_log("Got from UPS [%s]:\n%s", cachekey, self.raw)
+            cache_set(cachekey, value=tree)
 
         try:
             status_code = tree.getiterator('ResponseStatusCode')
             self.verbose_log("UPS Status Code for cart #%s = %s", int(cart.id), status_val)
         except AttributeError:
             status_val = "-1"
-        
+
         if status_val == '1':
             self.is_valid = False
             self._calculated = False
                         self.delivery_days = response.find('.//GuaranteedDaysToDelivery').text
                     self.is_valid = True
                     self._calculated = True
-                    if needs_cache:
-                        cache.set(cache_key_response, tree, 60)
-                        
+
             if not self.is_valid:
                 self.verbose_log("UPS Cannot find rate for code: %s [%s]", self.service_type_code, self.service_type_text)
-        
+
         else:
             self.is_valid = False
             self._calculated = False
                 log.info("UPS %s Error: Code %s - %s" % (errors[0].text, errors[1].text, errors[2].text))
             except AttributeError:
                 log.info("UPS error - cannot parse response:\n %s", self.raw)
-            
+
+        if self.is_valid and settings.TIME_IN_TRANSIT.value:
+            self.verbose_log('Now getting time in transit for cart')
+            self.time_in_transit(contact, cart)
+
+    def time_in_transit(self, contact, cart):
+        total = Decimal('0')
+        for item in cart.cartitem_set.all():
+            if item.is_shippable:
+                total += item.line_total
+
+        delivery_days = self.ups_time_in_transit(contact, price=total)
+        if delivery_days is not None:
+            self.delivery_days = delivery_days
+
+    def ups_time_in_transit(self, contact, pickup_date = None, price=None, test=False):
+        """Calculate est delivery days for a zipcode, from Store Zipcode"""
+
+        from satchmo_store.shop.models import Config
+
+        delivery_days = None
+
+        if pickup_date is None:
+            pickup_date = datetime.datetime.now() + datetime.timedelta(days=1)
+
+        if price is None:
+            price = Decimal('10.0')
+
+        shipaddr = contact.shipping_address
+        shop_details = Config.objects.get_current()
+        settings =  config_get_group('shipping.modules.ups')
+
+        configuration = {
+            'xml_key': settings.XML_KEY.value,
+            'account': settings.ACCOUNT.value,
+            'userid': settings.USER_ID.value,
+            'password': settings.USER_PASSWORD.value,
+            'container': settings.SHIPPING_CONTAINER.value,
+            'pickup': settings.PICKUP_TYPE.value,
+            'shop_details':shop_details,
+        }
+
+        shippingdata = {
+            'config': configuration,
+            'zipcode': shipaddr.postal_code,
+            'contact': contact,
+            'shipping_address' : shop_details,
+            'shipping_phone' : shop_details.phone,
+            'shipping_country_code' : shop_details.country.iso2_code,
+            'pickup_date' : pickup_date.strftime('%Y%m%d'),
+            'price' : "%.2f" % price
+        }
+
+        c = Context(shippingdata)
+        t = loader.get_template('shipping/ups/transit_request.xml')
+        request = t.render(c)
+
+        if settings.LIVE.value and not test:
+            connection = 'https://wwwcie.ups.com/ups.app/xml/TimeInTransit'
+        else:
+            connection = 'https://onlinetools.ups.com/ups.app/xml/TimeInTransit'
+
+        cachekey = cache_key("UPS-TIT", shipaddr.postal_code, pickup_date, price)
+
+        try:
+            ups = cache_get(cachekey)
+        except NotCachedError:
+            ups = None
+
+        if ups is None:
+            log.debug('Requesting from UPS: %s\n%s', connection, request)
+            conn = urllib2.Request(url=connection, data=request.encode("utf-8"))
+            f = urllib2.urlopen(conn)
+            all_results = f.read()
+
+            self.verbose_log("Received from UPS:\n%s", all_results)
+            ups = fromstring(all_results)
+            needs_cache = True
+        else:
+            needs_cache = False
+
+        ok = False
+        try:
+            ok = ups.find('Response/ResponseStatusCode').text == '1'
+        except AttributeError:
+            log.warning('Bad response from UPS TimeInTransit')
+            pass
+
+        if not ok:
+            try:
+                response = ups.find('Response/ResponseStatusDescription').text
+                log.warning('Bad response from UPS TimeInTransit: %s', response)
+            except AttributeError:
+                log.warning('Unknown UPS TimeInTransit response')
+
+        if ok:
+            services = ups.findall('TransitResponse/ServiceSummary')
+            for service in services:
+                transit_code = service.find('Service/Code').text
+                if self.service_type_code == TRANSIT_CODE_MAP.get(transit_code, ''):
+                    try:
+                        delivery_days = service.find('EstimatedArrival/BusinessTransitDays').text
+                        self.verbose_log('Found delivery days %s for %s', delivery_days, self.service_type_code)
+                    except AttributeError:
+                        log.warning('Could not find BusinessTransitDays in UPS response')
+
+                    try:
+                        delivery_days = int(delivery_days)
+                    except ValueError:
+                        pass
+
+                    break
+
+            if delivery_days is not None and needs_cache:
+                cache_set(cachekey, value=ups, length=600)
+
+        return delivery_days
+
     def verbose_log(self, *args, **kwargs):
         if config_value('shipping.modules.ups', 'VERBOSE_LOG'):
             log.debug(*args, **kwargs)
-        
-        
+
+

File satchmo/apps/shipping/templates/shipping/ups/transit_request.xml

+<?xml version="1.0"?>
+<AccessRequest xml:lang="en-US">
+  <AccessLicenseNumber>{{config.xml_key}}</AccessLicenseNumber>
+  <UserId>{{config.userid}}</UserId>
+  <Password>{{config.password}}</Password>
+</AccessRequest>
+<?xml version="1.0"?>
+<TimeInTransitRequest xml:lang="un-US">
+  <Request>
+    <TransactionReference>
+      <CustomerContext>Time In Transit</CustomerContext>
+      <XpciVersion>1.0002</XpciVersion>
+    </TransactionReference>
+    <RequestAction>TimeInTransit</RequestAction>
+  </Request>
+  <TransitFrom>
+    <AddressArtifactFormat>
+      <PostcodePrimaryHigh>{{ shipping_address.postal_code }}</PostcodePrimaryHigh>
+      <PostcodePrimaryLow>{{ shipping_address.postal_code }}</PostcodePrimaryLow>
+      <CountryCode>{{shipping_country_code}}</CountryCode>
+    </AddressArtifactFormat>
+  </TransitFrom>
+  <TransitTo>
+    <AddressArtifactFormat>
+      <PostcodePrimaryHigh>{{ contact.shipping_address.postal_code }}</PostcodePrimaryHigh>
+      <PostcodePrimaryLow>{{ contact.shipping_address.postal_code }}</PostcodePrimaryLow>
+      <CountryCode>{{ contact.shipping_address.country.iso2_code }}</CountryCode>
+      <ResidentialAddressIndicator />
+    </AddressArtifactFormat>
+  </TransitTo>
+  <PickupDate>{{ pickup_date }}</PickupDate>
+  <MonetaryValue>{{ price }}</MonetaryValue>
+</TimeInTransitRequest>