diff --git a/README.md b/README.md
index 45743c4149fb428ee661c6924287f7fe065c1d8c..2a77b25e219e35c681850f40873111152cd92286 100644
--- a/README.md
+++ b/README.md
@@ -130,6 +130,25 @@ un utilisateur:
 **Attention:** les tests unitaires **effacent** complètement le contenu de la
 base de données (tous les départements, et les utilisateurs) avant de commencer !
 
+#### Modification du schéma de la base
+
+On utilise SQLAlchemy avec Alembic et Flask-Migrate.
+
+    flask db migrate -m "ScoDoc 9.0.4" # ajuster le message !
+    flask db upgrade
+
+Ne pas oublier de commiter les migrations (`git add migrations` ...).
+
+Mémo pour développeurs: séquence re-création d'une base:
+
+    dropdb SCODOC_DEV
+    tools/create_database.sh SCODOC_DEV # créé base SQL
+    flask db upgrade # créé les tables à partir des migrations
+    flask sco-db-init # ajoute au besoin les constantes (todo: mettre en migration 0)
+    
+    # puis imports:
+    flask import-scodoc7-users
+    flask import-scodoc7-dept STID SCOSTID
 
 # Paquet debian 11
 
diff --git a/app/__init__.py b/app/__init__.py
index 001bbc241b021be7f747d4f6b5648f4d2caba4ce..fa9bb0c03743e64d496179ec72faeb9ba963e473 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -25,7 +25,7 @@ from config import DevConfig
 import sco_version
 
 db = SQLAlchemy()
-migrate = Migrate()
+migrate = Migrate(compare_type=True)
 login = LoginManager()
 login.login_view = "auth.login"
 login.login_message = "Please log in to access this page."
@@ -140,7 +140,9 @@ def set_sco_dept(scodoc_dept: str):
 
 
 def user_db_init():
-    """Initialize the users database."""
+    """Initialize the users database.
+    Check that basic roles and admin user exist.
+    """
     from app.auth.models import User, Role
 
     current_app.logger.info("Init User's db")
@@ -167,8 +169,8 @@ def user_db_init():
             )
 
 
-def sco_db_init():
-    """Initialize Sco database"""
+def sco_db_insert_constants():
+    """Initialize Sco database: insert some constants (modalités, ...)."""
     from app import models
 
     current_app.logger.info("Init Sco db")
@@ -176,25 +178,25 @@ def sco_db_init():
     models.NotesFormModalite.insert_modalites()
 
 
-def initialize_scodoc_database(erase=False):
-    """Initialize the database.
+def initialize_scodoc_database(erase=False, create_all=False):
+    """Initialize the database for unit tests
     Starts from an existing database and create all necessary
     SQL tables and functions.
     If erase is True, _erase_ all database content.
     """
     from app import models
 
-    # - our specific functions and sequences, not generated by SQLAlchemy
-    models.create_database_functions()
     # - ERASE (the truncation sql function has been defined above)
     if erase:
         truncate_database()
     # - Create all tables
-    db.create_all()
+    if create_all:
+        # managed by migrations, except for TESTS
+        db.create_all()
     # - Insert initial roles and create super-admin user
     user_db_init()
     # - Insert some constant values (modalites, ...)
-    sco_db_init()
+    sco_db_insert_constants()
     # - Flush cache
     clear_scodoc_cache()
 
diff --git a/app/models/formations.py b/app/models/formations.py
index 81828b824b1235437354284c09b495a4c109331b..756879d5939c202a7edaf8eb9034d0d2b5a29bd6 100644
--- a/app/models/formations.py
+++ b/app/models/formations.py
@@ -87,7 +87,8 @@ class NotesModule(db.Model):
     module_id = db.synonym("id")
     titre = db.Column(db.Text())
     abbrev = db.Column(db.Text())  # nom court
-    code = db.Column(db.String(SHORT_STR_LEN), nullable=False)
+    # certains départements ont des codes infiniment longs: donc Text !
+    code = db.Column(db.Text(), nullable=False)
     heures_cours = db.Column(db.Float)
     heures_td = db.Column(db.Float)
     heures_tp = db.Column(db.Float)
diff --git a/app/models/raw_sql_init.py b/app/models/raw_sql_init.py
index e10f8d14254855f2b39c9ef3d9ffbdf8d1787cd0..6cfde9d9a7749d7040a4e1e0d9e3c68c68715646 100644
--- a/app/models/raw_sql_init.py
+++ b/app/models/raw_sql_init.py
@@ -8,8 +8,12 @@ using raw SQL
 from app import db
 
 
-def create_database_functions():
-    """Create specific SQL functions and sequences"""
+def create_database_functions():  # XXX obsolete
+    """Create specific SQL functions and sequences
+
+    XXX Obsolete: cette fonction est dans la première migration 9.0.3
+    Flask-Migrate fait maintenant (dans les versions >= 9.0.4) ce travail.
+    """
     # Important: toujours utiliser IF NOT EXISTS
     # car cette fonction peut être appelée plusieurs fois sur la même db
     db.session.execute(
diff --git a/migrations/README b/migrations/README
new file mode 100755
index 0000000000000000000000000000000000000000..0e048441597444a7e2850d6d7c4ce15550f79bda
--- /dev/null
+++ b/migrations/README
@@ -0,0 +1 @@
+Single-database configuration for Flask.
diff --git a/migrations/alembic.ini b/migrations/alembic.ini
new file mode 100644
index 0000000000000000000000000000000000000000..ec9d45c26a6bb54e833fd4e6ce2de29343894f4b
--- /dev/null
+++ b/migrations/alembic.ini
@@ -0,0 +1,50 @@
+# A generic, single database configuration.
+
+[alembic]
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic,flask_migrate
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[logger_flask_migrate]
+level = INFO
+handlers =
+qualname = flask_migrate
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/migrations/env.py b/migrations/env.py
new file mode 100755
index 0000000000000000000000000000000000000000..68feded2a040005310d770ac7136b2e4ff8a6312
--- /dev/null
+++ b/migrations/env.py
@@ -0,0 +1,91 @@
+from __future__ import with_statement
+
+import logging
+from logging.config import fileConfig
+
+from flask import current_app
+
+from alembic import context
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+logger = logging.getLogger('alembic.env')
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+config.set_main_option(
+    'sqlalchemy.url',
+    str(current_app.extensions['migrate'].db.get_engine().url).replace(
+        '%', '%%'))
+target_metadata = current_app.extensions['migrate'].db.metadata
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def run_migrations_offline():
+    """Run migrations in 'offline' mode.
+
+    This configures the context with just a URL
+    and not an Engine, though an Engine is acceptable
+    here as well.  By skipping the Engine creation
+    we don't even need a DBAPI to be available.
+
+    Calls to context.execute() here emit the given string to the
+    script output.
+
+    """
+    url = config.get_main_option("sqlalchemy.url")
+    context.configure(
+        url=url, target_metadata=target_metadata, literal_binds=True
+    )
+
+    with context.begin_transaction():
+        context.run_migrations()
+
+
+def run_migrations_online():
+    """Run migrations in 'online' mode.
+
+    In this scenario we need to create an Engine
+    and associate a connection with the context.
+
+    """
+
+    # this callback is used to prevent an auto-migration from being generated
+    # when there are no changes to the schema
+    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
+    def process_revision_directives(context, revision, directives):
+        if getattr(config.cmd_opts, 'autogenerate', False):
+            script = directives[0]
+            if script.upgrade_ops.is_empty():
+                directives[:] = []
+                logger.info('No changes in schema detected.')
+
+    connectable = current_app.extensions['migrate'].db.get_engine()
+
+    with connectable.connect() as connection:
+        context.configure(
+            connection=connection,
+            target_metadata=target_metadata,
+            process_revision_directives=process_revision_directives,
+            **current_app.extensions['migrate'].configure_args
+        )
+
+        with context.begin_transaction():
+            context.run_migrations()
+
+
+if context.is_offline_mode():
+    run_migrations_offline()
+else:
+    run_migrations_online()
diff --git a/migrations/script.py.mako b/migrations/script.py.mako
new file mode 100755
index 0000000000000000000000000000000000000000..2c0156303a8df3ffdc9de87765bf801bf6bea4a5
--- /dev/null
+++ b/migrations/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+    ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+    ${downgrades if downgrades else "pass"}
diff --git a/migrations/versions/6b071b7947e5_scodoc_9_0_4_code_module_en_text.py b/migrations/versions/6b071b7947e5_scodoc_9_0_4_code_module_en_text.py
new file mode 100644
index 0000000000000000000000000000000000000000..c16e858206ea4c477123189320d7c577f9abcb55
--- /dev/null
+++ b/migrations/versions/6b071b7947e5_scodoc_9_0_4_code_module_en_text.py
@@ -0,0 +1,34 @@
+"""ScoDoc 9.0.4: code module en Text
+
+Revision ID: 6b071b7947e5
+Revises: 993ce4a01d57
+Create Date: 2021-08-27 16:00:27.322153
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '6b071b7947e5'
+down_revision = '993ce4a01d57'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.alter_column('notes_modules', 'code',
+               existing_type=sa.VARCHAR(length=32),
+               type_=sa.Text(),
+               existing_nullable=False)
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.alter_column('notes_modules', 'code',
+               existing_type=sa.Text(),
+               type_=sa.VARCHAR(length=32),
+               existing_nullable=False)
+    # ### end Alembic commands ###
diff --git a/migrations/versions/993ce4a01d57_scodoc_9_0_3.py b/migrations/versions/993ce4a01d57_scodoc_9_0_3.py
new file mode 100644
index 0000000000000000000000000000000000000000..c1e87c93aaa5716dd4d793708cb2b46e273dc66c
--- /dev/null
+++ b/migrations/versions/993ce4a01d57_scodoc_9_0_3.py
@@ -0,0 +1,1199 @@
+"""ScoDoc 9.0.3
+
+Revision ID: 993ce4a01d57
+Revises: 
+Create Date: 2021-08-27 11:17:55.205910
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = "993ce4a01d57"
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # Added by Emmanuel: ScoDoc SQl functions creation
+    op.execute(
+        """
+CREATE SEQUENCE IF NOT EXISTS notes_idgen_fcod;
+CREATE OR REPLACE FUNCTION notes_newid_fcod() RETURNS TEXT
+    AS $$ SELECT 'FCOD' || to_char(nextval('notes_idgen_fcod'), 'FM999999999');  $$
+    LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION notes_newid_ucod() RETURNS TEXT
+    AS $$ SELECT 'UCOD' || to_char(nextval('notes_idgen_fcod'), 'FM999999999');  $$
+    LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION truncate_tables(username IN VARCHAR) RETURNS void AS $$
+DECLARE
+    statements CURSOR FOR
+        SELECT tablename FROM pg_tables
+        WHERE tableowner = username AND schemaname = 'public'
+        AND tablename <> 'notes_semestres'
+        AND tablename <> 'notes_form_modalites';
+BEGIN
+    FOR stmt IN statements LOOP
+        EXECUTE 'TRUNCATE TABLE ' || quote_ident(stmt.tablename) || ' CASCADE;';
+    END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+-- Fonction pour anonymisation:
+-- inspirée par https://www.simononsoftware.com/random-string-in-postgresql/
+CREATE OR REPLACE FUNCTION random_text_md5( integer ) returns text
+    LANGUAGE SQL
+    AS $$
+    select upper( substring( (SELECT string_agg(md5(random()::TEXT), '')
+    FROM generate_series(
+        1,
+        CEIL($1 / 32.)::integer)
+    ), 1, $1) );
+    $$;
+    """
+    )
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table(
+        "departement",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("acronym", sa.String(length=32), nullable=False),
+        sa.Column("description", sa.Text(), nullable=True),
+        sa.Column(
+            "date_creation",
+            sa.DateTime(timezone=True),
+            server_default=sa.text("now()"),
+            nullable=True,
+        ),
+        sa.Column("visible", sa.Boolean(), server_default="true", nullable=False),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_index(
+        op.f("ix_departement_acronym"), "departement", ["acronym"], unique=False
+    )
+    op.create_table(
+        "etud_annotations",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column(
+            "date",
+            sa.DateTime(timezone=True),
+            server_default=sa.text("now()"),
+            nullable=True,
+        ),
+        sa.Column("etudid", sa.Integer(), nullable=True),
+        sa.Column("author", sa.Text(), nullable=True),
+        sa.Column("comment", sa.Text(), nullable=True),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_table(
+        "itemsuivi_tags",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("title", sa.Text(), nullable=False),
+        sa.PrimaryKeyConstraint("id"),
+        sa.UniqueConstraint("title"),
+    )
+    op.create_table(
+        "notes_form_modalites",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("modalite", sa.String(length=32), server_default="FI", nullable=True),
+        sa.Column("titre", sa.Text(), nullable=True),
+        sa.Column("numero", sa.Integer(), nullable=True),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_index(
+        op.f("ix_notes_form_modalites_modalite"),
+        "notes_form_modalites",
+        ["modalite"],
+        unique=True,
+    )
+    op.create_table(
+        "role",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("name", sa.String(length=64), nullable=True),
+        sa.Column("default", sa.Boolean(), nullable=True),
+        sa.Column("permissions", sa.BigInteger(), nullable=True),
+        sa.PrimaryKeyConstraint("id"),
+        sa.UniqueConstraint("name"),
+    )
+    op.create_index(op.f("ix_role_default"), "role", ["default"], unique=False)
+    op.create_table(
+        "scolog",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column(
+            "date",
+            sa.DateTime(timezone=True),
+            server_default=sa.text("now()"),
+            nullable=True,
+        ),
+        sa.Column("method", sa.Text(), nullable=True),
+        sa.Column("msg", sa.Text(), nullable=True),
+        sa.Column("etudid", sa.Integer(), nullable=True),
+        sa.Column("authenticated_user", sa.Text(), nullable=True),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_table(
+        "user",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("user_name", sa.String(length=64), nullable=True),
+        sa.Column("email", sa.String(length=120), nullable=True),
+        sa.Column("nom", sa.String(length=64), nullable=True),
+        sa.Column("prenom", sa.String(length=64), nullable=True),
+        sa.Column("dept", sa.String(length=32), nullable=True),
+        sa.Column("active", sa.Boolean(), nullable=True),
+        sa.Column("password_hash", sa.String(length=128), nullable=True),
+        sa.Column("password_scodoc7", sa.String(length=42), nullable=True),
+        sa.Column("last_seen", sa.DateTime(), nullable=True),
+        sa.Column("date_modif_passwd", sa.DateTime(), nullable=True),
+        sa.Column("date_created", sa.DateTime(), nullable=True),
+        sa.Column("date_expiration", sa.DateTime(), nullable=True),
+        sa.Column("passwd_temp", sa.Boolean(), nullable=True),
+        sa.Column("token", sa.String(length=32), nullable=True),
+        sa.Column("token_expiration", sa.DateTime(), nullable=True),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_index(op.f("ix_user_active"), "user", ["active"], unique=False)
+    op.create_index(op.f("ix_user_dept"), "user", ["dept"], unique=False)
+    op.create_index(op.f("ix_user_token"), "user", ["token"], unique=True)
+    op.create_index(op.f("ix_user_user_name"), "user", ["user_name"], unique=True)
+    op.create_table(
+        "entreprises",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("dept_id", sa.Integer(), nullable=True),
+        sa.Column("nom", sa.Text(), nullable=True),
+        sa.Column("adresse", sa.Text(), nullable=True),
+        sa.Column("ville", sa.Text(), nullable=True),
+        sa.Column("codepostal", sa.Text(), nullable=True),
+        sa.Column("pays", sa.Text(), nullable=True),
+        sa.Column("contact_origine", sa.Text(), nullable=True),
+        sa.Column("secteur", sa.Text(), nullable=True),
+        sa.Column("note", sa.Text(), nullable=True),
+        sa.Column("privee", sa.Text(), nullable=True),
+        sa.Column("localisation", sa.Text(), nullable=True),
+        sa.Column("qualite_relation", sa.Integer(), nullable=True),
+        sa.Column("plus10salaries", sa.Boolean(), nullable=True),
+        sa.Column(
+            "date_creation",
+            sa.DateTime(timezone=True),
+            server_default=sa.text("now()"),
+            nullable=True,
+        ),
+        sa.ForeignKeyConstraint(
+            ["dept_id"],
+            ["departement.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_index(
+        op.f("ix_entreprises_dept_id"), "entreprises", ["dept_id"], unique=False
+    )
+    op.create_table(
+        "identite",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("dept_id", sa.Integer(), nullable=True),
+        sa.Column("nom", sa.Text(), nullable=True),
+        sa.Column("prenom", sa.Text(), nullable=True),
+        sa.Column("nom_usuel", sa.Text(), nullable=True),
+        sa.Column("civilite", sa.String(length=1), nullable=False),
+        sa.Column("date_naissance", sa.Date(), nullable=True),
+        sa.Column("lieu_naissance", sa.Text(), nullable=True),
+        sa.Column("dept_naissance", sa.Text(), nullable=True),
+        sa.Column("nationalite", sa.Text(), nullable=True),
+        sa.Column("statut", sa.Text(), nullable=True),
+        sa.Column("boursier", sa.Boolean(), nullable=True),
+        sa.Column("photo_filename", sa.Text(), nullable=True),
+        sa.Column("code_nip", sa.Text(), nullable=True),
+        sa.Column("code_ine", sa.Text(), nullable=True),
+        sa.CheckConstraint("civilite IN ('M', 'F', 'X')"),
+        sa.ForeignKeyConstraint(
+            ["dept_id"],
+            ["departement.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_index(op.f("ix_identite_dept_id"), "identite", ["dept_id"], unique=False)
+    op.create_table(
+        "notes_formations",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("dept_id", sa.Integer(), nullable=True),
+        sa.Column("acronyme", sa.Text(), nullable=False),
+        sa.Column("titre", sa.Text(), nullable=False),
+        sa.Column("titre_officiel", sa.Text(), nullable=False),
+        sa.Column("version", sa.Integer(), server_default="1", nullable=True),
+        sa.Column(
+            "formation_code",
+            sa.String(length=32),
+            server_default=sa.text("notes_newid_fcod()"),
+            nullable=False,
+        ),
+        sa.Column("type_parcours", sa.Integer(), server_default="0", nullable=True),
+        sa.Column("code_specialite", sa.String(length=32), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["dept_id"],
+            ["departement.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+        sa.UniqueConstraint("acronyme", "titre", "version"),
+    )
+    op.create_index(
+        op.f("ix_notes_formations_dept_id"),
+        "notes_formations",
+        ["dept_id"],
+        unique=False,
+    )
+    op.create_table(
+        "notes_semset",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("dept_id", sa.Integer(), nullable=True),
+        sa.Column("title", sa.Text(), nullable=True),
+        sa.Column("annee_scolaire", sa.Integer(), nullable=True),
+        sa.Column("sem_id", sa.Integer(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["dept_id"],
+            ["departement.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_table(
+        "notes_tags",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("dept_id", sa.Integer(), nullable=True),
+        sa.Column("title", sa.Text(), nullable=False),
+        sa.ForeignKeyConstraint(
+            ["dept_id"],
+            ["departement.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+        sa.UniqueConstraint("title", "dept_id"),
+    )
+    op.create_index(
+        op.f("ix_notes_tags_dept_id"), "notes_tags", ["dept_id"], unique=False
+    )
+    op.create_table(
+        "scolar_news",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("dept_id", sa.Integer(), nullable=True),
+        sa.Column(
+            "date",
+            sa.DateTime(timezone=True),
+            server_default=sa.text("now()"),
+            nullable=True,
+        ),
+        sa.Column("authenticated_user", sa.Text(), nullable=True),
+        sa.Column("type", sa.String(length=32), nullable=True),
+        sa.Column("object", sa.Integer(), nullable=True),
+        sa.Column("text", sa.Text(), nullable=True),
+        sa.Column("url", sa.Text(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["dept_id"],
+            ["departement.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_index(
+        op.f("ix_scolar_news_dept_id"), "scolar_news", ["dept_id"], unique=False
+    )
+    op.create_table(
+        "user_role",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("user_id", sa.Integer(), nullable=True),
+        sa.Column("role_id", sa.Integer(), nullable=True),
+        sa.Column("dept", sa.String(length=64), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["role_id"],
+            ["role.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["user_id"],
+            ["user.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_table(
+        "admissions",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("etudid", sa.Integer(), nullable=True),
+        sa.Column("annee", sa.Integer(), nullable=True),
+        sa.Column("bac", sa.Text(), nullable=True),
+        sa.Column("specialite", sa.Text(), nullable=True),
+        sa.Column("annee_bac", sa.Integer(), nullable=True),
+        sa.Column("math", sa.Text(), nullable=True),
+        sa.Column("physique", sa.Float(), nullable=True),
+        sa.Column("anglais", sa.Float(), nullable=True),
+        sa.Column("francais", sa.Float(), nullable=True),
+        sa.Column("rang", sa.Integer(), nullable=True),
+        sa.Column("qualite", sa.Float(), nullable=True),
+        sa.Column("rapporteur", sa.Text(), nullable=True),
+        sa.Column("decision", sa.Text(), nullable=True),
+        sa.Column("score", sa.Float(), nullable=True),
+        sa.Column("commentaire", sa.Text(), nullable=True),
+        sa.Column("nomlycee", sa.Text(), nullable=True),
+        sa.Column("villelycee", sa.Text(), nullable=True),
+        sa.Column("codepostallycee", sa.Text(), nullable=True),
+        sa.Column("codelycee", sa.Text(), nullable=True),
+        sa.Column("type_admission", sa.Text(), nullable=True),
+        sa.Column("boursier_prec", sa.Boolean(), nullable=True),
+        sa.Column("classement", sa.Integer(), nullable=True),
+        sa.Column("apb_groupe", sa.Text(), nullable=True),
+        sa.Column("apb_classement_gr", sa.Integer(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["etudid"],
+            ["identite.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_table(
+        "adresse",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("etudid", sa.Integer(), nullable=True),
+        sa.Column("email", sa.Text(), nullable=True),
+        sa.Column("emailperso", sa.Text(), nullable=True),
+        sa.Column("domicile", sa.Text(), nullable=True),
+        sa.Column("codepostaldomicile", sa.Text(), nullable=True),
+        sa.Column("villedomicile", sa.Text(), nullable=True),
+        sa.Column("paysdomicile", sa.Text(), nullable=True),
+        sa.Column("telephone", sa.Text(), nullable=True),
+        sa.Column("telephonemobile", sa.Text(), nullable=True),
+        sa.Column("fax", sa.Text(), nullable=True),
+        sa.Column("typeadresse", sa.Text(), server_default="domicile", nullable=False),
+        sa.Column("description", sa.Text(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["etudid"],
+            ["identite.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_table(
+        "billet_absence",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("etudid", sa.Integer(), nullable=True),
+        sa.Column("abs_begin", sa.DateTime(timezone=True), nullable=True),
+        sa.Column("abs_end", sa.DateTime(timezone=True), nullable=True),
+        sa.Column("description", sa.Text(), nullable=True),
+        sa.Column("etat", sa.Boolean(), server_default="false", nullable=True),
+        sa.Column(
+            "entry_date",
+            sa.DateTime(timezone=True),
+            server_default=sa.text("now()"),
+            nullable=True,
+        ),
+        sa.Column("justified", sa.Boolean(), server_default="false", nullable=True),
+        sa.ForeignKeyConstraint(
+            ["etudid"],
+            ["identite.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_index(
+        op.f("ix_billet_absence_etudid"), "billet_absence", ["etudid"], unique=False
+    )
+    op.create_table(
+        "entreprise_correspondant",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("entreprise_id", sa.Integer(), nullable=True),
+        sa.Column("nom", sa.Text(), nullable=True),
+        sa.Column("prenom", sa.Text(), nullable=True),
+        sa.Column("civilite", sa.Text(), nullable=True),
+        sa.Column("fonction", sa.Text(), nullable=True),
+        sa.Column("phone1", sa.Text(), nullable=True),
+        sa.Column("phone2", sa.Text(), nullable=True),
+        sa.Column("mobile", sa.Text(), nullable=True),
+        sa.Column("mail1", sa.Text(), nullable=True),
+        sa.Column("mail2", sa.Text(), nullable=True),
+        sa.Column("fax", sa.Text(), nullable=True),
+        sa.Column("note", sa.Text(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["entreprise_id"],
+            ["entreprises.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_table(
+        "itemsuivi",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("etudid", sa.Integer(), nullable=True),
+        sa.Column(
+            "item_date",
+            sa.DateTime(timezone=True),
+            server_default=sa.text("now()"),
+            nullable=True,
+        ),
+        sa.Column("situation", sa.Text(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["etudid"],
+            ["identite.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_table(
+        "notes_formsemestre",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("dept_id", sa.Integer(), nullable=True),
+        sa.Column("formation_id", sa.Integer(), nullable=True),
+        sa.Column("semestre_id", sa.Integer(), server_default="1", nullable=False),
+        sa.Column("titre", sa.Text(), nullable=True),
+        sa.Column("date_debut", sa.Date(), nullable=True),
+        sa.Column("date_fin", sa.Date(), nullable=True),
+        sa.Column("etat", sa.Boolean(), server_default="true", nullable=False),
+        sa.Column("modalite", sa.String(length=32), nullable=True),
+        sa.Column(
+            "gestion_compensation", sa.Boolean(), server_default="false", nullable=False
+        ),
+        sa.Column("bul_hide_xml", sa.Boolean(), server_default="false", nullable=False),
+        sa.Column(
+            "gestion_semestrielle", sa.Boolean(), server_default="false", nullable=False
+        ),
+        sa.Column(
+            "bul_bgcolor", sa.String(length=32), server_default="white", nullable=True
+        ),
+        sa.Column(
+            "resp_can_edit", sa.Boolean(), server_default="false", nullable=False
+        ),
+        sa.Column(
+            "resp_can_change_ens", sa.Boolean(), server_default="true", nullable=False
+        ),
+        sa.Column(
+            "ens_can_edit_eval", sa.Boolean(), server_default="False", nullable=False
+        ),
+        sa.Column("elt_sem_apo", sa.Text(), nullable=True),
+        sa.Column("elt_annee_apo", sa.Text(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["dept_id"],
+            ["departement.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["formation_id"],
+            ["notes_formations.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["modalite"],
+            ["notes_form_modalites.modalite"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_index(
+        op.f("ix_notes_formsemestre_dept_id"),
+        "notes_formsemestre",
+        ["dept_id"],
+        unique=False,
+    )
+    op.create_table(
+        "notes_notes_log",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("etudid", sa.Integer(), nullable=True),
+        sa.Column("evaluation_id", sa.Integer(), nullable=True),
+        sa.Column("value", sa.Float(), nullable=True),
+        sa.Column("comment", sa.Text(), nullable=True),
+        sa.Column(
+            "date",
+            sa.DateTime(timezone=True),
+            server_default=sa.text("now()"),
+            nullable=True,
+        ),
+        sa.Column("uid", sa.Integer(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["etudid"],
+            ["identite.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["uid"],
+            ["user.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_index(
+        op.f("ix_notes_notes_log_evaluation_id"),
+        "notes_notes_log",
+        ["evaluation_id"],
+        unique=False,
+    )
+    op.create_table(
+        "notes_ue",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("formation_id", sa.Integer(), nullable=True),
+        sa.Column("acronyme", sa.Text(), nullable=False),
+        sa.Column("numero", sa.Integer(), nullable=True),
+        sa.Column("titre", sa.Text(), nullable=True),
+        sa.Column("type", sa.Integer(), server_default="0", nullable=True),
+        sa.Column(
+            "ue_code",
+            sa.String(length=32),
+            server_default=sa.text("notes_newid_ucod()"),
+            nullable=False,
+        ),
+        sa.Column("ects", sa.Float(), nullable=True),
+        sa.Column("is_external", sa.Boolean(), server_default="false", nullable=True),
+        sa.Column("code_apogee", sa.String(length=16), nullable=True),
+        sa.Column("coefficient", sa.Float(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["formation_id"],
+            ["notes_formations.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_table(
+        "absences_notifications",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("etudid", sa.Integer(), nullable=True),
+        sa.Column(
+            "notification_date",
+            sa.DateTime(timezone=True),
+            server_default=sa.text("now()"),
+            nullable=True,
+        ),
+        sa.Column("email", sa.Text(), nullable=True),
+        sa.Column("nbabs", sa.Integer(), nullable=True),
+        sa.Column("nbabsjust", sa.Integer(), nullable=True),
+        sa.Column("formsemestre_id", sa.Integer(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["etudid"],
+            ["identite.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["formsemestre_id"],
+            ["notes_formsemestre.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_table(
+        "entreprise_contact",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("date", sa.DateTime(timezone=True), nullable=True),
+        sa.Column("type_contact", sa.Text(), nullable=True),
+        sa.Column("entreprise_id", sa.Integer(), nullable=True),
+        sa.Column("entreprise_corresp_id", sa.Integer(), nullable=True),
+        sa.Column("etudid", sa.Integer(), nullable=True),
+        sa.Column("description", sa.Text(), nullable=True),
+        sa.Column("enseignant", sa.Text(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["entreprise_corresp_id"],
+            ["entreprise_correspondant.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["entreprise_id"],
+            ["entreprises.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_table(
+        "itemsuivi_tags_assoc",
+        sa.Column("tag_id", sa.Integer(), nullable=True),
+        sa.Column("itemsuivi_id", sa.Integer(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["itemsuivi_id"],
+            ["itemsuivi.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["tag_id"],
+            ["itemsuivi_tags.id"],
+        ),
+    )
+    op.create_table(
+        "notes_appreciations",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column(
+            "date",
+            sa.DateTime(timezone=True),
+            server_default=sa.text("now()"),
+            nullable=True,
+        ),
+        sa.Column("etudid", sa.Integer(), nullable=True),
+        sa.Column("formsemestre_id", sa.Integer(), nullable=True),
+        sa.Column("author", sa.Text(), nullable=True),
+        sa.Column("comment", sa.Text(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["etudid"],
+            ["identite.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["formsemestre_id"],
+            ["notes_formsemestre.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_index(
+        op.f("ix_notes_appreciations_etudid"),
+        "notes_appreciations",
+        ["etudid"],
+        unique=False,
+    )
+    op.create_table(
+        "notes_formsemestre_custommenu",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("formsemestre_id", sa.Integer(), nullable=True),
+        sa.Column("title", sa.Text(), nullable=True),
+        sa.Column("url", sa.Text(), nullable=True),
+        sa.Column("idx", sa.Integer(), server_default="0", nullable=True),
+        sa.ForeignKeyConstraint(
+            ["formsemestre_id"],
+            ["notes_formsemestre.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_table(
+        "notes_formsemestre_etapes",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("formsemestre_id", sa.Integer(), nullable=True),
+        sa.Column("etape_apo", sa.String(length=16), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["formsemestre_id"],
+            ["notes_formsemestre.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_table(
+        "notes_formsemestre_inscription",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("etudid", sa.Integer(), nullable=True),
+        sa.Column("formsemestre_id", sa.Integer(), nullable=True),
+        sa.Column("etat", sa.String(length=16), nullable=True),
+        sa.Column("etape", sa.String(length=16), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["etudid"],
+            ["identite.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["formsemestre_id"],
+            ["notes_formsemestre.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+        sa.UniqueConstraint("formsemestre_id", "etudid"),
+    )
+    op.create_table(
+        "notes_formsemestre_responsables",
+        sa.Column("formsemestre_id", sa.Integer(), nullable=True),
+        sa.Column("responsable_id", sa.Integer(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["formsemestre_id"],
+            ["notes_formsemestre.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["responsable_id"],
+            ["user.id"],
+        ),
+    )
+    op.create_table(
+        "notes_formsemestre_ue_computation_expr",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("formsemestre_id", sa.Integer(), nullable=True),
+        sa.Column("ue_id", sa.Integer(), nullable=True),
+        sa.Column("computation_expr", sa.Text(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["formsemestre_id"],
+            ["notes_formsemestre.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["ue_id"],
+            ["notes_ue.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+        sa.UniqueConstraint("formsemestre_id", "ue_id"),
+    )
+    op.create_table(
+        "notes_formsemestre_uecoef",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("formsemestre_id", sa.Integer(), nullable=True),
+        sa.Column("ue_id", sa.Integer(), nullable=True),
+        sa.Column("coefficient", sa.Float(), nullable=False),
+        sa.ForeignKeyConstraint(
+            ["formsemestre_id"],
+            ["notes_formsemestre.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["ue_id"],
+            ["notes_ue.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+        sa.UniqueConstraint("formsemestre_id", "ue_id"),
+    )
+    op.create_table(
+        "notes_matieres",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("ue_id", sa.Integer(), nullable=True),
+        sa.Column("titre", sa.Text(), nullable=True),
+        sa.Column("numero", sa.Integer(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["ue_id"],
+            ["notes_ue.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+        sa.UniqueConstraint("ue_id", "titre"),
+    )
+    op.create_table(
+        "notes_semset_formsemestre",
+        sa.Column("formsemestre_id", sa.Integer(), nullable=True),
+        sa.Column("semset_id", sa.Integer(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["formsemestre_id"],
+            ["notes_formsemestre.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["semset_id"],
+            ["notes_semset.id"],
+        ),
+        sa.UniqueConstraint("formsemestre_id", "semset_id"),
+    )
+    op.create_table(
+        "partition",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("formsemestre_id", sa.Integer(), nullable=True),
+        sa.Column("partition_name", sa.String(length=32), nullable=True),
+        sa.Column("numero", sa.Integer(), nullable=True),
+        sa.Column(
+            "bul_show_rank", sa.Boolean(), server_default="false", nullable=False
+        ),
+        sa.Column("show_in_lists", sa.Boolean(), server_default="true", nullable=False),
+        sa.ForeignKeyConstraint(
+            ["formsemestre_id"],
+            ["notes_formsemestre.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+        sa.UniqueConstraint("formsemestre_id", "partition_name"),
+    )
+    op.create_index(
+        op.f("ix_partition_formsemestre_id"),
+        "partition",
+        ["formsemestre_id"],
+        unique=False,
+    )
+    op.create_table(
+        "sco_prefs",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("dept_id", sa.Integer(), nullable=True),
+        sa.Column("name", sa.String(length=128), nullable=False),
+        sa.Column("value", sa.Text(), nullable=True),
+        sa.Column("formsemestre_id", sa.Integer(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["dept_id"],
+            ["departement.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["formsemestre_id"],
+            ["notes_formsemestre.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_index(op.f("ix_sco_prefs_name"), "sco_prefs", ["name"], unique=False)
+    op.create_table(
+        "scolar_autorisation_inscription",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("etudid", sa.Integer(), nullable=True),
+        sa.Column("formation_code", sa.String(length=32), nullable=False),
+        sa.Column("semestre_id", sa.Integer(), nullable=True),
+        sa.Column(
+            "date",
+            sa.DateTime(timezone=True),
+            server_default=sa.text("now()"),
+            nullable=True,
+        ),
+        sa.Column("origin_formsemestre_id", sa.Integer(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["etudid"],
+            ["identite.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["origin_formsemestre_id"],
+            ["notes_formsemestre.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_table(
+        "scolar_events",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("etudid", sa.Integer(), nullable=True),
+        sa.Column(
+            "event_date",
+            sa.DateTime(timezone=True),
+            server_default=sa.text("now()"),
+            nullable=True,
+        ),
+        sa.Column("formsemestre_id", sa.Integer(), nullable=True),
+        sa.Column("ue_id", sa.Integer(), nullable=True),
+        sa.Column("event_type", sa.String(length=32), nullable=True),
+        sa.Column("comp_formsemestre_id", sa.Integer(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["comp_formsemestre_id"],
+            ["notes_formsemestre.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["etudid"],
+            ["identite.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["formsemestre_id"],
+            ["notes_formsemestre.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["ue_id"],
+            ["notes_ue.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_table(
+        "scolar_formsemestre_validation",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("etudid", sa.Integer(), nullable=True),
+        sa.Column("formsemestre_id", sa.Integer(), nullable=True),
+        sa.Column("ue_id", sa.Integer(), nullable=True),
+        sa.Column("code", sa.String(length=16), nullable=False),
+        sa.Column("assidu", sa.Boolean(), nullable=True),
+        sa.Column(
+            "event_date",
+            sa.DateTime(timezone=True),
+            server_default=sa.text("now()"),
+            nullable=True,
+        ),
+        sa.Column("compense_formsemestre_id", sa.Integer(), nullable=True),
+        sa.Column("moy_ue", sa.Float(), nullable=True),
+        sa.Column("semestre_id", sa.Integer(), nullable=True),
+        sa.Column("is_external", sa.Boolean(), server_default="false", nullable=True),
+        sa.ForeignKeyConstraint(
+            ["compense_formsemestre_id"],
+            ["notes_formsemestre.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["etudid"],
+            ["identite.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["formsemestre_id"],
+            ["notes_formsemestre.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["ue_id"],
+            ["notes_ue.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+        sa.UniqueConstraint("etudid", "formsemestre_id", "ue_id"),
+    )
+    op.create_table(
+        "group_descr",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("partition_id", sa.Integer(), nullable=True),
+        sa.Column("group_name", sa.String(length=64), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["partition_id"],
+            ["partition.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+        sa.UniqueConstraint("partition_id", "group_name"),
+    )
+    op.create_table(
+        "notes_modules",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("titre", sa.Text(), nullable=True),
+        sa.Column("abbrev", sa.Text(), nullable=True),
+        sa.Column("code", sa.String(length=32), nullable=False),
+        sa.Column("heures_cours", sa.Float(), nullable=True),
+        sa.Column("heures_td", sa.Float(), nullable=True),
+        sa.Column("heures_tp", sa.Float(), nullable=True),
+        sa.Column("coefficient", sa.Float(), nullable=True),
+        sa.Column("ects", sa.Float(), nullable=True),
+        sa.Column("ue_id", sa.Integer(), nullable=True),
+        sa.Column("formation_id", sa.Integer(), nullable=True),
+        sa.Column("matiere_id", sa.Integer(), nullable=True),
+        sa.Column("semestre_id", sa.Integer(), server_default="1", nullable=False),
+        sa.Column("numero", sa.Integer(), nullable=True),
+        sa.Column("code_apogee", sa.String(length=16), nullable=True),
+        sa.Column("module_type", sa.Integer(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["formation_id"],
+            ["notes_formations.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["matiere_id"],
+            ["notes_matieres.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["ue_id"],
+            ["notes_ue.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_index(
+        op.f("ix_notes_modules_ue_id"), "notes_modules", ["ue_id"], unique=False
+    )
+    op.create_table(
+        "group_membership",
+        sa.Column("etudid", sa.Integer(), nullable=True),
+        sa.Column("group_id", sa.Integer(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["etudid"],
+            ["identite.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["group_id"],
+            ["group_descr.id"],
+        ),
+        sa.UniqueConstraint("etudid", "group_id"),
+    )
+    op.create_table(
+        "notes_moduleimpl",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("module_id", sa.Integer(), nullable=True),
+        sa.Column("formsemestre_id", sa.Integer(), nullable=True),
+        sa.Column("responsable_id", sa.Integer(), nullable=True),
+        sa.Column("computation_expr", sa.Text(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["formsemestre_id"],
+            ["notes_formsemestre.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["module_id"],
+            ["notes_modules.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["responsable_id"],
+            ["user.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+        sa.UniqueConstraint("formsemestre_id", "module_id"),
+    )
+    op.create_index(
+        op.f("ix_notes_moduleimpl_formsemestre_id"),
+        "notes_moduleimpl",
+        ["formsemestre_id"],
+        unique=False,
+    )
+    op.create_table(
+        "notes_modules_tags",
+        sa.Column("tag_id", sa.Integer(), nullable=True),
+        sa.Column("module_id", sa.Integer(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["module_id"],
+            ["notes_modules.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["tag_id"],
+            ["notes_tags.id"],
+        ),
+    )
+    op.create_table(
+        "absences",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("etudid", sa.Integer(), nullable=True),
+        sa.Column("jour", sa.Date(), nullable=True),
+        sa.Column("estabs", sa.Boolean(), nullable=True),
+        sa.Column("estjust", sa.Boolean(), nullable=True),
+        sa.Column("matin", sa.Boolean(), nullable=True),
+        sa.Column("description", sa.Text(), nullable=True),
+        sa.Column(
+            "entry_date",
+            sa.DateTime(timezone=True),
+            server_default=sa.text("now()"),
+            nullable=True,
+        ),
+        sa.Column("moduleimpl_id", sa.Integer(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["etudid"],
+            ["identite.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["moduleimpl_id"],
+            ["notes_moduleimpl.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_index(op.f("ix_absences_etudid"), "absences", ["etudid"], unique=False)
+    op.create_table(
+        "notes_evaluation",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("moduleimpl_id", sa.Integer(), nullable=True),
+        sa.Column("jour", sa.Date(), nullable=True),
+        sa.Column("heure_debut", sa.Time(), nullable=True),
+        sa.Column("heure_fin", sa.Time(), nullable=True),
+        sa.Column("description", sa.Text(), nullable=True),
+        sa.Column("note_max", sa.Float(), nullable=True),
+        sa.Column("coefficient", sa.Float(), nullable=True),
+        sa.Column("visibulletin", sa.Boolean(), server_default="true", nullable=False),
+        sa.Column(
+            "publish_incomplete", sa.Boolean(), server_default="false", nullable=False
+        ),
+        sa.Column("evaluation_type", sa.Integer(), server_default="0", nullable=False),
+        sa.Column("numero", sa.Integer(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["moduleimpl_id"],
+            ["notes_moduleimpl.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_index(
+        op.f("ix_notes_evaluation_moduleimpl_id"),
+        "notes_evaluation",
+        ["moduleimpl_id"],
+        unique=False,
+    )
+    op.create_table(
+        "notes_moduleimpl_inscription",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("moduleimpl_id", sa.Integer(), nullable=True),
+        sa.Column("etudid", sa.Integer(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["etudid"],
+            ["identite.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["moduleimpl_id"],
+            ["notes_moduleimpl.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_index(
+        op.f("ix_notes_moduleimpl_inscription_etudid"),
+        "notes_moduleimpl_inscription",
+        ["etudid"],
+        unique=False,
+    )
+    op.create_index(
+        op.f("ix_notes_moduleimpl_inscription_moduleimpl_id"),
+        "notes_moduleimpl_inscription",
+        ["moduleimpl_id"],
+        unique=False,
+    )
+    op.create_table(
+        "notes_modules_enseignants",
+        sa.Column("moduleimpl_id", sa.Integer(), nullable=True),
+        sa.Column("ens_id", sa.Integer(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["ens_id"],
+            ["user.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["moduleimpl_id"],
+            ["notes_moduleimpl.id"],
+        ),
+    )
+    op.create_table(
+        "notes_notes",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("etudid", sa.Integer(), nullable=True),
+        sa.Column("evaluation_id", sa.Integer(), nullable=True),
+        sa.Column("value", sa.Float(), nullable=True),
+        sa.Column("comment", sa.Text(), nullable=True),
+        sa.Column(
+            "date",
+            sa.DateTime(timezone=True),
+            server_default=sa.text("now()"),
+            nullable=True,
+        ),
+        sa.Column("uid", sa.Integer(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["etudid"],
+            ["identite.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["evaluation_id"],
+            ["notes_evaluation.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["uid"],
+            ["user.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+        sa.UniqueConstraint("etudid", "evaluation_id"),
+    )
+    op.create_index(
+        op.f("ix_notes_notes_evaluation_id"),
+        "notes_notes",
+        ["evaluation_id"],
+        unique=False,
+    )
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_index(op.f("ix_notes_notes_evaluation_id"), table_name="notes_notes")
+    op.drop_table("notes_notes")
+    op.drop_table("notes_modules_enseignants")
+    op.drop_index(
+        op.f("ix_notes_moduleimpl_inscription_moduleimpl_id"),
+        table_name="notes_moduleimpl_inscription",
+    )
+    op.drop_index(
+        op.f("ix_notes_moduleimpl_inscription_etudid"),
+        table_name="notes_moduleimpl_inscription",
+    )
+    op.drop_table("notes_moduleimpl_inscription")
+    op.drop_index(
+        op.f("ix_notes_evaluation_moduleimpl_id"), table_name="notes_evaluation"
+    )
+    op.drop_table("notes_evaluation")
+    op.drop_index(op.f("ix_absences_etudid"), table_name="absences")
+    op.drop_table("absences")
+    op.drop_table("notes_modules_tags")
+    op.drop_index(
+        op.f("ix_notes_moduleimpl_formsemestre_id"), table_name="notes_moduleimpl"
+    )
+    op.drop_table("notes_moduleimpl")
+    op.drop_table("group_membership")
+    op.drop_index(op.f("ix_notes_modules_ue_id"), table_name="notes_modules")
+    op.drop_table("notes_modules")
+    op.drop_table("group_descr")
+    op.drop_table("scolar_formsemestre_validation")
+    op.drop_table("scolar_events")
+    op.drop_table("scolar_autorisation_inscription")
+    op.drop_index(op.f("ix_sco_prefs_name"), table_name="sco_prefs")
+    op.drop_table("sco_prefs")
+    op.drop_index(op.f("ix_partition_formsemestre_id"), table_name="partition")
+    op.drop_table("partition")
+    op.drop_table("notes_semset_formsemestre")
+    op.drop_table("notes_matieres")
+    op.drop_table("notes_formsemestre_uecoef")
+    op.drop_table("notes_formsemestre_ue_computation_expr")
+    op.drop_table("notes_formsemestre_responsables")
+    op.drop_table("notes_formsemestre_inscription")
+    op.drop_table("notes_formsemestre_etapes")
+    op.drop_table("notes_formsemestre_custommenu")
+    op.drop_index(
+        op.f("ix_notes_appreciations_etudid"), table_name="notes_appreciations"
+    )
+    op.drop_table("notes_appreciations")
+    op.drop_table("itemsuivi_tags_assoc")
+    op.drop_table("entreprise_contact")
+    op.drop_table("absences_notifications")
+    op.drop_table("notes_ue")
+    op.drop_index(
+        op.f("ix_notes_notes_log_evaluation_id"), table_name="notes_notes_log"
+    )
+    op.drop_table("notes_notes_log")
+    op.drop_index(
+        op.f("ix_notes_formsemestre_dept_id"), table_name="notes_formsemestre"
+    )
+    op.drop_table("notes_formsemestre")
+    op.drop_table("itemsuivi")
+    op.drop_table("entreprise_correspondant")
+    op.drop_index(op.f("ix_billet_absence_etudid"), table_name="billet_absence")
+    op.drop_table("billet_absence")
+    op.drop_table("adresse")
+    op.drop_table("admissions")
+    op.drop_table("user_role")
+    op.drop_index(op.f("ix_scolar_news_dept_id"), table_name="scolar_news")
+    op.drop_table("scolar_news")
+    op.drop_index(op.f("ix_notes_tags_dept_id"), table_name="notes_tags")
+    op.drop_table("notes_tags")
+    op.drop_table("notes_semset")
+    op.drop_index(op.f("ix_notes_formations_dept_id"), table_name="notes_formations")
+    op.drop_table("notes_formations")
+    op.drop_index(op.f("ix_identite_dept_id"), table_name="identite")
+    op.drop_table("identite")
+    op.drop_index(op.f("ix_entreprises_dept_id"), table_name="entreprises")
+    op.drop_table("entreprises")
+    op.drop_index(op.f("ix_user_user_name"), table_name="user")
+    op.drop_index(op.f("ix_user_token"), table_name="user")
+    op.drop_index(op.f("ix_user_dept"), table_name="user")
+    op.drop_index(op.f("ix_user_active"), table_name="user")
+    op.drop_table("user")
+    op.drop_table("scolog")
+    op.drop_index(op.f("ix_role_default"), table_name="role")
+    op.drop_table("role")
+    op.drop_index(
+        op.f("ix_notes_form_modalites_modalite"), table_name="notes_form_modalites"
+    )
+    op.drop_table("notes_form_modalites")
+    op.drop_table("itemsuivi_tags")
+    op.drop_table("etud_annotations")
+    op.drop_index(op.f("ix_departement_acronym"), table_name="departement")
+    op.drop_table("departement")
+    # ### end Alembic commands ###
diff --git a/scodoc.py b/scodoc.py
index 45779e4ceb299582604f34e651c8e32fc6bd4e85..a79bcafd86887171a9a4932bddf1d82ada3718ec 100755
--- a/scodoc.py
+++ b/scodoc.py
@@ -66,7 +66,7 @@ def make_shell_context():
 
 
 @app.cli.command()
-def db_init():  # db-init
+def sco_db_init():  # sco-db-init
     """Initialize the database.
     Starts from an existing database and create all
     the necessary SQL tables and functions.
diff --git a/tests/conftest.py b/tests/conftest.py
index a3f1e9c79a666c8ee87521eabaa6d0f9fdbe769f..ea654c70388405600c2a17ddb962012497a8e17b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -23,7 +23,7 @@ def test_client():
         with apptest.app_context():
             with apptest.test_request_context():
                 # erase and reset database:
-                initialize_scodoc_database(erase=True)
+                initialize_scodoc_database(erase=True, create_all=True)
                 # Loge l'utilisateur super-admin
                 admin_user = get_super_admin()
                 login_user(admin_user)
diff --git a/tools/configure-scodoc9.sh b/tools/configure-scodoc9.sh
index f058378d5e09cbd242a1917e9448b5e6c3a1f8f6..1c914830ac6457a07b89cb0edf5f25f60805ec23 100755
--- a/tools/configure-scodoc9.sh
+++ b/tools/configure-scodoc9.sh
@@ -121,7 +121,7 @@ then
     echo
     echo "Création des tables et du compte admin"
     echo
-    su -c "(cd /opt/scodoc; source venv/bin/activate; flask db-init; flask user-password admin)" "$SCODOC_USER" || die "Erreur: db-init"
+    su -c "(cd /opt/scodoc; source venv/bin/activate; flask db upgrade; flask sco-db-init; flask user-password admin)" "$SCODOC_USER" || die "Erreur: sco-db-init"
     echo
     echo "base initialisée et admin créé."
     echo
diff --git a/tools/debian/postinst b/tools/debian/postinst
index 53db633c5c19dac2e38e234dbb4c5b28e3e80b8d..835425d6e3134d3163d61313ce4dc6166d6211cc 100644
--- a/tools/debian/postinst
+++ b/tools/debian/postinst
@@ -15,7 +15,7 @@ check_create_scodoc_user
 
 # -- Répertoires /opt/scodoc donné à scodoc
 change_scodoc_file_ownership
-# --- Création au bseoin de /opt/scodoc-data
+# --- Création au besoin de /opt/scodoc-data
 set_scodoc_var_dir
 
 # ------------ LOCALES (pour compat bases ScoDoc 7 et plus anciennes)
@@ -71,11 +71,11 @@ fi
 # ------------ CREATION DU VIRTUALENV
 # donc re-créé sur le client à chaque install ou upgrade
 #echo "Creating python3 virtualenv..."
-(cd $SCODOC_DIR && python3 -m venv venv) || die "Error creating Python 3 virtualenv"
+su -c "(cd $SCODOC_DIR && python3 -m venv venv)" "$SCODOC_USER" || die "Error creating Python 3 virtualenv"
 
 # ------------ INSTALL DES PAQUETS PYTHON (3.9)
 # pip in our env, as user "scodoc"
-(cd $SCODOC_DIR && source venv/bin/activate && pip install wheel && pip install -r requirements-3.9.txt) || die "Error installing python packages"
+su -c "(cd $SCODOC_DIR && source venv/bin/activate && pip install wheel && pip install -r requirements-3.9.txt)" "$SCODOC_USER" || die "Error installing python packages"
 
 # --- NGINX
 if [ ! -L /etc/nginx/sites-enabled/scodoc9.nginx ]
@@ -89,6 +89,19 @@ fi
 # --- Ensure postgres user "scodoc" ($POSTGRES_USER) exists
 init_postgres_user
 
+# ------------  BASE DE DONNEES
+# gérées avec Flask-Migrate (Alembic/SQLAlchemy)
+# Si la base SCODOC existe, tente de la mettre à jour
+# (Ne gère pas les bases DEV et TEST)
+n=$(su -c "psql -l | grep -c -E '^[[:blank:]]*SCODOC[[:blank:]]*\|'" "$SCODOC_USER")
+if [ "$n" == 1 ]
+then
+    echo "Upgrading existing SCODOC database..."
+    # utilise les scripts dans migrations/version/
+    # pour mettre à jour notre base (en tant qu'utilisateur scodoc)
+    export SQLALCHEMY_DATABASE_URI="postgresql:///SCODOC"
+    su -c "(cd $SCODOC_DIR && source venv/bin/activate && flask db upgrade)" "$SCODOC_USER"
+fi
 
 # ------------ CONFIG SERVICE SCODOC
 echo