Fix sundry creditors ageing with forward FIFO allocation

This commit is contained in:
A R R R Associates
2026-09-09 17:53:53 +05:30
parent a1828d645b
commit 1a7563c9da
@@ -472,57 +472,81 @@ def sundry_creditors_aging(
if closing_balance <= 0.009: if closing_balance <= 0.009:
continue continue
credit_rows = [ # True forward FIFO reconstruction. The selected-FY opening balance is
row for row in rows # the oldest outstanding lot. Every creditor-increasing movement creates
if str(row.get("dr_cr") or "").upper() == "CR" # a new dated lot; every creditor-reducing movement settles the oldest lot
and _money(row.get("amount")) > 0.009 # first. The lots that survive at FY end are therefore the actual FIFO
] # composition of the closing balance.
closing_lots: list[dict[str, Any]] = []
# FIFO reverse reconstruction. FIFO settlements clear oldest balances, if opening_balance > 0.009:
# so the newest credit lots are the ones that survive at the reporting date. closing_lots.append({
amount_to_allocate = closing_balance
lots_newest_first: list[dict[str, Any]] = []
for row in reversed(credit_rows):
if amount_to_allocate <= 0.009:
break
source_amount = _money(row.get("amount"))
allocated = round(min(source_amount, amount_to_allocate), 2)
if allocated <= 0.009:
continue
lots_newest_first.append({
"party_name": party_name,
"source": "Voucher",
"voucher_date": str(row.get("voucher_date") or ""),
"voucher_type": str(row.get("voucher_type") or ""),
"voucher_number": str(row.get("voucher_number") or ""),
"reference": str(row.get("reference") or ""),
"original_credit": source_amount,
"remaining": allocated,
"outstanding_at_fy_end": allocated,
"paid_subsequently": 0.0,
"final_payment_date": "",
})
amount_to_allocate = round(amount_to_allocate - allocated, 2)
# If the closing cannot be fully represented by dated credit rows available
# in this mirror, the residual is a genuine brought-forward component.
if amount_to_allocate > 0.009:
lots_newest_first.append({
"party_name": party_name, "party_name": party_name,
"source": "Opening / brought forward", "source": "Opening / brought forward",
"voucher_date": opening_fallback_date.isoformat(), "voucher_date": opening_fallback_date.isoformat(),
"voucher_type": "Opening", "voucher_type": "Opening",
"voucher_number": "", "voucher_number": "",
"reference": "", "reference": "",
"original_credit": amount_to_allocate, "original_credit": opening_balance,
"remaining": amount_to_allocate, "remaining": opening_balance,
"outstanding_at_fy_end": amount_to_allocate, "outstanding_at_fy_end": 0.0,
"paid_subsequently": 0.0, "paid_subsequently": 0.0,
"final_payment_date": "", "final_payment_date": "",
}) })
def settle_fifo(lots: list[dict[str, Any]], amount: float) -> None:
remaining_payment = max(0.0, _money(amount))
for lot in lots:
if remaining_payment <= 0.009:
break
available = max(0.0, _money(lot.get("remaining")))
if available <= 0.009:
continue
applied = round(min(available, remaining_payment), 2)
lot["remaining"] = round(available - applied, 2)
remaining_payment = round(remaining_payment - applied, 2)
for row in fy_rows:
side = str(row.get("dr_cr") or "").strip().upper()
amount = _money(row.get("amount"))
if amount <= 0.009:
continue
if side == "CR":
closing_lots.append({
"party_name": party_name,
"source": "Voucher",
"voucher_date": str(row.get("voucher_date") or ""),
"voucher_type": str(row.get("voucher_type") or ""),
"voucher_number": str(row.get("voucher_number") or ""),
"reference": str(row.get("reference") or ""),
"original_credit": amount,
"remaining": amount,
"outstanding_at_fy_end": 0.0,
"paid_subsequently": 0.0,
"final_payment_date": "",
})
elif side == "DR":
settle_fifo(closing_lots, amount)
# Remove fully settled lots and freeze the surviving FY-end outstanding
# amounts before any optional follow-up-year settlement is applied.
closing_lots = [
lot for lot in closing_lots
if _money(lot.get("remaining")) > 0.009
]
for lot in closing_lots:
lot["outstanding_at_fy_end"] = _money(lot.get("remaining"))
fifo_total = round(sum(_money(lot.get("remaining")) for lot in closing_lots), 2)
if abs(fifo_total - closing_balance) > 0.01:
raise AccountingMirrorError(
f"FIFO movement reconstruction does not reconcile for {party_name}: "
f"selected-FY closing {closing_balance:.2f}, FIFO lots {fifo_total:.2f}. "
"Refresh the Accounting Mirror for the selected FY and retry."
)
closing_lots = sorted( closing_lots = sorted(
lots_newest_first, closing_lots,
key=lambda lot: ( key=lambda lot: (
_parse_date(lot.get("voucher_date")) or opening_fallback_date, _parse_date(lot.get("voucher_date")) or opening_fallback_date,
str(lot.get("voucher_number") or ""), str(lot.get("voucher_number") or ""),
@@ -633,7 +657,7 @@ def sundry_creditors_aging(
"allocation_basis": ( "allocation_basis": (
"FIFO from dated Accounting Mirror movements; historical closing is " "FIFO from dated Accounting Mirror movements; historical closing is "
"reconstructed from ledger opening plus selected-FY movements, " "reconstructed from ledger opening plus selected-FY movements, "
"and the newest surviving credit lots form the unpaid closing balance" "and debit-side settlements clear the oldest outstanding lots first"
), ),
"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 {},