diff --git a/noethysweb/collaborateurs/templates/collaborateurs/collaborateur_recherche_ent.html b/noethysweb/collaborateurs/templates/collaborateurs/collaborateur_recherche_ent.html new file mode 100644 index 00000000..bffa13ca --- /dev/null +++ b/noethysweb/collaborateurs/templates/collaborateurs/collaborateur_recherche_ent.html @@ -0,0 +1,181 @@ +{% extends "core/page.html" %} +{% load static %} +{% load crispy_forms_tags %} + +{% block styles %} + {{ block.super }} + +{% endblock %} + +{% block contenu_page %} +
+ + + Revenir aux paramètres de recherche + +
+ +{% if collaborateurs %} + {% for collab in collaborateurs %} +
+
+ {{ collab.prenom }} {{ collab.nom }} +
+ {% csrf_token %} + + +
+
+
+
+
📧 Email : {{ collab.mail }}
+
📞 Téléphones : {{ collab.numeros_de_telephone|join:", " }}
+
👤 Profil : {{ collab.profil }}
+
🏫 École : {{ collab.ecole }}
+
+
+
+ {% if not forloop.last %} +
+ {% endif %} + {% endfor %} +{% else %} +
+ +

Aucun collaborateur trouvé.

+
+{% endif %} + + +
+
+ Pas trouvé le bon collaborateur ? +
+
+

+ Aucun collaborateur ne correspond à ce que vous souhaitez ajouter? +

