diff --git a/app/but/bulletin_but_pdf.py b/app/but/bulletin_but_pdf.py
index f849cebc76453c588b9cde5cdb3999a28b5a5616..e76aa62a529054dd5dfed60247ce2edef66b8263 100644
--- a/app/but/bulletin_but_pdf.py
+++ b/app/but/bulletin_but_pdf.py
@@ -73,6 +73,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
                 html_class="notes_bulletin",
                 html_class_ignore_default=True,
                 html_with_td_classes=True,
+                table_id="bul-table",
             )
             table_objects = table.gen(fmt=fmt)
             objects += table_objects
diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py
index 3c08326cd8693a81c26fb0fb8c322d96c37b3429..a58125f9f496cc77fa58a332da9b270f76ab2c32 100644
--- a/app/scodoc/gen_tables.py
+++ b/app/scodoc/gen_tables.py
@@ -176,6 +176,7 @@ class GenTable:
         self.xml_link = xml_link
         # HTML parameters:
         if not table_id:  # random id
+            log("Warning: GenTable() called without table_id")
             self.table_id = "gt_" + str(random.randint(0, 1000000))
         else:
             self.table_id = table_id
diff --git a/app/scodoc/sco_abs_billets.py b/app/scodoc/sco_abs_billets.py
index e20bf1a2501f43cc39142b8ff97b7ba2556d089f..ecb3b33c74b7ba1e6397868d039fad1729c9dc5d 100644
--- a/app/scodoc/sco_abs_billets.py
+++ b/app/scodoc/sco_abs_billets.py
@@ -157,5 +157,6 @@ def table_billets(
         rows=rows,
         html_sortable=True,
         html_class="table_leftalign",
+        table_id="table_billets",
     )
     return tab
diff --git a/app/scodoc/sco_apogee_compare.py b/app/scodoc/sco_apogee_compare.py
index d67dde9f0d4fe054d69fa54e4117439dcdc03539..308403c114e126dd72d36010638ac5e022cf78f2 100644
--- a/app/scodoc/sco_apogee_compare.py
+++ b/app/scodoc/sco_apogee_compare.py
@@ -288,6 +288,7 @@ def apo_table_compare_etud_results(A, B):
         html_class="table_leftalign",
         html_with_td_classes=True,
         preferences=sco_preferences.SemPreferences(),
+        table_id="apo_table_compare_etud_results",
     )
     return T
 
diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py
index a0bff221a97daba8311750f9835cda2d4f8f151a..c046df40730032c912c3c3baaf46a5bfb9729fa9 100644
--- a/app/scodoc/sco_apogee_csv.py
+++ b/app/scodoc/sco_apogee_csv.py
@@ -917,6 +917,7 @@ class ApoData:
             columns_ids=columns_ids,
             titles=dict(zip(columns_ids, columns_ids)),
             rows=rows,
+            table_id="build_cr_table",
             xls_sheet_name="Decisions ScoDoc",
         )
         return T
@@ -969,6 +970,7 @@ class ApoData:
                 "rcue": "RCUE",
             },
             rows=rows,
+            table_id="adsup_table",
             xls_sheet_name="ADSUPs",
         )
 
@@ -1054,6 +1056,7 @@ def nar_etuds_table(apo_data, nar_etuds):
         columns_ids=columns_ids,
         titles=dict(zip(columns_ids, columns_ids)),
         rows=rows,
+        table_id="nar_etuds_table",
         xls_sheet_name="NAR ScoDoc",
     )
     return table.excel()
diff --git a/app/scodoc/sco_bulletins_standard.py b/app/scodoc/sco_bulletins_standard.py
index f96730b54d9b98c8270a79cf0d3c4bf374d454be..d841627f2d8bddd6932880ff5b3c6f4018ef9eba 100644
--- a/app/scodoc/sco_bulletins_standard.py
+++ b/app/scodoc/sco_bulletins_standard.py
@@ -114,6 +114,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
             html_class="notes_bulletin",
             html_class_ignore_default=True,
             html_with_td_classes=True,
+            table_id="std_bul_table",
         )
 
         return T.gen(fmt=fmt)
diff --git a/app/scodoc/sco_cost_formation.py b/app/scodoc/sco_cost_formation.py
index 649623a6c33bd3a247fd69a99c678670d583e3c3..0a211f14c0243c7bfd2c040fa7853d3447c29e4e 100644
--- a/app/scodoc/sco_cost_formation.py
+++ b/app/scodoc/sco_cost_formation.py
@@ -141,6 +141,7 @@ def formsemestre_table_estim_cost(
                     """,
         origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""",
         filename=f"EstimCout-S{formsemestre.semestre_id}",
+        table_id="formsemestre_table_estim_cost",
     )
     return tab
 
diff --git a/app/scodoc/sco_debouche.py b/app/scodoc/sco_debouche.py
index 85a0f8cef8a25a53b3b3ca7ad77910788140d7df..8c0eb28759e84a206a85f0799dcd6e9e20aa6541 100644
--- a/app/scodoc/sco_debouche.py
+++ b/app/scodoc/sco_debouche.py
@@ -222,6 +222,7 @@ def table_debouche_etudids(etudids, keep_numeric=True):
         html_sortable=True,
         html_class="table_leftalign table_listegroupe",
         preferences=sco_preferences.SemPreferences(),
+        table_id="table_debouche_etudids",
     )
     return tab
 
diff --git a/app/scodoc/sco_dept.py b/app/scodoc/sco_dept.py
index 0097aaf43481fd3c174af8559e10faee791ee3e2..6a0ee01460e3e2ff142a5efccf6ebfb8466eb3fe 100644
--- a/app/scodoc/sco_dept.py
+++ b/app/scodoc/sco_dept.py
@@ -198,6 +198,18 @@ def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable
     if current_user.has_permission(Permission.EditApogee):
         html_class += " apo_editable"
     tab = GenTable(
+        columns_ids=columns_ids,
+        html_class_ignore_default=True,
+        html_class=html_class,
+        html_sortable=True,
+        html_table_attrs=f"""
+            data-apo_save_url="{url_for('notes.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)}"
+            data-elt_annee_apo_save_url="{url_for('notes.formsemestre_set_elt_annee_apo', scodoc_dept=g.scodoc_dept)}"
+            data-elt_sem_apo_save_url="{url_for('notes.formsemestre_set_elt_sem_apo', scodoc_dept=g.scodoc_dept)}"
+        """,
+        html_with_td_classes=True,
+        preferences=sco_preferences.SemPreferences(),
+        rows=sems,
         titles={
             "formsemestre_id": "id",
             "semestre_id_n": "S#",
@@ -211,19 +223,7 @@ def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable
             "elt_sem_apo": "Elt. sem. Apo.",
             "formation": "Formation",
         },
-        columns_ids=columns_ids,
-        rows=sems,
         table_id="semlist",
-        html_class_ignore_default=True,
-        html_class=html_class,
-        html_sortable=True,
-        html_table_attrs=f"""
-            data-apo_save_url="{url_for('notes.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)}"
-            data-elt_annee_apo_save_url="{url_for('notes.formsemestre_set_elt_annee_apo', scodoc_dept=g.scodoc_dept)}"
-            data-elt_sem_apo_save_url="{url_for('notes.formsemestre_set_elt_sem_apo', scodoc_dept=g.scodoc_dept)}"
-        """,
-        html_with_td_classes=True,
-        preferences=sco_preferences.SemPreferences(),
     )
 
     return tab
diff --git a/app/scodoc/sco_etape_apogee_view.py b/app/scodoc/sco_etape_apogee_view.py
index f6b38b37f78bda16fa704acac09e39463c58124d..158fe9aeaec17db49db49fae9942af1a67db0ba4 100644
--- a/app/scodoc/sco_etape_apogee_view.py
+++ b/app/scodoc/sco_etape_apogee_view.py
@@ -490,6 +490,7 @@ def table_apo_csv_list(semset):
         # base_url = '%s?formsemestre_id=%s' % (request.base_url, formsemestre_id),
         # caption='Maquettes enregistrées',
         preferences=sco_preferences.SemPreferences(),
+        table_id="apo_csv_list",
     )
 
     return tab
@@ -582,6 +583,7 @@ def _view_etuds_page(
         html_class="table_leftalign",
         filename="students_apo",
         preferences=sco_preferences.SemPreferences(),
+        table_id="view_etuds_page",
     )
     if fmt != "html":
         return tab.make_page(fmt=fmt)
@@ -798,6 +800,7 @@ def view_apo_csv(etape_apo="", semset_id="", fmt="html"):
         filename="students_" + etape_apo,
         caption="Étudiants Apogée en " + etape_apo,
         preferences=sco_preferences.SemPreferences(),
+        table_id="view_apo_csv",
     )
 
     if fmt != "html":
diff --git a/app/scodoc/sco_etape_bilan.py b/app/scodoc/sco_etape_bilan.py
index 86da71667d8ced199a36c934af5cdbb3675d9e56..fec1cb2d08157ebbf1a0c0d765283c53118f593e 100644
--- a/app/scodoc/sco_etape_bilan.py
+++ b/app/scodoc/sco_etape_bilan.py
@@ -666,7 +666,9 @@ class EtapeBilan:
                 col_ids,
                 self.titres,
                 html_class="repartition",
+                html_sortable=True,
                 html_with_td_classes=True,
+                table_id="apo-repartition",
             ).gen(fmt="html")
         )
         return "\n".join(H)
@@ -762,9 +764,9 @@ class EtapeBilan:
                 rows,
                 col_ids,
                 titles,
-                table_id="detail",
                 html_class="table_leftalign",
                 html_sortable=True,
+                table_id="apo-detail",
             ).gen(fmt="html")
         )
         return "\n".join(H)
diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py
index 49a0716ff7389ee2bbbc372745c6f584f84a7549..7074f919c921843d1ff3e24a1e9c99c326cf6fd0 100644
--- a/app/scodoc/sco_evaluations.py
+++ b/app/scodoc/sco_evaluations.py
@@ -633,6 +633,7 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"):
         base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id),
         origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""",
         filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()),
+        table_id="formsemestre_evaluations_delai_correction",
     )
     return tab.make_page(fmt=fmt)
 
diff --git a/app/scodoc/sco_export_results.py b/app/scodoc/sco_export_results.py
index beaf7378e23246510c4a79eee95bbcd2aa82700d..2d90f3c1153f4030281b08d9ab64d86647b94016 100644
--- a/app/scodoc/sco_export_results.py
+++ b/app/scodoc/sco_export_results.py
@@ -106,6 +106,7 @@ def _build_results_table(start_date=None, end_date=None, types_parcours=[]):
         html_class="table_leftalign",
         html_sortable=True,
         preferences=sco_preferences.SemPreferences(),
+        table_id="export_result_table",
     )
     return tab, semlist
 
diff --git a/app/scodoc/sco_find_etud.py b/app/scodoc/sco_find_etud.py
index eeb5c6d7b77fc0c8624cd84224bd1bcae803db6b..87686cfb108489d92f910d48b2dfc7fbddb004c1 100644
--- a/app/scodoc/sco_find_etud.py
+++ b/app/scodoc/sco_find_etud.py
@@ -236,6 +236,7 @@ def search_etud_in_dept(expnom=""):
             html_sortable=True,
             html_class="table_leftalign",
             preferences=sco_preferences.SemPreferences(),
+            table_id="search_etud_in_dept",
         )
         H.append(tab.html())
         if len(etuds) > 20:  # si la page est grande
@@ -384,6 +385,7 @@ def table_etud_in_accessible_depts(expnom=None):
                 rows=etuds,
                 html_sortable=True,
                 html_class="table_leftalign",
+                table_id="etud_in_accessible_depts",
             )
 
             H.append('<div class="table_etud_in_dept">')
@@ -419,13 +421,13 @@ def search_inscr_etud_by_nip(code_nip, fmt="json"):
     """
     result, _ = search_etud_in_accessible_depts(code_nip=code_nip)
 
-    T = []
+    rows = []
     for etuds in result:
         if etuds:
             dept_id = etuds[0]["dept"]
             for e in etuds:
                 for sem in e["sems"]:
-                    T.append(
+                    rows.append(
                         {
                             "dept": dept_id,
                             "etudid": e["etudid"],
@@ -450,6 +452,6 @@ def search_inscr_etud_by_nip(code_nip, fmt="json"):
         "date_debut_iso",
         "date_fin_iso",
     )
-    tab = GenTable(columns_ids=columns_ids, rows=T)
+    tab = GenTable(columns_ids=columns_ids, rows=rows, table_id="inscr_etud_by_nip")
 
     return tab.make_page(fmt=fmt, with_html_headers=False, publish=True)
diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py
index 461eb1ee67c266dff32d17e3f3e4eab0a6134eff..46f751e2e445fa6de4f55d13a1958c0be903d317 100644
--- a/app/scodoc/sco_formations.py
+++ b/app/scodoc/sco_formations.py
@@ -649,20 +649,20 @@ def formation_list_table(detail: bool) -> GenTable:
         "semestres_ues": "Semestres avec UEs",
     }
     return GenTable(
-        columns_ids=columns_ids,
-        rows=rows,
-        titles=titles,
-        origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
+        base_url=f"{request.base_url}" + ("?detail=on" if detail else ""),
         caption=title,
+        columns_ids=columns_ids,
         html_caption=title,
-        table_id="formation_list_table",
         html_class="formation_list_table table_leftalign",
-        html_with_td_classes=True,
         html_sortable=True,
-        base_url=f"{request.base_url}" + ("?detail=on" if detail else ""),
+        html_with_td_classes=True,
+        origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
         page_title=title,
         pdf_title=title,
         preferences=sco_preferences.SemPreferences(),
+        rows=rows,
+        table_id="formation_list_table",
+        titles=titles,
     )
 
 
diff --git a/app/scodoc/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py
index bebe7fccd113908b1f60cf88c012dfaedb940c3b..f9a86475c7946b59228a496ae1ffb6b4c3b38ca6 100644
--- a/app/scodoc/sco_formsemestre.py
+++ b/app/scodoc/sco_formsemestre.py
@@ -527,15 +527,16 @@ def table_formsemestres(
         preferences = sco_preferences.SemPreferences()
     tab = GenTable(
         columns_ids=columns_ids,
-        rows=sems,
-        titles=titles,
         html_class="table_leftalign",
+        html_empty_element="<p><em>aucun résultat</em></p>",
+        html_next_section=html_next_section,
         html_sortable=True,
         html_title=html_title,
-        html_next_section=html_next_section,
-        html_empty_element="<p><em>aucun résultat</em></p>",
         page_title="Semestres",
         preferences=preferences,
+        rows=sems,
+        table_id="table_formsemestres",
+        titles=titles,
     )
     return tab
 
diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py
index 08657c16d0f796649cc2f980e7bc9ea087cbf25a..ec09a508fa16c4deea467eed5a22fe02c69ccffc 100755
--- a/app/scodoc/sco_formsemestre_status.py
+++ b/app/scodoc/sco_formsemestre_status.py
@@ -726,20 +726,21 @@ def formsemestre_description_table(
     rows.append(sums)
 
     return GenTable(
-        columns_ids=columns_ids,
-        rows=rows,
-        titles=titles,
-        origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
+        base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}&with_evals={with_evals}",
         caption=title,
+        columns_ids=columns_ids,
         html_caption=title,
         html_class="table_leftalign formsemestre_description",
-        base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}&with_evals={with_evals}",
-        page_title=title,
         html_title=html_sco_header.html_sem_header(
             "Description du semestre", with_page_header=False
         ),
+        origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
+        page_title=title,
         pdf_title=title,
         preferences=sco_preferences.SemPreferences(formsemestre_id),
+        rows=rows,
+        table_id="formsemestre_description_table",
+        titles=titles,
     )
 
 
diff --git a/app/scodoc/sco_groups_exports.py b/app/scodoc/sco_groups_exports.py
index b8571fb4ab3de01335f743704f4fc25c2150648e..c92e6da47e6de55cf7d20b83725b200cf09102dd 100644
--- a/app/scodoc/sco_groups_exports.py
+++ b/app/scodoc/sco_groups_exports.py
@@ -92,5 +92,6 @@ def groups_export_annotations(group_ids, formsemestre_id=None, fmt="html"):
         html_sortable=True,
         html_class="table_leftalign",
         preferences=sco_preferences.SemPreferences(formsemestre_id),
+        table_id="groups_export_annotations",
     )
     return table.make_page(fmt=fmt)
diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py
index 707a41af0c79a782a716f4930e4eaaa4fdd5afab..0ebed472704613926ca3524423c109ac680a3760 100644
--- a/app/scodoc/sco_groups_view.py
+++ b/app/scodoc/sco_groups_view.py
@@ -661,6 +661,7 @@ def groups_table(
         text_fields_separator=prefs["moodle_csv_separator"],
         text_with_titles=prefs["moodle_csv_with_headerline"],
         preferences=prefs,
+        table_id="groups_table",
     )
     #
     if fmt == "html":
@@ -1028,10 +1029,9 @@ def export_groups_as_moodle_csv(formsemestre_id=None):
     moodle_sem_name = sem["session_id"]
 
     columns_ids = ("email", "semestre_groupe")
-    T = []
-    for partition_id in partitions_etud_groups:
+    rows = []
+    for partition_id, members in partitions_etud_groups.items():
         partition = sco_groups.get_partition(partition_id)
-        members = partitions_etud_groups[partition_id]
         for etudid in members:
             etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
             group_name = members[etudid]["group_name"]
@@ -1040,16 +1040,17 @@ def export_groups_as_moodle_csv(formsemestre_id=None):
                 elts.append(partition["partition_name"])
             if group_name:
                 elts.append(group_name)
-            T.append({"email": etud["email"], "semestre_groupe": "-".join(elts)})
+            rows.append({"email": etud["email"], "semestre_groupe": "-".join(elts)})
     # Make table
     prefs = sco_preferences.SemPreferences(formsemestre_id)
     tab = GenTable(
-        rows=T,
         columns_ids=("email", "semestre_groupe"),
         filename=moodle_sem_name + "-moodle",
-        titles={x: x for x in columns_ids},
+        preferences=prefs,
+        rows=rows,
         text_fields_separator=prefs["moodle_csv_separator"],
         text_with_titles=prefs["moodle_csv_with_headerline"],
-        preferences=prefs,
+        table_id="export_groups_as_moodle_csv",
+        titles={x: x for x in columns_ids},
     )
     return tab.make_page(fmt="csv")
diff --git a/app/scodoc/sco_import_etuds.py b/app/scodoc/sco_import_etuds.py
index ddab91ee0bb48731bb991a4d1512a96e0e27194e..0a569047b7f4860a2a88f7d5cd96722ad83fe9cb 100644
--- a/app/scodoc/sco_import_etuds.py
+++ b/app/scodoc/sco_import_etuds.py
@@ -834,11 +834,12 @@ def adm_table_description_format():
     columns_ids = ("attribute", "type", "writable", "description", "aliases_str")
 
     tab = GenTable(
-        titles=titles,
         columns_ids=columns_ids,
-        rows=list(Fmt.values()),
-        html_sortable=True,
         html_class="table_leftalign",
+        html_sortable=True,
         preferences=sco_preferences.SemPreferences(),
+        rows=list(Fmt.values()),
+        table_id="adm_table_description_format",
+        titles=titles,
     )
     return tab
diff --git a/app/scodoc/sco_inscr_passage.py b/app/scodoc/sco_inscr_passage.py
index 85f2b18baad27a2958bc06f4003f0352e9e62d95..2e1268ed75ea9cc116129b8ef08e809de922abdd 100644
--- a/app/scodoc/sco_inscr_passage.py
+++ b/app/scodoc/sco_inscr_passage.py
@@ -747,10 +747,11 @@ def etuds_select_box_xls(src_cat):
         else:
             e["paiementinscription_str"] = "-"
     tab = GenTable(
-        titles=titles,
-        columns_ids=columns_ids,
-        rows=etuds,
         caption="%(title)s. %(help)s" % src_cat["infos"],
+        columns_ids=columns_ids,
         preferences=sco_preferences.SemPreferences(),
+        rows=etuds,
+        table_id="etuds_select_box_xls",
+        titles=titles,
     )
     return tab.excel()  # tab.make_page(filename=src_cat["infos"]["filename"])
diff --git a/app/scodoc/sco_liste_notes.py b/app/scodoc/sco_liste_notes.py
index 46f28adc12cfdd6393522a5942e196c647178809..c71238ee40547645a93b422b682af3e35df9260a 100644
--- a/app/scodoc/sco_liste_notes.py
+++ b/app/scodoc/sco_liste_notes.py
@@ -599,20 +599,21 @@ def _make_table_notes(
         )
     # display
     tab = GenTable(
-        titles=titles,
-        columns_ids=columns_ids,
-        rows=rows,
-        html_sortable=True,
         base_url=base_url,
-        filename=filename,
-        origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
         caption=caption,
+        columns_ids=columns_ids,
+        filename=filename,
+        html_class="notes_evaluation",
         html_next_section=html_next_section,
-        page_title="Notes de " + formsemestre.titre_mois(),
+        html_sortable=True,
         html_title=html_title,
+        origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
+        page_title="Notes de " + formsemestre.titre_mois(),
         pdf_title=pdf_title,
-        html_class="notes_evaluation",
         preferences=sco_preferences.SemPreferences(formsemestre.id),
+        rows=rows,
+        table_id="table-liste-notes",
+        titles=titles,
         # html_generate_cells=False # la derniere ligne (moyennes) est incomplete
     )
     if fmt == "bordereau":
diff --git a/app/scodoc/sco_lycee.py b/app/scodoc/sco_lycee.py
index d9c27633b1f3e7be4ec83eeff01678db29d097b5..1508c70a381c76b0b328b24a01fbe4a2697f7de2 100644
--- a/app/scodoc/sco_lycee.py
+++ b/app/scodoc/sco_lycee.py
@@ -180,6 +180,7 @@ def _table_etuds_lycees(etuds, group_lycees, title, preferences, no_links=False)
         html_class="table_leftalign table_listegroupe",
         bottom_titles=bottom_titles,
         preferences=preferences,
+        table_id="table_etuds_lycees",
     )
     return tab, etuds_by_lycee
 
diff --git a/app/scodoc/sco_pdf.py b/app/scodoc/sco_pdf.py
index c43f652dc439d986a4c339723add3ca5142dfefb..34a6b6b13a6280cbb63d184325625e8b043d4e50 100755
--- a/app/scodoc/sco_pdf.py
+++ b/app/scodoc/sco_pdf.py
@@ -351,7 +351,7 @@ class ScoDocPageTemplate(PageTemplate):
         canv.drawString(
             self.preferences["pdf_footer_x"] * mm,
             self.preferences["pdf_footer_y"] * mm,
-            content + " " + self.preferences["pdf_footer_extra"],
+            content + " " + (self.preferences["pdf_footer_extra"] or ""),
         )
         canv.restoreState()
 
diff --git a/app/scodoc/sco_placement.py b/app/scodoc/sco_placement.py
index c1dbbfad443b41fe49931b83aef980d1ee2e8d5a..c019bf1dd7f07665e3363ddea597b71aea48b24b 100644
--- a/app/scodoc/sco_placement.py
+++ b/app/scodoc/sco_placement.py
@@ -378,6 +378,7 @@ class PlacementRunner:
             preferences=sco_preferences.SemPreferences(
                 self.moduleimpl_data["formsemestre_id"]
             ),
+            table_id="placement_pdf",
         )
         return tab.make_page(fmt="pdf", with_html_headers=False)
 
diff --git a/app/scodoc/sco_poursuite_dut.py b/app/scodoc/sco_poursuite_dut.py
index 475d59808d7d0b998bfc77c01db7786d49b8941b..e312076354391ca872e46c702b59e99949ba5393 100644
--- a/app/scodoc/sco_poursuite_dut.py
+++ b/app/scodoc/sco_poursuite_dut.py
@@ -221,6 +221,7 @@ def formsemestre_poursuite_report(formsemestre_id, fmt="html"):
         html_class="table_leftalign table_listegroupe",
         pdf_link=False,  # pas d'export pdf
         preferences=sco_preferences.SemPreferences(formsemestre_id),
+        table_id="formsemestre_poursuite_report",
     )
     tab.filename = scu.make_filename("poursuite " + sem["titreannee"])
 
diff --git a/app/scodoc/sco_pv_forms.py b/app/scodoc/sco_pv_forms.py
index ce5e624c129cc06ea1d817ba286f392f5bd7d553..27336f1fd979d7754ede82fe53442e5b217700b1 100644
--- a/app/scodoc/sco_pv_forms.py
+++ b/app/scodoc/sco_pv_forms.py
@@ -252,6 +252,7 @@ def formsemestre_pvjury(formsemestre_id, fmt="html", publish=True):
         html_class="table_leftalign",
         html_sortable=True,
         preferences=sco_preferences.SemPreferences(formsemestre_id),
+        table_id="formsemestre_pvjury",
     )
     if fmt != "html":
         return tab.make_page(
@@ -312,6 +313,7 @@ def formsemestre_pvjury(formsemestre_id, fmt="html", publish=True):
             html_sortable=True,
             html_with_td_classes=True,
             preferences=sco_preferences.SemPreferences(formsemestre_id),
+            table_id="formsemestre_pvjury_counts",
         ).html()
     )
     H.append(
diff --git a/app/scodoc/sco_report.py b/app/scodoc/sco_report.py
index fa5e1c74f188d895b82a0cb41205b84a7d813437..0262c0dfc668d4b831e9cc0807ed7ff94fefcfce 100644
--- a/app/scodoc/sco_report.py
+++ b/app/scodoc/sco_report.py
@@ -236,6 +236,7 @@ def _results_by_category(
         html_col_width="4em",
         html_sortable=True,
         preferences=sco_preferences.SemPreferences(formsemestre_id),
+        table_id=f"results_by_category-{category_name}",
     )
 
 
@@ -695,19 +696,18 @@ def table_suivi_cohorte(
     if statut:
         dbac += " statut: %s" % statut
     tab = GenTable(
-        titles=titles,
+        caption="Suivi cohorte " + pp + sem["titreannee"] + dbac,
         columns_ids=columns_ids,
-        rows=L,
+        filename=scu.make_filename("cohorte " + sem["titreannee"]),
+        html_class="table_cohorte",
         html_col_width="4em",
         html_sortable=True,
-        filename=scu.make_filename("cohorte " + sem["titreannee"]),
-        origin="Généré par %s le " % sco_version.SCONAME
-        + scu.timedate_human_repr()
-        + "",
-        caption="Suivi cohorte " + pp + sem["titreannee"] + dbac,
+        origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
         page_title="Suivi cohorte " + sem["titreannee"],
-        html_class="table_cohorte",
         preferences=sco_preferences.SemPreferences(formsemestre.id),
+        rows=L,
+        table_id="table_suivi_cohorte",
+        titles=titles,
     )
     # Explication: liste des semestres associés à chaque date
     if not P:
@@ -1304,6 +1304,7 @@ def table_suivi_cursus(formsemestre_id, only_primo=False, grouped_parcours=True)
             "code_cursus": len(etuds),
         },
         preferences=sco_preferences.SemPreferences(formsemestre_id),
+        table_id="table_suivi_cursus",
     )
     return tab
 
diff --git a/app/scodoc/sco_report_but.py b/app/scodoc/sco_report_but.py
index 5692c32321febeff2289f8cec4ad86afff473d34..56ffe6d3118dde6da0500d558f86d0a696dbfaf3 100644
--- a/app/scodoc/sco_report_but.py
+++ b/app/scodoc/sco_report_but.py
@@ -87,15 +87,16 @@ def formsemestre_but_indicateurs(formsemestre_id: int, fmt="html"):
     bacs.append("Total")
 
     tab = GenTable(
-        titles={bac: bac for bac in bacs},
+        base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}",
         columns_ids=["titre_indicateur"] + bacs,
-        rows=rows,
-        html_sortable=False,
-        preferences=sco_preferences.SemPreferences(formsemestre_id),
         filename=scu.make_filename(f"Indicateurs_BUT_{formsemestre.titre_annee()}"),
-        origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
         html_caption="Indicateurs BUT annuels.",
