88 lines
3.4 KiB
Python
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,
|
|
}
|