groupe-meditation/backend/app/routers/pv.py

224 lines
8 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, snapshot, verifier_version_historisee
from app.services.rapports_adoptes import verifier_reunion_non_cristallisee
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")
await verifier_reunion_non_cristallisee(
db, membre.groupe_id, reunion.id, "assemblee", "Cette assemblée est adoptée"
)
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")
if pv.approuve:
raise HTTPException(
status_code=400,
detail="Un PV adopté ne peut pas être modifié directement. Inscrivez la correction au PV d'une assemblée suivante.",
)
if req.approuve is not None:
raise HTTPException(status_code=400, detail="Utilisez le processus d'adoption du PV")
reunion = await db.get(Reunion, pv.reunion_id)
if reunion:
await verifier_reunion_non_cristallisee(
db, membre.groupe_id, reunion.id, "assemblee", "Cette assemblée est adoptée"
)
await verifier_version_historisee(db, pv, version_attendue, table_cible="pv_reunions")
avant = snapshot(pv, "contenu", "decisions", "approuve")
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,
avant=avant,
apres=snapshot(pv, "contenu", "decisions", "approuve"),
)
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")
reunion = await db.get(Reunion, pv.reunion_id)
if reunion:
await verifier_reunion_non_cristallisee(
db, membre.groupe_id, reunion.id, "assemblee", "Cette assemblée est adoptée"
)
await verifier_version_historisee(db, pv, version_attendue, table_cible="pv_reunions")
avant = snapshot(pv, "contenu", "decisions", "approuve")
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,
avant=avant,
apres=snapshot(pv, "contenu", "decisions", "approuve"),
)
await db.flush()
return await _to_out(pv, db)