-        base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}",
+        html_sortable=False,
+        origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
+        preferences=sco_preferences.SemPreferences(formsemestre_id),
+        rows=rows,
+        titles={bac: bac for bac in bacs},
+        table_id="formsemestre_but_indicateurs",
     )
     title = "Indicateurs suivi annuel BUT"
     t = tab.make_page(
diff --git a/app/scodoc/sco_semset.py b/app/scodoc/sco_semset.py
index fba228fe3c0d45dcf33195d5600136b08b8f90b2..985f9618f8e5d19eeeacf00d73925a930ff702ad 100644
--- a/app/scodoc/sco_semset.py
+++ b/app/scodoc/sco_semset.py
@@ -378,10 +378,9 @@ class SemSet(dict):
 
     def html_diagnostic(self):
         """Affichage de la partie Effectifs et Liste des étudiants
-        (actif seulement si un portail est configuré) XXX pourquoi ??
+        (actif seulement si un portail est configuré)
         """
-        if sco_portal_apogee.has_portal():
-            return self.bilan.html_diagnostic()
+        return self.bilan.html_diagnostic()
         return ""
 
 
@@ -482,10 +481,9 @@ def semset_page(fmt="html"):
         # (remplacé par n liens vers chacun des semestres)
         # s['_semtitles_str_target'] = s['_export_link_target']
         # Experimental:
-        s[
-            "_title_td_attrs"
-        ] = 'class="inplace_edit" data-url="edit_semset_set_title" id="%s"' % (
-            s["semset_id"]
+        s["_title_td_attrs"] = (
+            'class="inplace_edit" data-url="edit_semset_set_title" id="%s"'
+            % (s["semset_id"])
         )
 
     tab = GenTable(
@@ -513,6 +511,7 @@ def semset_page(fmt="html"):
         html_class="table_leftalign",
         filename="semsets",
         preferences=sco_preferences.SemPreferences(),
+        table_id="table-semsets",
     )
     if fmt != "html":
         return tab.make_page(fmt=fmt)
diff --git a/app/scodoc/sco_undo_notes.py b/app/scodoc/sco_undo_notes.py
index 480e0b2e310cfca28d2d68d57f3af5823aee0063..1268a83141d6e2cdb84eea392ccfde1070ae273d 100644
--- a/app/scodoc/sco_undo_notes.py
+++ b/app/scodoc/sco_undo_notes.py
@@ -169,6 +169,7 @@ def evaluation_list_operations(evaluation_id):
         preferences=sco_preferences.SemPreferences(
             evaluation.moduleimpl.formsemestre_id
         ),
+        table_id="evaluation_list_operations",
     )
     return tab.make_page()
 
@@ -241,6 +242,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, fmt="html"):
         preferences=sco_preferences.SemPreferences(formsemestre_id),
         base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id),
         origin=f"Généré par {sco_version.SCONAME} le " + scu.timedate_human_repr() + "",
+        table_id="formsemestre_list_saisies_notes",
     )
     return tab.make_page(fmt=fmt)
 
diff --git a/app/scodoc/sco_users.py b/app/scodoc/sco_users.py
index c896c2af7d99563ae11576a034b6a5e19d617dc9..eb19901b30cebf423c59eb3a626faa3a0329a51f 100644
--- a/app/scodoc/sco_users.py
+++ b/app/scodoc/sco_users.py
@@ -239,6 +239,7 @@ def list_users(
         base_url="%s?all_depts=%s" % (request.base_url, 1 if all_depts else 0),
         pdf_link=False,  # table is too wide to fit in a paper page => disable pdf
         preferences=sco_preferences.SemPreferences(),
+        table_id="list-users",
     )
 
     return tab.make_page(fmt=fmt, with_html_headers=False)
