groupe-meditation/backend/app/routers/ventes_litterature.py
Daniel Allaire 69309acd13 Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.

Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.

Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.

Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00

204 lines
7.1 KiB
Python

"""Routes S3 — Vente de littérature."""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc
from app.core.database import get_db
from app.core.security import get_current_membre, is_sysadmin
from app.core.roles import membre_a_module
from app.models.litterature import Litterature
from app.models.vente_litterature import VenteLitterature
from app.models.membre import Membre
from app.models.affectation import Affectation
from app.models.poste import Poste
from app.models.transaction_comptable import TransactionComptable
from app.schemas.phase6 import VenteCreate, VenteOut
router = APIRouter(prefix="/ventes-litterature", tags=["S3 — Vente littérature"])
async def require_resp_litterature(membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db)):
"""Vérifie que le membre est responsable de la littérature."""
if is_sysadmin(membre) or await membre_a_module(membre.id, "S3", db) or await membre_a_module(membre.id, "M5", db):
return membre
result = await db.execute(
select(Affectation).join(Poste).where(
Affectation.membre_id == membre.id,
Affectation.statut == "actif",
(Poste.nom.ilike("%littérature%") | (Poste.categorie == "executif")),
)
)
if not result.scalars().first():
raise HTTPException(status_code=403, detail="Réservé au responsable de la littérature")
return membre
@router.post("/", response_model=VenteOut)
async def enregistrer_vente(
req: VenteCreate,
membre=Depends(require_resp_litterature),
db: AsyncSession = Depends(get_db),
):
"""S3 — Enregistrer une vente de littérature. Le stock est décrémenté automatiquement."""
# Vérifier le titre
result = await db.execute(
select(Litterature).where(
Litterature.id == req.litterature_id,
Litterature.groupe_id == membre.groupe_id,
)
)
item = result.scalar_one_or_none()
if not item:
raise HTTPException(status_code=404, detail="Titre introuvable")
if req.quantite <= 0:
raise HTTPException(status_code=400, detail="La quantité doit être positive")
if item.quantite_stock < req.quantite:
raise HTTPException(
status_code=400,
detail=f"Stock insuffisant ({item.quantite_stock} disponible, {req.quantite} demandé)"
)
# Décrémenter le stock
item.quantite_stock -= req.quantite
vendeur_id = req.vendu_par_id or membre.id
vendeur = await db.get(Membre, vendeur_id)
if not vendeur or vendeur.groupe_id != membre.groupe_id or not vendeur.actif:
raise HTTPException(status_code=404, detail="Vendeur introuvable")
# Créer la vente
vente = VenteLitterature(
litterature_id=req.litterature_id,
groupe_id=membre.groupe_id,
reunion_id=req.reunion_id,
quantite=req.quantite,
montant=req.montant,
date_vente=req.date_vente,
vendu_par=vendeur_id,
)
db.add(vente)
await db.flush()
return VenteOut(
id=vente.id,
litterature_titre=item.titre,
quantite=vente.quantite,
montant=float(vente.montant),
date_vente=vente.date_vente,
vendu_par_prenom=vendeur.prenom,
cree_le=vente.cree_le,
)
@router.get("/", response_model=list[VenteOut])
async def liste_ventes(
limite: int = 20,
membre=Depends(get_current_membre),
db: AsyncSession = Depends(get_db),
):
"""Lister les ventes récentes."""
result = await db.execute(
select(VenteLitterature)
.where(VenteLitterature.groupe_id == membre.groupe_id)
.order_by(desc(VenteLitterature.date_vente))
.limit(limite)
)
ventes = result.scalars().all()
out = []
for v in ventes:
item = await db.get(Litterature, v.litterature_id)
vendeur = await db.get(Membre, v.vendu_par) if v.vendu_par else None
out.append(VenteOut(
id=v.id,
litterature_titre=item.titre if item else "?",
quantite=v.quantite,
montant=float(v.montant),
date_vente=v.date_vente,
vendu_par_prenom=vendeur.prenom if vendeur else None,
remis_tresorier=v.remis_tresorier,
cree_le=v.cree_le,
))
return out
@router.get("/a-recevoir")
async def ventes_a_recevoir(
membre=Depends(get_current_membre),
db: AsyncSession = Depends(get_db),
):
"""Ventes dont le montant n'a pas encore été remis au trésorier."""
result = await db.execute(
select(VenteLitterature)
.where(
VenteLitterature.groupe_id == membre.groupe_id,
VenteLitterature.remis_tresorier == False,
)
.order_by(desc(VenteLitterature.date_vente))
)
ventes = result.scalars().all()
out = []
for v in ventes:
item = await db.get(Litterature, v.litterature_id)
vendeur = await db.get(Membre, v.vendu_par) if v.vendu_par else None
out.append({
"id": str(v.id),
"litterature_titre": item.titre if item else "?",
"quantite": v.quantite,
"montant": float(v.montant),
"date_vente": str(v.date_vente),
"vendu_par_prenom": vendeur.prenom if vendeur else None,
})
return out
@router.post("/{vente_id}/recevoir")
async def confirmer_reception_vente(
vente_id: str,
membre=Depends(get_current_membre),
db: AsyncSession = Depends(get_db),
):
"""Le trésorier confirme la réception du montant de la vente."""
# Vérifier que c'est le trésorier ou exécutif
if not is_sysadmin(membre) and not await membre_a_module(membre.id, "S1T", db):
result = await db.execute(
select(Affectation).join(Poste).where(
Affectation.membre_id == membre.id,
Affectation.statut == "actif",
(Poste.nom.ilike("%trésorier%") | (Poste.categorie == "executif")),
)
)
if not result.scalars().first():
raise HTTPException(status_code=403, detail="Réservé au trésorier ou à l'exécutif")
result = await db.execute(
select(VenteLitterature).where(
VenteLitterature.id == vente_id,
VenteLitterature.groupe_id == membre.groupe_id,
)
)
vente = result.scalar_one_or_none()
if not vente:
raise HTTPException(status_code=404, detail="Vente introuvable")
if vente.remis_tresorier:
raise HTTPException(status_code=400, detail="Déjà reçue")
from datetime import date as date_type
vente.remis_tresorier = True
vente.recu_par = membre.id
vente.date_remise = date_type.today()
db.add(TransactionComptable(
groupe_id=vente.groupe_id,
date_transaction=vente.date_vente,
type="vente_litterature",
compte="encaisse",
montant=vente.montant,
sens="debit",
source_table="ventes_litterature",
source_id=vente.id,
note="Vente de littérature reçue",
cree_par=membre.id,
))
await db.flush()
return {"message": "Réception confirmée"}