Support current Indian Bank FY statement transaction layout
This commit is contained in:
@@ -1,11 +1,31 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import re, pandas as pd
|
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
from .base import *
|
from .base import *
|
||||||
from .common import find
|
from .common import find
|
||||||
|
|
||||||
|
|
||||||
|
def _signed_amount(value: str | None, suffix: str | None) -> float | None:
|
||||||
|
"""Return CR as positive and DR as negative."""
|
||||||
|
parsed = amount(value)
|
||||||
|
if parsed is None:
|
||||||
|
return None
|
||||||
|
return -parsed if (suffix or "").upper() == "DR" else parsed
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_modern_date(value: str) -> str:
|
||||||
|
"""Normalize both '01 Apr 2025' and 'Apr 01 2025' to ISO date."""
|
||||||
|
return pd.to_datetime(value, dayfirst=True, errors="raise").strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
|
||||||
class IndianBankModernParser(BaseParser):
|
class IndianBankModernParser(BaseParser):
|
||||||
bank_name='Indian Bank'; parser_name='IndianBankModernParser'
|
bank_name = "Indian Bank"
|
||||||
|
parser_name = "IndianBankModernParser"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def detect(cls, text):
|
def detect(cls, text):
|
||||||
# pdfplumber's layout mode can insert multiple spaces inside headings
|
# pdfplumber's layout mode can insert multiple spaces inside headings
|
||||||
@@ -13,101 +33,358 @@ class IndianBankModernParser(BaseParser):
|
|||||||
# matching so the same bank PDF is detected whether pdftotext is
|
# matching so the same bank PDF is detected whether pdftotext is
|
||||||
# installed in the runtime image or the pdfplumber fallback is used.
|
# installed in the runtime image or the pdfplumber fallback is used.
|
||||||
u = norm(text).upper()
|
u = norm(text).upper()
|
||||||
return 0.98 if 'ACCOUNT STATEMENT' in u and 'TRANSACTION DETAILS' in u and 'TOTAL CREDITS' in u else 0
|
return (
|
||||||
|
0.98
|
||||||
|
if "ACCOUNT STATEMENT" in u
|
||||||
|
and "TRANSACTION DETAILS" in u
|
||||||
|
and "TOTAL CREDITS" in u
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
|
||||||
def parse(self, path, text=None):
|
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')
|
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",
|
||||||
|
)
|
||||||
|
|
||||||
# Some Indian Bank statements leave these values blank. Do not let a
|
# Some Indian Bank statements leave these values blank. Do not let a
|
||||||
# value from the adjacent ACCOUNT SUMMARY column become the customer
|
# value from the adjacent ACCOUNT SUMMARY column become the customer
|
||||||
# name merely because PDF text extraction merges the two columns.
|
# name merely because PDF text extraction merges the two columns.
|
||||||
customer=find(r'Account Holder Name[ \t]*([^\n]*)',text)
|
customer = find(r"Account Holder Name[ \t]*([^\n]*)", text)
|
||||||
customer = norm(customer)
|
customer = norm(customer)
|
||||||
if customer and not re.search(r'^(Opening Balance|Account Type|Account Number|Customer)',customer,re.I):
|
if customer and not re.search(
|
||||||
|
r"^(Opening Balance|Account Type|Account Number|Customer)",
|
||||||
|
customer,
|
||||||
|
re.I,
|
||||||
|
):
|
||||||
meta.customer_name = customer
|
meta.customer_name = customer
|
||||||
meta.account_number=find(r'Account Number[ \t]*([0-9X*]{4,})',text)
|
|
||||||
m=re.search(r'For period:\s*(\d{2}\s+[A-Za-z]{3}\s+\d{4})\s*-\s*(\d{2}\s+[A-Za-z]{3}\s+\d{4})',text,re.I)
|
meta.account_number = find(
|
||||||
if m:
|
r"Account Number[ \t]*([0-9X*]{4,})",
|
||||||
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')
|
text,
|
||||||
meta.opening_balance=amount(find(r'Opening Balance\s+INR\s*([\d,]+\.\d{2})',text))
|
)
|
||||||
meta.total_credit=amount(find(r'Total Credits\s+\+\s*INR\s*([\d,]+\.\d{2})',text))
|
|
||||||
meta.total_debit=amount(find(r'Total Debits\s+-\s*INR\s*([\d,]+\.\d{2})',text))
|
period_match = re.search(
|
||||||
meta.closing_balance=amount(find(r'Ending Balance\s+INR\s*([\d,]+\.\d{2})',text))
|
r"For period:\s*(\d{2}\s+[A-Za-z]{3}\s+\d{4})\s*-\s*"
|
||||||
date_re=re.compile(r'^\s*([A-Za-z]{3}\s+\d{2}\s+\d{4})\s+(.*)$')
|
r"(\d{2}\s+[A-Za-z]{3}\s+\d{4})",
|
||||||
rows=[]; cur=None; page=1
|
text,
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
if period_match:
|
||||||
|
meta.period_from = _parse_modern_date(period_match.group(1))
|
||||||
|
meta.period_to = _parse_modern_date(period_match.group(2))
|
||||||
|
|
||||||
|
opening_match = re.search(
|
||||||
|
r"Opening Balance\s+INR\s*([\d,]+\.\d{2})\s*(CR|DR)?",
|
||||||
|
text,
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
if opening_match:
|
||||||
|
meta.opening_balance = _signed_amount(
|
||||||
|
opening_match.group(1),
|
||||||
|
opening_match.group(2),
|
||||||
|
)
|
||||||
|
|
||||||
|
meta.total_credit = amount(
|
||||||
|
find(r"Total Credits\s+\+\s*INR\s*([\d,]+\.\d{2})", text)
|
||||||
|
)
|
||||||
|
meta.total_debit = amount(
|
||||||
|
find(r"Total Debits\s+-\s*INR\s*([\d,]+\.\d{2})", text)
|
||||||
|
)
|
||||||
|
|
||||||
|
closing_match = re.search(
|
||||||
|
r"Ending Balance\s+INR\s*([\d,]+\.\d{2})\s*(CR|DR)?",
|
||||||
|
text,
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
if closing_match:
|
||||||
|
meta.closing_balance = _signed_amount(
|
||||||
|
closing_match.group(1),
|
||||||
|
closing_match.group(2),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Indian Bank's current PDF layout is seen in both date orders,
|
||||||
|
# depending on the text extraction engine:
|
||||||
|
# 01 Apr 2025 ...
|
||||||
|
# Apr 01 2025 ...
|
||||||
|
# Accept both without changing the parser selected for older samples.
|
||||||
|
date_re = re.compile(
|
||||||
|
r"^\s*((?:\d{2}\s+[A-Za-z]{3}|[A-Za-z]{3}\s+\d{2})\s+\d{4})\s+(.*)$"
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
current = None
|
||||||
|
page = 1
|
||||||
for line in text.splitlines():
|
for line in text.splitlines():
|
||||||
if '\f' in line: page+=line.count('\f')
|
if "\f" in line:
|
||||||
m=date_re.match(line)
|
page += line.count("\f")
|
||||||
if m:
|
|
||||||
if cur: rows.append(cur)
|
match = date_re.match(line)
|
||||||
cur={'transaction_date':m.group(1),'value_date':m.group(1),'lines':[m.group(2)],'source_page':page}
|
if match:
|
||||||
elif cur and line.strip() and not re.match(r'^(Date\s+Transaction|ACCOUNT STATEMENT|Page)',line.strip(),re.I): cur['lines'].append(line)
|
if current:
|
||||||
if cur: rows.append(cur)
|
rows.append(current)
|
||||||
out=[]; prev=meta.opening_balance
|
transaction_date = _parse_modern_date(match.group(1))
|
||||||
for r in rows:
|
current = {
|
||||||
first=r['lines'][0]; alltxt=norm(' '.join(r['lines']))
|
"transaction_date": transaction_date,
|
||||||
vals=[(m.start(),amount(m.group(1))) for m in re.finditer(r'INR\s*([\d,]+\.\d{2})',first,re.I)]
|
"value_date": transaction_date,
|
||||||
# columns in sample: debit ~45, credit ~65, balance ~85. Use signs/placeholders and running balance.
|
"lines": [match.group(2)],
|
||||||
bal=vals[-1][1] if vals else None; debit=credit=None
|
"source_page": page,
|
||||||
if len(vals)>=2:
|
}
|
||||||
txn=vals[-2][1]
|
elif (
|
||||||
before=first[:re.search(r'INR\s*[\d,]+\.\d{2}',first,re.I).start()] if re.search(r'INR\s*[\d,]+\.\d{2}',first,re.I) else first
|
current
|
||||||
# presence of '-' before first amount often means no debit; inspect spacing/position
|
and line.strip()
|
||||||
p=vals[-2][0]
|
and not re.match(
|
||||||
if prev is not None and bal is not None:
|
r"^(Date\s+Transaction|ACCOUNT STATEMENT|Page)",
|
||||||
if abs((prev-txn)-bal)<0.05: debit=txn
|
line.strip(),
|
||||||
elif abs((prev+txn)-bal)<0.05: credit=txn
|
re.I,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
current["lines"].append(line)
|
||||||
|
|
||||||
|
if current:
|
||||||
|
rows.append(current)
|
||||||
|
|
||||||
|
out = []
|
||||||
|
previous_balance = meta.opening_balance
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
first_line = row["lines"][0]
|
||||||
|
all_text = norm(" ".join(row["lines"]))
|
||||||
|
|
||||||
|
# Balance is always the last INR amount and may be CR or DR.
|
||||||
|
balance_match = re.search(
|
||||||
|
r"INR\s*([\d,]+\.\d{2})\s*(CR|DR)\s*$",
|
||||||
|
first_line,
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
if balance_match:
|
||||||
|
balance = _signed_amount(
|
||||||
|
balance_match.group(1),
|
||||||
|
balance_match.group(2),
|
||||||
|
)
|
||||||
|
prefix = first_line[: balance_match.start()]
|
||||||
|
else:
|
||||||
|
balance = None
|
||||||
|
prefix = first_line
|
||||||
|
|
||||||
|
amount_matches = list(
|
||||||
|
re.finditer(r"INR\s*([\d,]+\.\d{2})", prefix, re.I)
|
||||||
|
)
|
||||||
|
transaction_amount = (
|
||||||
|
amount(amount_matches[-1].group(1)) if amount_matches else None
|
||||||
|
)
|
||||||
|
|
||||||
|
debit = None
|
||||||
|
credit = None
|
||||||
|
|
||||||
|
if (
|
||||||
|
transaction_amount is not None
|
||||||
|
and previous_balance is not None
|
||||||
|
and balance is not None
|
||||||
|
):
|
||||||
|
if abs((previous_balance - transaction_amount) - balance) < 0.05:
|
||||||
|
debit = transaction_amount
|
||||||
|
elif abs((previous_balance + transaction_amount) - balance) < 0.05:
|
||||||
|
credit = transaction_amount
|
||||||
|
|
||||||
|
# Fallback for the first row or when running-balance inference is
|
||||||
|
# unavailable. In the rendered Indian Bank table, a dash occupies
|
||||||
|
# the empty debit/credit column. Determine the side from text
|
||||||
|
# preceding the transaction amount.
|
||||||
|
if transaction_amount is not None and debit is None and credit is None:
|
||||||
|
before_amount = prefix[: amount_matches[-1].start()]
|
||||||
|
after_amount = prefix[amount_matches[-1].end() :]
|
||||||
|
|
||||||
|
# "- INR 1,000.00" means debit is empty, therefore credit.
|
||||||
|
if re.search(r"-\s*$", before_amount):
|
||||||
|
credit = transaction_amount
|
||||||
|
# "INR 1,000.00 -" means credit is empty, therefore debit.
|
||||||
|
elif re.match(r"^\s*-", after_amount):
|
||||||
|
debit = transaction_amount
|
||||||
|
else:
|
||||||
|
# Position fallback retained for extraction engines that
|
||||||
|
# preserve table spacing but omit the dash placeholder.
|
||||||
|
if amount_matches[-1].start() < 52:
|
||||||
|
debit = transaction_amount
|
||||||
|
else:
|
||||||
|
credit = transaction_amount
|
||||||
|
|
||||||
|
narration = re.split(
|
||||||
|
r"\s+INR\s*[\d,]+\.\d{2}",
|
||||||
|
all_text,
|
||||||
|
1,
|
||||||
|
flags=re.I,
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
reference_no = ""
|
||||||
|
reference_match = re.search(
|
||||||
|
r"(?:NEFT|IMPS|UPI|RTGS)[/A-Z0-9-]{6,}",
|
||||||
|
all_text,
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
if reference_match:
|
||||||
|
reference_no = reference_match.group(0)
|
||||||
|
|
||||||
if debit is None and credit is None:
|
if debit is None and credit is None:
|
||||||
if p<52: debit=txn
|
continue
|
||||||
else: credit=txn
|
|
||||||
narr=re.split(r'\s+INR\s*[\d,]+\.\d{2}',alltxt,1,flags=re.I)[0]
|
out.append(
|
||||||
ref=''; z=re.search(r'(?:NEFT|IMPS|UPI|RTGS)[/A-Z0-9-]{6,}',alltxt,re.I); ref=z.group(0) if z else ''
|
{
|
||||||
if debit is None and credit is None: continue
|
**row,
|
||||||
out.append({**r,'narration':narr,'reference_no':ref,'debit':debit,'credit':credit,'balance':bal})
|
"narration": narration,
|
||||||
if bal is not None: prev=bal
|
"reference_no": reference_no,
|
||||||
|
"debit": debit,
|
||||||
|
"credit": credit,
|
||||||
|
"balance": balance,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if balance is not None:
|
||||||
|
previous_balance = balance
|
||||||
|
|
||||||
return meta, finalize(pd.DataFrame(out), meta)
|
return meta, finalize(pd.DataFrame(out), meta)
|
||||||
|
|
||||||
|
|
||||||
class IndianBankLegacyParser(BaseParser):
|
class IndianBankLegacyParser(BaseParser):
|
||||||
bank_name='Indian Bank'; parser_name='IndianBankLegacyParser'
|
bank_name = "Indian Bank"
|
||||||
|
parser_name = "IndianBankLegacyParser"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def detect(cls, text):
|
def detect(cls, text):
|
||||||
u=text.upper(); return 0.97 if 'STATEMENT OF ACCOUNT FROM' in u and 'REMITTER' in u and 'CHEQUE NO' in u else 0
|
u = text.upper()
|
||||||
|
return (
|
||||||
|
0.97
|
||||||
|
if "STATEMENT OF ACCOUNT FROM" in u
|
||||||
|
and "REMITTER" in u
|
||||||
|
and "CHEQUE NO" in u
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
|
||||||
def parse(self, path, text=None):
|
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='Medium')
|
text = text or extract_text(path)
|
||||||
meta.account_number=find(r'for Account Number\s*\.?\s*([0-9X*]+)',text)
|
meta = StatementMeta(
|
||||||
m=re.search(r'STATEMENT OF ACCOUNT from\s*(\d{2}/\d{2}/\d{4})\s*to\s*(\d{2}/\d{2}/\d{4})',text,re.I)
|
bank_name=self.bank_name,
|
||||||
|
source_file=Path(path).name,
|
||||||
|
parser_name=self.parser_name,
|
||||||
|
confidence="Medium",
|
||||||
|
)
|
||||||
|
meta.account_number = find(r"for Account Number\s*\.?\s*([0-9X*]+)", text)
|
||||||
|
m = re.search(
|
||||||
|
r"STATEMENT OF ACCOUNT from\s*(\d{2}/\d{2}/\d{4})\s*to\s*"
|
||||||
|
r"(\d{2}/\d{2}/\d{4})",
|
||||||
|
text,
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
if m:
|
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.period_from = pd.to_datetime(
|
||||||
date_re=re.compile(r'^\s*(\d{2}/\d{2})(?:/\d{4})?\s+(\d{2}/\d{2})(?:/\d{4})?\s+(.*)$')
|
m.group(1), dayfirst=True
|
||||||
rows=[]; cur=None; page=1
|
).strftime("%Y-%m-%d")
|
||||||
|
meta.period_to = pd.to_datetime(
|
||||||
|
m.group(2), dayfirst=True
|
||||||
|
).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
date_re = re.compile(
|
||||||
|
r"^\s*(\d{2}/\d{2})(?:/\d{4})?\s+"
|
||||||
|
r"(\d{2}/\d{2})(?:/\d{4})?\s+(.*)$"
|
||||||
|
)
|
||||||
|
rows = []
|
||||||
|
current = None
|
||||||
|
page = 1
|
||||||
for line in text.splitlines():
|
for line in text.splitlines():
|
||||||
if '\f' in line: page+=line.count('\f')
|
if "\f" in line:
|
||||||
m=date_re.match(line)
|
page += line.count("\f")
|
||||||
if m:
|
match = date_re.match(line)
|
||||||
if cur: rows.append(cur)
|
if match:
|
||||||
year=(meta.period_from[:4] if meta.period_from else '2024')
|
if current:
|
||||||
td=m.group(1).replace(' ','')+'/'+year; vd=m.group(2).replace(' ','')+'/'+year
|
rows.append(current)
|
||||||
cur={'transaction_date':td,'value_date':vd,'lines':[m.group(3)],'source_page':page}
|
year = meta.period_from[:4] if meta.period_from else "2024"
|
||||||
elif cur and line.strip() and not re.match(r'^(Value Post|Date Date|STATEMENT OF ACCOUNT|Page No)',line.strip(),re.I): cur['lines'].append(line)
|
transaction_date = match.group(1).replace(" ", "") + "/" + year
|
||||||
if cur: rows.append(cur)
|
value_date = match.group(2).replace(" ", "") + "/" + year
|
||||||
out=[]; prev=None
|
current = {
|
||||||
for r in rows:
|
"transaction_date": transaction_date,
|
||||||
first=r['lines'][0]; alltxt=norm(' '.join(r['lines']))
|
"value_date": value_date,
|
||||||
# balance is amount followed by CR/DR at far right
|
"lines": [match.group(3)],
|
||||||
mb=re.search(r'([\d,]+\.\d{2})(CR|DR)\s*$',first,re.I)
|
"source_page": page,
|
||||||
if not mb: continue
|
}
|
||||||
bal=amount(mb.group(1)); prefix=first[:mb.start()]
|
elif (
|
||||||
nums=list(re.finditer(r'(?<!\d)([\d,]+\.\d{2})(?!\d)',prefix))
|
current
|
||||||
txn=amount(nums[-1].group(1)) if nums else None
|
and line.strip()
|
||||||
debit=credit=None
|
and not re.match(
|
||||||
if txn is not None and prev is not None:
|
r"^(Value Post|Date Date|STATEMENT OF ACCOUNT|Page No)",
|
||||||
if abs((prev-txn)-bal)<0.05: debit=txn
|
line.strip(),
|
||||||
elif abs((prev+txn)-bal)<0.05: credit=txn
|
re.I,
|
||||||
if txn is not None and debit is None and credit is None:
|
)
|
||||||
# column location: DR before CR in legacy format
|
):
|
||||||
credit=txn if nums[-1].start()>70 else None; debit=txn if nums[-1].start()<=70 else None
|
current["lines"].append(line)
|
||||||
narr=alltxt
|
if current:
|
||||||
ref=''; z=re.search(r'(?:UPI|NEFT|IMPS|RTGS)[/A-Z0-9-]{6,}',alltxt,re.I); ref=z.group(0) if z else ''
|
rows.append(current)
|
||||||
out.append({**r,'narration':narr,'reference_no':ref,'debit':debit,'credit':credit,'balance':bal})
|
|
||||||
prev=bal
|
out = []
|
||||||
|
previous_balance = None
|
||||||
|
for row in rows:
|
||||||
|
first_line = row["lines"][0]
|
||||||
|
all_text = norm(" ".join(row["lines"]))
|
||||||
|
balance_match = re.search(
|
||||||
|
r"([\d,]+\.\d{2})(CR|DR)\s*$",
|
||||||
|
first_line,
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
if not balance_match:
|
||||||
|
continue
|
||||||
|
balance = _signed_amount(
|
||||||
|
balance_match.group(1),
|
||||||
|
balance_match.group(2),
|
||||||
|
)
|
||||||
|
prefix = first_line[: balance_match.start()]
|
||||||
|
numbers = list(
|
||||||
|
re.finditer(r"(?<!\d)([\d,]+\.\d{2})(?!\d)", prefix)
|
||||||
|
)
|
||||||
|
transaction_amount = (
|
||||||
|
amount(numbers[-1].group(1)) if numbers else None
|
||||||
|
)
|
||||||
|
debit = None
|
||||||
|
credit = None
|
||||||
|
if transaction_amount is not None and previous_balance is not None:
|
||||||
|
if abs((previous_balance - transaction_amount) - balance) < 0.05:
|
||||||
|
debit = transaction_amount
|
||||||
|
elif abs((previous_balance + transaction_amount) - balance) < 0.05:
|
||||||
|
credit = transaction_amount
|
||||||
|
if transaction_amount is not None and debit is None and credit is None:
|
||||||
|
credit = transaction_amount if numbers[-1].start() > 70 else None
|
||||||
|
debit = transaction_amount if numbers[-1].start() <= 70 else None
|
||||||
|
|
||||||
|
reference_no = ""
|
||||||
|
reference_match = re.search(
|
||||||
|
r"(?:UPI|NEFT|IMPS|RTGS)[/A-Z0-9-]{6,}",
|
||||||
|
all_text,
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
if reference_match:
|
||||||
|
reference_no = reference_match.group(0)
|
||||||
|
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
**row,
|
||||||
|
"narration": all_text,
|
||||||
|
"reference_no": reference_no,
|
||||||
|
"debit": debit,
|
||||||
|
"credit": credit,
|
||||||
|
"balance": balance,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
previous_balance = balance
|
||||||
|
|
||||||
if out and meta.opening_balance is None:
|
if out and meta.opening_balance is None:
|
||||||
first=out[0]; txn=(first.get('debit') or 0)-(first.get('credit') or 0); meta.opening_balance=round((first['balance'] or 0)+txn,2)
|
first = out[0]
|
||||||
|
transaction_amount = (first.get("debit") or 0) - (
|
||||||
|
first.get("credit") or 0
|
||||||
|
)
|
||||||
|
meta.opening_balance = round(
|
||||||
|
(first["balance"] or 0) + transaction_amount,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
|
||||||
return meta, finalize(pd.DataFrame(out), meta)
|
return meta, finalize(pd.DataFrame(out), meta)
|
||||||
|
|||||||
Reference in New Issue
Block a user