В процессе разработки конфигураций на платформе 1С:Предприятие часто возникает задача по обработке данных из табличных частей документов. Иногда требуется реализовать универсальный механизм, который мог бы перебирать все табличные части документа, не зная заранее их имен. Однако, при попытке использовать метаданные документа для такого перебора, разработчики могут столкнуться с распространенной ошибкой: "Итератор для значения не определен". Давайте разберем причину этой ошибки и рассмотрим различные способы ее исправления, а также другие подходы к обработке табличных частей.
Рассмотрим исходный код, который привел к ошибке:
Для Каждого ТабличнаяЧастьДокумента Из ЭтотОбъект.Метаданные.ТабличныеЧасти Цикл
Движения.ДоходПоНоменклатурам.Записывать = Истина;
Для Каждого СтрокаТабЧасти Из ТабличнаяЧастьДокумента Цикл // Здесь возникает ошибка
Движение = Движения.ДоходПоНоменклатурам.Добавить;
Движение.ВидДвижения = ВидДвиженияНакопления.Приход;
Движение.Период = Дата;
Движение.Номенклатура = СтрокаТабЧасти.Номенклатура;
Если ВалютныйУчет=Истина Тогда
Движение.Сумма =СтрокаТабЧасти.Сумма*ТекущийКурс;
Иначе
Движение.Сумма = СтрокаТабЧасти.Сумма;
КонецЕсли;
КонецЦикла;
КонецЦикла;
Причина ошибки кроется в том, что в первом цикле Для Каждого ТабличнаяЧастьДокумента Из ЭтотОбъект.Метаданные.ТабличныеЧасти переменная ТабличнаяЧастьДокумента получает объект типа ОбъектМетаданныхТабличнаяЧасть. Этот объект является описанием структуры табличной части — он содержит информацию о ее имени, реквизитах и других свойствах, но не сами данные, хранящиеся в документе. Когда мы пытаемся использовать этот объект в следующем цикле Для Каждого СтрокаТабЧасти Из ТабличнаяЧастьДокумента Цикл, система 1С ожидает, что ТабличнаяЧастьДокумента будет коллекцией элементов (например, строк табличной части), по которой можно итерировать. Однако ОбъектМетаданныхТабличнаяЧасть не является такой коллекцией и не имеет внутреннего итератора, что и приводит к ошибке "Итератор для значения не определен".
Для того чтобы корректно перебрать данные всех табличных частей документа универсальным способом, нам необходимо использовать информацию из метаданных (а именно имя табличной части) для обращения к фактической коллекции строк этой табличной части в объекте документа. Рассмотрим, как это сделать по шагам:
Для Каждого ОписаниеТабличнойЧасти Из ЭтотОбъект.Метаданные.ТабличныеЧасти Цикл.Имя объекта ОбъектМетаданныхТабличнаяЧасть: ИмяТекущейТабличнойЧасти = ОписаниеТабличнойЧасти.Имя;.ТабличнаяЧастьОбъекта = ЭтотОбъект[ИмяТекущейТабличнойЧасти];. Переменная ТабличнаяЧастьОбъекта теперь будет содержать коллекцию строк конкретной табличной части документа.Для Каждого СтрокаТабЧасти Из ТабличнаяЧастьОбъекта Цикл.Применим этот подход к нашему исходному примеру, чтобы исправить ошибку:
// В модуле объекта документа "Продажа", например, в обработчике "ОбработкаПроведения"
// Флаг для однократной установки Записывать = Истина;
Движения.ДоходПоНоменклатурам.Записывать = Истина;
Для Каждого ОписаниеТабличнойЧасти Из ЭтотОбъект.Метаданные.ТабличныеЧасти Цикл
// Получаем имя текущей табличной части
ИмяТабличнойЧасти = ОписаниеТабличнойЧасти.Имя;
// Получаем саму табличную часть из объекта документа по ее имени
// Здесь мы обращаемся к коллекции данных, а не к метаданным
ТабличнаяЧастьДокумента = ЭтотОбъект[ИмяТабличнойЧасти];
// Проверяем, что это действительно коллекция строк (табличная часть), а не что-то другое
// Это может быть полезно, если в документе есть динамические свойства,
// которые можно получить по имени, но они не являются табличными частями.
// Однако, в данном случае, ЭтотОбъект.Метаданные.ТабличныеЧасти
// уже гарантирует, что мы работаем с описаниями ТЧ.
Если ТипЗнч(ТабличнаяЧастьДокумента) = Тип("ТабличнаяЧастьДокумента") Тогда
Для Каждого СтрокаТабЧасти Из ТабличнаяЧастьДокумента Цикл
// Теперь СтрокаТабЧасти - это объект типа "СтрокаТабличнойЧастиДокумента",
// который содержит данные конкретной строки.
Движение = Движения.ДоходПоНоменклатурам.Добавить;
Движение.ВидДвижения = ВидДвиженияНакопления.Приход;
Движение.Период = Дата;
// Предполагаем, что во всех табличных частях есть реквизит "Номенклатура"
// В реальных условиях может потребоваться дополнительная проверка наличия реквизита
// или более сложная логика, если ТЧ имеют разный состав реквизитов.
Если ТабличнаяЧастьДокумента.Метаданные().Реквизиты.Найти("Номенклатура") <> Неопределено Тогда
Движение.Номенклатура = СтрокаТабЧасти.Номенклатура;
Иначе
// Обработка случая, когда реквизит "Номенклатура" отсутствует
Сообщить("В табличной части '" + ИмяТабличнойЧасти + "' отсутствует реквизит 'Номенклатура'.");
Продолжить; // Переходим к следующей строке или ТЧ
КонецЕсли;
Если ТабличнаяЧастьДокумента.Метаданные().Реквизиты.Найти("Сумма") <> Неопределено Тогда
Если ВалютныйУчет=Истина Тогда
Движение.Сумма =СтрокаТабЧасти.Сумма*ТекущийКурс;
Иначе
Движение.Сумма = СтрокаТабЧасти.Сумма;
КонецЕсли;
Иначе
Сообщить("В табличной части '" + ИмяТабличнойЧасти + "' отсутствует реквизит 'Сумма'.");
Продолжить;
КонецЕсли;
// Если нужно установить другие общие реквизиты движений, например, ДокументОснование
Движение.ДокументОснование = ЭтотОбъект.Ссылка;
КонецЦикла;
КонецЕсли;
КонецЦикла;
Этот подход является наиболее универсальным, так как он позволяет обрабатывать все табличные части документа, даже если их состав меняется или добавляются новые. Однако, следует быть внимательными к тому, что разные табличные части могут иметь разный набор реквизитов, и код внутри внутреннего цикла должен это учитывать, чтобы избежать ошибок обращения к несуществующим полям.
Если количество табличных частей невелико, и их имена известны заранее, гораздо более простым и часто более читаемым способом будет прямое обращение к каждой табличной части по ее имени. Этот подход не требует работы с метаданными и является более производительным, поскольку исключает дополнительные шаги по получению имени и динамическому доступу.
Рассмотрим пример с прямым обращением к двум табличным частям: "Товары" и "Услуги" (предполагая, что они существуют в документе "Продажа"):
// В модуле объекта документа "Продажа", например, в обработчике "ОбработкаПроведения"
Движения.ДоходПоНоменклатурам.Записывать = Истина;
// Обработка табличной части "Товары"
Для Каждого СтрокаТЧ_Товары Из ЭтотОбъект.Товары Цикл
Движение = Движения.ДоходПоНоменклатурам.Добавить;
Движение.ВидДвижения = ВидДвиженияНакопления.Приход;
Движение.Период = Дата;
Движение.Номенклатура = СтрокаТЧ_Товары.Номенклатура;
Если ВалютныйУчет=Истина Тогда
Движение.Сумма = СтрокаТЧ_Товары.Сумма*ТекущийКурс;
Иначе
Движение.Сумма = СтрокаТЧ_Товары.Сумма;
КонецЕсли;
Движение.ДокументОснование = ЭтотОбъект.Ссылка;
КонецЦикла;
// Обработка табличной части "Услуги"
Для Каждого СтрокаТЧ_Услуги Из ЭтотОбъект.Услуги Цикл
Движение = Движения.ДоходПоНоменклатурам.Добавить;
Движение.ВидДвижения = ВидДвиженияНакопления.Приход;
Движение.Период = Дата;
Движение.Номенклатура = СтрокаТЧ_Услуги.Номенклатура; // Предполагаем, что и здесь есть Номенклатура
Если ВалютныйУчет=Истина Тогда
Движение.Сумма = СтрокаТЧ_Услуги.Сумма*ТекущийКурс;
Иначе
Движение.Сумма = СтрокаТЧ_Услуги.Сумма;
КонецЕсли;
Движение.ДокументОснование = ЭтотОбъект.Ссылка;
КонецЦикла;
При таком подходе возникает дублирование кода, если логика обработки строк одинакова для разных табличных частей. Для соблюдения стандартов разработки и повышения читаемости, мы можем вынести повторяющийся код в отдельную процедуру или функцию. Например:
// В модуле объекта документа "Продажа"
Процедура ОбработатьСтрокуТабличнойЧастиДляДвижений(СтрокаТабличнойЧасти, ВалютныйУчет, ТекущийКурс, Дата, ДокументОснование, Движения)
Движение = Движения.ДоходПоНоменклатурам.Добавить;
Движение.ВидДвижения = ВидДвиженияНакопления.Приход;
Движение.Период = Дата;
// Используем динамический доступ, если реквизиты могут отличаться,
// или проверяем их наличие. В простом случае, как здесь, предполагаем их наличие.
Движение.Номенклатура = СтрокаТабличнойЧасти.Номенклатура;
Если ВалютныйУчет=Истина Тогда
Движение.Сумма = СтрокаТабличнойЧасти.Сумма*ТекущийКурс;
Иначе
Движение.Сумма = СтрокаТабличнойЧасти.Сумма;
КонецЕсли;
Движение.ДокументОснование = ДокументОснование;
КонецПроцедуры
Процедура ОбработкаПроведения(...)
// ... Ваш код инициализации (ВалютныйУчет, ТекущийКурс и т.д.) ...
Движения.ДоходПоНоменклатурам.Записывать = Истина;
Для Каждого СтрокаТЧ_Товары Из ЭтотОбъект.Товары Цикл
ОбработатьСтрокуТабличнойЧастиДляДвижений(СтрокаТЧ_Товары, ВалютныйУчет, ТекущийКурс, Дата, ЭтотОбъект.Ссылка, Движения);
КонецЦикла;
Для Каждого СтрокаТЧ_Услуги Из ЭтотОбъект.Услуги Цикл
ОбработатьСтрокуТабличнойЧастиДляДвижений(СтрокаТЧ_Услуги, ВалютныйУчет, ТекущийКурс, Дата, ЭтотОбъект.Ссылка, Движения);
КонецЦикла;
// ... Ваш код завершения ...
КонецПроцедуры
Такой подход делает код более чистым и легким для поддержки. Однако, если добавится новая табличная часть, которую также нужно обрабатывать, придется вручную добавлять для нее новый цикл и вызов процедуры.
Одним из наиболее мощных и производительных способов обработки данных табличных частей, особенно при больших объемах данных или необходимости сложной выборки, группировки или объединения, является использование запросов 1С. Запросы позволяют выбрать необходимые данные непосредственно из базы данных, минимизируя нагрузку на оперативную память и CPU сервера 1С:Предприятие.
ИЗ Документ.Продажа.Товары КАК Товары.ОБЪЕДИНИТЬ или ОБЪЕДИНИТЬ ВСЕ.
ОБЪЕДИНИТЬ: Удаляет дублирующиеся строки.ОБЪЕДИНИТЬ ВСЕ: Включает все строки, в том числе дублирующиеся, что часто более производительно, так как не требуется дополнительная операция по уникализации.Давайте посмотрим на пример запроса, который объединяет данные из двух табличных частей ("Товары" и "Услуги") документа "Продажа" и формирует на их основе движения:
// В модуле объекта документа "Продажа", например, в обработчике "ОбработкаПроведения"
Движения.ДоходПоНоменклатурам.Записывать = Истина;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| &Дата КАК Период,
| ВидДвиженияНакопления.Приход КАК ВидДвижения,
| Товары.Номенклатура КАК Номенклатура,
| Товары.Сумма * ВЫБОР КОГДА &ВалютныйУчет ТОГДА &ТекущийКурс ИНАЧЕ 1 КОНЕЦ КАК Сумма,
| &ДокументОснование КАК ДокументОснование
|ИЗ
| Документ.Продажа.Товары КАК Товары
|ГДЕ
| Товары.Ссылка = &СсылкаДокумента
|ОБЪЕДИНИТЬ ВСЕ
|ВЫБРАТЬ
| &Дата,
| ВидДвиженияНакопления.Приход,
| Услуги.Номенклатура,
| Услуги.Сумма * ВЫБОР КОГДА &ВалютныйУчет ТОГДА &ТекущийКурс ИНАЧЕ 1 КОНЕЦ,
| &ДокументОснование
|ИЗ
| Документ.Продажа.Услуги КАК Услуги
|ГДЕ
| Услуги.Ссылка = &СсылкаДокумента";
// Устанавливаем параметры запроса
Запрос.УстановитьПараметр("Дата", Дата);
Запрос.УстановитьПараметр("ВалютныйУчет", ВалютныйУчет);
Запрос.УстановитьПараметр("ТекущийКурс", ТекущийКурс);
Запрос.УстановитьПараметр("СсылкаДокумента", ЭтотОбъект.Ссылка);
Запрос.УстановитьПараметр("ДокументОснование", ЭтотОбъект.Ссылка); // Может быть равен СсылкаДокумента
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
Движение = Движения.ДоходПоНоменклатурам.Добавить();
Движение.Период = Выборка.Период;
Движение.ВидДвижения = Выборка.ВидДвижения;
Движение.Номенклатура = Выборка.Номенклатура;
Движение.Сумма = Выборка.Сумма;
Движение.ДокументОснование = Выборка.ДокументОснование;
КонецЦикла;
Этот запрос выбирает данные из двух табличных частей, применяет логику пересчета валютной суммы прямо в запросе и объединяет результаты. Затем мы перебираем результат запроса и создаем движения. Такой подход очень эффективен, так как все агрегации и фильтрации выполняются на уровне СУБД.
Для более сложных сценариев, когда требуется выполнить несколько этапов обработки или объединить данные с другими источниками (например, справочниками или регистрами), мы можем использовать временные таблицы в запросе. Это позволяет разбить сложный запрос на более простые и читаемые части, а также повышает производительность, поскольку промежуточные результаты сохраняются и могут быть использованы повторно.
// Пример использования временных таблиц
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТЧ.Ссылка КАК СсылкаДокумента,
| ТЧ.Номенклатура КАК Номенклатура,
| ТЧ.Сумма КАК Сумма,
| ""Товары"" КАК ТипТЧ
|ПОМЕСТИТЬ ВТ_ВсеСтрокиТЧ
|ИЗ
| Документ.Продажа.Товары КАК ТЧ
|ГДЕ
| ТЧ.Ссылка = &СсылкаДокумента
|ОБЪЕДИНИТЬ ВСЕ
|ВЫБРАТЬ
| ТЧ.Ссылка,
| ТЧ.Номенклатура,
| ТЧ.Сумма,
| ""Услуги""
|ИЗ
| Документ.Продажа.Услуги КАК ТЧ
|ГДЕ
| ТЧ.Ссылка = &СсылкаДокумента
|;
////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| &Дата КАК Период,
| ВидДвиженияНакопления.Приход КАК ВидДвижения,
| ВТ.Номенклатура КАК Номенклатура,
| ВТ.Сумма * ВЫБОР КОГДА &ВалютныйУчет ТОГДА &ТекущийКурс ИНАЧЕ 1 КОНЕЦ КАК Сумма,
| &ДокументОснование КАК ДокументОснование,
| ВТ.ТипТЧ КАК ТипТабличнойЧасти
|ИЗ
| ВТ_ВсеСтрокиТЧ КАК ВТ";
// Устанавливаем параметры запроса (аналогично предыдущему примеру)
Запрос.УстановитьПараметр("Дата", Дата);
Запрос.УстановитьПараметр("ВалютныйУчет", ВалютныйУчет);
Запрос.УстановитьПараметр("ТекущийКурс", ТекущийКурс);
Запрос.УстановитьПараметр("СсылкаДокумента", ЭтотОбъект.Ссылка);
Запрос.УстановитьПараметр("ДокументОснование", ЭтотОбъект.Ссылка);
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
Движение = Движения.ДоходПоНоменклатурам.Добавить();
Движение.Период = Выборка.Период;
Движение.ВидДвижения = Выборка.ВидДвижения;
Движение.Номенклатура = Выборка.Номенклатура;
Движение.Сумма = Выборка.Сумма;
Движение.ДокументОснование = Выборка.ДокументОснование;
// Можно использовать Выборка.ТипТабличнойЧасти для дополнительной логики
КонецЦикла;
Использование запросов особенно эффективно при необходимости:
Выбор способа перебора табличных частей зависит от конкретной задачи и требований:
ЭтотОбъект.Метаданные.ТабличныеЧасти):
ЭтотОбъект.ИмяТабличнойЧасти):
Каждый из этих методов имеет свои преимущества и недостатки. Мы, как разработчики, должны анализировать требования задачи и выбирать наиболее подходящий инструмент, обеспечивающий баланс между универсальностью, производительностью, читаемостью и простотой поддержки кода.