Files
gaehsnitz/gaehsnitz/models.py
T
flo 47d46e8e6f Add suff drink booking tool with PIN auth
Self-service drink tab at /suff/ for festival attendees. Users log in
with username + 3-digit PIN stored in a separate User.pin field, so
staff/admin accounts can keep their strong password for /admin/ and
also use the drink tool with the same username. PINs for staff users
must be set from the admin panel via a dedicated "PIN setzen" view to
prevent account takeover by name collision.

Time-gated to the festival window (Thu–Sun in Berlin tz) with phases
before/booking/readonly/closed; in non-production mode the tool is
always in booking phase for local testing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 12:05:25 +02:00

147 lines
4.5 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
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(max_length=128, blank=True, default="")
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.annotate(cost=F("amount") * F("drink__sale_price_per_bottle")).aggregate(
sum=Sum("cost")
)
return query["sum"] or 0
class Donation(models.Model):
date = models.DateField()
amount = PriceField()
note = models.CharField(max_length=64, blank=True, default="")
class Payment(models.Model):
class Topic(models.IntegerChoices):
site = 10, "Gelände"
bands = 11, "Bands"
supply_purchase = 12, "Getränke-/Essenseinkauf"
purpose = models.CharField(max_length=64)
date = models.DateField()
amount = PriceField()
class Drink(models.Model):
name = models.CharField(max_length=32, unique=True)
crates_ordered = models.PositiveSmallIntegerField(
help_text="just informational to see how good we planned, not the actual consumed/paid drinks"
)
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()
def __str__(self):
return self.name
@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
class Consumption(models.Model):
user = models.ForeignKey(
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"
)
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)