Bladeren bron

Add new feature Surcharges.

master
Viggo de Vries 5 jaren geleden
bovenliggende
commit
24b349836e

+ 103
- 0
docs/source/howto/how_to_configure_surcharges.rst Bestand weergeven

@@ -0,0 +1,103 @@
1
+=========================
2
+How to configure surcharges
3
+=========================
4
+
5
+A surcharge, also known as checkout fee, is an extra fee charged by a merchant when receiving a payment by cheque, credit card, charge card or debit card (but not cash) which at least covers the cost to the merchant of accepting that means of payment, such as the merchant service fee imposed by a credit card company.
6
+
7
+
8
+Surcharges in Oscar
9
+~~~~~~~~~~~~~~~~~
10
+
11
+Configuring surcharges requires overriding Oscar's core 'checkout' app
12
+and providing your own ``SurchargeApplicator`` class (see :doc:`/topics/customisation`) that
13
+returns your chosen surcharge instances.
14
+
15
+The primary responsibility of the
16
+``SurchargeApplicator`` class is to provide the available surcharge methods for a
17
+particular scenario. This is done via the
18
+:func:`~oscar.apps.checkout.applicator.SurchargeApplicator.get_applicable_surcharges` method,
19
+which returns the surcharges available to the customer.
20
+
21
+This method is called in several places:
22
+
23
+* To look up the "default" surcharges so that sample surcharges can be
24
+  shown on the basket detail page.
25
+
26
+* To give the applicable surcharges to the order total calculator so wo can show the correct price breakdown.
27
+
28
+The ``get_applicable_surcharges`` method takes the basket and any other kwargs. 
29
+These kwargs can later be determined when setting up your own surcharges.
30
+
31
+Note that you can also implement surcharges as models just like shipping methods.
32
+
33
+Custom applicators
34
+-------------------
35
+
36
+If the available surcharges are the same for all customers and payment 
37
+methods, then override the ``get_surcharges`` method of the repository:
38
+
39
+.. code-block:: python
40
+
41
+   from decimal import Decimal as D
42
+   from oscar.apps.checkout import applicator
43
+   from . import surcharges
44
+
45
+   class SurchargeApplicator(applicator.SurchargeApplicator):
46
+       def get_surcharges(self, basket, **kwargs):
47
+           return (
48
+               surcharges.PercentageCharge(percentage=D("2.00")),
49
+           )
50
+
51
+For more complex logic, override the ``is_applicable`` method:
52
+
53
+.. code-block:: python
54
+
55
+   from oscar.apps.checkout import applicator
56
+
57
+   class SurchargeApplicator(applicator.SurchargeApplicator):
58
+
59
+       def is_applicable(self, surcharge, basket, **kwargs):
60
+           payment_method_code = kwargs.get("payment_method_code", None)
61
+           if payment_method is not None and payment_method_code == "paypal":
62
+               return True
63
+           else:
64
+               return False
65
+               
66
+
67
+Surcharges
68
+----------------
69
+
70
+Surcharges need to implement a certain API. They need to have the
71
+following properties which define the metadata about the surcharge:
72
+
73
+* ``name`` - The name of the surcharges. This will be visible to the
74
+  customer during checkout and is translatable
75
+  
76
+* ``code`` - The code of the surcharge. This could be the slugified name or anything else. 
77
+  The code is used as a non-translatable identifier for a charge.
78
+
79
+Further, each surcharge must implement a ``calculate`` method which accepts the
80
+basket instance as a parameter and returns a ``Price`` instance.  Most surcharges
81
+subclass
82
+:class:`~oscar.apps.checkout.surcharges.BaseSurcharge`, which stubs this API.
83
+
84
+
85
+Core surcharges
86
+~~~~~~~~~~~~~~~~~~~~~
87
+
88
+Oscar ships with several re-usable surcharges which can be used as-is, or
89
+subclassed and customised:
90
+
91
+* :class:`~oscar.apps.checkout.surcharges.PercentageCharge` - percentage based charge
92
+
93
+* :class:`~oscar.apps.checkout.surcharges.FlatCharge` - flat surcharge
94
+  
95
+  Example usage:
96
+
97
+.. code-block:: python
98
+
99
+   from decimal import Decimal as D
100
+   from oscar.apps.checkout import surcharges
101
+
102
+   percentage_charge = surcharges.PercentageCharge(percentage=D("2.00"))
103
+   flat_charge = surcharges.FlatCharge(excl_tax=D("10.00"), incl_tax=D("12.10"))

+ 10
- 0
docs/source/howto/index.rst Bestand weergeven

@@ -61,6 +61,16 @@ Shipping
61 61
     :maxdepth: 1
62 62
 
63 63
     how_to_configure_shipping
64
+    
65
+    
66
+Surcharges
67
+----------
68
+
69
+.. toctree::
70
+    :maxdepth: 1
71
+    
72
+    how_to_configure_surcharges
73
+
64 74
 
65 75
 Order processing
66 76
 ----------------

+ 6
- 3
src/oscar/apps/basket/views.py Bestand weergeven

@@ -27,6 +27,7 @@ Repository = get_class('shipping.repository', 'Repository')
27 27
 OrderTotalCalculator = get_class(
28 28
     'checkout.calculators', 'OrderTotalCalculator')
29 29
 BasketMessageGenerator = get_class('basket.utils', 'BasketMessageGenerator')
30
+SurchargeApplicator = get_class("checkout.applicator", "SurchargeApplicator")
30 31
 
31 32
 
32 33
 class BasketView(ModelFormSetView):
@@ -115,9 +116,6 @@ class BasketView(ModelFormSetView):
115 116
         if method.is_discounted:
116 117
             excl_discount = method.calculate_excl_discount(self.request.basket)
117 118
             context['shipping_charge_excl_discount'] = excl_discount
118
-
119
-        context['order_total'] = OrderTotalCalculator().calculate(
120
-            self.request.basket, shipping_charge)
121 119
         context['basket_warnings'] = self.get_basket_warnings(
122 120
             self.request.basket)
123 121
         context['upsell_messages'] = self.get_upsell_messages(
@@ -138,6 +136,11 @@ class BasketView(ModelFormSetView):
138 136
                                                queryset=saved_queryset,
139 137
                                                prefix='saved')
140 138
                     context['saved_formset'] = formset
139
+
140
+        surcharges = SurchargeApplicator(self.request, context).get_applicable_surcharges(self.request.basket)
141
+        context['surcharges'] = surcharges
142
+        context['order_total'] = OrderTotalCalculator().calculate(
143
+            self.request.basket, shipping_charge, surcharges=surcharges)
141 144
         return context
142 145
 
143 146
     def get_success_url(self):

+ 55
- 0
src/oscar/apps/checkout/applicator.py Bestand weergeven

@@ -0,0 +1,55 @@
1
+class SurchargeList(list):
2
+    @property
3
+    def total(self):
4
+        return sum([surcharge.price for surcharge in self])
5
+
6
+
7
+class SurchargePrice():
8
+    surcharge = None
9
+    price = None
10
+
11
+    def __init__(self, surcharge, price):
12
+        self.surcharge = surcharge
13
+        self.price = price
14
+
15
+
16
+class SurchargeApplicator():
17
+
18
+    def __init__(self, request=None, context=None):
19
+        self.context = context
20
+        self.request = request
21
+
22
+    def get_surcharges(self, basket, **kwargs):
23
+        """
24
+        For example::
25
+            return (
26
+                PercentageCharge(percentage=D("2.00")),
27
+                FlatCharge(excl_tax=D("20.0"), incl_tax=D("20.0")),
28
+            )
29
+
30
+        Surcharges must implement the minimal API in ``oscar.apps.checkout.surcharges.BaseSurcharge``.
31
+        Note that you can also make it a model if you want, just like shipping methods.
32
+        """
33
+
34
+        return ()
35
+
36
+    def get_applicable_surcharges(self, basket, **kwargs):
37
+        methods = [
38
+            SurchargePrice(
39
+                surcharge,
40
+                surcharge.calculate(basket=basket, **kwargs)
41
+            )
42
+            for surcharge in self.get_surcharges(basket=basket, **kwargs)
43
+            if self.is_applicable(surcharge=surcharge, basket=basket, **kwargs)
44
+        ]
45
+
46
+        if methods:
47
+            return SurchargeList(methods)
48
+        else:
49
+            return None
50
+
51
+    def is_applicable(self, surcharge, basket, **kwargs):
52
+        """
53
+        Checks if surcharge is applicable to certain conditions
54
+        """
55
+        return True

