diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py
index 622cdbb002453b1ef5940905551906d0dc0ee093..97a555ca60151439b9a1e93b9fb644fa508d4259 100644
--- a/app/but/cursus_but.py
+++ b/app/but/cursus_but.py
@@ -102,7 +102,7 @@ class EtudCursusBUT:
         "Liste des inscriptions aux sem. de la formation, triées par indice et chronologie"
         self.parcour: ApcParcours = self.inscriptions[-1].parcour
         "Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
-        self.niveaux_by_annee = {}
+        self.niveaux_by_annee: dict[int, list[ApcNiveau]] = {}
         "{ annee:int : liste des niveaux à valider }"
         self.niveaux: dict[int, ApcNiveau] = {}
         "cache les niveaux"
diff --git a/app/but/jury_but.py b/app/but/jury_but.py
index 05978de2d817bc9e58c3929c85e832443f4d3d39..954481ad09959a1bb9598fbb67425b219913cbf1 100644
--- a/app/but/jury_but.py
+++ b/app/but/jury_but.py
@@ -342,21 +342,11 @@ class DecisionsProposeesAnnee(DecisionsProposees):
         # Cas particulier du passage en BUT 3: nécessité d'avoir validé toutes les UEs du BUT 1.
         if self.passage_de_droit and self.annee_but == 2:
             inscription = formsemestre.etuds_inscriptions.get(etud.id)
-            if inscription:
-                ues_but1_non_validees = cursus_but.etud_ues_de_but1_non_validees(
-                    etud, self.formsemestre.formation, self.parcour
-                )
-                self.passage_de_droit = not ues_but1_non_validees
-                explanation += (
-                    f"""UEs de BUT1 non validées: <b>{
-                    ', '.join(ue.acronyme for ue in ues_but1_non_validees)
-                    }</b>. """
-                    if ues_but1_non_validees
-                    else ""
-                )
-            else:
+            if not inscription or inscription.etat != scu.INSCRIT:
                 # pas inscrit dans le semestre courant ???
                 self.passage_de_droit = False
+            else:
+                self.passage_de_droit, explanation = self.passage_de_droit_en_but3()
 
         # Enfin calcule les codes des UEs:
         for dec_ue in self.decisions_ues.values():
@@ -423,6 +413,53 @@ class DecisionsProposeesAnnee(DecisionsProposees):
             )
         self.codes = [self.codes[0]] + sorted(self.codes[1:])
 
+    def passage_de_droit_en_but3(self) -> tuple[bool, str]:
+        """Vérifie si les conditions supplémentaires de passage BUT2 vers BUT3 sont satisfaites"""
+        cursus: EtudCursusBUT = EtudCursusBUT(self.etud, self.formsemestre.formation)
+        niveaux_but1 = cursus.niveaux_by_annee[1]
+
+        niveaux_but1_non_valides = []
+        for niveau in niveaux_but1:
+            ok = False
+            validation_par_annee = cursus.validation_par_competence_et_annee[
+                niveau.competence_id
+            ]
+            if validation_par_annee:
+                validation_niveau = validation_par_annee.get("BUT1")
+                if validation_niveau and validation_niveau.code in CODES_RCUE_VALIDES:
+                    ok = True
+            if not ok:
+                niveaux_but1_non_valides.append(niveau)
+
+        # Les niveaux de BUT1 manquants passent-ils en ADSUP ?
+        # en vertu de l'article 4.3,
+        # "La validation des deux UE du niveau d’une compétence emporte la validation de
+        # l’ensemble des UE du niveau inférieur de cette même compétence."
+        explanation = ""
+        ok = True
+        for niveau_but1 in niveaux_but1_non_valides:
+            niveau_but2 = niveau_but1.competence.niveaux.filter_by(annee="BUT2").first()
+            if niveau_but2:
+                rcue = self.rcue_by_niveau.get(niveau_but2.id)
+                if (rcue is None) or (
+                    not rcue.est_validable() and not rcue.code_valide()
+                ):
+                    # le RCUE de BUT2 n'est ni validable (avec les notes en cours) ni déjà validé
+                    ok = False
+                    explanation += (
+                        f"Compétence {niveau_but1} de BUT 1 non validée.<br> "
+                    )
+                else:
+                    explanation += (
+                        f"Compétence {niveau_but1} de BUT 1 validée par ce BUT2.<br> "
+                    )
+            else:
+                ok = False
+                explanation += f"""Compétence {
+                    niveau_but1} de BUT 1 non validée et non existante en BUT2.<br> """
+
+        return ok, explanation
+
     # WIP TODO XXX def get_moyenne_annuelle(self)
 
     def infos(self) -> str:
@@ -1235,13 +1272,16 @@ class DecisionsProposeesRCUE(DecisionsProposees):
         self, semestre_id: int, ordre_inferieur: int, competence: ApcCompetence
     ):
         """Au besoin, enregistre une validation d'UE ADSUP pour le niveau de compétence
-        semestre_id : l'indice du semestre concerné (le pair ou l'impair)
+        semestre_id : l'indice du semestre concerné (le pair ou l'impair du niveau courant)
         """
-        # Les validations d'UE impaires existantes pour ce niveau inférieur ?
+        semestre_id_inferieur = semestre_id - 2
+        if semestre_id_inferieur < 1:
+            return
+        # Les validations d'UE existantes pour ce niveau inférieur ?
         validations_ues: list[ScolarFormSemestreValidation] = (
             ScolarFormSemestreValidation.query.filter_by(etudid=self.etud.id)
             .join(UniteEns)
-            .filter_by(semestre_idx=semestre_id)
+            .filter_by(semestre_idx=semestre_id_inferieur)
             .join(ApcNiveau)
             .filter_by(ordre=ordre_inferieur)
             .join(ApcCompetence)
@@ -1256,13 +1296,14 @@ class DecisionsProposeesRCUE(DecisionsProposees):
             # Il faut créer une validation d'UE
             # cherche l'UE de notre formation associée à ce niveau
             # et warning si il n'y en a pas
-            ue = self._get_ue_inferieure(semestre_id, ordre_inferieur, competence)
+            ue = self._get_ue_inferieure(
+                semestre_id_inferieur, ordre_inferieur, competence
+            )
             if not ue:
                 # programme incomplet ou mal paramétré
                 flash(
-                    f"""Impossible de valider l'UE inférieure du niveau {
-                      ordre_inferieur
-                    } de la compétence {competence.titre}
+                    f"""Impossible de valider l'UE inférieure de la compétence {
+                        competence.titre} (niveau {ordre_inferieur})
                     car elle n'existe pas dans la formation
                     """,
                     "warning",
diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py
index c4983bdd229925d76b21eed7baa791e73e6ed81b..b33e2f2bb08f7666c5d2f340afe22180961dd82b 100644
--- a/app/models/but_refcomp.py
+++ b/app/models/but_refcomp.py
@@ -364,6 +364,9 @@ class ApcNiveau(db.Model, XMLModel):
         return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={
             self.annee!r} {self.competence!r}>"""
 
+    def __str__(self):
+        return f"""{self.competence.titre} niveau {self.ordre}"""
+
     def to_dict(self, with_app_critiques=True):
         "as a dict, recursif (ou non) sur les AC"
         return {
diff --git a/app/templates/jury/erase_decisions_annee_formation.j2 b/app/templates/jury/erase_decisions_annee_formation.j2
index fc8fd0ce0217549f380d4f8202d51c3930337410..5f81e69ce583774da4a1809d0e709212f74fa443 100644
--- a/app/templates/jury/erase_decisions_annee_formation.j2
+++ b/app/templates/jury/erase_decisions_annee_formation.j2
@@ -35,6 +35,8 @@ quelle que soit leur origine.</p>
     </form>
 </div>
 
+{% endif %}
+
 <div class="sco_box">
 <div class="sco_box_title">Autres actions:</div>
 <ul>
@@ -60,8 +62,6 @@ quelle que soit leur origine.</p>
 </ul>
 </div>
 
-{% endif %}
-
 
 
 {% endblock %}
\ No newline at end of file
diff --git a/tests/ressources/yaml/cursus_but_geii_lyon.yaml b/tests/ressources/yaml/cursus_but_geii_lyon.yaml
index 701585d61f8203a24552323cabd9b661ddd6d32d..fefdc9b6bd3bd4a31acbd8c450affab984cd82c3 100644
--- a/tests/ressources/yaml/cursus_but_geii_lyon.yaml
+++ b/tests/ressources/yaml/cursus_but_geii_lyon.yaml
@@ -109,6 +109,16 @@ FormSemestres:
     idx: 1
     date_debut: 2022-09-02
     date_fin: 2023-01-12
+  S3:
+    idx: 3
+    codes_parcours: ['AII']
+    date_debut: 2022-09-01
+    date_fin: 2023-01-15
+  S4:
+    idx: 4
+    codes_parcours: ['AII']
+    date_debut: 2023-01-16
+    date_fin: 2023-07-10
 
 Etudiants:
   geii8:
@@ -1326,3 +1336,74 @@ Etudiants:
                   # moy_rcue: 13.5000	#  Pas de moyenne calculée
                   est_compensable: False
           decision_annee: ATJ # Passage tout de même en S3
+  #
+  # -----------------------  geii90 : ADSUP envoyés par BUT2 vers BUT1
+  #
+  geii90:
+    prenom: etugeii90
+    civilite: M
+    code_nip: geii90
+    formsemestres:
+      S1: # 2 UEs, les deux en AJ
+        notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
+          "S1.1": 9.5000
+          "S1.2": 8.5000
+        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", "..." ]
+              "UE12":
+                codes: [ "AJ", "..." ]
+      S2: # pareil, mais le jury le fait passer en S3
+        notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
+          "S2.1": 9.8000
+          "S2.2": 9.9000
+        attendu: # les codes jury que l'on doit vérifier
+          deca:
+            passage_de_droit: False # d'apres les notes, on ne peut pas passer
+            autorisations_inscription: [2] # et le jury manuel nous fait passer
+            nb_competences: 2
+            nb_rcue_annee: 2
+            valide_moitie_rcue: False
+            codes: [ "ADJ", "ATJ", "RED", "..." ]
+            code_valide: RED # le code proposé en auto
+            decisions_ues:
+              "UE21":
+                codes: [ "AJ", "..." ]
+                code_valide: AJ
+                moy_ue: 9.8
+              "UE22":
+                code_valide: AJ
+                moy_ue: 9.9
+            decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
+              "UE11":
+                code_valide: AJ # le code proposé en auto
+                rcue:
+                  # moy_rcue: 14.0000  #  Pas de moyenne calculée
+                  est_compensable: False
+              "UE12":
+                code_valide: AJ # le code proposé en auto
+                rcue:
+                  # moy_rcue: 13.5000	#  Pas de moyenne calculée
+                  est_compensable: False
+          decision_annee: ADJ # Passage tout de même en S3 !
+      S3: # le S3 avec 4 niveaux
+        parcours: AII
+        notes_modules: # combinaison pour avoir ADM AJ AJ AJ
+            "AII3": 9
+            "ER3": 10.75
+            "AU3": 8
+        attendu: # les codes jury que l'on doit vérifier
+          deca:
+            passage_de_droit: False # d'apres les notes, on ne peut pas passer
+            autorisations_inscription: [4] # passe en S4
+            nb_competences: 4
+      S4: # le S4 avec 4 niveaux
+        parcours: AII
+        notes_modules: # combinaison pour avoir ADM ADM ADM AJ
+          "PF4": 12
+          "SAE4AII": 8