Compare commits
1 Commits
main
..
06803389b8
| Author | SHA1 | Date | |
|---|---|---|---|
| 06803389b8 |
+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", "method")
|
list_display = ("date", "purpose", "amount")
|
||||||
list_filter = ("method", "date")
|
list_filter = ("date",)
|
||||||
ordering = ("-date",)
|
ordering = ("-date",)
|
||||||
search_fields = ("purpose",)
|
search_fields = ("purpose",)
|
||||||
|
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
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")
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
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."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
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."))
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# 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,16 +73,9 @@ 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"
|
||||||
|
|||||||
+5
-15
@@ -1,6 +1,5 @@
|
|||||||
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
|
||||||
@@ -25,7 +24,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, 17, 23, 59, 0, tzinfo=BERLIN)
|
BOOKING_END = datetime(2026, 6, 14, 22, 0, 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
|
||||||
@@ -178,11 +177,9 @@ def name_view(request):
|
|||||||
existing = None
|
existing = None
|
||||||
|
|
||||||
if existing is None:
|
if existing is None:
|
||||||
return render(
|
request.session["pending_username"] = username
|
||||||
request,
|
request.session["pending_mode"] = "create"
|
||||||
"suff/party_over.html",
|
return HttpResponseRedirect(reverse("suff:pin"))
|
||||||
{"phase": phase, "username": username},
|
|
||||||
)
|
|
||||||
|
|
||||||
if not existing.pin:
|
if not existing.pin:
|
||||||
if _user_has_activity(existing):
|
if _user_has_activity(existing):
|
||||||
@@ -748,14 +745,9 @@ 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
|
||||||
cash_raw = UserPayment.objects.filter(created_at__year=year, method="cash").aggregate(s=Sum("amount"))["s"] or Decimal("0")
|
user_payments_total = UserPayment.objects.filter(created_at__year=year).aggregate(s=Sum("amount"))["s"] or 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 = [
|
||||||
@@ -964,8 +956,6 @@ 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,16 +30,12 @@
|
|||||||
<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">Cashless (User-Tab)</span>
|
<span class="hist-what">Zahlungen (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, nach Vorschuss)</span>
|
<span class="hist-what">Kasse (bar)</span>
|
||||||
<span class="hist-price">{{ cash_net|euro }}</span>
|
<span class="hist-price">{{ cash_total|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>
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
{% 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]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"ruff==0.15.16",
|
"ruff==0.15.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
|
|||||||
Reference in New Issue
Block a user