+ 7
- 1
src/oscar/apps/checkout/calculators.py Bestand weergeven

@@ -13,12 +13,18 @@ class OrderTotalCalculator(object):
13 13
         # always changes the order total.
14 14
         self.request = request
15 15
 
16
-    def calculate(self, basket, shipping_charge, **kwargs):
16
+    def calculate(self, basket, shipping_charge, surcharges=None, **kwargs):
17 17
         excl_tax = basket.total_excl_tax + shipping_charge.excl_tax
18 18
         if basket.is_tax_known and shipping_charge.is_tax_known:
19 19
             incl_tax = basket.total_incl_tax + shipping_charge.incl_tax
20 20
         else:
21 21
             incl_tax = None
22
+
23
+        if surcharges is not None:
24
+            excl_tax += surcharges.total.excl_tax
25
+            if incl_tax is not None:
26
+                incl_tax += surcharges.total.incl_tax
27
+
22 28
         return prices.Price(
23 29
             currency=basket.currency,
24 30
             excl_tax=excl_tax, incl_tax=incl_tax)

+ 4
- 3
src/oscar/apps/checkout/mixins.py Bestand weergeven

@@ -93,7 +93,7 @@ class OrderPlacementMixin(CheckoutSessionMixin):
93 93
     def handle_order_placement(self, order_number, user, basket,
94 94
                                shipping_address, shipping_method,
95 95
                                shipping_charge, billing_address, order_total,
96
-                               **kwargs):
96
+                               surcharges=None, **kwargs):
97 97
         """
98 98
         Write out the order models and return the appropriate HTTP response
99 99
 
@@ -105,13 +105,13 @@ class OrderPlacementMixin(CheckoutSessionMixin):
105 105
             order_number=order_number, user=user, basket=basket,
106 106
             shipping_address=shipping_address, shipping_method=shipping_method,
107 107
             shipping_charge=shipping_charge, order_total=order_total,
108
-            billing_address=billing_address, **kwargs)
108
+            billing_address=billing_address, surcharges=surcharges, **kwargs)
109 109
         basket.submit()
110 110
         return self.handle_successful_order(order)
111 111
 
112 112
     def place_order(self, order_number, user, basket, shipping_address,
113 113
                     shipping_method, shipping_charge, order_total,
114
-                    billing_address=None, **kwargs):
114
+                    billing_address=None, surcharges=None, **kwargs):
115 115
         """
116 116
         Writes the order out to the DB including the payment models
117 117
         """
@@ -145,6 +145,7 @@ class OrderPlacementMixin(CheckoutSessionMixin):
145 145
             billing_address=billing_address,
146 146
             status=status,
147 147
             request=request,
148
+            surcharges=surcharges,
148 149
             **kwargs)
149 150
         self.save_payment_details(order)
150 151
         return order

+ 19
- 12
src/oscar/apps/checkout/session.py Bestand weergeven

@@ -12,6 +12,7 @@ from oscar.core.loading import get_class, get_model
12 12
 from . import exceptions
13 13
 
14 14
 Repository = get_class('shipping.repository', 'Repository')
15
+SurchargeApplicator = get_class("checkout.applicator", "SurchargeApplicator")
15 16
 OrderTotalCalculator = get_class(
16 17
     'checkout.calculators', 'OrderTotalCalculator')
17 18
 CheckoutSessionData = get_class(
@@ -228,6 +229,7 @@ class CheckoutSessionMixin(object):
228 229
         shipping_address = self.get_shipping_address(request.basket)
229 230
         shipping_method = self.get_shipping_method(
230 231
             request.basket, shipping_address)
232
+        surcharges = SurchargeApplicator(request).get_applicable_surcharges(basket=request.basket)
231 233
         if shipping_method:
232 234
             shipping_charge = shipping_method.calculate(request.basket)
233 235
         else:
@@ -238,7 +240,7 @@ class CheckoutSessionMixin(object):
238 240
                 currency=request.basket.currency, excl_tax=D('0.00'),
239 241
                 tax=D('0.00')
240 242
             )
241
-        total = self.get_order_totals(request.basket, shipping_charge)
243
+        total = self.get_order_totals(request.basket, shipping_charge, surcharges)
242 244
         if total.excl_tax == D('0.00'):
243 245
             raise exceptions.PassedSkipCondition(
244 246
                 url=reverse('checkout:preview')
@@ -270,22 +272,27 @@ class CheckoutSessionMixin(object):
270 272
         shipping_method = self.get_shipping_method(
271 273
             basket, shipping_address)
272 274
         billing_address = self.get_billing_address(shipping_address)
273
-        if not shipping_method:
274
-            total = shipping_charge = None
275
-        else:
276
-            shipping_charge = shipping_method.calculate(basket)
277
-            total = self.get_order_totals(
278
-                basket, shipping_charge=shipping_charge, **kwargs)
279 275
         submission = {
280 276
             'user': self.request.user,
281 277
             'basket': basket,
282 278
             'shipping_address': shipping_address,
283 279
             'shipping_method': shipping_method,
284
-            'shipping_charge': shipping_charge,
285 280
             'billing_address': billing_address,
286
-            'order_total': total,
287 281
             'order_kwargs': {},
288
-            'payment_kwargs': {}}
282
+            'payment_kwargs': {}
283
+        }
284
+
285
+        surcharges = SurchargeApplicator(self.request, submission).get_applicable_surcharges(self.request.basket)
286
+        if not shipping_method:
287
+            total = shipping_charge = None
288
+        else:
289
+            shipping_charge = shipping_method.calculate(basket)
290
+            total = self.get_order_totals(
291
+                basket, shipping_charge=shipping_charge, surcharges=surcharges, **kwargs)
292
+
293
+        submission["shipping_charge"] = shipping_charge
294
+        submission["order_total"] = total
295
+        submission['surcharges'] = surcharges
289 296
 
290 297
         # If there is a billing address, add it to the payment kwargs as calls
291 298
         # to payment gateways generally require the billing address. Note, that
@@ -408,9 +415,9 @@ class CheckoutSessionMixin(object):
408 415
                 user_address.populate_alternative_model(billing_address)
409 416
                 return billing_address
410 417
 
411
-    def get_order_totals(self, basket, shipping_charge, **kwargs):
418
+    def get_order_totals(self, basket, shipping_charge, surcharges=None, **kwargs):
412 419
         """
413 420
         Returns the total for the order with and without tax
