Browse Source

Vouchers now work through the ordering process

master
David Winterbottom 14 years ago
parent
commit
e1c3e68f12

+ 2
- 1
oscar/basket/signals.py View File

@@ -1,3 +1,4 @@
1 1
 import django.dispatch
2 2
 
3
-basket_addition = django.dispatch.Signal(providing_args=["product", "user"])
3
+basket_addition = django.dispatch.Signal(providing_args=["product", "user"])
4
+basket_voucher = django.dispatch.Signal(providing_args=["basket", "voucher"])

+ 11
- 2
oscar/basket/views.py View File

@@ -73,13 +73,22 @@ class BasketView(ModelView):
73 73
     
74 74
     def do_add_voucher(self, basket):
75 75
         code = self.request.POST['voucher_code']
76
+        # First check if the voucher is already in the basket
77
+        try:
78
+            voucher = basket.vouchers.get(code=code)
79
+            messages.error(self.request, "You have already added the '%s' voucher to your basket" % voucher.code)
80
+            return
81
+        except ObjectDoesNotExist:    
82
+            pass
83
+        
76 84
         try:
77 85
             voucher = offer_models.Voucher._default_manager.get(code=code)
78 86
             if not voucher.is_active():
79 87
                 messages.error(self.request, "The '%s' voucher has expired" % voucher.code)
80 88
                 return
81
-            if not voucher.is_available_to_user(self.request.user):
82
-                messages.error(self.request, "The '%s' voucher has already been used" % voucher.code)
89
+            is_available, message = voucher.is_available_to_user(self.request.user)
90
+            if not is_available:
91
+                messages.error(self.request, message)
83 92
                 return
84 93
             
85 94
             basket.vouchers.add(voucher)

+ 1
- 1
oscar/checkout/views.py View File

@@ -264,7 +264,7 @@ class ThankYouView(object):
264 264
             
265 265
             # Remove order number from session to ensure that the thank-you page is only 
266 266
             # viewable once.
267
-            del request.session['checkout_order_id']
267
+            #del request.session['checkout_order_id']
268 268
         except KeyError, ObjectDoesNotExist:
269 269
             return HttpResponseRedirect(reverse('oscar-checkout-index'))
270 270
         return render(request, 'checkout/thank_you.html', locals())

+ 35
- 5
oscar/offer/abstract_models.py View File

@@ -44,11 +44,18 @@ class AbstractConditionalOffer(models.Model):
44 44
 
45 45
     # Some complicated situations require offers to be applied in a set order.
46 46
     priority = models.IntegerField(default=0, help_text="The highest priority offers are applied first")
47
+
48
+    # We track some information on usage
49
+    total_discount = models.DecimalField(decimal_places=2, max_digits=12, default=Decimal('0.00'))
50
+    
47 51
     date_created = models.DateTimeField(auto_now_add=True)
48 52
 
49 53
     objects = models.Manager()
50 54
     active = ActiveOfferManager()
51 55
 
56
+    # We need to track the voucher that this offer came from (if it is a voucher offer)
57
+    _voucher = None
58
+
52 59
     class Meta:
53 60
         ordering = ['-priority']
54 61
         abstract = True
@@ -77,6 +84,12 @@ class AbstractConditionalOffer(models.Model):
77 84
             self._proxy_condition().consume_items(basket)
78 85
         return discount    
79 86
         
87
+    def set_voucher(self, voucher):
88
+        self._voucher = voucher
89
+        
90
+    def get_voucher(self):
91
+        return self._voucher        
92
+        
80 93
     def _proxy_condition(self):
81 94
         u"""
82 95
         Returns the appropriate proxy model for the condition
@@ -247,17 +260,31 @@ class AbstractVoucher(models.Model):
247 260
     def is_available_to_user(self, user=None):
248 261
         u"""
249 262
         Tests whether this voucher is available to the passed user.
