klydekushy commited on
Commit
84d518b
·
verified ·
1 Parent(s): fc338ef

Update src/modules/loans_engine.py

Browse files
Files changed (1) hide show
  1. src/modules/loans_engine.py +301 -302
src/modules/loans_engine.py CHANGED
@@ -371,7 +371,7 @@ def show_loans_engine(client, sheet_name):
371
  df_clients['search_label'] = df_clients['ID_Client'].astype(str) + " - " + df_clients['Nom_Complet'].astype(str)
372
  selected_client_label = st.selectbox("Rechercher une cible ", [""] + df_clients['search_label'].tolist())
373
 
374
- if selected_client_label:
375
  # Récupération de la ligne complète
376
  client_info = df_clients[df_clients['search_label'] == selected_client_label].iloc[0]
377
  client_id = client_info['ID_Client']
@@ -399,323 +399,322 @@ def show_loans_engine(client, sheet_name):
399
 
400
  st.markdown(f"**Profession :** {client_info['Statut_Pro']} | **Ville :** {client_info['Ville']}")
401
 
402
- # ============================================================================
403
- # 3. SÉLECTION GARANT (Optionnel)
404
- # ============================================================================
405
- selected_garant = None
406
- garant_id = ""
407
-
408
- if df_garants.empty:
409
- st.info("Aucun garant n'est actuellement enregistré dans la base (Optionnel).")
410
- else:
411
- selected_garant_label = st.selectbox("Rechercher un garant (Optionnel)", [""] + df_garants['search_label'].tolist())
412
-
413
- if selected_garant_label:
414
- # Récupération de la ligne complète
415
- garant_info = df_garants[df_garants['search_label'] == selected_garant_label].iloc[0]
416
- selected_garant = garant_info # Pour l'utiliser dans la génération PDF
417
- garant_id = garant_info['ID_Garant']
418
- st.info(f"🔸Garant Détectée : **{garant_info['Nom_Complet']}**")
419
- with st.expander(f"Analyse de la Caution : {garant_info['Nom_Complet']}", expanded=True):
420
- rev_g = clean_val(garant_info['Revenus_Mensuels'])
421
- chg_g = clean_val(garant_info['Charges_Estimees'])
422
-
423
- g1, g2, g3 = st.columns(3)
424
- g1.metric("Revenus Garant", f"{int(rev_g):,} XOF".replace(",", " "))
425
- g2.metric("Charges Garant", f"{int(chg_g):,} XOF".replace(",", " "))
426
- g3.metric("Reste à vivre", f"{int(rev_g - chg_g):,} XOF".replace(",", " "))
427
-
428
- st.warning("⚠️ **Engagement solidaire** : Le garant renonce aux bénéfices de discussion et de division. Il s'engage à payer en cas de défaillance de l'emprunteur.")
429
-
430
-
431
- # 4. CONFIGURATION PRÊT
432
- st.markdown("---")
433
- st.subheader("Configuration")
434
-
435
- col_motif, col_type = st.columns(2)
436
- with col_motif:
437
- # NOUVEAU : MOTIF
438
- motif = st.selectbox(
439
- "Motif du prêt",
440
- [
441
- "Commerce / Achat de stock",
442
- "Investissement",
443
- "Trésorerie professionnelle",
444
- "Lancement d’activité",
445
- "Développement d’activité",
446
- "Agriculture / Élevage",
447
- "Transport / Logistique",
448
- "Urgence médicale",
449
- "Scolarité / Formation",
450
- "Logement / Habitat",
451
- "Réparations",
452
- "Événements familiaux",
453
- "Voyage / Déplacement",
454
- "Consommation",
455
- "Achat d’équipement personnel",
456
- "Projet personnel",
457
- "Autre"
458
- ]
459
- )
460
- with col_type:
461
- type_pret = st.selectbox("Type de remboursement", ["In Fine", "Mensuel - Intérêts", "Mensuel - Constant", "Hebdomadaire", "Personnalisé"])
462
-
463
- # Mapping Type
464
- type_code_map = {"In Fine": "IN_FINE", "Mensuel - Intérêts": "MENSUEL_INTERETS", "Mensuel - Constant": "MENSUEL_CONSTANT", "Hebdomadaire": "HEBDOMADAIRE", "Personnalisé": "PERSONNALISE"}
465
- type_code = type_code_map[type_pret]
466
-
467
- col1, col2, col3 = st.columns(3)
468
- montant = col1.number_input("Montant (XOF)", 10000, value=100000, step=10000)
469
- taux_hebdo = col2.number_input("Taux Hebdo (%)", 0.1, value=2.0, step=0.1)
470
- duree_val = col3.number_input("Durée (Semaines/Mois)", 1, value=12) # Simplifié pour l'exemple
471
-
472
- # Logique de conversion durée selon type (à adapter finement comme dans ton code original)
473
- duree_semaines = duree_val if type_code in ["IN_FINE", "HEBDOMADAIRE"] else duree_val * 4.33
474
-
475
- # ====================================================================
476
- # DÉBUT DU BLOC LOGIQUE À COPIER-COLLER
477
- # ====================================================================
478
-
479
- # Initialisation des variables pour éviter les erreurs
480
- montant_versement = 0
481
- montant_total = 0
482
- cout_credit = 0
483
- nb_versements = 0
484
- duree_semaines = 0
485
- dates_versements = []
486
- # Initialisation
487
- date_debut = date.today()
488
- date_fin = date_debut # Par défaut
489
-
490
- # -----------------------------------------------------------
491
- # 1. LOGIQUE IN FINE (1 seul versement à la fin)
492
- # -----------------------------------------------------------
493
- if type_code == "IN_FINE":
494
- duree_semaines = col3.number_input("Durée (en semaines)", min_value=1, max_value=104, value=8)
495
- date_fin = date_debut + timedelta(weeks=duree_semaines)
496
 
497
- # Calculs
498
- montant_total = montant * (1 + (taux_hebdo / 100) * duree_semaines)
499
- cout_credit = montant_total - montant
500
- montant_versement = montant_total
501
- nb_versements = 1
502
-
503
- # Affichage résultat simulation immédiate
504
- st.markdown("### Simulation")
505
- res1, res2 = st.columns(2)
506
- res1.metric("Versement unique", f"{int(montant_versement):,} XOF".replace(",", " "))
507
- res2.metric("Coût du crédit", f"{int(cout_credit):,} XOF".replace(",", " "))
508
-
509
- # -----------------------------------------------------------
510
- # 2. LOGIQUE MENSUEL - INTÉRÊTS (Remboursement capital à la fin)
511
- # -----------------------------------------------------------
512
- elif type_code == "MENSUEL_INTERETS":
513
- duree_mois = col3.number_input("Durée (en mois)", min_value=1, max_value=60, value=12)
514
- date_fin = date_debut + timedelta(days=duree_mois * 30) # Approximation standard mensuelle
515
-
516
- # Conversion et Calculs
517
- duree_semaines = duree_mois * 4.33 # Standard bancaire
518
- taux_mensuel = (taux_hebdo / 100) * 4.33
519
- interet_mensuel = montant * taux_mensuel
520
-
521
- montant_versement = interet_mensuel # Ce que le client paie chaque mois
522
- montant_final_mois = montant + interet_mensuel # Dernier mois
523
- montant_total = (interet_mensuel * duree_mois) + montant
524
- cout_credit = montant_total - montant
525
- nb_versements = int(duree_mois)
526
-
527
- # Affichage résultat simulation
528
- st.markdown("### Simulation")
529
- res1, res2, res3 = st.columns(3)
530
- res1.metric("Intérêts mensuels", f"{int(interet_mensuel):,} XOF".replace(",", " "))
531
- res2.metric("Dernier versement", f"{int(montant_final_mois):,} XOF".replace(",", " "))
532
- res3.metric("Coût total", f"{int(cout_credit):,} XOF".replace(",", " "))
533
-
534
- # -----------------------------------------------------------
535
- # 3. LOGIQUE MENSUEL - CONSTANT (Amortissement classique)
536
- # -----------------------------------------------------------
537
- elif type_code == "MENSUEL_CONSTANT":
538
- duree_mois = col3.number_input("Durée (en mois)", min_value=1, max_value=60, value=12)
539
- date_fin = date_debut + timedelta(days=duree_mois * 30)
540
-
541
- # Conversion et Calculs
542
- duree_semaines = duree_mois * 4.33
543
- taux_mensuel = (taux_hebdo / 100) * 4.33
544
-
545
- if taux_mensuel > 0:
546
- # Formule mathématique des mensualités constantes
547
- mensualite = (montant * taux_mensuel) / (1 - (1 + taux_mensuel)**(-duree_mois))
548
- else:
549
- mensualite = montant / duree_mois
550
-
551
- montant_versement = mensualite
552
- montant_total = mensualite * duree_mois
553
- cout_credit = montant_total - montant
554
- nb_versements = int(duree_mois)
555
-
556
- # Affichage résultat simulation
557
- st.markdown("### Simulation")
558
- res1, res2 = st.columns(2)
559
- res1.metric("Mensualité constante", f"{int(mensualite):,} XOF".replace(",", " "))
560
- res2.metric("Coût total", f"{int(cout_credit):,} XOF".replace(",", " "))
561
-
562
- # -----------------------------------------------------------
563
- # 4. LOGIQUE HEBDOMADAIRE
564
- # -----------------------------------------------------------
565
- elif type_code == "HEBDOMADAIRE":
566
- duree_semaines = col3.number_input("Durée (en semaines)", min_value=1, max_value=104, value=12)
567
- date_fin = date_debut + timedelta(weeks=duree_semaines)
568
-
569
- # Calculs
570
- taux_hebdo_decimal = taux_hebdo / 100
571
- if taux_hebdo_decimal > 0:
572
- hebdomadalite = (montant * taux_hebdo_decimal) / (1 - (1 + taux_hebdo_decimal)**(-duree_semaines))
573
  else:
