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