Ajoute modifie_le aux objets sensibles et fait transmettre la version affichée par le frontend pour refuser les écritures périmées avec un message HTTP 409. Ajoute une table historique_modifications et un service d'historique métier afin de journaliser automatiquement les raisons des changements importants sans intervention de l'utilisateur. Expose l'historique métier dans le module Journal, distinct du journal technique, et documente les processus utilisateurs ainsi qu'une présentation sobre aux membres.
154 lines
5.6 KiB
Python
154 lines
5.6 KiB
Python
"""Routes C4 / M5 — Littérature : inventaire, ajustement, gestion catalogue."""
|
|
from fastapi import APIRouter, Depends, Header, HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
|
|
from app.core.database import get_db
|
|
from app.core.security import get_current_membre, is_sysadmin
|
|
from app.core.roles import membre_a_module
|
|
from app.models.litterature import Litterature
|
|
from app.models.affectation import Affectation
|
|
from app.models.poste import Poste
|
|
from app.schemas.phase6 import LitteratureCreate, LitteratureOut, LitteratureUpdate, AjustementStock
|
|
from app.services.historique import enregistrer_historique, verifier_version_historisee
|
|
|
|
router = APIRouter(prefix="/litterature", tags=["C4/M5 — Littérature"])
|
|
|
|
|
|
async def require_resp_litterature(membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db)):
|
|
"""Vérifie que le membre est responsable de la littérature."""
|
|
if is_sysadmin(membre) or await membre_a_module(membre.id, "M5", db):
|
|
return membre
|
|
result = await db.execute(
|
|
select(Affectation).join(Poste).where(
|
|
Affectation.membre_id == membre.id,
|
|
Affectation.statut == "actif",
|
|
Poste.nom.ilike("%littérature%"),
|
|
)
|
|
)
|
|
if not result.scalars().first():
|
|
raise HTTPException(status_code=403, detail="Réservé au responsable de la littérature")
|
|
return membre
|
|
|
|
|
|
def _to_out(item: Litterature) -> LitteratureOut:
|
|
return LitteratureOut(
|
|
id=item.id,
|
|
titre=item.titre,
|
|
categorie=item.categorie,
|
|
prix_coutant=float(item.prix_coutant) if item.prix_coutant else None,
|
|
quantite_stock=item.quantite_stock,
|
|
seuil_commande=item.seuil_commande,
|
|
code_bsg=item.code_bsg,
|
|
stock_bas=item.quantite_stock <= item.seuil_commande,
|
|
modifie_le=item.modifie_le,
|
|
)
|
|
|
|
|
|
@router.get("/", response_model=list[LitteratureOut])
|
|
async def inventaire(
|
|
membre=Depends(get_current_membre),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""C4 — Inventaire de la littérature du groupe avec prix."""
|
|
result = await db.execute(
|
|
select(Litterature)
|
|
.where(Litterature.groupe_id == membre.groupe_id)
|
|
.order_by(Litterature.categorie, Litterature.titre)
|
|
)
|
|
return [_to_out(item) for item in result.scalars().all()]
|
|
|
|
|
|
@router.post("/", response_model=LitteratureOut)
|
|
async def ajouter_titre(
|
|
req: LitteratureCreate,
|
|
membre=Depends(require_resp_litterature),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Ajouter un titre au catalogue du groupe."""
|
|
categories_valides = ["livre", "brochure", "depliant", "pamphlet"]
|
|
if req.categorie not in categories_valides:
|
|
raise HTTPException(status_code=400, detail=f"Catégorie invalide. Valeurs: {categories_valides}")
|
|
|
|
item = Litterature(
|
|
groupe_id=membre.groupe_id,
|
|
titre=req.titre,
|
|
categorie=req.categorie,
|
|
prix_coutant=req.prix_coutant,
|
|
quantite_stock=req.quantite_stock,
|
|
seuil_commande=req.seuil_commande,
|
|
code_bsg=req.code_bsg,
|
|
)
|
|
db.add(item)
|
|
await db.flush()
|
|
return _to_out(item)
|
|
|
|
|
|
@router.patch("/{item_id}", response_model=LitteratureOut)
|
|
async def modifier_titre(
|
|
item_id: str,
|
|
req: LitteratureUpdate,
|
|
version_attendue: str | None = Header(default=None, alias="X-Resource-Version"),
|
|
membre=Depends(require_resp_litterature),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Modifier les infos d'un titre."""
|
|
result = await db.execute(
|
|
select(Litterature).where(Litterature.id == item_id, Litterature.groupe_id == membre.groupe_id)
|
|
)
|
|
item = result.scalar_one_or_none()
|
|
if not item:
|
|
raise HTTPException(status_code=404, detail="Titre introuvable")
|
|
await verifier_version_historisee(db, item, version_attendue, table_cible="litterature")
|
|
|
|
for field, value in req.model_dump(exclude_none=True).items():
|
|
setattr(item, field, value)
|
|
await enregistrer_historique(
|
|
db,
|
|
groupe_id=membre.groupe_id,
|
|
table_cible="litterature",
|
|
objet_id=item.id,
|
|
action="litterature.modifiee",
|
|
raison="Titre de littérature modifié.",
|
|
membre_id=membre.id,
|
|
)
|
|
await db.flush()
|
|
return _to_out(item)
|
|
|
|
|
|
@router.patch("/{item_id}/stock", response_model=LitteratureOut)
|
|
async def ajuster_stock(
|
|
item_id: str,
|
|
req: AjustementStock,
|
|
version_attendue: str | None = Header(default=None, alias="X-Resource-Version"),
|
|
membre=Depends(require_resp_litterature),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""M5 — Ajuster le stock après réception d'une commande ou correction."""
|
|
result = await db.execute(
|
|
select(Litterature).where(Litterature.id == item_id, Litterature.groupe_id == membre.groupe_id)
|
|
)
|
|
item = result.scalar_one_or_none()
|
|
if not item:
|
|
raise HTTPException(status_code=404, detail="Titre introuvable")
|
|
await verifier_version_historisee(db, item, version_attendue, table_cible="litterature")
|
|
|
|
nouveau_stock = item.quantite_stock + req.quantite
|
|
if nouveau_stock < 0:
|
|
raise HTTPException(status_code=400, detail="Le stock ne peut pas être négatif")
|
|
|
|
ancien_stock = item.quantite_stock
|
|
item.quantite_stock = nouveau_stock
|
|
await enregistrer_historique(
|
|
db,
|
|
groupe_id=membre.groupe_id,
|
|
table_cible="litterature",
|
|
objet_id=item.id,
|
|
action="litterature.stock_ajuste",
|
|
raison=f"Stock de littérature ajusté de {req.quantite}.",
|
|
membre_id=membre.id,
|
|
avant={"quantite_stock": ancien_stock},
|
|
apres={"quantite_stock": nouveau_stock},
|
|
)
|
|
await db.flush()
|
|
return _to_out(item)
|