Почему внешняя печатная форма не формирует табличный документ в облаке или клиент-сервере и как это исправить

Программист 1С v8.3 (Управляемые формы) 1С:Зарплата и Управление Персоналом IT и автоматизация бизнеса
← На главную

При переносе внешних печатных форм (ВПФ) из локальных файловых баз в облачные сервисы (например, 1С:Фреш, Scloud, 42 Cloud) или на стандартный клиент-серверный вариант работы разработчики часто сталкиваются со специфической ошибкой подсистемы печати БСП (Библиотеки Стандартных Подсистем): «В обработчике печати не был сформирован табличный документ для: ВнешняяОбработка...». При этом на локальном компьютере в файловом режиме та же самая печатная форма может работать абсолютно корректно и без каких-либо сбоев.

В этой статье мы подробно разберем, почему возникает эта ошибка, проанализируем механизмы работы подсистемы печати БСП на сервере, исправим логику регистрации внешней обработки и попутно оптимизируем критические узлы кода, которые могут привести к зависанию базы данных в облаке.

Выясняем причину: почему локально работает, а в облаке — нет?

Основная причина кроется в том, как подсистема УправлениеПечатью из состава БСП обрабатывает идентификаторы команд печати, и как ведет себя платформа в различных режимах компиляции. Давайте проанализируем исходный код, который вызвал сбой у автора темы.

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


Функция УказатьНаименованиеКомандыПечати()
    Наименование = ЭтотОбъект.Метаданные.ПолноеИмя;
    Возврат Наименование; // Возвращает "ВнешняяОбработка.ИмяВнешнейОбработки"
КонецФункции

Использование свойства ЭтотОбъект.Метаданные.ПолноеИмя приводит к тому, что в качестве идентификатора команды в таблицу команд записывается строка вида "ВнешняяОбработка.ИмяВашейОбработки". Наличие точки в идентификаторе команды — это критическая уязвимость для парсера БСП. Разберем по шагам, что происходит на сервере при вызове печати:

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

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

Решение проблемы с идентификаторами команд по шагам

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

Шаг 1. Исправление функции регистрации команды

Откроем модуль объекта внешней печатной формы и найдем экспортную функцию СведенияОВнешнейОбработке. Нам необходимо прописать простой строковый идентификатор без точек (в «верблюжьем» регистре) вместо вызова УказатьНаименованиеКомандыПечати().

Рассмотрим корректный пример заполнения сведений:


Функция СведенияОВнешнейОбработке() Экспорт
    ПараметрыРегистрации = Новый Структура;
    МассивНазначений = Новый Массив;
    
    // Указываем документ-адресат печатной формы
    МассивНазначений.Добавить("Документ.НачислениеЗарплаты"); 
    
    ПараметрыРегистрации.Вставить("Вид", "ПечатнаяФорма");
    ПараметрыРегистрации.Вставить("Назначение", МассивНазначений);
    ПараметрыРегистрации.Вставить("Наименование", "Приказ о поощрении сотрудников");
    ПараметрыРегистрации.Вставить("Версия", "1.2");
    
    // Для работы в облаке (например, 1С:Фреш) безопасный режим должен быть равен Истина
    ПараметрыРегистрации.Вставить("БезопасныйРежим", Истина); 
    ПараметрыРегистрации.Вставить("Информация", "Дополнительная печатная форма приказа о поощрении");
    
    ТаблицаКоманд = ПолучитьТаблицуКоманд();
    
    // ВАЖНО: Третий параметр - идентификатор команды. Никаких точек!
    // Используем простое и понятное имя "ПриказОПоощрении"
    ДобавитьКоманду(ТаблицаКоманд, 
                    "Приказ о поощрении работников", 
                    "ПриказОПоощрении", 
                    "ВызовСерверногоМетода", 
                    Истина, 
                    "ПечатьMXL");
                    
    ПараметрыРегистрации.Вставить("Команды", TablicaKomand);
    Возврат ПараметрыРегистрации;
КонецФункции

Шаг 2. Корректировка экспортной процедуры Печать

Теперь перейдем к процедуре Печать, которая является точкой входа при вызове формирования документа из интерфейса БСП. Нам нужно передать точно такой же строковый идентификатор в метод вывода коллекции.


Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
    
    // Проверяем, что БСП запрашивает именно нашу команду
    ПараметрыВывода.ДоступнаПечатьПоУмолчанию = Истина;
    
    // Получаем сформированный табличный документ из нашей внутренней функции
    ТабличныйДокумент = ПечатьФормы(МассивОбъектов, ОбъектыПечати, КоллекцияПечатныхФорм, ПараметрыВывода);
    
    // Выводим документ в коллекцию. 
    // Второй параметр должен строго совпадать с идентификатором из СведенияОВнешнейОбработке!
    УправлениеПечатью.ВывестиТабличныйДокументВКоллекцию(
        КоллекцияПечатныхФорм, 
        "ПриказОПоощрении", // Тот самый строковый идентификатор без точек
        "Приказ о поощрении работников", 
        ТабличныйДокумент
    );
    
