Как вывести суммы с разбивкой по периодам вправо в отчете 1С 7.7?

Программист 1С v7.7 Управленческий учет Торговля и дистрибуция
← На главную

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

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

Анализ ошибок в исходном коде

Для начала проанализируем фрагмент исходного кода из первого сообщения темы и выясним, почему он не решает поставленную задачу:


Пока Запрос.Группировка(1) = 1 Цикл
    Таб.ВывестиСекцию("Менеджер|Менеджер_");
    Таб.ПрисоединитьСекцию("Менеджер|СуммаСНДС"); 
    Если фКлиенты=1 Тогда
        Пока Запрос.Группировка(2) = 1 Цикл  
             Таб.ВывестиСекцию("Контрагент|Менеджер_");
             Таб.ПрисоединитьСекцию("Контрагент|СуммаСНДС");
             Если фДокументы=1 Тогда
                 Пока Запрос.Группировка(3) = 1 Цикл
                     ...
                     Если фНеделя=1 Тогда
                         Пока Запрос.Группировка(6) = 1 Цикл         
                             Таб.ВывестиСекцию("Строка|Менеджер_");
                             Таб.ПрисоединитьСекцию("Строка|СуммаСНДС");   
                         КонецЦикла;
                     КонецЕсли;
                 ...

В этом алгоритме скрыто сразу несколько критических ошибок:

  1. Жесткая адресация индексов группировок. Обращение к группировкам по номерам (например, Запрос.Группировка(6) или Запрос.Группировка(3)) крайне опасно в динамических запросах. В коде автора текст запроса собирается по условиям: если флаг фКлиенты равен 0, то группировка по контрагентам вообще не добавляется в запрос. В результате индексы сдвигаются, и обращение к жестко прописанному номеру группировки либо вызовет ошибку выполнения, либо вернет не те данные. Правило: в динамических запросах 1С 7.7 всегда обращайтесь к группировкам по их строковому имени, например, Запрос.Группировка("Контрагент").
  2. Нарушение структуры вывода секций. Метод Таб.ПрисоединитьСекцию() присоединяет секцию справа к последней выведенной строке. Вложенные циклы автора хаотично вызывают то ВывестиСекцию() (которая создает новую строку внизу), то ПрисоединитьСекцию(). В результате макет печатной формы «поплывет»: данные за разные периоды будут перескакивать на новые строки, образуя лесенку вместо ровной таблицы.
  3. Проблема «пустых» периодов. Если у конкретного менеджера или контрагента не было продаж в какую-то из недель (например, во вторую неделю периода), запрос просто пропустит эту группировку для данного контрагента. При обходе выборки следующая существующая неделя (например, третья) выведется в следующую по порядку колонку справа. В итоге у одного контрагента во второй колонке будут данные за вторую неделю, а у другого — за третью. Данные по вертикали перестанут совпадать с шапкой отчета.

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

Способ 1. Выгрузка в Таблицу Значений и ручной разворот (Штатные средства)

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

Рассмотрим реализацию этого алгоритма по шагам.

Шаг 1. Модификация запроса и выгрузка данных

В начало запроса необходимо добавить директиву Без итогов;, чтобы отключить расчет древовидных итогов самой СУБД, так как мы будем рассчитывать их самостоятельно в таблице значений. После выполнения запроса мы выгружаем его результат в плоскую таблицу.


// Добавляем в текст запроса директиву Без итогов
ТекстЗапроса = "
|Период с ВыбНачПериода по ВыбКонПериода;
|Без итогов;
|... // описание переменных запроса
|Группировка Менеджер;
|Группировка Контрагент;
|Группировка Неделя; // или День, Месяц в зависимости от настроек
|";

Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
    Возврат;
EndIf;

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

Шаг 2. Создание списка колонок-периодов

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


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

Шаг 3. Создание результирующей кросс-таблицы

Теперь мы динамически создадим новую таблицу значений ТЗ_Итог. Первыми колонками в ней будут наши группировки строк (Менеджер, Контрагент), а последующие колонки мы добавим динамически на основании подготовленной таблицы ТЗ_Периоды.


ТЗ_Итог = СоздатьОбъект("ТаблицаЗначений");
ТЗ_Итог.НоваяКолонка("Менеджер", "Справочник.ФизическиеЛица");
ТЗ_Итог.НоваяКолонка("Контрагент", "Справочник.Контрагенты");

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

Шаг 4. Заполнение результирующей таблицы

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


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

Шаг 5. Вывод кросс-таблицы в печатную форму

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

Проанализируем алгоритм вывода макета на печать:


Таб = СоздатьОбъект("Таблица");
Таб.ИсходнаяТаблица("Сформировать");

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

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

Таб.ТолькоПросмотр(1);
Таб.Показать("Отчет по периодам");

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

Способ 2. Использование универсальной внешней обработки NotaBene

В Сообщении 8 пользователю рекомендуют не изобретать велосипед, а воспользоваться проверенным решением сообщества разработчиков — внешней обработкой NotaBene (публикация на портале Infostart под номером 14794).

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

Для ее использования программисту достаточно выполнить три простых шага в своем коде:

  1. Добавить в текст запроса директиву Без итогов;.
  2. Выгрузить результат запроса в плоскую таблицу значений: Запрос.Выгрузить(ТЗ_Данные).
  3. Передать эту ТЗ в обработку NotaBene через механизм ОткрытьФорму() или вызвать ее функции напрямую.

Пример интеграции выглядит следующим образом:


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

// Подготавливаем параметры для внешней обработки NotaBene
Параметры = СоздатьОбъект("СписокЗначений");
Параметры.Установить("ТаблицаЗначений", ТЗ_Данные);
Параметры.Установить("Строки", "Менеджер,Контрагент"); // Группировки строк
Параметры.Установить("Колонки", "Неделя");            // Что разворачивать вправо
Параметры.Установить("Ресурсы", "СуммаСНДС");         // Суммируемое поле

ОткрытьФорму("Отчет", Параметры, КаталогИБ() + "ExtForms\NotaBene.ert");

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

Способ 3. Использование внешних компонент (библиотека 1С++)

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

Резюме

Мы выяснили, что попытка построить горизонтальный отчет через вложенные циклы группировок запроса в 1С 7.7 — это тупиковый путь для динамических отчетов. Для создания качественной и надежной «шахматки» всегда используйте предварительную выгрузку данных в Таблицу Значений.

Для простых задач и в целях обучения отлично подойдет Способ 1 (ручной разворот ТЗ) — он позволяет детально понять принципы управления печатными формами в 1С. Для быстрого получения профессионального результата с интерактивными функциями рекомендуем использовать готовое решение NotaBene (Способ 2).

← На главную