Как правильно рассчитать и вывести промежуточные и общие итоги в отчете 1С на собственном макете

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

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

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

Анализ проблемы: почему не считаются итоги?

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

При реализации этой задачи начинающие разработчики часто совершают три критические ошибки:

  1. Попытка получить данные из выборки после завершения цикла. Когда мы пишем цикл Пока ВыборкаДетальныеЗаписи.Следующий(), платформа последовательно перемещает указатель (курсор) по строкам результата запроса. Как только цикл завершается, метод Следующий() возвращает значение Ложь. Это означает, что указатель вышел за пределы последней строки. Любое обращение к полям выборки (например, ВыборкаДетальныеЗаписи.ПодрезкаИтого) после цикла вернет либо дефолтное значение (например, 0 для чисел), либо вызовет ошибку выполнения.
  2. Смешивание детальных записей, группировок и итогов в одном запросе. Использование конструкции СГРУППИРОВАТЬ ПО вместе со списком всех полей детальных записей делает агрегатные функции (такие как СУММА) бесполезными. Запрос просто возвращает уникальные строки, где сумма по каждой строке равна самому значению этой строки. Конструкция ИТОГИ ПО работает иначе — она добавляет в результат запроса дополнительные иерархические строки итогов.
  3. Ошибки преобразования типов (проблема NULL). Если в базе данных есть записи с незаполненными числовыми реквизитами, база данных возвращает для них значение NULL. Попытка сложить число со значением NULL в коде 1С приводит к ошибке приведения типов: "Операция сложения не определена для данных типов".

Решение №1: Накопление итогов в коде при обходе выборки

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

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

Рассмотрим пример правильной реализации этого алгоритма:


// Инициализируем переменные для накопления итогов
ИтогоПоВсемуОтчету = 0;

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

Пока ВыборкаЛомосдатчик.Следующий() Цикл
    
    // Выводим группировочную область (шапку группы)
    ОбластьГруппировка.Параметры.Заполнить(ВыборкаЛомосдатчик);
    ТабДокумент.Вывести(ОбластьГруппировка);
    
    // Перед обходом детальных записей группы обнуляем промежуточный итог
    ИтогоПоГруппе = 0;
    
    ВыборкаДетальныеЗаписи = ВыборкаЛомосдатчик.Выбрать();
    Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
        
        // Выводим строку с детальными данными
        ОбластьДанные.Параметры.Заполнить(ВыборкаДетальныеЗаписи);
        ТабДокумент.Вывести(ОбластьДанные);
        
        // Накапливаем промежуточный итог по текущей группе
        // Используем функцию Число(), чтобы избежать проблем с неопределенными типами
        ТекущееЗначение = ?(ЗначениеЗаполнено(ВыборкаДетальныеЗаписи.Подрезка), ВыборкаДетальныеЗаписи.Подрезка, 0);
        ИтогоПоГруппе = ИтогоПоГруппе + ТекущееЗначение;
        
    КонецЦикла;
    
    // Теперь, когда цикл по детальным записям завершен, выводим промежуточный подвал
    // Мы берем значение из переменной ИтогоПоГруппе, а не из выборки!
    ОбластьПодвалИтог.Параметры.ИтогоПоКолонке = ИтогоПоГруппе;
    ТабДокумент.Вывести(ОбластьПодвалИтог);
    
    // Накапливаем общий итог по всему отчету
    ИтогоПоВсемуОтчету = ИтогоПоВсемуОтчету + ИтогоПоГруппе;
    
КонецЦикла;

// В самом конце выводим общий подвал отчета
ОбластьПодвал1.Параметры.ИтогоПоЗвену = ИтогоПоВсемуОтчету;
ТабДокумент.Вывести(ОбластьПодвал1);

Важный момент: Обратите внимание на строку ИтогоПоГруппе = ИтогоПоГруппе + ТекущееЗначение;. Мы обезопасили код от получения значения Неопределено или NULL с помощью тернарного оператора ?(). Если поле пустое, мы прибавляем 0.

Решение №2: Использование возможностей языка запросов (ИТОГИ ПО)

Посмотрим на альтернативный способ решения задачи. Платформа 1С позволяет переложить расчет итогов на СУБД с помощью предложения ИТОГИ ПО в тексте запроса. Это более профессиональный подход, так как СУБД оптимизирована для проведения подобных расчетов и выполнит их быстрее, чем код на встроенном языке.

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

Рассмотрим структуру запроса:


ВЫБРАТЬ
    дфСведенияПоЗаготовке.Дата КАК Дата,
    дфСведенияПоЗаготовке.Ломосдатчик КАК Ломосдатчик,
    дфСведенияПоЗаготовке.УНП КАК УНП,
    дфСведенияПоЗаготовке.Подрезка КАК Подрезка
ИЗ
    РегистрСведений.дфСведенияПоЗаготовке КАК дфСведенияПоЗаготовке
ГДЕ
    дфСведенияПоЗаготовке.Дата МЕЖДУ &ДатаНачала И &ДатаКонца
УПОРЯДОЧИТЬ ПО
    Дата
ИТОГИ
    СУММА(ЕСТЬNULL(Подрезка, 0)) КАК ПодрезкаИтого
ПО
    ОБЩИЕ,
    Ломосдатчик

Давайте разберем по шагам особенности этого запроса:

Теперь посмотрим, как правильно написать программный обход такого иерархического результата запроса. При использовании ИТОГИ ПО обход нужно делать строго по группировкам:


РезультатЗапроса = Запрос.Выполнить();

// 1. Получаем самый верхний уровень выборки - ОБЩИЕ ИТОГИ
ВыборкаОбщая = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Если ВыборкаОбщая.Следующий() Тогда
    // Мы позиционировались на строке общего итога. 
    // Значение общего итога уже доступно в ВыборкаОбщая.ПодрезкаИтого
    ОбщийИтог = ВыборкаОбщая.ПодрезкаИтого;
Иначе
    ОбщийИтог = 0;
КонецЕсли;

// 2. Получаем выборку второго уровня - по группировке Ломосдатчик
ВыборкаЛомосдатчик = ВыборкаОбщая.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Ломосдатчик");

Пока ВыборкаЛомосдатчик.Следующий() Цикл
    
    // Выводим шапку группировки
    ОбластьГруппировка.Параметры.НаименованиеЛомосдатчика = ВыборкаЛомосдатчик.Ломосдатчик;
    ТабДокумент.Вывести(ОбластьГруппировка);
    
    // 3. Получаем детальные записи внутри текущего Ломосдатчика
    ВыборкаДетальныеЗаписи = ВыборкаЛомосдатчик.Выбрать();
    Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
        
        // Выводим детальную строку
        ОбластьДанные.Параметры.Заполнить(ВыборкаДетальныеЗаписи);
        ТабДокумент.Вывести(ОбластьДанные);
        
    КонецЦикла;
    
    // Выводим подвал группировки с промежуточным итогом.
    // Значение промежуточного итога уже рассчитано базой данных! 
    // Оно хранится в группировочной выборке ВыборкаЛомосдатчик.
    ОбластьПодвалИтог.Параметры.ИтогоПоКолонке = ВыборкаЛомосдатчик.ПодрезкаИтого;
    ТабДокумент.Вывести(ОбластьПодвалИтог);
    
КонецЦикла;

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

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

Рекомендация: переход на СКД

Хотя ручная работа с макетами полезна для понимания внутренних механизмов платформы "1С:Предприятие 8", мы настоятельно рекомендуем использовать для построения отчетов **Систему компоновки данных (СКД)**.

СКД автоматически решает все описанные проблемы:

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

← На главную