diff --git a/app/models/but_validations.py b/app/models/but_validations.py
index 8c23694d38a9266611cb52c89a948639ffa31ff2..adf0d203fbcf6fafb280587da15d42ede920afcc 100644
--- a/app/models/but_validations.py
+++ b/app/models/but_validations.py
@@ -113,6 +113,12 @@ class ApcValidationRCUE(db.Model):
             "formsemestre_id": self.formsemestre_id,
         }
 
+    def get_codes_apogee(self) -> set[str]:
+        """Les codes Apogée associés à cette validation RCUE.
+        Prend les codes des deux UEs
+        """
+        return self.ue1.get_codes_apogee_rcue() | self.ue2.get_codes_apogee_rcue()
+
 
 class ApcValidationAnnee(db.Model):
     """Validation des années du BUT"""
diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index c3623bb3d255c6e0c66f53a445b7eaf35a12fba6..39c657aa9c043de9c85365ef26dcb56ea3e375f2 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -610,6 +610,41 @@ class FormSemestre(models.ScoDocModel):
             )
         )
 
+    @classmethod
+    def est_in_semestre_scolaire(
+        cls,
+        date_debut: datetime.date,
+        year=False,
+        periode=None,
+        mois_pivot_annee=scu.MONTH_DEBUT_ANNEE_SCOLAIRE,
+        mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2,
+    ) -> bool:
+        """Vrai si la date_debut est dans la période indiquée (1,2,0)
+        du semestre `periode` de l'année scolaire indiquée
+        (ou, à défaut, de celle en cours).
+
+        La période utilise les même conventions que semset["sem_id"];
+        * 1 : première période
+        * 2 : deuxième période
+        * 0 ou période non précisée: annualisé (donc inclut toutes les périodes)
+        )
+        """
+        if not year:
+            year = scu.annee_scolaire()
+        # n'utilise pas le jour pivot
+        jour_pivot_annee = jour_pivot_periode = 1
+        # calcule l'année universitaire et la période
+        sem_annee, sem_periode = cls.comp_periode(
+            date_debut,
+            mois_pivot_annee,
+            mois_pivot_periode,
+            jour_pivot_annee,
+            jour_pivot_periode,
+        )
+        if periode is None or periode == 0:
+            return sem_annee == year
+        return sem_annee == year and sem_periode == periode
+
     def est_terminal(self) -> bool:
         "Vrai si dernier semestre de son cursus (ou formation mono-semestre)"
         return (self.semestre_id < 0) or (
@@ -1225,9 +1260,17 @@ class FormSemestreEtape(db.Model):
         "Etape False if code empty"
         return self.etape_apo is not None and (len(self.etape_apo) > 0)
 
+    def __eq__(self, other):
+        if isinstance(other, ApoEtapeVDI):
+            return self.as_apovdi() == other
+        return str(self) == str(other)
+
     def __repr__(self):
         return f"<Etape {self.id} apo={self.etape_apo!r}>"
 
+    def __str__(self):
+        return self.etape_apo or ""
+
     def as_apovdi(self) -> ApoEtapeVDI:
         return ApoEtapeVDI(self.etape_apo)
 
@@ -1381,8 +1424,9 @@ class FormSemestreInscription(db.Model):
 
     def __repr__(self):
         return f"""<{self.__class__.__name__} {self.id} etudid={self.etudid} sem={
-            self.formsemestre_id} etat={self.etat} {
-            ('parcours='+str(self.parcour)) if self.parcour else ''}>"""
+            self.formsemestre_id} (S{self.formsemestre.semestre_id}) etat={self.etat} {
+            ('parcours="'+str(self.parcour.code)+'"') if self.parcour else ''
+            } {('etape="'+self.etape+'"') if self.etape else ''}>"""
 
 
 class NotesSemSet(db.Model):
diff --git a/app/models/ues.py b/app/models/ues.py
index 51467c7e2be5e418abe91768444e3c1adf9f592e..d6282fc7d472f0bc91b1421c0b3f099c19cbab40 100644
--- a/app/models/ues.py
+++ b/app/models/ues.py
@@ -276,6 +276,12 @@ class UniteEns(models.ScoDocModel):
             return {x.strip() for x in self.code_apogee.split(",") if x}
         return set()
 
+    def get_codes_apogee_rcue(self) -> set[str]:
+        """Les codes Apogée RCUE (codés en base comme "VRT1,VRT2")"""
+        if self.code_apogee_rcue:
+            return {x.strip() for x in self.code_apogee_rcue.split(",") if x}
+        return set()
+
     def _parcours_niveaux_ids(self, parcours=list[ApcParcours]) -> set[int]:
         """set des ids de niveaux communs à tous les parcours listés"""
         return set.intersection(
diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py
index 7feea971a16adf1e234bdae71742a4fcbf809c25..80d1e8e43ccb674ead171937bcbd04329341c289 100644
--- a/app/scodoc/sco_apogee_csv.py
+++ b/app/scodoc/sco_apogee_csv.py
@@ -43,12 +43,11 @@ import re
 import time
 from zipfile import ZipFile
 
-from flask import send_file
+from flask import g, send_file
 import numpy as np
 
 
 from app import log
-from app.but import jury_but
 from app.comp import res_sem
 from app.comp.res_compat import NotesTableCompat
 from app.models import (
@@ -79,7 +78,6 @@ from app.scodoc.codes_cursus import (
 )
 from app.scodoc import sco_cursus
 from app.scodoc import sco_formsemestre
-from app.scodoc import sco_etud
 
 
 def _apo_fmt_note(note, fmt="%3.2f"):
@@ -99,7 +97,7 @@ class EtuCol:
     """Valeurs colonnes d'un element pour un etudiant"""
 
     def __init__(self, nip, apo_elt, init_vals):
-        pass  # XXX
+        pass
 
 
 ETUD_OK = "ok"
@@ -150,9 +148,9 @@ class ApoEtud(dict):
             _apo_fmt_note, fmt=ScoDocSiteConfig.get_code_apo("NOTES_FMT") or "3.2f"
         )
         # Initialisés par associate_sco:
-        self.autre_sem: dict = None
+        self.autre_formsemestre: FormSemestre = None
         self.autre_res: NotesTableCompat = None
-        self.cur_sem: dict = None
+        self.cur_formsemestre: FormSemestre = None
         self.cur_res: NotesTableCompat = None
         self.new_cols = {}
         "{ col_id : value to record in csv }"
@@ -222,7 +220,8 @@ class ApoEtud(dict):
                         self.new_cols[col_id] = self.cols[col_id]
                     except KeyError as exc:
                         raise ScoFormatError(
-                            f"""Fichier Apogee invalide : ligne mal formatée ? <br>colonne <tt>{col_id}</tt> non déclarée ?"""
+                            f"""Fichier Apogee invalide : ligne mal formatée ? <br>colonne <tt>{
+                                col_id}</tt> non déclarée ?"""
                         ) from exc
                 else:
                     try:
@@ -248,7 +247,7 @@ class ApoEtud(dict):
     #     codes = set([apo_data.apo_csv.cols[col_id].code for col_id in apo_data.apo_csv.col_ids])
     #     return codes - set(sco_elts)
 
-    def search_elt_in_sem(self, code, sem) -> dict:
+    def search_elt_in_sem(self, code: str, sem: dict) -> dict:
         """
         VET code jury etape (en BUT, le code annuel)
         ELP élément pédagogique: UE, module
@@ -263,8 +262,8 @@ class ApoEtud(dict):
            sem (dict): semestre dans lequel on cherche l'élément
 
         Utilise notamment:
-           cur_sem (dict): semestre "courant" pour résultats annuels (VET)
-           autre_sem (dict): autre semestre utilisé pour calculer les résultats annuels (VET)
+           cur_formsemestre    : semestre "courant" pour résultats annuels (VET)
+           autre_formsemestre  : autre formsemestre utilisé pour les résultats annuels (VET)
 
         Returns:
            dict: with N, B, J, R keys, ou None si elt non trouvé
@@ -314,10 +313,10 @@ class ApoEtud(dict):
             code in {x.strip() for x in sem["elt_annee_apo"].split(",")}
         ):
             export_res_etape = self.export_res_etape
-            if (not export_res_etape) and self.cur_sem:
+            if (not export_res_etape) and self.cur_formsemestre:
                 # exporte toujours le résultat de l'étape si l'étudiant est diplômé
                 Se = sco_cursus.get_situation_etud_cursus(
-                    self.etud, self.cur_sem["formsemestre_id"]
+                    self.etud, self.cur_formsemestre.id
                 )
                 export_res_etape = Se.all_other_validated()
 
@@ -375,8 +374,20 @@ class ApoEtud(dict):
 
         if module_code_found:
             return VOID_APO_RES
+
         # RCUE du BUT
-        deca = jury_but.DecisionsProposeesAnnee(self.etud, formsemestre)
+        if res.is_apc:
+            for val_rcue in ApcValidationRCUE.query.filter_by(
+                etudid=etudid, formsemestre_id=sem["formsemestre_id"]
+            ):
+                if code in val_rcue.get_codes_apogee():
+                    return dict(
+                        N="",  # n'exporte pas de moyenne RCUE
+                        B=20,
+                        J="",
+                        R=ScoDocSiteConfig.get_code_apo(val_rcue.code),
+                        M="",
+                    )
         #
         return None  # element Apogee non trouvé dans ce semestre
 
@@ -418,11 +429,10 @@ class ApoEtud(dict):
         #
         #    XXX cette règle est discutable, à valider
 
-        # log('comp_elt_annuel cur_sem=%s autre_sem=%s' % (cur_sem['formsemestre_id'], autre_sem['formsemestre_id']))
-        if not self.cur_sem:
+        if not self.cur_formsemestre:
             # l'étudiant n'a pas de semestre courant ?!
             self.log.append("pas de semestre courant")
-            log(f"comp_elt_annuel: etudid {etudid} has no cur_sem")
+            log(f"comp_elt_annuel: etudid {etudid} has no cur_formsemestre")
             return VOID_APO_RES
 
         if self.is_apc:
@@ -438,7 +448,7 @@ class ApoEtud(dict):
                 # ne touche pas aux RATs
                 return VOID_APO_RES
 
-        if not self.autre_sem:
+        if not self.autre_formsemestre:
             # formations monosemestre, ou code VET semestriel,
             # ou jury intermediaire et etudiant non redoublant...
             return self.comp_elt_semestre(self.cur_res, cur_decision, etudid)
@@ -518,7 +528,7 @@ class ApoEtud(dict):
         self.validation_annee_but: ApcValidationAnnee = (
             ApcValidationAnnee.query.filter_by(
                 formsemestre_id=formsemestre.id,
-                etudid=self.etud["etudid"],
+                etudid=self.etud.id,
                 referentiel_competence_id=self.cur_res.formsemestre.formation.referentiel_competence_id,
             ).first()
         )
@@ -527,7 +537,7 @@ class ApoEtud(dict):
         )
 
     def etud_set_semestres_de_etape(self, apo_data: "ApoData"):
-        """Set .cur_sem and .autre_sem et charge les résultats.
+        """Set .cur_formsemestre and .autre_formsemestre et charge les résultats.
         Lorsqu'on a une formation semestrialisée mais avec un code étape annuel,
         il faut considérer les deux semestres ((S1,S2) ou (S3,S4)) pour calculer
         le code annuel (VET ou VRT1A (voir elt_annee_apo)).
@@ -535,52 +545,49 @@ class ApoEtud(dict):
         Pour les jurys intermediaires (janvier, S1 ou S3):  (S2 ou S4) de la même
         étape lors d'une année précédente ?
 
-        Set cur_sem: le semestre "courant" et autre_sem, ou None s'il n'y en a pas.
+        Set cur_formsemestre: le formsemestre "courant"
+        et autre_formsemestre, ou None s'il n'y en a pas.
         """
