from rest_framework import serializers from datetime import timedelta from booking.models import Event, MarketSlot import logging from decimal import Decimal, ROUND_HALF_UP, InvalidOperation try: from commerce.serializers import PriceCalculationSerializer except ImportError: PriceCalculationSerializer = None from trznice.utils import RoundedDateTimeField from .models import Event, MarketSlot, Reservation, Square, ReservationCheck from account.models import CustomUser from product.serializers import EventProductSerializer logger = logging.getLogger(__name__) #----------------------SHORT SERIALIZERS--------------------------------- class EventShortSerializer(serializers.ModelSerializer): class Meta: model = Square fields = ["id", "name"] extra_kwargs = { "id": {"read_only": True}, "name": {"read_only": True, "help_text": "Název náměstí"} } class UserShortSerializer(serializers.ModelSerializer): class Meta: model = CustomUser fields = ["id", "username"] extra_kwargs = { "id": {"read_only": True}, "username": {"read_only": True, "help_text": "username uživatele"} } class SquareShortSerializer(serializers.ModelSerializer): class Meta: model = Square fields = ["id", "name"] extra_kwargs = { "id": {"read_only": True}, "name": {"read_only": True, "help_text": "Název náměstí"} } class ReservationShortSerializer(serializers.ModelSerializer): user = UserShortSerializer(read_only=True) event = EventShortSerializer(read_only=True) class Meta: model = Reservation fields = ["id", "user", "event"] extra_kwargs = { "id": {"read_only": True}, "user": {"read_only": True, "help_text": "Majitel rezervace"}, "event": {"read_only": True, "help_text": "Akce na které je vytvořena rezervace"} } #------------------------------------------------------------------------ #------------------------NORMAL SERIALIZERS------------------------------ class ReservationCheckSerializer(serializers.ModelSerializer): reservation = serializers.PrimaryKeyRelatedField( queryset=Reservation.objects.all(), write_only=True, help_text="ID rezervace, která se kontroluje." ) reservation_info = ReservationShortSerializer(source="reservation", read_only=True) checker = serializers.HiddenField(default=serializers.CurrentUserDefault()) checker_info = UserShortSerializer(source="checker", read_only=True) class Meta: model = ReservationCheck fields = [ "id", "reservation", "reservation_info", "checker", "checker_info", "checked_at" ] read_only_fields = ["id", "checked_at"] def validate_reservation(self, value): if value.status != "reserved": raise serializers.ValidationError("Rezervaci lze kontrolovat pouze pokud je ve stavu 'reserved'.") return value def validate_checker(self, value): user = self.context["request"].user if not user.is_staff and value != user: raise serializers.ValidationError("Pouze administrátor může nastavit jiného uživatele jako kontrolora.") return value class ReservationSerializer(serializers.ModelSerializer): reserved_from = serializers.DateField() reserved_to = serializers.DateField() event = EventShortSerializer(read_only=True) user = UserShortSerializer(read_only=True) market_slot = serializers.PrimaryKeyRelatedField( queryset=MarketSlot.objects.filter(is_deleted=False), required=True ) last_checked_by = UserShortSerializer(read_only=True) class Meta: model = Reservation fields = [ "id", "market_slot", "used_extension", "reserved_from", "reserved_to", "created_at", "status", "note", "final_price", "event", "user", "is_checked", "last_checked_by", "last_checked_at" ] read_only_fields = ["id", "created_at", "is_checked", "last_checked_by", "last_checked_at"] extra_kwargs = { "event": {"help_text": "ID (Event), ke které rezervace patří", "required": True}, "market_slot": {"help_text": "ID konkrétního prodejního místa (MarketSlot)", "required": True}, "user": {"help_text": "ID a název uživatele, který rezervaci vytváří", "required": True}, "used_extension": {"help_text": "Velikost rozšíření v m², které chce uživatel využít", "required": True}, "reserved_from": {"help_text": "Datum a čas začátku rezervace", "required": True}, "reserved_to": {"help_text": "Datum a čas konce rezervace", "required": True}, "status": {"help_text": "Stav rezervace (reserved / cancelled)", "required": False, "default": "reserved"}, "note": {"help_text": "Poznámka k rezervaci (volitelné)", "required": False}, "final_price": {"help_text": "Cena za Rezervaci, počítá se podle plochy prodejního místa a počtů dní.", "required": False, "default": 0}, "is_checked": {"help_text": "Stav je True, pokud již byla provedena aspoň jedna kontrola.", "required": False, "read_only": True}, "last_checked_by": {"help_text": "Kontrolor, který provedl poslední kontrolu.", "required": False, "read_only": True}, "last_checked_at": {"help_text": "Čas kdy byla provedena poslední kontrola.", "required": False, "read_only": True} } def to_internal_value(self, data): # Accept both "market_slot" and legacy "marketSlot" keys for compatibility if "marketSlot" in data and "market_slot" not in data: data["market_slot"] = data["marketSlot"] # Debug: log incoming data for troubleshooting logger.debug(f"ReservationSerializer.to_internal_value input data: {data}") return super().to_internal_value(data) def to_internal_value(self, data): # Accept both "market_slot" and legacy "marketSlot" keys for compatibility if "marketSlot" in data and "market_slot" not in data: data["market_slot"] = data["marketSlot"] # Debug: log incoming data for troubleshooting logger.debug(f"ReservationSerializer.to_internal_value input data: {data}") return super().to_internal_value(data) def validate(self, data): logger.debug(f"ReservationSerializer.validate market_slot: {data.get('market_slot')}, event: {data.get('event')}") # Get the event object from the provided event id (if present) event_id = self.initial_data.get("event") if event_id: try: event = Event.objects.get(pk=event_id) data["event"] = event except Event.DoesNotExist: raise serializers.ValidationError({"event": "Zadaná akce (event) neexistuje."}) else: event = data.get("event") market_slot = data.get("market_slot") # --- FIX: Ensure event is set before permission check in views --- if event is None and market_slot is not None: event = market_slot.event data["event"] = event logger.debug(f"ReservationSerializer.validate auto-filled event from market_slot: {event}") user = data.get("user") request_user = self.context["request"].user if "request" in self.context else None # If user is not specified, use the logged-in user if user is None and request_user is not None: user = request_user data["user"] = user # If user is specified and differs from logged-in user, check permissions if user is not None and request_user is not None and user != request_user: if request_user.role not in ["admin", "cityClerk", "squareManager"]: raise serializers.ValidationError("Pouze administrátor, úředník nebo správce tržiště může vytvářet rezervace pro jiné uživatele.") if user is None: raise serializers.ValidationError("Rezervace musí mít přiřazeného uživatele.") if user.user_reservations.filter(status="reserved").count() >= 5: raise serializers.ValidationError("Uživatel už má 5 aktivních rezervací.") reserved_from = data.get("reserved_from") reserved_to = data.get("reserved_to") used_extension = data.get("used_extension", 0) final_price = data.get("final_price", 0) if "status" in data: if self.instance: # update if data["status"] != self.instance.status and user.role not in ["admin", "cityClerk"]: raise serializers.ValidationError({ "status": "Pouze administrátor nebo úředník může upravit status rezervace." }) else: data["status"] = "reserved" privileged_roles = ["admin", "cityClerk"] # Define max allowed price based on model's decimal constraints (8 digits, 2 decimal places) MAX_FINAL_PRICE = Decimal("999999.99") if user and getattr(user, "role", None) in privileged_roles: # 🧠 Automatický výpočet ceny rezervace pokud není zadána if not final_price or final_price == 0: market_slot = data.get("market_slot") event = data.get("event") reserved_from = data.get("reserved_from") reserved_to = data.get("reserved_to") used_extension = data.get("used_extension", 0) # --- Prefer PriceCalculationSerializer if available --- if PriceCalculationSerializer: try: price_serializer = PriceCalculationSerializer(data={ "market_slot": market_slot.id if market_slot else None, "used_extension": used_extension, "reserved_from": reserved_from, "reserved_to": reserved_to, "event": event.id if event else None, "user": user.id if user else None, }) price_serializer.is_valid(raise_exception=True) calculated_price = price_serializer.validated_data.get("final_price") if calculated_price is not None: try: # Always quantize to two decimals decimal_price = Decimal(str(calculated_price)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) # Clamp value to max allowed and raise error if exceeded if decimal_price > MAX_FINAL_PRICE: logger.error(f"ReservationSerializer: final_price ({decimal_price}) exceeds max allowed ({MAX_FINAL_PRICE})") data["final_price"] = MAX_FINAL_PRICE raise serializers.ValidationError({"final_price": f"Cena je příliš vysoká, maximálně {MAX_FINAL_PRICE} Kč."}) else: data["final_price"] = decimal_price except (InvalidOperation, TypeError, ValueError): raise serializers.ValidationError("Výsledná cena není platné číslo.") else: raise serializers.ValidationError("Výpočet ceny selhal.") except Exception as e: logger.error(f"PriceCalculationSerializer failed: {e}", exc_info=True) market_slot = data.get("market_slot") event = data.get("event") reserved_from = data.get("reserved_from") reserved_to = data.get("reserved_to") used_extension = data.get("used_extension", 0) price_per_m2 = data.get("price_per_m2") if price_per_m2 is None: if market_slot and hasattr(market_slot, "price_per_m2"): price_per_m2 = market_slot.price_per_m2 elif event and hasattr(event, "price_per_m2"): price_per_m2 = event.price_per_m2 else: raise serializers.ValidationError("Cena za m² není dostupná.") base_size = getattr(market_slot, "base_size", None) if base_size is None: raise serializers.ValidationError("Základní velikost (base_size) není dostupná.") duration_days = (reserved_to - reserved_from).days base_size_decimal = Decimal(str(base_size)) used_extension_decimal = Decimal(str(used_extension)) duration_days_decimal = Decimal(str(duration_days)) price_per_m2_decimal = Decimal(str(price_per_m2)) calculated_price = duration_days_decimal * (price_per_m2_decimal * (base_size_decimal + used_extension_decimal)) try: decimal_price = Decimal(str(calculated_price)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) # Clamp value to max allowed and raise error if exceeded if decimal_price > MAX_FINAL_PRICE: logger.error(f"ReservationSerializer: final_price ({decimal_price}) exceeds max allowed ({MAX_FINAL_PRICE})") data["final_price"] = MAX_FINAL_PRICE raise serializers.ValidationError({"final_price": f"Cena je příliš vysoká, maximálně {MAX_FINAL_PRICE} Kč."}) else: data["final_price"] = decimal_price except (InvalidOperation, TypeError, ValueError): raise serializers.ValidationError("Výsledná cena není platné číslo.") else: price_per_m2 = data.get("price_per_m2") if price_per_m2 is None: if market_slot and hasattr(market_slot, "price_per_m2"): price_per_m2 = market_slot.price_per_m2 elif event and hasattr(event, "price_per_m2"): price_per_m2 = event.price_per_m2 else: raise serializers.ValidationError("Cena za m² není dostupná.") resolution = event.square.cellsize if event and hasattr(event, "square") else 1 width = getattr(market_slot, "width", 1) height = getattr(market_slot, "height", 1) # If you want to include used_extension, add it to area area_m2 = Decimal(width) * Decimal(height) * Decimal(resolution) * Decimal(resolution) duration_days = (reserved_to - reserved_from).days price_per_m2_decimal = Decimal(str(price_per_m2)) calculated_price = Decimal(duration_days) * area_m2 * price_per_m2_decimal try: decimal_price = Decimal(str(calculated_price)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) # Clamp value to max allowed and raise error if exceeded if decimal_price > MAX_FINAL_PRICE: logger.error(f"ReservationSerializer: final_price ({decimal_price}) exceeds max allowed ({MAX_FINAL_PRICE})") data["final_price"] = MAX_FINAL_PRICE raise serializers.ValidationError({"final_price": f"Cena je příliš vysoká, maximálně {MAX_FINAL_PRICE} Kč."}) else: data["final_price"] = decimal_price except (InvalidOperation, TypeError, ValueError): raise serializers.ValidationError("Výsledná cena není platné číslo.") else: if self.instance: # update if final_price != self.instance.final_price and (not user or user.role not in privileged_roles): raise serializers.ValidationError({ "final_price": "Pouze administrátor nebo úředník může upravit finální cenu." }) else: # create if not user or user.role not in privileged_roles: raise serializers.ValidationError({ "final_price": "Pouze administrátor nebo úředník může nastavit finální cenu." }) if data.get("final_price") is not None: try: decimal_price = Decimal(str(data["final_price"])).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) # Clamp value to max allowed and raise error if exceeded if decimal_price > MAX_FINAL_PRICE: logger.error(f"ReservationSerializer: final_price ({decimal_price}) exceeds max allowed ({MAX_FINAL_PRICE})") data["final_price"] = MAX_FINAL_PRICE raise serializers.ValidationError({"final_price": f"Cena je příliš vysoká, maximálně {MAX_FINAL_PRICE} Kč."}) data["final_price"] = decimal_price except (InvalidOperation, TypeError, ValueError): raise serializers.ValidationError("Výsledná cena není platné číslo.") if data.get("final_price") < 0: raise serializers.ValidationError("Cena za m² nemůže být záporná.") else: # Remove final_price if not privileged data.pop("final_price", None) if reserved_from >= reserved_to: raise serializers.ValidationError("Datum začátku rezervace musí být dříve než její konec.") if reserved_from < event.start or reserved_to > event.end: raise serializers.ValidationError("Rezervace musí být v rámci trvání akce.") overlapping = None if market_slot: if market_slot.event != event: raise serializers.ValidationError("Prodejní místo nepatří do dané akce.") if used_extension > market_slot.available_extension: raise serializers.ValidationError("Požadované rozšíření překračuje dostupné rozšíření.") overlapping = Reservation.objects.exclude(id=self.instance.id if self.instance else None).filter( event=event, market_slot=market_slot, reserved_from__lt=reserved_to, reserved_to__gt=reserved_from, status="reserved" ) if overlapping is not None and overlapping.exists(): logger.debug(f"ReservationSerializer.validate: Found overlapping reservations for market_slot {market_slot.id} in event {event.id}") raise serializers.ValidationError("Rezervace se překrývá s jinou rezervací na stejném místě.") return data class ReservationAvailabilitySerializer(serializers.Serializer): event_id = serializers.IntegerField() market_slot_id = serializers.IntegerField() reserved_from = serializers.DateField() reserved_to = serializers.DateField() class Meta: model = Reservation fields = ["event", "market_slot", "reserved_from", "reserved_to"] extra_kwargs = { "event": {"help_text": "ID of the event"}, "market_slot": {"help_text": "ID of the market slot"}, "reserved_from": {"help_text": "Start date of the reservation"}, "reserved_to": {"help_text": "End date of the reservation"}, } def validate(self, data): event_id = data.get("event_id") market_slot_id = data.get("market_slot_id") reserved_from = data.get("reserved_from") reserved_to = data.get("reserved_to") if reserved_from >= reserved_to: raise serializers.ValidationError("Konec rezervace musí být po začátku.") # Zkontroluj existenci Eventu a Slotu try: event = Event.objects.get(id=event_id) except Event.DoesNotExist: raise serializers.ValidationError("Událost neexistuje.") try: market_slot = MarketSlot.objects.get(id=market_slot_id) except MarketSlot.DoesNotExist: raise serializers.ValidationError("Slot neexistuje.") # Zkontroluj status slotu if market_slot.status == "blocked": raise serializers.ValidationError("Tento slot je zablokovaný správcem.") # Zkontroluj, že datumy spadají do rozsahu události if reserved_from < event.date_from or reserved_to > event.date_to: raise serializers.ValidationError("Vybrané datumy nespadají do trvání akce.") # Zkontroluj, jestli už neexistuje kolizní rezervace conflict = Reservation.objects.filter( event=event, market_slot=market_slot, reserved_from__lt=reserved_to, reserved_to__gt=reserved_from, status="reserved" ).exists() if conflict: raise serializers.ValidationError("Tento slot je v daném termínu již rezervován.") return data #--- Reservation end ---- class MarketSlotSerializer(serializers.ModelSerializer): class Meta: model = MarketSlot fields = [ "id", "event", "number", "status", "base_size", "available_extension", "x", "y", "width", "height", "price_per_m2" ] read_only_fields = ["id", "number"] extra_kwargs = { "event": {"help_text": "ID akce (Event), ke které toto místo patří", "required": True}, "number": {"help_text": "Pořadové číslo prodejního místa u Akce, ke které toto místo patří", "required": False}, "status": {"help_text": "Stav prodejního místa", "required": False}, "base_size": {"help_text": "Základní velikost (m²)", "required": True}, "available_extension": {"help_text": "Možnost rozšíření (m²)", "required": False, "default": 0}, "x": {"help_text": "X souřadnice levého horního rohu", "required": True}, "y": {"help_text": "Y souřadnice levého horního rohu", "required": True}, "width": {"help_text": "Šířka Slotu", "required": True}, "height": {"help_text": "Výška Slotu", "required": True}, "price_per_m2": {"help_text": "Cena za m² tohoto místa", "required": False, "default": 0}, } def validate_base_size(self, value): if value <= 0: raise serializers.ValidationError("Základní velikost musí být větší než nula.") return value def validate(self, data): price_per_m2 = data.setdefault("price_per_m2", 0) if price_per_m2 < 0: raise serializers.ValidationError("Cena za m² nemůže být záporná.") if data.setdefault("available_extension", 0) < 0: raise serializers.ValidationError("Velikost možného rozšíření musí být větší než nula.") if data.get("width", 0) <= 0 or data.get("height", 0) <= 0: raise serializers.ValidationError("Šířka a výška místa musí být větší než nula.") return data class EventSerializer(serializers.ModelSerializer): square = SquareShortSerializer(read_only=True) square_id = serializers.PrimaryKeyRelatedField( queryset=Square.objects.all(), source="square", write_only=True ) market_slots = MarketSlotSerializer(many=True, read_only=True, source="event_marketSlots") event_products = EventProductSerializer(many=True, read_only=True) start = serializers.DateField() end = serializers.DateField() class Meta: model = Event fields = [ "id", "name", "description", "start", "end", "price_per_m2", "image", "market_slots", "event_products", "square", # nested read-only "square_id" # required in POST/PUT ] read_only_fields = ["id"] extra_kwargs = { "name": {"help_text": "Název události", "required": True}, "description": {"help_text": "Popis události", "required": False}, "start": {"help_text": "Datum a čas začátku události", "required": True}, "end": {"help_text": "Datum a čas konce události", "required": True}, "price_per_m2": {"help_text": "Cena za m² pro rezervaci", "required": True}, "image": {"help_text": "Obrázek nebo plán náměstí", "required": False, "allow_null": True}, "market_slots": {"help_text": "Seznam prodejních míst vytvořených v rámci této události", "required": False}, "event_products": {"help_text": "Seznam povolených zboží k prodeji v rámci této události", "required": False}, "square": {"help_text": "Náměstí, na kterém se akce koná (jen ke čtení)", "required": False}, "square_id": {"help_text": "ID Náměstí, na kterém se akce koná (jen ke zápis)", "required": True}, } def validate(self, data): start = data.get("start") end = data.get("end") square = data.get("square") if not start or not end or not square: raise serializers.ValidationError("Pole start, end a square musí být vyplněné.") if start >= end: raise serializers.ValidationError("Datum začátku musí být před datem konce.") if data.get("price_per_m2", 0) <= 0: raise serializers.ValidationError("Cena za m² plochy pro rezervaci musí být větší než 0.") overlapping = Event.objects.exclude(id=self.instance.id if self.instance else None).filter( square=square, start__lt=end, end__gt=start, ) if overlapping.exists(): raise serializers.ValidationError("V tomto termínu už na daném náměstí probíhá jiná událost.") return data class SquareSerializer(serializers.ModelSerializer): image = serializers.ImageField(required=False, allow_null=True) # Ensure DRF handles image upload class Meta: model = Square fields = [ "id", "name", "description", "street", "city", "psc", "width", "height", "grid_rows", "grid_cols", "cellsize", "image" ] read_only_fields = ["id"] extra_kwargs = { "name": {"help_text": "Název náměstí", "required": True}, "description": {"help_text": "Popis náměstí", "required": False}, "street": {"help_text": "Ulice, kde se náměstí nachází", "required": False}, "city": {"help_text": "Město, kde se náměstí nachází", "required": False}, "psc": {"help_text": "PSČ (5 číslic)", "required": False}, "width": {"help_text": "Šířka náměstí v metrech", "required": True}, "height": {"help_text": "Výška náměstí v metrech", "required": True}, "grid_rows": {"help_text": "Počet řádků gridu", "required": True}, "grid_cols": {"help_text": "Počet sloupců gridu", "required": True}, "cellsize": {"help_text": "Velikost buňky gridu v pixelech", "required": True}, "image": {"help_text": "Obrázek / mapa náměstí", "required": False}, } #----------------------------------------------------------------------- class ReservedDaysSerializer(serializers.Serializer): market_slot_id = serializers.IntegerField() reserved_days = serializers.ListField(child=serializers.DateField(), read_only=True) def to_representation(self, instance): # Accept instance as dict or int if isinstance(instance, dict): market_slot_id = instance.get("market_slot_id") else: market_slot_id = instance # assume int try: market_slot = MarketSlot.objects.get(id=market_slot_id) except MarketSlot.DoesNotExist: return {"market_slot_id": market_slot_id, "reserved_days": []} # Get all reserved days for this slot, return each day individually reservations = Reservation.objects.filter( market_slot_id=market_slot_id, status="reserved" ) reserved_days = set() for reservation in reservations: current = reservation.reserved_from end = reservation.reserved_to # Convert to date if it's a datetime if hasattr(current, "date"): current = current.date() if hasattr(end, "date"): end = end.date() # Include both start and end dates while current <= end: reserved_days.add(current) current += timedelta(days=1) # Return reserved days as a sorted list of individual dates return { "market_slot_id": market_slot_id, "reserved_days": sorted(reserved_days) }