Skip to content
This repository
  • 8 commits
  • 8 files changed
  • 0 comments
  • 2 contributors
123  oscar/apps/offer/models.py
@@ -25,14 +25,14 @@ class ConditionalOffer(models.Model):
25 25
     description = models.TextField(_('Description'), blank=True, null=True)
26 26
 
27 27
     # Offers come in a few different types:
28  
-    # (a) Offers that are available to all customers on the site.  Eg a 
  28
+    # (a) Offers that are available to all customers on the site.  Eg a
29 29
     #     3-for-2 offer.
30 30
     # (b) Offers that are linked to a voucher, and only become available once
31 31
     #     that voucher has been applied to the basket
32 32
     # (c) Offers that are linked to a user.  Eg, all students get 10% off.  The code
33 33
     #     to apply this offer needs to be coded
34  
-    # (d) Session offers - these are temporarily available to a user after some trigger 
35  
-    #     event.  Eg, users coming from some affiliate site get 10% off.     
  34
+    # (d) Session offers - these are temporarily available to a user after some trigger
  35
+    #     event.  Eg, users coming from some affiliate site get 10% off.
36 36
     SITE, VOUCHER, USER, SESSION = ("Site", "Voucher", "User", "Session")
37 37
     TYPE_CHOICES = (
38 38
         (SITE, _("Site offer - available to all users")),
@@ -46,7 +46,7 @@ class ConditionalOffer(models.Model):
46 46
     benefit = models.ForeignKey('offer.Benefit')
47 47
 
48 48
     # Range of availability.  Note that if this is a voucher offer, then these
49  
-    # dates are ignored and only the dates from the voucher are used to determine 
  49
+    # dates are ignored and only the dates from the voucher are used to determine
50 50
     # availability.
51 51
     start_date = models.DateField(_('Start Date'), blank=True, null=True)
52 52
     end_date = models.DateField(_('End Date'), blank=True, null=True,
@@ -60,7 +60,7 @@ class ConditionalOffer(models.Model):
60 60
     # We track some information on usage
61 61
     total_discount = models.DecimalField(_('Total Discount'), decimal_places=2, max_digits=12, default=Decimal('0.00'))
62 62
     num_orders = models.PositiveIntegerField(_('Number of Orders'), default=0)
63  
-    
  63
+
64 64
     date_created = models.DateTimeField(auto_now_add=True)
65 65
 
66 66
     objects = models.Manager()
@@ -83,19 +83,19 @@ def save(self, *args, **kwargs):
83 83
 
84 84
     def get_absolute_url(self):
85 85
         return reverse('offer:detail', kwargs={'slug': self.slug})
86  
-        
  86
+
87 87
     def __unicode__(self):
88  
-        return self.name    
  88
+        return self.name
89 89
 
90 90
     def clean(self):
91 91
         if self.start_date and self.end_date and self.start_date > self.end_date:
92 92
             raise exceptions.ValidationError(_('End date should be later than start date'))
93  
-        
  93
+
94 94
     def is_active(self, test_date=None):
95 95
         if not test_date:
96 96
             test_date = datetime.date.today()
97 97
         return self.start_date <= test_date and test_date < self.end_date
98  
-    
  98
+
99 99
     def is_condition_satisfied(self, basket):
100 100
         return self._proxy_condition().is_satisfied(basket)
101 101
 
@@ -104,7 +104,7 @@ def is_condition_partially_satisfied(self, basket):
104 104
 
105 105
     def get_upsell_message(self, basket):
106 106
         return self._proxy_condition().get_upsell_message(basket)
107  
-        
  107
+
108 108
     def apply_benefit(self, basket):
109 109
         """
110 110
         Applies the benefit to the given basket and returns the discount.
@@ -112,13 +112,13 @@ def apply_benefit(self, basket):
112 112
         if not self.is_condition_satisfied(basket):
113 113
             return Decimal('0.00')
114 114
         return self._proxy_benefit().apply(basket, self._proxy_condition())
115  
-        
  115
+
116 116
     def set_voucher(self, voucher):
117 117
         self._voucher = voucher
118  
-        
  118
+
119 119
     def get_voucher(self):
120  
-        return self._voucher        
121  
-        
  120
+        return self._voucher
  121
+
122 122
     def _proxy_condition(self):
123 123
         """
124 124
         Returns the appropriate proxy model for the condition
@@ -135,7 +135,7 @@ def _proxy_condition(self):
135 135
         elif self.condition.type == self.condition.COVERAGE:
136 136
             return CoverageCondition(**field_dict)
137 137
         return self.condition
138  
-    
  138
+
139 139
     def _proxy_benefit(self):
140 140
         """
141 141
         Returns the appropriate proxy model for the condition
@@ -157,7 +157,7 @@ def record_usage(self, discount):
157 157
         self.num_orders += 1
158 158
         self.total_discount += discount
159 159
         self.save()
160  
-        
  160
+
161 161
 
162 162
 class Condition(models.Model):
163 163
     COUNT, VALUE, COVERAGE = ("Count", "Value", "Coverage")
@@ -185,10 +185,10 @@ def __unicode__(self):
185 185
                 'count': self.value, 'range': unicode(self.range).lower()}
186 186
 
187 187
     description = __unicode__
188  
-    
  188
+
189 189
     def consume_items(self, basket, lines=None):
190 190
         return ()
191  
-    
  191
+
192 192
     def is_satisfied(self, basket):
193 193
         """
194 194
         Determines whether a given basket meets this condition.  This is
@@ -212,9 +212,9 @@ def can_apply_condition(self, product):
212 212
         """
213 213
             Determines whether the condition can be applied to a given product
214 214
         """
215  
-        return (self.range.contains_product(product) 
  215
+        return (self.range.contains_product(product)
216 216
                 and product.is_discountable)
217  
-    
  217
+
218 218
 
219 219
 class Benefit(models.Model):
220 220
     PERCENTAGE, FIXED, MULTIBUY, FIXED_PRICE = ("Percentage", "Absolute", "Multibuy", "Fixed price")
@@ -231,7 +231,7 @@ class Benefit(models.Model):
231 231
 
232 232
     price_field = 'price_incl_tax'
233 233
 
234  
-    # If this is not set, then there is no upper limit on how many products 
  234
+    # If this is not set, then there is no upper limit on how many products
235 235
     # can be discounted by this benefit.
236 236
     max_affected_items = models.PositiveIntegerField(_('Max Affected Items'), blank=True, null=True,
237 237
         help_text=_("""Set this to prevent the discount consuming all items within the range that are in the basket."""))
@@ -257,10 +257,10 @@ def __unicode__(self):
257 257
         return desc
258 258
 
259 259
     description = __unicode__
260  
-    
  260
+
261 261
     def apply(self, basket, condition=None):
262 262
         return Decimal('0.00')
263  
-    
  263
+
264 264
     def clean(self):
265 265
         if self.value is None:
266 266
             if not self.type:
@@ -306,7 +306,7 @@ class Range(models.Model):
306 306
     classes = models.ManyToManyField('catalogue.ProductClass', related_name='classes', blank=True)
307 307
     included_categories = models.ManyToManyField('catalogue.Category', related_name='includes', blank=True)
308 308
     date_created = models.DateTimeField(auto_now_add=True)
309  
-    
  309
+
310 310
     __included_product_ids = None
311 311
     __excluded_product_ids = None
312 312
     __class_ids = None
@@ -316,8 +316,8 @@ class Meta:
316 316
         verbose_name_plural = _("Ranges")
317 317
 
318 318
     def __unicode__(self):
319  
-        return self.name    
320  
-        
  319
+        return self.name
  320
+
321 321
     def contains_product(self, product):
322 322
         """
323 323
         Check whether the passed product is part of this range
@@ -334,28 +334,28 @@ def contains_product(self, product):
334 334
         if self.includes_all_products:
335 335
             return True
336 336
         if product.product_class_id in self._class_ids():
337  
-            return True   
  337
+            return True
338 338
         included_product_ids = self._included_product_ids()
339 339
         if product.id in included_product_ids:
340 340
             return True
341  
-        test_categories = self.included_categories.all() 
  341
+        test_categories = self.included_categories.all()
342 342
         if test_categories:
343 343
             for category in product.categories.all():
344 344
                 for test_category in test_categories:
345 345
                     if category == test_category or category.is_descendant_of(test_category):
346 346
                         return True
347 347
         return False
348  
-    
  348
+
349 349
     def _included_product_ids(self):
350 350
         if None == self.__included_product_ids:
351 351
             self.__included_product_ids = [row['id'] for row in self.included_products.values('id')]
352 352
         return self.__included_product_ids
353  
-    
  353
+
354 354
     def _excluded_product_ids(self):
355 355
         if None == self.__excluded_product_ids:
356 356
             self.__excluded_product_ids = [row['id'] for row in self.excluded_products.values('id')]
357 357
         return self.__excluded_product_ids
358  
-    
  358
+
359 359
     def _class_ids(self):
360 360
         if None == self.__class_ids:
361 361
             self.__class_ids = [row['id'] for row in self.classes.values('id')]
@@ -365,7 +365,7 @@ def num_products(self):
365 365
         if self.includes_all_products:
366 366
             return None
367 367
         return self.included_products.all().count()
368  
-        
  368
+
369 369
 
370 370
 class CountCondition(Condition):
371 371
     """
@@ -383,7 +383,7 @@ def is_satisfied(self, basket):
383 383
         """
384 384
         num_matches = 0
385 385
         for line in basket.all_lines():
386  
-            if (self.can_apply_condition(line.product) 
  386
+            if (self.can_apply_condition(line.product)
387 387
                 and line.quantity_without_discount > 0):
388 388
                 num_matches += line.quantity_without_discount
389 389
             if num_matches >= self.value:
@@ -395,7 +395,7 @@ def _get_num_matches(self, basket):
395 395
             return getattr(self, '_num_matches')
396 396
         num_matches = 0
397 397
         for line in basket.all_lines():
398  
-            if (self.can_apply_condition(line.product) 
  398
+            if (self.can_apply_condition(line.product)
399 399
                 and line.quantity_without_discount > 0):
400 400
                 num_matches += line.quantity_without_discount
401 401
         self._num_matches = num_matches
@@ -429,8 +429,8 @@ def consume_items(self, basket, lines=None, value=None):
429 429
             if len(consumed_products) == value:
430 430
                 break
431 431
         return consumed_products
432  
-        
433  
-        
  432
+
  433
+
434 434
 class CoverageCondition(Condition):
435 435
     """
436 436
     An offer condition dependent on the number of DISTINCT matching items from the basket.
@@ -474,7 +474,7 @@ def get_upsell_message(self, basket):
474 474
 
475 475
     def is_partially_satisfied(self, basket):
476 476
         return 0 < self._get_num_covered_products(basket) < self.value
477  
-    
  477
+
478 478
     def consume_items(self, basket, lines=None, value=None):
479 479
         """
480 480
         Marks items within the basket lines as consumed so they
@@ -492,7 +492,7 @@ def consume_items(self, basket, lines=None, value=None):
492 492
             if len(consumed_products) >= value:
493 493
                 break
494 494
         return consumed_products
495  
-    
  495
+
496 496
     def get_value_of_satisfying_items(self, basket):
497 497
         covered_ids = []
498 498
         value = Decimal('0.00')
@@ -503,8 +503,8 @@ def get_value_of_satisfying_items(self, basket):
503 503
             if len(covered_ids) >= self.value:
504 504
                 return value
505 505
         return value
506  
-        
507  
-        
  506
+
  507
+
508 508
 class ValueCondition(Condition):
509 509
     """
510 510
     An offer condition dependent on the VALUE of matching items from the basket.
@@ -522,7 +522,7 @@ def is_satisfied(self, basket):
522 522
         value_of_matches = Decimal('0.00')
523 523
         for line in basket.all_lines():
524 524
             product = line.product
525  
-            if (self.can_apply_condition(product) and product.has_stockrecord 
  525
+            if (self.can_apply_condition(product) and product.has_stockrecord
526 526
                 and line.quantity_without_discount > 0):
527 527
                 price = getattr(product.stockrecord, self.price_field)
528 528
                 value_of_matches += price * int(line.quantity_without_discount)
@@ -536,7 +536,7 @@ def _get_value_of_matches(self, basket):
536 536
         value_of_matches = Decimal('0.00')
537 537
         for line in basket.all_lines():
538 538
             product = line.product
539  
-            if (self.can_apply_condition(product) and product.has_stockrecord 
  539
+            if (self.can_apply_condition(product) and product.has_stockrecord
540 540
                 and line.quantity_without_discount > 0):
541 541
                 price = getattr(product.stockrecord, self.price_field)
542 542
                 value_of_matches += price * int(line.quantity_without_discount)
@@ -550,12 +550,12 @@ def is_partially_satisfied(self, basket):
550 550
     def get_upsell_message(self, basket):
551 551
         value_of_matches = self._get_value_of_matches(basket)
552 552
         return _('Spend %(value)s more from %(range)s') % {'value': value_of_matches, 'range': self.range}
553  
-    
  553
+
554 554
     def consume_items(self, basket, lines=None, value=None):
555 555
         """
556 556
         Marks items within the basket lines as consumed so they
557 557
         can't be reused in other offers.
558  
-        
  558
+
559 559
         We allow lines to be passed in as sometimes we want them sorted
560 560
         in a specific order.
561 561
         """
@@ -597,7 +597,7 @@ def apply(self, basket, condition=None):
597 597
         discount = Decimal('0.00')
598 598
         affected_items = 0
599 599
         max_affected_items = self._effective_max_affected_items()
600  
-        
  600
+
601 601
         for line in basket.all_lines():
602 602
             if affected_items >= max_affected_items:
603 603
                 break
@@ -605,13 +605,13 @@ def apply(self, basket, condition=None):
605 605
             if (self.range.contains_product(product) and product.has_stockrecord
606 606
                 and self.can_apply_benefit(product)):
607 607
                 price = getattr(product.stockrecord, self.price_field)
608  
-                quantity = min(line.quantity_without_discount, 
  608
+                quantity = min(line.quantity_without_discount,
609 609
                                max_affected_items - affected_items)
610 610
                 line_discount = self.round(self.value/100 * price * int(quantity))
611 611
                 line.discount(line_discount, quantity)
612 612
                 affected_items += quantity
613 613
                 discount += line_discount
614  
-                
  614
+
615 615
         if discount > 0 and condition:
616 616
             condition.consume_items(basket)
617 617
         return discount
@@ -631,7 +631,7 @@ def apply(self, basket, condition=None):
631 631
         discount = Decimal('0.00')
632 632
         affected_items = 0
633 633
         max_affected_items = self._effective_max_affected_items()
634  
-        
  634
+
635 635
         for line in basket.all_lines():
636 636
             if affected_items >= max_affected_items:
637 637
                 break
@@ -643,33 +643,30 @@ def apply(self, basket, condition=None):
643 643
                     # Avoid zero price products
644 644
                     continue
645 645
                 remaining_discount = self.value - discount
646  
-                quantity_affected = int(min(line.quantity_without_discount, 
  646
+                quantity_affected = int(min(line.quantity_without_discount,
647 647
                                         max_affected_items - affected_items,
648 648
                                         math.ceil(remaining_discount / price)))
649  
-                
  649
+
650 650
                 # Update line with discounts
651 651
                 line_discount = self.round(min(remaining_discount, quantity_affected * price))
652  
-                if not condition:
653  
-                    line.discount(line_discount, quantity_affected)
654  
-                
  652
+                line.discount(line_discount, quantity_affected)
  653
+
655 654
                 # Update loop vars
656 655
                 affected_items += quantity_affected
657 656
                 remaining_discount -= line_discount
658 657
                 discount += line_discount
659  
-        if discount > 0 and condition:
660  
-            condition.consume_items(basket)
661  
-            
  658
+
662 659
         return discount
663 660
 
664 661
 
665 662
 class FixedPriceBenefit(Benefit):
666 663
     """
667  
-    An offer benefit that gives the items in the condition for a 
  664
+    An offer benefit that gives the items in the condition for a
668 665
     fixed price.  This is useful for "bundle" offers.
669  
-    
  666
+
670 667
     Note that we ignore the benefit range here and only give a fixed price
671 668
     for the products in the condition range.
672  
-    
  669
+
673 670
     The condition should be a count condition
674 671
     """
675 672
     class Meta:
@@ -686,7 +683,7 @@ def apply(self, basket, condition=None):
686 683
             product = line.product
687 684
             if (condition.range.contains_product(product) and line.quantity_without_discount > 0
688 685
                 and self.can_apply_benefit(product)):
689  
-                # Line is available - determine quantity to consume and 
  686
+                # Line is available - determine quantity to consume and
690 687
                 # record the total of the consumed products
691 688
                 if isinstance(condition, CoverageCondition):
692 689
                     quantity = 1
@@ -698,10 +695,10 @@ def apply(self, basket, condition=None):
698 695
             if num_covered >= num_permitted:
699 696
                 break
700 697
         discount = max(product_total - self.value, Decimal('0.00'))
701  
-        
  698
+
702 699
         if not discount:
703 700
             return discount
704  
-        
  701
+
705 702
         # Apply discount weighted by original value of line
706 703
         discount_applied = Decimal('0.00')
707 704
         last_line = covered_lines[-1][0]
@@ -714,7 +711,7 @@ def apply(self, basket, condition=None):
714 711
                 line_discount = self.round(discount * (line.unit_price_incl_tax * quantity) / product_total)
715 712
             line.discount(line_discount, quantity)
716 713
             discount_applied += line_discount
717  
-        return discount 
  714
+        return discount
718 715
 
719 716
 
720 717
 class MultibuyDiscountBenefit(Benefit):
2  oscar/apps/shipping/__init__.py
@@ -23,6 +23,6 @@ def weigh_product(self, product):
23 23
     def weigh_basket(self, basket):
24 24
         weight = 0.0
25 25
         for line in basket.lines.all():
26  
-            weight += self.weigh_product(line.product)
  26
+            weight += self.weigh_product(line.product) * line.quantity
27 27
         return weight
28 28
 
10  tests/unit/offer/__init__.py
... ...
@@ -0,0 +1,10 @@
  1
+from django.test import TestCase
  2
+
  3
+from oscar.apps.offer import models
  4
+from oscar.apps.basket.models import Basket
  5
+
  6
+
  7
+class OfferTest(TestCase):
  8
+    def setUp(self):
  9
+        self.range = models.Range.objects.create(name="All products range", includes_all_products=True)
  10
+        self.basket = Basket.objects.create()
362  tests/unit/offer/benefit_tests.py
... ...
@@ -0,0 +1,362 @@
  1
+from decimal import Decimal
  2
+
  3
+from django.conf import settings
  4
+
  5
+from oscar.apps.basket.models import Basket
  6
+from oscar.apps.offer import models
  7
+from oscar.test.helpers import create_product
  8
+from tests.unit.offer import OfferTest
  9
+
  10
+
  11
+class PercentageDiscountBenefitTest(OfferTest):
  12
+
  13
+    def setUp(self):
  14
+        super(PercentageDiscountBenefitTest, self).setUp()
  15
+        self.benefit = models.PercentageDiscountBenefit(range=self.range, type="Percentage", value=Decimal('15.00'))
  16
+        self.item = create_product(price=Decimal('5.00'))
  17
+        self.original_offer_rounding_function = getattr(settings, 'OSCAR_OFFER_ROUNDING_FUNCTION', None)
  18
+        if self.original_offer_rounding_function is not None:
  19
+            delattr(settings, 'OSCAR_OFFER_ROUNDING_FUNCTION')
  20
+
  21
+    def tearDown(self):
  22
+        super(PercentageDiscountBenefitTest, self).tearDown()
  23
+        if self.original_offer_rounding_function is not None:
  24
+            settings.OSCAR_OFFER_ROUNDING_FUNCTION = self.original_offer_rounding_function
  25
+
  26
+    def test_no_discount_for_empty_basket(self):
  27
+        self.assertEquals(Decimal('0.00'), self.benefit.apply(self.basket))
  28
+
  29
+    def test_no_discount_for_not_discountable_product(self):
  30
+        self.item.is_discountable = False
  31
+        self.item.save()
  32
+        self.basket.add_product(self.item, 1)
  33
+        self.assertEquals(Decimal('0.00'), self.benefit.apply(self.basket))
  34
+
  35
+    def test_discount_for_single_item_basket(self):
  36
+        self.basket.add_product(self.item, 1)
  37
+        self.assertEquals(Decimal('0.15') * Decimal('5.00'), self.benefit.apply(self.basket))
  38
+
  39
+    def test_discount_for_multi_item_basket(self):
  40
+        self.basket.add_product(self.item, 3)
  41
+        self.assertEquals(Decimal('3') * Decimal('0.15') * Decimal('5.00'), self.benefit.apply(self.basket))
  42
+
  43
+    def test_discount_for_multi_item_basket_with_max_affected_items_set(self):
  44
+        self.basket.add_product(self.item, 3)
  45
+        self.benefit.max_affected_items = 1
  46
+        self.assertEquals(Decimal('0.15') * Decimal('5.00'), self.benefit.apply(self.basket))
  47
+
  48
+    def test_discount_can_only_be_applied_once(self):
  49
+        self.basket.add_product(self.item, 3)
  50
+        self.benefit.apply(self.basket)
  51
+        second_discount = self.benefit.apply(self.basket)
  52
+        self.assertEquals(Decimal('0.00'), second_discount)
  53
+
  54
+    def test_discount_can_be_applied_several_times_when_max_is_set(self):
  55
+        self.basket.add_product(self.item, 3)
  56
+        self.benefit.max_affected_items = 1
  57
+        for i in range(1, 4):
  58
+            self.assertTrue(self.benefit.apply(self.basket) > 0)
  59
+
  60
+
  61
+class AbsoluteDiscountBenefitTest(OfferTest):
  62
+
  63
+    def setUp(self):
  64
+        super(AbsoluteDiscountBenefitTest, self).setUp()
  65
+        self.benefit = models.AbsoluteDiscountBenefit(
  66
+            range=self.range, type="Absolute", value=Decimal('10.00'))
  67
+        self.item = create_product(price=Decimal('5.00'))
  68
+        self.original_offer_rounding_function = getattr(settings, 'OSCAR_OFFER_ROUNDING_FUNCTION', None)
  69
+        if self.original_offer_rounding_function is not None:
  70
+            delattr(settings, 'OSCAR_OFFER_ROUNDING_FUNCTION')
  71
+
  72
+    def tearDown(self):
  73
+        super(AbsoluteDiscountBenefitTest, self).tearDown()
  74
+        if self.original_offer_rounding_function is not None:
  75
+            settings.OSCAR_OFFER_ROUNDING_FUNCTION = self.original_offer_rounding_function
  76
+
  77
+    def test_no_discount_for_empty_basket(self):
  78
+        self.assertEquals(Decimal('0.00'), self.benefit.apply(self.basket))
  79
+
  80
+    def test_no_discount_for_not_discountable_product(self):
  81
+        self.item.is_discountable = False
  82
+        self.item.save()
  83
+        self.basket.add_product(self.item, 1)
  84
+        self.assertEquals(Decimal('0.00'), self.benefit.apply(self.basket))
  85
+
  86
+    def test_discount_for_single_item_basket(self):
  87
+        self.basket.add_product(self.item, 1)
  88
+        self.assertEquals(Decimal('5.00'), self.benefit.apply(self.basket))
  89
+
  90
+    def test_discount_for_multi_item_basket(self):
  91
+        self.basket.add_product(self.item, 3)
  92
+        self.assertEquals(Decimal('10.00'), self.benefit.apply(self.basket))
  93
+
  94
+    def test_discount_for_multi_item_basket_with_max_affected_items_set(self):
  95
+        self.basket.add_product(self.item, 3)
  96
+        self.benefit.max_affected_items = 1
  97
+        self.assertEquals(Decimal('5.00'), self.benefit.apply(self.basket))
  98
+
  99
+    def test_discount_can_only_be_applied_once(self):
  100
+        # Add 3 items to make total 15.00
  101
+        self.basket.add_product(self.item, 3)
  102
+        first_discount = self.benefit.apply(self.basket)
  103
+        self.assertEquals(Decimal('10.00'), first_discount)
  104
+
  105
+        second_discount = self.benefit.apply(self.basket)
  106
+        self.assertEquals(Decimal('5.00'), second_discount)
  107
+
  108
+    def test_absolute_does_not_consume_twice(self):
  109
+        product = create_product(Decimal('25000'))
  110
+        rng = models.Range.objects.create(name='Dummy')
  111
+        rng.included_products.add(product)
  112
+        condition = models.ValueCondition(range=rng, type='Value', value=Decimal('5000'))
  113
+        basket = Basket.objects.create()
  114
+        basket.add_product(product, 5)
  115
+        benefit = models.AbsoluteDiscountBenefit(range=rng, type='Absolute', value=Decimal('100'))
  116
+        self.assertTrue(condition.is_satisfied(basket))
  117
+        self.assertEquals(Decimal('100'), benefit.apply(basket, condition))
  118
+        self.assertEquals(Decimal('100'), benefit.apply(basket, condition))
  119
+        self.assertEquals(Decimal('100'), benefit.apply(basket, condition))
  120
+        self.assertEquals(Decimal('100'), benefit.apply(basket, condition))
  121
+        self.assertEquals(Decimal('100'), benefit.apply(basket, condition))
  122
+        self.assertEquals(Decimal('0'), benefit.apply(basket, condition))
  123
+
  124
+    def test_discount_is_applied_to_lines(self):
  125
+        condition = models.Condition.objects.create(
  126
+            range=self.range, type="Count", value=1)
  127
+        self.basket.add_product(self.item, 1)
  128
+        self.benefit.apply(self.basket, condition)
  129
+
  130
+        self.assertTrue(self.basket.all_lines()[0].has_discount)
  131
+
  132
+
  133
+class MultibuyDiscountBenefitTest(OfferTest):
  134
+
  135
+    def setUp(self):
  136
+        super(MultibuyDiscountBenefitTest, self).setUp()
  137
+        self.benefit = models.MultibuyDiscountBenefit(range=self.range, type="Multibuy", value=1)
  138
+        self.item = create_product(price=Decimal('5.00'))
  139
+
  140
+    def test_no_discount_for_empty_basket(self):
  141
+        self.assertEquals(Decimal('0.00'), self.benefit.apply(self.basket))
  142
+
  143
+    def test_discount_for_single_item_basket(self):
  144
+        self.basket.add_product(self.item, 1)
  145
+        self.assertEquals(Decimal('5.00'), self.benefit.apply(self.basket))
  146
+
  147
+    def test_discount_for_multi_item_basket(self):
  148
+        self.basket.add_product(self.item, 3)
  149
+        self.assertEquals(Decimal('5.00'), self.benefit.apply(self.basket))
  150
+
  151
+    def test_no_discount_for_not_discountable_product(self):
  152
+        self.item.is_discountable = False
  153
+        self.item.save()
  154
+        self.basket.add_product(self.item, 1)
  155
+        self.assertEquals(Decimal('0.00'), self.benefit.apply(self.basket))
  156
+
  157
+    def test_discount_does_not_consume_item_if_in_condition_range(self):
  158
+        self.basket.add_product(self.item, 1)
  159
+        first_discount = self.benefit.apply(self.basket)
  160
+        self.assertEquals(Decimal('5.00'), first_discount)
  161
+        second_discount = self.benefit.apply(self.basket)
  162
+        self.assertEquals(Decimal('5.00'), second_discount)
  163
+
  164
+    def test_product_does_consume_item_if_not_in_condition_range(self):
  165
+        # Set up condition using a different range from benefit
  166
+        range = models.Range.objects.create(name="Small range")
  167
+        other_product = create_product(price=Decimal('15.00'))
  168
+        range.included_products.add(other_product)
  169
+        cond = models.ValueCondition(range=range, type="Value", value=Decimal('10.00'))
  170
+
  171
+        self.basket.add_product(self.item, 1)
  172
+        self.benefit.apply(self.basket, cond)
  173
+        line = self.basket.all_lines()[0]
  174
+        self.assertEqual(line.quantity_without_discount, 0)
  175
+
  176
+    def test_condition_consumes_most_expensive_lines_first(self):
  177
+        for i in range(10, 0, -1):
  178
+            product = create_product(price=Decimal(i), title='%i'%i, upc='upc_%i' % i)
  179
+            self.basket.add_product(product, 1)
  180
+
  181
+        condition = models.CountCondition(range=self.range, type="Count", value=2)
  182
+
  183
+        self.assertTrue(condition.is_satisfied(self.basket))
  184
+        # consume 1 and 10
  185
+        first_discount = self.benefit.apply(self.basket, condition=condition)
  186
+        self.assertEquals(Decimal('1.00'), first_discount)
  187
+
  188
+        self.assertTrue(condition.is_satisfied(self.basket))
  189
+        # consume 2 and 9
  190
+        second_discount = self.benefit.apply(self.basket, condition=condition)
  191
+        self.assertEquals(Decimal('2.00'), second_discount)
  192
+
  193
+        self.assertTrue(condition.is_satisfied(self.basket))
  194
+        # consume 3 and 8
  195
+        third_discount = self.benefit.apply(self.basket, condition=condition)
  196
+        self.assertEquals(Decimal('3.00'), third_discount)
  197
+
  198
+        self.assertTrue(condition.is_satisfied(self.basket))
  199
+        # consume 4 and 7
  200
+        fourth_discount = self.benefit.apply(self.basket, condition=condition)
  201
+        self.assertEquals(Decimal('4.00'), fourth_discount)
  202
+
  203
+        self.assertTrue(condition.is_satisfied(self.basket))
  204
+        # consume 5 and 6
  205
+        fifth_discount = self.benefit.apply(self.basket, condition=condition)
  206
+        self.assertEquals(Decimal('5.00'), fifth_discount)
  207
+
  208
+        # end of items (one not discounted item in basket)
  209
+        self.assertFalse(condition.is_satisfied(self.basket))
  210
+
  211
+    def test_condition_consumes_most_expensive_lines_first_when_products_are_repeated(self):
  212
+        for i in range(5, 0, -1):
  213
+            product = create_product(price=Decimal(i), title='%i'%i, upc='upc_%i' % i)
  214
+            self.basket.add_product(product, 2)
  215
+
  216
+        condition = models.CountCondition(range=self.range, type="Count", value=2)
  217
+
  218
+        # initial basket: [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]
  219
+        self.assertTrue(condition.is_satisfied(self.basket))
  220
+        # consume 1 and 5
  221
+        first_discount = self.benefit.apply(self.basket, condition=condition)
  222
+        self.assertEquals(Decimal('1.00'), first_discount)
  223
+
  224
+        self.assertTrue(condition.is_satisfied(self.basket))
  225
+        # consume 1 and 5
  226
+        second_discount = self.benefit.apply(self.basket, condition=condition)
  227
+        self.assertEquals(Decimal('1.00'), second_discount)
  228
+
  229
+        self.assertTrue(condition.is_satisfied(self.basket))
  230
+        # consume 2 and 4
  231
+        third_discount = self.benefit.apply(self.basket, condition=condition)
  232
+        self.assertEquals(Decimal('2.00'), third_discount)
  233
+
  234
+        self.assertTrue(condition.is_satisfied(self.basket))
  235
+        # consume 2 and 4
  236
+        third_discount = self.benefit.apply(self.basket, condition=condition)
  237
+        self.assertEquals(Decimal('2.00'), third_discount)
  238
+
  239
+        self.assertTrue(condition.is_satisfied(self.basket))
  240
+        # consume 3 and 3
  241
+        third_discount = self.benefit.apply(self.basket, condition=condition)
  242
+        self.assertEquals(Decimal('3.00'), third_discount)
  243
+
  244
+        # end of items (one not discounted item in basket)
  245
+        self.assertFalse(condition.is_satisfied(self.basket))
  246
+
  247
+    def test_products_with_no_stockrecord_are_handled_ok(self):
  248
+        self.basket.add_product(self.item, 3)
  249
+        self.basket.add_product(create_product())
  250
+        condition = models.CountCondition(range=self.range, type="Count", value=3)
  251
+        self.benefit.apply(self.basket, condition)
  252
+
  253
+
  254
+class FixedPriceBenefitTest(OfferTest):
  255
+
  256
+    def setUp(self):
  257
+        super(FixedPriceBenefitTest, self).setUp()
  258
+        self.benefit = models.FixedPriceBenefit(range=self.range, type="FixedPrice", value=Decimal('10.00'))
  259
+
  260
+    def test_correct_discount_for_count_condition(self):
  261
+        products = [create_product(Decimal('7.00')),
  262
+                    create_product(Decimal('8.00')),
  263
+                    create_product(Decimal('12.00'))]
  264
+
  265
+        # Create range that includes the products
  266
+        range = models.Range.objects.create(name="Dummy range")
  267
+        for product in products:
  268
+            range.included_products.add(product)
  269
+        condition = models.CountCondition(range=range, type="Count", value=3)
  270
+
  271
+        # Create basket that satisfies condition but with one extra product
  272
+        basket = Basket.objects.create()
  273
+        [basket.add_product(p, 2) for p in products]
  274
+
  275
+        benefit = models.FixedPriceBenefit(range=range, type="FixedPrice", value=Decimal('20.00'))
  276
+        self.assertEquals(Decimal('2.00'), benefit.apply(basket, condition))
  277
+        self.assertEquals(Decimal('12.00'), benefit.apply(basket, condition))
  278
+        self.assertEquals(Decimal('0.00'), benefit.apply(basket, condition))
  279
+
  280
+    def test_correct_discount_is_returned(self):
  281
+        products = [create_product(Decimal('8.00')), create_product(Decimal('4.00'))]
  282
+        range = models.Range.objects.create(name="Dummy range")
  283
+        for product in products:
  284
+            range.included_products.add(product)
  285
+            range.included_products.add(product)
  286
+
  287
+        basket = Basket.objects.create()
  288
+        [basket.add_product(p) for p in products]
  289
+
  290
+        condition = models.CoverageCondition(range=range, type="Coverage", value=2)
  291
+        discount = self.benefit.apply(basket, condition)
  292
+        self.assertEquals(Decimal('2.00'), discount)
  293
+
  294
+    def test_no_discount_when_product_not_discountable(self):
  295
+        product = create_product(Decimal('18.00'))
  296
+        product.is_discountable = False
  297
+        product.save()
  298
+
  299
+        product_range = models.Range.objects.create(name="Dummy range")
  300
+        product_range.included_products.add(product)
  301
+
  302
+        basket = Basket.objects.create()
  303
+        basket.add_product(product)
  304
+
  305
+        condition = models.CoverageCondition(range=product_range, type="Coverage", value=1)
  306
+        discount = self.benefit.apply(basket, condition)
  307
+        self.assertEquals(Decimal('0.00'), discount)
  308
+
  309
+    def test_no_discount_is_returned_when_value_is_greater_than_product_total(self):
  310
+        products = [create_product(Decimal('4.00')), create_product(Decimal('4.00'))]
  311
+        range = models.Range.objects.create(name="Dummy range")
  312
+        for product in products:
  313
+            range.included_products.add(product)
  314
+            range.included_products.add(product)
  315
+
  316
+        basket = Basket.objects.create()
  317
+        [basket.add_product(p) for p in products]
  318
+
  319
+        condition = models.CoverageCondition(range=range, type="Coverage", value=2)
  320
+        discount = self.benefit.apply(basket, condition)
  321
+        self.assertEquals(Decimal('0.00'), discount)
  322
+
  323
+    def test_discount_when_more_products_than_required(self):
  324
+        products = [create_product(Decimal('4.00')),
  325
+                    create_product(Decimal('8.00')),
  326
+                    create_product(Decimal('12.00'))]
  327
+
  328
+        # Create range that includes the products
  329
+        range = models.Range.objects.create(name="Dummy range")
  330
+        for product in products:
  331
+            range.included_products.add(product)
  332
+        condition = models.CoverageCondition(range=range, type="Coverage", value=3)
  333
+
  334
+        # Create basket that satisfies condition but with one extra product
  335
+        basket = Basket.objects.create()
  336
+        [basket.add_product(p) for p in products]
  337
+        basket.add_product(products[0])
  338
+
  339
+        benefit = models.FixedPriceBenefit(range=range, type="FixedPrice", value=Decimal('20.00'))
  340
+        discount = benefit.apply(basket, condition)
  341
+        self.assertEquals(Decimal('4.00'), discount)
  342
+
  343
+    def test_discount_when_applied_twice(self):
  344
+        products = [create_product(Decimal('4.00')),
  345
+                    create_product(Decimal('8.00')),
  346
+                    create_product(Decimal('12.00'))]
  347
+
  348
+        # Create range that includes the products
  349
+        range = models.Range.objects.create(name="Dummy range")
  350
+        for product in products:
  351
+            range.included_products.add(product)
  352
+        condition = models.CoverageCondition(range=range, type="Coverage", value=3)
  353
+
  354
+        # Create basket that satisfies condition but with one extra product
  355
+        basket = Basket.objects.create()
  356
+        [basket.add_product(p, 2) for p in products]
  357
+
  358
+        benefit = models.FixedPriceBenefit(range=range, type="FixedPrice", value=Decimal('20.00'))
  359
+        first_discount = benefit.apply(basket, condition)
  360
+        self.assertEquals(Decimal('4.00'), first_discount)
  361
+        second_discount = benefit.apply(basket, condition)
  362
+        self.assertEquals(Decimal('4.00'), second_discount)
215  tests/unit/offer/condition_tests.py
... ...
@@ -0,0 +1,215 @@
  1
+from decimal import Decimal
  2
+
  3
+from django.test import TestCase
  4
+
  5
+from oscar.apps.offer import models
  6
+from oscar.apps.basket.models import Basket
  7
+from oscar.test.helpers import create_product
  8
+from tests.unit.offer import OfferTest
  9
+
  10
+
  11
+class CountConditionTest(OfferTest):
  12
+
  13
+    def setUp(self):
  14
+        super(CountConditionTest, self).setUp()
  15
+        self.cond = models.CountCondition(range=self.range, type="Count", value=2)
  16
+
  17
+    def test_empty_basket_fails_condition(self):
  18
+        self.assertFalse(self.cond.is_satisfied(self.basket))
  19
+
  20
+    def test_not_discountable_product_fails_condition(self):
  21
+        prod1, prod2 = create_product(), create_product()
  22
+        prod2.is_discountable = False
  23
+        prod2.save()
  24
+        self.basket.add_product(prod1)
  25
+        self.basket.add_product(prod2)
  26
+        self.assertFalse(self.cond.is_satisfied(self.basket))
  27
+
  28
+    def test_empty_basket_fails_partial_condition(self):
  29
+        self.assertFalse(self.cond.is_partially_satisfied(self.basket))
  30
+
  31
+    def test_smaller_quantity_basket_passes_partial_condition(self):
  32
+        self.basket.add_product(create_product(), 1)
  33
+        self.assertTrue(self.cond.is_partially_satisfied(self.basket))
  34
+
  35
+    def test_smaller_quantity_basket_upsell_message(self):
  36
+        self.basket.add_product(create_product(), 1)
  37
+        self.assertTrue('Buy 1 more product from ' in
  38
+                        self.cond.get_upsell_message(self.basket))
  39
+
  40
+    def test_matching_quantity_basket_fails_partial_condition(self):
  41
+        self.basket.add_product(create_product(), 2)
  42
+        self.assertFalse(self.cond.is_partially_satisfied(self.basket))
  43
+
  44
+    def test_matching_quantity_basket_passes_condition(self):
  45
+        self.basket.add_product(create_product(), 2)
  46
+        self.assertTrue(self.cond.is_satisfied(self.basket))
  47
+
  48
+    def test_greater_quantity_basket_passes_condition(self):
  49
+        self.basket.add_product(create_product(), 3)
  50
+        self.assertTrue(self.cond.is_satisfied(self.basket))
  51
+
  52
+    def test_consumption(self):
  53
+        self.basket.add_product(create_product(), 3)
  54
+        self.cond.consume_items(self.basket)
  55
+        self.assertEquals(1, self.basket.all_lines()[0].quantity_without_discount)
  56
+
  57
+    def test_is_satisfied_accounts_for_consumed_items(self):
  58
+        self.basket.add_product(create_product(), 3)
  59
+        self.cond.consume_items(self.basket)
  60
+        self.assertFalse(self.cond.is_satisfied(self.basket))
  61
+
  62
+    def test_count_condition_is_applied_multpile_times(self):
  63
+        benefit = models.AbsoluteDiscountBenefit(range=self.range, type="Absolute", value=Decimal('10.00'))
  64
+        for i in range(10):
  65
+            self.basket.add_product(create_product(price=Decimal('5.00'), upc='upc_%i' % i), 1)
  66
+        product_range = models.Range.objects.create(name="All products", includes_all_products=True)
  67
+        condition = models.CountCondition(range=product_range, type="Count", value=2)
  68
+
  69
+        first_discount = benefit.apply(self.basket, condition=condition)
  70
+        self.assertEquals(Decimal('10.00'), first_discount)
  71
+
  72
+        second_discount = benefit.apply(self.basket, condition=condition)
  73
+        self.assertEquals(Decimal('10.00'), second_discount)
  74
+
  75
+
  76
+class ValueConditionTest(OfferTest):
  77
+    def setUp(self):
  78
+        super(ValueConditionTest, self).setUp()
  79
+        self.cond = models.ValueCondition(range=self.range, type="Value", value=Decimal('10.00'))
  80
+        self.item = create_product(price=Decimal('5.00'))
  81
+        self.expensive_item = create_product(price=Decimal('15.00'))
  82
+
  83
+    def test_empty_basket_fails_condition(self):
  84
+        self.assertFalse(self.cond.is_satisfied(self.basket))
  85
+
  86
+    def test_empty_basket_fails_partial_condition(self):
  87
+        self.assertFalse(self.cond.is_partially_satisfied(self.basket))
  88
+
  89
+    def test_less_value_basket_fails_condition(self):
  90
+        self.basket.add_product(self.item, 1)
  91
+        self.assertFalse(self.cond.is_satisfied(self.basket))
  92
+
  93
+    def test_not_discountable_item_fails_condition(self):
  94
+        self.expensive_item.is_discountable = False
  95
+        self.expensive_item.save()
  96
+        self.basket.add_product(self.expensive_item, 1)
  97
+        self.assertFalse(self.cond.is_satisfied(self.basket))
  98
+
  99
+    def test_upsell_message(self):
  100
+        self.basket.add_product(self.item, 1)
  101
+        self.assertTrue('Spend' in self.cond.get_upsell_message(self.basket))
  102
+
  103
+    def test_matching_basket_fails_partial_condition(self):
  104
+        self.basket.add_product(self.item, 2)
  105
+        self.assertFalse(self.cond.is_partially_satisfied(self.basket))
  106
+
  107
+    def test_less_value_basket_passes_partial_condition(self):
  108
+        self.basket.add_product(self.item, 1)
  109
+        self.assertTrue(self.cond.is_partially_satisfied(self.basket))
  110
+
  111
+    def test_matching_basket_passes_condition(self):
  112
+        self.basket.add_product(self.item, 2)
  113
+        self.assertTrue(self.cond.is_satisfied(self.basket))
  114
+
  115
+    def test_greater_than_basket_passes_condition(self):
  116
+        self.basket.add_product(self.item, 3)
  117
+        self.assertTrue(self.cond.is_satisfied(self.basket))
  118
+
  119
+    def test_consumption(self):
  120
+        self.basket.add_product(self.item, 3)
  121
+        self.cond.consume_items(self.basket)
  122
+        self.assertEquals(1, self.basket.all_lines()[0].quantity_without_discount)
  123
+
  124
+    def test_consumption_with_high_value_product(self):
  125
+        self.basket.add_product(self.expensive_item, 1)
  126
+        self.cond.consume_items(self.basket)
  127
+        self.assertEquals(0, self.basket.all_lines()[0].quantity_without_discount)
  128
+
  129
+    def test_is_consumed_respects_quantity_consumed(self):
  130
+        self.basket.add_product(self.expensive_item, 1)
  131
+        self.assertTrue(self.cond.is_satisfied(self.basket))
  132
+        self.cond.consume_items(self.basket)
  133
+        self.assertFalse(self.cond.is_satisfied(self.basket))
  134
+
  135
+
  136
+class CoverageConditionTest(TestCase):
  137
+
  138
+    def setUp(self):
  139
+        self.products = [create_product(Decimal('5.00')), create_product(Decimal('10.00'))]
  140
+        self.range = models.Range.objects.create(name="Some products")
  141
+        for product in self.products:
  142
+            self.range.included_products.add(product)
  143
+            self.range.included_products.add(product)
  144
+
  145
+        self.basket = Basket.objects.create()
  146
+        self.cond = models.CoverageCondition(range=self.range, type="Coverage", value=2)
  147
+
  148
+    def test_empty_basket_fails(self):
  149
+        self.assertFalse(self.cond.is_satisfied(self.basket))
  150
+
  151
+    def test_empty_basket_fails_partial_condition(self):
  152
+        self.assertFalse(self.cond.is_partially_satisfied(self.basket))
  153
+
  154
+    def test_single_item_fails(self):
  155
+        self.basket.add_product(self.products[0])
  156
+        self.assertFalse(self.cond.is_satisfied(self.basket))
  157
+
  158
+    def test_not_discountable_item_fails(self):
  159
+        self.products[0].is_discountable = False
  160
+        self.products[0].save()
  161
+        self.basket.add_product(self.products[0])
  162
+        self.basket.add_product(self.products[1])
  163
+        self.assertFalse(self.cond.is_satisfied(self.basket))
  164
+
  165
+    def test_single_item_passes_partial_condition(self):
  166
+        self.basket.add_product(self.products[0])
  167
+        self.assertTrue(self.cond.is_partially_satisfied(self.basket))
  168
+
  169
+    def test_upsell_message(self):
  170
+        self.basket.add_product(self.products[0])
  171
+        self.assertTrue('Buy 1 more' in self.cond.get_upsell_message(self.basket))
  172
+
  173
+    def test_duplicate_item_fails(self):
  174
+        self.basket.add_product(self.products[0])
  175
+        self.basket.add_product(self.products[0])
  176
+        self.assertFalse(self.cond.is_satisfied(self.basket))
  177
+
  178
+    def test_duplicate_item_passes_partial_condition(self):
  179
+        self.basket.add_product(self.products[0], 2)
  180
+        self.assertTrue(self.cond.is_partially_satisfied(self.basket))
  181
+
  182
+    def test_covering_items_pass(self):
  183
+        self.basket.add_product(self.products[0])
  184
+        self.basket.add_product(self.products[1])
  185
+        self.assertTrue(self.cond.is_satisfied(self.basket))
  186
+
  187
+    def test_covering_items_fail_partial_condition(self):
  188
+        self.basket.add_product(self.products[0])
  189
+        self.basket.add_product(self.products[1])
  190
+        self.assertFalse(self.cond.is_partially_satisfied(self.basket))
  191
+
  192
+    def test_covering_items_are_consumed(self):
  193
+        self.basket.add_product(self.products[0])
  194
+        self.basket.add_product(self.products[1])
  195
+        self.cond.consume_items(self.basket)
  196
+        self.assertEquals(0, self.basket.num_items_without_discount)
  197
+
  198
+    def test_consumed_items_checks_affected_items(self):
  199
+        # Create new offer
  200
+        range = models.Range.objects.create(name="All products", includes_all_products=True)
  201
+        cond = models.CoverageCondition(range=range, type="Coverage", value=2)
  202
+
  203
+        # Get 4 distinct products in the basket
  204
+        self.products.extend([create_product(Decimal('15.00')), create_product(Decimal('20.00'))])
  205
+
  206
+        for product in self.products:
  207
+            self.basket.add_product(product)
  208
+
  209
+        self.assertTrue(cond.is_satisfied(self.basket))
  210
+        cond.consume_items(self.basket)
  211
+        self.assertEquals(2, self.basket.num_items_without_discount)
  212
+
  213
+        self.assertTrue(cond.is_satisfied(self.basket))
  214
+        cond.consume_items(self.basket)
  215
+        self.assertEquals(0, self.basket.num_items_without_discount)
89  tests/unit/offer/offer_tests.py
... ...
@@ -0,0 +1,89 @@
  1
+import datetime
  2
+
  3
+from django.conf import settings
  4
+from django.test import TestCase
  5
+
  6
+from oscar.apps.offer import models
  7
+from oscar.test.helpers import create_product
  8
+
  9
+
  10
+class WholeSiteRangeWithGlobalBlacklistTest(TestCase):
  11
+
  12
+    def setUp(self):
  13
+        self.range = models.Range.objects.create(name="All products", includes_all_products=True)
  14
+
  15
+    def tearDown(self):
  16
+        settings.OSCAR_OFFER_BLACKLIST_PRODUCT = None
  17
+
  18
+    def test_blacklisting_prevents_products_being_in_range(self):
  19
+        settings.OSCAR_OFFER_BLACKLIST_PRODUCT = lambda p: True
  20
+        prod = create_product()
  21
+        self.assertFalse(self.range.contains_product(prod))
  22
+
  23
+    def test_blacklisting_can_use_product_class(self):
  24
+        settings.OSCAR_OFFER_BLACKLIST_PRODUCT = lambda p: p.product_class.name == 'giftcard'
  25
+        prod = create_product(product_class="giftcard")
  26
+        self.assertFalse(self.range.contains_product(prod))
  27
+
  28
+    def test_blacklisting_doesnt_exlude_everything(self):
  29
+        settings.OSCAR_OFFER_BLACKLIST_PRODUCT = lambda p: p.product_class.name == 'giftcard'
  30
+        prod = create_product(product_class="book")
  31
+        self.assertTrue(self.range.contains_product(prod))
  32
+
  33
+
  34
+class WholeSiteRangeTest(TestCase):
  35
+
  36
+    def setUp(self):
  37
+        self.range = models.Range.objects.create(name="All products", includes_all_products=True)
  38
+        self.prod = create_product()
  39
+
  40
+    def test_all_products_range(self):
  41
+        self.assertTrue(self.range.contains_product(self.prod))
  42
+
  43
+    def test_all_products_range_with_exception(self):
  44
+        self.range.excluded_products.add(self.prod)
  45
+        self.assertFalse(self.range.contains_product(self.prod))
  46
+
  47
+    def test_whitelisting(self):
  48
+        self.range.included_products.add(self.prod)
  49
+        self.assertTrue(self.range.contains_product(self.prod))
  50
+
  51
+    def test_blacklisting(self):
  52
+        self.range.excluded_products.add(self.prod)
  53
+        self.assertFalse(self.range.contains_product(self.prod))
  54
+
  55
+
  56
+class PartialRangeTest(TestCase):
  57
+
  58
+    def setUp(self):
  59
+        self.range = models.Range.objects.create(name="All products", includes_all_products=False)
  60
+        self.prod = create_product()
  61
+
  62
+    def test_empty_list(self):
  63
+        self.assertFalse(self.range.contains_product(self.prod))
  64
+
  65
+    def test_included_classes(self):
  66
+        self.range.classes.add(self.prod.product_class)
  67
+        self.assertTrue(self.range.contains_product(self.prod))
  68
+
  69
+    def test_included_class_with_exception(self):
  70
+        self.range.classes.add(self.prod.product_class)
  71
+        self.range.excluded_products.add(self.prod)
  72
+        self.assertFalse(self.range.contains_product(self.prod))
  73
+
  74
+
  75
+class ConditionalOfferTest(TestCase):
  76
+
  77
+    def test_is_active(self):
  78
+        start = datetime.date(2011, 01, 01)
  79
+        test = datetime.date(2011, 01, 10)
  80
+        end = datetime.date(2011, 02, 01)
  81
+        offer = models.ConditionalOffer(start_date=start, end_date=end)
  82
+        self.assertTrue(offer.is_active(test))
  83
+
  84
+    def test_is_inactive(self):
  85
+        start = datetime.date(2011, 01, 01)
  86
+        test = datetime.date(2011, 03, 10)
  87
+        end = datetime.date(2011, 02, 01)
  88
+        offer = models.ConditionalOffer(start_date=start, end_date=end)
  89
+        self.assertFalse(offer.is_active(test))
656  tests/unit/offer_tests.py
... ...
@@ -1,656 +0,0 @@
1  
-from decimal import Decimal
2  
-import datetime
3  
-
4  
-from django.conf import settings
5  
-from django.test import TestCase
6  
-
7  
-from oscar.apps.offer.models import (Range, CountCondition, ValueCondition,
8  
-                                     CoverageCondition, ConditionalOffer,
9  
-                                     PercentageDiscountBenefit, FixedPriceBenefit,
10  
-                                     MultibuyDiscountBenefit, AbsoluteDiscountBenefit)
11  
-from oscar.apps.basket.models import Basket
12  
-from oscar.test.helpers import create_product
13  
-
14  
-
15  
-class WholeSiteRangeWithGlobalBlacklistTest(TestCase):
16  
-
17  
-    def setUp(self):
18  
-        self.range = Range.objects.create(name="All products", includes_all_products=True)
19  
-
20  
-    def tearDown(self):
21  
-        settings.OSCAR_OFFER_BLACKLIST_PRODUCT = None
22  
-
23  
-    def test_blacklisting_prevents_products_being_in_range(self):
24  
-        settings.OSCAR_OFFER_BLACKLIST_PRODUCT = lambda p: True
25  
-        prod = create_product()
26  
-        self.assertFalse(self.range.contains_product(prod))
27  
-
28  
-    def test_blacklisting_can_use_product_class(self):
29  
-        settings.OSCAR_OFFER_BLACKLIST_PRODUCT = lambda p: p.product_class.name == 'giftcard'
30  
-        prod = create_product(product_class="giftcard")
31  
-        self.assertFalse(self.range.contains_product(prod))
32  
-
33  
-    def test_blacklisting_doesnt_exlude_everything(self):
34  
-        settings.OSCAR_OFFER_BLACKLIST_PRODUCT = lambda p: p.product_class.name == 'giftcard'
35  
-        prod = create_product(product_class="book")
36  
-        self.assertTrue(self.range.contains_product(prod))
37  
-
38  
-
39  
-class WholeSiteRangeTest(TestCase):
40  
-    
41  
-    def setUp(self):
42  
-        self.range = Range.objects.create(name="All products", includes_all_products=True)
43  
-        self.prod = create_product()
44  
-    
45  
-    def test_all_products_range(self):
46  
-        self.assertTrue(self.range.contains_product(self.prod))
47  
-        
48  
-    def test_all_products_range_with_exception(self):
49  
-        self.range.excluded_products.add(self.prod)
50  
-        self.assertFalse(self.range.contains_product(self.prod))
51  
-        
52  
-    def test_whitelisting(self):
53  
-        self.range.included_products.add(self.prod)
54  
-        self.assertTrue(self.range.contains_product(self.prod))
55  
-        
56  
-    def test_blacklisting(self):
57  
-        self.range.excluded_products.add(self.prod)
58  
-        self.assertFalse(self.range.contains_product(self.prod))
59  
-        
60  
-
61  
-class PartialRangeTest(TestCase):
62  
-    
63  
-    def setUp(self):
64  
-        self.range = Range.objects.create(name="All products", includes_all_products=False)
65  
-        self.prod = create_product()
66  
-
67  
-    def test_empty_list(self):
68  
-        self.assertFalse(self.range.contains_product(self.prod))
69  
-        
70  
-    def test_included_classes(self):
71  
-        self.range.classes.add(self.prod.product_class)
72  
-        self.assertTrue(self.range.contains_product(self.prod))
73  
-        
74  
-    def test_included_class_with_exception(self):
75  
-        self.range.classes.add(self.prod.product_class)
76  
-        self.range.excluded_products.add(self.prod)
77  
-        self.assertFalse(self.range.contains_product(self.prod))
78  
-
79  
-
80  
-class OfferTest(TestCase):
81  
-    def setUp(self):
82  
-        self.range = Range.objects.create(name="All products range", includes_all_products=True)
83  
-        self.basket = Basket.objects.create()
84  
-
85  
-
86  
-class CountConditionTest(OfferTest):
87  
-    
88  
-    def setUp(self):
89  
-        super(CountConditionTest, self).setUp()
90  
-        self.cond = CountCondition(range=self.range, type="Count", value=2)
91  
-    
92  
-    def test_empty_basket_fails_condition(self):
93  
-        self.assertFalse(self.cond.is_satisfied(self.basket))
94  
-
95  
-    def test_not_discountable_product_fails_condition(self):
96  
-        prod1, prod2 = create_product(), create_product()
97  
-        prod2.is_discountable = False
98  
-        prod2.save()
99  
-        self.basket.add_product(prod1)
100  
-        self.basket.add_product(prod2)
101  
-        self.assertFalse(self.cond.is_satisfied(self.basket))
102  
-
103  
-    def test_empty_basket_fails_partial_condition(self):
104  
-        self.assertFalse(self.cond.is_partially_satisfied(self.basket))
105  
-
106  
-    def test_smaller_quantity_basket_passes_partial_condition(self):
107  
-        self.basket.add_product(create_product(), 1)
108  
-        self.assertTrue(self.cond.is_partially_satisfied(self.basket))
109  
-
110  
-    def test_smaller_quantity_basket_upsell_message(self):
111  
-        self.basket.add_product(create_product(), 1)
112  
-        self.assertTrue('Buy 1 more product from ' in
113  
-                        self.cond.get_upsell_message(self.basket))
114  
-
115  
-    def test_matching_quantity_basket_fails_partial_condition(self):
116  
-        self.basket.add_product(create_product(), 2)
117  
-        self.assertFalse(self.cond.is_partially_satisfied(self.basket))
118  
-        
119  
-    def test_matching_quantity_basket_passes_condition(self):
120  
-        self.basket.add_product(create_product(), 2)
121  
-        self.assertTrue(self.cond.is_satisfied(self.basket))
122  
-        
123  
-    def test_greater_quantity_basket_passes_condition(self):
124  
-        self.basket.add_product(create_product(), 3)
125  
-        self.assertTrue(self.cond.is_satisfied(self.basket))
126  
-
127  
-    def test_consumption(self):