from django import forms from django.contrib import admin, messages from django.contrib.admin.utils import unquote from django.contrib.auth import get_user_model 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.template.response import TemplateResponse from django.urls import path, reverse from django.utils.html import format_html from django.utils.safestring import mark_safe from django.db.models import Sum from gaehsnitz.models import Donation, Payment, Drink, Consumption, UserPayment, current_year from gaehsnitz.templatetags.money import euro User = get_user_model() class ConsumptionInline(admin.TabularInline): model = Consumption extra = 0 class UserPaymentInline(admin.TabularInline): model = UserPayment extra = 0 readonly_fields = ("created_at",) class SetPinForm(forms.Form): pin = forms.CharField( label="Neue PIN (3 Ziffern)", min_length=3, max_length=3, ) def clean_pin(self): pin = self.cleaned_data["pin"] if not pin.isdigit(): raise forms.ValidationError("PIN muss aus genau 3 Ziffern bestehen.") return pin @admin.register(User) class CustomUserAdmin(UserAdmin): list_display = ("username", "consumed_drinks_price", "paid_amount", "open_balance") ordering = ("username",) list_filter = [] fieldsets = ( ( None, { "fields": ( "username", "password", "pin_status", ) }, ), ( "BILANZ", { "fields": ( "consumed_drinks_price", "paid_amount", "open_balance", "drinks_breakdown", "free_drinks_breakdown", ) }, ), ) readonly_fields = ( "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): 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") def pin_status(self, user: User): status = "gesetzt" if user.pin else "nicht gesetzt" if user.pk is None: return status url = reverse("admin:gaehsnitz_user_set_pin", args=[user.pk]) return format_html('{}   PIN setzen', status, url) def get_urls(self): urls = super().get_urls() custom = [ path( "/pin/", self.admin_site.admin_view(self.set_pin_view), name="gaehsnitz_user_set_pin", ), ] return custom + urls def set_pin_view(self, request, id): if not self.has_change_permission(request): raise Http404 user = self.get_object(request, unquote(id)) if user is None: raise Http404 if request.method == "POST": form = SetPinForm(request.POST) if form.is_valid(): user.set_pin(form.cleaned_data["pin"]) user.save(update_fields=["pin"]) messages.success(request, f"PIN für {user.username} gesetzt.") return HttpResponseRedirect(reverse("admin:gaehsnitz_user_change", args=[user.pk])) else: form = SetPinForm() context = { **self.admin_site.each_context(request), "title": f"PIN setzen für {user.username}", "opts": self.model._meta, "original": user, "form": form, } 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) class DonationAdmin(admin.ModelAdmin): list_display = ("date", "amount", "note") list_filter = ("date",) ordering = ("-date",) search_fields = ("note",) @admin.display(ordering="amount") def amount(self, donation: Donation): return euro(donation.amount) @admin.register(Payment) class PaymentAdmin(admin.ModelAdmin): list_display = ("date", "purpose", "amount") list_filter = ("date",) ordering = ("-date",) search_fields = ("purpose",) @admin.display(ordering="amount") def amount(self, payment: Payment): return euro(payment.amount) @admin.register(Drink) class DrinkAdmin(admin.ModelAdmin): list_display = ( "name", "year", "crates_purchased", "bottles_sold", "bottles_remaining", "purchase_price_total", "balance", ) list_filter = ("year",) fieldsets = ( (None, {"fields": ("name", "year")}), ( "Kästen", { "fields": ( "crates_ordered", "crates_purchased", "crates_full_returned", "crates_returned", "crates_remaining", ) }, ), ( "Flaschen", { "fields": ( "bottles_per_crate", "bottles_total", "bottles_returned", "bottles_sold", "bottles_given_away", "bottles_consumed", "bottles_remaining", ) }, ), ( "Menge", { "fields": ( "bottle_size", "amount_per_crate", "amount_total", ) }, ), ( "Einkauf", { "fields": ( "purchase_price_per_crate", "purchase_price_per_bottle", "purchase_price_total", "remaining_purchase_value", ) }, ), ( "Pfand", { "fields": ( "deposit_per_crate", "deposit_total", "deposit_refund", "deposit_kept", ) }, ), ( "Verkauf", { "fields": ( "sale_price_per_bottle", "sales_purchase_value", "sale_price_total", "giveaway_purchase_value", "balance", ) }, ), ) readonly_fields = ( "bottles_total", "bottles_returned", "bottles_consumed", "bottles_remaining", "crates_full_returned", "crates_remaining", "amount_per_crate", "amount_total", "purchase_price_per_bottle", "purchase_price_total", "remaining_purchase_value", "deposit_total", "deposit_refund", "deposit_kept", "bottles_sold", "sales_purchase_value", "sale_price_total", "bottles_given_away", "giveaway_purchase_value", "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): return euro(drink.purchase_price_per_bottle) @admin.display(description="Einkaufspreis gesamt") def purchase_price_total(self, drink: Drink): 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): return euro(drink.deposit_total) @admin.display(description="Pfand zurück") def deposit_refund(self, drink: Drink): return euro(drink.deposit_refund) @admin.display(description="Pfand einbehalten") def deposit_kept(self, drink: Drink): return euro(drink.deposit_kept) @admin.display(description="Einkaufswert verkauft") def sales_purchase_value(self, drink: Drink): return euro(drink.sales_purchase_value) @admin.display(description="Verkaufserlös") def sale_price_total(self, drink: Drink): return euro(drink.sale_price_total) @admin.display(description="Einkaufswert verschenkt") def giveaway_purchase_value(self, drink: Drink): return euro(drink.giveaway_purchase_value) @admin.display(description="Bilanz") def balance(self, drink: Drink): return mark_safe(f"{euro(drink.balance)}")