Как вручную построить кросс-отчет (шахматку) по месяцам без СКД в 1С

Программист 1С v8.3 (Обычные формы) Управленческий учет Торговля и дистрибуция
← На главную

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

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

В данной статье мы подробно разберем, как реализовать такой отчет на встроенном языке 1С без использования конструкторов СКД. Мы научимся правильно писать запросы с итогами, использовать методы обхода группировок и динамически присоединять секции макета по горизонтали.

Ниже представлена подробная схема решения этой задачи, разделенная на логические шаги.

Шаг 1. Проектирование макета табличного документа

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

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

  1. Область шапки с названием номенклатуры (левый угол): назовем эту область Шапка|Номенклатура. Здесь будет располагаться заголовок строки — "Номенклатура / Месяц".
  2. Область шапки с периодом (динамическая колонка): назовем ее Шапка|Период. В этой ячейке мы разместим параметр ИмяМесяца.
  3. Область строки с названием номенклатуры (левая часть строки): назовем ее Строка|Номенклатура. Здесь будет выводиться параметр Номенклатура.
  4. Область данных (ячейка пересечения): назовем ее Строка|Данные. В ней будет выводиться параметр Количество.

Обратите внимание на синтаксис именования областей через вертикальную черту |. Это позволяет системе 1С понимать пересечения строк и колонок, что критически важно при использовании методов Вывести() и Присоединить().

Шаг 2. Разработка правильного запроса с итогами

При ручном обходе группировок запроса начинающие разработчики часто совершают ошибку: они используют конструкцию СГРУППИРОВАТЬ ПО и пытаются выполнить обход по группировкам. При этом система выдает ошибку о том, что измерение не найдено.

Запомните важное правило: для того чтобы в коде работал обход результатов запроса по группировкам (метод Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам)), в тексте запроса обязательно должна присутствовать секция ИТОГИ ПО.

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


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ПродажиОбороты.Номенклатура КАК Номенклатура,
    |	ПродажиОбороты.Период КАК Период,
    |	ПродажиОбороты.КоличествоОборот КАК Количество
    |ИЗ
    |	РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода, Месяц, ) КАК ПродажиОбороты
    |УПОРЯДОЧИТЬ ПО
    |	Номенклатура,
    |	Период
    |ИТОГИ
    |	СУММА(Количество)
    |ПО
    |	ОБЩИЕ,
    |	Номенклатура,
    |	Период ПЕРИОДАМИ(Месяц, &НачалоПериода, &КонецПериода)";

Разберем ключевые особенности этого запроса:

Шаг 3. Оптимальный обход данных и секрет параметра "ВСЕ"

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

Здесь кроется главная сложность: как сделать так, чтобы при выводе конкретной номенклатуры колонки с месяцами выстроились строго под соответствующими заголовками шапки? Если у товара "Курица" были продажи только в марте, а у "Яйца" — только в мае, то без специальной настройки колонки "съедут" влево, нарушив структуру отчета.

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

ВыборкаПериод = ВыборкаНоменклатура.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Период", "ВСЕ");

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

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

Шаг 4. Полный программный код построения отчета

Давайте соберем всю логику воедино и напишем готовую процедуру вывода отчета в табличный документ.


Процедура СформироватьОтчет(ТабДок) Экспорт

    // Очищаем табличный документ перед выводом
    ТабДок.Очистить();
    
    // Получаем макет отчета
    Макет = ПолучитьМакет("Макет");
    
    // Инициализируем и заполняем параметры запроса
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	ПродажиОбороты.Номенклатура КАК Номенклатура,
        |	ПродажиОбороты.Период КАК Период,
        |	ПродажиОбороты.КоличествоОборот КАК Количество
        |ИЗ
        |	РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода, Месяц, ) КАК ПродажиОбороты
        |УПОРЯДОЧИТЬ ПО
        |	Номенклатура,
        |	Период
        |ИТОГИ
        |	СУММА(Количество)
        |ПО
        |	ОБЩИЕ,
        |	Номенклатура,
        |	Период ПЕРИОДАМИ(Месяц, &НачалоПериода, &КонецПериода)";
        
    // Предположим, Даты начала и конца периода выбраны пользователем на форме
    Запрос.УстановитьПараметр("НачалоПериода", НачалоДня(ПериодНачало));
    Запрос.УстановитьПараметр("КонецПериода", КонецДня(ПериодКонец));
    
    // Выполняем запрос ровно ОДИН раз
    РезультатЗапроса = Запрос.Выполнить();
    
    // -----------------------------------------------------------------
    // ЭТАП 1: Вывод шапки таблицы (заголовки колонок)
    // -----------------------------------------------------------------
    
    // Получаем и выводим левый угол шапки (заголовок строк)
    ОбластьШапкаНоменклатура = Макет.ПолучитьОбласть("Шапка|Номенклатура");
    ТабДок.Вывести(ОбластьШапкаНоменклатура);
    
    // Получаем выборку по периодам из общего результата, чтобы построить колонки
    ВыборкаПериодовШапки = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Период");
    ОбластьШапкаПериод = Макет.ПолучитьОбласть("Шапка|Период");
    
    Пока ВыборкаПериодовШапки.Следующий() Цикл
        // Форматируем дату, выводя только название месяца и год (например, "март 2019")
        ОбластьШапкаПериод.Параметры.ИмяМесяца = Формат(ВыборкаПериодовШапки.Период, "ДФ='MMMM yyyy'");
        // Важно: для колонок используем метод Присоединить()
        ТабДок.Присоединить(ОбластьШапкаПериод);
    КонецЦикла;
    
    // -----------------------------------------------------------------
    // ЭТАП 2: Вывод строк с данными (номенклатура и количество)
    // -----------------------------------------------------------------
    
    // Выбираем группировку Номенклатура
    ВыборкаНоменклатура = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Номенклатура");
    
    ОбластьСтрокаНоменклатура = Макет.ПолучитьОбласть("Строка|Номенклатура");
    ОбластьСтрокаДанные = Макет.ПолучитьОбласть("Строка|Данные");
    
    Пока ВыборкаНоменклатура.Следующий() Цикл
        
        // Проверяем, что номенклатура заполнена (чтобы избежать пустых строк общей группировки)
        Если ЗначениеЗаполнено(ВыборкаНоменклатура.Номенклатура) Тогда
            
            // Выводим левую ячейку строки с наименованием товара
            ОбластьСтрокаНоменклатура.Параметры.Номенклатура = ВыборкаНоменклатура.Номенклатура;
            ТабДок.Вывести(ОбластьСтрокаНоменклатура);
            
            // Теперь обходим периоды для ТЕКУЩЕЙ номенклатуры.
            // Ключевой параметр "ВСЕ" гарантирует вывод всех месяцев шапки по порядку!
            ВыборкаПериодовДанных = ВыборкаНоменклатура.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Период", "ВСЕ");
            
            Пока ВыборкаПериодовДанных.Следующий() Цикл
                
                // Заполняем количество. Если продаж не было, запишем 0
                КолВо = ?(ВыборкаПериодовДанных.Количество = Неопределено, 0, ВыборкаПериодовДанных.Количество);
                ОбластьСтрокаДанные.Параметры.Количество = КолВо;
                
                // Присоединяем ячейку с данными по горизонтали
                ТабДок.Присоединить(ОбластьСтрокаДанные);
                
            КонецЦикла;
            
        КонецЕсли;
        
    КонецЦикла;

КонецПроцедуры

Шаг 5. Полезные советы по доработке отчета

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

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

← На главную