diff --git a/app/comp/res_common.py b/app/comp/res_common.py
index ed2232713ec074d12623ed4e137b1c34c53a7efc..112cc6493c1ed7ff49550bc9d7f8d1525213b1c6 100644
--- a/app/comp/res_common.py
+++ b/app/comp/res_common.py
@@ -23,6 +23,7 @@ from app.models import ModuleImpl, ModuleImplInscription
 from app.models.ues import UniteEns
 from app.scodoc.sco_cache import ResultatsSemestreCache
 from app.scodoc.sco_codes_parcours import UE_SPORT, DEF, DEM
+from app.scodoc import sco_evaluation_db
 from app.scodoc.sco_exceptions import ScoValueError
 from app.scodoc import sco_groups
 from app.scodoc import sco_users
@@ -387,7 +388,7 @@ class ResultatsSemestre(ResultatsCache):
 
     # --- TABLEAU RECAP
 
-    def get_table_recap(self, convert_values=False):
+    def get_table_recap(self, convert_values=False, include_evaluations=False):
         """Result: tuple avec
         - rows: liste de dicts { column_id : value }
         - titles: { column_id : title }
@@ -457,6 +458,11 @@ class ResultatsSemestre(ResultatsCache):
             idx = 0  # index de la colonne
             etud = Identite.query.get(etudid)
             row = {"etudid": etudid}
+            # --- Codes (seront cachés, mais exportés en excel)
+            idx = add_cell(row, "etudid", "etudid", etudid, "codes", idx)
+            idx = add_cell(
+                row, "code_nip", "code_nip", etud.code_nip or "", "codes", idx
+            )
             # --- Rang
             idx = add_cell(
                 row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang", idx
@@ -618,11 +624,14 @@ class ResultatsSemestre(ResultatsCache):
 
         self._recap_add_partitions(rows, titles)
         self._recap_add_admissions(rows, titles)
+
         # tri par rang croissant
         rows.sort(key=lambda e: e["_rang_order"])
 
         # INFOS POUR FOOTER
         bottom_infos = self._recap_bottom_infos(ues_sans_bonus, modimpl_ids, fmt_note)
+        if include_evaluations:
+            self._recap_add_evaluations(rows, titles, bottom_infos)
 
         # Ajoute style "col_empty" aux colonnes de modules vides
         for col_id in titles:
@@ -641,7 +650,9 @@ class ResultatsSemestre(ResultatsCache):
             row["moy_gen"] = row.get("moy_gen", "")
             row["_moy_gen_class"] = "col_moy_gen"
             # titre de la ligne:
-            row["prenom"] = row["nom_short"] = bottom_line.capitalize()
+            row["prenom"] = row["nom_short"] = (
+                row.get(f"_title", "") or bottom_line.capitalize()
+            )
             row["_tr_class"] = bottom_line.lower() + (
                 (" " + row["_tr_class"]) if "_tr_class" in row else ""
             )
@@ -656,53 +667,58 @@ class ResultatsSemestre(ResultatsCache):
 
     def _recap_bottom_infos(self, ues, modimpl_ids: set, fmt_note) -> dict:
         """Les informations à mettre en bas de la table: min, max, moy, ECTS"""
-        row_min, row_max, row_moy, row_coef, row_ects = (
-            {"_tr_class": "bottom_info"},
+        row_min, row_max, row_moy, row_coef, row_ects, row_apo = (
+            {"_tr_class": "bottom_info", "_title": "Min."},
             {"_tr_class": "bottom_info"},
             {"_tr_class": "bottom_info"},
             {"_tr_class": "bottom_info"},
             {"_tr_class": "bottom_info"},
+            {"_tr_class": "bottom_info", "_title": "Code Apogée"},
         )
         # --- ECTS
         for ue in ues:
-            row_ects[f"moy_ue_{ue.id}"] = ue.ects
-            row_ects[f"_moy_ue_{ue.id}_class"] = "col_ue"
+            colid = f"moy_ue_{ue.id}"
+            row_ects[colid] = ue.ects
+            row_ects[f"_{colid}_class"] = "col_ue"
             # style cases vides pour borders verticales
-            row_coef[f"moy_ue_{ue.id}"] = ""
-            row_coef[f"_moy_ue_{ue.id}_class"] = "col_ue"
+            row_coef[colid] = ""
+            row_coef[f"_{colid}_class"] = "col_ue"
+            # row_apo[colid] = ue.code_apogee or ""
         row_ects["moy_gen"] = sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT])
         row_ects["_moy_gen_class"] = "col_moy_gen"
 
-        # --- MIN, MAX, MOY
+        # --- MIN, MAX, MOY, APO
 
         row_min["moy_gen"] = fmt_note(self.etud_moy_gen.min())
         row_max["moy_gen"] = fmt_note(self.etud_moy_gen.max())
         row_moy["moy_gen"] = fmt_note(self.etud_moy_gen.mean())
         for ue in ues:
-            col_id = f"moy_ue_{ue.id}"
-            row_min[col_id] = fmt_note(self.etud_moy_ue[ue.id].min())
-            row_max[col_id] = fmt_note(self.etud_moy_ue[ue.id].max())
-            row_moy[col_id] = fmt_note(self.etud_moy_ue[ue.id].mean())
-            row_min[f"_{col_id}_class"] = "col_ue"
-            row_max[f"_{col_id}_class"] = "col_ue"
-            row_moy[f"_{col_id}_class"] = "col_ue"
+            colid = f"moy_ue_{ue.id}"
+            row_min[colid] = fmt_note(self.etud_moy_ue[ue.id].min())
+            row_max[colid] = fmt_note(self.etud_moy_ue[ue.id].max())
+            row_moy[colid] = fmt_note(self.etud_moy_ue[ue.id].mean())
+            row_min[f"_{colid}_class"] = "col_ue"
+            row_max[f"_{colid}_class"] = "col_ue"
+            row_moy[f"_{colid}_class"] = "col_ue"
+            row_apo[colid] = ue.code_apogee or ""
 
             for modimpl in self.formsemestre.modimpls_sorted:
                 if modimpl.id in modimpl_ids:
-                    col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
+                    colid = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
                     if self.is_apc:
                         coef = self.modimpl_coefs_df[modimpl.id][ue.id]
                     else:
                         coef = modimpl.module.coefficient or 0
-                    row_coef[col_id] = fmt_note(coef)
+                    row_coef[colid] = fmt_note(coef)
                     notes = self.modimpl_notes(modimpl.id, ue.id)
-                    row_min[col_id] = fmt_note(np.nanmin(notes))
-                    row_max[col_id] = fmt_note(np.nanmax(notes))
+                    row_min[colid] = fmt_note(np.nanmin(notes))
+                    row_max[colid] = fmt_note(np.nanmax(notes))
                     moy = np.nanmean(notes)
-                    row_moy[col_id] = fmt_note(moy)
+                    row_moy[colid] = fmt_note(moy)
                     if np.isnan(moy):
                         # aucune note dans ce module
-                        row_moy[f"_{col_id}_class"] = "col_empty"
+                        row_moy[f"_{colid}_class"] = "col_empty"
+                    row_apo[colid] = modimpl.module.code_apogee or ""
 
         return {  # { key : row } avec key = min, max, moy, coef
             "min": row_min,
@@ -710,6 +726,7 @@ class ResultatsSemestre(ResultatsCache):
             "moy": row_moy,
             "coef": row_coef,
             "ects": row_ects,
+            "apo": row_apo,
         }
 
     def _recap_etud_groups_infos(self, etudid: int, row: dict, titles: dict):
@@ -803,3 +820,48 @@ class ResultatsSemestre(ResultatsCache):
                     row[f"{cid}"] = gr_name
                     row[f"_{cid}_class"] = klass
             first_partition = False
+
+    def _recap_add_evaluations(
+        self, rows: list[dict], titles: dict, bottom_infos: dict
+    ):
+        """Ajoute les colonnes avec les notes aux évaluations
+        rows est une liste de dict avec une clé "etudid"
+        Les colonnes ont la classe css "evaluation"
+        """
+        # nouvelle ligne pour description évaluations:
+        bottom_infos["descr_evaluation"] = {
+            "_tr_class": "bottom_info",
+            "_title": "Description évaluation",
+        }
+        first = True
+        for modimpl in self.formsemestre.modimpls_sorted:
+            evals = self.modimpls_results[modimpl.id].get_evaluations_completes(modimpl)
+            eval_index = len(evals) - 1
+            inscrits = {i.etudid for i in modimpl.inscriptions}
+            klass = "evaluation first" if first else "evaluation"
+            first = False
+            for i, e in enumerate(evals):
+                cid = f"eval_{e.id}"
+                titles[
+                    cid
+                ] = f'{modimpl.module.code} {eval_index} {e.jour.isoformat() if e.jour else ""}'
+                titles[f"_{cid}_class"] = klass
+                titles[f"_{cid}_col_order"] = 9000 + i  # à droite
+                eval_index -= 1
+                notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
+                    e.evaluation_id
+                )
+                for row in rows:
+                    etudid = row["etudid"]
+                    if etudid in inscrits:
+                        if etudid in notes_db:
+                            val = notes_db[etudid]["value"]
+                        else:
+                            # Note manquante mais prise en compte immédiate: affiche ATT
+                            val = scu.NOTES_ATTENTE
+                        row[cid] = scu.fmt_note(val)
+                        row[f"_{cid}_class"] = klass
+                bottom_infos["coef"][cid] = e.coefficient
+                bottom_infos["min"][cid] = "0"
+                bottom_infos["max"][cid] = scu.fmt_note(e.note_max)
+                bottom_infos["descr_evaluation"][cid] = e.description or ""
diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py
index 685707b9937d14e04d449e9827b0401692756350..7d903f6cfeccddf6edfa44744b5940b38ccb4573 100644
--- a/app/scodoc/sco_formsemestre_edit.py
+++ b/app/scodoc/sco_formsemestre_edit.py
@@ -1314,7 +1314,7 @@ def _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new)
 
 def formsemestre_delete(formsemestre_id):
     """Delete a formsemestre (affiche avertissements)"""
-    sem = sco_formsemestre.get_formsemestre(formsemestre_id)
+    sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
     F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
     H = [
         html_sco_header.html_sem_header("Suppression du semestre"),
diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py
index a00e7df39fd25d11b54a0a33b95e61ac72444888..f7b365ed01b6027e16f57de487bb1af45ac39a67 100644
--- a/app/scodoc/sco_recapcomplet.py
+++ b/app/scodoc/sco_recapcomplet.py
@@ -130,12 +130,10 @@ def formsemestre_recapcomplet(
                 '<select name="tabformat" onchange="document.f.submit()" class="noprint">'
             )
             for (format, label) in (
-                ("html", "HTML"),
-                ("xls", "Fichier tableur (Excel)"),
-                ("xlsall", "Fichier tableur avec toutes les évals"),
-                ("csv", "Fichier tableur (CSV)"),
-                ("xml", "Fichier XML"),
-                ("json", "JSON"),
+                ("html", "Tableau"),
+                ("evals", "Avec toutes les évaluations"),
+                ("xml", "Bulletins XML (obsolète)"),
+                ("json", "Bulletins JSON"),
             ):
                 if format == tabformat:
                     selected = " selected"
@@ -149,7 +147,6 @@ def formsemestre_recapcomplet(
                 href="{url_for('notes.formsemestre_bulletins_pdf', 
                 scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)}">
                 ici avoir le classeur papier</a>)
-                <div class="warning">Nouvelle version: export excel inachevés. Merci de signaler les problèmes.</div>
                 """
             )
 
