diff --git a/app/modules/accounting/accounting_mirror_service.py b/app/modules/accounting/accounting_mirror_service.py index 2eda17e..b7e6b85 100644 --- a/app/modules/accounting/accounting_mirror_service.py +++ b/app/modules/accounting/accounting_mirror_service.py @@ -272,11 +272,10 @@ def sundry_creditors_aging( At a reporting date, the unpaid closing balance is therefore represented by the *latest* credit-side additions, with any residual carried from opening. - We reconstruct that closing composition backwards from the authoritative - ledger closing balance. This is algebraically the same FIFO result as a - complete forward replay, but it is materially safer for an extracted mirror: - it cannot mis-age a closing balance merely because the opening balance is a - brought-forward aggregate rather than individual bills. + We first reconstruct the selected FY closing balance from the mirror opening + balance and selected-period movements, then reconstruct its unpaid composition + backwards. This avoids using ledger_master.closing_balance for a historical FY + because that master value can reflect a different/current reporting period. Tally bill-allocation references are not yet persisted in the .act mirror, so this remains a transparent FIFO analysis rather than Agst Ref/New Ref matching. @@ -358,20 +357,36 @@ def sundry_creditors_aging( key = party_name.casefold() meta = ledger_meta.get(key) or {} - # Sundry Creditor liability balances in the canonical mirror are credit - # balances (positive). Debit/zero closing balances are deliberately not - # reported as closing creditors. - closing_balance = max(0.0, _money(meta.get("closing_balance"))) + rows = by_ledger.get(key, []) + + # IMPORTANT: ledger_master.closing_balance is a master-level snapshot and + # is not reliable as the closing balance for an arbitrary historical FY. + # Reconstruct the selected FY closing from the mirror's opening balance and + # the selected-period creditor movements. This is the same balance control + # used by the first mirror implementation and keeps historical FY analysis + # tied to the voucher stream rather than the current ledger-master closing. + opening_balance = _money(meta.get("opening_balance")) + period_credit = round(sum( + _money(row.get("amount")) for row in rows + if str(row.get("dr_cr") or "").upper() == "CR" + ), 2) + period_debit = round(sum( + _money(row.get("amount")) for row in rows + if str(row.get("dr_cr") or "").upper() == "DR" + ), 2) + historical_closing = round(opening_balance + period_credit - period_debit, 2) + + # Only credit closing balances are Sundry Creditors for this report. + closing_balance = max(0.0, historical_closing) if closing_balance <= 0.009: continue - rows = by_ledger.get(key, []) credit_rows = [ row for row in rows if str(row.get("dr_cr") or "").upper() == "CR" and _money(row.get("amount")) > 0.009 ] - # Reverse reconstruction of the closing balance under FIFO: + # Reverse reconstruction of the selected FY closing balance under FIFO: # oldest balances are settled first, therefore the newest credits remain. amount_to_allocate = closing_balance lots_newest_first: list[dict[str, Any]] = [] @@ -488,7 +503,7 @@ def sundry_creditors_aging( "age_bucket": bucket, "paid_subsequently": paid, "balance_after_follow_up": after, - "allocation_basis": "FIFO reconstructed from Accounting Mirror closing balance", + "allocation_basis": "FIFO from selected-FY Accounting Mirror movements", }) # Date on which the party's >180 FY-end component became fully paid. @@ -499,12 +514,12 @@ def sundry_creditors_aging( final_payment_date = max(dates) reconstructed = round(within_180 + over_180, 2) - # Maintain the mirror ledger closing balance as the authoritative control. + # Maintain the selected-FY movement-derived closing balance as the control. variance = round(closing_balance - reconstructed, 2) if abs(variance) > 0.01: raise AccountingMirrorError( f"FIFO reconstruction does not reconcile for {party_name}: " - f"mirror closing {closing_balance:.2f}, reconstructed {reconstructed:.2f}." + f"selected-FY closing {closing_balance:.2f}, reconstructed {reconstructed:.2f}." ) parties.append({ @@ -530,9 +545,9 @@ def sundry_creditors_aging( "parties": parties, "details": details, "allocation_basis": ( - "FIFO reconstructed from the Accounting Mirror closing balance; " - "newest credit-side additions form the unpaid closing balance and " - "debit-side settlements are deemed to clear the oldest balance first" + "FIFO from selected-FY Accounting Mirror movements; the historical " + "closing balance is reconstructed from opening balance plus FY credits " + "less FY debits, and newest credit-side additions form the unpaid closing balance" ), "current_mirror": current.get("mirror") or {}, "follow_up_mirror": later.get("mirror") or {}, diff --git a/app/modules/accounting/templates/accounting/sundry_creditors_aging.html b/app/modules/accounting/templates/accounting/sundry_creditors_aging.html index f53f216..038c6ba 100644 --- a/app/modules/accounting/templates/accounting/sundry_creditors_aging.html +++ b/app/modules/accounting/templates/accounting/sundry_creditors_aging.html @@ -5,7 +5,7 @@
Accounting · Audit Analysis
Analyses closing Sundry Creditors into ≤180 days and >180 days from the local Accounting Mirror. The closing balance is reconstructed on FIFO: debit-side settlements clear the oldest balances first, so the newest credit-side additions form the unpaid closing balance. Bill-reference allocations are not yet stored in the .act mirror.
+Analyses closing Sundry Creditors into ≤180 days and >180 days from the local Accounting Mirror. The selected-FY closing balance and ageing are reconstructed from the Accounting Mirror on FIFO: debit-side settlements clear the oldest balances first, so the newest credit-side additions form the unpaid closing balance. Bill-reference allocations are not yet stored in the .act mirror.