Как правильно вывести кросс-таблицу (шахматку) в макет 1С 7.7?

Программист 1С v7.7 IT и автоматизация бизнеса
← На главную

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

В этой статье мы подробно разберем, как устроен механизм пересекающихся секций в 1С 7.7, проанализируем популярные ошибки позиционирования ячеек и рассмотрим три практических метода реализации кросс-отчетов: от простого алгоритма на таблицах значений до высокопроизводительного запроса с автодополнением данных.

Для успешного освоения материала нам понадобятся базовые знания о работе с объектами ТаблицаЗначений, Запрос и методами табличного документа (макета).

---

1. Принцип работы двумерных секций в 1С 7.7

Прежде чем переходить к написанию кода, давайте разберем, как платформа 1С 7.7 понимает координаты выводимых областей. В отличие от версии 8.х, где работа ведется с областями ячеек, в версии 7.7 позиционирование строится на пересечении именованных горизонтальных и вертикальных секций.

В шаблоне вашего макета должны быть созданы:

Имя итоговой секции для вывода формируется через вертикальную черту: "ИмяСтроки|ИмяКолонки". Управление выводом осуществляется двумя ключевыми методами объекта Таблица (макета):

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

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

---

2. Способ №1. Метод трех таблиц значений (классический подход)

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

Проанализируем ситуацию по шагам:

  1. Шаг 1. Создаем общую таблицу значений ТЗ_Общая, содержащую три базовые колонки: Сотрудник, ВидРасчета, Результат. Заполняем ее исходными данными.
  2. Шаг 2. Формируем уникальный список строк. Для этого выгружаем колонку Сотрудник в новую таблицу ТЗ_Строки и сворачиваем ее для удаления дублей.
  3. Шаг 3. Формируем уникальный список колонок. Аналогично выгружаем ВидРасчета в таблицу ТЗ_Колонки и тоже сворачиваем.

Ниже представлен пример кода, реализующий вывод данных по этой схеме:


// Предположим, что ТЗ_Общая уже заполнена данными
// 1. Создаем уникальные списки строк и колонок
ТЗ_Строки = СоздатьОбъект("ТаблицаЗначений");
ТЗ_Общая.Выгрузить(ТЗ_Строки, , , "Сотрудник");
ТЗ_Строки.Свернуть("Сотрудник", "");

ТЗ_Колонки = СоздатьОбъект("ТаблицаЗначений");
ТЗ_Общая.Выгрузить(ТЗ_Колонки, , , "ВидРасчета");
ТЗ_Колонки.Свернуть("ВидРасчета", "");

// 2. Выводим шапку таблицы
Таб.ВывестиСекцию("Шапка|Общая");

Для НомКол = 1 По ТЗ_Колонки.КоличествоСтрок() Цикл
    // Заполняем переменную шапки в макете
    ТекущийВидРасчета = ТЗ_Колонки.ПолучитьЗначение(НомКол, "ВидРасчета");
    Таб.ПрисоединитьСекцию("Шапка|Параметр");
КонецЦикла;

// 3. Выводим строки с данными
Для НомСтр = 1 По ТЗ_Строки.КоличествоСтрок() Цикл
    ТекущийСотрудник = ТЗ_Строки.ПолучитьЗначение(НомСтр, "Сотрудник");
    
    // Инициализируем новую строку в макете
    Таб.ВывестиСекцию("Строка|Общая");
    
    // Перебираем все возможные колонки для этого сотрудника
    Для НомКол = 1 По ТЗ_Колонки.КоличествоСтрок() Цикл
        ТекущийВидРасчета = ТЗ_Колонки.ПолучитьЗначение(НомКол, "ВидРасчета");
        
        // Ищем значение в общей таблице
        СтрокаПоиска = 0;
        СуммаЗначения = 0;
        
        Если ТЗ_Общая.НайтиЗначение(ТекущийСотрудник, СтрокаПоиска, "Сотрудник") = 1 Тогда
            // Нам нужно убедиться, что вид расчета тоже совпадает
            Пока СтрокаПоиска <= ТЗ_Общая.КоличествоСтрок() Цикл
                Если (ТЗ_Общая.ПолучитьЗначение(СтрокаПоиска, "Сотрудник") = ТекущийСотрудник) И 
                     (ТЗ_Общая.ПолучитьЗначение(СтрокаПоиска, "ВидРасчета") = ТекущийВидРасчета) Тогда
                    СуммаЗначения = ТЗ_Общая.ПолучитьЗначение(СтрокаПоиска, "Результат");
                    Прервать;
                КонецЕсли;
                СтрокаПоиска = СтрокаПоиска + 1;
            КонецЦикла;
        КонецЕсли;
        
        // Передаем значение в параметр макета (если пусто, выводим 0)
        ЗначениеДляВывода = ?(СуммаЗначения = 0, 0, СуммаЗначения);
        Таб.ПрисоединитьСекцию("Строка|Параметр");
    КонецЦикла;
КонецЦикла;

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

---

3. Способ №2. Оптимальный метод транспонирования (сведение в одну ТЗ)

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

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

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

  1. Создаем результирующую таблицу ТЗ_Результат и сразу добавляем в нее одну базовую колонку — Сотрудник.
  2. Перебираем уникальный список видов расчетов и для каждого динамически добавляем новую числовую колонку методом НоваяКолонка(ИдентификаторКолонки, "Число").
  3. Заполняем ТЗ_Результат данными, обращаясь напрямую по именам созданных динамических колонок.
  4. Выводим данные в макет простым и очень быстрым линейным обходом без использования тяжелых функций поиска.

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


// Создаем результирующую таблицу
ТЗ_Результат = СоздатьОбъект("ТаблицаЗначений");
ТЗ_Результат.НоваяКолонка("Сотрудник", "Справочник.Сотрудники");

// Получаем уникальный список видов расчета
ТЗ_ВидыРасчета = СоздатьОбъект("TablicaZnacheniy");
ТЗ_Общая.Выгрузить(ТЗ_ВидыРасчета, , , "ВидРасчета");
ТЗ_ВидыРасчета.Свернуть("ВидРасчета", "");

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

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

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

Для НомСтр = 1 По ТЗ_Результат.КоличествоСтрок() Цикл
    СотрудникДляМакета = ТЗ_Результат.ПолучитьЗначение(НомСтр, "Сотрудник");
    Таб.ВывестиСекцию("Строка|Общая");
    
    Для НомКол = 2 По ТЗ_Результат.КоличествоКолонок() Цикл
        ЗначениеДляМакета = ТЗ_Результат.ПолучитьЗначение(НомСтр, НомКол);
        Таб.ПрисоединитьСекцию("Строка|Параметр");
    КонецЦикла;
КонецЦикла;

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

---

4. Способ №3. Использование языка запросов с опцией "Все"

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

Рассмотрим стандартный запрос со следующими группировками:


Группировка Сотрудник;
Группировка ВидРасчета;

Если мы попытаемся вывести его результаты стандартным вложенным циклом:


Пока Запрос.Группировка("Сотрудник") = 1 Цикл
    Таб.ВывестиСекцию("Строка|Общая");
    Пока Запрос.Группировка("ВидРасчета") = 1 Цикл
        // Выводим данные...
        Таб.ПрисоединитьСекцию("Строка|Параметр");
    КонецЦикла;
КонецЦикла;

То у сотрудников с разным набором начислений колонки сместятся. Например, если у первого сотрудника есть "Оклад" и "Премия", а у второго только "Премия", то у второго сотрудника выполнится всего одно присоединение секции, и значение премии визуально встанет под заголовок оклада первой колонки.

Решение проблемы сдвигов в запросе:

Для горизонтальной группировки (в нашем примере это ВидРасчета) необходимо принудительно указать ключевое слово Все (или ВошедшиеВЗапрос):


// Фрагмент текста запроса
Группировка Сотрудник;
Группировка ВидРасчета Все ВошедшиеВЗапрос;

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

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

---

5. Полезные технические приемы при оформлении кросс-таблиц

При создании широких динамических отчетов рекомендуем использовать следующие системные возможности платформы 1С 7.7:

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

← На главную