diff --git a/app/but/validations_view.py b/app/but/validations_view.py
index 83c8430159c0eb126d858a2ddc0b17a58b6f4684..06145a194f4845986aae61199b1d97c84e7075a6 100644
--- a/app/but/validations_view.py
+++ b/app/but/validations_view.py
@@ -4,13 +4,10 @@
 # See LICENSE
 ##############################################################################
 
-"""Jury édition manuelle des décisions (correction d'erreurs, parcours hors normes)
-
-Non spécifique au BUT.
+"""Jury édition manuelle des décisions RCUE antérieures
 """
 
 from flask import render_template
-import sqlalchemy as sa
 
 from app import log
 from app.but import cursus_but
diff --git a/app/scodoc/sco_formation_recap.py b/app/scodoc/sco_formation_recap.py
index f0074e4eb1cab69d8afe16b8eb2dd72c6fd7c84e..331e641cb33104584b420f36e38b5008e1155757 100644
--- a/app/scodoc/sco_formation_recap.py
+++ b/app/scodoc/sco_formation_recap.py
@@ -45,16 +45,15 @@ import app.scodoc.sco_utils as scu
 
 
 # ---- Table recap formation
-def formation_table_recap(formation_id, fmt="html") -> Response:
+def formation_table_recap(formation: Formation, fmt="html") -> Response:
     """Table recapitulant formation."""
-    T = []
-    formation = Formation.query.get_or_404(formation_id)
+    rows = []
     ues = formation.ues.order_by(UniteEns.semestre_idx, UniteEns.numero)
     can_edit = current_user.has_permission(Permission.EditFormation)
     li = 0
     for ue in ues:
         # L'UE
-        T.append(
+        rows.append(
             {
                 "sem": f"S{ue.semestre_idx}" if ue.semestre_idx is not None else "-",
                 "_sem_order": f"{li:04d}",
@@ -83,7 +82,7 @@ def formation_table_recap(formation_id, fmt="html") -> Response:
             for mod in modules:
                 nb_moduleimpls = mod.modimpls.count()
                 # le module (ou ressource ou sae)
-                T.append(
+                rows.append(
                     {
                         "sem": (
                             f"S{mod.semestre_id}"
@@ -152,7 +151,7 @@ def formation_table_recap(formation_id, fmt="html") -> Response:
 
     tab = GenTable(
         columns_ids=columns_ids,
-        rows=T,
+        rows=rows,
         titles=titles,
         origin=f"Généré par {scu.sco_version.SCONAME} le {scu.timedate_human_repr()}",
         caption=title,
@@ -168,7 +167,7 @@ def formation_table_recap(formation_id, fmt="html") -> Response:
         }"
         """,
         html_with_td_classes=True,
-        base_url=f"{request.base_url}?formation_id={formation_id}",
+        base_url=f"{request.base_url}",
         page_title=title,
         html_title=f"<h2>{title}</h2>",
         pdf_title=title,
@@ -192,7 +191,7 @@ def export_recap_formations_annee_scolaire(annee_scolaire):
     formation_ids = {formsemestre.formation.id for formsemestre in formsemestres}
     for formation_id in formation_ids:
         formation = db.session.get(Formation, formation_id)
-        xls = formation_table_recap(formation_id, fmt="xlsx").data
+        xls = formation_table_recap(formation, fmt="xlsx").data
         filename = (
             scu.sanitize_filename(formation.get_titre_version()) + scu.XLSX_SUFFIX
         )
diff --git a/app/views/__init__.py b/app/views/__init__.py
index df78e7cfaf22fc0a38d535e1f32253fca251de96..152a725c295d8026ff10de269b109e97d7449b0e 100644
--- a/app/views/__init__.py
+++ b/app/views/__init__.py
@@ -140,10 +140,15 @@ class ScoData:
         return sco_formsemestre_status.formsemestre_status_menubar(self.formsemestre)
 
 
