diff --git a/gaehsnitz/migrations/0004_drink_year.py b/gaehsnitz/migrations/0004_drink_year.py new file mode 100644 index 0000000..6c5e45a --- /dev/null +++ b/gaehsnitz/migrations/0004_drink_year.py @@ -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")}, + ), + ] diff --git a/gaehsnitz/migrations/0005_alter_consumption_options_alter_donation_options_and_more.py b/gaehsnitz/migrations/0005_alter_consumption_options_alter_donation_options_and_more.py new file mode 100644 index 0000000..9143c04 --- /dev/null +++ b/gaehsnitz/migrations/0005_alter_consumption_options_alter_donation_options_and_more.py @@ -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"), + ), + ] diff --git a/gaehsnitz/migrations/0006_user_payment.py b/gaehsnitz/migrations/0006_user_payment.py new file mode 100644 index 0000000..a2e6c8e --- /dev/null +++ b/gaehsnitz/migrations/0006_user_payment.py @@ -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", + }, + ), + ] diff --git a/gaehsnitz/models.py b/gaehsnitz/models.py index 9bc01f5..7429e20 100644 --- a/gaehsnitz/models.py +++ b/gaehsnitz/models.py @@ -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" diff --git a/gaehsnitzproject/settings.py b/gaehsnitzproject/settings.py index 073f30d..cdb1cfc 100644 --- a/gaehsnitzproject/settings.py +++ b/gaehsnitzproject/settings.py @@ -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