"""Routes C1 — Rapports PDF (finances, PV, rapport RSG).""" import io from datetime import date, datetime from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import StreamingResponse from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, extract from app.core.database import get_db from app.core.security import get_current_membre from app.models.groupe import Groupe from app.models.collecte import Collecte from app.models.depense import Depense from app.models.membre import Membre from app.models.reunion import Reunion from app.models.envoi_contribution import EnvoiContribution from app.models.proposition import Proposition router = APIRouter(prefix="/rapports", tags=["C1 — Rapports PDF"]) MOIS_FR = [ "", "Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre", ] def _css(): return """ body { font-family: 'DejaVu Sans', sans-serif; font-size: 11pt; color: #333; margin: 2cm; } h1 { color: #1a5276; font-size: 16pt; margin-bottom: 4pt; } h2 { color: #1a5276; font-size: 13pt; border-bottom: 2px solid #1a5276; padding-bottom: 4pt; margin-top: 20pt; } .header { text-align: center; margin-bottom: 20pt; } .header .groupe { font-size: 18pt; font-weight: bold; color: #1a5276; } .header .sub { font-size: 9pt; color: #888; } table { width: 100%; border-collapse: collapse; margin-top: 8pt; } th { background: #1a5276; color: white; padding: 6pt 8pt; text-align: left; font-size: 9pt; } td { padding: 5pt 8pt; border-bottom: 1px solid #ddd; font-size: 10pt; } .total { font-weight: bold; font-size: 12pt; margin-top: 10pt; } .footer { text-align: center; font-size: 8pt; color: #aaa; margin-top: 30pt; } .badge-paye { color: #27ae60; } .badge-impaye { color: #e74c3c; } """ @router.get("/finances/{annee}/{mois}") async def rapport_finances( annee: int, mois: int, membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db), ): """C1 — Rapport financier mensuel en PDF.""" try: from weasyprint import HTML except ImportError: raise HTTPException(status_code=500, detail="WeasyPrint non installé. Installer avec: pip install weasyprint") groupe = await db.get(Groupe, membre.groupe_id) # Collectes du mois result = await db.execute( select(Collecte).where( Collecte.groupe_id == membre.groupe_id, extract("year", Collecte.date_collecte) == annee, extract("month", Collecte.date_collecte) == mois, ).order_by(Collecte.date_collecte) ) collectes = result.scalars().all() # Dépenses du mois result = await db.execute( select(Depense).where( Depense.groupe_id == membre.groupe_id, extract("year", Depense.date_depense) == annee, extract("month", Depense.date_depense) == mois, ).order_by(Depense.date_depense) ) depenses = result.scalars().all() total_collectes = sum(float(c.montant) for c in collectes) total_depenses = sum(float(d.montant) for d in depenses) solde = total_collectes - total_depenses # Construire le HTML rows_collectes = "" for c in collectes: saisi = await db.get(Membre, c.saisi_par) if c.saisi_par else None rows_collectes += f"
| Date | Montant | Saisi par |
|---|---|---|
| Aucune collecte ce mois | ||
Total collectes : {total_collectes:.2f} $
| Date | Montant | Qui | Quoi | Méthode | Payé |
|---|---|---|---|---|---|
| Aucune dépense ce mois | |||||
Total dépenses : {total_depenses:.2f} $
| Collectes | {total_collectes:.2f} $ |
| Dépenses | −{total_depenses:.2f} $ |
| Solde du mois | {'+'if solde>=0 else ''}{solde:.2f} $ |
| Date | Type | Animateur | Thème | Annonces |
|---|---|---|---|---|
| Aucune réunion enregistrée ce mois | ||||
{len(reunions)} réunion(s) enregistrée(s)
""" pdf_bytes = HTML(string=html_content).write_pdf() filename = f"rapport_reunions_{annee}_{mois:02d}.pdf" return StreamingResponse( io.BytesIO(pdf_bytes), media_type="application/pdf", headers={"Content-Disposition": f'inline; filename="{filename}"'}, ) @router.get("/disponibles") async def rapports_disponibles( membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db), ): """Liste les mois pour lesquels des rapports sont disponibles.""" # Mois avec collectes result = await db.execute( select( extract("year", Collecte.date_collecte).label("annee"), extract("month", Collecte.date_collecte).label("mois"), ).where(Collecte.groupe_id == membre.groupe_id) .group_by("annee", "mois") .order_by(extract("year", Collecte.date_collecte).desc(), extract("month", Collecte.date_collecte).desc()) ) mois_finances = [{"annee": int(r.annee), "mois": int(r.mois)} for r in result.all()] # Mois avec réunions result = await db.execute( select( extract("year", Reunion.date_reunion).label("annee"), extract("month", Reunion.date_reunion).label("mois"), ).where(Reunion.groupe_id == membre.groupe_id) .group_by("annee", "mois") .order_by(extract("year", Reunion.date_reunion).desc(), extract("month", Reunion.date_reunion).desc()) ) mois_reunions = [{"annee": int(r.annee), "mois": int(r.mois)} for r in result.all()] # Mois avec contributions result = await db.execute( select( extract("year", EnvoiContribution.date_envoi).label("annee"), extract("month", EnvoiContribution.date_envoi).label("mois"), ).where(EnvoiContribution.groupe_id == membre.groupe_id) .group_by("annee", "mois") ) mois_contributions = [{"annee": int(r.annee), "mois": int(r.mois)} for r in result.all()] # Combiner et dédupliquer all_mois = {} for m in mois_finances: key = f"{m['annee']}-{m['mois']:02d}" all_mois[key] = {"annee": m["annee"], "mois": m["mois"], "finances": True, "reunions": False, "contributions": False} for m in mois_reunions: key = f"{m['annee']}-{m['mois']:02d}" if key in all_mois: all_mois[key]["reunions"] = True else: all_mois[key] = {"annee": m["annee"], "mois": m["mois"], "finances": False, "reunions": True, "contributions": False} for m in mois_contributions: key = f"{m['annee']}-{m['mois']:02d}" if key in all_mois: all_mois[key]["contributions"] = True else: all_mois[key] = {"annee": m["annee"], "mois": m["mois"], "finances": False, "reunions": False, "contributions": True} return sorted(all_mois.values(), key=lambda x: (x["annee"], x["mois"]), reverse=True) @router.get("/resolutions/annees") async def annees_resolutions( membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db), ): """Années pour lesquelles des résolutions existent.""" result = await db.execute( select(extract("year", Proposition.cree_le).label("annee")) .where( Proposition.groupe_id == membre.groupe_id, Proposition.statut.in_(["adoptee", "rejetee"]), ) .group_by("annee") .order_by(extract("year", Proposition.cree_le).desc()) ) return [{"annee": int(r.annee)} for r in result.all()] @router.get("/financier-complet/{annee}/{mois}") async def rapport_financier_complet( annee: int, mois: int, membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db), ): """Rapport financier complet incluant collectes, dépenses et contributions.""" gid = membre.groupe_id # Collectes result = await db.execute( select(Collecte).where( Collecte.groupe_id == gid, extract("year", Collecte.date_collecte) == annee, extract("month", Collecte.date_collecte) == mois, ) ) collectes = result.scalars().all() total_collectes = sum(float(c.montant) for c in collectes) # Dépenses result = await db.execute( select(Depense).where( Depense.groupe_id == gid, extract("year", Depense.date_depense) == annee, extract("month", Depense.date_depense) == mois, ) ) depenses = result.scalars().all() total_depenses = sum(float(d.montant) for d in depenses) depenses_impayees = sum(float(d.montant) for d in depenses if not d.paye) # Contributions result = await db.execute( select(EnvoiContribution).where( EnvoiContribution.groupe_id == gid, extract("year", EnvoiContribution.date_envoi) == annee, extract("month", EnvoiContribution.date_envoi) == mois, ) ) contributions = result.scalars().all() total_contributions = sum(float(c.montant) for c in contributions) solde = total_collectes - total_depenses - total_contributions return { "mois": mois, "annee": annee, "total_collectes": round(total_collectes, 2), "total_depenses": round(total_depenses, 2), "total_contributions": round(total_contributions, 2), "solde_mois": round(solde, 2), "nb_collectes": len(collectes), "nb_depenses": len(depenses), "nb_contributions": len(contributions), "depenses_impayees": round(depenses_impayees, 2), } @router.get("/resolutions/{annee}") async def rapport_resolutions( annee: int, membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db), ): """C1 — Registre des résolutions adoptées en PDF.""" try: from weasyprint import HTML except ImportError: raise HTTPException(status_code=500, detail="WeasyPrint non installé") groupe = await db.get(Groupe, membre.groupe_id) result = await db.execute( select(Proposition).where( Proposition.groupe_id == membre.groupe_id, Proposition.statut.in_(["adoptee", "rejetee"]), extract("year", Proposition.cree_le) == annee, ).order_by(Proposition.numero_resolution, Proposition.cree_le) ) propositions = result.scalars().all() rows = "" for p in propositions: proposeur = await db.get(Membre, p.proposeur_id) if p.proposeur_id else None secondeur = await db.get(Membre, p.secondeur_id) if p.secondeur_id else None badge = '✓ Adoptée' if p.statut == "adoptee" else '✗ Rejetée' rows += f"""| N° | Date | Sujet | Proposeur | Secondeur | Résultat |
|---|---|---|---|---|---|
| Aucune résolution pour cette année | |||||
{nb_adoptees} résolution(s) adoptée(s), {nb_rejetees} rejetée(s)
""" pdf_bytes = HTML(string=html_content).write_pdf() filename = f"registre_resolutions_{groupe.nom.replace(' ', '_')}_{annee}.pdf" return StreamingResponse( io.BytesIO(pdf_bytes), media_type="application/pdf", headers={"Content-Disposition": f'inline; filename="{filename}"'}, ) @router.get("/etat-financier") async def etat_financier_temps_reel( membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db), ): """État des résultats et transactions en temps réel (JSON).""" from sqlalchemy import func, desc from app.models.operation_bancaire import OperationBancaire from app.models.vente_litterature import VenteLitterature from app.models.litterature import Litterature gid = membre.groupe_id groupe = await db.get(Groupe, gid) # ── 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 # ── Revenus ── result = await db.execute( select(func.coalesce(func.sum(Collecte.montant), 0)).where( Collecte.groupe_id == gid, Collecte.statut == "recu")) total_collectes = float(result.scalar()) result = await db.execute( select(func.coalesce(func.sum(VenteLitterature.montant), 0)).where( VenteLitterature.groupe_id == gid, VenteLitterature.remis_tresorier == True)) total_ventes = float(result.scalar()) revenus = total_collectes + total_ventes # ── Dépenses ── result = await db.execute( select(func.coalesce(func.sum(Depense.montant), 0)).where( Depense.groupe_id == gid, Depense.paye == True)) total_depenses_payees = float(result.scalar()) result = await db.execute( select(func.coalesce(func.sum(Depense.montant), 0)).where( Depense.groupe_id == gid, Depense.paye == False, Depense.confirme_par.isnot(None))) total_engagements = float(result.scalar()) # ── Contributions ── result = await db.execute( select(func.coalesce(func.sum(EnvoiContribution.montant), 0)).where( EnvoiContribution.groupe_id == gid)) total_contributions = float(result.scalar()) # ── Encaisse ── 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()) 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()) encaisse = total_collectes + total_ventes - total_depots - depenses_cash # ── Solde banque ── filtre_depuis = dernier_releve.date_operation if dernier_releve else None q_depots = select(func.coalesce(func.sum(OperationBancaire.montant), 0)).where( OperationBancaire.groupe_id == gid, OperationBancaire.type == "depot") q_retraits = select(func.coalesce(func.sum(OperationBancaire.montant), 0)).where( OperationBancaire.groupe_id == gid, OperationBancaire.type == "retrait") if filtre_depuis: q_depots = q_depots.where(OperationBancaire.date_operation > filtre_depuis) q_retraits = q_retraits.where(OperationBancaire.date_operation > filtre_depuis) depots_depuis = float((await db.execute(q_depots)).scalar()) retraits_depuis = float((await db.execute(q_retraits)).scalar()) solde_banque = solde_releve + depots_depuis - retraits_depuis # ── Réserves ── reserve_prudente = 500.0 mois_depuis = 0 if groupe and groupe.cree_le: from datetime import date as d aujourdhui = d.today() cree = groupe.cree_le.date() if hasattr(groupe.cree_le, 'date') else groupe.cree_le mois_depuis = max(0, (aujourdhui.year - cree.year) * 12 + (aujourdhui.month - cree.month)) reserve_anniversaire = mois_depuis * 20.0 disponible = solde_banque - total_engagements - reserve_prudente - reserve_anniversaire # ── Dernières transactions ── result = await db.execute( select(Collecte).where(Collecte.groupe_id == gid, Collecte.statut == "recu") .order_by(desc(Collecte.date_collecte)).limit(10)) dernieres_collectes = [{ "date": str(c.date_collecte), "montant": float(c.montant), } for c in result.scalars().all()] result = await db.execute( select(Depense).where(Depense.groupe_id == gid) .order_by(desc(Depense.date_depense)).limit(10)) dernieres_depenses = [{ "date": str(d.date_depense), "montant": float(d.montant), "quoi": d.quoi, "paye": d.paye, "methode": d.methode, } for d in result.scalars().all()] return { "groupe": groupe.nom if groupe else "?", "date_releve": str(dernier_releve.date_operation) if dernier_releve else None, "solde_releve": round(solde_releve, 2), "solde_banque": round(solde_banque, 2), "encaisse": round(max(0, encaisse), 2), "disponible": round(disponible, 2), "revenus": { "collectes": round(total_collectes, 2), "ventes_litterature": round(total_ventes, 2), "total": round(revenus, 2), }, "sorties": { "depenses_payees": round(total_depenses_payees, 2), "engagements": round(total_engagements, 2), "contributions": round(total_contributions, 2), "total": round(total_depenses_payees + total_contributions, 2), }, "reserves": { "prudente": reserve_prudente, "anniversaire": round(reserve_anniversaire, 2), "total": round(reserve_prudente + reserve_anniversaire, 2), }, "resultat_net": round(revenus - total_depenses_payees - total_contributions, 2), "dernieres_collectes": dernieres_collectes, "dernieres_depenses": dernieres_depenses, }