574
- hebdomadalite = montant / duree_semaines
575
-
576
- montant_versement = hebdomadalite
577
- montant_total = hebdomadalite * duree_semaines
578
- cout_credit = montant_total - montant
579
- nb_versements = int(duree_semaines)
580
-
581
- # Affichage résultat simulation
582
- st.markdown("### Simulation")
583
- res1, res2 = st.columns(2)
584
- res1.metric("Versement Hebdo", f"{int(hebdomadalite):,} XOF".replace(",", " "))
585
- res2.metric("Coût total", f"{int(cout_credit):,} XOF".replace(",", " "))
 
 
 
 
 
 
586
 
587
- # -----------------------------------------------------------
588
- # 5. LOGIQUE PERSONNALISÉE (Dates manuelles)
589
- # -----------------------------------------------------------
590
- else: # PERSONNALISE
591
- # On n'a pas besoin de l'input durée ici car elle dépend des dates
592
- st.info("Configurez les dates de versement ci-dessous")
593
 
594
- # Gestion des dates dans la session state pour persistance
595
- if 'dates_perso' not in st.session_state:
596
- dates_sorted = sorted(st.session_state.dates_perso)
597
- date_fin = dates_sorted[-1] # La dernière date devient la date_fin
598
- st.session_state.dates_perso = [date.today() + timedelta(weeks=2)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
599
 
600
- # Interface d'ajout de dates
601
- st.markdown("**Dates de versement :**")
602
- col_add, col_reset = st.columns([1, 4])
603
- if col_add.button("➕ Ajouter"):
604
- last_date = st.session_state.dates_perso[-1]
605
- st.session_state.dates_perso.append(last_date + timedelta(weeks=1))
606
- st.rerun()
607
 
608
- # Affichage des date pickers
 
 
 
 
 
 
 
 
 
609
  dates_versements = []
610
- for idx, dt in enumerate(st.session_state.dates_perso):
611
- col_d, col_x = st.columns([4, 1])
612
- new_date = col_d.date_input(f"Echéance {idx+1}", value=dt, key=f"d_{idx}", min_value=date.today())
613
- dates_versements.append(new_date)
614
- if col_x.button("❌", key=f"del_{idx}") and len(st.session_state.dates_perso) > 1:
615
- st.session_state.dates_perso.pop(idx)
616
- st.rerun()
617
-
618
- st.session_state.dates_perso = dates_versements # Mise à jour state
619
 
620
- # Calculs basés sur les dates
621
- if dates_versements:
622
- dates_versements.sort()
623
- date_fin_calc = dates_versements[-1]
624
- delta_days = (date_fin - date_debut).days
625
- duree_semaines = max(1, delta_days // 7)
626
 
 
627
  montant_total = montant * (1 + (taux_hebdo / 100) * duree_semaines)
628
  cout_credit = montant_total - montant
629
- nb_versements = len(dates_sorted)
630
- montant_versement = montant_total / nb_versements
631
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632
  # Affichage résultat simulation
633
  st.markdown("### Simulation")
634
  res1, res2, res3 = st.columns(3)
635
- res1.metric("Moyenne/Versement", f"{int(montant_versement):,} XOF".replace(",", " "))
636
- res2.metric("Durée est.", f"{duree_semaines} sem")
637
  res3.metric("Coût total", f"{int(cout_credit):,} XOF".replace(",", " "))
638
-
639
- # ====================================================================
640
- # FIN DU BLOC LOGIQUE
641
- # ====================================================================
642
-
643
- # 5. APPEL CERVEAU ANALYTIQUE (AUTO-TRIGGER)
644
- # On passe les données brutes, le module fait le reste
645
- analyse = analyser_capacite(
646
- type_code, montant, taux_hebdo, duree_semaines, montant_versement, nb_versements,
647
- client_info['Revenus_Mensuels'], client_info.get('Charges_Estimees', 0), montant_total
648
- )
649
-
650
- # AFFICHAGE ANALYSE
651
- st.markdown(f"### Analyse : <span style='color:{analyse['couleur']}'>{analyse['statut']}</span>", unsafe_allow_html=True)
652
- st.info(analyse['message'])
653
- with st.expander("Détails financiers"):
654
- st.markdown(analyse['details'])
655
-
656
- # 6. TABLEAU AMORTISSEMENT
657
- date_debut = date.today()
658
- df_amort = generer_tableau_amortissement(type_code, montant, taux_hebdo, duree_semaines, montant_versement, nb_versements, date_debut)
659
- st.dataframe(df_amort, hide_index=True)
660
-
661
- # 7. VALIDATION & DOCUMENTS
662
- with st.form("valid_pret"):
663
- submit = st.form_submit_button("OCTROYER & GÉNÉRER DOCS")
664
-
665
- if submit:
666
- # 7a. SAUVEGARDE GOOGLE SHEETS
667
- ws_prets = sh.worksheet("Prets_Master")
668
- new_id = f"PRT-{len(ws_prets.get_all_values()) + 1:04d}"
669
-
670
- # ORDRE STRICT DEMANDÉ POUR Prets_Master
671
- row_data = [
672
- new_id, # ID_Pret
673
- client_id, # ID_Client
674
- client_info['Nom_Complet'], # Nom_Complet
675
- type_code, # Type_Pret
676
- motif, # Motif
677
- montant, # Montant_Capital
678
- taux_hebdo, # Taux_Hebdo
679
- duree_semaines, # Duree_Semaines
680
- round(montant_versement), # Montant_Versement
681
- round(montant_total), # Montant_Total
682
- round(cout_credit), # Cout_Credit
683
- nb_versements, # Nb_Versements
684
- ";".join([d.strftime("%d/%m/%Y") for d in dates_versements]) if dates_versements else "", # Dates_Versements
685
- date_debut.strftime("%d/%m/%Y"), # Date_Deblocage
686
- date_fin.strftime("%d/%m/%Y"), # Date_Fin (Maintenant garantie)
687
- moyen_transfert, # Moyen_Transfert
688
- "ACTIF", # Statut
689
- garant_id, # ID_Garant
690
- datetime.now().strftime("%d-%m-%Y %H:%M:%S") # Date_Creation
691
- ]
692
- ws_prets.append_row(row_data)
693
- time.sleep(1)
694
- st.success(f"Prêt {new_id} enregistré !")
695
-
696
- # 7b. GÉNÉRATION PDF (USINE A DOCS)
697
- loan_data_dict = {
698
- "ID_Pret": new_id,
699
- "Montant_Capital": montant,
700
- "Montant_Total": montant_total,
701
- "Taux_Hebdo": taux_hebdo,
702
- "Duree_Semaines": duree_semaines,
703
- "Motif": motif,
704
- "Date_Deblocage": date_debut.strftime("%d/%m/%Y"),
705
- "Date_Fin": date_fin.strftime("%d/%m/%Y") # Ajout ici
706
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
707
 
708
- # PDF 1 : CONTRAT
709
- pdf_contrat = generer_contrat_pret(loan_data_dict, client_info, df_amort)
710
- st.download_button("Contrat de Prêt", pdf_contrat, f"Contrat_{new_id}.pdf", "application/pdf")
 
 
 
711
 
712
- # PDF 2 : RECONNAISSANCE
713
- pdf_dette = generer_reconnaissance_dette(loan_data_dict, client_info)
714
- st.download_button("Reconnaissance de Dette", pdf_dette, f"Dette_{new_id}.pdf", "application/pdf")
 
 
 
 
 
 
 
 
 
 
 
715
 
716
- # PDF 3 : CAUTION (SI GARANT)
717
- if selected_garant is not None:
718
- pdf_caution = generer_contrat_caution(loan_data_dict, selected_garant)
719
- st.download_button("Contrat de Caution", pdf_caution, f"Caution_{new_id}.pdf", "application/pdf")
720
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
721
  st.markdown('</div>', unsafe_allow_html=True)
 
371
  df_clients['search_label'] = df_clients['ID_Client'].astype(str) + " - " + df_clients['Nom_Complet'].astype(str)
372
  selected_client_label = st.selectbox("Rechercher une cible ", [""] + df_clients['search_label'].tolist())
373
 
374
+ if selected_client_label: # <--- TOUT DOIT ÊTRE DANS CE BLOC
375
  # Récupération de la ligne complète
376
  client_info = df_clients[df_clients['search_label'] == selected_client_label].iloc[0]
377
  client_id = client_info['ID_Client']
 
399
 
400
  st.markdown(f"**Profession :** {client_info['Statut_Pro']} | **Ville :** {client_info['Ville']}")
401
 
402
+ # ============================================================================
403
+ # 3. SÉLECTION GARANT (Optionnel)
404
+ # ============================================================================
405
+ selected_garant = None
406
+ garant_id = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
 
408
+ if df_garants.empty:
409
+ st.info("Aucun garant n'est actuellement enregistré dans la base (Optionnel).")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
  else:
411
+ selected_garant_label = st.selectbox("Rechercher un garant (Optionnel)", [""] + df_garants['search_label'].tolist())
412
+
413
+ if selected_garant_label:
414
+ # Récupération de la ligne complète
415
+ garant_info = df_garants[df_garants['search_label'] == selected_garant_label].iloc[0]
416
+ selected_garant = garant_info # Pour l'utiliser dans la génération PDF
417
+ garant_id = garant_info['ID_Garant']
418
+ st.info(f"🔸Garant Détectée : **{garant_info['Nom_Complet']}**")
419
+ with st.expander(f"Analyse de la Caution : {garant_info['Nom_Complet']}", expanded=True):
420
+ rev_g = clean_val(garant_info['Revenus_Mensuels'])
421
+ chg_g = clean_val(garant_info['Charges_Estimees'])
422
+
423
+ g1, g2, g3 = st.columns(3)
424
+ g1.metric("Revenus Garant", f"{int(rev_g):,} XOF".replace(",", " "))
425
+ g2.metric("Charges Garant", f"{int(chg_g):,} XOF".replace(",", " "))
426
+ g3.metric("Reste à vivre", f"{int(rev_g - chg_g):,} XOF".replace(",", " "))
427
+
428
+ st.warning("⚠️ **Engagement solidaire** : Le garant renonce aux bénéfices de discussion et de division. Il s'engage à payer en cas de défaillance de l'emprunteur.")
429
 
430
+
431
+ # 4. CONFIGURATION PRÊT
432
+ st.markdown("---")
433
+ st.subheader("Configuration")
 
 
434
 
435
+ col_motif, col_type = st.columns(2)
436
+ with col_motif:
437
+ # NOUVEAU : MOTIF
438
+ motif = st.selectbox(
439
+ "Motif du prêt",
440
+ [
441
+ "Commerce / Achat de stock",
442
+ "Investissement",
443
+ "Trésorerie professionnelle",
444
+ "Lancement d’activité",
445
+ "Développement d’activité",
446
+ "Agriculture / Élevage",
447
+ "Transport / Logistique",
448
+ "Urgence médicale",
449
+ "Scolarité / Formation",
450
+ "Logement / Habitat",
451
+ "Réparations",
452
+ "Événements familiaux",
453
+ "Voyage / Déplacement",
454
+ "Consommation",
455
+ "Achat d’équipement personnel",
456
+ "Projet personnel",
457
+ "Autre"
458
+ ]
459
+ )
460
+ with col_type:
461
+ type_pret = st.selectbox("Type de remboursement", ["In Fine", "Mensuel - Intérêts", "Mensuel - Constant", "Hebdomadaire", "Personnalisé"])
462
+
463
+ # Mapping Type
464
+ type_code_map = {"In Fine": "IN_FINE", "Mensuel - Intérêts": "MENSUEL_INTERETS", "Mensuel - Constant": "MENSUEL_CONSTANT", "Hebdomadaire": "HEBDOMADAIRE", "Personnalisé": "PERSONNALISE"}
465
+ type_code = type_code_map[type_pret]
466
+
467
+ col1, col2, col3 = st.columns(3)
468
+ montant = col1.number_input("Montant (XOF)", 10000, value=100000, step=10000)
469
+ taux_hebdo = col2.number_input("Taux Hebdo (%)", 0.1, value=2.0, step=0.1)
470
+ duree_val = col3.number_input("Durée (Semaines/Mois)", 1, value=12) # Simplifié pour l'exemple
471
 
472
+ # Logique de conversion durée selon type (à adapter finement comme dans ton code original)
473
+ duree_semaines = duree_val if type_code in ["IN_FINE", "HEBDOMADAIRE"] else duree_val * 4.33
 
 
 
 
 
474
 
475
+ # ====================================================================
476
+ # DÉBUT DU BLOC LOGIQUE À COPIER-COLLER
477
+ # ====================================================================
478
+
479
+ # Initialisation des variables pour éviter les erreurs
480
+ montant_versement = 0
481
+ montant_total = 0
482
+ cout_credit = 0
483
+ nb_versements = 0
484
+ duree_semaines = 0
485
  dates_versements = []
486
+ # Initialisation
487
+ date_debut = date.today()
488
+ date_fin = date_debut # Par défaut
 
 
 
 
 
 
489
 
490
+ # -----------------------------------------------------------
491
+ # 1. LOGIQUE IN FINE (1 seul versement à la fin)
492
+ # -----------------------------------------------------------
493
+ if type_code == "IN_FINE":
494
+ duree_semaines = col3.number_input("Durée (en semaines)", min_value=1, max_value=104, value=8)
495
+ date_fin = date_debut + timedelta(weeks=duree_semaines)
496
 
497
+ # Calculs
498
  montant_total = montant * (1 + (taux_hebdo / 100) * duree_semaines)
499
  cout_credit = montant_total - montant
500
+ montant_versement = montant_total
501
+ nb_versements = 1
502
+
503
+ # Affichage résultat simulation immédiate
504
+ st.markdown("### Simulation")
505
+ res1, res2 = st.columns(2)
506
+ res1.metric("Versement unique", f"{int(montant_versement):,} XOF".replace(",", " "))
507
+ res2.metric("Coût du crédit", f"{int(cout_credit):,} XOF".replace(",", " "))
508
+
509
+ # -----------------------------------------------------------
510
+ # 2. LOGIQUE MENSUEL - INTÉRÊTS (Remboursement capital à la fin)
511
+ # -----------------------------------------------------------
512
+ elif type_code == "MENSUEL_INTERETS":
513
+ duree_mois = col3.number_input("Durée (en mois)", min_value=1, max_value=60, value=12)
514
+ date_fin = date_debut + timedelta(days=duree_mois * 30) # Approximation standard mensuelle
515
+
516
+ # Conversion et Calculs
517
+ duree_semaines = duree_mois * 4.33 # Standard bancaire
518
+ taux_mensuel = (taux_hebdo / 100) * 4.33
519
+ interet_mensuel = montant * taux_mensuel
520
+
521
+ montant_versement = interet_mensuel # Ce que le client paie chaque mois
522
+ montant_final_mois = montant + interet_mensuel # Dernier mois
523
+ montant_total = (interet_mensuel * duree_mois) + montant
524
+ cout_credit = montant_total - montant
525
+ nb_versements = int(duree_mois)
526
+
527
  # Affichage résultat simulation
528
  st.markdown("### Simulation")
529
  res1, res2, res3 = st.columns(3)
530
+ res1.metric("Intérêts mensuels", f"{int(interet_mensuel):,} XOF".replace(",", " "))
531
+ res2.metric("Dernier versement", f"{int(montant_final_mois):,} XOF".replace(",", " "))
532
  res3.metric("Coût total", f"{int(cout_credit):,} XOF".replace(",", " "))
533
+
534
+ # -----------------------------------------------------------
535
+ # 3. LOGIQUE MENSUEL - CONSTANT (Amortissement classique)
536
+ # -----------------------------------------------------------
537
+ elif type_code == "MENSUEL_CONSTANT":
538
+ duree_mois = col3.number_input("Durée (en mois)", min_value=1, max_value=60, value=12)
539
+ date_fin = date_debut + timedelta(days=duree_mois * 30)
540
+
541
+ # Conversion et Calculs
542
+ duree_semaines = duree_mois * 4.33
543
+ taux_mensuel = (taux_hebdo / 100) * 4.33
544
+
545
+ if taux_mensuel > 0:
546
+ # Formule mathématique des mensualités constantes
547
+ mensualite = (montant * taux_mensuel) / (1 - (1 + taux_mensuel)**(-duree_mois))
548
+ else:
549
+ mensualite = montant / duree_mois
550
+
551
+ montant_versement = mensualite
552
+ montant_total = mensualite * duree_mois
553
+ cout_credit = montant_total - montant
554
+ nb_versements = int(duree_mois)
555
+
556
+ # Affichage résultat simulation
557
+ st.markdown("### Simulation")
558
+ res1, res2 = st.columns(2)
559
+ res1.metric("Mensualité constante", f"{int(mensualite):,} XOF".replace(",", " "))
560
+ res2.metric("Coût total", f"{int(cout_credit):,} XOF".replace(",", " "))
561
+
562
+ # -----------------------------------------------------------
563
+ # 4. LOGIQUE HEBDOMADAIRE
564
+ # -----------------------------------------------------------
565
+ elif type_code == "HEBDOMADAIRE":
566
+ duree_semaines = col3.number_input("Durée (en semaines)", min_value=1, max_value=104, value=12)
567
+ date_fin = date_debut + timedelta(weeks=duree_semaines)
568
+
569
+ # Calculs
570
+ taux_hebdo_decimal = taux_hebdo / 100
571
+ if taux_hebdo_decimal > 0:
572
+ hebdomadalite = (montant * taux_hebdo_decimal) / (1 - (1 + taux_hebdo_decimal)**(-duree_semaines))
573
+ else:
574
+ hebdomadalite = montant / duree_semaines
575
+
576
+ montant_versement = hebdomadalite
577
+ montant_total = hebdomadalite * duree_semaines
578
+ cout_credit = montant_total - montant
579
+ nb_versements = int(duree_semaines)
580
+
581
+ # Affichage résultat simulation
582
+ st.markdown("### Simulation")
583
+ res1, res2 = st.columns(2)
584
+ res1.metric("Versement Hebdo", f"{int(hebdomadalite):,} XOF".replace(",", " "))
585
+ res2.metric("Coût total", f"{int(cout_credit):,} XOF".replace(",", " "))
586
+
587
+ # -----------------------------------------------------------
588
+ # 5. LOGIQUE PERSONNALISÉE (Dates manuelles)
589
+ # -----------------------------------------------------------
590
+ else: # PERSONNALISE
591
+ # On n'a pas besoin de l'input durée ici car elle dépend des dates
592
+ st.info("Configurez les dates de versement ci-dessous")
593
+
594
+ # Gestion des dates dans la session state pour persistance
595
+ if 'dates_perso' not in st.session_state:
596
+ dates_sorted = sorted(st.session_state.dates_perso)
597
+ date_fin = dates_sorted[-1] # La dernière date devient la date_fin
598
+ st.session_state.dates_perso = [date.today() + timedelta(weeks=2)]
599
+
600
+ # Interface d'ajout de dates
601
+ st.markdown("**Dates de versement :**")
602
+ col_add, col_reset = st.columns([1, 4])
603
+ if col_add.button("➕ Ajouter"):
604
+ last_date = st.session_state.dates_perso[-1]
605
+ st.session_state.dates_perso.append(last_date + timedelta(weeks=1))
606
+ st.rerun()
607
+
608
+ # Affichage des date pickers
609
+ dates_versements = []
610
+ for idx, dt in enumerate(st.session_state.dates_perso):
611
+ col_d, col_x = st.columns([4, 1])
612
+ new_date = col_d.date_input(f"Echéance {idx+1}", value=dt, key=f"d_{idx}", min_value=date.today())
613
+ dates_versements.append(new_date)
614
+ if col_x.button("❌", key=f"del_{idx}") and len(st.session_state.dates_perso) > 1:
615
+ st.session_state.dates_perso.pop(idx)
616
+ st.rerun()
617
+
618
+ st.session_state.dates_perso = dates_versements # Mise à jour state
619
+
620
+ # Calculs basés sur les dates
621
+ if dates_versements:
622
+ dates_versements.sort()
623
+ date_fin_calc = dates_versements[-1]
624
+ delta_days = (date_fin - date_debut).days
625
+ duree_semaines = max(1, delta_days // 7)
626
+
627
+ montant_total = montant * (1 + (taux_hebdo / 100) * duree_semaines)
628
+ cout_credit = montant_total - montant
629
+ nb_versements = len(dates_sorted)
630
+ montant_versement = montant_total / nb_versements
631
+
632
+ # Affichage résultat simulation
633
+ st.markdown("### Simulation")
634
+ res1, res2, res3 = st.columns(3)
635
+ res1.metric("Moyenne/Versement", f"{int(montant_versement):,} XOF".replace(",", " "))
636
+ res2.metric("Durée est.", f"{duree_semaines} sem")
637
+ res3.metric("Coût total", f"{int(cout_credit):,} XOF".replace(",", " "))
638
+
639
+ # ====================================================================
640
+ # FIN DU BLOC LOGIQUE
641
+ # ====================================================================
642
 
643
+ # 5. APPEL CERVEAU ANALYTIQUE (AUTO-TRIGGER)
644
+ # On passe les données brutes, le module fait le reste
645
+ analyse = analyser_capacite(
646
+ type_code, montant, taux_hebdo, duree_semaines, montant_versement, nb_versements,
647
+ client_info['Revenus_Mensuels'], client_info.get('Charges_Estimees', 0), montant_total
648
+ )
649
 
650
+ # AFFICHAGE ANALYSE
651
+ st.markdown(f"### Analyse : <span style='color:{analyse['couleur']}'>{analyse['statut']}</span>", unsafe_allow_html=True)
652
+ st.info(analyse['message'])
653
+ with st.expander("Détails financiers"):
654
+ st.markdown(analyse['details'])
655
+
656
+ # 6. TABLEAU AMORTISSEMENT
657
+ date_debut = date.today()
658
+ df_amort = generer_tableau_amortissement(type_code, montant, taux_hebdo, duree_semaines, montant_versement, nb_versements, date_debut)
659
+ st.dataframe(df_amort, hide_index=True)
660
+
661
+ # 7. VALIDATION & DOCUMENTS
662
+ with st.form("valid_pret"):
663
+ submit = st.form_submit_button("OCTROYER & GÉNÉRER DOCS")
664
 
665
+ if submit:
666
+ # 7a. SAUVEGARDE GOOGLE SHEETS
667
+ ws_prets = sh.worksheet("Prets_Master")
668
+ new_id = f"PRT-{len(ws_prets.get_all_values()) + 1:04d}"
669
+
670
+ # ORDRE STRICT DEMANDÉ POUR Prets_Master
671
+ row_data = [
672
+ new_id, # ID_Pret
673
+ client_id, # ID_Client
674
+ client_info['Nom_Complet'], # Nom_Complet
675
+ type_code, # Type_Pret
676
+ motif, # Motif
677
+ montant, # Montant_Capital
678
+ taux_hebdo, # Taux_Hebdo
679
+ duree_semaines, # Duree_Semaines
680
+ round(montant_versement), # Montant_Versement
681
+ round(montant_total), # Montant_Total
682
+ round(cout_credit), # Cout_Credit
683
+ nb_versements, # Nb_Versements
684
+ ";".join([d.strftime("%d/%m/%Y") for d in dates_versements]) if dates_versements else "", # Dates_Versements
685
+ date_debut.strftime("%d/%m/%Y"), # Date_Deblocage
686
+ date_fin.strftime("%d/%m/%Y"), # Date_Fin (Maintenant garantie)
687
+ moyen_transfert, # Moyen_Transfert
688
+ "ACTIF", # Statut
689
+ garant_id, # ID_Garant
690
+ datetime.now().strftime("%d-%m-%Y %H:%M:%S") # Date_Creation
691
+ ]
692
+ ws_prets.append_row(row_data)
693
+ time.sleep(1)
694
+ st.success(f"Prêt {new_id} enregistré !")
695
+
696
+ # 7b. GÉNÉRATION PDF (USINE A DOCS)
697
+ loan_data_dict = {
698
+ "ID_Pret": new_id,
699
+ "Montant_Capital": montant,
700
+ "Montant_Total": montant_total,
701
+ "Taux_Hebdo": taux_hebdo,
702
+ "Duree_Semaines": duree_semaines,
703
+ "Motif": motif,
704
+ "Date_Deblocage": date_debut.strftime("%d/%m/%Y"),
705
+ "Date_Fin": date_fin.strftime("%d/%m/%Y") # Ajout ici
706
+ }
707
+
708
+ # PDF 1 : CONTRAT
709
+ pdf_contrat = generer_contrat_pret(loan_data_dict, client_info, df_amort)
710
+ st.download_button("Contrat de Prêt", pdf_contrat, f"Contrat_{new_id}.pdf", "application/pdf")
711
+
712
+ # PDF 2 : RECONNAISSANCE
713
+ pdf_dette = generer_reconnaissance_dette(loan_data_dict, client_info)
714
+ st.download_button("Reconnaissance de Dette", pdf_dette, f"Dette_{new_id}.pdf", "application/pdf")
715
+
716
+ # PDF 3 : CAUTION (SI GARANT)
717
+ if selected_garant is not None:
718
+ pdf_caution = generer_contrat_caution(loan_data_dict, selected_garant)
719
+ st.download_button("Contrat de Caution", pdf_caution, f"Caution_{new_id}.pdf", "application/pdf")
720
  st.markdown('</div>', unsafe_allow_html=True)