Source code for oscar.apps.checkout.mixins

import logging

from django.contrib.sites.models import Site
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponseRedirect
from django.urls import NoReverseMatch, reverse

from oscar.apps.checkout.signals import post_checkout
from oscar.core.loading import get_class, get_model

OrderCreator = get_class("order.utils", "OrderCreator")
OrderDispatcher = get_class("order.utils", "OrderDispatcher")
CheckoutSessionMixin = get_class("checkout.session", "CheckoutSessionMixin")
BillingAddress = get_model("order", "BillingAddress")
ShippingAddress = get_model("order", "ShippingAddress")
OrderNumberGenerator = get_class("order.utils", "OrderNumberGenerator")
PaymentEventType = get_model("order", "PaymentEventType")
PaymentEvent = get_model("order", "PaymentEvent")
PaymentEventQuantity = get_model("order", "PaymentEventQuantity")
UserAddress = get_model("address", "UserAddress")
Basket = get_model("basket", "Basket")

# Standard logger for checkout events
logger = logging.getLogger("oscar.checkout")


[docs]class OrderPlacementMixin(CheckoutSessionMixin): """ Mixin which provides functionality for placing orders. Any view class which needs to place an order should use this mixin. """ # 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. We need to have the order instance before the # payment sources can be saved. _payment_sources = None # Any payment events should be added to this list as part of the # handle_payment method. _payment_events = None view_signal = post_checkout # Payment handling methods # ------------------------
[docs] def handle_payment(self, order_number, total, **kwargs): """ Handle any payment processing and record payment sources and events. This method is designed to be overridden within your project. The default is to do nothing as payment is domain-specific. This method is responsible for handling payment and recording the payment sources (using the add_payment_source method) and payment events (using add_payment_event) so they can be linked to the order when it is saved later on. """
[docs] def add_payment_source(self, source): """ Record a payment source for this order """ if self._payment_sources is None: self._payment_sources = [] self._payment_sources.append(source)
[docs] def add_payment_event(self, event_type_name, amount, reference=""): """ Record a payment event for creation once the order is placed """ event_type, __ = PaymentEventType.objects.get_or_create(name=event_type_name) # We keep a local cache of (unsaved) payment events if self._payment_events is None: self._payment_events = [] event = PaymentEvent(event_type=event_type, amount=amount, reference=reference) self._payment_events.append(event)
# Placing order methods # ---------------------
[docs] def generate_order_number(self, basket): """ Return a new order number """ return OrderNumberGenerator().order_number(basket)
[docs] def handle_order_placement( self, order_number, user, basket, shipping_address, shipping_method, shipping_charge, billing_address, order_total, surcharges=None, **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=order_number, user=user, basket=basket, shipping_address=shipping_address, shipping_method=shipping_method, shipping_charge=shipping_charge, order_total=order_total, billing_address=billing_address, surcharges=surcharges, **kwargs ) basket.submit() return self.handle_successful_order(order)
[docs] def place_order( self, order_number, user, basket, shipping_address, shipping_method, shipping_charge, order_total, billing_address=None, surcharges=None, **kwargs ): """ Writes the order out to the DB including the payment models """ # Create saved shipping address instance from passed in unsaved # instance shipping_address = self.create_shipping_address(user, shipping_address) # We pass the kwargs as they often include the billing address form # which will be needed to save a billing address. billing_address = self.create_billing_address( user, billing_address, shipping_address, **kwargs ) if "status" not in kwargs: # pylint: disable=assignment-from-none status = self.get_initial_order_status(basket) else: status = kwargs.pop("status") if "request" not in kwargs: request = getattr(self, "request", None) else: request = kwargs.pop("request") order = OrderCreator().place_order( user=user, order_number=order_number, basket=basket, shipping_address=shipping_address, shipping_method=shipping_method, shipping_charge=shipping_charge, total=order_total, billing_address=billing_address, status=status, request=request, surcharges=surcharges, **kwargs ) self.save_payment_details(order) return order
[docs] def create_shipping_address(self, user, shipping_address): """ Create and return the shipping address for the current order. Compared to self.get_shipping_address(), ShippingAddress is saved and makes sure that appropriate UserAddress exists. """ # For an order that only contains items that don't require shipping we # won't have a shipping address, so we have to check for it. if not shipping_address: return None shipping_address.save() if user.is_authenticated: self.update_address_book(user, shipping_address) return shipping_address
[docs] def update_address_book(self, user, addr): """ Update the user's address book based on the new shipping address """ try: user_addr = user.addresses.get(hash=addr.generate_hash()) except ObjectDoesNotExist: # Create a new user address user_addr = UserAddress(user=user) addr.populate_alternative_model(user_addr) if isinstance(addr, ShippingAddress): user_addr.num_orders_as_shipping_address += 1 if isinstance(addr, BillingAddress): user_addr.num_orders_as_billing_address += 1 user_addr.save()
# pylint: disable=unused-argument
[docs] def create_billing_address( self, user, billing_address=None, shipping_address=None, **kwargs ): """ Saves any relevant billing data (e.g. a billing address). """ if not billing_address: return None billing_address.save() if user.is_authenticated: self.update_address_book(user, billing_address) return billing_address
[docs] 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)
[docs] 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() for line in order.lines.all(): PaymentEventQuantity.objects.create( event=event, line=line, quantity=line.quantity )
[docs] 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()
# pylint: disable=unused-argument def get_initial_order_status(self, basket): return None # Post-order methods # ------------------
[docs] 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_order_placed_email(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 response = HttpResponseRedirect(self.get_success_url()) self.send_signal(self.request, response, order) return response
def send_signal(self, request, response, order): self.view_signal.send( sender=self, order=order, user=request.user, request=request, response=response, ) def get_success_url(self): return reverse("checkout:thank-you") def send_order_placed_email(self, order): extra_context = self.get_message_context(order) dispatcher = OrderDispatcher(logger=logger) dispatcher.send_order_placed_email_for_user(order, extra_context) def get_message_context(self, order): ctx = { "user": self.request.user, "order": order, "lines": order.lines.all(), "request": self.request, } # Attempt to add the order status URL to the email template ctx. try: if self.request.user.is_authenticated: path = reverse("customer:order", kwargs={"order_number": order.number}) else: path = reverse( "customer:anon-order", kwargs={ "order_number": order.number, "hash": order.verification_hash(), }, ) except NoReverseMatch: # We don't care that much if we can't resolve the URL pass else: ctx["status_path"] = path # status_url is deprecated, see https://github.com/django-oscar/django-oscar/issues/3826 site = Site.objects.get_current(self.request) ctx["status_url"] = "http://%s%s" % (site.domain, path) return ctx # Basket helpers # -------------- def get_submitted_basket(self): basket_id = self.checkout_session.get_submitted_basket_id() return Basket._default_manager.get(pk=basket_id)
[docs] def freeze_basket(self, basket): """ Freeze the basket so it can no longer be modified """ # 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()
[docs] 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) # Use same strategy as current request basket fzn_basket.strategy = self.request.basket.strategy self.request.basket = fzn_basket