Understanding the Checkout Flow in Django Oscar 3.x

Blog / Django · July 5, 2014 · Updated June 10, 2026 · 9 min read
Understanding the Checkout Flow in Django Oscar 3.x

Django Oscar's checkout is a sequence of class-based views - one per step - that pass the in-progress order through the user's session until it is finally placed. In Oscar 3.x the flow runs in this order:

  1. Basket - review items and apply vouchers (basket app).
  2. Gateway / sign-in (IndexView) - log in, register, or continue as a guest.
  3. Shipping address (ShippingAddressView) - pick a saved address or enter a new one.
  4. Shipping method (ShippingMethodView) - choose from the methods your Repository returns.
  5. Payment method (PaymentMethodView) - optional step to pick how to pay.
  6. Payment details (PaymentDetailsView) - capture card/billing data and talk to the gateway.
  7. Preview (PaymentDetailsView with preview = True) - final confirmation.
  8. Place order - submit() freezes the basket, takes payment, creates the order, and shows the thank-you page.

Each step is a thin view backed by two mixins: CheckoutSessionMixin (reads and writes the partial choices) and, for the final view, OrderPlacementMixin (takes payment and writes the order). You override only the steps you care about. New to Oscar? Our guides on building an e-commerce shop with Django Oscar and customizing Oscar models, views and URLs cover the surrounding setup.

Key takeaways

  • The checkout is six logical steps - basket, shipping address, shipping method, payment, preview, place order - each backed by its own class-based view.
  • CheckoutSessionMixin holds the partial order in the session via CheckoutSessionData; nothing is written to the database until the order is placed.
  • PaymentDetailsView is where you integrate a real gateway. Override handle_payment() - by default Oscar takes no payment at all.
  • submit() (from OrderPlacementMixin) is the transactional core: it generates the order number, freezes the basket, calls handle_payment(), then handle_order_placement().
  • Customize by forking the checkout app (python manage.py oscar_fork_app checkout ...) and subclassing the view you need.
  • Oscar 3.2 needs modern Django (3.2-4.2 LTS) and Python 3.8-3.11 - upgrade legacy Oscar 1.x/2.x apps before extending checkout.

What are the steps in the Django Oscar checkout flow?

Every step maps to one URL and one view in oscar/apps/checkout/. The table below maps each step to its view and the most common reason you would override it.

Step URL View class What you typically customize
Basket /basket/ BasketView (basket app) Vouchers, offers, stock messaging
Gateway / sign-in /checkout/ IndexView Force login vs. guest-checkout rules
Shipping address /checkout/shipping-address/ ShippingAddressView Address form fields, country limits
Shipping method /checkout/shipping-method/ ShippingMethodView Custom Repository, carrier rates
Payment method /checkout/payment-method/ PaymentMethodView Choosing / splitting payment sources
Payment details /checkout/payment-details/ PaymentDetailsView handle_payment(), gateway SDK, forms
Preview /checkout/preview/ PaymentDetailsView (preview=True) Final review, terms acceptance
Place order / thank you /checkout/thank-you/ submit() -> ThankYouView handle_order_placement(), post-order hooks

Oscar skips steps that don't apply: if the basket needs no shipping, ShippingMethodView auto-selects NoShippingRequired and moves on; if only one shipping method exists, it is chosen automatically.

How does each step before payment work?

Gateway (IndexView). Logged-in users are redirected straight to the next step. Anonymous users are prompted to sign in, register, or continue as a guest. Even for guests, Oscar still collects an email address so it can send order confirmations.

Shipping address (ShippingAddressView). Logged-in users can reuse a saved address or enter a new one; guests always enter one. The submitted address is stashed in the session - it is only persisted as a ShippingAddress row when the order is finally placed.

Shipping method (ShippingMethodView). This view asks the shipping Repository for the methods available to the current basket and address. Out of the box Oscar ships only a Free method; you add real carriers by overriding Repository.get_available_shipping_methods() in your forked shipping app. If exactly one method is available it is auto-selected and the user is sent forward.

Payment method (PaymentMethodView). A hook for choosing how to pay (for example card vs. invoice) or splitting a payment across sources. By default it does nothing and redirects on to payment details.

Where do you plug in a real payment gateway?

PaymentDetailsView is the integration point, and every Oscar project must customize it - by default no payment is taken. This is the view that renders the payment form, talks to your gateway (Stripe, PayPal, Adyen, Braintree, a bank API, and so on), and records the result.

