Vortex-Flux / src /modules /Check_Up_Loans.py
klydekushy's picture
Update src/modules/Check_Up_Loans.py
89eec79 verified
# -*- 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
# ============================================================================
@st.cache_data(ttl=600)
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)