В практике разработки на платформе 1С:Предприятие 8 часто возникает необходимость работы со сложными, связанными или иерархическими структурами данных. Одной из таких структур является вложенная таблица значений. Например, при проведении документов списания запасов система партионного учета может возвращать основную таблицу материалов, в одной из колонок которой хранится еще одна, вложенная таблица значений со сведениями о списанных партиях и суммах списания по каждой из них.
Для начинающих разработчиков обработка подобных конструкций часто становится серьезным препятствием. Типичные симптомы ошибок — запись в регистр только одной (последней) строки, дублирование записей или неверное распределение сумм по номенклатурным позициям. В этой статье мы подробно разберем, почему возникают такие проблемы, проанализируем типичные ошибки в кодовой логике и рассмотрим два оптимальных варианта решения задачи в зависимости от требований к детализации учета.
Давайте вместе разберем эту задачу по шагам, чтобы раз и навсегда научиться правильно работать с вложенными коллекциями в 1С.
Для начала давайте внимательно изучим фрагмент кода, который часто пишут разработчики при попытке решить подобную задачу. Рассмотрим логическую структуру, которая приводит к некорректному результату:
Для Каждого ТекСтрокаМатериалы Из Материалы Цикл
Движение = Движения.Запчасти.Добавить;
Движение.Организация = Организация;
Движение.Водитель = Водитель;
Движение.Номенклатура = ТекСтрокаМатериалы.Номенклатура;
Движение.Автомобиль = Автомобиль;
Движение.Количество = ТекСтрокаМатериалы.Количество;
Движение.Документ = ЭтотОбъект.Ссылка;
Для Каждого СтрокаМатериалов Из ТаблицаМатериалов Цикл
Для Каждого ТекущСтр Из СтрокаМатериалов.СписанныеПартииБУ Цикл
СуммаПартий = ТекущСтр.СуммаСписания;
КонецЦикла;
движение.Сумма = суммаПартий;
КонецЦикла;
КонецЦикла;
Проанализируем эту ситуацию и выясним причины, по которым этот код работает некорректно:
Материалы. Внутри него запускается полный обход таблицы ТаблицаМатериалов без какого-либо отбора по номенклатуре. Это означает, что для абсолютно каждого материала из документа система будет полностью перебирать все партии всех остальных материалов.Добавить(): Новая строка движения в регистре создается во внешнем цикле (один раз для материала). Затем во внутренних циклах по партиям свойству Движение.Сумма многократно присваивается значение. Каждая новая итерация просто перезаписывает это свойство. В итоге в регистр записывается только сумма из самой последней строки самой последней партии.СуммаПартий = ТекущСтр.СуммаСписания; происходит обычное замещение значения переменной. Даже если бы мы хотели накопить общую сумму по партиям, данный код этого не сделал бы. Для накопления требовалось бы написать СуммаПартий = СуммаПартий + ТекущСтр.СуммаСписания;.Если перед нами стоит задача отразить в регистре каждую списанную партию отдельно (например, для ведения полноценного партионного учета непосредственно в регистре «Запчасти»), нам необходимо перенести инициализацию и добавление новой записи движения в самый глубокий уровень вложенности. Также критически важно установить жесткую связь между обрабатываемым материалом и строками таблицы партий.
Рассмотрим пример правильной реализации этого подхода:
// Включаем флаг записи набора движений
Движения.Запчасти.Записывать = Истина;
Для Каждого ТекСтрокаМатериалы Из Материалы Цикл
// Шаг 1. Ищем строку в ТаблицеМатериалов, соответствующую текущей номенклатуре
ПараметрыОтбора = Новый Структура("Номенклатура", ТекСтрокаМатериалы.Номенклатура);
НайденныеСтроки = ТаблицаМатериалов.НайтиСтроки(ПараметрыОтбора);
// Если по какой-то причине партии для номенклатуры не найдены, переходим к следующей строке
Если НайденныеСтроки.Количество() = 0 Тогда
Продолжить;
КонецЕсли;
СтрокаМатериалов = НайденныеСтроки[0];
// Шаг 2. Обязательно проверяем тип данных во избежание падения системы при пустых значениях
Если ТипЗнч(СтрокаМатериалов.СписанныеПартииБУ) = Тип("ТаблицаЗначений") Тогда
// Шаг 3. Запускаем обход вложенной таблицы списанных партий конкретного материала
Для Каждого ТекущСтр Из СтрокаМатериалов.СписанныеПартииБУ Цикл
// Создаем запись движения строго ВНУТРИ цикла по партиям
Движение = Движения.Запчасти.Добавить();
Движение.Организация = Организация;
Движение.Водитель = Водитель;
Движение.Номенклатура = ТекСтрокаМатериалы.Номенклатура;
Движение.Автомобиль = Автомобиль;
Движение.Документ = ЭтотОбъект.Ссылка;
// Распределяем количество и сумму конкретной партии
Движение.Количество = ТекущСтр.Количество;
Движение.Сумма = ТекущСтр.СуммаСписания;
КонецЦикла;
КонецЕсли;
КонецЦикла;
Разберем подробнее, за счет чего этот алгоритм работает корректно. Метод НайтиСтроки() позволяет нам мгновенно сопоставить текущую позицию номенклатуры с нужной строкой из ТаблицаМатериалов. После этого мы точечно обращаемся к ячейке СписанныеПартииБУ, которая содержит вложенную таблицу значений. Создание нового движения методом Добавить() происходит непосредственно в момент итерации по строкам этой вложенной таблицы. Сколько строк партий содержится во вложенной таблице, столько строк движений и будет сформировано в регистре, при этом каждая строка получит свою собственную сумму.
Довольно часто детальный учет по партиям в самом регистре «Запчасти» не требуется. Регистр может быть предназначен только для контроля общих затрат в разрезе номенклатуры и автомобилей. В этом случае создавать несколько движений по каждой партии нецелесообразно — это приведет к избыточному росту базы данных.
Для решения этой задачи нам вообще не нужно писать вложенные циклы для обхода партий. Мы можем воспользоваться высокоэффективным встроенным методом платформы 1С — Итог(). Этот метод реализован на уровне платформенного C++ кода и работает значительно быстрее любого ручного перебора строк в цикле.
Посмотрим на пример реализации свернутого учета:
Движения.Запчасти.Записывать = Истина;
Для Каждого ТекСтрокаМатериалы Из Материалы Цикл
// Ищем соответствующую строку в таблице партий по Номенклатуре
ПараметрыОтбора = Новый Структура("Номенклатура", ТекСтрокаМатериалы.Номенклатура);
НайденныеСтроки = ТаблицаМатериалов.НайтиСтроки(ПараметрыОтбора);
Если НайденныеСтроки.Количество() > 0 Тогда
СтрокаМатериалов = НайденныеСтроки[0];
// Убеждаемся, что внутри ячейки действительно находится ТаблицаЗначений
Если ТипЗнч(СтрокаМатериалов.СписанныеПартииБУ) = Тип("ТаблицаЗначений") Тогда
// Рассчитываем сумму по всем партиям без использования циклов
ОбщаяСуммаСписания = СтрокаМатериалов.СписанныеПартииБУ.Итог("СуммаСписания");
// Создаем ровно ОДНО движение на одну строку материала
Движение = Движения.Запчасти.Добавить();
Движение.Организация = Организация;
Движение.Водитель = Водитель;
Движение.Номенклатура = ТекСтрокаМатериалы.Номенклатура;
Движение.Автомобиль = Автомобиль;
Движение.Количество = ТекСтрокаМатериалы.Количество;
Движение.Документ = ЭтотОбъект.Ссылка;
Движение.Сумма = ОбщаяСуммаСписания;
КонецЕсли;
КонецЕсли;
КонецЦикла;
При таком подходе мы получаем чистый, легко читаемый код. Мы избавились от внутреннего цикла, снизили вероятность логических ошибок и обеспечили максимальное быстродействие алгоритма проведения документа.
При проектировании механизмов, использующих вложенные таблицы значений, крайне важно соблюдать несколько правил разработки, которые помогут избежать труднодиагностируемых ошибок в будущем:
ТаблицаЗначений. Если при формировании данных произойдет сбой или партия не спишется, там может оказаться значение Неопределено. Попытка вызвать метод у такого значения вызовет критическую ошибку выполнения. Используйте конструкцию Если ТипЗнч(...) = Тип("ТаблицаЗначений").ОписаниеТипов. Этот тип не поддерживается конструктором описания типов для колонок. Поэтому колонка создается без указания типов (как произвольный тип), а объект вложенной таблицы помещается туда динамически при заполнении строк: НоваяСтрока.СписанныеПартииБУ = Новый ТаблицаЗначений;.ДеревоЗначений вместо вложенных таблиц. С деревом значений гораздо проще работать при выводе данных на управляемые формы, и оно изначально оптимизировано для хранения иерархических структур.Применяя описанные подходы и паттерны проектирования циклов, вы сможете эффективно и безопасно обрабатывать любые многомерные коллекции данных в среде 1С:Предприятие.