Champ « Photo du bénévole » (PHOTO) : photo du formulaire recadrée au cadre, orientation EXIF appliquée, réduite à 300 dpi ; cadre vide sans photo. Champ « Texte libre » (FREE_TEXT) alimenté par le nouveau « Texte du badge » de la fiche bénévole (Volunteer.badge_text, migration 0024), repris dans l'export Excel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
144 lines
4.6 KiB
Python
144 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",
|
|
"Texte du badge",
|
|
"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 ""),
|
|
_safe_text(v.badge_text 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()
|