263
+        
264
+        Returns a tuple of a boolean for whether it is successulf, and a message
250 265
         """
266
+        is_available, message = False, ''
251 267
         if self.usage == self.SINGLE_USE:
252
-            return self.applications.count() == 0
268
+            is_available = self.applications.count() == 0
269
+            if not is_available:
270
+                message = "This voucher has already been used"
253 271
         elif self.usage == self.MULTI_USE:
254
-            return True
272
+            is_available = True
255 273
         elif self.usage == self.ONCE_PER_CUSTOMER:
256 274
             if not user.is_authenticated():
257
-                return False
275
+                is_available = False
276
+                message = "This voucher is only available to signed in users"
258 277
             else:
259
-                return self.applications.filter(voucher=self, user=user).count() == 0
260
-        return False
278
+                is_available = self.applications.filter(voucher=self, user=user).count() == 0
279
+                if not is_available:
280
+                    message = "You have already used this voucher in a previous order"
281
+        return is_available, message
282
+    
283
+    def record_usage(self, user):
284
+        u"""
285
+        Records a usage of this voucher in an order.
286
+        """
287
+        self.applications.create(voucher=self, user=user)
261 288
 
262 289
 
263 290
 class AbstractVoucherApplication(models.Model):
@@ -272,6 +299,9 @@ class AbstractVoucherApplication(models.Model):
272 299
 
273 300
     class Meta:
274 301
         abstract = True
302
+        
303
+    def __unicode__(self):
304
+        return u"'%s' used by '%s'" % (self.voucher, self.user)
275 305
 
276 306
 
277 307
 

+ 27
- 2
oscar/offer/admin.py View File

@@ -13,14 +13,39 @@ class BenefitAdmin(admin.ModelAdmin):
13 13
 class VoucherAdmin(admin.ModelAdmin):
14 14
     list_display = ('name', 'code', 'usage', 'num_basket_additions', 'num_orders', 'total_discount')    
15 15
     readonly_fields = ('num_basket_additions', 'num_orders', 'total_discount')
16
+    fieldsets = (
17
+        (None, {
18
+            'fields': ('name', 'code', 'usage', 'start_date', 'end_date')
19
+        }),
20
+        ('Benefit', {
21
+            'fields': ('offers', 'free_shipping')
22
+        }),
23
+        ('Usage', {
24
+            'fields': ('num_basket_additions', 'num_orders', 'total_discount')
25
+        }),
26
+        
27
+    )
28
+    
29
+class VoucherApplicationAdmin(admin.ModelAdmin):
30
+    list_display = ('voucher', 'user', 'date_created')
31
+    readonly_fields = ('voucher', 'user')        
16 32
     
17 33
 class ConditionalOfferAdmin(admin.ModelAdmin):
18
-    list_display = ('name', 'offer_type', 'start_date', 'end_date', 'condition', 'benefit')
34
+    list_display = ('name', 'offer_type', 'start_date', 'end_date', 'condition', 'benefit', 'total_discount')
19 35
     list_filter = ('offer_type',)
36
+    readonly_fields = ('total_discount',)
37
+    fieldsets = (
38
+        (None, {
39
+            'fields': ('name', 'description', 'offer_type', 'condition', 'benefit', 'start_date', 'end_date', 'priority')
40
+        }),
41
+        ('Usage', {
42
+            'fields': ('total_discount',)
43
+        }),
44
+    )
20 45
 
21 46
 admin.site.register(models.ConditionalOffer, ConditionalOfferAdmin)
22 47
 admin.site.register(models.Condition, ConditionAdmin)
23 48
 admin.site.register(models.Benefit, BenefitAdmin)
24 49
 admin.site.register(models.Range)
25 50
 admin.site.register(models.Voucher, VoucherAdmin)
26
-admin.site.register(models.VoucherApplication)
51
+admin.site.register(models.VoucherApplication, VoucherApplicationAdmin)

+ 10
- 10
oscar/offer/fixtures/sample-voucher.json View File

@@ -1,4 +1,14 @@
1 1
 [
2
+    {
3
+        "pk": 1, 
4
+        "model": "offer.range", 
5
+        "fields": {
6
+            "includes_all_products": true, 
7
+            "excluded_products": [], 
8
+            "name": "Whole site", 
9
+            "included_products": []
10
+        }
11
+    }, 
2 12
     {
3 13
         "pk": 1, 
4 14
         "model": "offer.condition", 
@@ -33,16 +43,6 @@
33 43
             "description": ""
34 44
         }
35 45
     }, 
36
-    {
37
-        "pk": 1, 
38
-        "model": "offer.range", 
39
-        "fields": {
40
-            "includes_all_products": true, 
41
-            "excluded_products": [], 
42
-            "name": "Whole site", 
43
-            "included_products": []
44
-        }
45
-    }, 
46 46
     {
47 47
         "pk": 1, 
48 48
         "model": "offer.voucher", 

+ 4
- 1
oscar/offer/models.py View File

@@ -209,4 +209,7 @@ class Voucher(AbstractVoucher):
209 209
     pass
210 210
 
211 211
 class VoucherApplication(AbstractVoucherApplication):
212
-    pass
212
+    pass
213
+
214
+# We need to import receivers at the bottom of this script
215
+from oscar.offer.receivers import receive_basket_voucher_change

+ 29
- 0
oscar/offer/receivers.py View File

@@ -0,0 +1,29 @@
1
+from django.dispatch import receiver
2
+from django.db.models.signals import m2m_changed, post_save
3
+
4
+from oscar.services import import_module
5
+offer_models = import_module('offer.models', ['Voucher'])
6
+order_models = import_module('order.models', ['OrderDiscount'])
7
+
8
+@receiver(m2m_changed)
9
+def receive_basket_voucher_change(sender, **kwargs):
10
+    if kwargs['model'] == offer_models.Voucher and kwargs['action'] == 'post_add':
11
+        voucher_id = list(kwargs['pk_set'])[0]
12
+        voucher = offer_models.Voucher._default_manager.get(pk=voucher_id)
13
+        voucher.num_basket_additions += 1
14
+        voucher.save()
15
+
16
+@receiver(post_save, sender=order_models.OrderDiscount)        
17
+def receive_order_discount_save(sender, instance, **kwargs):
18
+    # Record the amount of discount against the appropriate offers
19
+    # and vouchers
20
+    discount = instance
21
+    if discount.voucher:
22
+        discount.voucher.total_discount += discount.amount
23
+        discount.voucher.save()
24
+    discount.offer.total_discount += discount.amount
25
+    discount.offer.save()
26
+    
27
+    
28
+        
29
+    

+ 8
- 3
oscar/offer/utils.py View File

@@ -13,7 +13,7 @@ class Applicator(object):
13 13
     def apply(self, request, basket):
14 14
         u"""
