klydekushy commited on
Commit
2805446
·
verified ·
1 Parent(s): b83537c

Update src/modules/repayments.py

Browse files
Files changed (1) hide show
  1. src/modules/repayments.py +79 -118
src/modules/repayments.py CHANGED
@@ -598,10 +598,10 @@ def show_repayments_module(client, sheet_name):
598
  # --- 5. SAISIE DU PAIEMENT ---
599
  st.divider()
600
  st.subheader("5. Enregistrement du Paiement")
601
-
602
  with st.form("repayment_form", clear_on_submit=True):
603
  col_a, col_b = st.columns(2)
604
-
605
  with col_a:
606
  montant_verse = st.number_input(
607
  "Montant Versé (XOF)",
@@ -610,28 +610,28 @@ def show_repayments_module(client, sheet_name):
610
  value=int(st.session_state.get('selected_scenario', {}).get('total_a_encaisser', echeance_info['montant_ajuste'])),
611
  help="Montant exact reçu"
612
  )
613
-
614
  with col_b:
615
  moyen = st.selectbox(
616
  "Moyen de Paiement",
617
  ["Espèces", "Mobile Money (Wave)", "Mobile Money (Orange Money)", "Virement Bancaire", "Chèque"],
618
  help="Canal de réception des fonds"
619
  )
620
-
621
  reference_externe = st.text_input(
622
  "Référence Transaction",
623
  placeholder="Ex: WAVE-TXN-123456, Numéro de chèque...",
624
  help="Référence externe pour traçabilité"
625
  )
626
-
627
  commentaire = st.text_area(
628
  "Commentaire",
629
  placeholder="Notes additionnelles sur la transaction...",
630
  help="Informations complémentaires"
631
  )
632
-
633
  submit = st.form_submit_button("✅ VALIDER L'ENCAISSEMENT", use_container_width=True)
634
-
635
  if submit:
636
  if montant_verse <= 0:
637
  st.error("❌ Le montant versé doit être supérieur à 0 XOF")
@@ -639,7 +639,7 @@ def show_repayments_module(client, sheet_name):
639
  try:
640
  # Récupération du taux de pénalité sélectionné
641
  taux_penalite = st.session_state.get('selected_scenario', {}).get('taux', 0.0) / 100
642
-
643
  # Analyse complète
644
  analyse_complete = analyser.analyser_remboursement_complet(
645
  loan_id,
@@ -647,7 +647,7 @@ def show_repayments_module(client, sheet_name):
647
  montant_verse,
648
  taux_penalite=taux_penalite
649
  )
650
-
651
  if 'error' in analyse_complete:
652
  st.error(f"❌ {analyse_complete['error']}")
653
  else:
@@ -656,19 +656,19 @@ def show_repayments_module(client, sheet_name):
656
  next_id = len(existing_remb)
657
  trans_id = f"TRX-2026-{next_id:04d}"
658
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
659
-
660
  # Génération Numéro Reçu
661
  annee_actuelle = datetime.now().year
662
  count_annee = len([r for r in existing_remb if f"REC-{annee_actuelle}" in str(r)])
663
  numero_recu = f"REC-{annee_actuelle}-{count_annee + 1:04d}"
664
-
665
  # 1. Écriture dans Remboursements (avec conversions explicites)
666
  new_row = [
667
  str(trans_id),
668
  str(loan_id),
669
  str(analyse_complete['id_client']),
670
  str(date_paiement),
671
- int(montant_verse), # Conversion explicite
672
  int(analyse_complete['montant_principal']),
673
  int(analyse_complete['montant_interets']),
674
  int(analyse_complete['penalites_retard']),
@@ -686,7 +686,7 @@ def show_repayments_module(client, sheet_name):
686
  str(timestamp)
687
  ]
688
  ws_remb.append_row(new_row)
689
-
690
  # 2. Gestion des ajustements si PARTIEL
691
  if analyse_complete['statut_paiement'] == "PARTIEL" and analyse_complete['montant_a_reporter'] > 0:
692
  if analyse_complete['prochaine_echeance']:
@@ -697,9 +697,9 @@ def show_repayments_module(client, sheet_name):
697
  ws_ajust = sh.add_worksheet(title="Ajustements_Echeances", rows="1000", cols="7")
698
  ws_ajust.append_row(["ID_Ajustement", "ID_Pret", "Numero_Echeance", "Montant_Additionnel", "Raison", "Date_Creation", "Timestamp"])
699
  next_ajust_id = 1
700
-
701
  ajust_id = f"ADJ-{annee_actuelle}-{next_ajust_id:04d}"
702
-
703
  ajust_row = [
704
  str(ajust_id),
705
  str(loan_id),
@@ -710,52 +710,46 @@ def show_repayments_module(client, sheet_name):
710
  str(timestamp)
711
  ]
712
  ws_ajust.append_row(ajust_row)
713
-
714
  st.warning(f"⚠️ Paiement PARTIEL détecté. {analyse_complete['montant_a_reporter']:,.0f} XOF reportés sur l'échéance #{analyse_complete['prochaine_echeance']}")
715
-
716
- # === 3. CLÔTURE AUTOMATIQUE (GESTION UPDATED) ===
717
  if analyse_complete['doit_cloturer']:
718
- # Déterminer si le prêt est UPDATED
719
  if is_updated and ws_prets_update is not None:
720
- # Mise à jour dans Prets_Update
721
  try:
722
  cell = ws_prets_update.find(loan_id)
723
  header_update = ws_prets_update.row_values(1)
724
  col_statut_idx = header_update.index("Statut") + 1
725
  ws_prets_update.update_cell(cell.row, col_statut_idx, "TERMINE")
726
-
727
- # Mise à jour de Date_Fin si la colonne existe
728
  if "Date_Fin" in header_update:
729
  col_fin_idx = header_update.index("Date_Fin") + 1
730
  ws_prets_update.update_cell(cell.row, col_fin_idx, str(date.today()))
731
-
732
  st.info(f"📝 Statut mis à jour dans Prets_Update (Version {version_info})")
733
  except Exception as e:
734
  st.warning(f"⚠️ Impossible de mettre à jour Prets_Update : {e}")
735
  else:
736
- # Mise à jour dans Prets_Master
737
  try:
738
  cell = ws_prets.find(loan_id)
739
  header = ws_prets.row_values(1)
740
  col_statut_idx = header.index("Statut") + 1
741
  ws_prets.update_cell(cell.row, col_statut_idx, "TERMINE")
742
-
743
- # Mise à jour de Date_Cloture si existe
744
  if "Date_Cloture" in header:
745
  col_cloture_idx = header.index("Date_Cloture") + 1
746
  ws_prets.update_cell(cell.row, col_cloture_idx, str(date.today()))
747
-
748
  st.info(f"📝 Statut mis à jour dans Prets_Master")
749
  except Exception as e:
750
  st.warning(f"⚠️ Impossible de mettre à jour Prets_Master : {e}")
751
-
752
- # Message de clôture
753
  critere_messages = {
754
  'SOLDE_ZERO': 'Solde intégralement remboursé',
755
  'TOUTES_ECHEANCES': 'Toutes les échéances payées',
756
  'MONTANT_TOTAL': 'Montant total du prêt remboursé'
757
  }
758
-
759
  st.markdown(f"""
760
  <div class="repayment-success-badge">
761
  <h3>✅ DOSSIER {loan_id} AUTOMATIQUEMENT CLÔTURÉ</h3>
@@ -765,27 +759,25 @@ def show_repayments_module(client, sheet_name):
765
  </p>
766
  </div>
767
  """, unsafe_allow_html=True)
768
-
769
  st.info(f"Détails : {analyse_complete['message_cloture']}")
770
  else:
771
  st.success(f"✅ Paiement de **{montant_verse:,.0f} XOF** enregistré avec succès")
772
  st.info(f"Référence de transaction : **{trans_id}**")
773
-
774
  if analyse_complete['solde_apres'] > 0:
775
  st.info(f"Solde restant à rembourser : **{analyse_complete['solde_apres']:,.0f} XOF**")
776
-
777
  # Préparer les données pour le reçu
778
  client_data_dict = df_clients[df_clients['ID_Client'] == analyse_complete['id_client']].iloc[0].to_dict()
779
 
780
- # Convertir loan_data en dict si c'est une Series
781
  if hasattr(loan_data, 'to_dict'):
782
  loan_data_dict = loan_data.to_dict()
783
  else:
784
  loan_data_dict = dict(loan_data)
785
 
786
- # === CONVERSION DES TYPES PANDAS EN TYPES PYTHON NATIFS ===
787
  def convert_to_native(obj):
788
- """Convertit récursivement les types pandas/numpy en types Python natifs"""
789
  import numpy as np
790
 
791
  if isinstance(obj, dict):
@@ -803,7 +795,6 @@ def show_repayments_module(client, sheet_name):
803
  else:
804
  return obj
805
 
806
- # Nettoyer toutes les données
807
  client_data_dict = convert_to_native(client_data_dict)
808
  loan_data_dict = convert_to_native(loan_data_dict)
809
  analyse_complete_clean = convert_to_native(analyse_complete)
@@ -826,7 +817,7 @@ def show_repayments_module(client, sheet_name):
826
  'numero_recu': numero_recu,
827
  'loan_id': loan_id
828
  }
829
-
830
  # Récapitulatif détaillé
831
  with st.expander("📋 Récapitulatif de la transaction", expanded=True):
832
  recap_col1, recap_col2, recap_col3 = st.columns(3)
@@ -845,93 +836,63 @@ def show_repayments_module(client, sheet_name):
845
  st.write(f"**Solde Après :** {analyse_complete['solde_apres']:,.0f} XOF")
846
  st.write(f"**Moyen :** {moyen}")
847
  st.write(f"**Statut :** {analyse_complete['statut_paiement']}")
848
-
849
  except Exception as e:
850
  st.error(f"❌ Erreur lors de l'enregistrement : {e}")
851
  st.exception(e)
852
- else:
853
- st.error("❌ Impossible d'analyser l'échéance pour ce prêt")
854
- # [Code de diagnostic existant...]
855
-
856
- # === GÉNÉRATION DU REÇU (Section persistante) ===
857
- st.divider()
858
- st.subheader("📄 Génération du Reçu")
859
-
860
- # Debug 1 : Vérifier session_state
861
- st.write("🔍 DEBUG - Clés dans session_state :", list(st.session_state.keys()))
862
-
863
- if 'derniere_transaction' in st.session_state:
864
- trans_data = st.session_state['derniere_transaction']
865
-
866
- st.success(f"✅ Transaction **{trans_data['trans_id']}** trouvée dans session_state")
867
- st.caption(f"Reçu N° : **{trans_data['numero_recu']}**")
868
-
869
- # Debug 2 : Afficher les données
870
- with st.expander("🔍 Données pour le reçu (Debug)", expanded=False):
871
- st.json({
872
- 'trans_id': trans_data.get('trans_id'),
873
- 'numero_recu': trans_data.get('numero_recu'),
874
- 'loan_id': trans_data.get('loan_id'),
875
- 'recu_data_keys': list(trans_data.get('recu_data', {}).keys())
876
- })
877
 
878
- col_btn1, col_btn2 = st.columns(2)
879
-
880
- with col_btn1:
881
- btn_clicked = st.button("📥 Générer et Télécharger le Reçu PDF", type="primary")
882
 
883
- if btn_clicked:
884
- st.write("🔄 BOUTON CLIQUÉ !")
885
-
886
- try:
887
- st.write("📋 Appel de generer_recu()...")
888
-
889
- # Import
890
- from DocumentGen.InvoiceRepayment import generer_recu
891
-
892
- st.write(f"✅ Module importé")
893
-
894
- # Appel
895
- pdf_bytes = generer_recu(trans_data['recu_data'])
896
-
897
- st.write(f"✅ Résultat: {type(pdf_bytes)}")
898
-
899
- if pdf_bytes:
900
- st.write(f"✅ Taille PDF: {len(pdf_bytes.getvalue())} bytes")
901
 
902
- # Mise à jour du statut
903
- try:
904
- cell_trans = ws_remb.find(trans_data['trans_id'])
905
- header_remb = ws_remb.row_values(1)
906
- col_recu_idx = header_remb.index("Recu_Emis") + 1
907
- ws_remb.update_cell(cell_trans.row, col_recu_idx, "OUI")
908
- st.write("✅ Statut mis à jour")
909
- except Exception as e_update:
910
- st.warning(f"⚠️ Mise à jour statut : {e_update}")
911
 
912
- # Bouton de téléchargement
913
- st.download_button(
914
- label="💾 TÉLÉCHARGER LE PDF ICI",
915
- data=pdf_bytes,
916
- file_name=f"{trans_data['numero_recu']}_{trans_data['loan_id']}.pdf",
917
- mime="application/pdf"
918
- )
919
- st.success("✅ Reçu prêt ! Cliquez sur le bouton ci-dessus.")
920
- else:
921
- st.error("❌ generer_recu() a retourné None")
922
-
923
- except Exception as e_pdf:
924
- st.error(f" ERREUR : {e_pdf}")
925
- import traceback
926
- st.code(traceback.format_exc())
927
-
928
- with col_btn2:
929
- if st.button("🔄 Nouvelle Transaction", key="btn_new_transaction"):
930
- if 'derniere_transaction' in st.session_state:
931
- del st.session_state['derniere_transaction']
932
- if 'selected_scenario' in st.session_state:
933
- del st.session_state['selected_scenario']
934
- st.rerun()
 
 
 
 
 
 
 
 
 
 
935
 
936
  else:
937
  st.warning("⚠️ Aucune transaction enregistrée. Veuillez d'abord effectuer un remboursement.")
 
598
  # --- 5. SAISIE DU PAIEMENT ---
599
  st.divider()
600
  st.subheader("5. Enregistrement du Paiement")
601
+
602
  with st.form("repayment_form", clear_on_submit=True):
603
  col_a, col_b = st.columns(2)
604
+
605
  with col_a:
606
  montant_verse = st.number_input(
607
  "Montant Versé (XOF)",
 
610
  value=int(st.session_state.get('selected_scenario', {}).get('total_a_encaisser', echeance_info['montant_ajuste'])),
611
  help="Montant exact reçu"
612
  )
613
+
614
  with col_b:
615
  moyen = st.selectbox(
616
  "Moyen de Paiement",
617
  ["Espèces", "Mobile Money (Wave)", "Mobile Money (Orange Money)", "Virement Bancaire", "Chèque"],
618
  help="Canal de réception des fonds"
619
  )
620
+
621
  reference_externe = st.text_input(
622
  "Référence Transaction",
623
  placeholder="Ex: WAVE-TXN-123456, Numéro de chèque...",
624
  help="Référence externe pour traçabilité"
625
  )
626
+
627
  commentaire = st.text_area(
628
  "Commentaire",
629
  placeholder="Notes additionnelles sur la transaction...",
630
  help="Informations complémentaires"
631
  )
632
+
633
  submit = st.form_submit_button("✅ VALIDER L'ENCAISSEMENT", use_container_width=True)
634
+
635
  if submit:
636
  if montant_verse <= 0:
637
  st.error("❌ Le montant versé doit être supérieur à 0 XOF")
 
639
  try:
640
  # Récupération du taux de pénalité sélectionné
641
  taux_penalite = st.session_state.get('selected_scenario', {}).get('taux', 0.0) / 100
642
+
643
  # Analyse complète
644
  analyse_complete = analyser.analyser_remboursement_complet(
645
  loan_id,
 
647
  montant_verse,
648
  taux_penalite=taux_penalite
649
  )
650
+
651
  if 'error' in analyse_complete:
652
  st.error(f"❌ {analyse_complete['error']}")
653
  else:
 
656
  next_id = len(existing_remb)
657
  trans_id = f"TRX-2026-{next_id:04d}"
658
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
659
+
660
  # Génération Numéro Reçu
661
  annee_actuelle = datetime.now().year
662
  count_annee = len([r for r in existing_remb if f"REC-{annee_actuelle}" in str(r)])
663
  numero_recu = f"REC-{annee_actuelle}-{count_annee + 1:04d}"
664
+
665
  # 1. Écriture dans Remboursements (avec conversions explicites)
666
  new_row = [
667
  str(trans_id),
668
  str(loan_id),
669
  str(analyse_complete['id_client']),
670
  str(date_paiement),
671
+ int(montant_verse),
672
  int(analyse_complete['montant_principal']),
673
  int(analyse_complete['montant_interets']),
674
  int(analyse_complete['penalites_retard']),
 
686
  str(timestamp)
687
  ]
688
  ws_remb.append_row(new_row)
689
+
690
  # 2. Gestion des ajustements si PARTIEL
691
  if analyse_complete['statut_paiement'] == "PARTIEL" and analyse_complete['montant_a_reporter'] > 0:
692
  if analyse_complete['prochaine_echeance']:
 
697
  ws_ajust = sh.add_worksheet(title="Ajustements_Echeances", rows="1000", cols="7")
698
  ws_ajust.append_row(["ID_Ajustement", "ID_Pret", "Numero_Echeance", "Montant_Additionnel", "Raison", "Date_Creation", "Timestamp"])
699
  next_ajust_id = 1
700
+
701
  ajust_id = f"ADJ-{annee_actuelle}-{next_ajust_id:04d}"
702
+
703
  ajust_row = [
704
  str(ajust_id),
705
  str(loan_id),
 
710
  str(timestamp)
711
  ]
712
  ws_ajust.append_row(ajust_row)
713
+
714
  st.warning(f"⚠️ Paiement PARTIEL détecté. {analyse_complete['montant_a_reporter']:,.0f} XOF reportés sur l'échéance #{analyse_complete['prochaine_echeance']}")
715
+
716
+ # 3. CLÔTURE AUTOMATIQUE (GESTION UPDATED)
717
  if analyse_complete['doit_cloturer']:
 
718
  if is_updated and ws_prets_update is not None:
 
719
  try:
720
  cell = ws_prets_update.find(loan_id)
721
  header_update = ws_prets_update.row_values(1)
722
  col_statut_idx = header_update.index("Statut") + 1
723
  ws_prets_update.update_cell(cell.row, col_statut_idx, "TERMINE")
724
+
 
725
  if "Date_Fin" in header_update:
726
  col_fin_idx = header_update.index("Date_Fin") + 1
727
  ws_prets_update.update_cell(cell.row, col_fin_idx, str(date.today()))
728
+
729
  st.info(f"📝 Statut mis à jour dans Prets_Update (Version {version_info})")
730
  except Exception as e:
731
  st.warning(f"⚠️ Impossible de mettre à jour Prets_Update : {e}")
732
  else:
 
733
  try:
734
  cell = ws_prets.find(loan_id)
735
  header = ws_prets.row_values(1)
736
  col_statut_idx = header.index("Statut") + 1
737
  ws_prets.update_cell(cell.row, col_statut_idx, "TERMINE")
738
+
 
739
  if "Date_Cloture" in header:
740
  col_cloture_idx = header.index("Date_Cloture") + 1
741
  ws_prets.update_cell(cell.row, col_cloture_idx, str(date.today()))
742
+
743
  st.info(f"📝 Statut mis à jour dans Prets_Master")
744
  except Exception as e:
745
  st.warning(f"⚠️ Impossible de mettre à jour Prets_Master : {e}")
746
+
 
747
  critere_messages = {
748
  'SOLDE_ZERO': 'Solde intégralement remboursé',
749
  'TOUTES_ECHEANCES': 'Toutes les échéances payées',
750
  'MONTANT_TOTAL': 'Montant total du prêt remboursé'
751
  }
752
+
753
  st.markdown(f"""
754
  <div class="repayment-success-badge">
755
  <h3>✅ DOSSIER {loan_id} AUTOMATIQUEMENT CLÔTURÉ</h3>
 
759
  </p>
760
  </div>
761
  """, unsafe_allow_html=True)
762
+
763
  st.info(f"Détails : {analyse_complete['message_cloture']}")
764
  else:
765
  st.success(f"✅ Paiement de **{montant_verse:,.0f} XOF** enregistré avec succès")
766
  st.info(f"Référence de transaction : **{trans_id}**")
767
+
768
  if analyse_complete['solde_apres'] > 0:
769
  st.info(f"Solde restant à rembourser : **{analyse_complete['solde_apres']:,.0f} XOF**")
770
+
771
  # Préparer les données pour le reçu
772
  client_data_dict = df_clients[df_clients['ID_Client'] == analyse_complete['id_client']].iloc[0].to_dict()
773
 
 
774
  if hasattr(loan_data, 'to_dict'):
775
  loan_data_dict = loan_data.to_dict()
776
  else:
777
  loan_data_dict = dict(loan_data)
778
 
779
+ # Conversion des types pandas
780
  def convert_to_native(obj):
 
781
  import numpy as np
782
 
783
  if isinstance(obj, dict):
 
795
  else:
796
  return obj
797
 
 
798
  client_data_dict = convert_to_native(client_data_dict)
799
  loan_data_dict = convert_to_native(loan_data_dict)
800
  analyse_complete_clean = convert_to_native(analyse_complete)
 
817
  'numero_recu': numero_recu,
818
  'loan_id': loan_id
819
  }
820
+
821
  # Récapitulatif détaillé
822
  with st.expander("📋 Récapitulatif de la transaction", expanded=True):
823
  recap_col1, recap_col2, recap_col3 = st.columns(3)
 
836
  st.write(f"**Solde Après :** {analyse_complete['solde_apres']:,.0f} XOF")
837
  st.write(f"**Moyen :** {moyen}")
838
  st.write(f"**Statut :** {analyse_complete['statut_paiement']}")
839
+
840
  except Exception as e:
841
  st.error(f"❌ Erreur lors de l'enregistrement : {e}")
842
  st.exception(e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
843
 
844
+ # === GÉNÉRATION DU REÇU (EN DEHORS DU FORMULAIRE) ===
845
+ if 'derniere_transaction' in st.session_state:
846
+ st.divider()
847
+ st.subheader("📄 Génération du Reçu")
848
 
849
+ trans_data = st.session_state['derniere_transaction']
850
+
851
+ st.success(f"✅ Transaction **{trans_data['trans_id']}** enregistrée avec succès")
852
+ st.caption(f"Reçu N° : **{trans_data['numero_recu']}**")
853
+
854
+ col_btn1, col_btn2 = st.columns(2)
855
+
856
+ with col_btn1:
857
+ if st.button("📥 Générer et Télécharger le Reçu PDF", type="primary"):
858
+ try:
859
+ from DocumentGen.InvoiceRepayment import generer_recu
 
 
 
 
 
 
 
860
 
861
+ pdf_bytes = generer_recu(trans_data['recu_data'])
 
 
 
 
 
 
 
 
862
 
863
+ if pdf_bytes:
864
+ # Mise à jour du statut
865
+ try:
866
+ cell_trans = ws_remb.find(trans_data['trans_id'])
867
+ header_remb = ws_remb.row_values(1)
868
+ col_recu_idx = header_remb.index("Recu_Emis") + 1
869
+ ws_remb.update_cell(cell_trans.row, col_recu_idx, "OUI")
870
+ except Exception as e_update:
871
+ st.warning(f"⚠️ Impossible de mettre à jour le statut : {e_update}")
872
+
873
+ # Bouton de téléchargement
874
+ st.download_button(
875
+ label="💾 CLIQUER ICI POUR TÉLÉCHARGER LE PDF",
876
+ data=pdf_bytes,
877
+ file_name=f"{trans_data['numero_recu']}_{trans_data['loan_id']}.pdf",
878
+ mime="application/pdf"
879
+ )
880
+ st.success(" Reçu généré ! Cliquez sur le bouton ci-dessus pour télécharger.")
881
+ else:
882
+ st.error("❌ Erreur lors de la génération du PDF")
883
+
884
+ except Exception as e_pdf:
885
+ st.error(f"❌ Erreur : {e_pdf}")
886
+ import traceback
887
+ st.code(traceback.format_exc())
888
+
889
+ with col_btn2:
890
+ if st.button("🔄 Nouvelle Transaction"):
891
+ if 'derniere_transaction' in st.session_state:
892
+ del st.session_state['derniere_transaction']
893
+ if 'selected_scenario' in st.session_state:
894
+ del st.session_state['selected_scenario']
895
+ st.rerun()
896
 
897
  else:
898
  st.warning("⚠️ Aucune transaction enregistrée. Veuillez d'abord effectuer un remboursement.")