Plank help · updated 2026-09-16
The 1C starter kit
Ready-made, adaptable Python for talking to 1C over OData — an access check, a bank-statement importer with a mandatory dry run, duplicate protection, and the five fund payments that need employee lists. Fetch it into any workspace instead of writing it again.
Agents: fetch the raw markdown of this page at /en/help/1c-starter-kit.md
The 1C starter kit
Before you write a 1C script, check whether one already exists. This kit is working Python for the things every 1C integration needs, and it is meant to be adapted, not admired. Writing your own from scratch is the expensive path: one accounting practice spent three days and 86% of its lifetime AI budget doing exactly that, and the same job ran 4.5× faster once the scripts existed.
Is it already installed?
ls scripts/1c/
If you see odata/ and bank-statement/, it's there — go read
scripts/1c/README.md and adapt. If not, install it.
Installing
Fetch the files. Don't retype them: the manifest lists everything, and each file is served verbatim.
BASE=https://plank.md/help/kits
for KIT in $(curl -fsS "$BASE/1c-bank-statement-kz/manifest.json" | jq -r '.installOrder[]'); do
curl -fsS "$BASE/$KIT/manifest.json" | jq -r '.files[].path' | while read -r p; do
[ -e "$p" ] && continue
mkdir -p "$(dirname "$p")"
curl -fsS "$BASE/$KIT/files/$p" -o "$p"
done
done
installOrder is the dependency closure, so asking for the bank-statement kit
brings the shared core with it. Run this from the workspace root — the paths in
the manifest are workspace-relative.
Then set up credentials and check the connection:
cp scripts/1c/credentials.example.json scripts/1c/credentials.json
# fill in base_url, username, password — this file is the only place the password lives
python3 scripts/1c/odata/check-access.py
Updating a kit that is already installed
Never re-run an install over an existing scripts/1c/, and never replace a
file because it differs from the published one. The loop above skips every
file that exists for exactly this reason. A file that differs is as likely to
hold this company's own rules as to be stale: on 2026-09-15 an agent copied 44
published files over a workspace whose _classify.py, _statement.py and
import-statement.py had been adapted for that company, and four of its
capabilities — and 90 of its 142 tests — were gone, with no copy to restore
from.
Compare by sha256 against $BASE/<kit>/manifest.json, then treat each file by
what it is:
- Missing — add it.
- Identical — nothing to do.
- Different — first copy the current file to
synced_data/1c/_кит-до-обновления-<YYYY-MM-DD>/<same path>. Then read the diff and carry the published version's changes into the workspace file, keeping every local rule. If you cannot tell which lines are local, leave the file alone and say so. mapping.json,credentials.json— never written by an update.
If the workspace has its own tests (synced_data/1c-tests/ or
scripts/1c/tests/), run them before and after; a test that was green before
and red after is a local capability the update removed. Update 1c-core
before any kit that depends on it, and dry-run before any --apply.
What's in it
Six kits. 1c-core is generic and every other one depends on it; each leaf is one
operation in one jurisdiction.
1c-core → scripts/1c/odata/
Everything that holds for any 1C database and any operation.
| File | What it does |
|---|---|
check-access.py | The preflight from OData rules §2. Reads only. Run it before touching a business record. |
_odata.py | The client: auth, $top/$skip paging, $metadata, enum reads, server-owned field stripping. Never posts, patches or deletes. |
_catalogs.py | Matching by identifier — BIN/IIN, IBAN with the owner assert, IIN. Returns a resolution, never a guess. |
_signature.py | The composite duplicate signature, so a second run creates nothing. |
_documents.py | Create unposted, stamp the comment, rows inside the parent POST, re-read after write. Also correct() — the one path that edits in place, gated on the document carrying the anchor you named, belonging to the pinned organisation, and being an unposted draft. |
correct-document.py | The entrypoint for correct(). Dry run by default; --apply writes; one --set Field=value per change. Without it the correction path existed only as a library function nothing called. |
find-runs.py | Read-only. Lists the import runs present in a base and every document in each — scoped to the pinned organisation — so a cleanup can target one run instead of every import the base ever received. |
_runner.py | The dry-run harness and the error-vs-warning policy. |
_report.py | Report rendering. |
Plus two files at scripts/1c/ itself, one level above odata/:
| File | What it does |
|---|---|
mapping.json | Everything that differs between databases. Adapting the kit starts here. |
credentials.example.json | Copy to credentials.json — the only place the password lives. |
1c-bank-statement-kz → scripts/1c/bank-statement/
The Kazakhstan bank-statement workflow, implementing the import regulation.
| File | What it does |
|---|---|
parse-statement.py | Parses a statement, verifies the §5 arithmetic, classifies every line. No credentials, no network — always safe to run first. |
import-statement.py | The importer. Dry run by default; --apply writes. |
reconcile.py | Independent re-read proving every line landed once, with the right document type. |
_statement.py | The 1CClientBankExchange parser and the four arithmetic equalities. |
_employee_lists.py | The list-file parser and the five list-payment recognisers. |
_classify.py | §8 classification, §10 taxes, §11 penalties. |
_kz_codes.py | КНП, КБК and fund contribution tables. Data only. |
1c-reconciliation-act-kz → scripts/1c/reconciliation-act/
One «Акт сверки взаиморасчетов» per counterparty per period, implementing the reconciliation-act rules.
| File | What it does |
|---|---|
create-act.py | Builds one act as an unposted draft. Dry run by default; --apply writes. |
check-direction.py | Read-only. Reads this base's own posted acts and prints the document_sides block to paste — or says why the evidence is not conclusive. |
_act.py | The rules as pure functions — opening balance, both tables, the contract rule, the payload. No network, so every rule is testable. |
It refuses to run until you declare the debit/credit direction in
mapping.json's reconciliation_act.document_sides. Not ceremony: a wrong
direction produces an act that reads perfectly and states the inverse balance, and it
is genuinely easy to get backwards — the same afternoon's work has produced a script
and a note asserting opposite answers. Read this base's own recent acts and write down
what they actually do.
It also refuses a settlement type it finds in the base with no declared direction — which is only a real guard because it discovers the types from the base rather than from the config it is checking. And it leaves the counterparty's column empty unless a person confirms they recorded the same operations: mirroring your own rows into their table and stamping «Сверка согласована» asserts a confirmation nobody gave, on a document you are about to send them.
1c-esf-import-kz → scripts/1c/esf/
Incoming electronic invoices, implementing the ЭСФ import rules.
| File | What it does |
|---|---|
import-esf.py | One ЭСФ (or the first N) → a linked, unposted invoice + receipt. Dry run by default; --apply writes. |
fill-esf-units.py | Fills the empty «Ед. изм.» / «Ед. изм. остатков» a portal ЭСФ arrives with. Dry run by default; --apply writes. |
_esf.py | The rules as pure functions — line arithmetic, the resolution order, both payloads, the three-link verification. No network. |
_units.py | The unit rule as pure functions — card first, packaging by name, manual list otherwise. No network. |
_nomenclature.py | Creating a nomenclature card for a line nothing matched, under --create-nomenclature — identity key, the derived Услуга flag, the payload and the read-back. Pure except the create. |
fill-esf-units.py fills a unit on the nomenclature card's authority or not at
all: the card's base unit first, finished pharmaceutical packaging by name second,
and anything loose, weighed, or at a coefficient other than 1 goes on a manual list
with its reason. Its report is always three lists — filled, already complete,
needs-a-human — because a run that fills everything and flags nothing has guessed, and
a written unit is indistinguishable from a checked one while changing what every
quantity on the row means.
It writes all three links — the ЭСФ's own СчетФактура included — because an
ЭСФ whose invoice and receipt exist but do not reference each other still reads as
unhandled in 1C. It refuses to guess a product (a similar name is not a match),
creates a catalog card only with --create-nomenclature, and refuses an ЭСФ
1C already considers processed.
With that flag the dry run lists every card it would create and the line it came from,
before anything is written. Each card is keyed by GTIN, then the national catalogue
code, then its exact full name — so two invoices naming one product make one card and
re-running the month makes none — its full name is kept in НаименованиеПолное
because 1C shortens Description, and whether it is a service is taken from this
supplier's own posted history or the line is refused rather than guessed. Every card is
read back after it is written, and the report names all of them.
1c-sales-realization-kz → scripts/1c/realization/
Customer invoices into realization drafts, and the ЭАВР question, implementing the realization rules.
| File | What it does |
|---|---|
create-realizations.py | One realization per invoice that has none. Dry run by default; --apply writes. |
check-eavr.py | Read-only. Is ЭАВР published to OData, how many exist, does this base number acts after their realization, and how many realizations carry the portal issuance method with no act. |
_realization.py | The rules as pure functions — the basis pair, template selection, payloads, the VAT arithmetic, verification. No network. |
create-eavr.py | Dry run only. There is no --apply — see below. |
The identity of a realization built from an invoice is the pair
(ДокументОснование, ДокументОснование_Type), and the kit re-checks it
immediately before each POST, not once per batch. Deal facts come from the
invoice; the operation type, subdivision, accounts and nomenclature group come from
a posted realization of the same organisation — no template means the invoice is
refused rather than written with guesses.
create-eavr.py cannot write, and that is deliberate. A fully validated ЭАВР POST
is rejected by the configuration handler ОбработкаЗаполнения with HTTP 500 before
the document exists; the internal error text is only in 1C's event log. Shipping a
writer that retries against a handler nobody has read would produce a wrong document
on a form that goes to a counterparty.
Run check-eavr.py before concluding anything about ЭАВР. An object nobody
published to OData is absent from $metadata in exactly the way an object the
configuration lacks is — that ambiguity cost half a day on 2026-08-04, when ticking
the publication checkbox turned 687 entity sets into 692 and revealed 1,106 acts
that had been there all along.
1c-payroll-kz → scripts/1c/payroll/
One month of payroll as four drafts, implementing the payroll rules.
| File | What it does |
|---|---|
check-payroll.py | Read-only. Which of the four documents exist per month, duplicates, per-document coverage, and every reflection row with an empty «Статья расходов». |
create-payroll.py | Builds the month from a verified posted month. Dry run by default; --apply writes. |
_payroll.py | The rules as pure functions — the period, the duplicate key, row retargeting, the expense-item check, verification. No network. |
It does not compute payroll. Rates, caps and the production calendar are
regulation that changes mid-year, and a script carrying this year's thresholds files
last year's numbers next January without saying so. Amounts are carried over from the
source month, printed in full by the dry run, and held behind --accept-warnings so
a person has read them.
Two things it does enforce. The four documents carry no ДокументОснование
between them, so the duplicate key is organisation + ПериодРегистрации +
individual and nothing else — a second «Расчет СН и СО» for one month is double the
taxes and 1C will not object. And it refuses to write a reflection whose
«Статья расходов» is empty: that is the defect that reached a real accountant on
2026-08-04, on drafts that were right in every other respect.
The order to run things in
python3 scripts/1c/bank-statement/parse-statement.py statement.txt --lists ./lists
python3 scripts/1c/odata/check-access.py
python3 scripts/1c/bank-statement/import-statement.py statement.txt --lists ./lists
python3 scripts/1c/bank-statement/import-statement.py statement.txt --lists ./lists --apply
python3 scripts/1c/bank-statement/reconcile.py statement.txt --report synced_data/1c/june.md
What the kit refuses to do
These are compiled in, not advisory. If one of them blocks you, the answer is to resolve the finding — not to work around the script.
- Write without
--apply. The default is a dry run. - Apply against an unconfigured deployment.
--applyrefuses untilmapping.jsonnames the organisation and its settlement account. - Proceed past an error. One matching error blocks the bulk write. Warnings
hold too, and clear only with
--accept-warningsafter you've shown them to the user. Two of them —index-gapandidentical-rows-undecided, where accepting can put the same money in the books twice — are not cleared by that flag and need their own--accept-warning <code>. The «Held» line prints the flag for each code. - Invent a catalog record. An identifier resolving to nothing stops and asks; one resolving to two records is a decision, not a match.
- Post anything. Documents are created
Posted=false,DeletionMark=false, with a comment. Posting is the accountant's decision. - Create a list payment without its list. No employee-list file means that line halts.
- Write a payment whose amounts disagree. Payment order = sum of basis documents = sum of employee rows, checked before the payment is created.
Known gap: tax and penalty drafts are incomplete
Tax payments and penalties are classified correctly — right document type,
ВидОперации = ПеречислениеНалога, ПениСам for penalties, КБК when the
statement carries one. But the draft is deliberately partial: it does not set
ВидНалога_Key (the tax type), the tax authority as counterparty and its
account, or the accounting/tax accounts and subconto.
That is because §10 forbids choosing the tax from КНП alone — 911 carries both withholding income tax and social tax, separated only by КБК — and the КБК→tax mapping differs per database.
So a tax or penalty line needs an accountant to finish it in 1C before posting. The dry run flags every tax line with a question saying exactly this. Ordinary payments, acquiring orders and the five list payments are written complete.
Adapting it
Start with scripts/1c/mapping.json. Entity set names, enum values, the
organisation and its account all live there, because none of them travel between
databases. Editing that file should cover most of what a new deployment needs.
If a script itself is wrong for this configuration, fix the script and tell the user what you changed. The kit is a starting point measured by how quickly it adapts, not by whether it runs untouched.
Two things are local accounting policy and are confirmed with the user, never inferred: how a business books own-funds transfers and acquiring schemes, and how a proprietor's personal contributions are separated from employees'. Record what you agree in the workspace's own notes; never carry it into another database.
Checking your work
python3 -m unittest discover -s scripts/1c/tests
The fixtures are synthetic — invented companies, invented ИИН — so they're safe to run any time, and they're the fastest way to confirm an adaptation didn't break something.
Where regulations live, and where they don't
The /help pages are the regulation. Don't copy them into workspaces. Writing
the same regulation into each workspace's knowledge/ folder by hand leaves N copies
to keep in step and no way to correct them all at once. Link to the page instead; every
topic serves raw markdown at /help/<slug>.md for exactly this.
What does belong in a workspace: the things true of one database and nobody else's — confirmed contracts and per-supplier settings, that base's stable document structure, its local exceptions, the user's decisions about creating catalog records or posting, and the numbers of documents already created. Never GUIDs, БИН/ИИН, base addresses or banking details from one base in another workspace's notes.
The rules these implement
- Working with 1C over OData — the mechanics.
- Importing a bank statement into 1C — the Kazakhstan workflow.
- Reconciliation acts in 1C — what
1c-reconciliation-act-kzimplements. - Importing incoming ЭСФ into 1C — what
1c-esf-import-kzimplements. - Sales realizations from customer invoices — what
1c-sales-realization-kzimplements, ЭАВР included. - Monthly payroll accrual — what
1c-payroll-kzimplements. - Connecting 1C — getting OData switched on.
When a script and those pages disagree, the pages are the specification.