Как определить последнюю строку группы документов при обходе таблицы значений в 1С?

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

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

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

Рассмотрим пошагово каждый из подходов, их достоинства, недостатки и примеры реализации.

---

Способ №1. Проверка следующего элемента через индекс (с учетом ленивых вычислений)

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

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

Проанализируем следующий пример кода:


КоличествоСтрок = ТЗ.Количество();
Для Индекс = 0 По КоличествоСтрок - 1 Цикл
    ТекущаяСтрока = ТЗ[Индекс];
    ТекущийДокумент = ТекущаяСтрока.РегистраторДокументПродажи;
    
    // Выполняем заполнение основных данных строки табличного документа
    // ... (код заполнения параметров области)
    
    // Проверяем, является ли текущая строка последней в группе документов
    Если (Индекс = КоличествоСтрок - 1) ИЛИ (ТекущийДокумент <> ТЗ[Индекс + 1].РегистраторДокументПродажи) Тогда
        // Это последняя строка в группе документов!
        // Заполняем колонку с итогом или бонусом
        ТекущаяСтрока.БонуснаяНакладная = "Итог по документу";
        
        // Если рассчитываем показатели на лету:
        // ОбластьСтрока.Параметры.РОИ = РассчитатьРОИ(СуммаЗаказа, ПредоставленоСкидки);
    КонецЕсли;
    
    // Выводим строку в табличный документ
    ТабДок.Вывести(ОбластьСтрока);
КонецЦикла;

Важный нюанс: механизм ленивых вычислений (Short-circuit evaluation). Обратите внимание на условие проверки:

Если (Индекс = КоличествоСтрок - 1) ИЛИ (ТекущийДокумент <> ТЗ[Индекс + 1].РегистраторДокументПродажи) Тогда

Платформа 1С использует ленивое вычисление логических выражений. Если первая часть условия перед оператором ИЛИ возвращает Истина (то есть мы находимся на самой последней строке таблицы значений), то вычисление второй части выражения даже не начинается. Благодаря этому не происходит обращения к несуществующему индексу Индекс + 1, и программа не падает с ошибкой "Индекс находится за границами массива". Если вы поменяете эти условия местами, программа гарантированно завершится с ошибкой при обработке последней строки.

Критическая ловушка производительности! Избегайте использования метода ТЗ.Индекс(Строка) внутри цикла Для Каждого. Посмотрим на типичную ошибку начинающих разработчиков:


// ТАК ДЕЛАТЬ НЕЛЬЗЯ! Квадратичная сложность O(N^2)
Для Каждого СтрокаТЗ Из ТЗ Цикл
    ТекущийИндекс = ТЗ.Индекс(СтрокаТЗ); // Метод перебирает ТЗ с начала для поиска индекса!
    Если (ТекущийИндекс = ТЗ.Количество() - 1) ИЛИ (СтрокаТЗ.Документ <> ТЗ[ТекущийИндекс + 1].Документ) Тогда
        // Код обработки
    КонецЕсли;
КонецЦикла;

Метод Индекс() у таблицы значений не хранит позицию строки в памяти как константу. Чтобы найти индекс, платформа вынуждена каждый раз выполнять линейный поиск, перебирая строки таблицы с самого начала. При обходе таблицы из 10 000 строк этот цикл выполнит порядка 50 миллионов операций сравнения, что приведет к полному зависанию системы. Используйте только обход по целочисленному счетчику (цикл Для... По...).

---

Способ №2. Безопасный однопроходный обход с буферизацией (сравнение "назад")

Чтобы полностью исключить риск возникновения ошибок с выходом индекса за границы массива и сделать код максимально понятным и быстрым (со сложностью алгоритма $O(N)$), применим метод сравнения с предыдущим элементом. Мы будем двигаться по циклу Для Каждого, накапливать данные и фиксировать итоги "задним числом" в момент, когда обнаруживаем смену документа.

Разберем этот алгоритм по шагам:

  1. Создаем переменную для хранения ссылки на документ предыдущей строки (изначально Неопределено).
  2. Создаем переменную для хранения ссылки на саму строку таблицы значений, которая потенциально является последней в текущей группе (буферная строка).
  3. В цикле сравниваем документ текущей строки с предыдущим. Если они не совпадают (и предыдущий документ уже был заполнен), значит, мы перешли на новую группу. Соответственно, строка в буфере — это последняя строка предыдущей группы. Мы обрабатываем её (записываем итоги, выводим в табличный документ).
  4. После окончания цикла обрабатываем буферную строку для самой последней группы в таблице значений, так как для неё событие смены группы в цикле уже не наступит.

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


ПредыдущийДокумент = Неопределено;
СтрокаДляИтога = Неопределено;

// Накапливаемые показатели по группе документов
СуммаГруппы = 0;
СкидкаГруппы = 0;

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

// ВАЖНО: обрабатываем самую последнюю строку всей таблицы значений
Если СтрокаДляИтога <> Неопределено Тогда
    СтрокаДляИтога.БонуснаяНакладная = "Итог: " + СуммаГруппы;
    // ТабДок.Вывести(ОбластьСтрока);
КонецЕсли;

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

---

Способ №3. Архитектурно правильное решение средствами СУБД (Итоги в запросе)

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

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

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


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

Запрос.УстановитьПараметр("НачДата", НачДата);
Запрос.УстановитьПараметр("КонДата", КонДата);

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

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


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

Преимущества работы через выборку запроса:

---

Способ №4. Использование СКД (Системы компоновки данных)

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

Для реализации нашей задачи в СКД достаточно выполнить следующие простые настройки:

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

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

---

Сводная таблица сравнения методов

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

Метод Сложность алгоритма Безопасность (риск ошибок) Рекомендуемая область применения
1. Сравнение со следующей строкой ТЗ[Индекс + 1] $O(N)$ (при использовании правильного цикла по индексу) Средняя (требуется строгий контроль ленивых вычислений и последней строки) Быстрые локальные доработки существующего legacy-кода, где уже используется обход ТЗ.
2. Буферизация и сравнение "назад" $O(N)$ (один линейный проход) Высокая (нет индексных операций, ошибки выхода за границы массива исключены) Универсальный алгоритм для обработки любых коллекций данных в памяти (ТЗ, Дерево значений, Массивы).
3. Итоги в запросе (двухуровневая выборка) $O(N)$ на стороне 1С, СУБД оптимизирует группировку Максимальная (структура выборки гарантирует правильное разделение на группы) Создание новых сложных печатных форм и отчетов, формируемых вручную через код.
4. Разработка отчета на СКД Оптимизировано платформой Абсолютная безопасность (код не пишется вручную) Разработка любых пользовательских отчетов с группировками и итогами. Стандарт современной разработки в 1С.

Выбирайте метод исходя из контекста вашей задачи. Если вы поддерживаете старый отчёт с ручным выводом областей — используйте однопроходный обход с буферизацией (Способ №2). Если вы пишете функционал с нуля — всегда отдавайте предпочтение запросам с итогами (Способ №3) или полноценной разработке отчета на СКД (Способ №4).

← На главную