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

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

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

Анализ ошибок в коде ручной группировки

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


Т_М = Новый ТаблицаЗначений;
Т_М.Колонки.Добавить("Номенклатура");
Т_М.Колонки.Добавить("СтавкаНДС");
Т_М.Колонки.Добавить("КоличествоПоступило");
Т_М.Колонки.Добавить("КоличествоПродано");    
Т_М.Колонки.Добавить("Сумма");    
Т_М.Колонки.Добавить("Разница");
Т_М.Колонки.Добавить("СуммаНДС");    
Т_М.Колонки.Добавить("ТаблТЗ");

Для Каждого ТекущаяСтрока Из Т1 Цикл 
    НайденнаяСтрока = Т_М.Найти(ТекущаяСтрока.Номенклатура, "Номенклатура");
    Если НайденнаяСтрока = Неопределено Тогда
        СтрокаОсновная = Т_М.Добавить();
        СтрокаОсновная.Номенклатура = стр.Номенклатура;
        СтрокаОсновная.СтавкаНДС = стр.СтавкаНДС;
        СтрокаОсновная.КоличествоПоступило = стр.КоличествоПоступило;
        СтрокаОсновная.КоличествоПродано = стр.КоличествоПродано;    
        СтрокаОсновная.Сумма = стр.Сумма;    
        СтрокаОсновная.Разница = стр.Разница;    
        СтрокаОсновная.СуммаНДС = стр.СуммаНДС;    
        СтрокаОсновная.ТаблТЗ = Новый ТаблицаЗначений;
        СтрокаОсновная.ТаблТЗ.Колонки.Добавить("СФПолученный");
    Иначе
        НайденнаяСтрока = Т1.Найти(ТекущаяСтрока.Номенклатура, "Номенклатура");
    КонецЕсли;
    
    СтрокаТаблТЗ = СтрокаОсновная.ТаблТЗ.Добавить(); 
    СтрокаТаблТЗ.СФПолученный = ТекущаяСтрока.СФПолученный;
КонецЦикла;

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

  1. Обращение к чужой переменной: При добавлении строки реквизиты заполняются из неопределенной внешней переменной стр вместо текущей итерационной переменной ТекущаяСтрока. Если переменная стр была инициализирована где-то выше первой строкой, именно ее значения будут бесконечно копироваться во все новые строки.
  2. Потеря контекста текущей родительской строки: В ветке Иначе переменной СтрокаОсновная не присваивается найденная строка таблицы Т_М. Вместо этого вызывается бессмысленный повторный поиск по исходной таблице Т1. В результате переменная СтрокаОсновная продолжает хранить ссылку на строку из предыдущей итерации, и подчиненные данные добавляются не в найденную группу, а в ту строку, которая была создана последней.

Способ 1. Исправление ручного алгоритма

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


Т_М = Новый ТаблицаЗначений;
Т_М.Колонки.Добавить("Номенклатура");
Т_М.Колонки.Добавить("СтавкаНДС");
Т_М.Колонки.Добавить("КоличествоПоступило");
Т_М.Колонки.Добавить("КоличествоПродано");    
Т_М.Колонки.Добавить("Сумма");    
Т_М.Колонки.Добавить("Разница");
Т_М.Колонки.Добавить("СуммаНДС");    
Т_М.Колонки.Добавить("ТаблТЗ");

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

Способ 2. Использование Дерева Значений (рекомендуемый подход для иерархии)

Хранение объекта ТаблицаЗначений внутри ячейки другой таблицы значений считается устаревшей практикой, доставшейся в наследство от платформы 1С 7.7. В 1С:Предприятие 8 для многоуровневых структур данных предназначен штатный объект ДеревоЗначений.

Рассмотрим преимущества использования дерева значений:

  1. Единый состав колонок для всех уровней иерархии.
  2. Простая навигация по подчиненным строкам через коллекцию Строки.
  3. Возможность прямого отображения на управляемой форме через тип ДанныеФормыДерево.

Посмотрим на пример построения двухуровневой структуры с помощью ДеревоЗначений:


Дерево = Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Номенклатура");
Дерево.Колонки.Добавить("СтавкаНДС");
Дерево.Колонки.Добавить("КоличествоПоступило");
Дерево.Колонки.Добавить("КоличествоПродано");
Дерево.Колонки.Добавить("Сумма");
Дерево.Колонки.Добавить("Разница");
Дерево.Колонки.Добавить("СуммаНДС");
Дерево.Колонки.Добавить("СФПолученный");

// Для быстрого поиска родительских строк используем Соответствие
СоответствиеГрупп = Новый Соответствие;

Для Каждого ТекущаяСтрока Из Т1 Цикл
    СтрокаРодитель = СоответствиеГрупп[ТекущаяСтрока.Номенклатура];
    
    Если СтрокаРодитель = Неопределено Тогда
        СтрокаРодитель = Дерево.Строки.Добавить();
        ЗаполнитьЗначенияСвойств(СтрокаРодитель, ТекущаяСтрока);
        СоответствиеГрупп.Вставить(ТекущаяСтрока.Номенклатура, СтрокаРодитель);
    КонецЕсли;
    
    СтрокаПодчиненная = СтрокаРодитель.Строки.Добавить();
    СтрокаПодчиненная.СФПолученный = ТекущаяСтрока.СФПолученный;
КонецЦикла;

Важный момент оптимизации: Использование объекта Соответствие вместо метода Найти() позволяет осуществлять поиск родительской строки со скоростью $O(1)$, что кардинально ускоряет обработку больших массивов данных.

Способ 3. Получение иерархии через временные таблицы и итоги в запросе

Если исходные данные получены из нескольких источников или таблиц, наиболее производительный и лаконичный путь — передать эти таблицы в качестве параметров нового запроса, объединить их и рассчитать итоги средствами СУБД.

Разберем пример реализации через язык запросов:


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

Запрос.УстановитьПараметр("ТаблицаДанных", Т1);

// Выгрузка результата сразу формирует готовое ДеревоЗначений с итогами
ДеревоРезультат = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);

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

← На главную