Plank help · updated 2026-10-16

Dashboards on 1C data

How to build a management dashboard that reads a live 1C base: cache the closed periods, refresh only the open window, and reconcile the figures before anybody sees them.

Agents: fetch the raw markdown of this page at /en/help/1c-dashboards.md

Dashboards on 1C data

A management dashboard over a live 1C base is not a report you generate once. It is a cache the dashboard reads, a sync that extends it, and an arithmetic check that has to pass before anybody looks at it. Build it in that order.

Everything here was measured on one real base — a trading company, eight months of data, ~2.6M documents in the base, 1C Fresh over OData — on 12 October 2026.

On «Управление нашей фирмой», install the kit instead

For a УНФ base this page is already built: the 1c-dashboards-unf kit puts the sync in scripts/1c/dashboards/ and a live page with the refresh button in dashboards/1c/finance.html (sales, margin by product, P&L, cash and bank, stock, balances, and a «Проверки» panel). Install it the way the 1C starter kit describes, with 1c-dashboards-unf as the kit name, then:

python3 scripts/1c/odata/organizations.py                 # record organization.ref_key in mapping.json
python3 scripts/1c/dashboards/sync.py --benchmark --full  # reads and reconciles, writes nothing
python3 scripts/1c/dashboards/sync.py --full              # first load, from chat

Every rule below is in it: closed periods are kept, the button re-reads the open month and the stock/ledger movements since the last snapshot, the four reconciliations that must hold run before the first write (a failure exits 1 and changes no table), and the gaps a person must explain are written to unf_checks and drawn on the page. Report every WARN line the run prints. For «Бухгалтерия для Казахстана» or any other configuration, build from the rules below.

Never ask the person for an API key

A script that runs in the workspace reaches the app-data tables as the container it runs in. The sandbox already holds PLANK_API_URL and PLANK_INTERNAL_API_KEY; a 1C workspace has a ready client at scripts/1c/odata/_plank_app.py. A public API key is for code on somebody's laptop and nothing else.

If you find yourself about to ask for a token to make a refresh button work, you have the wrong door. Read /help/app-data.

The table is the cache, not a copy of 1C

Do not re-read history. Measured on that base: a full rebuild of one year was 388 seconds and even a windowed recompute was 201 seconds, against the 90-second budget a @plank-button script gets. No amount of tuning closes that. Not reading does — the same year refreshed in 55.7 seconds once the closed months stopped being re-read.

sync_state / sync_control    one row per dataset
  dataset            which figures this row tracks
  synced_through     how far the cache is complete
  as_of              the snapshot date it last wrote
  monthly_checksums  the fingerprint per closed month (below)

The sync reads 1C only from synced_through forward, rewrites just that window (delete_where the period, then bulk_insert), and moves the watermark. Keep a --full mode for the first load and for rebuilds from chat: it should stay reachable and stay slow, not become the default.

Use bulk_insert, never insert in a loop — a loop is one HTTP round trip per row, and a few hundred rows will not fit the button's budget.

A back-dated document is normal, so verify "closed"

A document posted into an already-closed period is ordinary accounting practice. A cache that trusts "closed" will go quietly wrong, which is worse than going loudly wrong.

So store a cheap fingerprint per month — document count and sum — and re-check the previous month only on each run with one filtered request. If it disagrees, re-read that month and say so in the output. Full reconciliation of all history belongs in a chat run or a schedule, not in a button.

(Server-side $filter against 1C works. If one register refuses to filter, say which one by name and skip its fingerprint — do not disable the whole check.)

Snapshots accumulate, they do not get overwritten

A stock level or an account balance is a reading at a moment. Give it an as_of and append. The dashboard shows max(as_of); you also get the history of that number for free, which is almost always the next thing somebody asks for. One reading per day is plenty.

Monthly aggregates (revenue, cash flow, P&L) are different — those you replace for the window you re-read.

Quantity is not comparable across units; money is

The same product in the same month can legitimately appear under two units of measure — шт and Упак8шт. On that base 70 product-months did.

  • Amounts can be summed. They are in one currency.
  • Quantities cannot. 360 packs plus 50 pieces is 410 of nothing.

So make the unit part of the key, keep quantity in a map keyed by unit, and sum only the money. If the dashboard shows a quantity, it must show its unit beside it.

Two different units for one product is also a signal worth reading rather than flattening: if you are resolving products by a reference, two differently-named products sharing one reference is counter-evidence, not confirmation.

Reconcile before you publish. A clean render is not evidence.

This is the section that matters. Both defects found on that base by checking were invisible on the screen — the page looked finished.

Run these and put the result in your message:

1. Several independent sources must agree to the tenge. Daily sales, per-product sales and the monthly aggregate are three different reads of the same money; if they disagree, one of them is wrong. On that base all three matched exactly across all eight months — that is what makes the figures publishable.

2. A derived total must tie to the account it represents. Stock value reconciled to account 1310 at 0.00 difference. State the difference, not the word "verified".

3. The balance must close. Sum of all account balances = 0.00. A service account (000) is shown as its own line, never folded silently into equity.

4. Two measures of the same thing need their gap explained. P&L income and net sales matched in six months of eight, differed by 20,400 ₸ in one — and by 62,541,900 ₸ in April, a 44% gap nobody had explained. That may be legitimate non-sales income. It may be missing sales documents. Either way a dashboard showing both figures side by side must say which, because the first person to notice will not be you.

5. Never draw a zero you have not verified. The cash-flow table on that base rendered a bank channel as eight months of 0 in / 0 out / 0 movements — while the base held five posted bank receipts for 920,416 ₸. A zero is a claim. If a channel is empty because the data is genuinely absent, label it ("банк в 1С почти не отражён"); if it is empty because your query missed it, that is a bug the UI will never show you.

6. Say what the coverage is. If one side of the business is barely in 1C — bank movements, a warehouse, a branch — show it separately with the caveat attached, never merged into a total that then looks complete.

Wiring the refresh button

Give the sync script a @plank-integration header and a @plank-button one, then have the page post plank:run-script and listen for plank:script-result — see interactive HTML dashboards. Keep the button disabled until the result arrives.

Say plainly, next to the button, what it does and does not do: the normal refresh fits in the budget, the first or full run has to be asked for in chat.

Do not claim the button works until you have run the script yourself, shown the rows changed, and measured the time.

Two small traps that cost a day between them

A jsonb column takes an object, not a bare string. Writing "complete" into a jsonb column answers 400 invalid input syntax for type json — correct, and it used to name no column, which read as "the table rejects writes". If a write fails with a 400 while an empty insert succeeds, the fault is a column, not the table: check them one at a time rather than rebuilding the schema. Put a string in text and an object in jsonb.

Read only the rows the page shows. fetch('apps/<table>') takes where, orderBy and limit — see App data. The dashboards on that base pulled every row of every table on each open, 455 KB in eight requests; fine at that size, wrong as soon as the history grows, and the fix is a query parameter rather than a rewrite.