Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. from decimal import Decimal as D
  2. from django.contrib import messages
  3. from django import http
  4. from django.core.exceptions import ImproperlyConfigured
  5. from django.core.urlresolvers import reverse
  6. from django.utils.translation import ugettext_lazy as _
  7. from oscar.core.loading import get_model, get_class
  8. from . import exceptions
  9. Repository = get_class('shipping.repository', 'Repository')
  10. OrderTotalCalculator = get_class(
  11. 'checkout.calculators', 'OrderTotalCalculator')
  12. CheckoutSessionData = get_class(
  13. 'checkout.utils', 'CheckoutSessionData')
  14. ShippingAddress = get_model('order', 'ShippingAddress')
  15. BillingAddress = get_model('order', 'BillingAddress')
  16. UserAddress = get_model('address', 'UserAddress')
  17. class CheckoutSessionMixin(object):
  18. """
  19. Mixin to provide common functionality shared between checkout views.
  20. All checkout views subclass this mixin. It ensures that all relevant
  21. checkout information is available in the template context.
  22. """
  23. # A pre-condition is a condition that MUST be met in order for a view
  24. # to be available. If it isn't then the customer should be redirected
  25. # to a view *earlier* in the chain.
  26. # pre_conditions is a list of method names that get executed before the
  27. # normal flow of the view. Each method should check some condition has been
  28. # met. If not, then an exception is raised that indicates the URL the
  29. # customer will be redirected to.
  30. pre_conditions = None
  31. # A *skip* condition is a condition that MUST NOT be met in order for a
  32. # view to be available. If the condition is met, this means the view MUST
  33. # be skipped and the customer should be redirected to a view *later* in
  34. # the chain.
  35. # Skip conditions work similar to pre-conditions, and get evaluated after
  36. # pre-conditions have been evaluated.
  37. skip_conditions = None
  38. def dispatch(self, request, *args, **kwargs):
  39. # Assign the checkout session manager so it's available in all checkout
  40. # views.
  41. self.checkout_session = CheckoutSessionData(request)
  42. # Enforce any pre-conditions for the view.
  43. try:
  44. self.check_pre_conditions(request)
  45. except exceptions.FailedPreCondition as e:
  46. for message in e.messages:
  47. messages.warning(request, message)
  48. return http.HttpResponseRedirect(e.url)
  49. # Check if this view should be skipped
  50. try:
  51. self.check_skip_conditions(request)
  52. except exceptions.PassedSkipCondition as e:
  53. return http.HttpResponseRedirect(e.url)
  54. return super(CheckoutSessionMixin, self).dispatch(
  55. request, *args, **kwargs)
  56. def check_pre_conditions(self, request):
  57. pre_conditions = self.get_pre_conditions(request)
  58. for method_name in pre_conditions:
  59. if not hasattr(self, method_name):
  60. raise ImproperlyConfigured(
  61. "There is no method '%s' to call as a pre-condition" % (
  62. method_name))
  63. getattr(self, method_name)(request)
  64. def get_pre_conditions(self, request):
  65. """
  66. Return the pre-condition method names to run for this view
  67. """
  68. if self.pre_conditions is None:
  69. return []
  70. return self.pre_conditions
  71. def check_skip_conditions(self, request):
  72. skip_conditions = self.get_skip_conditions(request)
  73. for method_name in skip_conditions:
  74. if not hasattr(self, method_name):
  75. raise ImproperlyConfigured(
  76. "There is no method '%s' to call as a skip-condition" % (
  77. method_name))
  78. getattr(self, method_name)(request)
  79. def get_skip_conditions(self, request):
  80. """
  81. Return the skip-condition method names to run for this view
  82. """
  83. if self.skip_conditions is None:
  84. return []
  85. return self.skip_conditions
  86. # Re-usable pre-condition validators
  87. def check_basket_is_not_empty(self, request):
  88. if request.basket.is_empty:
  89. raise exceptions.FailedPreCondition(
  90. url=reverse('basket:summary'),
  91. message=_(
  92. "You need to add some items to your basket to checkout")
  93. )
  94. def check_basket_is_valid(self, request):
  95. """
  96. Check that the basket is permitted to be submitted as an order. That
  97. is, all the basket lines are available to buy - nothing has gone out of
  98. stock since it was added to the basket.
  99. """
  100. messages = []
  101. strategy = request.strategy
  102. for line in request.basket.all_lines():
  103. result = strategy.fetch_for_product(line.product)
  104. is_permitted, reason = result.availability.is_purchase_permitted(
  105. line.quantity)
  106. if not is_permitted:
  107. # Create a more meaningful message to show on the basket page
  108. msg = _(
  109. "'%(title)s' is no longer available to buy (%(reason)s). "
  110. "Please adjust your basket to continue"
  111. ) % {
  112. 'title': line.product.get_title(),
  113. 'reason': reason}
  114. messages.append(msg)
  115. if messages:
  116. raise exceptions.FailedPreCondition(
  117. url=reverse('basket:summary'),
  118. messages=messages
  119. )
  120. def check_user_email_is_captured(self, request):
  121. if not request.user.is_authenticated() \
  122. and not self.checkout_session.get_guest_email():
  123. raise exceptions.FailedPreCondition(
  124. url=reverse('checkout:index'),
  125. message=_(
  126. "Please either sign in or enter your email address")
  127. )
  128. def check_shipping_data_is_captured(self, request):
  129. if not request.basket.is_shipping_required():
  130. # Even without shipping being required, we still need to check that
  131. # a shipping method code has been set.
  132. if not self.checkout_session.is_shipping_method_set(
  133. self.request.basket):
  134. raise exceptions.FailedPreCondition(
  135. url=reverse('checkout:shipping-method'),
  136. )
  137. return
  138. # Basket requires shipping: check address and method are captured and
  139. # valid.
  140. self.check_a_valid_shipping_address_is_captured()
  141. self.check_a_valid_shipping_method_is_captured()
  142. def check_a_valid_shipping_address_is_captured(self):
  143. # Check that shipping address has been completed
  144. if not self.checkout_session.is_shipping_address_set():
  145. raise exceptions.FailedPreCondition(
  146. url=reverse('checkout:shipping-address'),
  147. message=_("Please choose a shipping address")
  148. )
  149. # Check that the previously chosen shipping address is still valid
  150. shipping_address = self.get_shipping_address(
  151. basket=self.request.basket)
  152. if not shipping_address:
  153. raise exceptions.FailedPreCondition(
  154. url=reverse('checkout:shipping-address'),
  155. message=_("Your previously chosen shipping address is "
  156. "no longer valid. Please choose another one")
  157. )
  158. def check_a_valid_shipping_method_is_captured(self):
  159. # Check that shipping method has been set
  160. if not self.checkout_session.is_shipping_method_set(
  161. self.request.basket):
  162. raise exceptions.FailedPreCondition(
  163. url=reverse('checkout:shipping-method'),
  164. message=_("Please choose a shipping method")
  165. )
  166. # Check that a *valid* shipping method has been set
  167. shipping_address = self.get_shipping_address(
  168. basket=self.request.basket)
  169. shipping_method = self.get_shipping_method(
  170. basket=self.request.basket,
  171. shipping_address=shipping_address)
  172. if not shipping_method:
  173. raise exceptions.FailedPreCondition(
  174. url=reverse('checkout:shipping-method'),
  175. message=_("Your previously chosen shipping method is "
  176. "no longer valid. Please choose another one")
  177. )
  178. def check_payment_data_is_captured(self, request):
  179. # We don't collect payment data by default so we don't have anything to
  180. # validate here. If your shop requires forms to be submitted on the
  181. # payment details page, then override this method to check that the
  182. # relevant data is available. Often just enforcing that the preview
  183. # view is only accessible from a POST request is sufficient.
  184. pass
  185. # Re-usable skip conditions
  186. def skip_unless_basket_requires_shipping(self, request):
  187. # Check to see that a shipping address is actually required. It may
  188. # not be if the basket is purely downloads
  189. if not request.basket.is_shipping_required():
  190. raise exceptions.PassedSkipCondition(
  191. url=reverse('checkout:shipping-method')
  192. )
  193. def skip_unless_payment_is_required(self, request):
  194. # Check to see if payment is actually required for this order.
  195. shipping_address = self.get_shipping_address(request.basket)
  196. shipping_method = self.get_shipping_method(
  197. request.basket, shipping_address)
  198. if shipping_method:
  199. shipping_charge = shipping_method.calculate(request.basket)
  200. else:
  201. # It's unusual to get here as a shipping method should be set by
  202. # the time this skip-condition is called. In the absence of any
  203. # other evidence, we assume the shipping charge is zero.
  204. shipping_charge = D('0.00')
  205. total = self.get_order_totals(request.basket, shipping_charge)
  206. if total.excl_tax == D('0.00'):
  207. raise exceptions.PassedSkipCondition(
  208. url=reverse('checkout:preview')
  209. )
  210. # Helpers
  211. def get_context_data(self, **kwargs):
  212. # Use the proposed submission as template context data. Flatten the
  213. # order kwargs so they are easily available too.
  214. ctx = self.build_submission(**kwargs)
  215. ctx.update(kwargs)
  216. ctx.update(ctx['order_kwargs'])
  217. return ctx
  218. def build_submission(self, **kwargs):
  219. """
  220. Return a dict of data that contains everything required for an order
  221. submission. This includes payment details (if any).
  222. This can be the right place to perform tax lookups and apply them to
  223. the basket.
  224. """
  225. basket = kwargs.get('basket', self.request.basket)
  226. shipping_address = self.get_shipping_address(basket)
  227. shipping_method = self.get_shipping_method(
  228. basket, shipping_address)
  229. if not shipping_method:
  230. total = shipping_charge = None
  231. else:
  232. shipping_charge = shipping_method.calculate(basket)
  233. total = self.get_order_totals(
  234. basket, shipping_charge=shipping_charge)
  235. submission = {
  236. 'user': self.request.user,
  237. 'basket': basket,
  238. 'shipping_address': shipping_address,
  239. 'shipping_method': shipping_method,
  240. 'shipping_charge': shipping_charge,
  241. 'order_total': total,
  242. 'order_kwargs': {},
  243. 'payment_kwargs': {}}
  244. # Allow overrides to be passed in
  245. submission.update(kwargs)
  246. # Set guest email after overrides as we need to update the order_kwargs
  247. # entry.
  248. if (not submission['user'].is_authenticated() and
  249. 'guest_email' not in submission['order_kwargs']):
  250. email = self.checkout_session.get_guest_email()
  251. submission['order_kwargs']['guest_email'] = email
  252. return submission
  253. def get_shipping_address(self, basket):
  254. """
  255. Return the (unsaved) shipping address for this checkout session.
  256. If the shipping address was entered manually, then we instantiate a
  257. ``ShippingAddress`` model with the appropriate form data (which is
  258. saved in the session).
  259. If the shipping address was selected from the user's address book,
  260. then we convert the ``UserAddress`` to a ``ShippingAddress``.
  261. The ``ShippingAddress`` instance is not saved as sometimes you need a
  262. shipping address instance before the order is placed. For example, if
  263. you are submitting fraud information as part of a payment request.
  264. The ``OrderPlacementMixin.create_shipping_address`` method is
  265. responsible for saving a shipping address when an order is placed.
  266. """
  267. if not basket.is_shipping_required():
  268. return None
  269. addr_data = self.checkout_session.new_shipping_address_fields()
  270. if addr_data:
  271. # Load address data into a blank shipping address model
  272. return ShippingAddress(**addr_data)
  273. addr_id = self.checkout_session.shipping_user_address_id()
  274. if addr_id:
  275. try:
  276. address = UserAddress._default_manager.get(pk=addr_id)
  277. except UserAddress.DoesNotExist:
  278. # An address was selected but now it has disappeared. This can
  279. # happen if the customer flushes their address book midway
  280. # through checkout. No idea why they would do this but it can
  281. # happen. Checkouts are highly vulnerable to race conditions
  282. # like this.
  283. return None
  284. else:
  285. # Copy user address data into a blank shipping address instance
  286. shipping_addr = ShippingAddress()
  287. address.populate_alternative_model(shipping_addr)
  288. return shipping_addr
  289. def get_shipping_method(self, basket, shipping_address=None, **kwargs):
  290. """
  291. Return the selected shipping method instance from this checkout session
  292. The shipping address is passed as we need to check that the method
  293. stored in the session is still valid for the shipping address.
  294. """
  295. code = self.checkout_session.shipping_method_code(basket)
  296. methods = Repository().get_shipping_methods(
  297. basket=basket, user=self.request.user,
  298. shipping_addr=shipping_address, request=self.request)
  299. for method in methods:
  300. if method.code == code:
  301. return method
  302. def get_billing_address(self, shipping_address):
  303. """
  304. Return an unsaved instance of the billing address (if one exists)
  305. """
  306. if not self.checkout_session.is_billing_address_set():
  307. return None
  308. if self.checkout_session.is_billing_address_same_as_shipping():
  309. if shipping_address:
  310. address = BillingAddress()
  311. shipping_address.populate_alternative_model(address)
  312. return address
  313. addr_data = self.checkout_session.new_billing_address_fields()
  314. if addr_data:
  315. # Load address data into a blank billing address model
  316. return BillingAddress(**addr_data)
  317. addr_id = self.checkout_session.billing_user_address_id()
  318. if addr_id:
  319. try:
  320. user_address = UserAddress._default_manager.get(pk=addr_id)
  321. except UserAddress.DoesNotExist:
  322. # An address was selected but now it has disappeared. This can
  323. # happen if the customer flushes their address book midway
  324. # through checkout. No idea why they would do this but it can
  325. # happen. Checkouts are highly vulnerable to race conditions
  326. # like this.
  327. return None
  328. else:
  329. # Copy user address data into a blank shipping address instance
  330. billing_address = BillingAddress()
  331. user_address.populate_alternative_model(billing_address)
  332. return billing_address
  333. def get_order_totals(self, basket, shipping_charge, **kwargs):
  334. """
  335. Returns the total for the order with and without tax
  336. """
  337. return OrderTotalCalculator(self.request).calculate(
  338. basket, shipping_charge, **kwargs)