diff --git a/app/static/js/scodoc.js b/app/static/js/scodoc.js
index cdcfffeb4b20953f12f8ac0fe2239ebf555a9b79..32de37347597a37802369b2ec3cd2c4c7d7329eb 100644
--- a/app/static/js/scodoc.js
+++ b/app/static/js/scodoc.js
@@ -155,18 +155,9 @@ function get_query_args() {
 
 // Tables (gen_tables)
 $(function () {
-  if ($("table.gt_table").length > 0) {
-    const url = new URL(document.URL);
-    const order_info_key = JSON.stringify(["table_order", url.pathname]);
-    let order_info;
-    const x = localStorage.getItem(order_info_key);
-    if (x) {
-      try {
-        order_info = JSON.parse(x);
-      } catch (error) {
-        console.error(error);
-      }
-    }
+  if ($("table.gt_table, table.gt_table_searchable").length > 0) {
+
+
     var table_options = {
       paging: false,
       searching: false,
@@ -178,20 +169,47 @@ $(function () {
       },
       orderCellsTop: true, // cellules ligne 1 pour tri
       aaSorting: [], // Prevent initial sorting
-      order: order_info,
+      order: "",
       drawCallback: function (settings) {
         // permet de conserver l'ordre de tri des colonnes
-        let table = $("table.gt_table").DataTable();
-        let order_info = JSON.stringify(table.order());
+        let currentTable = $(settings.nTable);
+        let order_info_key = get_table_order_info_key(currentTable.attr("id"));
+        let dataTableInstance = $(currentTable).DataTable();
+        let order_info = JSON.stringify(dataTableInstance.order());
         localStorage.setItem(order_info_key, order_info);
       },
     };
-    $("table.gt_table").DataTable(table_options);
+
+    $('.gt_table').each(function() {
+      const x = localStorage.getItem(get_table_order_info_key(this.id));
+      if (x) {
+        try {
+          let order_info = JSON.parse(x);
+          console.log("set order=" + order_info);
+          table_options.order = order_info;
+        } catch (error) {
+          console.error(error);
+          delete table_options.order;
+        }
+      } else {
+        delete table_options.order;
+      }
+
+      $(this).DataTable(table_options);
+    });
+
     table_options["searching"] = true;
-    $("table.gt_table_searchable").DataTable(table_options);
+    $("table.gt_table_searchable").each(function() {
+      $(this).DataTable(table_options);
+    });
   }
 });
 
+function get_table_order_info_key(table_id) {
+  const url = new URL(document.URL);
+  return JSON.stringify(["table_order", table_id, url.pathname]);
+}
+
 // Show tags (readonly)
 function readOnlyTags(nodes) {
   // nodes are textareas, hide them and create a span showing tags
diff --git a/app/views/notes.py b/app/views/notes.py
index 51a74b3e7e88455b5a0c524ace6647c9f581d4d1..79c2dbdf47e79788afdddf8b1c01b5768b717a08 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -1248,6 +1248,7 @@ def view_module_abs(moduleimpl_id, fmt="html"):
         filename="absmodule_" + scu.make_filename(modimpl.module.titre_str()),
         caption=f"Absences dans le module {modimpl.module.titre_str()}",
         preferences=sco_preferences.SemPreferences(),
+        table_id="view_module_abs",
     )
 
     if fmt != "html":
@@ -1340,7 +1341,7 @@ def formsemestre_enseignants_list(formsemestre_id, fmt="html"):
 
     # --- Generate page with table
     title = f"Enseignants de {formsemestre.titre_mois()}"
-    T = GenTable(
+    table = GenTable(
         columns_ids=["nom_fmt", "prenom_fmt", "descr_mods", "nbabsadded", "email"],
         titles={
             "nom_fmt": "Nom",
@@ -1361,8 +1362,9 @@ def formsemestre_enseignants_list(formsemestre_id, fmt="html"):
         caption="""Tous les enseignants (responsables ou associés aux modules de
         ce semestre) apparaissent. Le nombre de saisies d'absences est indicatif.""",
         preferences=sco_preferences.SemPreferences(formsemestre_id),
+        table_id="formsemestre_enseignants_list",
     )
-    return T.make_page(page_title=title, title=title, fmt=fmt)
+    return table.make_page(page_title=title, title=title, fmt=fmt)
 
 
 @bp.route("/edit_enseignants_form_delete", methods=["GET", "POST"])
diff --git a/app/views/refcomp.py b/app/views/refcomp.py
index b6402a2600603d1cd5ea1a1bb995f3cc4d46e28c..42193d7cfc167097ae6c0789fd8a0b130b28e85e 100644
--- a/app/views/refcomp.py
+++ b/app/views/refcomp.py
@@ -128,6 +128,7 @@ def refcomp_table():
             }
             for ref in refs
         ],
+        table_id="refcomp_table",
     )
     return render_template(
         "but/refcomp_table.j2",
diff --git a/app/views/scolar.py b/app/views/scolar.py
index c45af3fe5a6598dd30ffce3fb76d285d9006e120..de147fee3c9b6ce4f16d04e2fae307c92cf24d0e 100644
--- a/app/views/scolar.py
+++ b/app/views/scolar.py
@@ -332,6 +332,7 @@ def showEtudLog(etudid, fmt="html"):
         fiche de {etud['nomprenom']}</a></li>
         </ul>""",
         preferences=sco_preferences.SemPreferences(),
+        table_id="showEtudLog",
     )
 
     return tab.make_page(fmt=fmt)
diff --git a/tests/unit/test_export_xml.py b/tests/unit/test_export_xml.py
index c220638c2165d28c47c3daa1fa70720e04089aba..6e5d7ee41d9bbfcea10e1c1d3c932866b362053a 100644
--- a/tests/unit/test_export_xml.py
+++ b/tests/unit/test_export_xml.py
@@ -36,7 +36,7 @@ def xmls_compare(x, y):
 def test_export_xml(test_client):
     """exports XML compatibles ScoDoc 7"""
     # expected_result est le résultat de l'ancienne fonction ScoDoc7:
-    for (data, expected_result) in (
+    for data, expected_result in (
         (
             [{"id": 1, "ues": [{"note": 10}, {}, {"valeur": 25}]}, {"bis": 2}],
             """<?xml version="1.0" encoding="utf-8"?>
@@ -122,6 +122,7 @@ def test_export_xml(test_client):
     table = GenTable(
         rows=[{"nom": "Toto", "age": 26}, {"nom": "Titi", "age": 21}],
         columns_ids=("nom", "age"),
+        table_id="test_export_xml",
     )
     table_xml = table.xml()
 
@@ -138,4 +139,4 @@ def test_export_xml(test_client):
         </row>
     </table>
     """
-    assert xmls_compare(table_xml, expected_result)
\ No newline at end of file
+    assert xmls_compare(table_xml, expected_result)