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.
204 lines
7.1 KiB
Python
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"}
|