414 421
         """
415 422
         return OrderTotalCalculator(self.request).calculate(
416
-            basket, shipping_charge, **kwargs)
423
+            basket, shipping_charge, surcharges, **kwargs)

+ 56
- 0
src/oscar/apps/checkout/surcharges.py Bestand weergeven

@@ -0,0 +1,56 @@
1
+from decimal import Decimal as D
2
+
3
+from django.utils.translation import gettext_lazy as _
4
+
5
+from oscar.core import prices
6
+
7
+
8
+class BaseSurcharge:
9
+    """
10
+    Surcharge interface class
11
+
12
+    This is the superclass to the classes in surcharges.py. This allows using all
13
+    surcharges interchangeably (aka polymorphism).
14
+
15
+    The interface is all properties.
16
+    """
17
+
18
+    def calculate(self, basket, **kwargs):
19
+        raise NotImplementedError
20
+
21
+
22
+class PercentageCharge(BaseSurcharge):
23
+    name = _("Percentage surcharge")
24
+    code = "percentage-surcharge"
25
+
26
+    def __init__(self, percentage):
27
+        self.percentage = percentage
28
+
29
+    def calculate(self, basket, **kwargs):
30
+        if basket.total_excl_tax:
31
+            return prices.Price(
32
+                currency=basket.currency,
33
+                excl_tax=basket.total_excl_tax * self.percentage / 100,
34
+                incl_tax=basket.total_incl_tax * self.percentage / 100
35
+            )
36
+        else:
37
+            return prices.Price(
38
+                currency=basket.currency,
39
+                excl_tax=D('0.0'),
40
+                incl_tax=D('0.0')
41
+            )
42
+
43
+
44
+class FlatCharge(BaseSurcharge):
45
+    name = _("Flat surcharge")
46
+    code = "flat-surcharge"
47
+
48
+    def __init__(self, excl_tax=None, incl_tax=None):
49
+        self.excl_tax = excl_tax
50
+        self.incl_tax = incl_tax
51
+
52
+    def calculate(self, basket, **kwargs):
53
+        return prices.Price(
54
+            currency=basket.currency,
55
+            excl_tax=self.excl_tax,
56
+            incl_tax=self.incl_tax)

+ 2
- 2
src/oscar/apps/checkout/views.py Bestand weergeven

@@ -512,7 +512,7 @@ class PaymentDetailsView(OrderPlacementMixin, generic.TemplateView):
512 512
 
513 513
     def submit(self, user, basket, shipping_address, shipping_method,  # noqa (too complex (10))
514 514
                shipping_charge, billing_address, order_total,
515
-               payment_kwargs=None, order_kwargs=None):
515
+               payment_kwargs=None, order_kwargs=None, surcharges=None):
516 516
         """
517 517
         Submit a basket for order placement.
518 518
 
@@ -623,7 +623,7 @@ class PaymentDetailsView(OrderPlacementMixin, generic.TemplateView):
623 623
         try:
624 624
             return self.handle_order_placement(
625 625
                 order_number, user, basket, shipping_address, shipping_method,
626
-                shipping_charge, billing_address, order_total, **order_kwargs)
626
+                shipping_charge, billing_address, order_total, surcharges=surcharges, **order_kwargs)
627 627
         except UnableToPlaceOrder as e:
628 628
             # It's possible that something will go wrong while trying to
629 629
             # actually place an order.  Not a good situation to be in as a

+ 43
- 2
src/oscar/apps/order/abstract_models.py Bestand weergeven

@@ -186,14 +186,14 @@ class AbstractOrder(models.Model):
186 186
         """
187 187
         Return basket total including tax
188 188
         """
189
-        return self.total_incl_tax - self.shipping_incl_tax
189
+        return self.total_incl_tax - self.shipping_incl_tax - self.surcharge_incl_tax
190 190
 
191 191
     @property
192 192
     def basket_total_excl_tax(self):
193 193
         """
194 194
         Return basket total excluding tax
195 195
         """
196
-        return self.total_excl_tax - self.shipping_excl_tax
196
+        return self.total_excl_tax - self.shipping_excl_tax - self.surcharge_excl_tax
197 197
 
198 198
     @property
199 199
     def total_before_discounts_incl_tax(self):
@@ -226,6 +226,14 @@ class AbstractOrder(models.Model):
226 226
     def total_tax(self):
227 227
         return self.total_incl_tax - self.total_excl_tax
228 228
 
229
+    @property
230
+    def surcharge_excl_tax(self):
231
+        return sum(charge.excl_tax for charge in self.surcharges.all())
232
+
233
+    @property
234
+    def surcharge_incl_tax(self):
235
+        return sum(charge.incl_tax for charge in self.surcharges.all())
236
+
229 237
     @property
230 238
     def num_lines(self):
231 239
         return self.lines.count()
@@ -1169,3 +1177,36 @@ class AbstractOrderDiscount(models.Model):
1169 1177
         if self.voucher_code:
1170 1178
             return self.voucher_code
1171 1179
         return self.offer_name or ""
1180
+
1181
+
1182
+class AbstractSurcharge(models.Model):
1183
+    order = models.ForeignKey(
1184
+        'order.Order',
1185
+        on_delete=models.CASCADE,
1186
+        related_name="surcharges",
1187
+        verbose_name=_("Surcharges"))
1188
+
1189
+    name = models.CharField(
1190
+        _("Surcharge"), max_length=128
1191
+    )
1192
+
1193
+    code = models.CharField(
1194
+        _("Surcharge code"), max_length=128
1195
+    )
1196
+
1197
+    incl_tax = models.DecimalField(
1198
+        _("Surcharge (inc. tax)"), decimal_places=2, max_digits=12,
1199
+        default=0)
1200
+
1201
+    excl_tax = models.DecimalField(
1202
+        _("Surcharge (excl. tax)"), decimal_places=2, max_digits=12,
1203
+        default=0)
1204
+
1205
+    @property
1206
+    def tax(self):
1207
+        return self.incl_tax - self.excl_tax
1208
+
1209
+    class Meta:
1210
+        abstract = True
1211
+        app_label = 'order'
1212
+        ordering = ['pk']

+ 6
- 0
src/oscar/apps/order/admin.py Bestand weergeven

@@ -17,6 +17,7 @@ PaymentEventType = get_model('order', 'PaymentEventType')
17 17
 PaymentEventQuantity = get_model('order', 'PaymentEventQuantity')
18 18
 LineAttribute = get_model('order', 'LineAttribute')
19 19
 OrderDiscount = get_model('order', 'OrderDiscount')
20
+Surcharge = get_model('order', 'Surcharge')
20 21
 
21 22
 
22 23
 class LineInline(admin.TabularInline):
@@ -67,6 +68,10 @@ class OrderDiscountAdmin(admin.ModelAdmin):
67 68
                     'voucher_code', 'amount')
68 69
 
69 70
 
71
+class SurchargeAdmin(admin.ModelAdmin):
72
+    raw_id_fields = ("order",)
73
+
74
+
70 75
 admin.site.register(Order, OrderAdmin)
71 76
 admin.site.register(OrderNote)
72 77
 admin.site.register(OrderStatusChange)
@@ -81,3 +86,4 @@ admin.site.register(LineAttribute)
81 86
 admin.site.register(OrderDiscount, OrderDiscountAdmin)
82 87
 admin.site.register(CommunicationEvent)
83 88
 admin.site.register(BillingAddress)
89
+admin.site.register(Surcharge, SurchargeAdmin)

+ 33
- 0
src/oscar/apps/order/migrations/0009_surcharge.py Bestand weergeven

