Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

test_creator.py 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. import datetime
  2. import threading
  3. import time
  4. from decimal import Decimal as D
  5. import pytest
  6. from django.contrib.auth.models import AnonymousUser
  7. from django.http import HttpRequest
  8. from django.test import TestCase, TransactionTestCase
  9. from django.test.utils import override_settings
  10. from django.utils import timezone
  11. from oscar.apps.catalogue.models import Product, ProductClass
  12. from oscar.apps.checkout import calculators
  13. from oscar.apps.offer.utils import Applicator
  14. from oscar.apps.offer import models
  15. from oscar.apps.order.models import Order
  16. from oscar.apps.order.utils import OrderCreator
  17. from oscar.apps.shipping.methods import FixedPrice, Free
  18. from oscar.apps.shipping.repository import Repository
  19. from oscar.apps.voucher.models import Voucher
  20. from oscar.core.loading import get_class
  21. from oscar.test import factories
  22. from oscar.test.basket import add_product
  23. from oscar.test.utils import run_concurrently
  24. Range = get_class("offer.models", "Range")
  25. Benefit = get_class("offer.models", "Benefit")
  26. SurchargeApplicator = get_class("checkout.applicator", "SurchargeApplicator")
  27. UK = get_class("partner.strategy", "UK")
  28. def place_order(creator, **kwargs):
  29. """
  30. Helper function to place an order without the boilerplate
  31. """
  32. if "shipping_method" not in kwargs:
  33. kwargs["shipping_method"] = Free()
  34. shipping_charge = kwargs["shipping_method"].calculate(kwargs["basket"])
  35. kwargs["total"] = calculators.OrderTotalCalculator().calculate(
  36. basket=kwargs["basket"],
  37. shipping_charge=shipping_charge,
  38. surcharges=kwargs["surcharges"],
  39. )
  40. kwargs["shipping_charge"] = shipping_charge
  41. return creator.place_order(**kwargs)
  42. class TestOrderCreatorErrorCases(TestCase):
  43. def setUp(self):
  44. self.creator = OrderCreator()
  45. self.basket = factories.create_basket(empty=True)
  46. self.surcharges = SurchargeApplicator().get_applicable_surcharges(self.basket)
  47. def test_raises_exception_when_empty_basket_passed(self):
  48. with self.assertRaises(ValueError):
  49. place_order(self.creator, surcharges=self.surcharges, basket=self.basket)
  50. def test_raises_exception_if_duplicate_order_number_passed(self):
  51. add_product(self.basket, D("12.00"))
  52. place_order(
  53. self.creator,
  54. surcharges=self.surcharges,
  55. basket=self.basket,
  56. order_number="1234",
  57. )
  58. with self.assertRaises(ValueError):
  59. place_order(
  60. self.creator,
  61. surcharges=self.surcharges,
  62. basket=self.basket,
  63. order_number="1234",
  64. )
  65. class TestSuccessfulOrderCreation(TestCase):
  66. def setUp(self):
  67. self.creator = OrderCreator()
  68. self.basket = factories.create_basket(empty=True)
  69. self.surcharges = SurchargeApplicator().get_applicable_surcharges(self.basket)
  70. def test_saves_shipping_code(self):
  71. add_product(self.basket, D("12.00"))
  72. free_method = Free()
  73. order = place_order(
  74. self.creator,
  75. surcharges=self.surcharges,
  76. basket=self.basket,
  77. order_number="1234",
  78. shipping_method=free_method,
  79. )
  80. self.assertEqual(order.shipping_code, free_method.code)
  81. def test_creates_order_and_line_models(self):
  82. add_product(self.basket, D("12.00"))
  83. place_order(
  84. self.creator,
  85. surcharges=self.surcharges,
  86. basket=self.basket,
  87. order_number="1234",
  88. )
  89. order = Order.objects.get(number="1234")
  90. lines = order.lines.all()
  91. self.assertEqual(1, len(lines))
  92. def test_sets_correct_order_status(self):
  93. add_product(self.basket, D("12.00"))
  94. place_order(
  95. self.creator,
  96. surcharges=self.surcharges,
  97. basket=self.basket,
  98. order_number="1234",
  99. status="Active",
  100. )
  101. order = Order.objects.get(number="1234")
  102. self.assertEqual("Active", order.status)
  103. def test_defaults_to_using_free_shipping(self):
  104. add_product(self.basket, D("12.00"))
  105. place_order(
  106. self.creator,
  107. surcharges=self.surcharges,
  108. basket=self.basket,
  109. order_number="1234",
  110. )
  111. order = Order.objects.get(number="1234")
  112. self.assertEqual(
  113. order.total_incl_tax,
  114. self.basket.total_incl_tax + self.surcharges.total.incl_tax,
  115. )
  116. self.assertEqual(
  117. order.total_excl_tax,
  118. self.basket.total_excl_tax + self.surcharges.total.excl_tax,
  119. )
  120. def test_uses_default_order_status_from_settings(self):
  121. add_product(self.basket, D("12.00"))
  122. with override_settings(OSCAR_INITIAL_ORDER_STATUS="A"):
  123. place_order(
  124. self.creator,
  125. surcharges=self.surcharges,
  126. basket=self.basket,
  127. order_number="1234",
  128. )
  129. order = Order.objects.get(number="1234")
  130. self.assertEqual("A", order.status)
  131. def test_uses_default_line_status_from_settings(self):
  132. add_product(self.basket, D("12.00"))
  133. with override_settings(OSCAR_INITIAL_LINE_STATUS="A"):
  134. place_order(
  135. self.creator,
  136. surcharges=self.surcharges,
  137. basket=self.basket,
  138. order_number="1234",
  139. )
  140. order = Order.objects.get(number="1234")
  141. line = order.lines.all()[0]
  142. self.assertEqual("A", line.status)
  143. def test_partner_name_is_optional(self):
  144. for partner_name, order_number in [("", "A"), ("p1", "B")]:
  145. self.basket = factories.create_basket(empty=True)
  146. product = factories.create_product(partner_name=partner_name)
  147. add_product(self.basket, D("12.00"), product=product)
  148. place_order(
  149. self.creator,
  150. surcharges=self.surcharges,
  151. basket=self.basket,
  152. order_number=order_number,
  153. )
  154. line = Order.objects.get(number=order_number).lines.all()[0]
  155. partner = product.stockrecords.all()[0].partner
  156. self.assertTrue(partner_name == line.partner_name == partner.name)
  157. class TestPlacingOrderForDigitalGoods(TestCase):
  158. def setUp(self):
  159. self.creator = OrderCreator()
  160. self.basket = factories.create_basket(empty=True)
  161. self.surcharges = SurchargeApplicator().get_applicable_surcharges(self.basket)
  162. def test_does_not_allocate_stock(self):
  163. ProductClass.objects.create(name="Digital", track_stock=False)
  164. product = factories.create_product(product_class="Digital")
  165. record = factories.create_stockrecord(product, num_in_stock=None)
  166. self.assertTrue(record.num_allocated is None)
  167. add_product(self.basket, D("12.00"), product=product)
  168. place_order(
  169. self.creator,
  170. surcharges=self.surcharges,
  171. basket=self.basket,
  172. order_number="1234",
  173. )
  174. product = Product.objects.get(id=product.id)
  175. stockrecord = product.stockrecords.all()[0]
  176. self.assertTrue(stockrecord.num_in_stock is None)
  177. self.assertTrue(stockrecord.num_allocated is None)
  178. class TestShippingOfferForOrder(TestCase):
  179. def setUp(self):
  180. self.creator = OrderCreator()
  181. self.basket = factories.create_basket(empty=True)
  182. # add the product now so we can calculate the correct surcharges
  183. add_product(self.basket, D("12.00"))
  184. self.surcharges = SurchargeApplicator().get_applicable_surcharges(self.basket)
  185. def apply_20percent_shipping_offer(self):
  186. """Shipping offer 20% off"""
  187. product_range = Range.objects.create(
  188. name="All products range", includes_all_products=True
  189. )
  190. benefit = Benefit.objects.create(
  191. range=product_range, type=Benefit.SHIPPING_PERCENTAGE, value=20
  192. )
  193. offer = factories.create_offer(product_range=product_range, benefit=benefit)
  194. Applicator().apply_offers(self.basket, [offer])
  195. return offer
  196. def test_shipping_offer_is_applied(self):
  197. offer = self.apply_20percent_shipping_offer()
  198. shipping = FixedPrice(D("5.00"), D("5.00"))
  199. shipping = Repository().apply_shipping_offer(self.basket, shipping, offer)
  200. place_order(
  201. self.creator,
  202. surcharges=self.surcharges,
  203. basket=self.basket,
  204. order_number="1234",
  205. shipping_method=shipping,
  206. )
  207. order = Order.objects.get(number="1234")
  208. self.assertEqual(1, len(order.shipping_discounts))
  209. self.assertEqual(D("4.00"), order.shipping_incl_tax)
  210. self.assertEqual(D("38.00"), order.total_incl_tax)
  211. def test_zero_shipping_discount_is_not_created(self):
  212. offer = self.apply_20percent_shipping_offer()
  213. shipping = Free()
  214. shipping = Repository().apply_shipping_offer(self.basket, shipping, offer)
  215. place_order(
  216. self.creator,
  217. surcharges=self.surcharges,
  218. basket=self.basket,
  219. order_number="1234",
  220. shipping_method=shipping,
  221. )
  222. order = Order.objects.get(number="1234")
  223. # No shipping discount
  224. self.assertEqual(0, len(order.shipping_discounts))
  225. self.assertEqual(D("0.00"), order.shipping_incl_tax)
  226. self.assertEqual(D("34.00"), order.total_incl_tax)
  227. class TestOrderOfferCreation(TestCase):
  228. def setUp(self):
  229. self.creator = OrderCreator()
  230. self.basket = factories.create_basket(empty=True)
  231. self.basket.strategy = UK()
  232. self.surcharges = SurchargeApplicator().get_applicable_surcharges(self.basket)
  233. product_range = Range.objects.create(
  234. name="All products range", includes_all_products=True
  235. )
  236. condition = models.CountCondition.objects.create(
  237. range=product_range, type=models.Condition.COUNT, value=1
  238. )
  239. benefit = models.PercentageDiscountBenefit.objects.create(
  240. range=product_range,
  241. type=models.Benefit.PERCENTAGE,
  242. value=20,
  243. )
  244. self.offer = models.ConditionalOffer(
  245. name="Test",
  246. offer_type=models.ConditionalOffer.SITE,
  247. condition=condition,
  248. benefit=benefit,
  249. )
  250. self.offer.save()
  251. self.applicator = Applicator()
  252. def test_multi_lines_discount(self):
  253. add_product(self.basket, D(10))
  254. add_product(self.basket, D(20))
  255. self.applicator.apply_offers(self.basket, [self.offer])
  256. place_order(
  257. self.creator,
  258. surcharges=self.surcharges,
  259. basket=self.basket,
  260. order_number="klatsieknoekie666",
  261. )
  262. order = Order.objects.get(number="klatsieknoekie666")
  263. discount = order.discounts.first()
  264. self.assertEqual(discount.amount, D("7.20"))
  265. self.assertEqual(discount.discount_lines.count(), 2)
  266. self.assertEqual(
  267. discount.amount,
  268. sum([discount.amount for discount in discount.discount_lines.all()]),
  269. )
  270. class TestMultiSiteOrderCreation(TestCase):
  271. def setUp(self):
  272. self.creator = OrderCreator()
  273. self.basket = factories.create_basket(empty=True)
  274. self.site1 = factories.SiteFactory()
  275. self.site2 = factories.SiteFactory()
  276. self.surcharges = SurchargeApplicator().get_applicable_surcharges(self.basket)
  277. def test_default_site(self):
  278. add_product(self.basket, D("12.00"))
  279. place_order(
  280. self.creator,
  281. surcharges=self.surcharges,
  282. basket=self.basket,
  283. order_number="1234",
  284. )
  285. order = Order.objects.get(number="1234")
  286. self.assertEqual(order.site_id, 1)
  287. def test_multi_sites(self):
  288. add_product(self.basket, D("12.00"))
  289. place_order(
  290. self.creator,
  291. surcharges=self.surcharges,
  292. basket=self.basket,
  293. order_number="12345",
  294. site=self.site1,
  295. )
  296. order1 = Order.objects.get(number="12345")
  297. self.assertEqual(order1.site, self.site1)
  298. add_product(self.basket, D("12.00"))
  299. place_order(
  300. self.creator,
  301. surcharges=self.surcharges,
  302. basket=self.basket,
  303. order_number="12346",
  304. site=self.site2,
  305. )
  306. order2 = Order.objects.get(number="12346")
  307. self.assertEqual(order2.site, self.site2)
  308. @override_settings(SITE_ID="")
  309. def test_request(self):
  310. request = HttpRequest()
  311. request.META["SERVER_PORT"] = 80
  312. request.META["SERVER_NAME"] = self.site1.domain
  313. add_product(self.basket, D("12.00"))
  314. place_order(
  315. self.creator,
  316. surcharges=self.surcharges,
  317. basket=self.basket,
  318. order_number="12345",
  319. request=request,
  320. )
  321. order1 = Order.objects.get(number="12345")
  322. self.assertEqual(order1.site, self.site1)
  323. add_product(self.basket, D("12.00"))
  324. request.META["SERVER_NAME"] = self.site2.domain
  325. place_order(
  326. self.creator,
  327. surcharges=self.surcharges,
  328. basket=self.basket,
  329. order_number="12346",
  330. request=request,
  331. )
  332. order2 = Order.objects.get(number="12346")
  333. self.assertEqual(order2.site, self.site2)
  334. class TestPlaceOrderWithVoucher(TestCase):
  335. def test_single_usage(self):
  336. user = AnonymousUser()
  337. basket = factories.create_basket()
  338. creator = OrderCreator()
  339. voucher = factories.VoucherFactory(usage=Voucher.SINGLE_USE)
  340. voucher.offers.add(factories.create_offer(offer_type="Voucher"))
  341. basket.vouchers.add(voucher)
  342. surcharges = SurchargeApplicator().get_applicable_surcharges(basket)
  343. place_order(
  344. creator,
  345. surcharges=surcharges,
  346. basket=basket,
  347. order_number="12346",
  348. user=user,
  349. )
  350. assert voucher.applications.count() == 1
  351. # Make sure the voucher usage is rechecked
  352. with pytest.raises(ValueError):
  353. place_order(
  354. creator,
  355. surcharges=surcharges,
  356. basket=basket,
  357. order_number="12347",
  358. user=user,
  359. )
  360. def test_expired_voucher(self):
  361. user = AnonymousUser()
  362. basket = factories.create_basket()
  363. creator = OrderCreator()
  364. voucher = factories.VoucherFactory(usage=Voucher.SINGLE_USE)
  365. voucher.offers.add(factories.create_offer(offer_type="Voucher"))
  366. basket.vouchers.add(voucher)
  367. voucher.end_datetime = timezone.now() - datetime.timedelta(days=100)
  368. voucher.save()
  369. surcharges = SurchargeApplicator().get_applicable_surcharges(basket)
  370. place_order(
  371. creator,
  372. surcharges=surcharges,
  373. basket=basket,
  374. order_number="12346",
  375. user=user,
  376. )
  377. assert voucher.applications.count() == 0
  378. class TestConcurrentOrderPlacement(TransactionTestCase):
  379. def test_single_usage(self):
  380. user = AnonymousUser()
  381. creator = OrderCreator()
  382. product = factories.ProductFactory(stockrecords__num_in_stock=1000)
  383. # Make the order creator a bit more slow too reliable trigger
  384. # concurrency issues
  385. org_create_order_model = OrderCreator.create_order_model
  386. def new_create_order_model(*args, **kwargs):
  387. time.sleep(0.5)
  388. return org_create_order_model(creator, *args, **kwargs)
  389. creator.create_order_model = new_create_order_model
  390. # Start 5 threads to place an order concurrently
  391. def worker():
  392. order_number = threading.current_thread().name
  393. basket = factories.BasketFactory()
  394. basket.add_product(product)
  395. surcharges = SurchargeApplicator().get_applicable_surcharges(basket)
  396. place_order(
  397. creator,
  398. surcharges=surcharges,
  399. basket=basket,
  400. order_number=order_number,
  401. user=user,
  402. )
  403. exceptions = run_concurrently(worker, num_threads=5)
  404. assert all(isinstance(x, ValueError) for x in exceptions), exceptions
  405. assert len(exceptions) == 0
  406. assert Order.objects.count() == 5
  407. stockrecord = product.stockrecords.first()
  408. assert stockrecord.num_allocated == 5
  409. def test_voucher_single_usage(self):
  410. user = AnonymousUser()
  411. creator = OrderCreator()
  412. product = factories.ProductFactory()
  413. voucher = factories.VoucherFactory(usage=Voucher.SINGLE_USE)
  414. voucher.offers.add(factories.create_offer(offer_type="Voucher"))
  415. # Make the order creator a bit more slow too reliable trigger
  416. # concurrency issues
  417. org_create_order_model = OrderCreator.create_order_model
  418. def new_create_order_model(*args, **kwargs):
  419. time.sleep(0.5)
  420. return org_create_order_model(creator, *args, **kwargs)
  421. creator.create_order_model = new_create_order_model
  422. org_record_voucher_usage = OrderCreator.record_voucher_usage
  423. def record_voucher_usage(*args, **kwargs):
  424. time.sleep(0.5)
  425. return org_record_voucher_usage(creator, *args, **kwargs)
  426. creator.record_voucher_usage = record_voucher_usage
  427. # Start 5 threads to place an order concurrently
  428. def worker():
  429. order_number = threading.current_thread().name
  430. basket = factories.BasketFactory()
  431. basket.add_product(product)
  432. basket.vouchers.add(voucher)
  433. surcharges = SurchargeApplicator().get_applicable_surcharges(basket)
  434. place_order(
  435. creator,
  436. surcharges=surcharges,
  437. basket=basket,
  438. order_number=order_number,
  439. user=user,
  440. )
  441. exceptions = run_concurrently(worker, num_threads=5)
  442. voucher.refresh_from_db()
  443. assert all(isinstance(x, ValueError) for x in exceptions), exceptions
  444. assert len(exceptions) == 4
  445. assert voucher.applications.count() == 1
  446. assert Order.objects.count() == 1