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 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. from decimal import Decimal
  2. import logging
  3. from django.conf import settings
  4. from django.http import HttpResponse, Http404, HttpResponseRedirect, HttpResponseBadRequest
  5. from django.template import RequestContext
  6. from django.shortcuts import get_object_or_404
  7. from django.core.urlresolvers import reverse
  8. from django.forms import ModelForm
  9. from django.contrib import messages
  10. from django.core.urlresolvers import resolve
  11. from django.core.exceptions import ObjectDoesNotExist
  12. from django.utils.translation import ugettext as _
  13. from django.template.response import TemplateResponse
  14. from oscar.view.generic import ModelView
  15. from oscar.core.loading import import_module
  16. import_module('checkout.forms', ['ShippingAddressForm'], locals())
  17. import_module('checkout.calculators', ['OrderTotalCalculator'], locals())
  18. import_module('checkout.utils', ['ProgressChecker', 'CheckoutSessionData'], locals())
  19. import_module('checkout.signals', ['pre_payment', 'post_payment'], locals())
  20. import_module('checkout.core_views', ['CheckoutView', 'mark_step_as_complete'], locals())
  21. import_module('order.models', ['Order', 'ShippingAddress'], locals())
  22. import_module('order.utils', ['OrderNumberGenerator', 'OrderCreator'], locals())
  23. import_module('address.models', ['UserAddress'], locals())
  24. import_module('shipping.repository', ['Repository'], locals())
  25. logger = logging.getLogger('oscar.checkout')
  26. class IndexView(object):
  27. template_file = 'oscar/checkout/gateway.html'
  28. def __call__(self, request):
  29. if request.user.is_authenticated():
  30. return HttpResponseRedirect(reverse('oscar-checkout-shipping-address'))
  31. return TemplateResponse(request, self.template_file)
  32. class ShippingAddressView(CheckoutView):
  33. template_file = 'oscar/checkout/shipping_address.html'
  34. def handle_POST(self):
  35. if self.request.user.is_authenticated and 'address_id' in self.request.POST:
  36. address = UserAddress._default_manager.get(pk=self.request.POST['address_id'])
  37. if 'action' in self.request.POST and self.request.POST['action'] == 'ship_to':
  38. # User has selected a previous address to ship to
  39. self.co_data.ship_to_user_address(address)
  40. return self.get_success_response()
  41. elif 'action' in self.request.POST and self.request.POST['action'] == 'delete':
  42. address.delete()
  43. messages.info(self.request, "Address deleted from your address book")
  44. return HttpResponseRedirect(reverse('oscar-checkout-shipping-method'))
  45. else:
  46. return HttpResponseBadRequest()
  47. else:
  48. form = ShippingAddressForm(self.request.POST)
  49. if form.is_valid():
  50. # Address data is valid - store in session and redirect to next step.
  51. self.co_data.ship_to_new_address(form.clean())
  52. return self.get_success_response()
  53. return self.handle_GET(form)
  54. def handle_GET(self, form=None):
  55. if not form:
  56. addr_fields = self.co_data.new_address_fields()
  57. if addr_fields:
  58. form = ShippingAddressForm(addr_fields)
  59. else:
  60. form = ShippingAddressForm()
  61. self.context['form'] = form
  62. # Look up address book data
  63. if self.request.user.is_authenticated():
  64. self.context['addresses'] = UserAddress._default_manager.filter(user=self.request.user)
  65. return TemplateResponse(self.request, self.template_file, self.context)
  66. class ShippingMethodView(CheckoutView):
  67. u"""
  68. Shipping methods are domain-specific and so need implementing in a
  69. subclass of this class.
  70. """
  71. template_file = 'oscar/checkout/shipping_methods.html';
  72. def handle_GET(self):
  73. methods = self.get_available_shipping_methods()
  74. if len(methods) == 1:
  75. # Only one method - set this and redirect onto the next step
  76. self.co_data.use_shipping_method(methods[0].code)
  77. return self.get_success_response()
  78. self.context['methods'] = methods
  79. return TemplateResponse(self.request, self.template_file, self.context)
  80. def get_shipping_methods_for_basket(self, basket):
  81. u"""Return available shipping methods for a basket"""
  82. return shipping_models.Method.objects.all()
  83. def get_available_shipping_methods(self):
  84. u"""
  85. Returns all applicable shipping method objects
  86. for a given basket.
  87. """
  88. repo = Repository()
  89. return repo.get_shipping_methods(self.request.user, self.basket, self.get_shipping_address())
  90. def handle_POST(self):
  91. method_code = self.request.POST['method_code']
  92. self.co_data.use_shipping_method(method_code)
  93. return self.get_success_response()
  94. class PaymentMethodView(CheckoutView):
  95. u"""
  96. View for a user to choose which payment method(s) they want to use.
  97. This would include setting allocations if payment is to be split
  98. between multiple sources.
  99. """
  100. pass
  101. class OrderPreviewView(CheckoutView):
  102. """
  103. View a preview of the order before submitting.
  104. """
  105. template_file = 'oscar/checkout/preview.html'
  106. def handle_GET(self):
  107. mark_step_as_complete(self.request)
  108. return TemplateResponse(self.request, self.template_file, self.context)
  109. class PaymentDetailsView(CheckoutView):
  110. u"""
  111. For taking the details of payment and creating the order
  112. The class is deliberately split into fine-grained method, responsible for only one
  113. thing. This is to make it easier to subclass and override just one component of
  114. functionality.
  115. """
  116. # Any payment sources should be added to this list as part of the
  117. # _handle_payment method. If the order is placed successfully, then
  118. # they will be persisted.
  119. payment_sources = []
  120. def handle_GET(self):
  121. """
  122. This method needs to be overridden if there are any payment details
  123. to be taken from the user, such as a bankcard.
  124. """
  125. return self.handle_POST()
  126. def handle_POST(self):
  127. """
  128. This method is designed to be overridden by subclasses which will
  129. validate the forms from the payment details page. If the forms are valid
  130. then the method can call submit()."""
  131. return self.submit()
  132. def submit(self):
  133. # We generate the order number first as this will be used
  134. # in payment requests (ie before the order model has been
  135. # created).
  136. order_number = self.generate_order_number(self.basket)
  137. logger.info(_("Submitting order #%s" % order_number))
  138. # We freeze the basket to prevent it being modified once the payment
  139. # process has started. If your payment fails, then the basket will
  140. # need to be "unfrozen".
  141. self.basket.freeze()
  142. # Calculate totals
  143. calc = OrderTotalCalculator(self.request)
  144. shipping_method = self.get_shipping_method(self.basket)
  145. total_incl_tax = calc.order_total_incl_tax(self.basket, shipping_method)
  146. total_excl_tax = calc.order_total_excl_tax(self.basket, shipping_method)
  147. # Handle payment. Any payment problems should be handled by the
  148. # _handle_payment method raise an exception, which should be caught
  149. # within handle_POST and the appropriate forms redisplayed.
  150. pre_payment.send_robust(sender=self, view=self)
  151. self.handle_payment(order_number, total_incl_tax)
  152. post_payment.send_robust(sender=self, view=self)
  153. # Everything is ok, we place the order and save the payment details
  154. order = self.place_order(self.basket, order_number, total_incl_tax, total_excl_tax)
  155. self.save_payment_details(order)
  156. self.reset_checkout()
  157. logger.info(_("Order #%s submitted successfully" % order_number))
  158. # Save order id in session so thank-you page can load it
  159. self.request.session['checkout_order_id'] = order.id
  160. return HttpResponseRedirect(reverse('oscar-checkout-thank-you'))
  161. def generate_order_number(self, basket):
  162. generator = OrderNumberGenerator()
  163. return generator.order_number(basket)
  164. def handle_payment(self, order_number, total):
  165. """
  166. Handle any payment processing.
  167. This method is designed to be overridden within your project. The
  168. default is to do nothing.
  169. """
  170. pass
  171. def save_payment_details(self, order):
  172. """
  173. Saves all payment-related details. This could include a billing
  174. address, payment sources and any order payment events.
  175. """
  176. self.save_payment_events(order)
  177. self.save_payment_sources(order)
  178. def create_billing_address(self):
  179. """
  180. Saves any relevant billing data (eg a billing address).
  181. """
  182. return None
  183. def save_payment_events(self, order):
  184. """
  185. Saves any relevant payment events for this order
  186. """
  187. pass
  188. def save_payment_sources(self, order):
  189. u"""
  190. Saves any payment sources used in this order.
  191. When the payment sources are created, the order model does not exist and
  192. so they need to have it set before saving.
  193. """
  194. for source in self.payment_sources:
  195. source.order = order
  196. source.save()
  197. def reset_checkout(self):
  198. u"""Reset any checkout session state"""
  199. self.co_data.flush()
  200. ProgressChecker().all_steps_complete(self.request)
  201. def place_order(self, basket, order_number, total_incl_tax, total_excl_tax):
  202. u"""Writes the order out to the DB"""
  203. shipping_address = self.create_shipping_address()
  204. shipping_method = self.get_shipping_method(basket)
  205. billing_address = self.create_billing_address()
  206. return OrderCreator().place_order(self.request.user,
  207. basket,
  208. shipping_address,
  209. shipping_method,
  210. billing_address,
  211. total_incl_tax,
  212. total_excl_tax,
  213. order_number)
  214. def get_shipping_method(self, basket):
  215. u"""Returns the shipping method object"""
  216. method = self.co_data.shipping_method()
  217. method.set_basket(basket)
  218. return method
  219. def get_shipping_address(self):
  220. addr_data = self.co_data.new_address_fields()
  221. addr_id = self.co_data.user_address_id()
  222. if addr_data:
  223. addr = ShippingAddress(**addr_data)
  224. elif addr_id:
  225. addr = UserAddress._default_manager.get(pk=addr_id)
  226. return addr
  227. def create_shipping_address(self):
  228. u"""Returns the shipping address"""
  229. addr_data = self.co_data.new_address_fields()
  230. addr_id = self.co_data.user_address_id()
  231. if addr_data:
  232. addr = self.create_shipping_address_from_form_fields(addr_data)
  233. self.create_user_address(addr_data)
  234. elif addr_id:
  235. addr = self.create_shipping_address_from_user_address(addr_id)
  236. else:
  237. raise AttributeError("No shipping address data found")
  238. return addr
  239. def create_shipping_address_from_form_fields(self, addr_data):
  240. u"""Creates a shipping address model from the saved form fields"""
  241. shipping_addr = ShippingAddress(**addr_data)
  242. shipping_addr.save()
  243. return shipping_addr
  244. def create_user_address(self, addr_data):
  245. u"""
  246. For signed-in users, we create a user address model which will go
  247. into their address book.
  248. """
  249. if self.request.user.is_authenticated():
  250. addr_data['user_id'] = self.request.user.id
  251. user_addr = UserAddress(**addr_data)
  252. # Check that this address isn't already in the db as we don't want
  253. # to fill up the customer address book with duplicate addresses
  254. try:
  255. UserAddress._default_manager.get(hash=user_addr.generate_hash())
  256. except ObjectDoesNotExist:
  257. user_addr.save()
  258. def create_shipping_address_from_user_address(self, addr_id):
  259. u"""Creates a shipping address from a user address"""
  260. address = UserAddress._default_manager.get(pk=addr_id)
  261. # Increment the number of orders to help determine popularity of orders
  262. address.num_orders += 1
  263. address.save()
  264. shipping_addr = ShippingAddress()
  265. address.populate_alternative_model(shipping_addr)
  266. shipping_addr.save()
  267. return shipping_addr
  268. class ThankYouView(object):
  269. """
  270. Displays the 'thank you' page which summarises the order just submitted.
  271. """
  272. def __call__(self, request):
  273. try:
  274. order = Order._default_manager.get(pk=request.session['checkout_order_id'])
  275. # Remove order number from session to ensure that the thank-you page is only
  276. # viewable once.
  277. del request.session['checkout_order_id']
  278. except KeyError, ObjectDoesNotExist:
  279. return HttpResponseRedirect(reverse('oscar-checkout-index'))
  280. return TemplateResponse(request, 'oscar/checkout/thank_you.html', {'order': order})