726 lines
22 KiB
Python

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,
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):
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})