640 lines
26 KiB
Python
640 lines
26 KiB
Python
"""Routes S1 — Collecte 7e Tradition."""
|
|
from datetime import date, datetime, time as dt_time, timedelta, timezone
|
|
from fastapi import APIRouter, Depends, Header, HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, desc, extract, func
|
|
|
|
from app.core.database import get_db
|
|
from app.core.ownership import ensure_owned_or_404
|
|
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
|
|
from app.models.collecte import Collecte
|
|
from app.models.groupe import Groupe
|
|
from app.models.membre import Membre
|
|
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
|
|
from app.models.mouvement_reserve import MouvementReserve
|
|
from app.models.reunion import Reunion
|
|
from app.schemas.phase1 import CollecteCreate, CollecteOut, CollecteUpdate, SoldesOut
|
|
from app.services.finance import encaisse, solde_banque, solde_reserve, transaction
|
|
from app.services.historique import enregistrer_historique, snapshot, verifier_version_historisee
|
|
from app.services.rapports_adoptes import verifier_reunion_non_cristallisee
|
|
|
|
router = APIRouter(prefix="/collectes", tags=["S1 — Collecte 7e Tradition"])
|
|
|
|
|
|
async def _est_tresorier(membre_id, groupe_id, db: AsyncSession) -> bool:
|
|
"""Vérifie si un membre est trésorier ou exécutif."""
|
|
return await membre_a_poste_ou_executif(membre_id, "trésorier", db)
|
|
|
|
|
|
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,
|
|
)
|
|
|
|
|
|
@router.post("/", response_model=CollecteOut)
|
|
async def saisir_collecte(
|
|
req: CollecteCreate,
|
|
membre=Depends(require_executif),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""S1 — Un membre exécutif saisit la collecte."""
|
|
groupe = await db.get(Groupe, membre.groupe_id)
|
|
aujourdhui = date.today()
|
|
retro_sysadmin = is_sysadmin(membre)
|
|
|
|
# Validation : pas dans le futur
|
|
if req.date_collecte > aujourdhui and not retro_sysadmin:
|
|
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 and not retro_sysadmin:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"La collecte doit avoir lieu un {groupe.jour_reunion}",
|
|
)
|
|
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",
|
|
)
|
|
|
|
# Validation : si c'est aujourd'hui, pas avant l'heure de la réunion
|
|
if req.date_collecte == aujourdhui and groupe.heure_reunion and not retro_sysadmin:
|
|
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
|
|
detenteur = await db.get(Membre, req.detenteur_id)
|
|
if not detenteur or detenteur.groupe_id != membre.groupe_id or not detenteur.actif:
|
|
raise HTTPException(status_code=404, detail="Détenteur introuvable ou inactif")
|
|
if req.reunion_id:
|
|
await ensure_owned_or_404(db, Reunion, req.reunion_id, membre.groupe_id, "Réunion introuvable")
|
|
await verifier_reunion_non_cristallisee(
|
|
db, membre.groupe_id, req.reunion_id, "rencontre", "Cette rencontre est clôturée"
|
|
)
|
|
|
|
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}",
|
|
)
|
|
|
|
collecte = Collecte(
|
|
groupe_id=membre.groupe_id,
|
|
date_collecte=req.date_collecte,
|
|
montant=req.montant,
|
|
reunion_id=req.reunion_id,
|
|
detenteur_id=req.detenteur_id,
|
|
statut="en_attente",
|
|
saisi_par=membre.id,
|
|
)
|
|
db.add(collecte)
|
|
await db.flush()
|
|
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"),
|
|
membre=Depends(require_executif),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Ajuster le montant avant confirmation."""
|
|
result = await db.execute(
|
|
select(Collecte).where(
|
|
Collecte.id == collecte_id,
|
|
Collecte.groupe_id == membre.groupe_id,
|
|
).with_for_update()
|
|
)
|
|
collecte = result.scalar_one_or_none()
|
|
if not collecte:
|
|
raise HTTPException(status_code=404, detail="Collecte introuvable")
|
|
if collecte.reunion_id:
|
|
await verifier_reunion_non_cristallisee(
|
|
db, membre.groupe_id, collecte.reunion_id, "rencontre", "Cette rencontre est clôturée"
|
|
)
|
|
await verifier_version_historisee(db, collecte, version_attendue, table_cible="collectes")
|
|
if collecte.statut != "en_attente":
|
|
raise HTTPException(status_code=400, detail="Le montant ne peut plus être modifié après confirmation")
|
|
|
|
avant = snapshot(collecte, "montant", "statut")
|
|
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,
|
|
avant=avant,
|
|
apres=snapshot(collecte, "montant", "statut"),
|
|
)
|
|
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"),
|
|
membre=Depends(require_executif),
|
|
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()
|
|
)
|
|
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")
|
|
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")
|
|
|
|
avant = snapshot(collecte, "statut", "confirme_par", "recu_par", "date_reception")
|
|
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()
|
|
db.add(transaction(collecte.groupe_id, tresorier_id, collecte.date_collecte, "collecte", "encaisse", collecte.montant, "debit", "collectes", collecte.id, "Collecte reçue"))
|
|
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,
|
|
avant=avant,
|
|
apres=snapshot(collecte, "statut", "confirme_par", "recu_par", "date_reception"),
|
|
)
|
|
|
|
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"),
|
|
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,
|
|
).with_for_update()
|
|
)
|
|
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")
|
|
if collecte.statut == "recu":
|
|
raise HTTPException(status_code=400, detail="Cette collecte a déjà été reçue")
|
|
|
|
avant = snapshot(collecte, "statut", "confirme_par", "recu_par", "date_reception")
|
|
# 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()
|
|
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,
|
|
avant=avant,
|
|
apres=snapshot(collecte, "statut", "confirme_par", "recu_par", "date_reception"),
|
|
)
|
|
await db.flush()
|
|
return await _to_out(collecte, db)
|
|
|
|
|
|
@router.get("/", response_model=list[CollecteOut])
|
|
async def liste_collectes(
|
|
statut: str | None = None,
|
|
mois: int | None = None,
|
|
annee: int | None = None,
|
|
limite: int = 20,
|
|
membre=Depends(require_executif),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
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
|
|
|
|
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
|
|
|
|
solde_banque_actuel = await solde_banque(gid, db)
|
|
encaisse_actuelle = await encaisse(gid, db)
|
|
|
|
# ── 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
|
|
|
|
result = await db.execute(
|
|
select(Reserve).where(
|
|
Reserve.groupe_id == gid,
|
|
Reserve.active == True,
|
|
).order_by(Reserve.nom)
|
|
)
|
|
reserves = result.scalars().all()
|
|
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,
|
|
"montant": round(montant, 2),
|
|
"note": r.note,
|
|
"modifie_le": r.modifie_le.isoformat(),
|
|
})
|
|
total_reserves_reelles = sum(r["montant"] for r in reserves_details)
|
|
|
|
# ── Somme disponible ──
|
|
disponible = solde_banque_actuel + encaisse_actuelle - engagements_non_debites - total_reserves_reelles
|
|
# ── 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({
|
|
"id": str(c.id),
|
|
"date": str(c.date_collecte),
|
|
"montant": float(c.montant),
|
|
"detenteur": det.prenom if det else "?",
|
|
"statut": c.statut,
|
|
"modifie_le": c.modifie_le.isoformat(),
|
|
})
|
|
|
|
# ── 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",
|
|
})
|
|
|
|
return {
|
|
"solde_banque": round(solde_banque_actuel, 2),
|
|
"date_releve": date_releve,
|
|
"encaisse": round(encaisse_actuelle, 2),
|
|
"disponible": round(disponible, 2),
|
|
"engagements_non_debites": round(engagements_non_debites, 2),
|
|
"reserve_prudente": 0,
|
|
"reserve_anniversaire": 0,
|
|
"total_reserves": round(total_reserves_reelles, 2),
|
|
"total_reserves_reelles": round(total_reserves_reelles, 2),
|
|
"reserves_details": reserves_details,
|
|
"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,
|
|
}
|
|
|
|
|
|
@router.get("/registre")
|
|
async def registre_transactions(
|
|
mois: int | None = None,
|
|
annee: int | None = None,
|
|
type_transaction: str | None = None,
|
|
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
|
|
from app.models.transaction_comptable import TransactionComptable
|
|
from app.models.vente_jeton import VenteJeton
|
|
|
|
gid = membre.groupe_id
|
|
transactions = []
|
|
dons_operation_ids = set()
|
|
|
|
# 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)
|
|
|
|
# 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({
|
|
"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({
|
|
"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",
|
|
})
|
|
|
|
# 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({
|
|
"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,
|
|
)
|
|
.order_by(EnvoiContribution.date_envoi)
|
|
)
|
|
for e in result.scalars().all():
|
|
if filtre_mois(e.date_envoi):
|
|
ajouter_transaction({
|
|
"date": str(e.date_envoi),
|
|
"type": "contribution",
|
|
"description": f"Contribution {e.destination} ({e.methode})",
|
|
"entree": 0,
|
|
"sortie": float(e.montant),
|
|
"par": "Trésorier",
|
|
})
|
|
|
|
# 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)
|
|
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 op.id in dons_operation_ids:
|
|
continue
|
|
if filtre_mois(op.date_operation):
|
|
m = await db.get(Membre, op.effectue_par)
|
|
is_depot = op.type == "depot"
|
|
ajouter_transaction({
|
|
"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 "?",
|
|
})
|
|
|
|
# 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)
|
|
return transactions
|