klydekushy commited on
Commit
c7c046e
·
verified ·
1 Parent(s): c82dc00

Update src/modules/repayments.py

Browse files
Files changed (1) hide show
  1. src/modules/repayments.py +178 -178
src/modules/repayments.py CHANGED
@@ -449,25 +449,25 @@ def show_repayments_module(client, sheet_name):
449
  # Affichage du contexte
450
  st.divider()
451
  with st.expander("Détails du contrat", expanded=True):
452
- c1, c2, c3 = st.columns(3)
453
- with c1:
454
- st.metric("Capital Prêté", f"{loan_data['Montant_Capital']:,.0f} XOF")
455
- with c2:
456
- st.metric("Montant Total Dû", f"{loan_data['Montant_Total']:,.0f} XOF")
457
- with c3:
458
- st.metric("Échéance Prévue", f"{loan_data['Montant_Versement']:,.0f} XOF")
459
-
460
- st.caption(f"**Client :** {loan_data['Nom_Complet']}")
461
- st.caption(f"**Date de début :** {loan_data.get('Date_Deblocage', 'N/A')}")
462
- st.caption(f"**Statut actuel :** {loan_data['Statut']}")
463
 
464
  # --- 3. ANALYSE DE L'ÉCHÉANCE ---
465
  st.divider()
466
  st.subheader("3. Analyse de l'Échéance en Cours")
467
 
468
- # Date du paiement
469
- date_paiement = st.date_input("Date du paiement", value=date.today())
470
-
471
  # Analyse automatique
472
  echeance_info = analyser.detecter_echeance_attendue(loan_id, date_paiement)
473
 
@@ -479,7 +479,7 @@ def show_repayments_module(client, sheet_name):
479
  st.metric("Date Prévue", echeance_info['date_prevue'].strftime("%d/%m/%Y"))
480
  with col_c:
481
  st.metric("Montant Échéance", f"{echeance_info['montant_ajuste']:,.0f} XOF")
482
-
483
  # Calcul du retard
