Как настроить разрыв страниц и разрешить печать непроведенных документов во внешней печатной форме 1С?

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

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

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

Часть 1. Разрешаем печать непроведенных документов в БСП

Если ваша конфигурация построена на базе БСП (а это практически все современные типовые решения: ЗУП, ERP, КА, УТ, БП), при попытке распечатать непроведенный документ система выдаст предупреждение: «Для того чтобы распечатать документ, его нужно предварительно провести». Это поведение заложено на уровне ядра БСП для предотвращения печати недостоверных данных.

Выясним причину этого поведения и рассмотрим два способа обхода данного ограничения.

Способ А. Перевод команды печати на клиентский метод (рекомендуемый)

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

Разберем этот процесс по шагам:

  1. Откройте модуль объекта вашей внешней обработки и найдите функцию СведенияОВнешнейОбработке().
  2. В таблице команд измените тип использования команды печати с "ВызовСерверногоМетода" на "ВызовКлиентскогоМетода":

// Было:
ДобавитьКоманду(ТаблицаКоманд, "Ведомость по премиям", "ВедомостьПоПремиям", "ВызовСерверногоМетода", Истина, "Печать");

// Стало:
ДобавитьКоманду(ТаблицаКоманд, "Ведомость по премиям", "ВедомостьПоПремиям", "ВызовКлиентскогоМетода", Истина, "Печать");
  1. Создайте основную форму внешней обработки (если её ещё нет).
  2. В модуле этой управляемой формы определите клиентскую процедуру Печать(), которая перехватит вызов и принудительно откроет стандартную форму печати БСП, игнорируя проверку проведения:

&НаКлиенте
Процедура Печать(ИдентификаторКоманды, МассивОбъектов) Экспорт
    
    ПараметрыОткрытия = Новый Структура("ИсточникДанных, ПараметрыИсточника");
    ПараметрыОткрытия.ИсточникДанных = ЭтотОбъект.Ссылка;
    
    ПараметрыИсточника = Новый Структура("ИдентификаторКоманды, ОбъектыНазначения");
    ПараметрыИсточника.ИдентификаторКоманды = ИдентификаторКоманды;
    ПараметрыИсточника.ОбъектыНазначения = МассивОбъектов;
    
    ПараметрыОткрытия.ПараметрыИсточника = ПараметрыИсточника;
    
    ОткрытьФорму("ОбщаяФорма.ПечатьДокументов", ПараметрыОткрытия);
    
КонецПроцедуры

Способ Б. Отключение проверки через расширение конфигурации

Если у вас есть возможность вносить изменения в конфигурацию или использовать расширения, вы можете глобально отключить проверку проведения для внешних печатных форм. Для этого в общем модуле ДополнительныеОтчетыИОбработкиКлиентПереопределяемый найдите процедуру ПередВыполнениемКомандыПечатиВнешнейПечатнойФормы и установите параметр СтандартнаяОбработка = Ложь.

Важное архитектурное замечание по запросам: Если вы планируете печатать непроведенные документы, ваши запросы в печатной форме должны обращаться строго к табличным частям документа (например, Документ.Премия.Начисления), а не к регистрам накопления или расчета. У непроведенного документа физически нет движений в регистрах, поэтому запросы к ним вернут пустой результат.

Часть 2. Правильный разрыв страниц с помощью метода ПроверитьВывод()

Перейдем ко второй проблеме. Автор темы пытался реализовать разрыв страниц с помощью метода ПроверитьВывод(), но столкнулся с тем, что код не работал. Проанализируем ситуацию и поймем, почему так произошло.

В исходном коде автора проверка выполнялась следующим образом:


Если Под = Выб.Количество() и Стр = Выборка.Количество() Тогда
    МассивВ = Новый Массив;
    МассивВ.Добавить(ОбластьСтроки);
    МассивВ.Добавить(ОбластьИтоги);
    МассивВ.Добавить(ОбластьПодвал);
    Если не ТабличныйДокумент.ПроверитьВывод(МассивВ) Тогда
        ТабличныйДокумент.ВывестиГоризонтальныйРазделительСтраниц();
    КонецЕсли;
КонецЕсли;

Главная логическая ошибка автора: эта проверка выполнялась только один раз в самом конце — при выводе последней строки последней группировки. К этому моменту все предыдущие 99 строк таблицы уже были успешно выведены в табличный документ. Проверять, поместятся ли они на страницу, постфактум — абсолютно бессмысленно.

Посмотрим на правильный алгоритм работы с методом ПроверитьВывод():

  1. Проверку возможности вывода необходимо делать на каждой итерации цикла, непосредственно перед фактическим выводом очередной строки в результирующий документ.
  2. Для проверки формируется массив областей. В него обязательно добавляется текущая область строки (ОбластьСтроки).
  3. Секретный прием: если выводимая строка является последней в документе или группировке, в этот же массив проверочных областей необходимо временно добавить ОбластьИтоги и ОбластьПодвал. Метод ПроверитьВывод() сымитирует вывод всей этой цепочки объектов. Если они вместе не влезают на текущий лист, функция вернет Ложь. Мы принудительно перенесем саму строку на новую страницу, и подвал гарантированно уйдет вместе с ней, а не останется сиротой на следующем листе.

Разберем корректную программную реализацию этого алгоритма:


// Определяем общее количество строк для контроля последней строки
ВсегоСтрок = Выборка.Количество();
ТекущаяСтрокаНомер = 0;

Пока Выборка.Следующий() Цикл
    ТекущаяСтрокаНомер = ТекущаяСтрокаНомер + 1;
    
    // 1. Заполняем параметры области строки (но пока не выводим ее в документ!)
    ОбластьСтроки.Параметры.Сотрудник = Выборка.Сотрудник;
    ОбластьСтроки.Параметры.Результат = Выборка.Результат;
    // ... заполнение остальных параметров ...
    
    // 2. Создаем массив областей для проверки компоновки на листе
    МассивПроверки = Новый Массив;
    МассивПроверки.Добавить(ОбластьСтроки);
    
    // Если строка последняя — проверяем её в связке с итогами и подвалом
    Если ТекущаяСтрокаНомер = ВсегоСтрок Тогда
        МассивПроверки.Добавить(ОбластьИтоги);
        МассивПроверки.Добавить(ОбластьПодвал);
    КонецЕсли;
    
    // 3. Проверяем, помещается ли пакет областей на текущую страницу
    Если Не ТабличныйДокумент.ПроверитьВывод(МассивПроверки) Тогда
        // Если не помещается — осуществляем разрыв страницы
        ТабличныйДокумент.ВывестиГоризонтальныйРазделительСтраниц();
        
        // При необходимости здесь можно повторно вывести шапку таблицы на новой странице
        // ТабличныйДокумент.Вывести(ОбластьШапка);
    КонецЕсли;
    
    // 4. Теперь со спокойной душой выводим строку на печать
    ТабличныйДокумент.Вывести(ОбластьСтроки);
КонецЦикла;

Важное техническое замечание: Метод ПроверитьВывод() физически обращается к драйверу принтера, установленного в операционной системе по умолчанию. Если на сервере 1С или локальном компьютере пользователя не настроен ни один принтер (или служба печати зависла/отключена), этот метод всегда будет возвращать непредсказуемый результат (чаще всего Ложь) или вызывать аварийное завершение работы программы. Обязательно убедитесь в наличии активного принтера в системе.

Также помните, что свойства масштабирования и полей табличного документа (такие как АвтоМасштаб = Истина, ОриентацияСтраницы, значения полей сверху/снизу) должны быть установлены до вызова метода ПроверитьВывод(), иначе расчет высоты страниц будет неточным.

Часть 3. Критика исходного кода и исправление грубых ошибок (Code Review)

Помимо озвученных автором проблем, в предоставленном исходном коде содержится несколько критических архитектурных ошибок, которые могут привести к падению системы при работе реальных пользователей или вызвать колоссальные задержки производительности (база данных будет «зависать»). Проанализируем эти ошибки.

Ошибка 1. Переменная не определена (Runtime Error)

В строке установщика параметров запроса написан следующий код:

Запрос.УстановитьПараметр("ДатаДокумента", СсылкаНаДокументПремия.Дата);

При этом в объявлении функции переменная называется СсылкаНаДокумент. Переменной с именем СсылкаНаДокументПремия в контексте функции не существует. При попытке запустить печать программа немедленно упадет с ошибкой выполнения. Исправим это обращение:

Запрос.УстановитьПараметр("ДатаДокумента", СсылкаНаДокумент.Дата); Ошибка 2. Запрос внутри цикла (Критическое падение производительности!)

Внутри цикла по выборке сотрудников автор создает и выполняет еще один запрос к базе данных:


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

Это классический антипаттерн разработки в 1С. Если в документе будет 100 сотрудников, система выполнит 101 отдельный запрос к СУБД. На реальной базе данных с сотнями сотрудников формирование такой печатной формы займет несколько минут и заблокирует работу других пользователей.

Решение: Весь пакет данных нужно получить одним-единственным запросом на самом старте процедуры, соединив таблицы начислений и показателей по ключу связи (номеру строки или идентификатору строки вида расчета).

Ошибка 3. НайтиПоНаименованию() и НайтиПоКоду() в цикле и коде

Конструкции поиска элементов справочников по наименованию (например, Справочники.ПоказателиРасчетаЗарплаты.НайтиПоНаименованию(...)) или по коду (НайтиПоКоду("000000104")) генерируют неявные и крайне неоптимальные запросы к базе данных.

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

Часть 4. Полностью оптимизированный и исправленный код печатной формы

Объединим все исправления в единый красивый, быстрый и отказоустойчивый алгоритм. Мы полностью избавились от запросов в цикле, оптимизировали связи таблиц и внедрили корректную проверку вывода страниц.


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

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

← На главную