"""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, snapshot, 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.groupe_id == membre.groupe_id, Affectation.statut == "actif", Poste.groupe_id == membre.groupe_id, 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") avant = snapshot(item, "titre", "categorie", "prix_coutant", "seuil_commande", "code_bsg") 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, avant=avant, apres=snapshot(item, "titre", "categorie", "prix_coutant", "seuil_commande", "code_bsg"), ) 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)