L'horaire de l'email récapitulatif se configure maintenant sur /destinataires (raccourcis courants + expression cron personnalisée) au lieu de .env, avec reprogrammation immédiate sans redémarrage. Stocké en base (ParametrePlanification) plutôt que dans un fichier de config. Défaut changé pour tous les lundis à 9h. Les exemples et le code n'utilisent que des noms de jours (mon, tue, ...) dans les expressions cron : vérifié empiriquement qu'APScheduler ne suit pas la convention Unix pour les jours numériques (0=lundi chez lui, pas 0=dimanche), ce qui rendrait "0 9 * * 1" trompeur.
96 lines
3.7 KiB
Python
96 lines
3.7 KiB
Python
"""
|
|
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)
|
|
|
|
|
|
class ParametrePlanification(SQLModel, table=True):
|
|
"""Réglage de la planification de l'email récapitulatif de stock.
|
|
|
|
Une seule ligne (id=1, upsert), modifiable depuis l'interface
|
|
(/destinataires) sans éditer .env ni redémarrer le serveur — voir
|
|
app/scheduler.py qui lit/écrit cette table et reprogramme la tâche en
|
|
direct à chaque changement.
|
|
"""
|
|
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
expression_cron: str = Field(default="0 9 * * mon")
|