generated from gilles/template-webapp
159 lines
5.6 KiB
Python
159 lines
5.6 KiB
Python
"""Schémas Pydantic pour les objets d'inventaire.
|
|
|
|
Définit les schémas de validation pour les requêtes et réponses API.
|
|
"""
|
|
|
|
from datetime import date, datetime
|
|
from decimal import Decimal
|
|
from typing import Any
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
|
|
from app.models.item import ItemStatus
|
|
from app.schemas.category import CategoryResponse
|
|
from app.schemas.location import LocationResponse
|
|
|
|
|
|
class ItemBase(BaseModel):
|
|
"""Schéma de base pour les objets."""
|
|
|
|
name: str = Field(..., min_length=1, max_length=200, description="Nom de l'objet")
|
|
description: str | None = Field(None, description="Description détaillée")
|
|
quantity: int = Field(default=1, ge=0, description="Quantité en stock")
|
|
status: ItemStatus = Field(default=ItemStatus.IN_STOCK, description="Statut de l'objet")
|
|
brand: str | None = Field(None, max_length=100, description="Marque")
|
|
model: str | None = Field(None, max_length=100, description="Modèle")
|
|
serial_number: str | None = Field(None, max_length=100, description="Numéro de série")
|
|
url: str | None = Field(None, max_length=500, description="Lien vers page produit")
|
|
price: Decimal | None = Field(None, ge=0, decimal_places=2, description="Prix d'achat")
|
|
purchase_date: date | None = Field(None, description="Date d'achat")
|
|
characteristics: dict[str, str] | None = Field(None, description="Caractéristiques techniques (clé-valeur)")
|
|
notes: str | None = Field(None, description="Notes libres")
|
|
|
|
|
|
class ItemCreate(ItemBase):
|
|
"""Schéma pour la création d'un objet."""
|
|
|
|
category_id: int = Field(..., description="ID de la catégorie")
|
|
location_id: int = Field(..., description="ID de l'emplacement")
|
|
parent_item_id: int | None = Field(None, description="ID de l'objet parent (si intégré)")
|
|
shop_id: int | None = Field(None, description="ID de la boutique d'achat")
|
|
|
|
|
|
class ItemUpdate(BaseModel):
|
|
"""Schéma pour la mise à jour d'un objet (tous les champs optionnels)."""
|
|
|
|
name: str | None = Field(None, min_length=1, max_length=200)
|
|
description: str | None = None
|
|
quantity: int | None = Field(None, ge=0)
|
|
status: ItemStatus | None = None
|
|
brand: str | None = Field(None, max_length=100)
|
|
model: str | None = Field(None, max_length=100)
|
|
serial_number: str | None = Field(None, max_length=100)
|
|
url: str | None = Field(None, max_length=500)
|
|
price: Decimal | None = Field(None, ge=0)
|
|
purchase_date: date | None = None
|
|
characteristics: dict[str, str] | None = None
|
|
notes: str | None = None
|
|
category_id: int | None = None
|
|
location_id: int | None = None
|
|
parent_item_id: int | None = None
|
|
shop_id: int | None = None
|
|
|
|
|
|
class ItemResponse(ItemBase):
|
|
"""Schéma de réponse pour un objet."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
category_id: int
|
|
location_id: int
|
|
parent_item_id: int | None = None
|
|
shop_id: int | None = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
class ItemWithRelations(ItemResponse):
|
|
"""Schéma de réponse avec les relations (catégorie et emplacement)."""
|
|
|
|
category: CategoryResponse
|
|
location: LocationResponse
|
|
thumbnail_id: int | None = None
|
|
parent_item_name: str | None = None
|
|
|
|
@model_validator(mode="before")
|
|
@classmethod
|
|
def extract_computed_fields(cls, data: Any) -> Any:
|
|
"""Extrait les champs calculés : thumbnail et nom du parent."""
|
|
from sqlalchemy.orm import InstanceState
|
|
|
|
thumbnail_id = None
|
|
parent_item_name = None
|
|
|
|
# Vérifier que les relations sont chargées (éviter lazy load en async)
|
|
loaded_relations: set[str] = set()
|
|
if hasattr(data, "_sa_instance_state"):
|
|
state: InstanceState = data._sa_instance_state
|
|
loaded_relations = set(state.dict.keys())
|
|
|
|
if "documents" in loaded_relations:
|
|
for doc in data.documents:
|
|
if doc.type.value == "photo":
|
|
thumbnail_id = doc.id
|
|
break
|
|
|
|
if "parent_item" in loaded_relations and data.parent_item is not None:
|
|
parent_item_name = data.parent_item.name
|
|
|
|
if isinstance(data, dict):
|
|
if thumbnail_id:
|
|
data["thumbnail_id"] = thumbnail_id
|
|
if parent_item_name:
|
|
data["parent_item_name"] = parent_item_name
|
|
elif thumbnail_id or parent_item_name:
|
|
result = {}
|
|
for k in dir(data):
|
|
if k.startswith("_"):
|
|
continue
|
|
# Ne pas accéder aux relations non chargées
|
|
if k in ("documents", "parent_item", "children", "shop"):
|
|
continue
|
|
try:
|
|
result[k] = getattr(data, k)
|
|
except Exception:
|
|
pass
|
|
if thumbnail_id:
|
|
result["thumbnail_id"] = thumbnail_id
|
|
if parent_item_name:
|
|
result["parent_item_name"] = parent_item_name
|
|
return result
|
|
|
|
return data
|
|
|
|
|
|
class ItemSummary(BaseModel):
|
|
"""Schéma résumé pour les listes d'objets."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
name: str
|
|
quantity: int
|
|
status: ItemStatus
|
|
brand: str | None
|
|
category_id: int
|
|
location_id: int
|
|
|
|
|
|
class ItemFilter(BaseModel):
|
|
"""Schéma pour filtrer les objets."""
|
|
|
|
category_id: int | None = None
|
|
location_id: int | None = None
|
|
status: ItemStatus | None = None
|
|
search: str | None = Field(None, min_length=2, description="Recherche textuelle")
|
|
min_price: Decimal | None = None
|
|
max_price: Decimal | None = None
|