2026-03-04 14:01:00 -05:00
|
|
|
|
"""Routes C1 — Rapports PDF (finances, PV, rapport RSG)."""
|
|
|
|
|
|
import io
|
2026-05-25 17:39:38 -04:00
|
|
|
|
from html import escape
|
|
|
|
|
|
from datetime import date, datetime, time, timedelta, timezone
|
2026-03-04 14:01:00 -05:00
|
|
|
|
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",
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-25 17:39:38 -04:00
|
|
|
|
def _texte_decision_resolution(p: Proposition) -> str:
|
|
|
|
|
|
"""Retourne le texte décisionnel si la résolution utilise le format structuré."""
|
|
|
|
|
|
if p.groupe_a_decide_de:
|
|
|
|
|
|
return p.groupe_a_decide_de
|
|
|
|
|
|
if not p.note:
|
|
|
|
|
|
return p.sujet
|
|
|
|
|
|
marqueur = "LE GROUPE A DÉCIDÉ DE"
|
|
|
|
|
|
if marqueur not in p.note:
|
|
|
|
|
|
return p.sujet
|
|
|
|
|
|
decision = p.note.split(marqueur, 1)[1].strip()
|
|
|
|
|
|
return decision or p.sujet
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-04 14:01:00 -05:00
|
|
|
|
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
|
2026-05-25 17:39:38 -04:00
|
|
|
|
numero_date = f"{escape(p.numero_resolution or '—')}<br><span class=\"muted\">{escape(str(p.date_adoption or p.date_vote or '—'))}</span>"
|
|
|
|
|
|
resolution = escape(_texte_decision_resolution(p)).replace("\n", "<br>")
|
|
|
|
|
|
proposeurs = f"{escape(proposeur.prenom if proposeur else '—')} / {escape(secondeur.prenom if secondeur else '—')}"
|
2026-04-17 04:04:04 -04:00
|
|
|
|
rows += f"""<tr>
|
2026-05-25 17:39:38 -04:00
|
|
|
|
<td>{numero_date}</td>
|
|
|
|
|
|
<td>{resolution}</td>
|
|
|
|
|
|
<td>{proposeurs}</td>
|
2026-04-17 04:04:04 -04:00
|
|
|
|
</tr>"""
|
|
|
|
|
|
|
|
|
|
|
|
html_content = f"""<!DOCTYPE html><html><head><style>{_css()}</style></head><body>
|
2026-05-25 17:39:38 -04:00
|
|
|
|
<style>.muted {{ color: #777; font-size: 8pt; }}</style>
|
2026-04-17 04:04:04 -04:00
|
|
|
|
<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>
|
2026-05-25 17:39:38 -04:00
|
|
|
|
<p>Décisions prises en Groupe</p>
|
2026-04-17 04:04:04 -04:00
|
|
|
|
|
|
|
|
|
|
<table>
|
2026-05-25 17:39:38 -04:00
|
|
|
|
<colgroup>
|
|
|
|
|
|
<col style="width: 18%;">
|
|
|
|
|
|
<col style="width: 60%;">
|
|
|
|
|
|
<col style="width: 22%;">
|
|
|
|
|
|
</colgroup>
|
|
|
|
|
|
<tr><th>N° / date</th><th>Résolution</th><th>Proposeur / secondeur</th></tr>
|
|
|
|
|
|
{rows if rows else '<tr><td colspan="3">Aucune résolution pour cette année</td></tr>'}
|
2026-04-17 04:04:04 -04:00
|
|
|
|
</table>
|
|
|
|
|
|
|
|
|
|
|
|
<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(
|
2026-05-25 17:39:38 -04:00
|
|
|
|
annee: int | None = None,
|
|
|
|
|
|
mois: int | None = None,
|
2026-04-17 04:04:04 -04:00
|
|
|
|
membre=Depends(get_current_membre),
|
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
):
|
2026-05-25 17:39:38 -04:00
|
|
|
|
"""État des résultats par période et soldes courants (JSON)."""
|
2026-04-17 04:04:04 -04:00
|
|
|
|
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-05-25 17:39:38 -04:00
|
|
|
|
from app.models.transaction_comptable import TransactionComptable
|
2026-04-17 04:04:04 -04:00
|
|
|
|
from app.models.vente_litterature import VenteLitterature
|
2026-05-25 17:39:38 -04:00
|
|
|
|
from app.services.finance import encaisse as solde_encaisse
|
|
|
|
|
|
from app.services.finance import engagements_non_debites
|
|
|
|
|
|
from app.services.finance import solde_banque as solde_banque_actuel
|
|
|
|
|
|
from app.services.finance import solde_reserve
|
2026-04-17 04:04:04 -04:00
|
|
|
|
|
|
|
|
|
|
gid = membre.groupe_id
|
|
|
|
|
|
groupe = await db.get(Groupe, gid)
|
2026-05-25 17:39:38 -04:00
|
|
|
|
aujourd_hui = date.today()
|
|
|
|
|
|
annee = annee or aujourd_hui.year
|
|
|
|
|
|
mois = mois or aujourd_hui.month
|
2026-04-17 04:04:04 -04:00
|
|
|
|
|
|
|
|
|
|
# ── 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
|
|
|
|
|
|
|
2026-05-25 17:39:38 -04:00
|
|
|
|
# ── État des résultats : période demandée ──
|
2026-04-17 04:04:04 -04:00
|
|
|
|
result = await db.execute(
|
|
|
|
|
|
select(func.coalesce(func.sum(Collecte.montant), 0)).where(
|
2026-05-25 17:39:38 -04:00
|
|
|
|
Collecte.groupe_id == gid,
|
|
|
|
|
|
Collecte.statut == "recu",
|
|
|
|
|
|
extract("year", Collecte.date_reception) == annee,
|
|
|
|
|
|
extract("month", Collecte.date_reception) == mois,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2026-04-17 04:04:04 -04:00
|
|
|
|
total_collectes = float(result.scalar())
|
|
|
|
|
|
|
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
|
select(func.coalesce(func.sum(VenteLitterature.montant), 0)).where(
|
2026-05-25 17:39:38 -04:00
|
|
|
|
VenteLitterature.groupe_id == gid,
|
|
|
|
|
|
VenteLitterature.remis_tresorier == True,
|
|
|
|
|
|
extract("year", VenteLitterature.date_remise) == annee,
|
|
|
|
|
|
extract("month", VenteLitterature.date_remise) == mois,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2026-04-17 04:04:04 -04:00
|
|
|
|
total_ventes = float(result.scalar())
|
|
|
|
|
|
|
2026-05-25 17:39:38 -04:00
|
|
|
|
result = await db.execute(
|
|
|
|
|
|
select(func.coalesce(func.sum(TransactionComptable.montant), 0)).where(
|
|
|
|
|
|
TransactionComptable.groupe_id == gid,
|
|
|
|
|
|
TransactionComptable.type == "don",
|
|
|
|
|
|
TransactionComptable.compte == "banque",
|
|
|
|
|
|
TransactionComptable.sens == "debit",
|
|
|
|
|
|
extract("year", TransactionComptable.date_transaction) == annee,
|
|
|
|
|
|
extract("month", TransactionComptable.date_transaction) == mois,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
total_dons = float(result.scalar())
|
|
|
|
|
|
|
|
|
|
|
|
revenus = total_collectes + total_ventes + total_dons
|
2026-04-17 04:04:04 -04:00
|
|
|
|
|
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
|
select(func.coalesce(func.sum(Depense.montant), 0)).where(
|
2026-05-25 17:39:38 -04:00
|
|
|
|
Depense.groupe_id == gid,
|
|
|
|
|
|
Depense.paye == True,
|
|
|
|
|
|
extract("year", Depense.date_confirmation) == annee,
|
|
|
|
|
|
extract("month", Depense.date_confirmation) == mois,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2026-04-17 04:04:04 -04:00
|
|
|
|
total_depenses_payees = float(result.scalar())
|
|
|
|
|
|
|
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
|
select(func.coalesce(func.sum(Depense.montant), 0)).where(
|
2026-05-25 17:39:38 -04:00
|
|
|
|
Depense.groupe_id == gid,
|
|
|
|
|
|
Depense.paye == False,
|
|
|
|
|
|
Depense.confirme_par.isnot(None),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2026-04-17 04:04:04 -04:00
|
|
|
|
total_engagements = float(result.scalar())
|
|
|
|
|
|
|
|
|
|
|
|
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-05-25 17:39:38 -04:00
|
|
|
|
extract("year", EnvoiContribution.date_envoi) == annee,
|
|
|
|
|
|
extract("month", EnvoiContribution.date_envoi) == mois,
|
2026-05-15 16:41:17 -04:00
|
|
|
|
))
|
2026-04-17 04:04:04 -04:00
|
|
|
|
total_contributions = float(result.scalar())
|
|
|
|
|
|
|
2026-05-25 17:39:38 -04:00
|
|
|
|
encaisse = await solde_encaisse(gid, db)
|
|
|
|
|
|
solde_banque = await solde_banque_actuel(gid, db)
|
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)
|
|
|
|
|
|
)
|
2026-05-25 17:39:38 -04:00
|
|
|
|
reserves_details = []
|
|
|
|
|
|
for reserve in result.scalars().all():
|
|
|
|
|
|
reserves_details.append({
|
|
|
|
|
|
"id": str(reserve.id),
|
|
|
|
|
|
"nom": reserve.nom,
|
|
|
|
|
|
"montant": round(await solde_reserve(reserve.id, db), 2),
|
|
|
|
|
|
"note": reserve.note,
|
|
|
|
|
|
})
|
2026-05-15 16:41:17 -04:00
|
|
|
|
total_reserves = sum(r["montant"] for r in reserves_details)
|
2026-04-17 04:04:04 -04:00
|
|
|
|
|
2026-05-25 17:39:38 -04:00
|
|
|
|
engagements_courants = await engagements_non_debites(gid, db)
|
|
|
|
|
|
disponible = solde_banque + encaisse - engagements_courants - 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 "?",
|
2026-05-25 17:39:38 -04:00
|
|
|
|
"periode": {
|
|
|
|
|
|
"annee": annee,
|
|
|
|
|
|
"mois": mois,
|
|
|
|
|
|
"libelle": f"{MOIS_FR[mois]} {annee}",
|
|
|
|
|
|
},
|
2026-04-17 04:04:04 -04:00
|
|
|
|
"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),
|
2026-05-25 17:39:38 -04:00
|
|
|
|
"dons": round(total_dons, 2),
|
2026-04-17 04:04:04 -04:00
|
|
|
|
"total": round(revenus, 2),
|
|
|
|
|
|
},
|
|
|
|
|
|
"sorties": {
|
|
|
|
|
|
"depenses_payees": round(total_depenses_payees, 2),
|
2026-05-25 17:39:38 -04:00
|
|
|
|
"engagements": round(engagements_courants, 2),
|
2026-04-17 04:04:04 -04:00
|
|
|
|
"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-05-25 17:39:38 -04:00
|
|
|
|
"total_reelles": 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,
|
|
|
|
|
|
}
|
2026-05-25 17:39:38 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/etat-resultats/{annee}/{mois}")
|
|
|
|
|
|
async def rapport_etat_resultats(
|
|
|
|
|
|
annee: int,
|
|
|
|
|
|
mois: int,
|
|
|
|
|
|
membre=Depends(get_current_membre),
|
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
):
|
|
|
|
|
|
"""PDF mensuel: état des résultats et position de trésorerie."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
from weasyprint import HTML
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
raise HTTPException(status_code=500, detail="WeasyPrint non installé")
|
|
|
|
|
|
|
|
|
|
|
|
if mois < 1 or mois > 12:
|
|
|
|
|
|
raise HTTPException(status_code=400, detail="Mois invalide")
|
|
|
|
|
|
|
|
|
|
|
|
from sqlalchemy import func, desc
|
|
|
|
|
|
from app.models.operation_bancaire import OperationBancaire
|
|
|
|
|
|
from app.models.mouvement_reserve import MouvementReserve
|
|
|
|
|
|
from app.models.reserve import Reserve
|
|
|
|
|
|
from app.models.transaction_comptable import TransactionComptable
|
|
|
|
|
|
from app.models.vente_litterature import VenteLitterature
|
|
|
|
|
|
|
|
|
|
|
|
gid = membre.groupe_id
|
|
|
|
|
|
groupe = await db.get(Groupe, gid)
|
|
|
|
|
|
fin_mois = (date(annee + 1, 1, 1) if mois == 12 else date(annee, mois + 1, 1)) - timedelta(days=1)
|
|
|
|
|
|
fin_mois_dt = datetime.combine(fin_mois, time.max).replace(tzinfo=timezone.utc)
|
|
|
|
|
|
|
|
|
|
|
|
async def solde_compte_a_date(compte: str) -> float:
|
|
|
|
|
|
debits = float((await db.execute(
|
|
|
|
|
|
select(func.coalesce(func.sum(TransactionComptable.montant), 0)).where(
|
|
|
|
|
|
TransactionComptable.groupe_id == gid,
|
|
|
|
|
|
TransactionComptable.compte == compte,
|
|
|
|
|
|
TransactionComptable.sens == "debit",
|
|
|
|
|
|
TransactionComptable.date_transaction <= fin_mois,
|
|
|
|
|
|
)
|
|
|
|
|
|
)).scalar())
|
|
|
|
|
|
credits = float((await db.execute(
|
|
|
|
|
|
select(func.coalesce(func.sum(TransactionComptable.montant), 0)).where(
|
|
|
|
|
|
TransactionComptable.groupe_id == gid,
|
|
|
|
|
|
TransactionComptable.compte == compte,
|
|
|
|
|
|
TransactionComptable.sens == "credit",
|
|
|
|
|
|
TransactionComptable.date_transaction <= fin_mois,
|
|
|
|
|
|
)
|
|
|
|
|
|
)).scalar())
|
|
|
|
|
|
return debits - credits
|
|
|
|
|
|
|
|
|
|
|
|
async def solde_reserve_a_date(reserve_id) -> float:
|
|
|
|
|
|
entrees = float((await db.execute(
|
|
|
|
|
|
select(func.coalesce(func.sum(MouvementReserve.montant), 0)).where(
|
|
|
|
|
|
MouvementReserve.destination_id == reserve_id,
|
|
|
|
|
|
MouvementReserve.cree_le <= fin_mois_dt,
|
|
|
|
|
|
)
|
|
|
|
|
|
)).scalar())
|
|
|
|
|
|
sorties = float((await db.execute(
|
|
|
|
|
|
select(func.coalesce(func.sum(MouvementReserve.montant), 0)).where(
|
|
|
|
|
|
MouvementReserve.source_id == reserve_id,
|
|
|
|
|
|
MouvementReserve.cree_le <= fin_mois_dt,
|
|
|
|
|
|
)
|
|
|
|
|
|
)).scalar())
|
|
|
|
|
|
return entrees - sorties
|
|
|
|
|
|
|
|
|
|
|
|
async def engagements_a_date() -> float:
|
|
|
|
|
|
depenses = float((await db.execute(
|
|
|
|
|
|
select(func.coalesce(func.sum(Depense.montant), 0)).where(
|
|
|
|
|
|
Depense.groupe_id == gid,
|
|
|
|
|
|
Depense.paye == False,
|
|
|
|
|
|
Depense.methode.in_(["VIREMENT", "CHEQUE"]),
|
|
|
|
|
|
Depense.date_confirmation <= fin_mois_dt,
|
|
|
|
|
|
)
|
|
|
|
|
|
)).scalar())
|
|
|
|
|
|
contributions = float((await db.execute(
|
|
|
|
|
|
select(func.coalesce(func.sum(EnvoiContribution.montant), 0)).where(
|
|
|
|
|
|
EnvoiContribution.groupe_id == gid,
|
|
|
|
|
|
EnvoiContribution.engagee == True,
|
|
|
|
|
|
EnvoiContribution.debitee == False,
|
|
|
|
|
|
EnvoiContribution.date_envoi <= fin_mois,
|
|
|
|
|
|
)
|
|
|
|
|
|
)).scalar())
|
|
|
|
|
|
return depenses + contributions
|
|
|
|
|
|
|
|
|
|
|
|
total_collectes = float((await db.execute(
|
|
|
|
|
|
select(func.coalesce(func.sum(Collecte.montant), 0)).where(
|
|
|
|
|
|
Collecte.groupe_id == gid,
|
|
|
|
|
|
Collecte.statut == "recu",
|
|
|
|
|
|
extract("year", Collecte.date_reception) == annee,
|
|
|
|
|
|
extract("month", Collecte.date_reception) == mois,
|
|
|
|
|
|
)
|
|
|
|
|
|
)).scalar())
|
|
|
|
|
|
total_ventes = float((await db.execute(
|
|
|
|
|
|
select(func.coalesce(func.sum(VenteLitterature.montant), 0)).where(
|
|
|
|
|
|
VenteLitterature.groupe_id == gid,
|
|
|
|
|
|
VenteLitterature.remis_tresorier == True,
|
|
|
|
|
|
extract("year", VenteLitterature.date_remise) == annee,
|
|
|
|
|
|
extract("month", VenteLitterature.date_remise) == mois,
|
|
|
|
|
|
)
|
|
|
|
|
|
)).scalar())
|
|
|
|
|
|
total_dons = float((await db.execute(
|
|
|
|
|
|
select(func.coalesce(func.sum(TransactionComptable.montant), 0)).where(
|
|
|
|
|
|
TransactionComptable.groupe_id == gid,
|
|
|
|
|
|
TransactionComptable.type == "don",
|
|
|
|
|
|
TransactionComptable.compte == "banque",
|
|
|
|
|
|
TransactionComptable.sens == "debit",
|
|
|
|
|
|
extract("year", TransactionComptable.date_transaction) == annee,
|
|
|
|
|
|
extract("month", TransactionComptable.date_transaction) == mois,
|
|
|
|
|
|
)
|
|
|
|
|
|
)).scalar())
|
|
|
|
|
|
total_depenses = float((await db.execute(
|
|
|
|
|
|
select(func.coalesce(func.sum(Depense.montant), 0)).where(
|
|
|
|
|
|
Depense.groupe_id == gid,
|
|
|
|
|
|
Depense.paye == True,
|
|
|
|
|
|
extract("year", Depense.date_confirmation) == annee,
|
|
|
|
|
|
extract("month", Depense.date_confirmation) == mois,
|
|
|
|
|
|
)
|
|
|
|
|
|
)).scalar())
|
|
|
|
|
|
total_contributions = float((await db.execute(
|
|
|
|
|
|
select(func.coalesce(func.sum(EnvoiContribution.montant), 0)).where(
|
|
|
|
|
|
EnvoiContribution.groupe_id == gid,
|
|
|
|
|
|
EnvoiContribution.debitee == True,
|
|
|
|
|
|
extract("year", EnvoiContribution.date_envoi) == annee,
|
|
|
|
|
|
extract("month", EnvoiContribution.date_envoi) == mois,
|
|
|
|
|
|
)
|
|
|
|
|
|
)).scalar())
|
|
|
|
|
|
|
|
|
|
|
|
revenus = total_collectes + total_ventes + total_dons
|
|
|
|
|
|
charges = total_depenses + total_contributions
|
|
|
|
|
|
resultat = revenus - charges
|
|
|
|
|
|
banque = await solde_compte_a_date("banque")
|
|
|
|
|
|
caisse = await solde_compte_a_date("encaisse")
|
|
|
|
|
|
engagements = await engagements_a_date()
|
|
|
|
|
|
|
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
|
select(Reserve).where(Reserve.groupe_id == gid).order_by(Reserve.nom)
|
|
|
|
|
|
)
|
|
|
|
|
|
reserves = []
|
|
|
|
|
|
total_reserves = 0.0
|
|
|
|
|
|
for reserve in result.scalars().all():
|
|
|
|
|
|
montant = await solde_reserve_a_date(reserve.id)
|
|
|
|
|
|
if not reserve.active and round(montant, 2) == 0:
|
|
|
|
|
|
continue
|
|
|
|
|
|
total_reserves += montant
|
|
|
|
|
|
reserves.append((reserve.nom, montant))
|
|
|
|
|
|
disponible = banque + caisse - engagements - total_reserves
|
|
|
|
|
|
|
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
|
select(OperationBancaire).where(
|
|
|
|
|
|
OperationBancaire.groupe_id == gid,
|
|
|
|
|
|
OperationBancaire.type == "releve",
|
|
|
|
|
|
OperationBancaire.date_operation <= fin_mois,
|
|
|
|
|
|
).order_by(desc(OperationBancaire.date_operation)).limit(1)
|
|
|
|
|
|
)
|
|
|
|
|
|
dernier_releve = result.scalar_one_or_none()
|
|
|
|
|
|
|
|
|
|
|
|
rows_reserves = "".join(
|
|
|
|
|
|
f"<tr><td>{escape(nom)}</td><td class=\"montant\">{montant:.2f} $</td></tr>"
|
|
|
|
|
|
for nom, montant in reserves
|
|
|
|
|
|
) or "<tr><td colspan=\"2\">Aucune réserve enregistrée</td></tr>"
|
|
|
|
|
|
|
|
|
|
|
|
css = _css() + """
|
|
|
|
|
|
.montant { text-align: right; white-space: nowrap; }
|
|
|
|
|
|
.positif { color: #1e8449; }
|
|
|
|
|
|
.negatif { color: #c0392b; }
|
|
|
|
|
|
.note { color: #777; font-size: 9pt; }
|
|
|
|
|
|
"""
|
|
|
|
|
|
signe = "+" if resultat >= 0 else ""
|
|
|
|
|
|
classe_resultat = "positif" if resultat >= 0 else "negatif"
|
|
|
|
|
|
|
|
|
|
|
|
groupe_nom = groupe.nom if groupe and groupe.nom else "Groupe"
|
|
|
|
|
|
district = str(groupe.district if groupe and groupe.district else "87-16")
|
|
|
|
|
|
region = str(groupe.region if groupe and groupe.region else "87")
|
|
|
|
|
|
|
|
|
|
|
|
html_content = f"""<!DOCTYPE html><html><head><style>{css}</style></head><body>
|
|
|
|
|
|
<div class="header">
|
|
|
|
|
|
<div class="groupe">{escape(groupe_nom)}</div>
|
|
|
|
|
|
<div class="sub">District {escape(district)} · Région {escape(region)}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<h1>État des résultats — {MOIS_FR[mois]} {annee}</h1>
|
|
|
|
|
|
|
|
|
|
|
|
<h2>Revenus</h2>
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<tr><td>Collectes 7e Tradition</td><td class="montant">{total_collectes:.2f} $</td></tr>
|
|
|
|
|
|
<tr><td>Ventes de littérature</td><td class="montant">{total_ventes:.2f} $</td></tr>
|
|
|
|
|
|
<tr><td>Dons déposés directement</td><td class="montant">{total_dons:.2f} $</td></tr>
|
|
|
|
|
|
<tr><td><strong>Total des revenus</strong></td><td class="montant"><strong>{revenus:.2f} $</strong></td></tr>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
|
|
|
|
<h2>Charges</h2>
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<tr><td>Dépenses payées</td><td class="montant">{total_depenses:.2f} $</td></tr>
|
|
|
|
|
|
<tr><td>Contributions envoyées</td><td class="montant">{total_contributions:.2f} $</td></tr>
|
|
|
|
|
|
<tr><td><strong>Total des charges</strong></td><td class="montant"><strong>{charges:.2f} $</strong></td></tr>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
|
|
|
|
<p class="total {classe_resultat}">{'Excédent' if resultat >= 0 else 'Déficit'} : {signe}{resultat:.2f} $</p>
|
|
|
|
|
|
|
|
|
|
|
|
<h2>Position de trésorerie</h2>
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<tr><td>Solde bancaire au {fin_mois.isoformat()}</td><td class="montant">{banque:.2f} $</td></tr>
|
|
|
|
|
|
<tr><td>Encaisse au {fin_mois.isoformat()}</td><td class="montant">{caisse:.2f} $</td></tr>
|
|
|
|
|
|
<tr><td>Engagements non débités</td><td class="montant">{engagements:.2f} $</td></tr>
|
|
|
|
|
|
<tr><td>Réserves</td><td class="montant">{total_reserves:.2f} $</td></tr>
|
|
|
|
|
|
<tr><td>Disponible au {fin_mois.isoformat()}</td><td class="montant">{disponible:.2f} $</td></tr>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
{f'<p class="note">Dernier relevé bancaire : {dernier_releve.date_operation}</p>' if dernier_releve else ''}
|
|
|
|
|
|
|
|
|
|
|
|
<h2>Réserves</h2>
|
|
|
|
|
|
<table>
|
|
|
|
|
|
{rows_reserves}
|
|
|
|
|
|
<tr><td><strong>Total des réserves</strong></td><td class="montant"><strong>{total_reserves:.2f} $</strong></td></tr>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
|
|
|
|
<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"etat_resultats_{groupe_nom.replace(' ', '_')}_{annee}_{mois:02d}.pdf"
|
|
|
|
|
|
return StreamingResponse(
|
|
|
|
|
|
io.BytesIO(pdf_bytes),
|
|
|
|
|
|
media_type="application/pdf",
|
|
|
|
|
|
headers={"Content-Disposition": f'inline; filename="{filename}"'},
|
|
|
|
|
|
)
|