diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py
index 32cf27c0..5ac78d9f 100644
--- a/app/scodoc/sco_excel.py
+++ b/app/scodoc/sco_excel.py
@@ -40,10 +40,15 @@ from openpyxl.cell import WriteOnlyCell
 from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
 from openpyxl.styles.numbers import FORMAT_NUMBER_00, FORMAT_GENERAL
 from openpyxl.comments import Comment
+from openpyxl import Workbook, load_workbook
+from openpyxl.cell import WriteOnlyCell
+from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
 
 import app.scodoc.sco_utils as scu
 from app.scodoc import notesdb
 from app.scodoc import sco_preferences
+from app.scodoc.sco_exceptions import ScoValueError
+from app.scodoc import sco_preferences
 from app import log
 from app.scodoc.sco_exceptions import ScoValueError
 
@@ -79,52 +84,14 @@ def send_excel_file(request, data, filename, mime=scu.XLSX_MIMETYPE):
 # font, border, number_format, fill, ..  (cf https://openpyxl.readthedocs.io/en/stable/styles.html#working-with-styles)
 
 
-#  (stolen from xlrd)
-# Convert an Excel number (presumed to represent a date, a datetime or a time) into
-# a Python datetime.datetime
-# @param xldate The Excel number
-# @param datemode 0: 1900-based, 1: 1904-based.
-# @return a datetime.datetime object, to the nearest_second.
-# <br>Special case: if 0.0 <= xldate < 1.0, it is assumed to represent a time;
-# a datetime.time object will be returned.
-# <br>Note: 1904-01-01 is not regarded as a valid date in the datemode 1 system; its "serial number"
-# is zero.
-#
-# _XLDAYS_TOO_LARGE = (2958466, 2958466 - 1462)  # This is equivalent to 10000-01-01
-#
-
-
 def xldate_as_datetime(xldate, datemode=0):
     return openpyxl.utils.datetime.from_ISO8601(xldate)
-    # if datemode not in (0, 1):
-    #     raise ValueError("invalid mode %s" % datemode)
-    # if xldate == 0.00:
-    #     return datetime.time(0, 0, 0)
-    # if xldate < 0.00:
-    #     raise ValueError("invalid date code %s" % xldate)
-    # xldays = int(xldate)
-    # frac = xldate - xldays
-    # seconds = int(round(frac * 86400.0))
-    # assert 0 <= seconds <= 86400
-    # if seconds == 86400:
-    #     seconds = 0
-    #     xldays += 1
-    # if xldays >= _XLDAYS_TOO_LARGE[datemode]:
-    #     raise ValueError("date too large %s" % xldate)
-    #
-    # if xldays == 0:
-    #     # second = seconds % 60; minutes = seconds // 60
-    #     minutes, second = divmod(seconds, 60)
-    #     # minute = minutes % 60; hour    = minutes // 60
-    #     hour, minute = divmod(minutes, 60)
-    #     return datetime.time(hour, minute, second)
-    #
-    # if xldays < 61 and datemode == 0:
-    #     raise ValueError("ambiguous date %s" % xldate)
-    #
-    # return datetime.datetime.fromordinal(
-    #     xldays + 693594 + 1462 * datemode
-    # ) + datetime.timedelta(seconds=seconds)
+
+
+def adjust_sheetname(sheet_name):
+    # Le nom de la feuille ne peut faire plus de 31 caractères.
+    # si la taille du nom de feuille est > 31 on tronque (on pourrait remplacer par 'feuille' ?)
+    return sheet_name[:31]
 
 
 class ScoExcelBook:
@@ -139,13 +106,16 @@ class ScoExcelBook:
 
     def __init__(self):
         self.sheets = []  # list of sheets
+        self.wb = Workbook(write_only=True)
 
     def create_sheet(self, sheet_name="feuille", default_style=None):
         """Crée une nouvelle feuille dans ce classeur
         sheet_name -- le nom de la feuille
         default_style -- le style par défaut
         """
-        sheet = ScoExcelSheet(sheet_name, default_style)
+        sheet_name = adjust_sheetname(sheet_name)
+        ws = self.wb.create_sheet(sheet_name)
+        sheet = ScoExcelSheet(sheet_name, default_style, ws)
         self.sheets.append(sheet)
         return sheet
 
@@ -153,12 +123,11 @@ class ScoExcelBook:
         """génération d'un stream binaire représentant la totalité du classeur.
         retourne le flux
         """
-        wb = Workbook(write_only=True)
         for sheet in self.sheets:
-            sheet.generate(self)
+            sheet.prepare()
         # construction d'un flux (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream)
         with NamedTemporaryFile() as tmp:
-            wb.save(tmp.name)
+            self.wb.save(tmp.name)
             tmp.seek(0)
             return tmp.read()
 
