Add settled balance panel, cash booking mode, and Kasse dashboard line

- Balance panel turns green with "Bezahlt ✓" + breakdown when open_balance <= 0, on all four tab pages (me, pay, staff_user, staff_pay)
- booking_mode radio on me.html and staff_user.html: normal / for_free / cash_paid; cash_paid auto-creates matching UserPayment(method=cash)
- Dashboard finance section shows "Kasse (bar)" sum of all cash payments for cross-checking
- staff_pay prefill lower-capped at 0; "Zahlung eintragen" always visible on staff_user

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 20:02:14 +02:00
parent 51d079a467
commit d612acd715
8 changed files with 173 additions and 70 deletions
+23
View File
@@ -280,6 +280,28 @@ section {
text-shadow: 0 0 12px #CC6611;
}
.total-box-settled {
border-color: #3a7a44;
background-color: rgba(30, 80, 40, 0.35);
}
.total-settled {
color: #66cc77;
font-size: 1.6rem;
font-weight: bold;
text-shadow: 0 0 12px #2a6633;
}
.total-breakdown {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 4px 6px;
margin-top: 4px;
font-size: 0.82rem;
color: #889988;
}
.for-free-toggle {
display: flex;
flex-direction: row;
@@ -314,6 +336,7 @@ section {
color: #EE9933;
}
.drink-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
+19 -4
View File
@@ -282,6 +282,9 @@ def book_view(request):
raise Http404
drink_id = request.POST.get("drink_id")
booking_mode = request.POST.get("booking_mode", "normal")
for_free = booking_mode == "for_free"
cash_paid = booking_mode == "cash_paid"
try:
drink = Drink.objects.get(pk=int(drink_id))
except (Drink.DoesNotExist, TypeError, ValueError):
@@ -292,9 +295,17 @@ def book_view(request):
drink=drink,
amount=1,
day=_current_festival_day(),
for_free=False,
for_free=for_free,
)
return HttpResponseRedirect(f"{reverse('suff:me')}?booked={drink.id}")
if cash_paid:
UserPayment.objects.create(
user=request.user,
amount=drink.sale_price_per_bottle,
method=UserPayment.Method.cash,
note=f"Auto: {drink.name}",
)
free_suffix = "&free=1" if for_free else ""
return HttpResponseRedirect(f"{reverse('suff:me')}?booked={drink.id}{free_suffix}")
@login_required
@@ -546,7 +557,9 @@ 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"
booking_mode = request.POST.get("booking_mode", "normal")
for_free = booking_mode == "for_free"
cash_paid = booking_mode == "cash_paid"
try:
drink = Drink.objects.get(pk=int(drink_id))
except (Drink.DoesNotExist, TypeError, ValueError):
@@ -559,7 +572,7 @@ def staff_book_view(request, username):
day=_current_festival_day(),
for_free=for_free,
)
if _is_anonymous(target) and not for_free:
if cash_paid or (_is_anonymous(target) and not for_free):
UserPayment.objects.create(
user=target,
amount=drink.sale_price_per_bottle,
@@ -848,6 +861,7 @@ def dashboard_view(request):
.annotate(s=Sum("amount"))
)
method_split = {r["method"]: r["s"] for r in method_rows}
cash_total = method_split.get(UserPayment.Method.cash, 0)
# Free drinks
free_recipient_rows = (
@@ -893,6 +907,7 @@ def dashboard_view(request):
"biggest_tip": biggest_tip,
"total_entry": total_entry,
"method_split": method_split,
"cash_total": cash_total,
"top_free_recipient": top_free_recipient,
"free_total_count": free_total_count,
}
+4
View File
@@ -33,6 +33,10 @@
<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">Kasse (bar)</span>
<span class="hist-price">{{ cash_total|euro }}</span>
</li>
<li class="fin-row">
<span class="hist-what">Einkaufspreis Getränke</span>
<span class="hist-price">{{ purchase_cost|euro }}</span>
+39 -18
View File
@@ -21,14 +21,29 @@
</div>
{% endif %}
<section class="total-box">
<span class="total-label">Deine Rechnung</span>
<span class="total-value">{{ total|floatformat:2 }} €</span>
{% if paid %}
<span class="total-label">Bezahlt</span>
<span class="total-value">{{ paid|floatformat:2 }} €</span>
<span class="total-label">Offen</span>
<span class="total-value">{{ open_balance|floatformat:2 }} €</span>
<section class="total-box {% if paid and open_balance <= 0 %}total-box-settled{% endif %}">
{% if paid and open_balance <= 0 %}
<span class="total-settled">Bezahlt ✓</span>
<div class="total-breakdown">
<span>Drinks {{ total|floatformat:2 }} €</span>
<span>·</span>
<span>Bezahlt {{ paid|floatformat:2 }} €</span>
<span>·</span>
{% if open_balance < 0 %}
<span>Spende {{ open_balance|floatformat:2|slice:"1:" }} €</span>
{% else %}
<span>Genau bezahlt</span>
{% endif %}
</div>
{% else %}
<span class="total-label">Deine Rechnung</span>
<span class="total-value">{{ total|floatformat:2 }} €</span>
{% if paid %}
<span class="total-label">Bezahlt</span>
<span class="total-value">{{ paid|floatformat:2 }} €</span>
<span class="total-label">Offen</span>
<span class="total-value">{{ open_balance|floatformat:2 }} €</span>
{% endif %}
{% endif %}
</section>
@@ -37,19 +52,26 @@
{% if phase == "booking" %}
<section>
<h3>Neues Getränk buchen</h3>
<div class="drink-grid">
{% for drink in drinks %}
<form method="post" action="{% url 'suff:book' %}">
{% 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:book' %}">
{% csrf_token %}
<label class="for-free-toggle">
<input type="radio" name="booking_mode" value="for_free" />
<span>Gratis (z.B. Artists am Spieltag)</span>
</label>
<label class="for-free-toggle">
<input type="radio" name="booking_mode" value="cash_paid" />
<span>Direkt bar bezahlt</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 %}
@@ -80,7 +102,6 @@
<div class="empty-state">
<p class="empty-emoji">🍺</p>
<p>Noch nichts gebucht.</p>
{% if phase == "booking" %}<p class="muted">Tipp dich rein, sobald du was trinkst!</p>{% endif %}
</div>
{% endif %}
</section>
+24 -9
View File
@@ -4,15 +4,30 @@
{% block content %}
<h2>Spenden</h2>
<section class="total-box">
<span class="total-label">Deine Rechnung</span>
<span class="total-value">{{ total|floatformat:2 }} €</span>
{% if paid %}
<span class="total-label">Bezahlt</span>
<span class="total-value">{{ paid|floatformat:2 }} €</span>
<section class="total-box {% if paid and open_balance <= 0 %}total-box-settled{% endif %}">
{% if paid and open_balance <= 0 %}
<span class="total-settled">Bezahlt ✓</span>
<div class="total-breakdown">
<span>Drinks {{ total|floatformat:2 }} €</span>
<span>·</span>
<span>Bezahlt {{ paid|floatformat:2 }} €</span>
<span>·</span>
{% if open_balance < 0 %}
<span>Spende {{ open_balance|floatformat:2|slice:"1:" }} €</span>
{% else %}
<span>Genau bezahlt</span>
{% endif %}
</div>
{% else %}
<span class="total-label">Deine Rechnung</span>
<span class="total-value">{{ total|floatformat:2 }} €</span>
{% if paid %}
<span class="total-label">Bezahlt</span>
<span class="total-value">{{ paid|floatformat:2 }} €</span>
{% endif %}
<span class="total-label">Offen (Drinks)</span>
<span class="total-value">{{ open_balance|floatformat:2 }} €</span>
{% endif %}
<span class="total-label">Offen (Drinks)</span>
<span class="total-value">{{ open_balance|floatformat:2 }} €</span>
</section>
<p class="intro">
@@ -24,7 +39,7 @@
<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
hilft uns das sehr, die Kosten für Bands, Toiletten usw. zu
decken. So oder so: <strong>danke, dass du da bist!</strong>
</p>
+24 -9
View File
@@ -4,15 +4,30 @@
<p class="muted">Crew-Ansicht</p>
<h2>Zahlung für <span class="staff-target">{{ tab_user.username }}</span></h2>
<section class="total-box">
<span class="total-label">Rechnung</span>
<span class="total-value">{{ total|floatformat:2 }} €</span>
{% if paid %}
<span class="total-label">Bezahlt</span>
<span class="total-value">{{ paid|floatformat:2 }} €</span>
<section class="total-box {% if paid and open_balance <= 0 %}total-box-settled{% endif %}">
{% if paid and open_balance <= 0 %}
<span class="total-settled">Bezahlt ✓</span>
<div class="total-breakdown">
<span>Drinks {{ total|floatformat:2 }} €</span>
<span>·</span>
<span>Bezahlt {{ paid|floatformat:2 }} €</span>
<span>·</span>
{% if open_balance < 0 %}
<span>Spende {{ open_balance|floatformat:2|slice:"1:" }} €</span>
{% else %}
<span>Genau bezahlt</span>
{% endif %}
</div>
{% else %}
<span class="total-label">Rechnung</span>
<span class="total-value">{{ total|floatformat:2 }} €</span>
{% if paid %}
<span class="total-label">Bezahlt</span>
<span class="total-value">{{ paid|floatformat:2 }} €</span>
{% endif %}
<span class="total-label">Offen</span>
<span class="total-value">{{ open_balance|floatformat:2 }} €</span>
{% endif %}
<span class="total-label">Offen</span>
<span class="total-value">{{ open_balance|floatformat:2 }} €</span>
</section>
{% if error %}
@@ -24,7 +39,7 @@
<label>
Betrag (€)
<input type="number" name="amount" step="0.01" min="0.01"
value="{{ open_balance|floatformat:2 }}" required />
value="{% if open_balance > 0 %}{{ open_balance|floatformat:2 }}{% else %}0.00{% endif %}" required />
</label>
<label>
Methode
+30 -13
View File
@@ -19,20 +19,33 @@
{% if is_anonymous_target %}
<p class="muted">Anonyme Buchungen werden automatisch als bar bezahlt eingetragen.</p>
{% else %}
<section class="total-box">
<span class="total-label">Rechnung</span>
<span class="total-value">{{ total|floatformat:2 }} €</span>
{% if paid %}
<span class="total-label">Bezahlt</span>
<span class="total-value">{{ paid|floatformat:2 }} €</span>
<span class="total-label">Offen</span>
<span class="total-value">{{ open_balance|floatformat:2 }} €</span>
<section class="total-box {% if paid and open_balance <= 0 %}total-box-settled{% endif %}">
{% if paid and open_balance <= 0 %}
<span class="total-settled">Bezahlt ✓</span>
<div class="total-breakdown">
<span>Drinks {{ total|floatformat:2 }} €</span>
<span>·</span>
<span>Bezahlt {{ paid|floatformat:2 }} €</span>
<span>·</span>
{% if open_balance < 0 %}
<span>Spende {{ open_balance|floatformat:2|slice:"1:" }} €</span>
{% else %}
<span>Genau bezahlt</span>
{% endif %}
</div>
{% else %}
<span class="total-label">Rechnung</span>
<span class="total-value">{{ total|floatformat:2 }} €</span>
{% if paid %}
<span class="total-label">Bezahlt</span>
<span class="total-value">{{ paid|floatformat:2 }} €</span>
<span class="total-label">Offen</span>
<span class="total-value">{{ open_balance|floatformat:2 }} €</span>
{% endif %}
{% endif %}
</section>
{% if open_balance > 0 %}
<p><a href="{% url 'suff:staff_pay' tab_user.username %}" class="btn-primary">Zahlung eintragen</a></p>
{% endif %}
<p><a href="{% url 'suff:staff_pay' tab_user.username %}" class="btn-primary">Zahlung eintragen</a></p>
{% endif %}
{% if phase == "booking" %}
@@ -41,8 +54,12 @@
<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>
<input type="radio" name="booking_mode" value="for_free" />
<span>Gratis (z.B. Artists am Spieltag)</span>
</label>
<label class="for-free-toggle">
<input type="radio" name="booking_mode" value="cash_paid" />
<span>Direkt bar bezahlt</span>
</label>
<div class="drink-grid">
{% for drink in drinks %}