Files
benevoles/backend/app/exporting/xlsx.py
T
AdamThomasandClaude Opus 5.5 ff57bbd863
ci-cd / build-backend (push) Successful in 31s
ci-cd / build-frontend (push) Successful in 18s
ci-cd / deploy (push) Successful in 12s
Mode de paiement de la cotisation visible (en ligne, sur place)
Jusqu'ici seul un paiement en ligne apparaissait ; un bénévole qui paiera
sur place ne se distinguait pas d'un bénévole sans cotisation. Mode déduit
des champs existants (lib/fee.ts:feeMode, exporting/xlsx.py:_payment_mode),
sans migration :
- fiche : section « Cotisation » toujours visible — paiement en ligne
  (type, montant, statut, mécénat et adresse du reçu) ou case « Paiera sur
  place » modifiable à la main
- liste : colonne « Cotisation » (Sur place / En ligne ✓ / Mécénat ✓ / —)
- export Excel : colonne « Mode de paiement »

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 18:47:06 +02:00

138 lines
4.4 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",
"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.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()