import logging import json from datetime import datetime from django.conf import settings from django.utils import timezone from django import forms from django.apps import apps from django.shortcuts import render, reverse, redirect, get_object_or_404 from django.http import JsonResponse, 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.views.decorators.http import require_POST 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 core.models import ( ProductCategory, Product, ProductPhoto, ProductVariant, ProductOption, Order, OrderLine, ShippingRate, Transaction, TrackingNumber, Coupon, WholesaleOrder, SiteSettings ) from core import ( DiscountValueType, VoucherType, OrderStatus ) from .forms import ( ProductVariantUpdateForm, OrderLineFulfillForm, OrderLineFormset, OrderCancelForm, OrderTrackingFormset, WholesaleOrderFulfillForm, WholesaleOrderCancelForm, 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): status = self.request.GET.get('status') query = self.request.GET.get('q') object_list = Order.objects.order_by( '-created_at' ).select_related('customer') if status == 'unfulfilled': object_list = object_list.filter( Q(status=OrderStatus.UNFULFILLED) | Q(status=OrderStatus.PARTIALLY_FULFILLED) ) if query: object_list = find_order_by_no_or_customer(object_list, query) return object_list.order_by('-created_at').select_related('customer') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["query"] = self.request.GET.get("q") return context 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', ).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', 'order_limit', ] 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') def sort(objs, order): for i, pk in enumerate(order): m = objs.get(pk=pk) m.sorting = i+1 yield m @require_POST def update_sorting(request): data = json.loads(request.body) model = apps.get_model('core', data['model_name']) objs = model.objects.filter( Q((data['filter'], data['filter_id'])) ).order_by('sorting') updated_objs = sort(objs, data['order']) model.objects.bulk_update(updated_objs, ['sorting']) return JsonResponse({'message': 'Sorting updated'}) def find_user_by_name_or_email(qs, query): for term in query.split(): qs = qs.filter( Q(first_name__icontains = term) | Q(last_name__icontains = term) | Q(email__icontains = term) ) return qs def find_order_by_no_or_customer(qs, query): for term in query.split(): qs = qs.filter( Q(pk__icontains = term) | Q(customer__first_name__icontains = term) | Q(customer__last_name__icontains = term) | Q(customer__email__icontains = term) ) return qs class CustomerListView(LoginRequiredMixin, ListView): model = User template_name = 'dashboard/customer/list.html' paginate_by = 100 def get_queryset(self): query = self.request.GET.get('q') 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') if query: object_list = find_user_by_name_or_email(object_list, query) return object_list def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["query"] = self.request.GET.get("q") return context 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', 'shipping_street_address_1', 'shipping_street_address_2', 'shipping_city', 'shipping_state', 'shipping_postal_code', ) def get_success_url(self): return reverse('dashboard:customer-detail', kwargs={'pk': self.object.pk}) class WholesaleOrderListView(LoginRequiredMixin, ListView): model = WholesaleOrder template_name = 'dashboard/wholesale_order/list.html' context_object_name = 'order_list' paginate_by = 50 class WholesaleOrderDetailView( LoginRequiredMixin, DetailView ): model = WholesaleOrder context_object_name = 'order' template_name = 'dashboard/wholesale_order/detail.html' class WholesaleOrderCancelView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView ): permission_required = 'core.cancel_order' model = WholesaleOrder context_object_name = 'order' template_name = 'dashboard/wholesale_order/cancel_form.html' form_class = WholesaleOrderCancelForm success_message = 'Wholesale Order canceled.' initial = { 'is_cancelled': True } def get_success_url(self): return reverse('dashboard:wholesale-order-detail', kwargs={'pk': self.object.pk}) class WholesaleOrderFulfillView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView ): permission_required = 'core.change_wholesaleorder' model = WholesaleOrder context_object_name = 'order' template_name = 'dashboard/wholesale_order/fulfill_form.html' form_class = WholesaleOrderFulfillForm success_message = 'Wholesale Order fulfilled.' initial = { 'is_fulfilled': True } def get_success_url(self): return reverse('dashboard:wholesale-order-detail', kwargs={'pk': self.object.pk})