rehan953 commited on
Commit
e6912c4
·
verified ·
1 Parent(s): 416ddf3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +219 -0
app.py CHANGED
@@ -113,6 +113,12 @@ Primary goals for reconcile rate:
113
  Additions/Subtractions tables to the PDF: credits as Number|Date|Description|Additions
114
  (blank Number when unused); debits as Date|Description|Subtractions (no Number); merges
115
  OCR column splits. Adjacent duplicate HTML tables (same header + row multiset) are removed.
 
 
 
 
 
 
116
  """
117
 
118
  # Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
@@ -2867,6 +2873,216 @@ def _normalize_fused_date_posted_amount_table(grid):
2867
  return grid
2868
  return out
2869
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2870
  def normalize_html_tables(text: str) -> str:
2871
  if not text or "<table" not in text.lower():
2872
  return text
@@ -2908,6 +3124,9 @@ def normalize_html_tables(text: str) -> str:
2908
 
2909
  grid = _split_fused_date_transaction_header(grid)
2910
 
 
 
 
2911
  grid = _normalize_fused_date_posted_amount_table(grid)
2912
 
2913
  grid = _split_fused_multicolumn_header(grid)
 
113
  Additions/Subtractions tables to the PDF: credits as Number|Date|Description|Additions
114
  (blank Number when unused); debits as Date|Description|Subtractions (no Number); merges
115
  OCR column splits. Adjacent duplicate HTML tables (same header + row multiset) are removed.
116
+ - Fix TD-1: TD Bank continuation register: (a) four OCR columns with DATE+DESCRIPTION fused +
117
+ DEBIT/CREDIT/BALANCE — split col0 into Date|Description; (b) three OCR columns
118
+ Date|Transaction Description|Amount where Amount is the running balance — expand to
119
+ five columns using trailing amount in the description cell and TD-specific debit/credit
120
+ routing. Fires on \"continued\" in-table text and/or TD body cues (TD ZELLE, LENDINGCLUB,
121
+ etc.). Safe no-op elsewhere.
122
  """
123
 
124
  # Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
 
2873
  return grid
2874
  return out
2875
 
