klydekushy commited on
Commit
da721ed
·
verified ·
1 Parent(s): bdc2baa

Update src/modules/repayments.py

Browse files
Files changed (1) hide show
  1. src/modules/repayments.py +491 -120
src/modules/repayments.py CHANGED
@@ -1,6 +1,13 @@
1
  import streamlit as st
2
  import pandas as pd
3
  from datetime import datetime, date
 
 
 
 
 
 
 
4
 
5
  # === CSS SPÉCIFIQUE REPAYMENTS MODULE ===
6
  def apply_repayments_styles():
@@ -44,6 +51,118 @@ def apply_repayments_styles():
44
  font-weight: 700 !important;
45
  }
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  /* Expanders pour détails contrat */
48
  #repayments-module .streamlit-expanderHeader {
49
  background: rgba(22, 27, 34, 0.7) !important;
@@ -164,22 +283,6 @@ def apply_repayments_styles():
164
  border-left: 4px solid #e74c3c !important;
165
  }
166
 
167
- /* Selectbox dropdown */
168
- #repayments-module .stSelectbox [data-baseweb="select"] > div {
169
- background: rgba(13, 17, 23, 0.9) !important;
170
- }
171
-
172
- /* Colonnes */
173
- #repayments-module [data-testid="column"] {
174
- padding: 8px;
175
- }
176
-
177
- /* Placeholder text */
178
- #repayments-module input::placeholder {
179
- color: #6e7681 !important;
180
- font-style: italic;
181
- }
182
-
183
  /* Success badge style */
