Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

We’re showing branches in this repository, but 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
123 oscar/apps/offer/models.py
View
@@ -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):
2  oscar/apps/shipping/__init__.py
View
@@ -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
10 tests/unit/offer/__init__.py
View
@@ -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()
362 tests/unit/offer/benefit_tests.py
View
@@ -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)
215 tests/unit/offer/condition_tests.py
View
@@ -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)
89 tests/unit/offer/offer_tests.py
View
@@ -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))
656 tests/unit/offer_tests.py
View
@@ -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")
- for product in products:
- range.included_products.add(product)
- condition = 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 = 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 = Range.objects.create(name="Dummy range")
- for product in products:
- range.included_products.add(product)
- condition = 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 = 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)
-
-
-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 = 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 = ConditionalOffer(start_date=start, end_date=end)
- self.assertFalse(offer.is_active(test))
-
-
-
-
60 tests/unit/shipping_tests.py
View
@@ -16,7 +16,7 @@ class FreeTest(TestCase):
def setUp(self):
self.method = Free()
-
+
def test_shipping_is_free_for_empty_basket(self):
basket = Basket()
self.method.set_basket(basket)
@@ -29,51 +29,51 @@ def test_shipping_is_free_for_nonempty_basket(self):
self.method.set_basket(basket)
self.assertEquals(D('0.00'), self.method.basket_charge_incl_tax())
self.assertEquals(D('0.00'), self.method.basket_charge_excl_tax())
-
-
-class FixedPriceTest(TestCase):
-
+
+
+class FixedPriceTest(TestCase):
+
def test_fixed_price_shipping_charges_for_empty_basket(self):
method = FixedPrice(D('10.00'), D('10.00'))
basket = Basket()
method.set_basket(basket)
self.assertEquals(D('10.00'), method.basket_charge_incl_tax())
self.assertEquals(D('10.00'), method.basket_charge_excl_tax())
-
+
def test_fixed_price_shipping_assumes_no_tax(self):
method = FixedPrice(D('10.00'))
basket = Basket()
method.set_basket(basket)
self.assertEquals(D('10.00'), method.basket_charge_excl_tax())
-
- shipping_values = lambda: [('1.00',),
- ('5.00',),
- ('10.00',),
- ('12.00',)]
-
- @dataProvider(shipping_values)
+
+ shipping_values = lambda: [('1.00',),
+ ('5.00',),
+ ('10.00',),
+ ('12.00',)]
+
+ @dataProvider(shipping_values)
def test_different_values(self, value):
method = FixedPrice(D(value))
basket = Basket()
method.set_basket(basket)
self.assertEquals(D(value), method.basket_charge_excl_tax())
-
-
+
+
class OrderAndItemChargesTests(TestCase):
-