Build out the shop domain: Cart/CartItem (one open cart per user per club), Order/OrderLine, Discount/AppliedDiscount, Payment and Invoice. Order and Invoice allocate a per-club, per-year sequential number (ORD-<year>-<seq> / INV-<year>-<seq>) via shared helpers, retrying on collision with the (club, number) unique constraint as the source of truth. Fixes found while testing: - Invoice had no number generator, so a second invoice in a club collided on the empty string and could never be created. - AppliedDiscount printed a "%" suffix even for fixed-amount discounts, and had no (order, discount) uniqueness, so a discount could be applied twice. Every model validates its club-scoped FKs (product/team/discount/order/season/ staff_role) against the owning club, and Member FKs against club membership. Register all models in the admin, with FK dropdowns scoped to the owning club. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
141 lines
4.5 KiB
Python
141 lines
4.5 KiB
Python
from django.contrib import admin
|
|
|
|
from .models import (
|
|
AppliedDiscount,
|
|
Cart,
|
|
CartItem,
|
|
Discount,
|
|
Invoice,
|
|
Order,
|
|
OrderLine,
|
|
Payment,
|
|
Product,
|
|
)
|
|
|
|
|
|
class ClubScopedFKMixin:
|
|
"""Restrict club-scoped FK dropdowns to the edited object's (or parent's) club.
|
|
|
|
``scoped_fk_fields`` names FKs whose target is a club-scoped model. The club
|
|
is taken from the object being edited (or, for inlines, the parent object).
|
|
Enforcement still lives in each model's ``clean()``; this just keeps invalid
|
|
options out of the dropdowns.
|
|
"""
|
|
|
|
scoped_fk_fields = ()
|
|
|
|
def get_form(self, request, obj=None, **kwargs):
|
|
request._club_obj = obj
|
|
return super().get_form(request, obj, **kwargs)
|
|
|
|
def get_formset(self, request, obj=None, **kwargs):
|
|
request._club_obj = obj
|
|
return super().get_formset(request, obj, **kwargs)
|
|
|
|
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
|
if db_field.name in self.scoped_fk_fields:
|
|
club_id = getattr(getattr(request, "_club_obj", None), "club_id", None)
|
|
if club_id is not None:
|
|
kwargs["queryset"] = db_field.related_model._default_manager.filter(club_id=club_id)
|
|
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
|
|
|
|
|
@admin.register(Product)
|
|
class ProductAdmin(ClubScopedFKMixin, admin.ModelAdmin):
|
|
scoped_fk_fields = ("season", "staff_role")
|
|
list_display = ["name", "product_type", "club", "is_active", "is_public"]
|
|
list_filter = ["club", "product_type", "is_active", "is_public"]
|
|
search_fields = ["name", "slug"]
|
|
prepopulated_fields = {"slug": ["name"]}
|
|
|
|
|
|
class CartItemInline(ClubScopedFKMixin, admin.TabularInline):
|
|
model = CartItem
|
|
scoped_fk_fields = ("product", "team")
|
|
extra = 0
|
|
raw_id_fields = ["beneficiary"]
|
|
|
|
|
|
@admin.register(Cart)
|
|
class CartAdmin(admin.ModelAdmin):
|
|
list_display = ["user", "status", "club"]
|
|
list_filter = ["club", "status"]
|
|
search_fields = ["user__email"]
|
|
raw_id_fields = ["user"]
|
|
inlines = [CartItemInline]
|
|
|
|
|
|
@admin.register(CartItem)
|
|
class CartItemAdmin(admin.ModelAdmin):
|
|
list_display = ["cart", "product", "quantity", "beneficiary"]
|
|
search_fields = ["product__name"]
|
|
raw_id_fields = ["cart", "product", "beneficiary", "team"]
|
|
|
|
|
|
class OrderLineInline(ClubScopedFKMixin, admin.TabularInline):
|
|
model = OrderLine
|
|
scoped_fk_fields = ("product", "team")
|
|
extra = 0
|
|
raw_id_fields = ["beneficiary"]
|
|
|
|
|
|
class AppliedDiscountInline(ClubScopedFKMixin, admin.TabularInline):
|
|
model = AppliedDiscount
|
|
scoped_fk_fields = ("discount",)
|
|
extra = 0
|
|
raw_id_fields = ["applied_by"]
|
|
|
|
|
|
class PaymentInline(admin.TabularInline):
|
|
model = Payment
|
|
extra = 0
|
|
|
|
|
|
@admin.register(Order)
|
|
class OrderAdmin(admin.ModelAdmin):
|
|
list_display = ["number", "purchaser", "status", "total", "club", "created"]
|
|
list_filter = ["club", "status"]
|
|
search_fields = ["number", "purchaser__first_name", "purchaser__last_name"]
|
|
raw_id_fields = ["purchaser"]
|
|
readonly_fields = ["number", "created", "modified"]
|
|
inlines = [OrderLineInline, AppliedDiscountInline, PaymentInline]
|
|
|
|
|
|
@admin.register(OrderLine)
|
|
class OrderLineAdmin(admin.ModelAdmin):
|
|
list_display = ["order", "product", "quantity", "line_total"]
|
|
search_fields = ["product__name", "order__number"]
|
|
raw_id_fields = ["order", "product", "beneficiary", "team"]
|
|
|
|
|
|
@admin.register(Discount)
|
|
class DiscountAdmin(admin.ModelAdmin):
|
|
list_display = ["name", "discount_type", "discount_amount", "club", "is_active"]
|
|
list_filter = ["club", "discount_type", "is_active"]
|
|
search_fields = ["name", "slug"]
|
|
prepopulated_fields = {"slug": ["name"]}
|
|
|
|
|
|
@admin.register(AppliedDiscount)
|
|
class AppliedDiscountAdmin(admin.ModelAdmin):
|
|
list_display = ["order", "discount", "discount_type", "discount_amount"]
|
|
search_fields = ["order__number", "discount__name"]
|
|
raw_id_fields = ["order", "discount", "applied_by"]
|
|
|
|
|
|
@admin.register(Payment)
|
|
class PaymentAdmin(admin.ModelAdmin):
|
|
list_display = ["order", "amount", "method", "status", "paid_at"]
|
|
list_filter = ["method", "status"]
|
|
search_fields = ["order__number", "reference"]
|
|
raw_id_fields = ["order"]
|
|
|
|
|
|
@admin.register(Invoice)
|
|
class InvoiceAdmin(ClubScopedFKMixin, admin.ModelAdmin):
|
|
scoped_fk_fields = ("order",)
|
|
list_display = ["number", "order", "club", "issued_at", "due_date"]
|
|
list_filter = ["club"]
|
|
search_fields = ["number", "order__number"]
|
|
readonly_fields = ["number", "issued_at"]
|