Spaces:
Running
Running
| # -*- 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) | |
| # ============================================================================ | |
| # 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(""" | |
| <style> | |
| /* ======================================== | |
| WRAPPER D'ISOLATION DU MODULE | |
| ======================================== */ | |
| #checkup-loans-module { | |
| padding: 1rem; | |
| margin: 0 auto; | |
| max-width: 100%; | |
| } | |
| /* ======================================== | |
| CARTES D'INFORMATION PRÊT | |
| ======================================== */ | |
| #checkup-loans-module .checkup-loan-card { | |
| background: rgba(22, 27, 34, 0.6); | |
| border: 1px solid rgba(48, 54, 61, 0.8); | |
| border-left: 4px solid #58a6ff; | |
| border-radius: 8px; | |
| padding: 20px; | |
| margin: 1rem 0; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); | |
| } | |
| #checkup-loans-module .checkup-loan-card h3 { | |
| color: #58a6ff !important; | |
| margin-bottom: 1rem; | |
| font-size: 1.2rem; | |
| } | |
| /* ======================================== | |
| TABLEAU COMPARATIF AVANT/APRÈS | |
| ======================================== */ | |
| #checkup-loans-module .checkup-comparison-table { | |
| background: rgba(22, 27, 34, 0.4); | |
| border: 1px solid rgba(88, 166, 255, 0.3); | |
| border-radius: 8px; | |
| padding: 16px; | |
| margin: 1.5rem 0; | |
| } | |
| #checkup-loans-module .checkup-comparison-table table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| } | |
| #checkup-loans-module .checkup-comparison-table th { | |
| background: rgba(88, 166, 255, 0.2); | |
| color: #58a6ff !important; | |
| padding: 12px; | |
| text-align: left; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| font-size: 0.85rem; | |
| } | |
| #checkup-loans-module .checkup-comparison-table td { | |
| padding: 10px 12px; | |
| border-bottom: 1px solid rgba(48, 54, 61, 0.4); | |
| } | |
| /* ======================================== | |
| ALERTES DE BLOCAGE | |
| ======================================== */ | |
| #checkup-loans-module .checkup-blocked-alert { | |
| background: rgba(231, 76, 60, 0.15); | |
| border: 2px solid rgba(231, 76, 60, 0.6); | |
| border-left: 6px solid #e74c3c; | |
| border-radius: 8px; | |
| padding: 20px; | |
| margin: 2rem 0; | |
| } | |
| #checkup-loans-module .checkup-blocked-alert h3 { | |
| color: #e74c3c !important; | |
| margin-bottom: 1rem; | |
| } | |
| /* ======================================== | |
| WARNINGS (AVERTISSEMENTS) | |
| ======================================== */ | |
| #checkup-loans-module .checkup-warning-box { | |
| background: rgba(243, 156, 18, 0.12); | |
| border: 1px solid rgba(243, 156, 18, 0.4); | |
| border-left: 4px solid #f39c12; | |
| border-radius: 6px; | |
| padding: 16px; | |
| margin: 1rem 0; | |
| } | |
| /* ======================================== | |
| BOUTONS D'ACTION | |
| ======================================== */ | |
| #checkup-loans-module .checkup-action-btn button { | |
| background: linear-gradient(135deg, rgba(88, 166, 255, 0.15) 0%, rgba(88, 166, 255, 0.08) 100%); | |
| border: 1px solid rgba(88, 166, 255, 0.6); | |
| color: #58a6ff !important; | |
| font-weight: 600; | |
| font-size: 0.9rem; | |
| letter-spacing: 0.5px; | |
| border-radius: 6px; | |
| padding: 12px 24px; | |
| transition: all 0.3s ease; | |
| text-transform: uppercase; | |
| } | |
| #checkup-loans-module .checkup-action-btn button:hover { | |
| background: linear-gradient(135deg, rgba(88, 166, 255, 0.25) 0%, rgba(88, 166, 255, 0.15) 100%); | |
| border-color: rgba(88, 166, 255, 1); | |
| box-shadow: 0 0 20px rgba(88, 166, 255, 0.4); | |
| transform: translateY(-2px); | |
| } | |
| /* ======================================== | |
| BADGES DE STATUT | |
| ======================================== */ | |
| #checkup-loans-module .checkup-status-badge { | |
| display: inline-block; | |
| padding: 6px 14px; | |
| border-radius: 20px; | |
| font-size: 0.8rem; | |
| font-weight: 600; | |
| letter-spacing: 0.5px; | |
| text-transform: uppercase; | |
| } | |
| #checkup-loans-module .checkup-status-actif { | |
| background: rgba(84, 189, 75, 0.2); | |
| color: #54bd4b; | |
| border: 1px solid rgba(84, 189, 75, 0.4); | |
| } | |
| #checkup-loans-module .checkup-status-updated { | |
| background: rgba(243, 156, 18, 0.2); | |
| color: #f39c12; | |
| border: 1px solid rgba(243, 156, 18, 0.4); | |
| } | |
| /* ======================================== | |
| SÉPARATEURS | |
| ======================================== */ | |
| #checkup-loans-module .checkup-section-divider { | |
| height: 2px; | |
| background: linear-gradient(90deg, transparent 0%, rgba(88, 166, 255, 0.4) 50%, transparent 100%); | |
| margin: 2.5rem 0; | |
| } | |
| /* ======================================== | |
| MÉTRIQUES | |
| ======================================== */ | |
| #checkup-loans-module [data-testid="stMetric"] { | |
| background: rgba(22, 27, 34, 0.6); | |
| border: 1px solid rgba(48, 54, 61, 0.8); | |
| border-radius: 6px; | |
| padding: 16px; | |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); | |
| } | |
| #checkup-loans-module [data-testid="stMetric"] label { | |
| color: #8b949e !important; | |
| font-size: 0.75rem !important; | |
| font-weight: 500 !important; | |
| text-transform: uppercase; | |
| letter-spacing: 0.8px; | |
| } | |
| #checkup-loans-module [data-testid="stMetric"] [data-testid="stMetricValue"] { | |
| color: #58a6ff !important; | |
| font-size: 1.6rem !important; | |
| font-weight: 600 !important; | |
| } | |
| /* ======================================== | |
| RESPONSIVE | |
| ======================================== */ | |
| @media (max-width: 768px) { | |
| #checkup-loans-module .checkup-loan-card { | |
| padding: 15px; | |
| } | |
| #checkup-loans-module [data-testid="stMetric"] [data-testid="stMetricValue"] { | |
| font-size: 1.3rem !important; | |
| } | |
| } | |
| </style> | |
| """, 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('<div id="checkup-loans-module">', 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('<div class="checkup-section-divider"></div>', 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('</div>', 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('</div>', 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('</div>', 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('</div>', 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('<div class="checkup-section-divider"></div>', 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('<div class="checkup-blocked-alert">', 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('</div>', unsafe_allow_html=True) | |
| st.markdown('</div>', 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('<div class="checkup-blocked-alert">', 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('</div>', unsafe_allow_html=True) | |
| st.markdown('</div>', 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('<div class="checkup-section-divider"></div>', unsafe_allow_html=True) | |
| # ============================================================================ | |
| # ÉTAPE 5 : AFFICHAGE DES INFORMATIONS ACTUELLES | |
| # ============================================================================ | |
| st.subheader("Étape 3 : Informations actuelles du prêt") | |
| st.markdown('<div class="checkup-loan-card">', 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"<span class='checkup-status-badge checkup-status-actif'>Statut : {selected_loan['Statut']}</span>", unsafe_allow_html=True) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="checkup-section-divider"></div>', 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('<div class="checkup-section-divider"></div>', unsafe_allow_html=True) | |
| st.subheader(" Comparaison Avant / Après") | |
| st.markdown('<div class="checkup-comparison-table">', 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('</div>', 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('<div class="checkup-warning-box">', unsafe_allow_html=True) | |
| st.markdown("### ⚠️ Points d'attention") | |
| for warning in warnings: | |
| st.markdown(f"- {warning}") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # ============================================================================ | |
| # ANALYSE DE SOLVABILITÉ (NOUVEAU PRÊT) | |
| # ============================================================================ | |
| st.markdown('<div class="checkup-section-divider"></div>', 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 : <span style='color:{analyse['couleur']}'>{analyse['statut']}</span>", | |
| 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('<div class="checkup-section-divider"></div>', 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('<div class="checkup-section-divider"></div>', 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('<div class="checkup-section-divider"></div>', 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('</div>', unsafe_allow_html=True) # Fermer le wrapper | |
| return # ✅ STOP - Ne pas continuer le reste du module |