Kaynağa Gözat

Multiply charges when exceeding top bands upper limit

This approach imitates sending multiple parcels for a defined cost, and
hopefully approximates reality much better.

Note we still make some assumptions about the cost structure to avoid
having to solve an NP hard problem.

This commit also removes the unused max_upper_limit property and
replaces it by a more useful top_band property.
master
Maik Hoepfel 11 yıl önce
ebeveyn
işleme
83a154845e

+ 8
- 18
docs/source/releases/v0.8.rst Dosyayı Görüntüle

@@ -64,23 +64,12 @@ Customisation just got easier!
64 64
 Reworked shipping app
65 65
 ~~~~~~~~~~~~~~~~~~~~~
66 66
 
67
-Several parts of the shipping app have been altered The most important change is a
67
+Several parts of the shipping app have been altered. The most important change is a
68 68
 to the API of shipping methods to avoid a potential thread safety issue.
69 69
 Any existing Oscar sites with custom shipping methods will need to adjust them
70
-to confirm to the new API.
70
+to confirm to the new API. The new API and the other changes are detailed below.
71 71
 
72
-Other changes to the shipping app include:
73
-
74
-* All shipping models now have abstract base classes, similar to
75
-  the rest of Oscar, allowing them to be customised in the standard way.
76
-
77
-* The ``WeightBand.upper_limit`` model field is now a ``DecimalField``, just like the other
78
-  weight-related fields.
79
-
80
-* The Django admin interface for the ``WeightBased`` shipping method has been
81
-  made slightly more useful. 
82
-
83
-See the 
72
+See the
84 73
 :ref:`backwards incompatible changes <incompatible_shipping_changes_in_0.8>` 
85 74
 for the shipping app and the 
86 75
 :doc:`guide to configuring shipping </howto/how_to_configure_shipping>` 
@@ -154,10 +143,6 @@ Cleanup around shipping methods
154 143
 * ``WeightBand.upper_limit`` is now a ``DecimalField``, just like the other
155 144
   weight-related fields.
156 145
 
157
-* The Django admin interface for the ``WeightBased`` shipping method has been
158
-  made slightly more useful. Contributions for a dedicated dashboard app are
159
-  most welcome!
160
-
161 146
 .. _minor_changes_in_0.8:
162 147
 
163 148
 Minor changes
@@ -273,6 +258,11 @@ Note that shipping address should be passed around as instances not classes.
273 258
 
274 259
 Other potentially breaking changes related to shipping include:
275 260
 
261
+* Weight based shipping methods used to have an ``upper_charge`` field which was
262
+  returned if no weight band matched. That doesn't work very well in practice,
263
+  and has been removed. Instead, charges from bands are now added together to
264
+  match the weight of the basket.
265
+
276 266
 * The :class:`~oscar.apps.order.utils.OrderCreator` class no longer defaults to
277 267
   free shipping: a shipping method and charge have to be explicitly passed in.
278 268
 

+ 43
- 22
oscar/apps/shipping/abstract_models.py Dosyayı Görüntüle

@@ -107,13 +107,7 @@ class AbstractWeightBased(AbstractBase):
107 107
         scale = Scale(attribute_code=self.weight_attribute,
108 108
                       default_weight=self.default_weight)
109 109
         weight = scale.weigh_basket(basket)
110
-        band = self.get_band_for_weight(weight)
111
-        if band is not None:
112
-            charge = band.charge
113
-        elif self.bands.all().exists() and self.upper_charge:
114
-            charge = self.upper_charge
115
-        else:
116
-            charge = D('0.00')
110
+        charge = self.get_charge(weight)
117 111
 
118 112
         # Zero tax is assumed...
119 113
         return prices.Price(
@@ -121,30 +115,57 @@ class AbstractWeightBased(AbstractBase):
121 115
             excl_tax=charge,
122 116
             incl_tax=charge)
123 117
 
118
+    def get_charge(self, weight):
119
+        """
120
+        Calculates shipping charges for a given weight.
121
+
122
+        If there is one or more matching weight band for a given weight, the
123
+        charge of the closest matching weight band is returned.
124
+
125
+        If the weight exceeds the top weight band, the top weight band charge
126
+        is added until a matching weight band is found. This models the concept
127
+        of "sending as many of the large boxes as needed".
128
+
129
+        Please note that it is assumed that the closest matching weight band
130
+        is the most cost-effective one, and that the top weight band is more
131
+        cost effective than e.g. sending out two smaller parcels.
132
+        Without that assumption, determining the cheapest shipping solution
133
+        becomes an instance of the bin packing problem. The bin packing problem
134
+        is NP-hard and solving it is left as an exercise to the reader.
135
+        """
136
+        weight = D(weight)  # weight really should be stored as a decimal
137
+        if not weight or not self.bands.exists():
138
+            return D('0.00')
139
+
140
+        top_band = self.top_band
141
+        if weight < top_band.upper_limit:
142
+            band = self.get_band_for_weight(weight)
143
+            return band.charge
144
+        else:
145
+            quotient, remaining_weight = divmod(weight, top_band.upper_limit)
146
+            remainder_band = self.get_band_for_weight(remaining_weight)
147
+            return quotient * top_band.charge + remainder_band.charge
148
+
124 149
     def get_band_for_weight(self, weight):
125 150
         """
126
-        Return the weight band for a given weight
151
+        Return the closest matching weight band for a given weight.
127 152
         """
128
-        bands = self.bands.filter(
129
-            upper_limit__gte=weight).order_by('upper_limit')[:1]
130
-        # Query return only one row, so we can evaluate it
131
-        if not bands:
132
-            # No band for this weight
153
+        try:
154
+            return self.bands.filter(
155
+                upper_limit__gte=weight).order_by('upper_limit')[0]
156
+        except IndexError:
133 157
             return None
134
-        return bands[0]
135 158
 
136 159
     @property
137 160
     def num_bands(self):
138
-        return self.bands.all().count()
161
+        return self.bands.count()
139 162
 
140 163
     @property
141
-    def max_upper_limit(self):
142
-        """
143
-        Return the max upper_limit from this method's weight bands
144
-        """
145
-        bands = self.bands.all().order_by('-upper_limit')
146
-        if bands:
147
-            return bands[0].upper_limit
164
+    def top_band(self):
165
+        try:
166
+            return self.bands.order_by('-upper_limit')[0]
167
+        except IndexError:
168
+            return None
148 169
 
149 170
 
150 171
 class AbstractWeightBand(models.Model):

+ 12
- 0
tests/integration/shipping/model_method_tests.py Dosyayı Görüntüle

@@ -175,3 +175,15 @@ class WeightBasedMethodTests(TestCase):
175 175
         charge = self.standard.calculate(basket)
176 176
 
177 177
         self.assertEqual(expected_charge, charge.excl_tax)
178
+
179
+    def test_overflow_shipping_cost_scenario_handled_correctly(self):
180
+        basket = newfactories.BasketFactory()
181
+        product_attribute_value = newfactories.ProductAttributeValueFactory(
182
+            value_float=2.5)
183
+        basket.add_product(product_attribute_value.product)
184
+
185
+        self.standard.bands.create(upper_limit=1, charge=D('1.00'))
186
+        self.standard.bands.create(upper_limit=2, charge=D('2.00'))
187
+        charge = self.standard.calculate(basket)
188
+
189
+        self.assertEqual(D('1.00') + D('2.00'), charge.excl_tax)

Loading…
İptal
Kaydet