From 246fa62920d909156ca99102be80b2e24218f7ef Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet <emmanuel.viennet@gmail.com>
Date: Mon, 20 Feb 2023 21:04:29 +0100
Subject: [PATCH] Modernisation code: formations

---
 app/pe/pe_tagtable.py                 |    4 +-
 app/scodoc/notes_table.py             | 1356 -------------------------
 app/scodoc/sco_compute_moy.py         |  345 +------
 app/scodoc/sco_edit_formation.py      |   60 +-
 app/scodoc/sco_formations.py          |   13 +-
 app/scodoc/sco_formsemestre_edit.py   |   23 +-
 app/scodoc/sco_formsemestre_status.py |    5 +-
 app/scodoc/sco_inscr_passage.py       |    6 +-
 app/scodoc/sco_moduleimpl_status.py   |   74 +-
 app/scodoc/sco_utils.py               |   35 +
 app/views/notes.py                    |   19 -
 tests/unit/sco_fake_gen.py            |   26 +-
 tests/unit/test_formations.py         |    5 +-
 13 files changed, 172 insertions(+), 1799 deletions(-)
 delete mode 100644 app/scodoc/notes_table.py

diff --git a/app/pe/pe_tagtable.py b/app/pe/pe_tagtable.py
index e6ddb19c4..e14ca6ade 100644
--- a/app/pe/pe_tagtable.py
+++ b/app/pe/pe_tagtable.py
@@ -40,7 +40,7 @@ Created on Thu Sep  8 09:36:33 2016
 import datetime
 import numpy as np
 
-from app.scodoc import notes_table
+from app.scodoc import sco_utils as scu
 
 
 class TableTag(object):
@@ -186,7 +186,7 @@ class TableTag(object):
                 if isinstance(col[0], float)
                 else 0,  # remplace les None et autres chaines par des zéros
             )  # triées
-            self.rangs[tag] = notes_table.comp_ranks(lesMoyennesTriees)  # les rangs
+            self.rangs[tag] = scu.comp_ranks(lesMoyennesTriees)  # les rangs
 
             # calcul des stats
             self.comp_stats_d_un_tag(tag)
diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py
deleted file mode 100644
index 953697484..000000000
--- a/app/scodoc/notes_table.py
+++ /dev/null
@@ -1,1356 +0,0 @@
-# -*- mode: python -*-
-# -*- coding: utf-8 -*-
-
-##############################################################################
-#
-# Gestion scolarite IUT
-#
-# Copyright (c) 1999 - 2023 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
-#
-##############################################################################
-
-"""Calculs sur les notes et cache des résultats
-
-    Ancien code ScoDoc 7 en cours de rénovation 
-"""
-
-from operator import itemgetter
-
-from flask import g, url_for
-
-from app.but import bulletin_but
-from app.models import FormSemestre, Identite
-from app.models import ScoDocSiteConfig
-import app.scodoc.sco_utils as scu
-from app.scodoc.sco_utils import ModuleType
-import app.scodoc.notesdb as ndb
-from app import log
-from app.scodoc.sco_formulas import NoteVector
-from app.scodoc.sco_exceptions import ScoValueError
-
-from app.scodoc.sco_formsemestre import (
-    formsemestre_uecoef_list,
-    formsemestre_uecoef_create,
-)
-from app.scodoc.codes_cursus import (
-    DEF,
-    UE_SPORT,
-    ue_is_fondamentale,
-    ue_is_professionnelle,
-)
-from app.scodoc import sco_cache
-from app.scodoc import codes_cursus
-from app.scodoc import sco_compute_moy
-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_preferences
-
-
-def comp_ranks(T):
-    """Calcul rangs à partir d'une liste ordonnée de tuples [ (valeur, ..., etudid) ]
-    (valeur est une note numérique), en tenant compte des ex-aequos
-    Le resultat est: { etudid : rang } où rang est une chaine decrivant le rang
-    """
-    rangs = {}  # { etudid : rang } (rang est une chaine)
-    nb_ex = 0  # nb d'ex-aequo consécutifs en cours
-    for i in range(len(T)):
-        # test ex-aequo
-        if i < len(T) - 1:
-            next = T[i + 1][0]
-        else:
-            next = None
-        moy = T[i][0]
-        if nb_ex:
-            srang = "%d ex" % (i + 1 - nb_ex)
-            if moy == next:
-                nb_ex += 1
-            else:
-                nb_ex = 0
-        else:
-            if moy == next:
-                srang = "%d ex" % (i + 1 - nb_ex)
-                nb_ex = 1
-            else:
-                srang = "%d" % (i + 1)
-        rangs[T[i][-1]] = srang  # str(i+1)
-    return rangs
-
-
-def get_sem_ues_modimpls(formsemestre_id, modimpls=None):
-    """Get liste des UE du semestre (à partir des moduleimpls)
-    (utilisé quand on ne peut pas construire nt et faire nt.get_ues_stat_dict())
-    """
-    if modimpls is None:
-        modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
-    uedict = {}
-    for modimpl in modimpls:
-        mod = sco_edit_module.module_list(args={"module_id": modimpl["module_id"]})[0]
-        modimpl["module"] = mod
-        if not mod["ue_id"] in uedict:
-            ue = sco_edit_ue.ue_list(args={"ue_id": mod["ue_id"]})[0]
-            uedict[ue["ue_id"]] = ue
-    ues = list(uedict.values())
-    ues.sort(key=lambda u: u["numero"])
-    return ues, modimpls
-
-
-def comp_etud_sum_coef_modules_ue(formsemestre_id, etudid, ue_id):
-    """Somme des coefficients des modules de l'UE dans lesquels cet étudiant est inscrit
-    ou None s'il n'y a aucun module.
-
-    (nécessaire pour éviter appels récursifs de nt, qui peuvent boucler)
-    """
-    infos = ndb.SimpleDictFetch(
-        """SELECT mod.coefficient
-    FROM notes_modules mod, notes_moduleimpl mi, notes_moduleimpl_inscription ins
-    WHERE mod.id = mi.module_id
-    and ins.etudid = %(etudid)s
-    and ins.moduleimpl_id = mi.id
-    and mi.formsemestre_id = %(formsemestre_id)s
-    and mod.ue_id = %(ue_id)s
-    """,
-        {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id},
-    )
-
-    if not infos:
-        return None
-    else:
-        s = sum(x["coefficient"] for x in infos)
-        return s
-
-
-class NotesTable:
-    """Une NotesTable représente un tableau de notes pour un semestre de formation.
-    Les colonnes sont des modules.
-    Les lignes des étudiants.
-    On peut calculer les moyennes par étudiant (pondérées par les coefs)
-    ou les moyennes par module.
-
-    Attributs publics (en lecture):
-    - inscrlist: étudiants inscrits à ce semestre, par ordre alphabétique (avec demissions)
-    - identdict: { etudid : ident }
-    - sem : le formsemestre
-    get_table_moyennes_triees: [ (moy_gen, moy_ue1, moy_ue2, ... moy_ues, moy_mod1, ..., moy_modn, etudid) ]
-    (où toutes les valeurs sont soit des nombres soit des chaines spéciales comme 'NA', 'NI'),
-    incluant les UE de sport
-
-    - bonus[etudid] : valeur du bonus "sport".
-
-    Attributs privés:
-    - _modmoys : { moduleimpl_id : { etudid: note_moyenne_dans_ce_module } }
-    - _ues : liste des UE de ce semestre (hors capitalisees)
-    - _matmoys : { matiere_id : { etudid: note moyenne dans cette matiere } }
-
-    """
-
-    def __init__(self, formsemestre_id):
-        # log(f"NotesTable( formsemestre_id={formsemestre_id} )")
-        raise NotImplementedError()  # XXX
-        if not formsemestre_id:
-            raise ValueError("invalid formsemestre_id (%s)" % formsemestre_id)
-        self.formsemestre_id = formsemestre_id
-        cnx = ndb.GetDBConnexion()
-        self.sem = sco_formsemestre.get_formsemestre(formsemestre_id)
-        self.moduleimpl_stats = {}  # { moduleimpl_id : {stats} }
-        self._uecoef = {}  # { ue_id : coef } cache coef manuels ue cap
-        self._evaluations_etats = None  # liste des evaluations avec état
-        self.use_ue_coefs = sco_preferences.get_preference(
-            "use_ue_coefs", formsemestre_id
-        )
-        # si vrai, bloque calcul des moy gen. et d'UE.:
-        self.block_moyennes = self.sem["block_moyennes"]
-        # Infos sur les etudiants
-        self.inscrlist = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
-            args={"formsemestre_id": formsemestre_id}
-        )
-        # infos identite etudiant
-        # xxx sous-optimal: 1/select par etudiant -> 0.17" pour identdict sur GTR1 !
-        self.identdict = {}  # { etudid : ident }
-        self.inscrdict = {}  # { etudid : inscription }
-        for x in self.inscrlist:
-            i = sco_etud.etudident_list(cnx, {"etudid": x["etudid"]})[0]
-            self.identdict[x["etudid"]] = i
-            self.inscrdict[x["etudid"]] = x
-            x["nomp"] = (i["nom_usuel"] or i["nom"]) + i["prenom"]  # pour tri
-
-        # Tri les etudids par NOM
-        self.inscrlist.sort(key=itemgetter("nomp"))
-
-        # { etudid : rang dans l'ordre alphabetique }
-        self._rang_alpha = {e["etudid"]: i for i, e in enumerate(self.inscrlist)}
-
-        self.bonus = scu.DictDefault(defaultvalue=0)
-        # Notes dans les modules  { moduleimpl_id : { etudid: note_moyenne_dans_ce_module } }
-        (
-            self._modmoys,
-            self._modimpls,
-            self._valid_evals_per_mod,
-            valid_evals,
-            mods_att,
-            self.expr_diagnostics,
-        ) = sco_compute_moy.formsemestre_compute_modimpls_moyennes(
-            self, formsemestre_id
-        )
-        self._mods_att = mods_att  # liste des modules avec des notes en attente
-        self._matmoys = {}  # moyennes par matieres
-        self._valid_evals = {}  # { evaluation_id : eval }
-        for e in valid_evals:
-            self._valid_evals[e["evaluation_id"]] = e  # Liste des modules et UE
-        uedict = {}  # public member: { ue_id : ue }
-        self.uedict = uedict  # les ues qui ont un modimpl dans ce semestre
-        for modimpl in self._modimpls:
-            # module has been added by formsemestre_compute_modimpls_moyennes
-            mod = modimpl["module"]
-            if not mod["ue_id"] in uedict:
-                ue = sco_edit_ue.ue_list(args={"ue_id": mod["ue_id"]})[0]
-                uedict[ue["ue_id"]] = ue
-            else:
-                ue = uedict[mod["ue_id"]]
-            modimpl["ue"] = ue  # add ue dict to moduleimpl
-            self._matmoys[mod["matiere_id"]] = {}
-            mat = sco_edit_matiere.matiere_list(args={"matiere_id": mod["matiere_id"]})[
-                0
-            ]
-            modimpl["mat"] = mat  # add matiere dict to moduleimpl
-            # calcul moyennes du module et stocke dans le module
-            # nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif=
-
-        self.formation = sco_formations.formation_list(
-            args={"formation_id": self.sem["formation_id"]}
-        )[0]
-        self.parcours = codes_cursus.get_cursus_from_code(
-            self.formation["type_parcours"]
-        )
-
-        # En APC, il faut avoir toutes les UE du semestre
-        # (elles n'ont pas nécessairement un module rattaché):
-        if self.parcours.APC_SAE:
-            formsemestre = FormSemestre.query.get(formsemestre_id)
-            for ue in formsemestre.query_ues():
-                if ue.id not in self.uedict:
-                    self.uedict[ue.id] = ue.to_dict()
-
-        # Decisions jury et UE capitalisées
-        self.comp_decisions_jury()
-        self.comp_ue_capitalisees()
-
-        # Liste des moyennes de tous, en chaines de car., triées
-        self._ues = list(uedict.values())
-        self._ues.sort(key=lambda u: u["numero"])
-
-        T = []
-
-        self.moy_gen = {}  # etudid : moy gen (avec UE capitalisées)
-        self.moy_ue = {}  # ue_id : { etudid : moy ue } (valeur numerique)
-        self.etud_moy_infos = {}  # etudid : resultats de comp_etud_moy_gen()
-        valid_moy = []  # liste des valeurs valides de moyenne generale (pour min/max)
-        for ue in self._ues:
-            self.moy_ue[ue["ue_id"]] = {}
-        self._etud_moy_ues = {}  # { etudid : { ue_id : {'moy', 'sum_coefs', ... } }
-
-        for etudid in self.get_etudids():
-            etud_moy_gen = self.comp_etud_moy_gen(etudid, cnx)
-            self.etud_moy_infos[etudid] = etud_moy_gen
-            ue_status = etud_moy_gen["moy_ues"]
-            self._etud_moy_ues[etudid] = ue_status
-
-            moy_gen = etud_moy_gen["moy"]
-            self.moy_gen[etudid] = moy_gen
-            if etud_moy_gen["sum_coefs"] > 0:
-                valid_moy.append(moy_gen)
-
-            moy_ues = []
-            for ue in self._ues:
-                moy_ue = ue_status[ue["ue_id"]]["moy"]
-                moy_ues.append(moy_ue)
-                self.moy_ue[ue["ue_id"]][etudid] = moy_ue
-
-            t = [moy_gen] + moy_ues
-            #
-            is_cap = {}  # ue_id : is_capitalized
-            for ue in self._ues:
-                is_cap[ue["ue_id"]] = ue_status[ue["ue_id"]]["is_capitalized"]
-
-            for modimpl in self.get_modimpls_dict():
-                val = self.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
-                if is_cap[modimpl["module"]["ue_id"]]:
-                    t.append("-c-")
-                else:
-                    t.append(val)
-            #
-            t.append(etudid)
-            T.append(t)
-
-        self.T = T
-        # tri par moyennes décroissantes,
-        # en laissant les demissionnaires a la fin, par ordre alphabetique
-        self.T.sort(key=self._row_key)
-
-        if len(valid_moy):
-            self.moy_min = min(valid_moy)
-            self.moy_max = max(valid_moy)
-        else:
-            self.moy_min = self.moy_max = "NA"
-
-        # calcul rangs (/ moyenne generale)
-        self.etud_moy_gen_ranks = comp_ranks(T)
-
-        self.rangs_groupes = (
-            {}
-        )  # { group_id : { etudid : rang } }  (lazy, see get_etud_rang_group)
-        self.group_etuds = (
-            {}
-        )  # { group_id : set of etudids } (lazy, see get_etud_rang_group)
-
-        # calcul rangs dans chaque UE
-        ue_rangs = (
-            {}
-        )  # ue_rangs[ue_id] = ({ etudid : rang }, nb_inscrits) (rang est une chaine)
-        for ue in self._ues:
-            ue_id = ue["ue_id"]
-            val_ids = [
-                (self.moy_ue[ue_id][etudid], etudid) for etudid in self.moy_ue[ue_id]
-            ]
-            ue_eff = len(
-                [x for x in val_ids if isinstance(x[0], float)]
-            )  # nombre d'étudiants avec une note dans l'UE
-            val_ids.sort(key=self._row_key)
-            ue_rangs[ue_id] = (
-                comp_ranks(val_ids),
-                ue_eff,
-            )  # et non: len(self.moy_ue[ue_id]) qui est l'effectif de la promo
-        self.ue_rangs = ue_rangs
-        # ---- calcul rangs dans les modules
-        self.mod_rangs = {}
-        for modimpl in self._modimpls:
-            vals = self._modmoys[modimpl["moduleimpl_id"]]
-            val_ids = [(vals[etudid], etudid) for etudid in vals.keys()]
-            val_ids.sort(key=self._row_key)
-            self.mod_rangs[modimpl["moduleimpl_id"]] = (comp_ranks(val_ids), len(vals))
-        #
-        self.compute_moy_moy()
-        #
-        log(f"NotesTable( formsemestre_id={formsemestre_id} ) done.")
-
-    def _row_key(self, x):
-        """clé de tri par moyennes décroissantes,
-        en laissant les demissionnaires a la fin, par ordre alphabetique.
-        (moy_gen, rang_alpha)
-        """
-        try:
-            moy = -float(x[0])
-        except (ValueError, TypeError):
-            moy = 1000.0
-        return (moy, self._rang_alpha[x[-1]])
-
-    def get_etudids(self, sorted=False):
-        if sorted:
-            # Tri par moy. generale décroissante
-            return [x[-1] for x in self.T]
-        else:
-            # Tri par ordre alphabetique de NOM
-            return [x["etudid"] for x in self.inscrlist]
-
-    def get_sexnom(self, etudid):
-        "M. DUPONT"
-        etud = self.identdict[etudid]
-        return etud["civilite_str"] + " " + (etud["nom_usuel"] or etud["nom"]).upper()
-
-    def get_nom_short(self, etudid):
-        "formatte nom d'un etud (pour table recap)"
-        etud = self.identdict[etudid]
-        # Attention aux caracteres multibytes pour decouper les 2 premiers:
-        return (
-            (etud["nom_usuel"] or etud["nom"]).upper()
-            + " "
-            + etud["prenom"].capitalize()[:2]
-            + "."
-        )
-
-    def get_nom_long(self, etudid):
-        "formatte nom d'un etud:  M. Pierre DUPONT"
-        etud = self.identdict[etudid]
-        return sco_etud.format_nomprenom(etud)
-
-    def get_displayed_etud_code(self, etudid):
-        'code à afficher sur les listings "anonymes"'
-        return self.identdict[etudid]["code_nip"] or self.identdict[etudid]["etudid"]
-
-    def get_etud_etat(self, etudid):
-        "Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)"
-        if etudid in self.inscrdict:
-            return self.inscrdict[etudid]["etat"]
-        else:
-            return ""
-
-    def get_etud_etat_html(self, etudid):
-        etat = self.inscrdict[etudid]["etat"]
-        if etat == scu.INSCRIT:
-            return ""
-        elif etat == scu.DEMISSION:
-            return ' <font color="red">(DEMISSIONNAIRE)</font> '
-        elif etat == DEF:
-            return ' <font color="red">(DEFAILLANT)</font> '
-        else:
-            return ' <font color="red">(%s)</font> ' % etat
-
-    def get_ues_stat_dict(self, filter_sport=False):  # was get_ues()
-        """Liste des UEs, ordonnée par numero.
-        Si filter_sport, retire les UE de type SPORT
-        """
-        if not filter_sport:
-            return self._ues
-        else:
-            return [ue for ue in self._ues if ue["type"] != UE_SPORT]
-
-    def get_modimpls_dict(self, ue_id=None):
-        "Liste des modules pour une UE (ou toutes si ue_id==None), triés par matières."
-        if ue_id is None:
-            r = self._modimpls
-        else:
-            r = [m for m in self._modimpls if m["ue"]["ue_id"] == ue_id]
-        # trie la liste par ue.numero puis mat.numero puis mod.numero
-        r.sort(
-            key=lambda x: (x["ue"]["numero"], x["mat"]["numero"], x["module"]["numero"])
-        )
-        return r
-
-    def get_etud_eval_note(self, etudid, evaluation_id):
-        "note d'un etudiant a une evaluation"
-        return self._valid_evals[evaluation_id]["notes"][etudid]
-
-    def get_evals_in_mod(self, moduleimpl_id):
-        "liste des evaluations valides dans un module"
-        return [
-            e for e in self._valid_evals.values() if e["moduleimpl_id"] == moduleimpl_id
-        ]
-
-    def get_mod_stats(self, moduleimpl_id):
-        """moyenne generale, min, max pour un module
-        Ne prend en compte que les evaluations où toutes les notes sont entrées
-        Cache le resultat.
-        """
-        if moduleimpl_id in self.moduleimpl_stats:
-            return self.moduleimpl_stats[moduleimpl_id]
-        nb_notes = 0
-        sum_notes = 0.0
-        nb_missing = 0
-        moys = self._modmoys[moduleimpl_id]
-        vals = []
-        for etudid in self.get_etudids():
-            # saute les demissionnaires et les défaillants:
-            if self.inscrdict[etudid]["etat"] != scu.INSCRIT:
-                continue
-            val = moys.get(etudid, None)  # None si non inscrit
-            try:
-                vals.append(float(val))
-            except:
-                nb_missing = nb_missing + 1
-        sum_notes = sum(vals)
-        nb_notes = len(vals)
-        if nb_notes > 0:
-            moy = sum_notes / nb_notes
-            max_note, min_note = max(vals), min(vals)
-        else:
-            moy, min_note, max_note = "NA", "-", "-"
-        s = {
-            "moy": moy,
-            "max": max_note,
-            "min": min_note,
-            "nb_notes": nb_notes,
-            "nb_missing": nb_missing,
-            "nb_valid_evals": len(self._valid_evals_per_mod[moduleimpl_id]),
-        }
-        self.moduleimpl_stats[moduleimpl_id] = s
-        return s
-
-    def compute_moy_moy(self):
-        """precalcule les moyennes d'UE et generale (moyennes sur tous
-        les etudiants), et les stocke dans self.moy_moy, self.ue['moy']
-
-        Les moyennes d'UE ne tiennent pas compte des capitalisations.
-        """
-        ues = self.get_ues_stat_dict()
-        sum_moy = 0  # la somme des moyennes générales valides
-        nb_moy = 0  # le nombre de moyennes générales valides
-        for ue in ues:
-            ue["_notes"] = []  # liste tmp des valeurs de notes valides dans l'ue
-        nb_dem = 0  # nb d'étudiants démissionnaires dans le semestre
-        nb_def = 0  # nb d'étudiants défaillants dans le semestre
-        T = self.get_table_moyennes_triees()
-        for t in T:
-            etudid = t[-1]
-            # saute les demissionnaires et les défaillants:
-            if self.inscrdict[etudid]["etat"] != scu.INSCRIT:
-                if self.inscrdict[etudid]["etat"] == scu.DEMISSION:
-                    nb_dem += 1
-                if self.inscrdict[etudid]["etat"] == DEF:
-                    nb_def += 1
-                continue
-            try:
-                sum_moy += float(t[0])
-                nb_moy += 1
-            except:
-                pass
-            i = 0
-            for ue in ues:
-                i += 1
-                try:
-                    ue["_notes"].append(float(t[i]))
-                except:
-                    pass
-        self.nb_demissions = nb_dem
-        self.nb_defaillants = nb_def
-        if nb_moy > 0:
-            self.moy_moy = sum_moy / nb_moy
-        else:
-            self.moy_moy = "-"
-
-        i = 0
-        for ue in ues:
-            i += 1
-            ue["nb_vals"] = len(ue["_notes"])
-            if ue["nb_vals"] > 0:
-                ue["moy"] = sum(ue["_notes"]) / ue["nb_vals"]
-                ue["max"] = max(ue["_notes"])
-                ue["min"] = min(ue["_notes"])
-            else:
-                ue["moy"], ue["max"], ue["min"] = "", "", ""
-            del ue["_notes"]
-
-    def get_etud_mod_moy(self, moduleimpl_id, etudid):
-        """moyenne d'un etudiant dans un module (ou NI si non inscrit)"""
-        return self._modmoys[moduleimpl_id].get(etudid, "NI")
-
-    def get_etud_mat_moy(self, matiere_id, etudid):
-        """moyenne d'un étudiant dans une matière (ou NA si pas de notes)"""
-        matmoy = self._matmoys.get(matiere_id, None)
-        if not matmoy:
-            return "NM"  # non inscrit
-            # log('*** oups: get_etud_mat_moy(%s, %s)' % (matiere_id, etudid))
-            # raise ValueError('matiere invalide !') # should not occur
-        return matmoy.get(etudid, "NA")
-
-    def comp_etud_moy_ue(self, etudid, ue_id=None, cnx=None):
-        """Calcule moyenne gen. pour un etudiant dans une UE
-        Ne prend en compte que les evaluations où toutes les notes sont entrées
-        Return a dict(moy, nb_notes, nb_missing, sum_coefs)
-        Si pas de notes, moy == 'NA' et sum_coefs==0
-        Si non inscrit, moy == 'NI' et sum_coefs==0
-        """
-        assert ue_id
-        modimpls = self.get_modimpls_dict(ue_id)
-        nb_notes = 0  # dans cette UE
-        sum_notes = 0.0
-        sum_coefs = 0.0
-        nb_missing = 0  # nb de modules sans note dans cette UE
-
-        notes_bonus_gen = []  # liste des notes de sport et culture
-        coefs_bonus_gen = []
-
-        ue_malus = 0.0  # malus à appliquer à cette moyenne d'UE
-
-        notes = NoteVector()
-        coefs = NoteVector()
-        coefs_mask = NoteVector()  # 0/1, 0 si coef a ete annulé
-
-        matiere_id_last = None
-        matiere_sum_notes = matiere_sum_coefs = 0.0
-
-        est_inscrit = False  # inscrit à l'un des modules de cette UE ?
-
-        for modimpl in modimpls:
-            # module ne faisant pas partie d'une UE capitalisee
-            val = self._modmoys[modimpl["moduleimpl_id"]].get(etudid, "NI")
-            # si 'NI', etudiant non inscrit a ce module
-            if val != "NI":
-                est_inscrit = True
-            if modimpl["module"]["module_type"] == ModuleType.STANDARD:
-                coef = modimpl["module"]["coefficient"]
-                if modimpl["ue"]["type"] != UE_SPORT:
-                    notes.append(val, name=modimpl["module"]["code"])
-                    try:
-                        sum_notes += val * coef
-                        sum_coefs += coef
-                        nb_notes = nb_notes + 1
-                        coefs.append(coef)
-                        coefs_mask.append(1)
-                        matiere_id = modimpl["module"]["matiere_id"]
-                        if (
-                            matiere_id_last
-                            and matiere_id != matiere_id_last
-                            and matiere_sum_coefs
-                        ):
-                            self._matmoys[matiere_id_last][etudid] = (
-                                matiere_sum_notes / matiere_sum_coefs
-                            )
-                            matiere_sum_notes = matiere_sum_coefs = 0.0
-                        matiere_sum_notes += val * coef
-                        matiere_sum_coefs += coef
-                        matiere_id_last = matiere_id
-                    except TypeError:  # val == "NI" "NA"
-                        assert val == "NI" or val == "NA" or val == "ERR"
-                        nb_missing = nb_missing + 1
-                        coefs.append(0)
-                        coefs_mask.append(0)
-
-                else:  # UE_SPORT:
-                    # la note du module de sport agit directement sur la moyenne gen.
-                    try:
-                        notes_bonus_gen.append(float(val))
-                        coefs_bonus_gen.append(coef)
-                    except:
-                        # log('comp_etud_moy_ue: exception: val=%s coef=%s' % (val,coef))
-                        pass
-            elif modimpl["module"]["module_type"] == ModuleType.MALUS:
-                try:
-                    ue_malus += val
-                except:
-                    pass  # si non inscrit ou manquant, ignore
-            elif modimpl["module"]["module_type"] in (
-                ModuleType.RESSOURCE,
-                ModuleType.SAE,
-            ):
-                # XXX temporaire pour ne pas bloquer durant le dev
-                pass
-            else:
-                raise ValueError(
-                    "invalid module type (%s)" % modimpl["module"]["module_type"]
-                )
-
-        if matiere_id_last and matiere_sum_coefs:
-            self._matmoys[matiere_id_last][etudid] = (
-                matiere_sum_notes / matiere_sum_coefs
-            )
-
-        # Calcul moyenne:
-        if sum_coefs > 0:
-            moy = sum_notes / sum_coefs
-            if ue_malus:
-                moy -= ue_malus
-                moy = max(scu.NOTES_MIN, min(moy, 20.0))
-            moy_valid = True
-        else:
-            moy = "NA"
-            moy_valid = False
-
-        # Recalcule la moyenne en utilisant une formule utilisateur
-        expr_diag = {}
-        formula = sco_compute_moy.get_ue_expression(self.formsemestre_id, ue_id)
-        if formula:
-            moy = sco_compute_moy.compute_user_formula(
-                self.sem,
-                etudid,
-                moy,
-                moy_valid,
-                notes,
-                coefs,
-                coefs_mask,
-                formula,
-                diag_info=expr_diag,
-            )
-            if expr_diag:
-                expr_diag["ue_id"] = ue_id
-                self.expr_diagnostics.append(expr_diag)
-
-        return dict(
-            moy=moy,
-            nb_notes=nb_notes,
-            nb_missing=nb_missing,
-            sum_coefs=sum_coefs,
-            notes_bonus_gen=notes_bonus_gen,
-            coefs_bonus_gen=coefs_bonus_gen,
-            expr_diag=expr_diag,
-            ue_malus=ue_malus,
-            est_inscrit=est_inscrit,
-        )
-
-    def comp_etud_moy_gen(self, etudid, cnx):
-        """Calcule moyenne gen. pour un etudiant
-        Return a dict:
-         moy  : moyenne générale
-         nb_notes, nb_missing, sum_coefs
-         ects_pot : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury),
-         ects_pot_fond: (float) nb d'ECTS issus d'UE fondamentales (non électives)
-         ects_pot_pro: (float) nb d'ECTS issus d'UE pro
-         moy_ues : { ue_id : ue_status }
-        où ue_status = {
-             'est_inscrit' : True si étudiant inscrit à au moins un module de cette UE
-             'moy' :  moyenne, avec capitalisation eventuelle
-             'capitalized_ue_id' : id de l'UE capitalisée
-             'coef_ue' : coef de l'UE utilisé pour le calcul de la moyenne générale
-                         (la somme des coefs des modules, ou le coef d'UE capitalisée,
-                         ou encore le coef d'UE si l'option use_ue_coefs est active)
-             'cur_moy_ue' : moyenne de l'UE en cours (sans considérer de capitalisation)
-             'cur_coef_ue': coefficient de l'UE courante (inutilisé ?)
-             'is_capitalized' : True|False,
-             'ects_pot' : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury),
-             'ects_pot_fond': 0. si UE non fondamentale, = ects_pot sinon,
-             'ects_pot_pro' : 0 si UE non pro, = ects_pot sinon,
-             'formsemestre_id' : (si capitalisee),
-             'event_date' : (si capitalisee)
-             }
-        Si pas de notes, moy == 'NA' et sum_coefs==0
-
-        Prend toujours en compte les UE capitalisées.
-        """
-        # Si l'étudiant a Démissionné ou est DEFaillant, on n'enregistre pas ses moyennes
-        block_computation = (
-            self.inscrdict[etudid]["etat"] == "D"
-            or self.inscrdict[etudid]["etat"] == DEF
-            or self.block_moyennes
-        )
-
-        moy_ues = {}
-        notes_bonus_gen = (
-            []
-        )  # liste des notes de sport et culture (s'appliquant à la MG)
-        coefs_bonus_gen = []
-        nb_notes = 0  # nb de notes d'UE (non capitalisees)
-        sum_notes = 0.0  # somme des notes d'UE
-        # somme des coefs d'UE (eux-même somme des coefs de modules avec notes):
-        sum_coefs = 0.0
-
-        nb_missing = 0  # nombre d'UE sans notes
-        sem_ects_pot = 0.0
-        sem_ects_pot_fond = 0.0
-        sem_ects_pot_pro = 0.0
-
-        for ue in self.get_ues_stat_dict():
-            # - On calcule la moyenne d'UE courante:
-            if not block_computation:
-                mu = self.comp_etud_moy_ue(etudid, ue_id=ue["ue_id"], cnx=cnx)
-            else:
-                mu = dict(
-                    moy="NA",
-                    nb_notes=0,
-                    nb_missing=0,
-                    sum_coefs=0,
-                    notes_bonus_gen=0,
-                    coefs_bonus_gen=0,
-                    expr_diag="",
-                    est_inscrit=False,
-                )
-            # infos supplementaires pouvant servir au calcul du bonus sport
-            mu["ue"] = ue
-            moy_ues[ue["ue_id"]] = mu
-
-            # - Faut-il prendre une UE capitalisée ?
-            if mu["moy"] != "NA" and mu["est_inscrit"]:
-                max_moy_ue = mu["moy"]
-            else:
-                # pas de notes dans l'UE courante, ou pas inscrit
-                max_moy_ue = 0.0
-            if not mu["est_inscrit"]:
-                coef_ue = 0.0
-            else:
-                if self.use_ue_coefs:
-                    coef_ue = mu["ue"]["coefficient"]
-                else:
-                    # coef UE = sum des coefs modules
-                    coef_ue = mu["sum_coefs"]
-
-            # is_capitalized si l'UE prise en compte est une UE capitalisée
-            mu["is_capitalized"] = False
-            # was_capitalized s'il y a precedemment une UE capitalisée (pas forcement meilleure)
-            mu["was_capitalized"] = False
-
-            is_external = False
-            event_date = None
-            if not block_computation:
-                for ue_cap in self.ue_capitalisees[etudid]:
-                    if ue_cap["ue_code"] == ue["ue_code"]:
-                        moy_ue_cap = ue_cap["moy"]
-                        mu["was_capitalized"] = True
-                        event_date = event_date or ue_cap["event_date"]
-                        if (
-                            (moy_ue_cap != "NA")
-                            and isinstance(moy_ue_cap, float)
-                            and isinstance(max_moy_ue, float)
-                            and (moy_ue_cap > max_moy_ue)
-                        ):
-                            # meilleure UE capitalisée
-                            event_date = ue_cap["event_date"]
-                            max_moy_ue = moy_ue_cap
-                            mu["is_capitalized"] = True
-                            capitalized_ue_id = ue_cap["ue_id"]
-                            formsemestre_id = ue_cap["formsemestre_id"]
-                            coef_ue = self.get_etud_ue_cap_coef(
-                                etudid, ue, ue_cap, cnx=cnx
-                            )
-                            is_external = ue_cap["is_external"]
-
-            mu["cur_moy_ue"] = mu["moy"]  # la moyenne dans le sem. courant
-            if mu["est_inscrit"]:
-                mu["cur_coef_ue"] = mu["sum_coefs"]
-            else:
-                mu["cur_coef_ue"] = 0.0
-            mu["moy"] = max_moy_ue  # la moyenne d'UE a prendre en compte
-            mu["is_external"] = is_external  # validation externe (dite "antérieure")
-            mu["coef_ue"] = coef_ue  # coef reel ou coef de l'ue si capitalisee
-
-            if mu["is_capitalized"]:
-                mu["formsemestre_id"] = formsemestre_id
-                mu["capitalized_ue_id"] = capitalized_ue_id
-            if mu["was_capitalized"]:
-                mu["event_date"] = event_date
-            # - ECTS ? ("pot" pour "potentiels" car les ECTS ne seront acquises qu'apres validation du jury
-            if (
-                isinstance(mu["moy"], float)
-                and mu["moy"] >= self.parcours.NOTES_BARRE_VALID_UE
-            ):
-                mu["ects_pot"] = ue["ects"] or 0.0
-                if ue_is_fondamentale(ue["type"]):
-                    mu["ects_pot_fond"] = mu["ects_pot"]
-                else:
-                    mu["ects_pot_fond"] = 0.0
-                if ue_is_professionnelle(ue["type"]):
-                    mu["ects_pot_pro"] = mu["ects_pot"]
-                else:
-                    mu["ects_pot_pro"] = 0.0
-            else:
-                mu["ects_pot"] = 0.0
-                mu["ects_pot_fond"] = 0.0
-                mu["ects_pot_pro"] = 0.0
-            sem_ects_pot += mu["ects_pot"]
-            sem_ects_pot_fond += mu["ects_pot_fond"]
-            sem_ects_pot_pro += mu["ects_pot_pro"]
-
-            # - Calcul moyenne générale dans le semestre:
-            if mu["is_capitalized"]:
-                try:
-                    sum_notes += mu["moy"] * mu["coef_ue"]
-                    sum_coefs += mu["coef_ue"]
-                except:  # pas de note dans cette UE
-                    pass
-            else:
-                if mu["coefs_bonus_gen"]:
-                    notes_bonus_gen.extend(mu["notes_bonus_gen"])
-                    coefs_bonus_gen.extend(mu["coefs_bonus_gen"])
-                #
-                try:
-                    sum_notes += mu["moy"] * mu["sum_coefs"]
-                    sum_coefs += mu["sum_coefs"]
-                    nb_notes = nb_notes + 1
-                except TypeError:
-                    nb_missing = nb_missing + 1
-        # Le resultat:
-        infos = dict(
-            nb_notes=nb_notes,
-            nb_missing=nb_missing,
-            sum_coefs=sum_coefs,
-            moy_ues=moy_ues,
-            ects_pot=sem_ects_pot,
-            ects_pot_fond=sem_ects_pot_fond,
-            ects_pot_pro=sem_ects_pot_pro,
-            sem=self.sem,
-        )
-        # ---- Calcul moyenne (avec bonus sport&culture)
-        if sum_coefs <= 0 or block_computation:
-            infos["moy"] = "NA"
-        else:
-            if self.use_ue_coefs:
-                # Calcul optionnel (mai 2020)
-                # moyenne pondére par leurs coefficients des moyennes d'UE
-                sum_moy_ue = 0
-                sum_coefs_ue = 0
-                for mu in moy_ues.values():
-                    # mu["moy"] can be a number, or "NA", or "ERR" (user-defined UE formulas)
-                    if (
-                        (mu["ue"]["type"] != UE_SPORT)
-                        and scu.isnumber(mu["moy"])
-                        and (mu["est_inscrit"] or mu["is_capitalized"])
-                    ):
-                        coef_ue = mu["ue"]["coefficient"]
-                        sum_moy_ue += mu["moy"] * coef_ue
-                        sum_coefs_ue += coef_ue
-                if sum_coefs_ue != 0:
-                    infos["moy"] = sum_moy_ue / sum_coefs_ue
-                else:
-                    infos["moy"] = "NA"
-            else:
-                # Calcul standard ScoDoc: moyenne pondérée des notes de modules
-                infos["moy"] = sum_notes / sum_coefs
-
-            if notes_bonus_gen and infos["moy"] != "NA":
-                # regle de calcul maison (configurable, voir bonus_sport.py)
-                if sum(coefs_bonus_gen) <= 0 and len(coefs_bonus_gen) != 1:
-                    log(
-                        "comp_etud_moy_gen: invalid or null coefficient (%s) for notes_bonus_gen=%s (etudid=%s, formsemestre_id=%s)"
-                        % (
-                            coefs_bonus_gen,
-                            notes_bonus_gen,
-                            etudid,
-                            self.formsemestre_id,
-                        )
-                    )
-                    bonus = 0
-                else:
-                    if len(coefs_bonus_gen) == 1:
-                        coefs_bonus_gen = [1.0]  # irrelevant, may be zero
-
-                    # XXX attention: utilise anciens bonus_sport, évidemment
-                    bonus_func = ScoDocSiteConfig.get_bonus_sport_func()
-                    if bonus_func:
-                        bonus = bonus_func(
-                            notes_bonus_gen, coefs_bonus_gen, infos=infos
-                        )
-                    else:
-                        bonus = 0.0
-                self.bonus[etudid] = bonus
-                infos["moy"] += bonus
-                infos["moy"] = min(infos["moy"], 20.0)  # clip bogus bonus
-
-        return infos
-
-    def get_etud_moy_gen(self, etudid):  # -> float | str
-        """Moyenne generale de cet etudiant dans ce semestre.
-        Prend en compte les UE capitalisées.
-        Si pas de notes: 'NA'
-        """
-        return self.moy_gen[etudid]
-
-    def get_etud_moy_infos(self, etudid):  # XXX OBSOLETE
-        """Infos sur moyennes"""
-        return self.etud_moy_infos[etudid]
-
-    # was etud_has_all_ue_over_threshold:
-    def etud_check_conditions_ues(self, etudid):
-        """Vrai si les conditions sur les UE sont remplies.
-        Ne considère que les UE ayant des notes (moyenne calculée).
-        (les UE sans notes ne sont pas comptées comme sous la barre)
-        Prend en compte les éventuelles UE capitalisées.
-
-        Pour les parcours habituels, cela revient à vérifier que
-        les moyennes d'UE sont toutes > à leur barre (sauf celles sans notes)
-
-        Pour les parcours non standards (LP2014), cela peut être plus compliqué.
-
-        Return: True|False, message explicatif
-        """
-        ue_status_list = []
-        for ue in self._ues:
-            ue_status = self.get_etud_ue_status(etudid, ue["ue_id"])
-            if ue_status:
-                ue_status_list.append(ue_status)
-        return self.parcours.check_barre_ues(ue_status_list)
-
-    def get_table_moyennes_triees(self):
-        return self.T
-
-    def get_etud_rang(self, etudid) -> str:
-        return self.etud_moy_gen_ranks.get(etudid, "999")
-
-    def get_etud_rang_group(self, etudid, group_id):
-        """Returns rank of etud in this group and number of etuds in group.
-        If etud not in group, returns None.
-        """
-        if not group_id in self.rangs_groupes:
-            # lazy: fill rangs_groupes on demand
-            # { groupe : { etudid : rang } }
-            if not group_id in self.group_etuds:
-                # lazy fill: list of etud in group_id
-                etuds = sco_groups.get_group_members(group_id)
-                self.group_etuds[group_id] = set([x["etudid"] for x in etuds])
-            # 1- build T restricted to group
-            Tr = []
-            for t in self.get_table_moyennes_triees():
-                t_etudid = t[-1]
-                if t_etudid in self.group_etuds[group_id]:
-                    Tr.append(t)
-            #
-            self.rangs_groupes[group_id] = comp_ranks(Tr)
-
-        return (
-            self.rangs_groupes[group_id].get(etudid, None),
-            len(self.rangs_groupes[group_id]),
-        )
-
-    def get_table_moyennes_dict(self):
-        """{ etudid : (liste des moyennes) } comme get_table_moyennes_triees"""
-        D = {}
-        for t in self.T:
-            D[t[-1]] = t
-        return D
-
-    def get_moduleimpls_attente(self):
-        "Liste des moduleimpls avec des notes en attente"
-        return self._mods_att
-
-    # Decisions existantes du jury
-    def comp_decisions_jury(self):
-        """Cherche les decisions du jury pour le semestre (pas les UE).
-        Calcule l'attribut:
-        decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}
-        decision_jury_ues={ etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' }}}
-        Si la decision n'a pas été prise, la clé etudid n'est pas présente.
-        Si l'étudiant est défaillant, met un code DEF sur toutes les UE
-        """
-        cnx = ndb.GetDBConnexion()
-        cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
-        cursor.execute(
-            """SELECT etudid, code, assidu, compense_formsemestre_id, event_date 
-            FROM scolar_formsemestre_validation 
-            WHERE formsemestre_id=%(formsemestre_id)s AND ue_id is NULL;
-            """,
-            {"formsemestre_id": self.formsemestre_id},
-        )
-        decisions_jury = {}
-        for (
-            etudid,
-            code,
-            assidu,
-            compense_formsemestre_id,
-            event_date,
-        ) in cursor.fetchall():
-            decisions_jury[etudid] = {
-                "code": code,
-                "assidu": assidu,
-                "compense_formsemestre_id": compense_formsemestre_id,
-                "event_date": ndb.DateISOtoDMY(event_date),
-            }
-
-        self.decisions_jury = decisions_jury
-        # UEs:
-        cursor.execute(
-            "select etudid, ue_id, code, event_date from scolar_formsemestre_validation where formsemestre_id=%(formsemestre_id)s and ue_id is not NULL;",
-            {"formsemestre_id": self.formsemestre_id},
-        )
-        decisions_jury_ues = {}
-        for (etudid, ue_id, code, event_date) in cursor.fetchall():
-            if etudid not in decisions_jury_ues:
-                decisions_jury_ues[etudid] = {}
-            # Calcul des ECTS associes a cette UE:
-            ects = 0.0
-            if codes_cursus.code_ue_validant(code):
-                ue = self.uedict.get(ue_id, None)
-                if ue is None:  # not in list for this sem ??? (probably an error)
-                    log(
-                        "Warning: %s capitalized an UE %s which is not part of current sem %s"
-                        % (etudid, ue_id, self.formsemestre_id)
-                    )
-                    ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
-                    self.uedict[ue_id] = ue  # record this UE
-                    if ue_id not in self._uecoef:
-                        cl = formsemestre_uecoef_list(
-                            cnx,
-                            args={
-                                "formsemestre_id": self.formsemestre_id,
-                                "ue_id": ue_id,
-                            },
-                        )
-                        if not cl:
-                            # cas anormal: UE capitalisee, pas dans ce semestre, et sans coef
-                            log("Warning: setting UE coef to zero")
-                            formsemestre_uecoef_create(
-                                cnx,
-                                args={
-                                    "formsemestre_id": self.formsemestre_id,
-                                    "ue_id": ue_id,
-                                    "coefficient": 0,
-                                },
-                            )
-
-                ects = ue["ects"] or 0.0  # 0 if None
-
-            decisions_jury_ues[etudid][ue_id] = {
-                "code": code,
-                "ects": ects,  # 0. si non UE validée ou si mode de calcul different (?)
-                "event_date": ndb.DateISOtoDMY(event_date),
-            }
-
-        self.decisions_jury_ues = decisions_jury_ues
-
-    def get_etud_decision_sem(self, etudid):
-        """Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu.
-        { 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id }
-        Si état défaillant, force le code a DEF
-        """
-        if self.get_etud_etat(etudid) == DEF:
-            return {
-                "code": DEF,
-                "assidu": False,
-                "event_date": "",
-                "compense_formsemestre_id": None,
-            }
-        else:
-            return self.decisions_jury.get(etudid, None)
-
-    def get_etud_decisions_ue(self, etudid):
-        """Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu.
-        Ne tient pas compte des UE capitalisées.
-        { ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : }
-        Ne renvoie aucune decision d'UE pour les défaillants
-        """
-        if self.get_etud_etat(etudid) == DEF:
-            return {}
-        else:
-            return self.decisions_jury_ues.get(etudid, None)
-
-    def sem_has_decisions(self):
-        """True si au moins une decision de jury dans ce semestre"""
-        if [x for x in self.decisions_jury_ues.values() if x]:
-            return True
-
-        return len([x for x in self.decisions_jury_ues.values() if x]) > 0
-
-    def etud_has_decision(self, etudid):
-        """True s'il y a une décision de jury pour cet étudiant"""
-        return self.get_etud_decisions_ue(etudid) or self.get_etud_decision_sem(etudid)
-
-    def all_etuds_have_sem_decisions(self):
-        """True si tous les étudiants du semestre ont une décision de jury.
-        ne regarde pas les décisions d'UE (todo: à voir ?)
-        """
-        for etudid in self.get_etudids():
-            if self.inscrdict[etudid]["etat"] == "D":
-                continue  # skip demissionnaires
-            if self.get_etud_decision_sem(etudid) is None:
-                return False
-        return True
-
-    # Capitalisation des UEs
-    def comp_ue_capitalisees(self):
-        """Cherche pour chaque etudiant ses UE capitalisées dans ce semestre.
-        Calcule l'attribut:
-        ue_capitalisees = { etudid :
-                             [{ 'moy':, 'event_date' : ,'formsemestre_id' : }, ...] }
-        """
-        self.ue_capitalisees = scu.DictDefault(defaultvalue=[])
-        cnx = None
-        semestre_id = self.sem["semestre_id"]
-        for etudid in self.get_etudids():
-            capital = formsemestre_get_etud_capitalisation(
-                self.formation["id"],
-                semestre_id,
-                ndb.DateDMYtoISO(self.sem["date_debut"]),
-                etudid,
-            )
-            for ue_cap in capital:
-                # Si la moyenne d'UE n'avait pas été stockée (anciennes versions de ScoDoc)
-                # il faut la calculer ici et l'enregistrer
-                if ue_cap["moy_ue"] is None:
-                    log(
-                        "comp_ue_capitalisees: recomputing UE moy (etudid=%s, ue_id=%s formsemestre_id=%s)"
-                        % (etudid, ue_cap["ue_id"], ue_cap["formsemestre_id"])
-                    )
-                    nt_cap = sco_cache.NotesTableCache.get(
-                        ue_cap["formsemestre_id"]
-                    )  # > UE capitalisees par un etud
-                    ue_cap_status = nt_cap.get_etud_ue_status(etudid, ue_cap["ue_id"])
-                    if ue_cap_status:
-                        moy_ue_cap = ue_cap_status["moy"]
-                    else:
-                        moy_ue_cap = ""
-                    ue_cap["moy_ue"] = moy_ue_cap
-                    if (
-                        isinstance(moy_ue_cap, float)
-                        and moy_ue_cap >= self.parcours.NOTES_BARRE_VALID_UE
-                    ):
-                        if not cnx:
-                            cnx = ndb.GetDBConnexion()
-                        sco_cursus_dut.do_formsemestre_validate_ue(
-                            cnx,
-                            nt_cap,
-                            ue_cap["formsemestre_id"],
-                            etudid,
-                            ue_cap["ue_id"],
-                            ue_cap["code"],
-                        )
-                    else:
-                        log(
-                            "*** valid inconsistency: moy_ue_cap=%s (etudid=%s, ue_id=%s formsemestre_id=%s)"
-                            % (
-                                moy_ue_cap,
-                                etudid,
-                                ue_cap["ue_id"],
-                                ue_cap["formsemestre_id"],
-                            )
-                        )
-                ue_cap["moy"] = ue_cap["moy_ue"]  # backward compat (needs refactoring)
-                self.ue_capitalisees[etudid].append(ue_cap)
-        if cnx:
-            cnx.commit()
-        # log('comp_ue_capitalisees=\n%s' % pprint.pformat(self.ue_capitalisees) )
-
-    # def comp_etud_sum_coef_modules_ue( etudid, ue_id):
-    #     """Somme des coefficients des modules de l'UE dans lesquels cet étudiant est inscrit
-    #     ou None s'il n'y a aucun module
-    #     """
-    #     c_list = [ mod['module']['coefficient']
-    #                for mod in self._modimpls
-    #                if (( mod['module']['ue_id'] == ue_id)
-    #                    and self._modmoys[mod['moduleimpl_id']].get(etudid, False) is not False)
-    #     ]
-    #     if not c_list:
-    #         return None
-    #     return sum(c_list)
-
-    def get_etud_ue_cap_coef(self, etudid, ue, ue_cap, cnx=None):
-        """Calcule le coefficient d'une UE capitalisée, pour cet étudiant,
-        injectée dans le semestre courant.
-
-        ue : ue du semestre courant
-
-        ue_cap = resultat de formsemestre_get_etud_capitalisation
-        { 'ue_id' (dans le semestre source),
-          'ue_code', 'moy', 'event_date','formsemestre_id' }
-        """
-        # log("get_etud_ue_cap_coef\nformsemestre_id='%s'\netudid='%s'\nue=%s\nue_cap=%s\n" % (self.formsemestre_id, etudid, ue, ue_cap))
-        # 1- Coefficient explicitement déclaré dans le semestre courant pour cette UE ?
-        if ue["ue_id"] not in self._uecoef:
-            self._uecoef[ue["ue_id"]] = formsemestre_uecoef_list(
-                cnx,
-                args={"formsemestre_id": self.formsemestre_id, "ue_id": ue["ue_id"]},
-            )
-
-        if len(self._uecoef[ue["ue_id"]]):
-            # utilisation du coef manuel
-            return self._uecoef[ue["ue_id"]][0]["coefficient"]
-
-        # 2- Mode automatique: calcul du coefficient
-        # Capitalisation depuis un autre semestre ScoDoc ?
-        coef = None
-        if ue_cap["formsemestre_id"]:
-            # Somme des coefs dans l'UE du semestre d'origine (nouveau: 23/01/2016)
-            coef = comp_etud_sum_coef_modules_ue(
-                ue_cap["formsemestre_id"], etudid, ue_cap["ue_id"]
-            )
-        if coef != None:
-            return coef
-        else:
-            # Capitalisation UE externe: quel coef appliquer ?
-            # Si l'étudiant est inscrit dans le semestre courant,
-            # somme des coefs des modules de l'UE auxquels il est inscrit
-            c = comp_etud_sum_coef_modules_ue(self.formsemestre_id, etudid, ue["ue_id"])
-            if c is not None:  # inscrit à au moins un module de cette UE
-                return c
-            # arfff: aucun moyen de déterminer le coefficient de façon sûre
-            log(
-                "* oups: calcul coef UE impossible\nformsemestre_id='%s'\netudid='%s'\nue=%s\nue_cap=%s"
-                % (self.formsemestre_id, etudid, ue, ue_cap)
-            )
-            raise ScoValueError(
-                """<div class="scovalueerror"><p>Coefficient de l'UE capitalisée %s impossible à déterminer
-                pour l'étudiant <a href="%s" class="discretelink">%s</a></p>
-                <p>Il faut <a href="%s">saisir le coefficient de cette UE avant de continuer</a></p>
-                </div>
-                """
-                % (
-                    ue["acronyme"],
-                    url_for(
-                        "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
-                    ),
-                    self.get_nom_long(etudid),
-                    url_for(
-                        "notes.formsemestre_edit_uecoefs",
-                        scodoc_dept=g.scodoc_dept,
-                        formsemestre_id=self.formsemestre_id,
-                        err_ue_id=ue["ue_id"],
-                    ),
-                )
-            )
-
-        return 0.0  # ?
-
-    def get_etud_ue_status(self, etudid, ue_id):
-        "Etat de cette UE (note, coef, capitalisation, ...)"
-        return self._etud_moy_ues[etudid][ue_id]
-
-    def etud_has_notes_attente(self, etudid):
-        """Vrai si cet etudiant a au moins une note en attente dans ce semestre.
-        (ne compte que les notes en attente dans des évaluation avec coef. non nul).
-        """
-        cnx = ndb.GetDBConnexion()
-        cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
-        cursor.execute(
-            """SELECT n.*
-            FROM notes_notes n, notes_evaluation e, notes_moduleimpl m,
-            notes_moduleimpl_inscription i
-            WHERE n.etudid = %(etudid)s
-            and n.value = %(code_attente)s
-            and n.evaluation_id = e.id
-            and e.moduleimpl_id = m.id
-            and m.formsemestre_id = %(formsemestre_id)s
-            and e.coefficient != 0
-            and m.id = i.moduleimpl_id
-            and i.etudid=%(etudid)s
-            """,
-            {
-                "formsemestre_id": self.formsemestre_id,
-                "etudid": etudid,
-                "code_attente": scu.NOTES_ATTENTE,
-            },
-        )
-        return len(cursor.fetchall()) > 0
-
-    def get_evaluations_etats(self):  # evaluation_list_in_sem
-        """[ {...evaluation et son etat...} ]"""
-        if self._evaluations_etats is None:
-            self._evaluations_etats = sco_evaluations.do_evaluation_list_in_sem(
-                self.formsemestre_id
-            )
-
-        return self._evaluations_etats
-
-    def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
-        """Liste des évaluations de ce module"""
-        return [
-            e
-            for e in self.get_evaluations_etats()
-            if e["moduleimpl_id"] == moduleimpl_id
-        ]
-
-    def apc_recompute_moyennes(self):
-        """recalcule les moyennes en APC (BUT)
-        et modifie en place le tableau T.
-        XXX Raccord provisoire avant refonte de cette classe.
-        """
-        assert self.parcours.APC_SAE
-        formsemestre = FormSemestre.query.get(self.formsemestre_id)
-        results = bulletin_but.ResultatsSemestreBUT(formsemestre)
-
-        # Rappel des épisodes précédents: T est une liste de liste
-        # Colonnes: 0 moy_gen, moy_ue1, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid
-        ues = self.get_ues_stat_dict()  # incluant le(s) UE de sport
-        for t in self.T:
-            etudid = t[-1]
-            if etudid in results.etud_moy_gen:  # evite les démissionnaires
-                t[0] = results.etud_moy_gen[etudid]
-                for i, ue in enumerate(ues, start=1):
-                    if ue["type"] != UE_SPORT:
-                        # temporaire pour 9.1.29 !
-                        if ue["id"] in results.etud_moy_ue:
-                            t[i] = results.etud_moy_ue[ue["id"]][etudid]
-                        else:
-                            t[i] = ""
-        # re-trie selon la nouvelle moyenne générale:
-        self.T.sort(key=self._row_key)
-        # Remplace aussi le rang:
-        self.etud_moy_gen_ranks = results.etud_moy_gen_ranks
diff --git a/app/scodoc/sco_compute_moy.py b/app/scodoc/sco_compute_moy.py
index fb162170c..e2642f5e9 100644
--- a/app/scodoc/sco_compute_moy.py
+++ b/app/scodoc/sco_compute_moy.py
@@ -25,43 +25,22 @@
 #
 ##############################################################################
 
