|
@@ -2,152 +2,162 @@
|
2
|
2
|
How to configure shipping
|
3
|
3
|
=========================
|
4
|
4
|
|
5
|
|
-Configuring shipping is not trivial. It generally requires creating a
|
6
|
|
-'shipping' app within your project where you can define your own shipping
|
7
|
|
-methods as well as a 'repository' class which determines when methods are
|
8
|
|
-available.
|
|
5
|
+Shipping can be very complicated. Depending on the domain, a wide variety of
|
|
6
|
+shipping scenarios are found in the wild. For instance, calculation of
|
|
7
|
+shipping costs can depend on:
|
9
|
8
|
|
10
|
|
-This recipe explains in more detail how Oscar models shipping as well as the
|
11
|
|
-steps involved in configuring shipping for your project.
|
|
9
|
+* Shipping method (e.g., standard, courier)
|
|
10
|
+* Shipping address
|
|
11
|
+* Time of day of order (e.g., if requesting next-day delivery)
|
|
12
|
+* Weight of items in basket
|
|
13
|
+* Customer type (e.g., business accounts get discounted shipping rates)
|
|
14
|
+* Offers and vouchers that give free or discounted shipping
|
12
|
15
|
|
13
|
|
-How Oscar handles shipping charges
|
14
|
|
-----------------------------------
|
|
16
|
+Further complications can arise such as:
|
15
|
17
|
|
16
|
|
-Oscar uses a "repository" class to manage shipping charges. The class is used
|
17
|
|
-in two ways:
|
|
18
|
+* Only making certain shipping methods available to certain customers
|
|
19
|
+* Tax is only applicable in certain situations
|
|
20
|
+
|
|
21
|
+Oscar can handle all of these shipping scenarios.
|
18
|
22
|
|
19
|
|
-* **It provides a list of shipping methods available to the user.** This is used to
|
20
|
|
- generate the content for the shipping methods page of checkout, where the user
|
21
|
|
- can choose a shipping method. The methods available generally depend on the
|
22
|
|
- user, the basket and the shipping address.
|
|
23
|
+Shipping in Oscar
|
|
24
|
+~~~~~~~~~~~~~~~~~
|
23
|
25
|
|
24
|
|
-* **It allows a shipping method to be retrieved based on a identifying code.** When
|
25
|
|
- a user selects a shipping method during checkout, it is persisted in the
|
26
|
|
- session using a code. This code is used to retrieve the chosen shipping
|
27
|
|
- method when it is required.
|
|
26
|
+Configuring shipping charges requires overriding Oscar's core 'shipping' app
|
|
27
|
+and providing your own ``Repository`` class (see :doc:`/topics/customisation`) that
|
|
28
|
+returns your chosen shipping method instances.
|
28
|
29
|
|
29
|
|
-The default shipping repository `can be seen here`_. It defaults to only
|
30
|
|
-providing one shipping method, which has no charge.
|
|
30
|
+The primary responsibility of the
|
|
31
|
+``Repository`` class is to provide the available shipping methods for a
|
|
32
|
+particular scenario. This is done via the
|
|
33
|
+:func:`~oscar.apps.shipping.repository.Repository.get_shipping_methods` method,
|
|
34
|
+which returns the shipping methods available to the customer.
|
31
|
35
|
|
32
|
|
-.. note::
|
|
36
|
+This method is called in several places:
|
33
|
37
|
|
34
|
|
- Oscar's checkout process includes a page for choosing your shipping method.
|
35
|
|
- If there is only one method available for your basket then it will be chosen
|
36
|
|
- automatically and the user immediately redirected to the next step.
|
|
38
|
+* To look up a "default" shipping method so that sample shipping charges can be
|
|
39
|
+ shown on the basket detail page.
|
37
|
40
|
|
38
|
|
-Custom shipping charges
|
39
|
|
------------------------
|
|
41
|
+* To list the available shipping methods on the checkout shipping method page.
|
40
|
42
|
|
41
|
|
-In order to control shipping logic for your project, you need to define your own
|
42
|
|
-repository class (see :doc:`/topics/customisation`). It normally makes
|
43
|
|
-sense to subclass the core ``Repository`` class and override the
|
44
|
|
-``get_shipping_methods`` and ``find_by_code`` methods.
|
|
43
|
+* To check the selected shipping method is still available when an order is
|
|
44
|
+ submitted.
|
45
|
45
|
|
46
|
|
-Here's a very simple example where all shipping costs are a fixed price,
|
47
|
|
-irrespective of basket and shipping address::
|
|
46
|
+The ``get_shipping_methods`` method takes the basket, user, shipping address
|
|
47
|
+and request as parameters. These can be used to provide different sets of
|
|
48
|
+shipping methods depending on the circumstances. For instance, you could use
|
|
49
|
+the shipping address to provide international shipping rates if the address is
|
|
50
|
+overseas.
|
48
|
51
|
|
49
|
|
- # myproject/shipping/repository.py
|
|
52
|
+The default behaviour is to return a single free shipping method.
|
50
|
53
|
|
51
|
|
- from decimal import Decimal as D
|
52
|
|
- from oscar.apps.shipping import repository, methods as core_methods
|
|
54
|
+.. note::
|
53
|
55
|
|
54
|
|
- class Repository(repository.Repository):
|
55
|
|
- methods = [core_methods.FixedPrice(D('9.99'))]
|
|
56
|
+ Oscar's checkout process includes a page for choosing your shipping method.
|
|
57
|
+ If there is only one method available for your basket (as is the default)
|
|
58
|
+ then it will be chosen automatically and the user immediately redirected to
|
|
59
|
+ the next step.
|
56
|
60
|
|
57
|
|
- def get_shipping_methods(self, user, basket, shipping_addr=None, **kwargs):
|
58
|
|
- return self.prime_methods(basket, self.methods)
|
|
61
|
+Custom repositories
|
|
62
|
+-------------------
|
59
|
63
|
|
60
|
|
- def find_by_code(self, code, basket):
|
61
|
|
- for method in self.methods:
|
62
|
|
- if code == method.code:
|
63
|
|
- return self.prime_method(basket, method)
|
|
64
|
+If the available shipping methods are the same for all customers and shipping
|
|
65
|
+addresses, then override the ``methods`` property of the repository:
|
64
|
66
|
|
65
|
|
-Note that both these methods must return 'primed' method instances, which means
|
66
|
|
-the basket instance has been injected into the method. This allows the method
|
67
|
|
-instance to return the shipping charge directly without requiring the basket to
|
68
|
|
-be passed again (which is useful in templates).
|
|
67
|
+.. code-block:: python
|
69
|
68
|
|
70
|
|
-As you can see the ``get_shipping_methods`` can depend on several things:
|
|
69
|
+ from oscar.apps.shipping import repository
|
|
70
|
+ from . import methods
|
71
|
71
|
|
72
|
|
-* the user in question (e.g., staff get cheaper shipping rates)
|
73
|
|
-* the basket (e.g., shipping is charged based on the weight of the basket)
|
74
|
|
-* the shipping address (e.g., overseas shipping is more expensive)
|
|
72
|
+ class Repository(repository.Repository):
|
|
73
|
+ methods = (methods.Standard(), methods.Express())
|
75
|
74
|
|
76
|
|
-Here's a more involved example repository that has two fixed price charges::
|
|
75
|
+For more complex logic, override the ``get_available_shipping_methods`` method:
|
77
|
76
|
|
78
|
|
- # myproject/shipping/repository.py
|
|
77
|
+.. code-block:: python
|
79
|
78
|
|
80
|
|
- from decimal import Decimal as D
|
81
|
|
- from oscar.apps.shipping import repository, methods as core_methods
|
|
79
|
+ from oscar.apps.shipping import repository
|
|
80
|
+ from . import methods
|
82
|
81
|
|
83
|
|
- # We create subclasses so we can give them different codes and names
|
84
|
|
- class Standard(core_methods.FixedPrice):
|
85
|
|
- code = 'standard'
|
86
|
|
- name = _("Standard shipping")
|
|
82
|
+ class Repository(repository.Repository):
|
87
|
83
|
|
88
|
|
- class Express(core_methods.FixedPrice):
|
89
|
|
- code = 'express'
|
90
|
|
- name = _("Express shipping")
|
|
84
|
+ def get_available_shipping_methods(
|
|
85
|
+ self, basket, user=None, shipping_addr=None,
|
|
86
|
+ request=None, **kwargs):
|
|
87
|
+ methods = (methods.Standard())
|
|
88
|
+ if shipping_addr and shipping.addr.country.code == 'GB':
|
|
89
|
+ # Express is only available in the UK
|
|
90
|
+ methods = (methods.Standard(), methods.Express())
|
|
91
|
+ return methods
|
91
|
92
|
|
92
|
|
- class Repository(repository.Repository):
|
93
|
|
- methods = [Standard(D('10.00')), Express(D('20.00'))]
|
|
93
|
+Note that the ``get_shipping_methods`` method wraps
|
|
94
|
+``get_available_shipping_methods`` in order to handle baskets that don't
|
|
95
|
+require shipping and to apply shipping discounts.
|
94
|
96
|
|
95
|
|
- def get_shipping_methods(self, user, basket, shipping_addr=None, **kwargs):
|
96
|
|
- return self.prime_methods(basket, self.methods)
|
|
97
|
+Shipping methods
|
|
98
|
+----------------
|
97
|
99
|
|
98
|
|
- def find_by_code(self, code, basket):
|
99
|
|
- for method in self.methods:
|
100
|
|
- if code == method.code:
|
101
|
|
- return self.prime_method(basket, method)
|
|
100
|
+Shipping methods need to implement a certain API. They need to have the
|
|
101
|
+following properties which define the metadata about the shipping method:
|
102
|
102
|
|
103
|
|
-.. _`can be seen here`: https://github.com/tangentlabs/django-oscar/blob/master/oscar/apps/shipping/repository.py
|
|
103
|
+* ``code`` - This is used as an identifier for the shipping method and so should
|
|
104
|
+ be unique amongst the shipping methods available in your shop.
|
104
|
105
|
|
105
|
|
-Shipping methods
|
106
|
|
-----------------
|
|
106
|
+* ``name`` - The name of the shipping method. This will be visible to the
|
|
107
|
+ customer during checkout.
|
107
|
108
|
|
108
|
|
-The repository class is responsible for return shipping method instances. Oscar
|
109
|
|
-defines several of these but it is easy to write your own, their interface is
|
110
|
|
-simple.
|
|
109
|
+* ``description`` - An optional description of the shipping method. This can
|
|
110
|
+ contain HTML.
|
111
|
111
|
|
112
|
|
-The base shipping method class ``oscar.apps.shipping.methods.Base`` (that
|
113
|
|
-all shipping methods should subclass has API:
|
|
112
|
+Further, each method must implement a ``calculate`` method which accepts the
|
|
113
|
+basket instance as a parameter and returns a ``Price`` instance. Most shipping
|
|
114
|
+methods subclass
|
|
115
|
+:class:`~oscar.apps.shipping.methods.Base`, which stubs this API.
|
114
|
116
|
|
115
|
|
-.. autoclass:: oscar.apps.shipping.methods.Base
|
116
|
|
- :members:
|
117
|
|
- :noindex:
|
|
117
|
+Here's an example:
|
118
|
118
|
|
119
|
|
-Core shipping methods
|
120
|
|
-~~~~~~~~~~~~~~~~~~~~~
|
|
119
|
+.. code-block:: python
|
121
|
120
|
|
122
|
|
-The shipping methods that ship with Oscar are:
|
|
121
|
+ from oscar.apps.shipping import methods
|
|
122
|
+ from oscar.core import prices
|
123
|
123
|
|
124
|
|
-* ``oscar.apps.shipping.methods.Free``. No shipping charges.
|
|
124
|
+ class Standard(methods.Base):
|
|
125
|
+ code = 'standard'
|
|
126
|
+ name = 'Standard shipping (free)'
|
125
|
127
|
|
126
|
|
-* ``oscar.apps.shipping.methods.FixedPrice``. This simply charges a fixed price for
|
127
|
|
- shipping, irrespective of the basket contents.
|
|
128
|
+ def calculate(self, basket):
|
|
129
|
+ return prices.Price(
|
|
130
|
+ currency=basket.currency,
|
|
131
|
+ excl_tax=D('0.00'), incl_tax=D('0.00'))
|
|
132
|
+
|
|
133
|
+Core shipping methods
|
|
134
|
+~~~~~~~~~~~~~~~~~~~~~
|
128
|
135
|
|
129
|
|
-* ``oscar.apps.shipping.methods.OfferDiscount``. This applies a discount
|
130
|
|
- to an existing shipping method's charges.
|
|
136
|
+Oscar ships with several re-usable shipping methods which can be used as-is, or
|
|
137
|
+subclassed and customised:
|
131
|
138
|
|
132
|
|
-* ``oscar.apps.shipping.methods.TaxExclusiveOfferDiscount``. Children of ``OfferDiscount``
|
|
139
|
+* :class:`~oscar.apps.shipping.methods.Free` - no shipping charges
|
133
|
140
|
|
134
|
|
-* ``oscar.apps.shipping.methods.TaxInclusiveOfferDiscount``. Children of ``OfferDiscount``
|
|
141
|
+* :class:`~oscar.apps.shipping.methods.FixedPrice` - fixed-price shipping charges.
|
|
142
|
+ Example usage:
|
135
|
143
|
|
136
|
|
-To apply your domain logic for shipping, you will need to override
|
137
|
|
-the default repository class (see :doc:`/topics/customisation`) and alter
|
138
|
|
-the implementation of the ``get_shipping_methods`` method. This method
|
139
|
|
-should return a list of "shipping method" classes already instantiated
|
140
|
|
-and holding a reference to the basket instance.
|
|
144
|
+.. code-block:: python
|
141
|
145
|
|
142
|
|
-Building a custom shipping method
|
143
|
|
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
146
|
+ from oscar.apps.shipping import methods
|
|
147
|
+ from oscar.core import prices
|
144
|
148
|
|
145
|
|
-At a minimum, a custom shipping method class should define a ``code`` and
|
146
|
|
-``name`` attribute to distinguish it from other methods. It is also normal to
|
147
|
|
-override the ``basket_charge_incl_tax`` and ``basket_charge_excl_tax`` methods
|
148
|
|
-to implement your custom shipping charge logic.
|
|
149
|
+ class Standard(methods.Base):
|
|
150
|
+ code = 'standard'
|
|
151
|
+ name = 'Standard shipping'
|
|
152
|
+ charge_excl_tax = D('5.00')
|
149
|
153
|
|
150
|
|
-.. tip::
|
|
154
|
+ class Express(methods.Base):
|
|
155
|
+ code = 'express'
|
|
156
|
+ name = 'Express shipping'
|
|
157
|
+ charge_excl_tax = D('10.00')
|
151
|
158
|
|
152
|
|
- Most of the shipping logic should live in the repository class, the method
|
153
|
|
- instance is only responsible for returning the charge for a given basket.
|
|
159
|
+There is also a weight-based shipping method,
|
|
160
|
+:class:`~oscar.apps.shipping.abstract_models.AbstractWeightBased`
|
|
161
|
+which determines a shipping charge by calculating the weight of a basket's
|
|
162
|
+contents and looking this up in a model-based set of weight bands.
|
|
163
|
+
|