diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py
index 4af56a3cacd47d7ccb4f860b35d9ea600068041e..9f0a9b85c6fb0d7daf0ad067978847f5a1540f98 100644
--- a/app/but/bulletin_but.py
+++ b/app/but/bulletin_but.py
@@ -385,7 +385,7 @@ class BulletinBUT:
                 "injustifie": nbabs - nbabsjust,
                 "total": nbabs,
             }
-        decisions_ues = self.res.get_etud_decision_ues(etud.id) or {}
+        decisions_ues = self.res.get_etud_decisions_ue(etud.id) or {}
         if self.prefs["bul_show_ects"]:
             ects_tot = res.etud_ects_tot_sem(etud.id)
             ects_acquis = res.get_etud_ects_valides(etud.id, decisions_ues)
diff --git a/app/comp/res_compat.py b/app/comp/res_compat.py
index e973a7a6c39c1da4250b9802bdd145361e1aa654..1ebac8b176f8448355b548cba97c9dcbe907188d 100644
--- a/app/comp/res_compat.py
+++ b/app/comp/res_compat.py
@@ -271,9 +271,9 @@ class NotesTableCompat(ResultatsSemestre):
 
     def etud_has_decision(self, etudid):
         """True s'il y a une décision de jury pour cet étudiant"""
-        return self.get_etud_decision_ues(etudid) or self.get_etud_decision_sem(etudid)
+        return self.get_etud_decisions_ue(etudid) or self.get_etud_decision_sem(etudid)
 
-    def get_etud_decision_ues(self, etudid: int) -> dict:
+    def get_etud_decisions_ue(self, etudid: int) -> dict:
         """Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu.
         Ne tient pas compte des UE capitalisées.
         { ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : "d/m/y", 'ects' : x }
@@ -288,10 +288,10 @@ class NotesTableCompat(ResultatsSemestre):
     def get_etud_ects_valides(self, etudid: int, decisions_ues: dict = False) -> 0:
         """Le total des ECTS validés (et enregistrés) par l'étudiant dans ce semestre.
         NB: avant jury, rien d'enregistré, donc zéro ECTS.
-        Optimisation: si decisions_ues est passé, l'utilise, sinon appelle get_etud_decision_ues()
+        Optimisation: si decisions_ues est passé, l'utilise, sinon appelle get_etud_decisions_ue()
         """
         if decisions_ues is False:
-            decisions_ues = self.get_etud_decision_ues(etudid)
+            decisions_ues = self.get_etud_decisions_ue(etudid)
         if not decisions_ues:
             return 0.0
         return sum([d.get("ects", 0.0) for d in decisions_ues.values()])
diff --git a/app/models/etudiants.py b/app/models/etudiants.py
index cc1d4d528314948724a60ebff96036bb6ae0aaa6..c49717111bc049e003934390146418c9051cb1ff 100644
--- a/app/models/etudiants.py
+++ b/app/models/etudiants.py
@@ -183,6 +183,10 @@ class Identite(db.Model):
         e["etudid"] = self.id
         e["date_naissance"] = ndb.DateISOtoDMY(e["date_naissance"])
         e["ne"] = self.e
+        e["nomprenom"] = self.nomprenom
+        adresse = self.adresses.first()
+        if adresse:
+            e.update(adresse.to_dict())
         return {k: e[k] or "" for k in e}  # convert_null_outputs_to_empty
 
     def to_dict_bul(self, include_urls=True):
diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py
index 0379bbc2926429b8a7e3d01be314849808456f88..953697484bbd1e9db770d8cebcd627c0a29543c8 100644
--- a/app/scodoc/notes_table.py
+++ b/app/scodoc/notes_table.py
@@ -1102,7 +1102,7 @@ class NotesTable:
         else:
             return self.decisions_jury.get(etudid, None)
 
-    def get_etud_decision_ues(self, etudid):
+    def get_etud_decisions_ue(self, etudid):
         """Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu.
         Ne tient pas compte des UE capitalisées.
         { ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : }
@@ -1122,7 +1122,7 @@ class NotesTable:
 
     def etud_has_decision(self, etudid):
         """True s'il y a une décision de jury pour cet étudiant"""