-"""Calcul des moyennes de module
+"""Calcul des moyennes de module (restes de fonctions ScoDoc 7)
 """
-import pprint
-import traceback
-
-from flask import url_for, g
-import app.scodoc.sco_utils as scu
+from app.models import ModuleImpl
 import app.scodoc.notesdb as ndb
-from app.scodoc.sco_utils import (
-    ModuleType,
-    NOTES_ATTENTE,
-    NOTES_NEUTRALISE,
-    EVALUATION_NORMALE,
-    EVALUATION_RATTRAPAGE,
-    EVALUATION_SESSION2,
-)
-from app.scodoc.sco_exceptions import ScoValueError
-from app import log
-from app.scodoc import sco_abs
-from app.scodoc import sco_edit_module
-from app.scodoc import sco_evaluation_db
-from app.scodoc import sco_formsemestre
-from app.scodoc import sco_formsemestre_inscriptions
-from app.scodoc import sco_formulas
-from app.scodoc import sco_moduleimpl
-from app.scodoc import sco_etud
 
 
-def moduleimpl_has_expression(mod):
-    "True if we should use a user-defined expression"
-    expr = mod["computation_expr"]
-    if not expr:
-        return False
-    expr = expr.strip()
-    if not expr or expr[0] == "#":
-        return False
-    return True
+def moduleimpl_has_expression(modimpl: ModuleImpl):
+    """True if we should use a user-defined expression
+    En ScoDoc 9, utilisé pour afficher un avertissement, l'expression elle même
+    n'est plus supportée.
+    """
+    return (
+        modimpl.computation_expr
+        and modimpl.computation_expr.strip()
+        and modimpl.computation_expr.strip()[0] != "#"
+    )
 
 
 def formsemestre_expressions_use_abscounts(formsemestre_id):
@@ -81,9 +60,10 @@ def formsemestre_expressions_use_abscounts(formsemestre_id):
         if expr and expr[0] != "#" and ab in expr:
             return True
     # 2- moyennes de modules
-    for mod in sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id):
-        if moduleimpl_has_expression(mod) and ab in mod["computation_expr"]:
-            return True
+    # #sco9 il n'y a plus d'expressions
+    # for mod in sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id):
+    #    if moduleimpl_has_expression(mod) and ab in mod["computation_expr"]:
+    #        return True
     return False
 
 
@@ -120,296 +100,3 @@ def get_ue_expression(formsemestre_id, ue_id, html_quote=False):
             return expr
         else:
             return None
-
-
-def compute_user_formula(
-    sem,
-    etudid,
-    moy,
-    moy_valid,
-    notes,
-    coefs,
-    coefs_mask,
-    formula,
-    diag_info=None,  # infos supplementaires a placer ds messages d'erreur
-    use_abs=True,
-):
-    """Calcul moyenne a partir des notes et coefs, en utilisant la formule utilisateur (une chaine).
-    Retourne moy, et en cas d'erreur met à jour diag_info (msg)
-    """
-    if use_abs:
-        nbabs, nbabs_just = sco_abs.get_abs_count(etudid, sem)
-    else:
-        nbabs, nbabs_just = 0, 0
-    try:
-        moy_val = float(moy)
-    except ValueError:
-        moy_val = 0.0  # 0. when no valid value
-    variables = {
-        "cmask": coefs_mask,  # NoteVector(v=coefs_mask),
-        "notes": notes,  # NoteVector(v=notes),
-        "coefs": coefs,  # NoteVector(v=coefs),
-        "moy": moy,
-        "moy_valid": moy_valid,  # deprecated, use moy_is_valid
-        "moy_is_valid": moy_valid,  # True si moyenne numerique
-        "moy_val": moy_val,
-        "nb_abs": float(nbabs),
-        "nb_abs_just": float(nbabs_just),
-        "nb_abs_nojust": float(nbabs - nbabs_just),
-    }
-    try:
-        formula = formula.replace("\n", "").replace("\r", "")
-        # log('expression : %s\nvariables=%s\n' % (formula, variables)) #  debug
-        user_moy = sco_formulas.eval_user_expression(formula, variables)
-        # log('user_moy=%s' % user_moy)
-        if user_moy != "NA":
-            user_moy = float(user_moy)
-            if (user_moy > 20) or (user_moy < 0):
-                etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
-
-                raise ScoValueError(
-                    f"""
-                    Valeur moyenne {user_moy} hors limite pour
-                    <a href="{url_for('notes.formsemestre_bulletinetud',
-                    scodoc_dept=g.scodoc_dept,
-                    formsemestre_id=sem["formsemestre_id"],
-                    etudid=etudid
-                    )}">{etud["nomprenom"]}</a>"""
-                )
-    except:
-        log(
-            "invalid expression : %s\nvariables=%s\n"
-            % (formula, pprint.pformat(variables))
-        )
-        tb = traceback.format_exc()
-        log("Exception during evaluation:\n%s\n" % tb)
-        diag_info.update({"msg": tb.splitlines()[-1]})
-        user_moy = "ERR"
-
-    # log('formula=%s\nvariables=%s\nmoy=%s\nuser_moy=%s' % (formula, variables, moy, user_moy))
-
-    return user_moy
-
-
-# XXX OBSOLETE
-def compute_moduleimpl_moyennes(nt, modimpl):
-    """Retourne dict { etudid : note_moyenne } pour tous les etuds inscrits
-    au moduleimpl mod, la liste des evaluations "valides" (toutes notes entrées
-    ou en attente), et att (vrai s'il y a des notes en attente dans ce module).
-    La moyenne est calculée en utilisant les coefs des évaluations.
-    Les notes NEUTRES (abs. excuses) ne sont pas prises en compte.
-    Les notes ABS sont remplacées par des zéros.
-    S'il manque des notes et que le coef n'est pas nul,
-    la moyenne n'est pas calculée: NA
-    Ne prend en compte que les evaluations où toutes les notes sont entrées.
-    Le résultat note_moyenne est une note sur 20.
-    """
-    diag_info = {}  # message d'erreur formule
-    moduleimpl_id = modimpl["moduleimpl_id"]
-    is_malus = modimpl["module"]["module_type"] == ModuleType.MALUS
-    sem = sco_formsemestre.get_formsemestre(modimpl["formsemestre_id"])
-    etudids = sco_moduleimpl.moduleimpl_listeetuds(
-        moduleimpl_id
-    )  # tous, y compris demissions
-    # Inscrits au semestre (pour traiter les demissions):
-    inssem_set = set(
-        [
-            x["etudid"]
-            for x in sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits(
-                modimpl["formsemestre_id"]
-            )
-        ]
-    )
-    insmod_set = inssem_set.intersection(etudids)  # inscrits au semestre et au module
-
-    evals = nt.get_mod_evaluation_etat_list(moduleimpl_id)
-    evals.sort(
-        key=lambda x: (x["numero"], x["jour"], x["heure_debut"])
-    )  # la plus ancienne en tête
-
-    user_expr = moduleimpl_has_expression(modimpl)
-    attente = False
-    # récupere les notes de toutes les evaluations
-    eval_rattr = None
-    for e in evals:
-        e["nb_inscrits"] = e["etat"]["nb_inscrits"]
-        # XXX OBSOLETE
-        notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
-            e["evaluation_id"]
-        )  # toutes, y compris demissions
-        # restreint aux étudiants encore inscrits à ce module
-        notes = [
-            notes_db[etudid]["value"] for etudid in notes_db if (etudid in insmod_set)
-        ]
-        e["nb_notes"] = len(notes)
-        e["nb_abs"] = len([x for x in notes if x is None])
-        e["nb_neutre"] = len([x for x in notes if x == NOTES_NEUTRALISE])
-        e["nb_att"] = len([x for x in notes if x == NOTES_ATTENTE])
-        e["notes"] = notes_db
-
-        if e["etat"]["evalattente"]:
-            attente = True
-        if (
-            e["evaluation_type"] == EVALUATION_RATTRAPAGE
-            or e["evaluation_type"] == EVALUATION_SESSION2
-        ):
-            if eval_rattr:
-                # !!! plusieurs rattrapages !
-                diag_info.update(
-                    {
-                        "msg": "plusieurs évaluations de rattrapage !",
-                        "moduleimpl_id": moduleimpl_id,
-                    }
-                )
-            eval_rattr = e
-
-    # Les modules MALUS ne sont jamais considérés en attente
-    if is_malus:
-        attente = False
-
-    # filtre les evals valides (toutes les notes entrées)
-    valid_evals = [
-        e
-        for e in evals
-        if (
-            (e["etat"]["evalcomplete"] or e["etat"]["evalattente"])
-            and (e["note_max"] > 0)
-        )
-    ]
-    #
-    R = {}
-    formula = scu.unescape_html(modimpl["computation_expr"])
-    formula_use_abs = "abs" in formula
-
-    for etudid in insmod_set:  # inscrits au semestre et au module
-        sum_notes = 0.0
-        sum_coefs = 0.0
-        nb_missing = 0
-        for e in valid_evals:
-            if e["evaluation_type"] != EVALUATION_NORMALE:
-                continue
-            if etudid in e["notes"]:
-                note = e["notes"][etudid]["value"]
-                if note is None:  # ABSENT
-                    note = 0
-                if note != NOTES_NEUTRALISE and note != NOTES_ATTENTE:
-                    sum_notes += (note * 20.0 / e["note_max"]) * e["coefficient"]
-                    sum_coefs += e["coefficient"]
-            else:
-                # il manque une note ! (si publish_incomplete, cela peut arriver, on ignore)
-                if e["coefficient"] > 0 and not e["publish_incomplete"]:
-                    nb_missing += 1
-                    # ne devrait pas arriver ?
-                    log("\nXXX SCM298\n")
-        if nb_missing == 0 and sum_coefs > 0:
-            if sum_coefs > 0:
-                R[etudid] = sum_notes / sum_coefs
-                moy_valid = True
-            else:
-                R[etudid] = "NA"
-                moy_valid = False
-        else:
-            R[etudid] = "NA"
-            moy_valid = False
-
-        if user_expr:
-            # recalcule la moyenne en utilisant la formule utilisateur
-            notes = []
-            coefs = []
-            coefs_mask = []  # 0/1, 0 si coef a ete annulé
-            nb_notes = 0  # nombre de notes valides
-            for e in evals:
-                if (
-                    (e["etat"]["evalcomplete"] or e["etat"]["evalattente"])
-                    and etudid in e["notes"]
-                ) and (e["note_max"] > 0):
-                    note = e["notes"][etudid]["value"]
-                    if note is None:
-                        note = 0
-                    if note != NOTES_NEUTRALISE and note != NOTES_ATTENTE:
-                        notes.append(note * 20.0 / e["note_max"])
-                        coefs.append(e["coefficient"])
-                        coefs_mask.append(1)
-                        nb_notes += 1
-                    else:
-                        notes.append(0.0)
-                        coefs.append(0.0)
-                        coefs_mask.append(0)
-                else:
-                    notes.append(0.0)
-                    coefs.append(0.0)
-                    coefs_mask.append(0)
-            if nb_notes > 0 or formula_use_abs:
-                user_moy = compute_user_formula(
-                    sem,
-                    etudid,
-                    R[etudid],
-                    moy_valid,
-                    notes,
-                    coefs,
-                    coefs_mask,
-                    formula,
-                    diag_info=diag_info,
-                    use_abs=formula_use_abs,
-                )
-                if diag_info:
-                    diag_info["moduleimpl_id"] = moduleimpl_id
-                R[etudid] = user_moy
-        # Note de rattrapage ou deuxième session ?
-        if eval_rattr:
-            if etudid in eval_rattr["notes"]:
-                note = eval_rattr["notes"][etudid]["value"]
-                if note != None and note != NOTES_NEUTRALISE and note != NOTES_ATTENTE:
-                    if not isinstance(R[etudid], float):
-                        R[etudid] = note
-                    else:
-                        note_sur_20 = note * 20.0 / eval_rattr["note_max"]
-                        if eval_rattr["evaluation_type"] == EVALUATION_RATTRAPAGE:
-                            # rattrapage classique: prend la meilleure note entre moyenne
-                            # module et note eval rattrapage
-                            if (R[etudid] == "NA") or (note_sur_20 > R[etudid]):
-                                # log('note_sur_20=%s' % note_sur_20)
-                                R[etudid] = note_sur_20
-                        elif eval_rattr["evaluation_type"] == EVALUATION_SESSION2:
-                            # rattrapage type "deuxième session": remplace la note moyenne
-                            R[etudid] = note_sur_20
-
-    return R, valid_evals, attente, diag_info
-
-
-def formsemestre_compute_modimpls_moyennes(nt, formsemestre_id):
-    """retourne dict { moduleimpl_id : { etudid, note_moyenne_dans_ce_module } },
-    la liste des moduleimpls, la liste des evaluations valides,
-    liste des moduleimpls  avec notes en attente.
-    """
-    # sem = sco_formsemestre.get_formsemestre( formsemestre_id)
-    # inscr = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
-    #    args={"formsemestre_id": formsemestre_id}
-    # )
-    # etudids = [x["etudid"] for x in inscr]
-    modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
-    # recupere les moyennes des etudiants de tous les modules
-    D = {}
-    valid_evals = []
-    valid_evals_per_mod = {}  # { moduleimpl_id : eval }
-    mods_att = []
-    expr_diags = []
-    for modimpl in modimpls:
-        mod = sco_edit_module.module_list(args={"module_id": modimpl["module_id"]})[0]
-        modimpl["module"] = mod  # add module dict to moduleimpl (used by nt)
-        moduleimpl_id = modimpl["moduleimpl_id"]
-        assert moduleimpl_id not in D
-        (
-            D[moduleimpl_id],
-            valid_evals_mod,
-            attente,
-            expr_diag,
-        ) = compute_moduleimpl_moyennes(nt, modimpl)
-        valid_evals_per_mod[moduleimpl_id] = valid_evals_mod
-        valid_evals += valid_evals_mod
-        if attente:
-            mods_att.append(modimpl)
-        if expr_diag:
-            expr_diags.append(expr_diag)
-    #
-    return D, modimpls, valid_evals_per_mod, valid_evals, mods_att, expr_diags
diff --git a/app/scodoc/sco_edit_formation.py b/app/scodoc/sco_edit_formation.py
index 11c31436c..e712a8bc1 100644
--- a/app/scodoc/sco_edit_formation.py
+++ b/app/scodoc/sco_edit_formation.py
@@ -29,18 +29,16 @@
 (portage from DTML)
 """
 import flask
-from flask import g, url_for, request
+from flask import flash, g, url_for, request
 import sqlalchemy
 
 from app import db
-from app import log
 from app.models import SHORT_STR_LEN
 from app.models.formations import Formation
 from app.models.modules import Module
 from app.models.ues import UniteEns
 from app.models import ScolarNews
 
-import app.scodoc.notesdb as ndb
 import app.scodoc.sco_utils as scu
 from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
 from app.scodoc.sco_exceptions import ScoValueError, ScoNonEmptyFormationObject
@@ -49,7 +47,6 @@ from app.scodoc import html_sco_header
 from app.scodoc import sco_cache
 from app.scodoc import codes_cursus
 from app.scodoc import sco_edit_ue
-from app.scodoc import sco_formations
 from app.scodoc import sco_formsemestre
 
 
@@ -283,38 +280,55 @@ def formation_edit(formation_id=None, create=False):
             )
         #
         if create:
-            formation_id = do_formation_create(tf[2])
+            formation = do_formation_create(tf[2])
         else:
             do_formation_edit(tf[2])
+            flash(
+                f"""Création de la formation {
+            formation.titre} ({formation.acronyme}) version {formation.version}"""
+            )
         return flask.redirect(
             url_for(
-                "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id
+                "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation.id
             )
         )
 
 
-def do_formation_create(args):
+def do_formation_create(args: dict) -> Formation:
     "create a formation"
-    cnx = ndb.GetDBConnexion()
-    # check unique acronyme/titre/version
-    a = args.copy()
-    if "formation_id" in a:
-        del a["formation_id"]
-    f_dicts = sco_formations.formation_list(args=a)
-    if len(f_dicts) > 0:
-        log(f"do_formation_create: error: {len(f_dicts)} formations matching args={a}")
-        raise ScoValueError(f"Formation non unique ({a}) !")
-    # Si pas de formation_code, l'enleve (default SQL)
-    if "formation_code" in args and not args["formation_code"]:
-        del args["formation_code"]
-    #
-    r = sco_formations._formationEditor.create(cnx, args)
+    formation = Formation(
+        dept_id=g.scodoc_dept_id,
+        acronyme=args["acronyme"].strip(),
+        titre=args["titre"].strip(),
+        titre_officiel=args["titre_officiel"].strip(),
+        version=args.get("version"),
+        commentaire=scu.strip_str(args["commentaire"]),
+        formation_code=args.get("formation_code", "").strip() or None,
+        type_parcours=args.get("type_parcours"),
+        code_specialite=args.get("code_specialite").strip() or None,
+        referentiel_competence_id=args.get("referentiel_competence_id"),
+    )
+    db.session.add(formation)
+
+    try:
+        db.session.commit()
+    except sqlalchemy.exc.IntegrityError as exc:
+        db.session.rollback()
+        raise ScoValueError(
+            "On ne peut pas créer deux formations avec mêmes acronymes, titres et versions !",
+            dest_url=url_for(
+                "notes.formation_edit",
+                scodoc_dept=g.scodoc_dept,
+                formation_id=formation.id,
+            ),
+        ) from exc
 
     ScolarNews.add(
         typ=ScolarNews.NEWS_FORM,
-        text="Création de la formation %(titre)s (%(acronyme)s)" % args,
+        text=f"""Création de la formation {
+            formation.titre} ({formation.acronyme}) version {formation.version}""",
     )
-    return r
+    return formation
 
 
 def do_formation_edit(args):
diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py
index b9a44d3a3..9022169de 100644
--- a/app/scodoc/sco_formations.py
+++ b/app/scodoc/sco_formations.py
@@ -303,8 +303,8 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
     f_dict["version"] = version + 1
 
     # create formation
-    formation_id = sco_edit_formation.do_formation_create(f_dict)
-    log(f"formation {formation_id} created")
+    formation = sco_edit_formation.do_formation_create(f_dict)
+    log(f"formation {formation.id} created")
 
     ues_old2new = {}  # xml ue_id : new ue_id
     modules_old2new = {}  # xml module_id : new module_id
@@ -316,7 +316,7 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
         # -- create UEs
         for ue_info in D[2]:
             assert ue_info[0] == "ue"
-            ue_info[1]["formation_id"] = formation_id
+            ue_info[1]["formation_id"] = formation.id
             if "ue_id" in ue_info[1]:
                 xml_ue_id = int(ue_info[1]["ue_id"])
                 del ue_info[1]["ue_id"]
@@ -365,7 +365,7 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
                         del mod_info[1]["module_id"]
                     else:
                         xml_module_id = None
-                    mod_info[1]["formation_id"] = formation_id
+                    mod_info[1]["formation_id"] = formation.id
                     mod_info[1]["matiere_id"] = mat_id
                     mod_info[1]["ue_id"] = ue_id
                     if not "module_type" in mod_info[1]:
@@ -428,14 +428,15 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
             }
             module.set_ue_coef_dict(ue_coef_dict)
         db.session.commit()
-    return formation_id, modules_old2new, ues_old2new
+    return formation.id, modules_old2new, ues_old2new
 
 
-def formation_list_table(formation_id=None, args={}):
+def formation_list_table(formation_id=None, args: dict = None):
     """List formation, grouped by titre and sorted by versions
     and listing associated semestres
     returns a table
     """
+    args = args or {}
     formations = formation_list(formation_id=formation_id, args=args)
     title = "Programmes pédagogiques"
     lockicon = scu.icontag(
diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py
index bc7a26d8d..e262f8456 100644
--- a/app/scodoc/sco_formsemestre_edit.py
+++ b/app/scodoc/sco_formsemestre_edit.py
@@ -53,6 +53,7 @@ from app.scodoc import html_sco_header
 from app.scodoc import codes_cursus
 from app.scodoc import sco_compute_moy
 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_evaluation_db
 from app.scodoc import sco_formations
@@ -1707,12 +1708,10 @@ def formsemestre_change_publication_bul(
 
 def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
     """Changement manuel des coefficients des UE capitalisées."""
-    from app.scodoc import notes_table
 
     ok, err = sco_permissions_check.check_access_diretud(formsemestre_id)
     if not ok:
         return err
-    sem = sco_formsemestre.get_formsemestre(formsemestre_id)
 
     footer = html_sco_header.sco_footer()
     help = """<p class="help">
