diff --git a/app/__init__.py b/app/__init__.py
index f1fba060a450ec2f83094cfc63843c749cfa83c6..cf1495be3001cb6dab503a2c2c45891dff496680 100755
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -34,9 +34,9 @@ from flask_sqlalchemy import SQLAlchemy
 
 from jinja2 import select_autoescape
 import sqlalchemy as sa
+import werkzeug.debug
 
 from flask_cas import CAS
-import werkzeug.debug
 
 from app.scodoc.sco_exceptions import (
     AccessDenied,
@@ -44,6 +44,7 @@ from app.scodoc.sco_exceptions import (
     ScoException,
     ScoGenError,
     ScoInvalidCSRF,
+    ScoPDFFormatError,
     ScoValueError,
     APIInvalidParams,
 )
@@ -74,6 +75,7 @@ cache = Cache(
 
 
 def handle_sco_value_error(exc):
+    "page d'erreur avec message"
     return render_template("sco_value_error.j2", exc=exc), 404
 
 
@@ -90,6 +92,10 @@ def handle_invalid_csrf(exc):
     return render_template("error_csrf.j2", exc=exc), 404
 
 
+def handle_pdf_format_error(exc):
+    return "ay ay ay"
+
+
 def internal_server_error(exc):
     """Bugs scodoc, erreurs 500"""
     # note that we set the 500 status explicitly
@@ -310,6 +316,7 @@ def create_app(config_class=DevConfig):
     app.register_error_handler(ScoValueError, handle_sco_value_error)
     app.register_error_handler(ScoBugCatcher, handle_sco_bug)
     app.register_error_handler(ScoInvalidCSRF, handle_invalid_csrf)
+    app.register_error_handler(ScoPDFFormatError, handle_pdf_format_error)
     app.register_error_handler(AccessDenied, handle_access_denied)
     app.register_error_handler(500, internal_server_error)
     app.register_error_handler(503, postgresql_server_error)
diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py
index 74d167eba06bde69c047dbab559eab222dd0e3cc..4cf2e88c2069e294b72173f2aa97a04519fee1d4 100644
--- a/app/but/bulletin_but.py
+++ b/app/but/bulletin_but.py
@@ -333,7 +333,6 @@ class BulletinBUT:
     def bulletin_etud(
         self,
         etud: Identite,
-        formsemestre: FormSemestre,
         force_publishing=False,
         version="long",
     ) -> dict:
@@ -346,6 +345,7 @@ class BulletinBUT:
         (bulletins non publiés).
         """
         res = self.res
+        formsemestre = res.formsemestre
         etat_inscription = etud.inscription_etat(formsemestre.id)
         nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT]
         published = (not formsemestre.bul_hide_xml) or force_publishing
@@ -489,9 +489,7 @@ class BulletinBUT:
         (pas utilisé pour json/html)
         Résultat compatible avec celui de sco_bulletins.formsemestre_bulletinetud_dict
         """
-        d = self.bulletin_etud(
-            etud, self.res.formsemestre, version=version, force_publishing=True
-        )
+        d = self.bulletin_etud(etud, version=version, force_publishing=True)
         d["etudid"] = etud.id
         d["etud"] = d["etudiant"]
         d["etud"]["nomprenom"] = etud.nomprenom
diff --git a/app/but/bulletin_but_court.py b/app/but/bulletin_but_court.py
index 0396b1bc5b1a779926f2caee39e5af8a8af7a43d..4ba1aee9a0352a30c16652a9f6f67dc69f70aa64 100644
--- a/app/but/bulletin_but_court.py
+++ b/app/but/bulletin_but_court.py
@@ -25,8 +25,8 @@ Ces données sont des objets passés au template.
 import datetime
 import time
 
-from flask import render_template, url_for
-from flask import g, request
+from flask import render_template
+from flask import g
 
 from app.but.bulletin_but import BulletinBUT
 from app.but import bulletin_but_court_pdf, cursus_but, validations_view
@@ -35,6 +35,7 @@ from app.decorators import (
     permission_required,
 )
 from app.models import FormSemestre, FormSemestreInscription, Identite
+from app.scodoc.codes_cursus import UE_STANDARD
 from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
 from app.scodoc.sco_logos import find_logo
 from app.scodoc.sco_permissions import Permission
@@ -59,7 +60,10 @@ def bulletin_but(formsemestre_id: int, etudid: int = None, fmt="html"):
         .first_or_404()
     )
     bulletins_sem = BulletinBUT(formsemestre)
-    bul = bulletins_sem.bulletin_etud(etud, formsemestre)  # dict
+    if fmt == "pdf":
+        bul: dict = bulletins_sem.bulletin_etud_complet(etud)
+    else:  # la même chose avec un peu moins d'infos
+        bul: dict = bulletins_sem.bulletin_etud(etud)
     decision_ues = {x["acronyme"]: x for x in bul["semestre"]["decision_ue"]}
     cursus = cursus_but.EtudCursusBUT(etud, formsemestre.formation)
     refcomp = formsemestre.formation.referentiel_competence
@@ -82,8 +86,10 @@ def bulletin_but(formsemestre_id: int, etudid: int = None, fmt="html"):
         "logo": logo,
         "title": f"Bul. {etud.nom_disp()} BUT (court)",
         "ue_validation_by_niveau": ue_validation_by_niveau,
+        "ues_acronyms": [
+            ue.acronyme for ue in bulletins_sem.res.ues if ue.type == UE_STANDARD
+        ],
     }
