import logging import requests import json import stripe 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.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 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 paypalcheckoutsdk.orders import OrdersCreateRequest, OrdersCaptureRequest from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment from accounts.models import User, Address from accounts.utils import get_or_create_customer from accounts.forms import ( AddressForm as AccountAddressForm, CustomerUpdateForm ) from core.models import ( ProductCategory, Product, ProductVariant, ProductOption, Order, Transaction, OrderLine, Coupon, ShippingRate, SiteSettings ) from core.forms import ShippingRateForm from core import OrderStatus, ShippingContainer from .forms import ( AddToCartForm, UpdateCartItemForm, OrderCreateForm, AddressForm, CouponApplyForm, ContactForm, CheckoutShippingForm, SubscriptionCreateForm ) from .cart import Cart from .payments import CaptureOrder logger = logging.getLogger(__name__) class CartView(TemplateView): template_name = 'storefront/cart_detail.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) cart = Cart(self.request) for i, item in enumerate(cart): item['update_quantity_form'] = UpdateCartItemForm( initial={ 'item_pk': i, 'quantity': item['quantity'] } ) context['cart'] = cart 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( request=request, item={ 'variant': cleaned_data.pop('variant'), 'quantity': cleaned_data.pop('quantity'), 'options': cleaned_data } ) return self.form_valid(form) else: return self.form_invalid(form) class CartUpdateProductView(SingleObjectMixin, FormView): model = Product form_class = UpdateCartItemForm 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.add( request=request, item={ 'variant': form.cleaned_data['item_pk'], 'quantity': form.cleaned_data['quantity'] }, update_quantity=form.cleaned_data['update'] ) 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(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() code = form.cleaned_data['code'].upper() try: coupon = Coupon.objects.get( code__iexact=code, valid_from__date__lte=today, valid_to__date__gte=today ) if coupon.is_valid: self.request.session['coupon_code'] = coupon.code except ObjectDoesNotExist: self.request.session['coupon_code'] = None 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__track_inventory=False) | Q(variants__track_inventory=True) & Q(variants__stock__gt=0) ).distinct() ) ) return object_list class ProductListView(ListView): model = Product template_name = 'storefront/product_list.html' ordering = 'sorting' queryset = Product.objects.filter( visible_in_listings=True, category__main_category=True ) 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) | Q( track_inventory=True, stock__gt=0 ) ) 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:checkout-shipping') def get_initial(self): user = self.request.user initial = None if user.is_authenticated and user.default_shipping_address: address = user.default_shipping_address initial = { 'full_name': address.first_name + ' ' + address.last_name, 'email': user.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 } 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_choices() 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( reverse('storefront:order-create') ) elif len(self.get_containers(request)) == 1: self.request.session['shipping_container'] = self.get_containers(request)[0] return HttpResponseRedirect( reverse('storefront:order-create') ) 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_cost(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') ) elif self.request.session.get('coupon_code'): address = self.request.session.get('shipping_address') coupon = Coupon.objects.get( code=self.request.session.get('coupon_code') ) try: user = User.objects.get(email=address['email']) except ObjectDoesNotExist: user = None if user in coupon.users.all(): del self.request.session['coupon_code'] messages.warning(request, 'Coupon already used.') return super().get(request, *args, **kwargs) def get_initial(self): cart = Cart(self.request) shipping_container = self.request.session.get( 'shipping_container' ).container try: shipping_cost = cart.get_shipping_cost(shipping_container) except Exception as e: logger.error('Could not get shipping information') raise shipping_cost = Decimal('0.00') initial = { 'total_amount': cart.get_total_price(), 'shipping_total': shipping_cost } if self.request.session.get('shipping_address'): a = self.request.session.get('shipping_address') user_info = { 'email': a['email'], 'first_name': a['first_name'], 'last_name': a['last_name'] } initial |= user_info return initial 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.get_subtotal_price_after_discount() form.instance.coupon_amount = cart.get_discount() form.instance.total_amount = cart.get_total_price_after_discount() form.instance.weight = cart.get_total_weight() shipping_address = self.request.session.get('shipping_address') shipping_container = self.request.session.get('shipping_container').container form.instance.customer, form.instance.shipping_address = get_or_create_customer(self.request, form, 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(shipping_container) data = response.result.__dict__['_dict'] self.request.session['order_id'] = self.object.pk return JsonResponse(data) @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') 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 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 CustomerAddressCreateView( UserPassesTestMixin, LoginRequiredMixin, CreateView ): model = Address template_name = 'storefront/address_create_form.html' form_class = AccountAddressForm permission_denied_message = 'Not authorized.' raise_exception = True def test_func(self): return self.request.user.pk == self.kwargs['pk'] def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['customer'] = User.objects.get(pk=self.kwargs['pk']) return context def form_valid(self, form): customer = User.objects.get(pk=self.kwargs['pk']) self.object = form.save() customer.addresses.add(self.object) return super().form_valid(form) def get_success_url(self): return reverse( 'storefront:customer-detail', kwargs={'pk': self.kwargs['pk']} ) class CustomerAddressUpdateView( UserPassesTestMixin, LoginRequiredMixin, UpdateView ): model = Address pk_url_kwarg = 'address_pk' template_name = 'storefront/address_form.html' form_class = AccountAddressForm permission_denied_message = 'Not authorized.' raise_exception = True def test_func(self): return self.request.user.pk == self.kwargs['pk'] def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['customer'] = User.objects.get(pk=self.kwargs['pk']) return context def get_success_url(self): return reverse('storefront:customer-detail', kwargs={'pk': self.kwargs['pk']}) 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 ContactFormView(FormView, SuccessMessageMixin): template_name = 'storefront/contact_form.html' form_class = ContactForm success_url = reverse_lazy('storefront:product-list') success_message = 'Message sent.' def form_valid(self, form): form.send_email() return super().form_valid(form) class SubscriptionCreateView(FormView): template_name = 'storefront/subscriptions.html' form_class = SubscriptionCreateForm success_url = reverse_lazy('storefront:payment-done') def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) context['STRIPE_API_KEY'] = settings.STRIPE_API_KEY context['product_list'] = Product.objects.filter( visible_in_listings=True ) return context class CreatePayment(View): def post(self, request, *args, **kwargs): stripe.api_key = settings.STRIPE_API_KEY intent = stripe.PaymentIntent.create( amount=2000, currency=settings.DEFAULT_CURRENCY, automatic_payment_methods={ 'enabled': True, }, ) return JsonResponse({ 'clientSecret': intent['client_secret'] })