Skip to content

Commit

Permalink
Consolidate shipping docs
Browse files Browse the repository at this point in the history
The howto recipe on shipping has been rewritten to describe the new
changes to shipping functionality. The old shipping app reference has
been merged in too to avoid duplication.
  • Loading branch information
codeinthehole committed Jun 2, 2014
1 parent 96f2a2f commit c58b181
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 208 deletions.
220 changes: 115 additions & 105 deletions docs/source/howto/how_to_configure_shipping.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,152 +2,162 @@
How to configure shipping
=========================

Configuring shipping is not trivial. It generally requires creating a
'shipping' app within your project where you can define your own shipping
methods as well as a 'repository' class which determines when methods are
available.
Shipping can be very complicated. Depending on the domain, a wide variety of
shipping scenarios are found in the wild. For instance, calculation of
shipping costs can depend on:

This recipe explains in more detail how Oscar models shipping as well as the
steps involved in configuring shipping for your project.
* Shipping method (e.g., standard, courier)
* Shipping address
* Time of day of order (e.g., if requesting next-day delivery)
* Weight of items in basket
* Customer type (e.g., business accounts get discounted shipping rates)
* Offers and vouchers that give free or discounted shipping

How Oscar handles shipping charges
----------------------------------
Further complications can arise such as:

Oscar uses a "repository" class to manage shipping charges. The class is used
in two ways:
* Only making certain shipping methods available to certain customers
* Tax is only applicable in certain situations

Oscar can handle all of these shipping scenarios.

* **It provides a list of shipping methods available to the user.** This is used to
generate the content for the shipping methods page of checkout, where the user
can choose a shipping method. The methods available generally depend on the
user, the basket and the shipping address.
Shipping in Oscar
~~~~~~~~~~~~~~~~~

* **It allows a shipping method to be retrieved based on a identifying code.** When
a user selects a shipping method during checkout, it is persisted in the
session using a code. This code is used to retrieve the chosen shipping
method when it is required.
Configuring shipping charges requires overriding Oscar's core 'shipping' app
and providing your own ``Repository`` class (see :doc:`/topics/customisation`) that
returns your chosen shipping method instances.

The default shipping repository `can be seen here`_. It defaults to only
providing one shipping method, which has no charge.
The primary responsibility of the
``Repository`` class is to provide the available shipping methods for a
particular scenario. This is done via the
:func:`~oscar.apps.shipping.repository.Repository.get_shipping_methods` method,
which returns the shipping methods available to the customer.

.. note::
This method is called in several places:

Oscar's checkout process includes a page for choosing your shipping method.
If there is only one method available for your basket then it will be chosen
automatically and the user immediately redirected to the next step.
* To look up a "default" shipping method so that sample shipping charges can be
shown on the basket detail page.

Custom shipping charges
-----------------------
* To list the available shipping methods on the checkout shipping method page.

In order to control shipping logic for your project, you need to define your own
repository class (see :doc:`/topics/customisation`). It normally makes
sense to subclass the core ``Repository`` class and override the
``get_shipping_methods`` and ``find_by_code`` methods.
* To check the selected shipping method is still available when an order is
submitted.

Here's a very simple example where all shipping costs are a fixed price,
irrespective of basket and shipping address::
The ``get_shipping_methods`` method takes the basket, user, shipping address
and request as parameters. These can be used to provide different sets of
shipping methods depending on the circumstances. For instance, you could use
the shipping address to provide international shipping rates if the address is
overseas.

# myproject/shipping/repository.py
The default behaviour is to return a single free shipping method.

from decimal import Decimal as D
from oscar.apps.shipping import repository, methods as core_methods
.. note::

class Repository(repository.Repository):
methods = [core_methods.FixedPrice(D('9.99'))]
Oscar's checkout process includes a page for choosing your shipping method.
If there is only one method available for your basket (as is the default)
then it will be chosen automatically and the user immediately redirected to
the next step.

def get_shipping_methods(self, user, basket, shipping_addr=None, **kwargs):
return self.prime_methods(basket, self.methods)
Custom repositories
-------------------

def find_by_code(self, code, basket):
for method in self.methods:
if code == method.code:
return self.prime_method(basket, method)
If the available shipping methods are the same for all customers and shipping
addresses, then override the ``methods`` property of the repository:

Note that both these methods must return 'primed' method instances, which means
the basket instance has been injected into the method. This allows the method
instance to return the shipping charge directly without requiring the basket to
be passed again (which is useful in templates).
.. code-block:: python
As you can see the ``get_shipping_methods`` can depend on several things:
from oscar.apps.shipping import repository
from . import methods
* the user in question (e.g., staff get cheaper shipping rates)
* the basket (e.g., shipping is charged based on the weight of the basket)
* the shipping address (e.g., overseas shipping is more expensive)
class Repository(repository.Repository):
methods = (methods.Standard(), methods.Express())
Here's a more involved example repository that has two fixed price charges::
For more complex logic, override the ``get_available_shipping_methods`` method:

# myproject/shipping/repository.py
.. code-block:: python
from decimal import Decimal as D
from oscar.apps.shipping import repository, methods as core_methods
from oscar.apps.shipping import repository
from . import methods
# We create subclasses so we can give them different codes and names
class Standard(core_methods.FixedPrice):
code = 'standard'
name = _("Standard shipping")
class Repository(repository.Repository):
class Express(core_methods.FixedPrice):
code = 'express'
name = _("Express shipping")
def get_available_shipping_methods(
self, basket, user=None, shipping_addr=None,
request=None, **kwargs):
methods = (methods.Standard())
if shipping_addr and shipping.addr.country.code == 'GB':
# Express is only available in the UK
methods = (methods.Standard(), methods.Express())
return methods
class Repository(repository.Repository):
methods = [Standard(D('10.00')), Express(D('20.00'))]
Note that the ``get_shipping_methods`` method wraps
``get_available_shipping_methods`` in order to handle baskets that don't
require shipping and to apply shipping discounts.

def get_shipping_methods(self, user, basket, shipping_addr=None, **kwargs):
return self.prime_methods(basket, self.methods)
Shipping methods
----------------

def find_by_code(self, code, basket):
for method in self.methods:
if code == method.code:
return self.prime_method(basket, method)
Shipping methods need to implement a certain API. They need to have the
following properties which define the metadata about the shipping method:

.. _`can be seen here`: https://github.com/tangentlabs/django-oscar/blob/master/oscar/apps/shipping/repository.py
* ``code`` - This is used as an identifier for the shipping method and so should
be unique amongst the shipping methods available in your shop.

Shipping methods
----------------
* ``name`` - The name of the shipping method. This will be visible to the
customer during checkout.

The repository class is responsible for return shipping method instances. Oscar
defines several of these but it is easy to write your own, their interface is
simple.
* ``description`` - An optional description of the shipping method. This can
contain HTML.

