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.
197 lines
6.7 KiB
Python
197 lines
6.7 KiB
Python
"""Routes M1 — Rédiger un PV de réunion d'affaires."""
|
|
from fastapi import APIRouter, Depends, Header, HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, desc
|
|
|
|
from app.core.database import get_db
|
|
from app.core.roles import require_secretaire
|
|
from app.core.security import get_current_membre
|
|
from app.models.pv_reunion import PvReunion
|
|
from app.models.reunion import Reunion
|
|
from app.models.membre import Membre
|
|
from app.schemas.phase4 import PvAdoption, PvCreate, PvOut, PvUpdate
|
|
from app.services.historique import enregistrer_historique, verifier_version_historisee
|
|
|
|
router = APIRouter(prefix="/pv", tags=["M1 — Procès-verbal"])
|
|
|
|
|
|
async def _to_out(pv: PvReunion, db: AsyncSession) -> PvOut:
|
|
redacteur = await db.get(Membre, pv.redacteur_id)
|
|
reunion = await db.get(Reunion, pv.reunion_id) if pv.reunion_id else None
|
|
return PvOut(
|
|
id=pv.id,
|
|
reunion_id=pv.reunion_id,
|
|
redacteur_prenom=redacteur.prenom if redacteur else "?",
|
|
contenu=pv.contenu,
|
|
decisions=pv.decisions,
|
|
approuve=pv.approuve,
|
|
cree_le=pv.cree_le,
|
|
modifie_le=pv.modifie_le,
|
|
date_reunion=reunion.date_reunion if reunion else None,
|
|
)
|
|
|
|
|
|
@router.get("/", response_model=list[PvOut])
|
|
async def liste_pv(
|
|
limite: int = 12,
|
|
membre=Depends(get_current_membre),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Lister les PV des réunions d'affaires."""
|
|
# Récupérer les PV via les réunions du groupe
|
|
result = await db.execute(
|
|
select(PvReunion)
|
|
.join(Reunion, PvReunion.reunion_id == Reunion.id)
|
|
.where(Reunion.groupe_id == membre.groupe_id)
|
|
.order_by(desc(PvReunion.cree_le))
|
|
.limit(limite)
|
|
)
|
|
return [await _to_out(pv, db) for pv in result.scalars().all()]
|
|
|
|
|
|
@router.get("/{pv_id}", response_model=PvOut)
|
|
async def detail_pv(
|
|
pv_id: str,
|
|
membre=Depends(get_current_membre),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await db.execute(
|
|
select(PvReunion)
|
|
.join(Reunion, PvReunion.reunion_id == Reunion.id)
|
|
.where(PvReunion.id == pv_id, Reunion.groupe_id == membre.groupe_id)
|
|
)
|
|
pv = result.scalar_one_or_none()
|
|
if not pv:
|
|
raise HTTPException(status_code=404, detail="PV introuvable")
|
|
return await _to_out(pv, db)
|
|
|
|
|
|
@router.post("/", response_model=PvOut)
|
|
async def creer_pv(
|
|
req: PvCreate,
|
|
membre=Depends(require_secretaire),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""M1 — Rédiger un procès-verbal."""
|
|
# Vérifier la réunion
|
|
result = await db.execute(
|
|
select(Reunion).where(
|
|
Reunion.id == req.reunion_id,
|
|
Reunion.groupe_id == membre.groupe_id,
|
|
)
|
|
)
|
|
reunion = result.scalar_one_or_none()
|
|
if not reunion:
|
|
raise HTTPException(status_code=404, detail="Réunion introuvable")
|
|
|
|
decisions_data = None
|
|
if req.decisions:
|
|
decisions_data = [d.model_dump() for d in req.decisions]
|
|
|
|
pv = PvReunion(
|
|
reunion_id=req.reunion_id,
|
|
redacteur_id=membre.id,
|
|
contenu=req.contenu.strip(),
|
|
decisions=decisions_data,
|
|
)
|
|
db.add(pv)
|
|
await db.flush()
|
|
|
|
return await _to_out(pv, db)
|
|
|
|
|
|
@router.patch("/{pv_id}", response_model=PvOut)
|
|
async def modifier_pv(
|
|
pv_id: str,
|
|
req: PvUpdate,
|
|
version_attendue: str | None = Header(default=None, alias="X-Resource-Version"),
|
|
membre=Depends(require_secretaire),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Modifier un PV existant."""
|
|
result = await db.execute(
|
|
select(PvReunion)
|
|
.join(Reunion, PvReunion.reunion_id == Reunion.id)
|
|
.where(PvReunion.id == pv_id, Reunion.groupe_id == membre.groupe_id)
|
|
)
|
|
pv = result.scalar_one_or_none()
|
|
if not pv:
|
|
raise HTTPException(status_code=404, detail="PV introuvable")
|
|
await verifier_version_historisee(db, pv, version_attendue, table_cible="pv_reunions")
|
|
|
|
if req.contenu is not None:
|
|
pv.contenu = req.contenu.strip()
|
|
if req.decisions is not None:
|
|
pv.decisions = [d.model_dump() for d in req.decisions]
|
|
if req.approuve is not None:
|
|
pv.approuve = req.approuve
|
|
await enregistrer_historique(
|
|
db,
|
|
groupe_id=membre.groupe_id,
|
|
table_cible="pv_reunions",
|
|
objet_id=pv.id,
|
|
action="pv.modifie",
|
|
raison="Procès-verbal modifié.",
|
|
membre_id=membre.id,
|
|
)
|
|
await db.flush()
|
|
return await _to_out(pv, db)
|
|
|
|
|
|
@router.patch("/{pv_id}/adopter", response_model=PvOut)
|
|
async def adopter_pv(
|
|
pv_id: str,
|
|
req: PvAdoption,
|
|
version_attendue: str | None = Header(default=None, alias="X-Resource-Version"),
|
|
membre=Depends(require_secretaire),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await db.execute(
|
|
select(PvReunion)
|
|
.join(Reunion, PvReunion.reunion_id == Reunion.id)
|
|
.where(PvReunion.id == pv_id, Reunion.groupe_id == membre.groupe_id)
|
|
)
|
|
pv = result.scalar_one_or_none()
|
|
if not pv:
|
|
raise HTTPException(status_code=404, detail="PV introuvable")
|
|
await verifier_version_historisee(db, pv, version_attendue, table_cible="pv_reunions")
|
|
if pv.approuve:
|
|
raise HTTPException(status_code=400, detail="PV déjà adopté")
|
|
if req.proposeur_id == req.secondeur_id:
|
|
raise HTTPException(status_code=400, detail="Le proposeur et le secondeur doivent être différents")
|
|
proposeur = await db.get(Membre, req.proposeur_id)
|
|
secondeur = await db.get(Membre, req.secondeur_id)
|
|
if not proposeur or proposeur.groupe_id != membre.groupe_id or not proposeur.actif:
|
|
raise HTTPException(status_code=404, detail="Proposeur introuvable ou inactif")
|
|
if not secondeur or secondeur.groupe_id != membre.groupe_id or not secondeur.actif:
|
|
raise HTTPException(status_code=404, detail="Secondeur introuvable ou inactif")
|
|
|
|
if req.contenu is not None:
|
|
contenu = req.contenu.strip()
|
|
if not contenu:
|
|
raise HTTPException(status_code=400, detail="Le contenu du PV ne peut pas être vide")
|
|
pv.contenu = contenu
|
|
|
|
decisions = list(pv.decisions or [])
|
|
decisions.append({
|
|
"sujet": "Adoption du procès-verbal précédent",
|
|
"decision": "Le procès-verbal précédent est adopté.",
|
|
"proposeur": proposeur.prenom,
|
|
"secondeur": secondeur.prenom,
|
|
"proposeur_id": str(proposeur.id),
|
|
"secondeur_id": str(secondeur.id),
|
|
"statut": "complete",
|
|
})
|
|
pv.decisions = decisions
|
|
pv.approuve = True
|
|
await enregistrer_historique(
|
|
db,
|
|
groupe_id=membre.groupe_id,
|
|
table_cible="pv_reunions",
|
|
objet_id=pv.id,
|
|
action="pv.adopte",
|
|
raison="Procès-verbal adopté.",
|
|
membre_id=membre.id,
|
|
)
|
|
await db.flush()
|
|
return await _to_out(pv, db)
|