При разработке в 1С часто возникает классическая задача: имеется набор данных (например, результат запроса), в котором есть повторяющиеся сущности (контрагенты, оборудование, договоры) и детализированные записи к ним (пломбы, товары, услуги). Проблема заключается в том, что при простом обходе результата выборки в цикле система создает новый документ на каждую строку. Наша цель — разобраться, как создавать ровно один документ (шапку) на каждое уникальное значение, а все связанные с ним детализированные записи добавлять в табличную часть этого документа.
Проанализируем ситуацию по шагам и рассмотрим несколько эффективных способов решения этой архитектурной задачи.
Этот метод считается «золотым стандартом» разработки в 1С. Разберем подробнее, как он работает. Чтобы разделить данные на уровень шапки и уровень табличной части, нам нужно сгруппировать результат самого запроса. Для этого в тексте запроса используется конструкция ИТОГИ ПО.
В самом конце текста запроса (после секции УПОРЯДОЧИТЬ ПО) необходимо добавить подведение итогов по тому полю, которое будет определять уникальность создаваемого документа. Например, если мы группируем документы по оборудованию, мы пишем:
ИТОГИ ПО ОборудованиеСсылка
Далее посмотрим на пример обхода результата такого запроса. Вместо одного цикла мы будем использовать два вложенных. Первый цикл будет идти по уникальному оборудованию (формируя шапку документа), а второй цикл — по детальным записям внутри этой группы (заполняя табличную часть).
РезультатЗапроса = Запрос.Выполнить();
// Выбираем только верхний уровень (группировки по оборудованию)
ВыборкаОборудование = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаОборудование.Следующий() Цикл
// 1. Создаем один документ на текущее уникальное Оборудование
Демонтаж = Документы.ИзменениеПараметровОборудования.СоздатьДокумент();
Демонтаж.Дата = ТекущаяДата();
// Заполнение остальных реквизитов шапки из ВыборкаОборудование
Демонтаж.Оборудование = ВыборкаОборудование.ОборудованиеСсылка;
// 2. Получаем детальные записи, относящиеся ТОЛЬКО к этому оборудованию
ВыборкаДетали = ВыборкаОборудование.Выбрать();
Пока ВыборкаДетали.Следующий() Цикл
// 3. Заполняем табличную часть "Пломбы"
Док = Демонтаж.Пломбы.Добавить();
Док.Пломба = ВыборкаДетали.ПломбаСсылка;
// Заполнение остальных реквизитов табличной части
КонецЦикла;
// 4. Записываем сформированный документ со всеми строками
Демонтаж.Записать(РежимЗаписиДокумента.Проведение);
КонецЦикла;
Важно: При вызове метода Выбрать во вложенном цикле мы не передаем никаких параметров, так как детальные записи по умолчанию подчинены текущей группировке.
Если по каким-то причинам вы не можете изменить текст запроса и добавить итоги, рассмотрим второй подход. Он базируется на сравнении текущего значения с предыдущим. В этом случае результат запроса обязательно должен быть отсортирован по ключевому полю (используйте УПОРЯДОЧИТЬ ПО ОборудованиеСсылка).
Выясним логику работы этого метода: мы заводим переменную для хранения предыдущего значения оборудования. В цикле проверяем: если оборудование изменилось, значит, начался новый блок данных — мы записываем старый документ и создаем новый.
ПредыдущееОборудование = Неопределено;
Демонтаж = Неопределено;
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// Если оборудование сменилось, значит начался новый блок данных
Если ПредыдущееОборудование <> Выборка.ОборудованиеСсылка Тогда
// Записываем предыдущий документ, если он уже был создан (пропускаем первую итерацию)
Если Демонтаж <> Неопределено Тогда
Демонтаж.Записать(РежимЗаписиДокумента.Проведение);
КонецЕсли;
// Создаем новый документ для нового оборудования
Демонтаж = Документы.ИзменениеПараметровОборудования.СоздатьДокумент();
Демонтаж.Дата = ТекущаяДата();
// ... заполняем реквизиты шапки ...
// Запоминаем текущее оборудование как "предыдущее" для следующих шагов цикла
ПредыдущееОборудование = Выборка.ОборудованиеСсылка;
КонецЕсли;
// В любом случае добавляем строку в табличную часть текущего документа
Док = Демонтаж.Пломбы.Добавить();
Док.Пломба = Выборка.ПломбаСсылка;
КонецЦикла;
// ВАЖНО: После выхода из цикла нужно обязательно записать самый последний созданный документ!
Если Демонтаж <> Неопределено Тогда
Демонтаж.Записать(РежимЗаписиДокумента.Проведение);
КонецЕсли;
Проанализируем еще один альтернативный подход, который часто применяют, когда данные приходят не напрямую из базы данных, а загружаются из внешнего файла (например, Excel), либо когда логика предварительной обработки слишком сложна для одного запроса. В таких случаях мы работаем с объектом ТаблицаЗначений.
Разберем по шагам:
ТЗДанных = Запрос.Выполнить().Выгрузить();Свернуть. Мы получаем уникальный список документов, которые нужно создать.НайтиСтроки, передавая структуру поиска с ключевым полем.Создание документов в цикле — ресурсоемкая операция. Если выборка данных большая, процесс будет работать медленно и может заблокировать работу других пользователей. Рассмотрим подробнее, как ускорить код и сделать его надежнее:
НачатьТранзакцию() и ЗафиксироватьТранзакцию(). Внутри цикла используйте блок Попытка ... Исключение ... ОтменитьТранзакцию(). Это ускорит запись в базу данных и предотвратит появление "битых" данных, если на одном из документов возникнет ошибка проведения.ОбработкаЗаполнения, которые обычно заполняют валюты, ответственных и другие реквизиты по умолчанию. Имеет смысл явно вызывать метод Демонтаж.Заполнить(Неопределено) перед тем, как вы начнете присваивать реквизиты из выборки.Демонтаж.ОбменДанными.Загрузка = Истина. Это отключит проверки при записи и существенно ускорит работу алгоритма.Применяя описанные методы группировки данных и следуя правилам оптимизации, вы сможете создавать эффективные и надежные алгоритмы автоматической генерации документов со сложной иерархией табличных частей.