diff --git a/README.md b/README.md
index 209a2a01766d82a2e9a501ecd01bacecd420c90d..f571216abc8bc1ce34f6db680f60d2b53a947155 100644
--- a/README.md
+++ b/README.md
@@ -20,10 +20,10 @@ Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
 
 ### État actuel (26 jan 22)
 
- - 9.1 (master) reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf: 
+ - 9.1.5x (master) reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf: 
     - ancien module "Entreprises" (obsolète) et ajoute la gestion du BUT.
 
- - 9.2 (branche refactor_nt) est la version de développement.
+ - 9.2 (branche dev92) est la version de développement.
 
  
 ### Lignes de commandes
diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py
index 5d4fd74f34d3e946b2ef3d50b9e758e958801721..771746bf4a6fcd8332c0141018bba4f225f8cfc3 100644
--- a/app/but/bulletin_but.py
+++ b/app/but/bulletin_but.py
@@ -9,14 +9,15 @@
 
 import datetime
 from flask import url_for, g
-from app.models.formsemestre import FormSemestre
 
+from app.comp.res_but import ResultatsSemestreBUT
+from app.models import FormSemestre, Identite
 from app.scodoc import sco_utils as scu
 from app.scodoc import sco_bulletins_json
+from app.scodoc import sco_bulletins_pdf
 from app.scodoc import sco_preferences
 from app.scodoc.sco_codes_parcours import UE_SPORT
 from app.scodoc.sco_utils import fmt_note
-from app.comp.res_but import ResultatsSemestreBUT
 
 
 class BulletinBUT:
@@ -28,6 +29,7 @@ class BulletinBUT:
     def __init__(self, formsemestre: FormSemestre):
         """ """
         self.res = ResultatsSemestreBUT(formsemestre)
+        self.prefs = sco_preferences.SemPreferences(formsemestre.id)
 
     def etud_ue_mod_results(self, etud, ue, modimpls) -> dict:
         "dict synthèse résultats dans l'UE pour les modules indiqués"
@@ -84,7 +86,7 @@ class BulletinBUT:
             "saes": self.etud_ue_mod_results(etud, ue, res.saes),
         }
         if ue.type != UE_SPORT:
-            if sco_preferences.get_preference("bul_show_ue_rangs", res.formsemestre.id):
+            if self.prefs["bul_show_ue_rangs"]:
                 rangs, effectif = res.ue_rangs[ue.id]
                 rang = rangs[etud.id]
             else:
@@ -155,9 +157,7 @@ class BulletinBUT:
                         if e.visibulletin
                         and (
                             modimpl_results.evaluations_etat[e.id].is_complete
-                            or sco_preferences.get_preference(
-                                "bul_show_all_evals", res.formsemestre.id
-                            )
+                            or self.prefs["bul_show_all_evals"]
                         )
                     ],
                 }
@@ -216,9 +216,11 @@ class BulletinBUT:
         else:
             return f"Bonus de {fmt_note(bonus_vect.iloc[0])}"
 
