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 At a reporting date, the unpaid closing balance is therefore represented by
the *latest* credit-side additions, with any residual carried from opening. the *latest* credit-side additions, with any residual carried from opening.
We reconstruct that closing composition backwards from the authoritative We first reconstruct the selected FY closing balance from the mirror opening
ledger closing balance. This is algebraically the same FIFO result as a balance and selected-period movements, then reconstruct its unpaid composition
complete forward replay, but it is materially safer for an extracted mirror: backwards. This avoids using ledger_master.closing_balance for a historical FY
it cannot mis-age a closing balance merely because the opening balance is a because that master value can reflect a different/current reporting period.
brought-forward aggregate rather than individual bills.
Tally bill-allocation references are not yet persisted in the .act mirror, so 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. 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() key = party_name.casefold()
meta = ledger_meta.get(key) or {} meta = ledger_meta.get(key) or {}
# Sundry Creditor liability balances in the canonical mirror are credit rows = by_ledger.get(key, [])
# balances (positive). Debit/zero closing balances are deliberately not
# reported as closing creditors. # IMPORTANT: ledger_master.closing_balance is a master-level snapshot and
closing_balance = max(0.0, _money(meta.get("closing_balance"))) # 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: if closing_balance <= 0.009:
continue continue
rows = by_ledger.get(key, [])
credit_rows = [ credit_rows = [
row for row in rows row for row in rows
if str(row.get("dr_cr") or "").upper() == "CR" and _money(row.get("amount")) > 0.009 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. # oldest balances are settled first, therefore the newest credits remain.
amount_to_allocate = closing_balance amount_to_allocate = closing_balance
lots_newest_first: list[dict[str, Any]] = [] lots_newest_first: list[dict[str, Any]] = []
@@ -488,7 +503,7 @@ def sundry_creditors_aging(
"age_bucket": bucket, "age_bucket": bucket,
"paid_subsequently": paid, "paid_subsequently": paid,
"balance_after_follow_up": after, "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. # 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) final_payment_date = max(dates)
reconstructed = round(within_180 + over_180, 2) 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) variance = round(closing_balance - reconstructed, 2)
if abs(variance) > 0.01: if abs(variance) > 0.01:
raise AccountingMirrorError( raise AccountingMirrorError(
f"FIFO reconstruction does not reconcile for {party_name}: " 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({ parties.append({
@@ -530,9 +545,9 @@ def sundry_creditors_aging(
"parties": parties, "parties": parties,
"details": details, "details": details,
"allocation_basis": ( "allocation_basis": (
"FIFO reconstructed from the Accounting Mirror closing balance; " "FIFO from selected-FY Accounting Mirror movements; the historical "
"newest credit-side additions form the unpaid closing balance and " "closing balance is reconstructed from opening balance plus FY credits "
"debit-side settlements are deemed to clear the oldest balance first" "less FY debits, and newest credit-side additions form the unpaid closing balance"
), ),
"current_mirror": current.get("mirror") or {}, "current_mirror": current.get("mirror") or {},
"follow_up_mirror": later.get("mirror") or {}, "follow_up_mirror": later.get("mirror") or {},
@@ -5,7 +5,7 @@
<div> <div>
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-brand-600">Accounting · Audit Analysis</p> <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> <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> </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> <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> </div>