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

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

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

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

Почему вторая таблица оказывается внизу: типовые ошибки

Проанализируем механику работы объекта ТабличныйДокумент:

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

Способ 1. Использование промежуточного соответствия (оптимально для разных алгоритмов)

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

Пошагово реализуем этот алгоритм:

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

Посмотрим на пример реализации расчетной функции:


Функция ПолучитьДанныеВторойКолонки(ДатаОтчета) Экспорт

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

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

    Возврат ДанныеПоПодразделениям;

КонецФункции

Теперь в главной процедуре формирования отчета связываем данные при выводе строк:


// Получаем предварительно рассчитанные данные
ДанныеВторойКолонки = ПолучитьДанныеВторойКолонки(ДатаОтчета);

ВыборкаП = Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Подразделение");
Пока ВыборкаП.Следующий() Цикл

    ОбластьСтрока.Параметры.п1 = "Итого по " + ВыборкаП.Подразделение;
    
    // Получаем значение из соответствия по текущему подразделению
    ЗначениеКолонки = ДанныеВторойКолонки[ВыборкаП.Подразделение];
    ОбластьСтрока.Параметры.п2 = ?(ЗначениеКолонки = Неопределено, 0, ЗначениеКолонки);
    
    ОбластьСтрока.Параметры.п3 = ВыборкаП.КоличествоПлан;
    ОбластьСтрока.Параметры.п4 = ВыборкаП.Количество;
    // Заполнение остальных параметров строки...
    
    ТабДок.Вывести(ОбластьСтрока);

КонецЦикла;

Способ 2. Построчный вывод с методом Присоединить()

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

  1. Левая часть: колонки первого отчета, идущие до вставляемого столбца.
  2. Центральная часть: колонка (или группа колонок) из второго макета.
  3. Правая часть («хвост»): оставшиеся колонки основного отчета.

Разберем цикл вывода в коде:


ОбластьНачало = Макет.ПолучитьОбласть("Строка|ЛеваяКолонка");
ОбластьВставка = Макет.ПолучитьОбласть("Строка|ВставляемаяКолонка");
ОбластьХвост  = Макет.ПолучитьОбласть("Строка|ПравыеКолонки");

Пока Выборка.Следующий() Цикл

    // Заполняем параметры левой секции
    ОбластьНачало.Параметры.Номенклатура = Выборка.Номенклатура;
    ТабДок.Вывести(ОбластьНачало); // Начинает новую строку документа
    
    // Заполняем и пристыковываем среднюю секцию в ту же строку
    ОбластьВставка.Параметры.КоличествоНаДату = ВычислитьЗначение(Выборка.Подразделение);
    ТабДок.Присоединить(ОбластьВставка);
    
    // Заполняем и пристыковываем правые секции
    ОбластьХвост.Параметры.КоличествоПлан = Выборка.КоличествоПлан;
    ТабДок.Присоединить(ОбластьХвост);

КонецЦикла;

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

Способ 3. Объединение запросов через Менеджер временных таблиц

Если нет необходимости выполнять сложные формульные расчеты на встроенном языке, наиболее надежным и быстрым решением является объединение выборок непосредственно на уровне СУБД. Даже если тексты запросов очень объемные, их не обязательно переписывать целиком — достаточно связать их через МенеджерВременныхТаблиц.

Разберем схему реализации:

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

МВТ = Новый МенеджерВременныхТаблиц;

// Запрос 1 помещает расчеты плана и факта в ВТ_ОсновныеДанные
Запрос1 = Новый Запрос;
Запрос1.МенеджерВременныхТаблиц = МВТ;
Запрос1.Текст = ТекстЗапроса1; // Заканчивается на: ПОМЕСТИТЬ ВТ_ОсновныеДанные
Запрос1.Выполнить();

// Запрос 2 помещает расчеты остатков в ВТ_ДополнительныеДанные
Запрос2 = Новый Запрос;
Запрос2.МенеджерВременныхТаблиц = МВТ;
Запрос2.Текст = ТекстЗапроса2; // Заканчивается на: ПОМЕСТИТЬ ВТ_ДополнительныеДанные
Запрос2.Выполнить();

// Итоговый запрос соединяет данные
ИтоговыйЗапрос = Новый Запрос;
ИтоговыйЗапрос.МенеджерВременныхТаблиц = МВТ;
ИтоговыйЗапрос.Текст = 
    "ВЫБРАТЬ
    |   Основа.Подразделение КАК Подразделение,
    |   Основа.Номенклатура КАК Номенклатура,
    |   Основа.КоличествоПлан КАК КоличествоПлан,
    |   ЕСТЬNULL(Доп.КоличествоНаДату, 0) КАК КоличествоНаДату
    |ИЗ
    |   ВТ_ОсновныеДанные КАК Основа
    |   ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ДополнительныеДанные КАК Доп
    |   ПО Основа.Подразделение = Доп.Подразделение";

Результат = ИтоговыйЗапрос.Выполнить();

На выходе получается единая плоская выборка, которая выводится в стандартный макет в один проход без разрывов строк и смещений.

Способ 4. Прямая адресация ячеек в сформированном документе

Если структура первого отчета слишком сложная (содержит многоуровневые группировки с нестандартными объединениями ячеек), можно применить заполнение по координатам в уже сформированном табличном документе:

  1. При выводе строк первого отчета запоминаем номера строк таблицы для каждого подразделения в структуре или соответствии: КартаСтрок[ВыборкаП.Подразделение] = ТабДок.ВысотаТаблицы;.
  2. После завершения основного цикла вывода рассчитываем второй набор данных.
  3. Зная точный номер колонки для вставки, обращаемся к конкретным ячейкам сформированного документа:

// Записываем значение в конкретную ячейку по номеру строки и столбца
НомерКолонкиВставки = 2;
Для Каждого КлючИЗначение Из КартаСтрок Цикл
    Подразделение = КлючИЗначение.Ключ;
    НомерСтроки   = КлючИЗначение.Значение;
    
    Сумма = ДанныеВторойКолонки[Подразделение];
    Если Сумма <> Неопределено Тогда
        ТабДок.Область(НомерСтроки, НомерКолонкиВставки).Значение = Сумма;
        ТабДок.Область(НомерСтроки, НомерКолонкиВставки).Текст = Формат(Сумма, "ЧДЦ=2");
    КонецЕсли;
КонецЦикла;

Такой способ исключает смещение колонок и позволяет точечно заполнять недостающие ячейки без нарушения разметки макета.

← На главную