Signed-off-by: Cherif <cbalde@mysy-training.com>
master_Elyos_FI
Cherif 2025-06-27 20:00:53 +02:00
parent 050e60b608
commit 191d205a75
6 changed files with 2820 additions and 53 deletions

View File

@ -4,9 +4,12 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="c6d0259a-16e1-410d-91a1-830590ee2a08" name="Changes" comment="25/06/2025 - 18h">
<list default="true" id="c6d0259a-16e1-410d-91a1-830590ee2a08" name="Changes" comment="qsd">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Log/log_file.log" beforeDir="false" afterPath="$PROJECT_DIR$/Log/log_file.log" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Session_Formation.py" beforeDir="false" afterPath="$PROJECT_DIR$/Session_Formation.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/base_class_calcul_note.py" beforeDir="false" afterPath="$PROJECT_DIR$/base_class_calcul_note.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/main.py" beforeDir="false" afterPath="$PROJECT_DIR$/main.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/prj_common.py" beforeDir="false" afterPath="$PROJECT_DIR$/prj_common.py" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
@ -449,7 +452,7 @@
<option name="project" value="LOCAL" />
<updated>1747251650255</updated>
</task>
<option name="localTasksCounter" value="481" />
<option name="localTasksCounter" value="482" />
<servers />
</component>
<component name="Vcs.Log.Tabs.Properties">
@ -510,12 +513,12 @@
<MESSAGE value="08/06/2025 - 16h30" />
<MESSAGE value="09/06/2025 - 21h30" />
<MESSAGE value="11/06/2025 - 16h30" />
<MESSAGE value="qsd" />
<MESSAGE value="19/06/2025 -18h" />
<MESSAGE value="20/06/2025 -18h" />
<MESSAGE value="ss" />
<MESSAGE value="ssqsdqsd" />
<MESSAGE value="25/06/2025 - 18h" />
<option name="LAST_COMMIT_MESSAGE" value="25/06/2025 - 18h" />
<MESSAGE value="qsd" />
<option name="LAST_COMMIT_MESSAGE" value="qsd" />
</component>
</project>

File diff suppressed because one or more lines are too long

View File

@ -6585,7 +6585,7 @@ def Prepare_and_Send_Convention_From_Session_By_PDF(diction):
local_diction['courrier_template_id'] = diction['courrier_template_id']
local_diction['client_id'] = str(val)
print(" ### UUU Client_id = ", str(val))
#print(" ### UUU Client_id = ", str(val))
local_status, local_full_file_name = Create_Convention_By_Client_PDF(local_diction)
if( local_status is False):
return local_status, local_full_file_name

View File

