diff --git a/app/api/formations.py b/app/api/formations.py
index def1c12e7ef7e6f0006dd50e247b16597874018f..815233d3e422558b0139423ca402fc790b9552a0 100644
--- a/app/api/formations.py
+++ b/app/api/formations.py
@@ -209,26 +209,40 @@ def ue_set_parcours(ue_id: int):
     return {"status": ok, "message": error_message}
 
 
+@bp.route(
+    "/formation/ue/<int:ue_id>/assoc_niveau/<int:niveau_id>/force",
+    defaults={"force": True},
+    methods=["POST"],
+)
+@api_web_bp.route(
+    "/formation/ue/<int:ue_id>/assoc_niveau/<int:niveau_id>/force",
+    defaults={"force": True},
+    methods=["POST"],
+)
 @bp.route(
     "/formation/ue/<int:ue_id>/assoc_niveau/<int:niveau_id>",
+    defaults={"force": False},
     methods=["POST"],
 )
 @api_web_bp.route(
     "/formation/ue/<int:ue_id>/assoc_niveau/<int:niveau_id>",
+    defaults={"force": False},
     methods=["POST"],
 )
 @login_required
 @scodoc
 @permission_required(Permission.EditFormation)
 @as_json
-def ue_assoc_niveau(ue_id: int, niveau_id: int):
-    """Associe l'UE au niveau de compétence."""
+def ue_assoc_niveau(ue_id: int, niveau_id: int, force=False):
+    """Associe l'UE au niveau de compétence.
+    Si force, modifie l'association même si des décisions de jury sont présentes.
+    """
     query = UniteEns.query.filter_by(id=ue_id)
     if g.scodoc_dept:
         query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
     ue: UniteEns = query.first_or_404()
     niveau: ApcNiveau = ApcNiveau.get_or_404(niveau_id)
-    ok, error_message = ue.set_niveau_competence(niveau)
+    ok, error_message = ue.set_niveau_competence(niveau, force=force)
     if not ok:
         if g.scodoc_dept:  # "usage web"
             flash(error_message, "error")
@@ -238,19 +252,31 @@ def ue_assoc_niveau(ue_id: int, niveau_id: int):
     return {"status": 0}
 
 
+@bp.route(
+    "/formation/ue/<int:ue_id>/desassoc_niveau/force",
+    defaults={"force": True},
+    methods=["POST"],
+)
+@api_web_bp.route(
+    "/formation/ue/<int:ue_id>/desassoc_niveau/force",
+    defaults={"force": True},
+    methods=["POST"],
+)
 @bp.route(
     "/formation/ue/<int:ue_id>/desassoc_niveau",
+    defaults={"force": False},
     methods=["POST"],
 )
 @api_web_bp.route(
     "/formation/ue/<int:ue_id>/desassoc_niveau",
+    defaults={"force": False},
     methods=["POST"],
 )
 @login_required
 @scodoc
 @permission_required(Permission.EditFormation)
 @as_json
-def ue_desassoc_niveau(ue_id: int):
+def ue_desassoc_niveau(ue_id: int, force=False):
     """Désassocie cette UE de son niveau de compétence
     (si elle n'est pas associée, ne fait rien).
     """
@@ -258,7 +284,7 @@ def ue_desassoc_niveau(ue_id: int):
     if g.scodoc_dept:
         query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
     ue: UniteEns = query.first_or_404()
-    ok, error_message = ue.set_niveau_competence(None)
+    ok, error_message = ue.set_niveau_competence(None, force=force)
     if not ok:
         if g.scodoc_dept:  # "usage web"
             flash(error_message, "error")
diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py
index 3785f694bc86cf0472ec868614bece654b4ffe88..a31f34003f8aa9dffcdacb7c84f6123217d14f3c 100644
--- a/app/but/cursus_but.py
+++ b/app/but/cursus_but.py
@@ -96,11 +96,13 @@ def but_parcours_validated(etud: Identite, parcour_id: int | None) -> bool:
 class EtudCursusBUT:
     """L'état de l'étudiant dans son cursus BUT
     Liste des niveaux validés/à valider
-    (utilisé pour le résumé sur la fiche étudiant)
+    (utilisé pour le résumé sur la fiche étudiant).
     """
 
     def __init__(self, etud: Identite, formation: Formation):
