Fix multi-account bank reconciliation and output filename

This commit is contained in:
A R R R Associates
2026-07-20 22:17:25 +05:30
parent 7cf330b213
commit 0e931411d6
3 changed files with 90 additions and 8 deletions
@@ -483,6 +483,78 @@ def reconcile(metas, all_df):
return pd.DataFrame(rows)
def _account_portfolio_summary(metas, transactions):
"""Build account-level and portfolio reconciliation values.
Running-balance delta validates each statement row. This helper handles a
separate issue: a workbook may contain multiple bank accounts or multiple
statement segments for the same account. Opening and closing balances must
therefore be selected per account, not from the first and last metadata row
across the entire upload.
"""
def _key(meta):
account = str(meta.account_number or '').strip()
return (str(meta.bank_name or '').strip(), account or str(meta.source_file or '').strip())
groups = {}
for position, meta in enumerate(metas):
groups.setdefault(_key(meta), []).append((position, meta))
rows = []
for (bank_name, account_key), items in groups.items():
def _sort_key(item):
position, meta = item
start = pd.to_datetime(meta.period_from, errors='coerce')
end = pd.to_datetime(meta.period_to, errors='coerce')
start_key = start if pd.notna(start) else pd.Timestamp.max
end_key = end if pd.notna(end) else pd.Timestamp.max
return (start_key, end_key, position)
ordered = sorted(items, key=_sort_key)
first_meta = ordered[0][1]
last_meta = ordered[-1][1]
opening = next((m.opening_balance for _, m in ordered if m.opening_balance is not None), 0.0) or 0.0
closing = next((m.closing_balance for _, m in reversed(ordered) if m.closing_balance is not None), 0.0) or 0.0
account_number = str(first_meta.account_number or '').strip()
customer_name = next((str(m.customer_name).strip() for _, m in ordered if str(m.customer_name or '').strip()), '')
if transactions is None or transactions.empty:
data = pd.DataFrame()
else:
mask = transactions.get('bank_name', pd.Series('', index=transactions.index)).fillna('').astype(str).eq(bank_name)
if account_number:
mask &= transactions.get('account_number', pd.Series('', index=transactions.index)).fillna('').astype(str).eq(account_number)
else:
source_files = {str(m.source_file or '') for _, m in ordered}
mask &= transactions.get('source_file', pd.Series('', index=transactions.index)).fillna('').astype(str).isin(source_files)
data = transactions.loc[mask]
debit = float(pd.to_numeric(data.get('debit'), errors='coerce').fillna(0).sum()) if not data.empty else 0.0
credit = float(pd.to_numeric(data.get('credit'), errors='coerce').fillna(0).sum()) if not data.empty else 0.0
computed = round(float(opening) + credit - debit, 2)
difference = round(computed - float(closing), 2)
rows.append({
'bank_name': bank_name,
'customer_name': customer_name,
'account_number': account_number or account_key,
'statement_segments': len(ordered),
'period_from': next((m.period_from for _, m in ordered if m.period_from), ''),
'period_to': next((m.period_to for _, m in reversed(ordered) if m.period_to), ''),
'opening_balance': round(float(opening), 2),
'total_debit': round(debit, 2),
'total_credit': round(credit, 2),
'computed_closing_balance': computed,
'statement_closing_balance': round(float(closing), 2),
'difference': difference,
'status': 'Reconciled' if abs(difference) <= 0.01 else 'Review',
})
frame = pd.DataFrame(rows)
portfolio_opening = float(frame['opening_balance'].sum()) if not frame.empty else 0.0
portfolio_closing = float(frame['statement_closing_balance'].sum()) if not frame.empty else 0.0
return frame, round(portfolio_opening, 2), round(portfolio_closing, 2)
def monthly_summary(df):
if df.empty:
return pd.DataFrame()
@@ -546,8 +618,7 @@ def draft_financials(df, metas):
other_receipts = float(sum(categories.loc[c, "credit"] for c in categories.index if c not in receipt_categories and categories.loc[c, "credit"] > 0))
classified_payments = float(sum(categories.loc[c, "debit"] for c in payment_categories if c in categories.index))
other_payments = float(sum(categories.loc[c, "debit"] for c in categories.index if c not in payment_categories and categories.loc[c, "debit"] > 0))
opening = next((meta.opening_balance for meta in metas if meta.opening_balance is not None), None)
closing = next((meta.closing_balance for meta in reversed(metas) if meta.closing_balance is not None), None)
_account_recon, opening, closing = _account_portfolio_summary(metas, df)
return pd.DataFrame(
[
["Potential business receipts / collections", business_receipts, "Narration-based grouping of customer, cash and settlement credits", "Verify with GST/sales register and cash book"],
@@ -629,11 +700,12 @@ def export_excel(output, metas, all_df, unique_df, financial_year="", selected_b
all_df = _ensure_analysis_columns(all_df)
unique_df = _ensure_analysis_columns(unique_df)
recon = reconcile(metas, all_df)
customer = next((meta.customer_name for meta in metas if meta.customer_name), "")
account = next((meta.account_number for meta in metas if meta.account_number), "")
account_recon, opening, statement_closing = _account_portfolio_summary(metas, unique_df)
customer_names = sorted({str(meta.customer_name).strip() for meta in metas if str(meta.customer_name or '').strip()})
account_numbers = sorted({str(meta.account_number).strip() for meta in metas if str(meta.account_number or '').strip()})
customer = customer_names[0] if len(customer_names) == 1 else ("Multiple Clients" if customer_names else "")
account = account_numbers[0] if len(account_numbers) == 1 else (f"Multiple Accounts ({len(account_numbers)})" if account_numbers else "")
banks = ", ".join(sorted({meta.bank_name for meta in metas}))
opening = next((meta.opening_balance for meta in metas if meta.opening_balance is not None), 0.0) or 0.0
statement_closing = next((meta.closing_balance for meta in reversed(metas) if meta.closing_balance is not None), 0.0) or 0.0
tx = unique_df[_workbook_columns(unique_df)].copy() if not unique_df.empty else pd.DataFrame(columns=_workbook_columns(unique_df))
tx.insert(0, "row_id", range(1, len(tx) + 1))
@@ -717,6 +789,7 @@ def export_excel(output, metas, all_df, unique_df, financial_year="", selected_b
# Raw and Python-analysis sheets.
recon.to_excel(writer, sheet_name="Statement Reconciliation", index=False)
account_recon.to_excel(writer, sheet_name="Account Reconciliation", index=False)
raw_export.to_excel(writer, sheet_name="All Extracted Rows", index=False)
exact_export.to_excel(writer, sheet_name="Exact Duplicates", index=False)
possible_export.to_excel(writer, sheet_name="Possible Duplicates", index=False)