diff --git a/app/formations/edit_ue.py b/app/formations/edit_ue.py
index 1b9e3e334997076efdd51cf7010747438a79d265..483d9e1fa96d9ca2190b91c9681cf058548ed0f8 100644
--- a/app/formations/edit_ue.py
+++ b/app/formations/edit_ue.py
@@ -49,7 +49,6 @@ from app.models import (
 )
 from app.models import ApcValidationRCUE, ScolarFormSemestreValidation, ScolarEvent
 from app.models import ScolarNews
-import app.scodoc.notesdb as ndb
 import app.scodoc.sco_utils as scu
 from app.scodoc.sco_utils import ModuleType
 from app.scodoc.TrivialFormulator import TrivialFormulator
@@ -65,54 +64,13 @@ from app.scodoc import sco_edit_apc
 from app.scodoc import sco_groups
 from app.scodoc import sco_moduleimpl
 
-_ueEditor = ndb.EditableTable(
-    "notes_ue",
-    "ue_id",
-    (
-        "ue_id",
-        "formation_id",
-        "acronyme",
-        "numero",
-        "titre",
-        "semestre_idx",
-        "type",
-        "ue_code",
-        "ects",
-        "is_external",
-        "code_apogee",
-        "code_apogee_rcue",
-        "coefficient",
-        "coef_rcue",
-        "color",
-        "niveau_competence_id",
-    ),
-    convert_empty_to_nulls=False,  # necessaire pour ue_code == ""
-    sortkey="numero",
-    input_formators={
-        "type": ndb.int_null_is_zero,
-        "is_external": scu.to_bool,
-        "ects": ndb.float_null_is_null,
-    },
-    output_formators={
-        "numero": ndb.int_null_is_zero,
-        "ects": ndb.float_null_is_null,
-        "coefficient": ndb.float_null_is_zero,
-        "semestre_idx": ndb.int_null_is_null,
-    },
-)
-
-
-def ue_list(*args, **kw):
-    "list UEs"
-    cnx = ndb.GetDBConnexion()
-    return _ueEditor.list(cnx, *args, **kw)
 
-
-def do_ue_create(args, allow_empty_ue_code=False):
+def do_ue_create(args, allow_empty_ue_code=False) -> UniteEns:
     "create an ue"
-    cnx = ndb.GetDBConnexion()
     # check duplicates
-    ues = ue_list({"formation_id": args["formation_id"], "acronyme": args["acronyme"]})
+    ues = UniteEns.query.filter_by(
+        formation_id=args["formation_id"], acronyme=args["acronyme"]
+    ).all()
     if ues:
         raise ScoValueError(
             f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
@@ -138,21 +96,21 @@ def do_ue_create(args, allow_empty_ue_code=False):
         args["coefficient"] = None
 
     # create
-    # XXX TODO utiliser UniteEns.create_from_dict
-    ue_id = _ueEditor.create(cnx, args)
-    log(f"do_ue_create: created {ue_id} with {args}")
+    ue = UniteEns.create_from_dict(args)
+    db.session.commit()
+    log(f"do_ue_create: created {ue} with {args}")
 
-    formation: Formation = db.session.get(Formation, args["formation_id"])
-    formation.invalidate_module_coefs()
+    # caches
+    ue.formation.invalidate_module_coefs()
+    ue.formation.invalidate_cached_sems()
     # news
-    formation = db.session.get(Formation, args["formation_id"])
     ScolarNews.add(
         typ=ScolarNews.NEWS_FORM,
         obj=args["formation_id"],
-        text=f"Modification de la formation {formation.acronyme}",
+        text=f"Modification de la formation {ue.formation.acronyme}",
     )
-    formation.invalidate_cached_sems()
-    return ue_id
+
+    return ue
 
 
 def do_ue_delete(ue: UniteEns, delete_validations=False, force=False):
@@ -544,13 +502,13 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
                 tf[2]["numero"] = next_ue_numero(
                     formation_id, int(tf[2]["semestre_idx"])
                 )
-            ue_id = do_ue_create(tf[2])
+            ue = do_ue_create(tf[2])
             matiere_id = None
             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 = Matiere.create_from_dict(
-                    {"ue_id": ue_id, "titre": tf[2]["titre"], "numero": 1}
+                    {"ue_id": ue.id, "titre": tf[2]["titre"], "numero": 1}
                 )
                 matiere_id = matiere.id
             if cursus.UE_IS_MODULE:
@@ -561,14 +519,13 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
                         "code": tf[2]["acronyme"],
                         # tous les modules auront coef 1, et on utilisera les ECTS:
                         "coefficient": 1.0,
-                        "ue_id": ue_id,
+                        "ue_id": ue.id,
                         "matiere_id": matiere_id,
                         "formation_id": formation_id,
                         "semestre_id": tf[2]["semestre_idx"],
                     },
                 )
                 db.session.commit()
-            ue = db.session.get(UniteEns, ue_id)
             flash(f"UE créée (code {ue.ue_code})")
         else:
             if not tf[2]["numero"]:
@@ -1118,12 +1075,12 @@ def _ue_table_ues(
                 ue.code_apogee or ""
             }</span>"""
 
-        if cur_ue_semestre_id != ue.semestre_id:
-            cur_ue_semestre_id = ue.semestre_id
-            if ue.semestre_id == codes_cursus.UE_SEM_DEFAULT:
+        if cur_ue_semestre_id != ue.get_semestre_id():
+            cur_ue_semestre_id = ue.get_semestre_id()
+            if ue.semestre_idx == codes_cursus.UE_SEM_DEFAULT:
                 lab = "Pas d'indication de semestre:"
             else:
-                lab = f"""Semestre {ue.semestre_id}:"""
+                lab = f"""Semestre {ue.get_semestre_id()}:"""
             H.append(
                 f'<div class="ue_list_div"><div class="ue_list_tit_sem">{lab}</div>'
             )
@@ -1205,8 +1162,8 @@ def _ue_table_ues(
             H.append(
                 f"""</ul><ul><li><a href="{
                     url_for('notes.ue_create', scodoc_dept=g.scodoc_dept,
-                    formation_id=ue.formation_id, semestre_idx=ue.semestre_id)
-            }">Ajouter une UE dans le semestre {ue.semestre_id or ''}</a></li></ul>
+                    formation_id=ue.formation_id, semestre_idx=ue.get_semestre_id())
+            }">Ajouter une UE dans le semestre {ue.get_semestre_id() or ''}</a></li></ul>
             </div>
             """
             )
diff --git a/app/formations/formation_io.py b/app/formations/formation_io.py
index f966fdb9ec3a62b0c43884e6877b3e5d6453ed8d..38fc53d13000ad0f2d87ce7a249042f47894b539 100644
--- a/app/formations/formation_io.py
+++ b/app/formations/formation_io.py
@@ -350,16 +350,15 @@ def formation_import_xml(doc: str | bytes, import_tags=True, use_local_refcomp=F
                 )
             # Note: si le code est indiqué "" dans le xml, il faut le conserver vide
             # pour la comparaison ultérieure des formations XXX
-            ue_id = edit_ue.do_ue_create(ue_info[1], allow_empty_ue_code=True)
-            ue: UniteEns = db.session.get(UniteEns, ue_id)
+            ue = edit_ue.do_ue_create(ue_info[1], allow_empty_ue_code=True)
             assert ue
             if xml_ue_id:
-                ues_old2new[xml_ue_id] = ue_id
+                ues_old2new[xml_ue_id] = ue.id
 
             # élément optionnel présent dans les exports BUT:
             ue_reference = ue_info[1].get("reference")
             if ue_reference:
-                ue_reference_to_id[int(ue_reference)] = ue_id
+                ue_reference_to_id[int(ue_reference)] = ue.id
 
             # -- Create matieres
             for mat_info in ue_info[2]:
@@ -397,7 +396,7 @@ def formation_import_xml(doc: str | bytes, import_tags=True, use_local_refcomp=F
                     continue
 
                 assert mat_info[0] == "matiere"
-                mat_info[1]["ue_id"] = ue_id
+                mat_info[1]["ue_id"] = ue.id
                 mat = Matiere.create_from_dict(mat_info[1])
                 mat_id = mat.id
                 # -- create modules
@@ -410,7 +409,7 @@ def formation_import_xml(doc: str | bytes, import_tags=True, use_local_refcomp=F
                         xml_module_id = None
                     mod_info[1]["formation_id"] = formation.id
                     mod_info[1]["matiere_id"] = mat_id
-                    mod_info[1]["ue_id"] = ue_id
+                    mod_info[1]["ue_id"] = ue.id
                     if not "module_type" in mod_info[1]:
                         mod_info[1]["module_type"] = scu.ModuleType.STANDARD
                     module = Module.create_from_dict(
diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index 4afd6a3f34af7895d7a2c1ad58ba8ae17b351c44..cd2f0e7d25281aef58595706852a7b4af88c663b 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -19,6 +19,7 @@ from operator import attrgetter
 from flask_login import current_user
 
 from flask import abort, flash, g, url_for
+from flask_sqlalchemy.query import Query
 from sqlalchemy.sql import text
 from sqlalchemy import func
 
@@ -799,7 +800,7 @@ class FormSemestre(models.ScoDocModel):
     @classmethod
     def get_dept_formsemestres_courants(
         cls, dept: Departement, date_courante: datetime.datetime | None = None
-    ) -> db.Query:
+    ) -> Query:
         """Liste (query) ordonnée des formsemestres courants, c'est
         à dire contenant la date courant (si None, la date actuelle)"""
         date_courante = date_courante or db.func.current_date()
diff --git a/app/models/ues.py b/app/models/ues.py
index f7ad5ea70897b1703282ae9b0052e2daa2d068d8..dce90b4e6c77d774317d182b8edfa675150686d8 100644
--- a/app/models/ues.py
+++ b/app/models/ues.py
@@ -120,7 +120,7 @@ class UniteEns(models.ScoDocModel):
         if "is_external" in args:
             args["is_external"] = scu.to_bool(args["is_external"])
         if "ects" in args:
-            args["ects"] = float(args["ects"])
+            args["ects"] = None if args["ects"] is None else float(args["ects"])
 
         return args
 
@@ -190,7 +190,7 @@ class UniteEns(models.ScoDocModel):
         utilisée dans un formsemestre verrouillé ou validations de jury de cette UE.
         Renvoie aussi une explication.
         """
-        from app.models import FormSemestre, ModuleImpl, ScolarFormSemestreValidation
+        from app.models import ModuleImpl, ScolarFormSemestreValidation
 
         # before 9.7.23: contains modules used in a locked formsemestre
         # starting from 9.7.23: + existence de validations de jury de cette UE
diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py
index 1352ea655d5200ebad9c32fe26ba5dae1f28a60d..99a9b8b93069f30abbea8ae0d50e35e96a9b9857 100644
--- a/app/scodoc/sco_bulletins_json.py
+++ b/app/scodoc/sco_bulletins_json.py
@@ -495,16 +495,16 @@ def dict_decision_jury(
                 # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id):
                 # always publish (car utile pour export Apogee)
                 for ue_id in decision["decisions_ue"].keys():
-                    ue = edit_ue.ue_list({"ue_id": ue_id})[0]
+                    ue = UniteEns.get_ue(ue_id)
                     d["decision_ue"].append(
-                        dict(
-                            ue_id=ue["ue_id"],
-                            numero=ue["numero"],
-                            acronyme=ue["acronyme"],
-                            titre=ue["titre"],
-                            code=decision["decisions_ue"][ue_id]["code"],
-                            ects=ue["ects"] or "",
-                        )
+                        {
+                            "ue_id": ue.ue_id,
+                            "numero": ue.numero,
+                            "acronyme": ue.acronyme,
+                            "titre": ue.titre,
+                            "code": decision["decisions_ue"][ue.id]["code"],
+                            "ects": ue.ects or "",
+                        }
                     )
             d["autorisation_inscription"] = []
             for aut in decision["autorisations"]:
@@ -515,7 +515,7 @@ def dict_decision_jury(
                     )
                 )
         else:
-            d["decision"] = dict(code="", etat="DEM")
+            d["decision"] = {"code": "", "etat": "DEM"}
         # Ajout jury BUT:
         if formsemestre.formation.is_apc():
             d.update(but_validations.dict_decision_jury(etud, formsemestre))
diff --git a/app/scodoc/sco_bulletins_xml.py b/app/scodoc/sco_bulletins_xml.py
index dfff8feeb5b53709469e2ab09a29aac39108552b..1a2e2d1f395241ad414a3c686b7172ab79f021d6 100644
--- a/app/scodoc/sco_bulletins_xml.py
+++ b/app/scodoc/sco_bulletins_xml.py
@@ -51,7 +51,7 @@ import app.scodoc.sco_utils as scu
 import app.scodoc.notesdb as ndb
 from app import log
 from app.but.bulletin_but_xml_compat import bulletin_but_xml_compat
-from app.models import BulAppreciations, Evaluation, FormSemestre
+from app.models import BulAppreciations, Evaluation, FormSemestre, UniteEns
 from app.scodoc import sco_assiduites
 from app.scodoc import codes_cursus
 from app.scodoc import sco_formsemestre
@@ -389,19 +389,19 @@ def make_xml_formsemestre_bulletinetud(
             else:
                 doc.append(Element("decision", code=code, etat=str(etat)))
 
-            if decision[
-                "decisions_ue"
-            ]:  # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
+            if decision["decisions_ue"]:
+                # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id):
+                # always publish (car utile pour export Apogee)
                 for ue_id in decision["decisions_ue"].keys():
-                    ue = edit_ue.ue_list({"ue_id": ue_id})[0]
+                    ue = UniteEns.get_ue(ue_id)
                     doc.append(
                         Element(
                             "decision_ue",
-                            ue_id=str(ue["ue_id"]),
-                            numero=quote_xml_attr(ue["numero"]),
-                            acronyme=quote_xml_attr(ue["acronyme"]),
-                            titre=quote_xml_attr(ue["titre"]),
-                            code=decision["decisions_ue"][ue_id]["code"],
+                            ue_id=str(ue.id),
+                            numero=quote_xml_attr(ue.numero),
+                            acronyme=quote_xml_attr(ue.acronyme),
+                            titre=quote_xml_attr(ue.titre or ""),
+                            code=decision["decisions_ue"][ue.id]["code"],
                         )
                     )
 
diff --git a/app/scodoc/sco_formsemestre_exterieurs.py b/app/scodoc/sco_formsemestre_exterieurs.py
index 9b2c59748243c3df4458693b222ce2e995cd904c..6ad279ff0f5ff860bfabd444dc64f165e11f2ba0 100644
--- a/app/scodoc/sco_formsemestre_exterieurs.py
+++ b/app/scodoc/sco_formsemestre_exterieurs.py
@@ -453,7 +453,7 @@ def _ue_form_description(
     return descr
 
 
-def _check_values(formsemestre: FormSemestre, ue_list, values):
+def _check_values(formsemestre: FormSemestre, ue_list: list[UniteEns], values):
     """Check that form values are ok
     for each UE:
         code != None => note and coef
diff --git a/app/scodoc/sco_ue_external.py b/app/scodoc/sco_ue_external.py
index fa85beebf7cb382d13c4883a5aaf7d554d38addf..4e74e25dc733eb763f7036a292e817087a0531e4 100644
--- a/app/scodoc/sco_ue_external.py
+++ b/app/scodoc/sco_ue_external.py
@@ -96,7 +96,7 @@ def external_ue_create(
     formation_id = formsemestre.formation.id
 
     numero = edit_ue.next_ue_numero(formation_id, semestre_id=formsemestre.semestre_id)
-    ue_id = edit_ue.do_ue_create(
+    ue = edit_ue.do_ue_create(
         {
             "formation_id": formation_id,
             "semestre_idx": formsemestre.semestre_id,
@@ -108,10 +108,9 @@ def external_ue_create(
             "is_external": True,
         },
     )
-    ue = db.session.get(UniteEns, ue_id)
     flash(f"UE créée (code {ue.ue_code})")
     matiere = Matiere.create_from_dict(
-        {"ue_id": ue_id, "titre": titre or acronyme, "numero": 1}
+        {"ue_id": ue.id, "titre": titre or acronyme, "numero": 1}
     )
 
     module = Module.create_from_dict(
@@ -119,7 +118,7 @@ def external_ue_create(
             "titre": "UE extérieure",
             "code": acronyme,
             "coefficient": ects,  # tous le coef. module est egal à la quantite d'ECTS
-            "ue_id": ue_id,
+            "ue_id": ue.id,
             "matiere_id": matiere.id,
             "formation_id": formation_id,
             "semestre_id": formsemestre.semestre_id,
diff --git a/tests/unit/sco_fake_gen.py b/tests/unit/sco_fake_gen.py
index d9545c96142585fdc8f4ae95e5d2e741b07ea03a..06a10f3f6aaceb73f21323d0da221dd24d2a32e0 100644
--- a/tests/unit/sco_fake_gen.py
+++ b/tests/unit/sco_fake_gen.py
@@ -197,11 +197,8 @@ class ScoFake(object):
         """
         if numero is None:
             numero = edit_ue.next_ue_numero(formation_id, 0)
-        oid = edit_ue.do_ue_create(locals())
-        oids = edit_ue.ue_list(args={"ue_id": oid})
-        if not oids:
-            raise ScoValueError("ue not created !")
-        return oid
+        ue = edit_ue.do_ue_create(locals())
+        return ue.id
 
     @logging_meth
     def create_matiere(self, ue_id=None, titre=None, numero=0) -> int:
diff --git a/tests/unit/test_formations.py b/tests/unit/test_formations.py
index a6811489172b5923d5267ffd037e01147ea2beaa..d047a07082540e3d00967fdbf4510977901343cd 100644
--- a/tests/unit/test_formations.py
+++ b/tests/unit/test_formations.py
@@ -51,7 +51,7 @@ from app.formations import (
     edit_ue,
     formation_io,
 )
-from app.models import Formation, Matiere, Module, ModuleImpl
+from app.models import Formation, Matiere, Module, ModuleImpl, UniteEns
 from app.scodoc import sco_formsemestre
 from app.scodoc import sco_exceptions
 from app.scodoc import sco_formsemestre_edit
@@ -292,11 +292,9 @@ def test_formations(test_client):
     li_mat2 = Matiere.query.all()
     assert len(li_mat2) == 3  # verification de la suppression de la matiere
 
-    li_ue = edit_ue.ue_list()
-    assert len(li_ue) == 4
+    assert UniteEns.query.count() == 4
     edit_ue.ue_delete(ue_id=uet_id, dialog_confirmed=True)
-    li_ue2 = edit_ue.ue_list()
-    assert len(li_ue2) == 3  # verification de la suppression de l'UE
+    assert UniteEns.query.count() == 3  # verification de la suppression de l'UE
 
     # --- Suppression d'une formation
 
@@ -321,9 +319,9 @@ def test_import_formation(test_client, filename="formation-exemple-1.xml"):
     assert len(f) == 3  # 3-uple
     formation_id = f[0]
     # --- Vérification des UE
-    ues = edit_ue.ue_list({"formation_id": formation_id})
+    ues = UniteEns.query.filter_by(formation_id=formation_id).all()
     assert len(ues) == 10
-    assert all(not ue["is_external"] for ue in ues)  # aucune UE externe dans le XML
+    assert all(not ue.is_external for ue in ues)  # aucune UE externe dans le XML
     # --- Mise en place de 4 semestres
     formsemestre_ids = [
         G.create_formsemestre(