59 lines
3.5 KiB
Python
59 lines
3.5 KiB
Python
from __future__ import annotations
|
|
import re, pandas as pd
|
|
from pathlib import Path
|
|
from .base import *
|
|
from .common import DATE_TOKEN_PATTERN, DATE_TOKEN_RE, date_iso, find
|
|
|
|
class HDFCParser(BaseParser):
|
|
bank_name='HDFC Bank'; parser_name='HDFCParser'
|
|
@classmethod
|
|
def detect(cls,text):
|
|
u=text.upper(); return 0.97 if 'HDFC' in u and 'WITHDRAWAL AMT.' in u and 'DEPOSIT AMT.' 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 No\s*:?\s*([0-9X*]+)',text)
|
|
meta.customer_id=find(r'Cust ID\s*:?\s*([0-9X*]+)',text)
|
|
meta.ifsc=find(r'(?:RTGS/\s*NEFT IFSC|IFSC)\s*:?\s*([A-Z0-9]+)',text)
|
|
m=re.search(rf'From\s*:\s*({DATE_TOKEN_PATTERN})\s+To\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))
|
|
# first non-empty line before address usually contains customer name; allow manual override in UI
|
|
m=re.search(r'\n\s{2,}([^\n:]{3,60})\s*\n.*?Address\s*:',text,re.S|re.I)
|
|
if m: meta.customer_name=norm(m.group(1).splitlines()[-1])
|
|
lines=text.splitlines(); rows=[]; cur=None; page=1
|
|
wd_pos,dep_pos,bal_pos=130,165,190
|
|
date_re=re.compile(rf'^\s*({DATE_TOKEN_PATTERN})\s+(.*)$', re.I)
|
|
for line in lines:
|
|
if '\f' in line: page += line.count('\f')
|
|
if 'Withdrawal Amt.' in line and 'Deposit Amt.' in line:
|
|
wd_pos=line.find('Withdrawal Amt.'); dep_pos=line.find('Deposit Amt.'); bal_pos=line.find('Closing Balance'); continue
|
|
m=date_re.match(line)
|
|
if m:
|
|
if cur: rows.append(cur)
|
|
cur={'transaction_date':m.group(1),'value_date':'','raw_lines':[line],'source_page':page,'wd_pos':wd_pos,'dep_pos':dep_pos,'bal_pos':bal_pos}
|
|
elif cur and line.strip() and not re.match(r'^(Page No|Statement of account|Date\s+Narration|This is a computer)',line.strip(),re.I): cur['raw_lines'].append(line)
|
|
if cur: rows.append(cur)
|
|
out=[]
|
|
for r in rows:
|
|
first=r['raw_lines'][0]; body=' '.join(x.strip() for x in r['raw_lines'])
|
|
# identify value date and ref on first line
|
|
dts=list(DATE_TOKEN_RE.finditer(first))
|
|
if len(dts)>1: r['value_date']=dts[-1].group()
|
|
else: r['value_date']=r['transaction_date']
|
|
nums=list(re.finditer(r'(?<!\d)(?:\d{1,3}(?:,\d{3})+|\d+)\.\d{2}(?!\d)',first))
|
|
debit=credit=bal=None
|
|
for n in nums:
|
|
x=amount(n.group()); p=n.start()
|
|
if p>=r['bal_pos']-5: bal=x
|
|
elif p>=r['dep_pos']-5: credit=x
|
|
elif p>=r['wd_pos']-5: debit=x
|
|
if bal is None and nums: bal=amount(nums[-1].group())
|
|
# narration is text between date and likely ref/value-date region, plus continuation lines
|
|
narr=first[dts[0].end():]
|
|
if len(dts)>1: narr=narr[:dts[-1].start()-dts[0].end()]
|
|
narr=norm(narr+' '+' '.join(x.strip() for x in r['raw_lines'][1:]))
|
|
ref=''; z=re.search(r'\b([A-Z0-9]{10,})\b',body); ref=z.group(1) if z else ''
|
|
if debit is None and credit is None: continue
|
|
out.append({**r,'narration':narr,'reference_no':ref,'debit':debit,'credit':credit,'balance':bal})
|
|
return meta,finalize(pd.DataFrame(out),meta)
|