diff --git a/app/scodoc/sco_portal_apogee.py b/app/scodoc/sco_portal_apogee.py
index 4da1700b334dcc8a49d4ece16014a74ecd84871f..d0ef82c4ade492845ff5437671e43de919e1ce6a 100644
--- a/app/scodoc/sco_portal_apogee.py
+++ b/app/scodoc/sco_portal_apogee.py
@@ -202,8 +202,7 @@ def get_inscrits_etape(
                 return False
         else:
             log(
-                "get_inscrits_etape: pas inscription dans code_etape=%s e=%s"
-                % (code_etape, e)
+                "get_inscrits_etape: pas inscription dans code_etape={code_etape} e={e}"
             )
             return False  # ??? pas d'annee d'inscription dans la réponse
 
@@ -215,8 +214,7 @@ def get_inscrits_etape(
 
 def query_apogee_portal(**args):
     """Recupere les infos sur les etudiants nommés
-    args: nom, prenom, code_nip
-    (nom et prenom matchent des parties de noms)
+    args: nom, prenom, code_ine, code_nip
     """
     etud_url = get_etud_url()
     api_ver = get_portal_api_version()
@@ -225,7 +223,6 @@ def query_apogee_portal(**args):
     if api_ver > 1:
         if args["nom"] or args["prenom"]:
             # Ne fonctionne pas avec l'API 2 sur nom et prenom
-            # XXX TODO : va poser problème pour la page modif données étudiants : A VOIR
             return []
     portal_timeout = sco_preferences.get_preference("portal_timeout")
     req = etud_url + "?" + urllib.parse.urlencode(list(args.items()))
@@ -243,22 +240,21 @@ def xml_to_list_of_dicts(doc, req=None):
         "& ": "& ",  # only when followed by a space (avoid affecting entities)
         # to be completed...
     }
-    for k in invalid_entities:
-        doc = doc.replace(k, invalid_entities[k])
+    for k, repl in invalid_entities.items():
+        doc = doc.replace(k, repl)
     #
     try:
         dom = xml.dom.minidom.parseString(doc)
-    except xml.parsers.expat.ExpatError as e:
+    except xml.parsers.expat.ExpatError as exc:
         # Find faulty part
-        err_zone = doc.splitlines()[e.lineno - 1][e.offset : e.offset + 20]
+        err_zone = doc.splitlines()[exc.lineno - 1][exc.offset : exc.offset + 20]
         # catch bug: log and re-raise exception
         log(
-            "xml_to_list_of_dicts: exception in XML parseString\ndoc:\n%s\n(end xml doc)\n"
-            % doc
+            f"xml_to_list_of_dicts: exception in XML parseString\ndoc:\n{doc}\n(end xml doc)\n"
         )
         raise ScoValueError(
-            'erreur dans la réponse reçue du portail ! (peut être : "%s")' % err_zone
-        )
+            f'erreur dans la réponse reçue du portail ! (peut être : "{err_zone}")'
+        ) from exc
     infos = []
     try:
         if dom.childNodes[0].nodeName != "etudiants":
@@ -267,17 +263,19 @@ def xml_to_list_of_dicts(doc, req=None):
         for etudiant in etudiants:
             d = {}
             # recupere toutes les valeurs <valeur>XXX</valeur>
-            for e in etudiant.childNodes:
-                if e.nodeType == e.ELEMENT_NODE:
-                    childs = e.childNodes
+            for exc in etudiant.childNodes:
+                if exc.nodeType == exc.ELEMENT_NODE:
+                    childs = exc.childNodes
                     if len(childs):
-                        d[str(e.nodeName)] = childs[0].nodeValue
+                        d[str(exc.nodeName)] = childs[0].nodeValue
             infos.append(d)
-    except:
+    except Exception as exc:
         log("*** invalid XML response from Etudiant Web Service")
-        log("req=%s" % req)
-        log("doc=%s" % doc)
-        raise ValueError("invalid XML response from Etudiant Web Service\n%s" % doc)
+        log(f"req={req}")
+        log(f"doc={doc}")
+        raise ValueError(
+            f"invalid XML response from Etudiant Web Service\n{doc}"
+        ) from exc
     return infos
 
 
@@ -301,8 +299,8 @@ def get_infos_apogee_allaccents(nom, prenom):
     return infos
 
 
-def get_infos_apogee(nom, prenom):
-    """recupere les codes Apogee en utilisant le web service CRIT"""
+def get_etuds_apogee_for_nom_prenom(nom: str, prenom: str) -> list[dict]:
+    """Récupere les codes Apogee en utilisant le portail Apogée"""
     if (not nom) and (not prenom):
         return []
     # essaie plusieurs codages: tirets, accents
@@ -326,27 +324,27 @@ def get_infos_apogee(nom, prenom):
     return infos
 
 
-def get_etud_apogee(code_nip):
+def get_etuds_apogee_from_nip(code_nip: str) -> list[dict]:
     """Informations à partir du code NIP.
-    None si pas d'infos sur cet etudiant.
+    Liste des étudiants ayant ce code NIP.
     Exception si reponse invalide.
     """
     if not code_nip:
-        return {}
+        return []
     etud_url = get_etud_url()
     if not etud_url:
-        return {}
+        return []
     portal_timeout = sco_preferences.get_preference("portal_timeout")
     req = etud_url + "?" + urllib.parse.urlencode((("nip", code_nip),))
     doc = scu.query_portal(req, timeout=portal_timeout)
     d = _normalize_apo_fields(xml_to_list_of_dicts(doc, req=req))
     if not d:
-        return None
+        return []
     if len(d) > 1:
-        log(f"get_etud_apogee({code_nip}): {len(d)} etudiants !\n{doc}")
-        flash("Attention: plusieurs étudiants inscrits avec le NIP {code_nip}")
+        log(f"get_etuds_apogee_from_nip({code_nip}): {len(d)} etudiants !\n{doc}")
+        flash(f"Attention: plusieurs étudiants inscrits avec le NIP {code_nip}")
         # dans ce cas, renvoie le premier étudiant
-    return d[0]
+    return d
 
 
 def get_default_etapes():
@@ -413,14 +411,14 @@ def get_etapes_apogee():
                     SCO_CACHE_ETAPE_FILENAME, "w", encoding=scu.SCO_ENCODING
                 ) as f:
                     f.write(doc)
-        except:
+        except Exception:
             log(f"invalid XML response from getEtapes Web Service\n{etapes_url}")
             # Avons-nous la copie d'une réponse récente ?
             try:
                 doc = open(SCO_CACHE_ETAPE_FILENAME, encoding=scu.SCO_ENCODING).read()
                 infos = _parse_etapes_from_xml(doc)
                 log(f"using last saved version from {SCO_CACHE_ETAPE_FILENAME}")
-            except:
+            except Exception:
                 infos = {}
     else:
         # Pas de portail: utilise étapes par défaut livrées avec ScoDoc
@@ -452,7 +450,7 @@ def get_etapes_apogee_dept():
     xml_etapes_by_dept = sco_preferences.get_preference("xml_etapes_by_dept")
     if xml_etapes_by_dept:
         portal_dept_name = sco_preferences.get_preference("portal_dept_name")
-        log('get_etapes_apogee_dept: portal_dept_name="%s"' % portal_dept_name)
+        log(f'get_etapes_apogee_dept: portal_dept_name="{portal_dept_name}"')
     else:
         portal_dept_name = ""
         log("get_etapes_apogee_dept: pas de sections par departement")
@@ -460,8 +458,7 @@ def get_etapes_apogee_dept():
     infos = get_etapes_apogee()
     if portal_dept_name and portal_dept_name not in infos:
         log(
-            "get_etapes_apogee_dept: pas de section '%s' dans la reponse portail"
-            % portal_dept_name
+            f"get_etapes_apogee_dept: pas de section '{portal_dept_name}' dans la reponse portail"
         )
         return []
     if portal_dept_name:
@@ -469,8 +466,8 @@ def get_etapes_apogee_dept():
     else:
         # prend toutes les etapes
         etapes = []
-        for k in infos.keys():
-            etapes += list(infos[k].items())
+        for info in infos.values():
+            etapes += list(info.items())
 
     etapes.sort()  # tri sur le code etape
     return etapes
@@ -571,8 +568,8 @@ def check_paiement_etuds(etuds):
             etud["etape"] = None
         else:
             # Modifie certains champs de l'étudiant:
-            infos = get_etud_apogee(etud["code_nip"])
-            if infos:
+            etuds_apo = get_etuds_apogee_from_nip(etud["code_nip"])
+            if etuds_apo:
                 for k in (
                     "paiementinscription",
                     "paiementinscription_str",
@@ -580,7 +577,7 @@ def check_paiement_etuds(etuds):
                     "datefinalisationinscription_str",
                     "etape",
                 ):
-                    etud[k] = infos[k]
+                    etud[k] = etuds_apo[0][k]
             else:
                 etud["datefinalisationinscription"] = None
                 etud["datefinalisationinscription_str"] = "Erreur"
diff --git a/app/scodoc/sco_synchro_etuds.py b/app/scodoc/sco_synchro_etuds.py
index 4f7168f615007d4399e000bf9f6ed65ef7d3c97a..80f8fcf16ff68260ad34f5e8ec714941d935884f 100644
--- a/app/scodoc/sco_synchro_etuds.py
+++ b/app/scodoc/sco_synchro_etuds.py
@@ -837,7 +837,7 @@ def formsemestre_import_etud_admission(
 
     # Essaie de recuperer les etudiants des étapes, car
     # la requete get_inscrits_etape est en général beaucoup plus
-    # rapide que les requetes individuelles get_etud_apogee
+    # rapide que les requetes individuelles get_etuds_apogee_from_nip
     annee_apogee = str(
         scu.annee_scolaire_debut(sem["annee_debut"], sem["mois_debut_ord"])
     )
@@ -858,7 +858,8 @@ def formsemestre_import_etud_admission(
             data_apo = apo_etuds.get(code_nip)
             if not data_apo:
                 # pas vu dans les etudiants de l'étape, tente en individuel
-                data_apo = sco_portal_apogee.get_etud_apogee(code_nip)
+                etuds_apo = sco_portal_apogee.get_etuds_apogee_from_nip(code_nip)
+                data_apo = etuds_apo[0] if etuds_apo else None
             if data_apo:
                 update_etape_formsemestre_inscription(i, data_apo)
                 do_import_etud_admission(
diff --git a/app/views/scolar.py b/app/views/scolar.py
index ae2c6ef981b017a20c254e633cc489afa5a5240c..756b051e19c64f4f62f744d24ddc0f3f50ee3aa2 100644
--- a/app/views/scolar.py
+++ b/app/views/scolar.py
@@ -1374,15 +1374,17 @@ def _etudident_create_or_edit_form(edit):
         submitlabel = "Ajouter cet étudiant"
         H.append(
             """<h2>Création d'un étudiant</h2>
-        <p class="warning">En général, il est <b>recommandé</b> d'importer les
-        étudiants depuis Apogée ou via un fichier Excel (menu <b>Inscriptions</b>
-        dans le semestre).
-        </p>
-        <p>
-        N'utilisez ce formulaire au cas par cas que <b>pour les cas particuliers</b>
-        ou si votre établissement n'utilise pas d'autre logiciel de gestion des
-        inscriptions.
-        </p>
+        <div class="scobox warning">Attention
+            <p>En général, il est <b>recommandé</b> d'importer les
+            étudiants depuis Apogée ou via un fichier Excel (menu <b>Inscriptions</b>
+            dans le semestre).
+            </p>
+            <p>
+            N'utilisez ce formulaire au cas par cas que <b>pour les cas particuliers</b>
+            ou si votre établissement n'utilise pas d'autre logiciel de gestion des
+            inscriptions.
+            </p>
+        </div>
         <p class"warning"><em>L'étudiant créé ne sera pas inscrit.
         Pensez à l'inscrire dans un semestre !</em></p>
         """
@@ -1395,63 +1397,13 @@ def _etudident_create_or_edit_form(edit):
         etud_o: Identite = Identite.get_etud(etudid)
         descr.append(("etudid", {"default": etudid, "input_type": "hidden"}))
         H.append(f"""<h2>Modification des données de {etud_o.html_link_fiche()}</h2>""")
-        initvalues = sco_etud.etudident_list(cnx, {"etudid": etudid})
+        initvalues = sco_etud.etudident_list(cnx, {"etudid": etudid})  # XXX TODO
         assert len(initvalues) == 1
         initvalues = initvalues[0]
         submitlabel = "Modifier les données"
 
+    infos_apogee_html = _infos_apogee_html_etuds(scu.get_request_args(), initvalues)
     vals = scu.get_request_args()
-    nom = vals.get("nom", None)
-    if nom is None:
-        nom = initvalues.get("nom", None)
-    if nom is None:
-        infos = []
-    else:
-        prenom = vals.get("prenom", "")
-        if vals.get("tf_submitted", False) and not prenom:
-            prenom = initvalues.get("prenom", "")
-        infos = sco_portal_apogee.get_infos_apogee(nom, prenom)
-
-    if infos:
-        formatted_infos = [
-            """
-        <script type="text/javascript">
-        function copy_nip(nip) {
-        document.tf.code_nip.value = nip;
-        }
-        </script>
-        <ol>"""
-        ]
-        nanswers = len(infos)
-        nmax = 10  # nb max de reponse montrées
-        infos = infos[:nmax]
-        for i in infos:
-            formatted_infos.append("<li><ul>")
-            for k in i.keys():
-                if k != "nip":
-                    item = "<li>%s : %s</li>" % (k, i[k])
-                else:
-                    item = (
-                        '<li><form>%s : %s <input type="button" value="copier ce code" onmousedown="copy_nip(%s);"/></form></li>'
-                        % (k, i[k], i[k])
-                    )
-                formatted_infos.append(item)
-
-            formatted_infos.append("</ul></li>")
-        formatted_infos.append("</ol>")
-        m = "%d étudiants trouvés" % nanswers
-        if len(infos) != nanswers:
-            m += " (%d montrés)" % len(infos)
-        A = """<div class="infoapogee">
-        <h5>Informations Apogée</h5>
-        <p>%s</p>
-        %s
-        </div>""" % (
-            m,
-            "\n".join(formatted_infos),
-        )
-    else:
-        A = """<div class="infoapogee"><p>Pas d'informations d'Apogée</p></div>"""
 
     require_ine = sco_preferences.get_preference("always_require_ine")
 
@@ -1727,7 +1679,7 @@ def _etudident_create_or_edit_form(edit):
     if tf[0] in (0, -1):
         return render_template(
             "sco_page_dept.j2",
-            content="\n".join(H) + tf[1] + "<p>" + A,
+            content="\n".join(H) + tf[1] + infos_apogee_html,
             title="Création/édition d'étudiant",
         )
     else:
@@ -1746,8 +1698,7 @@ def _etudident_create_or_edit_form(edit):
                 content="\n".join(H)
                 + tf_error_message("Nom ou prénom invalide")
                 + tf[1]
-                + "<p>"
-                + A,
+                + infos_apogee_html,
                 title="Création/édition d'étudiant",
             )
         if not tf[2]["dont_check_homonyms"] and nb_homonyms > 0:
@@ -1773,9 +1724,8 @@ def _etudident_create_or_edit_form(edit):
                     """
                     )
                     + tf[1]
-                    + "<p>"
-                    + A
                     + homonyms_html
+                    + infos_apogee_html
                 ),
             )
         tf[2]["date_naissance"] = (
@@ -1796,19 +1746,70 @@ def _etudident_create_or_edit_form(edit):
                 etud_o.admission = admission
             admission.from_dict(tf[2])
             db.session.commit()
-
-            etud = sco_etud.etudident_list(cnx, {"etudid": etud_o.id})[0]
-            sco_etud.fill_etuds_info([etud])
-        # Inval semesters with this student:
-        to_inval = [s["formsemestre_id"] for s in etud["sems"]]
-        for formsemestre_id in to_inval:
-            sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
-        #
+            # Inval semesters with this student:
+            for inscription in etud_o.formsemestre_inscriptions:
+                sco_cache.invalidate_formsemestre(
+                    formsemestre_id=inscription.formsemestre_id
+                )
+            #
         return flask.redirect(
             url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
         )
 
 
+def _infos_apogee_html_etuds(vals: dict, initvalues: dict) -> str:
+    "fragment de html pour lister les étudiants correspondants"
+    nom = vals.get("nom", initvalues.get("nom", None))
+    nip = vals.get("code_nip", initvalues.get("code_nip", "")).strip()
+    if nom is None and nip is None:
+        etuds_apo = []
+    elif nip:
+        etuds_apo = sco_portal_apogee.get_etuds_apogee_from_nip(nip)
+    else:
+        prenom = vals.get("prenom", "")
+        if vals.get("tf_submitted", False) and not prenom:
+            prenom = initvalues.get("prenom", "")
+        etuds_apo = sco_portal_apogee.get_etuds_apogee_for_nom_prenom(nom, prenom)
+
+    if etuds_apo:
+        formatted_infos = [
+            """
+        <script type="text/javascript">
+        function copy_nip(nip) {
+        document.tf.code_nip.value = nip;
+        }
+        </script>
+        <ol>"""
+        ]
+        nanswers = len(etuds_apo)
+        nmax = 10  # nb max de réponse montrées
+        etuds_apo = etuds_apo[:nmax]
+        for i in etuds_apo:
+            formatted_infos.append("<li><ul>")
+            for k in i.keys():
+                if k != "nip":
+                    item = f"<li><tt>{k}</tt> : {i[k]}</li>"
+                else:
+                    item = f"""<li><form><tt>{k}</tt> : {i[k]}
+                    <input type="button" value="copier ce code" onmousedown="copy_nip({i[k]});"/>
+                    </form>
+                    </li>"""
+                formatted_infos.append(item)
+
+            formatted_infos.append("</ul></li>")
+        formatted_infos.append("</ol>")
+        m = f"{nanswers} étudiants trouvés"
+        if len(etuds_apo) != nanswers:
+            m += " ({len(etuds_apo)} affichés)"
+        return f"""
+        <div class="scobox infoapogee">
+            <div class="scobox-title">Informations Apogée</div>
+            <p>{m}</p>
+            {''.join(formatted_infos)}
+        </div>"""
+    return """<div class="scobox infoapogee">Pas d'informations d'Apogée</div>"""
+
+
 @bp.route("/etud_copy_in_other_dept/<int:etudid>", methods=["GET", "POST"])
 @scodoc
 @permission_required(
@@ -1985,7 +1986,7 @@ def check_group_apogee(group_id, etat=None, fix=False, fixmail=False):
             t["email"],
             t["code_nip"],
         )
-        infos = sco_portal_apogee.get_infos_apogee(nom, prenom)
+        infos = sco_portal_apogee.get_etuds_apogee_for_nom_prenom(nom, prenom)
         if not infos:
             info_apogee = f"""<b>Pas d'information</b>
                 (<a class="stdlink" href="{