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.

views.py 29KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707
  1. import six
  2. import logging
  3. from django.http import Http404, HttpResponseRedirect, HttpResponseBadRequest
  4. from django.core.urlresolvers import reverse, reverse_lazy
  5. from django.contrib import messages
  6. from django.contrib.auth import login
  7. from oscar.core.loading import get_model
  8. from django.utils.translation import ugettext as _
  9. from django.views.generic import (DetailView, TemplateView, FormView,
  10. DeleteView, UpdateView)
  11. from oscar.apps.shipping.methods import NoShippingRequired
  12. from oscar.core.loading import get_class, get_classes
  13. ShippingAddressForm, GatewayForm \
  14. = get_classes('checkout.forms', ['ShippingAddressForm', 'GatewayForm'])
  15. pre_payment, post_payment \
  16. = get_classes('checkout.signals', ['pre_payment', 'post_payment'])
  17. OrderNumberGenerator, OrderCreator \
  18. = get_classes('order.utils', ['OrderNumberGenerator', 'OrderCreator'])
  19. UserAddressForm = get_class('address.forms', 'UserAddressForm')
  20. Repository = get_class('shipping.repository', 'Repository')
  21. AccountAuthView = get_class('customer.views', 'AccountAuthView')
  22. RedirectRequired, UnableToTakePayment, PaymentError \
  23. = get_classes('payment.exceptions', ['RedirectRequired',
  24. 'UnableToTakePayment',
  25. 'PaymentError'])
  26. UnableToPlaceOrder = get_class('order.exceptions', 'UnableToPlaceOrder')
  27. OrderPlacementMixin = get_class('checkout.mixins', 'OrderPlacementMixin')
  28. CheckoutSessionMixin = get_class('checkout.session', 'CheckoutSessionMixin')
  29. Order = get_model('order', 'Order')
  30. ShippingAddress = get_model('order', 'ShippingAddress')
  31. CommunicationEvent = get_model('order', 'CommunicationEvent')
  32. PaymentEventType = get_model('order', 'PaymentEventType')
  33. PaymentEvent = get_model('order', 'PaymentEvent')
  34. UserAddress = get_model('address', 'UserAddress')
  35. Basket = get_model('basket', 'Basket')
  36. Email = get_model('customer', 'Email')
  37. CommunicationEventType = get_model('customer', 'CommunicationEventType')
  38. # Standard logger for checkout events
  39. logger = logging.getLogger('oscar.checkout')
  40. class IndexView(CheckoutSessionMixin, FormView):
  41. """
  42. First page of the checkout. We prompt user to either sign in, or
  43. to proceed as a guest (where we still collect their email address).
  44. """
  45. template_name = 'checkout/gateway.html'
  46. form_class = GatewayForm
  47. success_url = reverse_lazy('checkout:shipping-address')
  48. def get(self, request, *args, **kwargs):
  49. # We redirect immediately to shipping address stage if the user is
  50. # signed in
  51. if request.user.is_authenticated():
  52. return self.get_success_response()
  53. return super(IndexView, self).get(request, *args, **kwargs)
  54. def get_form_kwargs(self):
  55. kwargs = super(IndexView, self).get_form_kwargs()
  56. email = self.checkout_session.get_guest_email()
  57. if email:
  58. kwargs['initial'] = {
  59. 'username': email,
  60. }
  61. return kwargs
  62. def form_valid(self, form):
  63. if form.is_guest_checkout() or form.is_new_account_checkout():
  64. email = form.cleaned_data['username']
  65. self.checkout_session.set_guest_email(email)
  66. if form.is_new_account_checkout():
  67. messages.info(
  68. self.request,
  69. _("Create your account and then you will be redirected "
  70. "back to the checkout process"))
  71. self.success_url = "%s?next=%s&email=%s" % (
  72. reverse('customer:register'),
  73. reverse('checkout:shipping-address'),
  74. email
  75. )
  76. else:
  77. user = form.get_user()
  78. login(self.request, user)
  79. return self.get_success_response()
  80. def get_success_response(self):
  81. return HttpResponseRedirect(self.get_success_url())
  82. def get_success_url(self):
  83. return self.success_url
  84. # ================
  85. # SHIPPING ADDRESS
  86. # ================
  87. class ShippingAddressView(CheckoutSessionMixin, FormView):
  88. """
  89. Determine the shipping address for the order.
  90. The default behaviour is to display a list of addresses from the users's
  91. address book, from which the user can choose one to be their shipping
  92. address. They can add/edit/delete these USER addresses. This address will
  93. be automatically converted into a SHIPPING address when the user checks
  94. out.
  95. Alternatively, the user can enter a SHIPPING address directly which will be
  96. saved in the session and later saved as ShippingAddress model when the
  97. order is sucessfully submitted.
  98. """
  99. template_name = 'checkout/shipping_address.html'
  100. form_class = ShippingAddressForm
  101. def get(self, request, *args, **kwargs):
  102. # Check that the user's basket is not empty
  103. if request.basket.is_empty:
  104. messages.error(request, _("You need to add some items to your"
  105. " basket to checkout"))
  106. return HttpResponseRedirect(reverse('basket:summary'))
  107. # Check that guests have entered an email address
  108. if not request.user.is_authenticated() \
  109. and not self.checkout_session.get_guest_email():
  110. messages.error(request, _("Please either sign in or enter your"
  111. " email address"))
  112. return HttpResponseRedirect(reverse('checkout:index'))
  113. # Check to see that a shipping address is actually required. It may
  114. # not be if the basket is purely downloads
  115. if not request.basket.is_shipping_required():
  116. messages.info(request, _("Your basket does not require a shipping"
  117. " address to be submitted"))
  118. return HttpResponseRedirect(self.get_success_url())
  119. return super(ShippingAddressView, self).get(request, *args, **kwargs)
  120. def get_initial(self):
  121. return self.checkout_session.new_shipping_address_fields()
  122. def get_context_data(self, **kwargs):
  123. kwargs = super(ShippingAddressView, self).get_context_data(**kwargs)
  124. if self.request.user.is_authenticated():
  125. # Look up address book data
  126. kwargs['addresses'] = self.get_available_addresses()
  127. return kwargs
  128. def get_available_addresses(self):
  129. return self.request.user.addresses.filter(
  130. country__is_shipping_country=True).order_by(
  131. '-is_default_for_shipping')
  132. def post(self, request, *args, **kwargs):
  133. # Check if a shipping address was selected directly (eg no form was
  134. # filled in)
  135. if self.request.user.is_authenticated() \
  136. and 'address_id' in self.request.POST:
  137. address = UserAddress._default_manager.get(
  138. pk=self.request.POST['address_id'], user=self.request.user)
  139. action = self.request.POST.get('action', None)
  140. if action == 'ship_to':
  141. # User has selected a previous address to ship to
  142. self.checkout_session.ship_to_user_address(address)
  143. return HttpResponseRedirect(self.get_success_url())
  144. elif action == 'delete':
  145. # Delete the selected address
  146. address.delete()
  147. messages.info(self.request, _("Address deleted from your"
  148. " address book"))
  149. return HttpResponseRedirect(
  150. reverse('checkout:shipping-method'))
  151. else:
  152. return HttpResponseBadRequest()
  153. else:
  154. return super(ShippingAddressView, self).post(
  155. request, *args, **kwargs)
  156. def form_valid(self, form):
  157. # Store the address details in the session and redirect to next step
  158. address_fields = dict(
  159. (k, v) for (k, v) in form.instance.__dict__.items()
  160. if not k.startswith('_'))
  161. self.checkout_session.ship_to_new_address(address_fields)
  162. return super(ShippingAddressView, self).form_valid(form)
  163. def get_success_url(self):
  164. return reverse('checkout:shipping-method')
  165. class UserAddressUpdateView(CheckoutSessionMixin, UpdateView):
  166. """
  167. Update a user address
  168. """
  169. template_name = 'checkout/user_address_form.html'
  170. form_class = UserAddressForm
  171. def get_queryset(self):
  172. return self.request.user.addresses.all()
  173. def get_form_kwargs(self):
  174. kwargs = super(UserAddressUpdateView, self).get_form_kwargs()
  175. kwargs['user'] = self.request.user
  176. return kwargs
  177. def get_success_url(self):
  178. messages.info(self.request, _("Address saved"))
  179. return reverse('checkout:shipping-address')
  180. class UserAddressDeleteView(CheckoutSessionMixin, DeleteView):
  181. """
  182. Delete an address from a user's addressbook.
  183. """
  184. template_name = 'checkout/user_address_delete.html'
  185. def get_queryset(self):
  186. return self.request.user.addresses.all()
  187. def get_success_url(self):
  188. messages.info(self.request, _("Address deleted"))
  189. return reverse('checkout:shipping-address')
  190. # ===============
  191. # Shipping method
  192. # ===============
  193. class ShippingMethodView(CheckoutSessionMixin, TemplateView):
  194. """
  195. View for allowing a user to choose a shipping method.
  196. Shipping methods are largely domain-specific and so this view
  197. will commonly need to be subclassed and customised.
  198. The default behaviour is to load all the available shipping methods
  199. using the shipping Repository. If there is only 1, then it is
  200. automatically selected. Otherwise, a page is rendered where
  201. the user can choose the appropriate one.
  202. """
  203. template_name = 'checkout/shipping_methods.html'
  204. def get(self, request, *args, **kwargs):
  205. # Check that the user's basket is not empty
  206. if request.basket.is_empty:
  207. messages.error(request, _("You need to add some items to your"
  208. " basket to checkout"))
  209. return HttpResponseRedirect(reverse('basket:summary'))
  210. # Check that shipping is required at all
  211. if not request.basket.is_shipping_required():
  212. self.checkout_session.use_shipping_method(
  213. NoShippingRequired().code)
  214. return self.get_success_response()
  215. # Check that shipping address has been completed
  216. if not self.checkout_session.is_shipping_address_set():
  217. messages.error(request, _("Please choose a shipping address"))
  218. return HttpResponseRedirect(reverse('checkout:shipping-address'))
  219. # Save shipping methods as instance var as we need them both here
  220. # and when setting the context vars.
  221. self._methods = self.get_available_shipping_methods()
  222. if len(self._methods) == 0:
  223. # No shipping methods available for given address
  224. messages.warning(request, _("Shipping is unavailable for your"
  225. " chosen address - please choose"
  226. " another"))
  227. return HttpResponseRedirect(reverse('checkout:shipping-address'))
  228. elif len(self._methods) == 1:
  229. # Only one shipping method - set this and redirect onto the next
  230. # step
  231. self.checkout_session.use_shipping_method(self._methods[0].code)
  232. return self.get_success_response()
  233. # Must be more than one available shipping method, we present them to
  234. # the user to make a choice.
  235. return super(ShippingMethodView, self).get(request, *args, **kwargs)
  236. def get_context_data(self, **kwargs):
  237. kwargs = super(ShippingMethodView, self).get_context_data(**kwargs)
  238. kwargs['methods'] = self._methods
  239. return kwargs
  240. def get_available_shipping_methods(self):
  241. """
  242. Returns all applicable shipping method objects
  243. for a given basket.
  244. """
  245. # Shipping methods can depend on the user, the contents of the basket
  246. # and the shipping address. I haven't come across a scenario that
  247. # doesn't fit this system.
  248. return Repository().get_shipping_methods(
  249. user=self.request.user, basket=self.request.basket,
  250. shipping_addr=self.get_shipping_address(self.request.basket),
  251. request=self.request)
  252. def post(self, request, *args, **kwargs):
  253. # Need to check that this code is valid for this user
  254. method_code = request.POST.get('method_code', None)
  255. is_valid = False
  256. for method in self.get_available_shipping_methods():
  257. if method.code == method_code:
  258. is_valid = True
  259. if not is_valid:
  260. messages.error(request, _("Your submitted shipping method is not"
  261. " permitted"))
  262. return HttpResponseRedirect(reverse('checkout:shipping-method'))
  263. # Save the code for the chosen shipping method in the session
  264. # and continue to the next step.
  265. self.checkout_session.use_shipping_method(method_code)
  266. return self.get_success_response()
  267. def get_success_response(self):
  268. return HttpResponseRedirect(reverse('checkout:payment-method'))
  269. # ==============
  270. # Payment method
  271. # ==============
  272. class PaymentMethodView(CheckoutSessionMixin, TemplateView):
  273. """
  274. View for a user to choose which payment method(s) they want to use.
  275. This would include setting allocations if payment is to be split
  276. between multiple sources.
  277. """
  278. def get(self, request, *args, **kwargs):
  279. # Check that the user's basket is not empty
  280. if request.basket.is_empty:
  281. messages.error(request, _("You need to add some items to your"
  282. " basket to checkout"))
  283. return HttpResponseRedirect(reverse('basket:summary'))
  284. shipping_required = request.basket.is_shipping_required()
  285. # Check that shipping address has been completed
  286. if shipping_required \
  287. and not self.checkout_session.is_shipping_address_set():
  288. messages.error(request, _("Please choose a shipping address"))
  289. return HttpResponseRedirect(reverse('checkout:shipping-address'))
  290. # Check that shipping method has been set
  291. if shipping_required \
  292. and not self.checkout_session.is_shipping_method_set(
  293. self.request.basket):
  294. messages.error(request, _("Please choose a shipping method"))
  295. return HttpResponseRedirect(reverse('checkout:shipping-method'))
  296. return self.get_success_response()
  297. def get_success_response(self):
  298. return HttpResponseRedirect(reverse('checkout:payment-details'))
  299. # ================
  300. # Order submission
  301. # ================
  302. class PaymentDetailsView(OrderPlacementMixin, TemplateView):
  303. """
  304. For taking the details of payment and creating the order
  305. The class is deliberately split into fine-grained methods, responsible for
  306. only one thing. This is to make it easier to subclass and override just
  307. one component of functionality.
  308. All projects will need to subclass and customise this class.
  309. """
  310. template_name = 'checkout/payment_details.html'
  311. template_name_preview = 'checkout/preview.html'
  312. preview = False
  313. def get(self, request, *args, **kwargs):
  314. error_response = self.get_error_response()
  315. if error_response:
  316. return error_response
  317. return super(PaymentDetailsView, self).get(request, *args, **kwargs)
  318. def post(self, request, *args, **kwargs):
  319. """
  320. This method is designed to be overridden by subclasses which will
  321. validate the forms from the payment details page. If the forms are
  322. valid then the method can call submit()
  323. """
  324. error_response = self.get_error_response()
  325. if error_response:
  326. return error_response
  327. if self.preview:
  328. # We use a custom parameter to indicate if this is an attempt to
  329. # place an order. Without this, we assume a payment form is being
  330. # submitted from the payment-details page
  331. if request.POST.get('action', '') == 'place_order':
  332. # We pull together all the things that are needed to place an
  333. # order.
  334. submission = self.build_submission()
  335. return self.submit(**submission)
  336. return self.render_preview(request)
  337. # Posting to payment-details isn't the right thing to do
  338. return self.get(request, *args, **kwargs)
  339. def get_error_response(self):
  340. # Check that the user's basket is not empty
  341. if self.request.basket.is_empty:
  342. messages.error(self.request, _(
  343. "You need to add some items to your basket to checkout"))
  344. return HttpResponseRedirect(reverse('basket:summary'))
  345. if self.request.basket.is_shipping_required():
  346. shipping_address = self.get_shipping_address(
  347. self.request.basket)
  348. shipping_method = self.get_shipping_method(
  349. self.request.basket, shipping_address)
  350. # Check that shipping address has been completed
  351. if not shipping_address:
  352. messages.error(
  353. self.request, _("Please choose a shipping address"))
  354. return HttpResponseRedirect(
  355. reverse('checkout:shipping-address'))
  356. # Check that shipping method has been set
  357. if not shipping_method:
  358. messages.error(
  359. self.request, _("Please choose a shipping method"))
  360. return HttpResponseRedirect(
  361. reverse('checkout:shipping-method'))
  362. def build_submission(self, **kwargs):
  363. """
  364. Return a dict of data to submitted to pay for, and create an order
  365. """
  366. basket = kwargs.get('basket', self.request.basket)
  367. shipping_address = self.get_shipping_address(basket)
  368. shipping_method = self.get_shipping_method(
  369. basket, shipping_address)
  370. total = self.get_order_totals(
  371. basket, shipping_method=shipping_method)
  372. submission = {
  373. 'user': self.request.user,
  374. 'basket': basket,
  375. 'shipping_address': shipping_address,
  376. 'shipping_method': shipping_method,
  377. 'order_total': total,
  378. 'order_kwargs': {},
  379. 'payment_kwargs': {}}
  380. if not submission['user'].is_authenticated():
  381. email = self.checkout_session.get_guest_email()
  382. submission['order_kwargs']['guest_email'] = email
  383. return submission
  384. def get_context_data(self, **kwargs):
  385. # Use the proposed submission as template context data. Flatten the
  386. # order kwargs so they are easily available too.
  387. ctx = self.build_submission(**kwargs)
  388. ctx.update(kwargs)
  389. ctx.update(ctx['order_kwargs'])
  390. return ctx
  391. def get_template_names(self):
  392. return [self.template_name_preview] if self.preview else [
  393. self.template_name]
  394. def render_preview(self, request, **kwargs):
  395. """
  396. Show a preview of the order.
  397. If sensitive data was submitted on the payment details page, you will
  398. need to pass it back to the view here so it can be stored in hidden
  399. form inputs. This avoids ever writing the sensitive data to disk.
  400. """
  401. ctx = self.get_context_data()
  402. ctx.update(kwargs)
  403. return self.render_to_response(ctx)
  404. def can_basket_be_submitted(self, basket):
  405. """
  406. Check if the basket is permitted to be submitted as an order
  407. """
  408. strategy = self.request.strategy
  409. for line in basket.all_lines():
  410. result = strategy.fetch_for_product(line.product)
  411. is_permitted, reason = result.availability.is_purchase_permitted(
  412. line.quantity)
  413. if not is_permitted:
  414. return False, reason, reverse('basket:summary')
  415. return True, None, None
  416. def get_default_billing_address(self):
  417. """
  418. Return default billing address for user
  419. This is useful when the payment details view includes a billing address
  420. form - you can use this helper method to prepopulate the form.
  421. Note, this isn't used in core oscar as there is no billing address form
  422. by default.
  423. """
  424. if not self.request.user.is_authenticated():
  425. return None
  426. try:
  427. return self.request.user.addresses.get(is_default_for_billing=True)
  428. except UserAddress.DoesNotExist:
  429. return None
  430. def submit(self, user, basket, shipping_address, shipping_method, # noqa (too complex (10))
  431. order_total, payment_kwargs=None, order_kwargs=None):
  432. """
  433. Submit a basket for order placement.
  434. The process runs as follows:
  435. * Generate an order number
  436. * Freeze the basket so it cannot be modified any more (important when
  437. redirecting the user to another site for payment as it prevents the
  438. basket being manipulated during the payment process).
  439. * Attempt to take payment for the order
  440. - If payment is successful, place the order
  441. - If a redirect is required (eg PayPal, 3DSecure), redirect
  442. - If payment is unsuccessful, show an appropriate error message
  443. :basket: The basket to submit.
  444. :payment_kwargs: Additional kwargs to pass to the handle_payment method
  445. :order_kwargs: Additional kwargs to pass to the place_order method
  446. """
  447. if payment_kwargs is None:
  448. payment_kwargs = {}
  449. if order_kwargs is None:
  450. order_kwargs = {}
  451. # Taxes must be known at this point
  452. assert basket.is_tax_known, (
  453. "Basket tax must be set before a user can place an order")
  454. assert shipping_method.is_tax_known, (
  455. "Shipping method tax must be set before a user can place an order")
  456. # Domain-specific checks on the basket
  457. is_valid, reason, url = self.can_basket_be_submitted(basket)
  458. if not is_valid:
  459. messages.error(self.request, reason)
  460. return HttpResponseRedirect(url)
  461. # We generate the order number first as this will be used
  462. # in payment requests (ie before the order model has been
  463. # created). We also save it in the session for multi-stage
  464. # checkouts (eg where we redirect to a 3rd party site and place
  465. # the order on a different request).
  466. order_number = self.generate_order_number(basket)
  467. self.checkout_session.set_order_number(order_number)
  468. logger.info("Order #%s: beginning submission process for basket #%d",
  469. order_number, basket.id)
  470. # Freeze the basket so it cannot be manipulated while the customer is
  471. # completing payment on a 3rd party site. Also, store a reference to
  472. # the basket in the session so that we know which basket to thaw if we
  473. # get an unsuccessful payment response when redirecting to a 3rd party
  474. # site.
  475. self.freeze_basket(basket)
  476. self.checkout_session.set_submitted_basket(basket)
  477. # Handle payment. Any payment problems should be handled by the
  478. # handle_payment method raise an exception, which should be caught
  479. # within handle_POST and the appropriate forms redisplayed.
  480. error_msg = _("A problem occurred while processing payment for this "
  481. "order - no payment has been taken. Please "
  482. "contact customer services if this problem persists")
  483. pre_payment.send_robust(sender=self, view=self)
  484. try:
  485. self.handle_payment(order_number, order_total, **payment_kwargs)
  486. except RedirectRequired as e:
  487. # Redirect required (eg PayPal, 3DS)
  488. logger.info("Order #%s: redirecting to %s", order_number, e.url)
  489. return HttpResponseRedirect(e.url)
  490. except UnableToTakePayment as e:
  491. # Something went wrong with payment but in an anticipated way. Eg
  492. # their bankcard has expired, wrong card number - that kind of
  493. # thing. This type of exception is supposed to set a friendly error
  494. # message that makes sense to the customer.
  495. msg = six.text_type(e)
  496. logger.warning(
  497. "Order #%s: unable to take payment (%s) - restoring basket",
  498. order_number, msg)
  499. self.restore_frozen_basket()
  500. # We re-render the payment details view
  501. self.preview = False
  502. return self.render_to_response(self.get_context_data(error=msg))
  503. except PaymentError as e:
  504. # A general payment error - Something went wrong which wasn't
  505. # anticipated. Eg, the payment gateway is down (it happens), your
  506. # credentials are wrong - that king of thing.
  507. # It makes sense to configure the checkout logger to
  508. # mail admins on an error as this issue warrants some further
  509. # investigation.
  510. msg = six.text_type(e)
  511. logger.error("Order #%s: payment error (%s)", order_number, msg,
  512. exc_info=True)
  513. self.restore_frozen_basket()
  514. self.preview = False
  515. return self.render_to_response(
  516. self.get_context_data(error=error_msg))
  517. except Exception as e:
  518. # Unhandled exception - hopefully, you will only ever see this in
  519. # development.
  520. logger.error(
  521. "Order #%s: unhandled exception while taking payment (%s)",
  522. order_number, e, exc_info=True)
  523. self.restore_frozen_basket()
  524. self.preview = False
  525. return self.render_to_response(
  526. self.get_context_data(error=error_msg))
  527. post_payment.send_robust(sender=self, view=self)
  528. # If all is ok with payment, try and place order
  529. logger.info("Order #%s: payment successful, placing order",
  530. order_number)
  531. try:
  532. return self.handle_order_placement(
  533. order_number, user, basket, shipping_address, shipping_method,
  534. order_total, **order_kwargs)
  535. except UnableToPlaceOrder as e:
  536. # It's possible that something will go wrong while trying to
  537. # actually place an order. Not a good situation to be in as a
  538. # payment transaction may already have taken place, but needs
  539. # to be handled gracefully.
  540. logger.error("Order #%s: unable to place order - %s",
  541. order_number, e, exc_info=True)
  542. msg = six.text_type(e)
  543. self.restore_frozen_basket()
  544. return self.render_to_response(self.get_context_data(error=msg))
  545. def generate_order_number(self, basket):
  546. """
  547. Return a new order number
  548. """
  549. return OrderNumberGenerator().order_number(basket)
  550. def freeze_basket(self, basket):
  551. """
  552. Freeze the basket so it can no longer be modified
  553. """
  554. # We freeze the basket to prevent it being modified once the payment
  555. # process has started. If your payment fails, then the basket will
  556. # need to be "unfrozen". We also store the basket ID in the session
  557. # so the it can be retrieved by multistage checkout processes.
  558. basket.freeze()
  559. def handle_payment(self, order_number, total, **kwargs):
  560. """
  561. Handle any payment processing and record payment sources and events.
  562. This method is designed to be overridden within your project. The
  563. default is to do nothing as payment is domain-specific.
  564. This method is responsible for handling payment and recording the
  565. payment sources (using the add_payment_source method) and payment
  566. events (using add_payment_event) so they can be
  567. linked to the order when it is saved later on.
  568. """
  569. pass
  570. # =========
  571. # Thank you
  572. # =========
  573. class ThankYouView(DetailView):
  574. """
  575. Displays the 'thank you' page which summarises the order just submitted.
  576. """
  577. template_name = 'checkout/thank_you.html'
  578. context_object_name = 'order'
  579. def get_object(self):
  580. # We allow superusers to force an order thankyou page for testing
  581. order = None
  582. if self.request.user.is_superuser:
  583. if 'order_number' in self.request.GET:
  584. order = Order._default_manager.get(
  585. number=self.request.GET['order_number'])
  586. elif 'order_id' in self.request.GET:
  587. order = Order._default_manager.get(
  588. id=self.request.GET['order_id'])
  589. if not order:
  590. if 'checkout_order_id' in self.request.session:
  591. order = Order._default_manager.get(
  592. pk=self.request.session['checkout_order_id'])
  593. else:
  594. raise Http404(_("No order found"))
  595. return order