diff --git a/Back-End/Subscriptions/templates/pdfs/fiche_eleve.html b/Back-End/Subscriptions/templates/pdfs/fiche_eleve.html index e311472..72a5017 100644 --- a/Back-End/Subscriptions/templates/pdfs/fiche_eleve.html +++ b/Back-End/Subscriptions/templates/pdfs/fiche_eleve.html @@ -1,228 +1,319 @@ - + - - - Fiche élève de {{ student.last_name }} {{ student.first_name }} + + + + Fiche élève — {{ student.last_name }} {{ student.first_name }} + - - + + {% load myTemplateTag %} -
- -
-

Fiche élève de {{ student.last_name }} {{ student.first_name }}

- {% if student.photo %} - Photo de l'élève - {% else %} - Photo par défaut - {% endif %} -
- -
-
ÉLÈVE
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Nom{{ student.last_name }}Prénom{{ student.first_name }}
Adresse{{ student.address }}
Genre{{ student|getStudentGender }}Né(e) le{{ student.birth_date }}
À{{ student.birth_place }} ({{ student.birth_postal_code }})Nationalité{{ student.nationality }}
Niveau{{ student|getStudentLevel }}
-
+ + + + + + +
+ {% if establishment %} +

{{ establishment.name }}

+ {% endif %} +

Fiche Élèves

+ +

{{ student.last_name }} {{ student.first_name }}{% if school_year %} — {{ school_year }}{% endif %}

+
+ {% if student.photo %} + Photo + {% endif %} +
+
- -
-
RESPONSABLES
- {% for guardian in student.getGuardians %} -
-
Responsable {{ forloop.counter }}
- - - - - - - - - - - - - - - - - - - - - - - - - -
Nom{{ guardian.last_name }}Prénom{{ guardian.first_name }}
Adresse{{ guardian.address }}
Email{{ guardian.email }}
Né(e) le{{ guardian.birth_date }}Téléphone{{ guardian.phone|phone_format }}
Profession{{ guardian.profession }}
-
- {% endfor %} -
- - -
-
FRATRIE
- {% for sibling in student.getSiblings %} -
-
Frère/Soeur {{ forloop.counter }}
- - - - - - - - - - - -
Nom{{ sibling.last_name }}Prénom{{ sibling.first_name }}
Né(e) le{{ sibling.birth_date }}
-
- {% endfor %} -
- - -
-
MODALITÉS DE PAIEMENT
- - - - - - - - - -
Frais d'inscription{{ student|getRegistrationPaymentMethod }} en {{ student|getRegistrationPaymentPlan }}
Frais de scolarité{{ student|getTuitionPaymentMethod }} en {{ student|getTuitionPaymentPlan }}
-
- - -
- Fait le {{ signatureDate }} à {{ signatureTime }} -
+ +
+
INFORMATIONS DE L'ÉLÈVE
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Nom{{ student.last_name }}Prénom{{ student.first_name }}
Genre{{ student|getStudentGender }}Niveau{{ student|getStudentLevel }}
Date de naissance{{ student.formatted_birth_date }}Lieu de naissance{{ student.birth_place }}{% if student.birth_postal_code %} ({{ student.birth_postal_code }}){% endif %}
Nationalité{{ student.nationality }}Médecin traitant{{ student.attending_physician }}
Adresse{{ student.address }}
- - \ No newline at end of file + + +
+
RESPONSABLES LÉGAUX
+ {% for guardian in student.getGuardians %} +
Responsable {{ forloop.counter }}
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Nom{{ guardian.last_name }}Prénom{{ guardian.first_name }}
Date de naissance{{ guardian.birth_date }}Téléphone{{ guardian.phone|phone_format }}
Email{{ guardian.email }}
Adresse{{ guardian.address }}
Profession{{ guardian.profession }}
+ {% empty %} +

+ Aucun responsable renseigné. +

+ {% endfor %} +
+ + + {% if student.getSiblings %} +
+
FRATRIE
+ {% for sibling in student.getSiblings %} +
Frère / Sœur {{ forloop.counter }}
+ + + + + + + + + + + +
Nom{{ sibling.last_name }}Prénom{{ sibling.first_name }}
Date de naissance{{ sibling.birth_date }}
+ {% endfor %} +
+ {% endif %} + + +
+
MODALITÉS DE PAIEMENT
+ + + + + + + + + + + +
Frais d'inscription{{ student|getRegistrationPaymentMethod }} — {{ student|getRegistrationPaymentPlan }}
Frais de scolarité{{ student|getTuitionPaymentMethod }} — {{ student|getTuitionPaymentPlan }}
+
+ + +
+

+ Document généré le + {{ signatureDate }} à + {{ signatureTime }} +

+
+ + + + + diff --git a/Back-End/Subscriptions/urls.py b/Back-End/Subscriptions/urls.py index 9a91d11..8638e88 100644 --- a/Back-End/Subscriptions/urls.py +++ b/Back-End/Subscriptions/urls.py @@ -3,16 +3,16 @@ from django.urls import path, re_path from . import views # RF -from .views import RegisterFormView, RegisterFormWithIdView, send, resend, archive +from .views import RegisterFormView, RegisterFormWithIdView, send, resend, archive, generate_registration_pdf # SubClasses from .views import StudentView, GuardianView, ChildrenListView, StudentListView, DissociateGuardianView # Files from .views import ( - RegistrationSchoolFileMasterView, - RegistrationSchoolFileMasterSimpleView, - RegistrationSchoolFileTemplateView, - RegistrationSchoolFileTemplateSimpleView, - RegistrationParentFileMasterSimpleView, + RegistrationSchoolFileMasterView, + RegistrationSchoolFileMasterSimpleView, + RegistrationSchoolFileTemplateView, + RegistrationSchoolFileTemplateSimpleView, + RegistrationParentFileMasterSimpleView, RegistrationParentFileMasterView, RegistrationParentFileTemplateSimpleView, RegistrationParentFileTemplateView, @@ -25,11 +25,12 @@ from .views import ( from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group from .views import ( - get_school_file_templates_by_rf, + get_school_file_templates_by_rf, get_parent_file_templates_by_rf ) urlpatterns = [ + re_path(r'^registerForms/(?P[0-9]+)/pdf$', generate_registration_pdf, name="generate_registration_pdf"), re_path(r'^registerForms/(?P[0-9]+)/archive$', archive, name="archive"), re_path(r'^registerForms/(?P[0-9]+)/resend$', resend, name="resend"), re_path(r'^registerForms/(?P[0-9]+)/send$', send, name="send"), @@ -52,7 +53,7 @@ urlpatterns = [ re_path(r'^registrationFileGroups/(?P[0-9]+)$', RegistrationFileGroupSimpleView.as_view(), name='registrationFileGroupDetail'), re_path(r'^registrationFileGroups/(?P[0-9]+)/templates$', get_registration_files_by_group, name="get_registration_files_by_group"), re_path(r'^registrationFileGroups$', RegistrationFileGroupView.as_view(), name='registrationFileGroups'), - + re_path(r'^registrationSchoolFileMasters/(?P[0-9]+)$', RegistrationSchoolFileMasterSimpleView.as_view(), name='registrationSchoolFileMasters'), re_path(r'^registrationSchoolFileMasters$', RegistrationSchoolFileMasterView.as_view(), name='registrationSchoolFileMasters'), diff --git a/Back-End/Subscriptions/util.py b/Back-End/Subscriptions/util.py index cb85046..75b5d92 100644 --- a/Back-End/Subscriptions/util.py +++ b/Back-End/Subscriptions/util.py @@ -36,7 +36,7 @@ def save_file_replacing_existing(file_field, filename, content, save=True): """ Sauvegarde un fichier en écrasant l'existant s'il porte le même nom. Évite les suffixes automatiques Django (ex: fichier_N5QdZpk.pdf). - + Args: file_field: Le FileField Django (ex: registerForm.registration_file) filename: Le nom du fichier à sauvegarder @@ -51,7 +51,7 @@ def save_file_replacing_existing(file_field, filename, content, save=True): logger.debug(f"[save_file] Ancien fichier supprimé: {file_field.path}") except Exception as e: logger.error(f"[save_file] Erreur suppression ancien fichier: {e}") - + # Sauvegarder le nouveau fichier file_field.save(filename, content, save=save) @@ -207,19 +207,21 @@ def create_templates_for_registration_form(register_form): logger.error(f"Erreur lors de la génération du PDF pour le template: {e}") file_name = None - from django.core.files.base import ContentFile - upload_rel_path = registration_school_file_upload_to( - type("Tmp", (), { - "registration_form": register_form, - "establishment": getattr(register_form, "establishment", None), - "student": getattr(register_form, "student", None) - })(), - file_name - ) - abs_path = os.path.join(settings.MEDIA_ROOT, upload_rel_path) master_file_path = m.file.path if m.file and hasattr(m.file, 'path') else None + def _build_upload_path(template_pk): + """Génère le chemin relatif et absolu pour un template avec un pk connu.""" + rel = registration_school_file_upload_to( + type("Tmp", (), { + "registration_form": register_form, + "pk": template_pk, + })(), + file_name, + ) + return rel, os.path.join(settings.MEDIA_ROOT, rel) + if tmpl: + upload_rel_path, abs_path = _build_upload_path(tmpl.pk) template_file_name = os.path.basename(tmpl.file.name) if tmpl.file and tmpl.file.name else None master_file_changed = template_file_name != file_name # --- GESTION FORM EXISTANT : suppression ancien template si nom ou contenu master changé --- @@ -254,7 +256,7 @@ def create_templates_for_registration_form(register_form): logger.info("util.create_templates_for_registration_form - Mise à jour school template %s from master %s for RF %s", tmpl.pk, m.pk, register_form.pk) continue - # Sinon, création du template comme avant + # Sinon, création du template — sauvegarder d'abord pour obtenir un pk tmpl = RegistrationSchoolFileTemplate( master=m, registration_form=register_form, @@ -262,8 +264,10 @@ def create_templates_for_registration_form(register_form): formTemplateData=m.formMasterData or [], slug=slug, ) + tmpl.save() # pk attribué ici if file_name: - # Copier le fichier du master si besoin (form existant) + upload_rel_path, abs_path = _build_upload_path(tmpl.pk) + # Copier le fichier du master si besoin if master_file_path and not os.path.exists(abs_path): try: import shutil @@ -273,7 +277,7 @@ def create_templates_for_registration_form(register_form): except Exception as e: logger.error(f"Erreur lors de la copie du fichier master pour le template: {e}") tmpl.file.name = upload_rel_path - tmpl.save() + tmpl.save() created.append(tmpl) logger.info("util.create_templates_for_registration_form - Created school template %s from master %s for RF %s", tmpl.pk, m.pk, register_form.pk) @@ -453,6 +457,8 @@ def rfToPDF(registerForm, filename): 'signatureDate': convertToStr(_now(), '%d-%m-%Y'), 'signatureTime': convertToStr(_now(), '%H:%M'), 'student': registerForm.student, + 'establishment': registerForm.establishment, + 'school_year': registerForm.school_year, } # Générer le PDF @@ -474,6 +480,24 @@ def rfToPDF(registerForm, filename): return registerForm.registration_file +def generateRegistrationPDF(registerForm): + """ + Génère le PDF d'un dossier d'inscription à la volée et retourne le contenu binaire. + Ne sauvegarde pas le fichier sur disque. + """ + data = { + 'pdf_title': f"Dossier d'inscription de {registerForm.student.first_name}", + 'signatureDate': convertToStr(_now(), '%d-%m-%Y'), + 'signatureTime': convertToStr(_now(), '%H:%M'), + 'student': registerForm.student, + 'establishment': registerForm.establishment, + 'school_year': registerForm.school_year, + } + pdf = renderers.render_to_pdf('pdfs/fiche_eleve.html', data) + if not pdf: + raise ValueError("Erreur lors de la génération du PDF.") + return pdf.content + def delete_registration_files(registerForm): """ Supprime le fichier et le dossier associés à un RegistrationForm. diff --git a/Back-End/Subscriptions/views/__init__.py b/Back-End/Subscriptions/views/__init__.py index 0aa6428..b6880bf 100644 --- a/Back-End/Subscriptions/views/__init__.py +++ b/Back-End/Subscriptions/views/__init__.py @@ -1,22 +1,23 @@ from .register_form_views import ( - RegisterFormView, - RegisterFormWithIdView, - send, - resend, - archive, - get_school_file_templates_by_rf, - get_parent_file_templates_by_rf + RegisterFormView, + RegisterFormWithIdView, + send, + resend, + archive, + get_school_file_templates_by_rf, + get_parent_file_templates_by_rf, + generate_registration_pdf ) from .registration_school_file_masters_views import ( - RegistrationSchoolFileMasterView, + RegistrationSchoolFileMasterView, RegistrationSchoolFileMasterSimpleView, ) from .registration_school_file_templates_views import ( - RegistrationSchoolFileTemplateView, + RegistrationSchoolFileTemplateView, RegistrationSchoolFileTemplateSimpleView ) from .registration_parent_file_masters_views import ( - RegistrationParentFileMasterView, + RegistrationParentFileMasterView, RegistrationParentFileMasterSimpleView ) from .registration_parent_file_templates_views import ( @@ -48,6 +49,7 @@ __all__ = [ 'get_registration_files_by_group', 'get_school_file_templates_by_rf', 'get_parent_file_templates_by_rf', + 'generate_registration_pdf', 'StudentView', 'StudentListView', 'ChildrenListView', diff --git a/Back-End/Subscriptions/views/register_form_views.py b/Back-End/Subscriptions/views/register_form_views.py index e86916d..7b9d1ad 100644 --- a/Back-End/Subscriptions/views/register_form_views.py +++ b/Back-End/Subscriptions/views/register_form_views.py @@ -1,4 +1,5 @@ from django.http.response import JsonResponse +from django.http import HttpResponse from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect from django.utils.decorators import method_decorator from rest_framework.views import APIView @@ -365,7 +366,7 @@ class RegisterFormWithIdView(APIView): student = registerForm.student student_name = f"{student.first_name} {student.last_name}" notes = registerForm.notes or "Aucun motif spécifié" - + guardians = student.guardians.all() for guardian in guardians: email = None @@ -373,13 +374,13 @@ class RegisterFormWithIdView(APIView): email = guardian.profile_role.profile.email if not email: email = getattr(guardian, "email", None) - + if email: logger.info(f"[RF_SENT] Envoi email de refus à {email} pour l'élève {student_name}") mailer.sendRefusDossier(email, registerForm.establishment.pk, student_name, notes) except Exception as e: logger.error(f"[RF_SENT] Erreur lors de l'envoi de l'email de refus: {e}") - + updateStateMachine(registerForm, 'EVENT_REFUSE') util.delete_registration_files(registerForm) elif _status == RegistrationForm.RegistrationFormStatus.RF_SEPA_SENT: @@ -411,6 +412,17 @@ class RegisterFormWithIdView(APIView): # Initialisation de la liste des fichiers à fusionner fileNames = [] + # Régénérer la fiche élève avec le nouveau template avant fusion + try: + base_dir = os.path.join(settings.MEDIA_ROOT, f"registration_files/dossier_rf_{registerForm.pk}") + os.makedirs(base_dir, exist_ok=True) + initial_pdf = f"{base_dir}/Inscription_{registerForm.student.last_name}_{registerForm.student.first_name}.pdf" + registerForm.registration_file = util.rfToPDF(registerForm, initial_pdf) + registerForm.save() + logger.debug(f"[RF_VALIDATED] Fiche élève régénérée avant fusion") + except Exception as e: + logger.error(f"[RF_VALIDATED] Erreur lors de la régénération de la fiche élève: {e}") + # Ajout du fichier registration_file en première position if registerForm.registration_file: fileNames.append(registerForm.registration_file.path) @@ -488,7 +500,7 @@ class RegisterFormWithIdView(APIView): class_name = None if student.associated_class: class_name = student.associated_class.atmosphere_name - + guardians = student.guardians.all() for guardian in guardians: email = None @@ -496,13 +508,13 @@ class RegisterFormWithIdView(APIView): email = guardian.profile_role.profile.email if not email: email = getattr(guardian, "email", None) - + if email: logger.info(f"[RF_VALIDATED] Envoi email de validation à {email} pour l'élève {student_name}") mailer.sendValidationDossier(email, registerForm.establishment.pk, student_name, class_name) except Exception as e: logger.error(f"[RF_VALIDATED] Erreur lors de l'envoi de l'email de validation: {e}") - + # Lancer l'envoi d'email dans un thread séparé pour ne pas bloquer la réponse email_thread = threading.Thread(target=send_validation_emails) email_thread.start() @@ -518,7 +530,7 @@ class RegisterFormWithIdView(APIView): student = registerForm.student student_name = f"{student.first_name} {student.last_name}" notes = data.get('notes', '') or "Aucun motif spécifié" - + guardians = student.guardians.all() for guardian in guardians: email = None @@ -526,17 +538,17 @@ class RegisterFormWithIdView(APIView): email = guardian.profile_role.profile.email if not email: email = getattr(guardian, "email", None) - + if email: logger.info(f"[RF_ARCHIVED] Envoi email de refus définitif à {email} pour l'élève {student_name}") mailer.sendRefusDefinitif(email, registerForm.establishment.pk, student_name, notes) except Exception as e: logger.error(f"[RF_ARCHIVED] Erreur lors de l'envoi de l'email de refus définitif: {e}") - + # Lancer l'envoi d'email dans un thread séparé pour ne pas bloquer la réponse email_thread = threading.Thread(target=send_refus_definitif_emails) email_thread.start() - + updateStateMachine(registerForm, 'EVENT_ARCHIVE') # Retourner les données mises à jour @@ -946,3 +958,26 @@ def get_parent_file_templates_by_rf(request, id): return JsonResponse(serializer.data, safe=False) except RegistrationParentFileTemplate.DoesNotExist: return JsonResponse({'error': 'Aucune pièce à fournir trouvée pour ce dossier d\'inscription'}, status=status.HTTP_404_NOT_FOUND) + +@swagger_auto_schema( + method='get', + responses={200: openapi.Response('PDF file', schema=openapi.Schema(type=openapi.TYPE_FILE))}, + operation_description="Génère et retourne le PDF de la fiche élève à la volée", + operation_summary="Télécharger la fiche élève (régénérée)" +) +@api_view(['GET']) +def generate_registration_pdf(request, id): + try: + registerForm = RegistrationForm.objects.select_related('student', 'establishment').get(student__id=id) + except RegistrationForm.DoesNotExist: + return JsonResponse({"error": "Dossier d'inscription introuvable"}, status=status.HTTP_404_NOT_FOUND) + + try: + pdf_content = util.generateRegistrationPDF(registerForm) + except ValueError as e: + return JsonResponse({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + filename = f"Inscription_{registerForm.student.last_name}_{registerForm.student.first_name}.pdf" + response = HttpResponse(pdf_content, content_type='application/pdf') + response['Content-Disposition'] = f'inline; filename="{filename}"' + return response diff --git a/Front-End/src/components/Inscription/FilesModal.js b/Front-End/src/components/Inscription/FilesModal.js index 191eb29..5453651 100644 --- a/Front-End/src/components/Inscription/FilesModal.js +++ b/Front-End/src/components/Inscription/FilesModal.js @@ -53,10 +53,10 @@ const FilesModal = ({ .then((parentFiles) => { // Construct the categorized files list const categorizedFiles = { - registrationFile: selectedRegisterForm.registration_file + registrationFile: selectedRegisterForm.student?.id ? { name: 'Fiche élève', - url: getSecureFileUrl(selectedRegisterForm.registration_file), + url: `/api/generate-pdf?studentId=${selectedRegisterForm.student.id}`, } : null, fusionFile: selectedRegisterForm.fusion_file diff --git a/Front-End/src/components/Inscription/ValidateSubscription.js b/Front-End/src/components/Inscription/ValidateSubscription.js index 19228fd..45d0c79 100644 --- a/Front-End/src/components/Inscription/ValidateSubscription.js +++ b/Front-End/src/components/Inscription/ValidateSubscription.js @@ -152,7 +152,11 @@ export default function ValidateSubscription({ }; const allTemplates = [ - { name: 'Fiche élève', file: student_file, type: 'main' }, + { + name: 'Fiche élève', + file: `/api/generate-pdf?studentId=${studentId}`, + type: 'main', + }, ...schoolFileTemplates.map((template) => ({ name: template.name || 'Document scolaire', file: template.file, @@ -213,7 +217,11 @@ export default function ValidateSubscription({ {allTemplates[currentTemplateIndex].name || 'Document sans nom'}