-        return self.get_etud_decision_ues(etudid) or self.get_etud_decision_sem(etudid)
+        return self.get_etud_decisions_ue(etudid) or self.get_etud_decision_sem(etudid)
 
     def all_etuds_have_sem_decisions(self):
         """True si tous les étudiants du semestre ont une décision de jury.
diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py
index 1045e7ba49702feb13466cbac81ca747dfd751ad..1e9610c1bab4955f70bebee621f80bb1ef3b2759 100644
--- a/app/scodoc/sco_apogee_csv.py
+++ b/app/scodoc/sco_apogee_csv.py
@@ -433,7 +433,7 @@ class ApoEtud(dict):
                 return VOID_APO_RES
 
         # Elements UE
-        decisions_ue = nt.get_etud_decision_ues(etudid)
+        decisions_ue = nt.get_etud_decisions_ue(etudid)
         for ue in nt.get_ues_stat_dict():
             if ue["code_apogee"] and code in {
                 x.strip() for x in ue["code_apogee"].split(",")
diff --git a/app/scodoc/sco_bulletins_pdf.py b/app/scodoc/sco_bulletins_pdf.py
index 0e0883704511a15aa187712a8a5eccc3d3637ef3..a26397915ded351ebaeca4aa0eebed96304bac24 100644
--- a/app/scodoc/sco_bulletins_pdf.py
+++ b/app/scodoc/sco_bulletins_pdf.py
@@ -124,6 +124,24 @@ def replacement_function(match):
     )
 
 
+class WrapDict(object):
+    """Wrap a dict so that getitem returns '' when values are None"""
+
+    def __init__(self, adict, NoneValue=""):
+        self.dict = adict
+        self.NoneValue = NoneValue
+
+    def __getitem__(self, key):
+        try:
+            value = self.dict[key]
+        except KeyError:
+            return f"XXX {key} invalide XXX"
+        if value is None:
+            return self.NoneValue
+        else:
+            return value
+
+
 def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
     """Process a field given in preferences, returns
     - if format = 'pdf': a list of Platypus objects
@@ -137,18 +155,19 @@ def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
     If format = 'html', replaces <para> by <p>. HTML does not allow logos.
     """
     try:
-        text = (field or "") % scu.WrapDict(
-            cdict
-        )  # note that None values are mapped to empty strings
+        # None values are mapped to empty strings by WrapDict
+        text = (field or "") % WrapDict(cdict)
     except KeyError as exc:
+        missing_key = exc.args[0] if len(exc.args) > 0 else "?"
         log(
-            f"""process_field: KeyError on field={field!r}
+            f"""process_field: KeyError {missing_key} on field={field!r}
         values={pprint.pformat(cdict)}
         """
         )
-        if len(exc.args) > 0:
-            missing_field = exc.args[0]
-        text = f"""<para><i>format invalide: champs</i> {missing_field} <i>inexistant !</i></para>"""
+        text = f"""<para><i>format invalide: champs</i> {missing_key} <i>inexistant !</i></para>"""
+        scu.flash_once(
+            f"Attention: format PDF invalide (champs {field}, clef {missing_key})"
+        )
     except:  # pylint: disable=bare-except
         log(
             f"""process_field: invalid format. field={field!r}
@@ -156,7 +175,7 @@ def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
         """
         )
         # ne sera pas visible si lien vers pdf:
