#Ce fichier est pour les initiations aux prêts et pour les modifications des prêts #Il est utiliser dans loans et dans check up import pandas as pd from datetime import date, timedelta # ============================================================================ # CONSTANTES ET SEUILS # ============================================================================ SEUILS = { "taux_endettement_mensuel": { "excellent": 25, "bon": 33, "acceptable": 40, "critique": 50 }, "taux_effort_epargne": { "excellent": 20, "bon": 30, "acceptable": 40, "critique": 50 }, "reste_a_vivre_min": { "excellent": 50000, "bon": 30000, "acceptable": 20000, "critique": 15000 }, "liquidite_min_ratio": 1.2, "duree_courte_semaines": 6 } # ============================================================================ # FONCTIONS DE NETTOYAGE # ============================================================================ def clean_currency_value(val): """Convertit une valeur monétaire (string ou float) en float propre""" try: if isinstance(val, (int, float)): return float(val) cleaned = str(val).upper().replace("XOF", "").replace("FCFA", "").replace(" ", "").replace(",", "") return float(cleaned) if cleaned else 0.0 except (ValueError, AttributeError): return 0.0 def clean_taux_value(val): """ Convertit un taux D'INTÉRÊT correctement. Gère : "5,3%" → 5.3 | "5.3%" → 5.3 | "5.3" → 5.3 | 53 → 5.3 ⚠️ ATTENTION : Cette fonction divise par 10 si > 20%, donc à utiliser UNIQUEMENT pour les taux d'intérêt ! Pour les taux d'endettement, utiliser clean_percentage_value() """ try: if isinstance(val, str): val = val.replace('%', '').replace(' ', '').replace(',', '.') taux = float(val) # Division par 10 si > 20% (logique spécifique aux taux d'intérêt) if taux > 20: taux = taux / 10 return round(taux, 2) except (ValueError, AttributeError, TypeError): return 0.0 def clean_percentage_value(val): """ Convertit un POURCENTAGE (taux d'endettement, etc.) SANS division par 10. Gère : "54,94%" → 54.94 | "54.94" → 54.94 | "54,94" → 54.94 | 0.5494 → 54.94 | 5494 → 54.94 """ import numpy as np # ✅ Import numpy pour gérer les types numpy try: # ✅ GESTION DES TYPES NUMPY (int64, float64, etc.) if isinstance(val, (int, float, np.integer, np.floating)): taux = float(val) # ✅ Si > 100, c'est probablement stocké comme 5494 au lieu de 54.94 # On divise par 100 if taux > 100: taux = taux / 100 # Si entre 0 et 1, c'est une fraction (0.5494 → 54.94%) elif 0 < taux < 1: taux = taux * 100 return round(taux, 2) # ✅ GESTION DES STRINGS elif isinstance(val, str): # Retirer les symboles et normaliser les séparateurs décimaux val = val.replace('%', '').replace(' ', '').replace(',', '.') taux = float(val) # Même logique if taux > 100: taux = taux / 100 elif 0 < taux < 1: taux = taux * 100 return round(taux, 2) else: return 0.0 except (ValueError, AttributeError, TypeError): return 0.0 # ============================================================================ # ✅ NOUVEAU — IPF : INDICE DE PRESSION FAMILIALE # ============================================================================ def calculer_ipf(revenus_mensuels, pers_charge): """ Calcule l'IPF (Indice de Pression Familiale) = Revenus / (1 + Pers_Charge). L'IPF reflète le revenu réellement disponible par unité de ménage. Activé uniquement si le client a au moins 1 personne à charge (colonne Pers_Charge du CSV KYC Vortex-Flux). Args: revenus_mensuels (float | str) : Revenus mensuels bruts du client. pers_charge (int) : Nombre de personnes à charge (0 = pas d'ajustement). Returns: float : IPF arrondi à 2 décimales. Identique aux revenus si pers_charge == 0. """ revenus_mensuels = clean_currency_value(revenus_mensuels) try: pers_charge = int(pers_charge) except (ValueError, TypeError): pers_charge = 0 if pers_charge <= 0: return round(revenus_mensuels, 2) return round(revenus_mensuels / (1 + pers_charge), 2) def get_revenus_ajustes(revenus_mensuels, pers_charge): """ Retourne la base de revenus à utiliser pour le calcul du taux d'endettement. - pers_charge >= 1 → IPF = Revenus / (1 + Pers_Charge) - pers_charge == 0 → revenus bruts (aucun ajustement) Args: revenus_mensuels (float | str) : Revenus mensuels du client. pers_charge (int) : Nombre de personnes à charge (colonne Pers_Charge du KYC). Returns: tuple(float, bool) : (revenus_base, ipf_applique) - revenus_base : montant à utiliser comme dénominateur du taux d'endettement - ipf_applique : True si l'IPF a été appliqué (pour affichage dans l'UI) """ try: pers_charge = int(pers_charge) except (ValueError, TypeError): pers_charge = 0 ipf = calculer_ipf(revenus_mensuels, pers_charge) ipf_applique = pers_charge >= 1 return ipf, ipf_applique # ============================================================================ # FONCTIONS DE CALCUL DU TAUX D'ENDETTEMENT # ============================================================================ def calculer_taux_endettement(type_code, montant_versement, nb_versements, duree_semaines, revenus_mensuels, pers_charge=0): """ Calcule le taux d'endettement mensuel selon le type de prêt. Retourne un float arrondi à 2 décimales. ✅ IPF : Si pers_charge >= 1 (colonne Pers_Charge du KYC), le calcul utilise l'IPF = Revenus / (1 + Pers_Charge) comme base au lieu des revenus bruts. """ # ✅ Base de revenus ajustée par l'IPF si nécessaire revenus_base, _ = get_revenus_ajustes(revenus_mensuels, pers_charge) if revenus_base == 0: return 0.0 # Calcul de la charge mensuelle selon le type if type_code == "IN_FINE": duree_mois = duree_semaines / 4.33 if duree_mois < 2: charge_mensuelle = montant_versement / duree_mois else: charge_mensuelle = 0 elif type_code == "MENSUEL_INTERETS": charge_mensuelle = montant_versement elif type_code == "MENSUEL_CONSTANT": charge_mensuelle = montant_versement elif type_code == "HEBDOMADAIRE": charge_mensuelle = montant_versement * 4.33 elif type_code == "PERSONNALISE": charge_mensuelle = montant_versement * (nb_versements / (duree_semaines / 4.33)) if duree_semaines > 0 else montant_versement else: charge_mensuelle = 0 taux = (charge_mensuelle / revenus_base) * 100 return round(taux, 2) # ============================================================================ # LOGIQUE D'AMORTISSEMENT # ============================================================================ def generer_tableau_amortissement(type_code, montant, taux_hebdo, duree_semaines, montant_versement, nb_versements, date_debut, dates_versements=None): tableau = [] if type_code == "IN_FINE": date_fin = date_debut + timedelta(weeks=duree_semaines) montant_total = montant * (1 + (taux_hebdo / 100) * duree_semaines) interets = montant_total - montant tableau.append({ "Periode": 1, "Date": date_fin.strftime("%d/%m/%Y"), "Capital": round(montant), "Interets": round(interets), "Versement": round(montant_total), "Solde_Restant": 0 }) elif type_code == "MENSUEL_INTERETS": duree_mois = nb_versements taux_mensuel = (taux_hebdo / 100) * 4.33 interet_mensuel = montant * taux_mensuel solde = montant for mois in range(1, duree_mois + 1): date_echeance = date_debut + timedelta(days=30 * mois) if mois == duree_mois: capital_paye = montant versement = montant + interet_mensuel solde = 0 else: capital_paye = 0 versement = interet_mensuel tableau.append({ "Periode": mois, "Date": date_echeance.strftime("%d/%m/%Y"), "Capital": round(capital_paye), "Interets": round(interet_mensuel), "Versement": round(versement), "Solde_Restant": round(solde) }) elif type_code == "MENSUEL_CONSTANT": duree_mois = nb_versements taux_mensuel = (taux_hebdo / 100) * 4.33 solde = montant for mois in range(1, duree_mois + 1): date_echeance = date_debut + timedelta(days=30 * mois) interets = solde * taux_mensuel capital_paye = montant_versement - interets solde -= capital_paye tableau.append({ "Periode": mois, "Date": date_echeance.strftime("%d/%m/%Y"), "Capital": round(capital_paye), "Interets": round(interets), "Versement": round(montant_versement), "Solde_Restant": max(0, round(solde)) }) elif type_code == "HEBDOMADAIRE": taux_hebdo_decimal = taux_hebdo / 100 solde = montant for semaine in range(1, int(duree_semaines) + 1): date_echeance = date_debut + timedelta(weeks=semaine) interets = solde * taux_hebdo_decimal capital_paye = montant_versement - interets solde -= capital_paye tableau.append({ "Periode": semaine, "Date": date_echeance.strftime("%d/%m/%Y"), "Capital": round(capital_paye), "Interets": round(interets), "Versement": round(montant_versement), "Solde_Restant": max(0, round(solde)) }) elif type_code == "PERSONNALISE" and dates_versements: # ✅ CORRECTION : Pour type PERSONNALISÉ, le solde doit partir du MONTANT TOTAL # Car les intérêts sont répartis sur toute la durée montant_total_pret = montant * (1 + (taux_hebdo / 100) * duree_semaines) solde = montant_total_pret # ✅ On part du montant TOTAL (capital + intérêts) for idx, date_v in enumerate(dates_versements): solde_apres = max(0, solde - montant_versement) # Répartition capital/intérêts proportionnelle ratio_interets = (taux_hebdo / 100) * duree_semaines interets_part = (montant_versement * ratio_interets) / (1 + ratio_interets) capital_part = montant_versement - interets_part tableau.append({ "Periode": idx + 1, "Date": date_v.strftime("%d/%m/%Y"), "Capital": round(capital_part), "Interets": round(interets_part), "Versement": round(montant_versement), "Solde_Restant": round(solde_apres) }) solde = solde_apres return pd.DataFrame(tableau) # ============================================================================ # LOGIQUE D'ANALYSE (CORRIGÉE - TOUS LES TYPES) # ============================================================================ def analyser_capacite(type_code, montant, taux_hebdo, duree_semaines, montant_versement, nb_versements, revenus_mensuels, charges_mensuelles, montant_total=0, cout_credit=0, pers_charge=0): """ Analyse la capacité de remboursement du client. ✅ Nouveau paramètre : pers_charge (int) — colonne Pers_Charge du KYC CSV. Si >= 1, tous les calculs de taux d'endettement et reste à vivre utilisent l'IPF = Revenus / (1 + Pers_Charge) au lieu des revenus bruts, afin de refléter la pression financière réelle du foyer. """ revenus_mensuels = clean_currency_value(revenus_mensuels) charges_mensuelles = clean_currency_value(charges_mensuelles) # ✅ Calcul de la base de revenus ajustée par l'IPF revenus_base, ipf_applique = get_revenus_ajustes(revenus_mensuels, pers_charge) ipf_note = ( f"\n📊 IPF appliqué ({int(pers_charge)} pers. à charge) : " f"base revenus = {int(revenus_base):,} XOF " f"(au lieu de {int(revenus_mensuels):,} XOF bruts)" ) if ipf_applique else "" # --- LOGIQUE IN FINE --- if type_code == "IN_FINE": if duree_semaines < SEUILS["duree_courte_semaines"]: duree_mois = duree_semaines / 4.33 revenus_cumules = revenus_base * duree_mois # ✅ IPF charges_cumules = charges_mensuelles * duree_mois disponible_echeance = revenus_cumules - charges_cumules marge_securite = disponible_echeance - montant_total ratio_couverture = disponible_echeance / montant_total if montant_total > 0 else 0 if ratio_couverture >= 1.5: statut, couleur, msg = "EXCELLENT", "green", "Le client dispose de liquidités très confortables." elif ratio_couverture >= SEUILS["liquidite_min_ratio"]: statut, couleur, msg = "BON", "blue", "Liquidités suffisantes." elif ratio_couverture >= 1.0: statut, couleur, msg = "ACCEPTABLE", "orange", "⚠️ Liquidités justes sans marge d'erreur." else: statut, couleur, msg = "INSUFFISANT", "red", "❌ ATTENTION : Liquidités insuffisantes." details = f"**Analyse Liquidité Directe**\nRatio couverture: {ratio_couverture:.2f}x\nDisponible: {int(disponible_echeance):,} XOF{ipf_note}" return {"statut": statut, "couleur": couleur, "message": msg, "details": details, "metriques": {"ratio": ratio_couverture, "ipf_applique": ipf_applique, "revenus_base": revenus_base}} else: # Épargne progressive epargne_hebdo = montant_total / duree_semaines revenus_hebdo = revenus_base / 4.33 # ✅ IPF taux_effort = (epargne_hebdo / revenus_hebdo * 100) if revenus_hebdo > 0 else 0 if taux_effort <= SEUILS["taux_effort_epargne"]["excellent"]: statut, couleur, msg = "EXCELLENT", "green", "Capacité d'épargne très confortable." elif taux_effort <= SEUILS["taux_effort_epargne"]["acceptable"]: statut, couleur, msg = "ACCEPTABLE", "orange", "⚠️ Capacité d'épargne limitée." else: statut, couleur, msg = "INSUFFISANT", "red", "❌ Capacité insuffisante." details = f"**Analyse Épargne**\nTaux effort: {taux_effort:.1f}%\nEpargne req/sem: {int(epargne_hebdo):,} XOF{ipf_note}" return {"statut": statut, "couleur": couleur, "message": msg, "details": details, "metriques": {"taux_effort": taux_effort, "ipf_applique": ipf_applique, "revenus_base": revenus_base}} # --- LOGIQUE MENSUEL - INTÉRÊTS --- elif type_code == "MENSUEL_INTERETS": duree_mois = nb_versements taux_mensuel = (taux_hebdo / 100) * 4.33 interet_mensuel = montant * taux_mensuel # ✅ IPF taux_endettement = (interet_mensuel / revenus_base * 100) if revenus_base > 0 else 0 reste_vivre = revenus_base - charges_mensuelles - interet_mensuel epargne_necessaire = montant / duree_mois ratio_epargne = (epargne_necessaire / reste_vivre * 100) if reste_vivre > 0 else 0 if taux_endettement <= SEUILS["taux_endettement_mensuel"]["excellent"] and reste_vivre >= SEUILS["reste_a_vivre_min"]["bon"]: statut, couleur, msg = "EXCELLENT", "green", "Capacité très confortable pour les intérêts mensuels." elif taux_endettement <= SEUILS["taux_endettement_mensuel"]["bon"] and reste_vivre >= SEUILS["reste_a_vivre_min"]["acceptable"]: statut, couleur, msg = "BON", "blue", "Bonne capacité de paiement des intérêts." elif taux_endettement <= SEUILS["taux_endettement_mensuel"]["acceptable"]: statut, couleur, msg = "ACCEPTABLE", "orange", "⚠️ Charge acceptable mais attention au capital final." else: statut, couleur, msg = "INSUFFISANT", "red", "🚨 Risque élevé : intérêts mensuels trop lourds." if ratio_epargne > 50: msg += f"\n⚠️ ATTENTION : Le capital final ({int(montant):,} XOF) nécessiterait une épargne de {int(epargne_necessaire):,} XOF/mois." details = f"""**Analyse Mensuel - Intérêts** Taux endettement (intérêts seuls): {taux_endettement:.1f}% Intérêts mensuels: {int(interet_mensuel):,} XOF Reste à vivre: {int(reste_vivre):,} XOF Capital final à prévoir: {int(montant):,} XOF au mois {duree_mois}{ipf_note}""" return {"statut": statut, "couleur": couleur, "message": msg, "details": details, "metriques": {"taux_endettement": taux_endettement, "reste_vivre": reste_vivre, "ipf_applique": ipf_applique, "revenus_base": revenus_base}} # --- LOGIQUE MENSUEL CONSTANT --- elif type_code == "MENSUEL_CONSTANT": # ✅ IPF taux_endettement = (montant_versement / revenus_base * 100) if revenus_base > 0 else 0 reste_vivre = revenus_base - charges_mensuelles - montant_versement if taux_endettement <= SEUILS["taux_endettement_mensuel"]["excellent"] and reste_vivre >= SEUILS["reste_a_vivre_min"]["excellent"]: statut, couleur, msg = "EXCELLENT", "green", "Capacité très confortable." elif taux_endettement <= SEUILS["taux_endettement_mensuel"]["acceptable"]: statut, couleur, msg = "ACCEPTABLE", "orange", "⚠️ Marge limitée." else: statut, couleur, msg = "INSUFFISANT", "red", "🚨 Risque élevé." details = f"**Analyse Endettement**\nTaux: {taux_endettement:.1f}%\nReste à vivre: {int(reste_vivre):,} XOF{ipf_note}" return {"statut": statut, "couleur": couleur, "message": msg, "details": details, "metriques": {"taux_endettement": taux_endettement, "ipf_applique": ipf_applique, "revenus_base": revenus_base}} # --- LOGIQUE HEBDOMADAIRE --- elif type_code == "HEBDOMADAIRE": charge_mensuelle = montant_versement * 4.33 # ✅ IPF taux_endettement = (charge_mensuelle / revenus_base * 100) if revenus_base > 0 else 0 if taux_endettement <= SEUILS["taux_endettement_mensuel"]["bon"]: statut, couleur, msg = "BON", "blue", "Bonne capacité hebdo." elif taux_endettement <= SEUILS["taux_endettement_mensuel"]["critique"]: statut, couleur, msg = "LIMITE", "red", "🚨 Charge hebdo élevée." else: statut, couleur, msg = "INSUFFISANT", "darkred", "❌ Trop lourd." details = f"**Analyse Rythme Hebdo**\nCharge mensuelle équivalente: {int(charge_mensuelle):,} XOF\nTaux: {taux_endettement:.1f}%{ipf_note}" return {"statut": statut, "couleur": couleur, "message": msg, "details": details, "metriques": {"taux_endettement": taux_endettement, "ipf_applique": ipf_applique, "revenus_base": revenus_base}} # --- LOGIQUE PERSONNALISÉ --- elif type_code == "PERSONNALISE": charge_mensuelle = montant_versement * (nb_versements / (duree_semaines / 4.33)) if duree_semaines > 0 else montant_versement # ✅ IPF taux_endettement = (charge_mensuelle / revenus_base * 100) if revenus_base > 0 else 0 if taux_endettement <= SEUILS["taux_endettement_mensuel"]["bon"]: statut, couleur, msg = "BON", "blue", "Rythme personnalisé acceptable." elif taux_endettement <= SEUILS["taux_endettement_mensuel"]["acceptable"]: statut, couleur, msg = "ACCEPTABLE", "orange", "⚠️ Rythme à surveiller." else: statut, couleur, msg = "INSUFFISANT", "red", "🚨 Rythme trop exigeant." details = f"**Analyse Rythme Personnalisé**\nCharge mensuelle moyenne: {int(charge_mensuelle):,} XOF\nTaux: {taux_endettement:.1f}%{ipf_note}" return {"statut": statut, "couleur": couleur, "message": msg, "details": details, "metriques": {"taux_endettement": taux_endettement, "ipf_applique": ipf_applique, "revenus_base": revenus_base}} # --- PAR DEFAUT (Ne devrait plus arriver) --- return {"statut": "INCONNU", "couleur": "grey", "message": "Type non analysé", "details": "", "metriques": {}}