-        # Cherche le semestre "courant":
-        cur_sems = [
-            sem
-            for sem in self.etud["sems"]
+        # Cherche le formsemestre "courant":
+        cur_formsemestres = [
+            formsemestre
+            for formsemestre in self.etud.get_formsemestres()
             if (
-                (sem["semestre_id"] == apo_data.cur_semestre_id)
-                and (apo_data.etape in sem["etapes"])
+                (formsemestre.semestre_id == apo_data.cur_semestre_id)
+                and (apo_data.etape in formsemestre.etapes)
                 and (
-                    sco_formsemestre.sem_in_semestre_scolaire(
-                        sem,
+                    FormSemestre.est_in_semestre_scolaire(
+                        formsemestre.date_debut,
                         apo_data.annee_scolaire,
                         0,  # annee complete
                     )
                 )
             )
         ]
-        if not cur_sems:
-            cur_sem = None
-        else:
-            # prend le plus recent avec decision
-            cur_sem = None
-            for sem in cur_sems:
-                formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
+        cur_formsemestre = None
+        if cur_formsemestres:
+            # prend le plus récent avec décision
+            for formsemestre in cur_formsemestres:
                 res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
-                has_decision = res.etud_has_decision(self.etud["etudid"])
+                has_decision = res.etud_has_decision(self.etud.id)
                 if has_decision:
-                    cur_sem = sem
+                    cur_formsemestre = formsemestre
                     self.cur_res = res
                     break
-            if cur_sem is None:
-                cur_sem = cur_sems[0]  # aucun avec décision, prend le plus recent
-                if res.formsemestre.id == cur_sem["formsemestre_id"]:
+            if cur_formsemestres is None:
+                cur_formsemestre = cur_formsemestres[
+                    0
+                ]  # aucun avec décision, prend le plus recent
+                if res.formsemestre.id == cur_formsemestre.id:
                     self.cur_res = res
                 else:
-                    formsemestre = FormSemestre.query.get_or_404(
-                        cur_sem["formsemestre_id"]
-                    )
-                    self.cur_res = res_sem.load_formsemestre_results(formsemestre)
+                    self.cur_res = res_sem.load_formsemestre_results(cur_formsemestre)
 
-        self.cur_sem = cur_sem
+        self.cur_formsemestre = cur_formsemestre
 
         if apo_data.cur_semestre_id <= 0:
-            # "autre_sem" non pertinent pour sessions sans semestres:
-            self.autre_sem = None
+            # autre_formsemestre non pertinent pour sessions sans semestres:
+            self.autre_formsemestre = None
             self.autre_res = None
             return
 
@@ -601,52 +608,49 @@ class ApoEtud(dict):
                 courant_mois_debut = 1  # ou 2 (fev-jul)
             else:
                 raise ValueError("invalid periode value !")  # bug ?
-            courant_date_debut = "%d-%02d-01" % (
-                courant_annee_debut,
-                courant_mois_debut,
+            courant_date_debut = datetime.date(
+                day=1, month=courant_mois_debut, year=courant_annee_debut
             )
         else:
-            courant_date_debut = "9999-99-99"
+            courant_date_debut = datetime.date(day=31, month=12, year=9999)
 
-        # etud['sems'] est la liste des semestres de l'étudiant, triés par date,
-        # le plus récemment effectué en tête.
         # Cherche les semestres (antérieurs) de l'indice autre de la même étape apogée
         # s'il y en a plusieurs, choisit le plus récent ayant une décision
 
         autres_sems = []
-        for sem in self.etud["sems"]:
+        for formsemestre in self.etud.get_formsemestres():
             if (
-                sem["semestre_id"] == autre_semestre_id
-                and apo_data.etape_apogee in sem["etapes"]
+                formsemestre.semestre_id == autre_semestre_id
+                and apo_data.etape_apogee in formsemestre.etapes
             ):
                 if (
-                    sem["date_debut_iso"] < courant_date_debut
+                    formsemestre.date_debut < courant_date_debut
                 ):  # on demande juste qu'il ait démarré avant
-                    autres_sems.append(sem)
+                    autres_sems.append(formsemestre)
         if not autres_sems:
-            autre_sem = None
+            autre_formsemestre = None
         elif len(autres_sems) == 1:
-            autre_sem = autres_sems[0]
+            autre_formsemestre = autres_sems[0]
         else:
-            autre_sem = None
-            for sem in autres_sems:
-                formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
+            autre_formsemestre = None
+            for formsemestre in autres_sems:
                 res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
                 if res.is_apc:
-                    has_decision = res.etud_has_decision(self.etud["etudid"])
+                    has_decision = res.etud_has_decision(self.etud.id)
                 else:
-                    has_decision = res.get_etud_decision_sem(self.etud["etudid"])
+                    has_decision = res.get_etud_decision_sem(self.etud.id)
                 if has_decision:
-                    autre_sem = sem
+                    autre_formsemestre = formsemestre
                     break
-            if autre_sem is None:
-                autre_sem = autres_sems[0]  # aucun avec decision, prend le plus recent
+            if autre_formsemestre is None:
+                autre_formsemestre = autres_sems[
+                    0
+                ]  # aucun avec decision, prend le plus recent
 
-        self.autre_sem = autre_sem
+        self.autre_formsemestre = autre_formsemestre
         # Charge les résultats:
-        if autre_sem:
-            formsemestre = FormSemestre.query.get_or_404(autre_sem["formsemestre_id"])
-            self.autre_res = res_sem.load_formsemestre_results(formsemestre)
+        if autre_formsemestre:
+            self.autre_res = res_sem.load_formsemestre_results(self.autre_formsemestre)
         else:
             self.autre_res = None
 
@@ -873,6 +877,16 @@ class ApoData:
             codes_ues = set().union(
                 *[ue.get_codes_apogee() for ue in formsemestre.get_ues(with_sport=True)]
             )
+            codes_rcues = (
+                set().union(
+                    *[
+                        ue.get_codes_apogee_rcue()
+                        for ue in formsemestre.get_ues(with_sport=True)
+                    ]
+                )
+                if self.is_apc
+                else set()
+            )
             s = set()
             codes_by_sem[sem["formsemestre_id"]] = s
             for col_id in self.apo_csv.col_ids[4:]:
@@ -885,9 +899,14 @@ class ApoData:
                 if code in codes_ues:
                     s.add(code)
                     continue
+                # associé à un RCUE BUT
+                if code in codes_rcues:
+                    s.add(code)
+                    continue
                 # associé à un module:
                 if code in codes_modules:
                     s.add(code)
+
         # log('codes_by_sem=%s' % pprint.pformat(codes_by_sem))
         return codes_by_sem
 
diff --git a/app/scodoc/sco_archives.py b/app/scodoc/sco_archives.py
index 56d501ab6c654c9c43087cf727b458f6ee6d76ea..44acd08ae345e8176513bb538fbfe59029bee062 100644
--- a/app/scodoc/sco_archives.py
+++ b/app/scodoc/sco_archives.py
@@ -139,7 +139,7 @@ class BaseArchiver:
         dirs = glob.glob(base + "*")
         return [os.path.split(x)[1] for x in dirs]
 
-    def list_obj_archives(self, oid: int, dept_id: int = None):
+    def list_obj_archives(self, oid: int, dept_id: int = None) -> list[str]:
         """Returns
         :return: list of archive identifiers for this object (paths to non empty dirs)
         """
diff --git a/app/scodoc/sco_cursus_dut.py b/app/scodoc/sco_cursus_dut.py
index 6c1344a4b8a574cf0def22e4e398de52532cffe1..7cb14eb1c940bf6054743da0cd6b8f27aea428e3 100644
--- a/app/scodoc/sco_cursus_dut.py
+++ b/app/scodoc/sco_cursus_dut.py
@@ -291,14 +291,14 @@ class SituationEtudCursusClassic(SituationEtudCursus):
         if s_idx == NO_SEMESTRE_ID or s_idx > self.parcours.NB_SEM - 2:
             return False  # n+2 en dehors du parcours
         if self._sem_list_validated(set(range(1, s_idx))):
-            # antérieurs validé, teste suivant
+            # antérieurs validés, teste suivant
             n1 = s_idx + 1
-            for sem in self.get_semestres():
+            for formsemestre in self.formsemestres:
                 if (
-                    sem["semestre_id"] == n1
-                    and sem["formation_code"] == self.formation.formation_code
+                    formsemestre.semestre_id == n1
+                    and formsemestre.formation.formation_code
+                    == self.formation.formation_code
                 ):
-                    formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
                     nt: NotesTableCompat = res_sem.load_formsemestre_results(
                         formsemestre
                     )
diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py
index 870421ea613d9e3a141bb30b46448575bf78008d..152103aef1da016f15a90ef02dea9043e289ebcd 100644
--- a/app/scodoc/sco_edit_ue.py
+++ b/app/scodoc/sco_edit_ue.py
@@ -84,6 +84,7 @@ _ueEditor = ndb.EditableTable(
         "ects",
         "is_external",
         "code_apogee",
+        "code_apogee_rcue",
         "coefficient",
         "coef_rcue",
         "color",
@@ -425,6 +426,20 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
                 "max_length": APO_CODE_STR_LEN,
             },
         ),
+    ]
+    if is_apc:
+        form_descr += [
+            (
+                "code_apogee_rcue",
+                {
+                    "title": "Code Apogée du RCUE",
+                    "size": 25,
+                    "explanation": "(optionnel) code(s) élément pédagogique Apogée du RCUE",
+                    "max_length": APO_CODE_STR_LEN,
+                },
+            ),
+        ]
+    form_descr += [
         (
             "is_external",
             {
diff --git a/app/scodoc/sco_etape_apogee.py b/app/scodoc/sco_etape_apogee.py
index a4fcdb02f9a9af24862d54082d3a778168279850..e11e5713001ec9da1824b983998c2761a6b6d504 100644
--- a/app/scodoc/sco_etape_apogee.py
+++ b/app/scodoc/sco_etape_apogee.py
@@ -247,9 +247,7 @@ def apo_csv_check_etape(semset, set_nips, etape_apo):
     return nips_ok, apo_nips, nips_no_apo, nips_no_sco, maq_elems, sem_elems
 
 
-def apo_csv_semset_check(
-    semset, allow_missing_apo=False, allow_missing_csv=False
-):  # was apo_csv_check
+def apo_csv_semset_check(semset, allow_missing_apo=False, allow_missing_csv=False):
     """
     check students in stored maqs vs students in semset
       Cas à détecter:
@@ -346,120 +344,3 @@ def apo_csv_retreive_etuds_by_nip(semset, nips):
         etuds[nip] = apo_etuds_by_nips.get(nip, {"nip": nip, "etape_apo": "?"})
 
     return etuds
-
-
-"""
-Tests:
-
-from debug import *
-from app.scodoc import sco_groups
-from app.scodoc import sco_groups_view
-from app.scodoc import sco_formsemestre
-from app.scodoc.sco_etape_apogee import *
-from app.scodoc.sco_apogee_csv import *
-from app.scodoc.sco_semset import *
-
-app.set_sco_dept('RT')
-csv_data = open('/opt/misc/VDTRT_V1RT.TXT').read()
-annee_scolaire=2015
-sem_id=1
-
-apo_data = sco_apogee_csv.ApoData(csv_data, periode=sem_id)
-print apo_data.etape_apogee
-
-apo_data.setup()
-e = apo_data.etuds[0]
-e.lookup_scodoc( apo_data.etape_formsemestre_ids)
-e.associate_sco( apo_data)
-
-print apo_csv_list_stored_archives()
-
-
-# apo_csv_store(csv_data, annee_scolaire, sem_id)
-
-
-
-groups_infos = sco_groups_view.DisplayedGroupsInfos( [sco_groups.get_default_group(formsemestre_id)], formsemestre_id=formsemestre_id)
-
-formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
-nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
-#
-s = SemSet('NSS29902')
-apo_data = sco_apogee_csv.ApoData(open('/opt/scodoc/var/scodoc/archives/apo_csv/RT/2015-2/2016-07-10-11-26-15/V1RT.csv').read(), periode=1)
-
-# cas Tiziri K. (inscrite en S1, démission en fin de S1, pas inscrite en S2)
-# => pas de décision, ce qui est voulu (?)
-#
-
-apo_data.setup()
-e = [ e for e in apo_data.etuds if e['nom'] == 'XYZ' ][0]
-e.lookup_scodoc( apo_data.etape_formsemestre_ids)
-e.associate_sco(apo_data)
-
-self=e
-col_id='apoL_c0129'
-
-# --
-from app.scodoc import sco_portal_apogee
-_ = go_dept(app, 'GEA').Notes
-#csv_data = sco_portal_apogee.get_maquette_apogee(etape='V1GE', annee_scolaire=2015)
-csv_data = open('/tmp/V1GE.txt').read()
-apo_data = sco_apogee_csv.ApoData(csv_data, periode=1)
-
-
-# ------
-# les elements inconnus:
-
-from debug import *
-from app.scodoc import sco_groups
-from app.scodoc import sco_groups_view
-from app.scodoc import sco_formsemestre
-from app.scodoc.sco_etape_apogee import *
-from app.scodoc.sco_apogee_csv import *
-from app.scodoc.sco_semset import *
-
-_ = go_dept(app, 'RT').Notes
-csv_data = open('/opt/misc/V2RT.csv').read()
-annee_scolaire=2015
-sem_id=1
-
-apo_data = sco_apogee_csv.ApoData(csv_data, periode=1)
-print apo_data.etape_apogee
-
-apo_data.setup()
-for e in apo_data.etuds:
-    e.lookup_scodoc( apo_data.etape_formsemestre_ids)
-    e.associate_sco(apo_data)
-
-# ------
-# test export jury intermediaire
-from debug import *
-from app.scodoc import sco_groups
-from app.scodoc import sco_groups_view
-from app.scodoc import sco_formsemestre
-from app.scodoc.sco_etape_apogee import *
-from app.scodoc.sco_apogee_csv import *
-from app.scodoc.sco_semset import *
-
-_ = go_dept(app, 'CJ').Notes
-csv_data = open('/opt/scodoc/var/scodoc/archives/apo_csv/CJ/2016-1/2017-03-06-21-46-32/V1CJ.csv').read()
-annee_scolaire=2016
-sem_id=1
-
-apo_data = sco_apogee_csv.ApoData(csv_data, periode=1)
-print apo_data.etape_apogee
-
-apo_data.setup()
-e = [ e for e in apo_data.etuds if e['nom'] == 'XYZ' ][0] #
-e.lookup_scodoc( apo_data.etape_formsemestre_ids)
-e.associate_sco(apo_data)
-
-self=e
-
-sco_elts = {}
-col_id='apoL_c0001'
-code = apo_data.cols[col_id]['Code'] # 'V1RT'
-
-sem = apo_data.sems_periode[0] # le S1
-
-"""
diff --git a/app/scodoc/sco_etape_apogee_view.py b/app/scodoc/sco_etape_apogee_view.py
index 158fe9aeaec17db49db49fae9942af1a67db0ba4..9e4611990eb421388095d3b35dbbf32a865cbde6 100644
--- a/app/scodoc/sco_etape_apogee_view.py
+++ b/app/scodoc/sco_etape_apogee_view.py
@@ -125,14 +125,19 @@ def apo_semset_maq_status(
         H.append("""<p><em>Aucune maquette chargée</em></p>""")
     # Upload fichier:
     H.append(
-        """<form id="apo_csv_add" action="view_apo_csv_store" method="post" enctype="multipart/form-data">
-        Charger votre fichier maquette Apogée:
+        f"""<form id="apo_csv_add" action="view_apo_csv_store"
+            method="post" enctype="multipart/form-data"
+            style="margin-bottom: 8px;"
+            >
+        <div style="margin-top: 12px; margin-bottom: 8px;">
+        {'Charger votre fichier' if tab_archives.is_empty() else 'Ajouter un autre fichier'}
+        maquette Apogée:
+        </div>
         <input type="file" size="30" name="csvfile"/>
-        <input type="hidden" name="semset_id" value="%s"/>
+        <input type="hidden" name="semset_id" value="{semset_id}"/>
         <input type="submit" value="Ajouter ce fichier"/>
         <input type="checkbox" name="autodetect" checked/>autodétecter encodage</input>
         </form>"""
-        % (semset_id,)
     )
     # Récupération sur portail:
     maquette_url = sco_portal_apogee.get_maquette_url()
@@ -335,7 +340,7 @@ def apo_semset_maq_status(
     missing = maq_elems - sem_elems
     H.append('<div id="apo_elements">')
     H.append(
-        '<p>Elements Apogée: <span class="apo_elems">%s</span></p>'
+        '<p>Élements Apogée: <span class="apo_elems">%s</span></p>'
         % ", ".join(
             [
                 e if not e in missing else '<span class="missing">' + e + "</span>"
@@ -351,7 +356,7 @@ def apo_semset_maq_status(
         ]
         H.append(
             f"""<div class="apo_csv_status_missing_elems">
-            <span class="fontred">Elements Apogée absents dans ScoDoc: </span>
+            <span class="fontred">Élements Apogée absents dans ScoDoc: </span>
             <span class="apo_elems fontred">{
                 ", ".join(sorted(missing))
             }</span>
@@ -442,11 +447,11 @@ def table_apo_csv_list(semset):
     annee_scolaire = semset["annee_scolaire"]
     sem_id = semset["sem_id"]
 
-    T = sco_etape_apogee.apo_csv_list_stored_archives(
+    rows = sco_etape_apogee.apo_csv_list_stored_archives(
         annee_scolaire, sem_id, etapes=semset.list_etapes()
     )
 
-    for t in T:
+    for t in rows:
         # Ajoute qq infos pour affichage:
         csv_data = sco_etape_apogee.apo_csv_get(t["etape_apo"], annee_scolaire, sem_id)
         apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])
@@ -484,7 +489,7 @@ def table_apo_csv_list(semset):
             "date_str": "Enregistré le",
         },
         columns_ids=columns_ids,
-        rows=T,
+        rows=rows,
         html_class="table_leftalign apo_maq_list",
         html_sortable=True,
         # base_url = '%s?formsemestre_id=%s' % (request.base_url, formsemestre_id),
diff --git a/app/scodoc/sco_etape_bilan.py b/app/scodoc/sco_etape_bilan.py
index fec1cb2d08157ebbf1a0c0d765283c53118f593e..fb2c8bdf46945dbf09237cbd92e6a8447b0bd5a7 100644
--- a/app/scodoc/sco_etape_bilan.py
+++ b/app/scodoc/sco_etape_bilan.py
@@ -93,7 +93,7 @@ import json
 
 from flask import url_for, g
 
-from app.scodoc.sco_portal_apogee import get_inscrits_etape
+from app.scodoc import sco_portal_apogee
 from app import log
 from app.scodoc.sco_utils import annee_scolaire_debut
 from app.scodoc.gen_tables import GenTable
@@ -136,11 +136,16 @@ class DataEtudiant(object):
         self.etudid = etudid
         self.data_apogee = None
         self.data_scodoc = None
-        self.etapes = set()  # l'ensemble des étapes où il est inscrit
-        self.semestres = set()  # l'ensemble des formsemestre_id où il est inscrit
-        self.tags = set()  # les anomalies relevées
-        self.ind_row = "-"  # là où il compte dans les effectifs (ligne et colonne)
+        self.etapes = set()
+        "l'ensemble des étapes où il est inscrit"
+        self.semestres = set()
+        "l'ensemble des formsemestre_id où il est inscrit"
+        self.tags = set()
+        "les anomalies relevées"
+        self.ind_row = "-"
+        "ligne où il compte dans les effectifs"
         self.ind_col = "-"
+        "colonne où il compte dans les effectifs"
 
     def add_etape(self, etape):
         self.etapes.add(etape)
@@ -163,9 +168,9 @@ class DataEtudiant(object):
     def set_ind_col(self, indicatif):
         self.ind_col = indicatif
 
-    def get_identity(self):
+    def get_identity(self) -> str:
         """
-        Calcul le nom/prénom de l'étudiant (données ScoDoc en priorité, sinon données Apogée)
+        Calcule le nom/prénom de l'étudiant (données ScoDoc en priorité, sinon données Apogée)
         :return: L'identité calculée
         """
         if self.data_scodoc is not None:
@@ -176,9 +181,12 @@ class DataEtudiant(object):
 
 def _help() -> str:
     return """
-    <div id="export_help" class="pas_help"> <span>Explications sur les tableaux des effectifs et liste des
-    étudiants</span>
-        <div> <p>Le tableau des effectifs présente le nombre d'étudiants selon deux critères:</p>
+    <div id="export_help" class="pas_help">
+    <span>Explications sur les tableaux des effectifs
+    et liste des étudiants</span>
+        <div>
+            <p>Le tableau des effectifs présente le nombre d'étudiants selon deux critères:
+            </p>
         <ul>
         <li>En colonne le statut de l'étudiant par rapport à Apogée:
             <ul>
@@ -406,7 +414,8 @@ class EtapeBilan:
         for key_etape in self.etapes:
             annee_apogee, etapestr = key_to_values(key_etape)
             self.etu_etapes[key_etape] = set()
-            for etud in get_inscrits_etape(etapestr, annee_apogee):
+            # get_inscrits_etape interroge portail Apo:
+            for etud in sco_portal_apogee.get_inscrits_etape(etapestr, annee_apogee):
                 key_etu = self.register_etud_apogee(etud, key_etape)
                 self.etu_etapes[key_etape].add(key_etu)
 
@@ -444,7 +453,6 @@ class EtapeBilan:
                 data_etu = self.etudiants[key_etu]
                 ind_col = "-"
                 ind_row = "-"
-
                 # calcul de la colonne
                 if len(data_etu.etapes) == 1:
                     ind_col = self.indicatifs[list(data_etu.etapes)[0]]
@@ -478,32 +486,34 @@ class EtapeBilan:
         affichage de l'html
         :return: Le code html à afficher
         """
+        if not sco_portal_apogee.has_portal():
+            return """<div id="synthese" class="semset_description">
+            <em>Pas de portail Apogée configuré</em>
+            </div>"""
         self.load_listes()  # chargement des données
         self.dispatch()  # analyse et répartition
         # calcul de la liste des colonnes et des lignes de la table des effectifs
         self.all_rows_str = "'" + ",".join(["." + r for r in self.all_rows_ind]) + "'"
         self.all_cols_str = "'" + ",".join(["." + c for c in self.all_cols_ind]) + "'"
 
-        H = [
-            """<div id="synthese" class="semset_description">
+        return f"""
+        <div id="synthese" class="semset_description">
             <details open="true">
-            <summary><b>Tableau des effectifs</b>
-            </summary>
-            """,
-            self._diagtable(),
-            """</details>""",
-            self.display_tags(),
-            """<details open="true">
-            <summary><b id="effectifs">Liste des étudiants <span id="compte"></span></b>
-            </summary>
-            """,
-            entete_liste_etudiant(),
-            self.table_effectifs(),
-            """</details>""",
-            _help(),
-        ]
-
-        return "\n".join(H)
+                <summary><b>Tableau des effectifs</b>
+                </summary>
+                {self._diagtable()}
+            </details>
+            {self.display_tags()}
+            <details open="true">
+                <summary>
+                    <b id="effectifs">Liste des étudiants <span id="compte"></span></b>
+                </summary>
+                {entete_liste_etudiant()}
+                {self.table_effectifs()}
+            </details>
+            {_help()}
+        </div>
+        """
 
     def _inc_count(self, ind_row, ind_col):
         if (ind_row, ind_col) not in self.repartition:
@@ -692,26 +702,34 @@ class EtapeBilan:
         return "\n".join(H)
 
     @staticmethod
-    def link_etu(etudid, nom):
-        return '<a class="stdlink" href="%s">%s</a>' % (
-            url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid),
-            nom,
-        )
-
-    def link_semestre(self, semestre, short=False):
-        if short:
-            return (
-                '<a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%('
-                "formsemestre_id)s</a> " % self.semestres[semestre]
-            )
-        else:
-            return (
-                '<a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s'
-                " %(mois_debut)s - %(mois_fin)s)</a>" % self.semestres[semestre]
-            )
+    def link_etu(etudid, nom) -> str:
+        "Lien html vers fiche de l'étudiant"
+        return f"""<a class="stdlink" href="{
+            url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
+            }">{nom}</a>"""
+
+    def link_semestre(self, semestre, short=False) -> str:
+        "Lien html vers tableau de bord semestre"
+        key = "session_id" if short else "titremois"
+        sem = self.semestres[semestre]
+        return f"""<a class="stdlink" href="{
+                url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,
+                    formsemestre_id=sem['formsemestre_id']
+                )}">{sem[key]}</a>
+        """
 
-    def table_effectifs(self):
-        H = []
+    def table_effectifs(self) -> str:
+        "Table html donnant les étudiants dans chaque semestre"
+        H = [
+            """
+        <style>
+        table#apo-detail td.semestre {
+            white-space: nowrap;
+            word-break: normal;
+        }
+        </style>
+        """
+        ]
 
         col_ids = ["tag", "etudiant", "prenom", "nip", "semestre", "apogee", "annee"]
         titles = {
@@ -766,6 +784,7 @@ class EtapeBilan:
                 titles,
                 html_class="table_leftalign",
                 html_sortable=True,
+                html_with_td_classes=True,
                 table_id="apo-detail",
             ).gen(fmt="html")
         )
diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py
index 46f751e2e445fa6de4f55d13a1958c0be903d317..86893ebb54bef1476ddbe6a9bf5dd31ffe27456c 100644
--- a/app/scodoc/sco_formations.py
+++ b/app/scodoc/sco_formations.py
@@ -143,6 +143,7 @@ def formation_export_dict(
 
         if not export_codes_apo:
             ue_dict.pop("code_apogee", None)
+            ue_dict.pop("code_apogee_rcue", None)
         if ue_dict.get("ects") is None:
             ue_dict.pop("ects", None)
         mats = sco_edit_matiere.matiere_list({"ue_id": ue.id})
diff --git a/app/scodoc/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py
index f9a86475c7946b59228a496ae1ffb6b4c3b38ca6..0a9264ea80a1d94ba093cb318187a7aa60c8f4dc 100644
--- a/app/scodoc/sco_formsemestre.py
+++ b/app/scodoc/sco_formsemestre.py
@@ -419,49 +419,23 @@ def sem_set_responsable_name(sem):
     )
 
 
-def sem_in_semestre_scolaire(
-    sem,
-    year=False,
-    periode=None,
-    mois_pivot_annee=scu.MONTH_DEBUT_ANNEE_SCOLAIRE,
-    mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2,
-) -> bool:
-    """Vrai si la date du début du semestre est dans la période indiquée (1,2,0)
-    du semestre `periode` de l'année scolaire indiquée
-    (ou, à défaut, de celle en cours).
-
-    La période utilise les même conventions que semset["sem_id"];
-    * 1 : première période
-    * 2 : deuxième période
-    * 0 ou période non précisée: annualisé (donc inclut toutes les périodes)
-    )
-    """
-    if not year:
-        year = scu.annee_scolaire()
-    # n'utilise pas le jour pivot
-    jour_pivot_annee = jour_pivot_periode = 1
-    # calcule l'année universitaire et la période
-    sem_annee, sem_periode = FormSemestre.comp_periode(
-        datetime.datetime.fromisoformat(sem["date_debut_iso"]),
-        mois_pivot_annee,
-        mois_pivot_periode,
-        jour_pivot_annee,
-        jour_pivot_periode,
-    )
-    if periode is None or periode == 0:
-        return sem_annee == year
-    return sem_annee == year and sem_periode == periode
-
-
-def sem_in_annee_scolaire(sem, year=False):
+def sem_in_annee_scolaire(sem: dict, year=False):  # OBSOLETE
     """Test si sem appartient à l'année scolaire year (int).
     N'utilise que la date de début, pivot au 1er août.
     Si année non specifiée, année scolaire courante
     """
-    return sem_in_semestre_scolaire(sem, year, periode=0)
+    return FormSemestre.est_in_semestre_scolaire(
+        datetime.date.fromisoformat(sem["date_debut_iso"]), year, periode=0
+    )
+
+
+def sem_in_semestre_scolaire(sem, year=False, periode=None):  # OBSOLETE
+    return FormSemestre.est_in_semestre_scolaire(
+        datetime.date.fromisoformat(sem["date_debut_iso"]), year, periode=periode
+    )
 
 
-def sem_est_courant(sem):  # -> FormSemestre.est_courant
+def sem_est_courant(sem: dict):  # -> FormSemestre.est_courant
     """Vrai si la date actuelle (now) est dans le semestre (les dates de début et fin sont incluses)"""
     now = time.strftime("%Y-%m-%d")
     debut = ndb.DateDMYtoISO(sem["date_debut"])
diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py
index c2f375f3e58fab40478742a4c86376218c4e2b25..07717f591969fc270ae4e150e4b18440121b0fd9 100644
--- a/app/scodoc/sco_formsemestre_edit.py
+++ b/app/scodoc/sco_formsemestre_edit.py
@@ -438,12 +438,13 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
             "elt_sem_apo",
             {
                 "size": 32,
-                "title": "Element(s) Apogée:",
+                "title": "Element(s) Apogée sem.:",
                 "explanation": "associé(s) au résultat du semestre (ex: VRTW1). Inutile en BUT. Séparés par des virgules.",
                 "allow_null": not sco_preferences.get_preference(
                     "always_require_apo_sem_codes"
                 )
-                or (formsemestre and formsemestre.modalite == "EXT"),
+                or (formsemestre and formsemestre.modalite == "EXT")
+                or (formsemestre.formation.is_apc()),
             },
         )
     )
@@ -452,7 +453,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
             "elt_annee_apo",
             {
                 "size": 32,
-                "title": "Element(s) Apogée:",
+                "title": "Element(s) Apogée année:",
                 "explanation": "associé(s) au résultat de l'année (ex: VRT1A). Séparés par des virgules.",
                 "allow_null": not sco_preferences.get_preference(
                     "always_require_apo_sem_codes"
diff --git a/app/scodoc/sco_semset.py b/app/scodoc/sco_semset.py
index 985f9618f8e5d19eeeacf00d73925a930ff702ad..e70392c9ef517ca9e38b2407aecada28a73c18f0 100644
--- a/app/scodoc/sco_semset.py
+++ b/app/scodoc/sco_semset.py
@@ -381,7 +381,6 @@ class SemSet(dict):
         (actif seulement si un portail est configuré)
         """
         return self.bilan.html_diagnostic()
-        return ""
 
 
 def get_semsets_list():
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index c4d559765caae99311c4397d2adf0ac004b2c8e1..1c4908420695fba3a7ea072b713ae0ca3d2dded2 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -4165,6 +4165,11 @@ div.apo_csv_list {
   border: 1px dashed rgb(150, 10, 40);
 }
 
+table#apo_csv_list td {
+  white-space: nowrap;
+  word-break: no-wrap;
+}
+
 #apo_csv_download {
   margin-top: 5px;
 }