import logging from datetime import datetime from django.conf import settings from django.utils import timezone from django import forms from django.shortcuts import render, reverse, redirect, get_object_or_404 from django.http import HttpResponseRedirect from django.urls import reverse, reverse_lazy from django.views.generic.base import 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 from django.forms import inlineformset_factory from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin from django.db.models import ( Exists, OuterRef, Prefetch, Subquery, Count, Sum, Avg, F, Q, Value, ExpressionWrapper, IntegerField ) from django.db.models.functions import Coalesce from accounts.models import User from accounts.utils import get_or_create_customer from accounts.forms import AddressForm from core.models import ( ProductCategory, Product, ProductPhoto, ProductVariant, ProductOption, Order, OrderLine, ShippingRate, Transaction, TrackingNumber, Coupon, SiteSettings ) from core import ( DiscountValueType, VoucherType, OrderStatus ) from .forms import ( ProductVariantUpdateForm, OrderLineFulfillForm, OrderLineFormset, OrderCancelForm, OrderTrackingFormset, CouponForm, ProductPhotoForm ) logger = logging.getLogger(__name__) class DashboardHomeView(LoginRequiredMixin, TemplateView): template_name = 'dashboard/dashboard_detail.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) today = timezone.localtime(timezone.now()).date() context['order_count'] = Order.objects.exclude( status=OrderStatus.DRAFT ).filter( created_at__date=today ).count() context['orders_unfulfilled'] = Order.objects.filter( status=OrderStatus.UNFULFILLED ).count() context['todays_sales'] = Order.objects.exclude( status=OrderStatus.DRAFT ).filter( created_at__date=today ).aggregate(total=Sum('total_amount'))['total'] return context class DashboardConfigView(TemplateView): template_name = 'dashboard/config.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['shipping_rate_list'] = ShippingRate.objects.all() return context class SiteSettingsUpdateView(UpdateView): model = SiteSettings context_object_name = 'settings' template_name = 'dashboard/settings_form.html' fields = '__all__' success_url = reverse_lazy('dashboard:config') success_message = 'Settings saved.' class CatalogView(ListView): model = ProductCategory context_object_name = 'category_list' template_name = 'dashboard/catalog.html' def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) context['uncategorized_products'] = Product.objects.filter( category=None ) context['option_list'] = ProductOption.objects.all() return context class StockView(ListView): model = ProductVariant context_object_name = 'variant_list' template_name = 'dashboard/stock.html' def get_queryset(self): object_list = ProductVariant.objects.filter( track_inventory=True ).prefetch_related('order_lines', 'product').annotate( total_in_warehouse=F('stock') + Coalesce(Sum('order_lines__quantity', filter=Q( order_lines__order__status=OrderStatus.UNFULFILLED) | Q( order_lines__order__status=OrderStatus.PARTIALLY_FULFILLED) ) - Sum('order_lines__quantity_fulfilled', filter=Q( order_lines__order__status=OrderStatus.UNFULFILLED) | Q( order_lines__order__status=OrderStatus.PARTIALLY_FULFILLED)), 0) ).order_by('product') return object_list class ShippingRateDetailView(LoginRequiredMixin, DetailView): model = ShippingRate context_object_name = 'rate' template_name = 'dashboard/rate_detail.html' class ShippingRateCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): model = ShippingRate context_object_name = 'rate' template_name = 'dashboard/rate_create_form.html' fields = '__all__' success_message = '%(name)s created.' class ShippingRateUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): model = ShippingRate context_object_name = 'rate' template_name = 'dashboard/rate_form.html' success_message = 'ShippingRate saved.' fields = '__all__' class ShippingRateDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView): model = ShippingRate context_object_name = 'rate' template_name = 'dashboard/rate_confirm_delete.html' success_message = 'ShippingRate deleted.' success_url = reverse_lazy('dashboard:config') class CouponListView(LoginRequiredMixin, ListView): model = Coupon template_name = 'dashboard/coupon_list.html' class CouponCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): model = Coupon template_name = 'dashboard/coupon_create_form.html' form_class = CouponForm success_message = '%(name)s created.' class CouponDetailView(LoginRequiredMixin, DetailView): model = Coupon template_name = 'dashboard/coupon_detail.html' class CouponUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): model = Coupon template_name = 'dashboard/coupon_form.html' success_message = '%(name)s saved.' form_class = CouponForm class CouponDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView): model = Coupon template_name = 'dashboard/coupon_confirm_delete.html' success_url = reverse_lazy('dashboard:coupon-list') success_message = 'Coupon deleted.' class OrderListView(LoginRequiredMixin, ListView): model = Order template_name = 'dashboard/order_list.html' paginate_by = 50 def get_queryset(self): query = self.request.GET.get('status') if query == 'unfulfilled': object_list = Order.objects.filter( Q(status=OrderStatus.UNFULFILLED) | Q(status=OrderStatus.PARTIALLY_FULFILLED) ).order_by( '-created_at' ).select_related( 'customer' ) else: object_list = Order.objects.order_by( '-created_at' ).select_related( 'customer' ) return object_list class OrderDetailView(LoginRequiredMixin, DetailView): model = Order template_name = 'dashboard/order_detail.html' def get_object(self): queryset = Order.objects.with_fulfillment_and_filter( self.kwargs.get(self.pk_url_kwarg) ).select_related( 'customer', 'billing_address', 'shipping_address' ).prefetch_related( 'lines__variant__product__productphoto_set' ) obj = queryset.get() return obj def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) return context class OrderFulfillView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): model = Order template_name = 'dashboard/order_fulfill.html' form_class = OrderLineFormset success_message = 'Order saved.' def form_valid(self, form): form.save() return HttpResponseRedirect(self.get_success_url()) def get_success_url(self): return reverse('dashboard:order-detail', kwargs={'pk': self.object.pk}) class OrderCancelView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): model = Order template_name = "dashboard/order_cancel_form.html" form_class = OrderCancelForm success_message = "Order canceled." initial = { 'status': OrderStatus.CANCELED } def form_valid(self, form): form.instance.add_stock() form.instance.save() return super().form_valid(form) def get_success_url(self): return reverse('dashboard:order-detail', kwargs={'pk': self.object.pk}) class OrderTrackingView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): model = Order template_name = "dashboard/order_tracking_form.html" form_class = OrderTrackingFormset success_message = "Order saved." def form_valid(self, form): form.save() return HttpResponseRedirect(self.get_success_url()) def get_success_url(self): return reverse('dashboard:order-detail', kwargs={'pk': self.object.pk}) class CategoryListView(ListView): model = ProductCategory context_object_name = 'category_list' template_name = 'dashboard/category_list.html' class CategoryCreateView(SuccessMessageMixin, CreateView): model = ProductCategory context_object_name = 'category' success_message = 'Category created.' template_name = 'dashboard/category_create_form.html' fields = '__all__' class CategoryDetailView(DetailView): model = ProductCategory context_object_name = 'category' template_name = 'dashboard/category_detail.html' class CategoryUpdateView(SuccessMessageMixin, UpdateView): model = ProductCategory context_object_name = 'category' success_message = 'Category saved.' template_name = 'dashboard/category_form.html' fields = '__all__' class CategoryDeleteView(SuccessMessageMixin, DeleteView): model = ProductCategory context_object_name = 'category' success_message = 'Category deleted.' template_name = 'dashboard/category_confirm_delete.html' success_url = reverse_lazy('dashboard:catalog') class ProductListView(LoginRequiredMixin, ListView): model = Product template_name = 'dashboard/product_list.html' ordering = 'sorting' # def get_queryset(self): # object_list = Product.objects.filter( # status=OrderStatus.UNFULFILLED # ).select_related( # 'customer' # ) # return object_list class ProductDetailView(LoginRequiredMixin, DetailView): model = Product template_name = 'dashboard/product_detail.html' def get_object(self): pk = self.kwargs.get(self.pk_url_kwarg) queryset = Product.objects.filter( pk=pk ).select_related( 'category', ).prefetch_related( 'variants', 'options', 'productphoto_set' ) obj = queryset.get() return obj class ProductCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): model = Product template_name = 'dashboard/product_create_form.html' fields = '__all__' success_message = '%(name)s created.' class ProductUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): model = Product template_name = 'dashboard/product_update_form.html' fields = '__all__' success_message = '%(name)s saved.' class ProductDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView): model = Product template_name = 'dashboard/product_confirm_delete.html' success_url = reverse_lazy('dashboard:catalog') success_message = 'Product deleted.' class ProductPhotoCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): model = ProductPhoto pk_url_kwarg = 'photo_pk' template_name = 'dashboard/prodphoto_create_form.html' form_class = ProductPhotoForm success_message = 'Photo added.' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['product'] = Product.objects.get(pk=self.kwargs['pk']) return context def form_valid(self, form): form.instance.product = Product.objects.get(pk=self.kwargs['pk']) return super().form_valid(form) def get_success_url(self): return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']}) class ProductPhotoDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView): model = ProductPhoto pk_url_kwarg = 'photo_pk' template_name = 'dashboard/prodphoto_confirm_delete.html' success_message = 'Photo deleted.' def get_success_url(self): return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']}) class ProductVariantCreateView(SuccessMessageMixin, CreateView): model = ProductVariant success_message = 'Variant created.' template_name = 'dashboard/variant_create_form.html' fields = [ 'name', 'sku', 'price', 'weight', 'track_inventory', 'stock', ] def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['product'] = Product.objects.get(pk=self.kwargs['pk']) return context def form_valid(self, form): form.instance.product = Product.objects.get(pk=self.kwargs['pk']) return super().form_valid(form) def get_success_url(self): return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']}) class ProductVariantUpdateView(SuccessMessageMixin, UpdateView): model = ProductVariant pk_url_kwarg = 'variant_pk' success_message = 'ProductVariant saved.' template_name = 'dashboard/variant_form.html' form_class = ProductVariantUpdateForm context_object_name = 'variant' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['product'] = Product.objects.get(pk=self.kwargs['pk']) return context def form_valid(self, form): form.instance.product = Product.objects.get(pk=self.kwargs['pk']) return super().form_valid(form) def get_success_url(self): return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']}) class ProductVariantDeleteView(SuccessMessageMixin, DeleteView): model = ProductVariant pk_url_kwarg = 'variant_pk' success_message = 'ProductVariant deleted.' template_name = 'dashboard/variant_confirm_delete.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['product'] = Product.objects.get(pk=self.kwargs['pk']) return context def get_success_url(self): return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']}) class ProductVariantStockUpdateView(LoginRequiredMixin, UpdateView): model = ProductVariant pk_url_kwarg = 'variant_pk' success_message = 'ProductVariant saved.' success_url = reverse_lazy('dashboard:stock') template_name = 'dashboard/variant_restock.html' fields = [ 'stock', ] context_object_name = 'variant' def get_queryset(self): queryset = ProductVariant.objects.annotate( total_in_warehouse=F('stock') + Coalesce(Sum('order_lines__quantity', filter=Q( order_lines__order__status=OrderStatus.UNFULFILLED) | Q( order_lines__order__status=OrderStatus.PARTIALLY_FULFILLED) ) - Sum('order_lines__quantity_fulfilled', filter=Q( order_lines__order__status=OrderStatus.UNFULFILLED) | Q( order_lines__order__status=OrderStatus.PARTIALLY_FULFILLED)), 0) ).prefetch_related('order_lines', 'product') return queryset def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['product'] = Product.objects.get(pk=self.kwargs['pk']) return context def form_valid(self, form): form.instance.product = Product.objects.get(pk=self.kwargs['pk']) return super().form_valid(form) class ProductOptionDetailView(LoginRequiredMixin, DetailView): model = ProductOption template_name = 'dashboard/option_detail.html' context_object_name = 'option' class ProductOptionCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): model = ProductOption template_name = 'dashboard/option_create_form.html' fields = [ 'name', 'options', 'products', ] success_message = '%(name)s created.' class ProductOptionUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): model = ProductOption success_message = 'Option saved.' template_name = 'dashboard/option_form.html' fields = [ 'name', 'options', 'products', ] context_object_name = 'option' success_url = reverse_lazy('dashboard:catalog') class ProductOptionDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView): model = ProductOption success_message = 'ProductOption deleted.' template_name = 'dashboard/option_confirm_delete.html' context_object_name = 'option' success_url = reverse_lazy('dashboard:catalog') class CustomerListView(LoginRequiredMixin, ListView): model = User template_name = 'dashboard/customer_list.html' paginate_by = 100 def get_queryset(self): object_list = User.objects.filter( Exists( Order.objects.filter(customer=OuterRef('pk')) ) | Q(is_staff=False) ).prefetch_related( 'orders' ).annotate( num_orders=Count('orders') ).order_by('first_name', 'last_name') return object_list class CustomerDetailView(LoginRequiredMixin, DetailView): model = User template_name = 'dashboard/customer_detail.html' context_object_name = 'customer' class CustomerUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): model = User template_name = 'dashboard/customer_form.html' context_object_name = 'customer' success_message = 'Customer saved.' fields = ( 'first_name', 'last_name', 'email', 'is_staff', 'addresses', 'default_shipping_address' ) def get_success_url(self): return reverse('dashboard:customer-detail', kwargs={'pk': self.object.pk})