2026-03-04 14:01:00 -05:00
|
|
|
|
"""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
|
2026-04-17 04:04:04 -04:00
|
|
|
|
from app.models.proposition import Proposition
|
2026-03-04 14:01:00 -05:00
|
|
|
|
|
|
|
|
|
|
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"<tr><td>{c.date_collecte}</td><td>{float(c.montant):.2f} $</td><td>{saisi.prenom if saisi else '-'}</td></tr>"
|
|
|
|
|
|
|
|
|
|
|
|
rows_depenses = ""
|
|
|
|
|
|
for d in depenses:
|
|
|
|
|
|
qui = await db.get(Membre, d.qui) if d.qui else None
|
|
|
|
|
|
badge = '<span class="badge-paye">✓</span>' if d.paye else '<span class="badge-impaye">✗ DÛ</span>'
|
|
|
|
|
|
rows_depenses += f"<tr><td>{d.date_depense}</td><td>{float(d.montant):.2f} $</td><td>{qui.prenom if qui else '-'}</td><td>{d.quoi}</td><td>{d.methode}</td><td>{badge}</td></tr>"
|
|
|
|
|
|
|
|
|
|
|
|
html_content = f"""<!DOCTYPE html><html><head><style>{_css()}</style></head><body>
|
|
|
|
|
|
<div class="header">
|
|
|
|
|
|
<div class="groupe">{groupe.nom if groupe else 'Groupe'}</div>
|
|
|
|
|
|
<div class="sub">District {groupe.district if groupe else '87-16'} · Région {groupe.region if groupe else '87'}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<h1>Rapport financier — {MOIS_FR[mois]} {annee}</h1>
|
|
|
|
|
|
|
|
|
|
|
|
<h2>Collectes (7e Tradition)</h2>
|
|
|
|
|
|
<table><tr><th>Date</th><th>Montant</th><th>Saisi par</th></tr>
|
|
|
|
|
|
{rows_collectes if rows_collectes else '<tr><td colspan="3">Aucune collecte ce mois</td></tr>'}
|
|
|
|
|
|
</table>
|
|
|
|
|
|
<p class="total">Total collectes : {total_collectes:.2f} $</p>
|
|
|
|
|
|
|
|
|
|
|
|
<h2>Dépenses</h2>
|
|
|
|
|
|
<table><tr><th>Date</th><th>Montant</th><th>Qui</th><th>Quoi</th><th>Méthode</th><th>Payé</th></tr>
|
|
|
|
|
|
{rows_depenses if rows_depenses else '<tr><td colspan="6">Aucune dépense ce mois</td></tr>'}
|
|
|
|
|
|
</table>
|
|
|
|
|
|
<p class="total">Total dépenses : {total_depenses:.2f} $</p>
|
|
|
|
|
|
|
|
|
|
|
|
<h2>Sommaire</h2>
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<tr><td><strong>Collectes</strong></td><td><strong>{total_collectes:.2f} $</strong></td></tr>
|
|
|
|
|
|
<tr><td><strong>Dépenses</strong></td><td><strong>−{total_depenses:.2f} $</strong></td></tr>
|
|
|
|
|
|
<tr><td><strong>Solde du mois</strong></td><td><strong>{'+'if solde>=0 else ''}{solde:.2f} $</strong></td></tr>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="footer">
|
|
|
|
|
|
Rapport généré automatiquement · {datetime.now().strftime('%Y-%m-%d %H:%M')} · 100% logiciel libre
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</body></html>"""
|
|
|
|
|
|
|
|
|
|
|
|
pdf_bytes = HTML(string=html_content).write_pdf()
|
|
|
|
|
|
filename = f"rapport_finances_{groupe.nom.replace(' ','_')}_{annee}_{mois:02d}.pdf"
|
|
|
|
|
|
|
|
|
|
|
|
return StreamingResponse(
|
|
|
|
|
|
io.BytesIO(pdf_bytes),
|
|
|
|
|
|
media_type="application/pdf",
|
|
|
|
|
|
headers={"Content-Disposition": f'inline; filename="{filename}"'},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/reunions/{annee}/{mois}")
|
|
|
|
|
|
async def rapport_reunions(
|
|
|
|
|
|
annee: int,
|
|
|
|
|
|
mois: int,
|
|
|
|
|
|
membre=Depends(get_current_membre),
|
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
):
|
|
|
|
|
|
"""C1 — Résumé des réunions du mois 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(Reunion).where(
|
|
|
|
|
|
Reunion.groupe_id == membre.groupe_id,
|
|
|
|
|
|
extract("year", Reunion.date_reunion) == annee,
|
|
|
|
|
|
extract("month", Reunion.date_reunion) == mois,
|
|
|
|
|
|
).order_by(Reunion.date_reunion)
|
|
|
|
|
|
)
|
|
|
|
|
|
reunions = result.scalars().all()
|
|
|
|
|
|
|
|
|
|
|
|
rows = ""
|
|
|
|
|
|
for r in reunions:
|
|
|
|
|
|
anim = await db.get(Membre, r.animateur_id) if r.animateur_id else None
|
|
|
|
|
|
annonces_str = ", ".join(r.annonces) if r.annonces else "—"
|
|
|
|
|
|
rows += f"<tr><td>{r.date_reunion.strftime('%Y-%m-%d')}</td><td>{r.type}</td><td>{anim.prenom if anim else '-'}</td><td>{r.theme or '-'}</td><td>{annonces_str}</td></tr>"
|
|
|
|
|
|
|
|
|
|
|
|
html_content = f"""<!DOCTYPE html><html><head><style>{_css()}</style></head><body>
|
|
|
|
|
|
<div class="header">
|
|
|
|
|
|
<div class="groupe">{groupe.nom if groupe else 'Groupe'}</div>
|
|
|
|
|
|
<div class="sub">District {groupe.district if groupe else '87-16'} · Région {groupe.region if groupe else '87'}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<h1>Réunions — {MOIS_FR[mois]} {annee}</h1>
|
|
|
|
|
|
<table><tr><th>Date</th><th>Type</th><th>Animateur</th><th>Thème</th><th>Annonces</th></tr>
|
|
|
|
|
|
{rows if rows else '<tr><td colspan="5">Aucune réunion enregistrée ce mois</td></tr>'}
|
|
|
|
|
|
</table>
|
|
|
|
|
|
<p class="total">{len(reunions)} réunion(s) enregistrée(s)</p>
|
|
|
|
|
|
<div class="footer">Rapport généré automatiquement · {datetime.now().strftime('%Y-%m-%d %H:%M')}</div>
|
|
|
|
|
|
</body></html>"""
|
|
|
|
|
|
|
|
|
|
|
|
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"),
|
2026-05-15 16:41:17 -04:00
|
|
|
|
).where(
|
|
|
|
|
|
EnvoiContribution.groupe_id == membre.groupe_id,
|
|
|
|
|
|
EnvoiContribution.debitee == True,
|
|
|
|
|
|
)
|
2026-03-04 14:01:00 -05:00
|
|
|
|
.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)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-17 04:04:04 -04:00
|
|
|
|
@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()]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-04 14:01:00 -05:00
|
|
|
|
@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,
|
2026-05-15 16:41:17 -04:00
|
|
|
|
EnvoiContribution.debitee == True,
|
2026-03-04 14:01:00 -05:00
|
|
|
|
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),
|
|
|
|
|
|
}
|
2026-04-17 04:04:04 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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 = '<span class="badge-paye">✓ Adoptée</span>' if p.statut == "adoptee" else '<span class="badge-impaye">✗ Rejetée</span>'
|
|
|
|
|
|
rows += f"""<tr>
|
|
|
|
|
|
<td>{p.numero_resolution or '—'}</td>
|
|
|
|
|
|
<td>{p.date_vote or '—'}</td>
|
|
|
|
|
|
<td>{p.sujet}</td>
|
|
|
|
|
|
<td>{proposeur.prenom if proposeur else '—'}</td>
|
|
|
|
|
|
<td>{secondeur.prenom if secondeur else '—'}</td>
|
|
|
|
|
|
<td>{badge}</td>
|
|
|
|
|
|
</tr>"""
|
|
|
|
|
|
|
|
|
|
|
|
nb_adoptees = sum(1 for p in propositions if p.statut == "adoptee")
|
|
|
|
|
|
nb_rejetees = sum(1 for p in propositions if p.statut == "rejetee")
|
|
|
|
|
|
|
|
|
|
|
|
html_content = f"""<!DOCTYPE html><html><head><style>{_css()}</style></head><body>
|
|
|
|
|
|
<div class="header">
|
|
|
|
|
|
<div class="groupe">{groupe.nom if groupe else 'Groupe'}</div>
|
|
|
|
|
|
<div class="sub">District {groupe.district if groupe else '87-16'} · Région {groupe.region if groupe else '87'}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<h1>Registre des résolutions — {annee}</h1>
|
|
|
|
|
|
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<tr><th>N°</th><th>Date</th><th>Sujet</th><th>Proposeur</th><th>Secondeur</th><th>Résultat</th></tr>
|
|
|
|
|
|
{rows if rows else '<tr><td colspan="6">Aucune résolution pour cette année</td></tr>'}
|
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
|
|
|
|
<p class="total">{nb_adoptees} résolution(s) adoptée(s), {nb_rejetees} rejetée(s)</p>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="footer">
|
|
|
|
|
|
Registre généré automatiquement · {datetime.now().strftime('%Y-%m-%d %H:%M')} · 100% logiciel libre
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</body></html>"""
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-05-15 16:41:17 -04:00
|
|
|
|
from app.models.reserve import Reserve
|
2026-04-17 04:04:04 -04:00
|
|
|
|
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(
|
2026-05-15 16:41:17 -04:00
|
|
|
|
EnvoiContribution.groupe_id == gid,
|
|
|
|
|
|
EnvoiContribution.debitee == True,
|
|
|
|
|
|
))
|
2026-04-17 04:04:04 -04:00
|
|
|
|
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())
|
|
|
|
|
|
|
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())
|
|
|
|
|
|
|
2026-05-15 16:41:17 -04:00
|
|
|
|
encaisse = total_collectes + total_ventes + total_retraits - total_depots - depenses_cash
|
2026-04-17 04:04:04 -04:00
|
|
|
|
|
|
|
|
|
|
# ── 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")
|
2026-05-15 16:41:17 -04:00
|
|
|
|
q_depenses_bancaires = select(func.coalesce(func.sum(Depense.montant), 0)).where(
|
|
|
|
|
|
Depense.groupe_id == gid,
|
|
|
|
|
|
Depense.paye == True,
|
|
|
|
|
|
Depense.methode.in_(["VIREMENT", "CHEQUE"]),
|
|
|
|
|
|
)
|
|
|
|
|
|
q_contributions = select(func.coalesce(func.sum(EnvoiContribution.montant), 0)).where(
|
|
|
|
|
|
EnvoiContribution.groupe_id == gid,
|
|
|
|
|
|
EnvoiContribution.debitee == True,
|
|
|
|
|
|
)
|
2026-04-17 04:04:04 -04:00
|
|
|
|
if filtre_depuis:
|
|
|
|
|
|
q_depots = q_depots.where(OperationBancaire.date_operation > filtre_depuis)
|
|
|
|
|
|
q_retraits = q_retraits.where(OperationBancaire.date_operation > filtre_depuis)
|
2026-05-15 16:41:17 -04:00
|
|
|
|
q_depenses_bancaires = q_depenses_bancaires.where(Depense.date_confirmation > filtre_depuis)
|
|
|
|
|
|
q_contributions = q_contributions.where(EnvoiContribution.date_envoi > filtre_depuis)
|
2026-04-17 04:04:04 -04:00
|
|
|
|
depots_depuis = float((await db.execute(q_depots)).scalar())
|
|
|
|
|
|
retraits_depuis = float((await db.execute(q_retraits)).scalar())
|
2026-05-15 16:41:17 -04:00
|
|
|
|
depenses_bancaires_depuis = float((await db.execute(q_depenses_bancaires)).scalar())
|
|
|
|
|
|
contributions_depuis = float((await db.execute(q_contributions)).scalar())
|
|
|
|
|
|
solde_banque = solde_releve + depots_depuis - retraits_depuis - depenses_bancaires_depuis - contributions_depuis
|
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_details = [
|
|
|
|
|
|
{"id": str(r.id), "nom": r.nom, "montant": float(r.montant), "note": r.note}
|
|
|
|
|
|
for r in result.scalars().all()
|
|
|
|
|
|
]
|
|
|
|
|
|
total_reserves = sum(r["montant"] for r in reserves_details)
|
2026-04-17 04:04:04 -04:00
|
|
|
|
|
2026-05-15 16:41:17 -04:00
|
|
|
|
disponible = solde_banque - total_engagements - total_reserves
|
2026-04-17 04:04:04 -04:00
|
|
|
|
|
|
|
|
|
|
# ── 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),
|
2026-05-15 16:41:17 -04:00
|
|
|
|
"encaisse": round(encaisse, 2),
|
2026-04-17 04:04:04 -04:00
|
|
|
|
"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": {
|
2026-05-15 16:41:17 -04:00
|
|
|
|
"details": reserves_details,
|
|
|
|
|
|
"total": round(total_reserves, 2),
|
2026-04-17 04:04:04 -04:00
|
|
|
|
},
|
|
|
|
|
|
"resultat_net": round(revenus - total_depenses_payees - total_contributions, 2),
|
|
|
|
|
|
"dernieres_collectes": dernieres_collectes,
|
|
|
|
|
|
"dernieres_depenses": dernieres_depenses,
|
|
|
|
|
|
}
|