Skip to content

Instantly share code, notes, and snippets.

@kamilion
Last active April 11, 2016 22:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kamilion/23e01fb4c6904619c151f29ccccfb0c9 to your computer and use it in GitHub Desktop.
Save kamilion/23e01fb4c6904619c151f29ccccfb0c9 to your computer and use it in GitHub Desktop.
django/mezzanine/cartridge and shippingness
# Dis goes in kamicart/admin.py
# To enable this, append to INSTALLED_APPS:
# "m3shop.kamicart",
# You need the comma at the end too.
# Now add to EXTRA_MODEL_FIELDS
#EXTRA_MODEL_FIELDS = (
# (
# "cartridge.shop.models.Product.weight_shipping",
# "DecimalField",
# ("Shipping Weight",),
# {"blank": False, "default": 0.0, "max_digits": 5, "decimal_places": 2,
# "help_text": 'Specify the shipping weight of the Product including packing material'},
# ),
#)
# Run manage.py makemigrations && manage.py migrate
# And the billship_handler module should work!
# This file is responsible for making the needed override to display extra fields in the Product
from copy import deepcopy
from django.contrib import admin
# Need to override the ProductAdmin to add the weight_shipping field.
# Pull in the original modules
from cartridge.shop.admin import ProductAdmin
from cartridge.shop.models import Product
# Deepcopy the original to a new object.
new_fieldsets = deepcopy(ProductAdmin.fieldsets)
# Append the weight_shipping field.
new_fieldsets[0][1]["fields"].insert(-3, "weight_shipping")
# Define an override class for ProductAdmin
class WeightedProductAdmin(ProductAdmin):
# Set the fieldsets to the new fieldsets with the additional field
fieldsets = new_fieldsets
# Unregister the original instance of Product, freeing it's admin module as well.
admin.site.unregister(Product)
# Reregister the original instance of Product with a new subclassed ProductAdmin.
admin.site.register(Product, WeightedProductAdmin)
# Blank lines above so we don't get hashbang processed on unix if set executable.
# This file is referred to as m3shop.kamicart.billship_handler by django.
from __future__ import unicode_literals
from future.builtins import int
from future.builtins import str
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import ugettext as _
from mezzanine.conf import settings
from cartridge.shop.checkout import CheckoutError
from cartridge.shop.models import Order
from cartridge.shop.utils import set_shipping
import sys
from fedex.config import FedexConfig
from fedex.services.rate_service import FedexRateServiceRequest
try:
fedexcfg = FedexConfig(key=settings.FEDEX_KEY,
password=settings.FEDEX_PASSWORD,
account_number=settings.FEDEX_ACCOUNT_NUM,
meter_number=settings.FEDEX_METER_NUM,
freight_account_number=settings.FEDEX_FREIGHT_NUM,
use_test_server=settings.FEDEX_USE_TEST_SERVER)
except AttributeError:
raise ImproperlyConfigured("You need to define the following keys "
"in your settings module to use this "
"shipping calculation module."
"FEDEX_KEY, "
"FEDEX_PASSWORD, "
"FEDEX_ACCOUNT_NUM, "
"FEDEX_METER_NUM, "
"FEDEX_FREIGHT_NUM, "
"FEDEX_USE_TEST_SERVER.")
def m3_billship_handler_fedex(request, order_form):
try:
settings.use_editable()
########## Do some processing here for gathering the weights of items in the cart.
print("FEDEX:0:CART:{}".format(request.cart))
for item in request.cart:
print("FEDEX:0:CART_ITEM:{}".format(item))
########## Fedex Object Creation
# This is the object that will be handling our request.
# We're using the FedexConfig object from above, populated
# by the mezzanine settings defined in local_settings.py
customer_transaction_id = "*** RateService Request v18 using Python ***" # Optional transaction_id
rate_request = FedexRateServiceRequest(fedexcfg, customer_transaction_id=customer_transaction_id)
########## Fedex Object Configuration
# If you wish to have transit data returned with your request you
# need to uncomment the following
# rate_request.ReturnTransitAndCommit = True
# Who pays for the rate_request?
# RECIPIENT, SENDER or THIRD_PARTY
rate_request.RequestedShipment.ShippingChargesPayment.PaymentType = 'SENDER'
# 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 = 'FEDEX_GROUND'
########## Define the source and customer addresses
# Shipper's address
rate_request.RequestedShipment.Shipper.Address.PostalCode = '95008'
rate_request.RequestedShipment.Shipper.Address.CountryCode = 'US'
rate_request.RequestedShipment.Shipper.Address.Residential = False
# Recipient address
rate_request.RequestedShipment.Recipient.Address.PostalCode = request.session.get('order')['shipping_detail_postcode']
rate_request.RequestedShipment.Recipient.Address.CountryCode = request.session.get('order')['shipping_detail_country'].lower()
# 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'
########## Define the package manifest
# What kind of package this will be shipped in.
# FEDEX_BOX, FEDEX_PAK, FEDEX_TUBE, YOUR_PACKAGING
rate_request.RequestedShipment.PackagingType = 'YOUR_PACKAGING'
total_weight = 1.00
#for item in request.cart:
# total_weight = total_weight + item.weight_shipping
package1_weight = rate_request.create_wsdl_object_of_type('Weight')
# Weight, in LB.
package1_weight.Value = total_weight
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
########## Debugging for the request
# Un-comment this to see the other variables you may set on a package.
#print("FEDEX:1:{}".format(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("FEDEX:2:{}".format(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("FEDEX:3:{}".format(rate_request.RequestedShipment))
print("FEDEX:4:SENDING_REQUEST")
########## Send the WSDL Request to fedex's API
# Fires off the request, sets the 'response' attribute on the object.
rate_request.send_request()
print("FEDEX:5:SENT_REQUEST")
########## Debugging for the reply
# 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("FEDEX:6:{}".format(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("FEDEX:7:{}".format(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("FEDEX:8:{}".format(sobject_to_json(rate_request.response)))
# Here is the overall end result of the query.
print("FEDEX:10:HighestSeverity: {}".format(rate_request.response.HighestSeverity))
########## Iterate over the reply details
# 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("FEXEX:15:{}: ODA rate_request charge {}".format(service.ServiceType, surcharge.Amount.Amount))
for rate_detail in service.RatedShipmentDetails:
print("FEDEX:20:{}: Net FedEx Charge {} {}".format(service.ServiceType,
rate_detail.ShipmentRateDetail.TotalNetFedExCharge.Currency,
rate_detail.ShipmentRateDetail.TotalNetFedExCharge.Amount))
########## Warning checks
# 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))
fedex_charge = rate_detail.ShipmentRateDetail.TotalNetFedExCharge.Amount
print("FEDEX:99:COMPLETE:RATE_IS {}".format(fedex_charge))
########## Do more processing here for handling charges based on item count and weight.
########## Set the final amount for shipping and return.
set_shipping(request, _("FedEx Shipping"), rate_detail.ShipmentRateDetail.TotalNetFedExCharge.Amount)
except Exception as e:
raise CheckoutError(_("A general error occured while calculating fedex shipping rate: ") + str(e))
def m3_billship_handler_testing(request, order_form):
try:
settings.use_editable()
subtotal = request.cart.total_price()
item_count = request.cart.total_quantity()
if subtotal > 149 or item_count > 4:
set_shipping(request, _("Flat rate shipping"), settings.SHOP_DEFAULT_SHIPPING_VALUE)
else:
set_shipping(request, _("Free shipping"), 0)
except Exception as e:
raise CheckoutError(_("A general error occured while calculating tax: ") + str(e))
########## Soap object helpers
def basic_sobject_to_dict(obj):
"""Converts suds object to dict very quickly.
Does not serialize date time or normalize key case.
:param obj: suds object
:return: dict object
"""
if not hasattr(obj, '__keylist__'):
return obj
data = {}
fields = obj.__keylist__
for field in fields:
val = getattr(obj, field)
if isinstance(val, list):
data[field] = []
for item in val:
data[field].append(basic_sobject_to_dict(item))
else:
data[field] = basic_sobject_to_dict(val)
return data
def sobject_to_dict(obj, key_to_lower=False, json_serialize=False):
"""
Converts a suds object to a dict. Includes advanced features.
:param json_serialize: If set, changes date and time types to iso string.
:param key_to_lower: If set, changes index key name to lower case.
:param obj: suds object
:return: dict object
"""
import datetime
if not hasattr(obj, '__keylist__'):
if json_serialize and isinstance(obj, (datetime.datetime, datetime.time, datetime.date)):
return obj.isoformat()
else:
return obj
data = {}
fields = obj.__keylist__
for field in fields:
val = getattr(obj, field)
if key_to_lower:
field = field.lower()
if isinstance(val, list):
data[field] = []
for item in val:
data[field].append(sobject_to_dict(item, json_serialize=json_serialize))
else:
data[field] = sobject_to_dict(val, json_serialize=json_serialize)
return data
def sobject_to_json(obj, key_to_lower=False):
"""
Converts a suds object to a JSON string.
:param obj: suds object
:param key_to_lower: If set, changes index key name to lower case.
:return: json object
"""
import json
data = sobject_to_dict(obj, key_to_lower=key_to_lower, json_serialize=True)
return json.dumps(data)
# Blank lines above so we don't get hashbang processed on unix if set executable.
# This file is referred to as m3shop.kamicart.models by django.
from copy import deepcopy
from django.utils.encoding import force_text
from cartridge.shop.models import Cart
def add_item_mod(self, variation, quantity):
"""
Increase quantity of existing item if SKU matches, otherwise create
new.
"""
if not self.pk:
self.save()
kwargs = {"sku": variation.sku, "unit_price": variation.price()}
item, created = self.items.get_or_create(**kwargs)
if created:
item.description = force_text(variation)
item.unit_price = variation.price()
item.url = variation.product.get_absolute_url()
try:
item.weight_shipping = self.weight_shipping
except AttributeError:
pass
image = variation.image
if image is not None:
item.image = force_text(image.file)
variation.product.actions.added_to_cart()
item.quantity += quantity
item.save()
Cart.add_item = add_item_mod
#cartridge.shop.models.Cart.add_item = add_item_mod
# Blank lines above so we don't get hashbang processed on unix if set executable.
# This file is referred to as m3shop.kamicart.tax_handler by django.
from __future__ import unicode_literals
from future.builtins import int
from future.builtins import str
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import ugettext as _
from mezzanine.conf import settings
from decimal import Decimal
from cartridge.shop.checkout import CheckoutError
from cartridge.shop.models import Order
from cartridge.shop.utils import set_tax, set_shipping
try:
tax_percentage = Decimal(settings.TAX_PERCENTAGE)
taxed_state_id = settings.TAX_STATE_ID.lower()
taxed_state_name = settings.TAX_STATE_NAME.lower()
except AttributeError:
raise ImproperlyConfigured("You need to define TAX_PERCENTAGE, "
"TAX_STATE_ID, and TAX_STATE_NAME "
"in your settings module to use this "
"tax calculation module.")
def m3_tax_handler(request, order_form):
"""
Cartridge tax handler - called immediately after the handler defined
by ``SHOP_HANDLER_BILLING_SHIPPING``.
Specify the path to import this from via the setting
``SHOP_HANDLER_TAX``.
This function will typically contain any tax calculation where the
tax amount can then be set using the function
``cartridge.shop.utils.set_tax``.
The Cart object is also accessible via ``request.cart``.
"""
settings.use_editable() # Drop the settings cache and reload so we don't run into time-to-use vulns.
try:
# Compare lowercase texts between where we tax and where the order is shipped to.
dest_state = request.session.get('order')['shipping_detail_state'].lower()
# Decide if we need to add the tax based on the state's name.
if dest_state in (taxed_state_id, taxed_state_name):
# Calculate the total after discounts
total = request.cart.total_price()
if request.session.has_key('discount'):
#if 'discount' in request.session: # which one??
discount = request.cart.calculate_discount(request.session['discount'])
total -= discount # decrement the discount from the total value.
# Get the properly capitalized string directly from the mezzanine settings.
set_tax(request, _("{} Sales Tax".format(settings.TAX_STATE_NAME)), total * tax_percentage )
except Exception as e:
raise CheckoutError(_("A general error occured while calculating tax: ") + str(e))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment