diff --git a/app/models/etudiants.py b/app/models/etudiants.py
index 9bb6cc56d22e5e6ed6cd51bc0590b46dc359c5b5..18387a5656c5e523f820f2c1d2b7c1626dfc05d3 100644
--- a/app/models/etudiants.py
+++ b/app/models/etudiants.py
@@ -237,6 +237,8 @@ class Identite(models.ScoDocModel):
         Les clés adresses et admission ne SONT PAS utilisées.
         (added to session but not flushed nor commited)
         """
+        check_etud_duplicate_code(args, "code_nip", dest_url=None)
+        check_etud_duplicate_code(args, "code_ine", dest_url=None)
         if not "dept_id" in args:
             if "dept" in args:
                 departement = Departement.query.filter_by(acronym=args["dept"]).first()
@@ -248,8 +250,19 @@ class Identite(models.ScoDocModel):
         if args.get("admission_id", None) is None:
             etud.admission = Admission()
         etud.adresses.append(Adresse(typeadresse="domicile"))
-        db.session.flush()
-
+        try:
+            db.session.flush()
+        except sqlalchemy.exc.IntegrityError as e:
+            db.session.rollback()
+            if "unique_dept_nip_except_null" in str(e):
+                raise ScoValueError(
+                    "Code NIP déjà utilisé pour un autre étudiant"
+                ) from e
+            if "unique_dept_ine_except_null" in str(e):
+                raise ScoValueError(
+                    "Code INE déjà utilisé pour un autre étudiant"
+                ) from e
+            raise
         event = ScolarEvent(etud=etud, event_type="CREATION")
         db.session.add(event)
         log(f"Identite.create {etud}")
@@ -796,9 +809,12 @@ class Identite(models.ScoDocModel):
         )
 
 
-def check_etud_duplicate_code(args, code_name, edit=True, etudid: int | None = None):
+def check_etud_duplicate_code(
+    args, code_name, edit=True, etudid: int | None = None, dest_url: str | None = ""
+):
     """Vérifie que le code n'est pas dupliqué.
     Raises ScoGenError si problème.
+    Si dest_url === None, pas de lien continuer/annuler.
     """
     etudid = etudid or args.get("etudid", None)
     if not args.get(code_name, None):
@@ -837,11 +853,17 @@ def check_etud_duplicate_code(args, code_name, edit=True, etudid: int | None = N
         <ul><li>
         { '</li><li>'.join(listh) }
         </li></ul>
+        """
+        err_page += (
+            f"""
         <p>
-        <a href="{ url_for(dest_endpoint, scodoc_dept=g.scodoc_dept, **parameters) }
+        <a href="{ dest_url or url_for(dest_endpoint, scodoc_dept=g.scodoc_dept, **parameters) }
         ">{submit_label}</a>
         </p>
         """
+            if dest_url is not None
+            else ""
+        )
 
         log(f"*** error: code {code_name} duplique: {args[code_name]}")
 
diff --git a/app/scodoc/sco_import_etuds.py b/app/scodoc/sco_import_etuds.py
index 5e129d69ddf656c9922efa17f07811dff934d204..ceedd9a9c359f6028d9746f34c6f9d7e01468440 100644
--- a/app/scodoc/sco_import_etuds.py
+++ b/app/scodoc/sco_import_etuds.py
@@ -127,7 +127,16 @@ def sco_import_format(with_codesemestre=True):
             if fieldname not in aliases:
                 aliases.insert(0, fieldname)  # prepend
             if with_codesemestre or fs[0] != "codesemestre":
-                r.append((fieldname, typ, table, allow_nulls, description, aliases))
+                r.append(
+                    (
+                        fieldname,
+                        typ,
+                        table,
+                        scu.to_bool(allow_nulls),
+                        description,
+                        aliases,
+                    )
+                )
     return r
 
 
@@ -249,18 +258,17 @@ def students_import_excel(
             if formsemestre_id
             else url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
         )
-        H = ["<ul>"]
-        for d in diag:
-            H.append(f"<li>{d}</li>")
-        H.append(
-            f"""
-            </ul>)
+        return render_template(
+            "sco_page.j2",
+            title="Import etudiants",
+            content=f"""
+            <h2>Import etudiants</h2>
             <p>Import terminé !</p>
+            <ul>
+                {''.join('<li>' + d + '</li>' for d in diag)}
+            </ul>
             <p><a class="stdlink" href="{dest_url}">Continuer</a></p>
-            """
-        )
-        return render_template(
-            "sco_page.j2", title="Import etudiants", content="\n".join(H)
+            """,
         )
     return ""
 
@@ -488,12 +496,12 @@ def scolars_import_excel_file(
         log("scolars_import_excel_file: re-raising exception")
         raise
 
-    diag.append("Import et inscription de %s étudiants" % len(created_etudids))
+    diag.append(f"Import et inscription de {len(created_etudids)} étudiants")
 
     ScolarNews.add(
         typ=ScolarNews.NEWS_INSCR,
-        text="Inscription de %d étudiants"  # peuvent avoir ete inscrits a des semestres differents
-        % len(created_etudids),
+        text=f"Inscription de {len(created_etudids)} étudiants",
+        # peuvent avoir ete inscrits a des semestres differents
         obj=formsemestre_id,
         max_frequency=0,
     )
@@ -502,8 +510,8 @@ def scolars_import_excel_file(
     cnx.commit()
 
     # Invalide les caches des semestres dans lesquels on a inscrit des etudiants:
-    for formsemestre_id in formsemestre_to_invalidate:
-        sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
+    for fid in formsemestre_to_invalidate:
+        sco_cache.invalidate_formsemestre(formsemestre_id=fid)
 
     return diag
 
diff --git a/app/templates/scolar/students_import_excel.j2 b/app/templates/scolar/students_import_excel.j2
new file mode 100644
index 0000000000000000000000000000000000000000..db5378b5417ccf2790f09fc33eb9f0d2bd1b0cde
--- /dev/null
+++ b/app/templates/scolar/students_import_excel.j2
@@ -0,0 +1,103 @@
+{% extends "sco_page.j2" %}
+
+
+{% block styles %}
+{{super()}}
+<style>
+table.import_format {
+    border-collapse: collapse;
+    width: 100%;
+}
+table.import_format td {
+    border: 1px solid #ddd;
+    padding: 8px;
+}
+</style>
+{% endblock %}
+
+{% block app_content %}
+
+<h2 class="formsemestre">Téléchargement d'une nouvelle liste d'etudiants</h2>
+
+<div class="scobox help explanation">
+    <p>A utiliser pour importer de <b>nouveaux</b> étudiants (typiquement au
+    <b>premier semestre</b>).
+    </p>
+    <p class="fontred">Si les étudiants à inscrire sont déjà dans un autre
+    semestre, utiliser le menu "<em>Inscriptions (passage des étudiants)
+    depuis d'autres semestres</em> à partir du semestre destination.
+    </p>
+    <p class="fontred">Si vous avez un portail Apogée, il est en général préférable d'importer les
+    étudiants depuis Apogée, via le menu "<em>Synchroniser avec étape Apogée</em>".
+    </p>
+    <p class="space-before-18">
+    L'opération se déroule en deux étapes. Dans un premier temps,
+    vous téléchargez une feuille Excel type. Vous devez remplir
+    cette feuille, une ligne décrivant chaque étudiant. Ensuite,
+    vous indiquez le nom de votre fichier dans la case "Fichier Excel"
+    ci-dessous, et cliquez sur "Télécharger" pour envoyer au serveur
+    votre liste.
+    </p>
+
+{% if formsemestre %}
+    <p style="color: red">Les étudiants importés seront inscrits dans
+        le semestre <b>{{formsemestre.html_link_status()|safe}}</b>
+    </p>
+{% else %}
+    <div class="warning">Cette fonction est réservé à certains cas particuliers.
+    Pour importer et inscrire de nouveaux étudiants dans un semestre de
+    formation, passer par le menu "<em>Inscriptions / Importer des étudiants</em>"
+    du semestre visé.
+    </div>
+{% endif %}
+</div>
+
+<div class="scobox">
+    <div class="scobox-title">Feuille excel à remplir</div>
+    <div class="vspaced">
+
+    {% if formsemestre %}
+        <a class="stdlink" href="{{
+        url_for('scolar.import_generate_excel_sample', scodoc_dept=g.scodoc_dept, with_codesemestre=0)
+        }}">
+    {% else %}
+        <a class="stdlink" href="{{
+        url_for('scolar.import_generate_excel_sample', scodoc_dept=g.scodoc_dept)
+        }}">
+    {% endif -%}
+    Obtenir la feuille excel vierge</a> (que vous importerez ci-dessous après l'avoir remplie)
+    </div>
+</div>
+
+<div class="scobox">
+    <div class="scobox-title">Importation des données</div>
+    {{ tf_form | safe }}
+</div>
+
+<div class="scobox explanation">
+    <p>Le fichier Excel décrivant les étudiants doit comporter les colonnes suivantes.</p>
+    <p>Les colonnes peuvent être placées dans n'importe quel ordre, mais
+    le <b>titre</b> exact (tel que ci-dessous) doit être sur la première ligne.
+    </p>
+    <p>
+    Les champs avec un astérisque (*) doivent être présents (vides non autorisés).
+    </p>
+
+    <table class="import_format">
+    <tr>
+        <td><b>Attribut</b></td>
+        <td><b>Type</b></td>
+        <td><b>Description</b></td>
+        <td><b>Requis</b></td>
+    </tr>
+    {% for t in import_format %}
+        <tr>
+            <td>{{t[0]}}</td>
+            <td>{{t[1]}}</td>
+            <td>{{t[4]}}</td>
+            <td>{{'*' if t[3] else ''}}</td>
+        </tr>
+    {% endfor %}
+</div>
+
+{% endblock %}
\ No newline at end of file
diff --git a/app/views/scolar.py b/app/views/scolar.py
index b5c8a126b6ccb7063fdf60acc4206c3c6a082039..688be452419dc0fbf6463eb95bad7d499399430c 100644
--- a/app/views/scolar.py
+++ b/app/views/scolar.py
@@ -2163,73 +2163,23 @@ def export_etudiants_courants():
 def form_students_import_excel(formsemestre_id=None):
     "formulaire import xls"
     formsemestre_id = int(formsemestre_id) if formsemestre_id else None
-    if formsemestre_id:
-        sem = sco_formsemestre.get_formsemestre(formsemestre_id)
-        dest_url = url_for(
+    dest_url = (
+        url_for(
             "notes.formsemestre_status",
             scodoc_dept=g.scodoc_dept,
             formsemestre_id=formsemestre_id,
         )
-    else:
-        sem = None
-        dest_url = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
-    if sem and not sem["etat"]:
-        raise ScoValueError("Modification impossible: semestre verrouille")
-    H = [
-        """<h2 class="formsemestre">Téléchargement d\'une nouvelle liste d\'etudiants</h2>
-            <div style="color: red">
-            <p>A utiliser pour importer de <b>nouveaux</b> étudiants (typiquement au
-            <b>premier semestre</b>).</p>
-            <p>Si les étudiants à inscrire sont déjà dans un autre
-            semestre, utiliser le menu "<em>Inscriptions (passage des étudiants)
-            depuis d'autres semestres</em> à partir du semestre destination.
-            </p>
-            <p>Si vous avez un portail Apogée, il est en général préférable d'importer les
-            étudiants depuis Apogée, via le menu "<em>Synchroniser avec étape Apogée</em>".
-            </p>
-            </div>
-            <p>
-            L'opération se déroule en deux étapes. Dans un premier temps,
-            vous téléchargez une feuille Excel type. Vous devez remplir
-            cette feuille, une ligne décrivant chaque étudiant. Ensuite,
-            vous indiquez le nom de votre fichier dans la case "Fichier Excel"
-            ci-dessous, et cliquez sur "Télécharger" pour envoyer au serveur
-            votre liste.
-            </p>
-            """,
-    ]  # '
-    if sem:
-        H.append(
-            """<p style="color: red">Les étudiants importés seront inscrits dans
-        le semestre <b>%s</b></p>"""
-            % sem["titremois"]
-        )
-    else:
-        H.append(
-            f"""
-            <p>Pour inscrire directement les étudiants dans un semestre de
-            formation, il suffit d'indiquer le code de ce semestre
-            (qui doit avoir été créé au préalable).
-            <a class="stdlink" href="{
-                url_for("scolar.index_html", showcodes=1, scodoc_dept=g.scodoc_dept)
-            }">Cliquez ici pour afficher les codes</a>
-            </p>
-            """
-        )
-
-    H.append("""<ol><li>""")
-    if formsemestre_id:
-        H.append(
-            """
-        <a class="stdlink" href="import_generate_excel_sample?with_codesemestre=0">
-        """
-        )
-    else:
-        H.append("""<a class="stdlink" href="import_generate_excel_sample">""")
-    H.append(
-        """Obtenir la feuille excel à remplir</a></li>
-    <li>"""
+        if formsemestre_id is not None
+        else url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
     )
+    formsemestre = (
+        FormSemestre.get_formsemestre(formsemestre_id)
+        if formsemestre_id is not None
+        else None
+    )
+
+    if formsemestre and not formsemestre.etat:
+        raise ScoValueError("Modification impossible: semestre verrouille")
 
     tf = TrivialFormulator(
         request.base_url,
@@ -2260,46 +2210,27 @@ def form_students_import_excel(formsemestre_id=None):
         initvalues={"check_homonyms": True, "require_ine": False},
         submitlabel="Télécharger",
     )
-    S = [
-        """<hr/><p>Le fichier Excel décrivant les étudiants doit comporter les colonnes suivantes.
-<p>Les colonnes peuvent être placées dans n'importe quel ordre, mais
-le <b>titre</b> exact (tel que ci-dessous) doit être sur la première ligne.
-</p>
-<p>
-Les champs avec un astérisque (*) doivent être présents (nulls non autorisés).
-</p>
-
-
-<p>
-<table>
-<tr><td><b>Attribut</b></td><td><b>Type</b></td><td><b>Description</b></td></tr>"""
-    ]
-    for t in sco_import_etuds.sco_import_format(
-        with_codesemestre=(formsemestre_id is None)
-    ):
-        if int(t[3]):
-            ast = ""
-        else:
-            ast = "*"
-        S.append(
-            "<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>"
-            % (t[0], t[1], t[4], ast)
-        )
+
     if tf[0] == 0:
         return render_template(
-            "sco_page.j2",
+            "scolar/students_import_excel.j2",
             title="Import etudiants",
-            content="\n".join(H) + tf[1] + "</li></ol>" + "\n".join(S),
+            import_format=sco_import_etuds.sco_import_format(
+                with_codesemestre=(formsemestre_id is None)
+            ),
+            tf_form=tf[1],
+            formsemestre=formsemestre,
+            sco=ScoData(formsemestre=formsemestre),
         )
-    elif tf[0] == -1:
+    if tf[0] == -1:
         return flask.redirect(dest_url)
-    else:
-        return sco_import_etuds.students_import_excel(
-            tf[2]["csvfile"],
-            formsemestre_id=int(formsemestre_id) if formsemestre_id else None,
-            check_homonyms=tf[2]["check_homonyms"],
-            require_ine=tf[2]["require_ine"],
-        )
+
+    return sco_import_etuds.students_import_excel(
+        tf[2]["csvfile"],
+        formsemestre_id=int(formsemestre_id) if formsemestre_id else None,
+        check_homonyms=tf[2]["check_homonyms"],
+        require_ine=tf[2]["require_ine"],
+    )
 
 
 @bp.route("/import_generate_excel_sample")