forked from ScoDoc/ScoDoc
placement fait
This commit is contained in:
parent
050e54de3e
commit
35768e9241
@ -141,14 +141,15 @@ class PlacementForm(FlaskForm):
|
||||
self.groups.choices = choices
|
||||
|
||||
|
||||
def placement_eval_selectetuds(evaluation_id, REQUEST=None):
|
||||
def placement_eval_selectetuds(evaluation_id):
|
||||
"""Creation de l'écran de placement"""
|
||||
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)
|
||||
exec_placement(form) # calcul et generation du fichier
|
||||
return flask.redirect(titi())
|
||||
H = [html_sco_header.sco_header(init_jquery_ui=True)]
|
||||
H.append(sco_evaluations.evaluation_describe(evaluation_id=evaluation_id))
|
||||
@ -158,238 +159,227 @@ def placement_eval_selectetuds(evaluation_id, REQUEST=None):
|
||||
return "\n".join(H) + "<p>" + F
|
||||
|
||||
|
||||
def do_placement_selectetuds():
|
||||
"""
|
||||
Choisi les étudiants et les infos sur la salle pour leur placement.
|
||||
"""
|
||||
# M = sco_moduleimpl.do_moduleimpl_list( moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
# description de l'evaluation
|
||||
H = [
|
||||
sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
|
||||
"<h3>Placement et émargement des étudiants</h3>",
|
||||
]
|
||||
# def do_placement_selectetuds():
|
||||
# """
|
||||
# Choisi les étudiants et les infos sur la salle pour leur placement.
|
||||
# """
|
||||
# # M = sco_moduleimpl.do_moduleimpl_list( moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
# # description de l'evaluation
|
||||
# H = [
|
||||
# sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
|
||||
# "<h3>Placement et émargement des étudiants</h3>",
|
||||
# ]
|
||||
# #
|
||||
# descr = [
|
||||
# ("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
|
||||
# (
|
||||
# "placement_method",
|
||||
# {
|
||||
# "input_type": "radio",
|
||||
# "default": "xls",
|
||||
# "allow_null": False,
|
||||
# "allowed_values": ["pdf", "xls"],
|
||||
# "labels": ["fichier pdf", "fichier xls"],
|
||||
# "title": "Format de fichier :",
|
||||
# },
|
||||
# ),
|
||||
# ("teachers", {"size": 25, "title": "Surveillants :"}),
|
||||
# ("building", {"size": 25, "title": "Batiment :"}),
|
||||
# ("room", {"size": 10, "title": "Salle :"}),
|
||||
# (
|
||||
# "columns",
|
||||
# {
|
||||
# "input_type": "radio",
|
||||
# "default": "5",
|
||||
# "allow_null": False,
|
||||
# "allowed_values": ["3", "4", "5", "6", "7", "8"],
|
||||
# "labels": [
|
||||
# "3 colonnes",
|
||||
# "4 colonnes",
|
||||
# "5 colonnes",
|
||||
# "6 colonnes",
|
||||
# "7 colonnes",
|
||||
# "8 colonnes",
|
||||
# ],
|
||||
# "title": "Nombre de colonnes :",
|
||||
# },
|
||||
# ),
|
||||
# (
|
||||
# "numbering",
|
||||
# {
|
||||
# "input_type": "radio",
|
||||
# "default": "coordinate",
|
||||
# "allow_null": False,
|
||||
# "allowed_values": ["continuous", "coordinate"],
|
||||
# "labels": ["continue", "coordonnées"],
|
||||
# "title": "Numérotation :",
|
||||
# },
|
||||
# ),
|
||||
# ]
|
||||
# if no_groups:
|
||||
# submitbuttonattributes = []
|
||||
# descr += [
|
||||
# (
|
||||
# "group_ids",
|
||||
# {
|
||||
# "default": [
|
||||
# g["group_id"] # pylint: disable=invalid-sequence-index
|
||||
# for g in groups
|
||||
# ],
|
||||
# "input_type": "hidden",
|
||||
# "type": "list",
|
||||
# },
|
||||
# )
|
||||
# ]
|
||||
# else:
|
||||
# descr += [
|
||||
# (
|
||||
# "group_ids",
|
||||
# {
|
||||
# "input_type": "checkbox",
|
||||
# "title": "Choix groupe(s) d'étudiants :",
|
||||
# "allowed_values": grnams,
|
||||
# "labels": grlabs,
|
||||
# "attributes": ['onchange="gr_change(this);"'],
|
||||
# },
|
||||
# )
|
||||
# ]
|
||||
#
|
||||
descr = [
|
||||
("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
|
||||
(
|
||||
"placement_method",
|
||||
{
|
||||
"input_type": "radio",
|
||||
"default": "xls",
|
||||
"allow_null": False,
|
||||
"allowed_values": ["pdf", "xls"],
|
||||
"labels": ["fichier pdf", "fichier xls"],
|
||||
"title": "Format de fichier :",
|
||||
},
|
||||
),
|
||||
("teachers", {"size": 25, "title": "Surveillants :"}),
|
||||
("building", {"size": 25, "title": "Batiment :"}),
|
||||
("room", {"size": 10, "title": "Salle :"}),
|
||||
(
|
||||
"columns",
|
||||
{
|
||||
"input_type": "radio",
|
||||
"default": "5",
|
||||
"allow_null": False,
|
||||
"allowed_values": ["3", "4", "5", "6", "7", "8"],
|
||||
"labels": [
|
||||
"3 colonnes",
|
||||
"4 colonnes",
|
||||
"5 colonnes",
|
||||
"6 colonnes",
|
||||
"7 colonnes",
|
||||
"8 colonnes",
|
||||
],
|
||||
"title": "Nombre de colonnes :",
|
||||
},
|
||||
),
|
||||
(
|
||||
"numbering",
|
||||
{
|
||||
"input_type": "radio",
|
||||
"default": "coordinate",
|
||||
"allow_null": False,
|
||||
"allowed_values": ["continuous", "coordinate"],
|
||||
"labels": ["continue", "coordonnées"],
|
||||
"title": "Numérotation :",
|
||||
},
|
||||
),
|
||||
]
|
||||
if no_groups:
|
||||
submitbuttonattributes = []
|
||||
descr += [
|
||||
(
|
||||
"group_ids",
|
||||
{
|
||||
"default": [
|
||||
g["group_id"] # pylint: disable=invalid-sequence-index
|
||||
for g in groups
|
||||
],
|
||||
"input_type": "hidden",
|
||||
"type": "list",
|
||||
},
|
||||
)
|
||||
]
|
||||
else:
|
||||
descr += [
|
||||
(
|
||||
"group_ids",
|
||||
{
|
||||
"input_type": "checkbox",
|
||||
"title": "Choix groupe(s) d'étudiants :",
|
||||
"allowed_values": grnams,
|
||||
"labels": grlabs,
|
||||
"attributes": ['onchange="gr_change(this);"'],
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
if not ("group_ids" in REQUEST.form and REQUEST.form["group_ids"]):
|
||||
submitbuttonattributes = ['disabled="1"']
|
||||
else:
|
||||
submitbuttonattributes = [] # groupe(s) preselectionnés
|
||||
H.append(
|
||||
# JS pour desactiver le bouton OK si aucun groupe selectionné
|
||||
"""<script type="text/javascript">
|
||||
function gr_change(e) {
|
||||
var boxes = document.getElementsByName("group_ids:list");
|
||||
var nbchecked = 0;
|
||||
for (var i=0; i < boxes.length; i++) {
|
||||
if (boxes[i].checked)
|
||||
nbchecked++;
|
||||
}
|
||||
if (nbchecked > 0) {
|
||||
document.getElementsByName('gr_submit')[0].disabled=false;
|
||||
} else {
|
||||
document.getElementsByName('gr_submit')[0].disabled=true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
|
||||
tf = TrivialFormulator(
|
||||
REQUEST.URL0,
|
||||
REQUEST.form,
|
||||
descr,
|
||||
cancelbutton="Annuler",
|
||||
submitbuttonattributes=submitbuttonattributes,
|
||||
submitlabel="OK",
|
||||
formid="gr",
|
||||
)
|
||||
if tf[0] == 0:
|
||||
# H.append( """<div class="saisienote_etape1">
|
||||
# <span class="titredivplacementetudiants">Choix du groupe et de la localisation</span>
|
||||
# """)
|
||||
H.append("""<div class="saisienote_etape1">""")
|
||||
return "\n".join(H) + "\n" + tf[1] + "\n</div>"
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(
|
||||
"%s/Notes/moduleimpl_status?moduleimpl_id=%s"
|
||||
% (scu.ScoURL(), E["moduleimpl_id"])
|
||||
)
|
||||
else:
|
||||
placement_method = tf[2]["placement_method"]
|
||||
teachers = tf[2]["teachers"]
|
||||
building = tf[2]["building"]
|
||||
room = tf[2]["room"]
|
||||
group_ids = tf[2]["group_ids"]
|
||||
columns = tf[2]["columns"]
|
||||
numbering = tf[2]["numbering"]
|
||||
if columns in ("3", "4", "5", "6", "7", "8"):
|
||||
gs = [
|
||||
("group_ids%3Alist=" + six.moves.urllib.parse.quote_plus(x))
|
||||
for x in group_ids
|
||||
]
|
||||
query = (
|
||||
"evaluation_id=%s&placement_method=%s&teachers=%s&building=%s&room=%s&columns=%s&numbering=%s&"
|
||||
% (
|
||||
evaluation_id,
|
||||
placement_method,
|
||||
teachers,
|
||||
building,
|
||||
room,
|
||||
columns,
|
||||
numbering,
|
||||
)
|
||||
+ "&".join(gs)
|
||||
)
|
||||
return flask.redirect(scu.NotesURL() + "/do_placement?" + query)
|
||||
else:
|
||||
raise ValueError(
|
||||
"invalid placement_method (%s)" % tf[2]["placement_method"]
|
||||
)
|
||||
# if not ("group_ids" in REQUEST.form and REQUEST.form["group_ids"]):
|
||||
# submitbuttonattributes = ['disabled="1"']
|
||||
# else:
|
||||
# submitbuttonattributes = [] # groupe(s) preselectionnés
|
||||
# H.append(
|
||||
# # JS pour desactiver le bouton OK si aucun groupe selectionné
|
||||
# """<script type="text/javascript">
|
||||
# function gr_change(e) {
|
||||
# var boxes = document.getElementsByName("group_ids:list");
|
||||
# var nbchecked = 0;
|
||||
# for (var i=0; i < boxes.length; i++) {
|
||||
# if (boxes[i].checked)
|
||||
# nbchecked++;
|
||||
# }
|
||||
# if (nbchecked > 0) {
|
||||
# document.getElementsByName('gr_submit')[0].disabled=false;
|
||||
# } else {
|
||||
# document.getElementsByName('gr_submit')[0].disabled=true;
|
||||
# }
|
||||
# }
|
||||
# </script>
|
||||
# """
|
||||
# )
|
||||
#
|
||||
# tf = TrivialFormulator(
|
||||
# REQUEST.URL0,
|
||||
# REQUEST.form,
|
||||
# descr,
|
||||
# cancelbutton="Annuler",
|
||||
# submitbuttonattributes=submitbuttonattributes,
|
||||
# submitlabel="OK",
|
||||
# formid="gr",
|
||||
# )
|
||||
# if tf[0] == 0:
|
||||
# # H.append( """<div class="saisienote_etape1">
|
||||
# # <span class="titredivplacementetudiants">Choix du groupe et de la localisation</span>
|
||||
# # """)
|
||||
# H.append("""<div class="saisienote_etape1">""")
|
||||
# return "\n".join(H) + "\n" + tf[1] + "\n</div>"
|
||||
# elif tf[0] == -1:
|
||||
# return flask.redirect(
|
||||
# "%s/Notes/moduleimpl_status?moduleimpl_id=%s"
|
||||
# % (scu.ScoURL(), E["moduleimpl_id"])
|
||||
# )
|
||||
# else:
|
||||
# placement_method = tf[2]["placement_method"]
|
||||
# teachers = tf[2]["teachers"]
|
||||
# building = tf[2]["building"]
|
||||
# room = tf[2]["room"]
|
||||
# group_ids = tf[2]["group_ids"]
|
||||
# columns = tf[2]["columns"]
|
||||
# numbering = tf[2]["numbering"]
|
||||
# if columns in ("3", "4", "5", "6", "7", "8"):
|
||||
# gs = [
|
||||
# ("group_ids%3Alist=" + six.moves.urllib.parse.quote_plus(x))
|
||||
# for x in group_ids
|
||||
# ]
|
||||
# query = (
|
||||
# "evaluation_id=%s&placement_method=%s&teachers=%s&building=%s&room=%s&columns=%s&numbering=%s&"
|
||||
# % (
|
||||
# evaluation_id,
|
||||
# placement_method,
|
||||
# teachers,
|
||||
# building,
|
||||
# room,
|
||||
# columns,
|
||||
# numbering,
|
||||
# )
|
||||
# + "&".join(gs)
|
||||
# )
|
||||
# return flask.redirect(scu.NotesURL() + "/do_placement?" + query)
|
||||
# else:
|
||||
# raise ValueError(
|
||||
# "invalid placement_method (%s)" % tf[2]["placement_method"]
|
||||
# )
|
||||
|
||||
|
||||
def exec_placement(form):
|
||||
try:
|
||||
evaluation_id = int(form["evaluation_id"].data)
|
||||
except:
|
||||
raise ScoValueError(
|
||||
"Formulaire incomplet ! Vous avez sans doute attendu trop longtemps, veuillez vous reconnecter. Si le problème persiste, contacter l'administrateur. Merci."
|
||||
)
|
||||
eval_data = 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(
|
||||
current_user, eval_data["moduleimpl_id"]
|
||||
):
|
||||
return (
|
||||
"<h2>Génération du placement impossible pour %s</h2>" % authusername
|
||||
+ """<p>(vérifiez que le semestre n'est pas verrouillé et que vous
|
||||
avez l'autorisation d'effectuer cette opération)</p>
|
||||
<p><a href="moduleimpl_status?moduleimpl_id=%s">Continuer</a></p>
|
||||
"""
|
||||
% E["moduleimpl_id"]
|
||||
)
|
||||
plan = repartition(form, eval_data)
|
||||
"""Calcul et génération du fichier sur la base des données du formulaire"""
|
||||
breakpoint()
|
||||
sem_preferences = sco_preferences.SemPreferences()
|
||||
space = sem_preferences.get("feuille_placement_emargement")
|
||||
maxlines = sem_preferences.get("feuille_placement_positions")
|
||||
d = {
|
||||
"evaluation_id": form["evaluation_id"].data,
|
||||
"etiquetage": form["etiquetage"].data,
|
||||
"surveillants": form["surveillants"].data,
|
||||
"batiment": form["batiment"].data,
|
||||
"salle": form["salle"].data,
|
||||
"nb_rangs": form["nb_rangs"].data,
|
||||
"groups": form["groups"].data,
|
||||
}
|
||||
d["eval_data"] = sco_evaluations.do_evaluation_list(d)[0]
|
||||
# Check access (admin, respformation, and responsable_id)
|
||||
d["current_user"] = current_user
|
||||
d["moduleimpl_id"] = d["eval_data"]["moduleimpl_id"]
|
||||
if not sco_permissions_check.can_edit_notes(d["current_user"], d["moduleimpl_id"]):
|
||||
return (
|
||||
"""<h2>Génération du placement impossible pour %(current_user)s</h2>
|
||||
<p>(vérifiez que le semestre n'est pas verrouillé et que vous
|
||||
avez l'autorisation d'effectuer cette opération)</p>
|
||||
<p><a href="moduleimpl_status?moduleimpl_id=%(module_id)s">Continuer</a></p>
|
||||
"""
|
||||
% d
|
||||
)
|
||||
d["cnx"] = ndb.GetDBConnexion()
|
||||
d["plan"] = repartition(d)
|
||||
d["gr_title_filename"] = sco_groups.listgroups_filename(d['groups'])
|
||||
# gr_title = sco_groups.listgroups_abbrev(d['groups'])
|
||||
d["moduleimpl_data"] = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=d["moduleimpl_id"])
|
||||
d["Mod"] = sco_edit_module.do_module_list(args={"module_id": d["moduleimpl_id"]})[0]
|
||||
d["sem"] = sco_formsemestre.get_formsemestre(d["moduleimpl_data"]["formsemestre_id"])
|
||||
d["evalname"] = "%s-%s" % (d["Mod"]["code"], ndb.DateDMYtoISO(eval_data["jour"]))
|
||||
if d["eval_data"]["description"]:
|
||||
d["evaltitre"] = d["eval_data"]["description"]
|
||||
else:
|
||||
d["evaltitre"] = "évaluation du %s" % eval_data["jour"]
|
||||
d["desceval"] = [
|
||||
["%s" % d["sem"]["titreannee"]],
|
||||
["Module : %s - %s" % (d["Mod"]["code"], d["Mod"]["abbrev"])],
|
||||
["Surveillants : %(surveillant)s" % d],
|
||||
["Batiment : %(batiment)s - Salle : %(salle)s" % d],
|
||||
["Controle : %s (coef. %g)" % (d["evaltitre"], d["eval_data"]["coefficient"])],
|
||||
] # une liste de liste de chaines: description de l'evaluation
|
||||
if form["file_format"].data == "xls":
|
||||
production_xls(d)
|
||||
else:
|
||||
production_pdf(d)
|
||||
|
||||
|
||||
def repartition(form, eval_data):
|
||||
def repartition(d):
|
||||
"""
|
||||
Calcule le placement. retourne une liste de couples ((nom, prenom), position)
|
||||
"""
|
||||
cnx = ndb.GetDBConnexion()
|
||||
# Infos transmises
|
||||
evaluation_id = form["evaluation_id"].data
|
||||
etiquetage = form["etiquetage"].data
|
||||
teachers = form["surveillants"].data
|
||||
building = form["batiment"].data
|
||||
room = form["salle"].data
|
||||
nb_rangs = form["nb_rangs"].data
|
||||
group_ids = form["groups"].data
|
||||
|
||||
# Construit liste des etudiants
|
||||
groups = sco_groups.listgroups(group_ids)
|
||||
gr_title_filename = sco_groups.listgroups_filename(groups)
|
||||
# gr_title = sco_groups.listgroups_abbrev(groups)
|
||||
|
||||
moduleimpl_data = sco_moduleimpl.do_moduleimpl_list(
|
||||
moduleimpl_id=eval_data["moduleimpl_id"]
|
||||
)[0]
|
||||
Mod = sco_edit_module.do_module_list(
|
||||
args={"module_id": moduleimpl_data["module_id"]}
|
||||
)[0]
|
||||
sem = sco_formsemestre.get_formsemestre(moduleimpl_data["formsemestre_id"])
|
||||
evalname = "%s-%s" % (Mod["code"], ndb.DateDMYtoISO(eval_data["jour"]))
|
||||
if eval_data["description"]:
|
||||
evaltitre = eval_data["description"]
|
||||
else:
|
||||
evaltitre = "évaluation du %s" % eval_data["jour"]
|
||||
|
||||
desceval = [
|
||||
["%s" % sem["titreannee"]],
|
||||
["Module : %s - %s" % (Mod["code"], Mod["abbrev"])],
|
||||
["Surveillants : %s" % teachers],
|
||||
["Batiment : %s - Salle : %s" % (building, room)],
|
||||
["Controle : %s (coef. %g)" % (evaltitre, eval_data["coefficient"])]
|
||||
] # une liste de liste de chaines: description de l'evaluation
|
||||
listetud = build_listetud(cnx, groups, evaluation_id, moduleimpl_data)
|
||||
return affectation_places(listetud, etiquetage, nb_rangs)
|
||||
groups = sco_groups.listgroups(d["groups"])
|
||||
d["listetud"] = build_listetud(d)
|
||||
return affectation_places(d)
|
||||
|
||||
|
||||
def build_listetud(cnx, groups, evaluation_id, moduleimpl_data):
|
||||
@ -446,42 +436,38 @@ class Distributeur2D:
|
||||
return retour
|
||||
|
||||
|
||||
def affectation_places(listetud, etiquetage, nb_rangs=1):
|
||||
affectation = []
|
||||
if etiquetage == "continu":
|
||||
def affectation_places(d):
|
||||
plan = []
|
||||
if d["etiquetage"] == "continu":
|
||||
distributeur = DistributeurContinu()
|
||||
else:
|
||||
distributeur = Distributeur2D(nb_rangs)
|
||||
for etud in listetud:
|
||||
affectation.append((etud, distributeur.suivant()))
|
||||
return affectation
|
||||
distributeur = Distributeur2D(d["nb_rangs"])
|
||||
for etud in d["listetud"]:
|
||||
plan.append((etud, distributeur.suivant()))
|
||||
return plan
|
||||
|
||||
|
||||
def production_xls(file_format, eval_dat, plan):
|
||||
|
||||
|
||||
def production(file_format, eval_dat, plan):
|
||||
if file_format == "xls":
|
||||
def production_xls(d):
|
||||
filename = f"placement_{evalname}_{gr_title_filename}{scu.XLSX_SUFFIX}"
|
||||
xls = _excel_feuille_placement(
|
||||
eval_data, desceval, listetud, columns, space, maxlines, building, room, numbering
|
||||
d["eval_data"],
|
||||
d["desceval"],
|
||||
d["listetud"],
|
||||
d["nb_rangs"],
|
||||
d["batiment"],
|
||||
d["salle"],
|
||||
d["etiquetage"],
|
||||
)
|
||||
return sco_excel.send_excel_file(REQUEST, xls, filename)
|
||||
else:
|
||||
nbcolumns = int(columns)
|
||||
|
||||
pdf_title = "%s<br/>" % sem["titreannee"]
|
||||
pdf_title += "Module : %s - %s<br/>" % (Mod["code"], Mod["abbrev"])
|
||||
pdf_title += "Surveillants : %s<br/>" % teachers
|
||||
pdf_title += "Batiment : %s - Salle : %s<br/>" % (building, room)
|
||||
pdf_title += "Controle : %s (coef. %g)<br/>" % (evaltitre, E["coefficient"])
|
||||
pdf_title += "Date : %s - Horaire : %s à %s" % (
|
||||
E["jour"],
|
||||
E["heure_debut"],
|
||||
E["heure_fin"],
|
||||
|
||||
def production_pdf(d):
|
||||
pdf_title = d["desceval"]
|
||||
pdf_title += (
|
||||
"Date : %(jour)s - Horaire : %(heure_debut)s à %(heure_fin)s" % d["eval_data"]
|
||||
)
|
||||
|
||||
filename = "placement_%s_%s.pdf" % (evalname, gr_title_filename)
|
||||
filename = "placement_%(evalname)s_%(gr_title_filename)s.pdf" % d
|
||||
titles = {
|
||||
"nom": "Nom",
|
||||
"prenom": "Prenom",
|
||||
@ -489,6 +475,7 @@ def production(file_format, eval_dat, plan):
|
||||
"ligne": "Ligne",
|
||||
"place": "Place",
|
||||
}
|
||||
nbcolumns = int(columns)
|
||||
if numbering == "coordinate":
|
||||
columns_ids = ["nom", "prenom", "colonne", "ligne"]
|
||||
else:
|
||||
@ -882,6 +869,9 @@ def _excel_feuille_placement(
|
||||
lines: liste de tuples
|
||||
(etudid, nom, prenom, etat, groupe, val, explanation)
|
||||
"""
|
||||
sem_preferences = sco_preferences.SemPreferences()
|
||||
space = sem_preferences.get("feuille_placement_emargement")
|
||||
maxlines = sem_preferences.get("feuille_placement_positions")
|
||||
nbcolumns = int(columns)
|
||||
column_width_ratio = 1 / 250 # changement d unités entre pyExcelerator et openpyxl
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user