@@ -1741,7 +1740,7 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
         help,
     ]
     #
-    ues, modimpls = notes_table.get_sem_ues_modimpls(formsemestre_id)
+    ues, modimpls = _get_sem_ues_modimpls(formsemestre_id)
     for ue in ues:
         ue["sum_coefs"] = sum(
             [
@@ -1865,6 +1864,24 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
             """
 
 
+def _get_sem_ues_modimpls(formsemestre_id, modimpls=None):
+    """Get liste des UE du semestre (à partir des moduleimpls)
+    (utilisé quand on ne peut pas construire nt et faire nt.get_ues_stat_dict())
+    """
+    if modimpls is None:
+        modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
+    uedict = {}
+    for modimpl in modimpls:
+        mod = sco_edit_module.module_list(args={"module_id": modimpl["module_id"]})[0]
+        modimpl["module"] = mod
+        if not mod["ue_id"] in uedict:
+            ue = sco_edit_ue.ue_list(args={"ue_id": mod["ue_id"]})[0]
+            uedict[ue["ue_id"]] = ue
+    ues = list(uedict.values())
+    ues.sort(key=lambda u: u["numero"])
+    return ues, modimpls
+
+
 # ----- identification externe des sessions (pour SOJA et autres logiciels)
 def get_formsemestre_session_id(sem, code_specialite, parcours):
     """Identifiant de session pour ce semestre
diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py
index 133a50240..f1077b4f4 100644
--- a/app/scodoc/sco_formsemestre_status.py
+++ b/app/scodoc/sco_formsemestre_status.py
@@ -577,8 +577,9 @@ def fill_formsemestre(sem):
         }">{eyeicon}</a>"""
     else:
         sem["eyelink"] = ""
-    F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
-    sem["formation"] = F
+    sem["formation"] = Formation.query.get_or_404(sem["formation_id"]).to_dict(
+        with_departement=False
+    )
     parcours = codes_cursus.get_cursus_from_code(F["type_parcours"])
     if sem["semestre_id"] != -1:
         sem["num_sem"] = f""", {parcours.SESSION_NAME} {sem["semestre_id"]}"""
diff --git a/app/scodoc/sco_inscr_passage.py b/app/scodoc/sco_inscr_passage.py
index 1a5953be2..1ebf679fb 100644
--- a/app/scodoc/sco_inscr_passage.py
+++ b/app/scodoc/sco_inscr_passage.py
@@ -36,7 +36,7 @@ from flask import url_for, g, request
 import app.scodoc.notesdb as ndb
 import app.scodoc.sco_utils as scu
 from app import log
-from app.models import FormSemestre
+from app.models import Formation, FormSemestre
 from app.scodoc.gen_tables import GenTable
 from app.scodoc import html_sco_header
 from app.scodoc import sco_cache
@@ -261,8 +261,8 @@ def list_source_sems(sem, delai=None) -> list[dict]:
         if s["semestre_id"] == codes_cursus.NO_SEMESTRE_ID:
             continue
         #
-        F = sco_formations.formation_list(args={"formation_id": s["formation_id"]})[0]
-        parcours = codes_cursus.get_cursus_from_code(F["type_parcours"])
+        formation: Formation = Formation.query.get_or_404(s["formation_id"])
+        parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
         if not parcours.ALLOW_SEM_SKIP:
             if s["semestre_id"] < (sem["semestre_id"] - 1):
                 continue
diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py
index 3e6b256d8..a70cb0f49 100644
--- a/app/scodoc/sco_moduleimpl_status.py
+++ b/app/scodoc/sco_moduleimpl_status.py
@@ -38,9 +38,7 @@ from app.auth.models import User
 from app.comp import res_sem
 from app.comp.res_common import ResultatsSemestre
 from app.comp.res_compat import NotesTableCompat
-from app.models import FormSemestre, ModuleImpl
-from app.models.evaluations import Evaluation
-from app.models.ues import UniteEns
+from app.models import Evaluation, FormSemestre, Module, ModuleImpl, UniteEns
 import app.scodoc.sco_utils as scu
 from app.scodoc.codes_cursus import UE_SPORT
 from app.scodoc.sco_exceptions import ScoInvalidIdType
@@ -51,11 +49,8 @@ from app.scodoc import html_sco_header
 from app.scodoc import htmlutils
 from app.scodoc import sco_abs
 from app.scodoc import sco_compute_moy
-from app.scodoc import sco_edit_module
 from app.scodoc import sco_evaluations
 from app.scodoc import sco_evaluation_db
-from app.scodoc import sco_formations
-from app.scodoc import sco_formsemestre
 from app.scodoc import sco_formsemestre_status
 from app.scodoc import sco_groups
 from app.scodoc import sco_moduleimpl
@@ -80,7 +75,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str:
     else:
         sup_label = "Supprimer évaluation"
 
-    menuEval = [
+    menu_eval = [
         {
             "title": "Saisir notes",
             "endpoint": "notes.saisie_notes",
@@ -159,7 +154,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str:
         },
     ]
 
-    return htmlutils.make_menu("actions", menuEval, alone=True)
+    return htmlutils.make_menu("actions", menu_eval, alone=True)
 
 
 def _ue_coefs_html(coefs_lst) -> str:
@@ -195,14 +190,9 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
     if not isinstance(moduleimpl_id, int):
         raise ScoInvalidIdType("moduleimpl_id must be an integer !")
     modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
-    mi_dict = modimpl.to_dict()
+    module: Module = modimpl.module
     formsemestre_id = modimpl.formsemestre_id
     formsemestre: FormSemestre = modimpl.formsemestre
-    mod_dict = sco_edit_module.module_list(args={"module_id": modimpl.module_id})[0]
-    sem = sco_formsemestre.get_formsemestre(formsemestre_id)
-    formation_dict = sco_formations.formation_list(
-        args={"formation_id": sem["formation_id"]}
-    )[0]
     mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
         moduleimpl_id=moduleimpl_id
     )
@@ -223,10 +213,10 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
         or [0]
     )
     #
