klydekushy commited on
Commit
2b09707
·
verified ·
1 Parent(s): 9537caf

Update src/modules/loans_engine.py

Browse files
Files changed (1) hide show
  1. src/modules/loans_engine.py +616 -181
src/modules/loans_engine.py CHANGED
@@ -9,17 +9,508 @@ from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph,
9
  from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
10
  from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
11
 
12
- def generer_tableau_amortissement(type_code, montant, taux_hebdo, duree_semaines, montant_versement, nb_versements, date_debut, dates_versements=None):
13
- """Génère le tableau d'amortissement détaillé selon le type de prêt"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  tableau = []
16
 
17
  if type_code == "IN_FINE":
18
- # 1 seule ligne : versement unique
19
  date_fin = date_debut + timedelta(weeks=duree_semaines)
20
  montant_total = montant * (1 + (taux_hebdo / 100) * duree_semaines)
21
  interets = montant_total - montant
22
-
23
  tableau.append({
24
  "Periode": 1,
25
  "Date": date_fin.strftime("%d/%m/%Y"),
@@ -34,20 +525,15 @@ def generer_tableau_amortissement(type_code, montant, taux_hebdo, duree_semaines
34
  taux_mensuel = (taux_hebdo / 100) * 4.33
35
  interet_mensuel = montant * taux_mensuel
36
  solde = montant
37
-
38
  for mois in range(1, duree_mois + 1):
39
  date_echeance = date_debut + timedelta(days=30 * mois)
40
-
41
  if mois == duree_mois:
42
- # Dernier mois : capital + intérêts
43
  capital_paye = montant
44
  versement = montant + interet_mensuel
45
  solde = 0
46
  else:
47
- # Mois intermédiaires : seulement intérêts
48
  capital_paye = 0
49
  versement = interet_mensuel
50
-
51
  tableau.append({
52
  "Periode": mois,
53
  "Date": date_echeance.strftime("%d/%m/%Y"),
@@ -61,13 +547,11 @@ def generer_tableau_amortissement(type_code, montant, taux_hebdo, duree_semaines
61
  duree_mois = nb_versements
62
  taux_mensuel = (taux_hebdo / 100) * 4.33
63
  solde = montant
64
-
65
  for mois in range(1, duree_mois + 1):
66
  date_echeance = date_debut + timedelta(days=30 * mois)
67
  interets = solde * taux_mensuel
68
  capital_paye = montant_versement - interets
69
  solde -= capital_paye
70
-
71
  tableau.append({
72
  "Periode": mois,
73
  "Date": date_echeance.strftime("%d/%m/%Y"),
@@ -80,13 +564,11 @@ def generer_tableau_amortissement(type_code, montant, taux_hebdo, duree_semaines
80
  elif type_code == "HEBDOMADAIRE":
81
  taux_hebdo_decimal = taux_hebdo / 100
82
  solde = montant
83
-
84
  for semaine in range(1, int(duree_semaines) + 1):
85
  date_echeance = date_debut + timedelta(weeks=semaine)
86
  interets = solde * taux_hebdo_decimal
87
  capital_paye = montant_versement - interets
88
  solde -= capital_paye
89
-
90
  tableau.append({
91
  "Periode": semaine,
92
  "Date": date_echeance.strftime("%d/%m/%Y"),
@@ -97,7 +579,6 @@ def generer_tableau_amortissement(type_code, montant, taux_hebdo, duree_semaines
97
  })
98
 
99
  elif type_code == "PERSONNALISE":
100
- # Calcul In Fine divisé
101
  for idx, date_v in enumerate(dates_versements):
102
  tableau.append({
103
  "Periode": idx + 1,
@@ -111,41 +592,19 @@ def generer_tableau_amortissement(type_code, montant, taux_hebdo, duree_semaines
111
  return pd.DataFrame(tableau)
112
 
113
  def generer_pdf_contrat(loan_data, client_info, df_amortissement):
114
- """Génère un contrat de prêt en PDF"""
115
-
116
  buffer = BytesIO()
117
  doc = SimpleDocTemplate(buffer, pagesize=A4, rightMargin=2*cm, leftMargin=2*cm, topMargin=2*cm, bottomMargin=2*cm)
118
-
119
  styles = getSampleStyleSheet()
120
  story = []
121
 
122
- # Style personnalisé
123
- style_titre = ParagraphStyle(
124
- 'CustomTitle',
125
- parent=styles['Heading1'],
126
- fontSize=18,
127
- textColor=colors.HexColor("#1f4788"),
128
- spaceAfter=30,
129
- alignment=TA_CENTER
130
- )
131
-
132
- style_section = ParagraphStyle(
133
- 'Section',
134
- parent=styles['Heading2'],
135
- fontSize=14,
136
- textColor=colors.HexColor("#2c5f99"),
137
- spaceAfter=12,
138
- spaceBefore=20
139
- )
140
 
141
- # TITRE
142
  story.append(Paragraph("CONTRAT DE PRÊT", style_titre))
143
  story.append(Paragraph(f"N° {loan_data['ID_Pret']}", styles['Normal']))
144
  story.append(Spacer(1, 20))
145
 
146
- # INFORMATIONS CLIENT
147
  story.append(Paragraph("1. IDENTITÉ DU BÉNÉFICIAIRE", style_section))
148
-
149
  data_client = [
150
  ["Nom complet", client_info['Nom_Complet']],
151
  ["ID Client", client_info['ID_Client']],
@@ -153,7 +612,6 @@ def generer_pdf_contrat(loan_data, client_info, df_amortissement):
153
  ["Revenus mensuels", f"{client_info['Revenus_Mensuels']} XOF"],
154
  ["Ville", client_info['Ville']]
155
  ]
156
-
157
  table_client = Table(data_client, colWidths=[6*cm, 10*cm])
158
  table_client.setStyle(TableStyle([
159
  ('BACKGROUND', (0, 0), (0, -1), colors.HexColor("#e8f0f7")),
@@ -164,13 +622,10 @@ def generer_pdf_contrat(loan_data, client_info, df_amortissement):
164
  ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
165
  ('GRID', (0, 0), (-1, -1), 0.5, colors.grey)
166
  ]))
167
-
168
  story.append(table_client)
169
  story.append(Spacer(1, 20))
170
 
171
- # CARACTÉRISTIQUES DU PRÊT
172
  story.append(Paragraph("2. CARACTÉRISTIQUES DU PRÊT", style_section))
173
-
174
  type_labels = {
175
  "IN_FINE": "In Fine (versement unique)",
176
  "MENSUEL_INTERETS": "Mensuel - Intérêts périodiques",
@@ -178,7 +633,6 @@ def generer_pdf_contrat(loan_data, client_info, df_amortissement):
178
  "HEBDOMADAIRE": "Hebdomadaire",
179
  "PERSONNALISE": "Périodicité personnalisée"
180
  }
181
-
182
  data_pret = [
183
  ["Type de prêt", type_labels.get(loan_data['Type_Pret'], loan_data['Type_Pret'])],
184
  ["Montant du capital", f"{loan_data['Montant_Capital']:,} XOF".replace(",", " ")],
@@ -191,7 +645,6 @@ def generer_pdf_contrat(loan_data, client_info, df_amortissement):
191
  ["Date de déblocage", loan_data['Date_Deblocage']],
192
  ["Date de fin", loan_data['Date_Fin']]
193
  ]
194
-
195
  table_pret = Table(data_pret, colWidths=[7*cm, 9*cm])
196
  table_pret.setStyle(TableStyle([
197
  ('BACKGROUND', (0, 0), (0, -1), colors.HexColor("#e8f0f7")),
@@ -202,17 +655,12 @@ def generer_pdf_contrat(loan_data, client_info, df_amortissement):
202
  ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
203
  ('GRID', (0, 0), (-1, -1), 0.5, colors.grey)
204
  ]))
205
-
206
  story.append(table_pret)
207
  story.append(PageBreak())
208
 
209
- # TABLEAU D'AMORTISSEMENT
210
  story.append(Paragraph("3. TABLEAU D'AMORTISSEMENT", style_section))
211
  story.append(Spacer(1, 10))
212
-
213
- # Préparation des données
214
  data_amort = [["Période", "Date", "Capital", "Intérêts", "Versement", "Solde Restant"]]
215
-
216
  for _, row in df_amortissement.iterrows():
217
  data_amort.append([
218
  str(row['Periode']),
@@ -222,7 +670,6 @@ def generer_pdf_contrat(loan_data, client_info, df_amortissement):
222
  f"{row['Versement']:,}".replace(",", " "),
223
  f"{row['Solde_Restant']:,}".replace(",", " ")
224
  ])
225
-
226
  table_amort = Table(data_amort, colWidths=[2*cm, 3*cm, 3*cm, 3*cm, 3*cm, 3*cm])
227
  table_amort.setStyle(TableStyle([
228
  ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#1f4788")),
@@ -235,14 +682,11 @@ def generer_pdf_contrat(loan_data, client_info, df_amortissement):
235
  ('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
236
  ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor("#f5f5f5")])
237
  ]))
238
-
239
  story.append(table_amort)
240
  story.append(Spacer(1, 30))
241
 
242
- # SIGNATURES
243
  story.append(Paragraph("4. SIGNATURES", style_section))
244
  story.append(Spacer(1, 20))
245
-
246
  data_signatures = [
247
  ["Le Prêteur", "Le Bénéficiaire"],
248
  ["", ""],
@@ -250,7 +694,6 @@ def generer_pdf_contrat(loan_data, client_info, df_amortissement):
250
  ["Signature :", "Signature :"],
251
  ["", ""]
252
  ]
253
-
254
  table_sign = Table(data_signatures, colWidths=[8*cm, 8*cm], rowHeights=[0.8*cm, 2*cm, 0.8*cm, 0.8*cm, 2*cm])
255
  table_sign.setStyle(TableStyle([
256
  ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
@@ -258,18 +701,19 @@ def generer_pdf_contrat(loan_data, client_info, df_amortissement):
258
  ('FONTSIZE', (0, 0), (-1, -1), 10),
259
  ('VALIGN', (0, 0), (-1, -1), 'TOP')
260
  ]))
261
-
262
  story.append(table_sign)
263
 
264
- # Génération du PDF
265
  doc.build(story)
266
  buffer.seek(0)
267
  return buffer
268
 
 
 
 
 
269
  def show_loans_engine(client, sheet_name):
270
  st.header("MOTEUR FINANCIER : OCTROI DE PRÊT")
271
 
272
- # --- 1. BARRE DE RECHERCHE INTELLIGENTE ---
273
  st.subheader("Sélection du Bénéficiaire")
274
 
275
  try:
@@ -308,7 +752,6 @@ def show_loans_engine(client, sheet_name):
308
 
309
  st.divider()
310
 
311
- # --- 2. CHOIX DU TYPE DE PRÊT ---
312
  st.subheader("⚙️ Configuration du Prêt")
313
 
314
  type_pret = st.radio(
@@ -325,7 +768,6 @@ def show_loans_engine(client, sheet_name):
325
 
326
  st.divider()
327
 
328
- # --- 3. PARAMÈTRES SELON LE TYPE ---
329
  col1, col2 = st.columns(2)
330
 
331
  with col1:
@@ -333,7 +775,6 @@ def show_loans_engine(client, sheet_name):
333
  with col2:
334
  taux_hebdo = st.number_input("📊 Taux hebdomadaire (%)", min_value=0.1, max_value=50.0, value=2.0, step=0.1)
335
 
336
- # Variables pour les calculs
337
  montant_versement = 0
338
  montant_total = 0
339
  cout_credit = 0
@@ -344,7 +785,9 @@ def show_loans_engine(client, sheet_name):
344
  type_code = ""
345
  date_debut = date.today()
346
 
347
- # --- LOGIQUE CONDITIONNELLE SELON LE TYPE ---
 
 
348
 
349
  if "In Fine" in type_pret:
350
  type_code = "IN_FINE"
@@ -492,13 +935,10 @@ def show_loans_engine(client, sheet_name):
492
  for idx, dt in enumerate(dates_versements):
493
  st.write(f"• Versement {idx + 1} : {dt.strftime('%d/%m/%Y')} → {round(montant_versement):,} XOF".replace(",", " "))
494
 
495
- # --- 4. ANALYSE DE SOLVABILITÉ (mise à jour dynamique) ---
496
- # S'affiche dès que montant et taux sont définis
497
- if montant > 0 and taux_hebdo > 0 and (montant_versement > 0 or type_code == "PERSONNALISE" or duree_semaines > 0):
498
- st.divider()
499
- st.subheader("🔍 Analyse de capacité de remboursement")
500
-
501
- # Conversion des charges en nombre si nécessaire
502
  try:
503
  charges_mensuelles = float(str(client_info['Charges_Estimees']).replace(" ", "").replace("XOF", "").replace(",", ""))
504
  except:
@@ -506,89 +946,102 @@ def show_loans_engine(client, sheet_name):
506
 
507
  revenus_mensuels = float(client_info['Revenus_Mensuels'])
508
 
509
- # Calcul de la charge mensuelle équivalente selon le type de prêt
510
- if type_code == "IN_FINE" and duree_semaines > 0:
511
- # Pour In Fine, on estime la capacité d'épargne mensuelle nécessaire
512
- montant_total_calc = montant * (1 + (taux_hebdo / 100) * duree_semaines)
513
- charge_mensuelle_equivalente = montant_total_calc / (duree_semaines / 4.33)
514
- type_charge = "Épargne mensuelle nécessaire"
515
- elif type_code == "MENSUEL_INTERETS" and montant_versement > 0:
516
- charge_mensuelle_equivalente = montant_versement
517
- type_charge = "Mensualité"
518
- elif type_code == "MENSUEL_CONSTANT" and montant_versement > 0:
519
- charge_mensuelle_equivalente = montant_versement
520
- type_charge = "Mensualité"
521
- elif type_code == "HEBDOMADAIRE" and montant_versement > 0:
522
- # Conversion hebdomadaire en mensuel
523
- charge_mensuelle_equivalente = montant_versement * 4.33
524
- type_charge = "Charge mensuelle équivalente"
525
- elif type_code == "PERSONNALISE" and duree_semaines > 0:
526
- # Calcul de la charge mensuelle moyenne
527
- montant_total_calc = montant * (1 + (taux_hebdo / 100) * duree_semaines)
528
- charge_mensuelle_equivalente = montant_total_calc / (duree_semaines / 4.33)
529
- type_charge = "Charge mensuelle moyenne"
530
- else:
531
- # Valeur par défaut si pas encore calculé
532
- charge_mensuelle_equivalente = 0
533
- type_charge = "En attente de calcul"
534
-
535
- # Calculs de solvabilité
536
- if charge_mensuelle_equivalente > 0:
537
- taux_endettement = (charge_mensuelle_equivalente / revenus_mensuels * 100) if revenus_mensuels > 0 else 0
538
- reste_a_vivre = revenus_mensuels - charges_mensuelles - charge_mensuelle_equivalente
539
-
540
- # Seuils de décision
541
- SEUIL_ENDETTEMENT_BON = 33
542
- SEUIL_ENDETTEMENT_LIMITE = 40
543
- SEUIL_RESTE_A_VIVRE_MIN = 30000 # 30 000 XOF minimum
544
 
545
- # Détermination du statut
546
- if taux_endettement <= SEUIL_ENDETTEMENT_BON and reste_a_vivre >= SEUIL_RESTE_A_VIVRE_MIN:
547
- statut = "✅ CAPACITÉ EXCELLENTE"
548
- couleur = "green"
549
- message = "Le client dispose d'une capacité de remboursement confortable."
550
- elif taux_endettement <= SEUIL_ENDETTEMENT_LIMITE and reste_a_vivre >= SEUIL_RESTE_A_VIVRE_MIN * 0.6:
551
- statut = "⚠️ CAPACITÉ ACCEPTABLE"
552
- couleur = "orange"
553
- message = "Le client peut rembourser mais sa marge de manœuvre est limitée. Surveillance recommandée."
554
  else:
555
- statut = "❌ CAPACITÉ INSUFFISANTE"
556
- couleur = "red"
557
- message = "⚠️ ATTENTION : Le taux d'endettement est élevé ou le reste à vivre est trop faible. Risque de défaut élevé."
558
 
559
- # Affichage
560
- col_sol1, col_sol2, col_sol3, col_sol4 = st.columns(4)
561
- col_sol1.metric("Revenus mensuels", f"{int(revenus_mensuels):,} XOF".replace(",", " "))
562
- col_sol2.metric(type_charge, f"{round(charge_mensuelle_equivalente):,} XOF".replace(",", " "))
563
- col_sol3.metric("Taux d'endettement", f"{round(taux_endettement, 1)} %")
564
- col_sol4.metric("Reste à vivre", f"{round(reste_a_vivre):,} XOF".replace(",", " "))
565
 
566
- # Statut avec couleur
567
- if couleur == "green":
568
- st.success(f"**{statut}** - {message}")
569
- elif couleur == "orange":
570
- st.warning(f"**{statut}** - {message}")
571
- else:
572
- st.error(f"**{statut}** - {message}")
 
 
 
 
 
 
573
 
574
- # Détails supplémentaires
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
575
  with st.expander("📊 Détails de l'analyse"):
576
- st.write(f"""
577
- **Méthode de calcul :**
578
- - Revenus mensuels : {int(revenus_mensuels):,} XOF
579
- - Charges actuelles : {int(charges_mensuelles):,} XOF
580
- - {type_charge} : {round(charge_mensuelle_equivalente):,} XOF
581
- - **Taux d'endettement** = ({round(charge_mensuelle_equivalente):,} / {int(revenus_mensuels):,}) × 100 = **{round(taux_endettement, 1)} %**
582
- - **Reste à vivre** = {int(revenus_mensuels):,} - {int(charges_mensuelles):,} - {round(charge_mensuelle_equivalente):,} = **{round(reste_a_vivre):,} XOF**
583
 
