Finance-Accounts/ar-aging-app/backend/app/api/routes/accounts_summary.py

88 lines
3.4 KiB
Python

"""
Accounts Summary — the cross-month view of APPROVED journal entries.
One row per (month, marketplace): the journal's GL lines (accrual presentation — Transfer
excluded, Receivable = net revenue Dr A/R) in local currency plus the session's FX rate, so
the UI can show any marketplace across every month, or all marketplaces converted to USD.
Only months whose journal has been APPROVED appear — approval on the Journal Entry tab is
the publish step. A month drops out again if its sign-off is withdrawn, if it is
re-processed (the sign-off clears with the numbers), or if a month-end control blocks it.
"""
from __future__ import annotations
import json
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session as OrmSession
from ...core.journal import ACCRUAL_DISPLAY_EXCLUDES, LINE_ACCOUNTS, RECEIVABLE_KEY
from ...db import models
from ..deps import db_dep, is_blocked
router = APIRouter(prefix="/api", tags=["accounts-summary"])
@router.get("/accounts-summary")
def accounts_summary(db: OrmSession = Depends(db_dep)) -> dict:
from .ar import _journal_for, _market_list, fx_for
entries = (
db.query(models.JournalEntry, models.Session)
.join(models.Session, models.Session.id == models.JournalEntry.session_id)
.filter(models.JournalEntry.approved_by != "")
.order_by(models.Session.reporting_month, models.Session.id)
.all()
)
line_keys = [k for k, _ in LINE_ACCOUNTS if k not in ACCRUAL_DISPLAY_EXCLUDES]
months: list[dict] = []
cells: list[dict] = []
marketplaces: set[str] = set()
for j, s in entries:
if is_blocked(s) or not j.data:
continue # a blocked closing publishes nothing
payload = json.loads(j.data)
month = s.reporting_month or (s.month_end_date.isoformat()[:7]
if s.month_end_date else f"session-{s.id}")
months.append({
"month": month,
"session_id": s.id,
"session_name": s.name,
"reviewed_by": j.reviewed_by or "",
"approved_by": j.approved_by or "",
"approved_at": j.approved_at.isoformat() if j.approved_at else None,
"entry_no": j.entry_no or "",
})
for mkt in _market_list(payload):
sub, _ = _journal_for(payload, mkt)
rate, currency = fx_for(db, s.id, mkt)
values = {ln["key"]: ln["total"] for ln in sub.get("lines", [])
if ln["key"] not in ACCRUAL_DISPLAY_EXCLUDES}
accrual = sub.get("receivable_accrual")
if accrual is None:
# Payload stored before the accrual figure existed: derive it.
accrual_total = -round(sum(values.values()), 2)
else:
accrual_total = accrual["total"]
marketplaces.add(mkt)
cells.append({
"month": month,
"session_id": s.id,
"marketplace": mkt,
"currency": currency,
"fx_rate": rate,
"values": values, # line key -> local-currency total
"receivable": accrual_total, # Dr A/R (net revenue accrued)
})
return {
"available": bool(months),
"line_keys": line_keys,
"receivable_key": RECEIVABLE_KEY,
"months": months,
"marketplaces": sorted(marketplaces),
"cells": cells,
}