В практике разработчика 1С часто возникает задача массового изменения данных в документах, которую можно решить как написанием собственного кода, так и с помощью готовых инструментов, таких как универсальная групповая обработка справочников и документов (для этого подойдет универсальная обработка массового редактирования данных). Например, когда нужно пересчитать цены, заменить ставку НДС, обновить номенклатуру или выполнить поиск и устранение дублей контрагентов в табличной части (ТЧ) сразу во множестве документов. На первый взгляд задача кажется простой, но она таит в себе подводные камни, связанные с производительностью, блокировками данных и сохранением логической целостности учета. В этой статье мы подробно разберем, как правильно получить данные из табличной части в цикле и корректно их изменить.
Рассмотрим ситуацию: нам необходимо найти документы ПередачаТоваров с видом операции «Безвозмездная передача», где цена в строках равна «1», и заменить её на «100» — здесь ускоряет процесс модуль автоматического заполнения и расчета цен в ТЧ. Проанализируем типичную ошибку начинающего программиста, которая часто встречается на форумах:
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
ДокументОбъект = ВыборкаДетальныеЗаписи.Ссылка.ПолучитьОбъект();
ТчТовары = ДокументОбъект.Товары;
// Как обратиться к полю "Цена" и заменить его?
КонецЦикла;
Проблема здесь заключается не только в синтаксисе обращения к полю, но и в архитектуре подхода. Если в запросе мы выбираем строки табличной части, то для каждого документа с 10 строками мы будем вызывать ПолучитьОбъект() 10 раз. Это классическая проблема производительности «N+1», которая способна «положить» сервер при большом объеме данных.
Прежде чем приступать к циклу, выясним причину медленной работы. Если мы собираемся менять данные в документе, нам нужно сгруппировать выборку по ссылкам на документы. Нет смысла обрабатывать одну и ту же ссылку несколько раз в разных итерациях основного цикла.
Рассмотрим оптимальный текст запроса:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| ПередачаТоваровТовары.Ссылка КАК Ссылка
|ИЗ
| Документ.ПередачаТоваров.Товары КАК ПередачаТоваровТовары
|ГДЕ
| ПередачаТоваровТовары.Ссылка.ВидОперации = ЗНАЧЕНИЕ(Перечисление.ВидыОперацийПередачаТоваров.БезвозмезднаяПередача)
| И ПередачаТоваровТовары.Цена = 1";
РезультатЗапроса = Запрос.Выполнить();
Обратим внимание на использование ВЫБРАТЬ РАЗЛИЧНЫЕ. Это позволит нам получить список только тех документов, которые действительно требуют изменений, без дублей по количеству строк.
Теперь разберем, как правильно организовать цикл. Основное правило: один документ — одна загрузка в память и одна запись в базу данных. Проанализируем ситуацию, когда мы уже получили выборку ссылок.
Разберем по шагам процесс изменения:
Пока ... Следующий() получаем объект документа через метод ПолучитьОбъект().Для каждого ... Из для перебора строк табличной части самого объекта.Посмотрим на пример кода:
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
// Блокируем данные перед изменением, чтобы избежать конфликтов
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Документ.ПередачаТоваров");
ЭлементБлокировки.УстановитьЗначение("Ссылка", Выборка.Ссылка);
Блокировка.Заблокировать();
ДокументОбъект = Выборка.Ссылка.ПолучитьОбъект();
// Пройдемся по всем строкам табличной части "Товары"
Для каждого СтрокаТЧ Из ДокументОбъект.Товары Цикл
// Проверяем условие, так как в документе могут быть и другие строки
Если СтрокаТЧ.Цена = 1 Тогда
СтрокаТЧ.Цена = 100;
// Важно! Не забываем про зависимые реквизиты
СтрокаТЧ.Сумма = СтрокаТЧ.Цена * СтрокаТЧ.Количество;
КонецЕсли;
КонецЦикла;
// Записываем измененный документ
Попытка
ДокументОбъект.Записать(РежимЗаписиДокумента.Запись);
Исключение
Сообщить("Не удалось записать документ: " + Выборка.Ссылка + " Ошибка: " + ОписаниеОшибки());
КонецПопытки;
КонецЦикла;
Если табличная часть документа содержит сотни или тысячи строк, а изменить нужно всего две-три, полный перебор в цикле Для каждого может быть неэффективным. Выясним причину: метод НайтиСтроки() работает быстрее, так как использует внутренние механизмы платформы для поиска по индексам в памяти.
Рассмотрим подробнее этот подход:
Отбор = Новый Структура("Цена", 1);
НайденныеСтроки = ДокументОбъект.Товары.НайтиСтроки(Отбор);
Для каждого СтрокаТЧ Из НайденныеСтроки Цикл
СтрокаТЧ.Цена = 100;
// Пересчет суммы обязателен
СтрокаТЧ.Сумма = СтрокаТЧ.Цена * СтрокаТЧ.Количество;
КонецЦикла;
Такой код выглядит чище и работает быстрее на больших табличных частях. Однако помните, что НайтиСтроки() ищет только по строгому равенству.
Проанализируем несколько критических моментов, которые часто упускают из виду:
Цена недостаточно. В типовых конфигурациях (УТ 11, ERP, БП 3.0) существуют процедуры в общих модулях, которые отвечают за расчет налогов, скидок и итоговых сумм. Рекомендуется вызывать их после ручного изменения полей. Например: ОбработкаТабличныхЧастей.РассчитатьСуммуТабЧасти(СтрокаТЧ, ДокументОбъект);.Записать(РежимЗаписиДокумента.Запись) просто обновит данные в документе, но не изменит движения в регистрах. Это создаст расхождение между «картинкой» в документе и данными в отчетах. Если нужно актуализировать учет, используйте РежимЗаписиДокумента.Проведение. Для больших объемов данных такие задачи часто выносят в перепроведение документов в фоновом режиме (поможет обработка фонового перепроведения документов).Иногда новички пытаются использовать ВыборкаДетальныеЗаписи.НомерСтроки внутри цикла. Разберем ситуацию: поле НомерСтроки будет доступно в выборке только в том случае, если вы явно добавили его в текст запроса. Однако использовать индекс строки из запроса для обращения к табличной части объекта — опасная практика. Пользователь мог изменить порядок строк или удалить строку в тот момент, когда ваш запрос уже выполнился, но объект еще не был получен. Это приведет к ошибке «Индекс находится за границами массива» или, что хуже, к изменению не той строки.
Выясним причину: всегда лучше работать непосредственно с коллекцией строк самого ДокументОбъект, а не полагаться на данные из запроса, полученные мгновением ранее.
Мы проанализировали процесс получения и изменения данных в табличных частях 1С. Подведем итог: наиболее надежным и производительным способом является получение списка уникальных ссылок через запрос, последующее получение объекта каждого документа, поиск нужных строк методом НайтиСтроки() или циклом Для каждого, и обязательный пересчет всех зависимых сумм перед записью. Впрочем, для многих типовых задач, когда требуется точечная замена, можно использовать готовые инструменты, такие как поиск и замена значений с универсальным отбором (например, обработка поиска и устранения дублей контрагентов). Соблюдение этих правил гарантирует стабильную работу системы и корректность данных в базе.