Wiki

Clone wiki

satchmo / FancyProducts

Fancy Products

This is a rough overview on how to extend standard product model in Satchmo. Some shops may change offer basing on customer's history, some may sell extensions to products already purchased by customer. Satchmo has interface which gives you an opportunity to be aware of most critical operations and to take care of your additional data there.

Before you read this, you should:

  • Know at least basics of Satchmo internals and how the flow from product selection to payment looks like,
  • Be familiar with Django signals,
  • Use Satchmo trunk revision [1433] or newer,
  • Be '''very patient'''. This is still a development version.

Imagine you want to have a shop with ''Products'' which have a ''WarrantyPeriod''.

  • Each product comes with default ''WarrantyPeriod''.
  • Users can purchase ''WarrantyPeriodExtension'' for their existing ''WarrantyPeriod''.

To support that, you may create an extension of Satchmo, which:

1. Allows to add ''WarrantyPeriodExtension'' associated with existing ''WarrantyPeriod'' to shop cart. 2. Continues with standard checkout process. 3. After the ''Order'' is successfully paid, extends ''WarrantyPeriod''s by purchased extensions.

There are important steps to do:

Models

class WarrantyPeriod(models.Model):
	product = models.ForeignKey(Product)
	order_item = models.ForeignKey(OrderItem)	# helps find the Order which began this period
	end_date = models.DateField()

class WarrantyPeriodExtension(models.Model):
	product = models.ForeignKey(Product)
	cart_item = models.ForeignKey(OrderItem, null=True)
	order_item = models.ForeignKey(OrderItem, null=True)
	length_days = models.PositiveIntegerField()

Add to cart

Custom form

You may add an ''Extend'' button to each existing period on customer's order history listing. This button works just like purchasing additional item of ''Product'', but adds an ''warranty_id'' field to the form sent to standard Satchmo view (''satchmo_smart_add''). This field, of course, contains an ID of ''WarrantyPeriod'' object which is going to be extended.

To capture this information, you will want to listen to '''satchmo_cart_add_complete''' signal. This signal receives original ''request'' from cart-add view and, of course, contains our variable.

def cart_add_complete_listener(sender, cartitem=None, product=None, form=None, request=None, **kwargs):
	if not request.POST.has_key('warranty_id'):
		# user is adding regular Product to cart, not an extension
		return
	# I won't deal with NotFound exceptions here. DIY :)
	warranty = WarrantyPeriod.objects.get(id=request.POST['warranty_id'], order_item__order__contact__user=request.user)
	extension = WarrantyExtension(
			product=product,
			warranty=warranty,
			cart_item=cartitem,
			)
	extension.save()

Now we have a ''WarrantyExtension'' associated with existing ''CartItem''. We can (and should) treat this as differrent item than a simple ''CartItem'' of the same product. We may apply discount which is defined in ''Warranty'' model or somewhere else. To apply discount to item, remember of very useful Satchmo signal named '''satchmo_cartitem_price_query'''.

Cart stacking

Wait a moment! Satchmo doesn't know anything about our app and if we add two differrent extensions to the cart, it will stack them together in the cart only if products match.

To prevent this, we must add details to the item. Just hook to the '''satchmo_cart_details_query''' signal and do the following:

def satchmo_cart_details_query_listener(sender, product=None, quantity=None, details=None, request=None, form=None, **kwargs):
	if not request.POST.has_key('warranty_id'):
		# user is adding regular Product to cart, not an extension
		return
	# I won't deal with NotFound exceptions here. DIY :)
	warranty = WarrantyPeriod.objects.get(id=request.POST['warranty_id'], order_item__order__contact__user=request.user)
	if warranty:
		details.append({
					'name': 'warranty_id',
					'value': warranty.id,
					'sort_order': 0,
					'price_change': 0
				})

When two items have differrent set of details, they are not stacked together.

Placing an order

The main problem is that ''CartItem'' and ''OrderItem'' are differrent objects. This means, all data from the former is copied to the latter. All but our extensions, because Satchmo has no way of knowing about their existence.

Here comes another signal: '''satchmo_post_copy_item_to_order'''.

def satchmo_post_copy_item_to_order_listener(sender, cartitem=None, order=None, orderitem=None, **kwargs):
	# I won't deal with NotFound exceptions here. DIY :)
	extension = cartitem.warrantyperiodextension
	# We remove pk here, which will make a copy of object on save().
	# Bit hackish, but Django deletes related objects by cascade, even on null=True relations. And it does
	# collect item for deletion BEFORE pre_delete signal on parent is sent.
	extension.order_item = orderitem
	extension.cart_item = None
	extension.pk = extension.id = None
	extension.save()

Now a copy of our extension is attached to ''OrderItem'' and will be stored forever (if order succeeds, of course). Simple? Can be even more when guys at Django add custom behavior on cascade delete.

Finishing order

We finally receive our lovely money and Satchmo gives us two ways to create ''WarrantyPeriod'' object on successful product purchase. There are two ways to do it:

1. Hook to '''order_success''' signal and scan ''OrderItems'' for interesting products. 1. Make a product extension (just like Satchmo's ''DownloadableProduct'' for example) and add ''order_success()'' method to it.

Have fun!

Updated