diff --git a/README.md b/README.md
index b80ecd5b588d9166aa4209e1a7f829077f0cab61..4b3eda5fd2543e8a2d73d8e98cf9c2b19e2a2d3b 100644
--- a/README.md
+++ b/README.md
@@ -89,6 +89,17 @@ exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
 
 ### Tests unitaires
 
+Les tests unitaires utilisent normalement la base postgresql `SCODOC_TEST`.
+Avant le premier lancement, créer cette base ainsi:
+
+    ./tools/create_database.sh SCODOC_TEST
+    export FLASK_ENV=test
+    flask db upgrade
+
+Cette commande n'est nécessaire que la première fois (le contenu de la base
+est effacé au début de chaque test, mais son schéma reste) et aussi si des 
+migrations (changements de schéma) ont eu lieu dans le code.
+
 Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les 
 scripts de tests:
 Lancer au préalable:
@@ -109,7 +120,8 @@ On peut aussi utiliser les tests unitaires pour mettre la base
 de données de développement dans un état connu, par exemple pour éviter de
 recréer à la main étudiants et semestres quand on développe. 
 
-Il suffit de positionner une variable d'environnement indiquant la BD utilisée par les tests:
+Il suffit de positionner une variable d'environnement indiquant la BD 
+utilisée par les tests:
 
     export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
 
diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py
new file mode 100644
index 0000000000000000000000000000000000000000..d819beda5fdefdd9b58c9f37491fb313433b3b13
--- /dev/null
+++ b/app/comp/moy_mod.py
@@ -0,0 +1,75 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2021 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
+#
+##############################################################################
+
+"""Fonctions de calcul des moyennes de modules (modules, ressources ou SAÉ)
+
+Rappel: pour éviter les confusions, on appelera *poids* les coefficients d'une
+évaluation dans un module, et *coefficients* ceux utilisés pour le calcul de la
+moyenne générale d'une UE.
+"""
+import numpy as np
+import pandas as pd
+
+from app import db
+from app import models
+
+
+def df_load_evaluations_poids(moduleimpl_id: int, default_poids=1.0) -> pd.DataFrame:
+    """Charge poids des évaluations d'un module et retourne un dataframe
+    rows = evaluations, columns = UE, value = poids (float).
+    Les valeurs manquantes (évaluations sans coef vers des UE) sont
+    remplies par default_poids.
+    """
+    modimpl = models.ModuleImpl.query.get(moduleimpl_id)
+    evaluations = models.Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
+    ues = modimpl.formsemestre.query_ues().all()
+    ue_ids = [ue.id for ue in ues]
+    evaluation_ids = [evaluation.id for evaluation in evaluations]
+    df = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
+    for eval_poids in models.EvaluationUEPoids.query.join(
+        models.EvaluationUEPoids.evaluation
+    ).filter_by(moduleimpl_id=moduleimpl_id):
+        df[eval_poids.ue_id][eval_poids.evaluation_id] = eval_poids.poids
+    if default_poids is not None:
+        df.fillna(value=default_poids, inplace=True)
+    return df
+
+
+def check_moduleimpl_conformity(
+    moduleimpl, evals_poids: pd.DataFrame, modules_coefficients: pd.DataFrame
+) -> bool:
+    """Vérifie que les évaluations de ce moduleimpl sont bien conformes
+    au PN.
+    Un module est dit *conforme* si et seulement si la somme des poids de ses
+    évaluations vers une UE de coefficient non nul est non nulle.
+    """
+    module_evals_poids = evals_poids.transpose().sum(axis=1).to_numpy() != 0
+    check = all(
+        (modules_coefficients[moduleimpl.module.id].to_numpy() != 0)
+        == module_evals_poids
+    )
+    return check
diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py
index b2eb29e062be41726cf721d1180c83b65467e5f4..6a0ca770a8172495ad8cc4959c937aafd017d086 100644
--- a/app/comp/moy_ue.py
+++ b/app/comp/moy_ue.py
@@ -34,21 +34,22 @@ from app import db
 from app import models
 
 
-def df_load_ue_coefs(formation_id):
+def df_load_ue_coefs(formation_id: int, semestre_idx: int) -> pd.DataFrame:
     """Load coefs of all modules in formation and returns a DataFrame
-    rows = modules, columns = UE, value = coef.
+    rows = UEs, columns = modules, value = coef.
+    On considère toutes les UE et modules du semestre.
     Unspecified coefs (not defined in db) are set to zero.
     """
-    ues = models.UniteEns.query.filter_by(formation_id=formation_id).all()
-    modules = models.Module.query.filter_by(formation_id=formation_id).all()
+    ues = models.UniteEns.query.filter_by(formation_id=formation_id)
+    modules = models.Module.query.filter_by(formation_id=formation_id)
     ue_ids = [ue.id for ue in ues]
     module_ids = [module.id for module in modules]
-    df = pd.DataFrame(columns=ue_ids, index=module_ids, dtype=float)
+    df = pd.DataFrame(columns=module_ids, index=ue_ids, dtype=float)
     for mod_coef in (
         db.session.query(models.ModuleUECoef)
         .filter(models.UniteEns.formation_id == formation_id)
         .filter(models.ModuleUECoef.ue_id == models.UniteEns.id)
     ):
-        df[mod_coef.ue_id][mod_coef.module_id] = mod_coef.coef
+        df[mod_coef.module_id][mod_coef.ue_id] = mod_coef.coef
     df.fillna(value=0, inplace=True)
     return df
diff --git a/app/models/__init__.py b/app/models/__init__.py
index 6f844602c25b13f8b9ba01e750d6f1b9aaa72bac..b07f12da686b0810111b8c5463767a5c4427d885 100644
--- a/app/models/__init__.py
+++ b/app/models/__init__.py
@@ -37,7 +37,6 @@ from app.models.formations import (
     Module,
     ModuleUECoef,
     NotesTag,
-    notes_modules_tags,
 )
 from app.models.formsemestre import (
     FormSemestre,
diff --git a/app/models/formations.py b/app/models/formations.py
index db154e500c2e4f6acf88b79576ce2292445b0a0e..38309a5b892e48a935488459d6921d0c59579ae2 100644
--- a/app/models/formations.py
+++ b/app/models/formations.py
@@ -36,6 +36,7 @@ class Formation(db.Model):
     ues = db.relationship("UniteEns", backref="formation", lazy="dynamic")
     formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation")
     ues = db.relationship("UniteEns", lazy="dynamic", backref="formation")
+    modules = db.relationship("Module", lazy="dynamic", backref="formation")
 
     def __repr__(self):
         return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme}')>"
@@ -133,6 +134,12 @@ class Module(db.Model):
     # Relations:
     modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic")
     ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True)
+    tags = db.relationship(
+        "NotesTag",
+        secondary="notes_modules_tags",
+        lazy=True,
+        backref=db.backref("modules", lazy=True),
+    )
 
     def __init__(self, **kwargs):
         self.ue_coefs = []
diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index 1207d6bfa3bdc24636962d338d22b74eff321e92..eef37102cd2a5d4c90c0134a90876ea4ecd2a846 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -4,6 +4,8 @@
 """
 from typing import Any
 
+import flask_sqlalchemy
+
 from app import db
 from app.models import APO_CODE_STR_LEN
 from app.models import SHORT_STR_LEN
@@ -12,6 +14,8 @@ from app.models import UniteEns
 
 import app.scodoc.notesdb as ndb
 from app.scodoc import sco_evaluation_db
+from app.models.formations import UniteEns, Module
+from app.models.moduleimpls import ModuleImpl
 
 
 class FormSemestre(db.Model):
@@ -87,8 +91,24 @@ class FormSemestre(db.Model):
         if self.modalite is None:
             self.modalite = FormationModalite.DEFAULT_MODALITE
 
-    def get_ues(self):
-        "UE des modules de ce semestre"
+    def query_ues(self) -> flask_sqlalchemy.BaseQuery:
+        """UE des modules de ce semestre.
+        - Formations classiques: les UEs auxquelles appartiennent
+          les modules mis en place dans ce semestre.
+        - Formations APC / BUT: les UEs de la formation qui ont
+        le même numéro de semestre que ce formsemestre.
+        """
+        if self.formation.get_parcours().APC_SAE:
+            sem_ues = UniteEns.query.filter_by(
+                formation=self.formation, semestre_idx=self.semestre_id
+            )
+        else:
+            sem_ues = db.session.query(UniteEns).filter(
+                ModuleImpl.formsemestre_id == self.id,
+                Module.id == ModuleImpl.module_id,
+                UniteEns.id == Module.ue_id,
+            )
+        return sem_ues
 
 
 # Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
diff --git a/app/scodoc/sco_edit_apc.py b/app/scodoc/sco_edit_apc.py
new file mode 100644
index 0000000000000000000000000000000000000000..2652c4f913785d8b0338be2a89c9d5cc66819165
--- /dev/null
+++ b/app/scodoc/sco_edit_apc.py
@@ -0,0 +1,122 @@
+##############################################################################
+#
+# ScoDoc
+#
+# Copyright (c) 1999 - 2021 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
+#
+##############################################################################
+
+"""Édition formation APC (BUT)
+"""
+import flask
+from flask import url_for, render_template
+from flask import g, request
+from flask_login import current_user
+from app.models.formations import Formation, UniteEns, Matiere, Module
+
+import app.scodoc.notesdb as ndb
+import app.scodoc.sco_utils as scu
+from app.scodoc import sco_groups
+from app.scodoc.sco_utils import ModuleType
+
+
+def html_edit_formation_apc(
+    formation,
+    editable=True,
+    tag_editable=True,
+):
+    """Formulaire html pour visualisation ou édition d'une formation APC.
+    - Les UEs
+    - Les ressources
+    - Les SAÉs
+    """
+    parcours = formation.get_parcours()
+    assert parcours.APC_SAE
+    ressources = formation.modules.filter_by(module_type=ModuleType.RESSOURCE)
+    saes = formation.modules.filter_by(module_type=ModuleType.SAE)
+    other_modules = formation.modules.filter(
+        Module.module_type != ModuleType.SAE
+        and Module.module_type != ModuleType.RESSOURCE
+    )
+    arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
+    delete_icon = scu.icontag(
+        "delete_small_img", title="Supprimer (module inutilisé)", alt="supprimer"
+    )
+    delete_disabled_icon = scu.icontag(
+        "delete_small_dis_img", title="Suppression impossible (module utilisé)"
+    )
+    H = [
+        render_template(
+            "pn/form_ues.html",
+            formation=formation,
+            editable=editable,
+            arrow_up=arrow_up,
+            arrow_down=arrow_down,
+            arrow_none=arrow_none,
+            delete_icon=delete_icon,
+            delete_disabled_icon=delete_disabled_icon,
+        ),
+        render_template(
+            "pn/form_mods.html",
+            formation=formation,
+            titre="Ressources",
+            create_element_msg="créer une nouvelle ressource",
+            modules=ressources,
+            module_type=ModuleType.RESSOURCE,
+            editable=editable,
+            arrow_up=arrow_up,
+            arrow_down=arrow_down,
+            arrow_none=arrow_none,
+            delete_icon=delete_icon,
+            delete_disabled_icon=delete_disabled_icon,
+            scu=scu,
+        ),
+        render_template(
+            "pn/form_mods.html",
+            formation=formation,
+            titre="Situations d'Apprentissage et d'Évaluation (SAÉs)",
+            create_element_msg="créer une nouvelle SAÉ",
+            modules=saes,
+            module_type=ModuleType.SAE,
+            editable=editable,
+            arrow_up=arrow_up,
+            arrow_down=arrow_down,
+            arrow_none=arrow_none,
+            delete_icon=delete_icon,
+            delete_disabled_icon=delete_disabled_icon,
+            scu=scu,
+        ),
+        render_template(
+            "pn/form_mods.html",
+            formation=formation,
+            titre="Autres modules (non BUT)",
+            create_element_msg="créer un nouveau module",
+            modules=other_modules,
+            module_type=ModuleType.STANDARD,
+            editable=editable,
+            arrow_up=arrow_up,
+            arrow_down=arrow_down,
+            arrow_none=arrow_none,
+            delete_icon=delete_icon,
+            delete_disabled_icon=delete_disabled_icon,
+            scu=scu,
+        ),
+    ]
+
+    return "\n".join(H)
diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py
index 979ed3af7bbe0610b646af3d58b424a3b46fbbe9..b61854d28a252d039625199df39ab9b3f5c7be0b 100644
--- a/app/scodoc/sco_edit_module.py
+++ b/app/scodoc/sco_edit_module.py
@@ -32,6 +32,7 @@ import flask
 from flask import url_for, render_template
 from flask import g, request
 from flask_login import current_user
+from app.models.formations import Matiere, UniteEns
 
 import app.scodoc.notesdb as ndb
 import app.scodoc.sco_utils as scu
@@ -106,80 +107,99 @@ def do_module_create(args) -> int:
     return r
 
 
-def module_create(matiere_id=None):
+def module_create(matiere_id=None, module_type=None, semestre_id=None):
     """Création d'un module"""
     from app.scodoc import sco_formations
     from app.scodoc import sco_edit_ue
 
-    if matiere_id is None:
+    matiere = Matiere.query.get(matiere_id)
+    if matiere is None:
         raise ScoValueError("invalid matiere !")
-    matiere = sco_edit_matiere.matiere_list(args={"matiere_id": matiere_id})[0]
-    UE = sco_edit_ue.ue_list(args={"ue_id": matiere["ue_id"]})[0]
-    formation = sco_formations.formation_list(
-        args={"formation_id": UE["formation_id"]}
-    )[0]
-    parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"])
+    ue = matiere.ue
+    parcours = ue.formation.get_parcours()
     is_apc = parcours.APC_SAE
-    semestres_indices = list(range(1, parcours.NB_SEM + 1))
+    if is_apc and module_type is not None:
+        object_name = scu.MODULE_TYPE_NAMES[module_type]
+    else:
+        object_name = "Module"
     H = [
-        html_sco_header.sco_header(page_title="Création d'un module"),
-        """<h2>Création d'un module dans la matière %(titre)s""" % matiere,
-        """ (UE %(acronyme)s)</h2>""" % UE,
-        render_template("scodoc/help/modules.html", is_apc=is_apc),
+        html_sco_header.sco_header(page_title=f"Création {object_name}"),
+        f"""<h2>Création {object_name} dans la matière {matiere.titre},
+        (UE {ue.acronyme})</h2>
+        """,
+        render_template(
+            "scodoc/help/modules.html",
+            is_apc=is_apc,
+            ue=ue,
+            semestre_id=semestre_id,
+        ),
     ]
     # cherche le numero adéquat (pour placer le module en fin de liste)
-    modules = module_list(args={"matiere_id": matiere_id})
+    modules = Matiere.query.get(1).modules.all()
     if modules:
-        default_num = max([m["numero"] for m in modules]) + 10
+        default_num = max([m.numero for m in modules]) + 10
     else:
         default_num = 10
-    tf = TrivialFormulator(
-        request.base_url,
-        scu.get_request_args(),
+    descr = [
         (
+            "code",
+            {
+                "size": 10,
+                "explanation": "code du module (doit être unique dans la formation)",
+                "allow_null": False,
+                "validator": lambda val, field, formation_id=ue.formation_id: check_module_code_unicity(
+                    val, field, formation_id
+                ),
+            },
+        ),
+        ("titre", {"size": 30, "explanation": "nom du module"}),
+        ("abbrev", {"size": 20, "explanation": "nom abrégé (pour bulletins)"}),
+        (
+            "module_type",
+            {
+                "input_type": "menu",
+                "title": "Type",
+                "explanation": "",
+                "labels": [x.name.capitalize() for x in scu.ModuleType],
+                "allowed_values": [str(int(x)) for x in scu.ModuleType],
+            },
+        ),
+        (
+            "heures_cours",
+            {"size": 4, "type": "float", "explanation": "nombre d'heures de cours"},
+        ),
+        (
+            "heures_td",
+            {
+                "size": 4,
+                "type": "float",
+                "explanation": "nombre d'heures de Travaux Dirigés",
+            },
+        ),
+        (
+            "heures_tp",
+            {
+                "size": 4,
+                "type": "float",
+                "explanation": "nombre d'heures de Travaux Pratiques",
+            },
+        ),
+    ]
+    if is_apc:
+        descr += [
             (
-                "code",
-                {
-                    "size": 10,
-                    "explanation": "code du module (doit être unique dans la formation)",
-                    "allow_null": False,
-                    "validator": lambda val, field, formation_id=formation[
-                        "formation_id"
-                    ]: check_module_code_unicity(val, field, formation_id),
-                },
-            ),
-            ("titre", {"size": 30, "explanation": "nom du module"}),
-            ("abbrev", {"size": 20, "explanation": "nom abrégé (pour bulletins)"}),
-            (
-                "module_type",
-                {
-                    "input_type": "menu",
-                    "title": "Type",
-                    "explanation": "",
-                    "labels": [x.name.capitalize() for x in scu.ModuleType],
-                    "allowed_values": [str(int(x)) for x in scu.ModuleType],
-                },
-            ),
-            (
-                "heures_cours",
-                {"size": 4, "type": "float", "explanation": "nombre d'heures de cours"},
-            ),
-            (
-                "heures_td",
-                {
-                    "size": 4,
-                    "type": "float",
-                    "explanation": "nombre d'heures de Travaux Dirigés",
-                },
-            ),
-            (
-                "heures_tp",
+                "sep_ue_coefs",
                 {
-                    "size": 4,
-                    "type": "float",
-                    "explanation": "nombre d'heures de Travaux Pratiques",
+                    "input_type": "separator",
+                    "title": """
+                    <div>(<em>les coefficients vers les UE se fixent sur la page dédiée</em>)
+                    </div>""",
                 },
             ),
+        ]
+    else:
+        semestres_indices = list(range(1, parcours.NB_SEM + 1))
+        descr += [
             (
                 "coefficient",
                 {
@@ -189,10 +209,6 @@ def module_create(matiere_id=None):
                     "allow_null": False,
                 },
             ),
-            # ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS (inutilisés: les crédits sont associés aux UE)' }),
-            ("formation_id", {"default": UE["formation_id"], "input_type": "hidden"}),
-            ("ue_id", {"default": matiere["ue_id"], "input_type": "hidden"}),
-            ("matiere_id", {"default": matiere["matiere_id"], "input_type": "hidden"}),
             (
                 "semestre_id",
                 {
@@ -205,24 +221,35 @@ def module_create(matiere_id=None):
                     "allowed_values": semestres_indices,
                 },
             ),
-            (
-                "code_apogee",
-                {
-                    "title": "Code Apogée",
-                    "size": 25,
-                    "explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
-                },
-            ),
-            (
-                "numero",
-                {
-                    "size": 2,
-                    "explanation": "numéro (1,2,3,4...) pour ordre d'affichage",
-                    "type": "int",
-                    "default": default_num,
-                },
-            ),
+        ]
+    descr += [
+        # ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS (inutilisés: les crédits sont associés aux UE)' }),
+        ("formation_id", {"default": ue.formation_id, "input_type": "hidden"}),
+        ("ue_id", {"default": ue.id, "input_type": "hidden"}),
+        ("matiere_id", {"default": matiere.id, "input_type": "hidden"}),
+        (
+            "code_apogee",
+            {
+                "title": "Code Apogée",
+                "size": 25,
+                "explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
+            },
+        ),
+        (
+            "numero",
+            {
+                "size": 2,
+                "explanation": "numéro (1,2,3,4...) pour ordre d'affichage",
+                "type": "int",
+                "default": default_num,
+            },
         ),
+    ]
+    args = scu.get_request_args()
+    tf = TrivialFormulator(
+        request.base_url,
+        args,
+        descr,
         submitlabel="Créer ce module",
     )
     if tf[0] == 0:
@@ -233,7 +260,7 @@ def module_create(matiere_id=None):
             url_for(
                 "notes.ue_table",
                 scodoc_dept=g.scodoc_dept,
-                formation_id=UE["formation_id"],
+                formation_id=ue.formation_id,
             )
         )
 
@@ -344,6 +371,7 @@ def module_edit(module_id=None):
     if not modules:
         raise ScoValueError("invalid module !")
     module = modules[0]
+    a_module = models.Module.query.get(module_id)
     unlocked = not module_is_locked(module_id)
     formation_id = module["formation_id"]
     formation = sco_formations.formation_list(args={"formation_id": formation_id})[0]
@@ -434,7 +462,6 @@ def module_edit(module_id=None):
         ),
     ]
     if is_apc:
-        a_module = models.Module.query.get(module_id)
         coefs_descr = a_module.ue_coefs_descr()
         if coefs_descr:
             coefs_descr_txt = ", ".join(["%s: %s" % x for x in coefs_descr])
@@ -479,19 +506,36 @@ def module_edit(module_id=None):
                 "enabled": unlocked,
             },
         ),
-        (
-            "semestre_id",
-            {
-                "input_type": "menu",
-                "type": "int",
-                "title": parcours.SESSION_NAME.capitalize(),
-                "explanation": "%s de début du module dans la formation standard"
-                % parcours.SESSION_NAME,
-                "labels": [str(x) for x in semestres_indices],
-                "allowed_values": semestres_indices,
-                "enabled": unlocked,
-            },
-        ),
+    ]
+    if is_apc:
+        # le semestre du module est toujours celui de son UE
+        descr += [
+            (
+                "semestre_id",
+                {
+                    "input_type": "hidden",
+                    "type": "int",
+                    "readonly": True,
+                },
+            )
+        ]
+    else:
+        descr += [
+            (
+                "semestre_id",
+                {
+                    "input_type": "menu",
+                    "type": "int",
+                    "title": parcours.SESSION_NAME.capitalize(),
+                    "explanation": "%s de début du module dans la formation standard"
+                    % parcours.SESSION_NAME,
+                    "labels": [str(x) for x in semestres_indices],
+                    "allowed_values": semestres_indices,
+                    "enabled": unlocked,
+                },
+            )
+        ]
+    descr += [
         (
             "code_apogee",
             {
@@ -509,7 +553,8 @@ def module_edit(module_id=None):
             },
         ),
     ]
-
+    # force module semestre_idx to its UE
+    module["semestre_id"] = a_module.ue.semestre_idx
     tf = TrivialFormulator(
         request.base_url,
         scu.get_request_args(),
@@ -538,7 +583,7 @@ def module_edit(module_id=None):
 def edit_module_set_code_apogee(id=None, value=None):
     "Set UE code apogee"
     module_id = id
-    value = value.strip("-_ \t")
+    value = str(value).strip("-_ \t")
     log("edit_module_set_code_apogee: module_id=%s code_apogee=%s" % (module_id, value))
 
     modules = module_list(args={"module_id": module_id})
diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py
index 1a46be44a669457aa3f37feb850565b0006e8158..77b89b1d00bf514e0879c29be03230526be9b0ab 100644
--- a/app/scodoc/sco_edit_ue.py
+++ b/app/scodoc/sco_edit_ue.py
@@ -45,6 +45,7 @@ from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError
 from app.scodoc import html_sco_header
 from app.scodoc import sco_cache
 from app.scodoc import sco_codes_parcours
+from app.scodoc import sco_edit_apc
 from app.scodoc import sco_edit_formation
 from app.scodoc import sco_edit_matiere
 from app.scodoc import sco_edit_module
@@ -382,27 +383,32 @@ def ue_edit(ue_id=None, create=False, formation_id=None):
         )
 
 
-def _add_ue_semestre_id(ues):
+def _add_ue_semestre_id(ues: list[dict], is_apc):
     """ajoute semestre_id dans les ue, en regardant
-    semestre_idx ou à défaut le premier module de chacune.
-    Les UE sans modules se voient attribuer le numero UE_SEM_DEFAULT (1000000),
+    semestre_idx ou à défaut, pour les formations non APC, le premier module
+    de chacune.
+    Les UE sans modules se voient attribuer  le numero UE_SEM_DEFAULT (1000000),
     qui les place à la fin de la liste.
     """
     for ue in ues:
         if ue["semestre_idx"] is not None:
             ue["semestre_id"] = ue["semestre_idx"]
+        elif is_apc:
+            ue["semestre_id"] = sco_codes_parcours.UE_SEM_DEFAULT
         else:
+            # était le comportement ScoDoc7
             modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
             if modules:
                 ue["semestre_id"] = modules[0]["semestre_id"]
             else:
-                ue["semestre_id"] = 1000000
+                ue["semestre_id"] = sco_codes_parcours.UE_SEM_DEFAULT
 
 
 def next_ue_numero(formation_id, semestre_id=None):
     """Numero d'une nouvelle UE dans cette formation.
     Si le semestre est specifie, cherche les UE ayant des modules de ce semestre
     """
+    formation = Formation.query.get(formation_id)
     ues = ue_list(args={"formation_id": formation_id})
     if not ues:
         return 0
@@ -410,7 +416,7 @@ def next_ue_numero(formation_id, semestre_id=None):
         return ues[-1]["numero"] + 1000
     else:
         # Avec semestre: (prend le semestre du 1er module de l'UE)
-        _add_ue_semestre_id(ues)
+        _add_ue_semestre_id(ues, formation.get_parcours().APC_SAE)
         ue_list_semestre = [ue for ue in ues if ue["semestre_id"] == semestre_id]
         if ue_list_semestre:
             return ue_list_semestre[-1]["numero"] + 10
@@ -447,27 +453,27 @@ def ue_table(formation_id=None, msg=""):  # was ue_list
     from app.scodoc import sco_formations
     from app.scodoc import sco_formsemestre_validation
 
-    F = sco_formations.formation_list(args={"formation_id": formation_id})
-    if not F:
+    formation = Formation.query.get(formation_id)
+    if not formation:
         raise ScoValueError("invalid formation_id")
-    F = F[0]
-    parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
+    parcours = formation.get_parcours()
     is_apc = parcours.APC_SAE
     locked = sco_formations.formation_has_locked_sems(formation_id)
 
     ues = ue_list(args={"formation_id": formation_id, "is_external": False})
     ues_externes = ue_list(args={"formation_id": formation_id, "is_external": True})
     # tri par semestre et numero:
-    _add_ue_semestre_id(ues)
-    _add_ue_semestre_id(ues_externes)
+    _add_ue_semestre_id(ues, is_apc)
+    _add_ue_semestre_id(ues_externes, is_apc)
     ues.sort(key=lambda u: (u["semestre_id"], u["numero"]))
     ues_externes.sort(key=lambda u: (u["semestre_id"], u["numero"]))
     has_duplicate_ue_codes = len(set([ue["ue_code"] for ue in ues])) != len(ues)
 
     has_perm_change = current_user.has_permission(Permission.ScoChangeFormation)
     # editable = (not locked) and has_perm_change
-    # On autorise maintanant la modification des formations qui ont des semestres verrouillés,
-    # sauf si cela affect les notes passées (verrouillées):
+    # On autorise maintenant la modification des formations qui ont
+    # des semestres verrouillés, sauf si cela affect les notes passées
+    # (verrouillées):
     #   - pas de modif des modules utilisés dans des semestres verrouillés
     #   - pas de changement des codes d'UE utilisés dans des semestres verrouillés
     editable = has_perm_change
@@ -496,12 +502,13 @@ def ue_table(formation_id=None, msg=""):  # was ue_list
                 "libjs/jQuery-tagEditor/jquery.caret.min.js",
                 "js/module_tag_editor.js",
             ],
-            page_title="Programme %s" % F["acronyme"],
+            page_title=f"Programme {formation.acronyme}",
         ),
-        """<h2>Formation %(titre)s (%(acronyme)s) [version %(version)s] code %(formation_code)s"""
-        % F,
-        lockicon,
-        "</h2>",
+        f"""<h2>Formation {formation.titre} ({formation.acronyme}) 
+        [version {formation.version}] code {formation.formation_code}
+        {lockicon}
+        </h2>
+        """,
     ]
     if locked:
         H.append(
@@ -533,41 +540,41 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
     # Description de la formation
     H.append('<div class="formation_descr">')
     H.append(
-        '<div class="fd_d"><span class="fd_t">Titre:</span><span class="fd_v">%(titre)s</span></div>'
-        % F
-    )
-    H.append(
-        '<div class="fd_d"><span class="fd_t">Titre officiel:</span><span class="fd_v">%(titre_officiel)s</span></div>'
-        % F
-    )
-    H.append(
-        '<div class="fd_d"><span class="fd_t">Acronyme:</span><span class="fd_v">%(acronyme)s</span></div>'
-        % F
-    )
-    H.append(
-        '<div class="fd_d"><span class="fd_t">Code:</span><span class="fd_v">%(formation_code)s</span></div>'
-        % F
-    )
-    H.append(
-        '<div class="fd_d"><span class="fd_t">Version:</span><span class="fd_v">%(version)s</span></div>'
-        % F
-    )
-    H.append(
-        '<div class="fd_d"><span class="fd_t">Type parcours:</span><span class="fd_v">%s</span></div>'
-        % parcours.__doc__
+        f"""<div class="fd_d"><span class="fd_t">Titre:
+        </span><span class="fd_v">{formation.titre}</span>
+        </div>
+        <div class="fd_d"><span class="fd_t">Titre officiel:</span>
+        <span class="fd_v">{formation.titre_officiel}</span>
+        </div>
+        <div class="fd_d"><span class="fd_t">Acronyme:</span>
+        <span class="fd_v">{formation.acronyme}</span>
+        </div>
+        <div class="fd_d"><span class="fd_t">Code:</span>
+        <span class="fd_v">{formation.formation_code}</span>
+        </div>
+        <div class="fd_d"><span class="fd_t">Version:</span>
+        <span class="fd_v">{formation.version}</span>
+        </div>
+        <div class="fd_d"><span class="fd_t">Type parcours:</span>
+        <span class="fd_v">{parcours.__doc__}</span>
+        </div>
+        """
     )
     if parcours.UE_IS_MODULE:
         H.append(
-            '<div class="fd_d"><span class="fd_t"> </span><span class="fd_n">(Chaque module est une UE)</span></div>'
+            """<div class="fd_d"><span class="fd_t"> </span>
+            <span class="fd_n">(Chaque module est une UE)</span></div>"""
         )
 
     if editable:
         H.append(
-            '<div><a href="formation_edit?formation_id=%(formation_id)s" class="stdlink">modifier ces informations</a></div>'
-            % F
+            f"""<div><a href="{
+                url_for('notes.formation_edit', scodoc_dept=g.scodoc_dept, 
+                formation_id=formation_id)
+                }" class="stdlink">modifier ces informations</a></div>"""
         )
-
     H.append("</div>")
+
     # Formation APC (BUT) ?
     if is_apc:
         H.append(
@@ -575,50 +582,67 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
         <div class="ue_list_tit">Formation par compétences (BUT)</div>
         <ul>
         <li><a class="stdlink" href="{
-            url_for('notes.edit_modules_ue_coefs', scodoc_dept=g.scodoc_dept, formation_id=formation_id)
+            url_for('notes.edit_modules_ue_coefs', scodoc_dept=g.scodoc_dept, formation_id=formation_id, semestre_idx=None)
         }">éditer les coefficients des ressources et SAÉs</a></li>
         </ul>
         </div>"""
         )
     # Description des UE/matières/modules
-    H.append('<div class="formation_ue_list">')
-    H.append('<div class="ue_list_tit">Programme pédagogique:</div>')
-
-    H.append(
-        '<form><input type="checkbox" class="sco_tag_checkbox">montrer les tags</input></form>'
-    )
     H.append(
-        _ue_table_ues(
-            parcours,
-            ues,
-            editable,
-            tag_editable,
-            has_perm_change,
-            arrow_up,
-            arrow_down,
-            arrow_none,
-            delete_icon,
-            delete_disabled_icon,
-        )
+        """
+    <div class="formation_ue_list">
+    <div class="ue_list_tit">Programme pédagogique:</div>
+    <form>
+    <input type="checkbox" class="sco_tag_checkbox">montrer les tags</input>
+    </form>
+    """
     )
-    if editable:
+    if is_apc:
         H.append(
-            '<ul><li><a class="stdlink" href="ue_create?formation_id=%s">Ajouter une UE</a></li>'
-            % formation_id
+            sco_edit_apc.html_edit_formation_apc(
+                formation, editable=editable, tag_editable=tag_editable
+            )
         )
+    else:
         H.append(
-            '<li><a href="formation_add_malus_modules?formation_id=%(formation_id)s" class="stdlink">Ajouter des modules de malus dans chaque UE</a></li></ul>'
-            % F
+            _ue_table_ues(
+                parcours,
+                ues,
+                editable,
+                tag_editable,
+                has_perm_change,
+                arrow_up,
+                arrow_down,
+                arrow_none,
+                delete_icon,
+                delete_disabled_icon,
+            )
         )
+        if editable:
+            H.append(
+                f"""<ul>
+                <li><a class="stdlink" href="{
+                    url_for('notes.ue_create', scodoc_dept=g.scodoc_dept, formation_id=formation_id)
+                    }">Ajouter une UE</a>
+                </li>
+                <li><a href="{
+                    url_for('notes.formation_add_malus_modules', 
+                    scodoc_dept=g.scodoc_dept, formation_id=formation_id)
+                }" class="stdlink">Ajouter des modules de malus dans chaque UE</a>
+                </li>
+                </ul>
+                """
+            )
+
     H.append("</div>")  # formation_ue_list
 
     if ues_externes:
-        H.append('<div class="formation_ue_list formation_ue_list_externes">')
         H.append(
-            '<div class="ue_list_tit">UE externes déclarées (pour information):</div>'
-        )
-        H.append(
-            _ue_table_ues(
+            f"""
+        <div class="formation_ue_list formation_ue_list_externes">
+            <div class="ue_list_tit">UE externes déclarées (pour information):
+            </div>
+            {_ue_table_ues(
                 parcours,
                 ues_externes,
                 editable,
@@ -629,30 +653,49 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
                 arrow_none,
                 delete_icon,
                 delete_disabled_icon,
-            )
+            )}
+        </div>
+        """
         )
-        H.append("</div>")  # formation_ue_list
-
     H.append("<p><ul>")
     if editable:
         H.append(
-            """
-<li><a class="stdlink" href="formation_create_new_version?formation_id=%(formation_id)s">Créer une nouvelle version (non verrouillée)</a></li>
-"""
-            % F
+            f"""
+            <li><a class="stdlink" href="{
+            url_for('notes.formation_create_new_version', 
+            scodoc_dept=g.scodoc_dept, formation_id=formation_id
+            )
+            }">Créer une nouvelle version (non verrouillée)</a>
+            </li>
+        """
         )
     H.append(
-        """
-<li><a class="stdlink" href="formation_table_recap?formation_id=%(formation_id)s">Table récapitulative de la formation</a></li>
-    
-<li><a class="stdlink" href="formation_export?formation_id=%(formation_id)s&format=xml">Export XML de la formation</a> (permet de la sauvegarder pour l'échanger avec un autre site)</li>
+        f"""
+        <li><a class="stdlink" href="{
+            url_for('notes.formation_table_recap', scodoc_dept=g.scodoc_dept,
+                formation_id=formation_id)
+            }">Table récapitulative de la formation</a>
+        </li>    
+        <li><a class="stdlink" href="{
+            url_for('notes.formation_export', scodoc_dept=g.scodoc_dept,
+                formation_id=formation_id, format='xml')
+            }">Export XML de la formation</a> 
+            (permet de la sauvegarder pour l'échanger avec un autre site)
+        </li>
 
-<li><a class="stdlink" href="formation_export?formation_id=%(formation_id)s&format=json">Export JSON de la formation</a></li>
+        <li><a class="stdlink" href="{
+            url_for('notes.formation_export', scodoc_dept=g.scodoc_dept,
+            formation_id=formation_id, format='json')
+            }">Export JSON de la formation</a>
+        </li>
 
-<li><a class="stdlink" href="module_list?formation_id=%(formation_id)s">Liste détaillée des modules de la formation</a> (debug) </li>
-</ul>
-</p>"""
-        % F
+        <li><a class="stdlink" href="{
+            url_for('notes.module_table', scodoc_dept=g.scodoc_dept,
+            formation_id=formation_id)
+            }">Liste détaillée des modules de la formation</a> (debug) 
+        </li>
+        </ul>
+        </p>"""
     )
     if has_perm_change:
         H.append(
@@ -679,12 +722,13 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
 
     if current_user.has_permission(Permission.ScoImplement):
         H.append(
-            """<ul>
-        <li><a class="stdlink" href="formsemestre_createwithmodules?formation_id=%(formation_id)s&semestre_id=1">Mettre en place un nouveau semestre de formation %(acronyme)s</a>
- </li>
-
-</ul>"""
-            % F
+            f"""<ul>
+        <li><a class="stdlink" href="{
+            url_for('notes.formsemestre_createwithmodules', scodoc_dept=g.scodoc_dept,
+            formation_id=formation_id, semestre_id=1)
+        }">Mettre en place un nouveau semestre de formation %(acronyme)s</a>
+        </li>
+        </ul>"""
         )
     #   <li>(debug) <a class="stdlink" href="check_form_integrity?formation_id=%(formation_id)s">Vérifier cohérence</a></li>
 
@@ -707,7 +751,9 @@ def _ue_table_ues(
     delete_icon,
     delete_disabled_icon,
 ):
-    """Édition de programme: liste des UEs (avec leurs matières et modules)."""
+    """Édition de programme: liste des UEs (avec leurs matières et modules).
+    Pour les formations classiques (non APC/BUT)
+    """
     H = []
     cur_ue_semestre_id = None
     iue = 0
@@ -802,6 +848,7 @@ def _ue_table_ues(
                 arrow_none,
                 delete_icon,
                 delete_disabled_icon,
+                module_type=module_type,
             )
         )
     return "\n".join(H)
@@ -817,6 +864,7 @@ def _ue_table_matieres(
     arrow_none,
     delete_icon,
     delete_disabled_icon,
+    module_type=None,
 ):
     """Édition de programme: liste des matières (et leurs modules) d'une UE."""
     H = []
@@ -883,6 +931,7 @@ def _ue_table_ressources_saes(
     arrow_none,
     delete_icon,
     delete_disabled_icon,
+    module_type=None,
 ):
     """Édition de programme: liste des ressources et SAÉs d'une UE.
     (pour les parcours APC_SAE)
@@ -905,7 +954,7 @@ def _ue_table_ressources_saes(
     """
     ]
     modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
-    for titre, element_name, module_type in (
+    for titre, element_name, element_type in (
         ("Ressources", "ressource", scu.ModuleType.RESSOURCE),
         ("SAÉs", "SAÉ", scu.ModuleType.SAE),
         ("Autres modules", "xxx", None),
@@ -914,9 +963,9 @@ def _ue_table_ressources_saes(
         elements = [
             m
             for m in modules
-            if module_type == m["module_type"]
+            if element_type == m["module_type"]
             or (
-                (module_type is None)
+                (element_type is None)
                 and m["module_type"]
                 not in (scu.ModuleType.RESSOURCE, scu.ModuleType.SAE)
             )
@@ -933,6 +982,7 @@ def _ue_table_ressources_saes(
                 arrow_none,
                 delete_icon,
                 delete_disabled_icon,
+                module_type=module_type,
                 empty_list_msg="Aucune " + element_name,
                 create_element_msg="créer une " + element_name,
                 add_suppress_link=False,
@@ -1273,5 +1323,5 @@ def ue_list_semestre_ids(ue):
     Mais cela n'a pas toujours été le cas dans les programmes pédagogiques officiels,
     aussi ScoDoc laisse le choix.
     """
-    Modlist = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
-    return sorted(list(set([mod["semestre_id"] for mod in Modlist])))
+    modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
+    return sorted(list(set([mod["semestre_id"] for mod in modules])))
diff --git a/app/scodoc/sco_evaluation_edit.py b/app/scodoc/sco_evaluation_edit.py
index a310ecdd5f818ee8e4ddc49812a40177eeef48c5..36b12ca2525f8ddc9b30730b40ad1ab342c93679 100644
--- a/app/scodoc/sco_evaluation_edit.py
+++ b/app/scodoc/sco_evaluation_edit.py
@@ -39,6 +39,7 @@ from flask import request
 from app import db
 from app import log
 from app import models
+from app.models.formsemestre import FormSemestre
 import app.scodoc.sco_utils as scu
 from app.scodoc.sco_utils import ModuleType
 from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
@@ -64,11 +65,8 @@ def evaluation_create_form(
     modimpl = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0]
     mod = modimpl["module"]
     formsemestre_id = modimpl["formsemestre_id"]
-    sem_ues = db.session.query(models.UniteEns).filter(
-        models.ModuleImpl.formsemestre_id == formsemestre_id,
-        models.Module.id == models.ModuleImpl.module_id,
-        models.UniteEns.id == models.Module.ue_id,
-    )
+    sem = FormSemestre.query.get(formsemestre_id)
+    sem_ues = sem.query_ues().all()
     is_malus = mod["module_type"] == ModuleType.MALUS
     is_apc = mod["module_type"] in (ModuleType.RESSOURCE, ModuleType.SAE)
 
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index e091b90a849d0554e08fd3736f7d40bb454e2490..e9ae3f0737c308e4da5dd84150484359a4f48e39 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -263,7 +263,8 @@ div.logo-logo {
 }
 div.logo-logo img {
   margin-top: 20px;
-  width: 100px;
+  width: 55px; /* 100px */
+  padding-right: 50px;
 }
 div.sidebar-bottom {
   margin-top: 10px;
@@ -1480,7 +1481,40 @@ div.formation_ue_list {
   margin-right: 12px;
   padding-left: 5px;
 }
+div.formation_list_ues_titre {
+  padding-left: 24px;
+  padding-right: 24px;
+  font-size: 120%;
+}
 
+div.formation_list_modules {
+  border-radius: 18px;
+  margin-left: 10px;
+  margin-right: 10px;
+  margin-bottom: 10px;
+  padding-bottom: 1px;
+}
+div.formation_list_modules_titre {
+  padding-left: 24px;
+  padding-right: 24px;
+  font-weight: bold;
+  font-size: 120%;
+}
+div.formation_list_modules_RESSOURCE {
+  background-color: #f8c844;
+}
+div.formation_list_modules_SAE {
+  background-color: #c6ffab;
+}
+div.formation_list_modules_STANDARD {
+  background-color: #afafc2;
+}
+div.formation_list_modules ul.notes_module_list {
+  margin-top: 0px;
+  margin-bottom: -1px;
+  padding-top: 5px;
+  padding-bottom: 5px;
+}
 li.module_malus span.formation_module_tit {
   color: red;
   font-weight: bold;
@@ -1498,7 +1532,6 @@ div.ue_list_tit {
 
 ul.notes_ue_list {
   background-color: rgb(240,240,240);
-  font-weight: bold;
   margin-top: 4px;
   margin-right: 1em;
 }
@@ -1519,7 +1552,10 @@ span.ue_type {
   margin-left: 1.5em;
   margin-right: 1.5em;
 }
-
+ul.notes_module_list span.ue_coefs_list {
+  color: blue;
+  font-size: 70%;
+}
 div.formation_ue_list_externes {
   background-color: #98cc98;
 }
@@ -1572,7 +1608,7 @@ div#ue_list_code {
 }
 
 ul.notes_module_list {
-    list-style-type: none;
+  list-style-type: none;
 }
 
 div#ue_list_etud_validations {
diff --git a/app/static/icons/scologo_img.png b/app/static/icons/scologo_img.png
index b0871c44b810556e121e0ef4b2a0ffca5dbab65c..38e0da1a46650583f59431b38610acb25b5102d2 100644
Binary files a/app/static/icons/scologo_img.png and b/app/static/icons/scologo_img.png differ
diff --git a/app/templates/pn/form_mods.html b/app/templates/pn/form_mods.html
new file mode 100644
index 0000000000000000000000000000000000000000..830c3f67295074371cb8c2b52f80131790c6557d
--- /dev/null
+++ b/app/templates/pn/form_mods.html
@@ -0,0 +1,82 @@
+{# Édition liste modules APC (SAÉ ou ressources) #}
+
+<div class="formation_list_modules formation_list_modules_{{module_type.name}}">
+<div class="formation_list_modules_titre">{{titre}}</div>
+
+<ul class="notes_module_list">
+
+{% if not formation.ues.count() %}
+    <li class="notes_module_list"><em>aucune UE</em></li>
+{% else %}
+    {% for mod in modules %}
+        <li class="notes_module_list module_{{mod.type_name()}}">
+        <span class="notes_module_list_buts">
+        {% if editable and not loop.first %}
+            <a href="{{ url_for('notes.module_move', 
+            scodoc_dept=g.scodoc_dept, module_id=mod.id, after=0 )
+            }}" class="aud">{{arrow_up|safe}}</a>
+        {% else %}
+            {{arrow_none|safe}}
+        {% endif %}
+        {% if editable and not loop.last %}
+            <a href="{{ url_for('notes.module_move', 
+            scodoc_dept=g.scodoc_dept, module_id=mod.id, after=1 )
+            }}" class="aud">{{arrow_down|safe}}</a>
+        {% else %}
+            {{arrow_none|safe}}
+        {% endif %}
+        </span>
+        {% if editable and not mod.modimpls.count() %}
+        <a class="smallbutton" href="{{ url_for('notes.module_delete', 
+            scodoc_dept=g.scodoc_dept, module_id=mod.id)
+            }}">{{delete_icon|safe}}</a>
+        {% else %}
+            {{delete_disabled_icon|safe}}
+        {% endif %}
+
+        {% if editable %}
+        <a class="discretelink" title="Modifier le module {{mod.code}}, 
+        utilisé par {{mod.modimpls.count()}} sessions" 
+        href="{{ url_for('notes.module_edit', 
+            scodoc_dept=g.scodoc_dept, module_id=mod.id)
+            }}">
+        {% endif %}
+        <span class="formation_module_tit">{{mod.code}} {{mod.titre|default("", true)}}</span>
+        {% if editable %}
+        </a>
+        {% endif %}
+
+        {{formation.get_parcours().SESSION_NAME}} {{mod.semestre_id}}
+
+        ({{mod.heures_cours}}/{{mod.heures_td}}/{{mod.heures_tp}}, 
+        
+        Apo:<span class="{% if editable %}span_apo_edit{% endif %}"
+        data-url="edit_module_set_code_apogee" 
+        id="{{mod.id}}" 
+        data-placeholder="{{scu.APO_MISSING_CODE_STR}}">
+        {{mod.code_apogee|default("", true)}}</span>)
+
+        <span class="ue_coefs_list">
+        {% for coef in mod.ue_coefs %}
+        <span>{{coef.ue.acronyme}}:{{coef.coef}}</span>
+        {% endfor %}
+        </span>
+
+        <span class="sco_tag_edit"><form><textarea data-module_id="{{mod.id}}" 
+        class="{% if editable %}module_tag_editor{% else %}module_tag_editor_ro{% endif %}">{{mod.tags|join(', ', attribute='title')}}</textarea></form></span>
+
+        </li>
+    {% endfor %}
+
+    {% if editable %}
+        <li><a class="stdlink" href="{{
+            url_for("notes.module_create", 
+                    scodoc_dept=g.scodoc_dept, 
+                    module_type=module_type|int,
+                    )}}"
+                >{{create_element_msg}}</a>
+        </li>
+    {% endif %}
+{% endif %}
+</ul>
+</div>
\ No newline at end of file
diff --git a/app/templates/pn/form_modules_ue_coefs.html b/app/templates/pn/form_modules_ue_coefs.html
index b0116ba791c12fa45bbeedc7d45fe9fedc45130a..a93db230ba447bd88609ef70d77e5e40c1169d1e 100644
--- a/app/templates/pn/form_modules_ue_coefs.html
+++ b/app/templates/pn/form_modules_ue_coefs.html
@@ -15,13 +15,35 @@
     <h2>Formation {{formation.titre}} ({{formation.acronyme}})
         [version {{formation.version}}] code {{formation.code}}</h2>
 
+    <form onchange="change_semestre()">Semestre: 
+    <select name="semestre_idx" id="semestre_idx" >
+        {% for i in semestre_ids %}
+        <option value="{{i}}" {%if semestre_idx == i%}selected{%endif%}>{{i}}</option>
+        {% endfor %}
+    </select>
+    </form>
+
     <div class="tableau"></div>
 
     <script>
+        function change_semestre() {
+            let semestre_idx = $("#semestre_idx")[0].value;
+            let url = window.location.href.replace( /\/[\-0-9]*$/, "/" + semestre_idx);
+            window.location.href = url;
+        };
+
+
         $(function () {
-            $.getJSON("{{data_source}}", function (data) {
-                build_table(data);
-            });
+            let semestre_idx = $("#semestre_idx")[0].value;
+            if (semestre_idx > -10) {
+                let base_url = "{{data_source}}";
+                let data_url = base_url.replace( /\/[\-0-9]*$/, "/" + semestre_idx);
+                console.log("data_url=", data_url );
+                $.getJSON(data_url, function (data) {
+                    console.log("build_table")
+                    build_table(data);
+                });
+            }
         });
         function save(obj) {
             var value = obj.innerText.trim();
diff --git a/app/templates/pn/form_ues.html b/app/templates/pn/form_ues.html
new file mode 100644
index 0000000000000000000000000000000000000000..ac0486446ae2ad76d0016734fa6253829801d8ec
--- /dev/null
+++ b/app/templates/pn/form_ues.html
@@ -0,0 +1,49 @@
+{# Édition liste UEs APC #}
+<div class="formation_list_ues">
+    <div class="formation_list_ues_titre">Unités d'Enseignement (UEs)</div>
+    <ul class="notes_ue_list">
+        {% if not formation.ues.count() %}
+            <li class="notes_ue_list"><em>aucune UE</em></li>
+        {% else %}
+            {% for ue in formation.ues %}
+                <li class="notes_ue_list">
+                {% if editable and not loop.first %}
+                    <a href="{{ url_for('notes.ue_move', 
+                    scodoc_dept=g.scodoc_dept, ue_id=ue.id, after=0 )
+                    }}" class="aud">{{arrow_up|safe}}</a>
+                {% else %}
+                    {{arrow_none|safe}}
+                {% endif %}
+                {% if editable and not loop.last %}
+                    <a href="{{ url_for('notes.ue_move', 
+                    scodoc_dept=g.scodoc_dept, ue_id=ue.id, after=1 )
+                    }}" class="aud">{{arrow_down|safe}}</a>
+                {% else %}
+                    {{arrow_none|safe}}
+                {% endif %}
+                </span>
+                {% if editable and not ue.modules.count() %}
+                <a class="smallbutton" href="{{ url_for('notes.ue_delete', 
+                    scodoc_dept=g.scodoc_dept, ue_id=ue.id)
+                    }}">{{delete_icon|safe}}</a>
+                {% else %}
+                    {{delete_disabled_icon|safe}}
+                {% endif %}
+
+                <b>{{ue.acronyme}}</b> {{ue.titre}}
+
+                </li>
+            {% endfor %}
+        {% endif %}
+
+        {% if editable %}
+            <li><a class="stdlink" href="{{
+                url_for("notes.ue_create", 
+                        scodoc_dept=g.scodoc_dept, 
+                        formation_id=formation.id,
+                        )}}"
+                    >ajouter une UE</a>
+            </li>
+        {% endif %}
+    </ul>
+</div>
\ No newline at end of file
diff --git a/app/views/notes.py b/app/views/notes.py
index ac67f0e11af585ae3aa7cab7c68550d309bb2f2e..1b1c97dd93c343ccbbdabebe184e6f8d276b393f 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -429,6 +429,7 @@ sco_publish(
     "/edit_module_set_code_apogee",
     sco_edit_module.edit_module_set_code_apogee,
     Permission.ScoChangeFormation,
+    methods=["GET", "POST"],
 )
 sco_publish("/module_list", sco_edit_module.module_table, Permission.ScoView)
 sco_publish("/module_tag_search", sco_tag_module.module_tag_search, Permission.ScoView)
diff --git a/app/views/pn_modules.py b/app/views/pn_modules.py
index b9385209633fa3ade5a9cfef25868db167a91d98..0eb0d46e45ebff3d58e8537e3ac7830b3306f9bf 100644
--- a/app/views/pn_modules.py
+++ b/app/views/pn_modules.py
@@ -66,16 +66,20 @@ from app.scodoc import html_sco_header
 from app.scodoc.sco_permissions import Permission
 
 
-@bp.route("/table_modules_ue_coefs/<formation_id>")
+@bp.route("/table_modules_ue_coefs/<int:formation_id>/<semestre_idx>")
 @scodoc
 @permission_required(Permission.ScoView)
-def table_modules_ue_coefs(formation_id):
+def table_modules_ue_coefs(formation_id, semestre_idx):
     """Description JSON de la table des coefs modules/UE dans une formation"""
     _ = models.Formation.query.get_or_404(formation_id)  # check
 
-    df = moy_ue.df_load_ue_coefs(formation_id)
-    ues = models.UniteEns.query.filter_by(formation_id=formation_id).all()
-    modules = models.Module.query.filter_by(formation_id=formation_id).all()
+    df = moy_ue.df_load_ue_coefs(formation_id, semestre_idx)
+    ues = models.UniteEns.query.filter_by(
+        formation_id=formation_id, semestre_idx=semestre_idx
+    )
+    modules = models.Module.query.filter_by(
+        formation_id=formation_id, semestre_id=semestre_idx
+    )
     # Titre des modules, en ligne
     col_titres_mods = [
         {
@@ -108,7 +112,7 @@ def table_modules_ue_coefs(formation_id):
                     "x": col,
                     "y": row,
                     "style": "champs",
-                    "data": df[ue.id][mod.id] or "",
+                    "data": df[mod.id][ue.id] or "",
                     "editable": True,
                     "module_id": mod.id,
                     "ue_id": ue.id,
@@ -146,10 +150,11 @@ def set_module_ue_coef():
     return scu.json_error("ok", success=True, status=201)
 
 
-@bp.route("/edit_modules_ue_coefs/<formation_id>")
+@bp.route("/edit_modules_ue_coefs/<formation_id>", defaults={"semestre_idx": -100})
+@bp.route("/edit_modules_ue_coefs/<formation_id>/<semestre_idx>")
 @scodoc
 @permission_required(Permission.ScoChangeFormation)
-def edit_modules_ue_coefs(formation_id):
+def edit_modules_ue_coefs(formation_id, semestre_idx=None):
     """Formulaire édition grille coefs EU/modules"""
     formation = models.Formation.query.filter_by(
         formation_id=formation_id
@@ -161,9 +166,12 @@ def edit_modules_ue_coefs(formation_id):
             "notes.table_modules_ue_coefs",
             scodoc_dept=g.scodoc_dept,
             formation_id=formation_id,
+            semestre_idx=semestre_idx or "",
         ),
         data_save=url_for(
             "notes.set_module_ue_coef",
             scodoc_dept=g.scodoc_dept,
         ),
+        semestre_idx=int(semestre_idx),
+        semestre_ids=range(1, formation.get_parcours().NB_SEM + 1),
     )
diff --git a/scodoc.py b/scodoc.py
index d15c3f7dba05542bed3b715e176e0c7547d2eeb0..2a48a794364ad3e6f778d4b9ddba84f7b821d0e9 100755
--- a/scodoc.py
+++ b/scodoc.py
@@ -281,7 +281,7 @@ def list_depts(depts=""):  # list-dept
     "--name",
     is_flag=True,
     help="show database name instead of connexion string (required for "
-    "dropdb/createddb commands)",
+    "dropdb/createdb commands)",
 )
 def scodoc_database(name):  # list-dept
     """print the database connexion string"""
diff --git a/tests/unit/sco_fake_gen.py b/tests/unit/sco_fake_gen.py
index 19d6f0ecdb0f91f54fad425db993522f7a8d5dc8..371cd21dbbf3a73e16937583a2be0e3414c062f1 100644
--- a/tests/unit/sco_fake_gen.py
+++ b/tests/unit/sco_fake_gen.py
@@ -166,6 +166,7 @@ class ScoFake(object):
         is_external=None,
         code_apogee=None,
         coefficient=None,
+        semestre_idx=None,
     ):
         """Crée une UE"""
         if numero is None:
diff --git a/tests/unit/test_but_modules.py b/tests/unit/test_but_modules.py
index 74899a0aeb5e704093d330f80d35b660272111ac..0edd198d0aec31cecb538744f6e920cc88e7018d 100644
--- a/tests/unit/test_but_modules.py
+++ b/tests/unit/test_but_modules.py
@@ -1,10 +1,15 @@
 """
 Test modèles évaluations avec poids BUT
 """
+import numpy as np
+import pandas as pd
 
 from tests.unit import sco_fake_gen
 from app import db
 from app import models
+from app.comp import moy_mod
+from app.comp import moy_ue
+from app.scodoc import sco_codes_parcours
 
 """
 mapp.set_sco_dept("RT")
@@ -18,11 +23,24 @@ login_user(admin_user)
 def setup_formation_test():
     G = sco_fake_gen.ScoFake(verbose=False)
     _f = G.create_formation(
-        acronyme="F3", titre="Formation 2", titre_officiel="Titre officiel 2"
+        acronyme="F3",
+        titre="Formation 2",
+        titre_officiel="Titre officiel 2",
+        type_parcours=sco_codes_parcours.ParcoursBUT.TYPE_PARCOURS,
+    )
+    _ue1 = G.create_ue(
+        formation_id=_f["formation_id"], acronyme="UE1", titre="ue 1", semestre_idx=2
+    )
+    _ue2 = G.create_ue(
+        formation_id=_f["formation_id"], acronyme="UE2", titre="ue 2", semestre_idx=2
+    )
+    _ue3 = G.create_ue(
+        formation_id=_f["formation_id"], acronyme="UE3", titre="ue 3", semestre_idx=2
+    )
+    # une 4eme UE en dehors du semestre 2
+    _ = G.create_ue(
+        formation_id=_f["formation_id"], acronyme="UE41", titre="ue 41", semestre_idx=4
     )
-    _ue1 = G.create_ue(formation_id=_f["formation_id"], acronyme="UE1", titre="ue 1")
-    _ue2 = G.create_ue(formation_id=_f["formation_id"], acronyme="UE2", titre="ue 2")
-    _ue3 = G.create_ue(formation_id=_f["formation_id"], acronyme="UE3", titre="ue 3")
     _mat = G.create_matiere(ue_id=_ue1["ue_id"], titre="matière test")
     _mod = G.create_module(
         matiere_id=_mat["matiere_id"],
@@ -31,6 +49,7 @@ def setup_formation_test():
         titre="module test",
         ue_id=_ue1["ue_id"],
         formation_id=_f["formation_id"],
+        semestre_id=2,
     )
     return G, _f["id"], _ue1["id"], _ue2["id"], _ue3["id"], _mod["id"]
 
@@ -66,7 +85,9 @@ def test_evaluation_poids(test_client):
     e1.set_ue_poids(ue1, p1)
     db.session.commit()
     assert e1.get_ue_poids_dict()[ue1_id] == p1
-    ues = models.UniteEns.query.filter_by(formation_id=formation_id).all()
+    ues = models.UniteEns.query.filter_by(
+        formation_id=formation_id, semestre_idx=2
+    ).all()
     poids = [1.0, 2.0, 3.0]
     for (ue, p) in zip(ues, poids):
         e1.set_ue_poids(ue, p)
@@ -109,3 +130,60 @@ def test_modules_coefs(test_client):
     mod.set_ue_coef(ue2, 0.0)
     db.session.commit()
     assert len(mod.ue_coefs) == 0
+
+
+def test_modules_conformity(test_client):
+    """Vérification coefficients module<->UE vs poids des évaluations"""
+    G, formation_id, ue1_id, ue2_id, ue3_id, module_id = setup_formation_test()
+    ue1 = models.UniteEns.query.get(ue1_id)
+    ue2 = models.UniteEns.query.get(ue2_id)
+    ue3 = models.UniteEns.query.get(ue3_id)
+    mod = models.Module.query.get(module_id)
+    nb_ues = 3  # 3 UEs dans ce test
+    nb_mods = 1  # 1 seul module
+    # Coef du module vers les UE
+    c1, c2, c3 = 1.0, 2.0, 3.0
+    coefs_mod = {ue1.id: c1, ue2.id: c2, ue3.id: c3}
+    mod.set_ue_coef_dict(coefs_mod)
+    assert mod.get_ue_coef_dict() == coefs_mod
+    # Mise en place:
+    sem = G.create_formsemestre(
+        formation_id=formation_id,
+        semestre_id=2,
+        date_debut="01/01/2021",
+        date_fin="30/06/2021",
+    )
+    mi = G.create_moduleimpl(
+        module_id=module_id,
+        formsemestre_id=sem["formsemestre_id"],
+    )
+    moduleimpl_id = mi["id"]
+    modimpl = models.ModuleImpl.query.get(moduleimpl_id)
+    assert modimpl.formsemestre.formation.get_parcours().APC_SAE  # BUT
+    # Check ModuleImpl
+    ues = modimpl.formsemestre.query_ues().all()
+    assert len(ues) == 3
+    #
+    _e1 = G.create_evaluation(
+        moduleimpl_id=moduleimpl_id,
+        jour="01/01/2021",
+        description="evaluation 1",
+        coefficient=0,
+    )
+    evaluation_id = _e1["evaluation_id"]
+    nb_evals = 1  # 1 seule evaluation pour l'instant
+    p1, p2, p3 = 1.0, 2.0, 0.0  # poids de l'éval vers les UE 1, 2 et 3
+    evaluation = models.Evaluation.query.get(evaluation_id)
+    evaluation.set_ue_poids_dict({ue1.id: p1, ue2.id: p2})
+    assert evaluation.get_ue_poids_dict() == {ue1.id: p1, ue2.id: p2}
+    # On n'est pas conforme car p3 est nul alors que c3 est non nul
+    modules_coefficients = moy_ue.df_load_ue_coefs(formation_id)
+    assert isinstance(modules_coefficients, pd.DataFrame)
+    assert modules_coefficients.shape == (nb_ues, nb_mods)
+    evals_poids = moy_mod.df_load_evaluations_poids(moduleimpl_id)
+    assert isinstance(evals_poids, pd.DataFrame)
+    assert all(evals_poids.dtypes == np.float64)
+    assert evals_poids.shape == (nb_evals, nb_ues)
+    assert not moy_mod.check_moduleimpl_conformity(
+        modimpl, evals_poids, modules_coefficients
+    )