diff --git a/gaehsnitz/static/suff/style.css b/gaehsnitz/static/suff/style.css
index c6177ed..a68da67 100644
--- a/gaehsnitz/static/suff/style.css
+++ b/gaehsnitz/static/suff/style.css
@@ -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);
diff --git a/gaehsnitz/suff.py b/gaehsnitz/suff.py
index 7cd77ed..1d01aed 100644
--- a/gaehsnitz/suff.py
+++ b/gaehsnitz/suff.py
@@ -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,
}
diff --git a/gaehsnitz/templates/suff/dashboard.html b/gaehsnitz/templates/suff/dashboard.html
index 3ce89ba..aef4cda 100644
--- a/gaehsnitz/templates/suff/dashboard.html
+++ b/gaehsnitz/templates/suff/dashboard.html
@@ -33,6 +33,10 @@
Zahlungen (User-Tab)
{{ user_payments_total|euro }}
+
+ Kasse (bar)
+ {{ cash_total|euro }}
+
Einkaufspreis Getränke
{{ purchase_cost|euro }}
diff --git a/gaehsnitz/templates/suff/me.html b/gaehsnitz/templates/suff/me.html
index 1f4edd8..407fbc2 100644
--- a/gaehsnitz/templates/suff/me.html
+++ b/gaehsnitz/templates/suff/me.html
@@ -21,14 +21,29 @@
{% endif %}
-
- Deine Rechnung
- {{ total|floatformat:2 }} €
- {% if paid %}
- Bezahlt
- {{ paid|floatformat:2 }} €
- Offen
- {{ open_balance|floatformat:2 }} €
+
+ {% if paid and open_balance <= 0 %}
+ Bezahlt ✓
+
+ Drinks {{ total|floatformat:2 }} €
+ ·
+ Bezahlt {{ paid|floatformat:2 }} €
+ ·
+ {% if open_balance < 0 %}
+ Spende {{ open_balance|floatformat:2|slice:"1:" }} €
+ {% else %}
+ Genau bezahlt
+ {% endif %}
+
+ {% else %}
+ Deine Rechnung
+ {{ total|floatformat:2 }} €
+ {% if paid %}
+ Bezahlt
+ {{ paid|floatformat:2 }} €
+ Offen
+ {{ open_balance|floatformat:2 }} €
+ {% endif %}
{% endif %}
@@ -37,19 +52,26 @@
{% if phase == "booking" %}
Neues Getränk buchen
-
- {% for drink in drinks %}
-
+
{% endif %}
@@ -80,7 +102,6 @@
🍺
Noch nichts gebucht.
- {% if phase == "booking" %}
Tipp dich rein, sobald du was trinkst!
{% endif %}
{% endif %}
diff --git a/gaehsnitz/templates/suff/pay.html b/gaehsnitz/templates/suff/pay.html
index b38b264..c622ba3 100644
--- a/gaehsnitz/templates/suff/pay.html
+++ b/gaehsnitz/templates/suff/pay.html
@@ -4,15 +4,30 @@
{% block content %}
Spenden
-
- Deine Rechnung
- {{ total|floatformat:2 }} €
- {% if paid %}
- Bezahlt
- {{ paid|floatformat:2 }} €
+
+ {% if paid and open_balance <= 0 %}
+ Bezahlt ✓
+
+ Drinks {{ total|floatformat:2 }} €
+ ·
+ Bezahlt {{ paid|floatformat:2 }} €
+ ·
+ {% if open_balance < 0 %}
+ Spende {{ open_balance|floatformat:2|slice:"1:" }} €
+ {% else %}
+ Genau bezahlt
+ {% endif %}
+
+ {% else %}
+ Deine Rechnung
+ {{ total|floatformat:2 }} €
+ {% if paid %}
+ Bezahlt
+ {{ paid|floatformat:2 }} €
+ {% endif %}
+ Offen (Drinks)
+ {{ open_balance|floatformat:2 }} €
{% endif %}
- Offen (Drinks)
- {{ open_balance|floatformat:2 }} €
@@ -24,7 +39,7 @@
Du darfst gerne weniger geben, wenn das gerade besser
passt – kein Problem. Und wenn du mehr 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: danke, dass du da bist!
diff --git a/gaehsnitz/templates/suff/staff_pay.html b/gaehsnitz/templates/suff/staff_pay.html
index 584fdbc..b76d496 100644
--- a/gaehsnitz/templates/suff/staff_pay.html
+++ b/gaehsnitz/templates/suff/staff_pay.html
@@ -4,15 +4,30 @@
Crew-Ansicht
Zahlung für {{ tab_user.username }}
-
- Rechnung
- {{ total|floatformat:2 }} €
- {% if paid %}
- Bezahlt
- {{ paid|floatformat:2 }} €
+
+ {% if paid and open_balance <= 0 %}
+ Bezahlt ✓
+
+ Drinks {{ total|floatformat:2 }} €
+ ·
+ Bezahlt {{ paid|floatformat:2 }} €
+ ·
+ {% if open_balance < 0 %}
+ Spende {{ open_balance|floatformat:2|slice:"1:" }} €
+ {% else %}
+ Genau bezahlt
+ {% endif %}
+
+ {% else %}
+ Rechnung
+ {{ total|floatformat:2 }} €
+ {% if paid %}
+ Bezahlt
+ {{ paid|floatformat:2 }} €
+ {% endif %}
+ Offen
+ {{ open_balance|floatformat:2 }} €
{% endif %}
- Offen
- {{ open_balance|floatformat:2 }} €
{% if error %}
@@ -24,7 +39,7 @@
Betrag (€)
+ value="{% if open_balance > 0 %}{{ open_balance|floatformat:2 }}{% else %}0.00{% endif %}" required />
Methode
diff --git a/gaehsnitz/templates/suff/staff_user.html b/gaehsnitz/templates/suff/staff_user.html
index 701ee7b..3eb3000 100644
--- a/gaehsnitz/templates/suff/staff_user.html
+++ b/gaehsnitz/templates/suff/staff_user.html
@@ -19,20 +19,33 @@
{% if is_anonymous_target %}
Anonyme Buchungen werden automatisch als bar bezahlt eingetragen.
{% else %}
-
- Rechnung
- {{ total|floatformat:2 }} €
- {% if paid %}
- Bezahlt
- {{ paid|floatformat:2 }} €
- Offen
- {{ open_balance|floatformat:2 }} €
+
+ {% if paid and open_balance <= 0 %}
+ Bezahlt ✓
+
+ Drinks {{ total|floatformat:2 }} €
+ ·
+ Bezahlt {{ paid|floatformat:2 }} €
+ ·
+ {% if open_balance < 0 %}
+ Spende {{ open_balance|floatformat:2|slice:"1:" }} €
+ {% else %}
+ Genau bezahlt
+ {% endif %}
+
+ {% else %}
+ Rechnung
+ {{ total|floatformat:2 }} €
+ {% if paid %}
+ Bezahlt
+ {{ paid|floatformat:2 }} €
+ Offen
+ {{ open_balance|floatformat:2 }} €
+ {% endif %}
{% endif %}
- {% if open_balance > 0 %}
- Zahlung eintragen
- {% endif %}
+ Zahlung eintragen
{% endif %}
{% if phase == "booking" %}
@@ -41,8 +54,12 @@
{% csrf_token %}
-
- Gratis (z.b. Artists am Spieltag)
+
+ Gratis (z.B. Artists am Spieltag)
+
+
+
+ Direkt bar bezahlt
{% for drink in drinks %}
diff --git a/suff.md b/suff.md
index 3cac3fb..f4b4ab7 100644
--- a/suff.md
+++ b/suff.md
@@ -20,7 +20,10 @@ Username = `slugify(input)`. POST name:
## Booking
- `/suff/me/` shows: greeting, total/paid/open balance, drink grid, day-grouped history.
-- `+1` POST creates `Consumption(amount=1, day=current_weekday, for_free=False)`.
+- Single form wraps two radio buttons (`booking_mode`) + all drink buttons (`name="drink_id"`).
+- `booking_mode` values: `normal` (default, no radio selected), `for_free`, `cash_paid`.
+- `+1` POST creates `Consumption(amount=1, day=current_weekday, for_free=...)`.
+- `cash_paid` booking auto-creates matching `UserPayment(method=cash)` — same as anonymous walk-ins.
- Trash icon per row → `confirm_delete.html` → POST deletes own consumption (booking phase only).
- History grouped by festival day, newest-first per day.
@@ -29,6 +32,7 @@ Username = `slugify(input)`. POST name:
- `/suff/pay/` — user enters amount + method (cash/paypal/bank/other) + optional note. Creates `UserPayment`. Pre-fills with current `open_balance`.
- Method choices: `UserPayment.Method`.
- Open balance = sum(Consumption.price where !for_free) − sum(UserPayment.amount).
+- Balance panel shows settled state (green "Bezahlt ✓" + breakdown) when `open_balance <= 0`; amber with "Offen X €" otherwise. Applied on `/suff/me/`, `/suff/pay/`, `/suff/staff/u//`, `/suff/staff/u//pay/`.
## Crew (`is_staff`)
@@ -36,11 +40,13 @@ Separate page tree under `/suff/staff/`:
- `/suff/staff/` — alphabetical user list, anonymous gast on top, "Neuen Benutzer anlegen" link.
- `/suff/staff/new/` — register user. Name required, PIN optional 3 digits.
-- `/suff/staff/u//` — book/pay/delete for that user. Mirrors `me.html`.
+- `/suff/staff/u//` — book/pay/delete for that user. Mirrors `me.html`. "Zahlung eintragen" link always visible.
- `/suff/staff/u//pin/` — overwrite PIN (3 digits required, no clear).
-- `/suff/staff/u//pay/` — record payment for that user.
+- `/suff/staff/u//pay/` — record payment for that user. Amount pre-fills with `max(open_balance, 0)`.
- `/suff/staff/u//book//delete/` — delete consumption (and matching auto-payment if anon).
+Staff booking form has same `booking_mode` radios (normal / for_free / cash_paid) as user view.
+
Target username highlighted via `.staff-target` (cyan pill) on every crew page.
## Anonymous walk-ins
@@ -60,6 +66,7 @@ Target username highlighted via `.staff-target` (cyan pill) on every crew page.
## Dashboard
- `/suff/dashboard/` (staff only). Donations vs. expenses with progress bar, drink inventory rows, refinance %, per-user open balances, top spender, top drink, busiest day, top drinker per day.
+- Finance section includes "Kasse (bar)" — sum of all `UserPayment(method=cash)` — for cross-checking real cash in the box.
## Drink categories
@@ -82,24 +89,10 @@ Mobile-first dark theme. `#161616` bg, `#EE9933`/`#FFCC77` amber, `#885522` brow
## Open ideas / next session
-### Entry fee handling
-
-Festival entry is flexible (10–30 €, sometimes paid as overpayment on drink tab). Options brainstormed:
-
-- **A. Implicit overpayment** — pre-fill pay form with `open_balance + 20`, treat anything above drinks as entry/donation. Zero schema change.
-- **B. Quick-pick suggestions** — pay page offers buttons: `Nur Getränke (X €)`, `+10`, `+20`, `+30`, plus free-form. Recommended.
-- **C. Explicit split** — separate entry field/checkbox. More UI, more accurate stats (`entry = paid − consumed`), more friction.
-
-Lean B. Stats can later derive entry as `paid − consumed_drinks_price` per user.
-
### Pay-on-the-spot / quick-pay-cash
Single button on `me` (and crew_user) page: "Offenen Betrag bar bezahlen" → creates `UserPayment(method=cash, amount=open_balance)`. Lets bar crew clear tab in one tap when guest pays cash directly. (Skipped for now, keep in mind.)
-### Round to nearest 10 €
-
-When pre-filling pay amount or showing quick-pick buttons, optionally round up to nearest multiple of 10 €. E.g. drinks 26.50 € → suggest 30, 40, 50. Easier cash handling, implicit donation/entry.
-
### Prepay vs. pay-at-end
Currently a single open balance. Could surface "Prepay 50 €" as a flow vs. "Pay at the end" — same data model, different framing. Maybe a "Vorkasse" preset on pay page.