Как универсально перебрать табличные части документа в 1С и избежать ошибки "Итератор для значения не определен"?

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

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

Понимание причины ошибки "Итератор для значения не определен"

Рассмотрим исходный код, который привел к ошибке:


Для Каждого ТабличнаяЧастьДокумента Из ЭтотОбъект.Метаданные.ТабличныеЧасти Цикл
    Движения.ДоходПоНоменклатурам.Записывать = Истина;
    Для Каждого СтрокаТабЧасти Из ТабличнаяЧастьДокумента Цикл // Здесь возникает ошибка
        Движение = Движения.ДоходПоНоменклатурам.Добавить;
        Движение.ВидДвижения = ВидДвиженияНакопления.Приход;
        Движение.Период = Дата;
        Движение.Номенклатура = СтрокаТабЧасти.Номенклатура;
        Если ВалютныйУчет=Истина Тогда
            Движение.Сумма =СтрокаТабЧасти.Сумма*ТекущийКурс;
        Иначе
            Движение.Сумма = СтрокаТабЧасти.Сумма;
        КонецЕсли;
    КонецЦикла;
КонецЦикла;

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

Универсальный перебор табличных частей через метаданные

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

  1. Мы начинаем с первого цикла, который перебирает описания табличных частей из метаданных документа: Для Каждого ОписаниеТабличнойЧасти Из ЭтотОбъект.Метаданные.ТабличныеЧасти Цикл.
  2. Внутри этого цикла мы получаем имя текущей табличной части. Имя находится в свойстве Имя объекта ОбъектМетаданныхТабличнаяЧасть: ИмяТекущейТабличнойЧасти = ОписаниеТабличнойЧасти.Имя;.
  3. Теперь, имея имя табличной части, мы можем обратиться к ней как к свойству объекта документа, используя синтаксис доступа по имени: ТабличнаяЧастьОбъекта = ЭтотОбъект[ИмяТекущейТабличнойЧасти];. Переменная ТабличнаяЧастьОбъекта теперь будет содержать коллекцию строк конкретной табличной части документа.
  4. После этого мы можем безопасно использовать второй цикл для перебора строк этой коллекции: Для Каждого СтрокаТабЧасти Из ТабличнаяЧастьОбъекта Цикл.

Применим этот подход к нашему исходному примеру, чтобы исправить ошибку:


// В модуле объекта документа "Продажа", например, в обработчике "ОбработкаПроведения"

// Флаг для однократной установки Записывать = Истина;
Движения.ДоходПоНоменклатурам.Записывать = Истина;

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

            Если ТабличнаяЧастьДокумента.Метаданные().Реквизиты.Найти("Сумма") <> Неопределено Тогда
                Если ВалютныйУчет=Истина Тогда
                    Движение.Сумма =СтрокаТабЧасти.Сумма*ТекущийКурс;
                Иначе
                    Движение.Сумма = СтрокаТабЧасти.Сумма;
                КонецЕсли;
            Иначе
                Сообщить("В табличной части '" + ИмяТабличнойЧасти + "' отсутствует реквизит 'Сумма'.");
                Продолжить;
            КонецЕсли;
            
            // Если нужно установить другие общие реквизиты движений, например, ДокументОснование
            Движение.ДокументОснование = ЭтотОбъект.Ссылка;

        КонецЦикла;
    КонецЕсли;
КонецЦикла;

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

Прямое обращение к табличным частям документа

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

Рассмотрим пример с прямым обращением к двум табличным частям: "Товары" и "Услуги" (предполагая, что они существуют в документе "Продажа"):


// В модуле объекта документа "Продажа", например, в обработчике "ОбработкаПроведения"

Движения.ДоходПоНоменклатурам.Записывать = Истина;

// Обработка табличной части "Товары"
Для Каждого СтрокаТЧ_Товары Из ЭтотОбъект.Товары Цикл
    Движение = Движения.ДоходПоНоменклатурам.Добавить;
    Движение.ВидДвижения = ВидДвиженияНакопления.Приход;
    Движение.Период = Дата;
    Движение.Номенклатура = СтрокаТЧ_Товары.Номенклатура;
    Если ВалютныйУчет=Истина Тогда
        Движение.Сумма = СтрокаТЧ_Товары.Сумма*ТекущийКурс;
    Иначе
        Движение.Сумма = СтрокаТЧ_Товары.Сумма;
    КонецЕсли;
    Движение.ДокументОснование = ЭтотОбъект.Ссылка;
КонецЦикла;

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

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


// В модуле объекта документа "Продажа"