+# Ajout des routes
+from app.but import bulletin_but_court  # ne pas enlever: ajoute des routes !
+from app.but import jury_dut120  # ne pas enlever: ajoute des routes !
+from app.pe import pe_view  # ne pas enlever, ajoute des routes !
 from app.views import (
     absences,
     assiduites,
     but_formation,
+    jury_validations,
     notes_formsemestre,
     notes,
     pn_modules,
diff --git a/app/views/jury_validations.py b/app/views/jury_validations.py
new file mode 100644
index 0000000000000000000000000000000000000000..ccef894650e3b07e4922d55e95db202bd7eb2e29
--- /dev/null
+++ b/app/views/jury_validations.py
@@ -0,0 +1,904 @@
+##############################################################################
+#
+# ScoDoc
+#
+# Copyright (c) 1999 - 2024 Emmanuel Viennet.  All rights reserved.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+#   Emmanuel Viennet      emmanuel.viennet@viennet.net
+#
+##############################################################################
+
+"""
+Vues sur les jurys et validations
+
+Emmanuel Viennet, 2024
+"""
+
+import datetime
+import flask
+from flask import flash, g, redirect, render_template, request, url_for
+from flask_login import current_user
+
+from app import log
+from app.but import (
+    cursus_but,
+    jury_edit_manual,
+    jury_but,
+    jury_but_validation_auto,
+    jury_but_view,
+)
+from app.but.forms import jury_but_forms
+from app.comp import jury
+from app.decorators import (
+    scodoc,
+    scodoc7func,
+    permission_required,
+)
+from app.models import (
+    Evaluation,
+    Formation,
+    FormSemestre,
+    FormSemestreInscription,
+    Identite,
+    ScolarAutorisationInscription,
+    ScolarNews,
+    ScoDocSiteConfig,
+)
+from app.scodoc import (
+    html_sco_header,
+    sco_bulletins_json,
+    sco_cache,
+    sco_formsemestre_exterieurs,
+    sco_formsemestre_validation,
+    sco_preferences,
+)
+from app.scodoc import sco_utils as scu
+from app.scodoc.sco_exceptions import (
+    ScoPermissionDenied,
+    ScoValueError,
+)
+from app.scodoc.sco_permissions import Permission
+from app.scodoc.sco_pv_dict import descr_autorisations
+
+# from app.scodoc.TrivialFormulator import TrivialFormulator
+from app.views import notes_bp as bp
+from app.views import ScoData
+
+
+# --- FORMULAIRE POUR VALIDATION DES UE ET SEMESTRES
+
+
+@bp.route("/formsemestre_validation_etud_form")
+@scodoc
+@permission_required(Permission.ScoView)
+@scodoc7func
+def formsemestre_validation_etud_form(
+    formsemestre_id,
+    etudid=None,
+    etud_index=None,
+    check=0,
+    desturl="",
+    sortcol=None,
+):
+    "Formulaire choix jury pour un étudiant"
+    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
+    read_only = not formsemestre.can_edit_jury()
+    if formsemestre.formation.is_apc():
+        return redirect(
+            url_for(
+                "notes.formsemestre_validation_but",
+                scodoc_dept=g.scodoc_dept,
+                formsemestre_id=formsemestre_id,
+                etudid=etudid,
+            )
+        )
+    return sco_formsemestre_validation.formsemestre_validation_etud_form(
+        formsemestre_id,
+        etudid=etudid,
+        etud_index=etud_index,
+        check=check,
+        read_only=read_only,
+        dest_url=desturl,
+        sortcol=sortcol,
+    )
+
+
+@bp.route("/formsemestre_validation_etud")
+@scodoc
+@permission_required(Permission.ScoView)
+@scodoc7func
+def formsemestre_validation_etud(
+    formsemestre_id,
+    etudid=None,
+    codechoice=None,
+    desturl="",
+    sortcol=None,
+):
+    "Enregistre choix jury pour un étudiant"
+    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
+    if not formsemestre.can_edit_jury():
+        raise ScoPermissionDenied(
+            dest_url=url_for(
+                "notes.formsemestre_status",
+                scodoc_dept=g.scodoc_dept,
+                formsemestre_id=formsemestre_id,
+            )
+        )
+
+    return sco_formsemestre_validation.formsemestre_validation_etud(
+        formsemestre_id,
+        etudid=etudid,
+        codechoice=codechoice,
+        desturl=desturl,
+        sortcol=sortcol,
+    )
+
+
+@bp.route("/formsemestre_validation_etud_manu")
+@scodoc
+@permission_required(Permission.ScoView)
+@scodoc7func
+def formsemestre_validation_etud_manu(
+    formsemestre_id,
+    etudid=None,
+    code_etat="",
+    new_code_prev="",
+    devenir="",
+    assidu=False,
+    desturl="",
+    sortcol=None,
+):
+    "Enregistre choix jury pour un étudiant"
+    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
+    if not formsemestre.can_edit_jury():
+        raise ScoPermissionDenied(
+            dest_url=url_for(
+                "notes.formsemestre_status",
+                scodoc_dept=g.scodoc_dept,
+                formsemestre_id=formsemestre_id,
+            )
+        )
+
+    return sco_formsemestre_validation.formsemestre_validation_etud_manu(
+        formsemestre_id,
+        etudid=etudid,
+        code_etat=code_etat,
+        new_code_prev=new_code_prev,
+        devenir=devenir,
+        assidu=assidu,
+        desturl=desturl,
+        sortcol=sortcol,
+    )
+
+
+# --- Jurys BUT
+@bp.route(
+    "/formsemestre_validation_but/<int:formsemestre_id>/<etudid>",
+    methods=["GET", "POST"],
+)
+@scodoc
+@permission_required(Permission.ScoView)
+def formsemestre_validation_but(
+    formsemestre_id: int,
+    etudid: int,
+):
+    "Form. saisie décision jury semestre BUT"
+    formsemestre: FormSemestre = FormSemestre.query.filter_by(
+        id=formsemestre_id, dept_id=g.scodoc_dept_id
+    ).first_or_404()
+    # la route ne donne pas le type d'etudid pour pouvoir construire des URLs
+    # provisoires avec NEXT et PREV
+    try:
+        etudid = int(etudid)
+    except ValueError as exc:
+        raise ScoValueError("adresse invalide") from exc
+    etud = Identite.get_etud(etudid)
+    nb_etuds = formsemestre.etuds.count()
+    read_only = not formsemestre.can_edit_jury()
+    can_erase = current_user.has_permission(Permission.EtudInscrit)
+    # --- Navigation
+    prev_lnk = (
+        f"""{scu.EMO_PREV_ARROW}&nbsp;<a href="{url_for(
+                "notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept,
+                formsemestre_id=formsemestre_id, etudid="PREV"
+            )}" class="stdlink"">précédent</a>
+    """
+        if nb_etuds > 1
+        else ""
+    )
+    next_lnk = (
+        f"""<a href="{url_for(
+                "notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept,
+                formsemestre_id=formsemestre_id, etudid="NEXT"
+            )}" class="stdlink"">suivant</a>&nbsp;{scu.EMO_NEXT_ARROW}
+    """
+        if nb_etuds > 1
+        else ""
+    )
+    navigation_div = f"""
+    <div class="but_navigation">
+        <div class="prev">
+           {prev_lnk}
+        </div>
+        <div class="back_list">
+            <a href="{
+                url_for(
+                    "notes.formsemestre_recapcomplet",
+                    scodoc_dept=g.scodoc_dept,
+                    formsemestre_id=formsemestre_id,
+                    mode_jury=1,
+                    selected_etudid=etud.id
+            )}" class="stdlink">retour à la liste</a>
+        </div>
+        <div class="next">
+            {next_lnk}
+        </div>
+    </div>
+    """
+
+    H = [
+        html_sco_header.sco_header(
+            page_title=f"Validation BUT S{formsemestre.semestre_id}",
+            formsemestre_id=formsemestre_id,
+            etudid=etudid,
+            cssstyles=[
+                "css/jury_but.css",
+                "css/cursus_but.css",
+            ],
+            javascripts=("js/jury_but.js",),
+        ),
+        """<div class="jury_but">
+        """,
+    ]
+
+    if formsemestre.etuds_inscriptions[etudid].etat != scu.INSCRIT:
+        return (
+            "\n".join(H)
+            + f"""
+            <div>
+                <div class="bull_head">
+                <div>
+                    <div class="titre_parcours">Jury BUT</div>
+                    <div class="nom_etud">{etud.html_link_fiche()}</div>
+                </div>
+                <div class="bull_photo"><a href="{
+                    etud.url_fiche()
+                    }">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
+                </div>
+                </div>
+                <div class="warning">Impossible de statuer sur cet étudiant:
+                        il est démissionnaire ou défaillant (voir <a class="stdlink" href="{
+                    etud.url_fiche()}">sa fiche</a>)
+                </div>
+            </div>
+            {navigation_div}
+            </div>
+        """
+            + html_sco_header.sco_footer()
+        )
+
+    deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
+    has_notes_en_attente = deca.has_notes_en_attente()
+    evaluations_a_debloquer = Evaluation.get_evaluations_blocked_for_etud(
+        formsemestre, etud
+    )
+    if has_notes_en_attente or evaluations_a_debloquer:
+        read_only = True
+    if request.method == "POST":
+        if not read_only:
+            deca.record_form(request.form)
+            ScolarNews.add(
+                typ=ScolarNews.NEWS_JURY,
+                obj=formsemestre.id,
+                text=f"""Saisie décision jury dans {formsemestre.html_link_status()}""",
+                url=url_for(
+                    "notes.formsemestre_status",
+                    scodoc_dept=g.scodoc_dept,
+                    formsemestre_id=formsemestre.id,
+                ),
+            )
+            flash("codes enregistrés")
+        return flask.redirect(
+            url_for(
+                "notes.formsemestre_validation_but",
+                scodoc_dept=g.scodoc_dept,
+                formsemestre_id=formsemestre_id,
+                etudid=etudid,
+            )
+        )
+
+    warning = ""
+    if len(deca.niveaux_competences) != len(deca.decisions_rcue_by_niveau):
+        warning += f"""<div class="warning">Attention: {len(deca.niveaux_competences)}
+                niveaux mais {len(deca.decisions_rcue_by_niveau)} regroupements RCUE.</div>"""
+    if (deca.parcour is None) and len(formsemestre.parcours) > 0:
+        warning += (
+            """<div class="warning">L'étudiant n'est pas inscrit à un parcours.</div>"""
+        )
+    if formsemestre.date_fin - datetime.date.today() > datetime.timedelta(days=12):
+        # encore loin de la fin du semestre de départ de ce jury ?
+        warning += f"""<div class="warning">Le semestre S{formsemestre.semestre_id}
+            terminera le {formsemestre.date_fin.strftime(scu.DATE_FMT)}&nbsp;:
+            êtes-vous certain de vouloir enregistrer une décision de jury&nbsp;?
+            </div>"""
+
+    if deca.formsemestre_impair:
+        inscription = deca.formsemestre_impair.etuds_inscriptions.get(etud.id)
+        if (not inscription) or inscription.etat != scu.INSCRIT:
+            etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?")
+            warning += f"""<div class="warning">{etat_ins}
+                en S{deca.formsemestre_impair.semestre_id}</div>"""
+
+    if deca.formsemestre_pair:
+        inscription = deca.formsemestre_pair.etuds_inscriptions.get(etud.id)
+        if (not inscription) or inscription.etat != scu.INSCRIT:
+            etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?")
+            warning += f"""<div class="warning">{etat_ins}
+                en S{deca.formsemestre_pair.semestre_id}</div>"""
+
+    if has_notes_en_attente:
+        warning += f"""<div class="warning-bloquant">{etud.html_link_fiche()
+            } a des notes en ATTente dans les modules suivants.
+            Vous devez régler cela avant de statuer en jury !
+            <ul class="modimpls_att">
+            """
+        for modimpl in deca.get_modimpls_attente():
+            warning += f"""<li><a href="{
+                    url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
+                }" class="stdlink">{modimpl.module.code} {modimpl.module.titre_str()}</a></li>"""
+        warning += "</ul></div>"
+    if evaluations_a_debloquer:
+        links_evals = [
+            f"""<a class="stdlink" href="{url_for(
+                    'notes.evaluation_listenotes', scodoc_dept=g.scodoc_dept, evaluation_id=e.id
+                )}">{e.description} en {e.moduleimpl.module.code}</a>"""
+            for e in evaluations_a_debloquer
+        ]
+        warning += f"""<div class="warning-bloquant">Impossible de statuer sur cet étudiant:
+                il a des notes dans des évaluations qui seront débloquées plus tard:
+                voir {", ".join(links_evals)}
+                """
+
+    if warning:
+        warning = f"""<div class="jury_but_warning jury_but_box">{warning}</div>"""
+    H.append(
+        f"""
+    <div>
+        <div class="bull_head">
+        <div>
+            <div class="titre_parcours">Jury BUT{deca.annee_but}
+            - Parcours {(deca.parcour.libelle if deca.parcour else False) or "non spécifié"}
+            - {deca.annee_scolaire_str()}</div>
+            <div class="nom_etud">{etud.html_link_fiche()}</div>
+        </div>
+        <div class="bull_photo"><a href="{
+            etud.url_fiche()}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
+        </div>
+        </div>
+        {warning}
+    </div>
+
+    <form method="post" class="jury_but_box" id="jury_but">
+    """
+    )
+
+    H.append(jury_but_view.show_etud(deca, read_only=read_only))
+
+    autorisations_idx = deca.get_autorisations_passage()
+    div_autorisations_passage = (
+        f"""
+        <div class="but_autorisations_passage">
+            <span>Autorisé à passer en&nbsp;:</span>
+            { ", ".join( ["S" + str(i) for i in autorisations_idx ] )}
+        </div>
+    """
+        if autorisations_idx
+        else """<div class="but_autorisations_passage but_explanation">
+            pas d'autorisations de passage enregistrées.
+            </div>
+            """
+    )
+    H.append(div_autorisations_passage)
+
+    if read_only:
+        H.append(
+            f"""
+            <div class="but_explanation">
+            {"Vous n'avez pas la permission de modifier ces décisions."
+            if formsemestre.etat
+            else "Semestre verrouillé."}
+            Les champs entourés en vert sont enregistrés.
+            </div>"""
+        )
+    else:
+        erase_span = f"""
+            <a style="margin-left: 16px;" class="stdlink {'' if can_erase else 'link_unauthorized'}"
+            title="{'' if can_erase else 'réservé au responsable'}"
+            href="{
+                url_for("notes.erase_decisions_annee_formation",
+                scodoc_dept=g.scodoc_dept, formation_id=deca.formsemestre.formation.id,
+                etudid=deca.etud.id, annee=deca.annee_but, formsemestre_id=formsemestre_id)
+                if can_erase else ''
+                }"
+            >effacer des décisions de jury</a>
+
+            <a style="margin-left: 16px;" class="stdlink"
+            href="{
+                url_for("notes.formsemestre_validate_previous_ue",
+                scodoc_dept=g.scodoc_dept,
+                etudid=deca.etud.id, formsemestre_id=formsemestre_id)}"
+            >enregistrer des UEs antérieures</a>
+
+            <a style="margin-left: 16px;" class="stdlink"
+            href="{
+                url_for("notes.validate_dut120_etud",
+                scodoc_dept=g.scodoc_dept,
+                etudid=deca.etud.id, formsemestre_id=formsemestre_id)}"
+            >décerner le DUT "120ECTS"</a>
+            """
+        H.append(
+            f"""<div class="but_settings">
+            <input type="checkbox" onchange="enable_manual_codes(this)">
+                <em>permettre la saisie manuelles des codes
+                {"d'année et " if deca.jury_annuel else ""}
+                de niveaux.
+                Dans ce cas, assurez-vous de la cohérence entre les codes d'UE/RCUE/Année !
+                </em>
+            </input>
+            </div>
+
+            <div class="but_buttons">
+                <span><input type="submit" value="Enregistrer ces décisions"></span>
+                <span>{erase_span}</span>
+            </div>
+            """
+        )
+    H.append(navigation_div)
+    H.append("</form>")
+
+    # Affichage cursus BUT
+    but_cursus = cursus_but.EtudCursusBUT(etud, deca.formsemestre.formation)
+    H += [
+        """<div class="jury_but_box">
+        <div class="jury_but_box_title"><b>Niveaux de compétences enregistrés :</b></div>
+        """,
+        render_template(
+            "but/cursus_etud.j2",
+            cursus=but_cursus,
+            scu=scu,
+        ),
+        "</div>",
+    ]
+    H.append(
+        render_template(
+            "but/documentation_codes_jury.j2",
+            nom_univ=f"""Export {sco_preferences.get_preference("InstituteName")
+            or sco_preferences.get_preference("UnivName")
+            or "Apogée"}""",
+            codes=ScoDocSiteConfig.get_codes_apo_dict(),
+        )
+    )
+    H.append(
+        f"""<div class="but_doc_codes but_warning_rcue_cap">
+    {scu.EMO_WARNING} Rappel: pour les redoublants, seules les UE <b>capitalisées</b> (note > 10)
+    lors d'une année précédente peuvent être prise en compte pour former
+    un RCUE (associé à un niveau de compétence du BUT).
+    </div>
+    """
+    )
+    return "\n".join(H) + html_sco_header.sco_footer()
+
+
+@bp.route(
+    "/formsemestre_validation_auto_but/<int:formsemestre_id>", methods=["GET", "POST"]
+)
+@scodoc
+@permission_required(Permission.ScoView)
+def formsemestre_validation_auto_but(formsemestre_id: int = None):
+    "Saisie automatique des décisions de jury BUT"
+    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
+    if not formsemestre.can_edit_jury():
+        raise ScoPermissionDenied(
+            dest_url=url_for(
+                "notes.formsemestre_status",
+                scodoc_dept=g.scodoc_dept,
+                formsemestre_id=formsemestre_id,
+            )
+        )
+    if not formsemestre.formation.is_apc():
+        raise ScoValueError(
+            "formsemestre_validation_auto_but est réservé aux formations APC"
+        )
+
+    formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
+    form = jury_but_forms.FormSemestreValidationAutoBUTForm()
+    if request.method == "POST":
+        if not form.cancel.data:
+            nb_etud_modif, _ = (
+                jury_but_validation_auto.formsemestre_validation_auto_but(formsemestre)
+            )
+            flash(f"Décisions enregistrées ({nb_etud_modif} étudiants modifiés)")
+        return redirect(
+            url_for(
+                "notes.formsemestre_recapcomplet",
+                scodoc_dept=g.scodoc_dept,
+                formsemestre_id=formsemestre_id,
+                mode_jury=1,
+            )
+        )
+    # Avertissement si formsemestre impair
+    formsemestres_suspects = {}
+    if formsemestre.semestre_id % 2:
+        _, decas = jury_but_validation_auto.formsemestre_validation_auto_but(
+            formsemestre, dry_run=True
+        )
+        # regarde si il y a des semestres pairs postérieurs qui ne soient pas bloqués
+        formsemestres_suspects = {
+            deca.formsemestre_pair.id: deca.formsemestre_pair
+            for deca in decas
+            if deca.formsemestre_pair
+            and deca.formsemestre_pair.date_debut > formsemestre.date_debut
+            and not deca.formsemestre_pair.block_moyennes
+        }
+
+    return render_template(
+        "but/formsemestre_validation_auto_but.j2",
+        form=form,
+        formsemestres_suspects=formsemestres_suspects,
+        sco=ScoData(formsemestre=formsemestre),
+        title="Calcul automatique jury BUT",
+    )
+
+
+@bp.route(
+    "/formsemestre_validate_previous_ue/<int:formsemestre_id>/<int:etudid>",
+    methods=["GET", "POST"],
+)
+@scodoc
+@permission_required(Permission.ScoView)
+def formsemestre_validate_previous_ue(formsemestre_id, etudid=None):
+    "Form. saisie UE validée hors ScoDoc"
+    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
+    if not formsemestre.can_edit_jury():
+        raise ScoPermissionDenied(
+            dest_url=url_for(
+                "notes.formsemestre_status",
+                scodoc_dept=g.scodoc_dept,
+                formsemestre_id=formsemestre_id,
+            )
+        )
+    etud: Identite = (
+        Identite.query.filter_by(id=etudid)
+        .join(FormSemestreInscription)
+        .filter_by(formsemestre_id=formsemestre_id)
+        .first_or_404()
+    )
+
+    return sco_formsemestre_validation.formsemestre_validate_previous_ue(
+        formsemestre, etud
+    )
+
+
+@bp.route("/formsemestre_ext_edit_ue_validations", methods=["GET", "POST"])
+@scodoc
+@permission_required(Permission.ScoView)
+@scodoc7func
+def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid=None):
+    "Form. edition UE semestre extérieur"
+    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
+    if not formsemestre.can_edit_jury():
+        raise ScoPermissionDenied(
+            dest_url=url_for(
+                "notes.formsemestre_status",
+                scodoc_dept=g.scodoc_dept,
+                formsemestre_id=formsemestre_id,
+            )
+        )
+
+    return sco_formsemestre_exterieurs.formsemestre_ext_edit_ue_validations(
+        formsemestre_id, etudid
+    )
+
+
+@bp.route("/formsemestre_validation_auto")
+@scodoc
+@permission_required(Permission.ScoView)
+@scodoc7func
+def formsemestre_validation_auto(formsemestre_id):
+    "Formulaire saisie automatisee des decisions d'un semestre"
+    formsemestre: FormSemestre = FormSemestre.query.filter_by(
+        id=formsemestre_id, dept_id=g.scodoc_dept_id
+    ).first_or_404()
+    if not formsemestre.can_edit_jury():
+        raise ScoPermissionDenied(
+            dest_url=url_for(
+                "notes.formsemestre_status",
+                scodoc_dept=g.scodoc_dept,
+                formsemestre_id=formsemestre_id,
+            )
+        )
+
+    if formsemestre.formation.is_apc():
+        return redirect(
+            url_for(
+                "notes.formsemestre_validation_auto_but",
+                scodoc_dept=g.scodoc_dept,
+                formsemestre_id=formsemestre.id,
+            )
+        )
+    return sco_formsemestre_validation.formsemestre_validation_auto(formsemestre_id)
+
+
+@bp.route("/do_formsemestre_validation_auto")
+@scodoc
+@permission_required(Permission.ScoView)
+@scodoc7func
+def do_formsemestre_validation_auto(formsemestre_id):
+    "Formulaire saisie automatisee des decisions d'un semestre"
+    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
+    if not formsemestre.can_edit_jury():
+        raise ScoPermissionDenied(
+            dest_url=url_for(
+                "notes.formsemestre_status",
+                scodoc_dept=g.scodoc_dept,
+                formsemestre_id=formsemestre_id,
+            )
+        )
+
+    return sco_formsemestre_validation.do_formsemestre_validation_auto(formsemestre_id)
+
+
+@bp.route("/formsemestre_validation_suppress_etud", methods=["GET", "POST"])
+@scodoc
+@permission_required(Permission.ScoView)
+@scodoc7func
+def formsemestre_validation_suppress_etud(
+    formsemestre_id, etudid, dialog_confirmed=False
+):
+    """Suppression des décisions de jury pour un étudiant."""
+    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
+    if not formsemestre.can_edit_jury():
+        raise ScoPermissionDenied(
+            dest_url=url_for(
+                "notes.formsemestre_status",
+                scodoc_dept=g.scodoc_dept,
+                formsemestre_id=formsemestre_id,
+            )
+        )
+    etud = Identite.get_etud(etudid)
+    if formsemestre.formation.is_apc():
+        next_url = url_for(
+            "scolar.fiche_etud",
+            scodoc_dept=g.scodoc_dept,
+            etudid=etudid,
+        )
+    else:
+        next_url = url_for(
+            "notes.formsemestre_validation_etud_form",
+            scodoc_dept=g.scodoc_dept,
+            formsemestre_id=formsemestre_id,
+            etudid=etudid,
+        )
+    if not dialog_confirmed:
+        d = sco_bulletins_json.dict_decision_jury(
+            etud, formsemestre, with_decisions=True
+        )
+
+        descr_ues = [f"{u['acronyme']}: {u['code']}" for u in d.get("decision_ue", [])]
+        dec_annee = d.get("decision_annee")
+        if dec_annee:
+            descr_annee = dec_annee.get("code", "-")
+        else:
+            descr_annee = "-"
+
+        existing = f"""
+        <ul>
+        <li>Semestre : {d.get("decision", {"code":"-"})['code'] or "-"}</li>
+        <li>Année BUT: {descr_annee}</li>
+        <li>UEs : {", ".join(descr_ues)}</li>
+        <li>RCUEs: {len(d.get("decision_rcue", []))} décisions</li>
+        <li>Autorisations: {descr_autorisations(ScolarAutorisationInscription.query.filter_by(origin_formsemestre_id=formsemestre_id,
+            etudid=etudid))}
+        </ul>
+        """
+        return scu.confirm_dialog(
+            f"""<h2>Confirmer la suppression des décisions du semestre
+            {formsemestre.titre_mois()} pour {etud.nomprenom}
+            </h2>
+            <p>Cette opération est irréversible.</p>
+            <div>
+            {existing}
+            </div>
+            """,
+            OK="Supprimer",
+            dest_url="",
+            cancel_url=next_url,
+            parameters={"etudid": etudid, "formsemestre_id": formsemestre_id},
+        )
+
+    sco_formsemestre_validation.formsemestre_validation_suppress_etud(
+        formsemestre_id, etudid
+    )
+    flash("Décisions supprimées")
+    return flask.redirect(next_url)
+
+
+@bp.route(
+    "/formsemestre_jury_erase/<int:formsemestre_id>",
+    methods=["GET", "POST"],
+    defaults={"etudid": None},
+)
+@bp.route(
+    "/formsemestre_jury_erase/<int:formsemestre_id>/<int:etudid>",
+    methods=["GET", "POST"],
+)
+@scodoc
+@permission_required(Permission.ScoView)
+def formsemestre_jury_erase(formsemestre_id: int, etudid: int = None):
+    """Supprime toutes les décisions de jury (classique ou BUT) pour cette année.
+    Si l'étudiant n'est pas spécifié, efface les décisions de tous les inscrits.
+    En BUT, si only_one_sem n'efface que pour le formsemestre indiqué, pas les deux de l'année.
+    En classique, n'affecte que les décisions issues de ce formsemestre.
+    """
+    only_one_sem = int(request.args.get("only_one_sem") or False)
+    formsemestre: FormSemestre = FormSemestre.query.filter_by(
+        id=formsemestre_id, dept_id=g.scodoc_dept_id
+    ).first_or_404()
+    if not formsemestre.can_edit_jury():
+        raise ScoPermissionDenied(
+            dest_url=url_for(
+                "notes.formsemestre_status",
+                scodoc_dept=g.scodoc_dept,
+                formsemestre_id=formsemestre_id,
+            )
+        )
+    is_apc = formsemestre.formation.is_apc()
+    if etudid is None:
+        etud = None
+        etuds = formsemestre.get_inscrits(include_demdef=True)
+        dest_url = url_for(
+            "notes.formsemestre_recapcomplet",
+            scodoc_dept=g.scodoc_dept,
+            formsemestre_id=formsemestre_id,
+            mode_jury=1,
+        )
+    else:
+        etud = Identite.get_etud(etudid)
+        etuds = [etud]
+        endpoint = (
+            "notes.formsemestre_validation_but"
+            if is_apc
+            else "notes.formsemestre_validation_etud_form"
+        )
+        dest_url = url_for(
+            endpoint,
+            scodoc_dept=g.scodoc_dept,
+            formsemestre_id=formsemestre_id,
+            etudid=etudid,
+        )
+    if request.method == "POST":
+        with sco_cache.DeferredSemCacheManager():
+            for etud in etuds:
+                if is_apc:
+                    deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
+                    deca.erase(only_one_sem=only_one_sem)
+                else:
+                    sco_formsemestre_validation.formsemestre_validation_suppress_etud(
+                        formsemestre.id, etud.id
+                    )
+                log(f"formsemestre_jury_erase({formsemestre_id}, {etud.id})")
+        flash(
+            (
+                "décisions de jury du semestre effacées"
+                if (only_one_sem or is_apc)
+                else "décisions de jury des semestres de l'année BUT effacées"
+            )
+            + f" pour {len(etuds)} étudiant{'s' if len(etuds) > 1 else ''}"
+        )
+        return redirect(dest_url)
+
+    return render_template(
+        "confirm_dialog.j2",
+        title=f"""Effacer les validations de jury {
+            ("de " + etud.nomprenom)
+            if etud
+            else ("des " + str(len(etuds)) + " étudiants inscrits dans ce semestre")
+            } ?""",
+        explanation=(
+            (
+                f"""Les validations d'UE et autorisations de passage
+            du semestre S{formsemestre.semestre_id} seront effacées."""
+                if (only_one_sem or is_apc)
+                else """Les validations de toutes les UE, RCUE (compétences) et année
+        issues de cette année scolaire seront effacées.
+        """
+            )
+            + """
+        <p>Les décisions des années scolaires précédentes ne seront pas modifiées.</p>
+        """
+            + """
+        <p>Efface aussi toutes les validations concernant l'année BUT de ce semestre,
+        même si elles ont été acquises ailleurs, ainsi que les validations de DUT en 120 ECTS
+        obtenues après BUT1/BUT2.
+        </p>
+        """
+            if is_apc
+            else ""
+            + """
+        <div class="warning">Cette opération est irréversible !
+        A n'utiliser que dans des cas exceptionnels, vérifiez bien tous les étudiants ensuite.
+        </div>
+        """
+        ),
+        cancel_url=dest_url,
+    )
+
+
+@bp.route(
+    "/erase_decisions_annee_formation/<int:etudid>/<int:formation_id>/<int:annee>",
+    methods=["GET", "POST"],
+)
+@scodoc
+@permission_required(Permission.EtudInscrit)
+def erase_decisions_annee_formation(etudid: int, formation_id: int, annee: int):
+    """Efface toute les décisions d'une année pour cet étudiant"""
+    etud: Identite = Identite.query.get_or_404(etudid)
+    formation: Formation = Formation.query.filter_by(
+        id=formation_id, dept_id=g.scodoc_dept_id
+    ).first_or_404()
+    if request.method == "POST":
+        jury.erase_decisions_annee_formation(etud, formation, annee, delete=True)
+        flash("Décisions de jury effacées")
+        return redirect(
+            url_for(
+                "scolar.fiche_etud",
+                scodoc_dept=g.scodoc_dept,
+                etudid=etud.id,
+            )
+        )
+    validations = jury.erase_decisions_annee_formation(etud, formation, annee)
+    formsemestre_origine_id = request.args.get("formsemestre_id")
+    formsemestre_origine = (
+        FormSemestre.query.get_or_404(formsemestre_origine_id)
+        if formsemestre_origine_id
+        else None
+    )
+    return render_template(
+        "jury/erase_decisions_annee_formation.j2",
+        annee=annee,
+        cancel_url=url_for(
+            "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id
+        ),
+        etud=etud,
+        formation=formation,
+        formsemestre_origine=formsemestre_origine,
+        validations=validations,
+        sco=ScoData(),
+        title=f"Effacer décisions de jury {etud.nom} - année {annee}",
+    )
+
+
+@bp.route(
+    "/jury_delete_manual/<int:etudid>",
+    methods=["GET", "POST"],
+)
+@scodoc
+@permission_required(Permission.EtudInscrit)
+def jury_delete_manual(etudid: int):
+    """Efface toute les décisions d'une année pour cet étudiant"""
+    etud = Identite.get_etud(etudid)
+    return jury_edit_manual.jury_delete_manual(etud)
diff --git a/app/views/notes.py b/app/views/notes.py
index 482b6c48fa13b0aebaa2cb4d5a11755b02fb45cb..806a607ddaedcf2dbb439a7ebbf846e312463d86 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -30,7 +30,6 @@ Module notes: issu de ScoDoc7 / ZNotes.py
 
 Emmanuel Viennet, 2021
 """
-import datetime
 import html
 from operator import itemgetter
 import time
@@ -40,24 +39,13 @@ from flask import flash, redirect, render_template, url_for
 from flask import g, request
 from flask_login import current_user
 
-from app import db
+from app import db, log, send_scodoc_alarm
 from app import models
 from app.auth.models import User
-from app.but import (
-    apc_edit_ue,
-    cursus_but,
-    jury_edit_manual,
-    jury_but,
-    jury_but_pv,
-    jury_but_validation_auto,
-    jury_but_view,
-)
-from app.but import bulletin_but_court  # ne pas enlever: ajoute des routes !
-from app.but import jury_dut120  # ne pas enlever: ajoute des routes !
-from app.but.forms import jury_but_forms
+from app.but import apc_edit_ue, jury_but_pv
 
 
-from app.comp import jury, res_sem
+from app.comp import res_sem
 from app.comp.res_compat import NotesTableCompat
 from app.models import (
     ApcNiveau,
@@ -72,10 +60,7 @@ from app.models import (
     Identite,
     Module,
     ModuleImpl,
-    ScolarAutorisationInscription,
-    ScolarNews,
     Scolog,
-    ScoDocSiteConfig,
     UniteEns,
 )
 from app.scodoc.sco_exceptions import ScoFormationConflict, ScoPermissionDenied
@@ -90,9 +75,7 @@ from app.decorators import (
 
 
 # ---------------
-from app.pe import pe_view  # ne pas enlever, ajoute des vues
-from app.scodoc import sco_bulletins_json, sco_utils as scu
-from app import log, send_scodoc_alarm
+from app.scodoc import sco_utils as scu
 
 from app.scodoc.sco_exceptions import (
     AccessDenied,
@@ -131,7 +114,6 @@ from app.scodoc import (
     sco_formsemestre_exterieurs,
     sco_formsemestre_inscriptions,
     sco_formsemestre_status,
-    sco_formsemestre_validation,
     sco_groups_view,
     sco_inscr_passage,
     sco_liste_notes,
@@ -157,7 +139,6 @@ from app.scodoc import (
     sco_users,
 )
 from app.scodoc.gen_tables import GenTable
-from app.scodoc.sco_pv_dict import descr_autorisations
 from app.scodoc.sco_permissions import Permission
 from app.scodoc.TrivialFormulator import TrivialFormulator
 from app.views import ScoData
@@ -560,12 +541,14 @@ sco_publish(
 )
 
 
-@bp.route("/formation_table_recap")
+@bp.route("/formation_table_recap/<int:formation_id>")
 @scodoc
 @permission_required(Permission.ScoView)
-@scodoc7func
-def formation_table_recap(formation_id, fmt="html"):
-    return sco_formation_recap.formation_table_recap(formation_id, fmt=fmt)
+def formation_table_recap(formation_id: int):
+    "Tableau récap. de la formation"
+    formation = Formation.get_formation(formation_id)
+    fmt = request.args.get("fmt", "html")
+    return sco_formation_recap.formation_table_recap(formation, fmt=fmt)
 
 
 sco_publish(
@@ -829,9 +812,9 @@ sco_publish("/ue_move", sco_edit_formation.ue_move, Permission.EditFormation)
 @permission_required(Permission.EditFormation)
 def ue_clone():
     """Clone existing UE"""
-    ue_id = int(request.form.get("ue_id"))
-    ue = UniteEns.query.get_or_404(ue_id)
-    ue2 = ue.clone()
+    ue_id = request.form.get("ue_id")
+    ue = UniteEns.get_ue(ue_id)
+    _ = ue.clone()
     db.session.commit()
     flash(f"UE {ue.acronyme} dupliquée")
     return flask.redirect(
@@ -2205,520 +2188,6 @@ def appreciation_add_form(
         return flask.redirect(bul_url)
 
 
-# --- FORMULAIRE POUR VALIDATION DES UE ET SEMESTRES
-
-
-@bp.route("/formsemestre_validation_etud_form")
-@scodoc
-@permission_required(Permission.ScoView)
-@scodoc7func
-def formsemestre_validation_etud_form(
-    formsemestre_id,
-    etudid=None,
-    etud_index=None,
-    check=0,
-    desturl="",
-    sortcol=None,
-):
-    "Formulaire choix jury pour un étudiant"
-    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
-    read_only = not formsemestre.can_edit_jury()
-    if formsemestre.formation.is_apc():
-        return redirect(
-            url_for(
-                "notes.formsemestre_validation_but",
-                scodoc_dept=g.scodoc_dept,
-                formsemestre_id=formsemestre_id,
-                etudid=etudid,
-            )
-        )
-    return sco_formsemestre_validation.formsemestre_validation_etud_form(
-        formsemestre_id,
-        etudid=etudid,
-        etud_index=etud_index,
-        check=check,
-        read_only=read_only,
-        dest_url=desturl,
-        sortcol=sortcol,
-    )
-
-
-@bp.route("/formsemestre_validation_etud")
-@scodoc
-@permission_required(Permission.ScoView)
-@scodoc7func
-def formsemestre_validation_etud(
-    formsemestre_id,
-    etudid=None,
-    codechoice=None,
-    desturl="",
-    sortcol=None,
-):
-    "Enregistre choix jury pour un étudiant"
-    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
-    if not formsemestre.can_edit_jury():
-        raise ScoPermissionDenied(
-            dest_url=url_for(
-                "notes.formsemestre_status",
-                scodoc_dept=g.scodoc_dept,
-                formsemestre_id=formsemestre_id,
-            )
-        )
-
-    return sco_formsemestre_validation.formsemestre_validation_etud(
-        formsemestre_id,
-        etudid=etudid,
-        codechoice=codechoice,
-        desturl=desturl,
-        sortcol=sortcol,
-    )
-
-
-@bp.route("/formsemestre_validation_etud_manu")
-@scodoc
-@permission_required(Permission.ScoView)
-@scodoc7func
-def formsemestre_validation_etud_manu(
-    formsemestre_id,
-    etudid=None,
-    code_etat="",
-    new_code_prev="",
-    devenir="",
-    assidu=False,
-    desturl="",
-    sortcol=None,
-):
-    "Enregistre choix jury pour un étudiant"
-    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
-    if not formsemestre.can_edit_jury():
-        raise ScoPermissionDenied(
-            dest_url=url_for(
-                "notes.formsemestre_status",
-                scodoc_dept=g.scodoc_dept,
-                formsemestre_id=formsemestre_id,
-            )
-        )
-
-    return sco_formsemestre_validation.formsemestre_validation_etud_manu(
-        formsemestre_id,
-        etudid=etudid,
-        code_etat=code_etat,
-        new_code_prev=new_code_prev,
-        devenir=devenir,
-        assidu=assidu,
-        desturl=desturl,
-        sortcol=sortcol,
-    )
-
-
-# --- Jurys BUT
-@bp.route(
-    "/formsemestre_validation_but/<int:formsemestre_id>/<etudid>",
-    methods=["GET", "POST"],
-)
-@scodoc
-@permission_required(Permission.ScoView)
-def formsemestre_validation_but(
-    formsemestre_id: int,
-    etudid: int,
-):
-    "Form. saisie décision jury semestre BUT"
-    formsemestre: FormSemestre = FormSemestre.query.filter_by(
-        id=formsemestre_id, dept_id=g.scodoc_dept_id
-    ).first_or_404()
-    # la route ne donne pas le type d'etudid pour pouvoir construire des URLs
-    # provisoires avec NEXT et PREV
-    try:
-        etudid = int(etudid)
-    except ValueError as exc:
-        raise ScoValueError("adresse invalide") from exc
-    etud = Identite.get_etud(etudid)
-    nb_etuds = formsemestre.etuds.count()
-    read_only = not formsemestre.can_edit_jury()
-    can_erase = current_user.has_permission(Permission.EtudInscrit)
-    # --- Navigation
-    prev_lnk = (
-        f"""{scu.EMO_PREV_ARROW}&nbsp;<a href="{url_for(
-                "notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept,
-                formsemestre_id=formsemestre_id, etudid="PREV"
-            )}" class="stdlink"">précédent</a>
-    """
-        if nb_etuds > 1
-        else ""
-    )
-    next_lnk = (
-        f"""<a href="{url_for(
-                "notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept,
-                formsemestre_id=formsemestre_id, etudid="NEXT"
-            )}" class="stdlink"">suivant</a>&nbsp;{scu.EMO_NEXT_ARROW}
-    """
-        if nb_etuds > 1
-        else ""
-    )
-    navigation_div = f"""
-    <div class="but_navigation">
-        <div class="prev">
-           {prev_lnk}
-        </div>
-        <div class="back_list">
-            <a href="{
-                url_for(
-                    "notes.formsemestre_recapcomplet",
-                    scodoc_dept=g.scodoc_dept,
-                    formsemestre_id=formsemestre_id,
-                    mode_jury=1,
-                    selected_etudid=etud.id
-            )}" class="stdlink">retour à la liste</a>
-        </div>
-        <div class="next">
-            {next_lnk}
-        </div>
-    </div>
-    """
-
-    H = [
-        html_sco_header.sco_header(
-            page_title=f"Validation BUT S{formsemestre.semestre_id}",
-            formsemestre_id=formsemestre_id,
-            etudid=etudid,
-            cssstyles=[
-                "css/jury_but.css",
-                "css/cursus_but.css",
-            ],
-            javascripts=("js/jury_but.js",),
-        ),
-        """<div class="jury_but">
-        """,
-    ]
-
-    if formsemestre.etuds_inscriptions[etudid].etat != scu.INSCRIT:
-        return (
-            "\n".join(H)
-            + f"""
-            <div>
-                <div class="bull_head">
-                <div>
-                    <div class="titre_parcours">Jury BUT</div>
-                    <div class="nom_etud">{etud.html_link_fiche()}</div>
-                </div>
-                <div class="bull_photo"><a href="{
-                    etud.url_fiche()
-                    }">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
-                </div>
-                </div>
-                <div class="warning">Impossible de statuer sur cet étudiant:
-                        il est démissionnaire ou défaillant (voir <a class="stdlink" href="{
-                    etud.url_fiche()}">sa fiche</a>)
-                </div>
-            </div>
-            {navigation_div}
-            </div>
-        """
-            + html_sco_header.sco_footer()
-        )
-
-    deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
-    has_notes_en_attente = deca.has_notes_en_attente()
-    evaluations_a_debloquer = Evaluation.get_evaluations_blocked_for_etud(
-        formsemestre, etud
-    )
-    if has_notes_en_attente or evaluations_a_debloquer:
-        read_only = True
-    if request.method == "POST":
-        if not read_only:
-            deca.record_form(request.form)
-            ScolarNews.add(
-                typ=ScolarNews.NEWS_JURY,
-                obj=formsemestre.id,
-                text=f"""Saisie décision jury dans {formsemestre.html_link_status()}""",
-                url=url_for(
-                    "notes.formsemestre_status",
-                    scodoc_dept=g.scodoc_dept,
-                    formsemestre_id=formsemestre.id,
-                ),
-            )
-            flash("codes enregistrés")
-        return flask.redirect(
-            url_for(
-                "notes.formsemestre_validation_but",
-                scodoc_dept=g.scodoc_dept,
-                formsemestre_id=formsemestre_id,
-                etudid=etudid,
-            )
-        )
-
-    warning = ""
-    if len(deca.niveaux_competences) != len(deca.decisions_rcue_by_niveau):
-        warning += f"""<div class="warning">Attention: {len(deca.niveaux_competences)}
-                niveaux mais {len(deca.decisions_rcue_by_niveau)} regroupements RCUE.</div>"""
-    if (deca.parcour is None) and len(formsemestre.parcours) > 0:
-        warning += (
-            """<div class="warning">L'étudiant n'est pas inscrit à un parcours.</div>"""
-        )
-    if formsemestre.date_fin - datetime.date.today() > datetime.timedelta(days=12):
-        # encore loin de la fin du semestre de départ de ce jury ?
-        warning += f"""<div class="warning">Le semestre S{formsemestre.semestre_id}
-            terminera le {formsemestre.date_fin.strftime(scu.DATE_FMT)}&nbsp;:
-            êtes-vous certain de vouloir enregistrer une décision de jury&nbsp;?
-            </div>"""
-
-    if deca.formsemestre_impair:
-        inscription = deca.formsemestre_impair.etuds_inscriptions.get(etud.id)
-        if (not inscription) or inscription.etat != scu.INSCRIT:
-            etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?")
-            warning += f"""<div class="warning">{etat_ins}
-                en S{deca.formsemestre_impair.semestre_id}</div>"""
-
-    if deca.formsemestre_pair:
-        inscription = deca.formsemestre_pair.etuds_inscriptions.get(etud.id)
-        if (not inscription) or inscription.etat != scu.INSCRIT:
-            etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?")
-            warning += f"""<div class="warning">{etat_ins}
-                en S{deca.formsemestre_pair.semestre_id}</div>"""
-
-    if has_notes_en_attente:
-        warning += f"""<div class="warning-bloquant">{etud.html_link_fiche()
-            } a des notes en ATTente dans les modules suivants.
-            Vous devez régler cela avant de statuer en jury !
-            <ul class="modimpls_att">
-            """
-        for modimpl in deca.get_modimpls_attente():
-            warning += f"""<li><a href="{
-                    url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
-                }" class="stdlink">{modimpl.module.code} {modimpl.module.titre_str()}</a></li>"""
-        warning += "</ul></div>"
-    if evaluations_a_debloquer:
-        links_evals = [
-            f"""<a class="stdlink" href="{url_for(
-                    'notes.evaluation_listenotes', scodoc_dept=g.scodoc_dept, evaluation_id=e.id
-                )}">{e.description} en {e.moduleimpl.module.code}</a>"""
-            for e in evaluations_a_debloquer
-        ]
-        warning += f"""<div class="warning-bloquant">Impossible de statuer sur cet étudiant:
-                il a des notes dans des évaluations qui seront débloquées plus tard:
-                voir {", ".join(links_evals)}
-                """
-
-    if warning:
-        warning = f"""<div class="jury_but_warning jury_but_box">{warning}</div>"""
-    H.append(
-        f"""
-    <div>
-        <div class="bull_head">
-        <div>
-            <div class="titre_parcours">Jury BUT{deca.annee_but}
-            - Parcours {(deca.parcour.libelle if deca.parcour else False) or "non spécifié"}
-            - {deca.annee_scolaire_str()}</div>
-            <div class="nom_etud">{etud.html_link_fiche()}</div>
-        </div>
-        <div class="bull_photo"><a href="{
-            etud.url_fiche()}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
-        </div>
-        </div>
-        {warning}
-    </div>
-
-    <form method="post" class="jury_but_box" id="jury_but">
-    """
-    )
-
-    H.append(jury_but_view.show_etud(deca, read_only=read_only))
-
-    autorisations_idx = deca.get_autorisations_passage()
-    div_autorisations_passage = (
-        f"""
-        <div class="but_autorisations_passage">
-            <span>Autorisé à passer en&nbsp;:</span>
-            { ", ".join( ["S" + str(i) for i in autorisations_idx ] )}
-        </div>
-    """
-        if autorisations_idx
-        else """<div class="but_autorisations_passage but_explanation">
-            pas d'autorisations de passage enregistrées.
-            </div>
-            """
-    )
-    H.append(div_autorisations_passage)
-
-    if read_only:
-        H.append(
-            f"""
-            <div class="but_explanation">
-            {"Vous n'avez pas la permission de modifier ces décisions."
-            if formsemestre.etat
-            else "Semestre verrouillé."}
-            Les champs entourés en vert sont enregistrés.
-            </div>"""
-        )
-    else:
-        erase_span = f"""
-            <a style="margin-left: 16px;" class="stdlink {'' if can_erase else 'link_unauthorized'}"
-            title="{'' if can_erase else 'réservé au responsable'}"
-            href="{
-                url_for("notes.erase_decisions_annee_formation",
-                scodoc_dept=g.scodoc_dept, formation_id=deca.formsemestre.formation.id,
-                etudid=deca.etud.id, annee=deca.annee_but, formsemestre_id=formsemestre_id)
-                if can_erase else ''
-                }"
-            >effacer des décisions de jury</a>
-
-            <a style="margin-left: 16px;" class="stdlink"
-            href="{
-                url_for("notes.formsemestre_validate_previous_ue",
-                scodoc_dept=g.scodoc_dept,
-                etudid=deca.etud.id, formsemestre_id=formsemestre_id)}"
-            >enregistrer des UEs antérieures</a>
-
-            <a style="margin-left: 16px;" class="stdlink"
-            href="{
-                url_for("notes.validate_dut120_etud",
-                scodoc_dept=g.scodoc_dept,
-                etudid=deca.etud.id, formsemestre_id=formsemestre_id)}"
-            >décerner le DUT "120ECTS"</a>
-            """
-        H.append(
-            f"""<div class="but_settings">
-            <input type="checkbox" onchange="enable_manual_codes(this)">
-                <em>permettre la saisie manuelles des codes
-                {"d'année et " if deca.jury_annuel else ""}
-                de niveaux.
-                Dans ce cas, assurez-vous de la cohérence entre les codes d'UE/RCUE/Année !
-                </em>
-            </input>
-            </div>
-
-            <div class="but_buttons">
-                <span><input type="submit" value="Enregistrer ces décisions"></span>
-                <span>{erase_span}</span>
-            </div>
-            """
-        )
-    H.append(navigation_div)
-    H.append("</form>")
-
-    # Affichage cursus BUT
-    but_cursus = cursus_but.EtudCursusBUT(etud, deca.formsemestre.formation)
-    H += [
-        """<div class="jury_but_box">
-        <div class="jury_but_box_title"><b>Niveaux de compétences enregistrés :</b></div>
-        """,
-        render_template(
-            "but/cursus_etud.j2",
-            cursus=but_cursus,
-            scu=scu,
-        ),
-        "</div>",
-    ]
-    H.append(
-        render_template(
-            "but/documentation_codes_jury.j2",
-            nom_univ=f"""Export {sco_preferences.get_preference("InstituteName")
-            or sco_preferences.get_preference("UnivName")
-            or "Apogée"}""",
-            codes=ScoDocSiteConfig.get_codes_apo_dict(),
-        )
-    )
-    H.append(
-        f"""<div class="but_doc_codes but_warning_rcue_cap">
-    {scu.EMO_WARNING} Rappel: pour les redoublants, seules les UE <b>capitalisées</b> (note > 10)
-    lors d'une année précédente peuvent être prise en compte pour former
-    un RCUE (associé à un niveau de compétence du BUT).
-    </div>
-    """
-    )
-    return "\n".join(H) + html_sco_header.sco_footer()
-
-
-@bp.route(
-    "/formsemestre_validation_auto_but/<int:formsemestre_id>", methods=["GET", "POST"]
-)
-@scodoc
-@permission_required(Permission.ScoView)
-def formsemestre_validation_auto_but(formsemestre_id: int = None):
-    "Saisie automatique des décisions de jury BUT"
-    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
-    if not formsemestre.can_edit_jury():
-        raise ScoPermissionDenied(
-            dest_url=url_for(
-                "notes.formsemestre_status",
-                scodoc_dept=g.scodoc_dept,
-                formsemestre_id=formsemestre_id,
-            )
-        )
-    if not formsemestre.formation.is_apc():
-        raise ScoValueError(
-            "formsemestre_validation_auto_but est réservé aux formations APC"
-        )
-
-    formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
-    form = jury_but_forms.FormSemestreValidationAutoBUTForm()
-    if request.method == "POST":
-        if not form.cancel.data:
-            nb_etud_modif, _ = (
-                jury_but_validation_auto.formsemestre_validation_auto_but(formsemestre)
-            )
-            flash(f"Décisions enregistrées ({nb_etud_modif} étudiants modifiés)")
-        return redirect(
-            url_for(
-                "notes.formsemestre_recapcomplet",
-                scodoc_dept=g.scodoc_dept,
-                formsemestre_id=formsemestre_id,
-                mode_jury=1,
-            )
-        )
-    # Avertissement si formsemestre impair
-    formsemestres_suspects = {}
-    if formsemestre.semestre_id % 2:
-        _, decas = jury_but_validation_auto.formsemestre_validation_auto_but(
-            formsemestre, dry_run=True
-        )
-        # regarde si il y a des semestres pairs postérieurs qui ne soient pas bloqués
-        formsemestres_suspects = {
-            deca.formsemestre_pair.id: deca.formsemestre_pair
-            for deca in decas
-            if deca.formsemestre_pair
-            and deca.formsemestre_pair.date_debut > formsemestre.date_debut
-            and not deca.formsemestre_pair.block_moyennes
-        }
-
-    return render_template(
-        "but/formsemestre_validation_auto_but.j2",
-        form=form,
-        formsemestres_suspects=formsemestres_suspects,
-        sco=ScoData(formsemestre=formsemestre),
-        title="Calcul automatique jury BUT",
-    )
-
-
-@bp.route(
-    "/formsemestre_validate_previous_ue/<int:formsemestre_id>/<int:etudid>",
-    methods=["GET", "POST"],
-)
-@scodoc
-@permission_required(Permission.ScoView)
-def formsemestre_validate_previous_ue(formsemestre_id, etudid=None):
-    "Form. saisie UE validée hors ScoDoc"
-    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
-    if not formsemestre.can_edit_jury():
-        raise ScoPermissionDenied(
-            dest_url=url_for(
-                "notes.formsemestre_status",
-                scodoc_dept=g.scodoc_dept,
-                formsemestre_id=formsemestre_id,
-            )
-        )
-    etud: Identite = (
-        Identite.query.filter_by(id=etudid)
-        .join(FormSemestreInscription)
-        .filter_by(formsemestre_id=formsemestre_id)
-        .first_or_404()
-    )
-
-    return sco_formsemestre_validation.formsemestre_validate_previous_ue(
-        formsemestre, etud
-    )
-
-
 sco_publish(
     "/formsemestre_ext_create_form",
     sco_formsemestre_exterieurs.formsemestre_ext_create_form,
@@ -2727,150 +2196,6 @@ sco_publish(
 )
 
 
-@bp.route("/formsemestre_ext_edit_ue_validations", methods=["GET", "POST"])
-@scodoc
-@permission_required(Permission.ScoView)
-@scodoc7func
-def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid=None):
-    "Form. edition UE semestre extérieur"
-    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
-    if not formsemestre.can_edit_jury():
-        raise ScoPermissionDenied(
-            dest_url=url_for(
-                "notes.formsemestre_status",
-                scodoc_dept=g.scodoc_dept,
-                formsemestre_id=formsemestre_id,
-            )
-        )
-
-    return sco_formsemestre_exterieurs.formsemestre_ext_edit_ue_validations(
-        formsemestre_id, etudid
-    )
-
-
-@bp.route("/formsemestre_validation_auto")
-@scodoc
-@permission_required(Permission.ScoView)
-@scodoc7func
-def formsemestre_validation_auto(formsemestre_id):
-    "Formulaire saisie automatisee des decisions d'un semestre"
-    formsemestre: FormSemestre = FormSemestre.query.filter_by(
-        id=formsemestre_id, dept_id=g.scodoc_dept_id
-    ).first_or_404()
-    if not formsemestre.can_edit_jury():
-        raise ScoPermissionDenied(
-            dest_url=url_for(
-                "notes.formsemestre_status",
-                scodoc_dept=g.scodoc_dept,
-                formsemestre_id=formsemestre_id,
-            )
-        )
-
-    if formsemestre.formation.is_apc():
-        return redirect(
-            url_for(
-                "notes.formsemestre_validation_auto_but",
-                scodoc_dept=g.scodoc_dept,
-                formsemestre_id=formsemestre.id,
-            )
-        )
-    return sco_formsemestre_validation.formsemestre_validation_auto(formsemestre_id)
-
-
-@bp.route("/do_formsemestre_validation_auto")
-@scodoc
-@permission_required(Permission.ScoView)
-@scodoc7func
-def do_formsemestre_validation_auto(formsemestre_id):
-    "Formulaire saisie automatisee des decisions d'un semestre"
-    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
-    if not formsemestre.can_edit_jury():
-        raise ScoPermissionDenied(
-            dest_url=url_for(
-                "notes.formsemestre_status",
-                scodoc_dept=g.scodoc_dept,
-                formsemestre_id=formsemestre_id,
-            )
-        )
-
-    return sco_formsemestre_validation.do_formsemestre_validation_auto(formsemestre_id)
-
-
-@bp.route("/formsemestre_validation_suppress_etud", methods=["GET", "POST"])
-@scodoc
-@permission_required(Permission.ScoView)
-@scodoc7func
-def formsemestre_validation_suppress_etud(
-    formsemestre_id, etudid, dialog_confirmed=False
-):
-    """Suppression des décisions de jury pour un étudiant."""
-    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
-    if not formsemestre.can_edit_jury():
-        raise ScoPermissionDenied(
-            dest_url=url_for(
-                "notes.formsemestre_status",
-                scodoc_dept=g.scodoc_dept,
-                formsemestre_id=formsemestre_id,
-            )
-        )
-    etud = Identite.get_etud(etudid)
-    if formsemestre.formation.is_apc():
-        next_url = url_for(
-            "scolar.fiche_etud",
-            scodoc_dept=g.scodoc_dept,
-            etudid=etudid,
-        )
-    else:
-        next_url = url_for(
-            "notes.formsemestre_validation_etud_form",
-            scodoc_dept=g.scodoc_dept,
-            formsemestre_id=formsemestre_id,
-            etudid=etudid,
-        )
-    if not dialog_confirmed:
-        d = sco_bulletins_json.dict_decision_jury(
-            etud, formsemestre, with_decisions=True
-        )
-
-        descr_ues = [f"{u['acronyme']}: {u['code']}" for u in d.get("decision_ue", [])]
-        dec_annee = d.get("decision_annee")
-        if dec_annee:
-            descr_annee = dec_annee.get("code", "-")
-        else:
-            descr_annee = "-"
-
-        existing = f"""
-        <ul>
-        <li>Semestre : {d.get("decision", {"code":"-"})['code'] or "-"}</li>
-        <li>Année BUT: {descr_annee}</li>
-        <li>UEs : {", ".join(descr_ues)}</li>
-        <li>RCUEs: {len(d.get("decision_rcue", []))} décisions</li>
-        <li>Autorisations: {descr_autorisations(ScolarAutorisationInscription.query.filter_by(origin_formsemestre_id=formsemestre_id,
-            etudid=etudid))}
-        </ul>
-        """
-        return scu.confirm_dialog(
-            f"""<h2>Confirmer la suppression des décisions du semestre
-            {formsemestre.titre_mois()} pour {etud.nomprenom}
-            </h2>
-            <p>Cette opération est irréversible.</p>
-            <div>
-            {existing}
-            </div>
-            """,
-            OK="Supprimer",
-            dest_url="",
-            cancel_url=next_url,
-            parameters={"etudid": etudid, "formsemestre_id": formsemestre_id},
-        )
-
-    sco_formsemestre_validation.formsemestre_validation_suppress_etud(
-        formsemestre_id, etudid
-    )
-    flash("Décisions supprimées")
-    return flask.redirect(next_url)
-
-
 # ------------- PV de JURY et archives
 sco_publish(
     "/formsemestre_pvjury", sco_pv_forms.formsemestre_pvjury, Permission.ScoView
@@ -2878,192 +2203,6 @@ sco_publish(
 
 sco_publish("/pvjury_page_but", jury_but_pv.pvjury_page_but, Permission.ScoView)
 
-
-@bp.route("/formsemestre_saisie_jury")
-@scodoc
-@permission_required(Permission.ScoView)
-@scodoc7func
-def formsemestre_saisie_jury(formsemestre_id: int, selected_etudid: int = None):
-    """Page de saisie: liste des étudiants et lien vers page jury
-    sinon, redirect vers page recap en mode jury
-    """
-    return redirect(
-        url_for(
-            "notes.formsemestre_recapcomplet",
-            scodoc_dept=g.scodoc_dept,
-            formsemestre_id=formsemestre_id,
-            mode_jury=1,
-        )
-    )
-
-
-@bp.route(
-    "/formsemestre_jury_erase/<int:formsemestre_id>",
-    methods=["GET", "POST"],
-    defaults={"etudid": None},
-)
-@bp.route(
-    "/formsemestre_jury_erase/<int:formsemestre_id>/<int:etudid>",
-    methods=["GET", "POST"],
-)
-@scodoc
-@permission_required(Permission.ScoView)
-def formsemestre_jury_erase(formsemestre_id: int, etudid: int = None):
-    """Supprime toutes les décisions de jury (classique ou BUT) pour cette année.
-    Si l'étudiant n'est pas spécifié, efface les décisions de tous les inscrits.
-    En BUT, si only_one_sem n'efface que pour le formsemestre indiqué, pas les deux de l'année.
-    En classique, n'affecte que les décisions issues de ce formsemestre.
-    """
-    only_one_sem = int(request.args.get("only_one_sem") or False)
-    formsemestre: FormSemestre = FormSemestre.query.filter_by(
-        id=formsemestre_id, dept_id=g.scodoc_dept_id
-    ).first_or_404()
-    if not formsemestre.can_edit_jury():
-        raise ScoPermissionDenied(
-            dest_url=url_for(
-                "notes.formsemestre_status",
-                scodoc_dept=g.scodoc_dept,
-                formsemestre_id=formsemestre_id,
-            )
-        )
-    is_apc = formsemestre.formation.is_apc()
-    if etudid is None:
-        etud = None
-        etuds = formsemestre.get_inscrits(include_demdef=True)
-        dest_url = url_for(
-            "notes.formsemestre_recapcomplet",
-            scodoc_dept=g.scodoc_dept,
-            formsemestre_id=formsemestre_id,
-            mode_jury=1,
-        )
-    else:
-        etud = Identite.get_etud(etudid)
-        etuds = [etud]
-        endpoint = (
-            "notes.formsemestre_validation_but"
-            if is_apc
-            else "notes.formsemestre_validation_etud_form"
-        )
-        dest_url = url_for(
-            endpoint,
-            scodoc_dept=g.scodoc_dept,
-            formsemestre_id=formsemestre_id,
-            etudid=etudid,
-        )
-    if request.method == "POST":
-        with sco_cache.DeferredSemCacheManager():
-            for etud in etuds:
-                if is_apc:
-                    deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
-                    deca.erase(only_one_sem=only_one_sem)
-                else:
-                    sco_formsemestre_validation.formsemestre_validation_suppress_etud(
-                        formsemestre.id, etud.id
-                    )
-                log(f"formsemestre_jury_erase({formsemestre_id}, {etud.id})")
-        flash(
-            (
-                "décisions de jury du semestre effacées"
-                if (only_one_sem or is_apc)
-                else "décisions de jury des semestres de l'année BUT effacées"
-            )
-            + f" pour {len(etuds)} étudiant{'s' if len(etuds) > 1 else ''}"
-        )
-        return redirect(dest_url)
-
-    return render_template(
-        "confirm_dialog.j2",
-        title=f"""Effacer les validations de jury {
-            ("de " + etud.nomprenom)
-            if etud
-            else ("des " + str(len(etuds)) + " étudiants inscrits dans ce semestre")
-            } ?""",
-        explanation=(
-            (
-                f"""Les validations d'UE et autorisations de passage
-            du semestre S{formsemestre.semestre_id} seront effacées."""
-                if (only_one_sem or is_apc)
-                else """Les validations de toutes les UE, RCUE (compétences) et année
-        issues de cette année scolaire seront effacées.
-        """
-            )
-            + """
-        <p>Les décisions des années scolaires précédentes ne seront pas modifiées.</p>
-        """
-            + """
-        <p>Efface aussi toutes les validations concernant l'année BUT de ce semestre,
-        même si elles ont été acquises ailleurs, ainsi que les validations de DUT en 120 ECTS
-        obtenues après BUT1/BUT2.
-        </p>
-        """
-            if is_apc
-            else ""
-            + """
-        <div class="warning">Cette opération est irréversible !
-        A n'utiliser que dans des cas exceptionnels, vérifiez bien tous les étudiants ensuite.
-        </div>
-        """
-        ),
-        cancel_url=dest_url,
-    )
-
-
-@bp.route(
-    "/erase_decisions_annee_formation/<int:etudid>/<int:formation_id>/<int:annee>",
-    methods=["GET", "POST"],
-)
-@scodoc
-@permission_required(Permission.EtudInscrit)
-def erase_decisions_annee_formation(etudid: int, formation_id: int, annee: int):
-    """Efface toute les décisions d'une année pour cet étudiant"""
-    etud: Identite = Identite.query.get_or_404(etudid)
-    formation: Formation = Formation.query.filter_by(
-        id=formation_id, dept_id=g.scodoc_dept_id
-    ).first_or_404()
-    if request.method == "POST":
-        jury.erase_decisions_annee_formation(etud, formation, annee, delete=True)
-        flash("Décisions de jury effacées")
-        return redirect(
-            url_for(
-                "scolar.fiche_etud",
-                scodoc_dept=g.scodoc_dept,
-                etudid=etud.id,
-            )
-        )
-    validations = jury.erase_decisions_annee_formation(etud, formation, annee)
-    formsemestre_origine_id = request.args.get("formsemestre_id")
-    formsemestre_origine = (
-        FormSemestre.query.get_or_404(formsemestre_origine_id)
-        if formsemestre_origine_id
-        else None
-    )
-    return render_template(
-        "jury/erase_decisions_annee_formation.j2",
-        annee=annee,
-        cancel_url=url_for(
-            "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id
-        ),
-        etud=etud,
-        formation=formation,
-        formsemestre_origine=formsemestre_origine,
-        validations=validations,
-        sco=ScoData(),
-        title=f"Effacer décisions de jury {etud.nom} - année {annee}",
-    )
-
-
-@bp.route(
-    "/jury_delete_manual/<int:etudid>",
-    methods=["GET", "POST"],
-)
-@scodoc
-@permission_required(Permission.EtudInscrit)
-def jury_delete_manual(etudid: int):
-    """Efface toute les décisions d'une année pour cet étudiant"""
-    etud: Identite = Identite.query.get_or_404(etudid)
-    return jury_edit_manual.jury_delete_manual(etud)
-
-
 sco_publish(
     "/formsemestre_lettres_individuelles",
     sco_pv_forms.formsemestre_lettres_individuelles,
@@ -3306,12 +2445,12 @@ def check_sem_integrity(formsemestre_id, fix=False):
     if not bad_ue and not bad_sem:
         H.append("<p>Aucun problème à signaler !</p>")
     else:
-        log("check_sem_integrity: problem detected: formations_set=%s" % formations_set)
+        log(f"check_sem_integrity: problem detected: formations_set={formations_set}")
         if sem["formation_id"] in formations_set:
             formations_set.remove(sem["formation_id"])
         if len(formations_set) == 1:
             if fix:
-                log("check_sem_integrity: trying to fix %s" % formsemestre_id)
+                log(f"check_sem_integrity: trying to fix {formsemestre_id}")
                 formation_id = formations_set.pop()
                 if sem["formation_id"] != formation_id:
                     sem["formation_id"] = formation_id
@@ -3319,11 +2458,11 @@ def check_sem_integrity(formsemestre_id, fix=False):
                 H.append("""<p class="alert">Problème réparé: vérifiez</p>""")
             else:
                 H.append(
-                    """
+                    f"""
                 <p class="alert">Problème détecté réparable:
-                <a href="check_sem_integrity?formsemestre_id=%s&fix=1">réparer maintenant</a></p>
+                <a href="check_sem_integrity?formsemestre_id={
+                    formsemestre_id}&fix=1">réparer maintenant</a></p>
                 """
-                    % (formsemestre_id,)
                 )
         else:
             H.append("""<p class="alert">Problème détecté !</p>""")
diff --git a/misc/example-api-python2.py b/misc/example-api-python2.py
deleted file mode 100644
index 03fd26beb2320178f11059645089a8d1247e125c..0000000000000000000000000000000000000000
--- a/misc/example-api-python2.py
+++ /dev/null
@@ -1,99 +0,0 @@
-#!/usr/bin/env python
-# -*- mode: python -*-
-# -*- coding: utf-8 -*-
-
-"""Exemple connexion sur ScoDoc et utilisation de l'API
-
-Attention: cet exemple est en Python 2.
-Voir example-api-1.py pour une version en Python3 plus moderne.
-"""
-
-import urllib, urllib2
-
-# A modifier pour votre serveur:
-BASEURL = "https://scodoc.xxx.net/ScoDoc/RT/Scolarite"
-USER = "XXX"
-PASSWORD = "XXX"
-
-values = {
-    "__ac_name": USER,
-    "__ac_password": PASSWORD,
-}
-
-# Configure memorisation des cookies:
-opener = urllib2.build_opener(urllib2.HTTPCookieProcessor())
-urllib2.install_opener(opener)
-
-data = urllib.urlencode(values)
-
-req = urllib2.Request(BASEURL, data)  # this is a POST http request
-response = urllib2.urlopen(req)
-
-# --- Use API
-
-# Affiche la liste des formations en format XML
-req = urllib2.Request(BASEURL + "/Notes/formation_list?fmt=xml")
-response = urllib2.urlopen(req)
-print response.read()[:100]  # limite aux 100 premiers caracteres...
-
-# Recupere la liste de tous les semestres:
-req = urllib2.Request(BASEURL + "/Notes/formsemestre_list?fmt=json")  # format json
-response = urllib2.urlopen(req)
-js_data = response.read()
-
-# Plus amusant: va retrouver le bulletin de notes du premier etudiant (au hasard donc) du premier semestre (au hasard aussi)
-try:
-    import json  # Attention: ceci demande Python >= 2.6
-except:
-    import simplejson as json  # python2.4 with simplejson installed
-
-data = json.loads(js_data)  # decode la reponse JSON
-if not data:
-    print "Aucun semestre !"
-else:
-    formsemestre_id = str(data[0]["formsemestre_id"])
-    # Obtient la liste des groupes:
-    req = urllib2.Request(
-        BASEURL
-        + "/Notes/formsemestre_partition_list?fmt=json&formsemestre_id="
-        + str(formsemestre_id)
-    )  # format json
-    response = urllib2.urlopen(req)
-    js_data = response.read()
-    data = json.loads(js_data)
-    group_id = data[0]["group"][0][
-        "group_id"
-    ]  # premier groupe (normalement existe toujours)
-    # Liste les étudiants de ce groupe:
-    req = urllib2.Request(
-        BASEURL + "/Notes/group_list?fmt=json&with_codes=1&group_id=" + str(group_id)
-    )  # format json
-    response = urllib2.urlopen(req)
-    js_data = response.read()
-    data = json.loads(js_data)
-    # Le code du premier étudiant:
-    if not data:
-        print ("pas d'etudiants dans ce semestre !")
-    else:
-        etudid = data[0]["etudid"]
-        # Récupère bulletin de notes:
-        req = urllib2.Request(
-            BASEURL
-            + "/Notes/formsemestre_bulletinetud?formsemestre_id="
-            + str(formsemestre_id)
-            + "&etudid="
-            + str(etudid)
-            + "&fmt=xml"
-        )  # format XML ici !
-        response = urllib2.urlopen(req)
-        xml_bulletin = response.read()
-        print "----- Bulletin de notes en XML:"
-        print xml_bulletin
-        # Récupère la moyenne générale:
-        import xml.dom.minidom
-
-        doc = xml.dom.minidom.parseString(xml_bulletin)
-        moy = doc.getElementsByTagName("note")[0].getAttribute(
-            "value"
-        )  # une chaine unicode
-        print "\nMoyenne generale: ", moy