From 81e7914620b82c05fe859ed0f803067e0e2acc60 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet <emmanuel.viennet@gmail.com>
Date: Thu, 7 Jul 2022 16:24:52 +0200
Subject: [PATCH] Refactoring: cursus Classic/ECTS/BUT

---
 app/but/cursus_but.py                         |  56 ++++++
 app/models/validations.py                     |  15 ++
 app/scodoc/notes_table.py                     |  10 +-
 app/scodoc/sco_apogee_csv.py                  |   4 +-
 app/scodoc/sco_cache.py                       |   4 +-
 app/scodoc/sco_cursus.py                      | 134 ++++++++++++++
 ...{sco_parcours_dut.py => sco_cursus_dut.py} | 168 +++---------------
 app/scodoc/sco_edit_ue.py                     |   4 +-
 app/scodoc/sco_formsemestre_edit.py           |   6 +-
 app/scodoc/sco_formsemestre_exterieurs.py     |   4 +-
 app/scodoc/sco_formsemestre_validation.py     |  44 +++--
 app/scodoc/sco_groups.py                      |   7 +-
 app/scodoc/sco_groups_view.py                 |   4 +-
 app/scodoc/sco_moduleimpl_status.py           |   2 +-
 app/scodoc/sco_page_etud.py                   |   4 +-
 app/scodoc/sco_permissions_check.py           |   4 +-
 app/scodoc/sco_prepajury.py                   |  15 +-
 app/scodoc/sco_pvjury.py                      |  28 ++-
 app/scodoc/sco_pvpdf.py                       |   4 +-
 app/scodoc/sco_report.py                      |  11 +-
 tests/unit/test_sco_basic.py                  |  12 +-
 21 files changed, 312 insertions(+), 228 deletions(-)
 create mode 100644 app/but/cursus_but.py
 create mode 100644 app/scodoc/sco_cursus.py
 rename app/scodoc/{sco_parcours_dut.py => sco_cursus_dut.py} (86%)

diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py
new file mode 100644
index 000000000..6626d20e5
--- /dev/null
+++ b/app/but/cursus_but.py
@@ -0,0 +1,56 @@
+##############################################################################
+# ScoDoc
+# Copyright (c) 1999 - 2022 Emmanuel Viennet.  All rights reserved.
+# See LICENSE
+##############################################################################
+
+"""Cursus en BUT
+
+Classe raccordant avec ScoDoc 7:
+ ScoDoc 7 utilisait sco_cursus_dut.SituationEtudCursus
+
+ Ce module définit une classe SituationEtudCursusBUT
+ avec la même interface.
+
+"""
+
+from typing import Union
+
+from flask import g, url_for
+
+from app import db
+from app import log
+from app.comp.res_but import ResultatsSemestreBUT
+from app.comp import res_sem
+from app.models import formsemestre
+
+from app.models.but_refcomp import (
+    ApcAnneeParcours,
+    ApcCompetence,
+    ApcNiveau,
+    ApcParcours,
+    ApcParcoursNiveauCompetence,
+)
+from app.models import Scolog, ScolarAutorisationInscription
+from app.models.but_validations import (
+    ApcValidationAnnee,
+    ApcValidationRCUE,
+    RegroupementCoherentUE,
+)
+from app.models.etudiants import Identite
+from app.models.formations import Formation
+from app.models.formsemestre import FormSemestre, FormSemestreInscription
+from app.models.ues import UniteEns
+from app.models.validations import ScolarFormSemestreValidation
+from app.scodoc import sco_codes_parcours as sco_codes
+from app.scodoc.sco_codes_parcours import RED, UE_STANDARD
+from app.scodoc import sco_utils as scu
+from app.scodoc.sco_exceptions import ScoException, ScoValueError
+
+from app.scodoc import sco_cursus_dut
+
+
+class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursus):
+    def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT):
+        self.semestre_non_terminal = bool
+        self.formation
diff --git a/app/models/validations.py b/app/models/validations.py
index 42d7ba0d6..b24685a87 100644
--- a/app/models/validations.py
+++ b/app/models/validations.py
@@ -57,6 +57,11 @@ class ScolarFormSemestreValidation(db.Model):
     def __repr__(self):
         return f"{self.__class__.__name__}({self.formsemestre_id}, {self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})"
 
+    def to_dict(self) -> dict:
+        d = dict(self.__dict__)
+        d.pop("_sa_instance_state", None)
+        return d
+
 
 class ScolarAutorisationInscription(db.Model):
     """Autorisation d'inscription dans un semestre"""
@@ -78,6 +83,11 @@ class ScolarAutorisationInscription(db.Model):
         db.ForeignKey("notes_formsemestre.id"),
     )
 
+    def to_dict(self) -> dict:
+        d = dict(self.__dict__)
+        d.pop("_sa_instance_state", None)
+        return d
+
     @classmethod
     def autorise_etud(
         cls,
@@ -146,3 +156,8 @@ class ScolarEvent(db.Model):
         db.Integer,
         db.ForeignKey("notes_formsemestre.id"),
     )
+
+    def to_dict(self) -> dict:
+        d = dict(self.__dict__)
+        d.pop("_sa_instance_state", None)
+        return d
diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py
index a8cd0eb73..56f2b93e3 100644
--- a/app/scodoc/notes_table.py
+++ b/app/scodoc/notes_table.py
@@ -54,22 +54,22 @@ from app.scodoc.sco_codes_parcours import (
     ue_is_fondamentale,
     ue_is_professionnelle,
 )
-from app.scodoc.sco_parcours_dut import formsemestre_get_etud_capitalisation
+from app.scodoc import sco_cache
 from app.scodoc import sco_codes_parcours
 from app.scodoc import sco_compute_moy
-from app.scodoc import sco_cache
+from app.scodoc.sco_cursus import formsemestre_get_etud_capitalisation
+from app.scodoc import sco_cursus_dut
 from app.scodoc import sco_edit_matiere
 from app.scodoc import sco_edit_module
 from app.scodoc import sco_edit_ue