484
  penalites_base = analyser.calculer_penalites(
485
  echeance_info['montant_ajuste'],
@@ -494,7 +494,7 @@ def show_repayments_module(client, sheet_name):
494
  st.divider()
495
  st.warning(f"⚠️ RETARD DÉTECTÉ : {jours_retard} jours")
496
  st.subheader("4. Choix du Scénario de Pénalités")
497
-
498
  # Génération des 3 scénarios
499
  scenarios = analyser.generer_scenarios_penalites(
500
  echeance_info['montant_ajuste'],
@@ -621,180 +621,180 @@ def show_repayments_module(client, sheet_name):
621
  st.error("❌ Le montant versé doit être supérieur à 0 XOF")
622
  else:
623
  try:
624
- # Récupération du taux de pénalité sélectionné
625
- taux_penalite = st.session_state.get('selected_scenario', {}).get('taux', 0.0) / 100
626
-
627
- # Analyse complète
628
- analyse_complete = analyser.analyser_remboursement_complet(
629
- loan_id,
630
- date_paiement,
631
- montant_verse,
632
- taux_penalite=taux_penalite
633
- )
634
-
635
- if 'error' in analyse_complete:
636
- st.error(f"❌ {analyse_complete['error']}")
637
- else:
638
- # Génération ID Transaction
639
- existing_remb = ws_remb.get_all_values()
640
- next_id = len(existing_remb)
641
- trans_id = f"TRX-2026-{next_id:04d}"
642
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
643
 
644
- # Génération Numéro Reçu
645
- annee_actuelle = datetime.now().year
646
- count_annee = len([r for r in existing_remb if f"REC-{annee_actuelle}" in str(r)])
647
- numero_recu = f"REC-{annee_actuelle}-{count_annee + 1:04d}"
648
-
649
- # 1. Écriture dans Remboursements
650
- new_row = [
651
- trans_id,
652
  loan_id,
653
- analyse_complete['id_client'],
654
- str(date_paiement),
655
  montant_verse,
656
- analyse_complete['montant_principal'],
657
- analyse_complete['montant_interets'],
658
- analyse_complete['penalites_retard'],
659
- analyse_complete['solde_avant'],
660
- analyse_complete['solde_apres'],
661
- analyse_complete['numero_echeance'],
662
- str(analyse_complete['date_echeance_prevue']),
663
- analyse_complete['jours_retard'],
664
- analyse_complete['statut_paiement'],
665
- moyen,
666
- reference_externe if reference_externe else "N/A",
667
- commentaire if commentaire else "N/A",
668
- "NON", # Recu_Emis (par défaut NON, sera mis à OUI si généré)
669
- numero_recu,
670
- timestamp
671
- ]
672
- ws_remb.append_row(new_row)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
673
 
674
- # 2. Gestion des ajustements si PARTIEL
675
- if analyse_complete['statut_paiement'] == "PARTIEL" and analyse_complete['montant_a_reporter'] > 0:
676
- if analyse_complete['prochaine_echeance']:
677
- # Génération ID Ajustement
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
678
  try:
679
- existing_ajust = ws_ajust.get_all_values()
680
- next_ajust_id = len(existing_ajust)
 
681
  except:
682
- # Si la table n'existe pas, la créer
683
- ws_ajust = sh.add_worksheet(title="Ajustements_Echeances", rows="1000", cols="7")
684
- ws_ajust.append_row(["ID_Ajustement", "ID_Pret", "Numero_Echeance", "Montant_Additionnel", "Raison", "Date_Creation", "Timestamp"])
685
- next_ajust_id = 1
686
 
687
- ajust_id = f"ADJ-{annee_actuelle}-{next_ajust_id:04d}"
 
 
 
 
 
688
 
689
- ajust_row = [
690
- ajust_id,
691
- loan_id,
692
- analyse_complete['prochaine_echeance'],
693
- analyse_complete['montant_a_reporter'],
694
- "PAIEMENT_PARTIEL",
695
- str(date.today()),
696
- timestamp
697
- ]
698
- ws_ajust.append_row(ajust_row)
699
 
700
- st.warning(f"⚠️ Paiement PARTIEL détecté. {analyse_complete['montant_a_reporter']:,.0f} XOF reportés sur l'échéance #{analyse_complete['prochaine_echeance']}")
701
-
702
- # 3. Vérification automatique de la clôture
703
- if analyse_complete['doit_cloturer']:
704
- # Mise à jour automatique du statut
705
- cell = ws_prets.find(loan_id)
706
- header = ws_prets.row_values(1)
707
- col_statut_idx = header.index("Statut") + 1
708
- ws_prets.update_cell(cell.row, col_statut_idx, "TERMINE")
709
-
710
- # Mise à jour de la date de clôture si la colonne existe
711
- try:
712
- if "Date_Cloture" in header:
713
- col_cloture_idx = header.index("Date_Cloture") + 1
714
- ws_prets.update_cell(cell.row, col_cloture_idx, str(date.today()))
715
- except:
716
- pass # La colonne n'existe pas encore
717
-
718
- # Message de clôture avec le critère
719
- critere_messages = {
720
- 'SOLDE_ZERO': 'Solde intégralement remboursé',
721
- 'TOUTES_ECHEANCES': 'Toutes les échéances payées',
722
- 'MONTANT_TOTAL': 'Montant total du prêt remboursé'
723
- }
724
-
725
- st.markdown(f"""
726
- <div class="repayment-success-badge">
727
- <h3>DOSSIER {loan_id} AUTOMATIQUEMENT CLÔTURÉ</h3>
728
- <p style="color: #8b949e; margin: 8px 0 0 0;">
729
- Motif : {critere_messages.get(analyse_complete['critere_cloture'], 'Prêt terminé')}<br>
730
- Paiement de {montant_verse:,.0f} XOF enregistré | Ref: {trans_id}
731
- </p>
732
- </div>
733
- """, unsafe_allow_html=True)
734
-
735
- st.info(f"Détails : {analyse_complete['message_cloture']}")
736
- else:
737
- st.success(f"Paiement de **{montant_verse:,.0f} XOF** enregistré avec succès")
738
- st.info(f"Référence de transaction : **{trans_id}**")
739
-
740
- # Afficher l'état du prêt
741
- if analyse_complete['solde_apres'] > 0:
742
- st.info(f"Solde restant à rembourser : **{analyse_complete['solde_apres']:,.0f} XOF**")
743
-
744
- # Afficher un récapitulatif détaillé
745
- with st.expander("Récapitulatif de la transaction", expanded=True):
746
- recap_col1, recap_col2, recap_col3 = st.columns(3)
747
- with recap_col1:
748
- st.write(f"**ID Transaction :** {trans_id}")
749
- st.write(f"**ID Prêt :** {loan_id}")
750
- st.write(f"**Date :** {date_paiement}")
751
- st.write(f"**Échéance :** {analyse_complete['numero_echeance']}")
752
- with recap_col2:
753
- st.write(f"**Montant Versé :** {montant_verse:,.0f} XOF")
754
- st.write(f"**Principal :** {analyse_complete['montant_principal']:,.0f} XOF")
755
- st.write(f"**Intérêts :** {analyse_complete['montant_interets']:,.0f} XOF")
756
- st.write(f"**Pénalités :** {analyse_complete['penalites_retard']:,.0f} XOF")
757
- with recap_col3:
758
- st.write(f"**Solde Avant :** {analyse_complete['solde_avant']:,.0f} XOF")
759
- st.write(f"**Solde Après :** {analyse_complete['solde_apres']:,.0f} XOF")
760
- st.write(f"**Moyen :** {moyen}")
761
- st.write(f"**Statut :** {analyse_complete['statut_paiement']}")
762
-
763
- # Bouton pour générer le reçu
764
- st.divider()
765
- if st.button("Générer le Reçu", use_container_width=True):
766
- # Préparer les données pour le reçu
767
- client_data = df_clients[df_clients['ID_Client'] == analyse_complete['id_client']].iloc[0].to_dict()
768
-
769
- recu_data = {
770
- 'numero_recu': numero_recu,
771
- 'trans_id': trans_id,
772
- 'date_paiement': date_paiement,
773
- 'client': client_data,
774
- 'loan': loan_data.to_dict(),
775
- 'paiement': analyse_complete,
776
- 'moyen': moyen,
777
- 'reference': reference_externe
778
- }
779
 
780
- pdf_bytes = generer_recu(recu_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
781
 
782
- if pdf_bytes:
783
- # Mise à jour de Recu_Emis dans Remboursements
784
- cell_trans = ws_remb.find(trans_id)
785
- header_remb = ws_remb.row_values(1)
786
- col_recu_idx = header_remb.index("Recu_Emis") + 1
787
- ws_remb.update_cell(cell_trans.row, col_recu_idx, "OUI")
788
 
789
- st.download_button(
790
- label="Télécharger le Reçu PDF",
791
- data=pdf_bytes,
792
- file_name=f"{numero_recu}_{loan_id}.pdf",
793
- mime="application/pdf",
794
- use_container_width=True
795
- )
796
- st.success("Reçu généré avec succès")
797
- else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
798
  st.error("❌ Erreur lors de la génération du reçu")
799
 
800
  except Exception as e:
 
449
  # Affichage du contexte
450
  st.divider()
451
  with st.expander("Détails du contrat", expanded=True):
452
+ c1, c2, c3 = st.columns(3)
453
+ with c1:
454
+ st.metric("Capital Prêté", f"{loan_data['Montant_Capital']:,.0f} XOF")
455
+ with c2:
456
+ st.metric("Montant Total Dû", f"{loan_data['Montant_Total']:,.0f} XOF")
457
+ with c3:
458
+ st.metric("Échéance Prévue", f"{loan_data['Montant_Versement']:,.0f} XOF")
459
+
460
+ st.caption(f"**Client :** {loan_data['Nom_Complet']}")
461
+ st.caption(f"**Date de début :** {loan_data.get('Date_Deblocage', 'N/A')}")
462
+ st.caption(f"**Statut actuel :** {loan_data['Statut']}")
463
 
464
  # --- 3. ANALYSE DE L'ÉCHÉANCE ---
465
  st.divider()
466
  st.subheader("3. Analyse de l'Échéance en Cours")
467
 
468
+ # Date du paiement
469
+ date_paiement = st.date_input("Date du paiement", value=date.today())
470
+
471
  # Analyse automatique
472
  echeance_info = analyser.detecter_echeance_attendue(loan_id, date_paiement)
473
 
 
479
  st.metric("Date Prévue", echeance_info['date_prevue'].strftime("%d/%m/%Y"))
480
  with col_c:
481
  st.metric("Montant Échéance", f"{echeance_info['montant_ajuste']:,.0f} XOF")
482
+
483
  # Calcul du retard
484
  penalites_base = analyser.calculer_penalites(
485
  echeance_info['montant_ajuste'],
 
494
  st.divider()
495
  st.warning(f"⚠️ RETARD DÉTECTÉ : {jours_retard} jours")
496
  st.subheader("4. Choix du Scénario de Pénalités")
497
+
498
  # Génération des 3 scénarios
499
  scenarios = analyser.generer_scenarios_penalites(
500
  echeance_info['montant_ajuste'],
 
621
  st.error("❌ Le montant versé doit être supérieur à 0 XOF")
622
  else:
623
  try:
624
+ # Récupération du taux de pénalité sélectionné
625
+ taux_penalite = st.session_state.get('selected_scenario', {}).get('taux', 0.0) / 100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
626
 
627
+ # Analyse complète
628
+ analyse_complete = analyser.analyser_remboursement_complet(
 
 
 
 
 
 
629
  loan_id,
630
+ date_paiement,
 
631
  montant_verse,
632
+ taux_penalite=taux_penalite
633
+ )
634
+
635
+ if 'error' in analyse_complete:
636
+ st.error(f"❌ {analyse_complete['error']}")
637
+ else:
638
+ # Génération ID Transaction
639
+ existing_remb = ws_remb.get_all_values()
640
+ next_id = len(existing_remb)
641
+ trans_id = f"TRX-2026-{next_id:04d}"
642
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
643
+
644
+ # Génération Numéro Reçu
645
+ annee_actuelle = datetime.now().year
646
+ count_annee = len([r for r in existing_remb if f"REC-{annee_actuelle}" in str(r)])
647
+ numero_recu = f"REC-{annee_actuelle}-{count_annee + 1:04d}"
648
+
649
+ # 1. Écriture dans Remboursements
650
+ new_row = [
651
+ trans_id,
652
+ loan_id,
653
+ analyse_complete['id_client'],
654
+ str(date_paiement),
655
+ montant_verse,
656
+ analyse_complete['montant_principal'],
657
+ analyse_complete['montant_interets'],
658
+ analyse_complete['penalites_retard'],
659
+ analyse_complete['solde_avant'],
660
+ analyse_complete['solde_apres'],
661
+ analyse_complete['numero_echeance'],
662
+ str(analyse_complete['date_echeance_prevue']),
663
+ analyse_complete['jours_retard'],
664
+ analyse_complete['statut_paiement'],
665
+ moyen,
666
+ reference_externe if reference_externe else "N/A",
667
+ commentaire if commentaire else "N/A",
668
+ "NON", # Recu_Emis (par défaut NON, sera mis à OUI si généré)
669
+ numero_recu,
670
+ timestamp
671
+ ]
672
+ ws_remb.append_row(new_row)
673
 
674
+ # 2. Gestion des ajustements si PARTIEL
675
+ if analyse_complete['statut_paiement'] == "PARTIEL" and analyse_complete['montant_a_reporter'] > 0:
676
+ if analyse_complete['prochaine_echeance']:
677
+ # Génération ID Ajustement
678
+ try:
679
+ existing_ajust = ws_ajust.get_all_values()
680
+ next_ajust_id = len(existing_ajust)
681
+ except:
682
+ # Si la table n'existe pas, la créer
683
+ ws_ajust = sh.add_worksheet(title="Ajustements_Echeances", rows="1000", cols="7")
684
+ ws_ajust.append_row(["ID_Ajustement", "ID_Pret", "Numero_Echeance", "Montant_Additionnel", "Raison", "Date_Creation", "Timestamp"])
685
+ next_ajust_id = 1
686
+
687
+ ajust_id = f"ADJ-{annee_actuelle}-{next_ajust_id:04d}"
688
+
689
+ ajust_row = [
690
+ ajust_id,
691
+ loan_id,
692
+ analyse_complete['prochaine_echeance'],
693
+ analyse_complete['montant_a_reporter'],
694
+ "PAIEMENT_PARTIEL",
695
+ str(date.today()),
696
+ timestamp
697
+ ]
698
+ ws_ajust.append_row(ajust_row)
699
+
700
+ st.warning(f"⚠️ Paiement PARTIEL détecté. {analyse_complete['montant_a_reporter']:,.0f} XOF reportés sur l'échéance #{analyse_complete['prochaine_echeance']}")
701
+
702
+ # 3. Vérification automatique de la clôture
703
+ if analyse_complete['doit_cloturer']:
704
+ # Mise à jour automatique du statut
705
+ cell = ws_prets.find(loan_id)
706
+ header = ws_prets.row_values(1)
707
+ col_statut_idx = header.index("Statut") + 1
708
+ ws_prets.update_cell(cell.row, col_statut_idx, "TERMINE")
709
+
710
+ # Mise à jour de la date de clôture si la colonne existe
711
  try:
712
+ if "Date_Cloture" in header:
713
+ col_cloture_idx = header.index("Date_Cloture") + 1
714
+ ws_prets.update_cell(cell.row, col_cloture_idx, str(date.today()))
715
  except:
716
+ pass # La colonne n'existe pas encore
 
 
 
717
 
718
+ # Message de clôture avec le critère
719
+ critere_messages = {
720
+ 'SOLDE_ZERO': 'Solde intégralement remboursé',
721
+ 'TOUTES_ECHEANCES': 'Toutes les échéances payées',
722
+ 'MONTANT_TOTAL': 'Montant total du prêt remboursé'
723
+ }
724
 
725
+ st.markdown(f"""
726
+ <div class="repayment-success-badge">
727
+ <h3>DOSSIER {loan_id} AUTOMATIQUEMENT CLÔTURÉ</h3>
728
+ <p style="color: #8b949e; margin: 8px 0 0 0;">
729
+ Motif : {critere_messages.get(analyse_complete['critere_cloture'], 'Prêt terminé')}<br>
730
+ Paiement de {montant_verse:,.0f} XOF enregistré | Ref: {trans_id}
731
+ </p>
732
+ </div>
733
+ """, unsafe_allow_html=True)
 
734
 
735
+ st.info(f"Détails : {analyse_complete['message_cloture']}")
736
+ else:
737
+ st.success(f"Paiement de **{montant_verse:,.0f} XOF** enregistré avec succès")
738
+ st.info(f"Référence de transaction : **{trans_id}**")
739
+
740
+ # Afficher l'état du prêt
741
+ if analyse_complete['solde_apres'] > 0:
742
+ st.info(f"Solde restant à rembourser : **{analyse_complete['solde_apres']:,.0f} XOF**")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
743
 
744
+ # Afficher un récapitulatif détaillé
745
+ with st.expander("Récapitulatif de la transaction", expanded=True):
746
+ recap_col1, recap_col2, recap_col3 = st.columns(3)
747
+ with recap_col1:
748
+ st.write(f"**ID Transaction :** {trans_id}")
749
+ st.write(f"**ID Prêt :** {loan_id}")
750
+ st.write(f"**Date :** {date_paiement}")
751
+ st.write(f"**Échéance :** {analyse_complete['numero_echeance']}")
752
+ with recap_col2:
753
+ st.write(f"**Montant Versé :** {montant_verse:,.0f} XOF")
754
+ st.write(f"**Principal :** {analyse_complete['montant_principal']:,.0f} XOF")
755
+ st.write(f"**Intérêts :** {analyse_complete['montant_interets']:,.0f} XOF")
756
+ st.write(f"**Pénalités :** {analyse_complete['penalites_retard']:,.0f} XOF")
757
+ with recap_col3:
758
+ st.write(f"**Solde Avant :** {analyse_complete['solde_avant']:,.0f} XOF")
759
+ st.write(f"**Solde Après :** {analyse_complete['solde_apres']:,.0f} XOF")
760
+ st.write(f"**Moyen :** {moyen}")
761
+ st.write(f"**Statut :** {analyse_complete['statut_paiement']}")
762
 
763
+ # Bouton pour générer le reçu
764
+ st.divider()
765
+ if st.button("Générer le Reçu", use_container_width=True):
766
+ # Préparer les données pour le reçu
767
+ client_data = df_clients[df_clients['ID_Client'] == analyse_complete['id_client']].iloc[0].to_dict()
 
768
 
769
+ recu_data = {
770
+ 'numero_recu': numero_recu,
771
+ 'trans_id': trans_id,
772
+ 'date_paiement': date_paiement,
773
+ 'client': client_data,
774
+ 'loan': loan_data.to_dict(),
775
+ 'paiement': analyse_complete,
776
+ 'moyen': moyen,
777
+ 'reference': reference_externe
778
+ }
779
+
780
+ pdf_bytes = generer_recu(recu_data)
781
+
782
+ if pdf_bytes:
783
+ # Mise à jour de Recu_Emis dans Remboursements
784
+ cell_trans = ws_remb.find(trans_id)
785
+ header_remb = ws_remb.row_values(1)
786
+ col_recu_idx = header_remb.index("Recu_Emis") + 1
787
+ ws_remb.update_cell(cell_trans.row, col_recu_idx, "OUI")
788
+
789
+ st.download_button(
790
+ label="Télécharger le Reçu PDF",
791
+ data=pdf_bytes,
792
+ file_name=f"{numero_recu}_{loan_id}.pdf",
793
+ mime="application/pdf",
794
+ use_container_width=True
795
+ )
796
+ st.success("Reçu généré avec succès")
797
+ else:
798
  st.error("❌ Erreur lors de la génération du reçu")
799
 
800
  except Exception as e: