您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

views.py 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  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 django.core.mail import EmailMessage
  15. from django.views.generic import DetailView, TemplateView, FormView, DeleteView, UpdateView, CreateView
  16. from oscar.core.loading import import_module
  17. import_module('checkout.forms', ['ShippingAddressForm'], locals())
  18. import_module('checkout.calculators', ['OrderTotalCalculator'], locals())
  19. import_module('checkout.utils', ['CheckoutSessionData'], locals())
  20. import_module('checkout.signals', ['pre_payment', 'post_payment'], locals())
  21. import_module('order.models', ['Order', 'ShippingAddress', 'CommunicationEventType', 'CommunicationEvent'], locals())
  22. import_module('order.utils', ['OrderNumberGenerator', 'OrderCreator'], locals())
  23. import_module('address.models', ['UserAddress'], locals())
  24. import_module('address.forms', ['UserAddressForm'], locals())
  25. import_module('shipping.repository', ['Repository'], locals())
  26. import_module('customer.models', ['Email'], locals())
  27. import_module('payment.exceptions', ['RedirectRequired', 'UnableToTakePayment',
  28. 'PaymentError'], locals())
  29. import_module('basket.models', ['Basket'], locals())
  30. # Standard logger for checkout events
  31. logger = logging.getLogger('oscar.checkout')
  32. from oscar.apps.customer.views import AccountAuthView
  33. class IndexView(AccountAuthView):
  34. """
  35. First page of the checkout. If the user is signed in then we forward
  36. straight onto the next step. Otherwise, we provide options to login, register and
  37. (if the option is enabled) proceed anonymously.
  38. """
  39. template_name = 'checkout/gateway.html'
  40. def get_logged_in_redirect(self):
  41. return reverse('oscar-checkout-shipping-address')
  42. class CheckoutSessionMixin(object):
  43. """
  44. Mixin to provide common functionality shared between checkout views.
  45. """
  46. def dispatch(self, request, *args, **kwargs):
  47. self.checkout_session = CheckoutSessionData(request)
  48. return super(CheckoutSessionMixin, self).dispatch(request, *args, **kwargs)
  49. def get_shipping_address(self):
  50. """
  51. Return the current shipping address for this checkout session.
  52. This could either be a ShippingAddress model which has been
  53. pre-populated (not saved), or a UserAddress model which will
  54. need converting into a ShippingAddress model at submission
  55. """
  56. addr_data = self.checkout_session.new_address_fields()
  57. if addr_data:
  58. # Load address data into a blank address model
  59. return ShippingAddress(**addr_data)
  60. addr_id = self.checkout_session.user_address_id()
  61. if addr_id:
  62. try:
  63. return UserAddress._default_manager.get(pk=addr_id)
  64. except UserAddress.DoesNotExist:
  65. # This can happen if you reset all your tables and you still have
  66. # session data that refers to addresses that no longer exist
  67. pass
  68. return None
  69. def get_shipping_method(self, basket=None):
  70. method = self.checkout_session.shipping_method()
  71. if method:
  72. if not basket:
  73. basket = self.request.basket
  74. method.set_basket(basket)
  75. return method
  76. def get_order_totals(self, basket=None, shipping_method=None):
  77. """
  78. Returns the total for the order with and without tax (as a tuple)
  79. """
  80. calc = OrderTotalCalculator(self.request)
  81. if not basket:
  82. basket = self.request.basket
  83. if not shipping_method:
  84. shipping_method = self.get_shipping_method(basket)
  85. total_incl_tax = calc.order_total_incl_tax(basket, shipping_method)
  86. total_excl_tax = calc.order_total_excl_tax(basket, shipping_method)
  87. return total_incl_tax, total_excl_tax
  88. def get_context_data(self, **kwargs):
  89. """
  90. Assign common template variables to the context.
  91. """
  92. ctx = super(CheckoutSessionMixin, self).get_context_data(**kwargs)
  93. ctx['shipping_address'] = self.get_shipping_address()
  94. method = self.get_shipping_method()
  95. if method:
  96. ctx['shipping_method'] = method
  97. ctx['shipping_total_excl_tax'] = method.basket_charge_excl_tax()
  98. ctx['shipping_total_incl_tax'] = method.basket_charge_incl_tax()
  99. ctx['order_total_incl_tax'], ctx['order_total_excl_tax'] = self.get_order_totals()
  100. return ctx
  101. # ================
  102. # SHIPPING ADDRESS
  103. # ================
  104. class ShippingAddressView(CheckoutSessionMixin, FormView):
  105. """
  106. Determine the shipping address for the order.
  107. The default behaviour is to display a list of addresses from the users's
  108. address book, from which the user can choose one to be their shipping address.
  109. They can add/edit/delete these USER addresses. This address will be
  110. automatically converted into a SHIPPING address when the user checks out.
  111. Alternatively, the user can enter a SHIPPING address directly which will be
  112. saved in the session and saved as a model when the order is sucessfully submitted.
  113. """
  114. template_name = 'checkout/shipping_address.html'
  115. form_class = ShippingAddressForm
  116. def get_initial(self):
  117. return self.checkout_session.new_address_fields()
  118. def get_context_data(self, **kwargs):
  119. if self.request.user.is_authenticated():
  120. # Look up address book data
  121. kwargs['addresses'] = UserAddress._default_manager.filter(user=self.request.user)
  122. return kwargs
  123. def post(self, request, *args, **kwargs):
  124. # Check if a shipping address was selected directly (eg no form was filled in)
  125. if self.request.user.is_authenticated and 'address_id' in self.request.POST:
  126. address = UserAddress._default_manager.get(pk=self.request.POST['address_id'])
  127. if 'action' in self.request.POST and self.request.POST['action'] == 'ship_to':
  128. # User has selected a previous address to ship to
  129. self.checkout_session.ship_to_user_address(address)
  130. return HttpResponseRedirect(self.get_success_url())
  131. elif 'action' in self.request.POST and self.request.POST['action'] == 'delete':
  132. address.delete()
  133. messages.info(self.request, "Address deleted from your address book")
  134. return HttpResponseRedirect(reverse('oscar-checkout-shipping-method'))
  135. else:
  136. return HttpResponseBadRequest()
  137. else:
  138. return super(ShippingAddressView, self).post(request, *args, **kwargs)
  139. def form_valid(self, form):
  140. self.checkout_session.ship_to_new_address(form.clean())
  141. return super(ShippingAddressView, self).form_valid(form)
  142. def get_success_url(self):
  143. return reverse('oscar-checkout-shipping-method')
  144. class UserAddressCreateView(CreateView):
  145. """
  146. Add a USER address to the user's addressbook.
  147. This is not the same as creating a SHIPPING Address, although if used for the order,
  148. it will be converted into a shipping address at submission-time.
  149. """
  150. template_name = 'checkout/user_address_form.html'
  151. form_class = UserAddressForm
  152. def get_context_data(self, **kwargs):
  153. kwargs = super(UserAddressCreateView, self).get_context_data(**kwargs)
  154. kwargs['form_url'] = reverse('oscar-checkout-user-address-create')
  155. return kwargs
  156. def form_valid(self, form):
  157. self.object = form.save(commit=False)
  158. self.object.user = self.request.user
  159. self.object.save()
  160. return self.get_success_response()
  161. def get_success_response(self):
  162. messages.info(self.request, _("Address saved"))
  163. # We redirect back to the shipping address page
  164. return HttpResponseRedirect(reverse('oscar-checkout-shipping-address'))
  165. class UserAddressUpdateView(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 UserAddress._default_manager.filter(user=self.request.user)
  173. def get_context_data(self, **kwargs):
  174. kwargs = super(UserAddressUpdateView, self).get_context_data(**kwargs)
  175. kwargs['form_url'] = reverse('oscar-checkout-user-address-update', args=(str(kwargs['object'].id)))
  176. return kwargs
  177. def get_success_url(self):
  178. messages.info(self.request, _("Address saved"))
  179. return reverse('oscar-checkout-shipping-address')
  180. class UserAddressDeleteView(DeleteView):
  181. """
  182. Delete an address from a user's addressbook.
  183. """
  184. def get_queryset(self):
  185. return UserAddress._default_manager.filter(user=self.request.user)
  186. def get_success_url(self):
  187. messages.info(self.request, _("Address deleted"))
  188. return reverse('oscar-checkout-shipping-address')
  189. # ===============
  190. # Shipping method
  191. # ===============
  192. class ShippingMethodView(CheckoutSessionMixin, TemplateView):
  193. """
  194. Shipping methods are domain-specific and so need implementing in a
  195. subclass of this class.
  196. """
  197. template_name = 'checkout/shipping_methods.html';
  198. def get(self, request, *args, **kwargs):
  199. # Save shipping methods as instance var as we need them both here
  200. # and when setting the context vars.
  201. self._methods = self.get_available_shipping_methods()
  202. if len(self._methods) == 1:
  203. # Only one shipping method - set this and redirect onto the next step
  204. self.checkout_session.use_shipping_method(self._methods[0].code)
  205. return self.get_success_response()
  206. return super(ShippingMethodView, self).get(request, *args, **kwargs)
  207. def get_context_data(self, **kwargs):
  208. kwargs = super(ShippingMethodView, self).get_context_data(**kwargs)
  209. kwargs['methods'] = self._methods
  210. return kwargs
  211. def get_available_shipping_methods(self):
  212. """
  213. Returns all applicable shipping method objects
  214. for a given basket.
  215. """
  216. repo = Repository()
  217. return repo.get_shipping_methods(self.request.user, self.request.basket,
  218. self.get_shipping_address())
  219. def post(self, request, *args, **kwargs):
  220. method_code = request.POST['method_code']
  221. self.checkout_session.use_shipping_method(method_code)
  222. return self.get_success_response()
  223. def get_success_response(self):
  224. return HttpResponseRedirect(reverse('oscar-checkout-payment-method'))
  225. class PaymentMethodView(CheckoutSessionMixin, TemplateView):
  226. """
  227. View for a user to choose which payment method(s) they want to use.
  228. This would include setting allocations if payment is to be split
  229. between multiple sources.
  230. """
  231. def get(self, request, *args, **kwargs):
  232. return self.get_success_response()
  233. def get_success_response(self):
  234. return HttpResponseRedirect(reverse('oscar-checkout-preview'))
  235. class OrderPreviewView(CheckoutSessionMixin, TemplateView):
  236. """
  237. View a preview of the order before submitting.
  238. """
  239. template_name = 'checkout/preview.html'
  240. class PaymentDetailsView(CheckoutSessionMixin, TemplateView):
  241. """
  242. For taking the details of payment and creating the order
  243. The class is deliberately split into fine-grained method, responsible for only one
  244. thing. This is to make it easier to subclass and override just one component of
  245. functionality.
  246. """
  247. # Any payment sources should be added to this list as part of the
  248. # _handle_payment method. If the order is placed successfully, then
  249. # they will be persisted.
  250. payment_sources = []
  251. def post(self, request, *args, **kwargs):
  252. """
  253. This method is designed to be overridden by subclasses which will
  254. validate the forms from the payment details page. If the forms are valid
  255. then the method can call submit()."""
  256. return self.submit(request.basket, **kwargs)
  257. def submit(self, basket, **kwargs):
  258. # We generate the order number first as this will be used
  259. # in payment requests (ie before the order model has been
  260. # created).
  261. order_number = self.generate_order_number(basket)
  262. logger.info(_("Order #%s: beginning submission process" % order_number))
  263. # We freeze the basket to prevent it being modified once the payment
  264. # process has started. If your payment fails, then the basket will
  265. # need to be "unfrozen". We also store the basket ID in the session
  266. # so the it can be retrieved by multistage checkout processes.
  267. basket.freeze()
  268. self.checkout_session.set_submitted_basket(basket)
  269. # Handle payment. Any payment problems should be handled by the
  270. # handle_payment method raise an exception, which should be caught
  271. # within handle_POST and the appropriate forms redisplayed.
  272. try:
  273. pre_payment.send_robust(sender=self, view=self)
  274. total_incl_tax, total_excl_tax = self.get_order_totals(basket)
  275. self.handle_payment(order_number, total_incl_tax, **kwargs)
  276. post_payment.send_robust(sender=self, view=self)
  277. except RedirectRequired, e:
  278. # Redirect required (eg PayPal, 3DS)
  279. return HttpResponseRedirect(e.url)
  280. except UnableToTakePayment, e:
  281. # Something went wrong with payment, need to show
  282. # error to the user. This type of exception is supposed
  283. # to set a friendly error message.
  284. return self.handle_GET(error=e.message)
  285. except PaymentError:
  286. # Something went wrong which wasn't anticipated.
  287. return self.handle_GET(error="A problem occured processing payment.")
  288. else:
  289. # If all is ok with payment, place order
  290. return self.place_order(order_number, basket, total_incl_tax, total_excl_tax)
  291. def get_submitted_basket(self):
  292. basket_id = self.checkout_session.get_submitted_basket_id()
  293. return Basket._default_manager.get(pk=basket_id)
  294. def restore_frozen_basket(self):
  295. """
  296. Restores a frozen basket as the sole OPEN basket. Note that this also merges
  297. in any new products that have been added to a basket that has been created while payment.
  298. """
  299. fzn_basket = self.get_submitted_basket()
  300. fzn_basket.thaw()
  301. fzn_basket.merge(self.request.basket)
  302. self.set_template_context(fzn_basket)
  303. def place_order(self, order_number, basket, total_incl_tax=None, total_excl_tax=None):
  304. """
  305. Place the order
  306. We deliberately pass the basket in here as the one tied to the request
  307. isn't necessarily the correct one to use in placing the order. This can
  308. happen when a basket gets frozen.
  309. """
  310. if total_incl_tax is None or total_excl_tax is None:
  311. total_incl_tax, total_excl_tax = self.get_order_totals(basket)
  312. order = self.create_order_models(basket, order_number, total_incl_tax, total_excl_tax)
  313. self.save_payment_details(order)
  314. self.reset_checkout()
  315. logger.info(_("Order #%s: submitted successfully" % order_number))
  316. # Send confirmation message (normally an email)
  317. self.send_confirmation_message(order)
  318. # Save order id in session so thank-you page can load it
  319. self.request.session['checkout_order_id'] = order.id
  320. return HttpResponseRedirect(reverse('oscar-checkout-thank-you'))
  321. def generate_order_number(self, basket):
  322. generator = OrderNumberGenerator()
  323. return generator.order_number(basket)
  324. def handle_payment(self, order_number, total, **kwargs):
  325. """
  326. Handle any payment processing.
  327. This method is designed to be overridden within your project. The
  328. default is to do nothing.
  329. """
  330. pass
  331. def save_payment_details(self, order):
  332. """
  333. Saves all payment-related details. This could include a billing
  334. address, payment sources and any order payment events.
  335. """
  336. self.save_payment_events(order)
  337. self.save_payment_sources(order)
  338. def create_billing_address(self):
  339. """
  340. Saves any relevant billing data (eg a billing address).
  341. """
  342. return None
  343. def save_payment_events(self, order):
  344. """
  345. Saves any relevant payment events for this order
  346. """
  347. pass
  348. def save_payment_sources(self, order):
  349. """
  350. Saves any payment sources used in this order.
  351. When the payment sources are created, the order model does not exist and
  352. so they need to have it set before saving.
  353. """
  354. for source in self.payment_sources:
  355. source.order = order
  356. source.save()
  357. def reset_checkout(self):
  358. """Reset any checkout session state"""
  359. self.checkout_session.flush()
  360. def create_order_models(self, basket, order_number, total_incl_tax, total_excl_tax):
  361. """Writes the order out to the DB"""
  362. shipping_address = self.create_shipping_address()
  363. shipping_method = self.get_shipping_method(basket)
  364. billing_address = self.create_billing_address()
  365. status = self.get_initial_order_status(basket)
  366. return OrderCreator().place_order(self.request.user,
  367. basket,
  368. shipping_address,
  369. shipping_method,
  370. billing_address,
  371. total_incl_tax,
  372. total_excl_tax,
  373. order_number,
  374. status)
  375. def get_initial_order_status(self, basket):
  376. return None
  377. def create_shipping_address(self):
  378. """
  379. Create and returns the shipping address for the current order.
  380. If the shipping address was entered manually, then we simply
  381. write out a ShippingAddress model with the appropriate form data. If
  382. the user is authenticated, then we create a UserAddress from this data
  383. too so it can be re-used in the future.
  384. If the shipping address was selected from the user's address book,
  385. then we convert the UserAddress to a ShippingAddress.
  386. """
  387. addr_data = self.checkout_session.new_address_fields()
  388. addr_id = self.checkout_session.user_address_id()
  389. if addr_data:
  390. addr = self.create_shipping_address_from_form_fields(addr_data)
  391. self.create_user_address(addr_data)
  392. elif addr_id:
  393. addr = self.create_shipping_address_from_user_address(addr_id)
  394. else:
  395. raise AttributeError("No shipping address data found")
  396. return addr
  397. def create_shipping_address_from_form_fields(self, addr_data):
  398. """Creates a shipping address model from the saved form fields"""
  399. shipping_addr = ShippingAddress(**addr_data)
  400. shipping_addr.save()
  401. return shipping_addr
  402. def create_user_address(self, addr_data):
  403. """
  404. For signed-in users, we create a user address model which will go
  405. into their address book.
  406. """
  407. if self.request.user.is_authenticated():
  408. addr_data['user_id'] = self.request.user.id
  409. user_addr = UserAddress(**addr_data)
  410. # Check that this address isn't already in the db as we don't want
  411. # to fill up the customer address book with duplicate addresses
  412. try:
  413. UserAddress._default_manager.get(hash=user_addr.generate_hash())
  414. except ObjectDoesNotExist:
  415. user_addr.save()
  416. def create_shipping_address_from_user_address(self, addr_id):
  417. """Creates a shipping address from a user address"""
  418. address = UserAddress._default_manager.get(pk=addr_id)
  419. # Increment the number of orders to help determine popularity of orders
  420. address.num_orders += 1
  421. address.save()
  422. shipping_addr = ShippingAddress()
  423. address.populate_alternative_model(shipping_addr)
  424. shipping_addr.save()
  425. return shipping_addr
  426. def send_confirmation_message(self, order):
  427. # Create order communication event
  428. try:
  429. event_type = CommunicationEventType._default_manager.get(code='order-placed')
  430. except CommunicationEventType.DoesNotExist:
  431. logger.error(_("Order #%s: unable to find 'order_placed' comms event" % order.number))
  432. else:
  433. if self.request.user.is_authenticated() and event_type.has_email_templates():
  434. logger.info(_("Order #%s: sending confirmation email" % order.number))
  435. # Send the email
  436. subject = event_type.get_email_subject_for_order(order)
  437. body = event_type.get_email_body_for_order(order)
  438. email = EmailMessage(subject, body, to=[self.request.user.email])
  439. email.send()
  440. # Record email against user for their email history
  441. Email._default_manager.create(user=self.request.user,
  442. subject=email.subject,
  443. body_text=email.body)
  444. # Record communication event against order
  445. CommunicationEvent._default_manager.create(order=order, type=event_type)
  446. class ThankYouView(DetailView):
  447. """
  448. Displays the 'thank you' page which summarises the order just submitted.
  449. """
  450. template_name = 'checkout/thank_you.html'
  451. context_object_name = 'order'
  452. def get_object(self):
  453. return Order._default_manager.get(pk=self.request.session['checkout_order_id'])