|
@@ -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):
|