@@ -0,0 +1,33 @@
1
+# Generated by Django 2.2.6 on 2020-02-19 09:16
2
+
3
+from django.db import migrations, models
4
+import django.db.models.deletion
5
+
6
+
7
+class Migration(migrations.Migration):
8
+
9
+    dependencies = [
10
+        ('order', '0008_auto_20190301_1035'),
11
+    ]
12
+    
13
+    replaces=[
14
+        ("order", "0008_surcharge"),
15
+    ]
16
+
17
+    operations = [
18
+        migrations.CreateModel(
19
+            name='Surcharge',
20
+            fields=[
21
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22
+                ('name', models.CharField(max_length=128, verbose_name='Surcharge')),
23
+                ('code', models.CharField(max_length=128, verbose_name='Surcharge code')),
24
+                ('incl_tax', models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='Surcharge (inc. tax)')),
25
+                ('excl_tax', models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='Surcharge (excl. tax)')),
26
+                ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='surcharges', to='order.Order', verbose_name='Surcharges')),
27
+            ],
28
+            options={
29
+                'abstract': False,
30
+                'ordering': ['pk']
31
+            },
32
+        ),
33
+    ]

+ 6
- 0
src/oscar/apps/order/models.py Bestand weergeven

@@ -102,3 +102,9 @@ if not is_model_registered('order', 'OrderDiscount'):
102 102
         pass
103 103
 
104 104
     __all__.append('OrderDiscount')
105
+
106
+if not is_model_registered('order', 'Surcharge'):
107
+    class Surcharge(AbstractSurcharge):
108
+        pass
109
+
110
+    __all__.append('Surcharge')

+ 13
- 2
src/oscar/apps/order/utils.py Bestand weergeven

@@ -16,6 +16,7 @@ OrderDiscount = get_model('order', 'OrderDiscount')
16 16
 CommunicationEvent = get_model('order', 'CommunicationEvent')
17 17
 CommunicationEventType = get_model('communication', 'CommunicationEventType')
18 18
 Dispatcher = get_class('communication.utils', 'Dispatcher')
19
+Surcharge = get_model('order', 'Surcharge')
19 20
 
20 21
 
21 22
 class OrderNumberGenerator(object):
@@ -41,7 +42,7 @@ class OrderCreator(object):
41 42
     def place_order(self, basket, total,  # noqa (too complex (12))
42 43
                     shipping_method, shipping_charge, user=None,
43 44
                     shipping_address=None, billing_address=None,
44
-                    order_number=None, status=None, request=None, **kwargs):
45
+                    order_number=None, status=None, request=None, surcharges=None, **kwargs):
45 46
         """
46 47
         Placing an order involves creating all the relevant models based on the
47 48
         basket and session data.
@@ -60,6 +61,7 @@ class OrderCreator(object):
60 61
 
61 62
         with transaction.atomic():
62 63
 
64
+            kwargs['surcharges'] = surcharges
63 65
             # Ok - everything seems to be in order, let's place the order
64 66
             order = self.create_order_model(
65 67
                 user, basket, shipping_address, shipping_method, shipping_charge,
@@ -106,7 +108,7 @@ class OrderCreator(object):
106 108
 
107 109
     def create_order_model(self, user, basket, shipping_address,
108 110
                            shipping_method, shipping_charge, billing_address,
109
-                           total, order_number, status, request=None, **extra_order_fields):
111
+                           total, order_number, status, request=None, surcharges=None, **extra_order_fields):
110 112
         """Create an order model."""
111 113
         order_data = {'basket': basket,
112 114
                       'number': order_number,
@@ -131,6 +133,15 @@ class OrderCreator(object):
131 133
             order_data['site'] = Site._default_manager.get_current(request)
132 134
         order = Order(**order_data)
133 135
         order.save()
136
+        if surcharges is not None:
137
+            for charge in surcharges:
138
+                Surcharge.objects.create(
139
+                    order=order,
140
+                    name=charge.surcharge.name,
141
+                    code=charge.surcharge.code,
142
+                    excl_tax=charge.price.excl_tax,
143
+                    incl_tax=charge.price.incl_tax
144
+                )
134 145
         return order
135 146
 
136 147
     def create_line_models(self, order, basket_line, extra_line_fields=None):

+ 16
- 0
src/oscar/core/prices.py Bestand weergeven

@@ -54,3 +54,19 @@ class Price(object):
54 54
         return (self.currency == other.currency
55 55
                 and self.excl_tax == other.excl_tax
56 56
                 and self.incl_tax == other.incl_tax)
57
+
58
+    def __add__(self, other):
59
+        if self.currency != other.currency:
60
+            raise ValueError("Cannot add prices with different currencies.")
61
+
62
+        return Price(
63
+            currency=self.currency,
64
+            incl_tax=self.incl_tax + other.incl_tax,
65
+            excl_tax=self.excl_tax + other.excl_tax
66
+        )
67
+
68
+    def __radd__(self, other):
69
+        if other == 0:
70
+            return self
71
+        else:
72
+            return self.__add__(other)

+ 30
- 0
src/oscar/templates/oscar/basket/partials/basket_totals.html Bestand weergeven

@@ -149,6 +149,36 @@
149 149
                 {% endif %}
150 150
             {% endblock %}
151 151
 
152
+            {% block surcharges %}
153
+                {% if surcharges %}
154
+                    <tr>
155
+                        <th>&nbsp;</th>
156
+                        <td></td>
157
+                    </tr>
158
+
159
+                    <tr>
160
+                        <th colspan="2">
161
+                            <h3>{% trans "Surcharges" %}</h3>
162
+                        </th>
163
+                    </tr>
164
+                    {% for surcharge in surcharges %}
165
+                        <tr> 
166
+                            <th class="total"> 
167
+                                {{ surcharge.surcharge.name }}
168
+                            </th>
169
+                            <th class="total align-right">
170
+                                {% if not show_tax_separately and surcharge.price.is_tax_known %}
171
+                                    {{ surcharge.price.incl_tax|currency:basket.currency }}
172
+                                {% else %}
173
+                                    {{ surcharge.price.excl_tax|currency:basket.currency }}
174
+                                {% endif %}
175
+                            </th>
176
+                        </tr>
177
+                    {% endfor %}
178
+                {% endif %}
179
+            {% endblock %}
180
+
181
+
152 182
             {% block tax_totals %}
153 183
                 {% if show_tax_separately %}
154 184
                     <tr>

+ 14
- 0
src/oscar/templates/oscar/dashboard/orders/order_detail.html Bestand weergeven

@@ -237,6 +237,20 @@
237 237
                                     </tr>
238 238
                                 {% endif %}
239 239
 
240
+                                {% with surcharges=order.surcharges.all %}
241
+                                {% if surcharges %}
242
+                                    {% for charge in surcharges %}
243
+                                        <tr>
244
+                                            <td colspan="9"></td>
245
+                                            <th>{% blocktrans with name=charge.name %}Surcharge (name){% endblocktrans %}</th>
246
+                                            <th class="text-right">{{ charge.excl_tax|currency:order.currency }}</th>
247
+                                            <th class="text-right">{{ charge.incl_tax|currency:order.currency }}</th>
248
+                                            <td></td>
249
+                                        </tr>
250
+                                    {% endfor %}
251
+                                {% endif %}
252
+                                {% endwith %}
253
+
240 254
                                 <tr>
241 255
                                     <td colspan="9"></td>
242 256
                                     <th>{% trans "Order total" %}</th>

+ 26
- 0
src/oscar/templates/oscar/order/partials/basket_totals.html Bestand weergeven

@@ -84,6 +84,32 @@
84 84
             {% endif %}
85 85
         {% endblock shipping_total %}
86 86
 
87
+        {% block surcharges %}
88
+            {% with surcharges=order.surcharges.all %}
89
+            {% if surcharges %}
90
+                <tr>
91
+                    <th>&nbsp;</th>
92
+                    <td></td>
93
+                </tr>
94
+                <tr>
95
+                    <th colspan="2"><h3>{% trans "Surcharges" %}</h3></th>
96
+                </tr>
97
+                {% for charge in surcharges %}
98
+                <tr>
99
+                        <th class="total">{{ charge.name }}</th>
100
+                        <th class="total align-right"> 
101
+                            {% if show_tax_separately %}
102
+                                {{ charge.excl_tax|currency:order.currency }}
103
+                            {% else %}
104
+                                {{ charge.incl_tax|currency:order.currency }}
105
+                            {% endif %}
106
+                        </th>
107
+                </tr>
108
+                {% endfor %}
109
+            {% endif %}
110
+            {% endwith %}
111
+        {% endblock %}
112
+
87 113
         {% if show_tax_separately %}
88 114
             <tr>
89 115
                 <th colspan="2">&nbsp;</th>

+ 4
- 1
src/oscar/test/factories/__init__.py Bestand weergeven

@@ -26,6 +26,7 @@ Voucher = get_model('voucher', 'Voucher')
26 26
 OrderCreator = get_class('order.utils', 'OrderCreator')
27 27
 OrderTotalCalculator = get_class('checkout.calculators',
28 28
                                  'OrderTotalCalculator')
29
+SurchargeApplicator = get_class('checkout.applicator', 'SurchargeApplicator')
29 30
 Partner = get_model('partner', 'Partner')
30 31
 StockRecord = get_model('partner', 'StockRecord')
31 32
 PurchaseInfo = get_class('partner.strategy', 'PurchaseInfo')
@@ -164,8 +165,10 @@ def create_order(number=None, basket=None, user=None, shipping_address=None,
164 165
     if shipping_method is None:
165 166
         shipping_method = Free()
166 167
     shipping_charge = shipping_method.calculate(basket)
168
+    surcharges = SurchargeApplicator().get_applicable_surcharges(basket)
167 169
     if total is None:
168
-        total = OrderTotalCalculator().calculate(basket, shipping_charge)
170
+        total = OrderTotalCalculator().calculate(basket, shipping_charge, surcharges)
171
+    kwargs['surcharges'] = surcharges
169 172
     order = OrderCreator().place_order(
170 173
         order_number=number,
171 174
         user=user,

+ 1
- 0
tests/_site/apps/checkout/__init__.py Bestand weergeven

@@ -0,0 +1 @@
1
+default_app_config = 'tests._site.apps.checkout.apps.CheckoutConfig'

+ 22
- 0
tests/_site/apps/checkout/applicator.py Bestand weergeven

@@ -0,0 +1,22 @@
1
+from decimal import Decimal as D
2
+
3
+from oscar.apps.checkout.applicator import (
4
+    SurchargeApplicator as BaseSurchargeApplicator)
5
+from oscar.apps.checkout.surcharges import FlatCharge
6
+
7
+
8
+class SurchargeApplicator(BaseSurchargeApplicator):
9
+    def get_surcharges(self, basket, **kwargs):
10
+        return (
11
+            FlatCharge(excl_tax=D("10.0"), incl_tax=D("10.0")),
12
+            FlatCharge(excl_tax=D("10.0"), incl_tax=D("12.0")),
13
+        )
14
+
15
+    def is_applicable(self, surcharge, basket, **kwargs):
16
+        if surcharge.incl_tax > surcharge.excl_tax:
17
+            if basket.is_tax_known:
18
+                return True
19
+        else:
20
+            return True
21
+
22
+        return False

+ 5
- 0
tests/_site/apps/checkout/apps.py Bestand weergeven

@@ -0,0 +1,5 @@
1
+from oscar.apps.checkout import apps
2
+
3
+
4
+class CheckoutConfig(apps.CheckoutConfig):
5
+    name = 'tests._site.apps.checkout'

+ 4
- 4
tests/functional/dashboard/test_dashboard.py Bestand weergeven

@@ -146,7 +146,7 @@ class TestDashboardIndexStatsForNonStaffUser(WebTestCase):
146 146
         context = response.context
147 147
         self.assertEqual(context['total_orders_last_day'], 1)
148 148
         self.assertEqual(context['total_lines_last_day'], 1)
149
-        self.assertEqual(context['total_revenue_last_day'], D(5))
149
+        self.assertEqual(context['total_revenue_last_day'], D(27))
150 150
         self.assertEqual(context['total_customers_last_day'], 1)
151 151
         self.assertEqual(context['total_open_baskets_last_day'], 1)
152 152
         self.assertEqual(context['total_products'], 1)
@@ -156,7 +156,7 @@ class TestDashboardIndexStatsForNonStaffUser(WebTestCase):
156 156
         self.assertEqual(context['total_open_baskets'], 1)
157 157
         self.assertEqual(context['total_orders'], 1)
158 158
         self.assertEqual(context['total_lines'], 1)
159
-        self.assertEqual(context['total_revenue'], D(5))
159
+        self.assertEqual(context['total_revenue'], D(27))
160 160
 
161 161
     def test_partner2(self):
162 162
         user = self.create_user(username='user', email='testuser@example.com')
@@ -166,7 +166,7 @@ class TestDashboardIndexStatsForNonStaffUser(WebTestCase):
166 166
         context = response.context
167 167
         self.assertEqual(context['total_orders_last_day'], 9)
168 168
         self.assertEqual(context['total_lines_last_day'], 9)
169
-        self.assertEqual(context['total_revenue_last_day'], D(90))
169
+        self.assertEqual(context['total_revenue_last_day'], D(288))
170 170
         self.assertEqual(context['total_customers_last_day'], 1)
171 171
         self.assertEqual(context['total_open_baskets_last_day'], 0)
172 172
         self.assertEqual(context['total_products'], 2)
@@ -176,4 +176,4 @@ class TestDashboardIndexStatsForNonStaffUser(WebTestCase):
176 176
         self.assertEqual(context['total_open_baskets'], 0)
177 177
         self.assertEqual(context['total_orders'], 9)
178 178
         self.assertEqual(context['total_lines'], 9)
179
-        self.assertEqual(context['total_revenue'], D(90))
179
+        self.assertEqual(context['total_revenue'], D(288))

+ 15
- 6
tests/integration/checkout/test_calculator.py Bestand weergeven

@@ -5,6 +5,9 @@ from django.test import TestCase
5 5
 
6 6
 from oscar.apps.checkout import calculators
7 7
 from oscar.core import prices
8
+from oscar.core.loading import get_class
9
+
10
+SurchargeApplicator = get_class("checkout.applicator", "SurchargeApplicator")
8 11
 
9 12
 
10 13
 class TestOrderTotalCalculator(TestCase):
@@ -20,10 +23,13 @@ class TestOrderTotalCalculator(TestCase):
20 23
         shipping_charge = prices.Price(
21 24
             currency=basket.currency, excl_tax=D('5.00'))
22 25
 
23
-        total = self.calculator.calculate(basket, shipping_charge)
26
+        applicator = SurchargeApplicator()
27
+        surcharges = applicator.get_applicable_surcharges(basket)
28
+
29
+        total = self.calculator.calculate(basket, shipping_charge, surcharges)
24 30
 
25 31
         self.assertIsInstance(total, prices.Price)
26
-        self.assertEqual(D('10.00') + D('5.00'), total.excl_tax)
32
+        self.assertEqual(D('10.00') + D('5.00') + D('10.0'), total.excl_tax)
27 33
         self.assertFalse(total.is_tax_known)
28 34
 
29 35
     def test_returns_correct_totals_when_tax_is_known(self):
@@ -36,10 +42,13 @@ class TestOrderTotalCalculator(TestCase):
36 42
             currency=basket.currency, excl_tax=D('5.00'),
37 43
             tax=D('0.50'))
38 44
 
39
-        total = self.calculator.calculate(basket, shipping_charge)
45
+        applicator = SurchargeApplicator()
46
+        surcharges = applicator.get_applicable_surcharges(basket)
47
+
48
+        total = self.calculator.calculate(basket, shipping_charge, surcharges)
40 49
 
41 50
         self.assertIsInstance(total, prices.Price)
42
-        self.assertEqual(D('10.00') + D('5.00'), total.excl_tax)
51
+        self.assertEqual(D('10.00') + D('5.00') + D('20.0'), total.excl_tax)
43 52
         self.assertTrue(total.is_tax_known)
44
-        self.assertEqual(D('12.00') + D('5.50'), total.incl_tax)
45
-        self.assertEqual(D('2.00') + D('0.50'), total.tax)
53
+        self.assertEqual(D('12.00') + D('5.50') + D('22.00'), total.incl_tax)
54
+        self.assertEqual(D('2.00') + D('0.50') + D('2.00'), total.tax)

+ 27
- 4
tests/integration/checkout/test_mixins.py Bestand weergeven

@@ -10,12 +10,15 @@ from oscar.apps.checkout.exceptions import FailedPreCondition
10 10
 from oscar.apps.checkout.mixins import (
11 11
     CheckoutSessionMixin, OrderPlacementMixin)
12 12
 from oscar.apps.shipping.methods import FixedPrice, Free
13
-from oscar.core.loading import get_model
13
+from oscar.core.loading import get_class, get_model
14 14
 from oscar.test import factories
15 15
 from oscar.test.basket import add_product
16 16
 from oscar.test.utils import RequestFactory
17 17
 
18 18
 Order = get_model('order', 'Order')
19
+Surcharge = get_model('order', 'Surcharge')
20
+
21
+SurchargeApplicator = get_class("checkout.applicator", "SurchargeApplicator")
19 22
 
20 23
 
21 24
 class TestOrderPlacementMixin(TestCase):
@@ -39,7 +42,9 @@ class TestOrderPlacementMixin(TestCase):
39 42
                                                             line2='159',
40 43
                                                             line4='London')
41 44
         shipping_charge = shipping_method.calculate(basket)
42
-        order_total = OrderTotalCalculator().calculate(basket, shipping_charge)
45
+        applicator = SurchargeApplicator()
46
+        surcharges = applicator.get_applicable_surcharges(basket)
47
+        order_total = OrderTotalCalculator().calculate(basket, shipping_charge, surcharges)
43 48
 
44 49
         order_submission_data = {'user': user,
45 50
                                  'order_number': '12345',
@@ -99,7 +104,9 @@ class TestOrderPlacementMixin(TestCase):
99 104
         add_product(basket, D('12.00'))
100 105
         shipping_method = Free()
101 106
         shipping_charge = shipping_method.calculate(basket)
102
-        order_total = OrderTotalCalculator().calculate(basket, shipping_charge)
107
+        applicator = SurchargeApplicator()
108
+        surcharges = applicator.get_applicable_surcharges(basket)
109
+        order_total = OrderTotalCalculator().calculate(basket, shipping_charge, surcharges)
103 110
 
104 111
         billing_address = factories.BillingAddressFactory()
105 112
         shipping_address = factories.ShippingAddressFactory()
@@ -114,6 +121,13 @@ class TestOrderPlacementMixin(TestCase):
114 121
                                  'request': request}
115 122
         OrderPlacementMixin().place_order(**order_submission_data)
116 123
         order1 = Order.objects.get(number='12345')
124
+        for charge in surcharges:
125
+            Surcharge.objects.create(
126
+                order=order1,
127
+                name=charge.surcharge.name,
128
+                code=charge.surcharge.code,
129
+                excl_tax=charge.price.excl_tax,
130
+                incl_tax=charge.price.incl_tax)
117 131
         self.assertEqual(order1.site, site1)
118 132
 
119 133
         add_product(basket, D('12.00'))
@@ -133,7 +147,9 @@ class TestOrderPlacementMixin(TestCase):
133 147
         order_placement.add_payment_event('Credit Card Payment', D('90'))
134 148
         shipping_method = Free()
135 149
         shipping_charge = shipping_method.calculate(basket)
136
-        order_total = OrderTotalCalculator().calculate(basket, shipping_charge)
150
+        applicator = SurchargeApplicator()
151
+        surcharges = applicator.get_applicable_surcharges(basket)
152
+        order_total = OrderTotalCalculator().calculate(basket, shipping_charge, surcharges)
137 153
 
138 154
         billing_address = factories.BillingAddressFactory()
139 155
         shipping_address = factories.ShippingAddressFactory()
@@ -147,6 +163,13 @@ class TestOrderPlacementMixin(TestCase):
147 163
                                  'shipping_address': shipping_address}
148 164
         order_placement.place_order(**order_submission_data)
149 165
         order1 = Order.objects.get(number='12345')
166
+        for charge in surcharges:
167
+            Surcharge.objects.create(
168
+                order=order1,
169
+                name=charge.surcharge.name,
170
+                code=charge.surcharge.code,
171
+                excl_tax=charge.price.excl_tax,
172
+                incl_tax=charge.price.incl_tax)
150 173
         self.assertEqual(order1.payment_events.count(), 2)
151 174
         event1 = order1.payment_events.all()[0]
152 175
         event2 = order1.payment_events.all()[1]

+ 48
- 0
tests/integration/checkout/test_surcharges.py Bestand weergeven

@@ -0,0 +1,48 @@
1
+from decimal import Decimal as D
2
+
3
+from django.test import TestCase
4
+
5
+from oscar.core.loading import get_class
6
+from oscar.test import factories
7
+from oscar.test.basket import add_product
8
+
9
+SurchargeApplicator = get_class("checkout.applicator", "SurchargeApplicator")
10
+PercentageCharge = get_class("checkout.surcharges", "PercentageCharge")
11
+FlatCharge = get_class("checkout.surcharges", "FlatCharge")
12
+
13
+
14
+class TestSurcharges(TestCase):
15
+    def setUp(self):
16
+        self.applicator = SurchargeApplicator()
17
+        self.basket = factories.create_basket(empty=True)
18
+
19
+    def test_stock_surcharges(self):
20
+        add_product(self.basket, D('12.00'))
21
+        surcharges = self.applicator.get_applicable_surcharges(self.basket)
22
+
23
+        self.assertEqual(surcharges.total.excl_tax, D('20.0'))
24
+        self.assertEqual(surcharges.total.incl_tax, D('22.0'))
25
+
26
+    def test_percentage_surcharge(self):
27
+        percentage_surcharge = PercentageCharge(percentage=D(10))
28
+        add_product(self.basket, D(12))
29
+        price = percentage_surcharge.calculate(self.basket)
30
+
31
+        self.assertEqual(self.basket.total_incl_tax, D(12))
32
+        self.assertEqual(price.incl_tax, D("1.20"))
33
+
34
+    def test_percentage_empty_basket(self):
35
+        percentage_surcharge = PercentageCharge(percentage=D(10))
36
+        price = percentage_surcharge.calculate(self.basket)
37
+
38
+        self.assertEqual(self.basket.total_incl_tax, D(0))
39
+        self.assertEqual(price.incl_tax, D(0))
40
+
41
+    def test_flat_surcharge(self):
42
+        flat_surcharge = FlatCharge(excl_tax=D(1), incl_tax=D("1.21"))
43
+        add_product(self.basket, D(12))
44
+        price = flat_surcharge.calculate(self.basket)
45
+
46
+        self.assertEqual(self.basket.total_incl_tax, D(12))
47
+        self.assertEqual(price.incl_tax, D("1.21"))
48
+        self.assertEqual(price.excl_tax, D(1))

+ 43
- 21
tests/integration/order/test_creator.py Bestand weergeven

@@ -26,6 +26,8 @@ from oscar.test.utils import run_concurrently
26 26
 Range = get_class('offer.models', 'Range')
27 27
 Benefit = get_class('offer.models', 'Benefit')
28 28
 
29
+SurchargeApplicator = get_class("checkout.applicator", "SurchargeApplicator")
30
+
29 31
 
30 32
 def place_order(creator, **kwargs):
31 33
     """
