Plank help · updated 2026-09-17
Importing a bank statement into 1C (Kazakhstan)
The working regulation for posting a bank statement into 1C over OData — arithmetic controls, duplicate protection, classifying each operation, taxes and penalties by КНП/КБК, and the five fund payments that need employee lists before they can be created.
Agents: fetch the raw markdown of this page at /en/help/1c-bank-statement-import.md
Importing a bank statement into 1C (Kazakhstan)
This is the working regulation for loading a bank statement into 1C over the standard OData interface, safely and verifiably. It was distilled from a production import against a «Бухгалтерский учет для Казахстана, редакция 2.5» database and is written for both the accountant and the assistant doing the technical work — the assistant runs every command here, and the user never needs a terminal.
The scripts already exist. This whole regulation ships as working, adaptable Python in the 1C starter kit — the parser, the arithmetic controls, the five list-payment recognisers, the dry run, the importer, the reconciliation. Look for
scripts/1c/before writing anything, and install the kit if it isn't there. Each section below names the file that implements it.
Read Working with 1C over OData first. This page assumes its rules and does not repeat them — in particular §3, on establishing what your integration user is allowed to do. If that user can create documents but not edit, delete, or post them, get the role widened before a real import: this workflow produces dozens of documents at a time, and a restricted user turns every mistake into manual cleanup in the 1C interface.
Use it when you need to:
- parse a
1CClientBankExchangestatement; - create incoming and outgoing payment documents;
- handle payment orders from a QR/acquiring provider;
- match counterparties, contracts, and bank accounts;
- form pension, social, and medical-insurance payments with employee lists;
- record taxes and penalties with the right КНП, КБК, and operation type;
- prove every operation in the statement is reflected once and only once;
- leave every document unposted until an accountant has reviewed it.
1. The safety principle
Full verification first, then drafts, then a second reconciliation, and only after a human review — posting.
Until the review is finished, every created document must have:
Posted=false— not posted;DeletionMark=false— not marked for deletion;Оплачено=true— the operation genuinely appears in the bank statement;- a clear comment saying the document was created from a bank statement and needs review.
The assistant never posts documents automatically. Posting changes accounting registers and happens only on an explicit decision by the user.
2. What is the source of truth
In priority order:
- The bank statement — the source of operation date, bank document number, amount, direction, purpose, КНП, BIN/IIN, and IBAN.
- The separate bank file with the employee list — the source of each employee's full name, IIN, period, and amount.
- 1C catalogs — the source of internal references to counterparty, individual, contract, bank account, tax, and accounting accounts.
- Previously correct 1C documents — the source of structure and configuration attributes, but never of the current amount or the current bank account.
- The user — the source of decisions on ambiguous classification.
Critically: the "main account" stored on a counterparty card does not outrank the IBAN in the current statement. For each payment, use the account the bank named.
3. Terms
| Term | Meaning |
|---|---|
| КНП | Payment purpose code |
| КБК | Budget classification code |
| ИИК | Bank account in IBAN format |
| Draft | An unposted 1C document, Posted=false |
| Bank number | НомерДокумента from the bank file; stored in 1C as НомерВходящегоДокумента |
| Internal 1C number | The Number field, assigned automatically by 1C |
| Basis document | ОПВПеречислениеВФонды or СОПеречислениеВФонды, carrying the list of individuals |
The OData entity sets involved:
| Purpose | Entity set |
|---|---|
| Outgoing payment order | Document_ПлатежноеПоручениеИсходящее |
| Incoming payment order | Document_ПлатежноеПоручениеВходящее |
| Payment order, debit | Document_ПлатежныйОрдерСписаниеДенежныхСредств |
| Payment order, credit | Document_ПлатежныйОрдерПоступлениеДенежныхСредств |
| Basis for ОПВ/ОПВР | Document_ОПВПеречислениеВФонды |
| Basis for СО/ООСМС/ВОСМС | Document_СОПеречислениеВФонды |
| Counterparties | Catalog_Контрагенты |
| Bank accounts | Catalog_БанковскиеСчета |
| Contracts | Catalog_ДоговорыКонтрагентов |
| Individuals | Catalog_ФизическиеЛица |
| Taxes, duties, contributions | Catalog_НалогиСборыОтчисления |
| VAT rates | Catalog_СтавкиНДС |
4. Input files
Parsed by scripts/1c/bank-statement/_statement.py (the statement) and _employee_lists.py (the list files).
4.1 The statement
Supported format: 1CClientBankExchange. The header must carry ДатаНачала, ДатаКонца, РасчСчет, НачальныйОстаток, ВсегоПоступило, ВсегоСписано, КонечныйОстаток.
Each operation needs at least: НомерДокумента, ДатаДокумента, ДатаОперации, ВидДокумента, payer and payee, BIN/IIN, ИИК, СуммаПриход or СуммаРасход, НазначениеПлатежа, КодНазначенияПлатежа.
Direction is never guessed. A row with only Сумма is read as outgoing when the payer's account is the statement's own and as incoming when the payee's is. A row that says neither — no СуммаПриход/СуммаРасход and our account on neither side — stops the parse with the row's number, because direction decides whether the document is an incoming or an outgoing payment. Until 2026-09-17 such a row was silently written as outgoing.
4.2 Employee-list files
A «Платежное поручение со списком» cannot be created from the main statement alone. It needs a separate 1CClientBankExchange 2.00 file containing a СПИСОКСОТРУДНИКОВ block, where every employee row carries СОТРУДНИК (full name), СотрудникБИН_ИИН, СотрудникДатаРождения, Период in ММГГГГ format, and Сумма.
If the file is absent, the document is not created. Stop that line of the import and ask the user for the list. A list payment created from the aggregate amount alone is a wrong document that you will not be able to repair.
4.3 Whose statement is it?
Before the arithmetic, before anything: confirm the statement belongs to the company this workspace is configured for. The statement's own РасчСчет must equal mapping.organization_account.iban, and the base must actually hold the organisation mapping.json pins.
This is not a formality, and the four arithmetic equalities in §5 do not cover it: they prove the file is internally consistent, which it is just as much when the file belongs to another client. An accountant with 17 companies has 17 bases, and a mapping that was copied and never adapted points at the previous one. The import then succeeds, reconciles, and reports cleanly — into somebody else's books.
import-statement.py now checks both and treats either failure as an error. The full rule, including the case where one base holds two organisations with almost the same name, is §2.5 of the OData rules.
4.4 Several bases in one workspace
One workspace can be set up for several 1C bases — one accounting practice runs four out of a single scripts/1c, another ten. Each base is one pair of files: a credentials.json and a mapping. The mapping is what selects the base, and it is named after the client:
scripts/1c/mapping.json ← the base a run with no --mapping means
scripts/1c/<client>-mapping.json
scripts/1c/<client>-mapping.json
See what is configured — it connects to nothing and takes a second:
python3 scripts/1c/odata/bases.py
It prints, for each base, the --mapping argument to pass, the organisation the mapping pins, the platform connection it uses, and the base profile learned for it.
Three things follow, and they matter more than they look:
- Name the base in every command. With more than one base configured, a run with no
--mappingrefuses instead of guessing, and lists the bases. It used to takemapping.jsonbecause of what that file is called, which is right once and somebody else's books the rest of the time. - Everything learned per base is stored per base.
<client>-mapping.jsongets<client>-base-profile.jsonand its own «Правила учёта» page. A workspace with ONE base is unaffected:mapping.jsonkeepsbase-profile.jsonexactly as before, so there is nothing to move or rename. - A mapping that does not match its base stops the run. If the credentials reach one company's base while the mapping describes another, the run refuses before writing anything and names both files and both companies. That is a configuration mismatch — the two files are from different clients — not a fault in the kit. Fix it by pointing
--mapping(and--credentials) at the pair that belongs together.
5. Arithmetic controls
Implemented by verify_arithmetic() in scripts/1c/bank-statement/_statement.py. Run it on its own, without credentials, with parse-statement.py.
Before touching 1C, four equalities must hold:
- The sum of all
СуммаПриходequalsВсегоПоступило. - The sum of all
СуммаРасходequalsВсегоСписано. Начальный остаток + Поступления − Списания = Конечный остаток.- The number of recognised
СекцияДокумент=выпискаsections equals the number of rows processed.
If the arithmetic doesn't reconcile, stop the import. Never paper over a discrepancy with a manual correcting row that the bank doesn't explain.
6. The dry run is mandatory
Implemented by scripts/1c/bank-statement/import-statement.py, which writes nothing without --apply — and refuses --apply until mapping.json names the organisation and its settlement account.
Run without --apply first. The dry run must report:
- how many operations are in the file;
- how many already exist in 1C;
- how many documents would be created;
- the planned total;
- the breakdown by 1C document type;
- missing counterparties;
- missing bank accounts;
- operations that require an employee list;
- tax or otherwise ambiguous operations.
Warnings may be accepted only on a deliberate decision by the user. Errors never yield to that flag.
6.1 One bad line does not hold the good ones
A line the run cannot write is declined — named in the report with its reason and with what you have to supply — while every other line is written. It is not a reason to hold the statement. That used to be the rule, and it cost a real February import 257 lines: 21 were unresolvable and nothing at all was written.
So a run has four outcomes, and the exit code tells them apart:
| exit | what happened |
|---|---|
0 | every line of the statement is in 1C |
3 | partial — the writable lines are in 1C, the rest are listed under «Не записано» |
1 | nothing was written: a problem with the whole run, or 1C refused a write |
4 | held — --apply was not allowed to write because warnings are waiting for the user's decision. Nothing was written. Show the warnings; pass the flags the report names only on the user's deliberate decision |
What still stops everything is what is true of the run rather than of one line: the statement's arithmetic does not reconcile, the base is not the company in the file, this base's rules have never been derived, the duplicate guard cannot be proven, or the final reconciliation does not add up.
Re-running after a partial import is the normal next step. Fix what the report asked for and run it again: the lines that landed are recognised and skipped, and only the ones you have resolved are written. No duplicates — a declined line was never written, so there is nothing to duplicate.
Never report a partial import as a finished one. The report says «ЧАСТИЧНЫЙ ИМПОРТ» in its title when it is one, and the exit code is not 0. Tell the user how many lines landed, how many did not, and what each of the latter needs.
6.2 The line to read first: «Сверка с источником»
Every report — dry run and result alike — opens with one table: what was written, what was already in 1C, what is waiting on files, what a standing decision excludes, and what was declined, with a count and a sum for each, against the file's own totals.
Read it before anything else in the report. A clean report is not evidence that a statement landed; this is. Every line of the file is in exactly one of those rows and the totals agree to the tiyn — and a run whose arithmetic does not close says so at the top and exits non-zero, because the only way a line can go missing silently is if nothing counts it.
7. Duplicate protection
Implemented by scripts/1c/odata/_signature.py.
Bank numbers repeat across dates and years, so a number alone is not an identity. Use the composite signature — document type + bank number + operation date + amount — corroborated by direction, BIN/IIN, ИИК, purpose, and КНП. The full reasoning is in Working with 1C over OData §6.
That signature is also the only thing that makes a row a duplicate. Two rows that merely look alike — same amount, same day, different bank numbers — are two movements until something proves otherwise; §13.2 is the bar.
And the signature only finds documents that were written the way you write them. A payment the accountant typed into 1C by hand carries no bank number and no anchor, so it can never collide with a statement line however complete the index looks — see Working with 1C over OData §6, "Signable is not matchable". Before writing, sweep the settlement account over the statement period for documents with no anchor of yours and no matching signature, cross-check them against the statement by (date, amount, direction), and put the hits in the dry run as questions. That sweep is the only thing standing between a hand-entered payment and a second copy of it.
7.1 A transfer between the company's own accounts
Money moved from one of the company's accounts to another is one document in 1C and appears in two statements — as a debit on the source account and as a credit on the destination. Whoever imports first creates it; the other statement then offers the same money again under a different bank number.
1C models it as a different operation kind (ПереводНаДругойСчет in a typical Kazakhstan configuration): no counterparty and no contract, with «Счет контрагента» holding the destination account of the same organisation. Configure the kind in mapping.json (operation_types.own_transfer); leave it empty and the kit reports such lines and refuses to guess.
The line is recognised as already imported, not as an error. Erroring would be permanent — the document is never going to stop existing, so the statement could never be imported at all.
Two things make that recognition work, and one breaks it:
- Matching is on the pair of accounts, direction-agnostic, because the same transfer is a debit in one file and a credit in the other.
- It looks across every document type the run read, not just the one this line classifies as. A base may book these as «Платежный ордер» while the statement says «Платежное поручение».
- It goes quiet if a counterparty card in the base carries one of your own IBANs. That card is usually the by-product of an interrupted import: the other side of an own transfer is named in the statement exactly like a counterparty, with the company's own name and БИН. The kit refuses to create one. If you find one, report it — do not work around it, because with it in place four transfers already in the base look like four new payments.
Measured on one April statement: four such lines, 4 400 000 ₸, all already in the base.
7.2 Lines you have decided not to import
Some statement lines are not this flow's to book — a company whose salary is paid outside it, or a payment the accountant has already entered by hand under 1C's own number (§7). Record the decision in mapping.json and the run stops asking:
"intentional_source_skips": [
{"source_key": "83|2026-04-03|300000.00",
"reason": "Зарплатная строка. Правило компании: зарплату из выписок не разносить."}
]
The key is the statement's own three facts — номер|дата|сумма — normalised the way the duplicate guard normalises them, so padding or a comma decimal cannot make a decision miss. A skipped line is counted, listed in the report with its amount and your reason, and never written, and it is checked before anything else, so a line nobody is going to write does not fill the report with findings about itself.
Write down why, not just that. The report shows the reason back to whoever reads it next month, and «no reason recorded» is how a rule outlives the situation it was written for. These skips are also the right home for a §7 hand-entered payment once the accountant has confirmed it.
7.3 «Это уже в 1С» — записать один раз
A payment the accountant entered by hand carries 1C's own number, not the bank's, so the duplicate guard cannot see it (§7). Once a person has confirmed which document it is, write the answer down and the run stops asking. Three shapes, all read:
"duplicate_decisions": {
"known_existing_by_signature": {
"outgoing_payment_order|91|2026-02-14|1500000.00": "<Ref_Key в 1С>"
}
},
"existing_document_overrides": [
{"source_number": "250", "entity": "outgoing_payment_order", "ref_key": "<Ref_Key>"}
],
"preaccounted_operations": {
"<Ref_Key в 1С>": ["366", "363", "365"]
}
preaccounted_operations is the many-to-one shape: several payments went out, and the accountant entered them as one document. Its only evidence that it names the right document is that the lines add up, so every part of that is checked rather than trusted, and each check is a refusal you can read:
- every number it names must be in this statement. Some present and some missing is refused whole and names the missing ones — a sum cannot be checked against part of a set, and the remainder would read as unaccounted for. None present means the record belongs to another period, and this run says nothing about it;
- the document must be in the base and posted. An unposted draft moves no register, so lines it «covers» would be in no ledger at all;
- «Сумма документа» must equal the lines' total to the tiyn.
Its «Вид операции» is reported, never required: the record says «these lines are that document», not what kind of document it is.
The line is then counted as already imported, never written again, and the report names which of the two records answered — a run that says «already in 1C» without saying how it knows is a run you cannot check.
The order matters and it is deliberate: this is asked only after the duplicate index has said «this row is new». A record can never hide a document the guard can see for itself; it only covers the one case the guard structurally cannot.
7.4 When the base is slow, or stops answering
Reading the whole history is what makes duplicate protection possible — a partial index makes every unread operation look absent — so the run refuses to write when it could not read everything.
- A page that takes longer than a minute is a slow publication, not a broken one. Raise
timeout_secondsinscripts/1c/credentials.json(240 is a reasonable first try). - HTTP 503 with a «Service Unavailable» page means the web server did not hand the request to 1C at all. It comes and goes on a hosted base under load, typically right after a long run. Wait; do not go changing publication settings.
- HTTP 403 with «Ошибка разделения доступа к информационной базе» means a file base is being held exclusively — a configurator session, «Тестирование и исправление», or a session that never let go. It is not a rights problem. Wait, or ask whoever is in the base to close it.
- Check the 1C web client to tell these apart. If the browser opens the base normally and OData does not, nothing is misconfigured.
- A file base serves one request at a time. Do not start a second script against it while a long one is running: the second dies on its own timeout and looks exactly like an unreachable base.
«Вид операции» for fund contributions
A payment of ОПВ, ОПВР, СО, ООСМС or ВОСМС is written as ПеречислениеНалога by default. Some configurations give contributions their own operation kind. If yours does, declare it — and only then does anything change:
"operation_types": {
"pension_contribution": "ПеречислениеПенсионныхВзносов",
"social_contribution": "ПеречислениеСоциальныхОтчислений"
}
pension_contribution covers ОПВ and ОПВР, social_contribution covers СО, ООСМС and ВОСМС. A base that declares neither behaves exactly as before. Every line decided this way names the key that decided it in the report — and if no document the run read from your base carries that operation kind, it warns: enum values come from $metadata, and one your configuration does not publish fails the write.
7.5 «Этот ключ никто не читает»
Every run that can write checks whether the copy of the kit installed in this workspace still reads the decisions your mapping.json holds, and lists them worst first.
This is not a formality. A kit update can replace the module that used to read a key: nothing errors, the report stays clean, the arithmetic still closes, and the run writes as if you had never answered. That has happened — 22 rows, 6 690 234,00 ₸ an accountant had explicitly ordered suppressed came back silently.
What to do when it fires:
- It holds the run, and you can clear it. Read the findings, decide, and re-run with
--accept-warnings. It never blocks outright, because a run you cannot start at month-end is worse than one you have to think about. - A key read by a kit that is not installed here is only information. A workspace with the ЭСФ scripts and no
bank-statement/folder is told which bank-statement decisions it holds, and its runs are not held over them. Install that kit and the same keys hold again if nothing reads them. - Some key names are also table members —
taxesandbank_accountsare entity sets,own_transferis an operation type. For those, a mention in the code is not enough: the kit has to read the key itself (mapping["taxes"],.get("taxes")). - It reads code, not prose. A key mentioned only in a comment or a docstring does not count as read, however clearly the file talks about it.
- If a decision is genuinely obsolete, rename its key with a leading underscore —
_payment_familiesinstead ofpayment_families. The record stays in the file, in your own words, and the check goes quiet. Deleting it loses the reasoning. - If the decision still applies, the kit in this workspace is behind: run
onec_kit_checkand update it. Do not delete the key to silence the warning.
Decisions recorded for ONE line
Where a rule for a whole family of payments cannot answer, the decision is recorded against one statement line and the run obeys it. The order is always the same: a record about this row beats a rule, and a rule beats what the base has done before.
| key | what it decides |
|---|---|
document_type_overrides.by_source_signature | which 1C document your base files this line as. Read FIRST: every key below is keyed by document type, so until this one is settled none of them is visible. The check that no two lines of one file are the same document asks about this type too — the one the line will be written as |
document_type_families | the same, for a KIND of line («Kaspi acquiring is an incoming payment order in this base») — so the decision keeps working next month without being re-recorded |
payment_families | counterparty, account, cash-flow article and VAT for a whole class of payments. If two records match one line the run refuses rather than taking the first |
operation_type_overrides.by_source_signature | «Вид операции» for this line — a transfer between your own accounts, a refund to a customer |
payment_parties.by_signature | «Контрагент» and «Счет контрагента». An empty string means none, not look it up |
organization_account_overrides.by_signature | which of your accounts the payment left, when it is not the account this statement belongs to |
payment_templates.by_signature / .by_entity | a posted document to copy the accounting analytics from; requires_contract: false says this document carries no «Договор»; contract_ref names one outright |
payment_templates.vat_by_signature | «Сумма НДС» and «Ставка НДС» for the row, as an exact amount |
tax_decisions | the tax office for every tax line, and per line the tax itself, its КБК and both settlement accounts |
payment_header_defaults / payment_breakdown_defaults | fields your base puts on every payment header and every breakdown row |
contract_names_by_counterparty | the contract for a counterparty, by name — looked up among that counterparty's own contracts |
vat_by_source_number | VAT keyed by the statement's document number. A number is not an identity: a number the file carries twice is refused, not applied to both lines |
vat_overrides | VAT keyed `дата |
preaccounted_operations | SEVERAL statement lines that one posted document already accounts for (§7.3) |
Two behaviours that are deliberate and may look like omissions:
payment_templates.no_vat_when_not_mentionedis read and not applied as a blanket rule. A statement that does not mention VAT is not a statement that there is none. On one client base 214 acquiring lines worth 131 583 704,00 ₸ mention no VAT and carry 16% of it by that base's ownpayment_familiesrecord, so a blanket «Без НДС» would destroy about 20 000 000 ₸ of VAT to fill in an empty field. Lines covered byvat_by_signatureor by apayment_familiesrecord do get their rate and amount; the rest keep an empty «Ставка НДС», and the run prints the reason.- A family describes
ВидДокумента— the one the bank writes on the line itself, and that is the one the parser now reads. A Kaspi export opens every section asСекцияДокумент=выписка— the name of the FILE, not of a document form — and names the real form («Платежный ордер», «Платежное поручение») on the next line. Until 2026-09-14 the parser preferred the section header, so every line'sdoc_kindread «выписка», no line could ever become a payment order, and a family naming the form matched nothing. An emptyВидДокумента=never overwrites the header, and the «со списком» suffix is read from either field. - The 1C field names for a tax are found, never guessed.
tax_decisionsnames a posted document of your base that already carries every value it records, so the field holding that value in that document is the field the run writes it into. A value the example document does not carry is reported instead of being placed by a guessed name — an unknown property makes 1C refuse the whole write, and a wrong КБК never shows up in the journal. - A recorded document type is now APPLIED, with one refusal. These keys are keyed by document type as well as number, date and amount, so the type decides which of your other records the run can see. It obeys yours. The single case it refuses: your record reverses the direction on a line the statement itself shows as being between two DIFFERENT identified parties — that is not an own transfer seen from the other end, and writing it would book the money against the wrong side. A record under a type NOTHING names for the row is still reported by name rather than going silent.
Two things that follow from that change, both worth knowing before you re-run a month:
parse-statement.pysays when the row decided the form — one line for the whole file, naming both words and the number of rows, e.g. «797 lines carry their own ВидДокумента and it disagrees with the section header: «выписка» → «Платежный ордер»». It is not a warning. It is there so you can see which of the file's two fields the parse believed.- Re-importing a month you already imported before this change will not duplicate it. A 1C document's identity includes its type, so a line that moves from «Платежное поручение исходящее» to «Платежный ордер» would otherwise look like a line nobody had imported. The duplicate check now asks all four payment document types, not just the one the run is about to write into: a document found under another type counts as already imported, the run writes nothing, and the report names both types and the document so you can decide. The kit will never move a document from one type to another — that is an accountant's decision and it is made in 1C by hand.
8. Classifying each operation
Implemented by scripts/1c/bank-statement/_classify.py. What it cannot decide offline — which tax, which aggregator counterparty — it raises as a question rather than guessing.
| Bank data | Direction | 1C document | Typical operation |
|---|---|---|---|
Платежное поручение | Debit | Outgoing payment order | By purpose: supplier, tax, own funds |
Платежное поручение | Credit | Incoming payment order | ОплатаПокупателя |
Платежный ордер | Debit | Payment order, debit | Usually ОплатаПоставщику |
Платежный ордер | Credit | Payment order, credit | Acquiring sale or refund |
Платежное поручение со списком | Debit | Outgoing payment order + basis documents | Only when a separate employee list exists |
| Tax payment | Debit | Outgoing payment order | ПеречислениеНалога |
| Penalty (пеня) | Debit | Outgoing payment order | ПеречислениеНалога, type ПениСам |
Classification is never decided by the words «платежное поручение» alone. Direction, КНП, purpose text, and the presence of a list all matter.
9. Ordinary payments
For an outgoing payment, verify: organisation; organisation's settlement account; counterparty; the payee ИИК from the statement; contract; operation type; cash-flow item; accounting and tax accounts; amount; statement date; bank number; purpose; КНП; and VAT where applicable.
For supplier payments, resolve counterparty and account by BIN/IIN and ИИК, and copy the purpose text from the bank without abbreviating. If the purpose references an invoice, check for the matching 1C document where possible — but the absence of a link must never become an invented one.
For an incoming customer payment: match the payer to a counterparty, match their ИИК to a bank account, use operation type ОплатаПокупателя, and copy amount and purpose from the bank. If the purpose states VAT explicitly, take the rate from Catalog_СтавкиНДС and make the VAT amount match the bank's text exactly. If VAT is not stated, do not infer it from the amount.
For contracts, use the counterparty's main contract unless the purpose or a basis document names a different one. Never auto-create a contract just because the main one wasn't found.
Own-funds transfers and acquiring/QR schemes are local accounting policy. How a particular business books a transfer to its owner's card, or which counterparty stands in for a payment aggregator, is a decision confirmed by that user for that database. Follow the scheme already used in that database's earlier documents, record it in the workspace's own notes, and never carry it into another database automatically.
9.1 Settlement or advance — 3310 against 1710
A payment to a supplier who is owed money settles a debt (Дт 3310). A payment to a supplier who is owed nothing is an advance (Дт 1710). A customer receipt is the same question one account over: 1210 against 3510. The two look identical on the bank statement and differ only in what the base already holds.
This is off unless the workspace has configured it, and a workspace that has not is unchanged — the account then comes from what this base puts on comparable posted documents, exactly as before. Where it is configured, the balance is computed from the base's own posted documents, so no register has to be published: each source in mapping.json declares the direction it moves the balance by, +1 for a document that raises what you owe a supplier and -1 for one that reduces it.
Four things it will not do, and each of them is the point:
- A proven settlement changes nothing. That account already comes from this base's own posted history, and replacing it with a constant from the settings would trade evidence for a guess — including on the bases whose accountant leaves the field empty deliberately. Only the advance is written, because the advance is the case that history cannot reveal.
- A zero on a brand-new contract is not «no debt». A supplier moved onto a fresh contract carries a zero there while the debt still stands on the old one. The run says so, names the amount, and changes nothing. Moving a debt between contracts is a correction an accountant makes deliberately — never a mass rewrite of earlier documents.
- A payment larger than the debt is not rounded to one account. It settles the debt and the rest is an advance. Whether the document can carry both accounts is asked of the base itself (
allow_split: "auto", the default): where it cannot, or whereallow_splitisfalse, the run reports both halves and leaves the account alone. - Each account is written only where the document has that field. On the configuration we measured, the advance account (
СчетУчетаРасчетовПоАвансам_Key, without «БУ») exists on the payment breakdown row and not on the document header — and a single field the document does not have makes 1C reject the whole document. So the run reads the base's field list once, writes each account only where it exists, and names in the report any configured field it could not place. - An empty advance account is filled. 1C divides a payment into «Оплата» and «Оплата (аванс)» by itself when it posts, and if the advance account on the row is empty it books the advance to the settlement account without any error. With the accounts configured, a payment row that would carry no advance account gets the configured one; a row that already has one is left as it is, and nothing is filled where the debt may be on another contract.
- Nothing is blocked. Every refusal is a note in the report; the line still imports.
The report distinguishes «3310 (остаток по договору на 22.06.2026: 450 000,00 ₸)» from a line that could not be checked. Those are the same account for two very different reasons, and only the first one is an answer.
9.2 Every payment carries a cash-flow article
«Статья движения денежных средств» decides which line of the cash-flow statement the money lands on. A payment order without one passes every arithmetic check, looks complete in the journal, and is wrong in the report — which is why it has to be an error rather than a warning.
The kit writes one РасшифровкаПлатежа row carrying the whole amount and the article, and refuses to write anything at all while the article cannot be resolved. Configure it in mapping.json:
"cash_flow_articles": {
"default_incoming": "Реализация работ и услуг",
"default_outgoing": "Расчеты с поставщиками и подрядчиками",
"by_knp": { "851": "Расчеты с поставщиками и подрядчиками" }
}
Do not copy those names as defaults. The article is this company's accounting policy: read what this base's own earlier documents used for the same kind of operation, and record that. by_knp overrides the direction default per КНП. Articles are named, not pinned by Ref_Key, because a Ref_Key means nothing in another database (§11 of the OData rules) and this catalog exposes no other identifier.
This is an error rather than a warning because the failure is invisible: an import can write every line, balance every total, and reconcile clean while the article is empty on most of them. Reconciliation now checks the article too, and treats 1C's all-zeroes GUID as absent rather than as a value.
9.2.1 Which article wins, when more than one rule could apply
Narrowest first, and nothing beyond the last step — guessing the article is guessing which line of the cash-flow statement the money lands on:
by_signature— this one row. An exception only you know about.by_entity_knp— this КНП within one document kind. The same КНП means different things incoming and outgoing, and this is the level that tells them apart.by_knp— this КНП.default_incoming/default_outgoing.
onec_learn_base derives steps 3 and 4 from the base's own posted documents and can never derive step 1 — nobody but you knows that this row is an exception.
9.3 Card payments cannot be posted without an acquiring contract
If the user asks for «Оплата платежными картами» documents alongside the acquiring receipts, check first that Catalog_ДоговорыЭквайринга and Catalog_ВидыОплатЭквайринга are not empty — check-access.py reports this. When they are, Post() answers HTTP 500 with no usable detail, and no payload will fix it: the reference data the posting algorithm needs does not exist in the database.
Say so before creating any. Card payments written against an empty acquiring catalog are not partial progress — they are unpostable drafts sitting in a live base looking like work that got done.
10. Taxes
A tax payment uses operation ПеречислениеНалога and requires ВидНалога_Key, КБК, КНП, the tax authority and its account, accounting and tax accounts, the tax-type subconto, payment type Налог, and the amount and purpose from the statement.
Never choose the tax type from КНП alone. Different taxes share a КНП while differing in КБК and accounting accounts — for example, both withholding income tax and social tax commonly arrive under КНП 911 and are distinguished only by КБК.
10.1 The statement has no КБК — that is normal, and not a reason to stop
Kazakhstan bank statements do not carry a КБК field. Its absence is the ordinary case, not missing information, and it must not by itself become a question for the user. The КБК is not a value you choose — it is an attribute of the catalogue element you choose. The document references the tax by ВидНалога_Key, and Catalog_НалогиСборыОтчисления.КодБК comes along with it. So resolve the element, in this order, and stop at the first step that decides it:
- Match the payment purpose to a catalogue element. Read
Catalog_НалогиСборыОтчисленияand match the statement's назначение платежа againstDescription. «Таможенный сбор», «Таможенная пошлина» and «НДС при импорте» are distinct elements with distinctКодБК— the purpose text separates them even when КНП does not. - Corroborate against posted history. Look for posted documents with the same КНП, the same tax authority, and the same or a near amount. A match confirms the element and is the strongest evidence available; record it in the report.
- Only then ask — and only when two or more elements genuinely fit the purpose and history cannot separate them. Name the candidates and say what distinguishes them, rather than asking the user to supply a code.
Do not invent a КБК that is not on a catalogue element, and do not write the document with an empty one. But equally: a purpose that maps cleanly onto exactly one element, corroborated by posted history, is a resolved row — create it. Asking the user to confirm a code that their own database already stores is not a safety control; it stalls the import and teaches them the assistant cannot read their own 1C.
10.2 When the answer is a standing decision, record it once
Some lines cannot be resolved from the file or from history — a tax this base has never paid, or a bank that exports a КБК the base's own catalogue does not use. That is a decision, and once the accountant makes it, it belongs in mapping.json rather than in a chat message that the next month's run cannot read:
"tax_fields": {
"tax_ref_field": "ВидНалога_Key",
"kbk_field": "КодБК",
"by_kbk": {
"101110": { "name": "Налог на прибыль (корпоративный подоходный налог)", "write_kbk": "101101" }
}
}
The key is the code the statement carries; name is the catalogue element it means. write_kbk is optional and only for the case above — a code in the file that the base holds under a different one — so the report always names both codes when they differ. Two things it will not do: use an element whose own КодБК contradicts the code being written, and fill ВидНалога_Key on a base that does not publish it (leave tax_ref_field empty there — an unknown property loses the whole write).
A recorded rule silences the «which tax is this» question for that КБК and nothing else. Every other question still gets asked.
10.4 The accounts and the subconto come from this base's payments of the same tax
A tax payment also carries settlement accounts — СчетУчетаРасчетовСКонтрагентомБУ_Key, its НУ twin — and the tax-type subconto. Since 2026-09-02 the importer fills them from this base's own posted payments of the same tax, and the scope is the load-bearing word: measured across one customer's whole posted history, all 26 posted ПеречислениеНалога carry those fields while 0 of the 151 ordinary payments do, and inside that one operation kind they use four different accounts, one per tax — 3150 for social tax, 3120 for withholding income tax, 3110 for КПН, 3131 for VAT. Asked per operation kind, the answer is the most frequent of the four: a real liability account belonging to another tax, on a draft that reads as finished.
So name the fields — they differ per configuration, and a property this base does not publish makes the whole read answer 400, not just that field — and let the base answer:
"tax_fields": {
"tax_ref_field": "ВидНалога_Key",
"recall_by_tax": [
"СчетУчетаРасчетовСКонтрагентомБУ_Key",
"СчетУчетаРасчетовСКонтрагентомНУ_Key",
"СубконтоДтБУ1", "СубконтоДтБУ1_Type",
"СубконтоДтНУ1", "СубконтоДтНУ1_Type"
]
}
Verify each with check-field.py <entity-set> <field> first. The recall needs tax_ref_field, because that is the field the question is scoped on, and it uses whichever tax the line ended up with — a recorded by_kbk rule or §10.1's purpose recall, whichever answered.
The answer must be unanimous across the posted payments of that tax. One posted payment is an answer, and the report says how many it counted so you can judge how thin that is. A disagreement, a truncated read, or a tax this base has never paid leaves the fields empty and says so — and it also removes any value the wider per-operation template had put there, because a plausible account belonging to another tax is worse than a blank one: the blank is the thing an accountant looks at twice. No history is never an error. A company paying a tax for the first time still gets its statement imported, with those fields for the accountant, exactly as before.
10.3 The tax authority is a counterparty like any other
A tax payment names УГД by БИН in the statement, so it resolves the same way every other counterparty does, and lands on the document in Контрагент and Счет контрагента. Until 2026-09-01 the importer skipped that lookup for tax lines and wrote them empty; if you are looking at older documents, that is where the blanks came from. When the tax office genuinely cannot be resolved the line still gets created without it, with a warning — one unknown УГД does not stop the rest of the statement.
The same reasoning applies wherever the statement lacks a field the database already knows: prefer the catalogue, then the base's own posted history, and reserve questions for real ambiguity and for decisions that are genuinely the accountant's — which rows to skip, which counterparties to create, how to treat an unfamiliar receipt.
11. Penalties (пени)
The КНП tables are in scripts/1c/bank-statement/_kz_codes.py; the classification is in _classify.py.
Penalties on fund contributions are not list payments. They are recorded as:
- operation
ПеречислениеНалога; - payment type
ПениСам; - the matching element of
Catalog_НалогиСборыОтчисления; - КНП and the fund account from the statement;
- amount and purpose from the statement;
- empty pension and social tabular sections.
| КНП | Penalty on | Contribution element |
|---|---|---|
| 098 | Employer pension contributions (ОПВР) | Обязательные пенсионные взносы работодателя |
| 019 | Pension contributions (ОПВ) | Обязательные пенсионные взносы |
| 017 | Social contributions (СО) | Обязательные социальные отчисления |
| 124 | Medical insurance contributions (ВОСМС) | Взносы на обязательное социальное медицинское страхование |
The enum value is ПениСам. There is no Пеня value in this configuration — using it produces an OData error. Read the EnumType from $metadata if in doubt.
12. The five list payments
Recognised by scripts/1c/bank-statement/_employee_lists.py and _kz_codes.py. You should never need to write a parser for these again.
Only these five require employee lists:
| КНП | Payment | 1C basis document | Payment tabular section |
|---|---|---|---|
| 010 | ОПВ | ОПВПеречислениеВФонды | ПеречислениеПенсионныхВзносов |
| 089 | ОПВ работодателя (ОПВР) | ОПВПеречислениеВФонды | ПеречислениеПенсионныхВзносов |
| 122 | ВОСМС | СОПеречислениеВФонды | ПеречислениеСоциальныхОтчислений |
| 121 | ООСМС | СОПеречислениеВФонды | ПеречислениеСоциальныхОтчислений |
| 012 | Социальные отчисления (СО) | СОПеречислениеВФонды | ПеречислениеСоциальныхОтчислений |
12.1 The triple amount control
Three amounts must agree:
payment order amount
= sum of the linked basis documents
= sum of the per-employee rows
Any discrepancy blocks posting.
12.2 Period
Период=062026 becomes 2026-06-01T00:00:00. Write it to ПериодРегистрации, and for social rows also to МесяцПериода.
12.3 Separating the proprietor's own amounts
Where an individual entrepreneur's personal contributions are booked separately from employees', that separation is local accounting policy — follow the pattern used in that database's previous periods, and confirm it with the user rather than inferring it. Typically it means separate basis documents for the personal portion of ОПВ, СО, and ВОСМС, while employer pension contributions go as one basis document covering all rows.
12.4 Basis document operation types
| Payment | ВидОперации of the basis document |
|---|---|
| ОПВ | ПеречислениеОбязательныхПенсионныхВзносов |
| ОПВР | ПеречислениеОбязательныхПенсионныхВзносовРаботодателя |
| СО | ПеречислениеОбязательныхСоциальныхОтчислений |
| ООСМС | ПеречислениеОтчисленийОСМС |
| ВОСМС | ПеречислениеВзносовОСМС |
12.5 Row structure
For ОПВ/ОПВР:
ФизЛицо = Ref_Key of the individual
ФизЛицо_Type = StandardODATA.Catalog_ФизическиеЛица
Сумма = the employee's amount
For СО/ООСМС/ВОСМС, additionally:
МесяцПериода = first day of the accrual month
Employees are matched by IIN via Catalog_ФизическиеЛица.ИдентификационныйКодЛичности, confirmed by name and date of birth. If two individuals share an IIN, use the reference that previous correct documents already used — never pick one at random.
12.6 Linking payment to basis
In the payment order, create a row:
Документ_Key = Ref_Key of the basis document
СуммаКПеречислению = the basis amount
Tabular rows cannot be created through their own entity set, so this row must be part of the payment order's own POST.
12.7 The correct order
- Read the main statement.
- Detect «Платежное поручение со списком».
- Do not create a payment document from the aggregate amount.
- Obtain the separate list file.
- Verify bank number, КНП, period, and total against it.
- Verify the sum of all employees.
- Match each employee by IIN.
- Separate the proprietor's personal amounts if that is the database's practice.
- Create the basis documents, unposted.
- Verify their rows and amounts.
- Create the payment order with the reference rows already filled in.
- Re-read the created document from 1C.
- Run the triple amount control.
- Only now show the document to the user for review.
13. Final reconciliation
Implemented by scripts/1c/bank-statement/reconcile.py.
After the import, build an independent index of 1C documents and re-match every statement line. The required result:
- every statement line found;
- missing lines: 0;
- unexpected duplicates: 0 — counted on the account, by
(date, amount, direction), not on your signature alone. A signature-only count cannot see the twin you just created next to a hand-entered document, which is the duplicate this step exists to catch (§7); - incoming total matches;
- outgoing total matches;
- closing balance matches;
- the account's own 1C turnover, applied to the statement's opening balance, lands on the statement's closing balance (§13.1);
- every intentionally skipped row is listed with its amount, and the balance variance it causes is stated in advance (§13.1);
- all new documents unposted;
- all new documents not marked for deletion;
- a cash-flow article on every breakdown row — an all-zeroes GUID is absent, not a value, and a section the OData composition does not publish is unchecked, which the report says rather than passing over;
- КНП and КБК match;
- bank accounts match the statement;
- employee lists are populated;
- list sums match.
For each line, check not just presence but the correct document type.
13.1 Reconcile against the bank, not against your own decisions
A reconciliation that matches the rows you decided to import against the rows you decided to expect is comparing a set with itself. Every row dropped along the way — as a suspected duplicate, as ambiguous, as one for the accountant — leaves both sides at once, so the report reads «0 missing», the totals tie, a repeat dry run proposes 0 documents and every test passes. Then the accountant opens the account and it does not tally. That has happened on a real import, and nothing in the run said anything was wrong: the check was blind to the skipped rows by construction.
So put the bank on the other side of the comparison:
- Compare the account's 1C turnover to the statement's own header. Sum the debits and credits actually recorded in 1C on the organisation's settlement account over the statement period, apply them to
НачальныйОстаток, and require the result to equalКонечныйОстаток. This is the only control in the whole regulation with an outside reference. The four §5 equalities prove the file is internally consistent; per-line matching proves each document you created corresponds to a line you kept. Neither one can notice a line you never kept. - Report skipped rows as a named, quantified variance — before the accountant finds it. Every intentionally excluded row belongs in the report by bank number, date, amount and reason, with a total, and the balance check must state the difference it causes explicitly: "1C closing is X below the statement — expected: N rows skipped as Y, totalling Z." A variance you predicted is a decision the accountant can accept or overturn in a minute. The identical variance discovered afterwards is a defect, and what they find first is the missing sum, not the rows behind it.
- Never report a clean reconciliation over a set you narrowed. If rows were skipped, "everything reconciles" is false no matter what the counters say. Say what was excluded, and what the books therefore show.
13.2 The bar for calling a row a duplicate
Same amount on the same day is not, by itself, evidence of a duplicate. Ordinary business produces look-alike movements: merchant and acquiring settlements land repeatedly within a day, a supplier is paid twice for two deliveries, a retried transfer follows a failed one, and a scheduled standing debit — a loan repayment reserve, a daily sweep — fires most business days at a fixed amount, so two or three of them on one date is its normal shape rather than a defect. In a 1CClientBankExchange file each of those carries its own bank document number, and two different bank numbers mean the bank recorded two movements.
Three checks are cheap and usually decisive:
- The statement's own arithmetic. If
Начальный остаток + Поступления − Списанияequals the bank's statedКонечныйОстатокonly when all the look-alike rows are counted, then all of them are real money — the equality is the bank asserting that exact row set. Drop one and it stops holding, which is precisely the shortfall the accountant will see. - How that same row behaves on every other day of the file. Take the amount, purpose and counterparty and count them across the whole statement. A payment that stands alone on twenty dates and doubles up on three of them is a schedule, not a re-export — and a same-day-same-amount rule flags only those three, which is exactly backwards: it clears the days that prove the pattern and stops on the days that follow it. A recurring fixed amount is identical by design, so identity is evidence of a schedule, not of a duplicate.
- The base's own history and the workspace's notes. A company's accumulated knowledge may already have settled a specific look-alike pair as two genuine transactions. Read it before re-opening the question, and write the answer back so the next import does not re-open it either.
A duplicate worth acting on is a re-export artefact: the same document type, bank number, operation date and amount twice in the file, or a document already in 1C carrying that composite signature (§7). That is what «unexpected duplicates: 0» means. Anything weaker is a question for the accountant, not a decision for the assistant — and if they do confirm a skip, it becomes a quantified variance under §13.1 rather than a silent omission.
When the §5 arithmetic balances over the full row set, there is no artefact to find — stop looking. A re-export duplicates a row without the bank duplicating the money, so it breaks the header equalities by exactly its own amount. If Начальный остаток + Поступления − Списания = Конечный остаток holds with every look-alike counted, the bank is asserting that each of them moved money, and the question is closed by the file itself. Do not put it to the accountant anyway. Asking "are these three 347 825 ₸ debits duplicates?" over a file that already proved they are not invites a "yes" that costs real turnover — and the answer arrives as an instruction, so nothing downstream questions it again.
A duplicate decision removes copies. It never removes the original. N look-alike rows can resolve to at most N−1 skips; "skip the whole group" is not a possible outcome of a duplicate finding, whoever proposes it. If the accountant asks for the group to be dropped entirely, that is a different decision — those rows are being excluded for some other reason — and it must be recorded and reported as such under §13.1, not filed under duplicates. This is not a hypothetical distinction: a real import skipped three whole groups on that instruction and lost 2 367 455 ₸ of genuine debits, every one of them a movement the bank had recorded.
14. The accountant's manual checklist
Before posting, a human checks:
- Organisation is correct.
- The organisation's settlement account matches the statement.
- Document date and statement date are right.
- The bank number was carried over correctly.
- The amount matches to the tiyn.
- The counterparty was found by BIN/IIN.
- The counterparty's ИИК matches the statement.
- The payment purpose is not distorted.
- КНП matches.
- For taxes, the right КБК was chosen.
- For penalties,
ПениСамwas used. - For list payments, every employee is visible.
- The employees' period is right.
- The employees' total equals the payment.
- VAT matches the bank's purpose text.
- There is no second document with the same signature.
- Every skipped row is listed with its amount, and the balance difference it causes was stated in advance.
- The account's closing balance in 1C matches the statement, or differs by exactly that stated amount.
- The document about to be posted is the corrected version.
15. The report
Rendered by scripts/1c/odata/_report.py; pass --report <path> to any entrypoint.
Every import ends with its own report file — keep them in synced_data/1c/, one per statement — covering: source and period; settlement account; operation count; incoming and outgoing totals; balance control; how many documents already existed; how many were created; which rows were skipped, with their amounts and the balance variance they cause (§13.1); how many duplicates were found; the breakdown by document type; list payments and the result of the triple reconciliation; taxes and penalties with КНП/КБК; missing catalog data; documents that must not be posted; and the table of replacements and documents to delete.
16. The assistant's short checklist
- Read the workspace's
index.md, its context notes, and this regulation. - Check OData access.
- Read only the files the user explicitly named.
- Parse the statement and verify the arithmetic.
- Find existing documents by composite signature.
- Classify the operations.
- Match counterparties by BIN/IIN.
- Match accounts by exact ИИК.
- Halt any list payment that has no separate file.
- Verify taxes and penalties by КНП/КБК and operation type.
- Run the dry run.
- Show the user any warning that affects the accounting.
- Create unposted documents only.
- Re-read the created documents from 1C.
- Reconcile every line and every total.
- Produce the report.
- Do not post anything without a separate instruction.
17. When the operation is finished
Technically finished when: every statement line is matched; no lines are missing; there are no unexplained duplicates; amounts and balances agree; catalogs are matched; list payments contain their employees; taxes and penalties are correctly classified; every new document is unposted; and the user has the report and the list of manual actions.
Finished in accounting terms only after the human review, the deletion of wrong drafts, and the posting of the approved documents in 1C.