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-corescripts/1c/odata/

Everything that holds for any 1C database and any operation.

FileWhat it does
check-access.pyThe preflight from OData rules §2. Reads only. Run it before touching a business record.
_odata.pyThe client: auth, $top/$skip paging, $metadata, enum reads, server-owned field stripping. Never posts, patches or deletes.
_catalogs.pyMatching by identifier — BIN/IIN, IBAN with the owner assert, IIN. Returns a resolution, never a guess.
_signature.pyThe composite duplicate signature, so a second run creates nothing.
_documents.pyCreate 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.pyThe 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.pyRead-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.pyThe dry-run harness and the error-vs-warning policy.
_report.pyReport rendering.

Plus two files at scripts/1c/ itself, one level above odata/:

FileWhat it does
mapping.jsonEverything that differs between databases. Adapting the kit starts here.
credentials.example.jsonCopy to credentials.json — the only place the password lives.

1c-bank-statement-kzscripts/1c/bank-statement/

The Kazakhstan bank-statement workflow, implementing the import regulation.

FileWhat it does
parse-statement.pyParses a statement, verifies the §5 arithmetic, classifies every line. No credentials, no network — always safe to run first.
import-statement.pyThe importer. Dry run by default; --apply writes.
reconcile.pyIndependent re-read proving every line landed once, with the right document type.
_statement.pyThe 1CClientBankExchange parser and the four arithmetic equalities.
_employee_lists.pyThe 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-kzscripts/1c/reconciliation-act/

One «Акт сверки взаиморасчетов» per counterparty per period, implementing the reconciliation-act rules.

FileWhat it does
create-act.pyBuilds one act as an unposted draft. Dry run by default; --apply writes.
check-direction.pyRead-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.pyThe 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-kzscripts/1c/esf/

Incoming electronic invoices, implementing the ЭСФ import rules.

FileWhat it does
import-esf.pyOne ЭСФ (or the first N) → a linked, unposted invoice + receipt. Dry run by default; --apply writes.
fill-esf-units.pyFills the empty «Ед. изм.» / «Ед. изм. остатков» a portal ЭСФ arrives with. Dry run by default; --apply writes.
_esf.pyThe rules as pure functions — line arithmetic, the resolution order, both payloads, the three-link verification. No network.
_units.pyThe unit rule as pure functions — card first, packaging by name, manual list otherwise. No network.
_nomenclature.pyCreating 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-kzscripts/1c/realization/

Customer invoices into realization drafts, and the ЭАВР question, implementing the realization rules.

FileWhat it does
create-realizations.pyOne realization per invoice that has none. Dry run by default; --apply writes.
check-eavr.pyRead-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.pyThe rules as pure functions — the basis pair, template selection, payloads, the VAT arithmetic, verification. No network.
create-eavr.pyDry 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-kzscripts/1c/payroll/

One month of payroll as four drafts, implementing the payroll rules.

FileWhat it does
check-payroll.pyRead-only. Which of the four documents exist per month, duplicates, per-document coverage, and every reflection row with an empty «Статья расходов».
create-payroll.pyBuilds the month from a verified posted month. Dry run by default; --apply writes.
_payroll.pyThe 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. --apply refuses until mapping.json names 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-warnings after you've shown them to the user. Two of them — index-gap and identical-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

When a script and those pages disagree, the pages are the specification.