diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py
index 622cdbb002..97a555ca60 100644
--- a/app/but/cursus_but.py
+++ b/app/but/cursus_but.py
@@ -102,7 +102,7 @@ class EtudCursusBUT:
"Liste des inscriptions aux sem. de la formation, triées par indice et chronologie"
self.parcour: ApcParcours = self.inscriptions[-1].parcour
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
- self.niveaux_by_annee = {}
+ self.niveaux_by_annee: dict[int, list[ApcNiveau]] = {}
"{ annee:int : liste des niveaux à valider }"
self.niveaux: dict[int, ApcNiveau] = {}
"cache les niveaux"
diff --git a/app/but/jury_but.py b/app/but/jury_but.py
index 05978de2d8..954481ad09 100644
--- a/app/but/jury_but.py
+++ b/app/but/jury_but.py
@@ -342,21 +342,11 @@ class DecisionsProposeesAnnee(DecisionsProposees):
# Cas particulier du passage en BUT 3: nécessité d'avoir validé toutes les UEs du BUT 1.
if self.passage_de_droit and self.annee_but == 2:
inscription = formsemestre.etuds_inscriptions.get(etud.id)
- if inscription:
- ues_but1_non_validees = cursus_but.etud_ues_de_but1_non_validees(
- etud, self.formsemestre.formation, self.parcour
- )
- self.passage_de_droit = not ues_but1_non_validees
- explanation += (
- f"""UEs de BUT1 non validées: {
- ', '.join(ue.acronyme for ue in ues_but1_non_validees)
- }. """
- if ues_but1_non_validees
- else ""
- )
- else:
+ if not inscription or inscription.etat != scu.INSCRIT:
# pas inscrit dans le semestre courant ???
self.passage_de_droit = False
+ else:
+ self.passage_de_droit, explanation = self.passage_de_droit_en_but3()
# Enfin calcule les codes des UEs:
for dec_ue in self.decisions_ues.values():
@@ -423,6 +413,53 @@ class DecisionsProposeesAnnee(DecisionsProposees):
)
self.codes = [self.codes[0]] + sorted(self.codes[1:])
+ def passage_de_droit_en_but3(self) -> tuple[bool, str]:
+ """Vérifie si les conditions supplémentaires de passage BUT2 vers BUT3 sont satisfaites"""
+ cursus: EtudCursusBUT = EtudCursusBUT(self.etud, self.formsemestre.formation)
+ niveaux_but1 = cursus.niveaux_by_annee[1]
+
+ niveaux_but1_non_valides = []
+ for niveau in niveaux_but1:
+ ok = False
+ validation_par_annee = cursus.validation_par_competence_et_annee[
+ niveau.competence_id
+ ]
+ if validation_par_annee:
+ validation_niveau = validation_par_annee.get("BUT1")
+ if validation_niveau and validation_niveau.code in CODES_RCUE_VALIDES:
+ ok = True
+ if not ok:
+ niveaux_but1_non_valides.append(niveau)
+
+ # Les niveaux de BUT1 manquants passent-ils en ADSUP ?
+ # en vertu de l'article 4.3,
+ # "La validation des deux UE du niveau d’une compétence emporte la validation de
+ # l’ensemble des UE du niveau inférieur de cette même compétence."
+ explanation = ""
+ ok = True
+ for niveau_but1 in niveaux_but1_non_valides:
+ niveau_but2 = niveau_but1.competence.niveaux.filter_by(annee="BUT2").first()
+ if niveau_but2:
+ rcue = self.rcue_by_niveau.get(niveau_but2.id)
+ if (rcue is None) or (
+ not rcue.est_validable() and not rcue.code_valide()
+ ):
+ # le RCUE de BUT2 n'est ni validable (avec les notes en cours) ni déjà validé
+ ok = False
+ explanation += (
+ f"Compétence {niveau_but1} de BUT 1 non validée.
"
+ )
+ else:
+ explanation += (
+ f"Compétence {niveau_but1} de BUT 1 validée par ce BUT2.
"
+ )
+ else:
+ ok = False
+ explanation += f"""Compétence {
+ niveau_but1} de BUT 1 non validée et non existante en BUT2.
"""
+
+ return ok, explanation
+
# WIP TODO XXX def get_moyenne_annuelle(self)
def infos(self) -> str:
@@ -1235,13 +1272,16 @@ class DecisionsProposeesRCUE(DecisionsProposees):
self, semestre_id: int, ordre_inferieur: int, competence: ApcCompetence
):
"""Au besoin, enregistre une validation d'UE ADSUP pour le niveau de compétence
- semestre_id : l'indice du semestre concerné (le pair ou l'impair)
+ semestre_id : l'indice du semestre concerné (le pair ou l'impair du niveau courant)
"""
- # Les validations d'UE impaires existantes pour ce niveau inférieur ?
+ semestre_id_inferieur = semestre_id - 2
+ if semestre_id_inferieur < 1:
+ return
+ # Les validations d'UE existantes pour ce niveau inférieur ?
validations_ues: list[ScolarFormSemestreValidation] = (
ScolarFormSemestreValidation.query.filter_by(etudid=self.etud.id)
.join(UniteEns)
- .filter_by(semestre_idx=semestre_id)
+ .filter_by(semestre_idx=semestre_id_inferieur)
.join(ApcNiveau)
.filter_by(ordre=ordre_inferieur)
.join(ApcCompetence)
@@ -1256,13 +1296,14 @@ class DecisionsProposeesRCUE(DecisionsProposees):
# Il faut créer une validation d'UE
# cherche l'UE de notre formation associée à ce niveau
# et warning si il n'y en a pas
- ue = self._get_ue_inferieure(semestre_id, ordre_inferieur, competence)
+ ue = self._get_ue_inferieure(
+ semestre_id_inferieur, ordre_inferieur, competence
+ )
if not ue:
# programme incomplet ou mal paramétré
flash(
- f"""Impossible de valider l'UE inférieure du niveau {
- ordre_inferieur
- } de la compétence {competence.titre}
+ f"""Impossible de valider l'UE inférieure de la compétence {
+ competence.titre} (niveau {ordre_inferieur})
car elle n'existe pas dans la formation
""",
"warning",
diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py
index c4983bdd22..b33e2f2bb0 100644
--- a/app/models/but_refcomp.py
+++ b/app/models/but_refcomp.py
@@ -364,6 +364,9 @@ class ApcNiveau(db.Model, XMLModel):
return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={
self.annee!r} {self.competence!r}>"""
+ def __str__(self):
+ return f"""{self.competence.titre} niveau {self.ordre}"""
+
def to_dict(self, with_app_critiques=True):
"as a dict, recursif (ou non) sur les AC"
return {
diff --git a/app/templates/jury/erase_decisions_annee_formation.j2 b/app/templates/jury/erase_decisions_annee_formation.j2
index fc8fd0ce02..5f81e69ce5 100644
--- a/app/templates/jury/erase_decisions_annee_formation.j2
+++ b/app/templates/jury/erase_decisions_annee_formation.j2
@@ -35,6 +35,8 @@ quelle que soit leur origine.