-        """formation indique la spécialité préparée"""
+        """formation indique la spécialité préparée.
+        Peut lever l'exception ScoValueError ou ScoNoReferentielCompetences
+        """
         # Vérifie que l'étudiant est bien inscrit à un sem. de cette formation
         if formation.id not in (
             ins.formsemestre.formation.id for ins in etud.formsemestre_inscriptions
@@ -142,10 +144,10 @@ class EtudCursusBUT:
             if niveau is None:
                 raise ScoValueError(
                     f"""UE d'un RCUE ({
-                        validation_rcue.ue1.acronyme}/{validation_rcue.ue1.acronyme
+                        validation_rcue.ue1.acronyme}/{validation_rcue.ue2.acronyme
                     }) non associée à un niveau de compétence.
                     Vérifiez la formation et les associations de ses UEs.
-                    Étudiant {etud.nomprenom}.
+                    Étudiant {etud.html_link_fiche()}.
                     Formations concernées: <a href="{
                         url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
                             formation_id=validation_rcue.ue1.formation_id,
@@ -156,7 +158,8 @@ class EtudCursusBUT:
                             formation_id=validation_rcue.ue2.formation_id,
                             semestre_idx=validation_rcue.ue2.semestre_idx)
                     }">{validation_rcue.ue2.acronyme}</a>.
-                    """
+                    """,
+                    safe=True,
                 )
             if not niveau.competence.id in self.validation_par_competence_et_annee:
                 self.validation_par_competence_et_annee[niveau.competence.id] = {}
@@ -625,8 +628,9 @@ def formsemestre_warning_apc_setup(
         )
         if nb_ues_sans_parcours != nb_ues_tot:
             H.append(
-                """Le semestre n'est associé à aucun parcours,
+                f"""Le semestre n'est associé à aucun parcours,
                 mais les UEs de la formation ont des parcours
+                ({nb_ues_sans_parcours} UEs sans parcours sur {nb_ues_tot} UEs au total).
                 """
             )
     # Vérifie les niveaux de chaque parcours
diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py
index a480b2109673b62109b31b6e636565388e6b8b12..7c9f33550b3d0664d0fe30ad01e7edccc718b25e 100644
--- a/app/comp/bonus_spo.py
+++ b/app/comp/bonus_spo.py
@@ -1473,7 +1473,7 @@ class BonusTarbes(BonusIUTRennes1):
     """Calcul bonus optionnels (sport, culture), règle IUT de Tarbes.
 
     <ul>
-    <li>Les étudiants opeuvent suivre un ou plusieurs activités optionnelles notées.
+    <li>Les étudiants peuvent suivre un ou plusieurs activités optionnelles notées.
     La meilleure des notes obtenue est prise en compte, si elle est supérieure à 10/20.
     </li>
     <li>Le trentième des points au dessus de 10 est ajouté à la moyenne des UE en BUT,
diff --git a/app/models/ues.py b/app/models/ues.py
index 6c13402479c4efefe3ea9f536350f3138e8d44b1..7cf25bc63238a9511339125ec69a7061087ee6c6 100644
--- a/app/models/ues.py
+++ b/app/models/ues.py
@@ -464,7 +464,9 @@ class UniteEns(models.ScoDocModel):
             > 0
         )
 
-    def set_niveau_competence(self, niveau: ApcNiveau | None) -> tuple[bool, str]:
+    def set_niveau_competence(
+        self, niveau: ApcNiveau | None, force: bool = False
+    ) -> tuple[bool, str]:
         """Associe cette UE au niveau de compétence indiqué.
         Le niveau doit être dans l'un des parcours de l'UE (si elle n'est pas
         de tronc commun).
@@ -473,7 +475,8 @@ class UniteEns(models.ScoDocModel):
 
         Si niveau est None, désassocie.
 
-        Si l'UE est utilisée dans un validation de RCUE, on ne peut plus la changer de niveau.
+        Si l'UE est utilisée dans un validation de RCUE, on ne peut plus la changer
+        de niveau, sauf si force est vrai.
 
         Returns
         - True if (de)association done, False on error.
@@ -486,7 +489,7 @@ class UniteEns(models.ScoDocModel):
                 "La formation n'est pas associée à un référentiel de compétences",
             )
         # UE utilisée dans des validations RCUE ?
