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