Ajoute modifie_le aux objets sensibles et fait transmettre la version affichée par le frontend pour refuser les écritures périmées avec un message HTTP 409. Ajoute une table historique_modifications et un service d'historique métier afin de journaliser automatiquement les raisons des changements importants sans intervention de l'utilisateur. Expose l'historique métier dans le module Journal, distinct du journal technique, et documente les processus utilisateurs ainsi qu'une présentation sobre aux membres.
615 lines
24 KiB
Python
615 lines
24 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.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.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, verifier_version_historisee
|
|
|
|
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
|
|
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")
|
|
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")
|
|
|
|
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,
|
|
)
|
|
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")
|
|
|
|
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,
|
|
)
|
|
|
|
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")
|
|
|
|
# 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,
|
|
)
|
|
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
|