This commit is contained in:
2025-10-02 00:54:34 +02:00
commit 84b34c9615
200 changed files with 42048 additions and 0 deletions

395
backend/booking/models.py Normal file
View File

@@ -0,0 +1,395 @@
from decimal import Decimal
from django.db import models
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.conf import settings
from django.db.models import Max
from django.utils import timezone
from trznice.models import SoftDeleteModel
from trznice.utils import truncate_to_minutes
#náměstí
class Square(SoftDeleteModel):
name = models.CharField(max_length=255, default="", null=False, blank=False)
description = models.TextField(null=True, blank=True)
street = models.CharField(max_length=255, default="Ulice není zadaná", null=False, blank=False)
city = models.CharField(max_length=255, default="Město není zadané", null=False, blank=False)
psc = models.PositiveIntegerField(
default=12345,
validators=[
MaxValueValidator(99999),
MinValueValidator(10000)
],
help_text="Zadejte platné PSČ (5 číslic)",
null=False, blank=False,
)
width = models.PositiveIntegerField(default=10)
height = models.PositiveIntegerField(default=10)
#Grid Parameters
grid_rows = models.PositiveSmallIntegerField(default=60)
grid_cols = models.PositiveSmallIntegerField(default=45)
cellsize = models.PositiveIntegerField(default=10)
image = models.ImageField(upload_to="squares-imgs/", blank=True, null=True)
def clean(self):
if self.width <= 0 :
raise ValidationError("Šířka náměstí nemůže být menší nebo rovna nule.")
if self.height <= 0:
raise ValidationError("Výška náměstí nemůže být menší nebo rovna nule.")
if self.grid_rows <= 0:
raise ValidationError("Počet řádků mapy nemůže být menší nebo rovna nule.")
if self.grid_cols <= 0:
raise ValidationError("Počet sloupců mapy nemůže být menší nebo rovna nule.")
if self.cellsize <= 0:
raise ValidationError("Velikost mapové buňky nemůže být menší nebo rovna nule.")
return super().clean()
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
def __str__(self):
return self.name
def delete(self, *args, **kwargs):
for event in self.square_events.all():
event.delete() # ✅ This triggers Event.delete()
super().delete(*args, **kwargs)
class Event(SoftDeleteModel):
"""Celé náměstí
Args:
models (args): w,h skutečné rozměry náměstí | x,y souřadnice levého horního rohu
"""
name = models.CharField(max_length=255, null=False, blank=False)
description = models.TextField(blank=True, null=True)
square = models.ForeignKey(Square, on_delete=models.CASCADE, related_name="square_events", null=False, blank=False)
start = models.DateField()
end = models.DateField()
price_per_m2 = models.DecimalField(max_digits=8, decimal_places=2, help_text="Cena za m² pro rezervaci", validators=[MinValueValidator(0)], null=False, blank=False)
image = models.ImageField(upload_to="squares-imgs/", blank=True, null=True)
def clean(self):
if not (self.start and self.end):
raise ValidationError("Datum začátku a konce musí být neprázdné.")
# Remove truncate_to_minutes and timezone logic
if self.start >= self.end:
raise ValidationError("Datum začátku musí být před datem konce.")
overlapping = Event.objects.exclude(id=self.id).filter(
square=self.square,
start__lt=self.end,
end__gt=self.start,
)
if overlapping.exists():
raise ValidationError("V tomto termínu už na daném náměstí probíhá jiná událost.")
return super().clean()
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
def __str__(self):
return self.name
def delete(self, *args, **kwargs):
for market_slot in self.event_marketSlots.all():
market_slot.delete()
# self.event_marketSlots.all().update(is_deleted=True, deleted_at=timezone.now())
# self.event_reservations.all().update(is_deleted=True, deleted_at=timezone.now())
self.event_products.all().update(is_deleted=True, deleted_at=timezone.now())
return super().delete(*args, **kwargs)
class MarketSlot(SoftDeleteModel):
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="event_marketSlots", null=False, blank=False)
STATUS_CHOICES = [
("allowed", "Povoleno"),
("blocked", "Zablokováno"),
]
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="allowed")
number = models.PositiveSmallIntegerField(default=1, help_text="Pořadové číslo prodejního místa na svém Eventu", editable=False)
base_size = models.FloatField(default=0, help_text="Základní velikost (m²)", validators=[MinValueValidator(0.0)], null=False, blank=False)
available_extension = models.FloatField(default=0, help_text="Možnost rozšíření (m²)", validators=[MinValueValidator(0.0)], null=False, blank=False)
x = models.SmallIntegerField(default=0, blank=False, validators=[MinValueValidator(0)])
y = models.SmallIntegerField(default=0, blank=False, validators=[MinValueValidator(0)])
width = models.PositiveIntegerField(default=0, blank=False, validators=[MinValueValidator(0)])
height = models.PositiveIntegerField(default=0, blank=False, validators=[MinValueValidator(0)])
price_per_m2 = models.DecimalField(
default=Decimal("0.00"),
max_digits=8,
decimal_places=2,
validators=[MinValueValidator(0)],
help_text="Cena za m² pro toto prodejní místo. Neuvádět, pokud chcete nechat výchozí cenu za m² na tomto Eventu."
)
def clean(self):
if self.base_size <= 0:
raise ValidationError("Základní velikost prodejního místa musí být větší než nula.")
return super().clean()
def save(self, *args, **kwargs):
self.full_clean()
# TODO: Fix this hovno logic, kdy uyivatel zada 0, se nastavi cena. Vymyslet neco noveho
# If price_per_m2 is 0, use the event default
# if self.event and hasattr(self.event, 'price_per_m2'):
if self.price_per_m2 == 0 and self.event and hasattr(self.event, 'price_per_m2'):
self.price_per_m2 = self.event.price_per_m2
# Automatically assign next available number within the same event
if self._state.adding:
max_number = MarketSlot.objects.filter(event=self.event).aggregate(Max('number'))['number__max'] or 0
self.number = max_number + 1
super().save(*args, **kwargs)
def __str__(self):
return f"Prodejní místo {self.number} na {self.event}"
def delete(self, *args, **kwargs):
for reservation in self.reservations.all():
reservation.delete()
# self.marketslot_reservations.all().update(is_deleted=True, deleted_at=timezone.now())
return super().delete(*args, **kwargs)
class Reservation(SoftDeleteModel):
STATUS_CHOICES = [
("reserved", "Zarezervováno"),
("cancelled", "Zrušeno"),
]
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="event_reservations", null=False, blank=False)
market_slot = models.ForeignKey(
'MarketSlot',
on_delete=models.CASCADE,
related_name='reservations',
null=True,
blank=True
)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="user_reservations", null=False, blank=False)
used_extension = models.FloatField(default=0 ,help_text="Použité rozšíření (m2)", validators=[MinValueValidator(0.0)])
reserved_from = models.DateField(null=False, blank=False)
reserved_to = models.DateField(null=False, blank=False)
created_at = models.DateTimeField(auto_now_add=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="reserved")
note = models.TextField(blank=True, null=True)
final_price = models.DecimalField(
default=0,
blank=False,
null=False,
max_digits=8,
decimal_places=2,
validators=[MinValueValidator(0)],
help_text="Cena vypočtena automaticky na zakladě ceny za m² prodejního místa a počtu dní rezervace."
)
price = models.DecimalField(
max_digits=10,
decimal_places=2,
default=0,
null=False,
blank=False
)
event_products = models.ManyToManyField("product.EventProduct", related_name="reservations", blank=True)
# Datails about checking
#TODO: Dodelat frontend
is_checked = models.BooleanField(default=False)
last_checked_at = models.DateTimeField(null=True, blank=True)
last_checked_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="reservations_checker"
)
def update_check_status(self):
last_check = self.checks.filter(is_deleted=False).order_by("-checked_at").first()
if last_check:
self.is_checked = True
self.last_checked_at = last_check.checked_at
self.last_checked_by = last_check.checker
else:
self.is_checked = False
self.last_checked_at = None
self.last_checked_by = None
def calculate_price(self):
# Use market_slot width and height for area
if not self.event or not self.event.square:
raise ValidationError("Rezervace musí mít přiřazenou akci s náměstím.")
if not self.market_slot:
raise ValidationError("Rezervace musí mít přiřazené prodejní místo.")
area = self.market_slot.width * self.market_slot.height
price_per_m2 = None
if self.market_slot.price_per_m2 and self.market_slot.price_per_m2 > 0:
price_per_m2 = self.market_slot.price_per_m2
else:
price_per_m2 = self.event.price_per_m2
if not price_per_m2 or price_per_m2 < 0:
raise ValidationError("Cena za m² není dostupná nebo je záporná.")
# Calculate number of days
days = (self.reserved_to - self.reserved_from).days + 1
# Calculate final price using slot area and reserved days
final_price = Decimal(area) * Decimal(price_per_m2) * Decimal(days)
final_price = final_price.quantize(Decimal("0.01"))
return final_price
def clean(self):
if not self.reserved_from or not self.reserved_to:
raise ValidationError("Datum rezervace nemůže být prázdný.")
# Remove truncate_to_minutes and timezone logic
if self.reserved_from > self.reserved_to:
raise ValidationError("Datum začátku rezervace musí být dříve než její konec.")
if self.reserved_from == self.reserved_to:
raise ValidationError("Začátek a konec rezervace nemohou být stejné.")
# Only check for overlapping reservations on the same market_slot
if self.market_slot:
overlapping = Reservation.objects.exclude(id=self.id).filter(
market_slot=self.market_slot,
status="reserved",
reserved_from__lt=self.reserved_to,
reserved_to__gt=self.reserved_from,
)
else:
raise ValidationError("Rezervace musí mít v sobě prodejní místo (MarketSlot).")
if overlapping.exists():
raise ValidationError("Rezervace se překrývá s jinou rezervací na stejném místě.")
# Check event bounds (date only)
if self.event:
event_start = self.event.start
event_end = self.event.end
if self.reserved_from < event_start or self.reserved_to > event_end:
raise ValidationError("Rezervace musí být v rámci trvání akce.")
if self.used_extension > self.market_slot.available_extension:
raise ValidationError("Požadované rozšíření je větší než možné rožšíření daného prodejního místa.")
if self.market_slot and self.event != self.market_slot.event:
raise ValidationError(f"Prodejní místo {self.market_slot} není část této akce, musí být ze stejné akce jako rezervace.")
if self.user:
if self.user.user_reservations.all().count() > 5:
raise ValidationError(f"{self.user} už má 5 rezervací, víc není možno rezervovat pro jednoho uživatele.")
else:
raise ValidationError("Rezervace musí mít v sobě uživatele.")
if self.final_price == 0 or self.final_price is None:
self.final_price = self.calculate_price()
elif self.final_price < 0:
raise ValidationError("Cena nemůže být záporná.")
return super().clean()
def save(self, *args, validate=True, **kwargs):
if validate:
self.full_clean()
super().save(*args, **kwargs)
def __str__(self):
return f"Rezervace {self.user} na event {self.event.name}"
def delete(self, *args, **kwargs):
order = getattr(self, "order", None)
if order is not None:
order.delete()
# Fix: Use a valid status value for MarketSlot
if self.market_slot:
event_end_date = self.market_slot.event.end
now_date = timezone.now().date()
if event_end_date > now_date:
self.market_slot.status = "allowed"
self.market_slot.save()
self.checks.all().update(is_deleted=True, deleted_at=timezone.now())
return super().delete(*args, **kwargs)
class ReservationCheck(SoftDeleteModel):
reservation = models.ForeignKey(
Reservation,
on_delete=models.CASCADE,
related_name="checks"
)
checker = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
related_name="performed_checks"
)
checked_at = models.DateTimeField(auto_now_add=True)
def clean(self):
# Check checker role
if not self.checker or not hasattr(self.checker, "role") or self.checker.role not in ["admin", "checker"]:
raise ValidationError("Uživatel není Kontrolor.")
# Validate reservation existence (safe check)
if not Reservation.objects.filter(pk=self.reservation_id).exists():
raise ValidationError("Neplatné ID Rezervace.")
super().clean()
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
self.is_deleted = True
self.deleted_at = timezone.now()
self.save()
from .signals import update_reservation_check_status
# Simulate post_delete behavior
update_reservation_check_status(sender=ReservationCheck, instance=self)