@ -26,13 +26,21 @@ La collection : "note_evaluation_participant" :
- Elle contient les notes attribuées à un apprenant sur une evaluation.
- Elle contient les informations :evaluation_id, inscription_id, group_inscription_id, note (la note obtenue par l'apprenant)
"""
from zipfile import ZipFile
import bson
import pymongo
from flask import send_file
from pandas.io.formats.style import jinja2
from pymongo import MongoClient
import json
from bson import ObjectId
import re
from datetime import datetime
from datetime import datetime, date
from xhtml2pdf import pisa
import module_editique
import prj_common as mycommon
import secrets
import inspect
@ -1376,7 +1384,7 @@ def Cacul_Session_Note_Finale_For_calcul_mode_1(diction):
for my_inscription in tab_inscriptions_ids_splited:
my_inscription = str(my_inscription).strip()
print(" ### pour l'inscrit = ", my_inscription)
#print(" ### pour l'inscrit = ", my_inscription)
nb_coef = 0
session_somme_note_with_coef = 0
session_moyenne_note_with_coef = 0
@ -1408,14 +1416,14 @@ def Cacul_Session_Note_Finale_For_calcul_mode_1(diction):
if( "credit" in tmps_data.keys() and tmps_data['credit']):
session_somme_credit_acquis = round( (float(session_somme_credit_acquis) + float(tmps_data['credit'])), 2)
print(" ### session_somme_credit_acquis total = ", session_somme_credit_acquis)
print(" ### nb_coef total = ", nb_coef)
print(" ### session_somme_note_with_coef = ", session_somme_note_with_coef)
#print(" ### session_somme_credit_acquis total = ", session_somme_credit_acquis)
#print(" ### nb_coef total = ", nb_coef)
#print(" ### session_somme_note_with_coef = ", session_somme_note_with_coef)
session_moyenne_note_with_coef = round( (float(session_somme_note_with_coef) / float(nb_coef)), 2)
print(" ### session_moyenne_note_with_coef = ", session_moyenne_note_with_coef)
#print(" ### session_moyenne_note_with_coef = ", session_moyenne_note_with_coef)
# MAJ de la note glabale
print(" MAJJ classement glabal sur l'inscription = ")
#print(" MAJJ classement glabal sur l'inscription = ")
MYSY_GV.dbname['inscription'].find_one_and_update(
{'_id': ObjectId(str(my_inscription)),
'partner_owner_recid': str(is_session_valide_data['partner_owner_recid']),
@ -1450,7 +1458,7 @@ def Cacul_Session_Note_Finale_For_calcul_mode_1(diction):
note_precedente = str(sorted_data['note_finale'])
# MAJ du rang
print(" MAJJ classement = ", classement)
#print(" MAJJ classement = ", classement)
MYSY_GV.dbname['inscription'].find_one_and_update(
{'_id': ObjectId(str(sorted_data['_id'])),
'partner_owner_recid': str(sorted_data['partner_owner_recid']),
@ -1924,40 +1932,10 @@ def Cacul_UE_Note_Finale_Standard(diction):
return False, " La formation ne contient pas l'UE selectionnée code = " + str(
is_ue_valide_data['code'])
moyenne_generale_session_ue = 0
somme_generale_session_ue = 0
nb_generale_session_ue = 0
tab_note_inscrit = []
"""
Aller recuperer les UE et les types d'evaluation concernée par ce calcul
"" "
somme_ponderation = 0
for tmp in MYSY_GV.dbname['class_unite_enseignement_type_evaluation'].find({'partner_owner_recid':str(my_partner['recid']),
'valide':'1',
'locked':'0',
'class_id':str(diction['class_id']),
'class_ue_id': str(is_ue_valide_data['_id']),
}):
print(" ### COUCOU tmp = ", tmp)
if( "type_evaluation_id" in tmp.keys() ):
type_evaluation_data = MYSY_GV.dbname['type_evaluation'].find_one({'_id': ObjectId(str(tmp['type_evaluation_id'])),
'partner_owner_recid': str(my_partner['recid']),
'valide': '1',
'locked': '0',
})
if(type_evaluation_data ):
print(" ### PR EVAL TYPEE = ", type_evaluation_data)
basic_qry = {'partner_owner_recid': str(my_partner['recid']),
'valide': '1',
'locked': '0',
'session_id': str(diction['session_id']),
'ue_id': str(is_ue_valide_data['_id']),
}
"""
basic_qry = {'partner_owner_recid': str(my_partner['recid']),
'valide': '1',
'locked': '0',
@ -1965,9 +1943,10 @@ def Cacul_UE_Note_Finale_Standard(diction):
'ue_id': str(is_ue_valide_data['_id']),
}
for my_inscription in tab_inscriptions_ids_splited:
nb_generale_session_ue = nb_generale_session_ue + 1
# 1 - recuperer toutes les notes pre_calculée de l'apprenant
print(" ### TRAITEMENT DE L'APPRENN my_inscription = ", my_inscription)
#print(" ### TRAITEMENT DE L'APPRENN my_inscription = ", my_inscription)
tab_note_inscrit = []
local_node = {}
@ -2064,7 +2043,6 @@ def Cacul_UE_Note_Finale_Standard(diction):
note_finale_ue = note_finale_ue + float(str(float(note_eu_inscrit['moyenne']) * float(note_eu_inscrit['evaluation_ponderation'])))
#print(" ### somme note pondéezeee = ", note_finale_ue)
"""
Aller chercher la somme des ponderation sur l'ue pour ce type d'evaluation
@ -2090,7 +2068,6 @@ def Cacul_UE_Note_Finale_Standard(diction):
somme_note_pondere = 0
for list_note_inscrit in MYSY_GV.dbname['session_formation_final_note_classement_detail'].find(
basic_qry):
somme_note_pondere = somme_note_pondere + (float(list_note_inscrit['moyenne']) * float(list_note_inscrit['ponderation_type_eval']))
local_node['detail_note'].append(list_note_inscrit)
@ -2099,6 +2076,18 @@ def Cacul_UE_Note_Finale_Standard(diction):
note_finale_ue = round(somme_note_pondere / somme_ponderation, 2)
local_node['note_finale'] = str(note_finale_ue)
somme_generale_session_ue = somme_generale_session_ue + note_finale_ue
"""
Ajouter la categorie de la note (A, B, C...) :
selon la collection : note_categorie.
Ceci est utilisé pour faire un graphe après
"""
note_categorie_status, note_categorie_code = mycommon.Get_Note_Categorie_Code(str(note_finale_ue), my_partner['recid'])
local_node['note_categorie'] = str(note_categorie_code)
#print(" ### note_finale = ", note_finale_ue)
# Acter si la personne valide l'UE
@ -2129,8 +2118,14 @@ def Cacul_UE_Note_Finale_Standard(diction):
)
if( nb_generale_session_ue > 0 ):
moyenne_generale_session_ue = round(somme_generale_session_ue/nb_generale_session_ue, 2)
"""
Après le calcul de la moyenne finale, il faut faire le classement
Après le calcul de la moyenne finale, il faut faire le classement et calculer la moyenne de la classe
"""
basic_qry = {'partner_owner_recid': str(my_partner['recid']),
'valide': '1',
@ -2142,6 +2137,7 @@ def Cacul_UE_Note_Finale_Standard(diction):
classement = 0
note_precedente = ""
for sorted_data in MYSY_GV.dbname['session_formation_final_note_classement'].find(basic_qry).sort(
[("note_finale", pymongo.DESCENDING), ]):
@ -2160,7 +2156,7 @@ def Cacul_UE_Note_Finale_Standard(diction):
'locked': '0'
},
{"$set": {'rang': str(classement), 'rang_calculation_date': str(datetime.now())}},
{"$set": {'rang': str(classement), 'rang_calculation_date': str(datetime.now()), 'moyenne_session_ue':str(moyenne_generale_session_ue)}},
return_document=ReturnDocument.AFTER,
upsert=False,
)
@ -2423,7 +2419,7 @@ def run_ue_pre_calcul_moyenne_somme_v2(diction):
"""
Mise à jour de la collection des notes d'une session de formation : 'session_formation_final_note_classement_detail'
"""
print(" ### tab_note_final = ", tab_note_final)
#print(" ### tab_note_final = ", tab_note_final)
for note_finale in tab_note_final:
qry_key = {}
qry_key['partner_owner_recid'] = my_partner['recid']
@ -2680,3 +2676,253 @@ def Cacul_Session_Note_Finale_For_calcul_Stanard(diction):
mycommon.myprint(str(inspect.stack()[0][3]) + " -" + str(e) + " - Line : " + str(exc_tb.tb_lineno))
return False, " Impossible de lancer Cacul_Session_Note_Finale_For_calcul_mode_1 "
"""
Cette fonction permet d'imprimer le bulleting
d'un appernant (bulletin de note)
"""
def Prepare_and_Send_Inscrit_Bulletin_Note_By_PDF(diction):
try:
field_list_obligatoire = ['token', 'tab_inscrit_ids', 'courrier_template_id']
for val in field_list_obligatoire:
if val not in diction:
mycommon.myprint(
str(inspect.stack()[0][
3]) + " La valeur '" + val + "' n'est pas presente dans la liste des arguments ")
return False, " La valeur '" + val + "' n'est pas presente dans la liste des arguments"
elif str(diction[val]).strip() == "":
mycommon.myprint(
str(inspect.stack()[0][
3]) + " Le champ obligatoire '" + val + "' est vide ")
return False, " Le champ obligatoire '" + val + "' est vide "
my_token = ""
if ("token" in diction.keys()):
if diction['token']:
my_token = diction['token']
local_status, my_partner = mycommon.Check_Connexion_And_Return_Partner_Data(diction)
if (local_status is not True):
return local_status, my_partner
"""
Verifier la validité des inscrit
"""
tab_inscrit_ids_work = str(diction['tab_inscrit_ids']).split(',')
tab_inscrit_ids = []
for tmp in tab_inscrit_ids_work:
if( tmp ):
if( MYSY_GV.dbname['inscription'].count_documents({'_id':ObjectId(str(tmp)),
'valide':'1',
'locked':'0',
'partner_owner_recid':str(my_partner['recid'])}) == 1):
tab_inscrit_ids.append(str(tmp))
else:
mycommon.myprint(
str(inspect.stack()[0][
3]) + " L'inscription '" + tmp + "' est invalide ")
return False, " L'inscription '" + tmp + "' est invalide "
# Stokage des nom de fichier à zipper
list_file_name_to_zip = []
for inscrit in tab_inscrit_ids:
inscription_data = MYSY_GV.dbname['inscription'].find_one({'_id': ObjectId(str(inscrit)),
'valide': '1',
'locked': '0',
'partner_owner_recid': str(my_partner['recid'])})
local_node = {}
local_node['inscription_id'] = str(inscription_data['_id'])
local_node['courrier_template_id'] = str(diction['courrier_template_id'])
local_node['token'] = str(diction['token'])
local_node['session_id'] = str(inscription_data['session_id'])
local_node['class_id'] = str(inscription_data['class_id'])
print(" #### Traitemnt de local_diction rrr = ", local_node)
local_status, local_full_file_name = Create_Bulletin_By_Inscrit_PDF(local_node)
if (local_status is False):
return local_status, local_full_file_name
else:
list_file_name_to_zip.append(str(local_full_file_name))
# Create a ZipFile Object
todays_date = str(date.today().strftime("%d/%m/%Y"))
ts = datetime.now().timestamp()
ts = str(ts).replace(".", "").replace(",", "")[-3:]
zip_file_name = str(MYSY_GV.TEMPORARY_DIRECTORY_V2)+"List_Bulletin_Notes_"+str(ts)+".zip"
with ZipFile(zip_file_name, 'w') as zip_object:
for pdf_files in list_file_name_to_zip :
#print(" ### fichier a zipper = ", pdf_files)
zip_object.write(str(pdf_files))
if os.path.exists(zip_file_name):
# print(" ### ok os.path.exists(outputFilename) "+str(outputFilename))
return True, send_file(zip_file_name, as_attachment=True)
return False, " Impossible de générer le bulletin de note en PDF (1) "
except Exception as e:
exc_type, exc_obj, exc_tb = sys.exc_info()
mycommon.myprint(str(inspect.stack()[0][3]) + " -" + str(e) + " - Line : " + str(exc_tb.tb_lineno))
return False, " Impossible de générer le bulletin de note en PDF "
"""
Cette fonction créer le PDF bulletin pour chaque inscrit et retourne l'url complet du fichier pdf
"""
def Create_Bulletin_By_Inscrit_PDF(diction):
try:
field_list_obligatoire = ['token', 'courrier_template_id', 'inscription_id', 'class_id', 'session_id']
for val in field_list_obligatoire:
if val not in diction:
mycommon.myprint(
str(inspect.stack()[0][
3]) + " La valeur '" + val + "' n'est pas presente dans la liste des arguments ")
return False, " La valeur '" + val + "' n'est pas presente dans la liste des arguments"
my_token = ""
if ("token" in diction.keys()):
if diction['token']:
my_token = diction['token']
local_status, my_partner = mycommon.Check_Connexion_And_Return_Partner_Data(diction)
if (local_status is not True):
return local_status, my_partner
# 1 - Verifier que le modele de courrier est bien editable par individu
template_courrier_data = MYSY_GV.dbname['courrier_template'].find_one({'_id':ObjectId(str(diction['courrier_template_id'])),
'valide':'1',
'locked':'0',
'ref_interne': 'INSCRIT_BULLETIN_NOTE',
'partner_owner_recid':str(my_partner['recid'])})
if (template_courrier_data is None ):
mycommon.myprint(
str(inspect.stack()[0][
3]) + "L'identifiant du modèle de document "+str(diction['courrier_template_id'])+" est invalide")
return False, "L'identifiant du modèle de document "+str(diction['courrier_template_id'])+" est invalide "
if( "edit_by_client" in template_courrier_data.keys() and str(template_courrier_data['edit_by_client']) == "1" ):
mycommon.myprint(
str(inspect.stack()[0][
3]) + " Le modèle de courrier n'est pas éditable par stagiaire. ")
return False, " Le modèle de courrier n'est pas éditable par stagiaire "
statgiaire_data = MYSY_GV.dbname['inscription'].find_one({'_id':ObjectId(str(diction['inscription_id'])), 'status':'1',
'partner_owner_recid':str(my_partner['recid'])})
tab_stagiaire = []
tab_stagiaire.append(statgiaire_data['_id'])
"""
update du 30/01/2024
si le participant à un apprenant_id, alors on recupere la valeur de l'apprenant id. On doit travailler sur cette dernier.
En general, avant d'envoyer une convocation ou convention, l'inscription est validée et donc le dossier apprenant existe.
Du coup si tout se passe bien, dans cette fonction, on travaillera tjrs avec l'apprenant_id
"""
tab_apprenant = []
if ("apprenant_id" in statgiaire_data.keys() and statgiaire_data['apprenant_id']):
tab_apprenant.append(ObjectId(str(statgiaire_data['apprenant_id'])))
# Recuperer les notes des UE dans la collection 'session_formation_final_note_classement'
session_formation_final_note_classement_data = []
for note_classement_data in MYSY_GV.dbname['session_formation_final_note_classement'].find({'inscription_id': str(diction['inscription_id']),
'valide': '1', 'partner_owner_recid': str(my_partner['recid'])}):
local_node = {}
local_node = note_classement_data
if( "ue_id" in note_classement_data.keys() and note_classement_data['ue_id']):
ue_id_data = MYSY_GV.dbname['unite_enseignement'].find_one({'_id':ObjectId(note_classement_data['ue_id']),
'partner_owner_recid':my_partner['recid']}, {'_id':1, 'code':1, 'titre':1})
if( ue_id_data ):
local_node['ue_id_data'] = ue_id_data
else:
local_node['ue_id_data'] = {}
session_formation_final_note_classement_data.append(local_node)
print(" ### session_formation_final_note_classement_data = ",session_formation_final_note_classement_data )
tab_session = []
tab_session.append(ObjectId(diction['session_id']))
tab_class = []
tab_class.append(ObjectId(diction['class_id']))
# Creation du dictionnaire d'information à utiliser pour la creation du doc
convention_dictionnary_data = {}
new_diction = {}
new_diction['token'] = diction['token']
new_diction['list_stagiaire_id'] = tab_stagiaire
new_diction['list_session_id'] = tab_session
new_diction['list_class_id'] = tab_class
new_diction['list_client_id'] = []
new_diction['list_apprenant_id'] = tab_apprenant
local_status, local_retval = mycommon.Get_Dictionnary_data_For_Template(new_diction)
if (local_status is False):
return local_status, local_retval
convention_dictionnary_data = local_retval
convention_dictionnary_data['session_formation_final_note_classement'] = session_formation_final_note_classement_data
convention_dictionnary_data['full_statgiaire_data'] = statgiaire_data
body = {
"params": convention_dictionnary_data,
}
"""
Creation du ficier PDF
"""
contenu_doc_Template = jinja2.Template(str(template_courrier_data['contenu_doc']))
sourceHtml = contenu_doc_Template.render(params=body["params"])
todays_date = str(date.today().strftime("%d/%m/%Y"))
ts = datetime.now().timestamp()
ts = str(ts).replace(".", "").replace(",", "")[-3:]
orig_file_name = "Bulletin_Note_" + str(statgiaire_data['nom']+'_'+statgiaire_data['prenom']) + "_" + str(ts) + ".pdf"
outputFilename = str(MYSY_GV.TEMPORARY_DIRECTORY) + "/" + str(orig_file_name)
# open output file for writing (truncated binary)
resultFile = open(outputFilename, "w+b")
# convert HTML to PDF
pisaStatus = pisa.CreatePDF(
src=sourceHtml, # the HTML to convert
dest=resultFile) # file handle to receive result
# close output file
resultFile.close()
return True, outputFilename
except Exception as e:
exc_type, exc_obj, exc_tb = sys.exc_info()
mycommon.myprint(str(inspect.stack()[0][3]) + " -" + str(e) + " - Line : " + str(exc_tb.tb_lineno))
return False, " Impossible de créer le fichier pdf Create_Bulletin_By_Inscrit_PDF "

