diff --git a/app/but/jury_but.py b/app/but/jury_but.py
index d2d1039b2a1e9877c5bcad448e62cb4b0e652398..f8611914da7a2ff66f6a27f80ece7933a0916754 100644
--- a/app/but/jury_but.py
+++ b/app/but/jury_but.py
@@ -584,9 +584,10 @@ class DecisionsProposeesAnnee(DecisionsProposees):
 
     def compute_decisions_niveaux(self) -> dict[int, "DecisionsProposeesRCUE"]:
         """Pour chaque niveau de compétence de cette année, construit
-        le DecisionsProposeesRCUE,
-        ou None s'il n'y en a pas
+        le DecisionsProposeesRCUE, ou None s'il n'y en a pas
             (ne devrait pas arriver car compute_rcues_annee vérifie déjà cela).
+
+        Appelé à la construction du deca, donc avant décisions manuelles.
         Return: { niveau_id : DecisionsProposeesRCUE }
         """
         # Retrouve le RCUE associé à chaque niveau
@@ -633,6 +634,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
 
     def record_form(self, form: dict):
         """Enregistre les codes de jury en base
+        à partir d'un dict représentant le formulaire jury BUT:
         form dict:
         - 'code_ue_1896' : 'AJ'  code pour l'UE id 1896
         - 'code_rcue_6" : 'ADM'  code pour le RCUE du niveau 6
@@ -642,31 +644,41 @@ class DecisionsProposeesAnnee(DecisionsProposees):
         et qu'il n'y en a pas déjà, enregistre ceux par défaut.
         """
         log("jury_but.DecisionsProposeesAnnee.record_form")
-        with sco_cache.DeferredSemCacheManager():
-            for key in form:
-                code = form[key]
-                # Codes d'UE
-                m = re.match(r"^code_ue_(\d+)$", key)
+        code_annee = None
+        codes_rcues = []  # [ (dec_rcue, code), ... ]
+        codes_ues = []  #  [ (dec_ue, code), ... ]
+        for key in form:
+            code = form[key]
+            # Codes d'UE
+            m = re.match(r"^code_ue_(\d+)$", key)
+            if m:
+                ue_id = int(m.group(1))
+                dec_ue = self.decisions_ues.get(ue_id)
+                if not dec_ue:
+                    raise ScoValueError(f"UE invalide ue_id={ue_id}")
+                codes_ues.append((dec_ue, code))
+            else:
+                # Codes de RCUE
+                m = re.match(r"^code_rcue_(\d+)$", key)
                 if m:
-                    ue_id = int(m.group(1))
-                    dec_ue = self.decisions_ues.get(ue_id)
-                    if not dec_ue:
-                        raise ScoValueError(f"UE invalide ue_id={ue_id}")
+                    niveau_id = int(m.group(1))
+                    dec_rcue = self.decisions_rcue_by_niveau.get(niveau_id)
+                    if not dec_rcue:
+                        raise ScoValueError(f"RCUE invalide niveau_id={niveau_id}")
+                    codes_rcues.append((dec_rcue, code))
+                elif key == "code_annee":
+                    # Code annuel
+                    code_annee = code
+
+            with sco_cache.DeferredSemCacheManager():
+                # Enregistre les codes, dans l'ordre UE, RCUE, Année
+                for dec_ue, code in codes_ues:
                     dec_ue.record(code)
-                else:
-                    # Codes de RCUE
-                    m = re.match(r"^code_rcue_(\d+)$", key)
-                    if m:
-                        niveau_id = int(m.group(1))
-                        dec_rcue = self.decisions_rcue_by_niveau.get(niveau_id)
-                        if not dec_rcue:
-                            raise ScoValueError(f"RCUE invalide niveau_id={niveau_id}")
-                        dec_rcue.record(code)
-                    elif key == "code_annee":
-                        # Code annuel
-                        self.record(code)
-
-            self.record_all()
+                for dec_rcue, code in codes_rcues:
+                    dec_rcue.record(code)
+                self.record(code_annee)
+                self.record_all()
+
             db.session.commit()
 
     def record(self, code: str, no_overwrite=False):
@@ -790,6 +802,15 @@ class DecisionsProposeesAnnee(DecisionsProposees):
                     msg=f"Validation année BUT{self.annee_but}: effacée",
                 )
                 db.session.delete(validation)
+        # Efface éventuelle validation de semestre
+        # (en principe inutilisées en BUT)
+        # et autres UEs (en cas de changement d'architecture de formation depuis le jury ?)
+        #
+        for validation in ScolarFormSemestreValidation.query.filter_by(
+            etudid=self.etud.id, formsemestre_id=self.formsemestre_id
+        ):
+            db.session.delete(validation)
+
         db.session.flush()
         self.invalidate_formsemestre_cache()
 
@@ -878,6 +899,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
         inscription_etat: str = scu.INSCRIT,
     ):
         super().__init__(etud=dec_prop_annee.etud)
+        self.deca = dec_prop_annee
         self.rcue = rcue
         if rcue is None:  # RCUE non dispo, eg un seul semestre
             self.codes = []
@@ -928,7 +950,11 @@ class DecisionsProposeesRCUE(DecisionsProposees):
         } codes={self.codes} explanation={self.explanation}"""
 
     def record(self, code: str, no_overwrite=False):
-        """Enregistre le code"""
+        """Enregistre le code RCUE.
+        Note:
+            - si le RCUE est ADJ, les UE non validées sont passées à ADJ
+        XXX on pourra imposer ici d'autres règles de cohérence
+        """
         if self.rcue is None:
             return  # pas de RCUE a enregistrer
         if self.inscription_etat != scu.INSCRIT:
@@ -964,6 +990,15 @@ class DecisionsProposeesRCUE(DecisionsProposees):
                 msg=f"Validation {self.rcue}: {code}",
             )
             db.session.add(self.validation)
+            # Modifie au besoin les codes d'UE
+            if code == "ADJ":
+                deca = self.deca
+                for ue_id in (self.rcue.ue_1.id, self.rcue.ue_2.id):
+                    dec_ue = deca.decisions_ues.get(ue_id)
+                    if dec_ue and dec_ue.code_valide not in CODES_UE_VALIDES:
+                        log(f"rcue.record: force ADJ sur {dec_ue}")
+                        dec_ue.record("ADJ")
+
         if self.rcue.formsemestre_1 is not None:
             sco_cache.invalidate_formsemestre(
                 formsemestre_id=self.rcue.formsemestre_1.id
@@ -972,6 +1007,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
             sco_cache.invalidate_formsemestre(
                 formsemestre_id=self.rcue.formsemestre_2.id
             )
+        self.code_valide = code  # mise à jour état
         self.recorded = True
 
     def erase(self):
@@ -1032,14 +1068,14 @@ class DecisionsProposeesUE(DecisionsProposees):
     ):
         # Une UE peut être validée plusieurs fois en cas de redoublement (qu'elle soit capitalisée ou non)
         # mais ici on a restreint au formsemestre donc une seule (prend la première)
-        self.validation = ScolarFormSemestreValidation.query.filter_by(
+        validation = ScolarFormSemestreValidation.query.filter_by(
             etudid=etud.id, formsemestre_id=formsemestre.id, ue_id=ue.id
         ).first()
         super().__init__(
             etud=etud,
-            code_valide=self.validation.code if self.validation is not None else None,
+            code_valide=validation.code if validation is not None else None,
         )
-        # log(f"built {self}")
+        self.validation = validation
         self.formsemestre = formsemestre
         self.ue: UniteEns = ue
         self.rcue: RegroupementCoherentUE = None
@@ -1082,7 +1118,7 @@ class DecisionsProposeesUE(DecisionsProposees):
 
     def set_rcue(self, rcue: RegroupementCoherentUE):
         """Rattache cette UE à un RCUE. Cela peut modifier les codes
-        proposés (si compensation)"""
+        proposés par compute_codes() (si compensation)"""
         self.rcue = rcue
 
     def compute_codes(self):
@@ -1138,6 +1174,7 @@ class DecisionsProposeesUE(DecisionsProposees):
             log(f"DecisionsProposeesUE: recording {self.validation}")
 
         sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre.id)
+        self.code_valide = code  # mise à jour
         self.recorded = True
 
     def erase(self):
diff --git a/app/but/jury_but_view.py b/app/but/jury_but_view.py
index d08329ea477e70f0c92f2ef43ba6a49e7f40f5a6..3140bd02e228c29d2f10a5e3a8e2b2a499bb7dd5 100644
--- a/app/but/jury_but_view.py
+++ b/app/but/jury_but_view.py
@@ -30,6 +30,7 @@ from app.models import (
     Identite,
     UniteEns,
     ScolarAutorisationInscription,
+    ScolarFormSemestreValidation,
 )
 from app.scodoc import html_sco_header
 from app.scodoc.sco_exceptions import ScoValueError
@@ -50,7 +51,7 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
                 _gen_but_select("code_annee", deca.codes, deca.code_valide,
                     disabled=True, klass="manual")
             }
-            <span>({'non ' if deca.code_valide is None else ''}enregistrée)</span>
+            <span>({deca.code_valide or 'non'} enregistrée)</span>
         </div>
         """
     )
@@ -108,8 +109,8 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
             if ue.niveau_competence and ue.niveau_competence.id == niveau.id
         ]
         ue_pair = ues[0] if ues else None
-        # Les UEs à afficher, toujours en readonly
-        #  sur le formsemestre de l'année précédente du redoublant
+        # Les UEs à afficher,
+        #  qui seront toujours en readonly sur le formsemestre de l'année précédente du redoublant
         ues_ro = [
             (
                 ue_impair,
@@ -132,6 +133,7 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
                         deca.decisions_ues[ue.id],
                         disabled=read_only or ue_read_only,
                         annee_prec=ue_read_only,
+                        niveau_id=ue.niveau_competence.id,
                     )
                 )
             else:
@@ -150,6 +152,7 @@ def _gen_but_select(
     code_valide: str,
     disabled: bool = False,
     klass: str = "",
+    data: dict = {},
 ) -> str:
     "Le menu html select avec les codes"
     # if disabled:  # mauvaise idée car le disabled est traité en JS
@@ -165,8 +168,11 @@ def _gen_but_select(
     )
     return f"""<select required name="{name}"
         class="but_code {klass}" 
