from django.contrib import admin from django.shortcuts import render, redirect from django.utils.html import format_html from django.urls import path from django.http import HttpResponseRedirect from django import forms from django.db.models import Sum, Count, Avg from datetime import datetime, timedelta from django.utils import timezone from .models import ( Product, Color, Size, Order, OrderItem, GuestUser, Coupon, CouponUsage, OrderStatus, ShippingAddress ) class ShopAdminSite(admin.AdminSite): site_header = "Shop Administration" site_title = "Shop Admin Portal" index_title = "Welcome to Shop Administration" def index(self, request, extra_context=None): """Custom admin index view with dashboard""" # Calculate order statistics order_status_data = [] total_orders = Order.objects.count() total_revenue = Order.objects.aggregate(Sum('total_price'))['total_price__sum'] or 0 # Get data for each status for status_choice in OrderStatus.choices: status_code, status_label = status_choice orders_for_status = Order.objects.filter(status=status_code) count = orders_for_status.count() total_amount = orders_for_status.aggregate(Sum('total_price'))['total_price__sum'] or 0 avg_order_value = orders_for_status.aggregate(Avg('total_price'))['total_price__avg'] or 0 percentage = (count / total_orders * 100) if total_orders > 0 else 0 order_status_data.append({ 'status': status_code, 'label': status_label, 'count': count, 'total_amount': total_amount, 'avg_order_value': avg_order_value, 'percentage': percentage }) # Recent activity calculations now = timezone.now() today = now.date() week_ago = today - timedelta(days=7) month_ago = today - timedelta(days=30) orders_today = Order.objects.filter(date_ordered__date=today).count() orders_this_week = Order.objects.filter(date_ordered__date__gte=week_ago).count() orders_this_month = Order.objects.filter(date_ordered__date__gte=month_ago).count() orders_to_prepare = Order.objects.filter(status=OrderStatus.PAID).count() extra_context = extra_context or {} extra_context.update({ 'order_status_data': order_status_data, 'total_orders': total_orders, 'total_revenue': total_revenue, 'orders_today': orders_today, 'orders_this_week': orders_this_week, 'orders_this_month': orders_this_month, 'orders_to_prepare': orders_to_prepare, }) return render(request, 'admin/shop/dashboard.html', extra_context) # Create an instance of the custom admin site shop_admin_site = ShopAdminSite(name='shop_admin') @admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display = ("title", "ordering_value", "price", "cut") search_fields = ["title", "description"] # Enable search for autocomplete @admin.register(Color) class ColorAdmin(admin.ModelAdmin): list_display = ("color_preview", "name", "ordering", "colorHex", "secondary_hex_color") list_editable = ("ordering",) ordering = ["ordering"] search_fields = ["name"] list_per_page = 20 def color_preview(self, obj): if obj.secondary_hex_color: return format_html( '
', obj.colorHex, obj.secondary_hex_color ) return format_html( '
', obj.colorHex ) @admin.register(Size) class SizeAdmin(admin.ModelAdmin): list_display = ("name",) class OrderItemInline(admin.TabularInline): model = OrderItem extra = 1 # Show one extra row for adding new items autocomplete_fields = ['product'] # Enable product search fields = ('product', 'quantity', 'color', 'size', 'price') @admin.register(OrderItem) class OrderItemAdmin(admin.ModelAdmin): list_display = ('order', 'product', 'quantity', 'color', 'size', 'price', 'get_total_price') list_filter = ('product', 'color', 'size', 'order__status') search_fields = ('order__id', 'product__title', 'order__user__email', 'order__guest_user__email') autocomplete_fields = ['order', 'product'] list_editable = ('quantity', 'price') def get_total_price(self, obj): return obj.get_total_price() get_total_price.short_description = 'Total Price' get_total_price.admin_order_field = 'price' # Allows column to be sortable @admin.register(ShippingAddress) class ShippingAddressAdmin(admin.ModelAdmin): list_display = ('street_address', 'city', 'postal_code', 'country') search_fields = ('street_address', 'city', 'postal_code', 'country') class ChangeOrderStatusForm(forms.Form): _selected_action = forms.CharField(widget=forms.MultipleHiddenInput) status = forms.ChoiceField(choices=OrderStatus.choices, label="New Status") @admin.register(Order) class OrderAdmin(admin.ModelAdmin): list_display = ('id', 'get_email', 'date_ordered', 'status', 'total_price', 'get_shipping_address') inlines = [OrderItemInline] list_filter = ('status', 'payment_status') readonly_fields = ('shipping_address_details',) actions = ['change_order_status'] autocomplete_fields = ['user'] # Add this line for user search functionality search_fields = ['id', 'user__email', 'user__username', 'guest_user__email'] # Add this line def get_email(self, obj): if obj.guest_user: return obj.guest_user.email else: return obj.user.email get_email.short_description = 'Email' def get_shipping_address(self, obj): if obj.shipping_address: return f"{obj.shipping_address.street_address}, {obj.shipping_address.city}" return "No shipping address" get_shipping_address.short_description = 'Shipping Address' def shipping_address_details(self, obj): if obj.shipping_address: return format_html( """
Street: {}
{} City: {}
State: {}
Postal Code: {}
Country: {}
""", obj.shipping_address.street_address, f"Apartment: {obj.shipping_address.apartment}
" if obj.shipping_address.apartment else "", obj.shipping_address.city, obj.shipping_address.state, obj.shipping_address.postal_code, obj.shipping_address.country, ) return "No shipping address set" shipping_address_details.short_description = 'Shipping Address Details' fieldsets = ( (None, { 'fields': ('user', 'guest_user', 'status', 'payment_status', 'total_price') }), ('Shipping Information', { 'fields': ('shipping_address_details',), }), ('Payment Details', { 'fields': ('stripe_payment_intent_id', 'stripe_checkout_session_id', 'stripe_mode'), 'classes': ('collapse',) }), ('Discount Information', { 'fields': ('coupon', 'discount_amount'), 'classes': ('collapse',) }), ) def dashboard_view(self, request): """Dashboard view with order statistics""" # Calculate order statistics order_status_data = [] total_orders = Order.objects.count() total_revenue = Order.objects.aggregate(Sum('total_price'))['total_price__sum'] or 0 # Get data for each status for status_choice in OrderStatus.choices: status_code, status_label = status_choice orders_for_status = Order.objects.filter(status=status_code) count = orders_for_status.count() total_amount = orders_for_status.aggregate(Sum('total_price'))['total_price__sum'] or 0 avg_order_value = orders_for_status.aggregate(Avg('total_price'))['total_price__avg'] or 0 percentage = (count / total_orders * 100) if total_orders > 0 else 0 order_status_data.append({ 'status': status_code, 'label': status_label, 'count': count, 'total_amount': total_amount, 'avg_order_value': avg_order_value, 'percentage': percentage }) # Recent activity calculations now = timezone.now() today = now.date() week_ago = today - timedelta(days=7) month_ago = today - timedelta(days=30) orders_today = Order.objects.filter(date_ordered__date=today).count() orders_this_week = Order.objects.filter(date_ordered__date__gte=week_ago).count() orders_this_month = Order.objects.filter(date_ordered__date__gte=month_ago).count() orders_to_prepare = Order.objects.filter(status=OrderStatus.PAID).count() context = { 'title': 'Shop Dashboard', 'app_label': 'shop', 'opts': Order._meta, 'order_status_data': order_status_data, 'total_orders': total_orders, 'total_revenue': total_revenue, 'orders_today': orders_today, 'orders_this_week': orders_this_week, 'orders_this_month': orders_this_month, 'orders_to_prepare': orders_to_prepare, } return render(request, 'admin/shop/dashboard.html', context) def changelist_view(self, request, extra_context=None): # If 'show_preparation' parameter is in the request, show the preparation view if 'show_preparation' in request.GET: return self.preparation_view(request) # Otherwise show the normal change list extra_context = extra_context or {} paid_orders_count = Order.objects.filter(status=OrderStatus.PAID).count() extra_context['paid_orders_count'] = paid_orders_count return super().changelist_view(request, extra_context=extra_context) def preparation_view(self, request): """View for items that need to be prepared""" # Get paid orders orders = Order.objects.filter(status=OrderStatus.PAID).order_by('-date_ordered') # Group items by product, color, size items_by_variant = {} all_items = OrderItem.objects.filter(order__status=OrderStatus.PAID) for item in all_items: # Create a key for grouping items key = ( str(item.product.id), str(item.color.id) if item.color else 'none', str(item.size.id) if item.size else 'none' ) if key not in items_by_variant: items_by_variant[key] = { 'product': item.product, 'color': item.color, 'size': item.size, 'quantity': 0, 'orders': set() } items_by_variant[key]['quantity'] += item.quantity items_by_variant[key]['orders'].add(item.order.id) # Convert to list and sort items_list = list(items_by_variant.values()) items_list.sort(key=lambda x: x['product'].title) context = { 'title': 'Orders to Prepare', 'app_label': 'shop', 'opts': Order._meta, 'orders': orders, 'items': items_list, 'total_orders': orders.count(), 'total_items': sum(i['quantity'] for i in items_list) } return render( request, 'admin/shop/order/preparation_view.html', context ) def get_urls(self): urls = super().get_urls() custom_urls = [ path('dashboard/', self.admin_site.admin_view(self.dashboard_view), name='shop_order_dashboard'), ] return custom_urls + urls def prepare_all_orders(self, request): if request.method == 'POST': Order.objects.filter(status=OrderStatus.PAID).update(status=OrderStatus.PREPARED) self.message_user(request, "All orders have been marked as prepared.") return redirect('admin:shop_order_changelist') def prepare_order(self, request, order_id): if request.method == 'POST': order = Order.objects.get(id=order_id) order.status = OrderStatus.PREPARED order.save() self.message_user(request, f"Order #{order_id} has been marked as prepared.") return redirect('admin:shop_order_changelist') def cancel_and_refund_order(self, request, order_id): if request.method == 'POST': order = Order.objects.get(id=order_id) try: # Reuse the cancel_order logic from your views from .views import cancel_order cancel_order(request, order_id) self.message_user(request, f"Order #{order_id} has been cancelled and refunded.") except Exception as e: self.message_user(request, f"Error cancelling order: {str(e)}", level='ERROR') return redirect('admin:shop_order_changelist') def change_order_status(self, request, queryset): """Admin action to change the status of selected orders""" form = None if 'apply' in request.POST: form = ChangeOrderStatusForm(request.POST) if form.is_valid(): status = form.cleaned_data['status'] count = 0 for order in queryset: order.status = status order.save() count += 1 self.message_user(request, f"{count} orders have been updated to status '{OrderStatus(status).label}'.") return HttpResponseRedirect(request.get_full_path()) if not form: form = ChangeOrderStatusForm(initial={'_selected_action': request.POST.getlist('_selected_action')}) context = { 'title': 'Change Order Status', 'orders': queryset, 'form': form, 'action': 'change_order_status' } return render(request, 'admin/shop/order/change_status.html', context) change_order_status.short_description = "Change status for selected orders" class GuestUserOrderInline(admin.TabularInline): model = Order extra = 0 readonly_fields = ('date_ordered', 'total_price') can_delete = False show_change_link = True exclude = ('user',) # Exclude the user field from the inline display @admin.register(GuestUser) class GuestUserAdmin(admin.ModelAdmin): list_display = ('email', 'phone') inlines = [GuestUserOrderInline] @admin.register(Coupon) class CouponAdmin(admin.ModelAdmin): list_display = ('code', 'discount_amount', 'discount_percent', 'is_active', 'valid_from', 'valid_to', 'current_uses', 'max_uses') list_filter = ('is_active', 'valid_from', 'valid_to') search_fields = ('code', 'description') readonly_fields = ('current_uses', 'created_at', 'stripe_coupon_id') fieldsets = ( ('Basic Information', { 'fields': ('code', 'description', 'is_active') }), ('Discount', { 'fields': ('discount_amount', 'discount_percent') }), ('Validity', { 'fields': ('valid_from', 'valid_to', 'max_uses', 'current_uses') }), ('Stripe Information', { 'fields': ('stripe_coupon_id',), 'classes': ('collapse',) }), ) @admin.register(CouponUsage) class CouponUsageAdmin(admin.ModelAdmin): list_display = ('coupon', 'user', 'guest_email', 'order', 'used_at') list_filter = ('used_at',) search_fields = ('coupon__code', 'user__username', 'user__email', 'guest_email') readonly_fields = ('used_at',)