@@ -221,9 +218,11 @@ def do_formsemestre_recapcomplet(
 ):
     """Calcule et renvoie le tableau récapitulatif."""
     formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
-    if format == "html" and not modejury:
+    if (format == "html" or format == "evals") and not modejury:
         res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
-        data, filename = gen_formsemestre_recapcomplet_html(formsemestre, res)
+        data, filename = gen_formsemestre_recapcomplet_html(
+            formsemestre, res, include_evaluations=(format == "evals")
+        )
     else:
         data, filename, format = make_formsemestre_recapcomplet(
             formsemestre_id=formsemestre_id,
@@ -239,7 +238,7 @@ def do_formsemestre_recapcomplet(
             force_publishing=force_publishing,
         )
     # ---
-    if format == "xml" or format == "html":
+    if format == "xml" or format == "html" or format == "evals":
         return data
     elif format == "csv":
         return scu.send_file(data, filename=filename, mime=scu.CSV_MIMETYPE)
@@ -251,12 +250,12 @@ def do_formsemestre_recapcomplet(
             js, filename=filename, suffix=scu.JSON_SUFFIX, mime=scu.JSON_MIMETYPE
         )
     else:
-        raise ValueError("unknown format %s" % format)
+        raise ValueError(f"unknown format {format}")
 
 
 def make_formsemestre_recapcomplet(
     formsemestre_id=None,
-    format="html",  # html, xml, xls, xlsall, json
+    format="html",  # html, evals, xml, json
     hidemodules=False,  # ne pas montrer les modules (ignoré en XML)
     hidebac=False,  # pas de colonne Bac (ignoré en XML)
     xml_nodate=False,  # format XML sans dates (sert pour debug cache: comparaison de XML)
@@ -1029,19 +1028,26 @@ def _gen_row(keys: list[str], row, elt="td"):
 
 
 def gen_formsemestre_recapcomplet_html(
-    formsemestre: FormSemestre, res: NotesTableCompat
+    formsemestre: FormSemestre, res: NotesTableCompat, include_evaluations=False
 ):
     """Construit table recap pour le BUT
     Return: data, filename
     """
-    rows, footer_rows, titles, column_ids = res.get_table_recap(convert_values=True)
+    rows, footer_rows, titles, column_ids = res.get_table_recap(
+        convert_values=True, include_evaluations=include_evaluations
+    )
     if not rows:
         return (
             '<div class="table_recap"><div class="message">aucun étudiant !</div></div>',
             "",
         )
+    filename = scu.sanitize_filename(
+        f"""recap-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}"""
+    )
     H = [
-        f"""<div class="table_recap"><table class="table_recap {'apc' if formsemestre.formation.is_apc() else 'classic'}">"""
+        f"""<div class="table_recap"><table class="table_recap {
+            'apc' if formsemestre.formation.is_apc() else 'classic'}"
+            data-filename="{filename}">"""
     ]
     # header
     H.append(
@@ -1068,7 +1074,4 @@ def gen_formsemestre_recapcomplet_html(
     </div>
     """
     )
-    return (
-        "".join(H),
-        f'recap-{formsemestre.titre_num().replace(" ", "_")}-{time.strftime("%d-%m-%Y")}',
-    )  # suffix ?
+    return ("".join(H), filename)  # suffix ?
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index f393ac46356ad96c965f161de0b06aa07d2e5ea0..88cc9ba69b5e75c28df17bf816bdf657d8f17614 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -3703,4 +3703,29 @@ table.table_recap tr.dem td {
 table.table_recap tr.def td {
   color: rgb(121, 74, 74);
   font-style: italic;
+}
+
+table.table_recap td.evaluation,
+table.table_recap tr.descr_evaluation {
+  font-size: 90%;
+  color: rgb(4, 16, 159);
+}
+
+table.table_recap tr.descr_evaluation {
+  vertical-align: top;
+}
+
+table.table_recap tr.apo {
+  font-size: 75%;
+  font-family: monospace;
+}
+
+table.table_recap tr.apo td {
+  border: 1px solid gray;
+  background-color: #d8f5fe;
+}
+
+table.table_recap td.evaluation.first,
+table.table_recap th.evaluation.first {
+  border-left: 2px solid rgb(4, 16, 159);
 }
\ No newline at end of file
diff --git a/app/static/js/table_recap.js b/app/static/js/table_recap.js
index 803daee218967a4f94d252d2d3524053e88c9921..322b2764cfaf045991545bde791707ce7ef13a62 100644
--- a/app/static/js/table_recap.js
+++ b/app/static/js/table_recap.js
@@ -90,13 +90,37 @@ $(function () {
                 colReorder: true,
                 "columnDefs": [
                     {
-                        // cache le détail de l'identité, les groupes, les colonnes admission et les vides
-                        "targets": ["identite_detail", "partition_aux", "admission", "col_empty"],
-                        "visible": false,
+                        // cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides
+                        targets: ["codes", "identite_detail", "partition_aux", "admission", "col_empty"],
+                        visible: false,
+                    },
+                    {
+                        // Elimine les 0 à gauche pour les exports excel et les "copy"
+                        targets: ["col_mod", "col_moy_gen", "col_ue"],
+                        render: function (data, type, row) {
+                            return type === 'export' ? data.replace(/0(\d\..*)/, '$1') : data;
+                        }
+                    },
+                    {
+                        // Elimine les décorations (fleches bonus/malus) pour les exports
+                        targets: ["col_ue_bonus", "col_malus"],
+                        render: function (data, type, row) {
+                            return type === 'export' ? data.replace(/.*(\d\d\.\d\d)/, '$1').replace(/0(\d\..*)/, '$1') : data;
+                        }
                     },
                 ],
                 dom: 'Bfrtip',
-                buttons: ['copy', 'excel', 'pdf',
+                buttons: [
+                    {
+                        extend: 'copyHtml5',
+                        text: 'Copier',
+                        exportOptions: { orthogonal: 'export' }
+                    },
+                    {
+                        extend: 'excelHtml5',
+                        exportOptions: { orthogonal: 'export' },
+                        title: document.querySelector('table.table_recap').dataset.filename
+                    },
                     {
                         extend: 'collection',
                         text: 'Colonnes affichées',