15 15
         Applies all relevant offers to the given basket.  The user is passed 
16
-        too as sometimes the available offers are dependent on the user
16
+        too as sometimes the available offers are dependent on the user.
17 17
         """
18 18
         offers = self.get_offers(request, basket)
19 19
         discounts = {}
@@ -25,13 +25,15 @@ class Applicator(object):
25 25
                 if discount > 0:
26 26
                     if offer.id not in discounts:
27 27
                         discounts[offer.id] = {'name': offer.name,
28
+                                               'offer': offer,
29
+                                               'voucher': offer.get_voucher(),
28 30
                                                'freq': 0,
29 31
                                                'discount': Decimal('0.00')} 
30 32
                     discounts[offer.id]['discount'] += discount
31 33
                     discounts[offer.id]['freq'] += 1
32 34
                 else:
33 35
                     break
34
-                
36
+               
35 37
         # Store this list of discounts with the basket so it can be 
36 38
         # rendered in templates
37 39
         basket.set_discounts(list(discounts.values()))
@@ -63,7 +65,10 @@ class Applicator(object):
63 65
         offers = []
64 66
         for voucher in basket.vouchers.all():
65 67
             if voucher.is_active() and voucher.is_available_to_user(user):
66
-                offers = list(chain(offers, voucher.offers.all()))
68
+                basket_offers = voucher.offers.all()
69
+                for offer in basket_offers:
70
+                    offer.set_voucher(voucher)
71
+                offers = list(chain(offers, basket_offers))
67 72
         return offers
68 73
     
69 74
     def get_user_offers(self, user):

+ 14
- 1
oscar/order/abstract_models.py View File

@@ -174,6 +174,10 @@ class AbstractLine(models.Model):
174 174
     line_price_incl_tax = models.DecimalField(decimal_places=2, max_digits=12)
175 175
     line_price_excl_tax = models.DecimalField(decimal_places=2, max_digits=12)
176 176
     
177
+    # Price information before discounts are applied
178
+    line_price_before_discounts_incl_tax = models.DecimalField(decimal_places=2, max_digits=12)
179
+    line_price_before_discounts_excl_tax = models.DecimalField(decimal_places=2, max_digits=12)
180
+    
177 181
     # Cost price (the price charged by the fulfilment partner for this product).  This
178 182
     # is useful for audit and financial reporting.
179 183
     cost_price = models.DecimalField(decimal_places=2, max_digits=12, blank=True, null=True)
@@ -418,4 +422,13 @@ class AbstractShippingEventType(models.Model):
418 422
         return self.name
419 423
         
420 424
         
421
-
425
+class AbstractOrderDiscount(models.Model):
426
+    
427
+    order = models.ForeignKey('order.Order', related_name="discounts")
428
+    offer = models.ForeignKey('offer.ConditionalOffer', null=True, on_delete=models.SET_NULL)
429
+    voucher = models.ForeignKey('offer.Voucher', related_name="discount_vouchers", null=True, on_delete=models.SET_NULL)
430
+    voucher_code = models.CharField(_("Code"), max_length=128, db_index=True)
431
+    amount = models.DecimalField(decimal_places=2, max_digits=12, default=0)
432
+    
433
+    class Meta:
434
+        abstract = True

+ 6
- 1
oscar/order/admin.py View File

@@ -4,7 +4,7 @@ from oscar.services import import_module
4 4
 models = import_module('order.models', ['Order', 'OrderNote', 'CommunicationEvent', 'CommunicationEventType',
5 5
                                         'BillingAddress', 'ShippingAddress', 'Line',
6 6
                                         'LinePrice', 'ShippingEvent', 'ShippingEventType', 
7
-                                        'PaymentEvent', 'PaymentEventType', 'LineAttribute'])
7
+                                        'PaymentEvent', 'PaymentEventType', 'LineAttribute', 'OrderDiscount'])
8 8
 
9 9
 class OrderAdmin(admin.ModelAdmin):
10 10
     list_display = ('number', 'total_incl_tax', 'site', 'user', 'billing_address', 'date_placed')
@@ -33,6 +33,10 @@ class OrderNoteAdmin(admin.ModelAdmin):
33 33
         if not change:
34 34
             obj.user = request.user
35 35
         obj.save()
36
+        
37
+class OrderDiscountAdmin(admin.ModelAdmin):
38
+    readonly_fields = ('order' ,'offer', 'voucher', 'voucher_code', 'amount')
39
+    list_display = ('order' ,'offer', 'voucher', 'voucher_code', 'amount')
36 40
 
37 41
 admin.site.register(models.Order, OrderAdmin)
38 42
 admin.site.register(models.ShippingAddress)
@@ -43,4 +47,5 @@ admin.site.register(models.ShippingEventType, ShippingEventTypeAdmin)
43 47
 admin.site.register(models.PaymentEvent)
44 48
 admin.site.register(models.PaymentEventType, PaymentEventTypeAdmin)
45 49
 admin.site.register(models.LineAttribute)
50
+admin.site.register(models.OrderDiscount, OrderDiscountAdmin)
46 51
 

+ 4
- 0
oscar/order/fixtures/sample-order.json View File

@@ -44,6 +44,8 @@
44 44
             "line_price_excl_tax": "69.90", 
45 45
             "partner_reference": null, 
46 46
             "line_price_incl_tax": "69.90", 
47
+            "line_price_before_discounts_incl_tax": "69.90", 
48
+            "line_price_before_discounts_excl_tax": "69.90", 
47 49
             "order": 1, 
48 50
             "quantity": 10
49 51
         }
@@ -58,6 +60,8 @@
58 60
             "line_price_excl_tax": "68.80", 
59 61
             "partner_reference": null, 
60 62
             "line_price_incl_tax": "68.80", 
63
+            "line_price_before_discounts_incl_tax": "68.80", 
64
+            "line_price_before_discounts_excl_tax": "68.80", 
61 65
             "order": 1, 
62 66
             "quantity": 5
63 67
         }

+ 3
- 0
oscar/order/models.py View File

@@ -43,5 +43,8 @@ class PaymentEvent(AbstractPaymentEvent):
43 43
 class PaymentEventType(AbstractPaymentEventType):
44 44
     pass
45 45
 
46
+class OrderDiscount(AbstractOrderDiscount):
47
+    pass
48
+
46 49
 
47 50
     

+ 23
- 2
oscar/order/utils.py View File

@@ -2,7 +2,7 @@ from django.contrib.sites.models import Site
2 2
 
3 3
 from oscar.services import import_module
4 4
 order_models = import_module('order.models', ['ShippingAddress', 'Order', 'Line', 
5
-                                              'LinePrice', 'LineAttribute'])
5
+                                              'LinePrice', 'LineAttribute', 'OrderDiscount'])
6 6
 order_signals = import_module('order.signals', ['order_placed'])
7 7
 
8 8
 class OrderNumberGenerator(object):
@@ -39,6 +39,10 @@ class OrderCreator(object):
39 39
         order = self._create_order_model(user, basket, shipping_address, shipping_method, order_number)
40 40
         for line in basket.all_lines():
41 41
             self._create_line_models(order, line)
42
+        for discount in basket.discounts:
43
+            self._create_discount_model(order, discount)
44
+        for voucher in basket.vouchers.all():
45
+            self._record_voucher_usage(order, voucher, user)
42 46
         
43 47
         basket.set_as_submitted()
44 48
         
@@ -79,7 +83,9 @@ class OrderCreator(object):
79 83
                                       title=basket_line.product.get_title(),
80 84
                                       quantity=basket_line.quantity, 
81 85
                                       line_price_excl_tax=basket_line.line_price_excl_tax_and_discounts, 
82
-                                      line_price_incl_tax=basket_line.line_price_incl_tax_and_discounts)
86
+                                      line_price_incl_tax=basket_line.line_price_incl_tax_and_discounts,
87
+                                      line_price_before_discounts_excl_tax=basket_line.line_price_excl_tax,
88
+                                      line_price_before_discounts_incl_tax=basket_line.line_price_incl_tax,)
83 89
         if basket_line.product.has_stockrecord:
84 90
             order_line.partner_reference = basket_line.product.stockrecord.partner_reference
85 91
             order_line.dispatch_date = basket_line.product.stockrecord.dispatch_date
@@ -102,3 +108,18 @@ class OrderCreator(object):
102 108
         for attr in basket_line.attributes.all():
103 109
             order_models.LineAttribute._default_manager.create(line=order_line, type=attr.option.code,
104 110
                                                       value=attr.value)
111
+            
112
+    def _create_discount_model(self, order, discount):
113
+        u"""
114
+        Creates an order discount model for each discount attached to the basket.
115
+        """
116
+        order_discount = order_models.OrderDiscount(order=order, offer=discount['offer'], amount=discount['discount'])
117
+        if discount['voucher']:
118
+            order_discount.voucher = discount['voucher']
119
+            order_discount.voucher_code = discount['voucher'].code
120
+        order_discount.save()
121
+        
122
+    def _record_voucher_usage(self, order, voucher, user):
123
+        voucher.record_usage(user)
124
+        voucher.num_orders += 1
125
+        voucher.save()

+ 1
- 1
oscar/templates/basket/summary.html View File

@@ -89,7 +89,7 @@ Your basket is currently empty - go and <a href="{% url oscar-products %}">buy s
89 89
     <li>
90 90
         <form action="{% url oscar-basket %}" method="POST">
91 91
             {% csrf_token %}
92
-            {{ voucher.code }}
92
+            {{ voucher.name }} ({{ voucher.code }})
93 93
             <input type="hidden" name="action" value="remove_voucher" />
94 94
             <input type="hidden" name="voucher_code" value="{{ voucher.code }}" />
95 95
             <input type="submit" value="Remove" />

+ 11
- 2
oscar/templates/checkout/thank_you.html View File

@@ -25,8 +25,17 @@
25 25
         <td><a href="{{ line.product.get_absolute_url }}">{{ line.description }}</a></td>
26 26
         <td>{{ line.est_dispatch_date }}</td>
27 27
         <td>{{ line.quantity }}</td>
28
-        <td>{{ line.line_price_excl_tax|currency }}</td>
29
-        <td>{{ line.line_price_incl_tax|currency }}</td>
28
+        <td>{{ line.line_price_before_discounts_excl_tax|currency }}</td>
29
+        <td>{{ line.line_price_before_discounts_incl_tax|currency }}</td>
30
+    </tr>
31
+    {% endfor %}
32
+    {% for discount in order.discounts.all %}
33
+    <tr>
34
+        <td>{{ discount.offer }}</td>
35
+        <td></td>
36
+        <td></td>
37
+        <td></td>
38
+        <td>- {{ discount.amount|currency }}</td>
30 39
     </tr>
31 40
     {% endfor %}
32 41
     <tr>

Loading…
Cancel
Save