Show year-scoped balances and breakdowns in admin
User list shows Konsumiert / Bezahlt / Offener Betrag for the current year. User detail page adds a BILANZ section with per-drink breakdowns (paid + free) so a quick look at the user page replaces what user_stats used to print. Drink list shows crates_purchased, bottles_sold, bottles_remaining, purchase_price_total, and balance, with a year filter. Detail page groups all derived values into German sections (Kästen, Flaschen, Menge, Einkauf, Pfand, Verkauf), replacing the drink_stats command. UserPayment admin gets a custom YearFilter (created_at__year is not admin-filterable directly). Donation/Payment get date filters and search. Group admin is unregistered (unused in this project). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+177
-15
@@ -3,13 +3,18 @@ from django.contrib import admin, messages
|
|||||||
from django.contrib.admin.utils import unquote
|
from django.contrib.admin.utils import unquote
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.admin import UserAdmin
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
|
||||||
|
admin.site.unregister(Group)
|
||||||
from django.http import Http404, HttpResponseRedirect
|
from django.http import Http404, HttpResponseRedirect
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.urls import path, reverse
|
from django.urls import path, reverse
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from gaehsnitz.models import Donation, Payment, Drink, Consumption
|
from django.db.models import Sum
|
||||||
|
|
||||||
|
from gaehsnitz.models import Donation, Payment, Drink, Consumption, UserPayment, current_year
|
||||||
from gaehsnitz.templatetags.money import euro
|
from gaehsnitz.templatetags.money import euro
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
@@ -20,6 +25,12 @@ class ConsumptionInline(admin.TabularInline):
|
|||||||
extra = 0
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
|
class UserPaymentInline(admin.TabularInline):
|
||||||
|
model = UserPayment
|
||||||
|
extra = 0
|
||||||
|
readonly_fields = ("created_at",)
|
||||||
|
|
||||||
|
|
||||||
class SetPinForm(forms.Form):
|
class SetPinForm(forms.Form):
|
||||||
pin = forms.CharField(
|
pin = forms.CharField(
|
||||||
label="Neue PIN (3 Ziffern)",
|
label="Neue PIN (3 Ziffern)",
|
||||||
@@ -36,7 +47,7 @@ class SetPinForm(forms.Form):
|
|||||||
|
|
||||||
@admin.register(User)
|
@admin.register(User)
|
||||||
class CustomUserAdmin(UserAdmin):
|
class CustomUserAdmin(UserAdmin):
|
||||||
list_display = ("username", "consumed_drinks_price")
|
list_display = ("username", "consumed_drinks_price", "paid_amount", "open_balance")
|
||||||
ordering = ("username",)
|
ordering = ("username",)
|
||||||
list_filter = []
|
list_filter = []
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
@@ -50,14 +61,62 @@ class CustomUserAdmin(UserAdmin):
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
("BALANCE", {"fields": ("consumed_drinks_price",)}),
|
(
|
||||||
|
"BILANZ",
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"consumed_drinks_price",
|
||||||
|
"paid_amount",
|
||||||
|
"open_balance",
|
||||||
|
"drinks_breakdown",
|
||||||
|
"free_drinks_breakdown",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
readonly_fields = ("consumed_drinks_price", "pin_status")
|
readonly_fields = (
|
||||||
inlines = (ConsumptionInline,)
|
"consumed_drinks_price",
|
||||||
|
"paid_amount",
|
||||||
|
"open_balance",
|
||||||
|
"pin_status",
|
||||||
|
"drinks_breakdown",
|
||||||
|
"free_drinks_breakdown",
|
||||||
|
)
|
||||||
|
inlines = (UserPaymentInline, ConsumptionInline)
|
||||||
|
|
||||||
|
@admin.display(description="Konsumiert")
|
||||||
def consumed_drinks_price(self, user: User):
|
def consumed_drinks_price(self, user: User):
|
||||||
return euro(user.consumed_drinks_price)
|
return euro(user.consumed_drinks_price)
|
||||||
|
|
||||||
|
@admin.display(description="Bezahlt")
|
||||||
|
def paid_amount(self, user: User):
|
||||||
|
return euro(user.paid_amount)
|
||||||
|
|
||||||
|
@admin.display(description="Offener Betrag")
|
||||||
|
def open_balance(self, user: User):
|
||||||
|
return euro(user.open_balance)
|
||||||
|
|
||||||
|
def _breakdown(self, user: User, for_free: bool):
|
||||||
|
if user.pk is None:
|
||||||
|
return "-"
|
||||||
|
rows = (
|
||||||
|
user.consumption_list.filter(for_free=for_free, drink__year=current_year())
|
||||||
|
.values("drink__name")
|
||||||
|
.annotate(amount=Sum("amount"))
|
||||||
|
.order_by("drink__name")
|
||||||
|
)
|
||||||
|
if not rows:
|
||||||
|
return "-"
|
||||||
|
return ", ".join(f"{r['amount']}x {r['drink__name']}" for r in rows)
|
||||||
|
|
||||||
|
@admin.display(description="Bezahlt")
|
||||||
|
def drinks_breakdown(self, user: User):
|
||||||
|
return self._breakdown(user, for_free=False)
|
||||||
|
|
||||||
|
@admin.display(description="Gratis")
|
||||||
|
def free_drinks_breakdown(self, user: User):
|
||||||
|
return self._breakdown(user, for_free=True)
|
||||||
|
|
||||||
@admin.display(description="PIN")
|
@admin.display(description="PIN")
|
||||||
def pin_status(self, user: User):
|
def pin_status(self, user: User):
|
||||||
status = "gesetzt" if user.pin else "nicht gesetzt"
|
status = "gesetzt" if user.pin else "nicht gesetzt"
|
||||||
@@ -104,10 +163,39 @@ class CustomUserAdmin(UserAdmin):
|
|||||||
return TemplateResponse(request, "admin/gaehsnitz/user/set_pin.html", context)
|
return TemplateResponse(request, "admin/gaehsnitz/user/set_pin.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
class YearFilter(admin.SimpleListFilter):
|
||||||
|
title = "Jahr"
|
||||||
|
parameter_name = "year"
|
||||||
|
field_name = "created_at"
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
years = model_admin.model.objects.dates(self.field_name, "year", order="DESC")
|
||||||
|
return [(y.year, str(y.year)) for y in years]
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
if self.value():
|
||||||
|
return queryset.filter(**{f"{self.field_name}__year": self.value()})
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(UserPayment)
|
||||||
|
class UserPaymentAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("created_at", "user", "amount", "method", "note")
|
||||||
|
list_filter = ("method", YearFilter)
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
search_fields = ("user__username", "note")
|
||||||
|
|
||||||
|
@admin.display(ordering="amount")
|
||||||
|
def amount(self, payment: UserPayment):
|
||||||
|
return euro(payment.amount)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Donation)
|
@admin.register(Donation)
|
||||||
class DonationAdmin(admin.ModelAdmin):
|
class DonationAdmin(admin.ModelAdmin):
|
||||||
list_display = ("date", "amount", "note")
|
list_display = ("date", "amount", "note")
|
||||||
|
list_filter = ("date",)
|
||||||
ordering = ("-date",)
|
ordering = ("-date",)
|
||||||
|
search_fields = ("note",)
|
||||||
|
|
||||||
@admin.display(ordering="amount")
|
@admin.display(ordering="amount")
|
||||||
def amount(self, donation: Donation):
|
def amount(self, donation: Donation):
|
||||||
@@ -117,7 +205,9 @@ class DonationAdmin(admin.ModelAdmin):
|
|||||||
@admin.register(Payment)
|
@admin.register(Payment)
|
||||||
class PaymentAdmin(admin.ModelAdmin):
|
class PaymentAdmin(admin.ModelAdmin):
|
||||||
list_display = ("date", "purpose", "amount")
|
list_display = ("date", "purpose", "amount")
|
||||||
|
list_filter = ("date",)
|
||||||
ordering = ("-date",)
|
ordering = ("-date",)
|
||||||
|
search_fields = ("purpose",)
|
||||||
|
|
||||||
@admin.display(ordering="amount")
|
@admin.display(ordering="amount")
|
||||||
def amount(self, payment: Payment):
|
def amount(self, payment: Payment):
|
||||||
@@ -126,31 +216,46 @@ class PaymentAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Drink)
|
@admin.register(Drink)
|
||||||
class DrinkAdmin(admin.ModelAdmin):
|
class DrinkAdmin(admin.ModelAdmin):
|
||||||
list_display = ("name", "purchase_price_per_crate", "crates_purchased", "purchase_price_total")
|
list_display = (
|
||||||
|
"name",
|
||||||
|
"year",
|
||||||
|
"crates_purchased",
|
||||||
|
"bottles_sold",
|
||||||
|
"bottles_remaining",
|
||||||
|
"purchase_price_total",
|
||||||
|
"balance",
|
||||||
|
)
|
||||||
|
list_filter = ("year",)
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {"fields": ("name",)}),
|
(None, {"fields": ("name", "year")}),
|
||||||
(
|
(
|
||||||
"crates",
|
"Kästen",
|
||||||
{
|
{
|
||||||
"fields": (
|
"fields": (
|
||||||
"crates_ordered",
|
"crates_ordered",
|
||||||
"crates_purchased",
|
"crates_purchased",
|
||||||
|
"crates_full_returned",
|
||||||
"crates_returned",
|
"crates_returned",
|
||||||
|
"crates_remaining",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"bottles",
|
"Flaschen",
|
||||||
{
|
{
|
||||||
"fields": (
|
"fields": (
|
||||||
"bottles_per_crate",
|
"bottles_per_crate",
|
||||||
"bottles_total",
|
"bottles_total",
|
||||||
"bottles_returned",
|
"bottles_returned",
|
||||||
|
"bottles_sold",
|
||||||
|
"bottles_given_away",
|
||||||
|
"bottles_consumed",
|
||||||
|
"bottles_remaining",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"amount",
|
"Menge",
|
||||||
{
|
{
|
||||||
"fields": (
|
"fields": (
|
||||||
"bottle_size",
|
"bottle_size",
|
||||||
@@ -160,17 +265,18 @@ class DrinkAdmin(admin.ModelAdmin):
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"purchase",
|
"Einkauf",
|
||||||
{
|
{
|
||||||
"fields": (
|
"fields": (
|
||||||
"purchase_price_per_crate",
|
"purchase_price_per_crate",
|
||||||
"purchase_price_per_bottle",
|
"purchase_price_per_bottle",
|
||||||
"purchase_price_total",
|
"purchase_price_total",
|
||||||
|
"remaining_purchase_value",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"deposit",
|
"Pfand",
|
||||||
{
|
{
|
||||||
"fields": (
|
"fields": (
|
||||||
"deposit_per_crate",
|
"deposit_per_crate",
|
||||||
@@ -181,14 +287,12 @@ class DrinkAdmin(admin.ModelAdmin):
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"sales",
|
"Verkauf",
|
||||||
{
|
{
|
||||||
"fields": (
|
"fields": (
|
||||||
"sale_price_per_bottle",
|
"sale_price_per_bottle",
|
||||||
"bottles_sold",
|
|
||||||
"sales_purchase_value",
|
"sales_purchase_value",
|
||||||
"sale_price_total",
|
"sale_price_total",
|
||||||
"bottles_given_away",
|
|
||||||
"giveaway_purchase_value",
|
"giveaway_purchase_value",
|
||||||
"balance",
|
"balance",
|
||||||
)
|
)
|
||||||
@@ -198,10 +302,15 @@ class DrinkAdmin(admin.ModelAdmin):
|
|||||||
readonly_fields = (
|
readonly_fields = (
|
||||||
"bottles_total",
|
"bottles_total",
|
||||||
"bottles_returned",
|
"bottles_returned",
|
||||||
|
"bottles_consumed",
|
||||||
|
"bottles_remaining",
|
||||||
|
"crates_full_returned",
|
||||||
|
"crates_remaining",
|
||||||
"amount_per_crate",
|
"amount_per_crate",
|
||||||
"amount_total",
|
"amount_total",
|
||||||
"purchase_price_per_bottle",
|
"purchase_price_per_bottle",
|
||||||
"purchase_price_total",
|
"purchase_price_total",
|
||||||
|
"remaining_purchase_value",
|
||||||
"deposit_total",
|
"deposit_total",
|
||||||
"deposit_refund",
|
"deposit_refund",
|
||||||
"deposit_kept",
|
"deposit_kept",
|
||||||
@@ -213,29 +322,82 @@ class DrinkAdmin(admin.ModelAdmin):
|
|||||||
"balance",
|
"balance",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@admin.display(description="Kästen voll zurück")
|
||||||
|
def crates_full_returned(self, drink: Drink):
|
||||||
|
return drink.crates_full_returned
|
||||||
|
|
||||||
|
@admin.display(description="Kästen übrig")
|
||||||
|
def crates_remaining(self, drink: Drink):
|
||||||
|
return drink.crates_remaining
|
||||||
|
|
||||||
|
@admin.display(description="Flaschen gesamt")
|
||||||
|
def bottles_total(self, drink: Drink):
|
||||||
|
return drink.bottles_total
|
||||||
|
|
||||||
|
@admin.display(description="Flaschen leer zurück")
|
||||||
|
def bottles_returned(self, drink: Drink):
|
||||||
|
return drink.bottles_returned
|
||||||
|
|
||||||
|
@admin.display(description="Flaschen verkauft")
|
||||||
|
def bottles_sold(self, drink: Drink):
|
||||||
|
return drink.bottles_sold
|
||||||
|
|
||||||
|
@admin.display(description="Flaschen verschenkt")
|
||||||
|
def bottles_given_away(self, drink: Drink):
|
||||||
|
return drink.bottles_given_away
|
||||||
|
|
||||||
|
@admin.display(description="Flaschen konsumiert")
|
||||||
|
def bottles_consumed(self, drink: Drink):
|
||||||
|
return drink.bottles_consumed
|
||||||
|
|
||||||
|
@admin.display(description="Flaschen übrig")
|
||||||
|
def bottles_remaining(self, drink: Drink):
|
||||||
|
return drink.bottles_remaining
|
||||||
|
|
||||||
|
@admin.display(description="Menge pro Kasten (l)")
|
||||||
|
def amount_per_crate(self, drink: Drink):
|
||||||
|
return drink.amount_per_crate
|
||||||
|
|
||||||
|
@admin.display(description="Menge gesamt (l)")
|
||||||
|
def amount_total(self, drink: Drink):
|
||||||
|
return drink.amount_total
|
||||||
|
|
||||||
|
@admin.display(description="Einkaufspreis pro Flasche")
|
||||||
def purchase_price_per_bottle(self, drink: Drink):
|
def purchase_price_per_bottle(self, drink: Drink):
|
||||||
return euro(drink.purchase_price_per_bottle)
|
return euro(drink.purchase_price_per_bottle)
|
||||||
|
|
||||||
|
@admin.display(description="Einkaufspreis gesamt")
|
||||||
def purchase_price_total(self, drink: Drink):
|
def purchase_price_total(self, drink: Drink):
|
||||||
return euro(drink.purchase_price_total)
|
return euro(drink.purchase_price_total)
|
||||||
|
|
||||||
|
@admin.display(description="Einkaufswert übrig")
|
||||||
|
def remaining_purchase_value(self, drink: Drink):
|
||||||
|
return euro(drink.remaining_purchase_value)
|
||||||
|
|
||||||
|
@admin.display(description="Pfand gesamt")
|
||||||
def deposit_total(self, drink: Drink):
|
def deposit_total(self, drink: Drink):
|
||||||
return euro(drink.deposit_total)
|
return euro(drink.deposit_total)
|
||||||
|
|
||||||
|
@admin.display(description="Pfand zurück")
|
||||||
def deposit_refund(self, drink: Drink):
|
def deposit_refund(self, drink: Drink):
|
||||||
return euro(drink.deposit_refund)
|
return euro(drink.deposit_refund)
|
||||||
|
|
||||||
|
@admin.display(description="Pfand einbehalten")
|
||||||
def deposit_kept(self, drink: Drink):
|
def deposit_kept(self, drink: Drink):
|
||||||
return euro(drink.deposit_kept)
|
return euro(drink.deposit_kept)
|
||||||
|
|
||||||
|
@admin.display(description="Einkaufswert verkauft")
|
||||||
def sales_purchase_value(self, drink: Drink):
|
def sales_purchase_value(self, drink: Drink):
|
||||||
return euro(drink.sales_purchase_value)
|
return euro(drink.sales_purchase_value)
|
||||||
|
|
||||||
|
@admin.display(description="Verkaufserlös")
|
||||||
def sale_price_total(self, drink: Drink):
|
def sale_price_total(self, drink: Drink):
|
||||||
return euro(drink.sale_price_total)
|
return euro(drink.sale_price_total)
|
||||||
|
|
||||||
|
@admin.display(description="Einkaufswert verschenkt")
|
||||||
def giveaway_purchase_value(self, drink: Drink):
|
def giveaway_purchase_value(self, drink: Drink):
|
||||||
return euro(drink.giveaway_purchase_value)
|
return euro(drink.giveaway_purchase_value)
|
||||||
|
|
||||||
|
@admin.display(description="Bilanz")
|
||||||
def balance(self, drink: Drink):
|
def balance(self, drink: Drink):
|
||||||
return mark_safe(f"<b>{euro(drink.balance)}</b>")
|
return mark_safe(f"<b>{euro(drink.balance)}</b>")
|
||||||
|
|||||||
Reference in New Issue
Block a user