+        data-orig_code="{code_valide or (codes[0] if codes else '')}"
+        data-orig_recorded="{code_valide or ''}"
         onchange="change_menu_code(this);"
         {"disabled" if disabled else ""}
+        {" ".join( f'data-{k}="{v}"' for (k,v) in data.items() )}
         >{options_htm}</select>
         """
 
@@ -176,6 +182,7 @@ def _gen_but_niveau_ue(
     dec_ue: DecisionsProposeesUE,
     disabled: bool = False,
     annee_prec: bool = False,
+    niveau_id: int = None,
 ) -> str:
     if dec_ue.ue_status and dec_ue.ue_status["is_capitalized"]:
         moy_ue_str = f"""<span class="ue_cap">{
@@ -196,7 +203,14 @@ def _gen_but_niveau_ue(
         """
     else:
         moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.moy_ue)}</span>"""
-        scoplement = ""
+        if dec_ue.code_valide:
+            scoplement = f"""<div class="scoplement">
+            Code {dec_ue.code_valide} enregistré le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
+            à {dec_ue.validation.event_date.strftime("%Hh%M")}
+            </div>
+            """
+        else:
+            scoplement = ""
 
     return f"""<div class="but_niveau_ue {
         'recorded' if dec_ue.code_valide is not None else ''}
@@ -209,8 +223,10 @@ def _gen_but_niveau_ue(
     </div>
     <div class="but_code">{
         _gen_but_select("code_ue_"+str(ue.id), 
-            dec_ue.codes, 
-            dec_ue.code_valide, disabled=disabled
+            dec_ue.codes,
+            dec_ue.code_valide,
+            disabled=disabled,
+            klass=f"code_ue ue_rcue_{niveau_id}" if not disabled else ""
         )
     }</div>
     
@@ -250,12 +266,13 @@ def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
                 {scoplement}
             </div>
             <div class="but_code">
-                <div>{_gen_but_select("code_rcue_"+str(niveau.id),
+                {_gen_but_select("code_rcue_"+str(niveau.id),
                     dec_rcue.codes,
                     dec_rcue.code_valide,
-                    disabled=True, klass="manual"
+                    disabled=True,
+                    klass="manual code_rcue",
+                    data = { "niveau_id" : str(niveau.id)}
                 )}
-                </div>
             </div>
         </div>
     """
@@ -274,17 +291,15 @@ def jury_but_semestriel(
     semestre_terminal = (
         formsemestre.semestre_id >= formsemestre.formation.get_parcours().NB_SEM
     )
+    autorisations_passage = ScolarAutorisationInscription.query.filter_by(
+        etudid=etud.id,
+        origin_formsemestre_id=formsemestre.id,
+    ).all()
     # Par défaut: autorisé à passer dans le semestre suivant si sem. impair,
     # ou si décision déjà enregistrée:
     est_autorise_a_passer = (formsemestre.semestre_id % 2) or (
         formsemestre.semestre_id + 1
-    ) in (
-        a.semestre_id
-        for a in ScolarAutorisationInscription.query.filter_by(
-            etudid=etud.id,
-            origin_formsemestre_id=formsemestre.id,
-        )
-    )
+    ) in (a.semestre_id for a in autorisations_passage)
     decisions_ues = {
         ue.id: DecisionsProposeesUE(etud, formsemestre, ue, inscription_etat)
         for ue in ues
@@ -308,7 +323,9 @@ def jury_but_semestriel(
             flash("codes enregistrés")
             if not semestre_terminal:
                 if request.form.get("autorisation_passage"):
-                    if not est_autorise_a_passer:
+                    if not formsemestre.semestre_id + 1 in (
+                        a.semestre_id for a in autorisations_passage
+                    ):
                         ScolarAutorisationInscription.autorise_etud(
                             etud.id,
                             formsemestre.formation.formation_code,
@@ -368,21 +385,31 @@ def jury_but_semestriel(
             }">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
         </div>
         </div>
-        <h3>Jury sur un semestre BUT isolé</h3>
+        <h3>Jury sur un semestre BUT isolé (ne concerne que les UEs)</h3>
         {warning}
         </div>
 
         <form method="post" id="jury_but">
         """,
     ]
-    if (not read_only) and any([dec.code_valide for dec in decisions_ues.values()]):
-        erase_span = f"""<a href="{
-            url_for("notes.formsemestre_jury_but_erase", 
-            scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, 
-            etudid=etud.id, only_one_sem=1)
-            }" class="stdlink">effacer les décisions enregistrées</a>"""
-    else:
-        erase_span = "Cet étudiant n'a aucune décision enregistrée pour ce semestre."
+
+    erase_span = ""
+    if not read_only:
+        # Requête toutes les validations (pas seulement celles du deca courant),
+        # au cas où: changement d'architecture, saisie en mode classique, ...
+        validations = ScolarFormSemestreValidation.query.filter_by(
+            etudid=etud.id, formsemestre_id=formsemestre.id
+        ).all()
+        if validations:
+            erase_span = f"""<a href="{
+                url_for("notes.formsemestre_jury_but_erase",
+                scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id,
+                etudid=etud.id, only_one_sem=1)
+                }" class="stdlink">effacer les décisions enregistrées</a>"""
+        else:
+            erase_span = (
+                "Cet étudiant n'a aucune décision enregistrée pour ce semestre."
+            )
 
     H.append(
         f"""
@@ -436,6 +463,9 @@ def jury_but_semestriel(
                 <input type="checkbox" name="autorisation_passage" value="1" {
                     "checked" if est_autorise_a_passer else ""}>
                 <em>autoriser à passer dans le semestre S{formsemestre.semestre_id+1}</em>
+                {("(autorisations enregistrées: " + ' '.join(
+                    'S' + str(a.semestre_id or '') for a in autorisations_passage) + ")"
+                ) if autorisations_passage else ""}
                 </input>
                 </div>
                 """
diff --git a/app/models/but_validations.py b/app/models/but_validations.py
index ab820bc38663792d48f9a94b3d548c64009bbd11..52826003e6a2d65dde3516c79208fe34738bba1a 100644
--- a/app/models/but_validations.py
+++ b/app/models/but_validations.py
@@ -68,7 +68,8 @@ class ApcValidationRCUE(db.Model):
         "description en HTML"
         return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}:
                 <b>{self.code}</b>
-                <em>enregistrée le {self.date.strftime("%d/%m/%Y")}</em>"""
+                <em>enregistrée le {self.date.strftime("%d/%m/%Y")}
+                à {self.date.strftime("%Hh%M")}</em>"""
 
     def niveau(self) -> ApcNiveau:
         """Le niveau de compétence associé à cet RCUE."""
diff --git a/app/models/validations.py b/app/models/validations.py
index 5998ccd7388989463c597b825eec126cb6c04635..aa532517090b85bf7c6819f3935442ababfa2850 100644
--- a/app/models/validations.py
+++ b/app/models/validations.py
@@ -93,6 +93,10 @@ class ScolarAutorisationInscription(db.Model):
         db.ForeignKey("notes_formsemestre.id"),
     )
 
+    def __repr__(self) -> str:
+        return f"""{self.__class__.__name__}(id={self.id}, etudid={
+            self.etudid}, semestre_id={self.semestre_id})"""
+
     def to_dict(self) -> dict:
         "as a dict"
         d = dict(self.__dict__)
diff --git a/app/scodoc/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py
index 98451498fef948e702e446a31f5f3fe6679fbe0d..2edd3655640f7b7a997b1344a7eef438ecc3ede4 100644
--- a/app/scodoc/sco_formsemestre.py
+++ b/app/scodoc/sco_formsemestre.py
@@ -87,6 +87,8 @@ _formsemestreEditor = ndb.EditableTable(
         "resp_can_edit": bool,
         "resp_can_change_ens": bool,
         "ens_can_edit_eval": bool,
+        "bul_bgcolor": lambda color: color or "white",
+        "titre": lambda titre: titre or "sans titre",
     },
 )
 
diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py
index a6e79b1f4048167829abbeb7460b2c31e2ad6551..5a776c35b296d53f79265f82021248a722b013a8 100644
--- a/app/scodoc/sco_formsemestre_edit.py
+++ b/app/scodoc/sco_formsemestre_edit.py
@@ -312,6 +312,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
                 le titre: ils seront automatiquement ajoutés <input type="button" 
                 value="remettre titre par défaut" onClick="document.tf.titre.value='{
                 _default_sem_title(formation)}';"/>""",
+                "allow_null": False,
             },
         ),
         (
diff --git a/app/static/css/jury_but.css b/app/static/css/jury_but.css
index f865400a046140695697e7ec7d8263d3b493b555..74a1f5b71e4df562def1abf07bca58fd06ee05d8 100644
--- a/app/static/css/jury_but.css
+++ b/app/static/css/jury_but.css
@@ -157,6 +157,8 @@ div.but_niveau_ue.annee_prec {
     background-color: rgb(167, 167, 0);
 }
 
+div.but_section_annee,
+div.but_niveau_rcue.modified,
 div.but_niveau_ue.modified {
     background-color: rgb(255, 214, 254);
 }
diff --git a/app/static/js/jury_but.js b/app/static/js/jury_but.js
index 54d28a841d7c98a6329e1adced62a812324a3ac6..b016f110cca407499aa704308e286c561441e4de 100644
--- a/app/static/js/jury_but.js
+++ b/app/static/js/jury_but.js
@@ -5,17 +5,36 @@ function enable_manual_codes(elt) {
     $(".jury_but select.manual").prop("disabled", !elt.checked);
 }
 
-// changement menu code:
+// changement d'un menu code:
 function change_menu_code(elt) {
-    elt.parentElement.parentElement.classList.remove("recorded");
-    // TODO: comparer avec valeur enregistrée (à mettre en data-orig ?)
-    // et colorer en fonction
-    elt.parentElement.parentElement.classList.add("modified");
+    // Ajuste styles pour visualiser codes enregistrés/modifiés
+    if (elt.value != elt.dataset.orig_code) {
+        elt.parentElement.parentElement.classList.add("modified");
+    } else {
+        elt.parentElement.parentElement.classList.remove("modified");
+    }
+    if (elt.value == elt.dataset.orig_recorded) {
+        elt.parentElement.parentElement.classList.add("recorded");
+    } else {
+        elt.parentElement.parentElement.classList.remove("recorded");
+    }
+    // Si RCUE passant en ADJ, change les menus des UEs associées
+    if (elt.classList.contains("code_rcue")
+        && elt.dataset.niveau_id
+        && elt.value == "ADJ"
+        && elt.value != elt.dataset.orig_recorded) {
+        let ue_selects = elt.parentElement.parentElement.parentElement.querySelectorAll(
+            "select.ue_rcue_" + elt.dataset.niveau_id);
+        ue_selects.forEach(select => {
+            select.value = "ADJ";
+            change_menu_code(select); // pour changer les styles
+        });
+    }
 }
 
 $(function () {
     // Recupère la liste ordonnées des etudids
-    // pour avoir le "suivant" etr le "précédent"
+    // pour avoir le "suivant" et le "précédent"
     // (liens de navigation)
     const url = new URL(document.URL);
     const frags = url.pathname.split("/"); // .../formsemestre_validation_but/formsemestre_id/etudid
diff --git a/tests/unit/cursus_but_geii_lyon.yaml b/tests/unit/cursus_but_geii_lyon.yaml
index e73b7b1db694526c97deaac80a2ac5f3ada875f2..4dd216cc6d5ec2b273cf22a31a53016ff3b68fcb 100644
--- a/tests/unit/cursus_but_geii_lyon.yaml
+++ b/tests/unit/cursus_but_geii_lyon.yaml
@@ -1,204 +1,279 @@
-# Tests unitaires jury BUT - IUT Lyon GEII
-# Essais avec un BUT GEII, 2 UE en BUT1 / 4 UE en BUT2-BUT3 et 3 parcours
-# Contrib Pascal B.
-
-ReferentielCompetences:
-  filename: but-GEII-05012022-081639.xml
-  specialite: GEII
-
-Formation:
-  filename: scodoc_formation_BUT_GEII_lyon_v1.xml
-  # Association des UE aux compétences:
-  ues:
-    # S1 : Tronc commun GEII
-    'UE11':
-      annee: BUT1
-      competence: Concevoir
-    'UE12':
-      annee: BUT1
-      competence: Vérifier
-
-    # S2 : Tronc commun GEII
-    'UE21':
-      annee: BUT1
-      competence: Concevoir
-    'UE22':
-      annee: BUT1
-      competence: Vérifier
-
-    # S3 : Tronc commun GEII
-    'UE31':
-      annee: BUT2
-      competence: Concevoir
-    'UE32':
-      annee: BUT2
-      competence: Vérifier
-    'UE33':
-      annee: BUT2
-      competence: Maintenir
-    # S3 : Parcours EME
-    'UE34EME':
-      annee: BUT2
-      competence: Installer
-      parcours: EME
-    # S3 : Parcours ESE
-    'UE34ESE':
-      annee: BUT2
-      competence: Implanter
-      parcours: ESE
-    # S3 : Parcours AII
-    'UE34AII':
-      annee: BUT2
-      competence: Intégrer
-      parcours: AII
-
-    # S4 : Tronc commun GEII
-    'UE41':
-      annee: BUT2
-      competence: Concevoir
-    'UE42':
-      annee: BUT2
-      competence: Vérifier
-    'UE43':
-      annee: BUT2
-      competence: Maintenir
-    # S4 : Parcours EME
-    'UE44EME':
-      annee: BUT2
-      competence: Installer
-      parcours: EME
-    # S4 : Parcours ESE
-    'UE44ESE':
-      annee: BUT2
-      competence: Implanter
-      parcours: ESE
-    # S4 : Parcours AII
-    'UE44AII':
-      annee: BUT2
-      competence: Intégrer
-      parcours: AII
-
-  modules_parcours:
-    # cette section permet d'associer des modules à des parcours
-    # les codes modules peuvent être des regexp
-    EME: [ .*EME.* ]
-    ESE: [ .*ESE.* ] 
-    AII: [ .*AII.* ] 
-
-FormSemestres:
-  # S1 et S2 :
-  S1:
-    idx: 1
-    date_debut: 2021-09-01
-    date_fin: 2022-01-15
-  S2: 
-    idx: 2
-    date_debut: 2022-01-16
-    date_fin: 2022-06-30
-  # S3 avec les trois parcours réunis:
-  # S3:
-  #   idx: 3
-  #   date_debut: 2022-09-01
-  #   date_fin: 2023-01-13
-  #   codes_parcours: ['AII', 'EME', 'ESE']
-  # Un S1 pour les redoublants
-  S1-red:
-    idx: 1
-    date_debut: 2022-09-02
-    date_fin: 2023-01-12
-
-Etudiants:
-  geii8:
-    prenom: etugeii8
-    civilite: M
-    formsemestres:
-      S1:
-        notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
-          "S1.1": 7.00
-          "S1.2": 9.00
-        attendu: # les codes jury que l'on doit vérifier
-          deca:
-            passage_de_droit: False
-            nb_competences: 2
-            nb_rcue_annee: 0
-            decisions_ues:
-              "UE11":
-                codes: [ "AJ", "..." ]
-                code_valide: AJ
-                decision_jury: AJ
-                moy_ue: 7.00
-              "UE12":
-                codes: [ "AJ", "..." ]
-                code_valide: AJ
-                decision_jury: AJ
-                moy_ue: 9.00
-      S2:
-        notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
-          "S2.1": 12.00
-          "S2.2": 12.00
-        attendu: # les codes jury que l'on doit vérifier
-          deca:
-            passage_de_droit: False
-            nb_competences: 2
-            nb_rcue_annee: 2
-            valide_moitie_rcue: False
-            codes: [ "RED", "..." ]
-            decisions_ues:
-              "UE21":
-                codes: [ "ADM", "..." ]
-                code_valide: ADM
-                decision_jury: ADM
-                moy_ue: 12.00
-              "UE22":
-                codes: [ "ADM", "..." ]
-                code_valide: ADM
-                decision_jury: ADM
-                moy_ue: 12.00
-            decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
-              "UE11":
-                code_valide: AJ
-                decision_jury: AJ
-                rcue:
-                  moy_rcue: 9.50
-                  est_compensable: False
-              "UE12":
-                code_valide: CMP
-                decision_jury: CMP
-                rcue:
-                  moy_rcue: 10.50
-                  est_compensable: True
-          decision_annee: RED
-      S1-red:
-        notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
-          "S1.1": 9.50
-          "S1.2": 7.00
-        attendu: # les codes jury que l'on doit vérifier
-          deca:
-            passage_de_droit: False
-            nb_competences: 2
-            nb_rcue_annee: 2
-            decisions_ues:
-              "UE11":
-                codes: [ "CMP", "..." ]
-                code_valide: CMP
-                decision_jury: CMP
-                moy_ue: 9.50
-              "UE12":
-                codes: [ "AJ", "..." ]
-                code_valide: AJ
-                decision_jury: AJ
-                moy_ue: 7.00
-            decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
-              "UE11":
-                code_valide: CMP
-                decision_jury: CMP
-                rcue:
-                  moy_rcue: 10.75
-                  est_compensable: True
-              "UE12":
-                code_valide: CMP # car validé en fin de S2
-                rcue:
-                  moy_rcue: 9.50 # la moyenne courante (et non enregistrée), donc pas 10.5
-                  est_compensable: False
-          decision_annee: ADM
-  
\ No newline at end of file
+# Tests unitaires jury BUT - IUT Lyon GEII
+# Essais avec un BUT GEII, 2 UE en BUT1 / 4 UE en BUT2-BUT3 et 3 parcours
+# Contrib Pascal B.
+
+ReferentielCompetences:
+  filename: but-GEII-05012022-081639.xml
+  specialite: GEII
+
+Formation:
+  filename: scodoc_formation_BUT_GEII_lyon_v1.xml
+  # Association des UE aux compétences:
+  ues:
+    # S1 : Tronc commun GEII
+    'UE11':
+      annee: BUT1
+      competence: Concevoir
+    'UE12':
+      annee: BUT1
+      competence: Vérifier
+
+    # S2 : Tronc commun GEII
+    'UE21':
+      annee: BUT1
+      competence: Concevoir
+    'UE22':
+      annee: BUT1
+      competence: Vérifier
+
+    # S3 : Tronc commun GEII
+    'UE31':
+      annee: BUT2
+      competence: Concevoir
+    'UE32':
+      annee: BUT2
+      competence: Vérifier
+    'UE33':
+      annee: BUT2
+      competence: Maintenir
+    # S3 : Parcours EME
+    'UE34EME':
+      annee: BUT2
+      competence: Installer
+      parcours: EME
+    # S3 : Parcours ESE
+    'UE34ESE':
+      annee: BUT2
+      competence: Implanter
+      parcours: ESE
+    # S3 : Parcours AII
+    'UE34AII':
+      annee: BUT2
+      competence: Intégrer
+      parcours: AII
+
+    # S4 : Tronc commun GEII
+    'UE41':
+      annee: BUT2
+      competence: Concevoir
+    'UE42':
+      annee: BUT2
+      competence: Vérifier
+    'UE43':
+      annee: BUT2
+      competence: Maintenir
+    # S4 : Parcours EME
+    'UE44EME':
+      annee: BUT2
+      competence: Installer
+      parcours: EME
+    # S4 : Parcours ESE
+    'UE44ESE':
+      annee: BUT2
+      competence: Implanter
+      parcours: ESE
+    # S4 : Parcours AII
+    'UE44AII':
+      annee: BUT2
+      competence: Intégrer
+      parcours: AII
+
+  modules_parcours:
+    # cette section permet d'associer des modules à des parcours
+    # les codes modules peuvent être des regexp
+    EME: [ .*EME.* ]
+    ESE: [ .*ESE.* ]
+    AII: [ .*AII.* ]
+
+FormSemestres:
+  # S1 et S2 :
+  S1:
+    idx: 1
+    date_debut: 2021-09-01
+    date_fin: 2022-01-15
+  S2:
+    idx: 2
+    date_debut: 2022-01-16
+    date_fin: 2022-06-30
+  # S3 avec les trois parcours réunis:
+  # S3:
+  #   idx: 3
+  #   date_debut: 2022-09-01
+  #   date_fin: 2023-01-13
+  #   codes_parcours: ['AII', 'EME', 'ESE']
+  # Un S1 pour les redoublants
+  S1-red:
+    idx: 1
+    date_debut: 2022-09-02
+    date_fin: 2023-01-12
+
+Etudiants:
+  geii8:
+    prenom: etugeii8
+    civilite: M
+    formsemestres:
+      S1:
+        notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
+          "S1.1": 7.00
+          "S1.2": 9.00
+        attendu: # les codes jury que l'on doit vérifier
+          deca:
+            passage_de_droit: False
+            nb_competences: 2
+            nb_rcue_annee: 0
+            decisions_ues:
+              "UE11":
+                codes: [ "AJ", "..." ]
+                code_valide: AJ
+                decision_jury: AJ
+                moy_ue: 7.00
+              "UE12":
+                codes: [ "AJ", "..." ]
+                code_valide: AJ
+                decision_jury: AJ
+                moy_ue: 9.00
+      S2:
+        notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
+          "S2.1": 12.00
+          "S2.2": 12.00
+        attendu: # les codes jury que l'on doit vérifier
+          deca:
+            passage_de_droit: False
+            nb_competences: 2
+            nb_rcue_annee: 2
+            valide_moitie_rcue: False
+            codes: [ "RED", "..." ]
+            decisions_ues:
+              "UE21":
+                codes: [ "ADM", "..." ]
+                code_valide: ADM
+                decision_jury: ADM
+                moy_ue: 12.00
+              "UE22":
+                codes: [ "ADM", "..." ]
+                code_valide: ADM
+                decision_jury: ADM
+                moy_ue: 12.00
+            decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
+              "UE11":
+                code_valide: AJ
+                decision_jury: AJ
+                rcue:
+                  moy_rcue: 9.50
+                  est_compensable: False
+              "UE12":
+                code_valide: CMP
+                decision_jury: CMP
+                rcue:
+                  moy_rcue: 10.50
+                  est_compensable: True
+          decision_annee: RED
+      S1-red:
+        notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
+          "S1.1": 9.50
+          "S1.2": 7.00
+        attendu: # les codes jury que l'on doit vérifier
+          deca:
+            passage_de_droit: False
+            nb_competences: 2
+            nb_rcue_annee: 2
+            decisions_ues:
+              "UE11":
+                codes: [ "CMP", "..." ]
+                code_valide: CMP
+                decision_jury: CMP
+                moy_ue: 9.50
+              "UE12":
+                codes: [ "AJ", "..." ]
+                code_valide: AJ
+                decision_jury: AJ
+                moy_ue: 7.00
+            decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
+              "UE11":
+                code_valide: CMP
+                decision_jury: CMP
+                rcue:
+                  moy_rcue: 10.75
+                  est_compensable: True
+              "UE12":
+                code_valide: CMP # car validé en fin de S2
+                rcue:
+                  moy_rcue: 9.50 # la moyenne courante (et non enregistrée), donc pas 10.5
+                  est_compensable: False
+          decision_annee: ADM
+  geii43:
+    prenom: etugeii43
+    civilite: M
+    formsemestres:
+      S1:
+        notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
+          "S1.1": 9.00
+          "S1.2": 9.00
+        attendu: # les codes jury que l'on doit vérifier
+          deca:
+            passage_de_droit: False
+            nb_competences: 2
+            nb_rcue_annee: 0
+            decisions_ues:
+              "UE11":
+                codes: [ "AJ", "..." ]
+                code_valide: AJ
+                decision_jury: AJ
+                moy_ue: 9.00
+              "UE12":
+                codes: [ "AJ", "..." ]
+                code_valide: AJ
+                decision_jury: AJ
+                moy_ue: 9.00
+      S2:
+        notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
+          "S2.1": 9.00
+          "S2.2": 9.00
+        attendu: # les codes jury que l'on doit vérifier
+          deca:
+            passage_de_droit: False
+            nb_competences: 2
+            nb_rcue_annee: 2
+            valide_moitie_rcue: False
+            codes: [ "RED", "..." ]
+            decisions_ues:
+              "UE21":
+                codes: [ "AJ", "..." ]
+                code_valide: AJ
+                moy_ue: 9.00
+              "UE22":
+                codes: [ "AJ", "..." ]
+                code_valide: AJ # va basculer en ADJ car RCUE en ADJ (mais le test est AVANT !)
+                moy_ue: 9.00
+            decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
+              "UE11":
+                code_valide: AJ
+                decision_jury: AJ
+                rcue:
+                  moy_rcue: 9.00
+                  est_compensable: False
+              "UE12":
+                code_valide: AJ # code par défaut proposé
+                decision_jury: ADJ # code donné par le jury de S2
+                rcue:
+                  moy_rcue: 9.00
+                  est_compensable: False
+          decision_annee: RED
+      S1-red:
+        notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
+          "S1.1": 11.00
+          "S1.2": 7.00
+        attendu: # les codes jury que l'on doit vérifier
+          deca:
+            passage_de_droit: false
+            nb_competences: 2
+            nb_rcue_annee: 0
+            decisions_ues:
+              "UE11":
+                codes: [ "ADM", "..." ]
+                code_valide: ADM
+                moy_ue: 11.00
+              "UE12":
+                code_valide: AJ
+                moy_ue: 7.00
+          decision_annee: AJ
diff --git a/tests/unit/sco_fake_gen.py b/tests/unit/sco_fake_gen.py
index 0c07a27eebe77735b55030563acc2a70669093c8..e70c434c1e5ba1d23f0e9e8e2f39e09ee7a7a90e 100644
--- a/tests/unit/sco_fake_gen.py
+++ b/tests/unit/sco_fake_gen.py
@@ -232,7 +232,7 @@ class ScoFake(object):
         self,
         formation_id=None,
         semestre_id=None,
-        titre=None,
+        titre="",
         date_debut=None,
         date_fin=None,
         etat=None,
@@ -253,6 +253,7 @@ class ScoFake(object):
     ) -> int:
         if responsables is None:
             responsables = (self.default_user.id,)
+        titre = titre or "sans titre"
         oid = sco_formsemestre.do_formsemestre_create(locals())
         oids = sco_formsemestre.do_formsemestre_list(
             args={"formsemestre_id": oid}
diff --git a/tests/unit/test_but_jury.py b/tests/unit/test_but_jury.py
index fa75ae329116a74fe00bed44a508ab58881b618a..29cf718265899702dbb011f865a98258ff317b07 100644
--- a/tests/unit/test_but_jury.py
+++ b/tests/unit/test_but_jury.py
@@ -1,19 +1,29 @@
+##############################################################################
+# ScoDoc
+# Copyright (c) 1999 - 2023 Emmanuel Viennet.  All rights reserved.
+# See LICENSE
+##############################################################################
+
 """ Test jury BUT avec parcours
+
+Ces tests sont généralement lents (construction de la base),
+et donc marqués par `@pytest.mark.slow`.
+
+Certains sont aussi marqués par @pytest.mark.lemans ou @pytest.mark.lyon
+pour lancer certains tests spécifiques seulement.
+
+Exemple utilisation spécifique:
+# test sur "Lyon" seulement:
+pytest --pdb -m lyon tests/unit/test_but_jury.py
+
 """
 
 import pytest
 from tests.unit import yaml_setup
 
 import app
-from app.but.jury_but import DecisionsProposeesAnnee
 from app.but.jury_but_validation_auto import formsemestre_validation_auto_but
-from app.models import (
-    Formation,
-    FormSemestre,
-    Identite,
-    UniteEns,
-)
-from app.scodoc import sco_utils as scu
+from app.models import FormSemestre
 from config import TestConfig
 
 DEPT = TestConfig.DEPT_TEST
diff --git a/tests/unit/yaml_setup.py b/tests/unit/yaml_setup.py
index 089b146197e8ad6b774f91ec32cf2fcdd93e77ae..de495dcccec9dcd4c35b492fc74a350821189b97 100644
--- a/tests/unit/yaml_setup.py
+++ b/tests/unit/yaml_setup.py
@@ -34,7 +34,7 @@ formsemestre_validation_auto_but(only_adm=False)
 
 test_but_jury()
     - compare décisions attendues indiquées dans le YAML avec celles de ScoDoc
-     et enregistre immédiatement après la décision manuelle indiquée par `decision_jury`
+     et enregistre immédiatement APRES la décision manuelle indiquée par `decision_jury`
      dans le YAML.