-
     if fmt == "pdf":
         filename = scu.bul_filename(formsemestre, etud, prefix="bul-but")
         bul_pdf = bulletin_but_court_pdf.make_bulletin_but_court_pdf(**args)
diff --git a/app/but/bulletin_but_court_pdf.py b/app/but/bulletin_but_court_pdf.py
index 173630c18ba5331b4ab05cc71807fbb8ac1773a7..3f0a1fb6517602c3a393b36925aefc58f91f3a48 100644
--- a/app/but/bulletin_but_court_pdf.py
+++ b/app/but/bulletin_but_court_pdf.py
@@ -10,19 +10,23 @@ On génère du PDF avec reportLab en utilisant les classes
 ScoDoc BulletinGenerator et GenTable.
 
 """
+import datetime
 from flask_login import current_user
 
-from reportlab.lib.colors import blue
+from reportlab.lib import styles
+from reportlab.lib.colors import black, white, Color
+from reportlab.lib.enums import TA_CENTER
 from reportlab.lib.units import cm, mm
-from reportlab.platypus import Paragraph, Spacer
+from reportlab.platypus import Paragraph, Spacer, Table
 
 from app.but import cursus_but
 from app.models import FormSemestre, Identite, ScolarFormSemestreValidation
 
 from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
+from app.scodoc import sco_bulletins
 from app.scodoc.sco_logos import Logo
-from app.scodoc import gen_tables, sco_pdf, sco_preferences
-from app.scodoc.sco_pdf import PDFLOCK
+from app.scodoc import sco_pdf, sco_preferences
+from app.scodoc.sco_pdf import PDFLOCK, SU
 
 
 def make_bulletin_but_court_pdf(
@@ -35,6 +39,7 @@ def make_bulletin_but_court_pdf(
     logo: Logo = None,
     title: str = "",
     ue_validation_by_niveau: dict[tuple[int, str], ScolarFormSemestreValidation] = None,
+    ues_acronyms: list[str] = None,
 ) -> bytes:
     # A priori ce verrou n'est plus nécessaire avec Flask (multi-process)
     # mais...
@@ -58,6 +63,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
     scale_table_in_page = True  # pas de mise à l'échelle pleine page auto
     multi_pages = False  # une page par bulletin
     small_fontsize = "8"
+    color_blue_bg = Color(0, 153 / 255, 204 / 255)
 
     def __init__(
         self,
@@ -72,16 +78,9 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
         ue_validation_by_niveau: dict[
             tuple[int, str], ScolarFormSemestreValidation
         ] = None,
+        ues_acronyms: list[str] = None,
     ):
-        # données pour anciens codes bulletins... à moderniser
-        infos = {
-            "etud": etud.to_dict_bul(),
-            "filigranne": None,
-            "formsemestre_id": formsemestre.id,
-            "nbabs": 0,
-            "nbabsjust": 0,
-        }
-        super().__init__(infos, authuser=current_user)
+        super().__init__(bul, authuser=current_user)
         self.bul = bul
         self.cursus = cursus
         self.decision_ues = decision_ues
@@ -91,32 +90,334 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
         self.logo = logo
         self.title = title
         self.ue_validation_by_niveau = ue_validation_by_niveau
+        self.ues_acronyms = ues_acronyms  # sans UEs sport
 
-    def bul_table(self, fmt=None):
+        self.nb_ues = len(self.ues_acronyms)
+        # Styles PDF
+        self.style_cell = styles.ParagraphStyle("style_cell")
+        self.style_cell.fontName = "Helvetica"
+        self.style_cell.fontSize = 7
+        self.style_cell.leading = 7
+        self.style_bold = styles.ParagraphStyle("style_bold", self.style_cell)
+        self.style_bold.fontName = "Helvetica-Bold"
+        self.style_head = styles.ParagraphStyle("style_head", self.style_bold)
+        self.style_head.fontSize = 9
+
+        self.style_niveaux = styles.ParagraphStyle("style_niveaux", self.style_cell)
+        self.style_niveaux.alignment = TA_CENTER
+        self.style_niveaux.leading = 9
+        self.style_niveaux.firstLineIndent = 0
+        self.style_niveaux.leftIndent = 1
+        self.style_niveaux.rightIndent = 1
+        self.style_niveaux.borderWidth = 1
+        self.style_niveaux.borderPadding = 2
+        self.style_niveaux.borderRadius = 2
+        self.style_niveaux_top = styles.ParagraphStyle(
+            "style_niveaux_top", self.style_niveaux
+        )
+        self.style_niveaux_top.fontName = "Helvetica-Bold"
+        self.style_niveaux_top.fontSize = 8
+        self.style_niveaux_titre = styles.ParagraphStyle(
+            "style_niveaux_titre", self.style_niveaux
+        )
+        self.style_niveaux_titre.textColor = white
+        self.style_niveaux_titre.backColor = self.color_blue_bg
+        self.style_niveaux_titre.borderColor = self.color_blue_bg
+
+        self.style_niveaux_code = styles.ParagraphStyle(
+            "style_niveaux_code", self.style_niveaux
+        )
+        self.style_niveaux_code.borderColor = black
+
+        # Géométrie page
+        self.width_page_avail = 185 * mm  # largeur utilisable
+        # Géométrie tableaux
+        self.width_col_ue = 18 * mm
+        self.width_col_ue_titres = 15 * mm
+        # Modules
+        self.width_col_code = self.width_col_ue
+        # Niveaux
+        self.width_col_niveaux_titre = 24 * mm
+        self.width_col_niveaux_code = 12 * mm
+
+    def bul_table(self, fmt=None) -> list:
         """Génère la table centrale du bulletin de notes
         Renvoie: une liste d'objets PLATYPUS (eg instance de Table).
         L'argument fmt est ici ignoré (toujours en PDF)
         """
-        ue_table = self.build_ue_table()
-
-        return ue_table.gen(format="pdf")
+        style_table_2cols = [
+            ("ALIGN", (0, -1), (0, -1), "LEFT"),
+            ("ALIGN", (-1, -1), (-1, -1), "RIGHT"),
+            ("VALIGN", (0, 0), (-1, 1), "TOP"),
+            ("LEFTPADDING", (0, 0), (-1, -1), 0),
+            ("TOPPADDING", (0, 0), (-1, -1), 0),
+            ("RIGHTPADDING", (0, 0), (-1, -1), 0),
+            ("BOTTOMPADDING", (0, 0), (-1, -1), 0),
+        ]
+        # Ligne avec boite assiduité et table UEs
+        table_abs_ues = Table(
+            [[self.box_assiduite(), self.table_ues()]],
+            colWidths=(3 * cm, self.width_page_avail - 3 * cm),
+            style=style_table_2cols,
+        )
+        table_abs_ues.hAlign = "RIGHT"
+        # Ligne (en bas) avec table cursus et boite jury
+        table_cursus_jury = Table(
+            [[self.table_cursus_but(), self.boite_decisions_jury()]],
+            colWidths=(self.width_page_avail - 45 * mm, 45 * mm),
+            style=style_table_2cols,
+        )
+        return [
+            table_abs_ues,
+            Spacer(0, 3 * mm),
+            self.table_ressources(),
+            Spacer(0, 3 * mm),
+            self.table_saes(),
+            Spacer(0, 5 * mm),
+            table_cursus_jury,
+        ]
 
-    def build_ue_table(self) -> gen_tables.GenTable:
+    def table_ues(self) -> Table:
         """Table avec les résultats d'UE du semestre courant"""
