diff --git a/app/formations/edit_matiere.py b/app/formations/edit_matiere.py
index 26add3956c6e5a32ffc16f1dd872335219baf4ad..d758ee1959dc8b884313f09b10cab60f4f38a7bc 100644
--- a/app/formations/edit_matiere.py
+++ b/app/formations/edit_matiere.py
@@ -25,16 +25,14 @@
 #
 ##############################################################################
 
-"""Ajout/Modification/Supression matieres
-(portage from DTML)
+"""Ajout/Modification/Suppression matieres
 """
 import flask
-from flask import g, render_template, request, url_for
+from flask import flash, g, render_template, request, url_for
 
 from app import db, log
-from app.models import Formation, Matiere, UniteEns, ScolarNews
+from app.models import Matiere, UniteEns
 
-import app.scodoc.notesdb as ndb
 import app.scodoc.sco_utils as scu
 
 from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
@@ -44,59 +42,9 @@ from app.scodoc.sco_exceptions import (
     ScoNonEmptyFormationObject,
 )
 
-_matiereEditor = ndb.EditableTable(
-    "notes_matieres",
-    "matiere_id",
-    ("matiere_id", "ue_id", "numero", "titre"),
-    sortkey="numero",
-    output_formators={"numero": ndb.int_null_is_zero},
-)
-
-
-def matiere_list(*args, **kw):
-    "list matieres"
-    cnx = ndb.GetDBConnexion()
-    return _matiereEditor.list(cnx, *args, **kw)
-
-
-def do_matiere_edit(*args, **kw):
-    "edit a matiere"
-    from app.formations import edit_ue
-
-    cnx = ndb.GetDBConnexion()
-    # check
-    mat = matiere_list({"matiere_id": args[0]["matiere_id"]})[0]
-    if matiere_is_locked(mat["matiere_id"]):
-        raise ScoLockedFormError()
-    # edit
-    _matiereEditor.edit(cnx, *args, **kw)
-    formation_id = edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]["formation_id"]
-    db.session.get(Formation, formation_id).invalidate_cached_sems()
-
-
-def do_matiere_create(args):
-    "create a matiere"
-    from app.formations import edit_ue
-
-    cnx = ndb.GetDBConnexion()
-    # check
-    ue = edit_ue.ue_list({"ue_id": args["ue_id"]})[0]
-    # create matiere
-    r = _matiereEditor.create(cnx, args)
-
-    # news
-    formation = db.session.get(Formation, ue["formation_id"])
-    ScolarNews.add(
-        typ=ScolarNews.NEWS_FORM,
-        obj=ue["formation_id"],
-        text=f"Modification de la formation {formation.acronyme}",
-    )
-    formation.invalidate_cached_sems()
-    return r
-
 
 def matiere_create(ue_id=None):
-    """Creation d'une matiere"""
+    """Formulaire création d'une matiere"""
     ue: UniteEns = UniteEns.query.get_or_404(ue_id)
     default_numero = max([mat.numero for mat in ue.matieres] or [9]) + 1
     H = [
@@ -153,8 +101,8 @@ associé.
     if tf[0] == -1:
         return flask.redirect(dest_url)
     # check unicity
-    mats = matiere_list(args={"ue_id": ue_id, "titre": tf[2]["titre"]})
-    if mats:
+    nb_mats = Matiere.query.filter_by(ue_id=ue_id, titre=tf[2]["titre"]).count()
+    if nb_mats:
         return render_template(
             "sco_page.j2",
             title="Création d'une matière",
@@ -164,56 +112,14 @@ associé.
                 + tf[1]
             ),
         )
-    _ = do_matiere_create(tf[2])
+    Matiere.create_from_dict(tf[2])
     return flask.redirect(dest_url)
 
 
-def can_delete_matiere(matiere: Matiere) -> tuple[bool, str]:
-    "True si la matiere n'est pas utilisée dans des formsemestre"
-    locked = matiere_is_locked(matiere.id)
-    if locked:
-        return False
-    if any(m.modimpls.all() for m in matiere.modules):
-        return False
-    return True
-
-
-def do_matiere_delete(oid):
-    "delete matiere and attached modules"
-    from app.formations import edit_module, edit_ue
-
-    cnx = ndb.GetDBConnexion()
-    # check
-    matiere = Matiere.query.get_or_404(oid)
-    mat = matiere_list({"matiere_id": oid})[0]  # compat sco7
-    ue = edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]
-    if not can_delete_matiere(matiere):
-        # il y a au moins un modimpl dans un module de cette matière
-        raise ScoNonEmptyFormationObject("Matière", matiere.titre)
-
-    log(f"do_matiere_delete: matiere_id={matiere.id}")
-    # delete all modules in this matiere
-    mods = edit_module.module_list({"matiere_id": matiere.id})
-    for mod in mods:
-        edit_module.do_module_delete(mod["module_id"])
-    _matiereEditor.delete(cnx, oid)
-
-    # news
-    formation = db.session.get(Formation, ue["formation_id"])
-    ScolarNews.add(
-        typ=ScolarNews.NEWS_FORM,
-        obj=ue["formation_id"],
-        text=f"Modification de la formation {formation.acronyme}",
-    )
-    formation.invalidate_cached_sems()
-
-
 def matiere_delete(matiere_id=None):
-    """Delete matière"""
-    from app.formations import edit_ue
-
-    matiere = Matiere.query.get_or_404(matiere_id)
-    if not can_delete_matiere(matiere):
+    """Form delete matière"""
+    matiere = Matiere.get_instance(matiere_id)
+    if not matiere.can_be_deleted():
         # il y a au moins un modimpl dans un module de cette matière
         raise ScoNonEmptyFormationObject(
             "Matière",
@@ -226,22 +132,20 @@ def matiere_delete(matiere_id=None):
             ),
         )
 
-    mat = matiere_list(args={"matiere_id": matiere_id})[0]
-    ue_dict = edit_ue.ue_list(args={"ue_id": mat["ue_id"]})[0]
     H = [
-        "<h2>Suppression de la matière %(titre)s" % mat,
-        " dans l'UE (%(acronyme)s))</h2>" % ue_dict,
+        f"""<h2>Suppression de la matière {matiere.titre}
+        dans l'UE {matiere.ue.acronyme}</h2>""",
     ]
     dest_url = url_for(
         "notes.ue_table",
         scodoc_dept=g.scodoc_dept,
-        formation_id=str(ue_dict["formation_id"]),
+        formation_id=matiere.ue.formation_id,
     )
     tf = TrivialFormulator(
         request.base_url,
         scu.get_request_args(),
         (("matiere_id", {"input_type": "hidden"}),),
-        initvalues=mat,
+        initvalues=matiere.to_dict(),
         submitlabel="Confirmer la suppression",
         cancelbutton="Annuler",
     )
@@ -254,29 +158,23 @@ def matiere_delete(matiere_id=None):
     if tf[0] == -1:
         return flask.redirect(dest_url)
 
-    do_matiere_delete(matiere_id)
+    matiere.delete()
     return flask.redirect(dest_url)
 
 
 def matiere_edit(matiere_id=None):
-    """Edit matiere"""
-    from app.formations import edit_ue
-
-    F = matiere_list(args={"matiere_id": matiere_id})
-    if not F:
-        raise ScoValueError("Matière inexistante !")
-    F = F[0]
-    ues = edit_ue.ue_list(args={"ue_id": F["ue_id"]})
-    if not ues:
-        raise ScoValueError("UE inexistante !")
-    ue = ues[0]
-    formation: Formation = Formation.query.get_or_404(ue["formation_id"])
-    ues = edit_ue.ue_list(args={"formation_id": ue["formation_id"]})
-    ue_names = ["%(acronyme)s (%(titre)s)" % u for u in ues]
-    ue_ids = [u["ue_id"] for u in ues]
+    """Form edit matiere"""
+    matiere: Matiere = Matiere.get_instance(matiere_id)
+    if matiere.is_locked():
+        raise ScoLockedFormError()
+    ue = matiere.ue
+    formation = ue.formation
+    ues = matiere.ue.formation.ues
+    ue_names = [f"{u.acronyme} ({u.titre or ''})" for u in ues]
+    ue_ids = [u.id for u in ues]
     H = [
-        """<h2>Modification de la matière %(titre)s""" % F,
-        f"""(formation ({formation.acronyme}, version {formation.version})</h2>""",
+        f"""<h2>Modification de la matière {matiere.titre or 'sans titre'}
+        (formation ({formation.acronyme}, version {formation.version})</h2>""",
     ]
     help_msg = """<p class="help">Les matières sont des groupes de modules dans une UE
 d'une formation donnée. Les matières servent surtout pour la
@@ -316,14 +214,14 @@ associé.
                 },
             ),
         ),
-        initvalues=F,
+        initvalues=matiere.to_dict(),
         submitlabel="Modifier les valeurs",
     )
 
     dest_url = url_for(
         "notes.ue_table",
         scodoc_dept=g.scodoc_dept,
-        formation_id=str(ue["formation_id"]),
+        formation_id=formation.id,
     )
     if tf[0] == 0:
         return render_template(
@@ -335,8 +233,8 @@ associé.
         return flask.redirect(dest_url)
     else:
         # check unicity
-        mats = matiere_list(args={"ue_id": tf[2]["ue_id"], "titre": tf[2]["titre"]})
-        if len(mats) > 1 or (len(mats) == 1 and mats[0]["matiere_id"] != matiere_id):
+        mats = Matiere.query.filter_by(ue_id=tf[2]["ue_id"], titre=tf[2]["titre"]).all()
+        if len(mats) > 1 or (len(mats) == 1 and mats[0].id != matiere_id):
             return render_template(
                 "sco_page.j2",
                 title="Modification d'une matière",
@@ -347,32 +245,18 @@ associé.
                 ),
             )
 
+        modif = False
         # changement d'UE ?
-        if tf[2]["ue_id"] != F["ue_id"]:
-            log("attaching mat %s to new UE %s" % (matiere_id, tf[2]["ue_id"]))
-            ndb.SimpleQuery(
-                "UPDATE notes_modules SET ue_id = %(ue_id)s WHERE matiere_id=%(matiere_id)s",
-                {"ue_id": tf[2]["ue_id"], "matiere_id": matiere_id},
-            )
-
-        do_matiere_edit(tf[2])
-
+        if tf[2]["ue_id"] != ue.id:
+            log(f"attaching mat {matiere_id} to new UE id={tf[2]['ue_id']}")
+            new_ue = UniteEns.get_ue(tf[2]["ue_id"])
+            if new_ue.formation_id != formation.id:
+                raise ScoValueError("UE does not belong to the same formation")
+            matiere.ue = new_ue
+            modif = True
+        modif |= matiere.from_dict(tf[2])
+        if modif:
+            db.session.commit()
+            matiere.ue.formation.invalidate_cached_sems()
+            flash("Matière modifiée", "info")
         return flask.redirect(dest_url)
-
-
-def matiere_is_locked(matiere_id):
-    """True if matiere should not be modified
-    (contains modules used in a locked formsemestre)
-    """
-    r = ndb.SimpleDictFetch(
-        """SELECT ma.id
-        FROM notes_matieres ma, notes_modules mod, notes_formsemestre sem, notes_moduleimpl mi
-        WHERE ma.id = mod.matiere_id
-        AND mi.module_id = mod.id
-        AND mi.formsemestre_id = sem.id
-        AND ma.id = %(matiere_id)s
-        AND sem.etat = false
-        """,
-        {"matiere_id": matiere_id},
-    )
-    return len(r) > 0
diff --git a/app/formations/edit_module.py b/app/formations/edit_module.py
index 8cb8dccf67cd71b6558489404f3707fcd5d31e15..71fd3ade7968522b004dcd9e86668d1f0379bc59 100644
--- a/app/formations/edit_module.py
+++ b/app/formations/edit_module.py
@@ -767,6 +767,7 @@ def module_edit(
             module_dict["semestre_id"] = 1
         else:
             module_dict["semestre_id"] = module.ue.semestre_idx
+    tags = module.tags if module else []
     tf = TrivialFormulator(
         request.base_url,
         scu.get_request_args(),
@@ -774,7 +775,9 @@ def module_edit(
         html_foot_markup=(
             f"""<div class="scobox sco_tag_module_edit"><span
         class="sco_tag_edit"><textarea data-module_id="{module_id}" class="module_tag_editor"
-        >{','.join(sco_tag_module.module_tag_list(module_id))}</textarea></span></div>
+        >{
+            ','.join(t.title for t in tags)
+        }</textarea></span></div>
         """
             if not create
             else ""
@@ -833,10 +836,14 @@ def module_edit(
             if matiere:
                 tf[2]["matiere_id"] = matiere.id
             else:
-                matiere_id = edit_matiere.do_matiere_create(
-                    {"ue_id": ue.id, "titre": ue.titre or "", "numero": 1},
+                matiere = Matiere.create_from_dict(
+                    {
+                        "ue_id": ue.id,
+                        "titre": ue.titre or "",
+                        "numero": 1,
+                    }
                 )
-                tf[2]["matiere_id"] = matiere_id
+                tf[2]["matiere_id"] = matiere.id
 
         tf[2]["semestre_id"] = ue.semestre_idx
         module_id = do_module_create(tf[2])
@@ -946,12 +953,6 @@ def module_is_locked(module_id):
     return len(r) > 0
 
 
-def module_count_moduleimpls(module_id):
-    "Number of moduleimpls using this module"
-    mods = sco_moduleimpl.moduleimpl_list(module_id=module_id)
-    return len(mods)
-
-
 def formation_add_malus_modules(
     formation_id: int, semestre_id: int = None, titre=None, redirect=True
 ):
diff --git a/app/formations/edit_ue.py b/app/formations/edit_ue.py
index 3e4d136416c0408edef8bebe9d580fbb3a649dce..dbf2a1dbd925606645db8c3ef2aa20fe574ae155 100644
--- a/app/formations/edit_ue.py
+++ b/app/formations/edit_ue.py
@@ -547,9 +547,10 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
             if is_apc or cursus.UE_IS_MODULE or tf[2]["create_matiere"]:
                 # rappel: en APC, toutes les UE ont une matière, créée ici
                 # (inutilisée mais à laquelle les modules sont rattachés)
-                matiere_id = edit_matiere.do_matiere_create(
-                    {"ue_id": ue_id, "titre": tf[2]["titre"], "numero": 1},
+                matiere = Matiere.create_from_dict(
+                    {"ue_id": ue_id, "titre": tf[2]["titre"], "numero": 1}
                 )
+                matiere_id = matiere.id
             if cursus.UE_IS_MODULE:
                 # dans ce mode, crée un (unique) module dans l'UE:
                 _ = edit_module.do_module_create(
@@ -676,9 +677,10 @@ def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
     return do_ue_delete(ue, delete_validations=delete_validations)
 
 
-def ue_table(formation_id=None, semestre_idx=1, msg=""):  # was ue_list
-    """Liste des matières et modules d'une formation, avec liens pour
-    éditer (si non verrouillée).
+def ue_table(formation_id=None, semestre_idx=1, msg=""):
+    """Page affiochage ou édition d'une formation
+    avec UEs, matières et module,
+    et liens pour éditer si non verrouillée et permission.
     """
     from app.scodoc import sco_formsemestre_validation
 
@@ -1240,7 +1242,7 @@ def _ue_table_ues(
 
 def _ue_table_matieres(
     parcours,
-    ue,
+    ue_dict: dict,
     editable,
     tag_editable,
     arrow_up,
@@ -1250,26 +1252,27 @@ def _ue_table_matieres(
     delete_disabled_icon,
 ):
     """Édition de programme: liste des matières (et leurs modules) d'une UE."""
+    ue = UniteEns.get_ue(ue_dict["ue_id"])
     H = []
     if not parcours.UE_IS_MODULE:
         H.append('<ul class="notes_matiere_list">')
-    matieres = edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]})
+    matieres = ue.matieres.all()
     for mat in matieres:
         if not parcours.UE_IS_MODULE:
             H.append('<li class="notes_matiere_list">')
-            if editable and not edit_matiere.matiere_is_locked(mat["matiere_id"]):
+            if editable and not mat.is_locked():
                 H.append(
                     f"""<a class="stdlink" href="{
                         url_for("notes.matiere_edit",
-                        scodoc_dept=g.scodoc_dept, matiere_id=mat["matiere_id"])
+                        scodoc_dept=g.scodoc_dept, matiere_id=mat.id)
                         }">
                     """
                 )
-            H.append("%(titre)s" % mat)
-            if editable and not edit_matiere.matiere_is_locked(mat["matiere_id"]):
+            H.append(f"{mat.titre or 'sans titre'}")
+            if editable and not mat.is_locked():
                 H.append("</a>")
 
-        modules = edit_module.module_list(args={"matiere_id": mat["matiere_id"]})
+        modules = mat.modules.all()
         H.append(
             _ue_table_modules(
                 parcours,
@@ -1291,14 +1294,17 @@ def _ue_table_matieres(
         H.append("<li>Aucune matière dans cette UE ! ")
         if editable:
             H.append(
-                """<a class="stdlink" href="ue_delete?ue_id=%(ue_id)s">supprimer l'UE</a>"""
-                % ue
+                f"""<a class="stdlink" href="{
+                    url_for('notes.ue_delete', scodoc_dept=g.scodoc_dept, ue_id=ue.id)
+                }">supprimer l'UE</a>"""
             )
         H.append("</li>")
     if editable and not parcours.UE_IS_MODULE:
         H.append(
-            '<li><a class="stdlink" href="matiere_create?ue_id=%(ue_id)s">créer une matière</a> </li>'
-            % ue
+            f"""<li><a class="stdlink" href="{
+                    url_for("notes.matiere_create", scodoc_dept=g.scodoc_dept, ue_id=ue.id)
+                    }">créer une matière</a>
+                </li>"""
         )
     if not parcours.UE_IS_MODULE:
         H.append("</ul>")
@@ -1307,9 +1313,9 @@ def _ue_table_matieres(
 
 def _ue_table_modules(
     parcours,
-    ue,
-    mat,
-    modules,
+    ue: UniteEns,
+    mat: Matiere,
+    modules: list[Module],
     editable,
     tag_editable,
     arrow_up,
@@ -1326,89 +1332,84 @@ def _ue_table_modules(
     H = ['<ul class="notes_module_list">']
     im = 0
     for mod in modules:
-        mod["nb_moduleimpls"] = edit_module.module_count_moduleimpls(mod["module_id"])
+        nb_moduleimpls = mod.modimpls.count()
         klass = "notes_module_list"
-        if mod["module_type"] == ModuleType.MALUS:
+        if mod.module_type == ModuleType.MALUS:
             klass += " module_malus"
-        H.append('<li class="%s">' % klass)
+        H.append(f'<li class="{klass}">')
 
         H.append('<span class="notes_module_list_buts">')
         if im != 0 and editable:
             H.append(
-                '<a href="module_move?module_id=%s&after=0" class="aud">%s</a>'
-                % (mod["module_id"], arrow_up)
+                f"""<a href="module_move?module_id={mod.id}&after=0" class="aud">{arrow_up}</a>"""
             )
         else:
             H.append(arrow_none)
         if im < len(modules) - 1 and editable:
             H.append(
-                '<a href="module_move?module_id=%s&after=1" class="aud">%s</a>'
-                % (mod["module_id"], arrow_down)
+                f"""<a href="module_move?module_id={mod.id}&after=1" class="aud">{arrow_down}</a>"""
             )
         else:
             H.append(arrow_none)
         im += 1
-        if mod["nb_moduleimpls"] == 0 and editable:
-            icon = delete_icon
-        else:
-            icon = delete_disabled_icon
+        icon = delete_icon if nb_moduleimpls == 0 and editable else delete_disabled_icon
         H.append(
-            '<a class="smallbutton" href="module_delete?module_id=%s">%s</a>'
-            % (mod["module_id"], icon)
+            f"""<a class="smallbutton" href="{
+                url_for("notes.module_delete", scodoc_dept=g.scodoc_dept, module_id=mod.id)
+            }">{icon}</a>"""
         )
-
         H.append("</span>")
 
-        mod_editable = (
-            editable  # and not edit_module.module_is_locked( Mod['module_id'])
-        )
+        mod_editable = editable
+        # and not edit_module.module_is_locked(Mod['module_id'])
         if mod_editable:
             H.append(
-                '<a class="discretelink" title="Modifier le module numéro %(numero)s, utilisé par %(nb_moduleimpls)d sessions" href="module_edit?module_id=%(module_id)s">'
-                % mod
+                f"""<a class="discretelink" title="Modifier le module numéro %(numero)s, utilisé par
+                {nb_moduleimpls} sessions" href="{
+                    url_for("notes.module_edit", scodoc_dept=g.scodoc_dept, module_id=mod.id)
+                }">"""
             )
-        if mod["module_type"] not in (scu.ModuleType.STANDARD, scu.ModuleType.MALUS):
+        if mod.module_type not in (scu.ModuleType.STANDARD, scu.ModuleType.MALUS):
             H.append(
                 f"""<span class="invalid-module-type">{scu.EMO_WARNING} type incompatible </span>"""
             )
         H.append(
-            '<span class="formation_module_tit">%s</span>'
-            % scu.join_words(mod["code"], mod["titre"])
+            f"""<span class="formation_module_tit">{scu.join_words(mod.code, mod.titre)}</span>"""
         )
         if mod_editable:
             H.append("</a>")
-        heurescoef = (
-            "%(heures_cours)s/%(heures_td)s/%(heures_tp)s, coef. %(coefficient)s" % mod
+        heures = (
+            f"""{mod.heures_cours or 0}/{mod.heures_td or 0}/{mod.heures_tp or 0}, """
+            if (mod.heures_cours or mod.heures_td or mod.heures_tp)
+            else ""
         )
+        heurescoef = f"""{heures}coef. {mod.coefficient}"""
         edit_url = url_for(
             "apiweb.formation_module_set_code_apogee",
             scodoc_dept=g.scodoc_dept,
-            module_id=mod["module_id"],
+            module_id=mod.id,
         )
         heurescoef += f""", Apo: <span
             class="{'span_apo_edit' if editable else ''}"
-            data-url="{edit_url}" id="{mod["module_id"]}"
+            data-url="{edit_url}" id="{mod.id}"
             data-placeholder="{scu.APO_MISSING_CODE_STR}">{
-                mod["code_apogee"] or ""
+                mod.code_apogee or ""
             }</span>"""
-        if tag_editable:
-            tag_cls = "module_tag_editor"
-        else:
-            tag_cls = "module_tag_editor_ro"
-        tag_mk = """<span class="sco_tag_edit"><form><textarea data-module_id="{}" class="{}">{}</textarea></form></span>"""
-        tag_edit = tag_mk.format(
-            mod["module_id"],
-            tag_cls,
-            ",".join(sco_tag_module.module_tag_list(mod["module_id"])),
-        )
-        if ue["semestre_idx"] is not None and mod["semestre_id"] != ue["semestre_idx"]:
+        tag_cls = "module_tag_editor" if tag_editable else "module_tag_editor_ro"
+        tag_edit = f"""<span class="sco_tag_edit">
+            <form>
+                <textarea data-module_id="{mod.id}" class="{tag_cls}">{
+                    ",".join([ tag.title for tag in mod.tags ])
+                    }</textarea>
+            </form>
+            </span>"""
+        if ue.semestre_idx is not None and mod.semestre_id != ue.semestre_idx:
             warning_semestre = ' <span class="red">incohérent ?</span>'
         else:
             warning_semestre = ""
         H.append(
-            " %s %s%s" % (parcours.SESSION_NAME, mod["semestre_id"], warning_semestre)
-            + " (%s)" % heurescoef
-            + tag_edit
+            f""" {parcours.SESSION_NAME} {mod.semestre_id}{warning_semestre}
+            {heurescoef}{tag_edit}"""
         )
         H.append("</li>")
     if not modules:
@@ -1417,7 +1418,7 @@ def _ue_table_modules(
             H.append(
                 f"""<a class="stdlink" href="{
                     url_for("notes.matiere_delete",
-                    scodoc_dept=g.scodoc_dept, matiere_id=mat["matiere_id"])}"
+                    scodoc_dept=g.scodoc_dept, matiere_id=mat.id)}"
                 >la supprimer</a>
                 """
             )
@@ -1426,7 +1427,7 @@ def _ue_table_modules(
         H.append(
             f"""<li> <a class="stdlink" href="{
                     url_for("notes.module_create",
-                    scodoc_dept=g.scodoc_dept, matiere_id=mat["matiere_id"])}"
+                    scodoc_dept=g.scodoc_dept, matiere_id=mat.id)}"
             >{create_element_msg}</a></li>
             """
         )
diff --git a/app/formations/formation_io.py b/app/formations/formation_io.py
index bd21357262c0efaafbb7b68435d639ee662bd8a4..bbf265cc5300e70b6570c3e0285ab81adfd5d1a3 100644
--- a/app/formations/formation_io.py
+++ b/app/formations/formation_io.py
@@ -36,8 +36,8 @@ from flask_login import current_user
 import app.scodoc.sco_utils as scu
 from app import db
 from app import log
-from app.formations import edit_matiere, edit_module, edit_ue
-from app.models import Formation, FormSemestre, Module, UniteEns
+from app.formations import edit_module, edit_ue
+from app.models import Formation, FormSemestre, Matiere, Module, UniteEns
 from app.models import ScolarNews
 from app.models.but_refcomp import (
     ApcAppCritique,
@@ -113,35 +113,35 @@ def formation_export_dict(
             ue_dict.pop("code_apogee_rcue", None)
         if ue_dict.get("ects") is None:
             ue_dict.pop("ects", None)
-        mats = edit_matiere.matiere_list({"ue_id": ue.id})
-        mats.sort(key=lambda m: m["numero"] or 0)
-        ue_dict["matiere"] = mats
-        for mat in mats:
-            matiere_id = mat["matiere_id"]
+        mats = ue.matieres.all()
+        mats.sort(key=lambda m: m.numero)
+        mats_dict = [mat.to_dict() for mat in mats]
+        ue_dict["matiere"] = mats_dict
+        for mat_d in mats_dict:
+            matiere_id = mat_d["matiere_id"]
             if not export_ids:
-                del mat["id"]
-                del mat["matiere_id"]
-                del mat["ue_id"]
+                del mat_d["id"]
+                del mat_d["matiere_id"]
+                del mat_d["ue_id"]
             mods = edit_module.module_list({"matiere_id": matiere_id})
             mods.sort(key=lambda m: (m["numero"] or 0, m["code"]))
-            mat["module"] = mods
-            for mod in mods:
-                module_id = mod["module_id"]
+            mat_d["module"] = mods
+            for mod_d in mods:
+                module: Module = db.session.get(Module, mod_d["module_id"])
                 if export_tags:
-                    tags = sco_tag_module.module_tag_list(module_id=mod["module_id"])
+                    tags = [t.title for t in module.tags]
                     if tags:
-                        mod["tags"] = [{"name": x} for x in tags]
+                        mod_d["tags"] = [{"name": x} for x in tags]
                 #
-                module: Module = db.session.get(Module, module_id)
                 if module.is_apc():
                     # Exporte les coefficients
                     if ue_reference_style == "id":
-                        mod["coefficients"] = [
+                        mod_d["coefficients"] = [
                             {"ue_reference": str(ue_id), "coef": str(coef)}
                             for (ue_id, coef) in module.get_ue_coef_dict().items()
                         ]
                     else:
-                        mod["coefficients"] = [
+                        mod_d["coefficients"] = [
                             {"ue_reference": ue_acronyme, "coef": str(coef)}
                             for (
                                 ue_acronyme,
@@ -149,29 +149,29 @@ def formation_export_dict(
                             ) in module.get_ue_coef_dict_acronyme().items()
                         ]
                     # Et les parcours
-                    mod["parcours"] = [
+                    mod_d["parcours"] = [
                         p.to_dict(with_annees=False) for p in module.parcours
                     ]
                     # Et les AC
                     if ac_as_list:
                         # XML préfère une liste
-                        mod["app_critiques"] = [
+                        mod_d["app_critiques"] = [
                             x.to_dict(with_code=True) for x in module.app_critiques
                         ]
                     else:
-                        mod["app_critiques"] = {
+                        mod_d["app_critiques"] = {
                             x.code: x.to_dict() for x in module.app_critiques
                         }
                 if not export_ids:
-                    del mod["id"]
-                    del mod["ue_id"]
-                    del mod["matiere_id"]
-                    del mod["module_id"]
-                    del mod["formation_id"]
+                    del mod_d["id"]
+                    del mod_d["ue_id"]
+                    del mod_d["matiere_id"]
+                    del mod_d["module_id"]
+                    del mod_d["formation_id"]
                 if not export_codes_apo:
-                    del mod["code_apogee"]
-                if mod["ects"] is None:
-                    del mod["ects"]
+                    del mod_d["code_apogee"]
+                if mod_d["ects"] is None:
+                    del mod_d["ects"]
     return f_dict
 
 
@@ -399,7 +399,8 @@ def formation_import_xml(doc: str | bytes, import_tags=True, use_local_refcomp=F
 
                 assert mat_info[0] == "matiere"
                 mat_info[1]["ue_id"] = ue_id
-                mat_id = edit_matiere.do_matiere_create(mat_info[1])
+                mat = Matiere.create_from_dict(mat_info[1])
+                mat_id = mat.id
                 # -- create modules
                 for mod_info in mat_info[2]:
                     assert mod_info[0] == "module"
@@ -460,7 +461,7 @@ def formation_import_xml(doc: str | bytes, import_tags=True, use_local_refcomp=F
                                             f"Warning: parcours {code_parcours} inexistant !"
                                         )
                         if import_tags and tag_names:
-                            sco_tag_module.module_tag_set(mod_id, tag_names)
+                            module.set_tags(tag_names)
                         if module.is_apc() and ue_coef_dict:
                             modules_a_coefficienter.append((module, ue_coef_dict))
         # Fixe les coefs APC (à la fin pour que les UE soient créées)
diff --git a/app/models/formations.py b/app/models/formations.py
index 1384873f1cc25bf5380fc38d18c36c166bee436a..5ae12d6f27dd4d1af53453259d624d763d8b542c 100644
--- a/app/models/formations.py
+++ b/app/models/formations.py
@@ -5,7 +5,7 @@ from flask import abort, g
 from flask_sqlalchemy.query import Query
 
 import app
-from app import db
+from app import db, log
 from app.comp import df_cache
 from app.models import ScoDocModel, SHORT_STR_LEN
 from app.models.but_refcomp import (
@@ -14,6 +14,7 @@ from app.models.but_refcomp import (
     ApcParcours,
     ApcParcoursNiveauCompetence,
 )
+from app.models.events import ScolarNews
 from app.models.modules import Module
 from app.models.moduleimpls import ModuleImpl
 from app.models.ues import UniteEns, UEParcours
@@ -21,6 +22,7 @@ from app.scodoc import sco_cache
 from app.scodoc import codes_cursus
 from app.scodoc import sco_utils as scu
 from app.scodoc.codes_cursus import UE_STANDARD
+from app.scodoc.sco_exceptions import ScoNonEmptyFormationObject, ScoValueError
 
 
 class Formation(ScoDocModel):
@@ -166,6 +168,7 @@ class Formation(ScoDocModel):
         sco_cache.invalidate_formsemestre()
 
     def invalidate_cached_sems(self):
+        "Invalide caches de tous les formssemestres de la formation"
         for sem in self.formsemestres:
             sco_cache.invalidate_formsemestre(formsemestre_id=sem.id)
 
@@ -312,7 +315,9 @@ class Matiere(ScoDocModel):
     titre = db.Column(db.Text())
     numero = db.Column(db.Integer, nullable=False, default=0)  # ordre de présentation
 
-    modules = db.relationship("Module", lazy="dynamic", backref="matiere")
+    modules = db.relationship(
+        "Module", lazy="dynamic", backref="matiere", cascade="all, delete-orphan"
+    )
     _sco_dept_relations = ("UniteEns", "Formation")  # accès au dept_id
 
     def __repr__(self):
@@ -325,5 +330,75 @@ class Matiere(ScoDocModel):
         e.pop("_sa_instance_state", None)
         # ScoDoc7 output_formators
         e["numero"] = e["numero"] if e["numero"] else 0
-        e["ue_id"] = self.id
+        e["matiere_id"] = self.id
         return e
+
+    def is_locked(self) -> bool:
+        """True if matiere cannot be be modified
+        because it contains modules used in a locked formsemestre.
+        """
+        from app.models.formsemestre import FormSemestre
+
+        mat = (
+            db.session.query(Matiere)
+            .filter_by(id=self.id)
+            .join(Module)
+            .join(ModuleImpl)
+            .join(FormSemestre)
+            .filter_by(etat=False)
+            .all()
+        )
+        return bool(mat)
+
+    def can_be_deleted(self) -> bool:
+        "True si la matiere n'est pas utilisée dans des formsemestres"
+        locked = self.is_locked()
+        if locked:
+            return False
+        if any(m.modimpls.all() for m in self.modules):
+            return False
+        return True
+
+    def delete(self):
+        "Delete matière. News, inval cache."
+        from app.models import ScolarNews
+
+        formation = self.ue.formation
+        log(f"matiere.delete: matiere_id={self.id}")
+        if not self.can_be_deleted():
+            # il y a au moins un modimpl dans un module de cette matière
+            raise ScoNonEmptyFormationObject("Matière", self.titre)
+        db.session.delete(self)
+        db.session.commit()
+        # news
+        ScolarNews.add(
+            typ=ScolarNews.NEWS_FORM,
+            obj=formation.id,
+            text=f"Modification de la formation {formation.acronyme}",
+        )
+        # cache
+        formation.invalidate_cached_sems()
+
+    @classmethod
+    def create_from_dict(cls, data: dict) -> "Matiere":
+        """Create matière from dict. Log, news, cache.
+        data must include ue_id, a valid UE id.
+        Commit session.
+        """
+        # check ue
+        if data.get("ue_id") is None:
+            raise ScoValueError("UE id missing")
+        _ = UniteEns.get_ue(data["ue_id"])
+
+        mat = super().create_from_dict(data)
+        db.session.commit()
+        db.session.refresh(mat)
+        # news
+        formation = mat.ue.formation
+        ScolarNews.add(
+            typ=ScolarNews.NEWS_FORM,
+            obj=formation.id,
+            text=f"Modification de la formation {formation.acronyme}",
+        )
+        formation.invalidate_cached_sems()
+        return mat
diff --git a/app/models/modules.py b/app/models/modules.py
index 8e1d8c5b7205fb002777ce577941e43176e65027..50ff5633ecc0ae7a168fe77d84ca6f7d49fa5145 100644
--- a/app/models/modules.py
+++ b/app/models/modules.py
@@ -1,9 +1,10 @@
 """ScoDoc 9 models : Modules
 """
 
+import http
 from flask import current_app, g
 
-from app import db
+from app import db, log
 from app import models
 from app.models import APO_CODE_STR_LEN
 from app.models.but_refcomp import (
@@ -75,11 +76,11 @@ class Module(models.ScoDocModel):
         backref=db.backref("modules", lazy=True),
     )
 
-    _sco_dept_relations = "Formation"  # accès au dept_id
+    _sco_dept_relations = ("Formation",)  # accès au dept_id
 
     def __init__(self, **kwargs):
         self.ue_coefs = []
-        super(Module, self).__init__(**kwargs)
+        super().__init__(**kwargs)
 
     def __repr__(self):
         return f"""<Module{ModuleType(self.module_type or ModuleType.STANDARD).name
@@ -449,6 +450,43 @@ class Module(models.ScoDocModel):
         db.session.add(self)
         db.session.flush()
 
+    def set_tags(self, taglist: str | list[str] | None = None):
+        """taglist may either be:
+        a string with tag names separated by commas ("un,deux")
+        or a list of strings (["un", "deux"])
+        Remplace les tags existants
+        """
+        # TODO refactoring ScoTag
+        # TODO code à moderniser (+ revoir classe ScoTag, utiliser modèle)
+        # TODO Voir ItemSuiviTag et api etud_suivi
+        from app.scodoc.sco_tag_module import ScoTag, ModuleTag
+
+        taglist = taglist or []
+        if isinstance(taglist, str):
+            taglist = taglist.split(",")
+        taglist = [t.strip() for t in taglist]
+        taglist = [t for t in taglist if t]
+        log(f"module.set_tags: module_id={self.id} taglist={taglist}")
+        # Check tags syntax
+        for tag in taglist:
+            if not ScoTag.check_tag_title(tag):
+                log(f"module.set_tags({self.id}): invalid tag title")
+                return scu.json_error(404, "invalid tag")
+
+        newtags = set(taglist)
+        oldtags = set(t.title for t in self.tags)
+        to_del = oldtags - newtags
+        to_add = newtags - oldtags
+
+        # should be atomic, but it's not.
+        for tagname in to_add:
+            t = ModuleTag(tagname, object_id=self.id)
+        for tagname in to_del:
+            t = ModuleTag(tagname)
+            t.remove_tag_from_object(self.id)
+
+        return "", http.HTTPStatus.NO_CONTENT
+
 
 class ModuleUECoef(db.Model):
     """Coefficients des modules vers les UE (APC, BUT)
@@ -499,7 +537,7 @@ class ModuleUECoef(db.Model):
         return d
 
 
-class NotesTag(db.Model):
+class NotesTag(models.ScoDocModel):
     """Tag sur un module"""
 
     __tablename__ = "notes_tags"
@@ -511,6 +549,9 @@ class NotesTag(db.Model):
     dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
     title = db.Column(db.Text(), nullable=False)
 
+    def __repr__(self):
+        return f"<Tag {self.id} {self.title!r}>"
+
     @classmethod
     def get_or_create(cls, title: str, dept_id: int | None = None) -> "NotesTag":
         """Get tag, or create it if it doesn't yet exists.
diff --git a/app/pe/moys/pe_ressemtag.py b/app/pe/moys/pe_ressemtag.py
index cff92b94e98d39a5466debfb7b5f7c2fa9576a8d..e424e8f3e5464fcabf848f635aaccea44ecb3393 100644
--- a/app/pe/moys/pe_ressemtag.py
+++ b/app/pe/moys/pe_ressemtag.py
@@ -274,7 +274,8 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
         pole=None,
     ) -> pd.DataFrame:
         """Calcule la moyenne par UE des étudiants pour un tag donné,
-        en ayant connaissance des informations sur le tag et des inscriptions des étudiants aux différentes UEs.
+        en ayant connaissance des informations sur le tag et des inscriptions
+        des étudiants aux différentes UEs.
 
         info_tag détermine les modules pris en compte :
         * si non `None`, seuls les modules rattachés au tag sont pris en compte
@@ -342,7 +343,8 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
         colonnes = [ue.id for ue in self.ues_standards]
         moyennes_ues_tag = moyennes_ues_tag[colonnes]
 
-        # Applique le masque d'inscription aux UE pour ne conserver que les UE dans lequel l'étudiant est inscrit
+        # Applique le masque d'inscription aux UE pour ne conserver
+        # que les UE dans lequel l'étudiant est inscrit
         moyennes_ues_tag = moyennes_ues_tag[colonnes] * ues_inscr_parcours_df[colonnes]
 
         # Transforme les UEs en acronyme
@@ -405,7 +407,7 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
         """Vérifie l'unicité des tags"""
         noms_tags_perso = sorted(list(set(dict_tags["personnalises"].keys())))
         noms_tags_auto = sorted(list(set(dict_tags["auto"].keys())))  # + noms_tags_comp
-        noms_tags = noms_tags_perso + noms_tags_auto
+        # noms_tags = noms_tags_perso + noms_tags_auto
 
         intersection = list(set(noms_tags_perso) & set(noms_tags_auto))
 
@@ -455,7 +457,7 @@ def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre):
         modimpl_id = modimpl.id
 
         # Liste des tags pour le module concerné
-        tags = sco_tag_module.module_tag_list(modimpl.module.id)
+        tags = [t.title for t in modimpl.module.tags]
 
         # Traitement des tags recensés, chacun pouvant étant de la forme
         # "mathématiques", "théorie", "pe:0", "maths:2"
diff --git a/app/scodoc/sco_tag_module.py b/app/scodoc/sco_tag_module.py
index 5dfc26a5554824b63e36986106cd91d2fe30d2f2..0d5bdff620e9dc36a3b5ee47d050f10ff6222736 100644
--- a/app/scodoc/sco_tag_module.py
+++ b/app/scodoc/sco_tag_module.py
@@ -30,16 +30,15 @@
    Implementation expérimentale (Jul. 2016) pour grouper les modules sur
    les avis de poursuites d'études.
 
+   TODO: réécrire avec SQLAlchemy.
 
    Pour l'UI, voir https://goodies.pixabay.com/jquery/tag-editor/demo.html
 """
-import http
 import re
 
 from flask import g
 
-from app import db, log
-from app.formations import edit_module
+from app import db
 from app.models import Formation, NotesTag
 import app.scodoc.sco_utils as scu
 import app.scodoc.notesdb as ndb
@@ -60,7 +59,7 @@ from app.scodoc.sco_exceptions import ScoValueError
 
 
 # NOTA: ancien code, n'utile pas de modèles SQLAlchemy
-class ScoTag(object):
+class ScoTag:
     """Generic tags for ScoDoc"""
 
     # must be overloaded:
@@ -208,8 +207,6 @@ class ModuleTag(ScoTag):
 
 
 # API
-
-
 # TODO placer dans la vraie API et ne plus utiliser sco_publish
 def module_tag_search(term: str | int):
     """List all used tag names (for auto-completion)"""
@@ -230,60 +227,6 @@ def module_tag_search(term: str | int):
     return scu.sendJSON(data)
 
 
-def module_tag_list(module_id="") -> list[str]:
-    """les noms de tags associés à ce module"""
-    r = ndb.SimpleDictFetch(
-        """SELECT t.title
-          FROM notes_modules_tags mt, notes_tags t
-          WHERE mt.tag_id = t.id
-          AND mt.module_id = %(module_id)s
-          """,
-        {"module_id": module_id},
-    )
-    return [x["title"] for x in r]
-
-
-def module_tag_set(module_id="", taglist=None):
-    """taglist may either be:
-    a string with tag names separated by commas ("un,deux")
-    or a list of strings (["un", "deux"])
-    Remplace les tags existants
-    """
-    if not taglist:
-        taglist = []
-    elif isinstance(taglist, str):
-        taglist = taglist.split(",")
-    taglist = [t.strip() for t in taglist]
-    log(f"module_tag_set: module_id={module_id} taglist={taglist}")
-    # Check tags syntax
-    for tag in taglist:
-        if not ScoTag.check_tag_title(tag):
-            log(f"module_tag_set({module_id}): invalid tag title")
-            return scu.json_error(404, "invalid tag")
-
-    # TODO code à moderniser (+ revoir classe ScoTag, utiliser modèle)
-    # TODO Voir ItemSuiviTag et api etud_suivi
-
-    # Sanity check:
-    mod_dict = edit_module.module_list(args={"module_id": module_id})
-    if not mod_dict:
-        raise ScoValueError("invalid module !")
-
-    newtags = set(taglist)
-    oldtags = set(module_tag_list(module_id))
-    to_del = oldtags - newtags
-    to_add = newtags - oldtags
-
-    # should be atomic, but it's not.
-    for tagname in to_add:
-        t = ModuleTag(tagname, object_id=module_id)
-    for tagname in to_del:
-        t = ModuleTag(tagname)
-        t.remove_tag_from_object(module_id)
-
-    return "", http.HTTPStatus.NO_CONTENT
-
-
 def split_tagname_coeff(tag: str, separateur=":") -> tuple[str, float]:
     """Découpage d'un tag, tel que saisi par un utilisateur dans le programme,
     pour en extraire :
diff --git a/app/scodoc/sco_ue_external.py b/app/scodoc/sco_ue_external.py
index b795002936da6f1fc35be7e158a267cf3140c89c..97f97259c9a0d51ab2b822bb8a7c2704aa7390d4 100644
--- a/app/scodoc/sco_ue_external.py
+++ b/app/scodoc/sco_ue_external.py
@@ -56,12 +56,12 @@ Solution proposée (nov 2014):
 import flask
 from flask import flash, g, request, render_template, url_for
 from flask_login import current_user
-from app.formations import edit_matiere, edit_module, edit_ue
+from app.formations import edit_module, edit_ue
 from app.models.formsemestre import FormSemestre
 
 
 from app import db, log
-from app.models import Evaluation, Identite, ModuleImpl, UniteEns
+from app.models import Evaluation, Identite, Matiere, ModuleImpl, UniteEns
 from app.scodoc import codes_cursus
 from app.scodoc import sco_moduleimpl
 from app.scodoc import sco_saisie_notes
@@ -110,7 +110,7 @@ def external_ue_create(
     )
     ue = db.session.get(UniteEns, ue_id)
     flash(f"UE créée (code {ue.ue_code})")
-    matiere_id = edit_matiere.do_matiere_create(
+    matiere = Matiere.create_from_dict(
         {"ue_id": ue_id, "titre": titre or acronyme, "numero": 1}
     )
 
@@ -120,7 +120,7 @@ def external_ue_create(
             "code": acronyme,
             "coefficient": ects,  # tous le coef. module est egal à la quantite d'ECTS
             "ue_id": ue_id,
-            "matiere_id": matiere_id,
+            "matiere_id": matiere.id,
             "formation_id": formation_id,
             "semestre_id": formsemestre.semestre_id,
             "module_type": scu.ModuleType.STANDARD,
diff --git a/app/views/notes.py b/app/views/notes.py
index 4aa70730972b43c521448c6e4f38dbd2593c9b79..855eceedd10b53dc26c73d70274df80568b134b3 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -612,9 +612,7 @@ def formation_tag_modules_by_type(formation_id: int, semestre_idx: int):
     """Taggue tous les modules de la formation en fonction de leur type : 'res', 'sae', 'malus'
     Ne taggue pas les modules standards.
     """
-    formation = Formation.query.filter_by(
-        id=formation_id, dept_id=g.scodoc_dept_id
-    ).first_or_404()
+    formation = Formation.get_formation(formation_id)
     sco_tag_module.formation_tag_modules_by_type(formation)
     flash("Formation tagguée")
     return flask.redirect(
@@ -631,11 +629,12 @@ def formation_tag_modules_by_type(formation_id: int, semestre_idx: int):
 @bp.route("/module_tag_set", methods=["POST"])
 @scodoc
 @permission_required(Permission.EditFormationTags)
-def module_tag_set():
+def module_tag_set():  # TODO passer dans l'API
     """Set tags on module"""
-    module_id = int(request.form.get("module_id"))
+    module_id = request.form.get("module_id")
+    module: Module = Module.get_instance(module_id)
     taglist = request.form.get("taglist")
-    return sco_tag_module.module_tag_set(module_id, taglist)
+    return module.set_tags(taglist)
 
 
 @bp.route("/module_clone", methods=["POST"])
@@ -643,8 +642,8 @@ def module_tag_set():
 @permission_required(Permission.EditFormation)
 def module_clone():
     """Clone existing module"""
-    module_id = int(request.form.get("module_id"))
-    module = Module.query.get_or_404(module_id)
+    module_id = request.form.get("module_id")
+    module: Module = Module.get_instance(module_id)
     module2 = module.clone()
     db.session.add(module2)
     db.session.commit()
diff --git a/tests/unit/sco_fake_gen.py b/tests/unit/sco_fake_gen.py
index 61bbb8f919069f435e5d566842229e6f41f3b223..ef3976826808cc89f93f03c8baeb0eeb0031317c 100644
--- a/tests/unit/sco_fake_gen.py
+++ b/tests/unit/sco_fake_gen.py
@@ -205,11 +205,8 @@ class ScoFake(object):
 
     @logging_meth
     def create_matiere(self, ue_id=None, titre=None, numero=0) -> int:
-        oid = edit_matiere.do_matiere_create(locals())
-        oids = edit_matiere.matiere_list(args={"matiere_id": oid})
-        if not oids:
-            raise ScoValueError("matiere not created !")
-        return oid
+        mat = Matiere.create_from_dict(locals())
+        return mat.id
 
     @logging_meth
     def create_module(
diff --git a/tests/unit/test_formations.py b/tests/unit/test_formations.py
index 66eea7867bb14966aab074d1a0040392bd81fc5c..e234e1ff5053ef855e23fe2880444b7a7f0b5990 100644
--- a/tests/unit/test_formations.py
+++ b/tests/unit/test_formations.py
@@ -36,8 +36,6 @@
 # - do_formsemestre_delete
 # - module_list
 # - do_module_delete
-# - matiere_list
-# - do_matiere_delete
 # - ue_list
 # - do_ue_delete
 # - do_formation_delete
@@ -50,12 +48,11 @@ import pytest
 from app import db
 from app.formations import (
     edit_formation,
-    edit_matiere,
     edit_module,
     edit_ue,
     formation_io,
 )
-from app.models import Formation, ModuleImpl
+from app.models import Formation, Matiere, ModuleImpl
 from app.scodoc import sco_formsemestre
 from app.scodoc import sco_exceptions
 from app.scodoc import sco_formsemestre_edit
@@ -287,10 +284,12 @@ def test_formations(test_client):
 
     assert len(lim_sem2) == 0  # deuxieme vérification si le module s'est bien sup
 
-    li_mat = edit_matiere.matiere_list()
+    li_mat = Matiere.query.all()
     assert len(li_mat) == 4
-    edit_matiere.do_matiere_delete(oid=matiere_id3)  # on supprime la matiere
-    li_mat2 = edit_matiere.matiere_list()
+    assert matiere_id3 in [m.matiere_id for m in li_mat]
+    matiere = db.session.get(Matiere, matiere_id3)
+    matiere.delete()  # on supprime la matiere
+    li_mat2 = Matiere.query.all()
     assert len(li_mat2) == 3  # verification de la suppression de la matiere
 
     li_ue = edit_ue.ue_list()