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)