Browse Source

Fixed issue with voucher discounts not being recorded.

Also added tests for vouchers
master
David Winterbottom 13 years ago
parent
commit
0d284ec0d8

+ 2
- 6
oscar/apps/offer/models.py View File

@@ -251,8 +251,8 @@ class Benefit(models.Model):
251 251
             desc = _("%(value).2f discount on %(range)s") % {'value': float(self.value),
252 252
                                                              'range': unicode(self.range).lower()}
253 253
 
254
-        max_item_str = ungettext(" (max %d item)", " (max %d items)", self.max_affected_items)
255
-        desc += max_item_str % self.max_affected_items
254
+        if self.max_affected_items:
255
+            desc += ungettext(" (max 1 item)", " (max %d items)", self.max_affected_items) % self.max_affected_items
256 256
 
257 257
         return desc
258 258
 
@@ -752,7 +752,3 @@ class MultibuyDiscountBenefit(Benefit):
752 752
         else:
753 753
             free_line.discount(discount, 0)
754 754
         return self.round(discount)
755
-
756
-
757
-# We need to import receivers at the bottom of this script
758
-from oscar.apps.offer.receivers import receive_basket_voucher_change

+ 0
- 12
oscar/apps/offer/receivers.py View File

@@ -16,15 +16,3 @@ def receive_basket_voucher_change(sender, **kwargs):
16 16
         voucher = Voucher._default_manager.get(pk=voucher_id)
17 17
         voucher.num_basket_additions += 1
18 18
         voucher.save()
19
-
20
-
21
-@receiver(post_save, sender=OrderDiscount)        
22
-def receive_order_discount_save(sender, instance, **kwargs):
23
-    # Record the amount of discount against the appropriate offers
24
-    # and vouchers
25
-    discount = instance
26
-    if discount.voucher:
27
-        discount.voucher.total_discount += discount.amount
28
-        discount.voucher.save()
29
-    if discount.offer:
30
-        discount.offer.record_usage(discount.amount)

+ 1
- 0
oscar/apps/offer/utils.py View File

@@ -48,6 +48,7 @@ class Applicator(object):
48 48
         offers = self.get_offers(request, basket) 
49 49
         logger.debug("Found %d offers to apply to basket %d", len(offers), basket.id)
50 50
         discounts = self.get_basket_discounts(basket, offers)
51
+
51 52
         # Store this list of discounts with the basket so it can be 
52 53
         # rendered in templates
53 54
         basket.set_discounts(list(discounts.values()))

+ 15
- 6
oscar/apps/order/utils.py View File

@@ -69,8 +69,11 @@ class OrderCreator(object):
69 69
         for line in basket.all_lines():
70 70
             self.create_line_models(order, line)
71 71
             self.update_stock_records(line)
72
+
72 73
         for discount in basket.get_discounts():
73 74
             self.create_discount_model(order, discount)
75
+            self.record_discount(discount)
76
+
74 77
         for voucher in basket.vouchers.all():
75 78
             self.record_voucher_usage(order, voucher, user)
76 79
         
@@ -78,6 +81,8 @@ class OrderCreator(object):
78 81
         order_placed.send(sender=self, order=order, user=user)
79 82
         
80 83
         return order
84
+
85
+
81 86
         