-    def bulletin_etud(self, etud, formsemestre, force_publishing=False) -> dict:
-        """Le bulletin de l'étudiant dans ce semestre.
-        Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai
+    def bulletin_etud(
+        self, etud: Identite, formsemestre, force_publishing=False
+    ) -> dict:
+        """Le bulletin de l'étudiant dans ce semestre: dict pour la version JSON / HTML.
+        - Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai
         (bulletins non publiés).
         """
         res = self.res
@@ -239,7 +241,9 @@ class BulletinBUT:
             },
             "formsemestre_id": formsemestre.id,
             "etat_inscription": etat_inscription,
-            "options": sco_preferences.bulletin_option_affichage(formsemestre.id),
+            "options": sco_preferences.bulletin_option_affichage(
+                formsemestre.id, self.prefs
+            ),
         }
         if not published:
             return d
@@ -312,3 +316,12 @@ class BulletinBUT:
             )
 
         return d
+
+    def bulletin_etud_complet(self, etud) -> dict:
+        """Bulletin dict complet avec toutes les infos pour les bulletins pdf"""
+        d = self.bulletin_etud(force_publishing=True)
+        d["filigranne"] = sco_bulletins_pdf.get_filigranne(
+            self.res.get_etud_etat(etud.id), self.prefs
+        )
+        # XXX TODO A COMPLETER
+        raise NotImplementedError()
diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index 245142484393afebdb6790c1dac38b7276a0d13a..2d491d7edcf1e5110c2900b1d0875130de871ed3 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -117,6 +117,7 @@ class FormSemestre(db.Model):
         return f"<{self.__class__.__name__} {self.id} {self.titre_num()}>"
 
     def to_dict(self):
+        "dict (compatible ScoDoc7)"
         d = dict(self.__dict__)
         d.pop("_sa_instance_state", None)
         # ScoDoc7 output_formators: (backward compat)
diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py
index 93a926b64fbfb602950623a0316fa53b31160226..642841058ae30a68a8875c3e268f4960474fb749 100644
--- a/app/scodoc/sco_bulletins.py
+++ b/app/scodoc/sco_bulletins.py
@@ -28,30 +28,21 @@
 """Génération des bulletins de notes
 
 """
-from app.models import formsemestre
-import time
-import pprint
 import email
-from email.mime.multipart import MIMEMultipart
-from email.mime.text import MIMEText
-from email.mime.base import MIMEBase
-from email.header import Header
-from reportlab.lib.colors import Color
-import urllib
+import pprint
+import time
 
 from flask import g, request
 from flask import url_for
 from flask_login import current_user
 from flask_mail import Message
-from app.models.moduleimpls import ModuleImplInscription
 
-import app.scodoc.sco_utils as scu
-from app.scodoc.sco_utils import ModuleType
-import app.scodoc.notesdb as ndb
+from app import email
 from app import log
+from app.but import bulletin_but
 from app.comp import res_sem
 from app.comp.res_common import NotesTableCompat
-from app.models import FormSemestre
+from app.models import FormSemestre, Identite, ModuleImplInscription
 from app.scodoc.sco_permissions import Permission
 from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
 from app.scodoc import html_sco_header
@@ -60,9 +51,9 @@ from app.scodoc import sco_abs
 from app.scodoc import sco_abs_views
 from app.scodoc import sco_bulletins_generator
 from app.scodoc import sco_bulletins_json
+from app.scodoc import sco_bulletins_pdf
 from app.scodoc import sco_bulletins_xml
 from app.scodoc import sco_codes_parcours
-from app.scodoc import sco_cache
 from app.scodoc import sco_etud
 from app.scodoc import sco_evaluation_db
 from app.scodoc import sco_formations
@@ -73,7 +64,9 @@ from app.scodoc import sco_photos
 from app.scodoc import sco_preferences
 from app.scodoc import sco_pvjury
 from app.scodoc import sco_users
-from app import email
+import app.scodoc.sco_utils as scu
+from app.scodoc.sco_utils import ModuleType
+import app.scodoc.notesdb as ndb
 
 # ----- CLASSES DE BULLETINS DE NOTES
 from app.scodoc import sco_bulletins_standard
@@ -190,28 +183,18 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
         show_mention=prefs["bul_show_mention"],
     )
 
-    if dpv:
-        I["decision_sem"] = dpv["decisions"][0]["decision_sem"]
-    else:
-        I["decision_sem"] = ""
     I.update(infos)
 
     I["etud_etat_html"] = _get_etud_etat_html(
         formsemestre.etuds_inscriptions[etudid].etat
     )
     I["etud_etat"] = nt.get_etud_etat(etudid)
-    I["filigranne"] = ""
+    I["filigranne"] = sco_bulletins_pdf.get_filigranne(I["etud_etat"], prefs)
     I["demission"] = ""
-    if I["etud_etat"] == "D":
+    if I["etud_etat"] == scu.DEMISSION:
         I["demission"] = "(Démission)"
-        I["filigranne"] = "Démission"
     elif I["etud_etat"] == sco_codes_parcours.DEF:
         I["demission"] = "(Défaillant)"
-        I["filigranne"] = "Défaillant"
-    elif (prefs["bul_show_temporary"] and not I["decision_sem"]) or prefs[
-        "bul_show_temporary_forced"
-    ]:
-        I["filigranne"] = prefs["bul_temporary_txt"]
 
     # --- Appreciations
     cnx = ndb.GetDBConnexion()
@@ -687,6 +670,7 @@ def etud_descr_situation_semestre(
     descr_defaillance  : "Défaillant" ou vide si non défaillant.
     decision_jury     :  "Validé", "Ajourné", ... (code semestre)
     descr_decision_jury : "Décision jury: Validé" (une phrase)
+    decision_sem        :
     decisions_ue        : noms (acronymes) des UE validées, séparées par des virgules.
     descr_decisions_ue  : ' UE acquises: UE1, UE2', ou vide si pas de dec. ou si pas show_uevalid
     descr_mention : 'Mention Bien', ou vide si pas de mention ou si pas show_mention
@@ -696,7 +680,7 @@ def etud_descr_situation_semestre(
 
     # --- Situation et décisions jury
 
-    # demission/inscription ?
+    # démission/inscription ?
     events = sco_etud.scolar_events_list(
         cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
     )
@@ -763,11 +747,15 @@ def etud_descr_situation_semestre(
         infos["situation"] += " " + infos["descr_defaillance"]
 
     dpv = sco_pvjury.dict_pvjury(formsemestre_id, etudids=[etudid])
+    if dpv:
+        infos["decision_sem"] = dpv["decisions"][0]["decision_sem"]
+    else:
+        infos["decision_sem"] = ""
 
     if not show_decisions:
         return infos, dpv
 
-    # Decisions de jury:
+    # Décisions de jury:
     pv = dpv["decisions"][0]
     dec = ""
     if pv["decision_sem_descr"]:
@@ -819,11 +807,15 @@ def formsemestre_bulletinetud(
     except:
         sco_etud.log_unknown_etud()
         raise ScoValueError("étudiant inconnu")
-    # API, donc erreurs admises en ScoValueError
-    sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
+
+    formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
+    if not formsemestre:
+        # API, donc erreurs admises
+        raise ScoValueError(f"semestre {formsemestre_id} inconnu !")
+    sem = formsemestre.to_dict()
 
     bulletin = do_formsemestre_bulletinetud(
-        formsemestre_id,
+        formsemestre,
         etudid,
         format=format,
         version=version,
@@ -835,7 +827,6 @@ def formsemestre_bulletinetud(
         filename = scu.bul_filename(sem, etud, format)
         return scu.send_file(bulletin, filename, mime=scu.get_mime_suffix(format)[0])
 
-    sem = sco_formsemestre.get_formsemestre(formsemestre_id)
     H = [
         _formsemestre_bulletinetud_header_html(
             etud, etudid, sem, formsemestre_id, format, version
@@ -892,14 +883,14 @@ def can_send_bulletin_by_mail(formsemestre_id):
 
 
 def do_formsemestre_bulletinetud(
-    formsemestre_id,
-    etudid,
+    formsemestre: FormSemestre,
+    etudid: int,
     version="long",  # short, long, selectedevals
     format="html",
     nohtml=False,
-    xml_with_decisions=False,  # force decisions dans XML
-    force_publishing=False,  # force publication meme si semestre non publie sur "portail"
-    prefer_mail_perso=False,  # mails envoyes sur adresse perso si non vide
+    xml_with_decisions=False,  # force décisions dans XML
+    force_publishing=False,  # force publication meme si semestre non publié sur "portail"
+    prefer_mail_perso=False,  # mails envoyés sur adresse perso si non vide
 ):
     """Génère le bulletin au format demandé.
     Retourne: (bul, filigranne)
