# -*- coding: utf-8 -*-
import streamlit as st
import pandas as pd
from datetime import datetime, date, timedelta
import time
# IMPORT DES MODULES EXISTANTS
from Analytics.AnalyseFinance import (
analyser_capacite,
generer_tableau_amortissement,
calculer_taux_endettement,
clean_taux_value,
clean_currency_value,
clean_percentage_value # ✅ AJOUT de la nouvelle fonction
)
from DocumentGen.AutoPDFGeneration import generer_contrat_pret, generer_reconnaissance_dette, generer_contrat_caution
# ============================================================================
# GESTION DU CACHE (réutilisé depuis loans_engine.py)
# ============================================================================
@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()
# ============================================================================
# ✅ CORRECTION 1 : FONCTIONS UTILITAIRES AVEC GESTION VIRGULE/POINT
# ============================================================================
def clean_currency_value(val):
"""Convertit une valeur monétaire (string ou float) en float propre"""
try:
if isinstance(val, (int, float)):
return float(val)
# Nettoyage des strings: "100 000 XOF" → 100000.0
cleaned = str(val).upper().replace("XOF", "").replace("FCFA", "").replace(" ", "")
# Ne touche PAS aux virgules pour les montants (on garde juste les chiffres)
cleaned = cleaned.replace(",", "").replace(".", "")
return float(cleaned) if cleaned else 0.0
except (ValueError, AttributeError):
return 0.0
def clean_percentage_value_local(val):
"""
✅ CORRECTION PROBLÈME 1 : Convertit un pourcentage avec virgule en float
Exemple : "54,94" → 54.94 (et non 5494.0)
"""
try:
if isinstance(val, (int, float)):
return float(val)
# Convertir en string et nettoyer
cleaned = str(val).strip().replace(" ", "").replace("%", "")
# ✅ CRUCIAL : Remplacer la virgule par un point (format Python)
cleaned = cleaned.replace(",", ".")
return float(cleaned) if cleaned else 0.0
except (ValueError, AttributeError):
return 0.0
# ============================================================================
# STYLES CSS SPÉCIFIQUES AU MODULE CHECK_UP_LOANS
# ============================================================================
def apply_checkup_loans_styles():
st.markdown("""
""", unsafe_allow_html=True)
# ============================================================================
# FONCTION PRINCIPALE DU MODULE
# ============================================================================
def show_check_up_loans(client, sheet_name):
"""
Module de mise à jour (Check-Up) des prêts existants.
Workflow:
1. Sélection du CLIENT depuis Clients_KYC
2. Identification des prêts ACTIF du client
3. Vérification des remboursements (blocage si existants)
4. Formulaire de modification
5. Mise à jour : Statut UPDATED dans Prets_Master + Création dans Prets_Update
6. Génération des nouveaux documents
"""
apply_checkup_loans_styles()
st.markdown('
', unsafe_allow_html=True)
st.header("MISE À JOUR DE PRÊT")
st.markdown("*Modification des conditions d'un prêt avant tout remboursement*")
st.markdown('
', unsafe_allow_html=True)
# ============================================================================
# ÉTAPE 1 : CHARGEMENT DES DONNÉES
# ============================================================================
try:
sh = client.open(sheet_name)
# Chargement des feuilles nécessaires
df_clients = get_cached_data(client, sheet_name, "Clients_KYC")
df_prets = get_cached_data(client, sheet_name, "Prets_Master")
df_remboursements = get_cached_data(client, sheet_name, "Remboursements")
# Chargement optionnel des garants
try:
df_garants = get_cached_data(client, sheet_name, "Garants_KYC")
except Exception as e:
st.warning(f"⚠️ Feuille Garants_KYC non accessible : {e}")
df_garants = pd.DataFrame()
except Exception as e:
st.error(f"❌ Erreur de connexion à Google Sheets : {e}")
st.markdown('
', unsafe_allow_html=True)
return
# Vérification données clients
if df_clients.empty:
st.error("🛑 Aucun client trouvé dans la base KYC.")
st.info(" Veuillez d'abord enregistrer des clients avant de pouvoir modifier des prêts.")
st.markdown('', unsafe_allow_html=True)
return
# Vérification données prêts
if df_prets.empty:
st.warning("Aucun prêt n'a encore été octroyé.")
st.info("Utilisez le module 'Moteur Financier : Octroi de Prêt' pour créer un premier prêt.")
st.markdown('', unsafe_allow_html=True)
return
# ============================================================================
# ÉTAPE 2 : SÉLECTION DU CLIENT
# ============================================================================
st.subheader(" Étape 1 : Sélectionner le client")
# Préparation de la liste de sélection
df_clients['search_label'] = df_clients['ID_Client'].astype(str) + " - " + df_clients['Nom_Complet'].astype(str)
selected_client_label = st.selectbox(
"Rechercher un client",
[""] + df_clients['search_label'].tolist(),
help="Sélectionnez le client dont vous souhaitez modifier un prêt"
)
if not selected_client_label:
st.info("Sélectionnez un client pour commencer")
st.markdown('', unsafe_allow_html=True)
return
# Récupération des informations du client
client_info = df_clients[df_clients['search_label'] == selected_client_label].iloc[0]
client_id = client_info['ID_Client']
st.success(f" Client sélectionné : **{client_info['Nom_Complet']}** (ID: {client_id})")
st.markdown('', unsafe_allow_html=True)
# ============================================================================
# ÉTAPE 3 : IDENTIFICATION DES PRÊTS ACTIFS DU CLIENT
# ============================================================================
st.subheader("Étape 2 : Identifier le prêt à modifier")
# Filtrage des prêts ACTIF du client sélectionné
df_prets_client = df_prets[
(df_prets['ID_Client'] == client_id) &
(df_prets['Statut'] == 'ACTIF')
]
# Cas A : Aucun prêt actif
if df_prets_client.empty:
st.markdown('', unsafe_allow_html=True)
st.markdown("### ⚠️ Aucun prêt actif trouvé")
st.markdown(f"""
Le client **{client_info['Nom_Complet']}** n'a actuellement aucun prêt avec le statut **ACTIF**.
**Raisons possibles :**
- Tous les prêts ont été remboursés (statut : SOLDE)
- Des prêts ont déjà été modifiés (statut : UPDATED)
- Aucun prêt n'a encore été octroyé à ce client
**Solution** : Utilisez le module 'Moteur Financier' pour octroyer un nouveau prêt.
""")
st.markdown('
', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
return
# Cas B : 1 seul prêt actif → Sélection automatique
if len(df_prets_client) == 1:
selected_loan = df_prets_client.iloc[0]
st.info(f"**1 prêt actif détecté** - Sélection automatique : **{selected_loan['ID_Pret']}**")
# Cas C : Plusieurs prêts actifs → Choix utilisateur
else:
st.warning(f"⚠️ **{len(df_prets_client)} prêts actifs** détectés pour ce client")
# Préparation de la liste de sélection des prêts
df_prets_client['loan_label'] = df_prets_client.apply(
lambda row: f"{row['ID_Pret']} | {int(clean_currency_value(row['Montant_Capital'])):,} XOF | Échéance: {row['Date_Fin']}".replace(",", " "),
axis=1
)
selected_loan_label = st.selectbox(
"Sélectionnez le prêt à modifier",
df_prets_client['loan_label'].tolist(),
help="Choisissez le prêt que vous souhaitez mettre à jour"
)
# Récupération du prêt sélectionné
selected_loan = df_prets_client[df_prets_client['loan_label'] == selected_loan_label].iloc[0]
# ============================================================================
# ÉTAPE 4 : VÉRIFICATION BLOQUANTE - REMBOURSEMENTS
# ============================================================================
loan_id = selected_loan['ID_Pret']
# Check si le prêt a reçu au moins 1 remboursement
if not df_remboursements.empty:
has_payments = not df_remboursements[df_remboursements['ID_Pret'] == loan_id].empty
if has_payments:
# BLOCAGE TOTAL
df_payments = df_remboursements[df_remboursements['ID_Pret'] == loan_id]
st.markdown('', unsafe_allow_html=True)
st.markdown("### ⛔ MODIFICATION IMPOSSIBLE")
st.markdown(f"""
Le prêt **{loan_id}** a déjà fait l'objet de **{len(df_payments)} remboursement(s)**.
**Remboursements détectés :**
""")
# Affichage des remboursements
for idx, payment in df_payments.head(5).iterrows():
montant = int(clean_currency_value(payment.get('Montant_Verse', 0)))
date_paiement = payment.get('Date_Paiement', 'N/A')
st.markdown(f"- **{date_paiement}** : {montant:,} XOF".replace(",", " "))
if len(df_payments) > 5:
st.markdown(f"- *... et {len(df_payments) - 5} autre(s) paiement(s)*")
st.markdown("""
---
**Règle de sécurité** : La modification d'un prêt n'est autorisée que **AVANT le premier remboursement**.
**Solutions alternatives :**
1. Créer un nouveau prêt avec les nouvelles conditions
2. Négocier un rééchelonnement (module à venir)
3. Contacter le service juridique pour un avenant contractuel
""")
st.markdown('
', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
return
# Aucun remboursement → Autorisation de modifier
st.success(f"Aucun remboursement détecté - **Modification autorisée** pour le prêt {loan_id}")
st.markdown('', unsafe_allow_html=True)
# ============================================================================
# ÉTAPE 5 : AFFICHAGE DES INFORMATIONS ACTUELLES
# ============================================================================
st.subheader("Étape 3 : Informations actuelles du prêt")
st.markdown('', unsafe_allow_html=True)
st.markdown(f"### Prêt {loan_id}")
# ✅ CORRECTION PROBLÈME 1 : Utiliser clean_percentage_value_local au lieu de clean_percentage_value
montant_capital_actuel = clean_currency_value(selected_loan['Montant_Capital'])
taux_hebdo_actuel = clean_taux_value(selected_loan['Taux_Hebdo'])
# ✅ CORRECTION TAUX ENDETTEMENT - Utilise clean_percentage_value corrigé
taux_endettement_actuel = clean_percentage_value(selected_loan.get('Taux_Endettement', 0))
duree_semaines_actuel = clean_currency_value(selected_loan['Duree_Semaines'])
montant_total_actuel = clean_currency_value(selected_loan['Montant_Total'])
cout_credit_actuel = clean_currency_value(selected_loan['Cout_Credit'])
# Affichage en colonnes
col1, col2, col3 = st.columns(3)
with col1:
st.metric("Montant Capital", f"{int(montant_capital_actuel):,} XOF".replace(",", " "))
st.metric("Type de prêt", selected_loan['Type_Pret'])
st.metric("Date déblocage", selected_loan['Date_Deblocage'])
with col2:
st.metric("Taux hebdomadaire", f"{taux_hebdo_actuel}%")
st.metric("Taux d'endettement", f"{taux_endettement_actuel:.2f}%") # ✅ Maintenant affiche 54.94%
st.metric("Durée", f"{int(duree_semaines_actuel)} semaines")
st.metric("Moyen de transfert", selected_loan.get('Moyen_Transfert', 'N/A'))
with col3:
st.metric("Montant Total", f"{int(montant_total_actuel):,} XOF".replace(",", " "))
st.metric("Coût du crédit", f"{int(cout_credit_actuel):,} XOF".replace(",", " "))
st.metric("Date de fin", selected_loan['Date_Fin'])
# Informations supplémentaires
st.markdown("---")
st.markdown(f"**Client :** {selected_loan['Nom_Complet']}")
st.markdown(f"**Motif du prêt :** {selected_loan.get('Motif', 'Non renseigné')}")
# Garant si existant
garant_id_actuel = selected_loan.get('ID_Garant', '')
if garant_id_actuel and garant_id_actuel != '' and not df_garants.empty:
garant_info = df_garants[df_garants['ID_Garant'] == garant_id_actuel]
if not garant_info.empty:
st.markdown(f"**Garant :** {garant_info.iloc[0]['Nom_Complet']} (ID: {garant_id_actuel})")
st.markdown(f"Statut : {selected_loan['Statut']}", unsafe_allow_html=True)
st.markdown('
', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
# ============================================================================
# ÉTAPE 6 : FORMULAIRE DE MODIFICATION
# ============================================================================
st.subheader(" Étape 4 : Nouvelles conditions du prêt")
st.info(" Modifiez les paramètres ci-dessous. Les montants seront recalculés automatiquement.")
# Initialisation des variables de session pour éviter les pertes de données
if 'dates_perso_update' not in st.session_state:
# Récupération des dates existantes si type PERSONNALISE
dates_str = selected_loan.get('Dates_Versements', '')
if dates_str and dates_str != '':
try:
dates_list = [datetime.strptime(d.strip(), "%d/%m/%Y").date() for d in dates_str.split(';')]
st.session_state.dates_perso_update = dates_list
except:
st.session_state.dates_perso_update = [date.today() + timedelta(weeks=2)]
else:
st.session_state.dates_perso_update = [date.today() + timedelta(weeks=2)]
# Configuration en colonnes
col_motif, col_type, col_moyen = st.columns(3)
with col_motif:
nouveau_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"
],
index=0 if selected_loan.get('Motif', '') not in [
"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"
] else [
"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"
].index(selected_loan.get('Motif', 'Autre'))
)
with col_type:
type_options = ["In Fine", "Mensuel - Intérêts", "Mensuel - Constant", "Hebdomadaire", "Personnalisé"]
type_code_map = {
"In Fine": "IN_FINE",
"Mensuel - Intérêts": "MENSUEL_INTERETS",
"Mensuel - Constant": "MENSUEL_CONSTANT",
"Hebdomadaire": "HEBDOMADAIRE",
"Personnalisé": "PERSONNALISE"
}
type_reverse_map = {v: k for k, v in type_code_map.items()}
current_type_display = type_reverse_map.get(selected_loan['Type_Pret'], "In Fine")
nouveau_type = st.selectbox(
"Type de remboursement",
type_options,
index=type_options.index(current_type_display)
)
nouveau_type_code = type_code_map[nouveau_type]
with col_moyen:
moyen_actuel = selected_loan.get('Moyen_Transfert', 'Wave')
nouveau_moyen = st.selectbox(
"Moyen de transfert",
["Wave", "Orange Money", "Virement"],
index=["Wave", "Orange Money", "Virement"].index(moyen_actuel) if moyen_actuel in ["Wave", "Orange Money", "Virement"] else 0
)
# Paramètres financiers
col1, col2, col3 = st.columns(3)
nouveau_montant = col1.number_input(
"Montant Capital (XOF)",
min_value=10000,
value=int(montant_capital_actuel),
step=10000,
help="⚠️ Modification autorisée du montant capital"
)
nouveau_taux = col2.number_input(
"Taux Hebdo (%)",
min_value=0.1,
value=float(taux_hebdo_actuel),
step=0.1
)
# La durée sera définie spécifiquement selon le type de prêt
# (voir bloc de calcul ci-dessous)
# ============================================================================
# CALCULS AUTOMATIQUES SELON LE TYPE DE PRÊT
# ============================================================================
# Initialisation des variables
nouveau_montant_versement = 0
nouveau_montant_total = 0
nouveau_cout_credit = 0
nouveau_nb_versements = 0
nouvelle_duree_semaines = 0
nouvelles_dates_versements = []
# ✅ CORRECTION : Utiliser Date_Deblocage au lieu de date.today()
try:
nouvelle_date_debut = datetime.strptime(selected_loan['Date_Deblocage'], "%d/%m/%Y").date()
except:
# Fallback si le format est différent
nouvelle_date_debut = date.today()
nouvelle_date_fin = nouvelle_date_debut
# -----------------------------------------------------------
# 1. LOGIQUE IN FINE (1 seul versement à la fin)
# -----------------------------------------------------------
if nouveau_type_code == "IN_FINE":
nouvelle_duree_semaines = col3.number_input(
"Durée (en semaines)",
min_value=1,
max_value=104,
value=int(duree_semaines_actuel) if selected_loan['Type_Pret'] == 'IN_FINE' else 8
)
nouvelle_date_fin = nouvelle_date_debut + timedelta(weeks=int(nouvelle_duree_semaines))
# Calculs
nouveau_montant_total = nouveau_montant * (1 + (nouveau_taux / 100) * nouvelle_duree_semaines)
nouveau_cout_credit = nouveau_montant_total - nouveau_montant
nouveau_montant_versement = nouveau_montant_total
nouveau_nb_versements = 1
# Affichage résultat simulation
st.markdown("### Simulation des nouveaux montants")
res1, res2, res3 = st.columns(3)
res1.metric("Versement unique", f"{int(nouveau_montant_versement):,} XOF".replace(",", " "))
res2.metric("Coût du crédit", f"{int(nouveau_cout_credit):,} XOF".replace(",", " "))
res3.metric("Montant Total", f"{int(nouveau_montant_total):,} XOF".replace(",", " "),
delta=f"+{int(nouveau_cout_credit):,}")
# -----------------------------------------------------------
# 2. LOGIQUE MENSUEL - INTÉRÊTS (Remboursement capital à la fin)
# -----------------------------------------------------------
elif nouveau_type_code == "MENSUEL_INTERETS":
nouvelle_duree_mois = col3.number_input(
"Durée (en mois)",
min_value=1,
max_value=60,
value=int(duree_semaines_actuel / 4.33) if selected_loan['Type_Pret'] == 'MENSUEL_INTERETS' else 12
)
nouvelle_date_fin = nouvelle_date_debut + timedelta(days=int(nouvelle_duree_mois * 30))
# Conversion et Calculs
nouvelle_duree_semaines = nouvelle_duree_mois * 4.33
taux_mensuel = (nouveau_taux / 100) * 4.33
interet_mensuel = nouveau_montant * taux_mensuel
nouveau_montant_versement = interet_mensuel
montant_final_mois = nouveau_montant + interet_mensuel
nouveau_montant_total = (interet_mensuel * nouvelle_duree_mois) + nouveau_montant
nouveau_cout_credit = nouveau_montant_total - nouveau_montant
nouveau_nb_versements = int(nouvelle_duree_mois)
# Affichage résultat simulation
st.markdown("### Simulation des nouveaux montants")
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(nouveau_cout_credit):,} XOF".replace(",", " "))
res4.metric("Montant Total", f"{int(nouveau_montant_total):,} XOF".replace(",", " "),
delta=f"+{int(nouveau_cout_credit):,}")
# -----------------------------------------------------------
# 3. LOGIQUE MENSUEL - CONSTANT (Amortissement classique)
# -----------------------------------------------------------
elif nouveau_type_code == "MENSUEL_CONSTANT":
nouvelle_duree_mois = col3.number_input(
"Durée (en mois)",
min_value=1,
max_value=60,
value=int(duree_semaines_actuel / 4.33) if selected_loan['Type_Pret'] == 'MENSUEL_CONSTANT' else 12
)
nouvelle_date_fin = nouvelle_date_debut + timedelta(days=int(nouvelle_duree_mois * 30))
# Conversion et Calculs
nouvelle_duree_semaines = nouvelle_duree_mois * 4.33
taux_mensuel = (nouveau_taux / 100) * 4.33
if taux_mensuel > 0:
mensualite = (nouveau_montant * taux_mensuel) / (1 - (1 + taux_mensuel)**(-nouvelle_duree_mois))
else:
mensualite = nouveau_montant / nouvelle_duree_mois
nouveau_montant_versement = mensualite
nouveau_montant_total = mensualite * nouvelle_duree_mois
nouveau_cout_credit = nouveau_montant_total - nouveau_montant
nouveau_nb_versements = int(nouvelle_duree_mois)
# Affichage résultat simulation
st.markdown("### Simulation des nouveaux montants")
res1, res2, res3 = st.columns(3)
res1.metric("Mensualité constante", f"{int(mensualite):,} XOF".replace(",", " "))
res2.metric("Coût du crédit", f"{int(nouveau_cout_credit):,} XOF".replace(",", " "))
res3.metric("Montant Total", f"{int(nouveau_montant_total):,} XOF".replace(",", " "),
delta=f"+{int(nouveau_cout_credit):,}")
# -----------------------------------------------------------
# 4. LOGIQUE HEBDOMADAIRE
# -----------------------------------------------------------
elif nouveau_type_code == "HEBDOMADAIRE":
nouvelle_duree_semaines = col3.number_input(
"Durée (en semaines)",
min_value=1,
max_value=104,
value=int(duree_semaines_actuel) if selected_loan['Type_Pret'] == 'HEBDOMADAIRE' else 12
)
nouvelle_date_fin = nouvelle_date_debut + timedelta(weeks=int(nouvelle_duree_semaines))
# Calculs
taux_hebdo_decimal = nouveau_taux / 100
if taux_hebdo_decimal > 0:
hebdomadalite = (nouveau_montant * taux_hebdo_decimal) / (1 - (1 + taux_hebdo_decimal)**(-nouvelle_duree_semaines))
else:
hebdomadalite = nouveau_montant / nouvelle_duree_semaines
nouveau_montant_versement = hebdomadalite
nouveau_montant_total = hebdomadalite * nouvelle_duree_semaines
nouveau_cout_credit = nouveau_montant_total - nouveau_montant
nouveau_nb_versements = int(nouvelle_duree_semaines)
# Affichage résultat simulation
st.markdown("### Simulation des nouveaux montants")
res1, res2, res3 = st.columns(3)
res1.metric("Versement Hebdo", f"{int(hebdomadalite):,} XOF".replace(",", " "))
res2.metric("Coût du crédit", f"{int(nouveau_cout_credit):,} XOF".replace(",", " "))
res3.metric("Montant Total", f"{int(nouveau_montant_total):,} XOF".replace(",", " "),
delta=f"+{int(nouveau_cout_credit):,}")
# -----------------------------------------------------------
# 5. LOGIQUE PERSONNALISÉE (Dates manuelles)
# -----------------------------------------------------------
else: # PERSONNALISE
st.info(" Configurez les dates de versement ci-dessous")
# Interface d'ajout de dates
st.markdown("**Dates de versement :**")
col_add, col_reset = st.columns([1, 4])
if col_add.button("Ajouter", key="add_date_update"):
last_date = st.session_state.dates_perso_update[-1]
st.session_state.dates_perso_update.append(last_date + timedelta(weeks=1))
st.rerun()
# ✅ CORRECTION PROBLÈME 2 : Ajuster les dates passées
nouvelles_dates_versements = []
for idx, dt in enumerate(st.session_state.dates_perso_update):
col_d, col_x = st.columns([4, 1])
# ✅ CORRECTION : S'assurer que la date par défaut n'est jamais avant aujourd'hui
today = date.today()
safe_default_date = max(dt, today) # Prend la plus récente entre la date stockée et aujourd'hui
# Afficher un warning si la date a été ajustée
if dt < today and idx == 0:
st.warning(f"⚠️ La date originale ({dt.strftime('%d/%m/%Y')}) est passée. Ajustée à aujourd'hui ({today.strftime('%d/%m/%Y')}).")
new_date = col_d.date_input(
f"Echéance {idx+1}",
value=safe_default_date, # ✅ CORRECTION : Utiliser safe_default_date
key=f"d_update_{idx}",
min_value=today # min_value = aujourd'hui
)
nouvelles_dates_versements.append(new_date)
if col_x.button("♦️", key=f"del_update_{idx}") and len(st.session_state.dates_perso_update) > 1:
st.session_state.dates_perso_update.pop(idx)
st.rerun()
st.session_state.dates_perso_update = nouvelles_dates_versements
# Calculs basés sur les dates
if nouvelles_dates_versements:
nouvelles_dates_versements.sort()
nouvelle_date_fin = nouvelles_dates_versements[-1]
delta_days = (nouvelle_date_fin - nouvelle_date_debut).days
nouvelle_duree_semaines = max(1, delta_days / 7)
nouveau_montant_total = nouveau_montant * (1 + (nouveau_taux / 100) * nouvelle_duree_semaines)
nouveau_cout_credit = nouveau_montant_total - nouveau_montant
nouveau_nb_versements = len(nouvelles_dates_versements)
nouveau_montant_versement = nouveau_montant_total / nouveau_nb_versements
# Affichage résultat simulation
st.markdown("### Simulation des nouveaux montants")
res1, res2 = st.columns(2)
res1.metric("Moyenne/Versement", f"{int(nouveau_montant_versement):,} XOF".replace(",", " "))
res2.metric("Durée estimée", f"{int(nouvelle_duree_semaines)} sem")
res3, res4 = st.columns(2)
res3.metric("Coût du crédit", f"{int(nouveau_cout_credit):,} XOF".replace(",", " "))
res4.metric("Montant Total", f"{int(nouveau_montant_total):,} XOF".replace(",", " "),
delta=f"+{int(nouveau_cout_credit):,}")
# ============================================================================
# CALCUL DU TAUX D'ENDETTEMENT POUR LE NOUVEAU PRÊT
# ============================================================================
from Analytics.AnalyseFinance import calculer_taux_endettement
# Calcul du nouveau taux
nouveau_taux_endettement = calculer_taux_endettement(
nouveau_type_code,
nouveau_montant_versement,
nouveau_nb_versements,
nouvelle_duree_semaines,
client_info['Revenus_Mensuels']
)
# Affichage pour information
if nouveau_taux_endettement > 0:
st.info(f" **Nouveau taux d'endettement calculé** : {nouveau_taux_endettement:.2f}%")
# Warning si le taux dépasse 33%
if nouveau_taux_endettement > 33:
st.warning(f"⚠️ Le taux d'endettement ({nouveau_taux_endettement:.2f}%) dépasse le seuil recommandé de 33%")
# ============================================================================
# COMPARAISON AVANT / APRÈS
# ============================================================================
st.markdown('', unsafe_allow_html=True)
st.subheader(" Comparaison Avant / Après")
st.markdown('', unsafe_allow_html=True)
# Calcul des différences
diff_montant_total = nouveau_montant_total - montant_total_actuel
diff_cout_credit = nouveau_cout_credit - cout_credit_actuel
diff_duree = nouvelle_duree_semaines - duree_semaines_actuel
# Tableau comparatif
comparison_data = {
"Paramètre": [
"Type de prêt",
"Montant Capital",
"Taux hebdomadaire",
"Taux endettement",
"Durée",
"Montant Total",
"Coût du crédit",
"Date de fin"
],
"AVANT": [
selected_loan['Type_Pret'],
f"{int(montant_capital_actuel):,} XOF".replace(",", " "),
f"{taux_hebdo_actuel}%",
f"{taux_endettement_actuel:.2f}%" if taux_endettement_actuel > 0 else "N/A", # ✅ Maintenant correct
f"{int(duree_semaines_actuel)} semaines",
f"{int(montant_total_actuel):,} XOF".replace(",", " "),
f"{int(cout_credit_actuel):,} XOF".replace(",", " "),
selected_loan['Date_Fin']
],
"APRÈS": [
nouveau_type_code,
f"{int(nouveau_montant):,} XOF".replace(",", " "),
f"{nouveau_taux}%",
f"{nouveau_taux_endettement:.2f}%",
f"{int(nouvelle_duree_semaines)} semaines",
f"{int(nouveau_montant_total):,} XOF".replace(",", " "),
f"{int(nouveau_cout_credit):,} XOF".replace(",", " "),
nouvelle_date_fin.strftime("%d/%m/%Y")
],
"DIFFÉRENCE": [
"Modifié" if nouveau_type_code != selected_loan['Type_Pret'] else "=",
f"{int(nouveau_montant - montant_capital_actuel):+,} XOF".replace(",", " ") if nouveau_montant != montant_capital_actuel else "=",
f"{nouveau_taux - taux_hebdo_actuel:+.1f}%" if nouveau_taux != taux_hebdo_actuel else "=",
f"{nouveau_taux_endettement - taux_endettement_actuel:+.1f}%" if taux_endettement_actuel > 0 else f"{nouveau_taux_endettement:.1f}%", # ✅ Maintenant correct
f"{int(diff_duree):+} sem" if diff_duree != 0 else "=",
f"{int(diff_montant_total):+,} XOF".replace(",", " ") if diff_montant_total != 0 else "=",
f"{int(diff_cout_credit):+,} XOF".replace(",", " ") if diff_cout_credit != 0 else "=",
f"{int((nouvelle_date_fin - datetime.strptime(selected_loan['Date_Fin'], '%d/%m/%Y').date()).days):+} jours" if nouvelle_date_fin != datetime.strptime(selected_loan['Date_Fin'], '%d/%m/%Y').date() else "="
]
}
df_comparison = pd.DataFrame(comparison_data)
st.dataframe(df_comparison, hide_index=True, use_container_width=True)
st.markdown('
', unsafe_allow_html=True)
# ============================================================================
# WARNINGS / ALERTES
# ============================================================================
warnings = []
# Warning 1 : Augmentation significative du coût
if diff_cout_credit > (cout_credit_actuel * 0.15):
warnings.append(f"⚠️ Le coût du crédit augmente de **{int(diff_cout_credit):,} XOF** (+{(diff_cout_credit/cout_credit_actuel*100):.1f}%)".replace(",", " "))
# Warning 2 : Prolongation importante
if diff_duree > 8:
warnings.append(f"⚠️ La durée du prêt est prolongée de **{int(diff_duree)} semaines**")
# Warning 3 : Date de fin très éloignée
if (nouvelle_date_fin - nouvelle_date_debut).days > 180:
warnings.append(f"⚠️ La nouvelle date de fin ({nouvelle_date_fin.strftime('%d/%m/%Y')}) dépasse **6 mois**")
# Warning 4 : Augmentation du montant capital
if nouveau_montant > montant_capital_actuel:
warnings.append(f" Le montant capital augmente de **{int(nouveau_montant - montant_capital_actuel):,} XOF**".replace(",", " "))
if warnings:
st.markdown('', unsafe_allow_html=True)
st.markdown("### ⚠️ Points d'attention")
for warning in warnings:
st.markdown(f"- {warning}")
st.markdown('
', unsafe_allow_html=True)
# ============================================================================
# ANALYSE DE SOLVABILITÉ (NOUVEAU PRÊT)
# ============================================================================
st.markdown('', unsafe_allow_html=True)
st.subheader(" Nouvelle analyse de solvabilité")
# Appel du cerveau analytique
analyse = analyser_capacite(
nouveau_type_code, nouveau_montant, nouveau_taux, nouvelle_duree_semaines,
nouveau_montant_versement, nouveau_nb_versements,
client_info['Revenus_Mensuels'], client_info.get('Charges_Estimees', 0),
nouveau_montant_total
)
st.markdown(f"### Statut : {analyse['statut']}",
unsafe_allow_html=True)
st.info(analyse['message'])
with st.expander(" Détails financiers"):
st.markdown(analyse['details'])
# ============================================================================
# GÉNÉRATION DU NOUVEAU TABLEAU D'AMORTISSEMENT
# ============================================================================
st.markdown('', unsafe_allow_html=True)
st.subheader(" Nouveau tableau d'échéances")
# Génération du tableau
if nouveau_type_code == "PERSONNALISE":
df_amort_nouveau = generer_tableau_amortissement(
nouveau_type_code, nouveau_montant, nouveau_taux, nouvelle_duree_semaines,
nouveau_montant_versement, nouveau_nb_versements, nouvelle_date_debut,
dates_versements=nouvelles_dates_versements
)
else:
df_amort_nouveau = generer_tableau_amortissement(
nouveau_type_code, nouveau_montant, nouveau_taux, nouvelle_duree_semaines,
nouveau_montant_versement, nouveau_nb_versements, nouvelle_date_debut
)
# Ajout de la colonne Type
if not df_amort_nouveau.empty:
df_amort_nouveau.insert(0, "Type", nouveau_type)
st.dataframe(df_amort_nouveau, hide_index=True, use_container_width=True)
# ============================================================================
# VALIDATION ET ENREGISTREMENT
# ============================================================================
st.markdown('', unsafe_allow_html=True)
st.subheader(" Validation de la mise à jour")
st.info("🔒 **Attention** : Cette action va créer un nouveau prêt et archiver l'ancien avec le statut UPDATED.")
# Formulaire de validation
with st.form("form_update_loan"):
st.markdown("### Confirmation")
col_confirm1, col_confirm2 = st.columns(2)
with col_confirm1:
st.markdown(f"**Prêt à mettre à jour :** {loan_id}")
st.markdown(f"**Client :** {client_info['Nom_Complet']}")
st.markdown(f"**Nouveau montant total :** {int(nouveau_montant_total):,} XOF".replace(",", " "))
with col_confirm2:
st.markdown(f"**Nouveau type :** {nouveau_type_code}")
st.markdown(f"**Nouvelle durée :** {int(nouvelle_duree_semaines)} semaines")
st.markdown(f"**Nouvelle échéance :** {nouvelle_date_fin.strftime('%d/%m/%Y')}")
# Champ optionnel : Commentaire de modification
commentaire_modification = st.text_area(
"Commentaire de modification (optionnel)",
placeholder="Ex: Report de 3 semaines à la demande du client, Augmentation du capital pour extension d'activité...",
help="Ce commentaire sera enregistré dans la feuille Prets_Update pour traçabilité"
)
submit_update = st.form_submit_button(" VALIDER LA MISE À JOUR", use_container_width=True)
if submit_update:
try:
# ========================================================================
# ÉTAPE A : MISE À JOUR DU STATUT DANS Prets_Master
# ========================================================================
with st.spinner(" Mise à jour du prêt en cours..."):
ws_prets_master = sh.worksheet("Prets_Master")
# Recherche de la ligne du prêt à modifier
all_values = ws_prets_master.get_all_values()
header = all_values[0]
# Trouver l'index de la colonne Statut et ID_Pret
try:
col_statut_idx = header.index('Statut') + 1 # +1 car gspread est en base 1
col_id_pret_idx = header.index('ID_Pret') + 1
col_date_update_idx = header.index('Date_Update') + 1 if 'Date_Update' in header else None
except ValueError as e:
st.error(f"❌ Colonne manquante dans Prets_Master : {e}")
st.stop()
# Trouver la ligne du prêt
row_index = None
for idx, row in enumerate(all_values[1:], start=2): # Start=2 car ligne 1 = header
if row[col_id_pret_idx - 1] == loan_id:
row_index = idx
break
if row_index is None:
st.error(f"❌ Prêt {loan_id} introuvable dans Prets_Master")
st.stop()
# Mise à jour du statut
ws_prets_master.update_cell(row_index, col_statut_idx, "UPDATED")
# Mise à jour de Date_Update si la colonne existe
if col_date_update_idx:
ws_prets_master.update_cell(
row_index,
col_date_update_idx,
datetime.now().strftime("%d-%m-%Y %H:%M:%S")
)
time.sleep(1) # Anti rate-limit
st.success(f" Prêt {loan_id} → Statut changé en **UPDATED**")
# ========================================================================
# ÉTAPE B : CRÉATION DU NOUVEAU PRÊT DANS Prets_Update
# ========================================================================
with st.spinner(" Création du nouveau prêt dans Prets_Update..."):
# Vérification/Création de la feuille Prets_Update
try:
ws_prets_update = sh.worksheet("Prets_Update")
except:
st.warning("⚠️ Feuille Prets_Update inexistante. Création en cours...")
# Création de la feuille avec les bonnes colonnes
ws_prets_update = sh.add_worksheet(
title="Prets_Update",
rows=1000,
cols=25
)
# ✅ CORRECTION - En-têtes avec Taux_Endettement
headers = [
"ID_Pret", "ID_Pret_Source", "Version", "Date_Modification",
"ID_Client", "Nom_Complet", "Type_Pret", "Motif",
"Montant_Capital", "Taux_Hebdo", "Taux_Endettement", "Duree_Semaines", # ✅ Taux_Endettement ajouté
"Montant_Versement", "Montant_Total", "Cout_Credit",
"Nb_Versements", "Dates_Versements", "Date_Deblocage",
"Date_Fin", "Moyen_Transfert", "Statut", "ID_Garant",
"Date_Creation", "Commentaire_Modification"
]
ws_prets_update.append_row(headers)
time.sleep(1)
# Calcul de la version
all_updates = ws_prets_update.get_all_values()
version_count = sum(1 for row in all_updates[1:] if row[1] == loan_id) # Colonne ID_Pret_Source
nouvelle_version = version_count + 2 # +2 car V1 = original
# Génération du nouvel ID
nouveau_id = f"{loan_id}-V{nouvelle_version}"
# Préparation des dates de versement
dates_versements_str = ";".join([
d.strftime("%d/%m/%Y") for d in nouvelles_dates_versements
]) if nouvelles_dates_versements else ""
# ✅ CORRECTION - Construction de la ligne de données (avec Taux_Endettement)
row_data_update = [
nouveau_id, # ID_Pret
loan_id, # ID_Pret_Source
nouvelle_version, # Version
datetime.now().strftime("%d-%m-%Y %H:%M:%S"), # Date_Modification
client_id, # ID_Client
client_info['Nom_Complet'], # Nom_Complet
nouveau_type_code, # Type_Pret
nouveau_motif, # Motif
nouveau_montant, # Montant_Capital
nouveau_taux, # Taux_Hebdo
round(nouveau_taux_endettement, 2), # ✅ Taux_Endettement (maintenant aligné avec headers)
nouvelle_duree_semaines, # Duree_Semaines
round(nouveau_montant_versement), # Montant_Versement
round(nouveau_montant_total), # Montant_Total
round(nouveau_cout_credit), # Cout_Credit
nouveau_nb_versements, # Nb_Versements
dates_versements_str, # Dates_Versements
selected_loan['Date_Deblocage'], # Date_Deblocage
nouvelle_date_fin.strftime("%d/%m/%Y"), # Date_Fin
nouveau_moyen, # Moyen_Transfert
"ACTIF", # Statut
garant_id_actuel, # ID_Garant
datetime.now().strftime("%d-%m-%Y %H:%M:%S"), # Date_Creation
commentaire_modification # Commentaire_Modification
]
# Enregistrement
ws_prets_update.append_row(row_data_update)
time.sleep(1)
st.success(f"✅ Nouveau prêt créé : **{nouveau_id}** dans Prets_Update")
# ========================================================================
# ÉTAPE C : SAUVEGARDE DANS SESSION STATE
# ========================================================================
st.session_state.loan_updated = True
st.session_state.new_loan_id = nouveau_id
st.session_state.new_loan_data = {
"ID_Pret": nouveau_id,
"ID_Pret_Source": loan_id,
"Version": nouvelle_version,
"Montant_Capital": nouveau_montant,
"Montant_Total": nouveau_montant_total,
"Taux_Hebdo": nouveau_taux,
"Duree_Semaines": nouvelle_duree_semaines,
"Montant_Versement": nouveau_montant_versement, # ✅ AJOUT
"Cout_Credit": nouveau_cout_credit, # ✅ AJOUT
"Nb_Versements": nouveau_nb_versements, # ✅ AJOUT
"Motif": nouveau_motif,
"Date_Deblocage": selected_loan['Date_Deblocage'],
"Date_Fin": nouvelle_date_fin.strftime("%d/%m/%Y"),
"Type_Pret": nouveau_type_code
}
st.session_state.new_client_data = client_info.to_dict()
# Récupération du garant si existant
if garant_id_actuel and garant_id_actuel != '' and not df_garants.empty:
garant_info = df_garants[df_garants['ID_Garant'] == garant_id_actuel]
if not garant_info.empty:
st.session_state.new_garant_data = garant_info.iloc[0].to_dict()
else:
st.session_state.new_garant_data = None
else:
st.session_state.new_garant_data = None
st.session_state.new_df_amort = df_amort_nouveau.copy()
# Nettoyage du cache pour forcer le rechargement
st.cache_data.clear()
st.success("Mise a jour effectuee avec succes !")
except Exception as e:
st.error(f"❌ Erreur lors de la mise à jour : {e}")
st.exception(e)
return # ✅ AJOUT : Arrêter l'exécution ici en cas d'erreur
# ============================================================================
# GÉNÉRATION DES DOCUMENTS PDF (SI VALIDATION EFFECTUÉE)
# ============================================================================
if st.session_state.get('loan_updated', False):
st.markdown('', unsafe_allow_html=True)
st.markdown(f"### Documents du prêt **{st.session_state.new_loan_id}**")
st.success(f" Prêt mis à jour : **{st.session_state.get('new_loan_data', {}).get('ID_Pret_Source')}** → **{st.session_state.new_loan_id}**")
# Préparation des données pour les PDFs
loan_data_pdf = st.session_state.new_loan_data
client_data_pdf = st.session_state.new_client_data
garant_data_pdf = st.session_state.new_garant_data
df_amort_pdf = st.session_state.new_df_amort
# Ajout d'une mention spéciale pour les documents
loan_data_pdf['Mention_Update'] = f"Mise à jour du contrat n° {loan_data_pdf['ID_Pret_Source']} (Version {loan_data_pdf['Version']})"
# Affichage des boutons de téléchargement
col_pdf1, col_pdf2, col_pdf3, col_reset = st.columns(4)
# PDF 1 : CONTRAT DE PRÊT
with col_pdf1:
pdf_contrat = generer_contrat_pret(loan_data_pdf, client_data_pdf, df_amort_pdf)
st.download_button(
" Contrat de Prêt",
pdf_contrat,
f"Contrat_{st.session_state.new_loan_id}.pdf",
"application/pdf",
use_container_width=True
)
# PDF 2 : RECONNAISSANCE DE DETTE
with col_pdf2:
pdf_dette = generer_reconnaissance_dette(loan_data_pdf, client_data_pdf)
st.download_button(
" Reconnaissance Dette",
pdf_dette,
f"Dette_{st.session_state.new_loan_id}.pdf",
"application/pdf",
use_container_width=True
)
# PDF 3 : CONTRAT DE CAUTION (SI GARANT)
with col_pdf3:
if garant_data_pdf is not None:
pdf_caution = generer_contrat_caution(loan_data_pdf, garant_data_pdf)
st.download_button(
" Contrat Caution",
pdf_caution,
f"Caution_{st.session_state.new_loan_id}.pdf",
"application/pdf",
use_container_width=True
)
else:
st.info("Pas de garant")
# BOUTON RESET : Nouvelle mise à jour
with col_reset:
if st.button(" Nouvelle Mise à Jour", use_container_width=True, type="primary"):
# Nettoyage du session state
st.session_state.loan_updated = False
st.session_state.pop('new_loan_id', None)
st.session_state.pop('new_loan_data', None)
st.session_state.pop('new_client_data', None)
st.session_state.pop('new_garant_data', None)
st.session_state.pop('new_df_amort', None)
st.session_state.pop('dates_perso_update', None)
st.cache_data.clear()
st.rerun()
# Affichage d'un récapitulatif
st.markdown("---")
with st.expander(" Récapitulatif de la mise à jour"):
col_recap1, col_recap2 = st.columns(2)
with col_recap1:
st.markdown("### Ancien prêt")
st.markdown(f"**ID :** {loan_data_pdf['ID_Pret_Source']}")
st.markdown(f"**Statut :** UPDATED (archivé)")
st.markdown(f"**Montant :** {int(montant_total_actuel):,} XOF".replace(",", " "))
with col_recap2:
st.markdown("### Nouveau prêt")
st.markdown(f"**ID :** {st.session_state.new_loan_id}")
st.markdown(f"**Statut :** ACTIF")
st.markdown(f"**Montant :** {int(loan_data_pdf['Montant_Total']):,} XOF".replace(",", " "))
st.markdown(f"**Différence :** {int(loan_data_pdf['Montant_Total'] - montant_total_actuel):+,} XOF".replace(",", " "))
# ✅ AJOUT CRITIQUE : Arrêter l'exécution du module ici
st.markdown('', unsafe_allow_html=True) # Fermer le wrapper
return # ✅ STOP - Ne pas continuer le reste du module