82 87
     def create_order_model(self, user, basket, shipping_address, shipping_method, 
83 88
                            billing_address, total_incl_tax, total_excl_tax, 
@@ -200,17 +205,21 @@ class OrderCreator(object):
200 205
         Creates an order discount model for each discount attached to the basket.
201 206
         """
202 207
         order_discount = OrderDiscount(order=order,
203
-                                       offer_id=discount['offer'].id, 
208
+                                       offer_id=discount['offer'].id,
204 209
                                        amount=discount['discount'])
205
-        if discount['voucher']:
206
-            order_discount.voucher_id = discount['voucher'].id
207
-            order_discount.voucher_code = discount['voucher'].code
210
+        voucher = discount.get('voucher', None)
211
+        if voucher:
212
+            order_discount.voucher_id = voucher.id
213
+            order_discount.voucher_code = voucher.code
208 214
         order_discount.save()
215
+
216
+    def record_discount(self, discount):
217
+        discount['offer'].record_usage(discount['discount'])
218
+        if 'voucher' in discount:
219
+            discount['voucher'].record_discount(discount['discount'])
209 220
         
210 221
     def record_voucher_usage(self, order, voucher, user):
211 222
         """
212 223
         Updates the models that care about this voucher.
213 224
         """
214 225
         voucher.record_usage(order, user)
215
-        voucher.num_orders += 1
216
-        voucher.save()

+ 17
- 6
oscar/apps/voucher/abstract_models.py View File

@@ -8,7 +8,7 @@ from django.utils.translation import ugettext as _
8 8
 
9 9
 class AbstractVoucher(models.Model):
10 10
     """
11
-    A voucher.  This is simply a link to a collection of offers
11
+    A voucher.  This is simply a link to a collection of offers.
12 12
 
13 13
     Note that there are three possible "usage" models:
14 14
     (a) Single use
@@ -19,7 +19,7 @@ class AbstractVoucher(models.Model):
19 19
         help_text=_("""This will be shown in the checkout and basket once the voucher is entered"""))
20 20
     code = models.CharField(_("Code"), max_length=128, db_index=True, unique=True,
21 21
         help_text=_("""Case insensitive / No spaces allowed"""))
22
-    offers = models.ManyToManyField('offer.ConditionalOffer', related_name='vouchers', 
22
+    offers = models.ManyToManyField('offer.ConditionalOffer', related_name='vouchers',
23 23
                                     limit_choices_to={'offer_type': "Voucher"})
24 24
 
25 25
     SINGLE_USE, MULTI_USE, ONCE_PER_CUSTOMER = ('Single use', 'Multi-use', 'Once per customer')
@@ -33,7 +33,7 @@ class AbstractVoucher(models.Model):
33 33
     start_date = models.DateField(_('Start Date'))
34 34
     end_date = models.DateField(_('End Date'))
35 35
 
36
-    # Summary information
36
+    # Audit information
37 37
     num_basket_additions = models.PositiveIntegerField(_('Times added to basket'), default=0)
38 38
     num_orders = models.PositiveIntegerField(_('Times on orders'), default=0)
39 39
     total_discount = models.DecimalField(_('Total discount'), decimal_places=2, max_digits=12, default=Decimal('0.00'))
@@ -59,7 +59,7 @@ class AbstractVoucher(models.Model):
59 59
 
60 60
     def is_active(self, test_date=None):
61 61
         """
62
-        Tests whether this voucher is currently active.
62
+        Test whether this voucher is currently active.
63 63
         """
64 64
         if not test_date:
65 65
             test_date = datetime.date.today()
@@ -67,7 +67,7 @@ class AbstractVoucher(models.Model):
67 67
 
68 68
     def is_available_to_user(self, user=None):
69 69
         """
70
-        Tests whether this voucher is available to the passed user.
70
+        Test whether this voucher is available to the passed user.
71 71
         
72 72
         Returns a tuple of a boolean for whether it is successulf, and a message
73 73
         """
@@ -96,6 +96,15 @@ class AbstractVoucher(models.Model):
96 96
             self.applications.create(voucher=self, order=order, user=user)
97 97
         else:
98 98
             self.applications.create(voucher=self, order=order)
99
+        self.num_orders += 1
100
+        self.save()
101
+
102
+    def record_discount(self, discount):
103
+        """
104
+        Record a discount that this offer has given
105
+        """
106
+        self.total_discount += discount
107
+        self.save()
99 108
 
100 109
     @property
101 110
     def benefit(self):
@@ -120,4 +129,6 @@ class AbstractVoucherApplication(models.Model):
120 129
         verbose_name_plural = _("Voucher Applications")
121 130
 
122 131
     def __unicode__(self):
123
-        return _("'%(voucher)s' used by '%(user)s'") % {'voucher': self.voucher, 'user': self.user}
132
+        return _("'%(voucher)s' used by '%(user)s'") % {
133
+            'voucher': self.voucher,
134
+            'user': self.user}

+ 0
- 2
oscar/apps/voucher/models.py View File

@@ -7,5 +7,3 @@ class Voucher(AbstractVoucher):
7 7
 
8 8
 class VoucherApplication(AbstractVoucherApplication):
9 9
     pass
10
-
11
-

+ 25
- 17
tests/functional/checkout_tests.py View File

@@ -10,6 +10,7 @@ from oscar.test import ClientTestCase, patch_settings
10 10
 from oscar.apps.basket.models import Basket
11 11
 from oscar.apps.order.models import Order
12 12
 from oscar.apps.address.models import Country
13
+from oscar.apps.voucher.models import Voucher
13 14
 
14 15
 
15 16
 class CheckoutMixin(object):
@@ -19,8 +20,9 @@ class CheckoutMixin(object):
19 20
         self.client.post(reverse('basket:add'), {'product_id': product.id,
20 21
                                                  'quantity': 1})
21 22
 
22
-    def add_voucher_to_basket(self):
23
-        voucher = create_voucher()
23
+    def add_voucher_to_basket(self, voucher=None):
24
+        if voucher is None:
25
+            voucher = create_voucher()
24 26
         self.client.post(reverse('basket:vouchers-add'),
25 27
                          {'code': voucher.code})
26 28
 
@@ -41,7 +43,7 @@ class CheckoutMixin(object):
41 43
                                       'postcode': 'N1 9RT',
42 44
                                       'country': 'GB',
43 45
                                      })
