Разработка внешних печатных форм (ВПФ) для конфигураций на базе Библиотеки стандартных подсистем (БСП), таких как 1С:Комплексная автоматизация 2, 1С:ERP или 1С:Управление торговлей 11, часто преподносит неприятные сюрпризы начинающим программистам. Типичная ситуация: при открытии обработки напрямую через меню Файл — Открыть всё работает идеально, но стоит зарегистрировать её в справочнике Дополнительные отчеты и обработки, как сеанс 1С намертво зависает или выводит абсолютно пустой бланк без данных.
В этой статье мы подробно разберем, почему возникают подобные проблемы, проанализируем внутренние механизмы работы платформы 1С и БСП, а также по шагам исправим код, сделав его отказоустойчивым и быстрым.
---
Выясним причину, по которой сеанс 1С полностью зависает при вызове команды печати из документа. Проанализируем фрагмент кода, который часто пишут разработчики при попытке адаптировать печать под стандарты БСП:
Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
МассивОбъектов = Новый Соответствие();
МассивОбъектов.Вставить("Документ.ПеремещениеТоваров", МассивОбъектов);
ПечатнаяФорма = ПечатьТребованиеНакладнаяНовая(МассивОбъектов, ОбъектыПечати);
...
КонецПроцедуры
Посмотрим внимательнее, что происходит в этих строках. Переменная МассивОбъектов передается в процедуру БСП в качестве входящего параметра (это массив ссылок на документы, выбранные пользователем для печати). Внутри процедуры разработчик переопределяет эту переменную, создавая объект Новый Соответствие(), а затем пытается вставить в это соответствие сам этот же объект МассивОбъектов в качестве значения.
Такая конструкция порождает циклическую (рекурсивную) ссылку в памяти. Когда платформа 1С пытается передать или обработать такую переменную на сервере (например, при сериализации данных для передачи между клиентом и сервером), движок платформы уходит в бесконечный цикл, пытаясь разыменовать бесконечно вложенный объект. В результате сеанс пользователя зависает, а на сервере резко возрастает нагрузка на процессор.
Правило: Никогда не переопределяйте входящие параметры БСП и избегайте вложенных циклических структур в коллекциях данных.
---
Теперь разберем вторую проблему: почему при запуске через Файл — Открыть макет заполняется данными, а при вызове из документа в штатном режиме БСП выводится пустой шаблон?
Когда вы тестируете печатную форму как обычную обработку, вы добавляете на её форму реквизит СсылкаНаОбъект для выбора документа-донора. В коде формирования запроса вы напрямую обращаетесь к этому реквизиту:
Запрос.УстановитьПараметр("ТекущийДокумент", СсылкаНаОбъект.Ссылка);
Запрос.УстановитьПараметр("СкладОтправитель", СсылкаНаОбъект.СкладОтправитель);
Однако при регистрации ВПФ в системе и её вызове через меню Печать БСП не открывает форму обработки. Она загружает объект во временное хранилище и напрямую вызывает экспортную процедуру Печать() в модуле объекта. В этом сценарии реквизит модуля объекта СсылкаНаОбъект остается пустым (Неопределено).
Вместо этого список документов передается в процедуру в виде массива в параметре МассивОбъектов. Если внутри алгоритма печати вы продолжаете обращаться к СсылкаНаОбъект, ваши запросы получают пустые параметры, возвращают пустые выборки, и на экран выводится пустой шаблон макета без реквизитов и табличной части.
---
Начнем исправление печатной формы с функции регистрации. Откажемся от устаревших ручных таблиц команд и напишем современную регистрационную функцию, совместимую с актуальными версиями БСП:
Функция СведенияОВнешнейОбработке() Экспорт
ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.4.5.24");
ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.ВидОбработкиПечатнаяФорма();
ПараметрыРегистрации.Версия = "1.1";
ПараметрыРегистрации.Наименование = "Требование-накладная с остатком";
ПараметрыРегистрации.Информация = "Внешняя печатная форма требования-накладной с выводом остатков на складе отправителе.";
ПараметрыРегистрации.БезопасныйРежим = Ложь; // Ложь, если требуется работа с привилегированным режимом
// Указываем документы-назначения
ПараметрыРегистрации.Назначение.Добавить("Документ.ПеремещениеТоваров");
// Добавляем команду печати
Команда = ПараметрыРегистрации.Команды.Добавить();
Команда.Представление = НСтр("ru = 'Требование-накладная с остатком'");
Команда.Идентификатор = "ТребованиеНакладнаяПеремещениеСОстатком";
Команда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода();
Команда.ПоказыватьОповещение = Ложь;
Команда.Модификатор = "ПечатьMXL";
Возврат ПараметрыРегистрации;
КонецФункции
---
Процедура Печать должна служить исключительно диспетчером: принимать массив документов, вызывать функцию генерации табличного документа и регистрировать его в коллекции печатных форм. Напишем её правильно:
Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
// Проверяем, есть ли наша команда в коллекции к выводу
ПараметрыВывода.БыстраяПечать = Ложь;
ТабличныйДокумент = ПечатьТребованиеНакладнаяНовая(МассивОбъектов, ОбъектыПечати);
// Выводим сформированный документ в коллекцию форм БСП
УправлениеПечатью.ВывестиТабличныйДокументВКоллекцию(
КоллекцияПечатныхФорм,
"ТребованиеНакладнаяПеремещениеСОстатком", // Идентификатор команды из СведенийОВнешнейОбработке
"Требование-накладная с остатком", // Представление для пользователя
ТабличныйДокумент
);
КонецПроцедуры
---
Теперь спроектируем главную функцию формирования табличного документа. Обратите внимание на то, как мы перебираем элементы коллекции МассивОбъектов. Мы полностью отказываемся от использования СсылкаНаОбъект и работаем исключительно с локальной переменной Ссылка, получаемой в цикле.
Функция ПечатьТребованиеНакладнаяНовая(МассивОбъектов, ОбъектыПечати) Экспорт
// Зададим режим повышенных прав, если это необходимо для чтения остатков
УстановитьПривилегированныйРежим(Истина);
ДопКолонка = Константы.ДополнительнаяКолонкаПечатныхФормДокументов.Получить();
Если ДопКолонка = Перечисления.ДополнительнаяКолонкаПечатныхФормДокументов.Артикул Тогда
ТоварКод = "Артикул";
Иначе
ТоварКод = "Код";
КонецЕсли;
Макет = ПолучитьМакет("ТребованиеНакладнаяПеремещениеСОстатком");
ТабличныйДокумент = Новый ТабличныйДокумент;
ТабличныйДокумент.ИмяПараметровПечати = "ПАРАМЕТРЫ_ПЕЧАТИ_ТребованиеНакладнаяПеремещениеСОстатком";
ПервыйДокумент = Истина;
// Обходим массив переданных ссылок на документы перемещения
Для Каждого Ссылка Из МассивОбъектов Цикл
Если Не ПервыйДокумент Тогда
ТабличныйДокумент.ВывестиГоризонтальныйРазделительСтраниц();
КонецЕсли;
ПервыйДокумент = Ложь;
НомерСтрокиНачало = ТабличныйДокумент.ВысотаТаблицы + 1;
// 1. Запрос для получения шапки конкретного документа
ЗапросШапка = Новый Запрос;
ЗапросШапка.Текст =
"ВЫБРАТЬ
| Перемещение.Номер КАК НомерДокумента,
| Перемещение.Дата КАК ДатаДокумента,
| Перемещение.Организация,
| Перемещение.СкладОтправитель КАК ОтправительПодразделение,
| Перемещение.СкладПолучатель КАК ПолучательПодразделение
|ИЗ
| Документ.ПеремещениеТоваров КАК Перемещение
|ГДЕ
| Перемещение.Ссылка = &ТекущийДокумент";
ЗапросШапка.УстановитьПараметр("ТекущийДокумент", Ссылка);
ВыборкаШапка = ЗапросШапка.Выполнить().Выбрать();
Если Не ВыборкаШапка.Следующий() Тогда
Продолжить;
КонецЕсли;
// 2. Запрос для получения товаров и остатков на складе отправителе
ЗапросТовары = Новый Запрос;
ЗапросТовары.Текст =
"ВЫБРАТЬ
| ПеремещениеТовары.Номенклатура,
| ПеремещениеТовары.Номенклатура.НаименованиеПолное КАК ТоварНаименование,
| ПеремещениеТовары.Номенклатура." + ТоварКод + " КАК ТоварКод,
| ПеремещениеТовары.Количество,
| ЕСТЬNULL(ТоварыНаСкладахОстатки.ВНаличииОстаток, 0) КАК КоличествоОстаток
|ИЗ
| Документ.ПеремещениеТоваров.Товары КАК ПеремещениеТовары
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки(&ДатаОстатков, Склад = &СкладОтправитель) КАК ТоварыНаСкладахОстатки
| ПО ПеремещениеТовары.Номенклатура = ТоварыНаСкладахОстатки.Номенклатура
|ГДЕ
| ПеремещениеТовары.Ссылка = &ТекущийДокумент
|УПОРЯДОЧИТЬ ПО
| ПеремещениеТовары.НомерСтроки";
ЗапросТовары.УстановитьПараметр("ТекущийДокумент", Ссылка);
ЗапросТовары.УстановитьПараметр("СкладОтправитель", Ссылка.СкладОтправитель);
ЗапросТовары.УстановитьПараметр("ДатаОстатков", Новый Граница(Ссылка.Дата, ВидГраницы.Включая));
ВыборкаТовары = ЗапросТовары.Выполнить().Выбрать();
// 3. Вывод областей макета
// Выводим шапку документа
ОбластьЗаголовок = Макет.ПолучитьОбласть("Заголовок");
ОбластьЗаголовок.Параметры.Заполнить(ВыборкаШапка);
ОбластьЗаголовок.Параметры.ТекстЗаголовка = "ТРЕБОВАНИЕ-НАКЛАДНАЯ № " + ВыборкаШапка.НомерДокумента;
ОбластьЗаголовок.Параметры.Отправитель = ВыборкаШапка.ОтправительПодразделение;
ОбластьЗаголовок.Параметры.Получатель = ВыборкаШапка.ПолучательПодразделение;
ТабличныйДокумент.Вывести(ОбластьЗаголовок);
// Выводим шапку таблицы
ОбластьШапкаТаблицы = Макет.ПолучитьОбласть("Шапка");
ТабличныйДокумент.Вывести(ОбластьШапкаТаблицы);
// Выводим строки товаров
Ном = 0;
КоличествоВсего = 0;
ОбластьСтрока = Макет.ПолучитьОбласть("Строка");
Пока ВыборкаТовары.Следующий() Цикл
Если Не ЗначениеЗаполнено(ВыборкаТовары.Номенклатура) Тогда
Продолжить;
КонецЕсли;
Ном = Ном + 1;
КоличествоВсего = КоличествоВсего + ВыборкаТовары.Количество;
ОбластьСтрока.Параметры.Заполнить(ВыборкаТовары);
ОбластьСтрока.Параметры.НомерСтроки = Строка(Ном);
ОбластьСтрока.Параметры.ТоварНаименование = ВыборкаТовары.ToварНаименование;
ТабличныйДокумент.Вывести(ОбластьСтрока);
КонецЦикла;
// Выводим итоги
ОбластьИтого = Макет.ПолучитьОбласть("Итого");
ОбластьИтого.Параметры.КоличествоВсего = КоличествоВсего;
ТабличныйДокумент.Вывести(ОбластьИтого);
// Выводим подписи
ОбластьПодписи = Макет.ПолучитьОбласть("Подписи");
ТабличныйДокумент.Вывести(ОбластьПодписи);
// Задаем область печати для БСП
ЗадатьОбластьПечатиДокумента(ТабличныйДокумент, НомерСтрокиНачало, ОбъектыПечати, Ссылка);
КонецЦикла;
Возврат ТабличныйДокумент;
КонецФункции
Процедура ЗадатьОбластьПечатиДокумента(ТабличныйДокумент, НомерСтрокиНачало, ОбъектыПечати, Ссылка)
Элемент = ОбъектыПечати.НайтиПоЗначению(Ссылка);
Если Элемент = Неопределено Тогда
ИмяОбласти = "Документ_" + Формат(ОбъектыПечати.Количество() + 1, "ЧН=; ЧГ=");
ОбъектыПечати.Добавить(Ссылка, ИмяОбласти);
Иначе
ИмяОбласти = Элемент.Представление;
КонецЕсли;
НомерСтрокиОкончание = ТабличныйДокумент.ВысотаТаблицы;
ТабличныйДокумент.Область(НомерСтрокиНачало, , НомерСтрокиОкончание, ).Имя = ИмяОбласти;
КонецПроцедуры
---
Рассмотрим ситуации, когда код написан корректно, но ВПФ всё равно вызывает зависание или ошибки при запуске в реальной рабочей базе:
При печати документов через дополнительные обработки БСП часто запускает генерацию табличного документа в фоновом задании на сервере. Если внутри алгоритма печати (например, при проверке условий или заполнении реквизитов) присутствует вызов клиентских функций (Предупреждение(), Вопрос(), ОткрытьФорму()), фоновый процесс зависнет навсегда, ожидая ответа пользователя. На сервере невозможно отобразить модальное окно, поэтому сеанс «засыпает».
При регистрации внешней обработки в базе данных по умолчанию устанавливается флажок Безопасный режим = Истина. В безопасном режиме платформа блокирует вызовы привилегированного режима (УстановитьПривилегированныйРежим(Истина)), работу с файловой системой сервера и COM-объектами. Попытка выполнить эти действия в безопасном режиме может привести к немому зависанию фонового процесса. Если вашей ВПФ требуются расширенные привилегии (например, для чтения цен или остатков товаров, к которым у обычного пользователя нет доступа), укажите при регистрации БезопасныйРежим = Ложь.
Это классический баг платформы 1С. Если у строк или ячеек вашего макета в свойствах во вкладке «Параграф» установлено свойство Вместе со следующим (Keep with next), и при этом табличный документ не помещается на одну страницу по ширине или высоте, движок печати уходит в бесконечный цикл расчета переноса страниц. Программа мгновенно зависает при вызове метода Показать(). Всегда проверяйте и отключайте это свойство в макетах.
---
Чтобы не загружать файл обработки в справочник «Дополнительные отчеты и обработки» после каждого изменения кода, добавим в саму обработку форму отладки. Создадим управляемую форму, добавим на неё реквизит ДокументСсылка (тип: ДокументСсылка.ПеремещениеТоваров) и кнопку «Выполнить отладку».
В модуле формы напишем следующий обработчик:
&НаКлиенте
Процедура ВыполнитьОтладку(Команда)
Если Не ЗначениеЗаполнено(Объект.ДокументСсылка) Тогда
ПоказатьПредупреждение(, "Выберите документ для отладки!");
Возврат;
КонецЕсли;
ТабДок = СформироватьПечатнуюФормуНаСервере();
ТабДок.Показать("Отладка печатной формы");
КонецПроцедуры
&НаСервере
Функция СформироватьПечатнуюФормуНаСервере()
// Эмулируем структуры БСП вручную
МассивОбъектов = Новый Массив;
МассивОбъектов.Добавить(Объект.ДокументСсылка);
ОбъектыПечати = Новый СписокЗначений;
// Вызываем экспортный метод формирования печатной формы из модуля объекта
ОбработкаОбъект = РеквизитФормыВЗначение("Объект");
ТабличныйДокумент = ОбработкаОбъект.ПечатьТребованиеНакладнаяНовая(МассивОбъектов, ОбъектыПечати);
Возврат ТабличныйДокумент;
КонецФункции
Благодаря этой схеме мы можем открыть обработку через Файл — Открыть, запустить конфигуратор, поставить точку останова в любом месте модуля объекта и пошагово отследить алгоритм выборки данных и заполнения макета.