- 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>
5.3 KiB
Suff – drink booking tool
Self-service drink tab for festival attendees. Lives at /suff/. Plain Django, no JS.
Auth
User.pin(hashed CharField, 3 digits), separate frompassword. Strong password stays for/admin/.PinBackendauthenticates byusername+pin.ModelBackendfirst inAUTHENTICATION_BACKENDSso admin still needs strong password.- PIN reset is crew-only. No self-reset, no random PINs, PINs never displayed.
Name flow
Username = slugify(input). POST name:
- not found →
createmode → mandatory 3-digit PIN → create user → login - found, has PIN →
loginmode → enter PIN - found, no PIN, no activity (no Consumption + no UserPayment) →
claimmode → set PIN → login - found, no PIN, has activity →
no_pin.html("ask someone at the bar")
Booking
/suff/me/shows: greeting, total/paid/open balance, drink grid, day-grouped history.- Single form wraps two radio buttons (
booking_mode) + all drink buttons (name="drink_id"). booking_modevalues:normal(default, no radio selected),for_free,cash_paid.+1POST createsConsumption(amount=1, day=current_weekday, for_free=...).cash_paidbooking auto-creates matchingUserPayment(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.
Payments
/suff/pay/— user enters amount + method (cash/paypal/bank/other) + optional note. CreatesUserPayment. Pre-fills with currentopen_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/<name>/,/suff/staff/u/<name>/pay/.
Crew (is_staff)
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/<name>/— book/pay/delete for that user. Mirrorsme.html. "Zahlung eintragen" link always visible./suff/staff/u/<name>/pin/— overwrite PIN (3 digits required, no clear)./suff/staff/u/<name>/pay/— record payment for that user. Amount pre-fills withmax(open_balance, 0)./suff/staff/u/<name>/book/<id>/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
- Seeded user
anonym(migration 0009). No PIN, never logs in. - Crew books for anonymous via staff_user page → drink booking auto-creates matching
UserPayment(method=cash, note="Auto: <drink>")so balance always 0. - Deleting an anonymous consumption removes one matching auto-payment.
- Anonymous has no pay page (404). PayPal walk-ins → register a real user instead.
Time gating (Berlin tz)
- Phases:
before/booking/closed. - Test window: 2026-05-15 → 2026-05-31. Original festival: 2026-06-11 → 2026-06-14.
closedshows static page; outside booking, all action endpoints redirect or 404.settings.PRODUCTION=Falseforcesbooking.
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
Drink.category: beer / alc_free_beer / radler / alc_free_radler / soft / water.- Buttons gradient-colored per category. Sorted by category in grid.
Files
gaehsnitz/auth_backends.py—PinBackendgaehsnitz/suff.py— all suff views + phase + crew helpersgaehsnitz/suff_urls.py— routesgaehsnitz/admin.py—SetPinForm+set_pin_viewgaehsnitz/templates/suff/{base,name,pin,no_pin,me,pay,dashboard,closed,confirm_delete,staff_index,staff_user,staff_pay,staff_register,staff_pin_reset,staff_confirm_delete}.htmlgaehsnitz/static/suff/{style.css,favicon.svg}gaehsnitz/migrations/0009_anonymous_user.py— seedsanonym
Frontend
Mobile-first dark theme. #161616 bg, #EE9933/#FFCC77 amber, #885522 brown borders, #66ddee cyan for crew target. Drink buttons gradient-colored per category. Toast banner for booking confirmation. :active scale feedback. SVG favicon.
Open ideas / next session
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.)
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.
Misc
- PWA manifest for add-to-homescreen
- Drink icons/emoji per type
- Style phase pages (
before/closed) - Per-user QR for fast crew lookup at the bar