"""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.instance_admin import InstanceAdmin 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 DEMO_GROUP_NAME = "Groupe Démonstration" OLD_DEMO_GROUP_NAMES = ("Groupe Résilience",) INSTANCE_SYSADMIN_IDENTIFIANT = "sysadmin" 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"), ] async def assurer_sysadmin_instance(db: AsyncSession) -> InstanceAdmin: result = await db.execute( select(InstanceAdmin).where(InstanceAdmin.identifiant == INSTANCE_SYSADMIN_IDENTIFIANT) ) admin = result.scalar_one_or_none() if admin: return admin admin = InstanceAdmin( identifiant=INSTANCE_SYSADMIN_IDENTIFIANT, prenom="Sysadmin", pin_hash=hash_pin("0000"), actif=True, ) db.add(admin) await db.flush() return admin 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 _groupe_demo(db: AsyncSession) -> Groupe | None: result = await db.execute(select(Groupe).where(Groupe.nom.in_([DEMO_GROUP_NAME, *OLD_DEMO_GROUP_NAMES]))) groupe = result.scalars().first() if groupe and groupe.nom != DEMO_GROUP_NAME: groupe.nom = DEMO_GROUP_NAME await db.flush() return groupe def _est_groupe_demo(groupe: Groupe | None) -> bool: return bool(groupe and groupe.nom in {DEMO_GROUP_NAME, *OLD_DEMO_GROUP_NAMES}) async def initialiser_demo(db: AsyncSession) -> dict: existant = await _groupe_demo(db) 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=DEMO_GROUP_NAME, 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 _groupe_demo(db) 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 reinitialiser_groupe(db: AsyncSession, groupe_id) -> dict: groupe = await db.get(Groupe, groupe_id) if not groupe: raise ValueError("Groupe introuvable") if _est_groupe_demo(groupe): return await reinitialiser_demo(db) await _vider_groupe(db, groupe.id) await _peupler_groupe_base(db, groupe) await db.flush() return {"message": f"Groupe {groupe.nom} réinitialisé", "groupe_id": groupe.id} async def creer_groupe(db: AsyncSession, data: dict) -> Groupe: nom = (data.get("nom") or "").strip() if not nom: raise ValueError("Nom requis") if nom == DEMO_GROUP_NAME or nom in OLD_DEMO_GROUP_NAMES: 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="", 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 _est_groupe_demo(groupe) and nom != DEMO_GROUP_NAME: raise ValueError("Le groupe de démonstration ne peut pas être renommé") if (nom == DEMO_GROUP_NAME or nom in OLD_DEMO_GROUP_NAMES) and not _est_groupe_demo(groupe): raise ValueError("Ce nom est réservé au groupe de démonstration") groupe.nom = nom for attr in ["district", "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 _est_groupe_demo(groupe): 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 = DEMO_GROUP_NAME, adresse_local: str = "Salle communautaire, Montréal", ): groupe = Groupe( nom=nom_groupe, district="87-16", region="", 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"
Rapport adopté pour {jour.strftime('%Y-%m')}.
Banque: {banque:.2f} $
Encaisse: {encaisse:.2f} $
", 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()