Fix sundry creditors FIFO ageing from accounting mirror
This commit is contained in:
@@ -266,132 +266,255 @@ def sundry_creditors_aging(
|
|||||||
follow_up_end: str = "",
|
follow_up_end: str = "",
|
||||||
limit: int = 50000,
|
limit: int = 50000,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Age Sundry Creditors at FY close and optionally trace later settlement.
|
"""Age closing Sundry Creditors using FIFO from the Accounting Mirror.
|
||||||
|
|
||||||
The v3 Accounting Mirror does not yet persist Tally bill-allocation references.
|
FIFO means debit-side settlements consume the oldest creditor balance first.
|
||||||
Accordingly this routine performs a transparent FIFO allocation over the actual
|
At a reporting date, the unpaid closing balance is therefore represented by
|
||||||
creditor-ledger movements. Debit-side payments, sales and credit adjustments
|
the *latest* credit-side additions, with any residual carried from opening.
|
||||||
settle the oldest credit balance first.
|
|
||||||
|
We reconstruct that closing composition backwards from the authoritative
|
||||||
|
ledger closing balance. This is algebraically the same FIFO result as a
|
||||||
|
complete forward replay, but it is materially safer for an extracted mirror:
|
||||||
|
it cannot mis-age a closing balance merely because the opening balance is a
|
||||||
|
brought-forward aggregate rather than individual bills.
|
||||||
|
|
||||||
|
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.
|
||||||
"""
|
"""
|
||||||
from datetime import date as _date, timedelta as _timedelta
|
from datetime import date as _date, timedelta as _timedelta
|
||||||
|
|
||||||
current = _creditor_movements(
|
current = _creditor_movements(
|
||||||
node_code=node_code, accounting_payload=accounting_payload,
|
node_code=node_code,
|
||||||
from_date=fy_start, to_date=fy_end, limit=limit,
|
accounting_payload=accounting_payload,
|
||||||
|
from_date=fy_start,
|
||||||
|
to_date=fy_end,
|
||||||
|
limit=limit,
|
||||||
)
|
)
|
||||||
later = {"ledgers": [], "rows": []}
|
later = {"ledgers": [], "rows": []}
|
||||||
if follow_up_accounting_payload is not None and follow_up_start and follow_up_end:
|
if follow_up_accounting_payload is not None and follow_up_start and follow_up_end:
|
||||||
later = _creditor_movements(
|
later = _creditor_movements(
|
||||||
node_code=node_code, accounting_payload=follow_up_accounting_payload,
|
node_code=node_code,
|
||||||
from_date=follow_up_start, to_date=follow_up_end, limit=limit,
|
accounting_payload=follow_up_accounting_payload,
|
||||||
|
from_date=follow_up_start,
|
||||||
|
to_date=follow_up_end,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
if current.get("truncated"):
|
||||||
|
raise AccountingMirrorError(
|
||||||
|
"Sundry Creditor movement extraction reached the safety limit. "
|
||||||
|
"Increase the mirror query limit before relying on this analysis."
|
||||||
|
)
|
||||||
|
if later.get("truncated"):
|
||||||
|
raise AccountingMirrorError(
|
||||||
|
"Follow-up Sundry Creditor movement extraction reached the safety limit. "
|
||||||
|
"Increase the mirror query limit before relying on the subsequent-payment analysis."
|
||||||
)
|
)
|
||||||
|
|
||||||
fy_start_date = _date.fromisoformat(fy_start)
|
fy_start_date = _date.fromisoformat(fy_start)
|
||||||
fy_end_date = _date.fromisoformat(fy_end)
|
fy_end_date = _date.fromisoformat(fy_end)
|
||||||
opening_date = fy_start_date - _timedelta(days=1)
|
opening_date = fy_start_date - _timedelta(days=1)
|
||||||
ledger_meta = {str(x.get("ledger_name") or "").casefold(): x for x in (current.get("ledgers") or [])}
|
|
||||||
current_rows = current.get("rows") or []
|
def _money(value: Any) -> float:
|
||||||
later_rows = later.get("rows") or []
|
try:
|
||||||
|
return round(float(value or 0), 2)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def _row_date(row: dict[str, Any]) -> str:
|
||||||
|
return str(row.get("voucher_date") or "").strip()
|
||||||
|
|
||||||
|
ledger_meta = {
|
||||||
|
str(x.get("ledger_name") or "").casefold(): x
|
||||||
|
for x in (current.get("ledgers") or [])
|
||||||
|
if str(x.get("ledger_name") or "").strip()
|
||||||
|
}
|
||||||
|
|
||||||
by_ledger: dict[str, list[dict[str, Any]]] = {}
|
by_ledger: dict[str, list[dict[str, Any]]] = {}
|
||||||
for row in current_rows:
|
for row in current.get("rows") or []:
|
||||||
name = str(row.get("ledger_name") or "").strip()
|
name = str(row.get("ledger_name") or "").strip()
|
||||||
if name:
|
if name:
|
||||||
by_ledger.setdefault(name, []).append(row)
|
by_ledger.setdefault(name.casefold(), []).append(row)
|
||||||
|
|
||||||
later_by_ledger: dict[str, list[dict[str, Any]]] = {}
|
later_by_ledger: dict[str, list[dict[str, Any]]] = {}
|
||||||
for row in later_rows:
|
for row in later.get("rows") or []:
|
||||||
name = str(row.get("ledger_name") or "").strip()
|
name = str(row.get("ledger_name") or "").strip()
|
||||||
if name:
|
if name:
|
||||||
later_by_ledger.setdefault(name, []).append(row)
|
later_by_ledger.setdefault(name.casefold(), []).append(row)
|
||||||
|
|
||||||
parties: list[dict[str, Any]] = []
|
parties: list[dict[str, Any]] = []
|
||||||
details: list[dict[str, Any]] = []
|
details: list[dict[str, Any]] = []
|
||||||
for party_name in sorted({str(x.get("ledger_name") or "").strip() for x in current.get("ledgers") or [] if str(x.get("ledger_name") or "").strip()}, key=str.casefold):
|
|
||||||
meta = ledger_meta.get(party_name.casefold()) or {}
|
|
||||||
opening_raw = float(meta.get("opening_balance") or 0)
|
|
||||||
opening_credit = max(0.0, opening_raw)
|
|
||||||
lots: list[dict[str, Any]] = []
|
|
||||||
if opening_credit > 0.009:
|
|
||||||
lots.append({
|
|
||||||
"party_name": party_name, "source": "Opening Balance", "voucher_date": opening_date.isoformat(),
|
|
||||||
"voucher_type": "Opening", "voucher_number": "", "reference": "",
|
|
||||||
"original_credit": opening_credit, "remaining": opening_credit,
|
|
||||||
"outstanding_at_fy_end": 0.0, "paid_subsequently": 0.0, "final_payment_date": "",
|
|
||||||
})
|
|
||||||
|
|
||||||
def settle(amount: float, paid_on: str, track_later: bool = False):
|
party_names = sorted(
|
||||||
remaining = max(0.0, float(amount or 0))
|
{
|
||||||
for lot in lots:
|
str(x.get("ledger_name") or "").strip()
|
||||||
if remaining <= 0.009:
|
for x in (current.get("ledgers") or [])
|
||||||
|
if str(x.get("ledger_name") or "").strip()
|
||||||
|
},
|
||||||
|
key=str.casefold,
|
||||||
|
)
|
||||||
|
|
||||||
|
for party_name in party_names:
|
||||||
|
key = party_name.casefold()
|
||||||
|
meta = ledger_meta.get(key) or {}
|
||||||
|
|
||||||
|
# Sundry Creditor liability balances in the canonical mirror are credit
|
||||||
|
# balances (positive). Debit/zero closing balances are deliberately not
|
||||||
|
# reported as closing creditors.
|
||||||
|
closing_balance = max(0.0, _money(meta.get("closing_balance")))
|
||||||
|
if closing_balance <= 0.009:
|
||||||
|
continue
|
||||||
|
|
||||||
|
rows = by_ledger.get(key, [])
|
||||||
|
credit_rows = [
|
||||||
|
row for row in rows
|
||||||
|
if str(row.get("dr_cr") or "").upper() == "CR" and _money(row.get("amount")) > 0.009
|
||||||
|
]
|
||||||
|
|
||||||
|
# Reverse reconstruction of the closing balance under FIFO:
|
||||||
|
# oldest balances are settled first, therefore the newest credits remain.
|
||||||
|
amount_to_allocate = closing_balance
|
||||||
|
lots_newest_first: list[dict[str, Any]] = []
|
||||||
|
for row in sorted(
|
||||||
|
credit_rows,
|
||||||
|
key=lambda r: (_row_date(r), int(r.get("line_no") or 0)),
|
||||||
|
reverse=True,
|
||||||
|
):
|
||||||
|
if amount_to_allocate <= 0.009:
|
||||||
break
|
break
|
||||||
available = float(lot.get("remaining") or 0)
|
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": _row_date(row),
|
||||||
|
"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)
|
||||||
|
|
||||||
|
# Anything not represented by current-year credits is necessarily a
|
||||||
|
# brought-forward closing component and is old at this FY close.
|
||||||
|
if amount_to_allocate > 0.009:
|
||||||
|
lots_newest_first.append({
|
||||||
|
"party_name": party_name,
|
||||||
|
"source": "Opening / brought forward",
|
||||||
|
"voucher_date": opening_date.isoformat(),
|
||||||
|
"voucher_type": "Opening",
|
||||||
|
"voucher_number": "",
|
||||||
|
"reference": "",
|
||||||
|
"original_credit": amount_to_allocate,
|
||||||
|
"remaining": amount_to_allocate,
|
||||||
|
"outstanding_at_fy_end": amount_to_allocate,
|
||||||
|
"paid_subsequently": 0.0,
|
||||||
|
"final_payment_date": "",
|
||||||
|
})
|
||||||
|
amount_to_allocate = 0.0
|
||||||
|
|
||||||
|
# FIFO settlement in the follow-up period must consume the oldest FY-end
|
||||||
|
# lots first. Later credits are new liabilities and do not change which
|
||||||
|
# FY-end lot a FIFO debit settles.
|
||||||
|
closing_lots = sorted(
|
||||||
|
lots_newest_first,
|
||||||
|
key=lambda lot: (str(lot.get("voucher_date") or ""), str(lot.get("voucher_number") or "")),
|
||||||
|
)
|
||||||
|
|
||||||
|
def settle_follow_up(amount: float, paid_on: str) -> None:
|
||||||
|
remaining_payment = max(0.0, _money(amount))
|
||||||
|
for lot in closing_lots:
|
||||||
|
if remaining_payment <= 0.009:
|
||||||
|
break
|
||||||
|
available = max(0.0, _money(lot.get("remaining")))
|
||||||
if available <= 0.009:
|
if available <= 0.009:
|
||||||
continue
|
continue
|
||||||
applied = min(available, remaining)
|
applied = round(min(available, remaining_payment), 2)
|
||||||
lot["remaining"] = round(available - applied, 2)
|
lot["remaining"] = round(available - applied, 2)
|
||||||
remaining = round(remaining - applied, 2)
|
lot["paid_subsequently"] = round(_money(lot.get("paid_subsequently")) + applied, 2)
|
||||||
if track_later and lot.get("outstanding_at_fy_end", 0) > 0:
|
remaining_payment = round(remaining_payment - applied, 2)
|
||||||
eligible = max(0.0, float(lot["outstanding_at_fy_end"]) - float(lot.get("paid_subsequently") or 0))
|
if _money(lot.get("remaining")) <= 0.009:
|
||||||
later_applied = min(eligible, applied)
|
|
||||||
if later_applied > 0:
|
|
||||||
lot["paid_subsequently"] = round(float(lot.get("paid_subsequently") or 0) + later_applied, 2)
|
|
||||||
if float(lot["outstanding_at_fy_end"]) - float(lot["paid_subsequently"]) <= 0.009:
|
|
||||||
lot["final_payment_date"] = paid_on
|
lot["final_payment_date"] = paid_on
|
||||||
|
|
||||||
for row in sorted(by_ledger.get(party_name, []), key=lambda r: (str(r.get("voucher_date") or ""), int(r.get("line_no") or 0))):
|
for row in sorted(
|
||||||
amount = abs(float(row.get("amount") or 0))
|
later_by_ledger.get(key, []),
|
||||||
if amount <= 0.009:
|
key=lambda r: (_row_date(r), int(r.get("line_no") or 0)),
|
||||||
continue
|
):
|
||||||
side = str(row.get("dr_cr") or "").upper()
|
|
||||||
if side == "CR":
|
|
||||||
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(amount, str(row.get("voucher_date") or ""), False)
|
|
||||||
|
|
||||||
closing_lots = [lot for lot in lots if float(lot.get("remaining") or 0) > 0.009]
|
|
||||||
if not closing_lots:
|
|
||||||
continue
|
|
||||||
for lot in closing_lots:
|
|
||||||
lot["outstanding_at_fy_end"] = round(float(lot["remaining"]), 2)
|
|
||||||
|
|
||||||
# Trace subsequent debit settlements only. Later credits create new liabilities
|
|
||||||
# and must not alter the settlement status of FY-end lots.
|
|
||||||
for row in sorted(later_by_ledger.get(party_name, []), key=lambda r: (str(r.get("voucher_date") or ""), int(r.get("line_no") or 0))):
|
|
||||||
if str(row.get("dr_cr") or "").upper() == "DR":
|
if str(row.get("dr_cr") or "").upper() == "DR":
|
||||||
settle(abs(float(row.get("amount") or 0)), str(row.get("voucher_date") or ""), True)
|
settle_follow_up(_money(row.get("amount")), _row_date(row))
|
||||||
|
|
||||||
|
within_180 = 0.0
|
||||||
|
over_180 = 0.0
|
||||||
|
paid_later = 0.0
|
||||||
|
still_unpaid = 0.0
|
||||||
|
over_180_lots: list[dict[str, Any]] = []
|
||||||
|
|
||||||
within_180 = over_180 = paid_later = still_unpaid = 0.0
|
|
||||||
final_dates: list[str] = []
|
|
||||||
for lot in closing_lots:
|
for lot in closing_lots:
|
||||||
lot_date = _date.fromisoformat(str(lot["voucher_date"]))
|
try:
|
||||||
|
lot_date = _date.fromisoformat(str(lot.get("voucher_date") or ""))
|
||||||
|
except ValueError:
|
||||||
|
# A malformed/missing date must not be classified as current.
|
||||||
|
lot_date = opening_date
|
||||||
age_days = (fy_end_date - lot_date).days
|
age_days = (fy_end_date - lot_date).days
|
||||||
bucket = ">180 Days" if age_days > 180 else "≤180 Days"
|
bucket = ">180 Days" if age_days > 180 else "≤180 Days"
|
||||||
outstanding = round(float(lot["outstanding_at_fy_end"]), 2)
|
outstanding = _money(lot.get("outstanding_at_fy_end"))
|
||||||
paid = round(min(outstanding, float(lot.get("paid_subsequently") or 0)), 2)
|
paid = round(min(outstanding, _money(lot.get("paid_subsequently"))), 2)
|
||||||
after = round(max(0.0, outstanding - paid), 2)
|
after = round(max(0.0, outstanding - paid), 2)
|
||||||
|
|
||||||
if bucket == ">180 Days":
|
if bucket == ">180 Days":
|
||||||
over_180 += outstanding
|
over_180 = round(over_180 + outstanding, 2)
|
||||||
paid_later += paid
|
paid_later = round(paid_later + paid, 2)
|
||||||
still_unpaid += after
|
still_unpaid = round(still_unpaid + after, 2)
|
||||||
|
over_180_lots.append(lot)
|
||||||
else:
|
else:
|
||||||
within_180 += outstanding
|
within_180 = round(within_180 + outstanding, 2)
|
||||||
if lot.get("final_payment_date"):
|
|
||||||
final_dates.append(str(lot["final_payment_date"]))
|
|
||||||
details.append({
|
details.append({
|
||||||
**{k: lot.get(k) for k in ("party_name","source","voucher_date","voucher_type","voucher_number","reference","original_credit","final_payment_date")},
|
**{
|
||||||
"outstanding_at_fy_end": outstanding, "age_days": age_days, "age_bucket": bucket,
|
k: lot.get(k)
|
||||||
"paid_subsequently": paid, "balance_after_follow_up": after, "allocation_basis": "FIFO from Accounting Mirror movements",
|
for k in (
|
||||||
|
"party_name", "source", "voucher_date", "voucher_type",
|
||||||
|
"voucher_number", "reference", "original_credit", "final_payment_date",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
"outstanding_at_fy_end": outstanding,
|
||||||
|
"age_days": age_days,
|
||||||
|
"age_bucket": bucket,
|
||||||
|
"paid_subsequently": paid,
|
||||||
|
"balance_after_follow_up": after,
|
||||||
|
"allocation_basis": "FIFO reconstructed from Accounting Mirror closing balance",
|
||||||
})
|
})
|
||||||
closing = round(within_180 + over_180, 2)
|
|
||||||
|
# Date on which the party's >180 FY-end component became fully paid.
|
||||||
|
final_payment_date = ""
|
||||||
|
if over_180_lots and all(_money(lot.get("remaining")) <= 0.009 for lot in over_180_lots):
|
||||||
|
dates = [str(lot.get("final_payment_date") or "") for lot in over_180_lots if lot.get("final_payment_date")]
|
||||||
|
if dates:
|
||||||
|
final_payment_date = max(dates)
|
||||||
|
|
||||||
|
reconstructed = round(within_180 + over_180, 2)
|
||||||
|
# Maintain the mirror ledger closing balance as the authoritative control.
|
||||||
|
variance = round(closing_balance - reconstructed, 2)
|
||||||
|
if abs(variance) > 0.01:
|
||||||
|
raise AccountingMirrorError(
|
||||||
|
f"FIFO reconstruction does not reconcile for {party_name}: "
|
||||||
|
f"mirror closing {closing_balance:.2f}, reconstructed {reconstructed:.2f}."
|
||||||
|
)
|
||||||
|
|
||||||
parties.append({
|
parties.append({
|
||||||
"party_name": party_name, "within_180": round(within_180,2), "over_180": round(over_180,2),
|
"party_name": party_name,
|
||||||
"closing_balance": closing, "over_180_paid_later": round(paid_later,2),
|
"within_180": within_180,
|
||||||
"over_180_still_unpaid": round(still_unpaid,2), "final_payment_date": max(final_dates) if final_dates else "",
|
"over_180": over_180,
|
||||||
|
"closing_balance": closing_balance,
|
||||||
|
"over_180_paid_later": paid_later,
|
||||||
|
"over_180_still_unpaid": still_unpaid,
|
||||||
|
"final_payment_date": final_payment_date,
|
||||||
})
|
})
|
||||||
|
|
||||||
summary = {
|
summary = {
|
||||||
@@ -403,7 +526,14 @@ def sundry_creditors_aging(
|
|||||||
"over_180_still_unpaid": round(sum(x["over_180_still_unpaid"] for x in parties), 2),
|
"over_180_still_unpaid": round(sum(x["over_180_still_unpaid"] for x in parties), 2),
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
"summary": summary, "parties": parties, "details": details,
|
"summary": summary,
|
||||||
"allocation_basis": "FIFO from Accounting Mirror movements; debit-side payments/sales/credit adjustments settle oldest balances first",
|
"parties": parties,
|
||||||
"current_mirror": current.get("mirror") or {}, "follow_up_mirror": later.get("mirror") or {},
|
"details": details,
|
||||||
|
"allocation_basis": (
|
||||||
|
"FIFO reconstructed from the Accounting Mirror closing balance; "
|
||||||
|
"newest credit-side additions form the unpaid closing balance and "
|
||||||
|
"debit-side settlements are deemed to clear the oldest balance first"
|
||||||
|
),
|
||||||
|
"current_mirror": current.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 >180 days from the local Accounting Mirror. Settlements use FIFO because bill-reference allocations are not yet stored in the .act mirror. Debit-side payments, sales and credit adjustments in the same creditor ledger reduce the oldest outstanding balance first.</p>
|
<p class="mt-1 max-w-4xl text-sm text-slate-500">Analyses closing Sundry Creditors into ≤180 days and >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>
|
||||||
</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>
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details class="rounded-2xl border border-slate-200 bg-white" open><summary class="cursor-pointer px-5 py-4 font-semibold text-slate-800">FIFO bill / balance detail ({{ analysis.details|length }})</summary>
|
<details class="rounded-2xl border border-slate-200 bg-white" open><summary class="cursor-pointer px-5 py-4 font-semibold text-slate-800">FIFO closing-balance detail ({{ analysis.details|length }})</summary>
|
||||||
<div class="overflow-x-auto border-t border-slate-100"><table class="min-w-full text-xs"><thead class="bg-slate-50 text-left font-semibold uppercase tracking-wide text-slate-600"><tr><th class="px-3 py-2">Party</th><th class="px-3 py-2">Source</th><th class="px-3 py-2">Date</th><th class="px-3 py-2">Voucher</th><th class="px-3 py-2 text-right">Original</th><th class="px-3 py-2 text-right">FY Closing</th><th class="px-3 py-2 text-right">Age</th><th class="px-3 py-2">Bucket</th><th class="px-3 py-2 text-right">Paid Later</th><th class="px-3 py-2 text-right">Follow-up Balance</th><th class="px-3 py-2">Final Paid</th></tr></thead><tbody class="divide-y divide-slate-100">{% for row in analysis.details %}<tr><td class="px-3 py-2">{{ row.party_name }}</td><td class="px-3 py-2">{{ row.source }}</td><td class="px-3 py-2">{{ row.voucher_date }}</td><td class="px-3 py-2">{{ row.voucher_type }} {{ row.voucher_number }}</td><td class="px-3 py-2 text-right">{{ '%.2f'|format(row.original_credit) }}</td><td class="px-3 py-2 text-right">{{ '%.2f'|format(row.outstanding_at_fy_end) }}</td><td class="px-3 py-2 text-right">{{ row.age_days }}</td><td class="px-3 py-2 {% if row.age_bucket == '>180 Days' %}font-semibold text-rose-700{% endif %}">{{ row.age_bucket }}</td><td class="px-3 py-2 text-right">{{ '%.2f'|format(row.paid_subsequently) }}</td><td class="px-3 py-2 text-right">{{ '%.2f'|format(row.balance_after_follow_up) }}</td><td class="px-3 py-2">{{ row.final_payment_date or '—' }}</td></tr>{% endfor %}</tbody></table></div>
|
<div class="overflow-x-auto border-t border-slate-100"><table class="min-w-full text-xs"><thead class="bg-slate-50 text-left font-semibold uppercase tracking-wide text-slate-600"><tr><th class="px-3 py-2">Party</th><th class="px-3 py-2">Source</th><th class="px-3 py-2">Date</th><th class="px-3 py-2">Voucher</th><th class="px-3 py-2 text-right">Original</th><th class="px-3 py-2 text-right">FY Closing</th><th class="px-3 py-2 text-right">Age</th><th class="px-3 py-2">Bucket</th><th class="px-3 py-2 text-right">Paid Later</th><th class="px-3 py-2 text-right">Follow-up Balance</th><th class="px-3 py-2">Final Paid</th></tr></thead><tbody class="divide-y divide-slate-100">{% for row in analysis.details %}<tr><td class="px-3 py-2">{{ row.party_name }}</td><td class="px-3 py-2">{{ row.source }}</td><td class="px-3 py-2">{{ row.voucher_date }}</td><td class="px-3 py-2">{{ row.voucher_type }} {{ row.voucher_number }}</td><td class="px-3 py-2 text-right">{{ '%.2f'|format(row.original_credit) }}</td><td class="px-3 py-2 text-right">{{ '%.2f'|format(row.outstanding_at_fy_end) }}</td><td class="px-3 py-2 text-right">{{ row.age_days }}</td><td class="px-3 py-2 {% if row.age_bucket == '>180 Days' %}font-semibold text-rose-700{% endif %}">{{ row.age_bucket }}</td><td class="px-3 py-2 text-right">{{ '%.2f'|format(row.paid_subsequently) }}</td><td class="px-3 py-2 text-right">{{ '%.2f'|format(row.balance_after_follow_up) }}</td><td class="px-3 py-2">{{ row.final_payment_date or '—' }}</td></tr>{% endfor %}</tbody></table></div>
|
||||||
</details>
|
</details>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user