Процедура ОбработатьСтрокуТабличнойЧастиДляДвижений(СтрокаТабличнойЧасти, ВалютныйУчет, ТекущийКурс, Дата, ДокументОснование, Движения)
    Движение = Движения.ДоходПоНоменклатурам.Добавить;
    Движение.ВидДвижения = ВидДвиженияНакопления.Приход;
    Движение.Период = Дата;
    // Используем динамический доступ, если реквизиты могут отличаться,
    // или проверяем их наличие. В простом случае, как здесь, предполагаем их наличие.
    Движение.Номенклатура = СтрокаТабличнойЧасти.Номенклатура; 
    Если ВалютныйУчет=Истина Тогда
        Движение.Сумма = СтрокаТабличнойЧасти.Сумма*ТекущийКурс;
    Иначе
        Движение.Сумма = СтрокаТабличнойЧасти.Сумма;
    КонецЕсли;
    Движение.ДокументОснование = ДокументОснование;
КонецПроцедуры

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

    Для Каждого СтрокаТЧ_Товары Из ЭтотОбъект.Товары Цикл
        ОбработатьСтрокуТабличнойЧастиДляДвижений(СтрокаТЧ_Товары, ВалютныйУчет, ТекущийКурс, Дата, ЭтотОбъект.Ссылка, Движения);
    КонецЦикла;

    Для Каждого СтрокаТЧ_Услуги Из ЭтотОбъект.Услуги Цикл
        ОбработатьСтрокуТабличнойЧастиДляДвижений(СтрокаТЧ_Услуги, ВалютныйУчет, ТекущийКурс, Дата, ЭтотОбъект.Ссылка, Движения);
    КонецЦикла;
    
    // ... Ваш код завершения ...
КонецПроцедуры

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

Обработка табличных частей с помощью запросов

Одним из наиболее мощных и производительных способов обработки данных табличных частей, особенно при больших объемах данных или необходимости сложной выборки, группировки или объединения, является использование запросов 1С. Запросы позволяют выбрать необходимые данные непосредственно из базы данных, минимизируя нагрузку на оперативную память и CPU сервера 1С:Предприятие.

Основные принципы использования запросов для табличных частей:

  1. Обращение к табличной части как к источнику данных: В запросе мы можем напрямую указать табличную часть документа как источник. Например, для табличной части "Товары" документа "Продажа" синтаксис будет выглядеть так: ИЗ Документ.Продажа.Товары КАК Товары.
  2. Выборка необходимых полей: Мы выбираем только те поля, которые нам действительно нужны для движений.
  3. Объединение нескольких табличных частей: Если нам нужно обработать данные из нескольких табличных частей как единый набор, мы используем оператор ОБЪЕДИНИТЬ или ОБЪЕДИНИТЬ ВСЕ.
    • ОБЪЕДИНИТЬ: Удаляет дублирующиеся строки.
    • ОБЪЕДИНИТЬ ВСЕ: Включает все строки, в том числе дублирующиеся, что часто более производительно, так как не требуется дополнительная операция по уникализации.
    При объединении важно, чтобы поля, выбираемые из каждой табличной части, имели одинаковый тип и порядок следования. Мы можем также добавить дополнительное поле для идентификации источника строки (например, "ТипТабличнойЧасти").
  4. Загрузка результата в движения: Результат запроса можно загрузить в коллекцию движений или перебрать его для создания движений.

Давайте посмотрим на пример запроса, который объединяет данные из двух табличных частей ("Товары" и "Услуги") документа "Продажа" и формирует на их основе движения:


// В модуле объекта документа "Продажа", например, в обработчике "ОбработкаПроведения"

Движения.ДоходПоНоменклатурам.Записывать = Истина;

Запрос = Новый Запрос;
Запрос.Текст = 
"ВЫБРАТЬ
|    &Дата КАК Период,
|    ВидДвиженияНакопления.Приход КАК ВидДвижения,
|    Товары.Номенклатура КАК Номенклатура,
|    Товары.Сумма * ВЫБОР КОГДА &ВалютныйУчет ТОГДА &ТекущийКурс ИНАЧЕ 1 КОНЕЦ КАК Сумма,
|    &ДокументОснование КАК ДокументОснование
|ИЗ
|    Документ.Продажа.Товары КАК Товары
|ГДЕ
|    Товары.Ссылка = &СсылкаДокумента

|ОБЪЕДИНИТЬ ВСЕ

|ВЫБРАТЬ
|    &Дата,
|    ВидДвиженияНакопления.Приход,
|    Услуги.Номенклатура,
|    Услуги.Сумма * ВЫБОР КОГДА &ВалютныйУчет ТОГДА &ТекущийКурс ИНАЧЕ 1 КОНЕЦ,
|    &ДокументОснование
|ИЗ
|    Документ.Продажа.Услуги КАК Услуги
|ГДЕ
|    Услуги.Ссылка = &СсылкаДокумента";

// Устанавливаем параметры запроса
Запрос.УстановитьПараметр("Дата", Дата);
Запрос.УстановитьПараметр("ВалютныйУчет", ВалютныйУчет);
Запрос.УстановитьПараметр("ТекущийКурс", ТекущийКурс);
Запрос.УстановитьПараметр("СсылкаДокумента", ЭтотОбъект.Ссылка);
Запрос.УстановитьПараметр("ДокументОснование", ЭтотОбъект.Ссылка); // Может быть равен СсылкаДокумента

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

Пока Выборка.Следующий() Цикл
    Движение = Движения.ДоходПоНоменклатурам.Добавить();
    Движение.Период = Выборка.Период;
    Движение.ВидДвижения = Выборка.ВидДвижения;
    Движение.Номенклатура = Выборка.Номенклатура;
    Движение.Сумма = Выборка.Сумма;
    Движение.ДокументОснование = Выборка.ДокументОснование;
КонецЦикла;

Этот запрос выбирает данные из двух табличных частей, применяет логику пересчета валютной суммы прямо в запросе и объединяет результаты. Затем мы перебираем результат запроса и создаем движения. Такой подход очень эффективен, так как все агрегации и фильтрации выполняются на уровне СУБД.

Временные таблицы в запросах

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


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

|ОБЪЕДИНИТЬ ВСЕ

|ВЫБРАТЬ
|    ТЧ.Ссылка,
|    ТЧ.Номенклатура,
|    ТЧ.Сумма,
|    ""Услуги""
|ИЗ
|    Документ.Продажа.Услуги КАК ТЧ
|ГДЕ
|    ТЧ.Ссылка = &СсылкаДокумента
|;
////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
|    &Дата КАК Период,
|    ВидДвиженияНакопления.Приход КАК ВидДвижения,
|    ВТ.Номенклатура КАК Номенклатура,
|    ВТ.Сумма * ВЫБОР КОГДА &ВалютныйУчет ТОГДА &ТекущийКурс ИНАЧЕ 1 КОНЕЦ КАК Сумма,
|    &ДокументОснование КАК ДокументОснование,
|    ВТ.ТипТЧ КАК ТипТабличнойЧасти
|ИЗ
|    ВТ_ВсеСтрокиТЧ КАК ВТ";

// Устанавливаем параметры запроса (аналогично предыдущему примеру)
Запрос.УстановитьПараметр("Дата", Дата);
Запрос.УстановитьПараметр("ВалютныйУчет", ВалютныйУчет);
Запрос.УстановитьПараметр("ТекущийКурс", ТекущийКурс);
Запрос.УстановитьПараметр("СсылкаДокумента", ЭтотОбъект.Ссылка);
Запрос.УстановитьПараметр("ДокументОснование", ЭтотОбъект.Ссылка);

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

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

Использование запросов особенно эффективно при необходимости:

Выбор оптимального подхода

Выбор способа перебора табличных частей зависит от конкретной задачи и требований:

  1. Универсальный перебор через метаданные (ЭтотОбъект.Метаданные.ТабличныеЧасти):
    • Идеален, когда мы хотим обработать все существующие и потенциально будущие табличные части документа, не меняя код при их добавлении.
    • Требует более внимательного отношения к различиям в структуре табличных частей (наличие реквизитов).
    • Может быть чуть менее производительным из-за динамического доступа по имени, но для большинства задач на уровне модуля объекта это некритично.
  2. Прямое обращение по имени (ЭтотОбъект.ИмяТабличнойЧасти):
    • Самый простой и наглядный способ, если количество табличных частей невелико и их имена заранее известны.
    • Рекомендуется выносить повторяющуюся логику в отдельные процедуры для улучшения читаемости и поддержки кода.
    • Требует модификации кода при добавлении новых табличных частей, которые также должны быть обработаны.
  3. Обработка с помощью запросов:
    • Наиболее производительный подход для больших объемов данных или сложных сценариев обработки (фильтрация, группировка, объединение с другими источниками).
    • Использует ресурсы СУБД, что снижает нагрузку на сервер 1С:Предприятие.
    • Требует хорошего знания языка запросов 1С.
    • Является предпочтительным решением согласно стандартам разработки 1С для обработчиков проведения, когда требуется формировать движения.

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

← На главную