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.

test_fixed_unit_benefit.py 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. from decimal import Decimal as D
  2. from unittest import mock
  3. from django.core.exceptions import ValidationError
  4. from django.test import TestCase
  5. from oscar.apps.offer import models
  6. from oscar.apps.offer.utils import Applicator
  7. from oscar.test import factories
  8. from oscar.test.basket import add_product, add_products
  9. class TestFixedUnitDiscountAppliedWithCountConditionOnDifferentRange(TestCase):
  10. def setUp(self):
  11. self.condition_product = factories.ProductFactory()
  12. condition_range = factories.RangeFactory()
  13. condition_range.add_product(self.condition_product)
  14. self.condition = models.CountCondition.objects.create(
  15. range=condition_range, type=models.Condition.COUNT, value=2
  16. )
  17. self.benefit_product = factories.ProductFactory()
  18. benefit_range = factories.RangeFactory()
  19. benefit_range.add_product(self.benefit_product)
  20. self.benefit = models.FixedUnitDiscountBenefit.objects.create(
  21. range=benefit_range, type=models.Benefit.FIXED_UNIT, value=D("3.00")
  22. )
  23. self.offer = models.ConditionalOffer(
  24. id=1, condition=self.condition, benefit=self.benefit
  25. )
  26. self.basket = factories.create_basket(empty=True)
  27. self.applicator = Applicator()
  28. def test_succcessful_application_consumes_correctly(self):
  29. add_product(self.basket, product=self.condition_product, quantity=2)
  30. add_product(self.basket, product=self.benefit_product, quantity=1)
  31. self.applicator.apply_offers(self.basket, [self.offer])
  32. discounts = self.basket.offer_applications.offer_discounts
  33. self.assertEqual(len(discounts), 1)
  34. self.assertEqual(discounts[0]["freq"], 1)
  35. def test_condition_is_consumed_correctly(self):
  36. # Testing an error case reported on the mailing list
  37. add_product(self.basket, product=self.condition_product, quantity=3)
  38. add_product(self.basket, product=self.benefit_product, quantity=2)
  39. self.applicator.apply_offers(self.basket, [self.offer])
  40. discounts = self.basket.offer_applications.offer_discounts
  41. self.assertEqual(len(discounts), 1)
  42. self.assertEqual(discounts[0]["freq"], 1)
  43. class TestFixedUnitDiscountAppliedWithCountCondition(TestCase):
  44. def setUp(self):
  45. product_range = models.Range.objects.create(
  46. name="All products", includes_all_products=True
  47. )
  48. self.condition = models.CountCondition.objects.create(
  49. range=product_range, type=models.Condition.COUNT, value=2
  50. )
  51. self.offer = mock.Mock()
  52. self.benefit = models.FixedUnitDiscountBenefit.objects.create(
  53. range=product_range, type=models.Benefit.FIXED_UNIT, value=D("3.00")
  54. )
  55. self.basket = factories.create_basket(empty=True)
  56. def test_applies_correctly_to_empty_basket(self):
  57. result = self.benefit.apply(self.basket, self.condition, self.offer)
  58. self.assertEqual(D("0.00"), result.discount)
  59. self.assertEqual(0, self.basket.num_items_with_discount)
  60. self.assertEqual(0, self.basket.num_items_without_discount)
  61. def test_applies_correctly_to_basket_which_matches_condition_with_one_line(self):
  62. add_product(self.basket, price=D("12.00"), quantity=2)
  63. result = self.benefit.apply(self.basket, self.condition, self.offer)
  64. self.assertEqual(D("6.00"), result.discount)
  65. self.assertEqual(2, self.basket.num_items_with_discount)
  66. self.assertEqual(0, self.basket.num_items_without_discount)
  67. def test_applies_correctly_to_basket_which_matches_condition_with_multiple_lines(
  68. self,
  69. ):
  70. # Use a basket with 2 lines
  71. add_products(self.basket, [(D("12.00"), 1), (D("12.00"), 1)])
  72. result = self.benefit.apply(self.basket, self.condition, self.offer)
  73. self.assertTrue(result.is_successful)
  74. self.assertFalse(result.is_final)
  75. self.assertEqual(D("6.00"), result.discount)
  76. self.assertEqual(2, self.basket.num_items_with_discount)
  77. self.assertEqual(0, self.basket.num_items_without_discount)
  78. def test_applies_correctly_to_basket_which_matches_condition_with_multiple_lines_and_lower_total_value(
  79. self,
  80. ):
  81. # Use a basket with 2 lines
  82. add_products(self.basket, [(D("1.00"), 1), (D("1.50"), 1)])
  83. result = self.benefit.apply(self.basket, self.condition, self.offer)
  84. self.assertTrue(result.is_successful)
  85. self.assertFalse(result.is_final)
  86. self.assertEqual(D("2.50"), result.discount)
  87. self.assertEqual(2, self.basket.num_items_with_discount)
  88. self.assertEqual(0, self.basket.num_items_without_discount)
  89. def test_applies_correctly_to_basket_which_exceeds_condition(self):
  90. add_products(self.basket, [(D("12.00"), 2), (D("10.00"), 2)])
  91. result = self.benefit.apply(self.basket, self.condition, self.offer)
  92. self.assertEqual(D("12.00"), result.discount)
  93. self.assertEqual(4, self.basket.num_items_with_discount)
  94. self.assertEqual(0, self.basket.num_items_without_discount)
  95. def test_applies_correctly_to_basket_which_exceeds_condition_with_smaller_prices_than_discount(
  96. self,
  97. ):
  98. add_products(self.basket, [(D("2.00"), 2), (D("4.00"), 1)])
  99. result = self.benefit.apply(self.basket, self.condition, self.offer)
  100. self.assertEqual(D("7.00"), result.discount)
  101. self.assertEqual(3, self.basket.num_items_with_discount)
  102. self.assertEqual(0, self.basket.num_items_without_discount)
  103. def test_applies_basket_exceeding_condition_smaller_prices_than_discount_higher_prices_first(
  104. self,
  105. ):
  106. add_products(self.basket, [(D("2.00"), 2), (D("4.00"), 2)])
  107. result = self.benefit.apply(self.basket, self.condition, self.offer)
  108. self.assertEqual(D("10.00"), result.discount)
  109. self.assertEqual(4, self.basket.num_items_with_discount)
  110. self.assertEqual(0, self.basket.num_items_without_discount)
  111. class TestFixedUnitDiscount(TestCase):
  112. def setUp(self):
  113. product_range = models.Range.objects.create(
  114. name="All products", includes_all_products=True
  115. )
  116. self.condition = models.CountCondition.objects.create(
  117. range=product_range, type=models.Condition.COUNT, value=2
  118. )
  119. self.benefit = models.FixedUnitDiscountBenefit.objects.create(
  120. range=product_range, type=models.Benefit.FIXED_UNIT, value=D("4.00")
  121. )
  122. self.offer = mock.Mock()
  123. self.basket = factories.create_basket(empty=True)
  124. def test_applies_correctly_when_discounts_need_rounding(self):
  125. # Split discount across 3 lines
  126. for price in [D("2.00"), D("2.00"), D("2.00")]:
  127. add_product(self.basket, price)
  128. result = self.benefit.apply(self.basket, self.condition, self.offer)
  129. self.assertEqual(D("6.00"), result.discount)
  130. class TestFixedUnitDiscountWithMaxItemsSetAppliedWithCountCondition(TestCase):
  131. def setUp(self):
  132. product_range = models.Range.objects.create(
  133. name="All products", includes_all_products=True
  134. )
  135. self.condition = models.CountCondition.objects.create(
  136. range=product_range, type=models.Condition.COUNT, value=2
  137. )
  138. self.benefit = models.FixedUnitDiscountBenefit.objects.create(
  139. range=product_range,
  140. type=models.Benefit.FIXED_UNIT,
  141. value=D("3.00"),
  142. max_affected_items=1,
  143. )
  144. self.offer = mock.Mock()
  145. self.basket = factories.create_basket(empty=True)
  146. def test_applies_correctly_to_empty_basket(self):
  147. result = self.benefit.apply(self.basket, self.condition, self.offer)
  148. self.assertEqual(D("0.00"), result.discount)
  149. self.assertEqual(0, self.basket.num_items_with_discount)
  150. self.assertEqual(0, self.basket.num_items_without_discount)
  151. def test_applies_correctly_to_basket_which_matches_condition(self):
  152. add_product(self.basket, D("12.00"), 2)
  153. result = self.benefit.apply(self.basket, self.condition, self.offer)
  154. self.assertEqual(D("3.00"), result.discount)
  155. self.assertEqual(1, self.basket.num_items_with_discount)
  156. self.assertEqual(1, self.basket.num_items_without_discount)
  157. def test_applies_correctly_to_basket_which_exceeds_condition(self):
  158. add_products(self.basket, [(D("12.00"), 2), (D("10.00"), 2)])
  159. result = self.benefit.apply(self.basket, self.condition, self.offer)
  160. self.assertEqual(D("3.00"), result.discount)
  161. self.assertEqual(1, self.basket.num_items_with_discount)
  162. self.assertEqual(3, self.basket.num_items_without_discount)
  163. def test_applies_correctly_to_basket_which_exceeds_condition_but_with_smaller_prices_than_discount(
  164. self,
  165. ):
  166. add_products(self.basket, [(D("2.00"), 2), (D("1.00"), 2)])
  167. result = self.benefit.apply(self.basket, self.condition, self.offer)
  168. self.assertEqual(D("1.00"), result.discount)
  169. self.assertEqual(1, self.basket.num_items_with_discount)
  170. self.assertEqual(3, self.basket.num_items_without_discount)
  171. class TestFixedUnitDiscountAppliedWithValueCondition(TestCase):
  172. def setUp(self):
  173. product_range = models.Range.objects.create(
  174. name="All products", includes_all_products=True
  175. )
  176. self.condition = models.ValueCondition.objects.create(
  177. range=product_range, type=models.Condition.VALUE, value=D("10.00")
  178. )
  179. self.benefit = models.FixedUnitDiscountBenefit.objects.create(
  180. range=product_range, type=models.Benefit.FIXED_UNIT, value=D("3.00")
  181. )
  182. self.offer = mock.Mock()
  183. self.basket = factories.create_basket(empty=True)
  184. def test_applies_correctly_to_empty_basket(self):
  185. result = self.benefit.apply(self.basket, self.condition, self.offer)
  186. self.assertEqual(D("0.00"), result.discount)
  187. self.assertEqual(0, self.basket.num_items_with_discount)
  188. self.assertEqual(0, self.basket.num_items_without_discount)
  189. def test_applies_correctly_to_single_item_basket_which_matches_condition(self):
  190. add_products(self.basket, [(D("10.00"), 1)])
  191. result = self.benefit.apply(self.basket, self.condition, self.offer)
  192. self.assertEqual(D("3.00"), result.discount)
  193. self.assertEqual(1, self.basket.num_items_with_discount)
  194. self.assertEqual(0, self.basket.num_items_without_discount)
  195. def test_applies_correctly_to_multi_item_basket_which_matches_condition(self):
  196. add_products(self.basket, [(D("5.00"), 2)])
  197. result = self.benefit.apply(self.basket, self.condition, self.offer)
  198. self.assertEqual(D("6.00"), result.discount)
  199. self.assertEqual(2, self.basket.num_items_with_discount)
  200. self.assertEqual(0, self.basket.num_items_without_discount)
  201. def test_applies_correctly_to_multi_item_basket_which_exceeds_condition(self):
  202. add_products(self.basket, [(D("4.00"), 3)])
  203. result = self.benefit.apply(self.basket, self.condition, self.offer)
  204. self.assertEqual(D("9.00"), result.discount)
  205. self.assertEqual(3, self.basket.num_items_with_discount)
  206. self.assertEqual(0, self.basket.num_items_without_discount)
  207. def test_applies_correctly_to_multi_item_basket_which_exceeds_condition_but_matches_boundary(
  208. self,
  209. ):
  210. add_products(self.basket, [(D("5.00"), 3)])
  211. result = self.benefit.apply(self.basket, self.condition, self.offer)
  212. self.assertEqual(D("9.00"), result.discount)
  213. self.assertEqual(3, self.basket.num_items_with_discount)
  214. self.assertEqual(0, self.basket.num_items_without_discount)
  215. class TestFixedUnitDiscountWithMaxItemsSetAppliedWithValueCondition(TestCase):
  216. def setUp(self):
  217. product_range = models.Range.objects.create(
  218. name="All products", includes_all_products=True
  219. )
  220. self.condition = models.ValueCondition.objects.create(
  221. range=product_range, type=models.Condition.VALUE, value=D("10.00")
  222. )
  223. self.benefit = models.FixedUnitDiscountBenefit.objects.create(
  224. range=product_range,
  225. type=models.Benefit.FIXED_UNIT,
  226. value=D("3.00"),
  227. max_affected_items=1,
  228. )
  229. self.offer = mock.Mock()
  230. self.basket = factories.create_basket(empty=True)
  231. def test_applies_correctly_to_empty_basket(self):
  232. result = self.benefit.apply(self.basket, self.condition, self.offer)
  233. self.assertEqual(D("0.00"), result.discount)
  234. self.assertEqual(0, self.basket.num_items_with_discount)
  235. self.assertEqual(0, self.basket.num_items_without_discount)
  236. def test_applies_correctly_to_single_item_basket_which_matches_condition(self):
  237. add_products(self.basket, [(D("10.00"), 1)])
  238. result = self.benefit.apply(self.basket, self.condition, self.offer)
  239. self.assertEqual(D("3.00"), result.discount)
  240. self.assertEqual(1, self.basket.num_items_with_discount)
  241. self.assertEqual(0, self.basket.num_items_without_discount)
  242. def test_applies_correctly_to_multi_item_basket_which_matches_condition(self):
  243. add_products(self.basket, [(D("5.00"), 2)])
  244. result = self.benefit.apply(self.basket, self.condition, self.offer)
  245. self.assertEqual(D("3.00"), result.discount)
  246. self.assertEqual(1, self.basket.num_items_with_discount)
  247. self.assertEqual(1, self.basket.num_items_without_discount)
  248. def test_applies_correctly_to_multi_item_basket_which_exceeds_condition(self):
  249. add_products(self.basket, [(D("4.00"), 3)])
  250. result = self.benefit.apply(self.basket, self.condition, self.offer)
  251. self.assertEqual(D("3.00"), result.discount)
  252. self.assertEqual(1, self.basket.num_items_with_discount)
  253. self.assertEqual(2, self.basket.num_items_without_discount)
  254. def test_applies_correctly_to_multi_item_basket_which_exceeds_condition_but_matches_boundary(
  255. self,
  256. ):
  257. add_products(self.basket, [(D("5.00"), 3)])
  258. result = self.benefit.apply(self.basket, self.condition, self.offer)
  259. self.assertEqual(D("3.00"), result.discount)
  260. self.assertEqual(1, self.basket.num_items_with_discount)
  261. self.assertEqual(2, self.basket.num_items_without_discount)
  262. def test_applies_correctly_to_multi_item_basket_which_matches_condition_but_with_lower_prices_than_discount(
  263. self,
  264. ):
  265. add_products(self.basket, [(D("2.00"), 6)])
  266. result = self.benefit.apply(self.basket, self.condition, self.offer)
  267. self.assertEqual(D("2.00"), result.discount)
  268. self.assertEqual(1, self.basket.num_items_with_discount)
  269. self.assertEqual(5, self.basket.num_items_without_discount)
  270. class TestFixedUnitDiscountBenefit(TestCase):
  271. def test_requires_a_benefit_value(self):
  272. rng = models.Range.objects.create(name="", includes_all_products=True)
  273. benefit = models.Benefit(type=models.Benefit.FIXED_UNIT, range=rng)
  274. with self.assertRaises(ValidationError):
  275. benefit.clean()
  276. def test_requires_a_range(self):
  277. benefit = models.Benefit(type=models.Benefit.FIXED_UNIT, value=10)
  278. with self.assertRaises(ValidationError):
  279. benefit.clean()
  280. def test_non_negative_basket_lines_values(self):
  281. # absolute product benefit is larger than the line price
  282. rng = models.Range.objects.create(name="", includes_all_products=True)
  283. benefit1 = models.Benefit.objects.create(
  284. type=models.Benefit.FIXED_UNIT, range=rng, value=D("100")
  285. )
  286. benefit2 = models.Benefit.objects.create(
  287. type=models.Benefit.FIXED_UNIT, range=rng, value=D("100")
  288. )
  289. condition = models.ValueCondition.objects.create(
  290. range=rng, type=models.Condition.VALUE, value=D("10")
  291. )
  292. models.ConditionalOffer.objects.create(
  293. name="offer1",
  294. offer_type=models.ConditionalOffer.SITE,
  295. benefit=benefit1,
  296. condition=condition,
  297. exclusive=False,
  298. )
  299. models.ConditionalOffer.objects.create(
  300. name="offer2",
  301. offer_type=models.ConditionalOffer.SITE,
  302. benefit=benefit2,
  303. condition=condition,
  304. exclusive=False,
  305. )
  306. basket = factories.create_basket(empty=True)
  307. add_products(basket, [(D("20"), 1)])
  308. Applicator().apply(basket)
  309. assert len(basket.offer_applications) == 2
  310. line = basket.all_lines().first()
  311. assert line.line_price_excl_tax_incl_discounts == D(0)
  312. assert line.line_price_incl_tax_incl_discounts == D(0)
  313. assert basket.total_incl_tax == 0