| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386 |
- from decimal import Decimal as D
-
- from django.contrib import messages
- from django import http
- from django.core.exceptions import ImproperlyConfigured
- from django.core.urlresolvers import reverse
- from django.utils.translation import ugettext_lazy as _
-
- from oscar.core.loading import get_model, get_class
- from . import exceptions
-
- Repository = get_class('shipping.repository', 'Repository')
- OrderTotalCalculator = get_class(
- 'checkout.calculators', 'OrderTotalCalculator')
- CheckoutSessionData = get_class(
- 'checkout.utils', 'CheckoutSessionData')
- ShippingAddress = get_model('order', 'ShippingAddress')
- BillingAddress = get_model('order', 'BillingAddress')
- UserAddress = get_model('address', 'UserAddress')
-
-
- class CheckoutSessionMixin(object):
- """
- Mixin to provide common functionality shared between checkout views.
-
- All checkout views subclass this mixin. It ensures that all relevant
- checkout information is available in the template context.
- """
-
- # A pre-condition is a condition that MUST be met in order for a view
- # to be available. If it isn't then the customer should be redirected
- # to a view *earlier* in the chain.
- # pre_conditions is a list of method names that get executed before the
- # normal flow of the view. Each method should check some condition has been
- # met. If not, then an exception is raised that indicates the URL the
- # customer will be redirected to.
-
- pre_conditions = None
-
- # A *skip* condition is a condition that MUST NOT be met in order for a
- # view to be available. If the condition is met, this means the view MUST
- # be skipped and the customer should be redirected to a view *later* in
- # the chain.
- # Skip conditions work similar to pre-conditions, and get evaluated after
- # pre-conditions have been evaluated.
- skip_conditions = None
-
- def dispatch(self, request, *args, **kwargs):
- # Assign the checkout session manager so it's available in all checkout
- # views.
- self.checkout_session = CheckoutSessionData(request)
-
- # Enforce any pre-conditions for the view.
- try:
- self.check_pre_conditions(request)
- except exceptions.FailedPreCondition as e:
- for message in e.messages:
- messages.warning(request, message)
- return http.HttpResponseRedirect(e.url)
-
- # Check if this view should be skipped
- try:
- self.check_skip_conditions(request)
- except exceptions.PassedSkipCondition as e:
- return http.HttpResponseRedirect(e.url)
-
- return super(CheckoutSessionMixin, self).dispatch(
- request, *args, **kwargs)
-
- def check_pre_conditions(self, request):
- pre_conditions = self.get_pre_conditions(request)
- for method_name in pre_conditions:
- if not hasattr(self, method_name):
- raise ImproperlyConfigured(
- "There is no method '%s' to call as a pre-condition" % (
- method_name))
- getattr(self, method_name)(request)
-
- def get_pre_conditions(self, request):
- """
- Return the pre-condition method names to run for this view
- """
- if self.pre_conditions is None:
- return []
- return self.pre_conditions
-
- def check_skip_conditions(self, request):
- skip_conditions = self.get_skip_conditions(request)
- for method_name in skip_conditions:
- if not hasattr(self, method_name):
- raise ImproperlyConfigured(
- "There is no method '%s' to call as a skip-condition" % (
- method_name))
- getattr(self, method_name)(request)
-
- def get_skip_conditions(self, request):
- """
- Return the skip-condition method names to run for this view
- """
- if self.skip_conditions is None:
- return []
- return self.skip_conditions
-
- # Re-usable pre-condition validators
-
- def check_basket_is_not_empty(self, request):
- if request.basket.is_empty:
- raise exceptions.FailedPreCondition(
- url=reverse('basket:summary'),
- message=_(
- "You need to add some items to your basket to checkout")
- )
-
- def check_basket_is_valid(self, request):
- """
- Check that the basket is permitted to be submitted as an order. That
- is, all the basket lines are available to buy - nothing has gone out of
- stock since it was added to the basket.
- """
- messages = []
- strategy = request.strategy
- for line in request.basket.all_lines():
- result = strategy.fetch_for_product(line.product)
- is_permitted, reason = result.availability.is_purchase_permitted(
- line.quantity)
- if not is_permitted:
- # Create a more meaningful message to show on the basket page
- msg = _(
- "'%(title)s' is no longer available to buy (%(reason)s). "
- "Please adjust your basket to continue"
- ) % {
- 'title': line.product.get_title(),
- 'reason': reason}
- messages.append(msg)
- if messages:
- raise exceptions.FailedPreCondition(
- url=reverse('basket:summary'),
- messages=messages
- )
-
- def check_user_email_is_captured(self, request):
- if not request.user.is_authenticated() \
- and not self.checkout_session.get_guest_email():
- raise exceptions.FailedPreCondition(
- url=reverse('checkout:index'),
- message=_(
- "Please either sign in or enter your email address")
- )
-
- def check_shipping_data_is_captured(self, request):
- if not request.basket.is_shipping_required():
- # Even without shipping being required, we still need to check that
- # a shipping method code has been set.
- if not self.checkout_session.is_shipping_method_set(
- self.request.basket):
- raise exceptions.FailedPreCondition(
- url=reverse('checkout:shipping-method'),
- )
- return
-
- # Basket requires shipping: check address and method are captured and
- # valid.
- self.check_a_valid_shipping_address_is_captured()
- self.check_a_valid_shipping_method_is_captured()
-
- def check_a_valid_shipping_address_is_captured(self):
- # Check that shipping address has been completed
- if not self.checkout_session.is_shipping_address_set():
- raise exceptions.FailedPreCondition(
- url=reverse('checkout:shipping-address'),
- message=_("Please choose a shipping address")
- )
-
- # Check that the previously chosen shipping address is still valid
- shipping_address = self.get_shipping_address(
- basket=self.request.basket)
- if not shipping_address:
- raise exceptions.FailedPreCondition(
- url=reverse('checkout:shipping-address'),
- message=_("Your previously chosen shipping address is "
- "no longer valid. Please choose another one")
- )
-
- def check_a_valid_shipping_method_is_captured(self):
- # Check that shipping method has been set
- if not self.checkout_session.is_shipping_method_set(
- self.request.basket):
- raise exceptions.FailedPreCondition(
- url=reverse('checkout:shipping-method'),
- message=_("Please choose a shipping method")
- )
-
- # Check that a *valid* shipping method has been set
- shipping_address = self.get_shipping_address(
- basket=self.request.basket)
- shipping_method = self.get_shipping_method(
- basket=self.request.basket,
- shipping_address=shipping_address)
- if not shipping_method:
- raise exceptions.FailedPreCondition(
- url=reverse('checkout:shipping-method'),
- message=_("Your previously chosen shipping method is "
- "no longer valid. Please choose another one")
- )
-
- def check_payment_data_is_captured(self, request):
- # We don't collect payment data by default so we don't have anything to
- # validate here. If your shop requires forms to be submitted on the
- # payment details page, then override this method to check that the
- # relevant data is available. Often just enforcing that the preview
- # view is only accessible from a POST request is sufficient.
- pass
-
- # Re-usable skip conditions
-
- def skip_unless_basket_requires_shipping(self, request):
- # Check to see that a shipping address is actually required. It may
- # not be if the basket is purely downloads
- if not request.basket.is_shipping_required():
- raise exceptions.PassedSkipCondition(
- url=reverse('checkout:shipping-method')
- )
-
- def skip_unless_payment_is_required(self, request):
- # Check to see if payment is actually required for this order.
- shipping_address = self.get_shipping_address(request.basket)
- shipping_method = self.get_shipping_method(
- request.basket, shipping_address)
- if shipping_method:
- shipping_charge = shipping_method.calculate(request.basket)
- else:
- # It's unusual to get here as a shipping method should be set by
- # the time this skip-condition is called. In the absence of any
- # other evidence, we assume the shipping charge is zero.
- shipping_charge = D('0.00')
- total = self.get_order_totals(request.basket, shipping_charge)
- if total.excl_tax == D('0.00'):
- raise exceptions.PassedSkipCondition(
- url=reverse('checkout:preview')
- )
-
- # Helpers
-
- def get_context_data(self, **kwargs):
- # Use the proposed submission as template context data. Flatten the
- # order kwargs so they are easily available too.
- ctx = self.build_submission(**kwargs)
- ctx.update(kwargs)
- ctx.update(ctx['order_kwargs'])
- return ctx
-
- def build_submission(self, **kwargs):
- """
- Return a dict of data that contains everything required for an order
- submission. This includes payment details (if any).
-
- This can be the right place to perform tax lookups and apply them to
- the basket.
- """
- basket = kwargs.get('basket', self.request.basket)
- shipping_address = self.get_shipping_address(basket)
- shipping_method = self.get_shipping_method(
- basket, shipping_address)
- if not shipping_method:
- total = shipping_charge = None
- else:
- shipping_charge = shipping_method.calculate(basket)
- total = self.get_order_totals(
- basket, shipping_charge=shipping_charge)
- submission = {
- 'user': self.request.user,
- 'basket': basket,
- 'shipping_address': shipping_address,
- 'shipping_method': shipping_method,
- 'shipping_charge': shipping_charge,
- 'order_total': total,
- 'order_kwargs': {},
- 'payment_kwargs': {}}
-
- # Allow overrides to be passed in
- submission.update(kwargs)
-
- # Set guest email after overrides as we need to update the order_kwargs
- # entry.
- if (not submission['user'].is_authenticated() and
- 'guest_email' not in submission['order_kwargs']):
- email = self.checkout_session.get_guest_email()
- submission['order_kwargs']['guest_email'] = email
- return submission
-
- def get_shipping_address(self, basket):
- """
- Return the (unsaved) shipping address for this checkout session.
-
- If the shipping address was entered manually, then we instantiate a
- ``ShippingAddress`` model with the appropriate form data (which is
- saved in the session).
-
- If the shipping address was selected from the user's address book,
- then we convert the ``UserAddress`` to a ``ShippingAddress``.
-
- The ``ShippingAddress`` instance is not saved as sometimes you need a
- shipping address instance before the order is placed. For example, if
- you are submitting fraud information as part of a payment request.
-
- The ``OrderPlacementMixin.create_shipping_address`` method is
- responsible for saving a shipping address when an order is placed.
- """
- if not basket.is_shipping_required():
- return None
-
- addr_data = self.checkout_session.new_shipping_address_fields()
- if addr_data:
- # Load address data into a blank shipping address model
- return ShippingAddress(**addr_data)
- addr_id = self.checkout_session.shipping_user_address_id()
- if addr_id:
- try:
- address = UserAddress._default_manager.get(pk=addr_id)
- except UserAddress.DoesNotExist:
- # An address was selected but now it has disappeared. This can
- # happen if the customer flushes their address book midway
- # through checkout. No idea why they would do this but it can
- # happen. Checkouts are highly vulnerable to race conditions
- # like this.
- return None
- else:
- # Copy user address data into a blank shipping address instance
- shipping_addr = ShippingAddress()
- address.populate_alternative_model(shipping_addr)
- return shipping_addr
-
- def get_shipping_method(self, basket, shipping_address=None, **kwargs):
- """
- Return the selected shipping method instance from this checkout session
-
- The shipping address is passed as we need to check that the method
- stored in the session is still valid for the shipping address.
- """
- code = self.checkout_session.shipping_method_code(basket)
- methods = Repository().get_shipping_methods(
- basket=basket, user=self.request.user,
- shipping_addr=shipping_address, request=self.request)
- for method in methods:
- if method.code == code:
- return method
-
- def get_billing_address(self, shipping_address):
- """
- Return an unsaved instance of the billing address (if one exists)
- """
- if not self.checkout_session.is_billing_address_set():
- return None
- if self.checkout_session.is_billing_address_same_as_shipping():
- if shipping_address:
- address = BillingAddress()
- shipping_address.populate_alternative_model(address)
- return address
-
- addr_data = self.checkout_session.new_billing_address_fields()
- if addr_data:
- # Load address data into a blank billing address model
- return BillingAddress(**addr_data)
- addr_id = self.checkout_session.billing_user_address_id()
- if addr_id:
- try:
- user_address = UserAddress._default_manager.get(pk=addr_id)
- except UserAddress.DoesNotExist:
- # An address was selected but now it has disappeared. This can
- # happen if the customer flushes their address book midway
- # through checkout. No idea why they would do this but it can
- # happen. Checkouts are highly vulnerable to race conditions
- # like this.
- return None
- else:
- # Copy user address data into a blank shipping address instance
- billing_address = BillingAddress()
- user_address.populate_alternative_model(billing_address)
- return billing_address
-
- def get_order_totals(self, basket, shipping_charge, **kwargs):
- """
- Returns the total for the order with and without tax
- """
- return OrderTotalCalculator(self.request).calculate(
- basket, shipping_charge, **kwargs)
|