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

616 lines
24 KiB
Python
Raw Normal View History

2026-03-04 14:01:00 -05:00
"""Routes S1 — Collecte 7e Tradition."""
2026-05-25 17:39:38 -04:00
from datetime import date, datetime, time as dt_time, timedelta, timezone
from fastapi import APIRouter, Depends, Header, HTTPException
2026-03-04 14:01:00 -05:00
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-05-25 17:39:38 -04:00
from app.core.roles import membre_a_poste_ou_executif, require_tresorier
from app.core.reunions import JOURS_SEMAINE
from app.core.security import get_current_membre, require_executif, is_sysadmin
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
from app.models.reserve import Reserve
2026-05-25 17:39:38 -04:00
from app.models.mouvement_reserve import MouvementReserve
2026-04-17 04:04:04 -04:00
from app.schemas.phase1 import CollecteCreate, CollecteOut, CollecteUpdate, SoldesOut
2026-05-25 17:39:38 -04:00
from app.services.finance import encaisse, solde_banque, solde_reserve, transaction
from app.services.historique import enregistrer_historique, verifier_version_historisee
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
async def _est_tresorier(membre_id, groupe_id, db: AsyncSession) -> bool:
"""Vérifie si un membre est trésorier ou exécutif."""
2026-05-25 17:39:38 -04:00
return await membre_a_poste_ou_executif(membre_id, "trésorier", db)
2026-03-04 14:01:00 -05:00
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,
modifie_le=c.modifie_le,
2026-04-17 04:04:04 -04:00
)
2026-03-04 14:01:00 -05:00
@router.post("/", response_model=CollecteOut)
async def saisir_collecte(
req: CollecteCreate,
2026-05-25 17:39:38 -04:00
membre=Depends(require_executif),
2026-03-04 14:01:00 -05:00
db: AsyncSession = Depends(get_db),
):
2026-05-25 17:39:38 -04:00
"""S1 — Un membre exécutif saisit la collecte."""
2026-04-17 04:04:04 -04:00
groupe = await db.get(Groupe, membre.groupe_id)
aujourdhui = date.today()
2026-05-25 17:39:38 -04:00
retro_sysadmin = is_sysadmin(membre)
2026-04-17 04:04:04 -04:00
# Validation : pas dans le futur
2026-05-25 17:39:38 -04:00
if req.date_collecte > aujourdhui and not retro_sysadmin:
2026-04-17 04:04:04 -04:00
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())
2026-05-25 17:39:38 -04:00
if jour_attendu is not None and req.date_collecte.weekday() != jour_attendu and not retro_sysadmin:
2026-04-17 04:04:04 -04:00
raise HTTPException(
status_code=400,
detail=f"La collecte doit avoir lieu un {groupe.jour_reunion}",
)
2026-05-25 17:39:38 -04:00
if jour_attendu is not None and not retro_sysadmin:
dernieres_reunions = []
d = aujourdhui
while len(dernieres_reunions) < 52:
if d.weekday() == jour_attendu:
dernieres_reunions.append(d)
d -= timedelta(days=1)
if req.date_collecte not in dernieres_reunions:
raise HTTPException(
status_code=400,
detail="La date doit faire partie des 52 dernières rencontres du groupe",
)
2026-04-17 04:04:04 -04:00
# Validation : si c'est aujourd'hui, pas avant l'heure de la réunion
2026-05-25 17:39:38 -04:00
if req.date_collecte == aujourdhui and groupe.heure_reunion and not retro_sysadmin:
2026-04-17 04:04:04 -04:00
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,
version_attendue: str | None = Header(default=None, alias="X-Resource-Version"),
2026-05-25 17:39:38 -04:00
membre=Depends(require_executif),
2026-04-17 04:04:04 -04:00
db: AsyncSession = Depends(get_db),
):
2026-05-25 17:39:38 -04:00
"""Ajuster le montant avant confirmation."""
2026-04-17 04:04:04 -04:00
result = await db.execute(
select(Collecte).where(
Collecte.id == collecte_id,
Collecte.groupe_id == membre.groupe_id,
).with_for_update()
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")
await verifier_version_historisee(db, collecte, version_attendue, table_cible="collectes")
2026-04-17 04:04:04 -04:00
if collecte.statut != "en_attente":
raise HTTPException(status_code=400, detail="Le montant ne peut plus être modifié après confirmation")
if req.montant is not None:
collecte.montant = req.montant
await enregistrer_historique(
db,
groupe_id=membre.groupe_id,
table_cible="collectes",
objet_id=collecte.id,
action="collecte.modifiee",
raison="Montant de collecte ajusté avant confirmation.",
membre_id=membre.id,
)
2026-04-17 04:04:04 -04:00
await db.flush()
return await _to_out(collecte, db)
@router.post("/{collecte_id}/confirmer", response_model=CollecteOut)
async def confirmer_collecte(
collecte_id: str,
version_attendue: str | None = Header(default=None, alias="X-Resource-Version"),
2026-05-25 17:39:38 -04:00
membre=Depends(require_executif),
2026-04-17 04:04:04 -04:00
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,
).with_for_update()
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")
await verifier_version_historisee(db, collecte, version_attendue, table_cible="collectes")
2026-04-17 04:04:04 -04:00
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()
2026-05-25 17:39:38 -04:00
db.add(transaction(collecte.groupe_id, tresorier_id, collecte.date_collecte, "collecte", "encaisse", collecte.montant, "debit", "collectes", collecte.id, "Collecte reçue"))
2026-04-17 04:04:04 -04:00
else:
collecte.statut = "confirmee"
await enregistrer_historique(
db,
groupe_id=membre.groupe_id,
table_cible="collectes",
objet_id=collecte.id,
action="collecte.confirmee" if collecte.statut == "confirmee" else "collecte.recue",
raison="Collecte confirmée." if collecte.statut == "confirmee" else "Collecte confirmée et reçue par le trésorier.",
membre_id=membre.id,
)
2026-04-17 04:04:04 -04:00
await db.flush()
return await _to_out(collecte, db)
@router.post("/{collecte_id}/reception", response_model=CollecteOut)
async def accuser_reception(
collecte_id: str,
version_attendue: str | None = Header(default=None, alias="X-Resource-Version"),
2026-05-25 17:39:38 -04:00
membre=Depends(require_tresorier),
2026-04-17 04:04:04 -04:00
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,
).with_for_update()
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")
await verifier_version_historisee(db, collecte, version_attendue, table_cible="collectes")
2026-04-17 04:04:04 -04:00
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()
2026-05-25 17:39:38 -04:00
db.add(transaction(collecte.groupe_id, membre.id, collecte.date_collecte, "collecte", "encaisse", collecte.montant, "debit", "collectes", collecte.id, "Collecte reçue"))
await enregistrer_historique(
db,
groupe_id=membre.groupe_id,
table_cible="collectes",
objet_id=collecte.id,
action="collecte.recue",
raison="Collecte reçue par le trésorier.",
membre_id=membre.id,
)
2026-04-17 04:04:04 -04:00
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,
2026-05-25 17:39:38 -04:00
membre=Depends(require_executif),
2026-03-04 14:01:00 -05:00
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
from app.models.vente_jeton import VenteJeton
2026-04-17 04:04:04 -04:00
gid = membre.groupe_id
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()
date_releve = str(dernier_releve.date_operation) if dernier_releve else None
2026-05-25 17:39:38 -04:00
solde_banque_actuel = await solde_banque(gid, db)
encaisse_actuelle = await encaisse(gid, db)
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())
# ── 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
result = await db.execute(
select(Reserve).where(
Reserve.groupe_id == gid,
Reserve.active == True,
).order_by(Reserve.nom)
)
reserves = result.scalars().all()
2026-05-25 17:39:38 -04:00
reserves_details = []
for r in reserves:
montant = await solde_reserve(r.id, db)
r.montant = montant
reserves_details.append({
"id": str(r.id),
"nom": r.nom,
2026-05-25 17:39:38 -04:00
"montant": round(montant, 2),
"note": r.note,
"modifie_le": r.modifie_le.isoformat(),
2026-05-25 17:39:38 -04:00
})
total_reserves_reelles = sum(r["montant"] for r in reserves_details)
2026-04-17 04:04:04 -04:00
# ── Somme disponible ──
2026-05-25 17:39:38 -04:00
disponible = solde_banque_actuel + encaisse_actuelle - engagements_non_debites - total_reserves_reelles
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({
2026-05-25 17:39:38 -04:00
"id": str(c.id),
2026-04-17 04:04:04 -04:00
"date": str(c.date_collecte),
"montant": float(c.montant),
"detenteur": det.prenom if det else "?",
"statut": c.statut,
"modifie_le": c.modifie_le.isoformat(),
2026-04-17 04:04:04 -04:00
})
# ── 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 "?",
})
result = await db.execute(
select(VenteJeton).where(
VenteJeton.groupe_id == gid,
VenteJeton.remis_tresorier == False,
)
)
ventes_jetons_non_remises = result.scalars().all()
details_ventes_jetons = []
total_ventes_jetons_non_remises = 0.0
for v in ventes_jetons_non_remises:
vendeur = await db.get(Membre, v.vendu_par) if v.vendu_par else None
total_ventes_jetons_non_remises += float(v.montant)
details_ventes_jetons.append({
"id": str(v.id),
"date": str(v.date_vente),
"montant": float(v.montant),
"titre": f"Jetons {v.type_jeton} (x{v.quantite})",
"vendu_par": vendeur.prenom if vendeur else "?",
"type": "jeton",
})
2026-04-17 04:04:04 -04:00
return {
2026-05-25 17:39:38 -04:00
"solde_banque": round(solde_banque_actuel, 2),
2026-04-17 04:04:04 -04:00
"date_releve": date_releve,
2026-05-25 17:39:38 -04:00
"encaisse": round(encaisse_actuelle, 2),
2026-04-17 04:04:04 -04:00
"disponible": round(disponible, 2),
"engagements_non_debites": round(engagements_non_debites, 2),
"reserve_prudente": 0,
"reserve_anniversaire": 0,
2026-05-25 17:39:38 -04:00
"total_reserves": round(total_reserves_reelles, 2),
"total_reserves_reelles": round(total_reserves_reelles, 2),
"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 + total_ventes_jetons_non_remises, 2),
"ventes_non_remises_details": details_ventes + details_ventes_jetons,
2026-04-17 04:04:04 -04:00
}
@router.get("/registre")
async def registre_transactions(
mois: int | None = None,
annee: int | None = None,
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
2026-05-25 17:39:38 -04:00
from app.models.transaction_comptable import TransactionComptable
from app.models.vente_jeton import VenteJeton
2026-04-17 04:04:04 -04:00
gid = membre.groupe_id
transactions = []
2026-05-25 17:39:38 -04:00
dons_operation_ids = set()
2026-04-17 04:04:04 -04:00
# 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
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
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)
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",
})
result = await db.execute(
select(VenteJeton).where(
VenteJeton.groupe_id == gid,
VenteJeton.remis_tresorier == True,
)
)
for v in result.scalars().all():
if filtre_mois(v.date_vente):
vendeur = await db.get(Membre, v.vendu_par) if v.vendu_par else None
ajouter_transaction({
"date": str(v.date_vente),
"type": "vente_jeton",
"description": f"Vente de jetons : {v.type_jeton} (x{v.quantite})",
"entree": float(v.montant),
"sortie": 0,
"par": vendeur.prenom if vendeur else "Jetons",
})
2026-04-17 04:04:04 -04:00
# 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)
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(
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):
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",
})
2026-05-25 17:39:38 -04:00
# 5. Dons déposés directement à la banque
result = await db.execute(
select(TransactionComptable).where(
TransactionComptable.groupe_id == gid,
TransactionComptable.type == "don",
TransactionComptable.compte == "banque",
TransactionComptable.sens == "debit",
).order_by(TransactionComptable.date_transaction)
)
for don in result.scalars().all():
if filtre_mois(don.date_transaction):
m = await db.get(Membre, don.cree_par) if don.cree_par else None
if don.source_id:
dons_operation_ids.add(don.source_id)
ajouter_transaction({
"date": str(don.date_transaction),
"type": "don",
"description": f"Don déposé directement{(' — ' + don.note) if don.note else ''}",
"entree": float(don.montant),
"sortie": 0,
"par": m.prenom if m else "?",
})
# 6. Opérations bancaires (dépôts/retraits)
2026-04-17 04:04:04 -04:00
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():
2026-05-25 17:39:38 -04:00
if op.id in dons_operation_ids:
continue
2026-04-17 04:04:04 -04:00
if filtre_mois(op.date_operation):
m = await db.get(Membre, op.effectue_par)
is_depot = op.type == "depot"
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 "?",
})
2026-05-25 17:39:38 -04:00
# 7. Mouvements virtuels entre le disponible et les réserves
result = await db.execute(
select(MouvementReserve).where(MouvementReserve.groupe_id == gid)
.order_by(MouvementReserve.cree_le)
)
for mv in result.scalars().all():
date_mouvement = mv.cree_le.date()
if filtre_mois(date_mouvement):
source = await db.get(Reserve, mv.source_id) if mv.source_id else None
destination = await db.get(Reserve, mv.destination_id) if mv.destination_id else None
createur = await db.get(Membre, mv.cree_par) if mv.cree_par else None
source_nom = source.nom if source else "Disponible"
destination_nom = destination.nom if destination else "Disponible"
ajouter_transaction({
"date": str(date_mouvement),
"type": "reserve_virement",
"description": f"Virement de réserve : {source_nom} → {destination_nom} ({float(mv.montant):.2f} $)",
"entree": 0,
"sortie": 0,
"par": createur.prenom if createur else "?",
})
transactions.sort(key=lambda t: t["date"], reverse=True)
2026-04-17 04:04:04 -04:00
return transactions