The base shipping method class ``oscar.apps.shipping.methods.Base`` (that
all shipping methods should subclass has API:
Further, each method must implement a ``calculate`` method which accepts the
basket instance as a parameter and returns a ``Price`` instance. Most shipping
methods subclass
:class:`~oscar.apps.shipping.methods.Base`, which stubs this API.

.. autoclass:: oscar.apps.shipping.methods.Base
:members:
:noindex:
Here's an example:

Core shipping methods
~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
The shipping methods that ship with Oscar are:
from oscar.apps.shipping import methods
from oscar.core import prices
* ``oscar.apps.shipping.methods.Free``. No shipping charges.
class Standard(methods.Base):
code = 'standard'
name = 'Standard shipping (free)'
* ``oscar.apps.shipping.methods.FixedPrice``. This simply charges a fixed price for
shipping, irrespective of the basket contents.
def calculate(self, basket):
return prices.Price(
currency=basket.currency,
excl_tax=D('0.00'), incl_tax=D('0.00'))
Core shipping methods
~~~~~~~~~~~~~~~~~~~~~

* ``oscar.apps.shipping.methods.OfferDiscount``. This applies a discount
to an existing shipping method's charges.
Oscar ships with several re-usable shipping methods which can be used as-is, or
subclassed and customised:

* ``oscar.apps.shipping.methods.TaxExclusiveOfferDiscount``. Children of ``OfferDiscount``
* :class:`~oscar.apps.shipping.methods.Free` - no shipping charges

* ``oscar.apps.shipping.methods.TaxInclusiveOfferDiscount``. Children of ``OfferDiscount``
* :class:`~oscar.apps.shipping.methods.FixedPrice` - fixed-price shipping charges.
Example usage:

To apply your domain logic for shipping, you will need to override
the default repository class (see :doc:`/topics/customisation`) and alter
the implementation of the ``get_shipping_methods`` method. This method
should return a list of "shipping method" classes already instantiated
and holding a reference to the basket instance.
.. code-block:: python
Building a custom shipping method
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from oscar.apps.shipping import methods
from oscar.core import prices
At a minimum, a custom shipping method class should define a ``code`` and
``name`` attribute to distinguish it from other methods. It is also normal to
override the ``basket_charge_incl_tax`` and ``basket_charge_excl_tax`` methods
to implement your custom shipping charge logic.
class Standard(methods.Base):
code = 'standard'
name = 'Standard shipping'
charge_excl_tax = D('5.00')
.. tip::
class Express(methods.Base):
code = 'express'
name = 'Express shipping'
charge_excl_tax = D('10.00')
Most of the shipping logic should live in the repository class, the method
instance is only responsible for returning the charge for a given basket.
There is also a weight-based shipping method,
:class:`~oscar.apps.shipping.abstract_models.AbstractWeightBased`
which determines a shipping charge by calculating the weight of a basket's
contents and looking this up in a model-based set of weight bands.

81 changes: 2 additions & 79 deletions docs/source/ref/apps/shipping.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,85 +2,8 @@
Shipping
========

Shipping can be very complicated. Depending on the domain, a wide variety of shipping
scenarios are found in the wild. For instance, calculation of shipping costs can depend on:

* Shipping method (e.g., standard, courier)
* Shipping address
* Time of day of order (e.g., if requesting next-day delivery)
* Weight of items in basket
* Customer type (e.g., business accounts get discounted shipping rates)
* Offers and vouchers that give free or discounted shipping

Further complications can arise such as:

* Only making certain shipping methods available to certain customers
* Tax is only applicable in certain situations

Oscar can handle all of these shipping scenarios.

Shipping in Oscar
-----------------

Shipping is handled using "method" objects which represent a means of shipping
an order (e.g., "standard" or "next-day" delivery). Each method is essentially a
named calculator that takes a basket and is able to calculate the shipping
costs with and without tax.

For example, you may model "standard" delivery by having a calculator object
that charges a fixed price for each item in the basket. The method object
could be configured by passing the fixed price to be used for calculation.

Shipping within checkout
------------------------

Shipping is first encountered by customers within the checkout flow, on the "shipping
method" view.

It is the responsibility of this class to either:

1. Offer an a set of delivery methods for the customer to choose from, displaying
the cost of each.
2. If there is only one method available, to construct the appropriate shipping method
and set it within the checkout session context.

The ``ShippingMethodView`` class handles this behaviour. Its core
implementation looks up a list of available shipping methods using the
``oscar.shipping.repository.Repository`` class. If there is only one, then
this is written out to the session and a redirect is issued to the next step of
the checkout. If more than one, then each available method is displayed so the
customer can choose.

Default behaviour
-----------------
Oscar ships with a simple model for calculating shipping based on a charge per
order, and a charge per item. This is the ``OrderAndItemLevelChargeMethod``
class and is configured by setting the two charges used for the calculation.
You can use this model to provide multiple methods - each identified by a code.

The core ``Repository`` class will load all defined
``OrderAndItemLevelChargeMethod`` models and make them available to the
customer. If none are set, then a `FreeShipping` method object will be
returned.

Shipping method classes
-----------------------

Each method object must subclass ``ShippingMethod`` from
``oscar.shipping.methods`` which provides the required interface. Note that the interface
does not depend on the many other factors that can affect shipping (e.g., shipping address). The
way to handle this is within your "factory" method which returns available shipping methods.

Writing your own shipping method
--------------------------------

Simple really - follow these steps:

1. Subclass ``oscar.shipping.methods.ShippingMethod`` and implement
the methods ``basket_charge_incl_tax`` and ``basket_charge_excl_tax`` for calculating shipping costs.
2. Override the default ``shipping.repository.Repository`` class and implement your domain logic
for determining which shipping methods are returned based on the user, basket and shipping address
passed in.
See :doc:`/howto/how_to_configure_shipping` for details on how shipping works
in Oscar.

Methods
-------
Expand Down

0 comments on commit c58b181

Please sign in to comment.