396 lines
15 KiB
Python
396 lines
15 KiB
Python
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)
|