Analytics
The Analytics section (Analytics in the navigation, also under Tell me) shows how a company
is doing: ten key figures against a comparison, four trends, four breakdowns and what moved.
Every number is computed on the server from the posted ledger (or, for the aging and the
customer and vendor breakdowns, from the posted documents), by report_line; the browser only
draws and formats what the API returns. The charts are plain SVG
(ADR-0022).
REST first: the screen calls four documented operations, each also an MCP tool, and nothing else.
| Operation | MCP tool | Gives |
|---|---|---|
GET /v1/entities/{entity}/analytics/series |
analytics_series |
One metric per month, quarter or year |
GET /v1/entities/{entity}/analytics/kpis |
analytics_kpis |
The ten KPIs with comparison and trend |
GET /v1/entities/{entity}/analytics/breakdowns |
analytics_breakdown |
Top customers, vendors, sales currencies, the expense mix |
GET /v1/entities/{entity}/analytics/insights |
analytics_insights |
Flux and unusual entries for any range, with a sentence per flagged account |
/analytics/flux and /analytics/unusual-entries (a fiscal period, for the close) are the same
rules for a period; insights generalises them to any range.
Metrics
The set is closed: metric takes one of these values and nothing else (no free-form queries).
Amounts are in the entity's functional currency. A flow is what moved in the period, a
stock is the balance at the period's end, a ratio is computed from the period's own
figures, an aging is the open documents as of the period's end. Sign rules: a flow is
added up in its natural direction (revenue credit less debit, expenses debit less credit), a
stock in the account's normal direction (assets debit less credit, liabilities credit less
debit), exactly as the trial balance and balance sheet do. Good news says which direction of
change is good (favourable in the KPI answer); Split whether by=report_line|account is
allowed.
| Metric | Kind | Unit | Definition | Good news | Split |
|---|---|---|---|---|---|
revenue |
flow | amount | Credit less debit of the accounts of type revenue: the P&L's total revenue. | up | yes |
expenses |
flow | amount | Debit less credit of the accounts of type expense: the P&L's total expenses. | down | yes |
net_income |
flow | amount | Revenue less expenses: the P&L's net income. | up | no |
cost_of_sales |
flow | amount | Debit less credit of the report line pl.cost_of_sales. |
down | yes |
operating_expenses |
flow | amount | Debit less credit of the report lines pl.opex.* and pl.depreciation. |
down | yes |
gross_margin_pct |
ratio | percent | (Revenue on pl.revenue less cost of sales) / revenue on pl.revenue x 100; empty when there is no such revenue. |
up | no |
cash |
stock | amount | Balance at the period's end of the report lines starting bs.assets.current.cash (the Cash figure of the Role Center). |
up | yes |
receivables |
stock | amount | Balance at the period's end of the report line bs.assets.current.receivables (the receivables control account in the ledger). |
neither | yes |
payables |
stock | amount | Balance at the period's end of the report line bs.liabilities.current.payables (the payables control account in the ledger). |
neither | yes |
unearned_revenue |
stock | amount | Balance at the period's end of the report line bs.liabilities.current.contract_liabilities: what customers paid or were billed for goods and services not yet delivered (contract liabilities, deferred revenue). |
neither | yes |
working_capital |
stock | amount | Report lines bs.assets.current.* less bs.liabilities.current.* at the period's end. |
up | no |
dso |
ratio | days | Open receivables without VAT at the period's end (from the documents: the carrying amount of each open customer invoice and credit note times its net total over its total) / revenue of the period x days in the period. | down | no |
dpo |
ratio | days | Open payables without VAT at the period's end (from the vendor bills, as the receivables for DSO) / purchases of the period x days in the period; purchases are cost of sales plus the operating expenses that are bought on account (pl.opex.* except pl.opex.personnel). Capital purchases on account are not in purchases. |
neither | no |
receivables_aging |
aging | amount | Open customer invoices less credit notes as of the period's end, in the functional currency at the booking rate, in the buckets of the aging report (current, 1-30, 31-60, 61-90, 90+ days past due). Equals totals_functional of GET /v1/entities/{entity}/reports/aging as of that day. |
down | no |
payables_aging |
aging | amount | Open vendor bills less credit notes as of the period's end, as the receivables aging. | neither | no |
Ratios have one decimal, rounded half up, and no value (null) where the denominator is zero.
The report lines are those of the chart of accounts the entity uses (pl.revenue,
pl.cost_of_sales, pl.opex.*, pl.depreciation, bs.assets.current.*,
bs.liabilities.current.*). The KPI Revenue is the P&L's total revenue, as on the Role
Center, while the gross margin uses only the revenue on pl.revenue (not interest or other
income). DSO and DPO use the length of the range in days and compare net with net: the numerator
is what is open without VAT, read from the documents (each open invoice, bill or credit note's
carrying amount times its net total over its total), the denominator net revenue or net purchases.
DPO's purchases are cost of sales and the operating expenses bought on account, so capital
purchases on account (a bill booked to a fixed asset) are not in them; while such a bill is open it
is in the numerator, which overstates DPO.
Periods and comparison
- Periods are calendar months, quarters or years (
granularity). The first and last period of a range are clamped to it, so the sum of a flow over the periods is the report's total for the range, and the last value of a stock is the trial balance as ofend. More than 60 periods is refused (422): ask for a coarser granularity. - Comparison (
compare):previousis the same number of whole months (or of days, when the range is not whole months) right before the range;year_agois the same dates one year earlier (29 February becomes 28 February, a month's last day stays its last day). The answer states the comparison range it used. - A delta is the value less the comparison (points for a percentage, days for days);
delta_pctis the delta as a percentage of the absolute comparison, for amounts only. - Calendar periods. Months, quarters and years are calendar ones, also for an entity with a
4-4-5 or 52/53-week fiscal year, whose periods do not coincide with them; the fiscal year to date
starts where the entity's fiscal year starts, and a fiscal period's own figures are the period
profit and loss (
reports/period-profit-and-loss). - The screen's selector offers a month, a quarter, the fiscal year to date and a custom range; it chooses the dates, the server does the arithmetic.
What ties to what
Each tie is a test (tests/erp/features/analytics/test_analytics.py) that reads the analytics endpoint and the
existing report for the same dates:
| Analytics | Equals |
|---|---|
revenue, expenses, net_income per month; their sum over the range |
profit-and-loss revenue, expenses, net_income for the month; for the range |
cost_of_sales, operating_expenses |
the sections of the profit and loss on those report lines |
cash, receivables, payables at a month end |
trial balance as of that day, the accounts on those report lines |
unearned_revenue at a month end |
balance sheet, the contract liabilities line |
working_capital at a month end |
balance sheet current assets less current liabilities |
receivables_aging, payables_aging at a month end |
totals_functional of GET .../reports/aging as of that day |
a KPI's value, comparison and spark |
the report for the same dates; the series of the same range |
| the expense mix | the expense sections of the profit and loss |
a customer, vendor or currency row's documents |
the count of the document list filtered by the same party or currency, dates and status=posted |
Aging as of a day. A payment or credit note made after the day has not settled anything yet:
the aging report and the aging trend count a settlement from the day of the payment or of the
credit note. "Open as of a day" has one definition (Position in modules/trade/open_items.py),
also read by the FX revaluation and the exposure view:
- the open amount in the document currency reserves pending allocations (a payment waiting for approval), so an item is not offered for payment twice;
- the functional amount (
open_functional,totals_functional) is the carrying amount the ledger holds: the booked total less the carrying amounts of the posted allocations, each rounded as it was booked. It ties to the revaluation, and to the control account until a revaluation moves it.
Reserved documents. A document that pending allocations reserve in full is listed all the same,
flagged reserved (open 0 in the document currency, its carrying amount in open_functional, its
due-date bucket), so the functional totals are complete and the document-currency totals do not change.
The aging ties to the control account. The report carries a reconciliation as of its day: the
ledger balance of the receivables or payables control account (control_balance), the aging's
totals_functional, their difference, and the part of it that is unrealised_fx: the period-end
revaluation postings (source fx) that sit on the control account, on no document, until their
auto-reversal. unexplained (the difference less the revaluation) is zero when the books are in order.
pending_settlements is a memo: payments waiting for approval are not booked, so they are in neither
control_balance nor totals_functional and explain no part of the difference. The screen shows the
line under the open items and under the aging chart.
Year-end closing entries. Profit-and-loss flows leave out entries of source close (the year-end
closing entry of ADR-0023), so the last month of a closed year keeps its revenue and result. Balances
include every entry, and an appropriation entry is read like any other. The rule sits in one place
(CLOSING_SOURCES in modules/closing_entries.py, shared with the reports); a year closed with the
year-end close keeps its income statement, and a trend that ends in the closing
month shows what the month earned.
Breakdowns
customers, vendors and currency are read from posted documents by document date, invoices
less credit notes, at the document's net total in the functional currency (proportional to the
booked total; a document in the functional currency is its net total exactly). Manual revenue
entries without an invoice are therefore not in them; the trend and the P&L have them.
expense_mix is read from the ledger by report line. Rows are largest first, at most limit
(default 10), the rest as one row other.
Insights
insights flags each account whose profit-and-loss movement over the range (or balance-sheet
balance at its end) differs from the comparison range by at least the materiality (default
1,000 in the functional currency) and 10%. Each flagged account has change (increase,
decrease, new, cleared) and one English sentence built from its numbers by a fixed
template, with no language model, so the same books always give the same words. The web UI
writes the same sentence from the numbers in the user's language. Unusual entries are the
entries with a risk flag (manual, created by an agent, round amount, large amount, weekend
date, reversal), largest first.
Performance
A series or the KPI strip reads the posted lines of the range with one aggregate query (a group
per account and period) and the balances before it with one more; the aging reads the open
documents once. The ledger query uses the partial index journal_entry_date_idx (entity, entry
date, posted entries); tests/erp/features/analytics/test_analytics.py reads its plan. Migration 0024 adds a
partial index on allocation (credit_note_id), for the credit-note side of settlements.