153 lines
6.8 KiB
Python
153 lines
6.8 KiB
Python
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"(?<![A-Za-z0-9])([0-9,]+\.\d{2})(?![A-Za-z0-9])")
|
|
|
|
@classmethod
|
|
def detect(cls, text: str) -> 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)
|