| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892 |
- import logging
-
- from django.http import Http404, HttpResponseRedirect, HttpResponseBadRequest
- from django.core.urlresolvers import reverse
- from django.contrib import messages
- from django.contrib.sites.models import Site
- from django.core.exceptions import ObjectDoesNotExist
- from django.contrib.auth import login
- from django.db.models import get_model
- from django.utils.translation import ugettext as _
- from django.views.generic import DetailView, TemplateView, FormView, \
- DeleteView, UpdateView, CreateView
-
- from oscar.apps.shipping.methods import Free, NoShippingRequired
- from oscar.core.loading import get_class, get_classes
- ShippingAddressForm, GatewayForm = get_classes('checkout.forms', ['ShippingAddressForm', 'GatewayForm'])
- OrderTotalCalculator = get_class('checkout.calculators', 'OrderTotalCalculator')
- CheckoutSessionData = get_class('checkout.utils', 'CheckoutSessionData')
- pre_payment, post_payment = get_classes('checkout.signals', ['pre_payment', 'post_payment'])
- OrderNumberGenerator, OrderCreator = get_classes('order.utils', ['OrderNumberGenerator', 'OrderCreator'])
- UserAddressForm = get_class('address.forms', 'UserAddressForm')
- Repository = get_class('shipping.repository', 'Repository')
- AccountAuthView = get_class('customer.views', 'AccountAuthView')
- Dispatcher = get_class('customer.utils', 'Dispatcher')
- RedirectRequired, UnableToTakePayment, PaymentError = get_classes(
- 'payment.exceptions', ['RedirectRequired', 'UnableToTakePayment', 'PaymentError'])
- UnableToPlaceOrder = get_class('order.exceptions', 'UnableToPlaceOrder')
-
- Order = get_model('order', 'Order')
- ShippingAddress = get_model('order', 'ShippingAddress')
- CommunicationEvent = get_model('order', 'CommunicationEvent')
- PaymentEventType = get_model('order', 'PaymentEventType')
- PaymentEvent = get_model('order', 'PaymentEvent')
- UserAddress = get_model('address', 'UserAddress')
- Basket = get_model('basket', 'Basket')
- Email = get_model('customer', 'Email')
- CommunicationEventType = get_model('customer', 'CommunicationEventType')
-
- # Standard logger for checkout events
- logger = logging.getLogger('oscar.checkout')
-
-
- class CheckoutSessionMixin(object):
- """
- Mixin to provide common functionality shared between checkout views.
- """
-
- def dispatch(self, request, *args, **kwargs):
- self.checkout_session = CheckoutSessionData(request)
- return super(CheckoutSessionMixin, self).dispatch(request, *args, **kwargs)
-
- def get_shipping_address(self):
- """
- Return the current shipping address for this checkout session.
-
- This could either be a ShippingAddress model which has been
- pre-populated (not saved), or a UserAddress model which will
- need converting into a ShippingAddress model at submission
- """
- addr_data = self.checkout_session.new_shipping_address_fields()
- if addr_data:
- # Load address data into a blank address model
- return ShippingAddress(**addr_data)
- addr_id = self.checkout_session.user_address_id()
- if addr_id:
- try:
- return UserAddress._default_manager.get(pk=addr_id)
- except UserAddress.DoesNotExist:
- # This can happen if you reset all your tables and you still have
- # session data that refers to addresses that no longer exist
- pass
- return None
-
- def get_shipping_method(self, basket=None):
- method = self.checkout_session.shipping_method()
- if method:
- if not basket:
- basket = self.request.basket
- method.set_basket(basket)
- else:
- # We default to using free shipping
- method = Free()
- return method
-
- def get_order_totals(self, basket=None, shipping_method=None, **kwargs):
- """
- Returns the total for the order with and without tax (as a tuple)
- """
- calc = OrderTotalCalculator(self.request)
- if not basket:
- basket = self.request.basket
- if not shipping_method:
- shipping_method = self.get_shipping_method(basket)
- total_incl_tax = calc.order_total_incl_tax(basket, shipping_method, **kwargs)
- total_excl_tax = calc.order_total_excl_tax(basket, shipping_method, **kwargs)
- return total_incl_tax, total_excl_tax
-
- def get_context_data(self, **kwargs):
- """
- Assign common template variables to the context.
- """
- ctx = super(CheckoutSessionMixin, self).get_context_data(**kwargs)
- ctx['shipping_address'] = self.get_shipping_address()
-
- method = self.get_shipping_method()
- if method:
- ctx['shipping_method'] = method
- ctx['shipping_total_excl_tax'] = method.basket_charge_excl_tax()
- ctx['shipping_total_incl_tax'] = method.basket_charge_incl_tax()
-
- ctx['order_total_incl_tax'], ctx['order_total_excl_tax'] = self.get_order_totals()
-
- return ctx
-
-
- class IndexView(CheckoutSessionMixin, FormView):
- """
- First page of the checkout. We prompt user to either sign in, or
- to proceed as a guest (where we still collect their email address).
- """
- template_name = 'checkout/gateway.html'
- form_class = GatewayForm
-
- def get(self, request, *args, **kwargs):
- # We redirect immediately to shipping address stage if the user is signed in or
- # has already filled out the anonymous checkout form.
- if request.user.is_authenticated() or self.checkout_session.get_guest_email():
- return self.get_success_response()
- return super(IndexView, self).get(request, *args, **kwargs)
-
- def get_form_kwargs(self):
- kwargs = super(IndexView, self).get_form_kwargs()
- email = self.checkout_session.get_guest_email()
- if email:
- kwargs['initial'] = {
- 'username': email,
- 'options': 'new'
- }
- return kwargs
-
- def form_valid(self, form):
- if form.is_guest_checkout():
- email = form.cleaned_data['username']
- self.checkout_session.set_guest_email(email)
- else:
- user = form.get_user()
- login(self.request, user)
- return self.get_success_response()
-
- def get_success_response(self):
- return HttpResponseRedirect(self.get_success_url())
-
- def get_success_url(self):
- return reverse('checkout:shipping-address')
-
-
- # ================
- # SHIPPING ADDRESS
- # ================
-
-
- class ShippingAddressView(CheckoutSessionMixin, FormView):
- """
- Determine the shipping address for the order.
-
- The default behaviour is to display a list of addresses from the users's
- address book, from which the user can choose one to be their shipping address.
- They can add/edit/delete these USER addresses. This address will be
- automatically converted into a SHIPPING address when the user checks out.
-
- Alternatively, the user can enter a SHIPPING address directly which will be
- saved in the session and saved as a model when the order is sucessfully submitted.
- """
-
- template_name = 'checkout/shipping_address.html'
- form_class = ShippingAddressForm
-
- def get(self, request, *args, **kwargs):
- # Check that the user's basket is not empty
- if request.basket.is_empty:
- messages.error(request, _("You need to add some items to your basket to checkout"))
- return HttpResponseRedirect(reverse('basket:summary'))
-
- # Check that guests have entered an email address
- if not request.user.is_authenticated() and not self.checkout_session.get_guest_email():
- messages.error(request, _("Please either sign in or enter your email address"))
- return HttpResponseRedirect(reverse('checkout:index'))
-
- # 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():
- messages.info(request, _("Your basket does not require a shipping address to be submitted"))
- return HttpResponseRedirect(self.get_success_url())
-
- return super(ShippingAddressView, self).get(request, *args, **kwargs)
-
- def get_initial(self):
- return self.checkout_session.new_shipping_address_fields()
-
- def get_context_data(self, **kwargs):
- kwargs = super(ShippingAddressView, self).get_context_data(**kwargs)
- if self.request.user.is_authenticated():
- # Look up address book data
- kwargs['addresses'] = self.get_available_addresses()
- return kwargs
-
- def get_available_addresses(self):
- return UserAddress._default_manager.filter(user=self.request.user).order_by('-is_default_for_shipping')
-
- def post(self, request, *args, **kwargs):
- # Check if a shipping address was selected directly (eg no form was filled in)
- if self.request.user.is_authenticated and 'address_id' in self.request.POST:
- address = UserAddress._default_manager.get(pk=self.request.POST['address_id'])
- if 'action' in self.request.POST and self.request.POST['action'] == 'ship_to':
- # User has selected a previous address to ship to
- self.checkout_session.ship_to_user_address(address)
- return HttpResponseRedirect(self.get_success_url())
- elif 'action' in self.request.POST and self.request.POST['action'] == 'delete':
- address.delete()
- messages.info(self.request, "Address deleted from your address book")
- return HttpResponseRedirect(reverse('checkout:shipping-method'))
- else:
- return HttpResponseBadRequest()
- else:
- return super(ShippingAddressView, self).post(request, *args, **kwargs)
-
- def form_valid(self, form):
- # Store the address details in the session and redirect to next step
- self.checkout_session.ship_to_new_address(form.clean())
- return super(ShippingAddressView, self).form_valid(form)
-
- def get_success_url(self):
- return reverse('checkout:shipping-method')
-
-
- class UserAddressCreateView(CheckoutSessionMixin, CreateView):
- """
- Add a USER address to the user's addressbook.
-
- This is not the same as creating a SHIPPING Address, although if used for the order,
- it will be converted into a shipping address at submission-time.
- """
- template_name = 'checkout/user_address_form.html'
- form_class = UserAddressForm
-
- def get_context_data(self, **kwargs):
- kwargs = super(UserAddressCreateView, self).get_context_data(**kwargs)
- kwargs['form_url'] = reverse('checkout:user-address-create')
- return kwargs
-
- def form_valid(self, form):
- self.object = form.save(commit=False)
- self.object.user = self.request.user
- self.object.save()
- return self.get_success_response()
-
- def get_success_response(self):
- messages.info(self.request, _("Address saved"))
- # We redirect back to the shipping address page
- return HttpResponseRedirect(reverse('checkout:shipping-address'))
-
-
- class UserAddressUpdateView(CheckoutSessionMixin, UpdateView):
- """
- Update a user address
- """
- template_name = 'checkout/user_address_form.html'
- form_class = UserAddressForm
-
- def get_queryset(self):
- return UserAddress._default_manager.filter(user=self.request.user)
-
- def get_context_data(self, **kwargs):
- kwargs = super(UserAddressUpdateView, self).get_context_data(**kwargs)
- kwargs['form_url'] = reverse('checkout:user-address-update', args=(str(kwargs['object'].id),))
- return kwargs
-
- def get_success_url(self):
- messages.info(self.request, _("Address saved"))
- return reverse('checkout:shipping-address')
-
-
- class UserAddressDeleteView(CheckoutSessionMixin, DeleteView):
- """
- Delete an address from a user's addressbook.
- """
- template_name = 'checkout/user_address_delete.html'
-
- def get_queryset(self):
- return UserAddress._default_manager.filter(user=self.request.user)
-
- def get_success_url(self):
- messages.info(self.request, _("Address deleted"))
- return reverse('checkout:shipping-address')
-
-
- # ===============
- # Shipping method
- # ===============
-
-
- class ShippingMethodView(CheckoutSessionMixin, TemplateView):
- """
- View for allowing a user to choose a shipping method.
-
- Shipping methods are largely domain-specific and so this view
- will commonly need to be subclassed and customised.
-
- The default behaviour is to load all the available shipping methods
- using the shipping Repository. If there is only 1, then it is
- automatically selected. Otherwise, a page is rendered where
- the user can choose the appropriate one.
- """
- template_name = 'checkout/shipping_methods.html';
-
- def get(self, request, *args, **kwargs):
- # Check that the user's basket is not empty
- if request.basket.is_empty:
- messages.error(request, _("You need to add some items to your basket to checkout"))
- return HttpResponseRedirect(reverse('basket:summary'))
-
- # Check that shipping is required at all
- if not request.basket.is_shipping_required():
- self.checkout_session.use_shipping_method(NoShippingRequired().code)
- return self.get_success_response()
-
- # Check that shipping address has been completed
- if not self.checkout_session.is_shipping_address_set():
- messages.error(request, _("Please choose a shipping address"))
- return HttpResponseRedirect(reverse('checkout:shipping-address'))
-
- # Save shipping methods as instance var as we need them both here
- # and when setting the context vars.
- self._methods = self.get_available_shipping_methods()
- if len(self._methods) == 0:
- # No shipping methods available for given address
- messages.warning(request, _("Shipping is not available for your chosen address - please choose another"))
- return HttpResponseRedirect(reverse('checkout:shipping-address'))
- elif len(self._methods) == 1:
- # Only one shipping method - set this and redirect onto the next step
- self.checkout_session.use_shipping_method(self._methods[0].code)
- return self.get_success_response()
-
- # Must be more than one available shipping method, we present them to
- # the user to make a choice.
- return super(ShippingMethodView, self).get(request, *args, **kwargs)
-
- def get_context_data(self, **kwargs):
- kwargs = super(ShippingMethodView, self).get_context_data(**kwargs)
- kwargs['methods'] = self._methods
- return kwargs
-
- def get_available_shipping_methods(self):
- """
- Returns all applicable shipping method objects
- for a given basket.
- """
- # Shipping methods can depend on the user, the contents of the basket
- # and the shipping address. I haven't come across a scenario that doesn't
- # fit this system.
- return Repository().get_shipping_methods(self.request.user, self.request.basket,
- self.get_shipping_address())
-
- def post(self, request, *args, **kwargs):
- # Need to check that this code is valid for this user
- method_code = request.POST.get('method_code', None)
- is_valid = False
- for method in self.get_available_shipping_methods():
- if method.code == method_code:
- is_valid = True
- if not is_valid:
- messages.error(request, _("Your submitted shipping method is not permitted"))
- return HttpResponseRedirect(reverse('checkout:shipping-method'))
-
- # Save the code for the chosen shipping method in the session
- # and continue to the next step.
- self.checkout_session.use_shipping_method(method_code)
- return self.get_success_response()
-
- def get_success_response(self):
- return HttpResponseRedirect(reverse('checkout:payment-method'))
-
-
- # ==============
- # Payment method
- # ==============
-
-
- class PaymentMethodView(CheckoutSessionMixin, TemplateView):
- """
- View for a user to choose which payment method(s) they want to use.
-
- This would include setting allocations if payment is to be split
- between multiple sources.
- """
-
- def get(self, request, *args, **kwargs):
- # Check that the user's basket is not empty
- if request.basket.is_empty:
- messages.error(request, _("You need to add some items to your basket to checkout"))
- return HttpResponseRedirect(reverse('basket:summary'))
-
- # Check that shipping address has been completed
- if request.basket.is_shipping_required() and not self.checkout_session.is_shipping_address_set():
- messages.error(request, _("Please choose a shipping address"))
- return HttpResponseRedirect(reverse('checkout:shipping-address'))
-
- # Check that shipping method has been set
- if not self.checkout_session.is_shipping_method_set():
- messages.error(request, _("Please choose a shipping method"))
- return HttpResponseRedirect(reverse('checkout:shipping-method'))
-
- return self.get_success_response()
-
- def get_success_response(self):
- return HttpResponseRedirect(reverse('checkout:payment-details'))
-
-
- # ================
- # Order submission
- # ================
-
-
- class OrderPlacementMixin(CheckoutSessionMixin):
- """
- Mixin which provides functionality for placing orders.
- """
- # Any payment sources should be added to this list as part of the
- # _handle_payment method. If the order is placed successfully, then
- # they will be persisted.
- _payment_sources = None
-
- _payment_events = None
-
- # Default code for the email to send after successful checkout
- communication_type_code = 'ORDER_PLACED'
-
- def handle_order_placement(self, order_number, basket, total_incl_tax, total_excl_tax, **kwargs):
- """
- Write out the order models and return the appropriate HTTP response
-
- We deliberately pass the basket in here as the one tied to the request
- isn't necessarily the correct one to use in placing the order. This can
- happen when a basket gets frozen.
- """
- order = self.place_order(order_number, basket, total_incl_tax, total_excl_tax, **kwargs)
- basket.set_as_submitted()
- return self.handle_successful_order(order)
-
- def add_payment_source(self, source):
- if self._payment_sources is None:
- self._payment_sources = []
- self._payment_sources.append(source)
-
- def add_payment_event(self, event_type_name, amount):
- event_type, n = PaymentEventType.objects.get_or_create(name=event_type_name)
- if self._payment_events is None:
- self._payment_events = []
- event = PaymentEvent(event_type=event_type, amount=amount)
- self._payment_events.append(event)
-
- def handle_successful_order(self, order):
- """
- Handle the various steps required after an order has been successfully placed.
-
- Override this view if you want to perform custom actions when an
- order is submitted.
- """
- # Send confirmation message (normally an email)
- self.send_confirmation_message(order)
-
- # Flush all session data
- self.checkout_session.flush()
-
- # Save order id in session so thank-you page can load it
- self.request.session['checkout_order_id'] = order.id
-
- return HttpResponseRedirect(reverse('checkout:thank-you'))
-
- def place_order(self, order_number, basket, total_incl_tax, total_excl_tax, **kwargs):
- """
- Writes the order out to the DB including the payment models
- """
- shipping_address = self.create_shipping_address()
- shipping_method = self.get_shipping_method(basket)
- billing_address = self.create_billing_address(shipping_address)
-
- if 'status' not in kwargs:
- status = self.get_initial_order_status(basket)
- else:
- status = kwargs.pop('status')
-
- # Set guest email address for anon checkout. Some libraries (eg
- # PayPal) will pass this explicitly so we take care not to clobber.
- if not self.request.user.is_authenticated() and 'guest_email' not in kwargs:
- kwargs['guest_email'] = self.checkout_session.get_guest_email()
-
- order = OrderCreator().place_order(basket=basket,
- total_incl_tax=total_incl_tax,
- total_excl_tax=total_excl_tax,
- user=self.request.user,
- shipping_method=shipping_method,
- shipping_address=shipping_address,
- billing_address=billing_address,
- order_number=order_number,
- status=status,
- **kwargs)
- self.save_payment_details(order)
- return order
-
- def create_shipping_address(self):
- """
- Create and returns the shipping address for the current order.
-
- If the shipping address was entered manually, then we simply
- write out a ShippingAddress model with the appropriate form data. If
- the user is authenticated, then we create a UserAddress from this data
- too so it can be re-used in the future.
-
- If the shipping address was selected from the user's address book,
- then we convert the UserAddress to a ShippingAddress.
- """
- if not self.request.basket.is_shipping_required():
- return None
-
- addr_data = self.checkout_session.new_shipping_address_fields()
- addr_id = self.checkout_session.user_address_id()
- if addr_data:
- addr = self.create_shipping_address_from_form_fields(addr_data)
- self.create_user_address(addr_data)
- elif addr_id:
- addr = self.create_shipping_address_from_user_address(addr_id)
- else:
- raise AttributeError("No shipping address data found")
- return addr
-
- def create_shipping_address_from_form_fields(self, addr_data):
- """Creates a shipping address model from the saved form fields"""
- shipping_addr = ShippingAddress(**addr_data)
- shipping_addr.save()
- return shipping_addr
-
- def create_user_address(self, addr_data):
- """
- For signed-in users, we create a user address model which will go
- into their address book.
- """
- if self.request.user.is_authenticated():
- addr_data['user_id'] = self.request.user.id
- user_addr = UserAddress(**addr_data)
- # Check that this address isn't already in the db as we don't want
- # to fill up the customer address book with duplicate addresses
- try:
- UserAddress._default_manager.get(hash=user_addr.generate_hash())
- except ObjectDoesNotExist:
- user_addr.save()
-
- def create_shipping_address_from_user_address(self, addr_id):
- """Creates a shipping address from a user address"""
- address = UserAddress._default_manager.get(pk=addr_id)
- # Increment the number of orders to help determine popularity of orders
- address.num_orders += 1
- address.save()
-
- shipping_addr = ShippingAddress()
- address.populate_alternative_model(shipping_addr)
- shipping_addr.save()
- return shipping_addr
-
- def create_billing_address(self, shipping_address=None):
- """
- Saves any relevant billing data (eg a billing address).
- """
- return None
-
- def save_payment_details(self, order):
- """
- Saves all payment-related details. This could include a billing
- address, payment sources and any order payment events.
- """
- self.save_payment_events(order)
- self.save_payment_sources(order)
-
- def save_payment_events(self, order):
- """
- Saves any relevant payment events for this order
- """
- if not self._payment_events:
- return
- for event in self._payment_events:
- event.order = order
- event.save()
-
- def save_payment_sources(self, order):
- """
- Saves any payment sources used in this order.
-
- When the payment sources are created, the order model does not exist and
- so they need to have it set before saving.
- """
- if not self._payment_sources:
- return
- for source in self._payment_sources:
- source.order = order
- source.save()
-
- def get_initial_order_status(self, basket):
- return None
-
- def get_submitted_basket(self):
- basket_id = self.checkout_session.get_submitted_basket_id()
- return Basket._default_manager.get(pk=basket_id)
-
- def restore_frozen_basket(self):
- """
- Restores a frozen basket as the sole OPEN basket. Note that this also merges
- in any new products that have been added to a basket that has been created while payment.
- """
- try:
- fzn_basket = self.get_submitted_basket()
- except Basket.DoesNotExist:
- # Strange place. The previous basket stored in the session does
- # not exist.
- pass
- else:
- fzn_basket.thaw()
- if self.request.basket.id != fzn_basket.id:
- fzn_basket.merge(self.request.basket)
- self.request.basket = fzn_basket
-
- def send_confirmation_message(self, order, **kwargs):
- code = self.communication_type_code
- ctx = {'order': order,
- 'lines': order.lines.all(),}
-
- if not self.request.user.is_authenticated():
- path = reverse('customer:anon-order',
- kwargs={'order_number': order.number,
- 'hash': order.verification_hash()})
- site = Site.objects.get_current()
- ctx['status_url'] = 'http://%s%s' % (site.domain, path)
-
- try:
- event_type = CommunicationEventType.objects.get(code=code)
- except CommunicationEventType.DoesNotExist:
- # No event in database, attempt to find templates for this type
- messages = CommunicationEventType.objects.get_and_render(code, ctx)
- event_type = None
- else:
- # Create order event
- CommunicationEvent._default_manager.create(order=order, event_type=event_type)
- messages = event_type.get_messages(ctx)
-
- if messages and messages['body']:
- logger.info("Order #%s - sending %s messages", order.number, code)
- dispatcher = Dispatcher(logger)
- dispatcher.dispatch_order_messages(order, messages, event_type, **kwargs)
- else:
- logger.warning("Order #%s - no %s communication event type", order.number, code)
-
-
- class PaymentDetailsView(OrderPlacementMixin, TemplateView):
- """
- For taking the details of payment and creating the order
-
- The class is deliberately split into fine-grained methods, responsible for only one
- thing. This is to make it easier to subclass and override just one component of
- functionality.
-
- Almost all projects will need to subclass and customise this class.
- """
- template_name = 'checkout/payment_details.html'
- template_name_preview = 'checkout/preview.html'
- preview = False
-
- def get_template_names(self):
- return [self.template_name_preview] if self.preview else [self.template_name]
-
- def get_error_response(self):
- # Check that the user's basket is not empty
- if self.request.basket.is_empty:
- messages.error(self.request, _("You need to add some items to your basket to checkout"))
- return HttpResponseRedirect(reverse('basket:summary'))
- # Check that shipping address has been completed
- if self.request.basket.is_shipping_required() and not self.checkout_session.is_shipping_address_set():
- messages.error(self.request, _("Please choose a shipping address"))
- return HttpResponseRedirect(reverse('checkout:shipping-address'))
- # Check that shipping method has been set
- if not self.checkout_session.is_shipping_method_set():
- messages.error(self.request, _("Please choose a shipping method"))
- return HttpResponseRedirect(reverse('checkout:shipping-method'))
-
- def get(self, request, *args, **kwargs):
- error_response = self.get_error_response()
- if error_response:
- return error_response
- return super(PaymentDetailsView, self).get(request, *args, **kwargs)
-
- def post(self, request, *args, **kwargs):
- """
- This method is designed to be overridden by subclasses which will
- validate the forms from the payment details page. If the forms are valid
- then the method can call submit()
- """
- error_response = self.get_error_response()
-
- if error_response:
- return error_response
- if self.preview:
- # We use a custom parameter to indicate if this is an attempt to place an order.
- # Without this, we assume a payment form is being submitted from the
- # payment-details page
- if request.POST.get('action', '') == 'place_order':
- return self.submit(request.basket)
- return self.render_preview(request)
-
- # Posting to payment-details isn't the right thing to do
- return self.get(request, *args, **kwargs)
-
- def render_preview(self, request, **kwargs):
- """
- Show a preview of the order.
-
- If sensitive data was submitted on the payment details page, you will
- need to pass it back to the view here so it can be stored in hidden form
- inputs. This avoids ever writing the sensitive data to disk.
- """
- ctx = self.get_context_data()
- ctx.update(kwargs)
- return self.render_to_response(ctx)
-
- def can_basket_be_submitted(self, basket):
- for line in basket.lines.all():
- is_permitted, reason = line.product.is_purchase_permitted(self.request.user, line.quantity)
- if not is_permitted:
- return False, reason, reverse('basket:summary')
- return True, None, None
-
- def get_default_billing_address(self):
- """
- Return default billing address for user
-
- This is useful when the payment details view includes a billing address
- form - you can use this helper method to prepopulate the form.
-
- Note, this isn't used in core oscar as there is no billing address form
- by default.
- """
- if not self.request.user.is_authenticated():
- return None
- try:
- return self.request.user.addresses.get(is_default_for_billing=True)
- except UserAddress.DoesNotExist:
- return None
-
- def submit(self, basket, payment_kwargs=None, order_kwargs=None):
- """
- Submit a basket for order placement.
-
- The process runs as follows:
- * Generate an order number
- * Freeze the basket so it cannot be modified any more.
- * Attempt to take payment for the order
- - If payment is successful, place the order
- - If a redirect is required (eg PayPal, 3DSecure), redirect
- - If payment is unsuccessful, show an appropriate error message
- """
- if payment_kwargs is None:
- payment_kwargs = {}
- if order_kwargs is None:
- order_kwargs = {}
-
- # Next, check that basket isn't empty
- if basket.is_empty:
- messages.error(self.request, _("This order cannot be submitted as the basket is empty"))
- url = self.request.META.get('HTTP_REFERER', reverse('basket:summary'))
- return HttpResponseRedirect(url)
-
- # Domain-specific checks on the basket
- is_valid, reason, url = self.can_basket_be_submitted(basket)
- if not is_valid:
- messages.error(self.request, reason)
- return HttpResponseRedirect(url)
-
- # We generate the order number first as this will be used
- # in payment requests (ie before the order model has been
- # created). We also save it in the session for multi-stage
- # checkouts (eg where we redirect to a 3rd party site and place
- # the order on a different request).
- order_number = self.generate_order_number(basket)
- logger.info("Order #%s: beginning submission process for basket %d", order_number, basket.id)
-
- self.freeze_basket(basket)
- self.checkout_session.set_submitted_basket(basket)
-
- # Handle payment. Any payment problems should be handled by the
- # handle_payment method raise an exception, which should be caught
- # within handle_POST and the appropriate forms redisplayed.
- try:
- pre_payment.send_robust(sender=self, view=self)
- total_incl_tax, total_excl_tax = self.get_order_totals(basket)
- self.handle_payment(order_number, total_incl_tax, **payment_kwargs)
- post_payment.send_robust(sender=self, view=self)
- except RedirectRequired, e:
- # Redirect required (eg PayPal, 3DS)
- logger.info("Order #%s: redirecting to %s", order_number, e.url)
- return HttpResponseRedirect(e.url)
- except UnableToTakePayment, e:
- # Something went wrong with payment, need to show
- # error to the user. This type of exception is supposed
- # to set a friendly error message.
- msg = unicode(e)
- logger.warning("Order #%s: unable to take payment (%s) - restoring basket", order_number, msg)
- self.restore_frozen_basket()
- return self.render_to_response(self.get_context_data(error=msg))
- except PaymentError, e:
- # Something went wrong which wasn't anticipated.
- msg = unicode(e)
- logger.error("Order #%s: payment error (%s)", order_number, msg)
- self.restore_frozen_basket()
- return self.render_to_response(self.get_context_data(error="A problem occurred processing payment."))
-
- # If all is ok with payment, try and place order
- logger.info("Order #%s: payment successful, placing order", order_number)
- try:
- return self.handle_order_placement(order_number, basket,
- total_incl_tax, total_excl_tax,
- **order_kwargs)
- except UnableToPlaceOrder, e:
- logger.warning("Order #%s: unable to place order - %s",
- order_number, e)
- msg = unicode(e)
- self.restore_frozen_basket()
- return self.render_to_response(self.get_context_data(error=msg))
-
- def generate_order_number(self, basket):
- generator = OrderNumberGenerator()
- order_number = generator.order_number(basket)
- self.checkout_session.set_order_number(order_number)
- return order_number
-
- def freeze_basket(self, basket):
- # We freeze the basket to prevent it being modified once the payment
- # process has started. If your payment fails, then the basket will
- # need to be "unfrozen". We also store the basket ID in the session
- # so the it can be retrieved by multistage checkout processes.
- basket.freeze()
-
- def handle_payment(self, order_number, total, **kwargs):
- """
- Handle any payment processing.
-
- This method is designed to be overridden within your project. The
- default is to do nothing.
- """
- pass
-
- def get_context_data(self, **kwargs):
- # Return kwargs directly instead of using 'params' as in django's TemplateView
- ctx = super(PaymentDetailsView, self).get_context_data(**kwargs)
- ctx.update(kwargs)
- return ctx
-
-
- # =========
- # Thank you
- # =========
-
-
- class ThankYouView(DetailView):
- """
- Displays the 'thank you' page which summarises the order just submitted.
- """
- template_name = 'checkout/thank_you.html'
- context_object_name = 'order'
-
- def get_object(self):
- # We allow superusers to force an order thankyou page for testing
- order = None
- if self.request.user.is_superuser:
- if 'order_number' in self.request.GET:
- order = Order._default_manager.get(number=self.request.GET['order_number'])
- elif 'order_id' in self.request.GET:
- order = Order._default_manager.get(id=self.request.GET['orderid'])
-
- if not order:
- if 'checkout_order_id' in self.request.session:
- order = Order._default_manager.get(pk=self.request.session['checkout_order_id'])
- else:
- raise Http404(_("No order found"))
-
- return order
|