-    sem_locked = not sem["etat"]
+    sem_locked = not formsemestre.etat
     can_edit_evals = (
         sco_permissions_check.can_edit_notes(
-            current_user, moduleimpl_id, allow_ens=sem["ens_can_edit_eval"]
+            current_user, moduleimpl_id, allow_ens=formsemestre.ens_can_edit_eval
         )
         and not sem_locked
     )
@@ -237,22 +227,22 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
     arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
     #
     module_resp = User.query.get(modimpl.responsable_id)
-    mod_type_name = scu.MODULE_TYPE_NAMES[mod_dict["module_type"]]
+    mod_type_name = scu.MODULE_TYPE_NAMES[module.module_type]
     H = [
         html_sco_header.sco_header(
-            page_title=f"{mod_type_name} {mod_dict['code']} {mod_dict['titre']}",
+            page_title=f"{mod_type_name} {module.code} {module.titre}",
             javascripts=["js/etud_info.js"],
             init_qtip=True,
         ),
-        f"""<h2 class="formsemestre">{mod_type_name} 
-        <tt>{mod_dict['code']}</tt> {mod_dict['titre']}
-        {"dans l'UE " + modimpl.module.ue.acronyme 
-        if modimpl.module.module_type == scu.ModuleType.MALUS 
+        f"""<h2 class="formsemestre">{mod_type_name}
+        <tt>{module.code}</tt> {module.titre}
+        {"dans l'UE " + modimpl.module.ue.acronyme
+        if modimpl.module.module_type == scu.ModuleType.MALUS
         else ""
         }
         </h2>
         <div class="moduleimpl_tableaubord moduleimpl_type_{
-            scu.ModuleType(mod_dict['module_type']).name.lower()}">
+            scu.ModuleType(module.module_type).name.lower()}">
         <table>
         <tr>
         <td class="fichetitre2">Responsable: </td><td class="redboldtext">
