Интеграция с реестром обученных лиц ЕИСОТ Минтруда через REST API Минтруда

31.07.26

Интеграция - WEB-интеграция

Разбирается реализация двустороннего обмена документа «Протокол» конфигурации 1С:Управление учебным центром, ред. 2.0 с реестром обученных по охране труда лиц (РОЛ) ЕИСОТ: формирование набора записей и передача методом /api/set/push, получение реестровых номеров методом /api/GetEducatedPersonXML. Приводятся состав методов API по руководству версии 2026.1, структуры XML, схема сборки тела multipart, требования к контейнеру .olot, порядок подписания квалифицированной электронной подписью средствами подсистемы «Электронная подпись» БСП. Отдельно рассмотрены причины отказа при подписании документа и дефекты исходного кода. Персональные данные на иллюстрациях скрыты, ключи и реквизиты организаций в примерах условные.

1С:ПРЕДПРИЯТИЕ 8.3 · ИНТЕГРАЦИЯ · ОХРАНА ТРУДА

Интеграция 1С:Управление учебным центром с реестром обученных лиц ЕИСОТ через REST API Минтруда

Формирование пакета, транспорт multipart, контейнер .olot, подписание КЭП средствами БСП. Разбор реализации и выявленных дефектов.

   Разбирается реализация двустороннего обмена документа «Протокол» конфигурации 1С:Управление учебным центром, ред. 2.0 с реестром обученных по охране труда лиц (РОЛ) ЕИСОТ: формирование набора записей и передача методом /api/set/push, получение реестровых номеров методом /api/GetEducatedPersonXML. Приводятся состав методов API по руководству версии 2026.1, структуры XML, схема сборки тела multipart, требования к контейнеру .olot, порядок подписания квалифицированной электронной подписью средствами подсистемы «Электронная подпись» БСП. Отдельно рассмотрены причины отказа при подписании документа и дефекты исходного кода. Персональные данные на иллюстрациях скрыты, ключи и реквизиты организаций в примерах условные.

Область разбора:

  Основная конфигурация — 1С:Управление учебным центром, ред. 2.0 (разработчик — компания «Онлайн»). Логика обмена сосредоточена в общем модуле Интеграция основной конфигурации. Команды формы, реквизиты документа «Протокол», справочник соответствия программ и регистр сведений вынесены в расширение конфигурации.
   При написании статьи были обнаружены неоптимальные алгоритмы в коде и запросах. Данные ошибки описаны в виде дефектов.

Сокращения

Сокращение Расшифровка Роль в статье
ЕИСОТ Единая общероссийская справочно-информационная система по охране труда Система Минтруда, куда учебный центр передаёт сведения об обучении
РОЛ Реестр обученных лиц (по охране труда) Раздел ЕИСОТ с записями об обученных работниках — целевой приёмник данных
ФГИС Федеральная государственная информационная система «ФГИС СОУТ» — система, в составе которой работает разбираемый API
СОУТ Специальная оценка условий труда Смежный контур того же API: отчёты и протоколы решения комиссии
ПНЗ Признаки нарушения законодательства Раздел API с предписаниями по результатам автоматических проверок
API Программный интерфейс приложения REST-интерфейс Минтруда, к которому обращается конфигурация
КЭП Квалифицированная электронная подпись Подпись файлов Data.xml и Request.xml
ЭЦП Электронная цифровая подпись (устаревшее обозначение) Встречается в цитатах из пользовательской документации
БСП Библиотека стандартных подсистем 1С Подсистемы «Электронная подпись», «Работа с файлами», «Получение файлов из интернета»
УУЦ 1С:Управление учебным центром Основная конфигурация, ред. 2.0
ТЧ Табличная часть документа ТЧ «Слушатели» документа «Протокол» — источник записей набора
СНИЛС Страховой номер индивидуального лицевого счёта Ключ поиска физлица в реестре; в конфигурации — реквизит СтраховойНомерПФР
ИНН Идентификационный номер налогоплательщика Элементы EmployerInn и Inn в передаваемом XML
XDTO XML Data Transfer Objects Механизм платформы для чтения XML — ФабрикаXDTO
XSD XML Schema Definition Схемы, по которым Минтруд валидирует входящие файлы
DSS Digital Signature Service Облачная подпись — СервисКриптографииDSS в БСП

CMS,

CAdES-BES

Cryptographic Message Syntax; профиль усовершенствованной подписи Формат открепленной подписи Data.xml
CRLF Carriage Return + Line Feed Разделитель строк в multipart — Символы.ВК + Символы.ПС
BOM Byte Order Mark Маркер кодировки, недопустимый в начале тела multipart
CRM Customer Relationship Management Внешняя система, из которой в конфигурацию грузятся физические лица

1. Постановка задачи

Обучающая организация передаёт сведения об обучении по охране труда в реестр обученных лиц ЕИСОТ. Порядок обучения и проверки знаний установлен постановлением Правительства РФ от 24.12.2021 № 2464. Единица передачи — набор записей: перечень работников с результатами проверки знаний по одной программе обучения. Реестр обрабатывает набор и присваивает каждой записи реестровый номер.

Источник данных в конфигурации — документ «Протокол»: реквизиты курса, программы обучения и комиссии в шапке, перечень слушателей с оценками в табличной части. Требуются две операции, инициируемые из формы документа:

  1. формирование набора записей по табличной части и передача его в РОЛ;

  2. получение реестрового номера по каждому слушателю и запись его в строку табличной части.

Обе операции идут через один транспорт: POST с телом multipart/form-data, внутри — XML-файлы. Авторизация — 32-символьный ключ в элементе ApiKey внутри самого XML. Ни заголовков авторизации, ни токенов с временем жизни здесь нет.

 

2. API ФГИС СОУТ: состав и требования

Хост реестра обученных лиц — https://edu.rosmintrud.ru. Интерфейс описан документом «ФГИС СОУТ. Руководство пользователя. Описание работы пользователя в ФГИС СОУТ по работе с API»; на момент подготовки материала актуальна версия документа 2026.1.

Минтруд меняет между редакциями и состав элементов, и адреса методов. Далее помечено, что взято из редакции 2026.1, а что проверено на живых запросах.

2.1. Ключ взаимодействия

Ключ выдаёт личный кабинет по охране труда: профиль пользователя, вкладка «Ключи для взаимодействия через программный интерфейс для организации». Там же его можно обновить или удалить. Длина — 32 символа.

Ключей два, и это разные ключи с разными правами: работодателя и аккредитованной обучающей организации. Учебному центру нужен второй. Права по ключу те же, что у пользователя в web-интерфейсе: чего он не видит в кабинете, того не увидит и через API.

XSD-схемы запросов и ответов лежат в разделе «Справочная информация» ? «Программный интерфейс»: https://akot.rosmintrud.ru/sout/info. Прогнать исходящий XML через схему локально дешевле, чем ловить «Документ не соответствует схеме XSD» на приёмнике.

2.2. Перечень методов

Руководство 2026.1 описывает 19 методов на четырёх хостах:

Отчёты СОУТ https://cache.rosmintrud.ru

Протоколы комиссии https://protocol.rosmintrud.ru

Признаки нарушений https://violations.rosmintrud.ru

Реестр обученных лиц https://edu.rosmintrud.ru

Раздел Метод (все POST) Назначение Вложение
Отчёты СОУТ /api/getReportList Список отчётов СОУТ по фильтру ZIP: Request.xml + .sig
Отчёты СОУТ /api/getReport Содержимое отчёта по ReportId ZIP: Request.xml + .sig
Протоколы /api/getProtocolList Список протоколов решения комиссии СОУТ ZIP: Request.xml + .sig
Протоколы /api/getProtocol Содержимое протокола комиссии ZIP: Request.xml + .sig
Протоколы /api/sendProtocol Передача подписанного протокола ZIP: Request.xml + .sig
Протоколы /api/cancelProtocol Аннулирование протокола ZIP: Request.xml + .sig
Протоколы /api/createSendProtocol Создание протокола (IsSend=false) либо создание с передачей (IsSend=true) ZIP: Request.xml + .sig + data.xml, при передаче — data.xml.sig
ПНЗ /api/getVerificationList Список признаков нарушения законодательства ZIP: Request.xml + .sig
РОЛ /api/set/push Создание набора записей об обученных лицах Две части формы: Request.xml + package.olot (ZIP: Data.xml, при передаче — Data.xml.sig)
РОЛ /api/GetEducatedPersonXML Выборка записей реестра по фильтру Одна часть формы: XML без архива и подписи
РОЛ /api/worker/get Выборка работников по фильтру ZIP: Request.xml + .sig
РОЛ /api/worker/create Создание записей о работниках ZIP: Request.xml + .sig
РОЛ /api/worker/edit Изменение записи о работнике ZIP: Request.xml + .sig
РОЛ /api/worker/delete Удаление записи о работнике по Id ZIP: Request.xml + .sig
РОЛ /api/test/get Выборка тестовых попыток ZIP: Request.xml + .sig
РОЛ /api/test/create Создание тестовых попыток ZIP: Request.xml + .sig
РОЛ /api/test/edit Изменение тестовой попытки ZIP: Request.xml + .sig
РОЛ /api/test/delete Удаление тестовой попытки

ZIP: Request.xml + .sig

 

 

 

 

 

 

.olot — это ZIP, которому поменяли расширение; отдельного формата за ним нет. Поэтому делить методы на «архивные» и «неархивные» бессмысленно. Различать их надо по двум другим признакам: что лежит внутри архива и что именно подписано.
  Получаются четыре формы:

 