The two methods that matter:

  • handle_payment(order_number, total, **kwargs) - call your gateway here. On success, record a Source with add_payment_source() and a PaymentEvent with add_payment_event(). On failure, raise oscar.apps.payment.exceptions.PaymentError (or UnableToTakePayment); Oscar catches it and re-renders the payment page with the error.
  • handle_payment_details_submission() / render_preview() - validate the bankcard and billing-address forms, then render the preview page with those forms re-submitted inside a hidden <div>, so sensitive details are never written to disk.

For card data, prefer a tokenized, PCI-friendly flow: collect the card with the gateway's client-side SDK (for example Stripe Elements), submit only a token to handle_payment(), and let the gateway hold the card number. Community packages such as django-oscar-paypal and Stripe-based integrations follow this pattern. Our PayPal integration walkthrough shows a concrete payment back-end you can adapt.

# myshop/checkout/views.py
from django.conf import settings
import stripe

from oscar.apps.checkout import views
from oscar.apps.payment import models
from oscar.apps.payment.exceptions import PaymentError, UnableToTakePayment

stripe.api_key = settings.STRIPE_SECRET_KEY


class PaymentDetailsView(views.PaymentDetailsView):
    """Take payment with Stripe before the order is placed."""

    def handle_payment(self, order_number, total, **kwargs):
        # `total` is an oscar.core.prices.Price; charge the tax-inclusive amount.
        token = self.checkout_session.payment_token()  # stored earlier from Stripe.js
        try:
            charge = stripe.PaymentIntent.create(
                amount=int(total.incl_tax * 100),      # gateways use minor units
                currency=settings.OSCAR_DEFAULT_CURRENCY,
                payment_method=token,
                confirm=True,
                description=order_number,
            )
        except stripe.error.CardError as e:
            # Declined card -> message the shopper, stay on the payment page.
            raise UnableToTakePayment(e.user_message)
        except stripe.error.StripeError as e:
            # Anything else is unexpected.
            raise PaymentError(str(e))

        # Record the payment so it is linked to the order on placement.
        source_type, _ = models.SourceType.objects.get_or_create(name="Stripe")
        source = models.Source(
            source_type=source_type,
            currency=settings.OSCAR_DEFAULT_CURRENCY,
            amount_allocated=total.incl_tax,
            amount_debited=total.incl_tax,
            reference=charge.id,
        )
        self.add_payment_source(source)
        self.add_payment_event("Settled", total.incl_tax, reference=charge.id)

How does submit() and order placement work?

When the shopper clicks Place order on the preview page, the request hits PaymentDetailsView again. If your payment page posted forms, override handle_place_order_submission() to re-validate them, build the submission dict with build_submission(), and call submit(**submission):

  • build_submission() (from CheckoutSessionMixin) assembles everything the order needs: user, basket, shipping address, shipping method, shipping charge, order total, and any payment kwargs. It is also the right place to apply taxes once the address is known.
  • submit() (from OrderPlacementMixin) is the transactional core. It generates an order number, freezes the basket so it can no longer be edited, then calls handle_payment(). If payment raises, the basket is thawed and the user is returned to the payment page.
  • On success, handle_order_placement() writes the Order, ShippingAddress, line items, and payment records, sets the initial order status, fires the order-placed signal/email, and redirects to ThankYouView.

Because the basket is frozen before payment and only converted into an order after payment succeeds, you never end up with an order that has no payment, or a basket that gets charged twice.

How do you customize a checkout view?

Oscar 3.x uses the fork-the-app pattern. Instead of editing Oscar's source, you generate a local copy of the app, register it in INSTALLED_APPS in place of Oscar's, and subclass only the classes you need. The oscar_fork_app management command does the scaffolding - it creates the app package, an apps.py config that extends Oscar's, and copies the migrations so your fork owns the schema. Run the fork, drop your PaymentDetailsView subclass (shown above) into the new views.py, and swap the app in settings:

# Fork the checkout app into your project (Oscar 3.x).
python manage.py oscar_fork_app checkout myshop/
# myshop/checkout/apps.py
from oscar.apps.checkout import apps


class CheckoutConfig(apps.CheckoutConfig):
    # Just point Oscar's config at your package. Oscar's dynamic class loader
    # (get_class) now resolves checkout.views.PaymentDetailsView to YOUR subclass,
    # so get_urls() wires the forked view automatically - no manual registration.
    name = "myshop.checkout"