-        columns_ids = ("titre", "UE1", "UE2")
+        bul = self.bul
         rows = [
-            {"titre": "ligne 1", "UE1": "12.", "UE2": "13"},
-            {"titre": "ligne 2", "UE1": "14.", "UE2": "15"},
+            [
+                f"Unités d'enseignement du semestre {self.formsemestre.semestre_id}",
+            ],
+            [""] + self.ues_acronyms,
+            ["Moyenne"]
+            + [bul["ues"][ue]["moyenne"]["value"] for ue in self.ues_acronyms],
+            ["Bonus"]
+            + [
+                bul["ues"][ue]["bonus"] if bul["ues"][ue]["bonus"] != "00.00" else ""
+                for ue in self.ues_acronyms
+            ],
+            ["Malus"]
+            + [
+                bul["ues"][ue]["malus"] if bul["ues"][ue]["malus"] != "00.00" else ""
+                for ue in self.ues_acronyms
+            ],
+            ["Rang"] + [bul["ues"][ue]["moyenne"]["rang"] for ue in self.ues_acronyms],
+            ["Effectif"]
+            + [bul["ues"][ue]["moyenne"]["total"] for ue in self.ues_acronyms],
+            ["ECTS"]
+            + [
+                f'{self.decision_ues[ue]["ects"]:g}' if ue in self.decision_ues else ""
+                for ue in self.ues_acronyms
+            ],
+            ["Jury"]
+            + [
+                self.decision_ues[ue]["code"] if ue in self.decision_ues else ""
+                for ue in self.ues_acronyms
+            ],
         ]
