Fix historical FY sundry creditors FIFO ageing from accounting mirror

This commit is contained in:
A R R R Associates
2026-09-09 16:29:37 +05:30
parent 4073a93386
commit 078116f14f
2 changed files with 33 additions and 18 deletions
@@ -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 {},
@@ -5,7 +5,7 @@
<div>
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-brand-600">Accounting · Audit Analysis</p>
<h1 class="mt-1 text-2xl font-semibold text-slate-900">Sundry Creditors – 180 Day Analysis</h1>
<p class="mt-1 max-w-4xl text-sm text-slate-500">Analyses closing Sundry Creditors into ≤180 days and &gt;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.</p>
<p class="mt-1 max-w-4xl text-sm text-slate-500">Analyses closing Sundry Creditors into ≤180 days and &gt;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.</p>
</div>
<a href="/tools/tally{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Back to Tally Accounting</a>
</div>