+from app.scodoc import sco_etud
 from app.scodoc import sco_evaluations
 from app.scodoc import sco_formations
 from app.scodoc import sco_formsemestre
 from app.scodoc import sco_formsemestre_inscriptions
 from app.scodoc import sco_groups
 from app.scodoc import sco_moduleimpl
-from app.scodoc import sco_parcours_dut
 from app.scodoc import sco_preferences
-from app.scodoc import sco_etud
 
 
 def comp_ranks(T):
@@ -1175,7 +1175,7 @@ class NotesTable:
                     ):
                         if not cnx:
                             cnx = ndb.GetDBConnexion()
-                        sco_parcours_dut.do_formsemestre_validate_ue(
+                        sco_cursus_dut.do_formsemestre_validate_ue(
                             cnx,
                             nt_cap,
                             ue_cap["formsemestre_id"],
diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py
index e309bedaa..da1aed8de 100644
--- a/app/scodoc/sco_apogee_csv.py
+++ b/app/scodoc/sco_apogee_csv.py
@@ -112,8 +112,8 @@ from app.scodoc.sco_codes_parcours import (
     NAR,
     RAT,
 )
+from app.scodoc import sco_cursus
 from app.scodoc import sco_formsemestre
-from app.scodoc import sco_parcours_dut
 from app.scodoc import sco_etud
 
 APO_PORTAL_ENCODING = (
@@ -413,7 +413,7 @@ class ApoEtud(dict):
             export_res_etape = self.export_res_etape
             if (not export_res_etape) and cur_sem:
                 # exporte toujours le résultat de l'étape si l'étudiant est diplômé
-                Se = sco_parcours_dut.SituationEtudParcours(
+                Se = sco_cursus.get_situation_etud_cursus(
                     self.etud, cur_sem["formsemestre_id"]
                 )
                 export_res_etape = Se.all_other_validated()
diff --git a/app/scodoc/sco_cache.py b/app/scodoc/sco_cache.py
index 7975e3b2c..0798a37d2 100644
--- a/app/scodoc/sco_cache.py
+++ b/app/scodoc/sco_cache.py
@@ -231,7 +231,7 @@ def invalidate_formsemestre(  # was inval_cache(formsemestre_id=None, pdfonly=Fa
     """expire cache pour un semestre (ou tous si formsemestre_id non spécifié).
     Si pdfonly, n'expire que les bulletins pdf cachés.
     """
-    from app.scodoc import sco_parcours_dut
+    from app.scodoc import sco_cursus
 
     if getattr(g, "defer_cache_invalidation", False):
         g.sem_to_invalidate.add(formsemestre_id)
@@ -252,7 +252,7 @@ def invalidate_formsemestre(  # was inval_cache(formsemestre_id=None, pdfonly=Fa
     else:
         formsemestre_ids = [
             formsemestre_id
-        ] + sco_parcours_dut.list_formsemestre_utilisateurs_uecap(formsemestre_id)
+        ] + sco_cursus.list_formsemestre_utilisateurs_uecap(formsemestre_id)
         log(f"----- invalidate_formsemestre: clearing {formsemestre_ids} -----")
 
     if not pdfonly:
diff --git a/app/scodoc/sco_cursus.py b/app/scodoc/sco_cursus.py
new file mode 100644
index 000000000..eec02b9f1
--- /dev/null
+++ b/app/scodoc/sco_cursus.py
@@ -0,0 +1,134 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2022 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
+#
+##############################################################################
+
+"""Gestion des cursus (jurys suivant la formation)
+"""
+
+from app.but import cursus_but
+from app.scodoc import sco_cursus_dut
+
+from app.comp.res_compat import NotesTableCompat
+from app.comp import res_sem
+from app.models import FormSemestre
+from app.scodoc import sco_formsemestre
+from app.scodoc import sco_formations
+import app.scodoc.notesdb as ndb
+
+# SituationEtudParcours -> get_situation_etud_cursus
+def get_situation_etud_cursus(
+    etud: dict, formsemestre_id: int
+) -> sco_cursus_dut.SituationEtudCursus:
+    """renvoie une instance de SituationEtudCursus (ou sous-classe spécialisée)"""
+    formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
+    nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
+
+    if formsemestre.formation.is_apc():
+        return cursus_but.SituationEtudCursusBUT(etud, formsemestre_id, nt)
+
+    parcours = nt.parcours
+    if parcours.ECTS_ONLY:
+        return sco_cursus_dut.SituationEtudCursusECTS(etud, formsemestre_id, nt)
+    return sco_cursus_dut.SituationEtudCursusClassic(etud, formsemestre_id, nt)
+
+
+def formsemestre_get_etud_capitalisation(
+    formation_id: int, semestre_idx: int, date_debut, etudid: int
+) -> list[dict]:
+    """Liste des UE capitalisées (ADM) correspondant au semestre sem et à l'étudiant.
+
+    Recherche dans les semestres de la même formation (code) avec le même
+    semestre_id et une date de début antérieure à celle du semestre mentionné.
+    Et aussi les UE externes validées.
+
+    Resultat: [ { 'formsemestre_id' :
+                  'ue_id' : ue_id dans le semestre origine
+                  'ue_code' :
+                  'moy_ue' :
+                  'event_date' :
+                  'is_external'
+                  } ]
+    """
+    cnx = ndb.GetDBConnexion()
+    cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
+    cursor.execute(
+        """
+    SELECT DISTINCT SFV.*, ue.ue_code
+    FROM notes_ue ue, notes_formations nf,
+    notes_formations nf2, scolar_formsemestre_validation SFV, notes_formsemestre sem
+
+    WHERE ue.formation_id = nf.id
+    and nf.formation_code = nf2.formation_code
+    and nf2.id=%(formation_id)s
+
+    and SFV.ue_id = ue.id
+    and SFV.code = 'ADM'
+    and SFV.etudid = %(etudid)s
+
+    and ( (sem.id = SFV.formsemestre_id
+        and sem.date_debut < %(date_debut)s
+        and sem.semestre_id = %(semestre_id)s )
+        or (
+            ((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures"
+            AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s)
+           ) )
+    """,
+        {
+            "etudid": etudid,
+            "formation_id": formation_id,
+            "semestre_id": semestre_idx,
+            "date_debut": date_debut,
+        },
+    )
+
+    return cursor.dictfetchall()
+
+
+def list_formsemestre_utilisateurs_uecap(formsemestre_id):
+    """Liste des formsemestres pouvant utiliser une UE capitalisee de ce semestre
+    (et qui doivent donc etre sortis du cache si l'on modifie ce
+    semestre): meme code formation, meme semestre_id, date posterieure"""
+    cnx = ndb.GetDBConnexion()
+    sem = sco_formsemestre.get_formsemestre(formsemestre_id)
+    F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
+    cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
+    cursor.execute(
+        """SELECT sem.id
+    FROM notes_formsemestre sem, notes_formations F
+    WHERE sem.formation_id = F.id
+    and F.formation_code = %(formation_code)s
+    and sem.semestre_id = %(semestre_id)s
+    and sem.date_debut >= %(date_debut)s
+    and sem.id != %(formsemestre_id)s;
+    """,
+        {
+            "formation_code": F["formation_code"],
+            "semestre_id": sem["semestre_id"],
+            "formsemestre_id": formsemestre_id,
+            "date_debut": ndb.DateDMYtoISO(sem["date_debut"]),
+        },
+    )
+    return [x[0] for x in cursor.fetchall()]
diff --git a/app/scodoc/sco_parcours_dut.py b/app/scodoc/sco_cursus_dut.py
similarity index 86%
rename from app/scodoc/sco_parcours_dut.py
rename to app/scodoc/sco_cursus_dut.py
index f34413864..5d2f31bfc 100644
--- a/app/scodoc/sco_parcours_dut.py
+++ b/app/scodoc/sco_cursus_dut.py
@@ -28,9 +28,10 @@
 """Semestres: gestion parcours DUT (Arreté du 13 août 2005)
 """
 
+from app import db
 from app.comp import res_sem
 from app.comp.res_compat import NotesTableCompat
-from app.models import FormSemestre, UniteEns
+from app.models import FormSemestre, UniteEns, ScolarAutorisationInscription
 
 import app.scodoc.sco_utils as scu
 import app.scodoc.notesdb as ndb
@@ -105,27 +106,14 @@ class DecisionSem(object):
                 )
             )
         )
-        # xxx debug
-        # log('%s: %s %s %s %s %s' % (self.codechoice,code_etat,new_code_prev,formsemestre_id_utilise_pour_compenser,devenir,assiduite) )
 
 
-def SituationEtudParcours(etud: dict, formsemestre_id: int):
-    """renvoie une instance de SituationEtudParcours (ou sous-classe spécialisée)"""
-    formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
-    nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
-
-    # if formsemestre.formation.is_apc():
-    #    return SituationEtudParcoursBUT(etud, formsemestre_id, nt)
-
-    parcours = nt.parcours
-    #
-    if parcours.ECTS_ONLY:
-        return SituationEtudParcoursECTS(etud, formsemestre_id, nt)
-    else:
-        return SituationEtudParcoursGeneric(etud, formsemestre_id, nt)
+class SituationEtudCursus:
+    "Semestre dans un cursus"
+    pass
 
 
-class SituationEtudParcoursGeneric:
+class SituationEtudCursusClassic(SituationEtudCursus):
     "Semestre dans un parcours"
 
     def __init__(self, etud: dict, formsemestre_id: int, nt: NotesTableCompat):
@@ -454,8 +442,7 @@ class SituationEtudParcoursGeneric:
                 break
         if not cur or cur["formsemestre_id"] != self.formsemestre_id:
             log(
-                "*** SituationEtudParcours: search_prev: cur not found (formsemestre_id=%s, etudid=%s)"
-                % (self.formsemestre_id, self.etudid)
+                f"*** SituationEtudCursus: search_prev: cur not found (formsemestre_id={self.formsemestre_id}, etudid={self.etudid})"
             )
             return None  # pas de semestre courant !!!
         # Cherche semestre antérieur de même formation (code) et semestre_id precedent
@@ -633,31 +620,27 @@ class SituationEtudParcoursGeneric:
                 formsemestre_id=self.prev["formsemestre_id"]
             )  # > modif decisions jury (sem, UE)
 
-        # -- supprime autorisations venant de ce formsemestre
-        cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
         try:
-            cursor.execute(
-                """delete from scolar_autorisation_inscription
-            where etudid = %(etudid)s and origin_formsemestre_id=%(origin_formsemestre_id)s
-            """,
-                {"etudid": self.etudid, "origin_formsemestre_id": self.formsemestre_id},
+            # -- Supprime autorisations venant de ce formsemestre
+            autorisations = ScolarAutorisationInscription.query.filter_by(
+                etudid=self.etudid, origin_formsemestre_id=self.formsemestre_id
             )
-
-            # -- enregistre autorisations inscription
+            for autorisation in autorisations:
+                db.session.delete(autorisation)
+            db.session.flush()
+            # -- Enregistre autorisations inscription
             next_semestre_ids = self.get_next_semestre_ids(decision.devenir)
             for next_semestre_id in next_semestre_ids:
-                _scolar_autorisation_inscription_editor.create(
-                    cnx,
-                    {
-                        "etudid": self.etudid,
-                        "formation_code": self.formation.formation_code,
-                        "semestre_id": next_semestre_id,
-                        "origin_formsemestre_id": self.formsemestre_id,
-                    },
+                autorisation = ScolarAutorisationInscription(
+                    etudid=self.etudid,
+                    formation_code=self.formation.formation_code,
+                    semestre_id=next_semestre_id,
+                    origin_formsemestre_id=self.formsemestre_id,
                 )
-            cnx.commit()
+                db.session.add(autorisation)
+            db.session.commit()
         except:
-            cnx.rollback()
+            cnx.session.rollback()
             raise
         sco_cache.invalidate_formsemestre(
             formsemestre_id=self.formsemestre_id
@@ -673,11 +656,11 @@ class SituationEtudParcoursGeneric:
             )  # > modif decision jury
 
 
-class SituationEtudParcoursECTS(SituationEtudParcoursGeneric):
+class SituationEtudCursusECTS(SituationEtudCursusClassic):
     """Gestion parcours basés sur ECTS"""
 
     def __init__(self, etud, formsemestre_id, nt):
-        SituationEtudParcoursGeneric.__init__(self, etud, formsemestre_id, nt)
+        SituationEtudCursusClassic.__init__(self, etud, formsemestre_id, nt)
 
     def could_be_compensated(self):
         return False  # jamais de compensations dans ce parcours
@@ -1020,9 +1003,9 @@ def etud_est_inscrit_ue(cnx, etudid, formsemestre_id, ue_id):
     """
     cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
     cursor.execute(
-        """SELECT mi.* 
+        """SELECT mi.*
     FROM notes_moduleimpl mi, notes_modules mo, notes_ue ue, notes_moduleimpl_inscription i
-    WHERE i.etudid = %(etudid)s 
+    WHERE i.etudid = %(etudid)s
     and i.moduleimpl_id=mi.id
     and mi.formsemestre_id = %(formsemestre_id)s
     and mi.module_id = mo.id
@@ -1032,102 +1015,3 @@ def etud_est_inscrit_ue(cnx, etudid, formsemestre_id, ue_id):
     )
 
     return len(cursor.fetchall())
-
-
-_scolar_autorisation_inscription_editor = ndb.EditableTable(
-    "scolar_autorisation_inscription",
-    "autorisation_inscription_id",
-    ("etudid", "formation_code", "semestre_id", "date", "origin_formsemestre_id"),
-    output_formators={"date": ndb.DateISOtoDMY},
-    input_formators={"date": ndb.DateDMYtoISO},
-)
-scolar_autorisation_inscription_list = _scolar_autorisation_inscription_editor.list
-
-
-def formsemestre_get_autorisation_inscription(etudid, origin_formsemestre_id):
-    """Liste des autorisations d'inscription pour cet étudiant
-    émanant du semestre indiqué.
-    """
-    cnx = ndb.GetDBConnexion()
-    return scolar_autorisation_inscription_list(
-        cnx, {"origin_formsemestre_id": origin_formsemestre_id, "etudid": etudid}
-    )
-
-
-def formsemestre_get_etud_capitalisation(
-    formation_id: int, semestre_idx: int, date_debut, etudid: int
-) -> list[dict]:
-    """Liste des UE capitalisées (ADM) correspondant au semestre sem et à l'étudiant.
-
-    Recherche dans les semestres de la même formation (code) avec le même
-    semestre_id et une date de début antérieure à celle du semestre mentionné.
-    Et aussi les UE externes validées.
-
-    Resultat: [ { 'formsemestre_id' :
-                  'ue_id' : ue_id dans le semestre origine
-                  'ue_code' :
-                  'moy_ue' :
-                  'event_date' :
-                  'is_external'
-                  } ]
-    """
-    cnx = ndb.GetDBConnexion()
-    cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
-    cursor.execute(
-        """
-    SELECT DISTINCT SFV.*, ue.ue_code
-    FROM notes_ue ue, notes_formations nf,
-    notes_formations nf2, scolar_formsemestre_validation SFV, notes_formsemestre sem
-
-    WHERE ue.formation_id = nf.id
-    and nf.formation_code = nf2.formation_code
-    and nf2.id=%(formation_id)s
-
-    and SFV.ue_id = ue.id
-    and SFV.code = 'ADM'
-    and SFV.etudid = %(etudid)s
-
-    and ( (sem.id = SFV.formsemestre_id
-        and sem.date_debut < %(date_debut)s
-        and sem.semestre_id = %(semestre_id)s )
-        or (
-            ((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures"
-            AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s)
-           ) )
-    """,
-        {
-            "etudid": etudid,
-            "formation_id": formation_id,
-            "semestre_id": semestre_idx,
-            "date_debut": date_debut,
-        },
-    )
-
-    return cursor.dictfetchall()
-
-
-def list_formsemestre_utilisateurs_uecap(formsemestre_id):
-    """Liste des formsemestres pouvant utiliser une UE capitalisee de ce semestre
-    (et qui doivent donc etre sortis du cache si l'on modifie ce
-    semestre): meme code formation, meme semestre_id, date posterieure"""
-    cnx = ndb.GetDBConnexion()
-    sem = sco_formsemestre.get_formsemestre(formsemestre_id)
-    F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
-    cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
-    cursor.execute(
-        """SELECT sem.id
-    FROM notes_formsemestre sem, notes_formations F
-    WHERE sem.formation_id = F.id
-    and F.formation_code = %(formation_code)s
-    and sem.semestre_id = %(semestre_id)s
-    and sem.date_debut >= %(date_debut)s
-    and sem.id != %(formsemestre_id)s;
-    """,
-        {
-            "formation_code": F["formation_code"],
-            "semestre_id": sem["semestre_id"],
-            "formsemestre_id": formsemestre_id,
-            "date_debut": ndb.DateDMYtoISO(sem["date_debut"]),
-        },
-    )
-    return [x[0] for x in cursor.fetchall()]
diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py
index def072c3c..0c0528057 100644
--- a/app/scodoc/sco_edit_ue.py
+++ b/app/scodoc/sco_edit_ue.py
@@ -140,7 +140,7 @@ def do_ue_create(args):
 
 def do_ue_delete(ue_id, delete_validations=False, force=False):
     "delete UE and attached matieres (but not modules)"
-    from app.scodoc import sco_parcours_dut
+    from app.scodoc import sco_cursus_dut
 
     ue = UniteEns.query.get_or_404(ue_id)
     formation = ue.formation
@@ -164,7 +164,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
     #    raise ScoLockedFormError()
     # Il y a-t-il des etudiants ayant validé cette UE ?
     # si oui, propose de supprimer les validations
-    validations = sco_parcours_dut.scolar_formsemestre_validation_list(
+    validations = sco_cursus_dut.scolar_formsemestre_validation_list(
         cnx, args={"ue_id": ue.id}
     )
     if validations and not delete_validations and not force:
diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py
index c4e329462..cf24c4b0c 100644
--- a/app/scodoc/sco_formsemestre_edit.py
+++ b/app/scodoc/sco_formsemestre_edit.py
@@ -60,7 +60,7 @@ from app.scodoc import sco_formsemestre
 from app.scodoc import sco_groups_copy
 from app.scodoc import sco_modalites
 from app.scodoc import sco_moduleimpl
-from app.scodoc import sco_parcours_dut
+from app.scodoc import sco_cursus_dut
 from app.scodoc import sco_permissions_check
 from app.scodoc import sco_portal_apogee
 from app.scodoc import sco_preferences
@@ -1362,14 +1362,14 @@ def _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new)
         if e["ue_id"]:
             e["ue_id"] = ues_old2new[e["ue_id"]]
         sco_etud.scolar_events_edit(cnx, e)
-    validations = sco_parcours_dut.scolar_formsemestre_validation_list(
+    validations = sco_cursus_dut.scolar_formsemestre_validation_list(
         cnx, args={"formsemestre_id": formsemestre_id}
     )
     for e in validations:
         if e["ue_id"]:
             e["ue_id"] = ues_old2new[e["ue_id"]]
         # log('e=%s' % e )
-        sco_parcours_dut.scolar_formsemestre_validation_edit(cnx, e)
+        sco_cursus_dut.scolar_formsemestre_validation_edit(cnx, e)
 
 
 def formsemestre_delete(formsemestre_id):
diff --git a/app/scodoc/sco_formsemestre_exterieurs.py b/app/scodoc/sco_formsemestre_exterieurs.py
index 0da85f2c3..62d49c6d8 100644
--- a/app/scodoc/sco_formsemestre_exterieurs.py
+++ b/app/scodoc/sco_formsemestre_exterieurs.py
@@ -51,7 +51,7 @@ from app.scodoc import sco_formations
 from app.scodoc import sco_formsemestre
 from app.scodoc import sco_formsemestre_inscriptions
 from app.scodoc import sco_formsemestre_validation
-from app.scodoc import sco_parcours_dut
+from app.scodoc import sco_cursus_dut
 from app.scodoc import sco_etud
 
 
@@ -450,7 +450,7 @@ def _list_ue_with_coef_and_validations(sem, etudid):
         else:
             ue["uecoef"] = {}
         # add validation
-        validation = sco_parcours_dut.scolar_formsemestre_validation_list(
+        validation = sco_cursus_dut.scolar_formsemestre_validation_list(
             cnx,
             args={
                 "formsemestre_id": formsemestre_id,
diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py
index a9d13c016..2effc1386 100644
--- a/app/scodoc/sco_formsemestre_validation.py
+++ b/app/scodoc/sco_formsemestre_validation.py
@@ -59,8 +59,9 @@ from app.scodoc import sco_edit_ue
 from app.scodoc import sco_etud
 from app.scodoc import sco_formsemestre
 from app.scodoc import sco_formsemestre_inscriptions
-from app.scodoc import sco_parcours_dut
-from app.scodoc.sco_parcours_dut import etud_est_inscrit_ue
+from app.scodoc import sco_cursus
+from app.scodoc import sco_cursus_dut
+from app.scodoc.sco_cursus_dut import etud_est_inscrit_ue
 from app.scodoc import sco_photos
 from app.scodoc import sco_preferences
 from app.scodoc import sco_pvjury
@@ -108,7 +109,7 @@ def formsemestre_validation_etud_form(
         check = True
 
     etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
-    Se = sco_parcours_dut.SituationEtudParcours(etud, formsemestre_id)
+    Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
     if not Se.sem["etat"]:
         raise ScoValueError("validation: semestre verrouille")
 
@@ -274,15 +275,12 @@ def formsemestre_validation_etud_form(
             ass = "non assidu"
         H.append("<p>Décision existante du %(event_date)s: %(code)s" % decision_jury)
         H.append(" (%s)" % ass)
-        auts = sco_parcours_dut.formsemestre_get_autorisation_inscription(
-            etudid, formsemestre_id
-        )
-        if auts:
+        autorisations = ScolarAutorisationInscription.query.filter_by(
+            etudid=etudid, origin_formsemestre_id=formsemestre_id
+        ).all()
+        if autorisations:
             H.append(". Autorisé%s à s'inscrire en " % etud["ne"])
-            alist = []
-            for aut in auts:
-                alist.append(str(aut["semestre_id"]))
-            H.append(", ".join(["S%s" % x for x in alist]) + ".")
+            H.append(", ".join([f"S{aut.semestre_id}" for aut in autorisations]) + ".")
         H.append("</p>")
 
     # Cas particulier pour ATJ: corriger precedent avant de continuer
@@ -382,7 +380,7 @@ def formsemestre_validation_etud(
 ):
     """Enregistre validation"""
     etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
-    Se = sco_parcours_dut.SituationEtudParcours(etud, formsemestre_id)
+    Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
     # retrouve la decision correspondant au code:
     choices = Se.get_possible_choices(assiduite=True)
     choices += Se.get_possible_choices(assiduite=False)
@@ -415,7 +413,7 @@ def formsemestre_validation_etud_manu(
     if assidu:
         assidu = True
     etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
-    Se = sco_parcours_dut.SituationEtudParcours(etud, formsemestre_id)
+    Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
     if code_etat in Se.parcours.UNUSED_CODES:
         raise ScoValueError("code decision invalide dans ce parcours")
     # Si code ADC, extrait le semestre utilisé:
@@ -430,7 +428,7 @@ def formsemestre_validation_etud_manu(
         formsemestre_id_utilise_pour_compenser = None
 
     # Construit le choix correspondant:
-    choice = sco_parcours_dut.DecisionSem(
+    choice = sco_cursus_dut.DecisionSem(
         code_etat=code_etat,
         new_code_prev=new_code_prev,
         devenir=devenir,
@@ -910,7 +908,7 @@ def do_formsemestre_validation_auto(formsemestre_id):
     conflicts = []  # liste des etudiants avec decision differente déjà saisie
     for etudid in etudids:
         etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
-        Se = sco_parcours_dut.SituationEtudParcours(etud, formsemestre_id)
+        Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
         ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
             {"etudid": etudid, "formsemestre_id": formsemestre_id}
         )[0]
@@ -932,15 +930,13 @@ def do_formsemestre_validation_auto(formsemestre_id):
             if decision_sem and decision_sem["code"] != ADM:
                 ok = False
                 conflicts.append(etud)
-            autorisations = sco_parcours_dut.formsemestre_get_autorisation_inscription(
-                etudid, formsemestre_id
-            )
-            if (
-                len(autorisations) != 0
-            ):  # accepte le cas ou il n'y a pas d'autorisation : BUG 23/6/7, A RETIRER ENSUITE
+            autorisations = ScolarAutorisationInscription.query.filter_by(
+                etudid=etudid, origin_formsemestre_id=formsemestre_id
+            ).all()
+            if len(autorisations) != 0:
                 if (
-                    len(autorisations) != 1
-                    or autorisations[0]["semestre_id"] != next_semestre_id
+                    len(autorisations) > 1
+                    or autorisations[0].semestre_id != next_semestre_id
                 ):
                     if ok:
                         conflicts.append(etud)
@@ -1176,7 +1172,7 @@ def do_formsemestre_validate_previous_ue(
         )
     else:
         sco_formsemestre.do_formsemestre_uecoef_delete(cnx, formsemestre_id, ue_id)
-    sco_parcours_dut.do_formsemestre_validate_ue(
+    sco_cursus_dut.do_formsemestre_validate_ue(
         cnx,
         nt,
         formsemestre_id,  # "importe" cette UE dans le semestre (new 3/2015)
diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py
index f17e017e6..d2cd636c3 100644
--- a/app/scodoc/sco_groups.py
+++ b/app/scodoc/sco_groups.py
@@ -56,8 +56,9 @@ import app.scodoc.notesdb as ndb
 from app import log, cache
 from app.scodoc.scolog import logdb
 from app.scodoc import html_sco_header
-from app.scodoc import sco_codes_parcours
 from app.scodoc import sco_cache
+from app.scodoc import sco_codes_parcours
+from app.scodoc import sco_cursus
 from app.scodoc import sco_etud
 from app.scodoc import sco_permissions_check
 from app.scodoc import sco_xml
@@ -1489,13 +1490,13 @@ def _get_prev_moy(etudid, formsemestre_id):
     """Donne la derniere moyenne generale calculee pour cette étudiant,
     ou 0 si on n'en trouve pas (nouvel inscrit,...).
     """
-    from app.scodoc import sco_parcours_dut
+    from app.scodoc import sco_cursus_dut
 
     info = sco_etud.get_etud_info(etudid=etudid, filled=True)
     if not info:
         raise ScoValueError("etudiant invalide: etudid=%s" % etudid)
     etud = info[0]
-    Se = sco_parcours_dut.SituationEtudParcours(etud, formsemestre_id)
+    Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
     if Se.prev:
         prev_sem = FormSemestre.query.get(Se.prev["formsemestre_id"])
         nt: NotesTableCompat = res_sem.load_formsemestre_results(prev_sem)
diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py
index 259256bd4..9588abd4b 100644
--- a/app/scodoc/sco_groups_view.py
+++ b/app/scodoc/sco_groups_view.py
@@ -49,7 +49,7 @@ from app.scodoc import sco_excel
 from app.scodoc import sco_formsemestre
 from app.scodoc import sco_groups
 from app.scodoc import sco_moduleimpl
-from app.scodoc import sco_parcours_dut
+from app.scodoc import sco_cursus
 from app.scodoc import sco_portal_apogee
 from app.scodoc import sco_preferences
 from app.scodoc import sco_etud
@@ -776,7 +776,7 @@ def groups_table(
             m.update(etud)
             sco_etud.etud_add_lycee_infos(etud)
             # et ajoute le parcours
-            Se = sco_parcours_dut.SituationEtudParcours(
+            Se = sco_cursus.get_situation_etud_cursus(
                 etud, groups_infos.formsemestre_id
             )
             m["parcours"] = Se.get_parcours_descr()
diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py
index c49ef080a..66387efa5 100644
--- a/app/scodoc/sco_moduleimpl_status.py
+++ b/app/scodoc/sco_moduleimpl_status.py
@@ -39,7 +39,7 @@ from app.models import ModuleImpl
 from app.models.evaluations import Evaluation
 import app.scodoc.sco_utils as scu
 from app.scodoc.sco_exceptions import ScoInvalidIdType
-from app.scodoc.sco_parcours_dut import formsemestre_has_decisions
+from app.scodoc.sco_cursus_dut import formsemestre_has_decisions
 from app.scodoc.sco_permissions import Permission
 
 from app.scodoc import html_sco_header
diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py
index b0690ea43..298181bf8 100644
--- a/app/scodoc/sco_page_etud.py
+++ b/app/scodoc/sco_page_etud.py
@@ -46,7 +46,7 @@ from app.scodoc import sco_codes_parcours
 from app.scodoc import sco_formsemestre
 from app.scodoc import sco_formsemestre_status
 from app.scodoc import sco_groups
-from app.scodoc import sco_parcours_dut
+from app.scodoc import sco_cursus
 from app.scodoc import sco_permissions_check
 from app.scodoc import sco_photos
 from app.scodoc import sco_users
@@ -269,7 +269,7 @@ def ficheEtud(etudid=None):
             sem_info[sem["formsemestre_id"]] = grlink
 
     if info["sems"]:
-        Se = sco_parcours_dut.SituationEtudParcours(etud, info["last_formsemestre_id"])
+        Se = sco_cursus.get_situation_etud_cursus(etud, info["last_formsemestre_id"])
         info["liste_inscriptions"] = formsemestre_recap_parcours_table(
             Se,
             etudid,
diff --git a/app/scodoc/sco_permissions_check.py b/app/scodoc/sco_permissions_check.py
index fe21b1672..9ad457d9f 100644
--- a/app/scodoc/sco_permissions_check.py
+++ b/app/scodoc/sco_permissions_check.py
@@ -24,14 +24,14 @@ def can_edit_notes(authuser, moduleimpl_id, allow_ens=True):
     seul le directeur des études peut saisir des notes (et il ne devrait pas).
     """
     from app.scodoc import sco_formsemestre
-    from app.scodoc import sco_parcours_dut
+    from app.scodoc import sco_cursus_dut
 
     M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
     sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
     if not sem["etat"]:
         return False  # semestre verrouillé
 
-    if sco_parcours_dut.formsemestre_has_decisions(sem["formsemestre_id"]):
+    if sco_cursus_dut.formsemestre_has_decisions(sem["formsemestre_id"]):
         # il y a des décisions de jury dans ce semestre !
         return (
             authuser.has_permission(Permission.ScoEditAllNotes)
diff --git a/app/scodoc/sco_prepajury.py b/app/scodoc/sco_prepajury.py
index 678b81ab2..81093af75 100644
--- a/app/scodoc/sco_prepajury.py
+++ b/app/scodoc/sco_prepajury.py
@@ -37,14 +37,14 @@ 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
+from app.models import FormSemestre, Identite, ScolarAutorisationInscription
 from app.scodoc import sco_abs
 from app.scodoc import sco_codes_parcours
 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_parcours_dut
+from app.scodoc import sco_cursus
 from app.scodoc import sco_preferences
 import app.scodoc.sco_utils as scu
 import sco_version
@@ -78,7 +78,7 @@ def feuille_preparation_jury(formsemestre_id):
     nbabs = {}
     nbabsjust = {}
     for etud in etuds:
-        Se = sco_parcours_dut.SituationEtudParcours(
+        Se = sco_cursus.get_situation_etud_cursus(
             etud.to_dict_scodoc7(), formsemestre_id
         )
         if Se.prev:
@@ -119,10 +119,11 @@ def feuille_preparation_jury(formsemestre_id):
             if decision["compense_formsemestre_id"]:
                 code[etud.id] += "+"  # indique qu'il a servi a compenser
             assidu[etud.id] = {False: "Non", True: "Oui"}.get(decision["assidu"], "")
-        aut_list = sco_parcours_dut.formsemestre_get_autorisation_inscription(
-            etud.id, formsemestre_id
-        )
-        autorisations[etud.id] = ", ".join(["S%s" % x["semestre_id"] for x in aut_list])
+
+        autorisations = ScolarAutorisationInscription.query.filter_by(
+            etudid=etud.id, origin_formsemestre_id=formsemestre_id
+        ).all()
+        autorisations[etud.id] = ", ".join(["S{x.semestre_id}" for x in autorisations])
         # parcours:
         parcours[etud.id] = Se.get_parcours_descr()
         # groupe principal (td)
diff --git a/app/scodoc/sco_pvjury.py b/app/scodoc/sco_pvjury.py
index 9b374b8e3..9975e7c60 100644
--- a/app/scodoc/sco_pvjury.py
+++ b/app/scodoc/sco_pvjury.py
@@ -57,24 +57,24 @@ from flask import g, request
 
 from app.comp import res_sem
 from app.comp.res_compat import NotesTableCompat
-from app.models import FormSemestre, UniteEns
+from app.models import FormSemestre, UniteEns, ScolarAutorisationInscription
 
 import app.scodoc.sco_utils as scu
 import app.scodoc.notesdb as ndb
 from app import log
 from app.scodoc import html_sco_header
 from app.scodoc import sco_codes_parcours
-from app.scodoc import sco_cache
+from app.scodoc import sco_cursus
+from app.scodoc import sco_cursus_dut
 from app.scodoc import sco_edit_ue
+from app.scodoc import sco_etud
 from app.scodoc import sco_formations
 from app.scodoc import sco_formsemestre
 from app.scodoc import sco_groups
 from app.scodoc import sco_groups_view
-from app.scodoc import sco_parcours_dut
 from app.scodoc import sco_pdf
 from app.scodoc import sco_preferences
 from app.scodoc import sco_pvpdf
-from app.scodoc import sco_etud
 from app.scodoc.gen_tables import GenTable
 from app.scodoc.sco_codes_parcours import NO_SEMESTRE_ID
 from app.scodoc.sco_pdf import PDFLOCK
@@ -138,12 +138,9 @@ def _descr_decision_sem_abbrev(etat, decision_sem):
     return decision
 
 
-def descr_autorisations(autorisations):
+def descr_autorisations(autorisations: list[ScolarAutorisationInscription]) -> str:
     "résumé textuel des autorisations d'inscription (-> 'S1, S3' )"
-    alist = []
-    for aut in autorisations:
-        alist.append("S" + str(aut["semestre_id"]))
-    return ", ".join(alist)
+    return ", ".join([f"S{a.semestre_id}" for a in autorisations])
 
 
 def _comp_ects_by_ue_code(nt, decision_ues):
@@ -234,7 +231,7 @@ def dict_pvjury(
     D = {}  # même chose que L, mais { etudid : dec }
     for etudid in etudids:
         etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
-        Se = sco_parcours_dut.SituationEtudParcours(etud, formsemestre_id)
+        Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
         semestre_non_terminal = semestre_non_terminal or Se.semestre_non_terminal
         d = {}
         d["identite"] = nt.identdict[etudid]
@@ -280,17 +277,18 @@ def dict_pvjury(
         else:
             d["decision_sem_descr"] = _descr_decision_sem(d["etat"], d["decision_sem"])
 
-        d["autorisations"] = sco_parcours_dut.formsemestre_get_autorisation_inscription(
-            etudid, formsemestre_id
-        )
-        d["autorisations_descr"] = descr_autorisations(d["autorisations"])
+        autorisations = ScolarAutorisationInscription.query.filter_by(
+            etudid=etudid, origin_formsemestre_id=formsemestre_id
+        ).all()
+        d["autorisations"] = [a.to_dict() for a in autorisations]
+        d["autorisations_descr"] = descr_autorisations(autorisations)
 
         d["validation_parcours"] = Se.parcours_validated()
         d["parcours"] = Se.get_parcours_descr(filter_futur=True)
         if with_parcours_decisions:
             d["parcours_decisions"] = Se.get_parcours_decisions()
         # Observations sur les compensations:
-        compensators = sco_parcours_dut.scolar_formsemestre_validation_list(
+        compensators = sco_cursus_dut.scolar_formsemestre_validation_list(
             cnx, args={"compense_formsemestre_id": formsemestre_id, "etudid": etudid}
         )
         obs = []
diff --git a/app/scodoc/sco_pvpdf.py b/app/scodoc/sco_pvpdf.py
index 8ef1c12cc..d889277e5 100644
--- a/app/scodoc/sco_pvpdf.py
+++ b/app/scodoc/sco_pvpdf.py
@@ -50,7 +50,7 @@ from app.scodoc import sco_formsemestre
 from app.scodoc import sco_pdf
 from app.scodoc import sco_preferences
 from app.scodoc.sco_logos import find_logo
-from app.scodoc.sco_parcours_dut import SituationEtudParcours
+from app.scodoc.sco_cursus_dut import SituationEtudCursus
 from app.scodoc.sco_pdf import SU
 import sco_version
 
@@ -428,7 +428,7 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None):
     """
     #
     formsemestre_id = sem["formsemestre_id"]
-    Se: SituationEtudParcours = decision["Se"]
+    Se: SituationEtudCursus = decision["Se"]
     t, s = _descr_jury(sem, Se.parcours_validated() or not Se.semestre_non_terminal)
     objects = []
     style = reportlab.lib.styles.ParagraphStyle({})
diff --git a/app/scodoc/sco_report.py b/app/scodoc/sco_report.py
index 214532a55..97f01f756 100644
--- a/app/scodoc/sco_report.py
+++ b/app/scodoc/sco_report.py
@@ -41,7 +41,7 @@ import pydot
 
 from app.comp import res_sem
 from app.comp.res_compat import NotesTableCompat
-from app.models import FormSemestre
+from app.models import FormSemestre, ScolarAutorisationInscription
 
 import app.scodoc.sco_utils as scu
 from app.models import FormationModalite
@@ -51,7 +51,6 @@ from app.scodoc import sco_codes_parcours
 from app.scodoc import sco_etud
 from app.scodoc import sco_formsemestre
 from app.scodoc import sco_formsemestre_inscriptions
-from app.scodoc import sco_parcours_dut
 from app.scodoc import sco_preferences
 import sco_version
 from app.scodoc.gen_tables import GenTable
@@ -81,10 +80,10 @@ def formsemestre_etuds_stats(sem, only_primo=False):
         if "codedecision" not in etud:
             etud["codedecision"] = "(nd)"  # pas de decision jury
         # Ajout devenir (autorisations inscriptions), utile pour stats passage
-        aut_list = sco_parcours_dut.formsemestre_get_autorisation_inscription(
-            etudid, sem["formsemestre_id"]
-        )
-        autorisations = ["S%s" % x["semestre_id"] for x in aut_list]
+        aut_list = ScolarAutorisationInscription.query.filter_by(
+            etudid=etudid, origin_formsemestre_id=sem["formsemestre_id"]
+        ).all()
+        autorisations = [f"S{a.semestre_id}" for a in aut_list]
         autorisations.sort()
         autorisations_str = ", ".join(autorisations)
         etud["devenir"] = autorisations_str
diff --git a/tests/unit/test_sco_basic.py b/tests/unit/test_sco_basic.py
index a00ab66e5..52ed79f0d 100644
--- a/tests/unit/test_sco_basic.py
+++ b/tests/unit/test_sco_basic.py
@@ -20,6 +20,7 @@ from config import TestConfig
 from tests.unit import sco_fake_gen
 
 import app
+from app import db
 from app.comp import res_sem
 from app.comp.res_compat import NotesTableCompat
 from app.models import FormSemestre
@@ -31,8 +32,7 @@ from app.scodoc import sco_codes_parcours
 from app.scodoc import sco_evaluations
 from app.scodoc import sco_evaluation_db
 from app.scodoc import sco_formsemestre_validation
-from app.scodoc import sco_parcours_dut
-from app.scodoc import sco_cache
+from app.scodoc import sco_cursus_dut
 from app.scodoc import sco_saisie_notes
 from app.scodoc import sco_utils as scu
 
@@ -194,20 +194,20 @@ def run_sco_basic(verbose=False):
 
     # --- Permission saisie notes et décisions de jury, avec ou sans démission ou défaillance
     # on n'a pas encore saisi de décisions
-    assert not sco_parcours_dut.formsemestre_has_decisions(formsemestre_id)
+    assert not sco_cursus_dut.formsemestre_has_decisions(formsemestre_id)
     # Saisie d'un décision AJ, non assidu
     etudid = etuds[-1]["etudid"]
-    sco_parcours_dut.formsemestre_validate_ues(
+    sco_cursus_dut.formsemestre_validate_ues(
         formsemestre_id, etudid, sco_codes_parcours.AJ, False
     )
-    assert sco_parcours_dut.formsemestre_has_decisions(
+    assert sco_cursus_dut.formsemestre_has_decisions(
         formsemestre_id
     ), "décisions manquantes"
     # Suppression de la décision
     sco_formsemestre_validation.formsemestre_validation_suppress_etud(
         formsemestre_id, etudid
     )
-    assert not sco_parcours_dut.formsemestre_has_decisions(
+    assert not sco_cursus_dut.formsemestre_has_decisions(
         formsemestre_id
     ), "décisions non effacées"
 
-- 
GitLab