Files
benevoles/backend/app/exporting/xlsx.py
T
AdamThomasandClaude Opus 5.5 d0fa1e109c
ci-cd / build-backend (push) Successful in 38s
ci-cd / build-frontend (push) Successful in 20s
ci-cd / deploy (push) Successful in 13s
Logs de diagnostic et rapprochement SumUp ↔ bénévole
- Une ligne de log par requête (route modèle, statut, durée, code d'erreur,
  compte), identifiant X-Request-ID repris dans chaque log et affiché à
  l'utilisateur sous une erreur, exceptions non gérées tracées (500 JSON)
- Journal d'accès d'uvicorn coupé : il exposait le jeton du formulaire public
- Logs métier : paiement SumUp, connexion, import, plannings, solveur
- Nom du bénévole dans la description du checkout SumUp
- Code et identifiant de transaction SumUp enregistrés au paiement
  (migration 0023), affichés sur la fiche et dans l'export Excel

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:54:35 +02:00

142 lines
4.6 KiB
Python

"""Export Excel de la liste des bénévoles d'une édition — voir
`app/api/volunteers.py:export_volunteers`. Même bibliothèque que l'import
(`openpyxl`, `app/importing/parser.py`), en écriture cette fois.
Texte en dur (en-têtes de colonnes, libellés) : comme `app/sheets/pdf.py` et
`app/badges/pdf.py`, un document généré n'est pas un texte d'interface —
pas de `app.i18n.t()` ici, contrairement aux réponses de l'API."""
import io
import uuid
from openpyxl import Workbook
from openpyxl.styles import Font
from app.api.schemas import VolunteerRead
_HEADER = [
"Prénom",
"Nom",
"Email",
"Téléphone",
"Disponible",
"Postes souhaités",
"Artistes souhaités",
"Compagnons souhaités",
"Note de compagnon",
"Mode de paiement",
"Cotisation choisie",
"Montant (€)",
"Statut du paiement",
"Payée le",
"Transaction SumUp (code)",
"Transaction SumUp (identifiant)",
"Mécénat — adresse",
"Mécénat — ville",
"Mécénat — code postal",
"Mécénat — pays",
"Commentaire",
"Inscrit le",
]
_FEE_STATUS_LABELS = {
"PENDING": "En attente",
"PAID": "Payée",
"FAILED": "Échouée",
"EXPIRED": "Expirée",
}
# Caractères qui font d'une cellule Excel/LibreOffice une formule plutôt
# qu'un texte littéral — injection de formule (CWE-1236) : la plupart de ces
# champs viennent du formulaire public, sans authentification (nom, email,
# téléphone, commentaire, note de compagnon, adresse de mécénat…). Un
# préfixe `'` force l'interprétation en texte, sans changer la valeur
# affichée.
_FORMULA_PREFIXES = ("=", "+", "-", "@")
def _safe_text(value: str) -> str:
return f"'{value}" if value.startswith(_FORMULA_PREFIXES) else value
def _payment_mode(v: VolunteerRead) -> str:
"""En ligne (un checkout SumUp a été créé), sur place (« je participe à
la cotisation » sans paiement en ligne), ou rien — même règle que
`frontend/src/lib/fee.ts:feeMode`."""
if v.fee_payment_status is not None:
return "En ligne"
if v.willing_to_pay_fee:
return "Sur place"
return ""
def _yes_no(value: bool) -> str:
return "Oui" if value else "Non"
def _join_names(ids: list[uuid.UUID], names: dict[uuid.UUID, str]) -> str:
return _safe_text(", ".join(names[i] for i in ids if i in names))
def _fmt_dt(value) -> str:
return value.strftime("%d/%m/%Y %H:%M") if value else ""
def generate_volunteers_xlsx(
volunteers: list[VolunteerRead],
*,
position_names: dict[uuid.UUID, str],
artist_names: dict[uuid.UUID, str],
volunteer_names: dict[uuid.UUID, str],
) -> bytes:
"""`position_names`/`artist_names`/`volunteer_names` : cartes id → nom
construites une fois par l'appelant (pas de requête par bénévole) —
voir `app/api/volunteers.py:export_volunteers`."""
workbook = Workbook()
sheet = workbook.active
sheet.title = "Bénévoles"
sheet.append(_HEADER)
for cell in sheet[1]:
cell.font = Font(bold=True)
for v in volunteers:
sheet.append(
[
_safe_text(v.first_name),
_safe_text(v.last_name),
_safe_text(v.email or ""),
_safe_text(v.phone or ""),
_yes_no(v.self_declared_available),
_join_names(v.wished_position_ids, position_names),
_join_names(v.artist_ids, artist_names),
_join_names(v.wished_companion_ids, volunteer_names),
_safe_text(v.companion_wish_note or ""),
_payment_mode(v),
_safe_text(v.fee_payment_label or ""),
v.fee_amount if v.fee_amount is not None else "",
_FEE_STATUS_LABELS.get(v.fee_payment_status, ""),
_fmt_dt(v.fee_paid_at),
_safe_text(v.fee_transaction_code or ""),
_safe_text(v.fee_transaction_id or ""),
_safe_text(v.mecenat_address or ""),
_safe_text(v.mecenat_city or ""),
_safe_text(v.mecenat_postal_code or ""),
_safe_text(v.mecenat_country or ""),
_safe_text(v.comment or ""),
_fmt_dt(v.created_at),
]
)
for column_cells in sheet.columns:
length = max(
(len(str(c.value)) for c in column_cells if c.value is not None),
default=8,
)
sheet.column_dimensions[column_cells[0].column_letter].width = min(
max(length + 2, 10), 40
)
buffer = io.BytesIO()
workbook.save(buffer)
return buffer.getvalue()