+
+ {% csrf_token %} + + +
+
+
+ +{% endblock %} diff --git a/noethysweb/collaborateurs/templates/collaborateurs/collaborateur_synchronisation.html b/noethysweb/collaborateurs/templates/collaborateurs/collaborateur_synchronisation.html new file mode 100644 index 00000000..9083cfcb --- /dev/null +++ b/noethysweb/collaborateurs/templates/collaborateurs/collaborateur_synchronisation.html @@ -0,0 +1,386 @@ +{% extends "collaborateurs/collaborateur.html" %} +{% load crispy_forms_tags %} +{% load static %} +{% load embed %} + +{% block page_titre %}{{ page_titre }}{% endblock page_titre %} + +{% block detail_collaborateur %} +
+ {% embed 'core/box.html' %} + {% block box_theme %}card-outline card-lightblue{% endblock %} + {% block box_titre %}{{ box_titre }}{% endblock %} + {% block box_introduction %}{{ box_introduction|safe }}{% endblock %} + {% endembed %} + +
+ {% csrf_token %} + +
+
+
+ + Légende : + Identique + Différent + Sélectionné pour synchronisation +
+ +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + ChampCoCliCoENT
+ + Civilité + {{ local.civilite|default:"-" }} + + {{ external.civilite|default:"-" }} +
+ + Nom + {{ local.nom|default:"-" }} + + {{ external.nom|default:"-" }} +
+ + Nom de jeune fille + {{ local.nom_jfille|default:"-" }} + + {{ external.nom_jfille|default:"-" }} +
+ + Prénom + {{ local.prenom|default:"-" }} + + {{ external.prenom|default:"-" }} +
+ + Rue + {{ local.rue|default:"-" }} + + {{ external.rue|default:"-" }} +
+ + Code postal + {{ local.cp|default:"-" }} + + {{ external.code_postal|default:"-" }} +
+ + Ville + {{ local.ville|default:"-" }} + + {{ external.ville|default:"-" }} +
+ + Téléphone travail + {{ local.travail_tel|default:"-" }} + + {{ external.travail_tel|default:"-" }} +
+ + Mail travail + {{ local.travail_mail|default:"-" }} + + {{ external.travail_mail|default:"-" }} +
+ + Téléphone domicile + {{ local.tel_domicile|default:"-" }} + + {{ external.tel_domicile|default:"-" }} +
+ + Téléphone mobile + {{ local.tel_mobile|default:"-" }} + + {{ external.tel_mobile|default:"-" }} +
+ + Mail + {{ local.mail|default:"-" }} + + {{ external.mail|default:"-" }} +
+
+ +
+
+ 0 + champ(s) sélectionné(s) +
+
+ +
+
+
+
+
+ + +
+{% endblock %} + +{% block scripts %} + {{ block.super }} + +{% endblock scripts %} \ No newline at end of file diff --git a/noethysweb/collaborateurs/urls.py b/noethysweb/collaborateurs/urls.py index 1c36a4e5..8356213c 100644 --- a/noethysweb/collaborateurs/urls.py +++ b/noethysweb/collaborateurs/urls.py @@ -10,7 +10,7 @@ collaborateur_pieces, collaborateur_notes, collaborateur_contrats, collaborateur_evenements, planning_collaborateurs, \ collaborateur_appliquer_modele_planning, liste_contrats, collaborateur_outils, collaborateur_historique, \ collaborateur_emails, collaborateur_sms, appliquer_modele_planning, collaborateur_groupes, collaborateur_voir_contrat, \ - fusionner_contrats_word + fusionner_contrats_word, collaborateur_ent urlpatterns = [ @@ -20,12 +20,14 @@ # Collaborateurs path('collaborateurs/collaborateurs/liste', collaborateur.Liste.as_view(), name='collaborateur_liste'), + path('collaborateurs/collaborateurs/ent/liste', collaborateur_ent.EntListeCollaborateur.as_view(), name='collaborateur_recherche_ent'), path('collaborateurs/collaborateurs/ajouter', collaborateur.Ajouter.as_view(), name='collaborateur_ajouter'), path('collaborateurs/collaborateurs/supprimer/', collaborateur.Supprimer.as_view(), name='collaborateur_supprimer'), path('collaborateurs/collaborateurs/resume/', collaborateur.Resume.as_view(), name='collaborateur_resume'), path('collaborateurs/collaborateurs/identite/', collaborateur_identite.Consulter.as_view(), name='collaborateur_identite'), path('collaborateurs/collaborateurs/identite/modifier/', collaborateur_identite.Modifier.as_view(), name='collaborateur_identite_modifier'), + path('collaborateurs/collaborateurs/synchroniser/', collaborateur_ent.SynchroniserCollaborateur.as_view(), name='collaborateur_synchroniser'), path('collaborateurs/collaborateurs/groupes/', collaborateur_groupes.Consulter.as_view(), name='collaborateur_groupes'), path('collaborateurs/collaborateurs/groupes/modifier/', collaborateur_groupes.Modifier.as_view(), name='collaborateur_groupes_modifier'), diff --git a/noethysweb/collaborateurs/utils/utils_collaborateur.py b/noethysweb/collaborateurs/utils/utils_collaborateur.py index 09a91c06..87256685 100644 --- a/noethysweb/collaborateurs/utils/utils_collaborateur.py +++ b/noethysweb/collaborateurs/utils/utils_collaborateur.py @@ -14,4 +14,5 @@ {"code": "contrats", "label": "Contrats", "icone": "fa-edit", "url": "collaborateur_contrats_liste"}, {"code": "evenements", "label": "Evènements", "icone": "fa-calendar", "url": "collaborateur_evenements_liste"}, {"code": "outils", "label": "Outils", "icone": "fa-wrench", "url": "collaborateur_outils"}, + {"code": "synchroniser", "label": "Synchroniser", "icone": "fa-refresh", "url": "collaborateur_synchroniser"}, ] diff --git a/noethysweb/collaborateurs/views/collaborateur.py b/noethysweb/collaborateurs/views/collaborateur.py index a271e80f..942d83be 100644 --- a/noethysweb/collaborateurs/views/collaborateur.py +++ b/noethysweb/collaborateurs/views/collaborateur.py @@ -7,13 +7,15 @@ from django.db.models import Q from django.views.generic.detail import DetailView from django.contrib import messages -from core.views.mydatatableview import MyDatatable, columns, helpers +from core.views.mydatatableview import MyDatatable, columns from core.views import crud from core.views.base import CustomView -from core.models import Collaborateur, Note +from core.models import Collaborateur, Note, Organisateur from collaborateurs.forms.collaborateur import Formulaire from collaborateurs.utils.utils_collaborateur import LISTE_ONGLETS from collaborateurs.utils import utils_pieces_manquantes +from django.http import HttpResponseRedirect +from core.utils.utils_ent import get_ent_collaborateur class Page(crud.Page): @@ -80,6 +82,34 @@ def Get_actions_speciales(self, instance, *args, **kwargs): class Ajouter(Page, crud.Ajouter): form_class = Formulaire + def form_valid(self, form): + organisateur = Organisateur.objects.filter(pk=1).first() + + if organisateur and organisateur.ent_active: + # ⚡ Appel API externe avec les données du formulaire + nom = form.cleaned_data.get("nom") + prenom = form.cleaned_data.get("prenom") + civilite = form.cleaned_data.get("civilite") + groupes = form.cleaned_data.get("groupes") + groupes_ids = list(groupes.values_list('idgroupe', flat=True)) if groupes else [] + collaborateur_ent = get_ent_collaborateur(nom, prenom) + + # Stocker le résultat dans la session (pour l’afficher après la redirection) + + self.request.session["collaborateurs_ent"] = collaborateur_ent + self.request.session["collaborateur_search_info"] = { + "nom": nom, + "prenom": prenom, + "civilite": civilite, + "groupes": groupes_ids + } + url_success = reverse_lazy("collaborateur_recherche_ent", kwargs={}) + # Rediriger vers une page dédiée (sans ajouter le collaborateur) + return HttpResponseRedirect(url_success) + + # Sinon, on garde le comportement classique (création du Collaborateur) + return super().form_valid(form) + def get_success_url(self): """ Renvoie vers la page résumé du collaborateur """ messages.add_message(self.request, messages.SUCCESS, "Ajout enregistré") diff --git a/noethysweb/collaborateurs/views/collaborateur_ent.py b/noethysweb/collaborateurs/views/collaborateur_ent.py new file mode 100644 index 00000000..e6fd6efd --- /dev/null +++ b/noethysweb/collaborateurs/views/collaborateur_ent.py @@ -0,0 +1,224 @@ +from django.views.generic import TemplateView +from core.views.base import CustomView +from django.db import transaction +from core.models import Collaborateur, GroupeCollaborateurs +from django.http import HttpResponseRedirect +from django.urls import reverse_lazy +import logging +from django.contrib import messages +from core.utils.utils_ent import get_collaborateur_by_ent_id +from collaborateurs.views.collaborateur import Onglet +from django.views.generic import TemplateView +from django.contrib import messages +from django.shortcuts import redirect + +logger = logging.getLogger(__name__) + + + +class EntListeCollaborateur(CustomView, TemplateView): + menu_code = "collaborateur_recherche_ent" + template_name = "collaborateurs/collaborateur_recherche_ent.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['collaborateurs'] = self.request.session.get('collaborateurs_ent', []) + context['page_titre'] = "Liste des collaborateurs de l'ENT" + + # Récupération des informations de recherche depuis la session + search_info = self.request.session.get('collaborateur_search_info', {}) + context['search_nom'] = search_info.get('nom', '') + context['search_prenom'] = search_info.get('prenom', '') + context['search_categorie'] = search_info.get('categorie', '') + + return context + + @transaction.atomic + def post(self, request, *args, **kwargs): + action = request.POST.get("action", "") + search_info = self.request.session.get('collaborateur_search_info', {}) + collaborateur_id = request.POST.get("collaborateur_id") + collaborateurs = self.request.session.get('collaborateurs_ent', []) + if action == "ajouter_nouveau_collaborateur": + collaborateur = Collaborateur( + nom = search_info.get("nom"), + prenom = search_info.get("prenom"), + civilite = search_info.get("civilite"), + ) + collaborateur.save() + groupes_ids = search_info.get("groupes", []) + if groupes_ids: + groupes_qs = GroupeCollaborateurs.objects.filter(idgroupe__in=groupes_ids) + collaborateur.groupes.set(groupes_qs) + collaborateur.save() + url_success = reverse_lazy("collaborateur_resume", kwargs={'idcollaborateur': collaborateur.idcollaborateur}) + else: + collaborateur = Collaborateur.objects.filter(ent_id=collaborateur_id).first() + if not collaborateur: + collaborateur_ent = self.trouver_collaborateur_par_ent_id(collaborateur_id, collaborateurs) + collaborateur = Collaborateur( + civilite = search_info.get("civilite"), + nom = collaborateur_ent.get("nom"), + nom_jfille = collaborateur_ent.get("nom_jfille"), + prenom = collaborateur_ent.get("prenom"), + ent_id = collaborateur_ent.get("ent_id"), + rue_resid = collaborateur_ent.get("rue"), + cp_resid = collaborateur_ent.get("code_postal"), + ville_resid = collaborateur_ent.get("ville"), + travail_tel = collaborateur_ent.get("travail_tel"), + travail_mail = collaborateur_ent.get("travail_mail"), + tel_domicile = collaborateur_ent.get("tel_domicile"), + tel_mobile = collaborateur_ent.get("tel_mobile"), + mail = collaborateur_ent.get("mail"), + ) + + collaborateur.save() + groupes_ids = search_info.get("groupes", []) + if groupes_ids: + groupes_qs = GroupeCollaborateurs.objects.filter(idgroupe__in=groupes_ids) + collaborateur.groupes.set(groupes_qs) + collaborateur.save() + messages.add_message(self.request, messages.SUCCESS, "Ajout enregistré") + else: + messages.add_message(self.request, messages.ERROR, "Collaborateur existe déjà") + url_success = reverse_lazy("collaborateur_resume", kwargs={'idcollaborateur': collaborateur.idcollaborateur}) + return HttpResponseRedirect(url_success) + + def trouver_collaborateur_par_ent_id(self, colaborateur_id, collaborateurs): + """ + Trouve un individu dans les données de session par son id_ent + """ + for col in collaborateurs: + if str(col.get("ent_id")) == str(colaborateur_id): + return col + return None + + +class SynchroniserCollaborateur(Onglet, TemplateView): + menu_code = "collaborateur_synchroniser" + template_name = "collaborateurs/collaborateur_synchronisation.html" + + def get_context_data(self, **kwargs): + context = super(SynchroniserCollaborateur, self).get_context_data(**kwargs) + context['box_titre'] = "Synchronisation" + context['onglet_actif'] = "synchroniser" + collab = Collaborateur.objects.get(pk=self.kwargs['idcollaborateur']) + + # Mapping des champs avec leurs labels + fields_mapping = [ + {'key': 'civilite', 'label': 'Civilité', 'local_key': 'civilite', 'external_key': 'civilite'}, + {'key': 'nom', 'label': 'Nom', 'local_key': 'nom', 'external_key': 'nom'}, + {'key': 'nom_jfille', 'label': 'Nom de jeune fille', 'local_key': 'nom_jfille', 'external_key': 'nom_jfille'}, + {'key': 'prenom', 'label': 'Prénom', 'local_key': 'prenom', 'external_key': 'prenom'}, + {'key': 'rue', 'label': 'Rue', 'local_key': 'rue', 'external_key': 'rue'}, + {'key': 'cp', 'label': 'Code postal', 'local_key': 'cp', 'external_key': 'code_postal'}, + {'key': 'ville', 'label': 'Ville', 'local_key': 'ville', 'external_key': 'ville'}, + {'key': 'travail_tel', 'label': 'Téléphone travail', 'local_key': 'travail_tel', 'external_key': 'travail_tel'}, + {'key': 'travail_mail', 'label': 'Mail travail', 'local_key': 'travail_mail', 'external_key': 'travail_mail'}, + {'key': 'tel_domicile', 'label': 'Téléphone domicile', 'local_key': 'tel_domicile', 'external_key': 'tel_domicile'}, + {'key': 'tel_mobile', 'label': 'Téléphone mobile', 'local_key': 'tel_mobile', 'external_key': 'tel_mobile'}, + {'key': 'mail', 'label': 'Mail personnel', 'local_key': 'mail', 'external_key': 'mail'}, + ] + + local_data = { + "id": collab.idcollaborateur, + "nom": collab.nom, + "prenom": collab.prenom, + "civilite": collab.civilite, + "nom_jfille": collab.nom_jfille if hasattr(collab, 'nom_jfille') else None, + "rue": collab.rue_resid, + "cp": collab.cp_resid, + "ville": collab.ville_resid, + "travail_tel": collab.travail_tel, + "travail_mail": collab.travail_mail, + "tel_domicile": collab.tel_domicile, + "tel_mobile": collab.tel_mobile, + "mail": collab.mail + } + + external_data = get_collaborateur_by_ent_id(collab.ent_id) + + context["collaborateur"] = collab + context["local"] = local_data + context["external"] = external_data + context["fields_mapping"] = fields_mapping + context["box_titre"] = f"Synchronisation - {collab.prenom} {collab.nom}" + context["box_introduction"] = "Sélectionnez les champs à synchroniser depuis l'ENT vers CoCliCo." + context["onglet_actif"] = "synchronisation" + return context + + def post(self, request, *args, **kwargs): + """Traite la synchronisation des champs sélectionnés""" + try: + collab = Collaborateur.objects.get(pk=self.kwargs['idcollaborateur']) + external_data = get_collaborateur_by_ent_id(collab.ent_id) + + # Récupérer les champs sélectionnés + selected_fields = request.POST.getlist('sync_fields') + + if not selected_fields: + messages.warning(request, "Aucun champ sélectionné pour la synchronisation.") + return redirect(request.path) + + # Mapping des champs du formulaire vers les attributs du modèle + field_mapping = { + 'civilite': 'civilite', + 'nom': 'nom', + 'nom_jfille': 'nom_jfille', + 'prenom': 'prenom', + 'rue': 'rue_resid', + 'cp': 'cp_resid', + 'ville': 'ville_resid', + 'travail_tel': 'travail_tel', + 'travail_mail': 'travail_mail', + 'tel_domicile': 'tel_domicile', + 'tel_mobile': 'tel_mobile', + 'mail': 'mail' + } + + # Mapping des champs externes + external_field_mapping = { + 'civilite': 'civilite', + 'nom': 'nom', + 'nom_jfille': 'nom_jfille', + 'prenom': 'prenom', + 'rue': 'rue', + 'cp': 'code_postal', + 'ville': 'ville', + 'travail_tel': 'travail_tel', + 'travail_mail': 'travail_mail', + 'tel_domicile': 'tel_domicile', + 'tel_mobile': 'tel_mobile', + 'mail': 'mail' + } + + updated_fields = [] + for field in selected_fields: + if field in field_mapping: + model_field = field_mapping[field] + external_field = external_field_mapping[field] + + if external_field in external_data: + new_value = external_data[external_field] + setattr(collab, model_field, new_value) + updated_fields.append(field) + + if updated_fields: + collab.save() + messages.success( + request, + f"Synchronisation réussie ! {len(updated_fields)} champ(s) mis à jour : {', '.join(updated_fields)}" + ) + else: + messages.info(request, "Aucun champ n'a été mis à jour.") + + return redirect(request.path) + + except Collaborateur.DoesNotExist: + messages.error(request, "Collaborateur introuvable.") + return redirect('collaborateurs_liste') + except Exception as e: + messages.error(request, f"Erreur lors de la synchronisation : {str(e)}") + return redirect(request.path) + + \ No newline at end of file diff --git a/noethysweb/core/models.py b/noethysweb/core/models.py index ecf08131..3a829ba2 100644 --- a/noethysweb/core/models.py +++ b/noethysweb/core/models.py @@ -611,6 +611,7 @@ class Organisateur(models.Model): logo = ResizedImageField(verbose_name="Logo", upload_to=get_uuid_path, blank=True, null=True) gps = models.CharField(verbose_name="GPS", max_length=200, blank=True, null=True) logo_update = models.DateTimeField(verbose_name="Date MAJ Logo", max_length=200, blank=True, null=True) + ent_active = models.BooleanField(verbose_name="ENT activé", default=False) expedition_active = models.BooleanField(verbose_name="Expedition active", default=False) class Meta: @@ -796,6 +797,7 @@ def __str__(self): class Ecole(models.Model): idecole = models.AutoField(verbose_name="ID", db_column='IDecole', primary_key=True) + uai = models.CharField(verbose_name="UAI", max_length=200, blank=True, null=True) nom = models.CharField(verbose_name="Nom", max_length=300) rue = models.CharField(verbose_name="Rue", max_length=200, blank=True, null=True) cp = models.CharField(verbose_name="Code postal", max_length=50, blank=True, null=True) @@ -1666,6 +1668,7 @@ def __str__(self): class Individu(models.Model): idindividu = models.AutoField(verbose_name="ID", db_column='IDindividu', primary_key=True) civilite = models.IntegerField(verbose_name=_("Civilité"), db_column='IDcivilite', choices=data_civilites.GetListeCivilitesForModels(), default=1) + ent_id = models.CharField(verbose_name="ent_id", max_length=200, blank=True, null=True) nom = models.CharField(verbose_name=_("Nom"), max_length=200) nom_jfille = models.CharField(verbose_name=_("Nom de naissance"), max_length=200, blank=True, null=True) prenom = models.CharField(verbose_name=_("Prénom"), max_length=200, blank=True, null=True) @@ -4180,6 +4183,7 @@ class Collaborateur(models.Model): nom = models.CharField(verbose_name="Nom", max_length=200) nom_jfille = models.CharField(verbose_name="Nom de naissance", max_length=200, blank=True, null=True) prenom = models.CharField(verbose_name="Prénom", max_length=200, blank=True, null=True) + ent_id = models.CharField(verbose_name="ent_id", max_length=200, blank=True, null=True) rue_resid = encrypt(models.CharField(verbose_name="Rue", max_length=200, blank=True, null=True)) cp_resid = encrypt(models.CharField(verbose_name="Code postal", max_length=50, blank=True, null=True)) ville_resid = encrypt(models.CharField(verbose_name="Ville", max_length=200, blank=True, null=True)) @@ -4794,3 +4798,24 @@ class Meta: def __str__(self): return "IDversion %d" % self.idversion if self.idversion else "Nouvelle version" + + +class SynchronisationLock(models.Model): + """Verrou global pour empêcher les synchronisations ENT simultanées""" + idlock = models.AutoField(verbose_name="ID", db_column="IDlock", primary_key=True) + type_sync = models.CharField(verbose_name="Type de synchronisation", max_length=50, unique=True, db_index=True) + est_verrouille = models.BooleanField(verbose_name="Est verrouillé", default=False) + utilisateur = models.ForeignKey('Utilisateur', verbose_name="Utilisateur", on_delete=models.SET_NULL, null=True, blank=True) + date_debut = models.DateTimeField(verbose_name="Date de début", null=True, blank=True) + date_derniere_fin = models.DateTimeField(verbose_name="Date de dernière fin", null=True, blank=True) + nb_individus = models.IntegerField(verbose_name="Nombre d'individus", default=0) + nb_succes_dernier = models.IntegerField(verbose_name="Nombre de succès (dernier)", default=0) + dernier_utilisateur = models.ForeignKey('Utilisateur', verbose_name="Dernier utilisateur", on_delete=models.SET_NULL, null=True, blank=True, related_name='dernieres_syncs') + + class Meta: + db_table = "synchronisation_locks" + verbose_name = "verrou de synchronisation" + verbose_name_plural = "verrous de synchronisation" + + def __str__(self): + return f"Lock {self.type_sync} - {'Verrouillé' if self.est_verrouille else 'Libre'}" diff --git a/noethysweb/core/utils/utils_ent.py b/noethysweb/core/utils/utils_ent.py new file mode 100644 index 00000000..ea030436 --- /dev/null +++ b/noethysweb/core/utils/utils_ent.py @@ -0,0 +1,920 @@ +from core.models import Organisateur + +def get_ent_users(nom, prenom): + """ + Fonction pour rechercher des utilisateurs dans l'ENT + + Args: + nom (str): Nom de famille + prenom (str): Prénom + + Returns: + list: Liste des familles ENT où l'utilisateur a été trouvé + """ + try: + exemple_users = [ + { + "representant": { + "id_ent": "user179", + "civilite": "Monsieur", + "prenom": "Test10", + "nom": "Test10", + "email": "jean.dupont@mail.fr", + "telephone": "0601020304", + }, + "enfants": [ + { + "id_ent": "user131", + "civilite": "Monsieur", + "prenom": "Lucas", + "nom": "Dupont", + "scolarite": { + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole": { + "id": 10, + "nom": "Collège Victor Hugo", + "rue": "12 rue de la République", + "cp": "75001", + "ville": "Paris", + "tel": "0140203040", + "fax": "0140203041", + "mail": "contact@victorhugo.fr", + "uai": "0751234A", + }, + "classe": { + "id": 101, + "nom": "6ème A", + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole_id": 10, + }, + "niveau": { + "id": 6, + "ordre": 6, + "nom": "Sixième", + "abrege": "6ème", + }, + }, + }, + { + "id_ent": "user132", + "civilite": "Madame", + "prenom": "Emma", + "nom": "Dupont", + "scolarite": { + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole": { + "id": 10, + "nom": "Collège Victor Hugo", + "rue": "12 rue de la République", + "cp": "75001", + "ville": "Paris", + "tel": "0140203040", + "fax": "0140203041", + "mail": "contact@victorhugo.fr", + "uai": "0751234A", + }, + "classe": { + "id": 102, + "nom": "5ème B", + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole_id": 10, + }, + "niveau": { + "id": 5, + "ordre": 5, + "nom": "Cinquième", + "abrege": "5ème", + }, + }, + }, + ], + }, + { + "representant": { + "id_ent": "user2087", + "civilite": "Monsieur", + "prenom": "Test10", + "nom": "Test10", + "email": "paul.martin@mail.fr", + "telephone": "0610101010", + }, + "enfants": [ + { + "id_ent": "user3001", + "civilite": "Madame", + "prenom": "Clara", + "nom": "Martin", + "scolarite": { + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole": { + "id": 20, + "nom": "École élémentaire Jean Moulin", + "rue": "25 avenue des Écoles", + "cp": "69001", + "ville": "Lyon", + "tel": "0472101112", + "fax": "0472101113", + "mail": "contact@jeanmoulin.fr", + "uai": "0695678B", + }, + "classe": { + "id": 201, + "nom": "CE2", + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole_id": 20, + }, + "niveau": { + "id": 3, + "ordre": 3, + "nom": "Cours Élémentaire 2", + "abrege": "CE2", + }, + }, + }, + { + "id_ent": "user3002", + "civilite": "Monsieur", + "prenom": "Tom", + "nom": "Martin", + "scolarite": { + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole": { + "id": 21, + "nom": "École élémentaire Jean Moulin", + "rue": "25 avenue des Écoles", + "cp": "69001", + "ville": "Lyon", + "tel": "0472101112", + "fax": "0472101113", + "mail": "contact@jeanmoulin.fr", + "uai": "0695678B", + }, + "classe": { + "id": 202, + "nom": "CM1", + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole_id": 21, + }, + "niveau": { + "id": 4, + "ordre": 4, + "nom": "Cours Moyen 1", + "abrege": "CM1", + }, + }, + }, + ], + }, + ] + + result = [] + organisateur = Organisateur.objects.filter(pk=1).first() + if organisateur.ent_active: + for famille in exemple_users: + rep = famille["representant"] + match_rep = ( + rep["nom"].lower() == nom.lower() + and rep["prenom"].lower() == prenom.lower() + ) + match_enfants = [ + enf for enf in famille["enfants"] + if enf["nom"].lower() == nom.lower() and enf["prenom"].lower() == prenom.lower() + ] + + if match_rep or match_enfants: + # On garde toute la famille, même si un seul correspond + result.append(famille) + + return result + else: + return [] + + except Exception as e: + print(f"Erreur lors de la recherche ENT: {e}") + return [] + +def get_ent_user_info(nom, prenom): + """ + Recherche un utilisateur dans l'ENT (représentant ou enfant). + Retourne uniquement ses infos, sans inclure la famille. + """ + + try: + # Liste plate des utilisateurs (representants + enfants) + exemple_users = [ + # Représentants DUPONT + { + "ent_id": "user179", + "civilite": "Monsieur", + "prenom": "Test10", + "nom": "Test10", + "email": "jean.dupont@mail.fr", + "telephone_mobile": "0601020304", + }, + { + "ent_id": "user124", + "civilite": "Madame", + "prenom": "Marie", + "nom": "DUPONT", + "email": "marie.dupont@mail.fr", + "telephone_mobile": "0605060708", + }, + # Enfant DUPONT + { + "ent_id": "user131", + "civilite": "Monsieur", + "prenom": "Lucas", + "nom": "DUPONT", + "scolarite": { + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole": { + "id": 10, + "nom": "Collège Victor Hugo", + "rue": "12 rue de la République", + "cp": "75001", + "ville": "Paris", + "tel": "0140203040", + "fax": "0140203041", + "mail": "contact@victorhugo.fr", + "uai": "0751234A", + }, + "classe": { + "id": 101, + "nom": "6ème A", + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole_id": 10, + }, + "niveau": { + "id": 6, + "ordre": 6, + "nom": "Sixième", + "abrege": "6ème", + }, + }, + }, + # Représentant MARTIN + { + "ent_id": "user2087", + "civilite": "Monsieur", + "prenom": "Test10", + "nom": "Test10", + "email": "Testent.Testent@mail.fr", + "telephone_mobile": "0610101010", + }, + # Enfant MARTIN + { + "ent_id": "user2087", + "civilite": "Madame", + "prenom": "TestEnfant10", + "nom": "TestEnfant10", + "scolarite": { + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole": { + "id": 20, + "nom": "École élémentaire Jean Moulin", + "rue": "25 avenue des Écoles", + "cp": "69001", + "ville": "Lyon", + "tel": "0472101112", + "fax": "0472101113", + "mail": "contact@jeanmoulin.fr", + "uai": "0695678B", + }, + "classe": { + "id": 201, + "nom": "CE2", + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole_id": 20, + }, + "niveau": { + "id": 3, + "ordre": 3, + "nom": "Cours Élémentaire 2", + "abrege": "CE2", + }, + }, + }, + ] + + organisateur = Organisateur.objects.filter(pk=1).first() + if not organisateur or not organisateur.ent_active: + return [] + + # Recherche de l’utilisateur + result = [ + user for user in exemple_users + if user["nom"].lower() == nom.lower() and user["prenom"].lower() == prenom.lower() + ] + + return result + + except Exception as e: + print(f"Erreur lors de la recherche ENT: {e}") + return [] + +def get_ent_user_info_by_ent_id(ent_id): + """ + Recherche un utilisateur dans l'ENT (représentant ou enfant). + Retourne uniquement ses infos, sans inclure la famille. + """ + + try: + # Liste plate des utilisateurs (representants + enfants) + exemple_users = [ + # Représentants DUPONT + { + "ent_id": "user179", + "civilite": "Monsieur", + "prenom": "Test10", + "nom": "Test10", + "email": "jean.dupont@mail.fr", + "telephone": "0601020304", + }, + { + "ent_id": "user124", + "civilite": "Madame", + "prenom": "Marie", + "nom": "DUPONT", + "email": "marie.dupont@mail.fr", + "telephone": "0605060708", + }, + { + "ent_id": "user129", + "civilite": "Monsieur", + "prenom": "Test11", + "nom": "Test11", + "email": "marie.dupont@mail.fr", + "telephone": "0605060708", + }, + # Enfant DUPONT + { + "ent_id": "user125", + "civilite": "Monsieur", + "prenom": "Lucas", + "nom": "DUPONT", + "scolarite": { + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole": { + "id": 10, + "nom": "Collège Victor Hugo", + "rue": "12 rue de la République", + "cp": "75001", + "ville": "Paris", + "tel": "0140203040", + "fax": "0140203041", + "mail": "contact@victorhugo.fr", + "uai": "0751234A", + }, + "classe": { + "id": 101, + "nom": "6ème A", + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole_id": 10, + }, + "niveau": { + "id": 6, + "ordre": 6, + "nom": "Sixième", + "abrege": "6ème", + }, + }, + }, + # Représentant MARTIN + { + "ent_id": "user2087", + "civilite": "Monsieur", + "prenom": "Test10", + "nom": "Test10", + "email": "Testent.Testent@mail.fr", + "telephone": "0610101010", + }, + # Enfant MARTIN + { + "ent_id": "user2087", + "civilite": "Madame", + "prenom": "TestEnfant10", + "nom": "TestEnfant10", + "scolarite": { + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole": { + "id": 20, + "nom": "École élémentaire Jean Moulin", + "rue": "25 avenue des Écoles", + "cp": "69001", + "ville": "Lyon", + "tel": "0472101112", + "fax": "0472101113", + "mail": "contact@jeanmoulin.fr", + "uai": "0695678B", + }, + "classe": { + "id": 201, + "nom": "CE2", + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole_id": 20, + }, + "niveau": { + "id": 3, + "ordre": 3, + "nom": "Cours Élémentaire 2", + "abrege": "CE2", + }, + }, + }, + ] + + organisateur = Organisateur.objects.filter(pk=1).first() + if not organisateur or not organisateur.ent_active: + return [] + + # Recherche de l’utilisateur + result = [ + user for user in exemple_users + if user["ent_id"].lower() == ent_id.lower() + ] + + return result[0] + + except Exception as e: + print(f"Erreur lors de la recherche ENT: {e}") + return None + + +def get_ent_collaborateur(nom, prenom): + """ + Fonction pour rechercher de Personnel Education National dans l'ENT + + Args: + nom (str): Nom de famille + prenom (str): Prénom + + Returns: + list: Liste des enseignats ENT + """ + try: + exemple_users = [ + { + "ent_id": "user101", + "nom": "TestCollabNom", + "prenom": "TestCollaPrenNom", + "mail": "jean.dupont@mail.fr", + "profil": "Parent", + "ecole": "Collège Victor Hugo", + "rue": "12 rue de la République", + "code_postal": "75012", + "ville": "Paris", + "travail_tel": "0145236789", + "travail_mail": "jean.dupont@entreprise.fr", + "tel_domicile": "0145789654", + "tel_mobile": "0601020304" + }, + { + "ent_id": "user102", + "nom": "TestCollabNom", + "prenom": "TestCollaPrenNom", + "mail": "sophie.martin@mail.fr", + "profil": "Tuteur", + "ecole": "Collège Victor Hugo", + "rue": "45 avenue de la Liberté", + "code_postal": "69007", + "ville": "Lyon", + "travail_tel": "0478234567", + "travail_mail": "sophie.martin@entreprise.fr", + "tel_domicile": "0478456987", + "tel_mobile": "0698765432" + }, + { + "ent_id": "user103", + "nom": "Bernard", + "prenom": "Luc", + "mail": "luc.bernard@mail.fr", + "profil": "Parent", + "ecole": "Lycée Louis Pasteur", + "rue": "78 boulevard Saint-Michel", + "code_postal": "34000", + "ville": "Montpellier", + "travail_tel": "0499554433", + "travail_mail": "luc.bernard@entreprise.fr", + "tel_domicile": "0499678877", + "tel_mobile": "0678901234" + }, + { + "ent_id": "user104", + "nom": "Bersellou", + "prenom": "Mustapha", + "mail": "jean.dupont@mail.fr", + "profil": "Parent", + "ecole": "Collège Victor Hugo", + "rue": "12 rue de la République", + "code_postal": "75012", + "ville": "Paris", + "travail_tel": "0145236789", + "travail_mail": "mus.bers@entreprise.fr", + "tel_domicile": "0145789654", + "tel_mobile": "0601020304" + }, + ] + result = [] + organisateur = Organisateur.objects.filter(pk=1).first() + if organisateur.ent_active: + result = [ + user for user in exemple_users + if user["nom"].lower() == nom.lower() and user["prenom"].lower() == prenom.lower() + ] + return result + else: + return [] + + except Exception as e: + print(f"Erreur lors de la recherche ENT: {e}") + return [] + +def get_collaborateur_by_ent_id(ent_id): + """ + Fonction pour rechercher de Personnel Education National dans l'ENT + + Args: + nom (str): Nom de famille + prenom (str): Prénom + + Returns: + list: Liste des enseignats ENT + """ + try: + exemple_users = [ + { + "ent_id": "user101", + "nom": "TestCollabNom", + "prenom": "TestCollaPrenNom", + "mail": "jean.dupont@mail.fr", + "profil": "Parent", + "ecole": "Collège Victor Hugo", + "rue": "12 rue de la République", + "code_postal": "75012", + "ville": "Paris", + "travail_tel": "0145236789", + "travail_mail": "jean.dupont@entreprise.fr", + "tel_domicile": "0145789654", + "tel_mobile": "0601020304" + }, + { + "ent_id": "user102", + "nom": "TestCollabNom", + "prenom": "TestCollaPrenNom", + "mail": "sophie.martin@mail.fr", + "profil": "Tuteur", + "ecole": "Collège Victor Hugo", + "rue": "45 avenue de la Liberté", + "code_postal": "69007", + "ville": "Lyon", + "travail_tel": "0478234567", + "travail_mail": "sophie.martin@entreprise.fr", + "tel_domicile": "0478456987", + "tel_mobile": "0698765432" + }, + { + "ent_id": "user103", + "nom": "Bernard", + "prenom": "Luc", + "mail": "luc.bernard@mail.fr", + "profil": "Parent", + "ecole": "Lycée Louis Pasteur", + "rue": "78 boulevard Saint-Michel", + "code_postal": "34000", + "ville": "Montpellier", + "travail_tel": "0499554433", + "travail_mail": "luc.bernard@entreprise.fr", + "tel_domicile": "0499678877", + "tel_mobile": "0678901234" + }, + { + "ent_id": "user104", + "nom": "Bersellou", + "prenom": "Mustapha", + "mail": "jean.dupont@mail.fr", + "profil": "Parent", + "ecole": "Collège Victor Hugo", + "rue": "12 rue de la République", + "code_postal": "75012", + "ville": "Paris", + "travail_tel": "0145236789", + "travail_mail": "mus.bers@entreprise.fr", + "tel_domicile": "0145789654", + "tel_mobile": "0601020304" + }, + ] + + organisateur = Organisateur.objects.filter(pk=1).first() + if organisateur.ent_active: + for collab in exemple_users: + if collab["ent_id"] == ent_id: + return collab + return None + else: + return None + + except Exception as e: + print(f"Erreur lors de la recherche ENT: {e}") + return [] + +def get_enfant_famille(ent_id): + """ + Récupère la famille d'un enfant à partir de son ent_id + + Args: + ent_id (str): L'identifiant ENT de l'enfant + + Returns: + dict: Un dictionnaire contenant: + - representants: liste des représentants de la famille + - enfant: l'objet enfant + None si l'enfant n'est pas trouvé + """ + try: + # Données d'exemple des familles ENT + exemple_familles = [ + { + "representants": [ + { + "id_ent": "user179", + "civilite": "Monsieur", + "prenom": "Test10", + "nom": "Test10", + "email": "jean.dupont@mail.fr", + "telephone": "0601020304", + } + ], + "enfants": [ + { + "id_ent": "user131", + "civilite": "Monsieur", + "prenom": "Lucas", + "nom": "Dupont", + "scolarite": { + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole": { + "id": 10, + "nom": "Collège Victor Hugo", + "rue": "12 rue de la République", + "cp": "75001", + "ville": "Paris", + "tel": "0140203040", + "fax": "0140203041", + "mail": "contact@victorhugo.fr", + "uai": "0751234A", + }, + "classe": { + "id": 101, + "nom": "6ème A", + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole_id": 10, + }, + "niveau": { + "id": 6, + "ordre": 6, + "nom": "Sixième", + "abrege": "6ème", + }, + }, + }, + { + "id_ent": "user132", + "civilite": "Madame", + "prenom": "Emma", + "nom": "Dupont", + "scolarite": { + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole": { + "id": 10, + "nom": "Collège Victor Hugo", + "rue": "12 rue de la République", + "cp": "75001", + "ville": "Paris", + "tel": "0140203040", + "fax": "0140203041", + "mail": "contact@victorhugo.fr", + "uai": "0751234A", + }, + "classe": { + "id": 102, + "nom": "5ème B", + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole_id": 10, + }, + "niveau": { + "id": 5, + "ordre": 5, + "nom": "Cinquième", + "abrege": "5ème", + }, + }, + }, + ], + }, + { + "representants": [ + { + "id_ent": "user2087", + "civilite": "Monsieur", + "prenom": "Test10", + "nom": "Test10", + "email": "paul.martin@mail.fr", + "telephone": "0610101010", + } + ], + "enfants": [ + { + "id_ent": "user3001", + "civilite": "Madame", + "prenom": "Clara", + "nom": "Martin", + "scolarite": { + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole": { + "id": 20, + "nom": "École élémentaire Jean Moulin", + "rue": "25 avenue des Écoles", + "cp": "69001", + "ville": "Lyon", + "tel": "0472101112", + "fax": "0472101113", + "mail": "contact@jeanmoulin.fr", + "uai": "0695678B", + }, + "classe": { + "id": 201, + "nom": "CE2", + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole_id": 20, + }, + "niveau": { + "id": 3, + "ordre": 3, + "nom": "Cours Élémentaire 2", + "abrege": "CE2", + }, + }, + }, + { + "id_ent": "user3002", + "civilite": "Monsieur", + "prenom": "Tom", + "nom": "Martin", + "scolarite": { + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole": { + "id": 21, + "nom": "École élémentaire Jean Moulin", + "rue": "25 avenue des Écoles", + "cp": "69001", + "ville": "Lyon", + "tel": "0472101112", + "fax": "0472101113", + "mail": "contact@jeanmoulin.fr", + "uai": "0695678B", + }, + "classe": { + "id": 202, + "nom": "CM1", + "date_debut": "2024-09-01", + "date_fin": "2025-06-30", + "ecole_id": 21, + }, + "niveau": { + "id": 4, + "ordre": 4, + "nom": "Cours Moyen 1", + "abrege": "CM1", + }, + }, + }, + ], + }, + ] + + organisateur = Organisateur.objects.filter(pk=1).first() + if not organisateur or not organisateur.ent_active: + return None + + # Chercher l'enfant dans toutes les familles + for famille in exemple_familles: + for enfant in famille.get("enfants", []): + if enfant.get("id_ent") == ent_id: + # Retourner les représentants et l'enfant trouvé + return { + "representants": famille.get("representants", []), + "enfant": enfant + } + + return None + + except Exception as e: + print(f"Erreur lors de la recherche de la famille de l'enfant: {e}") + return None + + +def get_ent_ecole(uai): + """ + Fonction pour rechercher une école dans l'ENT par son UAI + + Args: + uai (str): identifiant UAI de l'école + + Returns: + dict: école trouvée avec cet UAI ou None si non trouvée + """ + try: + exemple_ecoles = [ + { + "uai": "0751234A", + "nom": "École Primaire Jean Moulin", + "rue": "12 rue de la République", + "cp": "75012", + "ville": "Paris", + "telephone": "0145236789", + "mail": "contact@ecole-jeanmoulin.fr", + "niveaux": [ + {"ordre": 1, "nom": "CP", "abrege": "CP"}, + {"ordre": 2, "nom": "CE1", "abrege": "CE1"}, + ], + "classes": [ + {"nom": "CP A", "date_debut": "2024-09-01", "date_fin": "2025-07-05", "niveau_nom": "CP"}, + {"nom": "CP B", "date_debut": "2024-09-01", "date_fin": "2025-07-05", "niveau_nom": "CP"}, + {"nom": "CE1 A", "date_debut": "2024-09-01", "date_fin": "2025-07-05", "niveau_nom": "CE1"}, + ], + }, + { + "uai": "0695678B", + "nom": "Collège Victor Hugo", + "rue": "25 avenue des Lumières", + "cp": "69008", + "ville": "Lyon", + "telephone": "0472983456", + "mail": "secretariat@college-vhugo.fr", + "niveaux": [ + {"ordre": 6, "nom": "6ème", "abrege": "6e"}, + {"ordre": 5, "nom": "5ème", "abrege": "5e"}, + ], + "classes": [ + {"nom": "6ème 1", "date_debut": "2024-09-01", "date_fin": "2025-07-05", "niveau_nom": "6ème"}, + {"nom": "6ème 2", "date_debut": "2024-09-01", "date_fin": "2025-07-05", "niveau_nom": "6ème"}, + {"nom": "5ème A", "date_debut": "2024-09-01", "date_fin": "2025-07-05", "niveau_nom": "5ème"}, + {"nom": "5ème B", "date_debut": "2024-09-01", "date_fin": "2025-07-05", "niveau_nom": "5ème"}, + ], + }, + { + "uai": "1837227A", + "nom": "École Primaire Jean Moulin 2", + "rue": "12 rue de la République", + "cp": "75012", + "ville": "Paris", + "telephone": "0145236789", + "mail": "contact@ecole-jeanmoulin.fr", + "niveaux": [ + {"ordre": 1, "nom": "CP", "abrege": "CP"}, + {"ordre": 2, "nom": "CE1", "abrege": "CE1"}, + ], + "classes": [ + {"nom": "CP A", "date_debut": "2024-09-01", "date_fin": "2025-07-05", "niveau_nom": "CP"}, + {"nom": "CP B", "date_debut": "2024-09-01", "date_fin": "2025-07-05", "niveau_nom": "CP"}, + {"nom": "CE1 A", "date_debut": "2024-09-01", "date_fin": "2025-07-05", "niveau_nom": "CE1"}, + ], + }, + ] + + + organisateur = Organisateur.objects.filter(pk=1).first() + if organisateur and organisateur.ent_active: + for ecole in exemple_ecoles: + if ecole["uai"] == uai: + return ecole + return None + else: + return None + + except Exception as e: + print(f"Erreur lors de la recherche ENT: {e}") + return None diff --git a/noethysweb/core/views/menu.py b/noethysweb/core/views/menu.py index 0e6b837b..ef96eed0 100644 --- a/noethysweb/core/views/menu.py +++ b/noethysweb/core/views/menu.py @@ -29,6 +29,7 @@ def GetMenuPrincipal(parametres_generaux=None, organisateur=None, user=None): menu_structure.Add(code="organisateur_ajouter", titre="Organisateur", icone="file-text-o", compatible_demo=False) menu_structure.Add(code="structures_liste", titre="Structures", icone="file-text-o", compatible_demo=False) menu_structure.Add(code="parametres_generaux", titre="Paramètres généraux ", icone="file-text-o") + menu_structure.Add(code="parametres_ent", titre="Paramètres ENT ", icone="file-text-o") menu_structure.Add(code="parametres_mail_expedition", titre="Paramètres d’expédition des e-mails", icone="file-text-o") # Activités @@ -253,6 +254,8 @@ def GetMenuPrincipal(parametres_generaux=None, organisateur=None, user=None): menu_gestion_individus.Add(code="individus_recherche_avancee", titre="Recherche avancée d'individus", icone="file-text-o") menu_gestion_individus.Add(code="effacer_familles", titre="Effacer des fiches familles", icone="file-text-o") menu_gestion_individus.Add(code="importer_individus", titre="Importer des individus", icone="file-text-o") + menu_gestion_individus.Add(code="synchroniser_familles", titre="Synchroniser des familles", icone="file-text-o") + menu_gestion_individus.Add(code="mettre_a_jour_liste_individu_ent", titre="Mettre à jour des individus ENT", icone="file-text-o") # Inscriptions menu_inscriptions = menu_individus.Add(titre="Inscriptions") diff --git a/noethysweb/fiche_famille/templates/fiche_famille/famille_enfant_ent.html b/noethysweb/fiche_famille/templates/fiche_famille/famille_enfant_ent.html new file mode 100644 index 00000000..d51a8d9e --- /dev/null +++ b/noethysweb/fiche_famille/templates/fiche_famille/famille_enfant_ent.html @@ -0,0 +1,238 @@ +{% extends "core/page.html" %} +{% load crispy_forms_tags %} +{% load static %} + +{% block styles %} + {{ block.super }} + +{% endblock %} + +{% block contenu_page %} + + +{% if enfant %} +
+
+ Famille de {{ enfant.civilite }} {{ enfant.prenom }} {{ enfant.nom }} +
+
+ +
+ Enfant +
+
+
+ {{ enfant.civilite }} {{ enfant.prenom }} {{ enfant.nom }} +
+ {% if enfant.scolarite.ecole %} +
+ 🏫 {{ enfant.scolarite.ecole.nom }} +
+ {% endif %} + {% if enfant.scolarite.classe %} +
+ 📘 Classe: {{ enfant.scolarite.classe.nom }} +
+ {% endif %} +
+ + +
+ Représentants +
+ {% for representant in representants %} +
+
+ {{ representant.civilite }} {{ representant.prenom }} {{ representant.nom }} +
+
+ 📧 Email: {{ representant.email|default:"Non renseigné" }} +
+
+ 📞 Téléphone: {{ representant.telephone|default:"Non renseigné" }} +
+
+ {% empty %} +
Aucun représentant
+ {% endfor %} + + +
+

+ Ajouter cette famille complète (représentants + enfant) +

+
+ {% csrf_token %} + +
+
+
+
+{% else %} +
+
+
+ +

Aucune famille trouvée pour cet enfant.

+
+
+
+{% endif %} +{% endblock contenu_page %} diff --git a/noethysweb/fiche_famille/templates/fiche_famille/famille_ent_liste.html b/noethysweb/fiche_famille/templates/fiche_famille/famille_ent_liste.html new file mode 100644 index 00000000..77fcb46a --- /dev/null +++ b/noethysweb/fiche_famille/templates/fiche_famille/famille_ent_liste.html @@ -0,0 +1,291 @@ +{% extends "core/page.html" %} +{% load crispy_forms_tags %} +{% load static %} +{% load embed %} + +{% block styles %} + {{ block.super }} + +{% endblock %} + +{% block contenu_page %} + + +{% if rattachements %} + {% for famille in rattachements %} +
+
+ {{ famille.representant.civilite }} {{ famille.representant.prenom }} {{ famille.representant.nom }} +
+ {% csrf_token %} + + + +
+
+
+ +
+
+ 📧 Email: {{ famille.representant.email|default:"Non renseigné" }} +
+
+ 📞 Téléphone: {{ famille.representant.telephone|default:"Non renseigné" }} +
+
+ + +
+ Enfants +
+ {% for enfant in famille.enfants %} +
+
+ {{ enfant.civilite }} {{ enfant.prenom }} {{ enfant.nom }} +
+ {% if enfant.scolarite.ecole %} +
+ 🏫 {{ enfant.scolarite.ecole.nom }} +
+ {% endif %} + {% if enfant.scolarite.classe %} +
+ 📘 Classe: {{ enfant.scolarite.classe.nom }} +
+ {% endif %} +
+ {% empty %} +
Aucun enfant
+ {% endfor %} +
+
+ {% endfor %} +{% else %} +
+ +

Aucun résultat pour cette recherche.

+
+{% endif %} + +
+
+ Pas trouvé le bon individu ? +
+
+
+

+ Aucun des individus listés ne correspond à celui que vous souhaitez ajouter ? +

+
+ {% csrf_token %} + + +
+
+
+
+ + +{% endblock contenu_page %} \ No newline at end of file diff --git a/noethysweb/fiche_famille/templates/fiche_famille/familles_synchro.html b/noethysweb/fiche_famille/templates/fiche_famille/familles_synchro.html new file mode 100644 index 00000000..8627d8af --- /dev/null +++ b/noethysweb/fiche_famille/templates/fiche_famille/familles_synchro.html @@ -0,0 +1,746 @@ +{% extends "core/page.html" %} +{% load static %} + +{% block contenu_page %} + + +
+

+ Familles non synchronisées + {{ familles_non_sync|length }} +

+ + {% if familles_non_sync %} + {% for fam in familles_non_sync %} +
+
+ + Famille {{ fam.nom }} + + {{ fam.rattachement_set.count }} membre{{ fam.rattachement_set.count|pluralize }} + + + +
+
+
+

+ + Cliquez sur "Vérifier et Synchroniser" pour comparer avec les données de l'ENT +

+
+ +
+
+ {% endfor %} + {% else %} +
+ +

Aucune famille non synchronisée trouvée

+
+ {% endif %} +
+ + +{% endblock %} \ No newline at end of file diff --git a/noethysweb/fiche_famille/urls.py b/noethysweb/fiche_famille/urls.py index 4b31bc6f..e6d70847 100644 --- a/noethysweb/fiche_famille/urls.py +++ b/noethysweb/fiche_famille/urls.py @@ -10,20 +10,25 @@ famille_abo_recus_email, famille_abo_depots_email, famille_outils, famille_attestations, famille_devis, famille_historique, famille_export_xml, famille_sms, \ famille_voir_rappel, famille_rappels, famille_portail, famille_emails, reglement_recu, famille_messagerie_portail, famille_mandats, famille_voir_mandat, famille_prestations_modele, \ famille_attestations_fiscales, famille_voir_attestation_fiscale, famille_locations, famille_voir_location, famille_remboursement, famille_factures_consulter, famille_factures_selection, \ - famille_edition_renseignements, reglement_recu_auto, famille_formulaires, famille_releve_prestations, famille_liste_consommations + famille_edition_renseignements, reglement_recu_auto, famille_formulaires, famille_releve_prestations, famille_liste_consommations, famille_ent urlpatterns = [ # # Individus path('individus/individus/ajouter/', famille_ajouter.Ajouter_individu.as_view(), name='individu_ajouter'), + path('individus/individus/ent_liste/', famille_ent.EntListeIndividus.as_view(), name='ent_liste_individus'), path('individus/individus/supprimer//', famille_ajouter.Supprimer_individu.as_view(), name='individu_supprimer'), path('individus/individus/detacher//', famille_ajouter.Detacher_individu.as_view(), name='individu_detacher'), # Familles path('individus/familles/liste', famille.Liste.as_view(), name='famille_liste'), + path('individus/familles/ent_liste/', famille_ent.EntListeIndividus.as_view(), name='ent_liste_familles'), path('individus/familles/ajouter', famille_ajouter.Creer_famille.as_view(), name='famille_ajouter'), path('individus/familles/supprimer/', famille.Supprimer_famille.as_view(), name='famille_supprimer'), path('individus/familles/resume/', famille.Resume.as_view(), name='famille_resume'), + path('individus/familles/ent/synchroniser', famille_ent.FamillesSynchroView.as_view(), name='synchroniser_familles'), + path('individus/familles/ent/enfant//', famille_ent.FamilleEnfantEntView.as_view(), name='famille_enfant_ent'), + path('individus/familles/ent/enfant//famille//', famille_ent.FamilleEnfantEntView.as_view(), name='famille_enfant_ent_avec_famille'), path('individus/familles/questionnaire/', famille_questionnaire.Consulter.as_view(), name='famille_questionnaire'), path('individus/familles/questionnaire/modifier/', famille_questionnaire.Modifier.as_view(), name='famille_questionnaire_modifier'), diff --git a/noethysweb/fiche_famille/views/famille_ajouter.py b/noethysweb/fiche_famille/views/famille_ajouter.py index 929ab8e4..92d050b3 100644 --- a/noethysweb/fiche_famille/views/famille_ajouter.py +++ b/noethysweb/fiche_famille/views/famille_ajouter.py @@ -114,7 +114,25 @@ def form_valid(self, form): categorie = int(form.cleaned_data["categorie"]) titulaire = form.cleaned_data["titulaire"] idfamille = int(self.request.POST["idfamille"]) - + if action == "CREER" and form.has_redirect_to_ent_liste(): + # Sauvegarde temporaire des données du formulaire en session + self.request.session['ent_users_data'] = form.get_ent_users_data() + self.request.session['idfamille_temp'] = form.cleaned_data.get("idfamille", 0) + nom = form.cleaned_data.get("nom", "") + prenom = form.cleaned_data.get("prenom", "") + civilite = form.cleaned_data.get("civilite", "") + self.request.session["search_info"] = { + "nom": nom, + "prenom": prenom, + "categorie": categorie, + "civilite": civilite + } + if idfamille == 0: + url_success = reverse_lazy("ent_liste_familles", kwargs={}) + else: + url_success = reverse_lazy("ent_liste_individus", kwargs={"idfamille": idfamille}) + return HttpResponseRedirect(url_success) + if idfamille == 0: famille = self.Creation_famille() else: diff --git a/noethysweb/fiche_famille/views/famille_ent.py b/noethysweb/fiche_famille/views/famille_ent.py new file mode 100644 index 00000000..3216a822 --- /dev/null +++ b/noethysweb/fiche_famille/views/famille_ent.py @@ -0,0 +1,856 @@ +from django.views.generic import TemplateView +from core.views.base import CustomView +from django.db import transaction +from core.utils import utils_questionnaires +from fiche_famille.utils import utils_internet +from core.models import Individu, Famille, Rattachement, Utilisateur, Ecole, Classe, Scolarite, NiveauScolaire +from django.http import HttpResponseRedirect +from django.urls import reverse_lazy +import logging +from django.contrib import messages +from django.http import JsonResponse +from core.utils.utils_ent import get_ent_user_info, get_enfant_famille +from core.data import data_civilites +import time +import json + +logger = logging.getLogger(__name__) + + + +class EntListeIndividus(CustomView, TemplateView): + menu_code = "ent_liste_individus" + template_name = "fiche_famille/famille_ent_liste.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + idfamille = kwargs.get("idfamille", None) + context['rattachements'] = self.request.session.get('ent_users_data', []) + context['page_titre'] = "Liste des familles de l'ENT" + + # Récupération des informations de recherche depuis la session + search_info = self.request.session.get('search_info', {}) + context['search_nom'] = search_info.get('nom', '') + context['search_prenom'] = search_info.get('prenom', '') + context['search_categorie'] = search_info.get('categorie', '') + + if idfamille: + context["mode"] = "individus" + context["idfamille"] = int(idfamille) + else: + context["mode"] = "familles" + return context + + @transaction.atomic + def post(self, request, *args, **kwargs): + action = request.POST.get("action", "") + idfamille = kwargs.get("idfamille", None) + search_info = self.request.session.get('search_info', {}) + # Ce parametre est utilisé pour identifier s'il s'agit d'importer toute une famille de l'ent ou juste un individu à une famille + individu_id = request.POST.get("individu_id") + + # L'action est utilisée pour savoir si l'on veut simplement ajouter un nouvel individu indépendamment des données de l'ENT + if action == "ajouter_nouvel_individu": + # idfamille est utilisé pour vérifier s'il s'agit dun ajout d'un individu à une famille existante deja + if idfamille: + new_famille = Famille.objects.get(pk=idfamille) + else: + new_famille = self.creation_famille() + self.creation_nouvel_individu(search_info, new_famille) + new_famille.Maj_infos() + url_success = reverse_lazy("famille_resume", kwargs={'idfamille': new_famille.pk}) + + # Nouvelle action pour ajouter uniquement un représentant + elif action == "ajouter_representant": + familles = request.session.get("ent_users_data", []) + + if not individu_id: + messages.add_message(self.request, messages.ERROR, "Identifiant du représentant manquant") + return HttpResponseRedirect(reverse_lazy("ent_liste_familles")) + + # Trouver le représentant dans les données de session + individu_trouve = self.trouver_individu_par_ent_id(individu_id, familles) + + if not individu_trouve: + messages.add_message(self.request, messages.ERROR, "Représentant introuvable") + return HttpResponseRedirect(reverse_lazy("ent_liste_familles")) + + # Vérifier si on ajoute à une famille existante ou si on crée une nouvelle famille + if idfamille: + new_famille = Famille.objects.get(pk=idfamille) + else: + # Vérifier si ce représentant existe déjà dans une famille + existing_individu = Individu.objects.filter(ent_id=individu_id).first() + if existing_individu: + existing_rattachement = Rattachement.objects.filter(individu=existing_individu, categorie__in=[1, 3]).first() + if existing_rattachement: + messages.add_message(self.request, messages.ERROR, "Ce représentant existe déjà dans une famille") + return HttpResponseRedirect(reverse_lazy("famille_resume", kwargs={'idfamille': existing_rattachement.famille.pk})) + + # Créer une nouvelle famille + new_famille = self.creation_famille() + + # Ajouter le représentant (catégorie 1) + self.creation_individu_ent(individu_trouve, new_famille, 1) + new_famille.Maj_infos() + + messages.add_message(self.request, messages.SUCCESS, "Représentant ajouté avec succès") + url_success = reverse_lazy("famille_resume", kwargs={'idfamille': new_famille.pk}) + + else: + familles = request.session.get("ent_users_data", []) + if idfamille: + new_famille = Famille.objects.get(pk=idfamille) + if individu_id: + individu_trouve = self.trouver_individu_par_ent_id(individu_id, familles) + self.creation_individu_ent(individu_trouve, new_famille, search_info.get('categorie', '')) + else: + index = int(request.POST.get("famille_index")) + famille = familles[index] + # Avant de créer la famille, on vérifie si l'un des enfants appartient déjà à une famille dans la base (dans ce cas, cette famille existe déjà). + # Si une telle famille existe, on redirige alors l'utilisateur vers la fiche de cette famille. + new_famille = self.chercher_famille_avec_ent_id(famille) + if new_famille: + messages.add_message(self.request, messages.ERROR, "Famille existe déjà") + else: + new_famille = self.creation_famille() + for indiv in famille["representants"]: + self.creation_individu_ent(indiv, new_famille, 1) + + for enf in famille["enfants"]: + self.creation_individu_ent(enf, new_famille, 2) + new_famille.Maj_infos() + url_success = reverse_lazy("famille_resume", kwargs={'idfamille': new_famille.pk}) + + return HttpResponseRedirect(url_success) + + + def trouver_individu_par_ent_id(self, individu_id, familles): + """ + Trouve un individu dans les données de session par son id_ent + """ + for famille in familles: + # Chercher dans les représentants + if str(famille.get("representant", None).get("id_ent")) == str(individu_id): + return famille.get("representant", None) + + # Chercher dans les enfants + for enfant in famille.get("enfants", []): + if str(enfant.get("id_ent")) == str(individu_id): + return enfant + + return None + + @transaction.atomic + def creation_famille(self): + """ Le transaction.atomic permet de faire que les enregistrements suivants soient tous effectués en même temps dans la db """ + famille = Famille.objects.create() + + # Création des questionnaires de type famille + utils_questionnaires.Creation_reponses(categorie="famille", liste_instances=[famille]) + + # Création et enregistrement des codes pour le portail + internet_identifiant = utils_internet.CreationIdentifiant(IDfamille=famille.pk) + internet_mdp, date_expiration_mdp = utils_internet.CreationMDP() + + # Mémorisation des codes internet dans la table familles + famille.internet_identifiant = internet_identifiant + famille.internet_mdp = internet_mdp + + # Création de l'utilisateur + utilisateur = Utilisateur(username=internet_identifiant, categorie="famille", force_reset_password=True, date_expiration_mdp=date_expiration_mdp) + utilisateur.set_password(internet_mdp) + utilisateur.save() + # Association de l'utilisateur à la famille + famille.utilisateur = utilisateur + famille.save() + return famille + + + @transaction.atomic + def creation_nouvel_individu(self, new_indiv, famille): + categorie = new_indiv.get("categorie") + individu = Individu( + # Attributs principaux + prenom=new_indiv.get("prenom"), + nom=new_indiv.get("nom"), + civilite=new_indiv.get("civilite") + ) + individu.save() + # Création des questionnaires de type individu + utils_questionnaires.Creation_reponses(categorie="individu", liste_instances=[individu]) + internet_identifiant_individu = utils_internet.CreationIdentifiantIndividu(IDindividu=individu.pk) + internet_mdp_individu, date_expiration_mdp_individu = utils_internet.CreationMDP() + individu.internet_identifiant = internet_identifiant_individu + individu.internet_mdp = internet_mdp_individu + + # Vous pouvez aussi créer un utilisateur pour l'individu si nécessaire + utilisateur_individu = Utilisateur( + username=internet_identifiant_individu, + categorie="individu", # Ou une autre catégorie, selon votre besoin + force_reset_password=True, + date_expiration_mdp=date_expiration_mdp_individu + ) + utilisateur_individu.set_password(internet_mdp_individu) + utilisateur_individu.save() + + # Association de l'utilisateur à l'individu + individu.utilisateur = utilisateur_individu + individu.save() + titulaire = 1 if categorie!=2 else 0 + rattachement = Rattachement(famille=famille, individu=individu, categorie=categorie, titulaire=titulaire) + rattachement.save() + + + @transaction.atomic + def creation_individu_ent(self, new_indiv, famille, categorie): + individu = Individu.objects.filter(ent_id=new_indiv.get("id_ent")).first() + if not individu: + individu = Individu( + # Attributs principaux + prenom=new_indiv.get("prenom"), + nom=new_indiv.get("nom"), + civilite=1 if new_indiv.get("civilite") in ("M.", "Mr", "Monsieur") else 2, # mapping simple + mail=new_indiv.get("email"), + tel_mobile=new_indiv.get("telephone"), + internet_actif=True, + ent_id=new_indiv.get("id_ent") + ) + individu.save() + # Création des questionnaires de type individu + utils_questionnaires.Creation_reponses(categorie="individu", liste_instances=[individu]) + internet_identifiant_individu = utils_internet.CreationIdentifiantIndividu(IDindividu=individu.pk) + internet_mdp_individu, date_expiration_mdp_individu = utils_internet.CreationMDP() + individu.internet_identifiant = internet_identifiant_individu + individu.internet_mdp = internet_mdp_individu + + # Vous pouvez aussi créer un utilisateur pour l'individu si nécessaire + utilisateur_individu = Utilisateur( + username=internet_identifiant_individu, + categorie="individu", # Ou une autre catégorie, selon votre besoin + force_reset_password=True, + date_expiration_mdp=date_expiration_mdp_individu + ) + utilisateur_individu.set_password(internet_mdp_individu) + utilisateur_individu.save() + + # Association de l'utilisateur à l'individu + individu.utilisateur = utilisateur_individu + individu.save() + titulaire = 1 if categorie==1 else 0 + rattachement = Rattachement(famille=famille, individu=individu, categorie=categorie, titulaire=titulaire) + rattachement.save() + if new_indiv.get("scolarite"): + self.creation_scolarite(individu, new_indiv.get("scolarite")) + + + @transaction.atomic + def creation_scolarite(self, new_indiv, scolarite): + # Vérifie si cet utilisateur a déjà une scolarité + scolarite_ancienne = Scolarite.objects.filter(individu=new_indiv).first() + if not scolarite_ancienne: + # Création / récupération école + ecole= Ecole.objects.filter( + uai=scolarite.get("ecole", {}).get("uai")).first() + + # Récupérer les données de la classe depuis l'ENT + classe_data = scolarite.get("classe", {}) + date_debut = scolarite.get("date_debut") or classe_data.get("date_debut") + date_fin = scolarite.get("date_fin") or classe_data.get("date_fin") + + # Création / récupération classe avec les dates requises + classe, created = Classe.objects.get_or_create( + nom=classe_data.get("nom"), + ecole=ecole, + defaults={ + 'date_debut': date_debut, + 'date_fin': date_fin + } + ) + + # Création / récupération niveau + niveau= NiveauScolaire.objects.filter(nom=scolarite.get("niveau", {}).get("nom")).first() + + # Création scolarité + scolarite_obj = Scolarite.objects.create( + individu=new_indiv, + date_debut=date_debut, + date_fin=date_fin, + ecole=ecole, + classe=classe, + niveau=niveau + ) + scolarite_obj.save() + + + def chercher_famille_avec_ent_id(self, famille_ent): + # Cette fonction vérifie si une famille existe déjà en utilisant les ent_id des enfants + for enfant in famille_ent.get("enfants", []): + rattachemnt = Rattachement.objects.filter(individu__ent_id = enfant.get("id_ent", "")).first() + if rattachemnt: + return rattachemnt.famille + return None + +class FamillesSynchroView(CustomView, TemplateView): + template_name = "fiche_famille/familles_synchro.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["familles_non_sync"] = self.get_familles_non_sync() + context["page_titre"] = "Familles non synchronisées" + return context + + def get_familles_non_sync(self): + rattachements = Rattachement.objects.filter(individu__ent_id__isnull=True) + familles = Famille.objects.filter(idfamille__in=rattachements.values_list('famille_id', flat=True).distinct()) + return familles + + def post(self, request, *args, **kwargs): + """Gérer la synchronisation d'une famille""" + try: + # Vérifier si c'est une requête JSON (validation) ou form data (vérification) + if request.content_type == 'application/json': + data = json.loads(request.body) + action = data.get('action') + logger.info(f"Action: {action}") + + if action == 'validate': + return self.valider_synchronisation(data) + + # Sinon, c'est une vérification classique + famille_id = request.POST.get('famille_id') + logger.info(f"Vérification de la famille ID: {famille_id}") + time.sleep(1) + + famille = Famille.objects.get(idfamille=famille_id) + logger.info(f"Famille trouvée: {famille}") + + differences = self.comparer_famille(famille) + logger.info(f"Différences trouvées: {len(differences.get('representants', []))} représentants, {len(differences.get('enfants', []))} enfants") + + return JsonResponse({ + 'success': True, + 'differences': differences + }) + + except Famille.DoesNotExist: + logger.error(f"Famille {famille_id} introuvable") + return JsonResponse({ + 'success': False, + 'error': 'Famille introuvable' + }, status=404) + except Exception as e: + logger.exception(f"Erreur dans post(): {str(e)}") + return JsonResponse({ + 'success': False, + 'error': str(e) + }, status=500) + + def valider_synchronisation(self, data): + """ + Valide et applique les modifications sélectionnées + """ + try: + famille_id = data.get('famille_id') + selected_fields = data.get('selected_fields', {}) + + if not selected_fields: + return JsonResponse({ + 'success': False, + 'error': 'Aucun champ sélectionné' + }, status=400) + + famille = Famille.objects.get(idfamille=famille_id) + updated_count = 0 + errors = [] + + # Traiter chaque individu séparément avec sa propre transaction + for key, person_data in selected_fields.items(): + individu_id = person_data.get('id') + person_type = person_data.get('type') + fields_to_update = person_data.get('fields', []) + # Récupérer l'index de la personne sélectionnée (si plusieurs personnes avec même nom/prénom) + index_personne = person_data.get('index_personne', 0) + + try: + # Transaction séparée pour chaque individu + with transaction.atomic(): + # Récupérer l'individu + individu = Individu.objects.select_for_update().get(idindividu=individu_id) + + # Récupérer les données de l'API (toutes les personnes avec ce nom/prénom) + ent_results = get_ent_user_info(individu.nom, individu.prenom) + + if not ent_results: + errors.append(f"{individu.prenom} {individu.nom}: Données API introuvables") + continue + + # S'assurer que c'est une liste + if not isinstance(ent_results, list): + ent_results = [ent_results] + + # Sélectionner la bonne personne selon l'index + if index_personne >= len(ent_results): + errors.append(f"{individu.prenom} {individu.nom}: Index de personne invalide") + continue + + individu_api = ent_results[index_personne] + + # Appliquer les modifications pour chaque champ sélectionné + for field_label in fields_to_update: + result = self.appliquer_modification(individu, individu_api, field_label) + if result['success']: + updated_count += 1 + else: + errors.append(f"{individu.prenom} {individu.nom} - {field_label}: {result['error']}") + individu.ent_id = individu_api.get("ent_id", None) + # Sauvegarder l'individu + individu.save() + + except Individu.DoesNotExist: + errors.append(f"Individu ID {individu_id} introuvable") + except Exception as e: + errors.append(f"Erreur pour l'individu ID {individu_id}: {str(e)}") + + response_data = { + 'success': True, + 'updated_count': updated_count, + 'message': f'{updated_count} champ(s) mis à jour avec succès' + } + + if errors: + response_data['warnings'] = errors + + return JsonResponse(response_data) + + except Famille.DoesNotExist: + return JsonResponse({ + 'success': False, + 'error': 'Famille introuvable' + }, status=404) + except Exception as e: + return JsonResponse({ + 'success': False, + 'error': f'Erreur lors de la synchronisation: {str(e)}' + }, status=500) + + def appliquer_modification(self, individu, individu_api, field_label): + """ + Applique une modification à un individu à partir des données API + Retourne un dict avec 'success' et 'error' si applicable + """ + # Mapping inverse: label français -> (champ DB, champ API) + field_mapping = { + 'Nom': ('nom', 'nom'), + 'Prénom': ('prenom', 'prenom'), + 'Civilité': ('civilite', 'civilite'), + 'Date de naissance': ('date_naiss', 'date_naissance'), + 'Code postal de naissance': ('cp_naiss', 'code_postal_naissance'), + 'Ville de naissance': ('ville_naiss', 'ville_naissance'), + 'Email': ('mail', 'email'), + 'Téléphone mobile': ('tel_mobile', 'telephone_mobile'), + 'Téléphone domicile': ('tel_domicile', 'telephone'), + 'Adresse': ('adresse_auto', 'adresse'), + 'Code postal': ('cp_auto', 'code_postal'), + 'Ville': ('ville_auto', 'ville'), + 'ID ENT': ('ent_id', 'ent_id'), + } + + if field_label not in field_mapping: + return {'success': False, 'error': 'Champ inconnu'} + + champ_db, champ_api = field_mapping[field_label] + + try: + # Extraire la valeur de l'API + valeur_api = self.extraire_valeur_api(individu_api, champ_api) + + if valeur_api is None: + return {'success': False, 'error': 'Valeur API introuvable'} + + # Traitement spécial pour la civilité + if champ_db == 'civilite': + valeur_api = self.convertir_civilite(valeur_api) + + # Traitement spécial pour les dates + if champ_db == 'date_naiss' and isinstance(valeur_api, str): + from datetime import datetime + try: + # Essayer différents formats de date + for fmt in ['%Y-%m-%d', '%d/%m/%Y', '%d-%m-%Y']: + try: + valeur_api = datetime.strptime(valeur_api, fmt).date() + break + except ValueError: + continue + except: + return {'success': False, 'error': f'Format de date invalide: {valeur_api}'} + + # Appliquer la valeur + setattr(individu, champ_db, valeur_api) + return {'success': True} + + except Exception as e: + return {'success': False, 'error': str(e)} + + def convertir_civilite(self, valeur): + """ + Convertit la civilité de l'API en valeur compatible avec la base de données + Utilise le dictionnaire data_civilites pour la conversion + """ + from django.db.models import CharField, IntegerField + + field = Individu._meta.get_field('civilite') + + if isinstance(field, IntegerField): + # Créer un mapping inverse à partir de data_civilites + dict_civilites_data = data_civilites.GetDictCivilites() + + # Mapping: texte API -> ID civilité + mapping_civilite = {} + for civ_id, civ_info in dict_civilites_data.items(): + # Ajouter le label complet + if civ_info.get('label'): + mapping_civilite[civ_info['label']] = civ_id + # Ajouter l'abrégé si disponible + if civ_info.get('abrege'): + mapping_civilite[civ_info['abrege']] = civ_id + + # Ajouter des variations courantes + variations = { + 'M': 1, + 'M.': 1, + 'Mr': 1, + 'Melle': 2, # Correspond à Mademoiselle (id 2) + 'Mlle': 2, + } + mapping_civilite.update(variations) + + return mapping_civilite.get(valeur, 1) # Par défaut Monsieur (id=1) + + elif isinstance(field, CharField): + # Si c'est un CharField, retourner la valeur normalisée + return valeur + + else: + # Si c'est une ForeignKey, il faudrait récupérer l'objet correspondant + return valeur + + def comparer_famille(self, famille): + """ + Compare les données de chaque membre de la famille avec l'API. + Si plusieurs personnes dans l'ENT ont le même nom/prénom, on affiche toutes les familles correspondantes. + """ + differences = { + 'representants': [], + 'enfants': [], + 'info_recherche': None # Info sur les résultats de recherche + } + + rattachements = famille.rattachement_set.select_related('individu').all() + + for ratt in rattachements: + individu = ratt.individu + + try: + # get_ent_user_info retourne TOUTES les personnes avec ce nom/prénom + ent_results = get_ent_user_info(individu.nom, individu.prenom) + + if ent_results: + # S'assurer que c'est toujours une liste + if not isinstance(ent_results, list): + ent_results = [ent_results] + + nb_resultats = len(ent_results) + + # Message informatif si plusieurs résultats + if nb_resultats > 1 and not differences['info_recherche']: + differences['info_recherche'] = { + 'message': f"{nb_resultats} personnes trouvées avec le nom/prénom '{individu.prenom} {individu.nom}' dans l'ENT", + 'type': 'warning' + } + + # Afficher chaque personne trouvée dans l'ENT + for index, individu_api in enumerate(ent_results): + diff = self.comparer_individu(individu, individu_api) + + # Créer le nom d'affichage + nom_affiche = f"{individu.prenom} {individu.nom}" + + # Si plusieurs personnes, ajouter un badge pour différencier + if nb_resultats > 1: + # Récupérer des infos distinctives de l'API (email, téléphone, etc.) + info_distinctive = [] + if individu_api.get('email'): + info_distinctive.append(f"Email: {individu_api.get('email')}") + if individu_api.get('telephone'): + info_distinctive.append(f"Tél: {individu_api.get('telephone')}") + if individu_api.get('adresse'): + info_distinctive.append(f"Ville: {individu_api.get('ville', '')}") + + info_text = ' | '.join(info_distinctive[:2]) if info_distinctive else f"Personne {index + 1}" + nom_affiche += f" {info_text}" + + # Toujours afficher si plusieurs personnes, sinon seulement si différences + if diff or nb_resultats > 1: + diff_entry = { + 'id': individu.idindividu, + 'nom': nom_affiche, + 'differences': diff or {}, + 'multiple_personnes': nb_resultats > 1, + 'index_personne': index, + 'total_personnes': nb_resultats, + 'info_distinctive': individu_api.get('email', '') or individu_api.get('telephone', '') + } + + if ratt.categorie in [1, 3]: + differences['representants'].append(diff_entry) + else: + differences['enfants'].append(diff_entry) + else: + # Personne non trouvée dans l'ENT + diff_entry = { + 'id': individu.idindividu, + 'nom': f"{individu.prenom} {individu.nom}", + 'differences': { + 'statut': { + 'old': 'En base de données', + 'new': 'Non trouvé dans l\'ENT' + } + }, + 'multiple_personnes': False + } + + if ratt.categorie in [1, 2]: + differences['representants'].append(diff_entry) + else: + differences['enfants'].append(diff_entry) + + except Exception as e: + logger.exception(f"Erreur lors de la comparaison pour {individu.prenom} {individu.nom}") + diff_entry = { + 'id': individu.idindividu, + 'nom': f"{individu.prenom} {individu.nom}", + 'differences': { + 'erreur': { + 'old': 'Erreur', + 'new': f'Erreur API: {str(e)}' + } + }, + 'multiple_personnes': False + } + + if ratt.categorie in [1, 2]: + differences['representants'].append(diff_entry) + else: + differences['enfants'].append(diff_entry) + + return differences + + def comparer_individu(self, individu_db, individu_api): + """ + Compare un individu en base avec ses données API + """ + differences = {} + + champs_a_comparer = { + 'nom': 'nom', + 'prenom': 'prenom', + 'civilite': 'civilite', + 'date_naiss': 'date_naissance', + 'cp_naiss': 'code_postal_naissance', + 'ville_naiss': 'ville_naissance', + 'mail': 'email', + 'tel_mobile': 'telephone_mobile', + 'tel_domicile': 'telephone', + 'adresse_auto': 'adresse', + 'cp_auto': 'code_postal', + 'ville_auto': 'ville', + 'ent_id': 'id', + } + + for champ_db, champ_api in champs_a_comparer.items(): + valeur_db = getattr(individu_db, champ_db, None) + valeur_api = self.extraire_valeur_api(individu_api, champ_api) + + # Passer le nom du champ pour traitement spécial (civilité, etc.) + valeur_db_str = self.normaliser_valeur(valeur_db, champ_db) + valeur_api_str = self.normaliser_valeur(valeur_api, champ_db) + + if valeur_db_str != valeur_api_str: + nom_champ_affiche = self.traduire_nom_champ(champ_db) + + differences[nom_champ_affiche] = { + 'old': valeur_db_str if valeur_db_str else '(vide)', + 'new': valeur_api_str if valeur_api_str else '(vide)' + } + + return differences if differences else None + + def extraire_valeur_api(self, individu_api, champ): + """ + Extrait une valeur de l'objet API (qui peut être imbriqué) + """ + if '.' in champ: + keys = champ.split('.') + valeur = individu_api + for key in keys: + if isinstance(valeur, dict): + valeur = valeur.get(key) + else: + return None + return valeur + else: + return individu_api.get(champ) if isinstance(individu_api, dict) else None + + def normaliser_valeur(self, valeur, champ_db=None): + """ + Normalise une valeur pour la comparaison et l'affichage + """ + if valeur is None or valeur == '': + return '' + + # Traitement spécial pour la civilité + if champ_db == 'civilite': + # Récupérer le dictionnaire des civilités + dict_civilites_data = data_civilites.GetDictCivilites() + + # Si c'est un nombre (valeur DB) + if isinstance(valeur, int): + civilite_info = dict_civilites_data.get(valeur) + if civilite_info: + return civilite_info.get('abrege') or civilite_info.get('label', str(valeur)) + return str(valeur) + + # Si c'est déjà une chaîne (valeur API) + if isinstance(valeur, str): + # Normaliser les variations courantes + valeur_norm = valeur.strip() + mapping_api_to_abrege = { + 'M.': 'M.', + 'M': 'M.', + 'Monsieur': 'M.', + 'Mr': 'M.', + 'Mme': 'Mme', + 'Madame': 'Mme', + 'Melle': 'Melle', + 'Mlle': 'Melle', + 'Mademoiselle': 'Melle', + } + return mapping_api_to_abrege.get(valeur_norm, valeur_norm) + + # Traitement des dates + if hasattr(valeur, 'strftime'): + return valeur.strftime('%d/%m/%Y') + + valeur_str = str(valeur).strip() + + # Pour les numéros de téléphone, ne garder que les chiffres + if valeur_str and any(char.isdigit() for char in valeur_str) and champ_db in ['tel_mobile', 'tel_domicile']: + valeur_str = ''.join(filter(lambda x: x.isdigit() or x == '+', valeur_str)) + + return valeur_str + + def traduire_nom_champ(self, champ_db): + """ + Traduit les noms de champs techniques en français pour l'affichage + """ + traductions = { + 'nom': 'Nom', + 'prenom': 'Prénom', + 'civilite': 'Civilité', + 'date_naiss': 'Date de naissance', + 'cp_naiss': 'Code postal de naissance', + 'ville_naiss': 'Ville de naissance', + 'mail': 'Email', + 'tel_mobile': 'Téléphone mobile', + 'tel_domicile': 'Téléphone domicile', + 'adresse_auto': 'Adresse', + 'cp_auto': 'Code postal', + 'ville_auto': 'Ville', + 'ent_id': 'ID ENT', + } + + return traductions.get(champ_db, champ_db) + + +class FamilleEnfantEntView(CustomView, TemplateView): + """Vue pour afficher la famille d'un enfant spécifique depuis l'ENT""" + menu_code = "famille_enfant_ent" + template_name = "fiche_famille/famille_enfant_ent.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + enfant_ent_id = kwargs.get("enfant_ent_id", None) + idfamille = kwargs.get("idfamille", None) + + if enfant_ent_id: + famille_data = get_enfant_famille(enfant_ent_id) + if famille_data: + context['representants'] = famille_data.get('representants', []) + context['enfant'] = famille_data.get('enfant') + context['enfant_ent_id'] = enfant_ent_id + else: + context['representants'] = [] + context['enfant'] = None + messages.add_message(self.request, messages.ERROR, "Famille de l'enfant introuvable dans l'ENT") + + if idfamille: + context["idfamille"] = int(idfamille) + + context['page_titre'] = "Famille de l'enfant - ENT" + return context + + @transaction.atomic + def post(self, request, *args, **kwargs): + """Ajouter toute la famille (représentants + enfant)""" + enfant_ent_id = kwargs.get("enfant_ent_id", None) + idfamille = kwargs.get("idfamille", None) + + if not enfant_ent_id: + messages.add_message(request, messages.ERROR, "Identifiant de l'enfant manquant") + return HttpResponseRedirect(reverse_lazy("ent_liste_familles")) + + # Récupérer la famille de l'enfant + famille_data = get_enfant_famille(enfant_ent_id) + + if not famille_data: + messages.add_message(request, messages.ERROR, "Famille introuvable dans l'ENT") + return HttpResponseRedirect(reverse_lazy("ent_liste_familles")) + + # Vérifier si on ajoute à une famille existante ou si on crée une nouvelle famille + if idfamille: + new_famille = Famille.objects.get(pk=idfamille) + else: + # Vérifier si la famille existe déjà en base + enfant_obj = famille_data.get('enfant') + existing_rattachement = Rattachement.objects.filter( + individu__ent_id=enfant_obj.get('id_ent') + ).first() + + if existing_rattachement: + messages.add_message(request, messages.ERROR, "Cette famille existe déjà") + return HttpResponseRedirect(reverse_lazy("famille_resume", kwargs={'idfamille': existing_rattachement.famille.pk})) + + # Créer une nouvelle famille + view_instance = EntListeIndividus() + view_instance.request = request + new_famille = view_instance.creation_famille() + + # Ajouter les représentants + for representant in famille_data.get('representants', []): + view_instance = EntListeIndividus() + view_instance.request = request + view_instance.creation_individu_ent(representant, new_famille, 1) + + # Ajouter l'enfant + enfant_obj = famille_data.get('enfant') + view_instance = EntListeIndividus() + view_instance.request = request + view_instance.creation_individu_ent(enfant_obj, new_famille, 2) + + # Mettre à jour les infos de la famille + new_famille.Maj_infos() + + messages.add_message(request, messages.SUCCESS, "Famille ajoutée avec succès") + return HttpResponseRedirect(reverse_lazy("famille_resume", kwargs={'idfamille': new_famille.pk})) \ No newline at end of file diff --git a/noethysweb/fiche_individu/forms/individu.py b/noethysweb/fiche_individu/forms/individu.py index 2e388bcd..396d5266 100644 --- a/noethysweb/fiche_individu/forms/individu.py +++ b/noethysweb/fiche_individu/forms/individu.py @@ -8,10 +8,12 @@ from core.forms.base import FormulaireBase from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Hidden, Submit, HTML, Div, Column, Fieldset, ButtonHolder -from crispy_forms.bootstrap import Field, FormActions, PrependedText, StrictButton, InlineRadios +from crispy_forms.bootstrap import Field, InlineRadios from core.utils.utils_commandes import Commandes -from core.models import Individu, Rattachement, CATEGORIES_RATTACHEMENT +from core.models import Individu, Rattachement, CATEGORIES_RATTACHEMENT, Organisateur from core.forms.select2 import Select2Widget +from core.utils.utils_ent import get_ent_users + class Formulaire(FormulaireBase, ModelForm): @@ -60,9 +62,14 @@ def __init__(self, *args, **kwargs): # Si on saisit le premier individu de la fiche famille if not rattachements: - self.fields['categorie'].initial = 1 - self.fields['categorie'].widget.attrs['disabled'] = 'disabled' - self.fields['titulaire'].disabled = True + organisateur = Organisateur.objects.filter(pk=1).first() + if not organisateur.ent_active: + self.fields['categorie'].initial = 1 + self.fields['categorie'].widget.attrs['disabled'] = 'disabled' + self.fields['titulaire'].disabled = True + else: + self.fields['categorie'] = forms.ChoiceField(label="Catégorie*", widget=forms.RadioSelect, choices=[(1, "Représentant"), (2, "Enfant")], required=False) + # Désactive l'autocomplete self.fields['nom'].widget.attrs.update({'autocomplete': 'off'}) @@ -106,7 +113,6 @@ def clean(self): # Catégorie if "disabled" in self.fields['categorie'].widget.attrs: self.cleaned_data["categorie"] = 1 - if self.cleaned_data["categorie"] == "": self.add_error("categorie", "Vous devez sélectionner une catégorie") return @@ -127,7 +133,35 @@ def clean(self): if self.cleaned_data["civilite"] < 6 and self.cleaned_data["prenom"] in (None, ""): self.add_error("prenom", "Vous devez saisir un prénom") return - + nom = self.cleaned_data.get("nom", "").strip() + prenom = self.cleaned_data.get("prenom", "").strip() + + if nom and prenom: + # Vérifier dans l'ENT + try: + # Import dynamique pour éviter les problèmes de dépendances + + ent_result = get_ent_users(nom, prenom) + print("*/*/*/*/") + print(ent_result) + # Si get_ent_users retourne une liste non vide, on stocke l'information pour redirection + if isinstance(ent_result, list) and len(ent_result) > 0: + self.redirect_to_ent_liste = True + self.ent_users_data = ent_result + else: + # L'utilisateur n'existe ni dans la base ni dans l'ENT + # On continue avec le traitement normal (pas de redirection) + self.redirect_to_ent_liste = False + + except ImportError: + # Si la fonction get_ent_users n'existe pas encore, on continue normalement + self.redirect_to_ent_liste = False + except Exception as e: + # En cas d'erreur ENT, on continue normalement plutôt que de bloquer + self.redirect_to_ent_liste = False + else: + # Pas de nom/prénom fournis, traitement normal + self.redirect_to_ent_liste = False # Action RATTACHER if self.cleaned_data["action"] == "RATTACHER": @@ -144,7 +178,13 @@ def clean(self): return self.cleaned_data + def has_redirect_to_ent_liste(self): + """Méthode pour vérifier si on doit rediriger vers ent_liste_famille""" + return hasattr(self, 'redirect_to_ent_liste') and self.redirect_to_ent_liste + def get_ent_users_data(self): + """Méthode pour récupérer les données ENT""" + return getattr(self, 'ent_users_data', []) EXTRA_SCRIPT = """ diff --git a/noethysweb/fiche_individu/templates/fiche_individu/individu_liste_maj.html b/noethysweb/fiche_individu/templates/fiche_individu/individu_liste_maj.html new file mode 100644 index 00000000..fe8ea2e3 --- /dev/null +++ b/noethysweb/fiche_individu/templates/fiche_individu/individu_liste_maj.html @@ -0,0 +1,307 @@ +{% extends "core/page.html" %} +{% load static %} +{% load crispy_forms_tags %} + +{% block page_titre %}{{ page_titre }}{% endblock page_titre %} + +{% block contenu_page %} +
+
+
+
+
+

+ {{ box_titre }} +

+
+
+

{{ box_introduction }}

+ + + {% if sync_en_cours %} +
+ + Synchronisation en cours depuis {{ sync_date_debut|date:"H:i:s" }} + pour {{ sync_nb_individus }} individu(s)... +
+ {% elif sync_date_derniere_fin %} +
+ + Dernière synchronisation : + {{ sync_date_derniere_fin|date:"d/m/Y à H:i:s" }} + ({{ sync_nb_succes_dernier }} individu(s) synchronisé(s) avec succès) +
+ {% endif %} + + +
+
+
+ Recherche +
+
+
+
+
+
+ + +
+ +
+ + +
+ +
+
+ + + Réinitialiser + +
+
+
+
+
+
+ + +
+
+
+ +
+ Individus liés à l'ENT + {{ total_count }} +
+
+
+
+
+ +
+ Total individus dans la base + {{ total_all_individus }} +
+
+
+
+ +
+ {% csrf_token %} + + +
+
+ + +
+
+ 0 + individu(s) sélectionné(s) +
+
+ + +
+ + + + + + + + + + + + + + + + + {% if individus %} + {% for individu in individus %} + + + + + + + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
+ + ENT IDCivilitéNomPrénomTypeÉcoleClasseEmailTéléphone
+ + + {{ individu.ent_id }} + {{ individu.civilite }}{{ individu.nom }}{{ individu.prenom }} + {% if "Enfant" in individu.type %} + Enfant + {% endif %} + {% if "Représentant" in individu.type %} + Représentant + {% endif %} + {% if "Contact" in individu.type %} + Contact + {% endif %} + {% if individu.type == "-" %} + - + {% endif %} + + {% if "Enfant" in individu.type and individu.ecole != "-" %} + {{ individu.ecole }} + {% else %} + - + {% endif %} + + {% if "Enfant" in individu.type and individu.classe != "-" %} + {{ individu.classe }} + {% else %} + - + {% endif %} + + {{ individu.mail }} + + {{ individu.tel_mobile }} +
+ +

Aucun individu trouvé avec les critères sélectionnés.

+
+
+ + {% if individus %} + +
+ +
+ {% endif %} +
+
+
+
+
+
+ + + + +{% endblock contenu_page %} \ No newline at end of file diff --git a/noethysweb/fiche_individu/templates/fiche_individu/individu_update_ent.html b/noethysweb/fiche_individu/templates/fiche_individu/individu_update_ent.html new file mode 100644 index 00000000..98357bed --- /dev/null +++ b/noethysweb/fiche_individu/templates/fiche_individu/individu_update_ent.html @@ -0,0 +1,416 @@ +{% extends "fiche_individu/individu.html" %} +{% load crispy_forms_tags %} +{% load static %} +{% load embed %} + +{% block page_titre %}{{ page_titre }}{% endblock page_titre %} + +{% block detail_individu %} +
+ {% embed 'core/box.html' %} + {% block box_theme %}card-outline card-lightblue{% endblock %} + {% block box_titre %}{{ box_titre }}{% endblock %} + {% block box_introduction %}{{ box_introduction|safe }}{% endblock %} + {% endembed %} + + {% if user_not_found %} +
+
+ +

Utilisateur introuvable dans l'ENT

+

+ Aucune information n'a été trouvée dans l'ENT pour cet utilisateur.
+ ENT ID : {{ individu.ent_id|default:"Non renseigné" }} +

+
+ + Vérifiez que l'identifiant ENT est correct ou que l'utilisateur existe bien dans l'ENT. +
+
+
+ {% else %} +
+ {% csrf_token %} + +
+
+
+ + Légende : + Identique + Différent + Sélectionné pour synchronisation +
+ +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if has_scolarite %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% endif %} + +
+ + ChampCoCliCoENT
+ + Civilité + {{ local.civilite|default:"-" }} + + {{ external.civilite|default:"-" }} +
+ + Nom + {{ local.nom|default:"-" }} + + {{ external.nom|default:"-" }} +
+ + Nom de jeune fille + {{ local.nom_jfille|default:"-" }} + + {{ external.nom_jfille|default:"-" }} +
+ + Prénom + {{ local.prenom|default:"-" }} + + {{ external.prenom|default:"-" }} +
+ + Rue + {{ local.rue|default:"-" }} + + {{ external.rue|default:"-" }} +
+ + Code postal + {{ local.cp|default:"-" }} + + {{ external.cp|default:"-" }} +
+ + Ville + {{ local.ville|default:"-" }} + + {{ external.ville|default:"-" }} +
+ + Téléphone mobile + {{ local.tel_mobile|default:"-" }} + + {{ external.tel_mobile|default:"-" }} +
+ + Mail personnel + {{ local.mail|default:"-" }} + + {{ external.mail|default:"-" }} +
+ Informations de scolarité +
+ + École + {{ local.ecole_nom|default:"-" }} + + {{ external.ecole_nom|default:"-" }} +
+ + Classe + {{ local.classe_nom|default:"-" }} + + {{ external.classe_nom|default:"-" }} +
+ + Niveau + {{ local.niveau_nom|default:"-" }} + + {{ external.niveau_nom|default:"-" }} +
+
+ +
+
+ 0 + champ(s) sélectionné(s) +
+
+ +
+
+
+
+
+ {% endif %} + + +
+{% endblock %} + +{% block scripts %} + {{ block.super }} + +{% endblock scripts %} \ No newline at end of file diff --git a/noethysweb/fiche_individu/urls.py b/noethysweb/fiche_individu/urls.py index a467d597..590bfbe6 100644 --- a/noethysweb/fiche_individu/urls.py +++ b/noethysweb/fiche_individu/urls.py @@ -8,7 +8,7 @@ from fiche_individu.views import individu_portail, individu, individu_identite, individu_coords, individu_questionnaire, individu_scolarite, individu_inscriptions, \ individu_medical, individu_notes, individu_liens, individu_appliquer_forfait_date, individu_contacts, \ individu_regimes_alimentaires, individu_assurances, individu_maladies, individu_transports, \ - individu_appliquer_forfait_date_choix + individu_appliquer_forfait_date_choix, individu_ent urlpatterns = [ @@ -85,6 +85,9 @@ path('individus/individus/transports/modifier_transport///', individu_transports.Modifier_transport.as_view(), name='individu_transports_modifier'), path('individus/individus/transports/supprimer_transport///', individu_transports.Supprimer_transport.as_view(), name='individu_transports_supprimer'), + path('individus/individus/maj_ent_individu//', individu_ent.UpdateIndividu.as_view(), name='maj_ent_individu'), + path('individus/individus/maj__liste_ent/', individu_ent.SynchronisationMasseIndividus.as_view(), name='mettre_a_jour_liste_individu_ent'), + # AJAX path('individus/get_classes', secure_ajax(individu_scolarite.Get_classes), name='ajax_get_classes'), path('individus/get_niveaux', secure_ajax(individu_scolarite.Get_niveaux), name='ajax_get_niveaux'), diff --git a/noethysweb/fiche_individu/utils/utils_individu.py b/noethysweb/fiche_individu/utils/utils_individu.py index 82f23be3..a33c540a 100644 --- a/noethysweb/fiche_individu/utils/utils_individu.py +++ b/noethysweb/fiche_individu/utils/utils_individu.py @@ -19,6 +19,7 @@ {"code": "contacts", "label": "Contacts", "icone": "fa-users", "url": "individu_contacts_liste"}, {"code": "transports", "label": "Transports", "icone": "fa-bus", "url": "individu_transports_liste"}, {"code": "consommations", "label": "Consommations", "icone": "fa-calendar", "url": "famille_consommations"}, + {"code": "mise_a_jour_ent", "label": "Mise à jour ENT", "icone": "fa-refresh", "url": "maj_ent_individu"}, ] def Get_filtered_onglets(): diff --git a/noethysweb/fiche_individu/views/individu_ent.py b/noethysweb/fiche_individu/views/individu_ent.py new file mode 100644 index 00000000..f0db785f --- /dev/null +++ b/noethysweb/fiche_individu/views/individu_ent.py @@ -0,0 +1,518 @@ +from core.models import Scolarite, Individu, Classe, NiveauScolaire, Ecole, SynchronisationLock +from fiche_individu.views.individu import Onglet +from django.views.generic import TemplateView +from core.views.base import CustomView +from django.contrib import messages +from core.utils.utils_ent import get_ent_user_info_by_ent_id +from django.shortcuts import redirect +from django.db.models import Q +from django.db import transaction +from django.utils import timezone +import logging +import threading + +logger = logging.getLogger(__name__) + +# Type de verrou pour la synchronisation en masse +SYNC_TYPE_MASSE = 'synchronisation_masse_ent' + +class UpdateIndividu(Onglet, TemplateView): + menu_code = "individu_synchroniser" + template_name = "fiche_individu/individu_update_ent.html" + + def get_context_data(self, **kwargs): + context = super(UpdateIndividu, self).get_context_data(**kwargs) + context['box_titre'] = "Synchronisation" + context['onglet_actif'] = "synchroniser" + individu = Individu.objects.get(pk=self.kwargs['idindividu']) + + # Récupérer les données externes + external_data = get_ent_user_info_by_ent_id(individu.ent_id) + + # Vérifier si l'utilisateur existe dans l'ENT + if not external_data: + context["individu"] = individu + context["user_not_found"] = True + context["box_titre"] = f"Synchronisation - {individu.prenom} {individu.nom}" + context["box_introduction"] = "Cet utilisateur n'a pas été trouvé dans l'ENT." + context["onglet_actif"] = "synchronisation" + return context + + # Préparer les données locales + local_data = { + "id": individu.idindividu, + "civilite": individu.get_civilite_display() if individu.civilite else None, + "nom": individu.nom, + "prenom": individu.prenom, + "nom_jfille": individu.nom_jfille, + "rue": individu.rue_resid, + "cp": individu.cp_resid, + "ville": individu.ville_resid, + "tel_mobile": individu.tel_mobile, + "mail": individu.mail, + } + + # Préparer les données externes avec mapping + external_display = { + "civilite": external_data.get('civilite'), + "nom": external_data.get('nom'), + "prenom": external_data.get('prenom'), + "nom_jfille": external_data.get('nom_jfille'), + "rue": external_data.get('rue'), + "cp": external_data.get('cp'), + "ville": external_data.get('ville'), + "tel_mobile": external_data.get('telephone'), + "mail": external_data.get('email'), + } + + # Si c'est un enfant avec scolarité + has_scolarite = 'scolarite' in external_data and external_data['scolarite'] + if has_scolarite: + scolarite = external_data['scolarite'] + ecole = scolarite.get('ecole', {}) + classe = scolarite.get('classe', {}) + niveau = scolarite.get('niveau', {}) + + # Ajouter les informations de scolarité + external_display.update({ + 'ecole_nom': ecole.get('nom'), + 'ecole_rue': ecole.get('rue'), + 'ecole_cp': ecole.get('cp'), + 'ecole_ville': ecole.get('ville'), + 'classe_nom': classe.get('nom'), + 'niveau_nom': niveau.get('nom'), + }) + + local_data.update({ + 'ecole_nom': None, + 'ecole_rue': None, + 'ecole_cp': None, + 'ecole_ville': None, + 'classe_nom': None, + 'niveau_nom': None, + }) + + # Mapping des champs avec leurs labels + fields_mapping = [ + {'key': 'civilite', 'label': 'Civilité', 'local_key': 'civilite', 'external_key': 'civilite'}, + {'key': 'nom', 'label': 'Nom', 'local_key': 'nom', 'external_key': 'nom'}, + {'key': 'nom_jfille', 'label': 'Nom de jeune fille', 'local_key': 'nom_jfille', 'external_key': 'nom_jfille'}, + {'key': 'prenom', 'label': 'Prénom', 'local_key': 'prenom', 'external_key': 'prenom'}, + {'key': 'rue', 'label': 'Rue', 'local_key': 'rue', 'external_key': 'rue'}, + {'key': 'cp', 'label': 'Code postal', 'local_key': 'cp', 'external_key': 'cp'}, + {'key': 'ville', 'label': 'Ville', 'local_key': 'ville', 'external_key': 'ville'}, + {'key': 'tel_mobile', 'label': 'Téléphone mobile', 'local_key': 'tel_mobile', 'external_key': 'tel_mobile'}, + {'key': 'mail', 'label': 'Mail personnel', 'local_key': 'mail', 'external_key': 'mail'}, + ] + + # Ajouter les champs de scolarité si applicable + if has_scolarite: + fields_mapping.extend([ + {'key': 'ecole_nom', 'label': 'École', 'local_key': 'ecole_nom', 'external_key': 'ecole_nom'}, + {'key': 'classe_nom', 'label': 'Classe', 'local_key': 'classe_nom', 'external_key': 'classe_nom'}, + {'key': 'niveau_nom', 'label': 'Niveau', 'local_key': 'niveau_nom', 'external_key': 'niveau_nom'}, + ]) + + context["individu"] = individu + context["local"] = local_data + context["external"] = external_display + context["fields_mapping"] = fields_mapping + context["has_scolarite"] = has_scolarite + context["box_titre"] = f"Synchronisation - {individu.prenom} {individu.nom}" + context["box_introduction"] = "Sélectionnez les champs à synchroniser depuis l'ENT vers CoCliCo." + context["onglet_actif"] = "synchronisation" + return context + + def post(self, request, *args, **kwargs): + """Traite la synchronisation des champs sélectionnés""" + try: + individu = Individu.objects.get(pk=self.kwargs['idindividu']) + external_data = get_ent_user_info_by_ent_id(individu.ent_id) + + # Récupérer les champs sélectionnés + selected_fields = request.POST.getlist('sync_fields') + + if not selected_fields: + messages.warning(request, "Aucun champ sélectionné pour la synchronisation.") + return redirect(request.path) + + # Mapping des champs du formulaire vers les attributs du modèle + field_mapping = { + 'civilite': 'civilite', + 'nom': 'nom', + 'nom_jfille': 'nom_jfille', + 'prenom': 'prenom', + 'rue': 'rue_resid', + 'cp': 'cp_resid', + 'ville': 'ville_resid', + 'tel_mobile': 'tel_mobile', + 'mail': 'mail' + } + + # Mapping des champs externes (API) vers les clés + external_field_mapping = { + 'civilite': 'civilite', + 'nom': 'nom', + 'nom_jfille': 'nom_jfille', + 'prenom': 'prenom', + 'rue': 'rue', + 'cp': 'cp', + 'ville': 'ville', + 'tel_mobile': 'telephone', + 'mail': 'email' + } + + updated_fields = [] + for field in selected_fields: + if field in field_mapping: + model_field = field_mapping[field] + external_field = external_field_mapping[field] + + if external_field in external_data: + new_value = external_data[external_field] + + # Traitement spécial pour civilite (conversion texte vers ID) + if field == 'civilite' and new_value: + # Mapper les civilités texte vers les IDs du modèle + civilite_mapping = { + 'M.': 1, # Ajustez selon vos choix de civilité + 'Mme': 2, + 'Mlle': 3, + } + new_value = civilite_mapping.get(new_value, individu.civilite) + + setattr(individu, model_field, new_value) + updated_fields.append(field) + + # Gérer les champs de scolarité si présents (nécessite adaptation selon votre modèle) + if 'scolarite' in external_data and external_data['scolarite']: + # TODO: Implémenter la synchronisation de la scolarité selon votre modèle + # Exemple: créer ou mettre à jour les inscriptions, écoles, classes, etc. + pass + + if updated_fields: + individu.save() + messages.success( + request, + f"Synchronisation réussie ! {len(updated_fields)} champ(s) mis à jour : {', '.join(updated_fields)}" + ) + else: + messages.info(request, "Aucun champ n'a été mis à jour.") + + return redirect(request.path) + + except Individu.DoesNotExist: + messages.error(request, "Individu introuvable.") + return redirect('individus_liste') + except Exception as e: + messages.error(request, f"Erreur lors de la synchronisation : {str(e)}") + return redirect(request.path) + + + + + +class SynchronisationMasseIndividus(CustomView, TemplateView): + template_name = "fiche_individu/individu_liste_maj.html" + menu_code = "mettre_a_jour_liste_individu_ent" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + # Récupérer les filtres + search_query = self.request.GET.get('search', '') + type_filtre = self.request.GET.get('type', '') # '' = tous, '1' = Représentant, '2' = Enfant, '3' = Contact + + # Base query - Tous les individus avec ENT ID par défaut + individus = Individu.objects.filter(ent_id__isnull=False).exclude(ent_id='') + + # Filtrer par type si spécifié + if type_filtre: + from core.models import Rattachement + # Récupérer les IDs des individus ayant ce type de rattachement + individus_ids = Rattachement.objects.filter( + categorie=int(type_filtre) + ).values_list('individu_id', flat=True).distinct() + individus = individus.filter(idindividu__in=individus_ids) + + # Recherche textuelle + if search_query: + individus = individus.filter( + Q(nom__icontains=search_query) | + Q(prenom__icontains=search_query) | + Q(ent_id__icontains=search_query) + # Note: mail est un champ chiffré (EncryptedEmailField), impossible de faire une recherche dessus + ) + + individus = individus.order_by('nom', 'prenom') + + # Récupérer la liste des écoles - Non utilisé maintenant + ecoles = [] + + # Préparer les données des individus + individus_data = [] + for individu in individus[:200]: # Limiter à 200 pour la performance + # Récupérer les types d'individu via les rattachements + rattachements = individu.rattachement_set.all() + types_individu = [] + + for ratt in rattachements: + if ratt.categorie == 1: + types_individu.append("Représentant") + elif ratt.categorie == 2: + types_individu.append("Enfant") + elif ratt.categorie == 3: + types_individu.append("Contact") + + # Si plusieurs types, les joindre par des virgules + type_display = ", ".join(set(types_individu)) if types_individu else "-" + + # Récupérer l'école actuelle pour les enfants + ecole_nom = "-" + classe_nom = "-" + if "Enfant" in types_individu: + # Récupérer la scolarité actuelle (la plus récente) + from datetime import date + scolarite_actuelle = individu.scolarite_set.filter( + date_debut__lte=date.today() + ).order_by('-date_debut').first() + + if scolarite_actuelle: + if scolarite_actuelle.ecole: + ecole_nom = scolarite_actuelle.ecole.nom + if scolarite_actuelle.classe: + classe_nom = scolarite_actuelle.classe.nom + + individus_data.append({ + 'id': individu.idindividu, + 'ent_id': individu.ent_id, + 'civilite': individu.get_civilite_display() if individu.civilite else "-", + 'nom': individu.nom, + 'prenom': individu.prenom or "-", + 'type': type_display, + 'ecole': ecole_nom, + 'classe': classe_nom, + 'mail': individu.mail or "-", + 'tel_mobile': individu.tel_mobile or "-", + }) + + context['individus'] = individus_data + context['total_count'] = individus.count() + context['total_all_individus'] = Individu.objects.count() + context['search_query'] = search_query + context['type_filtre'] = type_filtre + context['page_titre'] = "Synchronisation en masse" + context['box_titre'] = "Synchronisation en masse depuis l'ENT" + context['box_introduction'] = "Visualisez tous les individus avec un ENT ID et sélectionnez ceux à synchroniser avec les données de l'ENT." + + # Récupérer les informations de la dernière synchronisation + try: + lock = SynchronisationLock.objects.get(type_sync=SYNC_TYPE_MASSE) + context['sync_en_cours'] = lock.est_verrouille + context['sync_date_derniere_fin'] = lock.date_derniere_fin + context['sync_nb_succes_dernier'] = lock.nb_succes_dernier + context['sync_dernier_utilisateur'] = lock.dernier_utilisateur + if lock.est_verrouille: + context['sync_date_debut'] = lock.date_debut + context['sync_utilisateur_actuel'] = lock.utilisateur + context['sync_nb_individus'] = lock.nb_individus + except SynchronisationLock.DoesNotExist: + context['sync_en_cours'] = False + + return context + + def post(self, request, *args, **kwargs): + """Lance la synchronisation en masse des individus sélectionnés""" + try: + # Récupérer les IDs des individus sélectionnés + selected_ids = request.POST.getlist('selected_individus') + + if not selected_ids: + messages.warning(request, "Aucun individu sélectionné pour la synchronisation.") + return redirect(request.path) + + # Convertir en integers + selected_ids = [int(id) for id in selected_ids] + + # Vérifier si une synchronisation est déjà en cours + with transaction.atomic(): + lock, created = SynchronisationLock.objects.select_for_update().get_or_create( + type_sync=SYNC_TYPE_MASSE, + defaults={'est_verrouille': False} + ) + + if lock.est_verrouille: + # Une synchronisation est déjà en cours + messages.warning( + request, + f"Une synchronisation est déjà en cours depuis {lock.date_debut.strftime('%H:%M:%S')} " + f"({lock.nb_individus} individu(s)). " + f"Veuillez attendre qu'elle se termine avant d'en lancer une nouvelle." + ) + return redirect(request.path + f"?{request.GET.urlencode()}") + + # Verrouiller pour cette synchronisation + lock.est_verrouille = True + lock.utilisateur = request.user + lock.date_debut = timezone.now() + lock.nb_individus = len(selected_ids) + lock.save() + + # Lancer la synchronisation en arrière-plan dans un thread + thread = threading.Thread( + target=self.synchroniser_individus_masse_background, + args=(selected_ids, request.user.pk) + ) + thread.daemon = True + thread.start() + + # Afficher un message immédiat à l'utilisateur + messages.info( + request, + f"Synchronisation de {len(selected_ids)} individu(s) lancée en arrière-plan. " + "Cette opération peut prendre quelques minutes. " + "Vous pouvez continuer à travailler pendant ce temps." + ) + + return redirect(request.path + f"?{request.GET.urlencode()}") + + except Exception as e: + messages.error(request, f"Erreur lors du lancement de la synchronisation : {str(e)}") + return redirect(request.path) + + def synchroniser_individus_masse_background(self, individu_ids, utilisateur_id): + """ + Méthode wrapper pour exécuter la synchronisation en arrière-plan + et logger les résultats. Déverrouille automatiquement à la fin. + """ + logger.info(f"=== DÉBUT Synchronisation en arrière-plan de {len(individu_ids)} individu(s) ===") + success_count = 0 + try: + success_count = self.synchroniser_individus_masse(individu_ids) + logger.info(f"=== FIN Synchronisation terminée: {success_count}/{len(individu_ids)} individu(s) synchronisé(s) avec succès ===") + except Exception as e: + logger.exception(f"=== ERREUR Synchronisation en arrière-plan échouée: {str(e)} ===") + finally: + # Toujours déverrouiller et enregistrer les stats, même en cas d'erreur + try: + from core.models import Utilisateur + lock = SynchronisationLock.objects.get(type_sync=SYNC_TYPE_MASSE) + lock.est_verrouille = False + lock.date_derniere_fin = timezone.now() + lock.nb_succes_dernier = success_count + lock.dernier_utilisateur = Utilisateur.objects.get(pk=utilisateur_id) + lock.save() + logger.info(f"=== Verrou de synchronisation libéré - {success_count} succès ===") + except SynchronisationLock.DoesNotExist: + logger.warning("=== Verrou de synchronisation introuvable lors de la libération ===") + + def synchroniser_individus_masse(self, individu_ids): + """ + Synchronise les individus sélectionnés avec les données de l'ENT + + Args: + individu_ids: Liste des IDs des individus à synchroniser + + Returns: + int: Nombre d'individus synchronisés avec succès + """ + success_count = 0 + errors = [] + import time + time.sleep(20) + # Mapping des champs à synchroniser + champs_mapping = { + 'nom': 'nom', + 'prenom': 'prenom', + 'civilite': 'civilite', + 'email': 'mail', + 'telephone_mobile': 'tel_mobile', + 'telephone': 'tel_domicile', + 'adresse': 'adresse_auto', + 'code_postal': 'cp_auto', + 'ville': 'ville_auto', + } + + for individu_id in individu_ids: + try: + with transaction.atomic(): + # Récupérer l'individu + individu = Individu.objects.select_for_update().get(idindividu=individu_id) + + if not individu.ent_id: + logger.warning(f"Individu {individu_id} n'a pas d'ent_id") + continue + + # Récupérer les données depuis l'ENT par ent_id + donnees_ent = get_ent_user_info_by_ent_id(individu.ent_id) + + if not donnees_ent: + logger.warning(f"Aucune donnée ENT trouvée pour {individu.nom} {individu.prenom} (ent_id: {individu.ent_id})") + errors.append(f"{individu.nom} {individu.prenom}: Données ENT introuvables") + continue + + # Mettre à jour les champs + updated = False + for champ_ent, champ_db in champs_mapping.items(): + if champ_ent in donnees_ent and donnees_ent[champ_ent]: + valeur_ent = donnees_ent[champ_ent] + + # Traitement spécial pour la civilité + if champ_db == 'civilite': + valeur_ent = self.convertir_civilite_ent(valeur_ent) + + # Mettre à jour si la valeur est différente + valeur_actuelle = getattr(individu, champ_db) + if valeur_actuelle != valeur_ent: + setattr(individu, champ_db, valeur_ent) + updated = True + + if updated: + individu.save() + success_count += 1 + logger.info(f"Individu {individu.nom} {individu.prenom} synchronisé avec succès") + + except Individu.DoesNotExist: + logger.error(f"Individu {individu_id} introuvable") + errors.append(f"Individu ID {individu_id} introuvable") + except Exception as e: + logger.exception(f"Erreur lors de la synchronisation de l'individu {individu_id}: {str(e)}") + errors.append(f"Individu ID {individu_id}: {str(e)}") + + # Log les erreurs s'il y en a + if errors: + logger.warning(f"Synchronisation terminée avec {len(errors)} erreur(s): {errors}") + + return success_count + + def convertir_civilite_ent(self, valeur): + """ + Convertit la civilité de l'ENT en ID pour la base de données + """ + from core.data import data_civilites + + dict_civilites_data = data_civilites.GetDictCivilites() + + # Mapping: texte ENT -> ID civilité + mapping_civilite = {} + for civ_id, civ_info in dict_civilites_data.items(): + if civ_info.get('label'): + mapping_civilite[civ_info['label']] = civ_id + if civ_info.get('abrege'): + mapping_civilite[civ_info['abrege']] = civ_id + + # Variations courantes + variations = { + 'M': 1, + 'M.': 1, + 'Mr': 1, + 'Monsieur': 1, + 'Mme': 3, + 'Madame': 3, + 'Melle': 2, + 'Mlle': 2, + 'Mademoiselle': 2, + } + mapping_civilite.update(variations) + + return mapping_civilite.get(valeur, 1) # Par défaut Monsieur \ No newline at end of file diff --git a/noethysweb/individus/urls.py b/noethysweb/individus/urls.py index b8cf7498..2638ccae 100644 --- a/noethysweb/individus/urls.py +++ b/noethysweb/individus/urls.py @@ -33,7 +33,6 @@ path('individus/recherche_avancee', recherche_avancee.View.as_view(), name='individus_recherche_avancee'), path('individus/effacer_familles', effacer_familles.Liste.as_view(), name='effacer_familles'), path('individus/importer_individus', importer_individus.View.as_view(), name='importer_individus'), - # Inscriptions path('individus/inscriptions', inscriptions_liste.Liste.as_view(), name='inscriptions_liste'), path('individus/inscriptions/ajouter', inscriptions_liste.Ajouter.as_view(), name='inscriptions_ajouter'), diff --git a/noethysweb/parametrage/forms/ecoles.py b/noethysweb/parametrage/forms/ecoles.py index 2e263945..8a5739d1 100644 --- a/noethysweb/parametrage/forms/ecoles.py +++ b/noethysweb/parametrage/forms/ecoles.py @@ -51,8 +51,38 @@ def __init__(self, *args, **kwargs): Field('tel'), Field('fax'), Field('mail'), + Field('uai'), ), Fieldset('Secteurs', Field('secteurs'), ), ) + +class FormulaireENT(FormulaireBase, ModelForm): + secteurs = forms.ModelMultipleChoiceField(label="Secteurs géographiques associés", widget=Select2MultipleWidget(), queryset=Secteur.objects.all(), required=False) + + class Meta: + model = Ecole + fields = ["uai", "secteurs"] + widgets = {} + + def __init__(self, *args, **kwargs): + super(FormulaireENT, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_id = 'ecoles_form' + self.helper.form_method = 'post' + + self.helper.form_class = 'form-horizontal' + self.helper.label_class = 'col-md-2' + self.helper.field_class = 'col-md-10' + + # Affichage + self.helper.layout = Layout( + Commandes(annuler_url="{% url 'ecoles_liste' %}"), + Fieldset("Identification", + Field('uai'), + ), + Fieldset('Secteurs', + Field('secteurs'), + ), + ) \ No newline at end of file diff --git a/noethysweb/parametrage/forms/parameters_ent.py b/noethysweb/parametrage/forms/parameters_ent.py new file mode 100644 index 00000000..f1af0458 --- /dev/null +++ b/noethysweb/parametrage/forms/parameters_ent.py @@ -0,0 +1,158 @@ +from django import forms +from core.forms.base import FormulaireBase +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, HTML, Div +from crispy_forms.bootstrap import Field +from core.utils.utils_commandes import Commandes +from django.core.cache import cache +from core.models import Organisateur + + +class Formulaire(FormulaireBase, forms.Form): + def __init__(self, *args, **kwargs): + super(Formulaire, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_id = 'compte_parametres_form' + self.helper.form_method = 'post' + self.helper.form_class = 'form-horizontal' + self.helper.label_class = 'col-md-2' + self.helper.field_class = 'col-md-10' + + # Initialisation du layout + self.helper.layout = Layout() + self.helper.layout.append(Commandes(annuler_url="{% url 'parametres_ent' %}", ajouter=False)) + organisateur = Organisateur.objects.filter(pk=1).first() + # === Création des fields === + self.fields["sans_ent"] = forms.BooleanField( + label="Sans ENT", + required=False, + widget=forms.CheckboxInput(attrs={'class': 'text-start'}) + ) + self.fields["sans_ent"].initial = False + + self.fields["activer_ent"] = forms.BooleanField( + label="ENT", + required=False, + widget=forms.CheckboxInput(attrs={'class': 'text-start'}) + ) + self.fields["activer_ent"].initial = True + self.fields["activer_ent"].initial = organisateur.ent_active + self.fields["sans_ent"].initial = not organisateur.ent_active + # === CSS personnalisé pour les checkboxes === + custom_css = """ + + """ + self.helper.layout.append(HTML(custom_css)) + + # === Placement Sans ENT en premier === + self.helper.layout.append( + Div( + Div("sans_ent", css_class="custom-checkbox"), + css_class="checkbox-container" + ) + ) + self.helper.layout.append( + Div( + HTML('
Cochez cette case pour Désactiver l\'intégration avec l\'Espace Numérique de Travail
'), + css_class="text-start" + ) + ) + + # === Ensuite ENT === + self.helper.layout.append( + Div( + Div("activer_ent", css_class="custom-checkbox"), + css_class="checkbox-container" + ) + ) + self.helper.layout.append( + Div( + HTML('
Cochez cette case pour activer l\'intégration avec l\'Espace Numérique de Travail
'), + css_class="text-start" + ) + ) + + self.helper.layout.append(HTML("
")) + self.helper.layout.append(HTML(EXTRA_SCRIPT)) + + def clean(self): + cleaned_data = super().clean() + activer_ent = cleaned_data.get('activer_ent') + sans_ent = cleaned_data.get('sans_ent') + organisateur = cache.get('organisateur', None) + if not organisateur: + organisateur = cache.get_or_set('organisateur', Organisateur.objects.filter(pk=1).first()) + organisateur.ent_active = activer_ent + organisateur.save() + print("*/*/*/*/") + print(activer_ent) + if not activer_ent and not sans_ent: + raise forms.ValidationError( + "Vous devez cocher au moins une option : ENT ou Sans ENT." + ) + return cleaned_data + +# Mise à jour du EXTRA_SCRIPT pour gérer les 2 checkboxes +EXTRA_SCRIPT = """ + +
+""" diff --git a/noethysweb/parametrage/templates/parametrage/ecole_ent.html b/noethysweb/parametrage/templates/parametrage/ecole_ent.html new file mode 100644 index 00000000..ac73cc6c --- /dev/null +++ b/noethysweb/parametrage/templates/parametrage/ecole_ent.html @@ -0,0 +1,387 @@ +{% extends "core/page.html" %} +{% load static %} + +{% block styles %} + {{ block.super }} + +{% endblock %} + +{% block contenu_page %} + + +{% if ecole %} + +
+
+
+
{{ ecole.nom }}
+
+ Code UAI: {{ ecole.uai }} +
+
+
+ {% csrf_token %} + + + +
+
+
+ +
+
+ + Informations générales +
+
+
+ 🏫 + Nom : + {{ ecole.nom }} +
+
+ 🆔 + Code UAI : + {{ ecole.uai }} +
+
+ 📍 + Adresse : + {{ ecole.rue }}, {{ ecole.cp }} {{ ecole.ville }} +
+
+
+ + +
+
+ + Informations de contact +
+
+
+ 📞 + Téléphone : + {{ ecole.telephone }} +
+
+ 📧 + Email : + {{ ecole.mail }} +
+
+
+ + +
+
+ + Statistiques +
+
+
+ {{ ecole.classes|length }} +
Classes
+
+
+ {{ ecole.niveaux|length }} +
Niveaux
+
+
+
+ +
+
+
+{% else %} +
+ +

École non trouvée.

+
+{% endif %} + +
+
+ Vous n’avez pas trouvé la bonne école ? +
+
+

+ Aucune école ne correspond à ce que vous souhaitez ajouter ? +

+
+ {% csrf_token %} + + +
+
+
+{% endblock %} \ No newline at end of file diff --git a/noethysweb/parametrage/templates/parametrage/parametres_ent.html b/noethysweb/parametrage/templates/parametrage/parametres_ent.html new file mode 100644 index 00000000..fb9d406b --- /dev/null +++ b/noethysweb/parametrage/templates/parametrage/parametres_ent.html @@ -0,0 +1,49 @@ +{% extends "core/page.html" %} +{% load crispy_forms_tags %} +{% load static %} +{% load embed %} + +{% block content %} +
+
+
+
+
+

+ + Paramètres ENT +

+
+
+
+ {% csrf_token %} + + +
+ {{ form.activer_ent }} + +
+ + + {% if form.errors %} +
+ {{ form.errors }} +
+ {% endif %} + + +
+ +
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/noethysweb/parametrage/urls.py b/noethysweb/parametrage/urls.py index 5334021b..e4d1f464 100644 --- a/noethysweb/parametrage/urls.py +++ b/noethysweb/parametrage/urls.py @@ -27,7 +27,7 @@ types_qualifications_collaborateurs, types_pieces_collaborateurs, types_evenements_collaborateurs, types_postes_collaborateurs, \ modeles_plannings_collaborateurs, groupes_collaborateurs, modeles_aides, transports, compagnies, lignes, lieux, arrets, modeles_impressions, \ modeles_word, releves_bancaires, sondages, achats_categories, achats_fournisseurs, modeles_commandes, modeles_commandes_colonnes, \ - activites_evenements_categories, activites_import_export, api_particulier, parametres_generaux, outils_parametres_generaux + activites_evenements_categories, activites_import_export, api_particulier, parametres_generaux, outils_parametres_generaux, parametres_ent, ecole_ent urlpatterns = [ @@ -41,6 +41,7 @@ # Paramètres généraux path('parametrage/parametres_generaux/', parametres_generaux.Modifier.as_view(), name='parametres_generaux'), + path('parametrage/parametres_ent/', parametres_ent.Modifier.as_view(), name='parametres_ent'), # Structures path('parametrage/structures/liste', structures.Liste.as_view(), name='structures_liste'), path('parametrage/structures/ajouter', structures.Ajouter.as_view(), name='structures_ajouter'), @@ -353,6 +354,8 @@ # Ecoles path('parametrage/ecoles/liste', ecoles.Liste.as_view(), name='ecoles_liste'), path('parametrage/ecoles/ajouter', ecoles.Ajouter.as_view(), name='ecoles_ajouter'), + path('parametrage/ecoles/ent/ajouter', ecoles.AjouterEnt.as_view(), name='ecoles_ajouter_ent'), + path('parametrage/ecoles/ent/liste', ecole_ent.EntEcole.as_view(), name='ecole_recherche_ent'), path('parametrage/ecoles/modifier/', ecoles.Modifier.as_view(), name='ecoles_modifier'), path('parametrage/ecoles/supprimer/', ecoles.Supprimer.as_view(), name='ecoles_supprimer'), diff --git a/noethysweb/parametrage/views/ecole_ent.py b/noethysweb/parametrage/views/ecole_ent.py new file mode 100644 index 00000000..868c71ce --- /dev/null +++ b/noethysweb/parametrage/views/ecole_ent.py @@ -0,0 +1,87 @@ +from django.views.generic import TemplateView +from core.views.base import CustomView +from django.db import transaction +from core.models import Ecole, Classe, NiveauScolaire, Secteur +from django.http import HttpResponseRedirect +from django.urls import reverse_lazy +import logging +from django.contrib import messages + +logger = logging.getLogger(__name__) + + + +class EntEcole(CustomView, TemplateView): + menu_code = "ecole_recherche_ent" + template_name = "parametrage/ecole_ent.html" + + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['ecole'] = self.request.session.get('ecole_ent', []) + context['page_titre'] = "L'école trouvé dans l'ENT" + return context + + @transaction.atomic + def post(self, request, *args, **kwargs): + action = request.POST.get("action", "") + search_info = self.request.session.get('ecole_search_info', {}) + secteurs_ids = search_info.get("secteurs", []) + ecole_id = request.POST.get("ecole_id") + ecole_ent = self.request.session.get('ecole_ent', []) + ecole = Ecole.objects.filter(uai=ecole_id).first() + if ecole: + messages.add_message(self.request, messages.ERROR, "Ecole existe déjà") + url_success = reverse_lazy("ecoles_liste", kwargs={}) + else: + if action == "ajouter_nouvelle_ecole": + url_success = reverse_lazy("ecoles_ajouter", kwargs={}) + else: + ecole = self.ajouter_ecole(ecole_ent, secteurs_ids) + self.ajouter_niveaux_scolaire(ecole_ent.get("niveaux")) + self.ajouter_classes(ecole, ecole_ent.get("classes")) + messages.add_message(self.request, messages.SUCCESS, "Ajout enregistré") + url_success = reverse_lazy("ecoles_liste", kwargs={}) + + return HttpResponseRedirect(url_success) + + def ajouter_ecole(self, ecole_info, secteurs_ids): + ecole = Ecole( + nom = ecole_info.get("nom"), + rue = ecole_info.get("rue"), + cp = ecole_info.get("cp"), + ville = ecole_info.get("ville"), + tel = ecole_info.get("tel"), + fax = ecole_info.get("fax"), + mail = ecole_info.get("mail"), + uai = ecole_info.get("uai") + ) + ecole.save() + if secteurs_ids: + secteurs_qs = Secteur.objects.filter(idsecteur__in=secteurs_ids) + ecole.secteurs.set(secteurs_qs) + ecole.save() + return ecole + + def ajouter_niveaux_scolaire(self, niveaux_list): + for niveau in niveaux_list: + exist_niveau = NiveauScolaire.objects.filter(nom=niveau.get("nom")).first() + if not exist_niveau: + nouveau_niveau = NiveauScolaire( + ordre = niveau.get("ordre"), + nom = niveau.get("nom"), + abrege = niveau.get("abrege"), + ) + nouveau_niveau.save() + + def ajouter_classes(self, ecole, classe_list): + for classe in classe_list: + niveau = NiveauScolaire.objects.filter(nom=classe.get("niveau_nom")).first() + classe = Classe( + ecole = ecole, + nom = classe.get("nom"), + date_debut = classe.get("date_debut"), + date_fin = classe.get("date_fin"), + ) + classe.save() + classe.niveaux.add(niveau) diff --git a/noethysweb/parametrage/views/ecoles.py b/noethysweb/parametrage/views/ecoles.py index e9ba1e30..12366194 100644 --- a/noethysweb/parametrage/views/ecoles.py +++ b/noethysweb/parametrage/views/ecoles.py @@ -6,8 +6,10 @@ from django.urls import reverse_lazy, reverse from core.views.mydatatableview import MyDatatable, columns, helpers from core.views import crud -from core.models import Ecole -from parametrage.forms.ecoles import Formulaire +from core.models import Ecole, Organisateur +from parametrage.forms.ecoles import Formulaire, FormulaireENT +from core.utils.utils_ent import get_ent_ecole +from django.http import HttpResponseRedirect class Page(crud.Page): @@ -33,21 +35,53 @@ def get_queryset(self): def get_context_data(self, **kwargs): context = super(Liste, self).get_context_data(**kwargs) + organisateur = Organisateur.objects.filter(pk=1).first() + if organisateur and organisateur.ent_active: + url_ajout = reverse_lazy("ecoles_ajouter_ent") + context["boutons_liste"] = [ + {"label": "Ajouter", "classe": "btn btn-success", "href": url_ajout, "icone": "fa fa-plus"}, + ] context['impression_introduction'] = "" context['impression_conclusion'] = "" context['afficher_menu_brothers'] = True return context class datatable_class(MyDatatable): - filtres = ["idecole", "nom", "rue", "cp", "ville"] + filtres = ["idecole", "nom", "rue", "cp", "ville", "uai"] actions = columns.TextColumn("Actions", sources=None, processor='Get_actions_standard') class Meta: structure_template = MyDatatable.structure_template - columns = ["idecole", "nom", "rue", "cp", "ville"] + columns = ["idecole", "nom", "rue", "cp", "ville", "uai"] ordering = ["nom"] +class AjouterEnt(Page, crud.Ajouter): + form_class = FormulaireENT + def form_valid(self, form): + organisateur = Organisateur.objects.filter(pk=1).first() + + if organisateur and organisateur.ent_active: + # ⚡ Appel API externe avec les données du formulaire + uai = form.cleaned_data.get("uai") + ecole_ent = get_ent_ecole(uai) + secteurs = form.cleaned_data.get("secteurs") + secteurs_ids = list(secteurs.values_list('idsecteur', flat=True)) if secteurs else [] + + # Stocker le résultat dans la session (pour l’afficher après la redirection) + + self.request.session["ecole_ent"] = ecole_ent + self.request.session["ecole_search_info"] = { + "uai": uai, + "secteurs": secteurs_ids + } + url_success = reverse_lazy("ecole_recherche_ent", kwargs={}) + # Rediriger vers une page dédiée (sans ajouter l'école) + return HttpResponseRedirect(url_success) + + # Sinon, on garde le comportement classique (création de l'école) + return super().form_valid(form) + class Ajouter(Page, crud.Ajouter): form_class = Formulaire diff --git a/noethysweb/parametrage/views/parametres_ent.py b/noethysweb/parametrage/views/parametres_ent.py new file mode 100644 index 00000000..ec3217d1 --- /dev/null +++ b/noethysweb/parametrage/views/parametres_ent.py @@ -0,0 +1,55 @@ +from django.views.generic import FormView +from django.shortcuts import render +from django.contrib import messages +from django.urls import reverse_lazy +from django import forms +from django.views.generic import TemplateView +from core.views.base import CustomView +from django.shortcuts import redirect +from parametrage.forms.parameters_ent import Formulaire +from django.http import HttpResponseRedirect +import django.contrib.messages + + +class Modifier(CustomView, TemplateView): + template_name = "core/crud/edit.html" + compatible_demo = False + + def get_context_data(self, **kwargs): + context = super(Modifier, self).get_context_data(**kwargs) + context['page_titre'] = "Paramètres ENT" + context['box_titre'] = "Paramètres" + context['box_introduction'] = "Ajustez les paramètres de ENT pour utiliser les données récupérés de l'ENT." + context['form'] = Formulaire() + return context + + def post(self, request, **kwargs): + form = Formulaire(request.POST, request=self.request) + if not form.is_valid(): + django.contrib.messages.error(request, 'Aucun paramétre coché!') + return self.render_to_response(self.get_context_data(form=form)) + + # Enregistrement + # dict_parametres = {parametre.code: parametre for parametre in PortailParametre.objects.all()} + # liste_modifications = [] + # for code, valeur in form.cleaned_data.items(): + # if code in dict_parametres: + # dict_parametres[code].valeur = str(valeur) + # liste_modifications.append(dict_parametres[code]) + # else: + # PortailParametre.objects.create(code=code, valeur=str(valeur)) + # if liste_modifications: + # PortailParametre.objects.bulk_update(liste_modifications, ["valeur"]) + + # # Stocker les états des cases à cocher dans la session + # request.session['compte_individu_active'] = form.cleaned_data.get("compte_individu", False) + # request.session['compte_famille_active'] = form.cleaned_data.get("compte_famille", False) + # cache.delete("parametres_portail") + + django.contrib.messages.success(request, 'Paramètres enregistrés') + return HttpResponseRedirect(reverse_lazy("parametres_ent")) + """ + Traite les erreurs du formulaire + """ + messages.error(self.request, 'Veuillez corriger les erreurs ci-dessous.') + return super().form_invalid(form) \ No newline at end of file