236 lines
11 KiB
Python
236 lines
11 KiB
Python
"""Routes — Opérations bancaires (dépôts, retraits)."""
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, desc, func
|
|
|
|
from app.core.database import get_db
|
|
from app.core.security import get_current_membre
|
|
from app.models.operation_bancaire import OperationBancaire
|
|
from app.models.collecte import Collecte
|
|
from app.models.depense import Depense
|
|
from app.models.reserve import Reserve
|
|
from app.models.vente_litterature import VenteLitterature
|
|
from app.models.membre import Membre
|
|
from app.models.affectation import Affectation
|
|
from app.models.poste import Poste
|
|
from app.schemas.phase1 import OperationBancaireCreate, OperationBancaireOut, ReserveCreate, ReserveOut, ReserveUpdate
|
|
|
|
router = APIRouter(prefix="/banque", tags=["Opérations bancaires"])
|
|
|
|
|
|
async def _require_tresorier(membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db)):
|
|
"""Vérifie que le membre est trésorier ou exécutif."""
|
|
result = await db.execute(
|
|
select(Affectation).join(Poste).where(
|
|
Affectation.membre_id == membre.id,
|
|
Affectation.statut == "actif",
|
|
(Poste.nom.ilike("%trésorier%") | (Poste.categorie == "executif")),
|
|
)
|
|
)
|
|
if not result.scalars().first():
|
|
raise HTTPException(status_code=403, detail="Réservé au trésorier ou à l'exécutif")
|
|
return membre
|
|
|
|
|
|
async def _encaisse(groupe_id, db: AsyncSession) -> float:
|
|
total_collectes = float((await db.execute(
|
|
select(func.coalesce(func.sum(Collecte.montant), 0)).where(
|
|
Collecte.groupe_id == groupe_id, Collecte.statut == "recu")
|
|
)).scalar())
|
|
ventes_recues = float((await db.execute(
|
|
select(func.coalesce(func.sum(VenteLitterature.montant), 0)).where(
|
|
VenteLitterature.groupe_id == groupe_id, VenteLitterature.remis_tresorier == True)
|
|
)).scalar())
|
|
retraits = float((await db.execute(
|
|
select(func.coalesce(func.sum(OperationBancaire.montant), 0)).where(
|
|
OperationBancaire.groupe_id == groupe_id, OperationBancaire.type == "retrait")
|
|
)).scalar())
|
|
depots = float((await db.execute(
|
|
select(func.coalesce(func.sum(OperationBancaire.montant), 0)).where(
|
|
OperationBancaire.groupe_id == groupe_id, OperationBancaire.type == "depot")
|
|
)).scalar())
|
|
depenses_cash = float((await db.execute(
|
|
select(func.coalesce(func.sum(Depense.montant), 0)).where(
|
|
Depense.groupe_id == groupe_id, Depense.paye == True, Depense.methode == "COLLECTE")
|
|
)).scalar())
|
|
return total_collectes + ventes_recues + retraits - depots - depenses_cash
|
|
|
|
|
|
async def _solde_banque_brut(groupe_id, db: AsyncSession) -> float:
|
|
from app.models.envoi_contribution import EnvoiContribution
|
|
|
|
result = await db.execute(
|
|
select(OperationBancaire).where(
|
|
OperationBancaire.groupe_id == groupe_id,
|
|
OperationBancaire.type == "releve",
|
|
).order_by(desc(OperationBancaire.date_operation)).limit(1)
|
|
)
|
|
dernier_releve = result.scalar_one_or_none()
|
|
solde_releve = float(dernier_releve.montant) if dernier_releve else 0.0
|
|
filtre_depuis = dernier_releve.date_operation if dernier_releve else None
|
|
|
|
q_depots = select(func.coalesce(func.sum(OperationBancaire.montant), 0)).where(
|
|
OperationBancaire.groupe_id == groupe_id, OperationBancaire.type == "depot")
|
|
q_retraits = select(func.coalesce(func.sum(OperationBancaire.montant), 0)).where(
|
|
OperationBancaire.groupe_id == groupe_id, OperationBancaire.type == "retrait")
|
|
q_contribs = select(func.coalesce(func.sum(EnvoiContribution.montant), 0)).where(
|
|
EnvoiContribution.groupe_id == groupe_id, EnvoiContribution.debitee == True)
|
|
q_depenses = select(func.coalesce(func.sum(Depense.montant), 0)).where(
|
|
Depense.groupe_id == groupe_id,
|
|
Depense.paye == True,
|
|
Depense.methode.in_(["VIREMENT", "CHEQUE"]),
|
|
)
|
|
if filtre_depuis:
|
|
q_depots = q_depots.where(OperationBancaire.date_operation > filtre_depuis)
|
|
q_retraits = q_retraits.where(OperationBancaire.date_operation > filtre_depuis)
|
|
q_contribs = q_contribs.where(EnvoiContribution.date_envoi > filtre_depuis)
|
|
q_depenses = q_depenses.where(Depense.date_confirmation > filtre_depuis)
|
|
|
|
depots = float((await db.execute(q_depots)).scalar())
|
|
retraits = float((await db.execute(q_retraits)).scalar())
|
|
contribs = float((await db.execute(q_contribs)).scalar())
|
|
depenses = float((await db.execute(q_depenses)).scalar())
|
|
return solde_releve + depots - retraits - contribs - depenses
|
|
|
|
|
|
async def _total_reserves(groupe_id, db: AsyncSession, exclure_id: str | None = None) -> float:
|
|
query = select(func.coalesce(func.sum(Reserve.montant), 0)).where(
|
|
Reserve.groupe_id == groupe_id,
|
|
Reserve.active == True,
|
|
)
|
|
if exclure_id:
|
|
query = query.where(Reserve.id != exclure_id)
|
|
return float((await db.execute(query)).scalar())
|
|
|
|
|
|
@router.post("/", response_model=OperationBancaireOut)
|
|
async def enregistrer_operation(
|
|
req: OperationBancaireCreate,
|
|
membre=Depends(_require_tresorier),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Enregistrer un dépôt ou retrait bancaire (trésorier)."""
|
|
if req.type not in ("depot", "retrait", "releve"):
|
|
raise HTTPException(status_code=400, detail="Type doit être 'depot', 'retrait' ou 'releve'")
|
|
if req.montant <= 0:
|
|
raise HTTPException(status_code=400, detail="Le montant doit être positif")
|
|
solde_banque = await _solde_banque_brut(membre.groupe_id, db)
|
|
total_reserves = await _total_reserves(membre.groupe_id, db)
|
|
if req.type == "depot" and req.montant > await _encaisse(membre.groupe_id, db):
|
|
raise HTTPException(status_code=400, detail="Le dépôt ne peut pas dépasser l'encaisse disponible")
|
|
if req.type == "retrait" and req.montant > solde_banque:
|
|
raise HTTPException(status_code=400, detail="Le retrait ne peut pas dépasser le solde en banque")
|
|
if req.type == "retrait" and solde_banque - req.montant < total_reserves:
|
|
raise HTTPException(status_code=400, detail="Le retrait ferait dépasser les réserves par rapport au solde en banque")
|
|
if req.type == "releve" and req.montant < total_reserves:
|
|
raise HTTPException(status_code=400, detail="Le relevé ne peut pas être inférieur au total des réserves")
|
|
|
|
op = OperationBancaire(
|
|
groupe_id=membre.groupe_id,
|
|
type=req.type,
|
|
montant=req.montant,
|
|
date_operation=req.date_operation,
|
|
effectue_par=membre.id,
|
|
note=req.note,
|
|
)
|
|
db.add(op)
|
|
await db.flush()
|
|
return OperationBancaireOut(
|
|
id=op.id,
|
|
type=op.type,
|
|
montant=float(op.montant),
|
|
date_operation=op.date_operation,
|
|
effectue_par_prenom=membre.prenom,
|
|
note=op.note,
|
|
cree_le=op.cree_le,
|
|
)
|
|
|
|
|
|
@router.get("/reserves", response_model=list[ReserveOut])
|
|
async def liste_reserves(membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(
|
|
select(Reserve).where(Reserve.groupe_id == membre.groupe_id, Reserve.active == True)
|
|
.order_by(Reserve.nom)
|
|
)
|
|
return [
|
|
ReserveOut(id=r.id, nom=r.nom, montant=float(r.montant), note=r.note, active=r.active, cree_le=r.cree_le)
|
|
for r in result.scalars().all()
|
|
]
|
|
|
|
|
|
@router.post("/reserves", response_model=ReserveOut)
|
|
async def creer_reserve(req: ReserveCreate, membre=Depends(_require_tresorier), db: AsyncSession = Depends(get_db)):
|
|
if not req.nom.strip():
|
|
raise HTTPException(status_code=400, detail="Nom requis")
|
|
if req.montant < 0:
|
|
raise HTTPException(status_code=400, detail="Le montant doit être positif ou nul")
|
|
total = await _total_reserves(membre.groupe_id, db) + req.montant
|
|
solde_banque = await _solde_banque_brut(membre.groupe_id, db)
|
|
if total > solde_banque:
|
|
raise HTTPException(status_code=400, detail="Le total des réserves ne peut pas dépasser le solde en banque")
|
|
reserve = Reserve(groupe_id=membre.groupe_id, nom=req.nom.strip(), montant=req.montant, note=req.note)
|
|
db.add(reserve)
|
|
await db.flush()
|
|
return ReserveOut(id=reserve.id, nom=reserve.nom, montant=float(reserve.montant), note=reserve.note, active=reserve.active, cree_le=reserve.cree_le)
|
|
|
|
|
|
@router.patch("/reserves/{reserve_id}", response_model=ReserveOut)
|
|
async def modifier_reserve(reserve_id: str, req: ReserveUpdate, membre=Depends(_require_tresorier), db: AsyncSession = Depends(get_db)):
|
|
reserve = await db.get(Reserve, reserve_id)
|
|
if not reserve or reserve.groupe_id != membre.groupe_id or not reserve.active:
|
|
raise HTTPException(status_code=404, detail="Réserve introuvable")
|
|
montant = float(reserve.montant) if req.montant is None else req.montant
|
|
if montant < 0:
|
|
raise HTTPException(status_code=400, detail="Le montant doit être positif ou nul")
|
|
total = await _total_reserves(membre.groupe_id, db, reserve_id) + montant
|
|
solde_banque = await _solde_banque_brut(membre.groupe_id, db)
|
|
if total > solde_banque:
|
|
raise HTTPException(status_code=400, detail="Le total des réserves ne peut pas dépasser le solde en banque")
|
|
if req.nom is not None:
|
|
if not req.nom.strip():
|
|
raise HTTPException(status_code=400, detail="Nom requis")
|
|
reserve.nom = req.nom.strip()
|
|
reserve.montant = montant
|
|
if req.note is not None:
|
|
reserve.note = req.note.strip() or None
|
|
await db.flush()
|
|
return ReserveOut(id=reserve.id, nom=reserve.nom, montant=float(reserve.montant), note=reserve.note, active=reserve.active, cree_le=reserve.cree_le)
|
|
|
|
|
|
@router.delete("/reserves/{reserve_id}")
|
|
async def supprimer_reserve(reserve_id: str, membre=Depends(_require_tresorier), db: AsyncSession = Depends(get_db)):
|
|
reserve = await db.get(Reserve, reserve_id)
|
|
if not reserve or reserve.groupe_id != membre.groupe_id or not reserve.active:
|
|
raise HTTPException(status_code=404, detail="Réserve introuvable")
|
|
reserve.active = False
|
|
await db.flush()
|
|
return {"message": "Réserve supprimée"}
|
|
|
|
|
|
@router.get("/", response_model=list[OperationBancaireOut])
|
|
async def liste_operations(
|
|
limite: int = 20,
|
|
membre=Depends(get_current_membre),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Lister les opérations bancaires du groupe."""
|
|
result = await db.execute(
|
|
select(OperationBancaire)
|
|
.where(OperationBancaire.groupe_id == membre.groupe_id)
|
|
.order_by(desc(OperationBancaire.date_operation))
|
|
.limit(limite)
|
|
)
|
|
ops = result.scalars().all()
|
|
out = []
|
|
for op in ops:
|
|
m = await db.get(Membre, op.effectue_par)
|
|
out.append(OperationBancaireOut(
|
|
id=op.id,
|
|
type=op.type,
|
|
montant=float(op.montant),
|
|
date_operation=op.date_operation,
|
|
effectue_par_prenom=m.prenom if m else "?",
|
|
note=op.note,
|
|
cree_le=op.cree_le,
|
|
))
|
|
return out
|