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