from __future__ import annotations import re from pathlib import Path import pandas as pd from .base import BaseParser, StatementMeta, amount, extract_text, finalize, norm from .common import date_iso, find, infer_mode class BankOfBarodaParser(BaseParser): bank_name = "Bank of Baroda" parser_name = "BankOfBarodaParser" _DATE = re.compile(r"^(\d{2}-\d{2}-\d{2})\s+(.*)$") _BALANCE = re.compile(r"(-?[0-9,]+\.\d{2})(Cr|Dr)\s*$", re.I) _MONEY = re.compile(r"(? float: upper = (text or "").upper() score = 0.0 if "BANK OF BARODA" in upper: score += 0.62 if "BARB0" in upper or "IFSC CODE: BARB" in upper: score += 0.20 if all(token in upper for token in ("DATE", "PARTICULARS", "CHQ.NO.", "WITHDRAWALS", "DEPOSITS", "BALANCE")): score += 0.16 if "BOB ADVANTAGE" in upper: score += 0.02 return min(score, 0.99) @staticmethod def _signed_balance(value: str, marker: str) -> float | None: parsed = amount(value) if parsed is None: return None return -abs(parsed) if marker.upper() == "DR" else abs(parsed) @staticmethod def _reference_number(narration: str, cheque_no: str) -> str: if norm(cheque_no): return norm(cheque_no) for pattern in ( r"\bNEFT[-/]([A-Z0-9]{10,30})\b", r"\bRTGS[-/]([A-Z0-9]{10,30})\b", r"\b([0-9]{12,18})\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"A/C Name\s*:\s*([^\n]+)", text, flags=re.I)) meta.account_number = find(r"A/C Number\s*:\s*([0-9]+)", text, flags=re.I) meta.ifsc = find(r"IFSC CODE:\s*([A-Z0-9]+)", text, flags=re.I) period = re.search(r"Statement of account for the period of\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] = [] current: dict | None = None previous_balance: float | None = None opening_balance: float | None = None pages = re.split(r"\f", text) for page_no, page_text in enumerate(pages, start=1): for raw_line in page_text.splitlines(): line = raw_line.rstrip() match = self._DATE.match(line.strip()) if match: if current is not None: rows.append(current) previous_balance = current["balance"] current = None txn_date, remainder = match.groups() balance_match = self._BALANCE.search(remainder) if not balance_match: continue balance_value = self._signed_balance(balance_match.group(1), balance_match.group(2)) before_balance = remainder[:balance_match.start()].rstrip() if norm(before_balance).upper() == "B/F": opening_balance = balance_value previous_balance = balance_value continue money_matches = list(self._MONEY.finditer(before_balance)) narration_part = before_balance cheque_no = "" if money_matches: narration_part = before_balance[:money_matches[0].start()].rstrip() cheque_match = re.search(r"\s([0-9]{1,8})\s*$", narration_part) if cheque_match: cheque_no = cheque_match.group(1) narration_part = narration_part[:cheque_match.start()].rstrip() debit_value = credit_value = None if previous_balance is not None and balance_value is not None: delta = round(balance_value - previous_balance, 2) if delta < -0.009: debit_value = abs(delta) elif delta > 0.009: credit_value = delta elif money_matches: debit_value = amount(money_matches[-1].group(1)) current = { "transaction_date": txn_date, "value_date": txn_date, "narration": norm(narration_part), "reference_no": self._reference_number(norm(narration_part), cheque_no), "debit": debit_value, "credit": credit_value, "balance": balance_value, "source_page": page_no, "mode": infer_mode(norm(narration_part)), } continue if current is not None: continuation = norm(line) upper = continuation.upper() if not continuation or upper.startswith(("PAGE TOTAL", "GRAND TOTAL", "BANK OF BARODA", "A/C NUMBER", "STATEMENT OF ACCOUNT", "DATE PARTICULARS", "NOTE:", "UNLESS ")): continue if set(continuation) <= {"-"}: continue current["narration"] = norm(f"{current['narration']} {continuation}") current["reference_no"] = self._reference_number(current["narration"], current["reference_no"]) current["mode"] = infer_mode(current["narration"]) if current is not None: rows.append(current) 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(opening_balance if opening_balance is not None else 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)