klydekushy commited on
Commit
ee2d1bd
·
verified ·
1 Parent(s): 3307ddd

Update src/modules/loans_engine.py

Browse files
Files changed (1) hide show
  1. src/modules/loans_engine.py +112 -1043
src/modules/loans_engine.py CHANGED
@@ -1,24 +1,14 @@
1
  import streamlit as st
2
  import pandas as pd
3
  from datetime import datetime, date, timedelta
4
- from io import BytesIO
5
- from reportlab.lib.pagesizes import A4
6
- from reportlab.lib import colors
7
- from reportlab.lib.units import cm
8
- from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, PageBreak
9
- from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
10
- from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
11
 
12
  # ============================================================================
13
- # STYLES CSS SPÉCIFIQUES AU MODULE (ISOLÉS)
14
  # ============================================================================
15
-
16
  def apply_loans_engine_styles():
17
- """
18
- Applique les styles CSS spécifiques au module de gestion de prêts.
19
- Tous les sélecteurs sont préfixés avec #loans-engine-module pour l'isolation.
20
- Les styles globaux (polices, couleurs de base) sont hérités du CSS global.
21
- """
22
  st.markdown("""
23
  <style>
24
  /* ========================================
@@ -319,1060 +309,139 @@ def apply_loans_engine_styles():
319
  """, unsafe_allow_html=True)
320
 
321
  # ============================================================================
322
- # SEUILS ADAPTATIFS PAR CONTEXTE
323
- # ============================================================================
324
-
325
- SEUILS = {
326
- "taux_endettement_mensuel": {
327
- "excellent": 25,
328
- "bon": 33,
329
- "acceptable": 40,
330
- "critique": 50
331
- },
332
- "taux_effort_epargne": {
333
- "excellent": 20,
334
- "bon": 30,
335
- "acceptable": 40,
336
- "critique": 50
337
- },
338
- "reste_a_vivre_min": {
339
- "excellent": 50000,
340
- "bon": 30000,
341
- "acceptable": 20000,
342
- "critique": 15000
343
- },
344
- "liquidite_min_ratio": 1.2,
345
- "duree_courte_semaines": 6
346
- }
347
-
348
- # ============================================================================
349
- # FONCTION CENTRALISÉE D'ANALYSE DE CAPACITÉ
350
- # ============================================================================
351
-
352
- def analyser_capacite_remboursement(
353
- type_code, montant, taux_hebdo, duree_semaines,
354
- montant_versement, nb_versements,
355
- revenus_mensuels, charges_mensuelles,
356
- montant_total=0, cout_credit=0
357
- ):
358
- """Analyse la capacité de remboursement selon le type de prêt."""
359
-
360
- revenus_mensuels = float(revenus_mensuels)
361
- charges_mensuelles = float(charges_mensuelles)
362
-
363
- # IN FINE
364
- if type_code == "IN_FINE":
365
- if duree_semaines < SEUILS["duree_courte_semaines"]:
366
- # LIQUIDITÉ DIRECTE
367
- duree_mois = duree_semaines / 4.33
368
- revenus_cumules = revenus_mensuels * duree_mois
369
- charges_cumules = charges_mensuelles * duree_mois
370
- disponible_echeance = revenus_cumules - charges_cumules
371
- marge_securite = disponible_echeance - montant_total
372
- ratio_couverture = disponible_echeance / montant_total if montant_total > 0 else 0
373
-
374
- metriques = {
375
- "revenus_cumules": revenus_cumules,
376
- "charges_cumules": charges_cumules,
377
- "disponible_echeance": disponible_echeance,
378
- "montant_requis": montant_total,
379
- "marge_securite": marge_securite,
380
- "ratio_couverture": ratio_couverture
381
- }
382
-
383
- if ratio_couverture >= 1.5:
384
- statut, couleur = "EXCELLENT", "green"
385
- message = "Le client dispose de liquidités très confortables avec une excellente marge de sécurité."
386
- elif ratio_couverture >= SEUILS["liquidite_min_ratio"]:
387
- statut, couleur = "BON", "blue"
388
- message = "Liquidités suffisantes avec une marge de sécurité acceptable."
389
- elif ratio_couverture >= 1.0:
390
- statut, couleur = "ACCEPTABLE", "orange"
391
- message = "⚠️ Liquidités justes. Le client peut payer mais sans marge d'erreur."
392
- else:
393
- statut, couleur = "INSUFFISANT", "red"
394
- message = "❌ ATTENTION : Liquidités insuffisantes pour honorer le versement unique."
395
-
396
- details = f"""
397
- **🔍 ANALYSE DE LIQUIDITÉ DIRECTE (Prêt court terme)**
398
-
399
- 📅 **Période :** {duree_semaines} semaines ({duree_mois:.2f} mois)
400
-
401
- **Flux de trésorerie jusqu'à l'échéance :**
402
- - 💵 Revenus cumulés : {int(revenus_cumules):,} XOF
403
- - ❌ Charges cumulées : {int(charges_cumules):,} XOF
404
- - 💰 **Disponible à l'échéance : {int(disponible_echeance):,} XOF**
405
-
406
- **Capacité de paiement :**
407
- - ✅ Montant requis : {int(montant_total):,} XOF
408
- - 📊 Marge de sécurité : {int(marge_securite):,} XOF ({(ratio_couverture - 1) * 100:.0f}%)
409
- - 📈 Ratio de couverture : {ratio_couverture:.2f}x
410
-
411
- **Seuils de référence :**
412
- - ✅ Excellent : > 1.5x | 🔵 Bon : > 1.2x | 🟠 Acceptable : ≥ 1.0x | ❌ Insuffisant : < 1.0x
413
- """.replace(",", " ")
414
-
415
- recommandations = []
416
- if ratio_couverture < 1.0:
417
- recommandations.append("🚨 Refuser le prêt ou réduire le montant")
418
- recommandations.append(f"💡 Montant maximum recommandé : {int(disponible_echeance * 0.8):,} XOF".replace(",", " "))
419
- elif ratio_couverture < SEUILS["liquidite_min_ratio"]:
420
- recommandations.append("⚠️ Surveillance étroite recommandée")
421
- recommandations.append("📞 Rappel de paiement 1 semaine avant l'échéance")
422
-
423
- return {
424
- "type_analyse": "liquidite_directe",
425
- "metriques": metriques,
426
- "statut": statut,
427
- "couleur": couleur,
428
- "message": message,
429
- "details": details,
430
- "recommandations": recommandations
431
- }
432
-
433
- else:
434
- # ÉPARGNE PROGRESSIVE
435
- epargne_hebdo_necessaire = montant_total / duree_semaines
436
- revenus_hebdo = revenus_mensuels / 4.33
437
- charges_hebdo = charges_mensuelles / 4.33
438
- capacite_epargne_hebdo = revenus_hebdo - charges_hebdo
439
- taux_effort_epargne = (epargne_hebdo_necessaire / revenus_hebdo * 100) if revenus_hebdo > 0 else 0
440
-
441
- metriques = {
442
- "epargne_hebdo_necessaire": epargne_hebdo_necessaire,
443
- "revenus_hebdo": revenus_hebdo,
444
- "charges_hebdo": charges_hebdo,
445
- "capacite_epargne_hebdo": capacite_epargne_hebdo,
446
- "taux_effort_epargne": taux_effort_epargne
447
- }
448
-
449
- if taux_effort_epargne <= SEUILS["taux_effort_epargne"]["excellent"]:
450
- statut, couleur = "EXCELLENT", "green"
451
- message = "Capacité d'épargne très confortable. Risque minimal."
452
- elif taux_effort_epargne <= SEUILS["taux_effort_epargne"]["bon"]:
453
- statut, couleur = "BON", "blue"
454
- message = "Bonne capacité d'épargne progressive."
455
- elif taux_effort_epargne <= SEUILS["taux_effort_epargne"]["acceptable"]:
456
- statut, couleur = "ACCEPTABLE", "orange"
457
- message = "⚠️ Capacité d'épargne limitée. Nécessite une discipline financière stricte."
458
- elif taux_effort_epargne <= SEUILS["taux_effort_epargne"]["critique"]:
459
- statut, couleur = "LIMITE", "red"
460
- message = "🚨 Capacité d'épargne très tendue. Risque élevé de défaut."
461
- else:
462
- statut, couleur = "INSUFFISANT", "darkred"
463
- message = "❌ ATTENTION : Capacité d'épargne insuffisante pour constituer le capital."
464
-
465
- details = f"""
466
- **🔍 ANALYSE DE CAPACITÉ D'ÉPARGNE (Prêt long terme)**
467
-
468
- 📅 **Période :** {duree_semaines} semaines
469
-
470
- **Épargne nécessaire :**
471
- - 💰 Épargne hebdomadaire requise : {int(epargne_hebdo_necessaire):,} XOF/semaine
472
- - 📊 Montant total à constituer : {int(montant_total):,} XOF
473
-
474
- **Capacité financière hebdomadaire :**
475
- - 💵 Revenus hebdo : {int(revenus_hebdo):,} XOF
476
- - ❌ Charges hebdo : {int(charges_hebdo):,} XOF
477
- - 💰 Capacité d'épargne : {int(capacite_epargne_hebdo):,} XOF
478
-
479
- **Taux d'effort d'épargne : {taux_effort_epargne:.1f}%** des revenus hebdomadaires
480
-
481
- **Seuils :** ✅ <{SEUILS["taux_effort_epargne"]["excellent"]}% | 🔵 <{SEUILS["taux_effort_epargne"]["bon"]}% | 🟠 <{SEUILS["taux_effort_epargne"]["acceptable"]}%
482
- """.replace(",", " ")
483
-
484
- recommandations = []
485
- if taux_effort_epargne > SEUILS["taux_effort_epargne"]["critique"]:
486
- recommandations.append("🚨 Refuser le prêt ou proposer un étalement plus long")
487
- epargne_max = capacite_epargne_hebdo * 0.4
488
- duree_recommandee = montant_total / epargne_max if epargne_max > 0 else 0
489
- recommandations.append(f"💡 Durée minimale recommandée : {int(duree_recommandee)} semaines")
490
- elif taux_effort_epargne > SEUILS["taux_effort_epargne"]["acceptable"]:
491
- recommandations.append("⚠️ Exiger des garanties supplémentaires")
492
- recommandations.append("📞 Suivi mensuel de l'épargne constituée")
493
-
494
- return {
495
- "type_analyse": "epargne_progressive",
496
- "metriques": metriques,
497
- "statut": statut,
498
- "couleur": couleur,
499
- "message": message,
500
- "details": details,
501
- "recommandations": recommandations
502
- }
503
-
504
- # MENSUEL - INTÉRÊTS PÉRIODIQUES
505
- elif type_code == "MENSUEL_INTERETS":
506
- taux_mensuel = (taux_hebdo / 100) * 4.33
507
- interet_mensuel = montant * taux_mensuel
508
-
509
- taux_endettement_courant = (interet_mensuel / revenus_mensuels * 100) if revenus_mensuels > 0 else 0
510
- reste_vivre_courant = revenus_mensuels - charges_mensuelles - interet_mensuel
511
-
512
- epargne_mensuelle_necessaire = montant / nb_versements
513
- charge_totale_finale = interet_mensuel + epargne_mensuelle_necessaire
514
- taux_effort_total = (charge_totale_finale / revenus_mensuels * 100) if revenus_mensuels > 0 else 0
515
- reste_vivre_final = revenus_mensuels - charges_mensuelles - charge_totale_finale
516
-
517
- metriques = {
518
- "interet_mensuel": interet_mensuel,
519
- "taux_endettement_courant": taux_endettement_courant,
520
- "reste_vivre_courant": reste_vivre_courant,
521
- "epargne_mensuelle_necessaire": epargne_mensuelle_necessaire,
522
- "charge_totale_finale": charge_totale_finale,
523
- "taux_effort_total": taux_effort_total,
524
- "reste_vivre_final": reste_vivre_final
525
- }
526
-
527
- phase_courante_ok = (taux_endettement_courant <= SEUILS["taux_endettement_mensuel"]["acceptable"]
528
- and reste_vivre_courant >= SEUILS["reste_a_vivre_min"]["acceptable"])
529
-
530
- phase_finale_ok = (taux_effort_total <= SEUILS["taux_effort_epargne"]["acceptable"]
531
- and reste_vivre_final >= SEUILS["reste_a_vivre_min"]["acceptable"])
532
-
533
- if phase_courante_ok and phase_finale_ok:
534
- if (taux_endettement_courant <= SEUILS["taux_endettement_mensuel"]["excellent"]
535
- and taux_effort_total <= SEUILS["taux_effort_epargne"]["bon"]):
536
- statut, couleur = "EXCELLENT", "green"
537
- message = "Le client peut gérer les intérêts mensuels ET constituer progressivement le capital."
538
- else:
539
- statut, couleur = "BON", "blue"
540
- message = "Capacité confirmée pour les deux phases du remboursement."
541
- elif phase_courante_ok and not phase_finale_ok:
542
- statut, couleur = "LIMITE", "orange"
543
- message = "⚠️ Le client peut payer les intérêts mais aura des difficultés à constituer le capital final."
544
- elif not phase_courante_ok and phase_finale_ok:
545
- statut, couleur = "LIMITE", "orange"
546
- message = "⚠️ Les intérêts mensuels sont élevés par rapport aux revenus."
547
- else:
548
- statut, couleur = "INSUFFISANT", "red"
549
- message = "❌ ATTENTION : Capacité insuffisante pour ce type de prêt."
550
-
551
- details = f"""
552
- **🔍 ANALYSE DOUBLE PHASE : Intérêts + Capital**
553
-
554
- 📅 **Structure du prêt :** {nb_versements} mois
555
-
556
- **PHASE 1 : Mois 1 à {nb_versements-1} (Intérêts seuls)**
557
- - 💳 Versement mensuel : {int(interet_mensuel):,} XOF
558
- - 📊 Taux d'endettement : {taux_endettement_courant:.1f}%
559
- - 💰 Reste à vivre : {int(reste_vivre_courant):,} XOF
560
- - {"✅ Phase gérable" if phase_courante_ok else "❌ Phase difficile"}
561
-
562
- **PHASE 2 : Mois {nb_versements} (Capital + Intérêts)**
563
- - 💵 Épargne mensuelle recommandée : {int(epargne_mensuelle_necessaire):,} XOF
564
- - 💳 Charge totale finale : {int(charge_totale_finale):,} XOF
565
- - 📊 Taux d'effort total : {taux_effort_total:.1f}%
566
- - 💰 Reste à vivre : {int(reste_vivre_final):,} XOF
567
- - {"✅ Capital constituable" if phase_finale_ok else "❌ Épargne difficile"}
568
- """.replace(",", " ")
569
-
570
- recommandations = []
571
- if not phase_courante_ok:
572
- recommandations.append("🚨 Réduire le montant ou proposer un taux plus bas")
573
- if not phase_finale_ok:
574
- recommandations.append("💡 Allonger la durée pour faciliter la constitution du capital")
575
- recommandations.append("📊 Proposer un prêt à mensualités constantes à la place")
576
- if phase_courante_ok and phase_finale_ok:
577
- recommandations.append("📞 Rappel de constitution d'épargne dès le 6ème mois")
578
-
579
- return {
580
- "type_analyse": "double_phase",
581
- "metriques": metriques,
582
- "statut": statut,
583
- "couleur": couleur,
584
- "message": message,
585
- "details": details,
586
- "recommandations": recommandations
587
- }
588
-
589
- # MENSUEL - MENSUALITÉS CONSTANTES
590
- elif type_code == "MENSUEL_CONSTANT":
591
- taux_endettement = (montant_versement / revenus_mensuels * 100) if revenus_mensuels > 0 else 0
592
- reste_vivre = revenus_mensuels - charges_mensuelles - montant_versement
593
-
594
- metriques = {
595
- "mensualite": montant_versement,
596
- "taux_endettement": taux_endettement,
597
- "reste_vivre": reste_vivre
598
- }
599
-
600
- if (taux_endettement <= SEUILS["taux_endettement_mensuel"]["excellent"]
601
- and reste_vivre >= SEUILS["reste_a_vivre_min"]["excellent"]):
602
- statut, couleur = "EXCELLENT", "green"
603
- message = "Capacité de remboursement très confortable. Risque minimal."
604
- elif (taux_endettement <= SEUILS["taux_endettement_mensuel"]["bon"]
605
- and reste_vivre >= SEUILS["reste_a_vivre_min"]["bon"]):
606
- statut, couleur = "BON", "blue"
607
- message = "Bonne capacité de remboursement mensuel."
608
- elif (taux_endettement <= SEUILS["taux_endettement_mensuel"]["acceptable"]
609
- and reste_vivre >= SEUILS["reste_a_vivre_min"]["acceptable"]):
610
- statut, couleur = "ACCEPTABLE", "orange"
611
- message = "⚠️ Capacité acceptable mais marge limitée. Surveillance recommandée."
612
- elif taux_endettement <= SEUILS["taux_endettement_mensuel"]["critique"]:
613
- statut, couleur = "LIMITE", "red"
614
- message = "🚨 Taux d'endettement élevé. Risque important de défaut."
615
- else:
616
- statut, couleur = "INSUFFISANT", "darkred"
617
- message = "❌ ATTENTION : Taux d'endettement critique. Refus recommandé."
618
-
619
- details = f"""
620
- **🔍 ANALYSE CLASSIQUE D'ENDETTEMENT**
621
-
622
- 📅 **Durée :** {nb_versements} mois
623
-
624
- **Flux mensuels :**
625
- - 💵 Revenus mensuels : {int(revenus_mensuels):,} XOF
626
- - ❌ Charges actuelles : {int(charges_mensuelles):,} XOF
627
- - 💳 Mensualité du prêt : {int(montant_versement):,} XOF
628
-
629
- **Indicateurs de solvabilité :**
630
- - 📊 **Taux d'endettement : {taux_endettement:.1f}%**
631
- - 💰 **Reste à vivre : {int(reste_vivre):,} XOF**
632
-
633
- **Seuils bancaires :** ✅ <{SEUILS["taux_endettement_mensuel"]["excellent"]}% | 🔵 <{SEUILS["taux_endettement_mensuel"]["bon"]}% | 🟠 <{SEUILS["taux_endettement_mensuel"]["acceptable"]}%
634
- """.replace(",", " ")
635
-
636
- recommandations = []
637
- if taux_endettement > SEUILS["taux_endettement_mensuel"]["critique"]:
638
- recommandations.append("🚨 Refuser le prêt")
639
- mensualite_max = revenus_mensuels * (SEUILS["taux_endettement_mensuel"]["acceptable"] / 100)
640
- recommandations.append(f"💡 Mensualité maximum recommandée : {int(mensualite_max):,} XOF".replace(",", " "))
641
- elif taux_endettement > SEUILS["taux_endettement_mensuel"]["acceptable"]:
642
- recommandations.append("⚠️ Exiger des garanties solides")
643
- recommandations.append("📊 Proposer d'allonger la durée pour réduire la mensualité")
644
- elif reste_vivre < SEUILS["reste_a_vivre_min"]["bon"]:
645
- recommandations.append("📞 Suivi mensuel de paiement recommandé")
646
-
647
- return {
648
- "type_analyse": "endettement_classique",
649
- "metriques": metriques,
650
- "statut": statut,
651
- "couleur": couleur,
652
- "message": message,
653
- "details": details,
654
- "recommandations": recommandations
655
- }
656
-
657
- # HEBDOMADAIRE
658
- elif type_code == "HEBDOMADAIRE":
659
- revenus_hebdo = revenus_mensuels / 4.33
660
- charges_hebdo = charges_mensuelles / 4.33
661
- taux_endettement_hebdo = (montant_versement / revenus_hebdo * 100) if revenus_hebdo > 0 else 0
662
- reste_vivre_hebdo = revenus_hebdo - charges_hebdo - montant_versement
663
-
664
- charge_mensuelle_equiv = montant_versement * 4.33
665
- taux_endettement_mensuel = (charge_mensuelle_equiv / revenus_mensuels * 100) if revenus_mensuels > 0 else 0
666
- reste_vivre_mensuel = revenus_mensuels - charges_mensuelles - charge_mensuelle_equiv
667
-
668
- metriques = {
669
- "versement_hebdo": montant_versement,
670
- "revenus_hebdo": revenus_hebdo,
671
- "charges_hebdo": charges_hebdo,
672
- "taux_endettement_hebdo": taux_endettement_hebdo,
673
- "reste_vivre_hebdo": reste_vivre_hebdo,
674
- "charge_mensuelle_equiv": charge_mensuelle_equiv,
675
- "taux_endettement_mensuel": taux_endettement_mensuel,
676
- "reste_vivre_mensuel": reste_vivre_mensuel
677
- }
678
-
679
- if (taux_endettement_mensuel <= SEUILS["taux_endettement_mensuel"]["excellent"]
680
- and reste_vivre_mensuel >= SEUILS["reste_a_vivre_min"]["excellent"]):
681
- statut, couleur = "EXCELLENT", "green"
682
- message = "Excellent rythme de remboursement hebdomadaire adapté aux revenus."
683
- elif (taux_endettement_mensuel <= SEUILS["taux_endettement_mensuel"]["bon"]
684
- and reste_vivre_mensuel >= SEUILS["reste_a_vivre_min"]["bon"]):
685
- statut, couleur = "BON", "blue"
686
- message = "Bonne capacité de paiement hebdomadaire."
687
- elif (taux_endettement_mensuel <= SEUILS["taux_endettement_mensuel"]["acceptable"]
688
- and reste_vivre_mensuel >= SEUILS["reste_a_vivre_min"]["acceptable"]):
689
- statut, couleur = "ACCEPTABLE", "orange"
690
- message = "⚠️ Capacité acceptable. Attention à la régularité des paiements."
691
- elif taux_endettement_mensuel <= SEUILS["taux_endettement_mensuel"]["critique"]:
692
- statut, couleur = "LIMITE", "red"
693
- message = "🚨 Charge hebdomadaire élevée. Risque de retards de paiement."
694
- else:
695
- statut, couleur = "INSUFFISANT", "darkred"
696
- message = "❌ ATTENTION : Versements hebdomadaires trop lourds pour les revenus."
697
-
698
- details = f"""
699
- **🔍 ANALYSE EN RYTHME HEBDOMADAIRE**
700
-
701
- 📅 **Durée :** {int(duree_semaines)} semaines
702
-
703
- **Capacité hebdomadaire :**
704
- - 💵 Revenus hebdo : {int(revenus_hebdo):,} XOF
705
- - ❌ Charges hebdo : {int(charges_hebdo):,} XOF
706
- - 💳 Versement hebdo : {int(montant_versement):,} XOF
707
- - 📊 Taux d'endettement : {taux_endettement_hebdo:.1f}%
708
- - 💰 Reste à vivre : {int(reste_vivre_hebdo):,} XOF
709
-
710
- **Équivalent mensuel (pour comparaison) :**
711
- - 💳 Charge mensuelle : {int(charge_mensuelle_equiv):,} XOF
712
- - 📊 Taux d'endettement : {taux_endettement_mensuel:.1f}%
713
- - 💰 Reste à vivre : {int(reste_vivre_mensuel):,} XOF
714
- """.replace(",", " ")
715
-
716
- recommandations = []
717
- if taux_endettement_mensuel > SEUILS["taux_endettement_mensuel"]["critique"]:
718
- recommandations.append("🚨 Refuser ou réduire drastiquement le montant")
719
- versement_max = revenus_hebdo * (SEUILS["taux_endettement_mensuel"]["acceptable"] / 100)
720
- recommandations.append(f"💡 Versement hebdo maximum : {int(versement_max):,} XOF".replace(",", " "))
721
- elif taux_endettement_mensuel > SEUILS["taux_endettement_mensuel"]["acceptable"]:
722
- recommandations.append("⚠️ Allonger la durée pour réduire le versement hebdomadaire")
723
- recommandations.append("📞 Système de rappel SMS avant chaque échéance")
724
- else:
725
- recommandations.append("✅ Rythme hebdomadaire adapté au profil")
726
- recommandations.append("📱 Mettre en place un système de paiement mobile")
727
-
728
- return {
729
- "type_analyse": "rythme_hebdomadaire",
730
- "metriques": metriques,
731
- "statut": statut,
732
- "couleur": couleur,
733
- "message": message,
734
- "details": details,
735
- "recommandations": recommandations
736
- }
737
-
738
- # PERSONNALISÉ
739
- elif type_code == "PERSONNALISE":
740
- if duree_semaines > 0 and montant_total > 0:
741
- epargne_moyenne_semaine = montant_total / duree_semaines
742
- revenus_hebdo = revenus_mensuels / 4.33
743
- charges_hebdo = charges_mensuelles / 4.33
744
- taux_effort_moyen = (epargne_moyenne_semaine / revenus_hebdo * 100) if revenus_hebdo > 0 else 0
745
-
746
- versement_moyen = montant_versement
747
- charge_mensuelle_moyenne = versement_moyen * (nb_versements / (duree_semaines / 4.33))
748
- taux_charge_moyenne = (charge_mensuelle_moyenne / revenus_mensuels * 100) if revenus_mensuels > 0 else 0
749
-
750
- metriques = {
751
- "epargne_moyenne_semaine": epargne_moyenne_semaine,
752
- "versement_moyen": versement_moyen,
753
- "taux_effort_moyen": taux_effort_moyen,
754
- "taux_charge_moyenne": taux_charge_moyenne,
755
- "nb_versements": nb_versements,
756
- "duree_semaines": duree_semaines
757
- }
758
-
759
- if taux_effort_moyen <= SEUILS["taux_effort_epargne"]["excellent"]:
760
- statut, couleur = "EXCELLENT", "green"
761
- message = "Calendrier personnalisé bien adapté aux capacités du client."
762
- elif taux_effort_moyen <= SEUILS["taux_effort_epargne"]["bon"]:
763
- statut, couleur = "BON", "blue"
764
- message = "Bon échelonnement des versements."
765
- elif taux_effort_moyen <= SEUILS["taux_effort_epargne"]["acceptable"]:
766
- statut, couleur = "ACCEPTABLE", "orange"
767
- message = "⚠️ Calendrier gérable mais nécessite une discipline stricte."
768
- elif taux_effort_moyen <= SEUILS["taux_effort_epargne"]["critique"]:
769
- statut, couleur = "LIMITE", "red"
770
- message = "🚨 Versements tendus. Risque de défaut sur certaines échéances."
771
- else:
772
- statut, couleur = "INSUFFISANT", "darkred"
773
- message = "❌ ATTENTION : Versements trop élevés pour les revenus."
774
-
775
- details = f"""
776
- **🔍 ANALYSE CALENDRIER PERSONNALISÉ**
777
-
778
- 📅 **Structure :**
779
- - Durée totale : {int(duree_semaines)} semaines
780
- - Nombre de versements : {nb_versements}
781
- - Versement moyen : {int(versement_moyen):,} XOF
782
-
783
- **Capacité d'épargne requise :**
784
- - 💰 Épargne hebdo moyenne : {int(epargne_moyenne_semaine):,} XOF
785
- - 💵 Revenus hebdo : {int(revenus_hebdo):,} XOF
786
- - 📊 Taux d'effort moyen : {taux_effort_moyen:.1f}%
787
- """.replace(",", " ")
788
-
789
- recommandations = []
790
- if taux_effort_moyen > SEUILS["taux_effort_epargne"]["critique"]:
791
- recommandations.append("🚨 Réduire le montant ou augmenter le nombre de versements")
792
- recommandations.append("📅 Espacer davantage les échéances")
793
- elif taux_effort_moyen > SEUILS["taux_effort_epargne"]["acceptable"]:
794
- recommandations.append("⚠️ Aligner les dates sur les pics de revenus du client")
795
- recommandations.append("📞 Rappel 3 jours avant chaque versement")
796
- else:
797
- recommandations.append("✅ Calendrier bien structuré")
798
- recommandations.append("💡 Automatiser les prélèvements si possible")
799
-
800
- return {
801
- "type_analyse": "calendrier_personnalise",
802
- "metriques": metriques,
803
- "statut": statut,
804
- "couleur": couleur,
805
- "message": message,
806
- "details": details,
807
- "recommandations": recommandations
808
- }
809
-
810
- return None
811
-
812
- # ============================================================================
813
- # FONCTIONS GÉNÉRATION TABLEAU + PDF
814
- # ============================================================================
815
-
816
- def generer_tableau_amortissement(type_code, montant, taux_hebdo, duree_semaines, montant_versement, nb_versements, date_debut, dates_versements=None):
817
- tableau = []
818
-
819
- if type_code == "IN_FINE":
820
- date_fin = date_debut + timedelta(weeks=duree_semaines)
821
- montant_total = montant * (1 + (taux_hebdo / 100) * duree_semaines)
822
- interets = montant_total - montant
823
- tableau.append({
824
- "Periode": 1,
825
- "Date": date_fin.strftime("%d/%m/%Y"),
826
- "Capital": round(montant),
827
- "Interets": round(interets),
828
- "Versement": round(montant_total),
829
- "Solde_Restant": 0
830
- })
831
-
832
- elif type_code == "MENSUEL_INTERETS":
833
- duree_mois = nb_versements
834
- taux_mensuel = (taux_hebdo / 100) * 4.33
835
- interet_mensuel = montant * taux_mensuel
836
- solde = montant
837
- for mois in range(1, duree_mois + 1):
838
- date_echeance = date_debut + timedelta(days=30 * mois)
839
- if mois == duree_mois:
840
- capital_paye = montant
841
- versement = montant + interet_mensuel
842
- solde = 0
843
- else:
844
- capital_paye = 0
845
- versement = interet_mensuel
846
- tableau.append({
847
- "Periode": mois,
848
- "Date": date_echeance.strftime("%d/%m/%Y"),
849
- "Capital": round(capital_paye),
850
- "Interets": round(interet_mensuel),
851
- "Versement": round(versement),
852
- "Solde_Restant": round(solde)
853
- })
854
-
855
- elif type_code == "MENSUEL_CONSTANT":
856
- duree_mois = nb_versements
857
- taux_mensuel = (taux_hebdo / 100) * 4.33
858
- solde = montant
859
- for mois in range(1, duree_mois + 1):
860
- date_echeance = date_debut + timedelta(days=30 * mois)
861
- interets = solde * taux_mensuel
862
- capital_paye = montant_versement - interets
863
- solde -= capital_paye
864
- tableau.append({
865
- "Periode": mois,
866
- "Date": date_echeance.strftime("%d/%m/%Y"),
867
- "Capital": round(capital_paye),
868
- "Interets": round(interets),
869
- "Versement": round(montant_versement),
870
- "Solde_Restant": max(0, round(solde))
871
- })
872
-
873
- elif type_code == "HEBDOMADAIRE":
874
- taux_hebdo_decimal = taux_hebdo / 100
875
- solde = montant
876
- for semaine in range(1, int(duree_semaines) + 1):
877
- date_echeance = date_debut + timedelta(weeks=semaine)
878
- interets = solde * taux_hebdo_decimal
879
- capital_paye = montant_versement - interets
880
- solde -= capital_paye
881
- tableau.append({
882
- "Periode": semaine,
883
- "Date": date_echeance.strftime("%d/%m/%Y"),
884
- "Capital": round(capital_paye),
885
- "Interets": round(interets),
886
- "Versement": round(montant_versement),
887
- "Solde_Restant": max(0, round(solde))
888
- })
889
-
890
- elif type_code == "PERSONNALISE":
891
- for idx, date_v in enumerate(dates_versements):
892
- tableau.append({
893
- "Periode": idx + 1,
894
- "Date": date_v.strftime("%d/%m/%Y"),
895
- "Capital": round(montant / nb_versements) if idx == len(dates_versements) - 1 else 0,
896
- "Interets": round(montant_versement - (montant / nb_versements)) if idx == len(dates_versements) - 1 else round(montant_versement),
897
- "Versement": round(montant_versement),
898
- "Solde_Restant": round(montant - (montant / nb_versements * (idx + 1)))
899
- })
900
-
901
- return pd.DataFrame(tableau)
902
-
903
- def generer_pdf_contrat(loan_data, client_info, df_amortissement):
904
- buffer = BytesIO()
905
- doc = SimpleDocTemplate(buffer, pagesize=A4, rightMargin=2*cm, leftMargin=2*cm, topMargin=2*cm, bottomMargin=2*cm)
906
- styles = getSampleStyleSheet()
907
- story = []
908
-
909
- style_titre = ParagraphStyle('CustomTitle', parent=styles['Heading1'], fontSize=18, textColor=colors.HexColor("#1f4788"), spaceAfter=30, alignment=TA_CENTER)
910
- style_section = ParagraphStyle('Section', parent=styles['Heading2'], fontSize=14, textColor=colors.HexColor("#2c5f99"), spaceAfter=12, spaceBefore=20)
911
-
912
- story.append(Paragraph("CONTRAT DE PRÊT", style_titre))
913
- story.append(Paragraph(f"N° {loan_data['ID_Pret']}", styles['Normal']))
914
- story.append(Spacer(1, 20))
915
-
916
- story.append(Paragraph("1. IDENTITÉ DU BÉNÉFICIAIRE", style_section))
917
- data_client = [
918
- ["Nom complet", client_info['Nom_Complet']],
919
- ["ID Client", client_info['ID_Client']],
920
- ["Statut professionnel", client_info['Statut_Pro']],
921
- ["Revenus mensuels", f"{client_info['Revenus_Mensuels']} XOF"],
922
- ["Ville", client_info['Ville']]
923
- ]
924
- table_client = Table(data_client, colWidths=[6*cm, 10*cm])
925
- table_client.setStyle(TableStyle([
926
- ('BACKGROUND', (0, 0), (0, -1), colors.HexColor("#e8f0f7")),
927
- ('TEXTCOLOR', (0, 0), (-1, -1), colors.black),
928
- ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
929
- ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
930
- ('FONTSIZE', (0, 0), (-1, -1), 10),
931
- ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
932
- ('GRID', (0, 0), (-1, -1), 0.5, colors.grey)
933
- ]))
934
- story.append(table_client)
935
- story.append(Spacer(1, 20))
936
-
937
- story.append(Paragraph("2. CARACTÉRISTIQUES DU PRÊT", style_section))
938
- type_labels = {
939
- "IN_FINE": "In Fine (versement unique)",
940
- "MENSUEL_INTERETS": "Mensuel - Intérêts périodiques",
941
- "MENSUEL_CONSTANT": "Mensuel - Mensualités constantes",
942
- "HEBDOMADAIRE": "Hebdomadaire",
943
- "PERSONNALISE": "Périodicité personnalisée"
944
- }
945
- data_pret = [
946
- ["Type de prêt", type_labels.get(loan_data['Type_Pret'], loan_data['Type_Pret'])],
947
- ["Moyen de transfert", loan_data['Moyen_Transfert']],
948
- ["Montant du capital", f"{loan_data['Montant_Capital']:,} XOF".replace(",", " ")],
949
- ["Taux hebdomadaire", f"{loan_data['Taux_Hebdo']} %"],
950
- ["Durée", f"{loan_data['Duree_Semaines']} semaines"],
951
- ["Montant du versement", f"{loan_data['Montant_Versement']:,} XOF".replace(",", " ") if loan_data['Montant_Versement'] > 0 else "Variable"],
952
- ["Nombre de versements", str(loan_data['Nb_Versements'])],
953
- ["Coût du crédit", f"{loan_data['Cout_Credit']:,} XOF".replace(",", " ")],
954
- ["Montant total à rembourser", f"{loan_data['Montant_Total']:,} XOF".replace(",", " ")],
955
- ["Date de déblocage", loan_data['Date_Deblocage']],
956
- ["Date de fin", loan_data['Date_Fin']]
957
- ]
958
- table_pret = Table(data_pret, colWidths=[7*cm, 9*cm])
959
- table_pret.setStyle(TableStyle([
960
- ('BACKGROUND', (0, 0), (0, -1), colors.HexColor("#e8f0f7")),
961
- ('TEXTCOLOR', (0, 0), (-1, -1), colors.black),
962
- ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
963
- ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
964
- ('FONTSIZE', (0, 0), (-1, -1), 10),
965
- ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
966
- ('GRID', (0, 0), (-1, -1), 0.5, colors.grey)
967
- ]))
968
- story.append(table_pret)
969
- story.append(PageBreak())
970
-
971
- story.append(Paragraph("3. TABLEAU D'AMORTISSEMENT", style_section))
972
- story.append(Spacer(1, 10))
973
- data_amort = [["Période", "Date", "Capital", "Intérêts", "Versement", "Solde Restant"]]
974
- for _, row in df_amortissement.iterrows():
975
- data_amort.append([
976
- str(row['Periode']),
977
- row['Date'],
978
- f"{row['Capital']:,}".replace(",", " "),
979
- f"{row['Interets']:,}".replace(",", " "),
980
- f"{row['Versement']:,}".replace(",", " "),
981
- f"{row['Solde_Restant']:,}".replace(",", " ")
982
- ])
983
- table_amort = Table(data_amort, colWidths=[2*cm, 3*cm, 3*cm, 3*cm, 3*cm, 3*cm])
984
- table_amort.setStyle(TableStyle([
985
- ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#1f4788")),
986
- ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
987
- ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
988
- ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
989
- ('FONTSIZE', (0, 0), (-1, 0), 10),
990
- ('FONTSIZE', (0, 1), (-1, -1), 9),
991
- ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
992
- ('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
993
- ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor("#f5f5f5")])
994
- ]))
995
- story.append(table_amort)
996
- story.append(Spacer(1, 30))
997
-
998
- story.append(Paragraph("4. SIGNATURES", style_section))
999
- story.append(Spacer(1, 20))
1000
- data_signatures = [
1001
- ["Le Prêteur", "Le Bénéficiaire"],
1002
- ["", ""],
1003
- ["Date : _______________", "Date : _______________"],
1004
- ["Signature :", "Signature :"],
1005
- ["", ""]
1006
- ]
1007
- table_sign = Table(data_signatures, colWidths=[8*cm, 8*cm], rowHeights=[0.8*cm, 2*cm, 0.8*cm, 0.8*cm, 2*cm])
1008
- table_sign.setStyle(TableStyle([
1009
- ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
1010
- ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
1011
- ('FONTSIZE', (0, 0), (-1, -1), 10),
1012
- ('VALIGN', (0, 0), (-1, -1), 'TOP')
1013
- ]))
1014
- story.append(table_sign)
1015
-
1016
- doc.build(story)
1017
- buffer.seek(0)
1018
- return buffer
1019
-
1020
- # ============================================================================
1021
- # INTERFACE PRINCIPALE - PARTIE 1 : Configuration complète de tous les types
1022
  # ============================================================================
