mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-05 20:51:26 +00:00
feat(backend,frontend): régénération et visualisation inline de la fiche élève PDF
This commit is contained in:
@ -1,228 +1,319 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Fiche élève de {{ student.last_name }} {{ student.first_name }}</title>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>
|
||||
Fiche élève — {{ student.last_name }} {{ student.first_name }}
|
||||
</title>
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 2cm;
|
||||
}
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
font-size: 12pt;
|
||||
color: #222;
|
||||
background: #fff;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
border-bottom: 2px solid #4CAF50;
|
||||
padding-bottom: 12px;
|
||||
position: relative;
|
||||
}
|
||||
.title {
|
||||
font-size: 22pt;
|
||||
font-weight: bold;
|
||||
color: #4CAF50;
|
||||
margin: 0;
|
||||
}
|
||||
.photo {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
object-fit: cover;
|
||||
border: 1px solid #4CAF50;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 32px; /* Espacement augmenté entre les sections */
|
||||
}
|
||||
.section-title {
|
||||
font-size: 15pt;
|
||||
font-weight: bold;
|
||||
color: #4CAF50;
|
||||
margin-bottom: 18px; /* Espacement sous le titre de section */
|
||||
border-bottom: 1px solid #4CAF50;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #bbb;
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background: #f3f3f3;
|
||||
font-weight: bold;
|
||||
}
|
||||
tr:nth-child(even) {
|
||||
background: #fafafa;
|
||||
}
|
||||
.label-cell {
|
||||
font-weight: bold;
|
||||
width: 30%;
|
||||
background: #f3f3f3;
|
||||
}
|
||||
.value-cell {
|
||||
width: 70%;
|
||||
}
|
||||
.signature {
|
||||
margin-top: 30px;
|
||||
text-align: right;
|
||||
font-style: italic;
|
||||
color: #555;
|
||||
}
|
||||
.signature-text {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.subsection-title {
|
||||
font-size: 12pt;
|
||||
color: #333;
|
||||
margin: 8px 0 4px 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 1.5cm 2cm;
|
||||
}
|
||||
body {
|
||||
font-family: "Helvetica", "Arial", sans-serif;
|
||||
font-size: 10pt;
|
||||
color: #1e293b;
|
||||
background: #fff;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
.header-table {
|
||||
width: 100%;
|
||||
border: none;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.header-table td {
|
||||
border: none;
|
||||
padding: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.header-left {
|
||||
width: 80%;
|
||||
}
|
||||
.header-right {
|
||||
width: 20%;
|
||||
text-align: right;
|
||||
}
|
||||
.school-name {
|
||||
font-size: 10pt;
|
||||
color: #64748b;
|
||||
margin: 0 0 4px 0;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.title {
|
||||
font-size: 20pt;
|
||||
font-weight: bold;
|
||||
color: #064e3b;
|
||||
margin: 0 0 2px 0;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 11pt;
|
||||
color: #059669;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
.header-line {
|
||||
border: none;
|
||||
border-top: 3px solid #059669;
|
||||
margin: 12px 0 20px 0;
|
||||
}
|
||||
.photo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
object-fit: cover;
|
||||
border: 2px solid #059669;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ── Sections ── */
|
||||
.section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.section-header {
|
||||
background-color: #059669;
|
||||
color: #ffffff;
|
||||
font-size: 11pt;
|
||||
font-weight: bold;
|
||||
padding: 6px 12px;
|
||||
margin-bottom: 0;
|
||||
letter-spacing: 0.5px;
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
.subsection-title {
|
||||
font-size: 10pt;
|
||||
color: #064e3b;
|
||||
font-weight: bold;
|
||||
padding: 6px 0 2px 0;
|
||||
margin: 8px 0 4px 0;
|
||||
border-bottom: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
/* ── Tables ── */
|
||||
table.data {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
table.data td {
|
||||
padding: 5px 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
font-size: 10pt;
|
||||
vertical-align: top;
|
||||
}
|
||||
table.data .label {
|
||||
font-weight: bold;
|
||||
color: #064e3b;
|
||||
background-color: #f0fdf4;
|
||||
width: 25%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
table.data .value {
|
||||
color: #1e293b;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
/* ── Paiement ── */
|
||||
table.payment {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.payment td {
|
||||
padding: 5px 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
font-size: 10pt;
|
||||
}
|
||||
table.payment .label {
|
||||
font-weight: bold;
|
||||
color: #064e3b;
|
||||
background-color: #f0fdf4;
|
||||
width: 35%;
|
||||
}
|
||||
table.payment .value {
|
||||
width: 65%;
|
||||
}
|
||||
|
||||
/* ── Footer / Signature ── */
|
||||
.signature-block {
|
||||
margin-top: 24px;
|
||||
padding: 10px 12px;
|
||||
background-color: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.signature-block p {
|
||||
margin: 0;
|
||||
font-size: 10pt;
|
||||
color: #475569;
|
||||
}
|
||||
.signature-date {
|
||||
font-weight: bold;
|
||||
color: #064e3b;
|
||||
}
|
||||
.footer-line {
|
||||
border: none;
|
||||
border-top: 2px solid #059669;
|
||||
margin: 20px 0 8px 0;
|
||||
}
|
||||
.footer-text {
|
||||
text-align: center;
|
||||
font-size: 8pt;
|
||||
color: #94a3b8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
</head>
|
||||
<body>
|
||||
{% load myTemplateTag %}
|
||||
<div class="container">
|
||||
<!-- Header Section -->
|
||||
<div class="header">
|
||||
<h1 class="title">Fiche élève de {{ student.last_name }} {{ student.first_name }}</h1>
|
||||
{% if student.photo %}
|
||||
<img src="{{ student.get_photo_url }}" alt="Photo de l'élève" class="photo" />
|
||||
{% else %}
|
||||
<img src="/static/img/default-photo.jpg" alt="Photo par défaut" class="photo" />
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Élève -->
|
||||
<div class="section">
|
||||
<div class="section-title">ÉLÈVE</div>
|
||||
<table>
|
||||
<tr>
|
||||
<td class="label-cell">Nom</td>
|
||||
<td class="value-cell">{{ student.last_name }}</td>
|
||||
<td class="label-cell">Prénom</td>
|
||||
<td class="value-cell">{{ student.first_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Adresse</td>
|
||||
<td class="value-cell" colspan="3">{{ student.address }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Genre</td>
|
||||
<td class="value-cell">{{ student|getStudentGender }}</td>
|
||||
<td class="label-cell">Né(e) le</td>
|
||||
<td class="value-cell">{{ student.birth_date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">À</td>
|
||||
<td class="value-cell">{{ student.birth_place }} ({{ student.birth_postal_code }})</td>
|
||||
<td class="label-cell">Nationalité</td>
|
||||
<td class="value-cell">{{ student.nationality }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Niveau</td>
|
||||
<td class="value-cell">{{ student|getStudentLevel }}</td>
|
||||
<td class="label-cell"></td>
|
||||
<td class="value-cell"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!-- ═══════ HEADER ═══════ -->
|
||||
<table class="header-table">
|
||||
<tr>
|
||||
<td class="header-left">
|
||||
{% if establishment %}
|
||||
<p class="school-name">{{ establishment.name }}</p>
|
||||
{% endif %}
|
||||
<h1 class="title">Fiche Élèves</h1>
|
||||
<!-- prettier-ignore -->
|
||||
<p class="subtitle">{{ student.last_name }} {{ student.first_name }}{% if school_year %} — {{ school_year }}{% endif %}</p>
|
||||
</td>
|
||||
<td class="header-right">
|
||||
{% if student.photo %}
|
||||
<img src="{{ student.get_photo_url }}" alt="Photo" class="photo" />
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr class="header-line" />
|
||||
|
||||
<!-- Responsables -->
|
||||
<div class="section">
|
||||
<div class="section-title">RESPONSABLES</div>
|
||||
{% for guardian in student.getGuardians %}
|
||||
<div>
|
||||
<div class="subsection-title">Responsable {{ forloop.counter }}</div>
|
||||
<table>
|
||||
<tr>
|
||||
<td class="label-cell">Nom</td>
|
||||
<td class="value-cell">{{ guardian.last_name }}</td>
|
||||
<td class="label-cell">Prénom</td>
|
||||
<td class="value-cell">{{ guardian.first_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Adresse</td>
|
||||
<td class="value-cell" colspan="3">{{ guardian.address }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Email</td>
|
||||
<td class="value-cell" colspan="3">{{ guardian.email }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Né(e) le</td>
|
||||
<td class="value-cell">{{ guardian.birth_date }}</td>
|
||||
<td class="label-cell">Téléphone</td>
|
||||
<td class="value-cell">{{ guardian.phone|phone_format }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Profession</td>
|
||||
<td class="value-cell" colspan="3">{{ guardian.profession }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Fratrie -->
|
||||
<div class="section">
|
||||
<div class="section-title">FRATRIE</div>
|
||||
{% for sibling in student.getSiblings %}
|
||||
<div>
|
||||
<div class="subsection-title">Frère/Soeur {{ forloop.counter }}</div>
|
||||
<table>
|
||||
<tr>
|
||||
<td class="label-cell">Nom</td>
|
||||
<td class="value-cell">{{ sibling.last_name }}</td>
|
||||
<td class="label-cell">Prénom</td>
|
||||
<td class="value-cell">{{ sibling.first_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Né(e) le</td>
|
||||
<td class="value-cell" colspan="3">{{ sibling.birth_date }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Paiement -->
|
||||
<div class="section">
|
||||
<div class="section-title">MODALITÉS DE PAIEMENT</div>
|
||||
<table>
|
||||
<tr>
|
||||
<td class="label-cell">Frais d'inscription</td>
|
||||
<td class="value-cell">{{ student|getRegistrationPaymentMethod }} en {{ student|getRegistrationPaymentPlan }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Frais de scolarité</td>
|
||||
<td class="value-cell">{{ student|getTuitionPaymentMethod }} en {{ student|getTuitionPaymentPlan }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Signature -->
|
||||
<div class="signature">
|
||||
Fait le <span class="signature-text">{{ signatureDate }}</span> à <span class="signature-text">{{ signatureTime }}</span>
|
||||
</div>
|
||||
<!-- ═══════ ÉLÈVE ═══════ -->
|
||||
<div class="section">
|
||||
<div class="section-header">INFORMATIONS DE L'ÉLÈVE</div>
|
||||
<table class="data">
|
||||
<tr>
|
||||
<td class="label">Nom</td>
|
||||
<td class="value">{{ student.last_name }}</td>
|
||||
<td class="label">Prénom</td>
|
||||
<td class="value">{{ student.first_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Genre</td>
|
||||
<td class="value">{{ student|getStudentGender }}</td>
|
||||
<td class="label">Niveau</td>
|
||||
<td class="value">{{ student|getStudentLevel }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Date de naissance</td>
|
||||
<td class="value">{{ student.formatted_birth_date }}</td>
|
||||
<td class="label">Lieu de naissance</td>
|
||||
<!-- prettier-ignore -->
|
||||
<td class="value">{{ student.birth_place }}{% if student.birth_postal_code %} ({{ student.birth_postal_code }}){% endif %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Nationalité</td>
|
||||
<td class="value">{{ student.nationality }}</td>
|
||||
<td class="label">Médecin traitant</td>
|
||||
<td class="value">{{ student.attending_physician }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Adresse</td>
|
||||
<td class="value" colspan="3">{{ student.address }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<!-- ═══════ RESPONSABLES ═══════ -->
|
||||
<div class="section">
|
||||
<div class="section-header">RESPONSABLES LÉGAUX</div>
|
||||
{% for guardian in student.getGuardians %}
|
||||
<div class="subsection-title">Responsable {{ forloop.counter }}</div>
|
||||
<table class="data">
|
||||
<tr>
|
||||
<td class="label">Nom</td>
|
||||
<td class="value">{{ guardian.last_name }}</td>
|
||||
<td class="label">Prénom</td>
|
||||
<td class="value">{{ guardian.first_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Date de naissance</td>
|
||||
<td class="value">{{ guardian.birth_date }}</td>
|
||||
<td class="label">Téléphone</td>
|
||||
<td class="value">{{ guardian.phone|phone_format }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Email</td>
|
||||
<td class="value" colspan="3">{{ guardian.email }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Adresse</td>
|
||||
<td class="value" colspan="3">{{ guardian.address }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Profession</td>
|
||||
<td class="value" colspan="3">{{ guardian.profession }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% empty %}
|
||||
<p style="color: #94a3b8; font-style: italic; padding: 8px">
|
||||
Aucun responsable renseigné.
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- ═══════ FRATRIE ═══════ -->
|
||||
{% if student.getSiblings %}
|
||||
<div class="section">
|
||||
<div class="section-header">FRATRIE</div>
|
||||
{% for sibling in student.getSiblings %}
|
||||
<div class="subsection-title">Frère / Sœur {{ forloop.counter }}</div>
|
||||
<table class="data">
|
||||
<tr>
|
||||
<td class="label">Nom</td>
|
||||
<td class="value">{{ sibling.last_name }}</td>
|
||||
<td class="label">Prénom</td>
|
||||
<td class="value">{{ sibling.first_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Date de naissance</td>
|
||||
<td class="value" colspan="3">{{ sibling.birth_date }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- ═══════ PAIEMENT ═══════ -->
|
||||
<div class="section">
|
||||
<div class="section-header">MODALITÉS DE PAIEMENT</div>
|
||||
<table class="payment">
|
||||
<tr>
|
||||
<td class="label">Frais d'inscription</td>
|
||||
<!-- prettier-ignore -->
|
||||
<td class="value">{{ student|getRegistrationPaymentMethod }} — {{ student|getRegistrationPaymentPlan }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Frais de scolarité</td>
|
||||
<!-- prettier-ignore -->
|
||||
<td class="value">{{ student|getTuitionPaymentMethod }} — {{ student|getTuitionPaymentPlan }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- ═══════ SIGNATURE ═══════ -->
|
||||
<div class="signature-block">
|
||||
<p>
|
||||
Document généré le
|
||||
<span class="signature-date">{{ signatureDate }}</span> à
|
||||
<span class="signature-date">{{ signatureTime }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr class="footer-line" />
|
||||
<p class="footer-text">
|
||||
Ce document est généré automatiquement et fait office de fiche
|
||||
d'inscription.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -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<id>[0-9]+)/pdf$', generate_registration_pdf, name="generate_registration_pdf"),
|
||||
re_path(r'^registerForms/(?P<id>[0-9]+)/archive$', archive, name="archive"),
|
||||
re_path(r'^registerForms/(?P<id>[0-9]+)/resend$', resend, name="resend"),
|
||||
re_path(r'^registerForms/(?P<id>[0-9]+)/send$', send, name="send"),
|
||||
@ -52,7 +53,7 @@ urlpatterns = [
|
||||
re_path(r'^registrationFileGroups/(?P<id>[0-9]+)$', RegistrationFileGroupSimpleView.as_view(), name='registrationFileGroupDetail'),
|
||||
re_path(r'^registrationFileGroups/(?P<id>[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<id>[0-9]+)$', RegistrationSchoolFileMasterSimpleView.as_view(), name='registrationSchoolFileMasters'),
|
||||
re_path(r'^registrationSchoolFileMasters$', RegistrationSchoolFileMasterView.as_view(), name='registrationSchoolFileMasters'),
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user