rehan953 commited on
Commit
4dcc6ed
·
verified ·
1 Parent(s): 853f781

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +118 -8
app.py CHANGED
@@ -36,6 +36,9 @@ Primary goals for reconcile rate:
36
  debit pattern) and at least one ITM DEPOSIT row exists, OCR stacked headings wrong — emit
37
  Electronic Credits, then the table, then Electronic Debits (section continues below). Safe no-op
38
  if the Debits heading was not before the table or any row looks like a card/ATM debit.
 
 
 
39
  - Post-pass: dedupe 3-col Date|Description|Amount tables when one is a duplicate fragment
40
  (later subset of earlier, earlier subset of later, or identical row sets)
41
  - Fix 18: adjacent DA3 tables — if the last K data rows of table N match the first K rows of table N+1
@@ -2952,6 +2955,23 @@ def _ucb_table_credits_only_no_strong_debits(grid) -> bool:
2952
  return bool(has_data and _ucb_has_itm_deposit_data_row(grid))
2953
 
2954
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2955
  def _ucb_split_grid_electronic_credits_debits_after_itm_prefix(grid):
2956
  """
2957
  Fix 17b: one fused DA3 table under Electronic Credits (+ optional empty EC/ED headings) where OCR
@@ -3006,10 +3026,7 @@ def _try_split_ucb_ec_ed_from_fused_table_html(
3006
 
3007
  # Fix 17c: headings stacked as EC / ED / table but rows are all credits (e.g. ITM-only) —
3008
  # put the table under Electronic Credits, then leave Electronic Debits for continuation.
3009
- if (
3010
- had_ed_heading_immediately_before_table
3011
- and _ucb_table_credits_only_no_strong_debits(grid)
3012
- ):
3013
  sect = '<div align="center">\n\n{title}\n\n</div>\n\n'
3014
  parts = []
3015
  if lead_before_ec and lead_before_ec.strip():
@@ -3023,20 +3040,112 @@ def _try_split_ucb_ec_ed_from_fused_table_html(
3023
  )
3024
  return "\n\n".join(parts)
3025
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3026
  return None
3027
 
3028
 
 
 
3029
  # Optional empty centered banner only; EC/ED title divs are re-emitted after a successful split
3030
  # so we do not duplicate headings.
3031
  _UCB_EC_ED_SINGLE_FUSED_TABLE = re.compile(
3032
- r'(?P<lead>(?:<div\s+align=["\']center["\']\s*>\s*</div>\s*)?)'
3033
- r'<div\s+align=["\']center["\']\s*>\s*Electronic\s+Credits\s*</div>\s*'
3034
- r'(?P<ed_before>(?:<div\s+align=["\']center["\']\s*>\s*Electronic\s+Debits\s*</div>\s*)?)'
3035
  r'(?P<table><table[^>]*>.*?</table>)',
3036
  re.DOTALL | re.IGNORECASE,
3037
  )
3038
 
3039
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3040
  def _split_ucb_fused_ec_ed_headings_single_table_html(text: str) -> str:
3041
  if not text or "electronic credits" not in text.lower():
3042
  return text
@@ -3095,7 +3204,8 @@ def _split_ucb_deposits_electronic_sections_html(text: str) -> str:
3095
  return new
3096
 
3097
  text = _UCB_DEP_CONT_BLOCK.sub(_repl, text)
3098
- return _split_ucb_fused_ec_ed_headings_single_table_html(text)
 
3099
 
3100
 
3101
  def _parse_da3_grid_from_table_html(table_html: str):
 
36
  debit pattern) and at least one ITM DEPOSIT row exists, OCR stacked headings wrong — emit
37
  Electronic Credits, then the table, then Electronic Debits (section continues below). Safe no-op
38
  if the Debits heading was not before the table or any row looks like a card/ATM debit.
39
+ - Fix 17c-html: Same as 17c using raw HTML cues when TableGridParser/header normalization would skip the table.
40
+ - Fix 17d: Drop a second Electronic Debits block + DA3 table when its rows are a loose (date+amount)
41
+ multiset subset of the immediately preceding DA3 table (OCR duplicate tail / wrong split).
42
  - Post-pass: dedupe 3-col Date|Description|Amount tables when one is a duplicate fragment
43
  (later subset of earlier, earlier subset of later, or identical row sets)
44
  - Fix 18: adjacent DA3 tables — if the last K data rows of table N match the first K rows of table N+1
 
2955
  return bool(has_data and _ucb_has_itm_deposit_data_row(grid))
2956
 
2957
 
2958
+ def _ucb_html_table_body_looks_itm_credits_only(table_html: str) -> bool:
2959
+ """
2960
+ Detect ITM-only credit block without relying on parsed grid headers (Fix 17c-html).
2961
+ Conservative: require ITM deposit text and absence of common electronic-debit verbs in the HTML.
2962
+ """
2963
+ if not table_html or "<table" not in table_html.lower():
2964
+ return False
2965
+ low = table_html.lower()
2966
+ if "itm deposit" not in low and "itm dep" not in low:
2967
+ return False
2968
+ if "pos purchase" in low or "atm withdrawal" in low:
2969
+ return False
2970
+ if "recur payment" in low or "debit card" in low:
2971
+ return False
2972
+ return True
2973
+
2974
+
2975
  def _ucb_split_grid_electronic_credits_debits_after_itm_prefix(grid):
2976
  """
