Dynamic Report
Overview
Dynamic Report is the applet for questions the fixed report applets do not answer. You write a
SELECT against the tenant database, save it as a query template or straight into a dynamic
report, run it, and BigLedger keeps the result set — as rows you can browse in a grid and as a CSV
file — under a run. A dashboard is a canvas of blocks (bar, line, pie, area, scatter, donut,
column, heatmap, score card or table) that each read one report’s stored result.
It is a read-only tool by design: the backend accepts only statements that begin with SELECT
or WITH, rejects the write keywords, and refuses ten login and permission tables outright. It
writes nothing to the ledger, the stock balance or any business document. Open it when an
accountant or analyst needs a one-off cut of the data; open the
Financial Report,
Sales Report or
Purchase Report applets for the
pre-built ones.
DynamicReport — with a trailing space. It is an ACTIVE
TNT-USER row created 2025-08-27; an older TNT-APPLET row named Dynamic Report (no trailing
space, staging bundle) is INACTIVE. Anything that matches on the code — permission seeding, the
catalogue export, this page’s applet_code — has to reproduce the space exactly.Where it fits
| Module | Cross-cutting. Filed under Finance because it reads the same tables the Financial Report and Debtor and Creditor Report applets read, and because it has no document flow of its own |
| Reads | Any table in the tenant database that the SQL names, except the ten in the restricted list below |
| Writes | Its own tables only: bl_bi_report_hdr, bl_bi_report_query_template_hdr, bl_bi_report_query_run_hdr, bl_bi_report_query_run_output_hdr, bl_bi_report_query_run_output_file_hdr, bl_bi_report_query_event_hdr, bl_bi_dashboard, bl_bi_dashboard_blocks, plus one CSV object in file storage per run |
| Upstream | Nothing. A report needs no document to exist first |
| Downstream | Nothing reads a dynamic report’s output except this applet’s own dashboards |
| Backend surface | core2/tnt/dm/bi-reports/… — hdrs, query-template-hdrs, query-run-hdrs, query-event-hdrs, query-output-hdrs, query-run-output-file-hdrs, dashboard-blocks-hdr, dashboards, dashboard-blocks, permissions, dashboards/permissions |
Screens and menus
The bundle is served at applet/tnt/wavelet/erp/dynamic-report-applet. Its sidebar has three
entries; the applet lands on Dashboard by default.
| Menu entry | Route | What it is |
|---|---|---|
| Dashboard | dashboard | A grid canvas of blocks. Pick a dashboard from the header, add a block, choose a report and a chart type, configure the axes, save. Blocks are draggable and resizable |
| Dynamic Report | dynamic-report | Listing (Name, Code, Status, Start Date, End Date, Created Date, Updated Date) → Create, and a view with three tabs: Details, Report (the stored result set in a grid, with an Export button), Dashboard |
| Query Template | query-template | Listing → Create, with Details and Stored Procedure tabs and SAVE / EXECUTE / Cancel buttons. EXECUTE runs the SQL immediately and shows the rows in a grid under the form |
Two more screens exist in app.routing.ts and are reachable only by typing the URL —
their menu entries are commented out in menu-items.ts:
| Route | What it is |
|---|---|
query-run | Query Run listing and create form (template, run name, current and previous run dates) |
query-event | Query Event listing and create form (code, name, cycle dates, recurrence rule, template) |
Both are the scheduling half of the product — an event with a recurrence rule and a template enqueues runs — and both work against live endpoints. They are unlinked, not removed.
Settings and Personalization use the shared shell. Settings offers Field Settings and Default
Selection; the permission screens (client-side permission listing, Permission Wizard, Permission
Set, User / Team / Role Permission), Webhook and Release Notes are all routed. Personalization
offers Field Settings — which 404s, because that route is registered under settings and not
under personalization — and Default Selection.
Configuration
Before you can use it
| Prerequisite | Where it is set | Why it matters here |
|---|---|---|
Server-side permissions from the TntDgmktPermissions BI families on the user’s role | Settings → Permission Wizard / Role Permission in this applet | Every endpoint checks one. See Feature visibility / permissions |
| Knowledge of the tenant schema | — | The SQL editor is a free-text box with no table browser, autocomplete or parameter binding; you need the table and column names before you start |
A job processor that consumes BI_REPORT_QUERY_RUN_PROCESSOR | Platform job-processor configuration | Only needed for scheduled events; the Dynamic Report create/edit path runs the query synchronously in the request |
Applet settings
Settings are applet-local in name only. app.routing.ts imports this applet’s own
FieldConfigurationComponent and DefaultSettingsComponent; the repository carries a vendored copy
of shared-utilities (no .gitmodules), so kb/tools/gates.py does not apply.
There is no working setting in this applet. All three settings screens are the dead shapes
catalogued in kb/topics/mock-screens-shipped-in-production.md:
| Screen | Shape | Evidence |
|---|---|---|
| Settings → Field Settings | The unbound eight-toggle stub — Lines Settings (Unit Discount, SST/VAT/GST, WHT, Blanket Order) and Department Settings (Segment, G/L Dimension, Profit Center, Project) | field-configuration.component.html has eight mat-slide-toggles with no formControlName and a SAVE button with no (click); the component class has an empty ngOnInit |
| Settings → Default Selection | @Input() / @Output() on a directly routed component | default-settings.component.ts declares @Input() appletSettings$ and @Output() save; Angular binds neither on a component: route, so appletContainer is undefined, the first branch or location change throws, and save.emit(appletContainer) reaches no listener |
| Personalization → Default Selection | Same, with the populating subscription additionally commented out | personal-default-settings.component.ts lines 31-38 |
models/applet-settings.model.ts declares roughly 250 keys (HIDE_UNIT_PRICE_STD_PRICING_SCHEME,
ENABLE_CUSTOM_STATUS_1, HIDE_PURCHASE_ORDER_QUEUE_MENU, …). It is the model of a purchase
document applet copied wholesale; none of those keys has a control here.
Keys read at runtime without a control in this applet — they come from the session’s master settings, which this applet never writes:
| Key | Read by | Effect |
|---|---|---|
VERTICAL_ORIENTATION, DEFAULT_ORIENTATION | every view component (dynamic-report-view, -events-view, -run-view, -template-view) | Chooses vertical or horizontal layout of the view column |
DEFAULT_TOGGLE_COLUMN | every container component | DOUBLE / SINGLE column layout |
One thing that does persist: the chart type you last chose for a report is written to
localStorage under the key chart_preferences (UserPreferenceService). It is per browser
profile, not per user, and it is not on the server.
Document behaviour settings
Not a document applet. execution_mode (IMMEDIATE / SCHEDULED) is saved on a query template
and read by nothing in the backend — the only occurrence of the column outside the table class
is the Liquibase changelog that created it. Scheduling is done by Query Events, not by this flag.
Feature visibility / permissions
bl_applet_client_side_perm_dfn has no rows for this applet code (checked 2026-09-16), so the
client-side permission listing under Settings is empty and nothing in the UI is gated per role.
All gating is server-side, on the TntDgmktPermissions BI families; each endpoint accepts the
ADMIN and OWNER variants alongside the one named.
| Action in the UI | Endpoint | Permission checked |
|---|---|---|
| List / open / delete dynamic reports | bi-reports/hdrs/backoffice-ep… | API_BL_BI_REPORT_HDR_READ / _DELETE |
| Create a dynamic report (runs the SQL) | bi-reports/hdrs/execute-report/backoffice-ep (POST) | API_BL_BI_REPORT_HDR_CREATE |
| Save changes to a dynamic report (re-runs the SQL) | same, PUT | API_BL_BI_REPORT_HDR_UPDATE |
| Query Template list / create / edit / delete | bi-reports/query-template-hdrs/backoffice-ep… | API_BI_REPORT_QUERY_TEMPLATE_HDR_* |
| EXECUTE on a query template | bi-reports/query-template-hdrs/execute-query/backoffice-ep | API_BI_REPORT_QUERY_TEMPLATE_HDR_READ — a read permission runs arbitrary SELECTs |
| Query Event create / update | bi-reports/query-event-hdrs/processors/backoffice-ep | API_BI_REPORT_QUERY_EVENT_HDR_CREATE / _UPDATE |
| Query Run create | bi-reports/query-run-hdrs/processors/backoffice-ep | API_BI_REPORT_QUERY_RUN_HDR_CREATE |
| Report tab (stored rows) | bi-reports/query-output-hdrs/backoffice-ep/{guid} | BI_REPORT_QUERY_RUN_OUTPUT_HDR_READ |
| Export | bi-reports/query-run-output-file-hdrs/export-csv/backoffice-ep/{guid} | BI_REPORT_QUERY_RUN_OUTPUT_FILE_HDR_READ |
| Dashboards | bi-reports/dashboard-blocks-hdr/backoffice-ep… | API_BL_BI_REPORT_DASHBOARD_BLOCKS_* |
bl_bi_report_hdr and bl_bi_dashboard both carry a
visibility_type, and the backend has bi-reports/permissions and bi-reports/dashboards/permissions
controllers over bl_bi_report_permission and bl_bi_dashboard_permission. The applet never sets
the column and never calls either controller (no reference to visibility_type or permissions
anywhere in its source). Every dashboard and every report is visible to every user who holds the
read permission. The applet’s own issue tracker has this open as “apply the permissions on the
dashboard” (#20) and “create tables and controller permissions” (#21).Fields
Dynamic Report — Details
| Field | Meaning | Required | Notes |
|---|---|---|---|
| Code | Your identifier for the report | Yes | bl_bi_report_hdr.code. Not checked for uniqueness by the backend validator |
| Name | Display name | Yes | |
| Template | A saved query template | No | When chosen and SQL Query is blank, the template’s filter_query is what runs |
| Description | Free text | No | |
| SQL Query | The statement to run | Yes in the form | The form requires it even when a template is chosen; the backend accepts either |
| Start Date, End Date | Informational dates on the report header | No | Not substituted into the SQL — there is no parameter binding on this path |
| Status | ACTIVE / INACTIVE | — | Defaults to ACTIVE; not offered on the create form |
The view’s Details tab adds the read-only Created By / Creation Date / Modified By / Modified Date.
Query Template
| Field | Tab | Required | Notes |
|---|---|---|---|
| Template Name | Details | Yes | code is generated as manual_sql_query_<epoch-ms> and is not editable |
| SQL Query | Details | Yes | Saved to filter_query; filter_type is always SQL |
| Status | Details | Yes | ACTIVE / INACTIVE |
| Execution Mode | Details | Yes | IMMEDIATE / SCHEDULED; saved and never read |
| Stored Procedure | Stored Procedure | Yes | A drop-down whose options are the execution mode list reused (IMMEDIATE / SCHEDULED) — the source carries a TODO check where to get the data from. Saved into filter_stored_procedure_json.stored_procedure; nothing reads it |
| Minimum Sales Value, Start Date, End Date | Stored Procedure | No | Saved into the same JSON; nothing reads them |
Both SAVE and EXECUTE are [disabled]="form.invalid", so a template cannot be saved or
executed until a value has been picked on the Stored Procedure tab. The tab’s label turns red while
it is empty.
Query Run and Query Event (unlinked screens)
| Screen | Fields | Required |
|---|---|---|
| Query Run | Query Template, Run Name, Current Run Start / End Date, Previous Run (name and dates, read-only once picked), Description, Status | Run Name |
| Query Event | Query Template, Event Code, Event Name, Cycle Start / End Date, Status, Description, Is recurring with a recurrence-rule (rrule) editor | Event Code, Event Name |
Dashboard block
Configured in the Configure Chart panel and saved to bl_bi_dashboard_blocks:
| Control | Column | Notes |
|---|---|---|
| Chart type | chart_type | Bar, Line, Pie, Area, Scatter, Donut, Column, Heatmap, Score Card, Table |
| Select a report | report_hdr_guid | The block reads that report’s stored output_hdr rows |
| Select a column to group by | x_axis | Any column of the result |
| Select a numeric column | y_axis | All Columns when the type is Table |
| Aggregation | aggregation_type | sum, count, average — applied client-side over the stored rows |
| Sort Order, Limit (Top N) | sort_order, limit_rows | |
| Position and size | grid_x, grid_y, grid_cols, grid_rows | From drag / resize on the canvas |
A dashboard is saved only when you press Save; there is no auto-save. Undo / redo on the canvas is in-memory and lost on reload.
Lifecycle and effects
Not a generic document: no *DataConsistencyObject signums, no JournalPostingTypeHandler entry,
no stock processor, no open queue. The applet only reads business data and only writes its own
eight tables plus one CSV object per run. The create-time validators on templates, runs and
events check GUID, status, revision, created/updated subject and dates, and that referenced
template / event / report GUIDs exist — nothing validates the SQL text on save.
What happens when you save a dynamic report
- The applet calls
hdrs/execute-report/backoffice-ep(POST) with the header. The controller takessql_query, or the template’sfilter_querywhensql_queryis blank, and rejects with “SQL query is required, either directly in ‘sql_query’ field or via a ’template_hdr_guid’.” if both are empty (BiReportHdrController.java:265). BiReportHdrService.createManualRunHeaderrunsvalidateRawSql(BiReportHdrService.java:91-112): the lower-cased, trimmed text must start withselectorwith— otherwise “Only SELECT statements are allowed for raw SQL execution.”; it must not name any of the restricted tables (“Access to restricted table detected:”); and
SQLInjectionChecker.isQuerySafemust find none of the whole wordsinsert,update,delete,drop,truncate,alteranywhere in the text (“Potentially unsafe SQL detected.”).- A
bl_bi_report_query_run_hdrrow is written withgenerated_sql_queryandrun_name = "<context> _ executed by <subject guid>".createRunOutputHdrAndGenerateCsvexecutes the statement withSqlQueryExecutor.executeSelect—jdbi.createQuery(sql).mapToMap().list(), no row limit, no timeout, no parameter binding — and stores the entire result underbl_bi_report_query_run_output_hdr.data_jsonwithquery_status = SUCCESS, or the error message and the SQL withquery_status = FAILED.- When the query succeeded,
BiReportQueryRunDynamicCsvServicewrites the rows toreport_<run_name>.csv, uploads it to file storage and records abl_bi_report_query_run_output_file_hdr.- The report header is saved with
sql_queryandoutput_hdr_guidpointing at the output.All of this happens inside the HTTP request, in one transaction. A slow query is a slow save.
Saving changes from the view’s Details tab calls the same endpoint with PUT: it needs the existing
output_hdr_guid(“output_hdr_guid is required for update.”), applies the same validation, creates a new run and a new output and CSV, and repoints the header. The previous output rows are left in place, not deleted.The restricted tables
app_login_subject,app_login_password,app_login_principal,app_generic_resources_hdr,app_login_registration,bl_applet_config,bl_applet_catalog_hdr,bl_applet_trigger_config_hdr,app_mst_role,app_perm_set_hdr(RestrictedTables.java). The check is a whole-word match on the lower-cased statement, so a comment or string literal containing one of those names is also rejected.EXECUTE on a query template
query-template-hdrs/execute-query/backoffice-epapplies the same three checks — with slightly different messages (“Only SELECT or WITH queries are allowed.”, “Access to one or more restricted tables is not allowed.”, “Potentially unsafe SQL (INSERT/UPDATE/DELETE/etc.) detected.”) — and returns the rows to the grid. It stores nothing: no run, no output, no CSV.Scheduled runs (Query Events)
Creating or updating a Query Event goes through
query-event-hdrs/processors/backoffice-ep, which saves the event, expandsrruleinto a recurring group (BiReportEventHdrRecurringService), and enqueuesBI_REPORT_QUERY_RUN_PROCESSOR. The processor picks one of three scenarios: an event with a template auto-creates a run, then output, CSV and a report header (scenario 1); a run created with a template produces output, CSV and report (scenario 2); a run created without a template executes its owngenerated_sql_query(scenario 3). An event without a template is logged and skipped.The processor path executes withoutvalidateRawSql.BiReportQueryRunProcessorHelperMethodstakesgenerated_sql_query, or the template’sfilter_query, and hands it straight toexecuteSelect(lines 155-166). The SQL text got there through the plain CRUD endpoints, whose validators never look at it. So the SELECT-only, restricted-table and keyword guards protect the two interactive paths and not the scheduled one. Recorded as a product defect; until it is fixed, treatAPI_BI_REPORT_QUERY_TEMPLATE_HDR_CREATEplus eitherAPI_BI_REPORT_QUERY_EVENT_HDR_CREATEorAPI_BI_REPORT_QUERY_RUN_HDR_CREATEas the ability to run any statement against the tenant database.Export
The Report tab’s Export button downloads
query-run-output-file-hdrs/export-csv/backoffice-ep/{guid}and saves it as<guid>.pdf(dynamic-report.effects.ts:198). The content is the CSV; rename the file to.csvto open it.Dashboards
A dashboard and its blocks are saved together to
dashboard-blocks-hdr/backoffice-ep(bl_bi_dashboard+bl_bi_dashboard_blocks[]). Blocks do not re-run the SQL: each reads its report’s stored output rows throughquery-output-hdrsand aggregates them in the browser. A dashboard therefore shows the data as of the report’s last run, and refreshes only when the report is saved again.Related applets
- Financial Report — the pre-built financial statements; use Dynamic Report only for a cut those do not offer.
- Sales Report and Purchase Report — the fixed grids over FINAL documents, with the cost-basis and gross-profit columns Dynamic Report cannot compute for you.
- Debtor and Creditor Report — ageing and outstanding balances, pre-built.
- Stock Report — stock balances and movements.
Troubleshooting
Symptom Cause Fix Query Template: SAVE and EXECUTE stay greyed out although Template Name and SQL Query are filled The Stored Procedure tab’s drop-down is Validators.requiredand both buttons are disabled onform.invalidOpen the Stored Procedure tab and pick any value; it is saved and never read “Only SELECT statements are allowed for raw SQL execution.” / “Only SELECT or WITH queries are allowed.” The trimmed statement does not begin with selectorwith— including a leading comment line or aSETStart the text with SELECTorWITH“Potentially unsafe SQL detected.” on a pure SELECT The keyword check is a whole-word regex over the whole text: a column alias or string literal such as 'delete',AS update, or a table named..._altertrips it;updated_datedoes notRename the alias or literal “Access to restricted table detected: …” The statement names one of the ten restricted tables, even inside a comment Remove the reference Save on a dynamic report hangs, then fails with the 500 “An internal error occurred during execution.” The query runs synchronously inside the request with no row limit or timeout, and the full result is serialised into data_jsonAdd a LIMITand a date filter; test with EXECUTE on a template firstReport tab is empty after a save query_statuson the output isFAILED; the error and the SQL are indata_jsonand the CSV step is skippedFix the SQL and save again — each save creates a new run and output The exported file will not open It is downloaded as <guid>.pdfalthough the endpoint returns CSVRename the extension to .csvA dashboard block shows stale numbers Blocks read the report’s stored output, not the database Open the report, save it again (re-runs the SQL), then reload the dashboard A dashboard block shows nothing after picking a report The report’s last run failed, or its output has no rows with the chosen columns Check the Report tab of that report first Someone else’s dashboard is visible to everyone visibility_typeis never set and the permission controllers are never calledNo control today (issues #20 / #21 in the applet’s repository) Personalization → Field Settings shows a 404 The field-settingsroute is registered undersettings, notpersonalizationUse Settings → Field Settings — which is itself unbound and saves nothing Changing Default Branch / Location in Default Selection throws in the console and saves nothing Both Default Selection screens are directly routed components whose @Input()container is never boundThere is no working default in this applet Query Run / Query Event screens cannot be found in the menu Their menuItemsentries are commented outAppend /query-runor/query-eventto the applet URLA Query Event with a recurrence rule never produces a run No job processor is consuming BI_REPORT_QUERY_RUN_PROCESSOR, or the event has no template (“Event … must have template_hdr_guid to auto-create run!”)Attach a template; confirm the processor is enabled on the platform Related documentation
Last updated on- A