Как программно вывести кросс-таблицу в 1С без использования СКД?

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

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

Самым простым и современным инструментом для решения этой задачи является Система компоновки данных (СКД). Она автоматически группирует данные и строит кросс-таблицы "из коробки". Однако на практике программисты часто сталкиваются с ограничениями, когда использовать СКД невозможно или нецелесообразно. Это может быть связано с необходимостью интеграции вывода в сложный существующий алгоритм, жесткими требованиями к оформлению нетиповых печатных форм или использованием старых заготовок кода. В таких случаях на помощь приходит классический ручной вывод с использованием методов Вывести() и Присоединить() объекта ТабличныйДокумент.

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

Шаг 1. Подготовка макета и использование пересекающихся областей

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

Рассмотрим структуру макета подробнее:

  1. Горизонтальные секции (строки): Создайте строку ШапкаТаблицы (для заголовков колонок) и строку СтрокаДанных (для вывода основных показателей).
  2. Вертикальные секции (колонки): Создайте колонку ЛеваяКолонка (для фиксированных заголовков строк, таких как Артикул, Размер, Цвет) и колонку ПраваяКолонка (для динамически добавляемых колонок, например, Размер2 и Цвет2). При необходимости можно добавить вертикальную секцию ИтоговаяКолонка.

При формировании итогового документа мы будем обращаться к областям на их пересечении, используя символ вертикального слэша |. Рассмотрим подробнее, как это выглядит в коде:


// Получаем фиксированную левую часть шапки
ОбластьШапкаЛево = Макет.ПолучитьОбласть("ШапкаТаблицы|ЛеваяКолонка");

// Получаем динамическую правую часть шапки (будет присоединяться в цикле)
ОбластьШапкаКолонка = Макет.ПолучитьОбласть("ШапкаТаблицы|ПраваяКолонка");

// Получаем левую часть строки данных (названия строк)
ОбластьСтрокаЗаголовок = Макет.ПолучитьОбласть("СтрокаДанных|ЛеваяКолонка");

// Получаем динамическую ячейку данных на пересечении строки и колонки
ОбластьСтрокаКолонка = Макет.ПолучитьОбласть("СтрокаДанных|ПраваяКолонка");

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

Способ 1. Обход выборки запроса с параметром "ВСЕ" (Классический вариант)

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

Для решения этой проблемы в платформе 1С предусмотрен специальный механизм — запуск выборки с третьим параметром "ВСЕ" (или "ALL") в методе Выбрать() объекта ВыборкаИзРезультатаЗапроса. Этот параметр заставляет систему возвращать абсолютно все значения подчиненной группировки, которые присутствуют в общем результате запроса, даже если для конкретной родительской записи данных по этой группировке нет. В таких "пустых" точках пересечения ресурсы запроса вернут значение NULL или Неопределено, что позволяет нам легко обработать их в коде и вывести пустую ячейку или ноль.

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


// Предположим, запрос уже выполнен, и мы получили РезультатЗапроса.
// Запрос должен содержать итоги по группировкам: ИТОГИ ПО Артикул, Размер, Размер2

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

ТабДок = Новый ТабличныйДокумент;

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

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

Способ 2. Использование промежуточной Таблицы Значений

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

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

Разберем данный метод по шагам:

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

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


// Шаг 1. Выгружаем данные запроса
ТЗДанные = РезультатЗапроса.Выгрузить();

// Шаг 2. Создаем вспомогательную таблицу для получения уникальных колонок
ТЗКолонки = ТЗДанные.Скопировать();
ТЗКолонки.Свернуть("Размер2");
ТЗКолонки.Сортировать("Размер2 Возр");

// Шаг 3. Создаем новую кросс-таблицу значений
ТЗКросс = Новый ТаблицаЗначений;
ТЗКросс.Колонки.Добавить("Артикул");
ТЗКросс.Колонки.Добавить("Размер");
ТЗКросс.Колонки.Добавить("Цвет");

// Добавляем динамические колонки. Имена колонок ТЗ должны соответствовать правилам идентификаторов,
// поэтому перед значением можно добавить префикс, например "Кол_"
Для Каждого СтрКолонка Из ТЗКолонки Цикл
    // Заменяем недопустимые символы, если они могут быть в значениях
    ИмяКолонки = "Кол_" + СтрЗаменить(Строка(СтрКолонка.Размер2), " ", "_");
    ТЗКросс.Колонки.Добавить(ИмяКолонки, Новый ОписаниеТипов("Число"));
КонецЦикла;

// Шаг 4. Заполняем ТЗКросс данными из исходной таблицы
Для Каждого СтрДанных Из ТЗДанные Цикл
    
    // Ищем уже существующую строку по ключевым полям
    ПараметрыПоиска = Новый Структура("Артикул, Размер, Цвет", СтрДанных.Артикул, СтрДанных.Размер, СтрДанных.Цвет);
    НайденныеСтроки = ТЗКросс.НайтиСтроки(ПараметрыПоиска);
    
    Если НайденныеСтроки.Количество() = 0 Тогда
        НоваяСтрока = ТЗКросс.Добавить();
        ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрДанных);
        СтрокаДляЗаписи = НоваяСтрока;
    Иначе
        СтрокаДляЗаписи = НайденныеСтроки[0];
    КонецЕсли;
    
    // Записываем значение в динамическую колонку
    ИмяКолонки = "Кол_" + СтрЗаменить(Строка(СтрДанных.Размер2), " ", "_");
    СтрокаДляЗаписи[ИмяКолонки] = ЕСТЬNULL(СтрокаДляЗаписи[ИмяКолонки], 0) + СтрДанных.Количество;
    
КонецЦикла;

// Шаг 5. Выводим полученную ТЗКросс в Табличный Документ
ТабДок = Новый ТабличныйДокумент;

// Группируем по Артикулу для вывода отдельных таблиц
ТЗАртикулы = ТЗКросс.Скопировать();
ТЗАртикулы.Свернуть("Артикул");

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

Рекомендации по оптимизации и оформлению кросс-таблиц

При ручном формировании сложных отчетов с большим объемом данных полезно придерживаться нескольких важных правил:

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

← На главную