Add free-drink toggle, payment deletion, donation flow, expanded dashboard
- Crew can mark bookings as free (Gratis checkbox) on staff_user page; free anonymous bookings skip the auto-payment. - Users (and crew) can delete their UserPayments with a confirmation page. - Pay page redesigned around "Eintrittsspende": quick-pick amount buttons (drinks rounded to 5 € + 10/15/20/25/30), "Nur Drinks" escape, intro text, type=text input to dodge browser locale formatting. - Me page: always-visible donate button, intro text linking drinks + Eintrittsspende. - Day cutoff at 06:00 Berlin: festival day rolls at 06:00, applied to current_festival_day plus Frühaufsteher/Nachtschwärmer/Goldene Stunde. - Dashboard: single Finanzen section (income vs costs) with breakdown, fun facts grouped (Trinker / Getränke / Zeit / Geld / Gratis). - Offene Beträge filtered to open > 0. - Staff list shows "(das bist du)" disabled row for own user. - Renamed seed_drinks_2026 -> seed_2026, added Baumarkt 194 € entry. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -9,6 +9,7 @@ PAYMENTS = [
|
||||
# purpose, date, amount
|
||||
("Toiletten", date(2026, 5, 6), 210.01),
|
||||
("Anzahlung Getränke+Kühlschrank+Bänke", date(2026, 5, 18), 400.00),
|
||||
("Baumarkt", date(2026, 5, 23), 194.00),
|
||||
("Band: Six Good Years", date(2026, 6, 12), 150.00),
|
||||
("Band: Melo-Komplott", date(2026, 6, 12), 100.00),
|
||||
("Band: Mörtel", date(2026, 6, 12), 150.00),
|
||||
@@ -34,7 +35,7 @@ DRINKS = [
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Seed Drink rows for the 2026 festival from the supplier invoice."
|
||||
help = "Seed Drink and Payment rows for the 2026 festival."
|
||||
|
||||
@transaction.atomic
|
||||
def handle(self, *args, **options):
|
||||
@@ -150,11 +150,100 @@ button:active {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.intro {
|
||||
font-size: 0.95rem;
|
||||
color: #CCCCCC;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.intro strong {
|
||||
color: #FFCC77;
|
||||
}
|
||||
|
||||
.amount-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.amount-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-height: 68px;
|
||||
padding: 10px 8px;
|
||||
border: 2px solid #885522;
|
||||
border-radius: 6px;
|
||||
background: #1f1f1f;
|
||||
color: #FFCC77;
|
||||
text-decoration: none;
|
||||
transition: background-color 100ms ease, border-color 100ms ease;
|
||||
}
|
||||
|
||||
.amount-main {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.amount-sub {
|
||||
font-size: 0.8rem;
|
||||
color: #AAAAAA;
|
||||
line-height: 1.1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.amount-btn:hover, .amount-btn:focus {
|
||||
border-color: #EE9933;
|
||||
background: #2a1f10;
|
||||
}
|
||||
|
||||
.amount-btn-active {
|
||||
border-color: #EE9933;
|
||||
background: #2a1f10;
|
||||
color: #EE9933;
|
||||
}
|
||||
|
||||
.amount-btn-weak {
|
||||
border-color: #553311;
|
||||
background: #181818;
|
||||
color: #AAAAAA;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.amount-btn-weak .amount-sub {
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.amount-link {
|
||||
color: #FFCC77;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.muted-left {
|
||||
font-size: 0.9rem;
|
||||
color: #AAAAAA;
|
||||
}
|
||||
|
||||
.user-self {
|
||||
color: #888888;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.muted-inline {
|
||||
color: #888888;
|
||||
font-size: 0.85rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.history-sub {
|
||||
margin-top: 18px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: normal;
|
||||
@@ -191,6 +280,40 @@ section {
|
||||
text-shadow: 0 0 12px #CC6611;
|
||||
}
|
||||
|
||||
.for-free-toggle {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 14px 20px;
|
||||
margin-bottom: 12px;
|
||||
min-height: 52px;
|
||||
border: 2px solid #885522;
|
||||
border-radius: 6px;
|
||||
background: #1f1f1f;
|
||||
color: #FFCC77;
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background-color 100ms ease, border-color 100ms ease;
|
||||
}
|
||||
|
||||
.for-free-toggle input {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
accent-color: #EE9933;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.for-free-toggle:has(input:checked) {
|
||||
border-color: #EE9933;
|
||||
background: #2a1f10;
|
||||
color: #EE9933;
|
||||
}
|
||||
|
||||
.drink-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
@@ -530,6 +653,10 @@ section {
|
||||
grid-template-columns: 1fr auto auto;
|
||||
}
|
||||
|
||||
.history li.fin-row {
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.link-btn-paypal {
|
||||
background-color: #003087;
|
||||
color: #FFFFFF;
|
||||
|
||||
+227
-22
@@ -1,4 +1,5 @@
|
||||
from datetime import datetime
|
||||
import math
|
||||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from django.conf import settings
|
||||
@@ -6,7 +7,7 @@ from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models import Case, F, IntegerField, Sum, Value, When
|
||||
from django.db.models import Case, Count, F, IntegerField, Sum, Value, When
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
@@ -28,6 +29,12 @@ BOOKING_START = datetime(2026, 5, 15, 0, 0, 0, tzinfo=BERLIN)
|
||||
BOOKING_END = datetime(2026, 5, 31, 23, 59, 59, tzinfo=BERLIN)
|
||||
|
||||
DAY_BY_WEEKDAY = {3: 1, 4: 2, 5: 3, 6: 4}
|
||||
DAY_CUTOFF_HOUR = 6
|
||||
|
||||
|
||||
def _shifted_local(dt):
|
||||
"""Local Berlin time minus 6h — so 05:59 belongs to previous calendar day."""
|
||||
return dt.astimezone(BERLIN) - timedelta(hours=DAY_CUTOFF_HOUR)
|
||||
|
||||
|
||||
def _now():
|
||||
@@ -54,7 +61,8 @@ def _require_open(request):
|
||||
|
||||
|
||||
def _current_festival_day():
|
||||
weekday = _now().weekday()
|
||||
# Day rolls at 06:00 Berlin: bookings before 06:00 count as previous day.
|
||||
weekday = _shifted_local(timezone.now()).weekday()
|
||||
return DAY_BY_WEEKDAY.get(weekday, 4)
|
||||
|
||||
|
||||
@@ -314,6 +322,31 @@ def delete_consumption_view(request, consumption_id):
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def delete_payment_view(request, payment_id):
|
||||
redirect, phase = _require_open(request)
|
||||
if redirect:
|
||||
return redirect
|
||||
if phase != "booking":
|
||||
raise Http404
|
||||
|
||||
try:
|
||||
payment = UserPayment.objects.get(pk=payment_id, user=request.user)
|
||||
except UserPayment.DoesNotExist:
|
||||
raise Http404
|
||||
|
||||
if request.method == "POST":
|
||||
payment.delete()
|
||||
return HttpResponseRedirect(reverse("suff:pay"))
|
||||
|
||||
return render(
|
||||
request,
|
||||
"suff/confirm_delete_payment.html",
|
||||
{"phase": phase, "payment": payment},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def pay_view(request):
|
||||
@@ -347,7 +380,33 @@ def pay_view(request):
|
||||
return HttpResponseRedirect(f"{reverse('suff:me')}?paid=1")
|
||||
|
||||
context = _tab_context(request.user)
|
||||
context.update({"phase": phase, "error": error})
|
||||
open_balance = max(float(context["open_balance"]), 0.0)
|
||||
drinks_rounded = int(math.ceil(open_balance / 5.0) * 5) if open_balance > 0 else 0
|
||||
suggestions = [
|
||||
{"amount": drinks_rounded + d, "entry": round(drinks_rounded + d - open_balance, 2)}
|
||||
for d in (10, 15, 20, 25, 30)
|
||||
]
|
||||
prefill_raw = request.GET.get("amount")
|
||||
try:
|
||||
prefill_amount = float(prefill_raw) if prefill_raw else None
|
||||
if prefill_amount is not None and prefill_amount <= 0:
|
||||
prefill_amount = None
|
||||
except ValueError:
|
||||
prefill_amount = None
|
||||
if prefill_amount is None:
|
||||
prefill_amount = float(suggestions[2]["amount"])
|
||||
prefill_value = f"{prefill_amount:.2f}"
|
||||
context.update(
|
||||
{
|
||||
"phase": phase,
|
||||
"error": error,
|
||||
"drinks_rounded": drinks_rounded,
|
||||
"suggestions": suggestions,
|
||||
"prefill_amount": prefill_amount,
|
||||
"prefill_value": prefill_value,
|
||||
"open_balance_url": f"{open_balance:.2f}",
|
||||
}
|
||||
)
|
||||
return render(request, "suff/pay.html", context)
|
||||
|
||||
|
||||
@@ -468,6 +527,7 @@ def staff_user_view(request, username):
|
||||
"drinks": drinks,
|
||||
"current_day": _current_festival_day(),
|
||||
"booked_drink": booked_drink,
|
||||
"booked_free": request.GET.get("free") == "1",
|
||||
"paid_toast": request.GET.get("paid") == "1",
|
||||
"is_anonymous_target": _is_anonymous(target),
|
||||
}
|
||||
@@ -486,6 +546,7 @@ def staff_book_view(request, username):
|
||||
|
||||
target = _get_staff_target(username)
|
||||
drink_id = request.POST.get("drink_id")
|
||||
for_free = request.POST.get("for_free") == "1"
|
||||
try:
|
||||
drink = Drink.objects.get(pk=int(drink_id))
|
||||
except (Drink.DoesNotExist, TypeError, ValueError):
|
||||
@@ -496,16 +557,17 @@ def staff_book_view(request, username):
|
||||
drink=drink,
|
||||
amount=1,
|
||||
day=_current_festival_day(),
|
||||
for_free=False,
|
||||
for_free=for_free,
|
||||
)
|
||||
if _is_anonymous(target):
|
||||
if _is_anonymous(target) and not for_free:
|
||||
UserPayment.objects.create(
|
||||
user=target,
|
||||
amount=drink.sale_price_per_bottle,
|
||||
method=UserPayment.Method.cash,
|
||||
note=f"Auto: {drink.name}",
|
||||
)
|
||||
return HttpResponseRedirect(f"{reverse('suff:staff_user', args=[username])}?booked={drink.id}")
|
||||
suffix = "&free=1" if for_free else ""
|
||||
return HttpResponseRedirect(f"{reverse('suff:staff_user', args=[username])}?booked={drink.id}{suffix}")
|
||||
|
||||
|
||||
@_staff_required
|
||||
@@ -549,6 +611,32 @@ def staff_pay_view(request, username):
|
||||
return render(request, "suff/staff_pay.html", context)
|
||||
|
||||
|
||||
@_staff_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def staff_delete_payment_view(request, username, payment_id):
|
||||
redirect, phase = _require_open(request)
|
||||
if redirect:
|
||||
return redirect
|
||||
if phase != "booking":
|
||||
raise Http404
|
||||
|
||||
target = _get_staff_target(username)
|
||||
try:
|
||||
payment = UserPayment.objects.get(pk=payment_id, user=target)
|
||||
except UserPayment.DoesNotExist:
|
||||
raise Http404
|
||||
|
||||
if request.method == "POST":
|
||||
payment.delete()
|
||||
return HttpResponseRedirect(reverse("suff:staff_pay", args=[username]))
|
||||
|
||||
return render(
|
||||
request,
|
||||
"suff/staff_confirm_delete_payment.html",
|
||||
{"phase": phase, "payment": payment, "tab_user": target},
|
||||
)
|
||||
|
||||
|
||||
@_staff_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def staff_delete_consumption_view(request, username, consumption_id):
|
||||
@@ -597,10 +685,8 @@ def dashboard_view(request):
|
||||
year = current_year()
|
||||
|
||||
total_donations = Donation.objects.filter(date__year=year).aggregate(s=Sum("amount"))["s"] or 0
|
||||
total_payments = Payment.objects.filter(date__year=year).aggregate(s=Sum("amount"))["s"] or 0
|
||||
total_balance = total_donations - total_payments
|
||||
total_pct = int(round((total_donations / total_payments) * 100)) if total_payments else 0
|
||||
total_pct_capped = min(total_pct, 100)
|
||||
total_costs = Payment.objects.filter(date__year=year).aggregate(s=Sum("amount"))["s"] or 0
|
||||
user_payments_total = UserPayment.objects.filter(created_at__year=year).aggregate(s=Sum("amount"))["s"] or 0
|
||||
|
||||
drinks = list(Drink.objects.filter(year=year).order_by("name"))
|
||||
drink_rows = [
|
||||
@@ -614,9 +700,14 @@ def dashboard_view(request):
|
||||
]
|
||||
sales_revenue = sum((d.sale_price_total for d in drinks), 0)
|
||||
purchase_cost = sum((d.purchase_price_total for d in drinks), 0)
|
||||
drinks_profit = sum((d.balance for d in drinks), 0)
|
||||
refinance_pct = int(round((sales_revenue / purchase_cost) * 100)) if purchase_cost else 0
|
||||
refinance_pct_capped = min(refinance_pct, 100)
|
||||
free_drinks_value = sum((d.giveaway_purchase_value for d in drinks), 0)
|
||||
unsold_purchase_value = sum((d.remaining_purchase_value for d in drinks), 0)
|
||||
unsold_sale_value = sum((d.bottles_remaining * d.sale_price_per_bottle for d in drinks), 0)
|
||||
|
||||
income_total = total_donations + user_payments_total
|
||||
finance_balance = income_total - total_costs
|
||||
finance_pct = int(round((income_total / total_costs) * 100)) if total_costs else 0
|
||||
finance_pct_capped = min(finance_pct, 100)
|
||||
|
||||
user_rows = []
|
||||
for user in User.objects.all():
|
||||
@@ -624,12 +715,15 @@ def dashboard_view(request):
|
||||
if not consumed:
|
||||
continue
|
||||
paid = user.paid_amount
|
||||
open_amount = consumed - paid
|
||||
if open_amount <= 0:
|
||||
continue
|
||||
user_rows.append(
|
||||
{
|
||||
"username": user.username,
|
||||
"consumed": consumed,
|
||||
"paid": paid,
|
||||
"open": consumed - paid,
|
||||
"open": open_amount,
|
||||
}
|
||||
)
|
||||
user_rows.sort(key=lambda r: r["open"], reverse=True)
|
||||
@@ -672,24 +766,135 @@ def dashboard_view(request):
|
||||
r = rows[0]
|
||||
top_per_day.append({"label": label, "username": r["user__username"], "amount": r["amount"]})
|
||||
|
||||
cons_qs = Consumption.objects.filter(drink__year=year)
|
||||
paid_cons_qs = cons_qs.filter(for_free=False)
|
||||
|
||||
# Time facts
|
||||
first_cons = cons_qs.exclude(created_at__isnull=True).order_by("created_at").select_related("user", "drink").first()
|
||||
last_cons = cons_qs.exclude(created_at__isnull=True).order_by("-created_at").select_related("user", "drink").first()
|
||||
|
||||
# Day rolls at 06:00 Berlin: 05:59 counts as late "previous day", 07:00 = early "today".
|
||||
# Compute time-of-day relative to 06:00 cutoff.
|
||||
earliest_per_user = None
|
||||
latest_per_user = None
|
||||
earliest_key = None
|
||||
latest_key = None
|
||||
hour_buckets = {}
|
||||
for c in cons_qs.exclude(created_at__isnull=True).values("user__username", "created_at", "amount"):
|
||||
local = c["created_at"].astimezone(BERLIN)
|
||||
# minutes after 06:00 cutoff (0 = 06:00, 1439 = 05:59 next morning)
|
||||
shifted = (local.hour * 60 + local.minute - DAY_CUTOFF_HOUR * 60) % (24 * 60)
|
||||
if earliest_key is None or shifted < earliest_key:
|
||||
earliest_key = shifted
|
||||
earliest_per_user = {"user__username": c["user__username"], "t": c["created_at"]}
|
||||
if latest_key is None or shifted > latest_key:
|
||||
latest_key = shifted
|
||||
latest_per_user = {"user__username": c["user__username"], "t": c["created_at"]}
|
||||
hour_buckets[local.hour] = hour_buckets.get(local.hour, 0) + (c["amount"] or 0)
|
||||
|
||||
golden_hour = None
|
||||
if hour_buckets:
|
||||
top_hour, top_amount = max(hour_buckets.items(), key=lambda kv: kv[1])
|
||||
golden_hour = {
|
||||
"label": f"{top_hour:02d} – {(top_hour + 1) % 24:02d} Uhr",
|
||||
"amount": top_amount,
|
||||
}
|
||||
|
||||
# Drink mix / variety
|
||||
variety_rows = (
|
||||
paid_cons_qs.values("user__username")
|
||||
.annotate(distinct=Count("drink", distinct=True))
|
||||
.order_by("-distinct")
|
||||
)
|
||||
variety_champ = variety_rows[0] if variety_rows and variety_rows[0]["distinct"] > 1 else None
|
||||
|
||||
cat_rows = (
|
||||
paid_cons_qs.values("drink__category")
|
||||
.annotate(amount=Sum("amount"))
|
||||
)
|
||||
cat_total = sum(r["amount"] for r in cat_rows) or 0
|
||||
beer_amount = sum(r["amount"] for r in cat_rows if r["drink__category"] == "beer")
|
||||
beer_share = int(round(beer_amount / cat_total * 100)) if cat_total else 0
|
||||
|
||||
def _top_in_categories(cats, label):
|
||||
rows = (
|
||||
paid_cons_qs.filter(drink__category__in=cats)
|
||||
.values("user__username")
|
||||
.annotate(amount=Sum("amount"))
|
||||
.order_by("-amount")
|
||||
)
|
||||
if rows:
|
||||
return {"label": label, "username": rows[0]["user__username"], "amount": rows[0]["amount"]}
|
||||
return None
|
||||
|
||||
alcfree_top = _top_in_categories(["alc_free_beer", "alc_free_radler"], "Alkoholfrei-Held")
|
||||
radler_top = _top_in_categories(["radler"], "Radler-Fan")
|
||||
water_top = _top_in_categories(["water"], "Wassertrinker")
|
||||
|
||||
# Money / behavior
|
||||
overpay_rows = []
|
||||
for user in User.objects.all():
|
||||
consumed = user.consumed_drinks_price or 0
|
||||
paid = user.paid_amount or 0
|
||||
if paid > consumed and consumed > 0:
|
||||
overpay_rows.append({"username": user.username, "tip": paid - consumed})
|
||||
overpay_rows.sort(key=lambda r: r["tip"], reverse=True)
|
||||
biggest_tip = overpay_rows[0] if overpay_rows else None
|
||||
total_entry = sum(r["tip"] for r in overpay_rows)
|
||||
|
||||
method_rows = (
|
||||
UserPayment.objects.filter(created_at__year=year)
|
||||
.values("method")
|
||||
.annotate(s=Sum("amount"))
|
||||
)
|
||||
method_split = {r["method"]: r["s"] for r in method_rows}
|
||||
|
||||
# Free drinks
|
||||
free_recipient_rows = (
|
||||
cons_qs.filter(for_free=True)
|
||||
.values("user__username")
|
||||
.annotate(amount=Sum("amount"))
|
||||
.order_by("-amount")
|
||||
)
|
||||
top_free_recipient = free_recipient_rows[0] if free_recipient_rows else None
|
||||
free_total_count = cons_qs.filter(for_free=True).aggregate(s=Sum("amount"))["s"] or 0
|
||||
|
||||
|
||||
context = {
|
||||
"year": year,
|
||||
"total_donations": total_donations,
|
||||
"total_payments": total_payments,
|
||||
"total_balance": total_balance,
|
||||
"total_pct": total_pct,
|
||||
"total_pct_capped": total_pct_capped,
|
||||
"total_costs": total_costs,
|
||||
"user_payments_total": user_payments_total,
|
||||
"income_total": income_total,
|
||||
"finance_balance": finance_balance,
|
||||
"finance_pct": finance_pct,
|
||||
"finance_pct_capped": finance_pct_capped,
|
||||
"drink_rows": drink_rows,
|
||||
"sales_revenue": sales_revenue,
|
||||
"purchase_cost": purchase_cost,
|
||||
"drinks_profit": drinks_profit,
|
||||
"refinance_pct": refinance_pct,
|
||||
"refinance_pct_capped": refinance_pct_capped,
|
||||
"free_drinks_value": free_drinks_value,
|
||||
"unsold_purchase_value": unsold_purchase_value,
|
||||
"unsold_sale_value": unsold_sale_value,
|
||||
"user_rows": user_rows,
|
||||
"top_spender": top_spender,
|
||||
"top_drink": top_drink,
|
||||
"busiest_day": busiest_day,
|
||||
"top_per_day": top_per_day,
|
||||
"first_cons": first_cons,
|
||||
"last_cons": last_cons,
|
||||
"earliest_per_user": earliest_per_user,
|
||||
"latest_per_user": latest_per_user,
|
||||
"golden_hour": golden_hour,
|
||||
"variety_champ": variety_champ,
|
||||
"beer_share": beer_share,
|
||||
"alcfree_top": alcfree_top,
|
||||
"radler_top": radler_top,
|
||||
"water_top": water_top,
|
||||
"biggest_tip": biggest_tip,
|
||||
"total_entry": total_entry,
|
||||
"method_split": method_split,
|
||||
"top_free_recipient": top_free_recipient,
|
||||
"free_total_count": free_total_count,
|
||||
}
|
||||
return render(request, "suff/dashboard.html", context)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from gaehsnitz.suff import (
|
||||
closed_view,
|
||||
dashboard_view,
|
||||
delete_consumption_view,
|
||||
delete_payment_view,
|
||||
logout_view,
|
||||
me_view,
|
||||
name_view,
|
||||
@@ -12,6 +13,7 @@ from gaehsnitz.suff import (
|
||||
pin_view,
|
||||
staff_book_view,
|
||||
staff_delete_consumption_view,
|
||||
staff_delete_payment_view,
|
||||
staff_index_view,
|
||||
staff_pay_view,
|
||||
staff_pin_reset_view,
|
||||
@@ -29,6 +31,7 @@ urlpatterns = [
|
||||
path("book/", book_view, name="book"),
|
||||
path("book/<int:consumption_id>/delete/", delete_consumption_view, name="delete_consumption"),
|
||||
path("pay/", pay_view, name="pay"),
|
||||
path("pay/<int:payment_id>/delete/", delete_payment_view, name="delete_payment"),
|
||||
path("dashboard/", dashboard_view, name="dashboard"),
|
||||
path("staff/", staff_index_view, name="staff_index"),
|
||||
path("staff/new/", staff_register_view, name="staff_register"),
|
||||
@@ -36,6 +39,11 @@ urlpatterns = [
|
||||
path("staff/u/<str:username>/pin/", staff_pin_reset_view, name="staff_pin_reset"),
|
||||
path("staff/u/<str:username>/book/", staff_book_view, name="staff_book"),
|
||||
path("staff/u/<str:username>/pay/", staff_pay_view, name="staff_pay"),
|
||||
path(
|
||||
"staff/u/<str:username>/pay/<int:payment_id>/delete/",
|
||||
staff_delete_payment_view,
|
||||
name="staff_delete_payment",
|
||||
),
|
||||
path(
|
||||
"staff/u/<str:username>/book/<int:consumption_id>/delete/",
|
||||
staff_delete_consumption_view,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{% extends "suff/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Zahlung löschen?</h2>
|
||||
|
||||
<p>
|
||||
Willst du wirklich diese Zahlung löschen?
|
||||
</p>
|
||||
|
||||
<section class="total-box">
|
||||
<span class="total-label">{{ payment.get_method_display }}{% if payment.note %} – {{ payment.note }}{% endif %}</span>
|
||||
<span class="total-value">{{ payment.amount|floatformat:2 }} €</span>
|
||||
<span class="total-label">{{ payment.created_at|date:"d.m. H:i" }}</span>
|
||||
</section>
|
||||
|
||||
<form method="post" action="{% url 'suff:delete_payment' payment.id %}" class="confirm-actions">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn-danger">Ja, löschen</button>
|
||||
<a href="{% url 'suff:pay' %}" class="btn-secondary">Nein, zurück</a>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -5,31 +5,55 @@
|
||||
<h2>Dashboard {{ year }}</h2>
|
||||
|
||||
<section>
|
||||
<h3>Refinanzierung gesamt</h3>
|
||||
<h3>Finanzen</h3>
|
||||
<div class="progress-wrap">
|
||||
<div class="progress-bar" style="width: {{ total_pct_capped }}%;"></div>
|
||||
<span class="progress-label">{{ total_pct }}%</span>
|
||||
<div class="progress-bar" style="width: {{ finance_pct_capped }}%;"></div>
|
||||
<span class="progress-label">{{ finance_pct }}%</span>
|
||||
</div>
|
||||
<p class="muted-left">
|
||||
Spenden {{ total_donations|euro }} / Ausgaben {{ total_payments|euro }}
|
||||
</p>
|
||||
<p class="muted-left">
|
||||
Bilanz: <b>{{ total_balance|euro }}</b>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Refinanzierung Getränke</h3>
|
||||
<div class="progress-wrap">
|
||||
<div class="progress-bar" style="width: {{ refinance_pct_capped }}%;"></div>
|
||||
<span class="progress-label">{{ refinance_pct }}%</span>
|
||||
</div>
|
||||
<p class="muted-left">
|
||||
Verkaufserlös {{ sales_revenue|euro }} / Einkaufspreis {{ purchase_cost|euro }}
|
||||
</p>
|
||||
<p class="muted-left">
|
||||
Aktueller Gewinn: <b>{{ drinks_profit|euro }}</b>
|
||||
</p>
|
||||
<ul class="history">
|
||||
<li class="fin-row">
|
||||
<span class="hist-what"><b>Ausgaben gesamt</b></span>
|
||||
<span class="hist-price"><b>{{ total_costs|euro }}</b></span>
|
||||
</li>
|
||||
<li class="fin-row">
|
||||
<span class="hist-what"><b>Einnahmen gesamt</b></span>
|
||||
<span class="hist-price"><b>{{ income_total|euro }}</b></span>
|
||||
</li>
|
||||
<li class="fin-row">
|
||||
<span class="hist-what"><b>Bilanz</b></span>
|
||||
<span class="hist-price"><b>{{ finance_balance|euro }}</b></span>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="history history-sub">
|
||||
<li class="fin-row">
|
||||
<span class="hist-what">Spenden (Donations)</span>
|
||||
<span class="hist-price">{{ total_donations|euro }}</span>
|
||||
</li>
|
||||
<li class="fin-row">
|
||||
<span class="hist-what">Zahlungen (User-Tab)</span>
|
||||
<span class="hist-price">{{ user_payments_total|euro }}</span>
|
||||
</li>
|
||||
<li class="fin-row">
|
||||
<span class="hist-what">Einkaufspreis Getränke</span>
|
||||
<span class="hist-price">{{ purchase_cost|euro }}</span>
|
||||
</li>
|
||||
<li class="fin-row">
|
||||
<span class="hist-what">Verkaufserlös Getränke</span>
|
||||
<span class="hist-price">{{ sales_revenue|euro }}</span>
|
||||
</li>
|
||||
<li class="fin-row">
|
||||
<span class="hist-what">Gratis-Getränke (EK-Wert)</span>
|
||||
<span class="hist-price">{{ free_drinks_value|euro }}</span>
|
||||
</li>
|
||||
<li class="fin-row">
|
||||
<span class="hist-what">Unverkauft (EK-Wert)</span>
|
||||
<span class="hist-price">{{ unsold_purchase_value|euro }}</span>
|
||||
</li>
|
||||
<li class="fin-row">
|
||||
<span class="hist-what">Unverkauft (potenzieller VK)</span>
|
||||
<span class="hist-price">{{ unsold_sale_value|euro }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
@@ -63,7 +87,7 @@
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Fun Facts</h3>
|
||||
<h3>Fun Facts: Trinker</h3>
|
||||
<ul class="history">
|
||||
{% if top_spender %}
|
||||
<li>
|
||||
@@ -72,6 +96,26 @@
|
||||
<span class="hist-price">{{ top_spender.total|euro }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if variety_champ %}
|
||||
<li>
|
||||
<span class="hist-when">Vielfalt-Champion</span>
|
||||
<span class="hist-what">{{ variety_champ.user__username }}</span>
|
||||
<span class="hist-price">{{ variety_champ.distinct }} Sorten</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for f in top_per_day %}
|
||||
<li>
|
||||
<span class="hist-when">Top {{ f.label }}</span>
|
||||
<span class="hist-what">{{ f.username }}</span>
|
||||
<span class="hist-price">{{ f.amount }} Flaschen</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Fun Facts: Getränke</h3>
|
||||
<ul class="history">
|
||||
{% if top_drink %}
|
||||
<li>
|
||||
<span class="hist-when">Top-Getränk</span>
|
||||
@@ -79,6 +123,73 @@
|
||||
<span class="hist-price">{{ top_drink.amount }}x</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<span class="hist-when">Bier-Anteil</span>
|
||||
<span class="hist-what">am Konsum</span>
|
||||
<span class="hist-price">{{ beer_share }}%</span>
|
||||
</li>
|
||||
{% if alcfree_top %}
|
||||
<li>
|
||||
<span class="hist-when">{{ alcfree_top.label }}</span>
|
||||
<span class="hist-what">{{ alcfree_top.username }}</span>
|
||||
<span class="hist-price">{{ alcfree_top.amount }} Flaschen</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if radler_top %}
|
||||
<li>
|
||||
<span class="hist-when">{{ radler_top.label }}</span>
|
||||
<span class="hist-what">{{ radler_top.username }}</span>
|
||||
<span class="hist-price">{{ radler_top.amount }} Flaschen</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if water_top %}
|
||||
<li>
|
||||
<span class="hist-when">{{ water_top.label }}</span>
|
||||
<span class="hist-what">{{ water_top.username }}</span>
|
||||
<span class="hist-price">{{ water_top.amount }} Flaschen</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Fun Facts: Zeit</h3>
|
||||
<ul class="history">
|
||||
{% if first_cons %}
|
||||
<li>
|
||||
<span class="hist-when">Erste Buchung</span>
|
||||
<span class="hist-what">{{ first_cons.user.username }} – {{ first_cons.drink.name }}</span>
|
||||
<span class="hist-price">{{ first_cons.created_at|date:"d.m. H:i" }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if last_cons %}
|
||||
<li>
|
||||
<span class="hist-when">Letzter Schluck</span>
|
||||
<span class="hist-what">{{ last_cons.user.username }} – {{ last_cons.drink.name }}</span>
|
||||
<span class="hist-price">{{ last_cons.created_at|date:"d.m. H:i" }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if earliest_per_user %}
|
||||
<li>
|
||||
<span class="hist-when">Frühaufsteher</span>
|
||||
<span class="hist-what">{{ earliest_per_user.user__username }}</span>
|
||||
<span class="hist-price">{{ earliest_per_user.t|date:"d.m. H:i" }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if latest_per_user %}
|
||||
<li>
|
||||
<span class="hist-when">Nachtschwärmer</span>
|
||||
<span class="hist-what">{{ latest_per_user.user__username }}</span>
|
||||
<span class="hist-price">{{ latest_per_user.t|date:"d.m. H:i" }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if golden_hour %}
|
||||
<li>
|
||||
<span class="hist-when">Goldene Stunde</span>
|
||||
<span class="hist-what">{{ golden_hour.label }}</span>
|
||||
<span class="hist-price">{{ golden_hour.amount }} Flaschen</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if busiest_day %}
|
||||
<li>
|
||||
<span class="hist-when">Härtester Tag</span>
|
||||
@@ -86,16 +197,52 @@
|
||||
<span class="hist-price">{{ busiest_day.amount }} Flaschen</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for f in top_per_day %}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Fun Facts: Geld</h3>
|
||||
<ul class="history">
|
||||
{% if biggest_tip %}
|
||||
<li>
|
||||
<span class="hist-when">{{ f.label }}</span>
|
||||
<span class="hist-what">{{ f.username }}</span>
|
||||
<span class="hist-price">{{ f.amount }} Flaschen</span>
|
||||
<span class="hist-when">Bigspender-Trinkgeld</span>
|
||||
<span class="hist-what">{{ biggest_tip.username }}</span>
|
||||
<span class="hist-price">{{ biggest_tip.tip|euro }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<span class="hist-when">Eintrittsspenden gesamt</span>
|
||||
<span class="hist-what">aus Überzahlungen</span>
|
||||
<span class="hist-price">{{ total_entry|euro }}</span>
|
||||
</li>
|
||||
{% for method, sum in method_split.items %}
|
||||
<li>
|
||||
<span class="hist-when">Zahlungen {{ method }}</span>
|
||||
<span class="hist-what"></span>
|
||||
<span class="hist-price">{{ sum|euro }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Fun Facts: Gratis</h3>
|
||||
<ul class="history">
|
||||
<li>
|
||||
<span class="hist-when">Aufmerksamkeit des Hauses</span>
|
||||
<span class="hist-what">Gratis-Flaschen</span>
|
||||
<span class="hist-price">{{ free_total_count }}x</span>
|
||||
</li>
|
||||
{% if top_free_recipient %}
|
||||
<li>
|
||||
<span class="hist-when">Glücklicher Empfänger</span>
|
||||
<span class="hist-what">{{ top_free_recipient.user__username }}</span>
|
||||
<span class="hist-price">{{ top_free_recipient.amount }} gratis</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div class="logout-form">
|
||||
<a href="{% url 'suff:me' %}" class="link-btn link-btn-secondary">Zurück</a>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
{% block content %}
|
||||
<h2>Hallo {{ tab_user.username }}</h2>
|
||||
|
||||
<p class="intro">
|
||||
Tipp dich rein, sobald du was trinkst. Am Ende deines Besuchs kannst du
|
||||
alles zusammen mit deiner <strong>Eintrittsspende</strong> bezahlen –
|
||||
bar oder per PayPal.
|
||||
</p>
|
||||
|
||||
{% if booked_drink %}
|
||||
<div class="toast" role="status">
|
||||
Gebucht: +1 {{ booked_drink.name }}
|
||||
@@ -26,9 +32,7 @@
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{% if open_balance > 0 %}
|
||||
<p><a href="{% url 'suff:pay' %}" class="btn-primary">Bezahlen</a></p>
|
||||
{% endif %}
|
||||
<p><a href="{% url 'suff:pay' %}" class="btn-primary">Für Drinks und Eintritt spenden</a></p>
|
||||
|
||||
{% if phase == "booking" %}
|
||||
<section>
|
||||
@@ -84,7 +88,7 @@
|
||||
{% if request.user.is_staff %}
|
||||
<div class="link-row">
|
||||
<a href="{% url 'suff:staff_index' %}" class="link-btn link-btn-secondary">Crew: Buchen für andere</a>
|
||||
<a href="{% url 'suff:dashboard' %}" class="link-btn link-btn-secondary">Dashboard</a>
|
||||
<a href="{% url 'suff:dashboard' %}" class="link-btn link-btn-secondary">Crew: Dashboard</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
{% extends "suff/base.html" %}
|
||||
{% load l10n %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Bezahlen</h2>
|
||||
|
||||
<p>
|
||||
Bitte zahle deinen offenen Betrag mit deiner bevorzugten Methode
|
||||
(z. B. Bar an der Kasse oder PayPal an Flo) und trage
|
||||
den bezahlten Betrag anschließend hier ein.
|
||||
</p>
|
||||
|
||||
<div class="link-row">
|
||||
<a href="https://www.paypal.com/paypalme/lotharwiener" target="_blank" rel="noopener noreferrer" class="link-btn link-btn-paypal">
|
||||
PayPal öffnen
|
||||
</a>
|
||||
</div>
|
||||
<h2>Spenden</h2>
|
||||
|
||||
<section class="total-box">
|
||||
<span class="total-label">Deine Rechnung</span>
|
||||
@@ -22,10 +11,41 @@
|
||||
<span class="total-label">Bezahlt</span>
|
||||
<span class="total-value">{{ paid|floatformat:2 }} €</span>
|
||||
{% endif %}
|
||||
<span class="total-label">Offen</span>
|
||||
<span class="total-label">Offen (Drinks)</span>
|
||||
<span class="total-value">{{ open_balance|floatformat:2 }} €</span>
|
||||
</section>
|
||||
|
||||
<p class="intro">
|
||||
Dein Beitrag deckt die <strong>Drinks</strong> und deine
|
||||
<strong>Eintrittsspende</strong>. Die Vorschläge unten runden deinen
|
||||
offenen Drink-Betrag auf die nächsten 5 € und legen 10–30 €
|
||||
Eintritt drauf.
|
||||
</p>
|
||||
<p class="intro">
|
||||
Du darfst gerne <strong>weniger</strong> geben, wenn das gerade besser
|
||||
passt – kein Problem. Und wenn du <strong>mehr</strong> geben kannst,
|
||||
hilft uns das sehr, die Kosten für Bands, Toiletten und Technik zu
|
||||
decken. So oder so: <strong>danke, dass du da bist!</strong>
|
||||
</p>
|
||||
|
||||
<section>
|
||||
<h3>Vorschläge</h3>
|
||||
<div class="amount-grid">
|
||||
{% for s in suggestions %}
|
||||
<a href="?amount={{ s.amount }}" class="amount-btn{% if prefill_amount == s.amount %} amount-btn-active{% endif %}">
|
||||
<span class="amount-main">{{ s.amount }} €</span>
|
||||
<span class="amount-sub">→ {{ s.entry|floatformat:2 }} € Eintritt</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% if open_balance > 0 %}
|
||||
<a href="?amount={{ open_balance_url }}" class="amount-btn amount-btn-weak">
|
||||
<span class="amount-main">{{ open_balance|floatformat:2 }} €</span>
|
||||
<span class="amount-sub">→ Nur Drinks</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if error %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endif %}
|
||||
@@ -34,8 +54,8 @@
|
||||
{% csrf_token %}
|
||||
<label>
|
||||
Betrag (€)
|
||||
<input type="number" name="amount" step="0.01" min="0.01"
|
||||
value="{{ open_balance|floatformat:2 }}" required />
|
||||
<input type="text" name="amount" inputmode="decimal" pattern="[0-9]+([.,][0-9]{1,2})?"
|
||||
value="{% if prefill_value %}{{ prefill_value }}{% else %}{{ suggestions.0.amount }}{% endif %}" required />
|
||||
</label>
|
||||
<label>
|
||||
Methode
|
||||
@@ -53,6 +73,12 @@
|
||||
<button type="submit" class="btn-primary">Zahlung eintragen</button>
|
||||
</form>
|
||||
|
||||
<div class="link-row">
|
||||
<a href="https://www.paypal.com/paypalme/lotharwiener" target="_blank" rel="noopener noreferrer" class="link-btn link-btn-paypal">
|
||||
PayPal öffnen
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if user_payments %}
|
||||
<section>
|
||||
<h3>Bisherige Zahlungen</h3>
|
||||
@@ -62,6 +88,9 @@
|
||||
<span class="hist-when">{{ p.created_at|date:"d.m. H:i" }}</span>
|
||||
<span class="hist-what">{{ p.get_method_display }}{% if p.note %} – {{ p.note }}{% endif %}</span>
|
||||
<span class="hist-price">{{ p.amount|floatformat:2 }} €</span>
|
||||
{% if phase == "booking" %}
|
||||
<a href="{% url 'suff:delete_payment' p.id %}" class="hist-delete" aria-label="Zahlung löschen">🗑</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{% extends "suff/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<p class="muted">Crew-Ansicht</p>
|
||||
<h2>Zahlung löschen für <span class="staff-target">{{ tab_user.username }}</span>?</h2>
|
||||
|
||||
<section class="total-box">
|
||||
<span class="total-label">{{ payment.get_method_display }}{% if payment.note %} – {{ payment.note }}{% endif %}</span>
|
||||
<span class="total-value">{{ payment.amount|floatformat:2 }} €</span>
|
||||
<span class="total-label">{{ payment.created_at|date:"d.m. H:i" }}</span>
|
||||
</section>
|
||||
|
||||
<form method="post" action="{% url 'suff:staff_delete_payment' tab_user.username payment.id %}" class="confirm-actions">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn-danger">Ja, löschen</button>
|
||||
<a href="{% url 'suff:staff_pay' tab_user.username %}" class="btn-secondary">Nein, zurück</a>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -25,7 +25,11 @@
|
||||
{% for u in users %}
|
||||
<li>
|
||||
<span class="hist-what">
|
||||
<a href="{% url 'suff:staff_user' u.username %}">{{ u.username }}</a>
|
||||
{% if u.username == request.user.username %}
|
||||
<span class="user-self">{{ u.username }} <span class="muted-inline">(das bist du)</span></span>
|
||||
{% else %}
|
||||
<a href="{% url 'suff:staff_user' u.username %}">{{ u.username }}</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
<span class="hist-when">{{ p.created_at|date:"d.m. H:i" }}</span>
|
||||
<span class="hist-what">{{ p.get_method_display }}{% if p.note %} – {{ p.note }}{% endif %}</span>
|
||||
<span class="hist-price">{{ p.amount|floatformat:2 }} €</span>
|
||||
{% if phase == "booking" %}
|
||||
<a href="{% url 'suff:staff_delete_payment' tab_user.username p.id %}" class="hist-delete" aria-label="Zahlung löschen">🗑</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
{% block content %}
|
||||
<p class="muted">Crew-Ansicht</p>
|
||||
<h2>Buchen für <span class="staff-target">{{ tab_user.username }}</span>{% if is_anonymous_target %} (Bar / Anonym){% endif %}</h2>
|
||||
<h2>Buchen für <span class="staff-target">{{ tab_user.username }}</span></h2>
|
||||
|
||||
{% if booked_drink %}
|
||||
<div class="toast" role="status">
|
||||
Gebucht: +1 {{ booked_drink.name }}{% if is_anonymous_target %} (bar bezahlt){% endif %}
|
||||
Gebucht: +1 {{ booked_drink.name }}{% if booked_free %} (gratis){% elif is_anonymous_target %} (bar bezahlt){% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -38,19 +38,22 @@
|
||||
{% if phase == "booking" %}
|
||||
<section>
|
||||
<h3>Getränk buchen</h3>
|
||||
<div class="drink-grid">
|
||||
{% for drink in drinks %}
|
||||
<form method="post" action="{% url 'suff:staff_book' tab_user.username %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="drink_id" value="{{ drink.id }}" />
|
||||
<button type="submit" class="drink-btn drink-btn-{{ drink.category }}">
|
||||
<form method="post" action="{% url 'suff:staff_book' tab_user.username %}">
|
||||
{% csrf_token %}
|
||||
<label class="for-free-toggle">
|
||||
<input type="checkbox" name="for_free" value="1" />
|
||||
<span>Gratis (z.b. Artists am Spieltag)</span>
|
||||
</label>
|
||||
<div class="drink-grid">
|
||||
{% for drink in drinks %}
|
||||
<button type="submit" name="drink_id" value="{{ drink.id }}" class="drink-btn drink-btn-{{ drink.category }}">
|
||||
<span class="drink-plus">+1</span>
|
||||
<span class="drink-name">{{ drink.name }}</span>
|
||||
<span class="drink-price">{{ drink.sale_price_per_bottle|floatformat:2 }} €</span>
|
||||
</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user