Make template parser primary and automate bank detection

This commit is contained in:
A R R R Associates
2026-08-04 12:49:58 +05:30
parent d40f68f51a
commit 565425c914
2 changed files with 44 additions and 58 deletions
@@ -15,7 +15,7 @@ from .city_union_bank import CityUnionBankParser
from .bank_of_baroda import BankOfBarodaParser from .bank_of_baroda import BankOfBarodaParser
from .rbl_bank import RBLBankParser from .rbl_bank import RBLBankParser
from .base import extract_text from .base import extract_text
from .template_engine import TemplateBasedStatementParser, parse_with_template from .template_engine import parse_with_template
PARSERS = [ PARSERS = [
IDFCFirstParser, IDFCFirstParser,
@@ -89,71 +89,50 @@ def _selected_parser(bank_key: str, text: str):
return None, 0 return None, 0
def _valid_result(result) -> bool: def _template_first(path, text: str, hint: str):
if not result or len(result) != 2: """Try the validated layout engine before bank-specific parsing.
return False
_meta, frame = result Any template error is deliberately swallowed here so the existing bank
return frame is not None and not frame.empty parser path remains an unchanged and reliable fallback.
"""
try:
return parse_with_template(path, text=text, bank_hint=hint)
except Exception:
return None
def parse_pdf(path, bank_hint: str | None = None): def parse_pdf(path, bank_hint: str | None = None):
"""Parse a bank statement without removing any existing parser behaviour.
Existing bank-specific parsers remain first priority. The template engine
is an additive fallback when a bank parser does not recognise a new layout
or returns no transactions. A template result is accepted only after
transaction-level running-balance validation.
"""
hint = (bank_hint or "auto").strip().lower() hint = (bank_hint or "auto").strip().lower()
text = extract_text(path) text = extract_text(path)
primary_error: Exception | None = None
template_result = _template_first(path, text, hint)
if template_result is not None:
return template_result
# Preserve the existing HSBC OCR route for image-only statements and for
# users/API clients that explicitly provide an HSBC override.
if hint == "hsbc":
return HSBCParser().parse(path, text)
if hint and hint != "auto": if hint and hint != "auto":
# Preserve HSBC's specialised OCR parser as the first route. parser, score = _selected_parser(hint, text)
candidates = BANK_PARSERS.get(hint, []) if parser is None:
scored = sorted(
((parser.detect(text), parser) for parser in candidates),
key=lambda item: item[0],
reverse=True,
)
for score, parser_class in scored:
if score <= 0:
continue
try:
result = parser_class().parse(path, text)
if _valid_result(result):
return result
except (ValueError, RuntimeError) as exc:
primary_error = exc
# New layouts from a known bank are routed by table structure rather
# than rejected merely because the bank-specific detector changed.
try:
return parse_with_template(path, text, hint)
except (ValueError, RuntimeError) as template_error:
label = dict(BANK_OPTIONS).get(hint, "the selected bank") label = dict(BANK_OPTIONS).get(hint, "the selected bank")
detail = str(primary_error or template_error)
raise ValueError( raise ValueError(
f"The uploaded statement could not be reliably parsed as {label}. " f"The uploaded statement could not be parsed by a validated layout template and does not match "
f"{detail}" f"the selected {label} format. Please verify the statement or use automatic detection."
) from template_error )
return parser.parse(path, text)
parser, _score = detect_parser(text) parser, score = detect_parser(text)
if parser is not None: if parser is None and not (text or "").strip():
try: try:
result = parser.parse(path, text) return HSBCParser().parse(path, text)
if _valid_result(result): except ValueError:
return result pass
except (ValueError, RuntimeError) as exc: if parser is None:
primary_error = exc
# Auto Detect now falls back to structural templates. Bank detection remains
# a hint for metadata and naming, never a hard gate for transaction parsing.
try:
return parse_with_template(path, text, "auto")
except (ValueError, RuntimeError) as template_error:
detail = str(primary_error or template_error)
raise ValueError( raise ValueError(
"Unsupported or unreconciled statement format. " "Unsupported statement format. The validated template engine and all available bank-specific parsers "
f"{detail}" "were unable to reconcile this statement."
) from template_error )
return parser.parse(path, text)
@@ -13,7 +13,14 @@
<form action="/tools/bank-statement-analyzer/analyze" method="post" enctype="multipart/form-data" class="rounded-2xl border border-slate-200 bg-white p-6 shadow-soft"> <form action="/tools/bank-statement-analyzer/analyze" method="post" enctype="multipart/form-data" class="rounded-2xl border border-slate-200 bg-white p-6 shadow-soft">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="grid gap-5 md:grid-cols-2"> <div class="grid gap-5 md:grid-cols-2">
<div><label class="mb-1 block text-sm font-semibold text-slate-700">Bank</label><select name="bank_selection" class="w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm">{% for value, label in bank_options %}<option value="{{ value }}" {% if selected_bank == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select><p class="mt-1 text-xs text-slate-500">Keep Auto Detect or select a bank for direct parser validation.</p></div> <div>
<input type="hidden" name="bank_selection" value="auto">
<label class="mb-1 block text-sm font-semibold text-slate-700">Bank detection</label>
<div class="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
<div class="text-sm font-semibold text-slate-800">Automatic layout detection</div>
<p class="mt-1 text-xs text-slate-500">The validated template engine runs first. Existing bank-specific parsers are used automatically when the layout template cannot be reconciled.</p>
</div>
</div>
<div><label class="mb-1 block text-sm font-semibold text-slate-700">Financial year <span class="font-normal text-slate-400">(optional)</span></label><input name="financial_year" value="{{ financial_year or '' }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" placeholder="Example: 2025-26"></div> <div><label class="mb-1 block text-sm font-semibold text-slate-700">Financial year <span class="font-normal text-slate-400">(optional)</span></label><input name="financial_year" value="{{ financial_year or '' }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" placeholder="Example: 2025-26"></div>
<div><label class="mb-1 block text-sm font-semibold text-slate-700">Account holder override <span class="font-normal text-slate-400">(optional)</span></label><input name="customer_name" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" placeholder="Use only when statement extraction needs correction"></div> <div><label class="mb-1 block text-sm font-semibold text-slate-700">Account holder override <span class="font-normal text-slate-400">(optional)</span></label><input name="customer_name" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" placeholder="Use only when statement extraction needs correction"></div>
<div><label class="mb-1 block text-sm font-semibold text-slate-700">Account number override <span class="font-normal text-slate-400">(optional)</span></label><input name="account_number" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" placeholder="Use only when statement extraction needs correction"></div> <div><label class="mb-1 block text-sm font-semibold text-slate-700">Account number override <span class="font-normal text-slate-400">(optional)</span></label><input name="account_number" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" placeholder="Use only when statement extraction needs correction"></div>