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:
|
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 {},
|
||||||
|
|||||||
Reference in New Issue
Block a user