Files
gaehsnitz/gaehsnitz/management/commands/finance_summary_2026.py
T
floandClaude Sonnet 4.6 ae9d749356 feat(finance): add payment method field and post-festival accounting tools
- Add Payment.method field (cash/card/paypal/bank) with migration
- Show method in PaymentAdmin list view
- Add merge_users management command (atomic, reassigns all FKs)
- Add update_crates_2026 command (actual purchased/returned crates)
- Add finance_summary_2026 command (Ausgaben, Einnahmen, Kasse, Bilanz)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 16:19:05 +02:00

87 lines
3.1 KiB
Python

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")