Add bank statement analyzer with automatic work storage
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
from __future__ import annotations
|
||||
import re
|
||||
import pandas as pd
|
||||
from pathlib import Path
|
||||
from .base import BaseParser, StatementMeta, extract_text, amount, norm, finalize, page_of_line
|
||||
from .common import find, date_iso
|
||||
|
||||
class IDFCFirstParser(BaseParser):
|
||||
bank_name='IDFC FIRST Bank'; parser_name='IDFCFirstParser'
|
||||
@classmethod
|
||||
def detect(cls,text):
|
||||
u=text.upper(); return 0.99 if 'IDFC FIRST BANK' in u and 'STATEMENT PERIOD' in u else 0
|
||||
def parse(self,path,text=None):
|
||||
text=text or extract_text(path)
|
||||
meta=StatementMeta(bank_name=self.bank_name,source_file=Path(path).name,parser_name=self.parser_name,confidence='High')
|
||||
meta.customer_name=find(r'CUSTOMER NAME\s*:\s*([^\n]+)',text)
|
||||
meta.account_number=find(r'ACCOUNT NO\s*:\s*([0-9X*]+)',text)
|
||||
meta.customer_id=find(r'CUSTOMER ID\s*:\s*([0-9X*]+)',text)
|
||||
meta.ifsc=find(r'IFSC\s*:\s*([A-Z0-9]+)',text)
|
||||
m=re.search(r'STATEMENT PERIOD\s*:\s*(\d{4}-\d{2}-\d{2})\s+TO\s+(\d{4}-\d{2}-\d{2})',text,re.I)
|
||||
if m: meta.period_from,meta.period_to=m.groups()
|
||||
m=re.search(r'Opening Balance\s+Total Debit\s+Total Credit\s+Closing Balance\s*\n\s*([\d,.]+)\s+([\d,.]+)\s+([\d,.]+)\s+([\d,.]+)',text,re.I)
|
||||
if m: meta.opening_balance,meta.total_debit,meta.total_credit,meta.closing_balance=map(amount,m.groups())
|
||||
lines=text.splitlines(); rows=[]; current=None; pos=0
|
||||
pat=re.compile(r'^\s*(\d{2}-[A-Za-z]{3}-\d{4})\s+(\d{2}-[A-Za-z]{3}-\d{4})\s+(.*)$')
|
||||
for line in lines:
|
||||
mm=pat.match(line)
|
||||
if mm:
|
||||
if current: rows.append(current)
|
||||
current={'transaction_date':mm.group(1),'value_date':mm.group(2),'body':mm.group(3),'source_page':page_of_line(text,pos)}
|
||||
elif current and line.strip() and not re.match(r'^(STATEMENT|Opening Balance|REGISTERED OFFICE|Page \d+)',line.strip(),re.I):
|
||||
current['body']+=' '+line.strip()
|
||||
pos+=len(line)+1
|
||||
if current: rows.append(current)
|
||||
out=[]; prev=meta.opening_balance
|
||||
for r in rows:
|
||||
body=norm(r['body']); nums=list(re.finditer(r'(?<!\d)(?:\d{1,3}(?:,\d{2,3})+|\d+)\.\d{2}(?!\d)',body))
|
||||
if not nums: continue
|
||||
bal=amount(nums[-1].group()); rem=body[:nums[-1].start()].strip(); vals=[amount(x.group()) for x in nums[:-1]]
|
||||
debit=credit=None
|
||||
if vals:
|
||||
txn=vals[-1]
|
||||
if prev is not None and bal is not None:
|
||||
d1=round(prev-txn,2); c1=round(prev+txn,2)
|
||||
if abs(d1-bal)<0.02: debit=txn
|
||||
elif abs(c1-bal)<0.02: credit=txn
|
||||
else:
|
||||
# explicit columns often leave one amount only; infer from change
|
||||
credit=txn if bal>=prev else None; debit=txn if bal<prev else None
|
||||
else: credit=txn
|
||||
ref=''
|
||||
z=re.search(r'(?:UPI|NEFT|IMPS|RTGS)[/-](?:MOB/|OPM/|INET/|DR/|CR/)?([A-Z0-9]{8,})',rem,re.I)
|
||||
if z: ref=z.group(1)
|
||||
out.append({**r,'narration':rem,'reference_no':ref,'debit':debit,'credit':credit,'balance':bal})
|
||||
if bal is not None: prev=bal
|
||||
return meta,finalize(pd.DataFrame(out),meta)
|
||||
Reference in New Issue
Block a user