Как создать кросс-отчет с динамическими колонками по датам во внешней печатной форме 1С?

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

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

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

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

Рассмотрим пример запроса, который часто пишут на начальном этапе:


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

Проанализируем ошибки и неточности в этом коде:

  1. Оптимизация виртуальной таблицы: Условие СебестоимостьТоваровОстаткиИОбороты.РазделУчета = &РазделУчета находится в секции ГДЕ. Это грубая ошибка производительности. Условия для виртуальных таблиц всегда нужно передавать в их параметры, чтобы СУБД отсекала лишние данные еще на этапе чтения.
  2. Левое соединение со всем справочником: Левое соединение справочника Номенклатура с регистром приведет к тому, что в выборку попадет абсолютно вся номенклатура из базы, включая помеченную на удаление или не имеющую движений (если не наложить дополнительных условий). Нам же нужны только те товары, по которым были обороты.
  3. Структура итогов: Для построения кросс-таблицы нам необходимо обходить данные по группировкам. Сейчас итоги подводятся только по наименованию.

Решение 1: Формирование «Полной матрицы» в запросе

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

Нам потребуется использовать метод декартова произведения таблиц с помощью полного соединения.

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

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

Решение 2: Обход результата запроса по группировкам и использование Соответствия

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

Рассмотрим по шагам логику построения кода:

Шаг 1. Получение правильных Итогов в запросе.
В запросе необходимо подвести итоги не только по номенклатуре, но и по периоду. В параметрах виртуальной таблицы укажем нужные отборы.

Шаг 2. Вывод шапки и запись индексов колонок.
Перед выводом строк с номенклатурой мы формируем шапку таблицы. Нам нужно "размножить" колонку с датами. В этот момент мы будем использовать объект Соответствие, где ключом будет выступать сама Дата, а значением — номер (индекс) выведенной колонки (или ее уникальное имя/идентификатор, если мы собираем строку перед выводом).

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

Правильный алгоритм вывода строки выглядит так:

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

Посмотрим на логику обхода более детально (псевдокод):


// Предполагается, что МассивДатШапки - массив дат, полученный при формировании шапки
ВыборкаНоменклатура = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Номенклатура");

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

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

Рекомендации по разработке печатных форм

При создании сложных отчетов и печатных форм с кросс-таблицами всегда помните:

  1. Метод Присоединить() добавляет область справа от последней выведенной. Если вы пропустите итерацию цикла из-за отсутствия данных, весь последующий ряд сместится влево.
  2. Для получения списка дат не стоит обращаться напрямую к таблицам документов (например, Документ.ОтпускТоваров). Лучше получать даты из того же регистра, по которому строятся обороты, сгруппировав их по периоду.
  3. Если структура отчета слишком сложная, рассмотрите возможность использования Схемы Компоновки Данных (СКД) программно. СКД отлично справляется с выводом кросс-таблиц автоматически, избавляя вас от ручного программирования макетов и циклов с присоединением областей.
← На главную