2d611dcac5
- Add Drink.Category.sekt for consignment alcoholic drinks - Order sekt under "mit Alkohol" section, after Radler - Rainbow gradient CSS for .drink-btn-sekt - Rebalance all drink button brightness to match rainbow midpoint - Seed Sekty Drink (3.50€, no deposit, no purchase price) - Update Sternburg Export 10.99→9.49, Altenburger Helles 15.99→13.99 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
253 lines
7.7 KiB
Python
253 lines
7.7 KiB
Python
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):
|
|
def __init__(self, verbose_name=None, name=None, **kwargs):
|
|
kwargs.update({"max_digits": 6, "decimal_places": 2})
|
|
super().__init__(verbose_name, name, **kwargs)
|
|
|
|
|
|
class User(AbstractUser):
|
|
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)
|
|
|
|
def check_pin(self, raw_pin):
|
|
if not self.pin:
|
|
return False
|
|
return check_password(raw_pin, self.pin)
|
|
|
|
@property
|
|
def paid_drinks(self):
|
|
return self.consumption_list.filter(for_free=False)
|
|
|
|
@property
|
|
def free_drinks(self):
|
|
return self.consumption_list.filter(for_free=True)
|
|
|
|
@property
|
|
def consumed_drinks_price(self):
|
|
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("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):
|
|
class Topic(models.IntegerChoices):
|
|
site = 10, "Gelände"
|
|
bands = 11, "Bands"
|
|
supply_purchase = 12, "Getränke-/Essenseinkauf"
|
|
|
|
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):
|
|
class Category(models.TextChoices):
|
|
beer = "beer", "Bier"
|
|
radler = "radler", "Radler"
|
|
sekt = "sekt", "Sekt"
|
|
alc_free_beer = "alc_free_beer", "Bier alkoholfrei"
|
|
alc_free_radler = "alc_free_radler", "Radler alkoholfrei"
|
|
soft = "soft", "Softdrink"
|
|
water = "water", "Wasser"
|
|
|
|
name = models.CharField("Name", max_length=32)
|
|
year = models.PositiveSmallIntegerField("Jahr", default=2024)
|
|
category = models.CharField(
|
|
"Kategorie",
|
|
max_length=16,
|
|
choices=Category.choices,
|
|
default=Category.beer,
|
|
)
|
|
crates_ordered = models.PositiveSmallIntegerField(
|
|
"Kästen bestellt",
|
|
help_text="nur zur Info, wie gut wir geplant haben — nicht die tatsächlich konsumierten/bezahlten Flaschen",
|
|
)
|
|
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 f"{self.name} {self.year}"
|
|
|
|
@property
|
|
def bottles_total(self):
|
|
return self.bottles_per_crate * self.crates_purchased
|
|
|
|
@property
|
|
def bottles_returned(self):
|
|
return self.bottles_per_crate * self.crates_returned
|
|
|
|
@property
|
|
def amount_per_crate(self):
|
|
return self.bottle_size * self.bottles_per_crate
|
|
|
|
@property
|
|
def amount_total(self):
|
|
return self.amount_per_crate * self.crates_purchased
|
|
|
|
@property
|
|
def purchase_price_per_bottle(self):
|
|
return self.purchase_price_per_crate / self.bottles_per_crate
|
|
|
|
@property
|
|
def purchase_price_total(self):
|
|
return self.purchase_price_per_crate * self.crates_purchased
|
|
|
|
@property
|
|
def deposit_total(self):
|
|
return self.deposit_per_crate * self.crates_purchased
|
|
|
|
@property
|
|
def deposit_refund(self):
|
|
return self.deposit_per_crate * self.crates_returned
|
|
|
|
@property
|
|
def deposit_kept(self):
|
|
return self.deposit_total - self.deposit_refund
|
|
|
|
@property
|
|
def bottles_sold(self):
|
|
query = self.consumption_list.filter(for_free=False).aggregate(sum=Sum("amount"))
|
|
return query["sum"] or 0
|
|
|
|
@property
|
|
def sales_purchase_value(self):
|
|
return self.bottles_sold * self.purchase_price_per_bottle
|
|
|
|
@property
|
|
def sale_price_total(self):
|
|
return self.bottles_sold * self.sale_price_per_bottle
|
|
|
|
@property
|
|
def bottles_given_away(self):
|
|
query = self.consumption_list.filter(for_free=True).aggregate(sum=Sum("amount"))
|
|
return query["sum"] or 0
|
|
|
|
@property
|
|
def giveaway_purchase_value(self):
|
|
return self.bottles_given_away * self.purchase_price_per_bottle
|
|
|
|
@property
|
|
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(
|
|
verbose_name="Benutzer",
|
|
to=User,
|
|
on_delete=models.CASCADE,
|
|
related_name="consumption_list",
|
|
related_query_name="consumption",
|
|
)
|
|
drink = models.ForeignKey(
|
|
verbose_name="Getränk",
|
|
to=Drink,
|
|
on_delete=models.CASCADE,
|
|
related_name="consumption_list",
|
|
related_query_name="consumption",
|
|
)
|
|
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"
|