- bouton Sysadmin public avec PIN - CRUD sysadmin des groupes - groupe demo protege et reinitialisable
796 lines
38 KiB
Python
796 lines
38 KiB
Python
"""Operations sysadmin de remise a zero et de donnees de demonstration."""
|
|
from __future__ import annotations
|
|
|
|
import csv
|
|
from datetime import date, datetime, time, timedelta, timezone
|
|
from pathlib import Path
|
|
|
|
from sqlalchemy import select, text
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.database import Base
|
|
from app.core.security import hash_pin
|
|
from app.models.affectation import Affectation
|
|
from app.models.candidature import Candidature
|
|
from app.models.collecte import Collecte
|
|
from app.models.config_repartition import ConfigRepartition
|
|
from app.models.depense import Depense
|
|
from app.models.destinataire_contribution import DestinataireContribution
|
|
from app.models.envoi_contribution import EnvoiContribution
|
|
from app.models.evenement import Evenement
|
|
from app.models.groupe import Groupe
|
|
from app.models.historique_modification import HistoriqueModification
|
|
from app.models.jeton import Jeton
|
|
from app.models.journal_action import JournalAction
|
|
from app.models.litterature import Litterature
|
|
from app.models.membre import Membre
|
|
from app.models.mouvement_reserve import MouvementReserve
|
|
from app.models.operation_bancaire import OperationBancaire
|
|
from app.models.poste import Poste
|
|
from app.models.poste_module import PosteModule
|
|
from app.models.presence import Presence
|
|
from app.models.proposition import Proposition
|
|
from app.models.pv_reunion import PvReunion
|
|
from app.models.rapport_adopte import RapportAdopte
|
|
from app.models.rapport_rsg import RapportRsg
|
|
from app.models.reserve import Reserve
|
|
from app.models.reunion import Reunion
|
|
from app.models.rotation import Rotation
|
|
from app.models.transaction_comptable import TransactionComptable
|
|
from app.models.vente_litterature import VenteLitterature
|
|
from app.models.vente_jeton import VenteJeton
|
|
|
|
|
|
POSTES = [
|
|
("RSG", "executif", "Représente le groupe auprès du district et rapporte les informations de service.", "2 ans de stabilité", 1, ["M3", "C2", "C5"]),
|
|
("RSG Substitut(e)", "executif", "Appuie le RSG et assure la relève lorsque nécessaire.", "1 an de stabilité", 2, ["M3", "C2", "C5"]),
|
|
("Trésorier(ière)", "executif", "Tient les registres, reçoit les sommes et présente les rapports mensuels.", "1 an de stabilité", 3, ["S1T", "S5", "M4", "G8"]),
|
|
("Secrétaire", "executif", "Prépare les procès-verbaux, les présences et la correspondance.", "6 mois de stabilité", 4, ["M0", "M1", "M2", "S0"]),
|
|
("Animateur(trice)", "service", "Anime les rencontres hebdomadaires.", "Disponibilité régulière", 5, ["S0"]),
|
|
("Responsable accueil", "physique", "Accueille les nouveaux et veille à l'atmosphère de la salle.", "Fiabilité", 6, ["C9"]),
|
|
("Responsable café", "physique", "Prépare le café et remet les petites dépenses au trésorier.", "Aucun", 7, ["S5"]),
|
|
("Responsable littérature", "service", "Maintient l'inventaire et remet les ventes au trésorier.", "Connaissance de la littérature", 8, ["S3", "C4", "M5", "M6"]),
|
|
("Responsable jetons", "service", "Prépare les jetons et les anniversaires.", "Aucun", 9, ["S2", "M5", "M6"]),
|
|
("Responsable clés", "physique", "Ouvre et ferme le local.", "Fiabilité", 10, []),
|
|
]
|
|
|
|
MEMBRES_DEMO = [
|
|
("Sysadmin", None, "sysadmin", "0000", "514-555-0000"),
|
|
("Claire", "B.", "claire.demo@example.test", "1111", "514-555-0101"),
|
|
("Marc", "L.", "marc.demo@example.test", "2222", "514-555-0102"),
|
|
("Nadia", "R.", "nadia.demo@example.test", "3333", "514-555-0103"),
|
|
("Simon", "T.", "simon.demo@example.test", "4444", "514-555-0104"),
|
|
("Julie", "P.", "julie.demo@example.test", "5555", "514-555-0105"),
|
|
("Karim", "A.", "karim.demo@example.test", "6666", "514-555-0106"),
|
|
("Mélanie", "S.", "melanie.demo@example.test", "7777", "514-555-0107"),
|
|
("Robert", "G.", "robert.demo@example.test", "8888", "514-555-0108"),
|
|
("Anne", "C.", "anne.demo@example.test", "9999", "514-555-0109"),
|
|
("Luc", "D.", "luc.demo@example.test", "1212", "514-555-0110"),
|
|
("Sophie", "M.", "sophie.demo@example.test", "3434", "514-555-0111"),
|
|
]
|
|
|
|
|
|
def _catalogue_litterature() -> list[dict]:
|
|
chemin = Path(__file__).resolve().parents[3] / "data" / "import" / "Liste-de-prix-rev.-Fevrier-2026.csv"
|
|
if chemin.exists():
|
|
with chemin.open(newline="", encoding="utf-8") as fichier:
|
|
return [
|
|
{
|
|
"code_bsg": row["code_bsg"].strip() or None,
|
|
"titre": row["titre"].strip(),
|
|
"categorie": row["categorie"].strip() or "brochure",
|
|
"prix_coutant": float(row["prix_coutant"]) if row["prix_coutant"].strip() else 0,
|
|
}
|
|
for row in csv.DictReader(fichier)
|
|
if row.get("titre", "").strip()
|
|
]
|
|
return [
|
|
{"code_bsg": "B-1", "titre": "Les Alcooliques anonymes", "categorie": "livre", "prix_coutant": 12.0},
|
|
{"code_bsg": "B-2", "titre": "Les Douze Étapes et les Douze Traditions", "categorie": "livre", "prix_coutant": 11.0},
|
|
{"code_bsg": "P-3", "titre": "Les AA sont-ils pour vous?", "categorie": "brochure", "prix_coutant": 0.0},
|
|
]
|
|
|
|
|
|
async def remettre_a_zero(db: AsyncSession) -> dict:
|
|
tables = ", ".join(table.name for table in reversed(Base.metadata.sorted_tables))
|
|
if tables:
|
|
await db.execute(text(f"TRUNCATE TABLE {tables} RESTART IDENTITY CASCADE"))
|
|
groupe, sysadmin, postes, litterature = await _creer_base(db)
|
|
await db.flush()
|
|
return {
|
|
"message": "Base remise à zéro",
|
|
"groupe_id": groupe.id,
|
|
"sysadmin_id": sysadmin.id,
|
|
"postes": len(postes),
|
|
"litterature": len(litterature),
|
|
}
|
|
|
|
|
|
async def initialiser_demo(db: AsyncSession) -> dict:
|
|
existant = (await db.execute(select(Groupe).where(Groupe.nom == "Groupe Résilience"))).scalar_one_or_none()
|
|
if existant:
|
|
membres_existants = await _membres_par_courriel(db, existant.id)
|
|
return {
|
|
"message": "Le groupe de démonstration existe déjà.",
|
|
"groupe_id": existant.id,
|
|
"membres": len(membres_existants),
|
|
"periode_mois": 12,
|
|
"rencontres": 52,
|
|
"assemblees": 12,
|
|
}
|
|
|
|
groupe, sysadmin, postes, litterature = await _creer_base(
|
|
db,
|
|
nom_groupe="Groupe Résilience",
|
|
adresse_local="Salle communautaire Saint-Louis, Montréal",
|
|
)
|
|
membres = {sysadmin.courriel: sysadmin}
|
|
|
|
await _ajouter_membres_demo(db, groupe, membres, postes)
|
|
membres = await _membres_par_courriel(db, groupe.id)
|
|
await _ajouter_historique_demo(db, groupe, membres, postes, litterature)
|
|
await db.flush()
|
|
return {
|
|
"message": "Groupe de démonstration installé sans modifier les autres groupes",
|
|
"groupe_id": groupe.id,
|
|
"membres": len(membres),
|
|
"periode_mois": 12,
|
|
"rencontres": 52,
|
|
"assemblees": 12,
|
|
}
|
|
|
|
|
|
async def reinitialiser_demo(db: AsyncSession) -> dict:
|
|
groupe = (await db.execute(select(Groupe).where(Groupe.nom == "Groupe Résilience"))).scalar_one_or_none()
|
|
if not groupe:
|
|
return await initialiser_demo(db)
|
|
await _vider_groupe(db, groupe.id)
|
|
sysadmin, postes, litterature = await _peupler_groupe_base(db, groupe)
|
|
membres = {sysadmin.courriel: sysadmin}
|
|
await _ajouter_membres_demo(db, groupe, membres, postes)
|
|
membres = await _membres_par_courriel(db, groupe.id)
|
|
await _ajouter_historique_demo(db, groupe, membres, postes, litterature)
|
|
await db.flush()
|
|
return {
|
|
"message": "Données fictives du groupe de démonstration réinitialisées",
|
|
"groupe_id": groupe.id,
|
|
"membres": len(membres),
|
|
"periode_mois": 12,
|
|
"rencontres": 52,
|
|
"assemblees": 12,
|
|
}
|
|
|
|
|
|
async def creer_groupe(db: AsyncSession, data: dict) -> Groupe:
|
|
nom = (data.get("nom") or "").strip()
|
|
if not nom:
|
|
raise ValueError("Nom requis")
|
|
if nom == "Groupe Résilience":
|
|
raise ValueError("Ce nom est réservé au groupe de démonstration")
|
|
groupe = Groupe(
|
|
nom=nom,
|
|
district=(data.get("district") or "87-16").strip(),
|
|
region=(data.get("region") or "87").strip(),
|
|
adresse_local=(data.get("adresse_local") or "").strip() or None,
|
|
jour_reunion=(data.get("jour_reunion") or "mardi").strip() or None,
|
|
heure_reunion=_parse_heure(data.get("heure_reunion")),
|
|
assemblee_affaires_ordre=(data.get("assemblee_affaires_ordre") or "premiere").strip(),
|
|
format_reunion=(data.get("format_reunion") or "Discussion ouverte").strip() or None,
|
|
lien_aa87=(data.get("lien_aa87") or "https://aa87.org").strip(),
|
|
)
|
|
db.add(groupe)
|
|
await db.flush()
|
|
await _peupler_groupe_base(db, groupe)
|
|
await db.flush()
|
|
return groupe
|
|
|
|
|
|
async def modifier_groupe(db: AsyncSession, groupe_id, data: dict) -> Groupe:
|
|
groupe = await db.get(Groupe, groupe_id)
|
|
if not groupe:
|
|
raise ValueError("Groupe introuvable")
|
|
nom = (data.get("nom") or groupe.nom).strip()
|
|
if groupe.nom == "Groupe Résilience" and nom != "Groupe Résilience":
|
|
raise ValueError("Le groupe de démonstration ne peut pas être renommé")
|
|
if nom == "Groupe Résilience" and groupe.nom != "Groupe Résilience":
|
|
raise ValueError("Ce nom est réservé au groupe de démonstration")
|
|
groupe.nom = nom
|
|
for attr in ["district", "region", "adresse_local", "jour_reunion", "assemblee_affaires_ordre", "format_reunion", "lien_aa87"]:
|
|
if attr in data:
|
|
valeur = data.get(attr)
|
|
setattr(groupe, attr, valeur.strip() if isinstance(valeur, str) and valeur.strip() else None)
|
|
if "heure_reunion" in data:
|
|
groupe.heure_reunion = _parse_heure(data.get("heure_reunion"))
|
|
await db.flush()
|
|
return groupe
|
|
|
|
|
|
async def supprimer_groupe(db: AsyncSession, groupe_id) -> dict:
|
|
groupe = await db.get(Groupe, groupe_id)
|
|
if not groupe:
|
|
raise ValueError("Groupe introuvable")
|
|
if groupe.nom == "Groupe Résilience":
|
|
raise ValueError("Le groupe de démonstration est protégé")
|
|
nb_groupes = (await db.execute(select(Groupe))).scalars().all()
|
|
if len(nb_groupes) <= 1:
|
|
raise ValueError("Il doit rester au moins un groupe")
|
|
await _vider_groupe(db, groupe.id)
|
|
await db.delete(groupe)
|
|
await db.flush()
|
|
return {"message": f"Groupe {groupe.nom} supprimé"}
|
|
|
|
|
|
async def _creer_base(
|
|
db: AsyncSession,
|
|
nom_groupe: str = "Groupe Méditation",
|
|
adresse_local: str = "Salle communautaire, Montréal",
|
|
):
|
|
groupe = Groupe(
|
|
nom=nom_groupe,
|
|
district="87-16",
|
|
region="87",
|
|
adresse_local=adresse_local,
|
|
jour_reunion="mardi",
|
|
heure_reunion=time(19, 30),
|
|
assemblee_affaires_ordre="premiere",
|
|
format_reunion="Discussion ouverte",
|
|
lien_aa87="https://aa87.org",
|
|
)
|
|
db.add(groupe)
|
|
await db.flush()
|
|
sysadmin, postes, litterature = await _peupler_groupe_base(db, groupe)
|
|
return groupe, sysadmin, postes, litterature
|
|
|
|
|
|
async def _peupler_groupe_base(db: AsyncSession, groupe: Groupe):
|
|
postes = {}
|
|
for nom, categorie, description, prerequis, ordre, modules in POSTES:
|
|
poste = Poste(
|
|
groupe_id=groupe.id,
|
|
nom=nom,
|
|
categorie=categorie,
|
|
description=description,
|
|
prerequis=prerequis,
|
|
ordre_affichage=ordre,
|
|
actif=True,
|
|
)
|
|
db.add(poste)
|
|
await db.flush()
|
|
postes[nom] = poste
|
|
for module_code in modules:
|
|
db.add(PosteModule(poste_id=poste.id, module_code=module_code))
|
|
|
|
sysadmin = Membre(
|
|
groupe_id=groupe.id,
|
|
prenom="Sysadmin",
|
|
nom=None,
|
|
telephone="514-555-0000",
|
|
courriel="sysadmin",
|
|
pin_hash=hash_pin("0000"),
|
|
actif=True,
|
|
)
|
|
db.add(sysadmin)
|
|
await db.flush()
|
|
|
|
litterature = []
|
|
for item in _catalogue_litterature():
|
|
lit = Litterature(
|
|
groupe_id=groupe.id,
|
|
titre=item["titre"][:200],
|
|
categorie=item["categorie"] if item["categorie"] in {"livre", "brochure", "depliant", "pamphlet"} else "brochure",
|
|
prix_coutant=item["prix_coutant"],
|
|
quantite_stock=8,
|
|
seuil_commande=2,
|
|
code_bsg=item["code_bsg"],
|
|
)
|
|
db.add(lit)
|
|
litterature.append(lit)
|
|
|
|
db.add(DestinataireContribution(groupe_id=groupe.id, nom="District 87-16", protege=True, actif=True))
|
|
db.add(DestinataireContribution(groupe_id=groupe.id, nom="Région 87", protege=False, actif=True))
|
|
db.add(DestinataireContribution(groupe_id=groupe.id, nom="Bureau des services généraux", protege=False, actif=True))
|
|
for destination, pct in [("district", 50), ("region", 20), ("bsg", 20), ("intergroupe", 10)]:
|
|
db.add(ConfigRepartition(groupe_id=groupe.id, destination=destination, pourcentage=pct, actif_depuis=date.today().replace(day=1)))
|
|
return sysadmin, postes, litterature
|
|
|
|
|
|
async def _ajouter_membres_demo(db: AsyncSession, groupe: Groupe, membres: dict, postes: dict) -> None:
|
|
for prenom, nom, courriel, pin, telephone in MEMBRES_DEMO[1:]:
|
|
membre = Membre(
|
|
groupe_id=groupe.id,
|
|
prenom=prenom,
|
|
nom=nom,
|
|
telephone=telephone,
|
|
courriel=courriel,
|
|
date_abstinence=date.today() - timedelta(days=365 * (2 + len(membres) % 12)),
|
|
pin_hash=hash_pin(pin),
|
|
actif=True,
|
|
)
|
|
db.add(membre)
|
|
await db.flush()
|
|
membres[courriel] = membre
|
|
|
|
affectations = [
|
|
("claire.demo@example.test", "RSG"),
|
|
("marc.demo@example.test", "RSG Substitut(e)"),
|
|
("nadia.demo@example.test", "Trésorier(ière)"),
|
|
("simon.demo@example.test", "Secrétaire"),
|
|
("julie.demo@example.test", "Animateur(trice)"),
|
|
("karim.demo@example.test", "Responsable littérature"),
|
|
("melanie.demo@example.test", "Responsable jetons"),
|
|
("robert.demo@example.test", "Responsable accueil"),
|
|
("anne.demo@example.test", "Responsable café"),
|
|
]
|
|
debut = date.today() - timedelta(days=240)
|
|
for courriel, poste_nom in affectations:
|
|
membre = membres.get(courriel)
|
|
if membre and poste_nom in postes:
|
|
db.add(Affectation(groupe_id=groupe.id, membre_id=membre.id, poste_id=postes[poste_nom].id, date_debut=debut, statut="actif"))
|
|
|
|
|
|
async def _ajouter_historique_demo(db: AsyncSession, groupe: Groupe, membres: dict, postes: dict, litterature: list[Litterature]) -> None:
|
|
tresorier = membres["nadia.demo@example.test"]
|
|
secretaire = membres["simon.demo@example.test"]
|
|
rsg = membres["claire.demo@example.test"]
|
|
membres_actifs = [m for m in membres.values() if m.courriel != "sysadmin"]
|
|
reserves = {
|
|
"Loyer": Reserve(groupe_id=groupe.id, nom="Loyer", montant=0, note="Prévoir trois mois de salle"),
|
|
"Littérature": Reserve(groupe_id=groupe.id, nom="Littérature", montant=0, note="Réapprovisionnement"),
|
|
"Prudentielle": Reserve(groupe_id=groupe.id, nom="Prudentielle", montant=0, note="Réserve de sécurité"),
|
|
}
|
|
for reserve in reserves.values():
|
|
db.add(reserve)
|
|
await db.flush()
|
|
|
|
debut = _premier_mardi(date.today() - timedelta(days=365))
|
|
dates_reunions = [debut + timedelta(days=7 * i) for i in range(52)]
|
|
mois_vus = set()
|
|
banque = 0.0
|
|
encaisse = 0.0
|
|
compteurs = {"RES": 0, "NOM": 0, "DON": 0}
|
|
rotations_postes = ["Animateur(trice)", "Responsable accueil", "Responsable café", "Responsable clés"]
|
|
|
|
for index, jour in enumerate(dates_reunions):
|
|
type_reunion = "affaires" if jour.month not in mois_vus else "reguliere"
|
|
mois_vus.add(jour.month)
|
|
reunion = Reunion(
|
|
groupe_id=groupe.id,
|
|
type=type_reunion,
|
|
date_reunion=datetime.combine(jour, groupe.heure_reunion or time(19, 30)).replace(tzinfo=timezone.utc),
|
|
animateur_id=membres_actifs[index % len(membres_actifs)].id,
|
|
theme=["Acceptation", "Service", "Gratitude", "Honnêteté"][index % 4],
|
|
annonces={"district": index % 5 == 0, "anniversaire": index % 8 == 0},
|
|
note="Réunion de démonstration réaliste.",
|
|
cree_le=_moment(jour, 18, 30),
|
|
modifie_le=_moment(jour, 18, 30),
|
|
)
|
|
db.add(reunion)
|
|
await db.flush()
|
|
await _trace(
|
|
db, groupe, secretaire, jour, "reunions", "reunion.planifiee",
|
|
f"{'Assemblée d affaires' if type_reunion == 'affaires' else 'Rencontre hebdomadaire'} planifiée.",
|
|
reunion.id,
|
|
apres={"date": jour.isoformat(), "type": type_reunion},
|
|
chemin="/reunions" if type_reunion == "reguliere" else "/presences/reunion",
|
|
)
|
|
|
|
for decalage, poste_nom in enumerate(rotations_postes):
|
|
poste = postes.get(poste_nom)
|
|
if not poste:
|
|
continue
|
|
serviteur = membres_actifs[(index + decalage) % len(membres_actifs)]
|
|
rotation = Rotation(
|
|
groupe_id=groupe.id,
|
|
date_reunion=jour,
|
|
poste_id=poste.id,
|
|
membre_id=serviteur.id,
|
|
note="Rotation planifiée automatiquement pour la démonstration.",
|
|
cree_par=secretaire.id,
|
|
cree_le=_moment(jour, 18, 35),
|
|
)
|
|
db.add(rotation)
|
|
await db.flush()
|
|
await _trace(
|
|
db, groupe, secretaire, jour, "rotations", "rotations.planifiees",
|
|
"Rotations hebdomadaires planifiées.",
|
|
reunion.id,
|
|
apres={"postes": rotations_postes},
|
|
chemin="/rotations/batch",
|
|
)
|
|
|
|
for membre in membres_actifs:
|
|
db.add(Presence(reunion_id=reunion.id, membre_id=membre.id, present=(index + len(membre.prenom)) % 7 != 0))
|
|
await _trace(
|
|
db, groupe, secretaire, jour, "presences", "presences.enregistrees",
|
|
"Présences de la rencontre enregistrées.",
|
|
reunion.id,
|
|
apres={"presents": sum(1 for m in membres_actifs if (index + len(m.prenom)) % 7 != 0)},
|
|
chemin="/presences/",
|
|
)
|
|
|
|
montant_collecte = round(82 + (index % 6) * 9 + (index % 3) * 4, 2)
|
|
collecte = Collecte(
|
|
groupe_id=groupe.id,
|
|
date_collecte=jour,
|
|
montant=montant_collecte,
|
|
reunion_id=reunion.id,
|
|
detenteur_id=membres_actifs[index % len(membres_actifs)].id,
|
|
statut="recu",
|
|
saisi_par=secretaire.id,
|
|
confirme_par=secretaire.id,
|
|
date_confirmation=datetime.combine(jour, time(21, 15)).replace(tzinfo=timezone.utc),
|
|
recu_par=tresorier.id,
|
|
date_reception=jour,
|
|
cree_le=_moment(jour, 20, 55),
|
|
modifie_le=_moment(jour, 21, 5),
|
|
)
|
|
db.add(collecte)
|
|
await db.flush()
|
|
encaisse += montant_collecte
|
|
db.add(TransactionComptable(groupe_id=groupe.id, date_transaction=jour, type="collecte", compte="encaisse", montant=montant_collecte, sens="debit", source_table="collectes", source_id=collecte.id, note="Collecte 7e Tradition", cree_par=tresorier.id))
|
|
await _trace(
|
|
db, groupe, tresorier, jour, "collectes", "collecte.recue",
|
|
"Collecte saisie puis reçue par le trésorier.",
|
|
collecte.id,
|
|
apres={"montant": montant_collecte, "statut": "recu"},
|
|
chemin="/collectes",
|
|
)
|
|
|
|
if index % 4 == 0:
|
|
don = 40 + (index % 5) * 10
|
|
banque += don
|
|
db.add(TransactionComptable(groupe_id=groupe.id, date_transaction=jour, type="don", compte="banque", montant=don, sens="debit", note="Don direct au compte bancaire", cree_par=tresorier.id))
|
|
await _trace(
|
|
db, groupe, tresorier, jour, "transactions_comptables", "don.direct",
|
|
"Don reçu directement au compte bancaire.",
|
|
reunion.id,
|
|
apres={"montant": don, "compte": "banque"},
|
|
chemin="/operations-bancaires/don",
|
|
)
|
|
|
|
if index % 4 == 3:
|
|
depot = round(encaisse - 25, 2) if encaisse > 50 else round(encaisse, 2)
|
|
if depot > 0:
|
|
encaisse -= depot
|
|
banque += depot
|
|
db.add(OperationBancaire(groupe_id=groupe.id, type="depot", montant=depot, date_operation=jour, effectue_par=tresorier.id, note="Dépôt mensuel de l'encaisse"))
|
|
db.add(TransactionComptable(groupe_id=groupe.id, date_transaction=jour, type="banque_depot", compte="banque", montant=depot, sens="debit", note="Dépôt de l'encaisse", cree_par=tresorier.id))
|
|
db.add(TransactionComptable(groupe_id=groupe.id, date_transaction=jour, type="banque_depot", compte="encaisse", montant=depot, sens="credit", note="Dépôt de l'encaisse", cree_par=tresorier.id))
|
|
await _trace(
|
|
db, groupe, tresorier, jour, "operations_bancaires", "encaisse.deposee",
|
|
"Dépôt de l'encaisse au compte bancaire.",
|
|
collecte.id,
|
|
avant={"encaisse": round(encaisse + depot, 2), "banque": round(banque - depot, 2)},
|
|
apres={"encaisse": round(encaisse, 2), "banque": round(banque, 2)},
|
|
chemin="/operations-bancaires",
|
|
)
|
|
|
|
if type_reunion == "affaires":
|
|
await _decision(db, groupe, reunion, compteurs, "RES", jour, secretaire, rsg, "Adopter l'ordre du jour", "L'assemblée doit traiter les affaires courantes.", "L'ordre du jour est adopté tel que présenté.")
|
|
await _decision(db, groupe, reunion, compteurs, "RES", jour, rsg, secretaire, f"Orientation du mois {jour.month:02d}", "Le groupe souhaite garder une conscience de groupe active.", "Le groupe retient les priorités présentées pour le mois.")
|
|
pv = PvReunion(
|
|
reunion_id=reunion.id,
|
|
redacteur_id=secretaire.id,
|
|
contenu=f"Procès-verbal de l'assemblée du {jour.isoformat()}.\nPrésences vérifiées. Rapports reçus. Décisions consignées.",
|
|
decisions=[{"sujet": "Adoption du procès-verbal précédent", "decision": "Adopté", "proposeur": rsg.prenom, "secondeur": secretaire.prenom, "statut": "complete"}],
|
|
approuve=index > 4,
|
|
)
|
|
db.add(pv)
|
|
db.add(RapportAdopte(
|
|
groupe_id=groupe.id,
|
|
adopte_par_id=tresorier.id,
|
|
type_rapport="tresorerie",
|
|
titre=f"Rapport du trésorier — {jour.strftime('%Y-%m')}",
|
|
annee=jour.year,
|
|
mois=jour.month,
|
|
contenu_html=f"<html><body><h1>Rapport du trésorier</h1><p>Rapport adopté pour {jour.strftime('%Y-%m')}.</p><p>Banque: {banque:.2f} $</p><p>Encaisse: {encaisse:.2f} $</p></body></html>",
|
|
donnees={"banque": round(banque, 2), "encaisse": round(encaisse, 2)},
|
|
adopte_le=_moment(jour, 21, 35),
|
|
))
|
|
await _trace(
|
|
db, groupe, secretaire, jour, "pv_reunions", "pv.redige",
|
|
"Procès-verbal d'assemblée rédigé et classé.",
|
|
pv.id,
|
|
apres={"approuve": pv.approuve},
|
|
chemin="/pv/",
|
|
)
|
|
await _trace(
|
|
db, groupe, tresorier, jour, "rapports_adoptes", "rapport_tresorier.adopte",
|
|
"Rapport du trésorier adopté par la conscience de groupe.",
|
|
reunion.id,
|
|
apres={"annee": jour.year, "mois": jour.month, "banque": round(banque, 2), "encaisse": round(encaisse, 2)},
|
|
chemin="/rapports/etat-resultats/adopter",
|
|
)
|
|
|
|
if index % 6 == 1:
|
|
montant = 32 + (index % 4) * 6
|
|
methode = "COLLECTE" if encaisse >= montant else "VIREMENT"
|
|
if methode == "COLLECTE":
|
|
encaisse -= montant
|
|
compte = "encaisse"
|
|
else:
|
|
banque -= montant
|
|
compte = "banque"
|
|
depense = Depense(groupe_id=groupe.id, date_depense=jour, montant=montant, qui=membres_actifs[index % len(membres_actifs)].id, quoi=["Café et lait", "Papeterie", "Location de salle", "Fournitures accueil"][index % 4], paye=True, methode=methode, saisi_par=membres_actifs[index % len(membres_actifs)].id, confirme_par=tresorier.id, date_confirmation=datetime.combine(jour, time(20, 45)).replace(tzinfo=timezone.utc))
|
|
db.add(depense)
|
|
await db.flush()
|
|
db.add(TransactionComptable(groupe_id=groupe.id, date_transaction=jour, type="depense", compte=compte, montant=montant, sens="credit", source_table="depenses", source_id=depense.id, note=depense.quoi, cree_par=tresorier.id))
|
|
await _trace(
|
|
db, groupe, tresorier, jour, "depenses", "depense.confirmee",
|
|
"Dépense soumise puis confirmée par le trésorier.",
|
|
depense.id,
|
|
apres={"montant": montant, "methode": methode, "quoi": depense.quoi},
|
|
chemin="/depenses",
|
|
)
|
|
|
|
if index % 8 == 2 and litterature:
|
|
item = litterature[index % len(litterature)]
|
|
montant = float(item.prix_coutant or 2) * 2
|
|
item.quantite_stock = max(0, item.quantite_stock - 2)
|
|
vente = VenteLitterature(groupe_id=groupe.id, reunion_id=reunion.id, litterature_id=item.id, quantite=2, montant=montant, date_vente=jour, vendu_par=membres["karim.demo@example.test"].id, remis_tresorier=True, recu_par=tresorier.id, date_remise=jour)
|
|
db.add(vente)
|
|
await db.flush()
|
|
encaisse += montant
|
|
db.add(TransactionComptable(groupe_id=groupe.id, date_transaction=jour, type="vente_litterature", compte="encaisse", montant=montant, sens="debit", source_table="ventes_litterature", source_id=vente.id, note="Vente de littérature", cree_par=tresorier.id))
|
|
await _trace(
|
|
db, groupe, membres["karim.demo@example.test"], jour, "ventes_litterature", "vente_litterature.recue",
|
|
"Vente de littérature enregistrée et remise au trésorier.",
|
|
vente.id,
|
|
apres={"titre": item.titre, "quantite": 2, "montant": montant},
|
|
chemin="/ventes-litterature",
|
|
)
|
|
|
|
if index % 12 == 6:
|
|
montant = 6
|
|
vente_jeton = VenteJeton(
|
|
groupe_id=groupe.id,
|
|
reunion_id=reunion.id,
|
|
type_jeton=["desir", "1mois", "1an"][index % 3],
|
|
quantite=2,
|
|
montant=montant,
|
|
date_vente=jour,
|
|
vendu_par=membres["melanie.demo@example.test"].id,
|
|
remis_tresorier=True,
|
|
recu_par=tresorier.id,
|
|
date_remise=jour,
|
|
)
|
|
db.add(vente_jeton)
|
|
await db.flush()
|
|
encaisse += montant
|
|
db.add(TransactionComptable(groupe_id=groupe.id, date_transaction=jour, type="vente_jeton", compte="encaisse", montant=montant, sens="debit", source_table="ventes_jetons", source_id=vente_jeton.id, note="Vente de jetons", cree_par=tresorier.id))
|
|
await _trace(
|
|
db, groupe, membres["melanie.demo@example.test"], jour, "ventes_jetons", "vente_jeton.recue",
|
|
"Vente de jetons enregistrée et remise au trésorier.",
|
|
vente_jeton.id,
|
|
apres={"type_jeton": vente_jeton.type_jeton, "quantite": 2, "montant": montant},
|
|
chemin="/jetons/ventes",
|
|
)
|
|
|
|
if index % 13 == 4:
|
|
montant = 125
|
|
if banque >= montant:
|
|
banque -= montant
|
|
decision = await _decision(db, groupe, reunion, compteurs, "DON", jour, rsg, secretaire, "Contribution au district", "La conscience de groupe prévoit soutenir les services.", "Le groupe décide d'envoyer une contribution au District 87-16.")
|
|
contribution = EnvoiContribution(groupe_id=groupe.id, date_envoi=jour, destination="District 87-16", montant=montant, methode="VIREMENT", resolution_id=decision.id, engagee=True, debitee=True, saisi_par=tresorier.id)
|
|
db.add(contribution)
|
|
await db.flush()
|
|
db.add(TransactionComptable(groupe_id=groupe.id, date_transaction=jour, type="contribution", compte="banque", montant=montant, sens="credit", source_table="envois_contributions", source_id=contribution.id, note="Contribution au district", cree_par=tresorier.id))
|
|
await _trace(
|
|
db, groupe, tresorier, jour, "envois_contributions", "contribution.debitee",
|
|
"Contribution décidée, engagée et débitée.",
|
|
contribution.id,
|
|
apres={"destination": contribution.destination, "montant": montant},
|
|
chemin="/contributions",
|
|
)
|
|
|
|
if index in {9, 22, 35}:
|
|
candidat = membres_actifs[index % len(membres_actifs)]
|
|
poste = postes[["Responsable accueil", "Responsable café", "Responsable clés"][index % 3]]
|
|
candidature = Candidature(groupe_id=groupe.id, poste_id=poste.id, membre_id=candidat.id, type="nomination", statut="acceptee", motivation="Disponibilité confirmée.", nomme_par=rsg.id, acceptee_par_membre=True)
|
|
db.add(candidature)
|
|
await db.flush()
|
|
decision = await _decision(db, groupe, reunion, compteurs, "NOM", jour, rsg, secretaire, f"Nomination de {candidat.prenom}", "Un poste doit être pourvu pour soutenir le groupe.", f"Le groupe nomme {candidat.prenom} au poste de {poste.nom}.")
|
|
decision.candidature_id = candidature.id
|
|
candidature.proposition_id = decision.id
|
|
db.add(Affectation(groupe_id=groupe.id, membre_id=candidat.id, poste_id=poste.id, date_debut=jour, statut="actif"))
|
|
await _trace(
|
|
db, groupe, rsg, jour, "candidatures", "nomination.adoptee",
|
|
"Nomination proposée, secondée et adoptée.",
|
|
candidature.id,
|
|
apres={"membre": candidat.prenom, "poste": poste.nom, "resolution": decision.numero_resolution},
|
|
chemin="/postes/candidatures",
|
|
)
|
|
|
|
if index % 10 == 5:
|
|
jeton = Jeton(groupe_id=groupe.id, reunion_id=reunion.id, membre_id=membres_actifs[index % len(membres_actifs)].id, type_jeton="multiple", date_remise=jour, annees=1 + index % 5, remis_par=membres["melanie.demo@example.test"].id, cree_le=_moment(jour, 20, 5))
|
|
db.add(jeton)
|
|
await db.flush()
|
|
await _trace(
|
|
db, groupe, membres["melanie.demo@example.test"], jour, "jetons", "jeton.remis",
|
|
"Remise de jeton enregistrée pour statistiques.",
|
|
jeton.id,
|
|
apres={"annees": jeton.annees},
|
|
chemin="/jetons",
|
|
)
|
|
|
|
if index % 17 == 7:
|
|
reserve = reserves[["Loyer", "Littérature", "Prudentielle"][index % 3]]
|
|
montant = 75
|
|
if banque >= montant:
|
|
reserve.montant = float(reserve.montant or 0) + montant
|
|
db.add(MouvementReserve(groupe_id=groupe.id, source_id=None, destination_id=reserve.id, montant=montant, type="reserve_virement", note="Affectation au solde réservé", cree_par=tresorier.id, cree_le=datetime.combine(jour, time(21, 0)).replace(tzinfo=timezone.utc)))
|
|
await _trace(
|
|
db, groupe, tresorier, jour, "mouvements_reserves", "reserve.transfert",
|
|
"Montant transféré du disponible vers une réserve.",
|
|
reserve.id,
|
|
avant={"reserve": round(float(reserve.montant or 0) - montant, 2)},
|
|
apres={"reserve": round(float(reserve.montant or 0), 2), "montant": montant},
|
|
chemin="/operations-bancaires/reserves/transfert",
|
|
)
|
|
|
|
db.add(Collecte(groupe_id=groupe.id, date_collecte=dates_reunions[-1] + timedelta(days=7), montant=96, detenteur_id=membres["luc.demo@example.test"].id, statut="en_attente", saisi_par=membres["luc.demo@example.test"].id))
|
|
db.add(Depense(groupe_id=groupe.id, date_depense=date.today(), montant=48, qui=membres["anne.demo@example.test"].id, quoi="Fournitures à confirmer", paye=False, methode=None, saisi_par=membres["anne.demo@example.test"].id))
|
|
db.add(Proposition(groupe_id=groupe.id, sujet="Réviser le format de discussion", description="Proposition laissée en réflexion pour la prochaine assemblée.", proposeur_id=membres["julie.demo@example.test"].id, secondeur_id=membres["marc.demo@example.test"].id, statut="secondee"))
|
|
db.add(Proposition(groupe_id=groupe.id, sujet="Acheter un projecteur", description="La dépense n'est pas jugée prioritaire.", proposeur_id=membres["luc.demo@example.test"].id, secondeur_id=membres["sophie.demo@example.test"].id, statut="rejetee", etant_donne_que="La réserve prudente doit d'abord être consolidée."))
|
|
db.add(Evenement(groupe_id=groupe.id, titre="Atelier Étapes et Traditions", description="Atelier régional ouvert aux membres.", date_evenement=date.today() + timedelta(days=24), lieu="Centre communautaire", cree_par=rsg.id))
|
|
db.add(Evenement(groupe_id=groupe.id, titre="Anniversaire du groupe", description="Souper partage et réunion ouverte.", date_evenement=date.today() - timedelta(days=40), lieu="Local habituel", cree_par=secretaire.id))
|
|
db.add(RapportRsg(groupe_id=groupe.id, reunion_dist=date.today() - timedelta(days=18), redacteur_id=rsg.id, decisions=["Le district prépare l'inventaire annuel.", "Besoin de bénévoles pour l'accueil."], infos_region="La région sollicite les groupes pour les ateliers.", sujets_groupe="Questions transmises par la conscience de groupe."))
|
|
|
|
|
|
async def _decision(db, groupe, reunion, compteurs, prefixe, jour, proposeur, secondeur, sujet, attendu, decide):
|
|
compteurs[prefixe] += 1
|
|
numero = f"{prefixe}-{jour.year}-{compteurs[prefixe]:03d}"
|
|
proposition = Proposition(
|
|
groupe_id=groupe.id,
|
|
reunion_id=reunion.id if reunion else None,
|
|
sujet=sujet,
|
|
description=attendu,
|
|
proposeur_id=proposeur.id,
|
|
secondeur_id=secondeur.id,
|
|
statut="adoptee",
|
|
numero_resolution=numero,
|
|
date_vote=jour,
|
|
date_adoption=jour,
|
|
etant_donne_que=attendu,
|
|
groupe_a_decide_de=decide,
|
|
cree_le=_moment(jour, 20, 30),
|
|
modifie_le=_moment(jour, 20, 45),
|
|
)
|
|
db.add(proposition)
|
|
await db.flush()
|
|
await _trace(
|
|
db, groupe, proposeur, jour, "propositions", f"decision.{prefixe.lower()}",
|
|
"Décision adoptée par la conscience de groupe.",
|
|
proposition.id,
|
|
apres={"numero": numero, "sujet": sujet, "decision": decide},
|
|
chemin="/propositions/voter",
|
|
)
|
|
return proposition
|
|
|
|
|
|
def _moment(jour: date, heure: int = 20, minute: int = 0) -> datetime:
|
|
return datetime.combine(jour, time(heure, minute)).replace(tzinfo=timezone.utc)
|
|
|
|
|
|
def _parse_heure(valeur) -> time | None:
|
|
if isinstance(valeur, time):
|
|
return valeur
|
|
if not valeur:
|
|
return None
|
|
heures, minutes = str(valeur).split(":", 1)
|
|
return time(int(heures), int(minutes[:2]))
|
|
|
|
|
|
async def _vider_groupe(db: AsyncSession, groupe_id) -> None:
|
|
params = {"gid": str(groupe_id)}
|
|
await db.execute(text("DELETE FROM presences WHERE reunion_id IN (SELECT id FROM reunions WHERE groupe_id = :gid)"), params)
|
|
await db.execute(text("DELETE FROM pv_reunions WHERE reunion_id IN (SELECT id FROM reunions WHERE groupe_id = :gid)"), params)
|
|
await db.execute(text("DELETE FROM postes_modules WHERE poste_id IN (SELECT id FROM postes WHERE groupe_id = :gid)"), params)
|
|
await db.execute(text("UPDATE candidatures SET proposition_id = NULL WHERE groupe_id = :gid"), params)
|
|
await db.execute(text("UPDATE propositions SET candidature_id = NULL WHERE groupe_id = :gid"), params)
|
|
for table in [
|
|
"journal_actions",
|
|
"historique_modifications",
|
|
"push_subscriptions",
|
|
"mouvements_reserves",
|
|
"transactions_comptables",
|
|
"rapports_adoptes",
|
|
"rapports_rsg",
|
|
"ventes_litterature",
|
|
"ventes_jetons",
|
|
"jetons",
|
|
"envois_contributions",
|
|
"destinataires_contributions",
|
|
"config_repartition",
|
|
"operations_bancaires",
|
|
"reserves",
|
|
"evenements",
|
|
"depenses",
|
|
"collectes",
|
|
"propositions",
|
|
"candidatures",
|
|
"rotations",
|
|
"affectations",
|
|
"litterature",
|
|
"invitations",
|
|
"reunions",
|
|
"postes",
|
|
"membres",
|
|
]:
|
|
await db.execute(text(f"DELETE FROM {table} WHERE groupe_id = :gid"), params)
|
|
|
|
|
|
async def _trace(
|
|
db: AsyncSession,
|
|
groupe: Groupe,
|
|
membre: Membre,
|
|
jour: date,
|
|
table: str,
|
|
action: str,
|
|
raison: str,
|
|
objet_id=None,
|
|
avant: dict | None = None,
|
|
apres: dict | None = None,
|
|
chemin: str | None = None,
|
|
):
|
|
moment = _moment(jour, 21, 50)
|
|
cible = objet_id or groupe.id
|
|
db.add(HistoriqueModification(
|
|
groupe_id=groupe.id,
|
|
table_cible=table,
|
|
objet_id=cible,
|
|
action=action,
|
|
raison=raison,
|
|
membre_id=membre.id if membre else None,
|
|
chemin_api=chemin,
|
|
avant=avant,
|
|
apres=apres,
|
|
cree_le=moment,
|
|
))
|
|
db.add(JournalAction(
|
|
groupe_id=groupe.id,
|
|
membre_id=membre.id if membre else None,
|
|
methode="POST",
|
|
chemin=chemin or f"/demo/{table}",
|
|
statut_http=200,
|
|
adresse_ip="127.0.0.1",
|
|
user_agent="DemoSeeder/1.0",
|
|
details={"action": action, "raison": raison},
|
|
cree_le=moment,
|
|
))
|
|
|
|
|
|
def _premier_mardi(jour: date) -> date:
|
|
delta = (1 - jour.weekday()) % 7
|
|
return jour + timedelta(days=delta)
|
|
|
|
|
|
async def _premier_groupe(db: AsyncSession) -> Groupe:
|
|
from sqlalchemy import select
|
|
|
|
result = await db.execute(select(Groupe).limit(1))
|
|
return result.scalar_one()
|
|
|
|
|
|
async def _membres_par_courriel(db: AsyncSession, groupe_id=None) -> dict[str, Membre]:
|
|
from sqlalchemy import select
|
|
|
|
query = select(Membre)
|
|
if groupe_id is not None:
|
|
query = query.where(Membre.groupe_id == groupe_id)
|
|
result = await db.execute(query)
|
|
return {m.courriel: m for m in result.scalars().all()}
|
|
|
|
|
|
async def _postes_par_nom(db: AsyncSession, groupe_id) -> dict[str, Poste]:
|
|
from sqlalchemy import select
|
|
|
|
result = await db.execute(select(Poste).where(Poste.groupe_id == groupe_id))
|
|
return {p.nom: p for p in result.scalars().all()}
|
|
|
|
|
|
async def _litterature(db: AsyncSession) -> list[Litterature]:
|
|
from sqlalchemy import select
|
|
|
|
result = await db.execute(select(Litterature))
|
|
return result.scalars().all()
|