@@ -166,6 +135,7 @@ class ScoExcelBook:
 def excel_make_style(
     bold=False,
     italic=False,
+    outline=False,
     color: COLORS = COLORS.BLACK,
     bgcolor: COLORS = None,
     halign=None,
@@ -186,7 +156,14 @@ def excel_make_style(
     size -- taille de police
     """
     style = {}
-    font = Font(name=font_name, bold=bold, italic=italic, color=color.value, size=size)
+    font = Font(
+        name=font_name,
+        bold=bold,
+        italic=italic,
+        outline=outline,
+        color=color.value,
+        size=size,
+    )
     style["font"] = font
     if bgcolor:
         style["fill"] = PatternFill(fill_type="solid", fgColor=bgcolor.value)
@@ -222,42 +199,94 @@ class ScoExcelSheet:
     * pour finit appel de la méthode de génération
     """
 
-    def __init__(self, sheet_name="feuille", default_style=None, wb=None):
-        """Création de la feuille.
-        sheet_name -- le nom de la feuille
-        default_style -- le style par défaut des cellules
-        wb -- le WorkBook dans laquelle se trouve la feuille. Si wb est None (cas d'un classeur mono-feuille),
-         un workbook est crée et associé à cette feuille.
+    def __init__(self, sheet_name="feuille", default_style=None, ws=None):
+        """Création de la feuille. sheet_name
+        -- le nom de la feuille default_style
+        -- le style par défaut des cellules ws
+        -- None si la feuille est autonome (dans ce cas ell crée son propre wb), sinon c'est la worksheet
+        créée par le workbook propriétaire un workbook est crée et associé à cette feuille.
         """
         # Le nom de la feuille ne peut faire plus de 31 caractères.
         # si la taille du nom de feuille est > 31 on tronque (on pourrait remplacer par 'feuille' ?)
-        self.sheet_name = sheet_name[
-            :31
-        ]  # if len(sheet_name) > 31: sheet_name = 'Feuille' ?
-        self.rows = []  # list of list of cells
-        # self.cells_styles_lico = {}  # { (li,co) : style }
-        # self.cells_styles_li = {}  # { li : style }
-        # self.cells_styles_co = {}  # { co : style }
+        self.sheet_name = adjust_sheetname(sheet_name)
         if default_style is None:
             default_style = excel_make_style()
         self.default_style = default_style
-        self.wb = wb or Workbook(write_only=True)  # Création de workbook si nécessaire
-        self.ws = self.wb.create_sheet(title=self.sheet_name)
+        if ws is None:
+            self.wb = Workbook()
+            self.ws = self.wb.active
+            self.ws.title = self.sheet_name
+        else:
+            self.wb = None
+            self.ws = ws
+        # internal data
+        self.rows = []  # list of list of cells
         self.column_dimensions = {}
+        self.row_dimensions = {}
 
-    def set_column_dimension_width(self, cle, value):
-        """Détermine la largeur d'une colonne.
-        cle -- identifie la colonne ("A"n "B", ...)
-        value -- la dimension (unité : 7 pixels comme affiché dans Excel)
+    def excel_make_composite_style(
+        self,
+        alignment=None,
+        border=None,
+        fill=None,
+        number_format=None,
+        font=None,
+    ):
+        style = {}
+        if font is not None:
+            style["font"] = font
+        if alignment is not None:
+            style["alignment"] = alignment
+        if border is not None:
+            style["border"] = border
+        if fill is not None:
+            style["fill"] = fill
+        if number_format is None:
+            style["number_format"] = FORMAT_GENERAL
+        else:
+            style["number_format"] = number_format
+        return style
+
+    @staticmethod
+    def i2col(idx):
+        if idx < 26:  # one letter key
+            return chr(idx + 65)
+        else:  # two letters AA..ZZ
+            first = (idx // 26) + 66
+            second = (idx % 26) + 65
+            return "" + chr(first) + chr(second)
+
+    def set_column_dimension_width(self, cle=None, value=21):
+        """Détermine la largeur d'une colonne. cle -- identifie la colonne ("A" "B", ... ou 0, 1, 2, ...) si None,
+        value donne la liste des largeurs de colonnes depuis A, B, C, ... value -- la dimension (unité : 7 pixels
+        comme affiché dans Excel)
         """
-        self.ws.column_dimensions[cle].width = value
+        if cle is None:
+            for i, val in enumerate(value):
+                self.ws.column_dimensions[self.i2col(i)].width = val
+            # No keys: value is a list of widths
+        elif type(cle) == str:  # accepts set_column_with("D", ...)
+            self.ws.column_dimensions[cle].width = value
+        else:
+            self.ws.column_dimensions[self.i2col(cle)].width = value
 
-    def set_column_dimension_hidden(self, cle, value):
-        """Masque ou affiche une colonne.
-        cle -- identifie la colonne ("A"n "B", ...)
+    def set_row_dimension_height(self, cle=None, value=21):
+        """Détermine la hauteur d'une ligne. cle -- identifie la ligne (1, 2, ...) si None,
+        value donne la liste des hauteurs de colonnes depuis 1, 2, 3, ... value -- la dimension
+        """
+        if cle is None:
+            for i, val in enumerate(value, start=1):
+                self.ws.row_dimensions[i].height = val
+            # No keys: value is a list of widths
+        else:
+            self.ws.row_dimensions[cle].height = value
+
+    def set_row_dimension_hidden(self, cle, value):
+        """Masque ou affiche une ligne.
+        cle -- identifie la colonne (1...)
         value -- boolean (vrai = colonne cachée)
         """
-        self.ws.column_dimensions[cle].hidden = value
+        self.ws.row_dimensions[cle].hidden = value
 
     def make_cell(self, value: any = None, style=None, comment=None):
         """Construit une cellule.
@@ -271,8 +300,12 @@ class ScoExcelSheet:
             style = self.default_style
         if "font" in style:
             cell.font = style["font"]
+        if "alignment" in style:
+            cell.alignment = style["alignment"]
         if "border" in style:
             cell.border = style["border"]
+        if "fill" in style:
+            cell.fill = style["fill"]
         if "number_format" in style:
             cell.number_format = style["number_format"]
         if "fill" in style:
@@ -311,73 +344,31 @@ class ScoExcelSheet:
         """ajoute une ligne déjà construite à la feuille."""
         self.rows.append(row)
 
-    # def set_style(self, style=None, li=None, co=None):
-    #     if li is not None and co is not None:
-    #         self.cells_styles_lico[(li, co)] = style
-    #     elif li is None:
-    #         self.cells_styles_li[li] = style
-    #     elif co is None:
-    #         self.cells_styles_co[co] = style
-    #
-    # def get_cell_style(self, li, co):
-    #     """Get style for specified cell"""
-    #     return (
-    #             self.cells_styles_lico.get((li, co), None)
-    #             or self.cells_styles_li.get(li, None)
-    #             or self.cells_styles_co.get(co, None)
-    #             or self.default_style
-    #     )
-
-    def _generate_ws(self):
+    def prepare(self):
         """génére un flux décrivant la feuille.
         Ce flux pourra ensuite être repris dans send_excel_file (classeur mono feille)
         ou pour la génération d'un classeur multi-feuilles
         """
         for col in self.column_dimensions.keys():
             self.ws.column_dimensions[col] = self.column_dimensions[col]
+        for col in self.row_dimensions.keys():
+            self.ws.row_dimensions[col] = self.row_dimensions[col]
         for row in self.rows:
             self.ws.append(row)
 
-    def generate_standalone(self):
+    def generate(self):
         """génération d'un classeur mono-feuille"""
-        self._generate_ws()
+        # this method makes sense only if it is a standalone worksheet (else call workbook.generate()
+        if self.wb is None:  # embeded sheet
+            raise ScoValueError("can't generate a single sheet from a ScoWorkbook")
+
         # construction d'un flux (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream)
+        self.prepare()
         with NamedTemporaryFile() as tmp:
             self.wb.save(tmp.name)
             tmp.seek(0)
             return tmp.read()
 
-    def generate_embeded(self):
-        """generation d'une feuille include dans un classeur multi-feuilles"""
-        self._generate_ws()
-
-    def gen_workbook(self, wb=None):
-        """TODO: à remplacer"""
-        """Generates and returns a workbook from stored data.
-        If wb, add a sheet (tab) to the existing workbook (in this case, returns None).
-        """
-        if wb is None:
-            wb = Workbook()  # Création du fichier
-            sauvegarde = True
-        else:
-            sauvegarde = False
-        ws0 = wb.add_sheet(self.sheet_name)
-        li = 0
-        for row in self.rows:
-            co = 0
-            for c in row:
-                # safety net: allow only str, int and float
-                # #py3 #sco8 A revoir lors de la ré-écriture de ce module
-                # XXX if type(c) not in (IntType, FloatType):
-                #    c = str(c).decode(scu.SCO_ENCODING)
-                ws0.write(li, co, c, self.get_cell_style(li, co))
-                co += 1
-            li += 1
-        if sauvegarde:
-            return wb.savetostr()
-        else:
-            return None
-
 
 def excel_simple_table(
     titles=None, lines=None, sheet_name=b"feuille", titles_styles=None, comments=None
@@ -416,7 +407,7 @@ def excel_simple_table(
                 cell_style = text_style
             cells.append(ws.make_cell(it, cell_style))
         ws.append_row(cells)
-    return ws.generate_standalone()
+    return ws.generate()
 
 
 def excel_feuille_saisie(e, titreannee, description, lines):
@@ -577,7 +568,7 @@ def excel_feuille_saisie(e, titreannee, description, lines):
             ws.make_cell("cellule vide -> note non modifiée", style_expl),
         ]
     )
-    return ws.generate_standalone()
+    return ws.generate()
 
 
 def excel_bytes_to_list(bytes_content):
@@ -797,4 +788,4 @@ def excel_feuille_listeappel(
     cell_2 = ws.make_cell(("Liste éditée le " + dt), style1i)
     ws.append_row([None, cell_2])
 
-    return ws.generate_standalone()
+    return ws.generate()
diff --git a/app/scodoc/sco_placement.py b/app/scodoc/sco_placement.py
index 4f9e9a0b..e0cf5b80 100644
--- a/app/scodoc/sco_placement.py
+++ b/app/scodoc/sco_placement.py
@@ -33,13 +33,30 @@ Contribution M. Salomon, UFC / IUT DE BELFORT-MONTBÉLIARD, 2016
 import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error
 import random
 import time
+from copy import copy
 
 import flask
+import wtforms.validators
+from flask import request, render_template
+from flask_login import current_user
+from werkzeug import Response
+from flask_wtf import FlaskForm
+from wtforms import (
+    StringField,
+    PasswordField,
+    BooleanField,
+    SubmitField,
+    SelectField,
+    RadioField,
+    HiddenField,
+    SelectMultipleField,
+    validators,
+)
+from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
 
 from app.scodoc.sco_exceptions import ScoValueError
 import app.scodoc.sco_utils as scu
 import app.scodoc.notesdb as ndb
-from app import log
 from app.scodoc import html_sco_header
 from app.scodoc import sco_edit_module
 from app.scodoc import sco_evaluations
@@ -57,23 +74,116 @@ from app.scodoc.gen_tables import GenTable
 from app.scodoc.sco_excel import *  # XXX à vérifier
 from app.scodoc.TrivialFormulator import TrivialFormulator
 
+_ = lambda x: x  # sans babel
+_l = _
 
-def do_placement_selectetuds(REQUEST):
+
+class PlacementForm(FlaskForm):
+    TOUS = "Tous"
+    evaluation_id = HiddenField("evaluation_id")
+    file_format = RadioField(
+        "Format de fichier",
+        choices=["pdf", "xls"],
+        validators=[
+            wtforms.validators.DataRequired("indiquez le format du fichier attendu"),
+        ],
+    )
+    surveillants = StringField(
+        "Surveillants", validators=[wtforms.validators.DataRequired("Test")]
+    )
+    batiment = StringField("Batiment")
+    salle = StringField("Salle")
+    nb_rangs = SelectField(
+        "nb_rangs", coerce=int, choices=[3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
+    )
+    etiquetage = RadioField(
+        "Numérotation",
+        choices=["Continue", "Coordonnées"],
+        validators=[
+            wtforms.validators.DataRequired("indiquez le style de  numérotation"),
+        ],
+    )
+    groups = SelectMultipleField(
+        "Groupe(s)",
+        validators=[
+            wtforms.validators.DataRequired("indiquez au moins un groupe"),
+        ],
+    )
+    submit = SubmitField("OK")
+
+    def _set_evaluation_infos(self, evaluation_id):
+        eval_data = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})
+        if not eval_data:
+            raise ScoValueError("invalid evaluation_id")
+        eval_data = eval_data[0]
+        # groupes
+        groups = sco_groups.do_evaluation_listegroupes(
+            evaluation_id, include_default=True
+        )
+        self.groups_tree = {}
+        self.has_groups = False
+        for group in groups:
+            partition = group["partition_name"] or self.TOUS  # TODO check required
+            group_id = group["group_id"]
+            group_name = group["group_name"] or self.TOUS
+            if partition not in self.groups_tree:
+                self.groups_tree[partition] = {}
+            self.groups_tree[partition][group_name] = group_id
+            if partition != self.TOUS:
+                self.has_groups = True
+                self.groups_tree_length = len(self.groups_tree)
+        if self.has_groups:
+            choices = []
+            for partition in self.groups_tree:
+                for groupe in self.groups_tree[partition]:
+                    id = str(self.groups_tree[partition][groupe])
+                    choices.append((id, "%s (%s)" % (str(groupe), partition)))
+            self.groups.choices = choices
+
+
+def placement_eval_selectetuds(evaluation_id, REQUEST=None):
+    form = PlacementForm(
+        request.form,
+        data={"evaluation_id": int(evaluation_id), "groups": PlacementForm.TOUS},
+    )
+    form._set_evaluation_infos(evaluation_id)
+    if form.validate_on_submit():
+        exec_placement(form)
+        return flask.redirect(titi())
+    H = [html_sco_header.sco_header(init_jquery_ui=True)]
+    H.append(sco_evaluations.evaluation_describe(evaluation_id=evaluation_id))
+    H.append("<h3>Placement et émargement des étudiants</h3>")
+    H.append(render_template("forms/placement.html", form=form))
+    H.append(
+        """<h3>Explications</h3> 
+        <ul> 
+            <li>préciser les surveillants et la localisation (bâtiment et salle) et indiquer le nombre de colonnes;</li>
+            <li>deux types de placements sont possibles : 
+                <ul> 
+                    <li>continue suppose que les tables ont toutes un numéro unique;</li>
+                    <li>coordonnées localise chaque table via un numéro de colonne et un numéro de ligne (ou rangée).</li>
+                </ul></li>
+            <li>Choisir le format du fichier résultat :
+                <ul> 
+                    <li>le format pdf consiste en un tableau précisant pour chaque étudiant la localisation de sa table;</li> 
+                    <li>le format xls produit un classeur avec deux onglets: 
+                        <ul> 
+                            <li>le premier onglet donne une vue de la salle avec la localisation des étudiants et 
+                                peut servir de feuille d'émargement;</li> 
+                            <li>le second onglet est un tableau similaire à celui du fichier pdf;</li>
+                        </ul></li>
+                </ul> </li> 
+        </ul> """
+    )
+    F = html_sco_header.sco_footer()
+    return "\n".join(H) + "<p>" + F
+
+
+def do_placement_selectetuds():
     """
     Choisi les étudiants et les infos sur la salle pour leur placement.
     """
-    evaluation_id = int(REQUEST.form["evaluation_id"])
-    E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})
-    if not E:
-        raise ScoValueError("invalid evaluation_id")
-    E = E[0]
     # M = sco_moduleimpl.do_moduleimpl_list( moduleimpl_id=E["moduleimpl_id"])[0]
-    # groupes
-    groups = sco_groups.do_evaluation_listegroupes(evaluation_id, include_default=True)
-    grlabs = [g["group_name"] or "tous" for g in groups]  # legendes des boutons
-    grnams = [g["group_id"] for g in groups]  # noms des checkbox
-    no_groups = (len(groups) == 1) and groups[0]["group_name"] is None
-
     # description de l'evaluation
     H = [
         sco_evaluations.evaluation_describe(
@@ -247,7 +357,6 @@ def do_placement(REQUEST):
             "Formulaire incomplet ! Vous avez sans doute attendu trop longtemps, veuillez vous reconnecter. Si le problème persiste, contacter l'administrateur. Merci."
         )
     E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0]
-
     # Check access
     # (admin, respformation, and responsable_id)
     if not sco_permissions_check.can_edit_notes(authuser, E["moduleimpl_id"]):
@@ -321,7 +430,7 @@ def do_placement(REQUEST):
 
     if placement_method == "xls":
         filename = f"placement_{evalname}_{gr_title_filename}{scu.XLSX_SUFFIX}"
-        xls = Excel_feuille_placement(
+        xls = _excel_feuille_placement(
             E, desceval, listetud, columns, space, maxlines, building, room, numbering
         )
         return sco_excel.send_excel_file(REQUEST, xls, filename)
@@ -399,7 +508,7 @@ def do_placement(REQUEST):
         return t
 
 
-def placement_eval_selectetuds(evaluation_id, REQUEST=None):
+def placement_eval_selectetuds_old(evaluation_id, REQUEST=None):
     """Dialogue placement etudiants: choix methode et localisation"""
     evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})
     if not evals:
@@ -415,298 +524,170 @@ def placement_eval_selectetuds(evaluation_id, REQUEST=None):
     formid = "placementfile"
     if not REQUEST.form.get("%s-submitted" % formid, False):
         # not submitted, choix groupe
-        r = do_placement_selectetuds(REQUEST)
+        r = do_placement_selectetuds()
         if r:
-            H.append(r)
-
+            if isinstance(r, str):
+                H.append(r)
+            elif isinstance(r, Response):
+                H.append(r.get_data().decode("utf-8"))
     H.append(
-        """<h3>Explications</h3>
-<ul>
-<li>Choisir le format du fichier résultat :</li>
-<ul>
-<li>le format pdf consiste en un tableau précisant pour chaque étudiant la localisation de sa table;</li>
-<li>le format xls produit un classeur avec deux onglets</li>
-<ul>
-<li>le premier onglet donne une vue de la salle avec la localisation des étudiants et peut servir de feuille d'émargement;</li>
-<li>le second onglet est un tableau similaire à celui du fichier pdf;</li>
-</ul>
-</ul> 
-<li>préciser les surveillants et la localisation (bâtiment et salle) et indiquer le nombre de colonnes;</li> 
-<li>deux types de placements sont possibles :</li>
-<ul>
-<li>continue suppose que les tables ont toutes un numéro unique;</li>
-<li>coordonnées localise chaque table via un numéro de colonne et un numéro de ligne (ou rangée).</li>
-</ul>
-</ul>
-"""
+        """<h3>Explications</h3> <ul> <li>Choisir le format du fichier résultat :</li> <ul> <li>le format pdf 
+        consiste en un tableau précisant pour chaque étudiant la localisation de sa table;</li> <li>le format xls 
+        produit un classeur avec deux onglets</li> <ul> <li>le premier onglet donne une vue de la salle avec la 
+        localisation des étudiants et peut servir de feuille d'émargement;</li> <li>le second onglet est un tableau 
+        similaire à celui du fichier pdf;</li> </ul> </ul> <li>préciser les surveillants et la localisation (bâtiment 
+        et salle) et indiquer le nombre de colonnes;</li> <li>deux types de placements sont possibles :</li> <ul> 
+        <li>continue suppose que les tables ont toutes un numéro unique;</li> <li>coordonnées localise chaque table 
+        via un numéro de colonne et un numéro de ligne (ou rangée).</li> </ul> </ul> """
     )
     H.append(html_sco_header.sco_footer())
     return "\n".join(H)
 
 
