Spaces:
Running
Running
Update src/modules/loans_engine.py
Browse files- 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 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 123 |
-
|
| 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 |
-
#
|
|
|
|
|
|
|
| 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 |
-
#
|
| 496 |
-
#
|
| 497 |
-
|
| 498 |
-
|
| 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 |
-
#
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 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 |
-
#
|
| 546 |
-
if
|
| 547 |
-
statut
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
elif
|
| 551 |
-
statut
|
| 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
|
| 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 |
-
|
| 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 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
|
| 574 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
with st.expander("📊 Détails de l'analyse"):
|
| 576 |
-
st.
|
| 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 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
#
|
| 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 |
-
#
|
|
|
|
|
|
|
| 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,
|
| 652 |
-
client_id,
|
| 653 |
-
client_info['Nom_Complet'],
|
| 654 |
-
type_code,
|
| 655 |
-
montant,
|
| 656 |
-
taux_hebdo,
|
| 657 |
-
duree_semaines,
|
| 658 |
-
round(montant_versement) if montant_versement > 0 else 0,
|
| 659 |
-
round(montant_total),
|
| 660 |
-
round(cout_credit),
|
| 661 |
-
nb_versements,
|
| 662 |
-
dates_str,
|
| 663 |
-
date_debut.strftime("%d/%m/%Y"),
|
| 664 |
-
date_fin.strftime("%d/%m/%Y") if date_fin else "",
|
| 665 |
-
"ACTIF",
|
| 666 |
-
datetime.now().strftime("%d-%m-%Y %H:%M:%S")
|
| 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 |
|