- 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>
87 lines
3.1 KiB
Python
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")
|