184
  .repayment-success-badge {
185
  background: linear-gradient(135deg, rgba(84, 189, 75, 0.2), rgba(63, 185, 80, 0.1));
@@ -204,6 +307,42 @@ def apply_repayments_styles():
204
  </style>
205
  """, unsafe_allow_html=True)
206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  def show_repayments_module(client, sheet_name):
208
  # Appliquer les styles spécifiques
209
  apply_repayments_styles()
@@ -211,17 +350,31 @@ def show_repayments_module(client, sheet_name):
211
  # Wrapper pour isolation
212
  st.markdown('<div id="repayments-module">', unsafe_allow_html=True)
213
 
214
- st.header("💰 GUICHET DE RECOUVREMENT (CASHFLOW IN)")
215
 
216
  try:
217
  sh = client.open(sheet_name)
218
  ws_prets = sh.worksheet("Prets_Master")
219
  ws_remb = sh.worksheet("Remboursements")
 
 
 
 
 
 
 
 
 
220
 
221
  # Chargement des données
222
  df_prets = pd.DataFrame(ws_prets.get_all_records())
 
 
223
 
224
- # On ne garde que les prêts ACTIFS ou EN RETARD (pas ceux déjà clos)
 
 
 
225
  if not df_prets.empty and 'ID_Pret' in df_prets.columns:
226
  active_loans = df_prets[df_prets['Statut'].isin(["ACTIF", "EN_RETARD", "LITIGE"])]
227
  else:
@@ -235,7 +388,7 @@ def show_repayments_module(client, sheet_name):
235
  return
236
 
237
  # --- 1. SÉLECTION DU PRÊT ---
238
- st.subheader("1️⃣ Identifier le Dossier")
239
 
240
  # Création liste de choix lisible
241
  choices = active_loans.apply(
@@ -255,132 +408,351 @@ def show_repayments_module(client, sheet_name):
255
  loan_data = active_loans[active_loans['ID_Pret'] == loan_id].iloc[0]
256
 
257
  # Affichage du contexte
258
- with st.expander("📄 Détails du contrat", expanded=True):
259
  c1, c2, c3 = st.columns(3)
260
  with c1:
261
- st.metric("💵 Capital Prêté", f"{loan_data['Montant_Capital']:,.0f} XOF")
262
  with c2:
263
- st.metric("💰 Montant Total Dû", f"{loan_data['Montant_Total']:,.0f} XOF")
264
  with c3:
265
- st.metric("📅 Échéance Prévue", f"{loan_data['Montant_Versement']:,.0f} XOF")
266
 
267
- # Informations supplémentaires
268
  st.caption(f"**Client :** {loan_data['Nom_Complet']}")
269
  st.caption(f"**Date de début :** {loan_data.get('Date_Deblocage', 'N/A')}")
270
  st.caption(f"**Statut actuel :** {loan_data['Statut']}")
271
 
272
- # --- 2. SAISIE DU PAIEMENT ---
273
  st.divider()
274
- st.subheader("2️⃣ Enregistrement du Paiement")
275
-
276
- with st.form("repayment_form", clear_on_submit=True):
277
- col_a, col_b = st.columns(2)
278
-
 
 
 
 
 
279
  with col_a:
280
- date_paiement = st.date_input(
281
- "📅 Date du paiement",
282
- value=date.today(),
283
- help="Date effective de réception des fonds"
284
- )
285
- montant_verse = st.number_input(
286
- "💵 Montant Versé (XOF)",
287
- min_value=1000,
288
- step=5000,
289
- help="Montant exact reçu"
290
- )
291
-
292
  with col_b:
293
- moyen = st.selectbox(
294
- "💳 Moyen de Paiement",
295
- ["Espèces", "Mobile Money (Wave)", "Mobile Money (Orange Money)", "Virement Bancaire", "Chèque"],
296
- help="Canal de réception des fonds"
297
- )
298
- commentaire = st.text_input(
299
- "📝 Référence / Commentaire",
300
- placeholder="Ex: Transaction ID Wave, Numéro de chèque...",
301
- help="Informations de traçabilité"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
 
304
- # Checkbox pour clore le dossier
305
- st.markdown("---")
306
- cloture = st.checkbox(
307
- "✅ Ce paiement solde la totalité du prêt (Clôturer le dossier)",
308
- help="Cochez uniquement si le montant versé couvre l'intégralité du solde restant"
309
- )
310
 
311
- submit = st.form_submit_button("🔒 VALIDER L'ENCAISSEMENT", use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
 
313
- if submit:
314
- # Validation du montant
315
- if montant_verse <= 0:
316
- st.error(" Le montant versé doit être supérieur à 0 XOF")
317
- else:
318
- try:
319
- # Génération ID Transaction
320
- existing_remb = ws_remb.get_all_values()
321
- next_id = len(existing_remb) # Compte les lignes (incluant header)
322
- trans_id = f"TRX-2025-{next_id:04d}"
323
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
324
 
325
- # 1. Écriture dans Remboursements
326
- new_row = [
327
- trans_id,
328
- loan_id,
329
- str(date_paiement),
330
- montant_verse,
331
- moyen,
332
- commentaire if commentaire else "N/A",
333
- timestamp
334
- ]
335
- ws_remb.append_row(new_row)
336
 
337
- # 2. Mise à jour du Statut si clôture
338
- if cloture:
339
- # Trouver la ligne du prêt
340
- cell = ws_prets.find(loan_id)
341
- header = ws_prets.row_values(1)
342
- col_statut_idx = header.index("Statut") + 1
 
343
 
344
- # Update de la cellule
345
- ws_prets.update_cell(cell.row, col_statut_idx, "CLOTURE")
 
 
 
 
 
346
 
347
- # Badge de succès
348
- st.markdown(f"""
349
- <div class="repayment-success-badge">
350
- <h3>✅ DOSSIER {loan_id} CLÔTURÉ</h3>
351
- <p style="color: #8b949e; margin: 8px 0 0 0;">
352
- Paiement de {montant_verse:,.0f} XOF enregistré | Ref: {trans_id}
353
- </p>
354
- </div>
355
- """, unsafe_allow_html=True)
356
- else:
357
- st.success(f"✅ Paiement de **{montant_verse:,.0f} XOF** enregistré avec succès")
358
- st.info(f"📋 Référence de transaction : **{trans_id}**")
359
-
360
- # Afficher un récapitulatif
361
- with st.expander("📊 Récapitulatif de la transaction", expanded=True):
362
- recap_col1, recap_col2 = st.columns(2)
363
- with recap_col1:
364
- st.write(f"**ID Transaction :** {trans_id}")
365
- st.write(f"**ID Prêt :** {loan_id}")
366
- st.write(f"**Date :** {date_paiement}")
367
- with recap_col2:
368
- st.write(f"**Montant :** {montant_verse:,.0f} XOF")
369
- st.write(f"**Moyen :** {moyen}")
370
- st.write(f"**Statut :** {'CLÔTURÉ ✅' if cloture else 'ACTIF 🔄'}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
 
372
- except Exception as e:
373
- st.error(f"❌ Erreur lors de l'enregistrement : {e}")
374
- st.exception(e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
 
376
  else:
377
- # Instructions si aucun prêt sélectionné
378
- st.info("👆 Veuillez sélectionner un prêt dans la liste ci-dessus pour commencer")
379
 
380
- # Afficher statistiques rapides
381
  if not active_loans.empty:
382
  st.markdown("---")
383
- st.subheader("📊 Vue d'ensemble des prêts actifs")
384
 
385
  stat_col1, stat_col2, stat_col3 = st.columns(3)
386
  with stat_col1:
@@ -392,5 +764,4 @@ def show_repayments_module(client, sheet_name):
392
  avg_loan = active_loans['Montant_Capital'].mean() if 'Montant_Capital' in active_loans.columns else 0
393
  st.metric("Prêt Moyen", f"{avg_loan:,.0f} XOF")
394
 
395
- # Fermeture du wrapper
396
  st.markdown('</div>', unsafe_allow_html=True)
 
1
  import streamlit as st
2
  import pandas as pd
3
  from datetime import datetime, date
4
+ import sys
5
+ import os
6
+
7
+ # Import du module d'analyse
8
+ sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
9
+ from Analytics.AnalyseRepayment import AnalyseRepayment
10
+ from DocumentGen.InvoiceRepayment import generer_recu
11
 
12
  # === CSS SPÉCIFIQUE REPAYMENTS MODULE ===
13
  def apply_repayments_styles():
 
51
  font-weight: 700 !important;
52
  }
53
 
54
+ /* Cartes Gotham pour scénarios */
55
+ .gotham-card {
56
+ background: linear-gradient(135deg, rgba(22, 27, 34, 0.9) 0%, rgba(13, 17, 23, 0.9) 100%);
57
+ border: 2px solid rgba(88, 166, 255, 0.3);
58
+ border-radius: 8px;
59
+ padding: 20px;
60
+ margin: 10px 0;
61
+ transition: all 0.3s ease;
62
+ position: relative;
63
+ overflow: hidden;
64
+ }
65
+
66
+ .gotham-card:hover {
67
+ border-color: rgba(88, 166, 255, 0.6);
68
+ transform: translateY(-2px);
69
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
70
+ }
71
+
72
+ .gotham-card::before {
73
+ content: '';
74
+ position: absolute;
75
+ top: 0;
76
+ left: 0;
77
+ right: 0;
78
+ height: 3px;
79
+ background: linear-gradient(90deg, #58a6ff, #54bd4b);
80
+ }
81
+
82
+ .gotham-card-header {
83
+ display: flex;
84
+ justify-content: space-between;
85
+ align-items: center;
86
+ margin-bottom: 16px;
87
+ padding-bottom: 12px;
88
+ border-bottom: 1px solid rgba(88, 166, 255, 0.2);
89
+ }
90
+
91
+ .gotham-card-title {
92
+ font-size: 1.1rem;
93
+ font-weight: 700;
94
+ color: #58a6ff;
95
+ margin: 0;
96
+ }
97
+
98
+ .gotham-card-badge {
99
+ background: rgba(88, 166, 255, 0.2);
100
+ color: #58a6ff;
101
+ padding: 4px 12px;
102
+ border-radius: 12px;
103
+ font-size: 0.75rem;
104
+ font-weight: 600;
105
+ }
106
+
107
+ .gotham-card-body {
108
+ color: #c9d1d9;
109
+ }
110
+
111
+ .gotham-card-row {
112
+ display: flex;
113
+ justify-content: space-between;
114
+ padding: 8px 0;
115
+ border-bottom: 1px solid rgba(48, 54, 61, 0.4);
116
+ }
117
+
118
+ .gotham-card-row:last-child {
119
+ border-bottom: none;
120
+ }
121
+
122
+ .gotham-card-label {
123
+ color: #8b949e;
124
+ font-size: 0.85rem;
125
+ }
126
+
127
+ .gotham-card-value {
128
+ color: #c9d1d9;
129
+ font-weight: 600;
130
+ font-size: 0.9rem;
131
+ }
132
+
133
+ .gotham-card-total {
134
+ background: rgba(88, 166, 255, 0.1);
135
+ padding: 12px;
136
+ border-radius: 6px;
137
+ margin-top: 12px;
138
+ text-align: center;
139
+ }
140
+
141
+ .gotham-card-total-label {
142
+ color: #8b949e;
143
+ font-size: 0.8rem;
144
+ margin-bottom: 4px;
145
+ }
146
+
147
+ .gotham-card-total-value {
148
+ color: #58a6ff;
149
+ font-size: 1.6rem;
150
+ font-weight: 700;
151
+ }
152
+
153
+ .gotham-card-impact {
154
+ margin-top: 12px;
155
+ padding: 8px;
156
+ background: rgba(84, 189, 75, 0.1);
157
+ border-left: 3px solid #54bd4b;
158
+ border-radius: 4px;
159
+ }
160
+
161
+ .gotham-card-impact.negative {
162
+ background: rgba(243, 156, 18, 0.1);
163
+ border-left-color: #f39c12;
164
+ }
165
+
166
  /* Expanders pour détails contrat */
167
  #repayments-module .streamlit-expanderHeader {
168
  background: rgba(22, 27, 34, 0.7) !important;
 
283
  border-left: 4px solid #e74c3c !important;
284
  }
285
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  /* Success badge style */
287
  .repayment-success-badge {
288
  background: linear-gradient(135deg, rgba(84, 189, 75, 0.2), rgba(63, 185, 80, 0.1));
 
307
  </style>
308
  """, unsafe_allow_html=True)
