Last active
November 3, 2016 23:29
-
-
Save molokov/36ab544df43efb224719d300761612a4 to your computer and use it in GitHub Desktop.
Mezzanine/Cartridge Shipping Rules
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ... | |
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ... | |
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | |
) | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ... | |
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ... | |
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 | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ... | |
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] | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ... | |
@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