2026-07-14 16:38:59 +00:00
|
|
|
"""
|
|
|
|
|
Routes pour la gestion des matériels : le cœur du stock.
|
|
|
|
|
|
|
|
|
|
Un "matériel" représente un type d'objet suivi en stock (nom + code-barre
|
|
|
|
|
optionnel + quantité), pas un exemplaire individuel — voir app/models.py.
|
|
|
|
|
"""
|
|
|
|
|
|
2026-07-14 17:17:27 +00:00
|
|
|
from itertools import groupby
|
|
|
|
|
|
2026-07-14 16:38:59 +00:00
|
|
|
from fastapi import APIRouter, Depends, Form
|
|
|
|
|
from fastapi.requests import Request
|
|
|
|
|
from fastapi.responses import RedirectResponse
|
|
|
|
|
from sqlmodel import Session, select
|
|
|
|
|
|
2026-07-14 17:17:27 +00:00
|
|
|
from app.categories_arbre import aplatir, categories_racines, ids_sous_arbre
|
2026-07-14 16:38:59 +00:00
|
|
|
from app.database import get_session
|
|
|
|
|
from app.email_alerts import verifier_et_alerter
|
2026-07-14 17:17:27 +00:00
|
|
|
from app.models import Categorie, Materiel
|
2026-07-14 16:38:59 +00:00
|
|
|
from app.templates_engine import templates
|
|
|
|
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
2026-07-14 17:17:27 +00:00
|
|
|
# Chaque clé de tri associe une fonction de clé de tri Python et un
|
|
|
|
|
# indicateur d'ordre inverse. "categorie" trie par chemin complet puis nom
|
|
|
|
|
# : les matériels d'une même catégorie se retrouvent ainsi consécutifs,
|
|
|
|
|
# ce qui permet de les regrouper sous un même en-tête dans le template
|
|
|
|
|
# sans logique de regroupement séparée à maintenir.
|
|
|
|
|
CLES_TRI = {
|
|
|
|
|
"nom_asc": (lambda m: m.nom.lower(), False),
|
|
|
|
|
"nom_desc": (lambda m: m.nom.lower(), True),
|
|
|
|
|
"quantite_asc": (lambda m: m.quantite, False),
|
|
|
|
|
"quantite_desc": (lambda m: m.quantite, True),
|
|
|
|
|
"categorie": (lambda m: (m.categorie.chemin().lower(), m.nom.lower()), False),
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 16:38:59 +00:00
|
|
|
|
2026-07-14 17:17:27 +00:00
|
|
|
@router.get("/")
|
2026-07-14 16:38:59 +00:00
|
|
|
@router.get("/materiels")
|
2026-07-14 17:17:27 +00:00
|
|
|
def lister_materiels(
|
|
|
|
|
request: Request,
|
|
|
|
|
q: str = "",
|
|
|
|
|
categorie_id: str = "",
|
|
|
|
|
stock_bas: bool = False,
|
|
|
|
|
tri: str = "nom_asc",
|
|
|
|
|
session: Session = Depends(get_session),
|
|
|
|
|
):
|
|
|
|
|
"""Liste les matériels avec recherche, filtre par catégorie (elle et
|
|
|
|
|
ses sous-catégories), filtre "stock bas uniquement" et tri — tous
|
|
|
|
|
combinables, indispensable dès que la liste devient longue.
|
|
|
|
|
|
|
|
|
|
Tout est fait côté serveur en Python (pas de pagination, juste des
|
|
|
|
|
filtres) : à l'échelle d'un stock IT d'un département, ça reste très
|
|
|
|
|
largement suffisant et évite d'avoir à gérer un état côté client.
|
|
|
|
|
"""
|
|
|
|
|
materiels = list(session.exec(select(Materiel)).all())
|
|
|
|
|
|
2026-07-14 17:06:37 +00:00
|
|
|
if q:
|
|
|
|
|
q_normalise = q.lower()
|
|
|
|
|
materiels = [
|
|
|
|
|
m
|
2026-07-14 17:17:27 +00:00
|
|
|
for m in materiels
|
2026-07-14 17:06:37 +00:00
|
|
|
if q_normalise in m.nom.lower()
|
|
|
|
|
or (m.code_barre and q_normalise in m.code_barre.lower())
|
|
|
|
|
]
|
2026-07-14 17:17:27 +00:00
|
|
|
|
|
|
|
|
if categorie_id:
|
|
|
|
|
categorie_selectionnee = session.get(Categorie, int(categorie_id))
|
|
|
|
|
ids_valides = ids_sous_arbre(categorie_selectionnee) if categorie_selectionnee else set()
|
|
|
|
|
materiels = [m for m in materiels if m.categorie_id in ids_valides]
|
|
|
|
|
|
|
|
|
|
if stock_bas:
|
|
|
|
|
materiels = [
|
|
|
|
|
m for m in materiels if m.seuil_alerte is not None and m.quantite < m.seuil_alerte
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
cle, inverse = CLES_TRI.get(tri, CLES_TRI["nom_asc"])
|
|
|
|
|
materiels.sort(key=cle, reverse=inverse)
|
|
|
|
|
|
|
|
|
|
groupes = None
|
|
|
|
|
if tri == "categorie":
|
|
|
|
|
groupes = [
|
|
|
|
|
(chemin, list(items))
|
|
|
|
|
for chemin, items in groupby(materiels, key=lambda m: m.categorie.chemin())
|
|
|
|
|
]
|
2026-07-14 17:06:37 +00:00
|
|
|
|
|
|
|
|
categories_aplaties = aplatir(categories_racines(session))
|
2026-07-14 16:38:59 +00:00
|
|
|
return templates.TemplateResponse(
|
|
|
|
|
request,
|
|
|
|
|
"materiels.html",
|
2026-07-14 17:17:27 +00:00
|
|
|
{
|
|
|
|
|
"materiels": materiels,
|
|
|
|
|
"groupes": groupes,
|
|
|
|
|
"categories_aplaties": categories_aplaties,
|
|
|
|
|
"q": q,
|
|
|
|
|
"categorie_id": categorie_id,
|
|
|
|
|
"stock_bas": stock_bas,
|
|
|
|
|
"tri": tri,
|
|
|
|
|
},
|
2026-07-14 16:38:59 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/materiels")
|
|
|
|
|
def creer_materiel(
|
|
|
|
|
nom: str = Form(...),
|
2026-07-14 17:06:37 +00:00
|
|
|
categorie_id: int = Form(...),
|
2026-07-14 16:38:59 +00:00
|
|
|
code_barre: str = Form(""),
|
|
|
|
|
quantite: int = Form(0),
|
|
|
|
|
seuil_alerte: str = Form(""),
|
|
|
|
|
session: Session = Depends(get_session),
|
|
|
|
|
):
|
|
|
|
|
"""Crée un nouveau type de matériel dans le stock."""
|
|
|
|
|
materiel = Materiel(
|
|
|
|
|
nom=nom,
|
2026-07-14 17:06:37 +00:00
|
|
|
categorie_id=categorie_id,
|
2026-07-14 16:38:59 +00:00
|
|
|
code_barre=code_barre or None,
|
|
|
|
|
quantite=quantite,
|
|
|
|
|
seuil_alerte=int(seuil_alerte) if seuil_alerte else None,
|
|
|
|
|
)
|
|
|
|
|
session.add(materiel)
|
|
|
|
|
session.commit()
|
|
|
|
|
session.refresh(materiel)
|
|
|
|
|
verifier_et_alerter(materiel, session)
|
|
|
|
|
return RedirectResponse(url="/materiels", status_code=303)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/materiels/{materiel_id}/modifier")
|
|
|
|
|
def modifier_materiel(
|
|
|
|
|
materiel_id: int,
|
|
|
|
|
nom: str = Form(...),
|
2026-07-14 17:06:37 +00:00
|
|
|
categorie_id: int = Form(...),
|
2026-07-14 16:38:59 +00:00
|
|
|
code_barre: str = Form(""),
|
|
|
|
|
quantite: int = Form(0),
|
|
|
|
|
seuil_alerte: str = Form(""),
|
|
|
|
|
session: Session = Depends(get_session),
|
|
|
|
|
):
|
2026-07-14 17:06:37 +00:00
|
|
|
"""Met à jour un matériel existant."""
|
2026-07-14 16:38:59 +00:00
|
|
|
materiel = session.get(Materiel, materiel_id)
|
|
|
|
|
if materiel:
|
|
|
|
|
materiel.nom = nom
|
2026-07-14 17:06:37 +00:00
|
|
|
materiel.categorie_id = categorie_id
|
2026-07-14 16:38:59 +00:00
|
|
|
materiel.code_barre = code_barre or None
|
|
|
|
|
materiel.quantite = quantite
|
|
|
|
|
materiel.seuil_alerte = int(seuil_alerte) if seuil_alerte else None
|
|
|
|
|
session.add(materiel)
|
|
|
|
|
session.commit()
|
|
|
|
|
session.refresh(materiel)
|
|
|
|
|
verifier_et_alerter(materiel, session)
|
|
|
|
|
return RedirectResponse(url="/materiels", status_code=303)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/materiels/{materiel_id}/supprimer")
|
|
|
|
|
def supprimer_materiel(materiel_id: int, session: Session = Depends(get_session)):
|
|
|
|
|
"""Supprime définitivement un matériel du stock."""
|
|
|
|
|
materiel = session.get(Materiel, materiel_id)
|
|
|
|
|
if materiel:
|
|
|
|
|
session.delete(materiel)
|
|
|
|
|
session.commit()
|
|
|
|
|
return RedirectResponse(url="/materiels", status_code=303)
|