@@ -35,8 +37,9 @@ def place_order(creator, **kwargs):
35 37
         kwargs['shipping_method'] = Free()
36 38
 
37 39
     shipping_charge = kwargs['shipping_method'].calculate(kwargs['basket'])
40
+
38 41
     kwargs['total'] = calculators.OrderTotalCalculator().calculate(
39
-        basket=kwargs['basket'], shipping_charge=shipping_charge)
42
+        basket=kwargs['basket'], shipping_charge=shipping_charge, surcharges=kwargs['surcharges'])
40 43
     kwargs['shipping_charge'] = shipping_charge
41 44
 
42 45
     return creator.place_order(**kwargs)
@@ -47,16 +50,17 @@ class TestOrderCreatorErrorCases(TestCase):
47 50
     def setUp(self):
48 51
         self.creator = OrderCreator()
49 52
         self.basket = factories.create_basket(empty=True)
53
+        self.surcharges = SurchargeApplicator().get_applicable_surcharges(self.basket)
50 54
 
51 55
     def test_raises_exception_when_empty_basket_passed(self):
52 56
         with self.assertRaises(ValueError):
53
-            place_order(self.creator, basket=self.basket)
57
+            place_order(self.creator, surcharges=self.surcharges, basket=self.basket)
54 58
 
