rehan953 commited on
Commit
e5c70c7
·
verified ·
1 Parent(s): e8fb17b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +127 -0
app.py CHANGED
@@ -1035,6 +1035,132 @@ def _extract_rows_plain_from_table(full_table: str) -> List[List[str]]:
1035
  return rows
1036
 
1037
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1038
  def _extract_daily_balance_by_date(md: str):
1039
  """
1040
  Build a date->balance map from daily-balance style tables.
@@ -1545,6 +1671,7 @@ def run_ocr(uploaded_file):
1545
  if merged and merged != "(No content)" and not merged.lstrip().startswith("Error:"):
1546
  merged = mask_non_monetary_long_numbers(merged)
1547
  merged = directionalize_amount_headers(merged)
 
1548
  merged = stabilize_table_markup(merged, rounds=4)
1549
  merged = enrich_transaction_tables_with_daily_balances(merged)
1550
  # Keep transaction amounts untouched; add explicit snapshot for metadata extraction.
 
1035
  return rows
1036
 
1037
 
1038
+ def _fmt_money(v: float) -> str:
1039
+ return f"{v:,.2f}"
1040
+
1041
+
1042
+ def _is_date_like(s: str) -> bool:
1043
+ t = (s or "").strip()
1044
+ return bool(re.match(r"^(?:\d{1,2}[/-]\d{1,2}(?:[/-]\d{2,4})?)$", t))
1045
+
1046
+
1047
+ def normalize_ambiguous_amount_balance_tables(md: str) -> str:
1048
+ """
1049
+ For transaction-like tables that expose only a single Amount column plus
1050
+ Balance, convert to explicit Credit/Debit using generic running-balance
1051
+ inference. This avoids sign ambiguity and improves reconcile stability.
1052
+ """
1053
+ if not md or "<table" not in md.lower():
1054
+ return md
1055
+
1056
+ credit_hint_re = re.compile(
1057
+ r"\b(deposit|deposits|credit|credits|incoming|received|interest earned|refund|return)\b",
1058
+ re.IGNORECASE,
1059
+ )
1060
+ debit_hint_re = re.compile(
1061
+ r"\b(debit|debits|withdrawal|withdrawals|check|checks|fee|fees|charge|charges|payment|transfer out|purchase)\b",
1062
+ re.IGNORECASE,
1063
+ )
1064
+
1065
+ def repl_table(m: re.Match) -> str:
1066
+ full = m.group(0)
1067
+ rows = _extract_rows_plain_from_table(full)
1068
+ if len(rows) < 3:
1069
+ return full
1070
+
1071
+ hdr = [c.strip().lower() for c in rows[0]]
1072
+ amount_idxs = [i for i, c in enumerate(hdr) if "amount" in c]
1073
+ balance_idxs = [i for i, c in enumerate(hdr) if "balance" in c]
1074
+ has_credit = any("credit" in c for c in hdr)
1075
+ has_debit = any("debit" in c for c in hdr)
1076
+ date_idxs = [i for i, c in enumerate(hdr) if "date" in c]
1077
+ desc_idxs = [i for i, c in enumerate(hdr) if "description" in c or "memo" in c or "details" in c]
1078
+
1079
+ if has_credit or has_debit or not amount_idxs or not balance_idxs:
1080
+ return full
1081
+
1082
+ amount_idx = amount_idxs[0]
1083
+ balance_idx = balance_idxs[0]
1084
+ date_idx = date_idxs[0] if date_idxs else 0
1085
+ desc_idx = desc_idxs[0] if desc_idxs else min(1, max(0, len(rows[0]) - 1))
1086
+
1087
+ new_rows: List[List[str]] = [["Date", "Description", "Credit", "Debit", "Balance"]]
1088
+ prev_bal = None
1089
+ section_hint = ""
1090
+ converted = 0
1091
+
1092
+ for r in rows[1:]:
1093
+ if not r:
1094
+ continue
1095
+ padded = r + [""] * (max(amount_idx, balance_idx, date_idx, desc_idx) + 1 - len(r))
1096
+ date_txt = (padded[date_idx] or "").strip()
1097
+ desc_txt = (padded[desc_idx] or "").strip()
1098
+ amt_txt = (padded[amount_idx] or "").strip()
1099
+ bal_txt = (padded[balance_idx] or "").strip()
1100
+ amt = _parse_amount_or_none(amt_txt)
1101
+ bal = _parse_amount_or_none(bal_txt)
1102
+
1103
+ row_blob = " ".join((c or "").strip() for c in padded).strip()
1104
+ if row_blob and not _is_date_like(date_txt) and amt is None:
1105
+ section_hint = row_blob
1106
+ continue
1107
+ if amt is None and bal is None:
1108
+ continue
1109
+
1110
+ direction = ""
1111
+ # Primary: infer from balance delta between consecutive rows.
1112
+ if amt is not None and bal is not None and prev_bal is not None:
1113
+ delta = bal - prev_bal
1114
+ tol = max(0.55, 0.025 * abs(amt))
1115
+ if abs(abs(delta) - abs(amt)) <= tol:
1116
+ direction = "credit" if delta > 0 else "debit"
1117
+
1118
+ # Secondary: section + description lexical hints.
1119
+ ctx = f"{section_hint} {desc_txt}"
1120
+ if not direction and credit_hint_re.search(ctx):
1121
+ direction = "credit"
1122
+ if not direction and debit_hint_re.search(ctx):
1123
+ direction = "debit"
1124
+
1125
+ credit = ""
1126
+ debit = ""
1127
+ if amt is not None:
1128
+ if direction == "credit":
1129
+ credit = _fmt_money(abs(amt))
1130
+ elif direction == "debit":
1131
+ debit = _fmt_money(abs(amt))
1132
+ else:
1133
+ # Conservative fallback for ambiguous rows: preserve as debit
1134
+ # to avoid overstating inflows.
1135
+ debit = _fmt_money(abs(amt))
1136
+
1137
+ balance_out = _fmt_money(bal) if bal is not None else ""
1138
+ new_rows.append([date_txt, desc_txt, credit, debit, balance_out])
1139
+ if bal is not None:
1140
+ prev_bal = bal
1141
+ converted += 1
1142
+
1143
+ if converted < 5:
1144
+ return full
1145
+
1146
+ html_rows = []
1147
+ html_rows.append(
1148
+ "<tr>" + "".join(f"<th>{html.escape(c)}</th>" for c in new_rows[0]) + "</tr>"
1149
+ )
1150
+ for rr in new_rows[1:]:
1151
+ html_rows.append(
1152
+ "<tr>" + "".join(f"<td>{html.escape(c)}</td>" for c in rr) + "</tr>"
1153
+ )
1154
+ return "<table>\n" + "\n".join(html_rows) + "\n</table>"
1155
+
1156
+ return re.sub(
1157
+ r"<table\b[^>]*>.*?</table>",
1158
+ repl_table,
1159
+ md,
1160
+ flags=re.IGNORECASE | re.DOTALL,
1161
+ )
1162
+
1163
+
1164
  def _extract_daily_balance_by_date(md: str):
1165
  """
1166
  Build a date->balance map from daily-balance style tables.
 
1671
  if merged and merged != "(No content)" and not merged.lstrip().startswith("Error:"):
1672
  merged = mask_non_monetary_long_numbers(merged)
1673
  merged = directionalize_amount_headers(merged)
1674
+ merged = normalize_ambiguous_amount_balance_tables(merged)
1675
  merged = stabilize_table_markup(merged, rounds=4)
1676
  merged = enrich_transaction_tables_with_daily_balances(merged)
1677
  # Keep transaction amounts untouched; add explicit snapshot for metadata extraction.