import logging import requests import json import stripe from decimal import Decimal from django.conf import settings from django.utils import timezone from django.shortcuts import render, reverse, redirect, get_object_or_404 from django.urls import reverse_lazy from django.core.mail import EmailMessage from django.core.cache import cache from django.contrib.sites.models import Site from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.http import JsonResponse, HttpResponseRedirect from django.views.generic.base import View, RedirectView, TemplateView from django.views.generic.edit import ( FormView, CreateView, UpdateView, DeleteView, FormMixin ) from django.views.generic.detail import DetailView, SingleObjectMixin from django.views.generic.list import ListView from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import ( PermissionRequiredMixin, LoginRequiredMixin, UserPassesTestMixin ) from django.contrib.messages.views import SuccessMessageMixin from django.contrib import messages from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from django.forms.models import model_to_dict from django.db.models import ( Exists, OuterRef, Prefetch, Subquery, Count, Sum, Avg, F, Q, Value ) from measurement.measures import Weight from measurement.utils import guess from paypalcheckoutsdk.orders import OrdersCreateRequest, OrdersCaptureRequest from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment from moneyed import Money, USD from accounts.models import User from accounts.utils import get_or_create_customer from accounts.forms import CustomerUpdateForm, CustomerShippingAddressUpdateForm from core.models import ( ProductCategory, Product, ProductVariant, ProductOption, Order, Transaction, OrderLine, Coupon, ShippingRate, Subscription, SiteSettings, WholesaleOrder ) from core.forms import ShippingRateForm from core.shipping import get_shipping_cost from core import OrderStatus, ShippingContainer, TransactionStatus from .forms import ( AddToCartForm, CartItemUpdateForm, OrderCreateForm, AddressForm, CouponApplyForm, CheckoutShippingForm, SubscriptionForm, WholesaleOrderCreateForm ) from .cart import CartItem, Cart from .payments import CaptureOrder logger = logging.getLogger(__name__) stripe.api_key = settings.STRIPE_API_KEY class CartView(FormView): template_name = 'storefront/cart_detail.html' form_class = CartItemUpdateForm def get_success_url(self): return reverse('storefront:cart-detail') def post(self, request, *args, **kwargs): cart = Cart(request) form = self.get_form() if form.is_valid(): cart.update_item_quantity( form.cleaned_data['item_index'], form.cleaned_data['quantity'] ) return self.form_valid(form) else: return self.form_invalid(form) def form_valid(self, form): return super().form_valid(form) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['coupon_apply_form'] = CouponApplyForm() return context class CartAddProductView(SingleObjectMixin, FormView): model = Product form_class = AddToCartForm http_method_names = ['post'] def get_success_url(self): return reverse('storefront:cart-detail') def get_form(self, form_class=None): variants = self.get_object().variants.filter( Q(track_inventory=False) | Q( track_inventory=True, stock__gt=0 ) ) options = ProductOption.objects.filter(products__pk=self.get_object().pk) if form_class is None: form_class = self.get_form_class() return form_class(variants, options, **self.get_form_kwargs()) def post(self, request, *args, **kwargs): cart = Cart(request) form = self.get_form() if form.is_valid(): cleaned_data = form.cleaned_data cart.add_item( CartItem({ 'variant_pk': cleaned_data.pop('variant').pk, 'quantity': cleaned_data.pop('quantity'), 'options': cleaned_data }) ) return self.form_valid(form) else: return self.form_invalid(form) class CartItemUpdateView(FormView): form_class = CartItemUpdateForm http_method_names = ['post'] def get_success_url(self): return reverse('storefront:cart-detail') def post(self, request, *args, **kwargs): cart = Cart(request) form = self.get_form() if form.is_valid(): cart.update_item_quantity( form.cleaned_data['item_index'], form.cleaned_data['quantity'] ) return self.form_valid(form) else: return self.form_invalid(form) def form_valid(self, form): return super().form_valid(form) def cart_remove_product_view(request, pk): cart = Cart(request) cart.remove_item(pk) return redirect('storefront:cart-detail') class CouponApplyView(FormView): form_class = CouponApplyForm success_url = reverse_lazy('storefront:cart-detail') http_method_names = ['post'] def form_valid(self, form): today = timezone.localtime(timezone.now()).date() try: coupon = Coupon.objects.get(code=form.cleaned_data['code']) except Coupon.DoesNotExist: messages.warning(self.request, 'Coupon does not exist.') else: if coupon.is_valid: cart = Cart(self.request) cart.add_coupon(coupon) else: messages.warning(self.request, 'Coupon is invalid.') return super().form_valid(form) class ProductCategoryDetailView(DetailView): model = ProductCategory template_name = 'storefront/category_detail.html' context_object_name = 'category' def get_queryset(self): object_list = ProductCategory.objects.prefetch_related( Prefetch( 'product_set', queryset=Product.objects.filter( Q(visible_in_listings=True), Q(variants__visible_in_listings=True, variants__track_inventory=False) | Q(variants__visible_in_listings=True, variants__track_inventory=True, variants__stock__gt=0) ).prefetch_related( Prefetch( 'variants', queryset=ProductVariant.objects.filter( visible_in_listings=True ).order_by('sorting', 'weight') ) ).distinct() ) ) return object_list class ProductListView(ListView): model = Product template_name = 'storefront/product_list.html' ordering = ['category', 'sorting'] queryset = Product.objects.filter( Q(visible_in_listings=True, category__main_category=True), Q(variants__visible_in_listings=True, variants__track_inventory=False) | Q(variants__visible_in_listings=True, variants__track_inventory=True, variants__stock__gt=0) ).prefetch_related( Prefetch( 'variants', queryset=ProductVariant.objects.filter( visible_in_listings=True ).order_by('sorting', 'weight') ) ).distinct() class ProductDetailView(FormMixin, DetailView): model = Product template_name = 'storefront/product_detail.html' form_class = AddToCartForm def get_form(self, form_class=None): variants = self.object.variants.filter( Q(track_inventory=False, visible_in_listings=True) | Q( visible_in_listings=True, track_inventory=True, stock__gt=0 ) ).order_by('sorting', 'name') options = ProductOption.objects.filter(products__pk=self.object.pk) if form_class is None: form_class = self.get_form_class() return form_class(variants, options, **self.get_form_kwargs()) class CheckoutAddressView(FormView): template_name = 'storefront/checkout_address.html' form_class = AddressForm success_url = reverse_lazy('storefront:order-create') def get_initial(self): user = self.request.user initial = None if user.is_authenticated: initial = { 'full_name': user.first_name + ' ' + user.last_name, 'email': user.email, 'street_address_1': user.shipping_street_address_1, 'street_address_2': user.shipping_street_address_2, 'city': user.shipping_city, 'state': user.shipping_state, 'postal_code': user.shipping_postal_code } elif self.request.session.get('shipping_address'): address = self.request.session.get('shipping_address') initial = { 'full_name': address['first_name'] + ' ' + address['last_name'], 'email': address['email'], 'street_address_1': address['street_address_1'], 'street_address_2': address['street_address_2'], 'city': address['city'], 'state': address['state'], 'postal_code': address['postal_code'] } return initial def form_valid(self, form): # save address data to session cleaned_data = form.cleaned_data first_name, last_name = form.process_full_name( cleaned_data.get('full_name') ) address = { 'first_name': first_name, 'last_name': last_name, 'email': cleaned_data['email'], 'street_address_1': cleaned_data['street_address_1'], 'street_address_2': cleaned_data['street_address_2'], 'city': cleaned_data['city'], 'state': cleaned_data['state'], 'postal_code': cleaned_data['postal_code'] } self.request.session['shipping_address'] = address return super().form_valid(form) class CheckoutShippingView(FormView): template_name = 'storefront/checkout_shipping_form.html' form_class = CheckoutShippingForm success_url = reverse_lazy('storefront:order-create') containers = None def get_containers(self, request): if self.containers is None: cart = Cart(request) self.containers = cart.get_shipping_container() return self.containers def get(self, request, *args, **kwargs): if not self.request.session.get('shipping_address'): messages.warning(request, 'Please add a shipping address.') return HttpResponseRedirect( reverse('storefront:checkout-address') ) site_settings = cache.get('SiteSettings') cart = Cart(self.request) if len(self.get_containers(request)) == 0: self.request.session['shipping_container'] = site_settings.default_shipping_rate return HttpResponseRedirect(self.success_url) elif len(self.get_containers(request)) == 1: self.request.session['shipping_container'] = self.get_containers(request)[0] return HttpResponseRedirect(self.success_url) return super().get(request, *args, **kwargs) def get_form(self, form_class=None): cart = Cart(self.request) for container in self.get_containers(self.request): container.s_cost = cart.get_shipping_price(container.container) if form_class is None: form_class = self.get_form_class() return form_class(self.get_containers(self.request), **self.get_form_kwargs()) def form_valid(self, form): shipping_container = ShippingRate.objects.get( pk=form.cleaned_data.get('shipping_method') ) self.request.session['shipping_container'] = shipping_container return super().form_valid(form) class OrderCreateView(CreateView): model = Order template_name = 'storefront/order_form.html' form_class = OrderCreateForm success_url = reverse_lazy('storefront:payment-done') def get(self, request, *args, **kwargs): if not self.request.session.get('shipping_address'): messages.warning(request, 'Please add a shipping address.') return HttpResponseRedirect( reverse('storefront:checkout-address') ) cart = Cart(request) try: user = User.objects.get( email=request.session.get('shipping_address').get('email') ) except User.DoesNotExist: user = None if user: variants_ordered = ProductVariant.objects.filter( pk__in=cart.item_variant_pks, order_lines__order__customer=user, order_lines__order__status__in=[ OrderStatus.UNFULFILLED, OrderStatus.PARTIALLY_FULFILLED, OrderStatus.FULFILLED ] ).values("id", "order_limit").annotate( num_ordered=Sum("order_lines__quantity") ).order_by() for variant in variants_ordered: if variant['order_limit']: index, item = cart.get_item_by_pk(variant['id']) available = variant['order_limit'] - variant['num_ordered'] new_qty = item.quantity if item.quantity < available else available if new_qty and new_qty <= 0: cart.remove_item(index) else: cart.update_item_quantity(index, new_qty) if len(cart) == 0: return HttpResponseRedirect( reverse('storefront:product-list') ) if cart.coupon is not None: if user in cart.coupon.users.all(): cart.remove_coupon() messages.warning(request, 'Coupon already used.') return super().get(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['shipping_address'] = self.request.session.get('shipping_address') context['PAYPAL_CLIENT_ID'] = settings.PAYPAL_CLIENT_ID return context def form_valid(self, form): cart = Cart(self.request) form.instance.subtotal_amount = cart.subtotal_price form.instance.coupon = cart.coupon form.instance.coupon_amount = cart.discount_amount form.instance.total_amount = cart.total_price form.instance.weight = cart.total_weight shipping_container = cart.get_shipping_container() form.instance.shipping_total = cart.get_shipping_price(shipping_container) shipping_address = self.request.session.get('shipping_address') form.instance.shipping_first_name = shipping_address['first_name'] form.instance.shipping_last_name = shipping_address['last_name'] form.instance.shipping_street_address_1 = shipping_address['street_address_1'] form.instance.shipping_street_address_2 = shipping_address['street_address_2'] form.instance.shipping_city = shipping_address['city'] form.instance.shipping_state = shipping_address['state'] form.instance.shipping_postal_code = shipping_address['postal_code'] form.instance.customer = get_or_create_customer(self.request, shipping_address) form.instance.status = OrderStatus.DRAFT self.object = form.save() bulk_list = cart.build_bulk_list(self.object) objs = OrderLine.objects.bulk_create(bulk_list) response = cart.create_order() data = response.result.__dict__['_dict'] self.request.session['order_id'] = self.object.pk return JsonResponse(data) class FreeOrderCreateView(CreateView): http_method_names = ['post'] model = Order form_class = OrderCreateForm success_url = reverse_lazy('storefront:payment-done') def form_valid(self, form): cart = Cart(self.request) form.instance.subtotal_amount = cart.subtotal_price form.instance.coupon = cart.coupon form.instance.coupon_amount = cart.discount_amount form.instance.total_amount = cart.total_price form.instance.weight = cart.total_weight shipping_container = cart.get_shipping_container() form.instance.shipping_total = cart.get_shipping_price(shipping_container) shipping_address = self.request.session.get('shipping_address') form.instance.shipping_first_name = shipping_address['first_name'] form.instance.shipping_last_name = shipping_address['last_name'] form.instance.shipping_street_address_1 = shipping_address['street_address_1'] form.instance.shipping_street_address_2 = shipping_address['street_address_2'] form.instance.shipping_city = shipping_address['city'] form.instance.shipping_state = shipping_address['state'] form.instance.shipping_postal_code = shipping_address['postal_code'] form.instance.customer = get_or_create_customer(self.request, shipping_address) form.instance.status = OrderStatus.UNFULFILLED self.object = form.save() bulk_list = cart.build_bulk_list(self.object) OrderLine.objects.bulk_create(bulk_list) self.object.minus_stock() try: coupon = Coupon.objects.get( code=self.request.session.get('coupon_code') ) except ObjectDoesNotExist: coupon = None if coupon: self.object.coupon = coupon coupon.users.add(self.object.customer) transaction = Transaction.objects.get(order=self.object) transaction.status = TransactionStatus.COMPLETED transaction.save() cart.clear() return HttpResponseRedirect(self.get_success_url()) @csrf_exempt @require_POST def paypal_order_transaction_capture(request, transaction_id): if request.method == "POST": data = CaptureOrder().capture_order(transaction_id) cart = Cart(request) order = Order.objects.get(pk=request.session.get('order_id')) order.status = OrderStatus.UNFULFILLED order.minus_stock() try: coupon = Coupon.objects.get( code=request.session.get('coupon_code') ) except ObjectDoesNotExist: coupon = None if coupon: order.coupon = coupon coupon.users.add(order.customer) order.save() transaction = Transaction.objects.get(order=order) transaction.paypal_id = data['purchase_units'][0]['payments']['captures'][0]['id'] transaction.status = data['status'] transaction.save() cart.clear() logger.debug(f'\nPayPal Response data: {data}\n') return JsonResponse(data) else: return JsonResponse({'details': 'invalid request'}) class PaymentDoneView(TemplateView): template_name = 'storefront/payment_done.html' class PaymentCanceledView(TemplateView): template_name = 'storefront/payment_canceled.html' class CustomerDetailView(UserPassesTestMixin, LoginRequiredMixin, DetailView): model = User template_name = 'storefront/customer_detail.html' context_object_name = 'customer' permission_denied_message = 'Not authorized.' raise_exception = True def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['order_list'] = Order.objects.without_drafts().filter( customer=self.object ).prefetch_related('lines') subscriptions = [] if self.object.stripe_id is not None: subscriptions = stripe.Subscription.list(customer=self.object.stripe_id)['data'] context['subscriptions'] = subscriptions return context def test_func(self): return self.request.user.pk == self.get_object().pk class CustomerUpdateView(UserPassesTestMixin, LoginRequiredMixin, UpdateView): model = User template_name = 'storefront/customer_form.html' context_object_name = 'customer' form_class = CustomerUpdateForm permission_denied_message = 'Not authorized.' raise_exception = True def test_func(self): return self.request.user.pk == self.get_object().pk def get_success_url(self): return reverse( 'storefront:customer-detail', kwargs={'pk': self.object.pk} ) class CustomerShippingAddressUpdateView(UserPassesTestMixin, LoginRequiredMixin, UpdateView): model = User template_name = 'storefront/customer_shipping_address_form.html' context_object_name = 'customer' form_class = CustomerShippingAddressUpdateForm permission_denied_message = 'Not authorized.' raise_exception = True def test_func(self): return self.request.user.pk == self.get_object().pk def get_success_url(self): return reverse( 'storefront:customer-detail', kwargs={'pk': self.object.pk} ) class OrderDetailView(UserPassesTestMixin, LoginRequiredMixin, DetailView): model = Order template_name = 'storefront/order_detail.html' pk_url_kwarg = 'order_pk' permission_denied_message = 'Not authorized.' raise_exception = True def test_func(self): return self.request.user.pk == self.get_object().customer.pk def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['customer'] = User.objects.get(pk=self.kwargs['pk']) return context class AboutView(TemplateView): template_name = 'storefront/about.html' class FairTradeView(TemplateView): template_name = 'storefront/fairtrade.html' class ReviewListView(TemplateView): template_name = 'storefront/reviews.html' class TermsAndConditionsView(TemplateView): template_name = 'storefront/terms_and_conditions.html' class PrivacyPolicyView(TemplateView): template_name = 'storefront/privacy_policy.html' class SubscriptionAdView(TemplateView): template_name = 'storefront/subscription/ad.html' class SubscriptionFormView(FormView): template_name = 'storefront/subscription/form.html' form_class = SubscriptionForm success_url = reverse_lazy('storefront:subscription-address') def get_stripe_products(self): # id, name product_list = [{ 'id': product['id'], 'name': product['name'], 'created': product['created'], 'weight_per_item': product['metadata']['weight_per_item'], 'cost': product['metadata']['cost'], 'prices': [] } for product in stripe.Product.list(active=True)] # id, product, recurring.interval_count, recurring.interval, unit_amount price_list = [{ 'id': price['id'], 'product': price['product'], 'interval_count': price.recurring.interval_count, 'interval': price.recurring.interval, 'unit_amount': price['unit_amount'] } for price in stripe.Price.list(active=True)] for prod in product_list: prod['prices'] = list(filter( lambda p: True if p['product'] == prod['id'] else False, price_list )) return sorted(product_list, key=lambda p: p['created']) def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) context['stripe_products'] = self.get_stripe_products() context['product_list'] = Product.objects.filter( visible_in_listings=True, category__name='Coffee' ) return context def form_valid(self, form): self.request.session['subscription'] = { 'items': [{ 'price': form.cleaned_data['stripe_price_id'], 'quantity': form.cleaned_data['total_quantity'] }], 'metadata': { 'grind': form.cleaned_data['grind'], 'total_weight': form.cleaned_data['total_weight'], 'products_and_quantities': json.loads(form.cleaned_data['products_and_quantities']) } } return super().form_valid(form) class SubscriptionAddAddressView(CheckoutAddressView): template_name = 'storefront/subscription/address.html' success_url = reverse_lazy('storefront:subscription-create') class SubscriptionCreateView(SuccessMessageMixin, CreateView): model = Subscription success_message = 'Subscription created.' template_name = 'storefront/subscription/create_form.html' fields = [] def get(self, request, *args, **kwargs): if not self.request.session.get('subscription'): return HttpResponseRedirect(reverse('storefront:subscription-form')) return super().get(request, *args, **kwargs) def get_item_list(self): item_list = [{ 'product': Product.objects.get(pk=item['pk']), 'quantity': item['quantity'] } for item in self.request.session['subscription']['metadata']['products_and_quantities']] return item_list def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) subscription = self.request.session['subscription'] metadata = subscription['metadata'] price = stripe.Price.retrieve( subscription['items'][0].get('price'), expand=['product'] ) shipping_address = self.request.session.get('shipping_address') weight, unit = metadata['total_weight'].split(':') total_weight = guess(float(weight), unit, measures=[Weight]) subtotal_price = (Decimal(price.unit_amount) * subscription['items'][0]['quantity']) / 100 shipping_cost = get_shipping_cost( total_weight, shipping_address['postal_code'] ) total_price = subtotal_price + shipping_cost context['sub_cart'] = { 'items': self.get_item_list(), 'size': price.product.name, 'grind': metadata['grind'], 'schedule': f'Every {price.recurring.interval_count} / {price.recurring.interval}', 'subtotal_price': Money(subtotal_price, USD), 'shipping_cost': shipping_cost, 'total_price': total_price, 'total_weight': guess(float(weight), unit, measures=[Weight]) } context['shipping_address'] = shipping_address return context def get_line_items(self): line_items = self.object.items recurring = stripe.Price.retrieve( line_items[0].get('price') ).get('recurring') shipping_cost = get_shipping_cost( self.object.total_weight, self.object.shipping_postal_code ) * 100 line_items.append({ 'price_data': { 'currency': settings.DEFAULT_CURRENCY.lower(), 'unit_amount': int(shipping_cost), 'product_data': { 'name': 'Shipping' }, 'recurring': { 'interval': recurring.interval, 'interval_count': recurring.interval_count } }, 'quantity': 1 }) return line_items def get_success_url(self): session = stripe.checkout.Session.create( customer=self.object.customer.get_or_create_stripe_id(), success_url='http://' + Site.objects.get_current().domain + reverse( 'storefront:subscription-done' ) + '?session_id={CHECKOUT_SESSION_ID}', cancel_url='http://' + Site.objects.get_current().domain + reverse( 'storefront:subscription-create'), mode='subscription', line_items=self.get_line_items(), subscription_data={'metadata': self.object.format_metadata()} ) return session.url def form_valid(self, form): shipping_address = self.request.session.get('shipping_address') subscription = self.request.session.get('subscription') form.instance.shipping_first_name = shipping_address['first_name'] form.instance.shipping_last_name = shipping_address['last_name'] form.instance.shipping_street_address_1 = shipping_address['street_address_1'] form.instance.shipping_street_address_2 = shipping_address['street_address_2'] form.instance.shipping_city = shipping_address['city'] form.instance.shipping_state = shipping_address['state'] form.instance.shipping_postal_code = shipping_address['postal_code'] form.instance.customer = get_or_create_customer( self.request, shipping_address ) weight, unit = subscription['metadata']['total_weight'].split(':') form.instance.total_weight = guess( float(weight), unit, measures=[Weight] ) form.instance.items = subscription['items'] form.instance.metadata = subscription['metadata'] return super().form_valid(form) class SubscriptionDoneView(TemplateView): template_name = 'storefront/subscription/done.html' # stripe listen --forward-to localhost:8000/stripe-webhook/ @csrf_exempt @require_POST def stripe_webhook(request): # You can use webhooks to receive information about asynchronous payment events. # For more about our webhook events check out https://stripe.com/docs/webhooks. endpoint_secret = settings.STRIPE_WEBHOOK_SECRET payload = request.body sig_header = request.META['HTTP_STRIPE_SIGNATURE'] event = None try: event = stripe.Webhook.construct_event( payload, sig_header, endpoint_secret ) except ValueError as e: # Invalid payload logger.warning('Stripe Webhook: Invalid payload') return JsonResponse({'status': 'ERROR: Invalid payload'}) except stripe.error.SignatureVerificationError as e: # Invalid signature logger.warning('Stripe Webhook: Invalid signature') return JsonResponse({'status': 'ERROR: Invalid signature'}) if event.type == 'customer.subscription.created': try: subscription = Subscription.objects.get( pk=event.data.object['metadata'].get('subscription_pk') ) except Subscription.DoesNotExist: logger.warning('Subscription does not exist') raise else: subscription.stripe_id = event.data.object['id'] subscription.is_active = True subscription.save() if event.type == 'invoice.paid': # Continue to provision the subscription as payments continue to be made. # Store the status in your database and check when a user accesses your service. # This approach helps you avoid hitting rate limits. try: subscription = Subscription.objects.get( stripe_id=event.data.object['subscription'] ) except Subscription.DoesNotExist: logger.warning('Subscription does not exist') raise else: subscription.create_order(event.data.object) return JsonResponse({'status': 'success'}) class WholesaleOrderView(TemplateView): template_name = "storefront/wholesale_order.html" class WholesaleOrderCreateView( PermissionRequiredMixin, LoginRequiredMixin, CreateView, SuccessMessageMixin ): permission_required = "core.create_wholesaleorder" model = WholesaleOrder template_name = 'storefront/wholesale_order_create_form.html' form_class = WholesaleOrderCreateForm success_message = "Wholesale order placed. Thank you." def get_success_url(self): return reverse( 'storefront:wholesale-order-detail', kwargs={ 'pk': self.object.customer.pk, 'order_pk': self.object.pk } ) def form_valid(self, form): form.instance.customer = self.request.user return super().form_valid(form) class WholesaleOrderDetailView( PermissionRequiredMixin, LoginRequiredMixin, DetailView ): permission_required = "core.view_wholesaleorder" model = WholesaleOrder context_object_name = 'order' pk_url_kwarg = 'order_pk' template_name = 'storefront/wholesale_order_detail.html' def test_func(self): return self.request.user.pk == self.get_object().customer.pk def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['customer'] = User.objects.get(pk=self.kwargs['pk']) return context