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