You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
285 lines
11 KiB
285 lines
11 KiB
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 .models import (
|
|
Product, Color, Size, Order, OrderItem, GuestUser, Coupon, CouponUsage,
|
|
OrderStatus, ShippingAddress
|
|
)
|
|
|
|
@admin.register(Product)
|
|
class ProductAdmin(admin.ModelAdmin):
|
|
list_display = ("title", "ordering_value", "price", "cut")
|
|
|
|
@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(
|
|
'<div style="background-image: linear-gradient(to right, {} 50%, {} 50%); '
|
|
'width: 60px; height: 30px; border-radius: 15px; border: 1px solid #ddd;"></div>',
|
|
obj.colorHex, obj.secondary_hex_color
|
|
)
|
|
return format_html(
|
|
'<div style="background-color: {}; width: 60px; height: 30px; '
|
|
'border-radius: 15px; border: 1px solid #ddd;"></div>',
|
|
obj.colorHex
|
|
)
|
|
|
|
@admin.register(Size)
|
|
class SizeAdmin(admin.ModelAdmin):
|
|
list_display = ("name",)
|
|
|
|
class OrderItemInline(admin.TabularInline):
|
|
model = OrderItem
|
|
extra = 0
|
|
readonly_fields = ('product', 'quantity', 'color', 'size', 'price')
|
|
|
|
@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']
|
|
|
|
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(
|
|
"""
|
|
<div style="padding: 10px; background-color: #f9f9f9; border-radius: 4px;">
|
|
<strong>Street:</strong> {}<br>
|
|
{}
|
|
<strong>City:</strong> {}<br>
|
|
<strong>State:</strong> {}<br>
|
|
<strong>Postal Code:</strong> {}<br>
|
|
<strong>Country:</strong> {}
|
|
</div>
|
|
""",
|
|
obj.shipping_address.street_address,
|
|
f"<strong>Apartment:</strong> {obj.shipping_address.apartment}<br>" 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 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('prepare-all-orders/', self.admin_site.admin_view(self.prepare_all_orders), name='prepare_all_orders'),
|
|
path('prepare-order/<int:order_id>/', self.admin_site.admin_view(self.prepare_order), name='prepare_order'),
|
|
path('cancel-and-refund-order/<int:order_id>/', self.admin_site.admin_view(self.cancel_and_refund_order), name='cancel_and_refund_order'),
|
|
]
|
|
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',) |