КонецПроцедуры

Критическая оптимизация: избавляемся от запросов в цикле

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


Для Каждого ТекСтрока Из Документ.Сотрудники Цикл
    // ...
    ЗапросСотр = Новый Запрос;
    ЗапросСотр.Текст = "ВЫБРАТЬ ... ИЗ РегистрСведений.КадроваяИсторияСотрудниковИнтервальный ...";
    // ...
    РезультатЗапросаСотр = ЗапросСотр.Выполнить();
    // ...
КонецЦикла;

В локальной файловой базе на 2–3 сотрудниках это отработает быстро. Но в облаке или крупной клиент-серверной базе, где одновременно работают сотни пользователей, такой код вызовет катастрофическое падение производительности из-за сетевых задержек (Latency) между сервером приложений 1С и СУБД. Каждый проход цикла — это отдельное TCP-соединение и транзакционный запрос к серверу баз данных.

Перепишем этот участок кода правильно. Перенесем запрос за пределы цикла, собрав все данные сотрудников одним пакетным запросом.

Проанализируем оптимизированный вариант формирования печатной формы:


Функция ПечатьФормы(МассивОбъектов, ОбъектыПечати, КомплектыПечати, ПараметрыПечати)
    
    Макет = ПолучитьМакет("МакетОтчета");
    ТабличныйДокумент = Новый ТабличныйДокумент;
    Первый = Истина;
    
    // 1. Сначала собираем всех сотрудников из всех документов на печать в один массив
    МассивСотрудников = Новый Массив;
    Для Каждого ДокументСсылка Из МассивОбъектов Цикл
        Для Каждого СтрокаСотр Из ДокументСсылка.Сотрудники Цикл
            Если МассивСотрудников.Найти(СтрокаСотр.Сотрудник) = Неопределено Тогда
                МассивСотрудников.Добавить(СтрокаСотр.Сотрудник);
            КонецЕсли;
        КонецЦикла;
    КонецЦикла;
    
    // Если сотрудников нет, возвращаем пустой документ
    Если МассивСотрудников.Количество() = 0 Тогда
        Возврат ТабличныйДокумент;
    КонецЕсли;
    
    // 2. Делаем ОДИН запрос для получения кадровых данных по всем сотрудникам сразу
    ЗапросКадры = Новый Запрос;
    ЗапросКадры.Текст = 
    "ВЫБРАТЬ
    |	КадроваяИстория.Сотрудник КАК Сотрудник,
    |	КадроваяИстория.Подразделение КАК Подразделение,
    |	КадроваяИстория.Должность КАК Должность
    |ИЗ
    |	РегистрСведений.КадроваяИсторияСотрудниковИнтервальный КАК КадроваяИстория
    |ГДЕ
    |	КадроваяИстория.Сотрудник В(&МассивСотрудников)
    |	И &ДатаПриказа МЕЖДУ КадроваяИстория.ДатаНачала И КадроваяИстория.ДатаОкончания";
    
    // В качестве даты среза берем дату первого документа (для упрощения примера)
    ЗапросКадры.УстановитьПараметр("МассивСотрудников", МассивСотрудников);
    ЗапросКадры.УстановитьПараметр("ДатаПриказа", МассивОбъектов[0].Дата);
    
    ТаблицаКадровыхДанных = ЗапросКадры.Выполнить().Выгрузить();
    
    // Для быстрого поиска в памяти построим соответствие "Сотрудник -> СтрокаДанных"
    КадровыеДанныеСотрудников = Новый Соответствие;
    Для Каждого СтрокаКадров Из ТаблицаКадровыхДанных Цикл
        КадровыеДанныеСотрудников.Вставить(СтрокаКадров.Сотрудник, СтрокаКадров);
    КонецЦикла;
    
    // 3. Основной цикл вывода печатных форм документов
    Для ИндексМассива = 0 По МассивОбъектов.Количество() - 1 Цикл
        
        Если НЕ Первый Тогда
            ТабличныйДокумент.ВывестиГоризонтальныйРазделительСтраниц();
        Иначе
            Первый = Ложь;
        КонецЕсли;
        
        Документ = МассивОбъектов[ИндексМассива];
        Организация = Документ.Организация;
        
        // ВЫВОД ШАПКИ
        ОбластьШапка = Макет.ПолучитьОбласть("Шапка");
        ОбластьШапка.Параметры.НазваниеОрганизации = Организация.НаименованиеПолное;
        ОбластьШапка.Параметры.КодОКПО = Организация.КодПоОКПО;
        ОбластьШапка.Параметры.ДатаДок = Формат(Документ.Дата, "ДЛФ=Д");
        
        // Получаем номер на печать стандартными средствами БСП
        НомерДок = СокрЛП(ЗарплатаКадрыОтчеты.НомерНаПечать(Документ.Номер));
        ОбластьШапка.Параметры.НомерДок = ?(ЗначениеЗаполнено(НомерДок), НомерДок, СокрЛП(Документ.Номер));
        
        ТабличныйДокумент.Вывести(ОбластьШапка);
        
        // ВЫВОД СОТРУДНИКОВ
        Для Каждого ТекСтрока Из Документ.Сотрудники Цикл
            
            ОбластьСотр = Макет.ПолучитьОбласть("Сотр");
            Сотрудник = ТекСтрока.Sotrudnik;
            
            ОбластьСотр.Параметры.ФИО = Сотрудник.Наименование;
            
            // Получаем табельный номер без лишних нулей
            ТабНомер = СокрЛП(ЗарплатаКадрыОтчеты.ТабельныйНомерНаПечать(Сотрудник.Код));
            ОбластьСотр.Параметры.ТабНомер = ТабНомер;
            
            // Быстро извлекаем данные из соответствия в памяти вместо тяжелого запроса к СУБД!
            ДанныеСотрудника = КадровыеДанныеСотрудников[Сотрудник];
            Если ДанныеСотрудника <> Неопределено Тогда
                ОбластьСотр.Параметры.Подразделение = СокрЛП(ДанныеСотрудника.Подразделение.Наименование);
                ОбластьСотр.Параметры.Должность = СокрЛП(ДанныеСотрудника.Должность.Наименование);
            Иначе
                ОбластьСотр.Параметры.Подразделение = "";
                ОбластьСотр.Параметры.Должность = "";
            КонецЕсли;
            
            ОбластьСотр.Параметры.Период = " " + Формат(ТекСтрока.ДатаНазначения, "ДЛФ=Д") + " - " + Формат(ТекСтрока.ДатаОкончания, "ДЛФ=Д");
            
            ТабличныйДокумент.Вывести(ОбластьСотр);
            
        КонецЦикла;
        
        // ВЫВОД ПОДВАЛА
        ОбластьПодвал = Макет.ПолучитьОбласть("Подвал");
        
        // Получаем ответственных лиц организации через БСП механизмы
        ОтветственныеЛица = Новый Структура("Организация, Руководитель", Организация);
        ЗарплатаКадры.ПолучитьЗначенияПоУмолчанию(ОтветственныеЛица, Документ.Дата);
        
        ОбластьПодвал.Параметры.ФИОРуководителя = ФизическиеЛицаЗарплатаКадры.РасшифровкаПодписи(ОтветственныеЛица.Руководитель);
        ТабличныйДокумент.Вывести(ОбластьПодвал);
        
    КонецЦикла;
    
    // Тонкие настройки вывода документа на печать
    ТабличныйДокумент.ОриентацияСтраницы = ОриентацияСтраницы.Портрет;
    ТабличныйДокумент.АвтоМасштаб = Истина;
    ТабличныйДокумент.ОтображатьСетку = Ложь;
    
    Возврат ТабличныйДокумент;
    
