Skip to content

Instantly share code, notes, and snippets.

@molokov
Last active November 3, 2016 23:29
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 molokov/36ab544df43efb224719d300761612a4 to your computer and use it in GitHub Desktop.
Save molokov/36ab544df43efb224719d300761612a4 to your computer and use it in GitHub Desktop.
Mezzanine/Cartridge Shipping Rules
# ...
from cartridge.shop.models import ShippingRule
# For ProductVariationAdmin, add weight, pickup_available and shipping_available
# to the variation_fieldsets so you can edit them for each product variation
# ...
##################
# SHIPPING RULES #
##################
class ShippingRuleAdmin(admin.ModelAdmin):
list_display = ('name', 'price', 'min_weight', 'max_weight', 'pickup_rule',
'shipping_rule', 'country_rule', 'priority')
list_editable = ('price', 'min_weight', 'max_weight', 'priority')
list_filter = ('min_weight', 'max_weight', 'pickup_rule',
'shipping_rule', 'country_rule')
search_fields = ('name', 'country_rule')
ordering = ('priority', 'name')
formfield_overrides = {MoneyField: {"widget": MoneyWidget}}
fieldsets = ((None, { 'fields' : ('name', ('price', 'priority'))}),
('Rules', { 'fields' :
(('min_weight', 'max_weight',), ('pickup_rule', 'shipping_rule'), 'country_rule')
})
)
admin.site.register(ShippingRule, ShippingRuleAdmin)
# ...
from cartridge.shop.models import Cart, ShippingRule
from cartridge.shop.utils import set_shipping_choices, clear_session
from cartridge.shop import shippingrule as shrule
# ...
def shippingrule_billship_handler(request, order_form):
"""
Billing/Shipping handler that makes use of shipping rules
defined in the admin.
Any rules that match are added as a possible shipping type
choice, which is then presented to the user in the payment
step (if there's more than one)
"""
#print "\nshippingrule_billship_handler"
if not request.session.get("free_shipping"):
if order_form is not None:
shipping_country = order_form.cleaned_data.get("shipping_detail_country")
else:
try:
shipping_country = request.session["order"]["shipping_detail_country"]
except KeyError:
shipping_country = u''
cart = Cart.objects.from_request(request)
cart_weight = cart.total_weight()
cart_pickup_items = cart.items_for_pickup()
cart_shipping_items = cart.items_for_shipping()
#print "country=",shipping_country
#print "cart_weight=",cart_weight
shipping_choices = []
# Find rules that match the weight of the product
rules = ShippingRule.objects.filter(Q(min_weight__lte=cart_weight) &
(Q(max_weight=None) | Q(max_weight__gte=cart_weight))
)
if cart_pickup_items == (False, False):
# No cart items have pickup
rules = rules.filter(pickup_rule=shrule.DONT_CARE)
elif cart_pickup_items == (True, False):
# At least one item has pickup, but not all
rules = rules.filter(pickup_rule__in=(shrule.DONT_CARE,
shrule.AT_LEAST_ONE_ITEM))
# else all items have pickup, so all pickup rules match.
if cart_shipping_items == (False, False):
# No cart items have shipping
rules = rules.filter(shipping_rule=shrule.DONT_CARE)
elif cart_shipping_items == (True, False):
# At least one item has shipping, but not all
rules = rules.filter(shipping_rule__in=(shrule.DONT_CARE,
shrule.AT_LEAST_ONE_ITEM))
# else all items have shipping, so all shipping rules match.
for rule in rules.order_by('priority'):
#print "Matching rule:", rule.name, rule.price, "weight=({0}, {1})".format(rule.min_weight, rule.max_weight)
# Check country
if rule.country_rule is None or re.match(rule.country_rule, shipping_country):
# The shipping country matches this rule, so add
# it to our shipping choices
shipping_choices.append((rule.name, rule.price))
#print shipping_choices
set_shipping_choices(request, shipping_choices)
if len(shipping_choices) == 1:
# Only one choice, so set this as our shipping type/price
set_shipping(request, shipping_choices[0][0],
shipping_choices[0][1])
else:
# Clear any previous shipping type
clear_session(request, "shipping_type", "shipping_total")
def shipping_method_handler(request, order_form):
"""
set shipping in session based on shipping method
"""
# print "\nshipping_method_handler"
if request.session.get("free_shipping") is True:
return
if order_form is not None:
method = order_form.cleaned_data.get("shipping_method")
#print "Shipping method from form is", method
else:
try:
method = request.session["order"]["shipping_method"]
# print "Shipping method from session is", method
except KeyError:
# No shipping method
return
if method is None:
return
# Get shipping choices from session
try:
shipping_choices = request.session["shipping_choices"]
# print "Shipping choices from session are", shipping_choices
except KeyError:
# No choices
return
shiptype, price = None, None
for n, p in shipping_choices:
if n == method:
shiptype, price = n, p
break
# print "Selected shipping type", shiptype, price
if shiptype is None or price is None:
# Out of range?
raise CheckoutError(_("Invalid shipping method"))
# Set shipping in session
set_shipping(request, shiptype, price)
register_setting(
name="SHOP_HANDLER_BILLING_SHIPPING",
label=_("Billing & Shipping Handler"),
description="Dotted package path and class name of the function "
"called upon submission of the billing/shipping checkout step. This "
"is where shipping calculations can be performed and set using the "
"function ``cartridge.shop.utils.set_shipping``.",
editable=False,
# default="cartridge.shop.checkout.default_billship_handler",
default="cartridge.shop.checkout.shippingrule_billship_handler",
)
# ...
class OrderForm(FormsetForm, DiscountForm):
# ...
# Shipping Method
shipping_method = forms.CharField(label=_("Shipping method"),
required=False)
def __init__(
self, request, step, data=None, initial=None, errors=None,
**kwargs):
# ...
if settings.SHOP_CHECKOUT_STEPS_SPLIT:
if is_first_step:
# Hide cc fields for billing/shipping if steps are split.
# Also hide shipping method
hidden_filter = lambda f: f.startswith("card_") or \
(f == "shipping_method")
elif is_payment_step:
# Hide non-cc fields for payment if steps are split.
# Except for shipping method
hidden_filter = lambda f: not f.startswith("card_") and \
(f != "shipping_method")
elif not settings.SHOP_PAYMENT_STEP_ENABLED:
# Hide all cc fields if payment step is not enabled.
hidden_filter = lambda f: f.startswith("card_")
if settings.SHOP_CHECKOUT_STEPS_CONFIRMATION and is_last_step:
# Hide all fields for the confirmation step.
hidden_filter = lambda f: True
for field in filter(hidden_filter, self.fields):
self.fields[field].widget = forms.HiddenInput()
self.fields[field].required = False
# Set year choices for cc expiry, relative to the current year.
year = now().year
choices = make_choices(list(range(year, year + 21)))
self.fields["card_expiry_year"].choices = choices
# Set shipping method choices, if required
shipping_choices = request.session.get("shipping_choices", None)
if is_payment_step and not request.session.get("free_shipping"):
if shipping_choices:
self._schoices = [(n, u"{0} ${1}".format(n, p)) for (n, p) in shipping_choices]
# print "Setting OrderForm.shipping_method choices to", self._schoices
self.fields["shipping_method"].widget = forms.Select(choices=self._schoices)
self.fields["shipping_method"].required = True
self.fields["shipping_method"].help_text = None
else:
# No valid shipping methods - user needs to be informed!
self.fields["shipping_method"].widget = forms.Select(
choices=((None, '-----'),), attrs={"readonly" : True})
self.fields["shipping_method"].required = False
self.fields["shipping_method"].help_text = "We can not find a valid shipping method " \
"for the items in your cart and/or for your shipping address. " \
"Please adjust your cart or shipping address and try again."
else:
# Hide the shipping method field
self._schoices = None
self.fields["shipping_method"].widget = forms.HiddenInput()
self.fields["shipping_method"].required = False
self.fields["shipping_method"].help_text = None
# ...
from cartridge.shop import shippingrule
# ...
@python_2_unicode_compatible
class ProductVariation(with_metaclass(ProductVariationMetaclass, Priced)):
#...
# Adding weight, pickup_available, shipping_available
weight = models.IntegerField(_("Weight"), null=True, blank=True,
help_text=_("Weight (g). 0 or None means virtual"))
pickup_available = models.BooleanField(_("Pickup available"), default=True)
shipping_available = models.BooleanField(_("Shipping available"), default=True)
#...
class Cart(models.Model):
#...
def total_weight(self):
"""
Template helper function - sum of all item weights
"""
return sum([(item.weight * item.quantity) for
item in self if item.weight is not None])
def items_for_pickup(self):
"""
Return a tuple for any or all items marked as "pickup_available"
i.e. True, True means all items are available for pickup
True, False means at least one item is available for pickup
False, False means no items are available for pickup.
"""
pickup = [item.pickup_available for item in self]
return any(pickup), all(pickup)
def items_for_shipping(self):
"""
Return a tuple for any or all items marked as "shipping_available"
i.e. True, True means all items are available for shipping
True, False means at least one item is available for shipping
False, False means no items are available for shipping
"""
shipping = [item.shipping_available for item in self]
return any(shipping), all(shipping)
# ...
class CartItem(SelectedProduct):
# ...
# Shipping properties of item
weight = models.IntegerField(_("Weight"), null=True, blank=True)
pickup_available = models.BooleanField(_("Pickup Available"), default=True)
shipping_available = models.BooleanField(_("Shipping Available"), default=True)
# ...
@python_2_unicode_compatible
class ShippingRule(models.Model):
"""
A rule to determine available shipping options for the user.
Fields:
- Name: descriptive name of the shipping option.
- Price: price of this shipping option.
And rules to match on:
- Max Weight: maximum weight of cart items
- Pickup Rule: Importance of the 'pickup_available' field of cart items
- Shipping Rule: Importance of the 'shipping_available' field of cart items
- Country Rule: Regexp used for matching on country.
Importance is one of 'Don't Care', 'At Least One Item', and 'All Items'
"""
name = models.CharField(_("Name"), blank=False, max_length=50,
help_text=_("Shipping option description. Seen by end user."))
price = fields.MoneyField(_("Price"), default=0.0, null=False, blank=False)
min_weight = models.IntegerField(_("Min Weight"), default=0,
help_text=_("Minimum weight for all cart items."))
max_weight = models.IntegerField(_("Max Weight"), null=True, blank=True,
help_text=_("Maximum weight for all cart items. None means don't care."))
pickup_rule = models.IntegerField(_("Pickup"), default=shippingrule.DONT_CARE,
choices=shippingrule.SHIPRULE_CHOICES,
help_text=_("Pickup Available for Cart Items"))
shipping_rule = models.IntegerField(_("Shipping"), default=shippingrule.DONT_CARE,
choices=shippingrule.SHIPRULE_CHOICES,
help_text=_("Shipping Available for Cart Items"))
country_rule = models.CharField(_("Country"), blank=True, max_length=200,
help_text=_("Regular expression to match against Country shipping field in Order"))
priority = models.IntegerField(_("Priority"), default=0, help_text=_("Priority of rule, lower is higher priority"))
def __str__(self):
return self.name
from __future__ import unicode_literals
from django.utils.translation import (ugettext, ugettext_lazy as _,
pgettext_lazy as __)
DONT_CARE = 1
AT_LEAST_ONE_ITEM = 2
ALL_ITEMS = 3
SHIPRULE_CHOICES = ((DONT_CARE, _("Don't Care")),
(AT_LEAST_ONE_ITEM, _("At Least One Item")),
(ALL_ITEMS, _("All Items")))
# ...
def set_shipping_choices(request, shipping_choices):
"""
Stores the shipping choices in the session.
"""
request.session["shipping_choices"] = [(_str(n), _str(p))
for n, p in shipping_choices]
# ...
@never_cache
def checkout_steps(request, form_class=OrderForm, extra_context=None):
# ...
# ALL STEPS - run billing/tax handlers. These are run on
# all steps, since all fields (such as address fields) are
# posted on each step, even as hidden inputs when not
# visible in the current step.
# ---- ADDED:
# Do the shipping method handling at each step from payment
# onwards. REQUIRED otherwise we lose the shipping price
# after confirmation.
try:
billship_handler(request, form)
tax_handler(request, form)
if step >= checkout.CHECKOUT_STEP_PAYMENT:
checkout.shipping_method_handler(request, form)
except checkout.CheckoutError as e:
checkout_errors.append(e)
# ...
elif step == checkout.CHECKOUT_STEP_LAST:
# Coming into the last step via a "Return to Checkout" button press
try:
checkout.shipping_method_handler(request, None)
except checkout.CheckoutError:
# Go back to previous step
step = step - 1
form = form_class(request, step, initial=initial)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment