diff --git a/app/but/jury_but.py b/app/but/jury_but.py
index 3a3472c337..3a797ad83f 100644
--- a/app/but/jury_but.py
+++ b/app/but/jury_but.py
@@ -667,6 +667,30 @@ class DecisionsProposeesAnnee(DecisionsProposees):
db.session.delete(validation)
db.session.flush()
+ def get_autorisations_passage(self) -> list[int]:
+ """Les liste des indices de semestres auxquels on est autorisé à
+ s'inscrire depuis cette année"""
+ formsemestre = self.formsemestre_pair or self.formsemestre_impair
+ if not formsemestre:
+ return []
+ return [
+ a.semestre_id
+ for a in ScolarAutorisationInscription.query.filter_by(
+ etudid=self.etud.id,
+ origin_formsemestre_id=formsemestre.id,
+ )
+ ]
+
+ def descr_niveaux_validation(self, line_sep: str = "\n") -> str:
+ """Description textuelle des niveaux validés (enregistrés)
+ pour PV jurys
+ """
+ validations = [
+ dec_rcue.descr_validation()
+ for dec_rcue in self.decisions_rcue_by_niveau.values()
+ ]
+ return line_sep.join([v for v in validations if v])
+
class DecisionsProposeesRCUE(DecisionsProposees):
"""Liste des codes de décisions que l'on peut proposer pour
@@ -742,6 +766,21 @@ class DecisionsProposeesRCUE(DecisionsProposees):
db.session.delete(validation)
db.session.flush()
+ def descr_validation(self) -> str:
+ """Description validation niveau enregistrée, pour PV jury.
+ Si le niveau est validé, done son acronyme, sinon chaine vide.
+ """
+ if self.code_valide in sco_codes.CODES_RCUE_VALIDES:
+ if (
+ self.rcue and self.rcue.ue_1 and self.rcue.ue_1.niveau_competence
+ ): # prudence !
+ niveau_titre = self.rcue.ue_1.niveau_competence.competence.titre or ""
+ ordre = self.rcue.ue_1.niveau_competence.ordre
+ else:
+ return "?" # oups ?
+ return f"{niveau_titre} niv. {ordre}"
+ return ""
+
class DecisionsProposeesUE(DecisionsProposees):
"""Décisions de jury sur une UE du BUT
@@ -859,6 +898,14 @@ class DecisionsProposeesUE(DecisionsProposees):
db.session.delete(validation)
db.session.flush()
+ def descr_validation(self) -> str:
+ """Description validation niveau enregistrée, pour PV jury.
+ Si l'UE est validé, done son acronyme, sinon chaine vide.
+ """
+ if self.code_valide in sco_codes.CODES_UE_VALIDES:
+ return f"{self.ue.acronyme}"
+ return ""
+
class BUTCursusEtud: # WIP TODO
"""Validation du cursus d'un étudiant"""
diff --git a/app/but/jury_but_pv.py b/app/but/jury_but_pv.py
new file mode 100644
index 0000000000..1524c09979
--- /dev/null
+++ b/app/but/jury_but_pv.py
@@ -0,0 +1,106 @@
+##############################################################################
+# ScoDoc
+# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
+# See LICENSE
+##############################################################################
+
+"""Jury BUT: table synthèse résultats semestre / PV
+"""
+from flask import g, request, url_for
+
+from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
+
+from app import log
+from app.but import jury_but
+from app.models.etudiants import Identite
+from app.models.formsemestre import FormSemestre
+from app.scodoc.gen_tables import GenTable
+from app.scodoc import sco_excel
+from app.scodoc.sco_exceptions import ScoValueError
+from app.scodoc import sco_preferences
+from app.scodoc import sco_utils as scu
+
+
+def _descr_cursus_but(etud: Identite) -> str:
+ "description de la liste des semestres BUT suivis"
+ # prend simplement tous les semestre de type APC, ce qui sera faux si
+ # l'étudiant change de spécialité au sein du même département
+ # (ce qui ne peut normalement pas se produire)
+ indices = [
+ ins.formsemestre.semestre_id
+ for ins in etud.formsemestre_inscriptions
+ if ins.formsemestre.formation.is_apc()
+ ]
+ return ", ".join(f"S{indice}" for indice in indices)
+
+
+def pvjury_table_but(formsemestre_id: int, format="html") -> list[dict]:
+ """Page récapitulant les décisions de jury BUT
+ formsemestre peut être pair ou impair
+ """
+ formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
+ assert formsemestre.formation.is_apc()
+ title = "Jury BUT annuel"
+
+ if format == "html":
+ line_sep = "
"
+ else:
+ line_sep = "\n"
+ # remplace pour le BUT la fonction sco_pvjury.pvjury_table
+ annee_but = (formsemestre.semestre_id + 1) // 2
+ titles = {
+ "nom": "Nom",
+ "cursus": "Cursus",
+ "niveaux": "Niveaux de compétences validés",
+ "decision_but": f"Décision BUT{annee_but}",
+ "diplome": "Résultat au diplôme",
+ "devenir": "Devenir",
+ "observations": "Observations",
+ }
+ rows = []
+ for etudid in formsemestre.etuds_inscriptions:
+ etud: Identite = Identite.query.get(etudid)
+ try:
+ deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
+ if deca.annee_but != annee_but: # wtf ?
+ log(
+ f"pvjury_table_but: inconsistent annee_but {deca.annee_but} != {annee_but}"
+ )
+ continue
+ except ScoValueError:
+ deca = None
+ row = {
+ "nom": etud.etat_civil_pv(line_sep=line_sep),
+ "cursus": _descr_cursus_but(etud),
+ "niveaux": deca.descr_niveaux_validation(line_sep=line_sep)
+ if deca
+ else "-",
+ "decision_but": deca.code_valide if deca else "",
+ "devenir": ", ".join([f"S{i}" for i in deca.get_autorisations_passage()]),
+ }
+
+ rows.append(row)
+
+ # Style excel... passages à la ligne sur \n
+ xls_style_base = sco_excel.excel_make_style()
+ xls_style_base["alignment"] = Alignment(wrapText=True, vertical="top")
+
+ tab = GenTable(
+ columns_ids=titles.keys(),
+ rows=rows,
+ titles=titles,
+ origin=f"Généré par {scu.sco_version.SCONAME} le {scu.timedate_human_repr()}",
+ caption=title,
+ html_caption=title,
+ html_class="pvjury_table_but table_leftalign",
+ # html_class_ignore_default=True,
+ html_with_td_classes=True,
+ xls_style_base=xls_style_base,
+ base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}",
+ page_title=title,
+ html_title=f"