КонецФункции

Резюме и правила хорошего тона при разработке ВПФ для облака

Для того чтобы ваши внешние обработки работали стабильно и быстро в любых средах (будь то скромный файловый киоск или высокопроизводительный кластер 1С в облаке), рекомендуем придерживаться следующих правил:

  1. Статичные идентификаторы: Никогда не используйте точки, тире или служебные префиксы в именах команд. Идентификатор должен состоять только из латинских или русских букв и цифр, начинаясь с буквы.
  2. Безопасный режим: В облачных сервисах публикации ВПФ всегда происходит в безопасном режиме (БезопасныйРежим = Истина). Убедитесь, что ваш код не использует запрещенные методы файловой системы на сервере, COM-соединения или прямые системные вызовы.
  3. Отказ от циклов запросов: Сначала собирайте все необходимые ссылки во временные массивы, выполняйте один пакетный запрос, выгружайте результат в Соответствие или ТаблицуЗначений и только потом заполняйте области макета табличного документа.
  4. Использование серверного контекста БСП: Получайте ФИО ответственных лиц, представление номеров документов, расшифровку подписей и табельные номера через встроенные общие модули БСП конфигурации (например, ФизическиеЛицаЗарплатаКадры или ОбщегоНазначения). Это гарантирует корректное склонение фамилий по падежам и учет актуальной истории кадровых изменений на дату документа.

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

← На главную