44
-        self.assertIsRedirect(response)
46
+        self.assertRedirectUrlName(response, 'checkout:shipping-method')
45 47
 
46 48
     def complete_shipping_method(self):
47 49
         self.client.get(reverse('checkout:shipping-method'))
@@ -205,7 +207,7 @@ class TestPreviewView(ClientTestCase, CheckoutMixin):
205 207
         self.assertIsOk(response)
206 208
 
207 209
 
208
-class PaymentDetailsViewTests(ClientTestCase, CheckoutMixin):
210
+class TestPaymentDetailsView(ClientTestCase, CheckoutMixin):
209 211
 
210 212
     def test_user_must_have_a_nonempty_basket(self):
211 213
         response = self.client.get(reverse('checkout:payment-details'))
@@ -230,12 +232,12 @@ class PaymentDetailsViewTests(ClientTestCase, CheckoutMixin):
230 232
         self.assertRedirectUrlName(response, 'basket:summary')
231 233
 
232 234
 
233
-class OrderPlacementTests(ClientTestCase, CheckoutMixin):
235
+class TestOrderPlacement(ClientTestCase, CheckoutMixin):
234 236
 
235 237
     def setUp(self):
236 238
         Order.objects.all().delete()
237 239
 
238
-        super(OrderPlacementTests, self).setUp()
240
+        super(TestOrderPlacement, self).setUp()
239 241
         self.basket = Basket.objects.create(owner=self.user)
240 242
         self.basket.add_product(create_product(price=D('12.00')))
241 243
 
@@ -253,19 +255,25 @@ class OrderPlacementTests(ClientTestCase, CheckoutMixin):
253 255
         self.assertEqual(1, len(orders))
254 256
         
255 257
 
256
-class TestAnonUserOrderPlacementScenarios(ClientTestCase, CheckoutMixin):
258
+class TestPlacingOrderUsingAVoucher(ClientTestCase, CheckoutMixin):
257 259
 
258
-    def test_basic_submission_gets_redirect_to_thankyou(self):
260
+    def setUp(self):
261
+        self.login()
259 262
         self.add_product_to_basket()
263
+        voucher = create_voucher()
264
+        self.add_voucher_to_basket(voucher)
260 265
         self.complete_shipping_address()
261 266
         self.complete_shipping_method()
262
-        response = self.submit()
263
-        self.assertRedirectUrlName(response, 'checkout:thank-you')
267
+        self.response = self.submit()
264 268
 
265
-    def test_submission_using_voucher(self):
266
-        self.add_product_to_basket()
267
-        self.add_voucher_to_basket()
268
-        self.complete_shipping_address()
269
-        self.complete_shipping_method()
270
-        response = self.submit()
271
-        self.assertRedirectUrlName(response, 'checkout:thank-you')
269
+        # Reload voucher
270
+        self.voucher = Voucher.objects.get(id=voucher.id)
271
+
272
+    def test_is_successful(self):
273
+        self.assertRedirectUrlName(self.response, 'checkout:thank-you')
274
+
275
+    def test_records_use(self):
276
+        self.assertEquals(1, self.voucher.num_orders)
277
+
278
+    def test_records_discount(self):
279
+        self.assertEquals(1, self.voucher.num_orders)

