diff --git a/app/scodoc/codes_cursus.py b/app/scodoc/codes_cursus.py
index 8e595ead7888a600b1402ac8eb193f16788d0dbe..5120aa05caff70e5012f75727cb138d963223cca 100644
--- a/app/scodoc/codes_cursus.py
+++ b/app/scodoc/codes_cursus.py
@@ -85,17 +85,6 @@ UE_ELECTIVE = 4  # UE "élective" dans certains cursus (UCAC?, ISCID)
 UE_PROFESSIONNELLE = 5  # UE "professionnelle" (ISCID, ...)
 UE_OPTIONNELLE = 6  # UE non fondamentales (ILEPS, ...)
 
-
-def ue_is_fondamentale(ue_type):
-    return ue_type in (UE_STANDARD, UE_STAGE_LP, UE_PROFESSIONNELLE)
-
-
-def ue_is_professionnelle(ue_type):
-    return (
-        ue_type == UE_PROFESSIONNELLE
-    )  # NB: les UE_PROFESSIONNELLE sont à la fois fondamentales et pro
-
-
 UE_TYPE_NAME = {
     UE_STANDARD: "Standard",
     UE_SPORT: "Sport/Culture (points bonus)",
@@ -104,8 +93,6 @@ UE_TYPE_NAME = {
     UE_ELECTIVE: "Elective (ISCID)",
     UE_PROFESSIONNELLE: "Professionnelle (ISCID)",
     UE_OPTIONNELLE: "Optionnelle",
-    #                 UE_FONDAMENTALE : '"Fondamentale" (eg UCAC)',
-    #                 UE_OPTIONNELLE : '"Optionnelle" (UCAC)'
 }
 
 # Couleurs RGB (dans [0.,1.]) des UE pour les bulletins:
diff --git a/app/scodoc/sco_formsemestre_inscriptions.py b/app/scodoc/sco_formsemestre_inscriptions.py
index e3474ebf22432e141aa2fbfe314ea053272fcdfe..7ee6439a7209399fc1c725298f510d8f6ce8a43f 100644
--- a/app/scodoc/sco_formsemestre_inscriptions.py
+++ b/app/scodoc/sco_formsemestre_inscriptions.py
@@ -191,7 +191,23 @@ def do_formsemestre_inscription_edit(args=None, formsemestre_id=None):
     )  # > modif inscription semestre
 
 
-def do_formsemestre_desinscription(etudid, formsemestre_id):
+def check_if_has_decision_jury(
+    formsemestre: FormSemestre, etudids: list[int] | set[int]
+):
+    "raise exception if one of the etuds has a decision in formsemestre"
+    nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
+    for etudid in etudids:
+        if nt.etud_has_decision(etudid):
+            etud = Identite.query.get(etudid)
+            raise ScoValueError(
+                f"""désinscription impossible: l'étudiant {etud.nomprenom} a
+                une décision de jury (la supprimer avant si nécessaire)"""
+            )
+
+
+def do_formsemestre_desinscription(
+    etudid, formsemestre_id: int, check_has_dec_jury=True
+):
     """Désinscription d'un étudiant.
     Si semestre extérieur et dernier inscrit, suppression de ce semestre.
     """
@@ -204,13 +220,8 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
         raise ScoValueError("désinscription impossible: semestre verrouille")
 
     # -- Si decisions de jury, désinscription interdite
-    nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
-
-    if nt.etud_has_decision(etudid):
-        raise ScoValueError(
-            f"""désinscription impossible: l'étudiant {etud.nomprenom} a
-            une décision de jury (la supprimer avant si nécessaire)"""
-        )
+    if check_has_dec_jury:
+        check_if_has_decision_jury(formsemestre, [etudid])
 
     insem = do_formsemestre_inscription_list(
         args={"formsemestre_id": formsemestre_id, "etudid": etudid}
diff --git a/app/scodoc/sco_inscr_passage.py b/app/scodoc/sco_inscr_passage.py
index f97e0d4409131d8f27a3634ea7b356f352135528..4f881022f1a17fc1b7823aa02f9dec3f49bb5034 100644
--- a/app/scodoc/sco_inscr_passage.py
+++ b/app/scodoc/sco_inscr_passage.py
@@ -36,13 +36,14 @@ from flask import url_for, g, request
 import app.scodoc.notesdb as ndb
 import app.scodoc.sco_utils as scu
 from app import db, log
-from app.models import Formation, FormSemestre, GroupDescr
+from app.comp import res_sem
+from app.comp.res_compat import NotesTableCompat
+from app.models import Formation, FormSemestre, GroupDescr, Identite
 from app.scodoc.gen_tables import GenTable
 from app.scodoc import html_sco_header
 from app.scodoc import sco_cache
 from app.scodoc import codes_cursus
 from app.scodoc import sco_etud
-from app.scodoc import sco_formsemestre
 from app.scodoc import sco_formsemestre_inscriptions
 from app.scodoc import sco_groups
 from app.scodoc import sco_preferences
@@ -50,62 +51,69 @@ from app.scodoc import sco_pv_dict
 from app.scodoc.sco_exceptions import ScoValueError
 
 
-def list_authorized_etuds_by_sem(sem, delai=274, ignore_jury=False):
+def _list_authorized_etuds_by_sem(
+    formsemestre: FormSemestre, ignore_jury=False
+) -> tuple[dict[int, dict], list[dict], dict[int, Identite]]:
     """Liste des etudiants autorisés à s'inscrire dans sem.
     delai = nb de jours max entre la date de l'autorisation et celle de debut du semestre cible.
     ignore_jury: si vrai, considère tous les étudiants comme autorisés, même
     s'ils n'ont pas de décision de jury.
     """
-    src_sems = list_source_sems(sem, delai=delai)
-    inscrits = list_inscrits(sem["formsemestre_id"])
+    src_sems = _list_source_sems(formsemestre)
+    inscrits = list_inscrits(formsemestre.id)
     r = {}
     candidats = {}  # etudid : etud (tous les etudiants candidats)
     nb = 0  # debug
-    for src in src_sems:
+    src_formsemestre: FormSemestre
+    for src_formsemestre in src_sems:
         if ignore_jury:
             # liste de tous les inscrits au semestre (sans dems)
-            liste = list_inscrits(src["formsemestre_id"]).values()
+            etud_list = list_inscrits(formsemestre.id).values()
         else:
             # liste des étudiants autorisés par le jury à s'inscrire ici
-            liste = list_etuds_from_sem(src, sem)
+            etud_list = _list_etuds_from_sem(src_formsemestre, formsemestre)
         liste_filtree = []
-        for e in liste:
+        for e in etud_list:
             # Filtre ceux qui se sont déjà inscrit dans un semestre APRES le semestre src
             auth_used = False  # autorisation deja utilisée ?
-            etud = sco_etud.get_etud_info(etudid=e["etudid"], filled=True)[0]
-            for isem in etud["sems"]:
-                if ndb.DateDMYtoISO(isem["date_debut"]) >= ndb.DateDMYtoISO(
-                    src["date_fin"]
-                ):
+            etud = Identite.get_etud(e["etudid"])
+            for inscription in etud.inscriptions():
+                if inscription.formsemestre.date_debut >= src_formsemestre.date_fin:
                     auth_used = True
             if not auth_used:
                 candidats[e["etudid"]] = etud
                 liste_filtree.append(e)
                 nb += 1
-        r[src["formsemestre_id"]] = {
+        r[src_formsemestre.id] = {
             "etuds": liste_filtree,
             "infos": {
-                "id": src["formsemestre_id"],
-                "title": src["titreannee"],
-                "title_target": "formsemestre_status?formsemestre_id=%s"
-                % src["formsemestre_id"],
+                "id": src_formsemestre.id,
+                "title": src_formsemestre.titre_annee(),
+                "title_target": url_for(
+                    "notes.formsemestre_status",
+                    scodoc_dept=g.scodoc_dept,
+                    formsemestre_id=src_formsemestre.id,
+                ),
                 "filename": "etud_autorises",
             },
         }
         # ajoute attribut inscrit qui indique si l'étudiant est déjà inscrit dans le semestre dest.
-        for e in r[src["formsemestre_id"]]["etuds"]:
+        for e in r[src_formsemestre.id]["etuds"]:
             e["inscrit"] = e["etudid"] in inscrits
 
     # Ajoute liste des etudiants actuellement inscrits
     for e in inscrits.values():
         e["inscrit"] = True
-    r[sem["formsemestre_id"]] = {
+    r[formsemestre.id] = {
         "etuds": list(inscrits.values()),
         "infos": {
-            "id": sem["formsemestre_id"],
-            "title": "Semestre cible: " + sem["titreannee"],
-            "title_target": "formsemestre_status?formsemestre_id=%s"
-            % sem["formsemestre_id"],
+            "id": formsemestre.id,
+            "title": "Semestre cible: " + formsemestre.titre_annee(),
+            "title_target": url_for(
+                "notes.formsemestre_status",
+                scodoc_dept=g.scodoc_dept,
+                formsemestre_id=formsemestre.id,
+            ),
             "comment": " actuellement inscrits dans ce semestre",
             "help": "Ces étudiants sont actuellement inscrits dans ce semestre. Si vous les décochez, il seront désinscrits.",
             "filename": "etud_inscrits",
@@ -115,7 +123,7 @@ def list_authorized_etuds_by_sem(sem, delai=274, ignore_jury=False):
     return r, inscrits, candidats
 
 
-def list_inscrits(formsemestre_id, with_dems=False):
+def list_inscrits(formsemestre_id: int, with_dems=False) -> list[dict]:
     """Étudiants déjà inscrits à ce semestre
     { etudid : etud }
     """
@@ -133,28 +141,27 @@ def list_inscrits(formsemestre_id, with_dems=False):
     return inscr
 
 
-def list_etuds_from_sem(src, dst) -> list[dict]:
-    """Liste des etudiants du semestre src qui sont autorisés à passer dans le semestre dst."""
-    target = dst["semestre_id"]
-    dpv = sco_pv_dict.dict_pvjury(src["formsemestre_id"])
+def _list_etuds_from_sem(src: FormSemestre, dst: FormSemestre) -> list[dict]:
+    """Liste des étudiants du semestre src qui sont autorisés à passer dans le semestre dst."""
+    target_semestre_id = dst.semestre_id
+    dpv = sco_pv_dict.dict_pvjury(src.id)
     if not dpv:
         return []
     etuds = [
         x["identite"]
         for x in dpv["decisions"]
-        if target in [a["semestre_id"] for a in x["autorisations"]]
+        if target_semestre_id in [a["semestre_id"] for a in x["autorisations"]]
     ]
     return etuds
 
 
-def list_inscrits_date(sem):
-    """Liste les etudiants inscrits dans n'importe quel semestre
-    du même département
-    SAUF sem à la date de début de sem.
+def list_inscrits_date(formsemestre: FormSemestre):
+    """Liste les etudiants inscrits à la date de début de formsemestre
+    dans n'importe quel semestre du même département
+    SAUF formsemestre
     """
     cnx = ndb.GetDBConnexion()
     cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
-    sem["date_debut_iso"] = ndb.DateDMYtoISO(sem["date_debut"])
     cursor.execute(
         """SELECT ins.etudid
         FROM
@@ -166,12 +173,18 @@ def list_inscrits_date(sem):
         AND S.date_fin >= %(date_debut_iso)s
         AND S.dept_id = %(dept_id)s
         """,
-        sem,
+        {
+            "formsemestre_id": formsemestre.id,
+            "date_debut_iso": formsemestre.date_debut.isoformat(),
+            "dept_id": formsemestre.dept_id,
+        },
     )
     return [x[0] for x in cursor.fetchall()]
 
 
-def do_inscrit(sem, etudids, inscrit_groupes=False, inscrit_parcours=False):
+def do_inscrit(
+    formsemestre: FormSemestre, etudids, inscrit_groupes=False, inscrit_parcours=False
+):
     """Inscrit ces etudiants dans ce semestre
     (la liste doit avoir été vérifiée au préalable)
     En option:
@@ -181,12 +194,11 @@ def do_inscrit(sem, etudids, inscrit_groupes=False, inscrit_parcours=False):
     (si les deux sont vrais, inscrit_parcours n'a pas d'effet)
     """
     # TODO à ré-écrire pour utiliser les modèles, notamment GroupDescr
-    formsemestre: FormSemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
     formsemestre.setup_parcours_groups()
     log(f"do_inscrit (inscrit_groupes={inscrit_groupes}): {etudids}")
     for etudid in etudids:
         sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
-            sem["formsemestre_id"],
+            formsemestre.id,
             etudid,
             etat=scu.INSCRIT,
             method="formsemestre_inscr_passage",
@@ -210,7 +222,7 @@ def do_inscrit(sem, etudids, inscrit_groupes=False, inscrit_parcours=False):
 
             cursem_groups_by_name = {
                 g["group_name"]: g
-                for g in sco_groups.get_sem_groups(sem["formsemestre_id"])
+                for g in sco_groups.get_sem_groups(formsemestre.id)
                 if g["group_name"]
             }
 
@@ -234,53 +246,46 @@ def do_inscrit(sem, etudids, inscrit_groupes=False, inscrit_parcours=False):
                     sco_groups.change_etud_group_in_partition(etudid, group)
 
 
-def do_desinscrit(sem: dict, etudids: list[int]):
+def do_desinscrit(
+    formsemestre: FormSemestre, etudids: list[int], check_has_dec_jury=True
+):
     "désinscrit les étudiants indiqués du formsemestre"
     log(f"do_desinscrit: {etudids}")
     for etudid in etudids:
         sco_formsemestre_inscriptions.do_formsemestre_desinscription(
-            etudid, sem["formsemestre_id"]
+            etudid, formsemestre.id, check_has_dec_jury=check_has_dec_jury
         )
 
 
-def list_source_sems(sem, delai=None) -> list[dict]:
+def _list_source_sems(formsemestre: FormSemestre) -> list[FormSemestre]:
     """Liste des semestres sources
-    sem est le semestre destination
+    formsemestre est le semestre destination
     """
-    # liste des semestres débutant a moins
-    # de delai (en jours) de la date de fin du semestre d'origine.
-    sems = sco_formsemestre.do_formsemestre_list()
-    othersems = []
-    d, m, y = [int(x) for x in sem["date_debut"].split("/")]
-    date_debut_dst = datetime.date(y, m, d)
-
-    delais = datetime.timedelta(delai)
-    for s in sems:
-        if s["formsemestre_id"] == sem["formsemestre_id"]:
-            continue  # saute le semestre destination
-        if s["date_fin"]:
-            d, m, y = [int(x) for x in s["date_fin"].split("/")]
-            date_fin = datetime.date(y, m, d)
-            if date_debut_dst - date_fin > delais:
-                continue  # semestre trop ancien
-            if date_fin > date_debut_dst:
-                continue  # semestre trop récent
-        # Elimine les semestres de formations speciales (sans parcours)
-        if s["semestre_id"] == codes_cursus.NO_SEMESTRE_ID:
-            continue
-        #
-        formation: Formation = Formation.query.get_or_404(s["formation_id"])
-        parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
-        if not parcours.ALLOW_SEM_SKIP:
-            if s["semestre_id"] < (sem["semestre_id"] - 1):
-                continue
-        othersems.append(s)
-    return othersems
+    # liste des semestres du même type de cursus terminant
+    # pas trop loin de la date de début du semestre destination
+    date_fin_min = formsemestre.date_debut - datetime.timedelta(days=275)
+    date_fin_max = formsemestre.date_debut + datetime.timedelta(days=45)
+    return (
+        FormSemestre.query.filter(
+            FormSemestre.dept_id == formsemestre.dept_id,
+            # saute le semestre destination:
+            FormSemestre.id != formsemestre.id,
+            # et les semestres de formations speciales (monosemestres):
+            FormSemestre.semestre_id != codes_cursus.NO_SEMESTRE_ID,
+            # semestre pas trop dans le futur
+            FormSemestre.date_fin <= date_fin_max,
+            # ni trop loin dans le passé
+            FormSemestre.date_fin >= date_fin_min,
+        )
+        .join(Formation)
+        .filter_by(type_parcours=formsemestre.formation.type_parcours)
+    ).all()
 
 
+# view, GET, POST
 def formsemestre_inscr_passage(
     formsemestre_id,
-    etuds=[],
+    etuds: str | list[int] | list[str] | int | None = None,
     inscrit_groupes=False,
     inscrit_parcours=False,
     submitted=False,
@@ -300,36 +305,41 @@ def formsemestre_inscr_passage(
     - Confirmation: indiquer les étudiants inscrits et ceux désinscrits, le total courant.
 
     """
+    formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
     inscrit_groupes = int(inscrit_groupes)
     inscrit_parcours = int(inscrit_parcours)
     ignore_jury = int(ignore_jury)
-    sem = sco_formsemestre.get_formsemestre(formsemestre_id)
     # -- check lock
-    if not sem["etat"]:
+    if not formsemestre.etat:
         raise ScoValueError("opération impossible: semestre verrouille")
-    header = html_sco_header.sco_header(page_title="Passage des étudiants")
+    header = html_sco_header.sco_header(
+        page_title="Passage des étudiants",
+        init_qtip=True,
+        javascripts=["js/etud_info.js"],
+    )
     footer = html_sco_header.sco_footer()
     H = [header]
+    etuds = [] if etuds is None else etuds
     if isinstance(etuds, str):
-        # list de strings, vient du form de confirmation
+        # string, vient du form de confirmation
         etuds = [int(x) for x in etuds.split(",") if x]
     elif isinstance(etuds, int):
         etuds = [etuds]
     elif etuds and isinstance(etuds[0], str):
         etuds = [int(x) for x in etuds]
 
-    auth_etuds_by_sem, inscrits, candidats = list_authorized_etuds_by_sem(
-        sem, ignore_jury=ignore_jury
+    auth_etuds_by_sem, inscrits, candidats = _list_authorized_etuds_by_sem(
+        formsemestre, ignore_jury=ignore_jury
     )
     etuds_set = set(etuds)
     candidats_set = set(candidats)
     inscrits_set = set(inscrits)
     candidats_non_inscrits = candidats_set - inscrits_set
-    inscrits_ailleurs = set(list_inscrits_date(sem))
+    inscrits_ailleurs = set(list_inscrits_date(formsemestre))
 
-    def set_to_sorted_etud_list(etudset):
+    def set_to_sorted_etud_list(etudset) -> list[Identite]:
         etuds = [candidats[etudid] for etudid in etudset]
-        etuds.sort(key=itemgetter("nom"))
+        etuds.sort(key=lambda e: e.sort_key)
         return etuds
 
     if submitted:
@@ -340,7 +350,7 @@ def formsemestre_inscr_passage(
 
     if not submitted:
         H += _build_page(
-            sem,
+            formsemestre,
             auth_etuds_by_sem,
             inscrits,
             candidats_non_inscrits,
@@ -355,30 +365,31 @@ def formsemestre_inscr_passage(
             if a_inscrire:
                 H.append("<h3>Étudiants à inscrire</h3><ol>")
                 for etud in set_to_sorted_etud_list(a_inscrire):
-                    H.append("<li>%(nomprenom)s</li>" % etud)
+                    H.append(f"<li>{etud.nomprenom}</li>")
                 H.append("</ol>")
             a_inscrire_en_double = inscrits_ailleurs.intersection(a_inscrire)
             if a_inscrire_en_double:
                 H.append("<h3>dont étudiants déjà inscrits:</h3><ul>")
                 for etud in set_to_sorted_etud_list(a_inscrire_en_double):
-                    H.append('<li class="inscrailleurs">%(nomprenom)s</li>' % etud)
+                    H.append(f'<li class="inscrit-ailleurs">{etud.nomprenom}</li>')
                 H.append("</ul>")
             if a_desinscrire:
                 H.append("<h3>Étudiants à désinscrire</h3><ol>")
-                for etudid in a_desinscrire:
-                    H.append(
-                        '<li class="desinscription">%(nomprenom)s</li>'
-                        % inscrits[etudid]
-                    )
+                a_desinscrire_ident = sorted(
+                    (Identite.query.get(eid) for eid in a_desinscrire),
+                    key=lambda x: x.sort_key,
+                )
+                for etud in a_desinscrire_ident:
+                    H.append(f'<li class="desinscription">{etud.nomprenom}</li>')
                 H.append("</ol>")
             todo = a_inscrire or a_desinscrire
             if not todo:
                 H.append("""<h3>Il n'y a rien à modifier !</h3>""")
             H.append(
                 scu.confirm_dialog(
-                    dest_url="formsemestre_inscr_passage"
-                    if todo
-                    else "formsemestre_status",
+                    dest_url=(
+                        "formsemestre_inscr_passage" if todo else "formsemestre_status"
+                    ),
                     message="<p>Confirmer ?</p>" if todo else "",
                     add_headers=False,
                     cancel_url="formsemestre_inscr_passage?formsemestre_id="
@@ -395,16 +406,26 @@ def formsemestre_inscr_passage(
                 )
             )
         else:
+            # check decisions jury ici pour éviter de recontruire le cache
+            # après chaque desinscription
+            sco_formsemestre_inscriptions.check_if_has_decision_jury(
+                formsemestre, a_desinscrire
+            )
+            # check decisions jury ici pour éviter de recontruire le cache
+            # après chaque desinscription
+            sco_formsemestre_inscriptions.check_if_has_decision_jury(
+                formsemestre, a_desinscrire
+            )
             with sco_cache.DeferredSemCacheManager():
                 # Inscription des étudiants au nouveau semestre:
                 do_inscrit(
-                    sem,
+                    formsemestre,
                     a_inscrire,
                     inscrit_groupes=inscrit_groupes,
                     inscrit_parcours=inscrit_parcours,
                 )
                 # Désinscriptions:
-                do_desinscrit(sem, a_desinscrire)
+                do_desinscrit(formsemestre, a_desinscrire, check_has_dec_jury=False)
 
             H.append(
                 f"""<h3>Opération effectuée</h3>
@@ -441,7 +462,7 @@ def formsemestre_inscr_passage(
 
 
 def _build_page(
-    sem,
+    formsemestre: FormSemestre,
     auth_etuds_by_sem,
     inscrits,
     candidats_non_inscrits,
@@ -450,7 +471,6 @@ def _build_page(
     inscrit_parcours=False,
     ignore_jury=False,
 ):
-    formsemestre: FormSemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
     inscrit_groupes = int(inscrit_groupes)
     inscrit_parcours = int(inscrit_parcours)
     ignore_jury = int(ignore_jury)
@@ -472,7 +492,7 @@ def _build_page(
         ),
         f"""<form name="f" method="post" action="{request.base_url}">
 
-        <input type="hidden" name="formsemestre_id" value="{sem['formsemestre_id']}"/>
+        <input type="hidden" name="formsemestre_id" value="{formsemestre.id}"/>
 
         <input type="submit" name="submitted" value="Appliquer les modifications"/>
         &nbsp;<a href="#help">aide</a>
@@ -491,7 +511,7 @@ def _build_page(
         </div>
 
         <div>{scu.EMO_WARNING}
-        <em>Seuls les semestres dont la date de fin est antérieure à la date de début
+        <em>Seuls les semestres dont la date de fin est proche de la date de début
         de ce semestre ({formsemestre.date_debut.strftime("%d/%m/%Y")}) sont pris en
         compte.</em>
         </div>
@@ -499,7 +519,7 @@ def _build_page(
 
         <input type="submit" name="submitted" value="Appliquer les modifications"/>
 
-        {formsemestre_inscr_passage_help(sem)}
+        {formsemestre_inscr_passage_help(formsemestre)}
 
         </form>
         """,
@@ -524,19 +544,20 @@ def _build_page(
     return H
 
 
-def formsemestre_inscr_passage_help(sem: dict):
+def formsemestre_inscr_passage_help(formsemestre: FormSemestre):
     "texte d'aide en bas  de la page passage des étudiants"
     return f"""<div class="pas_help"><h3><a name="help">Explications</a></h3>
     <p>Cette page permet d'inscrire des étudiants dans le semestre destination
     <a class="stdlink"
     href="{
-        url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=sem["formsemestre_id"] )
-    }">{sem['titreannee']}</a>,
+        url_for("notes.formsemestre_status",
+            scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id )
+    }">{formsemestre.titre_annee()}</a>,
     et d'en désincrire si besoin.
     </p>
     <p>Les étudiants sont groupés par semestre d'origine. Ceux qui sont en caractères
-    <span class="inscrit">gras</span> sont déjà inscrits dans le semestre destination.
-    Ceux qui sont en <span class"inscrailleurs">gras et en rouge</span> sont inscrits
+    <span class="deja-inscrit">gras</span> sont déjà inscrits dans le semestre destination.
+    Ceux qui sont en <span class="inscrit-ailleurs">gras et en rouge</span> sont inscrits
     dans un <em>autre</em> semestre.
     </p>
     <p>Au départ, les étudiants déjà inscrits sont sélectionnés; vous pouvez ajouter
@@ -555,7 +576,7 @@ def formsemestre_inscr_passage_help(sem: dict):
     conserve les groupes, on conserve les parcours (là aussi, pensez à les cocher dans
     <a class="stdlink" href="{
         url_for("notes.formsemestre_editwithmodules", scodoc_dept=g.scodoc_dept,
-                formsemestre_id=sem["formsemestre_id"] )
+                formsemestre_id=formsemestre.id )
     }">modifier le semestre</a> avant de faire passer les étudiants).
     </a>
 
@@ -656,25 +677,24 @@ def etuds_select_boxes(
             H.append("</div>")
             for etud in etuds:
                 if etud.get("inscrit", False):
-                    c = " inscrit"
+                    c = " deja-inscrit"
                     checked = 'checked="checked"'
                 else:
                     checked = ""
                     if etud["etudid"] in inscrits_ailleurs:
-                        c = " inscrailleurs"
+                        c = " inscrit-ailleurs"
                     else:
                         c = ""
                 sco_etud.format_etud_ident(etud)
                 if etud["etudid"]:
-                    elink = """<a class="discretelink %s" href="%s">%s</a>""" % (
-                        c,
-                        url_for(
-                            "scolar.fiche_etud",
+                    elink = f"""<a id="{etud['etudid']}" class="discretelink etudinfo {c}"
+                        href="{ url_for(
+                            'scolar.fiche_etud',
                             scodoc_dept=g.scodoc_dept,
-                            etudid=etud["etudid"],
-                        ),
-                        etud["nomprenom"],
-                    )
+                            etudid=etud['etudid'],
+                        )
+                        }">{etud['nomprenom']}</a>
+                    """
                 else:
                     # ce n'est pas un etudiant ScoDoc
                     elink = etud["nomprenom"]
diff --git a/app/scodoc/sco_synchro_etuds.py b/app/scodoc/sco_synchro_etuds.py
index 5cab9c870024a90b0161399fad3449c3c88bce06..a4a9fd2c8a293eacaa26c7ecb29931327a6f183d 100644
--- a/app/scodoc/sco_synchro_etuds.py
+++ b/app/scodoc/sco_synchro_etuds.py
@@ -35,7 +35,7 @@ from flask import g, url_for
 from flask_login import current_user
 
 from app import db, log
-from app.models import Admission, Adresse, Identite, ScolarNews
+from app.models import Admission, Adresse, FormSemestre, Identite, ScolarNews
 
 import app.scodoc.sco_utils as scu
 import app.scodoc.notesdb as ndb
@@ -94,6 +94,7 @@ def formsemestre_synchro_etuds(
     que l'on va importer/inscrire
     """
     etuds = etuds or []
+    formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
     inscrits_without_key = inscrits_without_key or []
     log(f"formsemestre_synchro_etuds: formsemestre_id={formsemestre_id}")
     sem = sco_formsemestre.get_formsemestre(formsemestre_id)
@@ -184,7 +185,7 @@ def formsemestre_synchro_etuds(
             inscrits_without_key
         )
         log("a_desinscrire_without_key=%s" % a_desinscrire_without_key)
-        inscrits_ailleurs = set(sco_inscr_passage.list_inscrits_date(sem))
+        inscrits_ailleurs = set(sco_inscr_passage.list_inscrits_date(formsemestre))
         a_inscrire = a_inscrire.intersection(etuds_set)
 
         if not dialog_confirmed:
@@ -205,10 +206,12 @@ def formsemestre_synchro_etuds(
 
             a_inscrire_en_double = inscrits_ailleurs.intersection(a_inscrire)
             if a_inscrire_en_double:
-                H.append("<h3>dont étudiants déjà inscrits:</h3><ol>")
+                H.append(
+                    "<h3>dont étudiants déjà inscrits dans un autre semestre:</h3><ol>"
+                )
                 for key in a_inscrire_en_double:
                     nom = f"""{etudsapo_ident[key]['nom']} {etudsapo_ident[key].get("prenom", "")}"""
-                    H.append(f'<li class="inscrailleurs">{nom}</li>')
+                    H.append(f'<li class="inscrit-ailleurs">{nom}</li>')
                 H.append("</ol>")
 
             if a_desinscrire:
@@ -260,16 +263,26 @@ def formsemestre_synchro_etuds(
             etudids_a_desinscrire = [nip2etudid(x) for x in a_desinscrire]
             etudids_a_desinscrire += a_desinscrire_without_key
             #
+            # check decisions jury ici pour éviter de recontruire le cache
+            # après chaque desinscription
+            sco_formsemestre_inscriptions.check_if_has_decision_jury(
+                formsemestre, a_desinscrire
+            )
             with sco_cache.DeferredSemCacheManager():
-                do_import_etuds_from_portal(sem, a_importer, etudsapo_ident)
-                sco_inscr_passage.do_inscrit(sem, etudids_a_inscrire)
-                sco_inscr_passage.do_desinscrit(sem, etudids_a_desinscrire)
+                do_import_etuds_from_portal(formsemestre, a_importer, etudsapo_ident)
+                sco_inscr_passage.do_inscrit(formsemestre, etudids_a_inscrire)
+                sco_inscr_passage.do_desinscrit(
+                    formsemestre, etudids_a_desinscrire, check_has_dec_jury=False
+                )
 
             H.append(
-                """<h3>Opération effectuée</h3>
+                f"""<h3>Opération effectuée</h3>
             <ul>
-                <li><a class="stdlink" href="formsemestre_synchro_etuds?formsemestre_id=%s">Continuer la synchronisation</a></li>"""
-                % formsemestre_id
+                <li><a class="stdlink" href="{
+                    url_for('notes.formsemestre_synchro_etuds',
+                    scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id
+                )}">Continuer la synchronisation</a>
+                </li>"""
             )
             #
             partitions = sco_groups.get_partitions_list(
@@ -279,8 +292,9 @@ def formsemestre_synchro_etuds(
                 H.append(
                     f"""<li><a class="stdlink" href="{
                         url_for("scolar.partition_editor",
-                            scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
-                    }">Répartir les groupes de {partitions[0]["partition_name"]}</a></li>
+                            scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id
+                    )}">Répartir les groupes de {partitions[0]["partition_name"]}</a>
+                    </li>
                     """
                 )
 
@@ -618,7 +632,7 @@ def get_annee_naissance(ddmmyyyyy: str) -> int:
         return None
 
 
-def do_import_etuds_from_portal(sem, a_importer, etudsapo_ident):
+def do_import_etuds_from_portal(formsemestre: FormSemestre, a_importer, etudsapo_ident):
     """Inscrit les etudiants Apogee dans ce semestre."""
     log(f"do_import_etuds_from_portal: a_importer={a_importer}")
     if not a_importer:
@@ -672,7 +686,7 @@ def do_import_etuds_from_portal(sem, a_importer, etudsapo_ident):
 
             # Inscription au semestre
             sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
-                sem["formsemestre_id"],
+                formsemestre.id,
                 etud.id,
                 etat=scu.INSCRIT,
                 etape=args["etape"],
@@ -716,7 +730,7 @@ def do_import_etuds_from_portal(sem, a_importer, etudsapo_ident):
     ScolarNews.add(
         typ=ScolarNews.NEWS_INSCR,
         text=f"Import Apogée de {len(created_etudids)} étudiants en ",
-        obj=sem["formsemestre_id"],
+        obj=formsemestre.id,
     )
 
 
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index 948554f2f5b3447981d8143e8ce3cd06c3edcaf1..5d141fd58d07fe976f0f2b760da0fb3e9e4067a4 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -3714,10 +3714,17 @@ span.sp_etape {
   color: black;
 }
 
-.inscrailleurs {
+.deja-inscrit {
+  font-weight: bold;
+  color: rgb(1, 76, 1) !important;
+}
+.inscrit-ailleurs {
   font-weight: bold;
   color: red !important;
 }
+div.etuds_select_boxes {
+  margin-bottom: 16px;
+}
 
 span.paspaye,
 span.paspaye a {