diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py
index 89a7bd669e41ec7e95c4bee8bb972a5419e6c592..d8ca17e154f62c2d40e80f3db132b7cff6fcc11d 100644
--- a/app/but/cursus_but.py
+++ b/app/but/cursus_but.py
@@ -14,6 +14,7 @@ Classe raccordant avec ScoDoc 7:
 
 """
 import collections
+from operator import attrgetter
 from typing import Union
 
 from flask import g, url_for
@@ -23,8 +24,6 @@ from app import log
 from app.comp.res_but import ResultatsSemestreBUT
 from app.comp.res_compat import NotesTableCompat
 
-from app.comp import res_sem
-
 from app.models.but_refcomp import (
     ApcAnneeParcours,
     ApcCompetence,
@@ -45,7 +44,7 @@ from app.models.formsemestre import FormSemestre, FormSemestreInscription
 from app.models.ues import UniteEns
 from app.models.validations import ScolarFormSemestreValidation
 from app.scodoc import codes_cursus as sco_codes
-from app.scodoc.codes_cursus import RED, UE_STANDARD
+from app.scodoc.codes_cursus import code_ue_validant, RED, UE_STANDARD
 from app.scodoc import sco_utils as scu
 from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
 
@@ -360,6 +359,40 @@ class FormSemestreCursusBUT:
         "cache { competence_id : competence }"
 
 
+def etud_ues_de_but1_non_validees(
+    etud: Identite, formation: Formation, parcour: ApcParcours
+) -> list[UniteEns]:
+    """Vrai si cet étudiant a validé toutes ses UEs de S1 et S2, dans son parcours"""
+    # Les UEs avec décisions, dans les S1 ou S2 d'une formation de même code:
+    validations = (
+        ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
+        .filter(ScolarFormSemestreValidation.ue_id != None)
+        .join(UniteEns)
+        .filter(db.or_(UniteEns.semestre_idx == 1, UniteEns.semestre_idx == 2))
+        .join(Formation)
+        .filter_by(formation_code=formation.formation_code)
+    )
+    codes_validations_by_ue = collections.defaultdict(list)
+    for v in validations:
+        codes_validations_by_ue[v.ue_id].append(v.code)
+
+    # Les UEs du parcours en S1 et S2:
+    ues = formation.query_ues_parcour(parcour).filter(
+        db.or_(UniteEns.semestre_idx == 1, UniteEns.semestre_idx == 2)
+    )
+    # Liste triée des ues non validées
+    return sorted(
+        [
+            ue
+            for ue in ues
+            if any(
+                (not code_ue_validant(code) for code in codes_validations_by_ue[ue.id])
+            )
+        ],
+        key=attrgetter("numero", "acronyme"),
+    )
+
+
 def formsemestre_warning_apc_setup(
     formsemestre: FormSemestre, res: ResultatsSemestreBUT
 ) -> str:
diff --git a/app/but/jury_but.py b/app/but/jury_but.py
index 227ead81c41752d2b516f0e06566420623c8d972..c1ded7b8837799c8126d8a5b39ae2a1d959fc85d 100644
--- a/app/but/jury_but.py
+++ b/app/but/jury_but.py
@@ -69,6 +69,7 @@ from flask import flash, g, url_for
 
 from app import db
 from app import log
+from app.but import cursus_but
 from app.but.cursus_but import EtudCursusBUT
 from app.comp.res_but import ResultatsSemestreBUT
 from app.comp import res_sem
@@ -363,15 +364,33 @@ class DecisionsProposeesAnnee(DecisionsProposees):
         "Vrai si plus de la moitié des RCUE validables"
         self.passage_de_droit = self.valide_moitie_rcue and (self.nb_rcues_under_8 == 0)
         "Vrai si peut passer dans l'année BUT suivante: plus de la moitié validables et tous > 8"
-        # XXX TODO ajouter condition pour passage en S5
+        explanation = ""
+        # 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, formation, inscription.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:
+                # pas inscrit dans le semestre courant ???
+                self.passage_de_droit = False
 
-        # Enfin calcule les codes des UE:
+        # Enfin calcule les codes des UEs:
         for dec_ue in self.decisions_ues.values():
             dec_ue.compute_codes()
 
         # Reste à attribuer ADM, ADJ, PASD, PAS1NCI, RED, NAR
         plural = self.nb_validables > 1
-        expl_rcues = f"""{self.nb_validables} niveau{"x" if plural else ""} validable{
+        explanation += f"""{self.nb_validables} niveau{"x" if plural else ""} validable{
                 "s" if plural else ""} sur {self.nb_competences}"""
         if self.admis:
             self.codes = [sco_codes.ADM] + self.codes
@@ -390,7 +409,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
                 sco_codes.ABL,
                 sco_codes.EXCLU,
             ]
-            expl_rcues = ""
+            explanation = ""
         elif self.passage_de_droit:
             self.codes = [sco_codes.PASD, sco_codes.ADJ] + self.codes
         elif self.valide_moitie_rcue:  # mais au moins 1 rcue insuffisante
