From 56a64df39479cd67ef9908e04b21d89890a6c312 Mon Sep 17 00:00:00 2001 From: mustaphaBersellou Date: Tue, 9 Sep 2025 09:56:37 +0200 Subject: [PATCH 01/12] add Importer des individus ENT --- .../migrations/0191_auto_20250903_1421.py | 155 +++++++++++++++++ .../core/migrations/0192_individu_ent_id.py | 19 +++ noethysweb/core/models.py | 1 + noethysweb/core/views/menu.py | 1 + .../individus/forms/importer_individus_ent.py | 66 ++++++++ .../individus/importer_individus_ent.html | 130 ++++++++++++++ noethysweb/individus/urls.py | 5 +- .../individus/views/importer_individus_ent.py | 159 ++++++++++++++++++ noethysweb/portail/views/auto_login.py | 106 ++++++++++++ 9 files changed, 640 insertions(+), 2 deletions(-) create mode 100644 noethysweb/core/migrations/0191_auto_20250903_1421.py create mode 100644 noethysweb/core/migrations/0192_individu_ent_id.py create mode 100644 noethysweb/individus/forms/importer_individus_ent.py create mode 100644 noethysweb/individus/templates/individus/importer_individus_ent.html create mode 100644 noethysweb/individus/views/importer_individus_ent.py create mode 100644 noethysweb/portail/views/auto_login.py diff --git a/noethysweb/core/migrations/0191_auto_20250903_1421.py b/noethysweb/core/migrations/0191_auto_20250903_1421.py new file mode 100644 index 00000000..e46b9c8d --- /dev/null +++ b/noethysweb/core/migrations/0191_auto_20250903_1421.py @@ -0,0 +1,155 @@ +# Generated by Django 3.2.25 on 2025-09-03 14:21 + +import core.models +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django_cryptography.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0190_auto_20250517_1944'), + ] + + operations = [ + migrations.CreateModel( + name='Utilisateur_Individu', + fields=[ + ], + options={ + 'verbose_name': 'Individu', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('core.utilisateur',), + managers=[ + ('objects', core.models.CustomUserManager()), + ], + ), + migrations.AlterModelOptions( + name='utilisateur', + options={'permissions': [('famille_resume', 'Fiche famille | Résumé'), ('famille_questionnaire', 'Fiche famille | Questionnaire'), ('famille_questionnaire_modifier', 'Fiche famille | Questionnaire | Modifier'), ('famille_pieces', 'Fiche famille | Pièces'), ('famille_pieces_modifier', 'Fiche famille | Pièces | Modifier'), ('famille_locations', 'Fiche famille | Locations'), ('famille_locations_modifier', 'Fiche famille | Locations | Modifier'), ('famille_cotisations', 'Fiche famille | Adhésions'), ('famille_cotisations_modifier', 'Fiche famille | Adhésions | Modifier'), ('famille_caisse', 'Fiche famille | Caisse'), ('famille_caisse_modifier', 'Fiche famille | Caisse | Modifier'), ('famille_aides', 'Fiche famille | Aides'), ('famille_aides_modifier', 'Fiche famille | Aides | Modifier'), ('famille_quotients', 'Fiche famille | Quotients familiaux'), ('famille_quotients_modifier', 'Fiche famille | Quotients familiaux | Modifier'), ('famille_prestations', 'Fiche famille | Prestations'), ('famille_prestations_modifier', 'Fiche famille | Prestations | Modifier'), ('famille_factures', 'Fiche famille | Factures'), ('famille_factures_modifier', 'Fiche famille | Factures | Modifier'), ('famille_reglements', 'Fiche famille | Règlements'), ('famille_reglements_modifier', 'Fiche famille | Règlements | Modifier'), ('famille_messagerie', 'Fiche famille | Messagerie'), ('famille_messagerie_modifier', 'Fiche famille | Messagerie | Modifier'), ('famille_portail', 'Fiche famille | Portail'), ('famille_portail_modifier', 'Fiche famille | Portail | Modifier'), ('famille_divers', 'Fiche famille | Paramètres'), ('famille_divers_modifier', 'Fiche famille | Paramètres | Modifier'), ('famille_outils', 'Fiche famille | Outils'), ('famille_consommations', 'Fiche famille | Consommations'), ('famille_consommations_modifier', 'Fiche famille | Consommations | Modifier'), ('individu_resume', 'Fiche individuelle | Résumé'), ('individu_identite', 'Fiche individuelle | Identité'), ('individu_identite_modifier', 'Fiche individuelle | Identité | Modifier'), ('individu_questionnaire', 'Fiche individuelle | Questionnaire'), ('individu_questionnaire_modifier', 'Fiche individuelle | Questionnaire | Modifier'), ('individu_liens', 'Fiche individuelle | Liens'), ('individu_liens_modifier', 'Fiche individuelle | Liens | Modifier'), ('individu_coords', 'Fiche individuelle | Coordonnées'), ('individu_coords_modifier', 'Fiche individuelle | Coordonnées | Modifier'), ('individu_scolarite', 'Fiche individuelle | Scolarité'), ('individu_scolarite_modifier', 'Fiche individuelle | Scolarité | Modifier'), ('individu_inscriptions', 'Fiche individuelle | Inscriptions'), ('individu_inscriptions_modifier', 'Fiche individuelle | Inscriptions | Modifier'), ('individu_regimes_alimentaires', 'Fiche individuelle | Régimes alimentaires'), ('individu_regimes_alimentaires_modifier', 'Fiche individuelle | Régimes alimentaires | Modifier'), ('individu_maladies', 'Fiche individuelle | Maladies'), ('individu_maladies_modifier', 'Fiche individuelle | Maladies | Modifier'), ('individu_medical', 'Fiche individuelle | Médical'), ('individu_medical_modifier', 'Fiche individuelle | Médical | Modifier'), ('individu_assurances', 'Fiche individuelle | Assurances'), ('individu_assurances_modifier', 'Fiche individuelle | Assurances | Modifier'), ('individu_portail', 'Fiche individuelle | Portail'), ('individu_portail_modifier', 'Fiche individuelle | Portail | Modifier'), ('individu_contacts', 'Fiche individuelle | Contacts'), ('individu_contacts_modifier', 'Fiche individuelle | Contacts | Modifier'), ('individu_transports', 'Fiche individuelle | Transports'), ('individu_transports_modifier', 'Fiche individuelle | Transports | Modifier'), ('individu_consommations', 'Fiche individuelle | Consommations')]}, + ), + migrations.AddField( + model_name='consentement', + name='individu', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.individu', verbose_name='Individu'), + ), + migrations.AddField( + model_name='destinataire', + name='activites', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.activite', verbose_name='Activites'), + ), + migrations.AddField( + model_name='destinataire', + name='inscription', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.inscription', verbose_name='Inscription'), + ), + migrations.AddField( + model_name='famille', + name='contact_facturation', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='contact_facturation', to='core.individu', verbose_name='Contact facturation'), + ), + migrations.AddField( + model_name='individu', + name='blocage_impayes_off', + field=models.BooleanField(default=False, help_text="En cochant cette case, vous permettez à cette famille d'accéder aux réservations du portail même s'il y a des impayés et que le paramètre 'blocage si impayés' a été activé dans les paramètres généraux du portail.", verbose_name='Ne jamais appliquer le blocage des réservations si impayés'), + ), + migrations.AddField( + model_name='individu', + name='certification_date', + field=models.DateTimeField(blank=True, null=True, verbose_name='Date de certification'), + ), + migrations.AddField( + model_name='individu', + name='internet_actif', + field=models.BooleanField(default=True, verbose_name='Compte internet activé'), + ), + migrations.AddField( + model_name='individu', + name='internet_categorie', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='internet_categori', to='core.categoriecompteinternet', verbose_name='Catégorie'), + ), + migrations.AddField( + model_name='individu', + name='internet_identifiant', + field=django_cryptography.fields.encrypt(models.CharField(blank=True, max_length=200, null=True, verbose_name='Identifiant')), + ), + migrations.AddField( + model_name='individu', + name='internet_mdp', + field=django_cryptography.fields.encrypt(models.CharField(blank=True, max_length=200, null=True, verbose_name='Mot de passe')), + ), + migrations.AddField( + model_name='individu', + name='internet_reservations', + field=models.BooleanField(default=True, verbose_name='Autoriser les réservations sur le portail'), + ), + migrations.AddField( + model_name='individu', + name='internet_secquest', + field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Question'), + ), + migrations.AddField( + model_name='individu', + name='mobile', + field=django_cryptography.fields.encrypt(models.CharField(blank=True, max_length=100, null=True, verbose_name='Portable favori')), + ), + migrations.AddField( + model_name='individu', + name='utilisateur', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='portailmessage', + name='individu', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='core.individu', verbose_name='Individu'), + ), + migrations.AlterField( + model_name='adressemail', + name='adresse', + field=models.EmailField(help_text="Saisissez l'adresse mail utilisée.", max_length=300, verbose_name="Adresse d'envoi"), + ), + migrations.AlterField( + model_name='portailmessage', + name='date_creation', + field=models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Date de création'), + ), + migrations.AlterField( + model_name='portailmessage', + name='date_lecture', + field=models.DateTimeField(blank=True, db_index=True, max_length=200, null=True, verbose_name='Date de lecture'), + ), + migrations.AlterField( + model_name='portailparametre', + name='code', + field=models.CharField(blank=True, max_length=200, null=True, unique=True, verbose_name='Code'), + ), + migrations.AddIndex( + model_name='portailmessage', + index=models.Index(fields=['famille'], name='portail_mes_famille_fd123f_idx'), + ), + migrations.AddIndex( + model_name='portailmessage', + index=models.Index(fields=['individu'], name='portail_mes_individ_e5b04e_idx'), + ), + migrations.AddIndex( + model_name='portailmessage', + index=models.Index(fields=['structure'], name='portail_mes_structu_e1e078_idx'), + ), + migrations.AddIndex( + model_name='portailmessage', + index=models.Index(fields=['utilisateur'], name='portail_mes_utilisa_16d110_idx'), + ), + migrations.AddIndex( + model_name='portailmessage', + index=models.Index(fields=['date_creation'], name='portail_mes_date_cr_58d8e7_idx'), + ), + migrations.AddIndex( + model_name='portailmessage', + index=models.Index(fields=['date_lecture'], name='portail_mes_date_le_d93ed6_idx'), + ), + ] diff --git a/noethysweb/core/migrations/0192_individu_ent_id.py b/noethysweb/core/migrations/0192_individu_ent_id.py new file mode 100644 index 00000000..92167e8b --- /dev/null +++ b/noethysweb/core/migrations/0192_individu_ent_id.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.25 on 2025-09-08 10:27 + +from django.db import migrations, models +import django_cryptography.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0191_auto_20250903_1421'), + ] + + operations = [ + migrations.AddField( + model_name='individu', + name='ent_id', + field=django_cryptography.fields.encrypt(models.IntegerField(blank=True, null=True, verbose_name='ent_id')), + ), + ] diff --git a/noethysweb/core/models.py b/noethysweb/core/models.py index ef978d6d..60dbe676 100644 --- a/noethysweb/core/models.py +++ b/noethysweb/core/models.py @@ -1665,6 +1665,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 = encrypt(models.IntegerField(verbose_name=_("ent_id"), 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) diff --git a/noethysweb/core/views/menu.py b/noethysweb/core/views/menu.py index 56466071..e282c9d7 100644 --- a/noethysweb/core/views/menu.py +++ b/noethysweb/core/views/menu.py @@ -240,6 +240,7 @@ 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="importer_individus_ent", titre="Importer des individus de l'ENT", icone="file-text-o") # Inscriptions menu_inscriptions = menu_individus.Add(titre="Inscriptions") diff --git a/noethysweb/individus/forms/importer_individus_ent.py b/noethysweb/individus/forms/importer_individus_ent.py new file mode 100644 index 00000000..c9a4408d --- /dev/null +++ b/noethysweb/individus/forms/importer_individus_ent.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019-2021 Ivan LUCAS. +# Noethysweb, application de gestion multi-activités. +# Distribué sous licence GNU GPL. + +from django import forms +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, HTML, Fieldset, ButtonHolder +from crispy_forms.bootstrap import Field, StrictButton +from core.forms.base import FormulaireBase +from core.widgets import DatePickerWidget, Telephone + + +class Formulaire(FormulaireBase, forms.Form): + nom = forms.CharField(label="Nom de famille", required=False) + prenom = forms.CharField(label="Prénom", required=False) + date_naiss = forms.DateField(label="Date de Naissance", required=False, widget=DatePickerWidget()) + rue_resid = forms.CharField(label="Rue", required=False) + cp_resid = forms.CharField(label="Code postal", required=False) + ville_resid = forms.CharField(label="Ville", required=False) + tel_domicile = forms.CharField(label="Tél fixe", required=False) + tel_mobile = forms.CharField(label="Tél portable", required=False) + travail_tel = forms.CharField(label="Tél pro.", required=False) + mail = forms.CharField(label="Email", required=False) + + class Meta: + widgets = { + 'tel_domicile': Telephone(), + 'tel_mobile': Telephone(), + 'travail_tel': Telephone(), + } + + def __init__(self, *args, **kwargs): + super(Formulaire, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_id = 'importer_individus_ent_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( + ButtonHolder( + StrictButton("Rechercher", title="Rechercher", name="rechercher", type="submit", css_class="btn-primary"), + HTML("""Annuler """), + css_class="mb-3", + ), + Fieldset("Etat-civil", + Field("nom"), + Field("prenom"), + Field("date_naiss"), + ), + Fieldset("Adresse", + Field("rue_resid"), + Field("cp_resid"), + Field("ville_resid"), + ), + Fieldset("Coordonnées", + Field("tel_domicile"), + Field("tel_mobile"), + Field("travail_tel"), + Field("mail"), + ), + ) diff --git a/noethysweb/individus/templates/individus/importer_individus_ent.html b/noethysweb/individus/templates/individus/importer_individus_ent.html new file mode 100644 index 00000000..7078dee4 --- /dev/null +++ b/noethysweb/individus/templates/individus/importer_individus_ent.html @@ -0,0 +1,130 @@ +{% extends "core/page.html" %} +{% load crispy_forms_tags %} +{% load static %} +{% load embed %} + + +{% block styles %} + {{ block.super }} + +{% endblock %} + + +{% block contenu_page %} + +
+ Revenir aux paramètres de recherche +
+ +{% if resultats_ent %} +
+
+

