# -*- 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