@@ -908,7 +899,7 @@ def do_formsemestre_bulletinetud(
     """
     if format == "xml":
         bul = sco_bulletins_xml.make_xml_formsemestre_bulletinetud(
-            formsemestre_id,
+            formsemestre.id,
             etudid,
             xml_with_decisions=xml_with_decisions,
             force_publishing=force_publishing,
@@ -919,7 +910,7 @@ def do_formsemestre_bulletinetud(
 
     elif format == "json":
         bul = sco_bulletins_json.make_json_formsemestre_bulletinetud(
-            formsemestre_id,
+            formsemestre.id,
             etudid,
             xml_with_decisions=xml_with_decisions,
             force_publishing=force_publishing,
@@ -927,8 +918,13 @@ def do_formsemestre_bulletinetud(
         )
         return bul, ""
 
-    I = formsemestre_bulletinetud_dict(formsemestre_id, etudid)
-    etud = I["etud"]
+    if formsemestre.formation.is_apc():
+        etud = Identite.query.get(etudid)
+        r = bulletin_but.BulletinBUT(formsemestre)
+        I = r.bulletin_etud_complet(etud, formsemestre)
+    else:
+        I = formsemestre_bulletinetud_dict(formsemestre.id, etudid)
+        etud = I["etud"]
 
     if format == "html":
         htm, _ = sco_bulletins_generator.make_formsemestre_bulletinetud(
@@ -954,7 +950,7 @@ def do_formsemestre_bulletinetud(
     elif format == "pdfmail":
         # format pdfmail: envoie le pdf par mail a l'etud, et affiche le html
         # check permission
-        if not can_send_bulletin_by_mail(formsemestre_id):
+        if not can_send_bulletin_by_mail(formsemestre.id):
             raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
 
         if nohtml:
@@ -983,7 +979,7 @@ def do_formsemestre_bulletinetud(
                 ) + htm
             return h, I["filigranne"]
         #
-        mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr)
+        mail_bulletin(formsemestre.id, I, pdfdata, filename, recipient_addr)
         emaillink = '<a class="stdlink" href="mailto:%s">%s</a>' % (
             recipient_addr,
             recipient_addr,
diff --git a/app/scodoc/sco_bulletins_generator.py b/app/scodoc/sco_bulletins_generator.py
index 04a9efaee4c4e1413e162c9107a7fea339c4c0d6..aafdc09f69c64bd80590009aa4dddc3f37a5b03c 100644
--- a/app/scodoc/sco_bulletins_generator.py
+++ b/app/scodoc/sco_bulletins_generator.py
@@ -99,7 +99,7 @@ def bulletin_get_class_name_displayed(formsemestre_id):
         return "invalide ! (voir paramètres)"
 
 
-class BulletinGenerator(object):
+class BulletinGenerator:
     "Virtual superclass for PDF bulletin generators" ""
     # Here some helper methods
     # see sco_bulletins_standard.BulletinGeneratorStandard subclass for real methods
diff --git a/app/scodoc/sco_bulletins_pdf.py b/app/scodoc/sco_bulletins_pdf.py
index 94cfcf6bc22c0ca4e8a860c33374a7893aebc79a..748fd5a0b7d44f69ad59c4d14ebdeee61784ec9b 100644
--- a/app/scodoc/sco_bulletins_pdf.py
+++ b/app/scodoc/sco_bulletins_pdf.py
@@ -61,12 +61,10 @@ from reportlab.platypus.doctemplate import BaseDocTemplate
 from flask import g, request
 
 from app import log, ScoValueError
-from app.comp import res_sem
-from app.comp.res_common import NotesTableCompat
 from app.models import FormSemestre
 
 from app.scodoc import sco_cache
-from app.scodoc import sco_formsemestre
+from app.scodoc import sco_codes_parcours
 from app.scodoc import sco_pdf
 from app.scodoc import sco_preferences
 from app.scodoc import sco_etud
@@ -190,7 +188,7 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
     i = 1
     for etud in formsemestre.get_inscrits(include_demdef=True, order=True):
         frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud(
-            formsemestre_id,
+            formsemestre,
             etud.id,
             format="pdfpart",
             version=version,
@@ -239,8 +237,9 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"):
     filigrannes = {}
     i = 1
     for sem in etud["sems"]:
+        formsemestre = FormSemestre.query.get(sem["formsemestre_id"])
         frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud(
-            sem["formsemestre_id"],
+            formsemestre,
             etudid,
             format="pdfpart",
             version=version,
@@ -275,3 +274,16 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"):
     )
 
     return pdfdoc, filename
+
+
+def get_filigranne(etud_etat: str, prefs) -> str:
+    """Texte à placer en "filigranne" sur le bulletin pdf"""
+    if etud_etat == scu.DEMISSION:
+        return "Démission"
+    elif etud_etat == sco_codes_parcours.DEF:
+        return "Défaillant"
+    elif (prefs["bul_show_temporary"] and not I["decision_sem"]) or prefs[
+        "bul_show_temporary_forced"
+    ]:
+        return prefs["bul_temporary_txt"]
+    return ""
diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py
index b639d5ee406a4a267e2b5f8f196a3a6ca18e22c6..e0fcc53788f6851ae75d80c853607102780aa377 100644
--- a/app/scodoc/sco_preferences.py
+++ b/app/scodoc/sco_preferences.py
@@ -2114,7 +2114,7 @@ class BasePreferences(object):
         return form
 
 
-class SemPreferences(object):
+class SemPreferences:
     """Preferences for a formsemestre"""
 
     def __init__(self, formsemestre_id=None):
@@ -2270,9 +2270,8 @@ def doc_preferences():
     return "\n".join([" | ".join(x) for x in L])
 
 
-def bulletin_option_affichage(formsemestre_id: int) -> dict:
+def bulletin_option_affichage(formsemestre_id: int, prefs: SemPreferences) -> dict:
     "dict avec les options d'affichages (préférences) pour ce semestre"
-    prefs = SemPreferences(formsemestre_id)
     fields = (
         "bul_show_abs",
         "bul_show_abs_modules",
diff --git a/app/views/notes.py b/app/views/notes.py
index efad2808b819f6d114884e800cd1904c024f97d6..b0b812a16f81dc4021313df0ed4f12993b097304 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -1925,7 +1925,7 @@ def formsemestre_bulletins_mailetuds(
     nb_send = 0
     for etudid in etudids:
         h, _ = sco_bulletins.do_formsemestre_bulletinetud(
-            formsemestre_id,
+            formsemestre,
             etudid,
             version=version,
             prefer_mail_perso=prefer_mail_perso,
diff --git a/sco_version.py b/sco_version.py
index 23fba00697c6fb99cf55c067f3fd0eb29848633d..035ab07f290463fddd66590734195d5a5dcf723d 100644
--- a/sco_version.py
+++ b/sco_version.py
@@ -1,7 +1,7 @@
 # -*- mode: python -*-
 # -*- coding: utf-8 -*-
 
-SCOVERSION = "9.1.56"
+SCOVERSION = "9.2a-57"
 
 SCONAME = "ScoDoc"