Individus

+
+
+ + + + + + + + + + + + {% for item in resultats_ent %} + + + + + + + + {% endfor %} + +
NomPrenomAdresseEmailActions
{{ item.nom }}{{ item.prenom }}{{ item.adresse|default:"" }} {{ item.ville|default:"" }} {{ item.code_postal|default:"" }}{{ item.email|default:"" }} + +
+
+
+{% endif %} + +{# Si recherche infructueuse #} +{% if not resultats_ent %} + Aucun résultat pour cette recherche. +{% endif %} + + + +{% endblock contenu_page %} diff --git a/noethysweb/individus/urls.py b/noethysweb/individus/urls.py index b8cf7498..6f6d2d1d 100644 --- a/noethysweb/individus/urls.py +++ b/noethysweb/individus/urls.py @@ -14,7 +14,7 @@ edition_contacts, edition_renseignements, edition_informations, liste_photos_manquantes, recherche_avancee, inscriptions_modifier, \ liste_titulaires_helios, inscriptions_activite_liste, effacer_familles, liste_transports, liste_progtransports, inscriptions_changer_groupe, \ abonnes_listes_diffusion, abonnes_listes_diffusion_ajouter, liste_mails, imprimer_liste_inscrits, sondages_reponses, certifications, \ - certifications_individus, certifications_familles, inscriptions_saisir_lot, importer_individus, importer_quotients + certifications_individus, certifications_familles, inscriptions_saisir_lot, importer_individus, importer_individus_ent, importer_quotients urlpatterns = [ @@ -33,7 +33,8 @@ 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'), - + path('individus/importer_individus_ent', importer_individus_ent.View.as_view(), name='importer_individus_ent'), + path('individus/ajouter_individu_ent', importer_individus_ent.ajouter_individu, name='ajouter_individu_ent'), # 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/individus/views/importer_individus_ent.py b/noethysweb/individus/views/importer_individus_ent.py new file mode 100644 index 00000000..0b2df7a9 --- /dev/null +++ b/noethysweb/individus/views/importer_individus_ent.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019-2021 Ivan LUCAS. +# Noethysweb, application de gestion multi-activités. +# Distribué sous licence GNU GPL. + +import logging, datetime +logger = logging.getLogger(__name__) +from django.shortcuts import render +from django.views.generic import TemplateView +from django.contrib import messages +from core.views.base import CustomView +from core.models import Utilisateur, Individu +from individus.forms.importer_individus_ent import Formulaire +from django.http import JsonResponse +from fiche_famille.utils import utils_internet + +def ajouter_individu(request): + if request.method == "POST": + try: + if Individu.objects.filter(ent_id=request.POST.get("entid")).exists(): + return JsonResponse({"success": False, "error": "Cet utilisateur existe déjà"}) + individu = Individu.objects.create( + ent_id=request.POST.get("entid"), + nom=request.POST.get("nom"), + prenom=request.POST.get("prenom"), + mail=request.POST.get("email"), + date_naiss=request.POST.get("date_naissance"), + rue_resid=request.POST.get("adresse"), + ville_resid=request.POST.get("ville"), + cp_resid=request.POST.get("codepostal"), + tel_mobile=request.POST.get("telephone_portable"), + tel_domicile=request.POST.get("telephone_domicile"), + tel_fax=request.POST.get("telephone_fixe"), + ) + 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() + return JsonResponse({"success": True, "id": individu.idindividu}) + except Exception as e: + return JsonResponse({"success": False, "error": str(e)}) + return JsonResponse({"success": False, "error": "Méthode non autorisée"}) + + +class View(CustomView, TemplateView): + menu_code = "importer_individus_ent" + template_name = "core/crud/edit.html" + + def get_context_data(self, **kwargs): + context = super(View, self).get_context_data(**kwargs) + context['page_titre'] = "Importer des individus de l'ENT" + context['box_titre'] = "Effectuer une recherche des individus de L'ENT" + context['box_introduction'] = "Saisissez un ou plusieurs critères de recherche et cliquez sur le bouton Rechercher." + context['form'] = context.get("form", Formulaire) + return context + + def post(self, request, **kwargs): + # Validation du form + form = Formulaire(request.POST, request.FILES, request=self.request) + if not form.is_valid(): + return self.render_to_response(self.get_context_data(form=form)) + + # Champs de recherche + champs_recherche = form.changed_data + # Recherche des résultats + import jellyfish + + resultats = {} + # rattachements = Rattachement.objects.select_related("individu", "famille").all() + # for rattachement in rattachements: + # score = resultats.get(rattachement, 0) + # + # for nom_champ in champs_recherche: + # valeur_recherche = form.cleaned_data[nom_champ] + # valeur_individu = getattr(rattachement.individu, nom_champ, "") + # + # # Recherche de texte + # if isinstance(valeur_individu, str): + # try: + # distance = jellyfish.jaro_distance(valeur_recherche.lower(), valeur_individu.lower()) + # except: + # distance = jellyfish.jaro_similarity(valeur_recherche.lower(), valeur_individu.lower()) + # score += distance + # + # if score >= 0.75: + # resultats[rattachement] = score + # + # # Recherche de date + # if isinstance(valeur_individu, datetime.date): + # if valeur_recherche == valeur_individu: + # resultats[rattachement] = score + # + # # Tri par score + # resultats = sorted([(score, rattachement) for rattachement, score in resultats.items()], key=lambda donnees: donnees[0], reverse=True) + resultats = [ + { + "ent_id": "550e8400-e29b-41d4-a716-446655440000", + "civilite": "M.", + "nom": "Dupont", + "nom_usage": "Durand", + "prenom": "Jean", + "date_naissance": "1985-07-12", + "adresse": "12 Rue de la République Bâtiment A Appartement 45", + "ville": "Paris", + "code_postal": "75001", + "telephone_fixe": "+33 1 44 55 66 77", + "telephone_domicile": "+33 1 40 22 33 44", + "telephone_portable": "+33 6 12 34 56 78", + "email": "jean.dupont@example.com" + }, + { + "ent_id": "660e8400-e29b-41d4-a716-446655440111", + "civilite": "Mme", + "nom": "Martin", + "nom_usage": "Leroy", + "prenom": "Sophie", + "date_naissance": "1990-03-25", + "adresse": "8 Avenue Victor Hugo", + "ville": "Lyon", + "code_postal": "69002", + "telephone_fixe": "+33 4 78 12 34 56", + "telephone_domicile": "+33 4 78 65 43 21", + "telephone_portable": "+33 6 98 76 54 32", + "email": "sophie.martin@example.com" + }, + { + "ent_id": "770e8400-e29b-41d4-a716-446655440222", + "civilite": "M.", + "nom": "Bernard", + "nom_usage": "", + "prenom": "Paul", + "date_naissance": "1978-11-02", + "adresse": "35 Boulevard Saint-Michel", + "ville": "Marseille", + "code_postal": "13006", + "telephone_fixe": "+33 4 91 23 45 67", + "telephone_domicile": "+33 4 91 11 22 33", + "telephone_portable": "+33 6 33 44 55 66", + "email": "paul.bernard@example.com" + } + ] + # Envoi des 50 premiers résultats + context = self.get_context_data(**kwargs) + context["resultats_ent"] = resultats[:50] # pas besoin de [r[1] for r in ...] + return render(request, "individus/importer_individus_ent.html", context) diff --git a/noethysweb/portail/views/auto_login.py b/noethysweb/portail/views/auto_login.py new file mode 100644 index 00000000..b17d21a4 --- /dev/null +++ b/noethysweb/portail/views/auto_login.py @@ -0,0 +1,106 @@ +from django.shortcuts import render, redirect +from django.contrib.auth import login, get_user_model +from django.http import HttpResponseBadRequest +from django.views.decorators.csrf import csrf_exempt +from django.utils.decorators import method_decorator +from django.views import View +from django.conf import settings +import jwt +from datetime import datetime, timedelta +from django.urls import reverse_lazy + +User = get_user_model() + +class AutoLoginView(View): + """ + Vue pour l'authentification automatique depuis une autre application avec JWT + """ + + @method_decorator(csrf_exempt) + def dispatch(self, *args, **kwargs): + return super().dispatch(*args, **kwargs) + + def get(self, request): + """Traitement de l'authentification automatique avec JWT""" + + # 1. Récupérer le token JWT + token = request.GET.get("token") + if not token: + return redirect("/") + + try: + # 2. Décoder et vérifier le token + payload = jwt.decode(token, settings.SSO_SECRET_KEY, algorithms=["HS256"]) + + # 3. Extraire les données utilisateur + username = payload["username"] + email = payload["email"] + + # Données optionnelles + first_name = payload.get("first_name", "") + last_name = payload.get("last_name", "") + + except jwt.InvalidTokenError: + return redirect("/") + except KeyError: + # Si des champs obligatoires manquent dans le payload + return redirect("/") + + # 4. Chercher ou créer l'utilisateur + # try: + user = self._get_or_create_user( + username=username, + email=email, + first_name=first_name, + last_name=last_name, + ) + # 5. Connecter automatiquement l'utilisateur + login(request, user, backend='django.contrib.auth.backends.ModelBackend') + # 6. Redirection + return redirect(reverse_lazy("portail_accueil")) + + # except Exception as e: + # # Log l'erreur en production + # print(f"Erreur lors de la connexion automatique: {str(e)}") + # return redirect("/") + + def _get_or_create_user(self, username, email, first_name="", last_name=""): + """ + Récupérer un utilisateur existant ou en créer un nouveau + """ + try: + # Chercher d'abord par email (plus fiable) + print("///////") + print(username) + print("///////") + user = User.objects.get(username=username) + # Mettre à jour les informations si nécessaire + updated = False + if first_name and not user.first_name: + user.first_name = first_name + updated = True + if last_name and not user.last_name: + user.last_name = last_name + updated = True + if updated: + user.save() + + return user + + except User.DoesNotExist: + # Créer un nouvel utilisateur + + # S'assurer que le username est unique + original_username = username + counter = 1 + while User.objects.filter(username=username).exists(): + username = f"{original_username}_{counter}" + counter += 1 + + user = User.objects.create_user( + username=username, + email=email, + first_name=first_name, + last_name=last_name, + ) + return user \ No newline at end of file From 4a403e74bbd11d653faeac00e2da30ff3a113bac Mon Sep 17 00:00:00 2001 From: mustaphaBersellou Date: Fri, 17 Oct 2025 12:17:09 +0200 Subject: [PATCH 02/12] ENT Modifcations --- .../collaborateur_recherche_ent.html | 181 +++++ .../collaborateur_synchronisation.html | 386 ++++++++++ noethysweb/collaborateurs/urls.py | 4 +- .../collaborateurs/views/collaborateur.py | 34 +- .../collaborateurs/views/collaborateur_ent.py | 224 ++++++ .../migrations/0193_collaborateur_ent_id.py | 18 + noethysweb/core/utils/utils_ent.py | 664 ++++++++++++++++++ noethysweb/core/views/menu.py | 3 + .../fiche_famille/famille_ent_liste.html | 231 ++++++ .../fiche_famille/familles_synchro.html | 538 ++++++++++++++ noethysweb/fiche_famille/urls.py | 5 +- .../fiche_famille/views/famille_ajouter.py | 20 +- noethysweb/fiche_famille/views/famille_ent.py | 629 +++++++++++++++++ .../fiche_individu/individu_liste_maj.html | 250 +++++++ .../fiche_individu/individu_update_ent.html | 416 +++++++++++ noethysweb/fiche_individu/urls.py | 5 +- .../fiche_individu/views/individu_ent.py | 313 +++++++++ .../parametrage/forms/parameters_ent.py | 158 +++++ .../templates/parametrage/ecole_ent.html | 387 ++++++++++ .../templates/parametrage/parametres_ent.html | 49 ++ noethysweb/parametrage/urls.py | 5 +- noethysweb/parametrage/views/ecole_ent.py | 87 +++ noethysweb/parametrage/views/ecoles.py | 42 +- .../parametrage/views/parametres_ent.py | 55 ++ 24 files changed, 4693 insertions(+), 11 deletions(-) create mode 100644 noethysweb/collaborateurs/templates/collaborateurs/collaborateur_recherche_ent.html create mode 100644 noethysweb/collaborateurs/templates/collaborateurs/collaborateur_synchronisation.html create mode 100644 noethysweb/collaborateurs/views/collaborateur_ent.py create mode 100644 noethysweb/core/migrations/0193_collaborateur_ent_id.py create mode 100644 noethysweb/core/utils/utils_ent.py create mode 100644 noethysweb/fiche_famille/templates/fiche_famille/famille_ent_liste.html create mode 100644 noethysweb/fiche_famille/templates/fiche_famille/familles_synchro.html create mode 100644 noethysweb/fiche_famille/views/famille_ent.py create mode 100644 noethysweb/fiche_individu/templates/fiche_individu/individu_liste_maj.html create mode 100644 noethysweb/fiche_individu/templates/fiche_individu/individu_update_ent.html create mode 100644 noethysweb/fiche_individu/views/individu_ent.py create mode 100644 noethysweb/parametrage/forms/parameters_ent.py create mode 100644 noethysweb/parametrage/templates/parametrage/ecole_ent.html create mode 100644 noethysweb/parametrage/templates/parametrage/parametres_ent.html create mode 100644 noethysweb/parametrage/views/ecole_ent.py create mode 100644 noethysweb/parametrage/views/parametres_ent.py 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 %} + + +{% 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/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/migrations/0193_collaborateur_ent_id.py b/noethysweb/core/migrations/0193_collaborateur_ent_id.py new file mode 100644 index 00000000..106796ee --- /dev/null +++ b/noethysweb/core/migrations/0193_collaborateur_ent_id.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2025-09-22 10:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0192_auto_20250917_1231'), + ] + + operations = [ + migrations.AddField( + model_name='collaborateur', + name='ent_id', + field=models.CharField(max_length=200, null=True, verbose_name='ent_id'), + ), + ] diff --git a/noethysweb/core/utils/utils_ent.py b/noethysweb/core/utils/utils_ent.py new file mode 100644 index 00000000..f308b9de --- /dev/null +++ b/noethysweb/core/utils/utils_ent.py @@ -0,0 +1,664 @@ +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 = [ + { + "famille_id": 1, + "nom_famille": "Famille DUPONT", + "representants": [ + { + "id_ent": "user179", + "civilite": "Monsieur", + "prenom": "Test10", + "nom": "Test10", + "email": "jean.dupont@mail.fr", + "telephone": "0601020304", + }, + { + "id_ent": "user124", + "civilite": "Madame", + "prenom": "Marie", + "nom": "DUPONT", + "email": "marie.dupont@mail.fr", + "telephone": "0605060708", + }, + ], + "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", + }, + }, + } + ], + }, + { + "famille_id": 2, + "nom_famille": "Famille MARTIN", + "representants": [ + { + "id_ent": "user2087", + "civilite": "Monsieur", + "prenom": "Test10", + "nom": "Test10", + "email": "Testent.Testent@mail.fr", + "telephone": "0610101010", + } + ], + "enfants": [ + { + "id_ent": "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", + }, + }, + } + ], + }, + ] + + result = [] + organisateur = Organisateur.objects.filter(pk=1).first() + if organisateur.ent_active: + for famille in exemple_users: + match_reps = [ + rep for rep in famille["representants"] + if 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_reps 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": "0601020304", + }, + { + "ent_id": "user124", + "civilite": "Madame", + "prenom": "Marie", + "nom": "DUPONT", + "email": "marie.dupont@mail.fr", + "telephone": "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": "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_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 e282c9d7..6e731bbd 100644 --- a/noethysweb/core/views/menu.py +++ b/noethysweb/core/views/menu.py @@ -27,6 +27,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") # Activités menu_activites = menu_parametrage.Add(titre="Activités") @@ -241,6 +242,8 @@ def GetMenuPrincipal(parametres_generaux=None, organisateur=None, user=None): 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="importer_individus_ent", titre="Importer des individus de l'ENT", 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_ent_liste.html b/noethysweb/fiche_famille/templates/fiche_famille/famille_ent_liste.html new file mode 100644 index 00000000..85d57824 --- /dev/null +++ b/noethysweb/fiche_famille/templates/fiche_famille/famille_ent_liste.html @@ -0,0 +1,231 @@ +{% 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.nom_famille }} +
+ {% csrf_token %} + + +
+
+
+ + +
+ Représentants +
+ {% for rep in famille.representants %} +
+
+ {{ rep.civilite }} {{ rep.prenom }} {{ rep.nom }} +
+
+ 📧 {{ rep.email }} +
+
+ 📞 {{ rep.telephone }} +
+
+ {% empty %} +
Aucun représentant
+ {% endfor %} + + +
+ Enfants +
+ {% for enfant in famille.enfants %} +
+
+ {{ enfant.civilite }} {{ enfant.prenom }} {{ enfant.nom }} +
+
+ 🏫 {{ enfant.ecole.nom }} +
+
+ 📘 Classe: {{ enfant.classe.nom }} +
+
+ {% empty %} +
Aucun enfant
+ {% endfor %} +
+
+ {% endfor %} +{% else %} +
+ +

Aucun résultat pour cette recherche.

+
+{% endif %} +{% 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..9bd5c340 --- /dev/null +++ b/noethysweb/fiche_famille/templates/fiche_famille/familles_synchro.html @@ -0,0 +1,538 @@ +{% 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..7f67cbd7 100644 --- a/noethysweb/fiche_famille/urls.py +++ b/noethysweb/fiche_famille/urls.py @@ -10,20 +10,23 @@ 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/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..750d5716 --- /dev/null +++ b/noethysweb/fiche_famille/views/famille_ent.py @@ -0,0 +1,629 @@ +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 +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'un ajout de toute une famille ou juste un individu à une famille + individu_id = request.POST.get("individu_id") + + if action == "ajouter_nouvel_individu": + # 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 idfamille: + new_famille = Famille.objects.get(pk=idfamille) + + else: + new_famille = self.creation_famille() + self.creation_nouvel_individu(search_info, new_famille) + 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 + for rep in famille.get("representants", []): + if str(rep.get("id_ent")) == str(individu_id): + return rep + + # 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() + + # Création / récupération classe + classe= Classe.objects.get_or_create( + nom=scolarite.get("classe", {}).get("nom"),ecole=ecole).first() + + # 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=scolarite.get("date_debut"), + date_fin=scolarite.get("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', []) + + 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 + ent_result = get_ent_user_info(individu.nom, individu.prenom) + + if not ent_result: + errors.append(f"{individu.prenom} {individu.nom}: Données API introuvables") + continue + + individu_api = ent_result[0] if isinstance(ent_result, list) else ent_result + + # 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 + Adapter selon votre modèle (CharField, IntegerField avec choices, FK, etc.) + """ + # Si c'est un CharField, retourner tel quel + # Vérifier le type de champ dans votre modèle Individu + from django.db.models import CharField, IntegerField + + field = Individu._meta.get_field('civilite') + + if isinstance(field, IntegerField): + # Si c'est un IntegerField avec choices (1=M, 2=Mme, etc.) + mapping_civilite = { + 'M.': 1, + 'M': 1, + 'Monsieur': 1, + 'Mme': 2, + 'Madame': 2, + 'Mlle': 3, + 'Mademoiselle': 3, + } + return mapping_civilite.get(valeur, 1) # Par défaut M. + + 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 + # À adapter selon votre structure + return valeur + + def comparer_famille(self, famille): + """ + Compare les données de chaque membre de la famille avec l'API + """ + differences = { + 'representants': [], + 'enfants': [] + } + + rattachements = famille.rattachement_set.select_related('individu').all() + + for ratt in rattachements: + individu = ratt.individu + + try: + ent_result = get_ent_user_info(individu.nom, individu.prenom) + + if ent_result: + individu_api = ent_result[0] if isinstance(ent_result, list) else ent_result + diff = self.comparer_individu(individu, individu_api) + + if diff: + diff_entry = { + 'id': individu.idindividu, + 'nom': f"{individu.prenom} {individu.nom}", + 'differences': diff + } + + if ratt.categorie in [1, 3]: + differences['representants'].append(diff_entry) + else: + differences['enfants'].append(diff_entry) + else: + 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' + } + } + } + + if ratt.categorie in [1, 2]: + differences['representants'].append(diff_entry) + else: + differences['enfants'].append(diff_entry) + + except Exception as e: + diff_entry = { + 'id': individu.idindividu, + 'nom': f"{individu.prenom} {individu.nom}", + 'differences': { + 'erreur': { + 'old': 'Erreur', + 'new': f'Erreur API: {str(e)}' + } + } + } + + 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) + + valeur_db_str = self.normaliser_valeur(valeur_db) + valeur_api_str = self.normaliser_valeur(valeur_api) + + 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): + """ + Normalise une valeur pour la comparaison + """ + if valeur is None or valeur == '': + return '' + + if hasattr(valeur, 'strftime'): + return valeur.strftime('%d/%m/%Y') + + valeur_str = str(valeur).strip() + + if valeur_str and any(char.isdigit() for char in valeur_str): + 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) \ No newline at end of file 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..8672fbed --- /dev/null +++ b/noethysweb/fiche_individu/templates/fiche_individu/individu_liste_maj.html @@ -0,0 +1,250 @@ +{% 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 }}

+ + +
+
+
+ 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énomEmailTéléphoneActions
+ + + {{ individu.ent_id }} + {{ individu.civilite }}{{ individu.nom }}{{ individu.prenom }} + {{ 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/views/individu_ent.py b/noethysweb/fiche_individu/views/individu_ent.py new file mode 100644 index 00000000..aa30fd7a --- /dev/null +++ b/noethysweb/fiche_individu/views/individu_ent.py @@ -0,0 +1,313 @@ +from core.models import Scolarite, Individu, Classe, NiveauScolaire, Ecole +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 + +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 uniquement le filtre de recherche textuelle + search_query = self.request.GET.get('search', '') + + # Base query - Tous les individus avec ENT ID par défaut + individus = Individu.objects.filter(ent_id__isnull=False).exclude(ent_id='').order_by('nom', 'prenom') + + # Recherche textuelle + + # if search_query: + # individus = individus.filter( + # Q(nom__icontains=search_query) | + # Q(prenom__icontains=search_query) | + # Q(ent_id__icontains=search_query) | + # Q(mail__icontains=search_query) + # ) + + # 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 + # TODO: Récupérer l'école de l'individu selon votre modèle + # Non utilisé pour l'instant + + 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 "-", + 'mail': individu.mail or "-", + 'tel_mobile': individu.tel_mobile or "-", + }) + + context['individus'] = individus_data + context['total_count'] = individus.count() + print(individus.count()) + context['total_all_individus'] = Individu.objects.count() + print(Individu.objects.count()) + # context['search_query'] = search_query + # print(search_query) + 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." + + 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] + + # TODO: Implémenter la logique de synchronisation en masse + # Cette fonction sera développée ultérieurement + success_count = self.synchroniser_individus_masse(selected_ids) + + if success_count > 0: + messages.success( + request, + f"Synchronisation lancée avec succès pour {success_count} individu(s)." + ) + else: + messages.info(request, "Aucune synchronisation effectuée.") + + return redirect(request.path + f"?{request.GET.urlencode()}") + + except Exception as e: + messages.error(request, f"Erreur lors de la synchronisation : {str(e)}") + return redirect(request.path) + + def synchroniser_individus_masse(self, individu_ids): + """ + TODO: Fonction à développer pour la synchronisation en masse + + Args: + individu_ids: Liste des IDs des individus à synchroniser + + Returns: + int: Nombre d'individus synchronisés avec succès + """ + # Cette fonction sera implémentée plus tard + # Pour l'instant, elle ne fait rien + + # Logique à implémenter: + # 1. Pour chaque individu_id: + # - Récupérer l'individu + # - Appeler get_ent_user_info(individu.ent_id) + # - Mettre à jour les champs si données disponibles + # - Gérer les erreurs individuellement + # 2. Retourner le nombre de synchronisations réussies + + return len(individu_ids) # Placeholder \ 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 3f08e64f..c3e42e9d 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'), @@ -351,6 +352,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 From 7e365ca95a6d77f589638d76582dbb87089074e1 Mon Sep 17 00:00:00 2001 From: mustaphaBersellou Date: Mon, 20 Oct 2025 14:26:45 +0200 Subject: [PATCH 03/12] fix errors --- .../utils/utils_collaborateur.py | 1 + noethysweb/core/models.py | 5 +++- .../fiche_individu/utils/utils_individu.py | 1 + noethysweb/parametrage/forms/ecoles.py | 30 +++++++++++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) 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/core/models.py b/noethysweb/core/models.py index 60dbe676..e2af17e3 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) class Meta: db_table = 'organisateur' @@ -795,6 +796,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) @@ -1665,7 +1667,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 = encrypt(models.IntegerField(verbose_name=_("ent_id"), blank=True, null=True)) + 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 +4182,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)) 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/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 From 7e9fada2216c92ef48773b38c8faeb72130e4263 Mon Sep 17 00:00:00 2001 From: mustaphaBersellou Date: Thu, 23 Oct 2025 12:51:20 +0200 Subject: [PATCH 04/12] display persons with same name in synchronisation --- .../fiche_famille/familles_synchro.html | 260 ++++++++++++++++-- noethysweb/fiche_famille/views/famille_ent.py | 227 ++++++++++----- .../fiche_individu/views/individu_ent.py | 6 +- 3 files changed, 397 insertions(+), 96 deletions(-) diff --git a/noethysweb/fiche_famille/templates/fiche_famille/familles_synchro.html b/noethysweb/fiche_famille/templates/fiche_famille/familles_synchro.html index 9bd5c340..8627d8af 100644 --- a/noethysweb/fiche_famille/templates/fiche_famille/familles_synchro.html +++ b/noethysweb/fiche_famille/templates/fiche_famille/familles_synchro.html @@ -287,32 +287,176 @@

const csrftoken = getCookie('csrftoken'); function updateSelectedCount(familleId) { - const checkboxes = document.querySelectorAll(`#result-${familleId} .field-checkbox:checked`); + // Compter uniquement les checkboxes cochées dans les containers visibles + const allCheckboxes = document.querySelectorAll(`#result-${familleId} .field-checkbox:checked`); + let visibleCount = 0; + + allCheckboxes.forEach(checkbox => { + const fieldsContainer = checkbox.closest('.fields-container'); + // Si pas de container ou container visible, compter + if (!fieldsContainer || fieldsContainer.style.display !== 'none') { + visibleCount++; + } + }); + const countElement = document.querySelector(`#count-${familleId}`); if (countElement) { - countElement.textContent = checkboxes.length; + countElement.textContent = visibleCount; } - + const validateBtn = document.querySelector(`#result-${familleId} .btn-valider-sync`); if (validateBtn) { - validateBtn.disabled = checkboxes.length === 0; + validateBtn.disabled = visibleCount === 0; + } +} + +function groupPersonsByIndividuId(personsList) { + // Regrouper les personnes par leur ID individu + const grouped = {}; + + personsList.forEach(person => { + const key = person.id; + if (!grouped[key]) { + grouped[key] = []; + } + grouped[key].push(person); + }); + + return grouped; +} + +function createGroupedDiffHTML(persons, type, familleId, baseIndex) { + if (!persons || persons.length === 0) return ''; + + const firstPerson = persons[0]; + const hasMultiple = persons.length > 1; + const personId = firstPerson.id; + + // Récupérer le nom sans le badge (si présent) + const nomBase = firstPerson.nom ? firstPerson.nom.split('`; + html += `
`; + html += ` `; + html += `${type === 'representant' ? 'Représentant' : 'Enfant'}: ${nomBase}`; + html += `
`; + html += `
`; + + // Si plusieurs personnes trouvées, afficher les radios + if (hasMultiple) { + const personKey = `person-${personId}-${type}`; + html += `
`; + html += ` `; + html += `${persons.length} personnes trouvées - Sélectionnez la bonne:`; + + persons.forEach((person, idx) => { + html += `
`; + html += ``; + html += ``; + html += `
`; + }); + + html += `
`; } + + // Afficher les champs pour chaque personne (masqués sauf la première) + persons.forEach((person, idx) => { + const isVisible = idx === 0; + const containerId = `fields-container-${personId}-${type}-${idx}`; + + html += `
`; + + if (person.differences && typeof person.differences === 'object') { + const diffEntries = Object.entries(person.differences); + const isNotFound = diffEntries.length === 1 && + (diffEntries[0][0] === 'statut' || diffEntries[0][0] === 'erreur'); + + if (isNotFound) { + const [field, values] = diffEntries[0]; + html += `
`; + html += ` `; + html += `${values.new}`; + html += `
`; + } else if (diffEntries.length > 0) { + html += `
`; + html += ``; + html += ``; + html += `
`; + + diffEntries.forEach(([field, values], fieldIndex) => { + const fieldId = `${type}-${baseIndex}-${idx}-field-${fieldIndex}`; + html += `
`; + html += `
`; + html += `
${field}
`; + html += `
`; + html += `${values.old || 'vide'}`; + html += ``; + html += `${values.new || 'vide'}`; + html += `
`; + html += `
`; + html += `
`; + html += ``; + html += `
`; + html += `
`; + }); + } + } + + html += `
`; // Fin fields-container + }); + + html += `
`; + return html; } function createDiffHTML(person, type, familleId, index) { - let html = `
  • `; + const hasMultiple = person.multiple_personnes || false; + const indexPersonne = person.index_personne || 0; + const totalPersonnes = person.total_personnes || 1; + + let html = `
  • `; html += `
    `; html += ` ${type === 'representant' ? 'Représentant' : 'Enfant'}: ${person.nom || 'ID ' + person.id}`; html += `
    `; html += `
    `; - + + // Si plusieurs personnes trouvées, afficher d'abord un sélecteur radio + if (hasMultiple) { + // IMPORTANT: le nom du radio doit être le même pour toutes les personnes avec le même ID + const personKey = `person-${person.id}-${type}`; + html += `
    `; + html += ` `; + html += `Plusieurs personnes trouvées - Sélectionnez la bonne personne:`; + html += `
    `; + html += ``; + html += ``; + html += `
    `; + html += `
    `; + } + if (person.differences && typeof person.differences === 'object') { const diffEntries = Object.entries(person.differences); - + // Vérifier si c'est un cas "non trouvé" ou "erreur" - const isNotFound = diffEntries.length === 1 && + const isNotFound = diffEntries.length === 1 && (diffEntries[0][0] === 'statut' || diffEntries[0][0] === 'erreur'); - + if (isNotFound) { // Affichage simplifié pour les individus non trouvés const [field, values] = diffEntries[0]; @@ -328,7 +472,7 @@

    html += ``; html += `

    `; } - + diffEntries.forEach(([field, values], fieldIndex) => { const fieldId = `${type}-${index}-field-${fieldIndex}`; html += `
    `; @@ -347,7 +491,7 @@

    }); } } - + html += `

  • `; return html; } @@ -385,25 +529,50 @@

    const hasDifferences = repDiffs.length > 0 || enfDiffs.length > 0; let html = '

    Résultat de la synchronisation
    '; - + + // Afficher le message d'information si plusieurs personnes trouvées + if (data.differences.info_recherche) { + const info = data.differences.info_recherche; + const alertClass = info.type === 'warning' ? 'alert-warning' : 'alert-info'; + html += `
    `; + html += ` ${info.message}`; + html += `
    `; + } + if (!hasDifferences) { html += '
    Aucune différence trouvée - Données synchronisées
    '; } else { html += '
      '; - - repDiffs.forEach((rep, index) => { - html += createDiffHTML(rep, 'representant', familleId, index); + + // Regrouper les représentants par ID + const groupedReps = groupPersonsByIndividuId(repDiffs); + let repIndex = 0; + Object.values(groupedReps).forEach(persons => { + html += createGroupedDiffHTML(persons, 'representant', familleId, repIndex++); }); - - enfDiffs.forEach((enf, index) => { - html += createDiffHTML(enf, 'enfant', familleId, index); + + // Regrouper les enfants par ID + const groupedEnfs = groupPersonsByIndividuId(enfDiffs); + let enfIndex = 0; + Object.values(groupedEnfs).forEach(persons => { + html += createGroupedDiffHTML(persons, 'enfant', familleId, enfIndex++); }); - + html += '
    '; - - const totalFields = repDiffs.reduce((acc, r) => acc + Object.keys(r.differences || {}).length, 0) + - enfDiffs.reduce((acc, e) => acc + Object.keys(e.differences || {}).length, 0); - + + // Compter uniquement les champs de la première personne de chaque groupe (celles visibles par défaut) + let totalFields = 0; + Object.values(groupedReps).forEach(persons => { + if (persons.length > 0) { + totalFields += Object.keys(persons[0].differences || {}).length; + } + }); + Object.values(groupedEnfs).forEach(persons => { + if (persons.length > 0) { + totalFields += Object.keys(persons[0].differences || {}).length; + } + }); + html += '
    '; html += `
    Modifications sélectionnées : ${totalFields}
    `; html += '
    '; @@ -418,7 +587,36 @@

    resultDiv.querySelectorAll('.field-checkbox').forEach(checkbox => { checkbox.addEventListener('change', () => updateSelectedCount(familleId)); }); - + + // Event listeners pour les radio buttons de sélection de personne + resultDiv.querySelectorAll('.person-selector').forEach(radio => { + radio.addEventListener('change', function() { + const personId = this.dataset.personId; + const selectedIndex = parseInt(this.value); + + // Trouver le diff-item parent + const diffItem = this.closest('.diff-item'); + if (!diffItem) return; + + // Mettre à jour l'attribut data-index-personne + diffItem.setAttribute('data-index-personne', selectedIndex); + + // Masquer tous les containers de champs pour cette personne + const allContainers = diffItem.querySelectorAll('.fields-container'); + allContainers.forEach(container => { + const containerIndex = parseInt(container.dataset.indexPersonne); + if (containerIndex === selectedIndex) { + container.style.display = 'block'; + } else { + container.style.display = 'none'; + } + }); + + // Mettre à jour le compteur + updateSelectedCount(familleId); + }); + }); + // Event listeners pour "Tout sélectionner" resultDiv.querySelectorAll('.select-all-person').forEach(selectAll => { selectAll.addEventListener('change', function() { @@ -435,15 +633,25 @@

    validateBtn.addEventListener('click', function() { const selectedFields = {}; resultDiv.querySelectorAll('.field-checkbox:checked').forEach(checkbox => { + // Vérifier que le checkbox est dans un container visible + const fieldsContainer = checkbox.closest('.fields-container'); + if (fieldsContainer && fieldsContainer.style.display === 'none') { + return; // Ignorer les checkboxes dans les containers masqués + } + const personId = checkbox.dataset.personId; const personType = checkbox.dataset.personType; const field = checkbox.dataset.field; - - const key = `${personType}_${personId}`; + + // Récupérer l'index_personne depuis le container visible + const indexPersonne = fieldsContainer ? parseInt(fieldsContainer.dataset.indexPersonne || 0) : 0; + + const key = `${personType}_${personId}_${indexPersonne}`; if (!selectedFields[key]) { selectedFields[key] = { id: personId, type: personType, + index_personne: indexPersonne, fields: [] }; } diff --git a/noethysweb/fiche_famille/views/famille_ent.py b/noethysweb/fiche_famille/views/famille_ent.py index 750d5716..212680d9 100644 --- a/noethysweb/fiche_famille/views/famille_ent.py +++ b/noethysweb/fiche_famille/views/famille_ent.py @@ -10,6 +10,7 @@ from django.contrib import messages from django.http import JsonResponse from core.utils.utils_ent import get_ent_user_info +from core.data import data_civilites import time import json @@ -320,21 +321,32 @@ def valider_synchronisation(self, data): 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 - ent_result = get_ent_user_info(individu.nom, individu.prenom) - - if not ent_result: + + # 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 - - individu_api = ent_result[0] if isinstance(ent_result, list) else ent_result + + # 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: @@ -436,69 +448,119 @@ def appliquer_modification(self, individu, individu_api, field_label): def convertir_civilite(self, valeur): """ Convertit la civilité de l'API en valeur compatible avec la base de données - Adapter selon votre modèle (CharField, IntegerField avec choices, FK, etc.) + Utilise le dictionnaire data_civilites pour la conversion """ - # Si c'est un CharField, retourner tel quel - # Vérifier le type de champ dans votre modèle Individu from django.db.models import CharField, IntegerField - + field = Individu._meta.get_field('civilite') - + if isinstance(field, IntegerField): - # Si c'est un IntegerField avec choices (1=M, 2=Mme, etc.) - mapping_civilite = { - 'M.': 1, + # 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, - 'Monsieur': 1, - 'Mme': 2, - 'Madame': 2, - 'Mlle': 3, - 'Mademoiselle': 3, + 'M.': 1, + 'Mr': 1, + 'Melle': 2, # Correspond à Mademoiselle (id 2) + 'Mlle': 2, } - return mapping_civilite.get(valeur, 1) # Par défaut M. - + 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 - # À adapter selon votre structure return valeur def comparer_famille(self, famille): """ - Compare les données de chaque membre de la famille avec l'API + 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': [] + '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: - ent_result = get_ent_user_info(individu.nom, individu.prenom) - - if ent_result: - individu_api = ent_result[0] if isinstance(ent_result, list) else ent_result - diff = self.comparer_individu(individu, individu_api) - - if diff: - diff_entry = { - 'id': individu.idindividu, - 'nom': f"{individu.prenom} {individu.nom}", - 'differences': diff + # 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' } - - if ratt.categorie in [1, 3]: - differences['representants'].append(diff_entry) - else: - differences['enfants'].append(diff_entry) + + # 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}", @@ -507,15 +569,17 @@ def comparer_famille(self, famille): '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}", @@ -524,14 +588,15 @@ def comparer_famille(self, famille): '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): @@ -559,18 +624,19 @@ def comparer_individu(self, individu_db, individu_api): 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) - - valeur_db_str = self.normaliser_valeur(valeur_db) - valeur_api_str = self.normaliser_valeur(valeur_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): @@ -589,21 +655,52 @@ def extraire_valeur_api(self, individu_api, champ): else: return individu_api.get(champ) if isinstance(individu_api, dict) else None - def normaliser_valeur(self, valeur): + def normaliser_valeur(self, valeur, champ_db=None): """ - Normalise une valeur pour la comparaison + 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() - - if valeur_str and any(char.isdigit() for char in valeur_str): + + # 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): diff --git a/noethysweb/fiche_individu/views/individu_ent.py b/noethysweb/fiche_individu/views/individu_ent.py index aa30fd7a..5d90067e 100644 --- a/noethysweb/fiche_individu/views/individu_ent.py +++ b/noethysweb/fiche_individu/views/individu_ent.py @@ -232,7 +232,6 @@ def get_context_data(self, **kwargs): # Préparer les données des individus individus_data = [] for individu in individus[:200]: # Limiter à 200 pour la performance - # TODO: Récupérer l'école de l'individu selon votre modèle # Non utilisé pour l'instant individus_data.append({ @@ -271,7 +270,6 @@ def post(self, request, *args, **kwargs): # Convertir en integers selected_ids = [int(id) for id in selected_ids] - # TODO: Implémenter la logique de synchronisation en masse # Cette fonction sera développée ultérieurement success_count = self.synchroniser_individus_masse(selected_ids) @@ -290,9 +288,7 @@ def post(self, request, *args, **kwargs): return redirect(request.path) def synchroniser_individus_masse(self, individu_ids): - """ - TODO: Fonction à développer pour la synchronisation en masse - + """ Args: individu_ids: Liste des IDs des individus à synchroniser From 0ad4f06e005d45576edaa81fd2eb949b2ad762cb Mon Sep 17 00:00:00 2001 From: mustaphaBersellou Date: Mon, 27 Oct 2025 14:43:58 +0100 Subject: [PATCH 05/12] mass synchronisation and many families per user --- noethysweb/fiche_famille/views/famille_ent.py | 2 +- .../fiche_individu/individu_liste_maj.html | 93 +++++--- .../fiche_individu/views/individu_ent.py | 205 +++++++++++++++--- 3 files changed, 238 insertions(+), 62 deletions(-) diff --git a/noethysweb/fiche_famille/views/famille_ent.py b/noethysweb/fiche_famille/views/famille_ent.py index 212680d9..738f933c 100644 --- a/noethysweb/fiche_famille/views/famille_ent.py +++ b/noethysweb/fiche_famille/views/famille_ent.py @@ -675,7 +675,7 @@ def normaliser_valeur(self, valeur, champ_db=None): return str(valeur) # Si c'est déjà une chaîne (valeur API) - if isinstance(valeur, str): + if isinstance(valeur, str): # Normaliser les variations courantes valeur_norm = valeur.strip() mapping_api_to_abrege = { diff --git a/noethysweb/fiche_individu/templates/fiche_individu/individu_liste_maj.html b/noethysweb/fiche_individu/templates/fiche_individu/individu_liste_maj.html index 8672fbed..75efd29c 100644 --- a/noethysweb/fiche_individu/templates/fiche_individu/individu_liste_maj.html +++ b/noethysweb/fiche_individu/templates/fiche_individu/individu_liste_maj.html @@ -25,25 +25,38 @@

    -
    -
    - - -
    - -
    - - - Réinitialiser - + +
    +
    + + +
    + +
    + + +
    + +
    +
    + + + Réinitialiser + +
    +
    @@ -102,9 +115,11 @@
    Civilité Nom Prénom + Type + École + Classe Email Téléphone - Actions @@ -112,9 +127,9 @@
    {% for individu in individus %} - @@ -123,19 +138,45 @@
    {{ 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 }} - - {% endfor %} {% else %} - +

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

    diff --git a/noethysweb/fiche_individu/views/individu_ent.py b/noethysweb/fiche_individu/views/individu_ent.py index 5d90067e..4435acff 100644 --- a/noethysweb/fiche_individu/views/individu_ent.py +++ b/noethysweb/fiche_individu/views/individu_ent.py @@ -6,6 +6,10 @@ 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 +import logging + +logger = logging.getLogger(__name__) class UpdateIndividu(Onglet, TemplateView): menu_code = "individu_synchroniser" @@ -209,52 +213,93 @@ class SynchronisationMasseIndividus(CustomView, TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - - # Récupérer uniquement le filtre de recherche textuelle + + # 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='').order_by('nom', 'prenom') - + 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) | - # Q(mail__icontains=search_query) - # ) - + 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 - # Non utilisé pour l'instant - + # 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() - print(individus.count()) context['total_all_individus'] = Individu.objects.count() - print(Individu.objects.count()) - # context['search_query'] = search_query - # print(search_query) + 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." - + return context def post(self, request, *args, **kwargs): @@ -288,22 +333,112 @@ def post(self, request, *args, **kwargs): return redirect(request.path) 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 """ - # Cette fonction sera implémentée plus tard - # Pour l'instant, elle ne fait rien - - # Logique à implémenter: - # 1. Pour chaque individu_id: - # - Récupérer l'individu - # - Appeler get_ent_user_info(individu.ent_id) - # - Mettre à jour les champs si données disponibles - # - Gérer les erreurs individuellement - # 2. Retourner le nombre de synchronisations réussies - - return len(individu_ids) # Placeholder \ No newline at end of file + success_count = 0 + errors = [] + + # 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 + from core.utils.utils_ent import get_ent_user_info_by_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 From e6671a6ac8547887716a474e3cd27fb9cdce4583 Mon Sep 17 00:00:00 2001 From: mustaphaBersellou Date: Wed, 29 Oct 2025 11:37:43 +0100 Subject: [PATCH 06/12] =?UTF-8?q?Display=20a=20loading=20message=20during?= =?UTF-8?q?=20synchronization=20and=20prevent=20user=20actions=20until=20i?= =?UTF-8?q?t=E2=80=99s=20complete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- noethysweb/core/models.py | 21 ++++ .../fiche_individu/individu_liste_maj.html | 16 +++ .../fiche_individu/views/individu_ent.py | 110 +++++++++++++++--- 3 files changed, 129 insertions(+), 18 deletions(-) diff --git a/noethysweb/core/models.py b/noethysweb/core/models.py index e2af17e3..902e6b70 100644 --- a/noethysweb/core/models.py +++ b/noethysweb/core/models.py @@ -4797,3 +4797,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/fiche_individu/templates/fiche_individu/individu_liste_maj.html b/noethysweb/fiche_individu/templates/fiche_individu/individu_liste_maj.html index 75efd29c..fe8ea2e3 100644 --- a/noethysweb/fiche_individu/templates/fiche_individu/individu_liste_maj.html +++ b/noethysweb/fiche_individu/templates/fiche_individu/individu_liste_maj.html @@ -17,6 +17,22 @@

    {{ 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 %} +
    diff --git a/noethysweb/fiche_individu/views/individu_ent.py b/noethysweb/fiche_individu/views/individu_ent.py index 4435acff..f0db785f 100644 --- a/noethysweb/fiche_individu/views/individu_ent.py +++ b/noethysweb/fiche_individu/views/individu_ent.py @@ -1,4 +1,4 @@ -from core.models import Scolarite, Individu, Classe, NiveauScolaire, Ecole +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 @@ -7,10 +7,15 @@ 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" @@ -300,6 +305,20 @@ def get_context_data(self, **kwargs): 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): @@ -307,30 +326,85 @@ def post(self, request, *args, **kwargs): 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] - - # Cette fonction sera développée ultérieurement - success_count = self.synchroniser_individus_masse(selected_ids) - - if success_count > 0: - messages.success( - request, - f"Synchronisation lancée avec succès pour {success_count} individu(s)." + + # 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} ) - else: - messages.info(request, "Aucune synchronisation effectuée.") - + + 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 de la synchronisation : {str(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): """ @@ -344,7 +418,8 @@ def synchroniser_individus_masse(self, individu_ids): """ success_count = 0 errors = [] - + import time + time.sleep(20) # Mapping des champs à synchroniser champs_mapping = { 'nom': 'nom', @@ -369,7 +444,6 @@ def synchroniser_individus_masse(self, individu_ids): continue # Récupérer les données depuis l'ENT par ent_id - from core.utils.utils_ent import get_ent_user_info_by_ent_id donnees_ent = get_ent_user_info_by_ent_id(individu.ent_id) if not donnees_ent: From 954490ab25a610ec13634d4d70fff6abfa83e5f7 Mon Sep 17 00:00:00 2001 From: mustaphaBersellou Date: Wed, 12 Nov 2025 11:19:01 +0100 Subject: [PATCH 07/12] fix form for adding individu --- noethysweb/fiche_individu/forms/individu.py | 53 ++++++++++++++++++--- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/noethysweb/fiche_individu/forms/individu.py b/noethysweb/fiche_individu/forms/individu.py index 2e388bcd..ee075e3e 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,34 @@ 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) + + # 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 +177,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 = """ From fb79ee3d9f5eb6a9b590175b20c981381e808dcd Mon Sep 17 00:00:00 2001 From: mustaphaBersellou Date: Fri, 14 Nov 2025 09:53:13 +0100 Subject: [PATCH 08/12] add page to display kid family --- noethysweb/core/utils/utils_ent.py | 344 +++++++++++++++--- .../fiche_famille/famille_enfant_ent.html | 238 ++++++++++++ .../fiche_famille/famille_ent_liste.html | 116 ++++-- noethysweb/fiche_famille/urls.py | 2 + noethysweb/fiche_famille/views/famille.py | 2 +- noethysweb/fiche_famille/views/famille_ent.py | 145 +++++++- noethysweb/fiche_individu/forms/individu.py | 3 +- 7 files changed, 763 insertions(+), 87 deletions(-) create mode 100644 noethysweb/fiche_famille/templates/fiche_famille/famille_enfant_ent.html diff --git a/noethysweb/core/utils/utils_ent.py b/noethysweb/core/utils/utils_ent.py index f308b9de..e6b90d9d 100644 --- a/noethysweb/core/utils/utils_ent.py +++ b/noethysweb/core/utils/utils_ent.py @@ -14,32 +14,20 @@ def get_ent_users(nom, prenom): try: exemple_users = [ { - "famille_id": 1, - "nom_famille": "Famille DUPONT", - "representants": [ - { - "id_ent": "user179", - "civilite": "Monsieur", - "prenom": "Test10", - "nom": "Test10", - "email": "jean.dupont@mail.fr", - "telephone": "0601020304", - }, - { - "id_ent": "user124", - "civilite": "Madame", - "prenom": "Marie", - "nom": "DUPONT", - "email": "marie.dupont@mail.fr", - "telephone": "0605060708", - }, - ], + "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", + "nom": "Dupont", "scolarite": { "date_debut": "2024-09-01", "date_fin": "2025-06-30", @@ -53,7 +41,7 @@ def get_ent_users(nom, prenom): "fax": "0140203041", "mail": "contact@victorhugo.fr", "uai": "0751234A", - }, + }, "classe": { "id": 101, "nom": "6ème A", @@ -68,28 +56,58 @@ def get_ent_users(nom, prenom): "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", + }, + }, + }, ], }, { - "famille_id": 2, - "nom_famille": "Famille MARTIN", - "representants": [ - { - "id_ent": "user2087", - "civilite": "Monsieur", - "prenom": "Test10", - "nom": "Test10", - "email": "Testent.Testent@mail.fr", - "telephone": "0610101010", - } - ], + "representant": { + "id_ent": "user2087", + "civilite": "Monsieur", + "prenom": "Test10", + "nom": "Test10", + "email": "paul.martin@mail.fr", + "telephone": "0610101010", + }, "enfants": [ { - "id_ent": "user2087", + "id_ent": "user3001", "civilite": "Madame", - "prenom": "TestEnfant10", - "nom": "TestEnfant10", + "prenom": "Clara", + "nom": "Martin", "scolarite": { "date_debut": "2024-09-01", "date_fin": "2025-06-30", @@ -118,7 +136,41 @@ def get_ent_users(nom, prenom): "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", + }, + }, + }, ], }, ] @@ -127,16 +179,17 @@ def get_ent_users(nom, prenom): organisateur = Organisateur.objects.filter(pk=1).first() if organisateur.ent_active: for famille in exemple_users: - match_reps = [ - rep for rep in famille["representants"] - if rep["nom"].lower() == nom.lower() and rep["prenom"].lower() == prenom.lower() - ] + 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_reps or match_enfants: + if match_rep or match_enfants: # On garde toute la famille, même si un seul correspond result.append(famille) @@ -580,6 +633,209 @@ def get_collaborateur_by_ent_id(ent_id): 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 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 index 85d57824..77fcb46a 100644 --- a/noethysweb/fiche_famille/templates/fiche_famille/famille_ent_liste.html +++ b/noethysweb/fiche_famille/templates/fiche_famille/famille_ent_liste.html @@ -94,8 +94,31 @@ .enfant { border-left: 4px solid #4299e1; + cursor: pointer; + position: relative; } - + + .enfant:hover { + background: #f0f4ff; + border-color: #3182ce; + } + + .enfant::after { + content: "Cliquer pour voir la famille"; + position: absolute; + right: 16px; + top: 50%; + transform: translateY(-50%); + font-size: 12px; + color: #4299e1; + opacity: 0; + transition: opacity 0.2s; + } + + .enfant:hover::after { + opacity: 1; + } + .info-line { font-size: 14px; margin: 4px 0; @@ -169,52 +192,46 @@ {% for famille in rattachements %}
    - {{ famille.nom_famille }} + {{ famille.representant.civilite }} {{ famille.representant.prenom }} {{ famille.representant.nom }}
    {% csrf_token %} - -
    - - -
    - Représentants -
    - {% for rep in famille.representants %} -
    -
    - {{ rep.civilite }} {{ rep.prenom }} {{ rep.nom }} -
    -
    - 📧 {{ rep.email }} -
    -
    - 📞 {{ rep.telephone }} -
    + +
    +
    + 📧 Email: {{ famille.representant.email|default:"Non renseigné" }}
    - {% empty %} -
    Aucun représentant
    - {% endfor %} +
    + 📞 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.ecole.nom }} + 🏫 {{ enfant.scolarite.ecole.nom }}
    + {% endif %} + {% if enfant.scolarite.classe %}
    - 📘 Classe: {{ enfant.classe.nom }} + 📘 Classe: {{ enfant.scolarite.classe.nom }}
    + {% endif %}
    {% empty %}
    Aucun enfant
    @@ -228,4 +245,47 @@

    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/urls.py b/noethysweb/fiche_famille/urls.py index 7f67cbd7..e6d70847 100644 --- a/noethysweb/fiche_famille/urls.py +++ b/noethysweb/fiche_famille/urls.py @@ -27,6 +27,8 @@ 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.py b/noethysweb/fiche_famille/views/famille.py index 60caa14e..c5525737 100644 --- a/noethysweb/fiche_famille/views/famille.py +++ b/noethysweb/fiche_famille/views/famille.py @@ -104,7 +104,7 @@ class Liste(Page, crud.Liste): model = Famille def get_queryset(self): - return Famille.objects.filter(self.Get_filtres("Q")) + return Famille.objects.filter(self.Get_filtres("Q")).exclude(nom="Famille effacée") def get_context_data(self, **kwargs): context = super(Liste, self).get_context_data(**kwargs) diff --git a/noethysweb/fiche_famille/views/famille_ent.py b/noethysweb/fiche_famille/views/famille_ent.py index 738f933c..db97759b 100644 --- a/noethysweb/fiche_famille/views/famille_ent.py +++ b/noethysweb/fiche_famille/views/famille_ent.py @@ -9,7 +9,7 @@ import logging from django.contrib import messages from django.http import JsonResponse -from core.utils.utils_ent import get_ent_user_info +from core.utils.utils_ent import get_ent_user_info, get_enfant_famille from core.data import data_civilites import time import json @@ -39,7 +39,6 @@ def get_context_data(self, **kwargs): context["idfamille"] = int(idfamille) else: context["mode"] = "familles" - return context @transaction.atomic @@ -47,18 +46,57 @@ 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'un ajout de toute une famille ou juste un individu à une famille + # 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": - # L’action est utilisée pour savoir si l’on veut simplement ajouter un nouvel individu indépendamment des données de l’ENT + # 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) 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: @@ -69,12 +107,12 @@ def post(self, request, *args, **kwargs): 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. + # 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: + else: new_famille = self.creation_famille() for indiv in famille["representants"]: self.creation_individu_ent(indiv, new_famille, 1) @@ -90,12 +128,11 @@ def post(self, request, *args, **kwargs): 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 - for rep in famille.get("representants", []): - if str(rep.get("id_ent")) == str(individu_id): - return rep + 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", []): @@ -723,4 +760,86 @@ def traduire_nom_champ(self, champ_db): 'ent_id': 'ID ENT', } - return traductions.get(champ_db, champ_db) \ No newline at end of file + 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 ee075e3e..396d5266 100644 --- a/noethysweb/fiche_individu/forms/individu.py +++ b/noethysweb/fiche_individu/forms/individu.py @@ -142,7 +142,8 @@ def clean(self): # 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 From bf32528f3da4dd7385fd1d10a5f1823afb46943b Mon Sep 17 00:00:00 2001 From: mustaphaBersellou Date: Fri, 14 Nov 2025 14:41:12 +0100 Subject: [PATCH 09/12] not adding the migration files --- .../migrations/0191_auto_20250903_1421.py | 155 ------------------ .../core/migrations/0192_individu_ent_id.py | 19 --- .../migrations/0193_collaborateur_ent_id.py | 18 -- 3 files changed, 192 deletions(-) delete mode 100644 noethysweb/core/migrations/0191_auto_20250903_1421.py delete mode 100644 noethysweb/core/migrations/0192_individu_ent_id.py delete mode 100644 noethysweb/core/migrations/0193_collaborateur_ent_id.py diff --git a/noethysweb/core/migrations/0191_auto_20250903_1421.py b/noethysweb/core/migrations/0191_auto_20250903_1421.py deleted file mode 100644 index e46b9c8d..00000000 --- a/noethysweb/core/migrations/0191_auto_20250903_1421.py +++ /dev/null @@ -1,155 +0,0 @@ -# Generated by Django 3.2.25 on 2025-09-03 14:21 - -import core.models -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import django_cryptography.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0190_auto_20250517_1944'), - ] - - operations = [ - migrations.CreateModel( - name='Utilisateur_Individu', - fields=[ - ], - options={ - 'verbose_name': 'Individu', - 'proxy': True, - 'indexes': [], - 'constraints': [], - }, - bases=('core.utilisateur',), - managers=[ - ('objects', core.models.CustomUserManager()), - ], - ), - migrations.AlterModelOptions( - name='utilisateur', - options={'permissions': [('famille_resume', 'Fiche famille | Résumé'), ('famille_questionnaire', 'Fiche famille | Questionnaire'), ('famille_questionnaire_modifier', 'Fiche famille | Questionnaire | Modifier'), ('famille_pieces', 'Fiche famille | Pièces'), ('famille_pieces_modifier', 'Fiche famille | Pièces | Modifier'), ('famille_locations', 'Fiche famille | Locations'), ('famille_locations_modifier', 'Fiche famille | Locations | Modifier'), ('famille_cotisations', 'Fiche famille | Adhésions'), ('famille_cotisations_modifier', 'Fiche famille | Adhésions | Modifier'), ('famille_caisse', 'Fiche famille | Caisse'), ('famille_caisse_modifier', 'Fiche famille | Caisse | Modifier'), ('famille_aides', 'Fiche famille | Aides'), ('famille_aides_modifier', 'Fiche famille | Aides | Modifier'), ('famille_quotients', 'Fiche famille | Quotients familiaux'), ('famille_quotients_modifier', 'Fiche famille | Quotients familiaux | Modifier'), ('famille_prestations', 'Fiche famille | Prestations'), ('famille_prestations_modifier', 'Fiche famille | Prestations | Modifier'), ('famille_factures', 'Fiche famille | Factures'), ('famille_factures_modifier', 'Fiche famille | Factures | Modifier'), ('famille_reglements', 'Fiche famille | Règlements'), ('famille_reglements_modifier', 'Fiche famille | Règlements | Modifier'), ('famille_messagerie', 'Fiche famille | Messagerie'), ('famille_messagerie_modifier', 'Fiche famille | Messagerie | Modifier'), ('famille_portail', 'Fiche famille | Portail'), ('famille_portail_modifier', 'Fiche famille | Portail | Modifier'), ('famille_divers', 'Fiche famille | Paramètres'), ('famille_divers_modifier', 'Fiche famille | Paramètres | Modifier'), ('famille_outils', 'Fiche famille | Outils'), ('famille_consommations', 'Fiche famille | Consommations'), ('famille_consommations_modifier', 'Fiche famille | Consommations | Modifier'), ('individu_resume', 'Fiche individuelle | Résumé'), ('individu_identite', 'Fiche individuelle | Identité'), ('individu_identite_modifier', 'Fiche individuelle | Identité | Modifier'), ('individu_questionnaire', 'Fiche individuelle | Questionnaire'), ('individu_questionnaire_modifier', 'Fiche individuelle | Questionnaire | Modifier'), ('individu_liens', 'Fiche individuelle | Liens'), ('individu_liens_modifier', 'Fiche individuelle | Liens | Modifier'), ('individu_coords', 'Fiche individuelle | Coordonnées'), ('individu_coords_modifier', 'Fiche individuelle | Coordonnées | Modifier'), ('individu_scolarite', 'Fiche individuelle | Scolarité'), ('individu_scolarite_modifier', 'Fiche individuelle | Scolarité | Modifier'), ('individu_inscriptions', 'Fiche individuelle | Inscriptions'), ('individu_inscriptions_modifier', 'Fiche individuelle | Inscriptions | Modifier'), ('individu_regimes_alimentaires', 'Fiche individuelle | Régimes alimentaires'), ('individu_regimes_alimentaires_modifier', 'Fiche individuelle | Régimes alimentaires | Modifier'), ('individu_maladies', 'Fiche individuelle | Maladies'), ('individu_maladies_modifier', 'Fiche individuelle | Maladies | Modifier'), ('individu_medical', 'Fiche individuelle | Médical'), ('individu_medical_modifier', 'Fiche individuelle | Médical | Modifier'), ('individu_assurances', 'Fiche individuelle | Assurances'), ('individu_assurances_modifier', 'Fiche individuelle | Assurances | Modifier'), ('individu_portail', 'Fiche individuelle | Portail'), ('individu_portail_modifier', 'Fiche individuelle | Portail | Modifier'), ('individu_contacts', 'Fiche individuelle | Contacts'), ('individu_contacts_modifier', 'Fiche individuelle | Contacts | Modifier'), ('individu_transports', 'Fiche individuelle | Transports'), ('individu_transports_modifier', 'Fiche individuelle | Transports | Modifier'), ('individu_consommations', 'Fiche individuelle | Consommations')]}, - ), - migrations.AddField( - model_name='consentement', - name='individu', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.individu', verbose_name='Individu'), - ), - migrations.AddField( - model_name='destinataire', - name='activites', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.activite', verbose_name='Activites'), - ), - migrations.AddField( - model_name='destinataire', - name='inscription', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.inscription', verbose_name='Inscription'), - ), - migrations.AddField( - model_name='famille', - name='contact_facturation', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='contact_facturation', to='core.individu', verbose_name='Contact facturation'), - ), - migrations.AddField( - model_name='individu', - name='blocage_impayes_off', - field=models.BooleanField(default=False, help_text="En cochant cette case, vous permettez à cette famille d'accéder aux réservations du portail même s'il y a des impayés et que le paramètre 'blocage si impayés' a été activé dans les paramètres généraux du portail.", verbose_name='Ne jamais appliquer le blocage des réservations si impayés'), - ), - migrations.AddField( - model_name='individu', - name='certification_date', - field=models.DateTimeField(blank=True, null=True, verbose_name='Date de certification'), - ), - migrations.AddField( - model_name='individu', - name='internet_actif', - field=models.BooleanField(default=True, verbose_name='Compte internet activé'), - ), - migrations.AddField( - model_name='individu', - name='internet_categorie', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='internet_categori', to='core.categoriecompteinternet', verbose_name='Catégorie'), - ), - migrations.AddField( - model_name='individu', - name='internet_identifiant', - field=django_cryptography.fields.encrypt(models.CharField(blank=True, max_length=200, null=True, verbose_name='Identifiant')), - ), - migrations.AddField( - model_name='individu', - name='internet_mdp', - field=django_cryptography.fields.encrypt(models.CharField(blank=True, max_length=200, null=True, verbose_name='Mot de passe')), - ), - migrations.AddField( - model_name='individu', - name='internet_reservations', - field=models.BooleanField(default=True, verbose_name='Autoriser les réservations sur le portail'), - ), - migrations.AddField( - model_name='individu', - name='internet_secquest', - field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Question'), - ), - migrations.AddField( - model_name='individu', - name='mobile', - field=django_cryptography.fields.encrypt(models.CharField(blank=True, max_length=100, null=True, verbose_name='Portable favori')), - ), - migrations.AddField( - model_name='individu', - name='utilisateur', - field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='portailmessage', - name='individu', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='core.individu', verbose_name='Individu'), - ), - migrations.AlterField( - model_name='adressemail', - name='adresse', - field=models.EmailField(help_text="Saisissez l'adresse mail utilisée.", max_length=300, verbose_name="Adresse d'envoi"), - ), - migrations.AlterField( - model_name='portailmessage', - name='date_creation', - field=models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Date de création'), - ), - migrations.AlterField( - model_name='portailmessage', - name='date_lecture', - field=models.DateTimeField(blank=True, db_index=True, max_length=200, null=True, verbose_name='Date de lecture'), - ), - migrations.AlterField( - model_name='portailparametre', - name='code', - field=models.CharField(blank=True, max_length=200, null=True, unique=True, verbose_name='Code'), - ), - migrations.AddIndex( - model_name='portailmessage', - index=models.Index(fields=['famille'], name='portail_mes_famille_fd123f_idx'), - ), - migrations.AddIndex( - model_name='portailmessage', - index=models.Index(fields=['individu'], name='portail_mes_individ_e5b04e_idx'), - ), - migrations.AddIndex( - model_name='portailmessage', - index=models.Index(fields=['structure'], name='portail_mes_structu_e1e078_idx'), - ), - migrations.AddIndex( - model_name='portailmessage', - index=models.Index(fields=['utilisateur'], name='portail_mes_utilisa_16d110_idx'), - ), - migrations.AddIndex( - model_name='portailmessage', - index=models.Index(fields=['date_creation'], name='portail_mes_date_cr_58d8e7_idx'), - ), - migrations.AddIndex( - model_name='portailmessage', - index=models.Index(fields=['date_lecture'], name='portail_mes_date_le_d93ed6_idx'), - ), - ] diff --git a/noethysweb/core/migrations/0192_individu_ent_id.py b/noethysweb/core/migrations/0192_individu_ent_id.py deleted file mode 100644 index 92167e8b..00000000 --- a/noethysweb/core/migrations/0192_individu_ent_id.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.2.25 on 2025-09-08 10:27 - -from django.db import migrations, models -import django_cryptography.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0191_auto_20250903_1421'), - ] - - operations = [ - migrations.AddField( - model_name='individu', - name='ent_id', - field=django_cryptography.fields.encrypt(models.IntegerField(blank=True, null=True, verbose_name='ent_id')), - ), - ] diff --git a/noethysweb/core/migrations/0193_collaborateur_ent_id.py b/noethysweb/core/migrations/0193_collaborateur_ent_id.py deleted file mode 100644 index 106796ee..00000000 --- a/noethysweb/core/migrations/0193_collaborateur_ent_id.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.25 on 2025-09-22 10:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0192_auto_20250917_1231'), - ] - - operations = [ - migrations.AddField( - model_name='collaborateur', - name='ent_id', - field=models.CharField(max_length=200, null=True, verbose_name='ent_id'), - ), - ] From f9f219c64a9dfffe0cc12c92462c8ed5c168a392 Mon Sep 17 00:00:00 2001 From: mustaphaBersellou Date: Fri, 14 Nov 2025 16:43:28 +0100 Subject: [PATCH 10/12] Delete unnecessary files --- noethysweb/core/views/menu.py | 1 - .../individus/forms/importer_individus_ent.py | 66 -------- .../individus/importer_individus_ent.html | 130 -------------- noethysweb/individus/urls.py | 2 - .../individus/views/importer_individus_ent.py | 159 ------------------ noethysweb/portail/views/auto_login.py | 106 ------------ 6 files changed, 464 deletions(-) delete mode 100644 noethysweb/individus/forms/importer_individus_ent.py delete mode 100644 noethysweb/individus/templates/individus/importer_individus_ent.html delete mode 100644 noethysweb/individus/views/importer_individus_ent.py delete mode 100644 noethysweb/portail/views/auto_login.py diff --git a/noethysweb/core/views/menu.py b/noethysweb/core/views/menu.py index 4ec0b132..ef96eed0 100644 --- a/noethysweb/core/views/menu.py +++ b/noethysweb/core/views/menu.py @@ -254,7 +254,6 @@ 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="importer_individus_ent", titre="Importer des individus de l'ENT", 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") diff --git a/noethysweb/individus/forms/importer_individus_ent.py b/noethysweb/individus/forms/importer_individus_ent.py deleted file mode 100644 index c9a4408d..00000000 --- a/noethysweb/individus/forms/importer_individus_ent.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2019-2021 Ivan LUCAS. -# Noethysweb, application de gestion multi-activités. -# Distribué sous licence GNU GPL. - -from django import forms -from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, HTML, Fieldset, ButtonHolder -from crispy_forms.bootstrap import Field, StrictButton -from core.forms.base import FormulaireBase -from core.widgets import DatePickerWidget, Telephone - - -class Formulaire(FormulaireBase, forms.Form): - nom = forms.CharField(label="Nom de famille", required=False) - prenom = forms.CharField(label="Prénom", required=False) - date_naiss = forms.DateField(label="Date de Naissance", required=False, widget=DatePickerWidget()) - rue_resid = forms.CharField(label="Rue", required=False) - cp_resid = forms.CharField(label="Code postal", required=False) - ville_resid = forms.CharField(label="Ville", required=False) - tel_domicile = forms.CharField(label="Tél fixe", required=False) - tel_mobile = forms.CharField(label="Tél portable", required=False) - travail_tel = forms.CharField(label="Tél pro.", required=False) - mail = forms.CharField(label="Email", required=False) - - class Meta: - widgets = { - 'tel_domicile': Telephone(), - 'tel_mobile': Telephone(), - 'travail_tel': Telephone(), - } - - def __init__(self, *args, **kwargs): - super(Formulaire, self).__init__(*args, **kwargs) - self.helper = FormHelper() - self.helper.form_id = 'importer_individus_ent_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( - ButtonHolder( - StrictButton("Rechercher", title="Rechercher", name="rechercher", type="submit", css_class="btn-primary"), - HTML("""Annuler """), - css_class="mb-3", - ), - Fieldset("Etat-civil", - Field("nom"), - Field("prenom"), - Field("date_naiss"), - ), - Fieldset("Adresse", - Field("rue_resid"), - Field("cp_resid"), - Field("ville_resid"), - ), - Fieldset("Coordonnées", - Field("tel_domicile"), - Field("tel_mobile"), - Field("travail_tel"), - Field("mail"), - ), - ) diff --git a/noethysweb/individus/templates/individus/importer_individus_ent.html b/noethysweb/individus/templates/individus/importer_individus_ent.html deleted file mode 100644 index 7078dee4..00000000 --- a/noethysweb/individus/templates/individus/importer_individus_ent.html +++ /dev/null @@ -1,130 +0,0 @@ -{% extends "core/page.html" %} -{% load crispy_forms_tags %} -{% load static %} -{% load embed %} - - -{% block styles %} - {{ block.super }} - -{% endblock %} - - -{% block contenu_page %} - - - -{% if resultats_ent %} -
    -
    -

    Individus

    -
    -
    - - - - - - - - - - - - {% for item in resultats_ent %} - - - - - - - - {% endfor %} - -
    NomPrenomAdresseEmailActions
    {{ item.nom }}{{ item.prenom }}{{ item.adresse|default:"" }} {{ item.ville|default:"" }} {{ item.code_postal|default:"" }}{{ item.email|default:"" }} - -
    -
    -
    -{% endif %} - -{# Si recherche infructueuse #} -{% if not resultats_ent %} - Aucun résultat pour cette recherche. -{% endif %} - - - -{% endblock contenu_page %} diff --git a/noethysweb/individus/urls.py b/noethysweb/individus/urls.py index 6f6d2d1d..646b1d05 100644 --- a/noethysweb/individus/urls.py +++ b/noethysweb/individus/urls.py @@ -33,8 +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'), - path('individus/importer_individus_ent', importer_individus_ent.View.as_view(), name='importer_individus_ent'), - path('individus/ajouter_individu_ent', importer_individus_ent.ajouter_individu, name='ajouter_individu_ent'), # 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/individus/views/importer_individus_ent.py b/noethysweb/individus/views/importer_individus_ent.py deleted file mode 100644 index 0b2df7a9..00000000 --- a/noethysweb/individus/views/importer_individus_ent.py +++ /dev/null @@ -1,159 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2019-2021 Ivan LUCAS. -# Noethysweb, application de gestion multi-activités. -# Distribué sous licence GNU GPL. - -import logging, datetime -logger = logging.getLogger(__name__) -from django.shortcuts import render -from django.views.generic import TemplateView -from django.contrib import messages -from core.views.base import CustomView -from core.models import Utilisateur, Individu -from individus.forms.importer_individus_ent import Formulaire -from django.http import JsonResponse -from fiche_famille.utils import utils_internet - -def ajouter_individu(request): - if request.method == "POST": - try: - if Individu.objects.filter(ent_id=request.POST.get("entid")).exists(): - return JsonResponse({"success": False, "error": "Cet utilisateur existe déjà"}) - individu = Individu.objects.create( - ent_id=request.POST.get("entid"), - nom=request.POST.get("nom"), - prenom=request.POST.get("prenom"), - mail=request.POST.get("email"), - date_naiss=request.POST.get("date_naissance"), - rue_resid=request.POST.get("adresse"), - ville_resid=request.POST.get("ville"), - cp_resid=request.POST.get("codepostal"), - tel_mobile=request.POST.get("telephone_portable"), - tel_domicile=request.POST.get("telephone_domicile"), - tel_fax=request.POST.get("telephone_fixe"), - ) - 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() - return JsonResponse({"success": True, "id": individu.idindividu}) - except Exception as e: - return JsonResponse({"success": False, "error": str(e)}) - return JsonResponse({"success": False, "error": "Méthode non autorisée"}) - - -class View(CustomView, TemplateView): - menu_code = "importer_individus_ent" - template_name = "core/crud/edit.html" - - def get_context_data(self, **kwargs): - context = super(View, self).get_context_data(**kwargs) - context['page_titre'] = "Importer des individus de l'ENT" - context['box_titre'] = "Effectuer une recherche des individus de L'ENT" - context['box_introduction'] = "Saisissez un ou plusieurs critères de recherche et cliquez sur le bouton Rechercher." - context['form'] = context.get("form", Formulaire) - return context - - def post(self, request, **kwargs): - # Validation du form - form = Formulaire(request.POST, request.FILES, request=self.request) - if not form.is_valid(): - return self.render_to_response(self.get_context_data(form=form)) - - # Champs de recherche - champs_recherche = form.changed_data - # Recherche des résultats - import jellyfish - - resultats = {} - # rattachements = Rattachement.objects.select_related("individu", "famille").all() - # for rattachement in rattachements: - # score = resultats.get(rattachement, 0) - # - # for nom_champ in champs_recherche: - # valeur_recherche = form.cleaned_data[nom_champ] - # valeur_individu = getattr(rattachement.individu, nom_champ, "") - # - # # Recherche de texte - # if isinstance(valeur_individu, str): - # try: - # distance = jellyfish.jaro_distance(valeur_recherche.lower(), valeur_individu.lower()) - # except: - # distance = jellyfish.jaro_similarity(valeur_recherche.lower(), valeur_individu.lower()) - # score += distance - # - # if score >= 0.75: - # resultats[rattachement] = score - # - # # Recherche de date - # if isinstance(valeur_individu, datetime.date): - # if valeur_recherche == valeur_individu: - # resultats[rattachement] = score - # - # # Tri par score - # resultats = sorted([(score, rattachement) for rattachement, score in resultats.items()], key=lambda donnees: donnees[0], reverse=True) - resultats = [ - { - "ent_id": "550e8400-e29b-41d4-a716-446655440000", - "civilite": "M.", - "nom": "Dupont", - "nom_usage": "Durand", - "prenom": "Jean", - "date_naissance": "1985-07-12", - "adresse": "12 Rue de la République Bâtiment A Appartement 45", - "ville": "Paris", - "code_postal": "75001", - "telephone_fixe": "+33 1 44 55 66 77", - "telephone_domicile": "+33 1 40 22 33 44", - "telephone_portable": "+33 6 12 34 56 78", - "email": "jean.dupont@example.com" - }, - { - "ent_id": "660e8400-e29b-41d4-a716-446655440111", - "civilite": "Mme", - "nom": "Martin", - "nom_usage": "Leroy", - "prenom": "Sophie", - "date_naissance": "1990-03-25", - "adresse": "8 Avenue Victor Hugo", - "ville": "Lyon", - "code_postal": "69002", - "telephone_fixe": "+33 4 78 12 34 56", - "telephone_domicile": "+33 4 78 65 43 21", - "telephone_portable": "+33 6 98 76 54 32", - "email": "sophie.martin@example.com" - }, - { - "ent_id": "770e8400-e29b-41d4-a716-446655440222", - "civilite": "M.", - "nom": "Bernard", - "nom_usage": "", - "prenom": "Paul", - "date_naissance": "1978-11-02", - "adresse": "35 Boulevard Saint-Michel", - "ville": "Marseille", - "code_postal": "13006", - "telephone_fixe": "+33 4 91 23 45 67", - "telephone_domicile": "+33 4 91 11 22 33", - "telephone_portable": "+33 6 33 44 55 66", - "email": "paul.bernard@example.com" - } - ] - # Envoi des 50 premiers résultats - context = self.get_context_data(**kwargs) - context["resultats_ent"] = resultats[:50] # pas besoin de [r[1] for r in ...] - return render(request, "individus/importer_individus_ent.html", context) diff --git a/noethysweb/portail/views/auto_login.py b/noethysweb/portail/views/auto_login.py deleted file mode 100644 index b17d21a4..00000000 --- a/noethysweb/portail/views/auto_login.py +++ /dev/null @@ -1,106 +0,0 @@ -from django.shortcuts import render, redirect -from django.contrib.auth import login, get_user_model -from django.http import HttpResponseBadRequest -from django.views.decorators.csrf import csrf_exempt -from django.utils.decorators import method_decorator -from django.views import View -from django.conf import settings -import jwt -from datetime import datetime, timedelta -from django.urls import reverse_lazy - -User = get_user_model() - -class AutoLoginView(View): - """ - Vue pour l'authentification automatique depuis une autre application avec JWT - """ - - @method_decorator(csrf_exempt) - def dispatch(self, *args, **kwargs): - return super().dispatch(*args, **kwargs) - - def get(self, request): - """Traitement de l'authentification automatique avec JWT""" - - # 1. Récupérer le token JWT - token = request.GET.get("token") - if not token: - return redirect("/") - - try: - # 2. Décoder et vérifier le token - payload = jwt.decode(token, settings.SSO_SECRET_KEY, algorithms=["HS256"]) - - # 3. Extraire les données utilisateur - username = payload["username"] - email = payload["email"] - - # Données optionnelles - first_name = payload.get("first_name", "") - last_name = payload.get("last_name", "") - - except jwt.InvalidTokenError: - return redirect("/") - except KeyError: - # Si des champs obligatoires manquent dans le payload - return redirect("/") - - # 4. Chercher ou créer l'utilisateur - # try: - user = self._get_or_create_user( - username=username, - email=email, - first_name=first_name, - last_name=last_name, - ) - # 5. Connecter automatiquement l'utilisateur - login(request, user, backend='django.contrib.auth.backends.ModelBackend') - # 6. Redirection - return redirect(reverse_lazy("portail_accueil")) - - # except Exception as e: - # # Log l'erreur en production - # print(f"Erreur lors de la connexion automatique: {str(e)}") - # return redirect("/") - - def _get_or_create_user(self, username, email, first_name="", last_name=""): - """ - Récupérer un utilisateur existant ou en créer un nouveau - """ - try: - # Chercher d'abord par email (plus fiable) - print("///////") - print(username) - print("///////") - user = User.objects.get(username=username) - # Mettre à jour les informations si nécessaire - updated = False - if first_name and not user.first_name: - user.first_name = first_name - updated = True - if last_name and not user.last_name: - user.last_name = last_name - updated = True - if updated: - user.save() - - return user - - except User.DoesNotExist: - # Créer un nouvel utilisateur - - # S'assurer que le username est unique - original_username = username - counter = 1 - while User.objects.filter(username=username).exists(): - username = f"{original_username}_{counter}" - counter += 1 - - user = User.objects.create_user( - username=username, - email=email, - first_name=first_name, - last_name=last_name, - ) - return user \ No newline at end of file From 116f2353231d245d6c49dd84424b165b43684767 Mon Sep 17 00:00:00 2001 From: mustaphaBersellou Date: Fri, 14 Nov 2025 16:44:17 +0100 Subject: [PATCH 11/12] fix errors --- noethysweb/core/utils/utils_ent.py | 6 +++--- noethysweb/fiche_famille/views/famille_ent.py | 2 +- noethysweb/individus/urls.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/noethysweb/core/utils/utils_ent.py b/noethysweb/core/utils/utils_ent.py index e6b90d9d..ea030436 100644 --- a/noethysweb/core/utils/utils_ent.py +++ b/noethysweb/core/utils/utils_ent.py @@ -217,7 +217,7 @@ def get_ent_user_info(nom, prenom): "prenom": "Test10", "nom": "Test10", "email": "jean.dupont@mail.fr", - "telephone": "0601020304", + "telephone_mobile": "0601020304", }, { "ent_id": "user124", @@ -225,7 +225,7 @@ def get_ent_user_info(nom, prenom): "prenom": "Marie", "nom": "DUPONT", "email": "marie.dupont@mail.fr", - "telephone": "0605060708", + "telephone_mobile": "0605060708", }, # Enfant DUPONT { @@ -269,7 +269,7 @@ def get_ent_user_info(nom, prenom): "prenom": "Test10", "nom": "Test10", "email": "Testent.Testent@mail.fr", - "telephone": "0610101010", + "telephone_mobile": "0610101010", }, # Enfant MARTIN { diff --git a/noethysweb/fiche_famille/views/famille_ent.py b/noethysweb/fiche_famille/views/famille_ent.py index db97759b..39097186 100644 --- a/noethysweb/fiche_famille/views/famille_ent.py +++ b/noethysweb/fiche_famille/views/famille_ent.py @@ -54,10 +54,10 @@ def post(self, request, *args, **kwargs): # 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 diff --git a/noethysweb/individus/urls.py b/noethysweb/individus/urls.py index 646b1d05..2638ccae 100644 --- a/noethysweb/individus/urls.py +++ b/noethysweb/individus/urls.py @@ -14,7 +14,7 @@ edition_contacts, edition_renseignements, edition_informations, liste_photos_manquantes, recherche_avancee, inscriptions_modifier, \ liste_titulaires_helios, inscriptions_activite_liste, effacer_familles, liste_transports, liste_progtransports, inscriptions_changer_groupe, \ abonnes_listes_diffusion, abonnes_listes_diffusion_ajouter, liste_mails, imprimer_liste_inscrits, sondages_reponses, certifications, \ - certifications_individus, certifications_familles, inscriptions_saisir_lot, importer_individus, importer_individus_ent, importer_quotients + certifications_individus, certifications_familles, inscriptions_saisir_lot, importer_individus, importer_quotients urlpatterns = [ From da48a822564284c5e98e38f10fc34ee73cdf6df5 Mon Sep 17 00:00:00 2001 From: mustaphaBersellou Date: Fri, 14 Nov 2025 17:08:36 +0100 Subject: [PATCH 12/12] fix adding student family with ENT --- noethysweb/fiche_famille/views/famille_ent.py | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/noethysweb/fiche_famille/views/famille_ent.py b/noethysweb/fiche_famille/views/famille_ent.py index 39097186..3216a822 100644 --- a/noethysweb/fiche_famille/views/famille_ent.py +++ b/noethysweb/fiche_famille/views/famille_ent.py @@ -253,9 +253,20 @@ def creation_scolarite(self, new_indiv, scolarite): ecole= Ecole.objects.filter( uai=scolarite.get("ecole", {}).get("uai")).first() - # Création / récupération classe - classe= Classe.objects.get_or_create( - nom=scolarite.get("classe", {}).get("nom"),ecole=ecole).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() @@ -263,8 +274,8 @@ def creation_scolarite(self, new_indiv, scolarite): # Création scolarité scolarite_obj = Scolarite.objects.create( individu=new_indiv, - date_debut=scolarite.get("date_debut"), - date_fin=scolarite.get("date_fin"), + date_debut=date_debut, + date_fin=date_fin, ecole=ecole, classe=classe, niveau=niveau