Платёжка ИИ: распознавание счёта на оплату и создание платёжного поручения в 1С:Бухгалтерии 3.0
Если вам приходится руками переносить реквизиты поставщика из входящего счёта в «Платёжное поручение» — эта обработка для вас. С её помощью достаточно выбрать файл счёта (PDF, JPG, PNG, DOCX, RTF, HTML, TXT), обработка сама отправит файл во внешний сервис распознавания, получит реквизиты, найдёт или создаст контрагента с банковским счётом и откроет заполненную форму нового платёжного поручения. Документ вы проверяете и записываете сами — никаких скрытых проведений.
В статье два слоя: сначала сценарий работы для пользователя, потом разбор ключевых методов для разработчиков.
Что в комплекте
Решение состоит из двух частей:
- Внешняя обработка «Платёжка ИИ» (эта публикация). Подключается как дополнительная обработка заполнения объекта для документа
ПлатежноеПоручениев 1С:Бухгалтерии 3.0. - Сервис распознавания «ИИ Сканер документов» — небольшое Windows-приложение, которое принимает файл по HTTP и возвращает JSON с реквизитами. Скачивается с Инфостарта. Условно-бесплатное.
Обработка не привязана к конкретному сервису жёстко — она работает с любым эндпоинтом, который принимает POST /api/extract_invoice и возвращает JSON формата, описанного ниже.
Установка и подключение
- Скачайте архив с обработкой, сохраните
ПлатёжкаИИ.epfв любую папку. - В 1С:Бухгалтерии 3.0 откройте «Администрирование — Печатные формы, отчёты и обработки — Дополнительные отчёты и обработки».
- Добавьте новую обработку, укажите файл
ПлатёжкаИИ.epf. - Установите и запустите «ИИ Сканер документов» (по умолчанию слушает
http://localhost:5000). - Откройте любой документ «Платёжное поручение» (или список) — в командной панели появится кнопка «Распознать счёт и создать платёжное поручение…».
Сценарий работы пользователя
- Нажмите кнопку «Распознать счёт и создать платёжное поручение…» — откроется форма обработки.
- При необходимости поправьте адрес сервиса (по умолчанию
http://localhost:5000). - Выберите файл счёта — кнопка «Выбрать файл…» запускает штатный диалог помещения файла во временное хранилище.
- Нажмите «Распознать счёт» — файл уйдёт в сервис распознавания, ответ распарсится и отобразится в поле «Данные распознавания (JSON)». Если по ИНН/КПП уже есть контрагент — он подставится в поле «Контрагент».
- Нажмите «Создать / Обновить контрагента» — обработка либо создаст нового контрагента (с головным контрагентом, основным банковским счётом, контактной информацией), либо аккуратно допишет пустые реквизиты существующего.
- Выберите вид операции (оплата поставщику / возврат покупателю) и нажмите «Создать платёжное поручение» — откроется форма нового документа
ПлатежноеПоручениес заполненными реквизитами, суммой, ставкой НДС и назначением платежа.
Документ не создаётся и не проводится автоматически: пользователь всегда видит итоговый документ и принимает решение.
Архитектура решения
Обработка состоит из двух модулей:
- Модуль объекта (
ObjectModule.bsl, ~1200 строк) — программный интерфейс для серверной логики: поиск и создание контрагента, банка, банковского счёта, формирование значений заполнения для документа. Никакого HTTP здесь нет — это переиспользуемые методы, которые можно дёргать и из других обработок. - Модуль формы (
Forms/Форма/Module.bsl) — клиентская логика: выбор файла, HTTP-запрос, разбор JSON, передача данных на сервер через временное хранилище, открытие формы нового документа.
Сценарий «распознать → создать/обновить контрагента → создать платёжное поручение» вынесен в форму именно так, потому что HTTP-вызов должен идти с клиента (на сервере 1С нет штатного HTTPСоединение в управляемом режиме под некоторыми сценариями безопасного режима), а все записи в ИБ — только с сервера. Это типовой паттерн для управляемых форм.
HTTP-клиент: отправка файла на распознавание
Это самая интересная техническая часть. Платформа 8.5.x умеет в HTTPСоединение/HTTPЗапрос, но в ней нет готового помощника для multipart/form-data. Поэтому тело запроса собирается руками — четыре кусочка склеиваются через СоединитьДвоичныеДанные:
&НаКлиенте
Функция ОтправитьЗапросРаспознавания(АдресСервиса, ИмяФайла, ДвоичныеДанныеФайла)
АдресСервисаБезСлеша = СокрЛП(АдресСервиса);
Если Прав(АдресСервисаБезСлеша, 1) = "/" Тогда
АдресСервисаБезСлеша = Лев(АдресСервисаБезСлеша, СтрДлина(АдресСервисаБезСлеша) - 1);
КонецЕсли;
ЗащищенноеСоединение = Неопределено;
ХостСПортом = АдресСервисаБезСлеша;
Если НРег(Лев(ХостСПортом, 8)) = "https://" Тогда
ХостСПортом = Сред(ХостСПортом, 9);
ЗащищенноеСоединение = Новый ЗащищенноеСоединениеOpenSSL(Неопределено, Неопределено);
ИначеЕсли НРег(Лев(ХостСПортом, 7)) = "http://" Тогда
ХостСПортом = Сред(ХостСПортом, 8);
КонецЕсли;
Хост = ХостСПортом;
Порт = 0;
ПозицияДвоеточия = СтрНайти(ХостСПортом, ":");
Если ПозицияДвоеточия > 0 Тогда
Хост = Лев(ХостСПортом, ПозицияДвоеточия - 1);
Порт = Число(СокрЛП(Сред(ХостСПортом, ПозицияДвоеточия + 1)));
КонецЕсли;
Разделитель = "WmkBoundary" + Строка(Новый УникальныйИдентификатор());
ТелоЗапроса = СобратьТелоMultipart(ДвоичныеДанныеФайла, ИмяФайла, Разделитель);
Соединение = Новый HTTPСоединение(Хост, Порт, , , , 120, ЗащищенноеСоединение);
Запрос = Новый HTTPЗапрос("/api/extract_invoice");
Запрос.Заголовки.Вставить("Content-Type", "multipart/form-data; boundary=" + Разделитель);
Запрос.Заголовки.Вставить("Accept", "application/json");
Запрос.УстановитьТелоИзДвоичныхДанных(ТелоЗапроса);
Ответ = Соединение.ВызватьHTTPМетод("POST", Запрос);
// ...
КонецФункции
Разбор адреса сервиса на хост и порт делается вручную — потому что конструктор HTTPСоединениепринимает их отдельными параметрами и не понимает запись host:port сам. Поддерживается http://, https:// (через ЗащищенноеСоединениеOpenSSL), хвостовой / в адресе аккуратно отрезается.
Таймаут выставлен в 120 секунд: распознавание PDF т.к. модель может быит достаточно медленной, поэтому запас по времени нужен.
Само тело multipart/form-data собирается по RFC 7578:
&НаКлиенте
Функция СобратьТелоMultipart(ДвоичныеДанныеФайла, ИмяФайла, Разделитель)
ПереводСтроки = Символы.ВК + Символы.ПС;
Кодировка = КодировкаТекста.UTF8;
ИмяФайлаБезПути = ИмяФайла;
ПозицияРазделителя = СтрНайти(ИмяФайла, "\", НаправлениеПоиска.СКонца);
Если ПозицияРазделителя > 0 Тогда
ИмяФайлаБезПути = Сред(ИмяФайла, ПозицияРазделителя + 1);
КонецЕсли;
ПозицияРазделителя = СтрНайти(ИмяФайлаБезПути, "/", НаправлениеПоиска.СКонца);
Если ПозицияРазделителя > 0 Тогда
ИмяФайлаБезПути = Сред(ИмяФайлаБезПути, ПозицияРазделителя + 1);
КонецЕсли;
ЧастьЗаголовок = "--" + Разделитель + ПереводСтроки
+ "Content-Disposition: form-data; name=""file""; filename=""" + ИмяФайлаБезПути + """" + ПереводСтроки
+ "Content-Type: application/octet-stream" + ПереводСтроки + ПереводСтроки;
ЧастьРазделитель = "--" + Разделитель + "--" + ПереводСтроки;
Части = Новый Массив;
Части.Добавить(ПолучитьДвоичныеДанныеИзСтроки(ЧастьЗаголовок, Кодировка));
Части.Добавить(ДвоичныеДанныеФайла);
Части.Добавить(ПолучитьДвоичныеДанныеИзСтроки(ПереводСтроки, Кодировка));
Части.Добавить(ПолучитьДвоичныеДанныеИзСтроки(ЧастьРазделитель, Кодировка));
Возврат СоединитьДвоичныеДанные(Части);
КонецФункции
Граница формируется как WmkBoundary + уникальный идентификатор — это валидный boundary без спецсимволов, которые могли бы сломать парсинг на сервере. Имя файла очищается от пути — иначе сервер может получить C:\Users\...\file.pdf в filename и расстроиться.
7. Разбор ответа и нормализация данных
Сервис возвращает JSON вида:
{
"success": true,
"file_name": "invoice_001.pdf",
"data": {
"organization_name": "ООО Ромашка",
"inn": "7701234567",
"kpp": "770101001",
"ogrn": "1027700132195",
"address": "101000, г. Москва, ул. Мясницкая, д. 1",
"phone": "+7 (495) 123-45-67",
"bank_name": "ПАО СБЕРБАНК",
"bik": "044525225",
"checking_account": "40702810038000123456",
"corr_account": "30101810400000000225",
"invoice_number": "СЧ-42",
"invoice_date": "2026-09-10",
"has_vat": true,
"total_amount": 118000.0,
"total_vat": 18000.0,
"total_with_vat": 118000.0,
"vat_rate": "20",
"currency": "643",
"items": [
{"name": "Услуга 1", "quantity": 1, "amount": 100000, "vat_rate": 20}
],
"requisites": {
"organization_name": "ООО Ромашка",
"inn": "7701234567"
}
}
}
Разбор — через ЧтениеJSON, ответ всегда приводится к Соответствие:
Формат ответа может различаться: некоторые сервисы кладут реквизиты сразу в data, некоторые — вложенным блоком data.requisites. Обработка умеет и так, и так: если в data нет нужного ключа, но в data.requisites он есть, значение подмешивается:
Если ТипЗнч(Данные) = Тип("Соответствие") Тогда
Requisites = Данные.Получить("requisites");
Если ТипЗнч(Requisites) = Тип("Соответствие") Тогда
Для Каждого КлючЗначение Из Requisites Цикл
Если Данные.Получить(КлючЗначение.Ключ) = Неопределено Тогда
Данные.Вставить(КлючЗначение.Ключ, КлючЗначение.Значение);
КонецЕсли;
КонецЦикла;
КонецЕсли;
Результат.Данные = Данные;
КонецЕсли;
Все остальные серверные методы (НайтиКонтрагентаПоИННКПП, СоздатьКонтрагента, СоздатьПлатежноеПоручение и пр.) работают уже не с JSON, а с этим Соответствие. Поэтому подмена сервиса распознавания — вопрос формата его ответа, не правки кода.
8. Поиск и создание контрагента, банка, банковского счёта
Все три сущности живут в одной транзакции. На вход — Соответствие с реквизитами, на выход — структура с результатом:
Функция СоздатьКонтрагента(Данные) Экспорт
Результат = НовыйРезультатСозданияКонтрагента();
НачатьТранзакцию();
Попытка
НовыйОбъект = Справочники.Контрагенты.СоздатьЭлемент();
ЗаполнитьРеквизитыКонтрагентаИзДанных(НовыйОбъект, Данные);
УстановитьРеквизитБезопасно(НовыйОбъект, "ГоловнойКонтрагент", НовыйОбъект.Ссылка);
НовыйОбъект.Записать();
Результат.Контрагент = НовыйОбъект.Ссылка;
РезультатСчета = СоздатьОбновитьБанковскийСчет(Результат.Контрагент, Данные);
Результат.СчетНовый = РезультатСчета.ЭтоНовый;
Результат.СчетОшибка = РезультатСчета.Ошибка;
Результат.БанкСоздан = РезультатСчета.БанкСоздан;
Результат.БанковскийСчет = РезультатСчета.БанковскийСчет;
Если РезультатСчета.БанковскийСчет <> Неопределено
И НЕ РезультатСчета.БанковскийСчет.Пустая() Тогда
Установлен = УстановитьРеквизитБезопасно(НовыйОбъект, "ОсновнойБанковскийСчет",
РезультатСчета.БанковскийСчет);
Если Установлен Тогда
НовыйОбъект.Записать();
КонецЕсли;
КонецЕсли;
ЗаписатьКонтактнуюИнформациюКонтрагента(Результат.Контрагент, Данные);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
Результат.Ошибка = СтрШаблон(НСтр("ru = 'Не удалось создать контрагента: %1'"),
ИнформацияОбОшибке().Описание);
Возврат Результат;
КонецПопытки;
Возврат Результат;
КонецФункции
ОбновитьКонтрагента устроен так же, только вместо СоздатьЭлемент() — ПолучитьОбъект(), и запись идёт только если БылиИзменения. Это ключевая гигиена: мы не должны перетирать ручные правки бухгалтера.
Поиск банка по БИК — стандартный:
Функция НайтиБанкПоБИК(БИК)
БИК = СокрЛП(БИК);
Если Не ЗначениеЗаполнено(БИК) Тогда
Возврат Неопределено;
КонецЕсли;
Попытка
СсылкаНаБанк = Справочники.Банки.НайтиПоКоду(БИК);
Если СсылкаНаБанк.Пустая() Тогда
Возврат Неопределено;
КонецЕсли;
Возврат СсылкаНаБанк;
Исключение
Возврат Неопределено;
КонецПопытки;
КонецФункции
Если банк не нашёлся — создаётся минимальная карточка с БИК и наименованием. То же самое с банковским счётом: ищем по Владелец + НомерСчета, не нашли — создаём с указанием банка и корр. счёта.
Контактная информация пишется в регистр сведений КонтактнаяИнформация через НаборЗаписей.Отбор, что позволяет обновлять существующие записи вместо дублей.
9. Формирование значений заполнения для платёжного поручения
Это второй важный приём — мы не создаём документ сами, а возвращаем структуру ЗначенияЗаполнения, и форма открывается через ОткрытьФорму с этими значениями. Так пользователь видит итоговый документ и сам решает, сохранять ли его:
Структура ЗначенияЗаполнения собирается по метаданным документа — потому что наименования реквизитов в разных конфигурациях могут отличаться (СуммаДокумента / СуммаПлатежа, Валюта/ ВалютаДокумента). Для каждого кандидата проверяется, есть ли такой реквизит в метаданных:
Процедура ДополнитьЗаполнение(Заполнение, МетаданныеДокумента, ИмяРеквизита, Значение)
Если НЕ ЗначениеЗаполнено(Значение) Тогда
Возврат;
КонецЕсли;
Если НЕ ЕстьРеквизит(МетаданныеДокумента, ИмяРеквизита) Тогда
Возврат;
КонецЕсли;
Заполнение.Вставить(ИмяРеквизита, Значение);
КонецПроцедуры
Заполняется основной набор: дата, организация, контрагент, сумма, валюта, оба счёта (организации и контрагента), назначение платежа, вид платежа (Электронно), очередность (5), ставка и сумма НДС, вид операции, номер счёта на оплату.
Отдельное внимание — назначению платежа. Оно собирается по банковским правилам: «Оплата по счёту №… от …», перечень позиций через ;, и строка с НДС в формате «Сумма NNN р. в т.ч. НДС (20%) NNN р.»:
Функция СформироватьНазначениеПлатежа(Данные)
НомерСчета = СокрЛП(Строка(Данные.Получить("invoice_number")));
ДатаСчета = СокрЛП(Строка(Данные.Получить("invoice_date")));
Части = Новый Массив;
Если ЗначениеЗаполнено(НомерСчета) Тогда
Шапка = "Оплата по счёту №" + НомерСчета;
Если ЗначениеЗаполнено(ДатаСчета) Тогда
Шапка = Шапка + " от " + ДатаСчета;
КонецЕсли;
Части.Добавить(Шапка);
Иначе
Части.Добавить("Оплата по счёту");
КонецЕсли;
// ...
Возврат СтрСоединить(Части, " ");
КонецФункции
10. Безопасная работа с метаданными
Обработка рассчитана на разные конфигурации (БП, УТ, ERP, КА), где имена и состав реквизитов отличаются. Поэтому в коде два помощника — УстановитьРеквизитБезопасно и УстановитьРеквизитБезопасноЛюбоеИз:
Функция УстановитьРеквизитБезопасно(Объект, ИмяРеквизита, Значение)
Если НЕ ЗначениеЗаполнено(Значение) Тогда
Возврат Ложь;
КонецЕсли;
МетаданныеОбъекта = Объект.Метаданные();
Если НЕ ЕстьРеквизит(МетаданныеОбъекта, ИмяРеквизита) Тогда
Возврат Ложь;
КонецЕсли;
Если Объект[ИмяРеквизита] = Значение Тогда
Возврат Ложь;
КонецЕсли;
Объект[ИмяРеквизита] = Значение;
Возврат Истина;
КонецФункции
Функция УстановитьРеквизитБезопасноЛюбоеИз(Объект, Значение, Имя1, Имя2 = "", Имя3 = "")
Если НЕ ЗначениеЗаполнено(Значение) Тогда
Возврат Ложь;
КонецЕсли;
МетаданныеОбъекта = Объект.Метаданные();
ИменаРеквизитов = Новый Массив;
ИменаРеквизитов.Добавить(Имя1);
Если ЗначениеЗаполнено(Имя2) Тогда
ИменаРеквизитов.Добавить(Имя2);
КонецЕсли;
Если ЗначениеЗаполнено(Имя3) Тогда
ИменаРеквизитов.Добавить(Имя3);
КонецЕсли;
Для Каждого ИмяРеквизита Из ИменаРеквизитов Цикл
Если ЕстьРеквизит(МетаданныеОбъекта, ИмяРеквизита) Тогда
Возврат УстановитьРеквизитБезопасно(Объект, ИмяРеквизита, Значение);
КонецЕсли;
КонецЦикла;
Возврат Ложь;
КонецФункции
Логика простая: перед присвоением проверить, что такой реквизит вообще есть; не присваивать пустые значения; не трогать реквизит, если значение не менялось (важно для ОбновитьКонтрагента, чтобы не перезаписывать ручные правки). Поиск идёт как по обычным реквизитам, так и по стандартным:
Функция ЕстьРеквизит(МетаданныеОбъекта, ИмяРеквизита)
Если МетаданныеОбъекта.Реквизиты.Найти(ИмяРеквизита) <> Неопределено Тогда
Возврат Истина;
КонецЕсли;
Для Каждого Описание Из МетаданныеОбъекта.СтандартныеРеквизиты Цикл
Если Описание.Имя = ИмяРеквизита Тогда
Возврат Истина;
КонецЕсли;
КонецЦикла;
Возврат Ложь;
КонецФункции
Такой подход — ключ к переносимости обработки между конфигурациями без форка кода.
11. Ограничения и планы
- Не реализовано: распознавание нескольких файлов за раз, распознавание акта/накладной (только счёт на оплату), Drag-and-Drop в форму, кеширование распознанных данных.
- Требует доработки под конкретную конфигурацию, если у вас очень специфичные реквизиты контрагента. Все ключевые места отмечены
УстановитьРеквизитБезопасноЛюбоеИз— добавить альтернативное имя просто. - Безопасность: сервис распознавания запускается на той же машине, что и 1С (
http://localhost:5000), поэтому трафик наружу не уходит. Если будете поднимать сервис на отдельной машине — позаботьтесь о HTTPS. - Совместимость: обработка проверялась на платформе 8.5.1 в режиме управляемых форм. На 8.3.20+ должна работать без изменений.
В планах: добавить массовую загрузку, поддержать акты и накладные.
Пишите Ваши вопросы в комментариях, буду рад ответить.
Проверено на следующих конфигурациях и релизах:
- Бухгалтерия предприятия, редакция 3.0, релизы 3.0.203.18
Вступайте в нашу телеграмм-группу Инфостарт