Add year-scoped drink data, UserPayment model, German verbose names
Drink gets a year field (default 2024 for legacy rows), with name+year unique together so each festival can have its own price/crate config without overwriting the previous year. User balance properties (consumed_drinks_price, paid_amount, open_balance) and the new current_year() helper all filter by current year so year-over-year data stays separated. UserPayment model tracks per-user payments (cash/PayPal/bank/other) against their drink tab, separate from Donation (event-level income). Verbose names + Meta verbose_name(_plural) added across all models, and LANGUAGE_CODE set to "de" so the admin renders German throughout. Drink gains a few derived properties (crates_full_returned, crates_remaining, bottles_consumed, bottles_remaining, remaining_purchase_value) used by the upcoming admin and dashboard views. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 6.0.5 on 2026-05-14 18:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("gaehsnitz", "0003_consumption_created_at_user_pin"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="drink",
|
||||
name="year",
|
||||
field=models.PositiveSmallIntegerField(default=2024),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="drink",
|
||||
name="name",
|
||||
field=models.CharField(max_length=32),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="drink",
|
||||
unique_together={("name", "year")},
|
||||
),
|
||||
]
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
# Generated by Django 6.0.5 on 2026-05-14 19:15
|
||||
|
||||
import django.db.models.deletion
|
||||
import gaehsnitz.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("gaehsnitz", "0004_drink_year"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="consumption",
|
||||
options={"verbose_name": "Konsum", "verbose_name_plural": "Konsum"},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="donation",
|
||||
options={"verbose_name": "Spende", "verbose_name_plural": "Spenden"},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="drink",
|
||||
options={"verbose_name": "Getränk", "verbose_name_plural": "Getränke"},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="payment",
|
||||
options={"verbose_name": "Ausgabe", "verbose_name_plural": "Ausgaben"},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="user",
|
||||
options={"verbose_name": "Benutzer", "verbose_name_plural": "Benutzer"},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="consumption",
|
||||
name="amount",
|
||||
field=models.PositiveSmallIntegerField(verbose_name="Anzahl"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="consumption",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(auto_now_add=True, null=True, verbose_name="Gebucht am"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="consumption",
|
||||
name="day",
|
||||
field=models.PositiveSmallIntegerField(
|
||||
choices=[(1, "Do"), (2, "Fr"), (3, "Sa"), (4, "So")], verbose_name="Tag"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="consumption",
|
||||
name="drink",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="consumption_list",
|
||||
related_query_name="consumption",
|
||||
to="gaehsnitz.drink",
|
||||
verbose_name="Getränk",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="consumption",
|
||||
name="for_free",
|
||||
field=models.BooleanField(default=False, verbose_name="Gratis"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="consumption",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="consumption_list",
|
||||
related_query_name="consumption",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Benutzer",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="donation",
|
||||
name="amount",
|
||||
field=gaehsnitz.models.PriceField(decimal_places=2, max_digits=6, verbose_name="Betrag"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="donation",
|
||||
name="date",
|
||||
field=models.DateField(verbose_name="Datum"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="donation",
|
||||
name="note",
|
||||
field=models.CharField(blank=True, default="", max_length=64, verbose_name="Notiz"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="drink",
|
||||
name="bottle_size",
|
||||
field=models.FloatField(verbose_name="Flaschengröße (l)"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="drink",
|
||||
name="bottles_per_crate",
|
||||
field=models.PositiveSmallIntegerField(verbose_name="Flaschen pro Kasten"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="drink",
|
||||
name="crates_ordered",
|
||||
field=models.PositiveSmallIntegerField(
|
||||
help_text="nur zur Info, wie gut wir geplant haben — nicht die tatsächlich konsumierten/bezahlten Flaschen",
|
||||
verbose_name="Kästen bestellt",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="drink",
|
||||
name="crates_purchased",
|
||||
field=models.PositiveSmallIntegerField(verbose_name="Kästen gekauft"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="drink",
|
||||
name="crates_returned",
|
||||
field=models.PositiveSmallIntegerField(verbose_name="Kästen leer zurück"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="drink",
|
||||
name="deposit_per_crate",
|
||||
field=gaehsnitz.models.PriceField(decimal_places=2, max_digits=6, verbose_name="Pfand pro Kasten"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="drink",
|
||||
name="name",
|
||||
field=models.CharField(max_length=32, verbose_name="Name"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="drink",
|
||||
name="purchase_price_per_crate",
|
||||
field=gaehsnitz.models.PriceField(decimal_places=2, max_digits=6, verbose_name="Einkaufspreis pro Kasten"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="drink",
|
||||
name="sale_price_per_bottle",
|
||||
field=gaehsnitz.models.PriceField(decimal_places=2, max_digits=6, verbose_name="Verkaufspreis pro Flasche"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="drink",
|
||||
name="year",
|
||||
field=models.PositiveSmallIntegerField(default=2024, verbose_name="Jahr"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="payment",
|
||||
name="amount",
|
||||
field=gaehsnitz.models.PriceField(decimal_places=2, max_digits=6, verbose_name="Betrag"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="payment",
|
||||
name="date",
|
||||
field=models.DateField(verbose_name="Datum"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="payment",
|
||||
name="purpose",
|
||||
field=models.CharField(max_length=64, verbose_name="Zweck"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="pin",
|
||||
field=models.CharField(blank=True, default="", max_length=128, verbose_name="PIN"),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,51 @@
|
||||
# Generated by Django 6.0.5 on 2026-05-14 19:37
|
||||
|
||||
import django.db.models.deletion
|
||||
import gaehsnitz.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("gaehsnitz", "0005_alter_consumption_options_alter_donation_options_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="UserPayment",
|
||||
fields=[
|
||||
("id", models.SmallAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("amount", gaehsnitz.models.PriceField(decimal_places=2, max_digits=6, verbose_name="Betrag")),
|
||||
(
|
||||
"method",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("cash", "Bar"),
|
||||
("paypal", "PayPal"),
|
||||
("bank", "Überweisung"),
|
||||
("other", "Sonstiges"),
|
||||
],
|
||||
max_length=16,
|
||||
verbose_name="Methode",
|
||||
),
|
||||
),
|
||||
("note", models.CharField(blank=True, default="", max_length=64, verbose_name="Notiz")),
|
||||
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Bezahlt am")),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="user_payments",
|
||||
related_query_name="user_payment",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Benutzer",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Zahlung",
|
||||
"verbose_name_plural": "Zahlungen",
|
||||
},
|
||||
),
|
||||
]
|
||||
+116
-25
@@ -2,6 +2,11 @@ from django.contrib.auth.hashers import check_password, make_password
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from django.db.models import Sum, F
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
def current_year():
|
||||
return timezone.now().year
|
||||
|
||||
|
||||
class PriceField(models.DecimalField):
|
||||
@@ -11,7 +16,11 @@ class PriceField(models.DecimalField):
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
pin = models.CharField(max_length=128, blank=True, default="")
|
||||
pin = models.CharField("PIN", max_length=128, blank=True, default="")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Benutzer"
|
||||
verbose_name_plural = "Benutzer"
|
||||
|
||||
def set_pin(self, raw_pin):
|
||||
self.pin = make_password(raw_pin)
|
||||
@@ -31,16 +40,31 @@ class User(AbstractUser):
|
||||
|
||||
@property
|
||||
def consumed_drinks_price(self):
|
||||
query = self.paid_drinks.annotate(cost=F("amount") * F("drink__sale_price_per_bottle")).aggregate(
|
||||
sum=Sum("cost")
|
||||
query = (
|
||||
self.paid_drinks.filter(drink__year=current_year())
|
||||
.annotate(cost=F("amount") * F("drink__sale_price_per_bottle"))
|
||||
.aggregate(sum=Sum("cost"))
|
||||
)
|
||||
return query["sum"] or 0
|
||||
|
||||
@property
|
||||
def paid_amount(self):
|
||||
query = self.user_payments.filter(created_at__year=current_year()).aggregate(sum=Sum("amount"))
|
||||
return query["sum"] or 0
|
||||
|
||||
@property
|
||||
def open_balance(self):
|
||||
return self.consumed_drinks_price - self.paid_amount
|
||||
|
||||
|
||||
class Donation(models.Model):
|
||||
date = models.DateField()
|
||||
amount = PriceField()
|
||||
note = models.CharField(max_length=64, blank=True, default="")
|
||||
date = models.DateField("Datum")
|
||||
amount = PriceField("Betrag")
|
||||
note = models.CharField("Notiz", max_length=64, blank=True, default="")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Spende"
|
||||
verbose_name_plural = "Spenden"
|
||||
|
||||
|
||||
class Payment(models.Model):
|
||||
@@ -49,26 +73,37 @@ class Payment(models.Model):
|
||||
bands = 11, "Bands"
|
||||
supply_purchase = 12, "Getränke-/Essenseinkauf"
|
||||
|
||||
purpose = models.CharField(max_length=64)
|
||||
date = models.DateField()
|
||||
amount = PriceField()
|
||||
purpose = models.CharField("Zweck", max_length=64)
|
||||
date = models.DateField("Datum")
|
||||
amount = PriceField("Betrag")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Ausgabe"
|
||||
verbose_name_plural = "Ausgaben"
|
||||
|
||||
|
||||
class Drink(models.Model):
|
||||
name = models.CharField(max_length=32, unique=True)
|
||||
name = models.CharField("Name", max_length=32)
|
||||
year = models.PositiveSmallIntegerField("Jahr", default=2024)
|
||||
crates_ordered = models.PositiveSmallIntegerField(
|
||||
help_text="just informational to see how good we planned, not the actual consumed/paid drinks"
|
||||
"Kästen bestellt",
|
||||
help_text="nur zur Info, wie gut wir geplant haben — nicht die tatsächlich konsumierten/bezahlten Flaschen",
|
||||
)
|
||||
crates_purchased = models.PositiveSmallIntegerField()
|
||||
crates_returned = models.PositiveSmallIntegerField()
|
||||
purchase_price_per_crate = PriceField()
|
||||
deposit_per_crate = PriceField()
|
||||
bottles_per_crate = models.PositiveSmallIntegerField()
|
||||
bottle_size = models.FloatField()
|
||||
sale_price_per_bottle = PriceField()
|
||||
crates_purchased = models.PositiveSmallIntegerField("Kästen gekauft")
|
||||
crates_returned = models.PositiveSmallIntegerField("Kästen leer zurück")
|
||||
purchase_price_per_crate = PriceField("Einkaufspreis pro Kasten")
|
||||
deposit_per_crate = PriceField("Pfand pro Kasten")
|
||||
bottles_per_crate = models.PositiveSmallIntegerField("Flaschen pro Kasten")
|
||||
bottle_size = models.FloatField("Flaschengröße (l)")
|
||||
sale_price_per_bottle = PriceField("Verkaufspreis pro Flasche")
|
||||
|
||||
class Meta:
|
||||
unique_together = (("name", "year"),)
|
||||
verbose_name = "Getränk"
|
||||
verbose_name_plural = "Getränke"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
return f"{self.name} {self.year}"
|
||||
|
||||
@property
|
||||
def bottles_total(self):
|
||||
@@ -132,15 +167,71 @@ class Drink(models.Model):
|
||||
def balance(self):
|
||||
return self.sale_price_total - self.sales_purchase_value - self.giveaway_purchase_value
|
||||
|
||||
@property
|
||||
def crates_full_returned(self):
|
||||
return self.crates_ordered - self.crates_purchased
|
||||
|
||||
@property
|
||||
def crates_remaining(self):
|
||||
return self.crates_purchased - self.crates_returned
|
||||
|
||||
@property
|
||||
def bottles_consumed(self):
|
||||
return self.bottles_sold + self.bottles_given_away
|
||||
|
||||
@property
|
||||
def bottles_remaining(self):
|
||||
return self.bottles_total - self.bottles_consumed
|
||||
|
||||
@property
|
||||
def remaining_purchase_value(self):
|
||||
return self.bottles_remaining * self.purchase_price_per_bottle
|
||||
|
||||
|
||||
class UserPayment(models.Model):
|
||||
class Method(models.TextChoices):
|
||||
cash = "cash", "Bar"
|
||||
paypal = "paypal", "PayPal"
|
||||
bank = "bank", "Überweisung"
|
||||
other = "other", "Sonstiges"
|
||||
|
||||
user = models.ForeignKey(
|
||||
verbose_name="Benutzer",
|
||||
to=User,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="user_payments",
|
||||
related_query_name="user_payment",
|
||||
)
|
||||
amount = PriceField("Betrag")
|
||||
method = models.CharField("Methode", max_length=16, choices=Method.choices)
|
||||
note = models.CharField("Notiz", max_length=64, blank=True, default="")
|
||||
created_at = models.DateTimeField("Bezahlt am", auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Zahlung"
|
||||
verbose_name_plural = "Zahlungen"
|
||||
|
||||
|
||||
class Consumption(models.Model):
|
||||
user = models.ForeignKey(
|
||||
to=User, on_delete=models.CASCADE, related_name="consumption_list", related_query_name="consumption"
|
||||
verbose_name="Benutzer",
|
||||
to=User,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="consumption_list",
|
||||
related_query_name="consumption",
|
||||
)
|
||||
drink = models.ForeignKey(
|
||||
to=Drink, on_delete=models.CASCADE, related_name="consumption_list", related_query_name="consumption"
|
||||
verbose_name="Getränk",
|
||||
to=Drink,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="consumption_list",
|
||||
related_query_name="consumption",
|
||||
)
|
||||
amount = models.PositiveSmallIntegerField()
|
||||
day = models.PositiveSmallIntegerField(choices=[(1, "Do"), (2, "Fr"), (3, "Sa"), (4, "So")])
|
||||
for_free = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True, null=True)
|
||||
amount = models.PositiveSmallIntegerField("Anzahl")
|
||||
day = models.PositiveSmallIntegerField("Tag", choices=[(1, "Do"), (2, "Fr"), (3, "Sa"), (4, "So")])
|
||||
for_free = models.BooleanField("Gratis", default=False)
|
||||
created_at = models.DateTimeField("Gebucht am", auto_now_add=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Konsum"
|
||||
verbose_name_plural = "Konsum"
|
||||
|
||||
@@ -94,7 +94,8 @@ TEMPLATES = [
|
||||
WSGI_APPLICATION = "gaehsnitzproject.wsgi.application"
|
||||
|
||||
TIME_ZONE = "Europe/Berlin"
|
||||
USE_I18N = False
|
||||
LANGUAGE_CODE = "de"
|
||||
USE_I18N = True
|
||||
USE_L10N = False
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
Reference in New Issue
Block a user