2977
  Fix 17b: one fused DA3 table under Electronic Credits (+ optional empty EC/ED headings) where OCR
 
3026
 
3027
  # Fix 17c: headings stacked as EC / ED / table but rows are all credits (e.g. ITM-only) —
3028
  # put the table under Electronic Credits, then leave Electronic Debits for continuation.
3029
+ if had_ed_heading_immediately_before_table and _ucb_table_credits_only_no_strong_debits(grid):
 
 
 
3030
  sect = '<div align="center">\n\n{title}\n\n</div>\n\n'
3031
  parts = []
3032
  if lead_before_ec and lead_before_ec.strip():
 
3040
  )
3041
  return "\n\n".join(parts)
3042
 
3043
+ # Fix 17c-html: same reorder when grid header checks fail but raw HTML is clearly ITM-only credits.
3044
+ if had_ed_heading_immediately_before_table and _ucb_html_table_body_looks_itm_credits_only(table_html):
3045
+ sect = '<div align="center">\n\n{title}\n\n</div>\n\n'
3046
+ parts = []
3047
+ if lead_before_ec and lead_before_ec.strip():
3048
+ parts.append(lead_before_ec.rstrip())
3049
+ parts.extend(
3050
+ [
3051
+ sect.format(title="Electronic Credits"),
3052
+ table_html.strip(),
3053
+ sect.format(title="Electronic Debits"),
3054
+ ]
3055
+ )
3056
+ return "\n\n".join(parts)
3057
+
3058
  return None
3059
 
3060
 
3061
+ _UCB_CENTER_DIV = r'<div[^>]*\balign\s*=\s*["\']center["\'][^>]*>'
3062
+
3063
  # Optional empty centered banner only; EC/ED title divs are re-emitted after a successful split
3064
  # so we do not duplicate headings.
3065
  _UCB_EC_ED_SINGLE_FUSED_TABLE = re.compile(
3066
+ r'(?P<lead>(?:' + _UCB_CENTER_DIV + r'\s*</div>\s*)?)'
3067
+ r'(?P<ecdiv>' + _UCB_CENTER_DIV + r'\s*Electronic\s+Credits\s*</div>\s*)'
3068
+ r'(?P<ed_before>(?:' + _UCB_CENTER_DIV + r'\s*Electronic\s+Debits\s*</div>\s*)?)'
3069
  r'(?P<table><table[^>]*>.*?</table>)',
3070
  re.DOTALL | re.IGNORECASE,
3071
  )
3072
 
3073
 
3074
+ def _ucb_strip_redundant_ed_subset_fragment(text: str) -> str:
3075
+ """
3076
+ Fix 17d: Remove a duplicate <div>Electronic Debits</div> + <table> when that table's rows are a
3077
+ loose (date, amount) multiset subset of the DA3 table that appears immediately before this block.
3078
+ """
3079
+ if not text or "electronic debits" not in text.lower():
3080
+ return text
3081
+ try:
3082
+ ed_block = re.compile(
3083
+ _UCB_CENTER_DIV + r'\s*Electronic\s+Debits\s*</div>\s*'
3084
+ r'(?P<tbl><table[^>]*>.*?</table>)',
3085
+ re.DOTALL | re.IGNORECASE,
3086
+ )
3087
+ tbl_pat = re.compile(r'<table[^>]*>.*?</table>', re.DOTALL | re.IGNORECASE)
3088
+ _DATE_NONEMPTY = re.compile(r"^\d{1,2}[/\-]\d{2}")
3089
+ # Stripped amounts may be "2194.19" (no comma) — must not use comma-group regex.
3090
+ _AMT_STRIPPED_OK = re.compile(r"^-?\d+(?:\.\d{1,4})?-?$")
3091
+
3092
+ def _loose_da_key_cells(cells):
3093
+ parts = [str(c or "").strip() for c in cells[:3]]
3094
+ while len(parts) < 3:
3095
+ parts.append("")
3096
+ date_s = re.sub(r"\s+", "", parts[0])
3097
+ amt_s = re.sub(r"[\s$,]", "", parts[2]).lower()
3098
+ amt_s = amt_s.replace("\u2212", "-").replace("\u2013", "-")
3099
+ return (date_s, amt_s)
3100
+
3101
+ def _loose_keys_from_grid(g):
3102
+ if not g or not _is_da3_header(g):
3103
+ return None
3104
+ keys = []
3105
+ for row in g[1:]:
3106
+ cells = [str(c or "").strip() for c in row]
3107
+ if not any(cells):
3108
+ continue
3109
+ k = _loose_da_key_cells(cells)
3110
+ if (
3111
+ _DATE_NONEMPTY.match(k[0])
3112
+ and _AMT_STRIPPED_OK.match(k[1])
3113
+ and "." in k[1]
3114
+ ):
3115
+ keys.append(k)
3116
+ return keys
3117
+
3118
+ for _ in range(12):
3119
+ changed = False
3120
+ for m in ed_block.finditer(text):
3121
+ before = text[: m.start()]
3122
+ prev_tbl_m = None
3123
+ for tm in tbl_pat.finditer(before):
3124
+ prev_tbl_m = tm
3125
+ if prev_tbl_m is None:
3126
+ continue
3127
+ g_big = _parse_da3_grid_from_table_html(prev_tbl_m.group(0))
3128
+ g_frag = _parse_da3_grid_from_table_html(m.group("tbl"))
3129
+ kb = _loose_keys_from_grid(g_big)
3130
+ kf = _loose_keys_from_grid(g_frag)
3131
+ if not kb or not kf:
3132
+ continue
3133
+ if len(kf) > 24 or len(kf) >= len(kb):
3134
+ continue
3135
+ cb, cf = Counter(kb), Counter(kf)
3136
+ if not all(cf[k] <= cb.get(k, 0) for k in cf):
3137
+ continue
3138
+ text = text[: m.start()] + text[m.end() :]
3139
+ changed = True
3140
+ break
3141
+ if not changed:
3142
+ break
3143
+ return text
3144
+ except Exception as e:
3145
+ log.warning("_ucb_strip_redundant_ed_subset_fragment failed: %s", e)
3146
+ return text
3147
+
3148
+
3149
  def _split_ucb_fused_ec_ed_headings_single_table_html(text: str) -> str:
3150
  if not text or "electronic credits" not in text.lower():
3151
  return text
 
3204
  return new
3205
 
3206
  text = _UCB_DEP_CONT_BLOCK.sub(_repl, text)
3207
+ text = _split_ucb_fused_ec_ed_headings_single_table_html(text)
3208
+ return _ucb_strip_redundant_ed_subset_fragment(text)
3209
 
3210
 
3211
  def _parse_da3_grid_from_table_html(table_html: str):