Files
AdamThomasandClaude Opus 5.5 ccd769d04e Numéro d'ordre unique sur chaque reçu fiscal mécénat
Volunteer.mecenat_receipt_number (migration 0020, unique) : année du
paiement + rang sur 5 chiffres (202600001…), une seule numérotation pour
toutes les éditions, repartant à 1 chaque année. Attribué à la
confirmation du paiement, ou au premier export pour les bénévoles payés
avant (dans l'ordre des paiements), puis jamais réattribué — un reçu
regénéré garde son numéro. Attribution sérialisée par un verrou
pg_advisory_xact_lock (webhook et sondage peuvent confirmer deux
paiements à la fois). Imprimé centré dans la case « Numéro d'ordre du
reçu » en page 1 du cerfa.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:48:05 +02:00

57 lines
2.4 KiB
Python

"""Passage d'un `Volunteer` à un `MecenatReceipt` — partagé par l'export
coordination (`app/api/volunteers.py:export_mecenat_receipts`) et l'envoi du
reçu au bénévole à la confirmation de son paiement (`app/api/forms.py:
_promote_pending`) — et attribution de son numéro d'ordre."""
from sqlalchemy import func, text
from sqlmodel import Session, select
from app.mecenat.pdf import MecenatReceipt
from app.models import Volunteer
RECEIPT_FILENAME = "recu-fiscal-mecenat.pdf"
# Verrou applicatif PostgreSQL (`pg_advisory_xact_lock`) sérialisant
# l'attribution des numéros : le webhook SumUp et le sondage du navigateur
# peuvent confirmer deux paiements en même temps — sans lui, les deux
# liraient le même maximum et se disputeraient le même numéro. Libéré
# automatiquement à la fin de la transaction. Valeur arbitraire, propre à
# cet usage.
_NUMBERING_LOCK = 11580_05
def assign_receipt_number(session: Session, volunteer: Volunteer) -> None:
"""Attribue (sans commit) le prochain numéro de l'année du paiement :
`AAAA` + rang sur 5 chiffres, à la suite du plus grand déjà attribué
cette année-là, toutes éditions confondues (une seule numérotation pour
l'association). Ne fait rien si le bénévole en a déjà un — un numéro
n'est jamais réattribué."""
if volunteer.mecenat_receipt_number:
return
session.exec(text("SELECT pg_advisory_xact_lock(:key)").bindparams(key=_NUMBERING_LOCK))
year = (volunteer.fee_paid_at or volunteer.created_at).year
prefix = str(year)
last = session.exec(
select(func.max(Volunteer.mecenat_receipt_number)).where(
Volunteer.mecenat_receipt_number.startswith(prefix)
)
).one()
rank = int(last[len(prefix):]) + 1 if last else 1
volunteer.mecenat_receipt_number = f"{prefix}{rank:05d}"
session.add(volunteer)
session.flush()
def receipt_for(volunteer: Volunteer) -> MecenatReceipt:
return MecenatReceipt(
last_name=volunteer.last_name,
first_name=volunteer.first_name,
address=volunteer.mecenat_address or "",
postal_code=volunteer.mecenat_postal_code or "",
city=volunteer.mecenat_city or "",
country=volunteer.mecenat_country or "",
amount=volunteer.fee_amount or 0.0,
payment_date=(volunteer.fee_paid_at or volunteer.created_at).date(),
number=volunteer.mecenat_receipt_number or "",
)