-def Excel_feuille_placement(
-    E, description, listetud, columns, space, maxlines, building, room, numbering
-):
-    """Genere feuille excel pour placement des etudiants.
-    E: evaluation (dict)
-    lines: liste de tuples
-               (etudid, nom, prenom, etat, groupe, val, explanation)
-    """
-    nbcolumns = int(columns)
-
-    wb = Workbook()
-
-    SheetName0 = "Emargement"
-    ws0 = wb.add_sheet(SheetName0.decode(scu.SCO_ENCODING))
-    # ajuste largeurs colonnes (unite inconnue, empirique)
-    width = 4500
-    if nbcolumns > 5:
-        width = 22500 // nbcolumns
-
-    for col in range(nbcolumns):
-        ws0.col(col + 1).width = width
-        ws0.col(0).width = 750
-
-    SheetName1 = "Positions"
-    ws1 = wb.add_sheet(SheetName1.decode(scu.SCO_ENCODING))
+def _one_header(ws, numbering, styles):
+    cells = []
     if numbering == "coordinate":
-        ws1.col(0).width = 4000
-        ws1.col(1).width = 4500
-        ws1.col(2).width = 1500
-        ws1.col(3).width = 1500
-
-        ws1.col(4).width = 500
-
-        ws1.col(5).width = 4000
-        ws1.col(6).width = 4500
-        ws1.col(7).width = 1500
-        ws1.col(8).width = 1500
+        cells.append(ws.make_cell("Nom", styles["2bi"]))
+        cells.append(ws.make_cell("Prénom", styles["2bi"]))
+        cells.append(ws.make_cell("Colonne", styles["2bi"]))
+        cells.append(ws.make_cell("Ligne", styles["2bi"]))
     else:
-        ws1.col(0).width = 4000
-        ws1.col(1).width = 4500
-        ws1.col(2).width = 3000
+        cells.append(ws.make_cell("Nom", styles["2bi"]))
+        cells.append(ws.make_cell("Prénom", styles["2bi"]))
+        cells.append(ws.make_cell("Place", styles["2bi"]))
+    return cells
 
-        ws1.col(3).width = 500
 
-        ws1.col(4).width = 4000
-        ws1.col(5).width = 4500
-        ws1.col(6).width = 3000
+def _headers(ws, numbering, styles, nb_listes):
+    cells = []
+    for _ in range(nb_listes):
+        cells += _one_header(ws, numbering, styles)
+        cells.append(ws.make_cell(""))
+    ws.append_row(cells)
+
+
+def _make_styles(ws0, ws1):
+    # polices
+    font0 = Font(name="Calibri", bold=True, size=12)
+    font1b = copy(font0)
+    font1b.size = 9
+    font1i = Font(name="Arial", italic=True, size=10)
+    font1o = Font(name="Arial", outline=True, size=10)
+    font2bi = Font(name="Arial", bold=True, italic=True, size=8)
+    font2 = Font(name="Arial", size=10)
+
+    # bordures
+    side_double = Side(border_style="double", color=COLORS.BLACK.value)
+    side_thin = Side(border_style="thin", color=COLORS.BLACK.value)
+
+    # bordures
+    border1t = Border(left=side_double, top=side_double, right=side_double)
+    border1bb = Border(left=side_double, bottom=side_double, right=side_double)
+    border1bm = Border(left=side_double, right=side_double)
+    border1m = Border(left=side_double, bottom=side_thin, right=side_double)
+    border2m = Border(top=side_thin, bottom=side_thin)
+    border2r = Border(top=side_thin, bottom=side_thin, right=side_thin)
+    border2l = Border(left=side_thin, top=side_thin, bottom=side_thin)
+    border2b = Border(left=side_thin, top=side_thin, bottom=side_thin, right=side_thin)
+
+    # alignements
+    align_center_center = Alignment(horizontal="center", vertical="center")
+    align_right_bottom = Alignment(horizontal="right", vertical="bottom")
+    align_left_center = Alignment(horizontal="left", vertical="center")
+    align_right_center = Alignment(horizontal="right", vertical="center")
+
+    # patterns
+    pattern = PatternFill(
+        fill_type="solid", fgColor=sco_excel.COLORS.LIGHT_YELLOW.value
+    )
 
     # styles
-    font0 = Font()
-    font0.name = "Arial"
-    font0.bold = True
-    font0.height = 12 * 0x14
+    styles = {
+        "titres": sco_excel.excel_make_style(font_name="Arial", bold=True, size=12),
+        "1t": ws0.excel_make_composite_style(
+            font=font0, alignment=align_center_center, border=border1t
+        ),
+        "1m": ws0.excel_make_composite_style(
+            font=font1b, alignment=align_center_center, border=border1m
+        ),
+        "1bm": ws0.excel_make_composite_style(
+            font=font1b, alignment=align_center_center, border=border1bm
+        ),
+        "1bb": ws0.excel_make_composite_style(
+            font=font1o, alignment=align_right_bottom, border=border1bb
+        ),
+        "2b": ws1.excel_make_composite_style(
+            font=font1i, alignment=align_center_center, border=border2b
+        ),
+        "2bi": ws1.excel_make_composite_style(
+            font=font2bi, alignment=align_center_center, border=border2b, fill=pattern
+        ),
+        "2l": ws1.excel_make_composite_style(
+            font=font2, alignment=align_left_center, border=border2l
+        ),
+        "2m1": ws1.excel_make_composite_style(
+            font=font2, alignment=align_left_center, border=border2m
+        ),
+        "2m2": ws1.excel_make_composite_style(
+            font=font2, alignment=align_right_center, border=border2m
+        ),
+        "2r": ws1.excel_make_composite_style(
+            font=font2, alignment=align_right_center, border=border2r
+        ),
+    }
+    return styles
 
-    font1b = Font()
-    font1b.name = "Arial"
-    font1b.bold = True
-    font1b.height = 9 * 0x14
 
-    font1i = Font()
-    font1i.name = "Arial"
-    font1i.height = 10 * 0x14
-    font1i.italic = True
+def _init_lines(maxlines):
+    return [
+        [] for _ in range(maxlines)
+    ]  # lines[no_ligne] -> liste des cellules de la ligne (no_lignes de 1..maxlines
 
-    font1o = Font()
-    font1o.name = "Arial"
-    font1o.height = 10 * 0x14
-    font1o.outline = True
 
-    font2bi = Font()
-    font2bi.name = "Arial"
-    font2bi.height = 8 * 0x14
-    font2bi.bold = True
-    font2bi.italic = True
+def _write_lines(ws, lines):
+    for line in lines:
+        ws.append_row(line)
 
-    font2 = Font()
-    font2.name = "Arial"
-    font2.height = 10 * 0x14
 