1023
-
1024
  def show_loans_engine(client, sheet_name):
1025
- """Fonction principale du module avec wrapper d'isolation CSS."""
1026
-
1027
  apply_loans_engine_styles()
1028
  st.markdown('<div id="loans-engine-module">', unsafe_allow_html=True)
1029
 
1030
  st.header("MOTEUR FINANCIER : OCTROI DE PRÊT")
1031
- st.subheader("Sélection du Bénéficiaire")
1032
 
 
1033
  try:
1034
  sh = client.open(sheet_name)
1035
  ws_clients = sh.worksheet("Clients_KYC")
1036
- clients_data = ws_clients.get_all_records()
1037
- df_clients = pd.DataFrame(clients_data)
 
 
 
 
 
 
 
 
1038
  except Exception as e:
1039
- st.error(f"Erreur de connexion : {e}")
1040
- st.markdown('</div>', unsafe_allow_html=True)
1041
- return
1042
-
1043
- if df_clients.empty:
1044
- st.warning("⚠️ Aucun client trouvé. Créez un client avant d'octroyer un prêt.")
1045
- st.markdown('</div>', unsafe_allow_html=True)
1046
  return
1047
 
 
1048
  df_clients['search_label'] = df_clients['ID_Client'] + " - " + df_clients['Nom_Complet']
