Compare commits
3 Commits
06803389b8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b5142ce3c7 | |||
| ae9d749356 | |||
| 89ea65a0c8 |
+2
-2
@@ -210,8 +210,8 @@ class DonationAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Payment)
|
@admin.register(Payment)
|
||||||
class PaymentAdmin(admin.ModelAdmin):
|
class PaymentAdmin(admin.ModelAdmin):
|
||||||
list_display = ("date", "purpose", "amount")
|
list_display = ("date", "purpose", "amount", "method")
|
||||||
list_filter = ("date",)
|
list_filter = ("method", "date")
|
||||||
ordering = ("-date",)
|
ordering = ("-date",)
|
||||||
search_fields = ("purpose",)
|
search_fields = ("purpose",)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
from decimal import Decimal, ROUND_HALF_UP
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db.models import F, Sum
|
||||||
|
|
||||||
|
from gaehsnitz.models import Consumption, Payment, UserPayment
|
||||||
|
|
||||||
|
|
||||||
|
YEAR = 2026
|
||||||
|
CASH_PREFILL = Decimal("500.00")
|
||||||
|
|
||||||
|
|
||||||
|
def eur(amount):
|
||||||
|
return str(Decimal(amount).quantize(Decimal("1"), rounding=ROUND_HALF_UP))
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Print full 2026 festival finance summary"
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
w = self.stdout.write
|
||||||
|
sep = "-" * 40
|
||||||
|
|
||||||
|
# --- Ausgaben ---
|
||||||
|
w("\nAUSGABEN")
|
||||||
|
w(sep)
|
||||||
|
payments = Payment.objects.filter(date__year=YEAR).order_by("date")
|
||||||
|
total_out = Decimal("0")
|
||||||
|
for p in payments:
|
||||||
|
method_label = dict(Payment.Method.choices).get(p.method, p.method)
|
||||||
|
w(f" {p.date} {eur(p.amount):>6}€ {method_label:<15} {p.purpose}")
|
||||||
|
total_out += p.amount
|
||||||
|
w(sep)
|
||||||
|
w(f" {'TOTAL':<11} {eur(total_out):>6}€")
|
||||||
|
|
||||||
|
# by method
|
||||||
|
w("")
|
||||||
|
for method, label in Payment.Method.choices:
|
||||||
|
subtotal = payments.filter(method=method).aggregate(s=Sum("amount"))["s"] or Decimal("0")
|
||||||
|
if subtotal:
|
||||||
|
w(f" {label:<15} {eur(subtotal):>6}€")
|
||||||
|
|
||||||
|
# --- Einnahmen ---
|
||||||
|
w("\nEINNAHMEN")
|
||||||
|
w(sep)
|
||||||
|
up = UserPayment.objects.filter(created_at__year=YEAR)
|
||||||
|
|
||||||
|
cash_prefill = CASH_PREFILL
|
||||||
|
cash_payments = up.filter(method="cash").aggregate(s=Sum("amount"))["s"] or Decimal("0")
|
||||||
|
cash_from_sales = cash_payments - cash_prefill
|
||||||
|
|
||||||
|
non_cash = up.exclude(method="cash").aggregate(s=Sum("amount"))["s"] or Decimal("0")
|
||||||
|
|
||||||
|
total_in = cash_from_sales + non_cash
|
||||||
|
|
||||||
|
drink_revenue = Consumption.objects.filter(for_free=False, drink__year=YEAR).annotate(
|
||||||
|
cost=F("amount") * F("drink__sale_price_per_bottle")
|
||||||
|
).aggregate(s=Sum("cost"))["s"] or Decimal("0")
|
||||||
|
entry_donations = total_in - drink_revenue
|
||||||
|
|
||||||
|
w(f" Cash: {eur(cash_from_sales):>6}€")
|
||||||
|
w(f" Cashless: {eur(non_cash):>6}€")
|
||||||
|
w(f" Einnahmen Gesamt: {eur(total_in):>6}€")
|
||||||
|
w(f" - Getränke-Umsatz: {eur(drink_revenue):>6}€")
|
||||||
|
w(f" - Eintrittsspenden: {eur(entry_donations):>6}€")
|
||||||
|
|
||||||
|
# --- Kassensaldo ---
|
||||||
|
w("\nKASSE")
|
||||||
|
w(sep)
|
||||||
|
cash_out = payments.filter(method="cash").aggregate(s=Sum("amount"))["s"] or Decimal("0")
|
||||||
|
expected_cash = cash_prefill + cash_from_sales - cash_out
|
||||||
|
w(f" Kassen-Vorschuss: {eur(cash_prefill):>6}€")
|
||||||
|
w(f" Cash-Einnahmen: {eur(cash_from_sales):>6}€")
|
||||||
|
w(f" Cash-Ausgaben: {eur(cash_out):>6}€")
|
||||||
|
w(f" -> in Kasse: {eur(expected_cash):>6}€")
|
||||||
|
|
||||||
|
# --- Gesamtbilanz ---
|
||||||
|
w("\nGESAMTBILANZ")
|
||||||
|
w(sep)
|
||||||
|
net = total_in - total_out
|
||||||
|
out_of_pocket = total_out - total_in
|
||||||
|
w(f" Gesamtausgaben: {eur(total_out):>6}€")
|
||||||
|
w(f" Gesamteinnahmen: {eur(total_in):>6}€")
|
||||||
|
w(sep)
|
||||||
|
label = "Verlust" if out_of_pocket > 0 else "Gewinn"
|
||||||
|
w(f" {label}: {eur(abs(net)):>6}€\n")
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
from gaehsnitz.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Merge source user into target user, then delete source"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument("source", help="Username of user to merge FROM (will be deleted)")
|
||||||
|
parser.add_argument("target", help="Username of user to merge INTO (will be kept)")
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
source_name = options["source"]
|
||||||
|
target_name = options["target"]
|
||||||
|
|
||||||
|
if source_name == target_name:
|
||||||
|
raise CommandError("Source and target must be different users")
|
||||||
|
|
||||||
|
try:
|
||||||
|
source = User.objects.get(username=source_name)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
raise CommandError(f"Source user '{source_name}' not found")
|
||||||
|
|
||||||
|
try:
|
||||||
|
target = User.objects.get(username=target_name)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
raise CommandError(f"Target user '{target_name}' not found")
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
payments = source.user_payments.count()
|
||||||
|
consumptions = source.consumption_list.count()
|
||||||
|
|
||||||
|
source.user_payments.update(user=target)
|
||||||
|
source.consumption_list.update(user=target)
|
||||||
|
|
||||||
|
source.delete()
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f"Merged '{source_name}' into '{target_name}': "
|
||||||
|
f"{payments} payment(s), {consumptions} consumption(s) reassigned. "
|
||||||
|
f"Source user deleted."
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
from gaehsnitz.models import Drink
|
||||||
|
|
||||||
|
# Actual crates bought from supplier (all others were returned full, no cost)
|
||||||
|
# (name, crates_ordered, crates_purchased)
|
||||||
|
# crates_returned = crates_purchased (all empties back, full deposit refunded)
|
||||||
|
ACTUAL_CRATES = [
|
||||||
|
("Sternburg Export", 12, 11),
|
||||||
|
("Ur-Krostitzer", 5, 2),
|
||||||
|
("Budweiser", 5, 2),
|
||||||
|
("Altenburger Helles", 5, 5),
|
||||||
|
("Feldschl. Radler", 2, 1),
|
||||||
|
("Lübzer Grapef. 0,0", 1, 1),
|
||||||
|
("Freiberger 0,0", 4, 3),
|
||||||
|
("Club Mate", 2, 2),
|
||||||
|
("Vita Cola", 2, 1),
|
||||||
|
("Paulaner Spezi", 2, 2),
|
||||||
|
("Wasser", 10, 5),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Update 2026 drink crates to actual purchased amounts"
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
with transaction.atomic():
|
||||||
|
for name, ordered, purchased in ACTUAL_CRATES:
|
||||||
|
updated = Drink.objects.filter(name=name, year=2026).update(
|
||||||
|
crates_ordered=ordered,
|
||||||
|
crates_purchased=purchased,
|
||||||
|
crates_returned=purchased,
|
||||||
|
)
|
||||||
|
if updated:
|
||||||
|
self.stdout.write(f" {name}: ordered={ordered}, purchased={purchased}, returned={purchased}")
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.WARNING(f" NOT FOUND: {name}"))
|
||||||
|
self.stdout.write(self.style.SUCCESS("Done."))
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.6 on 2026-06-20 12:06
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gaehsnitz', '0010_add_sekt_category'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='payment',
|
||||||
|
name='method',
|
||||||
|
field=models.CharField(choices=[('cash', 'Bar'), ('card', 'EC-Karte'), ('paypal', 'PayPal'), ('bank', 'Überweisung')], default='paypal', max_length=16, verbose_name='Methode'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -73,9 +73,16 @@ class Payment(models.Model):
|
|||||||
bands = 11, "Bands"
|
bands = 11, "Bands"
|
||||||
supply_purchase = 12, "Getränke-/Essenseinkauf"
|
supply_purchase = 12, "Getränke-/Essenseinkauf"
|
||||||
|
|
||||||
|
class Method(models.TextChoices):
|
||||||
|
cash = "cash", "Bar"
|
||||||
|
card = "card", "EC-Karte"
|
||||||
|
paypal = "paypal", "PayPal"
|
||||||
|
bank = "bank", "Überweisung"
|
||||||
|
|
||||||
purpose = models.CharField("Zweck", max_length=64)
|
purpose = models.CharField("Zweck", max_length=64)
|
||||||
date = models.DateField("Datum")
|
date = models.DateField("Datum")
|
||||||
amount = PriceField("Betrag")
|
amount = PriceField("Betrag")
|
||||||
|
method = models.CharField("Methode", max_length=16, choices=Method.choices, default=Method.paypal)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Ausgabe"
|
verbose_name = "Ausgabe"
|
||||||
|
|||||||
+15
-5
@@ -1,5 +1,6 @@
|
|||||||
import math
|
import math
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from decimal import Decimal
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -24,7 +25,7 @@ ANONYMOUS_USERNAME = "anonym"
|
|||||||
BERLIN = ZoneInfo("Europe/Berlin")
|
BERLIN = ZoneInfo("Europe/Berlin")
|
||||||
# Festival window: 2026-05-30 10:00 – 2026-06-14 22:00 Berlin time.
|
# Festival window: 2026-05-30 10:00 – 2026-06-14 22:00 Berlin time.
|
||||||
BOOKING_START = datetime(2026, 5, 30, 10, 0, 0, tzinfo=BERLIN)
|
BOOKING_START = datetime(2026, 5, 30, 10, 0, 0, tzinfo=BERLIN)
|
||||||
BOOKING_END = datetime(2026, 6, 14, 22, 0, 0, tzinfo=BERLIN)
|
BOOKING_END = datetime(2026, 6, 17, 23, 59, 0, tzinfo=BERLIN)
|
||||||
|
|
||||||
DAY_BY_WEEKDAY = {3: 1, 4: 2, 5: 3, 6: 4}
|
DAY_BY_WEEKDAY = {3: 1, 4: 2, 5: 3, 6: 4}
|
||||||
DAY_CUTOFF_HOUR = 6
|
DAY_CUTOFF_HOUR = 6
|
||||||
@@ -177,9 +178,11 @@ def name_view(request):
|
|||||||
existing = None
|
existing = None
|
||||||
|
|
||||||
if existing is None:
|
if existing is None:
|
||||||
request.session["pending_username"] = username
|
return render(
|
||||||
request.session["pending_mode"] = "create"
|
request,
|
||||||
return HttpResponseRedirect(reverse("suff:pin"))
|
"suff/party_over.html",
|
||||||
|
{"phase": phase, "username": username},
|
||||||
|
)
|
||||||
|
|
||||||
if not existing.pin:
|
if not existing.pin:
|
||||||
if _user_has_activity(existing):
|
if _user_has_activity(existing):
|
||||||
@@ -745,9 +748,14 @@ DAY_LABELS = {1: "Donnerstag", 2: "Freitag", 3: "Samstag", 4: "Sonntag"}
|
|||||||
def dashboard_view(request):
|
def dashboard_view(request):
|
||||||
year = current_year()
|
year = current_year()
|
||||||
|
|
||||||
|
CASH_PREFILL = Decimal("500.00")
|
||||||
|
|
||||||
total_donations = Donation.objects.filter(date__year=year).aggregate(s=Sum("amount"))["s"] or 0
|
total_donations = Donation.objects.filter(date__year=year).aggregate(s=Sum("amount"))["s"] or 0
|
||||||
total_costs = Payment.objects.filter(date__year=year).aggregate(s=Sum("amount"))["s"] or 0
|
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
|
cash_raw = UserPayment.objects.filter(created_at__year=year, method="cash").aggregate(s=Sum("amount"))["s"] or Decimal("0")
|
||||||
|
cash_net = cash_raw - CASH_PREFILL
|
||||||
|
non_cash = UserPayment.objects.filter(created_at__year=year).exclude(method="cash").aggregate(s=Sum("amount"))["s"] or Decimal("0")
|
||||||
|
user_payments_total = cash_net + non_cash # real income, prefill excluded
|
||||||
|
|
||||||
drinks = list(Drink.objects.filter(year=year).order_by("name"))
|
drinks = list(Drink.objects.filter(year=year).order_by("name"))
|
||||||
drink_rows = [
|
drink_rows = [
|
||||||
@@ -956,6 +964,8 @@ def dashboard_view(request):
|
|||||||
"total_entry": total_entry,
|
"total_entry": total_entry,
|
||||||
"method_split": method_split,
|
"method_split": method_split,
|
||||||
"cash_total": cash_total,
|
"cash_total": cash_total,
|
||||||
|
"cash_prefill": CASH_PREFILL,
|
||||||
|
"cash_net": cash_net,
|
||||||
"top_free_recipient": top_free_recipient,
|
"top_free_recipient": top_free_recipient,
|
||||||
"free_total_count": free_total_count,
|
"free_total_count": free_total_count,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,12 +30,16 @@
|
|||||||
<span class="hist-price">{{ total_donations|euro }}</span>
|
<span class="hist-price">{{ total_donations|euro }}</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="fin-row">
|
<li class="fin-row">
|
||||||
<span class="hist-what">Zahlungen (User-Tab)</span>
|
<span class="hist-what">Cashless (User-Tab)</span>
|
||||||
<span class="hist-price">{{ user_payments_total|euro }}</span>
|
<span class="hist-price">{{ user_payments_total|euro }}</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="fin-row">
|
<li class="fin-row">
|
||||||
<span class="hist-what">Kasse (bar)</span>
|
<span class="hist-what">Kasse (bar, nach Vorschuss)</span>
|
||||||
<span class="hist-price">{{ cash_total|euro }}</span>
|
<span class="hist-price">{{ cash_net|euro }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="fin-row">
|
||||||
|
<span class="hist-what">Kassen-Vorschuss</span>
|
||||||
|
<span class="hist-price">{{ cash_prefill|euro }}</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="fin-row">
|
<li class="fin-row">
|
||||||
<span class="hist-what">Einkaufspreis Getränke</span>
|
<span class="hist-what">Einkaufspreis Getränke</span>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{% extends "suff/base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Party vorbei!</h2>
|
||||||
|
<p>
|
||||||
|
Der Name <b>{{ username }}</b> existiert noch nicht —
|
||||||
|
neue Accounts können nicht mehr angelegt werden, weil das Festival vorbei ist.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Du hast schon einen Account? Gib deinen Namen nochmal ein.
|
||||||
|
</p>
|
||||||
|
<div class="link-row">
|
||||||
|
<a href="{% url 'suff:name' %}" class="link-btn">Zurück</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user