@@ -281,8 +271,8 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
 
     # 2ieme ligne: Semestre, Coef
     H.append("""<tr><td class="fichetitre2">""")
-    if sem["semestre_id"] >= 0:
-        H.append("""Semestre: </td><td>%s""" % sem["semestre_id"])
+    if formsemestre.semestre_id >= 0:
+        H.append("""Semestre: </td><td>%s""" % formsemestre.semestre_id)
     else:
         H.append("""</td><td>""")
     if sem_locked:
@@ -293,34 +283,34 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
     else:
         H.append(
             f"""Coef. dans le semestre: {
-            "non défini" if modimpl.module.coefficient is None else modimpl.module.coefficient  
+            "non défini" if modimpl.module.coefficient is None else modimpl.module.coefficient
         }"""
         )
     H.append("""</td><td></td></tr>""")
     # 3ieme ligne: Formation
     H.append(
-        """<tr><td class="fichetitre2">Formation: </td><td>%(titre)s</td></tr>"""
-        % formation_dict
+        f"""<tr>
+            <td class="fichetitre2">Formation: </td><td>{formsemestre.formation.titre}</td>
+        </tr>
+        """
     )
     # Ligne: Inscrits
     H.append(
-        """<tr><td class="fichetitre2">Inscrits: </td><td> %d étudiants"""
-        % len(mod_inscrits)
+        f"""<tr><td class="fichetitre2">Inscrits: </td><td> {len(mod_inscrits)} étudiants"""
     )
     if current_user.has_permission(Permission.ScoEtudInscrit):
         H.append(
-            """<a class="stdlink" style="margin-left:2em;" href="moduleimpl_inscriptions_edit?moduleimpl_id=%s">modifier</a>"""
-            % mi_dict["moduleimpl_id"]
+            f"""<a class="stdlink" style="margin-left:2em;" href="moduleimpl_inscriptions_edit?moduleimpl_id={modimpl.id}">modifier</a>"""
         )
     H.append("</td></tr>")
     # Ligne: règle de calcul
-    has_expression = sco_compute_moy.moduleimpl_has_expression(mi_dict)
+    has_expression = sco_compute_moy.moduleimpl_has_expression(modimpl)
     if has_expression:
         H.append(
             f"""<tr>
             <td class="fichetitre2" colspan="4">Règle de calcul:
             <span class="formula" title="mode de calcul de la moyenne du module"
-            >moyenne=<tt>{mi_dict["computation_expr"]}</tt>
+            >moyenne=<tt>{modimpl.computation_expr}</tt>
             </span>"""
         )
         H.append("""<span class="warning">inutilisée dans cette version de ScoDoc""")
@@ -335,7 +325,8 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
         H.append("</td></tr>")
     else:
         H.append(
-            '<tr><td colspan="4">'  # <em title="mode de calcul de la moyenne du module">règle de calcul standard</em>'
+            '<tr><td colspan="4">'
+            # <em title="mode de calcul de la moyenne du module">règle de calcul standard</em>'
         )
         # if sco_moduleimpl.can_change_ens(moduleimpl_id, raise_exc=False):
         #     H.append(
@@ -396,7 +387,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
     )
     #
     # Liste les noms de partitions
-    partitions = sco_groups.get_partitions_list(sem["formsemestre_id"])
+    partitions = sco_groups.get_partitions_list(formsemestre.id)
     H.append(
         """Afficher les groupes
         de&nbsp;<select name="partition_id" onchange="document.f.submit();">"""
@@ -417,28 +408,29 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
             f"""<option value="{partition['partition_id']}" {selected}>{name}</option>"""
         )
     H.append(
-        """</select>
+        f"""</select>
 &nbsp;&nbsp;&nbsp;&nbsp;
-<a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%(moduleimpl_id)s">Voir toutes les notes</a>
+<a class="stdlink" href="{
+    url_for("notes.evaluation_listenotes", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
+}">Voir toutes les notes</a>
 </span>
 </form>
 </p>
 """
-        % mi_dict
     )
 
     # -------- Tableau des evaluations
     top_table_links = ""
     if can_edit_evals:
         top_table_links = f"""<a class="stdlink" href="{
-                url_for("notes.evaluation_create", scodoc_dept=g.scodoc_dept, moduleimpl_id=mi_dict['moduleimpl_id'])
+                url_for("notes.evaluation_create", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
                 }">Créer nouvelle évaluation</a>
             """
         if nb_evaluations > 0:
             top_table_links += f"""
             <a class="stdlink" style="margin-left:2em;" href="{
                 url_for("notes.moduleimpl_evaluation_renumber", 
-                scodoc_dept=g.scodoc_dept, moduleimpl_id=mi_dict['moduleimpl_id'],
+                scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id,
                 redirect=1)
             }">Trier par date</a>
             """
diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py
index abdff099e..6ff484ca8 100644
--- a/app/scodoc/sco_utils.py
+++ b/app/scodoc/sco_utils.py
@@ -545,6 +545,11 @@ def strnone(s):
         return ""
 
 
+def strip_str(s):
+    "if s is a string, strip it, if is None, do nothing"
+    return s.strip() if s else s
+
+
 def stripquotes(s):
     "strip s from spaces and quotes"
     s = s.strip()
@@ -1136,6 +1141,36 @@ def objects_renumber(db, obj_list) -> None:
     db.session.commit()
 
 
+def comp_ranks(T: list[tuple]) -> dict[int, str]:
+    """Calcul rangs à partir d'une liste ordonnée de tuples [ (valeur, ..., etudid) ]
+    (valeur est une note numérique), en tenant compte des ex-aequos
+    Le resultat est: { etudid : rang } où rang est une chaine decrivant le rang
+    """
+    rangs = {}  # { etudid : rang } (rang est une chaine)
+    nb_ex = 0  # nb d'ex-aequo consécutifs en cours
+    for i in range(len(T)):
+        # test ex-aequo
+        if i < len(T) - 1:
+            next = T[i + 1][0]
+        else:
+            next = None
+        moy = T[i][0]
+        if nb_ex:
+            srang = "%d ex" % (i + 1 - nb_ex)
+            if moy == next:
+                nb_ex += 1
+            else:
+                nb_ex = 0
+        else:
+            if moy == next:
+                srang = "%d ex" % (i + 1 - nb_ex)
+                nb_ex = 1
+            else:
+                srang = "%d" % (i + 1)
+        rangs[T[i][-1]] = srang  # str(i+1)
+    return rangs
+
+
 def gen_cell(key: str, row: dict, elt="td", with_col_class=False):
     "html table cell"
     klass = row.get(f"_{key}_class", "")
diff --git a/app/views/notes.py b/app/views/notes.py
index 429adf387..545ba9f5b 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -3383,22 +3383,3 @@ def check_formsemestre_integrity(formsemestre_id):
     return (
         html_sco_header.sco_header() + "<br>".join(diag) + html_sco_header.sco_footer()
     )
-
-
-@bp.route("/check_integrity_all")
-@scodoc
-@permission_required(Permission.ScoView)
-@scodoc7func
-def check_integrity_all():
-    "debug: verifie tous les semestres et tt les formations"
-    # formations
-    for F in sco_formations.formation_list():
-        check_form_integrity(F["formation_id"])
-    # semestres
-    for sem in sco_formsemestre.do_formsemestre_list():
-        check_formsemestre_integrity(sem["formsemestre_id"])
-    return (
-        html_sco_header.sco_header()
-        + "<p>empty page: see logs and mails</p>"
-        + html_sco_header.sco_footer()
-    )
diff --git a/tests/unit/sco_fake_gen.py b/tests/unit/sco_fake_gen.py
index c3ec25725..770b6c2a8 100644
--- a/tests/unit/sco_fake_gen.py
+++ b/tests/unit/sco_fake_gen.py
@@ -14,20 +14,16 @@ import sys
 import string
 import typing
 
-
-from config import Config
+from app import db, log
 from app.auth.models import User
-from app.models import FormationModalite, Matiere
+from app.models import Formation, FormationModalite, Matiere
 from app.scodoc import notesdb as ndb
 from app.scodoc import codes_cursus
-from app.scodoc import sco_edit_formation
 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_evaluation_db
-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
@@ -35,8 +31,8 @@ from app.scodoc import sco_moduleimpl
 from app.scodoc import sco_saisie_notes
 from app.scodoc import sco_synchro_etuds
 from app.scodoc import sco_utils as scu
-from app import log
 from app.scodoc.sco_exceptions import ScoValueError
+from config import Config
 
 from tests.unit.setup import NOTES_T
 
@@ -161,11 +157,17 @@ class ScoFake(object):
         """Crée une formation"""
         if not acronyme:
             acronyme = "TEST" + str(random.randint(100000, 999999))
-        oid = sco_edit_formation.do_formation_create(locals())
-        oids = sco_formations.formation_list(formation_id=oid)
-        if not oids:
-            raise ScoValueError("formation not created !")
-        return oid
+        formation = Formation(
+            acronyme=scu.strip_str(acronyme),
+            titre=scu.strip_str(titre),
+            titre_officiel=scu.strip_str(titre_officiel),
+            type_parcours=scu.strip_str(type_parcours),
+            formation_code=scu.strip_str(formation_code),
+            code_specialite=scu.strip_str(code_specialite),
+        )
+        db.session.add(formation)
+        db.session.commit()
+        return formation.id
 
     @logging_meth
     def create_ue(
diff --git a/tests/unit/test_formations.py b/tests/unit/test_formations.py
index b3aaba0bc..b974d99e3 100644
--- a/tests/unit/test_formations.py
+++ b/tests/unit/test_formations.py
@@ -29,7 +29,6 @@
 # - create_module
 # - create_formsemestre
 # - create_moduleimpl
-# - formation_list
 # - formation_export
 # - formsemestre_list
 # - moduleimpl_list
@@ -73,7 +72,7 @@ def test_formations(test_client):
     formation_id = G.create_formation(
         acronyme="F1", titre="Formation 1", titre_officiel="Titre officiel 1"
     )
-    f = sco_formations.formation_list(formation_id)[0]
+    f = Formation.query.get(formation_id).to_dict()
     ue_id = G.create_ue(formation_id=formation_id, acronyme="TST1", titre="ue test")
     matiere_id = G.create_matiere(ue_id=ue_id, titre="matière test")
     module_id = G.create_module(
@@ -102,7 +101,7 @@ def test_formations(test_client):
     )
 
     formation_id2 = G.create_formation(acronyme="", titre="Formation test")
-    formation2 = sco_formations.formation_list(formation_id2)[0]
+    assert Formation.query.get(formation_id2)
     ue3 = G.create_ue(formation_id=formation_id2, acronyme="TST3", titre="ue test3")
     matiere_id4 = G.create_matiere(ue_id=ue3, titre="matière test3")
     module_id3 = G.create_module(
-- 
GitLab