55 59
     def test_raises_exception_if_duplicate_order_number_passed(self):
56 60
         add_product(self.basket, D('12.00'))
57
-        place_order(self.creator, basket=self.basket, order_number='1234')
61
+        place_order(self.creator, surcharges=self.surcharges, basket=self.basket, order_number='1234')
58 62
         with self.assertRaises(ValueError):
59
-            place_order(self.creator, basket=self.basket, order_number='1234')
63
+            place_order(self.creator, surcharges=self.surcharges, basket=self.basket, order_number='1234')
60 64
 
61 65
 
62 66
 class TestSuccessfulOrderCreation(TestCase):
@@ -64,46 +68,47 @@ class TestSuccessfulOrderCreation(TestCase):
64 68
     def setUp(self):
65 69
         self.creator = OrderCreator()
66 70
         self.basket = factories.create_basket(empty=True)
71
+        self.surcharges = SurchargeApplicator().get_applicable_surcharges(self.basket)
67 72
 
68 73
     def test_saves_shipping_code(self):
69 74
         add_product(self.basket, D('12.00'))
70 75
         free_method = Free()
71
-        order = place_order(self.creator, basket=self.basket,
76
+        order = place_order(self.creator, surcharges=self.surcharges, basket=self.basket,
72 77
                             order_number='1234', shipping_method=free_method)
73 78
         self.assertEqual(order.shipping_code, free_method.code)
74 79
 
75 80
     def test_creates_order_and_line_models(self):
76 81
         add_product(self.basket, D('12.00'))
77
-        place_order(self.creator, basket=self.basket, order_number='1234')
82
+        place_order(self.creator, surcharges=self.surcharges, basket=self.basket, order_number='1234')
78 83
         order = Order.objects.get(number='1234')
79 84
         lines = order.lines.all()
80 85
         self.assertEqual(1, len(lines))
81 86
 
82 87
     def test_sets_correct_order_status(self):
83 88
         add_product(self.basket, D('12.00'))
84
-        place_order(self.creator, basket=self.basket,
89
+        place_order(self.creator, surcharges=self.surcharges, basket=self.basket,
85 90
                     order_number='1234', status='Active')
86 91
         order = Order.objects.get(number='1234')
87 92
         self.assertEqual('Active', order.status)
88 93
 
89 94
     def test_defaults_to_using_free_shipping(self):
90 95
         add_product(self.basket, D('12.00'))
91
-        place_order(self.creator, basket=self.basket, order_number='1234')
96
+        place_order(self.creator, surcharges=self.surcharges, basket=self.basket, order_number='1234')
92 97
         order = Order.objects.get(number='1234')
93
-        self.assertEqual(order.total_incl_tax, self.basket.total_incl_tax)
94
-        self.assertEqual(order.total_excl_tax, self.basket.total_excl_tax)
98
+        self.assertEqual(order.total_incl_tax, self.basket.total_incl_tax + self.surcharges.total.incl_tax)
99
+        self.assertEqual(order.total_excl_tax, self.basket.total_excl_tax + self.surcharges.total.excl_tax)
95 100
 
96 101
     def test_uses_default_order_status_from_settings(self):
97 102
         add_product(self.basket, D('12.00'))
98 103
         with override_settings(OSCAR_INITIAL_ORDER_STATUS='A'):
99
-            place_order(self.creator, basket=self.basket, order_number='1234')
104
+            place_order(self.creator, surcharges=self.surcharges, basket=self.basket, order_number='1234')
100 105
         order = Order.objects.get(number='1234')
101 106
         self.assertEqual('A', order.status)
102 107
 
103 108
     def test_uses_default_line_status_from_settings(self):
104 109
         add_product(self.basket, D('12.00'))
105 110
         with override_settings(OSCAR_INITIAL_LINE_STATUS='A'):
106
-            place_order(self.creator, basket=self.basket, order_number='1234')
111
+            place_order(self.creator, surcharges=self.surcharges, basket=self.basket, order_number='1234')
107 112
         order = Order.objects.get(number='1234')
108 113
         line = order.lines.all()[0]
109 114
         self.assertEqual('A', line.status)
@@ -114,7 +119,7 @@ class TestSuccessfulOrderCreation(TestCase):
114 119
             product = factories.create_product(partner_name=partner_name)
115 120
             add_product(self.basket, D('12.00'), product=product)
116 121
             place_order(
117
-                self.creator, basket=self.basket, order_number=order_number)
122
+                self.creator, surcharges=self.surcharges, basket=self.basket, order_number=order_number)
118 123
             line = Order.objects.get(number=order_number).lines.all()[0]
119 124
             partner = product.stockrecords.all()[0].partner
120 125
             self.assertTrue(partner_name == line.partner_name == partner.name)
@@ -125,6 +130,7 @@ class TestPlacingOrderForDigitalGoods(TestCase):
125 130
     def setUp(self):
126 131
         self.creator = OrderCreator()
127 132
         self.basket = factories.create_basket(empty=True)
133
+        self.surcharges = SurchargeApplicator().get_applicable_surcharges(self.basket)
128 134
 
129 135
     def test_does_not_allocate_stock(self):
130 136
         ProductClass.objects.create(
@@ -134,7 +140,7 @@ class TestPlacingOrderForDigitalGoods(TestCase):
134 140
         self.assertTrue(record.num_allocated is None)
135 141
 
136 142
         add_product(self.basket, D('12.00'), product=product)
137
-        place_order(self.creator, basket=self.basket, order_number='1234')
143
+        place_order(self.creator, surcharges=self.surcharges, basket=self.basket, order_number='1234')
138 144
 
139 145
         product = Product.objects.get(id=product.id)
140 146
         stockrecord = product.stockrecords.all()[0]
@@ -147,6 +153,7 @@ class TestShippingOfferForOrder(TestCase):
147 153
     def setUp(self):
148 154
         self.creator = OrderCreator()
149 155
         self.basket = factories.create_basket(empty=True)
156
+        self.surcharges = SurchargeApplicator().get_applicable_surcharges(self.basket)
150 157
 
151 158
     def apply_20percent_shipping_offer(self):
152 159
         """Shipping offer 20% off"""
@@ -167,6 +174,7 @@ class TestShippingOfferForOrder(TestCase):
167 174
             self.basket, shipping, offer)
168 175
 
169 176
         place_order(self.creator,
177
+                    surcharges=self.surcharges,
170 178
                     basket=self.basket,
171 179
                     order_number='1234',
172 180
                     shipping_method=shipping)
@@ -174,7 +182,7 @@ class TestShippingOfferForOrder(TestCase):
174 182
 
175 183
         self.assertEqual(1, len(order.shipping_discounts))
176 184
         self.assertEqual(D('4.00'), order.shipping_incl_tax)
177
-        self.assertEqual(D('16.00'), order.total_incl_tax)
185
+        self.assertEqual(D('38.00'), order.total_incl_tax)
178 186
 
179 187
     def test_zero_shipping_discount_is_not_created(self):
180 188
         add_product(self.basket, D('12.00'))
@@ -185,6 +193,7 @@ class TestShippingOfferForOrder(TestCase):
185 193
             self.basket, shipping, offer)
