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)
|
||||
class PaymentAdmin(admin.ModelAdmin):
|
||||
list_display = ("date", "purpose", "amount")
|
||||
list_filter = ("date",)
|
||||
list_display = ("date", "purpose", "amount", "method")
|
||||
list_filter = ("method", "date")
|
||||
ordering = ("-date",)
|
||||
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"
|
||||
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)
|
||||
date = models.DateField("Datum")
|
||||
amount = PriceField("Betrag")
|
||||
method = models.CharField("Methode", max_length=16, choices=Method.choices, default=Method.paypal)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Ausgabe"
|
||||
|
||||
+15
-5
@@ -1,5 +1,6 @@
|
||||
import math
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from django.conf import settings
|
||||
@@ -24,7 +25,7 @@ ANONYMOUS_USERNAME = "anonym"
|
||||
BERLIN = ZoneInfo("Europe/Berlin")
|
||||
# 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_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_CUTOFF_HOUR = 6
|
||||
@@ -177,9 +178,11 @@ def name_view(request):
|
||||
existing = None
|
||||
|
||||
if existing is None:
|
||||
request.session["pending_username"] = username
|
||||
request.session["pending_mode"] = "create"
|
||||
return HttpResponseRedirect(reverse("suff:pin"))
|
||||
return render(
|
||||
request,
|
||||
"suff/party_over.html",
|
||||
{"phase": phase, "username": username},
|
||||
)
|
||||
|
||||
if not existing.pin:
|
||||
if _user_has_activity(existing):
|
||||
@@ -745,9 +748,14 @@ DAY_LABELS = {1: "Donnerstag", 2: "Freitag", 3: "Samstag", 4: "Sonntag"}
|
||||
def dashboard_view(request):
|
||||
year = current_year()
|
||||
|
||||
CASH_PREFILL = Decimal("500.00")
|
||||
|
||||
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
|
||||
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"))
|
||||
drink_rows = [
|
||||
@@ -956,6 +964,8 @@ def dashboard_view(request):
|
||||
"total_entry": total_entry,
|
||||
"method_split": method_split,
|
||||
"cash_total": cash_total,
|
||||
"cash_prefill": CASH_PREFILL,
|
||||
"cash_net": cash_net,
|
||||
"top_free_recipient": top_free_recipient,
|
||||
"free_total_count": free_total_count,
|
||||
}
|
||||
|
||||
@@ -30,12 +30,16 @@
|
||||
<span class="hist-price">{{ total_donations|euro }}</span>
|
||||
</li>
|
||||
<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>
|
||||
</li>
|
||||
<li class="fin-row">
|
||||
<span class="hist-what">Kasse (bar)</span>
|
||||
<span class="hist-price">{{ cash_total|euro }}</span>
|
||||
<span class="hist-what">Kasse (bar, nach Vorschuss)</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 class="fin-row">
|
||||
<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 %}
|
||||
+1
-1
@@ -10,7 +10,7 @@ dependencies = [
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"ruff==0.15.17",
|
||||
"ruff==0.15.16",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
|
||||
Reference in New Issue
Block a user