309
 
310
+ def render_gotham_card(option_num, label, badge, data):
311
+ """Génère une carte style Gotham"""
312
+
313
+ html = f"""
314
+ <div class="gotham-card">
315
+ <div class="gotham-card-header">
316
+ <h4 class="gotham-card-title">Option {option_num}</h4>
317
+ <span class="gotham-card-badge">{badge}</span>
318
+ </div>
319
+ <div class="gotham-card-body">
320
+ <div class="gotham-card-row">
321
+ <span class="gotham-card-label">Durée de retard</span>
322
+ <span class="gotham-card-value">{data['jours_retard']} jours ({data['semaines_retard']} semaine{'s' if data['semaines_retard'] > 1 else ''})</span>
323
+ </div>
324
+ <div class="gotham-card-row">
325
+ <span class="gotham-card-label">Montant échéance</span>
326
+ <span class="gotham-card-value">{data['montant_echeance']:,.0f} XOF</span>
327
+ </div>
328
+ <div class="gotham-card-row">
329
+ <span class="gotham-card-label">Pénalités ({data['taux']}%)</span>
330
+ <span class="gotham-card-value">+ {data['montant_penalites']:,.0f} XOF</span>
331
+ </div>
332
+ </div>
333
+ <div class="gotham-card-total">
334
+ <div class="gotham-card-total-label">TOTAL À ENCAISSER</div>
335
+ <div class="gotham-card-total-value">{data['total_a_encaisser']:,.0f} XOF</div>
336
+ </div>
337
+ <div class="gotham-card-impact {'negative' if data['montant_penalites'] == 0 else ''}">
338
+ <span style="font-size: 0.85rem; color: #c9d1d9;">
339
+ {data['impact']}
340
+ </span>
341
+ </div>
342
+ </div>
343
+ """
344
+ return html
345
+
346
  def show_repayments_module(client, sheet_name):
