Fix sundry creditors ageing with forward FIFO allocation
This commit is contained in:
@@ -472,57 +472,81 @@ def sundry_creditors_aging(
|
||||
if closing_balance <= 0.009:
|
||||
continue
|
||||
|
||||
credit_rows = [
|
||||
row for row in rows
|
||||
if str(row.get("dr_cr") or "").upper() == "CR"
|
||||
and _money(row.get("amount")) > 0.009
|
||||
]
|
||||
# True forward FIFO reconstruction. The selected-FY opening balance is
|
||||
# the oldest outstanding lot. Every creditor-increasing movement creates
|
||||
# a new dated lot; every creditor-reducing movement settles the oldest lot
|
||||
# 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,
|
||||
# so the newest credit lots are the ones that survive at the reporting date.
|
||||
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({
|
||||
if opening_balance > 0.009:
|
||||
closing_lots.append({
|
||||
"party_name": party_name,
|
||||
"source": "Opening / brought forward",
|
||||
"voucher_date": opening_fallback_date.isoformat(),
|
||||
"voucher_type": "Opening",
|
||||
"voucher_number": "",
|
||||
"reference": "",
|
||||
"original_credit": amount_to_allocate,
|
||||
"remaining": amount_to_allocate,
|
||||
"outstanding_at_fy_end": amount_to_allocate,
|
||||
"original_credit": opening_balance,
|
||||
"remaining": opening_balance,
|
||||
"outstanding_at_fy_end": 0.0,
|
||||
"paid_subsequently": 0.0,
|
||||
"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(
|
||||
lots_newest_first,
|
||||
closing_lots,
|
||||
key=lambda lot: (
|
||||
_parse_date(lot.get("voucher_date")) or opening_fallback_date,
|
||||
str(lot.get("voucher_number") or ""),
|
||||
@@ -633,7 +657,7 @@ def sundry_creditors_aging(
|
||||
"allocation_basis": (
|
||||
"FIFO from dated Accounting Mirror movements; historical closing is "
|
||||
"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 {},
|
||||
"follow_up_mirror": later.get("mirror") or {},
|
||||
|
||||
Reference in New Issue
Block a user