from __future__ import annotations import re, pandas as pd from pathlib import Path from .base import * from .common import DATE_TOKEN_PATTERN, date_iso, find class SBIModernParser(BaseParser): bank_name='State Bank of India'; parser_name='SBIModernParser' @classmethod def detect(cls,text): u=norm(text).upper() bank_marker='STATE BANK OF INDIA' in u digital_layout='REF NO./CHEQUE' in u and 'DETAILS' in u yono_print_layout=( 'STATEMENT OF ACCOUNT' in u and 'VALUE DATE' in u and 'POST DATE' in u and 'DEBIT' in u and 'CREDIT' in u and 'BALANCE' in u ) return 0.98 if bank_marker and (digital_layout or yono_print_layout) 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) if not meta.customer_name: meta.customer_name=find(r'\bMs\.?\s+([A-Z][A-Z .]{3,})',text) meta.account_number=find(r'Account Number\s*:?\s*([0-9X*]+)',text) meta.ifsc=find(r'(?:IFS|IFSC) Code\s*:?\s*([A-Z0-9]+)',text) m=re.search(rf'(?:Account Statement from|Statement From\s*:?)\s*({DATE_TOKEN_PATTERN})\s+to\s+({DATE_TOKEN_PATTERN})', text, re.I) if m: meta.period_from=date_iso(m.group(1)); meta.period_to=date_iso(m.group(2)) 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(rf'^\s*({DATE_TOKEN_PATTERN})\s+(.*)$', re.I) 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'(?=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: upper=alltxt.upper() if '/CR/' in upper or upper.startswith('DEP TFR') or 'INTEREST CREDIT' in upper: credit=txn elif '/DR/' in upper or upper.startswith('WDL TFR') or upper.startswith('DEBIT '): debit=txn elif 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(rf'Account Statement from\s*({DATE_TOKEN_PATTERN})\s+to\s+({DATE_TOKEN_PATTERN})', text, re.I) if m: meta.period_from=date_iso(m.group(1)); meta.period_to=date_iso(m.group(2)) m=re.search(r'Balance as on\s+[^\n]+\n',text,re.I) date_re=re.compile(rf'^\s*({DATE_TOKEN_PATTERN})\s+({DATE_TOKEN_PATTERN})\s+(.*)$', re.I) 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'(?=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)