233 lines
8.4 KiB
Python
233 lines
8.4 KiB
Python
"""Routes S2 — Remise de jeton."""
|
|
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.ownership import ensure_owned_or_404
|
|
from app.core.security import get_current_membre, is_sysadmin
|
|
from app.core.roles import membre_a_module
|
|
from app.models.jeton import Jeton
|
|
from app.models.vente_jeton import VenteJeton
|
|
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.models.reunion import Reunion
|
|
from app.schemas.phase5 import JetonCreate, JetonOut, TYPES_JETONS, VenteJetonCreate
|
|
|
|
router = APIRouter(prefix="/jetons", tags=["S2 — Remise de jeton"])
|
|
|
|
|
|
async def require_resp_jetons(membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db)):
|
|
"""Vérifie que le membre est responsable des jetons."""
|
|
if is_sysadmin(membre) or await membre_a_module(membre.id, "S2", db):
|
|
return membre
|
|
result = await db.execute(
|
|
select(Affectation).join(Poste).where(
|
|
Affectation.membre_id == membre.id,
|
|
Affectation.groupe_id == membre.groupe_id,
|
|
Affectation.statut == "actif",
|
|
Poste.groupe_id == membre.groupe_id,
|
|
(Poste.nom.ilike("%jeton%") | (Poste.categorie == "executif")),
|
|
)
|
|
)
|
|
if not result.scalars().first():
|
|
raise HTTPException(status_code=403, detail="Réservé au responsable des jetons")
|
|
return membre
|
|
|
|
|
|
@router.get("/types")
|
|
async def types_jetons():
|
|
"""Liste des types de jetons disponibles."""
|
|
return TYPES_JETONS
|
|
|
|
|
|
@router.post("/", response_model=JetonOut)
|
|
async def remettre_jeton(
|
|
req: JetonCreate,
|
|
membre=Depends(require_resp_jetons),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""S2 — Enregistrer la remise d'un jeton."""
|
|
# Validation
|
|
types_valides = [t["value"] for t in TYPES_JETONS]
|
|
if req.type_jeton not in types_valides:
|
|
raise HTTPException(status_code=400, detail=f"Type de jeton invalide. Valeurs: {types_valides}")
|
|
|
|
if req.type_jeton == "multiple" and (not req.annees or req.annees < 2):
|
|
raise HTTPException(status_code=400, detail="Le nombre d'années est requis (≥ 2) pour un jeton multiple")
|
|
|
|
# Vérifier que le membre existe si spécifié
|
|
membre_prenom = None
|
|
if req.membre_id:
|
|
cible = await db.get(Membre, req.membre_id)
|
|
if not cible or cible.groupe_id != membre.groupe_id or not cible.actif:
|
|
raise HTTPException(status_code=404, detail="Membre introuvable")
|
|
membre_prenom = cible.prenom
|
|
if req.reunion_id:
|
|
await ensure_owned_or_404(db, Reunion, req.reunion_id, membre.groupe_id, "Réunion introuvable")
|
|
|
|
jeton = Jeton(
|
|
groupe_id=membre.groupe_id,
|
|
reunion_id=req.reunion_id,
|
|
membre_id=req.membre_id,
|
|
type_jeton=req.type_jeton,
|
|
date_remise=req.date_remise,
|
|
annees=req.annees if req.type_jeton == "multiple" else None,
|
|
remis_par=membre.id,
|
|
)
|
|
db.add(jeton)
|
|
await db.flush()
|
|
|
|
return JetonOut(
|
|
id=jeton.id,
|
|
membre_prenom=membre_prenom,
|
|
type_jeton=jeton.type_jeton,
|
|
date_remise=jeton.date_remise,
|
|
annees=jeton.annees,
|
|
remis_par_prenom=membre.prenom,
|
|
cree_le=jeton.cree_le,
|
|
)
|
|
|
|
|
|
@router.post("/ventes")
|
|
async def enregistrer_vente_jeton(
|
|
req: VenteJetonCreate,
|
|
membre=Depends(require_resp_jetons),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Enregistrer une vente de jetons. Les remises ne touchent pas l'inventaire."""
|
|
types_valides = [t["value"] for t in TYPES_JETONS]
|
|
if req.type_jeton not in types_valides:
|
|
raise HTTPException(status_code=400, detail=f"Type de jeton invalide. Valeurs: {types_valides}")
|
|
if req.quantite <= 0:
|
|
raise HTTPException(status_code=400, detail="La quantité doit être positive")
|
|
if req.montant < 0:
|
|
raise HTTPException(status_code=400, detail="Le montant ne peut pas être négatif")
|
|
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")
|
|
if req.reunion_id:
|
|
await ensure_owned_or_404(db, Reunion, req.reunion_id, membre.groupe_id, "Réunion introuvable")
|
|
|
|
vente = VenteJeton(
|
|
groupe_id=membre.groupe_id,
|
|
reunion_id=req.reunion_id,
|
|
type_jeton=req.type_jeton,
|
|
quantite=req.quantite,
|
|
montant=req.montant,
|
|
date_vente=req.date_vente,
|
|
vendu_par=vendeur_id,
|
|
)
|
|
db.add(vente)
|
|
await db.flush()
|
|
return {
|
|
"id": str(vente.id),
|
|
"type_jeton": vente.type_jeton,
|
|
"quantite": vente.quantite,
|
|
"montant": float(vente.montant),
|
|
"date_vente": str(vente.date_vente),
|
|
"vendu_par_prenom": vendeur.prenom,
|
|
"remis_tresorier": vente.remis_tresorier,
|
|
}
|
|
|
|
|
|
@router.get("/ventes/a-recevoir")
|
|
async def ventes_jetons_a_recevoir(
|
|
membre=Depends(get_current_membre),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await db.execute(
|
|
select(VenteJeton)
|
|
.where(VenteJeton.groupe_id == membre.groupe_id, VenteJeton.remis_tresorier == False)
|
|
.order_by(desc(VenteJeton.date_vente))
|
|
)
|
|
out = []
|
|
for vente in result.scalars().all():
|
|
vendeur = await db.get(Membre, vente.vendu_par) if vente.vendu_par else None
|
|
out.append({
|
|
"id": str(vente.id),
|
|
"type_jeton": vente.type_jeton,
|
|
"quantite": vente.quantite,
|
|
"montant": float(vente.montant),
|
|
"date_vente": str(vente.date_vente),
|
|
"vendu_par_prenom": vendeur.prenom if vendeur else None,
|
|
})
|
|
return out
|
|
|
|
|
|
@router.post("/ventes/{vente_id}/recevoir")
|
|
async def confirmer_reception_vente_jeton(
|
|
vente_id: str,
|
|
membre=Depends(get_current_membre),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
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.groupe_id == membre.groupe_id,
|
|
Affectation.statut == "actif",
|
|
Poste.groupe_id == membre.groupe_id,
|
|
(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")
|
|
|
|
vente = await db.get(VenteJeton, vente_id)
|
|
if not vente or vente.groupe_id != membre.groupe_id:
|
|
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_jeton",
|
|
compte="encaisse",
|
|
montant=vente.montant,
|
|
sens="debit",
|
|
source_table="ventes_jetons",
|
|
source_id=vente.id,
|
|
note="Vente de jetons reçue",
|
|
cree_par=membre.id,
|
|
))
|
|
await db.flush()
|
|
return {"message": "Réception confirmée"}
|
|
|
|
|
|
@router.get("/", response_model=list[JetonOut])
|
|
async def liste_jetons(
|
|
limite: int = 30,
|
|
membre=Depends(get_current_membre),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Lister les jetons remis récemment (tout le groupe)."""
|
|
result = await db.execute(
|
|
select(Jeton)
|
|
.where(Jeton.groupe_id == membre.groupe_id)
|
|
.order_by(desc(Jeton.date_remise))
|
|
.limit(limite)
|
|
)
|
|
jetons = result.scalars().all()
|
|
out = []
|
|
for j in jetons:
|
|
m = await db.get(Membre, j.membre_id) if j.membre_id else None
|
|
r = await db.get(Membre, j.remis_par) if j.remis_par else None
|
|
out.append(JetonOut(
|
|
id=j.id,
|
|
membre_prenom=m.prenom if m else None,
|
|
type_jeton=j.type_jeton,
|
|
date_remise=j.date_remise,
|
|
annees=j.annees,
|
|
remis_par_prenom=r.prenom if r else None,
|
|
cree_le=j.cree_le,
|
|
))
|
|
return out
|