Un code-barre inconnu scanné propose désormais le même formulaire de création que /materiels au lieu d'un simple message d'erreur. La quantité peut être ajustée d'un delta choisi librement (pas figé à ±1) ou définie directement à une valeur absolue après un inventaire. La racine "/" sert directement la liste des matériels sans redirection. La page Matériels gagne un filtre par catégorie, un filtre stock bas, et un tri incluant un regroupement par catégorie.
159 lines
5.3 KiB
Python
159 lines
5.3 KiB
Python
"""
|
|
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.
|
|
"""
|
|
|
|
from itertools import groupby
|
|
|
|
from fastapi import APIRouter, Depends, Form
|
|
from fastapi.requests import Request
|
|
from fastapi.responses import RedirectResponse
|
|
from sqlmodel import Session, select
|
|
|
|
from app.categories_arbre import aplatir, categories_racines, ids_sous_arbre
|
|
from app.database import get_session
|
|
from app.email_alerts import verifier_et_alerter
|
|
from app.models import Categorie, Materiel
|
|
from app.templates_engine import templates
|
|
|
|
router = APIRouter()
|
|
|
|
# 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),
|
|
}
|
|
|
|
|
|
@router.get("/")
|
|
@router.get("/materiels")
|
|
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())
|
|
|
|
if q:
|
|
q_normalise = q.lower()
|
|
materiels = [
|
|
m
|
|
for m in materiels
|
|
if q_normalise in m.nom.lower()
|
|
or (m.code_barre and q_normalise in m.code_barre.lower())
|
|
]
|
|
|
|
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())
|
|
]
|
|
|
|
categories_aplaties = aplatir(categories_racines(session))
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"materiels.html",
|
|
{
|
|
"materiels": materiels,
|
|
"groupes": groupes,
|
|
"categories_aplaties": categories_aplaties,
|
|
"q": q,
|
|
"categorie_id": categorie_id,
|
|
"stock_bas": stock_bas,
|
|
"tri": tri,
|
|
},
|
|
)
|
|
|
|
|
|
@router.post("/materiels")
|
|
def creer_materiel(
|
|
nom: str = Form(...),
|
|
categorie_id: int = Form(...),
|
|
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,
|
|
categorie_id=categorie_id,
|
|
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(...),
|
|
categorie_id: int = Form(...),
|
|
code_barre: str = Form(""),
|
|
quantite: int = Form(0),
|
|
seuil_alerte: str = Form(""),
|
|
session: Session = Depends(get_session),
|
|
):
|
|
"""Met à jour un matériel existant."""
|
|
materiel = session.get(Materiel, materiel_id)
|
|
if materiel:
|
|
materiel.nom = nom
|
|
materiel.categorie_id = categorie_id
|
|
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)
|