You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

benefit_tests.py 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. from decimal import Decimal as D
  2. from django.conf import settings
  3. from oscar.apps.basket.models import Basket
  4. from oscar.apps.offer import models
  5. from oscar.test.helpers import create_product
  6. from tests.unit.offer import OfferTest
  7. class PercentageDiscountBenefitTest(OfferTest):
  8. def setUp(self):
  9. super(PercentageDiscountBenefitTest, self).setUp()
  10. self.benefit = models.PercentageDiscountBenefit(range=self.range, type="Percentage", value=D('15.00'))
  11. self.item = create_product(price=D('5.00'))
  12. self.original_offer_rounding_function = getattr(settings, 'OSCAR_OFFER_ROUNDING_FUNCTION', None)
  13. if self.original_offer_rounding_function is not None:
  14. delattr(settings, 'OSCAR_OFFER_ROUNDING_FUNCTION')
  15. def tearDown(self):
  16. super(PercentageDiscountBenefitTest, self).tearDown()
  17. if self.original_offer_rounding_function is not None:
  18. settings.OSCAR_OFFER_ROUNDING_FUNCTION = self.original_offer_rounding_function
  19. def test_no_discount_for_empty_basket(self):
  20. self.assertEquals(D('0.00'), self.benefit.apply(self.basket))
  21. def test_no_discount_for_not_discountable_product(self):
  22. self.item.is_discountable = False
  23. self.item.save()
  24. self.basket.add_product(self.item, 1)
  25. self.assertEquals(D('0.00'), self.benefit.apply(self.basket))
  26. def test_discount_for_single_item_basket(self):
  27. self.basket.add_product(self.item, 1)
  28. self.assertEquals(D('0.15') * D('5.00'), self.benefit.apply(self.basket))
  29. def test_discount_for_multi_item_basket(self):
  30. self.basket.add_product(self.item, 3)
  31. self.assertEquals(D('3') * D('0.15') * D('5.00'), self.benefit.apply(self.basket))
  32. def test_discount_for_multi_item_basket_with_max_affected_items_set(self):
  33. self.basket.add_product(self.item, 3)
  34. self.benefit.max_affected_items = 1
  35. self.assertEquals(D('0.15') * D('5.00'), self.benefit.apply(self.basket))
  36. def test_discount_can_only_be_applied_once(self):
  37. self.basket.add_product(self.item, 3)
  38. self.benefit.apply(self.basket)
  39. second_discount = self.benefit.apply(self.basket)
  40. self.assertEquals(D('0.00'), second_discount)
  41. def test_discount_can_be_applied_several_times_when_max_is_set(self):
  42. self.basket.add_product(self.item, 3)
  43. self.benefit.max_affected_items = 1
  44. for i in range(1, 4):
  45. self.assertTrue(self.benefit.apply(self.basket) > 0)
  46. class TestAbsoluteDiscount(OfferTest):
  47. def setUp(self):
  48. super(TestAbsoluteDiscount, self).setUp()
  49. self.benefit = models.AbsoluteDiscountBenefit(
  50. range=self.range, type="Absolute", value=D('10.00'))
  51. self.item = create_product(price=D('5.00'))
  52. self.original_offer_rounding_function = getattr(
  53. settings, 'OSCAR_OFFER_ROUNDING_FUNCTION', None)
  54. if self.original_offer_rounding_function is not None:
  55. delattr(settings, 'OSCAR_OFFER_ROUNDING_FUNCTION')
  56. def tearDown(self):
  57. if self.original_offer_rounding_function is not None:
  58. settings.OSCAR_OFFER_ROUNDING_FUNCTION = self.original_offer_rounding_function
  59. def test_gives_no_discount_for_an_empty_basket(self):
  60. self.assertEquals(D('0.00'), self.benefit.apply(self.basket))
  61. def test_gives_no_discount_for_a_non_discountable_product(self):
  62. product = create_product(price=D('5.00'), is_discountable=False)
  63. self.basket.add_product(product)
  64. self.assertEquals(D('0.00'), self.benefit.apply(self.basket))
  65. def test_gives_correct_discount_for_single_item_basket_cheaper_than_threshold(self):
  66. product = create_product(price=D('5.00'))
  67. self.basket.add_product(product)
  68. self.assertEquals(D('5.00'), self.benefit.apply(self.basket))
  69. def test_gives_correct_discount_for_single_item_basket_equal_to_threshold(self):
  70. product = create_product(price=D('10.00'))
  71. self.basket.add_product(product)
  72. self.assertEquals(D('10.00'), self.benefit.apply(self.basket))
  73. def test_gives_correct_discount_for_single_item_basket_more_expensive_than_threshold(self):
  74. product = create_product(price=D('16.00'))
  75. self.basket.add_product(product)
  76. self.assertEquals(D('10.00'), self.benefit.apply(self.basket))
  77. def test_gives_correct_discount_for_multi_item_basket_cheaper_than_threshold(self):
  78. product = create_product(price=D('2.00'))
  79. self.basket.add_product(product, 3)
  80. self.assertEquals(D('6.00'), self.benefit.apply(self.basket))
  81. def test_gives_correct_discount_for_multi_item_basket_more_expensive_than_threshold(self):
  82. product = create_product(price=D('5.00'))
  83. self.basket.add_product(product, 3)
  84. self.assertEquals(D('10.00'), self.benefit.apply(self.basket))
  85. def test_consumes_all_lines_for_multi_item_basket_cheaper_than_threshold(self):
  86. product = create_product(price=D('2.00'))
  87. self.basket.add_product(product, 3)
  88. self.benefit.apply(self.basket)
  89. for line in self.basket.all_lines():
  90. self.assertTrue(line.has_discount)
  91. self.assertEqual(0, line.quantity_without_discount)
  92. def test_consumes_correct_quantity_for_multi_item_basket_more_expensive_than_threshold(self):
  93. product = create_product(price=D('6.00'))
  94. self.basket.add_product(product, 3)
  95. self.benefit.apply(self.basket)
  96. line = self.basket.all_lines()[0]
  97. self.assertTrue(line.has_discount)
  98. self.assertEqual(1, line.quantity_without_discount)
  99. def test_gives_correct_discount_for_multi_item_basket_with_max_affected_items_set(self):
  100. product = create_product(price=D('5.00'))
  101. self.basket.add_product(product, 3)
  102. self.benefit.max_affected_items = 1
  103. self.assertEquals(D('5.00'), self.benefit.apply(self.basket))
  104. def test_gives_correct_discounts_when_applied_multiple_times(self):
  105. product = create_product(price=D('5.00'))
  106. self.basket.add_product(product, 3)
  107. self.assertEquals(D('10.00'), self.benefit.apply(self.basket))
  108. self.assertEquals(D('5.00'), self.benefit.apply(self.basket))
  109. self.assertEquals(D('0.00'), self.benefit.apply(self.basket))
  110. def test_gives_correct_discounts_when_applied_multiple_times_with_condition(self):
  111. product = create_product(D('25000'))
  112. rng = models.Range.objects.create(name='Dummy')
  113. rng.included_products.add(product)
  114. condition = models.ValueCondition(range=rng, type='Value', value=D('5000'))
  115. self.basket.add_product(product, 5)
  116. benefit = models.AbsoluteDiscountBenefit(range=rng, type='Absolute', value=D('100'))
  117. for _ in range(5):
  118. self.assertTrue(condition.is_satisfied(self.basket))
  119. self.assertEquals(D('100'), benefit.apply(self.basket, condition))
  120. self.assertFalse(condition.is_satisfied(self.basket))
  121. self.assertEquals(D('0'), benefit.apply(self.basket, condition))
  122. def test_consumes_all_products_for_heterogeneous_basket(self):
  123. rng = models.Range.objects.create(name='Dummy')
  124. products = [create_product(D('150')),
  125. create_product(D('300')),
  126. create_product(D('300'))]
  127. for product in products:
  128. rng.included_products.add(product)
  129. condition = models.ValueCondition(range=rng, type='Value', value=D('500'))
  130. basket = Basket.objects.create()
  131. for product in products:
  132. basket.add_product(product)
  133. benefit = models.AbsoluteDiscountBenefit(range=rng, type='Absolute', value=D('100'))
  134. self.assertTrue(condition.is_satisfied(basket))
  135. self.assertEquals(D('100'), benefit.apply(basket, condition))
  136. self.assertEquals(D('0'), benefit.apply(basket, condition))
  137. def test_correctly_discounts_line(self):
  138. product = create_product(D('500'))
  139. rng = models.Range.objects.create(name='Dummy')
  140. rng.included_products.add(product)
  141. condition = models.ValueCondition(range=rng, type='Value', value=D('500'))
  142. basket = Basket.objects.create()
  143. basket.add_product(product, 1)
  144. benefit = models.AbsoluteDiscountBenefit(range=rng, type='Absolute', value=D('100'))
  145. self.assertTrue(condition.is_satisfied(basket))
  146. self.assertEquals(D('100'), benefit.apply(basket, condition))
  147. self.assertEquals(D('100'), basket.all_lines()[0]._discount)
  148. def test_discount_is_applied_to_lines(self):
  149. condition = models.CountCondition.objects.create(
  150. range=self.range, type="Count", value=1)
  151. self.basket.add_product(self.item, 1)
  152. self.benefit.apply(self.basket, condition)
  153. self.assertTrue(self.basket.all_lines()[0].has_discount)
  154. class MultibuyDiscountBenefitTest(OfferTest):
  155. def setUp(self):
  156. super(MultibuyDiscountBenefitTest, self).setUp()
  157. self.benefit = models.MultibuyDiscountBenefit(range=self.range, type="Multibuy", value=1)
  158. self.item = create_product(price=D('5.00'))
  159. def test_no_discount_for_empty_basket(self):
  160. self.assertEquals(D('0.00'), self.benefit.apply(self.basket))
  161. def test_discount_for_single_item_basket(self):
  162. self.basket.add_product(self.item, 1)
  163. self.assertEquals(D('5.00'), self.benefit.apply(self.basket))
  164. def test_discount_for_multi_item_basket(self):
  165. self.basket.add_product(self.item, 3)
  166. self.assertEquals(D('5.00'), self.benefit.apply(self.basket))
  167. def test_no_discount_for_not_discountable_product(self):
  168. self.item.is_discountable = False
  169. self.item.save()
  170. self.basket.add_product(self.item, 1)
  171. self.assertEquals(D('0.00'), self.benefit.apply(self.basket))
  172. def test_discount_does_not_consume_item_if_in_condition_range(self):
  173. self.basket.add_product(self.item, 1)
  174. first_discount = self.benefit.apply(self.basket)
  175. self.assertEquals(D('5.00'), first_discount)
  176. second_discount = self.benefit.apply(self.basket)
  177. self.assertEquals(D('5.00'), second_discount)
  178. def test_product_does_consume_item_if_not_in_condition_range(self):
  179. # Set up condition using a different range from benefit
  180. range = models.Range.objects.create(name="Small range")
  181. other_product = create_product(price=D('15.00'))
  182. range.included_products.add(other_product)
  183. cond = models.ValueCondition(range=range, type="Value", value=D('10.00'))
  184. self.basket.add_product(self.item, 1)
  185. self.benefit.apply(self.basket, cond)
  186. line = self.basket.all_lines()[0]
  187. self.assertEqual(line.quantity_without_discount, 0)
  188. def test_condition_consumes_most_expensive_lines_first(self):
  189. for i in range(10, 0, -1):
  190. product = create_product(price=D(i), title='%i'%i, upc='upc_%i' % i)
  191. self.basket.add_product(product, 1)
  192. condition = models.CountCondition(range=self.range, type="Count", value=2)
  193. self.assertTrue(condition.is_satisfied(self.basket))
  194. # consume 1 and 10
  195. first_discount = self.benefit.apply(self.basket, condition=condition)
  196. self.assertEquals(D('1.00'), first_discount)
  197. self.assertTrue(condition.is_satisfied(self.basket))
  198. # consume 2 and 9
  199. second_discount = self.benefit.apply(self.basket, condition=condition)
  200. self.assertEquals(D('2.00'), second_discount)
  201. self.assertTrue(condition.is_satisfied(self.basket))
  202. # consume 3 and 8
  203. third_discount = self.benefit.apply(self.basket, condition=condition)
  204. self.assertEquals(D('3.00'), third_discount)
  205. self.assertTrue(condition.is_satisfied(self.basket))
  206. # consume 4 and 7
  207. fourth_discount = self.benefit.apply(self.basket, condition=condition)
  208. self.assertEquals(D('4.00'), fourth_discount)
  209. self.assertTrue(condition.is_satisfied(self.basket))
  210. # consume 5 and 6
  211. fifth_discount = self.benefit.apply(self.basket, condition=condition)
  212. self.assertEquals(D('5.00'), fifth_discount)
  213. # end of items (one not discounted item in basket)
  214. self.assertFalse(condition.is_satisfied(self.basket))
  215. def test_condition_consumes_most_expensive_lines_first_when_products_are_repeated(self):
  216. for i in range(5, 0, -1):
  217. product = create_product(price=D(i), title='%i'%i, upc='upc_%i' % i)
  218. self.basket.add_product(product, 2)
  219. condition = models.CountCondition(range=self.range, type="Count", value=2)
  220. # initial basket: [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]
  221. self.assertTrue(condition.is_satisfied(self.basket))
  222. # consume 1 and 5
  223. first_discount = self.benefit.apply(self.basket, condition=condition)
  224. self.assertEquals(D('1.00'), first_discount)
  225. self.assertTrue(condition.is_satisfied(self.basket))
  226. # consume 1 and 5
  227. second_discount = self.benefit.apply(self.basket, condition=condition)
  228. self.assertEquals(D('1.00'), second_discount)
  229. self.assertTrue(condition.is_satisfied(self.basket))
  230. # consume 2 and 4
  231. third_discount = self.benefit.apply(self.basket, condition=condition)
  232. self.assertEquals(D('2.00'), third_discount)
  233. self.assertTrue(condition.is_satisfied(self.basket))
  234. # consume 2 and 4
  235. third_discount = self.benefit.apply(self.basket, condition=condition)
  236. self.assertEquals(D('2.00'), third_discount)
  237. self.assertTrue(condition.is_satisfied(self.basket))
  238. # consume 3 and 3
  239. third_discount = self.benefit.apply(self.basket, condition=condition)
  240. self.assertEquals(D('3.00'), third_discount)
  241. # end of items (one not discounted item in basket)
  242. self.assertFalse(condition.is_satisfied(self.basket))
  243. def test_products_with_no_stockrecord_are_handled_ok(self):
  244. self.basket.add_product(self.item, 3)
  245. self.basket.add_product(create_product())
  246. condition = models.CountCondition(range=self.range, type="Count", value=3)
  247. self.benefit.apply(self.basket, condition)
  248. class FixedPriceBenefitTest(OfferTest):
  249. def setUp(self):
  250. super(FixedPriceBenefitTest, self).setUp()
  251. self.benefit = models.FixedPriceBenefit(range=self.range, type="FixedPrice", value=D('10.00'))
  252. def test_correct_discount_for_count_condition(self):
  253. products = [create_product(D('7.00')),
  254. create_product(D('8.00')),
  255. create_product(D('12.00'))]
  256. # Create range that includes the products
  257. range = models.Range.objects.create(name="Dummy range")
  258. for product in products:
  259. range.included_products.add(product)
  260. condition = models.CountCondition(range=range, type="Count", value=3)
  261. # Create basket that satisfies condition but with one extra product
  262. basket = Basket.objects.create()
  263. [basket.add_product(p, 2) for p in products]
  264. benefit = models.FixedPriceBenefit(range=range, type="FixedPrice", value=D('20.00'))
  265. self.assertEquals(D('2.00'), benefit.apply(basket, condition))
  266. self.assertEquals(D('12.00'), benefit.apply(basket, condition))
  267. self.assertEquals(D('0.00'), benefit.apply(basket, condition))
  268. def test_correct_discount_is_returned(self):
  269. products = [create_product(D('8.00')), create_product(D('4.00'))]
  270. range = models.Range.objects.create(name="Dummy range")
  271. for product in products:
  272. range.included_products.add(product)
  273. range.included_products.add(product)
  274. basket = Basket.objects.create()
  275. [basket.add_product(p) for p in products]
  276. condition = models.CoverageCondition(range=range, type="Coverage", value=2)
  277. discount = self.benefit.apply(basket, condition)
  278. self.assertEquals(D('2.00'), discount)
  279. def test_no_discount_when_product_not_discountable(self):
  280. product = create_product(D('18.00'))
  281. product.is_discountable = False
  282. product.save()
  283. product_range = models.Range.objects.create(name="Dummy range")
  284. product_range.included_products.add(product)
  285. basket = Basket.objects.create()
  286. basket.add_product(product)
  287. condition = models.CoverageCondition(range=product_range, type="Coverage", value=1)
  288. discount = self.benefit.apply(basket, condition)
  289. self.assertEquals(D('0.00'), discount)
  290. def test_no_discount_is_returned_when_value_is_greater_than_product_total(self):
  291. products = [create_product(D('4.00')), create_product(D('4.00'))]
  292. range = models.Range.objects.create(name="Dummy range")
  293. for product in products:
  294. range.included_products.add(product)
  295. range.included_products.add(product)
  296. basket = Basket.objects.create()
  297. [basket.add_product(p) for p in products]
  298. condition = models.CoverageCondition(range=range, type="Coverage", value=2)
  299. discount = self.benefit.apply(basket, condition)
  300. self.assertEquals(D('0.00'), discount)
  301. def test_discount_when_more_products_than_required(self):
  302. products = [create_product(D('4.00')),
  303. create_product(D('8.00')),
  304. create_product(D('12.00'))]
  305. # Create range that includes the products
  306. range = models.Range.objects.create(name="Dummy range")
  307. for product in products:
  308. range.included_products.add(product)
  309. condition = models.CoverageCondition(range=range, type="Coverage", value=3)
  310. # Create basket that satisfies condition but with one extra product
  311. basket = Basket.objects.create()
  312. [basket.add_product(p) for p in products]
  313. basket.add_product(products[0])
  314. benefit = models.FixedPriceBenefit(range=range, type="FixedPrice", value=D('20.00'))
  315. discount = benefit.apply(basket, condition)
  316. self.assertEquals(D('4.00'), discount)
  317. def test_discount_when_applied_twice(self):
  318. products = [create_product(D('4.00')),
  319. create_product(D('8.00')),
  320. create_product(D('12.00'))]
  321. # Create range that includes the products
  322. range = models.Range.objects.create(name="Dummy range")
  323. for product in products:
  324. range.included_products.add(product)
  325. condition = models.CoverageCondition(range=range, type="Coverage", value=3)
  326. # Create basket that satisfies condition but with one extra product
  327. basket = Basket.objects.create()
  328. [basket.add_product(p, 2) for p in products]
  329. benefit = models.FixedPriceBenefit(range=range, type="FixedPrice", value=D('20.00'))
  330. first_discount = benefit.apply(basket, condition)
  331. self.assertEquals(D('4.00'), first_discount)
  332. second_discount = benefit.apply(basket, condition)
  333. self.assertEquals(D('4.00'), second_discount)