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 KotakParser(BaseParser): bank_name='Kotak Mahindra Bank'; parser_name='KotakParser' @classmethod def detect(cls,text): u=text.upper(); return 0.98 if 'KOTAK' in u and 'CHEQUE/REFERENCE#' in u and 'TRANSACTION 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.account_number=find(r'Account\s*#\s*Variant\s*KOTAK\s*\n.*?([0-9X*]{6,})',text,flags=re.I|re.S) meta.ifsc=find(r'IFSC\s+([A-Z0-9]+)',text) m=re.search(rf'({DATE_TOKEN_PATTERN})\s*-\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)) 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 ' - ' in m.group(2) and re.match(r'\d{2}\s+[A-Za-z]{3},',m.group(2).strip()): continue 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+TRANSACTION|Need help|Page \d+)',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'])) # amounts are signed in debit/credit columns, final unsigned balance nums=list(re.finditer(r'([+-]?\d{1,3}(?:,\d{3})*\.\d{2})',first)) if not nums: continue bal=amount(nums[-1].group(1)); debit=credit=None if len(nums)>=2: raw=nums[-2].group(1); txn=abs(amount(raw) or 0) if raw.strip().startswith('-'): debit=txn elif raw.strip().startswith('+'): credit=txn else: # position fallback: debit column before credit credit=txn if nums[-2].start()>95 else None; debit=txn if nums[-2].start()<=95 else None elif 'OPENING BALANCE' in first.upper(): meta.opening_balance=bal; continue 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'\b(?:UPI|NEFT|IMPS|RTGS)-[A-Z0-9]+\b',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: 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)