347
  # Appliquer les styles spécifiques
348
  apply_repayments_styles()
 
350
  # Wrapper pour isolation
351
  st.markdown('<div id="repayments-module">', unsafe_allow_html=True)
352
 
353
+ st.header("GUICHET DE RECOUVREMENT (CASHFLOW IN)")
354
 
355
  try:
356
  sh = client.open(sheet_name)
357
  ws_prets = sh.worksheet("Prets_Master")
358
  ws_remb = sh.worksheet("Remboursements")
359
+ ws_clients = sh.worksheet("Clients_KYC")
360
+
361
+ # Tentative de charger Ajustements_Echeances (créer si n'existe pas)
362
+ try:
363
+ ws_ajust = sh.worksheet("Ajustements_Echeances")
364
+ df_ajust = pd.DataFrame(ws_ajust.get_all_records())
365
+ except:
366
+ st.warning("⚠️ Table Ajustements_Echeances non trouvée. Création nécessaire.")
367
+ df_ajust = pd.DataFrame()
368
 
369
  # Chargement des données
370
  df_prets = pd.DataFrame(ws_prets.get_all_records())
371
+ df_remb = pd.DataFrame(ws_remb.get_all_records())
372
+ df_clients = pd.DataFrame(ws_clients.get_all_records())
373
 
374
+ # Initialisation de l'analyseur
375
+ analyser = AnalyseRepayment(df_prets, df_remb, df_ajust)
376
+
377
+ # On ne garde que les prêts ACTIFS
378
  if not df_prets.empty and 'ID_Pret' in df_prets.columns:
379
  active_loans = df_prets[df_prets['Statut'].isin(["ACTIF", "EN_RETARD", "LITIGE"])]
380
  else:
 
388
  return
389
 
390
  # --- 1. SÉLECTION DU PRÊT ---
391
+ st.subheader("1. Identifier le Dossier")
392
 
