Bhuvi13 commited on
Commit
a423602
Β·
verified Β·
1 Parent(s): c5210a9

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +86 -88
src/streamlit_app.py CHANGED
@@ -38,11 +38,6 @@ import pandas as pd
38
  from PIL import Image
39
  from huggingface_hub import login
40
 
41
- # πŸ‘‡ ADD CALLBACK FUNCTION AT TOP LEVEL
42
- def trigger_rerun():
43
- # Forces immediate rerun after any edit β€” ensures edited_df is fresh
44
- pass
45
-
46
  # ---------------------------
47
  # UI: main
48
  # ---------------------------
@@ -178,7 +173,7 @@ def run_inference_on_image(image: Image.Image, processor, model, device, decoder
178
  pixel_values=pixel_values,
179
  decoder_input_ids=decoder_input_ids,
180
  max_length=1536,
181
- num_beams=1,
182
  early_stopping=False,
183
  )
184
 
@@ -681,15 +676,15 @@ if not st.session_state.is_processing_batch and len(st.session_state.batch_resul
681
  # RESULTS VIEW β€” Show selector + editable form
682
  # ---------------------------
683
  elif len(st.session_state.batch_results) > 0:
684
- # ---------------------------
685
- # Global Download All β€” produce a single Excel file (concatenated rows) and trigger direct download
686
- # ---------------------------
687
  if st.button("πŸ“¦ Download All Results (Excel)", key="download_all"):
688
- # Collect rows from all invoices and concatenate into one DataFrame
689
  all_rows = []
690
  for file_hash, result in st.session_state.batch_results.items():
691
  rows = flatten_invoice_to_rows(result["edited_data"])
692
- # Annotate rows with source file name so user can identify which invoice each row came from
693
  for r in rows:
694
  r["Source File"] = result.get("file_name", file_hash)
695
  all_rows.extend(rows)
@@ -699,13 +694,13 @@ elif len(st.session_state.batch_results) > 0:
699
  else:
700
  full_df = pd.DataFrame(all_rows)
701
 
702
- # Reorder columns to put Source File first
703
  cols = list(full_df.columns)
704
  if "Source File" in cols:
705
  cols = ["Source File"] + [c for c in cols if c != "Source File"]
706
  full_df = full_df[cols]
707
 
708
- # Try to write XLSX (preferred). If engine not available, fall back to CSV.
709
  buffer = BytesIO()
710
  dl_filename = "all_extracted_invoices.xlsx"
711
  tried_xlsx = False
@@ -726,7 +721,7 @@ elif len(st.session_state.batch_results) > 0:
726
  dl_filename = "all_extracted_invoices.csv"
727
  mime = "text/csv"
728
 
729
- # Trigger immediate download via a data URI and small HTML snippet
730
  import base64
731
  import streamlit.components.v1 as components
732
  b64 = base64.b64encode(file_bytes).decode()
@@ -768,8 +763,9 @@ elif len(st.session_state.batch_results) > 0:
768
  # Get current file data
769
  current = st.session_state.batch_results[selected_hash]
770
  image = current["image"]
771
- import copy
772
- form_data = copy.deepcopy(current["edited_data"])
 
773
 
774
  # Layout
775
  left_col, right_col = st.columns([1, 1])
@@ -806,6 +802,7 @@ elif len(st.session_state.batch_results) > 0:
806
  st.rerun()
807
  except Exception as e:
808
  st.error(f"Re-run failed: {e}")
 
809
  tabs = st.tabs(["Invoice Details", "Sender/Recipient info", "Bank Details", "Line Items"])
810
 
811
  st.markdown(
@@ -830,48 +827,53 @@ elif len(st.session_state.batch_results) > 0:
830
  """,
831
  unsafe_allow_html=True,
832
  )
 
833
  # ---------- Invoice Details ----------
 
834
  with tabs[0]:
835
  with st.container():
836
- form_data['Invoice Number'] = st.text_input("Invoice Number", value=form_data.get('Invoice Number', ''), key=f"invoice_number_{selected_hash}")
837
- form_data['Invoice Date'] = st.text_input("Invoice Date", value=str(form_data.get('Invoice Date', '')).strip(), key=f"invoice_date_text_{selected_hash}")
838
- form_data['Due Date'] = st.text_input("Due Date", value=str(form_data.get('Due Date', '')).strip(), key=f"due_date_text_{selected_hash}")
 
839
  curr_options = ['USD', 'EUR', 'GBP', 'INR', 'Other']
840
  curr_value = form_data.get('Currency', 'USD')
841
  curr_index = curr_options.index(curr_value) if curr_value in curr_options else (len(curr_options) - 1)
842
- new_curr = st.selectbox("Currency", options=curr_options, index=curr_index, key=f"currency_select_{selected_hash}")
843
- if new_curr == 'Other':
844
- new_curr = st.text_input("Specify Currency", value=form_data.get('Currency', ''), key=f"custom_currency_{selected_hash}")
845
- form_data['Currency'] = new_curr
846
- form_data['Subtotal'] = safe_number_input("Subtotal", form_data.get('Subtotal', 0.0), f"subtotal_{selected_hash}")
847
- form_data['Tax Percentage'] = safe_number_input("Tax Percentage", form_data.get('Tax Percentage', 0.0), f"tax_pct_{selected_hash}")
848
- form_data['Total Tax'] = safe_number_input("Total Tax", form_data.get('Total Tax', 0.0), f"total_tax_{selected_hash}")
849
- form_data['Total Amount'] = safe_number_input("Total Amount", form_data.get('Total Amount', 0.0), f"total_amount_{selected_hash}")
850
 
851
  # ---------- Sender / Recipient ----------
852
  with tabs[1]:
853
- # Get FLAT top-level keys (NOT nested dictionaries)
854
  sender_name = form_data.get('Sender Name', '')
855
  sender_address = form_data.get('Sender Address', '')
856
  recipient_name = form_data.get('Recipient Name', '')
857
  recipient_address = form_data.get('Recipient Address', '')
858
 
859
  with st.container():
860
- sender_name = st.text_input("Sender Name*", value=sender_name, key=f"sender_name_{selected_hash}")
861
- sender_address = st.text_area("Sender Address*", value=sender_address, key=f"sender_address_{selected_hash}")
862
- recipient_name = st.text_input("Recipient Name*", value=recipient_name, key=f"recipient_name_{selected_hash}")
863
- recipient_address = st.text_area("Recipient Address*", value=recipient_address, key=f"recipient_address_{selected_hash}")
864
 
865
  if st.button("⇄ Swap", help="Swap sender and recipient information", key=f"swap_{selected_hash}"):
866
- # Swap individual fields (NOT nested objects)
867
- form_data['Sender Name'], form_data['Recipient Name'] = form_data['Recipient Name'], form_data['Sender Name']
868
- form_data['Sender Address'], form_data['Recipient Address'] = form_data['Recipient Address'], form_data['Sender Address']
 
 
 
 
 
869
  st.rerun()
870
 
871
- # ---------- Bank Details ---------- (FIXED)
872
- # ---------- Bank Details ---------- (FIXED)
873
  with tabs[2]:
874
- # Get bank details from nested dictionary
875
  bank_details = form_data.get("Bank Details", {})
876
  if not isinstance(bank_details, dict):
877
  bank_details = {}
@@ -885,31 +887,20 @@ elif len(st.session_state.batch_results) > 0:
885
  bank_branch = bank_details.get('bank_branch', '')
886
 
887
  with st.container():
888
- bank_name = st.text_input("Bank Name", value=bank_name, key=f"bank_name_{selected_hash}")
889
- bank_acc_no = st.text_input("Account Number", value=bank_acc_no, key=f"bank_acc_no_{selected_hash}")
890
- bank_acc_name = st.text_input("Bank Account Name", value=bank_acc_name, key=f"bank_acc_name_{selected_hash}")
891
- bank_iban = st.text_input("IBAN", value=bank_iban, key=f"iban_{selected_hash}")
892
- bank_swift = st.text_input("SWIFT Code", value=bank_swift, key=f"swift_code_{selected_hash}")
893
- bank_routing = st.text_input("Routing Number", value=bank_routing, key=f"routing_{selected_hash}")
894
- bank_branch = st.text_input("Branch", value=bank_branch, key=f"branch_{selected_hash}")
895
-
896
- # Update the nested Bank Details dictionary
897
- form_data.setdefault("Bank Details", {})
898
- form_data["Bank Details"]["bank_name"] = bank_name
899
- form_data["Bank Details"]["bank_acc_no"] = bank_acc_no
900
- form_data["Bank Details"]["bank_acc_name"] = bank_acc_name
901
- form_data["Bank Details"]["bank_iban"] = bank_iban
902
- form_data["Bank Details"]["bank_swift"] = bank_swift
903
- form_data["Bank Details"]["bank_routing"] = bank_routing
904
- form_data["Bank Details"]["bank_branch"] = bank_branch
905
 
906
  # ---------- Line Items ----------
907
- # ---------- Line Items ----------
908
  with tabs[3]:
909
  editor_key = f"item_editor_{selected_hash}"
910
  item_rows = form_data.get('Itemized Data', []) or []
911
 
912
- # --- Normalize item keys produced by the model (e.g. "Item Description") into simple keys ---
913
  def normalize_item_keys(item):
914
  if not isinstance(item, dict):
915
  return {
@@ -938,17 +929,13 @@ elif len(st.session_state.batch_results) > 0:
938
  "line_total": "Line Total",
939
  }
940
  new = {}
941
- # map keys
942
  for k, v in item.items():
943
  key = mapping.get(k, mapping.get(str(k).lower(), k))
944
- # normalize to our canonical set where possible
945
  if key in ["Description", "Quantity", "Unit Price", "Amount", "Tax", "Line Total"]:
946
  new[key] = v
947
  else:
948
- # store unknowns too so user can see them if needed
949
  new[k] = v
950
 
951
- # ensure canonical columns exist (avoid missing columns in DataFrame)
952
  for kk in ["Description", "Quantity", "Unit Price", "Amount", "Tax", "Line Total"]:
953
  if kk not in new:
954
  new[kk] = ""
@@ -956,11 +943,8 @@ elif len(st.session_state.batch_results) > 0:
956
  return new
957
 
958
  normalized_items = [normalize_item_keys(it) for it in item_rows]
959
-
960
- # Create DataFrame from normalized items (these columns will be visible)
961
  df = pd.DataFrame(normalized_items)
962
 
963
- # Ensure columns exist and are named user-friendly
964
  for col in ["Description", "Quantity", "Unit Price", "Amount", "Tax", "Line Total"]:
965
  if col not in df.columns:
966
  df[col] = ""
@@ -975,38 +959,57 @@ elif len(st.session_state.batch_results) > 0:
975
  if len(edited_df) == 0:
976
  st.info("No line items found in the invoice.")
977
 
978
-
979
- # Save button (per file)
980
  if st.button("πŸ’Ύ Save Edits for This File", key=f"save_{selected_hash}"):
981
- # Update line items from data editor
982
- form_data['Itemized Data'] = edited_df.to_dict('records')
983
- # Update session state with complete form data
984
- st.session_state.batch_results[selected_hash]["edited_data"] = form_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
985
  st.success(f"βœ… Edits saved for {current['file_name']}")
986
 
987
  # Download buttons (per file)
988
  st.markdown("---")
989
  col_a, col_b, col_c = st.columns([1, 1, 1])
990
- #with col_a:
991
- #jsonl_str = json.dumps(data, ensure_ascii=False, indent=2)
992
- #st.download_button(
993
- # "πŸ“₯ Download JSON",
994
- #jsonl_str.encode("utf-8"),
995
- #file_name=f"{Path(current['file_name']).stem}_extracted.json",
996
- #mime="application/json",
997
- #key=f"dl_json_{selected_hash}"
998
- #)
999
  with col_b:
1000
- # βœ… Flatten entire invoice into rows (one per line item)
1001
- rows = flatten_invoice_to_rows(form_data)
1002
  full_df = pd.DataFrame(rows)
1003
 
1004
  # Optional: Reorder columns for better readability
1005
  desired_col_order = [
1006
  "Invoice Number", "Invoice Date", "Due Date", "Currency",
1007
- "Sender Name", "Sender Address", "Recipient Name", "Recipient Address",
1008
  "Subtotal", "Tax Percentage", "Total Tax", "Total Amount",
1009
- "bank_name", "bank_acc_no", "bank_iban", "bank_swift", "bank_routing", "bank_branch", "bank_acc_name",
 
1010
  "Item Description", "Item Quantity", "Item Unit Price", "Item Amount", "Item Tax", "Item Line Total"
1011
  ]
1012
  # Keep only columns that exist
@@ -1025,11 +1028,6 @@ elif len(st.session_state.batch_results) > 0:
1025
  mime="text/csv",
1026
  key=f"dl_csv_{selected_hash}"
1027
  )
1028
- # Global Download All β€” produce a single Excel file (concatenated rows) and trigger direct download
1029
-
1030
-
1031
- # ---------------------------
1032
- # PROCESSING STATE
1033
 
1034
  # ---------------------------
1035
  # PROCESSING STATE β€” Show progress
 
38
  from PIL import Image
39
  from huggingface_hub import login
40
 
 
 
 
 
 
41
  # ---------------------------
42
  # UI: main
43
  # ---------------------------
 
173
  pixel_values=pixel_values,
174
  decoder_input_ids=decoder_input_ids,
175
  max_length=1536,
176
+ num_beams=4,
177
  early_stopping=False,
178
  )
179
 
 
676
  # RESULTS VIEW β€” Show selector + editable form
677
  # ---------------------------
678
  elif len(st.session_state.batch_results) > 0:
679
+ # ---------------------------
680
+ # Global Download All β€” produce a single Excel file (concatenated rows) and trigger direct download
681
+ # ---------------------------
682
  if st.button("πŸ“¦ Download All Results (Excel)", key="download_all"):
683
+ # Collect rows from all invoices and concatenate into one DataFrame
684
  all_rows = []
685
  for file_hash, result in st.session_state.batch_results.items():
686
  rows = flatten_invoice_to_rows(result["edited_data"])
687
+ # Annotate rows with source file name so user can identify which invoice each row came from
688
  for r in rows:
689
  r["Source File"] = result.get("file_name", file_hash)
690
  all_rows.extend(rows)
 
694
  else:
695
  full_df = pd.DataFrame(all_rows)
696
 
697
+ # Reorder columns to put Source File first
698
  cols = list(full_df.columns)
699
  if "Source File" in cols:
700
  cols = ["Source File"] + [c for c in cols if c != "Source File"]
701
  full_df = full_df[cols]
702
 
703
+ # Try to write XLSX (preferred). If engine not available, fall back to CSV.
704
  buffer = BytesIO()
705
  dl_filename = "all_extracted_invoices.xlsx"
706
  tried_xlsx = False
 
721
  dl_filename = "all_extracted_invoices.csv"
722
  mime = "text/csv"
723
 
724
+ # Trigger immediate download via a data URI and small HTML snippet
725
  import base64
726
  import streamlit.components.v1 as components
727
  b64 = base64.b64encode(file_bytes).decode()
 
763
  # Get current file data
764
  current = st.session_state.batch_results[selected_hash]
765
  image = current["image"]
766
+
767
+ # βœ… FIX: Don't create a copy here - just reference the stored data
768
+ form_data = current["edited_data"]
769
 
770
  # Layout
771
  left_col, right_col = st.columns([1, 1])
 
802
  st.rerun()
803
  except Exception as e:
804
  st.error(f"Re-run failed: {e}")
805
+
806
  tabs = st.tabs(["Invoice Details", "Sender/Recipient info", "Bank Details", "Line Items"])
807
 
808
  st.markdown(
 
827
  """,
828
  unsafe_allow_html=True,
829
  )
830
+
831
  # ---------- Invoice Details ----------
832
+ # βœ… FIX: Read values directly from widgets without assigning back to form_data
833
  with tabs[0]:
834
  with st.container():
835
+ st.text_input("Invoice Number", value=form_data.get('Invoice Number', ''), key=f"invoice_number_{selected_hash}")
836
+ st.text_input("Invoice Date", value=str(form_data.get('Invoice Date', '')).strip(), key=f"invoice_date_text_{selected_hash}")
837
+ st.text_input("Due Date", value=str(form_data.get('Due Date', '')).strip(), key=f"due_date_text_{selected_hash}")
838
+
839
  curr_options = ['USD', 'EUR', 'GBP', 'INR', 'Other']
840
  curr_value = form_data.get('Currency', 'USD')
841
  curr_index = curr_options.index(curr_value) if curr_value in curr_options else (len(curr_options) - 1)
842
+ st.selectbox("Currency", options=curr_options, index=curr_index, key=f"currency_select_{selected_hash}")
843
+ if st.session_state.get(f"currency_select_{selected_hash}") == 'Other':
844
+ st.text_input("Specify Currency", value=form_data.get('Currency', ''), key=f"custom_currency_{selected_hash}")
845
+
846
+ safe_number_input("Subtotal", form_data.get('Subtotal', 0.0), f"subtotal_{selected_hash}")
847
+ safe_number_input("Tax Percentage", form_data.get('Tax Percentage', 0.0), f"tax_pct_{selected_hash}")
848
+ safe_number_input("Total Tax", form_data.get('Total Tax', 0.0), f"total_tax_{selected_hash}")
849
+ safe_number_input("Total Amount", form_data.get('Total Amount', 0.0), f"total_amount_{selected_hash}")
850
 
851
  # ---------- Sender / Recipient ----------
852
  with tabs[1]:
 
853
  sender_name = form_data.get('Sender Name', '')
854
  sender_address = form_data.get('Sender Address', '')
855
  recipient_name = form_data.get('Recipient Name', '')
856
  recipient_address = form_data.get('Recipient Address', '')
857
 
858
  with st.container():
859
+ st.text_input("Sender Name*", value=sender_name, key=f"sender_name_{selected_hash}")
860
+ st.text_area("Sender Address*", value=sender_address, key=f"sender_address_{selected_hash}")
861
+ st.text_input("Recipient Name*", value=recipient_name, key=f"recipient_name_{selected_hash}")
862
+ st.text_area("Recipient Address*", value=recipient_address, key=f"recipient_address_{selected_hash}")
863
 
864
  if st.button("⇄ Swap", help="Swap sender and recipient information", key=f"swap_{selected_hash}"):
865
+ # Swap in session_state widget values
866
+ temp_name = st.session_state.get(f"sender_name_{selected_hash}", "")
867
+ temp_addr = st.session_state.get(f"sender_address_{selected_hash}", "")
868
+
869
+ st.session_state[f"sender_name_{selected_hash}"] = st.session_state.get(f"recipient_name_{selected_hash}", "")
870
+ st.session_state[f"sender_address_{selected_hash}"] = st.session_state.get(f"recipient_address_{selected_hash}", "")
871
+ st.session_state[f"recipient_name_{selected_hash}"] = temp_name
872
+ st.session_state[f"recipient_address_{selected_hash}"] = temp_addr
873
  st.rerun()
874
 
875
+ # ---------- Bank Details ----------
 
876
  with tabs[2]:
 
877
  bank_details = form_data.get("Bank Details", {})
878
  if not isinstance(bank_details, dict):
879
  bank_details = {}
 
887
  bank_branch = bank_details.get('bank_branch', '')
888
 
889
  with st.container():
890
+ st.text_input("Bank Name", value=bank_name, key=f"bank_name_{selected_hash}")
891
+ st.text_input("Account Number", value=bank_acc_no, key=f"bank_acc_no_{selected_hash}")
892
+ st.text_input("Bank Account Name", value=bank_acc_name, key=f"bank_acc_name_{selected_hash}")
893
+ st.text_input("IBAN", value=bank_iban, key=f"iban_{selected_hash}")
894
+ st.text_input("SWIFT Code", value=bank_swift, key=f"swift_code_{selected_hash}")
895
+ st.text_input("Routing Number", value=bank_routing, key=f"routing_{selected_hash}")
896
+ st.text_input("Branch", value=bank_branch, key=f"branch_{selected_hash}")
 
 
 
 
 
 
 
 
 
 
897
 
898
  # ---------- Line Items ----------
 
899
  with tabs[3]:
900
  editor_key = f"item_editor_{selected_hash}"
901
  item_rows = form_data.get('Itemized Data', []) or []
902
 
903
+ # --- Normalize item keys produced by the model ---
904
  def normalize_item_keys(item):
905
  if not isinstance(item, dict):
906
  return {
 
929
  "line_total": "Line Total",
930
  }
931
  new = {}
 
932
  for k, v in item.items():
933
  key = mapping.get(k, mapping.get(str(k).lower(), k))
 
934
  if key in ["Description", "Quantity", "Unit Price", "Amount", "Tax", "Line Total"]:
935
  new[key] = v
936
  else:
 
937
  new[k] = v
938
 
 
939
  for kk in ["Description", "Quantity", "Unit Price", "Amount", "Tax", "Line Total"]:
940
  if kk not in new:
941
  new[kk] = ""
 
943
  return new
944
 
945
  normalized_items = [normalize_item_keys(it) for it in item_rows]
 
 
946
  df = pd.DataFrame(normalized_items)
947
 
 
948
  for col in ["Description", "Quantity", "Unit Price", "Amount", "Tax", "Line Total"]:
949
  if col not in df.columns:
950
  df[col] = ""
 
959
  if len(edited_df) == 0:
960
  st.info("No line items found in the invoice.")
961
 
962
+ # βœ… FIX: Save button now collects values from session_state widgets
 
963
  if st.button("πŸ’Ύ Save Edits for This File", key=f"save_{selected_hash}"):
964
+ # Collect all values from session_state
965
+ updated_data = {
966
+ 'Invoice Number': st.session_state.get(f"invoice_number_{selected_hash}", ""),
967
+ 'Invoice Date': st.session_state.get(f"invoice_date_text_{selected_hash}", ""),
968
+ 'Due Date': st.session_state.get(f"due_date_text_{selected_hash}", ""),
969
+ 'Currency': st.session_state.get(f"custom_currency_{selected_hash}", "") if st.session_state.get(f"currency_select_{selected_hash}") == 'Other' else st.session_state.get(f"currency_select_{selected_hash}", "USD"),
970
+ 'Subtotal': st.session_state.get(f"subtotal_{selected_hash}", 0.0),
971
+ 'Tax Percentage': st.session_state.get(f"tax_pct_{selected_hash}", 0.0),
972
+ 'Total Tax': st.session_state.get(f"total_tax_{selected_hash}", 0.0),
973
+ 'Total Amount': st.session_state.get(f"total_amount_{selected_hash}", 0.0),
974
+ 'Sender Name': st.session_state.get(f"sender_name_{selected_hash}", ""),
975
+ 'Sender Address': st.session_state.get(f"sender_address_{selected_hash}", ""),
976
+ 'Recipient Name': st.session_state.get(f"recipient_name_{selected_hash}", ""),
977
+ 'Recipient Address': st.session_state.get(f"recipient_address_{selected_hash}", ""),
978
+ 'Bank Details': {
979
+ 'bank_name': st.session_state.get(f"bank_name_{selected_hash}", ""),
980
+ 'bank_acc_no': st.session_state.get(f"bank_acc_no_{selected_hash}", ""),
981
+ 'bank_acc_name': st.session_state.get(f"bank_acc_name_{selected_hash}", ""),
982
+ 'bank_iban': st.session_state.get(f"iban_{selected_hash}", ""),
983
+ 'bank_swift': st.session_state.get(f"swift_code_{selected_hash}", ""),
984
+ 'bank_routing': st.session_state.get(f"routing_{selected_hash}", ""),
985
+ 'bank_branch': st.session_state.get(f"branch_{selected_hash}", "")
986
+ },
987
+ 'Itemized Data': edited_df.to_dict('records')
988
+ }
989
+
990
+ # Also set convenience fields
991
+ updated_data['Sender'] = {"Name": updated_data['Sender Name'], "Address": updated_data['Sender Address']}
992
+ updated_data['Recipient'] = {"Name": updated_data['Recipient Name'], "Address": updated_data['Recipient Address']}
993
+
994
+ # Update session state
995
+ st.session_state.batch_results[selected_hash]["edited_data"] = updated_data
996
  st.success(f"βœ… Edits saved for {current['file_name']}")
997
 
998
  # Download buttons (per file)
999
  st.markdown("---")
1000
  col_a, col_b, col_c = st.columns([1, 1, 1])
1001
+
 
 
 
 
 
 
 
 
1002
  with col_b:
1003
+ # Use the saved edited_data (not the temporary form_data)
1004
+ rows = flatten_invoice_to_rows(st.session_state.batch_results[selected_hash]["edited_data"])
1005
  full_df = pd.DataFrame(rows)
1006
 
1007
  # Optional: Reorder columns for better readability
1008
  desired_col_order = [
1009
  "Invoice Number", "Invoice Date", "Due Date", "Currency",
 
1010
  "Subtotal", "Tax Percentage", "Total Tax", "Total Amount",
1011
+ "Sender Name", "Sender Address", "Recipient Name", "Recipient Address",
1012
+ "bank_name", "bank_acc_no", "bank_acc_name", "bank_iban", "bank_swift", "bank_routing", "bank_branch",
1013
  "Item Description", "Item Quantity", "Item Unit Price", "Item Amount", "Item Tax", "Item Line Total"
1014
  ]
1015
  # Keep only columns that exist
 
1028
  mime="text/csv",
1029
  key=f"dl_csv_{selected_hash}"
1030
  )
 
 
 
 
 
1031
 
1032
  # ---------------------------
1033
  # PROCESSING STATE β€” Show progress