From b52ef42bfdb7956b022023c916ca55f802323f08 Mon Sep 17 00:00:00 2001 From: Noethys Date: Sat, 1 Feb 2025 14:49:28 +0100 Subject: [PATCH 1/4] Fix petits bugs --- noethysweb/consommations/utils/utils_impression_conso.py | 2 +- noethysweb/core/utils/utils_infos_individus.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/noethysweb/consommations/utils/utils_impression_conso.py b/noethysweb/consommations/utils/utils_impression_conso.py index 1ea8d519..f0d961f5 100644 --- a/noethysweb/consommations/utils/utils_impression_conso.py +++ b/noethysweb/consommations/utils/utils_impression_conso.py @@ -1355,7 +1355,7 @@ def Exporter_excel(self, dict_donnees=None): def Get_questions_evenement(self, conso=None): liste_questions_str = [] - if conso["evenement"].categorie.questions and conso["extra"]: + if conso["evenement"].categorie and conso["evenement"].categorie.questions and conso["extra"]: reponses = json.loads(conso["extra"]) liste_idquestion = [int(idq) for idq in conso["evenement"].categorie.questions.split(";")] for question in self.liste_questions: diff --git a/noethysweb/core/utils/utils_infos_individus.py b/noethysweb/core/utils/utils_infos_individus.py index b1da961a..75303727 100644 --- a/noethysweb/core/utils/utils_infos_individus.py +++ b/noethysweb/core/utils/utils_infos_individus.py @@ -335,7 +335,7 @@ def GetDictIndividus(self): "INDIVIDU_RUE": individu.rue_resid, "INDIVIDU_CP": individu.cp_resid, "INDIVIDU_VILLE": individu.ville_resid, - "INDIVIDU_SECTEUR": individu.secteur, + "INDIVIDU_SECTEUR": individu.secteur.nom if individu.secteur else "", "INDIVIDU_SECTEUR_COLORE": ("%s" % (individu.secteur.couleur or "#000000", individu.secteur)) if individu.secteur else "", "INDIVIDU_CATEGORIE_TRAVAIL": individu.categorie_travail, "INDIVIDU_PROFESSION": individu.profession, From 4175cada97ae7f2eeb7b8926c77694a37a89ce23 Mon Sep 17 00:00:00 2001 From: Noethys Date: Sun, 2 Feb 2025 20:25:39 +0100 Subject: [PATCH 2/4] =?UTF-8?q?Ajout=20de=20la=20s=C3=A9lection=20des=20pr?= =?UTF-8?q?estations=20dans=20les=20devis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/migrations/0184_devis_prestations.py | 18 +++ noethysweb/core/models.py | 1 + .../templates/core/widgets/checktree.html | 55 ++++---- .../facturation/utils/utils_facturation.py | 5 +- .../fiche_famille/forms/famille_devis.py | 133 +++++++----------- .../fiche_famille/famille_devis.html | 83 +++++++++++ noethysweb/fiche_famille/urls.py | 1 + .../fiche_famille/views/famille_devis.py | 78 ++++++---- noethysweb/fiche_famille/widgets.py | 50 +++++++ 9 files changed, 285 insertions(+), 139 deletions(-) create mode 100644 noethysweb/core/migrations/0184_devis_prestations.py create mode 100644 noethysweb/fiche_famille/templates/fiche_famille/famille_devis.html diff --git a/noethysweb/core/migrations/0184_devis_prestations.py b/noethysweb/core/migrations/0184_devis_prestations.py new file mode 100644 index 00000000..52fbadf0 --- /dev/null +++ b/noethysweb/core/migrations/0184_devis_prestations.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.21 on 2025-02-02 15:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0183_portailperiode_mode_consultation_date'), + ] + + operations = [ + migrations.AddField( + model_name='devis', + name='prestations', + field=models.TextField(blank=True, null=True, verbose_name='Prestations'), + ), + ] diff --git a/noethysweb/core/models.py b/noethysweb/core/models.py index 00e00c30..bdc32e3f 100644 --- a/noethysweb/core/models.py +++ b/noethysweb/core/models.py @@ -2852,6 +2852,7 @@ class Devis(models.Model): total = models.DecimalField(verbose_name="Total", max_digits=10, decimal_places=2, default=0.0) regle = models.DecimalField(verbose_name="Réglé", max_digits=10, decimal_places=2, default=0.0) solde = models.DecimalField(verbose_name="Solde", max_digits=10, decimal_places=2, default=0.0) + prestations = models.TextField(verbose_name="Prestations", blank=True, null=True) class Meta: db_table = 'devis' diff --git a/noethysweb/core/templates/core/widgets/checktree.html b/noethysweb/core/templates/core/widgets/checktree.html index 9e26b8e9..f46d9cb5 100644 --- a/noethysweb/core/templates/core/widgets/checktree.html +++ b/noethysweb/core/templates/core/widgets/checktree.html @@ -26,39 +26,38 @@ {% endblock styles %} {% if not liste_branches1 and texte_si_vide %} - {{ texte_si_vide }} -{% endif %} - -
- - {% for dict_branche1 in liste_branches1 %} - - - - {% for dict_branche2 in dict_branches2|get_item:dict_branche1.pk %} +
{{ texte_si_vide }}
+{% else %} +
+
-
- - {{ dict_branche1.label }} -
-
+ {% for dict_branche1 in liste_branches1 %} - + {% for dict_branche2 in dict_branches2|get_item:dict_branche1.pk %} + + + + {% endfor %} {% endfor %} - {% endfor %} -
-
- {% if dict_branche2.checkable != False %} - - {% if dict_branche2.pk in selections or coche_tout %} - - {% endif %} - {% endif %} - {{ dict_branche2.label }} +
+
+ + {{ dict_branche1.label }}
+
+ {% if dict_branche2.checkable != False %} + + {% if dict_branche2.pk in selections or coche_tout %} + + {% endif %} + {% endif %} + {{ dict_branche2.label }} +
+
-
- + + +{% endif %} - -""" \ No newline at end of file +class Formulaire_prestations(FormulaireBase, forms.Form): + prestations = forms.CharField(label="Prestations", required=False, widget=Prestations_devis( + attrs={"texte_si_vide": "Aucune prestation", "hauteur_libre": True, "coche_tout": True}), + help_text="Cochez les types de prestations à inclure dans le devis.") + + def __init__(self, *args, **kwargs): + prestations = kwargs.pop("prestations", {}) + selections = kwargs.pop("selections", {}) + super(Formulaire_prestations, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_id = 'famille_devis_prestations_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' + + self.fields["prestations"].widget.attrs.update({"prestations": prestations, "selections": selections}) + if selections: + self.fields["prestations"].widget.attrs.update({"coche_tout": False}) + + # Affichage + self.helper.layout = Layout( + Field("prestations"), + ) diff --git a/noethysweb/fiche_famille/templates/fiche_famille/famille_devis.html b/noethysweb/fiche_famille/templates/fiche_famille/famille_devis.html new file mode 100644 index 00000000..0518f300 --- /dev/null +++ b/noethysweb/fiche_famille/templates/fiche_famille/famille_devis.html @@ -0,0 +1,83 @@ +{% extends "fiche_famille/famille.html" %} +{% load crispy_forms_tags %} +{% load static %} +{% load embed %} + + +{% block detail_famille %} +
+ {% 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 %} + {% include 'core/erreurs_form.html' %} + {% crispy form %} + {% endblock %} + {% endembed %} +
+ + {# Insertion des modals #} + {% include 'outils/modal_editeur_emails.html' %} + {% include 'core/modal_pdf.html' %} + {% load static %} + + + +{% endblock %} diff --git a/noethysweb/fiche_famille/urls.py b/noethysweb/fiche_famille/urls.py index efd75a83..b4915392 100644 --- a/noethysweb/fiche_famille/urls.py +++ b/noethysweb/fiche_famille/urls.py @@ -180,6 +180,7 @@ path('individus/attestation_impression_pdf', secure_ajax(famille_attestations.Impression_pdf), name='ajax_attestation_impression_pdf'), path('individus/facture_mandat_pdf', secure_ajax(famille_voir_mandat.Impression_pdf), name='ajax_mandat_impression_pdf'), path('individus/devis_impression_pdf', secure_ajax(famille_devis.Impression_pdf), name='ajax_devis_impression_pdf'), + path('individus/devis_get_donnees', secure_ajax(famille_devis.Get_donnees), name='ajax_devis_get_donnees'), path('individus/cotisation_impression_pdf', secure_ajax(famille_voir_cotisation.Impression_pdf), name='ajax_cotisation_impression_pdf'), path('individus/rappel_impression_pdf', secure_ajax(famille_voir_rappel.Impression_pdf), name='ajax_rappel_impression_pdf'), path('individus/regenerer_identifiant', secure_ajax(famille_portail.Regenerer_identifiant), name='ajax_regenerer_identifiant'), diff --git a/noethysweb/fiche_famille/views/famille_devis.py b/noethysweb/fiche_famille/views/famille_devis.py index d938d5d4..173a31e4 100644 --- a/noethysweb/fiche_famille/views/famille_devis.py +++ b/noethysweb/fiche_famille/views/famille_devis.py @@ -4,24 +4,44 @@ # Distribué sous licence GNU GPL. from django.urls import reverse_lazy, reverse +from django.contrib import messages +from django.db.models import Q +from django.http import JsonResponse, HttpResponseRedirect +from django.template import Template, RequestContext +from core.data import data_modeles_emails from core.views.mydatatableview import MyDatatable, columns, helpers from core.views import crud -from core.models import Famille, Devis +from core.models import Devis, Prestation +from core.utils import utils_dates, utils_texte, utils_preferences from fiche_famille.forms.famille_devis import Formulaire as Form_parametres +from fiche_famille.forms.famille_devis import Formulaire_prestations from fiche_famille.views.famille import Onglet -from django.http import JsonResponse, HttpResponseRedirect -from core.utils import utils_dates, utils_texte, utils_preferences from facturation.utils import utils_facturation, utils_impression_facture -from django.contrib import messages -from django.db.models import Q -from core.data import data_modeles_emails +def Get_donnees(request): + # Récupération des données + form_parametres = Form_parametres(request.POST, idfamille=int(request.POST.get("famille")), utilisateur=request.user, request=request) + if not form_parametres.is_valid(): + liste_erreurs = form_parametres.errors.as_data().keys() + return JsonResponse({"erreur": "Veuillez renseigner les champs manquants : %s." % ", ".join(liste_erreurs)}, status=401) + parametres = form_parametres.cleaned_data + prestations_defaut = request.POST.get("prestations_defaut", "") + + # Création et rendu du formulaire contenant uniquement le widget Prestations + date_debut, date_fin = utils_dates.ConvertDateRangePicker(parametres["periode"]) + conditions = Q(date__gte=date_debut, date__lte=date_fin, famille=parametres["famille"], categorie__in=parametres["categories"]) + prestations = Prestation.objects.select_related("individu", "activite").filter(conditions).order_by("individu__prenom", "activite__nom", "label") + html = "{% load crispy_forms_tags %} {{ form.prestations|as_crispy_field }}" + html_widget_prestations = Template(html).render(RequestContext(request, {"form": Formulaire_prestations(prestations=prestations, selections=prestations_defaut)})) + + return JsonResponse({"html_widget_prestations": html_widget_prestations}) + def Impression_pdf(request): # Récupération des données form_parametres = Form_parametres(request.POST, idfamille=int(request.POST.get("famille")), utilisateur=request.user, request=request) - if form_parametres.is_valid() == False: + if not form_parametres.is_valid(): liste_erreurs = form_parametres.errors.as_data().keys() return JsonResponse({"erreur": "Veuillez renseigner les champs manquants : %s." % ", ".join(liste_erreurs)}, status=401) parametres = form_parametres.cleaned_data @@ -30,18 +50,28 @@ def Impression_pdf(request): if not parametres["date_edition"]: return JsonResponse({"erreur": "Vous devez saisir une date d'édition"}, status=401) if not parametres["numero"]: return JsonResponse({"erreur": "Vous devez saisir un numéro de reçu"}, status=401) if not parametres["modele"]: return JsonResponse({"erreur": "Vous devez sélectionner un modèle de document"}, status=401) + if not parametres["prestations"]: return JsonResponse({"erreur": "Vous devez cocher au moins une prestation à inclure"}, status=401) - # Récupération de la période - date_debut = utils_dates.ConvertDateENGtoDate(parametres["periode"].split(";")[0]) - date_fin = utils_dates.ConvertDateENGtoDate(parametres["periode"].split(";")[1]) + IDfamille = parametres["famille"].pk + date_debut, date_fin = utils_dates.ConvertDateRangePicker(parametres["periode"]) + + # Recherche les individus, activités et noms de prestations à inclure + individus, activites = [], [] + liste_conditions = Q() + for prestation in Prestation.objects.filter(pk__in=[int(idprestation) for idprestation in parametres["prestations"].split(";")]): + # Individus à inclure + idindividu = prestation.individu_id if prestation.individu_id else 0 + if idindividu not in individus: individus.append(idindividu) + # Activités à inclure + idactivite = prestation.activite_id if prestation.activite_id else None + if idactivite not in activites: activites.append(idactivite) + # Labels de prestation à inclure + liste_conditions |= Q(individu=prestation.individu, activite=prestation.activite, label=prestation.label) # Recherche des données de facturation facturation = utils_facturation.Facturation() - IDfamille = parametres["famille"].pk - individus = [int(idindividu) for idindividu in parametres["individus"]] - activites = [int(idactivite) for idactivite in parametres["activites"]] - categories_prestations = parametres["categories"] - dict_devis = facturation.GetDonnees(liste_activites=activites, date_debut=date_debut, date_fin=date_fin, mode="devis", IDfamille=IDfamille, liste_IDindividus=individus, categories_prestations=categories_prestations) + dict_devis = facturation.GetDonnees(liste_activites=activites, date_debut=date_debut, date_fin=date_fin, mode="devis", IDfamille=IDfamille, + liste_IDindividus=individus, categories_prestations=parametres["categories"], liste_conditions=liste_conditions) # Si aucun devis trouvé if not dict_devis: @@ -50,7 +80,6 @@ def Impression_pdf(request): # Rajoute les données du formulaire dict_devis[IDfamille].update({ "{DATE_EDITION}": parametres["date_edition"], - "{SIGNATAIRE}": parametres["signataire"], "{NUM_DEVIS}": parametres["numero"], }) @@ -76,7 +105,6 @@ def Impression_pdf(request): return JsonResponse({"infos": infos, "nom_fichier": nom_fichier, "categorie": "devis", "label_fichier": "Devis", "champs": champs, "idfamille": IDfamille}) - class Page(Onglet): model = Devis url_liste = "famille_devis_liste" @@ -84,7 +112,7 @@ class Page(Onglet): url_modifier = "famille_devis_modifier" url_supprimer = "famille_devis_supprimer" description_liste = "Vous pouvez créer ici des devis." - description_saisie = "Saisissez toutes les informations concernant le devis et cliquez sur le bouton Enregistrer." + description_saisie = "Saisissez toutes les informations concernant le devis et cliquez sur Aperçu PDF ou Envoyer par email. Vous pourrez ensuite mémoriser le devis en cliquant sur le bouton Enregistrer." objet_singulier = "un devis" objet_pluriel = "des devis" @@ -127,7 +155,8 @@ def form_valid(self, form): activites=form.cleaned_data["infos"]["activites"], individus=form.cleaned_data["infos"]["individus"], date_debut=form.cleaned_data["periode"].split(";")[0], date_fin=form.cleaned_data["periode"].split(";")[1], total=form.cleaned_data["infos"]["total"], regle=form.cleaned_data["infos"]["regle"], - solde=form.cleaned_data["infos"]["solde"], famille=form.cleaned_data["famille"]) + solde=form.cleaned_data["infos"]["solde"], famille=form.cleaned_data["famille"], + prestations=form.cleaned_data["prestations"]) else: self.object.numero = form.cleaned_data["numero"] self.object.date_edition = form.cleaned_data["date_edition"] @@ -139,13 +168,12 @@ def form_valid(self, form): self.object.regle = form.cleaned_data["infos"]["regle"] self.object.solde = form.cleaned_data["infos"]["solde"] self.object.famille = form.cleaned_data["famille"] + self.object.prestations = form.cleaned_data["prestations"] self.object.save() return HttpResponseRedirect(self.get_success_url()) - - class Liste(Page, crud.Liste): model = Devis template_name = "fiche_famille/famille_pieces.html" @@ -195,15 +223,15 @@ def Formate_solde(self, instance, **kwargs): return "%0.2f %s" % (instance.solde, utils_preferences.Get_symbole_monnaie()) - class Ajouter(Page, crud.Ajouter): form_class = Form_parametres - template_name = "fiche_famille/famille_edit.html" + template_name = "fiche_famille/famille_devis.html" + class Modifier(Page, crud.Modifier): form_class = Form_parametres - template_name = "fiche_famille/famille_edit.html" + template_name = "fiche_famille/famille_devis.html" + class Supprimer(Page, crud.Supprimer): template_name = "fiche_famille/famille_delete.html" - diff --git a/noethysweb/fiche_famille/widgets.py b/noethysweb/fiche_famille/widgets.py index 0c9e83d4..c473c6b2 100644 --- a/noethysweb/fiche_famille/widgets.py +++ b/noethysweb/fiche_famille/widgets.py @@ -249,3 +249,53 @@ def get_context(self, name, value, attrs=None): 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 Prestations_devis(Widget): + template_name = 'core/widgets/checktree.html' + request = None + + def get_context(self, name, value, attrs=None): + context = dict(self.attrs.items()) + if attrs is not None: + context.update(attrs) + context['name'] = name + + # Sélections par défaut + if context.get("selections", ""): + context["selections"] = [int(valeur) for valeur in context["selections"].split(";")] + + # Définit la hauteur du ctrl + if "hauteur" not in context: + context['hauteur'] = "600px" + + # Récupération des prestations + liste_branches1 = [] + dict_branches2 = {} + liste_labels_prestations_temp = [] + for prestation in context.get("prestations", []): + # Branche 1 + label_individu = prestation.individu.Get_nom() if prestation.individu else "Prestations familiales" + idindividu = prestation.individu.pk if prestation.individu else 0 + item_branche1 = {"pk": idindividu, "label": label_individu} + if item_branche1 not in liste_branches1: + liste_branches1.append(item_branche1) + + # Branche 2 + dict_branches2.setdefault(idindividu, []) + label_prestation = ("%s : %s" % (prestation.activite.nom, prestation.label)) if prestation.activite else prestation.label + if (idindividu, label_prestation) not in liste_labels_prestations_temp: + dict_branches2[idindividu].append({"pk": prestation.pk, "label": label_prestation}) + liste_labels_prestations_temp.append((idindividu, label_prestation)) + + context["liste_branches1"] = liste_branches1 + context["dict_branches2"] = dict_branches2 + 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)) + + def value_from_datadict(self, data, files, name): + selections = data.getlist(name, []) + return ";".join(selections) From 9c4362f7624860443bf42fa54cc131c619ec59b2 Mon Sep 17 00:00:00 2001 From: Noethys Date: Sun, 2 Feb 2025 22:42:12 +0100 Subject: [PATCH 3/4] =?UTF-8?q?Ajout=20de=20la=20s=C3=A9lection=20des=20pr?= =?UTF-8?q?estations=20dans=20les=20attestations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0185_attestation_prestations.py | 18 +++ noethysweb/core/models.py | 1 + .../forms/famille_attestations.py | 114 +++++++----------- .../fiche_famille/famille_devis.html | 4 +- noethysweb/fiche_famille/urls.py | 1 + .../views/famille_attestations.py | 69 +++++++---- .../fiche_famille/views/famille_devis.py | 7 +- 7 files changed, 113 insertions(+), 101 deletions(-) create mode 100644 noethysweb/core/migrations/0185_attestation_prestations.py diff --git a/noethysweb/core/migrations/0185_attestation_prestations.py b/noethysweb/core/migrations/0185_attestation_prestations.py new file mode 100644 index 00000000..f0931210 --- /dev/null +++ b/noethysweb/core/migrations/0185_attestation_prestations.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.21 on 2025-02-02 20:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0184_devis_prestations'), + ] + + operations = [ + migrations.AddField( + model_name='attestation', + name='prestations', + field=models.TextField(blank=True, null=True, verbose_name='Prestations'), + ), + ] diff --git a/noethysweb/core/models.py b/noethysweb/core/models.py index bdc32e3f..3e3cefc3 100644 --- a/noethysweb/core/models.py +++ b/noethysweb/core/models.py @@ -2829,6 +2829,7 @@ class Attestation(models.Model): total = models.DecimalField(verbose_name="Total", max_digits=10, decimal_places=2, default=0.0) regle = models.DecimalField(verbose_name="Réglé", max_digits=10, decimal_places=2, default=0.0) solde = models.DecimalField(verbose_name="Solde", max_digits=10, decimal_places=2, default=0.0) + prestations = models.TextField(verbose_name="Prestations", blank=True, null=True) class Meta: db_table = 'attestations' diff --git a/noethysweb/fiche_famille/forms/famille_attestations.py b/noethysweb/fiche_famille/forms/famille_attestations.py index c6a93cd0..7f9af444 100644 --- a/noethysweb/fiche_famille/forms/famille_attestations.py +++ b/noethysweb/fiche_famille/forms/famille_attestations.py @@ -9,39 +9,36 @@ from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Hidden, HTML from crispy_forms.bootstrap import Field -from core.forms.select2 import Select2Widget, Select2MultipleWidget +from core.forms.select2 import Select2Widget from core.forms.base import FormulaireBase from core.utils.utils_commandes import Commandes -from core.models import Attestation, ModeleDocument, Inscription +from core.models import Attestation, ModeleDocument from core.widgets import DatePickerWidget, DateRangePickerWidget, FormIntegreWidget from core.utils import utils_dates from facturation.forms.attestations_options_impression import Formulaire as Form_options_impression +from fiche_famille.widgets import Prestations_devis class Formulaire(FormulaireBase, ModelForm): - periode = forms.CharField(label="Période", required=True, widget=DateRangePickerWidget()) - date_edition = forms.DateField(label="Date d'édition", required=True, widget=DatePickerWidget(attrs={'afficher_fleches': True}), help_text="Par défaut la date du jour. Modifiez uniquement si nécessaire.") - individus = forms.MultipleChoiceField(label="Individus", widget=Select2MultipleWidget(), choices=[], required=True) - activites = forms.MultipleChoiceField(label="Activités", widget=Select2MultipleWidget(), choices=[], required=True) - numero = forms.IntegerField(label="Numéro", required=True, help_text="Ce numéro est attribué par défaut. Modifiez uniquement si nécessaire.") - modele = forms.ModelChoiceField(label="Modèle de document", widget=Select2Widget(), queryset=ModeleDocument.objects.filter(categorie="attestation").order_by("nom"), required=True) + periode = forms.CharField(label="Période", required=True, widget=DateRangePickerWidget(), help_text="Sélectionnez la période à inclure dans l'attestation.") + date_edition = forms.DateField(label="Date d'édition", required=True, widget=DatePickerWidget(attrs={'afficher_fleches': True}), help_text="La date d'édition est par défaut la date du jour. Il est généralement inutile de la modifier.") + numero = forms.IntegerField(label="Numéro", required=True, help_text="Le numéro de l'attestation est généré automatiquement. Il est généralement inutile de le modifier.") + modele = forms.ModelChoiceField(label="Modèle de document", widget=Select2Widget(), queryset=ModeleDocument.objects.filter(categorie="attestation").order_by("nom"), required=True, help_text="Sélectionnez le modèle de document à utiliser. Il doit avoir au préalable été créé dans Menu Paramétrage > Modèles de documents > Attestation.") signataire = forms.CharField(label="Signataire", required=True, help_text="Saisissez le nom du signataire du document (par défaut l'utilisateur en cours).") options_impression = forms.CharField(label="Options d'impression", required=False, widget=FormIntegreWidget()) + prestations = forms.CharField(label="Prestations", required=False, widget=Prestations_devis( + attrs={"texte_si_vide": "Aucune prestation", "hauteur_libre": True, "coche_tout": False}), help_text="Cochez les types de prestations à inclure dans l'attestation.") class Meta: model = Attestation - fields = ["famille", "numero", "filtre_prestations", "exclusions_prestations"] - help_texts = { - "filtre_prestations": "Si vous souhaitez uniquement certaines prestations, tapez leur nom ou une partie de leur nom, séparées par des points-virgules (;). Exemple : journées avec repas;piscine;matin.", - "exclusions_prestations": "Si vous souhaitez exclure certaines prestations, tapez leur nom ou une partie de leur nom, séparées par des points-virgules (;). Exemple : journées avec repas;piscine;matin.", - } + fields = ["famille", "numero"] def __init__(self, *args, **kwargs): idfamille = kwargs.pop("idfamille") utilisateur = kwargs.pop("utilisateur", None) super(Formulaire, self).__init__(*args, **kwargs) self.helper = FormHelper() - self.helper.form_id = 'famille_attestations_form' + self.helper.form_id = 'famille_devis_form' self.helper.form_method = 'post' self.helper.form_class = 'form-horizontal' @@ -51,19 +48,6 @@ def __init__(self, *args, **kwargs): # Date d'édition self.fields["date_edition"].initial = datetime.date.today() - # Recherche les inscriptions de la famille - inscriptions = Inscription.objects.select_related("activite", "activite__structure", "individu").filter(famille_id=idfamille, activite__structure__in=utilisateur.structures.all()) - - # Individus - liste_individus = list({(inscription.individu_id, inscription.individu.prenom): None for inscription in inscriptions}.keys()) - self.fields["individus"].choices = liste_individus - self.fields["individus"].initial = [id for id, nom in liste_individus] - - # Activités - liste_activites = list({(inscription.activite_id, inscription.activite.nom): None for inscription in inscriptions}.keys()) - self.fields["activites"].choices = liste_activites - self.fields["activites"].initial = [id for id, nom in liste_activites] - # Numéro self.fields["numero"].initial = 1 if not self.instance.idattestation: @@ -84,9 +68,8 @@ def __init__(self, *args, **kwargs): if self.instance.idattestation: self.fields["numero"].initial = self.instance.numero self.fields["date_edition"].initial = self.instance.date_edition - self.fields["individus"].initial = [int(idindividu) for idindividu in self.instance.individus.split(";")] if self.instance.individus else [] - self.fields["activites"].initial = [int(idindividu) for idindividu in self.instance.activites.split(";")] if self.instance.activites else [] self.fields["periode"].initial = "%s - %s" % (utils_dates.ConvertDateToFR(self.instance.date_debut), utils_dates.ConvertDateToFR(self.instance.date_fin)) + self.fields["prestations"].initial = self.instance.prestations # Options : Ajoute le request au form self.fields['options_impression'].widget.attrs.update({"form": Form_options_impression(request=self.request)}) @@ -98,22 +81,19 @@ def __init__(self, *args, **kwargs): HTML(""" """) ], autres_commandes=[ - HTML("""Envoyer par email """), - HTML("""Aperçu PDF """), + HTML("""Envoyer par email """), + HTML("""Aperçu PDF """), ]), Hidden('famille', value=idfamille), Hidden('infos', value=""), + Hidden("prestations_defaut", value=self.fields["prestations"].initial or ""), Field("periode"), + Field("prestations"), Field("date_edition"), - Field("individus"), - Field("activites"), - Field("filtre_prestations"), - Field("exclusions_prestations"), Field("numero"), Field("modele"), Field("signataire"), Field("options_impression"), - HTML(EXTRA_HTML), ) def clean(self): @@ -131,40 +111,28 @@ def clean(self): return self.cleaned_data -EXTRA_HTML = """ - -{# Insertion des modals #} -{% include 'outils/modal_editeur_emails.html' %} -{% include 'core/modal_pdf.html' %} -{% load static %} - - - -""" +class Formulaire_prestations(FormulaireBase, forms.Form): + prestations = forms.CharField(label="Prestations", required=False, widget=Prestations_devis( + attrs={"texte_si_vide": "Aucune prestation", "hauteur_libre": True, "coche_tout": True}), + help_text="Cochez les types de prestations à inclure dans l'attestation.") + + def __init__(self, *args, **kwargs): + prestations = kwargs.pop("prestations", {}) + selections = kwargs.pop("selections", {}) + super(Formulaire_prestations, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_id = 'famille_devis_prestations_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' + + self.fields["prestations"].widget.attrs.update({"prestations": prestations, "selections": selections}) + if selections: + self.fields["prestations"].widget.attrs.update({"coche_tout": False}) + + # Affichage + self.helper.layout = Layout( + Field("prestations"), + ) diff --git a/noethysweb/fiche_famille/templates/fiche_famille/famille_devis.html b/noethysweb/fiche_famille/templates/fiche_famille/famille_devis.html index 0518f300..f8f281d4 100644 --- a/noethysweb/fiche_famille/templates/fiche_famille/famille_devis.html +++ b/noethysweb/fiche_famille/templates/fiche_famille/famille_devis.html @@ -47,7 +47,7 @@ function maj_widget_prestations() { $.ajax({ type: "POST", - url: "{% url 'ajax_devis_get_donnees' %}", + url: "{% url url_get_donnees %}", data: $("#famille_devis_form").serialize(), datatype: "json", success: function(data) { @@ -63,7 +63,7 @@ function impression_pdf(email=false, afficher=true) { $.ajax({ type: "POST", - url: "{% url 'ajax_devis_impression_pdf' %}", + url: "{% url url_impression_pdf %}", data: $("#famille_devis_form").serialize(), datatype: "json", success: function(data){ diff --git a/noethysweb/fiche_famille/urls.py b/noethysweb/fiche_famille/urls.py index b4915392..d5ba883e 100644 --- a/noethysweb/fiche_famille/urls.py +++ b/noethysweb/fiche_famille/urls.py @@ -178,6 +178,7 @@ path('individus/recu_impression_pdf', secure_ajax(reglement_recu.Impression_pdf), name='ajax_recu_impression_pdf'), path('individus/facture_impression_pdf', secure_ajax(famille_voir_facture.Impression_pdf), name='ajax_facture_impression_pdf'), path('individus/attestation_impression_pdf', secure_ajax(famille_attestations.Impression_pdf), name='ajax_attestation_impression_pdf'), + path('individus/attestation_get_donnees', secure_ajax(famille_attestations.Get_donnees), name='ajax_attestation_get_donnees'), path('individus/facture_mandat_pdf', secure_ajax(famille_voir_mandat.Impression_pdf), name='ajax_mandat_impression_pdf'), path('individus/devis_impression_pdf', secure_ajax(famille_devis.Impression_pdf), name='ajax_devis_impression_pdf'), path('individus/devis_get_donnees', secure_ajax(famille_devis.Get_donnees), name='ajax_devis_get_donnees'), diff --git a/noethysweb/fiche_famille/views/famille_attestations.py b/noethysweb/fiche_famille/views/famille_attestations.py index 608f270a..c09b2817 100644 --- a/noethysweb/fiche_famille/views/famille_attestations.py +++ b/noethysweb/fiche_famille/views/famille_attestations.py @@ -9,16 +9,37 @@ from django.contrib import messages from django.db.models import Q from django.http import JsonResponse, HttpResponseRedirect +from django.template import Template, RequestContext from core.views.mydatatableview import MyDatatable, columns, helpers from core.views import crud -from core.models import Attestation +from core.models import Attestation, Prestation from core.utils import utils_dates, utils_texte, utils_preferences from core.data import data_modeles_emails from fiche_famille.forms.famille_attestations import Formulaire as Form_parametres +from fiche_famille.forms.famille_attestations import Formulaire_prestations from fiche_famille.views.famille import Onglet from facturation.utils import utils_facturation, utils_impression_facture +def Get_donnees(request): + # Récupération des données + form_parametres = Form_parametres(request.POST, idfamille=int(request.POST.get("famille")), utilisateur=request.user, request=request) + if not form_parametres.is_valid(): + liste_erreurs = form_parametres.errors.as_data().keys() + return JsonResponse({"erreur": "Veuillez renseigner les champs manquants : %s." % ", ".join(liste_erreurs)}, status=401) + parametres = form_parametres.cleaned_data + prestations_defaut = request.POST.get("prestations_defaut", "") + + # Création et rendu du formulaire contenant uniquement le widget Prestations + date_debut, date_fin = utils_dates.ConvertDateRangePicker(parametres["periode"]) + conditions = Q(date__gte=date_debut, date__lte=date_fin, famille=parametres["famille"]) + prestations = Prestation.objects.select_related("individu", "activite").filter(conditions).order_by("individu__prenom", "activite__nom", "label") + html = "{% load crispy_forms_tags %} {{ form.prestations|as_crispy_field }}" + html_widget_prestations = Template(html).render(RequestContext(request, {"form": Formulaire_prestations(prestations=prestations, selections=prestations_defaut)})) + + return JsonResponse({"html_widget_prestations": html_widget_prestations}) + + def Impression_pdf(request): # Récupération des données form_parametres = Form_parametres(request.POST, idfamille=int(request.POST.get("famille")), utilisateur=request.user, request=request) @@ -31,18 +52,28 @@ def Impression_pdf(request): if not parametres["date_edition"]: return JsonResponse({"erreur": "Vous devez saisir une date d'édition"}, status=401) if not parametres["numero"]: return JsonResponse({"erreur": "Vous devez saisir un numéro de reçu"}, status=401) if not parametres["modele"]: return JsonResponse({"erreur": "Vous devez sélectionner un modèle de document"}, status=401) + if not parametres["prestations"]: return JsonResponse({"erreur": "Vous devez cocher au moins une prestation à inclure"}, status=401) - # Récupération de la période - date_debut = utils_dates.ConvertDateENGtoDate(parametres["periode"].split(";")[0]) - date_fin = utils_dates.ConvertDateENGtoDate(parametres["periode"].split(";")[1]) + IDfamille = parametres["famille"].pk + date_debut, date_fin = utils_dates.ConvertDateRangePicker(parametres["periode"]) + + # Recherche les individus, activités et noms de prestations à inclure + individus, activites = [], [] + liste_conditions = Q() + for prestation in Prestation.objects.filter(pk__in=[int(idprestation) for idprestation in parametres["prestations"].split(";")]): + # Individus à inclure + idindividu = prestation.individu_id if prestation.individu_id else 0 + if idindividu not in individus: individus.append(idindividu) + # Activités à inclure + idactivite = prestation.activite_id if prestation.activite_id else None + if idactivite not in activites: activites.append(idactivite) + # Labels de prestation à inclure + liste_conditions |= Q(individu=prestation.individu, activite=prestation.activite, label=prestation.label) # Recherche des données de facturation facturation = utils_facturation.Facturation() - IDfamille = parametres["famille"].pk - individus = [int(idindividu) for idindividu in parametres["individus"]] - activites = [int(idactivite) for idactivite in parametres["activites"]] dict_attestations = facturation.GetDonnees(liste_activites=activites, date_debut=date_debut, date_fin=date_fin, mode="attestation", IDfamille=IDfamille, - liste_IDindividus=individus, filtre_prestations=parametres["filtre_prestations"], exclusions_prestations=parametres["exclusions_prestations"]) + liste_IDindividus=individus, liste_conditions=liste_conditions) # Si aucune attestation trouvée if not dict_attestations: @@ -64,8 +95,6 @@ def Impression_pdf(request): "total": float(dict_attestations[IDfamille]["total"]), "regle": float(dict_attestations[IDfamille]["ventilation"]), "solde": float(dict_attestations[IDfamille]["solde"]), - "individus": ";".join([str(idindividu) for idindividu in dict_attestations[IDfamille]["individus"].keys()]), - "activites": ";".join([str(idactivite) for idactivite in dict_attestations[IDfamille]["liste_idactivite"]]) } # Création du PDF @@ -84,7 +113,7 @@ class Page(Onglet): url_modifier = "famille_attestations_modifier" url_supprimer = "famille_attestations_supprimer" description_liste = "Vous pouvez créer ici des attestations de présence." - description_saisie = "Saisissez toutes les informations concernant l'attestation et cliquez sur le bouton Enregistrer." + description_saisie = "Saisissez toutes les informations concernant l'attestation et cliquez sur Aperçu PDF ou Envoyer par email. Vous pourrez ensuite mémoriser l'attestation en cliquant sur le bouton Enregistrer." objet_singulier = "une attestation de présence" objet_pluriel = "des attestations de présence" @@ -96,6 +125,8 @@ def get_context_data(self, **kwargs): context['boutons_liste'] = [ {"label": "Ajouter", "classe": "btn btn-success", "href": reverse_lazy(self.url_ajouter, kwargs={'idfamille': self.kwargs.get('idfamille', None)}), "icone": "fa fa-plus"}, ] + context["url_get_donnees"] = "ajax_attestation_get_donnees" + context["url_impression_pdf"] = "ajax_attestation_impression_pdf" return context def get_form_kwargs(self, **kwargs): @@ -123,26 +154,22 @@ def form_valid(self, form): # Enregistre l'attestation if not self.object and not Attestation.objects.filter(numero=form.cleaned_data["numero"]).exists(): - Attestation.objects.create(numero=form.cleaned_data["numero"], date_edition=form.cleaned_data["date_edition"], exclusions_prestations=form.cleaned_data["exclusions_prestations"], - activites=form.cleaned_data["infos"]["activites"], filtre_prestations=form.cleaned_data["filtre_prestations"], - individus=form.cleaned_data["infos"]["individus"], famille=form.cleaned_data["famille"], + Attestation.objects.create(numero=form.cleaned_data["numero"], date_edition=form.cleaned_data["date_edition"], + famille=form.cleaned_data["famille"], date_debut=form.cleaned_data["periode"].split(";")[0], date_fin=form.cleaned_data["periode"].split(";")[1], total=form.cleaned_data["infos"]["total"], regle=form.cleaned_data["infos"]["regle"], - solde=form.cleaned_data["infos"]["solde"]) + solde=form.cleaned_data["infos"]["solde"], prestations=form.cleaned_data["prestations"]) if self.object: self.object.numero = form.cleaned_data["numero"] self.object.date_edition = form.cleaned_data["date_edition"] - self.object.activites = form.cleaned_data["infos"]["activites"] - self.object.filtre_prestations = form.cleaned_data["filtre_prestations"] - self.object.exclusions_prestations = form.cleaned_data["exclusions_prestations"] - self.object.individus = form.cleaned_data["infos"]["individus"] self.object.date_debut = form.cleaned_data["periode"].split(";")[0] self.object.date_fin = form.cleaned_data["periode"].split(";")[1] self.object.total = form.cleaned_data["infos"]["total"] self.object.regle = form.cleaned_data["infos"]["regle"] self.object.solde = form.cleaned_data["infos"]["solde"] self.object.famille = form.cleaned_data["famille"] + self.object.prestations = form.cleaned_data["prestations"] self.object.save() return HttpResponseRedirect(self.get_success_url()) @@ -198,12 +225,12 @@ def Get_actions_speciales(self, instance, *args, **kwargs): class Ajouter(Page, crud.Ajouter): form_class = Form_parametres - template_name = "fiche_famille/famille_edit.html" + template_name = "fiche_famille/famille_devis.html" class Modifier(Page, crud.Modifier): form_class = Form_parametres - template_name = "fiche_famille/famille_edit.html" + template_name = "fiche_famille/famille_devis.html" class Supprimer(Page, crud.Supprimer): diff --git a/noethysweb/fiche_famille/views/famille_devis.py b/noethysweb/fiche_famille/views/famille_devis.py index 173a31e4..babc4638 100644 --- a/noethysweb/fiche_famille/views/famille_devis.py +++ b/noethysweb/fiche_famille/views/famille_devis.py @@ -92,8 +92,6 @@ def Impression_pdf(request): "total": float(dict_devis[IDfamille]["total"]), "regle": float(dict_devis[IDfamille]["ventilation"]), "solde": float(dict_devis[IDfamille]["solde"]), - "individus": ";".join([str(idindividu) for idindividu in dict_devis[IDfamille]["individus"].keys()]), - "activites": ";".join([str(idactivite) for idactivite in dict_devis[IDfamille]["liste_idactivite"]]) } # Création du PDF @@ -124,6 +122,8 @@ def get_context_data(self, **kwargs): context['boutons_liste'] = [ {"label": "Ajouter", "classe": "btn btn-success", "href": reverse_lazy(self.url_ajouter, kwargs={'idfamille': self.kwargs.get('idfamille', None)}), "icone": "fa fa-plus"}, ] + context["url_get_donnees"] = "ajax_devis_get_donnees" + context["url_impression_pdf"] = "ajax_devis_impression_pdf" return context def get_form_kwargs(self, **kwargs): @@ -152,7 +152,6 @@ def form_valid(self, form): # Enregistre le devis if not self.object: Devis.objects.create(numero=form.cleaned_data["numero"], date_edition=form.cleaned_data["date_edition"], - activites=form.cleaned_data["infos"]["activites"], individus=form.cleaned_data["infos"]["individus"], date_debut=form.cleaned_data["periode"].split(";")[0], date_fin=form.cleaned_data["periode"].split(";")[1], total=form.cleaned_data["infos"]["total"], regle=form.cleaned_data["infos"]["regle"], solde=form.cleaned_data["infos"]["solde"], famille=form.cleaned_data["famille"], @@ -160,8 +159,6 @@ def form_valid(self, form): else: self.object.numero = form.cleaned_data["numero"] self.object.date_edition = form.cleaned_data["date_edition"] - self.object.activites = form.cleaned_data["infos"]["activites"] - self.object.individus = form.cleaned_data["infos"]["individus"] self.object.date_debut = form.cleaned_data["periode"].split(";")[0] self.object.date_fin = form.cleaned_data["periode"].split(";")[1] self.object.total = form.cleaned_data["infos"]["total"] From 5d5ffddf2b7e9a5c21fe09a1d3119c84d9501bc0 Mon Sep 17 00:00:00 2001 From: Noethys Date: Sun, 2 Feb 2025 23:10:53 +0100 Subject: [PATCH 4/4] Version 1.2.8.3 --- noethysweb/versions.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/noethysweb/versions.txt b/noethysweb/versions.txt index 624c9c99..fa3631a3 100644 --- a/noethysweb/versions.txt +++ b/noethysweb/versions.txt @@ -1,3 +1,12 @@ +Version 1.2.8.3 (02/02/2025) : + +- Ajout de l'activation automatique du mode consultation sur le portail +- Ajout de la sélection des prestations dans les devis et les attestations +- Ajout de colonnes supplémentaires dans la liste des demandes de réservations +- Correction de l'impression d'un reçu de règlement depuis le portail +- Correction des pièces sans idfamille dans la liste des pièces fournies +- Correction de divers petits bugs + Version 1.2.8.2 (27/01/2025) : - Ajout de l'affichage du HT et de la TVA dans la synthèse des prestations