Как настроить автоматический перенос страниц при печати документов с плавающей высотой строк в 1С?

Программист 1С v8.3 (Обычные формы) Управленческий учет Промышленность, строительство и АПК
← На главную

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

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

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

Разбираемся в теории: как работает метод ПроверитьВывод()

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

Этот метод принимает в качестве параметра массив областей макета, которые мы планируем вывести следующими, и проверяет, поместятся ли они на текущей странице с учетом уже выведенного содержимого и установленных параметров печати (полей, ориентации, масштаба). Метод возвращает булево значение:

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


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

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

Критический фактор: параметры страницы и масштаб

Рассмотрим еще один важный технический нюанс. Метод ПроверитьВывод() производит расчет высоты строк виртуально. Для этого он обращается к текущим настройкам печатного листа результирующего табличного документа.

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

Золотое правило программиста 1С: всегда задавайте параметры печати целевого табличного документа ДО начала вывода первой области и до проведения любых проверок!

Пошаговый алгоритм реализации динамического переноса

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

  1. Инициализация параметров печати: Задаем настройки страницы (поля, ориентацию, масштаб) для результирующего табличного документа.
  2. Использование промежуточного табличного документа: Чтобы проверить вывод всей таблицы целиком вместе с шапкой, итогом и подвалом, нам нужно сначала собрать строки табличной части в отдельный временный объект ТабличныйДокумент. Если мы будем проверять каждую строку таблицы по отдельности, то не сможем гарантировать, что вся группа («Шапка» + «Таблица» + «Итоги» + «Подвал») красиво скомпонуется на одном листе.
  3. Формирование массива областей: В массив добавляются: область шапки текущего путевого листа, собранный промежуточный табличный документ с таблицей выработки, область итогов и область подвала с подписями.
  4. Проверка вывода: Вызываем метод ПроверитьВывод() для подготовленного массива. Если возвращается значение Ложь, принудительно добавляем в результирующий документ разделитель страниц с помощью метода ВывестиГоризонтальныйРазделительСтраниц().
  5. Окончательный вывод данных: Последовательно выводим все области в результирующий табличный документ.

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

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

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

Готовый оптимизированный шаблон решения

Давайте посмотрим на пример готового, чистого и быстрого кода, в котором учтены все вышеперечисленные правила и оптимизации:


Процедура СформироватьОтчет(ДокументРезультат, ОчищатьТабличныйДокумент = Истина) Экспорт

    // 1. Проверка заполнения параметров периода
    Если НЕ ЗначениеЗаполнено(ДатаНач) Или НЕ ЗначениеЗаполнено(ДатаКон) Тогда
        Сообщить("Не указаны даты начала или окончания периода!", СтатусСообщения.Важное);
        Возврат;
    КонецЕсли;

    // 2. Подготовка результирующего документа и настройка параметров печати
    Если ОчищатьТабличныйДокумент Тогда
        ДокументРезультат.Очистить();
    КонецЕсли;
    
    // КРИТИЧЕСКИ ВАЖНО: Задаем параметры печати ДО первой проверки ПроверитьВывод()
    ДокументРезультат.Автомасштаб = Истина;
    ДокументРезультат.ОриентацияСтраницы = ОриентацияСтраницы.Портрет;
    ДокументРезультат.ПолеСверху = 10;
    ДокументРезультат.ПолеСнизу = 10;
    ДокументРезультат.ПолеСлева = 10;
    ДокументРезультат.ПолеСправа = 10;

    // 3. Получение макета и областей
    Макет = ПолучитьМакет("Макет");
    ОбластьШапка  = Макет.ПолучитьОбласть("Шапка");
    ОбластьТаблица = Макет.ПолучитьОбласть("Таблица");
    ОбластьИтого  = Макет.ПолучитьОбласть("Итого");
    ОбластьПодвал  = Макет.ПолучитьОбласть("Подвал");

    // 4. Оптимизированный единый запрос без запросов в цикле
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |   ПутевойТракторный.Ссылка КАК Ссылка,
        |   ПутевойТракторный.Дата КАК Дата,
        |   ПутевойТракторный.ФизЛицо КАК ФизЛицо,
        |   ПутевойТракторный.ФизЛицо.Код КАК КодСотрудника,
        |   ПутевойТракторный.Трактор КАК Трактор,
        |   ПутевойТракторный.Трактор.Код КАК КодТрактора,
        |   ПутевойТракторный.Ответственный КАК Ответственный,
        |   ПутевойТракторный.НомерБланка КАК НомерБланка,
        |   Выработка.НомерСтроки КАК НомерСтроки,
        |   Выработка.СпособОтражения КАК СпособОтражения,
        |   Выработка.ТехнологическаяОперация КАК ТехнологическаяОперация,
        |   Выработка.Количество КАК Количество,
        |   Выработка.Дни КАК Дни,
        |   Выработка.Часы КАК Часы,
        |   Выработка.ВидРасчета.Код КАК ВидРасчетаКод,
        |   Выработка.Расценка КАК Расценка,
        |   Выработка.НормаВыработки КАК НормаВыработки,
        |   Выработка.НормаРасхода КАК НормаРасхода,
        |   Выработка.Сумма КАК Сумма,
        |   Выработка.Расход КАК Расход,
        |   Выработка.РаботаДвигателя КАК РаботаДвигателя,
        |   Выработка.РасходФакт КАК РасходФакт
        |ИЗ
        |   Документ.ПутевойТракторный КАК ПутевойТракторный
        |   ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПутевойТракторный.Выработка КАК Выработка
        |       ПО Выработка.Ссылка = ПутевойТракторный.Ссылка
        |ГДЕ
        |   ПутевойТракторный.ФизЛицо = &Работник
        |   И ПутевойТракторный.Дата МЕЖТУ &ДатаНач И &ДатаКон
        |УПОРЯДОЧИТЬ ПО
        |   Дата,
        |   Ссылка,
        |   НомерСтроки
        |ИТОГИ ПО
        |   Ссылка";

    Запрос.УстановитьПараметр("Работник", Работник);
    Запрос.УстановитьПараметр("ДатаНач",  ДатаНач);
    Запрос.УстановитьПараметр("ДатаКон",  ДатаКон);

    РезультатЗапроса = Запрос.Выполнить();
    
    // Обход по группировкам (по документам Ссылка)
    ВыборкаДокументов = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
    
    Пока ВыборкаДокументов.Следующий() Цикл
        
        // Заполняем параметры Шапки
        ОбластьШапка.Параметры.НомерЛиста = ВыборкаДокументов.НомерБланка;
        ОбластьШапка.Параметры.Авто       = ВыборкаДокументов.Трактор;
        ОбластьШапка.Параметры.КодА       = ВыборкаДокументов.КодТрактора;
        ОбластьШапка.Параметры.День       = День(ВыборкаДокументов.Дата);
        
        СтрокаДата = Формат(ВыборкаДокументов.Дата, "ДФ=ММММ");
        ОтчетныйМесяцСклонение(СтрокаДата);
        ОбластьШапка.Параметры.Месяц      = НРег(СтрокаДата);
        ОбластьШапка.Параметры.Год        = Год(ВыборкаДокументов.Дата);
        ОбластьШапка.Параметры.Сотрудник  = Строка(ВыборкаДокументов.КодСотрудника) + " " + Строка(ВыборкаДокументов.ФизЛицо);
        
        // Создаем временный табличный документ для строк ТЧ
        ТабДокТаблица = Новый ТабличныйДокумент;
        
        // Инициализируем переменные для подсчета итогов на уровне кода
        ИтогоКол     = 0;
        ИтогоДней    = 0;
        ИтогоЧасов   = 0;
        ИтогоСумма   = 0;
        ИтогоРасНорм = 0;
        ИтогоРасФакт = 0;
        
        // Обходим детальные записи (строки табличной части)
        ВыборкаСтрок = ВыборкаДокументов.Выбрать();
        Пока ВыборкаСтрок.Следующий() Цикл
            
            // Если документ пустой (нет строк выработки), пропускаем заполнение ТЧ
            Если НЕ ЗначениеЗаполнено(ВыборкаСтрок.НомерСтроки) Тогда
                Продолжить;
            КонецЕсли;
            
            ОбластьТаблица.Параметры.НомерПП         = ВыборкаСтрок.НомерСтроки;
            ОбластьТаблица.Параметры.СпособОтражения = ВыборкаСтрок.СпособОтражения;
            ОбластьТаблица.Параметры.ТехОперация     = ВыборкаСтрок.ТехнологическаяОперация;
            ОбластьТаблица.Параметры.Кол             = ВыборкаСтрок.Количество;
            ОбластьТаблица.Параметры.ОтрДней         = ВыборкаСтрок.Дни;
            ОбластьТаблица.Параметры.ОтрЧасы         = ВыборкаСтрок.Часы;
            ОбластьТаблица.Параметры.ВидРасчета      = ВыборкаСтрок.ВидРасчетаКод;
            ОбластьТаблица.Параметры.Расценка        = ВыборкаСтрок.Расценка;
            ОбластьТаблица.Параметры.Сумма           = ВыборкаСтрок.Сумма;
            ОбластьТаблица.Параметры.РабДвиг         = ВыборкаСтрок.РаботаДвигателя;
            ОбластьТаблица.Параметры.НормВыраб       = ВыборкаСтрок.НормаВыработки;
            ОбластьТаблица.Параметры.НормРасх        = ВыборкаСтрок.НормаРасхода;
            ОбластьТаблица.Параметры.РасхНорм        = ВыборкаСтрок.Расход;
            ОбластьТаблица.Параметры.РасхФакт        = ВыборкаСтрок.РасходФакт;
            
            // Накапливаем итоги
            ИтогоКол     = ИтогоКол     + ВыборкаСтрок.Количество;
            ИтогоДней    = ИтогоДней    + ВыборкаСтрок.Дни;
            ИтогоЧасов   = ИтогоЧасов   + ВыборкаСтрок.Часы;
            ИтогоСумма   = ИтогоСумма   + ВыборкаСтрок.Сумма;
            ИтогоРасНорм = ИтогоРасНорм + ВыборкаСтрок.Расход;
            ИтогоРасФакт = ИтогоРасФакт + ВыборкаСтрок.РасходФакт;
            
            ТабДокТаблица.Вывести(ОбластьТаблица);
            
        КонецЦикла;
        
        // Заполняем область итогов
        ОбластьИтого.Параметры.ИтогоКол     = ИтогоКол;
        ОбластьИтого.Параметры.ИтогоДней    = ИтогоДней;
        ОбластьИтого.Параметры.ИтогоЧасов   = ИтогоЧасов;
        ОбластьИтого.Параметры.ИтогоСумма   = ИтогоСумма;
        ОбластьИтого.Параметры.ИтогоРасНорм = ИтогоРасНорм;
        ОбластьИтого.Параметры.ИтогоРасФакт = ИтогоРасФакт;
        
        // Заполняем область подвала
        ОбластьПодвал.Параметры.Ответственный = ВыборкаДокументов.Ответственный;
        
        // Формируем массив областей для проверки вывода группы
        МассивОбластей = Новый Массив;
        МассивОбластей.Добавить(ОбластьШапка);
        МассивОбластей.Добавить(ТабДокТаблица);
        МассивОбластей.Добавить(ОбластьИтого);
        МассивОбластей.Добавить(ОбластьПодвал);
        
        // Проверяем, помещается ли весь текущий расчетный лист на страницу.
        // Если НЕ помещается, принудительно осуществляем переход на следующую страницу.
        Если НЕ ДокументРезультат.ПроверитьВывод(МассивОбластей) Тогда
            ДокументРезультат.ВывестиГоризонтальныйРазделительСтраниц();
        КонецЕсли;
        
        // Выводим скомпонованные области в итоговый табличный документ
        ДокументРезультат.Вывести(ОбластьШапка);
        ДокументРезультат.Вывести(ТабДокТаблица);
        ДокументРезультат.Вывести(ОбластьИтого);
        ДокументРезультат.Вывести(ОбластьПодвал);
        
    КонецЦикла;

КонецПроцедуры

Важные системные проблемы и способы их решения

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

Проблема 1. Ошибки принтера при выполнении кода на сервере 1С

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


Попытка
    ПоместитсяНаСтранице = ДокументРезультат.ПроверитьВывод(МассивОбластей);
Исключение
    // В случае сбоя драйвера принтера считаем, что все помещается, чтобы не прерывать печать
    ПоместитсяНаСтранице = Истина;
КонецПопытки;

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

Проблема 2. Оптимизация с использованием Библиотеки стандартных подсистем (БСП)

Если вы ведете разработку в современной конфигурации, созданной на базе БСП (например, 1С:Бухгалтерия, 1С:Зарплата и Управление Персоналом, 1С:ERP Управление предприятием), вам не нужно вручную прописывать сложные конструкции безопасности для работы с принтером на сервере.

Используйте готовый метод ОбщегоНазначения.ПроверитьВыводТабличногоДокумента(). Он автоматически инкапсулирует в себе обработку исключений и корректно рассчитывает высоту страниц даже в самых сложных серверных сценариях.

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

← На главную