import streamlit as st import pandas as pd from datetime import datetime, date, timedelta # IMPORT DES NOUVEAUX MODULES from Analytics.AnalyseFinance import ( analyser_capacite, generer_tableau_amortissement, calculer_taux_endettement, clean_taux_value # ✅ AJOUT ) from DocumentGen.AutoPDFGeneration import generer_contrat_pret, generer_reconnaissance_dette, generer_contrat_caution import time # ============================================================================ # GESTION DU CACHE (À PLACER ICI) # ============================================================================ @st.cache_data(ttl=600) # Le cache expire après 10 minutes def get_cached_data(_client, sheet_name, worksheet_name): """Lit une feuille spécifique et la transforme en DataFrame avec cache.""" try: sh = _client.open(sheet_name) ws = sh.worksheet(worksheet_name) return pd.DataFrame(ws.get_all_records()) except Exception as e: st.error(f"Erreur lors de la lecture de {worksheet_name}: {e}") return pd.DataFrame() def refresh_data(): """Fonction pour vider le cache manuellement.""" st.cache_data.clear() st.rerun() # ============================================================================ # STYLES CSS (Ta fonction existante) # ============================================================================ def apply_loans_engine_styles(): st.markdown(""" """, unsafe_allow_html=True) # ============================================================================ # MAIN FUNCTION # ============================================================================ def show_loans_engine(client, sheet_name): apply_loans_engine_styles() st.markdown('
', unsafe_allow_html=True) st.header("MOTEUR FINANCIER : OCTROI DE PRÊT") # 1. CHARGEMENT DONNÉES try: # On définit 'sh' ici pour qu'il soit accessible dans toute la fonction sh = client.open(sheet_name) # Utilise get_cached_data pour éviter les erreurs de quota df_clients = get_cached_data(client, sheet_name, "Clients_KYC") try: df_garants = get_cached_data(client, sheet_name, "Garants_KYC") if not df_garants.empty: df_garants['search_label'] = df_garants['ID_Garant'].astype(str) + " - " + df_garants['Nom_Complet'].astype(str) except Exception as e: st.error(f"Erreur sur la feuille Garants : {e}") df_garants = pd.DataFrame() except Exception as e: st.error(f"Erreur connexion : {e}") return # ============================================================================ # 2. SÉLECTION CLIENT (Obligatoire) # ============================================================================ if df_clients.empty: st.error("🛑 Aucun client trouvé dans la base KYC. Veuillez d'abord enregistrer un client pour octroyer un prêt.") return # On arrête l'exécution si pas de client else: df_clients['search_label'] = df_clients['ID_Client'].astype(str) + " - " + df_clients['Nom_Complet'].astype(str) selected_client_label = st.selectbox("Rechercher une cible ", [""] + df_clients['search_label'].tolist()) if selected_client_label: # <--- TOUT DOIT ÊTRE DANS CE BLOC # Récupération de la ligne complète client_info = df_clients[df_clients['search_label'] == selected_client_label].iloc[0] client_id = client_info['ID_Client'] # ✅ Lecture du nombre de personnes à charge depuis le KYC try: pers_charge = int(client_info.get('Pers_Charge', 0)) except (ValueError, TypeError): pers_charge = 0 st.info(f"♦️Cible Détectée : **{client_info['Nom_Complet']}**") with st.expander(f"Analyse Solvabilité : {client_info['Nom_Complet']}", expanded=True): def clean_val(val): try: return float(str(val).replace("XOF","").replace(" ","").replace(",","")) except: return 0.0 rev = clean_val(client_info['Revenus_Mensuels']) chg = clean_val(client_info['Charges_Estimees']) reste = rev - chg c1, c2, c3 = st.columns(3) c1.metric("Revenus Mensuels", f"{int(rev):,} XOF".replace(",", " ")) c2.metric("Charges Estimées", f"{int(chg):,} XOF".replace(",", " ")) # Alerte visuelle si le reste à vivre est faible delta_color = "normal" if reste > 20000 else "inverse" c3.metric("Reste à vivre (Net)", f"{int(reste):,} XOF".replace(",", " "), delta=f"{int(reste):,}", delta_color=delta_color) st.markdown(f"**Profession :** {client_info['Statut_Pro']} | **Ville :** {client_info['Ville']}") # ✅ Affichage IPF si le client a des personnes à charge if pers_charge >= 1: ipf = rev / (1 + pers_charge) st.info(f"📊 **IPF (Indice de Pression Familiale)** : {int(ipf):,} XOF — base de calcul ajustée ({pers_charge} pers. à charge)".replace(",", " ")) # ============================================================================ # 3. SÉLECTION GARANT (Optionnel) # ============================================================================ selected_garant = None garant_id = "" if df_garants.empty: st.info("Aucun garant n'est actuellement enregistré dans la base (Optionnel).") else: selected_garant_label = st.selectbox("Rechercher un garant (Optionnel)", [""] + df_garants['search_label'].tolist()) if selected_garant_label: # Récupération de la ligne complète garant_info = df_garants[df_garants['search_label'] == selected_garant_label].iloc[0] selected_garant = garant_info # Pour l'utiliser dans la génération PDF garant_id = garant_info['ID_Garant'] st.info(f"🔸Garant Détectée : **{garant_info['Nom_Complet']}**") with st.expander(f"Analyse de la Caution : {garant_info['Nom_Complet']}", expanded=True): rev_g = clean_val(garant_info['Revenus_Mensuels']) chg_g = clean_val(garant_info['Charges_Estimees']) g1, g2, g3 = st.columns(3) g1.metric("Revenus Garant", f"{int(rev_g):,} XOF".replace(",", " ")) g2.metric("Charges Garant", f"{int(chg_g):,} XOF".replace(",", " ")) g3.metric("Reste à vivre", f"{int(rev_g - chg_g):,} XOF".replace(",", " ")) st.warning("⚠️ **Engagement solidaire** : Le garant renonce aux bénéfices de discussion et de division. Il s'engage à payer en cas de défaillance de l'emprunteur.") # 4. CONFIGURATION PRÊT st.markdown("---") st.subheader("Configuration") col_motif, col_type, col_moyen = st.columns(3) with col_motif: # NOUVEAU : MOTIF motif = st.selectbox( "Motif du prêt", [ "Commerce / Achat de stock", "Investissement", "Trésorerie professionnelle", "Lancement d'activité", "Développement d'activité", "Agriculture / Élevage", "Transport / Logistique", "Urgence médicale", "Scolarité / Formation", "Logement / Habitat", "Réparations", "Événements familiaux", "Voyage / Déplacement", "Consommation", "Achat d'équipement personnel", "Projet personnel", "Autre" ] ) with col_type: type_pret = st.selectbox("Type de remboursement", ["In Fine", "Mensuel - Intérêts", "Mensuel - Constant", "Hebdomadaire", "Personnalisé"]) with col_moyen: # Correction : Définition de la variable moyen_transfert moyen_transfert = st.selectbox("Moyen de transfert", ["Wave", "Orange Money", "Virement"]) # Mapping Type type_code_map = {"In Fine": "IN_FINE", "Mensuel - Intérêts": "MENSUEL_INTERETS", "Mensuel - Constant": "MENSUEL_CONSTANT", "Hebdomadaire": "HEBDOMADAIRE", "Personnalisé": "PERSONNALISE"} type_code = type_code_map[type_pret] col1, col2, col3 = st.columns(3) montant = col1.number_input("Montant (XOF)", 10000, value=100000, step=10000) taux_hebdo = col2.number_input("Taux Hebdo (%)", 0.1, value=2.0, step=0.1) # ✅ La durée sera définie spécifiquement dans chaque type de prêt ci-dessous # ==================================================================== # DÉBUT DU BLOC LOGIQUE À COPIER-COLLER # ==================================================================== # Initialisation des variables pour éviter les erreurs montant_versement = 0 montant_total = 0 cout_credit = 0 nb_versements = 0 duree_semaines = 0 dates_versements = [] # Initialisation date_debut = date.today() date_fin = date_debut # Par défaut # ----------------------------------------------------------- # 1. LOGIQUE IN FINE (1 seul versement à la fin) # ----------------------------------------------------------- if type_code == "IN_FINE": duree_semaines = col3.number_input("Durée (en semaines)", min_value=1, max_value=104, value=8) date_fin = date_debut + timedelta(weeks=duree_semaines) # ✅ CORRECTION : On définit explicitement la date de versement pour qu'elle soit enregistrée dates_versements = [date_fin] # Calculs montant_total = montant * (1 + (taux_hebdo / 100) * duree_semaines) cout_credit = montant_total - montant montant_versement = montant_total nb_versements = 1 # Affichage résultat simulation immédiate st.markdown("### Simulation") res1, res2, res3 = st.columns(3) res1.metric("Versement unique", f"{int(montant_versement):,} XOF".replace(",", " ")) res2.metric("Coût du crédit", f"{int(cout_credit):,} XOF".replace(",", " ")) res3.metric("Montant Total", f"{int(montant_total):,} XOF".replace(",", " "), delta=f"+{int(cout_credit):,}") # ----------------------------------------------------------- # 2. LOGIQUE MENSUEL - INTÉRÊTS (Remboursement capital à la fin) # ----------------------------------------------------------- elif type_code == "MENSUEL_INTERETS": duree_mois = col3.number_input("Durée (en mois)", min_value=1, max_value=60, value=12) date_fin = date_debut + timedelta(days=duree_mois * 30) # Approximation standard mensuelle # Conversion et Calculs duree_semaines = duree_mois * 4.33 # Standard bancaire taux_mensuel = (taux_hebdo / 100) * 4.33 interet_mensuel = montant * taux_mensuel montant_versement = interet_mensuel # Ce que le client paie chaque mois montant_final_mois = montant + interet_mensuel # Dernier mois montant_total = (interet_mensuel * duree_mois) + montant cout_credit = montant_total - montant nb_versements = int(duree_mois) # Affichage résultat simulation st.markdown("### Simulation") res1, res2 = st.columns(2) res1.metric("Intérêts mensuels", f"{int(interet_mensuel):,} XOF".replace(",", " ")) res2.metric("Dernier versement", f"{int(montant_final_mois):,} XOF".replace(",", " ")) res3, res4 = st.columns(2) res3.metric("Coût du crédit", f"{int(cout_credit):,} XOF".replace(",", " ")) res4.metric("Montant Total", f"{int(montant_total):,} XOF".replace(",", " "), delta=f"+{int(cout_credit):,}") # ----------------------------------------------------------- # 3. LOGIQUE MENSUEL - CONSTANT (Amortissement classique) # ----------------------------------------------------------- elif type_code == "MENSUEL_CONSTANT": duree_mois = col3.number_input("Durée (en mois)", min_value=1, max_value=60, value=12) date_fin = date_debut + timedelta(days=duree_mois * 30) # Conversion et Calculs duree_semaines = duree_mois * 4.33 taux_mensuel = (taux_hebdo / 100) * 4.33 if taux_mensuel > 0: # Formule mathématique des mensualités constantes mensualite = (montant * taux_mensuel) / (1 - (1 + taux_mensuel)**(-duree_mois)) else: mensualite = montant / duree_mois montant_versement = mensualite montant_total = mensualite * duree_mois cout_credit = montant_total - montant nb_versements = int(duree_mois) # Affichage résultat simulation st.markdown("### Simulation") res1, res2, res3 = st.columns(3) res1.metric("Mensualité constante", f"{int(mensualite):,} XOF".replace(",", " ")) res2.metric("Coût du crédit", f"{int(cout_credit):,} XOF".replace(",", " ")) res3.metric("Montant Total", f"{int(montant_total):,} XOF".replace(",", " "), delta=f"+{int(cout_credit):,}") # ----------------------------------------------------------- # 4. LOGIQUE HEBDOMADAIRE # ----------------------------------------------------------- elif type_code == "HEBDOMADAIRE": duree_semaines = col3.number_input("Durée (en semaines)", min_value=1, max_value=104, value=12) date_fin = date_debut + timedelta(weeks=duree_semaines) # Calculs taux_hebdo_decimal = taux_hebdo / 100 if taux_hebdo_decimal > 0: hebdomadalite = (montant * taux_hebdo_decimal) / (1 - (1 + taux_hebdo_decimal)**(-duree_semaines)) else: hebdomadalite = montant / duree_semaines montant_versement = hebdomadalite montant_total = hebdomadalite * duree_semaines cout_credit = montant_total - montant nb_versements = int(duree_semaines) # Affichage résultat simulation st.markdown("### Simulation") res1, res2, res3 = st.columns(3) res1.metric("Versement Hebdo", f"{int(hebdomadalite):,} XOF".replace(",", " ")) res2.metric("Coût du crédit", f"{int(cout_credit):,} XOF".replace(",", " ")) res3.metric("Montant Total", f"{int(montant_total):,} XOF".replace(",", " "), delta=f"+{int(cout_credit):,}") # 5. LOGIQUE PERSONNALISÉE (Dates manuelles) # ----------------------------------------------------------- else: # PERSONNALISE st.info("Configurez les dates de versement ci-dessous") # ✅ CORRECTION : Initialisation AVANT utilisation if 'dates_perso' not in st.session_state: st.session_state.dates_perso = [date.today() + timedelta(weeks=2)] # Interface d'ajout de dates st.markdown("**Dates de versement :**") col_add, col_reset = st.columns([1, 4]) if col_add.button("➕ Ajouter"): last_date = st.session_state.dates_perso[-1] st.session_state.dates_perso.append(last_date + timedelta(weeks=1)) st.rerun() # Affichage des date pickers dates_versements = [] for idx, dt in enumerate(st.session_state.dates_perso): col_d, col_x = st.columns([4, 1]) new_date = col_d.date_input(f"Echéance {idx+1}", value=dt, key=f"d_{idx}", min_value=date.today()) dates_versements.append(new_date) if col_x.button("🔻", key=f"del_{idx}") and len(st.session_state.dates_perso) > 1: st.session_state.dates_perso.pop(idx) st.rerun() st.session_state.dates_perso = dates_versements # Mise à jour state # Calculs basés sur les dates if dates_versements: dates_versements.sort() date_fin = dates_versements[-1] # ✅ La dernière date triée delta_days = (date_fin - date_debut).days duree_semaines = max(1, delta_days // 7) montant_total = montant * (1 + (taux_hebdo / 100) * duree_semaines) cout_credit = montant_total - montant nb_versements = len(dates_versements) # ✅ Correction : utiliser dates_versements au lieu de dates_sorted montant_versement = montant_total / nb_versements # Affichage résultat simulation st.markdown("### Simulation") res1, res2 = st.columns(2) res1.metric("Moyenne/Versement", f"{int(montant_versement):,} XOF".replace(",", " ")) res2.metric("Durée estimée", f"{duree_semaines} sem") res3, res4 = st.columns(2) res3.metric("Coût du crédit", f"{int(cout_credit):,} XOF".replace(",", " ")) res4.metric("Montant Total", f"{int(montant_total):,} XOF".replace(",", " "), delta=f"+{int(cout_credit):,}") # ==================================================================== # FIN DU BLOC LOGIQUE # ==================================================================== # 5. APPEL CERVEAU ANALYTIQUE (AUTO-TRIGGER) # ✅ MODIFIÉ : passage de pers_charge pour activer l'IPF si nécessaire analyse = analyser_capacite( type_code, montant, taux_hebdo, duree_semaines, montant_versement, nb_versements, client_info['Revenus_Mensuels'], client_info.get('Charges_Estimees', 0), montant_total, pers_charge=pers_charge ) # AFFICHAGE ANALYSE st.markdown(f"### Analyse : {analyse['statut']}", unsafe_allow_html=True) st.info(analyse['message']) with st.expander("Détails financiers"): st.markdown(analyse['details']) # 6. TABLEAU AMORTISSEMENT date_debut = date.today() # On génère le tableau normalement # ✅ CORRECTION : Passage de dates_versements pour le type PERSONNALISÉ if type_code == "PERSONNALISE": # Pour le type personnalisé, on passe explicitement les dates df_amort = generer_tableau_amortissement( type_code, montant, taux_hebdo, duree_semaines, montant_versement, nb_versements, date_debut, dates_versements=dates_versements # ✅ AJOUT CRITIQUE ) else: # Pour les autres types, pas besoin de dates personnalisées df_amort = generer_tableau_amortissement( type_code, montant, taux_hebdo, duree_semaines, montant_versement, nb_versements, date_debut ) # INSERTION DE LA COLONNE TYPE : Juste ici avant l'affichage if not df_amort.empty: df_amort.insert(0, "Type", type_pret) st.write("### Tableau d'échéances") st.dataframe(df_amort, hide_index=True, use_container_width=True) # 7. VALIDATION & DOCUMENTS with st.form("valid_pret"): submit = st.form_submit_button("OCTROYER & GÉNÉRER DOCS") # ============================================================================ # CALCUL DU TAUX D'ENDETTEMENT AVANT VALIDATION # ✅ MODIFIÉ : passage de pers_charge pour activer l'IPF si nécessaire # ============================================================================ from Analytics.AnalyseFinance import calculer_taux_endettement taux_endettement = calculer_taux_endettement( type_code, montant_versement, nb_versements, duree_semaines, client_info['Revenus_Mensuels'], pers_charge=pers_charge ) # Affichage pour information (optionnel) st.info(f"📊 **Taux d'endettement calculé** : {taux_endettement:.2f}%") if submit: # 7a. SAUVEGARDE GOOGLE SHEETS ws_prets = sh.worksheet("Prets_Master") # Calcul de l'ID sous forme PRT-2026-xxxx current_year = datetime.now().year # Récupère tous les ID existants all_rows = ws_prets.get_all_values() if len(all_rows) <= 1: next_number = 1 else: # Extrait tous les numéros existants pour l'année en cours existing_numbers = [] for row in all_rows[1:]: # Ignore l'en-tête if row[0].startswith(f"PRT-{current_year}-"): try: num = int(row[0].split('-')[-1]) existing_numbers.append(num) except: continue # Prend le max + 1, ou 1 si aucun prêt cette année next_number = max(existing_numbers) + 1 if existing_numbers else 1 new_id = f"PRT-{current_year}-{next_number:04d}" # ORDRE STRICT DEMANDÉ POUR Prets_Master row_data = [ new_id, # ID_Pret client_id, # ID_Client client_info['Nom_Complet'], # Nom_Complet type_code, # Type_Pret motif, # Motif montant, # Montant_Capital taux_hebdo, # Taux_Hebdo round(taux_endettement, 2), # Taux_endettement duree_semaines, # Duree_Semaines round(montant_versement), # Montant_Versement round(montant_total), # Montant_Total round(cout_credit), # Cout_Credit nb_versements, # Nb_Versements ";".join([d.strftime("%d/%m/%Y") for d in dates_versements]) if dates_versements else "", # Dates_Versements date_debut.strftime("%d/%m/%Y"), # Date_Deblocage date_fin.strftime("%d/%m/%Y"), # Date_Fin (Maintenant garantie) moyen_transfert, # Moyen_Transfert "ACTIF", # Statut garant_id, # ID_Garant datetime.now().strftime("%d-%m-%Y %H:%M:%S") # Date_Creation ] ws_prets.append_row(row_data) time.sleep(1) # ✅ SAUVEGARDE DANS SESSION STATE POUR PERSISTANCE st.session_state.loan_validated = True st.session_state.loan_id = new_id st.session_state.loan_data = { "ID_Pret": new_id, "Montant_Capital": montant, "Montant_Total": montant_total, "Taux_Hebdo": taux_hebdo, "Taux_Endettement": taux_endettement, # ✅ AJOUT "Duree_Semaines": duree_semaines, "Motif": motif, "Date_Deblocage": date_debut.strftime("%d/%m/%Y"), "Date_Fin": date_fin.strftime("%d/%m/%Y") } st.session_state.client_data = client_info.to_dict() st.session_state.garant_data = selected_garant.to_dict() if selected_garant is not None else None st.session_state.df_amort = df_amort.copy() st.success(f" Le Prêt {new_id} enregistré avec succès !") # ✅ AFFICHAGE BOUTONS EN DEHORS DU FORMULAIRE (persistants) if st.session_state.get('loan_validated', False): st.markdown("---") st.markdown(f"### Documents du prêt **{st.session_state.loan_id}**") # Génération des PDFs (une seule fois, conservés en mémoire) loan_data = st.session_state.loan_data client_data = st.session_state.client_data garant_data = st.session_state.garant_data df_amort_saved = st.session_state.df_amort # Affichage des boutons en colonnes col_pdf1, col_pdf2, col_pdf3, col_reset = st.columns(4) # PDF 1 : CONTRAT with col_pdf1: pdf_contrat = generer_contrat_pret(loan_data, client_data, df_amort_saved) st.download_button( "Contrat de Prêt", pdf_contrat, f"Contrat_{st.session_state.loan_id}.pdf", "application/pdf", use_container_width=True ) # PDF 2 : RECONNAISSANCE with col_pdf2: pdf_dette = generer_reconnaissance_dette(loan_data, client_data) st.download_button( "Reconnaissance de Dette", pdf_dette, f"Dette_{st.session_state.loan_id}.pdf", "application/pdf", use_container_width=True ) # PDF 3 : CAUTION (SI GARANT) with col_pdf3: if garant_data is not None: pdf_caution = generer_contrat_caution(loan_data, garant_data) st.download_button( "Contrat de Caution", pdf_caution, f"Caution_{st.session_state.loan_id}.pdf", "application/pdf", use_container_width=True ) else: st.info("Pas de garant") # Bouton pour réinitialiser et créer un nouveau prêt with col_reset: if st.button("Nouveau Prêt", use_container_width=True, type="primary"): # Nettoyage du session state st.session_state.loan_validated = False st.session_state.pop('loan_id', None) st.session_state.pop('loan_data', None) st.session_state.pop('client_data', None) st.session_state.pop('garant_data', None) st.session_state.pop('df_amort', None) st.rerun() st.markdown('
', unsafe_allow_html=True)