Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

views.py 37KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892
  1. import logging
  2. from django.http import Http404, HttpResponseRedirect, HttpResponseBadRequest
  3. from django.core.urlresolvers import reverse
  4. from django.contrib import messages
  5. from django.contrib.sites.models import Site
  6. from django.core.exceptions import ObjectDoesNotExist
  7. from django.contrib.auth import login
  8. from django.db.models import get_model
  9. from django.utils.translation import ugettext as _
  10. from django.views.generic import DetailView, TemplateView, FormView, \
  11. DeleteView, UpdateView, CreateView
  12. from oscar.apps.shipping.methods import Free, NoShippingRequired
  13. from oscar.core.loading import get_class, get_classes
  14. ShippingAddressForm, GatewayForm = get_classes('checkout.forms', ['ShippingAddressForm', 'GatewayForm'])
  15. OrderTotalCalculator = get_class('checkout.calculators', 'OrderTotalCalculator')
  16. CheckoutSessionData = get_class('checkout.utils', 'CheckoutSessionData')
  17. pre_payment, post_payment = get_classes('checkout.signals', ['pre_payment', 'post_payment'])
  18. OrderNumberGenerator, OrderCreator = 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. Dispatcher = get_class('customer.utils', 'Dispatcher')
  23. RedirectRequired, UnableToTakePayment, PaymentError = get_classes(
  24. 'payment.exceptions', ['RedirectRequired', 'UnableToTakePayment', 'PaymentError'])
  25. UnableToPlaceOrder = get_class('order.exceptions', 'UnableToPlaceOrder')
  26. Order = get_model('order', 'Order')
  27. ShippingAddress = get_model('order', 'ShippingAddress')
  28. CommunicationEvent = get_model('order', 'CommunicationEvent')
  29. PaymentEventType = get_model('order', 'PaymentEventType')
  30. PaymentEvent = get_model('order', 'PaymentEvent')
  31. UserAddress = get_model('address', 'UserAddress')
  32. Basket = get_model('basket', 'Basket')
  33. Email = get_model('customer', 'Email')
  34. CommunicationEventType = get_model('customer', 'CommunicationEventType')
  35. # Standard logger for checkout events
  36. logger = logging.getLogger('oscar.checkout')
  37. class CheckoutSessionMixin(object):
  38. """
  39. Mixin to provide common functionality shared between checkout views.
  40. """
  41. def dispatch(self, request, *args, **kwargs):
  42. self.checkout_session = CheckoutSessionData(request)
  43. return super(CheckoutSessionMixin, self).dispatch(request, *args, **kwargs)
  44. def get_shipping_address(self):
  45. """
  46. Return the current shipping address for this checkout session.
  47. This could either be a ShippingAddress model which has been
  48. pre-populated (not saved), or a UserAddress model which will
  49. need converting into a ShippingAddress model at submission
  50. """
  51. addr_data = self.checkout_session.new_shipping_address_fields()
  52. if addr_data:
  53. # Load address data into a blank address model
  54. return ShippingAddress(**addr_data)
  55. addr_id = self.checkout_session.user_address_id()
  56. if addr_id:
  57. try:
  58. return UserAddress._default_manager.get(pk=addr_id)
  59. except UserAddress.DoesNotExist:
  60. # This can happen if you reset all your tables and you still have
  61. # session data that refers to addresses that no longer exist
  62. pass
  63. return None
  64. def get_shipping_method(self, basket=None):
  65. method = self.checkout_session.shipping_method()
  66. if method:
  67. if not basket:
  68. basket = self.request.basket
  69. method.set_basket(basket)
  70. else:
  71. # We default to using free shipping
  72. method = Free()
  73. return method
  74. def get_order_totals(self, basket=None, shipping_method=None, **kwargs):
  75. """
  76. Returns the total for the order with and without tax (as a tuple)
  77. """
  78. calc = OrderTotalCalculator(self.request)
  79. if not basket:
  80. basket = self.request.basket
  81. if not shipping_method:
  82. shipping_method = self.get_shipping_method(basket)
  83. total_incl_tax = calc.order_total_incl_tax(basket, shipping_method, **kwargs)
  84. total_excl_tax = calc.order_total_excl_tax(basket, shipping_method, **kwargs)
  85. return total_incl_tax, total_excl_tax
  86. def get_context_data(self, **kwargs):
  87. """
  88. Assign common template variables to the context.
  89. """
  90. ctx = super(CheckoutSessionMixin, self).get_context_data(**kwargs)
  91. ctx['shipping_address'] = self.get_shipping_address()
  92. method = self.get_shipping_method()
  93. if method:
  94. ctx['shipping_method'] = method
  95. ctx['shipping_total_excl_tax'] = method.basket_charge_excl_tax()
  96. ctx['shipping_total_incl_tax'] = method.basket_charge_incl_tax()
  97. ctx['order_total_incl_tax'], ctx['order_total_excl_tax'] = self.get_order_totals()
  98. return ctx
  99. class IndexView(CheckoutSessionMixin, FormView):
  100. """
  101. First page of the checkout. We prompt user to either sign in, or
  102. to proceed as a guest (where we still collect their email address).
  103. """
  104. template_name = 'checkout/gateway.html'
  105. form_class = GatewayForm
  106. def get(self, request, *args, **kwargs):
  107. # We redirect immediately to shipping address stage if the user is signed in or
  108. # has already filled out the anonymous checkout form.
  109. if request.user.is_authenticated() or self.checkout_session.get_guest_email():
  110. return self.get_success_response()
  111. return super(IndexView, self).get(request, *args, **kwargs)
  112. def get_form_kwargs(self):
  113. kwargs = super(IndexView, self).get_form_kwargs()
  114. email = self.checkout_session.get_guest_email()
  115. if email:
  116. kwargs['initial'] = {
  117. 'username': email,
  118. 'options': 'new'
  119. }
  120. return kwargs
  121. def form_valid(self, form):
  122. if form.is_guest_checkout():
  123. email = form.cleaned_data['username']
  124. self.checkout_session.set_guest_email(email)
  125. else:
  126. user = form.get_user()
  127. login(self.request, user)
  128. return self.get_success_response()
  129. def get_success_response(self):
  130. return HttpResponseRedirect(self.get_success_url())
  131. def get_success_url(self):
  132. return reverse('checkout:shipping-address')
  133. # ================
  134. # SHIPPING ADDRESS
  135. # ================
  136. class ShippingAddressView(CheckoutSessionMixin, FormView):
  137. """
  138. Determine the shipping address for the order.
  139. The default behaviour is to display a list of addresses from the users's
  140. address book, from which the user can choose one to be their shipping address.
  141. They can add/edit/delete these USER addresses. This address will be
  142. automatically converted into a SHIPPING address when the user checks out.
  143. Alternatively, the user can enter a SHIPPING address directly which will be
  144. saved in the session and saved as a model when the order is sucessfully submitted.
  145. """
  146. template_name = 'checkout/shipping_address.html'
  147. form_class = ShippingAddressForm
  148. def get(self, request, *args, **kwargs):
  149. # Check that the user's basket is not empty
  150. if request.basket.is_empty:
  151. messages.error(request, _("You need to add some items to your basket to checkout"))
  152. return HttpResponseRedirect(reverse('basket:summary'))
  153. # Check that guests have entered an email address
  154. if not request.user.is_authenticated() and not self.checkout_session.get_guest_email():
  155. messages.error(request, _("Please either sign in or enter your email address"))
  156. return HttpResponseRedirect(reverse('checkout:index'))
  157. # Check to see that a shipping address is actually required. It may not be if
  158. # the basket is purely downloads
  159. if not request.basket.is_shipping_required():
  160. messages.info(request, _("Your basket does not require a shipping address to be submitted"))
  161. return HttpResponseRedirect(self.get_success_url())
  162. return super(ShippingAddressView, self).get(request, *args, **kwargs)
  163. def get_initial(self):
  164. return self.checkout_session.new_shipping_address_fields()
  165. def get_context_data(self, **kwargs):
  166. kwargs = super(ShippingAddressView, self).get_context_data(**kwargs)
  167. if self.request.user.is_authenticated():
  168. # Look up address book data
  169. kwargs['addresses'] = self.get_available_addresses()
  170. return kwargs
  171. def get_available_addresses(self):
  172. return UserAddress._default_manager.filter(user=self.request.user).order_by('-is_default_for_shipping')
  173. def post(self, request, *args, **kwargs):
  174. # Check if a shipping address was selected directly (eg no form was filled in)
  175. if self.request.user.is_authenticated and 'address_id' in self.request.POST:
  176. address = UserAddress._default_manager.get(pk=self.request.POST['address_id'])
  177. if 'action' in self.request.POST and self.request.POST['action'] == 'ship_to':
  178. # User has selected a previous address to ship to
  179. self.checkout_session.ship_to_user_address(address)
  180. return HttpResponseRedirect(self.get_success_url())
  181. elif 'action' in self.request.POST and self.request.POST['action'] == 'delete':
  182. address.delete()
  183. messages.info(self.request, "Address deleted from your address book")
  184. return HttpResponseRedirect(reverse('checkout:shipping-method'))
  185. else:
  186. return HttpResponseBadRequest()
  187. else:
  188. return super(ShippingAddressView, self).post(request, *args, **kwargs)
  189. def form_valid(self, form):
  190. # Store the address details in the session and redirect to next step
  191. self.checkout_session.ship_to_new_address(form.clean())
  192. return super(ShippingAddressView, self).form_valid(form)
  193. def get_success_url(self):
  194. return reverse('checkout:shipping-method')
  195. class UserAddressCreateView(CheckoutSessionMixin, CreateView):
  196. """
  197. Add a USER address to the user's addressbook.
  198. This is not the same as creating a SHIPPING Address, although if used for the order,
  199. it will be converted into a shipping address at submission-time.
  200. """
  201. template_name = 'checkout/user_address_form.html'
  202. form_class = UserAddressForm
  203. def get_context_data(self, **kwargs):
  204. kwargs = super(UserAddressCreateView, self).get_context_data(**kwargs)
  205. kwargs['form_url'] = reverse('checkout:user-address-create')
  206. return kwargs
  207. def form_valid(self, form):
  208. self.object = form.save(commit=False)
  209. self.object.user = self.request.user
  210. self.object.save()
  211. return self.get_success_response()
  212. def get_success_response(self):
  213. messages.info(self.request, _("Address saved"))
  214. # We redirect back to the shipping address page
  215. return HttpResponseRedirect(reverse('checkout:shipping-address'))
  216. class UserAddressUpdateView(CheckoutSessionMixin, UpdateView):
  217. """
  218. Update a user address
  219. """
  220. template_name = 'checkout/user_address_form.html'
  221. form_class = UserAddressForm
  222. def get_queryset(self):
  223. return UserAddress._default_manager.filter(user=self.request.user)
  224. def get_context_data(self, **kwargs):
  225. kwargs = super(UserAddressUpdateView, self).get_context_data(**kwargs)
  226. kwargs['form_url'] = reverse('checkout:user-address-update', args=(str(kwargs['object'].id),))
  227. return kwargs
  228. def get_success_url(self):
  229. messages.info(self.request, _("Address saved"))
  230. return reverse('checkout:shipping-address')
  231. class UserAddressDeleteView(CheckoutSessionMixin, DeleteView):
  232. """
  233. Delete an address from a user's addressbook.
  234. """
  235. template_name = 'checkout/user_address_delete.html'
  236. def get_queryset(self):
  237. return UserAddress._default_manager.filter(user=self.request.user)
  238. def get_success_url(self):
  239. messages.info(self.request, _("Address deleted"))
  240. return reverse('checkout:shipping-address')
  241. # ===============
  242. # Shipping method
  243. # ===============
  244. class ShippingMethodView(CheckoutSessionMixin, TemplateView):
  245. """
  246. View for allowing a user to choose a shipping method.
  247. Shipping methods are largely domain-specific and so this view
  248. will commonly need to be subclassed and customised.
  249. The default behaviour is to load all the available shipping methods
  250. using the shipping Repository. If there is only 1, then it is
  251. automatically selected. Otherwise, a page is rendered where
  252. the user can choose the appropriate one.
  253. """
  254. template_name = 'checkout/shipping_methods.html';
  255. def get(self, request, *args, **kwargs):
  256. # Check that the user's basket is not empty
  257. if request.basket.is_empty:
  258. messages.error(request, _("You need to add some items to your basket to checkout"))
  259. return HttpResponseRedirect(reverse('basket:summary'))
  260. # Check that shipping is required at all
  261. if not request.basket.is_shipping_required():
  262. self.checkout_session.use_shipping_method(NoShippingRequired().code)
  263. return self.get_success_response()
  264. # Check that shipping address has been completed
  265. if not self.checkout_session.is_shipping_address_set():
  266. messages.error(request, _("Please choose a shipping address"))
  267. return HttpResponseRedirect(reverse('checkout:shipping-address'))
  268. # Save shipping methods as instance var as we need them both here
  269. # and when setting the context vars.
  270. self._methods = self.get_available_shipping_methods()
  271. if len(self._methods) == 0:
  272. # No shipping methods available for given address
  273. messages.warning(request, _("Shipping is not available for your chosen address - please choose another"))
  274. return HttpResponseRedirect(reverse('checkout:shipping-address'))
  275. elif len(self._methods) == 1:
  276. # Only one shipping method - set this and redirect onto the next step
  277. self.checkout_session.use_shipping_method(self._methods[0].code)
  278. return self.get_success_response()
  279. # Must be more than one available shipping method, we present them to
  280. # the user to make a choice.
  281. return super(ShippingMethodView, self).get(request, *args, **kwargs)
  282. def get_context_data(self, **kwargs):
  283. kwargs = super(ShippingMethodView, self).get_context_data(**kwargs)
  284. kwargs['methods'] = self._methods
  285. return kwargs
  286. def get_available_shipping_methods(self):
  287. """
  288. Returns all applicable shipping method objects
  289. for a given basket.
  290. """
  291. # Shipping methods can depend on the user, the contents of the basket
  292. # and the shipping address. I haven't come across a scenario that doesn't
  293. # fit this system.
  294. return Repository().get_shipping_methods(self.request.user, self.request.basket,
  295. self.get_shipping_address())
  296. def post(self, request, *args, **kwargs):
  297. # Need to check that this code is valid for this user
  298. method_code = request.POST.get('method_code', None)
  299. is_valid = False
  300. for method in self.get_available_shipping_methods():
  301. if method.code == method_code:
  302. is_valid = True
  303. if not is_valid:
  304. messages.error(request, _("Your submitted shipping method is not permitted"))
  305. return HttpResponseRedirect(reverse('checkout:shipping-method'))
  306. # Save the code for the chosen shipping method in the session
  307. # and continue to the next step.
  308. self.checkout_session.use_shipping_method(method_code)
  309. return self.get_success_response()
  310. def get_success_response(self):
  311. return HttpResponseRedirect(reverse('checkout:payment-method'))
  312. # ==============
  313. # Payment method
  314. # ==============
  315. class PaymentMethodView(CheckoutSessionMixin, TemplateView):
  316. """
  317. View for a user to choose which payment method(s) they want to use.
  318. This would include setting allocations if payment is to be split
  319. between multiple sources.
  320. """
  321. def get(self, request, *args, **kwargs):
  322. # Check that the user's basket is not empty
  323. if request.basket.is_empty:
  324. messages.error(request, _("You need to add some items to your basket to checkout"))
  325. return HttpResponseRedirect(reverse('basket:summary'))
  326. # Check that shipping address has been completed
  327. if request.basket.is_shipping_required() and not self.checkout_session.is_shipping_address_set():
  328. messages.error(request, _("Please choose a shipping address"))
  329. return HttpResponseRedirect(reverse('checkout:shipping-address'))
  330. # Check that shipping method has been set
  331. if not self.checkout_session.is_shipping_method_set():
  332. messages.error(request, _("Please choose a shipping method"))
  333. return HttpResponseRedirect(reverse('checkout:shipping-method'))
  334. return self.get_success_response()
  335. def get_success_response(self):
  336. return HttpResponseRedirect(reverse('checkout:payment-details'))
  337. # ================
  338. # Order submission
  339. # ================
  340. class OrderPlacementMixin(CheckoutSessionMixin):
  341. """
  342. Mixin which provides functionality for placing orders.
  343. """
  344. # Any payment sources should be added to this list as part of the
  345. # _handle_payment method. If the order is placed successfully, then
  346. # they will be persisted.
  347. _payment_sources = None
  348. _payment_events = None
  349. # Default code for the email to send after successful checkout
  350. communication_type_code = 'ORDER_PLACED'
  351. def handle_order_placement(self, order_number, basket, total_incl_tax, total_excl_tax, **kwargs):
  352. """
  353. Write out the order models and return the appropriate HTTP response
  354. We deliberately pass the basket in here as the one tied to the request
  355. isn't necessarily the correct one to use in placing the order. This can
  356. happen when a basket gets frozen.
  357. """
  358. order = self.place_order(order_number, basket, total_incl_tax, total_excl_tax, **kwargs)
  359. basket.set_as_submitted()
  360. return self.handle_successful_order(order)
  361. def add_payment_source(self, source):
  362. if self._payment_sources is None:
  363. self._payment_sources = []
  364. self._payment_sources.append(source)
  365. def add_payment_event(self, event_type_name, amount):
  366. event_type, n = PaymentEventType.objects.get_or_create(name=event_type_name)
  367. if self._payment_events is None:
  368. self._payment_events = []
  369. event = PaymentEvent(event_type=event_type, amount=amount)
  370. self._payment_events.append(event)
  371. def handle_successful_order(self, order):
  372. """
  373. Handle the various steps required after an order has been successfully placed.
  374. Override this view if you want to perform custom actions when an
  375. order is submitted.
  376. """
  377. # Send confirmation message (normally an email)
  378. self.send_confirmation_message(order)
  379. # Flush all session data
  380. self.checkout_session.flush()
  381. # Save order id in session so thank-you page can load it
  382. self.request.session['checkout_order_id'] = order.id
  383. return HttpResponseRedirect(reverse('checkout:thank-you'))
  384. def place_order(self, order_number, basket, total_incl_tax, total_excl_tax, **kwargs):
  385. """
  386. Writes the order out to the DB including the payment models
  387. """
  388. shipping_address = self.create_shipping_address()
  389. shipping_method = self.get_shipping_method(basket)
  390. billing_address = self.create_billing_address(shipping_address)
  391. if 'status' not in kwargs:
  392. status = self.get_initial_order_status(basket)
  393. else:
  394. status = kwargs.pop('status')
  395. # Set guest email address for anon checkout. Some libraries (eg
  396. # PayPal) will pass this explicitly so we take care not to clobber.
  397. if not self.request.user.is_authenticated() and 'guest_email' not in kwargs:
  398. kwargs['guest_email'] = self.checkout_session.get_guest_email()
  399. order = OrderCreator().place_order(basket=basket,
  400. total_incl_tax=total_incl_tax,
  401. total_excl_tax=total_excl_tax,
  402. user=self.request.user,
  403. shipping_method=shipping_method,
  404. shipping_address=shipping_address,
  405. billing_address=billing_address,
  406. order_number=order_number,
  407. status=status,
  408. **kwargs)
  409. self.save_payment_details(order)
  410. return order
  411. def create_shipping_address(self):
  412. """
  413. Create and returns the shipping address for the current order.
  414. If the shipping address was entered manually, then we simply
  415. write out a ShippingAddress model with the appropriate form data. If
  416. the user is authenticated, then we create a UserAddress from this data
  417. too so it can be re-used in the future.
  418. If the shipping address was selected from the user's address book,
  419. then we convert the UserAddress to a ShippingAddress.
  420. """
  421. if not self.request.basket.is_shipping_required():
  422. return None
  423. addr_data = self.checkout_session.new_shipping_address_fields()
  424. addr_id = self.checkout_session.user_address_id()
  425. if addr_data:
  426. addr = self.create_shipping_address_from_form_fields(addr_data)
  427. self.create_user_address(addr_data)
  428. elif addr_id:
  429. addr = self.create_shipping_address_from_user_address(addr_id)
  430. else:
  431. raise AttributeError("No shipping address data found")
  432. return addr
  433. def create_shipping_address_from_form_fields(self, addr_data):
  434. """Creates a shipping address model from the saved form fields"""
  435. shipping_addr = ShippingAddress(**addr_data)
  436. shipping_addr.save()
  437. return shipping_addr
  438. def create_user_address(self, addr_data):
  439. """
  440. For signed-in users, we create a user address model which will go
  441. into their address book.
  442. """
  443. if self.request.user.is_authenticated():
  444. addr_data['user_id'] = self.request.user.id
  445. user_addr = UserAddress(**addr_data)
  446. # Check that this address isn't already in the db as we don't want
  447. # to fill up the customer address book with duplicate addresses
  448. try:
  449. UserAddress._default_manager.get(hash=user_addr.generate_hash())
  450. except ObjectDoesNotExist:
  451. user_addr.save()
  452. def create_shipping_address_from_user_address(self, addr_id):
  453. """Creates a shipping address from a user address"""
  454. address = UserAddress._default_manager.get(pk=addr_id)
  455. # Increment the number of orders to help determine popularity of orders
  456. address.num_orders += 1
  457. address.save()
  458. shipping_addr = ShippingAddress()
  459. address.populate_alternative_model(shipping_addr)
  460. shipping_addr.save()
  461. return shipping_addr
  462. def create_billing_address(self, shipping_address=None):
  463. """
  464. Saves any relevant billing data (eg a billing address).
  465. """
  466. return None
  467. def save_payment_details(self, order):
  468. """
  469. Saves all payment-related details. This could include a billing
  470. address, payment sources and any order payment events.
  471. """
  472. self.save_payment_events(order)
  473. self.save_payment_sources(order)
  474. def save_payment_events(self, order):
  475. """
  476. Saves any relevant payment events for this order
  477. """
  478. if not self._payment_events:
  479. return
  480. for event in self._payment_events:
  481. event.order = order
  482. event.save()
  483. def save_payment_sources(self, order):
  484. """
  485. Saves any payment sources used in this order.
  486. When the payment sources are created, the order model does not exist and
  487. so they need to have it set before saving.
  488. """
  489. if not self._payment_sources:
  490. return
  491. for source in self._payment_sources:
  492. source.order = order
  493. source.save()
  494. def get_initial_order_status(self, basket):
  495. return None
  496. def get_submitted_basket(self):
  497. basket_id = self.checkout_session.get_submitted_basket_id()
  498. return Basket._default_manager.get(pk=basket_id)
  499. def restore_frozen_basket(self):
  500. """
  501. Restores a frozen basket as the sole OPEN basket. Note that this also merges
  502. in any new products that have been added to a basket that has been created while payment.
  503. """
  504. try:
  505. fzn_basket = self.get_submitted_basket()
  506. except Basket.DoesNotExist:
  507. # Strange place. The previous basket stored in the session does
  508. # not exist.
  509. pass
  510. else:
  511. fzn_basket.thaw()
  512. if self.request.basket.id != fzn_basket.id:
  513. fzn_basket.merge(self.request.basket)
  514. self.request.basket = fzn_basket
  515. def send_confirmation_message(self, order, **kwargs):
  516. code = self.communication_type_code
  517. ctx = {'order': order,
  518. 'lines': order.lines.all(),}
  519. if not self.request.user.is_authenticated():
  520. path = reverse('customer:anon-order',
  521. kwargs={'order_number': order.number,
  522. 'hash': order.verification_hash()})
  523. site = Site.objects.get_current()
  524. ctx['status_url'] = 'http://%s%s' % (site.domain, path)
  525. try:
  526. event_type = CommunicationEventType.objects.get(code=code)
  527. except CommunicationEventType.DoesNotExist:
  528. # No event in database, attempt to find templates for this type
  529. messages = CommunicationEventType.objects.get_and_render(code, ctx)
  530. event_type = None
  531. else:
  532. # Create order event
  533. CommunicationEvent._default_manager.create(order=order, event_type=event_type)
  534. messages = event_type.get_messages(ctx)
  535. if messages and messages['body']:
  536. logger.info("Order #%s - sending %s messages", order.number, code)
  537. dispatcher = Dispatcher(logger)
  538. dispatcher.dispatch_order_messages(order, messages, event_type, **kwargs)
  539. else:
  540. logger.warning("Order #%s - no %s communication event type", order.number, code)
  541. class PaymentDetailsView(OrderPlacementMixin, TemplateView):
  542. """
  543. For taking the details of payment and creating the order
  544. The class is deliberately split into fine-grained methods, responsible for only one
  545. thing. This is to make it easier to subclass and override just one component of
  546. functionality.
  547. Almost all projects will need to subclass and customise this class.
  548. """
  549. template_name = 'checkout/payment_details.html'
  550. template_name_preview = 'checkout/preview.html'
  551. preview = False
  552. def get_template_names(self):
  553. return [self.template_name_preview] if self.preview else [self.template_name]
  554. def get_error_response(self):
  555. # Check that the user's basket is not empty
  556. if self.request.basket.is_empty:
  557. messages.error(self.request, _("You need to add some items to your basket to checkout"))
  558. return HttpResponseRedirect(reverse('basket:summary'))
  559. # Check that shipping address has been completed
  560. if self.request.basket.is_shipping_required() and not self.checkout_session.is_shipping_address_set():
  561. messages.error(self.request, _("Please choose a shipping address"))
  562. return HttpResponseRedirect(reverse('checkout:shipping-address'))
  563. # Check that shipping method has been set
  564. if not self.checkout_session.is_shipping_method_set():
  565. messages.error(self.request, _("Please choose a shipping method"))
  566. return HttpResponseRedirect(reverse('checkout:shipping-method'))
  567. def get(self, request, *args, **kwargs):
  568. error_response = self.get_error_response()
  569. if error_response:
  570. return error_response
  571. return super(PaymentDetailsView, self).get(request, *args, **kwargs)
  572. def post(self, request, *args, **kwargs):
  573. """
  574. This method is designed to be overridden by subclasses which will
  575. validate the forms from the payment details page. If the forms are valid
  576. then the method can call submit()
  577. """
  578. error_response = self.get_error_response()
  579. if error_response:
  580. return error_response
  581. if self.preview:
  582. # We use a custom parameter to indicate if this is an attempt to place an order.
  583. # Without this, we assume a payment form is being submitted from the
  584. # payment-details page
  585. if request.POST.get('action', '') == 'place_order':
  586. return self.submit(request.basket)
  587. return self.render_preview(request)
  588. # Posting to payment-details isn't the right thing to do
  589. return self.get(request, *args, **kwargs)
  590. def render_preview(self, request, **kwargs):
  591. """
  592. Show a preview of the order.
  593. If sensitive data was submitted on the payment details page, you will
  594. need to pass it back to the view here so it can be stored in hidden form
  595. inputs. This avoids ever writing the sensitive data to disk.
  596. """
  597. ctx = self.get_context_data()
  598. ctx.update(kwargs)
  599. return self.render_to_response(ctx)
  600. def can_basket_be_submitted(self, basket):
  601. for line in basket.lines.all():
  602. is_permitted, reason = line.product.is_purchase_permitted(self.request.user, line.quantity)
  603. if not is_permitted:
  604. return False, reason, reverse('basket:summary')
  605. return True, None, None
  606. def get_default_billing_address(self):
  607. """
  608. Return default billing address for user
  609. This is useful when the payment details view includes a billing address
  610. form - you can use this helper method to prepopulate the form.
  611. Note, this isn't used in core oscar as there is no billing address form
  612. by default.
  613. """
  614. if not self.request.user.is_authenticated():
  615. return None
  616. try:
  617. return self.request.user.addresses.get(is_default_for_billing=True)
  618. except UserAddress.DoesNotExist:
  619. return None
  620. def submit(self, basket, payment_kwargs=None, order_kwargs=None):
  621. """
  622. Submit a basket for order placement.
  623. The process runs as follows:
  624. * Generate an order number
  625. * Freeze the basket so it cannot be modified any more.
  626. * Attempt to take payment for the order
  627. - If payment is successful, place the order
  628. - If a redirect is required (eg PayPal, 3DSecure), redirect
  629. - If payment is unsuccessful, show an appropriate error message
  630. """
  631. if payment_kwargs is None:
  632. payment_kwargs = {}
  633. if order_kwargs is None:
  634. order_kwargs = {}
  635. # Next, check that basket isn't empty
  636. if basket.is_empty:
  637. messages.error(self.request, _("This order cannot be submitted as the basket is empty"))
  638. url = self.request.META.get('HTTP_REFERER', reverse('basket:summary'))
  639. return HttpResponseRedirect(url)
  640. # Domain-specific checks on the basket
  641. is_valid, reason, url = self.can_basket_be_submitted(basket)
  642. if not is_valid:
  643. messages.error(self.request, reason)
  644. return HttpResponseRedirect(url)
  645. # We generate the order number first as this will be used
  646. # in payment requests (ie before the order model has been
  647. # created). We also save it in the session for multi-stage
  648. # checkouts (eg where we redirect to a 3rd party site and place
  649. # the order on a different request).
  650. order_number = self.generate_order_number(basket)
  651. logger.info("Order #%s: beginning submission process for basket %d", order_number, basket.id)
  652. self.freeze_basket(basket)
  653. self.checkout_session.set_submitted_basket(basket)
  654. # Handle payment. Any payment problems should be handled by the
  655. # handle_payment method raise an exception, which should be caught
  656. # within handle_POST and the appropriate forms redisplayed.
  657. try:
  658. pre_payment.send_robust(sender=self, view=self)
  659. total_incl_tax, total_excl_tax = self.get_order_totals(basket)
  660. self.handle_payment(order_number, total_incl_tax, **payment_kwargs)
  661. post_payment.send_robust(sender=self, view=self)
  662. except RedirectRequired, e:
  663. # Redirect required (eg PayPal, 3DS)
  664. logger.info("Order #%s: redirecting to %s", order_number, e.url)
  665. return HttpResponseRedirect(e.url)
  666. except UnableToTakePayment, e:
  667. # Something went wrong with payment, need to show
  668. # error to the user. This type of exception is supposed
  669. # to set a friendly error message.
  670. msg = unicode(e)
  671. logger.warning("Order #%s: unable to take payment (%s) - restoring basket", order_number, msg)
  672. self.restore_frozen_basket()
  673. return self.render_to_response(self.get_context_data(error=msg))
  674. except PaymentError, e:
  675. # Something went wrong which wasn't anticipated.
  676. msg = unicode(e)
  677. logger.error("Order #%s: payment error (%s)", order_number, msg)
  678. self.restore_frozen_basket()
  679. return self.render_to_response(self.get_context_data(error="A problem occurred processing payment."))
  680. # If all is ok with payment, try and place order
  681. logger.info("Order #%s: payment successful, placing order", order_number)
  682. try:
  683. return self.handle_order_placement(order_number, basket,
  684. total_incl_tax, total_excl_tax,
  685. **order_kwargs)
  686. except UnableToPlaceOrder, e:
  687. logger.warning("Order #%s: unable to place order - %s",
  688. order_number, e)
  689. msg = unicode(e)
  690. self.restore_frozen_basket()
  691. return self.render_to_response(self.get_context_data(error=msg))
  692. def generate_order_number(self, basket):
  693. generator = OrderNumberGenerator()
  694. order_number = generator.order_number(basket)
  695. self.checkout_session.set_order_number(order_number)
  696. return order_number
  697. def freeze_basket(self, basket):
  698. # We freeze the basket to prevent it being modified once the payment
  699. # process has started. If your payment fails, then the basket will
  700. # need to be "unfrozen". We also store the basket ID in the session
  701. # so the it can be retrieved by multistage checkout processes.
  702. basket.freeze()
  703. def handle_payment(self, order_number, total, **kwargs):
  704. """
  705. Handle any payment processing.
  706. This method is designed to be overridden within your project. The
  707. default is to do nothing.
  708. """
  709. pass
  710. def get_context_data(self, **kwargs):
  711. # Return kwargs directly instead of using 'params' as in django's TemplateView
  712. ctx = super(PaymentDetailsView, self).get_context_data(**kwargs)
  713. ctx.update(kwargs)
  714. return ctx
  715. # =========
  716. # Thank you
  717. # =========
  718. class ThankYouView(DetailView):
  719. """
  720. Displays the 'thank you' page which summarises the order just submitted.
  721. """
  722. template_name = 'checkout/thank_you.html'
  723. context_object_name = 'order'
  724. def get_object(self):
  725. # We allow superusers to force an order thankyou page for testing
  726. order = None
  727. if self.request.user.is_superuser:
  728. if 'order_number' in self.request.GET:
  729. order = Order._default_manager.get(number=self.request.GET['order_number'])
  730. elif 'order_id' in self.request.GET:
  731. order = Order._default_manager.get(id=self.request.GET['orderid'])
  732. if not order:
  733. if 'checkout_order_id' in self.request.session:
  734. order = Order._default_manager.get(pk=self.request.session['checkout_order_id'])
  735. else:
  736. raise Http404(_("No order found"))
  737. return order