Форма Состав multipart Объект подписи Методы
А Одна часть: *.zip = Request.xml + Request.xml.sig Запрос. Подпись обязательна Отчёты СОУТ, getProtocolList, getProtocol, sendProtocol, cancelProtocol, ПНЗ, worker/*, test/* — 15 методов
Б Одна часть: *.zip = Request.xml + Request.xml.sig + data.xml, при передаче — data.xml.sig Запрос — обязательно, данные — по значению IsSend createSendProtocol
В Две части: Request.xml без архива + package.olot (ZIP: Data.xml, при передаче — Data.xml.sig) Только данные, по значению NeedSend. Запрос не подписывается set/push
Г Одна часть: XML фильтра без архива Подпись не требуется GetEducatedPersonXML

 

 

Пятнадцать методов из девятнадцати без КЭП недоступны вообще: открепленная подпись Request.xml.sig обязательна даже для простого чтения списка. Наши два — самые щадящие в API. GetEducatedPersonXML не требует ни архива, ни подписи, поэтому загрузку реестровых номеров удалось автоматизировать сразу. set/push без подписи данных доводит набор только до состояния черновика в кабинете. Формы Б и В работают по одному механизму «признак передачи плюс подпись данных»: IsSend и data.xml.sig для протоколов комиссии, NeedSend и Data.xml.sig для РОЛ. Реализуете один — поймёте оба.

Регистр имён файлов

В форме Б руководство указывает файл данных как data.xml и data.xml.sig, в форме В — как Data.xml и Data.xml.sig. Имена задавайте буквально по описанию конкретного метода. Приводить их к единому стилю, не проверив на приёмнике, — плохая идея.

Расхождение источников

В распространённых сводных таблицах методов адреса РОЛ приводятся с дополнительным сегментом /api/rol/... (/api/rol/set, /api/rol/GetEducatedPersonXML, /api/rol/worker/get), а set/push описан как ZIP с Request.xml и Request.xml.sig. Руководство 2026.1 и фактические запросы дают /api/set/push, /api/GetEducatedPersonXML, /api/worker/get и две части формы .xml + .olot. В материале приведён вариант руководства. Отдельно: в разделе «создание записи о работнике» руководства 2026.1 указан адрес /api/worker/get; по контексту — /api/worker/create.

2.3. Методы, используемые учебным центром

Метод Вложения Успешный ответ
POST /api/set/push Две части формы: Request.xml (ApiKey, NeedSend) и package.olot — ZIP с Data.xml, при передаче в реестр дополнительно Data.xml.sig Элемент Response: SetId, SendEducatedPerson, Message
POST /api/GetEducatedPersonXML Одна часть формы: XML с элементом EducatedPersonFilter Элемент EducatedPersons с перечнем RegistryRecord; при отказе — Error с StatusCode и Message

2.4. Семантика элемента NeedSend

Что сделает приёмник, зависит от пары «значение NeedSend» ? «есть ли подпись внутри контейнера»:

Состав .olot NeedSend Результат
только Data.xml False либо элемент отсутствует Набор создан, в реестр не передан. Подписание и передача выполняются в личном кабинете.
Data.xml + Data.xml.sig True Набор создан и передан в реестр.
Data.xml + Data.xml.sig False Отказ: «Указан тег NeedSend со значением false, но добавлен файл Data.xml.sig».
только Data.xml True Отказ: «Указан тег NeedSend со значением true, но не найден файл Data.xml.sig».

Требования к контейнеру

Контейнер .olot — ZIP-архив с изменённым расширением. Файл данных внутри архива именуется Data.xml, файл подписи — Data.xml.sig; поиск ведётся по имени файла. Пути внутри архива не сохраняются.

2.5. Структура Request.xml

<?xml version="1.0" encoding="utf-8"?>
<Request xsi:noNamespaceSchemaLocation="schema.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ApiKey>00000000000000000000000000000000</ApiKey>
<NeedSend>False</NeedSend>
</Request>

2.6. Структура Data.xml

Корневой элемент — RegistrySet, вложенные — RegistryRecord. Одна запись соответствует одному слушателю по одной программе обучения.

<?xml version="1.0" encoding="utf-8"?>

<RegistrySet xsi:noNamespaceSchemaLocation="schema.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<RegistryRecord>
<Worker>
<LastName>Иванов</LastName>
<FirstName>Иван</FirstName>
<MiddleName>Иванович</MiddleName>
<Snils>000-000-000 00</Snils>
<Position>Электромонтёр</Position>
<EmployerInn>7700000000</EmployerInn>
<EmployerTitle>ООО «Заказчик»</EmployerTitle>
</Worker>
<Organization>
<Inn>7711111111</Inn>
<Title>ООО «Учебный центр»</Title>
</Organization>
<Test isPassed="true" learnProgram >
<Date>2026-06-24</Date>
<ProtocolNumber>1092</ProtocolNumber>
<LearnProgramTitle>Безопасные методы выполнения огневых работ</LearnProgramTitle>
</Test>
</RegistryRecord>
</RegistrySet>

Worker — обученное лицо и его работодатель,
Organization — обучающая организация,
Test — результат проверки знаний.


   Атрибуты элемента Test:

  • isPassed — результат проверки знаний. В рассматриваемой реализации значение зафиксировано равным 1: в реестр передаются только сдавшие. При необходимости передачи несдавших атрибут связывается с оценкой в табличной части.

  • learnProgramId — идентификатор программы обучения в справочнике Минтруда. Внутренние коды и наименования конфигурации в этом атрибуте недопустимы.

2.7. Структура EducatedPersonFilter

Обязателен только элемент ApiKey. Остальные элементы — необязательные условия отбора, комбинируются между собой:

Элемент Назначение Элемент Назначение
No реестровый номер SetId номер набора
PageNo номер страницы Position должность
PageSize размер страницы: по умолчанию 100, максимум 5000 DateCreatedFrom / DateCreatedBefore дата создания записи, от / до
LastName / FirstName / MiddleName фамилия, имя, отчество TestDateFrom / TestDateBefore дата проверки знаний, от / до
Snils СНИЛС LearnProgramId идентификатор программы обучения
OuterId идентификатор из внешней системы ProtocolNumber

номер протокола

 

 

 

Отбираем по Snils — это единственный по-настоящему уникальный идентификатор физлица, который есть в конфигурации. По ProtocolNumber не отбираем: номер протокола центр формирует сам, и уникальность его в реестре никто не гарантирует.

2.8. Ответ метода GetEducatedPersonXML

<EducatedPersons>
<RegistryRecord set " baseNo="" baseDateCreated="" outer " learnProgram odd">

Различие форматов отказа:

У методов set/push и GetEducatedPersonXML отказ возвращается элементом Error с вложенными StatusCode и Message. У остальных методов — корневым Response с элементом Error, содержащим StatusCode, Message, DateTime и RequestId.
RequestId — идентификатор запроса в журналах API, требуется при обращении в службу поддержки. Универсальный обработчик ответа должен поддерживать оба формата и фиксировать RequestId.

 

3. Объекты метаданных

Кроме констант, все перечисленные объекты добавлены расширением конфигурации.

3.1. Константы

Константа Тип Назначение
АдресСервераМинтруд Строка Хост API (edu.rosmintrud.ru). Вынесен из кода для переключения контура.
КлючAPI_Минтруд Строка (32) Ключ обучающей организации. Допускает перегенерацию в личном кабинете.

Хранение ключа

Ключ API — это пароль к реестру. В разобранном коде его значение осталось в комментарии рядом с обращением к константе (//"6cf7..."), а значит попало в репозиторий, в выгрузку конфигурации и в бэкапы. Держите ключ только в константе с ограничением прав на чтение или в защищённом хранилище, а комментарии-шпаргалки вычищайте перед коммитом.

 

3.2. Справочник соответствия программ

ПрограммыМинтруда — плоский справочник: числовой код длиной 3 (соответствует learnProgramId) и реквизит НаименованиеПолное длиной 150 (соответствует LearnProgramTitle). В справочник «Дисциплины» добавлен реквизит ПрограммаМинтруда со ссылкой на этот справочник.

Функция ПолучитьДанныеПрограммыМинтруда(врПрограмма)

Возврат Новый Структура("КодПрограммы, ТемаПрограммы", врПрограмма.ПрограммаМинтруда.Код, врПрограмма.ПрограммаМинтруда.НаименованиеПолное);

КонецФункции

Вызывается так: ПолучитьДанныеПрограммыМинтруда(Протокол.Курс.Курс). реквизит Курс документа «Протокол» ссылается на документ «Расписание курса», реквизит Курс которого — ссылка на Справочник.Дисциплины.

3.3. Регистр сведений

ДанныеОбученияИзМинтруда — непериодический независимый регистр сведений:

Роль Имя Тип
Измерение Слушатель СправочникСсылка.ФизическиеЛица
Измерение НомерПротокола Строка (10)
Измерение ИдПрограммы Число
Ресурс РегистрационныйНомер Строка (10)
Ресурс РезультатОбучения Булево
Ресурс ДатаПроверки Дата

 

 

 

 

 

 

При отборе по СНИЛС метод GetEducatedPersonXML возвращает все записи реестра по человеку — по всем программам и за все годы. Складывать их в регистр целиком дешевле, чем отбрасывать: так бесплатно получается история обучения физлица независимо от конкретного протокола, а по ней уже строится контроль сроков переобучения через реквизит СрокДействияУдостоверения справочника «Дисциплины».

3.4. Реквизиты документа «Протокол»:

Реквизит Размещение Назначение
ВыгруженВМинтруд шапка Признак передачи набора; основание для отбора непереданных протоколов.
ЗаполненыВсеНомера шапка Служебный признак получения реестровых номеров по всем строкам; рассчитывается при записи.
ПодписанЭП шапка Стандартный реквизит подсистемы «Электронная подпись» БСП.
РегистрационныйНомерМинтруд ТЧ «Слушатели» Реестровый номер (baseNo) по данному протоколу.
Разряд, ПроверкаЗнаний, Должность ТЧ «Слушатели» Данные печатных форм и источник элемента Position.

Расчёт признака заполненности реестровых номеров при записи документа:

массивПустыхСтрок = ТекущийОбъект.Слушатели.НайтиСтроки(Новый Структура("РегистрационныйНомерМинтруд", ""));
ТекущийОбъект.ЗаполненыВсеНомера = массивПустыхСтрок.Количество() = 0;

 

Схема 1. Потоки обмена:

Левая колонка — формирование и передача набора записей, правая — получение реестровых номеров.

 

4. Передача набора записей: POST /api/set/push:

Точка входа — экспортная функция Интеграция.ОтправитьвМинтрудНаборСлушателей(Слушатели). Параметр — ссылка на документ «Протокол», к реквизитам и табличной части обращаемся через точку. Последовательность такая:

  1. формирование Request.xml во временном файле;

  2. формирование Data.xml по табличной части в строку;

  3. упаковка Data.xml в ZIP-контейнер с расширением .olot;

  4. сборка тела multipart/form-data из двух частей;

  5. выполнение POST-запроса и разбор XML-ответа через ФабрикаXDTO;

  6. удаление временных файлов.

4.1. Формирование Request.xml:

Функция возвращает и адрес временного хранилища, и путь: по адресу читаем двоичные данные, по пути узнаём размер и потом удаляем файл.

Функция СоздатьXML_НаборДанныхМинтруд(ПараметрыЗапроса)

Путь = ПолучитьИмяВременногоФайла("xml");

ЗаписьXML = Новый ЗаписьXML;
ЗаписьXML.ОткрытьФайл(Путь, "UTF-8");
ЗаписьXML.ЗаписатьОбъявлениеXML();
ЗаписьXML.ЗаписатьНачалоЭлемента("Request");
ЗаписьXML.ЗаписатьАтрибут("xsi:noNamespaceSchemaLocation", "schema.xsd");
ЗаписьXML.ЗаписатьАтрибут("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
ЗаписьXML.ЗаписатьНачалоЭлемента("ApiKey");
ЗаписьXML.ЗаписатьТекст(ПараметрыЗапроса.ApiKey);
ЗаписьXML.ЗаписатьКонецЭлемента();
ЗаписьXML.ЗаписатьНачалоЭлемента("NeedSend");
ЗаписьXML.ЗаписатьТекст(ПараметрыЗапроса.NeedSend);
ЗаписьXML.ЗаписатьКонецЭлемента();
ЗаписьXML.ЗаписатьКонецЭлемента(); // Request
ЗаписьXML.Закрыть();
ДвоичныеДанные = Новый ДвоичныеДанные(Путь);

АдресXML = ПоместитьВоВременноеХранилище(ДвоичныеДанные, Новый УникальныйИдентификатор);

Возврат Новый Структура("Адрес, Путь", АдресXML, Путь);

КонецФункции

 

4.2. Формирование Data.xml:

Файл собираем сразу в строку — ЗаписьXML.УстановитьСтроку() вместо ОткрытьФайл(). Имя задаём при упаковке в архив.

Функция СоздатьXML_СлушателиДляМинтруда(Протокол)

ТаблицаСлушателей = Протокол.Слушатели;

ЗаписьXML = Новый ЗаписьXML;
ЗаписьXML.УстановитьСтроку();
ЗаписьXML.ЗаписатьОбъявлениеXML();
ЗаписьXML.ЗаписатьНачалоЭлемента("RegistrySet");
ЗаписьXML.ЗаписатьАтрибут("xsi:noNamespaceSchemaLocation", "schema.xsd");
ЗаписьXML.ЗаписатьАтрибут("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");

Программа = ПолучитьДанныеПрограммыМинтруда(Протокол.Курс.Курс);

Для каждого Слушатель Из ТаблицаСлушателей Цикл

ЗаписьXML.ЗаписатьНачалоЭлемента("RegistryRecord");
// --- Worker -------------------------------------------------
ЗаписьXML.ЗаписатьНачалоЭлемента("Worker");
СтруктураФИО = ФизическиеЛицаКлиентСервер.ЧастиИмени(Слушатель.Слушатель.Наименование);
ЗаписатьТег(ЗаписьXML, "LastName", СтруктураФИО.Фамилия);
ЗаписатьТег(ЗаписьXML, "FirstName", СтруктураФИО.Имя);
ЗаписатьТег(ЗаписьXML, "MiddleName", СтруктураФИО.Отчество);
ЗаписатьТег(ЗаписьXML, "Snils", Слушатель.Слушатель.СтраховойНомерПФР);

ДолжностьФЛ = Справочники.ФизическиеЛица.ВернутьАктуальнуюДолжностьФизлица(Слушатель.Слушатель);
ЗаписатьТег(ЗаписьXML, "Position", Строка(ДолжностьФЛ));
ЗаписатьТег(ЗаписьXML, "EmployerInn", Слушатель.Слушатель.МестоРаботы.ИНН);
ЗаписатьТег(ЗаписьXML, "EmployerTitle", Строка(Слушатель.Слушатель.МестоРаботы));
ЗаписьXML.ЗаписатьКонецЭлемента(); // Worker

// --- Organization (обучающая организация) -------------------
ЗаписьXML.ЗаписатьНачалоЭлемента("Organization");
ЗаписатьТег(ЗаписьXML, "Inn", Протокол.Организация.ИНН);
ЗаписатьТег(ЗаписьXML, "Title", Строка(Протокол.Организация));
ЗаписьXML.ЗаписатьКонецЭлемента();

// --- Test ---------------------------------------------------
ЗаписьXML.ЗаписатьНачалоЭлемента("Test");
ЗаписьXML.ЗаписатьАтрибут("isPassed", "1");
ЗаписьXML.ЗаписатьАтрибут("learnProgramId", Строка(Программа.КодПрограммы));
ЗаписатьТег(ЗаписьXML, "Date", Формат(Протокол.Дата, "ДФ=yyyy-MM-dd"));
ЗаписатьТег(ЗаписьXML, "ProtocolNumber", СокрЛП(Протокол.Номер));
ЗаписатьТег(ЗаписьXML, "LearnProgramTitle", СокрЛП(Программа.ТемаПрограммы));
ЗаписьXML.ЗаписатьКонецЭлемента(); // Test
ЗаписьXML.ЗаписатьКонецЭлемента(); // RegistryRecord
КонецЦикла;

ЗаписьXML.ЗаписатьКонецЭлемента(); // RegistrySet

Возврат ЗаписьXML.Закрыть();

КонецФункции

В исходной реализации каждый элемент пишется тремя вызовами, из-за чего функция раздувается до полутора сотен строк. Вспомогательная процедура сокращает её втрое и убирает целый класс ошибок — незакрытый элемент:

Процедура ЗаписатьТег(ЗаписьXML, Имя, Значение)

ЗаписьXML.ЗаписатьНачалоЭлемента(Имя);
ЗаписьXML.ЗаписатьТекст(?(Значение = Неопределено, "", Строка(Значение)));
ЗаписьXML.ЗаписатьКонецЭлемента();

КонецПроцедуры

 

Кодировка Data.xml:

Файл собирается в строку, а потом пишется на диск обычным Новый ЗаписьТекста(ИмяФайлаДанных) без указания кодировки. При записи в строку платформе кодировку применять некуда, поэтому ЗаписатьОбъявлениеXML() не формирует атрибут encoding — фактическую кодировку файла задают параметры ЗаписьТекста. Для файла, содержащего ФИО, должности и наименования организаций, корректнее исключить промежуточную строку: ЗаписьXML.ОткрытьФайл(Путь, "UTF-8") формирует объявление с encoding="utf-8" и гарантирует кодировку файла.

 

Требования к формату значений:

Дату пишем как Формат(Дата, "ДФ=yyyy-MM-dd"): XMLСтрока(Дата) даёт значение с временной частью, и схема его не примет. СНИЛС — по маске 000-000-000 00, с дефисами и пробелом перед контрольным разрядом; нормализовать надо на выгрузке, а не надеяться на аккуратность операторов. Элемент MiddleName присутствует всегда, при отсутствии отчества — пустым.

 

4.3. Контейнер .olot

Контейнер собираем как ZIP. Имя файла внутри архива фиксировано, поэтому вместо ПолучитьИмяВременногоФайла() создаём временный каталог:

Функция ЗаписатьДанныеВАрхивМинтруда(Данные)

Каталог = ФайловаяСистема.СоздатьВременныйКаталог();

// приёмник требует имя файла данных "Data.xml"
ИмяФайлаДанных = Каталог + "Data.xml";
ИмяФайлаАрхива = ПолучитьИмяВременногоФайла("olot");

ЗаписьТекста = Новый ЗаписьТекста(ИмяФайлаДанных);
ЗаписьТекста.Записать(Данные);
ЗаписьТекста.Закрыть();

АрхивZip = Новый ЗаписьZipФайла(ИмяФайлаАрхива, , , МетодСжатияZIP.Сжатие, УровеньСжатияZIP.Максимальный);
АрхивZip.Добавить(ИмяФайлаДанных, РежимСохраненияПутейZIP.НеСохранятьПути);
АрхивZip.Записать();

УдалитьФайлы(ИмяФайлаДанных);
ФайловаяСистема.УдалитьВременныйКаталог(Каталог);

Возврат ИмяФайлаАрхива;

КонецФункции

ФайловаяСистема.СоздатьВременныйКаталог() возвращает путь с конечным разделителем, поэтому конкатенация Каталог + "Data.xml" даёт файл внутри каталога с требуемым именем. Режим РежимСохраненияПутейZIP.НеСохранятьПути обязателен: при сохранении путей элемент архива получает имя вида C/Users/.../Temp/.../Data.xml и приёмником не обнаруживается.

Переименование архива не требуется:

Комментарий исходного модуля обещает изменение расширения ZIP на OLOT. В коде такой операции нет, и она не нужна: ПолучитьИмяВременногоФайла("olot") сразу отдаёт путь с целевым расширением, а ЗаписьZipФайла пишет корректный ZIP-контейнер в файл с любым именем. Расширение — признак для приёмника, формат внутри всегда ZIP. Так что ПереместитьФайл после АрхивZip.Записать() — лишняя строка из инструкции для человека.

 

Владение временным файлом:

Функция ЗаписатьДанныеВАрхивМинтруда удаляет Data.xml и временный каталог — объекты, являющиеся её внутренней деталью, — и возвращает путь к архиву как результат. Удаление архива внутри функции невозможно: файл требуется вызывающему коду для формирования тела запроса; обязанность удаления переходит вместе с путём. В ОтправитьвМинтрудНаборСлушателей удаляется только Request.xml, ИмяФайлаАрхива не удаляется — каждая передача оставляет файл .olot в каталоге временных файлов сервера.

В транспортной функции ветка ТипДанных = "ZIP" использует функцию ЗаписатьДанныеВАрхив с идентичной логикой (расширение zip вместо olot) и удаляет архив после получения ответа: цепочка владения замкнута в одном методе. В ветке передачи двоичных данных цепочка разорвана.

Функция ЗаписатьДанныеВАрхив(Данные) // вариант для ТипДанных = "ZIP"

ИмяФайлаДанных = ПолучитьИмяВременногоФайла("xml");
ИмяФайлаАрхива = ПолучитьИмяВременногоФайла("zip");

ЗаписьТекста = Новый ЗаписьТекста(ИмяФайлаДанных);
ЗаписьТекста.Записать(Данные);
ЗаписьТекста.Закрыть();

АрхивZip = Новый ЗаписьZipФайла(ИмяФайлаАрхива, , , МетодСжатияZIP.Сжатие, УровеньСжатияZIP.Максимальный);
АрхивZip.Добавить(ИмяФайлаДанных, РежимСохраненияПутейZIP.НеСохранятьПути);
АрхивZip.Записать();

УдалитьФайлы(ИмяФайлаДанных); // внутренняя деталь — удаляется

Возврат ИмяФайлаАрхива; // результат — передаётся вызывающему коду

КонецФункции

Правило простое: функция удаляет только то, что не отдаёт наружу. Всё возвращённое удаляет вызывающий код — и там же, где получил путь, чтобы владение не повисало.

Примечание: Заворачивать обе операции удаления в один блок Попытка нельзя. Исключение прерывает блок целиком, и падение первой строки просто отменит вторую — останется как раз тот файл, который мы хотели убрать:

// некорректно: при исключении в первой строке вторая не выполняется

Попытка

УдалитьФайлы(СтруктураФайла.Путь); // Request.xml
УдалитьФайлы(ИмяФайлаАрхива); // не выполняется

Исключение

ЗаписьЖурналаРегистрации(...);

КонецПопытки;

Корректные варианты — независимые блоки Попытка на каждую операцию либо штатный метод БСП, выполняющий обработку исключения самостоятельно:

// ФайловаяСистема.УдалитьВременныйФайл оборачивает УдалитьФайлы в Попытку
// и при неудаче записывает предупреждение в журнал регистрации

ФайловаяСистема.УдалитьВременныйФайл(СтруктураФайла.Путь); // Request.xml
ФайловаяСистема.УдалитьВременныйФайл(ИмяФайлаАрхива); // package.olot

Одно ограничение у метода есть: он проверяет, что путь ведёт в каталог временных файлов (ЭтоИмяВременногоФайла), и на чужом пути осознанно бросает исключение. Оба наших пути получены через ПолучитьИмяВременногоФайла(), так что проверка проходит.

4.4. Сборка тела multipart

Штатного конструктора multipart/form-data в платформе нет: HTTPЗапрос.УстановитьТелоИзСтроки() и УстановитьТелоИзДвоичныхДанных() принимают уже готовое тело. Значит, собираем его байт в байт сами — через ПотокВПамяти и ЗаписьДанных с явным разделителем строк.

Разделитель = СтрЗаменить(Новый УникальныйИдентификатор, "-", "");
Тело = Новый ПотокВПамяти();
// четвёртый параметр — разделитель строк: ВК + ПС (CRLF)
ЗаписьДанных = Новый ЗаписьДанных(Тело, , , Символы.ВК + Символы.ПС, "");

// --- часть 1: Request.xml -------------------------------------------
ЗаписьДанных.ЗаписатьСтроку("--" + Разделитель);
ЗаписьДанных.ЗаписатьСтроку("Content-Disposition: form-data; name=""file""; filename=""Request.xml""");
ЗаписьДанных.ЗаписатьСтроку("Content-Length:" + РазмерФайлаОтправки);
ЗаписьДанных.ЗаписатьСтроку("Content-Type: multipart/form-data");
ЗаписьДанных.ЗаписатьСтроку("");
ЗаписьДанных.Записать(ДанныеФайлаXML);
ЗаписьДанных.ЗаписатьСтроку("");

// --- часть 2: package.olot ------------------------------------------
ЗаписьДанных.ЗаписатьСтроку("--" + Разделитель);
ЗаписьДанных.ЗаписатьСтроку("Content-Disposition: form-data; name=""file""; filename=""package.olot""");
ЗаписьДанных.ЗаписатьСтроку("Content-Length:" + РазмерФайлаАрхива);
ЗаписьДанных.ЗаписатьСтроку("Content-Type: multipart/form-data");
ЗаписьДанных.ЗаписатьСтроку("");
ЗаписьДанных.Записать(ДвоичныеДанныеАрхива);
ЗаписьДанных.ЗаписатьСтроку("");

// --- закрывающий разделитель ----------------------------------------
ЗаписьДанных.ЗаписатьСтроку("--" + Разделитель + "--");
ЗаписьДанных.ЗаписатьСтроку("");
ЗаписьДанных.Закрыть();

ДанныеТела = Тело.ЗакрытьИПолучитьДвоичныеДанные();

Заголовки = Новый Соответствие();
Заголовки.Вставить("Content-Type", "multipart/form-data; boundary=" + Разделитель);

Существенные условия сборки:

  • Имя поля у обеих частей — file, и это не опечатка. Приёмник различает файлы по расширению в filename, а не по имени поля; попытка «навести порядок» и назвать части request/package даёт отказ.

  • Разделитель строк — CRLF, четвёртый параметр конструктора ЗаписьДанных: Символы.ВК + Символы.ПС. По умолчанию платформа поставит ПС, и сервер не разберёт границы частей.

  • Пятый параметр ЗаписьДанных — пустая кодировка, исключающая запись BOM в начало потока. BOM перед первым разделителем нарушает разбор.

  • Пустая строка после блока заголовков каждой части и перевод строки перед следующим разделителем входят в формат.

  • Content-Length внутри части спецификацией не требуется, приёмником ожидается. Размеры определяются через Новый Файл(Путь).Размер().

4.5. Транспорт

Транспортная функция одна на обмен с Минтрудом и на загрузку физлиц из внешней CRM. Прокси берётся из БСП, защищённое соединение — через ОбщегоНазначенияКлиентСервер.НовоеЗащищенноеСоединение().

Функция HTTPСервисОтправитьДанныеСлужебный(Параметры)

ЗащищенноеСоединение = Неопределено;
Если Параметры.ЗащищенноеСоединение Тогда
ЗащищенноеСоединение = ОбщегоНазначенияКлиентСервер.НовоеЗащищенноеСоединение();
КонецЕсли;

ИнтернетПрокси = Неопределено;
Если ОбщегоНазначения.ПодсистемаСуществует("СтандартныеПодсистемы.ПолучениеФайловИзИнтернета") Тогда

МодульПолучениеФайловИзИнтернета = ОбщегоНазначения.ОбщийМодуль("ПолучениеФайловИзИнтернета");
ИнтернетПрокси = МодульПолучениеФайловИзИнтернета.ПолучитьПрокси("https");

КонецЕсли;

HTTPСоединение = Новый HTTPСоединение(Параметры.Сервер, Параметры.Порт,,, ИнтернетПрокси, Параметры.Таймаут, ЗащищенноеСоединение);
HTTPЗапрос = Новый HTTPЗапрос(Параметры.АдресРесурса, Параметры.Заголовки);

Если Параметры.ТипДанных = "Текст" Тогда
HTTPЗапрос.УстановитьТелоИзСтроки(Параметры.Данные);
ИначеЕсли Параметры.ТипДанных = "ДвоичныеДанные" Тогда
HTTPЗапрос.УстановитьТелоИзДвоичныхДанных(Параметры.Данные);
КонецЕсли;

Попытка
Если Параметры.Метод = "POST" Тогда
HTTPОтвет = HTTPСоединение.ОтправитьДляОбработки(HTTPЗапрос);
ИначеЕсли Параметры.Метод = "GET" Тогда
HTTPОтвет = HTTPСоединение.Получить(HTTPЗапрос);
КонецЕсли;

HTTPОтветСтруктура = HTTPОтветВСтруктуру(HTTPОтвет);

Исключение

HTTPОтветСтруктура = Новый Структура("КодСостояния", 105);

КонецПопытки;

Возврат HTTPОтветСтруктура;

КонецФункции

Параметры вызова для передачи набора:

ПараметрыHTTP = Новый Структура;
ПараметрыHTTP.Вставить("Сервер", Константы.АдресСервераМинтруд.Получить());
ПараметрыHTTP.Вставить("АдресРесурса", "/api/set/push");
ПараметрыHTTP.Вставить("Данные", ДанныеТела);
ПараметрыHTTP.Вставить("Заголовки", Заголовки);
ПараметрыHTTP.Вставить("Порт", 443);
ПараметрыHTTP.Вставить("ЗащищенноеСоединение", Истина);
ПараметрыHTTP.Вставить("Метод", "POST");
ПараметрыHTTP.Вставить("ТипДанных", "ДвоичныеДанные");
ПараметрыHTTP.Вставить("Таймаут", 30);

HTTPОтвет = HTTPСервисОтправитьДанныеСлужебный(ПараметрыHTTP);

Код состояния 105

Новый Структура("КодСостояния", 105) в блоке Исключение — внутренний признак недоступности сервера: сеть, DNS, сертификат, прокси, таймаут. В номенклатуре HTTP такой код отсутствует, что позволяет обрабатывать ситуацию отдельно. Текст исключения в этой ветке теряется, поэтому первым делом сюда стоит добавить ЗаписьЖурналаРегистрации с ОбработкаОшибок.ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()). Без этого любая ошибка внутри блока — включая сбой разбора ответа в HTTPОтветВСтруктуру — превращается в безликое «105».

 

Дефект: удаление входных данных внутри блока Попытка

После получения ответа внутри Попытка идёт очистка временных файлов, и в ветке двоичных данных вызывается УдалитьФайлы(Параметры.Данные). В рассматриваемых вызовах Параметры.Данные — объект ДвоичныеДанные (тело multipart в памяти), а не путь к файлу: ветка рассчитана на второй допустимый вариант параметра — путь строкой (выше в функции присутствует конструкция Если НЕ ТипЗнч(Параметры.Данные) = Тип("ДвоичныеДанные") Тогда ДвоичныеДанныеАрхива = Новый ДвоичныеДанные(Параметры.Данные)). Вызов расположен внутри Попытка после присвоения HTTPОтветСтруктура: при исключении на нестроковом параметре управление переходит в Исключение, полученный код 200 замещается кодом 105, разбор элемента Response не выполняется, при этом набор на стороне приёмника создан. Проверяется одной записью в журнал в блоке Исключение. Строку надо убрать: чистить нужно файлы, которые функция создала сама, а не входные данные.

 

Попытка
Если Параметры.Метод = "POST" Тогда
HTTPОтвет = HTTPСоединение.ОтправитьДляОбработки(HTTPЗапрос);
ИначеЕсли Параметры.Метод = "GET" Тогда
HTTPОтвет = HTTPСоединение.Получить(HTTPЗапрос);
КонецЕсли;
HTTPОтветСтруктура = HTTPОтветВСтруктуру(HTTPОтвет);
Исключение
ЗаписьЖурналаРегистрации("Обмен с Минтрудом", УровеньЖурналаРегистрации.Ошибка,,, ОбработкаОшибок.ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
HTTPОтветСтруктура = Новый Структура("КодСостояния", 105);
КонецПопытки;

// очистка временных файлов — вне блока Попытка, только созданных функцией
Если Параметры.ТипДанных = "ZIP" Тогда
УдалитьФайлы(ИмяФайлаАрхива);
КонецЕсли;

Возврат HTTPОтветСтруктура;

 

4.6. Разбор ответа

Ответ приходит XML-строкой. Разбирать её регулярками не нужно: ФабрикаXDTO.ПрочитатьXML() без указания типа построит ОбъектXDTO по фактическому содержимому.

Если HTTPОтвет.КодСостояния = 200 Тогда
Чтение = Новый ЧтениеXML();
Чтение.УстановитьСтроку(HTTPОтвет.Тело);
СтруктураXDTO = ФабрикаXDTO.ПрочитатьXML(Чтение);
Если ТипЗнч(СтруктураXDTO) = Тип("ОбъектXDTO") Тогда
 ЗначениеСвойстваXDTO = СтруктураXDTO.ПолучитьXDTO("StatusCode");
   Если ЗначениеСвойстваXDTO <> Неопределено Тогда
    КодСтатуса = ПрочитатьЗначениеXDTO(ЗначениеСвойстваXDTO);
    Сообщение = ПрочитатьЗначениеXDTO(СтруктураXDTO.ПолучитьXDTO("Message"));
    ОбщегоНазначения.СообщитьПользователю(СтрШаблон("Статус: %1 %2", КодСтатуса, Сообщение));
   КонецЕсли;
 КонецЕсли;
КонецЕсли;

Успешный ответ:

<?xml version="1.0" encoding="utf-8"?>
<Response>
<SetId>4282613</SetId>
<SendEducatedPerson>False</SendEducatedPerson>
<Message>Набор был создан</Message>
</Response>

 

Дефект: разбор только формата отказа:

Обработчик ищет StatusCode — элемент, который есть только в формате отказа. В успешном ответе его нет, поэтому пользователь не видит ни номера набора, ни признака передачи в реестр. Разбирать надо оба формата: для Response — показать SetId и SendEducatedPerson, а SetId записать в реквизит документа, потому что иначе протокол с набором в кабинете уже не сопоставить; для Error — показать StatusCode и Message. И ВыгруженВМинтруд ставить по факту успешного ответа, а не по факту нажатия кнопки.

 

4.7. Команда формы документа

Команда размещена в расширении и подключена аннотацией &После к обработчику типовой формы.

&НаКлиенте
Процедура ууц_ВыгрузитьВМинтрудПосле(Команда)

Обработчик = Новый ОписаниеОповещения("ОтправкаВМинтрудЗавершение", ЭтотОбъект);
ПоказатьВопрос(Обработчик, "Будет отправлен пакет слушателей в Минтруд. Продолжить?", РежимДиалогаВопрос.ДаНет);

КонецПроцедуры

&НаКлиенте
Процедура ОтправкаВМинтрудЗавершение(Ответ, Сценарий) Экспорт

Если Ответ <> КодВозвратаДиалога.Да Тогда
Возврат;
КонецЕсли;

Интеграция.ОтправитьвМинтрудНаборСлушателей(Объект.Ссылка);

КонецПроцедуры

Вызов серверного общего модуля прямо из клиентского контекста работает, потому что у модуля Интеграция стоит признак «Вызов сервера». На большой табличной части это плохо: интерфейс замирает на всё время запроса. Правильнее обернуть операцию в ДлительныеОперации.ВыполнитьВФоне() и показать прогресс через ДлительнаяОперацияКлиент.

Схема 2. Форма документа «Протокол», команда передачи набора

Команда «Выгрузить в Минтруд» в командной панели формы. ФИО и реестровые номера скрыты.

 

Состояние табличной части до передачи набора: колонка «Рег. № (Минтруд)» не заполнена.

 

5. Получение реестровых номеров: POST /api/GetEducatedPersonXML

Реализация — функция ЗагрузитьИзМинтруда(СсылкаФЛ, НомерПротокола): формирование фильтра, выполнение запроса, разбор ответа, запись результата в регистр сведений.

5.1. Предпроверка и формирование фильтра

Если ПустаяСтрока(СсылкаФЛ.СтраховойНомерПФР) Тогда
ТекстСообщения = СтрШаблон("У сотрудника %1 пустой номер ПФР", СсылкаФЛ);
ОбщегоНазначенияКлиентСервер.СообщитьПользователю(ТекстСообщения);
ЗаписьЖурналаРегистрации("Загрузка данных физлица из Минтруда", УровеньЖурналаРегистрации.Информация, , , ТекстСообщения);
Возврат Новый Структура;
КонецЕсли;

ПараметрыФайла = Новый Структура;
ПараметрыФайла.Вставить("ApiKey", Константы.КлючAPI_Минтруд.Получить());
ПараметрыФайла.Вставить("Snils", СокрЛП(СсылкаФЛ.СтраховойНомерПФР));
СтруктураФайла = СоздатьXMLДляЗапроса(ПараметрыФайла);

 

<?xml version="1.0" encoding="utf-8"?>
<EducatedPersonFilter xsi:noNamespaceSchemaLocation="schema.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ApiKey>00000000000000000000000000000000</ApiKey>
<Snils>000-000-000 00</Snils>
</EducatedPersonFilter>

 

Тело собираем по схеме раздела 4.4, но с одной частью. Адрес ресурса — /api/GetEducatedPersonXML.

5.2. Разбор ответа

Здесь спотыкается почти каждый, кто читает XML через ФабрикаXDTO без схемы. Тип узла RegistryRecord зависит от количества записей: одна программа — ОбъектXDTO, несколько — список. Тип результата определяют данные, а не схема, поэтому обрабатывать надо оба варианта.

Если ТипЗнч(СтруктураXDTO.RegistryRecord) = Тип("ОбъектXDTO") Тогда
СписокЗаписей = СтруктураXDTO.RegistryRecord; // одна запись
Иначе
СписокЗаписей = СтруктураXDTO.ПолучитьСписок("RegistryRecord"); // несколько
КонецЕсли;

Реестровый номер содержится в атрибуте baseNo записи, результат и дата — во вложенном элементе Test.

Процедура ЗаписатьРезультатОбученияСлушателяИзМинТрудаВРегистр(СписокЗаписей, СсылкаФЛ)

Записи = Новый Массив;
Если ТипЗнч(СписокЗаписей) = Тип("ОбъектXDTO") Тогда
    Записи.Добавить(СписокЗаписей);
Иначе
  Для каждого элСписка Из СписокЗаписей Цикл
    Записи.Добавить(элСписка);
  КонецЦикла;
КонецЕсли;

Для каждого Запись Из Записи Цикл
Тест = Запись.Получить("Test");
Если НЕ ТипЗнч(Тест) = Тип("ОбъектXDTO") Тогда
  Продолжить;
КонецЕсли;

Если Запись.Получить("baseNo") = Неопределено Тогда
  Продолжить; // реестровый номер не присвоен
КонецЕсли;

СтрНабора = Новый Структура;
СтрНабора.Вставить("РегистрационныйНомер", СокрЛП(Запись.baseNo));
СтрНабора.Вставить("НомерПротокола", СокрЛП(Тест.ProtocolNumber));
СтрНабора.Вставить("РезультатОбучения", ?(НРег(Тест.isPassed) = "true", 1, 0));
СтрНабора.Вставить("ДатаПроверки", XMLЗначение(Тип("Дата"), Тест.Date));
СтрНабора.Вставить("ИдПрограммы", Тест.learnProgramId);
СтрНабора.Вставить("Слушатель", СсылкаФЛ.Ссылка);

РегистрыСведений.ДанныеОбученияИзМинтруда.СоздатьЗаписиВРегистре(СтрНабора);
КонецЦикла;

КонецПроцедуры

Мелочи, которые экономят время. Запись.Получить("Test") на отсутствующем узле возвращает Неопределено и не бросает исключение. XMLЗначение(Тип("Дата"), ...) корректно съедает и 2026-06-24, и 2026-06-24T00:00:00. Атрибут isPassed приходит строкой, поэтому сравнение — через НРег().

 

5.3. Запись в регистр

Процедура СоздатьЗаписиВРегистре(ПараметрыЗаписи) Экспорт

НачатьТранзакцию();

Попытка
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.ДанныеОбученияИзМинтруда");
ЭлементБлокировки.УстановитьЗначение("Слушатель", ПараметрыЗаписи.Слушатель);
Блокировка.Заблокировать();

НаборЗаписей = СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Слушатель.Установить(ПараметрыЗаписи.Слушатель);
НаборЗаписей.Отбор.НомерПротокола.Установить(ПараметрыЗаписи.НомерПротокола);
НаборЗаписей.Прочитать();

Если НаборЗаписей.Количество() > 0 Тогда
ТекущаяЗапись = НаборЗаписей[0];
Иначе
ТекущаяЗапись = НаборЗаписей.Добавить();
КонецЕсли;

ЗаполнитьЗначенияСвойств(ТекущаяЗапись, ПараметрыЗаписи); // вне условия
ОбновлениеИнформационнойБазы.ЗаписатьНаборЗаписей(НаборЗаписей);
ЗафиксироватьТранзакцию();

Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;

КонецПроцедуры

 

Дефект: повторная загрузка не обновляет запись:

   В исходной реализации ЗаполнитьЗначенияСвойств() вызывается только в ветке создания новой записи. При повторной загрузке существующая запись читается, но не изменяется и уходит в базу в исходном состоянии. Симптом ровно такой, каким его видит пользователь: нажал ещё раз, а номер не обновился. Вызов надо вынести за Если. Отбор при чтении набора идёт по двум измерениям из трёх — Слушатель и НомерПротокола. Измерение ИдПрограммы в отбор намеренно не включено: внутри одного протокола программа всегда одна, а его включение порождало бы дубли.

 

5.4. Перенос номеров в табличную часть

&НаКлиенте
Процедура ууц_ЗагрузитьИзМинтрудаПосле(Команда)

Для каждого текСтрока Из Объект.Слушатели Цикл
// запись в регистр всех реестровых номеров по программам слушателя
Интеграция.ЗагрузитьИзМинтруда(текСтрока.Слушатель, СокрЛП(Объект.Номер));
// отбор номера по данному протоколу
НомерПрограммы = ПолучитьНомерПрограммыИзКурса(Объект.Курс);
ИмяПрограммы = ПолучитьИмяПрограммыИзКурса(Объект.Курс);
текСтрока.РегистрационныйНомерМинтруд = ПолучитьРегНомерИзМинтруда(СокрЛП(Объект.Номер), текСтрока.Слушатель, НомерПрограммы, ИмяПрограммы);
КонецЦикла;

ЭтаФорма.Модифицированность = Истина;
ОбщегоНазначенияКлиент.СообщитьПользователю("Данные загружены!");

КонецПроцедуры
&НаСервереБезКонтекста
Функция ПолучитьРегНомерИзМинтруда(НомерПротокола, Слушатель, ИдПрограммы, ИмяПрограммы)

Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| Данные.РегистрационныйНомер КАК РегистрационныйНомер
|ИЗ
| РегистрСведений.ДанныеОбученияИзМинтруда КАК Данные
|ГДЕ
| Данные.НомерПротокола = &НомерПротокола
| И Данные.Слушатель = &Слушатель";

Запрос.УстановитьПараметр("НомерПротокола", НомерПротокола);
Запрос.УстановитьПараметр("Слушатель", Слушатель);

РезультатЗапроса = Запрос.Выполнить();
Если РезультатЗапроса.Пустой() Тогда

ОбщегоНазначения.СообщитьПользователю(СтрШаблон("По протоколу №%1 нет данных по слушателю %2 (программа №%4: ""%3"").",НомерПротокола, Строка(Слушатель), ИмяПрограммы, ИдПрограммы));

Возврат "";

КонецЕсли;

Выборка = РезультатЗапроса.Выбрать();
Выборка.Следующий();
Возврат СокрЛП(Выборка.РегистрационныйНомер);

КонецФункции

 

Дефект: обращение к сервису и запрос в цикле

На группе из тридцати человек этот код делает тридцать вызовов сервера, тридцать HTTP-запросов и тридцать запросов к базе. На плохом канале — минуты ожидания с замороженной формой. Лечится в три приёма: один серверный вызов на документ вместо вызова на строку; один запрос к регистру по всей табличной части через Слушатель В (&МассивСлушателей) с последующим сопоставлением; вынос HTTP-обращений в фон через ДлительныеОперации.ВыполнитьВФоне. Число обращений к API при этом не уменьшить — отбор по перечню СНИЛС интерфейсом не предусмотрен, — но их можно убрать из интерактивного сеанса и показать прогресс.

 

Схема 3. Результат получения реестровых номеров

Колонка «Рег. № (Минтруд)» заполнена по строкам табличной части. Персональные данные скрыты.

 

Номер документа при записи приравнивается к номеру группы; значение передаётся в элементе ProtocolNumber и используется при отборе записей реестра.

 

6. Подсистема «Электронная подпись»

6.1. Состав подсистемы в конфигурации

Подсистема «Электронная подпись» БСП развёрнута в полном составе, включая сервис криптографии DSS.

 

Общие модули подсистемы «Электронная подпись».

 

Модули сервиса криптографии DSS.

 

Дополнительно реализована обвязка РаботаСЭПУУЦ / РаботаСЭПКлиент / РаботаСЭПКлиентСервер, специфичная для конфигурации: подписывается не файл, а набор ключевых реквизитов документа. Изменение любого реквизита из набора делает подпись недействительной.

Метод Назначение
РаботаСЭПУУЦ.ПолучитьДвоичныеДанныеОбъекта(Ссылка, Версия, ДопПараметры) Формирует набор ключевых полей объекта, записывает его в текстовый файл, возвращает ДвоичныеДанные файла — подписываемые данные.
РаботаСЭПУУЦ.СформироватьНаборВажныхПолей(...) Определяет состав реквизитов, входящих в набор. Состав задаётся для каждого типа документа.
РаботаСЭПУУЦ.ЗаполнитьСписокПодписей(Ссылка, Дерево, УИД, ГруппаЭП) Заполняет дерево подписей формы из регистра сведений ЭлектронныеПодписи.
РаботаСЭПУУЦ.АктуальнаяВерсияПодписи() Версия схемы набора, сейчас 5. Нужна, чтобы старые подписи проверялись по старым правилам.
ЭлектроннаяПодписьКлиент.Подписать(ОписаниеДанных, Форма, ОбработкаРезультата, ПараметрыПодписи)

Штатная точка входа БСП: выбор сертификата, ввод пароля, обращение к криптопровайдеру, запись подписи в регистр.

 

 

6.2. Реализация команды подписания

&НаКлиенте
Процедура ууц_ПодписатьПосле(Команда)

Если Объект.Проведен Тогда
ОписаниеОповещения = Новый ОписаниеОповещения("ПодписатьЗавершение", ЭтотОбъект);
ОписаниеДанных = Новый Структура;
ОписаниеДанных.Вставить("Операция", НСтр("ru = 'Подписание документа'"));
ОписаниеДанных.Вставить("ЗаголовокДанных", НСтр("ru = 'Документ'"));
ОписаниеДанных.Вставить("Представление", Объект.Ссылка);
ОписаниеДанных.Вставить("ПоказатьКомментарий", Истина);
ОписаниеДанных.Вставить("Данные", РаботаСЭПУУЦ.ПолучитьДвоичныеДанныеОбъекта(Объект.Ссылка, , ));
ОписаниеДанных.Вставить("Объект", Объект.Ссылка);

ЭлектроннаяПодписьКлиент.Подписать(ОписаниеДанных, УникальныйИдентификатор, ОписаниеОповещения);

Иначе
ОбщегоНазначенияКлиентСервер.СообщитьПользователю("Перед подписью приказ нужно провести!");
КонецЕсли;

КонецПроцедуры

Код выглядит канонически — он и скопирован с рабочего подписания документа «Выпуск курса». На документе «Протокол» он тем не менее не работает, и причины лежат не в этом фрагменте, а в метаданных и в составе набора ключевых полей.

 

7. Диагностика отказа при подписании документа «Протокол»

7.1. Тип документа отсутствует в ОпределяемыйТип.ПодписанныйОбъект

Подписи хранятся в регистре сведений ЭлектронныеПодписи; измерение ПодписанныйОбъект имеет тип ОпределяемыйТип.ПодписанныйОбъект. Фактический состав определяемого типа в конфигурации:

ДокументСсылка.ЗачислениеНаКурс

ДокументСсылка.ОтчислениеСКурса

ДокументСсылка.ПереводНаДругойКурс

ДокументСсылка.ПродлениеОбучения

ДокументСсылка.ВыпускКурса

СправочникСсылка.Файлы

СправочникСсылка.ФизическиеЛицаПрисоединенныеФайлы

... (прочие «ПрисоединенныеФайлы»)

ДокументСсылка.Протокол в составе отсутствует, и расширение его туда не добавляет. Значит, ОписаниеДанных.Объект = Объект.Ссылка записать в измерение регистра нечем — подпись физически некуда положить. Что при этом увидит пользователь, зависит от версии БСП: от исключения по несоответствию типов до «подписали, а в списке подписей пусто».

Проверка состава:

ТипыПодписанныхОбъектов = Метаданные.ОпределяемыеТипы.ПодписанныйОбъект.Тип;
Сообщить(ТипыПодписанныхОбъектов.СодержитТип(Тип("ДокументСсылка.Протокол")));
// Ложь — тип в составе отсутствует

Лечится добавлением ДокументСсылка.Протокол в состав определяемого типа ПодписанныйОбъект средствами расширения — определяемые типы заимствуются, состав расширяется. После этого проверьте, что заполняется реквизит ПодписанЭП: процедура РаботаСЭПУУЦ.ЗаполнитьСписокПодписейДокумента читает регистр только при ПодписанЭП = Истина.

7.2. Пустой набор ключевых полей

Состав набора задаёт СформироватьНаборВажныхПолей:

МассивИмен = Новый Массив;
Если ТипЗнч(ОбъектСсылка) = Тип("ДокументСсылка.ЗачислениеНаКурс") Тогда
  МассивИмен = Документы.ЗачислениеНаКурс.ПолучитьИменаКлючевыхРеквизитов(Версия);
ИначеЕсли ТипЗнч(ОбъектСсылка) = Тип("ДокументСсылка.ОтчислениеСКурса") Тогда
  МассивИмен = Документы.ОтчислениеСКурса.ПолучитьИменаКлючевыхРеквизитов(Версия);
ИначеЕсли ТипЗнч(ОбъектСсылка) = Тип("ДокументСсылка.ПереводНаДругойКурс") Тогда
  МассивИмен = Документы.ПереводНаДругойКурс.ПолучитьИменаКлючевыхРеквизитов(Версия);
ИначеЕсли ТипЗнч(ОбъектСсылка) = Тип("ДокументСсылка.ПродлениеОбучения") Тогда
  МассивИмен = Документы.ПродлениеОбучения.ПолучитьИменаКлючевыхРеквизитов(Версия);
ИначеЕсли ТипЗнч(ОбъектСсылка) = Тип("ДокументСсылка.ВыпускКурса") Тогда
  МассивИмен = Документы.ВыпускКурса.ПолучитьИменаКлючевыхРеквизитов(Версия);
ИначеЕсли ТипЗнч(ОбъектСсылка) = Тип("Структура") Тогда

Для Каждого Элемент Из ОбъектСсылка Цикл
  МассивИмен.Добавить(Элемент.Ключ);
КонецЦикла;

КонецЕсли;

 

Ветки для документа «Протокол» в этой лестнице ИначеЕсли нет. Итог: МассивИмен пуст, МассивПолей пуст, СформироватьФайлКлючевыхПолей записывает файл нулевой длины, ПолучитьДвоичныеДанныеОбъекта возвращает пустые двоичные данные. Дальше либо криптопровайдер откажется работать с пустым входом, либо — что гораздо хуже — подпись сформируется от пустого файла. Такая подпись технически валидна и юридически бессмысленна: с содержимым протокола она не связана вообще, и изменение любого реквизита её не сломает.

Проверка:

Данные = РаботаСЭПУУЦ.ПолучитьДвоичныеДанныеОбъекта(СсылкаНаПротокол);
Сообщить(Данные.Размер()); // 0 — набор полей пуст

Вариант устранения А: передача структуры

Обратите внимание на последнюю ветку условия: если на вход пришла Структура, состав полей берётся из её ключей, а значения — из неё же. Это готовая точка расширения, при которой общий модуль основной конфигурации трогать не надо.

&НаСервереБезКонтекста
Функция ДанныеПротоколаДляПодписи(СсылкаПротокол)

Рек = ОбщегоНазначения.ЗначенияРеквизитовОбъекта(СсылкаПротокол, "Номер, Дата, Организация, Курс, Группа, ВидКомиссии, ВидАттестации, ФормаКонтроля, Решение, ПриказОСоставе, Преамбула");

Слепок = Новый Структура;
Слепок.Вставить("Ссылка", СсылкаПротокол);
Слепок.Вставить("Номер", Рек.Номер);
Слепок.Вставить("Дата", Рек.Дата);
Слепок.Вставить("Организация", Рек.Организация);
Слепок.Вставить("Курс", Рек.Курс);
Слепок.Вставить("Группа", Рек.Группа);
Слепок.Вставить("ВидКомиссии", Рек.ВидКомиссии);
Слепок.Вставить("ВидАттестации", Рек.ВидАттестации);
Слепок.Вставить("ФормаКонтроля", Рек.ФормаКонтроля);
Слепок.Вставить("Решение", Рек.Решение);
Слепок.Вставить("ПриказОСоставе", Рек.ПриказОСоставе);
Слепок.Вставить("Преамбула", Рек.Преамбула);
// табличная часть включается хеш-суммой

ТЧ = СсылкаПротокол.Слушатели.Выгрузить(, "НомерСтроки, Слушатель, Оценка, Разряд, РегистрационныйНомерМинтруд");

Хеш = Новый ХешированиеДанных(ХешФункция.SHA256);
Хеш.Добавить(ОбщегоНазначения.ЗначениеВСтрокуXML(ТЧ));
Слепок.Вставить("ХешСлушателей", Base64Строка(Хеш.ХешСумма));

Возврат РаботаСЭПУУЦ.ПолучитьДвоичныеДанныеОбъекта(Слепок);

КонецФункции

 В команде формы меняется одна строка: ОписаниеДанных.Вставить("Данные", ДанныеПротоколаДляПодписи(Объект.Ссылка)). Состав и порядок полей обязаны быть детерминированы, иначе повторная проверка подписи не сойдётся. Поэтому структура собирается явным перечислением, а не обходом метаданных.

Вариант устранения Б: менеджерный метод документа

    Второй путь — привести документ «Протокол» к той же схеме, что и остальные подписываемые документы: добавить расширением в модуль менеджера функцию ПолучитьИменаКлючевыхРеквизитов по образцу «Выпуска курса».

// Модуль менеджера Документа.Протокол (расширение)
Функция ПолучитьИменаКлючевыхРеквизитов(Версия = Неопределено) Экспорт

МассивИмен = Новый Массив;
МассивИмен.Добавить("Организация");
МассивИмен.Добавить("Номер");
МассивИмен.Добавить("Дата");
МассивИмен.Добавить("Курс");
МассивИмен.Добавить("Группа");
МассивИмен.Добавить("ВидКомиссии");
МассивИмен.Добавить("ВидАттестации");
МассивИмен.Добавить("Решение");
МассивИмен.Добавить("Слушатели.Слушатель");
МассивИмен.Добавить("Слушатели.Оценка");
МассивИмен.Добавить("Слушатели.РегистрационныйНомерМинтруд");

Возврат МассивИмен;

КонецФункции

  Дополнительно расширяется СформироватьНаборВажныхПолей аннотацией &После с добавлением ветки для документа «Протокол» (расширение общих модулей поддерживается платформой). Синтаксис «ТабличнаяЧасть.Колонка» обрабатывается штатно: имена полей разворачиваются в Слушатели_0_Слушатель, Слушатели_1_Слушатель и далее.

 

7.3. Режим выполнения операции подписания

Параметр ВыполнятьНаСервере в ОписаниеДанных не задан. Согласно документации подсистемы:

«когда не указан или Неопределено, тогда выполнение будет определено автоматически: если есть сервер, то сначала на сервере, потом (при неудаче) на клиенте, потом сообщение о двух ошибках».

В клиент-серверной базе с разрешённым подписанием на сервере первая попытка уйдёт на сервер 1С. Токен (Рутокен, JaCarta) вставлен в компьютер пользователя, на сервере его нет — попытка падает. Вторая попытка на клиенте предусмотрена, но первое сообщение об ошибке «нет закрытого ключа» пользователь уже увидел и сделал вывод, что не работает.

Устранение для сценария подписания локальным носителем:

ОписаниеДанных.Вставить("ВыполнятьНаСервере", Ложь); // только на клиенте

Заодно проверьте настройки подсистемы: «Электронная подпись и шифрование» ? снять «Разрешить подписание на сервере», если серверных сертификатов у центра нет. Для подписания регламентным заданием физический токен не подойдёт принципиально: нужен сертификат в хранилище сервера либо облачная подпись через СервисКриптографииDSS.

Сводка причин:

Метаданные: ДокументСсылка.Протокол отсутствует в ОпределяемыйТип.ПодписанныйОбъект — подпись не может быть записана в регистр. Логика: СформироватьНаборВажныхПолей не содержит ветки для документа «Протокол» — подписывается файл нулевой длины. Окружение: не задан ВыполнятьНаСервере = Ложь — первая попытка выполняется на сервере при отсутствии там носителя ключа. Первые две причины блокируют сценарий, третья искажает диагностику.

 

8. Подпись Data.xml для передачи в РОЛ

В реализации задействованы две различные по назначению подписи:

  Подпись документа Подпись для передачи в РОЛ
Объект подписи Набор ключевых реквизитов документа (текстовый файл) Файл Data.xml, включаемый в контейнер .olot
Формат CMS согласно настройкам подсистемы CAdES-BES, открепленная
Хранение Регистр сведений ЭлектронныеПодписи Файл Data.xml.sig внутри контейнера .olot
Назначение Контроль неизменности реквизитов документа Требование приёмника для передачи набора в реестр (NeedSend = True)

Отсюда главное: подпись набора ключевых реквизитов, даже правильно сделанная, для реестра бесполезна. В контейнере должна лежать открепленная подпись именно того XML, который уходит на приёмник.

8.1. Формирование открепленной подписи

Откреплённость включается конструктором ЭлектроннаяПодписьКлиент.ПараметрыCMS().

&НаКлиенте
Процедура ВыгрузитьВМинтрудСПодписью(Команда)

// 1. Формирование Data.xml на сервере, возврат адреса временного хранилища
Подготовка = ПодготовитьДанныеДляМинтруда(); // Структура: АдресDataXML
ПараметрыCMS = ЭлектроннаяПодписьКлиент.ПараметрыCMS();
ПараметрыCMS.ТипПодписи = "CAdES-BES";
ПараметрыCMS.Открепленная = Истина;

ОписаниеДанных = Новый Структура;
ОписаниеДанных.Вставить("Операция", НСтр("ru = 'Подписание набора для РОЛ'"));
ОписаниеДанных.Вставить("ЗаголовокДанных", НСтр("ru = 'Набор слушателей Data.xml'"));
ОписаниеДанных.Вставить("Представление", Объект.Ссылка);
ОписаниеДанных.Вставить("ПоказатьКомментарий", Ложь);
ОписаниеДанных.Вставить("ВыполнятьНаСервере", Ложь); // носитель ключа на клиенте

// вариант "структура с ПараметрыCMS" по документации ЭлектроннаяПодписьКлиент.Подписать
ОписаниеДанных.Вставить("Данные", Новый Структура("ПараметрыCMS, Данные", ПараметрыCMS, ПолучитьИзВременногоХранилища(Подготовка.АдресDataXML)));
ДопПараметры = Новый Структура("Подготовка", Подготовка);
Обработчик = Новый ОписаниеОповещения("ПослеПодписанияНабора", ЭтотОбъект, ДопПараметры);
ЭлектроннаяПодписьКлиент.Подписать(ОписаниеДанных, УникальныйИдентификатор, Обработчик);

КонецПроцедуры

Параметр Объект в ОписаниеДанных намеренно не указан. Эта подпись технологическая: она живёт внутри контейнера, к подписанию документа отношения не имеет и в регистр ЭлектронныеПодписи попадать не должна. Поэтому обработку результата берём на себя целиком.

8.2. Извлечение подписи из результата

При успешном завершении ОписаниеДанных дополняется свойством СвойстваПодписи — структурой по образцу ЭлектроннаяПодписьКлиентСервер.НовыеСвойстваПодписи(), где свойство Подпись содержит ДвоичныеДанные результата либо адрес временного хранилища, если операция выполнялась на сервере.

&НаКлиенте
Процедура ПослеПодписанияНабора(ОписаниеДанных, ДопПараметры) Экспорт

Если НЕ ОписаниеДанных.Успех Тогда
ОбщегоНазначенияКлиент.СообщитьПользователю("Набор не подписан.");
Возврат;
КонецЕсли;

СвойстваПодписи = ОписаниеДанных.СвойстваПодписи;
Если ТипЗнч(СвойстваПодписи) = Тип("Строка") Тогда
// операция выполнена на сервере: в свойстве адрес временного хранилища
СвойстваПодписи = ПолучитьИзВременногоХранилища(СвойстваПодписи);
КонецЕсли;

АдресПодписи = ПоместитьВоВременноеХранилище(СвойстваПодписи.Подпись, УникальныйИдентификатор);

// 2. Сборка контейнера и передача с NeedSend = True
Результат = ОтправитьНаборВРОЛ(ДопПараметры.Подготовка.АдресDataXML, АдресПодписи);
ОбщегоНазначенияКлиент.СообщитьПользователю(Результат.Сообщение);

КонецПроцедуры

 

8.3. Сборка контейнера с подписью

&НаСервере
Функция ОтправитьНаборВРОЛ(АдресDataXML, АдресПодписи)

Каталог = ФайловаяСистема.СоздатьВременныйКаталог();
ПутьXML = Каталог + "Data.xml";
ПутьSIG = Каталог + "Data.xml.sig";

ПолучитьИзВременногоХранилища(АдресDataXML).Записать(ПутьXML);
ПолучитьИзВременногоХранилища(АдресПодписи).Записать(ПутьSIG);
ИмяФайлаАрхива = ПолучитьИмяВременногоФайла("olot");

АрхивZip = Новый ЗаписьZipФайла(ИмяФайлаАрхива, , , МетодСжатияZIP.Сжатие, УровеньСжатияZIP.Максимальный);
АрхивZip.Добавить(ПутьXML, РежимСохраненияПутейZIP.НеСохранятьПути);
АрхивZip.Добавить(ПутьSIG, РежимСохраненияПутейZIP.НеСохранятьПути);
АрхивZip.Записать();

ФайловаяСистема.УдалитьВременныйКаталог(Каталог);
// Request.xml с NeedSend = True
ПараметрыФайла = Новый Структура;
ПараметрыФайла.Вставить("ApiKey", Константы.КлючAPI_Минтруд.Получить());
ПараметрыФайла.Вставить("NeedSend", "True");

// ... сборка multipart и POST /api/set/push — см. раздел 4 ...
КонецФункции

Имя файла подписи:

Руководство говорит обтекаемо: «документ с электронной подписью в текстовом файле с расширением .sig». Точное имя выдаёт таблица ошибок валидации: «Указан тег NeedSend со значением true, но не найден файл Data.xml.sig». То есть внутри контейнера должны лежать ровно Data.xml и Data.xml.sig. Симметричный отказ тоже описан: «Указан тег NeedSend со значением false, но добавлен файл Data.xml.sig». В БСП расширение файлов подписи по умолчанию p7s (ЭлектроннаяПодписьКлиент.ПерсональныеНастройки().РасширениеДляФайловПодписи), поэтому имя файла в архиве задавайте явно, а не из настроек.

 

9. Ошибки валидации

Самая полезная страница руководства 2026.1 — таблица типовых ошибок: почти каждая строка соответствует конкретному дефекту в коде формирования пакета. Ниже — те, что относятся к нашим методам, с переводом на язык 1С.

Текст ошибки Причина в реализации
«Неверное количество файлов. Требуется загрузить один файл» Число частей multipart превышает ожидаемое. Для GetEducatedPersonXML — одна часть.
«Некорректная структура файла импорта. Ожидался тип файла .zip» Метод группы worker/*, test/* или СОУТ требует ZIP; передан XML.
«Один файл должен быть расширения .olot, а другой .xml» Расширения в filename частей multipart не соответствуют требованию метода.
«Некорректное расширение файла запроса (допустимы только файлы *.xml)» В архиве присутствует посторонний файл либо неверное расширение.
«Не найден файл Request.xml» Пути в ZIP сохранены; требуется РежимСохраненияПутейZIP.НеСохранятьПути.
«Не найден файл Request.xml.sig» Отсутствует открепленная подпись запроса. Для worker/*, test/*, СОУТ и ПНЗ подпись обязательна.
«Документ не соответствует схеме XSD messages:{ErrorMessages}» Формат даты, отсутствие обязательного либо наличие лишнего элемента. Конкретные узлы перечислены в массиве {ErrorMessages}.
«Указан тег NeedSend со значением true, но не найден файл Data.xml.sig» NeedSend = True при отсутствии подписи в контейнере.
«Указан тег NeedSend со значением false, но добавлен файл Data.xml.sig» Подпись включена в контейнер без изменения значения NeedSend.
«Некорректное значение параметра ApiKey» Длина ключа не равна 32: не выполнено СокрЛП либо в значение константы включён перевод строки.
«Не найдена информация по ключу {apiKey}» Ключ удалён либо перегенерирован. Значение подставляется в текст ошибки, поэтому писать такое сообщение в общедоступный журнал без маскирования нельзя.
«Не найден тип ключа»

Ключ не соотнесён ни с работодателем, ни с обучающей организацией.

Отсюда практическое правило: логировать надо тело ответа целиком, а не только код состояния. Минтруд отдаёт HTTP 200 и с содержательной ошибкой внутри XML — если смотреть только на код, отклонённый набор выглядит успешной выгрузкой.

 

 

10. Контрольный перечень при внедрении

# Условие Следствие несоблюдения
1 Справочник ПрограммыМинтруда заполнен кодами Минтруда, элементы связаны с элементами справочника «Дисциплины» learnProgramId передаётся пустым или нулевым, набор отклоняется
2 СНИЛС заполнен по всем слушателям в формате 000-000-000 00 Часть записей не попадает в реестр; отбор по СНИЛС не возвращает данные
3 ИНН работодателя слушателя заполнен (МестоРаботы.ИНН) Отказ на валидации EmployerInn
4 Файл внутри .olot именован Data.xml, пути в архиве не сохраняются Отказ без детализации причины
5 Разделитель строк multipart — CRLF, кодировка ЗаписьДанных пустая (без BOM) Приёмник не разбирает части, код 400
6 Значение ApiKey хранится только в константе, в коде и комментариях отсутствует Ключ попадает в репозиторий и выгрузки конфигурации
7 ВыгруженВМинтруд устанавливается по факту успешного ответа, SetId сохраняется в документе Состав переданных наборов не восстанавливается, набор не сопоставляется с записью в кабинете
8 Обработчик ответа поддерживает форматы Response и Error Номер набора и причина отказа не обрабатываются
9 Разбор GetEducatedPersonXML учитывает оба типа узла RegistryRecord Исключение по слушателям с несколькими программами
10 ЗаполнитьЗначенияСвойств вынесен за условие создания записи регистра Повторная загрузка не обновляет реестровые номера
11 ДокументСсылка.Протокол включён в ОпределяемыйТип.ПодписанныйОбъект Подпись документа не сохраняется
12 СформироватьНаборВажныхПолей содержит ветку для документа «Протокол» либо используется передача структуры Подписывается файл нулевой длины
13 Для локального носителя ключа задан ВыполнятьНаСервере = Ложь Отказ по отсутствию закрытого ключа
14 Для передачи в РОЛ формируется открепленная подпись Data.xml, файл в архиве именован Data.xml.sig, NeedSend = True Набор создан, но в реестр не передан, либо отказ по отсутствию файла подписи
15 Массовые операции вынесены в ДлительныеОперации Блокировка интерфейса на время обработки табличной части
16 Используется ключ обучающей организации, длина 32 символа Отказы «Не найден тип ключа» и «Некорректное значение параметра ApiKey»
17 Тело ответа фиксируется в журнале целиком, RequestId сохраняется при наличии Отказ с кодом 200 трактуется как успех; обращение в поддержку без идентификатора запроса
18 Исходящий XML валидируется по XSD-схемам с akot.rosmintrud.ru/sout/info Отказ по несоответствию схеме на приёмнике
19 Адреса методов сверены с актуальной редакцией руководства Код 404 после изменения адресов между редакциями
20 Файл .olot удаляется в методе, получившем его путь; операции очистки независимы Накопление временных файлов на сервере
21 В блоке Исключение транспортной функции отсутствует удаление входных данных, присутствует запись в журнал Код 200 замещается кодом 105, ответ не разбирается
22 Data.xml формируется непосредственно в файл (ЗаписьXML.ОткрытьФайл(Путь, "UTF-8"))

Объявление XML без encoding, риск искажения кодировки

11. Заключение

Объём получился скромный: два метода REST API, общий модуль на 1100 строк (из них на Минтруд приходится около 400), справочник соответствия программ, регистр сведений и две команды формы. Сложность здесь не в протоколе обмена. Она в деталях: имя файла внутри контейнера, разделитель строк multipart, форматы даты и СНИЛС, семантика NeedSend, зависимость типа узла XDTO от количества записей в ответе.

Подсистема «Электронная подпись» БСП закрывает и локальный токен, и облачную КЭП через DSS. Но включить её копированием кода с другого документа не получится: тип документа должен быть в ОпределяемыйТип.ПодписанныйОбъект, состав набора ключевых полей — описан, а режим выполнения — согласован с тем, где физически лежит закрытый ключ. И подпись документа с подписью Data.xml путать нельзя: это разные задачи, и вторая всегда открепленная.

Направления развития: перевод обеих операций в фоновые задания с отображением состояния, регламентное задание получения реестровых номеров по протоколам с незаполненными номерами, хранение SetId в реквизите документа, нормализация СНИЛС при формировании набора, передача в реестр с NeedSend = True.

Остальные семнадцать методов ложатся на тот же транспортный слой. worker/create и test/create снимают ручное заведение работников и попыток проверки знаний, а outerId даёт легальный способ прошить в реестре ссылку на объект информационной базы и уйти от сопоставления по СНИЛС. getReportList, getReport и getVerificationList — уже другая история, про работодателя и СОУТ, но техника та же: ZIP, Request.xml, открепленная подпись, разбор XML через ФабрикаXDTO. Написав один транспортный слой, остальные методы получаешь почти бесплатно.

 

Источники

  • ФГИС СОУТ. Руководство пользователя. «Описание работы пользователя в ФГИС СОУТ по работе с API», версия документа 2026.1 — состав методов, структуры XML, таблица ошибок валидации

  • XSD-схемы запросов и ответов: https://akot.rosmintrud.ru/sout/info (раздел «Справочная информация» ? «Программный интерфейс»)

  • Описание интерфейса API ФГИС, редакция периода разработки: https://fgissout.rosmintrud.ru/managedfile/download?guid=4b7f1dcdd6d74900bdd5d87c3937691c

  • Хосты API: edu.rosmintrud.ru (РОЛ), cache.rosmintrud.ru (отчёты СОУТ), protocol.rosmintrud.ru (протоколы комиссии), violations.rosmintrud.ru (ПНЗ)

  • Постановление Правительства РФ от 24.12.2021 № 2464 «О порядке обучения по охране труда и проверки знания требований охраны труда»

  • Библиотека стандартных подсистем: общие модули ЭлектроннаяПодписьКлиент, ЭлектроннаяПодписьКлиентСервер, СервисКриптографииDSS, ФайловаяСистема — документирующие комментарии модулей конфигурации

  • 1С:Управление учебным центром, ред. 2.0. Руководство пользователя

 

Персональные данные на иллюстрациях скрыты. Ключи API, ИНН и наименования организаций в примерах условные.

Вступайте в нашу телеграмм-группу Инфостарт

интеграция Минтруд ЭЦП КЭП слушатель курсы протокол УУЦ Управление учебным центром http JSON REST API ЕИСОТ

Вы можете заказать платную адаптацию этой статьи под ваши задачи на «Бирже заказов».

  • 0% комиссии — оплата напрямую исполнителю;
  • Исполнители любого масштаба — от отдельных специалистов до команд под проект;
  • Прямой обмен контактами между заказчиком и исполнителем;
  • Безопасная сделка — при необходимости;
  • Рейтинги, кейсы и прозрачная система откликов.

См. также

WEB-интеграция Программист 1С:Предприятие 8 1С:Бухгалтерия 3.0 Бытовые услуги, сервис Платные (руб)

Расширение для автоматизации передачи данных между сервисом Vetmanager с 1С: Бухгалтерия 3.0. Решение позволяет загружать документы и справочники из Ветменеджер в 1С:Бухгалтерию, сокращая время на ручной ввод данных и минимизируя ошибки.

24000 руб.

02.02.2021    23601    73    52    

44

WEB-интеграция Анализ продаж Системный администратор Программист Пользователь 1С:Предприятие 8 1С:Розница 2 1С:Управление нашей фирмой 1.6 1С:ERP Управление предприятием 2 1С:Бухгалтерия 3.0 1С:Управление торговлей 11 1С:Комплексная автоматизация 2.х 1С:Управление нашей фирмой 3.0 1С:Розница 3.0 Управленческий учет Платные (руб)

Модуль "Подсистема интеграции AmoCRM с 1С" позволяет обеспечить единое информационное пространство, в котором пользователи могут эффективно управлять клиентской базой, следить за статусами сделок и поддерживать актуальность данных как в AmoCRM, так и в 1С.

60000 руб.

07.05.2019    43761    76    45    

32

Сайты и интернет-магазины WEB-интеграция Системный администратор Программист Пользователь 1С:Предприятие 8 1C:Бухгалтерия 1С:Управление торговлей 11 Автомобили, автосервисы Россия Управленческий учет Платные (руб)

Интеграционный модуль обмена между конфигурацией Альфа Авто 5 и Альфа Авто 6 и порталом AUTOCRM / LOGICSTARS. Данный модуль универсален. Позволяет работать с несколькими обменами AUTOCRM / LOGICSTAR разных брендов в одной информационной базе в ручном и автоматическом режиме.

42700 руб.

03.08.2020    24935    38    26    

29

WEB-интеграция Программист Бизнес-аналитик 1С:Предприятие 8 1С:ERP Управление предприятием 2 1С:Бухгалтерия 3.0 1С:Управление торговлей 11 1С:Комплексная автоматизация 2.х 1С:Управление нашей фирмой 3.0 1С:Розница 3.0 Оптовая торговля, дистрибуция, логистика ИТ-компания Платные (руб)

Модуль "Экспортер" — это расширение для 1С, предназначенное для автоматизации процессов выгрузки данных. Оно позволяет эффективно извлекать, преобразовывать и передавать данные из систем 1С в интеграционную платформу Spot2D. Подсистема упрощает настройку, снижает количество ручных операций и обеспечивает удобный контроль данных.

17568 руб.

20.12.2024    6906    28    4    

30
Для отправки сообщения требуется регистрация/авторизация