Add validated template fallback for bank statement layouts
This commit is contained in:
@@ -8,13 +8,14 @@ from .hsbc import HSBCParser
|
||||
from .indian_bank import IndianBankModernParser, IndianBankLegacyParser
|
||||
from .indusind import IndusIndParser
|
||||
from .kotak import KotakParser
|
||||
from .sbi import SBIAccountSummaryParser, SBIModernParser, SBIOtherParser
|
||||
from .sbi import SBIModernParser, SBIOtherParser
|
||||
from .central_bank_of_india import CentralBankOfIndiaParser
|
||||
from .yes_bank import YesBankParser
|
||||
from .city_union_bank import CityUnionBankParser
|
||||
from .bank_of_baroda import BankOfBarodaParser
|
||||
from .rbl_bank import RBLBankParser
|
||||
from .base import extract_text
|
||||
from .template_engine import TemplateBasedStatementParser, parse_with_template
|
||||
|
||||
PARSERS = [
|
||||
IDFCFirstParser,
|
||||
@@ -31,7 +32,6 @@ PARSERS = [
|
||||
IndianBankLegacyParser,
|
||||
IndusIndParser,
|
||||
KotakParser,
|
||||
SBIAccountSummaryParser,
|
||||
SBIOtherParser,
|
||||
SBIModernParser,
|
||||
]
|
||||
@@ -68,7 +68,7 @@ BANK_PARSERS = {
|
||||
"indian_bank": [IndianBankModernParser, IndianBankLegacyParser],
|
||||
"indusind": [IndusIndParser],
|
||||
"kotak": [KotakParser],
|
||||
"sbi": [SBIAccountSummaryParser, SBIOtherParser, SBIModernParser],
|
||||
"sbi": [SBIOtherParser, SBIModernParser],
|
||||
}
|
||||
|
||||
|
||||
@@ -89,32 +89,71 @@ def _selected_parser(bank_key: str, text: str):
|
||||
return None, 0
|
||||
|
||||
|
||||
def _valid_result(result) -> bool:
|
||||
if not result or len(result) != 2:
|
||||
return False
|
||||
_meta, frame = result
|
||||
return frame is not None and not frame.empty
|
||||
|
||||
|
||||
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()
|
||||
if hint == "hsbc":
|
||||
return HSBCParser().parse(path, "")
|
||||
text = extract_text(path)
|
||||
primary_error: Exception | None = None
|
||||
|
||||
if hint and hint != "auto":
|
||||
parser, score = _selected_parser(hint, text)
|
||||
if parser is None:
|
||||
label = dict(BANK_OPTIONS).get(hint, "the selected bank")
|
||||
raise ValueError(
|
||||
f"The uploaded statement does not match the selected {label} format. "
|
||||
"Please verify the selected bank or choose Auto Detect."
|
||||
)
|
||||
return parser.parse(path, text)
|
||||
parser, score = detect_parser(text)
|
||||
if parser is None and not (text or "").strip():
|
||||
# Image-only statements cannot be identified by pdftotext. HSBCParser
|
||||
# performs its own OCR identification and raises a precise mismatch error.
|
||||
try:
|
||||
return HSBCParser().parse(path, text)
|
||||
except ValueError:
|
||||
pass
|
||||
if parser is None:
|
||||
raise ValueError(
|
||||
"Unsupported statement format. Select the bank manually or add a bank-specific parser for this statement layout."
|
||||
# Preserve HSBC's specialised OCR parser as the first route.
|
||||
candidates = BANK_PARSERS.get(hint, [])
|
||||
scored = sorted(
|
||||
((parser.detect(text), parser) for parser in candidates),
|
||||
key=lambda item: item[0],
|
||||
reverse=True,
|
||||
)
|
||||
return parser.parse(path, text)
|
||||
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")
|
||||
detail = str(primary_error or template_error)
|
||||
raise ValueError(
|
||||
f"The uploaded statement could not be reliably parsed as {label}. "
|
||||
f"{detail}"
|
||||
) from template_error
|
||||
|
||||
parser, _score = detect_parser(text)
|
||||
if parser is not None:
|
||||
try:
|
||||
result = parser.parse(path, text)
|
||||
if _valid_result(result):
|
||||
return result
|
||||
except (ValueError, RuntimeError) as exc:
|
||||
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(
|
||||
"Unsupported or unreconciled statement format. "
|
||||
f"{detail}"
|
||||
) from template_error
|
||||
|
||||
Reference in New Issue
Block a user