Add City Union Bank Bank of Baroda and RBL statement parsers
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
import pdfplumber
|
||||
|
||||
from .base import BaseParser, StatementMeta, amount, extract_text, finalize, norm
|
||||
from .common import date_iso, find, infer_mode
|
||||
|
||||
|
||||
class CityUnionBankParser(BaseParser):
|
||||
bank_name = "City Union Bank"
|
||||
parser_name = "CityUnionBankParser"
|
||||
|
||||
@classmethod
|
||||
def detect(cls, text: str) -> float:
|
||||
upper = (text or "").upper()
|
||||
score = 0.0
|
||||
if "CITY UNION BANK" in upper:
|
||||
score += 0.62
|
||||
if "CIUB0" in upper or "IFSC :CIUB" in upper or "IFSC:CIUB" in upper:
|
||||
score += 0.20
|
||||
if all(token in upper for token in ("DATE", "DESCRIPTION", "CHEQUE NO", "DEBIT", "CREDIT", "BALANCE")):
|
||||
score += 0.16
|
||||
if "END OF STATEMENT - FROM INTERNET BANKING" in upper:
|
||||
score += 0.02
|
||||
return min(score, 0.99)
|
||||
|
||||
@staticmethod
|
||||
def _reference_number(narration: str, cheque_no: str) -> str:
|
||||
if norm(cheque_no):
|
||||
return norm(cheque_no)
|
||||
for pattern in (
|
||||
r"\bUTR[:/\s-]*([A-Z0-9]{10,30})\b",
|
||||
r"\bCHQ\s*NO\s*([0-9]{1,12})\b",
|
||||
r"\b([A-Z]{4}H\d{10,})\b",
|
||||
):
|
||||
match = re.search(pattern, narration, re.I)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return ""
|
||||
|
||||
def parse(self, path, text=None):
|
||||
pdf_path = Path(path)
|
||||
text = text or extract_text(pdf_path)
|
||||
meta = StatementMeta(
|
||||
bank_name=self.bank_name,
|
||||
source_file=pdf_path.name,
|
||||
parser_name=self.parser_name,
|
||||
confidence="High",
|
||||
)
|
||||
meta.customer_name = norm(find(r"CUSTOMER DETAILS\s*:\s*([^\n]+)", text, flags=re.I))
|
||||
meta.account_number = find(r"ACCOUNT NO\s*\(15 DIGIT\)\s*:\s*([0-9]+)", text, flags=re.I)
|
||||
if not meta.account_number:
|
||||
meta.account_number = find(r"ACCOUNT NO\s*:\s*([A-Z0-9-]+)", text, flags=re.I)
|
||||
meta.ifsc = find(r"IFSC\s*:\s*([A-Z0-9]+)", text, flags=re.I)
|
||||
period = re.search(r"STATEMENT OF ACCOUNT\s+from\s+(\d{2}/\d{2}/\d{4})\s+to\s+(\d{2}/\d{2}/\d{4})", text, re.I)
|
||||
if period:
|
||||
meta.period_from = date_iso(period.group(1))
|
||||
meta.period_to = date_iso(period.group(2))
|
||||
|
||||
rows: list[dict] = []
|
||||
with pdfplumber.open(str(pdf_path)) as pdf:
|
||||
for page_no, page in enumerate(pdf.pages, start=1):
|
||||
for table in page.extract_tables() or []:
|
||||
if not table:
|
||||
continue
|
||||
header = [norm(cell).upper() for cell in table[0]]
|
||||
if not all(token in header for token in ("DATE", "DESCRIPTION", "DEBIT", "CREDIT", "BALANCE")):
|
||||
continue
|
||||
for raw in table[1:]:
|
||||
cells = list(raw) + [None] * (6 - len(raw))
|
||||
txn_date, description, cheque_no, debit, credit, balance = cells[:6]
|
||||
txn_date = norm(txn_date)
|
||||
if not re.fullmatch(r"\d{2}/\d{2}/\d{4}", txn_date):
|
||||
continue
|
||||
debit_value = amount(debit)
|
||||
credit_value = amount(credit)
|
||||
balance_value = amount(balance)
|
||||
if debit_value is None and credit_value is None:
|
||||
continue
|
||||
narration = norm(description)
|
||||
rows.append({
|
||||
"transaction_date": txn_date,
|
||||
"value_date": txn_date,
|
||||
"narration": narration,
|
||||
"reference_no": self._reference_number(narration, norm(cheque_no)),
|
||||
"debit": debit_value,
|
||||
"credit": credit_value,
|
||||
"balance": balance_value,
|
||||
"source_page": page_no,
|
||||
"mode": infer_mode(narration),
|
||||
})
|
||||
|
||||
data = pd.DataFrame(rows)
|
||||
if not data.empty:
|
||||
first = data.iloc[0]
|
||||
first_balance = float(first["balance"])
|
||||
first_debit = float(first["debit"]) if pd.notna(first["debit"]) else 0.0
|
||||
first_credit = float(first["credit"]) if pd.notna(first["credit"]) else 0.0
|
||||
meta.opening_balance = round(first_balance + first_debit - first_credit, 2)
|
||||
meta.total_debit = round(float(pd.to_numeric(data["debit"], errors="coerce").fillna(0).sum()), 2)
|
||||
meta.total_credit = round(float(pd.to_numeric(data["credit"], errors="coerce").fillna(0).sum()), 2)
|
||||
meta.closing_balance = round(float(data.iloc[-1]["balance"]), 2)
|
||||
return meta, finalize(data, meta)
|
||||
Reference in New Issue
Block a user