186 194
 
187 195
         place_order(self.creator,
196
+                    surcharges=self.surcharges,
188 197
                     basket=self.basket,
189 198
                     order_number='1234',
190 199
                     shipping_method=shipping)
@@ -193,7 +202,7 @@ class TestShippingOfferForOrder(TestCase):
193 202
         # No shipping discount
194 203
         self.assertEqual(0, len(order.shipping_discounts))
195 204
         self.assertEqual(D('0.00'), order.shipping_incl_tax)
196
-        self.assertEqual(D('12.00'), order.total_incl_tax)
205
+        self.assertEqual(D('34.00'), order.total_incl_tax)
197 206
 
198 207
 
199 208
 class TestMultiSiteOrderCreation(TestCase):
@@ -203,10 +212,12 @@ class TestMultiSiteOrderCreation(TestCase):
203 212
         self.basket = factories.create_basket(empty=True)
204 213
         self.site1 = factories.SiteFactory()
205 214
         self.site2 = factories.SiteFactory()
215
+        self.surcharges = SurchargeApplicator().get_applicable_surcharges(self.basket)
206 216
 
207 217
     def test_default_site(self):
208 218
         add_product(self.basket, D('12.00'))
209 219
         place_order(self.creator,
220
+                    surcharges=self.surcharges,
210 221
                     basket=self.basket,
211 222
                     order_number='1234')
212 223
         order = Order.objects.get(number='1234')
@@ -215,6 +226,7 @@ class TestMultiSiteOrderCreation(TestCase):
215 226
     def test_multi_sites(self):
216 227
         add_product(self.basket, D('12.00'))
217 228
         place_order(self.creator,
229
+                    surcharges=self.surcharges,
218 230
                     basket=self.basket,
219 231
                     order_number='12345',
220 232
                     site=self.site1)
@@ -222,6 +234,7 @@ class TestMultiSiteOrderCreation(TestCase):
222 234
         self.assertEqual(order1.site, self.site1)
223 235
         add_product(self.basket, D('12.00'))
224 236
         place_order(self.creator,
237
+                    surcharges=self.surcharges,
225 238
                     basket=self.basket,
226 239
                     order_number='12346',
227 240
                     site=self.site2)
@@ -235,6 +248,7 @@ class TestMultiSiteOrderCreation(TestCase):
235 248
         request.META['SERVER_NAME'] = self.site1.domain
236 249
         add_product(self.basket, D('12.00'))
237 250
         place_order(self.creator,
251
+                    surcharges=self.surcharges,
238 252
                     basket=self.basket,
239 253
                     order_number='12345',
240 254
                     request=request)
@@ -243,6 +257,7 @@ class TestMultiSiteOrderCreation(TestCase):
243 257
         add_product(self.basket, D('12.00'))
244 258
         request.META['SERVER_NAME'] = self.site2.domain
245 259
         place_order(self.creator,
260
+                    surcharges=self.surcharges,
246 261
                     basket=self.basket,
247 262
                     order_number='12346',
248 263
                     request=request)
@@ -261,12 +276,14 @@ class TestPlaceOrderWithVoucher(TestCase):
261 276
         voucher.offers.add(factories.create_offer(offer_type='Voucher'))
262 277
         basket.vouchers.add(voucher)
263 278
 
264
-        place_order(creator, basket=basket, order_number='12346', user=user)
279
+        surcharges = SurchargeApplicator().get_applicable_surcharges(basket)
280
+
281
+        place_order(creator, surcharges=surcharges, basket=basket, order_number='12346', user=user)
265 282
         assert voucher.applications.count() == 1
266 283
 
267 284
         # Make sure the voucher usage is rechecked
268 285
         with pytest.raises(ValueError):
269
-            place_order(creator, basket=basket, order_number='12347', user=user)
286
+            place_order(creator, surcharges=surcharges, basket=basket, order_number='12347', user=user)
270 287
 
271 288
     def test_expired_voucher(self):
272 289
         user = AnonymousUser()
@@ -278,7 +295,8 @@ class TestPlaceOrderWithVoucher(TestCase):
278 295
         basket.vouchers.add(voucher)
279 296
         voucher.end_datetime = timezone.now() - datetime.timedelta(days=100)
280 297
         voucher.save()
281
-        place_order(creator, basket=basket, order_number='12346', user=user)
298
+        surcharges = SurchargeApplicator().get_applicable_surcharges(basket)
299
+        place_order(creator, surcharges=surcharges, basket=basket, order_number='12346', user=user)
282 300
         assert voucher.applications.count() == 0
283 301
 
284 302
 
@@ -304,8 +322,10 @@ class TestConcurrentOrderPlacement(TransactionTestCase):
304 322
 
305 323
             basket = factories.BasketFactory()
306 324
             basket.add_product(product)
325
+            surcharges = SurchargeApplicator().get_applicable_surcharges(basket)
326
+
307 327
             place_order(
308
-                creator, basket=basket, order_number=order_number, user=user)
328
+                creator, surcharges=surcharges, basket=basket, order_number=order_number, user=user)
309 329
 
310 330
         exceptions = run_concurrently(worker, num_threads=5)
311 331
 
@@ -346,8 +366,10 @@ class TestConcurrentOrderPlacement(TransactionTestCase):
346 366
             basket = factories.BasketFactory()
347 367
             basket.add_product(product)
348 368
             basket.vouchers.add(voucher)
369
+
370
+            surcharges = SurchargeApplicator().get_applicable_surcharges(basket)
349 371
             place_order(
350
-                creator, basket=basket, order_number=order_number, user=user)
372
+                creator, surcharges=surcharges, basket=basket, order_number=order_number, user=user)
351 373
 
352 374
         exceptions = run_concurrently(worker, num_threads=5)
353 375
 

+ 2
- 0
tests/settings.py Bestand weergeven

@@ -81,6 +81,8 @@ catalogue_app_idx = INSTALLED_APPS.index('oscar.apps.catalogue.apps.CatalogueCon
81 81
 INSTALLED_APPS[catalogue_app_idx] = 'tests._site.apps.catalogue.apps.CatalogueConfig'
82 82
 dashboard_app_idx = INSTALLED_APPS.index('oscar.apps.dashboard.apps.DashboardConfig')
83 83
 INSTALLED_APPS[dashboard_app_idx] = 'tests._site.apps.dashboard.apps.DashboardConfig'
84
+checkout_app_idx = INSTALLED_APPS.index('oscar.apps.checkout.apps.CheckoutConfig')
85
+INSTALLED_APPS[checkout_app_idx] = 'tests._site.apps.checkout.apps.CheckoutConfig'
84 86
 
85 87
 AUTH_USER_MODEL = 'myauth.User'
86 88
 

Laden…
Annuleren
Opslaan