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, PermissionRequiredMixin ) 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/home.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(LoginRequiredMixin, 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( LoginRequiredMixin, PermissionRequiredMixin, UpdateView ): permission_required = 'core.change_sitesettings' 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(LoginRequiredMixin, 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(LoginRequiredMixin, 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, PermissionRequiredMixin, SuccessMessageMixin, CreateView ): permission_required = 'core.add_shippingrate' model = ShippingRate context_object_name = 'rate' template_name = 'dashboard/rate/create_form.html' fields = '__all__' success_message = '%(name)s created.' class ShippingRateUpdateView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView ): permission_required = 'core.change_shippingrate' model = ShippingRate context_object_name = 'rate' template_name = 'dashboard/rate/form.html' success_message = 'ShippingRate saved.' fields = '__all__' class ShippingRateDeleteView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, DeleteView ): permission_required = 'core.delete_shippingrate' 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, PermissionRequiredMixin, SuccessMessageMixin, CreateView ): permission_required = 'core.add_coupon' 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, PermissionRequiredMixin, SuccessMessageMixin, UpdateView ): permission_required = 'core.change_coupon' model = Coupon template_name = 'dashboard/coupon/form.html' success_message = '%(name)s saved.' form_class = CouponForm class CouponDeleteView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, DeleteView ): permission_required = 'core.delete_coupon' 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, PermissionRequiredMixin, SuccessMessageMixin, UpdateView ): permission_required = 'core.cancel_order' 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(LoginRequiredMixin, ListView): model = ProductCategory context_object_name = 'category_list' template_name = 'dashboard/category/list.html' class CategoryCreateView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView ): permission_required = 'core.add_productcategory' model = ProductCategory context_object_name = 'category' success_message = 'Category created.' template_name = 'dashboard/category/create_form.html' fields = '__all__' class CategoryDetailView(LoginRequiredMixin, DetailView): model = ProductCategory context_object_name = 'category' template_name = 'dashboard/category/detail.html' class CategoryUpdateView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView ): permission_required = 'core.change_productcategory' model = ProductCategory context_object_name = 'category' success_message = 'Category saved.' template_name = 'dashboard/category/form.html' fields = '__all__' class CategoryDeleteView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, DeleteView ): permission_required = 'core.delete_productcategory' 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( 'options', 'productphoto_set', Prefetch( 'variants', queryset=ProductVariant.objects.all().order_by('sorting') ) ) obj = queryset.get() return obj class ProductCreateView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView ): permission_required = 'core.add_product' model = Product template_name = 'dashboard/product/create_form.html' fields = '__all__' success_message = '%(name)s created.' class ProductUpdateView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView ): permission_required = 'core.change_product' model = Product template_name = 'dashboard/product/form.html' fields = '__all__' success_message = '%(name)s saved.' class ProductDeleteView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, DeleteView ): permission_required = 'core.delete_product' model = Product template_name = 'dashboard/product/confirm_delete.html' success_url = reverse_lazy('dashboard:catalog') success_message = 'Product deleted.' class ProductPhotoCreateView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView ): permission_required = 'core.add_productphoto' model = ProductPhoto pk_url_kwarg = 'photo_pk' template_name = 'dashboard/product/photo_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, PermissionRequiredMixin, SuccessMessageMixin, DeleteView ): permission_required = 'core.delete_productphoto' 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( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView ): permission_required = 'core.add_productvariant' model = ProductVariant success_message = 'Variant created.' template_name = 'dashboard/variant/create_form.html' fields = [ 'name', 'sku', 'price', 'weight', 'visible_in_listings', '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( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView ): permission_required = 'core.change_productvariant' model = ProductVariant pk_url_kwarg = 'variant_pk' success_message = 'Variant 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( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, DeleteView ): permission_required = 'core.delete_productvariant' 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, PermissionRequiredMixin, UpdateView ): permission_required = 'core.change_productvariant' 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, PermissionRequiredMixin, SuccessMessageMixin, CreateView ): permission_required = 'core.add_productoption' model = ProductOption template_name = 'dashboard/option/create_form.html' fields = [ 'name', 'options', 'products', ] success_message = '%(name)s created.' class ProductOptionUpdateView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView ): permission_required = 'core.change_productoption' 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, PermissionRequiredMixin, SuccessMessageMixin, DeleteView ): permission_required = 'core.delete_productoption' 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, PermissionRequiredMixin, SuccessMessageMixin, UpdateView ): permission_required = 'accounts.change_user' 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})