# settings.py - swap Oscar's checkout app for your fork.
INSTALLED_APPS = [
    # ... django + oscar core apps ...
    "myshop.checkout.apps.CheckoutConfig",   # replaces oscar.apps.checkout
    # ... remaining oscar apps ...
]

What's different in Django Oscar 3.x?

If you are following an older Oscar tutorial, a few things have moved on:

  • Runtime requirements. Oscar 3.2 targets Django 3.2, 4.0, 4.1 and 4.2 (LTS) and Python 3.8-3.11. Oscar 1.x/2.x (with their Python 2 / old-Django assumptions) are end-of-life - migrate before building new checkout logic.
  • App overriding. The old get_core_apps() helper and the Application URL class are gone. Apps are forked with oscar_fork_app and configured through AppConfig subclasses; URLs come from get_urls() on the app config.
  • Dynamic class loading. get_class('checkout.views', 'PaymentDetailsView') resolves your forked class automatically, so you rarely import Oscar's views directly.
  • Session data. Partial checkout state still lives in CheckoutSessionData (a namespaced wrapper over request.session) - shipping address id, method code, billing address, payment token, and so on.

The pipeline itself - basket, shipping, payment, preview, place order - has been stable for years, so the concepts here apply equally to a fresh Oscar 3.x store and to a 1.x store you are modernizing.

How is order status handled after checkout?

handle_order_placement() sets the order's initial status from OSCAR_INITIAL_ORDER_STATUS, and the allowed transitions are defined by OSCAR_ORDER_STATUS_PIPELINE in settings. From there you advance orders (for example Pending -> Shipped -> Complete) and react to changes with an EventHandler:

# settings.py
OSCAR_INITIAL_ORDER_STATUS = "Pending"
OSCAR_INITIAL_LINE_STATUS = "Pending"
OSCAR_ORDER_STATUS_PIPELINE = {
    "Pending": ("Being processed", "Cancelled"),
    "Being processed": ("Shipped", "Cancelled"),
    "Shipped": ("Complete",),
    "Complete": (),
    "Cancelled": (),
}

Keep payment capture aligned with these statuses - for example, settle a charge when an order moves to Shipped if you only authorized it during checkout.

At MicroPyramid we have built and modernized Django Oscar storefronts since the early Oscar 1.x days - wiring in Stripe, PayPal, and bank gateways and upgrading legacy checkouts to Oscar 3.x on current Django and Python. If you are planning an Oscar build or an upgrade, our Django development team can help you scope it.

Frequently Asked Questions

What are the checkout steps in Django Oscar?

The flow is basket, gateway/sign-in (IndexView), shipping address (ShippingAddressView), shipping method (ShippingMethodView), an optional payment method step (PaymentMethodView), payment details (PaymentDetailsView), a preview, and finally place order via submit(), which shows the thank-you page. Oscar auto-skips steps that don't apply, such as shipping for a digital-only basket.

Which view do I override to take a real payment?

Override PaymentDetailsView and implement handle_payment(order_number, total, **kwargs). By default Oscar takes no payment, so this method is where you call your gateway, then record the result with add_payment_source() and add_payment_event(). Raise PaymentError or UnableToTakePayment on failure and Oscar re-renders the payment page with the error.

How do I add Stripe or PayPal to Django Oscar?

Fork the checkout app, subclass PaymentDetailsView, and call the provider's SDK inside handle_payment(). For PayPal, the maintained django-oscar-paypal package handles Express Checkout and PayFlow; for Stripe, collect the card with Stripe Elements client-side and pass only a token to handle_payment() so card numbers never touch your server.

What is CheckoutSessionData used for?

CheckoutSessionData is a namespaced wrapper around request.session that stashes the shopper's partial choices - selected shipping address id, shipping method code, billing address, and any payment token - as they move through the steps. Nothing is written to the database until submit() succeeds, so an abandoned checkout leaves no order behind.

Does Django Oscar support guest checkout?

Yes. The gateway step (IndexView) lets anonymous shoppers continue as a guest instead of registering. Oscar still collects a guest email address so it can send the order confirmation, and you can tighten or relax this behaviour by overriding IndexView in your forked checkout app.

Which Django and Python versions does Django Oscar 3.x need?

Oscar 3.2 supports Django 3.2, 4.0, 4.1 and 4.2 (LTS) on Python 3.8 to 3.11; Django 3.1 and Python 3.7 support was dropped. If you are on Oscar 1.x or 2.x, upgrade the framework and runtime first - those versions assume older, end-of-life Django and Python and will block modern checkout integrations.

Share this article