Add bank statement analyzer with automatic work storage
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
from __future__ import annotations
|
||||
import re, pandas as pd
|
||||
from pathlib import Path
|
||||
from .base import *
|
||||
from .common import find
|
||||
|
||||
class SBIModernParser(BaseParser):
|
||||
bank_name='State Bank of India'; parser_name='SBIModernParser'
|
||||
@classmethod
|
||||
def detect(cls,text):
|
||||
u=text.upper(); return 0.98 if 'STATE BANK OF INDIA' in u and 'REF NO./CHEQUE' in u and 'DETAILS' 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'Account Name\s*:?\s*([^\n]+)',text)
|
||||
meta.account_number=find(r'Account Number\s*:?\s*([0-9X*]+)',text)
|
||||
meta.ifsc=find(r'IFS Code\s*:?\s*([A-Z0-9]+)',text)
|
||||
m=re.search(r'Account Statement from\s*(\d{1,2}\s+[A-Za-z]{3}\s+\d{4})\s+to\s+(\d{1,2}\s+[A-Za-z]{3}\s+\d{4})',text,re.I)
|
||||
if m:
|
||||
meta.period_from=pd.to_datetime(m.group(1),dayfirst=True).strftime('%Y-%m-%d'); meta.period_to=pd.to_datetime(m.group(2),dayfirst=True).strftime('%Y-%m-%d')
|
||||
meta.opening_balance=amount(find(r'Balance as on[^\n]*\n\s*([\d,]+\.\d{2})',text))
|
||||
# Format A: date details ref debit credit balance
|
||||
date_re=re.compile(r'^\s*(\d{1,2}\s+[A-Za-z]{3}\s+\d{4})\s+(.*)$')
|
||||
rows=[]; cur=None; page=1
|
||||
for line in text.splitlines():
|
||||
if '\f' in line: page += line.count('\f')
|
||||
m=date_re.match(line)
|
||||
if m:
|
||||
if cur: rows.append(cur)
|
||||
cur={'transaction_date':m.group(1),'value_date':m.group(1),'lines':[m.group(2)],'source_page':page}
|
||||
elif cur and line.strip() and not re.match(r'^(Date\s+Details|Txn Date|Account Statement|State Bank)',line.strip(),re.I):
|
||||
cur['lines'].append(line)
|
||||
if cur: rows.append(cur)
|
||||
out=[]; prev=meta.opening_balance
|
||||
for r in rows:
|
||||
first=r['lines'][0]; alltxt=norm(' '.join(r['lines']))
|
||||
nums=list(re.finditer(r'(?<!\d)([\d,]+\.\d{2})(?!\d)',first))
|
||||
if not nums: continue
|
||||
bal=amount(nums[-1].group(1)); debit=credit=None
|
||||
# use dashes and positions from SBI layout: debit then credit then balance
|
||||
if len(nums)>=2:
|
||||
txn=amount(nums[-2].group(1)); p=nums[-2].start()
|
||||
if prev is not None and bal is not None:
|
||||
if abs((prev-txn)-bal)<0.05: debit=txn
|
||||
elif abs((prev+txn)-bal)<0.05: credit=txn
|
||||
if debit is None and credit is None:
|
||||
if p >= 78: credit=txn
|
||||
else: debit=txn
|
||||
narr=first[:nums[-2].start() if len(nums)>=2 else nums[-1].start()].strip()+' '+' '.join(x.strip() for x in r['lines'][1:])
|
||||
ref=''; z=re.search(r'(?:UPI|NEFT|IMPS|RTGS)[/A-Z0-9-]{6,}',alltxt,re.I); ref=z.group(0) if z else ''
|
||||
if debit is None and credit is None: continue
|
||||
out.append({**r,'narration':norm(narr),'reference_no':ref,'debit':debit,'credit':credit,'balance':bal})
|
||||
if bal is not None: prev=bal
|
||||
if out:
|
||||
if meta.opening_balance is None:
|
||||
f=out[0]; meta.opening_balance=round((f['balance'] or 0)+(f.get('debit') or 0)-(f.get('credit') or 0),2)
|
||||
meta.closing_balance=out[-1]['balance']
|
||||
return meta,finalize(pd.DataFrame(out),meta)
|
||||
|
||||
class SBIOtherParser(BaseParser):
|
||||
bank_name='State Bank of India'; parser_name='SBIOtherParser'
|
||||
@classmethod
|
||||
def detect(cls,text):
|
||||
u=text.upper(); return 0.97 if 'TXN DATE' in u and 'VALUE' in u and 'REF NO./CHEQUE' in u and 'BY TRANSFER' 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'Account Name\s*:\s*([^\n]+)',text)
|
||||
meta.account_number=find(r'Account Number\s*:?\s*([0-9X*]+)',text)
|
||||
meta.ifsc=find(r'IFS Code\s*:?\s*([A-Z0-9]+)',text)
|
||||
m=re.search(r'Account Statement from\s*(\d{1,2}\s+[A-Za-z]{3}\s+\d{4})\s+to\s+(\d{1,2}\s+[A-Za-z]{3}\s+\d{4})',text,re.I)
|
||||
if m:
|
||||
meta.period_from=pd.to_datetime(m.group(1),dayfirst=True).strftime('%Y-%m-%d'); meta.period_to=pd.to_datetime(m.group(2),dayfirst=True).strftime('%Y-%m-%d')
|
||||
m=re.search(r'Balance as on\s+[^\n]+\n',text,re.I)
|
||||
date_re=re.compile(r'^\s*(\d{1,2}\s+[A-Za-z]{3}\s+\d{4})\s+(\d{1,2}\s+[A-Za-z]{3}\s+\d{4})\s+(.*)$')
|
||||
rows=[]; cur=None; page=1
|
||||
for line in text.splitlines():
|
||||
if '\f' in line: page += line.count('\f')
|
||||
mm=date_re.match(line)
|
||||
if mm:
|
||||
if cur: rows.append(cur)
|
||||
cur={'transaction_date':mm.group(1),'value_date':mm.group(2),'lines':[mm.group(3)],'source_page':page}
|
||||
elif cur and line.strip() and not re.match(r'^(Txn Date|Account Statement|Account Name)',line.strip(),re.I): cur['lines'].append(line)
|
||||
if cur: rows.append(cur)
|
||||
out=[]
|
||||
for r in rows:
|
||||
first=r['lines'][0]; alltxt=norm(' '.join(r['lines']))
|
||||
nums=list(re.finditer(r'(?<!\d)([\d,]+\.\d{2})(?!\d)',first))
|
||||
if not nums: continue
|
||||
bal=amount(nums[-1].group(1)); debit=credit=None
|
||||
if len(nums)>=2:
|
||||
txn=amount(nums[-2].group(1)); p=nums[-2].start()
|
||||
# Based on header layout: Debit starts before Credit
|
||||
if p < 55: debit=txn
|
||||
else: credit=txn
|
||||
narr=first[:nums[-2].start() if len(nums)>=2 else nums[-1].start()].strip()+' '+' '.join(x.strip() for x in r['lines'][1:])
|
||||
ref=''; z=re.search(r'(?:UPI|NEFT|IMPS|RTGS)[/A-Z0-9-]{6,}',alltxt,re.I); ref=z.group(0) if z else ''
|
||||
if debit is None and credit is None: continue
|
||||
out.append({**r,'narration':norm(narr),'reference_no':ref,'debit':debit,'credit':credit,'balance':bal})
|
||||
if out:
|
||||
f=out[0]; meta.opening_balance=round((f['balance'] or 0)+(f.get('debit') or 0)-(f.get('credit') or 0),2); meta.closing_balance=out[-1]['balance']
|
||||
return meta,finalize(pd.DataFrame(out),meta)
|
||||
Reference in New Issue
Block a user