2876
+
2877
+ _TD_CONTINUED_BLOB_RE = re.compile(
2878
+ r"transactions\s+by\s+date.*continued|continued.*transactions\s+by\s+date",
2879
+ re.IGNORECASE | re.DOTALL,
2880
+ )
2881
+
2882
+ # GLM often omits the continued banner inside a standalone <table>; match TD register body text.
2883
+ _TD_REGISTER_BODY_SIGNAL_RE = re.compile(
2884
+ r"TD\s+ZELLE|\bZELLE\b.*\b(?:RECEIVED|SENT)\b|LENDINGCLUB|MM\s*&\s*(?:amp;)?\s*CB|"
2885
+ r"MAINTENANCE\s+FEE|OD\s+GRACE\s+FEE\s+REFUND|OVERDRAFT\s+PD",
2886
+ re.IGNORECASE,
2887
+ )
2888
+
2889
+
2890
+ def _is_td_four_col_ddcb_header(cells: List[str]) -> bool:
2891
+ """TD continuation: one fused DATE DESCRIPTION cell + DEBIT + CREDIT + BALANCE."""
2892
+ if len(cells) < 4:
2893
+ return False
2894
+ a = re.sub(r"\s+", " ", str(cells[0] or "").strip().lower())
2895
+ b = re.sub(r"\s+", " ", str(cells[1] or "").strip().lower())
2896
+ c = re.sub(r"\s+", " ", str(cells[2] or "").strip().lower())
2897
+ d = re.sub(r"\s+", " ", str(cells[3] or "").strip().lower())
2898
+ fused = "date" in a and "description" in a
2899
+ deb = b == "debit" or b.startswith("debit")
2900
+ cre = "credit" in c and "debit" not in c
2901
+ bal = "balance" in d
2902
+ return fused and deb and cre and bal
2903
+
2904
+
2905
+ def _split_td_continuation_date_description_grid(grid):
2906
+ """
2907
+ Fix TD-1: TD Convenience Checking continuation pages use four physical columns; split col0
2908
+ into Date + Description so json_data_extractor's five-way column map aligns with row cells.
2909
+ """
2910
+ if not grid or len(grid) < 3:
2911
+ return grid
2912
+
2913
+ blob = " ".join(
2914
+ " ".join(str(c or "") for c in row) for row in grid[: min(30, len(grid))]
2915
+ )
2916
+ if not _TD_CONTINUED_BLOB_RE.search(blob):
2917
+ return grid
2918
+
2919
+ hdr_idx = None
2920
+ for ri in range(min(20, len(grid))):
2921
+ raw = [str(c or "").strip() for c in grid[ri]]
2922
+ while len(raw) < 4:
2923
+ raw.append("")
2924
+ if len(raw) >= 4 and _is_td_four_col_ddcb_header(raw[:4]):
2925
+ hdr_idx = ri
2926
+ break
2927
+ if hdr_idx is None:
2928
+ return grid
2929
+
2930
+ date_led_re = re.compile(r"^(\d{1,2}/\d{1,2}(?:/\d{2,4})?)\s+(.*)$", re.DOTALL)
2931
+ new_grid: List[List[str]] = []
2932
+ for ri, row in enumerate(grid):
2933
+ cells = [str(c or "").strip() for c in row]
2934
+ if ri < hdr_idx:
2935
+ new_grid.append(cells)
2936
+ continue
2937
+ if ri == hdr_idx:
2938
+ c = cells[:4] + [""] * max(0, 4 - len(cells))
2939
+ new_grid.append(
2940
+ [
2941
+ "DATE",
2942
+ "DESCRIPTION",
2943
+ c[1] or "DEBIT",
2944
+ c[2] or "CREDIT",
2945
+ c[3] or "BALANCE",
2946
+ ]
2947
+ )
2948
+ continue
2949
+ while len(cells) < 4:
2950
+ cells.append("")
2951
+ if len(cells) == 4:
2952
+ m = date_led_re.match(cells[0].strip())
2953
+ if m:
2954
+ new_grid.append(
2955
+ [
2956
+ m.group(1).strip(),
2957
+ (m.group(2) or "").strip(),
2958
+ cells[1],
2959
+ cells[2],
2960
+ cells[3],
2961
+ ]
2962
+ )
2963
+ else:
2964
+ new_grid.append([cells[0], "", cells[1], cells[2], cells[3]])
2965
+ elif len(cells) >= 5:
2966
+ new_grid.append(cells)
2967
+ else:
2968
+ while len(cells) < 5:
2969
+ cells.append("")
2970
+ new_grid.append(cells[:5])
2971
+
2972
+ return new_grid
2973
+
2974
+
2975
+ def _is_td_three_col_register_header(cells: List[str]) -> bool:
2976
+ """OCR variant: Date | Transaction Description | Amount (Amount column is running balance)."""
2977
+ if len(cells) < 3:
2978
+ return False
2979
+ a = re.sub(r"\s+", " ", str(cells[0] or "").strip().lower())
2980
+ b = re.sub(r"\s+", " ", str(cells[1] or "").strip().lower())
2981
+ c = re.sub(r"\s+", " ", str(cells[2] or "").strip().lower())
2982
+ if a != "date":
2983
+ return False
2984
+ if "transaction" not in b or "description" not in b:
2985
+ return False
2986
+ if c not in ("amount", "balance") and "amount" not in c and "balance" not in c:
2987
+ return False
2988
+ return True
2989
+
2990
+
2991
+ def _td_trailing_amount_from_description(desc: str) -> Tuple[str, str]:
2992
+ """Return (description_without_trailing_amount, amount_str or '')."""
2993
+ if not desc or not str(desc).strip():
2994
+ return "", ""
2995
+ s = str(desc).strip()
2996
+ m = re.search(r"(?:,\s*|\s)(-?\d{1,3}(?:,\d{3})*\.\d{2})\s*$", s)
2997
+ if not m:
2998
+ return s, ""
2999
+ amt = m.group(1).strip()
3000
+ left = s[: m.start()].strip().rstrip(",")
3001
+ return left, amt
3002
+
3003
+
3004
+ def _td_route_debit_credit(desc_raw: str, amt_str: str) -> Tuple[str, str]:
3005
+ """Return (debit_cell, credit_cell) with positive amount string in one side."""
3006
+ if not amt_str:
3007
+ return "", ""
3008
+ u = str(desc_raw or "").upper()
3009
+ if (
3010
+ "ACH DEBIT" in u
3011
+ or "OVERDRAFT" in u
3012
+ or "ZELLE SENT" in u
3013
+ or "MAINTENANCE FEE" in u
3014
+ or "ATM DB" in u
3015
+ or "ATM DEBIT" in u
3016
+ or "ELECTRONIC PMT" in u
3017
+ or "PMT-WEB" in u
3018
+ ):
3019
+ return amt_str, ""
3020
+ if (
3021
+ "ZELLE RECEIVED" in u
3022
+ or "OD GRACE FEE REFUND" in u
3023
+ or u.startswith("CREDIT,")
3024
+ or u.startswith("DEPOSIT")
3025
+ ):
3026
+ return "", amt_str
3027
+ if "DEBIT" in u and "CREDIT," not in u[:12]:
3028
+ return amt_str, ""
3029
+ return "", amt_str
3030
+
3031
+
3032
+ def _expand_td_continuation_three_col_register_grid(grid):
3033
+ """
3034
+ Fix TD-1b: GLM emits Date|Transaction Description|Amount where col3 is balance; expand to
3035
+ DATE|DESCRIPTION|DEBIT|CREDIT|BALANCE for json_data_extractor.
3036
+ """
3037
+ if not grid or len(grid) < 2:
3038
+ return grid
3039
+
3040
+ blob = " ".join(
3041
+ " ".join(str(c or "") for c in row) for row in grid[: min(40, len(grid))]
3042
+ )
3043
+ if not (
3044
+ _TD_CONTINUED_BLOB_RE.search(blob)
3045
+ or _TD_REGISTER_BODY_SIGNAL_RE.search(blob)
3046
+ ):
3047
+ return grid
3048
+
3049
+ hdr_idx = None
3050
+ for ri in range(min(15, len(grid))):
3051
+ raw = [str(c or "").strip() for c in grid[ri]]
3052
+ while len(raw) < 3:
3053
+ raw.append("")
3054
+ if _is_td_three_col_register_header(raw[:3]):
3055
+ hdr_idx = ri
3056
+ break
3057
+ if hdr_idx is None:
3058
+ return grid
3059
+
3060
+ date_re = re.compile(r"^\d{1,2}/\d{1,2}(?:/\d{2,4})?$")
3061
+ new_grid: List[List[str]] = []
3062
+ for ri, row in enumerate(grid):
3063
+ cells = [str(c or "").strip() for c in row]
3064
+ if ri < hdr_idx:
3065
+ new_grid.append(cells)
3066
+ continue
3067
+ if ri == hdr_idx:
3068
+ new_grid.append(["DATE", "DESCRIPTION", "DEBIT", "CREDIT", "BALANCE"])
3069
+ continue
3070
+ while len(cells) < 3:
3071
+ cells.append("")
3072
+ if len(cells) != 3:
3073
+ new_grid.append(cells)
3074
+ continue
3075
+ d0, d1, bal = cells[0], cells[1], cells[2]
3076
+ if not date_re.match(d0):
3077
+ new_grid.append([d0, d1, "", "", bal])
3078
+ continue
3079
+ desc_only, amt_s = _td_trailing_amount_from_description(d1)
3080
+ db, cr = _td_route_debit_credit(d1, amt_s)
3081
+ new_grid.append([d0, desc_only, db, cr, bal])
3082
+
3083
+ return new_grid
3084
+
3085
+
3086
  def normalize_html_tables(text: str) -> str:
3087
  if not text or "<table" not in text.lower():
3088
  return text
 
3124
 
3125
  grid = _split_fused_date_transaction_header(grid)
3126
 
3127
+ grid = _split_td_continuation_date_description_grid(grid)
3128
+ grid = _expand_td_continuation_three_col_register_grid(grid)
3129
+
3130
  grid = _normalize_fused_date_posted_amount_table(grid)
3131
 
3132
  grid = _split_fused_multicolumn_header(grid)