При разработке внешних печатных форм (ВПФ) программисты 1С регулярно сталкиваются с двумя неприятными ограничениями. Во-первых, Библиотека стандартных подсистем (БСП) по умолчанию блокирует печать документов, которые еще не были проведены. Во-вторых, встроенный механизм переноса строк на новую страницу часто работает некорректно: подписи, итоговые суммы или даже последние строки табличной части «отрываются» от основного контента и переносятся на новый лист в одиночестве.
В этой статье мы подробно разберем, как победить обе эти проблемы, проанализируем ошибки в исходном коде автора и выполним глубокий рефакторинг для оптимизации производительности печатной формы.
Если ваша конфигурация построена на базе БСП (а это практически все современные типовые решения: ЗУП, ERP, КА, УТ, БП), при попытке распечатать непроведенный документ система выдаст предупреждение: «Для того чтобы распечатать документ, его нужно предварительно провести». Это поведение заложено на уровне ядра БСП для предотвращения печати недостоверных данных.
Выясним причину этого поведения и рассмотрим два способа обхода данного ограничения.
Способ А. Перевод команды печати на клиентский метод (рекомендуемый)
Самый надежный способ обойти серверную блокировку БСП — изменить тип вызова команды во внешней печатной форме с серверного на клиентский. Это позволит нам самостоятельно управлять процессом открытия формы печати.
Разберем этот процесс по шагам:
СведенияОВнешнейОбработке()."ВызовСерверногоМетода" на "ВызовКлиентскогоМетода":
// Было:
ДобавитьКоманду(ТаблицаКоманд, "Ведомость по премиям", "ВедомостьПоПремиям", "ВызовСерверногоМетода", Истина, "Печать");
// Стало:
ДобавитьКоманду(ТаблицаКоманд, "Ведомость по премиям", "ВедомостьПоПремиям", "ВызовКлиентскогоМетода", Истина, "Печать");
Печать(), которая перехватит вызов и принудительно откроет стандартную форму печати БСП, игнорируя проверку проведения:
&НаКлиенте
Процедура Печать(ИдентификаторКоманды, МассивОбъектов) Экспорт
ПараметрыОткрытия = Новый Структура("ИсточникДанных, ПараметрыИсточника");
ПараметрыОткрытия.ИсточникДанных = ЭтотОбъект.Ссылка;
ПараметрыИсточника = Новый Структура("ИдентификаторКоманды, ОбъектыНазначения");
ПараметрыИсточника.ИдентификаторКоманды = ИдентификаторКоманды;
ПараметрыИсточника.ОбъектыНазначения = МассивОбъектов;
ПараметрыОткрытия.ПараметрыИсточника = ПараметрыИсточника;
ОткрытьФорму("ОбщаяФорма.ПечатьДокументов", ПараметрыОткрытия);
КонецПроцедуры
Способ Б. Отключение проверки через расширение конфигурации
Если у вас есть возможность вносить изменения в конфигурацию или использовать расширения, вы можете глобально отключить проверку проведения для внешних печатных форм. Для этого в общем модуле ДополнительныеОтчетыИОбработкиКлиентПереопределяемый найдите процедуру ПередВыполнениемКомандыПечатиВнешнейПечатнойФормы и установите параметр СтандартнаяОбработка = Ложь.
Важное архитектурное замечание по запросам: Если вы планируете печатать непроведенные документы, ваши запросы в печатной форме должны обращаться строго к табличным частям документа (например, Документ.Премия.Начисления), а не к регистрам накопления или расчета. У непроведенного документа физически нет движений в регистрах, поэтому запросы к ним вернут пустой результат.
Перейдем ко второй проблеме. Автор темы пытался реализовать разрыв страниц с помощью метода ПроверитьВывод(), но столкнулся с тем, что код не работал. Проанализируем ситуацию и поймем, почему так произошло.
В исходном коде автора проверка выполнялась следующим образом:
Если Под = Выб.Количество() и Стр = Выборка.Количество() Тогда
МассивВ = Новый Массив;
МассивВ.Добавить(ОбластьСтроки);
МассивВ.Добавить(ОбластьИтоги);
МассивВ.Добавить(ОбластьПодвал);
Если не ТабличныйДокумент.ПроверитьВывод(МассивВ) Тогда
ТабличныйДокумент.ВывестиГоризонтальныйРазделительСтраниц();
КонецЕсли;
КонецЕсли;
Главная логическая ошибка автора: эта проверка выполнялась только один раз в самом конце — при выводе последней строки последней группировки. К этому моменту все предыдущие 99 строк таблицы уже были успешно выведены в табличный документ. Проверять, поместятся ли они на страницу, постфактум — абсолютно бессмысленно.
Посмотрим на правильный алгоритм работы с методом ПроверитьВывод():
ОбластьСтроки).ОбластьИтоги и ОбластьПодвал. Метод ПроверитьВывод() сымитирует вывод всей этой цепочки объектов. Если они вместе не влезают на текущий лист, функция вернет Ложь. Мы принудительно перенесем саму строку на новую страницу, и подвал гарантированно уйдет вместе с ней, а не останется сиротой на следующем листе.Разберем корректную программную реализацию этого алгоритма:
// Определяем общее количество строк для контроля последней строки
ВсегоСтрок = Выборка.Количество();
ТекущаяСтрокаНомер = 0;
Пока Выборка.Следующий() Цикл
ТекущаяСтрокаНомер = ТекущаяСтрокаНомер + 1;
// 1. Заполняем параметры области строки (но пока не выводим ее в документ!)
ОбластьСтроки.Параметры.Сотрудник = Выборка.Сотрудник;
ОбластьСтроки.Параметры.Результат = Выборка.Результат;
// ... заполнение остальных параметров ...
// 2. Создаем массив областей для проверки компоновки на листе
МассивПроверки = Новый Массив;
МассивПроверки.Добавить(ОбластьСтроки);
// Если строка последняя — проверяем её в связке с итогами и подвалом
Если ТекущаяСтрокаНомер = ВсегоСтрок Тогда
МассивПроверки.Добавить(ОбластьИтоги);
МассивПроверки.Добавить(ОбластьПодвал);
КонецЕсли;
// 3. Проверяем, помещается ли пакет областей на текущую страницу
Если Не ТабличныйДокумент.ПроверитьВывод(МассивПроверки) Тогда
// Если не помещается — осуществляем разрыв страницы
ТабличныйДокумент.ВывестиГоризонтальныйРазделительСтраниц();
// При необходимости здесь можно повторно вывести шапку таблицы на новой странице
// ТабличныйДокумент.Вывести(ОбластьШапка);
КонецЕсли;
// 4. Теперь со спокойной душой выводим строку на печать
ТабличныйДокумент.Вывести(ОбластьСтроки);
КонецЦикла;
Важное техническое замечание: Метод ПроверитьВывод() физически обращается к драйверу принтера, установленного в операционной системе по умолчанию. Если на сервере 1С или локальном компьютере пользователя не настроен ни один принтер (или служба печати зависла/отключена), этот метод всегда будет возвращать непредсказуемый результат (чаще всего Ложь) или вызывать аварийное завершение работы программы. Обязательно убедитесь в наличии активного принтера в системе.
Также помните, что свойства масштабирования и полей табличного документа (такие как АвтоМасштаб = Истина, ОриентацияСтраницы, значения полей сверху/снизу) должны быть установлены до вызова метода ПроверитьВывод(), иначе расчет высоты страниц будет неточным.
Помимо озвученных автором проблем, в предоставленном исходном коде содержится несколько критических архитектурных ошибок, которые могут привести к падению системы при работе реальных пользователей или вызвать колоссальные задержки производительности (база данных будет «зависать»). Проанализируем эти ошибки.
Ошибка 1. Переменная не определена (Runtime Error)
В строке установщика параметров запроса написан следующий код:
Запрос.УстановитьПараметр("ДатаДокумента", СсылкаНаДокументПремия.Дата);
При этом в объявлении функции переменная называется СсылкаНаДокумент. Переменной с именем СсылкаНаДокументПремия в контексте функции не существует. При попытке запустить печать программа немедленно упадет с ошибкой выполнения. Исправим это обращение:
Запрос.УстановитьПараметр("ДатаДокумента", СсылкаНаДокумент.Дата);
Ошибка 2. Запрос внутри цикла (Критическое падение производительности!)
Внутри цикла по выборке сотрудников автор создает и выполняет еще один запрос к базе данных:
Пока Выб.Следующий() Цикл
// ...
Пока Выборка.Следующий() Цикл
// ...
ДопЗапрос = Новый Запрос;
ДопЗапрос.Текст = "ВЫБРАТЬ ... ИЗ Документ.Премия.Показатели ...";
ДопРезультат = ДопЗапрос.Выполнить();
// ...
КонецЦикла;
КонецЦикла;
Это классический антипаттерн разработки в 1С. Если в документе будет 100 сотрудников, система выполнит 101 отдельный запрос к СУБД. На реальной базе данных с сотнями сотрудников формирование такой печатной формы займет несколько минут и заблокирует работу других пользователей.
Решение: Весь пакет данных нужно получить одним-единственным запросом на самом старте процедуры, соединив таблицы начислений и показателей по ключу связи (номеру строки или идентификатору строки вида расчета).
Ошибка 3. НайтиПоНаименованию() и НайтиПоКоду() в цикле и коде
Конструкции поиска элементов справочников по наименованию (например, Справочники.ПоказателиРасчетаЗарплаты.НайтиПоНаименованию(...)) или по коду (НайтиПоКоду("000000104")) генерируют неявные и крайне неоптимальные запросы к базе данных.
Кроме того, жесткое зашивание кодов («хардкод») приводит к тому, что при переносе печатной формы в другую базу данных или при банальном изменении кодов подразделений бухгалтером печатная форма мгновенно сломается. Списки подразделений нужно передавать через настройки или параметры, а ссылки на показатели — получать один раз перед началом циклов.
Объединим все исправления в единый красивый, быстрый и отказоустойчивый алгоритм. Мы полностью избавились от запросов в цикле, оптимизировали связи таблиц и внедрили корректную проверку вывода страниц.
Функция СформироватьПечатнуюФорму(СсылкаНаДокумент, ОбъектыПечати) Экспорт
ТабличныйДокумент = Новый ТабличныйДокумент;
// Настройки страницы задаем ДО вызова методов проверки вывода
ТабличныйДокумент.АвтоМасштаб = Истина;
ТабличныйДокумент.ПолеСверху = 10;
ТабличныйДокумент.ПолеСнизу = 10;
ТабличныйДокумент.ПолеСлева = 5;
ТабличныйДокумент.ПолеСправа = 5;
ТабличныйДокумент.ОриентацияСтраницы = ОриентацияСтраницы.Ландшафт;
ТабличныйДокумент.ИмяПараметровПечати = "ПАРАМЕТРЫ_ПЕЧАТИ_ВедомостьНаПремии";
МакетОбработки = ПолучитьМакет("ВедомостьПремия");
// Избавляемся от хардкода подразделений: собираем их коды в массив
МассивКодов = Новый Массив;
МассивКодов.Добавить("000000104"); МассивКодов.Добавить("000000205");
МассивКодов.Добавить("000000106"); МассивКодов.Добавить("000000111");
// ... остальные коды добавляются аналогично
// Получаем ссылки на показатели один раз до выполнения запроса
ПоказательБаза = Справочники.ПоказателиРасчетаЗарплаты.РасчетнаяБаза;
ПоказательПрогрессивКА = Справочники.ПоказателиРасчетаЗарплаты.НайтиПоНаименованию("Процент прогрессивки");
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ПремияНачисления.Ссылка.ВидПремии КАК ВидПремии,
| ПремияНачисления.Сотрудник КАК Сотрудник,
| КадроваяИсторияСотрудниковСрезПоследних.ДолжностьПоШтатномуРасписанию КАК ДолжностьПоШтатномуРасписанию,
| ПремияНачисления.Результат КАК СуммаПремии,
| ПремияНачисления.Подразделение КАК Подразделение,
| ПремияНачисления.ИдентификаторСтрокиВидаРасчета КАК ИдНомер,
| ПремияПоказатели.Показатель КАК Показатель,
| ПремияПоказатели.Значение КАК ЗначениеПоказателя
|ИЗ
| Документ.Preмия.Начисления КАК ПремияНачисления
| // Соединение с показателями прямо в основном запросе (избавляемся от запроса в цикле!)
| ЛЕВОЕ СОЕДИНЕНИЕ Документ.Премия.Показатели КАК ПремияПоказатели
| ПО ПремияНачисления.Ссылка = ПремияПоказатели.Ссылка
| И ПремияНачисления.ИдентификаторСтрокиВидаРасчета = ПремияПоказатели.ИдентификаторСтрокиВидаРасчета
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КадроваяИсторияСотрудников.СрезПоследних(&ДатаДокумента, ) КАК КадроваяИсторияСотрудниковСрезПоследних
| ПО ПремияНачисления.Сотрудник = КадроваяИсторияСотрудниковСрезПоследних.Сотрудник
|ГДЕ
| ПремияНачисления.Ссылка = &ТекДокумент
|ИТОГИ ПО
| Подразделение";
Запрос.УстановитьПараметр("ДатаДокумента", СсылкаНаДокумент.Дата); // Исправлена ошибка имени переменной
Запрос.УстановитьПараметр("ТекДокумент", СсылкаНаДокумент);
РезультатЗапроса = Запрос.Выполнить();
ВыборкаПодразделений = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
ОбластьЗаголовок = МакетОбработки.ПолучитьОбласть("Заголовок");
ОбластьШапка = МакетОбработки.ПолучитьОбласть("Шапка");
ОбластьСтрока = МакетОбработки.ПолучитьОбласть("Строка");
ОбластьИтоги = МакетОбработки.ПолучитьОбласть("Итоги");
ОбластьПодвал = МакетОбработки.ПолучитьОбласть("Подвал");
// Выводим заголовок ведомости
ОбластьЗаголовок.Параметры.ПериодРегистрации = Формат(СсылкаНаДокумент.ПериодРегистрации,"ДФ='MMMM yyyy'");
ТабличныйДокумент.Вывести(ОбластьЗаголовок);
ТабличныйДокумент.Вывести(ОбластьШапка);
Пока ВыборкаПодразделений.Следующий() Цикл
// Выводим группировку подразделения
ОбластьПодразделение = МакетОбработки.ПолучитьОбласть("ПодразделениеНазвание");
ОбластьПодразделение.Параметры.Подразделение = ВыборкаПодразделений.Подразделение;
ТабличныйДокумент.Вывести(ОбластьПодразделение);
ВыборкаДетали = ВыборкаПодразделений.Выбрать();
ВсегоСтрокДетали = ВыборкаДетали.Количество();
ТекущийНомерСтроки = 0;
ИтогоСумма = 0;
ИтогоБаза = 0;
Пока ВыборкаДетали.Следующий() Цикл
ТекущийНомерСтроки = ТекущийНомерСтроки + 1;
// Заполняем параметры строки
ОбластьСтрока.Параметры.Сотрудник = ВыборкаДетали.Сотрудник;
ОбластьСтрока.Параметры.Должность = ВыборкаДетали.ДолжностьПоШтатномуРасписанию;
ОбластьСтрока.Параметры.СуммаПремии = ВыборкаДетали.СуммаПремии;
// Распределяем значения показателей, полученные в одном пакете
Если ВыборкаДетали.Показатель = ПоказательБаза Тогда
ОбластьСтрока.Параметры.РасчетнаяБаза = ВыборкаДетали.ЗначениеПоказателя;
ИтогоБаза = ИтогоБаза + ВыборкаДетали.ЗначениеПоказателя;
ИначеЕсли ВыборкаДетали.Показатель = ПоказательПрогрессивКА Тогда
ОбластьСтрока.Параметры.ПроцентПремии = ВыборкаДетали.ЗначениеПоказателя;
КонецЕсли;
ИтогоСумма = ИтогоСумма + ВыборкаДетали.СуммаПремии;
// ---- АЛГОРИТМ ПРОВЕРКИ ВЫВОДА ----
МассивПроверки = Новый Массив;
МассивПроверки.Добавить(ОбластьСтрока);
// Если строка последняя во всей таблице, проверяем её с подвалами
Если ТекущийНомерСтроки = ВсегоСтрокДетали Тогда
МассивПроверки.Добавить(ОбластьИтоги);
МассивПроверки.Добавить(ОбластьПодвал);
КонецЕсли;
Если Не ТабличныйДокумент.ПроверитьВывод(МассивПроверки) Тогда
// Не влезает — делаем мягкий перенос строки на следующий лист
ТабличныйДокумент.ВывестиГоризонтальныйРазделительСтраниц();
КонецЕсли;
ТабличныйДокумент.Вывести(ОбластьСтрока);
КонецЦикла;
// Выводим итоги по подразделению
ОбластьИтоги.Параметры.ИтогоСуммаПремии = ИтогоСумма;
ОбластьИтоги.Параметры.ИтогоРасчетнаяБаза = ИтогоБаза;
ТабличныйДокумент.Вывести(ОбластьИтоги);
КонецЦикла;
// Выводим финальный подвал с подписями
ТабличныйДокумент.Вывести(ОбластьПодвал);
Возврат ТабличныйДокумент;
КонецФункции
Используя данный подход, вы гарантируете высокую скорость формирования отчетов даже на базах с миллионами записей, обеспечиваете красивый и профессиональный перенос строк без оторванных подписей и предоставляете пользователям удобную возможность печатать черновики документов без предварительного проведения.