20
main.py
View File

@ -12581,6 +12581,26 @@ def Get_List_Evaluation_Inscriptio_Note_Classement_With_Filter():
return jsonify(status=localStatus, message=message )
"""
API pour imprimer le bulletin de note d'un inscrit
"""
@app.route('/myclass/api/Prepare_and_Send_Inscrit_Bulletin_Note_By_PDF/<token>/<tab_inscrit_ids>/<courrier_template_id>', methods=['POST','GET'])
@crossdomain(origin='*')
def Prepare_and_Send_Inscrit_Bulletin_Note_By_PDF(token, tab_inscrit_ids, courrier_template_id):
# On recupere le corps (payload) de la requete
payload = mycommon.strip_dictionary (request.form.to_dict())
payload = {}
payload['token'] = str(token)
payload['tab_inscrit_ids'] = str(tab_inscrit_ids)
payload['courrier_template_id'] = str(courrier_template_id)
print(" ### Prepare_and_Send_Inscrit_Bulletin_Note_By_PDF : payload = ",str(payload))
localStatus, response= base_class_calcul_note.Prepare_and_Send_Inscrit_Bulletin_Note_By_PDF(payload)
if(localStatus ):
return response
else:
return False
"""

View File

@ -4848,7 +4848,8 @@ def Get_Dictionnary_data_For_Template(diction):
tab_apprenant_id = diction['list_apprenant_id']
apprenant_liste_champ = ['prenom', 'nom', 'email', 'civilite', 'telephone', 'employeur', 'adresse', 'code_postal', 'ville', 'pays', 'tuteur1_nom', 'tuteur1_prenom', 'tuteur1_email', 'tuteur1_telephone',
'tuteur2_nom', 'tuteur2_prenom', 'tuteur2_email', 'tuteur2_telephone', 'opco', 'tuteur1_adresse', 'tuteur1_cp', 'tuteur1_ville', 'tuteur1_pays', 'tuteur1_include_com', 'tuteur2_adresse',
'tuteur2_cp', 'tuteur2_ville', 'tuteur2_pays', 'tuteur2_include_com', 'client_rattachement_id', 'photo_profil']
'tuteur2_cp', 'tuteur2_ville', 'tuteur2_pays', 'tuteur2_include_com', 'client_rattachement_id', 'photo_profil',
'date_naissance', 'naissance_departement', 'naissance_lieu', 'naissance_pays', 'nationalite', 'num_secu', 'piece_identite_num', 'situation_famille']
qry_apprenant = {'_id': {'$in': tab_apprenant_id, },
'valide': '1',
@ -7970,3 +7971,35 @@ def Get_total_Ponderation_Class_UE_Type_Eval( class_id, ue_id):
return True, somme_ponderation
except ValueError:
return False, 0
"""
Cette fonction prends une note (float)
et retourne sa categorie selon la collection : note_categorie
"""
def Get_Note_Categorie_Code( note, partner_owner_recid ):
try:
local_note = tryFloat(str(note))
code_note = '?'
if( MYSY_GV.dbname['note_categorie'].count_documents({'partner_owner_recid':str(partner_owner_recid)}) > 0 ):
for tmp in MYSY_GV.dbname['note_categorie'].find({'partner_owner_recid':str(partner_owner_recid)}):
note_max = tryFloat(str(tmp['note_max']))
note_min = tryFloat(str(tmp['note_min']))
if( note_max >= local_note and local_note >= note_min):
code_note = str(tmp['code'])
else:
for tmp in MYSY_GV.dbname['note_categorie'].find({'partner_owner_recid':'default'}):
note_max = tryFloat(str(tmp['note_max']))
note_min = tryFloat(str(tmp['note_min']))
if( note_max >= local_note and local_note >= note_min):
code_note = str(tmp['code'])
return True, code_note
except ValueError:
return False, '?'