diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index acd7b3ee6d6a851d8ce121de47333cb147b823f5..438e6c011f1a8ce7cde874bac6b5f7c110c7396a 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -8,8 +8,7 @@
 # pylint génère trop de faux positifs avec les colonnes date:
 # pylint: disable=no-member,not-an-iterable
 
-"""ScoDoc models: formsemestre
-"""
+"""ScoDoc models: formsemestre"""
 from collections import defaultdict
 import datetime
 from functools import cached_property
@@ -1035,19 +1034,20 @@ class FormSemestre(models.ScoDocModel):
             descr_sem += " " + self.modalite
         return descr_sem
 
-    def get_abs_count(self, etudid) -> tuple[int, int, int]:
+    def get_abs_count(self, etudid, moduleimpl_id=None) -> tuple[int, int, int]:
         """Les comptes d'absences de cet étudiant dans ce semestre:
         tuple (nb abs non just, nb abs justifiées, nb abs total)
-        Utilise un cache.
+        Si le module n'est pas spécifié, utilise un cache.
         """
         from app.scodoc import sco_assiduites
 
         metrique = sco_preferences.get_preference("assi_metrique", self.id)
-        return sco_assiduites.get_assiduites_count_in_interval(
+        return sco_assiduites.get_assiduites_count_in_formsemestre(
             etudid,
             self.date_debut.isoformat(),
             self.date_fin.isoformat(),
             translate_assiduites_metric(metrique),
+            moduleimpl_id=moduleimpl_id,
         )
 
     def get_codes_apogee(self, category=None) -> set[str]:
diff --git a/app/scodoc/sco_abs_notification.py b/app/scodoc/sco_abs_notification.py
index e1ebfddff39f3780fb4e7f41796ec2c4a6dae3a7..f7dac412e8e8351f4571f34513d247a9d214f8a5 100644
--- a/app/scodoc/sco_abs_notification.py
+++ b/app/scodoc/sco_abs_notification.py
@@ -48,7 +48,6 @@ from app.models.events import Scolog
 from app.models.formsemestre import FormSemestre
 import app.scodoc.notesdb as ndb
 from app.scodoc import sco_preferences
-from app.scodoc import sco_utils as scu
 
 
 def abs_notify(etudid: int, date: str | datetime.datetime):
@@ -56,32 +55,13 @@ def abs_notify(etudid: int, date: str | datetime.datetime):
     Considère le nombre d'absence dans le semestre courant
     (s'il n'y a pas de semestre courant, ne fait rien,
     car l'etudiant n'est pas inscrit au moment de l'absence!).
-
-    NE FAIT RIEN EN MODE DEBUG.
     """
-    from app.scodoc import sco_assiduites
-
-    # if current_app and current_app.config["DEBUG"]:
-    #    return
-
     formsemestre = retreive_current_formsemestre(etudid, date)
     if not formsemestre:
         return  # non inscrit a la date, pas de notification
 
-    _, nbabsjust, nbabs = sco_assiduites.get_assiduites_count_in_interval(
-        etudid,
-        metrique=scu.translate_assiduites_metric(
-            sco_preferences.get_preference(
-                "assi_metrique", formsemestre.formsemestre_id
-            )
-        ),
-        date_debut=datetime.datetime.combine(
-            formsemestre.date_debut, datetime.datetime.min.time()
-        ),
-        date_fin=datetime.datetime.combine(
-            formsemestre.date_fin, datetime.datetime.min.time()
-        ),
-    )
+    _, nbabsjust, nbabs = formsemestre.get_abs_count(etudid)
+
     do_abs_notify(formsemestre, etudid, date, nbabs, nbabsjust)
 
 
diff --git a/app/scodoc/sco_assiduites.py b/app/scodoc/sco_assiduites.py
index c8c4b7943117480a217276ca4f1a61f08a67685b..66ed068c5ae14b88a5d7a4f739f4237e72ebfc49 100644
--- a/app/scodoc/sco_assiduites.py
+++ b/app/scodoc/sco_assiduites.py
@@ -854,42 +854,9 @@ def check_disabled(func):
 
 
 # Gestion du cache
-def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int, int]:
-    """Les comptes d'absences de cet étudiant dans ce semestre:
-    tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
-    Utilise un cache.
-    """
-    metrique = sco_preferences.get_preference("assi_metrique", sem["formsemestre_id"])
-    return get_assiduites_count_in_interval(
-        etudid,
-        sem["date_debut_iso"],
-        sem["date_fin_iso"],
-        scu.translate_assiduites_metric(metrique),
-    )
-
-
-def formsemestre_get_assiduites_count(
-    etudid: int, formsemestre: FormSemestre, moduleimpl_id: int = None
-) -> tuple[int, int, int]:
-    """Les comptes d'absences de cet étudiant dans ce semestre:
-    tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
-    Utilise un cache (si moduleimpl_id n'est pas spécifié).
-    """
-    metrique = sco_preferences.get_preference("assi_metrique", formsemestre.id)
-    return get_assiduites_count_in_interval(
-        etudid,
-        date_debut=scu.localize_datetime(
-            datetime.combine(formsemestre.date_debut, time(0, 0))
-        ),
-        date_fin=scu.localize_datetime(
-            datetime.combine(formsemestre.date_fin, time(23, 0))
-        ),
-        metrique=scu.translate_assiduites_metric(metrique),
-        moduleimpl_id=moduleimpl_id,
-    )
 
 
-def get_assiduites_count_in_interval(
+def get_assiduites_count_in_formsemestre(
     etudid,
     date_debut_iso: str = "",
     date_fin_iso: str = "",
@@ -907,8 +874,8 @@ def get_assiduites_count_in_interval(
     date_fin_iso = date_fin_iso or date_fin.strftime("%Y-%m-%d")
     key = f"{etudid}_{date_debut_iso}_{date_fin_iso}_assiduites"
 
-    r = sco_cache.AbsSemEtudCache.get(key)
-    if not r or moduleimpl_id is not None:
+    r = sco_cache.AbsSemEtudCache.get(key) if moduleimpl_id is None else None
+    if not r:  # pas caché (out ou avec modimpl)
         date_debut: datetime = date_debut or datetime.fromisoformat(date_debut_iso)
         date_fin: datetime = date_fin or datetime.fromisoformat(date_fin_iso)
 
@@ -927,7 +894,7 @@ def get_assiduites_count_in_interval(
         if moduleimpl_id is None:
             ans = sco_cache.AbsSemEtudCache.set(key, r)
             if not ans:
-                log("warning: get_assiduites_count failed to cache")
+                log("warning: get_assiduites_count_in_formsemestre failed to cache")
 
     nb_abs: int = r["absent"][metrique]
     nb_abs_nj: int = r["absent_non_just"][metrique]
@@ -941,7 +908,7 @@ def invalidate_assiduites_count(etudid: int, fs: FormSemestre):
     sco_cache.AbsSemEtudCache.delete(key)
 
 
-def invalidate_assiduites_etud_date(etudid: int, the_date: datetime):
+def invalidate_assiduites_etud_date(etudid: int, dt: datetime):
     """Doit etre appelé à chaque modification des assiduites
     pour cet étudiant et cette date.
     Invalide cache absence et caches semestre
@@ -949,12 +916,11 @@ def invalidate_assiduites_etud_date(etudid: int, the_date: datetime):
     etud = Identite.get_etud(etudid, accept_none=True)
     if not etud:
         return
-    the_day = the_date.replace(tzinfo=UTC).day
     # Semestres de l'étudiant à cette date:
     formsemestres = [
         ins.formsemestre
         for ins in etud.formsemestre_inscriptions
-        if ins.formsemestre.date_debut <= the_day <= ins.formsemestre.date_fin
+        if ins.formsemestre.date_debut <= dt.date() <= ins.formsemestre.date_fin
     ]
     # Invalide les PDF et les absences:
     for formsemestre in formsemestres:
diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py
index 73de955675a2d78ca62fb3ed1da659c75b7c1907..80af7b86e260596eafa0ba7cc232e48142809c7d 100644
--- a/app/scodoc/sco_bulletins.py
+++ b/app/scodoc/sco_bulletins.py
@@ -25,9 +25,7 @@
 #
 ##############################################################################
 
-"""Génération des bulletins de notes
-
-"""
+"""Génération des bulletins de notes"""
 import collections
 import email
 import time
@@ -57,7 +55,6 @@ from app.models import (
 from app.scodoc.sco_permissions import Permission
 from app.scodoc.sco_exceptions import AccessDenied, ScoValueError, ScoTemporaryError
 from app.scodoc import htmlutils
-from app.scodoc import sco_assiduites
 from app.scodoc import sco_bulletins_generator
 from app.scodoc import sco_bulletins_json
 from app.scodoc import sco_bulletins_pdf
@@ -141,8 +138,6 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
     Cette fonction est utilisée pour les bulletins CLASSIQUES (DUT, ...)
     en HTML et PDF, mais pas ceux en XML.
     """
-    from app.scodoc import sco_assiduites
-
     if version not in scu.BULLETINS_VERSIONS:
         raise ValueError("invalid version code !")
 
@@ -196,7 +191,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
         pid = partition["partition_id"]
         partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
     # --- Absences
-    _, I["nbabsjust"], I["nbabs"] = sco_assiduites.get_assiduites_count(etudid, nt.sem)
+    _, I["nbabsjust"], I["nbabs"] = formsemestre.get_abs_count(etudid)
 
     # --- Decision Jury
     infos, dpv = etud_descr_situation_semestre(
@@ -339,7 +334,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
             else:
                 u["ects"] = "-"
         modules, ue_attente = _ue_mod_bulletin(
-            etudid, formsemestre_id, ue["ue_id"], modimpls, nt, version
+            etudid, formsemestre, ue["ue_id"], modimpls, nt, version
         )
         #
         u["modules"] = modules  # detail des modules de l'UE (dans le semestre courant)
@@ -374,7 +369,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
 
                 u["modules_capitalized"], _ = _ue_mod_bulletin(
                     etudid,
-                    formsemestre_id,
+                    formsemestre_cap,
                     ue_status["capitalized_ue_id"],
                     nt_cap.get_modimpls_dict(),
                     nt_cap,
@@ -442,24 +437,25 @@ def _sort_mod_by_matiere(modlist, nt, etudid):
 
 
 def _ue_mod_bulletin(
-    etudid, formsemestre_id, ue_id, modimpls, nt: NotesTableCompat, version
+    etudid: int,
+    formsemestre: FormSemestre,
+    ue_id: int,
+    modimpls: list[dict],
+    nt: NotesTableCompat,
+    version: str,
 ):
     """Infos sur les modules (et évaluations) dans une UE
     (ajoute les informations aux modimpls)
     Result: liste de modules de l'UE avec les infos dans chacun (seulement
     ceux où l'étudiant est inscrit).
     """
+    formsemestre_id = formsemestre.id
     bul_show_mod_rangs = sco_preferences.get_preference(
         "bul_show_mod_rangs", formsemestre_id
     )
     bul_show_abs_modules = sco_preferences.get_preference(
         "bul_show_abs_modules", formsemestre_id
     )
-    if bul_show_abs_modules:
-        sem = sco_formsemestre.get_formsemestre(formsemestre_id)
-        debut_sem = ndb.DateDMYtoISO(sem["date_debut"])
-        fin_sem = ndb.DateDMYtoISO(sem["date_fin"])
-
     ue_modimpls = [mod for mod in modimpls if mod["module"]["ue_id"] == ue_id]
     mods = []  # result
     ue_attente = False  # true si une eval en attente dans cette UE
@@ -472,7 +468,9 @@ def _ue_mod_bulletin(
         )  # peut etre 'NI'
         is_malus = mod["module"]["module_type"] == ModuleType.MALUS
         if bul_show_abs_modules:
-            _, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem)
+            _, nbabsjust, nbabs = formsemestre.get_abs_count(
+                etudid, modimpl["moduleimpl_id"]
+            )
             mod_abs = [nbabs, nbabsjust]
             mod["mod_abs_txt"] = scu.fmt_abs(mod_abs)
         else:
@@ -1136,9 +1134,7 @@ def do_formsemestre_bulletinetud(
             flash(f"{etud.nomprenom} n'a pas d'adresse e-mail !")
             return False, bul_dict["filigranne"]
         else:
-            mail_bulletin(
-                formsemestre, etud, bul_dict, pdfdata, filename, recipient_addr
-            )
+            _mail_bulletin(formsemestre, etud, pdfdata, filename, recipient_addr)
             flash(f"mail envoyé à {recipient_addr}")
 
             return True, bul_dict["filigranne"]
@@ -1146,10 +1142,9 @@ def do_formsemestre_bulletinetud(
     raise ValueError(f"do_formsemestre_bulletinetud: invalid format ({fmt})")
 
 
-def mail_bulletin(
+def _mail_bulletin(
     formsemestre: FormSemestre,
     etud: Identite,
-    infos: dict,
     pdfdata,
     filename,
     recipient_addr,
diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py
index 99a9b8b93069f30abbea8ae0d50e35e96a9b9857..455dd1b168aef21d480314afce63d58868cba462 100644
--- a/app/scodoc/sco_bulletins_json.py
+++ b/app/scodoc/sco_bulletins_json.py
@@ -25,9 +25,7 @@
 #
 ##############################################################################
 
-"""Génération du bulletin en format JSON (formations classiques)
-
-"""
+"""Génération du bulletin en format JSON (formations classiques)"""
 import datetime
 import json
 
@@ -36,7 +34,6 @@ from flask import abort
 from app import db, ScoDocJSONEncoder
 from app.comp import res_sem
 from app.comp.res_compat import NotesTableCompat
-from app.formations import edit_ue
 from app.models import but_validations
 from app.models import BulAppreciations, Evaluation, Matiere, UniteEns
 from app.models.etudiants import Identite
@@ -44,7 +41,6 @@ from app.models.formsemestre import FormSemestre
 
 import app.scodoc.sco_utils as scu
 import app.scodoc.notesdb as ndb
-from app.scodoc import sco_assiduites
 from app.scodoc import sco_evaluations
 from app.scodoc import sco_formsemestre
 from app.scodoc import sco_groups
@@ -194,6 +190,7 @@ def formsemestre_bulletinetud_published_dict(
         rang = ""
         rang_gr = {}
         ninscrits_gr = {}
+        gr_name = {}
     else:
         rang = str(nt.get_etud_rang(etudid))
         rang_gr, ninscrits_gr, gr_name = sco_bulletins.get_etud_rangs_groups(
@@ -309,8 +306,8 @@ def formsemestre_bulletinetud_published_dict(
 
     # --- Absences
     if prefs["bul_show_abs"]:
-        _, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem)
-        d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust)
+        _, nbabsjust, nbabs = formsemestre.get_abs_count(etudid)
+        d["absences"] = {"nbabs": nbabs, "nbabsjust": nbabsjust}
 
     # --- Décision Jury
     d.update(dict_decision_jury(etud, formsemestre, with_decisions=xml_with_decisions))
diff --git a/app/scodoc/sco_bulletins_xml.py b/app/scodoc/sco_bulletins_xml.py
index 1a2e2d1f395241ad414a3c686b7172ab79f021d6..bd25e41ca6df4568b325b9f58cb639c75435fb54 100644
--- a/app/scodoc/sco_bulletins_xml.py
+++ b/app/scodoc/sco_bulletins_xml.py
@@ -46,13 +46,11 @@ from xml.etree.ElementTree import Element
 
 from app.comp import res_sem
 from app.comp.res_compat import NotesTableCompat
-from app.formations import edit_ue
 import app.scodoc.sco_utils as scu
 import app.scodoc.notesdb as ndb
 from app import log
 from app.but.bulletin_but_xml_compat import bulletin_but_xml_compat
 from app.models import BulAppreciations, Evaluation, FormSemestre, UniteEns
-from app.scodoc import sco_assiduites
 from app.scodoc import codes_cursus
 from app.scodoc import sco_formsemestre
 from app.scodoc import sco_groups
@@ -171,6 +169,7 @@ def make_xml_formsemestre_bulletinetud(
         rang = ""
         rang_gr = {}
         ninscrits_gr = {}
+        gr_name = {}
     else:
         rang = str(nt.get_etud_rang(etudid))
         rang_gr, ninscrits_gr, gr_name = sco_bulletins.get_etud_rangs_groups(
@@ -347,7 +346,7 @@ def make_xml_formsemestre_bulletinetud(
 
     # --- Absences
     if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
-        _, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem)
+        _, nbabsjust, nbabs = formsemestre.get_abs_count(etudid)
         doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
     # --- Decision Jury
     if (
diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py
index 2c75c41e27db9badaf9e4a4aa373c271ee742a5b..b4690148dc7784f6d8b4b60d5bf9d9089f8697a8 100644
--- a/app/scodoc/sco_formsemestre_validation.py
+++ b/app/scodoc/sco_formsemestre_validation.py
@@ -25,8 +25,7 @@
 #
 ##############################################################################
 
-"""Semestres: validation semestre et UE dans parcours
-"""
+"""Semestres: validation semestre et UE dans parcours"""
 import time
 
 import flask
@@ -125,35 +124,30 @@ def formsemestre_validation_etud_form(
 
     H = []
     # Navigation suivant/precedent
+    footer = ["""<div class="jury_footer"><span>"""]
     if etud_index_prev is not None:
         etud_prev = Identite.get_etud(T[etud_index_prev][-1])
         url_prev = url_for(
             "notes.formsemestre_validation_etud_form",
             scodoc_dept=g.scodoc_dept,
             formsemestre_id=formsemestre_id,
-            etud_index=etud_index_prev,
+            etudid=etud_prev.id,
         )
-    else:
-        url_prev = None
+        footer.append(
+            f'< <a class="stdlink" href="{url_prev}">{etud_prev.nomprenom}</a>'
+        )
+
+    footer.append(
+        f"""</span><span><a class="stdlink" href="{url_tableau}">retour à la liste</a></span><span>"""
+    )
     if etud_index_next is not None:
         etud_next = Identite.get_etud(T[etud_index_next][-1])
         url_next = url_for(
             "notes.formsemestre_validation_etud_form",
             scodoc_dept=g.scodoc_dept,
             formsemestre_id=formsemestre_id,
-            etud_index=etud_index_next,
+            etudid=etud_next.id,
         )
-    else:
-        url_next = None
-    footer = ["""<div class="jury_footer"><span>"""]
-    if url_prev:
-        footer.append(
-            f'< <a class="stdlink" href="{url_prev}">{etud_prev.nomprenom}</a>'
-        )
-    footer.append(
-        f"""</span><span><a class="stdlink" href="{url_tableau}">retour à la liste</a></span><span>"""
-    )
-    if url_next:
         footer.append(
             f'<a class="stdlink" href="{url_next}">{etud_next.nomprenom}</a> >'
         )
@@ -767,9 +761,7 @@ def formsemestre_recap_parcours_table(
             f"""<td class="rcp_moy">{scu.fmt_note(nt.get_etud_moy_gen(etudid))}</td>"""
         )
         # Absences (nb d'abs non just. dans ce semestre)
-        nbabsnj = sco_assiduites.formsemestre_get_assiduites_count(
-            etudid, formsemestre
-        )[0]
+        nbabsnj = formsemestre.get_abs_count(etudid)[0]
         H.append(f"""<td class="rcp_abs">{nbabsnj}</td>""")
 
         # UEs
diff --git a/app/scodoc/sco_poursuite_dut.py b/app/scodoc/sco_poursuite_dut.py
index 9cc47655309b643164624a2e52558554c1bfc490..ef7140fe7aa94bc3459b198f4a07be77fe2a7e53 100644
--- a/app/scodoc/sco_poursuite_dut.py
+++ b/app/scodoc/sco_poursuite_dut.py
@@ -37,7 +37,6 @@ from app.comp import res_sem
 from app.comp.res_compat import NotesTableCompat
 from app.models import FormSemestre
 import app.scodoc.sco_utils as scu
-from app.scodoc import sco_assiduites
 from app.scodoc import sco_formsemestre
 from app.scodoc import sco_groups
 from app.scodoc import sco_preferences
@@ -108,9 +107,7 @@ def etud_get_poursuite_info(sem: dict, etud: dict, keep_numeric: bool = False) -
                                 rangs.append(["rang_" + code_module, rang_module])
 
                 # Absences
-                nbabsnj, nbabsjust, _ = sco_assiduites.get_assiduites_count(
-                    etudid, nt.sem
-                )
+                nbabsnj, nbabsjust, _ = formsemestre.get_abs_count(etudid)
                 # En BUT, prend tout, sinon ne prend que les semestre validés par le jury
                 if nt.is_apc or (
                     dec
diff --git a/app/scodoc/sco_prepajury.py b/app/scodoc/sco_prepajury.py
index 0c552d6fc1e2abad06a45d9db49c76a129e4243c..abda900cbb640ab25a0678ca1d3314a870422a70 100644
--- a/app/scodoc/sco_prepajury.py
+++ b/app/scodoc/sco_prepajury.py
@@ -25,8 +25,7 @@
 #
 ##############################################################################
 
-"""Feuille excel pour préparation des jurys classiques (non BUT)
-"""
+"""Feuille excel pour préparation des jurys classiques (non BUT)"""
 import collections
 import time
 
@@ -38,11 +37,9 @@ from flask_login import current_user
 
 from app.comp import res_sem
 from app.comp.res_compat import NotesTableCompat
-from app.models import FormSemestre, Identite, ScolarAutorisationInscription
-from app.scodoc import sco_assiduites
+from app.models import FormSemestre, ScolarAutorisationInscription
 from app.scodoc import codes_cursus
 from app.scodoc import sco_groups
-from app.scodoc import sco_etud
 from app.scodoc import sco_excel
 from app.scodoc import sco_formsemestre
 from app.scodoc import sco_cursus
@@ -131,9 +128,7 @@ def feuille_preparation_jury(formsemestre_id):
         # groupe principal (td)
         groupestd[etud.id] = etud_groups.get(etud.id, {}).get(main_partition_id, "")
         # absences:
-        _, nbabsjust[etud.id], nbabs[etud.id] = sco_assiduites.get_assiduites_count(
-            etud.id, sem
-        )
+        _, nbabsjust[etud.id], nbabs[etud.id] = formsemestre.get_abs_count(etud.id)
 
     # Codes des UE "semestre précédent":
     ue_prev_codes = list(prev_moy_ue.keys())
diff --git a/app/views/__init__.py b/app/views/__init__.py
index 7193d7f5ec163c8813c0cb22b4d20764e0a2b645..a5de67c3d8ea0e6463549e7d327ddaebb62b8234 100644
--- a/app/views/__init__.py
+++ b/app/views/__init__.py
@@ -89,20 +89,13 @@ class ScoData:
         if etud is None:
             return None
         ins = self.etud.inscription_courante()
-        cur_sem = ins.formsemestre
         if ins:
+            cur_sem: FormSemestre = ins.formsemestre
             (
                 self.nb_abs_nj,
                 self.nb_abs_just,
                 self.nb_abs,
-            ) = sco_assiduites.get_assiduites_count_in_interval(
-                etud.id,
-                cur_sem.date_debut.isoformat(),
-                cur_sem.date_fin.isoformat(),
-                scu.translate_assiduites_metric(
-                    sco_preferences.get_preference("assi_metrique")
-                ),
-            )
+            ) = cur_sem.get_abs_count(etud.id)
             return cur_sem
         return None
 
diff --git a/app/views/assiduites.py b/app/views/assiduites.py
index 70182f03defe3265dcfcff2f3226bd41f3dc43f4..30867e03058e57bd20f69897e5290c9a00b9c036 100644
--- a/app/views/assiduites.py
+++ b/app/views/assiduites.py
@@ -79,10 +79,8 @@ from app.views import ScoData
 
 # ---------------
 from app.scodoc.sco_permissions import Permission
-from app.scodoc import sco_moduleimpl
 from app.scodoc import sco_preferences
 from app.scodoc import sco_groups_view, sco_groups
-from app.scodoc import sco_etud
 from app.scodoc import sco_excel
 from app.scodoc import sco_find_etud
 from app.scodoc import sco_assiduites as scass
@@ -1376,7 +1374,7 @@ def _get_anne_sco_from_request() -> int | None:
     try:
         annee_sco_int = int(annee_sco)
     except (ValueError, TypeError) as exc:
-        raise ScoValueError("Année scolaire invalide")
+        raise ScoValueError("Année scolaire invalide") from exc
 
     return annee_sco_int
 
@@ -1814,9 +1812,7 @@ def traitement_justificatifs():
     justif: Justificatif
     for justif in justificatifs_query:
         etud: Identite = justif.etudiant
-        assi_stats: tuple[int, int, int] = scass.get_assiduites_count(
-            etud.id, formsemestre.to_dict()
-        )
+        assi_stats: tuple[int, int, int] = formsemestre.get_abs_count(etud.id)
         etud_dict: dict = {
             "id": etud.id,
             "nom": etud.nom,
diff --git a/app/views/notes.py b/app/views/notes.py
index 122d77f137e81088327cb5bf6db2c2925cddc791..cce68b8c24dba8650935bbfe3099b28171df71f4 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -1181,9 +1181,7 @@ def view_module_abs(moduleimpl_id, fmt="html"):
             nb_abs_nj,
             nb_abs_just,
             nb_abs,
-        ) = sco_assiduites.formsemestre_get_assiduites_count(
-            etud.id, modimpl.formsemestre, moduleimpl_id=modimpl.id
-        )
+        ) = modimpl.formsemestre.get_abs_count(etud.id, moduleimpl_id=modimpl.id)
         rows.append(
             {
                 "civilite": etud.civilite_str,
diff --git a/tests/unit/test_assiduites.py b/tests/unit/test_assiduites.py
index 4046717175d99ced7092ff3aeb3d9685c05f5a5d..1ed0f2ce1d5e23b8f0388d1f4afd02115650751b 100644
--- a/tests/unit/test_assiduites.py
+++ b/tests/unit/test_assiduites.py
@@ -1698,15 +1698,13 @@ def test_cache_assiduites(test_client):
         ).justifier_assiduites()
 
     # Premier semestre 4nj / 2j / 6t
-    assert scass.get_assiduites_count(etud.id, formsemestre1.to_dict()) == (4, 2, 6)
-    assert scass.formsemestre_get_assiduites_count(etud.id, formsemestre1) == (4, 2, 6)
+    assert formsemestre1.get_abs_count(etud.id) == (4, 2, 6)
 
     # ModuleImpl 2nj / 1j / 3t
-    assert scass.formsemestre_get_assiduites_count(
-        etud.id, formsemestre1, moduleimpl.id
-    ) == (2, 1, 3)
+    assert formsemestre1.get_abs_count(etud.id, moduleimpl.id) == (2, 1, 3)
+
     # Deuxième semestre 2nj / 1j / 3t
-    assert scass.get_assiduites_count(etud.id, formsemestre2.to_dict()) == (2, 1, 3)
+    assert formsemestre2.get_abs_count(etud.id) == (2, 1, 3)
 
     # On supprime la première assiduité (sans invalider le cache)
     assi: Assiduite = Assiduite.query.filter_by(etudid=etud.id).first()
@@ -1714,27 +1712,23 @@ def test_cache_assiduites(test_client):
     db.session.commit()
 
     # Premier semestre 4nj / 2j / 6t (Identique car cache)
-    assert scass.get_assiduites_count(etud.id, formsemestre1.to_dict()) == (4, 2, 6)
-    assert scass.formsemestre_get_assiduites_count(etud.id, formsemestre1) == (4, 2, 6)
+    assert formsemestre1.get_abs_count(etud.id) == (4, 2, 6)
     # ModuleImpl 1nj / 1j / 2t (Change car non cache)
-    assert scass.formsemestre_get_assiduites_count(
-        etud.id, formsemestre1, moduleimpl.id
-    ) == (1, 1, 2)
+    assert formsemestre1.get_abs_count(etud.id, moduleimpl.id) == (1, 1, 2)
+
     # Deuxième semestre 2nj / 1j / 3t (Identique car cache et non modifié)
-    assert scass.get_assiduites_count(etud.id, formsemestre2.to_dict()) == (2, 1, 3)
+    assert formsemestre2.get_abs_count(etud.id) == (2, 1, 3)
 
     # On invalide maintenant le cache
     scass.invalidate_assiduites_count(etud.id, formsemestre1)
 
     # Premier semestre 3nj / 2j / 5t (Change car cache invalidé)
-    assert scass.get_assiduites_count(etud.id, formsemestre1.to_dict()) == (3, 2, 5)
-    assert scass.formsemestre_get_assiduites_count(etud.id, formsemestre1) == (3, 2, 5)
+    assert formsemestre1.get_abs_count(etud.id) == (3, 2, 5)
     # ModuleImpl 1nj / 1j / 2t (Ne change pas car pas de changement)
-    assert scass.formsemestre_get_assiduites_count(
-        etud.id, formsemestre1, moduleimpl.id
-    ) == (1, 1, 2)
+    assert formsemestre1.get_abs_count(etud.id, moduleimpl.id) == (1, 1, 2)
+
     # Deuxième semestre 2nj / 1j / 3t (Identique car cache et non modifié)
-    assert scass.get_assiduites_count(etud.id, formsemestre2.to_dict()) == (2, 1, 3)
+    assert formsemestre2.get_abs_count(etud.id) == (2, 1, 3)
 
 
 def test_recuperation_evaluations(test_client):
diff --git a/tests/unit/test_sco_basic.py b/tests/unit/test_sco_basic.py
index 35165871e0879a8a72f94303f4bd3a3dfb810f91..c7f049396c2d78c2f72ea994f0d9d2f1e33f2e7f 100644
--- a/tests/unit/test_sco_basic.py
+++ b/tests/unit/test_sco_basic.py
@@ -24,10 +24,8 @@ from app import db
 from app.comp import res_sem
 from app.comp.res_compat import NotesTableCompat
 from app.models import FormSemestre, Assiduite, Justificatif
-from app.scodoc import sco_formsemestre
 from app.scodoc import sco_bulletins
 from app.scodoc import codes_cursus
-from app.scodoc import sco_assiduites as scass
 from app.scodoc import sco_evaluations
 from app.scodoc import sco_formsemestre_validation
 from app.scodoc import sco_cursus_dut
@@ -74,7 +72,7 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre:
         date_debut="01/01/2020",
         date_fin="30/06/2020",
     )
-    sem = sco_formsemestre.get_formsemestre(formsemestre_id)
+    formsemestre: FormSemestre = FormSemestre.get_or_404(formsemestre_id)
     moduleimpl_id = G.create_moduleimpl(
         module_id=module_id,
         formsemestre_id=formsemestre_id,
@@ -194,7 +192,7 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre:
     etudid = etuds[0]["etudid"]
 
     _signal_absences_justificatifs(etudid)
-    _, nbabsjust, nbabs = scass.get_assiduites_count(etudid, sem)
+    _, nbabsjust, nbabs = formsemestre.get_abs_count(etudid)
     assert nbabs == 6, f"incorrect nbabs ({nbabs})"
     assert nbabsjust == 2, f"incorrect nbabsjust ({nbabsjust})"
 
@@ -227,7 +225,6 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre:
             redirect=False,
         )
     # Vérifie que toutes les UE des étudiants notés ont été acquises:
-    formsemestre: FormSemestre = FormSemestre.get_or_404(formsemestre_id)
     nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
     for etud in etuds[:5]:
         dec_ues = nt.get_etud_decisions_ue(etud["etudid"])