-        pdf_style = [
+        blue_bg = Color(183 / 255.0, 235 / 255.0, 255 / 255.0)
+        table_style = [
+            ("VALIGN", (0, 0), (-1, -1), "TOP"),
+            ("BOX", (0, 0), (-1, -1), 1.0, black),  # ajoute cadre extérieur
+            ("INNERGRID", (0, 0), (-1, -1), 0.25, black),
+            ("LEADING", (0, 1), (-1, -1), 5),
+            ("SPAN", (0, 0), (self.nb_ues, 0)),
+            ("BACKGROUND", (0, 0), (self.nb_ues, 0), blue_bg),
+        ]
+        col_widths = [self.width_col_ue_titres] + [self.width_col_ue] * self.nb_ues
+
+        rows_styled = [[Paragraph(SU(str(cell)), self.style_head) for cell in rows[0]]]
+        rows_styled += [[Paragraph(SU(str(cell)), self.style_bold) for cell in rows[1]]]
+        rows_styled += [
+            [Paragraph(SU(str(cell)), self.style_cell) for cell in row]
+            for row in rows[2:-1]
+        ]
+        rows_styled += [
+            [Paragraph(SU(str(cell)), self.style_bold) for cell in rows[-1]]
+        ]
+        table = Table(
+            rows_styled,
+            colWidths=col_widths,
+            style=table_style,
+        )
+        table.hAlign = "RIGHT"
+        return table
+
+    def _table_modules(self, mod_type: str = "ressources", title: str = "") -> Table:
+        "génère table des modules: resources ou SAEs"
+        bul = self.bul
+        rows = [
+            ["", "", "Unités d'enseignement"] + [""] * (self.nb_ues - 1),
+            [title, ""] + self.ues_acronyms,
+        ]
+        for mod in self.bul[mod_type]:
+            row = [mod, bul[mod_type][mod]["titre"]]
+            row += [
+                bul["ues"][ue][mod_type][mod]["moyenne"]
+                if mod in bul["ues"][ue][mod_type]
+                else ""
+                for ue in self.ues_acronyms
+            ]
+            rows.append(row)
+
+        title_bg = (
+            Color(255 / 255, 192 / 255, 0)
+            if mod_type == "ressources"
+            else Color(176 / 255, 255 / 255, 99 / 255)
+        )
+
+        table_style = [
             ("VALIGN", (0, 0), (-1, -1), "TOP"),
-            ("BOX", (0, 0), (-1, -1), 0.4, blue),  # ajoute cadre extérieur bleu
+            ("BOX", (0, 0), (-1, -1), 1.0, black),  # ajoute cadre extérieur
+            ("INNERGRID", (0, 0), (-1, -1), 0.25, black),
+            ("LEADING", (0, 1), (-1, -1), 5),
+            # 1ère ligne titre
+            ("SPAN", (0, 0), (1, 0)),
+            ("SPAN", (2, 0), (self.nb_ues, 0)),
+            # 2ème ligne titre
+            ("SPAN", (0, 1), (1, 1)),
+            ("BACKGROUND", (0, 1), (1, 1), title_bg),
+        ]
+        # Estime l'espace horizontal restant pour les titres de modules
+        width_col_titre_module = (
+            self.width_page_avail
+            - self.width_col_code
+            - self.width_col_ue * self.nb_ues
+        )
+        col_widths = [self.width_col_code, width_col_titre_module] + [
+            self.width_col_ue
+        ] * self.nb_ues
+
+        rows_styled = [
+            [Paragraph(SU(str(cell)), self.style_bold) for cell in row]
+            for row in rows[:2]
+        ]
+        rows_styled += [
+            [Paragraph(SU(str(cell)), self.style_cell) for cell in row]
+            for row in rows[2:]
         ]
-        col_widths = {"titre": 3 * cm, "UE1": 1 * cm, "UE2": 1 * cm}
-        return gen_tables.GenTable(
-            rows=rows,
-            columns_ids=columns_ids,
-            pdf_table_style=pdf_style,
-            pdf_col_widths=[col_widths[k] for k in columns_ids],
-            preferences=sco_preferences.SemPreferences(self.formsemestre.id),
+        table = Table(
+            rows_styled,
+            colWidths=col_widths,
+            style=table_style,
+        )
+        table.hAlign = "RIGHT"
+        return table
+
+    def table_ressources(self) -> Table:
+        "La table des ressources"
+        return self._table_modules("ressources", "Ressources")
+
+    def table_saes(self) -> Table:
+        "La table des SAEs"
+        return self._table_modules(
+            "saes", "Situations d'Apprentissage et d'Évaluation (SAÉ)"
         )
+
+    def box_assiduite(self) -> Table:
+        "Les informations sur l'assiduité"
+        if not self.bul["options"]["show_abs"]:
+            return Paragraph("")  # empty
+        color_bg = Color(245 / 255, 237 / 255, 200 / 255)
+        rows = [
+            ["Absences", ""],
+            [f'({self.bul["semestre"]["absences"]["metrique"]})', ""],
+            ["Non justifiées", self.bul["semestre"]["absences"]["injustifie"]],
+            ["Total", self.bul["semestre"]["absences"]["total"]],
+        ]
+        rows_styled = [
+            [Paragraph(SU(str(cell)), self.style_head) for cell in row]
+            for row in rows[:1]
+        ]
+        rows_styled += [
+            [Paragraph(SU(str(cell)), self.style_cell) for cell in row]
+            for row in rows[1:]
+        ]
+        table = Table(
+            rows_styled,
+            # [topLeft, topRight, bottomLeft bottomRight]
+            cornerRadii=[2 * mm] * 4,
+            style=[
+                ("BACKGROUND", (0, 0), (-1, -1), color_bg),
+                ("SPAN", (0, 0), (1, 0)),
+                ("SPAN", (0, 1), (1, 1)),
+                ("VALIGN", (0, 0), (-1, -1), "TOP"),
+            ],
+        )
+        table.hAlign = "LEFT"
+        return table
+
+    def table_cursus_but(self) -> Table:
+        "La table avec niveaux et validations BUT1, BUT2, BUT3"
+        rows = [
+            ["", "BUT 1", "BUT 2", "BUT 3"],
+        ]
+        for competence_id in self.cursus.to_dict():
+            row = [self.cursus.competences[competence_id].titre]
+            for annee in ("BUT1", "BUT2", "BUT3"):
+                validation = self.cursus.validation_par_competence_et_annee.get(
+                    competence_id, {}
+                ).get(annee)
+                has_niveau = self.cursus.competence_annee_has_niveau(
+                    competence_id, annee
+                )
+                txt = ""
+                if validation:
+                    txt = validation.code
+                elif has_niveau:
+                    txt = "-"
+                row.append(txt)
+            rows.append(row)
+
+        rows_styled = [
+            [Paragraph(SU(str(cell)), self.style_niveaux_top) for cell in rows[0]]
+        ] + [
+            [Paragraph(SU(str(row[0])), self.style_niveaux_titre)]
+            + [
+                Paragraph(SU(str(cell)), self.style_niveaux_code) if cell else ""
+                for cell in row[1:]
+            ]
+            for row in rows[1:]
+        ]
+
+        table = Table(
+            rows_styled,
+            colWidths=[
+                self.width_col_niveaux_titre,
+                self.width_col_niveaux_code,
+                self.width_col_niveaux_code,
+                self.width_col_niveaux_code,
+            ],
+            style=[
+                ("ALIGN", (0, 0), (-1, -1), "CENTER"),
+                ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
+                ("LEFTPADDING", (0, 0), (-1, -1), 2),
+                ("TOPPADDING", (0, 0), (-1, -1), 4),
+                ("RIGHTPADDING", (0, 0), (-1, -1), 2),
+                ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
+                # sert de séparateur entre les lignes:
+                ("LINEABOVE", (0, 1), (-1, -1), 3, white),
+            ],
+        )
+        table.hAlign = "LEFT"
+        return table
+
+    def boite_decisions_jury(self):
+        """La boite en bas à droite avec jury"""
+        txt = f"""ECTS acquis : {self.ects_total}<br/>"""
+        if self.bul["semestre"]["decision_annee"]:
+            txt += f"""
+            Jury tenu le {
+                datetime.datetime.fromisoformat(self.bul["semestre"]["decision_annee"]["date"]).strftime("%d/%m/%Y à %H:%M")
+            }, année BUT {self.bul["semestre"]["decision_annee"]["code"]}.
+            <br/>
+            """
+        if self.bul["semestre"]["autorisation_inscription"]:
+            txt += (
+                "Autorisé à s'inscrire en "
+                + ", ".join(
+                    [
+                        f"S{aut['semestre_id']}"
+                        for aut in self.bul["semestre"]["autorisation_inscription"]
+                    ]
+                )
+                + "."
+            )
+
+        return Paragraph(txt)
diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py
index 2b5e867a9259f235bc02ee9b63cc90e34a18034d..0920b81f5cada79a77a77f65d439a53031407c64 100644
--- a/app/scodoc/gen_tables.py
+++ b/app/scodoc/gen_tables.py
@@ -538,8 +538,8 @@ class GenTable:
             ]
         )
 
