Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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>(?:
|
| 3033 |
-
r'<
|
| 3034 |
-
r'(?P<ed_before>(?:
|
| 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 |
-
|
|
|
|
| 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):
|