Make template parser primary and automate bank detection
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user