From 151f5ce8c7ecfe46fbe8bbc7319b23ced655113c Mon Sep 17 00:00:00 2001 From: "faouzia.takaya" Date: Wed, 5 Mar 2025 11:21:03 +0100 Subject: [PATCH 01/25] Change : Configuration des serveurs --- noethysweb/noethysweb/settings.py | 25 ++++-- ...ction_modele.py => settings_production.py} | 82 +++++++++++-------- 2 files changed, 62 insertions(+), 45 deletions(-) rename noethysweb/noethysweb/{settings_production_modele.py => settings_production.py} (67%) diff --git a/noethysweb/noethysweb/settings.py b/noethysweb/noethysweb/settings.py index d4652e17..f284e78f 100644 --- a/noethysweb/noethysweb/settings.py +++ b/noethysweb/noethysweb/settings.py @@ -15,7 +15,7 @@ PORTAIL_ACTIF = True # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'cle_secrete_a_modifier_imperativement' +SECRET_KEY = 'gip_recia' SECRET_EXPORT_DESK = None # SECURITY WARNING: don't run with debug turned on in production! @@ -29,7 +29,7 @@ # LOGOUT_REDIRECT_URL = "connexion" # AXES -AXES_FAILURE_LIMIT = 5 +AXES_FAILURE_LIMIT = 24 AXES_COOLOFF_TIME = 24 AXES_LOCKOUT_URL = '/locked' @@ -51,14 +51,12 @@ AUTH_USER_MODEL = 'core.Utilisateur' DUREE_VALIDITE_MDP = 60*60*168 CORRECTEUR_JOURS_RETROACTION = 30 -PURGE_HISTORIQUE_JOURS = 365 -ATTRIBUTION_TARIF_FRATERIE_AINES = True -ATTRIBUTION_TARIF_FRATERIE_TARIF_IDENTIQUE = True +PURGE_HISTORIQUE_JOURS = None # CONFIGURATION ACCUEIL CONFIG_ACCUEIL_DEFAUT = [ [[6, "citation", "anniversaires"], [6, "celebrations", "anniversaires_demain"]], - [[8, "notes", "taches", "suivi_consommations", "suivi_inscriptions", "suivi_reservations"], [4, "graphe_individus", "messages", "astuce"]], + [[8, "notes", "taches", "suivi_consommations", "suivi_inscriptions"], [4, "graphe_individus", "messages", "astuce"]], ] # Application definition @@ -131,7 +129,8 @@ 'axes.middleware.AxesMiddleware', 'noethysweb.middleware.CustomMiddleware', ] - +# settings.py +SESSION_ENGINE = 'django.contrib.sessions.backends.db' ROOT_URLCONF = 'noethysweb.urls' TEMPLATES = [ @@ -161,8 +160,12 @@ # Database DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'noethysweb', + 'USER': 'noethysweb', + 'PASSWORD': 'noethysweb', + 'HOST': 'localhost', # ou l'adresse IP de votre serveur MariaDB + 'PORT': '3306', # le port par défaut de MariaDB } } @@ -206,6 +209,10 @@ STORAGE_PHOTO = "django.core.files.storage.FileSystemStorage" STORAGE_PIECE_COLLABORATEUR = "django.core.files.storage.FileSystemStorage" +# Media files +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +MEDIA_URL = '/media/' + # Crispy forms CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap4" CRISPY_TEMPLATE_PACK = 'bootstrap4' diff --git a/noethysweb/noethysweb/settings_production_modele.py b/noethysweb/noethysweb/settings_production.py similarity index 67% rename from noethysweb/noethysweb/settings_production_modele.py rename to noethysweb/noethysweb/settings_production.py index 530be183..aab8edde 100644 --- a/noethysweb/noethysweb/settings_production_modele.py +++ b/noethysweb/noethysweb/settings_production.py @@ -11,13 +11,17 @@ ######################################################################################### import os + BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +from noethysweb.noethysweb.settings import STATIC_ROOT +STATIC_ROOT = os.path.join(BASE_DIR,'staticfiles') + ######################################################################################### # CLE SECRETE : Saisissez une clé aléatoire complexe de votre choix (50 caract. minimum) ######################################################################################### -SECRET_KEY = 'cle_secrete_a_modifier_imperativement' +SECRET_KEY = 'gip_recia' ######################################################################################### # MODE DEMO : Pour désactiver des fonctionnalités @@ -48,7 +52,7 @@ # Exemple : ALLOWED_HOSTS = ["127.0.0.1", "www.monsite.com"] ######################################################################################### -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ["127.0.0.1", "localhost"] ######################################################################################### # BASE DE DONNEES : Modifier si besoin (en cas d'utilisation de MySQL par exemple) @@ -58,11 +62,14 @@ DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'noethysweb', + 'USER': 'noethysweb', + 'PASSWORD': 'noethysweb', + 'HOST': 'localhost', # ou l'adresse IP de votre serveur MariaDB + 'PORT': '3306', # le port par défaut de MariaDB } } - ######################################################################################### # SAUVEGARDES : Permet de générer et envoyer des sauvegardes chiffrées des données # vers un répertoire du serveur ou vers Dropbox. @@ -70,14 +77,14 @@ ######################################################################################### # ID de la clé GPG pour le chiffrement de la sauvegarde -# DBBACKUP_GPG_RECIPIENT = "XXXXXXXXX" +DBBACKUP_GPG_RECIPIENT = "D7FC7207637A035E76C58D26C77F465B04290FAA" # Pour un stockage de la sauvegarde sur le disque dur (Renseigner ci-dessous un répertoire existant) -# DBBACKUP_STORAGE = "django.core.files.storage.FileSystemStorage" -# DBBACKUP_STORAGE_OPTIONS = {"location": "C:/Users/XXXXXX/Desktop/sauvegardes/"} -# DBBACKUP_CONNECTORS = {"default": {"dump_suffix": "--hex-blob"}} -# DBBACKUP_CLEANUP_KEEP = 10 -# DBBACKUP_CLEANUP_KEEP_MEDIA = 10 +DBBACKUP_STORAGE = "django.core.files.storage.FileSystemStorage" +DBBACKUP_STORAGE_OPTIONS = {"location": "/home/faouzia-takaya/Documents/sauvegardes/"} +DBBACKUP_CONNECTORS = {"default": {"dump_suffix": "--hex-blob"}} +DBBACKUP_CLEANUP_KEEP = 10 +DBBACKUP_CLEANUP_KEEP_MEDIA = 10 # Pour un stockage de la sauvegarde sur Dropbox (Renseigner ci-dessous le token de Dropbox) # 1. Connectez-vous à Dropbox et accédez à Developper Apps : https://www.dropbox.com/developers/apps @@ -94,12 +101,15 @@ # Pour un stockage sur Dropbox : storages.backends.dropbox.DropBoxStorage ######################################################################################### -# STORAGE_PROBLEME = "django.core.files.storage.FileSystemStorage" -# STORAGE_PIECE = "django.core.files.storage.FileSystemStorage" -# STORAGE_QUOTIENT = "django.core.files.storage.FileSystemStorage" -# STORAGE_ASSURANCE = "django.core.files.storage.FileSystemStorage" -# STORAGE_PHOTO = "django.core.files.storage.FileSystemStorage" -# STORAGE_PIECE_COLLABORATEUR = "django.core.files.storage.FileSystemStorage" +STORAGE_PROBLEME = "django.core.files.storage.FileSystemStorage" +STORAGE_PIECE = "django.core.files.storage.FileSystemStorage" +STORAGE_QUOTIENT = "django.core.files.storage.FileSystemStorage" +STORAGE_ASSURANCE = "django.core.files.storage.FileSystemStorage" +STORAGE_PHOTO = "django.core.files.storage.FileSystemStorage" +STORAGE_PIECE_COLLABORATEUR = "django.core.files.storage.FileSystemStorage" + +# settings.py +SESSION_ENGINE = 'django.contrib.sessions.backends.db' # Si l'un des champs ci-dessus utilise Dropbox, renseignez le token Dropbox ci-dessous : # DROPBOX_OAUTH2_TOKEN = "XXXXXXXXXXXXXXX" @@ -113,31 +123,31 @@ # et modifiez si besoin les horaires de déclenchement et le path python : ######################################################################################### -# CRONTAB_PYTHON_EXECUTABLE = "/usr/bin/python3.9" -# CRONTAB_COMMAND_SUFFIX = '2>&1' -# CRONJOBS = [ -# ("* * * * *", "noethysweb.cron.Test_cron", ">> " + os.path.join(BASE_DIR, "debug_cron.log")) # Pour des tests -# ("00 23 * * *", "noethysweb.cron.Sauvegarder_db", ">> " + os.path.join(BASE_DIR, "debug_cron.log")), # Pour sauvegarder la base de données -# ("25 23 * * *", "noethysweb.cron.Vider_rep_temp", ">> " + os.path.join(BASE_DIR, "debug_cron.log")), # Pour purger le répertoire temp -# ("30 23 * * *", "noethysweb.cron.Sauvegarder_media", ">> " + os.path.join(BASE_DIR, "debug_cron.log")), # Pour sauvegarde le répertoire media -# ("45 23 * * *", "noethysweb.cron.Traiter_attentes", ">> " + os.path.join(BASE_DIR, "debug_cron.log")), # Pour traiter les réservations en attente -# ("50 23 * * *", "noethysweb.cron.Corriger_anomalies", ">> " + os.path.join(BASE_DIR, "debug_cron.log")), # Pour corriger les anomalies -# ("00 03 * * *", "noethysweb.cron.Generer_taches", ">> " + os.path.join(BASE_DIR, "debug_cron.log")), # Pour générer les tâches récurrentes -# ] +CRONTAB_PYTHON_EXECUTABLE = "/usr/bin/python3.9" +CRONTAB_COMMAND_SUFFIX = '2>&1' +CRONJOBS = [ + ("* * * * *", "noethysweb.cron.Test_cron", ">> " + os.path.join(BASE_DIR, "debug_cron.log")) ,# Pour des tests + ("00 23 * * *", "noethysweb.cron.Sauvegarder_db", ">> " + os.path.join(BASE_DIR, "debug_cron.log")), # Pour sauvegarder la base de données + ("25 23 * * *", "noethysweb.cron.Vider_rep_temp", ">> " + os.path.join(BASE_DIR, "debug_cron.log")), # Pour purger le répertoire temp + ("30 23 * * *", "noethysweb.cron.Sauvegarder_media", ">> " + os.path.join(BASE_DIR, "debug_cron.log")), # Pour sauvegarde le répertoire media + ("45 23 * * *", "noethysweb.cron.Traiter_attentes", ">> " + os.path.join(BASE_DIR, "debug_cron.log")), # Pour traiter les réservations en attente + ("50 23 * * *", "noethysweb.cron.Corriger_anomalies", ">> " + os.path.join(BASE_DIR, "debug_cron.log")), # Pour corriger les anomalies + ("00 03 * * *", "noethysweb.cron.Generer_taches", ">> " + os.path.join(BASE_DIR, "debug_cron.log")), # Pour générer les tâches récurrentes +] ######################################################################################### # SECURITE : Les paramètres par défaut conviendront généralement. ######################################################################################### -CSRF_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = False CSRF_COOKIE_SAMESITE = 'Strict' -SESSION_COOKIE_SECURE = True -SECURE_BROWSER_XSS_FILTER = True -SECURE_CONTENT_TYPE_NOSNIFF = True -SECURE_SSL_REDIRECT = True +SESSION_COOKIE_SECURE = False +SECURE_BROWSER_XSS_FILTER = False +SECURE_CONTENT_TYPE_NOSNIFF = False +SECURE_SSL_REDIRECT = False X_FRAME_OPTIONS = 'DENY' SECURE_HSTS_SECONDS = 15768000 -SECURE_HSTS_INCLUDE_SUBDOMAINS = True -SECURE_HSTS_PRELOAD = True +SECURE_HSTS_INCLUDE_SUBDOMAINS = False +SECURE_HSTS_PRELOAD = False SESSION_COOKIE_AGE = 60*60*12 # 12 heures -SESSION_EXPIRE_AT_BROWSER_CLOSE = True +SESSION_EXPIRE_AT_BROWSER_CLOSE = False From 21347906623f66286dbd0874d6bdfb2f37b1b1aa Mon Sep 17 00:00:00 2001 From: "faouzia.takaya" Date: Thu, 6 Mar 2025 19:03:02 +0100 Subject: [PATCH 02/25] =?UTF-8?q?ADD=20configuration=20de=20visibilt=C3=A9?= =?UTF-8?q?=20des=20param=C3=A8tres=20g=C3=A9n=C3=A9reaux=20de=20fichie=20?= =?UTF-8?q?famille,=20fiche=20individu=20et=20=20menu=20portail=20utilisat?= =?UTF-8?q?eur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/utils/utils_parametres_generaux.py | 98 ++++++++++++++++ noethysweb/core/utils/utils_permissions.py | 5 +- noethysweb/core/utils/utils_portail.py | 6 +- noethysweb/core/views/base.py | 13 ++- noethysweb/core/views/menu.py | 48 ++++---- .../fiche_famille/utils/utils_famille.py | 11 ++ noethysweb/fiche_famille/views/famille.py | 22 +++- .../fiche_individu/utils/utils_individu.py | 9 ++ noethysweb/fiche_individu/views/individu.py | 42 ++++++- .../parametrage/forms/parametres_generaux.py | 105 ++++++++++++++++++ noethysweb/parametrage/urls.py | 4 +- .../parametrage/views/parametres_generaux.py | 50 +++++++++ 12 files changed, 382 insertions(+), 31 deletions(-) create mode 100644 noethysweb/core/utils/utils_parametres_generaux.py create mode 100644 noethysweb/parametrage/forms/parametres_generaux.py create mode 100644 noethysweb/parametrage/views/parametres_generaux.py diff --git a/noethysweb/core/utils/utils_parametres_generaux.py b/noethysweb/core/utils/utils_parametres_generaux.py new file mode 100644 index 00000000..1d96047d --- /dev/null +++ b/noethysweb/core/utils/utils_parametres_generaux.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024-2025 Faouzia TEKAYA. +# Noethysweb, application de gestion multi-activités. +# Distribué sous licence GNU GPL. + +import decimal +from django import forms +from django_summernote.widgets import SummernoteInplaceWidget +from django.template.loader import render_to_string +from core.models import PortailParametre + + +class Parametre(): + def __init__(self, *args, **kwargs): + self.code = kwargs.get("code", None) + self.label = kwargs.get("label", None) + self.type = kwargs.get("type", None) + self.valeur = kwargs.get("valeur", None) + self.help_text = kwargs.get("help_text", None) + self.required = kwargs.get("required", False) + self.choix = kwargs.get("choix", []) + + def Get_ctrl(self): + if self.type == "boolean": + return forms.BooleanField(label=self.label, required=self.required, help_text=self.help_text) + + def From_db(self, valeur=""): + if self.type == "boolean": + self.valeur = valeur == "True" + else: + self.valeur = valeur + +LISTE_PARAMETRES = [ + + # Compte utilisateurs + Parametre(code="compte_famille", label="Compte Famille", valeur=False, type="boolean", help_text="Cochez cette case pour se connecter sur le portail en tant que Famille."), + Parametre(code="compte_individu", label="Compte Individu", valeur=True, type="boolean", help_text="Cochez cette case pour se connecter sur le portail en tant que Individu."), + + # Fiche Individu + Parametre(code="questionnaire_afficher_page_individu", label="Afficher la page Questionnaire", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Questionnaire sur la fiche Individu."), + Parametre(code="liens_afficher_page_individu", label="Afficher la page Liens", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Liens sur la fiche Individu."), + Parametre(code="regimes_alimentaires_afficher_page_individu", label="Afficher la page Régimes alimentaires", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Régimes alimentaires sur la fiche Individu."), + Parametre(code="maladies_afficher_page_individu", label="Afficher la page Maladies", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Maladies sur la fiche Individu."), + Parametre(code="medical_afficher_page_individu", label="Afficher la page Médical", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Médical sur la fiche Individu."), + Parametre(code="assurances_afficher_page_individu", label="Afficher la page Assurances", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Assurances sur la fiche Individu."), + Parametre(code="contacts_afficher_page_individu", label="Afficher la page Contacts", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Contacts sur la fiche Individu."), + Parametre(code="transports_afficher_page_individu", label="Afficher la page Transports", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Transports sur la fiche Individu."), + Parametre(code="consommations_afficher_page_individu", label="Afficher la page Consommations", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Consommations sur la fiche Individu."), + + # Fiche Famille + Parametre(code="questionnaire_afficher_page_famille", label="Afficher la page Questionnaire", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Questionnaire sur la fiche Famille."), + Parametre(code="pieces_afficher_page_famille", label="Afficher la page Pièces", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Pièces sur la fiche Famille."), + Parametre(code="locations_afficher_page_famille", label="Afficher la page Locations", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Locations sur la fiche Famille."), + Parametre(code="cotisations_afficher_page_famille", label="Afficher la page Adhésions", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Adhésions sur la fiche Famille."), + Parametre(code="caisse_afficher_page_famille", label="Afficher la page Caisse", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Caisse sur la fiche Famille."), + Parametre(code="aides_afficher_page_famille", label="Afficher la page Aides", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Aides sur la fiche Famille."), + Parametre(code="quotients_afficher_page_famille", label="Afficher la page Quotients familiaux", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Quotients familiaux sur la fiche Famille."), + Parametre(code="prestations_afficher_page_famille", label="Afficher la page Prestations", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Prestations sur la fiche Famille."), + Parametre(code="factures_afficher_page_famille", label="Afficher la page Factures", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Factures sur la fiche Famille."), + Parametre(code="reglements_afficher_page_famille", label="Afficher la page Règlements", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Règlements sur la fiche Famille."), + Parametre(code="messagerie_afficher_page_famille", label="Afficher la page Messagerie", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Questionnaire sur la fiche Famille."), + Parametre(code="outils_afficher_page_famille", label="Afficher la page Outils", type="boolean",valeur=True, help_text="Cochez cette case pour afficher la page Outils sur la fiche Famille."), + Parametre(code="consommations_afficher_page_famille", label="Afficher la page Consommations", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Consommations sur la fiche Famille."), + + # Portail Utilisateur + Parametre(code="outils_afficher_page_portailuser", label="Afficher la page Outils", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Outils sur le menu de Portail Utilisateur."), + Parametre(code="locations_afficher_page_portailuser", label="Afficher la page Locations", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Locations sur le menu de Portail Utilisateur."), + Parametre(code="adhesions_afficher_page_portailuser", label="Afficher la page Adhésions", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Adhésions sur le menu de Portail Utilisateur."), + Parametre(code="consommations_afficher_page", label="Afficher la page Consommations", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Consommations sur le menu de Portail Utilisateur."), + Parametre(code="factures_afficher_page_portailuser", label="Afficher la page Factures", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Factures sur le menu de Portail Utilisateur."), + Parametre(code="reglements_afficher_page_portailuser", label="Afficher la page Règlements", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Règlements sur le menu de Portail Utilisateur."), + Parametre(code="comptabilite_afficher_page_portailuser", label="Afficher la page Comptabilité", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Comptabilité sur le menu de Portail Utilisateur."), + Parametre(code="collabotrateurs_afficher_page_portailuser", label="Afficher la page Collabotrateurs", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Collabotrateurs sur le menu de Portail Utilisateur."), + Parametre(code="aides_afficher_page_portailuser", label="Afficher la page Aide", type="boolean", valeur=True, help_text="Cochez cette case pour afficher la page Aide sur le menu de Portail Utilisateur."), + + # Emails par LOT + Parametre(code="emails_individus_afficher_page", label="Afficher d'envoi des emails par Individus", type="boolean", valeur=False, help_text="Cochez cette case pour afficher la page Individus pour l'envoi des email par lot sur la partie Envoi email par lot."), + Parametre(code="emails_familles_afficher_page", label="Afficher d'envoi des emails par Familles", type="boolean", valeur=False, help_text="Cochez cette case pour afficher la page Familles pour l'envoi des email par lot sur la partie Envoi email par lot."), + Parametre(code="emails_activites_afficher_page", label="Afficher d'envoi des emails par Activites",type="boolean", valeur=True,help_text="Cochez cette case pour afficher la page Activites pour l'envoi des email par lot sur la partie Envoi email par lot."), + Parametre(code="emails_inscriptions_afficher_page", label="Afficher d'envoi des emails par Inscriptions", type="boolean", valeur=False, help_text="Cochez cette case pour afficher la page Insciptions pour l'envoi des email par lot sur la partie Envoi email par lot."), + Parametre(code="emails_collaborateurs_afficher_page", label="Afficher d'envoi des emails par collaborations", type="boolean", valeur=False, help_text="Cochez cette case pour afficher la page collaborations pour l'envoi des email par lot sur la partie Envoi email par lot."), + Parametre(code="emails_liste_diffusion_afficher_page", label="Afficher d'envoi des emails par listes de diffusion", type="boolean",valeur=False, help_text="Cochez cette case pour afficher la page Listes de diffusion pour l'envoi des email par lot sur la partie Envoi email par lot."), + +] + +def Get_dict_parametres(): + """ Renvoi un dict code: valeur des paramètres """ + dict_parametres = {parametre.code: parametre for parametre in LISTE_PARAMETRES} + + for parametre_db in PortailParametre.objects.all(): + if parametre_db.code in dict_parametres: + dict_parametres[parametre_db.code].From_db(parametre_db.valeur) + + return {code: parametre.valeur for code, parametre in dict_parametres.items()} + +def Get_parametre(code=""): + parametres = Get_dict_parametres() + return parametres.get(code, None) diff --git a/noethysweb/core/utils/utils_permissions.py b/noethysweb/core/utils/utils_permissions.py index 33ddee79..9e9ebc85 100644 --- a/noethysweb/core/utils/utils_permissions.py +++ b/noethysweb/core/utils/utils_permissions.py @@ -8,12 +8,13 @@ from fiche_individu.utils.utils_individu import LISTE_ONGLETS as LISTE_ONGLETS_INDIVIDUS -def GetPermissionsPossibles(organisateur=None): +def GetPermissionsPossibles(parametres_generaux=None, organisateur=None, categorie=None): """ Liste des commandes pour créer les permissions des utilisateurs """ liste_permissions = [] # Commandes de menu - menu_principal = GetMenuPrincipal(organisateur=organisateur) + menu_principal = GetMenuPrincipal(parametres_generaux=parametres_generaux, organisateur=organisateur) + for menu in menu_principal.GetChildren(): for sous_menu in menu.GetChildren(): for commande in sous_menu.GetChildren(): diff --git a/noethysweb/core/utils/utils_portail.py b/noethysweb/core/utils/utils_portail.py index e1ed87c7..f5aeb5bf 100644 --- a/noethysweb/core/utils/utils_portail.py +++ b/noethysweb/core/utils/utils_portail.py @@ -93,7 +93,11 @@ def From_db(self, valeur=""): Parametre(code="validation_auto:individu_medecin", label="Validation automatique de la page 'Médecin'", type="boolean", valeur=True), Parametre(code="renseignements_duree_certification", label="Durée de validité d'une certification", type="duree_certification", valeur=None, help_text="Par défaut, une certification est illimitée mais elle peut également être valable pour une durée donnée ou jusqu'à une date donnée. Une fois la certification expirée, la famille doit certifier de nouveau ses fiches de renseignements sur le portail."), - # Documents + #Compte utilisateurs + Parametre(code="compte_famille", label="Compte Famille",type="boolean", valeur=True , help_text="Cochez cette case pour se connecter sur le portail en tant que Famille."), + Parametre(code="compte_individu", label="Compte Individu", type="boolean", valeur=True, help_text="Cochez cette case pour se connecter sur le portail en tant que Individu."), + + # Cotisations Parametre(code="cotisations_afficher_page", label="Afficher la page", type="boolean", valeur=False), Parametre(code="cotisations_intro", label="Texte d'introduction", type="char_2lignes", valeur="Vous pouvez ici consulter les informations au sujet des adhésions."), diff --git a/noethysweb/core/views/base.py b/noethysweb/core/views/base.py index de17883d..b9977013 100644 --- a/noethysweb/core/views/base.py +++ b/noethysweb/core/views/base.py @@ -12,9 +12,9 @@ from django.core.cache import cache from core.views.menu import GetMenuPrincipal from core.models import Organisateur, Consommation, PortailMessage, PortailRenseignement -from core.utils import utils_parametres +from core.utils import utils_parametres , utils_parametres_generaux from noethysweb.version import GetVersion - +from django.db.models import Q def Memorise_option(request): """ Mémorise dans la DB et le cache une option d'interface pour l'utilisateur """ @@ -121,6 +121,13 @@ def get_context_data(self, **kwargs): cache.set('organisateur', organisateur) context['organisateur'] = organisateur + # Paramètres du portail + parametres_generaux = cache.get('parametres_generaux') + if not parametres_generaux: + parametres_generaux = utils_parametres_generaux.Get_dict_parametres() + cache.set('parametres_generaux', parametres_generaux, 1) + context['parametres_generaux'] = parametres_generaux + # Options d'interface key_cache = "options_interface_user%d" % self.request.user.pk if cache.get(key_cache, None) != None: @@ -138,7 +145,7 @@ def get_context_data(self, **kwargs): cache.set(key_cache, parametres) # Mémorise le menu principal - menu_principal = GetMenuPrincipal(organisateur=organisateur, user=self.request.user) + menu_principal = GetMenuPrincipal(parametres_generaux=parametres_generaux, organisateur=organisateur, user=self.request.user) context['menu_principal'] = menu_principal # Si la page est un crud, on récupère l'url de la liste en tant que menu_code diff --git a/noethysweb/core/views/menu.py b/noethysweb/core/views/menu.py index 167a6b3b..96753593 100644 --- a/noethysweb/core/views/menu.py +++ b/noethysweb/core/views/menu.py @@ -8,14 +8,16 @@ from django.conf import settings -def GetMenuPrincipal(organisateur=None, user=None): - menu = Menu(titre="Menu principal", user=user) +def GetMenuPrincipal(parametres_generaux=None, organisateur=None, user=None): + parametres_generaux = parametres_generaux or {} # Fallback to an empty dictionary + + menu = Menu(titre="Menu principal", user=user) # ------------------------------------ Accueil ------------------------------------ menu.Add(code="accueil", titre="Accueil", icone="home", toujours_afficher=True) # ------------------------------------ Paramétrage ------------------------------------ - menu_parametrage = menu.Add(code="parametrage_toc", titre="Paramétrage", icone="gear") + menu_parametrage = menu.Add(code="parametrage_toc", titre="Paramétrage", icone="gear", toujours_afficher=True) menu_structure = menu_parametrage.Add(titre="Généralités") if organisateur: @@ -23,6 +25,7 @@ def GetMenuPrincipal(organisateur=None, user=None): else: 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") # Activités menu_activites = menu_parametrage.Add(titre="Activités") @@ -164,7 +167,7 @@ def GetMenuPrincipal(organisateur=None, user=None): # ------------------------------------ Outils ------------------------------------ - menu_outils = menu.Add(code="outils_toc", titre="Outils", icone="wrench") + menu_outils = menu.Add(code="outils_toc", titre="Outils", icone="wrench", toujours_afficher=parametres_generaux.get("outils_afficher_page_portailuser", False)) # Statistiques menu_stats = menu_outils.Add(titre="Statistiques") @@ -225,7 +228,7 @@ def GetMenuPrincipal(organisateur=None, user=None): # ------------------------------------ Individus ------------------------------------ - menu_individus = menu.Add(code="individus_toc", titre="Individus", icone="user") + menu_individus = menu.Add(code="individus_toc", titre="Individus", icone="user", toujours_afficher=True ) # Liste des individus menu_gestion_individus = menu_individus.Add(titre="Gestion des individus") @@ -310,7 +313,7 @@ def GetMenuPrincipal(organisateur=None, user=None): # ------------------------------------ Locations ------------------------------------ - menu_locations = menu.Add(code="locations_toc", titre="Locations", icone="shopping-cart") + menu_locations = menu.Add(code="locations_toc", titre="Locations", icone="shopping-cart", toujours_afficher=parametres_generaux.get("locations_afficher_page_portailuser", False)) # Etat menu_etat_locations = menu_locations.Add(titre="Etat des locations") @@ -328,7 +331,7 @@ def GetMenuPrincipal(organisateur=None, user=None): # ------------------------------------ Cotisations ------------------------------------ - menu_cotisations = menu.Add(code="cotisations_toc", titre="Adhésions", icone="folder-o") + menu_cotisations = menu.Add(code="cotisations_toc", titre="Adhésions", icone="folder-o", toujours_afficher=parametres_generaux.get("adhesions_afficher_page_portailuser", False)) # Etat menu_etat_cotisations = menu_cotisations.Add(titre="Etat des adhésions") @@ -349,7 +352,7 @@ def GetMenuPrincipal(organisateur=None, user=None): # ------------------------------------ Consommations ------------------------------------ - menu_consommations = menu.Add(code="consommations_toc", titre="Consommations", icone="calendar") + menu_consommations = menu.Add(code="consommations_toc", titre="Consommations", icone="calendar", toujours_afficher=parametres_generaux.get("consommations_afficher_page", False)) # Gestion des consommations menu_gestion_conso = menu_consommations.Add(titre="Gestion des consommations") @@ -379,7 +382,7 @@ def GetMenuPrincipal(organisateur=None, user=None): # ------------------------------------ Facturation ------------------------------------ - menu_facturation = menu.Add(code="facturation_toc", titre="Facturation", icone="euro") + menu_facturation = menu.Add(code="facturation_toc", titre="Facturation", icone="euro", toujours_afficher=parametres_generaux.get("factures_afficher_page_portailuser", False)) # Factures menu_factures = menu_facturation.Add(titre="Factures") @@ -441,7 +444,7 @@ def GetMenuPrincipal(organisateur=None, user=None): # ------------------------------------ Règlements ------------------------------------ - menu_reglements = menu.Add(code="reglements_toc", titre="Règlements", icone="money") + menu_reglements = menu.Add(code="reglements_toc", titre="Règlements", icone="money", toujours_afficher=parametres_generaux.get("reglements_afficher_page_portailuser", False)) # Règlements menu_listes = menu_reglements.Add(titre="Règlements") @@ -470,7 +473,7 @@ def GetMenuPrincipal(organisateur=None, user=None): # ------------------------------------ Comptabilité ------------------------------------ - menu_comptabilite = menu.Add(code="comptabilite_toc", titre="Comptabilité", icone="line-chart") + menu_comptabilite = menu.Add(code="comptabilite_toc", titre="Comptabilité", icone="line-chart", toujours_afficher=parametres_generaux.get("comptabilite_afficher_page_portailuser", False)) # Opérations menu_comptabilite_operations = menu_comptabilite.Add(titre="Opérations") @@ -498,7 +501,7 @@ def GetMenuPrincipal(organisateur=None, user=None): # ------------------------------------ Collaborateurs ------------------------------------ - menu_collaborateurs = menu.Add(code="collaborateurs_toc", titre="Collaborateurs", icone="users") + menu_collaborateurs = menu.Add(code="collaborateurs_toc", titre="Collaborateurs", icone="users", toujours_afficher=parametres_generaux.get("collabotrateurs_afficher_page_portailuser", False)) # Liste des collaborateurs menu_gestion_collaborateurs = menu_collaborateurs.Add(titre="Gestion des collaborateurs") @@ -517,7 +520,7 @@ def GetMenuPrincipal(organisateur=None, user=None): # ------------------------------------ Aide ------------------------------------ - menu_aide = menu.Add(code="aide_toc", titre="Aide", icone="support", toujours_afficher=True) + menu_aide = menu.Add(code="aide_toc", titre="Aide", icone="support", toujours_afficher=parametres_generaux.get("aides_afficher_page_portailuser", False)) # ---------------------------------- Plugins ---------------------------------- @@ -575,13 +578,20 @@ def GetParent(self): return self.parent def Add(self, code="", titre="", icone="", url=None, toujours_afficher=False, compatible_demo=True, args=None, superutilisateur_only=False, masquer=False): - menu = Menu(self, code=code, titre=titre, icone=icone, url=url, args=args, user=self.user, compatible_demo=compatible_demo, toujours_afficher=toujours_afficher, masquer=masquer) - afficher = not code or not self.user or toujours_afficher or code.endswith("_toc") or self.user.has_perm("core.%s" % code) - if masquer or (self.user and superutilisateur_only and not self.user.is_superuser): - afficher = False - if afficher: + # Créer l'objet menu + menu = Menu(self, code=code, titre=titre, icone=icone, url=url, args=args, user=self.user, + compatible_demo=compatible_demo, toujours_afficher=toujours_afficher) + # Déterminer si ce menu doit être affiché + should_display = ( + toujours_afficher or # Toujours afficher si marqué comme tel + (code and self.user and self.user.has_perm( + f"core.{code}")) or # Afficher si l'utilisateur a la permission + self.toujours_afficher # Si le parent est marqué comme toujours visible + ) + # Ajouter le menu uniquement s'il doit être affiché + if should_display: self.children.append(menu) - return menu + return menu # Retourne toujours le menu, même s'il n'est pas affiché def GetUrl(self): if self.args: diff --git a/noethysweb/fiche_famille/utils/utils_famille.py b/noethysweb/fiche_famille/utils/utils_famille.py index c5bc3c68..e2c9cdd9 100644 --- a/noethysweb/fiche_famille/utils/utils_famille.py +++ b/noethysweb/fiche_famille/utils/utils_famille.py @@ -16,8 +16,19 @@ {"code": "prestations", "label": "Prestations", "icone": "fa-euro", "url": "famille_prestations_liste"}, {"code": "factures", "label": "Factures", "icone": "fa-euro", "url": "famille_factures_liste"}, {"code": "reglements", "label": "Règlements", "icone": "fa-money", "url": "famille_reglements_liste"}, + {"code": "messagerie", "label": "Messagerie", "icone": "fa-file-text-o", "url": "famille_messagerie_portail"}, {"code": "portail", "label": "Portail", "icone": "fa-globe", "url": "famille_portail"}, {"code": "divers", "label": "Paramètres", "icone": "fa-gear", "url": "famille_divers"}, {"code": "outils", "label": "Outils", "icone": "fa-wrench", "url": "famille_outils"}, {"code": "consommations", "label": "Consommations", "icone": "fa-calendar", "url": "famille_consommations"}, ] +def Get_filtered_onglets(): + from core.utils.utils_parametres_generaux import Get_dict_parametres + parametres = Get_dict_parametres() + + # Filtrer la liste en fonction des paramètres + filtered_onglets = [ + onglet for onglet in LISTE_ONGLETS + if parametres.get(f"{onglet['code']}_afficher_page_famille", True) + ] + return filtered_onglets \ No newline at end of file diff --git a/noethysweb/fiche_famille/views/famille.py b/noethysweb/fiche_famille/views/famille.py index ccfd8586..60caa14e 100644 --- a/noethysweb/fiche_famille/views/famille.py +++ b/noethysweb/fiche_famille/views/famille.py @@ -18,6 +18,7 @@ from fiche_individu.forms.individu import Formulaire from fiche_famille.utils.utils_famille import LISTE_ONGLETS from cotisations.utils import utils_cotisations_manquantes +from core.utils.utils_parametres_generaux import Get_dict_parametres def Definir_titulaire(request): @@ -172,12 +173,29 @@ class Onglet(CustomView): def get_context_data(self, **kwargs): context = super(Onglet, self).get_context_data(**kwargs) context['page_titre'] = "Fiche famille" - context['liste_onglets'] = [dict_onglet for dict_onglet in self.liste_onglets if self.request.user.has_perm("core.famille_%s" % dict_onglet["code"])] + + # Récupérer les paramètres généraux pour filtrer les onglets + parametres = Get_dict_parametres() + + # Vérifier si le compte famille est actif depuis la session + compte_famille_active = self.request.session.get('compte_famille_active', False) + + # Appliquer les permissions et les paramètres pour afficher les onglets + context['liste_onglets'] = [ + dict_onglet for dict_onglet in self.liste_onglets + if self.request.user.has_perm("core.famille_%s" % dict_onglet["code"]) + and (dict_onglet["code"] != "portail" or compte_famille_active) + and parametres.get(f"{dict_onglet['code']}_afficher_page_famille", True) # Filtrage dynamique + ] + context['famille'] = Famille.objects.get(pk=self.kwargs['idfamille']) context['idfamille'] = self.kwargs['idfamille'] context['categories'] = CATEGORIES_RATTACHEMENT - context['rattachements'] = Rattachement.objects.prefetch_related('individu').filter(famille_id=self.kwargs['idfamille']).order_by("individu__civilite") + context['rattachements'] = Rattachement.objects.prefetch_related('individu').filter( + famille_id=self.kwargs['idfamille'] + ).order_by("individu__civilite") context['categories_utilisees'] = self.Get_categories_utilisees(context['rattachements']) + return context def Get_categories_utilisees(self, rattachements=[]): diff --git a/noethysweb/fiche_individu/utils/utils_individu.py b/noethysweb/fiche_individu/utils/utils_individu.py index 69bd9789..3be46715 100644 --- a/noethysweb/fiche_individu/utils/utils_individu.py +++ b/noethysweb/fiche_individu/utils/utils_individu.py @@ -20,4 +20,13 @@ {"code": "consommations", "label": "Consommations", "icone": "fa-calendar", "url": "famille_consommations"}, ] +def Get_filtered_onglets(): + from core.utils.utils_parametres_generaux import Get_dict_parametres + parametres = Get_dict_parametres() + # Filtrer la liste en fonction des paramètres + filtered_onglets = [ + onglet for onglet in LISTE_ONGLETS + if parametres.get(f"{onglet['code']}_afficher_page_individu", True) + ] + return filtered_onglets diff --git a/noethysweb/fiche_individu/views/individu.py b/noethysweb/fiche_individu/views/individu.py index 04cace86..dc0a0c04 100644 --- a/noethysweb/fiche_individu/views/individu.py +++ b/noethysweb/fiche_individu/views/individu.py @@ -12,7 +12,8 @@ from core.views.base import CustomView from core.models import Individu, Famille, Note, Rattachement, Inscription from core.utils import utils_texte, utils_dates -from fiche_individu.utils.utils_individu import LISTE_ONGLETS +from fiche_individu.utils.utils_individu import LISTE_ONGLETS , Get_filtered_onglets +from core.utils.utils_parametres_generaux import Get_dict_parametres class Page(crud.Page): @@ -122,7 +123,24 @@ class Onglet(CustomView): def get_context_data(self, **kwargs): context = super(Onglet, self).get_context_data(**kwargs) context['page_titre'] = "Fiche individuelle" - context['liste_onglets'] = [dict_onglet for dict_onglet in self.liste_onglets if self.request.user.has_perm("core.individu_%s" % dict_onglet["code"])] + + # Récupération des paramètres pour le filtrage des onglets + parametres = Get_dict_parametres() + + # Récupération des droits utilisateur et paramètres spécifiques + titulaire_rattachement = self.get_titulaire_rattachement() + compte_individu_active = self.request.session.get('compte_individu_active', False) + + # Filtrage des onglets en fonction des permissions et paramètres + context['liste_onglets'] = [ + dict_onglet for dict_onglet in self.liste_onglets + if self.request.user.has_perm("core.individu_%s" % dict_onglet["code"]) # Vérifie les permissions + and parametres.get(f"{dict_onglet['code']}_afficher_page_individu", True) # Filtre selon les paramètres + and (dict_onglet["code"] != "portail" or (compte_individu_active and titulaire_rattachement == 1)) + # Logique spécifique au portail + ] + + # Récupération des informations supplémentaires context['idindividu'] = self.kwargs['idindividu'] context['individu'] = Individu.objects.get(pk=self.kwargs['idindividu']) if self.kwargs.get('idfamille', None): @@ -131,6 +149,7 @@ def get_context_data(self, **kwargs): else: context['idfamille'] = None context['famille'] = None + return context def Get_idindividu(self): @@ -152,7 +171,24 @@ def Maj_infos_famille(self): for rattachement in rattachements: rattachement.famille.Maj_infos() - + def get_rattachement(self): + """Fetches the rattachement based on the individual ID and family ID""" + idindividu = self.Get_idindividu() + idfamille = self.Get_idfamille() + if idindividu and idfamille: + try: + return Rattachement.objects.select_related('individu', 'famille').get( + individu_id=idindividu, + famille_id=idfamille + ) + except Rattachement.DoesNotExist: + return None + return None + + def get_titulaire_rattachement(self): + """Fetches the categorie of the rattachement""" + rattachement = self.get_rattachement() + return rattachement.titulaire if rattachement else None class Resume(Onglet, DetailView): template_name = "fiche_individu/individu_resume.html" diff --git a/noethysweb/parametrage/forms/parametres_generaux.py b/noethysweb/parametrage/forms/parametres_generaux.py new file mode 100644 index 00000000..10d575f7 --- /dev/null +++ b/noethysweb/parametrage/forms/parametres_generaux.py @@ -0,0 +1,105 @@ +# Copyright (c) 2024 GIP RECIA. +# Noethysweb, application de gestion multi-activités. +# Distribué sous licence GNU GPL. + +from django import forms +from core.forms.base import FormulaireBase +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, Hidden, Submit, HTML, Fieldset, Div, ButtonHolder +from crispy_forms.bootstrap import Field, InlineRadios +from core.utils.utils_commandes import Commandes +from core.models import PortailParametre +from core.utils.utils_parametres_generaux import LISTE_PARAMETRES + + +LISTE_RUBRIQUES = [ + ("Compte Utilisateurs", ["compte_famille", "compte_individu"]), + + ("Fiche Individu", ["questionnaire_afficher_page_individu" , "liens_afficher_page_individu", "regimes_alimentaires_afficher_page_individu", + "maladies_afficher_page_individu" ,"medical_afficher_page_individu" , "assurances_afficher_page_individu" , "contacts_afficher_page_individu" , + "transports_afficher_page_individu" , "consommations_afficher_page_individu"]), + + ("Fiche Famille", ["questionnaire_afficher_page_famille" , "pieces_afficher_page_famille" , "locations_afficher_page_famille" , + "cotisations_afficher_page_famille" , "caisse_afficher_page_famille" , "aides_afficher_page_famille" , "quotients_afficher_page_famille" , + "prestations_afficher_page_famille" , "factures_afficher_page_famille" , "reglements_afficher_page_famille" , + "messagerie_afficher_page_famille" , "outils_afficher_page_famille" , "consommations_afficher_page_famille"]), + + ("Portail Utilisateur", [ + "outils_afficher_page_portailuser" ,"locations_afficher_page_portailuser" , "adhesions_afficher_page_portailuser" , + "consommations_afficher_page" ,"factures_afficher_page_portailuser" , "reglements_afficher_page_portailuser" + , "comptabilite_afficher_page_portailuser" ,"collabotrateurs_afficher_page_portailuser" , "aides_afficher_page_portailuser"]) + +] + + +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_generaux' %}", ajouter=False)) + + dict_parametres = {parametre.code: parametre for parametre in LISTE_PARAMETRES} + for parametre_db in PortailParametre.objects.all(): + if parametre_db.code in dict_parametres: + dict_parametres[parametre_db.code].From_db(parametre_db.valeur) + + # Création des fields + for titre_rubrique, liste_parametres in LISTE_RUBRIQUES: + liste_fields = [] + for code_parametre in liste_parametres: + self.fields[code_parametre] = dict_parametres[code_parametre].Get_ctrl() + self.fields[code_parametre].initial = dict_parametres[code_parametre].valeur + liste_fields.append(Field(code_parametre)) + self.helper.layout.append(Fieldset(titre_rubrique, *liste_fields)) + + self.helper.layout.append(HTML("
")) + + self.helper.layout.append(HTML(EXTRA_SCRIPT)) + + def clean(self): + cleaned_data = super().clean() + compte_individu = cleaned_data.get("compte_individu") + compte_famille = cleaned_data.get("compte_famille") + # Validation pour s'assurer qu'un seul champ est sélectionné + if compte_individu and compte_famille: + raise forms.ValidationError("Vous ne pouvez sélectionner qu'un seul compte à la fois.") + return cleaned_data + + +EXTRA_SCRIPT = """ + +
+""" \ No newline at end of file diff --git a/noethysweb/parametrage/urls.py b/noethysweb/parametrage/urls.py index 719f7479..9d7b1d9b 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 + activites_evenements_categories, activites_import_export, parametres_generaux urlpatterns = [ @@ -39,6 +39,8 @@ path('parametrage/organisateur/ajouter', organisateur.Ajouter.as_view(), name='organisateur_ajouter'), path('parametrage/organisateur/modifier/', organisateur.Modifier.as_view(), name='organisateur_modifier'), + # Paramètres généraux + path('parametrage/parametres_generaux/', parametres_generaux.Modifier.as_view(), name='parametres_generaux'), # Structures path('parametrage/structures/liste', structures.Liste.as_view(), name='structures_liste'), path('parametrage/structures/ajouter', structures.Ajouter.as_view(), name='structures_ajouter'), diff --git a/noethysweb/parametrage/views/parametres_generaux.py b/noethysweb/parametrage/views/parametres_generaux.py new file mode 100644 index 00000000..ba4c1915 --- /dev/null +++ b/noethysweb/parametrage/views/parametres_generaux.py @@ -0,0 +1,50 @@ +# Copyright (c) 2024 GIP RECIA. +# Noethysweb, application de gestion multi-activités. +# Distribué sous licence GNU GPL. + +from django.urls import reverse_lazy +from django.core.cache import cache +from django.views.generic import TemplateView +from django.http import HttpResponseRedirect +from core.views.base import CustomView +from parametrage.forms.parametres_generaux import Formulaire +import django.contrib.messages +from core.models import PortailParametre + + +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 Généraux" + context['box_titre'] = "Paramètres" + context['box_introduction'] = "Ajustez les paramètres de Portail utilisateur et cliquez sur le bouton Enregistrer." + context['form'] = Formulaire() + return context + + def post(self, request, **kwargs): + form = Formulaire(request.POST, request=self.request) + if not form.is_valid(): + 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_generaux")) \ No newline at end of file From e5cc71ebd3815f0a509c2dc34642e60c0f20b8ce Mon Sep 17 00:00:00 2001 From: "faouzia.takaya" Date: Fri, 7 Mar 2025 14:03:10 +0100 Subject: [PATCH 03/25] ADD configuration de compte individu --- noethysweb/core/admin.py | 37 +++++ noethysweb/core/decorators.py | 17 ++- noethysweb/core/models.py | 39 +++-- noethysweb/core/utils/utils_historique.py | 6 + .../fiche_famille/utils/utils_internet.py | 108 ++++++++++---- .../fiche_famille/views/famille_ajouter.py | 108 +++++++++++--- .../fiche_individu/forms/individu_portail.py | 137 ++++++++++++++++++ .../fiche_individu/individu_portail.html | 60 ++++++++ .../widgets/internet_identifiant.html | 37 +++++ .../fiche_individu/widgets/internet_mdp.html | 58 ++++++++ noethysweb/fiche_individu/urls.py | 8 +- .../fiche_individu/utils/utils_individu.py | 1 + .../fiche_individu/views/individu_portail.py | 133 +++++++++++++++++ noethysweb/fiche_individu/widgets.py | 42 +++++- .../individus/views/effacer_familles.py | 33 +++-- noethysweb/noethysweb/middleware.py | 32 +++- .../outils/views/editeur_emails_express.py | 46 +++--- .../outils/views/editeur_sms_express.py | 24 ++- noethysweb/outils/views/historique.py | 23 ++- 19 files changed, 827 insertions(+), 122 deletions(-) create mode 100644 noethysweb/fiche_individu/forms/individu_portail.py create mode 100644 noethysweb/fiche_individu/templates/fiche_individu/individu_portail.html create mode 100644 noethysweb/fiche_individu/templates/fiche_individu/widgets/internet_identifiant.html create mode 100644 noethysweb/fiche_individu/templates/fiche_individu/widgets/internet_mdp.html create mode 100644 noethysweb/fiche_individu/views/individu_portail.py diff --git a/noethysweb/core/admin.py b/noethysweb/core/admin.py index 8ed069e0..ed092e87 100644 --- a/noethysweb/core/admin.py +++ b/noethysweb/core/admin.py @@ -134,3 +134,40 @@ class Meta: # admin.site.register(Utilisateur_Famille, FamilleAdmin) + +# --------------------------- Individu ---------------------------- + +class IndividuAdmin(UserAdmin): + form = UserEditForm + + # Affiche uniquement les colonnes suivantes dans la liste + list_display = ('username', 'email', 'is_active') + + # Affiche uniquement les filtres suivants + list_filter = ('is_active',) + + # Affiche uniquement les champs suivants + fieldsets = ( + (None, {'fields': ('username', 'password', 'email')}), + ('Permissions', {'fields': ('is_active', 'user_permissions')}), + ('Dates importantes', {'fields': ('last_login', 'date_joined')}) + ) + + def get_queryset(self, request): + """ Affiche uniquement les utilisateurs de type famille """ + qs = super().get_queryset(request) + return qs.filter(categorie="individu") + + def has_add_permission(self, request): + """ Empêche l'ajout d'un utilisateur famille """ + return False + + def has_delete_permission(self, request, obj=None): + """ Empêche la suppression d'un utilisateur famille """ + return False + + +class Utilisateur_Individu(Utilisateur): + class Meta: + proxy = True + verbose_name = "Individu" \ No newline at end of file diff --git a/noethysweb/core/decorators.py b/noethysweb/core/decorators.py index 2a3d24f2..42585211 100644 --- a/noethysweb/core/decorators.py +++ b/noethysweb/core/decorators.py @@ -2,12 +2,12 @@ # Copyright (c) 2019-2021 Ivan LUCAS. # Noethysweb, application de gestion multi-activités. # Distribué sous licence GNU GPL. - +import logging +logger = logging.getLogger(__name__) from django.urls import reverse_lazy, reverse from django.http import HttpResponseRedirect, HttpResponseBadRequest, HttpResponseForbidden -from django.contrib.auth import REDIRECT_FIELD_NAME -from django.contrib.auth.decorators import user_passes_test from reglements.utils import utils_ventilation +from core.models import PortailParametre def Verifie_ventilation(function): @@ -39,6 +39,10 @@ def _function(request, *args, **kwargs): def secure_ajax_portail(function): """ A associer aux requêtes AJAX """ def _function(request, *args, **kwargs): + compte_famille = PortailParametre.objects.filter(code="compte_famille").first() + compte_individu = PortailParametre.objects.filter(code="compte_individu").first() + # logger.debug("compte_famille: %s" % compte_famille.valeur) + # logger.debug("compte_individu: %s" % compte_individu.valeur) # Vérifie que c'est une requête AJAX if not request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest': return HttpResponseBadRequest() @@ -46,8 +50,11 @@ def _function(request, *args, **kwargs): if not request.user.is_authenticated: return HttpResponseForbidden() # Vérifie que c'est un user de type utilisateur - if request.user.categorie != "famille": + # et que si le paramètre de compte individu est true ( coché ) alors interdire l'accès ou l'utilisation des identifiants famille + if((request.user.categorie not in ["famille", "individu"]) # Vérification de la catégorie : Si l'utilisateur n'est ni "famille" ni "individu", l'accès est interdit. + or (compte_famille != "True" and request.user.categorie == "famille") #Compte famille non activé pour une famille: Si compte_famille n'est pas "True" et que la catégorie est "famille", l'accès est interdit. + or ( compte_individu == "True" and request.user.categorie == "famille")): #Compte individu activé pour une famille: Si compte_individu est "True" alors que l'utilisateur est de catégorie "famille", l'accès est interdit. + # if request.user.categorie != "individu": return HttpResponseForbidden() return function(request, *args, **kwargs) return _function - diff --git a/noethysweb/core/models.py b/noethysweb/core/models.py index 3075ab72..839c6bd0 100644 --- a/noethysweb/core/models.py +++ b/noethysweb/core/models.py @@ -1649,7 +1649,17 @@ class Meta: def __str__(self): return "Remplissage ID%d" % self.idremplissage if self.idremplissage else "Nouveau" +class CategorieCompteInternet(models.Model): + idcategorie = models.AutoField(verbose_name="ID", db_column='IDcategorie', primary_key=True) + nom = models.CharField(verbose_name="Nom", max_length=200) + + class Meta: + db_table = 'categories_compte_internet' + verbose_name = "catégorie de compte internet" + verbose_name_plural = "catégories de compte internet" + def __str__(self): + return self.nom class Individu(models.Model): idindividu = models.AutoField(verbose_name="ID", db_column='IDindividu', primary_key=True) @@ -1696,6 +1706,17 @@ class Individu(models.Model): type_garde_choix = [(1, "Mère"), (2, "Père"), (3, "Garde alternée"), (4, "Autre personne")] type_garde = models.IntegerField(verbose_name=_("Type de garde"), choices=type_garde_choix, blank=True, null=True) info_garde = models.TextField(verbose_name=_("Information sur la garde"), blank=True, null=True) + # new attributs + internet_categorie = models.ForeignKey(CategorieCompteInternet, verbose_name="Catégorie",related_name="internet_categori", on_delete=models.PROTECT, blank=True,null=True) + internet_actif = models.BooleanField(verbose_name="Compte internet activé", default=True) + internet_identifiant = encrypt(models.CharField(verbose_name="Identifiant", max_length=200, blank=True, null=True)) + internet_mdp = encrypt(models.CharField(verbose_name="Mot de passe", max_length=200, blank=True, null=True)) + internet_secquest = models.CharField(verbose_name="Question", max_length=200, blank=True, null=True) + internet_reservations = models.BooleanField(verbose_name="Autoriser les réservations sur le portail", default=True) + mobile = encrypt(models.CharField(verbose_name="Portable favori", max_length=100, blank=True, null=True)) + utilisateur = models.OneToOneField(Utilisateur, on_delete=models.CASCADE, null=True) + certification_date = models.DateTimeField(verbose_name="Date de certification", blank=True, null=True) + blocage_impayes_off = models.BooleanField(verbose_name="Ne jamais appliquer le blocage des réservations si impayés",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.") class Meta: db_table = 'individus' @@ -1767,6 +1788,9 @@ def Maj_infos(self): self.ville_resid = dict_adresse["ville"] self.secteur = dict_adresse["secteur"] self.save() + def save_individu(sender, instance, **kwargs): + if hasattr(instance, 'individu'): + instance.individu.save() class Scolarite(models.Model): @@ -1787,19 +1811,6 @@ def __str__(self): return "Etape de scolarité du %s au %s" % (self.date_debut.strftime('%d/%m/%Y'), self.date_fin.strftime('%d/%m/%Y')) -class CategorieCompteInternet(models.Model): - idcategorie = models.AutoField(verbose_name="ID", db_column='IDcategorie', primary_key=True) - nom = models.CharField(verbose_name="Nom", max_length=200) - - class Meta: - db_table = 'categories_compte_internet' - verbose_name = "catégorie de compte internet" - verbose_name_plural = "catégories de compte internet" - - def __str__(self): - return self.nom - - class Famille(models.Model): idfamille = models.AutoField(verbose_name="ID", db_column='IDfamille', primary_key=True) date_creation = models.DateTimeField(verbose_name="Date de création", auto_now_add=True) @@ -3144,7 +3155,7 @@ def Is_famille_authorized(self, famille=None): class PortailParametre(models.Model): idparametre = models.AutoField(verbose_name="ID", db_column='IDparametre', primary_key=True) - code = models.CharField(verbose_name="Code", max_length=200, blank=True, null=True) + code = models.CharField(verbose_name="Code", max_length=200, blank=True, null=True, unique=True) valeur = models.TextField(verbose_name="Valeur", blank=True, null=True) class Meta: diff --git a/noethysweb/core/utils/utils_historique.py b/noethysweb/core/utils/utils_historique.py index 07433e4d..b145ba38 100644 --- a/noethysweb/core/utils/utils_historique.py +++ b/noethysweb/core/utils/utils_historique.py @@ -4,12 +4,18 @@ # Distribué sous licence GNU GPL. import logging + +from core.models import Individu logger = logging.getLogger(__name__) from core.models import Historique def Ajouter(titre="", detail="", utilisateur=None, famille=None, individu=None, collaborateur=None, objet=None, idobjet=None, classe=None, old=None, portail=False, activite=None): try: + # Vérifiez si l'individu existe avant de créer une trace d'historique + if individu and not Individu.objects.filter(idindividu=individu).exists(): + detail += " (Individu a été supprimé)" + individu = None # Réglez None pour éviter les erreurs de clé étrangère Historique.objects.create(titre=titre, detail=detail, utilisateur=utilisateur, famille_id=famille, individu_id=individu, collaborateur_id=collaborateur, objet=objet, idobjet=idobjet, classe=classe, old=old, portail=portail, activite_id=activite) except Exception as err: diff --git a/noethysweb/fiche_famille/utils/utils_internet.py b/noethysweb/fiche_famille/utils/utils_internet.py index 691f0b28..cefde427 100644 --- a/noethysweb/fiche_famille/utils/utils_internet.py +++ b/noethysweb/fiche_famille/utils/utils_internet.py @@ -4,9 +4,10 @@ # Distribué sous licence GNU GPL. import logging, random, datetime + logger = logging.getLogger(__name__) from django.conf import settings -from core.models import Famille, Utilisateur +from core.models import Famille, Utilisateur , Individu from fiche_famille.utils import utils_internet @@ -22,6 +23,18 @@ def CreationIdentifiant(IDfamille=None, IDutilisateur=None, nbreCaract=8): identifiant = u"U%d" % (int(identifiant) + IDutilisateur) return identifiant +def CreationIdentifiantIndividu(IDindividu=None, IDutilisateur=None, nbreCaract=8): + """ Création d'un identifiant aléatoire """ + numTmp = "" + for x in range(0, nbreCaract-4): + numTmp += random.choice("123456789") + identifiant = numTmp + "0" * 5 + if IDindividu: + identifiant = u"I%d" % (int(identifiant) + IDindividu) + if IDutilisateur: + identifiant = u"U%d" % (int(identifiant) + IDutilisateur) + return identifiant + def CreationMDP(nbreCaract=10): """ Création d'un mot de passe aléatoire """ mdp = "".join([random.choice("bcdfghjkmnprstvwxzBCDFGHJKLMNPRSTVWXZ123456789") for x in range(0, nbreCaract)]) @@ -33,37 +46,74 @@ def CreationDateExpirationMDP(): return datetime.datetime.now() + datetime.timedelta(seconds=settings.DUREE_VALIDITE_MDP) if settings.DUREE_VALIDITE_MDP else None def ReinitTousMdp(): - for famille in Famille.objects.select_related("utilisateur").all(): - internet_mdp, date_expiration_mdp = utils_internet.CreationMDP() - famille.internet_mdp = internet_mdp - if not famille.utilisateur: - utilisateur = Utilisateur(username=famille.internet_identifiant, categorie="famille", force_reset_password=True, date_expiration_mdp=date_expiration_mdp) - utilisateur.save() - utilisateur.set_password(internet_mdp) - utilisateur.save() - famille.utilisateur = utilisateur - else: - famille.utilisateur.set_password(internet_mdp) - famille.utilisateur.force_reset_password = True - famille.utilisateur.date_expiration_mdp = date_expiration_mdp - famille.utilisateur.save() - famille.save() + if Famille.objects.select_related("utilisateur"): + for famille in Famille.objects.select_related("utilisateur").all(): + internet_mdp, date_expiration_mdp = utils_internet.CreationMDP() + famille.internet_mdp = internet_mdp + if not famille.utilisateur: + utilisateur = Utilisateur(username=famille.internet_identifiant, categorie="famille", force_reset_password=True, date_expiration_mdp=date_expiration_mdp) + utilisateur.save() + utilisateur.set_password(internet_mdp) + utilisateur.save() + famille.utilisateur = utilisateur + else: + famille.utilisateur.set_password(internet_mdp) + famille.utilisateur.force_reset_password = True + famille.utilisateur.date_expiration_mdp = date_expiration_mdp + famille.utilisateur.save() + famille.save() + + if Individu.objects.select_related("utilisateur"): + for individu in Individu.objects.select_related("utilisateur").all(): + internet_mdp, date_expiration_mdp = utils_internet.CreationMDP() + individu.internet_mdp = internet_mdp + if not individu.utilisateur: + utilisateur = Utilisateur(username=individu.internet_identifiant, categorie="individu", force_reset_password=True, date_expiration_mdp=date_expiration_mdp) + utilisateur.save() + utilisateur.set_password(internet_mdp) + utilisateur.save() + individu.utilisateur = utilisateur + else: + individu.utilisateur.set_password(internet_mdp) + individu.utilisateur.force_reset_password = True + individu.utilisateur.date_expiration_mdp = date_expiration_mdp + individu.utilisateur.save() + individu.save() def Purge_mdp_expires(): """ Efface tous les mdp expirés """ - utilisateurs = Utilisateur.objects.select_related("famille").filter(categorie="famille", date_expiration_mdp__lte=datetime.datetime.now() - datetime.timedelta(days=3)) - for utilisateur in utilisateurs: - if getattr(utilisateur, "famille", None): - logger.debug("Purge du mot de passe expiré de %s." % utilisateur.username) - utilisateur.set_password(None) - utilisateur.save() - utilisateur.famille.internet_mdp = None - utilisateur.famille.save() + if Utilisateur.objects.select_related("famille"): + utilisateurs = Utilisateur.objects.select_related("famille").filter(categorie="famille", date_expiration_mdp__lte=datetime.datetime.now() - datetime.timedelta(days=3)) + for utilisateur in utilisateurs: + if getattr(utilisateur, "famille", None): + logger.debug("Purge du mot de passe expiré de %s." % utilisateur.username) + utilisateur.set_password(None) + utilisateur.save() + utilisateur.famille.internet_mdp = None + utilisateur.famille.save() + + if Utilisateur.objects.select_related("individu"): + utilisateurs = Utilisateur.objects.select_related("individu").filter(categorie="individu", date_expiration_mdp__lte=datetime.datetime.now() - datetime.timedelta(days=3)) + for utilisateur in utilisateurs: + if getattr(utilisateur, "individu", None): + logger.debug("Purge du mot de passe expiré de %s." % utilisateur.username) + utilisateur.set_password(None) + utilisateur.save() + utilisateur.individu.internet_mdp = None + utilisateur.individu.save() def Fix_dates_expiration_mdp(): """ Applique une date d'expiration aux mots de passe existants """ - for famille in Famille.objects.select_related("utilisateur").filter(utilisateur__date_expiration_mdp__isnull=True): - if "*****" not in famille.internet_mdp: - logger.debug("Application d'une date d'expiration MDP pour %s" % famille.utilisateur.username) - famille.utilisateur.date_expiration_mdp = CreationDateExpirationMDP() - famille.utilisateur.save() + if Famille.objects.select_related("utilisateur"): + for famille in Famille.objects.select_related("utilisateur").filter(utilisateur__date_expiration_mdp__isnull=True): + if "*****" not in famille.internet_mdp: + logger.debug("Application d'une date d'expiration MDP pour %s" % famille.utilisateur.username) + famille.utilisateur.date_expiration_mdp = CreationDateExpirationMDP() + famille.utilisateur.save() + + if Individu.objects.select_related("utilisateur"): + for individu in Individu.objects.select_related("utilisateur").filter(utilisateur__date_expiration_mdp__isnull=True): + if "*****" not in individu.internet_mdp: + logger.debug("Application d'une date d'expiration MDP pour %s" % individu.utilisateur.username) + individu.utilisateur.date_expiration_mdp = CreationDateExpirationMDP() + individu.utilisateur.save() \ No newline at end of file diff --git a/noethysweb/fiche_famille/views/famille_ajouter.py b/noethysweb/fiche_famille/views/famille_ajouter.py index 390e025d..929ab8e4 100644 --- a/noethysweb/fiche_famille/views/famille_ajouter.py +++ b/noethysweb/fiche_famille/views/famille_ajouter.py @@ -18,6 +18,11 @@ from fiche_famille.views.famille import Onglet from fiche_famille.utils import utils_internet +import logging + +logger = logging.getLogger(__name__) +from core.models import Consentement + def Get_individus_existants(request): nom = request.POST.get("nom", "") @@ -132,6 +137,26 @@ def form_valid(self, form): self.object.Maj_infos() break + # Fournir un identifiant et un mot de passe à l'individu créé lors de la création d'une famille. + individu = Individu.objects.get(pk=self.object.idindividu) + 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() # Renvoie vers la fiche individuelle url_success = reverse_lazy("individu_resume", kwargs={'idindividu': self.object.idindividu, 'idfamille': famille.pk}) @@ -145,7 +170,6 @@ def form_valid(self, form): # Sauvegarde du rattachement rattachement = Rattachement(famille=famille, individu=self.object, categorie=categorie, titulaire=titulaire) rattachement.save() - # MAJ des infos de la famille famille.Maj_infos() @@ -169,17 +193,13 @@ def Creation_famille(self): # Création de l'utilisateur utilisateur = Utilisateur(username=internet_identifiant, categorie="famille", force_reset_password=True, date_expiration_mdp=date_expiration_mdp) - utilisateur.save() utilisateur.set_password(internet_mdp) utilisateur.save() - # Association de l'utilisateur à la famille famille.utilisateur = utilisateur famille.save() - return famille - class Ajouter_individu(Page, Ajouter): """ Ajouter un individu depuis la fiche famille """ form_class = Formulaire @@ -254,37 +274,79 @@ def delete(self, request, *args, **kwargs): def Supprime_famille(self, request=None, individu=None, famille=None): """ Supprime la famille si besoin""" - # Recherche si l'individu à supprimer est le dernier titulaire de la famille + + # Vérifier si l'individu à supprimer est le dernier titulaire de la famille rattachements = Rattachement.objects.filter(famille=famille, titulaire=True).exclude(individu=individu) + if not rattachements: - # S'il reste un autre membre, on empêche la suppression du dernier titulaire + # Vérifier s'il reste d'autres membres dans la famille if Rattachement.objects.filter(famille=famille).exclude(individu=individu).exists(): - messages.add_message(request, messages.ERROR, "Vous ne pouvez pas %s le dernier titulaire car il reste au moins un autre membre dans la famille" % self.mode) + messages.add_message(request, messages.ERROR, + "Vous ne pouvez pas %s le dernier titulaire car il reste au moins un autre membre dans la famille" % self.mode) return HttpResponseRedirect(self.get_success_url(), status=303) - # Si c'est le dernier membre, on essaye de supprimer la fiche famille + # Supprimer la famille en supprimant toutes ses dépendances try: - message_erreur = famille.delete() - if isinstance(message_erreur, str): - messages.add_message(request, messages.ERROR, message_erreur) - return HttpResponseRedirect(self.get_success_url(), status=303) + with transaction.atomic(): + logger.info(f"Tentative de suppression de la famille ID: {famille.pk}") + + # Supprimer toutes les dépendances connues + Rattachement.objects.filter(famille=famille).delete() + Inscription.objects.filter(famille=famille).delete() + Consentement.objects.filter(famille=famille).delete() + Utilisateur.objects.filter(famille=famille).delete() + + # # Vérification avant suppression + # liens_restants = self.verifier_liens_famille(famille.pk) + # if liens_restants: + # logger.error( + # f"Échec de suppression de la famille {famille.pk}. Liens restants : {liens_restants}") + # messages.add_message(request, messages.ERROR, + # f"La suppression de la famille est impossible car elle est toujours liée à : {liens_restants}") + # return HttpResponseRedirect(self.get_success_url(), status=303) + + # Suppression de la famille + famille.delete() + + logger.info(f"Famille ID {famille.pk} supprimée avec succès") + messages.add_message(self.request, messages.SUCCESS, + "La fiche famille a été supprimée car il s'agissait du dernier membre de la famille") + + # Supprimer la famille de l'historique des recherches + if "historique_recherche" in request.session: + request.session["historique_recherche"] = [ + dict_historique for dict_historique in request.session["historique_recherche"] + if dict_historique["idfamille"] != famille.pk + ] + request.session.modified = True + except ProtectedError as e: texte_resultats = crud.Formate_liste_objets(objets=e.protected_objects) - messages.add_message(request, messages.ERROR, "La suppression de la famille est impossible car cette famille est rattachée aux données suivantes : %s." % texte_resultats) + logger.error(f"Échec de suppression de la famille {famille.pk} : {texte_resultats}") + messages.add_message(request, messages.ERROR, + "La suppression de la famille est impossible car cette famille est rattachée aux données suivantes : %s." % texte_resultats) return HttpResponseRedirect(self.get_success_url(), status=303) - # Confirmation de la suppression - messages.add_message(self.request, messages.SUCCESS, "La fiche famille a été supprimée car il s'agissait du dernier membre de la famille") + return None - # On supprime la famille de l'historique des recherches - if "historique_recherche" in request.session: - for index, dict_historique in enumerate(request.session["historique_recherche"]): - if dict_historique["idfamille"] == famille.pk: - request.session["historique_recherche"].pop(index) - request.session.modified = True + def verifier_liens_famille(self, idfamille): + """ Vérifie s'il reste des liens bloquant la suppression de la famille """ + from django.db import connection - return None + tables = ["rattachements", "familles_individus_masques", "factures", "paiements", "quotients", "consentements", "contacts_urgence", "inscriptions" + , "devis", "portail_messages", "sondages_repondants", "liens", "pieces", "notes", "cotisations", "rappels", "assurances", "reglements", "destinataires" + , "lectures", "historique", "deductions", "pes_pieces", "destinataires_sms", "portail_renseignements", "prestations", "mandats", "prelevements" + , "payeurs", "ventilation", "attestations_fiscales", "attestations", "locations"] + liens_bloquants = [] + + for table in tables: + with connection.cursor() as cursor: + cursor.execute(f"SELECT COUNT(*) FROM {table} WHERE famille_id = %s", [idfamille]) + count = cursor.fetchone()[0] + if count > 0: + liens_bloquants.append(f"{table} ({count} enregistrements)") + return ", ".join(liens_bloquants) if liens_bloquants else None def get_object(self): if not hasattr(self, "objet"): self.objet = Individu.objects.get(pk=self.kwargs['idindividu']) diff --git a/noethysweb/fiche_individu/forms/individu_portail.py b/noethysweb/fiche_individu/forms/individu_portail.py new file mode 100644 index 00000000..6943f5f9 --- /dev/null +++ b/noethysweb/fiche_individu/forms/individu_portail.py @@ -0,0 +1,137 @@ +# -*- 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 django.forms import ModelForm +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, Hidden, HTML, Fieldset +from crispy_forms.bootstrap import Field +from django_select2.forms import Select2MultipleWidget +from core.forms.base import FormulaireBase +from core.utils.utils_commandes import Commandes +from core.models import Famille, Individu, Rattachement +from fiche_individu.widgets import Internet_identifiant, Internet_mdp + + +class Formulaire(FormulaireBase, ModelForm): + internet_identifiant = forms.CharField(label="Identifiant", required=False, widget=Internet_identifiant(), help_text="Cet identifiant est généré automatiquement, il est conseillé de ne pas le modifier.") + internet_mdp = forms.CharField(label="Mot de passe", required=False, widget=Internet_mdp(), help_text="Des étoiles ***** apparaissent lorsque le mot de passe a été personnalisé par l'utilisateur lors de la première connexion au portail. Dans ce cas, il vous est impossible de récupérer le mot de passe personnalisé, vous devez en générer un nouveau.") + date_expiration_mdp = forms.DateTimeField(label="Date d'expiration", required=False) + individus_masques = forms.ModelMultipleChoiceField(label="Individus masqués", widget=Select2MultipleWidget({"lang": "fr", "data-width": "100%"}), queryset=Individu.objects.none(), required=False, help_text="Vous pouvez sélectionner des membres de la individu qui n'apparaitront pas sur le portail.") + + class Meta: + model = Individu + fields = ["internet_actif", "internet_identifiant", "internet_mdp", "internet_categorie","internet_reservations", "individus_masques", "blocage_impayes_off"] + help_texts = { + "internet_categorie": "Les catégories permettent par exemple d'attribuer des périodes de réservations à certains comptes internet uniquement.", + "internet_reservations": "Décochez cette case pour interdire à cette individu d'accéder aux réservations sur le portail.", + "individus_masques": "Vous pouvez sélectionner des membres de la individu qui n'apparaitront pas sur le portail." + } + + def __init__(self, *args, **kwargs): + idindividu = kwargs.pop("idindividu") + individu = Individu.objects.get(pk=idindividu) + if "instance" not in kwargs: + self.instance = individu + super(Formulaire, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_id = 'individu_portail_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' + # Date d'expiration du mdp + # if individu.utilisateur and hasattr(individu.utilisateur, 'date_expiration_mdp'): + # self.fields["internet_mdp"].widget.attrs["date_expiration_mdp"] = individu.utilisateur.date_expiration_mdp + # else: + # self.fields["internet_mdp"].widget.attrs["date_expiration_mdp"] = None # or a default value + self.fields["internet_mdp"].widget.attrs["date_expiration_mdp"] = individu.utilisateur.date_expiration_mdp + + # Individus masqués + individus = [rattachement.individu_id for rattachement in Rattachement.objects.filter(individu_id=idindividu)] + self.fields["individus_masques"].queryset = Individu.objects.filter(pk__in=individus).order_by("nom") + + # Création des boutons de commande + autres_commandes = [HTML(""" + Envoyer les codes par email + Envoyer les codes par SMS + """)] + if self.mode == "CONSULTATION": + commandes = Commandes(modifier_url="individu_portail_modifier", modifier_args="idfamille=idfamille idindividu=idindividu", + modifier=self.request.user.has_perm("core.individu_portail_modifier"), enregistrer=False, annuler=False, ajouter=False, autres_commandes=autres_commandes) + self.Set_mode_consultation() + else: + commandes = Commandes(annuler_url="{% url 'individu_portail' idfamille=idfamille idindividu=idindividu %}", ajouter=False, autres_commandes=autres_commandes) + + # Affichage + self.helper.layout = Layout( + commandes, + Hidden("idindividu", value=idindividu), + Hidden("date_expiration_mdp", value=individu.utilisateur.date_expiration_mdp), + Fieldset("Activation", + Field("internet_actif"), + ), + Fieldset("Codes d'accès", + Field("internet_identifiant"), + Field("internet_mdp"), + Field("date_expiration_mdp", type="hidden"), + ), + Fieldset("Options", + Field("internet_categorie"), + Field("internet_reservations"), + Field("blocage_impayes_off"), + Field("individus_masques"), + ), + HTML(EXTRA_HTML), + ) + +EXTRA_HTML = """ + + + +
+ +""" \ No newline at end of file diff --git a/noethysweb/fiche_individu/templates/fiche_individu/individu_portail.html b/noethysweb/fiche_individu/templates/fiche_individu/individu_portail.html new file mode 100644 index 00000000..5a69160e --- /dev/null +++ b/noethysweb/fiche_individu/templates/fiche_individu/individu_portail.html @@ -0,0 +1,60 @@ +{% extends "fiche_individu/individu.html" %} +{% load crispy_forms_tags %} +{% load static %} +{% load embed %} + + +{% 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 %} + + {% block box_contenu %} + {% crispy form %} + {% endblock %} + + {% endembed %} +
+ + {# Insertion des modals #} + {% include 'outils/modal_editeur_emails.html' %} + {% include 'outils/modal_editeur_sms.html' %} + {% include 'core/modal_pdf.html' %} + + + +{% endblock %} diff --git a/noethysweb/fiche_individu/templates/fiche_individu/widgets/internet_identifiant.html b/noethysweb/fiche_individu/templates/fiche_individu/widgets/internet_identifiant.html new file mode 100644 index 00000000..2793970b --- /dev/null +++ b/noethysweb/fiche_individu/templates/fiche_individu/widgets/internet_identifiant.html @@ -0,0 +1,37 @@ +{% load static %} + +
+ + {% if not disabled %} +
+ +
+ {% endif %} +
+ + + diff --git a/noethysweb/fiche_individu/templates/fiche_individu/widgets/internet_mdp.html b/noethysweb/fiche_individu/templates/fiche_individu/widgets/internet_mdp.html new file mode 100644 index 00000000..8c6e9e1b --- /dev/null +++ b/noethysweb/fiche_individu/templates/fiche_individu/widgets/internet_mdp.html @@ -0,0 +1,58 @@ +{% load static %} + +
+ + {% if date_expiration_mdp and disabled %} +
+ {% if date_expiration_mdp < maintenant %} + Expiré depuis le {{ date_expiration_mdp|date:'d/m/Y H:i' }} + {% else %} + Valide jusqu'au {{ date_expiration_mdp|date:'d/m/Y H:i' }} + {% endif %} +
+ {% endif %} + {% if not disabled %} +
+ +
+ {% endif %} +
+ + diff --git a/noethysweb/fiche_individu/urls.py b/noethysweb/fiche_individu/urls.py index ca7efec5..a467d597 100644 --- a/noethysweb/fiche_individu/urls.py +++ b/noethysweb/fiche_individu/urls.py @@ -5,7 +5,7 @@ from django.urls import include, path from core.decorators import secure_ajax -from fiche_individu.views import individu, individu_identite, individu_coords, individu_questionnaire, individu_scolarite, individu_inscriptions, \ +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 @@ -98,5 +98,11 @@ path('individus/ajouter_maladie', secure_ajax(individu_maladies.Ajouter_maladie), name='ajax_ajouter_maladie'), path('individus/ajouter_assureur', secure_ajax(individu_assurances.Ajouter_assureur), name='ajax_ajouter_assureur'), path('individus/get_info_transport', secure_ajax(individu_transports.Get_info_transport), name='ajax_get_info_transport'), + # portail + path('individus/individus/portail//', individu_portail.Consulter.as_view(), name='individu_portail'), + path('individus/individus/portail/modifier//', individu_portail.Modifier.as_view(),name='individu_portail_modifier'), + path('individus/individu/regenerer_mdp', secure_ajax(individu_portail.Regenerer_mdp), name='ajax_regenerer_mdp'), + path('individus/individu/regenerer_identifiant_Individu', secure_ajax(individu_portail.Regenerer_identifiant),name='ajax_regenerer_identifiant_individu'), + path('individus/codes_internet_impression_Individu_pdf', secure_ajax(individu_portail.Envoyer_codes), name='ajax_codes_internet_envoyer_Individu'), ] diff --git a/noethysweb/fiche_individu/utils/utils_individu.py b/noethysweb/fiche_individu/utils/utils_individu.py index 3be46715..82f23be3 100644 --- a/noethysweb/fiche_individu/utils/utils_individu.py +++ b/noethysweb/fiche_individu/utils/utils_individu.py @@ -15,6 +15,7 @@ {"code": "maladies", "label": "Maladies", "icone": "fa-stethoscope", "url": "individu_maladies"}, {"code": "medical", "label": "Médical", "icone": "fa-heartbeat", "url": "individu_medical_liste"}, {"code": "assurances", "label": "Assurances", "icone": "fa-shield", "url": "individu_assurances_liste"}, + {"code": "portail", "label": "Portail", "icone": "fa-globe", "url": "individu_portail"}, {"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"}, diff --git a/noethysweb/fiche_individu/views/individu_portail.py b/noethysweb/fiche_individu/views/individu_portail.py new file mode 100644 index 00000000..d861e6b4 --- /dev/null +++ b/noethysweb/fiche_individu/views/individu_portail.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019-2021 Ivan LUCAS. +# Noethysweb, application de gestion multi-activités. +# Distribué sous licence GNU GPL. + +from django.urls import reverse_lazy +from django.http import JsonResponse, HttpResponseRedirect +from django.contrib import messages +from core.views import crud +from core.models import Individu, Utilisateur +from fiche_individu.forms.individu_portail import Formulaire +from fiche_individu.views.individu import Onglet +from fiche_famille.utils import utils_internet + + +def Envoyer_codes(request): + # Récupération des données du formulaire + internet_identifiant = request.POST.get("internet_identifiant") + internet_mdp = request.POST.get("internet_mdp") + idindividu = int(request.POST.get("idindividu")) + + # Récupération des valeurs de fusion + individu = Individu.objects.get(pk=idindividu) + champs = { + "{NOM_INDIVIDU": individu.nom, + "{IDENTIFIANT_INTERNET}": internet_identifiant, + "{MOTDEPASSE_INTERNET}": internet_mdp, + } + return JsonResponse({"categorie": "portail", "champs": champs, "idindividu": idindividu}) + +def Regenerer_identifiant(request): + IDindividu = int(request.POST.get("idindividu")) + identifiant = utils_internet.CreationIdentifiantIndividu(IDindividu=IDindividu) + return JsonResponse({"identifiant": identifiant}) + + +def Regenerer_mdp(request): + mdp, date_expiration_mdp = utils_internet.CreationMDP() + return JsonResponse({"mdp": mdp, "date_expiration_mdp": str(date_expiration_mdp)}) + + +class Consulter(Onglet, crud.Modifier): + form_class = Formulaire + template_name = "fiche_individu/individu_portail.html" + mode = "CONSULTATION" + + def get_context_data(self, **kwargs): + self.object = self.get_object() + context = super(Consulter, self).get_context_data(**kwargs) + context['box_titre'] = "Portail" + context['box_introduction'] = "Cliquez sur le bouton Modifier pour modifier une des informations ci-dessous." + if not self.request.user.has_perm("core.individu_portail_modifier"): + context['box_introduction'] = "Vous n'avez pas l'autorisation de modifier les informations de cette page." + context['onglet_actif'] = "portail" + return context + + def get_object(self): + return Individu.objects.get(pk=self.kwargs['idindividu']) + + def get_form_kwargs(self, **kwargs): + form_kwargs = super(Consulter, self).get_form_kwargs(**kwargs) + form_kwargs["idindividu"] = self.Get_idindividu() + return form_kwargs + + +class Modifier(Consulter): + mode = "EDITION" + + def get_context_data(self, **kwargs): + self.object = self.get_object() + context = super(Modifier, self).get_context_data(**kwargs) + context['box_introduction'] = "Vous pouvez modifier ici les paramètres du compte internet de l'individu." + return context + + def test_func_page(self): + return self.request.user.has_perm("core.individu_portail_modifier") + + def get_success_url(self): + return reverse_lazy("individu_portail", kwargs={'idindividu': self.kwargs.get('idindividu', None)}) + + def post(self, request, **kwargs): + form = Formulaire(request.POST, idindividu=self.kwargs['idindividu'], request=self.request, mode=self.mode) + if not form.is_valid(): + return self.render_to_response(self.get_context_data(form=form)) + + # Importation de l'individu' + individu = self.get_object() + utilisateur = individu.utilisateur + + # Récupération des données + internet_actif = form.cleaned_data.get("internet_actif") + internet_identifiant = form.cleaned_data.get("internet_identifiant") + mdp = form.cleaned_data.get("internet_mdp") + + internet_categorie = form.cleaned_data.get("internet_categorie") + date_expiration_mdp = form.cleaned_data.get("date_expiration_mdp") + individus_masques = form.cleaned_data.get("individus_masques") + blocage_impayes_off = form.cleaned_data.get("blocage_impayes_off") + + # Si changement de l'état actif + if internet_actif != individu.internet_actif: + individu.internet_actif = internet_actif + utilisateur.is_active = internet_actif + + # Si changement de l'identifiant + if internet_identifiant != individu.internet_identifiant: + if Utilisateur.objects.filter(username__iexact=internet_identifiant).exists(): + messages.add_message(request, messages.ERROR, "Cet identifiant a déjà été attribué à une autre individu !") + return self.render_to_response(self.get_context_data(form=form)) + individu.internet_identifiant = internet_identifiant + utilisateur.username = internet_identifiant + + # Si changement de mot de passe + if mdp != individu.internet_mdp: + individu.internet_mdp = mdp + utilisateur.set_password(mdp) + utilisateur.force_reset_password = True + utilisateur.date_expiration_mdp = date_expiration_mdp + + # Si changement de categorie + if internet_categorie != individu.internet_categorie: + individu.internet_categorie = internet_categorie + + # Enregistrement + utilisateur.save() + + individu.blocage_impayes_off = blocage_impayes_off + individu.save() + + # Enregistrement des individus masqués + individu.individus_masques.set(individus_masques) + + return HttpResponseRedirect(reverse_lazy("individu_portail", args=(self.kwargs['idfamille'],self.kwargs['idindividu'],))) diff --git a/noethysweb/fiche_individu/widgets.py b/noethysweb/fiche_individu/widgets.py index bc12ab21..7eef4575 100644 --- a/noethysweb/fiche_individu/widgets.py +++ b/noethysweb/fiche_individu/widgets.py @@ -2,7 +2,7 @@ # Copyright (c) 2019-2021 Ivan LUCAS. # Noethysweb, application de gestion multi-activités. # Distribué sous licence GNU GPL. - +import datetime from django.forms.widgets import Widget from django.template import loader from django.utils.safestring import mark_safe @@ -123,6 +123,46 @@ def value_from_datadict(self, data, files, name): selections = data.getlist(name, []) return ";".join(selections) +class Internet_identifiant(Widget): + template_name = 'fiche_individu/widgets/internet_identifiant.html' + + class Media: + js = ("lib/bootbox/bootbox.min.js",) + + def get_context(self, name, value, attrs=None): + context = dict(self.attrs.items()) + if attrs is not None: + context.update(attrs) + context['name'] = name + if value is not None: + context['value'] = value + return context + + def render(self, name, value, attrs=None, renderer=None): + context = self.get_context(name, value, attrs) + return mark_safe(loader.render_to_string(self.template_name, context)) + + +class Internet_mdp(Widget): + template_name = 'fiche_individu/widgets/internet_mdp.html' + + class Media: + js = ("lib/bootbox/bootbox.min.js",) + + def get_context(self, name, value, attrs=None): + context = dict(self.attrs.items()) + if attrs is not None: + context.update(attrs) + context['name'] = name + context['maintenant'] = datetime.datetime.now() + if value is not None: + context['value'] = value + return context + + def render(self, name, value, attrs=None, renderer=None): + context = self.get_context(name, value, attrs) + return mark_safe(loader.render_to_string(self.template_name, context)) + class SelectionAssurancesAutresFiches(Widget): template_name = 'core/widgets/checktree.html' diff --git a/noethysweb/individus/views/effacer_familles.py b/noethysweb/individus/views/effacer_familles.py index c16eb405..7d0c2a1f 100644 --- a/noethysweb/individus/views/effacer_familles.py +++ b/noethysweb/individus/views/effacer_familles.py @@ -19,7 +19,15 @@ def Effacer_attributs(objet=None, attributs=[]): for attribut in attributs: - setattr(objet, attribut, None) + try: + setattr(objet, attribut, None) + except Exception as e: + # Loggez l'erreur ici si nécessaire + print(f"Erreur lors de l'effacement de l'attribut {attribut} : {e}") + try: + objet.save() # Assurez-vous que l'objet est sauvegardé après l'effacement + except Exception as e: + print(f"Erreur lors de l'enregistrement de l'objet {objet}: {e}") def Effacer(request): @@ -30,28 +38,36 @@ def Effacer(request): if not coches: return JsonResponse({"erreur": "Veuillez cocher au moins une famille dans la liste"}, status=401) - # Vérifications + # Vérifications des anomalies (prestations ou factures récentes) liste_anomalies = [] - for famille in Famille.objects.filter(pk__in=coches).annotate(derniere_prestation=Max("prestation__date"), derniere_facture=Max("facture__date_edition")): - if famille not in liste_anomalies and famille.derniere_prestation and (datetime.date.today() - famille.derniere_prestation).days < 60: + today = datetime.date.today() + for famille in Famille.objects.filter(pk__in=coches).annotate( + derniere_prestation=Max("prestation__date"), + derniere_facture=Max("facture__date_edition") + ): + if famille.derniere_prestation and (today - famille.derniere_prestation).days < 60: liste_anomalies.append(famille) - if famille not in liste_anomalies and famille.derniere_facture and (datetime.date.today() - famille.derniere_facture).days < 60: + if famille.derniere_facture and (today - famille.derniere_facture).days < 60: liste_anomalies.append(famille) + if liste_anomalies: - return JsonResponse({"erreur": "Procédure annulée : Les familles suivantes ont une activité trop récente : %s." % ", ".join([famille.nom for famille in liste_anomalies])}, status=401) + familles_str = ", ".join([famille.nom for famille in liste_anomalies]) + return JsonResponse({"erreur": f"Procédure annulée : Les familles suivantes ont une activité trop récente : {familles_str}."}, status=401) # Effacer les fiches familles for famille in Famille.objects.filter(pk__in=coches): - # Fiche famille + # Mise à jour des champs de la famille famille.nom = "Famille effacée" famille.internet_actif = False Effacer_attributs(famille, [ "caisse", "num_allocataire", "allocataire", "memo", "email_factures_adresses", "email_recus_adresses", "email_depots_adresses", "rue_resid", "cp_resid", "ville_resid", - "secteur", "mail", "mobile", "code_compta"]) + "secteur", "mail", "mobile", "code_compta" + ]) famille.save() + # Suppression des objets liés Note.objects.filter(famille=famille).delete() Piece.objects.filter(famille=famille).delete() QuestionnaireReponse.objects.filter(famille=famille).delete() @@ -103,7 +119,6 @@ def Effacer(request): messages.add_message(request, messages.SUCCESS, "%d fiches familles ont été effacées avec succès" % len(coches)) return JsonResponse({"url": reverse_lazy("effacer_familles")}) - class Page(crud.Page): model = Famille url_liste = "effacer_familles" diff --git a/noethysweb/noethysweb/middleware.py b/noethysweb/noethysweb/middleware.py index 0bfeb902..b6fa0791 100644 --- a/noethysweb/noethysweb/middleware.py +++ b/noethysweb/noethysweb/middleware.py @@ -6,6 +6,9 @@ from django.http import HttpResponseRedirect from django.urls import reverse from portail.utils import utils_secquest +from core.models import Famille + +from core.models import PortailParametre class CustomMiddleware: @@ -13,12 +16,33 @@ def __init__(self, get_response): self.get_response = get_response def __call__(self, request): + # 🔹 Initialisation des paramètres de compte utilisateur dans la session + if 'compte_individu_active' not in request.session or 'compte_famille_active' not in request.session: + compte_individu = PortailParametre.objects.filter(code="compte_individu").first() + compte_famille = PortailParametre.objects.filter(code="compte_famille").first() + + request.session['compte_individu_active'] = bool(compte_individu and compte_individu.valeur == 'True') + request.session['compte_famille_active'] = bool(compte_famille and compte_famille.valeur == 'True') + + request.session.modified = True # 🔹 Assure que Django met bien à jour la session + response = self.get_response(request) - # Oblige la famille à changer son mot de passe + # URL pour changer le mot de passe url_change_password = reverse("password_change") - if request.user.is_authenticated and request.user.categorie == "famille" and request.user.force_reset_password and request.path != url_change_password: - utils_secquest.Generation_secquest(famille=request.user.famille) - return HttpResponseRedirect(url_change_password) + + # Oblige la famille ou l'individu à changer son mot de passe + if request.user.is_authenticated: + # Vérification pour les utilisateurs de type famille + if request.user.categorie == "famille" and request.user.force_reset_password and request.path != url_change_password: + utils_secquest.Generation_secquest(famille=request.user.famille) + return HttpResponseRedirect(url_change_password) + + # Vérification pour les utilisateurs de type individu + elif request.user.categorie == "individu" and request.user.force_reset_password and request.path != url_change_password: + famille = Famille.objects.filter(titulaire_helios_id=request.user.individu).first() + if famille: + utils_secquest.Generation_secquest(famille=famille) # Génération de la question de sécurité pour la famille associée + return HttpResponseRedirect(url_change_password) return response diff --git a/noethysweb/outils/views/editeur_emails_express.py b/noethysweb/outils/views/editeur_emails_express.py index 02d1e5fe..5adcb73c 100644 --- a/noethysweb/outils/views/editeur_emails_express.py +++ b/noethysweb/outils/views/editeur_emails_express.py @@ -3,13 +3,13 @@ # Noethysweb, application de gestion multi-activités. # Distribué sous licence GNU GPL. -import json -from email.utils import parseaddr from django.http import JsonResponse -from django.shortcuts import render -from core.models import ModeleEmail, Mail, Destinataire, Famille, DocumentJoint +from core.models import ModeleEmail, Mail, PieceJointe, Destinataire, Famille, Individu, Contact, AdresseMail, DocumentJoint from outils.utils import utils_email from outils.forms.editeur_emails_express import Formulaire +from django.shortcuts import render +import json, re +from email.utils import parseaddr def Envoyer_email(request): @@ -79,17 +79,6 @@ def Get_view_editeur_email(request): """ Renvoie l'éditeur d'emails dans un modal """ # Récupère d'éventuelles données donnees = json.loads(request.POST.get("donnees")) - - # Importation de la famille - if "idfamille" in donnees: - categorie = "famille" - famille = Famille.objects.get(pk=donnees["idfamille"]) - adresse = famille.mail - else: - categorie = "saisie_libre" - famille = None - adresse = donnees.get("adresse", "") - # Création du mail modele_email = ModeleEmail.objects.filter(categorie=donnees["categorie"], defaut=True).first() mail = Mail.objects.create( @@ -102,12 +91,25 @@ def Get_view_editeur_email(request): utilisateur=request.user, ) - # Création du destinataire et du document joint - destinataire = Destinataire.objects.create(categorie=categorie, famille=famille, adresse=adresse, valeurs=json.dumps(donnees["champs"])) - if "nom_fichier" in donnees: - document_joint = DocumentJoint.objects.create(nom=donnees["label_fichier"], fichier=donnees["nom_fichier"]) - destinataire.documents.add(document_joint) - mail.destinataires.add(destinataire) + if 'idfamille' in donnees: + + # Importation de la famille + famille = Famille.objects.get(pk=donnees["idfamille"]) + # Création du destinataire et du document joint + destinataire = Destinataire.objects.create(categorie="famille", famille=famille, adresse=famille.mail, valeurs=json.dumps(donnees["champs"])) + if "nom_fichier" in donnees: + document_joint = DocumentJoint.objects.create(nom=donnees["label_fichier"], fichier=donnees["nom_fichier"]) + destinataire.documents.add(document_joint) + mail.destinataires.add(destinataire) + + else : #individu + # Importation de l'individu + individu = Individu.objects.get(pk=donnees["idindividu"]) + destinataire = Destinataire.objects.create(categorie="individu", individu=individu, adresse=individu.mail, valeurs=json.dumps(donnees["champs"])) + if "nom_fichier" in donnees: + document_joint = DocumentJoint.objects.create(nom=donnees["label_fichier"], fichier=donnees["nom_fichier"]) + destinataire.documents.add(document_joint) + mail.destinataires.add(destinataire) # Prépare le context context = { @@ -115,4 +117,4 @@ def Get_view_editeur_email(request): "form": Formulaire(instance=mail, request=request), "modeles": ModeleEmail.objects.filter(categorie=request.POST.get("categorie", donnees["categorie"])), } - return render(request, 'outils/editeur_emails_express.html', context) + return render(request, 'outils/editeur_emails_express.html', context) \ No newline at end of file diff --git a/noethysweb/outils/views/editeur_sms_express.py b/noethysweb/outils/views/editeur_sms_express.py index a728e5a8..30f05aa5 100644 --- a/noethysweb/outils/views/editeur_sms_express.py +++ b/noethysweb/outils/views/editeur_sms_express.py @@ -6,7 +6,7 @@ import json from django.http import JsonResponse from django.shortcuts import render -from core.models import ModeleSMS, SMS, DestinataireSMS, Famille +from core.models import ModeleSMS, SMS, DestinataireSMS, Famille , Individu from outils.forms.editeur_sms_express import Formulaire from outils.utils import utils_sms @@ -49,9 +49,6 @@ def Get_view_editeur_sms(request): # Récupère d'éventuelles données donnees = json.loads(request.POST.get("donnees")) - # Importation de la famille - famille = Famille.objects.get(pk=donnees["idfamille"]) - # Création du SMS modele_sms = ModeleSMS.objects.filter(categorie=donnees["categorie"], defaut=True).first() sms = SMS.objects.create( @@ -62,9 +59,20 @@ def Get_view_editeur_sms(request): utilisateur=request.user, ) - # Création du destinataire - destinataire = DestinataireSMS.objects.create(categorie="famille", famille=famille, mobile=famille.mobile, valeurs=json.dumps(donnees["champs"])) - sms.destinataires.add(destinataire) + if 'idfamille' in donnees: + + # Importation de la famille + famille = Famille.objects.get(pk=donnees["idfamille"]) + # Création du destinataire + destinataire = DestinataireSMS.objects.create(categorie="famille", famille=famille, mobile=famille.mobile, valeurs=json.dumps(donnees["champs"])) + sms.destinataires.add(destinataire) + + else : #individu + # Importation de l'individu + individu = Individu.objects.get(pk=donnees["idindividu"]) + # Création du destinataire + destinataire = DestinataireSMS.objects.create(categorie="individu", individu=individu, mobile=individu.mobile, valeurs=json.dumps(donnees["champs"])) + sms.destinataires.add(destinataire) # Prépare le context context = { @@ -72,4 +80,4 @@ def Get_view_editeur_sms(request): "form": Formulaire(instance=sms, request=request), "modeles": ModeleSMS.objects.filter(categorie=request.POST.get("categorie", donnees["categorie"])), } - return render(request, 'outils/editeur_sms_express.html', context) + return render(request, 'outils/editeur_sms_express.html', context) \ No newline at end of file diff --git a/noethysweb/outils/views/historique.py b/noethysweb/outils/views/historique.py index f36e4a17..af64c7c9 100644 --- a/noethysweb/outils/views/historique.py +++ b/noethysweb/outils/views/historique.py @@ -32,10 +32,13 @@ class Liste(Page, crud.Liste): template_name = "outils/historique.html" def get_queryset(self): - conditions = (Q(utilisateur__structures__in=self.request.user.structures.all()) | Q(utilisateur__categorie="famille")) + conditions = ( + Q(utilisateur__structures__in=self.request.user.structures.all()) | + Q(utilisateur__categorie__in=["famille", "individu"]) + ) self.afficher_dernier_mois = utils_parametres.Get(nom="afficher_dernier_mois", categorie="historique", utilisateur=self.request.user, valeur=True) if self.afficher_dernier_mois: - conditions &= Q(horodatage__date__gte=datetime.date.today() - datetime.timedelta(days=7)) + conditions &= Q(horodatage__date__gte=datetime.date.today() - datetime.timedelta(days=14)) return Historique.objects.select_related("famille", "individu", "collaborateur", "utilisateur").filter(conditions, self.Get_filtres("Q")) def get_context_data(self, **kwargs): @@ -71,7 +74,15 @@ def Formate_collaborateur(self, instance, **kwargs): return instance.collaborateur.Get_nom() if instance.collaborateur else "" def Formate_utilisateur(self, instance, **kwargs): - if instance.utilisateur.categorie == "utilisateur": - return instance.utilisateur.username if instance.utilisateur else "" - else: - return instance.utilisateur.famille + if instance.utilisateur: + # Vérifie si la catégorie est "utilisateur" + if instance.utilisateur.categorie == "utilisateur": + return instance.utilisateur.username + # Vérifie si la catégorie est "famille" + elif instance.utilisateur.categorie == "famille": + return instance.utilisateur.famille.nom if instance.utilisateur.famille else "" + # Vérifie si la catégorie est "individu" + elif instance.utilisateur.categorie == "individu": + return instance.utilisateur.individu.Get_nom() if instance.utilisateur.individu else "" + # Si aucune catégorie n'est définie, retourne une chaîne vide + return "" \ No newline at end of file From 52def55406f57a77c48e200e68e45857bd2d919f Mon Sep 17 00:00:00 2001 From: "faouzia.takaya" Date: Wed, 12 Mar 2025 17:17:44 +0100 Subject: [PATCH 04/25] ADD configuration de partie envoi email par lot --- noethysweb/core/admin.py | 37 -- noethysweb/core/models.py | 4 +- noethysweb/core/templatetags/get_item.py | 10 +- noethysweb/core/views/menu.py | 13 +- .../fiche_individu/utils/utils_activites.py | 137 ++++++ .../utils/utils_impression_activites.py | 89 ++++ .../utils/utils_inscriptions.py | 27 +- .../individus/inscriptions_email.html | 2 +- .../templates/outils/editeur_emails.html | 116 ++++- .../outils/editeur_emails_activites.html | 100 +++++ .../outils/editeur_emails_inscriptions.html | 94 ++++ noethysweb/outils/urls.py | 8 +- noethysweb/outils/utils/utils_email.py | 52 ++- noethysweb/outils/views/editeur_emails.py | 423 ++++++++++++------ .../outils/views/editeur_emails_activites.py | 193 ++++++++ .../views/editeur_emails_inscriptions.py | 119 +++++ .../forms/outils_parametres_generaux.py | 67 +++ noethysweb/parametrage/urls.py | 5 +- .../views/outils_parametres_generaux.py | 66 +++ 19 files changed, 1347 insertions(+), 215 deletions(-) create mode 100644 noethysweb/fiche_individu/utils/utils_activites.py create mode 100644 noethysweb/fiche_individu/utils/utils_impression_activites.py create mode 100644 noethysweb/outils/templates/outils/editeur_emails_activites.html create mode 100644 noethysweb/outils/templates/outils/editeur_emails_inscriptions.html create mode 100644 noethysweb/outils/views/editeur_emails_activites.py create mode 100644 noethysweb/outils/views/editeur_emails_inscriptions.py create mode 100644 noethysweb/parametrage/forms/outils_parametres_generaux.py create mode 100644 noethysweb/parametrage/views/outils_parametres_generaux.py diff --git a/noethysweb/core/admin.py b/noethysweb/core/admin.py index ed092e87..8ed069e0 100644 --- a/noethysweb/core/admin.py +++ b/noethysweb/core/admin.py @@ -134,40 +134,3 @@ class Meta: # admin.site.register(Utilisateur_Famille, FamilleAdmin) - -# --------------------------- Individu ---------------------------- - -class IndividuAdmin(UserAdmin): - form = UserEditForm - - # Affiche uniquement les colonnes suivantes dans la liste - list_display = ('username', 'email', 'is_active') - - # Affiche uniquement les filtres suivants - list_filter = ('is_active',) - - # Affiche uniquement les champs suivants - fieldsets = ( - (None, {'fields': ('username', 'password', 'email')}), - ('Permissions', {'fields': ('is_active', 'user_permissions')}), - ('Dates importantes', {'fields': ('last_login', 'date_joined')}) - ) - - def get_queryset(self, request): - """ Affiche uniquement les utilisateurs de type famille """ - qs = super().get_queryset(request) - return qs.filter(categorie="individu") - - def has_add_permission(self, request): - """ Empêche l'ajout d'un utilisateur famille """ - return False - - def has_delete_permission(self, request, obj=None): - """ Empêche la suppression d'un utilisateur famille """ - return False - - -class Utilisateur_Individu(Utilisateur): - class Meta: - proxy = True - verbose_name = "Individu" \ No newline at end of file diff --git a/noethysweb/core/models.py b/noethysweb/core/models.py index 839c6bd0..44287593 100644 --- a/noethysweb/core/models.py +++ b/noethysweb/core/models.py @@ -1538,7 +1538,7 @@ class ModelePrestation(models.Model): structure = models.ForeignKey(Structure, verbose_name="Structure", on_delete=models.PROTECT, blank=True, null=True) # type_quotient = models.ForeignKey(TypeQuotient, verbose_name="Type de QF", blank=True, null=True, on_delete=models.SET_NULL, help_text="Sélectionnez un type de quotient familial ou laissez le champ vide pour tenir compte de tous les types de quotients.") multiprestations = models.TextField(verbose_name="Multi-prestations", blank=True, null=True) - + class Meta: db_table = 'modeles_prestations' verbose_name = "modèle de prestation" @@ -3053,6 +3053,8 @@ class Destinataire(models.Model): categorie = models.CharField(verbose_name="Catégorie", max_length=300, blank=True, null=True) individu = models.ForeignKey(Individu, verbose_name="Individu", blank=True, null=True, on_delete=models.CASCADE) famille = models.ForeignKey(Famille, verbose_name="Famille", blank=True, null=True, on_delete=models.CASCADE) + inscription = models.ForeignKey(Inscription, verbose_name="Inscription", blank=True, null=True,on_delete=models.CASCADE) + activites = models.ForeignKey(Activite, verbose_name="Activites", blank=True, null=True, on_delete=models.CASCADE) collaborateur = models.ForeignKey("Collaborateur", verbose_name="Collaborateur", blank=True, null=True, on_delete=models.CASCADE) contact = models.ForeignKey(Contact, verbose_name="Contact", blank=True, null=True, on_delete=models.CASCADE) liste_diffusion = models.ForeignKey(ListeDiffusion, verbose_name="Liste de diffusion", blank=True, null=True, on_delete=models.CASCADE) diff --git a/noethysweb/core/templatetags/get_item.py b/noethysweb/core/templatetags/get_item.py index fcc6de23..ab19a670 100644 --- a/noethysweb/core/templatetags/get_item.py +++ b/noethysweb/core/templatetags/get_item.py @@ -113,7 +113,15 @@ def calc_delai(date=None, nbre_jours=2): @register.simple_tag def get_item_defaut(dictionnaire, key, defaut=None): - return dictionnaire.get(key, defaut) + try: + if isinstance(dictionnaire, dict): + return dictionnaire.get(key, defaut) + elif hasattr(dictionnaire, '__getitem__'): # Gère d'autres types indexables (ex: QueryDict, objets JSON, etc.) + return dictionnaire[key] + except (KeyError, TypeError): + return defaut # Retourne la valeur par défaut si une erreur survient + + return defaut @register.filter def Convert_liste_to_texte_virgules(liste): diff --git a/noethysweb/core/views/menu.py b/noethysweb/core/views/menu.py index 96753593..e33646c0 100644 --- a/noethysweb/core/views/menu.py +++ b/noethysweb/core/views/menu.py @@ -6,12 +6,13 @@ import copy, importlib from django.urls import reverse_lazy from django.conf import settings +from django.utils.translation import gettext as _ def GetMenuPrincipal(parametres_generaux=None, organisateur=None, user=None): parametres_generaux = parametres_generaux or {} # Fallback to an empty dictionary - menu = Menu(titre="Menu principal", user=user) + menu = Menu(titre=_("Menu principal"), user=user) # ------------------------------------ Accueil ------------------------------------ menu.Add(code="accueil", titre="Accueil", icone="home", toujours_afficher=True) @@ -122,6 +123,7 @@ def GetMenuPrincipal(parametres_generaux=None, organisateur=None, user=None): menu_emails.Add(code="adresses_mail_liste", titre="Adresses d'expédition d'emails", icone="file-text-o", compatible_demo=False) menu_emails.Add(code="signatures_emails_liste", titre="Signatures d'emails", icone="file-text-o") menu_emails.Add(code="listes_diffusion_liste", titre="Listes de diffusion", icone="file-text-o") + menu_emails.Add(code="outils_parametres_generaux", titre="Paramètres généraux", icone="file-text-o") # SMS menu_sms = menu_parametrage.Add(titre="SMS") @@ -167,7 +169,7 @@ def GetMenuPrincipal(parametres_generaux=None, organisateur=None, user=None): # ------------------------------------ Outils ------------------------------------ - menu_outils = menu.Add(code="outils_toc", titre="Outils", icone="wrench", toujours_afficher=parametres_generaux.get("outils_afficher_page_portailuser", False)) + menu_outils = menu.Add(code="outils_toc", titre=_("Outils"), icone="wrench", toujours_afficher=parametres_generaux.get("outils_afficher_page_portailuser", False)) # Statistiques menu_stats = menu_outils.Add(titre="Statistiques") @@ -177,7 +179,7 @@ def GetMenuPrincipal(parametres_generaux=None, organisateur=None, user=None): # Emails menu_emails = menu_outils.Add(titre="Emails") menu_emails.Add(code="contacts_liste", titre="Carnets d'adresses", icone="file-text-o") - menu_emails.Add(code="editeur_emails", titre="Editeur d'Emails", icone="file-text-o") + menu_emails.Add(code="editeur_emails", titre="Envoi des emails par lot", icone="file-text-o") menu_emails.Add(code="emails_liste", titre="Liste des Emails", icone="file-text-o") # SMS @@ -204,7 +206,6 @@ def GetMenuPrincipal(parametres_generaux=None, organisateur=None, user=None): # Sauvegarde menu_sauvegarde = menu_outils.Add(titre="Sauvegardes") menu_sauvegarde.Add(code="sauvegarde_creer", titre="Créer une sauvegarde", icone="file-text-o", compatible_demo=False) - menu_sauvegarde.Add(code="desk_creer", titre="Récupérer les données", icone="file-text-o", compatible_demo=False, superutilisateur_only=True, masquer=not settings.SECRET_EXPORT_DESK) # Restauration menu_restauration = menu_outils.Add(titre="Restauration") @@ -451,7 +452,6 @@ def GetMenuPrincipal(parametres_generaux=None, organisateur=None, user=None): menu_listes.Add(code="liste_reglements", titre="Liste des règlements", icone="file-text-o") menu_listes.Add(code="liste_detaillee_reglements", titre="Liste détaillée des règlements", icone="file-text-o") menu_listes.Add(code="detail_ventilations_reglements", titre="Détail des ventilations des règlements", icone="file-text-o") - menu_listes.Add(code="edition_ventilations_reglements", titre="Edition des ventilations des règlements", icone="file-text-o") menu_listes.Add(code="reglements_lot_factures", titre="Liste des règlements associés à un lot de factures", icone="file-text-o") menu_listes.Add(code="synthese_modes_reglements", titre="Synthèse des modes de règlements", icone="file-text-o") @@ -577,7 +577,7 @@ def __repr__(self): def GetParent(self): return self.parent - def Add(self, code="", titre="", icone="", url=None, toujours_afficher=False, compatible_demo=True, args=None, superutilisateur_only=False, masquer=False): + def Add(self, code="", titre="", icone="", url=None, toujours_afficher=False, compatible_demo=True, args=None): # Créer l'objet menu menu = Menu(self, code=code, titre=titre, icone=icone, url=url, args=args, user=self.user, compatible_demo=compatible_demo, toujours_afficher=toujours_afficher) @@ -591,6 +591,7 @@ def Add(self, code="", titre="", icone="", url=None, toujours_afficher=False, co # Ajouter le menu uniquement s'il doit être affiché if should_display: self.children.append(menu) + return menu # Retourne toujours le menu, même s'il n'est pas affiché def GetUrl(self): diff --git a/noethysweb/fiche_individu/utils/utils_activites.py b/noethysweb/fiche_individu/utils/utils_activites.py new file mode 100644 index 00000000..fbc49f9c --- /dev/null +++ b/noethysweb/fiche_individu/utils/utils_activites.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2025 GIP RECIA. +# Noethysweb, application de gestion multi-activités. +# Distribué sous licence GNU GPL. + +import logging + +logger = logging.getLogger(__name__) +import datetime +from core.utils import utils_infos_individus, utils_texte, utils_conversion, utils_dates, utils_questionnaires +from core.models import Activite, Inscription, Famille +from django.db.models import Count +from fiche_individu.utils import utils_impression_activites + + +class Activites(): + def __init__(self): + """ Récupération de toutes les données de base """ + logger.debug("Recherche toutes les activités...") + self.questionnaires = utils_questionnaires.ChampsEtReponses(categorie="activite") + + def GetDonneesImpression(self, liste_activites=[]): + """ Récupération des données d'impression des activités """ + logger.debug("Recherche des données d'impression pour les activités...") + # Recherche les activités sélectionnées + activites = Activite.objects.filter(pk__in=liste_activites) + + # Récupération du nombre d'inscriptions par activité + activites_data = ( + Inscription.objects + .filter(activite_id__in=liste_activites) + .values("activite_id") + .annotate(nombre_inscriptions=Count("activite_id")) + ) + + # Dictionnaire pour stocker le nombre d'inscriptions par activité + dict_nb_inscriptions = {data["activite_id"]: data["nombre_inscriptions"] for data in activites_data} + + dictDonnees = {} + dictChampsFusion = {} + + for activite in activites: + activite_id = activite.pk + nombre_inscriptions = dict_nb_inscriptions.get(activite_id, 0) + + # Récupérer les familles inscrites à cette activité + familles = ( + Inscription.objects + .filter(activite_id=activite_id, famille__isnull=False) + .select_related("famille", "activite") + .values( + "activite_id", + "activite__nom", + "famille_id", + "famille__nom", + "famille__mail" + ) + .distinct() + ) + + # Mémorisation des données + dictDonnee = { + "{IDACTIVITE}": str(activite.pk), + "{ACTIVITE_NOM_LONG}": activite.nom, + "{ACTIVITE_NOM_COURT}": activite.abrege, + "{NOMBRE_INSCRIPTIONS}": nombre_inscriptions, + "{FAMILLES}": ", ".join( + [f"{famille['famille__nom']} ({famille['famille__mail']})" for famille in familles]) + } + + # Correction : récupérer les inscriptions liées à l'activité + for inscription in Inscription.objects.filter(activite=activite): + if inscription.famille: + for dictReponse in self.questionnaires.GetDonnees(inscription.famille_id): + dictDonnee[dictReponse["champ"]] = dictReponse["reponse"] + + dictDonnees[activite.pk] = dictDonnee + # Champs de fusion pour Email + dictChampsFusion[activite.pk] = {} + for key, valeur in dictDonnee.items(): + if key.startswith("{"): + dictChampsFusion[activite.pk][key] = valeur + + return dictDonnees, dictChampsFusion + + def Impression(self, liste_activites=[], dict_options=None, mode_email=False): + """ Impression des activités """ + # Récupération des données à partir des ID d'activités + logger.debug("Recherche des données d'impression...") + resultat = self.GetDonneesImpression(liste_activites) + + if resultat == False: + return False + dict_activites, dictChampsFusion = resultat + + # Envoi par email + noms_fichiers = {} + if mode_email: + logger.debug("Création des PDF des activités à l'unité...") + + if dict_options and "modele" in dict_options and dict_options["modele"]: + IDmodele = dict_options["modele"].pk + else: + from core.models import ModeleDocument + modele_defaut = ModeleDocument.objects.filter(categorie="activite", defaut=True).first() + IDmodele = modele_defaut.pk if modele_defaut else None + + # Si aucun modèle n'existe, on en crée un et on l'affecte + if IDmodele is None: + logger.warning("Aucun modèle de document par défaut pour 'activite' trouvé. Création en cours...") + + modele_defaut = ModeleDocument.objects.create( + categorie="activite", + defaut=True, + nom="Modèle Activités", + largeur="210", + hauteur="290", + objets=[] + ) + + IDmodele = modele_defaut.pk + + impression = utils_impression_activites.Impression(dict_options=dict_options, IDmodele=IDmodele,generation_auto=False) + for IDactivite, dictActivite in dict_activites.items(): + logger.debug("Création du PDF de l'activité ID%d..." % IDactivite) + impression.Generation_document(dict_donnees={IDactivite: dictActivite}) + noms_fichiers[IDactivite] = {"nom_fichier": impression.Get_nom_fichier(), + "valeurs": impression.Get_champs_fusion_pour_email("activite", IDactivite)} + + # Fabrication du PDF global + nom_fichier = None + if not mode_email: + impression = utils_impression_activites.Impression(dict_donnees=dict_activites, dict_options=dict_options, + IDmodele=dict_options["modele"].pk) + nom_fichier = impression.Get_nom_fichier() + logger.debug("Création des PDF terminée.") + return {"champs": dictChampsFusion, "nom_fichier": nom_fichier, "noms_fichiers": noms_fichiers} diff --git a/noethysweb/fiche_individu/utils/utils_impression_activites.py b/noethysweb/fiche_individu/utils/utils_impression_activites.py new file mode 100644 index 00000000..9fba637f --- /dev/null +++ b/noethysweb/fiche_individu/utils/utils_impression_activites.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2025 GIP RECIA. +# Noethysweb, application de gestion multi-activités. +# Distribué sous licence GNU GPL. + +import logging + +logger = logging.getLogger(__name__) +from core.utils import utils_dates, utils_impression, utils_preferences +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.platypus import Paragraph, Spacer, Table, TableStyle, PageBreak +from reportlab.platypus.flowables import DocAssign, Flowable +from reportlab.lib import colors + + +class Impression(utils_impression.Impression): + def __init__(self, *args, **kwds): + utils_impression.Impression.__init__(self, *args, **kwds) + + def Draw(self): + styleSheet = getSampleStyleSheet() + styleTexte = styleSheet['BodyText'] + styleTexte.fontName = "Helvetica" + styleTexte.fontSize = 9 + styleTexte.borderPadding = 9 + styleTexte.leading = 12 + + listeLabels = [] + for IDactivite, dictValeur in self.dict_donnees.items(): + if isinstance(IDactivite, int): + listeLabels.append((dictValeur["{ACTIVITE_NOM_LONG}"], IDactivite)) + listeLabels.sort() + + for labelDoc, IDactivite in listeLabels: + dictValeur = self.dict_donnees[IDactivite] + self.story.append(DocAssign("ID", IDactivite)) + nomSansCivilite = dictValeur["{ACTIVITE_NOM_LONG}"] + self.story.append(utils_impression.Bookmark(nomSansCivilite, str(IDactivite))) + + # ----------- Insertion du cadre principal -------------- + if self.modele_doc is None: + raise ValueError("Aucun modèle de document n'a été défini pour l'impression.") + + cadre_principal = self.modele_doc.FindObjet("cadre_principal") + if cadre_principal is not None: + # ------------------- TITRE ----------------- + dataTableau = [] + largeursColonnes = [self.taille_cadre[2], ] + dataTableau.append(("Détails de l'Activité",)) + dataTableau.append((u"",)) + style = TableStyle([ + ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), + ('FONT', (0, 0), (0, 0), "Helvetica-Bold", 19), + ('FONT', (0, 1), (0, 1), "Helvetica", 8), + ('LINEBELOW', (0, 0), (0, 0), 0.25, colors.black), + ('ALIGN', (0, 0), (-1, -1), 'LEFT'), + ]) + tableau = Table(dataTableau, largeursColonnes) + tableau.setStyle(style) + self.story.append(tableau) + self.story.append(Spacer(0, 10)) + + # ------------------- TABLEAU CONTENU ----------------- + dataTableau = [] + largeursColonnes = [120, self.taille_cadre[2] - 120] + paraStyle = ParagraphStyle(name="detail", fontName="Helvetica-Bold", fontSize=9) + + dataTableau.append(("Nom de l'Activité", Paragraph(dictValeur["{ACTIVITE_NOM_LONG}"], paraStyle))) + dataTableau.append(("Nom abrégé", Paragraph(dictValeur["{ACTIVITE_NOM_COURT}"], paraStyle))) + dataTableau.append( + ("Nombre d'inscriptions", Paragraph(str(dictValeur["{NOMBRE_INSCRIPTIONS}"]), paraStyle))) + dataTableau.append(("Familles inscrites", Paragraph(dictValeur["{FAMILLES}"], paraStyle))) + + style = TableStyle([ + ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), + ('FONT', (0, 0), (0, -1), "Helvetica", 9), + ('GRID', (0, 0), (-1, -1), 0.25, colors.black), + ('ALIGN', (0, 0), (0, -1), 'RIGHT'), + ]) + tableau = Table(dataTableau, largeursColonnes) + tableau.setStyle(style) + self.story.append(tableau) + + # Saut de page + self.story.append(PageBreak()) + + +if __name__ == u"__main__": + Impression() diff --git a/noethysweb/fiche_individu/utils/utils_inscriptions.py b/noethysweb/fiche_individu/utils/utils_inscriptions.py index b106a5a4..4add8f35 100644 --- a/noethysweb/fiche_individu/utils/utils_inscriptions.py +++ b/noethysweb/fiche_individu/utils/utils_inscriptions.py @@ -71,7 +71,7 @@ def GetDonneesImpression(self, liste_inscriptions=[]): # Ajoute les informations de base individus et familles dictDonnee.update(infosIndividus.GetDictValeurs(mode="famille", ID=inscription.famille_id, formatChamp=True)) - + # Ajoute les réponses des questionnaires for dictReponse in self.questionnaires.GetDonnees(inscription.famille_id): dictDonnee[dictReponse["champ"]] = dictReponse["reponse"] @@ -102,7 +102,30 @@ def Impression(self, liste_inscriptions=[], dict_options=None, mode_email=False) noms_fichiers = {} if mode_email: logger.debug("Création des PDF des inscriptions à l'unité...") - impression = utils_impression_inscription.Impression(dict_options=dict_options, IDmodele=dict_options["modele"].pk, generation_auto=False) + if dict_options and "modele" in dict_options and dict_options["modele"]: + IDmodele = dict_options["modele"].pk + + else: + from core.models import ModeleDocument # Importer si nécessaire + modele_defaut = ModeleDocument.objects.filter(categorie="inscription", defaut=True).first() + IDmodele = modele_defaut.pk if modele_defaut else None + # Si aucun modèle n'existe, on en crée un et on l'affecte + if IDmodele is None: + logger.warning( + "Aucun modèle de document par défaut pour 'inscription' trouvé. Création en cours...") + + modele_defaut = ModeleDocument.objects.create( + categorie="inscription", + defaut=True, + nom="Modèle Inscriptions", + largeur="210", + hauteur="290", + objets=[] + ) + + IDmodele = modele_defaut.pk + + impression = utils_impression_inscription.Impression(dict_options=dict_options, IDmodele=IDmodele, generation_auto=False) for IDinscription, dictInscription in dict_inscriptions.items(): logger.debug("Création du PDF de l'inscription ID%d..." % IDinscription) impression.Generation_document(dict_donnees={IDinscription: dictInscription}) diff --git a/noethysweb/individus/templates/individus/inscriptions_email.html b/noethysweb/individus/templates/individus/inscriptions_email.html index 86d4e365..d28e3d38 100644 --- a/noethysweb/individus/templates/individus/inscriptions_email.html +++ b/noethysweb/individus/templates/individus/inscriptions_email.html @@ -26,7 +26,7 @@ {# Boutons de commandes #}
- Annuler + Annuler
{# Liste des inscriptions générées #} diff --git a/noethysweb/outils/templates/outils/editeur_emails.html b/noethysweb/outils/templates/outils/editeur_emails.html index 9e18c72e..f8ad54e7 100644 --- a/noethysweb/outils/templates/outils/editeur_emails.html +++ b/noethysweb/outils/templates/outils/editeur_emails.html @@ -10,6 +10,15 @@ #div_id_html { margin-bottom: 0px; } + html body div.badge-container { + position: relative; + display: inline-block; + padding: 10px; + background-color: #f8f9fa; + border-radius: 5px; + width: 100%; + } + {% endblock styles %} @@ -20,10 +29,10 @@ {% block contenu_page %} +{% load get_item %}
-
@@ -32,33 +41,94 @@

Destinataires

-
- -
@@ -81,14 +151,14 @@

Envois

{{ destinataire.adresse }}
- {% if destinataire.famille %}{{ destinataire.famille.nom }}{% endif %} - {% if destinataire.collaborateur %}{{ destinataire.collaborateur.Get_nom }}{% endif %} - {% if destinataire.individu %}{{ destinataire.individu.Get_nom }}{% endif %} - {% if destinataire.contact %}{{ destinataire.contact.Get_nom }}{% endif %} + {% if destinataire.categorie == "expediteur" %} Expéditeur en copie : {{ destinataire.adresse }}{% endif %} + {% if destinataire.famille %} Famille: {{ destinataire.famille.nom }}{% endif %} + {% if destinataire.collaborateur %} Collaborateur: {{ destinataire.collaborateur.Get_nom }}{% endif %} + {% if destinataire.individu %} Individu: {{ destinataire.individu.Get_nom }}{% endif %} + {% if destinataire.inscription %} Inscriptions: {{ destinataire.inscription.Get_activite }}{% endif %} + {% if destinataire.activites %} Activites: {{ destinataire.activites.nom }}{% endif %} + {% if destinataire.contact %} Contact: {{ destinataire.contact.Get_nom }}{% endif %}
- {% for document in destinataire.documents.all %} - - {% endfor %} {% endfor %} @@ -115,7 +185,7 @@

Envois

- + + +{% endblock scripts %} + +{% block contenu_page %} +
+ +
+ {% embed 'core/box.html' %} + {% block box_titre %}{{ box_titre }}{% endblock %} + {% block box_introduction %}{{ box_introduction|safe }}{% endblock %} + {% block box_contenu %} + + {# Boutons de commandes #} +
+ + Annuler +
+ + {# Liste des activites générées #} + {% embed 'core/crud/liste.html' %} + {% endembed %} + +
+ + {% endblock box_contenu %} + {% block box_conclusion %}{{ box_conclusion }}{% endblock %} + {% endembed %} +
+ + + +
+ +{% endblock contenu_page %} + diff --git a/noethysweb/outils/templates/outils/editeur_emails_inscriptions.html b/noethysweb/outils/templates/outils/editeur_emails_inscriptions.html new file mode 100644 index 00000000..7737ecfd --- /dev/null +++ b/noethysweb/outils/templates/outils/editeur_emails_inscriptions.html @@ -0,0 +1,94 @@ +{% extends "core/box_in_page.html" %} +{% load crispy_forms_tags %} +{% load static %} +{% load embed %} + +{% block styles %} + {{ block.super }} +{% endblock styles %} + + +{% block scripts %} + {{ block.super }} + + +{% endblock scripts %} + +{% block contenu_page %} +
+ +
+ {% embed 'core/box.html' %} + {% block box_titre %}{{ box_titre }}{% endblock %} + {% block box_introduction %}{{ box_introduction|safe }}{% endblock %} + {% block box_contenu %} + + {# Boutons de commandes #} +
+ + Annuler +
+ + {# Liste des inscriptions générées #} + {% embed 'core/crud/liste.html' %} + {% endembed %} + +
+ + {% endblock box_contenu %} + {% block box_conclusion %}{{ box_conclusion }}{% endblock %} + {% endembed %} +
+ + + +
+ +{% endblock contenu_page %} + diff --git a/noethysweb/outils/urls.py b/noethysweb/outils/urls.py index d75ef873..87363743 100644 --- a/noethysweb/outils/urls.py +++ b/noethysweb/outils/urls.py @@ -11,7 +11,7 @@ editeur_emails_saisie_libre, emails, notes_versions, messages_portail, messagerie_portail, notes, calendrier_annuel, \ demandes_portail, liste_conso_sans_presta, statistiques_portail, correcteur, editeur_sms, editeur_sms_familles, \ editeur_sms_individus, editeur_sms_collaborateurs, editeur_sms_saisie_libre, sms, utilisateurs_bloques, procedures, editeur_sms_express, taches, \ - suivi_reservations, commandes, desk_creer + suivi_reservations, commandes, desk_creer, editeur_emails_inscriptions, editeur_emails_activites urlpatterns = [ @@ -39,6 +39,12 @@ path('outils/editeur_emails/saisie_libre/', editeur_emails_saisie_libre.Liste.as_view(), name='editeur_emails_saisie_libre'), path('outils/editeur_emails/exporter_excel', secure_ajax(editeur_emails.Exporter_excel), name='ajax_editeur_emails_exporter_excel'), + path('outils/editeur_emails/inscriptions', editeur_emails_inscriptions.Liste.as_view(), name='editeur_emails_inscriptions'), + path('outils/editeur_emails/inscriptions_emails_pdf', secure_ajax(editeur_emails_inscriptions.Impression_pdf),name='ajax_inscriptions_emails_pdf'), + path('outils/editeur_emails/activites', editeur_emails_activites.Liste.as_view(),name='editeur_emails_activites'), + path('outils/editeur_emails/activites_emails_pdf', secure_ajax(editeur_emails_activites.Impression_pdf),name='ajax_activites_emails_pdf'), + path('outils/editeur_emails/activites/transferer_activites/', editeur_emails_activites.Transferer_activites, name='transferer_activites'), + path('outils/emails/liste', emails.Liste.as_view(), name='emails_liste'), path('outils/emails/supprimer/', emails.Supprimer.as_view(), name='emails_supprimer'), path('outils/emails/supprimer_plusieurs/', emails.Supprimer_plusieurs.as_view(), name='emails_supprimer_plusieurs'), diff --git a/noethysweb/outils/utils/utils_email.py b/noethysweb/outils/utils/utils_email.py index 8725e366..72e5901e 100644 --- a/noethysweb/outils/utils/utils_email.py +++ b/noethysweb/outils/utils/utils_email.py @@ -37,7 +37,7 @@ def Envoyer_model_mail(idmail=None, request=None): mail = Mail.objects.prefetch_related('destinataires', 'pieces_jointes').select_related("adresse_exp").get(pk=idmail) # Backend CONSOLE (Par défaut) - backend = 'django.core.mail.backends.console.EmailBackend' + backend = 'django.core.mail.backends.dummy.EmailBackend' backend_kwargs = {} # Backend SMTP @@ -122,16 +122,28 @@ def Envoyer_model_mail(idmail=None, request=None): if mail.selection.startswith("NON_ENVOYE_"): destinataires = destinataires[:int(mail.selection.replace("NON_ENVOYE_", ""))] + # --- Éviter l'envoi en double à la même adresse : on dé-duplique --- + unique_destinataires = [] + deja_vues = set() # stockera les adresses normalisées + for d in destinataires: + if not d.adresse: + continue + addr_norm = d.adresse.strip().lower() + if addr_norm not in deja_vues: + deja_vues.add(addr_norm) + unique_destinataires.append(d) + # Envoi de chaque mail index_lot = 1 liste_envois_succes = [] - for destinataire in destinataires: + # --- Boucle d'envoi --- + for destinataire in unique_destinataires: html = mail.html objet = mail.objet if objet == None: objet = "" - # Remplacement des mots-clés + # Fusion des mots-clés spécifiques (destinataire.valeurs) + valeurs_defaut try: valeurs = json.loads(destinataire.valeurs) except: @@ -159,8 +171,15 @@ def Envoyer_model_mail(idmail=None, request=None): html += "

Si vous ne souhaitez plus recevoir nos mails groupés, cliquez sur le lien suivant : Désinscription

" % url_desinscription headers = {"List-Unsubscribe-Post": "List-Unsubscribe=One-Click", "List-Unsubscribe": url_desinscription} - # Création du message - message = EmailMultiAlternatives(subject=objet, body=utils_texte.Textify(html), from_email=mail.adresse_exp.adresse, to=[destinataire.adresse], connection=connection, headers=headers) + # Construction du message + message = EmailMultiAlternatives( + subject=objet, + body=utils_texte.Textify(html), + from_email=mail.adresse_exp.adresse, + to=[destinataire.adresse], + connection=connection, + headers=headers + ) message.mixed_subtype = 'related' message.attach_alternative(html, "text/html") @@ -203,19 +222,26 @@ def Envoyer_model_mail(idmail=None, request=None): if resultat == 1: liste_envois_succes.append(destinataire) - - # Mémorise l'envoi dans l'historique - utils_historique.Ajouter(titre="Envoi d'un email", detail=objet, utilisateur=request.user if request else None, famille=destinataire.famille_id, - individu=destinataire.individu_id, collaborateur=destinataire.collaborateur_id, objet="Email", idobjet=mail.pk, classe="Mail") - - # Pause si envoi par lot activé - if nbre_mails_lot and len(destinataires) > 1: + # Historique + utils_historique.Ajouter( + titre="Envoi d'un email", + detail=objet, + utilisateur=request.user if request else None, + famille=destinataire.famille_id, + individu=destinataire.individu_id, + collaborateur=destinataire.collaborateur_id, + objet="Email", + idobjet=mail.pk, + classe="Mail" + ) + + # Pause si envoi par lot + if nbre_mails_lot and len(unique_destinataires) > 1: if index_lot >= int(nbre_mails_lot): time.sleep(int(duree_pause)) index_lot = 0 index_lot += 1 - connection.close() return liste_envois_succes diff --git a/noethysweb/outils/views/editeur_emails.py b/noethysweb/outils/views/editeur_emails.py index 1f4072bd..516b90ee 100644 --- a/noethysweb/outils/views/editeur_emails.py +++ b/noethysweb/outils/views/editeur_emails.py @@ -1,40 +1,72 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2019-2021 Ivan LUCAS. -# Noethysweb, application de gestion multi-activités. -# Distribué sous licence GNU GPL. +# Copyright (c) 2019-2021 Ivan LUCAS. +# Noethysweb, application de gestion multi-activités. +# Distribué sous licence GNU GPL. import json, os, uuid +from collections import defaultdict + from django.urls import reverse_lazy from django.http import JsonResponse, HttpResponseRedirect from django.db.models import Q, Count from django.contrib import messages from django.conf import settings + from core.views import crud -from core.models import ModeleEmail, Mail, PieceJointe, Destinataire, Famille, Individu, Collaborateur, Contact, SignatureEmail +from core.models import ModeleEmail, Mail, PieceJointe, Destinataire, Famille, Individu, Activite, Inscription, Collaborateur, Contact, SignatureEmail, PortailParametre from core.utils import utils_texte from outils.forms.editeur_emails import Formulaire from outils.utils import utils_email +def RemoveDuplicates(mail): + """ + Supprime en base les doublons d'adresse pour un même Mail, + en ne gardant que la première occurrence pour chaque adresse. + """ + groups = defaultdict(list) + for d in mail.destinataires.all(): + addr_norm = (d.adresse or "").strip().lower() + groups[addr_norm].append(d) + + for addr_norm, items in groups.items(): + if len(items) > 1: + to_keep = items[0] # On garde le premier + for d in items[1:]: + d.delete() + def Get_modele_email(request): - """ Renvoie le contenu HTML d'un modèle d'emails """ + """Renvoie le contenu HTML d'un modèle d'emails""" idmodele = int(request.POST.get("idmodele")) modele = ModeleEmail.objects.filter(pk=idmodele).first() return JsonResponse({"objet": modele.objet, "html": modele.html}) def Get_signature_email(request): - """ Renvoie le contenu HTML d'une signature d'emails """ + """Renvoie le contenu HTML d'une signature d'emails""" idsignature = int(request.POST.get("idsignature")) signature = SignatureEmail.objects.filter(pk=idsignature).first() return JsonResponse({"html": signature.html}) def Exporter_excel(request): + """Exporte la liste des destinataires ou des envois vers un fichier Excel""" idmail = int(request.POST.get("idmail") or 0) mode = request.POST.get("mode") - destinataires = Destinataire.objects.select_related("famille", "individu", "contact", "collaborateur").filter(mail=idmail).order_by("adresse") - if not destinataires: + + # --- Fetch the Mail --- + try: + mail = Mail.objects.get(pk=idmail) + except Mail.DoesNotExist: + return JsonResponse({"erreur": "Mail inexistant."}, status=401) + + destinataires = ( + mail.destinataires + .select_related("famille", "individu", "activites", "inscription", "contact", "collaborateur") + .order_by("adresse") + ) + + if not destinataires.exists(): return JsonResponse({"erreur": "Aucun destinataire n'a été sélectionné"}, status=401) # Création du répertoire et du nom du fichier @@ -42,150 +74,245 @@ def Exporter_excel(request): rep_destination = os.path.join(settings.MEDIA_ROOT, rep_temp) if not os.path.isdir(rep_destination): os.makedirs(rep_destination) - nom_fichier = "export_%s.xlsx" % mode + nom_fichier = f"export_{mode}.xlsx" - # Création du classeur + # Création du classeur Excel (xlsxwriter) import xlsxwriter classeur = xlsxwriter.Workbook(os.path.join(rep_destination, nom_fichier)) feuille = classeur.add_worksheet(mode) - # Création des colonnes + # Colonnes si on exporte les "envois" if mode == "envois": - colonnes = ("Famille", "Individu", "Collaborateur", "Contact", "Email", "Résultat de l'envoi") + colonnes = ( + "Famille", "Individu", "Activité", "Inscription", + "Collaborateur", "Contact", "Email", "Résultat de l'envoi" + ) for num_colonne, label_colonne in enumerate(colonnes): feuille.set_column(num_colonne, num_colonne, 35) feuille.write(0, num_colonne, label_colonne) + # Remplir le classeur for num_ligne, destinataire in enumerate(destinataires): if mode == "destinataires": + # Juste la colonne avec l'adresse feuille.write(num_ligne, 0, destinataire.adresse) if mode == "envois": + # Remplir chaque colonne feuille.write(num_ligne+1, 0, destinataire.famille.nom if destinataire.famille else "") feuille.write(num_ligne+1, 1, destinataire.individu.Get_nom() if destinataire.individu else "") - feuille.write(num_ligne+1, 2, destinataire.collaborateur.Get_nom() if destinataire.collaborateur else "") - feuille.write(num_ligne+1, 3, destinataire.contact.Get_nom() if destinataire.contact else "") - feuille.write(num_ligne+1, 4, destinataire.adresse) - feuille.write(num_ligne+1, 5, destinataire.resultat_envoi) - classeur.close() + feuille.write(num_ligne+1, 2, destinataire.activites.nom if destinataire.activites else "") + feuille.write(num_ligne+1, 3, destinataire.inscription.Get_nom() if destinataire.inscription else "") + feuille.write(num_ligne+1, 4, destinataire.collaborateur.Get_nom() if destinataire.collaborateur else "") + feuille.write(num_ligne+1, 5, destinataire.contact.Get_nom() if destinataire.contact else "") + feuille.write(num_ligne+1, 6, destinataire.adresse) + feuille.write(num_ligne+1, 7, destinataire.resultat_envoi) + classeur.close() return JsonResponse({"nom_fichier": os.path.join(rep_temp, nom_fichier)}) class Page(crud.Page): + """Page principale de l'éditeur d'emails""" model = Mail menu_code = "editeur_emails" template_name = "outils/editeur_emails.html" + def Get_idmail(self): + return self.kwargs.get('pk', None) + def get_context_data(self, **kwargs): context = super(Page, self).get_context_data(**kwargs) - context['page_titre'] = "Editeur d'emails" - context['afficher_menu_brothers'] = True - context['categories'] = {} if not self.Get_idmail() else {item["categorie"]: item["nbre"] for item in Destinataire.objects.values('categorie').filter(mail=self.Get_idmail()).annotate(nbre=Count("pk"))} - mail = Mail.objects.get(pk=self.Get_idmail()) if self.Get_idmail() else None + context['page_titre'] = "Envoi d'emails par lot" + + mail = None + if self.Get_idmail(): + mail = Mail.objects.get(pk=self.Get_idmail()) + context['mail'] = mail + + # --- Supprimer les doublons si mail existe --- + if mail: + RemoveDuplicates(mail) + + # Charger la catégorie, les signatures, etc. categorie = mail.categorie if mail else "saisie_libre" context['modeles'] = ModeleEmail.objects.filter(categorie=categorie) - context['signatures'] = SignatureEmail.objects.filter(Q(structure__in=self.request.user.structures.all()) | Q(structure__isnull=True)) - context['mail'] = mail + context['signatures'] = SignatureEmail.objects.filter( + Q(structure__in=self.request.user.structures.all()) | Q(structure__isnull=True) + ) + + # Stats par catégorie + context['categories'] = {} + if mail: + categories_data = ( + Destinataire.objects + .values('categorie') + .filter(mail=mail) + .annotate(nbre=Count('pk', distinct=True)) + ) + for item in categories_data: + cat = item["categorie"] + if cat == "famille": + count_value = Destinataire.objects.filter(mail=mail, categorie=cat).values('famille').distinct().count() + elif cat == "activites": + count_value = Destinataire.objects.filter(mail=mail, categorie=cat).values('activite').distinct().count() + elif cat == "individu": + count_value = Destinataire.objects.filter(mail=mail, categorie=cat).values('individu').distinct().count() + else: + count_value = item["nbre"] + context['categories'][cat] = count_value + + # Récupérer tous les destinataires (il n'y a plus de doublons, thanks to RemoveDuplicates) + destinataires = [] + nbre_envois_attente = 0 + nbre_envois_reussis = 0 + nbre_envois_echec = 0 + + if mail: + for d in ( + mail.destinataires + .select_related("famille", "individu", "activites", "inscription", "contact", "collaborateur") + .prefetch_related("documents") + .order_by("adresse") + ): + destinataires.append(d) + if not d.resultat_envoi: + nbre_envois_attente += 1 + elif d.resultat_envoi == "ok": + nbre_envois_reussis += 1 + else: + nbre_envois_echec += 1 - # Importe la liste des destinataires (Elimine les doublons) - destinataires, adresses_temp = [], [] - nbre_envois_attente, nbre_envois_reussis, nbre_envois_echec = 0, 0, 0 - if self.Get_idmail(): - for destinataire in Destinataire.objects.select_related("famille", "individu", "contact", "collaborateur").prefetch_related("documents").filter(mail=self.Get_idmail()).order_by("adresse"): - if True:#destinataire.adresse not in adresses_temp: - destinataires.append(destinataire) - adresses_temp.append(destinataire.adresse) - if not destinataire.resultat_envoi: - nbre_envois_attente += 1 - elif destinataire.resultat_envoi == "ok": - nbre_envois_reussis += 1 - else: - nbre_envois_echec += 1 context['destinataires'] = destinataires context['nbre_envois_attente'] = nbre_envois_attente - context['nbre_envois_echec'] = nbre_envois_echec context['nbre_envois_reussis'] = nbre_envois_reussis + context['nbre_envois_echec'] = nbre_envois_echec - # Création du texte d'intro de la box Envois + # Intro envois intro_envois = [] - if not destinataires: intro_envois.append("Aucun envoi") - if nbre_envois_reussis: intro_envois.append("%d envois réussis" % nbre_envois_reussis) if nbre_envois_reussis > 1 else intro_envois.append("1 envoi réussi") - if nbre_envois_attente: intro_envois.append("%d envois en attente" % nbre_envois_attente) if nbre_envois_attente > 1 else intro_envois.append("1 envoi en attente") - if nbre_envois_echec: intro_envois.append("%d envois en échec" % nbre_envois_echec) if nbre_envois_echec > 1 else intro_envois.append("1 envoi en échec") + if not destinataires: + intro_envois.append("Aucun envoi") + if nbre_envois_reussis == 1: + intro_envois.append("1 envoi réussi") + elif nbre_envois_reussis > 1: + intro_envois.append(f"{nbre_envois_reussis} envois réussis") + + if nbre_envois_attente == 1: + intro_envois.append("1 envoi en attente") + elif nbre_envois_attente > 1: + intro_envois.append(f"{nbre_envois_attente} envois en attente") + + if nbre_envois_echec == 1: + intro_envois.append("1 envoi en échec") + elif nbre_envois_echec > 1: + intro_envois.append(f"{nbre_envois_echec} envois en échec") + context['intro_envoi'] = utils_texte.Convert_liste_to_texte_virgules(intro_envois) + # Parametres + parametres_db = PortailParametre.objects.filter( + code__in=[ + "emails_individus_afficher_page", + "emails_familles_afficher_page", + "emails_activites_afficher_page", + "emails_inscriptions_afficher_page", + "emails_collaborateurs_afficher_page", + "emails_liste_diffusion_afficher_page", + ] + ) + parametres = {} + for p in parametres_db: + if isinstance(p.valeur, str): + parametres[p.code] = p.valeur.strip().lower() in ["true", "1"] + + context["parametres"] = parametres return context - def Get_idmail(self): - return self.kwargs.get('pk', None) - def get_form_kwargs(self, **kwargs): - """ Envoie l'idactivite au formulaire """ - form_kwargs = super(Page, self).get_form_kwargs(**kwargs) + """Envoie l'idmail au formulaire""" + form_kwargs = super().get_form_kwargs(**kwargs) form_kwargs["idmail"] = self.Get_idmail() return form_kwargs def post(self, request, *args, **kwargs): - # Validation du formulaire + """Gère la sauvegarde du Mail ou la redirection vers la sélection de destinataires.""" form = Formulaire(request.POST, request=self.request) - if form.is_valid() == False: + if not form.is_valid(): return self.render_to_response(self.get_context_data(form=form)) - idmail = self.kwargs.get("pk") - - # Modification du mail + idmail = self.Get_idmail() if idmail: mail = Mail.objects.get(pk=idmail) - mail.objet = form.cleaned_data.get("objet") - mail.html = form.cleaned_data.get("html") - mail.adresse_exp = form.cleaned_data.get("adresse_exp") - mail.selection = form.cleaned_data.get("selection") + mail.objet = form.cleaned_data["objet"] + mail.html = form.cleaned_data["html"] + mail.adresse_exp = form.cleaned_data["adresse_exp"] + mail.selection = form.cleaned_data["selection"] mail.save() - - # Création du mail - if not idmail: + else: mail = Mail.objects.create( categorie="saisie_libre", - objet=form.cleaned_data.get("objet"), - html=form.cleaned_data.get("html"), - adresse_exp=form.cleaned_data.get("adresse_exp"), - selection=form.cleaned_data.get("selection"), + objet=form.cleaned_data["objet"], + html=form.cleaned_data["html"], + adresse_exp=form.cleaned_data["adresse_exp"], + selection=form.cleaned_data["selection"], utilisateur=request.user, ) - messages.add_message(request, messages.INFO, "Un brouillon du mail a été enregistré") + messages.info(request, "Un brouillon du mail a été enregistré") - # Enregistrement d'une nouvelle pièce jointe + # Gérer les pièces jointes piece = request.FILES.get('pieces_jointes') if piece: piece_jointe = PieceJointe.objects.create(nom=piece._name, fichier=piece) mail.pieces_jointes.add(piece_jointe) - # Suppression d'une pièce suppr_piece_jointe = request.POST.get("suppr_piece_jointe") if suppr_piece_jointe: - piece_jointe = PieceJointe.objects.get(pk=suppr_piece_jointe) - piece_jointe.delete() + PieceJointe.objects.filter(pk=suppr_piece_jointe).delete() - # Redirection vers pages de sélection des destinataires + # Gérer la redirection pour ajouter des destinataires action = request.POST.get("action") - if action == "ajouter_familles": return HttpResponseRedirect(reverse_lazy("editeur_emails_familles", kwargs={"idmail": mail.pk})) - if action == "ajouter_individus": return HttpResponseRedirect(reverse_lazy("editeur_emails_individus", kwargs={"idmail": mail.pk})) - if action == "ajouter_collaborateurs": return HttpResponseRedirect(reverse_lazy("editeur_emails_collaborateurs", kwargs={"idmail": mail.pk})) - if action == "ajouter_contacts": return HttpResponseRedirect(reverse_lazy("editeur_emails_contacts", kwargs={"idmail": mail.pk})) - if action == "ajouter_diffusion": return HttpResponseRedirect(reverse_lazy("editeur_emails_listes_diffusion", kwargs={"idmail": mail.pk})) - if action == "ajouter_saisie_libre": return HttpResponseRedirect(reverse_lazy("editeur_emails_saisie_libre", kwargs={"idmail": mail.pk})) - - # Envoyer + if action == "ajouter_familles": + return HttpResponseRedirect(reverse_lazy("editeur_emails_familles", kwargs={"idmail": mail.pk})) + if action == "ajouter_individus": + return HttpResponseRedirect(reverse_lazy("editeur_emails_individus", kwargs={"idmail": mail.pk})) + if action == "ajouter_activites": + return HttpResponseRedirect(reverse_lazy("editeur_emails_activites")) + if action == "ajouter_inscription": + return HttpResponseRedirect(reverse_lazy("editeur_emails_inscriptions")) + if action == "ajouter_collaborateurs": + return HttpResponseRedirect(reverse_lazy("editeur_emails_collaborateurs", kwargs={"idmail": mail.pk})) + if action == "ajouter_contacts": + return HttpResponseRedirect(reverse_lazy("editeur_emails_contacts", kwargs={"idmail": mail.pk})) + if action == "ajouter_diffusion": + return HttpResponseRedirect(reverse_lazy("editeur_emails_listes_diffusion", kwargs={"idmail": mail.pk})) + if action == "ajouter_saisie_libre": + return HttpResponseRedirect(reverse_lazy("editeur_emails_saisie_libre", kwargs={"idmail": mail.pk})) + + # Gérer l'envoi if action == "envoyer": - if not mail.destinataires: - messages.add_message(request, messages.ERROR, "Vous devez sélectionner au moins un destinataire") - elif not form.cleaned_data.get("adresse_exp"): - messages.add_message(request, messages.ERROR, "Vous devez sélectionner une adresse d'expédition dans la liste") - elif not form.cleaned_data.get("objet"): - messages.add_message(request, messages.ERROR, "Vous devez saisir un objet") - elif not form.cleaned_data.get("html"): - messages.add_message(request, messages.ERROR, "Vous devez saisir un texte") + # Vérifier la validité minimale + if mail.destinataires.count() == 0: + messages.error(request, "Vous devez sélectionner au moins un destinataire") + elif not form.cleaned_data["adresse_exp"]: + messages.error(request, "Vous devez sélectionner une adresse d'expédition dans la liste") + elif not form.cleaned_data["objet"]: + messages.error(request, "Vous devez saisir un objet") + elif not form.cleaned_data["html"]: + messages.error(request, "Vous devez saisir un texte") else: + # Définir la variable expediteur_email + expediteur_email = mail.adresse_exp + # Récupérer tous les destinataires existants de type "expediteur" + destinataires_expediteurs = list(mail.destinataires.filter(categorie="expediteur")) + # Comparaison en mémoire pour éviter l'erreur sur EncryptedEmailField + already_there = any(str(dest.adresse) == str(expediteur_email) for dest in destinataires_expediteurs) + + # Ajouter l'expéditeur seulement s'il n'existe pas déjà + if not already_there: + destinataire_expediteur = Destinataire.objects.create( + categorie="expediteur", + adresse=expediteur_email + ) + mail.destinataires.add(destinataire_expediteur) # Ajout au ManyToMany utils_email.Envoyer_model_mail(idmail=mail.pk, request=request) return HttpResponseRedirect(reverse_lazy("editeur_emails", kwargs={'pk': mail.pk})) @@ -199,66 +326,106 @@ class Modifier(Page, crud.Modifier): form_class = Formulaire - - -# ------------------------------------------------------------------------------------------- +# -------------------------------------------------------------------------- +# Pages d'ajout de destinataires (familles, individus, etc.) +# -------------------------------------------------------------------------- class Page_destinataires(crud.Page): menu_code = "editeur_emails" def get_context_data(self, **kwargs): - context = super(Page_destinataires, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) context['page_titre'] = "Editeur d'emails" context['idmail'] = self.kwargs.get("idmail") context['categorie'] = self.categorie return context def post(self, request, **kwargs): - liste_selections = json.loads(request.POST.get("selections")) - - # Importe le mail + liste_selections = json.loads(request.POST.get("selections") or "[]") mail = Mail.objects.get(pk=self.kwargs.get("idmail")) - # Importe les adresses dict_adresses = {} if self.categorie == "famille": - dict_adresses = {famille.pk: famille.mail for famille in Famille.objects.filter(email_blocage=False)} - if self.categorie == "individu": - dict_adresses = {individu.pk: individu.mail for individu in Individu.objects.all()} - if self.categorie == "collaborateur": - dict_adresses = {collaborateur.pk: collaborateur.mail for collaborateur in Collaborateur.objects.all()} - if self.categorie == "contact": - dict_adresses = {contact.pk: contact.mail for contact in Contact.objects.all()} - - # Importe la liste des destinataires actuels - destinataires = Destinataire.objects.filter(categorie=self.categorie, mail=mail) - liste_id = [getattr(destinataire, "%s_id" % self.categorie) for destinataire in destinataires] - - # Recherche le dernier ID de la table Destinataires - dernier_destinataire = Destinataire.objects.last() - idmax = dernier_destinataire.pk if dernier_destinataire else 0 - - # Ajout des destinataires + dict_adresses = { + f.pk: f.mail + for f in Famille.objects.filter(email_blocage=False) + } + elif self.categorie == "individu": + dict_adresses = { + i.pk: i.mail + for i in Individu.objects.all() + } + elif self.categorie == "activites": + inscriptions = ( + Inscription.objects + .filter(activite_id__in=liste_selections, famille__isnull=False) + .select_related("famille") + ) + for ins in inscriptions: + if ins.famille and ins.famille.mail: + dict_adresses[ins.famille.pk] = ins.famille.mail + elif self.categorie == "collaborateur": + dict_adresses = { + c.pk: c.mail + for c in Collaborateur.objects.all() + } + elif self.categorie == "contact": + dict_adresses = { + c.pk: c.mail + for c in Contact.objects.all() + } + + # Destinataires déjà existants pour cette catégorie + destinataires_existants = mail.destinataires.filter(categorie=self.categorie) + liste_id_existants = [ + getattr(dest, f"{self.categorie}_id") for dest in destinataires_existants + ] + + # ### AJOUT : On construit un set des adresses déjà présentes (pour ce mail), + # afin d'éviter les doublons en base + existing_addresses = set( + str(d.adresse).strip().lower() + for d in mail.destinataires.all() + if d.adresse + ) + + # On repère la fin de la table pour faire le bulk_create + dernier = Destinataire.objects.last() + idmax = dernier.pk if dernier else 0 + liste_ajouts = [] - for id in liste_selections: - if id not in liste_id: - adresse = dict_adresses.get(id, None) + for ident in dict_adresses: + # Ne pas ajouter deux fois la même "famille_id"/"individu_id", etc. + if ident not in liste_id_existants: + adresse = dict_adresses[ident] if adresse: - kwargs = {"{0}_id".format(self.categorie): id, "categorie": self.categorie, "adresse": adresse} - liste_ajouts.append(Destinataire(**kwargs)) + addr_norm = adresse.strip().lower() + # Vérifier si cette adresse est déjà utilisée + if addr_norm not in existing_addresses: + existing_addresses.add(addr_norm) # Évite de l'ajouter encore + if self.categorie == "activites": + liste_ajouts.append(Destinataire( + categorie="activites", + famille_id=ident, + adresse=adresse + )) + else: + kwargs2 = { + f"{self.categorie}_id": ident, + "categorie": self.categorie, + "adresse": adresse, + } + liste_ajouts.append(Destinataire(**kwargs2)) + if liste_ajouts: - # Enregistre les destinataires Destinataire.objects.bulk_create(liste_ajouts) - # Associe les destinataires au mail - destinataires = Destinataire.objects.filter(pk__gt=idmax) - ThroughModel = Mail.destinataires.through - ThroughModel.objects.bulk_create([ThroughModel(mail_id=mail.pk, destinataire_id=destinataire.pk) for destinataire in destinataires]) - - # Suppression des destinataires - for id in liste_id: - if id not in liste_selections: - kwargs = {"{0}_id".format(self.categorie): id, "mail": mail} - destinataire = Destinataire.objects.get(**kwargs) - destinataire.delete() + nouveaux = Destinataire.objects.filter(pk__gt=idmax) + mail.destinataires.add(*nouveaux) + + # Suppression si décoché + for ident in liste_id_existants: + if ident not in dict_adresses: + qs_suppr = destinataires_existants.filter(**{f"{self.categorie}_id": ident}) + qs_suppr.delete() return HttpResponseRedirect(reverse_lazy("editeur_emails", kwargs={'pk': mail.pk})) diff --git a/noethysweb/outils/views/editeur_emails_activites.py b/noethysweb/outils/views/editeur_emails_activites.py new file mode 100644 index 00000000..bb89a123 --- /dev/null +++ b/noethysweb/outils/views/editeur_emails_activites.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2025 Faouzia TEKAYA. +# Noethysweb, application de gestion multi-activités. +# Distribué sous licence GNU GPL. + +import logging +logger = logging.getLogger(__name__) +from django.urls import reverse_lazy, reverse +from core.views.mydatatableview import MyDatatable, columns, helpers +from core.views import crud +from django.db.models import Count, F +from core.models import Mail, DocumentJoint, Inscription, Destinataire, Activite +from django.http import JsonResponse +from django.contrib import messages +import json +def Transferer_activites(request): + if request.method != "POST": + return JsonResponse({"erreur": "Méthode invalide."}, status=405) + + data = json.loads(request.POST.get("activites_selectionnees", "[]")) + if not data: + return JsonResponse({"erreur": "Aucune activité sélectionnée."}, status=401) + + # Créer le Mail + mail = Mail.objects.create( + categorie="activites", + objet="Informations sur l'activité", + html="", + adresse_exp=request.user.Get_adresse_exp_defaut(), + selection="NON_ENVOYE", + verrouillage_destinataires=False, + utilisateur=request.user, + ) + + # Trouver toutes les familles ayant une inscription pour ces activités + inscriptions = ( + Inscription.objects + .filter(activite_id__in=data, famille__isnull=False) + .select_related("famille") + ) + + # Ajouter un Destinataire par famille + familles_dict = {} + for insc in inscriptions: + if insc.famille and insc.famille.mail: + familles_dict[insc.famille.pk] = insc.famille.mail + + liste_anomalies = [] + for fam_id, email in familles_dict.items(): + if email: + destinataire = Destinataire.objects.create( + categorie="activites", + famille_id=fam_id, + adresse=email, + ) + mail.destinataires.add(destinataire) + else: + # Facultatif : signaler l'absence d'email + liste_anomalies.append(str(fam_id)) + + if liste_anomalies: + messages.error( + request, + "Familles sans email : %s" % ", ".join(liste_anomalies) + ) + + # Retourne l’URL pour l’ouvrir dans l’éditeur d’emails + url = reverse_lazy("editeur_emails", kwargs={"pk": mail.pk}) + return JsonResponse({"url": url}) + +def Impression_pdf(request): + # Récupération des activites cochées + activites_cochees = json.loads(request.POST.get("activites_cochees")) + if not activites_cochees: + return JsonResponse({"erreur": "Veuillez cocher au moins une activité dans la liste"}, status=401) + + # Création des PDF + from fiche_individu.utils import utils_activites + activite = utils_activites.Activites() + resultat = activite.Impression(liste_activites=activites_cochees, dict_options={},mode_email=True) # Mise à jour du paramètre + if not resultat: + return JsonResponse({"success": False}, status=401) + + # Création du mail + logger.debug("Création d'un nouveau mail...") + mail = Mail.objects.create( + categorie="activites", + objet="Notification d'activité", + html="", + adresse_exp=request.user.Get_adresse_exp_defaut(), + selection="NON_ENVOYE", + verrouillage_destinataires=True, + utilisateur=request.user, + ) + + # Création des destinataires et des documents joints + logger.debug("Enregistrement des destinataires et documents joints...") + liste_anomalies = [] + for IDcotisation, donnees in resultat["noms_fichiers"].items(): + inscriptions = Inscription.objects.select_related('famille').filter(activite_id=IDcotisation) + + for cotisation in inscriptions: + if cotisation.famille.mail: + destinataire = Destinataire.objects.create(categorie="famille", famille=cotisation.famille,adresse=cotisation.famille.mail) + document_joint = DocumentJoint.objects.create(nom="Activite", fichier=donnees["nom_fichier"]) + destinataire.documents.add(document_joint) + mail.destinataires.add(destinataire) + + else: + liste_anomalies.append(cotisation.famille.nom) + + if liste_anomalies: + messages.add_message(request, messages.ERROR, "Adresses mail manquantes : %s" % ", ".join(liste_anomalies)) + + # Création de l'URL pour ouvrir l'éditeur d'emails + logger.debug("Redirection vers l'éditeur d'emails...") + url = reverse_lazy("editeur_emails", kwargs={'pk': mail.pk}) + return JsonResponse({"url": url}) + +class Page(crud.Page): + model = Activite + url_liste = "editeur_emails_activites" + menu_code = "editeur_emails_activites" + +from outils.views.editeur_emails import Page_destinataires +class Liste(Page_destinataires, crud.Liste): + template_name = "outils/editeur_emails_activites.html" + model = Activite + categorie = "activites" + + def get_queryset(self): + queryset = ( + Inscription.objects + .filter(famille__mail__isnull=False) + .annotate( + id_activite=F("activite__idactivite"), # <-- ADDED + nom_activite=F("activite__nom"), + nom_groupe=F("groupe__nom"), + ) + .values("id_activite", "nom_activite", "nom_groupe") + .annotate(nombre_inscriptions=Count("idinscription")) + .order_by("nom_activite", "nom_groupe") + ) + return queryset + + def get_context_data(self, **kwargs): + + context = super(Liste, self).get_context_data(**kwargs) + context['page_titre'] = "Envoi des Inscrits aux Activites par Email" + context['box_titre'] = "Envoyer des Inscrits aux Activites par Email en lot" + context['box_introduction'] = "Cochez les activites puis cliquez sur le bouton Aperçu." + context['onglet_actif'] = "activites_email" + context['active_checkbox'] = True + context['bouton_supprimer'] = False + return context + + class datatable_class(MyDatatable): + filtres = ['id_activite','activite__nom', 'groupe__nom', 'nombre_inscriptions'] + model = None + check = columns.CheckBoxSelectColumn(label="") + c_activite_id = columns.TextColumn("Activité", sources=["id_activite"]) + + c_activite = columns.TextColumn("Activité", sources=["nom_activite"]) + c_groupes = columns.TextColumn("Groupes", sources=["nom_groupe"]) + c_nb = columns.TextColumn("Nombre d'inscriptions", sources=["nombre_inscriptions"]) + + class Meta: + structure_template = MyDatatable.structure_template + columns = ["check","c_activite_id" ,"c_activite", "c_groupes", "c_nb"] + + def get_object_pk(self, row): + # Return the real integer ID + row=row["id_activite"] + return row + + def prepare_results(self, queryset): + results = [] + for row in queryset: + results.append({ + "id_activite": row["id_activite"], + "nom_activite": row["nom_activite"], + "nom_groupe": row["nom_groupe"], + "nombre_inscriptions": row["nombre_inscriptions"], + }) + return results + + def Formate_statut(self, instance, *args, **kwargs): + if instance.statut == "attente": + return " Attente" + elif instance.statut == "refus": + return " Refus" + else: + return " Valide" \ No newline at end of file diff --git a/noethysweb/outils/views/editeur_emails_inscriptions.py b/noethysweb/outils/views/editeur_emails_inscriptions.py new file mode 100644 index 00000000..6dc9282f --- /dev/null +++ b/noethysweb/outils/views/editeur_emails_inscriptions.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2025 Faouzia TEKAYA. +# Noethysweb, application de gestion multi-activités. +# Distribué sous licence GNU GPL. + +import logging + +logger = logging.getLogger(__name__) +from django.urls import reverse_lazy, reverse +from core.views.mydatatableview import MyDatatable, columns, helpers +from core.views import crud +from django.db.models import Q +from core.models import Mail, DocumentJoint, Inscription, Destinataire +from django.http import JsonResponse +from django.contrib import messages +import json + +def Impression_pdf(request): + # Récupération des inscriptions cochées + inscriptions_cochees = json.loads(request.POST.get("inscriptions_cochees")) + if not inscriptions_cochees: + return JsonResponse({"erreur": "Veuillez cocher au moins une inscription dans la liste"}, status=401) + + # Création des PDF + from fiche_individu.utils import utils_inscriptions + inscription = utils_inscriptions.Inscriptions() + resultat = inscription.Impression(liste_inscriptions=inscriptions_cochees, dict_options={}, mode_email=True) + + if not resultat: + return JsonResponse({"success": False}, status=401) + + # Création du mail + logger.debug("Création d'un nouveau mail...") + mail = Mail.objects.create( + categorie="inscription", + objet="Notification d'inscription", + html="", + adresse_exp=request.user.Get_adresse_exp_defaut(), + selection="NON_ENVOYE", + verrouillage_destinataires=True, + utilisateur=request.user, + ) + + # Création des destinataires et des documents joints + logger.debug("Enregistrement des destinataires et documents joints...") + liste_anomalies = [] + for IDcotisation, donnees in resultat["noms_fichiers"].items(): + cotisation = Inscription.objects.select_related('famille').get(pk=IDcotisation) + + if cotisation.famille.mail: + destinataire = Destinataire.objects.create(categorie="famille", famille=cotisation.famille,adresse=cotisation.famille.mail) + document_joint = DocumentJoint.objects.create(nom="Inscription", fichier=donnees["nom_fichier"]) + destinataire.documents.add(document_joint) + mail.destinataires.add(destinataire) + else: + liste_anomalies.append(cotisation.famille.nom) + + if liste_anomalies: + messages.add_message(request, messages.ERROR, "Adresses mail manquantes : %s" % ", ".join(liste_anomalies)) + + # Création de l'URL pour ouvrir l'éditeur d'emails + logger.debug("Redirection vers l'éditeur d'emails...") + url = reverse_lazy("editeur_emails", kwargs={'pk': mail.pk}) + return JsonResponse({"url": url}) + +class Page(crud.Page): + model = Inscription + url_liste = "editeur_emails_inscriptions" + menu_code = "editeur_emails_inscriptions" + +from outils.views.editeur_emails import Page_destinataires +class Liste(Page_destinataires, crud.Liste): + template_name = "outils/editeur_emails_inscriptions.html" + model = Inscription + categorie = "inscriptions" + + def get_queryset(self): + condition = Q(activite__structure__in=self.request.user.structures.all()) + return Inscription.objects.select_related("famille", "individu", "groupe", "categorie_tarif", + "activite").filter(condition) + + def get_context_data(self, **kwargs): + context = super(Liste, self).get_context_data(**kwargs) + context['page_titre'] = "Envoi des Inscriptions par Email" + context['box_titre'] = "Envoyer des Inscriptions par Email en lot" + context['box_introduction'] = "Cochez des inscriptions puis cliquez sur le bouton Aperçu." + context['onglet_actif'] = "inscriptions_email" + context['active_checkbox'] = True + context['bouton_supprimer'] = False + return context + + class datatable_class(MyDatatable): + filtres = ["igenerique:individu", "fgenerique:famille", 'idinscription', 'date_debut', 'date_fin', + 'activite__nom', 'groupe__nom', 'statut', 'categorie_tarif__nom'] + check = columns.CheckBoxSelectColumn(label="") + activite = columns.TextColumn("Activité", sources=['activite__nom']) + groupe = columns.TextColumn("Groupe", sources=['groupe__nom']) + categorie_tarif = columns.TextColumn("Catégorie de tarif", sources=['categorie_tarif__nom']) + individu = columns.CompoundColumn("Individu", sources=['individu__nom', 'individu__prenom']) + famille = columns.TextColumn("Famille", sources=['famille__nom']) + + class Meta: + structure_template = MyDatatable.structure_template + columns = ["check", "idinscription", 'date_debut', 'date_fin', 'individu', 'famille', 'activite', 'groupe', + 'categorie_tarif', 'statut'] + processors = { + 'date_debut': helpers.format_date('%d/%m/%Y'), + 'date_fin': helpers.format_date('%d/%m/%Y'), + 'statut': 'Formate_statut', + } + ordering = ['date_debut'] + + def Formate_statut(self, instance, *args, **kwargs): + if instance.statut == "attente": + return " Attente" + elif instance.statut == "refus": + return " Refus" + else: + return " Valide" \ No newline at end of file diff --git a/noethysweb/parametrage/forms/outils_parametres_generaux.py b/noethysweb/parametrage/forms/outils_parametres_generaux.py new file mode 100644 index 00000000..3013552e --- /dev/null +++ b/noethysweb/parametrage/forms/outils_parametres_generaux.py @@ -0,0 +1,67 @@ +# Copyright (c) 2024 GIP RECIA. +# Noethysweb, application de gestion multi-activités. +# Distribué sous licence GNU GPL. + +from django import forms +from core.forms.base import FormulaireBase +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, Hidden, Submit, HTML, Fieldset, Div, ButtonHolder +from crispy_forms.bootstrap import Field, InlineRadios +from core.utils.utils_commandes import Commandes +from core.models import PortailParametre +from core.utils.utils_parametres_generaux import LISTE_PARAMETRES + + +LISTE_RUBRIQUES = [ + ("Envoi emails par lots", ["emails_individus_afficher_page", "emails_familles_afficher_page", "emails_activites_afficher_page" + ,"emails_inscriptions_afficher_page","emails_collaborateurs_afficher_page","emails_liste_diffusion_afficher_page"]), +] + +class Formulaire(FormulaireBase, forms.Form): + def __init__(self, *args, **kwargs): + super(Formulaire, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_id = 'emails_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 'outils_parametres_generaux' %}", ajouter=False)) + + dict_parametres = {parametre.code: parametre for parametre in LISTE_PARAMETRES} + for parametre_db in PortailParametre.objects.all(): + if parametre_db.code in dict_parametres: + dict_parametres[parametre_db.code].From_db(parametre_db.valeur) + + # Création des fields + for titre_rubrique, liste_parametres in LISTE_RUBRIQUES: + liste_fields = [] + for code_parametre in liste_parametres: + self.fields[code_parametre] = dict_parametres[code_parametre].Get_ctrl() + self.fields[code_parametre].initial = dict_parametres[code_parametre].valeur + liste_fields.append(Field(code_parametre)) + self.helper.layout.append(Fieldset(titre_rubrique, *liste_fields)) + + self.helper.layout.append(HTML("
")) + + self.helper.layout.append(HTML(EXTRA_SCRIPT)) + + def clean(self): + cleaned_data = super().clean() + emails_activites = cleaned_data.get("emails_activites") + return cleaned_data + + +EXTRA_SCRIPT = """ + +
+""" \ No newline at end of file diff --git a/noethysweb/parametrage/urls.py b/noethysweb/parametrage/urls.py index 9d7b1d9b..1464f1f3 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, parametres_generaux + activites_evenements_categories, activites_import_export, parametres_generaux, outils_parametres_generaux urlpatterns = [ @@ -495,7 +495,8 @@ path('parametrage/adresses_mail/ajouter', adresses_mail.Ajouter.as_view(), name='adresses_mail_ajouter'), path('parametrage/adresses_mail/modifier/', adresses_mail.Modifier.as_view(), name='adresses_mail_modifier'), path('parametrage/adresses_mail/supprimer/', adresses_mail.Supprimer.as_view(), name='adresses_mail_supprimer'), - + # Paramètres généraux emails + path('parametrage/adresses_mail/outils_parametres_generaux/', outils_parametres_generaux.Modifier.as_view(),name='outils_parametres_generaux'), # Signatures d'emails path('parametrage/signatures_emails/liste', signatures_emails.Liste.as_view(), name='signatures_emails_liste'), path('parametrage/signatures_emails/ajouter', signatures_emails.Ajouter.as_view(), name='signatures_emails_ajouter'), diff --git a/noethysweb/parametrage/views/outils_parametres_generaux.py b/noethysweb/parametrage/views/outils_parametres_generaux.py new file mode 100644 index 00000000..d51b273c --- /dev/null +++ b/noethysweb/parametrage/views/outils_parametres_generaux.py @@ -0,0 +1,66 @@ +# Copyright (c) 2024 GIP RECIA. +# Noethysweb, application de gestion multi-activités. +# Distribué sous licence GNU GPL. + +from django.urls import reverse_lazy +from django.core.cache import cache +from django.views.generic import TemplateView +from django.http import HttpResponseRedirect +from core.views.base import CustomView +from parametrage.forms.outils_parametres_generaux import Formulaire +import django.contrib.messages +from core.models import PortailParametre + + +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 Généraux" + context['box_titre'] = "Paramètres Envoi Emails en LOT" + context['box_introduction'] = "Ajustez les paramètres de l'envoi d'emails en lot et cliquez sur le bouton Enregistrer." + + # # Récupérer ou initialiser les valeurs de session à partir de la base de données + if 'emails_activites_active' not in self.request.session : + # Tentative de récupération des paramètres de la base de données + emails_activites = PortailParametre.objects.filter(code="emails_activites").first() + + # Initialisez les valeurs de session en fonction des valeurs de la base de données ou par défaut sur False si elles ne sont pas trouvées + self.request.session['emails_activites_active'] = bool(emails_activites and emails_activites.valeur == 'True') + + # Si les paramètres n'existent pas dans la base de données, définissez-les sur False uniquement dans la session (sans créer de nouvelles entrées) + if not emails_activites: + self.request.session['emails_activites_active'] = False + + # Transmettre les valeurs de session au formulaire + initial_data = { + "emails_activites": self.request.session.get('emails_activites_active', True), + } + context['form'] = Formulaire(initial=initial_data) + return context + + def post(self, request, **kwargs): + form = Formulaire(request.POST, request=self.request) + if not form.is_valid(): + return self.render_to_response(self.get_context_data(form=form)) + + # Enregistrement :Récupérer les paramètres existants dans la base de données + 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['emails_activites_active'] = form.cleaned_data.get("emails_activites", False) + cache.delete("parametres_portail") + + django.contrib.messages.success(request, 'Paramètres enregistrés') + return HttpResponseRedirect(reverse_lazy("outils_parametres_generaux")) \ No newline at end of file From 086a7c1fde56e6f2ce9381c034c413120f78529f Mon Sep 17 00:00:00 2001 From: "faouzia.takaya" Date: Wed, 12 Mar 2025 17:39:33 +0100 Subject: [PATCH 05/25] ADD configuration de contact de facturation --- noethysweb/core/models.py | 1 + noethysweb/fiche_famille/forms/famille_divers.py | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/noethysweb/core/models.py b/noethysweb/core/models.py index 44287593..368e0139 100644 --- a/noethysweb/core/models.py +++ b/noethysweb/core/models.py @@ -1859,6 +1859,7 @@ class Famille(models.Model): mobile_blocage = models.BooleanField(verbose_name="La famille ne souhaite pas recevoir de SMS groupés", default=False, help_text="L'éditeur de SMS groupés du menu Outils ne proposera pas cette famille dans les destinataires.") individus_masques = models.ManyToManyField(Individu, verbose_name="Individus masqués", related_name="individus_masques", blank=True) blocage_impayes_off = models.BooleanField(verbose_name="Ne jamais appliquer le blocage des réservations si impayés", 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.") + contact_facturation = models.ForeignKey(Individu, verbose_name="Contact facturation", related_name="contact_facturation", on_delete=models.SET_NULL, blank=True, null=True) class Meta: db_table = 'familles' diff --git a/noethysweb/fiche_famille/forms/famille_divers.py b/noethysweb/fiche_famille/forms/famille_divers.py index 1a1bc2dc..0128beb9 100644 --- a/noethysweb/fiche_famille/forms/famille_divers.py +++ b/noethysweb/fiche_famille/forms/famille_divers.py @@ -21,7 +21,7 @@ class Meta: model = Famille fields = ["mail", "mobile", "memo", "code_compta", "titulaire_helios", "tiers_solidaire", "idtiers_helios", "natidtiers_helios", "reftiers_helios", "cattiers_helios", "natjur_helios", "facturation_nom", "facturation_rue_resid", "facturation_cp_resid", "facturation_ville_resid", - "email_blocage", "mobile_blocage"] + "email_blocage", "mobile_blocage","contact_facturation"] widgets = { "memo": forms.Textarea(attrs={'rows': 3}), "facturation_rue_resid": forms.Textarea(attrs={'rows': 2}), @@ -44,6 +44,9 @@ def __init__(self, *args, **kwargs): self.fields["mail"].choices = [(None, "---------")] + [(rattachement.individu.mail, rattachement.individu.mail) for rattachement in rattachements if rattachement.individu.mail] self.fields["mobile"].choices = [(None, "---------")] + [(rattachement.individu.tel_mobile, "%s (%s)" % (rattachement.individu.tel_mobile, rattachement.individu.Get_nom())) for rattachement in rattachements if rattachement.individu.tel_mobile] + # Facturation + self.fields["contact_facturation"].choices = [(None, "---------")] + [(rattachement.individu.idindividu, rattachement.individu) for rattachement in rattachements] + # Titulaire hélios self.fields["titulaire_helios"].choices = [(None, "---------")] + [(rattachement.individu.idindividu, rattachement.individu) for rattachement in rattachements] self.fields["tiers_solidaire"].choices = [(None, "---------")] + [(rattachement.individu.idindividu, rattachement.individu) for rattachement in rattachements] @@ -71,6 +74,9 @@ def __init__(self, *args, **kwargs): Fieldset("Comptabilité", Field("code_compta"), ), + Fieldset("Facturation", + Field("contact_facturation"), + ), Fieldset("Données tiers pour Hélios", Field("titulaire_helios"), Field("tiers_solidaire"), From 914e59753b403604e4de7cce78aaa5350f7f0ea1 Mon Sep 17 00:00:00 2001 From: "faouzia.takaya" Date: Wed, 12 Mar 2025 18:09:26 +0100 Subject: [PATCH 06/25] ADD configuration de la partie messagerie --- noethysweb/core/models.py | 19 +- .../famille_messagerie_portail.html | 309 ++++++++++++++---- .../fiche_famille/templatetags/famille.py | 17 + noethysweb/fiche_famille/urls.py | 2 + .../views/famille_messagerie_portail.py | 289 +++++++++++++++- 5 files changed, 547 insertions(+), 89 deletions(-) create mode 100644 noethysweb/fiche_famille/templatetags/famille.py diff --git a/noethysweb/core/models.py b/noethysweb/core/models.py index 368e0139..7b0d522b 100644 --- a/noethysweb/core/models.py +++ b/noethysweb/core/models.py @@ -3216,17 +3216,26 @@ def __str__(self): class PortailMessage(models.Model): idmessage = models.AutoField(verbose_name="ID", db_column='IDmessage', primary_key=True) - famille = models.ForeignKey(Famille, verbose_name="Famille", on_delete=models.CASCADE) - structure = models.ForeignKey(Structure, verbose_name="Structure", on_delete=models.CASCADE) - utilisateur = models.ForeignKey(Utilisateur, verbose_name="Utilisateur", blank=True, null=True, on_delete=models.PROTECT) + famille = models.ForeignKey(Famille, verbose_name="Famille", on_delete=models.CASCADE, db_index=True) + individu = models.ForeignKey(Individu, verbose_name="Individu", on_delete=models.CASCADE, null=True, db_index=True) + structure = models.ForeignKey(Structure, verbose_name="Structure", on_delete=models.CASCADE, db_index=True) + utilisateur = models.ForeignKey(Utilisateur, verbose_name="Utilisateur", blank=True, null=True,on_delete=models.PROTECT) texte = models.TextField(verbose_name="Texte") - date_creation = models.DateTimeField(verbose_name="Date de création", auto_now_add=True) - date_lecture = models.DateTimeField(verbose_name="Date de lecture", max_length=200, blank=True, null=True) + date_creation = models.DateTimeField(verbose_name="Date de création", auto_now_add=True, db_index=True) + date_lecture = models.DateTimeField(verbose_name="Date de lecture", max_length=200, blank=True, null=True,db_index=True) class Meta: db_table = 'portail_messages' verbose_name = "message" verbose_name_plural = "messages" + indexes = [ + models.Index(fields=["famille"]), + models.Index(fields=["individu"]), + models.Index(fields=["structure"]), + models.Index(fields=["utilisateur"]), + models.Index(fields=["date_creation"]), + models.Index(fields=["date_lecture"]), + ] def __str__(self): return "Message ID%d" % self.idmessage if self.idmessage else "Nouveau message" diff --git a/noethysweb/fiche_famille/templates/fiche_famille/famille_messagerie_portail.html b/noethysweb/fiche_famille/templates/fiche_famille/famille_messagerie_portail.html index 3b88ee52..2c97c654 100644 --- a/noethysweb/fiche_famille/templates/fiche_famille/famille_messagerie_portail.html +++ b/noethysweb/fiche_famille/templates/fiche_famille/famille_messagerie_portail.html @@ -1,18 +1,46 @@ + + + + + {% extends "fiche_famille/famille.html" %} {% load crispy_forms_tags %} {% load static %} {% load embed %} - +{% load famille %} {% block detail_famille %}
-
-
{% if not liste_structures %} - Aucune structure disponible + + Aucune structure disponible. ( car vous n'êtes pas inscrit à une activité associée à une structure ) + {% endif %} {# Liste des structures disponibles #} @@ -22,89 +50,230 @@ {% block box_titre %}Sélection de la structure{% endblock %} {% block box_contenu %} {% for structure in liste_structures %} - - {% with dict_messages_par_structure|get_item:structure.pk as nbre_messages %} - - {% if nbre_messages %} - {{ nbre_messages }} message{{ nbre_messages|pluralize }} + {% dict_get_item_2d dict_messages_par_structure famille.pk structure.pk as nbre_total %} + {% with dict_messages_non_lus_structure|get_item:structure.pk as nbre_messages_non_lus %} + + {% if nbre_messages_non_lus and nbre_messages_non_lus > 0 %} + + + {{ nbre_messages_non_lus }} non lus / {{ nbre_total }} messages + + {% elif nbre_total > 0 %} + + + {{ nbre_total }} message{{ nbre_total|pluralize }} + {% endif %} {{ structure.nom }} {% endwith %} - {% endfor %} {% endblock box_contenu %} {% block box_conclusion %}{{ box_conclusion|safe }}{% endblock %} {% endembed %} {% endif %} - {# Discussion #} + {% if structure %} - {% embed 'core/box.html' with box_titre=True box_conclusion=True %} - {% block box_theme %}card-outline card-lightblue direct-chat direct-chat-primary{% endblock %} - {% block box_titre %} - Discussion avec la structure {{ structure.nom }} - {% endblock %} - {% block box_contenu %} + {# Liste des parents titulaires associés à la famille #} + {% if liste_representants %} + {% embed 'core/box.html' with box_titre=True %} + {% block box_theme %}card-outline card-lightblue{% endblock %} + {% block box_titre %}Sélection des destinataires{% endblock %} + {% block box_contenu %} - {% if not liste_messages_discussion %} -
- Aucun message. -
- {% else %} -
- {% for message in liste_messages_discussion %} - - {% if message.utilisateur %} - - {# Si c'est un message de l'administrateur #} -
-
- {% firstof message.utilisateur.get_full_name message.utilisateur message.utilisateur.get_short_name %} - {{ message.date_creation|date:"l j F Y H:i" }} -
- -
- {{ message.texte|safe }} -
-
- - {% else %} - - {# Si c'est un message de la famille #} -
-
- {{ message.famille }} - {{ message.date_creation|date:"l j F Y H:i" }} - - - + {# Bouton Famille #} + {% dict_get_item_2d dict_messages_par_famille famille.pk structure.pk as nbre_famille %} + + {% with dict_messages_non_lus_family|get_item:famille.pk as nbre_messages_non_lus %} + {% if nbre_messages_non_lus and nbre_messages_non_lus > 0 %} + + + {{ nbre_messages_non_lus }} non lus / {{ nbre_famille }} messages + + {% elif nbre_famille > 0 %} + + + {{ nbre_famille }} message{{ nbre_famille|pluralize }} + + {% endif %} + {% endwith %} + {{ famille }} + + + {# Boutons Individu #} + {% for individu in liste_representants %} + {% dict_get_item_3d dict_messages_par_individu famille.pk structure.pk individu.idindividu as nbre_individu %} + + {% with dict_messages_non_lus_individu|get_item:individu.pk as nbre_messages_non_lus %} + {% if nbre_messages_non_lus and nbre_messages_non_lus > 0 %} + + + {{ nbre_messages_non_lus }} non lus / {{ nbre_individu }} messages + + {% elif nbre_individu > 0 %} + + + {{ nbre_individu }} message{{ nbre_individu|pluralize }} + + {% endif %} + {% endwith %} + {{ individu.nom }} {{ individu.prenom }} + + {% endfor %} + + {% endblock box_contenu %} + {% block box_conclusion %}{{ box_conclusion|safe }}{% endblock %} + {% endembed %} + {% endif %} + + + + {# Discussion #} + {% if structure %} + {% embed 'core/box.html' with box_titre=True box_conclusion=True %} + {% block box_theme %}card-outline card-lightblue direct-chat direct-chat-primary{% endblock %} + {% block box_titre %} + {% if individu_selectionne %} + Discussion entre la structure + + {{ structure.nom }} + + et le représentant + + {{ individu_selectionne.nom }} {{ individu_selectionne.prenom }} + + {% else %} + Discussion entre la structure + + {{ structure.nom }} + + et la Famille + + {{ famille }} + + {% endif %} + {% endblock %} + + {% block box_contenu %} + {% if not liste_messages_discussion %} +
+ Aucun message. +
+ {% else %} + +
+ {% for message in liste_messages_discussion %} + {% if message.utilisateur and message.utilisateur.pk == request.user.pk or message.utilisateur.categorie == "utilisateur" %} + {# Messages de l'utilisateur connecté : affichés à gauche avec un style bleu foncé #} +
+
+ + {% firstof message.utilisateur.get_full_name message.utilisateur message.utilisateur.get_short_name %} - - {% if not message.date_lecture %}Nouveau{% endif %} + + {{ message.date_creation|date:"l j F Y H:i" }} + + + +
+ Admin +
+ {{ message.texte|safe }} +
- -
- {{ message.texte|safe }} + {% else %} + {# Messages des autres utilisateurs : affichés à droite avec un style gris clair #} +
+
+ + {{ message.utilisateur.individu }} + + + + {{ message.date_creation|date:"l j F Y H:i" }} + + + + + + {% if message in liste_messages_non_lus %} + Nouveau + {% endif %} +
+ User +
+ {{ message.texte|safe }} +
-
+ {% endif %} + {% endfor %} +
- {% endif %} - {% endfor %} -
- {% endif %} + {% endif %} + {% endblock box_contenu %} - {% endblock box_contenu %} - {% block box_conclusion %} - {% crispy form %} - {% endblock %} - {% endembed %} + {% block box_conclusion %} + {% crispy form %} + {% endblock %} + {% endembed %} + {% endif %} + }); + {% endif %} -
-
-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/noethysweb/fiche_famille/templatetags/famille.py b/noethysweb/fiche_famille/templatetags/famille.py new file mode 100644 index 00000000..c3a89588 --- /dev/null +++ b/noethysweb/fiche_famille/templatetags/famille.py @@ -0,0 +1,17 @@ +from django import template + +register = template.Library() + +@register.simple_tag +def dict_get_item_2d(dictionary, famille_id, structure_id): + """ + Retourne dictionary[(famille_id, structure_id)] s'il existe, sinon 0 ou None + """ + return dictionary.get((famille_id, structure_id), 0) + +@register.simple_tag +def dict_get_item_3d(dictionary, famille_id, structure_id, individu_id): + """ + Retourne dictionary[(famille_id, structure_id, individu_id)] s'il existe, sinon 0 ou None + """ + return dictionary.get((famille_id, structure_id, individu_id), 0) \ No newline at end of file diff --git a/noethysweb/fiche_famille/urls.py b/noethysweb/fiche_famille/urls.py index d5ba883e..b3e6b237 100644 --- a/noethysweb/fiche_famille/urls.py +++ b/noethysweb/fiche_famille/urls.py @@ -165,6 +165,8 @@ # Messagerie du portail path('individus/familles/messagerie/', famille_messagerie_portail.Ajouter.as_view(), name='famille_messagerie_portail'), path('individus/familles/messagerie/ajouter//', famille_messagerie_portail.Ajouter.as_view(), name='famille_messagerie_portail'), + path('individus/familles/messagerie/ajouter///',famille_messagerie_portail.Ajouter.as_view(), name='famille_messagerie_portail'), + path('individus/familles/messagerie/ajouter//', famille_messagerie_portail.Ajouter.as_view(), name='portail_contact'), # AJAX path('individus/get_individus_existants', secure_ajax(famille_ajouter.Get_individus_existants), name='ajax_get_individus_existants'), diff --git a/noethysweb/fiche_famille/views/famille_messagerie_portail.py b/noethysweb/fiche_famille/views/famille_messagerie_portail.py index 95e89bf3..5e39caa8 100644 --- a/noethysweb/fiche_famille/views/famille_messagerie_portail.py +++ b/noethysweb/fiche_famille/views/famille_messagerie_portail.py @@ -9,7 +9,10 @@ from core.models import PortailMessage, Structure from outils.forms.messagerie_portail import Formulaire, Envoi_notification_message from django.db.models import Q, Count - +from core.models import Individu +from core.models import Rattachement +from core.models import Famille +import logging, datetime, json class Page(Onglet): model = PortailMessage @@ -17,35 +20,263 @@ class Page(Onglet): def get_context_data(self, **kwargs): """ Context data spécial pour onglet """ context = super(Page, self).get_context_data(**kwargs) - context['onglet_actif'] = "outils" + context['onglet_actif'] = "messagerie" + + idfamille = self.kwargs.get('idfamille', None) + idstructure = self.kwargs.get('idstructure', None) + idindividu = self.kwargs.get('idindividu', None) + + print(f"ID Famille: {idfamille}, ID Structure: {idstructure}, ID Individu: {idindividu}") + + # ---- 1) Préparer liste des structures auxquelles au moins un individu de la famille est inscrit via une activité ---- + context['liste_structures'] = ( + Structure.objects + # 1) Filtrer sur l'accès utilisateur (si nécessaire) + .filter(pk__in=self.request.user.structures.all()) + # 2) Filtrer sur "structures liées à la famille via inscriptions" + .filter(activite__inscription__famille_id=self.Get_idfamille()) + .order_by("nom") + .distinct() + ) - # Importation de toutes les structures - context['liste_structures'] = Structure.objects.filter(pk__in=self.request.user.structures.all()).order_by("nom") + # ---- 2) Préparer la liste des individus (représentants) si la famille existe ---- + if self.Get_idfamille(): + context['liste_representants'] = ( + Individu.objects + .filter(rattachement__famille_id=self.Get_idfamille(), + rattachement__titulaire=True) + ) - # Importation du nombre de messages non lus (regroupement par structure) - context['dict_messages_par_structure'] = {valeur["structure"]: valeur["nbre"] for valeur in PortailMessage.objects.values("structure").filter(famille_id=self.Get_idfamille()).annotate(nbre=Count('pk'))} + # ---- 3) Récupérer l'individu sélectionné (si idindividu est passé dans l'URL) ---- + if idindividu: + try: + context['individu_selectionne'] = Individu.objects.get(idindividu=idindividu) + except Individu.DoesNotExist: + context['individu_selectionne'] = None + else: + context['individu_selectionne'] = None - # Discussion sélectionnée - liste_messages_discussion = PortailMessage.objects.select_related("structure", "utilisateur").filter(famille_id=self.Get_idfamille(), structure_id=self.get_idstructure()).order_by("date_creation") - context['liste_messages_discussion'] = list(liste_messages_discussion) + # ---- 4) Récupérer la famille sélectionnée ---- + if self.Get_idfamille(): + context["famille"] = Famille.objects.get(pk=self.Get_idfamille()) - # Importation de la structure + # ---- 5) Récupérer la structure sélectionnée ---- if self.get_idstructure(): context["structure"] = Structure.objects.get(pk=self.get_idstructure()) + # ------------------------------------------------------------------- + # COMPTAGE DU NOMBRE DE MESSAGES + # ------------------------------------------------------------------- + # + # a) Comptage des messages "famille" (individu_id IS NULL) + qs_famille = ( + PortailMessage.objects + .select_related("famille", "structure", "individu", "utilisateur") # Charge en une seule requête + .only("idmessage", "famille_id", "structure_id", "individu_id", "utilisateur_id", "texte", + "date_creation", "date_lecture") # Charge seulement les colonnes nécessaires + .values("famille", "structure") + .filter(individu__isnull=True) # => messages pour la famille + .annotate(nbre=Count("pk")) + ) + # On construit un dict : clé = (famille_id, structure_id), valeur = nbre de messages + dict_messages_par_famille = {} + for val in qs_famille: + dict_messages_par_famille[ + (val["famille"], val["structure"]) + ] = val["nbre"] + + # b) Comptage des messages "individu" (individu_id non nul) + qs_individu = ( + PortailMessage.objects + .select_related("famille", "structure", "individu", "utilisateur") # Charge en une seule requête + .only("idmessage", "famille_id", "structure_id", "individu_id", "utilisateur_id", "texte", + "date_creation", "date_lecture") # Charge seulement les colonnes nécessaires + .values("famille", "structure", "individu") + .filter(individu__isnull=False) # => messages privés à un individu + .annotate(nbre=Count("pk")) + ) + # On construit un dict : clé = (famille_id, structure_id, individu_id), valeur = nbre + dict_messages_par_individu = {} + for val in qs_individu: + dict_messages_par_individu[ + (val["famille"], val["structure"], val["individu"]) + ] = val["nbre"] + # c) Comptage global : tous messages confondus + qs_total = ( + PortailMessage.objects + .select_related("famille", "structure", "individu", "utilisateur") # Charge en une seule requête + .only("idmessage", "famille_id", "structure_id", "individu_id", "utilisateur_id", "texte", + "date_creation", "date_lecture") # Charge seulement les colonnes nécessaires + .values("famille", "structure") + .annotate(nbre=Count("pk")) # pas de .filter(individu__isnull=...) + ) + + dict_messages_par_structure = {} + for val in qs_total: + dict_messages_par_structure[ + (val["famille"], val["structure"]) + ] = val["nbre"] + + # On stocke ces trois dictionnaires dans le contexte + context["dict_messages_par_famille"] = dict_messages_par_famille + context["dict_messages_par_individu"] = dict_messages_par_individu + context["dict_messages_par_structure"] = dict_messages_par_structure + + # ------------------------------------------------------------------- + # COMPTAGE DU NOMBRE DE MESSAGES NON LUS + # ------------------------------------------------------------------- + # + # a) Calcul des messages non lus par structure + qs_non_lus = ( + PortailMessage.objects + .select_related("famille", "structure", "individu", "utilisateur") # Charge en une seule requête + .only("idmessage", "famille_id", "structure_id", "individu_id", "utilisateur_id", "texte", + "date_creation", "date_lecture") # Charge seulement les colonnes nécessaires + .values("structure") + .filter( + famille_id=self.Get_idfamille(), + utilisateur__isnull=False, + date_lecture__isnull=True, + ) + .exclude(utilisateur=self.request.user) + .annotate(nbre=Count('pk')) + ) + # Création d'un dictionnaire {structure_id: nombre_de_messages_non_lus} + dict_messages_non_lus_structure = {} + for val in qs_non_lus: + dict_messages_non_lus_structure[val["structure"]] = val["nbre"] + + + # b) Calcul des messages non lus par famille + qs_non_lus_family = ( + PortailMessage.objects + .select_related("famille", "structure", "individu", "utilisateur") # Charge en une seule requête + .only("idmessage", "famille_id", "structure_id", "individu_id", "utilisateur_id", "texte", + "date_creation", "date_lecture") # Charge seulement les colonnes nécessaires + .values("famille") + .filter( + famille_id=self.Get_idfamille(), + utilisateur__isnull=False, + individu__isnull=True, + date_lecture__isnull=True, + ) + .exclude(utilisateur=self.request.user) + .annotate(nbre=Count('pk')) + ) + # Création d'un dictionnaire {famille_id: nombre_de_messages_non_lus} + dict_messages_non_lus_family= {} + for val in qs_non_lus_family: + dict_messages_non_lus_family[val["famille"]] = val["nbre"] + + + # c) Calcul des messages non lus par individu + qs_non_lus_individu = ( + PortailMessage.objects + .select_related("famille", "structure", "individu", "utilisateur") # Charge en une seule requête + .only("idmessage", "famille_id", "structure_id", "individu_id", "utilisateur_id", "texte", + "date_creation", "date_lecture") # Charge seulement les colonnes nécessaires + .values("individu") + .filter( + famille_id=self.Get_idfamille(), + utilisateur__isnull=False, + individu__isnull=False, + date_lecture__isnull=True, + ) + .exclude(utilisateur=self.request.user) + .annotate(nbre=Count('pk')) + ) + + # Création d'un dictionnaire {individu_id: nombre_de_messages_non_lus} + dict_messages_non_lus_individu= {} + for val in qs_non_lus_individu: + dict_messages_non_lus_individu[val["individu"]] = val["nbre"] + + # Ajout au contexte + context['dict_messages_non_lus_structure'] = dict_messages_non_lus_structure + context['dict_messages_non_lus_family'] = dict_messages_non_lus_family + context['dict_messages_non_lus_individu'] = dict_messages_non_lus_individu + + # ------------------------------------------------------------------- + # AFFICHAGE DE LA DISCUSSION + # ------------------------------------------------------------------- + # + # Si idindividu est renseigné => discussion individuelle + # Sinon => discussion familiale + if idindividu: + # Discussion avec un individu particulier + liste_messages_discussion = ( + PortailMessage.objects + .select_related("famille", "structure", "individu", "utilisateur") # Charge en une seule requête + .only("idmessage", "famille_id", "structure_id", "individu_id", "utilisateur_id", "texte", + "date_creation", "date_lecture") # Charge seulement les colonnes nécessaires + .select_related("structure", "utilisateur") + .filter( + structure_id=self.get_idstructure(), + famille_id=self.Get_idfamille(), + individu_id=idindividu + ) + .order_by("date_creation") + ) + + else: + # Discussion générale avec la famille (individu_id=NULL) + liste_messages_discussion = ( + PortailMessage.objects + .select_related("famille", "structure", "individu", "utilisateur") # Charge en une seule requête + .only("idmessage", "famille_id", "structure_id", "individu_id", "utilisateur_id", "texte", + "date_creation", "date_lecture") # Charge seulement les colonnes nécessaires + .select_related("structure", "utilisateur") + .filter( + famille_id=self.Get_idfamille(), + structure_id=self.get_idstructure(), + individu__isnull=True + ) + .order_by("date_creation") + ) + + # Ajouter les messages non lus au contexte + liste_messages_non_lus = liste_messages_discussion.filter( + date_lecture__isnull=True + ).exclude(utilisateur=self.request.user) + context['liste_messages_non_lus'] = list(liste_messages_non_lus) + + # Marquer les messages non lus comme lus + liste_messages_non_lus.update(date_lecture=datetime.datetime.now()) + context['liste_messages_discussion'] = liste_messages_discussion + # liste_messages_discussion.update(date_lecture=datetime.datetime.now()) + return context def get_form_kwargs(self, **kwargs): form_kwargs = super(Page, self).get_form_kwargs(**kwargs) form_kwargs["idstructure"] = self.get_idstructure() form_kwargs["idfamille"] = self.Get_idfamille() + idindividu = self.kwargs.get('idindividu', None) return form_kwargs def get_idstructure(self): return self.kwargs.get("idstructure", 0) + def Get_idfamille(self): + return self.kwargs.get("idfamille", 0) + def get_success_url(self): - return reverse_lazy("famille_messagerie_portail", kwargs={'idstructure': self.get_idstructure(), 'idfamille': self.Get_idfamille()}) + """ Rediriger vers la discussion appropriée après l’envoi du message. """ + idindividu = self.kwargs.get('idindividu', None) + + if idindividu: + # Discussion individuelle + return reverse_lazy("famille_messagerie_portail", kwargs={ + 'idstructure': self.get_idstructure(), + 'idfamille': self.Get_idfamille(), + 'idindividu': idindividu + }) + else: + # Discussion familiale + return reverse_lazy("famille_messagerie_portail", kwargs={ + 'idstructure': self.get_idstructure(), + 'idfamille': self.Get_idfamille() + }) class Ajouter(Page, crud.Ajouter): @@ -54,6 +285,38 @@ class Ajouter(Page, crud.Ajouter): texte_confirmation = "Le message a bien été envoyé" def form_valid(self, form): - """ Envoie une notification de nouveau message à la famille par email """ - Envoi_notification_message(request=self.request, famille=form.cleaned_data["famille"], structure=form.cleaned_data["structure"]) + """ Envoie une notification de nouveau message à la famille ou au représentant par email """ + idfamille = self.kwargs.get('idfamille', None) + idindividu = self.kwargs.get('idindividu', None) + structure = self.kwargs.get('idstructure', None) + # 2) On fait un commit=False pour pouvoir modifier l’objet avant l’enregistrement + message = form.save(commit=False) + + # 3) On renseigne individuellement les champs voulus + message.famille_id = self.Get_idfamille() # la famille + message.structure_id = self.get_idstructure() # la structure + message.utilisateur_id = self.request.user.pk # utilisateur qui envoie (admin ?) + + if idindividu: + message.individu_id = idindividu # discussion privée + else: + message.individu_id = None # discussion famille + + # On sauvegarde en base + message.save() + + # 4) Envoyer la notification si besoin + if idfamille: + Envoi_notification_message( + request=self.request, + famille=idfamille, + structure=structure + ) + elif idindividu: + Envoi_notification_message( + request=self.request, + individu=idindividu, + structure=structure + ) + return super(Ajouter, self).form_valid(form) From d2cf1efdbf31ce65621fec53c72b9e5e38067455 Mon Sep 17 00:00:00 2001 From: "faouzia.takaya" Date: Wed, 12 Mar 2025 22:40:27 +0100 Subject: [PATCH 07/25] fix le prefixe de l'id d'individu --- noethysweb/core/models.py | 39 +++++++++++-------- .../fiche_famille/utils/utils_internet.py | 2 +- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/noethysweb/core/models.py b/noethysweb/core/models.py index 7b0d522b..f3123c13 100644 --- a/noethysweb/core/models.py +++ b/noethysweb/core/models.py @@ -1538,7 +1538,7 @@ class ModelePrestation(models.Model): structure = models.ForeignKey(Structure, verbose_name="Structure", on_delete=models.PROTECT, blank=True, null=True) # type_quotient = models.ForeignKey(TypeQuotient, verbose_name="Type de QF", blank=True, null=True, on_delete=models.SET_NULL, help_text="Sélectionnez un type de quotient familial ou laissez le champ vide pour tenir compte de tous les types de quotients.") multiprestations = models.TextField(verbose_name="Multi-prestations", blank=True, null=True) - + class Meta: db_table = 'modeles_prestations' verbose_name = "modèle de prestation" @@ -1860,7 +1860,7 @@ class Famille(models.Model): individus_masques = models.ManyToManyField(Individu, verbose_name="Individus masqués", related_name="individus_masques", blank=True) blocage_impayes_off = models.BooleanField(verbose_name="Ne jamais appliquer le blocage des réservations si impayés", 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.") contact_facturation = models.ForeignKey(Individu, verbose_name="Contact facturation", related_name="contact_facturation", on_delete=models.SET_NULL, blank=True, null=True) - + utilisateur = models.OneToOneField(Utilisateur, on_delete=models.CASCADE, null=True) class Meta: db_table = 'familles' verbose_name = "famille" @@ -1919,18 +1919,25 @@ def Maj_infos(self, maj_adresse=True, maj_mail=True, maj_mobile=True, maj_titula # Titulaire Hélios if maj_titulaire_helios: - if self.titulaire_helios: - # recherche si le titulaire est toujours dans la famille - found = False - for rattachement in rattachements: - if rattachement.individu == self.titulaire_helios: - found = True - if not found: - self.titulaire_helios = None - if not self.titulaire_helios: - # Recherche un individu valide parmi les titulaires de la famille - if rattachements: - self.titulaire_helios = rattachements.first().individu + try: + if self.titulaire_helios: + # Recherche si le titulaire est toujours dans la famille + found = False + for rattachement in rattachements: + if rattachement.individu == self.titulaire_helios: + found = True + break # Si trouvé, on sort de la boucle + if not found: + # Si le titulaire n'est pas trouvé dans la famille, on le met à None + self.titulaire_helios = None + except Individu.DoesNotExist: + # Si le titulaire a été supprimé, on le met à None + self.titulaire_helios = None + + # Assurez-vous qu'il y a un titulaire dans la famille si nécessaire + if not self.titulaire_helios and rattachements.exists(): + # Recherche un individu valide parmi les rattachements + self.titulaire_helios = rattachements.first().individu if maj_tiers_solidaire: if self.tiers_solidaire: @@ -3221,8 +3228,8 @@ class PortailMessage(models.Model): structure = models.ForeignKey(Structure, verbose_name="Structure", on_delete=models.CASCADE, db_index=True) utilisateur = models.ForeignKey(Utilisateur, verbose_name="Utilisateur", blank=True, null=True,on_delete=models.PROTECT) texte = models.TextField(verbose_name="Texte") - date_creation = models.DateTimeField(verbose_name="Date de création", auto_now_add=True, db_index=True) - date_lecture = models.DateTimeField(verbose_name="Date de lecture", max_length=200, blank=True, null=True,db_index=True) + date_creation = models.DateTimeField(verbose_name="Date de création", auto_now_add=True) + date_lecture = models.DateTimeField(verbose_name="Date de lecture", max_length=200, blank=True, null=True) class Meta: db_table = 'portail_messages' diff --git a/noethysweb/fiche_famille/utils/utils_internet.py b/noethysweb/fiche_famille/utils/utils_internet.py index cefde427..a765bb17 100644 --- a/noethysweb/fiche_famille/utils/utils_internet.py +++ b/noethysweb/fiche_famille/utils/utils_internet.py @@ -30,7 +30,7 @@ def CreationIdentifiantIndividu(IDindividu=None, IDutilisateur=None, nbreCaract= numTmp += random.choice("123456789") identifiant = numTmp + "0" * 5 if IDindividu: - identifiant = u"I%d" % (int(identifiant) + IDindividu) + identifiant = u"R%d" % (int(identifiant) + IDindividu) if IDutilisateur: identifiant = u"U%d" % (int(identifiant) + IDutilisateur) return identifiant From d7903c3b02a8015140c04a699d81c6e3708060ac Mon Sep 17 00:00:00 2001 From: "faouzia.takaya" Date: Wed, 12 Mar 2025 23:07:38 +0100 Subject: [PATCH 08/25] fix envoi email par lot d'inscriptions --- .../utils/utils_impression_inscription.py | 3 +++ .../utils/utils_inscriptions.py | 25 ++++++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/noethysweb/fiche_individu/utils/utils_impression_inscription.py b/noethysweb/fiche_individu/utils/utils_impression_inscription.py index ac8bb380..110decbb 100644 --- a/noethysweb/fiche_individu/utils/utils_impression_inscription.py +++ b/noethysweb/fiche_individu/utils/utils_impression_inscription.py @@ -37,6 +37,9 @@ def Draw(self): self.story.append(utils_impression.Bookmark(nomSansCivilite, str(IDinscription))) # ----------- Insertion du cadre principal -------------- + if self.modele_doc is None: + raise ValueError("Aucun modèle de document n'a été défini pour l'impression.") + cadre_principal = self.modele_doc.FindObjet("cadre_principal") if cadre_principal != None: diff --git a/noethysweb/fiche_individu/utils/utils_inscriptions.py b/noethysweb/fiche_individu/utils/utils_inscriptions.py index 4add8f35..6c7843fe 100644 --- a/noethysweb/fiche_individu/utils/utils_inscriptions.py +++ b/noethysweb/fiche_individu/utils/utils_inscriptions.py @@ -4,6 +4,7 @@ # Distribué sous licence GNU GPL. import logging + logger = logging.getLogger(__name__) import datetime from core.utils import utils_infos_individus, utils_texte, utils_conversion, utils_dates, utils_questionnaires @@ -25,7 +26,8 @@ def GetDonneesImpression(self, liste_inscriptions=[]): logger.debug("Recherche des données d'impression...") # Recherche les inscriptions - inscriptions = Inscription.objects.select_related("famille", "individu", "groupe", "categorie_tarif", "activite").filter(pk__in=liste_inscriptions) + inscriptions = Inscription.objects.select_related("famille", "individu", "groupe", "categorie_tarif", + "activite").filter(pk__in=liste_inscriptions) # Recherche les familles concernées liste_idfamille = [inscription.famille_id for inscription in inscriptions] @@ -45,7 +47,7 @@ def GetDonneesImpression(self, liste_inscriptions=[]): "{DATE_FIN}": utils_dates.ConvertDateToFR(inscription.date_fin) or "", "{IDINDIVIDU}": inscription.individu_id, - "{INDIVIDU_NOM}": inscription.individu.nom, + "{INDIVIDU_NOM}": inscription.individu.nom, "{INDIVIDU_PRENOM}": inscription.individu.prenom, "{INDIVIDU_RUE}": inscription.individu.rue_resid, "{INDIVIDU_CP}": inscription.individu.cp_resid, @@ -70,12 +72,13 @@ def GetDonneesImpression(self, liste_inscriptions=[]): } # Ajoute les informations de base individus et familles - dictDonnee.update(infosIndividus.GetDictValeurs(mode="famille", ID=inscription.famille_id, formatChamp=True)) + dictDonnee.update( + infosIndividus.GetDictValeurs(mode="famille", ID=inscription.famille_id, formatChamp=True)) # Ajoute les réponses des questionnaires for dictReponse in self.questionnaires.GetDonnees(inscription.famille_id): dictDonnee[dictReponse["champ"]] = dictReponse["reponse"] - if dictReponse["controle"] == "codebarres" : + if dictReponse["controle"] == "codebarres": dictDonnee["{CODEBARRES_QUESTION_%d}" % dictReponse["IDquestion"]] = dictReponse["reponse"] dictDonnees[inscription.pk] = dictDonnee @@ -88,13 +91,12 @@ def GetDonneesImpression(self, liste_inscriptions=[]): return dictDonnees, dictChampsFusion - def Impression(self, liste_inscriptions=[], dict_options=None, mode_email=False): """ Impression des inscriptions """ # Récupération des données à partir des IDinscription logger.debug("Recherche des données d'impression...") resultat = self.GetDonneesImpression(liste_inscriptions) - if resultat == False : + if resultat == False: return False dict_inscriptions, dictChampsFusion = resultat @@ -125,16 +127,21 @@ def Impression(self, liste_inscriptions=[], dict_options=None, mode_email=False) IDmodele = modele_defaut.pk - impression = utils_impression_inscription.Impression(dict_options=dict_options, IDmodele=IDmodele, generation_auto=False) + impression = utils_impression_inscription.Impression(dict_options=dict_options, IDmodele=IDmodele, + generation_auto=False) for IDinscription, dictInscription in dict_inscriptions.items(): logger.debug("Création du PDF de l'inscription ID%d..." % IDinscription) impression.Generation_document(dict_donnees={IDinscription: dictInscription}) - noms_fichiers[IDinscription] = {"nom_fichier": impression.Get_nom_fichier(), "valeurs": impression.Get_champs_fusion_pour_email("inscription", IDinscription)} + noms_fichiers[IDinscription] = {"nom_fichier": impression.Get_nom_fichier(), + "valeurs": impression.Get_champs_fusion_pour_email("inscription", + IDinscription)} # Fabrication du PDF global nom_fichier = None if not mode_email: - impression = utils_impression_inscription.Impression(dict_donnees=dict_inscriptions, dict_options=dict_options, IDmodele=dict_options["modele"].pk) + impression = utils_impression_inscription.Impression(dict_donnees=dict_inscriptions, + dict_options=dict_options, + IDmodele=dict_options["modele"].pk) nom_fichier = impression.Get_nom_fichier() logger.debug("Création des PDF terminée.") From 020fea0229fe2832face95391011084e8d5c335c Mon Sep 17 00:00:00 2001 From: "faouzia.takaya" Date: Thu, 13 Mar 2025 00:07:04 +0100 Subject: [PATCH 09/25] ADD configuration de la notification des messages --- noethysweb/core/templates/core/header.html | 39 ++++++++++++------- noethysweb/core/views/base.py | 13 +++++-- noethysweb/core/views/menu.py | 7 ++-- .../fiche_famille/views/famille_outils.py | 4 +- noethysweb/noethysweb/settings.py | 6 +-- noethysweb/outils/urls.py | 6 +-- 6 files changed, 42 insertions(+), 33 deletions(-) diff --git a/noethysweb/core/templates/core/header.html b/noethysweb/core/templates/core/header.html index 5b468fdf..3783812d 100644 --- a/noethysweb/core/templates/core/header.html +++ b/noethysweb/core/templates/core/header.html @@ -104,7 +104,7 @@ {% if perms.core.factures_generation %}{% endif %} {% if perms.core.rappels_generation %}{% endif %} {% endif %} - + {% if perms.core.famille_reglements_ajouter %}{% endif %} @@ -120,7 +120,6 @@ - {# Messagerie #} {% if liste_messages_non_lus %} {% endif %} @@ -166,7 +175,7 @@
+
{% endblock box_contenu %} - {% endembed %} + {% endembed %} +{% endfor %} -""" +""" \ No newline at end of file diff --git a/noethysweb/portail/templates/portail/activites.html b/noethysweb/portail/templates/portail/activites.html index 4a3a39ec..5998c3bf 100644 --- a/noethysweb/portail/templates/portail/activites.html +++ b/noethysweb/portail/templates/portail/activites.html @@ -17,6 +17,9 @@ .card-individu { border-radius: 5px; } + .card { + margin-top: 2rem; + } {% endblock %} @@ -35,44 +38,77 @@

{% endif %} - {% for individu in liste_individus %} -
+ {% for famille, individus in famille_individus %} + {# Block for each family #} + {% embed 'core/box.html' with box_titre=True %} + {% block box_theme %}card-outline card-lightblue custom-card{% endblock %} + {% block box_titre %}{% blocktrans %} Famille: {{ famille }} {% endblocktrans %}{% endblock %} + {% block card_body_classe %}p-0{% endblock %} + {% block card_custom_styles %} + + {% endblock %} - - + {% block box_contenu %} + {% if individus %} + {% for individu in individus %} - -
-
- - - - - - - - - {% for inscription in individu.inscriptions %} +
+ + + + + +
+
+
{% trans "Activité" %}{% trans "Groupe" %}
+ - - + + - {% endfor %} - -
{{ inscription.activite.nom }}{{ inscription.groupe.nom }}{% trans "Activité" %}{% trans "Groupe" %}
- {% for demande in demandes_inscriptions_attente|get_item:individu.pk %} - demande={{ demande }} - {% endfor %} -
-
+ + + {% for inscription in individu.inscriptions %} + + {{ inscription.activite.nom }} + {{ inscription.groupe.nom }} + + {% endfor %} + + -
+ + {% for demande in demandes_inscriptions_attente|get_item:individu.pk %} +
+ {% trans "Demande en attente" %}: {{ demande.nom_activite }} +
+ {% endfor %} + + - {% endfor %} + + {% endfor %} + {% else %} +
+
+ {% blocktrans %}Aucune activité.{% endblocktrans %} +
+
+ {% endif %} + {% endblock box_contenu %} + {% endembed %} +{% endfor %} {% if demandes_inscriptions_attente %}
@@ -87,6 +123,7 @@ + @@ -94,7 +131,8 @@ {% for demande in demandes_inscriptions_attente %} - + + {% endfor %} diff --git a/noethysweb/portail/views/activites.py b/noethysweb/portail/views/activites.py index 22d6a8ce..b53ce11b 100644 --- a/noethysweb/portail/views/activites.py +++ b/noethysweb/portail/views/activites.py @@ -4,46 +4,113 @@ # Distribué sous licence GNU GPL. import logging, datetime, json -logger = logging.getLogger(__name__) +from django.http import Http404 +from core.models import Rattachement from django.db.models import Q from django.utils.translation import gettext as _ from django.views.generic import TemplateView from core.models import Inscription, PortailRenseignement, Activite from portail.views.base import CustomView +logger = logging.getLogger(__name__) class View(CustomView, TemplateView): menu_code = "portail_activites" template_name = "portail/activites.html" + def get_object(self): + """Récupérer l'objet famille ou individu selon l'utilisateur""" + if hasattr(self.request.user, 'famille'): + return self.request.user.famille + elif hasattr(self.request.user, 'individu'): + return self.request.user.individu + else: + raise Http404("Utilisateur non reconnu.") + + def get_famille_object(self): + """Récupérer les familles de l'individu si applicable""" + if hasattr(self.request.user, 'famille'): + return [self.request.user.famille] + elif hasattr(self.request.user, 'individu'): + rattachements = Rattachement.objects.filter(individu=self.request.user.individu) + familles = [rattachement.famille for rattachement in rattachements if rattachement.famille and rattachement.titulaire == 1] + return familles if familles else None + return None + def get_context_data(self, **kwargs): context = super(View, self).get_context_data(**kwargs) context['page_titre'] = _("Activités") + familles = self.get_famille_object() + + if familles: + inscriptions = Inscription.objects.none() + dict_famille_individus = {} + dict_inscriptions = {} + demandes = [] + dict_activites = {activite.pk: activite.nom for activite in Activite.objects.all()} + + for famille in familles: + # Importation des inscriptions pour chaque famille + conditions = Q(famille=famille) & Q(statut="ok") & ( + Q(date_fin__isnull=True) | Q(date_fin__gte=datetime.date.today())) & Q( + individu__deces=False) + inscriptions_famille = Inscription.objects.select_related("activite", "individu").filter( + conditions).exclude(individu__in=famille.individus_masques.all()) + inscriptions = inscriptions.union(inscriptions_famille) + + # Récupération des individus par famille + individus_famille = sorted( + set(inscription.individu for inscription in inscriptions_famille), + key=lambda individu: individu.prenom + ) + dict_famille_individus[famille] = individus_famille + # Demandes d'inscription en attente de traitement pour chaque famille + demandes_famille = PortailRenseignement.objects.select_related("individu").filter( + famille=famille, etat="ATTENTE", code="inscrire_activite" + ).order_by("individu__prenom") + + for demande in demandes_famille: + try: + # Séparer la chaîne en fonction du point-virgule + nouvelle_valeur_parts = demande.nouvelle_valeur.split(";") + + # Nettoyer les guillemets et récupérer l'ID de l'activité depuis la première partie + activite_id = int(nouvelle_valeur_parts[0].strip('"')) + + except (ValueError, IndexError) as e: + # Gérer les erreurs de conversion ou d'index manquant + print( + f"Erreur lors de l'extraction de l'activité pour la demande {demande.idrenseignement}: {e}") + activite_id = None + + # Affecter le nom de l'activité à partir de l'ID récupéré ou une valeur par défaut "?" + demande.nom_activite = dict_activites.get(activite_id, "?") + demandes.append(demande) + + # Récupération des inscriptions pour chaque individu + for inscription in inscriptions_famille: + dict_inscriptions.setdefault(inscription.individu, []) + dict_inscriptions[inscription.individu].append(inscription) + + # Trier les individus par famille, puis par prénom + context['famille_individus'] = sorted( + dict_famille_individus.items(), + key=lambda item: item[0].nom # Trier d'abord par le nom de la famille + ) + # Ajouter les inscriptions triées pour chaque individu + for famille, individus in context['famille_individus']: + for individu in individus: + individu.inscriptions = sorted(dict_inscriptions[individu], + key=lambda inscription: inscription.activite.nom) + + # Ajouter les demandes d'inscription en attente + context["demandes_inscriptions_attente"] = demandes + # Vérifie si des activités sont ouvertes à l'inscription + context["activites_ouvertes_inscription"] = Activite.objects.filter( + (Q(portail_inscriptions_affichage="TOUJOURS") | + (Q(portail_inscriptions_affichage="PERIODE") & + Q(portail_inscriptions_date_debut__lte=datetime.datetime.now()) & + Q(portail_inscriptions_date_fin__gte=datetime.datetime.now()))) + ).exists() - # Importation des inscriptions - conditions = Q(famille=self.request.user.famille) & Q(statut="ok") & (Q(date_fin__isnull=True) | Q(date_fin__gte=datetime.date.today())) & Q(individu__deces=False) - inscriptions = Inscription.objects.select_related("activite", "individu").filter(conditions).exclude(individu__in=self.request.user.famille.individus_masques.all()) - - # Récupération des individus - context['liste_individus'] = sorted(list(set([inscription.individu for inscription in inscriptions])), key=lambda individu: individu.prenom) - - # Récupération des activités pour chaque individu - dict_inscriptions = {} - for inscription in inscriptions: - dict_inscriptions.setdefault(inscription.individu, []) - dict_inscriptions[inscription.individu].append(inscription) - for individu in context['liste_individus']: - individu.inscriptions = sorted(dict_inscriptions[individu], key=lambda inscription: inscription.activite.nom) - - # Demandes d'inscription en attente de traitement - demandes = [] - dict_activites = {activite.pk: activite.nom for activite in Activite.objects.all()} - for demande in PortailRenseignement.objects.select_related("individu").filter(famille=self.request.user.famille, etat="ATTENTE", code="inscrire_activite").order_by("individu__prenom"): - demande.nom_activite = dict_activites.get(int(json.loads(demande.nouvelle_valeur).split(";")[0]), "?") - demandes.append(demande) - context["demandes_inscriptions_attente"] = demandes - - # Vérifie si des activités sont ouvertes à l'inscription - context["activites_ouvertes_inscription"] = Activite.objects.filter((Q(portail_inscriptions_affichage="TOUJOURS") | (Q(portail_inscriptions_affichage="PERIODE") & Q(portail_inscriptions_date_debut__lte=datetime.datetime.now()) & Q(portail_inscriptions_date_fin__gte=datetime.datetime.now())))).exists() - - return context + return context \ No newline at end of file diff --git a/noethysweb/portail/views/inscrire_activite.py b/noethysweb/portail/views/inscrire_activite.py index 682b70e7..7e3d0a0f 100644 --- a/noethysweb/portail/views/inscrire_activite.py +++ b/noethysweb/portail/views/inscrire_activite.py @@ -4,6 +4,11 @@ # Distribué sous licence GNU GPL. import logging, json, datetime + +from core.models import Rattachement + +from core.models import Individu + logger = logging.getLogger(__name__) from django.urls import reverse_lazy from django.utils.translation import gettext as _ @@ -98,13 +103,28 @@ def Valid_form(request): PortailRenseignement.objects.create(famille=request.user.famille, individu=individu, categorie="famille_pieces", code="Nouvelle pièce", validation_auto=True, nouvelle_valeur=json.dumps(piece.Get_nom(), cls=DjangoJSONEncoder), idobjet=piece.pk) + # Enregistre les détails de l'action lorsque la demande d'inscription est effectuée + logger.info( + f"Utilisateur {request.user.username} a demandé une inscription pour " + f"l'activité {activite.nom} pour l'individu {individu.nom} {individu.prenom} dans la famille {famille.nom}" + ) + # Message de confirmation messages.add_message(request, messages.SUCCESS, "Votre demande d'inscription a été transmise") - - # Retour de la réponse return JsonResponse({"succes": True, "url": reverse_lazy("portail_activites")}) +def Get_individus(request): + """ Returns a list of individuals for the selected family """ + famille_id = request.POST.get('famille') + individus = Individu.objects.filter(rattachement__famille_id=famille_id) + + individu_options = [ + ''.format(individu.pk, individu.Get_nom()) for individu in individus + ] + return JsonResponse(''.join(individu_options), safe=False) + + class Page(CustomView): model = PortailRenseignement menu_code = "portail_activites" @@ -119,7 +139,6 @@ def get_context_data(self, **kwargs): def get_success_url(self): return reverse_lazy("portail_activites") - class Ajouter(Page, crud.Ajouter): form_class = Formulaire texte_confirmation = _("La demande a bien été transmise") From 694b965fd47575f26942c297a8e5f5d9b27f3703 Mon Sep 17 00:00:00 2001 From: "faouzia.takaya" Date: Thu, 13 Mar 2025 03:15:54 +0100 Subject: [PATCH 17/25] ADD Portail famille: Partie Activites: configuration de lien --- noethysweb/core/models.py | 5 +++++ noethysweb/portail/urls.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/noethysweb/core/models.py b/noethysweb/core/models.py index f3123c13..056ada44 100644 --- a/noethysweb/core/models.py +++ b/noethysweb/core/models.py @@ -1968,6 +1968,11 @@ def Maj_infos(self, maj_adresse=True, maj_mail=True, maj_mobile=True, maj_titula def Get_rue_resid(self): return self.rue_resid.replace("\n", "
") if self.rue_resid else None + + def Get_nom(self): + texte = "" + texte = self.nom + return texte def Get_infos(self, avec_civilite=False): """ Renvoie le nom des titulaires """ diff --git a/noethysweb/portail/urls.py b/noethysweb/portail/urls.py index bdf6bb6f..d4104a22 100644 --- a/noethysweb/portail/urls.py +++ b/noethysweb/portail/urls.py @@ -155,4 +155,6 @@ path('individus/ajouter_assureur', secure_ajax_portail(individu_assurances.Ajouter_assureur), name='portail_ajax_ajouter_assureur'), path('activites/get_form_extra', secure_ajax_portail(inscrire_activite.Get_form_extra), name='portail_ajax_inscrire_get_form_extra'), path('activites/validation_form', secure_ajax_portail(inscrire_activite.Valid_form), name='portail_ajax_inscrire_valid_form'), + path('activites/get-individus', secure_ajax_portail(inscrire_activite.Get_individus), name='portail_ajax_inscrire_get_individus'), + ] From e065f6126044d314eda01d83520a5316498c333e Mon Sep 17 00:00:00 2001 From: "faouzia.takaya" Date: Thu, 13 Mar 2025 03:23:32 +0100 Subject: [PATCH 18/25] ADD Portail famille: Parti Reservation : Regroupement des reservations par famille --- .../portail/templates/portail/planning.html | 29 +--- .../templates/portail/reservations.html | 139 +++++++++------ noethysweb/portail/views/planning.py | 72 ++++++-- noethysweb/portail/views/reservations.py | 161 ++++++++++++------ 4 files changed, 256 insertions(+), 145 deletions(-) diff --git a/noethysweb/portail/templates/portail/planning.html b/noethysweb/portail/templates/portail/planning.html index c5cc8dbe..21ddd261 100644 --- a/noethysweb/portail/templates/portail/planning.html +++ b/noethysweb/portail/templates/portail/planning.html @@ -54,7 +54,7 @@ {# Entête de l'individu #}
-

{{ data.individu.prenom }}

+

{{ data.individu.nom }} {{ data.individu.prenom }}

Photo @@ -86,31 +86,28 @@
{% trans "Dates" %}
{# Grilles des réservations #}
- - {% if parametres_portail.reservations_afficher_facturation %} -
- {% else %} -
- {% endif %} - +
+ {# Grille des consommations #}
{# Texte d'introduction de la période #} + {{ data.periode_reservation }} + {% if data.periode_reservation.introduction %}
{{ data.periode_reservation.introduction }}
{% endif %} - + {# Texte si validation manuelle des réservations #} {% if data.periode_reservation.activite.validation_type == "MANUELLE" %}
Vos demandes de réservations devront être validées par l'administrateur du portail.
{% endif %} - + {# Tableau de la grille #} {% include "consommations/grille_tableau.html" %} @@ -128,23 +125,13 @@
{% trans "Dates" %}
-
-
- {% if parametres_portail.reservations_afficher_facturation %} -
- {% if parametres_portail.reservations_afficher_forfaits and data.tarifs_credits_exists %} - {% include "consommations/grille_forfaits.html" %} - {% endif %} - {% include "consommations/grille_facturation.html" %}
- {% endif %} - +
{% include 'consommations/grille_saisie_horaires.html' %} {% include 'consommations/grille_saisie_quantite.html' %} {% include 'consommations/grille_appliquer_semaine_type.html' %} - {% include 'consommations/grille_saisie_questionnaire.html' %} {% endblock contenu_page %} diff --git a/noethysweb/portail/templates/portail/reservations.html b/noethysweb/portail/templates/portail/reservations.html index 32fe838f..43c17ddf 100644 --- a/noethysweb/portail/templates/portail/reservations.html +++ b/noethysweb/portail/templates/portail/reservations.html @@ -17,9 +17,11 @@ .card-individu { border-radius: 5px; } + .card { + margin-top: 2rem; + } {% endblock %} - {% block contenu_page %} {# Texte d'introduction #} @@ -41,65 +43,94 @@
{% endif %} - {% if not blocage_impayes %} - {% for individu in liste_individus %} -
- - - - - - + {% endfor %} + {% else %} +
+ {% blocktrans %}Aucun individu associé à cette famille.{% endblocktrans %} +
+ {% endif %} - {% embed 'core/box.html' with box_titre=True %} - {% block box_theme %}card-outline card-lightblue{% endblock %} - {% block box_titre %}{% blocktrans %}Liste des périodes de réservations{% endblocktrans %}{% endblock %} - {% block box_contenu %} -
- {% blocktrans %}Aucune période de réservation disponible.{% endblocktrans %} -
- {% endblock box_contenu %} - {% endembed %} + {% else %} + {% embed 'core/box.html' with box_titre=True %} + {% block box_theme %}card-outline card-lightblue{% endblock %} + {% block box_titre %}{% blocktrans %}Liste des périodes de réservations{% endblocktrans %}{% endblock %} + {% block box_contenu %} +
+ {% blocktrans %}Aucune période de réservation disponible.{% endblocktrans %} +
+ {% endblock box_contenu %} + {% endembed %} + {% endif %} {% endfor %} + {% else %} +
+
+ {% blocktrans %}Aucune réservation.{% endblocktrans %} +
+
{% endif %} +{% endblock box_contenu %} +{% endembed %} +{% endfor %} -{% endblock contenu_page %} +{% endblock contenu_page %} \ No newline at end of file diff --git a/noethysweb/portail/views/planning.py b/noethysweb/portail/views/planning.py index 1d949126..7e1ab6ba 100644 --- a/noethysweb/portail/views/planning.py +++ b/noethysweb/portail/views/planning.py @@ -4,6 +4,7 @@ # Distribué sous licence GNU GPL. import logging, json, datetime +from core.models import Rattachement logger = logging.getLogger(__name__) from django.urls import reverse_lazy from django.views.generic import TemplateView @@ -27,24 +28,53 @@ class View(CustomView, TemplateView): menu_code = "portail_reservations" template_name = "portail/planning.html" + def get_object(self): + """Récupérer l'objet famille ou individu selon l'utilisateur""" + if hasattr(self.request.user, 'famille'): + return self.request.user.famille + elif hasattr(self.request.user, 'individu'): + return self.request.user.individu + else: + raise Http404("Utilisateur non reconnu.") + + def get_famille_object(self): + if hasattr(self.request.user, 'famille'): + return self.request.user.famille + elif hasattr(self.request.user, 'individu'): + rattachement = Rattachement.objects.filter(individu=self.request.user.individu).first() + if rattachement.famille and rattachement.titulaire == 1: + return rattachement.famille + + return None + def dispatch(self, request, *args, **kwargs): - """ Vérifie si des approbations sont requises """ + """ Vérifie si des approbations sont requises et utilise la famille ou l'individu selon l'utilisateur """ if not request.user.is_authenticated: return redirect("portail_connexion") + # Récupération de l'objet famille ou individu + utilisateur_obj = self.get_object() + + # Récupération de l'activité activite = Activite.objects.prefetch_related("types_consentements").get(pk=kwargs["idactivite"]) - approbations_requises = utils_approbations.Get_approbations_requises(famille=request.user.famille, activites=[activite,], idindividu=kwargs["idindividu"]) + + # Approbations requises + famille = self.get_famille_object() # Toujours utiliser la famille, que l'utilisateur soit une famille ou un individu + + approbations_requises = utils_approbations.Get_approbations_requises(famille=famille,activites=[activite,],idindividu=kwargs["idindividu"]) if approbations_requises["nbre_total"] > 0: messages.add_message(request, messages.ERROR, "L'accès à ces réservations nécessite au moins une approbation. Veuillez valider les approbations en attente.") return redirect("portail_renseignements") return super(View, self).dispatch(request, *args, **kwargs) def post(self, request, *args, **kwargs): + famille = self.get_famille_object() # Toujours utiliser la famille, que l'utilisateur soit une famille ou un individu + """ Sauvegarde de la grille """ resultat = Save_grille(request=request, donnees=json.loads(self.request.POST.get("donnees"))) # Ventilation auto si besoin - if utils_portail.Get_parametre(code="reservations_ventilation_auto") and utils_ventilation.GetAnomaliesVentilation(idfamille=self.request.user.famille.pk): - utils_ventilation.Ventilation_auto(IDfamille=self.request.user.famille.pk) + if utils_portail.Get_parametre(code="reservations_ventilation_auto") and utils_ventilation.GetAnomaliesVentilation(idfamille=famille.pk): + utils_ventilation.Ventilation_auto(IDfamille=famille.pk) # Envoi d'un mail de confirmation des modifications idadresse_exp = utils_portail.Get_parametre(code="reservations_adresse_exp") @@ -54,20 +84,26 @@ def post(self, request, *args, **kwargs): return HttpResponseRedirect(reverse_lazy("portail_reservations")) def test_func(self): - """ Vérifie que l'utilisateur peut se connecter à cette page """ + """ Vérifie que l'utilisateur peut accéder à cette page """ if not super(View, self).test_func(): return False - inscription = Inscription.objects.filter(famille=self.request.user.famille, individu_id=self.kwargs.get('idindividu'), activite_id=self.kwargs["idactivite"]) - if not inscription: - return False - if not inscription.first().internet_reservations: + + famille = self.get_famille_object() # Récupérer la famille peu importe le type d'utilisateur + + if not famille: return False - if not self.request.user.famille.internet_reservations: + + inscription = Inscription.objects.filter(famille=famille,individu_id=self.kwargs.get('idindividu'),activite_id=self.kwargs["idactivite"]) + if not inscription.exists() or not inscription.first().internet_reservations: return False + periode = PortailPeriode.objects.select_related("activite").prefetch_related("categories").get(pk=self.kwargs.get('idperiode')) if not periode or not periode.Is_active_today(): return False - if not periode.Is_famille_authorized(famille=self.request.user.famille): + if hasattr(self.request.user, 'famille') and not periode.Is_famille_authorized(famille=self.request.user.famille): + return False + + if hasattr(self.request.user, 'individu') and not periode.Is_individu_authorized(individu=self.request.user.individu): return False return True @@ -82,7 +118,9 @@ def get_context_data(self, **kwargs): return context def Get_data_planning(self): - data = {"mode": "portail", "idfamille": self.request.user.famille.pk, "consommations": {}, "prestations": {}, "memos": {}, "options": {"afficher_quantites": False}} + # Initialisation des données avec la famille de l'utilisateur + famille = self.get_famille_object() # Utilisation de la méthode pour récupérer la famille + data = {"mode": "portail", "idfamille": famille.pk, "consommations": {}, "prestations": {}, "memos": {},"options": {"afficher_quantites": False}} data["dict_suppressions"] = {"consommations": [], "prestations": [], "memos": []} # Importation de l'individu @@ -93,7 +131,7 @@ def Get_data_planning(self): periode_reservation = PortailPeriode.objects.select_related("activite").get(pk=self.kwargs.get('idperiode')) data['periode_reservation'] = periode_reservation - # Dates de la période + # Gestion des dates de la période afficher_dates_passees = int(periode_reservation.activite.portail_afficher_dates_passees) if afficher_dates_passees == 9999: data["date_min"] = periode_reservation.date_debut @@ -105,7 +143,7 @@ def Get_data_planning(self): data["liste_feries"] = Ferie.objects.all() # Création des périodes à afficher - data["periode"] = {'mode': 'dates', 'selections': {}, 'periodes': ['%s;%s' % (data["date_min"], data["date_max"])]} + data["periode"] = {'mode': 'dates', 'selections': {},'periodes': ['%s;%s' % (data["date_min"], data["date_max"])]} if periode_reservation.type_date == "VACANCES": data["periode"]["periodes"] = [] @@ -136,7 +174,7 @@ def Get_data_planning(self): # Importation de toutes les inscriptions de l'individu data['liste_inscriptions'] = [] - for inscription in Inscription.objects.select_related('individu', 'activite', 'groupe', 'famille', 'categorie_tarif').filter(famille=self.request.user.famille, individu=individu, activite=periode_reservation.activite): + for inscription in Inscription.objects.select_related('individu', 'activite', 'groupe', 'famille', 'categorie_tarif').filter(famille=famille, individu=individu, activite=periode_reservation.activite): if inscription.Is_inscription_in_periode(data["date_min"], data["date_max"]): data['liste_inscriptions'].append(inscription) @@ -199,7 +237,7 @@ def Envoi_mail_confirmation(self, request=None, resultat=None, idadresse_exp=Non logger.debug("Création du mail de confirmation des modifications...") modele_email = ModeleEmail.objects.filter(categorie="portail_confirmation_reservations", defaut=True).first() if not modele_email: - logger.error("Erreur : Aucune modèle d'email de catégorie 'portail_confirmation_reservations' n'a été paramétré !") + logger.error("Erreur : Aucun modèle d'email de catégorie 'portail_confirmation_reservations' n'a été paramétré !") return mail = Mail.objects.create( @@ -216,7 +254,7 @@ def Envoi_mail_confirmation(self, request=None, resultat=None, idadresse_exp=Non "{INDIVIDU_NOM}": individu.nom, "{INDIVIDU_PRENOM}": individu.prenom, "{INDIVIDU_NOM_COMPLET}": individu.Get_nom()} # Création du destinataire - destinataire = Destinataire.objects.create(categorie="famille", famille=self.request.user.famille, adresse=self.request.user.famille.mail, valeurs=json.dumps(valeurs_fusion)) + destinataire = Destinataire.objects.create(categorie="famille", famille=self.get_famille(), adresse=self.get_famille().mail, valeurs=json.dumps(valeurs_fusion)) mail.destinataires.add(destinataire) # Envoi du mail diff --git a/noethysweb/portail/views/reservations.py b/noethysweb/portail/views/reservations.py index 3e02e4b7..0d4cb9c4 100644 --- a/noethysweb/portail/views/reservations.py +++ b/noethysweb/portail/views/reservations.py @@ -4,6 +4,8 @@ # Distribué sous licence GNU GPL. import logging, datetime, decimal +from django.http import Http404 +from core.models import Rattachement logger = logging.getLogger(__name__) from django.db.models import Q from django.views.generic import TemplateView @@ -18,62 +20,115 @@ class View(CustomView, TemplateView): menu_code = "portail_reservations" template_name = "portail/reservations.html" + def get_object(self): + """Récupérer l'objet famille ou individu selon l'utilisateur""" + if hasattr(self.request.user, 'famille'): + return self.request.user.famille + elif hasattr(self.request.user, 'individu'): + return self.request.user.individu + else: + raise Http404("Utilisateur non reconnu.") + + def get_famille_object(self): + """Récupérer les familles de l'individu si applicable""" + if hasattr(self.request.user, 'famille'): + return [self.request.user.famille] + elif hasattr(self.request.user, 'individu'): + rattachements = Rattachement.objects.filter(individu=self.request.user.individu) + familles = [rattachement.famille for rattachement in rattachements if rattachement.famille and rattachement.titulaire == 1] + return familles if familles else None + return None + def get_context_data(self, **kwargs): context = super(View, self).get_context_data(**kwargs) context['page_titre'] = _("Réservations") + familles = self.get_famille_object() + context['familles']=familles + + if familles: + # Vérifie que la famille est autorisée à faire des réservations + + inscriptions = Inscription.objects.none() + for famille in familles: + # Conditions pour filtrer les inscriptions valides + conditions = Q(famille=famille) & Q(statut="ok") & ( + Q(date_fin__isnull=True) | Q(date_fin__gte=datetime.date.today())) + conditions &= Q(activite__portail_reservations_affichage="TOUJOURS") & ( + Q(activite__date_fin__isnull=True) | Q(activite__date_fin__gte=datetime.date.today())) + conditions &= Q(internet_reservations=True) & Q(individu__deces=False) + + # Récupération des inscriptions valides + inscriptions_famille = Inscription.objects.select_related("activite", "individu").filter( + conditions).exclude(individu__in=famille.individus_masques.all()) + inscriptions = inscriptions.union(inscriptions_famille) + # Trier les individus par prénom + # Récupération des individus par famille + individus_famille = sorted( + set(inscription.individu for inscription in inscriptions), + key=lambda individu: individu.prenom + ) + # Groupement des inscriptions par famille + reservations_par_famille = {} + + # Récupération des activités pour chaque individu + dict_inscriptions = {} + liste_activites = [] + + for inscription in inscriptions: + # Créer un ensemble de réservations pour chaque famille s'il n'existe pas + if inscription.famille not in reservations_par_famille: + reservations_par_famille[inscription.famille] = [] + + # Ajoutez l'individu et ses activités à la famille uniquement si l'individu n'est pas déjà présent + if inscription.individu not in reservations_par_famille[inscription.famille]: + reservations_par_famille[inscription.famille].append(inscription.individu) + + # Ajouter les activités par individu + dict_inscriptions.setdefault(inscription.individu, []) + if inscription.activite not in dict_inscriptions[inscription.individu]: + dict_inscriptions[inscription.individu].append(inscription.activite) + if inscription.activite not in liste_activites: + liste_activites.append(inscription.activite) + + # Trier les activités pour chaque individu + for individu in individus_famille: + individu.activites = sorted(dict_inscriptions[individu], key=lambda activite: activite.nom) + + # Récupération des périodes de réservation + conditions = Q(activite__in=liste_activites) + conditions &= (Q(affichage="TOUJOURS") | ( + Q(affichage="PERIODE") & Q(affichage_date_debut__lte=datetime.datetime.now()) & Q( + affichage_date_fin__gte=datetime.datetime.now()))) + periodes = PortailPeriode.objects.select_related("activite").prefetch_related("categories").filter( + conditions).order_by("date_debut") + + dict_periodes = {} + for periode in periodes: + if periode.Is_famille_authorized(famille=famille): + dict_periodes.setdefault(periode.activite, []) + dict_periodes[periode.activite].append(periode) + + # Ajouter les données au contexte + context['liste_individus'] = individus_famille + context['reservations_par_famille'] = reservations_par_famille # Groupement par famille + context['dict_periodes'] = dict_periodes + + # Approbations + approbations_requises = utils_approbations.Get_approbations_requises(famille=famille) + context['nbre_approbations_requises'] = approbations_requises["nbre_total"] + + # Blocage si impayés + blocage_impayes = context["parametres_portail"].get("reservations_blocage_impayes", None) + context["blocage_impayes"] = False - # Vérifie que la famille est autorisée à faire des réservations - if not self.request.user.famille.internet_reservations: - context['liste_individus'] = [] - return context - - # Importation des inscriptions - conditions = Q(famille=self.request.user.famille) & Q(statut="ok") & (Q(date_fin__isnull=True) | Q(date_fin__gte=datetime.date.today())) - conditions &= Q(activite__portail_reservations_affichage="TOUJOURS") & (Q(activite__date_fin__isnull=True) | Q(activite__date_fin__gte=datetime.date.today())) - conditions &= Q(internet_reservations=True) & Q(individu__deces=False) - inscriptions = Inscription.objects.select_related("activite", "individu").filter(conditions).exclude(individu__in=self.request.user.famille.individus_masques.all()) - - # Récupération des individus - context['liste_individus'] = sorted(list(set([inscription.individu for inscription in inscriptions])), key=lambda individu: individu.prenom) - - # Récupération des activités pour chaque individu - dict_inscriptions = {} - liste_activites = [] - for inscription in inscriptions: - dict_inscriptions.setdefault(inscription.individu, []) - if inscription.activite not in dict_inscriptions[inscription.individu]: - dict_inscriptions[inscription.individu].append(inscription.activite) - if inscription.activite not in liste_activites: - liste_activites.append(inscription.activite) - for individu in context['liste_individus']: - individu.activites = sorted(dict_inscriptions[individu], key=lambda activite: activite.nom) - - # Récupération des périodes de réservation - conditions = Q(activite__in=liste_activites) - conditions &= (Q(affichage="TOUJOURS") | (Q(affichage="PERIODE") & Q(affichage_date_debut__lte=datetime.datetime.now()) & Q(affichage_date_fin__gte=datetime.datetime.now()))) - periodes = PortailPeriode.objects.select_related("activite").prefetch_related("categories").filter(conditions).order_by("date_debut") - dict_periodes = {} - for periode in periodes: - if periode.Is_famille_authorized(famille=self.request.user.famille): - dict_periodes.setdefault(periode.activite, []) - dict_periodes[periode.activite].append(periode) - context['dict_periodes'] = dict_periodes - - # Approbations - approbations_requises = utils_approbations.Get_approbations_requises(famille=self.request.user.famille) - context['nbre_approbations_requises'] = approbations_requises["nbre_total"] - - # Blocage si impayés - blocage_impayes = context["parametres_portail"].get("reservations_blocage_impayes", None) - context["blocage_impayes"] = False - - if blocage_impayes and not self.request.user.famille.blocage_impayes_off: - blocage_impayes = decimal.Decimal(blocage_impayes) - factures = False - if blocage_impayes > decimal.Decimal(10000): - blocage_impayes -= decimal.Decimal(10000) - factures = True - if utils_familles.Get_solde_famille(idfamille=self.request.user.famille.pk, date_situation=datetime.date.today(), factures=factures) >= blocage_impayes: - context["blocage_impayes"] = True + if blocage_impayes and not famille.blocage_impayes_off: + blocage_impayes = decimal.Decimal(blocage_impayes) + factures = False + if blocage_impayes > decimal.Decimal(10000): + blocage_impayes -= decimal.Decimal(10000) + factures = True + if utils_familles.Get_solde_famille(idfamille=famille.pk, date_situation=datetime.date.today(), + factures=factures) >= blocage_impayes: + context["blocage_impayes"] = True return context From 1cfd0fca0b5b141b9bfdfa6691df02b8c8fe0fdf Mon Sep 17 00:00:00 2001 From: "faouzia.takaya" Date: Thu, 13 Mar 2025 03:31:00 +0100 Subject: [PATCH 19/25] ADD Portail famille: Partie Documents : l'ajout d'un url pour la fonction ajax pour lister les individus par famille --- noethysweb/portail/forms/transmettre_piece.py | 141 ++++++++++++++---- noethysweb/portail/urls.py | 1 + noethysweb/portail/views/documents.py | 39 +++-- noethysweb/portail/views/transmettre_piece.py | 36 ++++- 4 files changed, 174 insertions(+), 43 deletions(-) diff --git a/noethysweb/portail/forms/transmettre_piece.py b/noethysweb/portail/forms/transmettre_piece.py index 60edc6c6..a5af986a 100644 --- a/noethysweb/portail/forms/transmettre_piece.py +++ b/noethysweb/portail/forms/transmettre_piece.py @@ -15,13 +15,24 @@ from core.utils.utils_commandes import Commandes from portail.forms.fiche import FormulaireBase from individus.utils import utils_pieces_manquantes +from core.models import Famille class Formulaire(FormulaireBase, ModelForm): # Type de pièce - selection_piece = forms.TypedChoiceField(label=_("Type de document"), choices=[], required=True, help_text=_("Sélectionnez un type de document dans la liste. Sélectionnez 'Autre type' s'il ne s'agit pas d'un document prédéfini.")) - choix_individu = forms.TypedChoiceField(label=_("Individu concerné"), choices=[], required=False, help_text=_("Sélectionnez le nom de l'individu concerné ou la famille s'il s'agit d'un document qui concerne toute la famille.")) - document = forms.FileField(label=_("Document"), help_text=_("Sélectionnez un document à joindre (pdf, jpg ou png)."), required=True, validators=[FileExtensionValidator(allowed_extensions=['pdf', 'png', 'jpg'])]) + selection_piece = forms.TypedChoiceField(label=_("Type de document"), choices=[], required=True, help_text=_( + "Sélectionnez un type de document dans la liste. Sélectionnez 'Autre type' s'il ne s'agit pas d'un document prédéfini.")) + individu = forms.TypedChoiceField(label=_("Individu concerné"), choices=[], required=False, help_text=_( + "Sélectionnez le nom de l'individu concerné ou la famille s'il s'agit d'un document qui concerne toute la famille.")) + document = forms.FileField(label=_("Document"), + help_text=_("Sélectionnez un document à joindre (pdf, jpg ou png)."), required=True, + validators=[FileExtensionValidator(allowed_extensions=['pdf', 'png', 'jpg'])]) + famille = forms.ModelChoiceField( + queryset=Famille.objects.all(), # Initialement vide + label=_("Famille concernée"), + required=True, + help_text=_("Sélectionnez la famille concernée par le document.") + ) class Meta: model = Piece @@ -38,6 +49,20 @@ class Meta: "observations": _("Vous pouvez ajouter des observations si vous le souhaitez."), } + def get_rattachements_for_user(self): + rattachements = set() # Set pour éviter les doublons + # Vérifiez si l'utilisateur fait partie d'une famille ou d'un individu + if hasattr(self.request.user, 'individu'): + # Si l'utilisateur est un individu, obtenez toutes les familles auxquelles il est rattaché + rattachements_query = Rattachement.objects.filter(individu=self.request.user.individu) + rattachements.update(rattachements_query) + + elif hasattr(self.request.user, 'famille'): + # Si l'utilisateur fait partie d'une famille, obtenir les pièces jointes pour cette famille + rattachements_query = Rattachement.objects.filter(famille=self.request.user.famille) + rattachements.update(rattachements_query) + return list(rattachements) # Reconvertir en liste + def __init__(self, *args, **kwargs): super(Formulaire, self).__init__(*args, **kwargs) self.helper = FormHelper() @@ -48,32 +73,62 @@ def __init__(self, *args, **kwargs): self.helper.label_class = 'col-md-2 col-form-label' self.helper.field_class = 'col-md-10' self.helper.use_custom_control = False - - # Pièces à fournir - self.liste_pieces_fournir = utils_pieces_manquantes.Get_pieces_manquantes(famille=self.request.user.famille, exclure_individus=self.request.user.famille.individus_masques.all()) - self.fields['selection_piece'].choices = [(index, piece["label"]) for index, piece in enumerate(self.liste_pieces_fournir)] + [(9999, "Un autre type de pièce")] - self.fields['selection_piece'].initial = None - - # Auteur - if not self.instance.pk: - self.fields["auteur"].initial = self.request.user - - # Choix de l'individu - rattachements = Rattachement.objects.select_related("individu").filter(famille=self.request.user.famille).exclude(individu__in=self.request.user.famille.individus_masques.all()).order_by("categorie") - self.fields['choix_individu'].choices = [(None, "La famille")] + [(rattachement.individu_id, rattachement.individu.Get_nom()) for rattachement in rattachements] - - # Affichage - self.helper.layout = Layout( - Hidden("famille", value=self.request.user.famille.pk), - Field("auteur", type="hidden"), - Field('selection_piece'), - Field('titre'), - Field('choix_individu'), - Field('document'), - Field('observations'), - HTML(EXTRA_SCRIPT), - Commandes(enregistrer_label="%s" % _("Envoyer"), annuler_url="{% url 'portail_documents' %}", ajouter=False, aide=False, css_class="pull-right"), - ) + familles = [] + # Gérer le cas où l'utilisateur fait directement partie d'une famille + if hasattr(self.request.user, 'famille'): + familles.append(self.request.user.famille) + # Gérer le cas où l'utilisateur est un individu et lié via Rattachement + elif hasattr(self.request.user, 'individu'): + rattachements = Rattachement.objects.filter(individu=self.request.user.individu) + # Ajoutez chaque famille liée à la liste + for rattachement in rattachements: + if rattachement.famille and rattachement.titulaire == 1: + familles.append(rattachement.famille) + + # Vérifier si des familles existent avant de procéder + if familles: + for famille in familles: + # Pièces à fournir + self.liste_pieces_fournir = utils_pieces_manquantes.Get_pieces_manquantes(famille=famille, + exclure_individus=famille.individus_masques.all()) + self.fields['selection_piece'].choices = [(index, piece["label"]) for index, piece in + enumerate(self.liste_pieces_fournir)] + [ + (9999, "Un autre type de pièce")] + self.fields['selection_piece'].initial = None + + # Récupérer les rattachements pour l'utilisateur + rattachements = self.get_rattachements_for_user() + familless = {(rattachement.famille_id, rattachement.famille.Get_nom()) for rattachement in rattachements + if rattachement.famille and rattachement.titulaire == 1} + if len(familless) > 1: + self.fields['famille'].choices = [(None, "------------")] + list(familless) + self.fields['individu'].choices = [(None, "La famille")] + else: + self.fields['famille'].choices = list(familless) + famille_ids = {rattachement.famille_id for rattachement in rattachements if + rattachement.famille and rattachement.titulaire == 1} + individus = Individu.objects.filter(rattachement__famille_id__in=famille_ids) + self.fields['individu'].choices = [(None, "La famille")] + [ + (individu.idindividu, individu.Get_nom()) for individu in individus + ] + # Auteur + if not self.instance.pk: + self.fields["auteur"].initial = self.request.user + # Affichage + self.helper.layout = Layout( + Hidden("famille", value=famille.pk), + Field("auteur", type="hidden"), + Field('selection_piece'), + Field('titre'), + Field('famille'), + Field('individu'), + Field('document'), + Field('observations'), + HTML(EXTRA_SCRIPT), + Commandes(enregistrer_label="%s" % _("Envoyer"), + annuler_url="{% url 'portail_documents' %}", ajouter=False, aide=False, + css_class="pull-right"), + ) def clean(self): # Type de pièce @@ -92,7 +147,8 @@ def clean(self): self.cleaned_data["individu"] = None if piece["type_piece"].public == "famille" else piece["individu"] else: # Si pièce libre - self.cleaned_data["individu"] = Individu.objects.get(pk=self.cleaned_data["choix_individu"]) if self.cleaned_data["choix_individu"] else None + self.cleaned_data["individu"] = Individu.objects.get(pk=self.cleaned_data["individu"]) if self.cleaned_data[ + "individu"] else None # Famille if piece and piece["type_piece"].public == "individu" and piece["type_piece"].valide_rattachement: @@ -100,7 +156,8 @@ def clean(self): # Durée de validité self.cleaned_data["date_debut"] = datetime.date.today() - self.cleaned_data["date_fin"] = piece["type_piece"].Get_date_fin_validite() if piece else datetime.date(2999, 1, 1) + self.cleaned_data["date_fin"] = piece["type_piece"].Get_date_fin_validite() if piece else datetime.date(2999, 1, + 1) return self.cleaned_data @@ -108,6 +165,7 @@ def clean(self): EXTRA_SCRIPT = """ """ diff --git a/noethysweb/portail/urls.py b/noethysweb/portail/urls.py index d4104a22..87564123 100644 --- a/noethysweb/portail/urls.py +++ b/noethysweb/portail/urls.py @@ -110,6 +110,7 @@ # Documents path('documents', documents.View.as_view(), name='portail_documents'), path('documents/transmettre', transmettre_piece.Ajouter.as_view(), name='portail_transmettre_piece'), + path('documents/transmettre/get-individus', secure_ajax_portail(transmettre_piece.Get_individus), name='portail_ajax_inscrire_get_individus_by_famille'), # Activités path('activites', activites.View.as_view(), name='portail_activites'), diff --git a/noethysweb/portail/views/documents.py b/noethysweb/portail/views/documents.py index ead3e582..54b62e77 100644 --- a/noethysweb/portail/views/documents.py +++ b/noethysweb/portail/views/documents.py @@ -4,6 +4,8 @@ # Distribué sous licence GNU GPL. import logging +from django.http import Http404 +from core.models import Rattachement logger = logging.getLogger(__name__) from portail.views.base import CustomView from django.views.generic import TemplateView @@ -18,20 +20,39 @@ class View(CustomView, TemplateView): menu_code = "portail_documents" template_name = "portail/documents.html" + def get_object(self): + """Récupérer l'objet famille ou individu selon l'utilisateur""" + if hasattr(self.request.user, 'famille'): + return self.request.user.famille + elif hasattr(self.request.user, 'individu'): + return self.request.user.individu + else: + raise Http404("Utilisateur non reconnu.") + + def get_famille_object(self): + if hasattr(self.request.user, 'famille'): + return self.request.user.famille + elif hasattr(self.request.user, 'individu'): + rattachement = Rattachement.objects.filter(individu=self.request.user.individu).first() + if rattachement.famille and rattachement.titulaire == 1: + return rattachement.famille + return None + def get_context_data(self, **kwargs): context = super(View, self).get_context_data(**kwargs) context['page_titre'] = _("Documents") + famille = self.get_famille_object() - # Pièces à fournir - context['pieces_fournir'] = utils_pieces_manquantes.Get_pieces_manquantes(famille=self.request.user.famille, exclure_individus=self.request.user.famille.individus_masques.all()) + if famille: + # Pièces à fournir + context['pieces_fournir'] = utils_pieces_manquantes.Get_pieces_manquantes(famille=famille, exclure_individus=famille.individus_masques.all()) - # Importation des documents à télécharger - liste_documents = [] - for document in PortailDocument.objects.all().order_by("titre"): - liste_documents.append({"titre": document.titre, "texte": document.texte, "fichier": document.document, "couleur_fond": document.couleur_fond, "extension": document.Get_extension()}) - for unite_consentement in utils_approbations.Get_approbations_requises(famille=self.request.user.famille, avec_consentements_existants=False).get("consentements", []): - if unite_consentement.document.name: + # Importation des documents à télécharger + liste_documents = [] + for document in PortailDocument.objects.all().order_by("titre"): + liste_documents.append({"titre": document.titre, "texte": document.texte, "fichier": document.document, "couleur_fond": document.couleur_fond, "extension": document.Get_extension()}) + for unite_consentement in utils_approbations.Get_approbations_requises(famille=famille, avec_consentements_existants=False).get("consentements", []): liste_documents.append({"titre": unite_consentement.type_consentement.nom, "texte": "Version du %s" % utils_dates.ConvertDateToFR(unite_consentement.date_debut), "fichier": unite_consentement.document, "couleur_fond": "primary", "extension": unite_consentement.Get_extension()}) - context['liste_documents'] = liste_documents + context['liste_documents'] = liste_documents return context diff --git a/noethysweb/portail/views/transmettre_piece.py b/noethysweb/portail/views/transmettre_piece.py index ead859c5..af76449c 100644 --- a/noethysweb/portail/views/transmettre_piece.py +++ b/noethysweb/portail/views/transmettre_piece.py @@ -4,6 +4,9 @@ # Distribué sous licence GNU GPL. import logging, json +from django.http import Http404, JsonResponse +from core.models import Rattachement +from core.models import Individu logger = logging.getLogger(__name__) from django.urls import reverse_lazy from django.core.serializers.json import DjangoJSONEncoder @@ -14,6 +17,16 @@ from portail.views.base import CustomView +def Get_individus(request): + """Renvoie une liste d'individus pour la famille sélectionnée """ + famille_id = request.POST.get('famille') + individus = Individu.objects.filter(rattachement__famille_id=famille_id) + individu_options = [ + ''.format(individu.pk, individu.Get_nom()) for individu in individus + ] + return JsonResponse(''.join(individu_options), safe=False) + + class Page(CustomView): model = PortailMessage menu_code = "portail_documents" @@ -27,6 +40,23 @@ def get_context_data(self, **kwargs): def get_success_url(self): return reverse_lazy("portail_documents") + def get_object(self): + """Récupérer l'objet famille ou individu selon l'utilisateur""" + if hasattr(self.request.user, 'famille'): + return self.request.user.famille + elif hasattr(self.request.user, 'individu'): + return self.request.user.individu + else: + raise Http404("Utilisateur non reconnu.") + + def get_famille_object(self): + if hasattr(self.request.user, 'famille'): + return self.request.user.famille + elif hasattr(self.request.user, 'individu'): + rattachement = Rattachement.objects.filter(individu=self.request.user.individu).first() + if rattachement.famille and rattachement.titulaire == 1: + return rattachement.famille + return None class Ajouter(Page, crud.Ajouter): @@ -36,10 +66,12 @@ class Ajouter(Page, crud.Ajouter): template_name = "portail/edit.html" def Get_detail_historique(self, instance): - return "Famille=%s, Pièce=%s" % (self.request.user.famille, instance.Get_nom()) + famille = self.get_famille_object() + return "Famille=%s, Pièce=%s" % (famille, instance.Get_nom()) def Apres_form_valid(self, form=None, instance=None): # Mémorisation du renseignement - PortailRenseignement.objects.create(famille=self.request.user.famille, individu=instance.individu, + famille = self.get_famille_object() + PortailRenseignement.objects.create(famille=famille, individu=instance.individu, categorie="famille_pieces", code="Nouvelle pièce", validation_auto=True, nouvelle_valeur=json.dumps(instance.Get_nom(), cls=DjangoJSONEncoder), idobjet=instance.pk) From 33ab98585b724cec69d5439e09746c47eb4ad579 Mon Sep 17 00:00:00 2001 From: "faouzia.takaya" Date: Thu, 13 Mar 2025 03:36:43 +0100 Subject: [PATCH 20/25] ADD Portail famille: Partie Reglements: regroupement des reglements par famille --- .../portail/templates/portail/reglements.html | 63 ++++++++----------- noethysweb/portail/views/reglements.py | 40 +++++++++++- 2 files changed, 63 insertions(+), 40 deletions(-) diff --git a/noethysweb/portail/templates/portail/reglements.html b/noethysweb/portail/templates/portail/reglements.html index c7365422..d6a4c8b0 100644 --- a/noethysweb/portail/templates/portail/reglements.html +++ b/noethysweb/portail/templates/portail/reglements.html @@ -18,15 +18,22 @@
{% endif %} - {# Liste des factures #} - {% embed 'core/box.html' with box_titre=True %} - {% block box_theme %}card-outline card-lightblue{% endblock %} - {% block box_titre %}{% blocktrans %}Liste des règlements{% endblocktrans %}{% endblock %} - {% block box_contenu %} + {% embed 'core/box.html' with box_titre=True %} + {% block box_theme %}card-outline card-lightblue{% endblock %} + {% block box_titre %}{% blocktrans %}Liste des règlements{% endblocktrans %}{% endblock %} + {% block box_contenu %} + +
+ + {% if reglements_grouped %} + {% for famille_nom, reglements in reglements_grouped.items %} +
+ +
+
Famille: {{ famille_nom }}
+
+
- {# Liste des règlements #} -
- {% if liste_reglements %}
Famille d'individu Individu à inscrire Activité souhaitée
{{ demande.individu.prenom }}{{ demande.famille }}{{ demande.individu.nom }} {{ demande.individu.prenom }} {{ demande.nom_activite }}
@@ -43,7 +50,7 @@ - {% for reglement in liste_reglements %} + {% for reglement in reglements %} @@ -67,36 +74,18 @@ {% endfor %}
{{ reglement.date|date:'d/m/Y' }} {{ reglement.mode }}
- {% else %} - {% blocktrans %}Aucun règlement.{% endblocktrans %} - {% endif %} -
+ + - {% endblock box_contenu %} - {% block box_conclusion %}{{ box_conclusion|safe }}{% endblock %} - {% endembed %} + {% endfor %} + {% else %} + {% blocktrans %}Aucun règlement.{% endblocktrans %} + {% endif %} - + {% endblock box_contenu %} + {% block box_conclusion %}{{ box_conclusion|safe }}{% endblock %} +{% endembed %} {% endblock contenu_page %} diff --git a/noethysweb/portail/views/reglements.py b/noethysweb/portail/views/reglements.py index 80d572da..0135e9ae 100644 --- a/noethysweb/portail/views/reglements.py +++ b/noethysweb/portail/views/reglements.py @@ -4,8 +4,9 @@ # Distribué sous licence GNU GPL. import logging, json, datetime +from core.models import Rattachement logger = logging.getLogger(__name__) -from django.http import JsonResponse +from django.http import JsonResponse, Http404 from django.views.generic import TemplateView from django.utils.translation import gettext as _ from core.models import Reglement, ModeleImpression, Recu @@ -49,8 +50,41 @@ class View(CustomView, TemplateView): menu_code = "portail_reglements" template_name = "portail/reglements.html" + def get_object(self): + """Récupérer l'objet famille ou individu selon l'utilisateur""" + if hasattr(self.request.user, 'famille'): + return self.request.user.famille + elif hasattr(self.request.user, 'individu'): + return self.request.user.individu + else: + raise Http404("Utilisateur non reconnu.") + + def get_famille_object(self): + """Récupérer les familles de l'individu si applicable""" + if hasattr(self.request.user, 'famille'): + return [self.request.user.famille] + elif hasattr(self.request.user, 'individu'): + rattachements = Rattachement.objects.filter(individu=self.request.user.individu) + familles = [rattachement.famille for rattachement in rattachements if rattachement.famille and rattachement.titulaire == 1] + return familles if familles else None + return None + def get_context_data(self, **kwargs): context = super(View, self).get_context_data(**kwargs) context['page_titre'] = _("Règlements") - context['liste_reglements'] = Reglement.objects.select_related("mode", "depot").filter(famille=self.request.user.famille).order_by("-date") - return context + familles = self.get_famille_object() + + if familles: + # Fetch all regulations linked to these families + liste_reglements = Reglement.objects.select_related("mode", "depot").filter(famille__in=familles).order_by("-date") + + # Grouping by family name + reglements_grouped = {} + for reglement in liste_reglements: + famille_nom = reglement.famille.nom # Adjust this based on your actual model structure + if famille_nom not in reglements_grouped: + reglements_grouped[famille_nom] = [] + reglements_grouped[famille_nom].append(reglement) + + context['reglements_grouped'] = reglements_grouped + return context \ No newline at end of file From 89ad19e2e4df60ea0264760ac99a9b8fc832c75a Mon Sep 17 00:00:00 2001 From: "faouzia.takaya" Date: Thu, 13 Mar 2025 03:48:36 +0100 Subject: [PATCH 21/25] =?UTF-8?q?ADD=20Portail=20famille:=20Partie=20Factu?= =?UTF-8?q?ration=20:=20Ajout=20de=20param=C3=A9trage=20de=20contact=20Fac?= =?UTF-8?q?turation=20et=20Masqu=C3=A9=20la=20section=20Facturation=20de?= =?UTF-8?q?=20portail=20famille=20pour=20les=20individus=20qui=20sont=20pa?= =?UTF-8?q?s=20contact=20facturation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../templates/portail/facturation.html | 692 +++++++++--------- noethysweb/portail/views/facturation.py | 402 ++++++---- noethysweb/portail/views/menu.py | 15 +- 3 files changed, 626 insertions(+), 483 deletions(-) diff --git a/noethysweb/portail/templates/portail/facturation.html b/noethysweb/portail/templates/portail/facturation.html index c80c471e..fb8e256a 100644 --- a/noethysweb/portail/templates/portail/facturation.html +++ b/noethysweb/portail/templates/portail/facturation.html @@ -15,358 +15,386 @@ {% block contenu_page %} - {# Texte d'introduction #} - {% if parametres_portail.facturation_intro %} -
- {{ parametres_portail.facturation_intro }} -
- {% endif %} - - {# Solde famille #} - {% if parametres_portail.facturation_afficher_solde_famille %} -
- - {% blocktrans %}Solde du compte{% endblocktrans %} : - {% if solde_famille < 0 %} - {% blocktrans %}Il reste un crédit de{% endblocktrans %} {{ solde_famille|inverse_signe|montant }} - {% else %} - {% blocktrans %}Il reste à régler un total de{% endblocktrans %} {{ solde_famille|montant }} - {% endif %} -
- {% endif %} - - {# Texte impayés #} - {% if texte_impayes and parametres_portail.facturation_afficher_solde_facture %} -
- {{ texte_impayes|safe }} -
- {% endif %} - - {# Prélèvement activé #} - {% if prelevement_actif %} -
- {% blocktrans %}Vous êtes abonné au prélèvement automatique.{% endblocktrans %} -
- {% endif %} - - {# Paiements en cours #} - {% if liste_paiements %} -
- - {% if liste_paiements|length == 1 %} - {% blocktrans %}Un paiement est en attente.{% endblocktrans %} - {% else %} - {{ liste_paiements|length }} {% blocktrans %}paiements sont en attente.{% endblocktrans %} + + +{% for family in all_familles %} +{# Block for each family #} +{% embed 'core/box.html' with box_titre=True %} +{% block box_theme %}card-outline card-lightblue custom-card{% endblock %} +{% block box_titre %}{% blocktrans %} Famille: {{ family }} {% endblocktrans %}{% endblock %} +{% block card_body_classe %}p-0{% endblock %} + {% block box_contenu %} +
+ {% if user == family.contact_facturation %} + {# Texte d'introduction #} + {% if parametres_portail.facturation_intro %} +
+ {{ parametres_portail.facturation_intro }} +
+ {% endif %} + {% for famille in familles %} + + {% if famille.famille == family %} + {# Solde famille #} + + {% if parametres_portail.facturation_afficher_solde_famille %} +
+ + {% blocktrans %}Solde du compte{% endblocktrans %} : + {% if famille.solde < 0 %} + {% blocktrans %}Il reste un crédit de{% endblocktrans %} {{ famille.solde|inverse_signe|montant }} + {% else %} + {% blocktrans %}Il reste à régler un total de{% endblocktrans %} {{ famille.solde|montant }} + {% endif %} +
{% endif %} - {% blocktrans %}Si vous avez interrompu la procédure de paiement, vous devez attendre quelques minutes avant de réactualiser cette page pour voir de nouveau la facture impayée apparaître ci-dessous.{% endblocktrans %} -
- {% endif %} - - {# Récapitulatif des factures impayées #} - {% if texte_impayes and parametres_portail.facturation_afficher_solde_facture %} - {% embed 'core/box.html' with box_titre=True %} - {% block box_theme %}card-outline card-danger{% endblock %} - {% block box_titre %}{% blocktrans %}Récapitulatif des impayés{% endblocktrans %}{% endblock %} - {% block card_body_classe %}p-0{% endblock %} - {% block box_contenu %} -
- - - - - - - - - - {% for facture in liste_factures_impayees %} - - - - - - - {% endfor %} - - {% for periode in liste_periodes_prefacturation %} - - - - - - - {% endfor %} - - {% for prestation_cotisation in liste_cotisations_prefacturation %} - - - - - - - {% endfor %} - -
{% trans "Intitulé" %}{% trans "Montant" %}{% blocktrans %}Déjà réglé{% endblocktrans %}{% blocktrans %}Reste à régler{% endblocktrans %}
{% trans "Facture" %} n°{{ facture.numero }}{{ facture.total|montant }}{{ facture.Get_regle_actuel|montant }}{{ facture.solde_actuel|montant }}
{{ periode.nom }}{{ periode.total|montant }}{{ periode.regle|montant }}{{ periode.solde|montant }}
{{ prestation_cotisation.label }}{{ prestation_cotisation.total|montant }}{{ prestation_cotisation.regle|montant }}{{ prestation_cotisation.solde|montant }}
+ + {# Texte impayés #} + {% if famille.texte_impayes and parametres_portail.facturation_afficher_solde_facture %} +
+ {{ famille.texte_impayes|safe }}
- {% endblock box_contenu %} - {% endembed %} - {% endif %} - - {# Paiement en ligne #} - {% if texte_impayes and parametres_portail.paiement_ligne_systeme and paiement_actif %} - {% embed 'core/box.html' with box_titre=True %} - {% block box_theme %}card-outline card-lightblue{% endblock %} - {% block box_titre %}{% blocktrans %}Payer en ligne{% endblocktrans %}{% endblock %} - {% block box_contenu %} - {% if parametres_portail.paiement_ligne_systeme|is_in_list:"payzen;demo" %} - Logo Payzen -

{% blocktrans %}Vous pouvez payer en ligne à l'aide de votre carte bancaire. Les transactions effectuées sur ce portail sont sécurisées par la plateforme de paiement en ligne PayZen. Cliquez sur le bouton ci-dessous pour sélectionner le montant à régler et accéder au site sécurisé de notre partenaire.{% endblocktrans %}

- {% else %} - Logo Payfip -

{% blocktrans %}Vous pouvez payer en ligne à l'aide de votre carte bancaire. Les transactions effectuées sur ce portail sont sécurisées par la plateforme de paiement en ligne PAYFIP. Cliquez sur le bouton ci-dessous pour sélectionner le montant à régler et accéder au site sécurisé de notre partenaire.{% endblocktrans %}

- {% endif %} -
- {% blocktrans %}Payer en ligne{% endblocktrans %} + {% endif %} + + {# Prélèvement activé #} + {% if famille.prelevement_actif %} +
+ {% blocktrans %}Vous êtes abonné au prélèvement automatique.{% endblocktrans %}
- {% endblock box_contenu %} - {% block box_conclusion %}{{ box_conclusion|safe }}{% endblock %} - {% endembed %} - - {# Modal Sélection des impayés à régler #} - {% embed 'core/modal.html' with modal_footer=True %} - {% block modal_id %}modal_selection_factures{% endblock %} - {% block modal_titre %}{% blocktrans %}Payer en ligne{% endblocktrans %}{% endblock %} - {% block modal_classe %}modal-lg{% endblock %} - {% block modal_body %} -

{% blocktrans %}Cochez les sommes à payer et cliquez sur le bouton Valider pour accéder à la page de paiement sécurisée de notre partenaire.{% endblocktrans %}

-
-
- - - - - - - - - - - - {% for facture in liste_factures_impayees %} - - - - - - - - {% endfor %} - - {% for periode in liste_periodes_prefacturation %} - - - - - - - - {% endfor %} - - {% for prestation_cotisation in liste_cotisations_prefacturation %} - - - - - - - - {% endfor %} - -
{% trans "Sélection" %}{% trans "Intitulé" %}{% trans "Montant" %}{% blocktrans %}Déjà réglé{% endblocktrans %}{% blocktrans %}Reste à régler{% endblocktrans %}
Facture n°{{ facture.numero }}{{ facture.total|montant }}{{ facture.Get_regle_actuel|montant }}{{ facture.solde_actuel|montant }}
{{ periode.nom }}{{ periode.total|montant }}{{ periode.regle|montant }}{{ periode.solde|montant }}
{{ prestation_cotisation.label }}{{ prestation_cotisation.total|montant }}{{ prestation_cotisation.regle|montant }}{{ prestation_cotisation.solde|montant }}
- -
- {% blocktrans %}Montant du règlement{% endblocktrans %} : 0.00 € -
+ {% endif %} - {% if parametres_portail.payzen_echelonnement %} -
-
- {% blocktrans %}Payer en 3 fois sans frais (Echelonnement sur 3 mois){% endblocktrans %} -
+ {# Paiements en cours #} + {% if famille.liste_paiements %} +
+ + {% if famille.liste_paiements|length == 1 %} + {% blocktrans %}Un paiement est en attente.{% endblocktrans %} + {% else %} + {{ famille.liste_paiements|length }} {% blocktrans %}paiements sont en attente.{% endblocktrans %} {% endif %} + {% blocktrans %}Si vous avez interrompu la procédure de paiement, vous devez attendre quelques minutes avant de réactualiser cette page pour voir de nouveau la facture impayée apparaître ci-dessous.{% endblocktrans %}
- {% endblock %} - - {% block modal_footer %} - - - - {% endblock %} - - {% endembed %} - - - - + }); - {% endif %} + - {# Liste des factures #} - {% embed 'core/box.html' with box_titre=True %} - {% block box_theme %}card-outline card-lightblue{% endblock %} - {% block box_titre %}{% blocktrans %}Liste des dernières factures{% endblocktrans %}{% endblock %} - {% block card_body_classe %}p-0{% endblock %} - {% block box_contenu %} + {% endif %} {# Liste des factures #} -
- {% if liste_factures %} - - - - - - {% if parametres_portail.facturation_afficher_numero_facture %} - - {% endif %} - - {% if parametres_portail.facturation_afficher_solde_facture %} - - {% endif %} - {% if parametres_portail.facturation_autoriser_telechargement_facture or parametres_portail.facturation_autoriser_detail_facture %} - - {% endif %} - - - - {% for facture in liste_factures %} - - - - {% if parametres_portail.facturation_afficher_numero_facture %} - - {% endif %} - - {% if parametres_portail.facturation_afficher_solde_facture %} - - {% endif %} - {% if parametres_portail.facturation_autoriser_telechargement_facture or parametres_portail.facturation_autoriser_detail_facture %} - + {% endif %} + + {% endfor %} + +
{% trans "Du" %}{% trans "Au" %}N°{% trans "Montant" %}{% blocktrans %}Reste à régler{% endblocktrans %}{% trans "Actions" %}
{{ facture.date_debut|date:'d/m/Y' }}{{ facture.date_fin|date:'d/m/Y' }}{{ facture.numero }}{{ facture.total|montant }}{{ facture.solde_actuel|montant }} - {% if parametres_portail.facturation_autoriser_detail_facture %} - + {% embed 'core/box.html' with box_titre=True %} + {% block box_theme %}card-outline card-lightblue{% endblock %} + {% block box_titre %}{% blocktrans %}Liste des dernières factures{% endblocktrans %}{% endblock %} + {% block card_body_classe %}p-0{% endblock %} + {% block box_contenu %} + {# Liste des factures #} +
+ {% if famille.liste_factures %} + + + + + + + {% if parametres_portail.facturation_afficher_numero_facture %} + + {% endif %} + + {% if parametres_portail.facturation_afficher_solde_facture %} + + {% endif %} + {% if parametres_portail.facturation_autoriser_telechargement_facture or parametres_portail.facturation_autoriser_detail_facture %} + + {% endif %} + + + + {% for facture in famille.liste_factures %} + + + + {% if parametres_portail.facturation_afficher_numero_facture %} + {% endif %} - {% if parametres_portail.facturation_autoriser_telechargement_facture and parametres_portail.facturation_modele_impression_facture %} - + + {% if parametres_portail.facturation_afficher_solde_facture %} + {% endif %} - - {% endif %} - - {% endfor %} - -
{% trans "Du" %}{% trans "Au" %}N°{% trans "Montant" %}{% blocktrans %}Reste à régler{% endblocktrans %}{% trans "Actions" %}
{{ facture.date_debut|date:'d/m/Y' }}{{ facture.date_fin|date:'d/m/Y' }}{{ facture.numero }}{{ facture.total|montant }}{{ facture.solde_actuel|montant }}
- {% else %} -
{% blocktrans %}Aucune facture{% endblocktrans %}
- {% endif %} -
+ {% if parametres_portail.facturation_autoriser_telechargement_facture or parametres_portail.facturation_autoriser_detail_facture %} +
+ {% if parametres_portail.facturation_autoriser_detail_facture %} + + {% endif %} + {% if parametres_portail.facturation_autoriser_telechargement_facture and parametres_portail.facturation_modele_impression_facture %} + + {% endif %} +
+ {% else %} +
{% blocktrans %}Aucune facture{% endblocktrans %}
+ {% endif %} +
+ + {# Lien de téléchargement de la facture #} + - {# Lien de téléchargement de la facture #} - + {% endblock box_contenu %} + {% block box_conclusion %}{{ box_conclusion|safe }}{% endblock %} + {% endembed %} - {% endblock box_contenu %} - {% block box_conclusion %}{{ box_conclusion|safe }}{% endblock %} - {% endembed %} + {# Modal Détail d'une facture #} + {% embed 'core/modal.html' %} + {% block modal_id %}modal_detail_facture{% endblock %} + {% block modal_classe %}modal-xl modal-dialog-scrollable{% endblock %} + {% block modal_titre %}{% blocktrans %}Détail d'une facture{% endblocktrans %}{% endblock %} + {% block modal_body %} +
+ {% endblock %} + {% endembed %} - {# Modal Détail d'une facture #} - {% embed 'core/modal.html' %} - {% block modal_id %}modal_detail_facture{% endblock %} - {% block modal_classe %}modal-xl modal-dialog-scrollable{% endblock %} - {% block modal_titre %}{% blocktrans %}Détail d'une facture{% endblocktrans %}{% endblock %} - {% block modal_body %} -
- {% endblock %} - {% endembed %} + {% endif %} +{% endfor %} + {% else %} +
+ Vous ne pouvez pas consulter ici les données de facturation comme vous n'êtes pas contact de facturation. +
+{% endif %} +{% endblock box_contenu %} +{% endembed %} +
+{% endfor %} -{% endblock contenu_page %} +{% endblock contenu_page %} \ No newline at end of file diff --git a/noethysweb/portail/views/facturation.py b/noethysweb/portail/views/facturation.py index 2afa0552..5737e1e0 100644 --- a/noethysweb/portail/views/facturation.py +++ b/noethysweb/portail/views/facturation.py @@ -4,9 +4,14 @@ # Distribué sous licence GNU GPL. import logging, decimal, sys, datetime, re, copy, json +from core.models import Famille +from core.models import Rattachement + +from core.models import Individu + logger = logging.getLogger(__name__) from django.urls import reverse -from django.http import JsonResponse, HttpResponse +from django.http import JsonResponse, HttpResponse, Http404 from django.utils.translation import gettext as _ from django.views.generic import TemplateView from django.views.decorators.csrf import csrf_exempt @@ -72,10 +77,23 @@ def effectuer_paiement_en_ligne(request): # Vérifie que le montant est supérieur au montant minimal fixé if montant_reglement < decimal.Decimal(parametres_portail.get("paiement_ligne_montant_minimal", 0.0)): - return JsonResponse({"erreur": _("Le paiement en ligne nécessite un montant minimal de %.2f € !") % parametres_portail.paiement_ligne_montant_minimal}, status=401) + return JsonResponse({"erreur": _("Le paiement en ligne nécessite un montant minimal de %.2f € !") % parametres_portail["paiement_ligne_montant_minimal"]}, status=401) + + # Vérifie si l'utilisateur est un membre d'une famille ou un individu + famille = getattr(request.user, 'famille', None) + individu = getattr(request.user, 'individu', None) + + if famille: + email = famille.mail + elif individu: + email = individu.mail + # Si l'utilisateur est un individu, rechercher la famille s'il est rattaché + famille = Famille.objects.filter(nom__icontains=individu).first() + else: + return JsonResponse({"erreur": _("Utilisateur non reconnu. Le paiement en ligne est impossible sans information d'identification.")}, status=401) - # Vérifie que la famille a une adresse mail - if not request.user.famille.mail: + # Vérifie que l'email est disponible + if not email: return JsonResponse({"erreur": _("Aucune adresse mail n'a été renseignée sur votre dossier. Le paiement en ligne est impossible sans cette information.")}, status=401) # Mémorise les numéros de factures et la ventilation @@ -91,7 +109,7 @@ def effectuer_paiement_en_ligne(request): ventilation = [] for type_impaye in ["facture", "periode", "cotisation"]: for ID, solde in dict_ventilation[type_impaye].items(): - if type_impaye == "facture" : prefixe = "F" + if type_impaye == "facture": prefixe = "F" if type_impaye == "periode": prefixe = "P" if type_impaye == "cotisation": prefixe = "C" ventilation.append("%s%d#%s" % (prefixe, ID, solde)) @@ -100,14 +118,14 @@ def effectuer_paiement_en_ligne(request): # --------------------------- Mode démo ----------------------------- if parametres_portail.get("paiement_ligne_systeme") == "demo": - logger.debug("Page EFFECTUER_PAIEMENT_EN_LIGNE MODE DEMO (Famille %s) : montant=%s ventilation_str=%s", request.user.famille, str(montant_reglement), ventilation_str) + logger.debug("Page EFFECTUER_PAIEMENT_EN_LIGNE MODE DEMO (Famille %s) : montant=%s ventilation_str=%s", famille, str(montant_reglement), ventilation_str) return JsonResponse({"systeme_paiement": "demo", "texte": "Paiement impossible, vous êtes en mode démo !"}) # ----------------------- Paiement avec PAYFIP ------------------------- if parametres_portail.get("paiement_ligne_systeme") == "payfip": payfip_mode = parametres_portail.get("payfip_mode") - logger.debug("Page EFFECTUER_PAIEMENT EN LIGNE : famille=%s payfip_mode=%s", request.user.famille, payfip_mode) + logger.debug("Page EFFECTUER_PAIEMENT EN LIGNE : famille=%s payfip_mode=%s", famille, payfip_mode) if payfip_mode == "validation": saisie = "X" elif payfip_mode == "production": saisie = "A" @@ -115,35 +133,34 @@ def effectuer_paiement_en_ligne(request): # il y a plus d'une facture sélectionnée if len(dict_ventilation["facture"]) > 1: - logger.debug(u"Page EFFECTUER_PAIEMENT_EN_LIGNE (%s): plus d'une facture selectionnee pour PAYFIP NON TRAITE", request.user.famille) + logger.debug(u"Page EFFECTUER_PAIEMENT_EN_LIGNE (%s): plus d'une facture selectionnee pour PAYFIP NON TRAITE", famille) return JsonResponse({"erreur": "Paiement en ligne multi-factures impossible"}, status=401) # Vérifie qu'il n'y a pas de préfacturation dedans - if len(dict_ventilation["periode"]) > 0 : - logger.debug(u"Page EFFECTUER_PAIEMENT_EN_LIGNE (%s): Il n'est pas possible de régler de la préfacturation avec PAYFIP", request.user.famille) - return JsonResponse({"erreur": "Paiement de la prefacturation impossible avec TIPI"}, status=401) + if len(dict_ventilation["periode"]) > 0: + logger.debug(u"Page EFFECTUER_PAIEMENT_EN_LIGNE (%s): Il n'est pas possible de régler de la préfacturation avec PAYFIP", famille) + return JsonResponse({"erreur": "Paiement de la préfacturation impossible avec TIPI"}, status=401) # Envoi de la requete facture = liste_factures[0] if not facture.regie: - logger.debug(u"Page EFFECTUER_PAIEMENT_EN_LIGNE TIPI (%s): Aucune régie n'a été paramétrée.", request.user.famille) + logger.debug(u"Page EFFECTUER_PAIEMENT_EN_LIGNE TIPI (%s): Aucune régie n'a été paramétrée.", famille) return JsonResponse({"erreur": "Aucune régie n'a été paramétrée. Contactez l'administrateur du portail."}, status=401) p = Payment("tipi", {'numcli': facture.regie.numclitipi, "automatic_return_url": request.build_absolute_uri(reverse("retour_payfip"))}) objet = "Paiement %s" % utils_texte.Supprimer_accents(facture.regie.nom) - refdet = "%s%06d" % (facture.prefixe.prefixe if facture.prefixe else "", facture.numero) requete = p.request( amount=str(montant_reglement), exer=str(facture.date_debut.year), - refdet=refdet, + refdet="%06d" % facture.numero, objet=objet, - email=request.user.famille.mail, + email=email, saisie=saisie) - logger.debug(u"Page EFFECTUER_PAIEMENT_EN_LIGNE (%s): requete: %s // systeme_paiement(%s)", request.user.famille, requete, "payfip") + logger.debug(u"Page EFFECTUER_PAIEMENT_EN_LIGNE (%s): requete: %s // systeme_paiement(%s)", famille, requete, "payfip") # Enregistrement du paiement - Paiement.objects.create(famille=request.user.famille, systeme_paiement="payfip", idtransaction=requete[0], - refdet=refdet, montant=montant_reglement, objet=objet, saisie=saisie, ventilation=ventilation_str) + Paiement.objects.create(famille=famille, systeme_paiement="payfip", idtransaction=requete[0], + refdet=facture.numero, montant=montant_reglement, objet=objet, saisie=saisie, ventilation=ventilation_str) return JsonResponse({"systeme_paiement": "payfip", "urltoredirect": requete[2]}) @@ -168,13 +185,13 @@ def effectuer_paiement_en_ligne(request): vads_payment_config = "MULTI_EXT:%s;%s;%s" % (paiement_1, paiement_2, paiement_3) else: vads_payment_config = "SINGLE" - requete = p.request(amount=montant_reglement, email=request.user.famille.mail, vads_payment_config=vads_payment_config) + requete = p.request(amount=montant_reglement, email=email, vads_payment_config=vads_payment_config) transaction_id, f, form = requete - logger.debug("Page EFFECTUER_PAIEMENT_EN_LIGNE PAYZEN (Famille %s) : IDtransaction=%s montant=%s ventilation_str=%s", request.user.famille, transaction_id, str(montant_reglement), ventilation_str) + logger.debug("Page EFFECTUER_PAIEMENT_EN_LIGNE PAYZEN (Famille %s) : IDtransaction=%s montant=%s ventilation_str=%s", famille, transaction_id, str(montant_reglement), ventilation_str) # Enregistrement du paiement - Paiement.objects.create(famille=request.user.famille, systeme_paiement="payzen", idtransaction=transaction_id, + Paiement.objects.create(famille=famille, systeme_paiement="payzen", idtransaction=transaction_id, montant=montant_reglement, saisie=parametres_portail.get("payzen_mode"), ventilation=ventilation_str) # Renvoie le formulaire de paiement au template @@ -372,15 +389,14 @@ def Enregistrement_reglement(paiement=None, vads_payment_config=None): for facture in factures: facture.Maj_solde_actuel() - def get_detail_facture(request): """ Génére un texte HTML contenant le détail d'une facture """ idfacture = int(request.POST.get("idfacture", 0)) # Importation de la facture facture = Facture.objects.get(pk=idfacture) - if facture.famille != request.user.famille: - return JsonResponse({"texte": "Accès interdit"}, status=401) + # if facture.famille != request.user.famille: + # return JsonResponse({"texte": "Accès interdit"}, status=401) # Importation des prestations prestations = Prestation.objects.select_related("individu", "activite").filter(facture=facture).order_by("date") @@ -416,136 +432,226 @@ class View(CustomView, TemplateView): menu_code = "portail_facturation" template_name = "portail/facturation.html" + def get_object(self): + """Récupérer l'objet famille ou individu selon l'utilisateur""" + if hasattr(self.request.user, 'famille'): + return self.request.user.famille + elif hasattr(self.request.user, 'individu'): + return self.request.user.individu + else: + raise Http404("Utilisateur non reconnu.") + + def get_famille_object(self): + """Récupérer les familles de l'individu si applicable""" + if hasattr(self.request.user, 'famille'): + return [self.request.user.famille] + elif hasattr(self.request.user, 'individu'): + rattachements = Rattachement.objects.filter(individu=self.request.user.individu) + familles = [rattachement.famille for rattachement in rattachements if rattachement.famille and rattachement.titulaire == 1] + return familles if familles else None + return None + def get_context_data(self, **kwargs): context = super(View, self).get_context_data(**kwargs) context['page_titre'] = "Facturation" + familles = self.get_famille_object() + # Pass the families to the context + context['all_familles'] = familles + individu = Individu.objects.filter(utilisateur=self.request.user).first() + context['user'] = individu + familles_data=[] + for famille in familles: + if famille.contact_facturation and famille.contact_facturation.idindividu == individu.idindividu: + context['contact_facturation'] = famille.contact_facturation + print('le contact de facturation est : ',famille.contact_facturation) + prelevement_actif=[] + paiement_actif=[] + liste_paiements=[] + # Vérifie si la famille est abonnée au prélèvement automatique + prelevement_actif = Mandat.objects.filter(famille=famille, actif=True).exists() + context["prelevement_actif"]=prelevement_actif + paiement_actif= not (context['parametres_portail']["paiement_ligne_off_si_prelevement"] and context["prelevement_actif"]) + context['paiement_actif'] =paiement_actif + + # Importation des paiements PAYFIP en cours + liste_paiements= Paiement.objects.filter(famille=famille, systeme_paiement="payfip", resultat__isnull=True, horodatage__gt=datetime.datetime.now() - datetime.timedelta(minutes=5)) + context['liste_paiements'] =liste_paiements + + + dict_paiements = {"F": {}, "P": {}, "C": {}} + for paiement in context['liste_paiements']: + for texte in paiement.ventilation.split(","): + type_impaye = texte[0] + ID, montant = texte[1:].split("#") + ID, montant = int(ID), decimal.Decimal(montant) + dict_paiements[type_impaye].setdefault(ID, decimal.Decimal(0)) + dict_paiements[type_impaye][ID] += montant + + # Affichage des notifications de paiements récents + for paiement in Paiement.objects.filter(famille=famille, notification__isnull=True, horodatage__gt=datetime.datetime.now() - datetime.timedelta(hours=1)): + if paiement.resultat == "PAID": + messages.add_message(self.request, messages.SUCCESS, "Le paiement en ligne de %.2f Euros a été enregistré avec succès" % paiement.montant) + elif paiement.resultat == "DENIED": + messages.add_message(self.request, messages.ERROR, "Le paiement en ligne de %.2f Euros a été refusé" % paiement.montant) + elif paiement.resultat == "CANCELLED": + messages.add_message(self.request, messages.ERROR, "Le paiement en ligne de %.2f Euros a été annulé" % paiement.montant) + else: + messages.add_message(self.request, messages.ERROR, "Le paiement en ligne de %.2f Euros a rencontré une erreur. La notification de paiement semble absente." % paiement.montant) + paiement.notification = datetime.datetime.now() + paiement.save() + + # Importation des factures impayées + liste_factures = [] + liste_factures_impayees = [] + total_factures_impayees = decimal.Decimal(0) + for facture in Facture.objects.filter(famille=famille).exclude(etat="annulation").order_by("-date_edition"): + if facture.pk in dict_paiements["F"]: + facture.regle = facture.total + facture.solde_actuel = decimal.Decimal(0) + if facture.solde_actuel and (not facture.date_limite_paiement or datetime.date.today() <= facture.date_limite_paiement): + liste_factures_impayees.append(facture) + total_factures_impayees += facture.solde_actuel + liste_factures.append(facture) + context['liste_factures_impayees'] = liste_factures_impayees + context['liste_factures'] = liste_factures + + # Importation de la préfacturation des périodes + total_periodes_impayees = decimal.Decimal(0) + liste_finale_periodes = [] + + # Récupération des périodes de réservations + liste_dates_extremes = [] + liste_periodes = [] + for periode in PortailPeriode.objects.select_related("activite").prefetch_related("categories").filter(prefacturation=True): + if periode.Is_famille_authorized(famille=famille): + periode.total = decimal.Decimal(0) + periode.regle = decimal.Decimal(0) + periode.solde = decimal.Decimal(0) + liste_periodes.append(periode) + liste_dates_extremes.append(periode.date_debut) + liste_dates_extremes.append(periode.date_fin) - # Vérifie si la famille est abonnée au prélèvement automatique - context["prelevement_actif"] = Mandat.objects.filter(famille=self.request.user.famille, actif=True).exists() - context['paiement_actif'] = not (context['parametres_portail']["paiement_ligne_off_si_prelevement"] and context["prelevement_actif"]) - - # Importation des paiements PAYFIP en cours - context['liste_paiements'] = Paiement.objects.filter(famille=self.request.user.famille, systeme_paiement="payfip", resultat__isnull=True, horodatage__gt=datetime.datetime.now() - datetime.timedelta(minutes=5)) - dict_paiements = {"F": {}, "P": {}, "C": {}} - for paiement in context['liste_paiements']: - for texte in paiement.ventilation.split(","): - type_impaye = texte[0] - ID, montant = texte[1:].split("#") - ID, montant = int(ID), decimal.Decimal(montant) - dict_paiements[type_impaye].setdefault(ID, decimal.Decimal(0)) - dict_paiements[type_impaye][ID] += montant - - # Affichage des notifications de paiements récents - for paiement in Paiement.objects.filter(famille=self.request.user.famille, notification__isnull=True, horodatage__gt=datetime.datetime.now() - datetime.timedelta(hours=1)): - if paiement.resultat == "PAID": - messages.add_message(self.request, messages.SUCCESS, "Le paiement en ligne de %.2f Euros a été enregistré avec succès" % paiement.montant) - elif paiement.resultat == "DENIED": - messages.add_message(self.request, messages.ERROR, "Le paiement en ligne de %.2f Euros a été refusé" % paiement.montant) - elif paiement.resultat == "CANCELLED": - messages.add_message(self.request, messages.ERROR, "Le paiement en ligne de %.2f Euros a été annulé" % paiement.montant) - else: - messages.add_message(self.request, messages.ERROR, "Le paiement en ligne de %.2f Euros a rencontré une erreur. La notification de paiement semble absente." % paiement.montant) - paiement.notification = datetime.datetime.now() - paiement.save() - - # Importation des factures impayées - liste_factures = [] - liste_factures_impayees = [] - total_factures_impayees = decimal.Decimal(0) - for facture in Facture.objects.filter(famille=self.request.user.famille).exclude(etat="annulation").order_by("-date_edition"): - if facture.pk in dict_paiements["F"]: - facture.regle = facture.total - facture.solde_actuel = decimal.Decimal(0) - if facture.solde_actuel and (not facture.date_limite_paiement or datetime.date.today() <= facture.date_limite_paiement): - liste_factures_impayees.append(facture) - total_factures_impayees += facture.solde_actuel - liste_factures.append(facture) - context['liste_factures_impayees'] = liste_factures_impayees - context['liste_factures'] = liste_factures - - # Importation de la préfacturation des périodes - total_periodes_impayees = decimal.Decimal(0) - liste_finale_periodes = [] - - # Récupération des périodes de réservations - liste_dates_extremes = [] - liste_periodes = [] - for periode in PortailPeriode.objects.select_related("activite").prefetch_related("categories").filter(prefacturation=True): - if periode.Is_famille_authorized(famille=self.request.user.famille): - periode.total = decimal.Decimal(0) - periode.regle = decimal.Decimal(0) - periode.solde = decimal.Decimal(0) - liste_periodes.append(periode) - liste_dates_extremes.append(periode.date_debut) - liste_dates_extremes.append(periode.date_fin) - if liste_periodes: - date_min = min(liste_dates_extremes) - date_max = max(liste_dates_extremes) - - # Recherche les impayés par période de réservations - if liste_periodes: - ventilations = Ventilation.objects.values("prestation").filter(famille=self.request.user.famille, prestation__date__gte=date_min, prestation__date__lte=date_max).annotate(total=Sum("montant")) + if periode.Is_individu_authorized(individu=self.request.user.individu): + periode.total = decimal.Decimal(0) + periode.regle = decimal.Decimal(0) + periode.solde = decimal.Decimal(0) + liste_periodes.append(periode) + liste_dates_extremes.append(periode.date_debut) + liste_dates_extremes.append(periode.date_fin) + if liste_periodes: + date_min = min(liste_dates_extremes) + date_max = max(liste_dates_extremes) + + # Recherche les impayés par période de réservations + if liste_periodes: + ventilations = Ventilation.objects.values("prestation").filter(famille=famille, prestation__date__gte=date_min, prestation__date__lte=date_max).annotate(total=Sum("montant")) + dict_ventilations = {ventilation["prestation"]: ventilation["total"] for ventilation in ventilations} + for prestation in Prestation.objects.filter(famille=famille, date__gte=date_min, date__lte=date_max, facture__isnull=True): + solde_prestation = prestation.montant - dict_ventilations.get(prestation.pk, decimal.Decimal(0)) + if solde_prestation > decimal.Decimal(0): + for periode in liste_periodes: + if periode.date_debut <= prestation.date <= periode.date_fin and prestation.activite_id == periode.activite_id: + periode.total += prestation.montant + periode.regle += dict_ventilations.get(prestation.pk, decimal.Decimal(0)) + periode.solde += solde_prestation + + # Ajoute les paiements en cours + for periode in liste_periodes: + if periode.pk in dict_paiements["P"]: + periode.regle = periode.total + periode.solde = decimal.Decimal(0) + total_periodes_impayees += periode.solde + if periode.solde: + liste_finale_periodes.append(periode) + + context["liste_finale_periodes"] = liste_finale_periodes + # Importation de la préfacturation des cotisations + total_cotisations_impayees = decimal.Decimal(0) + liste_finale_cotisations = [] + + ventilations = Ventilation.objects.values("prestation").filter(famille=famille, prestation__cotisation__isnull=False).annotate(total=Sum("montant")) dict_ventilations = {ventilation["prestation"]: ventilation["total"] for ventilation in ventilations} - for prestation in Prestation.objects.filter(famille=self.request.user.famille, date__gte=date_min, date__lte=date_max, facture__isnull=True): + for prestation in Prestation.objects.select_related("cotisation").filter(famille=famille, cotisation__isnull=False, facture__isnull=True, cotisation__unite_cotisation__prefacturation=True): solde_prestation = prestation.montant - dict_ventilations.get(prestation.pk, decimal.Decimal(0)) if solde_prestation > decimal.Decimal(0): - for periode in liste_periodes: - if periode.date_debut <= prestation.date <= periode.date_fin and prestation.activite_id == periode.activite_id: - periode.total += prestation.montant - periode.regle += dict_ventilations.get(prestation.pk, decimal.Decimal(0)) - periode.solde += solde_prestation - - # Ajoute les paiements en cours - for periode in liste_periodes: - if periode.pk in dict_paiements["P"]: - periode.regle = periode.total - periode.solde = decimal.Decimal(0) - total_periodes_impayees += periode.solde - if periode.solde: - liste_finale_periodes.append(periode) - - context["liste_periodes_prefacturation"] = liste_finale_periodes - - # Importation de la préfacturation des cotisations - total_cotisations_impayees = decimal.Decimal(0) - liste_finale_cotisations = [] - - ventilations = Ventilation.objects.values("prestation").filter(famille=self.request.user.famille, prestation__cotisation__isnull=False).annotate(total=Sum("montant")) - dict_ventilations = {ventilation["prestation"]: ventilation["total"] for ventilation in ventilations} - for prestation in Prestation.objects.select_related("cotisation").filter(famille=self.request.user.famille, cotisation__isnull=False, facture__isnull=True, cotisation__unite_cotisation__prefacturation=True): - solde_prestation = prestation.montant - dict_ventilations.get(prestation.pk, decimal.Decimal(0)) - if solde_prestation > decimal.Decimal(0): - prestation.total = prestation.montant - prestation.regle = dict_ventilations.get(prestation.pk, decimal.Decimal(0)) - prestation.solde = solde_prestation - - # Ajoute les paiements en cours - if prestation.pk in dict_paiements["C"]: - prestation.regle = prestation.total - prestation.solde = decimal.Decimal(0) - total_cotisations_impayees += prestation.solde - if prestation.solde: - liste_finale_cotisations.append(prestation) - - context["liste_cotisations_prefacturation"] = liste_finale_cotisations - - # Création du texte de rappel des impayés - liste_impayes = [] - texte_impayes = None - if liste_factures_impayees: - liste_impayes.append(_("1 facture à régler") if len(liste_factures_impayees) == 1 else _("%d factures à régler") % len(liste_factures_impayees)) - if liste_finale_periodes or liste_finale_cotisations: - liste_impayes.append(_("des prestations à régler en avance")) - if liste_impayes: - texte_impayes = _("Il reste %s pour un total de") % utils_texte.Convert_liste_to_texte_virgules(liste_impayes) + " %s" % utils_texte.Formate_montant(total_factures_impayees + total_periodes_impayees + total_cotisations_impayees) - context["texte_impayes"] = texte_impayes - - # Calcul du solde - if context["parametres_portail"].get("facturation_afficher_solde_famille", False): - total_prestations = Prestation.objects.values('famille_id').filter(famille=self.request.user.famille).aggregate(total=Sum("montant")) - total_reglements = Reglement.objects.values('famille_id').filter(famille=self.request.user.famille).aggregate(total=Sum("montant")) - total_du = total_prestations["total"] if total_prestations["total"] else decimal.Decimal(0) - total_regle = total_reglements["total"] if total_reglements["total"] else decimal.Decimal(0) - context["solde_famille"] = total_du - total_regle + prestation.total = prestation.montant + prestation.regle = dict_ventilations.get(prestation.pk, decimal.Decimal(0)) + prestation.solde = solde_prestation + + # Ajoute les paiements en cours + if prestation.pk in dict_paiements["C"]: + prestation.regle = prestation.total + prestation.solde = decimal.Decimal(0) + total_cotisations_impayees += prestation.solde + if prestation.solde: + liste_finale_cotisations.append(prestation) + + context["liste_finale_cotisations"] = liste_finale_cotisations + + # Création du texte de rappel des impayés + liste_impayes = [] + solde=[] + texte_impayes = None + if liste_factures_impayees: + liste_impayes.append(_("1 facture à régler") if len(liste_factures_impayees) == 1 else _("%d factures à régler") % len(liste_factures_impayees)) + if liste_finale_periodes or liste_finale_cotisations: + liste_impayes.append(_("des prestations à régler en avance")) + if liste_impayes: + texte_impayes = _("Il reste %s pour un total de") % utils_texte.Convert_liste_to_texte_virgules(liste_impayes) + " %s" % utils_texte.Formate_montant(total_factures_impayees + total_periodes_impayees + total_cotisations_impayees) + context["texte_impayes"] = texte_impayes + + # Calcul du solde + if context["parametres_portail"].get("facturation_afficher_solde_famille", False): + total_prestations = Prestation.objects.values('famille_id').filter(famille=famille).aggregate(total=Sum("montant")) + total_reglements = Reglement.objects.values('famille_id').filter(famille=famille).aggregate(total=Sum("montant")) + total_du = total_prestations["total"] if total_prestations["total"] else decimal.Decimal(0) + total_regle = total_reglements["total"] if total_reglements["total"] else decimal.Decimal(0) + context["solde"] = solde + solde= total_du - total_regle + + familles_data.append({ + 'famille': famille, + 'prelevement_actif': prelevement_actif, + 'texte_impayes': texte_impayes, + 'paiement_actif': paiement_actif, + 'liste_finale_cotisations': liste_finale_cotisations, + 'liste_finale_periodes': liste_finale_periodes, + 'liste_dates_extremes': liste_dates_extremes, + 'liste_factures': liste_factures, + 'liste_factures_impayees': liste_factures_impayees, + 'solde': solde, + }) + context['familles'] = familles_data + + + # if individu: + # # Chercher tous les rattachements pour l'individu + # rattachements = Rattachement.objects.filter(individu=individu) + # + # facturation_affichee = False # Flag pour vérifier si la section facturation a été activée + # + # if rattachements.exists(): + # # Boucle pour vérifier chaque famille liée à l'individu + # for rattachement in rattachements: + # famille = rattachement.famille # Récupérer la famille à partir du rattachement + # + # if famille and not facturation_affichee: + # # Vérifiez si l'individu est le contact_facturation de cette famille + # if famille.contact_facturation and famille.contact_facturation.idindividu == individu.idindividu: + # # Si l'individu est le contact_facturation de la famille, afficher le contenu de la section "Facturation" + # + # + # facturation_affichee = True # Marquer que la facturation a été affichée pour cette famille + # break # Sortir de la boucle une fois que la section a été affichée + # else: + # # Si l'individu n'est pas le contact_facturation de cette famille, ne pas afficher la section + # menu.Add(code="portail_facturation", titre=_("Facturation"), icone="euro", + # toujours_afficher=False) + # else: + # # Si l'individu n'est rattaché à aucune famille, ne pas afficher la section "Facturation" + # menu.Add(code="portail_facturation", titre=_("Facturation"), icone="euro", toujours_afficher=False) + # else: + # # Si l'individu n'existe pas, ne pas afficher la section "Facturation" + # menu.Add(code="portail_facturation", titre=_("Facturation"), icone="euro", toujours_afficher=False) return context diff --git a/noethysweb/portail/views/menu.py b/noethysweb/portail/views/menu.py index 303fa479..5363bd97 100644 --- a/noethysweb/portail/views/menu.py +++ b/noethysweb/portail/views/menu.py @@ -9,8 +9,8 @@ def GetMenuPrincipal(parametres_portail=None, user=None): - menu = Menu(titre=_("Menu principal"), user=user) + menu = Menu(titre=_("Menu principal"), user=user) menu.Add(code="portail_accueil", titre=_("Accueil"), icone="home", toujours_afficher=True) menu.Add(code="portail_renseignements", titre=_("Renseignements"), icone="folder-open-o", toujours_afficher=parametres_portail.get("renseignements_afficher_page", False)) menu.Add(code="portail_cotisations", titre=_("Adhésions"), icone="folder-o", toujours_afficher=parametres_portail.get("cotisations_afficher_page", False)) @@ -47,9 +47,18 @@ def GetParent(self): return self.parent def Add(self, code="", titre="", icone="", url=None, toujours_afficher=False, compatible_demo=True, args=None): - menu = Menu(self, code=code, titre=titre, icone=icone, url=url, args=args, user=self.user, compatible_demo=compatible_demo, toujours_afficher=toujours_afficher) - if not code or not self.user or toujours_afficher or code.endswith("_toc") or self.user.has_perm("core.%s" % code): + menu = Menu(self, code=code, titre=titre, icone=icone, url=url, args=args, user=self.user, + compatible_demo=compatible_demo, toujours_afficher=toujours_afficher) + + # Determine if the menu should be added + if toujours_afficher or (code and self.user and self.user.has_perm(f"core.{code}")): self.children.append(menu) + else: + # If the parent is not set to always display, log and skip adding + if self.parent and not self.parent.toujours_afficher: + print(f"Skipping menu {code}: parent 'toujours_afficher'={self.parent.toujours_afficher}") + return None + return menu def GetUrl(self): From 12c12d5d9bf2534c292a398741f297eb0187ecae Mon Sep 17 00:00:00 2001 From: "faouzia.takaya" Date: Thu, 13 Mar 2025 10:13:54 +0100 Subject: [PATCH 22/25] =?UTF-8?q?ADD=20Portail=20famille:=20Partie=20Messa?= =?UTF-8?q?gerie:=20configuration=20de=20discussions=20priv=C3=A9=20et=20g?= =?UTF-8?q?roup=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- noethysweb/core/models.py | 6 +- noethysweb/outils/urls.py | 8 +- noethysweb/outils/utils/utils_email.py | 50 +-- noethysweb/portail/forms/messagerie.py | 18 +- .../portail/templates/portail/messagerie.html | 314 ++++++++++++++-- noethysweb/portail/templatetags/famille.py | 22 ++ noethysweb/portail/urls.py | 6 +- noethysweb/portail/views/contact.py | 70 +++- noethysweb/portail/views/messagerie.py | 336 ++++++++++++++++-- 9 files changed, 716 insertions(+), 114 deletions(-) create mode 100644 noethysweb/portail/templatetags/famille.py diff --git a/noethysweb/core/models.py b/noethysweb/core/models.py index 056ada44..15dc9488 100644 --- a/noethysweb/core/models.py +++ b/noethysweb/core/models.py @@ -262,7 +262,7 @@ def get_storage(nom_champ=None): class AdresseMail(models.Model): idadresse = models.AutoField(verbose_name="ID", db_column='IDadresse', primary_key=True) - adresse = encrypt(models.EmailField(verbose_name="Adresse d'envoi", max_length=300, help_text="Saisissez l'adresse mail utilisée.")) + adresse = models.EmailField(verbose_name="Adresse d'envoi", max_length=300, help_text="Saisissez l'adresse mail utilisée.") motdepasse = encrypt(models.CharField(verbose_name="Mot de passe", max_length=300, blank=True, null=True, help_text="Saisissez le mot de passe de la messagerie.")) hote = encrypt(models.CharField(verbose_name="Hôte", max_length=300, blank=True, null=True, help_text="Saisissez le nom de l'hôte de la messagerie (Ex: smtp.orange.fr, smtp.gmail.com...).")) port = models.IntegerField(verbose_name="Port", blank=True, null=True, help_text="Saisissez le numéro de port (Ex: 995, 465...)") @@ -2036,6 +2036,10 @@ def Is_inscription_in_periode(self, date_min=None, date_max=None): else: return self.date_debut <= date_max + def Get_activite(self): + texte = "" + texte = self.activite + return texte class Information(models.Model): idinformation = models.AutoField(verbose_name="ID", db_column='IDinformation', primary_key=True) diff --git a/noethysweb/outils/urls.py b/noethysweb/outils/urls.py index d6c5aa8d..d8e6b580 100644 --- a/noethysweb/outils/urls.py +++ b/noethysweb/outils/urls.py @@ -8,7 +8,7 @@ from core.decorators import secure_ajax from outils.views import editeur_emails, editeur_emails_express, historique, update, sauvegarde_creer, statistiques, contacts, \ editeur_emails_familles, editeur_emails_individus, editeur_emails_collaborateurs, editeur_emails_contacts, editeur_emails_listes_diffusion, \ - editeur_emails_saisie_libre, emails, notes_versions, messages_portail, notes, calendrier_annuel, \ + editeur_emails_saisie_libre, emails, notes_versions, messages_portail, messagerie_portail, notes, calendrier_annuel, \ demandes_portail, liste_conso_sans_presta, statistiques_portail, correcteur, editeur_sms, editeur_sms_familles, \ editeur_sms_individus, editeur_sms_collaborateurs, editeur_sms_saisie_libre, sms, utilisateurs_bloques, procedures, editeur_sms_express, taches, \ suivi_reservations, commandes, desk_creer, editeur_emails_inscriptions, editeur_emails_activites @@ -38,7 +38,6 @@ path('outils/editeur_emails/listes_diffusion/', editeur_emails_listes_diffusion.Liste.as_view(), name='editeur_emails_listes_diffusion'), path('outils/editeur_emails/saisie_libre/', editeur_emails_saisie_libre.Liste.as_view(), name='editeur_emails_saisie_libre'), path('outils/editeur_emails/exporter_excel', secure_ajax(editeur_emails.Exporter_excel), name='ajax_editeur_emails_exporter_excel'), - path('outils/editeur_emails/inscriptions', editeur_emails_inscriptions.Liste.as_view(), name='editeur_emails_inscriptions'), path('outils/editeur_emails/inscriptions_emails_pdf', secure_ajax(editeur_emails_inscriptions.Impression_pdf),name='ajax_inscriptions_emails_pdf'), path('outils/editeur_emails/activites', editeur_emails_activites.Liste.as_view(),name='editeur_emails_activites'), @@ -101,6 +100,10 @@ path('outils/portail/messages/liste', messages_portail.Liste.as_view(), name='messages_portail_liste'), path('outils/portail/messages/supprimer/', messages_portail.Supprimer.as_view(), name='messages_portail_supprimer'), + path('outils/portail/messagerie', messagerie_portail.Ajouter.as_view(), name='messagerie_portail'), + path('outils/portail/messagerie//', messagerie_portail.Ajouter.as_view(), name='messagerie_portail'), + path('outils/portail/messagerie///', messagerie_portail.Ajouter.as_view(),name='messagerie_portail'), + path('outils/portail/renseignements/liste', demandes_portail.Liste.as_view(), name='demandes_portail_liste'), path('individus/suivi_reservations', suivi_reservations.View.as_view(), name='suivi_reservations'), @@ -127,6 +130,7 @@ path('outils/sauvegarder_db/', secure_ajax(sauvegarde_creer.Sauvegarder_db), name='ajax_sauvegarder_db'), path('outils/sauvegarder_media/', secure_ajax(sauvegarde_creer.Sauvegarder_media), name='ajax_sauvegarder_media'), path('outils/sauvegarder_liste/', secure_ajax(sauvegarde_creer.Get_liste_sauvegardes), name='ajax_get_liste_sauvegardes'), + path('outils/messages/marquer_lu/', secure_ajax(messagerie_portail.Marquer_lu), name='ajax_message_marquer_lu'), path('outils/get_suivi_reservations', secure_ajax(suivi_reservations.Get_suivi_reservations), name='ajax_get_suivi_reservations'), path('outils/reservations_get_form_activites', secure_ajax(suivi_reservations.Get_form_activites), name='ajax_reservations_get_form_activites'), path('outils/reservations_get_form_periode', secure_ajax(suivi_reservations.Get_form_periode), name='ajax_reservations_get_form_periode'), diff --git a/noethysweb/outils/utils/utils_email.py b/noethysweb/outils/utils/utils_email.py index 72e5901e..348a53d5 100644 --- a/noethysweb/outils/utils/utils_email.py +++ b/noethysweb/outils/utils/utils_email.py @@ -122,28 +122,16 @@ def Envoyer_model_mail(idmail=None, request=None): if mail.selection.startswith("NON_ENVOYE_"): destinataires = destinataires[:int(mail.selection.replace("NON_ENVOYE_", ""))] - # --- Éviter l'envoi en double à la même adresse : on dé-duplique --- - unique_destinataires = [] - deja_vues = set() # stockera les adresses normalisées - for d in destinataires: - if not d.adresse: - continue - addr_norm = d.adresse.strip().lower() - if addr_norm not in deja_vues: - deja_vues.add(addr_norm) - unique_destinataires.append(d) - # Envoi de chaque mail index_lot = 1 liste_envois_succes = [] - # --- Boucle d'envoi --- - for destinataire in unique_destinataires: + for destinataire in destinataires: html = mail.html objet = mail.objet if objet == None: objet = "" - # Fusion des mots-clés spécifiques (destinataire.valeurs) + valeurs_defaut + # Remplacement des mots-clés try: valeurs = json.loads(destinataire.valeurs) except: @@ -171,15 +159,8 @@ def Envoyer_model_mail(idmail=None, request=None): html += "

Si vous ne souhaitez plus recevoir nos mails groupés, cliquez sur le lien suivant : Désinscription

" % url_desinscription headers = {"List-Unsubscribe-Post": "List-Unsubscribe=One-Click", "List-Unsubscribe": url_desinscription} - # Construction du message - message = EmailMultiAlternatives( - subject=objet, - body=utils_texte.Textify(html), - from_email=mail.adresse_exp.adresse, - to=[destinataire.adresse], - connection=connection, - headers=headers - ) + # Création du message + message = EmailMultiAlternatives(subject=objet, body=utils_texte.Textify(html), from_email=mail.adresse_exp.adresse, to=[destinataire.adresse], connection=connection, headers=headers) message.mixed_subtype = 'related' message.attach_alternative(html, "text/html") @@ -222,26 +203,19 @@ def Envoyer_model_mail(idmail=None, request=None): if resultat == 1: liste_envois_succes.append(destinataire) - # Historique - utils_historique.Ajouter( - titre="Envoi d'un email", - detail=objet, - utilisateur=request.user if request else None, - famille=destinataire.famille_id, - individu=destinataire.individu_id, - collaborateur=destinataire.collaborateur_id, - objet="Email", - idobjet=mail.pk, - classe="Mail" - ) - - # Pause si envoi par lot - if nbre_mails_lot and len(unique_destinataires) > 1: + + # Mémorise l'envoi dans l'historique + utils_historique.Ajouter(titre="Envoi d'un email", detail=objet, utilisateur=request.user if request else None, famille=destinataire.famille_id, + individu=destinataire.individu_id, collaborateur=destinataire.collaborateur_id, objet="Email", idobjet=mail.pk, classe="Mail") + + # Pause si envoi par lot activé + if nbre_mails_lot and len(destinataires) > 1: if index_lot >= int(nbre_mails_lot): time.sleep(int(duree_pause)) index_lot = 0 index_lot += 1 + connection.close() return liste_envois_succes diff --git a/noethysweb/portail/forms/messagerie.py b/noethysweb/portail/forms/messagerie.py index 7fa33583..9bade1bc 100644 --- a/noethysweb/portail/forms/messagerie.py +++ b/noethysweb/portail/forms/messagerie.py @@ -14,6 +14,8 @@ from portail.forms.fiche import FormulaireBase from portail.utils.utils_summernote import SummernoteTextFormField +from core.models import Famille + class Formulaire(FormulaireBase, ModelForm): texte = SummernoteTextFormField(label=_("Poster un message"), attrs={'summernote': {'width': '100%', 'height': '200px', 'toolbar': [ @@ -22,7 +24,7 @@ class Formulaire(FormulaireBase, ModelForm): ['para', ['ul', 'ol', 'paragraph']], ['insert', ['link', 'picture']], ['view', ['codeview', 'help']], - ]}}) + ]}}) class Meta: model = PortailMessage @@ -30,19 +32,31 @@ class Meta: def __init__(self, *args, **kwargs): idstructure = kwargs.pop("idstructure", None) + idindividu = kwargs.pop("idindividu", None) + super(Formulaire, self).__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_id = 'portail_messages_form' self.helper.form_method = 'post' + # Récupération de la famille + utilisateur = self.request.user + if hasattr(utilisateur, 'famille'): + famille = utilisateur.famille + elif hasattr(utilisateur, 'individu'): + famille = Famille.objects.filter(nom__icontains=utilisateur.individu).first() + else: + famille = None # Cas où l'utilisateur n'a ni famille ni individu + # Affichage self.helper.layout = Layout( - Hidden('famille', value=self.request.user.famille.pk), + Hidden('famille', value=famille.pk if famille else None), # Ajout d'une vérification pour éviter une erreur si famille est None Hidden('structure', value=idstructure), Field('texte'), Commandes(enregistrer_label="%s" % _("Envoyer"), annuler_url="{% url 'portail_contact' %}", ajouter=False, aide=False, css_class="pull-right"), ) + # def clean_texte(self): # # Retrait des emojis qui causent une erreur de charset MySQL # emoji_pattern = compile("[" diff --git a/noethysweb/portail/templates/portail/messagerie.html b/noethysweb/portail/templates/portail/messagerie.html index 02a2c4de..4e7d8ff3 100644 --- a/noethysweb/portail/templates/portail/messagerie.html +++ b/noethysweb/portail/templates/portail/messagerie.html @@ -1,9 +1,27 @@ + + + + + {% extends "portail/page.html" %} {% load i18n %} {% load crispy_forms_tags %} {% load static %} {% load embed %} - +{% load famille %} {% block styles %} {{ block.super }} {% endblock %} @@ -13,78 +31,304 @@ {# Texte d'introduction #} {% if parametres_portail.messagerie_intro %}
- {{ parametres_portail.messagerie_intro }} + + {{ parametres_portail.messagerie_intro }}
{% endif %} {# Lien de retour vers la page contact #} - + + + {# ------------------------------------------------------------- #} + {# 1) Boutons pour sélectionner la famille #} + {# ------------------------------------------------------------- #} +{% if familles %} + + {% embed 'core/box.html' with box_titre=True %} + {% block box_theme %}card-outline card-lightblue{% endblock %} + {% block box_titre %} + {% trans "Sélection de la famille" %} + {% endblock box_titre %} + + {% block box_contenu %} +

+ {% blocktrans %}Cliquez ci-dessous sur la famille à laquelle vous souhaitez participer{% endblocktrans %}. +

+ {# Boutons pour les familles #} + {% for fam in familles %} + {% dict_get_item unread_messages_by_family fam.pk as unread_count %} + + + {{ fam }} + {% if unread_count > 0 %} + + {{ unread_count }} message{{ messages_non_lus_individu|pluralize }} non lu{{ messages_non_lus_individu|pluralize }} + + {% endif %} + + {% endfor %} + {% endblock box_contenu %} + {% block box_conclusion %}{{ box_conclusion|safe }}{% endblock %} + {% endembed %} + + {# ------------------------------------------------------------- #} + {# 2) Boutons pour sélectionner la discussion #} + {# ------------------------------------------------------------- #} +
+ {% embed 'core/box.html' with box_titre=True %} + {% block box_theme %}card-outline card-lightblue{% endblock %} + {% block box_titre %} + {% trans "Sélection de la discussion" %} + {% endblock box_titre %} + + {% block box_contenu %} +

+ {% blocktrans %}Cliquez ci-dessous sur la discussion à laquelle vous souhaitez participer{% endblocktrans %}. +

+ {% for fam in familles %} + {% if fam == discussion_famille %} + {# Boutons pour la discussion en groupe de la famille #} + {% dict_get_item unread_messages_by_family fam.pk as unread_count %} + + + Discussion En famille + {% with unread_messages_family|get_item:fam.pk as unread_count %} + {% if unread_count > 0 %} + + {{ unread_count }} message{{ unread_count|pluralize:"s" }} non lu{{ unread_count|pluralize }} + + {% endif %} + {% endwith %} + + + {# Bouton "Moi uniquement" pour les messages privés #} + {% if request.user.individu %} + + + Discussion Privée + {% with unread_messages_private|get_item:fam.pk as unread_count %} + {% if unread_count > 0 %} + + {{ unread_count }} message{{ unread_count|pluralize }} non lu{{ unread_count|pluralize }} + + {% endif %} + {% endwith %} + + {% endif %} + {% endif %} + {% endfor %} + + {% endblock box_contenu %} + {% block box_conclusion %}{{ box_conclusion|safe }}{% endblock %} + {% endembed %} +
+{% endif %} + + {# ------------------------------------------------------------- #} + {# 3) Boîte de discussion : on n'affiche qu'UNE discussion, #} + {# celle choisie par l'URL (discussion familiale ou privée). #} + {# ------------------------------------------------------------- #} - {# Messagerie #} {% embed 'core/box.html' with box_titre=True box_conclusion=True %} {% block box_theme %}card-outline card-lightblue direct-chat direct-chat-primary{% endblock %} {% block box_titre %} - {% blocktrans %}Discussion avec{% endblocktrans %} '{{ structure }}' + {% if discussion_type == "famille" %} + {# Discussion familiale #} + {% trans "Discussion entre" %} + + {{ structure.nom }} + + et la {% trans "Famille" %} {{ discussion_famille }} + {% elif discussion_type == "individu" %} + {# Discussion privée #} + {% trans "Discussion entre" %} + + {{ structure.nom }} + + et {% trans "Moi uniquement" %} + {% else %} + {# Pas de discussion sélectionnée #} + {% trans "Aucune discussion sélectionnée" %} + {% endif %} + + {# Badge si messages non lus #} {% if liste_messages_non_lus %} - {{ liste_messages_non_lus|length }} nouveau{{ liste_messages_non_lus|length|pluralize:"x" }} message{{ liste_messages_non_lus|length|pluralize }} + + + {{ liste_messages_non_lus|length }} + {% trans "nouveau" %}{{ liste_messages_non_lus|length|pluralize:"x" }} + {% trans "message" %}{{ liste_messages_non_lus|length|pluralize }} + {% endif %} {% endblock %} - {% block box_contenu %} - {% if not liste_messages %} + {% block box_contenu %} + {% if not discussion_type %}
- {% blocktrans %}Aucun message récent.{% endblocktrans %} + {% trans "Veuillez sélectionner une discussion (d'une famille ou moi uniquement) ci-dessus." %} +
+ {% elif not liste_messages %} +
+ {% trans "Aucun message récent." %}
{% else %} + {# Affichage des messages #}
{% for message in liste_messages %} - - {% if message.utilisateur %} - - {# Si c'est un message de l'administrateur #} + {% if message.utilisateur and message.utilisateur.pk == request.user.pk %} + {# -- MESSAGE DE L’UTILISATEUR CONNECTÉ => gauche / gris -- #}
- {% firstof message.utilisateur.get_full_name message.utilisateur message.utilisateur.get_short_name %} - {{ message.date_creation|date:"l j F Y H:i" }} - {% if message in liste_messages_non_lus %}{% trans "Nouveau" %}{% endif %} + + {{ request.user.individu }} + + + {{ message.date_creation|date:"l j F Y H:i" }} +
- -
+ User +
{{ message.texte|safe }}
- {% else %} - - {# Si c'est un message de la famille #} -
+ {# -- MESSAGE D’UN AUTRE UTILISATEUR => droite / bleu -- #} +
- {{ message.famille }} - {{ message.date_creation|date:"l j F Y H:i" }} + + {% if message.utilisateur %} + {% firstof message.utilisateur.get_full_name message.utilisateur.individu message.utilisateur message.utilisateur.get_short_name %} + {% endif %} + + + {{ message.date_creation|date:"l j F Y H:i" }} + + {% if message in liste_messages_non_lus %}{% trans "Nouveau" %}{% endif %}
- -
+ Admin +
{{ message.texte|safe }}
- {% endif %} - {% endfor %}
{% endif %} - {% endblock box_contenu %} + {% block box_conclusion %} - {% crispy form %} + {# Le formulaire ne s'affiche que s'il y a une discussion en cours #} + {% if discussion_type %} + {% crispy form %} + {% endif %} {% endblock %} {% endembed %} + // Scroll tout en bas de la discussion + const divMessages = document.getElementById("div_messages"); + if (divMessages) { + divMessages.scrollTop = divMessages.scrollHeight; + } +document.addEventListener('DOMContentLoaded', function () { + const familleButtons = document.querySelectorAll('.button-class[data-idfamilles]'); + const discussionFamilleButtons = document.querySelectorAll('.button-class[data-idfamille]'); + const discussionIndividuButtons = document.querySelectorAll('.button-class[data-idindividu]'); + + // Récupération des sélections à partir de localStorage + const selectedFamillesId = localStorage.getItem('selectedFamillesId'); + const selectedFamilleDiscussionId = localStorage.getItem('selectedFamilleDiscussionId'); + const selectedDiscussionType = localStorage.getItem('selectedDiscussionType'); + + // Ajouter la classe "selected" au bouton correspondant à la famille + if (selectedFamillesId) { + const familleButton = document.querySelector(`.button-class[data-idfamilles="${selectedFamillesId}"]`); + if (familleButton) { + familleButton.classList.add('selected'); + } + } + // Ajouter la classe "selected" au bouton correspondant à la discussion en famille + if (selectedFamilleDiscussionId) { + const discussionFamilleButton = document.querySelector(`.button-class[data-idfamille="${selectedFamilleDiscussionId}"]`); + if (discussionFamilleButton) { + discussionFamilleButton.classList.add('selected'); + } + } + + // Ajouter la classe "selected" au bouton correspondant à la discussion privée + if (selectedDiscussionType) { + const discussionIndividuButton = document.querySelector(`.button-class[data-idindividu="${selectedDiscussionType}"]`); + if (discussionIndividuButton) { + discussionIndividuButton.classList.add('selected'); + } + } + + // Gestion des clics sur les boutons de famille + familleButtons.forEach(button => { + button.addEventListener('click', function () { + // Supprimer la classe "selected" des autres boutons famille + familleButtons.forEach(btn => btn.classList.remove('selected')); + + // Ajouter la classe "selected" au bouton cliqué + this.classList.add('selected'); + + // Sauvegarder la sélection dans localStorage + localStorage.setItem('selectedFamillesId', this.getAttribute('data-idfamilles')); + }); + }); + + // Gestion des clics sur les boutons de discussion en famille + discussionFamilleButtons.forEach(button => { + button.addEventListener('click', function () { + // Supprimer la classe "selected" des autres boutons de discussion + discussionFamilleButtons.forEach(btn => btn.classList.remove('selected')); + discussionIndividuButtons.forEach(btn => btn.classList.remove('selected')); // Désactiver discussion privée + + // Ajouter la classe "selected" au bouton cliqué + this.classList.add('selected'); + + // Sauvegarder la sélection dans localStorage + localStorage.setItem('selectedFamilleDiscussionId', this.getAttribute('data-idfamille')); + localStorage.removeItem('selectedDiscussionType'); // Retirer la sélection de discussion privée + }); + }); + + // Gestion des clics sur les boutons de discussion privée + discussionIndividuButtons.forEach(button => { + button.addEventListener('click', function () { + // Supprimer la classe "selected" des autres boutons de discussion + discussionFamilleButtons.forEach(btn => btn.classList.remove('selected')); // Désactiver discussion en famille + discussionIndividuButtons.forEach(btn => btn.classList.remove('selected')); + + // Ajouter la classe "selected" au bouton cliqué + this.classList.add('selected'); + + // Sauvegarder la sélection dans localStorage + localStorage.setItem('selectedDiscussionType', this.getAttribute('data-idindividu')); + localStorage.removeItem('selectedFamilleDiscussionId'); // Retirer la sélection de discussion en famille + }); + }); +}); + {% endblock contenu_page %} diff --git a/noethysweb/portail/templatetags/famille.py b/noethysweb/portail/templatetags/famille.py new file mode 100644 index 00000000..a32dbb94 --- /dev/null +++ b/noethysweb/portail/templatetags/famille.py @@ -0,0 +1,22 @@ +from django import template + +register = template.Library() + +@register.simple_tag +def dict_get_item(dictionary, key): + """Retrieve a value from a dictionary using a single key.""" + return dictionary.get(key, 0) + +@register.simple_tag +def dict_get_item_2d(dictionary, famille_id, structure_id): + """ + Retourne dictionary[(famille_id, structure_id)] s'il existe, sinon 0 ou None + """ + return dictionary.get((famille_id, structure_id), 0) + +@register.simple_tag +def dict_get_item_3d(dictionary, famille_id, structure_id, individu_id): + """ + Retourne dictionary[(famille_id, structure_id, individu_id)] s'il existe, sinon 0 ou None + """ + return dictionary.get((famille_id, structure_id, individu_id), 0) \ No newline at end of file diff --git a/noethysweb/portail/urls.py b/noethysweb/portail/urls.py index 87564123..af5358ec 100644 --- a/noethysweb/portail/urls.py +++ b/noethysweb/portail/urls.py @@ -135,7 +135,11 @@ # Contact path('contact', contact.View.as_view(), name='portail_contact'), - path('contact/messagerie/', messagerie.Ajouter.as_view(), name='portail_messagerie'), + path('messagerie//',messagerie.Ajouter.as_view(),name='portail_messagerie'), + # Discussion familiale + path('messagerie/famille///', messagerie.Ajouter.as_view(), name='portail_messagerie_famille'), + # Discussion individuelle + path('messagerie/individu////',messagerie.Ajouter.as_view(), name='portail_messagerie_individu'), # Mentions path('mentions', mentions.View.as_view(), name='portail_mentions'), diff --git a/noethysweb/portail/views/contact.py b/noethysweb/portail/views/contact.py index 4bf2d9c4..6d8d174d 100644 --- a/noethysweb/portail/views/contact.py +++ b/noethysweb/portail/views/contact.py @@ -4,6 +4,8 @@ # Distribué sous licence GNU GPL. import logging +from django.http import Http404 +from core.models import Rattachement logger = logging.getLogger(__name__) from portail.views.base import CustomView from django.views.generic import TemplateView @@ -15,21 +17,73 @@ class View(CustomView, TemplateView): menu_code = "portail_contact" template_name = "portail/contact.html" + def get_object(self): + """Récupérer l'objet famille ou individu selon l'utilisateur""" + if hasattr(self.request.user, 'famille'): + return self.request.user.famille + elif hasattr(self.request.user, 'individu'): + return self.request.user.individu + else: + raise Http404("Utilisateur non reconnu.") + + def get_famille_object(self): + """Récupérer les familles de l'individu si applicable""" + if hasattr(self.request.user, 'famille'): + return [self.request.user.famille] + elif hasattr(self.request.user, 'individu'): + rattachements = Rattachement.objects.filter(individu=self.request.user.individu) + familles = [rattachement.famille for rattachement in rattachements if rattachement.famille and rattachement.titulaire == 1] + return familles if familles else None + return None def get_context_data(self, **kwargs): context = super(View, self).get_context_data(**kwargs) context['page_titre'] = _("Contact") - # Importation de toutes les structures - liste_structures = Structure.objects.filter(messagerie_active=True).order_by("nom") + familles = self.get_famille_object() # liste de familles liées à l'utilisateur - # Structures avec messagerie - context['liste_structures_messagerie'] = [structure for structure in liste_structures if structure.messagerie_active] + if familles: + # 1) Structures liées par au moins une inscription d’un individu de l’une des familles + liste_structures = ( + Structure.objects + .filter( + messagerie_active=True, + activite__inscription__famille__in=familles + ) + .order_by("nom") + .distinct() + ) - # Structures avec coordonnées - context['liste_structures_coords'] = [structure for structure in liste_structures if structure.afficher_coords] + # 2) Séparer celles qui ont la messagerie active + context['liste_structures_messagerie'] = [ + s for s in liste_structures if s.messagerie_active + ] - # Importation du nombre de messages non lus (regroupement par structure) - context['dict_messages_non_lus'] = {valeur["structure"]: valeur["nbre"] for valeur in PortailMessage.objects.values("structure").filter(famille=self.request.user.famille, utilisateur__isnull=False, date_lecture__isnull=True).annotate(nbre=Count('pk'))} + # 3) Séparer celles qui affichent les coordonnées + context['liste_structures_coords'] = [ + s for s in liste_structures if s.afficher_coords + ] + print("utilisateur=", self.request.user) + # 4) Importation du nombre de messages non lus par structure + # pour toutes les familles (donc un filter(famille__in=familles)). + qs_non_lus = ( + PortailMessage.objects + .select_related("famille", "structure", "individu", "utilisateur") # Charge en une seule requête + .only("idmessage", "famille_id", "structure_id", "individu_id", "utilisateur_id", "texte", + "date_creation", "date_lecture") # Charge seulement les colonnes nécessaires + .values("structure") + .filter( + Q(famille__in=familles), + Q(utilisateur__isnull=False), + Q(date_lecture__isnull=True), + # Ajouter la condition pour l'individu : pour filtrer les messages associés à l'individu de l'utilisateur connecté et les messages où l'individu est NULL (aucun individu associé). + Q(individu=self.request.user.individu) | Q(individu__isnull=True) + ) + .exclude(utilisateur=self.request.user) + .annotate(nbre=Count('pk')) + ) + context['dict_messages_non_lus'] = { + val["structure"]: val["nbre"] for val in qs_non_lus + } return context diff --git a/noethysweb/portail/views/messagerie.py b/noethysweb/portail/views/messagerie.py index 860eb1af..514bc2fc 100644 --- a/noethysweb/portail/views/messagerie.py +++ b/noethysweb/portail/views/messagerie.py @@ -4,6 +4,11 @@ # Distribué sous licence GNU GPL. import logging, datetime, json +from core.models import Famille +from core.models import Rattachement +from core.models import Individu +from django.db.models import Count, QuerySet +from core.models import AdresseMail logger = logging.getLogger(__name__) from django.urls import reverse_lazy from django.template.defaultfilters import truncatechars, striptags @@ -14,24 +19,187 @@ from outils.utils import utils_email from portail.forms.messagerie import Formulaire from portail.views.base import CustomView - +from django.db.models import Q class Page(CustomView): model = PortailMessage menu_code = "portail_contact" + def get_famille_object(self): + """Récupérer les familles ayant des inscriptions pour la structure sélectionnée""" + idstructure = self.kwargs.get("idstructure", None) + if not idstructure: + return None + + # Vérifiez si l'utilisateur est une famille + if hasattr(self.request.user, 'famille'): + familles = [self.request.user.famille] + # Sinon, si c'est un individu, récupérez les familles auxquelles il est rattaché + elif hasattr(self.request.user, 'individu'): + rattachements = Rattachement.objects.filter(individu=self.request.user.individu) + familles = [ + rattachement.famille for rattachement in rattachements + if rattachement.famille and rattachement.titulaire == 1 + ] + else: + familles = [] + + # Filtrez les familles avec des inscriptions pour la structure donnée + familles_inscrites = Famille.objects.filter( + inscription__activite__structure_id=idstructure, + pk__in=[fam.pk for fam in familles] # Restreindre aux familles de l'utilisateur + ).distinct() + + return familles_inscrites if familles_inscrites.exists() else None + def get_context_data(self, **kwargs): - context = super(Page, self).get_context_data(**kwargs) - context['page_titre'] = _("Messagerie") - context['structure'] = Structure.objects.get(pk=self.get_idstructure()) - context['liste_messages'] = PortailMessage.objects.select_related("famille", "utilisateur").filter(famille=self.request.user.famille, structure_id=self.get_idstructure()).order_by("date_creation") + context = super().get_context_data(**kwargs) + + idstructure = self.kwargs.get("idstructure") + idfamille = self.kwargs.get("idfamille") + idindividu = self.kwargs.get("idindividu") + + # Récupérer la structure + structure = Structure.objects.get(pk=idstructure) + context["structure"] = structure - # Importation des messages non lus - liste_messages_non_lus = PortailMessage.objects.select_related("famille").filter(famille=self.request.user.famille, structure_id=self.get_idstructure(), utilisateur__isnull=False, date_lecture__isnull=True) - context['liste_messages_non_lus'] = list(liste_messages_non_lus) + # Récupérer les familles liées à l'utilisateur + familles = self.get_famille_object() + context["familles"] = familles + context["familles_count"] = familles.count() + + # Récupérer l'individu connecté + individu_connecte = getattr(self.request.user, 'individu', None) + context["individu_connecte"] = individu_connecte + + # Gérer le cas d'une seule famille + if familles.count() == 1: + single_family = familles.first() + context["discussion_famille"] = single_family + context["discussion_type"] = "famille" + + # Calculer les messages non lus pour chaque famille + unread_messages_by_family = ( + PortailMessage.objects + .select_related("famille","individu", "utilisateur") # Charge en une seule requête + .only("idmessage", "famille_id", "individu_id", "utilisateur_id", "texte", "date_creation", + "date_lecture") # Charge seulement les colonnes nécessaires + .filter( + Q(famille__in=familles), + Q(utilisateur__isnull=False), + Q(date_lecture__isnull=True), + # Ajouter la condition pour l'individu : + Q(individu=self.request.user.individu) | Q(individu__isnull=True) + ) + .exclude(utilisateur=self.request.user) # Exclure les messages envoyés par l'utilisateur connecté + .values("famille") + .annotate(unread_count=Count("famille")) + ) + context["unread_messages_by_family"] = { + item["famille"]: item["unread_count"] for item in unread_messages_by_family + } + + # Calculer les messages non lus en famille (individu NULL) + unread_messages_family = ( + PortailMessage.objects + .select_related("famille", "individu", "utilisateur") # Charge en une seule requête + .only("idmessage", "famille_id", "individu_id", "utilisateur_id", "texte", "date_creation", "date_lecture") # Charge seulement les colonnes nécessaires + .filter( + Q(famille__in=familles), + Q(utilisateur__isnull=False), + Q(date_lecture__isnull=True), + Q(individu__isnull=True) + ) + .exclude(utilisateur=self.request.user) + .values("famille") + .annotate(unread_count=Count("famille")) + ) + context["unread_messages_family"] = { + item["famille"]: item["unread_count"] for item in unread_messages_family + } - # Enregistre la date de lecture pour les messages non lus + # Calculer les messages non lus pour l'individu connecté + unread_messages_private = {} + if individu_connecte: + unread_messages_private = ( + PortailMessage.objects + .select_related("famille", "individu", "utilisateur") # Charge en une seule requête + .only("idmessage", "famille_id", "individu_id", "utilisateur_id", "texte", + "date_creation", "date_lecture") # Charge seulement les colonnes nécessaires + .filter( + Q(famille__in=familles), + Q(utilisateur__isnull=False), + Q(date_lecture__isnull=True), + Q(individu=individu_connecte) + ) + .exclude(utilisateur=self.request.user) + .values("famille") + .annotate(unread_count=Count("famille")) + ) + context["unread_messages_private"] = { + item["famille"]: item["unread_count"] for item in unread_messages_private + } + else: + context["unread_messages_private"] = {} + + # Gestion des discussions + if idindividu: + # Discussion individuelle + individu = Individu.objects.get(pk=idindividu) + famille = Famille.objects.get(pk=idfamille) + + context["discussion_type"] = "individu" + context["discussion_individu"] = individu + context["discussion_famille"] = famille + + # Messages de la discussion individuelle + liste_messages = ( + PortailMessage.objects + .select_related("famille", "structure", "individu", "utilisateur") # Charge en une seule requête + .only("idmessage", "famille_id", "structure_id", "individu_id", "utilisateur_id", "texte", + "date_creation", "date_lecture") # Charge seulement les colonnes nécessaires + .filter( + structure=structure, + famille=famille, + individu=individu + ).order_by("date_creation")) + context["liste_messages"] = liste_messages + + elif idfamille: + # Discussion familiale + famille = Famille.objects.get(pk=idfamille) + context["discussion_type"] = "famille" + context["discussion_famille"] = famille + + # Messages de la discussion familiale + liste_messages = ( + PortailMessage.objects + .select_related("famille", "structure", "individu", "utilisateur") # Charge en une seule requête + .only("idmessage", "famille_id", "structure_id", "individu_id", "utilisateur_id", "texte", + "date_creation", "date_lecture") # Charge seulement les colonnes nécessaires + .filter( + structure=structure, + famille=famille, + individu__isnull=True + ).order_by("date_creation")) + context["liste_messages"] = liste_messages + + # Ajouter les messages spécifiques à cette famille + context["unread_count_famille"] = context["unread_messages_by_family"].get(idfamille, 0) + + else: + # Aucune discussion sélectionnée + context["discussion_type"] = None + context["liste_messages"] = PortailMessage.objects.none() + # Calcul des messages non lus dans la discussion actuelle + liste_messages_non_lus = context["liste_messages"].filter(date_lecture__isnull=True).exclude( + utilisateur=self.request.user) + context["liste_messages_non_lus"] = list(liste_messages_non_lus) + + # Marquer comme lus les messages affichés liste_messages_non_lus.update(date_lecture=datetime.datetime.now()) + + logger.info(f"idstructure: {idstructure}, idfamille: {idfamille}, idindividu: {idindividu}") return context def get_form_kwargs(self, **kwargs): @@ -42,9 +210,6 @@ def get_form_kwargs(self, **kwargs): def get_idstructure(self): return self.kwargs.get("idstructure", 0) - def get_success_url(self): - return reverse_lazy("portail_messagerie", kwargs={'idstructure': self.get_idstructure()}) - class Ajouter(Page, crud.Ajouter): form_class = Formulaire @@ -52,40 +217,157 @@ class Ajouter(Page, crud.Ajouter): texte_confirmation = _("Le message a bien été envoyé") titre_historique = "Ajouter un message" + + def form_valid(self, form): + message = form.save(commit=False) + + idstructure = self.kwargs["idstructure"] + idfamille = self.kwargs.get("idfamille", None) + idindividu = self.kwargs.get("idindividu", None) + + message.structure_id = idstructure + message.utilisateur_id = self.request.user.pk + message.date_creation = datetime.datetime.now() + + if idindividu: + # discussion privée + message.individu_id = idindividu + message.famille_id = idfamille + elif idfamille: + # discussion familiale + message.famille_id = idfamille + message.individu_id = None + else: + # Enregistrer l'individu connecté si l'utilisateur est un individu + if hasattr(self.request.user, 'individu'): + message.individu_id = self.request.user.individu.pk + message.save() + + return super().form_valid(form) + + def get_success_url(self): + idstructure = self.kwargs["idstructure"] + idfamille = self.kwargs.get("idfamille", None) + idindividu = self.kwargs.get("idindividu", None) + + if idindividu: + return reverse_lazy("portail_messagerie_individu", kwargs={ + "idstructure": idstructure, + "idfamille": idfamille, + "idindividu": idindividu + }) + elif idfamille: + return reverse_lazy("portail_messagerie_famille", kwargs={ + "idstructure": idstructure, + "idfamille": idfamille + }) + return reverse_lazy("portail_contact") + def Get_detail_historique(self, instance): return "Destinataire=%s Texte=%s" % (instance.structure, truncatechars(striptags(instance.texte), 40)) def Apres_form_valid(self, form=None, instance=None): - """ Envoie une notification de nouveau message à l'administrateur par email """ + """Envoie une notification de nouveau message à l'administrateur par email""" try: - # Vérifie qu'une notification doit être envoyée + # Éviter les doublons + self._already_executed = getattr(self, "_already_executed", False) + if self._already_executed: + logger.warning("Apres_form_valid a déjà été exécuté. Ignoré.") + return + self._already_executed = True + + # Vérifiez si l'envoi de notification est activé parametres_portail = utils_portail.Get_dict_parametres() if not parametres_portail.get("messagerie_envoyer_notification_admin", False): + logger.info("Notification admin désactivée dans les paramètres.") return - # Importation de la structure concernée + # Récupérer la structure concernée structure = Structure.objects.get(pk=self.get_idstructure()) if not structure.adresse_exp: + logger.error("Erreur : Adresse email d'expédition (adresse_exp) manquante pour la structure.") + return + + # Récupérer l'individu connecté + individu_connecte = getattr(self.request.user, 'individu', None) + if not individu_connecte: + logger.error("Erreur : Aucun individu connecté trouvé.") + return + + # Vérifier si l'individu connecté a une adresse email + email_individu = getattr(individu_connecte, 'mail', None) + if not email_individu: + logger.error("Erreur : Aucun email trouvé pour l'individu connecté.") + return + + # Convertir ou associer l'email de l'individu à une instance d'AdresseMail + adresse_exp = AdresseMail.objects.filter(adresse=email_individu).first() + if not adresse_exp: + adresse_exp = AdresseMail.objects.create(adresse=email_individu) + logger.info(f"Nouvelle adresse email créée : {adresse_exp.adresse}") + + # Récupérer un individu si concerné + idindividu = self.kwargs.get("idindividu", None) + individu = Individu.objects.get(pk=idindividu) if idindividu else None + famille = self.get_famille_object() + # Par défaut, on considère une discussion liée à la famille + if isinstance(famille, QuerySet): + famille = famille.first() + if not famille: + logger.error("Erreur : Famille non trouvée pour l'utilisateur.") return - url_message = self.request.build_absolute_uri(reverse_lazy("messagerie_portail", kwargs={"idstructure": structure.pk, "idfamille": self.request.user.famille.pk})) - # Création du contenu du mail - contenu_message = """ + # Créer l'URL du message + if individu: + url_message = self.request.build_absolute_uri( + reverse_lazy("portail_messagerie_individu", kwargs={ + "idstructure": structure.pk, + "idfamille": famille.pk, + "idindividu": individu.pk + }) + ) + else: + + url_message = self.request.build_absolute_uri( + reverse_lazy("portail_messagerie_famille", kwargs={ + "idstructure": structure.pk, + "idfamille": famille.pk + }) + ) + + # Préparer le contenu de l'email + contenu_message = f"""

Bonjour,

-

Vous avez reçu un nouveau message de %s sur le portail.

-

Vous pouvez le consulter et y répondre en cliquant sur le lien suivant : Accéder au message.

+

Vous avez reçu un nouveau message de {individu_connecte.Get_nom()} qui appartient à la famille {famille} sur le portail.

+

Vous pouvez le consulter et y répondre en cliquant sur le lien suivant : Accéder au message.

L'administrateur du portail

- """ % (self.request.user.famille, url_message) + """ - # Création de l'email - mail = Mail.objects.create(categorie="saisie_libre", + # Créer un objet Mail + mail = Mail.objects.create( + categorie="saisie_libre", objet="Nouveau message sur le portail", html=contenu_message, - adresse_exp=structure.adresse_exp, + adresse_exp=adresse_exp, # Utilisation de l'instance AdresseMail utilisateur=self.request.user if self.request else None, ) - destinataire = Destinataire.objects.create(categorie="saisie_libre", adresse=structure.adresse_exp.adresse) + logger.debug(f"Mail créé : {mail}") + # logger.debug(f"Mail créé : {mail}, Type : {type(mail)}") + + + # Ajouter un destinataire au mail + destinataire = Destinataire.objects.create( + categorie="saisie_libre", + adresse=structure.adresse_exp.adresse, # L'email de l'administrateur + ) mail.destinataires.add(destinataire) - succes = utils_email.Envoyer_model_mail(idmail=mail.pk, request=self.request) + # Envoyer l'email + utils_email.Envoyer_model_mail(idmail=mail.pk, request=self.request) + logger.info(f"Notification email envoyée avec succès de {adresse_exp.adresse} à {structure.adresse_exp.adresse}.") + + except Structure.DoesNotExist: + logger.error("Erreur : La structure spécifiée n'existe pas.") + except Individu.DoesNotExist: + logger.error("Erreur : L'individu spécifié n'existe pas.") except Exception as err: - logger.error("Erreur dans l'envoi de la notification de message par email à l'amdinistrateur : %s" % err) + logger.error(f"Erreur dans l'envoi de la notification de message par email à l'administrateur : {err}") From e1f3964bd56fb21f608101febb89b463601d6f98 Mon Sep 17 00:00:00 2001 From: "faouzia.takaya" Date: Thu, 13 Mar 2025 10:26:47 +0100 Subject: [PATCH 23/25] Fix typos and HTML issues --- .../templates/fiche_famille/famille_composition.html | 2 +- noethysweb/parametrage/templates/parametrage/classes.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/noethysweb/fiche_famille/templates/fiche_famille/famille_composition.html b/noethysweb/fiche_famille/templates/fiche_famille/famille_composition.html index 0f6d17cb..5f9edfe2 100644 --- a/noethysweb/fiche_famille/templates/fiche_famille/famille_composition.html +++ b/noethysweb/fiche_famille/templates/fiche_famille/famille_composition.html @@ -107,7 +107,7 @@
{% for num_categorie, nom_categorie in categories_utilisees %}
-

{{ nom_categorie }}s

+

{{ nom_categorie }}s

{% endfor %}
diff --git a/noethysweb/parametrage/templates/parametrage/classes.html b/noethysweb/parametrage/templates/parametrage/classes.html index 0edcd759..0c982222 100644 --- a/noethysweb/parametrage/templates/parametrage/classes.html +++ b/noethysweb/parametrage/templates/parametrage/classes.html @@ -10,7 +10,7 @@ {% block box_introduction %}{{ box_introduction|safe }}{% endblock %} {% block box_contenu %} {% embed 'core/crud/liste.html' %} - {% endembed %}s + {% endembed %} {% endblock box_contenu %} {% block box_conclusion %}{{ box_conclusion|safe }}{% endblock %} {% endembed %} From 7b147d8de8cd501a4df4f4bad74af69ddfa3c9f7 Mon Sep 17 00:00:00 2001 From: "faouzia.takaya" Date: Thu, 13 Mar 2025 14:03:15 +0100 Subject: [PATCH 24/25] =?UTF-8?q?fix=20Supprime=20l=E2=80=99initialisation?= =?UTF-8?q?=20inutile=20de=20la=20variable=20texte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- noethysweb/core/models.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/noethysweb/core/models.py b/noethysweb/core/models.py index 15dc9488..56c95315 100644 --- a/noethysweb/core/models.py +++ b/noethysweb/core/models.py @@ -1970,7 +1970,6 @@ def Get_rue_resid(self): return self.rue_resid.replace("\n", "
") if self.rue_resid else None def Get_nom(self): - texte = "" texte = self.nom return texte @@ -2037,7 +2036,6 @@ def Is_inscription_in_periode(self, date_min=None, date_max=None): return self.date_debut <= date_max def Get_activite(self): - texte = "" texte = self.activite return texte From 769679a803469503a5c446fe37f1baf7aa02ac5e Mon Sep 17 00:00:00 2001 From: "faouzia.takaya" Date: Fri, 14 Mar 2025 13:12:38 +0100 Subject: [PATCH 25/25] fix : configuration des fichiers settings et models --- noethysweb/core/models.py | 35 +++++++++++++++----- noethysweb/noethysweb/settings.py | 10 ++---- noethysweb/noethysweb/settings_production.py | 8 ++--- noethysweb/outils/views/historique.py | 2 +- 4 files changed, 35 insertions(+), 20 deletions(-) diff --git a/noethysweb/core/models.py b/noethysweb/core/models.py index 56c95315..e33f605b 100644 --- a/noethysweb/core/models.py +++ b/noethysweb/core/models.py @@ -1538,7 +1538,7 @@ class ModelePrestation(models.Model): structure = models.ForeignKey(Structure, verbose_name="Structure", on_delete=models.PROTECT, blank=True, null=True) # type_quotient = models.ForeignKey(TypeQuotient, verbose_name="Type de QF", blank=True, null=True, on_delete=models.SET_NULL, help_text="Sélectionnez un type de quotient familial ou laissez le champ vide pour tenir compte de tous les types de quotients.") multiprestations = models.TextField(verbose_name="Multi-prestations", blank=True, null=True) - + class Meta: db_table = 'modeles_prestations' verbose_name = "modèle de prestation" @@ -1968,7 +1968,7 @@ def Maj_infos(self, maj_adresse=True, maj_mail=True, maj_mobile=True, maj_titula def Get_rue_resid(self): return self.rue_resid.replace("\n", "
") if self.rue_resid else None - + def Get_nom(self): texte = self.nom return texte @@ -3157,15 +3157,33 @@ def Is_active_today(self): """ Vérifie si la période est active ce jour """ return self.affichage == "TOUJOURS" or (datetime.datetime.now() >= self.affichage_date_debut and datetime.datetime.now() <= self.affichage_date_fin) - def Is_famille_authorized(self, famille=None): + def Is_famille_authorized(self, famille=None, individu=None): """ Vérifie si une famille est autorisée à accéder à cette période """ # Vérification du compte internet - if (self.types_categories == "AUCUNE" and famille.internet_categorie) or (self.types_categories == "SELECTION" and famille.internet_categorie not in self.categories.all()): + if (self.types_categories == "AUCUNE" and famille.internet_categorie) or ( + self.types_categories == "SELECTION" and famille.internet_categorie not in self.categories.all()): + return False + # Vérification de la ville de résidence + if self.types_villes != "TOUTES" and self.villes: + liste_villes = [ville.strip().upper() for ville in self.villes.split(",")] + if not famille.ville_resid or ( + self.types_villes == "SELECTION" and famille.ville_resid.upper() not in liste_villes) or ( + self.types_villes == "SELECTION_INVERSE" and famille.ville_resid.upper() in liste_villes): + return False + return True + + def Is_individu_authorized(self, individu=None): + """ Vérifie si un individu est autorisée à accéder à cette période """ + # Vérification du compte internet + if (self.types_categories == "AUCUNE" and individu.internet_categorie) or ( + self.types_categories == "SELECTION" and individu.internet_categorie not in self.categories.all()): return False # Vérification de la ville de résidence if self.types_villes != "TOUTES" and self.villes: liste_villes = [ville.strip().upper() for ville in self.villes.split(",")] - if not famille.ville_resid or (self.types_villes == "SELECTION" and famille.ville_resid.upper() not in liste_villes) or (self.types_villes == "SELECTION_INVERSE" and famille.ville_resid.upper() in liste_villes): + if not individu.ville_resid or ( + self.types_villes == "SELECTION" and individu.ville_resid.upper() not in liste_villes) or ( + self.types_villes == "SELECTION_INVERSE" and individu.ville_resid.upper() in liste_villes): return False return True @@ -3233,10 +3251,10 @@ class PortailMessage(models.Model): famille = models.ForeignKey(Famille, verbose_name="Famille", on_delete=models.CASCADE, db_index=True) individu = models.ForeignKey(Individu, verbose_name="Individu", on_delete=models.CASCADE, null=True, db_index=True) structure = models.ForeignKey(Structure, verbose_name="Structure", on_delete=models.CASCADE, db_index=True) - utilisateur = models.ForeignKey(Utilisateur, verbose_name="Utilisateur", blank=True, null=True,on_delete=models.PROTECT) + utilisateur = models.ForeignKey(Utilisateur, verbose_name="Utilisateur", blank=True, null=True,on_delete=models.PROTECT, db_index=True) texte = models.TextField(verbose_name="Texte") - date_creation = models.DateTimeField(verbose_name="Date de création", auto_now_add=True) - date_lecture = models.DateTimeField(verbose_name="Date de lecture", max_length=200, blank=True, null=True) + date_creation = models.DateTimeField(verbose_name="Date de création", auto_now_add=True, db_index=True) + date_lecture = models.DateTimeField(verbose_name="Date de lecture", max_length=200, blank=True, null=True, db_index=True) class Meta: db_table = 'portail_messages' @@ -3472,6 +3490,7 @@ def __str__(self): class Consentement(models.Model): idconsentement = models.AutoField(verbose_name="ID", db_column='IDconsentement', primary_key=True) famille = models.ForeignKey(Famille, verbose_name="Famille", on_delete=models.PROTECT, blank=True, null=True) + individu = models.ForeignKey(Individu, verbose_name="Individu", on_delete=models.PROTECT, blank=True, null=True) unite_consentement = models.ForeignKey(UniteConsentement, verbose_name="Unité de consentement", on_delete=models.PROTECT) horodatage = models.DateTimeField(verbose_name="Date", auto_now_add=True) diff --git a/noethysweb/noethysweb/settings.py b/noethysweb/noethysweb/settings.py index 6e525127..42e056bb 100644 --- a/noethysweb/noethysweb/settings.py +++ b/noethysweb/noethysweb/settings.py @@ -15,7 +15,7 @@ PORTAIL_ACTIF = True # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'gip_recia' +SECRET_KEY = 'cle_secrete_a_modifier_imperativement' SECRET_EXPORT_DESK = None # SECURITY WARNING: don't run with debug turned on in production! @@ -51,12 +51,12 @@ AUTH_USER_MODEL = 'core.Utilisateur' DUREE_VALIDITE_MDP = 60*60*168 CORRECTEUR_JOURS_RETROACTION = 30 -PURGE_HISTORIQUE_JOURS = None +PURGE_HISTORIQUE_JOURS = 365 # CONFIGURATION ACCUEIL CONFIG_ACCUEIL_DEFAUT = [ [[6, "citation", "anniversaires"], [6, "celebrations", "anniversaires_demain"]], - [[8, "notes", "taches", "suivi_consommations", "suivi_inscriptions"], [4, "graphe_individus", "messages", "astuce"]], + [[8, "notes", "taches", "suivi_consommations", "suivi_inscriptions", "suivi_reservations"], [4, "graphe_individus", "messages", "astuce"]], ] # Application definition @@ -197,10 +197,6 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'static') STATIC_URL = '/static/' -# Media files -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') -MEDIA_URL = '/media/' - # Stockage STORAGE_PROBLEME = "django.core.files.storage.FileSystemStorage" STORAGE_PIECE = "django.core.files.storage.FileSystemStorage" diff --git a/noethysweb/noethysweb/settings_production.py b/noethysweb/noethysweb/settings_production.py index aab8edde..2d151c27 100644 --- a/noethysweb/noethysweb/settings_production.py +++ b/noethysweb/noethysweb/settings_production.py @@ -77,11 +77,11 @@ ######################################################################################### # ID de la clé GPG pour le chiffrement de la sauvegarde -DBBACKUP_GPG_RECIPIENT = "D7FC7207637A035E76C58D26C77F465B04290FAA" +#DBBACKUP_GPG_RECIPIENT = "XXXXXXXXX" # Pour un stockage de la sauvegarde sur le disque dur (Renseigner ci-dessous un répertoire existant) -DBBACKUP_STORAGE = "django.core.files.storage.FileSystemStorage" -DBBACKUP_STORAGE_OPTIONS = {"location": "/home/faouzia-takaya/Documents/sauvegardes/"} +# DBBACKUP_STORAGE = "django.core.files.storage.FileSystemStorage" +# DBBACKUP_STORAGE_OPTIONS = {"location": ""} DBBACKUP_CONNECTORS = {"default": {"dump_suffix": "--hex-blob"}} DBBACKUP_CLEANUP_KEEP = 10 DBBACKUP_CLEANUP_KEEP_MEDIA = 10 @@ -139,7 +139,7 @@ # SECURITE : Les paramètres par défaut conviendront généralement. ######################################################################################### -CSRF_COOKIE_SECURE = False +CSRF_COOKIE_SECURE = True CSRF_COOKIE_SAMESITE = 'Strict' SESSION_COOKIE_SECURE = False SECURE_BROWSER_XSS_FILTER = False diff --git a/noethysweb/outils/views/historique.py b/noethysweb/outils/views/historique.py index af64c7c9..e569cd88 100644 --- a/noethysweb/outils/views/historique.py +++ b/noethysweb/outils/views/historique.py @@ -38,7 +38,7 @@ def get_queryset(self): ) self.afficher_dernier_mois = utils_parametres.Get(nom="afficher_dernier_mois", categorie="historique", utilisateur=self.request.user, valeur=True) if self.afficher_dernier_mois: - conditions &= Q(horodatage__date__gte=datetime.date.today() - datetime.timedelta(days=14)) + conditions &= Q(horodatage__date__gte=datetime.date.today() - datetime.timedelta(days=7)) return Historique.objects.select_related("famille", "individu", "collaborateur", "utilisateur").filter(conditions, self.Get_filtres("Q")) def get_context_data(self, **kwargs):