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

Программист 1С v8.3 (Обычные формы) Управленческий учет Торговля и дистрибуция
← На главную

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

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

Анализ проблемы и критических ошибок в коде

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

Посмотрим на типичные ошибки, которые допускают начинающие разработчики при реализации подобных циклов:


// Ошибка №1: Попытка использовать ссылку в качестве числового счетчика
Счетчик = ТаблНомПост[3].Поставщик; // В Счетчик попадает Ссылка на Справочник, а не число!
Пока Счетчик <> 0 Цикл
    // Код вывода...
    Счетчик = Счетчик - 1; // Вызовет ошибку несовместимости типов в рантайме или бесконечный цикл
КонецЦикла;

Выделим ключевые проблемы приведенного выше кода:

  1. Неверный тип данных в счетчике. Попытка получить значение ТаблНомПост[3].Поставщик возвращает ссылку на элемент справочника (например, «Контрагенты»), а не число. Операция вычитания единицы из ссылки в 1С невозможна. Сравнение ссылки с нулем также некорректно, что приводит к зависанию или аварийному завершению работы платформы.
  2. Жесткая адресация индекса (Out of bounds). Обращение к строке таблицы по индексу ТаблНомПост[3] предполагает, что в таблице всегда гарантированно есть как минимум 4 строки. Если запрос вернет меньше строк, программа завершится с ошибкой «Индекс находится за пределами границы».
  3. Бесконечное дублирование колонок. Если в цикле вывода вызывать метод Присоединить() без жесткого контроля количества выведенных секций, то для каждой строки будет выводиться разное число колонок. Это сделает макет нечитаемым для пользователя и нарушит структуру таблицы.

Выясним причину, почему таблица может искажаться. Чтобы макет оставался ровным, в каждой строке отчета должно выводиться строго одинаковое количество колонок. Если у одного товара 1 поставщик, а у другого 3, то для первого товара мы обязаны вывести 1 заполненную колонку и 2 пустые «заглушки». Только тогда вертикальные линии таблицы сойдутся.

---

Вариант решения №1: Использование фиксированных колонок в макете (Рекомендуемый)

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

Разберем этот алгоритм по шагам:

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

Посмотрим на пример программной реализации этого метода:


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

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

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

---

Вариант решения №2: Динамическое присоединение колонок с «пустышками»

Если максимальное количество колонок заранее неизвестно (например, в одном отчете это может быть 3 колонки, в другом 5), или если макет требует жесткого использования горизонтальных секций, нам необходимо применить метод Присоединить().

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

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


// Задаем лимит выводимых поставщиков (например, максимум 3)
МаксимальноПоставщиков = 3;

ТаблНомПост = РезультатЗапросаПоставщиков.Выгрузить();
ТаблНомПост.Индексы.Добавить("Номенклатура, ВариантИсполнения");

Пока ВыборкаНоменклатуры.Следующий() Цикл
    
    // Выводим основную секцию номенклатуры (строка уходит вниз)
    ОбластьСтрока = Макет.ПолучитьОбласть("Строка|Номенклатура");
    ОбластьСтрока.Параметры.Заполнить(ВыборкаНоменклатуры);
    ТабличныйДокумент.Вывести(ОбластьСтрока, ВыборкаНоменклатуры.Уровень());
    
    // Ищем связанных поставщиков
    СтруктураОтбора = Новый Структура("Номенклатура, ВариантИсполнения", 
        ВыборкаНоменклатуры.Номенклатура, 
        ВыборкаНоменклатуры.ВариантИсполнения
    );
    НайденныеПоставщики = ТаблНомПост.НайтиСтроки(СтруктураОтбора);
    
    // Запускаем цикл строго по фиксированному числу колонок (от 1 до МаксимальноПоставщиков)
    Для НомерКолонки = 1 По МаксимальноПоставщиков Цикл
        
        // Получаем горизонтальную область для присоединения справа
        ОбластьПоставщик = Макет.ПолучитьОбласть("Строка|Поставщик");
        
        // Проверяем, есть ли данные для текущего шага
        Если НомерКолонки <= НайденныеПоставщики.Количество() Тогда
            ТекущаяСтрокаДанных = НайденныеПоставщики[НомерКолонки - 1];
            ОбластьПоставщик.Параметры.ЗначениеГрСтрПостав = ТекущаяСтрокаДанных.Поставщик;
        Иначе
            // Если данных нет — обязательно очищаем параметр, чтобы не вывелись старые значения
            ОбластьПоставщик.Параметры.ЗначениеГрСтрПостав = "";
        КонецЕсли;
        
        // Всегда присоединяем область в текущую строку табличного документа
        ТабличныйДокумент.Присоединить(ОбластьПоставщик);
        
    КонецЦикла;
    
КонецЦикла;

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

---

Оптимизация производительности: Ранжирование и индексация

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

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

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

<code ТаблНомПост.Индексы.Добавить("Номенклатура, ВариантИсполнения");

Без индексации метод НайтиСтроки() выполняет полный перебор всей таблицы значений при каждом обращении (сложность поиска O(N)). При добавлении индекса платформа строит хэш-таблицу в памяти, сводя время поиска практически к мгновенному (сложность O(1)).

2. Предварительная фильтрация и нумерация поставщиков

Чтобы не хранить в оперативной памяти лишних контрагентов (если у номенклатуры их может быть, например, 10, а выводить нужно только 3), рекомендуется предварительно пронумеровать записи прямо в таблице значений и удалить лишние строки. Это избавит алгоритм от лишней работы.

Рассмотрим, как быстро пронумеровать и отсечь лишние строки программно в один проход:


// Сортируем таблицу, чтобы строки по одной номенклатуре шли последовательно
ТаблНомПост.Сортировать("Номенклатура Возр, ВариантИсполнения Возр");

ТекущаяНом = Неопределено;
ТекущийВариант = Неопределено;
ПорядковыйНомер = 0;

// Создаем массив для удаления лишних строк
СтрокиДляУдаления = Новый Массив;

Для Каждого СтрокаПост Из ТаблНомПост Цикл
    
    Если СтрокаПост.Номенклатура <> ТекущаяНом Или СтрокаПост.ВариантИсполнения <> ТекущийВариант Тогда
        // Сменилась аналитика — сбрасываем счетчик
        ТекущаяНом = СтрокаПост.Номенклатура;
        ТекущийВариант = СтрокаПост.ВариантИсполнения;
        ПорядковыйНомер = 1;
    Иначе
        // Та же номенклатура — инкрементируем счетчик
        ПорядковыйНомер = ПорядковыйНомер + 1;
    КонецЕсли;
    
    // Если превысили лимит в 3 поставщика, помечаем строку на удаление
    Если ПорядковыйНомер > 3 Тогда
        СтрокиДляУдаления.Добавить(СтрокаПост);
    КонецЕсли;
    
КонецЦикла;

// Удаляем лишние строки из таблицы значений
Для Каждого СтрокаУдаления Из СтрокиДляУдаления Цикл
    ТаблНомПост.Удалить(СтрокаУдаления);
КонецЦикла;

Применив данный алгоритм предварительной подготовки данных, мы гарантируем, что в таблице значений ТаблНомПост останется максимум по 3 записи для каждого уникального сочетания номенклатуры и варианта исполнения. Это минимизирует объем занимаемой памяти и в разы ускорит выполнение метода НайтиСтроки() в основном цикле отрисовки отчета.

← На главную