393
  # Création liste de choix lisible
394
  choices = active_loans.apply(
 
408
  loan_data = active_loans[active_loans['ID_Pret'] == loan_id].iloc[0]
409
 
410
  # Affichage du contexte
411
+ with st.expander("Détails du contrat", expanded=True):
412
  c1, c2, c3 = st.columns(3)
413
  with c1:
414
+ st.metric("Capital Prêté", f"{loan_data['Montant_Capital']:,.0f} XOF")
415
  with c2:
416
+ st.metric("Montant Total Dû", f"{loan_data['Montant_Total']:,.0f} XOF")
417
  with c3:
418
+ st.metric("Échéance Prévue", f"{loan_data['Montant_Versement']:,.0f} XOF")
419
 
 
420
  st.caption(f"**Client :** {loan_data['Nom_Complet']}")
421
  st.caption(f"**Date de début :** {loan_data.get('Date_Deblocage', 'N/A')}")
422
  st.caption(f"**Statut actuel :** {loan_data['Statut']}")
423
 
424
+ # --- 2. ANALYSE DE L'ÉCHÉANCE ---
425
  st.divider()
426
+ st.subheader("2. Analyse de l'Échéance en Cours")
427
+
428
+ # Date du paiement
429
+ date_paiement = st.date_input("Date du paiement", value=date.today())
430
+
431
+ # Analyse automatique
432
+ echeance_info = analyser.detecter_echeance_attendue(loan_id, date_paiement)
433
+
434
+ if echeance_info:
435
+ col_a, col_b, col_c = st.columns(3)
436
  with col_a:
437
+ st.metric("Échéance Attendue", f"{echeance_info['numero']}/{echeance_info['total']}")
 
 
 
 
 
 
 
 
 
 
 
438
  with col_b:
439
+ st.metric("Date Prévue", echeance_info['date_prevue'].strftime("%d/%m/%Y"))
440
+ with col_c:
441
+ st.metric("Montant Échéance", f"{echeance_info['montant_ajuste']:,.0f} XOF")
442
+
443
+ # Calcul du retard
444
+ penalites_base = analyser.calculer_penalites(
445
+ echeance_info['montant_ajuste'],
446
+ echeance_info['date_prevue'],
447
+ date_paiement
448
+ )
449
+
450
+ jours_retard = penalites_base['jours_retard']
451
+
452
+ # --- 3. SCÉNARIOS DE PÉNALITÉS (si retard détecté) ---
453
+ if jours_retard >= 1:
454
+ st.divider()
455
+ st.warning(f"⚠️ RETARD DÉTECTÉ : {jours_retard} jours")
456
+ st.subheader("3. Choix du Scénario de Pénalités")
457
+
458
+ # Génération des 3 scénarios
459
+ scenarios = analyser.generer_scenarios_penalites(
460
+ echeance_info['montant_ajuste'],
461
+ echeance_info['date_prevue'],
462
+ date_paiement
463
  )
464
+
465
+ # Ajout de l'impact business
466
+ scenarios[0]['impact'] = f"Gain structure : +{scenarios[0]['montant_penalites']:,.0f} XOF | Applique pénalités réglementaires"
467
+ scenarios[1]['impact'] = f"Manque à gagner : -{scenarios[0]['montant_penalites']:,.0f} XOF | Geste de fidélisation client"
468
+ scenarios[2]['impact'] = f"Gain structure : +{scenarios[2]['montant_penalites']:,.0f} XOF | Compromis équitable"
469
+
470
+ # Enrichir avec les données nécessaires
471
+ for s in scenarios:
472
+ s['montant_echeance'] = echeance_info['montant_ajuste']
473
+ s['semaines_retard'] = s.get('semaines_retard', penalites_base['semaines_retard'])
474
+
475
+ # Affichage des cartes
476
+ col1, col2, col3 = st.columns(3)
477
+
478
+ with col1:
479
+ st.markdown(render_gotham_card(1, "Règlement Standard", "5%", scenarios[0]), unsafe_allow_html=True)
480
+ if st.button("Sélectionner Option 1", key="opt1", use_container_width=True):
481
+ st.session_state['selected_scenario'] = scenarios[0]
482
+
483
+ with col2:
484
+ st.markdown(render_gotham_card(2, "Geste Commercial", "0%", scenarios[1]), unsafe_allow_html=True)
485
+ if st.button("Sélectionner Option 2", key="opt2", use_container_width=True):
486
+ st.session_state['selected_scenario'] = scenarios[1]
487
+
488
+ with col3:
489
+ st.markdown(render_gotham_card(3, "Taux Personnalisé", "Custom", scenarios[2]), unsafe_allow_html=True)
490
+
491
+ # Slider pour taux personnalisé
492
+ taux_custom = st.slider(
493
+ "Taux de pénalité (%)",
494
+ min_value=0.0,
495
+ max_value=5.0,
496
+ value=2.5,
497
+ step=0.5,
498
+ key="taux_slider"
499
+ )
500
+
501
+ # Recalcul automatique
502
+ if taux_custom != 2.5:
503
+ scenario_custom = analyser.calculer_penalites(
504
+ echeance_info['montant_ajuste'],
505
+ echeance_info['date_prevue'],
506
+ date_paiement,
507
+ taux_hebdo=taux_custom / 100
508
+ )
509
+ scenario_custom['label'] = "Taux Personnalisé"
510
+ scenario_custom['taux'] = taux_custom
511
+ scenario_custom['total_a_encaisser'] = echeance_info['montant_ajuste'] + scenario_custom['montant_penalites']
512
+ scenario_custom['impact'] = f"Gain structure : +{scenario_custom['montant_penalites']:,.0f} XOF | Taux ajusté"
513
+ scenario_custom['montant_echeance'] = echeance_info['montant_ajuste']
514
+ scenario_custom['semaines_retard'] = scenario_custom.get('semaines_retard', penalites_base['semaines_retard'])
515
+
516
+ st.markdown(render_gotham_card(3, "Taux Personnalisé", f"{taux_custom}%", scenario_custom), unsafe_allow_html=True)
517
+
518
+ if st.button("Sélectionner Option 3", key="opt3", use_container_width=True):
519
+ if taux_custom != 2.5:
520
+ st.session_state['selected_scenario'] = scenario_custom
521
+ else:
522
+ st.session_state['selected_scenario'] = scenarios[2]
523
+
524
+ # Affichage du scénario sélectionné
525
+ if 'selected_scenario' in st.session_state:
526
+ selected = st.session_state['selected_scenario']
527
+ st.success(f"Scénario sélectionné : {selected['label']} ({selected['taux']}% de pénalités)")
528
+ st.info(f"Montant total à encaisser : **{selected['total_a_encaisser']:,.0f} XOF**")
529
+ else:
530
+ # Pas de retard
531
+ if jours_retard < 0:
532
+ st.success(f"Paiement anticipé de {abs(jours_retard)} jours")
533
+ else:
534
+ st.success("Paiement dans les délais")
535
+
536
+ st.session_state['selected_scenario'] = {
537
+ 'taux': 0.0,
538
+ 'montant_penalites': 0,
539
+ 'total_a_encaisser': echeance_info['montant_ajuste']
540
+ }
541
 
542
+ # --- 4. SAISIE DU PAIEMENT ---
543
+ st.divider()
544
+ st.subheader("4. Enregistrement du Paiement")
 
 
 
545
 
546
+ with st.form("repayment_form", clear_on_submit=True):
547
+ col_a, col_b = st.columns(2)
548
+
549
+ with col_a:
550
+ montant_verse = st.number_input(
551
+ "Montant Versé (XOF)",
552
+ min_value=1000,
553
+ step=5000,
554
+ value=int(st.session_state.get('selected_scenario', {}).get('total_a_encaisser', echeance_info['montant_ajuste'])),
555
+ help="Montant exact reçu"
556
+ )
557
+
558
+ with col_b:
559
+ moyen = st.selectbox(
560
+ "Moyen de Paiement",
561
+ ["Espèces", "Mobile Money (Wave)", "Mobile Money (Orange Money)", "Virement Bancaire", "Chèque"],
562
+ help="Canal de réception des fonds"
563
+ )
564
+
565
+ reference_externe = st.text_input(
566
+ "Référence Transaction",
567
+ placeholder="Ex: WAVE-TXN-123456, Numéro de chèque...",
568
+ help="Référence externe pour traçabilité"
569
+ )
570
+
571
+ commentaire = st.text_area(
572
+ "Commentaire",
573
+ placeholder="Notes additionnelles sur la transaction...",
574
+ help="Informations complémentaires"
575
+ )
576
 
577
+ # Checkbox pour clore le dossier
578
+ st.markdown("---")
579
+ cloture = st.checkbox(
580
+ "Ce paiement solde la totalité du prêt (Clôturer le dossier)",
581
+ help="Cochez uniquement si le montant versé couvre l'intégralité du solde restant"
582
+ )
 
 
 
 
 
583
 
584
+ submit = st.form_submit_button("VALIDER L'ENCAISSEMENT", use_container_width=True)
 
 
 
 
 
 
 
 
 
 
585
 
586
+ if submit:
587
+ if montant_verse <= 0:
588
+ st.error("❌ Le montant versé doit être supérieur à 0 XOF")
589
+ else:
590
+ try:
591
+ # Récupération du taux de pénalité sélectionné
592
+ taux_penalite = st.session_state.get('selected_scenario', {}).get('taux', 0.0) / 100
593
 
594
+ # Analyse complète
595
+ analyse_complete = analyser.analyser_remboursement_complet(
596
+ loan_id,
597
+ date_paiement,
598
+ montant_verse,
599
+ taux_penalite=taux_penalite
600
+ )
601
 
602
+ if 'error' in analyse_complete:
603
+ st.error(f"❌ {analyse_complete['error']}")
604
+ else:
605
+ # Génération ID Transaction
606
+ existing_remb = ws_remb.get_all_values()
607
+ next_id = len(existing_remb)
608
+ trans_id = f"TRX-2026-{next_id:04d}"
609
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
610
+
611
+ # Génération Numéro Reçu
612
+ annee_actuelle = datetime.now().year
613
+ count_annee = len([r for r in existing_remb if f"REC-{annee_actuelle}" in str(r)])
614
+ numero_recu = f"REC-{annee_actuelle}-{count_annee + 1:04d}"
615
+
616
+ # 1. Écriture dans Remboursements
617
+ new_row = [
618
+ trans_id,
619
+ loan_id,
620
+ analyse_complete['id_client'],
621
+ str(date_paiement),
622
+ montant_verse,
623
+ analyse_complete['montant_principal'],
624
+ analyse_complete['montant_interets'],
625
+ analyse_complete['penalites_retard'],
626
+ analyse_complete['solde_avant'],
627
+ analyse_complete['solde_apres'],
628
+ analyse_complete['numero_echeance'],
629
+ str(analyse_complete['date_echeance_prevue']),
630
+ analyse_complete['jours_retard'],
631
+ analyse_complete['statut_paiement'],
632
+ moyen,
633
+ reference_externe if reference_externe else "N/A",
634
+ commentaire if commentaire else "N/A",
635
+ "NON", # Recu_Emis (par défaut NON, sera mis à OUI si généré)
636
+ numero_recu,
637
+ timestamp
638
+ ]
639
+ ws_remb.append_row(new_row)
640
+
641
+ # 2. Gestion des ajustements si PARTIEL
642
+ if analyse_complete['statut_paiement'] == "PARTIEL" and analyse_complete['montant_a_reporter'] > 0:
643
+ if analyse_complete['prochaine_echeance']:
644
+ # Génération ID Ajustement
645
+ try:
646
+ existing_ajust = ws_ajust.get_all_values()
647
+ next_ajust_id = len(existing_ajust)
648
+ except:
649
+ # Si la table n'existe pas, la créer
650
+ ws_ajust = sh.add_worksheet(title="Ajustements_Echeances", rows="1000", cols="7")
651
+ ws_ajust.append_row(["ID_Ajustement", "ID_Pret", "Numero_Echeance", "Montant_Additionnel", "Raison", "Date_Creation", "Timestamp"])
652
+ next_ajust_id = 1
653
+
654
+ ajust_id = f"ADJ-{annee_actuelle}-{next_ajust_id:04d}"
655
+
656
+ ajust_row = [
657
+ ajust_id,
658
+ loan_id,
659
+ analyse_complete['prochaine_echeance'],
660
+ analyse_complete['montant_a_reporter'],
661
+ "PAIEMENT_PARTIEL",
662
+ str(date.today()),
663
+ timestamp
664
+ ]
665
+ ws_ajust.append_row(ajust_row)
666
+
667
+ st.warning(f"⚠️ Paiement PARTIEL détecté. {analyse_complete['montant_a_reporter']:,.0f} XOF reportés sur l'échéance #{analyse_complete['prochaine_echeance']}")
668
 
669
+ # 3. Mise à jour du Statut si clôture
670
+ if cloture or analyse_complete['solde_apres'] <= 0:
671
+ cell = ws_prets.find(loan_id)
672
+ header = ws_prets.row_values(1)
673
+ col_statut_idx = header.index("Statut") + 1
674
+ ws_prets.update_cell(cell.row, col_statut_idx, "CLOTURE")
675
+
676
+ st.markdown(f"""
677
+ <div class="repayment-success-badge">
678
+ <h3>DOSSIER {loan_id} CLÔTURÉ</h3>
679
+ <p style="color: #8b949e; margin: 8px 0 0 0;">
680
+ Paiement de {montant_verse:,.0f} XOF enregistré | Ref: {trans_id}
681
+ </p>
682
+ </div>
683
+ """, unsafe_allow_html=True)
684
+ else:
685
+ st.success(f"Paiement de **{montant_verse:,.0f} XOF** enregistré avec succès")
686
+ st.info(f"Référence de transaction : **{trans_id}**")
687
+
688
+ # Afficher un récapitulatif détaillé
689
+ with st.expander("Récapitulatif de la transaction", expanded=True):
690
+ recap_col1, recap_col2, recap_col3 = st.columns(3)
691
+ with recap_col1:
692
+ st.write(f"**ID Transaction :** {trans_id}")
693
+ st.write(f"**ID Prêt :** {loan_id}")
694
+ st.write(f"**Date :** {date_paiement}")
695
+ st.write(f"**Échéance :** {analyse_complete['numero_echeance']}")
696
+ with recap_col2:
697
+ st.write(f"**Montant Versé :** {montant_verse:,.0f} XOF")
698
+ st.write(f"**Principal :** {analyse_complete['montant_principal']:,.0f} XOF")
699
+ st.write(f"**Intérêts :** {analyse_complete['montant_interets']:,.0f} XOF")
700
+ st.write(f"**Pénalités :** {analyse_complete['penalites_retard']:,.0f} XOF")
701
+ with recap_col3:
702
+ st.write(f"**Solde Avant :** {analyse_complete['solde_avant']:,.0f} XOF")
703
+ st.write(f"**Solde Après :** {analyse_complete['solde_apres']:,.0f} XOF")
704
+ st.write(f"**Moyen :** {moyen}")
705
+ st.write(f"**Statut :** {analyse_complete['statut_paiement']}")
706
+
707
+ # Bouton pour générer le reçu
708
+ st.divider()
709
+ if st.button("Générer le Reçu", use_container_width=True):
710
+ # Préparer les données pour le reçu
711
+ client_data = df_clients[df_clients['ID_Client'] == analyse_complete['id_client']].iloc[0].to_dict()
712
+
713
+ recu_data = {
714
+ 'numero_recu': numero_recu,
715
+ 'trans_id': trans_id,
716
+ 'date_paiement': date_paiement,
717
+ 'client': client_data,
718
+ 'loan': loan_data.to_dict(),
719
+ 'paiement': analyse_complete,
720
+ 'moyen': moyen,
721
+ 'reference': reference_externe
722
+ }
723
+
724
+ pdf_bytes = generer_recu(recu_data)
725
+
726
+ if pdf_bytes:
727
+ # Mise à jour de Recu_Emis dans Remboursements
728
+ cell_trans = ws_remb.find(trans_id)
729
+ header_remb = ws_remb.row_values(1)
730
+ col_recu_idx = header_remb.index("Recu_Emis") + 1
731
+ ws_remb.update_cell(cell_trans.row, col_recu_idx, "OUI")
732
+
733
+ st.download_button(
734
+ label="Télécharger le Reçu PDF",
735
+ data=pdf_bytes,
736
+ file_name=f"{numero_recu}_{loan_id}.pdf",
737
+ mime="application/pdf",
738
+ use_container_width=True
739
+ )
740
+ st.success("Reçu généré avec succès")
741
+ else:
742
+ st.error("❌ Erreur lors de la génération du reçu")
743
+
744
+ except Exception as e:
745
+ st.error(f"❌ Erreur lors de l'enregistrement : {e}")
746
+ st.exception(e)
747
+ else:
748
+ st.error("❌ Impossible d'analyser l'échéance pour ce prêt")
749
 
750
  else:
751
+ st.info("Veuillez sélectionner un prêt dans la liste ci-dessus pour commencer")
 
752
 
 
753
  if not active_loans.empty:
754
  st.markdown("---")
755
+ st.subheader("Vue d'ensemble des prêts actifs")
756
 
757
  stat_col1, stat_col2, stat_col3 = st.columns(3)
758
  with stat_col1:
 
764
  avg_loan = active_loans['Montant_Capital'].mean() if 'Montant_Capital' in active_loans.columns else 0
765
  st.metric("Prêt Moyen", f"{avg_loan:,.0f} XOF")
766
 
 
767
  st.markdown('</div>', unsafe_allow_html=True)