Разработка отчетов в платформе 1С:Предприятие 7.7 часто ставит перед программистом задачу вывода двумерных матриц данных (так называемых кросс-таблиц или «шахматок»). Типичный пример — расчетная ведомость, где по строкам идут сотрудники, а по колонкам — различные виды начислений и удержаний, состав которых заранее неизвестен и определяется динамически.
В этой статье мы подробно разберем, как устроен механизм пересекающихся секций в 1С 7.7, проанализируем популярные ошибки позиционирования ячеек и рассмотрим три практических метода реализации кросс-отчетов: от простого алгоритма на таблицах значений до высокопроизводительного запроса с автодополнением данных.
Для успешного освоения материала нам понадобятся базовые знания о работе с объектами ТаблицаЗначений, Запрос и методами табличного документа (макета).
---
Прежде чем переходить к написанию кода, давайте разберем, как платформа 1С 7.7 понимает координаты выводимых областей. В отличие от версии 8.х, где работа ведется с областями ячеек, в версии 7.7 позиционирование строится на пересечении именованных горизонтальных и вертикальных секций.
В шаблоне вашего макета должны быть созданы:
Шапка (для заголовка таблицы) и Строка (для основных данных).Общая (фиксированная левая часть с ФИО сотрудника) и Параметр (динамически присоединяемая правая часть с суммами).Имя итоговой секции для вывода формируется через вертикальную черту: "ИмяСтроки|ИмяКолонки". Управление выводом осуществляется двумя ключевыми методами объекта Таблица (макета):
ВывестиСекцию("Строка|Общая") — инициализирует создание новой строки в результирующем табличном документе и выводит ее левую, фиксированную часть. Каждый повторный вызов этого метода переводит виртуальный курсор принтера на новую строку.ПрисоединитьСекцию("Строка|Параметр") — пристраивает динамическую ячейку справа к уже созданной строке. Мы можем вызывать этот метод в цикле сколько угодно раз, последовательно дописывая колонки в текущей строке.Важное правило: Сначала всегда вызывается ВывестиСекцию() для начала строки, а затем в цикле — ПрисоединитьСекцию() для всех колонок этой строки. Если вы попытаетесь вызвать ПрисоединитьСекцию() до вывода строки, данные запишутся некорректно или возникнет ошибка позиционирования.
---
Рассмотрим алгоритм, предложенный в процессе обсуждения на форуме. Он отлично подходит для небольших объемов данных и прост в понимании. Суть метода заключается в разделении исходного плоского массива данных на две независимые аналитики (строки и колонки) с последующим их сопоставлением.
Проанализируем ситуацию по шагам:
ТЗ_Общая, содержащую три базовые колонки: Сотрудник, ВидРасчета, Результат. Заполняем ее исходными данными.Сотрудник в новую таблицу ТЗ_Строки и сворачиваем ее для удаления дублей.ВидРасчета в таблицу ТЗ_Колонки и тоже сворачиваем.Ниже представлен пример кода, реализующий вывод данных по этой схеме:
// Предположим, что ТЗ_Общая уже заполнена данными
// 1. Создаем уникальные списки строк и колонок
ТЗ_Строки = СоздатьОбъект("ТаблицаЗначений");
ТЗ_Общая.Выгрузить(ТЗ_Строки, , , "Сотрудник");
ТЗ_Строки.Свернуть("Сотрудник", "");
ТЗ_Колонки = СоздатьОбъект("ТаблицаЗначений");
ТЗ_Общая.Выгрузить(ТЗ_Колонки, , , "ВидРасчета");
ТЗ_Колонки.Свернуть("ВидРасчета", "");
// 2. Выводим шапку таблицы
Таб.ВывестиСекцию("Шапка|Общая");
Для НомКол = 1 По ТЗ_Колонки.КоличествоСтрок() Цикл
// Заполняем переменную шапки в макете
ТекущийВидРасчета = ТЗ_Колонки.ПолучитьЗначение(НомКол, "ВидРасчета");
Таб.ПрисоединитьСекцию("Шапка|Параметр");
КонецЦикла;
// 3. Выводим строки с данными
Для НомСтр = 1 По ТЗ_Строки.КоличествоСтрок() Цикл
ТекущийСотрудник = ТЗ_Строки.ПолучитьЗначение(НомСтр, "Сотрудник");
// Инициализируем новую строку в макете
Таб.ВывестиСекцию("Строка|Общая");
// Перебираем все возможные колонки для этого сотрудника
Для НомКол = 1 По ТЗ_Колонки.КоличествоСтрок() Цикл
ТекущийВидРасчета = ТЗ_Колонки.ПолучитьЗначение(НомКол, "ВидРасчета");
// Ищем значение в общей таблице
СтрокаПоиска = 0;
СуммаЗначения = 0;
Если ТЗ_Общая.НайтиЗначение(ТекущийСотрудник, СтрокаПоиска, "Сотрудник") = 1 Тогда
// Нам нужно убедиться, что вид расчета тоже совпадает
Пока СтрокаПоиска <= ТЗ_Общая.КоличествоСтрок() Цикл
Если (ТЗ_Общая.ПолучитьЗначение(СтрокаПоиска, "Сотрудник") = ТекущийСотрудник) И
(ТЗ_Общая.ПолучитьЗначение(СтрокаПоиска, "ВидРасчета") = ТекущийВидРасчета) Тогда
СуммаЗначения = ТЗ_Общая.ПолучитьЗначение(СтрокаПоиска, "Результат");
Прервать;
КонецЕсли;
СтрокаПоиска = СтрокаПоиска + 1;
КонецЦикла;
КонецЕсли;
// Передаем значение в параметр макета (если пусто, выводим 0)
ЗначениеДляВывода = ?(СуммаЗначения = 0, 0, СуммаЗначения);
Таб.ПрисоединитьСекцию("Строка|Параметр");
КонецЦикла;
КонецЦикла;
Критически важный нюанс: Обратите внимание на вывод нуля в случае отсутствия начисления. Если мы пропустим вывод ячейки для сотрудника, у которого нет конкретного начисления, вся строка "уплывет" влево, и значения окажутся в чужих колонках. Программа должна гарантированно совершить операцию ПрисоединитьСекцию ровно столько раз, сколько колонок выведено в шапке отчета.
---
Метод с тремя таблицами значений и поиском внутри вложенных циклов имеет один существенный недостаток — он медленно работает на больших объемах данных из-за частых поисков по таблице (сложность алгоритма близка к квадратичной).
Рассмотрим более профессиональный и быстрый подход — предварительное сведение плоских данных в одну результирующую ТаблицуЗначений, где колонки будут создаваться динамически.
Разберем данный метод по шагам:
ТЗ_Результат и сразу добавляем в нее одну базовую колонку — Сотрудник.НоваяКолонка(ИдентификаторКолонки, "Число").ТЗ_Результат данными, обращаясь напрямую по именам созданных динамических колонок.Посмотрим на практическую реализацию этого алгоритма:
// Создаем результирующую таблицу
ТЗ_Результат = СоздатьОбъект("ТаблицаЗначений");
ТЗ_Результат.НоваяКолонка("Сотрудник", "Справочник.Сотрудники");
// Получаем уникальный список видов расчета
ТЗ_ВидыРасчета = СоздатьОбъект("TablicaZnacheniy");
ТЗ_Общая.Выгрузить(ТЗ_ВидыРасчета, , , "ВидРасчета");
ТЗ_ВидыРасчета.Свернуть("ВидРасчета", "");
// Динамически добавляем колонки в ТЗ_Результат
Для Ном = 1 По ТЗ_ВидыРасчета.КоличествоСтрок() Цикл
ВР = ТЗ_ВидыРасчета.ПолучитьЗначение(Ном, "ВидРасчета");
// Используем внутренний идентификатор или код для имени колонки
ИмяКолонки = "ВР_" + ВР.Код;
ТЗ_Результат.НоваяКолонка(ИмяКолонки, "Число");
КонецЦикла;
// Заполняем ТЗ_Результат свернутыми данными
ТЗ_Общая.ВыбратьСтроки();
Пока ТЗ_Общая.ПолучитьСтроку() = 1 Цикл
ТекущийСотр = ТЗ_Общая.Сотрудник;
ТекущийВР = ТЗ_Общая.ВидРасчета;
Сумма = ТЗ_Общая.Результат;
// Ищем строку сотрудника в результирующей таблице
НомСтрРезилт = 0;
Если ТЗ_Результат.НайтиЗначение(ТекущийСотр, НомСтрРезилт, "Сотрудник") = 0 Тогда
ТЗ_Результат.НоваяСтрока();
ТЗ_Результат.Сотрудник = ТекущийСотр;
НомСтрРезилт = ТЗ_Результат.КоличествоСтрок();
КонецЕсли;
ИмяКолонки = "ВР_" + ТекущийВР.Код;
ТЗ_Результат.УстановитьЗначение(НомСтрРезилт, ИмяКолонки, Сумма);
КонецЦикла;
// Теперь вывод в макет становится тривиальным и максимально быстрым!
Таб.ВывестиСекцию("Шапка|Общая");
Для НомКол = 2 По ТЗ_Результат.КоличествоКолонок() Цикл
// Получаем заголовок колонки для вывода в макет
ИдентификаторКолонки = "";
ТЗ_Результат.ПолучитьПараметрыКолонки(НомКол, , , , ИдентификаторКолонки);
// Преобразуем идентификатор обратно в читаемый вид (или храним карту соответствий)
ТекущийВидРасчетаПредставление = Сред(ИдентификаторКолонки, 4);
Таб.ПрисоединитьСекцию("Шапка|Параметр");
КонецЦикла;
Для НомСтр = 1 По ТЗ_Результат.КоличествоСтрок() Цикл
СотрудникДляМакета = ТЗ_Результат.ПолучитьЗначение(НомСтр, "Сотрудник");
Таб.ВывестиСекцию("Строка|Общая");
Для НомКол = 2 По ТЗ_Результат.КоличествоКолонок() Цикл
ЗначениеДляМакета = ТЗ_Результат.ПолучитьЗначение(НомСтр, НомКол);
Таб.ПрисоединитьСекцию("Строка|Параметр");
КонецЦикла;
КонецЦикла;
Данный подход полностью исключает проблему "сдвигов" и гарантирует идеальное соответствие структуры шапки и строк данных, так как они читаются из одного и того же объекта-матрицы.
---
Если ваши исходные данные выбираются напрямую из базы данных с помощью встроенного языка запросов 1С 7.7, то построить кросс-таблицу можно без промежуточной выгрузки в таблицы значений. Однако здесь разработчиков подстерегает классическая архитектурная ловушка.
Рассмотрим стандартный запрос со следующими группировками:
Группировка Сотрудник;
Группировка ВидРасчета;
Если мы попытаемся вывести его результаты стандартным вложенным циклом:
Пока Запрос.Группировка("Сотрудник") = 1 Цикл
Таб.ВывестиСекцию("Строка|Общая");
Пока Запрос.Группировка("ВидРасчета") = 1 Цикл
// Выводим данные...
Таб.ПрисоединитьСекцию("Строка|Параметр");
КонецЦикла;
КонецЦикла;
То у сотрудников с разным набором начислений колонки сместятся. Например, если у первого сотрудника есть "Оклад" и "Премия", а у второго только "Премия", то у второго сотрудника выполнится всего одно присоединение секции, и значение премии визуально встанет под заголовок оклада первой колонки.
Решение проблемы сдвигов в запросе:
Для горизонтальной группировки (в нашем примере это ВидРасчета) необходимо принудительно указать ключевое слово Все (или ВошедшиеВЗапрос):
// Фрагмент текста запроса
Группировка Сотрудник;
Группировка ВидРасчета Все ВошедшиеВЗапрос;
Это ключевое слово заставит выборку запроса возвращать абсолютно все возможные значения видов расчета для каждого сотрудника, даже если по ним нет движений в базе данных за указанный период (в таком случае запрос просто вернет пустое значение или ноль).
Благодаря этому количество итераций во внутреннем цикле для каждого сотрудника будет строго одинаковым и равным общему количеству уникальных видов расчета. Таблица заполнится ровно и красиво.
---
При создании широких динамических отчетов рекомендуем использовать следующие системные возможности платформы 1С 7.7:
Опции() для фиксации областей:
Таб.Опции(1, 0, 4, 2);
ПрисоединитьСекцию() ширина добавляемой колонки в результирующем отчете в точности соответствует ширине этой секции в шаблоне макета. Старайтесь делать шаблон динамической колонки как можно уже (например, ровно под размер выводимых чисел), чтобы отчет не растягивался на много страниц при печати.
Мы подробно разобрали особенности построения динамических отчетов в системе 1С:Предприятие 7.7. Выбирайте наиболее подходящий для вашей конфигурации метод, контролируйте корректность циклов вывода секций и всегда помните о важности сохранения целостности структуры колонок для каждой строки таблицы.