-    style_titres = XFStyle()
-    style_titres.font = font0
-
-    style1t = XFStyle()
-    style1t.font = font1b
-    alignment = Alignment()
-    alignment.horz = Alignment.HORZ_CENTER
-    alignment.vert = Alignment.VERT_CENTER
-    style1t.alignment = alignment
-    borders = Borders()
-    borders.left = Borders.DOUBLE
-    borders.top = Borders.DOUBLE
-    borders.bottom = Borders.NO_LINE
-    borders.right = Borders.DOUBLE
-    style1t.borders = borders
-
-    style1m = XFStyle()
-    style1m.font = font1b
-    alignment = Alignment()
-    alignment.horz = Alignment.HORZ_CENTER
-    alignment.vert = Alignment.VERT_CENTER
-    style1m.alignment = alignment
-    borders = Borders()
-    borders.left = Borders.DOUBLE
-    borders.top = Borders.NO_LINE
-    borders.bottom = Borders.THIN
-    borders.right = Borders.DOUBLE
-    style1m.borders = borders
-
-    style1bm = XFStyle()
-    borders = Borders()
-    borders.left = Borders.DOUBLE
-    borders.top = Borders.NO_LINE
-    borders.bottom = Borders.NO_LINE
-    borders.right = Borders.DOUBLE
-    style1bm.borders = borders
-
-    style1bb = XFStyle()
-    style1bb.font = font1o
-    alignment = Alignment()
-    alignment.horz = Alignment.HORZ_RIGHT
-    alignment.vert = Alignment.VERT_BOTTOM
-    style1bb.alignment = alignment
-    borders = Borders()
-    borders.left = Borders.DOUBLE
-    borders.top = Borders.NO_LINE
-    borders.bottom = Borders.DOUBLE
-    borders.right = Borders.DOUBLE
-    style1bb.borders = borders
-
-    style2b = XFStyle()
-    style2b.font = font1i
-    alignment = Alignment()
-    alignment.horz = Alignment.HORZ_CENTER
-    alignment.vert = Alignment.VERT_CENTER
-    style2b.alignment = alignment
-    borders = Borders()
-    borders.left = Borders.THIN
-    borders.top = Borders.THIN
-    borders.bottom = Borders.THIN
-    borders.right = Borders.THIN
-    style2b.borders = borders
-
-    style2bi = XFStyle()
-    style2bi.font = font2bi
-    alignment = Alignment()
-    alignment.horz = Alignment.HORZ_CENTER
-    alignment.vert = Alignment.VERT_CENTER
-    style2bi.alignment = alignment
-    borders = Borders()
-    borders.left = Borders.THIN
-    borders.top = Borders.THIN
-    borders.bottom = Borders.THIN
-    borders.right = Borders.THIN
-    style2bi.borders = borders
-    pattern = Pattern()
-    pattern.pattern = Pattern.SOLID_PATTERN
-    pattern._pattern_back_colour = "gray"
-    style2bi.pattern = pattern
-
-    style2l = XFStyle()
-    style2l.font = font2
-    alignment = Alignment()
-    alignment.horz = Alignment.HORZ_LEFT
-    alignment.vert = Alignment.VERT_CENTER
-    style2l.alignment = alignment
-    borders = Borders()
-    borders.left = Borders.THIN
-    borders.top = Borders.THIN
-    borders.bottom = Borders.THIN
-    borders.right = Borders.NO_LINE
-    style2l.borders = borders
-
-    style2m1 = XFStyle()
-    style2m1.font = font2
-    alignment = Alignment()
-    alignment.horz = Alignment.HORZ_LEFT
-    alignment.vert = Alignment.VERT_CENTER
-    style2m1.alignment = alignment
-    borders = Borders()
-    borders.left = Borders.NO_LINE
-    borders.top = Borders.THIN
-    borders.bottom = Borders.THIN
-    borders.right = Borders.NO_LINE
-    style2m1.borders = borders
-
-    style2m2 = XFStyle()
-    style2l.font = font2
-    alignment = Alignment()
-    alignment.horz = Alignment.HORZ_RIGHT
-    alignment.vert = Alignment.VERT_CENTER
-    style2m2.alignment = alignment
-    borders = Borders()
-    borders.left = Borders.NO_LINE
-    borders.top = Borders.THIN
-    borders.bottom = Borders.THIN
-    borders.right = Borders.NO_LINE
-    style2m2.borders = borders
-
-    style2r = XFStyle()
-    style2l.font = font2
-    alignment = Alignment()
-    alignment.horz = Alignment.HORZ_RIGHT
-    alignment.vert = Alignment.VERT_CENTER
-    style2r.alignment = alignment
-    borders = Borders()
-    borders.left = Borders.NO_LINE
-    borders.top = Borders.THIN
-    borders.bottom = Borders.THIN
-    borders.right = Borders.THIN
-    style2r.borders = borders
-
-    # ligne de titres
-    li = 0
-    line = 0
+def _titres(ws, description, evaluation, building, room, styles):
     dt = time.strftime("%d/%m/%Y a %Hh%M")
