AccountedPro - Data sources and Server Scripts
This page is for developers. Everything else in this documentation is configuration; this is the one place a script is written.
A layout gets its rows one of two ways, chosen under List settings → Where the rows come from.
This DocType (the default)
Rows come from Frappe's own list query. Permissions apply exactly as they do everywhere else — including Advanced Permission scopes — because it is the same query the stock list runs.
Nothing needs writing. Filters, tabs, sorting and paging all work on their own.
A Server Script
Rows come from an API-type Server Script. Use this when the rows are computed rather than stored — an approval workload, a reconciliation view, anything that needs joins a list query cannot express.
| Setting | Meaning |
|---|---|
| API method | The Server Script's method name |
| Rows are under this key | Where the row array lives in the response, e.g. rows |
The app checks the script is API type and whitelisted before calling it.
What the script receives
The current filter values, by the Sends this argument name each filter declares, plus the active tab's arguments. A tab narrows a Server Script source through args rather than through filters, because there are no fieldnames to attach a filter to.
What it should return
# Server Script, type: API, name: order_workload
rows = frappe.get_all(
"Order Form",
fields=["name", "customer", "status", "grand_total"],
filters=build_filters(frappe.form_dict),
limit_page_length=int(frappe.form_dict.get("page_length") or 20),
)
for row in rows:
row["days_waiting"] = days_since(row) # a virtual field
frappe.response["message"] = {
"rows": rows,
"kpis": {"open": len(rows), "value": sum(r.grand_total for r in rows)},
"customer_options": ["ACME", "Globex"],
}
| Key | Read by |
|---|---|
rows |
The list itself — the name under Rows are under this key |
| Anything else | Virtual elements (row[key]), cards (A key from the data source), and link-filter options |
One call returns rows, KPI numbers and filter options together, which is the point: a dashboard that would otherwise be four round trips is one.
Virtual elements
An element of type Virtual (from data source) reads row[key] — a value the script computed. Colour rules, visibility and formatting all work on it exactly as on a stored field.
With a standard data source a virtual element instead names a Server Script that is called once per page with the visible row names and returns {name: value}. One round trip, never one per row.
The security boundary, stated plainly
A Server Script data source decides for itself what a reader may see. The app does not filter its rows afterwards, and cannot: it does not know what the script meant. If the script does not check permissions, the list does not check permissions.
The same is true of a Server Script action: it does whatever it was written to do. What the app guarantees is that the button's roles and row condition are re-checked on the server before the script is called — not what happens inside it.
The builder states both of these where the setting is, rather than leaving it to be discovered.
Writing one that behaves
- Respect the reader. Query with
frappe.get_list(which checks permissions) rather thanfrappe.db.sql, unless you are deliberately reporting across records the reader could not open one by one. - Page properly. Honour the page length and offset the app sends, or a large DocType will return everything on every keystroke.
- Return plain values. Formatting belongs to the layout — return
1250.0, not"$1,250.00", so a currency format and a colour rule can still work on it. - Keep it one call. If a KPI needs the same query as the rows, compute it while you have them and return it alongside.