47d46e8e6f
Self-service drink tab at /suff/ for festival attendees. Users log in with username + 3-digit PIN stored in a separate User.pin field, so staff/admin accounts can keep their strong password for /admin/ and also use the drink tool with the same username. PINs for staff users must be set from the admin panel via a dedicated "PIN setzen" view to prevent account takeover by name collision. Time-gated to the festival window (Thu–Sun in Berlin tz) with phases before/booking/readonly/closed; in non-production mode the tool is always in booking phase for local testing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
242 lines
6.8 KiB
Python
242 lines
6.8 KiB
Python
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.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 gaehsnitz.models import Donation, Payment, Drink, Consumption
|
|
from gaehsnitz.templatetags.money import euro
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
class ConsumptionInline(admin.TabularInline):
|
|
model = Consumption
|
|
extra = 0
|
|
|
|
|
|
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")
|
|
ordering = ("username",)
|
|
list_filter = []
|
|
fieldsets = (
|
|
(
|
|
None,
|
|
{
|
|
"fields": (
|
|
"username",
|
|
"password",
|
|
"pin_status",
|
|
)
|
|
},
|
|
),
|
|
("BALANCE", {"fields": ("consumed_drinks_price",)}),
|
|
)
|
|
readonly_fields = ("consumed_drinks_price", "pin_status")
|
|
inlines = (ConsumptionInline,)
|
|
|
|
def consumed_drinks_price(self, user: User):
|
|
return euro(user.consumed_drinks_price)
|
|
|
|
@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('{} <a href="{}">PIN setzen</a>', status, url)
|
|
|
|
def get_urls(self):
|
|
urls = super().get_urls()
|
|
custom = [
|
|
path(
|
|
"<id>/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)
|
|
|
|
|
|
@admin.register(Donation)
|
|
class DonationAdmin(admin.ModelAdmin):
|
|
list_display = ("date", "amount", "note")
|
|
ordering = ("-date",)
|
|
|
|
@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")
|
|
ordering = ("-date",)
|
|
|
|
@admin.display(ordering="amount")
|
|
def amount(self, payment: Payment):
|
|
return euro(payment.amount)
|
|
|
|
|
|
@admin.register(Drink)
|
|
class DrinkAdmin(admin.ModelAdmin):
|
|
list_display = ("name", "purchase_price_per_crate", "crates_purchased", "purchase_price_total")
|
|
fieldsets = (
|
|
(None, {"fields": ("name",)}),
|
|
(
|
|
"crates",
|
|
{
|
|
"fields": (
|
|
"crates_ordered",
|
|
"crates_purchased",
|
|
"crates_returned",
|
|
)
|
|
},
|
|
),
|
|
(
|
|
"bottles",
|
|
{
|
|
"fields": (
|
|
"bottles_per_crate",
|
|
"bottles_total",
|
|
"bottles_returned",
|
|
)
|
|
},
|
|
),
|
|
(
|
|
"amount",
|
|
{
|
|
"fields": (
|
|
"bottle_size",
|
|
"amount_per_crate",
|
|
"amount_total",
|
|
)
|
|
},
|
|
),
|
|
(
|
|
"purchase",
|
|
{
|
|
"fields": (
|
|
"purchase_price_per_crate",
|
|
"purchase_price_per_bottle",
|
|
"purchase_price_total",
|
|
)
|
|
},
|
|
),
|
|
(
|
|
"deposit",
|
|
{
|
|
"fields": (
|
|
"deposit_per_crate",
|
|
"deposit_total",
|
|
"deposit_refund",
|
|
"deposit_kept",
|
|
)
|
|
},
|
|
),
|
|
(
|
|
"sales",
|
|
{
|
|
"fields": (
|
|
"sale_price_per_bottle",
|
|
"bottles_sold",
|
|
"sales_purchase_value",
|
|
"sale_price_total",
|
|
"bottles_given_away",
|
|
"giveaway_purchase_value",
|
|
"balance",
|
|
)
|
|
},
|
|
),
|
|
)
|
|
readonly_fields = (
|
|
"bottles_total",
|
|
"bottles_returned",
|
|
"amount_per_crate",
|
|
"amount_total",
|
|
"purchase_price_per_bottle",
|
|
"purchase_price_total",
|
|
"deposit_total",
|
|
"deposit_refund",
|
|
"deposit_kept",
|
|
"bottles_sold",
|
|
"sales_purchase_value",
|
|
"sale_price_total",
|
|
"bottles_given_away",
|
|
"giveaway_purchase_value",
|
|
"balance",
|
|
)
|
|
|
|
def purchase_price_per_bottle(self, drink: Drink):
|
|
return euro(drink.purchase_price_per_bottle)
|
|
|
|
def purchase_price_total(self, drink: Drink):
|
|
return euro(drink.purchase_price_total)
|
|
|
|
def deposit_total(self, drink: Drink):
|
|
return euro(drink.deposit_total)
|
|
|
|
def deposit_refund(self, drink: Drink):
|
|
return euro(drink.deposit_refund)
|
|
|
|
def deposit_kept(self, drink: Drink):
|
|
return euro(drink.deposit_kept)
|
|
|
|
def sales_purchase_value(self, drink: Drink):
|
|
return euro(drink.sales_purchase_value)
|
|
|
|
def sale_price_total(self, drink: Drink):
|
|
return euro(drink.sale_price_total)
|
|
|
|
def giveaway_purchase_value(self, drink: Drink):
|
|
return euro(drink.giveaway_purchase_value)
|
|
|
|
def balance(self, drink: Drink):
|
|
return mark_safe(f"<b>{euro(drink.balance)}</b>")
|