+ 63
- 7
tests/unit/voucher_tests.py View File

@@ -1,11 +1,36 @@
1 1
 import datetime
2
+from decimal import Decimal as D
2 3
 
3 4
 from django.test import TestCase
5
+from django.core import exceptions
6
+from django.contrib.auth.models import User
7
+from django_dynamic_fixture import G
4 8
 
5 9
 from oscar.apps.voucher.models import Voucher
10
+from oscar.apps.order.models import Order
11
+
12
+START_DATE = datetime.date(2011, 01, 01)
13
+END_DATE = datetime.date(2012, 01, 01)
6 14
 
7 15
 
8 16
 class TestVoucher(TestCase):
17
+
18
+    def test_saves_code_as_uppercase(self):
19
+        start = datetime.date(2011, 01, 01)
20
+        end = datetime.date(2012, 01, 01)
21
+        voucher = Voucher.objects.create(code='lower',
22
+                                         start_date=start,
23
+                                         end_date=end)
24
+        self.assertEqual('LOWER', voucher.code)
25
+
26
+    def test_checks_dates_are_sensible(self):
27
+        start = datetime.date(2011, 01, 01)
28
+        end = datetime.date(2012, 01, 01)
29
+        with self.assertRaises(exceptions.ValidationError):
30
+            voucher = Voucher.objects.create(code='lower',
31
+                                            start_date=end,
32
+                                            end_date=start)
33
+            voucher.clean()
9 34
     
10 35
     def test_is_active_between_start_and_end_dates(self):
11 36
         start = datetime.date(2011, 01, 01)
@@ -20,10 +45,41 @@ class TestVoucher(TestCase):
20 45
         end = datetime.date(2011, 02, 01)
21 46
         voucher = Voucher(start_date=start, end_date=end)
22 47
         self.assertFalse(voucher.is_active(test))
23
-        
24
-    def test_codes_are_saved_as_uppercase(self):
25
-        start = datetime.date(2011, 01, 01)
26
-        end = datetime.date(2011, 02, 01)
27
-        voucher = Voucher(name="Dummy voucher", code="lowercase", start_date=start, end_date=end)
28
-        voucher.save()
29
-        self.assertEquals("LOWERCASE", voucher.code)
48
+
49
+    def test_increments_total_discount_when_recording_usage(self):
50
+        voucher = G(Voucher)
51
+        voucher.record_discount(D('10.00'))
52
+        self.assertEqual(voucher.total_discount, D('10.00'))
53
+        voucher.record_discount(D('10.00'))
54
+        self.assertEqual(voucher.total_discount, D('20.00'))
55
+
56
+
57
+class TestMultiuseVoucher(TestCase):
58
+
59
+    def setUp(self):
60
+        self.voucher = G(Voucher, usage=Voucher.MULTI_USE)
61
+
62
+    def test_is_available_to_same_user_multiple_times(self):
63
+        user, order = G(User), G(Order)
64
+        for i in xrange(10):
65
+            self.voucher.record_usage(order, user)
66
+            self.assertTrue(self.voucher.is_available_to_user(user)[0])
67
+
68
+
69
+class TestOncePerCustomerVoucher(TestCase):
70
+
71
+    def setUp(self):
72
+        self.voucher = G(Voucher, usage=Voucher.ONCE_PER_CUSTOMER)
73
+
74
+    def test_is_available_to_a_user_once(self):
75
+        user, order = G(User), G(Order)
76
+        self.assertTrue(self.voucher.is_available_to_user(user)[0])
77
+        self.voucher.record_usage(order, user)
78
+        self.assertFalse(self.voucher.is_available_to_user(user)[0])
79
+
80
+    def test_is_available_to_different_users(self):
81
+        users, order = [G(User), G(User)], G(Order)
82
+        for user in users:
83
+            self.assertTrue(self.voucher.is_available_to_user(user)[0])
84
+            self.voucher.record_usage(order, user)
85
+            self.assertFalse(self.voucher.is_available_to_user(user)[0])

Loading…
Cancel
Save