224 lines
8 KiB
Python
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)
|