1049
-
1050
- st.markdown('<div class="loans-engine-client-selector">', unsafe_allow_html=True)
1051
- selected_label = st.selectbox(
1052
- "Rechercher un client (ID ou Nom)",
1053
- options=[""] + df_clients['search_label'].tolist(),
1054
- help="Tapez les premières lettres"
1055
- )
1056
- st.markdown('</div>', unsafe_allow_html=True)
1057
-
1058
- if selected_label:
1059
- client_info = df_clients[df_clients['search_label'] == selected_label].iloc[0]
 
 
 
1060
  client_id = client_info['ID_Client']
1061
 
1062
- st.markdown('<div class="loans-engine-solvency-alert">', unsafe_allow_html=True)
1063
- st.success(f"Cible identifiée : {client_info['Nom_Complet']} ({client_id})")
1064
- st.markdown('</div>', unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1065
 
1066
- with st.expander("Détails de solvabilité"):
1067
- col_a, col_b, col_c, col_d = st.columns(4)
1068
- col_a.metric("Revenus", f"{client_info['Revenus_Mensuels']} XOF")
1069
- col_b.metric("Charges", client_info['Charges_Estimees'])
1070
- col_c.metric("Statut Pro", client_info['Statut_Pro'])
1071
- col_d.metric("Ville", client_info['Ville'])
1072
 
1073
- st.markdown('<div class="loans-engine-section-divider"></div>', unsafe_allow_html=True)
1074
- st.subheader("Configuration du Prêt")
 
 
 
 
1075
 
1076
- type_pret = st.radio(
1077
- "Type de remboursement",
1078
- options=[
1079
- "In Fine (1 versement unique)",
1080
- "Mensuel - Intérêts mensuels + Capital final",
1081
- "Mensuel - Mensualités constantes",
1082
- "Hebdomadaire",
1083
- "Périodicité personnalisée"
1084
- ]
1085
  )
 
 
 
 
 
 
1086
 
1087
- st.markdown('<div class="loans-engine-section-divider"></div>', unsafe_allow_html=True)
1088
- col1, col2, col3 = st.columns(3)
1089
- with col1:
1090
- montant = st.number_input("Montant du Capital (XOF)", min_value=1000, step=10000, value=100000)
1091
- with col2:
1092
- taux_hebdo = st.number_input("Taux hebdomadaire (%)", min_value=0.1, max_value=50.0, value=2.0, step=0.1)
1093
- with col3:
1094
- moyen_transfert = st.selectbox(
1095
- "Moyen de transfert",
1096
- options=["Wave", "Orange Money", "Espèce", "Chèque", "Virement"],
1097
- index=0
1098
- )
1099
-
1100
- montant_versement = 0
1101
- montant_total = 0
1102
- cout_credit = 0
1103
- nb_versements = 0
1104
- dates_versements = []
1105
- duree_semaines = 0
1106
- date_fin = None
1107
- type_code = ""
1108
  date_debut = date.today()
 
 
1109
 
1110
- # Configuration selon type - CODE COMPLET ICI
1111
- if "In Fine" in type_pret:
1112
- type_code = "IN_FINE"
1113
- st.markdown("### Paramètres In Fine")
1114
- duree_semaines = st.number_input("Durée (en semaines)", min_value=1, max_value=104, value=8)
1115
- date_debut = st.date_input("Date de déblocage", value=date.today())
1116
- date_fin = date_debut + timedelta(weeks=duree_semaines)
1117
-
1118
- montant_total = montant * (1 + (taux_hebdo / 100) * duree_semaines)
1119
- cout_credit = montant_total - montant
1120
- montant_versement = montant_total
1121
- nb_versements = 1
1122
-
1123
- st.markdown('<div class="loans-engine-simulation-results">', unsafe_allow_html=True)
1124
- st.markdown("### Résultats de simulation")
1125
- res1, res2, res3 = st.columns(3)
1126
- res1.metric("Versement unique", f"{round(montant_versement):,} XOF".replace(",", " "))
1127
- res2.metric("Coût du Crédit", f"{round(cout_credit):,} XOF".replace(",", " "))
1128
- res3.metric("Date de remboursement", date_fin.strftime("%d/%m/%Y"))
1129
- st.markdown('</div>', unsafe_allow_html=True)
1130
-
1131
- elif "Intérêts mensuels" in type_pret:
1132
- type_code = "MENSUEL_INTERETS"
1133
- st.markdown("### Paramètres Mensuel (Intérêts)")
1134
- duree_mois = st.number_input("Durée (en mois)", min_value=1, max_value=60, value=12)
1135
- date_debut = st.date_input("Date de déblocage", value=date.today())
1136
-
1137
- duree_semaines = duree_mois * 4
1138
- taux_mensuel = (taux_hebdo / 100) * 4.33
1139
- interet_mensuel = montant * taux_mensuel
1140
-
1141
- montant_versement = interet_mensuel
1142
- montant_final_mois = montant + interet_mensuel
1143
- montant_total = (interet_mensuel * duree_mois) + montant
1144
- cout_credit = montant_total - montant
1145
- nb_versements = duree_mois
1146
-
1147
- st.markdown('<div class="loans-engine-simulation-results">', unsafe_allow_html=True)
1148
- st.markdown("### Résultats de simulation")
1149
- res1, res2, res3, res4 = st.columns(4)
1150
- res1.metric("Intérêts mensuels", f"{round(interet_mensuel):,} XOF".replace(",", " "))
1151
- res2.metric(f"Dernier versement", f"{round(montant_final_mois):,} XOF".replace(",", " "))
1152
- res3.metric("Coût du Crédit", f"{round(cout_credit):,} XOF".replace(",", " "))
1153
- res4.metric("Total", f"{round(montant_total):,} XOF".replace(",", " "))
1154
- st.markdown('</div>', unsafe_allow_html=True)
1155
-
1156
- elif "Mensualités constantes" in type_pret:
1157
- type_code = "MENSUEL_CONSTANT"
1158
- st.markdown("### Paramètres Mensuel (Amortissement)")
1159
- duree_mois = st.number_input("Durée (en mois)", min_value=1, max_value=60, value=12)
1160
- date_debut = st.date_input("Date de déblocage", value=date.today())
1161
-
1162
- duree_semaines = duree_mois * 4
1163
- taux_mensuel = (taux_hebdo / 100) * 4.33
1164
-
1165
- if taux_mensuel > 0:
1166
- mensualite = (montant * taux_mensuel) / (1 - (1 + taux_mensuel)**(-duree_mois))
1167
- else:
1168
- mensualite = montant / duree_mois
1169
-
1170
- montant_versement = mensualite
1171
- montant_total = mensualite * duree_mois
1172
- cout_credit = montant_total - montant
1173
- nb_versements = duree_mois
1174
-
1175
- st.markdown('<div class="loans-engine-simulation-results">', unsafe_allow_html=True)
1176
- st.markdown("### Résultats de simulation")
1177
- res1, res2, res3 = st.columns(3)
1178
- res1.metric("Mensualité", f"{round(mensualite):,} XOF".replace(",", " "))
1179
- res2.metric("Coût du Crédit", f"{round(cout_credit):,} XOF".replace(",", " "))
1180
- res3.metric("Total", f"{round(montant_total):,} XOF".replace(",", " "))
1181
- st.markdown('</div>', unsafe_allow_html=True)
1182
-
1183
- elif "Hebdomadaire" in type_pret:
1184
- type_code = "HEBDOMADAIRE"
1185
- st.markdown("### Paramètres Hebdomadaire")
1186
- duree_semaines = st.number_input("Durée (en semaines)", min_value=1, max_value=104, value=12)
1187
- date_debut = st.date_input("Date de déblocage", value=date.today())
1188
-
1189
- taux_hebdo_decimal = taux_hebdo / 100
1190
- if taux_hebdo_decimal > 0:
1191
- hebdomadalite = (montant * taux_hebdo_decimal) / (1 - (1 + taux_hebdo_decimal)**(-duree_semaines))
1192
- else:
1193
- hebdomadalite = montant / duree_semaines
1194
-
1195
- montant_versement = hebdomadalite
1196
- montant_total = hebdomadalite * duree_semaines
1197
- cout_credit = montant_total - montant
1198
- nb_versements = duree_semaines
1199
-
1200
- st.markdown('<div class="loans-engine-simulation-results">', unsafe_allow_html=True)
1201
- st.markdown("### Résultats de simulation")
1202
- res1, res2, res3 = st.columns(3)
1203
- res1.metric("Versement hebdo", f"{round(hebdomadalite):,} XOF".replace(",", " "))
1204
- res2.metric("Coût du Crédit", f"{round(cout_credit):,} XOF".replace(",", " "))
1205
- res3.metric("Total", f"{round(montant_total):,} XOF".replace(",", " "))
1206
- st.markdown('</div>', unsafe_allow_html=True)
1207
-
1208
- else: # Personnalisé
1209
- type_code = "PERSONNALISE"
1210
- st.markdown("### Paramètres Personnalisés")
1211
- date_debut = st.date_input("Date de déblocage", value=date.today())
1212
-
1213
- st.markdown('<div class="loans-engine-custom-dates">', unsafe_allow_html=True)
1214
- st.markdown("**Dates de versement :**")
1215
-
1216
- if 'dates_perso' not in st.session_state:
1217
- st.session_state.dates_perso = [date.today() + timedelta(weeks=2)]
1218
-
1219
- for idx, dt in enumerate(st.session_state.dates_perso):
1220
- col_date, col_btn = st.columns([4, 1])
1221
- with col_date:
1222
- new_date = st.date_input(f"Versement {idx + 1}", value=dt, key=f"date_{idx}", min_value=date_debut)
1223
- st.session_state.dates_perso[idx] = new_date
1224
- with col_btn:
1225
- if len(st.session_state.dates_perso) > 1:
1226
- if st.button("❌", key=f"del_{idx}"):
1227
- st.session_state.dates_perso.pop(idx)
1228
- st.rerun()
1229
-
1230
- if st.button("➕ Ajouter une date"):
1231
- last = st.session_state.dates_perso[-1] if st.session_state.dates_perso else date_debut
1232
- st.session_state.dates_perso.append(last + timedelta(weeks=1))
1233
- st.rerun()
1234
-
1235
- st.markdown('</div>', unsafe_allow_html=True)
1236
-
1237
- if st.session_state.dates_perso:
1238
- dates_versements = sorted(st.session_state.dates_perso)
1239
- date_fin = dates_versements[-1]
1240
- delta = (date_fin - date_debut).days
1241
- duree_semaines = delta // 7
1242
- montant_total = montant * (1 + (taux_hebdo / 100) * duree_semaines)
1243
- cout_credit = montant_total - montant
1244
- nb_versements = len(dates_versements)
1245
- montant_versement = montant_total / nb_versements
1246
-
1247
- st.markdown('<div class="loans-engine-simulation-results">', unsafe_allow_html=True)
1248
- st.markdown("### Résultats")
1249
- res1, res2, res3, res4 = st.columns(4)
1250
- res1.metric("Durée", f"{duree_semaines} sem")
1251
- res2.metric("Versements", nb_versements)
1252
- res3.metric("Par versement", f"{round(montant_versement):,} XOF".replace(",", " "))
1253
- res4.metric("Total", f"{round(montant_total):,} XOF".replace(",", " "))
1254
- st.markdown('</div>', unsafe_allow_html=True)
1255
-
1256
- # ANALYSE CAPACITÉ
1257
- if montant > 0 and taux_hebdo > 0 and (montant_versement > 0 or duree_semaines > 0):
1258
- try:
1259
- charges_mensuelles = float(str(client_info['Charges_Estimees']).replace(" ", "").replace("XOF", "").replace(",", ""))
1260
- except:
1261
- charges_mensuelles = 0
1262
-
1263
- revenus_mensuels = float(client_info['Revenus_Mensuels'])
1264
-
1265
- analyse = analyser_capacite_remboursement(
1266
- type_code, montant, taux_hebdo, duree_semaines,
1267
- montant_versement, nb_versements,
1268
- revenus_mensuels, charges_mensuelles,
1269
- montant_total, cout_credit
1270
- )
1271
-
1272
- if analyse:
1273
- st.markdown('<div class="loans-engine-section-divider"></div>', unsafe_allow_html=True)
1274
- st.subheader("Analyse de capacité")
1275
-
1276
- status_class = f"loans-engine-status-{analyse['couleur'].replace('dark', '')}"
1277
- st.markdown(f'<div class="{status_class}">', unsafe_allow_html=True)
1278
-
1279
- if analyse['couleur'] == "green":
1280
- st.success(f"**{analyse['statut']}** - {analyse['message']}")
1281
- elif analyse['couleur'] == "blue":
1282
- st.info(f"**{analyse['statut']}** - {analyse['message']}")
1283
- elif analyse['couleur'] == "orange":
1284
- st.warning(f"**{analyse['statut']}** - {analyse['message']}")
1285
- else:
1286
- st.error(f"**{analyse['statut']}** - {analyse['message']}")
1287
-
1288
- st.markdown('</div>', unsafe_allow_html=True)
1289
-
1290
- st.markdown('<div class="loans-engine-analysis-expander">', unsafe_allow_html=True)
1291
- with st.expander("Détails de l'analyse"):
1292
- st.markdown(analyse['details'])
1293
- if analyse['recommandations']:
1294
- st.markdown("**Recommandations :**")
1295
- for reco in analyse['recommandations']:
1296
- st.write(f"- {reco}")
1297
- st.markdown('</div>', unsafe_allow_html=True)
1298
-
1299
- # TABLEAU AMORTISSEMENT
1300
- if montant_versement > 0 or type_code == "PERSONNALISE":
1301
- st.markdown('<div class="loans-engine-section-divider"></div>', unsafe_allow_html=True)
1302
- st.subheader("Tableau d'amortissement")
1303
 
1304
- df_amortissement = generer_tableau_amortissement(
1305
- type_code, montant, taux_hebdo, duree_semaines,
1306
- montant_versement, nb_versements, date_debut, dates_versements
1307
- )
1308
 
1309
- st.markdown('<div class="loans-engine-amortization-table">', unsafe_allow_html=True)
1310
- st.dataframe(df_amortissement, use_container_width=True, hide_index=True)
1311
- st.markdown('</div>', unsafe_allow_html=True)
1312
-
1313
- # VALIDATION
1314
- st.markdown('<div class="loans-engine-section-divider"></div>', unsafe_allow_html=True)
1315
- st.markdown('<div class="loans-engine-validation-form">', unsafe_allow_html=True)
1316
-
1317
- with st.form("loan_confirmation"):
1318
- st.markdown("### Validation")
1319
- final_check = st.checkbox("Analyse de risque validée")
1320
 
1321
- st.markdown('<div class="loans-engine-action-btn">', unsafe_allow_html=True)
1322
- submit_loan = st.form_submit_button("OCTROYER LE PRÊT")
1323
- st.markdown('</div>', unsafe_allow_html=True)
1324
-
1325
- st.markdown('</div>', unsafe_allow_html=True)
1326
-
1327
- if submit_loan and final_check:
1328
- try:
1329
- ws_prets = sh.worksheet("Prets_Master")
1330
- prets_data = ws_prets.get_all_values()
1331
- loan_id = f"PRT-2026-{len(prets_data):04d}"
1332
- dates_str = ";".join([d.strftime("%d/%m/%Y") for d in dates_versements]) if dates_versements else ""
1333
 
1334
- new_loan_row = [
1335
- loan_id, client_id, client_info['Nom_Complet'], type_code,
1336
- montant, taux_hebdo, duree_semaines,
1337
- round(montant_versement) if montant_versement > 0 else 0,
1338
- round(montant_total), round(cout_credit), nb_versements,
1339
- dates_str, date_debut.strftime("%d/%m/%Y"),
1340
- date_fin.strftime("%d/%m/%Y") if date_fin else "",
1341
- moyen_transfert,
1342
- "ACTIF", datetime.now().strftime("%d-%m-%Y %H:%M:%S")
1343
- ]
1344
-
1345
- ws_prets.append_row(new_loan_row)
1346
- st.success(f"🟢 Prêt N°{loan_id} accordé à la cible !")
1347
-
1348
- loan_data = {
1349
- 'ID_Pret': loan_id, 'Type_Pret': type_code,
1350
- 'Montant_Capital': montant, 'Taux_Hebdo': taux_hebdo,
1351
- 'Duree_Semaines': duree_semaines,
1352
- 'Montant_Versement': round(montant_versement),
1353
- 'Montant_Total': round(montant_total),
1354
- 'Cout_Credit': round(cout_credit),
1355
- 'Nb_Versements': nb_versements,
1356
- 'Date_Deblocage': date_debut.strftime("%d/%m/%Y"),
1357
- 'Date_Fin': date_fin.strftime("%d/%m/%Y") if date_fin else "",
1358
- 'Moyen_Transfert': moyen_transfert
1359
- }
1360
-
1361
- pdf_buffer = generer_pdf_contrat(loan_data, client_info, df_amortissement)
1362
-
1363
- st.markdown('<div class="loans-engine-action-btn">', unsafe_allow_html=True)
1364
- st.download_button(
1365
- "Télécharger le contrat PDF",
1366
- pdf_buffer,
1367
- f"Contrat_{loan_id}_{client_id}.pdf",
1368
- "application/pdf"
1369
- )
1370
- st.markdown('</div>', unsafe_allow_html=True)
1371
-
1372
- if 'dates_perso' in st.session_state:
1373
- del st.session_state.dates_perso
1374
-
1375
- except Exception as e:
1376
- st.error(f"⚠️ Erreur : {e}")
1377
-
1378
  st.markdown('</div>', unsafe_allow_html=True)
 
1
  import streamlit as st
2
  import pandas as pd
3
  from datetime import datetime, date, timedelta
4
+ # IMPORT DES NOUVEAUX MODULES (Assure-toi que les fichiers sont dans les bons dossiers)
5
+ from Analytics.AnalyseFinance import analyser_capacite, generer_tableau_amortissement
6
+ from DocumentGen.AutoPDFGeneration import generer_contrat_pret, generer_reconnaissance_dette, generer_contrat_caution
 
 
 
 
7
 
8
  # ============================================================================
9
+ # STYLES CSS (Ta fonction existante)
10
  # ============================================================================
 
11
  def apply_loans_engine_styles():
 
 
 
 
 
12
  st.markdown("""
13
  <style>
14
  /* ========================================
 
309
  """, unsafe_allow_html=True)
310
 
311
  # ============================================================================
312
+ # MAIN FUNCTION
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  # ============================================================================
 
314
  def show_loans_engine(client, sheet_name):
 
 
315
  apply_loans_engine_styles()
316
  st.markdown('<div id="loans-engine-module">', unsafe_allow_html=True)
317
 
318
  st.header("MOTEUR FINANCIER : OCTROI DE PRÊT")
 
319
 
320
+ # 1. CHARGEMENT DONNÉES
321
  try:
322
  sh = client.open(sheet_name)
323
  ws_clients = sh.worksheet("Clients_KYC")
324
+ df_clients = pd.DataFrame(ws_clients.get_all_records())
325
+
326
+ # Chargement Garants
327
+ try:
328
+ ws_garants = sh.worksheet("Garants_KYC")
329
+ df_garants = pd.DataFrame(ws_garants.get_all_records())
330
+ df_garants['search_label'] = df_garants['ID_Garant'] + " - " + df_garants['Nom_Complet']
331
+ except:
332
+ df_garants = pd.DataFrame() # Gestion cas vide
333
+
334
  except Exception as e:
335
+ st.error(f"Erreur connexion : {e}")
 
 
 
 
 
 
336
  return
337
 
338
+ # 2. SÉLECTION CLIENT
339
  df_clients['search_label'] = df_clients['ID_Client'] + " - " + df_clients['Nom_Complet']
340
+ selected_client = st.selectbox("Rechercher un client (ID ou Nom)", [""] + df_clients['search_label'].tolist())
341
+
342
+ # 3. SÉLECTION GARANT (OPTIONNEL) - NOUVEAU
343
+ selected_garant = None
344
+ garant_id = ""
345
+ if not df_garants.empty:
346
+ selected_garant_label = st.selectbox("Rechercher un garant (Optionnel)", [""] + df_garants['search_label'].tolist())
347
+ if selected_garant_label:
348
+ selected_garant = df_garants[df_garants['search_label'] == selected_garant_label].iloc[0]
349
+ garant_id = selected_garant['ID_Garant']
350
+ st.info(f"Garant sélectionné : {selected_garant['Nom_Complet']}")
351
+
352
+ if selected_client:
353
+ client_info = df_clients[df_clients['search_label'] == selected_client].iloc[0]
354
  client_id = client_info['ID_Client']
355
 
356
+ # 4. CONFIGURATION PRÊT
357
+ st.markdown("---")
358
+ st.subheader("Configuration")
359
+
360
+ col_motif, col_type = st.columns(2)
361
+ with col_motif:
362
+ # NOUVEAU : MOTIF
363
+ motif = st.selectbox("Motif du prêt", ["Commerce / Achat stock", "Urgence Médicale", "Scolarité", "Investissement", "Consommation", "Autre"])
364
+ with col_type:
365
+ type_pret = st.selectbox("Type de remboursement", ["In Fine", "Mensuel - Intérêts", "Mensuel - Constant", "Hebdomadaire", "Personnalisé"])
366
+
367
+ # Mapping Type
368
+ type_code_map = {"In Fine": "IN_FINE", "Mensuel - Intérêts": "MENSUEL_INTERETS", "Mensuel - Constant": "MENSUEL_CONSTANT", "Hebdomadaire": "HEBDOMADAIRE", "Personnalisé": "PERSONNALISE"}
369
+ type_code = type_code_map[type_pret]
370
+
371
+ col1, col2, col3 = st.columns(3)
372
+ montant = col1.number_input("Montant (XOF)", 10000, value=100000, step=10000)
373
+ taux_hebdo = col2.number_input("Taux Hebdo (%)", 0.1, value=2.0, step=0.1)
374
+ duree_val = col3.number_input("Durée (Semaines/Mois)", 1, value=12) # Simplifié pour l'exemple
375
 
376
+ # Logique de conversion durée selon type (à adapter finement comme dans ton code original)
377
+ duree_semaines = duree_val if type_code in ["IN_FINE", "HEBDOMADAIRE"] else duree_val * 4.33
 
 
 
 
378
 
379
+ # CALCULS PRELIMINAIRES (Simulation)
380
+ # Note: Pour simplifier l'exemple, je mets une logique basique, tu gardes ta logique complexe d'UI ici si tu veux
381
+ # L'important est de passer les BONS paramètres à l'Analytics
382
+ nb_versements = int(duree_val) if type_code != "IN_FINE" else 1
383
+ montant_versement = 0 # À calculer selon ta logique UI existante
384
+ if type_code == "IN_FINE": montant_versement = montant * (1 + taux_hebdo/100 * duree_val)
385
 
386
+ montant_total = montant_versement * nb_versements # Approx pour l'exemple
387
+ cout_credit = montant_total - montant
388
+
389
+ # 5. APPEL CERVEAU ANALYTIQUE (AUTO-TRIGGER)
390
+ # On passe les données brutes, le module fait le reste
391
+ analyse = analyser_capacite(
392
+ type_code, montant, taux_hebdo, duree_semaines, montant_versement, nb_versements,
393
+ client_info['Revenus_Mensuels'], client_info.get('Charges_Estimees', 0), montant_total
 
394
  )
395
+
396
+ # AFFICHAGE ANALYSE
397
+ st.markdown(f"### Analyse : <span style='color:{analyse['couleur']}'>{analyse['statut']}</span>", unsafe_allow_html=True)
398
+ st.info(analyse['message'])
399
+ with st.expander("Détails financiers"):
400
+ st.markdown(analyse['details'])
401
 
402
+ # 6. TABLEAU AMORTISSEMENT
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  date_debut = date.today()
404
+ df_amort = generer_tableau_amortissement(type_code, montant, taux_hebdo, duree_semaines, montant_versement, nb_versements, date_debut)
405
+ st.dataframe(df_amort, hide_index=True)
406
 
407
+ # 7. VALIDATION & DOCUMENTS
408
+ with st.form("valid_pret"):
409
+ submit = st.form_submit_button("OCTROYER & GÉNÉRER DOCS")
410
+
411
+ if submit:
412
+ # 7a. SAUVEGARDE GOOGLE SHEETS
413
+ ws_prets = sh.worksheet("Prets_Master")
414
+ new_id = f"PRT-{len(ws_prets.get_all_values()) + 1:04d}"
415
+
416
+ # ORDRE STRICT DEMANDÉ
417
+ row_data = [
418
+ new_id, client_id, client_info['Nom_Complet'], type_code, motif,
419
+ montant, taux_hebdo, duree_semaines, montant_versement, montant_total,
420
+ cout_credit, nb_versements, "", # Dates_Versements (str)
421
+ date_debut.strftime("%d/%m/%Y"), "", # Date_Fin
422
+ "ESPECE", "ACTIF", garant_id, datetime.now().strftime("%d-%m-%Y")
423
+ ]
424
+ ws_prets.append_row(row_data)
425
+ st.success(f"Prêt {new_id} enregistré !")
426
+
427
+ # 7b. GÉNÉRATION PDF (USINE A DOCS)
428
+ loan_data_dict = {
429
+ "ID_Pret": new_id, "Montant_Capital": montant, "Montant_Total": montant_total,
430
+ "Taux_Hebdo": taux_hebdo, "Duree_Semaines": duree_semaines, "Motif": motif,
431
+ "Date_Deblocage": date_debut.strftime("%d/%m/%Y")
432
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
 
434
+ # PDF 1 : CONTRAT
435
+ pdf_contrat = generer_contrat_pret(loan_data_dict, client_info, df_amort)
436
+ st.download_button("📄 Contrat de Prêt", pdf_contrat, f"Contrat_{new_id}.pdf", "application/pdf")
 
437
 
438
+ # PDF 2 : RECONNAISSANCE
439
+ pdf_dette = generer_reconnaissance_dette(loan_data_dict, client_info)
440
+ st.download_button("📝 Reconnaissance de Dette", pdf_dette, f"Dette_{new_id}.pdf", "application/pdf")
 
 
 
 
 
 
 
 
441
 
442
+ # PDF 3 : CAUTION (SI GARANT)
443
+ if selected_garant is not None:
444
+ pdf_caution = generer_contrat_caution(loan_data_dict, selected_garant)
445
+ st.download_button("🤝 Contrat de Caution", pdf_caution, f"Caution_{new_id}.pdf", "application/pdf")
 
 
 
 
 
 
 
 
446
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
  st.markdown('</div>', unsafe_allow_html=True)