Spaces:
Running
Running
| import streamlit as st | |
| import pandas as pd | |
| from datetime import datetime, date, timedelta | |
| # IMPORT DES NOUVEAUX MODULES | |
| from Analytics.AnalyseFinance import ( | |
| analyser_capacite, | |
| generer_tableau_amortissement, | |
| calculer_taux_endettement, | |
| clean_taux_value # ✅ AJOUT | |
| ) | |
| from DocumentGen.AutoPDFGeneration import generer_contrat_pret, generer_reconnaissance_dette, generer_contrat_caution | |
| import time | |
| # ============================================================================ | |
| # GESTION DU CACHE (À PLACER ICI) | |
| # ============================================================================ | |
| # Le cache expire après 10 minutes | |
| def get_cached_data(_client, sheet_name, worksheet_name): | |
| """Lit une feuille spécifique et la transforme en DataFrame avec cache.""" | |
| try: | |
| sh = _client.open(sheet_name) | |
| ws = sh.worksheet(worksheet_name) | |
| return pd.DataFrame(ws.get_all_records()) | |
| except Exception as e: | |
| st.error(f"Erreur lors de la lecture de {worksheet_name}: {e}") | |
| return pd.DataFrame() | |
| def refresh_data(): | |
| """Fonction pour vider le cache manuellement.""" | |
| st.cache_data.clear() | |
| st.rerun() | |
| # ============================================================================ | |
| # STYLES CSS (Ta fonction existante) | |
| # ============================================================================ | |
| def apply_loans_engine_styles(): | |
| st.markdown(""" | |
| <style> | |
| /* ======================================== | |
| WRAPPER D'ISOLATION DU MODULE | |
| ======================================== */ | |
| #loans-engine-module { | |
| padding: 1rem; | |
| margin: 0 auto; | |
| max-width: 100%; | |
| } | |
| /* ======================================== | |
| CARTES MÉTRIQUES SPÉCIFIQUES AU MODULE | |
| ======================================== */ | |
| #loans-engine-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); | |
| backdrop-filter: blur(8px); | |
| transition: transform 0.2s ease; | |
| } | |
| #loans-engine-module [data-testid="stMetric"]:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 8px rgba(88, 166, 255, 0.2); | |
| } | |
| #loans-engine-module [data-testid="stMetric"] label { | |
| color: #8b949e !important; | |
| font-size: 0.75rem !important; | |
| font-weight: 500 !important; | |
| text-transform: uppercase; | |
| letter-spacing: 0.8px; | |
| } | |
| #loans-engine-module [data-testid="stMetric"] [data-testid="stMetricValue"] { | |
| color: #58a6ff !important; | |
| font-size: 1.6rem !important; | |
| font-weight: 600 !important; | |
| } | |
| #loans-engine-module [data-testid="stMetric"] [data-testid="stMetricDelta"] { | |
| color: #58a6ff !important; | |
| font-size: 0.85rem !important; | |
| } | |
| /* ======================================== | |
| BOUTONS SPÉCIFIQUES MODULE PRÊTS | |
| ======================================== */ | |
| #loans-engine-module .loans-engine-action-btn button { | |
| background: linear-gradient(135deg, rgba(88, 166, 255, 0.1) 0%, rgba(88, 166, 255, 0.05) 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; | |
| } | |
| #loans-engine-module .loans-engine-action-btn button:hover { | |
| background: linear-gradient(135deg, rgba(88, 166, 255, 0.2) 0%, rgba(88, 166, 255, 0.1) 100%); | |
| border-color: rgba(88, 166, 255, 1); | |
| box-shadow: 0 0 20px rgba(88, 166, 255, 0.4); | |
| transform: translateY(-2px); | |
| } | |
| /* ======================================== | |
| EXPANDERS ANALYSE CAPACITÉ | |
| ======================================== */ | |
| #loans-engine-module .loans-engine-analysis-expander .streamlit-expanderHeader { | |
| background: rgba(22, 27, 34, 0.8); | |
| border-left: 3px solid rgba(88, 166, 255, 0.6); | |
| padding: 14px 18px; | |
| border-radius: 4px; | |
| font-weight: 600; | |
| } | |
| #loans-engine-module .loans-engine-analysis-expander .streamlit-expanderHeader:hover { | |
| background: rgba(88, 166, 255, 0.1); | |
| border-left-color: rgba(88, 166, 255, 1); | |
| } | |
| /* ======================================== | |
| SÉLECTEUR CLIENT - STYLE AMÉLIORÉ | |
| ======================================== */ | |
| #loans-engine-module .loans-engine-client-selector { | |
| margin-bottom: 2rem; | |
| } | |
| #loans-engine-module .loans-engine-client-selector .stSelectbox { | |
| background: rgba(22, 27, 34, 0.6); | |
| border-radius: 8px; | |
| padding: 8px; | |
| } | |
| /* ======================================== | |
| ALERTE SOLVABILITÉ | |
| ======================================== */ | |
| #loans-engine-module .loans-engine-solvency-alert { | |
| background: rgba(88, 166, 255, 0.08); | |
| border: 1px solid rgba(88, 166, 255, 0.3); | |
| border-left: 4px solid #58a6ff; | |
| border-radius: 6px; | |
| padding: 16px; | |
| margin: 1rem 0; | |
| } | |
| /* ======================================== | |
| RÉSULTATS DE SIMULATION | |
| ======================================== */ | |
| #loans-engine-module .loans-engine-simulation-results { | |
| background: rgba(22, 27, 34, 0.4); | |
| border: 1px solid rgba(48, 54, 61, 0.6); | |
| border-radius: 8px; | |
| padding: 20px; | |
| margin: 1.5rem 0; | |
| } | |
| #loans-engine-module .loans-engine-simulation-results h3 { | |
| color: #58a6ff !important; | |
| font-size: 1.1rem !important; | |
| margin-bottom: 1rem; | |
| border-bottom: 1px solid rgba(88, 166, 255, 0.2); | |
| padding-bottom: 8px; | |
| } | |
| /* ======================================== | |
| STATUTS ANALYSE CAPACITÉ | |
| ======================================== */ | |
| #loans-engine-module .loans-engine-status-excellent { | |
| background: rgba(84, 189, 75, 0.1); | |
| border-left: 4px solid #54bd4b; | |
| padding: 12px 16px; | |
| border-radius: 4px; | |
| margin: 1rem 0; | |
| } | |
| #loans-engine-module .loans-engine-status-good { | |
| background: rgba(88, 166, 255, 0.1); | |
| border-left: 4px solid #58a6ff; | |
| padding: 12px 16px; | |
| border-radius: 4px; | |
| margin: 1rem 0; | |
| } | |
| #loans-engine-module .loans-engine-status-warning { | |
| background: rgba(243, 156, 18, 0.1); | |
| border-left: 4px solid #f39c12; | |
| padding: 12px 16px; | |
| border-radius: 4px; | |
| margin: 1rem 0; | |
| } | |
| #loans-engine-module .loans-engine-status-critical { | |
| background: rgba(231, 76, 60, 0.1); | |
| border-left: 4px solid #e74c3c; | |
| padding: 12px 16px; | |
| border-radius: 4px; | |
| margin: 1rem 0; | |
| } | |
| /* ======================================== | |
| TABLEAU D'AMORTISSEMENT | |
| ======================================== */ | |
| #loans-engine-module .loans-engine-amortization-table { | |
| margin: 2rem 0; | |
| } | |
| #loans-engine-module .loans-engine-amortization-table .stDataFrame { | |
| border: 1px solid rgba(48, 54, 61, 0.6); | |
| border-radius: 8px; | |
| overflow: hidden; | |
| } | |
| #loans-engine-module .loans-engine-amortization-table thead tr th { | |
| background: rgba(48, 54, 61, 0.9) !important; | |
| color: #58a6ff !important; | |
| font-weight: 700 !important; | |
| font-size: 0.85rem !important; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| padding: 12px 8px !important; | |
| } | |
| #loans-engine-module .loans-engine-amortization-table tbody tr:nth-child(odd) { | |
| background: rgba(22, 27, 34, 0.4); | |
| } | |
| #loans-engine-module .loans-engine-amortization-table tbody tr:nth-child(even) { | |
| background: rgba(22, 27, 34, 0.2); | |
| } | |
| #loans-engine-module .loans-engine-amortization-table tbody tr:hover { | |
| background: rgba(88, 166, 255, 0.08) !important; | |
| } | |
| /* ======================================== | |
| FORMULAIRE VALIDATION | |
| ======================================== */ | |
| #loans-engine-module .loans-engine-validation-form { | |
| background: rgba(22, 27, 34, 0.6); | |
| border: 2px solid rgba(88, 166, 255, 0.3); | |
| border-radius: 10px; | |
| padding: 24px; | |
| margin: 2rem 0; | |
| } | |
| #loans-engine-module .loans-engine-validation-form h3 { | |
| color: #58a6ff !important; | |
| text-align: center; | |
| margin-bottom: 1.5rem; | |
| } | |
| /* ======================================== | |
| DATES PERSONNALISÉES | |
| ======================================== */ | |
| #loans-engine-module .loans-engine-custom-dates { | |
| background: rgba(22, 27, 34, 0.3); | |
| border-radius: 6px; | |
| padding: 16px; | |
| margin: 1rem 0; | |
| } | |
| #loans-engine-module .loans-engine-custom-dates .stDateInput { | |
| margin-bottom: 8px; | |
| } | |
| /* ======================================== | |
| BADGES TYPE PRÊT | |
| ======================================== */ | |
| #loans-engine-module .loans-engine-loan-type-badge { | |
| display: inline-block; | |
| background: rgba(88, 166, 255, 0.15); | |
| color: #58a6ff; | |
| padding: 6px 14px; | |
| border-radius: 20px; | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| letter-spacing: 0.5px; | |
| margin: 0.5rem 0; | |
| border: 1px solid rgba(88, 166, 255, 0.4); | |
| } | |
| /* ======================================== | |
| SÉPARATEURS SECTION | |
| ======================================== */ | |
| #loans-engine-module .loans-engine-section-divider { | |
| height: 2px; | |
| background: linear-gradient(90deg, transparent 0%, rgba(88, 166, 255, 0.3) 50%, transparent 100%); | |
| margin: 2rem 0; | |
| } | |
| /* ======================================== | |
| RESPONSIVE ADJUSTMENTS | |
| ======================================== */ | |
| @media (max-width: 768px) { | |
| #loans-engine-module [data-testid="stMetric"] { | |
| padding: 12px; | |
| } | |
| #loans-engine-module [data-testid="stMetric"] [data-testid="stMetricValue"] { | |
| font-size: 1.3rem !important; | |
| } | |
| #loans-engine-module .loans-engine-action-btn button { | |
| font-size: 0.8rem; | |
| padding: 10px 18px; | |
| } | |
| } | |
| /* ======================================== | |
| ANIMATIONS SUBTILES | |
| ======================================== */ | |
| @keyframes loans-engine-pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.7; } | |
| } | |
| #loans-engine-module .loans-engine-loading { | |
| animation: loans-engine-pulse 2s ease-in-out infinite; | |
| } | |
| /* ======================================== | |
| FOCUS STATES (Accessibilité) | |
| ======================================== */ | |
| #loans-engine-module button:focus-visible, | |
| #loans-engine-module input:focus-visible, | |
| #loans-engine-module select:focus-visible { | |
| outline: 2px solid #58a6ff !important; | |
| outline-offset: 2px; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ============================================================================ | |
| # MAIN FUNCTION | |
| # ============================================================================ | |
| def show_loans_engine(client, sheet_name): | |
| apply_loans_engine_styles() | |
| st.markdown('<div id="loans-engine-module">', unsafe_allow_html=True) | |
| st.header("MOTEUR FINANCIER : OCTROI DE PRÊT") | |
| # 1. CHARGEMENT DONNÉES | |
| try: | |
| # On définit 'sh' ici pour qu'il soit accessible dans toute la fonction | |
| sh = client.open(sheet_name) | |
| # Utilise get_cached_data pour éviter les erreurs de quota | |
| df_clients = get_cached_data(client, sheet_name, "Clients_KYC") | |
| try: | |
| df_garants = get_cached_data(client, sheet_name, "Garants_KYC") | |
| if not df_garants.empty: | |
| df_garants['search_label'] = df_garants['ID_Garant'].astype(str) + " - " + df_garants['Nom_Complet'].astype(str) | |
| except Exception as e: | |
| st.error(f"Erreur sur la feuille Garants : {e}") | |
| df_garants = pd.DataFrame() | |
| except Exception as e: | |
| st.error(f"Erreur connexion : {e}") | |
| return | |
| # ============================================================================ | |
| # 2. SÉLECTION CLIENT (Obligatoire) | |
| # ============================================================================ | |
| if df_clients.empty: | |
| st.error("🛑 Aucun client trouvé dans la base KYC. Veuillez d'abord enregistrer un client pour octroyer un prêt.") | |
| return # On arrête l'exécution si pas de client | |
| else: | |
| df_clients['search_label'] = df_clients['ID_Client'].astype(str) + " - " + df_clients['Nom_Complet'].astype(str) | |
| selected_client_label = st.selectbox("Rechercher une cible ", [""] + df_clients['search_label'].tolist()) | |
| if selected_client_label: # <--- TOUT DOIT ÊTRE DANS CE BLOC | |
| # Récupération de la ligne complète | |
| client_info = df_clients[df_clients['search_label'] == selected_client_label].iloc[0] | |
| client_id = client_info['ID_Client'] | |
| # ✅ Lecture du nombre de personnes à charge depuis le KYC | |
| try: | |
| pers_charge = int(client_info.get('Pers_Charge', 0)) | |
| except (ValueError, TypeError): | |
| pers_charge = 0 | |
| st.info(f"♦️Cible Détectée : **{client_info['Nom_Complet']}**") | |
| with st.expander(f"Analyse Solvabilité : {client_info['Nom_Complet']}", expanded=True): | |
| def clean_val(val): | |
| try: | |
| return float(str(val).replace("XOF","").replace(" ","").replace(",","")) | |
| except: return 0.0 | |
| rev = clean_val(client_info['Revenus_Mensuels']) | |
| chg = clean_val(client_info['Charges_Estimees']) | |
| reste = rev - chg | |
| c1, c2, c3 = st.columns(3) | |
| c1.metric("Revenus Mensuels", f"{int(rev):,} XOF".replace(",", " ")) | |
| c2.metric("Charges Estimées", f"{int(chg):,} XOF".replace(",", " ")) | |
| # Alerte visuelle si le reste à vivre est faible | |
| delta_color = "normal" if reste > 20000 else "inverse" | |
| c3.metric("Reste à vivre (Net)", f"{int(reste):,} XOF".replace(",", " "), | |
| delta=f"{int(reste):,}", delta_color=delta_color) | |
| st.markdown(f"**Profession :** {client_info['Statut_Pro']} | **Ville :** {client_info['Ville']}") | |
| # ✅ Affichage IPF si le client a des personnes à charge | |
| if pers_charge >= 1: | |
| ipf = rev / (1 + pers_charge) | |
| st.info(f"📊 **IPF (Indice de Pression Familiale)** : {int(ipf):,} XOF — base de calcul ajustée ({pers_charge} pers. à charge)".replace(",", " ")) | |
| # ============================================================================ | |
| # 3. SÉLECTION GARANT (Optionnel) | |
| # ============================================================================ | |
| selected_garant = None | |
| garant_id = "" | |
| if df_garants.empty: | |
| st.info("Aucun garant n'est actuellement enregistré dans la base (Optionnel).") | |
| else: | |
| selected_garant_label = st.selectbox("Rechercher un garant (Optionnel)", [""] + df_garants['search_label'].tolist()) | |
| if selected_garant_label: | |
| # Récupération de la ligne complète | |
| garant_info = df_garants[df_garants['search_label'] == selected_garant_label].iloc[0] | |
| selected_garant = garant_info # Pour l'utiliser dans la génération PDF | |
| garant_id = garant_info['ID_Garant'] | |
| st.info(f"🔸Garant Détectée : **{garant_info['Nom_Complet']}**") | |
| with st.expander(f"Analyse de la Caution : {garant_info['Nom_Complet']}", expanded=True): | |
| rev_g = clean_val(garant_info['Revenus_Mensuels']) | |
| chg_g = clean_val(garant_info['Charges_Estimees']) | |
| g1, g2, g3 = st.columns(3) | |
| g1.metric("Revenus Garant", f"{int(rev_g):,} XOF".replace(",", " ")) | |
| g2.metric("Charges Garant", f"{int(chg_g):,} XOF".replace(",", " ")) | |
| g3.metric("Reste à vivre", f"{int(rev_g - chg_g):,} XOF".replace(",", " ")) | |
| st.warning("⚠️ **Engagement solidaire** : Le garant renonce aux bénéfices de discussion et de division. Il s'engage à payer en cas de défaillance de l'emprunteur.") | |
| # 4. CONFIGURATION PRÊT | |
| st.markdown("---") | |
| st.subheader("Configuration") | |
| col_motif, col_type, col_moyen = st.columns(3) | |
| with col_motif: | |
| # NOUVEAU : MOTIF | |
| motif = st.selectbox( | |
| "Motif du prêt", | |
| [ | |
| "Commerce / Achat de stock", | |
| "Investissement", | |
| "Trésorerie professionnelle", | |
| "Lancement d'activité", | |
| "Développement d'activité", | |
| "Agriculture / Élevage", | |
| "Transport / Logistique", | |
| "Urgence médicale", | |
| "Scolarité / Formation", | |
| "Logement / Habitat", | |
| "Réparations", | |
| "Événements familiaux", | |
| "Voyage / Déplacement", | |
| "Consommation", | |
| "Achat d'équipement personnel", | |
| "Projet personnel", | |
| "Autre" | |
| ] | |
| ) | |
| with col_type: | |
| type_pret = st.selectbox("Type de remboursement", ["In Fine", "Mensuel - Intérêts", "Mensuel - Constant", "Hebdomadaire", "Personnalisé"]) | |
| with col_moyen: | |
| # Correction : Définition de la variable moyen_transfert | |
| moyen_transfert = st.selectbox("Moyen de transfert", ["Wave", "Orange Money", "Virement"]) | |
| # Mapping Type | |
| type_code_map = {"In Fine": "IN_FINE", "Mensuel - Intérêts": "MENSUEL_INTERETS", "Mensuel - Constant": "MENSUEL_CONSTANT", "Hebdomadaire": "HEBDOMADAIRE", "Personnalisé": "PERSONNALISE"} | |
| type_code = type_code_map[type_pret] | |
| col1, col2, col3 = st.columns(3) | |
| montant = col1.number_input("Montant (XOF)", 10000, value=100000, step=10000) | |
| taux_hebdo = col2.number_input("Taux Hebdo (%)", 0.1, value=2.0, step=0.1) | |
| # ✅ La durée sera définie spécifiquement dans chaque type de prêt ci-dessous | |
| # ==================================================================== | |
| # DÉBUT DU BLOC LOGIQUE À COPIER-COLLER | |
| # ==================================================================== | |
| # Initialisation des variables pour éviter les erreurs | |
| montant_versement = 0 | |
| montant_total = 0 | |
| cout_credit = 0 | |
| nb_versements = 0 | |
| duree_semaines = 0 | |
| dates_versements = [] | |
| # Initialisation | |
| date_debut = date.today() | |
| date_fin = date_debut # Par défaut | |
| # ----------------------------------------------------------- | |
| # 1. LOGIQUE IN FINE (1 seul versement à la fin) | |
| # ----------------------------------------------------------- | |
| if type_code == "IN_FINE": | |
| duree_semaines = col3.number_input("Durée (en semaines)", min_value=1, max_value=104, value=8) | |
| date_fin = date_debut + timedelta(weeks=duree_semaines) | |
| # ✅ CORRECTION : On définit explicitement la date de versement pour qu'elle soit enregistrée | |
| dates_versements = [date_fin] | |
| # Calculs | |
| montant_total = montant * (1 + (taux_hebdo / 100) * duree_semaines) | |
| cout_credit = montant_total - montant | |
| montant_versement = montant_total | |
| nb_versements = 1 | |
| # Affichage résultat simulation immédiate | |
| st.markdown("### Simulation") | |
| res1, res2, res3 = st.columns(3) | |
| res1.metric("Versement unique", f"{int(montant_versement):,} XOF".replace(",", " ")) | |
| res2.metric("Coût du crédit", f"{int(cout_credit):,} XOF".replace(",", " ")) | |
| res3.metric("Montant Total", f"{int(montant_total):,} XOF".replace(",", " "), delta=f"+{int(cout_credit):,}") | |
| # ----------------------------------------------------------- | |
| # 2. LOGIQUE MENSUEL - INTÉRÊTS (Remboursement capital à la fin) | |
| # ----------------------------------------------------------- | |
| elif type_code == "MENSUEL_INTERETS": | |
| duree_mois = col3.number_input("Durée (en mois)", min_value=1, max_value=60, value=12) | |
| date_fin = date_debut + timedelta(days=duree_mois * 30) # Approximation standard mensuelle | |
| # Conversion et Calculs | |
| duree_semaines = duree_mois * 4.33 # Standard bancaire | |
| taux_mensuel = (taux_hebdo / 100) * 4.33 | |
| interet_mensuel = montant * taux_mensuel | |
| montant_versement = interet_mensuel # Ce que le client paie chaque mois | |
| montant_final_mois = montant + interet_mensuel # Dernier mois | |
| montant_total = (interet_mensuel * duree_mois) + montant | |
| cout_credit = montant_total - montant | |
| nb_versements = int(duree_mois) | |
| # Affichage résultat simulation | |
| st.markdown("### Simulation") | |
| res1, res2 = st.columns(2) | |
| res1.metric("Intérêts mensuels", f"{int(interet_mensuel):,} XOF".replace(",", " ")) | |
| res2.metric("Dernier versement", f"{int(montant_final_mois):,} XOF".replace(",", " ")) | |
| res3, res4 = st.columns(2) | |
| res3.metric("Coût du crédit", f"{int(cout_credit):,} XOF".replace(",", " ")) | |
| res4.metric("Montant Total", f"{int(montant_total):,} XOF".replace(",", " "), delta=f"+{int(cout_credit):,}") | |
| # ----------------------------------------------------------- | |
| # 3. LOGIQUE MENSUEL - CONSTANT (Amortissement classique) | |
| # ----------------------------------------------------------- | |
| elif type_code == "MENSUEL_CONSTANT": | |
| duree_mois = col3.number_input("Durée (en mois)", min_value=1, max_value=60, value=12) | |
| date_fin = date_debut + timedelta(days=duree_mois * 30) | |
| # Conversion et Calculs | |
| duree_semaines = duree_mois * 4.33 | |
| taux_mensuel = (taux_hebdo / 100) * 4.33 | |
| if taux_mensuel > 0: | |
| # Formule mathématique des mensualités constantes | |
| mensualite = (montant * taux_mensuel) / (1 - (1 + taux_mensuel)**(-duree_mois)) | |
| else: | |
| mensualite = montant / duree_mois | |
| montant_versement = mensualite | |
| montant_total = mensualite * duree_mois | |
| cout_credit = montant_total - montant | |
| nb_versements = int(duree_mois) | |
| # Affichage résultat simulation | |
| st.markdown("### Simulation") | |
| res1, res2, res3 = st.columns(3) | |
| res1.metric("Mensualité constante", f"{int(mensualite):,} XOF".replace(",", " ")) | |
| res2.metric("Coût du crédit", f"{int(cout_credit):,} XOF".replace(",", " ")) | |
| res3.metric("Montant Total", f"{int(montant_total):,} XOF".replace(",", " "), delta=f"+{int(cout_credit):,}") | |
| # ----------------------------------------------------------- | |
| # 4. LOGIQUE HEBDOMADAIRE | |
| # ----------------------------------------------------------- | |
| elif type_code == "HEBDOMADAIRE": | |
| duree_semaines = col3.number_input("Durée (en semaines)", min_value=1, max_value=104, value=12) | |
| date_fin = date_debut + timedelta(weeks=duree_semaines) | |
| # Calculs | |
| taux_hebdo_decimal = taux_hebdo / 100 | |
| if taux_hebdo_decimal > 0: | |
| hebdomadalite = (montant * taux_hebdo_decimal) / (1 - (1 + taux_hebdo_decimal)**(-duree_semaines)) | |
| else: | |
| hebdomadalite = montant / duree_semaines | |
| montant_versement = hebdomadalite | |
| montant_total = hebdomadalite * duree_semaines | |
| cout_credit = montant_total - montant | |
| nb_versements = int(duree_semaines) | |
| # Affichage résultat simulation | |
| st.markdown("### Simulation") | |
| res1, res2, res3 = st.columns(3) | |
| res1.metric("Versement Hebdo", f"{int(hebdomadalite):,} XOF".replace(",", " ")) | |
| res2.metric("Coût du crédit", f"{int(cout_credit):,} XOF".replace(",", " ")) | |
| res3.metric("Montant Total", f"{int(montant_total):,} XOF".replace(",", " "), delta=f"+{int(cout_credit):,}") | |
| # 5. LOGIQUE PERSONNALISÉE (Dates manuelles) | |
| # ----------------------------------------------------------- | |
| else: # PERSONNALISE | |
| st.info("Configurez les dates de versement ci-dessous") | |
| # ✅ CORRECTION : Initialisation AVANT utilisation | |
| if 'dates_perso' not in st.session_state: | |
| st.session_state.dates_perso = [date.today() + timedelta(weeks=2)] | |
| # Interface d'ajout de dates | |
| st.markdown("**Dates de versement :**") | |
| col_add, col_reset = st.columns([1, 4]) | |
| if col_add.button("➕ Ajouter"): | |
| last_date = st.session_state.dates_perso[-1] | |
| st.session_state.dates_perso.append(last_date + timedelta(weeks=1)) | |
| st.rerun() | |
| # Affichage des date pickers | |
| dates_versements = [] | |
| for idx, dt in enumerate(st.session_state.dates_perso): | |
| col_d, col_x = st.columns([4, 1]) | |
| new_date = col_d.date_input(f"Echéance {idx+1}", value=dt, key=f"d_{idx}", min_value=date.today()) | |
| dates_versements.append(new_date) | |
| if col_x.button("🔻", key=f"del_{idx}") and len(st.session_state.dates_perso) > 1: | |
| st.session_state.dates_perso.pop(idx) | |
| st.rerun() | |
| st.session_state.dates_perso = dates_versements # Mise à jour state | |
| # Calculs basés sur les dates | |
| if dates_versements: | |
| dates_versements.sort() | |
| date_fin = dates_versements[-1] # ✅ La dernière date triée | |
| delta_days = (date_fin - date_debut).days | |
| duree_semaines = max(1, delta_days // 7) | |
| montant_total = montant * (1 + (taux_hebdo / 100) * duree_semaines) | |
| cout_credit = montant_total - montant | |
| nb_versements = len(dates_versements) # ✅ Correction : utiliser dates_versements au lieu de dates_sorted | |
| montant_versement = montant_total / nb_versements | |
| # Affichage résultat simulation | |
| st.markdown("### Simulation") | |
| res1, res2 = st.columns(2) | |
| res1.metric("Moyenne/Versement", f"{int(montant_versement):,} XOF".replace(",", " ")) | |
| res2.metric("Durée estimée", f"{duree_semaines} sem") | |
| res3, res4 = st.columns(2) | |
| res3.metric("Coût du crédit", f"{int(cout_credit):,} XOF".replace(",", " ")) | |
| res4.metric("Montant Total", f"{int(montant_total):,} XOF".replace(",", " "), delta=f"+{int(cout_credit):,}") | |
| # ==================================================================== | |
| # FIN DU BLOC LOGIQUE | |
| # ==================================================================== | |
| # 5. APPEL CERVEAU ANALYTIQUE (AUTO-TRIGGER) | |
| # ✅ MODIFIÉ : passage de pers_charge pour activer l'IPF si nécessaire | |
| analyse = analyser_capacite( | |
| type_code, montant, taux_hebdo, duree_semaines, montant_versement, nb_versements, | |
| client_info['Revenus_Mensuels'], client_info.get('Charges_Estimees', 0), montant_total, | |
| pers_charge=pers_charge | |
| ) | |
| # AFFICHAGE ANALYSE | |
| st.markdown(f"### Analyse : <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']) | |
| # 6. TABLEAU AMORTISSEMENT | |
| date_debut = date.today() | |
| # On génère le tableau normalement | |
| # ✅ CORRECTION : Passage de dates_versements pour le type PERSONNALISÉ | |
| if type_code == "PERSONNALISE": | |
| # Pour le type personnalisé, on passe explicitement les dates | |
| df_amort = generer_tableau_amortissement( | |
| type_code, montant, taux_hebdo, duree_semaines, | |
| montant_versement, nb_versements, date_debut, | |
| dates_versements=dates_versements # ✅ AJOUT CRITIQUE | |
| ) | |
| else: | |
| # Pour les autres types, pas besoin de dates personnalisées | |
| df_amort = generer_tableau_amortissement( | |
| type_code, montant, taux_hebdo, duree_semaines, | |
| montant_versement, nb_versements, date_debut | |
| ) | |
| # INSERTION DE LA COLONNE TYPE : Juste ici avant l'affichage | |
| if not df_amort.empty: | |
| df_amort.insert(0, "Type", type_pret) | |
| st.write("### Tableau d'échéances") | |
| st.dataframe(df_amort, hide_index=True, use_container_width=True) | |
| # 7. VALIDATION & DOCUMENTS | |
| with st.form("valid_pret"): | |
| submit = st.form_submit_button("OCTROYER & GÉNÉRER DOCS") | |
| # ============================================================================ | |
| # CALCUL DU TAUX D'ENDETTEMENT AVANT VALIDATION | |
| # ✅ MODIFIÉ : passage de pers_charge pour activer l'IPF si nécessaire | |
| # ============================================================================ | |
| from Analytics.AnalyseFinance import calculer_taux_endettement | |
| taux_endettement = calculer_taux_endettement( | |
| type_code, | |
| montant_versement, | |
| nb_versements, | |
| duree_semaines, | |
| client_info['Revenus_Mensuels'], | |
| pers_charge=pers_charge | |
| ) | |
| # Affichage pour information (optionnel) | |
| st.info(f"📊 **Taux d'endettement calculé** : {taux_endettement:.2f}%") | |
| if submit: | |
| # 7a. SAUVEGARDE GOOGLE SHEETS | |
| ws_prets = sh.worksheet("Prets_Master") | |
| # Calcul de l'ID sous forme PRT-2026-xxxx | |
| current_year = datetime.now().year | |
| # Récupère tous les ID existants | |
| all_rows = ws_prets.get_all_values() | |
| if len(all_rows) <= 1: | |
| next_number = 1 | |
| else: | |
| # Extrait tous les numéros existants pour l'année en cours | |
| existing_numbers = [] | |
| for row in all_rows[1:]: # Ignore l'en-tête | |
| if row[0].startswith(f"PRT-{current_year}-"): | |
| try: | |
| num = int(row[0].split('-')[-1]) | |
| existing_numbers.append(num) | |
| except: | |
| continue | |
| # Prend le max + 1, ou 1 si aucun prêt cette année | |
| next_number = max(existing_numbers) + 1 if existing_numbers else 1 | |
| new_id = f"PRT-{current_year}-{next_number:04d}" | |
| # ORDRE STRICT DEMANDÉ POUR Prets_Master | |
| row_data = [ | |
| new_id, # ID_Pret | |
| client_id, # ID_Client | |
| client_info['Nom_Complet'], # Nom_Complet | |
| type_code, # Type_Pret | |
| motif, # Motif | |
| montant, # Montant_Capital | |
| taux_hebdo, # Taux_Hebdo | |
| round(taux_endettement, 2), # Taux_endettement | |
| duree_semaines, # Duree_Semaines | |
| round(montant_versement), # Montant_Versement | |
| round(montant_total), # Montant_Total | |
| round(cout_credit), # Cout_Credit | |
| nb_versements, # Nb_Versements | |
| ";".join([d.strftime("%d/%m/%Y") for d in dates_versements]) if dates_versements else "", # Dates_Versements | |
| date_debut.strftime("%d/%m/%Y"), # Date_Deblocage | |
| date_fin.strftime("%d/%m/%Y"), # Date_Fin (Maintenant garantie) | |
| moyen_transfert, # Moyen_Transfert | |
| "ACTIF", # Statut | |
| garant_id, # ID_Garant | |
| datetime.now().strftime("%d-%m-%Y %H:%M:%S") # Date_Creation | |
| ] | |
| ws_prets.append_row(row_data) | |
| time.sleep(1) | |
| # ✅ SAUVEGARDE DANS SESSION STATE POUR PERSISTANCE | |
| st.session_state.loan_validated = True | |
| st.session_state.loan_id = new_id | |
| st.session_state.loan_data = { | |
| "ID_Pret": new_id, | |
| "Montant_Capital": montant, | |
| "Montant_Total": montant_total, | |
| "Taux_Hebdo": taux_hebdo, | |
| "Taux_Endettement": taux_endettement, # ✅ AJOUT | |
| "Duree_Semaines": duree_semaines, | |
| "Motif": motif, | |
| "Date_Deblocage": date_debut.strftime("%d/%m/%Y"), | |
| "Date_Fin": date_fin.strftime("%d/%m/%Y") | |
| } | |
| st.session_state.client_data = client_info.to_dict() | |
| st.session_state.garant_data = selected_garant.to_dict() if selected_garant is not None else None | |
| st.session_state.df_amort = df_amort.copy() | |
| st.success(f" Le Prêt {new_id} enregistré avec succès !") | |
| # ✅ AFFICHAGE BOUTONS EN DEHORS DU FORMULAIRE (persistants) | |
| if st.session_state.get('loan_validated', False): | |
| st.markdown("---") | |
| st.markdown(f"### Documents du prêt **{st.session_state.loan_id}**") | |
| # Génération des PDFs (une seule fois, conservés en mémoire) | |
| loan_data = st.session_state.loan_data | |
| client_data = st.session_state.client_data | |
| garant_data = st.session_state.garant_data | |
| df_amort_saved = st.session_state.df_amort | |
| # Affichage des boutons en colonnes | |
| col_pdf1, col_pdf2, col_pdf3, col_reset = st.columns(4) | |
| # PDF 1 : CONTRAT | |
| with col_pdf1: | |
| pdf_contrat = generer_contrat_pret(loan_data, client_data, df_amort_saved) | |
| st.download_button( | |
| "Contrat de Prêt", | |
| pdf_contrat, | |
| f"Contrat_{st.session_state.loan_id}.pdf", | |
| "application/pdf", | |
| use_container_width=True | |
| ) | |
| # PDF 2 : RECONNAISSANCE | |
| with col_pdf2: | |
| pdf_dette = generer_reconnaissance_dette(loan_data, client_data) | |
| st.download_button( | |
| "Reconnaissance de Dette", | |
| pdf_dette, | |
| f"Dette_{st.session_state.loan_id}.pdf", | |
| "application/pdf", | |
| use_container_width=True | |
| ) | |
| # PDF 3 : CAUTION (SI GARANT) | |
| with col_pdf3: | |
| if garant_data is not None: | |
| pdf_caution = generer_contrat_caution(loan_data, garant_data) | |
| st.download_button( | |
| "Contrat de Caution", | |
| pdf_caution, | |
| f"Caution_{st.session_state.loan_id}.pdf", | |
| "application/pdf", | |
| use_container_width=True | |
| ) | |
| else: | |
| st.info("Pas de garant") | |
| # Bouton pour réinitialiser et créer un nouveau prêt | |
| with col_reset: | |
| if st.button("Nouveau Prêt", use_container_width=True, type="primary"): | |
| # Nettoyage du session state | |
| st.session_state.loan_validated = False | |
| st.session_state.pop('loan_id', None) | |
| st.session_state.pop('loan_data', None) | |
| st.session_state.pop('client_data', None) | |
| st.session_state.pop('garant_data', None) | |
| st.session_state.pop('df_amort', None) | |
| st.rerun() | |
| st.markdown('</div>', unsafe_allow_html=True) |