Spaces:
Running
Running
File size: 59,453 Bytes
cfd6d98 10f86ec bb7f612 e9b6ddb bb7f612 10f86ec 9e6ea19 6b2ef27 10f86ec c7fecbd 9e6ea19 c7fecbd 9e6ea19 10f86ec 6b2ef27 10f86ec ededd7b 10f86ec cf7bccf ededd7b cf7bccf 79cf76b cf7bccf ededd7b cf7bccf ededd7b cf7bccf ededd7b cf7bccf ededd7b cf7bccf 5a63cae cf7bccf 1e01ea0 cf7bccf 5a63cae cf7bccf 5a63cae cf7bccf 5a63cae ededd7b cf7bccf ededd7b 839bc69 6b2ef27 839bc69 b5fbb97 cbbc6c2 2981f15 a3aefee 839bc69 6b2ef27 839bc69 ededd7b 839bc69 ededd7b 839bc69 ededd7b 839bc69 4eb58a2 ededd7b 4eb58a2 ededd7b 4eb58a2 ededd7b 4eb58a2 ededd7b 4eb58a2 ededd7b 4eb58a2 ededd7b 4eb58a2 6b2ef27 4eb58a2 6b2ef27 4eb58a2 6b2ef27 4eb58a2 6b2ef27 4eb58a2 ededd7b 4eb58a2 ededd7b 4eb58a2 ededd7b 4eb58a2 7879e34 ededd7b 7879e34 4eb58a2 ededd7b 4eb58a2 b5fbb97 6b2ef27 b5fbb97 e9b6ddb b5fbb97 4eb58a2 ededd7b 4eb58a2 e9b6ddb 4eb58a2 ededd7b 4eb58a2 ededd7b 4eb58a2 ededd7b 4eb58a2 79cf76b ededd7b 79cf76b ededd7b 0f863c7 79cf76b ededd7b 79cf76b ededd7b 79cf76b ededd7b 79cf76b e9b6ddb 79cf76b e9b6ddb 79cf76b e9b6ddb 79cf76b 0f863c7 e9b6ddb 0f863c7 79cf76b cfd6d98 79cf76b 6a0beb2 79cf76b cfd6d98 0f8ec9d 79cf76b 8ba5762 79cf76b ededd7b 79cf76b ededd7b 79cf76b ededd7b 79cf76b ededd7b 79cf76b ededd7b 79cf76b ededd7b 79cf76b ededd7b 79cf76b 10f86ec 8ba5762 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 |
# -*- coding: utf-8 -*-
import streamlit as st
import pandas as pd
from datetime import datetime, date, timedelta
import time
# IMPORT DES MODULES EXISTANTS
from Analytics.AnalyseFinance import (
analyser_capacite,
generer_tableau_amortissement,
calculer_taux_endettement,
clean_taux_value,
clean_currency_value,
clean_percentage_value # ✅ AJOUT de la nouvelle fonction
)
from DocumentGen.AutoPDFGeneration import generer_contrat_pret, generer_reconnaissance_dette, generer_contrat_caution
# ============================================================================
# GESTION DU CACHE (réutilisé depuis loans_engine.py)
# ============================================================================
@st.cache_data(ttl=600) # Le cache expire après 10 minutes
def get_cached_data(_client, sheet_name, worksheet_name):
"""Lit une feuille spécifique et la transforme en DataFrame avec cache."""
try:
sh = _client.open(sheet_name)
ws = sh.worksheet(worksheet_name)
return pd.DataFrame(ws.get_all_records())
except Exception as e:
st.error(f"Erreur lors de la lecture de {worksheet_name}: {e}")
return pd.DataFrame()
def refresh_data():
"""Fonction pour vider le cache manuellement."""
st.cache_data.clear()
st.rerun()
# ============================================================================
# ✅ CORRECTION 1 : FONCTIONS UTILITAIRES AVEC GESTION VIRGULE/POINT
# ============================================================================
def clean_currency_value(val):
"""Convertit une valeur monétaire (string ou float) en float propre"""
try:
if isinstance(val, (int, float)):
return float(val)
# Nettoyage des strings: "100 000 XOF" → 100000.0
cleaned = str(val).upper().replace("XOF", "").replace("FCFA", "").replace(" ", "")
# Ne touche PAS aux virgules pour les montants (on garde juste les chiffres)
cleaned = cleaned.replace(",", "").replace(".", "")
return float(cleaned) if cleaned else 0.0
except (ValueError, AttributeError):
return 0.0
def clean_percentage_value_local(val):
"""
✅ CORRECTION PROBLÈME 1 : Convertit un pourcentage avec virgule en float
Exemple : "54,94" → 54.94 (et non 5494.0)
"""
try:
if isinstance(val, (int, float)):
return float(val)
# Convertir en string et nettoyer
cleaned = str(val).strip().replace(" ", "").replace("%", "")
# ✅ CRUCIAL : Remplacer la virgule par un point (format Python)
cleaned = cleaned.replace(",", ".")
return float(cleaned) if cleaned else 0.0
except (ValueError, AttributeError):
return 0.0
# ============================================================================
# STYLES CSS SPÉCIFIQUES AU MODULE CHECK_UP_LOANS
# ============================================================================
def apply_checkup_loans_styles():
st.markdown("""
<style>
/* ========================================
WRAPPER D'ISOLATION DU MODULE
======================================== */
#checkup-loans-module {
padding: 1rem;
margin: 0 auto;
max-width: 100%;
}
/* ========================================
CARTES D'INFORMATION PRÊT
======================================== */
#checkup-loans-module .checkup-loan-card {
background: rgba(22, 27, 34, 0.6);
border: 1px solid rgba(48, 54, 61, 0.8);
border-left: 4px solid #58a6ff;
border-radius: 8px;
padding: 20px;
margin: 1rem 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
#checkup-loans-module .checkup-loan-card h3 {
color: #58a6ff !important;
margin-bottom: 1rem;
font-size: 1.2rem;
}
/* ========================================
TABLEAU COMPARATIF AVANT/APRÈS
======================================== */
#checkup-loans-module .checkup-comparison-table {
background: rgba(22, 27, 34, 0.4);
border: 1px solid rgba(88, 166, 255, 0.3);
border-radius: 8px;
padding: 16px;
margin: 1.5rem 0;
}
#checkup-loans-module .checkup-comparison-table table {
width: 100%;
border-collapse: collapse;
}
#checkup-loans-module .checkup-comparison-table th {
background: rgba(88, 166, 255, 0.2);
color: #58a6ff !important;
padding: 12px;
text-align: left;
font-weight: 700;
text-transform: uppercase;
font-size: 0.85rem;
}
#checkup-loans-module .checkup-comparison-table td {
padding: 10px 12px;
border-bottom: 1px solid rgba(48, 54, 61, 0.4);
}
/* ========================================
ALERTES DE BLOCAGE
======================================== */
#checkup-loans-module .checkup-blocked-alert {
background: rgba(231, 76, 60, 0.15);
border: 2px solid rgba(231, 76, 60, 0.6);
border-left: 6px solid #e74c3c;
border-radius: 8px;
padding: 20px;
margin: 2rem 0;
}
#checkup-loans-module .checkup-blocked-alert h3 {
color: #e74c3c !important;
margin-bottom: 1rem;
}
/* ========================================
WARNINGS (AVERTISSEMENTS)
======================================== */
#checkup-loans-module .checkup-warning-box {
background: rgba(243, 156, 18, 0.12);
border: 1px solid rgba(243, 156, 18, 0.4);
border-left: 4px solid #f39c12;
border-radius: 6px;
padding: 16px;
margin: 1rem 0;
}
/* ========================================
BOUTONS D'ACTION
======================================== */
#checkup-loans-module .checkup-action-btn button {
background: linear-gradient(135deg, rgba(88, 166, 255, 0.15) 0%, rgba(88, 166, 255, 0.08) 100%);
border: 1px solid rgba(88, 166, 255, 0.6);
color: #58a6ff !important;
font-weight: 600;
font-size: 0.9rem;
letter-spacing: 0.5px;
border-radius: 6px;
padding: 12px 24px;
transition: all 0.3s ease;
text-transform: uppercase;
}
#checkup-loans-module .checkup-action-btn button:hover {
background: linear-gradient(135deg, rgba(88, 166, 255, 0.25) 0%, rgba(88, 166, 255, 0.15) 100%);
border-color: rgba(88, 166, 255, 1);
box-shadow: 0 0 20px rgba(88, 166, 255, 0.4);
transform: translateY(-2px);
}
/* ========================================
BADGES DE STATUT
======================================== */
#checkup-loans-module .checkup-status-badge {
display: inline-block;
padding: 6px 14px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
}
#checkup-loans-module .checkup-status-actif {
background: rgba(84, 189, 75, 0.2);
color: #54bd4b;
border: 1px solid rgba(84, 189, 75, 0.4);
}
#checkup-loans-module .checkup-status-updated {
background: rgba(243, 156, 18, 0.2);
color: #f39c12;
border: 1px solid rgba(243, 156, 18, 0.4);
}
/* ========================================
SÉPARATEURS
======================================== */
#checkup-loans-module .checkup-section-divider {
height: 2px;
background: linear-gradient(90deg, transparent 0%, rgba(88, 166, 255, 0.4) 50%, transparent 100%);
margin: 2.5rem 0;
}
/* ========================================
MÉTRIQUES
======================================== */
#checkup-loans-module [data-testid="stMetric"] {
background: rgba(22, 27, 34, 0.6);
border: 1px solid rgba(48, 54, 61, 0.8);
border-radius: 6px;
padding: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
#checkup-loans-module [data-testid="stMetric"] label {
color: #8b949e !important;
font-size: 0.75rem !important;
font-weight: 500 !important;
text-transform: uppercase;
letter-spacing: 0.8px;
}
#checkup-loans-module [data-testid="stMetric"] [data-testid="stMetricValue"] {
color: #58a6ff !important;
font-size: 1.6rem !important;
font-weight: 600 !important;
}
/* ========================================
RESPONSIVE
======================================== */
@media (max-width: 768px) {
#checkup-loans-module .checkup-loan-card {
padding: 15px;
}
#checkup-loans-module [data-testid="stMetric"] [data-testid="stMetricValue"] {
font-size: 1.3rem !important;
}
}
</style>
""", unsafe_allow_html=True)
# ============================================================================
# FONCTION PRINCIPALE DU MODULE
# ============================================================================
def show_check_up_loans(client, sheet_name):
"""
Module de mise à jour (Check-Up) des prêts existants.
Workflow:
1. Sélection du CLIENT depuis Clients_KYC
2. Identification des prêts ACTIF du client
3. Vérification des remboursements (blocage si existants)
4. Formulaire de modification
5. Mise à jour : Statut UPDATED dans Prets_Master + Création dans Prets_Update
6. Génération des nouveaux documents
"""
apply_checkup_loans_styles()
st.markdown('<div id="checkup-loans-module">', unsafe_allow_html=True)
st.header("MISE À JOUR DE PRÊT")
st.markdown("*Modification des conditions d'un prêt avant tout remboursement*")
st.markdown('<div class="checkup-section-divider"></div>', unsafe_allow_html=True)
# ============================================================================
# ÉTAPE 1 : CHARGEMENT DES DONNÉES
# ============================================================================
try:
sh = client.open(sheet_name)
# Chargement des feuilles nécessaires
df_clients = get_cached_data(client, sheet_name, "Clients_KYC")
df_prets = get_cached_data(client, sheet_name, "Prets_Master")
df_remboursements = get_cached_data(client, sheet_name, "Remboursements")
# Chargement optionnel des garants
try:
df_garants = get_cached_data(client, sheet_name, "Garants_KYC")
except Exception as e:
st.warning(f"⚠️ Feuille Garants_KYC non accessible : {e}")
df_garants = pd.DataFrame()
except Exception as e:
st.error(f"❌ Erreur de connexion à Google Sheets : {e}")
st.markdown('</div>', unsafe_allow_html=True)
return
# Vérification données clients
if df_clients.empty:
st.error("🛑 Aucun client trouvé dans la base KYC.")
st.info(" Veuillez d'abord enregistrer des clients avant de pouvoir modifier des prêts.")
st.markdown('</div>', unsafe_allow_html=True)
return
# Vérification données prêts
if df_prets.empty:
st.warning("Aucun prêt n'a encore été octroyé.")
st.info("Utilisez le module 'Moteur Financier : Octroi de Prêt' pour créer un premier prêt.")
st.markdown('</div>', unsafe_allow_html=True)
return
# ============================================================================
# ÉTAPE 2 : SÉLECTION DU CLIENT
# ============================================================================
st.subheader(" Étape 1 : Sélectionner le client")
# Préparation de la liste de sélection
df_clients['search_label'] = df_clients['ID_Client'].astype(str) + " - " + df_clients['Nom_Complet'].astype(str)
selected_client_label = st.selectbox(
"Rechercher un client",
[""] + df_clients['search_label'].tolist(),
help="Sélectionnez le client dont vous souhaitez modifier un prêt"
)
if not selected_client_label:
st.info("Sélectionnez un client pour commencer")
st.markdown('</div>', unsafe_allow_html=True)
return
# Récupération des informations du client
client_info = df_clients[df_clients['search_label'] == selected_client_label].iloc[0]
client_id = client_info['ID_Client']
st.success(f" Client sélectionné : **{client_info['Nom_Complet']}** (ID: {client_id})")
st.markdown('<div class="checkup-section-divider"></div>', unsafe_allow_html=True)
# ============================================================================
# ÉTAPE 3 : IDENTIFICATION DES PRÊTS ACTIFS DU CLIENT
# ============================================================================
st.subheader("Étape 2 : Identifier le prêt à modifier")
# Filtrage des prêts ACTIF du client sélectionné
df_prets_client = df_prets[
(df_prets['ID_Client'] == client_id) &
(df_prets['Statut'] == 'ACTIF')
]
# Cas A : Aucun prêt actif
if df_prets_client.empty:
st.markdown('<div class="checkup-blocked-alert">', unsafe_allow_html=True)
st.markdown("### ⚠️ Aucun prêt actif trouvé")
st.markdown(f"""
Le client **{client_info['Nom_Complet']}** n'a actuellement aucun prêt avec le statut **ACTIF**.
**Raisons possibles :**
- Tous les prêts ont été remboursés (statut : SOLDE)
- Des prêts ont déjà été modifiés (statut : UPDATED)
- Aucun prêt n'a encore été octroyé à ce client
**Solution** : Utilisez le module 'Moteur Financier' pour octroyer un nouveau prêt.
""")
st.markdown('</div>', unsafe_allow_html=True)
st.markdown('</div>', unsafe_allow_html=True)
return
# Cas B : 1 seul prêt actif → Sélection automatique
if len(df_prets_client) == 1:
selected_loan = df_prets_client.iloc[0]
st.info(f"**1 prêt actif détecté** - Sélection automatique : **{selected_loan['ID_Pret']}**")
# Cas C : Plusieurs prêts actifs → Choix utilisateur
else:
st.warning(f"⚠️ **{len(df_prets_client)} prêts actifs** détectés pour ce client")
# Préparation de la liste de sélection des prêts
df_prets_client['loan_label'] = df_prets_client.apply(
lambda row: f"{row['ID_Pret']} | {int(clean_currency_value(row['Montant_Capital'])):,} XOF | Échéance: {row['Date_Fin']}".replace(",", " "),
axis=1
)
selected_loan_label = st.selectbox(
"Sélectionnez le prêt à modifier",
df_prets_client['loan_label'].tolist(),
help="Choisissez le prêt que vous souhaitez mettre à jour"
)
# Récupération du prêt sélectionné
selected_loan = df_prets_client[df_prets_client['loan_label'] == selected_loan_label].iloc[0]
# ============================================================================
# ÉTAPE 4 : VÉRIFICATION BLOQUANTE - REMBOURSEMENTS
# ============================================================================
loan_id = selected_loan['ID_Pret']
# Check si le prêt a reçu au moins 1 remboursement
if not df_remboursements.empty:
has_payments = not df_remboursements[df_remboursements['ID_Pret'] == loan_id].empty
if has_payments:
# BLOCAGE TOTAL
df_payments = df_remboursements[df_remboursements['ID_Pret'] == loan_id]
st.markdown('<div class="checkup-blocked-alert">', unsafe_allow_html=True)
st.markdown("### ⛔ MODIFICATION IMPOSSIBLE")
st.markdown(f"""
Le prêt **{loan_id}** a déjà fait l'objet de **{len(df_payments)} remboursement(s)**.
**Remboursements détectés :**
""")
# Affichage des remboursements
for idx, payment in df_payments.head(5).iterrows():
montant = int(clean_currency_value(payment.get('Montant_Verse', 0)))
date_paiement = payment.get('Date_Paiement', 'N/A')
st.markdown(f"- **{date_paiement}** : {montant:,} XOF".replace(",", " "))
if len(df_payments) > 5:
st.markdown(f"- *... et {len(df_payments) - 5} autre(s) paiement(s)*")
st.markdown("""
---
**Règle de sécurité** : La modification d'un prêt n'est autorisée que **AVANT le premier remboursement**.
**Solutions alternatives :**
1. Créer un nouveau prêt avec les nouvelles conditions
2. Négocier un rééchelonnement (module à venir)
3. Contacter le service juridique pour un avenant contractuel
""")
st.markdown('</div>', unsafe_allow_html=True)
st.markdown('</div>', unsafe_allow_html=True)
return
# Aucun remboursement → Autorisation de modifier
st.success(f"Aucun remboursement détecté - **Modification autorisée** pour le prêt {loan_id}")
st.markdown('<div class="checkup-section-divider"></div>', unsafe_allow_html=True)
# ============================================================================
# ÉTAPE 5 : AFFICHAGE DES INFORMATIONS ACTUELLES
# ============================================================================
st.subheader("Étape 3 : Informations actuelles du prêt")
st.markdown('<div class="checkup-loan-card">', unsafe_allow_html=True)
st.markdown(f"### Prêt {loan_id}")
# ✅ CORRECTION PROBLÈME 1 : Utiliser clean_percentage_value_local au lieu de clean_percentage_value
montant_capital_actuel = clean_currency_value(selected_loan['Montant_Capital'])
taux_hebdo_actuel = clean_taux_value(selected_loan['Taux_Hebdo'])
# ✅ CORRECTION TAUX ENDETTEMENT - Utilise clean_percentage_value corrigé
taux_endettement_actuel = clean_percentage_value(selected_loan.get('Taux_Endettement', 0))
duree_semaines_actuel = clean_currency_value(selected_loan['Duree_Semaines'])
montant_total_actuel = clean_currency_value(selected_loan['Montant_Total'])
cout_credit_actuel = clean_currency_value(selected_loan['Cout_Credit'])
# Affichage en colonnes
col1, col2, col3 = st.columns(3)
with col1:
st.metric("Montant Capital", f"{int(montant_capital_actuel):,} XOF".replace(",", " "))
st.metric("Type de prêt", selected_loan['Type_Pret'])
st.metric("Date déblocage", selected_loan['Date_Deblocage'])
with col2:
st.metric("Taux hebdomadaire", f"{taux_hebdo_actuel}%")
st.metric("Taux d'endettement", f"{taux_endettement_actuel:.2f}%") # ✅ Maintenant affiche 54.94%
st.metric("Durée", f"{int(duree_semaines_actuel)} semaines")
st.metric("Moyen de transfert", selected_loan.get('Moyen_Transfert', 'N/A'))
with col3:
st.metric("Montant Total", f"{int(montant_total_actuel):,} XOF".replace(",", " "))
st.metric("Coût du crédit", f"{int(cout_credit_actuel):,} XOF".replace(",", " "))
st.metric("Date de fin", selected_loan['Date_Fin'])
# Informations supplémentaires
st.markdown("---")
st.markdown(f"**Client :** {selected_loan['Nom_Complet']}")
st.markdown(f"**Motif du prêt :** {selected_loan.get('Motif', 'Non renseigné')}")
# Garant si existant
garant_id_actuel = selected_loan.get('ID_Garant', '')
if garant_id_actuel and garant_id_actuel != '' and not df_garants.empty:
garant_info = df_garants[df_garants['ID_Garant'] == garant_id_actuel]
if not garant_info.empty:
st.markdown(f"**Garant :** {garant_info.iloc[0]['Nom_Complet']} (ID: {garant_id_actuel})")
st.markdown(f"<span class='checkup-status-badge checkup-status-actif'>Statut : {selected_loan['Statut']}</span>", unsafe_allow_html=True)
st.markdown('</div>', unsafe_allow_html=True)
st.markdown('<div class="checkup-section-divider"></div>', unsafe_allow_html=True)
# ============================================================================
# ÉTAPE 6 : FORMULAIRE DE MODIFICATION
# ============================================================================
st.subheader(" Étape 4 : Nouvelles conditions du prêt")
st.info(" Modifiez les paramètres ci-dessous. Les montants seront recalculés automatiquement.")
# Initialisation des variables de session pour éviter les pertes de données
if 'dates_perso_update' not in st.session_state:
# Récupération des dates existantes si type PERSONNALISE
dates_str = selected_loan.get('Dates_Versements', '')
if dates_str and dates_str != '':
try:
dates_list = [datetime.strptime(d.strip(), "%d/%m/%Y").date() for d in dates_str.split(';')]
st.session_state.dates_perso_update = dates_list
except:
st.session_state.dates_perso_update = [date.today() + timedelta(weeks=2)]
else:
st.session_state.dates_perso_update = [date.today() + timedelta(weeks=2)]
# Configuration en colonnes
col_motif, col_type, col_moyen = st.columns(3)
with col_motif:
nouveau_motif = st.selectbox(
"Motif du prêt",
[
"Commerce / Achat de stock",
"Investissement",
"Trésorerie professionnelle",
"Lancement d'activité",
"Développement d'activité",
"Agriculture / Élevage",
"Transport / Logistique",
"Urgence médicale",
"Scolarité / Formation",
"Logement / Habitat",
"Réparations",
"Événements familiaux",
"Voyage / Déplacement",
"Consommation",
"Achat d'équipement personnel",
"Projet personnel",
"Autre"
],
index=0 if selected_loan.get('Motif', '') not in [
"Commerce / Achat de stock", "Investissement", "Trésorerie professionnelle",
"Lancement d'activité", "Développement d'activité", "Agriculture / Élevage",
"Transport / Logistique", "Urgence médicale", "Scolarité / Formation",
"Logement / Habitat", "Réparations", "Événements familiaux",
"Voyage / Déplacement", "Consommation", "Achat d'équipement personnel",
"Projet personnel", "Autre"
] else [
"Commerce / Achat de stock", "Investissement", "Trésorerie professionnelle",
"Lancement d'activité", "Développement d'activité", "Agriculture / Élevage",
"Transport / Logistique", "Urgence médicale", "Scolarité / Formation",
"Logement / Habitat", "Réparations", "Événements familiaux",
"Voyage / Déplacement", "Consommation", "Achat d'équipement personnel",
"Projet personnel", "Autre"
].index(selected_loan.get('Motif', 'Autre'))
)
with col_type:
type_options = ["In Fine", "Mensuel - Intérêts", "Mensuel - Constant", "Hebdomadaire", "Personnalisé"]
type_code_map = {
"In Fine": "IN_FINE",
"Mensuel - Intérêts": "MENSUEL_INTERETS",
"Mensuel - Constant": "MENSUEL_CONSTANT",
"Hebdomadaire": "HEBDOMADAIRE",
"Personnalisé": "PERSONNALISE"
}
type_reverse_map = {v: k for k, v in type_code_map.items()}
current_type_display = type_reverse_map.get(selected_loan['Type_Pret'], "In Fine")
nouveau_type = st.selectbox(
"Type de remboursement",
type_options,
index=type_options.index(current_type_display)
)
nouveau_type_code = type_code_map[nouveau_type]
with col_moyen:
moyen_actuel = selected_loan.get('Moyen_Transfert', 'Wave')
nouveau_moyen = st.selectbox(
"Moyen de transfert",
["Wave", "Orange Money", "Virement"],
index=["Wave", "Orange Money", "Virement"].index(moyen_actuel) if moyen_actuel in ["Wave", "Orange Money", "Virement"] else 0
)
# Paramètres financiers
col1, col2, col3 = st.columns(3)
nouveau_montant = col1.number_input(
"Montant Capital (XOF)",
min_value=10000,
value=int(montant_capital_actuel),
step=10000,
help="⚠️ Modification autorisée du montant capital"
)
nouveau_taux = col2.number_input(
"Taux Hebdo (%)",
min_value=0.1,
value=float(taux_hebdo_actuel),
step=0.1
)
# La durée sera définie spécifiquement selon le type de prêt
# (voir bloc de calcul ci-dessous)
# ============================================================================
# CALCULS AUTOMATIQUES SELON LE TYPE DE PRÊT
# ============================================================================
# Initialisation des variables
nouveau_montant_versement = 0
nouveau_montant_total = 0
nouveau_cout_credit = 0
nouveau_nb_versements = 0
nouvelle_duree_semaines = 0
nouvelles_dates_versements = []
# ✅ CORRECTION : Utiliser Date_Deblocage au lieu de date.today()
try:
nouvelle_date_debut = datetime.strptime(selected_loan['Date_Deblocage'], "%d/%m/%Y").date()
except:
# Fallback si le format est différent
nouvelle_date_debut = date.today()
nouvelle_date_fin = nouvelle_date_debut
# -----------------------------------------------------------
# 1. LOGIQUE IN FINE (1 seul versement à la fin)
# -----------------------------------------------------------
if nouveau_type_code == "IN_FINE":
nouvelle_duree_semaines = col3.number_input(
"Durée (en semaines)",
min_value=1,
max_value=104,
value=int(duree_semaines_actuel) if selected_loan['Type_Pret'] == 'IN_FINE' else 8
)
nouvelle_date_fin = nouvelle_date_debut + timedelta(weeks=int(nouvelle_duree_semaines))
# Calculs
nouveau_montant_total = nouveau_montant * (1 + (nouveau_taux / 100) * nouvelle_duree_semaines)
nouveau_cout_credit = nouveau_montant_total - nouveau_montant
nouveau_montant_versement = nouveau_montant_total
nouveau_nb_versements = 1
# Affichage résultat simulation
st.markdown("### Simulation des nouveaux montants")
res1, res2, res3 = st.columns(3)
res1.metric("Versement unique", f"{int(nouveau_montant_versement):,} XOF".replace(",", " "))
res2.metric("Coût du crédit", f"{int(nouveau_cout_credit):,} XOF".replace(",", " "))
res3.metric("Montant Total", f"{int(nouveau_montant_total):,} XOF".replace(",", " "),
delta=f"+{int(nouveau_cout_credit):,}")
# -----------------------------------------------------------
# 2. LOGIQUE MENSUEL - INTÉRÊTS (Remboursement capital à la fin)
# -----------------------------------------------------------
elif nouveau_type_code == "MENSUEL_INTERETS":
nouvelle_duree_mois = col3.number_input(
"Durée (en mois)",
min_value=1,
max_value=60,
value=int(duree_semaines_actuel / 4.33) if selected_loan['Type_Pret'] == 'MENSUEL_INTERETS' else 12
)
nouvelle_date_fin = nouvelle_date_debut + timedelta(days=int(nouvelle_duree_mois * 30))
# Conversion et Calculs
nouvelle_duree_semaines = nouvelle_duree_mois * 4.33
taux_mensuel = (nouveau_taux / 100) * 4.33
interet_mensuel = nouveau_montant * taux_mensuel
nouveau_montant_versement = interet_mensuel
montant_final_mois = nouveau_montant + interet_mensuel
nouveau_montant_total = (interet_mensuel * nouvelle_duree_mois) + nouveau_montant
nouveau_cout_credit = nouveau_montant_total - nouveau_montant
nouveau_nb_versements = int(nouvelle_duree_mois)
# Affichage résultat simulation
st.markdown("### Simulation des nouveaux montants")
res1, res2 = st.columns(2)
res1.metric("Intérêts mensuels", f"{int(interet_mensuel):,} XOF".replace(",", " "))
res2.metric("Dernier versement", f"{int(montant_final_mois):,} XOF".replace(",", " "))
res3, res4 = st.columns(2)
res3.metric("Coût du crédit", f"{int(nouveau_cout_credit):,} XOF".replace(",", " "))
res4.metric("Montant Total", f"{int(nouveau_montant_total):,} XOF".replace(",", " "),
delta=f"+{int(nouveau_cout_credit):,}")
# -----------------------------------------------------------
# 3. LOGIQUE MENSUEL - CONSTANT (Amortissement classique)
# -----------------------------------------------------------
elif nouveau_type_code == "MENSUEL_CONSTANT":
nouvelle_duree_mois = col3.number_input(
"Durée (en mois)",
min_value=1,
max_value=60,
value=int(duree_semaines_actuel / 4.33) if selected_loan['Type_Pret'] == 'MENSUEL_CONSTANT' else 12
)
nouvelle_date_fin = nouvelle_date_debut + timedelta(days=int(nouvelle_duree_mois * 30))
# Conversion et Calculs
nouvelle_duree_semaines = nouvelle_duree_mois * 4.33
taux_mensuel = (nouveau_taux / 100) * 4.33
if taux_mensuel > 0:
mensualite = (nouveau_montant * taux_mensuel) / (1 - (1 + taux_mensuel)**(-nouvelle_duree_mois))
else:
mensualite = nouveau_montant / nouvelle_duree_mois
nouveau_montant_versement = mensualite
nouveau_montant_total = mensualite * nouvelle_duree_mois
nouveau_cout_credit = nouveau_montant_total - nouveau_montant
nouveau_nb_versements = int(nouvelle_duree_mois)
# Affichage résultat simulation
st.markdown("### Simulation des nouveaux montants")
res1, res2, res3 = st.columns(3)
res1.metric("Mensualité constante", f"{int(mensualite):,} XOF".replace(",", " "))
res2.metric("Coût du crédit", f"{int(nouveau_cout_credit):,} XOF".replace(",", " "))
res3.metric("Montant Total", f"{int(nouveau_montant_total):,} XOF".replace(",", " "),
delta=f"+{int(nouveau_cout_credit):,}")
# -----------------------------------------------------------
# 4. LOGIQUE HEBDOMADAIRE
# -----------------------------------------------------------
elif nouveau_type_code == "HEBDOMADAIRE":
nouvelle_duree_semaines = col3.number_input(
"Durée (en semaines)",
min_value=1,
max_value=104,
value=int(duree_semaines_actuel) if selected_loan['Type_Pret'] == 'HEBDOMADAIRE' else 12
)
nouvelle_date_fin = nouvelle_date_debut + timedelta(weeks=int(nouvelle_duree_semaines))
# Calculs
taux_hebdo_decimal = nouveau_taux / 100
if taux_hebdo_decimal > 0:
hebdomadalite = (nouveau_montant * taux_hebdo_decimal) / (1 - (1 + taux_hebdo_decimal)**(-nouvelle_duree_semaines))
else:
hebdomadalite = nouveau_montant / nouvelle_duree_semaines
nouveau_montant_versement = hebdomadalite
nouveau_montant_total = hebdomadalite * nouvelle_duree_semaines
nouveau_cout_credit = nouveau_montant_total - nouveau_montant
nouveau_nb_versements = int(nouvelle_duree_semaines)
# Affichage résultat simulation
st.markdown("### Simulation des nouveaux montants")
res1, res2, res3 = st.columns(3)
res1.metric("Versement Hebdo", f"{int(hebdomadalite):,} XOF".replace(",", " "))
res2.metric("Coût du crédit", f"{int(nouveau_cout_credit):,} XOF".replace(",", " "))
res3.metric("Montant Total", f"{int(nouveau_montant_total):,} XOF".replace(",", " "),
delta=f"+{int(nouveau_cout_credit):,}")
# -----------------------------------------------------------
# 5. LOGIQUE PERSONNALISÉE (Dates manuelles)
# -----------------------------------------------------------
else: # PERSONNALISE
st.info(" Configurez les dates de versement ci-dessous")
# Interface d'ajout de dates
st.markdown("**Dates de versement :**")
col_add, col_reset = st.columns([1, 4])
if col_add.button("Ajouter", key="add_date_update"):
last_date = st.session_state.dates_perso_update[-1]
st.session_state.dates_perso_update.append(last_date + timedelta(weeks=1))
st.rerun()
# ✅ CORRECTION PROBLÈME 2 : Ajuster les dates passées
nouvelles_dates_versements = []
for idx, dt in enumerate(st.session_state.dates_perso_update):
col_d, col_x = st.columns([4, 1])
# ✅ CORRECTION : S'assurer que la date par défaut n'est jamais avant aujourd'hui
today = date.today()
safe_default_date = max(dt, today) # Prend la plus récente entre la date stockée et aujourd'hui
# Afficher un warning si la date a été ajustée
if dt < today and idx == 0:
st.warning(f"⚠️ La date originale ({dt.strftime('%d/%m/%Y')}) est passée. Ajustée à aujourd'hui ({today.strftime('%d/%m/%Y')}).")
new_date = col_d.date_input(
f"Echéance {idx+1}",
value=safe_default_date, # ✅ CORRECTION : Utiliser safe_default_date
key=f"d_update_{idx}",
min_value=today # min_value = aujourd'hui
)
nouvelles_dates_versements.append(new_date)
if col_x.button("♦️", key=f"del_update_{idx}") and len(st.session_state.dates_perso_update) > 1:
st.session_state.dates_perso_update.pop(idx)
st.rerun()
st.session_state.dates_perso_update = nouvelles_dates_versements
# Calculs basés sur les dates
if nouvelles_dates_versements:
nouvelles_dates_versements.sort()
nouvelle_date_fin = nouvelles_dates_versements[-1]
delta_days = (nouvelle_date_fin - nouvelle_date_debut).days
nouvelle_duree_semaines = max(1, delta_days / 7)
nouveau_montant_total = nouveau_montant * (1 + (nouveau_taux / 100) * nouvelle_duree_semaines)
nouveau_cout_credit = nouveau_montant_total - nouveau_montant
nouveau_nb_versements = len(nouvelles_dates_versements)
nouveau_montant_versement = nouveau_montant_total / nouveau_nb_versements
# Affichage résultat simulation
st.markdown("### Simulation des nouveaux montants")
res1, res2 = st.columns(2)
res1.metric("Moyenne/Versement", f"{int(nouveau_montant_versement):,} XOF".replace(",", " "))
res2.metric("Durée estimée", f"{int(nouvelle_duree_semaines)} sem")
res3, res4 = st.columns(2)
res3.metric("Coût du crédit", f"{int(nouveau_cout_credit):,} XOF".replace(",", " "))
res4.metric("Montant Total", f"{int(nouveau_montant_total):,} XOF".replace(",", " "),
delta=f"+{int(nouveau_cout_credit):,}")
# ============================================================================
# CALCUL DU TAUX D'ENDETTEMENT POUR LE NOUVEAU PRÊT
# ============================================================================
from Analytics.AnalyseFinance import calculer_taux_endettement
# Calcul du nouveau taux
nouveau_taux_endettement = calculer_taux_endettement(
nouveau_type_code,
nouveau_montant_versement,
nouveau_nb_versements,
nouvelle_duree_semaines,
client_info['Revenus_Mensuels']
)
# Affichage pour information
if nouveau_taux_endettement > 0:
st.info(f" **Nouveau taux d'endettement calculé** : {nouveau_taux_endettement:.2f}%")
# Warning si le taux dépasse 33%
if nouveau_taux_endettement > 33:
st.warning(f"⚠️ Le taux d'endettement ({nouveau_taux_endettement:.2f}%) dépasse le seuil recommandé de 33%")
# ============================================================================
# COMPARAISON AVANT / APRÈS
# ============================================================================
st.markdown('<div class="checkup-section-divider"></div>', unsafe_allow_html=True)
st.subheader(" Comparaison Avant / Après")
st.markdown('<div class="checkup-comparison-table">', unsafe_allow_html=True)
# Calcul des différences
diff_montant_total = nouveau_montant_total - montant_total_actuel
diff_cout_credit = nouveau_cout_credit - cout_credit_actuel
diff_duree = nouvelle_duree_semaines - duree_semaines_actuel
# Tableau comparatif
comparison_data = {
"Paramètre": [
"Type de prêt",
"Montant Capital",
"Taux hebdomadaire",
"Taux endettement",
"Durée",
"Montant Total",
"Coût du crédit",
"Date de fin"
],
"AVANT": [
selected_loan['Type_Pret'],
f"{int(montant_capital_actuel):,} XOF".replace(",", " "),
f"{taux_hebdo_actuel}%",
f"{taux_endettement_actuel:.2f}%" if taux_endettement_actuel > 0 else "N/A", # ✅ Maintenant correct
f"{int(duree_semaines_actuel)} semaines",
f"{int(montant_total_actuel):,} XOF".replace(",", " "),
f"{int(cout_credit_actuel):,} XOF".replace(",", " "),
selected_loan['Date_Fin']
],
"APRÈS": [
nouveau_type_code,
f"{int(nouveau_montant):,} XOF".replace(",", " "),
f"{nouveau_taux}%",
f"{nouveau_taux_endettement:.2f}%",
f"{int(nouvelle_duree_semaines)} semaines",
f"{int(nouveau_montant_total):,} XOF".replace(",", " "),
f"{int(nouveau_cout_credit):,} XOF".replace(",", " "),
nouvelle_date_fin.strftime("%d/%m/%Y")
],
"DIFFÉRENCE": [
"Modifié" if nouveau_type_code != selected_loan['Type_Pret'] else "=",
f"{int(nouveau_montant - montant_capital_actuel):+,} XOF".replace(",", " ") if nouveau_montant != montant_capital_actuel else "=",
f"{nouveau_taux - taux_hebdo_actuel:+.1f}%" if nouveau_taux != taux_hebdo_actuel else "=",
f"{nouveau_taux_endettement - taux_endettement_actuel:+.1f}%" if taux_endettement_actuel > 0 else f"{nouveau_taux_endettement:.1f}%", # ✅ Maintenant correct
f"{int(diff_duree):+} sem" if diff_duree != 0 else "=",
f"{int(diff_montant_total):+,} XOF".replace(",", " ") if diff_montant_total != 0 else "=",
f"{int(diff_cout_credit):+,} XOF".replace(",", " ") if diff_cout_credit != 0 else "=",
f"{int((nouvelle_date_fin - datetime.strptime(selected_loan['Date_Fin'], '%d/%m/%Y').date()).days):+} jours" if nouvelle_date_fin != datetime.strptime(selected_loan['Date_Fin'], '%d/%m/%Y').date() else "="
]
}
df_comparison = pd.DataFrame(comparison_data)
st.dataframe(df_comparison, hide_index=True, use_container_width=True)
st.markdown('</div>', unsafe_allow_html=True)
# ============================================================================
# WARNINGS / ALERTES
# ============================================================================
warnings = []
# Warning 1 : Augmentation significative du coût
if diff_cout_credit > (cout_credit_actuel * 0.15):
warnings.append(f"⚠️ Le coût du crédit augmente de **{int(diff_cout_credit):,} XOF** (+{(diff_cout_credit/cout_credit_actuel*100):.1f}%)".replace(",", " "))
# Warning 2 : Prolongation importante
if diff_duree > 8:
warnings.append(f"⚠️ La durée du prêt est prolongée de **{int(diff_duree)} semaines**")
# Warning 3 : Date de fin très éloignée
if (nouvelle_date_fin - nouvelle_date_debut).days > 180:
warnings.append(f"⚠️ La nouvelle date de fin ({nouvelle_date_fin.strftime('%d/%m/%Y')}) dépasse **6 mois**")
# Warning 4 : Augmentation du montant capital
if nouveau_montant > montant_capital_actuel:
warnings.append(f" Le montant capital augmente de **{int(nouveau_montant - montant_capital_actuel):,} XOF**".replace(",", " "))
if warnings:
st.markdown('<div class="checkup-warning-box">', unsafe_allow_html=True)
st.markdown("### ⚠️ Points d'attention")
for warning in warnings:
st.markdown(f"- {warning}")
st.markdown('</div>', unsafe_allow_html=True)
# ============================================================================
# ANALYSE DE SOLVABILITÉ (NOUVEAU PRÊT)
# ============================================================================
st.markdown('<div class="checkup-section-divider"></div>', unsafe_allow_html=True)
st.subheader(" Nouvelle analyse de solvabilité")
# Appel du cerveau analytique
analyse = analyser_capacite(
nouveau_type_code, nouveau_montant, nouveau_taux, nouvelle_duree_semaines,
nouveau_montant_versement, nouveau_nb_versements,
client_info['Revenus_Mensuels'], client_info.get('Charges_Estimees', 0),
nouveau_montant_total
)
st.markdown(f"### Statut : <span style='color:{analyse['couleur']}'>{analyse['statut']}</span>",
unsafe_allow_html=True)
st.info(analyse['message'])
with st.expander(" Détails financiers"):
st.markdown(analyse['details'])
# ============================================================================
# GÉNÉRATION DU NOUVEAU TABLEAU D'AMORTISSEMENT
# ============================================================================
st.markdown('<div class="checkup-section-divider"></div>', unsafe_allow_html=True)
st.subheader(" Nouveau tableau d'échéances")
# Génération du tableau
if nouveau_type_code == "PERSONNALISE":
df_amort_nouveau = generer_tableau_amortissement(
nouveau_type_code, nouveau_montant, nouveau_taux, nouvelle_duree_semaines,
nouveau_montant_versement, nouveau_nb_versements, nouvelle_date_debut,
dates_versements=nouvelles_dates_versements
)
else:
df_amort_nouveau = generer_tableau_amortissement(
nouveau_type_code, nouveau_montant, nouveau_taux, nouvelle_duree_semaines,
nouveau_montant_versement, nouveau_nb_versements, nouvelle_date_debut
)
# Ajout de la colonne Type
if not df_amort_nouveau.empty:
df_amort_nouveau.insert(0, "Type", nouveau_type)
st.dataframe(df_amort_nouveau, hide_index=True, use_container_width=True)
# ============================================================================
# VALIDATION ET ENREGISTREMENT
# ============================================================================
st.markdown('<div class="checkup-section-divider"></div>', unsafe_allow_html=True)
st.subheader(" Validation de la mise à jour")
st.info("🔒 **Attention** : Cette action va créer un nouveau prêt et archiver l'ancien avec le statut UPDATED.")
# Formulaire de validation
with st.form("form_update_loan"):
st.markdown("### Confirmation")
col_confirm1, col_confirm2 = st.columns(2)
with col_confirm1:
st.markdown(f"**Prêt à mettre à jour :** {loan_id}")
st.markdown(f"**Client :** {client_info['Nom_Complet']}")
st.markdown(f"**Nouveau montant total :** {int(nouveau_montant_total):,} XOF".replace(",", " "))
with col_confirm2:
st.markdown(f"**Nouveau type :** {nouveau_type_code}")
st.markdown(f"**Nouvelle durée :** {int(nouvelle_duree_semaines)} semaines")
st.markdown(f"**Nouvelle échéance :** {nouvelle_date_fin.strftime('%d/%m/%Y')}")
# Champ optionnel : Commentaire de modification
commentaire_modification = st.text_area(
"Commentaire de modification (optionnel)",
placeholder="Ex: Report de 3 semaines à la demande du client, Augmentation du capital pour extension d'activité...",
help="Ce commentaire sera enregistré dans la feuille Prets_Update pour traçabilité"
)
submit_update = st.form_submit_button(" VALIDER LA MISE À JOUR", use_container_width=True)
if submit_update:
try:
# ========================================================================
# ÉTAPE A : MISE À JOUR DU STATUT DANS Prets_Master
# ========================================================================
with st.spinner(" Mise à jour du prêt en cours..."):
ws_prets_master = sh.worksheet("Prets_Master")
# Recherche de la ligne du prêt à modifier
all_values = ws_prets_master.get_all_values()
header = all_values[0]
# Trouver l'index de la colonne Statut et ID_Pret
try:
col_statut_idx = header.index('Statut') + 1 # +1 car gspread est en base 1
col_id_pret_idx = header.index('ID_Pret') + 1
col_date_update_idx = header.index('Date_Update') + 1 if 'Date_Update' in header else None
except ValueError as e:
st.error(f"❌ Colonne manquante dans Prets_Master : {e}")
st.stop()
# Trouver la ligne du prêt
row_index = None
for idx, row in enumerate(all_values[1:], start=2): # Start=2 car ligne 1 = header
if row[col_id_pret_idx - 1] == loan_id:
row_index = idx
break
if row_index is None:
st.error(f"❌ Prêt {loan_id} introuvable dans Prets_Master")
st.stop()
# Mise à jour du statut
ws_prets_master.update_cell(row_index, col_statut_idx, "UPDATED")
# Mise à jour de Date_Update si la colonne existe
if col_date_update_idx:
ws_prets_master.update_cell(
row_index,
col_date_update_idx,
datetime.now().strftime("%d-%m-%Y %H:%M:%S")
)
time.sleep(1) # Anti rate-limit
st.success(f" Prêt {loan_id} → Statut changé en **UPDATED**")
# ========================================================================
# ÉTAPE B : CRÉATION DU NOUVEAU PRÊT DANS Prets_Update
# ========================================================================
with st.spinner(" Création du nouveau prêt dans Prets_Update..."):
# Vérification/Création de la feuille Prets_Update
try:
ws_prets_update = sh.worksheet("Prets_Update")
except:
st.warning("⚠️ Feuille Prets_Update inexistante. Création en cours...")
# Création de la feuille avec les bonnes colonnes
ws_prets_update = sh.add_worksheet(
title="Prets_Update",
rows=1000,
cols=25
)
# ✅ CORRECTION - En-têtes avec Taux_Endettement
headers = [
"ID_Pret", "ID_Pret_Source", "Version", "Date_Modification",
"ID_Client", "Nom_Complet", "Type_Pret", "Motif",
"Montant_Capital", "Taux_Hebdo", "Taux_Endettement", "Duree_Semaines", # ✅ Taux_Endettement ajouté
"Montant_Versement", "Montant_Total", "Cout_Credit",
"Nb_Versements", "Dates_Versements", "Date_Deblocage",
"Date_Fin", "Moyen_Transfert", "Statut", "ID_Garant",
"Date_Creation", "Commentaire_Modification"
]
ws_prets_update.append_row(headers)
time.sleep(1)
# Calcul de la version
all_updates = ws_prets_update.get_all_values()
version_count = sum(1 for row in all_updates[1:] if row[1] == loan_id) # Colonne ID_Pret_Source
nouvelle_version = version_count + 2 # +2 car V1 = original
# Génération du nouvel ID
nouveau_id = f"{loan_id}-V{nouvelle_version}"
# Préparation des dates de versement
dates_versements_str = ";".join([
d.strftime("%d/%m/%Y") for d in nouvelles_dates_versements
]) if nouvelles_dates_versements else ""
# ✅ CORRECTION - Construction de la ligne de données (avec Taux_Endettement)
row_data_update = [
nouveau_id, # ID_Pret
loan_id, # ID_Pret_Source
nouvelle_version, # Version
datetime.now().strftime("%d-%m-%Y %H:%M:%S"), # Date_Modification
client_id, # ID_Client
client_info['Nom_Complet'], # Nom_Complet
nouveau_type_code, # Type_Pret
nouveau_motif, # Motif
nouveau_montant, # Montant_Capital
nouveau_taux, # Taux_Hebdo
round(nouveau_taux_endettement, 2), # ✅ Taux_Endettement (maintenant aligné avec headers)
nouvelle_duree_semaines, # Duree_Semaines
round(nouveau_montant_versement), # Montant_Versement
round(nouveau_montant_total), # Montant_Total
round(nouveau_cout_credit), # Cout_Credit
nouveau_nb_versements, # Nb_Versements
dates_versements_str, # Dates_Versements
selected_loan['Date_Deblocage'], # Date_Deblocage
nouvelle_date_fin.strftime("%d/%m/%Y"), # Date_Fin
nouveau_moyen, # Moyen_Transfert
"ACTIF", # Statut
garant_id_actuel, # ID_Garant
datetime.now().strftime("%d-%m-%Y %H:%M:%S"), # Date_Creation
commentaire_modification # Commentaire_Modification
]
# Enregistrement
ws_prets_update.append_row(row_data_update)
time.sleep(1)
st.success(f"✅ Nouveau prêt créé : **{nouveau_id}** dans Prets_Update")
# ========================================================================
# ÉTAPE C : SAUVEGARDE DANS SESSION STATE
# ========================================================================
st.session_state.loan_updated = True
st.session_state.new_loan_id = nouveau_id
st.session_state.new_loan_data = {
"ID_Pret": nouveau_id,
"ID_Pret_Source": loan_id,
"Version": nouvelle_version,
"Montant_Capital": nouveau_montant,
"Montant_Total": nouveau_montant_total,
"Taux_Hebdo": nouveau_taux,
"Duree_Semaines": nouvelle_duree_semaines,
"Montant_Versement": nouveau_montant_versement, # ✅ AJOUT
"Cout_Credit": nouveau_cout_credit, # ✅ AJOUT
"Nb_Versements": nouveau_nb_versements, # ✅ AJOUT
"Motif": nouveau_motif,
"Date_Deblocage": selected_loan['Date_Deblocage'],
"Date_Fin": nouvelle_date_fin.strftime("%d/%m/%Y"),
"Type_Pret": nouveau_type_code
}
st.session_state.new_client_data = client_info.to_dict()
# Récupération du garant si existant
if garant_id_actuel and garant_id_actuel != '' and not df_garants.empty:
garant_info = df_garants[df_garants['ID_Garant'] == garant_id_actuel]
if not garant_info.empty:
st.session_state.new_garant_data = garant_info.iloc[0].to_dict()
else:
st.session_state.new_garant_data = None
else:
st.session_state.new_garant_data = None
st.session_state.new_df_amort = df_amort_nouveau.copy()
# Nettoyage du cache pour forcer le rechargement
st.cache_data.clear()
st.success("Mise a jour effectuee avec succes !")
except Exception as e:
st.error(f"❌ Erreur lors de la mise à jour : {e}")
st.exception(e)
return # ✅ AJOUT : Arrêter l'exécution ici en cas d'erreur
# ============================================================================
# GÉNÉRATION DES DOCUMENTS PDF (SI VALIDATION EFFECTUÉE)
# ============================================================================
if st.session_state.get('loan_updated', False):
st.markdown('<div class="checkup-section-divider"></div>', unsafe_allow_html=True)
st.markdown(f"### Documents du prêt **{st.session_state.new_loan_id}**")
st.success(f" Prêt mis à jour : **{st.session_state.get('new_loan_data', {}).get('ID_Pret_Source')}** → **{st.session_state.new_loan_id}**")
# Préparation des données pour les PDFs
loan_data_pdf = st.session_state.new_loan_data
client_data_pdf = st.session_state.new_client_data
garant_data_pdf = st.session_state.new_garant_data
df_amort_pdf = st.session_state.new_df_amort
# Ajout d'une mention spéciale pour les documents
loan_data_pdf['Mention_Update'] = f"Mise à jour du contrat n° {loan_data_pdf['ID_Pret_Source']} (Version {loan_data_pdf['Version']})"
# Affichage des boutons de téléchargement
col_pdf1, col_pdf2, col_pdf3, col_reset = st.columns(4)
# PDF 1 : CONTRAT DE PRÊT
with col_pdf1:
pdf_contrat = generer_contrat_pret(loan_data_pdf, client_data_pdf, df_amort_pdf)
st.download_button(
" Contrat de Prêt",
pdf_contrat,
f"Contrat_{st.session_state.new_loan_id}.pdf",
"application/pdf",
use_container_width=True
)
# PDF 2 : RECONNAISSANCE DE DETTE
with col_pdf2:
pdf_dette = generer_reconnaissance_dette(loan_data_pdf, client_data_pdf)
st.download_button(
" Reconnaissance Dette",
pdf_dette,
f"Dette_{st.session_state.new_loan_id}.pdf",
"application/pdf",
use_container_width=True
)
# PDF 3 : CONTRAT DE CAUTION (SI GARANT)
with col_pdf3:
if garant_data_pdf is not None:
pdf_caution = generer_contrat_caution(loan_data_pdf, garant_data_pdf)
st.download_button(
" Contrat Caution",
pdf_caution,
f"Caution_{st.session_state.new_loan_id}.pdf",
"application/pdf",
use_container_width=True
)
else:
st.info("Pas de garant")
# BOUTON RESET : Nouvelle mise à jour
with col_reset:
if st.button(" Nouvelle Mise à Jour", use_container_width=True, type="primary"):
# Nettoyage du session state
st.session_state.loan_updated = False
st.session_state.pop('new_loan_id', None)
st.session_state.pop('new_loan_data', None)
st.session_state.pop('new_client_data', None)
st.session_state.pop('new_garant_data', None)
st.session_state.pop('new_df_amort', None)
st.session_state.pop('dates_perso_update', None)
st.cache_data.clear()
st.rerun()
# Affichage d'un récapitulatif
st.markdown("---")
with st.expander(" Récapitulatif de la mise à jour"):
col_recap1, col_recap2 = st.columns(2)
with col_recap1:
st.markdown("### Ancien prêt")
st.markdown(f"**ID :** {loan_data_pdf['ID_Pret_Source']}")
st.markdown(f"**Statut :** UPDATED (archivé)")
st.markdown(f"**Montant :** {int(montant_total_actuel):,} XOF".replace(",", " "))
with col_recap2:
st.markdown("### Nouveau prêt")
st.markdown(f"**ID :** {st.session_state.new_loan_id}")
st.markdown(f"**Statut :** ACTIF")
st.markdown(f"**Montant :** {int(loan_data_pdf['Montant_Total']):,} XOF".replace(",", " "))
st.markdown(f"**Différence :** {int(loan_data_pdf['Montant_Total'] - montant_total_actuel):+,} XOF".replace(",", " "))
# ✅ AJOUT CRITIQUE : Arrêter l'exécution du module ici
st.markdown('</div>', unsafe_allow_html=True) # Fermer le wrapper
return # ✅ STOP - Ne pas continuer le reste du module |