Spaces:
Running
Running
| # -*- coding: utf-8 -*- | |
| """ | |
| Check_Up_Loans.py | |
| Module de mise à jour (Check-Up) des prêts existants. | |
| Gère les prêts issus de Prets_Master ET de Prets_Update. | |
| Workflow : | |
| 1. Sélection du client | |
| 2. Détection unifiée des prêts ACTIF (Prets_Master + Prets_Update) | |
| 3. Vérification bloquante des remboursements (sur ID_Pret racine) | |
| 4. Formulaire de modification + calculs automatiques | |
| 5. Archivage de l'ancienne version (statut → UPDATED) | |
| 6. Création de la nouvelle version dans Prets_Update (statut → ACTIF, Version = n+1) | |
| 7. Génération des documents PDF | |
| """ | |
| import streamlit as st | |
| import pandas as pd | |
| from datetime import datetime, date, timedelta | |
| import time | |
| from Analytics.AnalyseFinance import ( | |
| analyser_capacite, | |
| generer_tableau_amortissement, | |
| calculer_taux_endettement, | |
| clean_taux_value, | |
| clean_currency_value, | |
| clean_percentage_value, | |
| ) | |
| from DocumentGen.AutoPDFGeneration import ( | |
| generer_contrat_pret, | |
| generer_reconnaissance_dette, | |
| generer_contrat_caution, | |
| ) | |
| # ============================================================================ | |
| # CONSTANTES | |
| # ============================================================================ | |
| MOTIFS_PRET = [ | |
| "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", | |
| ] | |
| 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()} | |
| # En-têtes officiels de la feuille Prets_Update | |
| PRETS_UPDATE_HEADERS = [ | |
| "ID_Pret_Updated", | |
| "ID_Pret", | |
| "Version", | |
| "Date_Modification", | |
| "ID_Client", | |
| "Nom_Complet", | |
| "Type_Pret", | |
| "Motif", | |
| "Montant_Capital", | |
| "Taux_Hebdo", | |
| "Taux_Endettement", | |
| "Duree_Semaines", | |
| "Montant_Versement", | |
| "Montant_Total", | |
| "Cout_Credit", | |
| "Nb_Versements", | |
| "Dates_Versements", | |
| "Date_Deblocage", | |
| "Date_Fin", | |
| "Moyen_Transfert", | |
| "Statut", | |
| "ID_Garant", | |
| "Date_Creation", | |
| "Commentaire_Modification", | |
| ] | |
| # ============================================================================ | |
| # CACHE & REFRESH | |
| # ============================================================================ | |
| def get_cached_data(_client, sheet_name, worksheet_name): | |
| """Lit une feuille Google Sheets et retourne un DataFrame (avec cache 10 min).""" | |
| 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(): | |
| st.cache_data.clear() | |
| st.rerun() | |
| # ============================================================================ | |
| # UTILITAIRES — VERSIONING & SÉLECTION | |
| # ============================================================================ | |
| def _safe_version_int(v): | |
| """Convertit une valeur de version en entier. Retourne 0 si invalide.""" | |
| try: | |
| return int(str(v).strip()) | |
| except (ValueError, TypeError): | |
| return 0 | |
| def _get_next_version(df_prets_update: pd.DataFrame, root_id: str) -> int: | |
| """ | |
| Calcule la prochaine version à attribuer pour un ID_Pret racine donné. | |
| Règle : | |
| - Version 1 = prêt original dans Prets_Master (non stocké dans Prets_Update) | |
| - Version n+1 = max(Version existantes dans Prets_Update pour ce root_id) + 1 | |
| - Minimum retourné : 2 | |
| """ | |
| if df_prets_update.empty: | |
| return 2 | |
| subset = df_prets_update[df_prets_update["ID_Pret"].astype(str) == root_id] | |
| if subset.empty: | |
| return 2 | |
| max_v = subset["Version"].apply(_safe_version_int).max() | |
| return max(2, max_v + 1) | |
| def _get_latest_active_from_update(df_prets_update: pd.DataFrame, root_id: str): | |
| """ | |
| Retourne la ligne ACTIF de Prets_Update pour un root_id donné, | |
| en sélectionnant la version la plus élevée. | |
| Filtres cumulatifs : | |
| 1. ID_Pret == root_id | |
| 2. Statut == 'ACTIF' | |
| 3. Version == max(Version) | |
| Retourne None si aucune ligne ne correspond. | |
| """ | |
| if df_prets_update.empty: | |
| return None | |
| subset = df_prets_update[ | |
| (df_prets_update["ID_Pret"].astype(str) == root_id) | |
| & (df_prets_update["Statut"] == "ACTIF") | |
| ].copy() | |
| if subset.empty: | |
| return None | |
| subset["_version_int"] = subset["Version"].apply(_safe_version_int) | |
| return subset.loc[subset["_version_int"].idxmax()] | |
| def _sanitize_for_gspread(value): | |
| """ | |
| Convertit toute valeur numpy ou non-sérialisable en type Python natif | |
| avant envoi à gspread (qui sérialise en JSON). | |
| """ | |
| import numpy as np | |
| if isinstance(value, (np.integer,)): | |
| return int(value) | |
| if isinstance(value, (np.floating,)): | |
| return float(value) | |
| if isinstance(value, (np.bool_,)): | |
| return bool(value) | |
| if isinstance(value, float) and (value != value): # NaN | |
| return "" | |
| return value | |
| def _sanitize_row(row: list) -> list: | |
| """Applique _sanitize_for_gspread sur chaque élément d'une ligne.""" | |
| return [_sanitize_for_gspread(v) for v in row] | |
| def _find_row_index_in_sheet(all_values: list, col_idx_1based: int, search_value: str): | |
| """ | |
| Recherche la ligne (1-based) dans all_values dont la colonne col_idx_1based | |
| correspond à search_value. Retourne None si introuvable. | |
| """ | |
| for i, row in enumerate(all_values[1:], start=2): | |
| if str(row[col_idx_1based - 1]) == search_value: | |
| return i | |
| return None | |
| # ============================================================================ | |
| # CSS | |
| # ============================================================================ | |
| def apply_checkup_loans_styles(): | |
| st.markdown( | |
| """ | |
| <style> | |
| /* ── Wrapper ─────────────────────────────────────────────────────── */ | |
| #checkup-loans-module { | |
| padding: 1rem; margin: 0 auto; max-width: 100%; | |
| } | |
| /* ── Cartes prêt ──────────────────────────────────────────────────── */ | |
| #checkup-loans-module .checkup-loan-card { | |
| background: rgba(22,27,34,.6); | |
| border: 1px solid rgba(48,54,61,.8); | |
| border-left: 4px solid #58a6ff; | |
| border-radius: 8px; padding: 20px; margin: 1rem 0; | |
| box-shadow: 0 2px 4px rgba(0,0,0,.3); | |
| } | |
| #checkup-loans-module .checkup-loan-card h3 { | |
| color: #58a6ff !important; margin-bottom: 1rem; font-size: 1.2rem; | |
| } | |
| /* ── Tableau comparatif ───────────────────────────────────────────── */ | |
| #checkup-loans-module .checkup-comparison-table { | |
| background: rgba(22,27,34,.4); | |
| border: 1px solid rgba(88,166,255,.3); | |
| border-radius: 8px; padding: 16px; margin: 1.5rem 0; | |
| } | |
| /* ── Alerte blocage ───────────────────────────────────────────────── */ | |
| #checkup-loans-module .checkup-blocked-alert { | |
| background: rgba(231,76,60,.15); | |
| border: 2px solid rgba(231,76,60,.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; | |
| } | |
| /* ── Avertissements ───────────────────────────────────────────────── */ | |
| #checkup-loans-module .checkup-warning-box { | |
| background: rgba(243,156,18,.12); | |
| border: 1px solid rgba(243,156,18,.4); | |
| border-left: 4px solid #f39c12; | |
| border-radius: 6px; padding: 16px; margin: 1rem 0; | |
| } | |
| /* ── Badges statut ────────────────────────────────────────────────── */ | |
| #checkup-loans-module .checkup-status-badge { | |
| display: inline-block; padding: 6px 14px; border-radius: 20px; | |
| font-size: .8rem; font-weight: 600; | |
| letter-spacing: .5px; text-transform: uppercase; | |
| } | |
| #checkup-loans-module .checkup-status-actif { | |
| background: rgba(84,189,75,.2); color: #54bd4b; | |
| border: 1px solid rgba(84,189,75,.4); | |
| } | |
| #checkup-loans-module .checkup-status-updated { | |
| background: rgba(243,156,18,.2); color: #f39c12; | |
| border: 1px solid rgba(243,156,18,.4); | |
| } | |
| /* ── Badges source ────────────────────────────────────────────────── */ | |
| #checkup-loans-module .badge-master { | |
| background: rgba(88,166,255,.15); color: #58a6ff; | |
| border: 1px solid rgba(88,166,255,.4); | |
| padding: 3px 10px; border-radius: 12px; | |
| font-size: .75rem; font-weight: 600; | |
| } | |
| #checkup-loans-module .badge-update { | |
| background: rgba(163,113,247,.15); color: #a371f7; | |
| border: 1px solid rgba(163,113,247,.4); | |
| padding: 3px 10px; border-radius: 12px; | |
| font-size: .75rem; font-weight: 600; | |
| } | |
| /* ── Séparateur ───────────────────────────────────────────────────── */ | |
| #checkup-loans-module .checkup-section-divider { | |
| height: 2px; | |
| background: linear-gradient( | |
| 90deg, transparent 0%, | |
| rgba(88,166,255,.4) 50%, | |
| transparent 100% | |
| ); | |
| margin: 2.5rem 0; | |
| } | |
| /* ── Métriques ────────────────────────────────────────────────────── */ | |
| #checkup-loans-module [data-testid="stMetric"] { | |
| background: rgba(22,27,34,.6); | |
| border: 1px solid rgba(48,54,61,.8); | |
| border-radius: 6px; padding: 16px; | |
| box-shadow: 0 1px 3px rgba(0,0,0,.4); | |
| } | |
| #checkup-loans-module [data-testid="stMetric"] label { | |
| color: #8b949e !important; font-size: .75rem !important; | |
| font-weight: 500 !important; | |
| text-transform: uppercase; letter-spacing: .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 | |
| # ============================================================================ | |
| def show_check_up_loans(client, sheet_name): | |
| """ | |
| Point d'entrée du module Check-Up Prêts. | |
| Accepte les prêts ACTIF issus de Prets_Master ET de Prets_Update. | |
| """ | |
| 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) | |
| df_clients = get_cached_data(client, sheet_name, "Clients_KYC") | |
| df_prets_master = get_cached_data(client, sheet_name, "Prets_Master") | |
| df_remboursements = get_cached_data(client, sheet_name, "Remboursements") | |
| try: | |
| df_prets_update = get_cached_data(client, sheet_name, "Prets_Update") | |
| except Exception: | |
| df_prets_update = pd.DataFrame() | |
| try: | |
| df_garants = get_cached_data(client, sheet_name, "Garants_KYC") | |
| except Exception: | |
| 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 | |
| if df_clients.empty: | |
| st.error("🛑 Aucun client trouvé dans la base KYC.") | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| return | |
| if df_prets_master.empty and df_prets_update.empty: | |
| st.warning("Aucun prêt n'a encore été octroyé.") | |
| st.info("Utilisez le module *Moteur Financier* 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") | |
| 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 | |
| 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 — DÉTECTION UNIFIÉE DES PRÊTS ACTIFS (MASTER + UPDATE) | |
| # ========================================================================= | |
| st.subheader("Étape 2 : Identifier le prêt à modifier") | |
| loans_pool = [] # liste de dict : {source, row, display_id, root_id} | |
| # — Prêts ACTIF depuis Prets_Master | |
| if not df_prets_master.empty: | |
| mask_master = ( | |
| (df_prets_master["ID_Client"] == client_id) | |
| & (df_prets_master["Statut"] == "ACTIF") | |
| ) | |
| for _, row in df_prets_master[mask_master].iterrows(): | |
| root_id = str(row["ID_Pret"]) | |
| # Ne pas proposer un prêt Master si une version ACTIF existe déjà | |
| # dans Prets_Update pour ce même root_id (éviter le doublon) | |
| if _get_latest_active_from_update(df_prets_update, root_id) is None: | |
| loans_pool.append( | |
| { | |
| "source": "MASTER", | |
| "row": row, | |
| "display_id": root_id, | |
| "root_id": root_id, | |
| } | |
| ) | |
| # — Prêts ACTIF depuis Prets_Update (dernière version uniquement par root_id) | |
| if not df_prets_update.empty: | |
| mask_update = ( | |
| (df_prets_update["ID_Client"] == client_id) | |
| & (df_prets_update["Statut"] == "ACTIF") | |
| ) | |
| active_updates = df_prets_update[mask_update].copy() | |
| if not active_updates.empty: | |
| active_updates["_version_int"] = active_updates["Version"].apply( | |
| _safe_version_int | |
| ) | |
| # Conserver uniquement la version max par root_id | |
| idx_max = active_updates.groupby("ID_Pret")["_version_int"].idxmax() | |
| for _, row in active_updates.loc[idx_max].iterrows(): | |
| root_id = str(row["ID_Pret"]) | |
| display_id = str(row["ID_Pret_Updated"]) | |
| loans_pool.append( | |
| { | |
| "source": "UPDATE", | |
| "row": row, | |
| "display_id": display_id, | |
| "root_id": root_id, | |
| } | |
| ) | |
| # — Aucun prêt actif trouvé | |
| if not loans_pool: | |
| st.markdown('<div class="checkup-blocked-alert">', unsafe_allow_html=True) | |
| st.markdown( | |
| f""" | |
| ### ⚠️ Aucun prêt actif trouvé | |
| Le client **{client_info['Nom_Complet']}** ne possède aucun prêt | |
| avec le statut **ACTIF** (ni dans *Prets_Master*, ni dans *Prets_Update*). | |
| **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 | |
| # — Sélection du prêt dans la pool | |
| if len(loans_pool) == 1: | |
| selected_entry = loans_pool[0] | |
| source_label = ( | |
| "Prêt original" | |
| if selected_entry["source"] == "MASTER" | |
| else f"Version V{selected_entry['row'].get('Version', '?')}" | |
| ) | |
| st.info( | |
| f"**1 prêt actif détecté** — Sélection automatique : " | |
| f"**{selected_entry['display_id']}** ({source_label})" | |
| ) | |
| else: | |
| def _make_label(entry): | |
| row = entry["row"] | |
| montant = int(clean_currency_value(row["Montant_Capital"])) | |
| tag = ( | |
| "🔵 Original" | |
| if entry["source"] == "MASTER" | |
| else f"🟣 V{row.get('Version', '?')}" | |
| ) | |
| return ( | |
| f"{entry['display_id']} | {tag} | " | |
| f"{montant:,} XOF | Échéance : {row.get('Date_Fin', 'N/A')}" | |
| ).replace(",", " ") | |
| labels = [_make_label(e) for e in loans_pool] | |
| selected_label = st.selectbox( | |
| "Sélectionnez le prêt à modifier", | |
| labels, | |
| help=( | |
| "🔵 = prêt original (Prets_Master) • " | |
| "🟣 = version mise à jour (Prets_Update)" | |
| ), | |
| ) | |
| selected_entry = loans_pool[labels.index(selected_label)] | |
| # Extraction des variables de contexte | |
| selected_loan = selected_entry["row"] | |
| loan_source = selected_entry["source"] # "MASTER" ou "UPDATE" | |
| root_id = selected_entry["root_id"] # ID original, ex. "P001" | |
| display_id = selected_entry["display_id"] # ID affiché, ex. "P001-V2" | |
| # ========================================================================= | |
| # ÉTAPE 4 — VÉRIFICATION BLOQUANTE : REMBOURSEMENTS (sur ID_Pret racine) | |
| # ========================================================================= | |
| if not df_remboursements.empty: | |
| df_payments = df_remboursements[ | |
| df_remboursements["ID_Pret"].astype(str) == root_id | |
| ] | |
| if not df_payments.empty: | |
| st.markdown('<div class="checkup-blocked-alert">', unsafe_allow_html=True) | |
| st.markdown("### ⛔ MODIFICATION IMPOSSIBLE") | |
| st.markdown( | |
| f""" | |
| Le prêt **{display_id}** (racine : `{root_id}`) a déjà fait l'objet | |
| de **{len(df_payments)} remboursement(s)**. | |
| **Remboursements enregistrés :** | |
| """ | |
| ) | |
| for _, pmt in df_payments.head(5).iterrows(): | |
| montant_pmt = int(clean_currency_value(pmt.get("Montant_Verse", 0))) | |
| st.markdown( | |
| f"- **{pmt.get('Date_Paiement', 'N/A')}** : " | |
| f"{montant_pmt:,} 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 n'est autorisée | |
| qu'**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 | |
| # Accès autorisé | |
| source_badge_html = ( | |
| "<span class='badge-master'>Prêt Original</span>" | |
| if loan_source == "MASTER" | |
| else f"<span class='badge-update'>Version V{selected_loan.get('Version', '?')}</span>" | |
| ) | |
| st.markdown( | |
| f"Aucun remboursement détecté — **Modification autorisée** " | |
| f"pour **{display_id}** {source_badge_html}", | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown('<div class="checkup-section-divider"></div>', unsafe_allow_html=True) | |
| # ========================================================================= | |
| # ÉTAPE 5 — INFORMATIONS ACTUELLES DU PRÊT | |
| # ========================================================================= | |
| 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 {display_id}") | |
| montant_capital_actuel = clean_currency_value(selected_loan["Montant_Capital"]) | |
| taux_hebdo_actuel = clean_taux_value(selected_loan["Taux_Hebdo"]) | |
| 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"]) | |
| 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} %" if taux_endettement_actuel > 0 else "N/A", | |
| ) | |
| 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"]) | |
| st.markdown("---") | |
| st.markdown(f"**Client :** {selected_loan['Nom_Complet']}") | |
| st.markdown(f"**Motif :** {selected_loan.get('Motif', 'Non renseigné')}") | |
| # Garant | |
| garant_id_actuel = str(selected_loan.get("ID_Garant", "")).strip() | |
| if garant_id_actuel and not df_garants.empty: | |
| g_rows = df_garants[df_garants["ID_Garant"].astype(str) == garant_id_actuel] | |
| if not g_rows.empty: | |
| st.markdown( | |
| f"**Garant :** {g_rows.iloc[0]['Nom_Complet']} (ID : {garant_id_actuel})" | |
| ) | |
| # Version (uniquement si source UPDATE) | |
| if loan_source == "UPDATE": | |
| st.markdown( | |
| f"**Version courante :** " | |
| f"<span class='badge-update'>V{selected_loan.get('Version', '?')}</span>", | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown( | |
| f"<span class='checkup-status-badge checkup-status-actif'>" | |
| f"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 sont recalculés automatiquement.") | |
| # Initialisation des dates personnalisées en session state | |
| if "dates_perso_update" not in st.session_state: | |
| dates_str = str(selected_loan.get("Dates_Versements", "")).strip() | |
| if dates_str: | |
| try: | |
| dates_list = [ | |
| datetime.strptime(d.strip(), "%d/%m/%Y").date() | |
| for d in dates_str.split(";") | |
| if d.strip() | |
| ] | |
| st.session_state.dates_perso_update = dates_list | |
| except Exception: | |
| st.session_state.dates_perso_update = [date.today() + timedelta(weeks=2)] | |
| else: | |
| st.session_state.dates_perso_update = [date.today() + timedelta(weeks=2)] | |
| # — Ligne 1 : Motif / Type / Moyen ───────────────────────────────────── | |
| col_motif, col_type, col_moyen = st.columns(3) | |
| with col_motif: | |
| motif_actuel = selected_loan.get("Motif", "Autre") | |
| default_motif_idx = MOTIFS_PRET.index(motif_actuel) if motif_actuel in MOTIFS_PRET else 0 | |
| nouveau_motif = st.selectbox("Motif du prêt", MOTIFS_PRET, index=default_motif_idx) | |
| with col_type: | |
| 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: | |
| moyens_list = ["Wave", "Orange Money", "Virement"] | |
| moyen_actuel = selected_loan.get("Moyen_Transfert", "Wave") | |
| default_moyen_idx = moyens_list.index(moyen_actuel) if moyen_actuel in moyens_list else 0 | |
| nouveau_moyen = st.selectbox("Moyen de transfert", moyens_list, index=default_moyen_idx) | |
| # — Ligne 2 : Paramètres financiers ──────────────────────────────────── | |
| col1, col2, col3 = st.columns(3) | |
| nouveau_montant = col1.number_input( | |
| "Montant Capital (XOF)", | |
| min_value=10_000, | |
| value=int(montant_capital_actuel), | |
| step=10_000, | |
| ) | |
| nouveau_taux = col2.number_input( | |
| "Taux Hebdo (%)", | |
| min_value=0.1, | |
| value=float(taux_hebdo_actuel), | |
| step=0.1, | |
| ) | |
| # ========================================================================= | |
| # CALCULS AUTOMATIQUES SELON LE TYPE | |
| # ========================================================================= | |
| nouveau_montant_versement = 0.0 | |
| nouveau_montant_total = 0.0 | |
| nouveau_cout_credit = 0.0 | |
| nouveau_nb_versements = 0 | |
| nouvelle_duree_semaines = 0.0 | |
| nouvelles_dates_versements = [] | |
| try: | |
| nouvelle_date_debut = datetime.strptime( | |
| str(selected_loan["Date_Deblocage"]), "%d/%m/%Y" | |
| ).date() | |
| except Exception: | |
| nouvelle_date_debut = date.today() | |
| nouvelle_date_fin = nouvelle_date_debut | |
| # ── IN FINE ────────────────────────────────────────────────────────────── | |
| if nouveau_type_code == "IN_FINE": | |
| default_dur = ( | |
| int(duree_semaines_actuel) | |
| if selected_loan["Type_Pret"] == "IN_FINE" | |
| else 8 | |
| ) | |
| nouvelle_duree_semaines = col3.number_input( | |
| "Durée (semaines)", min_value=1, max_value=104, value=default_dur | |
| ) | |
| nouvelle_date_fin = nouvelle_date_debut + timedelta(weeks=int(nouvelle_duree_semaines)) | |
| 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 | |
| st.markdown("### Simulation des nouveaux montants") | |
| r1, r2, r3 = st.columns(3) | |
| r1.metric("Versement unique", f"{int(nouveau_montant_versement):,} XOF".replace(",", " ")) | |
| r2.metric("Coût du crédit", f"{int(nouveau_cout_credit):,} XOF".replace(",", " ")) | |
| r3.metric("Montant Total", f"{int(nouveau_montant_total):,} XOF".replace(",", " "), | |
| delta=f"+{int(nouveau_cout_credit):,}".replace(",", " ")) | |
| # ── MENSUEL — INTÉRÊTS ─────────────────────────────────────────────────── | |
| elif nouveau_type_code == "MENSUEL_INTERETS": | |
| default_dur = ( | |
| int(duree_semaines_actuel / 4.33) | |
| if selected_loan["Type_Pret"] == "MENSUEL_INTERETS" | |
| else 12 | |
| ) | |
| nouvelle_duree_mois = col3.number_input("Durée (mois)", min_value=1, max_value=60, value=default_dur) | |
| nouvelle_date_fin = nouvelle_date_debut + timedelta(days=int(nouvelle_duree_mois * 30)) | |
| nouvelle_duree_semaines = nouvelle_duree_mois * 4.33 | |
| taux_mensuel = (nouveau_taux / 100) * 4.33 | |
| interet_mensuel = nouveau_montant * taux_mensuel | |
| montant_final_mois = nouveau_montant + interet_mensuel | |
| nouveau_montant_versement = 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) | |
| st.markdown("### Simulation des nouveaux montants") | |
| r1, r2 = st.columns(2) | |
| r1.metric("Intérêts mensuels", f"{int(interet_mensuel):,} XOF".replace(",", " ")) | |
| r2.metric("Dernier versement", f"{int(montant_final_mois):,} XOF".replace(",", " ")) | |
| r3, r4 = st.columns(2) | |
| r3.metric("Coût du crédit", f"{int(nouveau_cout_credit):,} XOF".replace(",", " ")) | |
| r4.metric("Montant Total", f"{int(nouveau_montant_total):,} XOF".replace(",", " "), | |
| delta=f"+{int(nouveau_cout_credit):,}".replace(",", " ")) | |
| # ── MENSUEL — CONSTANT ─────────────────────────────────────────────────── | |
| elif nouveau_type_code == "MENSUEL_CONSTANT": | |
| default_dur = ( | |
| int(duree_semaines_actuel / 4.33) | |
| if selected_loan["Type_Pret"] == "MENSUEL_CONSTANT" | |
| else 12 | |
| ) | |
| nouvelle_duree_mois = col3.number_input("Durée (mois)", min_value=1, max_value=60, value=default_dur) | |
| nouvelle_date_fin = nouvelle_date_debut + timedelta(days=int(nouvelle_duree_mois * 30)) | |
| nouvelle_duree_semaines = nouvelle_duree_mois * 4.33 | |
| taux_mensuel = (nouveau_taux / 100) * 4.33 | |
| mensualite = ( | |
| (nouveau_montant * taux_mensuel) / (1 - (1 + taux_mensuel) ** (-nouvelle_duree_mois)) | |
| if taux_mensuel > 0 | |
| else 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) | |
| st.markdown("### Simulation des nouveaux montants") | |
| r1, r2, r3 = st.columns(3) | |
| r1.metric("Mensualité constante", f"{int(mensualite):,} XOF".replace(",", " ")) | |
| r2.metric("Coût du crédit", f"{int(nouveau_cout_credit):,} XOF".replace(",", " ")) | |
| r3.metric("Montant Total", f"{int(nouveau_montant_total):,} XOF".replace(",", " "), | |
| delta=f"+{int(nouveau_cout_credit):,}".replace(",", " ")) | |
| # ── HEBDOMADAIRE ───────────────────────────────────────────────────────── | |
| elif nouveau_type_code == "HEBDOMADAIRE": | |
| default_dur = ( | |
| int(duree_semaines_actuel) | |
| if selected_loan["Type_Pret"] == "HEBDOMADAIRE" | |
| else 12 | |
| ) | |
| nouvelle_duree_semaines = col3.number_input( | |
| "Durée (semaines)", min_value=1, max_value=104, value=default_dur | |
| ) | |
| nouvelle_date_fin = nouvelle_date_debut + timedelta(weeks=int(nouvelle_duree_semaines)) | |
| r_hebdo = nouveau_taux / 100 | |
| hebdo = ( | |
| (nouveau_montant * r_hebdo) / (1 - (1 + r_hebdo) ** (-nouvelle_duree_semaines)) | |
| if r_hebdo > 0 | |
| else nouveau_montant / nouvelle_duree_semaines | |
| ) | |
| nouveau_montant_versement = hebdo | |
| nouveau_montant_total = hebdo * nouvelle_duree_semaines | |
| nouveau_cout_credit = nouveau_montant_total - nouveau_montant | |
| nouveau_nb_versements = int(nouvelle_duree_semaines) | |
| st.markdown("### Simulation des nouveaux montants") | |
| r1, r2, r3 = st.columns(3) | |
| r1.metric("Versement Hebdo", f"{int(hebdo):,} XOF".replace(",", " ")) | |
| r2.metric("Coût du crédit", f"{int(nouveau_cout_credit):,} XOF".replace(",", " ")) | |
| r3.metric("Montant Total", f"{int(nouveau_montant_total):,} XOF".replace(",", " "), | |
| delta=f"+{int(nouveau_cout_credit):,}".replace(",", " ")) | |
| # ── PERSONNALISÉ ───────────────────────────────────────────────────────── | |
| else: | |
| st.info("Configurez les dates de versement ci-dessous.") | |
| st.markdown("**Dates de versement :**") | |
| col_add, _ = st.columns([1, 4]) | |
| if col_add.button("+ Ajouter une date", key="add_date_update"): | |
| last = st.session_state.dates_perso_update[-1] | |
| st.session_state.dates_perso_update.append(last + timedelta(weeks=1)) | |
| st.rerun() | |
| today = date.today() | |
| nouvelles_dates_versements = [] | |
| for idx, dt in enumerate(st.session_state.dates_perso_update): | |
| safe_dt = max(dt, today) | |
| col_d, col_x = st.columns([4, 1]) | |
| if dt < today and idx == 0: | |
| st.warning( | |
| f"⚠️ Date originale ({dt.strftime('%d/%m/%Y')}) passée — " | |
| f"ajustée à aujourd'hui ({today.strftime('%d/%m/%Y')})." | |
| ) | |
| new_date = col_d.date_input( | |
| f"Échéance {idx + 1}", | |
| value=safe_dt, | |
| key=f"d_update_{idx}", | |
| min_value=today, | |
| ) | |
| 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 | |
| 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.0, 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 | |
| st.markdown("### Simulation des nouveaux montants") | |
| r1, r2 = st.columns(2) | |
| r1.metric("Moyenne / Versement", f"{int(nouveau_montant_versement):,} XOF".replace(",", " ")) | |
| r2.metric("Durée estimée", f"{int(nouvelle_duree_semaines)} sem") | |
| r3, r4 = st.columns(2) | |
| r3.metric("Coût du crédit", f"{int(nouveau_cout_credit):,} XOF".replace(",", " ")) | |
| r4.metric("Montant Total", f"{int(nouveau_montant_total):,} XOF".replace(",", " "), | |
| delta=f"+{int(nouveau_cout_credit):,}".replace(",", " ")) | |
| # ========================================================================= | |
| # TAUX D'ENDETTEMENT | |
| # ========================================================================= | |
| nouveau_taux_endettement = calculer_taux_endettement( | |
| nouveau_type_code, | |
| nouveau_montant_versement, | |
| nouveau_nb_versements, | |
| nouvelle_duree_semaines, | |
| client_info["Revenus_Mensuels"], | |
| ) | |
| if nouveau_taux_endettement > 0: | |
| st.info(f"**Nouveau taux d'endettement calculé** : {nouveau_taux_endettement:.2f} %") | |
| if nouveau_taux_endettement > 33: | |
| st.warning( | |
| f"⚠️ Le taux d'endettement ({nouveau_taux_endettement:.2f} %) " | |
| f"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") | |
| 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 | |
| try: | |
| diff_jours_fin = ( | |
| nouvelle_date_fin | |
| - datetime.strptime(str(selected_loan["Date_Fin"]), "%d/%m/%Y").date() | |
| ).days | |
| diff_jours_str = f"{diff_jours_fin:+} jours" if diff_jours_fin != 0 else "=" | |
| except Exception: | |
| diff_jours_str = "N/A" | |
| 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", | |
| 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} %", | |
| 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 "=", | |
| diff_jours_str, | |
| ], | |
| } | |
| st.markdown('<div class="checkup-comparison-table">', unsafe_allow_html=True) | |
| st.dataframe(pd.DataFrame(comparison_data), hide_index=True, use_container_width=True) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| # ========================================================================= | |
| # WARNINGS | |
| # ========================================================================= | |
| warnings = [] | |
| if cout_credit_actuel > 0 and diff_cout_credit > cout_credit_actuel * 0.15: | |
| pct = diff_cout_credit / cout_credit_actuel * 100 | |
| warnings.append( | |
| f"⚠️ Le coût du crédit augmente de **{int(diff_cout_credit):,} XOF** " | |
| f"(+{pct:.1f} %)".replace(",", " ") | |
| ) | |
| if diff_duree > 8: | |
| warnings.append(f"⚠️ La durée est prolongée de **{int(diff_duree)} semaines**.") | |
| if (nouvelle_date_fin - nouvelle_date_debut).days > 180: | |
| warnings.append( | |
| f"⚠️ La nouvelle échéance ({nouvelle_date_fin.strftime('%d/%m/%Y')}) " | |
| f"dépasse **6 mois**." | |
| ) | |
| if nouveau_montant > montant_capital_actuel: | |
| diff_cap = int(nouveau_montant - montant_capital_actuel) | |
| warnings.append( | |
| f"ℹ️ Le capital augmente de **{diff_cap:,} XOF**.".replace(",", " ") | |
| ) | |
| if warnings: | |
| st.markdown('<div class="checkup-warning-box">', unsafe_allow_html=True) | |
| st.markdown("### ⚠️ Points d'attention") | |
| for w in warnings: | |
| st.markdown(f"- {w}") | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| # ========================================================================= | |
| # ANALYSE DE SOLVABILITÉ | |
| # ========================================================================= | |
| st.markdown('<div class="checkup-section-divider"></div>', unsafe_allow_html=True) | |
| st.subheader("Nouvelle analyse de solvabilité") | |
| 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']}'>" | |
| f"{analyse['statut']}</span>", | |
| unsafe_allow_html=True, | |
| ) | |
| st.info(analyse["message"]) | |
| with st.expander("Détails financiers"): | |
| st.markdown(analyse["details"]) | |
| # ========================================================================= | |
| # TABLEAU D'AMORTISSEMENT | |
| # ========================================================================= | |
| st.markdown('<div class="checkup-section-divider"></div>', unsafe_allow_html=True) | |
| st.subheader("Nouveau tableau d'échéances") | |
| 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 if nouveau_type_code == "PERSONNALISE" else None | |
| ), | |
| ) | |
| 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 archivera la version courante " | |
| "(statut → **UPDATED**) et créera une nouvelle version (statut → **ACTIF**)." | |
| ) | |
| with st.form("form_update_loan"): | |
| st.markdown("### Confirmation") | |
| col_c1, col_c2 = st.columns(2) | |
| with col_c1: | |
| st.markdown(f"**Prêt à archiver :** {display_id} *(source : {loan_source})*") | |
| st.markdown(f"**Client :** {client_info['Nom_Complet']}") | |
| st.markdown( | |
| f"**Nouveau montant total :** " | |
| f"{int(nouveau_montant_total):,} XOF".replace(",", " ") | |
| ) | |
| with col_c2: | |
| 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')}") | |
| 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é…" | |
| ), | |
| ) | |
| submit_update = st.form_submit_button( | |
| "VALIDER LA MISE À JOUR", use_container_width=True | |
| ) | |
| # ========================================================================= | |
| # TRAITEMENT À LA VALIDATION | |
| # ========================================================================= | |
| if submit_update: | |
| try: | |
| now_str = datetime.now().strftime("%d-%m-%Y %H:%M:%S") | |
| # ------------------------------------------------------------------ | |
| # A — Archivage de l'ancienne version | |
| # ------------------------------------------------------------------ | |
| with st.spinner("Archivage de l'ancienne version en cours…"): | |
| if loan_source == "MASTER": | |
| # Prets_Master → ligne root_id → statut UPDATED | |
| ws_master = sh.worksheet("Prets_Master") | |
| all_values = ws_master.get_all_values() | |
| header = all_values[0] | |
| col_statut_idx = header.index("Statut") + 1 | |
| col_id_pret_idx = header.index("ID_Pret") + 1 | |
| col_date_upd_idx = ( | |
| header.index("Date_Update") + 1 | |
| if "Date_Update" in header | |
| else None | |
| ) | |
| row_index = _find_row_index_in_sheet( | |
| all_values, col_id_pret_idx, root_id | |
| ) | |
| if row_index is None: | |
| st.error(f"❌ Prêt {root_id} introuvable dans Prets_Master.") | |
| st.stop() | |
| ws_master.update_cell(row_index, col_statut_idx, "UPDATED") | |
| if col_date_upd_idx: | |
| ws_master.update_cell(row_index, col_date_upd_idx, now_str) | |
| else: | |
| # Prets_Update → ligne display_id (ID_Pret_Updated) → statut UPDATED | |
| ws_update = sh.worksheet("Prets_Update") | |
| all_values = ws_update.get_all_values() | |
| header = all_values[0] | |
| col_statut_idx = header.index("Statut") + 1 | |
| col_id_updated_idx = header.index("ID_Pret_Updated") + 1 | |
| col_date_modif_idx = ( | |
| header.index("Date_Modification") + 1 | |
| if "Date_Modification" in header | |
| else None | |
| ) | |
| row_index = _find_row_index_in_sheet( | |
| all_values, col_id_updated_idx, display_id | |
| ) | |
| if row_index is None: | |
| st.error( | |
| f"❌ Ligne {display_id} introuvable dans Prets_Update." | |
| ) | |
| st.stop() | |
| ws_update.update_cell(row_index, col_statut_idx, "UPDATED") | |
| if col_date_modif_idx: | |
| ws_update.update_cell(row_index, col_date_modif_idx, now_str) | |
| time.sleep(1) | |
| st.success(f"{display_id} → statut **UPDATED**") | |
| # ------------------------------------------------------------------ | |
| # B — Création de la nouvelle version dans Prets_Update | |
| # ------------------------------------------------------------------ | |
| with st.spinner("Création de la nouvelle version…"): | |
| # Récupération (ou création) de la feuille Prets_Update | |
| try: | |
| ws_pu = sh.worksheet("Prets_Update") | |
| except Exception: | |
| ws_pu = sh.add_worksheet(title="Prets_Update", rows=1000, cols=30) | |
| ws_pu.append_row(PRETS_UPDATE_HEADERS) | |
| time.sleep(1) | |
| # Lecture fraîche pour calculer la prochaine version | |
| df_pu_fresh = pd.DataFrame(ws_pu.get_all_records()) | |
| next_version = _get_next_version(df_pu_fresh, root_id) | |
| new_id_updated = f"{root_id}-V{next_version}" | |
| # Dates versements (format string) | |
| dates_versements_str = ( | |
| ";".join(d.strftime("%d/%m/%Y") for d in nouvelles_dates_versements) | |
| if nouvelles_dates_versements | |
| else "" | |
| ) | |
| new_row = [ | |
| new_id_updated, # ID_Pret_Updated | |
| root_id, # ID_Pret (racine — inchangée) | |
| next_version, # Version | |
| now_str, # 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 | |
| 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 | |
| now_str, # Date_Creation | |
| commentaire_modification, # Commentaire_Modification | |
| ] | |
| ws_pu.append_row(_sanitize_row(new_row)) | |
| time.sleep(1) | |
| st.success( | |
| f"Nouvelle version créée : **{new_id_updated}** " | |
| f"(V{next_version}) → statut **ACTIF**" | |
| ) | |
| # ------------------------------------------------------------------ | |
| # C — Session state | |
| # ------------------------------------------------------------------ | |
| garant_data = None | |
| if garant_id_actuel and not df_garants.empty: | |
| g_rows = df_garants[df_garants["ID_Garant"].astype(str) == garant_id_actuel] | |
| if not g_rows.empty: | |
| garant_data = g_rows.iloc[0].to_dict() | |
| st.session_state.loan_updated = True | |
| st.session_state.new_loan_id = new_id_updated | |
| st.session_state.new_loan_data = { | |
| "ID_Pret": new_id_updated, | |
| "ID_Pret_Source": root_id, | |
| "Version": next_version, | |
| "Montant_Capital": nouveau_montant, | |
| "Montant_Total": round(nouveau_montant_total), | |
| "Taux_Hebdo": nouveau_taux, | |
| "Duree_Semaines": nouvelle_duree_semaines, | |
| "Montant_Versement": round(nouveau_montant_versement), | |
| "Cout_Credit": round(nouveau_cout_credit), | |
| "Nb_Versements": nouveau_nb_versements, | |
| "Motif": nouveau_motif, | |
| "Date_Deblocage": selected_loan["Date_Deblocage"], | |
| "Date_Fin": nouvelle_date_fin.strftime("%d/%m/%Y"), | |
| "Type_Pret": nouveau_type_code, | |
| "Mention_Update": ( | |
| f"Mise à jour du contrat n° {root_id} (Version {next_version})" | |
| ), | |
| } | |
| st.session_state.new_client_data = client_info.to_dict() | |
| st.session_state.new_garant_data = garant_data | |
| st.session_state.new_df_amort = df_amort_nouveau.copy() | |
| st.cache_data.clear() | |
| st.success("Mise à jour effectuée avec succès !") | |
| except Exception as e: | |
| st.error(f"❌ Erreur lors de la mise à jour : {e}") | |
| st.exception(e) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| return | |
| # ========================================================================= | |
| # GÉNÉRATION DES DOCUMENTS PDF | |
| # ========================================================================= | |
| 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}**") | |
| 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 | |
| col_pdf1, col_pdf2, col_pdf3, col_reset = st.columns(4) | |
| 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, | |
| ) | |
| 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, | |
| ) | |
| with col_pdf3: | |
| if garant_data_pdf: | |
| 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") | |
| with col_reset: | |
| if st.button("Nouvelle Mise à Jour", use_container_width=True, type="primary"): | |
| for key in [ | |
| "loan_updated", "new_loan_id", "new_loan_data", | |
| "new_client_data", "new_garant_data", "new_df_amort", | |
| "dates_perso_update", | |
| ]: | |
| st.session_state.pop(key, None) | |
| st.cache_data.clear() | |
| st.rerun() | |
| # Récapitulatif | |
| st.markdown("---") | |
| with st.expander("Récapitulatif de la mise à jour"): | |
| col_r1, col_r2 = st.columns(2) | |
| with col_r1: | |
| st.markdown("### Ancienne version") | |
| st.markdown(f"**ID :** {display_id}") | |
| st.markdown("**Statut :** UPDATED *(archivé)*") | |
| st.markdown( | |
| f"**Montant :** {int(montant_total_actuel):,} XOF".replace(",", " ") | |
| ) | |
| with col_r2: | |
| st.markdown(f"### Nouvelle version (V{loan_data_pdf['Version']})") | |
| st.markdown(f"**ID :** {st.session_state.new_loan_id}") | |
| st.markdown("**Statut :** ACTIF") | |
| st.markdown( | |
| f"**Montant :** " | |
| f"{int(loan_data_pdf['Montant_Total']):,} XOF".replace(",", " ") | |
| ) | |
| delta = loan_data_pdf["Montant_Total"] - montant_total_actuel | |
| st.markdown(f"**Différence :** {int(delta):+,} XOF".replace(",", " ")) | |
| st.markdown("</div>", unsafe_allow_html=True) |