Plank help · updated 2026-09-17
Работа с 1С через OData
Правила чтения и записи базы 1С через стандартный интерфейс OData — что интеграционный пользователь может и чего не может, как распознать уже существующий документ, как безопасно создать новый и какие ограничения сервера определяют устройство любого скрипта.
Agents: fetch the raw markdown of this page at /ru/help/1c-odata-working-rules.md
Работа с 1С через OData
Эта страница — механика работы с базой 1С через стандартный интерфейс OData. Она верна для любой базы и любой конфигурации.
- Как включить OData и сохранить учётные данные — сначала Подключение 1С.
- Регламент разноски банковской выписки и зарплатных платежей (Казахстан), построенный на этих правилах — Разноска банковской выписки в 1С.
- Документы продажи и ЭАВР — Реализации по счетам покупателям; месячная зарплатная цепочка — Начисление зарплаты.
Скрипты уже существуют. Вся механика с этой страницы поставляется готовым и адаптируемым Python-кодом в стартовом ките 1С — проверка доступа, сопоставление справочников, сигнатура дубля, дисциплина «создать непроведённым и перечитать». Проверьте наличие
scripts/1c/прежде чем писать что-либо из этого, и установите кит, если его нет. Каждый раздел ниже называет файл, который его реализует.Адаптируйте кит, а не пишите свой писатель. Разовый скрипт, названный по клиенту этого месяца, — соблазнительный и неверный путь: он начинается без dry-run-гейта, без защиты от дублей и без проверок, делающих повторный запуск безопасным, — и в течение часа применяется к живой учётной базе. Все случаи дублирования выписок до сих пор происходили из скриптов, написанных вручную, и ни один — из кита. Если кит не подходит вашей базе, правьте
mapping.json, затем конструкторы payload'ов в ките; а если новое всё же необходимо — используйте_guard.py: это три строки, и именно в них вся разница между безопасным скриптом и теми, из которых происходили все случаи дублирования:
from _guard import Guard
guard = Guard(client, catalogs, run_key=make_run_key(account, period, declared_total))
guard.create("outgoing_payment_order", payload,
number=op.number, date=op.operation_date, amount=op.amount)
Guard.create проставляет якорь и проверяет, что он сохранился, возвращает уже существующий документ вместо создания второго, отказывается работать, если индекс не удалось прочитать полностью, и никогда не проводит. run_key должен строиться из значений самого файла-источника — счёт, период, заявленные итоги, количество операций — и никогда из часов, иначе каждый запуск выглядит уникальным и защита не защищает.
Ещё две вещи вы получаете без дополнительных действий:
- Журнал записи. Каждый созданный документ и каждый предотвращённый дубль дописывается одной JSON-строкой в
scripts/1c/write-log.jsonl. Он никогда не ломает импорт, если записать его не удалось, и это единственная запись о том, что эта рабочая область записала в 1С, которая не требует перечитывать переписку. Журнал работает только на запись: ничто не сверяется с ним, решая, создавать ли документ, — документ, удалённый бухгалтером в 1С, остаётся в журнале, и отказ создать его заново был бы ошибкой. - Отчёт.
guard.report(source="kaspi-06-2026.csv")возвращает итоговый markdown — количества, ключ запуска, созданные документы с ихRef_Keyи предотвращённые дубли — построенный из того, что защита действительно сделала. Сохраните его черезwrite_reportвsynced_data/1c/. Используйте его вместо собственного пересказа: именно пересказанный итог и оказывался неверным, а этот не может расходиться с тем, что записано.
Самое важное на этой странице: выясните, что вашему интеграционному пользователю вообще разрешено, до того как что-то на этом строить. Урезанный пользователь — чтение и создание, без изменения, удаления и проведения — это то, что провайдеры выдают по умолчанию, и он меняет всю форму работы. Если у вас именно такой, скажите об этом сразу и добейтесь расширения прав (§3). Если расширить нельзя — собирайте каждый документ целиком, проверяйте локально и только затем выполняйте POST: второй попытки не будет.
1. Адреса
Адрес веб-клиента и адрес OData — разные адреса.
веб-клиент https://<хост>/<база>/ru_RU/
OData https://<хост>/<база>/odata/standard.odata
метаданные https://<хост>/<база>/odata/standard.odata/$metadata
Исправный endpoint возвращает XML или JSON либо запрашивает пароль. HTML-страница означает, что OData не опубликован — см. раздел о диагностике в Подключении 1С: там про два независимых переключателя и про «дружелюбную» страницу об ошибке. Это верно только для первого ответа: если та же база в этом прогоне уже отдавала данные, а потом ответила страницей, значит сессия прервана или хостинг на обслуживании — кит открывает новую сессию и один раз повторяет чтение, а иначе сообщает odata-session-lost («хостинг ответил страницей вместо данных … повторите позже»). Публикацию в этом случае проверять не нужно, а запись никогда не повторяйте — перечитайте документ и посмотрите, прошла ли она. А если страницу получает один запрос, а остальные к этой же базе отвечают данными (кит проверяет это контрольным чтением одной строки), ошибка — odata-request-page, и в ней назван сам запрос: читайте документ узко ($select), а поле найдите командой python3 scripts/1c/odata/probe-document.py <набор> <Ref_Key> --mapping … --credentials … (только чтение).
Учётные данные находятся только в scripts/1c/credentials.json. Пароль нельзя копировать в документацию, журналы, отчёты или исходный код.
2. Проверьте доступ до всего остального
Проверку доступа выполняют до чтения первой же учётной записи:
python3 scripts/1c/odata/check-access.py
Исправный результат — HTTP 200, application/xml для $metadata и сотни entity sets: типовая бухгалтерская конфигурация публикует несколько сотен. Если наборов единицы, значит состав OData (Администрирование → Состав стандартного интерфейса OData) почти ничего не открывает и его нужно расширить.
Ноль наборов при ответе 200 — это ошибка, и проверка так и говорит. База может вернуть $metadata с корректным, но пустым контейнером: OData опубликован, а состав внутри 1С ничего не выбирает. Исправляет это администратор в 1С, а не веб-сервер.
Ещё проверка сообщает, фильтрует ли база на сервере — «Server-side filtering: works», «REFUSED» или «UNRELIABLE» — по трём запросам на чтение к справочнику контрагентов. Если база отказывает, кит один раз за прогон читает справочники целиком и сопоставляет локально, поэтому прогон дольше. Скажите бухгалтеру, что это свойство её базы, и не повторяйте запросы с $filter в надежде на другой ответ (§4).
2.5 Сначала установите, какая это компания
У бухгалтерской практики нет «базы 1С». У неё по базе на клиента — у одной изученной практики их 17 — а один credentials.json плюс один mapping.json описывают ровно одну из них.
Неверная пара не выглядит неверной. Подключение проходит. Справочники читаются. Арифметика сходится. Сверка возвращается чистой. А документы оказываются в учёте другой компании.
Перед любой записью должны совпасть три вещи, и ни одна не следует из остальных:
- база, до которой дотягиваются учётные данные;
- организация, закреплённая в
mapping.json; - исходный документ — эта выписка, эта ЭСФ, этот запрос.
_identity.py проверяет 1 против 2 (verify_target) и даёт assert_source_account для 3. Оба скрипта вызывают их до того, как что-либо запланировано, и оба отказа — ошибки, а не предупреждения.
База, доступная только через портал хостера
У части баз нет собственного адреса: хостер публикует их за порталом, который сначала авторизует, а потом перенаправляет на OData-корень конкретного арендатора. Такая база настраивается блоком portal в credentials.json — вместо base_url, а не вместе с ним. Два записанных ответа на вопрос «какая база» означают, что проверяется только один из них.
publication — это то, с чем сверяется адрес приземления, и на нём держится вся безопасность схемы: портал, который успешно вас авторизовал и высадил в другом арендаторе, дальше отвечает HTTP 200 на всё.
Не пишите для такой базы отдельный коннектор. 28.08.2026 такой жил в одном воркспейсе приватным скриптом — и поиск шести документов «по всем OData-подключениям» молча пропустил эту базу, вернул «их нигде нет» и в одном шаге от того, чтобы пометить на удаление шесть проведённых документов в базе посторонней компании, совпавших по номерам. Транспорт, который невозможно перечислить, превращает отрицательный результат в ложь.
В одной базе могут быть две организации с почти одинаковыми названиями
В базе могут быть две организации, названия которых различаются только пунктуацией или порядком слов — одно и то же торговое имя, зарегистрированное дважды, или миграция, оставившая обе. Выбирать на глаз — это подбрасывать монету, поэтому решает закреплённый ref_key, а название — это утверждение, которое проверяется: если ссылка ведёт на другое имя, чем заявляет mapping.json, одно из двух устарело, и запуск останавливается. Не считайте, что ссылка «точнее и потому права» — устаревшая ссылка это ровно то, как запуск попадает в клиента прошлого квартала.
Если в базе больше одной организации, напишите об этом в отчёте даже при успехе. Журнал, в который смотрит бухгалтер, может быть отфильтрован по другой — и тогда «я ничего не вижу» относится к его фильтру, а не к вашей записи.
Исходный документ должен быть этой компании
verify_arithmetic доказывает, что файл выписки внутренне непротиворечив. Он доказывает это ровно так же, когда файл принадлежит кому-то другому, — то есть это не проверка компании. РасчСчет самой выписки должен совпадать с mapping.organization_account.iban, иначе запуск отказывает.
То же правило обобщается: для входящей ЭСФ базу выбирает организация, указанная в ЭСФ, а не адрес, который лежит в общем файле учётных данных. См. Загрузку входящих ЭСФ в 1С.
Работа с несколькими компаниями
Дайте каждой компании своё рабочее пространство со своими scripts/1c/credentials.json и scripts/1c/mapping.json. Если одно пространство должно дотягиваться до нескольких баз, держите их реестр — компания, БИН/ИИН, адрес базы, Ref_Key организации, расчётные счета — и определяйте цель по исходному документу через этот реестр. Чего делать нельзя никогда: переносить Ref_Key из одной базы в другую (§11) и переиспользовать mapping «потому что компании похожие».
3. Установите, что разрешено интеграционному пользователю
Делайте это при настройке, а не когда запись упадёт. Права различаются от пользователя к пользователю, и урезанный вариант ниже — это то, что провайдер выдаёт, если никто не попросил большего:
| Операция | Урезанный пользователь | Полная роль |
|---|---|---|
Чтение (GET) | Да | Да |
Создание документов (POST) | Да | Да |
Изменение документа (PATCH / PUT) | Нет | Да |
Удаление / пометка на удаление (DELETE) | Нет — ответ Нарушение прав доступа | Да |
Проведение (действие Post()) | Нет | Да |
| Создание строк табличной части отдельным entity set | Нет | Нет — ограничение конфигурации, а не прав |
Просите полную роль. Когда пользователь подключает 1С, скажите ему запросить у администратора базы чтение, создание, изменение, удаление / пометку на удаление и проведение — в том же сообщении, что и публикацию OData: это тот же человек и то же ожидание. Пользователь «чтение и создание» выглядит достаточным ровно до первого неверного черновика, после чего ничего нельзя ни исправить, ни удалить, ни довести до конца без ручного входа в 1С. Расширять роль потом — значит снова идти к провайдеру; сузить её — тривиально. Готовая формулировка для отправки есть в Подключении 1С.
Отказ в записи не выглядит как отказ
Это ловушка, и она уже стоила реальному импорту трёх часов. Когда у пользователя нет права на изменение объекта, 1С отвечает не 403, а вот этим:
HTTP 500
Не удалось записать "Платежное поручение (исходящее) 00000000672 от 22.06.2026"!
Читается это как претензия к вашему payload, и обычно так и читается — будто «OData в 1С не умеет изменять документы», о чём пользователю сообщают как об ограничении платформы. Это не так. Тот же PATCH без единого изменения проходит под пользователем, у которого есть право изменения.
Поэтому при отказе в записи: payload и матрица прав равновероятны, и по ответу их не различить. Проверяйте права первыми, потому что это дешевле — спросите администратора базы или попробуйте тот же запрос под пользователем с «Изменением». Никогда не делайте вывод о 1С или об OData вообще по одной пятисотке.
И какой бы ни была причина: не отвечайте на отказ созданием документа-замены с оставленным оригиналом. См. §10.
Кит проверяет это сам — на первом документе каждого вида. Сразу после создания create_and_verify записывает обратно несколько собственных значений этого документа без изменений. Если 1С отказывает с ошибкой прав, этот один документ остаётся (он учтён как записанный), больше документов этого вида в прогоне не создаётся, а отказ содержит фразу для бухгалтера: «В этой базе я могу создавать документы, но не могу их исправлять…». Только после её согласия запускайте снова с PLANK_1C_ACCEPT_CREATE_ONLY=1. Отказ без слова «прав» прогон не останавливает — на некоторых видах документов любой флаг отказывается перезаписываться даже там, где документ правится без проблем, — он только печатает предупреждение.
Если роль расширить нельзя, ваша рабочая реальность — левый столбец, и отсюда следует два вывода:
- Предварительная проверка решает всё. Ошибочный документ нельзя починить тем же скриптом, который его создал. Соберите полный payload, проверьте локально, запишите один раз. Никогда не создавайте «почти правильный» документ в расчёте исправить его позже.
- Исправление становится протоколом с участием человека — см. §10.
Наличие права на проведение не является разрешением проводить. Даже с полной ролью создавайте документы непроведёнными и оставляйте проведение человеку. Проведение меняет бухгалтерские регистры, поэтому остаётся явным решением пользователя — это политика, и она не зависит от матрицы прав. Полная роль даёт другое: возможность исправлять собственные ошибки и проводить тогда, когда пользователь действительно об этом просит.
Когда вы всё же проводите по явному указанию, пользуйтесь точкой входа кита, а не вызывайте действие сами — она проводит и тут же доказывает проведение, а именно это раз за разом и ломается:
python3 scripts/1c/odata/post-documents.py Document_<Имя> --number 58 --number 59
python3 scripts/1c/odata/post-documents.py Document_<Имя> --number 58 --apply
Скрипт читает документ, проводит его, ищет движение по регистру, записанное этим документом, и считает проведённым только при наличии движения. Пачка останавливается на первом документе, который не удалось подтвердить: если проведение не работает, разбираться придётся с одним документом, а не с шестьюдесятью — причём все шестьдесят были бы в состоянии, которое труднее всего заметить, потому что выглядят готовыми. У документа, который ничего не провёл, флаг снимается, чтобы он перестал заявлять учётный эффект, которого не было.
Если база отказывает в $filter по регистрам, добавьте --sweep. На такой базе проверка одного документа означает чтение каждого регистра целиком — замерено больше десяти минут на документ, а на 256 документах это контроль, который никто не может себе позволить; вместо него берутся писать голый цикл Post() вообще без проверки. С --sweep первый документ по-прежнему проводится и проверяется отдельно, и пока он не подтвердится, остальные не трогаются; затем проводятся остальные, и колонка Recorder каждого регистра читается один раз. Тот же вопрос про каждый документ, два прохода вместо двухсот. Отдаётся при этом остановка на втором сбое — к тому моменту остальные документы уже проведены, — поэтому каждый, кто ничего не двинул, называется поимённо, и флаг с него снимается. Неполный обход не отвечает ничего и прямо об этом говорит: регистр, отказавший в чтении, оставляет GUID'ы вне выборки, и отсутствие GUID неотличимо от «не проводился».
Под капотом это действие Post() у документа — и обратите внимание, что PostingModeOperational не передаётся:
POST <база>/Document_<Имя>(guid'<Ref_Key>')/Post()
Запись Posted=true как поля — это не проведение. Алгоритм проведения не выполняется, движений по регистрам не возникает, и документ выглядит проведённым, тогда как учёт за ним не сдвинулся. Обратная операция — Unpost().
Но и HTTP-статус, и флаг Posted не доказывают, что Post() сработал. Post() отвечает HTTP 200 с пустым телом независимо от того, провёл он что-нибудь или нет, а Post?PostingModeOperational=true в замерах отвечал 200, оставляя Posted равным false, — то есть полное бездействие, отчитавшееся успехом. Хуже того, документ может вернуться с Posted=true и не иметь ни одного движения по регистрам: в одной рабочей базе три поступления пробыли в таком состоянии девять дней, тогда как четвёртое, проведённое бухгалтером в клиенте 1С, имело четыре движения — та же база, тот же пользователь, тот же вид документа, одинаковая заполненность реквизитов.
Если документ ничего не провёл, дело почти всегда в документе, а не в канале. OData Post() на корректном документе даёт ровно те же движения, что и проведение в клиенте 1С — замерено A/B на одном документе: проведение в интерфейсе → 4 движения, OData Unpost() → 0, OData Post() → те же 4. Чего OData не сообщит — это почему он отказал. 1С проверяет документ при проведении, и клиент печатает причину полностью («Не совпадают сумма документа и ее расшифровка», «Поле "Договор" не заполнено в строке 1 списка "Расшифровка платежа"», «Укажите основной банковский счет в реквизитах организации»); через OData весь этот отказ приходит как HTTP 200 с пустым телом, а Posted остаётся true.
Поэтому если проверка ниже отвечает not-posted, откройте документ в клиенте 1С и нажмите «Провести». Напечатанные сообщения — это и есть список того, что чинить. Типичные причины: строка табличной части есть, но пуста (СуммаПлатежа = 0, ссылка из одних нулей), сумма шапки не равна сумме строк, настройки организации, от которых зависит документ.
Поэтому единственное доказательство проведения — движение по регистру, записанное этим документом. После проведения проверяйте его:
python3 scripts/1c/odata/verify-posting.py Document_<Имя> <Ref_Key>
python3 scripts/1c/odata/verify-posting.py Document_<Имя> --number 58
Если в пространстве несколько баз, добавьте --mapping <файл> --credentials <файл> — ту же пару, что принимают import-*.py и post-documents.py; её принимает каждый скрипт, который обращается к базе, а без неё кит не станет угадывать, какую базу вы имели в виду.
Скрипт читает все опубликованные регистры с полем Recorder и отвечает posted, not-posted или inconclusive — последнее, когда регистр отказал в запросе, а это не то же самое, что ответить «ноль». Чтобы задать тот же вопрос сразу по всем документам — а в первый раз нужно именно это, потому что флаг долго был критерием успеха и никто не знает, сколько документов затронуто:
python3 scripts/1c/odata/audit-posting.py Document_<Имя> --since 2026-07-01
Скрипт читает каждый регистр один раз, а не по разу на документ, и называет все документы с Posted=true, которые ничего не двинули. Если хотя бы один регистр не удалось прочитать полностью, он не называет никого и прямо об этом говорит: документ, отсутствующий в неполном индексе, неотличим от непроведённого, а отправить бухгалтера перепроводить исправные документы хуже, чем промолчать. Документ с Posted=true без движений нельзя починить через OData: нужно перепровести его в клиенте 1С, по одному документу, повторяя эту проверку после каждого.
4. Чтение данных
$filter по датам ненадёжен. Некоторые серверы отклоняют фильтр по полю Date сообщением Операция не разрешена в предложении ГДЕ. Не стройте процесс в расчёте на него. Отказ — это не пустой результат: если прочитать его как «подходящих строк нет», все существующие документы выглядят отсутствующими, и так появляется дубль (§6). Клиент кита распознаёт такой отказ и подсказывает переносимый путь вместо голой ошибки HTTP 500.
$orderby может молча игнорироваться. Никогда не полагайтесь на порядок выдачи.
Переносимый приём — читать страницами через $top / $skip и отбирать период локально.
GET <база>/Document_<Имя>?$top=1000&$skip=0&$format=json
Фильтрация по строковым и ссылочным полям (коды, Ref_Key, идентификационные номера) обычно работает — проблема именно с датами.
Объекта нет в $metadata — возможно, он просто не опубликован
1С отдает объекты в OData-интерфейс по одной галочке, в «Публикации OData». Неопубликованный объект отсутствует в $metadata полностью: без ошибки, без пустой коллекции, без единого признака, отличающего его от объекта, которого в конфигурации нет.
Поэтому вывод «база не использует X» нельзя сделать по одному $metadata. 2026-08-04 база отдавала 687 наборов и никакого объекта ЭАВР — это читалось как «в этой конфигурации ЭАВР не ведут». Галочку включили: наборов стало 692, и 1 106 ЭАВР оказались доступны — они лежали там всё время, а на объяснение их отсутствия ушла половина рабочего дня.
Если тип документа, на который есть прямые свидетельства — скриншот, описание бухгалтера, ссылающееся на него поле, — отсутствует в $metadata:
- Посчитайте число entity sets и запишите его.
- Скажите прямо, что объект не опубликован, а не что его не существует.
- Попросите опубликовать, затем перечитайте
$metadataи сравните число.
«Не опубликовано» и «не используется» требуют разных действий, и только одно из них ваше.
5. Сопоставление записей в справочниках
Сопоставляйте по идентификатору, никогда — по похожему наименованию.
| Что ищем | По какому полю |
|---|---|
| Контрагент | Catalog_Контрагенты.ИдентификационныйКодЛичности — БИН/ИИН |
| Банковский счёт | Catalog_БанковскиеСчета.НомерСчета — точный ИИК |
| Физическое лицо | Catalog_ФизическиеЛица.ИдентификационныйКодЛичности — ИИН |
Наименование нужно, чтобы подтвердить сопоставление, а не чтобы его сделать.
Два правила, которые предотвращают самые частые ошибки «не та запись»:
- Проверяйте владельца. Найдя банковский счёт по ИИК, убедитесь, что
БанковскийСчет.Owner == Контрагент.Ref_Key. ИИК, принадлежащий другому контрагенту, — это сигнал тревоги, а не совпадение. - Сохранённый «основной счёт» не имеет приоритета над счётом, указанным в исходном документе. Если в карточке контрагента один ИИК, а в загружаемом документе другой, побеждает документ. Молчаливая подстановка основного счёта даёт документ, который выглядит правильным и таковым не является.
Если идентификатор находит две записи — например, один ИИН у двух физических лиц — не выбирайте случайную. Используйте ссылку, которая уже применялась в ранее проверенных документах.
Если идентификатор не находит ничего, остановитесь и спросите. Не создавайте контрагента, банковский счёт или договор автоматически, лишь бы скрипт доработал: справочники общие, постоянные, и чистить их дорого.
Группа — не запись
Справочники 1С — это деревья, и у папки то же поле Description, что у элементов внутри неё. «Физические лица» регулярно существуют и как группа, и как элемент в одном и том же Catalog_Контрагенты. Различать их нужно по IsFolder.
Папка в поле Контрагент записывается через OData без ошибок, а затем документ не проводится: группа — не контрагент. Вывести обе строки и заметить разницу недостаточно — исключение должно быть в самом поиске, иначе папка всё равно будет использована.
_catalogs.py теперь исключает IsFolder и DeletionMark из любого сопоставления и говорит, что именно исключил: «нашлась группа, а не запись» и «не найдено» требуют разных действий, и вводящий в заблуждение вариант тратит время пользователя.
6. Как распознать уже существующий документ
Ошибка здесь создаёт дубли в живом учёте, поэтому раздел отдельный.
Номер документа сам по себе не является идентичностью. Внешние номера повторяются в разные даты и годы — один и тот же банковский номер закономерно встречается в нескольких периодах. Сопоставление только по номеру заставит вас пропустить операцию, которая никогда не загружалась.
Внутренний номер 1С (Number) тоже не идентичность. Он может повторяться между периодами и последовательностями нумерации.
Используйте составную сигнатуру:
тип документа 1С + внешний номер + дата операции + сумма
и подкрепляйте её направлением платежа, идентификатором контрагента, ИИК, назначением и кодом назначения, если они есть. Ref_Key — техническая идентичность уже найденной записи; сигнатура — то, по чему принимается решение, создавать ли запись вообще.
Идемпотентность обязательна. Повторный запуск импорта не должен создавать второй документ. Каждый запуск должен уметь показать existing и to create до того, как что-либо запишет.
Одной сигнатуры недостаточно
Составная сигнатура читает поля, которыми владеет конфигурация. Тип документа, который не несёт НомерВходящегоДокумента, или база, где это поле пустое, дают документы, невидимые для индекса — а индекс, молча пропускающий то, что не смог опознать, хуже отсутствия индекса: он показывает большое число загруженных подписей и внушает доверие, которого не заслужил.
Это не гипотеза. Защита, построенная так, уже привела к полному набору дублей в живой учётной базе: она сообщала о тысячах загруженных подписей, не узнавая ни одного документа, который сама только что записала, — поэтому каждый повторный запуск видел все операции как отсутствующие и создавал их заново.
Поэтому привязывайте идентичность к полю, которое пишете сами. Кит записывает [plank:doc=…;run=…] в Комментарий каждого документа: doc — по сигнатуре, run — по счёту, периоду, заявленным итогам и количеству операций самого файла выписки. Комментарий заполняется у каждого типа документов, который пишет этот кит, и перечитывается после каждой записи — именно это делает его пригодным как идентичность там, где поля нумерации непригодны.
И защиту нужно доказывать, а не предполагать:
- Считайте то, что индекс не смог опознать, а не только то, что смог. Ненулевое число внутри загружаемого периода — слепая зона, и единственный честный ответ на неё — отказаться от записи. (Вне периода это не важно: документ 2019 года не может продублировать строку июня 2026, и падать из-за него означало бы сделать контроль бесполезным.)
- Невозможность прочитать индекс — это ошибка, а не предупреждение. Рутинные вопросы по каждой строке приучают принимать предупреждения пачкой, и предупреждение со смыслом «защита слепа» проходит вместе с ними.
- Отказывайтесь писать тип документа, который индекс не покрывает. Расширение классификатора без расширения индекса молча снимает защиту с нового типа.
- Ограничивайте всё это теми типами документов, которые запуск действительно создаёт. Пробел в типе, который выписка вообще не упоминает, не может дать дубль, а множество баз не публикует документы эквайринга вовсе — отказ от обычной выписки из-за такого типа — это блокировка, а не контроль. Сообщите о пробеле и продолжайте. Это важнее, чем кажется: защиту, которая мешает легальной работе, обходят, написав вручную скрипт вообще без защиты.
- После записи первого документа перечитайте его и проверьте защиту на нём. Ничто, прочитанное заранее, не скажет вам, узнаёт ли защита собственный вывод. Если не узнаёт — остановитесь на одном созданном документе, а не выясняйте это на следующем запуске.
«Опознаваемый» — не значит «сопоставимый»
Подсчёт того, что индекс не смог подписать, ловит документ, у которого идентичности нет вообще. Он не ловит куда более частый случай: документ, у которого идентичность прекрасная — но по ключу, который загружаемый источник никогда не породит.
Строка, которую человек занёс в 1С руками, не несёт ни банковского номера, ни вашего якоря. НомерВходящегоДокумента пуст, или содержит внутренний номер 1С, или то, что бухгалтер в тот день счёл «номером». Индекс спокойно её подписывает и считает покрытой — и после этого ни одна строка выписки не сможет с ней столкнуться, потому что ключ строки строится из банковского номера. Защита рапортует о полном покрытии ровно тех документов, к которым слепа, а загрузка создаёт их вторые копии.
Это уже случалось на живой базе: запуск сообщил «16 884 проиндексировано, 0 неопознанных внутри периода выписки» — и всё равно продублировал перевод, занесённый руками, потому что на том, ручном, банковского номера не было. «0 неопознанных» было правдой и не значило ничего.
Поэтому показывайте покрытие по сопоставимости, а не число проиндексированных:
- По загружаемому счёту и периоду перечислите каждый документ, на котором нет вашего якоря и чья сигнатура не совпадает ни с одной строкой источника. Это кандидаты в двойники, и их достаточно мало, чтобы напечатать: номер, дата, сумма, контрагент.
- Сверьте этот список с источником по
(счёт, дата, сумма, направление)— полям, которые есть с обеих сторон, что бы ни творилось с нумерацией. Совпадение здесь — вероятный уже существующий документ, и это вопрос бухгалтеру, а не решение ассистента: альтернатива вопросу — создать дубль. - Назовите это число вслух в сухом прогоне. «N документов по этому счёту в этом периоде, которые загрузка не может опознать» — честный заголовок; «0 неопознанных» — тот, которому поверили.
Никогда не отлаживайте пишущий скрипт на живой базе. Правка скрипта и повторный --apply, чтобы проверить, помогло ли — это способ превратить одну ошибку в дубль каждой строки. Между каждой правкой делайте dry-run.
7. Создание документа
Сначала всегда dry-run, потом запись:
python3 scripts/1c/odata/<import-script>.py # dry-run, ничего не пишет
python3 scripts/1c/odata/<import-script>.py --apply # запись
Если payload построен на основе прочитанного существующего документа-шаблона, удалите из него поля, принадлежащие серверу, до POST:
odata.metadataRef_KeyDataVersionNumber- навигационные URL
1С сама присвоит новый Ref_Key и внутренний номер. Оставленные поля — частый источник непонятных ошибок.
Создавайте документы непроведёнными:
Posted = false
DeletionMark = false
и оставляйте комментарий о том, что документ создан импортом и требует проверки. Проверяющий не должен догадываться, откуда взялся черновик.
Значения перечислений берутся из $metadata, а не из интуиции. Конфигурация публикует фиксированный набор значений EnumType, и правдоподобно звучащее имя, которого в наборе нет, приводит к ошибке записи. Прочитайте перечисление из $metadata и используйте ровно то, что там есть.
Две вещи отклоняются при создании, потому что 1С отклоняет их только при проведении
Документ, который 1С принимает, а затем не может провести, хуже отклонённого: создание выглядит как успех, ущерб проявляется позже, а документ лежит в живой базе и выглядит как выполненная работа. Обе проверки теперь выполняются до записи — и через Guard.create, и через create_and_verify напрямую:
- «Расшифровка платежа» отсутствует или пуста при непустой сумме документа. Такое платёжное поручение провести невозможно. При нулевой сумме расшифровка не нужна и не требуется.
- Строка расшифровки без «Статьи движения денежных средств». Этот документ проводится — и затем неверен в отчёте о движении денежных средств, чего не поймает никакая арифметическая проверка. GUID из одних нулей считается отсутствующим: именно его 1С возвращает для незаполненной ссылки вместо null.
Если в этой базе раздел расшифровки назван иначе, передайте те же имена, что использует писатель, — иначе проверка читает поле, которое никто не заполнял:
from _documents import cash_flow_config
guard = Guard(client, catalogs, run_key=..., organisation_ref=...,
cash_flow=cash_flow_config(mapping))
Эти три имени лежат в mapping.json в разделе cash_flow_articles и различаются между конфигурациями 1С. Без аргумента применяются значения по умолчанию; в базе с переименованным разделом отказ прямо сообщает об этом и перечисляет найденные табличные части.
В сообщении названо поле и то, что нужно найти. Не обходите ни одну из проверок — не убирайте расшифровку, не обнуляйте сумму и не придумывайте Ref_Key статьи: найдите статью в Catalog_СтатьиДвиженияДенежныхСредств, посмотрев, что использовали более ранние документы того же вида в этой базе, и не создавайте новую статью — справочник общий. Если найти её действительно невозможно, это повод остановиться и спросить, как и с неопознанным контрагентом (§5).
Никогда не пишите сумму прописью самостоятельно
Это делает scripts/1c/odata/_words.py, и он покрыт тестами:
from _words import amount_in_words, format_amount
amount_in_words("2341502.75") # 'Два миллиона триста сорок одна тысяча пятьсот два тенге 75 тиын'
format_amount("2341502.75") # '2 341 502,75'
У русского числительного ровно одна верная форма, и это самое надёжное место ошибиться в платёжном поручении. «тысяча» женского рода: одна тысяча и две тысячи, но никогда «один тысяча». «миллион» мужского рода и после 2-4 требует родительного падежа единственного числа: два миллиона, но не «два миллион». А 11-14 требуют множественного числа независимо от последней цифры: 111 000 — это «сто одиннадцать тысяч», потому что 111 % 100 == 11. Оба названия валюты неизменяемы: «тенге» и «тиын» не склоняются.
Если функция даёт точный ответ, не заменяйте её фразой.
8. Табличные части
Строки табличной части нельзя создавать отдельным entity set — 1С отвечает Создание строк табличной части напрямую не поддерживается.
Строки должны передаваться внутри родительского документа в том же POST. Значит документ со строками должен быть собран полностью — все ссылки разрешены, все суммы окончательны — до любой записи. Если строки ссылаются на другие документы, те должны существовать раньше, и это задаёт порядок: создать документы-основания, проверить их, затем создать родительский документ с заполненными ссылками.
9. После записи — перечитать
Никогда не считайте 2xx доказательством правильного документа. Перечитайте то, что 1С действительно сохранила, и сверьте с источником:
- каждая строка источника найдена ровно один раз;
- нет отсутствующих строк и нет неожиданных дублей;
- итоги совпадают;
- все новые документы непроведены и не помечены на удаление;
- ссылки (счета, контрагенты, строки) — те, что вы имели в виду.
Индекс для сверки стройте независимо от импорта: повторное чтение из 1С и сравнение с исходным файлом ловит ошибки, которые сверка по собственному состоянию импорта поймать не может.
10. Исправление ошибочного документа
Исправляйте на месте. Не размножайте.
Исправление, которое нельзя внести на месте, не становится автоматически новым документом. Это самое дорогое правило на этой странице. Ассистент, который не может сделать PATCH (§3) и на каждую правку отвечает созданием исправленной копии, оставляя ошибочную живой, превращает один документ, требующий правки, в два, требующих решения, — и делает это на каждую правку. Дальше пользователя просят убрать это вручную, а сама уборка — отдельная опасность (§12).
Запускайте scripts/1c/odata/correct-document.py — по умолчанию сухой прогон, по одному --set Поле=значение на изменение:
# 1. Сухой прогон. Он печатает якорь, который несёт документ.
python3 scripts/1c/odata/correct-document.py --entity-set Document_ПлатежноеПоручениеИсходящее \
--ref <guid> --set НазначениеПлатежа="Оплата по счету 42"
# 2. Запись — с тем же якорем, названным явно.
python3 scripts/1c/odata/correct-document.py --entity-set Document_ПлатежноеПоручениеИсходящее \
--ref <guid> --set НазначениеПлатежа="Оплата по счету 42" \
--anchor doc=<12hex>,run=<12hex> --apply
--apply требует --anchor, и это не формальность. Если брать якорь из того же документа, который собираешься изменить, проверка владения сравнивает значение с самим собой — то есть сводится к «якорь вообще есть», а копия нашего черновика через «Копировать» его несёт. Назвать якорь — это и есть способ сказать, какой документ вы считаете своим.
Он вызывает _documents.correct(), который отказывается ещё до отправки, если:
- якоря импорта нет — этот кит документ не писал, значит это запись бухгалтера или самописного скрипта. Покажите пользователю, что нужно изменить; не правьте чужой учётный документ.
- якорь не тот, который вы назвали. Якорь — не доказательство владения:
parse_anchorпринимает любой корректный токен где угодно в свободном комментарии, а «Копировать» в 1С дублирует «Комментарий» дословно, так что собственная копия импортированного документа несёт валидный якорь. Вызывающий обязан сказать, какой документ он считает своим. - документ принадлежит другой организации. Это единственный путь записи, который не проходит через
verify_target(), поэтому граница живёт здесь. - проведён — регистры уже двинулись. Отмена проведения решается в 1С человеком.
- помечен на удаление — исправлять нечего.
В этих двух случаях отказ заканчивается готовой фразой для бухгалтера с номером документа. Не пишите разовый скрипт, который ставит Posted или DeletionMark напрямую, чтобы обойти отказ: такой скрипт пропускает все проверки выше, а решение изначально не ваше. Остановитесь, отправьте фразу и продолжайте, когда она сделает это в 1С.
- табличная часть потеряла бы строки. Секция заменяется целиком, поэтому изменить одну строку — значит отправить все строки, которые нужно сохранить; отправить одну — удалить остальные. Отказ, если не передан
--allow-row-removal. - документ изменился между проверкой и записью (
DataVersionсдвинулся). Кто-то правит его в 1С прямо сейчас, и его правка важнее.
Posted, DeletionMark, Комментарий, Организация_Key, Ref_Key, Number и DataVersion назвать нельзя вообще. А сам скрипт дополнительно отказывает по Date, СуммаДокумента и номеру входящего документа: якорь выводится из них, поэтому исправление одного оставляет документ, чья идентичность его больше не описывает, и следующий запуск создаёт дубль. Это к человеку.
После PATCH он перечитывает документ и проверяет, что названные поля действительно изменились: 1С принимает PATCH с полем, которое затем перезаписывает своей логикой, и отчёт об успехе в этом случае — это ровно то, как про пустое поле говорят «статья ДДС заполнена». Если часть полей записалась, а часть нет, он так и говорит: фраза «ничего не исправлено» про наполовину изменённый документ отправляет искать не туда.
ODataClient.update() — это сырой PATCH под всем этим, и он не защищён: без проверки якоря, организации и проведённости. Никогда не вызывайте его напрямую.
Когда изменить действительно нельзя
Предпочтительный вариант. Пользователь удаляет или помечает на удаление ошибочный черновик в интерфейсе 1С → вы убеждаетесь, что он больше не участвует в сверке → создаёте правильный документ → сверяете повторно.
Если пользователь явно разрешил временный дубль, и только тогда — это решение по конкретному документу, а не режим, в который вы переключаетесь: создайте замену с комментарием, помечающим её как исправленную замену, оставьте старый документ непроведённым, укажите в отчёте оба внутренних номера и прямо скажите, что сверка будет показывать ожидаемый дубль, пока пользователь не удалит старый документ. Проводить можно только замену.
Нельзя: создавать замену из-за упавшей записи, не спросив; проводить замену, не убедившись, что оригинал не проведён; удалять проведённые документы без решения бухгалтера; скрывать факт создания дубля; считать исправление завершённым без явной таблицы «старый → новый».
12. Уборка после запуска импорта
Когда документы нужно удалить, это делает бухгалтер в 1С — и то, что вы попросите его выделить, решает, те ли документы он удалит.
Никогда не давайте фильтр по тексту комментария. comment из mapping.json одинаков на каждом запуске — «Создано из банковской выписки…» — поэтому фильтр по нему выделяет все импорты, которые база когда-либо получала, включая только что записанный исправленный. Такая инструкция удаляет месяц хорошей работы вместе с дублями, которые он заменял, и это замечают только когда цифр не хватает.
Фильтруйте по якорю запуска. Каждый документ, который пишет этот кит, несёт в комментарии [plank:doc=…;run=…], и ключ run= идентифицирует один импорт одного исходного файла:
python3 scripts/1c/odata/find-runs.py # все запуски в этой базе
python3 scripts/1c/odata/find-runs.py --run 3f9c1a4b77de # один запуск, документ за документом
Скрипт только читает. Он печатает точную строку для поиска в журнале 1С и Ref_Key каждого документа запуска, так что выделение можно проверить до и после. Документы, записанные самописным скриптом, якоря не несут, в этот список не попадают и выборочно вычищены быть не могут — ещё одна причина адаптировать кит, а не писать свой писатель.
11. Что не переносится между базами
Явно отмечайте в скриптах, что переносимо, а что нет.
- Жёстко заданные
Ref_Key— документы-шаблоны, элементы налогов, ставки НДС, ссылки учётной политики. В другой базе они бессмысленны, и даже в своей их стоит периодически перепроверять. Лучше разрешать их во время выполнения: шаблон — по виду операции и последнему проверенному документу, налог — по наименованию/КНП/КБК, ставку НДС — по значению. Если пиннить приходится, держите их в конфигурационном JSON, а не по всему коду. - Локальная учётная политика — как конкретная организация классифицирует переводы собственных средств, какой контрагент используется для платёжного агрегатора, как отделяются личные суммы. Это решения одной организации, подтверждённые её пользователем. Записывайте их в заметки её рабочего пространства и никогда не переносите в другую базу автоматически.
- Скрипты под конкретный случай. Скрипт, написанный под один период, с зашитыми датами и ссылками этого периода, — это протокол сделанного, а не универсальный импорт. Назовите его так, чтобы это было очевидно, и никогда не запускайте на новом периоде без адаптации и нового dry-run.