-        if self.is_used_in_validation_rcue():
+        if not force and self.is_used_in_validation_rcue():
             return (
                 False,
                 "UE utilisée dans un RCUE validé: son niveau ne peut plus être modifié",
@@ -521,7 +524,7 @@ class UniteEns(models.ScoDocModel):
         db.session.commit()
         # Invalidation du cache
         self.formation.invalidate_cached_sems()
-        log(f"ue.set_niveau_competence( {self}, {niveau} )")
+        log(f"ue.set_niveau_competence( {self}, {niveau}, force={force} )")
         return True, ""
 
     def set_parcours(self, parcours: list[ApcParcours]) -> tuple[bool, str]:
diff --git a/app/scodoc/sco_archives_formsemestre.py b/app/scodoc/sco_archives_formsemestre.py
index 892e9438a5f1fc5b354dbf14770c426d27d2f029..3e4b54d2081cefc7bdbadf22e7cc618b7498e0dc 100644
--- a/app/scodoc/sco_archives_formsemestre.py
+++ b/app/scodoc/sco_archives_formsemestre.py
@@ -117,7 +117,7 @@ def do_formsemestre_archive(
             dept_id=formsemestre.dept_id,
         )
     # Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes)
-    table_html, _, _ = gen_formsemestre_recapcomplet_html_table(
+    table_html, _, _, _ = gen_formsemestre_recapcomplet_html_table(
         formsemestre, res, include_evaluations=True
     )
     if table_html:
diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py
index 0f70477f4c3abf6bd2527c0fdbec7364eb4856cd..b925716ab2c6d6d4e785f88e0841618c7e485a2d 100644
--- a/app/scodoc/sco_page_etud.py
+++ b/app/scodoc/sco_page_etud.py
@@ -58,7 +58,7 @@ from app.scodoc import (
 )
 from app.scodoc.html_sidebar import retreive_formsemestre_from_request
 from app.scodoc.sco_bulletins import etud_descr_situation_semestre
-from app.scodoc.sco_exceptions import ScoValueError
+from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
 from app.scodoc.sco_formsemestre_validation import formsemestre_recap_parcours_table
 from app.scodoc.sco_permissions import Permission
 import app.scodoc.sco_utils as scu
@@ -473,7 +473,7 @@ def fiche_etud(etudid=None):
     if last_formsemestre and last_formsemestre.formation.is_apc():
         try:
             but_cursus = cursus_but.EtudCursusBUT(etud, last_formsemestre.formation)
-        except ScoValueError:
+        except (ScoValueError, ScoNoReferentielCompetences):
             but_cursus = None
         refcomp = last_formsemestre.formation.referentiel_competence
         if refcomp:
diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py
index 55237e5e39cf0745e5a4d522516b0c7ba26569cf..8e9f93403c7f6425ee8a79dcf5bd491076fc4fa9 100644
--- a/app/scodoc/sco_recapcomplet.py
+++ b/app/scodoc/sco_recapcomplet.py
@@ -112,7 +112,7 @@ def formsemestre_recapcomplet(
             visible_col_ids=visible_col_ids,
         )
 
-    table_html, _, freq_codes_annuels = _formsemestre_recapcomplet_to_html(
+    table_html, _, freq_codes_annuels, warnings = _formsemestre_recapcomplet_to_html(
         formsemestre,
         filename=filename,
         mode_jury=mode_jury,
@@ -120,11 +120,17 @@ def formsemestre_recapcomplet(
         selected_etudid=selected_etudid,
     )
 
-    H = [
-        # sco_formsemestre_status.formsemestre_status_head(
-        #     formsemestre_id=formsemestre_id
-        # ),
-    ]
+    H = []
+    if warnings:
+        H.append(
+            f"""
+            <div class="sco_box table-warnings">
+                <div class="sco_box_title">Avertissements</div>
+                 <ul><li>
+                 {'</li><li>'.join(warnings)}
+                 </li></ul>
+            </div>"""
+        )
     if len(formsemestre.inscriptions) > 0:
         H.append(
             f"""<form id="export_menu" name="f" method="get" action="{request.base_url}">
@@ -300,15 +306,17 @@ def _formsemestre_recapcomplet_to_html(
     if tabformat not in ("html", "evals"):
         raise ScoValueError("invalid table format")
     res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
-    table_html, table, freq_codes_annuels = gen_formsemestre_recapcomplet_html_table(
-        formsemestre,
-        res,
-        include_evaluations=(tabformat == "evals"),
-        mode_jury=mode_jury,
-        filename=filename,
-        selected_etudid=selected_etudid,
+    table_html, table, freq_codes_annuels, warnings = (
+        gen_formsemestre_recapcomplet_html_table(
+            formsemestre,
+            res,
+            include_evaluations=(tabformat == "evals"),
+            mode_jury=mode_jury,
+            filename=filename,
+            selected_etudid=selected_etudid,
+        )
     )
-    return table_html, table, freq_codes_annuels
+    return table_html, table, freq_codes_annuels, warnings
 
 
 def _formsemestre_recapcomplet_to_file(
@@ -473,7 +481,7 @@ def gen_formsemestre_recapcomplet_html_table(
     mode_jury=False,
     filename="",
     selected_etudid=None,
-) -> tuple[str, TableRecap, collections.Counter]:
+) -> tuple[str, TableRecap, collections.Counter, list[str]]:
     """Construit table recap pour le BUT
     Cache le résultat pour le semestre.
     Note: on cache le HTML et non l'objet Table.
@@ -508,11 +516,12 @@ def gen_formsemestre_recapcomplet_html_table(
         freq_codes_annuels = (
             table.freq_codes_annuels if hasattr(table, "freq_codes_annuels") else None
         )
-        cache_class.set(formsemestre.id, (table_html, freq_codes_annuels))
+        warnings = table.warnings
+        cache_class.set(formsemestre.id, (table_html, freq_codes_annuels, warnings))
     else:
-        table_html, freq_codes_annuels = table_html_cached
+        table_html, freq_codes_annuels, warnings = table_html_cached
 
-    return table_html, table, freq_codes_annuels
+    return table_html, table, freq_codes_annuels, warnings
 
 
 def _gen_formsemestre_recapcomplet_table(
diff --git a/app/static/css/gt_table.css b/app/static/css/gt_table.css
index 850bbe1ced68307cb39f75beac038c858a2d76f0..2baa6f6e3959d55bb11e73b39a7ded2ee2137a65 100644
--- a/app/static/css/gt_table.css
+++ b/app/static/css/gt_table.css
@@ -65,4 +65,10 @@ div.gt_caption {
 
 .dt-scroll-foot {
   overflow: visible !important;
+}
+
+div.table-warnings {
+  background-color: yellow;
+  color: darkred;
+  max-width: 100%;
 }
\ No newline at end of file
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index 7e072b6080f23a67b59049889dcc5f5b23ca854a..28975ef6aac61f7c7ba47fdbf7916876041d8d8b 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -1284,6 +1284,10 @@ div.sco_box_title {
   background-color: rgb(209, 255, 214);
 }
 
+div.sco_box.sco_dashed {
+  border: 1px dashed red;
+}
+
 div.vertical_spacing_but {
   margin-top: 12px;
 }
diff --git a/app/tables/jury_recap.py b/app/tables/jury_recap.py
index 8189f978c4f804b3125a87cf23aa65923f645db6..dbd4a8a3e4c2138bbe752acd12788d2ce1785f17 100644
--- a/app/tables/jury_recap.py
+++ b/app/tables/jury_recap.py
@@ -24,6 +24,7 @@ from app.scodoc.codes_cursus import (
     BUT_BARRE_RCUE,
     BUT_RCUE_SUFFISANT,
 )
+from app.scodoc.sco_exceptions import ScoValueError
 from app.scodoc import sco_utils as scu
 from app.tables.recap import RowRecap, TableRecap
 
@@ -201,9 +202,13 @@ class TableJury(TableRecap):
             self.group_titles[group] = f"Compétences {annee}"
         for row in self.rows:
             etud = row.etud
-            cursus_dict = cursus_but.EtudCursusBUT(
-                etud, self.res.formsemestre.formation
-            ).to_dict()
+            try:
+                cursus_dict = cursus_but.EtudCursusBUT(
+                    etud, self.res.formsemestre.formation
+                ).to_dict()
+            except ScoValueError as exc:
+                cursus_dict = {}
+                self.warnings.append(exc.args[0])
             first = True
             for competence_id in cursus_dict:
                 for annee in ("BUT1", "BUT2", "BUT3"):
diff --git a/app/tables/table_builder.py b/app/tables/table_builder.py
index 466ac8e8a97cde8065ba6dbbf6e6aa5091651a49..2ae8f89f16b975a58cbf977401863280a1cd3124 100644
--- a/app/tables/table_builder.py
+++ b/app/tables/table_builder.py
@@ -117,6 +117,8 @@ class Table(Element):
         #
         self.caption = caption
         self.origin = origin
+        self.warnings: list[str] = []
+        "liste d'avertissements rencontrés en construisant la table"
 
     def _prepare(self):
         """Prepare the table before generation:
diff --git a/app/templates/but/parcour_formation.j2 b/app/templates/but/parcour_formation.j2
index 1b0f239689656cf2df7a1d35d51b5fe624b89340..92de363da22a3414754d91e0eeddf98a9e3dd78b 100644
--- a/app/templates/but/parcour_formation.j2
+++ b/app/templates/but/parcour_formation.j2
@@ -124,6 +124,17 @@ Choisissez un parcours...
 </div>
 {% endif %}
 
+{% if current_user.is_administrator() %}
+<div class="sco_box sco_dashed">
+<b>Vous êtes super-administrateur.</b>
+<div>
+    <input type="checkbox" id="force_modification" name="force_modification">
+    <label for="force_modification">forcer modification même si décisions de jury enregistrées (dangereux !)</label>
+</div>
+</div>
+{% endif %}
+
+
 
 {% if parcour %}
     <div class="help">
@@ -150,7 +161,7 @@ Choisissez un parcours...
 function ue_assoc_niveau(event, niveau_id) {
     let ue_id = event.target.value;
     let url = "";
-    let must_reload = false;
+    let force = document.getElementById('force_modification').checked;
     if (ue_id == "") {
         /* Dé-associe */
         ue_id = event.target.dataset.ue_id;
@@ -162,7 +173,9 @@ function ue_assoc_niveau(event, niveau_id) {
         )
         }}';
         url = desassoc_url.replace('11111', ue_id);
-        must_reload=true;
+        if (force) {
+            url += '/force';
+        }
     } else {
         const assoc_url = '{{
             url_for(
@@ -172,6 +185,9 @@ function ue_assoc_niveau(event, niveau_id) {
             )
         }}';
         url = assoc_url.replace('11111', ue_id).replace('22222', niveau_id);
+        if (force) {
+            url += '/force';
+        }
     }
     fetch(url, {
         method: 'POST',
diff --git a/tests/api/test_api_etudiants.py b/tests/api/test_api_etudiants.py
index 6ae352c4b7758985352cfc32ee10c00a920362d4..32b16112fee58caf0760a567a08d849e558c775a 100644
--- a/tests/api/test_api_etudiants.py
+++ b/tests/api/test_api_etudiants.py
@@ -75,6 +75,8 @@ ETUDID = 1
 NIP = "NIP2"
 INE = "INE1"
 
+BUL_NB_FIELDS = 15
+
 
 def test_etudiants_courant(api_headers):
     """
@@ -508,7 +510,7 @@ def test_etudiant_bulletin_semestre(api_headers):
     )
     assert r.status_code == 200
     bulletin = r.json()
-    assert len(bulletin) == 14  # HARDCODED
+    assert len(bulletin) == BUL_NB_FIELDS
 
     assert verify_fields(bulletin, BULLETIN_FIELDS) is True
     assert isinstance(bulletin["version"], str)
@@ -845,7 +847,7 @@ def test_etudiant_bulletin_semestre(api_headers):
     )
     assert r.status_code == 200
     bul = r.json()
-    assert len(bul) == 14  # HARDCODED
+    assert len(bul) == BUL_NB_FIELDS
 
     ######### Test code ine #########
     r = requests.get(
@@ -856,7 +858,7 @@ def test_etudiant_bulletin_semestre(api_headers):
     )
     assert r.status_code == 200
     bul = r.json()
-    assert len(bul) == 14  # HARDCODED
+    assert len(bul) == BUL_NB_FIELDS
 
     ######## Bulletin BUT court en pdf #########
     r = requests.get(
@@ -915,15 +917,15 @@ def test_etudiant_bulletin_semestre(api_headers):
     bul = GET(
         f"/etudiant/etudid/{ETUDID}/formsemestre/1/bulletin/short", headers=api_headers
     )
-    assert len(bul) == 14  # HARDCODED
+    assert len(bul) == BUL_NB_FIELDS
 
     ######### Test code nip #########
     bul = GET(f"/etudiant/nip/{NIP}/formsemestre/1/bulletin/short", headers=api_headers)
-    assert len(bul) == 14  # HARDCODED
+    assert len(bul) == BUL_NB_FIELDS
 
     ######### Test code ine #########
     bul = GET(f"/etudiant/ine/{INE}/formsemestre/1/bulletin/short", headers=api_headers)
-    assert len(bul) == 14  # HARDCODED
+    assert len(bul) == BUL_NB_FIELDS
 
     ################### SHORT + PDF #####################