-        scu.flash_once(f"Attention: format PDF invalide (champs {field}")
+        scu.flash_once(f"Attention: format PDF invalide (champs {field})")
         text = (
             "<para><i>format invalide !</i></para><para>"
             + traceback.format_exc()
diff --git a/app/scodoc/sco_pv_dict.py b/app/scodoc/sco_pv_dict.py
index 0dcc421ffd39459da89897ec8c596ec1cb468cd8..9c059b62a1293712b4945146e378d51a2b63dfca 100644
--- a/app/scodoc/sco_pv_dict.py
+++ b/app/scodoc/sco_pv_dict.py
@@ -42,6 +42,7 @@ from app.models import (
     but_validations,
 )
 from app.scodoc import codes_cursus
+from app.scodoc import sco_edit_ue
 from app.scodoc import sco_etud
 from app.scodoc import sco_formsemestre
 from app.scodoc import sco_cursus
@@ -109,7 +110,7 @@ def dict_pvjury(
             etudid
         )  # I|D|DEF  (inscription ou démission ou défaillant)
         d["decision_sem"] = nt.get_etud_decision_sem(etudid)
-        d["decisions_ue"] = nt.get_etud_decision_ues(etudid)
+        d["decisions_ue"] = nt.get_etud_decisions_ue(etudid)
         if formsemestre.formation.is_apc():
             d.update(but_validations.dict_decision_jury(etud, formsemestre))
         d["last_formsemestre_id"] = Se.get_semestres()[
@@ -241,17 +242,17 @@ def _comp_ects_capitalises_by_ue_code(nt: NotesTableCompat, etudid: int):
     return ects_by_ue_code
 
 
-def _comp_ects_by_ue_code(nt, decision_ues):
+def _comp_ects_by_ue_code(nt, decisions_ue):
     """Calcul somme des ECTS validés dans ce semestre (sans les UE capitalisées)
-    decision_ues est le resultat de nt.get_etud_decision_ues
+    decisions_ue est le resultat de nt.get_etud_decisions_ue
     Chaque resultat est un dict: { ue_code : ects }
     """
-    if not decision_ues:
+    if not decisions_ue:
         return {}
 
     ects_by_ue_code = {}
-    for ue_id in decision_ues:
-        d = decision_ues[ue_id]
+    for ue_id in decisions_ue:
+        d = decisions_ue[ue_id]
         ue = UniteEns.query.get(ue_id)
         ects_by_ue_code[ue.ue_code] = d["ects"]
 
@@ -269,26 +270,22 @@ def _descr_decisions_ues(nt, etudid, decisions_ue, decision_sem) -> list[dict]:
         return []
     uelist = []
     # Les UE validées dans ce semestre:
-    for ue_id in decisions_ue.keys():
-        try:
-            if decisions_ue[ue_id] and (
-                codes_cursus.code_ue_validant(decisions_ue[ue_id]["code"])
-                or (
-                    (not nt.is_apc)
-                    and (
-                        # XXX ceci devrait dépendre du parcours et non pas être une option ! #sco8
-                        decision_sem
-                        and scu.CONFIG.CAPITALIZE_ALL_UES
-                        and codes_cursus.code_semestre_validant(decision_sem["code"])
-                    )
+    for ue_id in decisions_ue:
+        if decisions_ue[ue_id] and (
+            codes_cursus.code_ue_validant(decisions_ue[ue_id].get("code"))
+            or (
+                (not nt.is_apc)
+                and (
+                    # XXX ceci devrait dépendre du parcours et non pas être une option ! #sco8
+                    decision_sem
+                    and scu.CONFIG.CAPITALIZE_ALL_UES
+                    and decision_sem
+                    and codes_cursus.code_semestre_validant(decision_sem.get("code"))
                 )
-            ):
-                ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
-                uelist.append(ue)
-        except:
-            log(
-                f"Exception in descr_decisions_ues: ue_id={ue_id} decisions_ue={decisions_ue}"
             )
+        ):
+            ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
+            uelist.append(ue)
     # Les UE capitalisées dans d'autres semestres:
     if etudid in nt.validations.ue_capitalisees.index:
         for ue_id in nt.validations.ue_capitalisees.loc[[etudid]]["ue_id"]:
diff --git a/app/scodoc/sco_pv_lettres_inviduelles.py b/app/scodoc/sco_pv_lettres_inviduelles.py
index 7a4f2f50ab34ee32b94bc81d5be0b50ece986469..66868bcbe3db69d27c92c90710d9ed0507a76429 100644
--- a/app/scodoc/sco_pv_lettres_inviduelles.py
+++ b/app/scodoc/sco_pv_lettres_inviduelles.py
@@ -47,7 +47,6 @@ from app.models import FormSemestre, Identite
 import app.scodoc.sco_utils as scu
 from app.scodoc import sco_bulletins_pdf
 from app.scodoc import sco_pv_dict
-from app.scodoc import sco_etud
 from app.scodoc import sco_pdf
 from app.scodoc import sco_preferences
 from app.scodoc.sco_exceptions import ScoValueError
@@ -70,9 +69,6 @@ def pdf_lettres_individuelles(
     dpv = sco_pv_dict.dict_pvjury(formsemestre_id, etudids=etudids, with_prev=True)
     if not dpv:
         return ""
-    # Ajoute infos sur etudiants
-    etuds = [x["identite"] for x in dpv["decisions"]]
-    sco_etud.fill_etuds_info(etuds)
     #
     formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
     prefs = sco_preferences.SemPreferences(formsemestre_id)
@@ -95,9 +91,10 @@ def pdf_lettres_individuelles(
             decision["decision_sem"]
             or decision.get("decision_annee")
             or decision.get("decision_rcue")
+            or decision.get("decisions_ue")
         ):  # decision prise
             etud: Identite = Identite.query.get(decision["identite"]["etudid"])
-            params["nomEtud"] = etud.nomprenom
+            params["nomEtud"] = etud.nomprenom  # backward compat
             bookmarks[npages + 1] = scu.suppress_accents(etud.nomprenom)
             try:
                 objects += pdf_lettre_individuelle(
@@ -217,7 +214,7 @@ def pdf_lettre_individuelle(sem, decision, etud: Identite, params, signature=Non
 
     params.update(decision["identite"])
     # fix domicile
-    if params["domicile"]:
+    if params.get("domicile"):
         params["domicile"] = params["domicile"].replace("\\n", "<br/>")
 
     # UE capitalisées:
diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py
index bf8d8c296122b05402e3fbd3231c0d459be51258..b8a2de08ca8668fef71a12532bab8ea263812f28 100644
--- a/app/scodoc/sco_saisie_notes.py
+++ b/app/scodoc/sco_saisie_notes.py
@@ -953,7 +953,7 @@ def has_existing_decision(M, E, etudid):
     nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
     if nt.get_etud_decision_sem(etudid):
         return True
-    dec_ues = nt.get_etud_decision_ues(etudid)
+    dec_ues = nt.get_etud_decisions_ue(etudid)
     if dec_ues:
         mod = sco_edit_module.module_list({"module_id": M["module_id"]})[0]
         ue_id = mod["ue_id"]
diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py
index 34ea2f0d1c3453d859dd87a1d0313bab8cb47653..abdff099e6658303f922f027f2a25c56d13fa51b 100644
--- a/app/scodoc/sco_utils.py
+++ b/app/scodoc/sco_utils.py
@@ -291,21 +291,6 @@ class DictDefault(dict):  # obsolete, use collections.defaultdict
         return value
 
 
-class WrapDict(object):
-    """Wrap a dict so that getitem returns '' when values are None"""
-
-    def __init__(self, adict, NoneValue=""):
-        self.dict = adict
-        self.NoneValue = NoneValue
-
-    def __getitem__(self, key):
-        value = self.dict[key]
-        if value is None:
-            return self.NoneValue
-        else:
-            return value
-
-
 def group_by_key(d, key):
     gr = DictDefault(defaultvalue=[])
     for e in d:
diff --git a/app/tables/jury_recap.py b/app/tables/jury_recap.py
index 78ed33b5cc43a26aa5cff8d3f4732b9703443b38..f10437a8daa96a1a2da55436e7145b0b89843354 100644
--- a/app/tables/jury_recap.py
+++ b/app/tables/jury_recap.py
@@ -158,7 +158,7 @@ class TableJury(TableRecap):
                             titre,
                             validation_rcue.code,
                             group="cursus_" + annee,
-                            classes=["recorded_code"],
+                            classes=[],
                             column_classes=["cursus_but" + (" first" if first else "")],
                             target_attrs={
                                 "title": f"{niveau.competence.titre} niveau {niveau.ordre}"
@@ -221,7 +221,7 @@ class RowJury(RowRecap):
         "Ajoute 2 colonnes: moyenne d'UE et code jury"
         # table recap standard (mais avec group différent)
         super().add_ue_cols(ue, ue_status, col_group=col_group or "col_ue")
-        dues = self.table.res.get_etud_decision_ues(self.etud.id)
+        dues = self.table.res.get_etud_decisions_ue(self.etud.id)
         due = dues.get(ue.id) if dues else None
 
         col_id = f"moy_ue_{ue.id}_code"
diff --git a/tests/unit/test_sco_basic.py b/tests/unit/test_sco_basic.py
index 5b5f85e6f6bc4e47a7e65149f1c5cc66c68dfed8..0030d036d8686dd001ba78e50a7636ba5c914a5b 100644
--- a/tests/unit/test_sco_basic.py
+++ b/tests/unit/test_sco_basic.py
@@ -233,7 +233,7 @@ def run_sco_basic(verbose=False) -> FormSemestre:
     formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
     nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
     for etud in etuds[:5]:
-        dec_ues = nt.get_etud_decision_ues(etud["etudid"])
+        dec_ues = nt.get_etud_decisions_ue(etud["etudid"])
         for ue_id in dec_ues:
             assert dec_ues[ue_id]["code"] in {"ADM", "CMP"}