-    ws0.write(li, 0, "Feuille placement etudiants éditée le %s" % dt, style_titres)
-    ws1.write(li, 0, "Feuille placement etudiants éditée le %s" % dt, style_titres)
-    for desceval in description:
-        if line % 2 == 0:
-            li += 2
-        else:
-            li += 1
-        line += 1
-        ws0.write(li, 0, desceval[0].decode(scu.SCO_ENCODING), style_titres)
-        ws1.write(li, 0, desceval[0].decode(scu.SCO_ENCODING), style_titres)
-    li += 1
-    ws0.write(
-        li,
-        0,
-        "Date : %s - Horaire : %s à %s" % (E["jour"], E["heure_debut"], E["heure_fin"]),
-        style_titres,
+    ws.append_single_cell_row(
+        "Feuille placement etudiants éditée le %s" % dt, styles["titres"]
     )
-    ws1.write(
-        li,
-        0,
-        "Date : %s - Horaire : %s à %s" % (E["jour"], E["heure_debut"], E["heure_fin"]),
-        style_titres,
+    for line, desceval in enumerate(description):
+        if line in [1, 4, 7]:
+            ws.append_blank_row()
+        ws.append_single_cell_row(desceval[0], styles["titres"])
+    ws.append_single_cell_row(
+        "Date : %s - Horaire : %s à %s"
+        % (evaluation["jour"], evaluation["heure_debut"], evaluation["heure_fin"]),
+        styles["titres"],
+    )
+    ws.append_single_cell_row(
+        "Date : %s - Horaire : %s à %s"
+        % (evaluation["jour"], evaluation["heure_debut"], evaluation["heure_fin"]),
+        styles["titres"],
     )
-    li += 1
 
+
+def _feuille0(
+    ws0,
+    description,
+    evaluation,
+    styles,
+    numbering,
+    listetud,
+    nbcolumns,
+    building,
+    room,
+    space,
+):
+    _titres(ws0, description, evaluation, building, room, styles)
     # entetes colonnes - feuille0
+    cells = [ws0.make_cell()]
     for col in range(nbcolumns):
-        ws0.write(li, col + 1, "colonne %s" % (col + 1), style2b)
-    # entetes colonnes - feuille1
-    if numbering == "coordinate":
-        ws1.write(li, 0, "Nom", style2bi)
-        ws1.write(li, 1, "Prénom", style2bi)
-        ws1.write(li, 2, "Colonne", style2bi)
-        ws1.write(li, 3, "Ligne", style2bi)
-
-        ws1.write(li, 5, "Nom", style2bi)
-        ws1.write(li, 6, "Prénom", style2bi)
-        ws1.write(li, 7, "Colonne", style2bi)
-        ws1.write(li, 8, "Ligne", style2bi)
-    else:
-        ws1.write(li, 0, "Nom", style2bi)
-        ws1.write(li, 1, "Prénom", style2bi)
-        ws1.write(li, 2, "Place", style2bi)
-
-        ws1.write(li, 4, "Nom", style2bi)
-        ws1.write(li, 5, "Prénom", style2bi)
-        ws1.write(li, 6, "Place", style2bi)
-
+        cells.append(ws0.make_cell("colonne %s" % (col + 1), styles["2b"]))
+    ws0.append_row(cells)
     # etudiants
     line = 1
     col = 1
@@ -730,78 +711,188 @@ def Excel_feuille_placement(
         placementetud.append(linetud)
 
     # etudiants - feuille0
-    line = 0
-    li0 = li
-    for linetud in placementetud:
-        li0 += 1
-        line += 1
-        ws0.write(li0, 0, line, style2b)
-        col = 1
+    place = 1
+    for rang, linetud in enumerate(placementetud, start=1):
+        # Chaque rang est affiché sur 3 lignes xlsx (notées A, B, C)
+        # ligne A: le nom, ligne B: le prénom, ligne C: un espace ou la place
+        cells_a = [ws0.make_cell(rang, styles["2b"])]
+        cells_b = [ws0.make_cell("", styles["2b"])]
+        cells_c = [ws0.make_cell("", styles["2b"])]
+        row = 14  # premieère ligne de signature
         for etudid in linetud:
-            ws0.write(li0, col, (etudid[0]).decode(scu.SCO_ENCODING), style1t)
-            ws0.write(li0 + 1, col, (etudid[1]).decode(scu.SCO_ENCODING), style1m)
-            ws0.row(li0 + 2).height = space
+            cells_a.append(ws0.make_cell(etudid[0], styles["1t"]))
+            cells_b.append(ws0.make_cell(etudid[1], styles["1m"]))
             if numbering == "coordinate":
-                ws0.write(li0 + 2, col, " ", style1bb)
+                cell_c = ws0.make_cell("", styles["1bb"])
             else:
-                ws0.write(
-                    li0 + 2, col, "place %s" % (col + (line - 1) * nbcolumns), style1bb
-                )
-            # ws0.write(li+3,col, ' ', style1bm )
-            # ws0.write(li+4,col, ' ', style1bb )
-
+                cell_c = ws0.make_cell("place %s" % place, styles["1bb"])
+            cells_c.append(cell_c)
+            ws0.set_row_dimension_height(row, space / 25)
+            row += 3
+            place = place + 1
             if col == nbcolumns:
-                col = 0
-                li0 += 2
-            col += 1
+                ws0.append_row(cells_a)
+                ws0.append_row(cells_b)
+                ws0.append_row(cells_c)
+                cells_a = [ws0.make_cell(rang, styles["2b"])]
+                cells_b = [ws0.make_cell("", styles["2b"])]
+                cells_c = [ws0.make_cell("", styles["2b"])]
+            # publication du rang final incomplet
+        ws0.append_row(cells_a)
+        ws0.append_row(cells_b)
+        ws0.append_row(cells_c)
+        ws0.set_row_dimension_height(row, space / 25)
 
-    # etudiants - feuille1
-    if numbering == "coordinate":
-        coloffset = 5
-    else:
-        coloffset = 4
-    line = 0
-    li1 = li
-    nbcol = 0
-    col = 0
-    orderetud.sort()
-    for etudid in orderetud:
-        li1 += 1
-        line += 1
-        ws1.write(li1, col, (etudid[0]).decode(scu.SCO_ENCODING), style2l)
-        ws1.write(li1, col + 1, (etudid[1]).decode(scu.SCO_ENCODING), style2m1)
+
+def _compute_ordretud(listetud, nbcolumns, numbering):
+    orderetud = []
+    line = 1
+    col = 1
+    for etudid in listetud:
         if numbering == "coordinate":
-            ws1.write(li1, col + 2, etudid[2], style2m2)
-            ws1.write(li1, col + 3, etudid[3], style2r)
+            orderetud.append((etudid[0], etudid[1], col, line))
         else:
-            ws1.write(li1, col + 2, etudid[2], style2r)
+            orderetud.append(
+                (etudid[0], etudid[1], "%s" % (col + (line - 1) * nbcolumns))
+            )
+        col += 1
+        if col > nbcolumns:
+            col = 1
+            line += 1
+    orderetud.sort()
+    return orderetud
 
-        if line == maxlines:
+
+def _next_page(ws):
+    pass
+
+
+def _feuille1(
+    ws,
+    description,
+    evaluation,
+    styles,
+    numbering,
+    maxlines,
+    nbcolumns,
+    building,
+    room,
+    listetud,
+):
+    # etudiants - feuille1
+    # structuration:
+    # 1 page = maxlistes listes
+    #   1 liste = 3 ou 4 colonnes(excel) (selon numbering) et (maximum maxlines) lignes
+    maxlistes = 2  # nombre de listes par page
+    # computes excel columns widths
+    if numbering == "coordinate":
+        gabarit = [16, 18, 6, 6, 2]
+    else:
+        gabarit = [16, 18, 12, 2]
+    widths = []
+    for _ in range(maxlistes):
+        widths += gabarit
+    ws.set_column_dimension_width(value=widths)
+    nb_etu_restant = len(listetud)
+    _titres(ws, description, evaluation, building, room, styles)
+    nb_listes = min(
+        maxlistes, nb_etu_restant // maxlines + 1
+    )  # nombre de colonnes dans la page
+    _headers(ws, numbering, styles, nb_listes)
+    # construction liste alphabétique
+    # Affichage
+    lines = _init_lines(maxlines)
+    orderetud = _compute_ordretud(listetud, nbcolumns, numbering)
+    line = 0
+    col = 0
+    for etudid in orderetud:
+        # check for skip of list or page
+        if col > 0:  # add a empty cell between lists
+            lines[line].append(ws.make_cell())
+        lines[line].append(ws.make_cell(etudid[0], styles["2l"]))
+        lines[line].append(ws.make_cell(etudid[1], styles["2m1"]))
+        if numbering == "coordinate":
+            lines[line].append(ws.make_cell(etudid[2], styles["2m2"]))
+            lines[line].append(ws.make_cell(etudid[3], styles["2r"]))
+        else:
+            lines[line].append(ws.make_cell(etudid[2], styles["2r"]))
+        line = line + 1
+        if line >= maxlines:  # fin de liste
+            col = col + 1
             line = 0
-            li1 = li
-            nbcol = nbcol + 1
-            col = col + coloffset
-            if nbcol == 2:
-                li = li + maxlines + 2
-                li1 = li
-                nbcol = 0
+            if col >= maxlistes:  # fin de page
+                _write_lines(ws, lines)
+                lines = _init_lines(maxlines)
                 col = 0
-                if numbering == "coordinate":
-                    ws1.write(li, 0, "Nom", style2bi)
-                    ws1.write(li, 1, "Prénom", style2bi)
-                    ws1.write(li, 2, "Colonne", style2bi)
-                    ws1.write(li, 3, "Ligne", style2bi)
+                ws.append_blank_row()
+                nb_etu_restant -= maxlistes * maxlines
+                nb_listes = min(
+                    maxlistes, nb_etu_restant // maxlines + 1
+                )  # nombre de colonnes dans la page
+                _headers(ws, numbering, styles, nb_listes)
+    _write_lines(ws, lines)
 
-                    ws1.write(li, 5, "Nom", style2bi)
-                    ws1.write(li, 6, "Prénom", style2bi)
-                    ws1.write(li, 7, "Colonne", style2bi)
-                    ws1.write(li, 8, "Ligne", style2bi)
-                else:
-                    ws1.write(li, 0, "Nom", style2bi)
-                    ws1.write(li, 1, "Prénom", style2bi)
-                    ws1.write(li, 2, "Place", style2bi)
 
-                    ws1.write(li, 4, "Nom", style2bi)
-                    ws1.write(li, 5, "Prénom", style2bi)
-                    ws1.write(li, 6, "Place", style2bi)
-    return wb.savetostr()
+def _excel_feuille_placement(
+    evaluation,
+    description,
+    listetud,
+    columns,
+    space,
+    maxlines,
+    building,
+    room,
+    numbering,
+):
+    """Genere feuille excel pour placement des etudiants.
+    E: evaluation (dict)
+    lines: liste de tuples
+               (etudid, nom, prenom, etat, groupe, val, explanation)
+    """
+    nbcolumns = int(columns)
+    column_width_ratio = 1 / 250  # changement d unités entre pyExcelerator et openpyxl
+
+    wb = ScoExcelBook()
+
+    SheetName0 = "Emargement"
+    ws0 = wb.create_sheet(SheetName0)
+    # ajuste largeurs colonnes (unite inconnue, empirique)
+    width = 4500 * column_width_ratio
+    if nbcolumns > 5:
+        width = 22500 * column_width_ratio // nbcolumns
+
+    ws0.set_column_dimension_width("A", 750 * column_width_ratio)
+    for col in range(nbcolumns):
+        ws0.set_column_dimension_width(
+            "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[col + 1 : col + 2], width
+        )
+
+    SheetName1 = "Positions"
+    ws1 = wb.create_sheet(SheetName1)
+
+    styles = _make_styles(ws0, ws1)
+    _feuille0(
+        ws0,
+        description,
+        evaluation,
+        styles,
+        numbering,
+        listetud,
+        nbcolumns,
+        building,
+        room,
+        space,
+    )
+    _feuille1(
+        ws1,
+        description,
+        evaluation,
+        styles,
+        numbering,
+        maxlines,
+        nbcolumns,
+        building,
+        room,
+        listetud,
+    )
+    return wb.generate()
diff --git a/app/templates/forms/placement.html b/app/templates/forms/placement.html
new file mode 100644
index 00000000..53bfe534
--- /dev/null
+++ b/app/templates/forms/placement.html
@@ -0,0 +1,56 @@
+{% import 'bootstrap/wtf.html' as wtf %}
+
+{% macro render_field(field) %}
+    <tr>
+        <td class="wtf-field">{{ field.label }}</td>
+        <td class="wtf-field">{{ field(**kwargs)|safe }}
+        {% if field.errors %}
+            <ul class=errors>
+            {% for error in field.errors %}
+              <li>{{ error }}</li>
+            {% endfor %}
+            </ul>
+        {% endif %}
+        </td>
+    </tr>
+{% endmacro %}
+
+<div class="saisienote_etape1 form_placement">
+<form method=post>
+    {{ form.evaluation_id }}
+    <table class="tf">
+        <tbody>
+            {{ render_field(form.surveillants) }}
+            {{ render_field(form.batiment) }}
+            {{ render_field(form.salle) }}
+            {{ render_field(form.nb_rangs) }}
+            {{ render_field(form.etiquetage) }}
+            {%  if form.has_groups %}
+            {{ render_field(form.groups) }}
+<!--
+                {% for partition in form.groups_tree %}
+                    <tr>
+                        {%  if partition == 'Tous' %}
+                            <td rowspan="{{ form.groups_tree_length }}">Groupes</td>
+                        {% endif %}
+                        <td>{{ partition }}</td>
+                        <td>
+                            {% for groupe in form.groups_tree[partition] %}
+                                {{ groupe }}{{ form[form.groups_tree[partition][groupe]] }}
+                            {%  endfor %}
+                        </td>
+                    </tr>
+                {%  endfor %}
+-->
+            {% endif %}
+            {{ render_field(form.file_format) }}
+        </tbody>
+    </table>
+  <p>
+      <input id="gr_submit" type=submit value="Ok">
+      <input id="gr_cancel" type=submit value="Annuler">
+</script>
+</form>
+</div>
+
+
diff --git a/app/views/notes.py b/app/views/notes.py
index 7e80550b..41bad1dd 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -1611,6 +1611,7 @@ sco_publish(
     "/placement_eval_selectetuds",
     sco_placement.placement_eval_selectetuds,
     Permission.ScoEnsView,
+    methods=["GET", "POST"],
 )
 sco_publish("/do_placement", sco_placement.do_placement, Permission.ScoEnsView)