Fix sundry creditors FIFO for debit opening balances
This commit is contained in:
@@ -479,6 +479,16 @@ def sundry_creditors_aging(
|
|||||||
# composition of the closing balance.
|
# composition of the closing balance.
|
||||||
closing_lots: list[dict[str, Any]] = []
|
closing_lots: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
# A Sundry Creditor ledger can legitimately open with either a credit
|
||||||
|
# balance (amount payable) or a debit balance (advance / sales-side net
|
||||||
|
# receivable in the same party ledger). The earlier FIFO code created
|
||||||
|
# a lot only for a positive opening balance and silently discarded a
|
||||||
|
# debit opening balance. That inflated the surviving creditor lots by
|
||||||
|
# exactly the unconsumed debit opening. Preserve both sides: a credit
|
||||||
|
# opening becomes the oldest payable lot; a debit opening becomes a
|
||||||
|
# carry-forward that offsets the earliest future creditor credits.
|
||||||
|
debit_carry = round(max(0.0, -opening_balance), 2)
|
||||||
|
|
||||||
if opening_balance > 0.009:
|
if opening_balance > 0.009:
|
||||||
closing_lots.append({
|
closing_lots.append({
|
||||||
"party_name": party_name,
|
"party_name": party_name,
|
||||||
@@ -494,7 +504,13 @@ def sundry_creditors_aging(
|
|||||||
"final_payment_date": "",
|
"final_payment_date": "",
|
||||||
})
|
})
|
||||||
|
|
||||||
def settle_fifo(lots: list[dict[str, Any]], amount: float) -> None:
|
def settle_fifo(lots: list[dict[str, Any]], amount: float) -> float:
|
||||||
|
"""Settle oldest payable lots and return any excess debit amount.
|
||||||
|
|
||||||
|
Excess debit is not discarded. In a mixed purchase/sales party
|
||||||
|
ledger it represents an advance / receivable that must offset the
|
||||||
|
next creditor-increasing credit before a new payable lot is born.
|
||||||
|
"""
|
||||||
remaining_payment = max(0.0, _money(amount))
|
remaining_payment = max(0.0, _money(amount))
|
||||||
for lot in lots:
|
for lot in lots:
|
||||||
if remaining_payment <= 0.009:
|
if remaining_payment <= 0.009:
|
||||||
@@ -505,6 +521,7 @@ def sundry_creditors_aging(
|
|||||||
applied = round(min(available, remaining_payment), 2)
|
applied = round(min(available, remaining_payment), 2)
|
||||||
lot["remaining"] = round(available - applied, 2)
|
lot["remaining"] = round(available - applied, 2)
|
||||||
remaining_payment = round(remaining_payment - applied, 2)
|
remaining_payment = round(remaining_payment - applied, 2)
|
||||||
|
return remaining_payment
|
||||||
|
|
||||||
for row in fy_rows:
|
for row in fy_rows:
|
||||||
side = str(row.get("dr_cr") or "").strip().upper()
|
side = str(row.get("dr_cr") or "").strip().upper()
|
||||||
@@ -512,6 +529,14 @@ def sundry_creditors_aging(
|
|||||||
if amount <= 0.009:
|
if amount <= 0.009:
|
||||||
continue
|
continue
|
||||||
if side == "CR":
|
if side == "CR":
|
||||||
|
# Consume any brought-forward / accumulated debit balance first.
|
||||||
|
# Only the residual credit creates a creditor FIFO lot.
|
||||||
|
if debit_carry > 0.009:
|
||||||
|
offset = round(min(debit_carry, amount), 2)
|
||||||
|
debit_carry = round(debit_carry - offset, 2)
|
||||||
|
amount = round(amount - offset, 2)
|
||||||
|
if amount <= 0.009:
|
||||||
|
continue
|
||||||
closing_lots.append({
|
closing_lots.append({
|
||||||
"party_name": party_name,
|
"party_name": party_name,
|
||||||
"source": "Voucher",
|
"source": "Voucher",
|
||||||
@@ -526,7 +551,9 @@ def sundry_creditors_aging(
|
|||||||
"final_payment_date": "",
|
"final_payment_date": "",
|
||||||
})
|
})
|
||||||
elif side == "DR":
|
elif side == "DR":
|
||||||
settle_fifo(closing_lots, amount)
|
excess_debit = settle_fifo(closing_lots, amount)
|
||||||
|
if excess_debit > 0.009:
|
||||||
|
debit_carry = round(debit_carry + excess_debit, 2)
|
||||||
|
|
||||||
# Remove fully settled lots and freeze the surviving FY-end outstanding
|
# Remove fully settled lots and freeze the surviving FY-end outstanding
|
||||||
# amounts before any optional follow-up-year settlement is applied.
|
# amounts before any optional follow-up-year settlement is applied.
|
||||||
@@ -538,11 +565,13 @@ def sundry_creditors_aging(
|
|||||||
lot["outstanding_at_fy_end"] = _money(lot.get("remaining"))
|
lot["outstanding_at_fy_end"] = _money(lot.get("remaining"))
|
||||||
|
|
||||||
fifo_total = round(sum(_money(lot.get("remaining")) for lot in closing_lots), 2)
|
fifo_total = round(sum(_money(lot.get("remaining")) for lot in closing_lots), 2)
|
||||||
if abs(fifo_total - closing_balance) > 0.01:
|
fifo_net = round(fifo_total - debit_carry, 2)
|
||||||
|
if abs(fifo_net - historical_closing) > 0.01 or abs(fifo_total - closing_balance) > 0.01:
|
||||||
raise AccountingMirrorError(
|
raise AccountingMirrorError(
|
||||||
f"FIFO movement reconstruction does not reconcile for {party_name}: "
|
f"FIFO movement reconstruction does not reconcile for {party_name}: "
|
||||||
f"selected-FY closing {closing_balance:.2f}, FIFO lots {fifo_total:.2f}. "
|
f"selected-FY closing {closing_balance:.2f}, FIFO lots {fifo_total:.2f}, "
|
||||||
"Refresh the Accounting Mirror for the selected FY and retry."
|
f"debit carry {debit_carry:.2f}. Refresh the Accounting Mirror for the "
|
||||||
|
"selected FY and retry."
|
||||||
)
|
)
|
||||||
|
|
||||||
closing_lots = sorted(
|
closing_lots = sorted(
|
||||||
|
|||||||
Reference in New Issue
Block a user