-    def pdf(self):
-        "PDF representation: returns a ReportLab's platypus Table instance"
+    def pdf(self) -> list:
+        "PDF representation: returns a list of ReportLab's platypus objects"
         r = []
         try:
             sco_pdf.PDFLOCK.acquire()
@@ -548,12 +548,12 @@ class GenTable:
             sco_pdf.PDFLOCK.release()
         return r
 
-    def _pdf(self):
+    def _pdf(self) -> list:
         """PDF representation: returns a list of ReportLab's platypus objects
         (notably a Table instance)
         """
+        LINEWIDTH = 0.5
         if not self.pdf_table_style:
-            LINEWIDTH = 0.5
             self.pdf_table_style = [
                 ("FONTNAME", (0, 0), (-1, 0), self.preferences["SCOLAR_FONT"]),
                 ("LINEBELOW", (0, 0), (-1, 0), LINEWIDTH, Color(0, 0, 0)),
@@ -570,7 +570,6 @@ class GenTable:
         CellStyle.fontSize = self.preferences["SCOLAR_FONT_SIZE"]
         CellStyle.fontName = self.preferences["SCOLAR_FONT"]
         CellStyle.leading = 1.0 * self.preferences["SCOLAR_FONT_SIZE"]  # vertical space
-        LINEWIDTH = 0.5
         #
         # titles = ["<para><b>%s</b></para>" % x for x in self.get_titles_list()]
         pdf_style_list = []
diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py
index ba27c9b96c2ed5a097a3b992cde382fdbfdf4d3e..a1e67d3382d3ff395b34c37f84518fda885d30a7 100644
--- a/app/scodoc/sco_bulletins.py
+++ b/app/scodoc/sco_bulletins.py
@@ -88,7 +88,6 @@ def get_formsemestre_bulletin_etud_json(
         return json_response(
             data_=bulletins_sem.bulletin_etud(
                 etud,
-                formsemestre,
                 force_publishing=force_publishing,
                 version=version,
             )
diff --git a/app/scodoc/sco_bulletins_pdf.py b/app/scodoc/sco_bulletins_pdf.py
index 314aff5d3f489bf50a8c1a58ecbf3765b3e7f0a0..4413741d43b648728f910c09c666180a636f8696 100644
--- a/app/scodoc/sco_bulletins_pdf.py
+++ b/app/scodoc/sco_bulletins_pdf.py
@@ -136,7 +136,7 @@ class WrapDict(object):
         try:
             value = self.dict[key]
         except KeyError:
-            return f"XXX {key} invalide XXX"
+            raise
         if value is None:
             return self.none_value
         return value
@@ -168,6 +168,7 @@ def process_field(field, cdict, style, suppress_empty_pars=False, fmt="pdf"):
         scu.flash_once(
             f"Attention: format PDF invalide (champs {field}, clef {missing_key})"
         )
+        raise
     except:  # pylint: disable=bare-except
         log(
             f"""process_field: invalid format. field={field!r}
diff --git a/app/scodoc/sco_pdf.py b/app/scodoc/sco_pdf.py
index 24cd548b11af71923046e471e62604c1816a1053..72e1f8df2776727f132c30ad8b3299ee1fc50bfa 100755
--- a/app/scodoc/sco_pdf.py
+++ b/app/scodoc/sco_pdf.py
@@ -163,6 +163,7 @@ def make_paras(txt: str, style, suppress_empty=False) -> list[Paragraph]:
                     style,
                 )
             ]
+            # si on voulait interrompre la génération raise ScoPDFFormatError("Erreur: format invalide")
         except ValueError:  # probleme font ? essaye sans style
             # récupère font en cause ?
             m = re.match(r".*family/bold/italic for (.*)", exc.args[0], re.DOTALL)
diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py
index 791fdb0f536050a4f67828fd385904dd7f75c55f..5560f21371e43a6a5bb0c43949881391a51f714d 100644
--- a/app/scodoc/sco_recapcomplet.py
+++ b/app/scodoc/sco_recapcomplet.py
@@ -416,7 +416,7 @@ def gen_formsemestre_recapcomplet_json(
         if is_apc:
             etud = Identite.get_etud(etudid)
             bulletins_sem = bulletin_but.BulletinBUT(formsemestre)
-            bul = bulletins_sem.bulletin_etud(etud, formsemestre)
+            bul = bulletins_sem.bulletin_etud(etud)
         else:
             bul = sco_bulletins_json.formsemestre_bulletinetud_published_dict(
                 formsemestre_id,
diff --git a/app/templates/but/bulletin_court_page.j2 b/app/templates/but/bulletin_court_page.j2
index 593ec54f4e3316c56c370e14e8685766a3240986..66da8bc398b187389f87382ac8d7b19d5322b8c6 100644
--- a/app/templates/but/bulletin_court_page.j2
+++ b/app/templates/but/bulletin_court_page.j2
@@ -12,11 +12,11 @@
     <thead>
         <tr class="titre_table">
             <th colspan="2"></th>
-            <th colspan="{{ bul.ues|length }}">Unités d'enseignement</th>
+            <th colspan="{{ ues_acronyms|length }}">Unités d'enseignement</th>
         </tr>
         <tr class="titres_ues">
             <td colspan="2">{{title}}</td>
-            {% for ue in bul.ues %}
+            {% for ue in ues_acronyms %}
                 <td class="col_ue">{{ue}}</td>
             {% endfor %}
         </tr>
@@ -26,7 +26,7 @@
         <tr>
             <td>{{mod}}</td>
             <td>{{bul[mod_type][mod].titre}}</td>
-            {% for ue in bul.ues %}
+            {% for ue in ues_acronyms %}
                 <td>{{
                     bul.ues[ue][mod_type][mod].moyenne
                     if mod in bul.ues[ue][mod_type] else ""
@@ -75,7 +75,7 @@
             </tr>
             <tr class="titres_ues">
                 <td></td>
-                {% for ue in bul.ues %}
+                {% for ue in ues_acronyms %}
                     <td class="col_ue">{{ue}}</td>
                 {% endfor %}
             </tr>
@@ -83,43 +83,45 @@
         <tbody>
             <tr>
                 <td>Moyenne</td>
-                {% for ue in bul.ues %}
+                {% for ue in ues_acronyms %}
                     <td class="col_ue">{{bul.ues[ue].moyenne.value}}</td>
                 {% endfor %}
             </tr>
             <tr>
                 <td>Bonus</td>
-                {% for ue in bul.ues %}
+                {% for ue in ues_acronyms %}
                     <td class="col_ue">{{bul.ues[ue].bonus if bul.ues[ue].bonus != "00.00" else ""}}</td>
                 {% endfor %}
             </tr>
             <tr>
                 <td>Malus</td>
-                {% for ue in bul.ues %}
+                {% for ue in ues_acronyms %}
                     <td class="col_ue">{{bul.ues[ue].malus if bul.ues[ue].malus != "00.00" else ""}}</td>
                 {% endfor %}
             </tr>
             <tr>
                 <td>Rang</td>
-                {% for ue in bul.ues %}
+                {% for ue in ues_acronyms %}
                     <td class="col_ue">{{bul.ues[ue].moyenne.rang}}</td>
                 {% endfor %}
             </tr>
             <tr>
                 <td>Effectif</td>
-                {% for ue in bul.ues %}
+                {% for ue in ues_acronyms %}
                     <td class="col_ue">{{bul.ues[ue].moyenne.total}}</td>
                 {% endfor %}
             </tr>
             <tr>
                 <td>ECTS</td>
-                {% for ue in bul.ues %}
-                    <td class="col_ue">{{bul.ues[ue].moyenne.ects}}</td>
+                {% for ue in ues_acronyms %}
+                    <td class="col_ue">{{
+                        "%g"|format(decision_ues[ue].ects) if ue in decision_ues else ""
+                    }}</td>
                 {% endfor %}
             </tr>
             <tr class="jury">
                 <td>Jury</td>
-                {% for ue in bul.ues %}
+                {% for ue in ues_acronyms %}
                     <td class="col_ue">{{
                         decision_ues[ue].code if ue in decision_ues else ""
                     }}</td>