Spaces:
Running
Running
Update src/modules/loans_engine.py
Browse files- 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 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 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 |
-
|
| 498 |
-
|
| 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 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 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 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 599 |
|
| 600 |
-
#
|
| 601 |
-
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 609 |
dates_versements = []
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 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 |
-
#
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
|
|
|
|
| 627 |
montant_total = montant * (1 + (taux_hebdo / 100) * duree_semaines)
|
| 628 |
cout_credit = montant_total - montant
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 632 |
# Affichage résultat simulation
|
| 633 |
st.markdown("### Simulation")
|
| 634 |
res1, res2, res3 = st.columns(3)
|
| 635 |
-
res1.metric("
|
| 636 |
-
res2.metric("
|
| 637 |
res3.metric("Coût total", f"{int(cout_credit):,} XOF".replace(",", " "))
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 707 |
|
| 708 |
-
#
|
| 709 |
-
|
| 710 |
-
|
|
|
|
|
|
|
|
|
|
| 711 |
|
| 712 |
-
#
|
| 713 |
-
|
| 714 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 715 |
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 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)
|