2026-03-04 14:01:00 -05:00
|
|
|
"""Routes S1 — Collecte 7e Tradition."""
|
2026-04-17 04:04:04 -04:00
|
|
|
from datetime import date, datetime, time as dt_time, timezone
|
2026-03-04 14:01:00 -05:00
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
2026-04-17 04:04:04 -04:00
|
|
|
from sqlalchemy import select, desc, extract, func
|
2026-03-04 14:01:00 -05:00
|
|
|
|
|
|
|
|
from app.core.database import get_db
|
2026-04-17 04:04:04 -04:00
|
|
|
from app.core.security import get_current_membre, require_executif
|
2026-03-04 14:01:00 -05:00
|
|
|
from app.models.collecte import Collecte
|
2026-04-17 04:04:04 -04:00
|
|
|
from app.models.groupe import Groupe
|
2026-03-04 14:01:00 -05:00
|
|
|
from app.models.membre import Membre
|
2026-04-17 04:04:04 -04:00
|
|
|
from app.models.depense import Depense
|
|
|
|
|
from app.models.envoi_contribution import EnvoiContribution
|
|
|
|
|
from app.models.operation_bancaire import OperationBancaire
|
2026-05-15 16:41:17 -04:00
|
|
|
from app.models.reserve import Reserve
|
2026-03-04 14:01:00 -05:00
|
|
|
from app.models.affectation import Affectation
|
|
|
|
|
from app.models.poste import Poste
|
2026-04-17 04:04:04 -04:00
|
|
|
from app.schemas.phase1 import CollecteCreate, CollecteOut, CollecteUpdate, SoldesOut
|
2026-03-04 14:01:00 -05:00
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/collectes", tags=["S1 — Collecte 7e Tradition"])
|
|
|
|
|
|
2026-04-17 04:04:04 -04:00
|
|
|
JOURS_SEMAINE = {
|
|
|
|
|
"lundi": 0, "mardi": 1, "mercredi": 2, "jeudi": 3,
|
|
|
|
|
"vendredi": 4, "samedi": 5, "dimanche": 6,
|
|
|
|
|
}
|
2026-03-04 14:01:00 -05:00
|
|
|
|
2026-04-17 04:04:04 -04:00
|
|
|
|
|
|
|
|
async def _est_tresorier(membre_id, groupe_id, db: AsyncSession) -> bool:
|
|
|
|
|
"""Vérifie si un membre est trésorier ou exécutif."""
|
2026-03-04 14:01:00 -05:00
|
|
|
result = await db.execute(
|
|
|
|
|
select(Affectation).join(Poste).where(
|
2026-04-17 04:04:04 -04:00
|
|
|
Affectation.membre_id == membre_id,
|
2026-03-04 14:01:00 -05:00
|
|
|
Affectation.statut == "actif",
|
2026-04-17 04:04:04 -04:00
|
|
|
(Poste.nom.ilike("%trésorier%") | (Poste.categorie == "executif")),
|
2026-03-04 14:01:00 -05:00
|
|
|
)
|
|
|
|
|
)
|
2026-04-17 04:04:04 -04:00
|
|
|
return result.scalars().first() is not None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _require_tresorier(membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db)):
|
|
|
|
|
if not await _est_tresorier(membre.id, membre.groupe_id, db):
|
|
|
|
|
raise HTTPException(status_code=403, detail="Réservé au trésorier ou à l'exécutif")
|
2026-03-04 14:01:00 -05:00
|
|
|
return membre
|
|
|
|
|
|
|
|
|
|
|
2026-04-17 04:04:04 -04:00
|
|
|
async def _to_out(c: Collecte, db: AsyncSession) -> CollecteOut:
|
|
|
|
|
detenteur = await db.get(Membre, c.detenteur_id) if c.detenteur_id else None
|
|
|
|
|
saisi = await db.get(Membre, c.saisi_par) if c.saisi_par else None
|
|
|
|
|
confirme = await db.get(Membre, c.confirme_par) if c.confirme_par else None
|
|
|
|
|
recu = await db.get(Membre, c.recu_par) if c.recu_par else None
|
|
|
|
|
return CollecteOut(
|
|
|
|
|
id=c.id,
|
|
|
|
|
date_collecte=c.date_collecte,
|
|
|
|
|
montant=float(c.montant),
|
|
|
|
|
detenteur_prenom=detenteur.prenom if detenteur else None,
|
|
|
|
|
statut=c.statut,
|
|
|
|
|
saisi_par_prenom=saisi.prenom if saisi else None,
|
|
|
|
|
confirme_par_prenom=confirme.prenom if confirme else None,
|
|
|
|
|
date_confirmation=c.date_confirmation,
|
|
|
|
|
recu_par_prenom=recu.prenom if recu else None,
|
|
|
|
|
date_reception=c.date_reception,
|
|
|
|
|
cree_le=c.cree_le,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-03-04 14:01:00 -05:00
|
|
|
@router.post("/", response_model=CollecteOut)
|
|
|
|
|
async def saisir_collecte(
|
|
|
|
|
req: CollecteCreate,
|
2026-04-17 04:04:04 -04:00
|
|
|
membre=Depends(get_current_membre),
|
2026-03-04 14:01:00 -05:00
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
2026-04-17 04:04:04 -04:00
|
|
|
"""S1 — N'importe quel membre saisit la collecte. Rétroactif possible sur les dates de réunion."""
|
|
|
|
|
groupe = await db.get(Groupe, membre.groupe_id)
|
|
|
|
|
aujourdhui = date.today()
|
|
|
|
|
|
|
|
|
|
# Validation : pas dans le futur
|
|
|
|
|
if req.date_collecte > aujourdhui:
|
|
|
|
|
raise HTTPException(status_code=400, detail="La date ne peut pas être dans le futur")
|
|
|
|
|
|
|
|
|
|
# Validation : la date doit tomber un jour de réunion
|
|
|
|
|
if groupe.jour_reunion:
|
|
|
|
|
jour_attendu = JOURS_SEMAINE.get(groupe.jour_reunion.lower())
|
|
|
|
|
if jour_attendu is not None and req.date_collecte.weekday() != jour_attendu:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=400,
|
|
|
|
|
detail=f"La collecte doit avoir lieu un {groupe.jour_reunion}",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Validation : si c'est aujourd'hui, pas avant l'heure de la réunion
|
|
|
|
|
if req.date_collecte == aujourdhui and groupe.heure_reunion:
|
|
|
|
|
now = datetime.now(timezone.utc)
|
|
|
|
|
if isinstance(groupe.heure_reunion, dt_time) and now.time() < groupe.heure_reunion:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=400,
|
|
|
|
|
detail=f"La saisie n'est possible qu'à partir de {groupe.heure_reunion.strftime('%H:%M')}",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Validation : une seule collecte par date de réunion
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Collecte).where(
|
|
|
|
|
Collecte.groupe_id == membre.groupe_id,
|
|
|
|
|
Collecte.date_collecte == req.date_collecte,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
if result.scalars().first():
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=400,
|
|
|
|
|
detail=f"Il y a déjà une collecte pour le {req.date_collecte}",
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-04 14:01:00 -05:00
|
|
|
collecte = Collecte(
|
|
|
|
|
groupe_id=membre.groupe_id,
|
|
|
|
|
date_collecte=req.date_collecte,
|
|
|
|
|
montant=req.montant,
|
|
|
|
|
reunion_id=req.reunion_id,
|
2026-04-17 04:04:04 -04:00
|
|
|
detenteur_id=req.detenteur_id,
|
|
|
|
|
statut="en_attente",
|
2026-03-04 14:01:00 -05:00
|
|
|
saisi_par=membre.id,
|
|
|
|
|
)
|
|
|
|
|
db.add(collecte)
|
|
|
|
|
await db.flush()
|
2026-04-17 04:04:04 -04:00
|
|
|
return await _to_out(collecte, db)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/{collecte_id}", response_model=CollecteOut)
|
|
|
|
|
async def ajuster_collecte(
|
|
|
|
|
collecte_id: str,
|
|
|
|
|
req: CollecteUpdate,
|
|
|
|
|
membre=Depends(get_current_membre),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
"""Ajuster le montant (exécutif ou celui qui a saisi, avant confirmation)."""
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Collecte).where(
|
|
|
|
|
Collecte.id == collecte_id,
|
|
|
|
|
Collecte.groupe_id == membre.groupe_id,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
collecte = result.scalar_one_or_none()
|
|
|
|
|
if not collecte:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Collecte introuvable")
|
|
|
|
|
if collecte.statut != "en_attente":
|
|
|
|
|
raise HTTPException(status_code=400, detail="Le montant ne peut plus être modifié après confirmation")
|
|
|
|
|
|
|
|
|
|
# Vérifier : exécutif ou saisi_par
|
|
|
|
|
est_exec = await _est_tresorier(membre.id, membre.groupe_id, db)
|
|
|
|
|
if collecte.saisi_par != membre.id and not est_exec:
|
|
|
|
|
raise HTTPException(status_code=403, detail="Seul un exécutif ou la personne qui a saisi peut modifier")
|
|
|
|
|
|
|
|
|
|
if req.montant is not None:
|
|
|
|
|
collecte.montant = req.montant
|
|
|
|
|
await db.flush()
|
|
|
|
|
return await _to_out(collecte, db)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/{collecte_id}/confirmer", response_model=CollecteOut)
|
|
|
|
|
async def confirmer_collecte(
|
|
|
|
|
collecte_id: str,
|
|
|
|
|
membre=Depends(get_current_membre),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
"""Un second membre confirme le montant. Si le trésorier est impliqué → réception auto."""
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Collecte).where(
|
|
|
|
|
Collecte.id == collecte_id,
|
|
|
|
|
Collecte.groupe_id == membre.groupe_id,
|
|
|
|
|
)
|
2026-03-04 14:01:00 -05:00
|
|
|
)
|
2026-04-17 04:04:04 -04:00
|
|
|
collecte = result.scalar_one_or_none()
|
|
|
|
|
if not collecte:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Collecte introuvable")
|
|
|
|
|
if collecte.statut != "en_attente":
|
|
|
|
|
raise HTTPException(status_code=400, detail="Cette collecte a déjà été confirmée")
|
|
|
|
|
if collecte.saisi_par == membre.id:
|
|
|
|
|
raise HTTPException(status_code=400, detail="Le montant doit être confirmé par un autre membre")
|
|
|
|
|
|
|
|
|
|
collecte.confirme_par = membre.id
|
|
|
|
|
collecte.date_confirmation = datetime.now(timezone.utc)
|
|
|
|
|
|
|
|
|
|
# Si le trésorier est celui qui confirme OU celui qui a saisi → réception auto
|
|
|
|
|
confirmateur_est_tres = await _est_tresorier(membre.id, membre.groupe_id, db)
|
|
|
|
|
saisisseur_est_tres = await _est_tresorier(collecte.saisi_par, collecte.groupe_id, db)
|
|
|
|
|
|
|
|
|
|
if confirmateur_est_tres or saisisseur_est_tres:
|
|
|
|
|
tresorier_id = membre.id if confirmateur_est_tres else collecte.saisi_par
|
|
|
|
|
collecte.statut = "recu"
|
|
|
|
|
collecte.recu_par = tresorier_id
|
|
|
|
|
collecte.date_reception = date.today()
|
|
|
|
|
else:
|
|
|
|
|
collecte.statut = "confirmee"
|
|
|
|
|
|
|
|
|
|
await db.flush()
|
|
|
|
|
return await _to_out(collecte, db)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/{collecte_id}/reception", response_model=CollecteOut)
|
|
|
|
|
async def accuser_reception(
|
|
|
|
|
collecte_id: str,
|
|
|
|
|
membre=Depends(_require_tresorier),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
"""Le trésorier accuse réception (pour les cas sans trésorier impliqué)."""
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Collecte).where(
|
|
|
|
|
Collecte.id == collecte_id,
|
|
|
|
|
Collecte.groupe_id == membre.groupe_id,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
collecte = result.scalar_one_or_none()
|
|
|
|
|
if not collecte:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Collecte introuvable")
|
|
|
|
|
if collecte.statut == "recu":
|
|
|
|
|
raise HTTPException(status_code=400, detail="Cette collecte a déjà été reçue")
|
|
|
|
|
|
|
|
|
|
# Si en_attente, confirme aussi
|
|
|
|
|
if collecte.statut == "en_attente":
|
|
|
|
|
collecte.confirme_par = membre.id
|
|
|
|
|
collecte.date_confirmation = datetime.now(timezone.utc)
|
|
|
|
|
|
|
|
|
|
collecte.statut = "recu"
|
|
|
|
|
collecte.recu_par = membre.id
|
|
|
|
|
collecte.date_reception = date.today()
|
|
|
|
|
await db.flush()
|
|
|
|
|
return await _to_out(collecte, db)
|
2026-03-04 14:01:00 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/", response_model=list[CollecteOut])
|
|
|
|
|
async def liste_collectes(
|
2026-04-17 04:04:04 -04:00
|
|
|
statut: str | None = None,
|
|
|
|
|
mois: int | None = None,
|
|
|
|
|
annee: int | None = None,
|
2026-03-04 14:01:00 -05:00
|
|
|
limite: int = 20,
|
|
|
|
|
membre=Depends(get_current_membre),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
2026-04-17 04:04:04 -04:00
|
|
|
query = select(Collecte).where(Collecte.groupe_id == membre.groupe_id)
|
|
|
|
|
if statut:
|
|
|
|
|
query = query.where(Collecte.statut == statut)
|
|
|
|
|
if mois and annee:
|
|
|
|
|
query = query.where(
|
|
|
|
|
extract("month", Collecte.date_collecte) == mois,
|
|
|
|
|
extract("year", Collecte.date_collecte) == annee,
|
|
|
|
|
)
|
|
|
|
|
elif annee:
|
|
|
|
|
query = query.where(extract("year", Collecte.date_collecte) == annee)
|
|
|
|
|
query = query.order_by(desc(Collecte.date_collecte)).limit(limite)
|
|
|
|
|
result = await db.execute(query)
|
|
|
|
|
return [await _to_out(c, db) for c in result.scalars().all()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/soldes")
|
|
|
|
|
async def soldes_tresorerie(
|
|
|
|
|
membre=Depends(get_current_membre),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
"""Soldes détaillés : relevé bancaire, sommes engagées, réserves, disponible."""
|
|
|
|
|
from app.models.vente_litterature import VenteLitterature
|
|
|
|
|
|
|
|
|
|
gid = membre.groupe_id
|
|
|
|
|
|
|
|
|
|
# ── Dernier relevé bancaire ──
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(OperationBancaire).where(
|
|
|
|
|
OperationBancaire.groupe_id == gid,
|
|
|
|
|
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
|
|
|
|
|
date_releve = str(dernier_releve.date_operation) if dernier_releve else None
|
|
|
|
|
|
|
|
|
|
# ── Dépôts et retraits depuis le dernier relevé ──
|
|
|
|
|
filtre_depuis = dernier_releve.date_operation if dernier_releve else None
|
|
|
|
|
|
|
|
|
|
# Dépôts après relevé
|
|
|
|
|
q = select(func.coalesce(func.sum(OperationBancaire.montant), 0)).where(
|
|
|
|
|
OperationBancaire.groupe_id == gid,
|
|
|
|
|
OperationBancaire.type == "depot",
|
|
|
|
|
)
|
|
|
|
|
if filtre_depuis:
|
|
|
|
|
q = q.where(OperationBancaire.date_operation > filtre_depuis)
|
|
|
|
|
result = await db.execute(q)
|
|
|
|
|
depots_depuis = float(result.scalar())
|
|
|
|
|
|
|
|
|
|
# Retraits après relevé
|
|
|
|
|
q = select(func.coalesce(func.sum(OperationBancaire.montant), 0)).where(
|
|
|
|
|
OperationBancaire.groupe_id == gid,
|
|
|
|
|
OperationBancaire.type == "retrait",
|
|
|
|
|
)
|
|
|
|
|
if filtre_depuis:
|
|
|
|
|
q = q.where(OperationBancaire.date_operation > filtre_depuis)
|
|
|
|
|
result = await db.execute(q)
|
|
|
|
|
retraits_depuis = float(result.scalar())
|
|
|
|
|
|
2026-05-15 16:41:17 -04:00
|
|
|
# Contributions débitées après relevé
|
|
|
|
|
q = select(func.coalesce(func.sum(EnvoiContribution.montant), 0)).where(
|
|
|
|
|
EnvoiContribution.groupe_id == gid,
|
|
|
|
|
EnvoiContribution.debitee == True,
|
|
|
|
|
)
|
|
|
|
|
if filtre_depuis:
|
|
|
|
|
q = q.where(EnvoiContribution.date_envoi > filtre_depuis)
|
|
|
|
|
result = await db.execute(q)
|
|
|
|
|
contributions_debitees_depuis = float(result.scalar())
|
|
|
|
|
|
|
|
|
|
# Dépenses bancaires débitées après relevé
|
|
|
|
|
q = select(func.coalesce(func.sum(Depense.montant), 0)).where(
|
|
|
|
|
Depense.groupe_id == gid,
|
|
|
|
|
Depense.paye == True,
|
|
|
|
|
Depense.methode.in_(["VIREMENT", "CHEQUE"]),
|
|
|
|
|
)
|
|
|
|
|
if filtre_depuis:
|
|
|
|
|
q = q.where(Depense.date_confirmation > filtre_depuis)
|
|
|
|
|
result = await db.execute(q)
|
|
|
|
|
depenses_bancaires_depuis = float(result.scalar())
|
|
|
|
|
|
|
|
|
|
solde_banque = solde_releve + depots_depuis - retraits_depuis - contributions_debitees_depuis - depenses_bancaires_depuis
|
2026-04-17 04:04:04 -04:00
|
|
|
|
|
|
|
|
# ── Dépenses engagées non payées (chèque/virement votés mais pas encore débités) ──
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(func.coalesce(func.sum(Depense.montant), 0)).where(
|
|
|
|
|
Depense.groupe_id == gid,
|
|
|
|
|
Depense.paye == False,
|
|
|
|
|
Depense.methode.in_(["VIREMENT", "CHEQUE"]),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
engagements_non_debites = float(result.scalar())
|
|
|
|
|
|
2026-05-15 16:41:17 -04:00
|
|
|
# ── Contributions engagées non débitées ──
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(func.coalesce(func.sum(EnvoiContribution.montant), 0)).where(
|
|
|
|
|
EnvoiContribution.groupe_id == gid,
|
|
|
|
|
EnvoiContribution.engagee == True,
|
|
|
|
|
EnvoiContribution.debitee == False,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
contributions_non_debitees = float(result.scalar())
|
|
|
|
|
engagements_non_debites += contributions_non_debitees
|
2026-04-17 04:04:04 -04:00
|
|
|
|
|
|
|
|
# ── Réserves ──
|
2026-05-15 16:41:17 -04:00
|
|
|
result = await db.execute(
|
|
|
|
|
select(Reserve).where(
|
|
|
|
|
Reserve.groupe_id == gid,
|
|
|
|
|
Reserve.active == True,
|
|
|
|
|
).order_by(Reserve.nom)
|
|
|
|
|
)
|
|
|
|
|
reserves = result.scalars().all()
|
|
|
|
|
reserves_details = [
|
|
|
|
|
{
|
|
|
|
|
"id": str(r.id),
|
|
|
|
|
"nom": r.nom,
|
|
|
|
|
"montant": float(r.montant),
|
|
|
|
|
"note": r.note,
|
|
|
|
|
}
|
|
|
|
|
for r in reserves
|
|
|
|
|
]
|
|
|
|
|
total_reserves = sum(r["montant"] for r in reserves_details)
|
2026-04-17 04:04:04 -04:00
|
|
|
|
|
|
|
|
# ── Somme disponible ──
|
|
|
|
|
disponible = solde_banque - engagements_non_debites - total_reserves
|
|
|
|
|
|
|
|
|
|
# ── Encaisse (cash reçu par le trésorier, pas déposé) ──
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(func.coalesce(func.sum(Collecte.montant), 0)).where(
|
|
|
|
|
Collecte.groupe_id == gid, Collecte.statut == "recu"))
|
|
|
|
|
total_collectes_recues = float(result.scalar())
|
|
|
|
|
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(func.coalesce(func.sum(OperationBancaire.montant), 0)).where(
|
|
|
|
|
OperationBancaire.groupe_id == gid, OperationBancaire.type == "depot"))
|
|
|
|
|
total_depots = float(result.scalar())
|
|
|
|
|
|
2026-05-15 16:41:17 -04:00
|
|
|
result = await db.execute(
|
|
|
|
|
select(func.coalesce(func.sum(OperationBancaire.montant), 0)).where(
|
|
|
|
|
OperationBancaire.groupe_id == gid, OperationBancaire.type == "retrait"))
|
|
|
|
|
total_retraits = float(result.scalar())
|
|
|
|
|
|
2026-04-17 04:04:04 -04:00
|
|
|
result = await db.execute(
|
|
|
|
|
select(func.coalesce(func.sum(Depense.montant), 0)).where(
|
|
|
|
|
Depense.groupe_id == gid, Depense.paye == True, Depense.methode == "COLLECTE"))
|
|
|
|
|
depenses_cash = float(result.scalar())
|
|
|
|
|
|
|
|
|
|
# Ventes de littérature remises au trésorier
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(func.coalesce(func.sum(VenteLitterature.montant), 0)).where(
|
|
|
|
|
VenteLitterature.groupe_id == gid, VenteLitterature.remis_tresorier == True))
|
|
|
|
|
ventes_recues = float(result.scalar())
|
|
|
|
|
|
2026-05-15 16:41:17 -04:00
|
|
|
encaisse = total_collectes_recues + ventes_recues + total_retraits - total_depots - depenses_cash
|
2026-04-17 04:04:04 -04:00
|
|
|
|
|
|
|
|
# ── Argent en circulation (collectes non remises au trésorier) ──
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Collecte).where(
|
|
|
|
|
Collecte.groupe_id == gid, Collecte.statut.in_(["en_attente", "confirmee"])))
|
|
|
|
|
en_circulation = result.scalars().all()
|
|
|
|
|
details_circulation = []
|
|
|
|
|
total_en_circulation = 0.0
|
|
|
|
|
for c in en_circulation:
|
|
|
|
|
det = await db.get(Membre, c.detenteur_id) if c.detenteur_id else None
|
|
|
|
|
total_en_circulation += float(c.montant)
|
|
|
|
|
details_circulation.append({
|
|
|
|
|
"date": str(c.date_collecte),
|
|
|
|
|
"montant": float(c.montant),
|
|
|
|
|
"detenteur": det.prenom if det else "?",
|
|
|
|
|
"statut": c.statut,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# ── Ventes de littérature non remises ──
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(VenteLitterature).where(
|
|
|
|
|
VenteLitterature.groupe_id == gid,
|
|
|
|
|
VenteLitterature.remis_tresorier == False,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
from app.models.litterature import Litterature
|
|
|
|
|
ventes_non_remises = result.scalars().all()
|
|
|
|
|
details_ventes = []
|
|
|
|
|
total_ventes_non_remises = 0.0
|
|
|
|
|
for v in ventes_non_remises:
|
|
|
|
|
vendeur = await db.get(Membre, v.vendu_par) if v.vendu_par else None
|
|
|
|
|
item = await db.get(Litterature, v.litterature_id) if v.litterature_id else None
|
|
|
|
|
total_ventes_non_remises += float(v.montant)
|
|
|
|
|
details_ventes.append({
|
|
|
|
|
"id": str(v.id),
|
|
|
|
|
"date": str(v.date_vente),
|
|
|
|
|
"montant": float(v.montant),
|
|
|
|
|
"titre": item.titre if item else "?",
|
|
|
|
|
"vendu_par": vendeur.prenom if vendeur else "?",
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"solde_banque": round(solde_banque, 2),
|
|
|
|
|
"date_releve": date_releve,
|
2026-05-15 16:41:17 -04:00
|
|
|
"encaisse": round(encaisse, 2),
|
2026-04-17 04:04:04 -04:00
|
|
|
"disponible": round(disponible, 2),
|
|
|
|
|
"engagements_non_debites": round(engagements_non_debites, 2),
|
2026-05-15 16:41:17 -04:00
|
|
|
"reserve_prudente": 0,
|
|
|
|
|
"reserve_anniversaire": 0,
|
2026-04-17 04:04:04 -04:00
|
|
|
"total_reserves": round(total_reserves, 2),
|
2026-05-15 16:41:17 -04:00
|
|
|
"reserves_details": reserves_details,
|
2026-04-17 04:04:04 -04:00
|
|
|
"en_possession": round(total_en_circulation, 2),
|
|
|
|
|
"en_possession_details": details_circulation,
|
|
|
|
|
"ventes_non_remises": round(total_ventes_non_remises, 2),
|
|
|
|
|
"ventes_non_remises_details": details_ventes,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/registre")
|
|
|
|
|
async def registre_transactions(
|
|
|
|
|
mois: int | None = None,
|
|
|
|
|
annee: int | None = None,
|
2026-05-15 16:41:17 -04:00
|
|
|
type_transaction: str | None = None,
|
2026-04-17 04:04:04 -04:00
|
|
|
membre=Depends(get_current_membre),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
"""Registre détaillé de toutes les transactions financières du groupe."""
|
|
|
|
|
from app.models.vente_litterature import VenteLitterature
|
|
|
|
|
from app.models.litterature import Litterature
|
|
|
|
|
|
|
|
|
|
gid = membre.groupe_id
|
|
|
|
|
transactions = []
|
|
|
|
|
|
|
|
|
|
# Filtres date
|
|
|
|
|
def filtre_mois(d):
|
|
|
|
|
if mois and annee:
|
|
|
|
|
return d.month == mois and d.year == annee
|
|
|
|
|
if annee:
|
|
|
|
|
return d.year == annee
|
|
|
|
|
return True
|
|
|
|
|
|
2026-05-15 16:41:17 -04:00
|
|
|
def ajouter_transaction(transaction):
|
|
|
|
|
if type_transaction and transaction["type"] != type_transaction:
|
|
|
|
|
return
|
|
|
|
|
transactions.append(transaction)
|
|
|
|
|
|
2026-04-17 04:04:04 -04:00
|
|
|
# 1. Collectes reçues
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Collecte).where(Collecte.groupe_id == gid, Collecte.statut == "recu")
|
|
|
|
|
.order_by(Collecte.date_collecte)
|
|
|
|
|
)
|
|
|
|
|
for c in result.scalars().all():
|
|
|
|
|
if filtre_mois(c.date_collecte):
|
|
|
|
|
saisi = await db.get(Membre, c.saisi_par) if c.saisi_par else None
|
2026-05-15 16:41:17 -04:00
|
|
|
ajouter_transaction({
|
2026-04-17 04:04:04 -04:00
|
|
|
"date": str(c.date_collecte),
|
|
|
|
|
"type": "collecte",
|
|
|
|
|
"description": f"Collecte 7e Tradition",
|
|
|
|
|
"entree": float(c.montant),
|
|
|
|
|
"sortie": 0,
|
|
|
|
|
"par": saisi.prenom if saisi else "?",
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# 2. Ventes de littérature remises
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(VenteLitterature).where(
|
|
|
|
|
VenteLitterature.groupe_id == gid,
|
|
|
|
|
VenteLitterature.remis_tresorier == True,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
for v in result.scalars().all():
|
|
|
|
|
if filtre_mois(v.date_vente):
|
|
|
|
|
item = await db.get(Litterature, v.litterature_id)
|
2026-05-15 16:41:17 -04:00
|
|
|
ajouter_transaction({
|
2026-04-17 04:04:04 -04:00
|
|
|
"date": str(v.date_vente),
|
|
|
|
|
"type": "vente_litterature",
|
|
|
|
|
"description": f"Vente : {item.titre if item else '?'} (x{v.quantite})",
|
|
|
|
|
"entree": float(v.montant),
|
|
|
|
|
"sortie": 0,
|
|
|
|
|
"par": "Littérature",
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# 3. Dépenses payées
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Depense).where(Depense.groupe_id == gid, Depense.paye == True)
|
|
|
|
|
.order_by(Depense.date_depense)
|
|
|
|
|
)
|
|
|
|
|
for d in result.scalars().all():
|
|
|
|
|
if filtre_mois(d.date_depense):
|
|
|
|
|
qui = await db.get(Membre, d.qui)
|
2026-05-15 16:41:17 -04:00
|
|
|
ajouter_transaction({
|
2026-04-17 04:04:04 -04:00
|
|
|
"date": str(d.date_depense),
|
|
|
|
|
"type": "depense",
|
|
|
|
|
"description": f"{d.quoi} ({d.methode})",
|
|
|
|
|
"entree": 0,
|
|
|
|
|
"sortie": float(d.montant),
|
|
|
|
|
"par": qui.prenom if qui else "?",
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# 4. Envois de contributions
|
|
|
|
|
result = await db.execute(
|
2026-05-15 16:41:17 -04:00
|
|
|
select(EnvoiContribution).where(
|
|
|
|
|
EnvoiContribution.groupe_id == gid,
|
|
|
|
|
EnvoiContribution.debitee == True,
|
|
|
|
|
)
|
2026-04-17 04:04:04 -04:00
|
|
|
.order_by(EnvoiContribution.date_envoi)
|
|
|
|
|
)
|
|
|
|
|
for e in result.scalars().all():
|
|
|
|
|
if filtre_mois(e.date_envoi):
|
2026-05-15 16:41:17 -04:00
|
|
|
ajouter_transaction({
|
2026-04-17 04:04:04 -04:00
|
|
|
"date": str(e.date_envoi),
|
|
|
|
|
"type": "contribution",
|
|
|
|
|
"description": f"Contribution {e.destination} ({e.methode})",
|
|
|
|
|
"entree": 0,
|
|
|
|
|
"sortie": float(e.montant),
|
|
|
|
|
"par": "Trésorier",
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# 5. Opérations bancaires (dépôts/retraits)
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(OperationBancaire).where(
|
|
|
|
|
OperationBancaire.groupe_id == gid,
|
|
|
|
|
OperationBancaire.type.in_(["depot", "retrait"]),
|
|
|
|
|
).order_by(OperationBancaire.date_operation)
|
|
|
|
|
)
|
|
|
|
|
for op in result.scalars().all():
|
|
|
|
|
if filtre_mois(op.date_operation):
|
|
|
|
|
m = await db.get(Membre, op.effectue_par)
|
|
|
|
|
is_depot = op.type == "depot"
|
2026-05-15 16:41:17 -04:00
|
|
|
ajouter_transaction({
|
2026-04-17 04:04:04 -04:00
|
|
|
"date": str(op.date_operation),
|
|
|
|
|
"type": f"banque_{op.type}",
|
|
|
|
|
"description": f"{'Dépôt' if is_depot else 'Retrait'} bancaire{(' — ' + op.note) if op.note else ''}",
|
|
|
|
|
"entree": float(op.montant) if is_depot else 0,
|
|
|
|
|
"sortie": float(op.montant) if not is_depot else 0,
|
|
|
|
|
"par": m.prenom if m else "?",
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
transactions.sort(key=lambda t: t["date"])
|
|
|
|
|
return transactions
|