@@ -400,7 +419,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
                 sco_codes.PAS1NCI,
                 sco_codes.ADJ,
             ] + self.codes
-            expl_rcues += f" et {self.nb_rcues_under_8} < 8"
+            explanation += f" et {self.nb_rcues_under_8} < 8"
         else:
             self.codes = [
                 sco_codes.RED,
@@ -409,7 +428,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
                 sco_codes.ADJ,
                 sco_codes.PASD,  # voir #488 (discutable, conventions locales)
             ] + self.codes
-            expl_rcues += f""" et {self.nb_rcues_under_8} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
+            explanation += f""" et {self.nb_rcues_under_8} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
 
         # Si l'un des semestres est extérieur, propose ADM
         if (
@@ -419,7 +438,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
         # Si validée par niveau supérieur:
         if self.code_valide == sco_codes.ADSUP:
             self.codes.insert(0, sco_codes.ADSUP)
-        self.explanation = f"<div>{expl_rcues}</div>"
+        self.explanation = f"<div>{explanation}</div>"
         messages = self.descr_pb_coherence()
         if messages:
             self.explanation += (
@@ -1261,6 +1280,8 @@ class DecisionsProposeesRCUE(DecisionsProposees):
             validation_rcue = validations_rcue[0]
             validation_rcue.code = sco_codes.ADSUP
             validation_rcue.date = datetime.now()
+            db.session.add(validation_rcue)
+            db.session.commit()
             log(f"updating {validation_rcue}")
             if validation_rcue.formsemestre_id is not None:
                 sco_cache.invalidate_formsemestre(
@@ -1271,9 +1292,9 @@ class DecisionsProposeesRCUE(DecisionsProposees):
             validation_rcue = ApcValidationRCUE(
                 etudid=self.etud.id, ue1_id=ue1.id, ue2_id=ue2.id, code=sco_codes.ADSUP
             )
+            db.session.add(validation_rcue)
+            db.session.commit()
             log(f"recording {validation_rcue}")
-        db.session.add(validation_rcue)
-        db.session.commit()
         self.valide_annee_inferieure()
 
     def valide_annee_inferieure(self) -> None:
diff --git a/app/models/validations.py b/app/models/validations.py
index cc55651878284abb9e5c3441f170a58a47f5115a..229d15ad5425b53d88226bb592b182ab43a36613 100644
--- a/app/models/validations.py
+++ b/app/models/validations.py
@@ -11,7 +11,7 @@ from app.models.events import Scolog
 
 
 class ScolarFormSemestreValidation(db.Model):
-    """Décisions de jury"""
+    """Décisions de jury (sur semestre ou UEs)"""
 
     __tablename__ = "scolar_formsemestre_validation"
     # Assure unicité de la décision:
diff --git a/tests/ressources/yaml/cursus_but_gccd_cy.yaml b/tests/ressources/yaml/cursus_but_gccd_cy.yaml
index 668951d2ca47aa40ab2e7f2ba4510805fcbc85f3..8852a3643667108f80372bc8716f56c23c5d51e5 100644
--- a/tests/ressources/yaml/cursus_but_gccd_cy.yaml
+++ b/tests/ressources/yaml/cursus_but_gccd_cy.yaml
@@ -255,3 +255,26 @@ Etudiants:
         parcours: TP
         notes_modules:
           "R4.01": 14 # toutes UE
+
+  D: # Etudiant arrive en S4 avec une UE manquante en S1
+    prenom: Étudiant_TP_malaise
+    civilite: M
+    formsemestres:
+      S1:
+        parcours: TP
+        notes_modules:
+          "R1.01": 11 # toutes UEs
+          "SAÉ 1-2": 8 # plombe l'UE 2
+      S2:
+        parcours: TP
+        notes_modules:
+          "R2.01": 11 # toutes UEs
+      S3:
+        parcours: TP
+        notes_modules:
+          "R3.01": 12 # toutes UEs
+      S4:
+        parcours: TP
+        notes_modules:
+          "R4.01": 14 # toutes UE
+          "R4.04": 6 # plombe l'UE1
diff --git a/tests/unit/yaml_setup.py b/tests/unit/yaml_setup.py
index f24b92b79a58b64636cc9326e8db1d11b523d753..35b016a2f1a0d35208344e588e52bbbc82786221 100644
--- a/tests/unit/yaml_setup.py
+++ b/tests/unit/yaml_setup.py
@@ -146,7 +146,7 @@ def create_formsemestre(
     return formsemestre
 
 
-def create_evaluations(formsemestre: FormSemestre):
+def create_evaluations(formsemestre: FormSemestre, publish_incomplete=True):
     """Crée une évaluation dans chaque module du semestre"""
     for modimpl in formsemestre.modimpls:
         evaluation = Evaluation(
@@ -156,6 +156,7 @@ def create_evaluations(formsemestre: FormSemestre):
             coefficient=1.0,
             note_max=20.0,
             numero=1,
+            publish_incomplete=publish_incomplete,
         )
         db.session.add(evaluation)