584
- **Seuils de référence :**
585
- - ✅ Taux d'endettement recommandé : < {SEUIL_ENDETTEMENT_BON} %
586
- - ⚠️ Taux d'endettement limite : < {SEUIL_ENDETTEMENT_LIMITE} %
587
- - ❌ Taux d'endettement critique : > {SEUIL_ENDETTEMENT_LIMITE} %
588
- - 💰 Reste à vivre minimum : {SEUIL_RESTE_A_VIVRE_MIN:,} XOF
589
- """.replace(",", " "))
590
-
591
- # --- 5. TABLEAU D'AMORTISSEMENT DÉTAILLÉ ---
592
  if montant_versement > 0 or type_code == "PERSONNALISE":
593
  st.divider()
594
  st.subheader("📋 Tableau d'amortissement détaillé")
@@ -612,7 +1065,9 @@ def show_loans_engine(client, sheet_name):
612
  }
613
  )
614
 
615
- # --- 6. VALIDATION & ENREGISTREMENT ---
 
 
616
  st.divider()
617
  with st.form("loan_confirmation"):
618
  st.markdown("### ✅ Validation de l'engagement")
@@ -628,49 +1083,29 @@ def show_loans_engine(client, sheet_name):
628
 
629
  dates_str = ";".join([d.strftime("%d/%m/%Y") for d in dates_versements]) if dates_versements else ""
630
 
631
- # STRUCTURE EXACTE DES COLONNES GOOGLE SHEETS "Prets_Master"
632
- # Ordre des colonnes (16 colonnes au total) :
633
- # 1. ID_Pret
634
- # 2. ID_Client
635
- # 3. Nom_Complet
636
- # 4. Type_Pret
637
- # 5. Montant_Capital
638
- # 6. Taux_Hebdo
639
- # 7. Duree_Semaines
640
- # 8. Montant_Versement
641
- # 9. Montant_Total
642
- # 10. Cout_Credit
643
- # 11. Nb_Versements
644
- # 12. Dates_Versements
645
- # 13. Date_Deblocage
646
- # 14. Date_Fin
647
- # 15. Statut
648
- # 16. Date_Creation
649
-
650
  new_loan_row = [
651
- loan_id, # 1. ID_Pret
652
- client_id, # 2. ID_Client
653
- client_info['Nom_Complet'], # 3. Nom_Complet
654
- type_code, # 4. Type_Pret
655
- montant, # 5. Montant_Capital
656
- taux_hebdo, # 6. Taux_Hebdo
657
- duree_semaines, # 7. Duree_Semaines
658
- round(montant_versement) if montant_versement > 0 else 0, # 8. Montant_Versement
659
- round(montant_total), # 9. Montant_Total
660
- round(cout_credit), # 10. Cout_Credit
661
- nb_versements, # 11. Nb_Versements
662
- dates_str, # 12. Dates_Versements
663
- date_debut.strftime("%d/%m/%Y"), # 13. Date_Deblocage
664
- date_fin.strftime("%d/%m/%Y") if date_fin else "", # 14. Date_Fin
665
- "ACTIF", # 15. Statut
666
- datetime.now().strftime("%d-%m-%Y %H:%M:%S") # 16. Date_Creation
667
  ]
668
 
669
  ws_prets.append_row(new_loan_row)
670
  st.success(f"✅ Prêt {loan_id} accordé au client {client_id} !")
671
  st.balloons()
672
 
673
- # --- 7. EXPORT PDF DU CONTRAT ---
674
  st.divider()
675
  st.subheader("📄 Export du contrat")
676
 
 
9
  from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
10
  from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
11
 
12
+ # ============================================================================
13
+ # SEUILS ADAPTATIFS PAR CONTEXTE
14
+ # ============================================================================
15
+
16
+ SEUILS = {
17
+ "taux_endettement_mensuel": {
18
+ "excellent": 25,
19
+ "bon": 33,
20
+ "acceptable": 40,
21
+ "critique": 50
22
+ },
23
+ "taux_effort_epargne": {
24
+ "excellent": 20,
25
+ "bon": 30,
26
+ "acceptable": 40,
27
+ "critique": 50
28
+ },
29
+ "reste_a_vivre_min": {
30
+ "excellent": 50000,
31
+ "bon": 30000,
32
+ "acceptable": 20000,
33
+ "critique": 15000
34
+ },
35
+ "liquidite_min_ratio": 1.2,
36
+ "duree_courte_semaines": 6
37
+ }
38
+
39
+ # ============================================================================
40
+ # FONCTION CENTRALISÉE D'ANALYSE DE CAPACITÉ
41
+ # ============================================================================
42
+
43
+ def analyser_capacite_remboursement(
44
+ type_code, montant, taux_hebdo, duree_semaines,
45
+ montant_versement, nb_versements,
46
+ revenus_mensuels, charges_mensuelles,
47
+ montant_total=0, cout_credit=0
48
+ ):
49
+ """Analyse la capacité de remboursement selon le type de prêt."""
50
+
51
+ revenus_mensuels = float(revenus_mensuels)
52
+ charges_mensuelles = float(charges_mensuelles)
53
+
54
+ # IN FINE
55
+ if type_code == "IN_FINE":
56
+ if duree_semaines < SEUILS["duree_courte_semaines"]:
57
+ # LIQUIDITÉ DIRECTE
58
+ duree_mois = duree_semaines / 4.33
59
+ revenus_cumules = revenus_mensuels * duree_mois
60
+ charges_cumules = charges_mensuelles * duree_mois
61
+ disponible_echeance = revenus_cumules - charges_cumules
62
+ marge_securite = disponible_echeance - montant_total
63
+ ratio_couverture = disponible_echeance / montant_total if montant_total > 0 else 0
64
+
65
+ metriques = {
66
+ "revenus_cumules": revenus_cumules,
67
+ "charges_cumules": charges_cumules,
68
+ "disponible_echeance": disponible_echeance,
69
+ "montant_requis": montant_total,
70
+ "marge_securite": marge_securite,
71
+ "ratio_couverture": ratio_couverture
72
+ }
73
+
74
+ if ratio_couverture >= 1.5:
75
+ statut, couleur = "EXCELLENT", "green"
76
+ message = "Le client dispose de liquidités très confortables avec une excellente marge de sécurité."
77
+ elif ratio_couverture >= SEUILS["liquidite_min_ratio"]:
78
+ statut, couleur = "BON", "blue"
79
+ message = "Liquidités suffisantes avec une marge de sécurité acceptable."
80
+ elif ratio_couverture >= 1.0:
81
+ statut, couleur = "ACCEPTABLE", "orange"
82
+ message = "⚠️ Liquidités justes. Le client peut payer mais sans marge d'erreur."
83
+ else:
84
+ statut, couleur = "INSUFFISANT", "red"
85
+ message = "❌ ATTENTION : Liquidités insuffisantes pour honorer le versement unique."
86
+
87
+ details = f"""
88
+ **🔍 ANALYSE DE LIQUIDITÉ DIRECTE (Prêt court terme)**
89
+
90
+ 📅 **Période :** {duree_semaines} semaines ({duree_mois:.2f} mois)
91
+
92
+ **Flux de trésorerie jusqu'à l'échéance :**
93
+ - 💵 Revenus cumulés : {int(revenus_cumules):,} XOF
94
+ - ❌ Charges cumulées : {int(charges_cumules):,} XOF
95
+ - 💰 **Disponible à l'échéance : {int(disponible_echeance):,} XOF**
96
+
97
+ **Capacité de paiement :**
98
+ - ✅ Montant requis : {int(montant_total):,} XOF
99
+ - 📊 Marge de sécurité : {int(marge_securite):,} XOF ({(ratio_couverture - 1) * 100:.0f}%)
100
+ - 📈 Ratio de couverture : {ratio_couverture:.2f}x
101
+
102
+ **Seuils de référence :**
103
+ - ✅ Excellent : > 1.5x | 🔵 Bon : > 1.2x | 🟠 Acceptable : ≥ 1.0x | ❌ Insuffisant : < 1.0x
104
+ """.replace(",", " ")
105
+
106
+ recommandations = []
107
+ if ratio_couverture < 1.0:
108
+ recommandations.append("🚨 Refuser le prêt ou réduire le montant")
109
+ recommandations.append(f"💡 Montant maximum recommandé : {int(disponible_echeance * 0.8):,} XOF".replace(",", " "))
110
+ elif ratio_couverture < SEUILS["liquidite_min_ratio"]:
111
+ recommandations.append("⚠️ Surveillance étroite recommandée")
112
+ recommandations.append("📞 Rappel de paiement 1 semaine avant l'échéance")
113
+
114
+ return {
115
+ "type_analyse": "liquidite_directe",
116
+ "metriques": metriques,
117
+ "statut": statut,
118
+ "couleur": couleur,
119
+ "message": message,
120
+ "details": details,
121
+ "recommandations": recommandations
122
+ }
123
+
124
+ else:
125
+ # ÉPARGNE PROGRESSIVE
126
+ epargne_hebdo_necessaire = montant_total / duree_semaines
127
+ revenus_hebdo = revenus_mensuels / 4.33
128
+ charges_hebdo = charges_mensuelles / 4.33
129
+ capacite_epargne_hebdo = revenus_hebdo - charges_hebdo
130
+ taux_effort_epargne = (epargne_hebdo_necessaire / revenus_hebdo * 100) if revenus_hebdo > 0 else 0
131
+
132
+ metriques = {
133
+ "epargne_hebdo_necessaire": epargne_hebdo_necessaire,
134
+ "revenus_hebdo": revenus_hebdo,
135
+ "charges_hebdo": charges_hebdo,
136
+ "capacite_epargne_hebdo": capacite_epargne_hebdo,
137
+ "taux_effort_epargne": taux_effort_epargne
138
+ }
139
+
140
+ if taux_effort_epargne <= SEUILS["taux_effort_epargne"]["excellent"]:
141
+ statut, couleur = "EXCELLENT", "green"
142
+ message = "Capacité d'épargne très confortable. Risque minimal."
143
+ elif taux_effort_epargne <= SEUILS["taux_effort_epargne"]["bon"]:
144
+ statut, couleur = "BON", "blue"
145
+ message = "Bonne capacité d'épargne progressive."
146
+ elif taux_effort_epargne <= SEUILS["taux_effort_epargne"]["acceptable"]:
147
+ statut, couleur = "ACCEPTABLE", "orange"
148
+ message = "⚠️ Capacité d'épargne limitée. Nécessite une discipline financière stricte."
149
+ elif taux_effort_epargne <= SEUILS["taux_effort_epargne"]["critique"]:
150
+ statut, couleur = "LIMITE", "red"
151
+ message = "🚨 Capacité d'épargne très tendue. Risque élevé de défaut."
152
+ else:
153
+ statut, couleur = "INSUFFISANT", "darkred"
154
+ message = "❌ ATTENTION : Capacité d'épargne insuffisante pour constituer le capital."
155
+
156
+ details = f"""
157
+ **🔍 ANALYSE DE CAPACITÉ D'ÉPARGNE (Prêt long terme)**
158
+
159
+ 📅 **Période :** {duree_semaines} semaines
160
+
161
+ **Épargne nécessaire :**
162
+ - 💰 Épargne hebdomadaire requise : {int(epargne_hebdo_necessaire):,} XOF/semaine
163
+ - 📊 Montant total à constituer : {int(montant_total):,} XOF
164
+
165
+ **Capacité financière hebdomadaire :**
166
+ - 💵 Revenus hebdo : {int(revenus_hebdo):,} XOF
167
+ - ❌ Charges hebdo : {int(charges_hebdo):,} XOF
168
+ - 💰 Capacité d'épargne : {int(capacite_epargne_hebdo):,} XOF
169
+
170
+ **Taux d'effort d'épargne : {taux_effort_epargne:.1f}%** des revenus hebdomadaires
171
+
172
+ **Seuils :** ✅ <{SEUILS["taux_effort_epargne"]["excellent"]}% | 🔵 <{SEUILS["taux_effort_epargne"]["bon"]}% | 🟠 <{SEUILS["taux_effort_epargne"]["acceptable"]}%
173
+ """.replace(",", " ")
174
+
175
+ recommandations = []
176
+ if taux_effort_epargne > SEUILS["taux_effort_epargne"]["critique"]:
177
+ recommandations.append("🚨 Refuser le prêt ou proposer un étalement plus long")
178
+ epargne_max = capacite_epargne_hebdo * 0.4
179
+ duree_recommandee = montant_total / epargne_max if epargne_max > 0 else 0
180
+ recommandations.append(f"💡 Durée minimale recommandée : {int(duree_recommandee)} semaines")
181
+ elif taux_effort_epargne > SEUILS["taux_effort_epargne"]["acceptable"]:
182
+ recommandations.append("⚠️ Exiger des garanties supplémentaires")
183
+ recommandations.append("📞 Suivi mensuel de l'épargne constituée")
184
+
185
+ return {
186
+ "type_analyse": "epargne_progressive",
187
+ "metriques": metriques,
188
+ "statut": statut,
189
+ "couleur": couleur,
190
+ "message": message,
191
+ "details": details,
192
+ "recommandations": recommandations
193
+ }
194
 
195
+ # MENSUEL - INTÉRÊTS PÉRIODIQUES
196
+ elif type_code == "MENSUEL_INTERETS":
197
+ taux_mensuel = (taux_hebdo / 100) * 4.33
198
+ interet_mensuel = montant * taux_mensuel
199
+
200
+ taux_endettement_courant = (interet_mensuel / revenus_mensuels * 100) if revenus_mensuels > 0 else 0
201
+ reste_vivre_courant = revenus_mensuels - charges_mensuelles - interet_mensuel
202
+
203
+ epargne_mensuelle_necessaire = montant / nb_versements
204
+ charge_totale_finale = interet_mensuel + epargne_mensuelle_necessaire
205
+ taux_effort_total = (charge_totale_finale / revenus_mensuels * 100) if revenus_mensuels > 0 else 0
206
+ reste_vivre_final = revenus_mensuels - charges_mensuelles - charge_totale_finale
207
+
208
+ metriques = {
209
+ "interet_mensuel": interet_mensuel,
210
+ "taux_endettement_courant": taux_endettement_courant,
211
+ "reste_vivre_courant": reste_vivre_courant,
212
+ "epargne_mensuelle_necessaire": epargne_mensuelle_necessaire,
213
+ "charge_totale_finale": charge_totale_finale,
214
+ "taux_effort_total": taux_effort_total,
215
+ "reste_vivre_final": reste_vivre_final
216
+ }
217
+
218
+ phase_courante_ok = (taux_endettement_courant <= SEUILS["taux_endettement_mensuel"]["acceptable"]
219
+ and reste_vivre_courant >= SEUILS["reste_a_vivre_min"]["acceptable"])
220
+
221
+ phase_finale_ok = (taux_effort_total <= SEUILS["taux_effort_epargne"]["acceptable"]
222
+ and reste_vivre_final >= SEUILS["reste_a_vivre_min"]["acceptable"])
223
+
224
+ if phase_courante_ok and phase_finale_ok:
225
+ if (taux_endettement_courant <= SEUILS["taux_endettement_mensuel"]["excellent"]
226
+ and taux_effort_total <= SEUILS["taux_effort_epargne"]["bon"]):
227
+ statut, couleur = "EXCELLENT", "green"
228
+ message = "Le client peut gérer les intérêts mensuels ET constituer progressivement le capital."
229
+ else:
230
+ statut, couleur = "BON", "blue"
231
+ message = "Capacité confirmée pour les deux phases du remboursement."
232
+ elif phase_courante_ok and not phase_finale_ok:
233
+ statut, couleur = "LIMITE", "orange"
234
+ message = "⚠️ Le client peut payer les intérêts mais aura des difficultés à constituer le capital final."
235
+ elif not phase_courante_ok and phase_finale_ok:
236
+ statut, couleur = "LIMITE", "orange"
237
+ message = "⚠️ Les intérêts mensuels sont élevés par rapport aux revenus."
238
+ else:
239
+ statut, couleur = "INSUFFISANT", "red"
240
+ message = "❌ ATTENTION : Capacité insuffisante pour ce type de prêt."
241
+
242
+ details = f"""
243
+ **🔍 ANALYSE DOUBLE PHASE : Intérêts + Capital**
244
+
245
+ 📅 **Structure du prêt :** {nb_versements} mois
246
+
247
+ **PHASE 1 : Mois 1 à {nb_versements-1} (Intérêts seuls)**
248
+ - 💳 Versement mensuel : {int(interet_mensuel):,} XOF
249
+ - 📊 Taux d'endettement : {taux_endettement_courant:.1f}%
250
+ - 💰 Reste à vivre : {int(reste_vivre_courant):,} XOF
251
+ - {"✅ Phase gérable" if phase_courante_ok else "❌ Phase difficile"}
252
+
253
+ **PHASE 2 : Mois {nb_versements} (Capital + Intérêts)**
254
+ - 💵 Épargne mensuelle recommandée : {int(epargne_mensuelle_necessaire):,} XOF
255
+ - 💳 Charge totale finale : {int(charge_totale_finale):,} XOF
256
+ - 📊 Taux d'effort total : {taux_effort_total:.1f}%
257
+ - 💰 Reste à vivre : {int(reste_vivre_final):,} XOF
258
+ - {"✅ Capital constituable" if phase_finale_ok else "❌ Épargne difficile"}
259
+ """.replace(",", " ")
260
+
261
+ recommandations = []
262
+ if not phase_courante_ok:
263
+ recommandations.append("🚨 Réduire le montant ou proposer un taux plus bas")
264
+ if not phase_finale_ok:
265
+ recommandations.append("💡 Allonger la durée pour faciliter la constitution du capital")
266
+ recommandations.append("📊 Proposer un prêt à mensualités constantes à la place")
267
+ if phase_courante_ok and phase_finale_ok:
268
+ recommandations.append("📞 Rappel de constitution d'épargne dès le 6ème mois")
269
+
270
+ return {
271
+ "type_analyse": "double_phase",
272
+ "metriques": metriques,
273
+ "statut": statut,
274
+ "couleur": couleur,
275
+ "message": message,
276
+ "details": details,
277
+ "recommandations": recommandations
278
+ }
279
+
280
+ # MENSUEL - MENSUALITÉS CONSTANTES
281
+ elif type_code == "MENSUEL_CONSTANT":
282
+ taux_endettement = (montant_versement / revenus_mensuels * 100) if revenus_mensuels > 0 else 0
283
+ reste_vivre = revenus_mensuels - charges_mensuelles - montant_versement
284
+
285
+ metriques = {
286
+ "mensualite": montant_versement,
287
+ "taux_endettement": taux_endettement,
288
+ "reste_vivre": reste_vivre
289
+ }
290
+
291
+ if (taux_endettement <= SEUILS["taux_endettement_mensuel"]["excellent"]
292
+ and reste_vivre >= SEUILS["reste_a_vivre_min"]["excellent"]):
293
+ statut, couleur = "EXCELLENT", "green"
294
+ message = "Capacité de remboursement très confortable. Risque minimal."
295
+ elif (taux_endettement <= SEUILS["taux_endettement_mensuel"]["bon"]
296
+ and reste_vivre >= SEUILS["reste_a_vivre_min"]["bon"]):
297
+ statut, couleur = "BON", "blue"
298
+ message = "Bonne capacité de remboursement mensuel."
299
+ elif (taux_endettement <= SEUILS["taux_endettement_mensuel"]["acceptable"]
300
+ and reste_vivre >= SEUILS["reste_a_vivre_min"]["acceptable"]):
301
+ statut, couleur = "ACCEPTABLE", "orange"
302
+ message = "⚠️ Capacité acceptable mais marge limitée. Surveillance recommandée."
303
+ elif taux_endettement <= SEUILS["taux_endettement_mensuel"]["critique"]:
304
+ statut, couleur = "LIMITE", "red"
305
+ message = "🚨 Taux d'endettement élevé. Risque important de défaut."
306
+ else:
307
+ statut, couleur = "INSUFFISANT", "darkred"
308
+ message = "❌ ATTENTION : Taux d'endettement critique. Refus recommandé."
309
+
310
+ details = f"""
311
+ **🔍 ANALYSE CLASSIQUE D'ENDETTEMENT**
312
+
313
+ 📅 **Durée :** {nb_versements} mois
314
+
315
+ **Flux mensuels :**
316
+ - 💵 Revenus mensuels : {int(revenus_mensuels):,} XOF
317
+ - ❌ Charges actuelles : {int(charges_mensuelles):,} XOF
318
+ - 💳 Mensualité du prêt : {int(montant_versement):,} XOF
319
+
320
+ **Indicateurs de solvabilité :**
321
+ - 📊 **Taux d'endettement : {taux_endettement:.1f}%**
322
+ - 💰 **Reste à vivre : {int(reste_vivre):,} XOF**
323
+
324
+ **Seuils bancaires :** ✅ <{SEUILS["taux_endettement_mensuel"]["excellent"]}% | 🔵 <{SEUILS["taux_endettement_mensuel"]["bon"]}% | 🟠 <{SEUILS["taux_endettement_mensuel"]["acceptable"]}%
325
+ """.replace(",", " ")
326
+
327
+ recommandations = []
328
+ if taux_endettement > SEUILS["taux_endettement_mensuel"]["critique"]:
329
+ recommandations.append("🚨 Refuser le prêt")
330
+ mensualite_max = revenus_mensuels * (SEUILS["taux_endettement_mensuel"]["acceptable"] / 100)
331
+ recommandations.append(f"💡 Mensualité maximum recommandée : {int(mensualite_max):,} XOF".replace(",", " "))
332
+ elif taux_endettement > SEUILS["taux_endettement_mensuel"]["acceptable"]:
333
+ recommandations.append("⚠️ Exiger des garanties solides")
334
+ recommandations.append("📊 Proposer d'allonger la durée pour réduire la mensualité")
335
+ elif reste_vivre < SEUILS["reste_a_vivre_min"]["bon"]:
336
+ recommandations.append("📞 Suivi mensuel de paiement recommandé")
337
+
338
+ return {
339
+ "type_analyse": "endettement_classique",
340
+ "metriques": metriques,
341
+ "statut": statut,
342
+ "couleur": couleur,
343
+ "message": message,
344
+ "details": details,
345
+ "recommandations": recommandations
346
+ }
347
+
348
+ # HEBDOMADAIRE
349
+ elif type_code == "HEBDOMADAIRE":
350
+ revenus_hebdo = revenus_mensuels / 4.33
351
+ charges_hebdo = charges_mensuelles / 4.33
352
+ taux_endettement_hebdo = (montant_versement / revenus_hebdo * 100) if revenus_hebdo > 0 else 0
353
+ reste_vivre_hebdo = revenus_hebdo - charges_hebdo - montant_versement
354
+
355
+ charge_mensuelle_equiv = montant_versement * 4.33
356
+ taux_endettement_mensuel = (charge_mensuelle_equiv / revenus_mensuels * 100) if revenus_mensuels > 0 else 0
357
+ reste_vivre_mensuel = revenus_mensuels - charges_mensuelles - charge_mensuelle_equiv
358
+
359
+ metriques = {
360
+ "versement_hebdo": montant_versement,
361
+ "revenus_hebdo": revenus_hebdo,
362
+ "charges_hebdo": charges_hebdo,
363
+ "taux_endettement_hebdo": taux_endettement_hebdo,
364
+ "reste_vivre_hebdo": reste_vivre_hebdo,
365
+ "charge_mensuelle_equiv": charge_mensuelle_equiv,
366
+ "taux_endettement_mensuel": taux_endettement_mensuel,
367
+ "reste_vivre_mensuel": reste_vivre_mensuel
368
+ }
369
+
370
+ if (taux_endettement_mensuel <= SEUILS["taux_endettement_mensuel"]["excellent"]
371
+ and reste_vivre_mensuel >= SEUILS["reste_a_vivre_min"]["excellent"]):
372
+ statut, couleur = "EXCELLENT", "green"
373
+ message = "Excellent rythme de remboursement hebdomadaire adapté aux revenus."
374
+ elif (taux_endettement_mensuel <= SEUILS["taux_endettement_mensuel"]["bon"]
375
+ and reste_vivre_mensuel >= SEUILS["reste_a_vivre_min"]["bon"]):
376
+ statut, couleur = "BON", "blue"
377
+ message = "Bonne capacité de paiement hebdomadaire."
378
+ elif (taux_endettement_mensuel <= SEUILS["taux_endettement_mensuel"]["acceptable"]
379
+ and reste_vivre_mensuel >= SEUILS["reste_a_vivre_min"]["acceptable"]):
380
+ statut, couleur = "ACCEPTABLE", "orange"
381
+ message = "⚠️ Capacité acceptable. Attention à la régularité des paiements."
382
+ elif taux_endettement_mensuel <= SEUILS["taux_endettement_mensuel"]["critique"]:
383
+ statut, couleur = "LIMITE", "red"
384
+ message = "🚨 Charge hebdomadaire élevée. Risque de retards de paiement."
385
+ else:
386
+ statut, couleur = "INSUFFISANT", "darkred"
387
+ message = "❌ ATTENTION : Versements hebdomadaires trop lourds pour les revenus."
388
+
389
+ details = f"""
390
+ **🔍 ANALYSE EN RYTHME HEBDOMADAIRE**
391
+
392
+ 📅 **Durée :** {int(duree_semaines)} semaines
393
+
394
+ **Capacité hebdomadaire :**
395
+ - 💵 Revenus hebdo : {int(revenus_hebdo):,} XOF
396
+ - ❌ Charges hebdo : {int(charges_hebdo):,} XOF
397
+ - 💳 Versement hebdo : {int(montant_versement):,} XOF
398
+ - 📊 Taux d'endettement : {taux_endettement_hebdo:.1f}%
399
+ - 💰 Reste à vivre : {int(reste_vivre_hebdo):,} XOF
400
+
401
+ **Équivalent mensuel (pour comparaison) :**
402
+ - 💳 Charge mensuelle : {int(charge_mensuelle_equiv):,} XOF
403
+ - 📊 Taux d'endettement : {taux_endettement_mensuel:.1f}%
404
+ - 💰 Reste à vivre : {int(reste_vivre_mensuel):,} XOF
405
+ """.replace(",", " ")
406
+
407
+ recommandations = []
408
+ if taux_endettement_mensuel > SEUILS["taux_endettement_mensuel"]["critique"]:
409
+ recommandations.append("🚨 Refuser ou réduire drastiquement le montant")
410
+ versement_max = revenus_hebdo * (SEUILS["taux_endettement_mensuel"]["acceptable"] / 100)
411
+ recommandations.append(f"💡 Versement hebdo maximum : {int(versement_max):,} XOF".replace(",", " "))
412
+ elif taux_endettement_mensuel > SEUILS["taux_endettement_mensuel"]["acceptable"]:
413
+ recommandations.append("⚠️ Allonger la durée pour réduire le versement hebdomadaire")
414
+ recommandations.append("📞 Système de rappel SMS avant chaque échéance")
415
+ else:
416
+ recommandations.append("✅ Rythme hebdomadaire adapté au profil")
417
+ recommandations.append("📱 Mettre en place un système de paiement mobile")
418
+
419
+ return {
420
+ "type_analyse": "rythme_hebdomadaire",
421
+ "metriques": metriques,
422
+ "statut": statut,
423
+ "couleur": couleur,
424
+ "message": message,
425
+ "details": details,
426
+ "recommandations": recommandations
427
+ }
428
+
429
+ # PERSONNALISÉ
430
+ elif type_code == "PERSONNALISE":
431
+ if duree_semaines > 0 and montant_total > 0:
432
+ epargne_moyenne_semaine = montant_total / duree_semaines
433
+ revenus_hebdo = revenus_mensuels / 4.33
434
+ charges_hebdo = charges_mensuelles / 4.33
435
+ taux_effort_moyen = (epargne_moyenne_semaine / revenus_hebdo * 100) if revenus_hebdo > 0 else 0
436
+
437
+ versement_moyen = montant_versement
438
+ charge_mensuelle_moyenne = versement_moyen * (nb_versements / (duree_semaines / 4.33))
439
+ taux_charge_moyenne = (charge_mensuelle_moyenne / revenus_mensuels * 100) if revenus_mensuels > 0 else 0
440
+
441
+ metriques = {
442
+ "epargne_moyenne_semaine": epargne_moyenne_semaine,
443
+ "versement_moyen": versement_moyen,
444
+ "taux_effort_moyen": taux_effort_moyen,
445
+ "taux_charge_moyenne": taux_charge_moyenne,
446
+ "nb_versements": nb_versements,
447
+ "duree_semaines": duree_semaines
448
+ }
449
+
450
+ if taux_effort_moyen <= SEUILS["taux_effort_epargne"]["excellent"]:
451
+ statut, couleur = "EXCELLENT", "green"
452
+ message = "Calendrier personnalisé bien adapté aux capacités du client."
453
+ elif taux_effort_moyen <= SEUILS["taux_effort_epargne"]["bon"]:
454
+ statut, couleur = "BON", "blue"
455
+ message = "Bon échelonnement des versements."
456
+ elif taux_effort_moyen <= SEUILS["taux_effort_epargne"]["acceptable"]:
457
+ statut, couleur = "ACCEPTABLE", "orange"
458
+ message = "⚠️ Calendrier gérable mais nécessite une discipline stricte."
459
+ elif taux_effort_moyen <= SEUILS["taux_effort_epargne"]["critique"]:
460
+ statut, couleur = "LIMITE", "red"
461
+ message = "🚨 Versements tendus. Risque de défaut sur certaines échéances."
462
+ else:
463
+ statut, couleur = "INSUFFISANT", "darkred"
464
+ message = "❌ ATTENTION : Versements trop élevés pour les revenus."
465
+
466
+ details = f"""
467
+ **🔍 ANALYSE CALENDRIER PERSONNALISÉ**
468
+
469
+ 📅 **Structure :**
470
+ - Durée totale : {int(duree_semaines)} semaines
471
+ - Nombre de versements : {nb_versements}
472
+ - Versement moyen : {int(versement_moyen):,} XOF
473
+
474
+ **Capacité d'épargne requise :**
475
+ - 💰 Épargne hebdo moyenne : {int(epargne_moyenne_semaine):,} XOF
476
+ - 💵 Revenus hebdo : {int(revenus_hebdo):,} XOF
477
+ - 📊 Taux d'effort moyen : {taux_effort_moyen:.1f}%
478
+ """.replace(",", " ")
479
+
480
+ recommandations = []
481
+ if taux_effort_moyen > SEUILS["taux_effort_epargne"]["critique"]:
482
+ recommandations.append("🚨 Réduire le montant ou augmenter le nombre de versements")
483
+ recommandations.append("📅 Espacer davantage les échéances")
484
+ elif taux_effort_moyen > SEUILS["taux_effort_epargne"]["acceptable"]:
485
+ recommandations.append("⚠️ Aligner les dates sur les pics de revenus du client")
486
+ recommandations.append("📞 Rappel 3 jours avant chaque versement")
487
+ else:
488
+ recommandations.append("✅ Calendrier bien structuré")
489
+ recommandations.append("💡 Automatiser les prélèvements si possible")
490
+
491
+ return {
492
+ "type_analyse": "calendrier_personnalise",
493
+ "metriques": metriques,
494
+ "statut": statut,
495
+ "couleur": couleur,
496
+ "message": message,
497
+ "details": details,
498
+ "recommandations": recommandations
499
+ }
500
+
501
+ return None
502
+
503
+ # ============================================================================
504
+ # FONCTIONS GÉNÉRATION TABLEAU + PDF (Inchangées)
505
+ # ============================================================================
506
+
507
+ def generer_tableau_amortissement(type_code, montant, taux_hebdo, duree_semaines, montant_versement, nb_versements, date_debut, dates_versements=None):
508
  tableau = []
509
 
510
  if type_code == "IN_FINE":
 
511
  date_fin = date_debut + timedelta(weeks=duree_semaines)
512
  montant_total = montant * (1 + (taux_hebdo / 100) * duree_semaines)
513
  interets = montant_total - montant
 
514
  tableau.append({
515
  "Periode": 1,
516
  "Date": date_fin.strftime("%d/%m/%Y"),
 
525
  taux_mensuel = (taux_hebdo / 100) * 4.33
526
  interet_mensuel = montant * taux_mensuel
527
  solde = montant
 
528
  for mois in range(1, duree_mois + 1):
529
  date_echeance = date_debut + timedelta(days=30 * mois)
 
530
  if mois == duree_mois:
 
531
  capital_paye = montant
532
  versement = montant + interet_mensuel
533
  solde = 0
534
  else:
 
535
  capital_paye = 0
536
  versement = interet_mensuel
 
537
  tableau.append({
538
  "Periode": mois,
539
  "Date": date_echeance.strftime("%d/%m/%Y"),
 
547
  duree_mois = nb_versements
548
  taux_mensuel = (taux_hebdo / 100) * 4.33
549
  solde = montant
 
550
  for mois in range(1, duree_mois + 1):
551
  date_echeance = date_debut + timedelta(days=30 * mois)
552
  interets = solde * taux_mensuel
553
  capital_paye = montant_versement - interets
554
  solde -= capital_paye
 
555
  tableau.append({
556
  "Periode": mois,
557
  "Date": date_echeance.strftime("%d/%m/%Y"),
 
564
  elif type_code == "HEBDOMADAIRE":
565
  taux_hebdo_decimal = taux_hebdo / 100
566
  solde = montant
 
567
  for semaine in range(1, int(duree_semaines) + 1):
568
  date_echeance = date_debut + timedelta(weeks=semaine)
569
  interets = solde * taux_hebdo_decimal
570
  capital_paye = montant_versement - interets
571
  solde -= capital_paye
 
572
  tableau.append({
573
  "Periode": semaine,
574
  "Date": date_echeance.strftime("%d/%m/%Y"),
 
579
  })
580
 
581
  elif type_code == "PERSONNALISE":
 
582
  for idx, date_v in enumerate(dates_versements):
583
  tableau.append({
584
  "Periode": idx + 1,
 
592
  return pd.DataFrame(tableau)
593
 
594
  def generer_pdf_contrat(loan_data, client_info, df_amortissement):
 
 
595
  buffer = BytesIO()
596
  doc = SimpleDocTemplate(buffer, pagesize=A4, rightMargin=2*cm, leftMargin=2*cm, topMargin=2*cm, bottomMargin=2*cm)
 
597
  styles = getSampleStyleSheet()
598
  story = []
599
 
600
+ style_titre = ParagraphStyle('CustomTitle', parent=styles['Heading1'], fontSize=18, textColor=colors.HexColor("#1f4788"), spaceAfter=30, alignment=TA_CENTER)
601
+ style_section = ParagraphStyle('Section', parent=styles['Heading2'], fontSize=14, textColor=colors.HexColor("#2c5f99"), spaceAfter=12, spaceBefore=20)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
602
 
 
603
  story.append(Paragraph("CONTRAT DE PRÊT", style_titre))
604
  story.append(Paragraph(f"N° {loan_data['ID_Pret']}", styles['Normal']))
605
  story.append(Spacer(1, 20))
606
 
 
607
  story.append(Paragraph("1. IDENTITÉ DU BÉNÉFICIAIRE", style_section))
 
608
  data_client = [
609
  ["Nom complet", client_info['Nom_Complet']],
610
  ["ID Client", client_info['ID_Client']],
 
612
  ["Revenus mensuels", f"{client_info['Revenus_Mensuels']} XOF"],
613
  ["Ville", client_info['Ville']]
614
  ]
 
615
  table_client = Table(data_client, colWidths=[6*cm, 10*cm])
616
  table_client.setStyle(TableStyle([
617
  ('BACKGROUND', (0, 0), (0, -1), colors.HexColor("#e8f0f7")),
 
622
  ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
623
  ('GRID', (0, 0), (-1, -1), 0.5, colors.grey)
624
  ]))
 
625
  story.append(table_client)
626
  story.append(Spacer(1, 20))
627
 
 
628
  story.append(Paragraph("2. CARACTÉRISTIQUES DU PRÊT", style_section))
 
629
  type_labels = {
630
  "IN_FINE": "In Fine (versement unique)",
631
  "MENSUEL_INTERETS": "Mensuel - Intérêts périodiques",
 
633
  "HEBDOMADAIRE": "Hebdomadaire",
634
  "PERSONNALISE": "Périodicité personnalisée"
635
  }
 
636
  data_pret = [
637
  ["Type de prêt", type_labels.get(loan_data['Type_Pret'], loan_data['Type_Pret'])],
638
  ["Montant du capital", f"{loan_data['Montant_Capital']:,} XOF".replace(",", " ")],
 
645
  ["Date de déblocage", loan_data['Date_Deblocage']],
646
  ["Date de fin", loan_data['Date_Fin']]
647
  ]
 
648
  table_pret = Table(data_pret, colWidths=[7*cm, 9*cm])
649
  table_pret.setStyle(TableStyle([
650
  ('BACKGROUND', (0, 0), (0, -1), colors.HexColor("#e8f0f7")),
 
655
  ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
656
  ('GRID', (0, 0), (-1, -1), 0.5, colors.grey)
657
  ]))
 
658
  story.append(table_pret)
659
  story.append(PageBreak())
660
 
 
661
  story.append(Paragraph("3. TABLEAU D'AMORTISSEMENT", style_section))
662
  story.append(Spacer(1, 10))
 
 
663
  data_amort = [["Période", "Date", "Capital", "Intérêts", "Versement", "Solde Restant"]]
 
664
  for _, row in df_amortissement.iterrows():
665
  data_amort.append([
666
  str(row['Periode']),
 
670
  f"{row['Versement']:,}".replace(",", " "),
671
  f"{row['Solde_Restant']:,}".replace(",", " ")
672
  ])
 
673
  table_amort = Table(data_amort, colWidths=[2*cm, 3*cm, 3*cm, 3*cm, 3*cm, 3*cm])
674
  table_amort.setStyle(TableStyle([
675
  ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#1f4788")),
 
682
  ('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
683
  ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor("#f5f5f5")])
684
  ]))
 
685
  story.append(table_amort)
686
  story.append(Spacer(1, 30))
687
 
 
688
  story.append(Paragraph("4. SIGNATURES", style_section))
689
  story.append(Spacer(1, 20))
 
690
  data_signatures = [
691
  ["Le Prêteur", "Le Bénéficiaire"],
692
  ["", ""],
 
694
  ["Signature :", "Signature :"],
695
  ["", ""]
696
  ]
 
697
  table_sign = Table(data_signatures, colWidths=[8*cm, 8*cm], rowHeights=[0.8*cm, 2*cm, 0.8*cm, 0.8*cm, 2*cm])
698
  table_sign.setStyle(TableStyle([
699
  ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
 
701
  ('FONTSIZE', (0, 0), (-1, -1), 10),
702
  ('VALIGN', (0, 0), (-1, -1), 'TOP')
703
  ]))
 
704
  story.append(table_sign)
705
 
 
706
  doc.build(story)
707
  buffer.seek(0)
708
  return buffer
709
 
710
+ # ============================================================================
711
+ # INTERFACE PRINCIPALE STREAMLIT
712
+ # ============================================================================
713
+
714
  def show_loans_engine(client, sheet_name):
715
  st.header("MOTEUR FINANCIER : OCTROI DE PRÊT")
716
 
 
717
  st.subheader("Sélection du Bénéficiaire")
718
 
719
  try:
 
752
 
753
  st.divider()
754
 
 
755
  st.subheader("⚙️ Configuration du Prêt")
756
 
757
  type_pret = st.radio(
 
768
 
769
  st.divider()
770
 
 
771
  col1, col2 = st.columns(2)
772
 
773
  with col1:
 
775
  with col2:
776
  taux_hebdo = st.number_input("📊 Taux hebdomadaire (%)", min_value=0.1, max_value=50.0, value=2.0, step=0.1)
777
 
 
778
  montant_versement = 0
779
  montant_total = 0
780
  cout_credit = 0
 
785
  type_code = ""
786
  date_debut = date.today()
787
 
788
+ # ====================================================================
789
+ # LOGIQUE CONDITIONNELLE SELON LE TYPE
790
+ # ====================================================================
791
 
792
  if "In Fine" in type_pret:
793
  type_code = "IN_FINE"
 
935
  for idx, dt in enumerate(dates_versements):
936
  st.write(f"• Versement {idx + 1} : {dt.strftime('%d/%m/%Y')} → {round(montant_versement):,} XOF".replace(",", " "))
937
 
938
+ # ====================================================================
939
+ # ANALYSE DE CAPACITÉ DE REMBOURSEMENT EN TEMPS RÉEL
940
+ # ====================================================================
941
+ if montant > 0 and taux_hebdo > 0 and (montant_versement > 0 or (type_code == "PERSONNALISE" and duree_semaines > 0) or duree_semaines > 0):
 
 
 
942
  try:
943
  charges_mensuelles = float(str(client_info['Charges_Estimees']).replace(" ", "").replace("XOF", "").replace(",", ""))
944
  except:
 
946
 
947
  revenus_mensuels = float(client_info['Revenus_Mensuels'])
948
 
949
+ # Appel de la fonction d'analyse centralisée
950
+ analyse = analyser_capacite_remboursement(
951
+ type_code=type_code,
952
+ montant=montant,
953
+ taux_hebdo=taux_hebdo,
954
+ duree_semaines=duree_semaines,
955
+ montant_versement=montant_versement,
956
+ nb_versements=nb_versements,
957
+ revenus_mensuels=revenus_mensuels,
958
+ charges_mensuelles=charges_mensuelles,
959
+ montant_total=montant_total,
960
+ cout_credit=cout_credit
961
+ )
962
+
963
+ if analyse:
964
+ st.divider()
965
+ st.subheader("🔍 Analyse de capacité de remboursement")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
966
 
967
+ # Affichage du statut avec couleur
968
+ if analyse['couleur'] == "green":
969
+ st.success(f"**{analyse['statut']}** - {analyse['message']}")
970
+ elif analyse['couleur'] == "blue":
971
+ st.info(f"**{analyse['statut']}** - {analyse['message']}")
972
+ elif analyse['couleur'] == "orange":
973
+ st.warning(f"**{analyse['statut']}** - {analyse['message']}")
 
 
974
  else:
975
+ st.error(f"**{analyse['statut']}** - {analyse['message']}")
 
 
976
 
977
+ # Affichage des métriques principales
978
+ metriques = analyse['metriques']
 
 
 
 
979
 
980
+ if analyse['type_analyse'] == "liquidite_directe":
981
+ col1, col2, col3, col4 = st.columns(4)
982
+ col1.metric("Revenus cumulés", f"{int(metriques['revenus_cumules']):,} XOF".replace(",", " "))
983
+ col2.metric("Disponible", f"{int(metriques['disponible_echeance']):,} XOF".replace(",", " "))
984
+ col3.metric("Montant requis", f"{int(metriques['montant_requis']):,} XOF".replace(",", " "))
985
+ col4.metric("Marge de sécurité", f"{int(metriques['marge_securite']):,} XOF".replace(",", " "))
986
+
987
+ elif analyse['type_analyse'] == "epargne_progressive":
988
+ col1, col2, col3, col4 = st.columns(4)
989
+ col1.metric("Revenus hebdo", f"{int(metriques['revenus_hebdo']):,} XOF".replace(",", " "))
990
+ col2.metric("Épargne hebdo nécessaire", f"{int(metriques['epargne_hebdo_necessaire']):,} XOF".replace(",", " "))
991
+ col3.metric("Capacité d'épargne", f"{int(metriques['capacite_epargne_hebdo']):,} XOF".replace(",", " "))
992
+ col4.metric("Taux d'effort", f"{metriques['taux_effort_epargne']:.1f}%")
993
 
994
+ elif analyse['type_analyse'] == "double_phase":
995
+ st.markdown("**Phase courante (Intérêts seuls) :**")
996
+ col1, col2, col3 = st.columns(3)
997
+ col1.metric("Intérêts mensuels", f"{int(metriques['interet_mensuel']):,} XOF".replace(",", " "))
998
+ col2.metric("Taux d'endettement", f"{metriques['taux_endettement_courant']:.1f}%")
999
+ col3.metric("Reste à vivre", f"{int(metriques['reste_vivre_courant']):,} XOF".replace(",", " "))
1000
+
1001
+ st.markdown("**Phase finale (Capital + Intérêts) :**")
1002
+ col4, col5, col6 = st.columns(3)
1003
+ col4.metric("Épargne mensuelle", f"{int(metriques['epargne_mensuelle_necessaire']):,} XOF".replace(",", " "))
1004
+ col5.metric("Taux d'effort total", f"{metriques['taux_effort_total']:.1f}%")
1005
+ col6.metric("Reste à vivre", f"{int(metriques['reste_vivre_final']):,} XOF".replace(",", " "))
1006
+
1007
+ elif analyse['type_analyse'] == "endettement_classique":
1008
+ col1, col2, col3 = st.columns(3)
1009
+ col1.metric("Mensualité", f"{int(metriques['mensualite']):,} XOF".replace(",", " "))
1010
+ col2.metric("Taux d'endettement", f"{metriques['taux_endettement']:.1f}%")
1011
+ col3.metric("Reste à vivre", f"{int(metriques['reste_vivre']):,} XOF".replace(",", " "))
1012
+
1013
+ elif analyse['type_analyse'] == "rythme_hebdomadaire":
1014
+ st.markdown("**Capacité hebdomadaire :**")
1015
+ col1, col2, col3 = st.columns(3)
1016
+ col1.metric("Versement hebdo", f"{int(metriques['versement_hebdo']):,} XOF".replace(",", " "))
1017
+ col2.metric("Taux d'endettement", f"{metriques['taux_endettement_hebdo']:.1f}%")
1018
+ col3.metric("Reste à vivre", f"{int(metriques['reste_vivre_hebdo']):,} XOF".replace(",", " "))
1019
+
1020
+ st.markdown("**Équivalent mensuel :**")
1021
+ col4, col5, col6 = st.columns(3)
1022
+ col4.metric("Charge mensuelle", f"{int(metriques['charge_mensuelle_equiv']):,} XOF".replace(",", " "))
1023
+ col5.metric("Taux d'endettement", f"{metriques['taux_endettement_mensuel']:.1f}%")
1024
+ col6.metric("Reste à vivre", f"{int(metriques['reste_vivre_mensuel']):,} XOF".replace(",", " "))
1025
+
1026
+ elif analyse['type_analyse'] == "calendrier_personnalise":
1027
+ col1, col2, col3, col4 = st.columns(4)
1028
+ col1.metric("Durée", f"{int(metriques['duree_semaines'])} semaines")
1029
+ col2.metric("Nb versements", metriques['nb_versements'])
1030
+ col3.metric("Versement moyen", f"{int(metriques['versement_moyen']):,} XOF".replace(",", " "))
1031
+ col4.metric("Taux d'effort", f"{metriques['taux_effort_moyen']:.1f}%")
1032
+
1033
+ # Détails et recommandations
1034
  with st.expander("📊 Détails de l'analyse"):
1035
+ st.markdown(analyse['details'])
 
 
 
 
 
 
1036
 
1037
+ if analyse['recommandations']:
1038
+ st.markdown("**Recommandations :**")
1039
+ for reco in analyse['recommandations']:
1040
+ st.write(f"- {reco}")
1041
+
1042
+ # ====================================================================
1043
+ # TABLEAU D'AMORTISSEMENT
1044
+ # ====================================================================
1045
  if montant_versement > 0 or type_code == "PERSONNALISE":
1046
  st.divider()
1047
  st.subheader("📋 Tableau d'amortissement détaillé")
 
1065
  }
1066
  )
1067
 
1068
+ # ====================================================================
1069
+ # VALIDATION & ENREGISTREMENT
1070
+ # ====================================================================
1071
  st.divider()
1072
  with st.form("loan_confirmation"):
1073
  st.markdown("### ✅ Validation de l'engagement")
 
1083
 
1084
  dates_str = ";".join([d.strftime("%d/%m/%Y") for d in dates_versements]) if dates_versements else ""
1085
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1086
  new_loan_row = [
1087
+ loan_id,
1088
+ client_id,
1089
+ client_info['Nom_Complet'],
1090
+ type_code,
1091
+ montant,
1092
+ taux_hebdo,
1093
+ duree_semaines,
1094
+ round(montant_versement) if montant_versement > 0 else 0,
1095
+ round(montant_total),
1096
+ round(cout_credit),
1097
+ nb_versements,
1098
+ dates_str,
1099
+ date_debut.strftime("%d/%m/%Y"),
1100
+ date_fin.strftime("%d/%m/%Y") if date_fin else "",
1101
+ "ACTIF",
1102
+ datetime.now().strftime("%d-%m-%Y %H:%M:%S")
1103
  ]
1104
 
1105
  ws_prets.append_row(new_loan_row)
1106
  st.success(f"✅ Prêt {loan_id} accordé au client {client_id} !")
1107
  st.balloons()
1108
 
 
1109
  st.divider()
1110
  st.subheader("📄 Export du contrat")
1111