diff --git a/app/api/etudiants.py b/app/api/etudiants.py
index 264271cda96999deab86755a2e3952c48b977195..03dc217d42aa22bcd146a63694c9c232226c37d3 100755
--- a/app/api/etudiants.py
+++ b/app/api/etudiants.py
@@ -18,6 +18,7 @@ from sqlalchemy import desc, func, or_
 from sqlalchemy.dialects.postgresql import VARCHAR
 
 import app
+from app import db
 from app.api import api_bp as bp, api_web_bp
 from app.api import tools
 from app.but import bulletin_but_court
@@ -28,10 +29,12 @@ from app.models import (
     FormSemestreInscription,
     FormSemestre,
     Identite,
+    ScolarNews,
 )
 from app.scodoc import sco_bulletins
 from app.scodoc import sco_groups
 from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
+from app.scodoc import sco_etud
 from app.scodoc.sco_permissions import Permission
 from app.scodoc.sco_utils import json_error, suppress_accents
 
@@ -475,3 +478,48 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None):
     data = sco_groups.get_etud_groups(etud.id, formsemestre.id)
 
     return data
+
+
+@bp.route("/etudiant/create", methods=["POST"], defaults={"force": False})
+@bp.route("/etudiant/create/force", methods=["POST"], defaults={"force": True})
+@scodoc
+@permission_required(Permission.EtudInscrit)
+@as_json
+def etudiant_create(force=False):
+    """Création d'un nouvel étudiant
+    Si force, crée même si homonymie détectée.
+    L'étudiant créé n'est pas inscrit à un semestre.
+    Champs requis: nom, prenom (sauf si config sans prénom), dept (string:acronyme)
+    """
+    args = request.get_json(force=True)  # may raise 400 Bad Request
+    dept = args.get("dept", None)
+    if not dept:
+        return scu.json_error(400, "dept requis")
+    dept_o = Departement.query.filter_by(acronym=dept).first()
+    if not dept_o:
+        return scu.json_error(400, "dept invalide")
+    app.set_sco_dept(dept)
+    args["dept_id"] = dept_o.id
+    # vérifie que le département de création est bien autorisé
+    if not current_user.has_permission(Permission.EtudInscrit, dept):
+        return json_error(403, "departement non autorisé")
+    nom = args.get("nom", None)
+    prenom = args.get("prenom", None)
+    ok, homonyms = sco_etud.check_nom_prenom_homonyms(nom=nom, prenom=prenom)
+    if not ok:
+        return scu.json_error(400, "nom ou prénom invalide")
+    if len(homonyms) > 0 and not force:
+        return scu.json_error(
+            400, f"{len(homonyms)} homonymes détectés. Vous pouvez utiliser /force."
+        )
+    etud = Identite.create_etud(**args)
+    # Poste une nouvelle dans le département concerné:
+    ScolarNews.add(
+        typ=ScolarNews.NEWS_INSCR,
+        text=f"Nouvel étudiant {etud.html_link_fiche()}",
+        url=etud.url_fiche(),
+        max_frequency=0,
+        dept_id=dept_o.id,
+    )
+    db.session.commit()
+    return etud.to_dict_short()
diff --git a/app/auth/models.py b/app/auth/models.py
index 9f84196de231ab62500fd3559bc7f8fee8e789e4..759c5bd730ec99aaa63a4b5a35cb6a3af6d214aa 100644
--- a/app/auth/models.py
+++ b/app/auth/models.py
@@ -27,7 +27,6 @@ from app.scodoc.sco_exceptions import ScoValueError
 from app.scodoc.sco_permissions import Permission
 from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS
 import app.scodoc.sco_utils as scu
-from app.scodoc import sco_etud  # a deplacer dans scu
 
 VALID_LOGIN_EXP = re.compile(r"^[a-zA-Z0-9@\\\-_\.]+$")
 
@@ -462,8 +461,8 @@ class User(UserMixin, db.Model, ScoDocModel):
         """nomplogin est le nom en majuscules suivi du prénom et du login
         e.g. Dupont Pierre (dupont)
         """
-        nom = sco_etud.format_nom(self.nom) if self.nom else self.user_name.upper()
-        return f"{nom} {sco_etud.format_prenom(self.prenom)} ({self.user_name})"
+        nom = scu.format_nom(self.nom) if self.nom else self.user_name.upper()
+        return f"{nom} {scu.format_prenom(self.prenom)} ({self.user_name})"
 
     @staticmethod
     def get_user_id_from_nomplogin(nomplogin: str) -> Optional[int]:
@@ -481,29 +480,29 @@ class User(UserMixin, db.Model, ScoDocModel):
     def get_nom_fmt(self):
         """Nom formaté: "Martin" """
         if self.nom:
-            return sco_etud.format_nom(self.nom, uppercase=False)
+            return scu.format_nom(self.nom, uppercase=False)
         else:
             return self.user_name
 
     def get_prenom_fmt(self):
         """Prénom formaté (minuscule capitalisées)"""
-        return sco_etud.format_prenom(self.prenom)
+        return scu.format_prenom(self.prenom)
 
     def get_nomprenom(self):
         """Nom capitalisé suivi de l'initiale du prénom:
         Viennet E.
         """
-        prenom_abbrv = scu.abbrev_prenom(sco_etud.format_prenom(self.prenom))
+        prenom_abbrv = scu.abbrev_prenom(scu.format_prenom(self.prenom))
         return (self.get_nom_fmt() + " " + prenom_abbrv).strip()
 
     def get_prenomnom(self):
         """L'initiale du prénom suivie du nom: "J.-C. Dupont" """
-        prenom_abbrv = scu.abbrev_prenom(sco_etud.format_prenom(self.prenom))
+        prenom_abbrv = scu.abbrev_prenom(scu.format_prenom(self.prenom))
         return (prenom_abbrv + " " + self.get_nom_fmt()).strip()
 
     def get_nomcomplet(self):
         "Prénom et nom complets"
-        return sco_etud.format_prenom(self.prenom) + " " + self.get_nom_fmt()
+        return scu.format_prenom(self.prenom) + " " + self.get_nom_fmt()
 
     # nomnoacc était le nom en minuscules sans accents (inutile)
 
diff --git a/app/entreprises/__init__.py b/app/entreprises/__init__.py
index 121b7e3cb8ca51101d6fd97c3fd39901d85795f5..1d41e77d014948647a21cbba921734fb3255dbfb 100644
--- a/app/entreprises/__init__.py
+++ b/app/entreprises/__init__.py
@@ -6,6 +6,7 @@ from flask import Blueprint
 from app.scodoc import sco_etud
 from app.auth.models import User
 from app.models import Departement
+import app.scodoc.sco_utils as scu
 
 bp = Blueprint("entreprises", __name__)
 
@@ -15,12 +16,12 @@ SIRET_PROVISOIRE_START = "xx"
 
 @bp.app_template_filter()
 def format_prenom(s):
-    return sco_etud.format_prenom(s)
+    return scu.format_prenom(s)
 
 
 @bp.app_template_filter()
 def format_nom(s):
-    return sco_etud.format_nom(s)
+    return scu.format_nom(s)
 
 
 @bp.app_template_filter()
diff --git a/app/entreprises/routes.py b/app/entreprises/routes.py
index 60d567abf75e6396d89bb90d6d81ae875b9eaab9..529706e99946354eb5e9f22960b5cdb77aee4c55 100644
--- a/app/entreprises/routes.py
+++ b/app/entreprises/routes.py
@@ -1580,8 +1580,8 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
             )
         )
     elif request.method == "GET":
-        form.etudiant.data = f"""{sco_etud.format_nom(etudiant.nom)} {
-            sco_etud.format_prenom(etudiant.prenom)}"""
+        form.etudiant.data = f"""{scu.format_nom(etudiant.nom)} {
+            scu.format_prenom(etudiant.prenom)}"""
         form.etudid.data = etudiant.id
         form.type_offre.data = stage_apprentissage.type_offre
         form.date_debut.data = stage_apprentissage.date_debut
@@ -1699,7 +1699,7 @@ def json_etudiants():
     list = []
     for etudiant in etudiants:
         content = {}
-        value = f"{sco_etud.format_nom(etudiant.nom)} {sco_etud.format_prenom(etudiant.prenom)}"
+        value = f"{scu.format_nom(etudiant.nom)} {scu.format_prenom(etudiant.prenom)}"
         if etudiant.inscription_courante() is not None:
             content = {
                 "id": f"{etudiant.id}",
diff --git a/app/models/__init__.py b/app/models/__init__.py
index ababbdbcd75334deb45d7fa0c38250cfd411ed4b..b4eea1636d871ab48feea07e1e247d172970a380 100644
--- a/app/models/__init__.py
+++ b/app/models/__init__.py
@@ -71,7 +71,7 @@ class ScoDocModel:
 
     @classmethod
     def convert_dict_fields(cls, args: dict) -> dict:
-        """Convert fields in the given dict. No side effect.
+        """Convert fields from the given dict to model's attributes values. No side effect.
         By default, do nothing, but is overloaded by some subclasses.
         args: dict with args in application.
         returns: dict to store in model's db.
@@ -133,7 +133,6 @@ from app.models.notes import (
     NotesNotesLog,
 )
 from app.models.validations import (
-    ScolarEvent,
     ScolarFormSemestreValidation,
     ScolarAutorisationInscription,
 )
@@ -152,3 +151,4 @@ from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
 from app.models.config import ScoDocSiteConfig
 
 from app.models.assiduites import Assiduite, Justificatif
+from app.models.scolar_event import ScolarEvent
diff --git a/app/models/etudiants.py b/app/models/etudiants.py
index e20a7b6e71b4af8de755bb14791d5b21a61a7dc7..192d7ff0c4ec7008efde5ff233f9fb6c3757012c 100644
--- a/app/models/etudiants.py
+++ b/app/models/etudiants.py
@@ -15,7 +15,7 @@ from sqlalchemy import desc, text
 
 from app import db, log
 from app import models
-
+from app.models.scolar_event import ScolarEvent
 from app.scodoc import notesdb as ndb
 from app.scodoc.sco_bac import Baccalaureat
 from app.scodoc.sco_exceptions import ScoInvalidParamError, ScoValueError
@@ -170,9 +170,13 @@ class Identite(db.Model, models.ScoDocModel):
 
     def html_link_fiche(self) -> str:
         "lien vers la fiche"
-        return f"""<a class="stdlink" href="{
-           url_for("scolar.ficheEtud", scodoc_dept=self.departement.acronym, etudid=self.id)
-        }">{self.nomprenom}</a>"""
+        return f"""<a class="stdlink" href="{self.url_fiche()}">{self.nomprenom}</a>"""
+
+    def url_fiche(self) -> str:
+        "url de la fiche étudiant"
+        return url_for(
+            "scolar.ficheEtud", scodoc_dept=self.departement.acronym, etudid=self.id
+        )
 
     @classmethod
     def from_request(cls, etudid=None, code_nip=None) -> "Identite":
@@ -211,6 +215,10 @@ class Identite(db.Model, models.ScoDocModel):
             etud.admission = Admission()
         etud.adresses.append(Adresse(typeadresse="domicile"))
         db.session.flush()
+
+        event = ScolarEvent(etud=etud, event_type="CREATION")
+        db.session.add(event)
+        log(f"Identite.create {etud}")
         return etud
 
     @property
diff --git a/app/models/events.py b/app/models/events.py
index 74583b8a4a85a38c94f570ff7ae93b22fd6539ad..06dbe558d1e6d955856ddea53335f7e03177f11e 100644
--- a/app/models/events.py
+++ b/app/models/events.py
@@ -133,7 +133,7 @@ class ScolarNews(db.Model):
         return query.order_by(cls.date.desc()).limit(n).all()
 
     @classmethod
-    def add(cls, typ, obj=None, text="", url=None, max_frequency=600):
+    def add(cls, typ, obj=None, text="", url=None, max_frequency=600, dept_id=None):
         """Enregistre une nouvelle
         Si max_frequency, ne génère pas 2 nouvelles "identiques"
         à moins de max_frequency secondes d'intervalle (10 minutes par défaut).
@@ -141,10 +141,11 @@ class ScolarNews(db.Model):
         même (obj, typ, user).
         La nouvelle enregistrée est aussi envoyée par mail.
         """
+        dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
         if max_frequency:
             last_news = (
                 cls.query.filter_by(
-                    dept_id=g.scodoc_dept_id,
+                    dept_id=dept_id,
                     authenticated_user=current_user.user_name,
                     type=typ,
                     object=obj,
@@ -163,7 +164,7 @@ class ScolarNews(db.Model):
                     return
 
         news = ScolarNews(
-            dept_id=g.scodoc_dept_id,
+            dept_id=dept_id,
             authenticated_user=current_user.user_name,
             type=typ,
             object=obj,
diff --git a/app/models/scolar_event.py b/app/models/scolar_event.py
new file mode 100644
index 0000000000000000000000000000000000000000..4294efb124e0b370c4d52e28b428f50fe706df1c
--- /dev/null
+++ b/app/models/scolar_event.py
@@ -0,0 +1,48 @@
+"""évènements scolaires dans la vie d'un étudiant(inscription, ...)
+"""
+from app import db
+from app.models import SHORT_STR_LEN
+
+
+class ScolarEvent(db.Model):
+    """Evenement dans le parcours scolaire d'un étudiant"""
+
+    __tablename__ = "scolar_events"
+    id = db.Column(db.Integer, primary_key=True)
+    event_id = db.synonym("id")
+    etudid = db.Column(
+        db.Integer,
+        db.ForeignKey("identite.id", ondelete="CASCADE"),
+    )
+    event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
+    formsemestre_id = db.Column(
+        db.Integer,
+        db.ForeignKey("notes_formsemestre.id", ondelete="SET NULL"),
+    )
+    ue_id = db.Column(
+        db.Integer,
+        db.ForeignKey("notes_ue.id", ondelete="SET NULL"),
+    )
+    # 'CREATION', 'INSCRIPTION', 'DEMISSION',
+    # 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM'
+    # 'ECHEC_SEM'
+    # 'UTIL_COMPENSATION'
+    event_type = db.Column(db.String(SHORT_STR_LEN))
+    # Semestre compensé par formsemestre_id:
+    comp_formsemestre_id = db.Column(
+        db.Integer,
+        db.ForeignKey("notes_formsemestre.id"),
+    )
+    etud = db.relationship("Identite", lazy="select", backref="events", uselist=False)
+    formsemestre = db.relationship(
+        "FormSemestre", lazy="select", uselist=False, foreign_keys=[formsemestre_id]
+    )
+
+    def to_dict(self) -> dict:
+        "as a dict"
+        d = dict(self.__dict__)
+        d.pop("_sa_instance_state", None)
+        return d
+
+    def __repr__(self) -> str:
+        return f"{self.__class__.__name__}({self.event_type}, {self.event_date.isoformat()}, {self.formsemestre})"
diff --git a/app/models/validations.py b/app/models/validations.py
index 14e7a5b7a4dcef22346b3cb8907c58951813d91b..17dd12a6bcd5aae533f76395314663f7e0cb6899 100644
--- a/app/models/validations.py
+++ b/app/models/validations.py
@@ -1,6 +1,6 @@
 # -*- coding: UTF-8 -*
 
-"""Notes, décisions de jury, évènements scolaires
+"""Notes, décisions de jury
 """
 
 from app import db
@@ -218,47 +218,3 @@ class ScolarAutorisationInscription(db.Model):
                 msg=f"Passage vers S{autorisation.semestre_id}: effacé",
             )
         db.session.flush()
-
-
-class ScolarEvent(db.Model):
-    """Evenement dans le parcours scolaire d'un étudiant"""
-
-    __tablename__ = "scolar_events"
-    id = db.Column(db.Integer, primary_key=True)
-    event_id = db.synonym("id")
-    etudid = db.Column(
-        db.Integer,
-        db.ForeignKey("identite.id", ondelete="CASCADE"),
-    )
-    event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
-    formsemestre_id = db.Column(
-        db.Integer,
-        db.ForeignKey("notes_formsemestre.id", ondelete="SET NULL"),
-    )
-    ue_id = db.Column(
-        db.Integer,
-        db.ForeignKey("notes_ue.id", ondelete="SET NULL"),
-    )
-    # 'CREATION', 'INSCRIPTION', 'DEMISSION',
-    # 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM'
-    # 'ECHEC_SEM'
-    # 'UTIL_COMPENSATION'
-    event_type = db.Column(db.String(SHORT_STR_LEN))
-    # Semestre compensé par formsemestre_id:
-    comp_formsemestre_id = db.Column(
-        db.Integer,
-        db.ForeignKey("notes_formsemestre.id"),
-    )
-    etud = db.relationship("Identite", lazy="select", backref="events", uselist=False)
-    formsemestre = db.relationship(
-        "FormSemestre", lazy="select", uselist=False, foreign_keys=[formsemestre_id]
-    )
-
-    def to_dict(self) -> dict:
-        "as a dict"
-        d = dict(self.__dict__)
-        d.pop("_sa_instance_state", None)
-        return d
-
-    def __repr__(self) -> str:
-        return f"{self.__class__.__name__}({self.event_type}, {self.event_date.isoformat()}, {self.formsemestre})"
diff --git a/app/scodoc/sco_etud.py b/app/scodoc/sco_etud.py
index a28728578f74f7f14d81291965db11d899a70e41..9bd09e899bc3af9fdd2bbd34d7dfb9a3373cc45b 100644
--- a/app/scodoc/sco_etud.py
+++ b/app/scodoc/sco_etud.py
@@ -45,6 +45,12 @@ from app.models.etudiants import (
     pivot_year,
 )
 import app.scodoc.sco_utils as scu
+from app.scodoc.sco_utils import (
+    format_civilite,
+    format_nom,
+    format_nomprenom,
+    format_prenom,
+)
 import app.scodoc.notesdb as ndb
 from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
 from app.scodoc import safehtml
@@ -102,60 +108,6 @@ def force_uppercase(s):
     return s.upper() if s else s
 
 
-def format_nomprenom(etud, reverse=False):
-    """Formatte civilité/nom/prenom pour affichages: "M. Pierre Dupont"
-    Si reverse, "Dupont Pierre", sans civilité.
-
-    DEPRECATED: utiliser Identite.nomprenom
-    """
-    nom = etud.get("nom_disp", "") or etud.get("nom_usuel", "") or etud["nom"]
-    prenom = format_prenom(etud["prenom"])
-    civilite = format_civilite(etud["civilite"])
-    if reverse:
-        fs = [nom, prenom]
-    else:
-        fs = [civilite, prenom, nom]
-    return " ".join([x for x in fs if x])
-
-
-def format_prenom(s):
-    """Formatte prenom etudiant pour affichage
-    DEPRECATED: utiliser Identite.prenom_str
-    """
-    if not s:
-        return ""
-    frags = s.split()
-    r = []
-    for frag in frags:
-        fs = frag.split("-")
-        r.append("-".join([x.lower().capitalize() for x in fs]))
-    return " ".join(r)
-
-
-def format_nom(s, uppercase=True):
-    if not s:
-        return ""
-    if uppercase:
-        return s.upper()
-    else:
-        return format_prenom(s)
-
-
-def format_civilite(civilite):
-    """returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
-    personne ne souhaitant pas d'affichage).
-    Raises ScoValueError if conversion fails.
-    """
-    try:
-        return {
-            "M": "M.",
-            "F": "Mme",
-            "X": "",
-        }[civilite]
-    except KeyError as exc:
-        raise ScoValueError(f"valeur invalide pour la civilité: {civilite}") from exc
-
-
 def _format_etat_civil(etud: dict) -> str:
     "Mme Béatrice DUPONT, en utilisant les données d'état civil si indiquées."
     if etud["prenom_etat_civil"] or etud["civilite_etat_civil"]:
@@ -657,16 +609,6 @@ def create_etud(cnx, args: dict = None):
     db.session.commit()
     etudid = etud.id
 
-    # event
-    scolar_events_create(
-        cnx,
-        args={
-            "etudid": etudid,
-            "event_date": time.strftime("%d/%m/%Y"),
-            "formsemestre_id": None,
-            "event_type": "CREATION",
-        },
-    )
     # log
     logdb(
         cnx,
@@ -674,16 +616,18 @@ def create_etud(cnx, args: dict = None):
         etudid=etudid,
         msg="creation initiale",
     )
-    etud = etudident_list(cnx, {"etudid": etudid})[0]
-    fill_etuds_info([etud])
-    etud["url"] = url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
+    etud_dict = etudident_list(cnx, {"etudid": etudid})[0]
+    fill_etuds_info([etud_dict])
+    etud_dict["url"] = url_for(
+        "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
+    )
     ScolarNews.add(
         typ=ScolarNews.NEWS_INSCR,
-        text='Nouvel étudiant <a href="%(url)s">%(nomprenom)s</a>' % etud,
+        text=f"Nouvel étudiant {etud.html_link_fiche()}",
         url=etud["url"],
         max_frequency=0,
     )
-    return etud
+    return etud_dict
 
 
 # ---------- "EVENTS"
diff --git a/app/scodoc/sco_find_etud.py b/app/scodoc/sco_find_etud.py
index 3b9cd1305ab565fbb9af1182f9462a58d10dddaf..eade74930c74ef3c1a0a9f3236e63f1390fbb6f4 100644
--- a/app/scodoc/sco_find_etud.py
+++ b/app/scodoc/sco_find_etud.py
@@ -42,6 +42,7 @@ from app.scodoc import sco_groups
 from app.scodoc.sco_exceptions import ScoException
 from app.scodoc.sco_permissions import Permission
 from app.scodoc import sco_preferences
+from app.scodoc import sco_utils as scu
 
 
 def form_search_etud(
@@ -271,7 +272,7 @@ def search_etud_by_name(term: str) -> list:
             data = [
                 {
                     "label": "%s %s %s"
-                    % (x["code_nip"], x["nom"], sco_etud.format_prenom(x["prenom"])),
+                    % (x["code_nip"], x["nom"], scu.format_prenom(x["prenom"])),
                     "value": x["code_nip"],
                 }
                 for x in r
@@ -290,7 +291,7 @@ def search_etud_by_name(term: str) -> list:
 
             data = [
                 {
-                    "label": "%s %s" % (x["nom"], sco_etud.format_prenom(x["prenom"])),
+                    "label": "%s %s" % (x["nom"], scu.format_prenom(x["prenom"])),
                     "value": x["etudid"],
                 }
                 for x in r
diff --git a/app/scodoc/sco_formsemestre_inscriptions.py b/app/scodoc/sco_formsemestre_inscriptions.py
index 7837c299f2a9de3250c174034bb202deeaf83509..887a0625ea1db7b03851064dd418e46295120d94 100644
--- a/app/scodoc/sco_formsemestre_inscriptions.py
+++ b/app/scodoc/sco_formsemestre_inscriptions.py
@@ -39,7 +39,7 @@ from app.comp.res_compat import NotesTableCompat
 from app.models import Formation, FormSemestre, FormSemestreInscription, Scolog
 from app.models.etudiants import Identite
 from app.models.groups import Partition, GroupDescr
-from app.models.validations import ScolarEvent
+from app.models.scolar_event import ScolarEvent
 import app.scodoc.sco_utils as scu
 from app import log
 from app.scodoc.scolog import logdb
@@ -222,10 +222,10 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
     cnx = ndb.GetDBConnexion()
     cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
     cursor.execute(
-        """SELECT Im.id AS moduleimpl_inscription_id 
+        """SELECT Im.id AS moduleimpl_inscription_id
         FROM notes_moduleimpl_inscription Im, notes_moduleimpl M
         WHERE Im.etudid=%(etudid)s
-        and Im.moduleimpl_id = M.id 
+        and Im.moduleimpl_id = M.id
         and M.formsemestre_id = %(formsemestre_id)s
         """,
         {"etudid": etudid, "formsemestre_id": formsemestre_id},
@@ -253,7 +253,7 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
         nbinscrits = len(inscrits)
         if nbinscrits == 0:
             log(
-                f"""do_formsemestre_desinscription: 
+                f"""do_formsemestre_desinscription:
                 suppression du semestre extérieur {formsemestre}"""
             )
             flash("Semestre exterieur supprimé")
@@ -436,7 +436,7 @@ def formsemestre_inscription_with_modules(
     if inscr is not None:
         H.append(
             f"""
-            <p class="warning">{etud.nomprenom} est déjà inscrit 
+            <p class="warning">{etud.nomprenom} est déjà inscrit
             dans le semestre {formsemestre.titre_mois()}
             </p>
             <ul>
@@ -482,8 +482,8 @@ def formsemestre_inscription_with_modules(
         H.append("</ul>")
         H.append(
             f"""<p><a href="{ url_for( "notes.formsemestre_inscription_with_modules",
-            scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=formsemestre_id, 
-            multiple_ok=1, 
+            scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=formsemestre_id,
+            multiple_ok=1,
             group_ids=group_ids )
             }">Continuer quand même l'inscription</a>
             </p>"""
@@ -644,7 +644,7 @@ function chkbx_select(field_id, state) {
             """
     <p>Voici la liste des modules du semestre choisi.</p>
     <p>
-    Les modules cochés sont ceux dans lesquels l'étudiant est inscrit. 
+    Les modules cochés sont ceux dans lesquels l'étudiant est inscrit.
     Vous pouvez l'inscrire ou le désincrire d'un ou plusieurs modules.
     </p>
     <p>Attention: cette méthode ne devrait être utilisée que pour les modules
diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py
index a37e7934cd3309ead5f941a525206ad14503dcd4..61b2e571a9f2f65c295658054c1270e9927d2c81 100644
--- a/app/scodoc/sco_groups.py
+++ b/app/scodoc/sco_groups.py
@@ -53,6 +53,7 @@ from app.scodoc import codes_cursus
 from app.scodoc import sco_cursus
 from app.scodoc import sco_etud
 from app.scodoc.sco_etud import etud_sort_key
+import app.scodoc.sco_utils as scu
 from app.scodoc import sco_xml
 from app.scodoc.sco_exceptions import ScoException, AccessDenied, ScoValueError
 from app.scodoc.TrivialFormulator import TrivialFormulator
@@ -573,8 +574,8 @@ def XMLgetGroupsInPartition(partition_id):  # was XMLgetGroupesTD
                     etudid=str(e["etudid"]),
                     civilite=etud["civilite_str"] or "",
                     sexe=etud["civilite_str"] or "",  # compat
-                    nom=sco_etud.format_nom(etud["nom"] or ""),
-                    prenom=sco_etud.format_prenom(etud["prenom"] or ""),
+                    nom=scu.format_nom(etud["nom"] or ""),
+                    prenom=scu.format_prenom(etud["prenom"] or ""),
                     origin=_comp_etud_origin(etud, formsemestre),
                 )
             )
@@ -599,8 +600,8 @@ def XMLgetGroupsInPartition(partition_id):  # was XMLgetGroupesTD
                     "etud",
                     etudid=str(etud["etudid"]),
                     sexe=etud["civilite_str"] or "",
-                    nom=sco_etud.format_nom(etud["nom"] or ""),
-                    prenom=sco_etud.format_prenom(etud["prenom"] or ""),
+                    nom=scu.format_nom(etud["nom"] or ""),
+                    prenom=scu.format_prenom(etud["prenom"] or ""),
                     origin=_comp_etud_origin(etud, formsemestre),
                 )
             )
diff --git a/app/scodoc/sco_prepajury.py b/app/scodoc/sco_prepajury.py
index bef329232fcda1079e81fb755029b39f61ab69d9..ba74b0aa9c1144c53c9dc069c2d34286b681d501 100644
--- a/app/scodoc/sco_prepajury.py
+++ b/app/scodoc/sco_prepajury.py
@@ -244,8 +244,8 @@ def feuille_preparation_jury(formsemestre_id):
             [
                 etud.id,
                 etud.civilite_str,
-                sco_etud.format_nom(etud.nom),
-                sco_etud.format_prenom(etud.prenom),
+                scu.format_nom(etud.nom),
+                scu.format_prenom(etud.prenom),
                 etud.date_naissance,
                 etud.admission.bac if etud.admission else "",
                 etud.admission.specialite if etud.admission else "",
diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py
index e67ebea5695440573ab738e3e10aa4a28c792db2..87bc1097ba8962f4c15efb21055492ae4ac23c39 100644
--- a/app/scodoc/sco_saisie_notes.py
+++ b/app/scodoc/sco_saisie_notes.py
@@ -1244,7 +1244,7 @@ def _form_saisie_notes(
             '<span class="%s">' % classdem
             + e["civilite_str"]
             + " "
-            + sco_etud.format_nomprenom(e, reverse=True)
+            + scu.format_nomprenom(e, reverse=True)
             + "</span>"
         )
 
diff --git a/app/scodoc/sco_trombino.py b/app/scodoc/sco_trombino.py
index 396df98ecdfb933c58d1a99df88638b7ddee8358..d5fbdebb1b921bd7e3b54fd8170edc1b0ef58cd7 100644
--- a/app/scodoc/sco_trombino.py
+++ b/app/scodoc/sco_trombino.py
@@ -129,7 +129,7 @@ def trombino_html(groups_infos):
     H = [
         f"""<table style="padding-top: 10px; padding-bottom: 10px;">
         <tr>
-        <td><span 
+        <td><span
         style="font-style: bold; font-size: 150%%; padding-right: 20px;"
         >{group_txt}</span></td>"""
     ]
@@ -164,9 +164,9 @@ def trombino_html(groups_infos):
         H.append("</span>")
         H.append(
             '<span class="trombi_legend"><span class="trombi_prenom">'
-            + sco_etud.format_prenom(t["prenom"])
+            + scu.format_prenom(t["prenom"])
             + '</span><span class="trombi_nom">'
-            + sco_etud.format_nom(t["nom"])
+            + scu.format_nom(t["nom"])
             + (" <i>(dem.)</i>" if t["etat"] == "D" else "")
         )
         H.append("</span></span></span>")
@@ -175,10 +175,10 @@ def trombino_html(groups_infos):
     H.append("</div>")
     H.append(
         f"""<div style="margin-bottom:15px;">
-        <a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept, 
+        <a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept,
             fmt='pdf', group_ids=groups_infos.group_ids)}">Version PDF</a>
         &nbsp;&nbsp;
-        <a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept, 
+        <a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept,
             fmt='doc', group_ids=groups_infos.group_ids)}">Version doc</a>
         </div>"""
     )
@@ -202,9 +202,9 @@ def check_local_photos_availability(groups_infos, fmt=""):
         return (
             False,
             scu.confirm_dialog(
-                f"""<p>Attention: {nb_missing} photos ne sont pas disponibles 
+                f"""<p>Attention: {nb_missing} photos ne sont pas disponibles
                 et ne peuvent pas être exportées.</p>
-                <p>Vous pouvez <a class="stdlink" 
+                <p>Vous pouvez <a class="stdlink"
                 href="{groups_infos.base_url}&dialog_confirmed=1&fmt={fmt}"
                 >exporter seulement les photos existantes</a>""",
                 dest_url="trombino",
@@ -263,11 +263,11 @@ def trombino_copy_photos(group_ids=[], dialog_confirmed=False):
     if not dialog_confirmed:
         return scu.confirm_dialog(
             f"""<h2>Copier les photos du portail vers ScoDoc ?</h2>
-            <p>Les photos du groupe {groups_infos.groups_titles} présentes 
+            <p>Les photos du groupe {groups_infos.groups_titles} présentes
             dans ScoDoc seront remplacées par celles du portail (si elles existent).
             </p>
-            <p>(les photos sont normalement automatiquement copiées 
-            lors de leur première utilisation, l'usage de cette fonction 
+            <p>(les photos sont normalement automatiquement copiées
+            lors de leur première utilisation, l'usage de cette fonction
             n'est nécessaire que si les photos du portail ont été modifiées)
             </p>
             """,
@@ -349,7 +349,7 @@ def _trombino_pdf(groups_infos):
                 [img],
                 [
                     Paragraph(
-                        SU(sco_etud.format_nomprenom(t)),
+                        SU(scu.format_nomprenom(t)),
                         style_sheet["Normal"],
                     )
                 ],
@@ -428,7 +428,7 @@ def _listeappel_photos_pdf(groups_infos):
         t = groups_infos.members[i]
         img = _get_etud_platypus_image(t, image_width=PHOTO_WIDTH)
         txt = Paragraph(
-            SU(sco_etud.format_nomprenom(t)),
+            SU(scu.format_nomprenom(t)),
             style_sheet["Normal"],
         )
         if currow:
diff --git a/app/scodoc/sco_trombino_doc.py b/app/scodoc/sco_trombino_doc.py
index b40da5771582295a5fc66f4692bb0baf4fff7494..7cdbad974efb83866033e6eb5ae871960d373b51 100644
--- a/app/scodoc/sco_trombino_doc.py
+++ b/app/scodoc/sco_trombino_doc.py
@@ -55,7 +55,7 @@ def trombino_doc(groups_infos):
         cell = table.rows[2 * li + 1].cells[co]
         cell.vertical_alignment = WD_ALIGN_VERTICAL.TOP
         cell_p, cell_f, cell_r = _paragraph_format_run(cell)
-        cell_r.add_text(sco_etud.format_nomprenom(t))
+        cell_r.add_text(scu.format_nomprenom(t))
         cell_f.space_after = Mm(8)
 
     return scu.send_docx(document, filename)
diff --git a/app/scodoc/sco_trombino_tours.py b/app/scodoc/sco_trombino_tours.py
index 821c0e9ee755d941dbdb2f2e1db7d5962e4d46ef..a709653636061b028cf62f4026f9a318d9474859 100644
--- a/app/scodoc/sco_trombino_tours.py
+++ b/app/scodoc/sco_trombino_tours.py
@@ -196,9 +196,9 @@ def pdf_trombino_tours(
                             Paragraph(
                                 SU(
                                     "<para align=center><font size=8>"
-                                    + sco_etud.format_prenom(m["prenom"])
+                                    + scu.format_prenom(m["prenom"])
                                     + " "
-                                    + sco_etud.format_nom(m["nom"])
+                                    + scu.format_nom(m["nom"])
                                     + text_group
                                     + "</font></para>"
                                 ),
@@ -413,11 +413,7 @@ def pdf_feuille_releve_absences(
         for m in members:
             currow = [
                 Paragraph(
-                    SU(
-                        sco_etud.format_nom(m["nom"])
-                        + " "
-                        + sco_etud.format_prenom(m["prenom"])
-                    ),
+                    SU(scu.format_nom(m["nom"]) + " " + scu.format_prenom(m["prenom"])),
                     StyleSheet["Normal"],
                 )
             ]
diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py
index f93bf05269cde7f411f1bc015105536706bdad7b..35fb343ea55734ea9fc01aa9bd7ae209d7cfada4 100644
--- a/app/scodoc/sco_utils.py
+++ b/app/scodoc/sco_utils.py
@@ -64,6 +64,7 @@ from config import Config
 from app import log, ScoDocJSONEncoder
 
 from app.scodoc.codes_cursus import NOTES_TOLERANCE, CODES_EXPL
+from app.scodoc.sco_exceptions import ScoValueError
 from app.scodoc import sco_xml
 import sco_version
 
@@ -1139,6 +1140,61 @@ def abbrev_prenom(prenom):
     return abrv
 
 
+def format_civilite(civilite):
+    """returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
+    personne ne souhaitant pas d'affichage).
+    Raises ScoValueError if conversion fails.
+    """
+    try:
+        return {
+            "M": "M.",
+            "F": "Mme",
+            "X": "",
+        }[civilite]
+    except KeyError as exc:
+        raise ScoValueError(f"valeur invalide pour la civilité: {civilite}") from exc
+
+
+def format_nomprenom(etud, reverse=False):
+    """Formatte civilité/nom/prenom pour affichages: "M. Pierre Dupont"
+    Si reverse, "Dupont Pierre", sans civilité.
+
+    DEPRECATED: utiliser Identite.nomprenom
+    """
+    nom = etud.get("nom_disp", "") or etud.get("nom_usuel", "") or etud["nom"]
+    prenom = format_prenom(etud["prenom"])
+    civilite = format_civilite(etud["civilite"])
+    if reverse:
+        fs = [nom, prenom]
+    else:
+        fs = [civilite, prenom, nom]
+    return " ".join([x for x in fs if x])
+
+
+def format_nom(s, uppercase=True):
+    "Formatte le nom"
+    if not s:
+        return ""
+    if uppercase:
+        return s.upper()
+    else:
+        return format_prenom(s)
+
+
+def format_prenom(s):
+    """Formatte prenom etudiant pour affichage
+    DEPRECATED: utiliser Identite.prenom_str
+    """
+    if not s:
+        return ""
+    frags = s.split()
+    r = []
+    for frag in frags:
+        fs = frag.split("-")
+        r.append("-".join([x.lower().capitalize() for x in fs]))
+    return " ".join(r)
+
+
 #
 def timedate_human_repr():
     "representation du temps courant pour utilisateur"
@@ -1480,6 +1536,7 @@ def is_assiduites_module_forced(
 
 def get_assiduites_time_config(config_type: str) -> str:
     from app.models import ScoDocSiteConfig
+
     match config_type:
         case "matin":
             return ScoDocSiteConfig.get("assi_morning_time", "08:00:00")
diff --git a/tests/api/test_api_etudiants.py b/tests/api/test_api_etudiants.py
index e66490fc35d85481761d08624cc903481bdd9af6..bc4563cbed578db7eb1682749c68f21df17ec036 100644
--- a/tests/api/test_api_etudiants.py
+++ b/tests/api/test_api_etudiants.py
@@ -28,10 +28,11 @@ from tests.api.setup_test_api import (
     API_URL,
     API_USER_ADMIN,
     CHECK_CERTIFICATE,
+    DEPT_ACRONYM,
     POST_JSON,
-    api_headers,
     get_auth_headers,
 )
+from tests.api.setup_test_api import api_headers  # pylint: disable=unused-import
 from tests.api.tools_test_api import (
     BULLETIN_ETUDIANT_FIELDS,
     BULLETIN_FIELDS,
@@ -923,3 +924,20 @@ def test_etudiant_groups(api_headers):
     group = groups[0]
     fields_ok = verify_fields(group, fields)
     assert fields_ok is True
+
+
+def test_etudiant_create(api_headers):
+    """/etudiant/create"""
+    admin_header = get_auth_headers(API_USER_ADMIN, API_PASSWORD_ADMIN)
+    args = {
+        "prenom": "Carl Philipp Emanuel",
+        "nom": "Bach",
+        "dept": DEPT_ACRONYM,
+        "civilite": "M",
+    }
+    etud = POST_JSON(
+        "/etudiant/create",
+        args,
+        headers=admin_header,
+    )
+    assert etud["nom"] == args["nom"].upper()