diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index 09b08b2fb999d803e76e0898b1fe496c72796e38..185e18a4ff1e699dddd55d61256e1727441dc043 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -36,7 +36,7 @@ from app.models.config import ScoDocSiteConfig
 from app.models.departements import Departement
 from app.models.etudiants import Identite
 from app.models.evaluations import Evaluation
-from app.models.events import ScolarNews
+from app.models.events import Scolog, ScolarNews
 from app.models.formations import Formation
 from app.models.groups import GroupDescr, Partition
 from app.models.moduleimpls import (
@@ -45,9 +45,10 @@ from app.models.moduleimpls import (
     notes_modules_enseignants,
 )
 from app.models.modules import Module
+from app.models.scolar_event import ScolarEvent
 from app.models.ues import UniteEns
 from app.models.validations import ScolarFormSemestreValidation
-from app.scodoc import codes_cursus, sco_preferences
+from app.scodoc import codes_cursus, sco_cache, sco_preferences
 from app.scodoc.sco_exceptions import ScoValueError
 from app.scodoc.sco_permissions import Permission
 from app.scodoc.sco_utils import MONTH_NAMES_ABBREV, translate_assiduites_metric
@@ -69,6 +70,8 @@ class FormSemestre(models.ScoDocModel):
     formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
     semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
     titre = db.Column(db.Text(), nullable=False)
+    # nb max d'inscriptions (non DEM), null si illimité:
+    capacite_accueil = db.Column(db.Integer, nullable=True)
     date_debut = db.Column(db.Date(), nullable=False)
     date_fin = db.Column(db.Date(), nullable=False)  # jour inclus
     edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
@@ -1019,20 +1022,74 @@ class FormSemestre(models.ScoDocModel):
             codes |= {x.strip() for x in self.elt_passage_apo.split(",") if x}
         return codes
 
-    def get_inscrits(self, include_demdef=False, order=False) -> list[Identite]:
+    def get_inscrits(
+        self, include_demdef=False, order=False, etats: set | None = None
+    ) -> list[Identite]:
         """Liste des étudiants inscrits à ce semestre
         Si include_demdef, tous les étudiants, avec les démissionnaires
         et défaillants.
+        Si etats, seuls les étudiants dans l'un des états indiqués.
         Si order, tri par clé sort_key
         """
         if include_demdef:
             etuds = [ins.etud for ins in self.inscriptions]
-        else:
+        elif not etats:
             etuds = [ins.etud for ins in self.inscriptions if ins.etat == scu.INSCRIT]
+        else:
+            etuds = [ins.etud for ins in self.inscriptions if ins.etat in etats]
         if order:
             etuds.sort(key=lambda e: e.sort_key)
         return etuds
 
+    def inscrit_etudiant(
+        self,
+        etud: "Identite",
+        etat: str = scu.INSCRIT,
+        etape: str | None = None,
+        method: str | None = None,
+    ) -> "FormSemestreInscription":
+        """Inscrit l'étudiant au semestre, ou renvoie son inscription s'il l'est déjà.
+        Vérifie la capacité d'accueil si indiquée (non null): si le semestre est plein,
+        lève une exception. Génère un évènement et un log étudiant.
+        method: indique origine de l'inscription pour le log étudiant.
+        """
+        # remplace ancien do_formsemestre_inscription_create()
+        if not self.etat:  # check lock
+            raise ScoValueError("inscrit_etudiant: semestre verrouille")
+        inscr = FormSemestreInscription.query.filter_by(
+            formsemestre_id=self.id, etudid=etud.id
+        ).first()
+        if inscr is not None:
+            return inscr
+
+        if self.capacite_accueil is not None:
+            inscriptions = self.get_inscrits(etats={scu.INSCRIT, scu.DEMISSION})
+            if len(inscriptions) >= self.capacite_accueil:
+                raise ScoValueError(
+                    f"Semestre {self.titre} complet: {len(self.inscriptions)} inscrits"
+                )
+
+        inscr = FormSemestreInscription(
+            formsemestre_id=self.id, etudid=etud.id, etat=etat, etape=etape
+        )
+        db.session.add(inscr)
+        # Évènement
+        event = ScolarEvent(
+            etudid=etud.id,
+            formsemestre_id=self.id,
+            event_type="INSCRIPTION",
+        )
+        db.session.add(event)
+        # Log etudiant
+        Scolog.logdb(
+            method=method,
+            etudid=etud.id,
+            msg=f"inscription en semestre {self.titre_annee()}",
+            commit=True,
+        )
+        sco_cache.invalidate_formsemestre(formsemestre_id=self.id)
+        return inscr
+
     def get_partitions_list(
         self, with_default=True, only_listed=False
     ) -> list[Partition]:
diff --git a/app/scodoc/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py
index 00ea831e374c4ffc40867ec3cf32a38d8ed421a5..5131947ff46d6b4ee51cbe32be668fffe30a52a6 100644
--- a/app/scodoc/sco_formsemestre.py
+++ b/app/scodoc/sco_formsemestre.py
@@ -53,6 +53,7 @@ _formsemestreEditor = ndb.EditableTable(
         "semestre_id",
         "formation_id",
         "titre",
+        "capacite_accueil",
         "date_debut",
         "date_fin",
         "gestion_compensation",
diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py
index c4cb7b84a81cff03ef057f6898eced74854485f8..87cfadd005767b37fda56c2f1046f789d6920768 100644
--- a/app/scodoc/sco_formsemestre_edit.py
+++ b/app/scodoc/sco_formsemestre_edit.py
@@ -350,8 +350,6 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
                 "labels": modalites_titles,
             },
         ),
-    ]
-    modform.append(
         (
             "semestre_id",
             {
@@ -367,10 +365,21 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
                 "attributes": ['onchange="change_semestre_id();"'] if is_apc else "",
             },
         ),
-    )
+        (
+            "capacite_accueil",
+            {
+                "title": "Capacité d'accueil",
+                "size": 4,
+                "explanation": "laisser vide si pas de limite au nombre d'inscrits non démissionnaires",
+                "type": "int",
+                "allow_null": True,
+            },
+        ),
+    ]
     etapes = sco_portal_apogee.get_etapes_apogee_dept()
     # Propose les etapes renvoyées par le portail
-    # et ajoute les étapes du semestre qui ne sont pas dans la liste (soit la liste a changé, soit l'étape a été ajoutée manuellement)
+    # et ajoute les étapes du semestre qui ne sont pas dans la liste
+    # (soit la liste a changé, soit l'étape a été ajoutée manuellement)
     etapes_set = {et[0] for et in etapes}
     if edit:
         for etape_vdi in formsemestre.etapes_apo_vdi():
diff --git a/app/scodoc/sco_formsemestre_inscriptions.py b/app/scodoc/sco_formsemestre_inscriptions.py
index ec4bb9303b8057474f0a269bf8c908296d414d59..e7f0ef9c8d6ab93435105a12c220cfa66d3fde2b 100644
--- a/app/scodoc/sco_formsemestre_inscriptions.py
+++ b/app/scodoc/sco_formsemestre_inscriptions.py
@@ -85,43 +85,6 @@ def do_formsemestre_inscription_listinscrits(formsemestre_id):
     return r
 
 
-def do_formsemestre_inscription_create(args, method=None):
-    "create a formsemestre_inscription (and sco event)"
-    cnx = ndb.GetDBConnexion()
-    log(f"do_formsemestre_inscription_create: args={args}")
-    sems = sco_formsemestre.do_formsemestre_list(
-        {"formsemestre_id": args["formsemestre_id"]}
-    )
-    if len(sems) != 1:
-        raise ScoValueError(f"code de semestre invalide: {args['formsemestre_id']}")
-    sem = sems[0]
-    # check lock
-    if not sem["etat"]:
-        raise ScoValueError("inscription: semestre verrouille")
-    #
-    r = _formsemestre_inscriptionEditor.create(cnx, args)
-    # Evenement
-    sco_etud.scolar_events_create(
-        cnx,
-        args={
-            "etudid": args["etudid"],
-            "event_date": time.strftime(scu.DATE_FMT),
-            "formsemestre_id": args["formsemestre_id"],
-            "event_type": "INSCRIPTION",
-        },
-    )
-    # Log etudiant
-    Scolog.logdb(
-        method=method,
-        etudid=args["etudid"],
-        msg=f"inscription en semestre {args['formsemestre_id']}",
-        commit=True,
-    )
-    #
-    sco_cache.invalidate_formsemestre(formsemestre_id=args["formsemestre_id"])
-    return r
-
-
 def do_formsemestre_inscription_delete(oid, formsemestre_id=None):
     "delete formsemestre_inscription"
     cnx = ndb.GetDBConnexion()
@@ -283,20 +246,18 @@ def do_formsemestre_inscription_with_modules(
     """Inscrit cet etudiant à ce semestre et TOUS ses modules STANDARDS
     (donc sauf le sport)
     Si dept_id est spécifié, utilise ce département au lieu du courant.
+    Vérifie la capacité d'accueil.
     """
+    etud = Identite.get_etud(etudid)
     group_ids = group_ids or []
     if isinstance(group_ids, int):
         group_ids = [group_ids]
     # Check that all groups exist before creating the inscription
     groups = [GroupDescr.query.get_or_404(group_id) for group_id in group_ids]
     formsemestre = FormSemestre.get_formsemestre(formsemestre_id, dept_id=dept_id)
-    # inscription au semestre
+    # Inscription au semestre
     args = {"formsemestre_id": formsemestre_id, "etudid": etudid}
-    if etat is not None:
-        args["etat"] = etat
-    if etape is not None:
-        args["etape"] = etape
-    do_formsemestre_inscription_create(args, method=method)
+    formsemestre.inscrit_etudiant(etud, etat=etat, etape=etape, method=method)
     log(
         f"""do_formsemestre_inscription_with_modules: etudid={
             etudid} formsemestre_id={formsemestre_id}"""
diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py
index db7143e137eeb0e79d3f685c63987d8f7b9c3a81..1f45cd626815a671432f603a4ba94dffcb180202 100755
--- a/app/scodoc/sco_formsemestre_status.py
+++ b/app/scodoc/sco_formsemestre_status.py
@@ -978,8 +978,8 @@ def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None
         html_sco_header.html_sem_header(
             page_title, with_page_header=False, with_h2=False
         ),
-        f"""<table>
-          <tr><td class="fichetitre2">Formation: </td><td>
+        f"""<table class="formsemestre_status_head">
+          <tr><td class="fichetitre2">Formation&nbsp;: </td><td>
          <a href="{url_for('notes.ue_table',
          scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id)}"
          class="discretelink" title="Formation {
@@ -1002,15 +1002,24 @@ def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None
         sem_parcours = formsemestre.get_parcours_apc()
         H.append(
             f"""
-        <tr><td class="fichetitre2">Parcours: </td>
+        <tr><td class="fichetitre2">Parcours&nbsp;: </td>
         <td style="color: blue;">{', '.join(parcours.code for parcours in sem_parcours)}</td>
         </tr>
         """
         )
+    if formsemestre.capacite_accueil is not None:
+        H.append(
+            f"""
+        <tr><td class="fichetitre2">Capacité d'accueil&nbsp;: </td>
+        <td>{formsemestre.capacite_accueil}</td>
+        </tr>
+        """
+        )
 
     evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre)
     H.append(
-        '<tr><td class="fichetitre2">Évaluations: </td><td> %(nb_evals_completes)s ok, %(nb_evals_en_cours)s en cours, %(nb_evals_vides)s vides'
+        """<tr><td class="fichetitre2">Évaluations&nbsp;: </td>
+        <td> %(nb_evals_completes)s ok, %(nb_evals_en_cours)s en cours, %(nb_evals_vides)s vides"""
         % evals
     )
     if evals["last_modif"]:
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index b844a9d4291b061316bba55c6a83afb982b697c7..783ac66691ec5410d332c369313757da1853ea6a 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -1844,6 +1844,15 @@ div.formsemestre_status {
   /* EMO_WARNING, "&#9888;&#65039;"  */
 }
 
+table.formsemestre_status_head {
+  border-collapse: collapse;
+
+}
+
+table.formsemestre_status_head tr td:nth-child(2) {
+  padding-left: 1em;
+}
+
 table.formsemestre_status {
   border-collapse: collapse;
 }
diff --git a/migrations/versions/2640b7686de6_formsemestre_description.py b/migrations/versions/2640b7686de6_formsemestre_description.py
index 88ec4388460c897ce75e9c80ed9cc094374b541d..c275d3e2239ed354df81a8aed5e7bbde33896fd5 100644
--- a/migrations/versions/2640b7686de6_formsemestre_description.py
+++ b/migrations/versions/2640b7686de6_formsemestre_description.py
@@ -1,4 +1,4 @@
-"""FormSemestreDescription
+"""FormSemestreDescription et capacité d'accueil
 
 Revision ID: 2640b7686de6
 Revises: f6cb3d4e44ec
@@ -32,7 +32,11 @@ def upgrade():
         ),
         sa.PrimaryKeyConstraint("id"),
     )
+    with op.batch_alter_table("notes_formsemestre", schema=None) as batch_op:
+        batch_op.add_column(sa.Column("capacite_accueil", sa.Integer(), nullable=True))
 
 
 def downgrade():
+    with op.batch_alter_table("notes_formsemestre", schema=None) as batch_op:
+        batch_op.drop_column("capacite_accueil")
     op.drop_table("notes_formsemestre_description")