Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base fork: django-oscar/django-oscar
...
head fork: django-oscar/django-oscar
  • 8 commits
  • 8 files changed
  • 0 commit comments
  • 2 contributors
View
123 oscar/apps/offer/models.py
@@ -25,14 +25,14 @@ class ConditionalOffer(models.Model):
description = models.TextField(_('Description'), blank=True, null=True)
# Offers come in a few different types:
- # (a) Offers that are available to all customers on the site. Eg a
+ # (a) Offers that are available to all customers on the site. Eg a
# 3-for-2 offer.
# (b) Offers that are linked to a voucher, and only become available once
# that voucher has been applied to the basket
# (c) Offers that are linked to a user. Eg, all students get 10% off. The code
# to apply this offer needs to be coded
- # (d) Session offers - these are temporarily available to a user after some trigger
- # event. Eg, users coming from some affiliate site get 10% off.
+ # (d) Session offers - these are temporarily available to a user after some trigger
+ # event. Eg, users coming from some affiliate site get 10% off.
SITE, VOUCHER, USER, SESSION = ("Site", "Voucher", "User", "Session")
TYPE_CHOICES = (
(SITE, _("Site offer - available to all users")),
@@ -46,7 +46,7 @@ class ConditionalOffer(models.Model):
benefit = models.ForeignKey('offer.Benefit')
# Range of availability. Note that if this is a voucher offer, then these
- # dates are ignored and only the dates from the voucher are used to determine
+ # dates are ignored and only the dates from the voucher are used to determine
# availability.
start_date = models.DateField(_('Start Date'), blank=True, null=True)
end_date = models.DateField(_('End Date'), blank=True, null=True,
@@ -60,7 +60,7 @@ class ConditionalOffer(models.Model):
# We track some information on usage
total_discount = models.DecimalField(_('Total Discount'), decimal_places=2, max_digits=12, default=Decimal('0.00'))
num_orders = models.PositiveIntegerField(_('Number of Orders'), default=0)
-
+
date_created = models.DateTimeField(auto_now_add=True)
objects = models.Manager()
@@ -83,19 +83,19 @@ def save(self, *args, **kwargs):
def get_absolute_url(self):
return reverse('offer:detail', kwargs={'slug': self.slug})
-
+
def __unicode__(self):
- return self.name
+ return self.name
def clean(self):
if self.start_date and self.end_date and self.start_date > self.end_date:
raise exceptions.ValidationError(_('End date should be later than start date'))
-
+
def is_active(self, test_date=None):
if not test_date:
test_date = datetime.date.today()
return self.start_date <= test_date and test_date < self.end_date
-
+
def is_condition_satisfied(self, basket):
return self._proxy_condition().is_satisfied(basket)
@@ -104,7 +104,7 @@ def is_condition_partially_satisfied(self, basket):
def get_upsell_message(self, basket):
return self._proxy_condition().get_upsell_message(basket)
-
+
def apply_benefit(self, basket):
"""
Applies the benefit to the given basket and returns the discount.
@@ -112,13 +112,13 @@ def apply_benefit(self, basket):
if not self.is_condition_satisfied(basket):
return Decimal('0.00')
return self._proxy_benefit().apply(basket, self._proxy_condition())
-
+
def set_voucher(self, voucher):
self._voucher = voucher
-
+
def get_voucher(self):
- return self._voucher
-
+ return self._voucher
+
def _proxy_condition(self):
"""
Returns the appropriate proxy model for the condition
@@ -135,7 +135,7 @@ def _proxy_condition(self):
elif self.condition.type == self.condition.COVERAGE:
return CoverageCondition(**field_dict)
return self.condition
-
+
def _proxy_benefit(self):
"""
Returns the appropriate proxy model for the condition
@@ -157,7 +157,7 @@ def record_usage(self, discount):
self.num_orders += 1
self.total_discount += discount
self.save()
-
+
class Condition(models.Model):
COUNT, VALUE, COVERAGE = ("Count", "Value", "Coverage")
@@ -185,10 +185,10 @@ def __unicode__(self):
'count': self.value, 'range': unicode(self.range).lower()}
description = __unicode__
-
+
def consume_items(self, basket, lines=None):
return ()
-
+
def is_satisfied(self, basket):
"""
Determines whether a given basket meets this condition. This is
@@ -212,9 +212,9 @@ def can_apply_condition(self, product):
"""
Determines whether the condition can be applied to a given product
"""
- return (self.range.contains_product(product)
+ return (self.range.contains_product(product)
and product.is_discountable)
-
+
class Benefit(models.Model):
PERCENTAGE, FIXED, MULTIBUY, FIXED_PRICE = ("Percentage", "Absolute", "Multibuy", "Fixed price")
@@ -231,7 +231,7 @@ class Benefit(models.Model):
price_field = 'price_incl_tax'
- # If this is not set, then there is no upper limit on how many products
+ # If this is not set, then there is no upper limit on how many products
# can be discounted by this benefit.
max_affected_items = models.PositiveIntegerField(_('Max Affected Items'), blank=True, null=True,
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):
return desc
description = __unicode__
-
+
def apply(self, basket, condition=None):
return Decimal('0.00')
-
+
def clean(self):
if self.value is None:
if not self.type:
@@ -306,7 +306,7 @@ class Range(models.Model):
classes = models.ManyToManyField('catalogue.ProductClass', related_name='classes', blank=True)
included_categories = models.ManyToManyField('catalogue.Category', related_name='includes', blank=True)
date_created = models.DateTimeField(auto_now_add=True)
-
+
__included_product_ids = None
__excluded_product_ids = None
__class_ids = None
@@ -316,8 +316,8 @@ class Meta:
verbose_name_plural = _("Ranges")
def __unicode__(self):
- return self.name
-
+ return self.name
+
def contains_product(self, product):
"""
Check whether the passed product is part of this range
@@ -334,28 +334,28 @@ def contains_product(self, product):
if self.includes_all_products:
return True
if product.product_class_id in self._class_ids():
- return True
+ return True
included_product_ids = self._included_product_ids()
if product.id in included_product_ids:
return True
- test_categories = self.included_categories.all()
+ test_categories = self.included_categories.all()
if test_categories:
for category in product.categories.all():
for test_category in test_categories:
if category == test_category or category.is_descendant_of(test_category):
return True
return False
-
+
def _included_product_ids(self):
if None == self.__included_product_ids:
self.__included_product_ids = [row['id'] for row in self.included_products.values('id')]
return self.__included_product_ids
-
+
def _excluded_product_ids(self):
if None == self.__excluded_product_ids:
self.__excluded_product_ids = [row['id'] for row in self.excluded_products.values('id')]
return self.__excluded_product_ids
-
+
def _class_ids(self):
if None == self.__class_ids:
self.__class_ids = [row['id'] for row in self.classes.values('id')]
@@ -365,7 +365,7 @@ def num_products(self):
if self.includes_all_products:
return None
return self.included_products.all().count()
-
+
class CountCondition(Condition):
"""
@@ -383,7 +383,7 @@ def is_satisfied(self, basket):
"""
num_matches = 0
for line in basket.all_lines():
- if (self.can_apply_condition(line.product)
+ if (self.can_apply_condition(line.product)
and line.quantity_without_discount > 0):
num_matches += line.quantity_without_discount
if num_matches >= self.value:
@@ -395,7 +395,7 @@ def _get_num_matches(self, basket):
return getattr(self, '_num_matches')
num_matches = 0
for line in basket.all_lines():
- if (self.can_apply_condition(line.product)
+ if (self.can_apply_condition(line.product)
and line.quantity_without_discount > 0):
num_matches += line.quantity_without_discount
self._num_matches = num_matches
@@ -429,8 +429,8 @@ def consume_items(self, basket, lines=None, value=None):
if len(consumed_products) == value:
break
return consumed_products
-
-
+
+
class CoverageCondition(Condition):
"""
An offer condition dependent on the number of DISTINCT matching items from the basket.
@@ -474,7 +474,7 @@ def get_upsell_message(self, basket):
def is_partially_satisfied(self, basket):
return 0 < self._get_num_covered_products(basket) < self.value
-
+
def consume_items(self, basket, lines=None, value=None):
"""
Marks items within the basket lines as consumed so they
@@ -492,7 +492,7 @@ def consume_items(self, basket, lines=None, value=None):
if len(consumed_products) >= value:
break
return consumed_products
-
+
def get_value_of_satisfying_items(self, basket):
covered_ids = []
value = Decimal('0.00')
@@ -503,8 +503,8 @@ def get_value_of_satisfying_items(self, basket):
if len(covered_ids) >= self.value:
return value
return value
-
-
+
+
class ValueCondition(Condition):
"""
An offer condition dependent on the VALUE of matching items from the basket.
@@ -522,7 +522,7 @@ def is_satisfied(self, basket):
value_of_matches = Decimal('0.00')
for line in basket.all_lines():
product = line.product
- if (self.can_apply_condition(product) and product.has_stockrecord
+ if (self.can_apply_condition(product) and product.has_stockrecord
and line.quantity_without_discount > 0):
price = getattr(product.stockrecord, self.price_field)
value_of_matches += price * int(line.quantity_without_discount)
@@ -536,7 +536,7 @@ def _get_value_of_matches(self, basket):
value_of_matches = Decimal('0.00')
for line in basket.all_lines():
product = line.product
- if (self.can_apply_condition(product) and product.has_stockrecord
+ if (self.can_apply_condition(product) and product.has_stockrecord
and line.quantity_without_discount > 0):
price = getattr(product.stockrecord, self.price_field)
value_of_matches += price * int(line.quantity_without_discount)
@@ -550,12 +550,12 @@ def is_partially_satisfied(self, basket):
def get_upsell_message(self, basket):
value_of_matches = self._get_value_of_matches(basket)
return _('Spend %(value)s more from %(range)s') % {'value': value_of_matches, 'range': self.range}
-
+
def consume_items(self, basket, lines=None, value=None):
"""
Marks items within the basket lines as consumed so they
can't be reused in other offers.
-
+
We allow lines to be passed in as sometimes we want them sorted
in a specific order.
"""
@@ -597,7 +597,7 @@ def apply(self, basket, condition=None):
discount = Decimal('0.00')
affected_items = 0
max_affected_items = self._effective_max_affected_items()
-
+
for line in basket.all_lines():
if affected_items >= max_affected_items:
break
@@ -605,13 +605,13 @@ def apply(self, basket, condition=None):
if (self.range.contains_product(product) and product.has_stockrecord
and self.can_apply_benefit(product)):
price = getattr(product.stockrecord, self.price_field)
- quantity = min(line.quantity_without_discount,
+ quantity = min(line.quantity_without_discount,
max_affected_items - affected_items)
line_discount = self.round(self.value/100 * price * int(quantity))
line.discount(line_discount, quantity)
affected_items += quantity
discount += line_discount
-
+
if discount > 0 and condition:
condition.consume_items(basket)
return discount
@@ -631,7 +631,7 @@ def apply(self, basket, condition=None):
discount = Decimal('0.00')
affected_items = 0
max_affected_items = self._effective_max_affected_items()
-
+
for line in basket.all_lines():
if affected_items >= max_affected_items:
break
@@ -643,33 +643,30 @@ def apply(self, basket, condition=None):
# Avoid zero price products
continue
remaining_discount = self.value - discount
- quantity_affected = int(min(line.quantity_without_discount,
+ quantity_affected = int(min(line.quantity_without_discount,
max_affected_items - affected_items,
math.ceil(remaining_discount / price)))
-
+
# Update line with discounts
line_discount = self.round(min(remaining_discount, quantity_affected * price))
- if not condition:
- line.discount(line_discount, quantity_affected)
-
+ line.discount(line_discount, quantity_affected)
+
# Update loop vars
affected_items += quantity_affected
remaining_discount -= line_discount
discount += line_discount
- if discount > 0 and condition:
- condition.consume_items(basket)
-
+
return discount
class FixedPriceBenefit(Benefit):
"""
- An offer benefit that gives the items in the condition for a
+ An offer benefit that gives the items in the condition for a
fixed price. This is useful for "bundle" offers.
-
+
Note that we ignore the benefit range here and only give a fixed price
for the products in the condition range.
-
+
The condition should be a count condition
"""
class Meta:
@@ -686,7 +683,7 @@ def apply(self, basket, condition=None):
product = line.product
if (condition.range.contains_product(product) and line.quantity_without_discount > 0
and self.can_apply_benefit(product)):
- # Line is available - determine quantity to consume and
+ # Line is available - determine quantity to consume and
# record the total of the consumed products
if isinstance(condition, CoverageCondition):
quantity = 1
@@ -698,10 +695,10 @@ def apply(self, basket, condition=None):
if num_covered >= num_permitted:
break
discount = max(product_total - self.value, Decimal('0.00'))
-
+
if not discount:
return discount
-
+
# Apply discount weighted by original value of line
discount_applied = Decimal('0.00')
last_line = covered_lines[-1][0]
@@ -714,7 +711,7 @@ def apply(self, basket, condition=None):
line_discount = self.round(discount * (line.unit_price_incl_tax * quantity) / product_total)
line.discount(line_discount, quantity)
discount_applied += line_discount
- return discount
+ return discount
class MultibuyDiscountBenefit(Benefit):
View
2  oscar/apps/shipping/__init__.py
@@ -23,6 +23,6 @@ def weigh_product(self, product):
def weigh_basket(self, basket):
weight = 0.0
for line in basket.lines.all():
- weight += self.weigh_product(line.product)
+ weight += self.weigh_product(line.product) * line.quantity
return weight
View
10 tests/unit/offer/__init__.py
@@ -0,0 +1,10 @@
+from django.test import TestCase
+
+from oscar.apps.offer import models
+from oscar.apps.basket.models import Basket
+
+
+class OfferTest(TestCase):
+ def setUp(self):
+ self.range = models.Range.objects.create(name="All products range", includes_all_products=True)
+ self.basket = Basket.objects.create()
View
362 tests/unit/offer/benefit_tests.py
@@ -0,0 +1,362 @@
+from decimal import Decimal
+
+from django.conf import settings
+
+from oscar.apps.basket.models import Basket
+from oscar.apps.offer import models
+from oscar.test.helpers import create_product
+from tests.unit.offer import OfferTest
+
+
+class PercentageDiscountBenefitTest(OfferTest):
+
+ def setUp(self):
+ super(PercentageDiscountBenefitTest, self).setUp()
+ self.benefit = models.PercentageDiscountBenefit(range=self.range, type="Percentage", value=Decimal('15.00'))
+ self.item = create_product(price=Decimal('5.00'))
+ self.original_offer_rounding_function = getattr(settings, 'OSCAR_OFFER_ROUNDING_FUNCTION', None)
+ if self.original_offer_rounding_function is not None:
+ delattr(settings, 'OSCAR_OFFER_ROUNDING_FUNCTION')
+
+ def tearDown(self):
+ super(PercentageDiscountBenefitTest, self).tearDown()
+ if self.original_offer_rounding_function is not None:
+ settings.OSCAR_OFFER_ROUNDING_FUNCTION = self.original_offer_rounding_function
+
+ def test_no_discount_for_empty_basket(self):
+ self.assertEquals(Decimal('0.00'), self.benefit.apply(self.basket))
+
+ def test_no_discount_for_not_discountable_product(self):
+ self.item.is_discountable = False
+ self.item.save()
+ self.basket.add_product(self.item, 1)
+ self.assertEquals(Decimal('0.00'), self.benefit.apply(self.basket))
+
+ def test_discount_for_single_item_basket(self):
+ self.basket.add_product(self.item, 1)
+ self.assertEquals(Decimal('0.15') * Decimal('5.00'), self.benefit.apply(self.basket))
+
+ def test_discount_for_multi_item_basket(self):
+ self.basket.add_product(self.item, 3)
+ self.assertEquals(Decimal('3') * Decimal('0.15') * Decimal('5.00'), self.benefit.apply(self.basket))
+
+ def test_discount_for_multi_item_basket_with_max_affected_items_set(self):
+ self.basket.add_product(self.item, 3)
+ self.benefit.max_affected_items = 1
+ self.assertEquals(Decimal('0.15') * Decimal('5.00'), self.benefit.apply(self.basket))
+
+ def test_discount_can_only_be_applied_once(self):
+ self.basket.add_product(self.item, 3)
+ self.benefit.apply(self.basket)
+ second_discount = self.benefit.apply(self.basket)
+ self.assertEquals(Decimal('0.00'), second_discount)
+
+ def test_discount_can_be_applied_several_times_when_max_is_set(self):
+ self.basket.add_product(self.item, 3)
+ self.benefit.max_affected_items = 1
+ for i in range(1, 4):
+ self.assertTrue(self.benefit.apply(self.basket) > 0)
+
+
+class AbsoluteDiscountBenefitTest(OfferTest):
+
+ def setUp(self):
+ super(AbsoluteDiscountBenefitTest, self).setUp()
+ self.benefit = models.AbsoluteDiscountBenefit(
+ range=self.range, type="Absolute", value=Decimal('10.00'))
+ self.item = create_product(price=Decimal('5.00'))
+ self.original_offer_rounding_function = getattr(settings, 'OSCAR_OFFER_ROUNDING_FUNCTION', None)
+ if self.original_offer_rounding_function is not None:
+ delattr(settings, 'OSCAR_OFFER_ROUNDING_FUNCTION')
+
+ def tearDown(self):
+ super(AbsoluteDiscountBenefitTest, self).tearDown()
+ if self.original_offer_rounding_function is not None:
+ settings.OSCAR_OFFER_ROUNDING_FUNCTION = self.original_offer_rounding_function
+
+ def test_no_discount_for_empty_basket(self):
+ self.assertEquals(Decimal('0.00'), self.benefit.apply(self.basket))
+
+ def test_no_discount_for_not_discountable_product(self):
+ self.item.is_discountable = False
+ self.item.save()
+ self.basket.add_product(self.item, 1)
+ self.assertEquals(Decimal('0.00'), self.benefit.apply(self.basket))
+
+ def test_discount_for_single_item_basket(self):
+ self.basket.add_product(self.item, 1)
+ self.assertEquals(Decimal('5.00'), self.benefit.apply(self.basket))
+
+ def test_discount_for_multi_item_basket(self):
+ self.basket.add_product(self.item, 3)
+ self.assertEquals(Decimal('10.00'), self.benefit.apply(self.basket))
+
+ def test_discount_for_multi_item_basket_with_max_affected_items_set(self):
+ self.basket.add_product(self.item, 3)
+ self.benefit.max_affected_items = 1
+ self.assertEquals(Decimal('5.00'), self.benefit.apply(self.basket))
+
+ def test_discount_can_only_be_applied_once(self):
+ # Add 3 items to make total 15.00
+ self.basket.add_product(self.item, 3)
+ first_discount = self.benefit.apply(self.basket)
+ self.assertEquals(Decimal('10.00'), first_discount)
+
+ second_discount = self.benefit.apply(self.basket)
+ self.assertEquals(Decimal('5.00'), second_discount)
+
+ def test_absolute_does_not_consume_twice(self):
+ product = create_product(Decimal('25000'))
+ rng = models.Range.objects.create(name='Dummy')
+ rng.included_products.add(product)
+ condition = models.ValueCondition(range=rng, type='Value', value=Decimal('5000'))
+ basket = Basket.objects.create()
+ basket.add_product(product, 5)
+ benefit = models.AbsoluteDiscountBenefit(range=rng, type='Absolute', value=Decimal('100'))
+ self.assertTrue(condition.is_satisfied(basket))
+ self.assertEquals(Decimal('100'), benefit.apply(basket, condition))
+ self.assertEquals(Decimal('100'), benefit.apply(basket, condition))
+ self.assertEquals(Decimal('100'), benefit.apply(basket, condition))
+ self.assertEquals(Decimal('100'), benefit.apply(basket, condition))
+ self.assertEquals(Decimal('100'), benefit.apply(basket, condition))
+ self.assertEquals(Decimal('0'), benefit.apply(basket, condition))
+
+ def test_discount_is_applied_to_lines(self):
+ condition = models.Condition.objects.create(
+ range=self.range, type="Count", value=1)
+ self.basket.add_product(self.item, 1)
+ self.benefit.apply(self.basket, condition)
+
+ self.assertTrue(self.basket.all_lines()[0].has_discount)
+
+
+class MultibuyDiscountBenefitTest(OfferTest):
+
+ def setUp(self):
+ super(MultibuyDiscountBenefitTest, self).setUp()
+ self.benefit = models.MultibuyDiscountBenefit(range=self.range, type="Multibuy", value=1)
+ self.item = create_product(price=Decimal('5.00'))
+
+ def test_no_discount_for_empty_basket(self):
+ self.assertEquals(Decimal('0.00'), self.benefit.apply(self.basket))
+
+ def test_discount_for_single_item_basket(self):
+ self.basket.add_product(self.item, 1)
+ self.assertEquals(Decimal('5.00'), self.benefit.apply(self.basket))
+
+ def test_discount_for_multi_item_basket(self):
+ self.basket.add_product(self.item, 3)
+ self.assertEquals(Decimal('5.00'), self.benefit.apply(self.basket))
+
+ def test_no_discount_for_not_discountable_product(self):
+ self.item.is_discountable = False
+ self.item.save()
+ self.basket.add_product(self.item, 1)
+ self.assertEquals(Decimal('0.00'), self.benefit.apply(self.basket))
+
+ def test_discount_does_not_consume_item_if_in_condition_range(self):
+ self.basket.add_product(self.item, 1)
+ first_discount = self.benefit.apply(self.basket)
+ self.assertEquals(Decimal('5.00'), first_discount)
+ second_discount = self.benefit.apply(self.basket)
+ self.assertEquals(Decimal('5.00'), second_discount)
+
+ def test_product_does_consume_item_if_not_in_condition_range(self):
+ # Set up condition using a different range from benefit
+ range = models.Range.objects.create(name="Small range")
+ other_product = create_product(price=Decimal('15.00'))
+ range.included_products.add(other_product)
+ cond = models.ValueCondition(range=range, type="Value", value=Decimal('10.00'))
+
+ self.basket.add_product(self.item, 1)
+ self.benefit.apply(self.basket, cond)
+ line = self.basket.all_lines()[0]
+ self.assertEqual(line.quantity_without_discount, 0)
+
+ def test_condition_consumes_most_expensive_lines_first(self):
+ for i in range(10, 0, -1):
+ product = create_product(price=Decimal(i), title='%i'%i, upc='upc_%i' % i)
+ self.basket.add_product(product, 1)
+
+ condition = models.CountCondition(range=self.range, type="Count", value=2)
+
+ self.assertTrue(condition.is_satisfied(self.basket))
+ # consume 1 and 10
+ first_discount = self.benefit.apply(self.basket, condition=condition)
+ self.assertEquals(Decimal('1.00'), first_discount)
+
+ self.assertTrue(condition.is_satisfied(self.basket))
+ # consume 2 and 9
+ second_discount = self.benefit.apply(self.basket, condition=condition)
+ self.assertEquals(Decimal('2.00'), second_discount)
+
+ self.assertTrue(condition.is_satisfied(self.basket))
+ # consume 3 and 8
+ third_discount = self.benefit.apply(self.basket, condition=condition)
+ self.assertEquals(Decimal('3.00'), third_discount)
+
+ self.assertTrue(condition.is_satisfied(self.basket))
+ # consume 4 and 7
+ fourth_discount = self.benefit.apply(self.basket, condition=condition)
+ self.assertEquals(Decimal('4.00'), fourth_discount)
+
+ self.assertTrue(condition.is_satisfied(self.basket))
+ # consume 5 and 6
+ fifth_discount = self.benefit.apply(self.basket, condition=condition)
+ self.assertEquals(Decimal('5.00'), fifth_discount)
+
+ # end of items (one not discounted item in basket)
+ self.assertFalse(condition.is_satisfied(self.basket))
+
+ def test_condition_consumes_most_expensive_lines_first_when_products_are_repeated(self):
+ for i in range(5, 0, -1):
+ product = create_product(price=Decimal(i), title='%i'%i, upc='upc_%i' % i)
+ self.basket.add_product(product, 2)
+
+ condition = models.CountCondition(range=self.range, type="Count", value=2)
+
+ # initial basket: [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]
+ self.assertTrue(condition.is_satisfied(self.basket))
+ # consume 1 and 5
+ first_discount = self.benefit.apply(self.basket, condition=condition)
+ self.assertEquals(Decimal('1.00'), first_discount)
+
+ self.assertTrue(condition.is_satisfied(self.basket))
+ # consume 1 and 5
+ second_discount = self.benefit.apply(self.basket, condition=condition)
+ self.assertEquals(Decimal('1.00'), second_discount)
+
+ self.assertTrue(condition.is_satisfied(self.basket))
+ # consume 2 and 4
+ third_discount = self.benefit.apply(self.basket, condition=condition)
+ self.assertEquals(Decimal('2.00'), third_discount)
+
+ self.assertTrue(condition.is_satisfied(self.basket))
+ # consume 2 and 4
+ third_discount = self.benefit.apply(self.basket, condition=condition)
+ self.assertEquals(Decimal('2.00'), third_discount)
+
+ self.assertTrue(condition.is_satisfied(self.basket))
+ # consume 3 and 3
+ third_discount = self.benefit.apply(self.basket, condition=condition)
+ self.assertEquals(Decimal('3.00'), third_discount)
+
+ # end of items (one not discounted item in basket)
+ self.assertFalse(condition.is_satisfied(self.basket))
+
+ def test_products_with_no_stockrecord_are_handled_ok(self):
+ self.basket.add_product(self.item, 3)
+ self.basket.add_product(create_product())
+ condition = models.CountCondition(range=self.range, type="Count", value=3)
+ self.benefit.apply(self.basket, condition)
+
+
+class FixedPriceBenefitTest(OfferTest):
+
+ def setUp(self):
+ super(FixedPriceBenefitTest, self).setUp()
+ self.benefit = models.FixedPriceBenefit(range=self.range, type="FixedPrice", value=Decimal('10.00'))
+
+ def test_correct_discount_for_count_condition(self):
+ products = [create_product(Decimal('7.00')),
+ create_product(Decimal('8.00')),
+ create_product(Decimal('12.00'))]
+
+ # Create range that includes the products
+ range = models.Range.objects.create(name="Dummy range")
+ for product in products:
+ range.included_products.add(product)
+ condition = models.CountCondition(range=range, type="Count", value=3)
+
+ # Create basket that satisfies condition but with one extra product
+ basket = Basket.objects.create()
+ [basket.add_product(p, 2) for p in products]
+
+ benefit = models.FixedPriceBenefit(range=range, type="FixedPrice", value=Decimal('20.00'))
+ self.assertEquals(Decimal('2.00'), benefit.apply(basket, condition))
+ self.assertEquals(Decimal('12.00'), benefit.apply(basket, condition))
+ self.assertEquals(Decimal('0.00'), benefit.apply(basket, condition))
+
+ def test_correct_discount_is_returned(self):
+ products = [create_product(Decimal('8.00')), create_product(Decimal('4.00'))]
+ range = models.Range.objects.create(name="Dummy range")
+ for product in products:
+ range.included_products.add(product)
+ range.included_products.add(product)
+
+ basket = Basket.objects.create()
+ [basket.add_product(p) for p in products]
+
+ condition = models.CoverageCondition(range=range, type="Coverage", value=2)
+ discount = self.benefit.apply(basket, condition)
+ self.assertEquals(Decimal('2.00'), discount)
+
+ def test_no_discount_when_product_not_discountable(self):
+ product = create_product(Decimal('18.00'))
+ product.is_discountable = False
+ product.save()
+
+ product_range = models.Range.objects.create(name="Dummy range")
+ product_range.included_products.add(product)
+
+ basket = Basket.objects.create()
+ basket.add_product(product)
+
+ condition = models.CoverageCondition(range=product_range, type="Coverage", value=1)
+ discount = self.benefit.apply(basket, condition)
+ self.assertEquals(Decimal('0.00'), discount)
+
+ def test_no_discount_is_returned_when_value_is_greater_than_product_total(self):
+ products = [create_product(Decimal('4.00')), create_product(Decimal('4.00'))]
+ range = models.Range.objects.create(name="Dummy range")
+ for product in products:
+ range.included_products.add(product)
+ range.included_products.add(product)
+
+ basket = Basket.objects.create()
+ [basket.add_product(p) for p in products]
+
+ condition = models.CoverageCondition(range=range, type="Coverage", value=2)
+ discount = self.benefit.apply(basket, condition)
+ self.assertEquals(Decimal('0.00'), discount)
+
+ def test_discount_when_more_products_than_required(self):
+ products = [create_product(Decimal('4.00')),
+ create_product(Decimal('8.00')),
+ create_product(Decimal('12.00'))]
+
+ # Create range that includes the products
+ range = models.Range.objects.create(name="Dummy range")
+ for product in products:
+ range.included_products.add(product)
+ condition = models.CoverageCondition(range=range, type="Coverage", value=3)
+
+ # Create basket that satisfies condition but with one extra product
+ basket = Basket.objects.create()
+ [basket.add_product(p) for p in products]
+ basket.add_product(products[0])
+
+ benefit = models.FixedPriceBenefit(range=range, type="FixedPrice", value=Decimal('20.00'))
+ discount = benefit.apply(basket, condition)
+ self.assertEquals(Decimal('4.00'), discount)
+
+ def test_discount_when_applied_twice(self):
+ products = [create_product(Decimal('4.00')),
+ create_product(Decimal('8.00')),
+ create_product(Decimal('12.00'))]
+
+ # Create range that includes the products
+ range = models.Range.objects.create(name="Dummy range")
+ for product in products:
+ range.included_products.add(product)
+ condition = models.CoverageCondition(range=range, type="Coverage", value=3)
+
+ # Create basket that satisfies condition but with one extra product
+ basket = Basket.objects.create()
+ [basket.add_product(p, 2) for p in products]
+
+ benefit = models.FixedPriceBenefit(range=range, type="FixedPrice", value=Decimal('20.00'))
+ first_discount = benefit.apply(basket, condition)
+ self.assertEquals(Decimal('4.00'), first_discount)
+ second_discount = benefit.apply(basket, condition)
+ self.assertEquals(Decimal('4.00'), second_discount)
View
215 tests/unit/offer/condition_tests.py
@@ -0,0 +1,215 @@
+from decimal import Decimal
+
+from django.test import TestCase
+
+from oscar.apps.offer import models
+from oscar.apps.basket.models import Basket
+from oscar.test.helpers import create_product
+from tests.unit.offer import OfferTest
+
+
+class CountConditionTest(OfferTest):
+
+ def setUp(self):
+ super(CountConditionTest, self).setUp()
+ self.cond = models.CountCondition(range=self.range, type="Count", value=2)
+
+ def test_empty_basket_fails_condition(self):
+ self.assertFalse(self.cond.is_satisfied(self.basket))
+
+ def test_not_discountable_product_fails_condition(self):
+ prod1, prod2 = create_product(), create_product()
+ prod2.is_discountable = False
+ prod2.save()
+ self.basket.add_product(prod1)
+ self.basket.add_product(prod2)
+ self.assertFalse(self.cond.is_satisfied(self.basket))
+
+ def test_empty_basket_fails_partial_condition(self):
+ self.assertFalse(self.cond.is_partially_satisfied(self.basket))
+
+ def test_smaller_quantity_basket_passes_partial_condition(self):
+ self.basket.add_product(create_product(), 1)
+ self.assertTrue(self.cond.is_partially_satisfied(self.basket))
+
+ def test_smaller_quantity_basket_upsell_message(self):
+ self.basket.add_product(create_product(), 1)
+ self.assertTrue('Buy 1 more product from ' in
+ self.cond.get_upsell_message(self.basket))
+
+ def test_matching_quantity_basket_fails_partial_condition(self):
+ self.basket.add_product(create_product(), 2)
+ self.assertFalse(self.cond.is_partially_satisfied(self.basket))
+
+ def test_matching_quantity_basket_passes_condition(self):
+ self.basket.add_product(create_product(), 2)
+ self.assertTrue(self.cond.is_satisfied(self.basket))
+
+ def test_greater_quantity_basket_passes_condition(self):
+ self.basket.add_product(create_product(), 3)
+ self.assertTrue(self.cond.is_satisfied(self.basket))
+
+ def test_consumption(self):
+ self.basket.add_product(create_product(), 3)
+ self.cond.consume_items(self.basket)
+ self.assertEquals(1, self.basket.all_lines()[0].quantity_without_discount)
+
+ def test_is_satisfied_accounts_for_consumed_items(self):
+ self.basket.add_product(create_product(), 3)
+ self.cond.consume_items(self.basket)
+ self.assertFalse(self.cond.is_satisfied(self.basket))
+
+ def test_count_condition_is_applied_multpile_times(self):
+ benefit = models.AbsoluteDiscountBenefit(range=self.range, type="Absolute", value=Decimal('10.00'))
+ for i in range(10):
+ self.basket.add_product(create_product(price=Decimal('5.00'), upc='upc_%i' % i), 1)
+ product_range = models.Range.objects.create(name="All products", includes_all_products=True)
+ condition = models.CountCondition(range=product_range, type="Count", value=2)
+
+ first_discount = benefit.apply(self.basket, condition=condition)
+ self.assertEquals(Decimal('10.00'), first_discount)
+
+ second_discount = benefit.apply(self.basket, condition=condition)
+ self.assertEquals(Decimal('10.00'), second_discount)
+
+
+class ValueConditionTest(OfferTest):
+ def setUp(self):
+ super(ValueConditionTest, self).setUp()
+ self.cond = models.ValueCondition(range=self.range, type="Value", value=Decimal('10.00'))
+ self.item = create_product(price=Decimal('5.00'))
+ self.expensive_item = create_product(price=Decimal('15.00'))
+
+ def test_empty_basket_fails_condition(self):
+ self.assertFalse(self.cond.is_satisfied(self.basket))
+
+ def test_empty_basket_fails_partial_condition(self):
+ self.assertFalse(self.cond.is_partially_satisfied(self.basket))
+
+ def test_less_value_basket_fails_condition(self):
+ self.basket.add_product(self.item, 1)
+ self.assertFalse(self.cond.is_satisfied(self.basket))
+
+ def test_not_discountable_item_fails_condition(self):
+ self.expensive_item.is_discountable = False
+ self.expensive_item.save()
+ self.basket.add_product(self.expensive_item, 1)
+ self.assertFalse(self.cond.is_satisfied(self.basket))
+
+ def test_upsell_message(self):
+ self.basket.add_product(self.item, 1)
+ self.assertTrue('Spend' in self.cond.get_upsell_message(self.basket))
+
+ def test_matching_basket_fails_partial_condition(self):
+ self.basket.add_product(self.item, 2)
+ self.assertFalse(self.cond.is_partially_satisfied(self.basket))
+
+ def test_less_value_basket_passes_partial_condition(self):
+ self.basket.add_product(self.item, 1)
+ self.assertTrue(self.cond.is_partially_satisfied(self.basket))
+
+ def test_matching_basket_passes_condition(self):
+ self.basket.add_product(self.item, 2)
+ self.assertTrue(self.cond.is_satisfied(self.basket))
+
+ def test_greater_than_basket_passes_condition(self):
+ self.basket.add_product(self.item, 3)
+ self.assertTrue(self.cond.is_satisfied(self.basket))
+
+ def test_consumption(self):
+ self.basket.add_product(self.item, 3)
+ self.cond.consume_items(self.basket)
+ self.assertEquals(1, self.basket.all_lines()[0].quantity_without_discount)
+
+ def test_consumption_with_high_value_product(self):
+ self.basket.add_product(self.expensive_item, 1)
+ self.cond.consume_items(self.basket)
+ self.assertEquals(0, self.basket.all_lines()[0].quantity_without_discount)
+
+ def test_is_consumed_respects_quantity_consumed(self):
+ self.basket.add_product(self.expensive_item, 1)
+ self.assertTrue(self.cond.is_satisfied(self.basket))
+ self.cond.consume_items(self.basket)
+ self.assertFalse(self.cond.is_satisfied(self.basket))
+
+
+class CoverageConditionTest(TestCase):
+
+ def setUp(self):
+ self.products = [create_product(Decimal('5.00')), create_product(Decimal('10.00'))]
+ self.range = models.Range.objects.create(name="Some products")
+ for product in self.products:
+ self.range.included_products.add(product)
+ self.range.included_products.add(product)
+
+ self.basket = Basket.objects.create()
+ self.cond = models.CoverageCondition(range=self.range, type="Coverage", value=2)
+
+ def test_empty_basket_fails(self):
+ self.assertFalse(self.cond.is_satisfied(self.basket))
+
+ def test_empty_basket_fails_partial_condition(self):
+ self.assertFalse(self.cond.is_partially_satisfied(self.basket))
+
+ def test_single_item_fails(self):
+ self.basket.add_product(self.products[0])
+ self.assertFalse(self.cond.is_satisfied(self.basket))
+
+ def test_not_discountable_item_fails(self):
+ self.products[0].is_discountable = False
+ self.products[0].save()
+ self.basket.add_product(self.products[0])
+ self.basket.add_product(self.products[1])
+ self.assertFalse(self.cond.is_satisfied(self.basket))
+
+ def test_single_item_passes_partial_condition(self):
+ self.basket.add_product(self.products[0])
+ self.assertTrue(self.cond.is_partially_satisfied(self.basket))
+
+ def test_upsell_message(self):
+ self.basket.add_product(self.products[0])
+ self.assertTrue('Buy 1 more' in self.cond.get_upsell_message(self.basket))
+
+ def test_duplicate_item_fails(self):
+ self.basket.add_product(self.products[0])
+ self.basket.add_product(self.products[0])
+ self.assertFalse(self.cond.is_satisfied(self.basket))
+
+ def test_duplicate_item_passes_partial_condition(self):
+ self.basket.add_product(self.products[0], 2)
+ self.assertTrue(self.cond.is_partially_satisfied(self.basket))
+
+ def test_covering_items_pass(self):
+ self.basket.add_product(self.products[0])
+ self.basket.add_product(self.products[1])
+ self.assertTrue(self.cond.is_satisfied(self.basket))
+
+ def test_covering_items_fail_partial_condition(self):
+ self.basket.add_product(self.products[0])
+ self.basket.add_product(self.products[1])
+ self.assertFalse(self.cond.is_partially_satisfied(self.basket))
+
+ def test_covering_items_are_consumed(self):
+ self.basket.add_product(self.products[0])
+ self.basket.add_product(self.products[1])
+ self.cond.consume_items(self.basket)
+ self.assertEquals(0, self.basket.num_items_without_discount)
+
+ def test_consumed_items_checks_affected_items(self):
+ # Create new offer
+ range = models.Range.objects.create(name="All products", includes_all_products=True)
+ cond = models.CoverageCondition(range=range, type="Coverage", value=2)
+
+ # Get 4 distinct products in the basket
+ self.products.extend([create_product(Decimal('15.00')), create_product(Decimal('20.00'))])
+
+ for product in self.products:
+ self.basket.add_product(product)
+
+ self.assertTrue(cond.is_satisfied(self.basket))
+ cond.consume_items(self.basket)
+ self.assertEquals(2, self.basket.num_items_without_discount)
+
+ self.assertTrue(cond.is_satisfied(self.basket))
+ cond.consume_items(self.basket)
+ self.assertEquals(0, self.basket.num_items_without_discount)
View
89 tests/unit/offer/offer_tests.py
@@ -0,0 +1,89 @@
+import datetime
+
+from django.conf import settings
+from django.test import TestCase
+
+from oscar.apps.offer import models
+from oscar.test.helpers import create_product
+
+
+class WholeSiteRangeWithGlobalBlacklistTest(TestCase):
+
+ def setUp(self):
+ self.range = models.Range.objects.create(name="All products", includes_all_products=True)
+
+ def tearDown(self):
+ settings.OSCAR_OFFER_BLACKLIST_PRODUCT = None
+
+ def test_blacklisting_prevents_products_being_in_range(self):
+ settings.OSCAR_OFFER_BLACKLIST_PRODUCT = lambda p: True
+ prod = create_product()
+ self.assertFalse(self.range.contains_product(prod))
+
+ def test_blacklisting_can_use_product_class(self):
+ settings.OSCAR_OFFER_BLACKLIST_PRODUCT = lambda p: p.product_class.name == 'giftcard'
+ prod = create_product(product_class="giftcard")
+ self.assertFalse(self.range.contains_product(prod))
+
+ def test_blacklisting_doesnt_exlude_everything(self):
+ settings.OSCAR_OFFER_BLACKLIST_PRODUCT = lambda p: p.product_class.name == 'giftcard'
+ prod = create_product(product_class="book")
+ self.assertTrue(self.range.contains_product(prod))
+
+
+class WholeSiteRangeTest(TestCase):
+
+ def setUp(self):
+ self.range = models.Range.objects.create(name="All products", includes_all_products=True)
+ self.prod = create_product()
+
+ def test_all_products_range(self):
+ self.assertTrue(self.range.contains_product(self.prod))
+
+ def test_all_products_range_with_exception(self):
+ self.range.excluded_products.add(self.prod)
+ self.assertFalse(self.range.contains_product(self.prod))
+
+ def test_whitelisting(self):
+ self.range.included_products.add(self.prod)
+ self.assertTrue(self.range.contains_product(self.prod))
+
+ def test_blacklisting(self):
+ self.range.excluded_products.add(self.prod)
+ self.assertFalse(self.range.contains_product(self.prod))
+
+
+class PartialRangeTest(TestCase):
+
+ def setUp(self):
+ self.range = models.Range.objects.create(name="All products", includes_all_products=False)
+ self.prod = create_product()
+
+ def test_empty_list(self):
+ self.assertFalse(self.range.contains_product(self.prod))
+
+ def test_included_classes(self):
+ self.range.classes.add(self.prod.product_class)
+ self.assertTrue(self.range.contains_product(self.prod))
+
+ def test_included_class_with_exception(self):
+ self.range.classes.add(self.prod.product_class)
+ self.range.excluded_products.add(self.prod)
+ self.assertFalse(self.range.contains_product(self.prod))
+
+
+class ConditionalOfferTest(TestCase):
+
+ def test_is_active(self):
+ start = datetime.date(2011, 01, 01)
+ test = datetime.date(2011, 01, 10)
+ end = datetime.date(2011, 02, 01)
+ offer = models.ConditionalOffer(start_date=start, end_date=end)
+ self.assertTrue(offer.is_active(test))
+
+ def test_is_inactive(self):
+ start = datetime.date(2011, 01, 01)
+ test = datetime.date(2011, 03, 10)
+ end = datetime.date(2011, 02, 01)
+ offer = models.ConditionalOffer(start_date=start, end_date=end)
+ self.assertFalse(offer.is_active(test))
View
656 tests/unit/offer_tests.py
@@ -1,656 +0,0 @@
-from decimal import Decimal
-import datetime
-
-from django.conf import settings
-from django.test import TestCase
-
-from oscar.apps.offer.models import (Range, CountCondition, ValueCondition,
- CoverageCondition, ConditionalOffer,
- PercentageDiscountBenefit, FixedPriceBenefit,
- MultibuyDiscountBenefit, AbsoluteDiscountBenefit)
-from oscar.apps.basket.models import Basket
-from oscar.test.helpers import create_product
-
-
-class WholeSiteRangeWithGlobalBlacklistTest(TestCase):
-
- def setUp(self):
- self.range = Range.objects.create(name="All products", includes_all_products=True)
-
- def tearDown(self):
- settings.OSCAR_OFFER_BLACKLIST_PRODUCT = None
-
- def test_blacklisting_prevents_products_being_in_range(self):
- settings.OSCAR_OFFER_BLACKLIST_PRODUCT = lambda p: True
- prod = create_product()
- self.assertFalse(self.range.contains_product(prod))
-
- def test_blacklisting_can_use_product_class(self):
- settings.OSCAR_OFFER_BLACKLIST_PRODUCT = lambda p: p.product_class.name == 'giftcard'
- prod = create_product(product_class="giftcard")
- self.assertFalse(self.range.contains_product(prod))
-
- def test_blacklisting_doesnt_exlude_everything(self):
- settings.OSCAR_OFFER_BLACKLIST_PRODUCT = lambda p: p.product_class.name == 'giftcard'
- prod = create_product(product_class="book")
- self.assertTrue(self.range.contains_product(prod))
-
-
-class WholeSiteRangeTest(TestCase):
-
- def setUp(self):
- self.range = Range.objects.create(name="All products", includes_all_products=True)
- self.prod = create_product()
-
- def test_all_products_range(self):
- self.assertTrue(self.range.contains_product(self.prod))
-
- def test_all_products_range_with_exception(self):
- self.range.excluded_products.add(self.prod)
- self.assertFalse(self.range.contains_product(self.prod))
-
- def test_whitelisting(self):
- self.range.included_products.add(self.prod)
- self.assertTrue(self.range.contains_product(self.prod))
-
- def test_blacklisting(self):
- self.range.excluded_products.add(self.prod)
- self.assertFalse(self.range.contains_product(self.prod))
-
-
-class PartialRangeTest(TestCase):
-
- def setUp(self):
- self.range = Range.objects.create(name="All products", includes_all_products=False)
- self.prod = create_product()
-
- def test_empty_list(self):
- self.assertFalse(self.range.contains_product(self.prod))
-
- def test_included_classes(self):
- self.range.classes.add(self.prod.product_class)
- self.assertTrue(self.range.contains_product(self.prod))
-
- def test_included_class_with_exception(self):
- self.range.classes.add(self.prod.product_class)
- self.range.excluded_products.add(self.prod)
- self.assertFalse(self.range.contains_product(self.prod))
-
-
-class OfferTest(TestCase):
- def setUp(self):
- self.range = Range.objects.create(name="All products range", includes_all_products=True)
- self.basket = Basket.objects.create()
-
-
-class CountConditionTest(OfferTest):
-
- def setUp(self):
- super(CountConditionTest, self).setUp()
- self.cond = CountCondition(range=self.range, type="Count", value=2)
-
- def test_empty_basket_fails_condition(self):
- self.assertFalse(self.cond.is_satisfied(self.basket))
-
- def test_not_discountable_product_fails_condition(self):
- prod1, prod2 = create_product(), create_product()
- prod2.is_discountable = False
- prod2.save()
- self.basket.add_product(prod1)
- self.basket.add_product(prod2)
- self.assertFalse(self.cond.is_satisfied(self.basket))
-
- def test_empty_basket_fails_partial_condition(self):
- self.assertFalse(self.cond.is_partially_satisfied(self.basket))
-
- def test_smaller_quantity_basket_passes_partial_condition(self):
- self.basket.add_product(create_product(), 1)
- self.assertTrue(self.cond.is_partially_satisfied(self.basket))
-
- def test_smaller_quantity_basket_upsell_message(self):
- self.basket.add_product(create_product(), 1)
- self.assertTrue('Buy 1 more product from ' in
- self.cond.get_upsell_message(self.basket))
-
- def test_matching_quantity_basket_fails_partial_condition(self):
- self.basket.add_product(create_product(), 2)
- self.assertFalse(self.cond.is_partially_satisfied(self.basket))
-
- def test_matching_quantity_basket_passes_condition(self):
- self.basket.add_product(create_product(), 2)
- self.assertTrue(self.cond.is_satisfied(self.basket))
-
- def test_greater_quantity_basket_passes_condition(self):
- self.basket.add_product(create_product(), 3)
- self.assertTrue(self.cond.is_satisfied(self.basket))
-
- def test_consumption(self):
- self.basket.add_product(create_product(), 3)
- self.cond.consume_items(self.basket)
- self.assertEquals(1, self.basket.all_lines()[0].quantity_without_discount)
-
- def test_is_satisfied_accounts_for_consumed_items(self):
- self.basket.add_product(create_product(), 3)
- self.cond.consume_items(self.basket)
- self.assertFalse(self.cond.is_satisfied(self.basket))
-
- def test_count_condition_is_applied_multpile_times(self):
- benefit = AbsoluteDiscountBenefit(range=self.range, type="Absolute", value=Decimal('10.00'))
- for i in range(10):
- self.basket.add_product(create_product(price=Decimal('5.00'), upc='upc_%i' % i), 1)
- product_range = Range.objects.create(name="All products", includes_all_products=True)
- condition = CountCondition(range=product_range, type="Count", value=2)
-
- first_discount = benefit.apply(self.basket, condition=condition)
- self.assertEquals(Decimal('10.00'), first_discount)
-
- second_discount = benefit.apply(self.basket, condition=condition)
- self.assertEquals(Decimal('10.00'), second_discount)
-
-
-class ValueConditionTest(OfferTest):
- def setUp(self):
- super(ValueConditionTest, self).setUp()
- self.cond = ValueCondition(range=self.range, type="Value", value=Decimal('10.00'))
- self.item = create_product(price=Decimal('5.00'))
- self.expensive_item = create_product(price=Decimal('15.00'))
-
- def test_empty_basket_fails_condition(self):
- self.assertFalse(self.cond.is_satisfied(self.basket))
-
- def test_empty_basket_fails_partial_condition(self):
- self.assertFalse(self.cond.is_partially_satisfied(self.basket))
-
- def test_less_value_basket_fails_condition(self):
- self.basket.add_product(self.item, 1)
- self.assertFalse(self.cond.is_satisfied(self.basket))
-
- def test_not_discountable_item_fails_condition(self):
- self.expensive_item.is_discountable = False
- self.expensive_item.save()
- self.basket.add_product(self.expensive_item, 1)
- self.assertFalse(self.cond.is_satisfied(self.basket))
-
- def test_upsell_message(self):
- self.basket.add_product(self.item, 1)
- self.assertTrue('Spend' in self.cond.get_upsell_message(self.basket))
-
- def test_matching_basket_fails_partial_condition(self):
- self.basket.add_product(self.item, 2)
- self.assertFalse(self.cond.is_partially_satisfied(self.basket))
-
- def test_less_value_basket_passes_partial_condition(self):
- self.basket.add_product(self.item, 1)
- self.assertTrue(self.cond.is_partially_satisfied(self.basket))
-
- def test_matching_basket_passes_condition(self):
- self.basket.add_product(self.item, 2)
- self.assertTrue(self.cond.is_satisfied(self.basket))
-
- def test_greater_than_basket_passes_condition(self):
- self.basket.add_product(self.item, 3)
- self.assertTrue(self.cond.is_satisfied(self.basket))
-
- def test_consumption(self):
- self.basket.add_product(self.item, 3)
- self.cond.consume_items(self.basket)
- self.assertEquals(1, self.basket.all_lines()[0].quantity_without_discount)
-
- def test_consumption_with_high_value_product(self):
- self.basket.add_product(self.expensive_item, 1)
- self.cond.consume_items(self.basket)
- self.assertEquals(0, self.basket.all_lines()[0].quantity_without_discount)
-
- def test_is_consumed_respects_quantity_consumed(self):
- self.basket.add_product(self.expensive_item, 1)
- self.assertTrue(self.cond.is_satisfied(self.basket))
- self.cond.consume_items(self.basket)
- self.assertFalse(self.cond.is_satisfied(self.basket))
-
-
-class CoverageConditionTest(TestCase):
-
- def setUp(self):
- self.products = [create_product(Decimal('5.00')), create_product(Decimal('10.00'))]
- self.range = Range.objects.create(name="Some products")
- for product in self.products:
- self.range.included_products.add(product)
- self.range.included_products.add(product)
-
- self.basket = Basket.objects.create()
- self.cond = CoverageCondition(range=self.range, type="Coverage", value=2)
-
- def test_empty_basket_fails(self):
- self.assertFalse(self.cond.is_satisfied(self.basket))
-
- def test_empty_basket_fails_partial_condition(self):
- self.assertFalse(self.cond.is_partially_satisfied(self.basket))
-
- def test_single_item_fails(self):
- self.basket.add_product(self.products[0])
- self.assertFalse(self.cond.is_satisfied(self.basket))
-
- def test_not_discountable_item_fails(self):
- self.products[0].is_discountable = False
- self.products[0].save()
- self.basket.add_product(self.products[0])
- self.basket.add_product(self.products[1])
- self.assertFalse(self.cond.is_satisfied(self.basket))
-
- def test_single_item_passes_partial_condition(self):
- self.basket.add_product(self.products[0])
- self.assertTrue(self.cond.is_partially_satisfied(self.basket))
-
- def test_upsell_message(self):
- self.basket.add_product(self.products[0])
- self.assertTrue('Buy 1 more' in self.cond.get_upsell_message(self.basket))
-
- def test_duplicate_item_fails(self):
- self.basket.add_product(self.products[0])
- self.basket.add_product(self.products[0])
- self.assertFalse(self.cond.is_satisfied(self.basket))
-
- def test_duplicate_item_passes_partial_condition(self):
- self.basket.add_product(self.products[0], 2)
- self.assertTrue(self.cond.is_partially_satisfied(self.basket))
-
- def test_covering_items_pass(self):
- self.basket.add_product(self.products[0])
- self.basket.add_product(self.products[1])
- self.assertTrue(self.cond.is_satisfied(self.basket))
-
- def test_covering_items_fail_partial_condition(self):
- self.basket.add_product(self.products[0])
- self.basket.add_product(self.products[1])
- self.assertFalse(self.cond.is_partially_satisfied(self.basket))
-
- def test_covering_items_are_consumed(self):
- self.basket.add_product(self.products[0])
- self.basket.add_product(self.products[1])
- self.cond.consume_items(self.basket)
- self.assertEquals(0, self.basket.num_items_without_discount)
-
- def test_consumed_items_checks_affected_items(self):
- # Create new offer
- range = Range.objects.create(name="All products", includes_all_products=True)
- cond = CoverageCondition(range=range, type="Coverage", value=2)
-
- # Get 4 distinct products in the basket
- self.products.extend([create_product(Decimal('15.00')), create_product(Decimal('20.00'))])
-
- for product in self.products:
- self.basket.add_product(product)
-
- self.assertTrue(cond.is_satisfied(self.basket))
- cond.consume_items(self.basket)
- self.assertEquals(2, self.basket.num_items_without_discount)
-
- self.assertTrue(cond.is_satisfied(self.basket))
- cond.consume_items(self.basket)
- self.assertEquals(0, self.basket.num_items_without_discount)
-
-
-class PercentageDiscountBenefitTest(OfferTest):
-
- def setUp(self):
- super(PercentageDiscountBenefitTest, self).setUp()
- self.benefit = PercentageDiscountBenefit(range=self.range, type="Percentage", value=Decimal('15.00'))
- self.item = create_product(price=Decimal('5.00'))
- self.original_offer_rounding_function = getattr(settings, 'OSCAR_OFFER_ROUNDING_FUNCTION', None)
- if self.original_offer_rounding_function is not None:
- delattr(settings, 'OSCAR_OFFER_ROUNDING_FUNCTION')
-
- def tearDown(self):
- super(PercentageDiscountBenefitTest, self).tearDown()
- if self.original_offer_rounding_function is not None:
- settings.OSCAR_OFFER_ROUNDING_FUNCTION = self.original_offer_rounding_function
-
- def test_no_discount_for_empty_basket(self):
- self.assertEquals(Decimal('0.00'), self.benefit.apply(self.basket))
-
- def test_no_discount_for_not_discountable_product(self):
- self.item.is_discountable = False
- self.item.save()
- self.basket.add_product(self.item, 1)
- self.assertEquals(Decimal('0.00'), self.benefit.apply(self.basket))
-
- def test_discount_for_single_item_basket(self):
- self.basket.add_product(self.item, 1)
- self.assertEquals(Decimal('0.15') * Decimal('5.00'), self.benefit.apply(self.basket))
-
- def test_discount_for_multi_item_basket(self):
- self.basket.add_product(self.item, 3)
- self.assertEquals(Decimal('3') * Decimal('0.15') * Decimal('5.00'), self.benefit.apply(self.basket))
-
- def test_discount_for_multi_item_basket_with_max_affected_items_set(self):
- self.basket.add_product(self.item, 3)
- self.benefit.max_affected_items = 1
- self.assertEquals(Decimal('0.15') * Decimal('5.00'), self.benefit.apply(self.basket))
-
- def test_discount_can_only_be_applied_once(self):
- self.basket.add_product(self.item, 3)
- first_discount = self.benefit.apply(self.basket)
- second_discount = self.benefit.apply(self.basket)
- self.assertEquals(Decimal('0.00'), second_discount)
-
- def test_discount_can_be_applied_several_times_when_max_is_set(self):
- self.basket.add_product(self.item, 3)
- self.benefit.max_affected_items = 1
- for i in range(1, 4):
- self.assertTrue(self.benefit.apply(self.basket) > 0)
-
-
-class AbsoluteDiscountBenefitTest(OfferTest):
-
- def setUp(self):
- super(AbsoluteDiscountBenefitTest, self).setUp()
- self.benefit = AbsoluteDiscountBenefit(range=self.range, type="Absolute", value=Decimal('10.00'))
- self.item = create_product(price=Decimal('5.00'))
- self.original_offer_rounding_function = getattr(settings, 'OSCAR_OFFER_ROUNDING_FUNCTION', None)
- if self.original_offer_rounding_function is not None:
- delattr(settings, 'OSCAR_OFFER_ROUNDING_FUNCTION')
-
- def tearDown(self):
- super(AbsoluteDiscountBenefitTest, self).tearDown()
- if self.original_offer_rounding_function is not None:
- settings.OSCAR_OFFER_ROUNDING_FUNCTION = self.original_offer_rounding_function
-
- def test_no_discount_for_empty_basket(self):
- self.assertEquals(Decimal('0.00'), self.benefit.apply(self.basket))
-
- def test_no_discount_for_not_discountable_product(self):
- self.item.is_discountable = False
- self.item.save()
- self.basket.add_product(self.item, 1)
- self.assertEquals(Decimal('0.00'), self.benefit.apply(self.basket))
-
- def test_discount_for_single_item_basket(self):
- self.basket.add_product(self.item, 1)
- self.assertEquals(Decimal('5.00'), self.benefit.apply(self.basket))
-
- def test_discount_for_multi_item_basket(self):
- self.basket.add_product(self.item, 3)
- self.assertEquals(Decimal('10.00'), self.benefit.apply(self.basket))
-
- def test_discount_for_multi_item_basket_with_max_affected_items_set(self):
- self.basket.add_product(self.item, 3)
- self.benefit.max_affected_items = 1
- self.assertEquals(Decimal('5.00'), self.benefit.apply(self.basket))
-
- def test_discount_can_only_be_applied_once(self):
- # Add 3 items to make total 15.00
- self.basket.add_product(self.item, 3)
- first_discount = self.benefit.apply(self.basket)
- self.assertEquals(Decimal('10.00'), first_discount)
-
- second_discount = self.benefit.apply(self.basket)
- self.assertEquals(Decimal('5.00'), second_discount)
-
- def test_absolute_does_not_consume_twice(self):
- product = create_product(Decimal('25000'))
- rng = Range.objects.create(name='Dummy')
- rng.included_products.add(product)
- condition = ValueCondition(range=rng, type='Value', value=Decimal('5000'))
- basket = Basket.objects.create()
- basket.add_product(product, 5)
- benefit = AbsoluteDiscountBenefit(range=rng, type='Absolute', value=Decimal('100'))
- self.assertTrue(condition.is_satisfied(basket))
- self.assertEquals(Decimal('100'), benefit.apply(basket, condition))
- self.assertEquals(Decimal('100'), benefit.apply(basket, condition))
- self.assertEquals(Decimal('100'), benefit.apply(basket, condition))
- self.assertEquals(Decimal('100'), benefit.apply(basket, condition))
- self.assertEquals(Decimal('100'), benefit.apply(basket, condition))
- self.assertEquals(Decimal('0'), benefit.apply(basket, condition))
-
-
-class MultibuyDiscountBenefitTest(OfferTest):
-
- def setUp(self):
- super(MultibuyDiscountBenefitTest, self).setUp()
- self.benefit = MultibuyDiscountBenefit(range=self.range, type="Multibuy", value=1)
- self.item = create_product(price=Decimal('5.00'))
-
- def test_no_discount_for_empty_basket(self):
- self.assertEquals(Decimal('0.00'), self.benefit.apply(self.basket))
-
- def test_discount_for_single_item_basket(self):
- self.basket.add_product(self.item, 1)
- self.assertEquals(Decimal('5.00'), self.benefit.apply(self.basket))
-
- def test_discount_for_multi_item_basket(self):
- self.basket.add_product(self.item, 3)
- self.assertEquals(Decimal('5.00'), self.benefit.apply(self.basket))
-
- def test_no_discount_for_not_discountable_product(self):
- self.item.is_discountable = False
- self.item.save()
- self.basket.add_product(self.item, 1)
- self.assertEquals(Decimal('0.00'), self.benefit.apply(self.basket))
-
- def test_discount_does_not_consume_item_if_in_condition_range(self):
- self.basket.add_product(self.item, 1)
- first_discount = self.benefit.apply(self.basket)
- self.assertEquals(Decimal('5.00'), first_discount)
- second_discount = self.benefit.apply(self.basket)
- self.assertEquals(Decimal('5.00'), second_discount)
-
- def test_product_does_consume_item_if_not_in_condition_range(self):
- # Set up condition using a different range from benefit
- range = Range.objects.create(name="Small range")
- other_product = create_product(price=Decimal('15.00'))
- range.included_products.add(other_product)
- cond = ValueCondition(range=range, type="Value", value=Decimal('10.00'))
-
- self.basket.add_product(self.item, 1)
- self.benefit.apply(self.basket, cond)
- line = self.basket.all_lines()[0]
- self.assertEqual(line.quantity_without_discount, 0)
-
- def test_condition_consumes_most_expensive_lines_first(self):
- for i in range(10, 0, -1):
- product = create_product(price=Decimal(i), title='%i'%i, upc='upc_%i' % i)
- self.basket.add_product(product, 1)
-
- condition = CountCondition(range=self.range, type="Count", value=2)
-
- self.assertTrue(condition.is_satisfied(self.basket))
- # consume 1 and 10
- first_discount = self.benefit.apply(self.basket, condition=condition)
- self.assertEquals(Decimal('1.00'), first_discount)
-
- self.assertTrue(condition.is_satisfied(self.basket))
- # consume 2 and 9
- second_discount = self.benefit.apply(self.basket, condition=condition)
- self.assertEquals(Decimal('2.00'), second_discount)
-
- self.assertTrue(condition.is_satisfied(self.basket))
- # consume 3 and 8
- third_discount = self.benefit.apply(self.basket, condition=condition)
- self.assertEquals(Decimal('3.00'), third_discount)
-
- self.assertTrue(condition.is_satisfied(self.basket))
- # consume 4 and 7
- fourth_discount = self.benefit.apply(self.basket, condition=condition)
- self.assertEquals(Decimal('4.00'), fourth_discount)
-
- self.assertTrue(condition.is_satisfied(self.basket))
- # consume 5 and 6
- fifth_discount = self.benefit.apply(self.basket, condition=condition)
- self.assertEquals(Decimal('5.00'), fifth_discount)
-
- # end of items (one not discounted item in basket)
- self.assertFalse(condition.is_satisfied(self.basket))
-
- def test_condition_consumes_most_expensive_lines_first_when_products_are_repeated(self):
- for i in range(5, 0, -1):
- product = create_product(price=Decimal(i), title='%i'%i, upc='upc_%i' % i)
- self.basket.add_product(product, 2)
-
- condition = CountCondition(range=self.range, type="Count", value=2)
-
- # initial basket: [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]
- self.assertTrue(condition.is_satisfied(self.basket))
- # consume 1 and 5
- first_discount = self.benefit.apply(self.basket, condition=condition)
- self.assertEquals(Decimal('1.00'), first_discount)
-
- self.assertTrue(condition.is_satisfied(self.basket))
- # consume 1 and 5
- second_discount = self.benefit.apply(self.basket, condition=condition)
- self.assertEquals(Decimal('1.00'), second_discount)
-
- self.assertTrue(condition.is_satisfied(self.basket))
- # consume 2 and 4
- third_discount = self.benefit.apply(self.basket, condition=condition)
- self.assertEquals(Decimal('2.00'), third_discount)
-
- self.assertTrue(condition.is_satisfied(self.basket))
- # consume 2 and 4
- third_discount = self.benefit.apply(self.basket, condition=condition)
- self.assertEquals(Decimal('2.00'), third_discount)
-
- self.assertTrue(condition.is_satisfied(self.basket))
- # consume 3 and 3
- third_discount = self.benefit.apply(self.basket, condition=condition)
- self.assertEquals(Decimal('3.00'), third_discount)
-
- # end of items (one not discounted item in basket)
- self.assertFalse(condition.is_satisfied(self.basket))
-
- def test_products_with_no_stockrecord_are_handled_ok(self):
- self.basket.add_product(self.item, 3)
- self.basket.add_product(create_product())
- condition = CountCondition(range=self.range, type="Count", value=3)
- self.benefit.apply(self.basket, condition)
-
-
-class FixedPriceBenefitTest(OfferTest):
-
- def setUp(self):
- super(FixedPriceBenefitTest, self).setUp()
- self.benefit = FixedPriceBenefit(range=self.range, type="FixedPrice", value=Decimal('10.00'))
-
- def test_correct_discount_for_count_condition(self):
- products = [create_product(Decimal('7.00')),
- create_product(Decimal('8.00')),
- create_product(Decimal('12.00'))]
-
- # Create range that includes the products
- range = Range.objects.create(name="Dummy range")
- for product in products:
- range.included_products.add(product)
- condition = CountCondition(range=range, type="Count", value=3)
-
- # Create basket that satisfies condition but with one extra product
- basket = Basket.objects.create()
- [basket.add_product(p, 2) for p in products]
-
- benefit = FixedPriceBenefit(range=range, type="FixedPrice", value=Decimal('20.00'))
- self.assertEquals(Decimal('2.00'), benefit.apply(basket, condition))
- self.assertEquals(Decimal('12.00'), benefit.apply(basket, condition))
- self.assertEquals(Decimal('0.00'), benefit.apply(basket, condition))
-
- def test_correct_discount_is_returned(self):
- products = [create_product(Decimal('8.00')), create_product(Decimal('4.00'))]
- range = Range.objects.create(name="Dummy range")
- for product in products:
- range.included_products.add(product)
- range.included_products.add(product)
-
- basket = Basket.objects.create()
- [basket.add_product(p) for p in products]
-
- condition = CoverageCondition(range=range, type="Coverage", value=2)
- discount = self.benefit.apply(basket, condition)
- self.assertEquals(Decimal('2.00'), discount)
-
- def test_no_discount_when_product_not_discountable(self):
- product = create_product(Decimal('18.00'))
- product.is_discountable = False
- product.save()
-
- product_range = Range.objects.create(name="Dummy range")
- product_range.included_products.add(product)
-
- basket = Basket.objects.create()
- basket.add_product(product)
-
- condition = CoverageCondition(range=product_range, type="Coverage", value=1)
- discount = self.benefit.apply(basket, condition)
- self.assertEquals(Decimal('0.00'), discount)
-
- def test_no_discount_is_returned_when_value_is_greater_than_product_total(self):
- products = [create_product(Decimal('4.00')), create_product(Decimal('4.00'))]
- range = Range.objects.create(name="Dummy range")
- for product in products:
- range.included_products.add(product)
- range.included_products.add(product)
-
- basket = Basket.objects.create()
- [basket.add_product(p) for p in products]
-
- condition = CoverageCondition(range=range, type="Coverage", value=2)
- discount = self.benefit.apply(basket, condition)
- self.assertEquals(Decimal('0.00'), discount)
-
- def test_discount_when_more_products_than_required(self):
- products = [create_product(Decimal('4.00')),
- create_product(Decimal('8.00')),
- create_product(Decimal('12.00'))]
-
- # Create range that includes the products
- range = Range.objects.create(name="Dummy range")