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

Update src/Analytics/AnalyseRepayment.py

Browse files
Files changed (1) hide show
  1. src/Analytics/AnalyseRepayment.py +402 -0
src/Analytics/AnalyseRepayment.py CHANGED
@@ -0,0 +1,402 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ from datetime import datetime, timedelta
3
+ import math
4
+
5
+ class AnalyseRepayment:
6
+ """
7
+ Classe pour gérer toute la logique métier des remboursements
8
+ """
9
+
10
+ def __init__(self, df_prets, df_remboursements, df_ajustements=None):
11
+ """
12
+ Initialise l'analyse avec les données des prêts et remboursements
13
+
14
+ Args:
15
+ df_prets: DataFrame de Prets_Master
16
+ df_remboursements: DataFrame de Remboursements
17
+ df_ajustements: DataFrame de Ajustements_Echeances (optionnel)
18
+ """
19
+ self.df_prets = df_prets
20
+ self.df_remboursements = df_remboursements
21
+ self.df_ajustements = df_ajustements if df_ajustements is not None else pd.DataFrame()
22
+
23
+ def get_loan_data(self, id_pret):
24
+ """Récupère les données d'un prêt spécifique"""
25
+ loan = self.df_prets[self.df_prets['ID_Pret'] == id_pret]
26
+ if loan.empty:
27
+ return None
28
+ return loan.iloc[0]
29
+
30
+ def get_previous_payments(self, id_pret):
31
+ """Récupère tous les paiements antérieurs pour un prêt"""
32
+ if self.df_remboursements.empty or 'ID_Pret' not in self.df_remboursements.columns:
33
+ return []
34
+
35
+ payments = self.df_remboursements[self.df_remboursements['ID_Pret'] == id_pret]
36
+ return payments.to_dict('records') if not payments.empty else []
37
+
38
+ def parse_echeances(self, dates_versements_str):
39
+ """
40
+ Parse la chaîne de dates d'échéances
41
+
42
+ Args:
43
+ dates_versements_str: "04/01/2026,11/01/2026,18/01/2026"
44
+
45
+ Returns:
46
+ Liste de datetime.date
47
+ """
48
+ if not dates_versements_str or pd.isna(dates_versements_str):
49
+ return []
50
+
51
+ dates_str = dates_versements_str.split(',')
52
+ echeances = []
53
+
54
+ for d in dates_str:
55
+ d = d.strip()
56
+ if d:
57
+ try:
58
+ # Format attendu : DD/MM/YYYY
59
+ date_obj = datetime.strptime(d, "%d/%m/%Y").date()
60
+ echeances.append(date_obj)
61
+ except ValueError:
62
+ continue
63
+
64
+ return echeances
65
+
66
+ def detecter_echeance_attendue(self, id_pret, date_paiement):
67
+ """
68
+ Détecte quelle échéance correspond au paiement
69
+
70
+ Returns:
71
+ dict: {
72
+ 'numero': 3,
73
+ 'total': 10,
74
+ 'date_prevue': date(2026,1,18),
75
+ 'montant_echeance': 10600,
76
+ 'montant_ajuste': 14200 # Si ajustement
77
+ }
78
+ """
79
+ loan_data = self.get_loan_data(id_pret)
80
+ if loan_data is None:
81
+ return None
82
+
83
+ # Parser les échéances
84
+ echeances = self.parse_echeances(loan_data['Dates_Versements'])
85
+ if not echeances:
86
+ return None
87
+
88
+ # Compter les paiements déjà effectués
89
+ previous_payments = self.get_previous_payments(id_pret)
90
+ nb_paiements_faits = len(previous_payments)
91
+
92
+ # L'échéance attendue est la suivante
93
+ numero_echeance = nb_paiements_faits + 1
94
+
95
+ # Si on dépasse le nombre d'échéances, on prend la dernière
96
+ if numero_echeance > len(echeances):
97
+ numero_echeance = len(echeances)
98
+
99
+ date_prevue = echeances[numero_echeance - 1]
100
+ montant_echeance_base = loan_data['Montant_Versement']
101
+
102
+ # Vérifier s'il y a un ajustement pour cette échéance
103
+ montant_ajuste = montant_echeance_base
104
+ if not self.df_ajustements.empty:
105
+ ajustements = self.df_ajustements[
106
+ (self.df_ajustements['ID_Pret'] == id_pret) &
107
+ (self.df_ajustements['Numero_Echeance'] == numero_echeance)
108
+ ]
109
+ if not ajustements.empty:
110
+ montant_additionnel = ajustements['Montant_Additionnel'].sum()
111
+ montant_ajuste = montant_echeance_base + montant_additionnel
112
+
113
+ return {
114
+ 'numero': numero_echeance,
115
+ 'total': len(echeances),
116
+ 'date_prevue': date_prevue,
117
+ 'montant_echeance': montant_echeance_base,
118
+ 'montant_ajuste': montant_ajuste
119
+ }
120
+
121
+ def calculer_penalites(self, montant_echeance, date_echeance_prevue, date_paiement, taux_hebdo=0.05):
122
+ """
123
+ Calcule les pénalités de retard
124
+
125
+ Args:
126
+ montant_echeance: Montant de l'échéance
127
+ date_echeance_prevue: Date attendue
128
+ date_paiement: Date effective
129
+ taux_hebdo: Taux par semaine (défaut: 5%)
130
+
131
+ Returns:
132
+ dict: {
133
+ 'jours_retard': 7,
134
+ 'semaines_retard': 1,
135
+ 'montant_penalites': 530
136
+ }
137
+ """
138
+ if isinstance(date_paiement, str):
139
+ date_paiement = datetime.strptime(date_paiement, "%Y-%m-%d").date()
140
+ if isinstance(date_echeance_prevue, str):
141
+ date_echeance_prevue = datetime.strptime(date_echeance_prevue, "%Y-%m-%d").date()
142
+
143
+ jours_retard = (date_paiement - date_echeance_prevue).days
144
+
145
+ if jours_retard <= 0:
146
+ return {
147
+ 'jours_retard': jours_retard,
148
+ 'semaines_retard': 0,
149
+ 'montant_penalites': 0
150
+ }
151
+
152
+ # Calcul par semaine entamée
153
+ semaines_retard = math.ceil(jours_retard / 7)
154
+ montant_penalites = montant_echeance * taux_hebdo * semaines_retard
155
+
156
+ return {
157
+ 'jours_retard': jours_retard,
158
+ 'semaines_retard': semaines_retard,
159
+ 'montant_penalites': round(montant_penalites, 0)
160
+ }
161
+
162
+ def generer_scenarios_penalites(self, montant_echeance, date_echeance_prevue, date_paiement):
163
+ """
164
+ Génère 3 scénarios de paiement
165
+
166
+ Returns:
167
+ list: [scenario1, scenario2, scenario3]
168
+ """
169
+ # Scénario 1: Avec pénalités réglementaires (5%)
170
+ scenario1 = self.calculer_penalites(montant_echeance, date_echeance_prevue, date_paiement, taux_hebdo=0.05)
171
+ scenario1['label'] = "Règlement Standard"
172
+ scenario1['taux'] = 5.0
173
+ scenario1['total_a_encaisser'] = montant_echeance + scenario1['montant_penalites']
174
+
175
+ # Scénario 2: Geste commercial (0%)
176
+ scenario2 = {
177
+ 'label': "Geste Commercial",
178
+ 'taux': 0.0,
179
+ 'jours_retard': scenario1['jours_retard'],
180
+ 'semaines_retard': 0,
181
+ 'montant_penalites': 0,
182
+ 'total_a_encaisser': montant_echeance
183
+ }
184
+
185
+ # Scénario 3: Taux personnalisé (défaut: 2.5%)
186
+ scenario3 = self.calculer_penalites(montant_echeance, date_echeance_prevue, date_paiement, taux_hebdo=0.025)
187
+ scenario3['label'] = "Taux Personnalisé"
188
+ scenario3['taux'] = 2.5
189
+ scenario3['total_a_encaisser'] = montant_echeance + scenario3['montant_penalites']
190
+
191
+ return [scenario1, scenario2, scenario3]
192
+
193
+ def decomposer_versement(self, loan_data, numero_echeance, montant_verse, penalites=0):
194
+ """
195
+ Décompose un versement en Principal + Intérêts
196
+
197
+ Args:
198
+ loan_data: Données du prêt
199
+ numero_echeance: Numéro de l'échéance
200
+ montant_verse: Montant total payé par le client
201
+ penalites: Montant des pénalités
202
+
203
+ Returns:
204
+ dict: {
205
+ 'montant_principal': 9434,
206
+ 'montant_interets': 1166,
207
+ 'montant_penalites': 530,
208
+ 'montant_pour_dette': 10600 # Montant qui réduit la dette
209
+ }
210
+ """
211
+ montant_total = loan_data['Montant_Total']
212
+ montant_capital = loan_data['Montant_Capital']
213
+ nb_versements = loan_data['Nb_Versements']
214
+
215
+ # Intérêts totaux du prêt
216
+ interets_totaux = montant_total - montant_capital
217
+
218
+ # Répartition linéaire
219
+ interets_par_echeance = interets_totaux / nb_versements
220
+ principal_par_echeance = montant_capital / nb_versements
221
+
222
+ # Les pénalités sont prélevées en priorité
223
+ montant_pour_dette = montant_verse - penalites
224
+
225
+ # Si le montant est insuffisant pour couvrir l'échéance complète
226
+ montant_echeance_theorique = principal_par_echeance + interets_par_echeance
227
+
228
+ if montant_pour_dette >= montant_echeance_theorique:
229
+ # Paiement complet ou supérieur
230
+ return {
231
+ 'montant_principal': round(principal_par_echeance, 0),
232
+ 'montant_interets': round(interets_par_echeance, 0),
233
+ 'montant_penalites': round(penalites, 0),
234
+ 'montant_pour_dette': round(montant_pour_dette, 0)
235
+ }
236
+ else:
237
+ # Paiement partiel : on priorise les intérêts puis le principal
238
+ if montant_pour_dette >= interets_par_echeance:
239
+ montant_interets = interets_par_echeance
240
+ montant_principal = montant_pour_dette - interets_par_echeance
241
+ else:
242
+ montant_interets = montant_pour_dette
243
+ montant_principal = 0
244
+
245
+ return {
246
+ 'montant_principal': round(montant_principal, 0),
247
+ 'montant_interets': round(montant_interets, 0),
248
+ 'montant_penalites': round(penalites, 0),
249
+ 'montant_pour_dette': round(montant_pour_dette, 0)
250
+ }
251
+
252
+ def calculer_soldes(self, loan_data, previous_payments, montant_pour_dette):
253
+ """
254
+ Calcule le solde avant et après paiement
255
+
256
+ Returns:
257
+ dict: {
258
+ 'solde_avant': 84800,
259
+ 'solde_apres': 74200
260
+ }
261
+ """
262
+ montant_total_du = loan_data['Montant_Total']
263
+
264
+ # Somme des paiements précédents (uniquement la partie qui réduit la dette)
265
+ total_rembourse = sum(
266
+ p.get('Montant_Principal', 0) + p.get('Montant_Interets', 0)
267
+ for p in previous_payments
268
+ )
269
+
270
+ solde_avant = montant_total_du - total_rembourse
271
+ solde_apres = solde_avant - montant_pour_dette
272
+
273
+ return {
274
+ 'solde_avant': round(max(0, solde_avant), 0),
275
+ 'solde_apres': round(max(0, solde_apres), 0)
276
+ }
277
+
278
+ def determiner_statut_paiement(self, jours_retard, montant_verse, montant_echeance_ajuste):
279
+ """
280
+ Détermine le statut du paiement
281
+
282
+ Returns:
283
+ str: PONCTUEL / EN_RETARD / ANTICIPE / PARTIEL
284
+ """
285
+ # Tolérance de 1%
286
+ tolerance = montant_echeance_ajuste * 0.01
287
+ montant_min_accepte = montant_echeance_ajuste - tolerance
288
+
289
+ # Priorité 1: Retard
290
+ if jours_retard > 0:
291
+ return "EN_RETARD"
292
+
293
+ # Priorité 2: Paiement partiel
294
+ if montant_verse < montant_min_accepte:
295
+ return "PARTIEL"
296
+
297
+ # Priorité 3: Anticipé
298
+ if jours_retard < 0:
299
+ return "ANTICIPE"
300
+
301
+ # Par défaut: Ponctuel
302
+ return "PONCTUEL"
303
+
304
+ def calculer_montant_a_reporter(self, montant_echeance_ajuste, montant_verse, penalites):
305
+ """
306
+ Calcule le montant à reporter sur l'échéance suivante en cas de paiement partiel
307
+
308
+ Returns:
309
+ float: Montant à ajouter à la prochaine échéance
310
+ """
311
+ montant_pour_dette = montant_verse - penalites
312
+ montant_manquant = montant_echeance_ajuste - montant_pour_dette
313
+
314
+ return max(0, montant_manquant)
315
+
316
+ def analyser_remboursement_complet(self, id_pret, date_paiement, montant_verse, taux_penalite=0.05):
317
+ """
318
+ Analyse complète d'un remboursement
319
+
320
+ Args:
321
+ id_pret: ID du prêt
322
+ date_paiement: Date du paiement (date ou str)
323
+ montant_verse: Montant payé par le client
324
+ taux_penalite: Taux de pénalité appliqué (0.05 = 5%)
325
+
326
+ Returns:
327
+ dict: Toutes les informations calculées
328
+ """
329
+ # Conversion de la date si nécessaire
330
+ if isinstance(date_paiement, str):
331
+ date_paiement = datetime.strptime(date_paiement, "%Y-%m-%d").date()
332
+
333
+ # 1. Récupération des données
334
+ loan_data = self.get_loan_data(id_pret)
335
+ if loan_data is None:
336
+ return {'error': 'Prêt non trouvé'}
337
+
338
+ previous_payments = self.get_previous_payments(id_pret)
339
+
340
+ # 2. Détection de l'échéance
341
+ echeance_info = self.detecter_echeance_attendue(id_pret, date_paiement)
342
+ if echeance_info is None:
343
+ return {'error': 'Impossible de déterminer l\'échéance'}
344
+
345
+ # 3. Calcul des pénalités
346
+ penalites_info = self.calculer_penalites(
347
+ echeance_info['montant_ajuste'],
348
+ echeance_info['date_prevue'],
349
+ date_paiement,
350
+ taux_hebdo=taux_penalite
351
+ )
352
+
353
+ # 4. Décomposition du versement
354
+ decomposition = self.decomposer_versement(
355
+ loan_data,
356
+ echeance_info['numero'],
357
+ montant_verse,
358
+ penalites=penalites_info['montant_penalites']
359
+ )
360
+
361
+ # 5. Calcul des soldes
362
+ soldes = self.calculer_soldes(
363
+ loan_data,
364
+ previous_payments,
365
+ decomposition['montant_pour_dette']
366
+ )
367
+
368
+ # 6. Détermination du statut
369
+ statut = self.determiner_statut_paiement(
370
+ penalites_info['jours_retard'],
371
+ montant_verse,
372
+ echeance_info['montant_ajuste']
373
+ )
374
+
375
+ # 7. Calcul du montant à reporter si partiel
376
+ montant_a_reporter = 0
377
+ if statut == "PARTIEL":
378
+ montant_a_reporter = self.calculer_montant_a_reporter(
379
+ echeance_info['montant_ajuste'],
380
+ montant_verse,
381
+ penalites_info['montant_penalites']
382
+ )
383
+
384
+ # Compilation des résultats
385
+ return {
386
+ 'id_pret': id_pret,
387
+ 'id_client': loan_data['ID_Client'],
388
+ 'numero_echeance': f"{echeance_info['numero']}/{echeance_info['total']}",
389
+ 'date_echeance_prevue': echeance_info['date_prevue'],
390
+ 'montant_echeance': echeance_info['montant_echeance'],
391
+ 'montant_ajuste': echeance_info['montant_ajuste'],
392
+ 'jours_retard': penalites_info['jours_retard'],
393
+ 'montant_verse': montant_verse,
394
+ 'montant_principal': decomposition['montant_principal'],
395
+ 'montant_interets': decomposition['montant_interets'],
396
+ 'penalites_retard': decomposition['montant_penalites'],
397
+ 'solde_avant': soldes['solde_avant'],
398
+ 'solde_apres': soldes['solde_apres'],
399
+ 'statut_paiement': statut,
400
+ 'montant_a_reporter': round(montant_a_reporter, 0),
401
+ 'prochaine_echeance': echeance_info['numero'] + 1 if echeance_info['numero'] < echeance_info['total'] else None
402
+ }