Fix multi-account bank reconciliation and output filename
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user