Spaces:
Running
Running
Create kyc_form.py
Browse files- src/modules/kyc_form.py +203 -0
src/modules/kyc_form.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import re
|
| 3 |
+
from datetime import date, timedelta
|
| 4 |
+
|
| 5 |
+
# --- FONCTIONS UTILITAIRES INTERNES ---
|
| 6 |
+
def inject_pulsing_css():
|
| 7 |
+
st.markdown("""
|
| 8 |
+
<style>
|
| 9 |
+
@keyframes pulse-red {
|
| 10 |
+
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(255, 82, 82, 0.7); }
|
| 11 |
+
70% { transform: scale(1); box-shadow: 0 0 0 10px rgba(255, 82, 82, 0); }
|
| 12 |
+
100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(255, 82, 82, 0); }
|
| 13 |
+
}
|
| 14 |
+
.mandatory-dot {
|
| 15 |
+
display: inline-block;
|
| 16 |
+
width: 10px;
|
| 17 |
+
height: 10px;
|
| 18 |
+
background-color: #ff5252;
|
| 19 |
+
border-radius: 50%;
|
| 20 |
+
margin-left: 8px;
|
| 21 |
+
animation: pulse-red 2s infinite;
|
| 22 |
+
vertical-align: middle;
|
| 23 |
+
}
|
| 24 |
+
.field-label { font-weight: 600; color: #F5F8FA; margin-bottom: 5px; display: block; }
|
| 25 |
+
</style>
|
| 26 |
+
""", unsafe_allow_html=True)
|
| 27 |
+
|
| 28 |
+
def lbl(text, mandatory=False):
|
| 29 |
+
if mandatory:
|
| 30 |
+
st.markdown(f'<span class="field-label">{text} <span class="mandatory-dot" title="Obligatoire"></span></span>', unsafe_allow_html=True)
|
| 31 |
+
else:
|
| 32 |
+
st.markdown(f'<span class="field-label">{text}</span>', unsafe_allow_html=True)
|
| 33 |
+
|
| 34 |
+
# Listes de référence
|
| 35 |
+
LISTE_PROFESSIONS = ["Commerçant", "Fonctionnaire", "Agriculteur", "Etudiant", "Ouvrier", "Cadre Supérieur", "Ingénieur", "Médecin", "Autre"]
|
| 36 |
+
LISTE_QUARTIERS = ["Plateau", "Médina", "Yoff", "Ouakam", "Almadies", "Mermoz", "Pikine", "Guédiawaye", "Autre"]
|
| 37 |
+
LISTE_VILLES = ["Dakar", "Touba", "Thiès", "Saint-Louis", "Ziguinchor", "Autre"]
|
| 38 |
+
LISTE_PAYS = ["Sénégal", "France", "Côte d'Ivoire", "Mali", "États-Unis", "Autre"]
|
| 39 |
+
|
| 40 |
+
# --- FONCTION PRINCIPALE APPELÉE PAR L'APP ---
|
| 41 |
+
def show_kyc_form(client, sheet_name, generate_id_func):
|
| 42 |
+
"""
|
| 43 |
+
Affiche le formulaire KYC.
|
| 44 |
+
Args:
|
| 45 |
+
client: La connexion gspread active (passée depuis main).
|
| 46 |
+
sheet_name: Le nom du fichier Google Sheet.
|
| 47 |
+
generate_id_func: La fonction pour générer l'ID (passée depuis main).
|
| 48 |
+
"""
|
| 49 |
+
inject_pulsing_css()
|
| 50 |
+
|
| 51 |
+
st.header("ENTITÉ : NOUVEL OBJET CLIENT")
|
| 52 |
+
|
| 53 |
+
# Appel de la fonction parente pour l'ID
|
| 54 |
+
new_id = generate_id_func("CLI", "Clients_KYC")
|
| 55 |
+
st.caption(f"Système ID : {new_id}")
|
| 56 |
+
|
| 57 |
+
type_personne = st.radio("Nature", ["Personne Physique", "Personne Morale"], horizontal=True)
|
| 58 |
+
if type_personne == "Personne Morale":
|
| 59 |
+
st.warning("⚠️ Module Personne Morale en construction.")
|
| 60 |
+
|
| 61 |
+
with st.form("kyc_form_module", clear_on_submit=False):
|
| 62 |
+
# --- BLOC 1 : IDENTITÉ ---
|
| 63 |
+
st.markdown("### 👤 IDENTITÉ & CONTACT")
|
| 64 |
+
c1, c2, c3 = st.columns(3)
|
| 65 |
+
with c1:
|
| 66 |
+
lbl("Nom Complet", True)
|
| 67 |
+
nom = st.text_input("Nom", label_visibility="collapsed")
|
| 68 |
+
lbl("Date de Naissance", True)
|
| 69 |
+
date_naiss = st.date_input("Date Naiss", value=date.today()-timedelta(days=365*18), min_value=date.today()-timedelta(days=365*100), max_value=date.today()-timedelta(days=365*18), label_visibility="collapsed")
|
| 70 |
+
with c2:
|
| 71 |
+
lbl("Adresse", True)
|
| 72 |
+
adresse = st.text_input("Adresse", label_visibility="collapsed")
|
| 73 |
+
lbl("Téléphone", True)
|
| 74 |
+
telephone = st.text_input("Tel", label_visibility="collapsed")
|
| 75 |
+
with c3:
|
| 76 |
+
lbl("Email", True)
|
| 77 |
+
email = st.text_input("Email", label_visibility="collapsed").lower()
|
| 78 |
+
lbl("État Civil", True)
|
| 79 |
+
etat_civil = st.selectbox("Etat", ["Célibataire", "Marié(e)", "Divorcé(e)", "Veuf(ve)"], label_visibility="collapsed")
|
| 80 |
+
|
| 81 |
+
# --- BLOC 2 : DOCUMENTS ---
|
| 82 |
+
st.markdown("### 🆔 DOCUMENTS OFFICIELS")
|
| 83 |
+
c4, c5, c6 = st.columns(3)
|
| 84 |
+
with c4:
|
| 85 |
+
lbl("Type Pièce", True)
|
| 86 |
+
type_id = st.selectbox("Type ID", ["CNI", "Passeport", "Permis", "Carte_Electeur"], label_visibility="collapsed")
|
| 87 |
+
with c5:
|
| 88 |
+
lbl("Numéro ID", True)
|
| 89 |
+
id_officiel = st.text_input("Num ID", label_visibility="collapsed")
|
| 90 |
+
with c6:
|
| 91 |
+
lbl("Expiration", True)
|
| 92 |
+
date_exp_id = st.date_input("Exp ID", value=date.today()+timedelta(days=90), min_value=date.today(), label_visibility="collapsed")
|
| 93 |
+
|
| 94 |
+
# --- BLOC 3 : PRO ---
|
| 95 |
+
st.markdown("### 💼 PROFESSION")
|
| 96 |
+
c7, c8, c9 = st.columns(3)
|
| 97 |
+
with c7:
|
| 98 |
+
lbl("Statut Pro", True)
|
| 99 |
+
statut_pro = st.selectbox("Statut", ["Salarié", "Indépendant", "Fonctionnaire", "Etudiant", "Sans_Emploi", "Retraité"], label_visibility="collapsed")
|
| 100 |
+
lbl("Profession", True)
|
| 101 |
+
prof_val = st.selectbox("Choix Profession", LISTE_PROFESSIONS, label_visibility="collapsed")
|
| 102 |
+
if prof_val == "Autre": prof_val = st.text_input("Préciser Profession")
|
| 103 |
+
with c8:
|
| 104 |
+
lbl("Employeur", False)
|
| 105 |
+
employeur = st.text_input("Employeur", label_visibility="collapsed")
|
| 106 |
+
lbl("Secteur", True)
|
| 107 |
+
secteur = st.selectbox("Secteur", ["Commerce", "Services", "Agriculture", "Autre"], label_visibility="collapsed")
|
| 108 |
+
with c9:
|
| 109 |
+
lbl("Pers. Charge", True)
|
| 110 |
+
pers_charge = st.number_input("Charge", min_value=0, step=1, label_visibility="collapsed")
|
| 111 |
+
lbl("Ancienneté (mois)", True)
|
| 112 |
+
anciennete = st.number_input("Ancienneté", min_value=0, step=1, label_visibility="collapsed")
|
| 113 |
+
|
| 114 |
+
# --- BLOC 4 : FINANCES ---
|
| 115 |
+
st.markdown("### 💰 FINANCES (XOF)")
|
| 116 |
+
c10, c11, c12 = st.columns(3)
|
| 117 |
+
with c10:
|
| 118 |
+
lbl("Revenus Mensuels", True)
|
| 119 |
+
revenus = st.number_input("Rev. Mensuel", min_value=0.0, step=10000.0, format="%.2f", label_visibility="collapsed")
|
| 120 |
+
with c11:
|
| 121 |
+
lbl("Autres Revenus", False)
|
| 122 |
+
autres_rev = st.number_input("Autre Rev.", min_value=0.0, step=5000.0, format="%.2f", label_visibility="collapsed")
|
| 123 |
+
with c12:
|
| 124 |
+
lbl("Charges Estimées", False)
|
| 125 |
+
charges = st.number_input("Charges", min_value=0.0, step=5000.0, format="%.2f", label_visibility="collapsed")
|
| 126 |
+
|
| 127 |
+
# --- BLOC 5 : LOCALISATION ---
|
| 128 |
+
st.markdown("### 🏠 LOCALISATION")
|
| 129 |
+
c13, c14, c15 = st.columns(3)
|
| 130 |
+
with c13:
|
| 131 |
+
lbl("Statut Logement", True)
|
| 132 |
+
statut_log = st.selectbox("Logement", ["Propriétaire", "Locataire", "Hébergé"], label_visibility="collapsed")
|
| 133 |
+
lbl("Patrimoine", False)
|
| 134 |
+
patrimoine = st.number_input("Patrimoine", min_value=0.0, step=100000.0, label_visibility="collapsed")
|
| 135 |
+
with c14:
|
| 136 |
+
lbl("Ville", True)
|
| 137 |
+
ville_val = st.selectbox("Ville", LISTE_VILLES, label_visibility="collapsed")
|
| 138 |
+
if ville_val == "Autre": ville_val = st.text_input("Préciser Ville")
|
| 139 |
+
lbl("Quartier", True)
|
| 140 |
+
quartier_val = st.selectbox("Quartier", LISTE_QUARTIERS, label_visibility="collapsed")
|
| 141 |
+
if quartier_val == "Autre": quartier_val = st.text_input("Préciser Quartier")
|
| 142 |
+
with c15:
|
| 143 |
+
lbl("Pays", True)
|
| 144 |
+
pays_val = st.selectbox("Pays", LISTE_PAYS, label_visibility="collapsed")
|
| 145 |
+
if pays_val == "Autre": pays_val = st.text_input("Préciser Pays")
|
| 146 |
+
lbl("N° Fiscal", False)
|
| 147 |
+
n_fiscal = st.text_input("Fiscal", label_visibility="collapsed")
|
| 148 |
+
|
| 149 |
+
# --- BLOC 6 : CONFORMITÉ ---
|
| 150 |
+
st.markdown("### 🛡️ CONFORMITÉ")
|
| 151 |
+
c16, c17 = st.columns(2)
|
| 152 |
+
with c16:
|
| 153 |
+
lbl("Moyen Transfert", True)
|
| 154 |
+
transfert = st.selectbox("Transfert", ["Virement", "Mobile_Money", "Cash", "Chèque"], label_visibility="collapsed")
|
| 155 |
+
lbl("Banque", True)
|
| 156 |
+
banque = st.text_input("Banque", label_visibility="collapsed")
|
| 157 |
+
with c17:
|
| 158 |
+
lbl("Origine Fonds", True)
|
| 159 |
+
origine = st.selectbox("Origine", ["Salaire", "Commerce", "Epargne", "Héritage", "Autre"], label_visibility="collapsed")
|
| 160 |
+
lbl("AML Check", True)
|
| 161 |
+
aml = st.selectbox("AML", ["Non_Fait", "OK", "Match", "Review"], label_visibility="collapsed")
|
| 162 |
+
|
| 163 |
+
st.divider()
|
| 164 |
+
lbl("Notes", False)
|
| 165 |
+
notes = st.text_area("Notes", label_visibility="collapsed")
|
| 166 |
+
|
| 167 |
+
submit_btn = st.form_submit_button("🔍 VÉRIFIER ET ENREGISTRER")
|
| 168 |
+
|
| 169 |
+
# --- LOGIQUE DE VALIDATION ET ENVOI ---
|
| 170 |
+
if submit_btn:
|
| 171 |
+
errors = []
|
| 172 |
+
if not re.match(r"^[a-zA-Z\s\-\']{2,100}$", nom): errors.append("❌ Nom invalide.")
|
| 173 |
+
clean_phone = re.sub(r"[\s\-\.]", "", telephone)
|
| 174 |
+
if not re.match(r"^(\+|00)?221(7[0678]|33)[0-9]{7}$", clean_phone) and not re.match(r"^\+?[0-9]{9,15}$", clean_phone): errors.append("❌ Téléphone invalide.")
|
| 175 |
+
if not re.match(r"[^@]+@[^@]+\.[^@]+", email): errors.append("❌ Email invalide.")
|
| 176 |
+
if " " in id_officiel or len(id_officiel) < 5: errors.append("❌ ID Officiel invalide.")
|
| 177 |
+
if statut_pro in ["Salarié", "Fonctionnaire"] and len(employeur) < 2: errors.append("❌ Employeur obligatoire.")
|
| 178 |
+
|
| 179 |
+
if errors:
|
| 180 |
+
for e in errors: st.error(e)
|
| 181 |
+
else:
|
| 182 |
+
try:
|
| 183 |
+
# Préparation de la ligne
|
| 184 |
+
row_to_add = [
|
| 185 |
+
new_id, nom.upper(), str(date_naiss), adresse, telephone, email,
|
| 186 |
+
type_id, id_officiel.upper(), str(date_exp_id), etat_civil, pers_charge,
|
| 187 |
+
prof_val.upper(), statut_pro, employeur.upper(), secteur, anciennete,
|
| 188 |
+
revenus, autres_rev, charges, patrimoine,
|
| 189 |
+
statut_log, quartier_val.upper(), ville_val.upper(), pays_val.upper(),
|
| 190 |
+
transfert, banque.upper(), n_fiscal, aml, origine, notes
|
| 191 |
+
]
|
| 192 |
+
|
| 193 |
+
# ÉCRITURE VIA LA CONNEXION PASSÉE EN ARGUMENT
|
| 194 |
+
sh = client.open(sheet_name)
|
| 195 |
+
worksheet = sh.worksheet("Clients_KYC")
|
| 196 |
+
worksheet.append_row(row_to_add)
|
| 197 |
+
|
| 198 |
+
st.success(f"✅ CLIENT {new_id} VALIDÉ ET ENREGISTRÉ !")
|
| 199 |
+
st.balloons()
|
| 200 |
+
except Exception as e:
|
| 201 |
+
st.error(f"Erreur technique module KYC : {e}")
|
| 202 |
+
|
| 203 |
+
|