""" Modèles de données du stock IT. Principe volontairement simple, demandé par le département IT : on ne suit PAS des exemplaires individuels (pas de numéro de série, pas d'historique détaillé par unité). On suit juste, pour chaque *type* de matériel, une quantité en stock. Ex: "PC Dell XPS 13 modèle 9310" -> quantite=5, "Câble RJ45 2m" -> quantite=42. Hiérarchie : Categorie (arborescence à profondeur illimitée, auto- référencée via parent_id) -> Materiel. Une catégorie peut avoir des sous-catégories, qui peuvent elles-mêmes en avoir, etc. (ex: "Réseau" > "Câbles" > "RJ45" > "2m"). Un matériel se rattache à n'importe quel nœud de l'arborescence, pas forcément une feuille. """ from typing import Optional from sqlmodel import Field, Relationship, SQLModel class Categorie(SQLModel, table=True): """Nœud d'une arborescence de catégories à profondeur illimitée. parent_id est vide pour une catégorie racine. La relation "parent" est auto-référencée (une Categorie pointe vers une autre Categorie) : c'est ce qui permet une profondeur arbitraire, contrairement à un modèle Categorie/SousCategorie à deux niveaux fixes. """ id: Optional[int] = Field(default=None, primary_key=True) nom: str parent_id: Optional[int] = Field(default=None, foreign_key="categorie.id") parent: Optional["Categorie"] = Relationship( back_populates="enfants", sa_relationship_kwargs={"remote_side": "Categorie.id"}, ) enfants: list["Categorie"] = Relationship(back_populates="parent") materiels: list["Materiel"] = Relationship(back_populates="categorie") def chemin(self) -> str: """Chemin complet depuis la racine, ex: 'Réseau / Câbles / RJ45'. Utilisé partout où on affiche une catégorie hors de son arbre (liste de matériels, résultat de scan, recherche) pour garder le contexte sans avoir à afficher l'arborescence entière. """ if self.parent is None: return self.nom return f"{self.parent.chemin()} / {self.nom}" class Materiel(SQLModel, table=True): """Un type de matériel suivi en stock. code_barre identifie le *type* de matériel (le "SKU"), pas un exemplaire précis : scanner deux PC XPS 13 identiques renvoie le même matériel, et chaque scan ajuste juste la quantité globale. """ id: Optional[int] = Field(default=None, primary_key=True) nom: str code_barre: Optional[str] = Field(default=None, index=True, unique=True) quantite: int = Field(default=0) # En dessous de ce seuil, une alerte email est envoyée (voir email_alerts.py). # None = pas d'alerte configurée pour ce matériel. seuil_alerte: Optional[int] = Field(default=None) categorie_id: int = Field(foreign_key="categorie.id") categorie: Optional[Categorie] = Relationship(back_populates="materiels") class DestinataireAlerte(SQLModel, table=True): """Adresse email qui reçoit les alertes de stock bas. Liste globale simple (pas de destinataires par matériel/catégorie) : c'est suffisant pour une petite équipe IT et ça reste facile à gérer. """ id: Optional[int] = Field(default=None, primary_key=True) email: str = Field(unique=True)