Как правильно сформировать движения по регистру накопления из выборки запроса в 1С?

Программист 1С v8.3 (Управляемые формы) Управленческий учет IT и автоматизация бизнеса
← На главную

При разработке алгоритмов проведения документов в платформе 1С:Предприятие 8 программисты часто сталкиваются с ситуацией, когда движения в регистрах накопления формируются некорректно: либо записывается только одна (последняя) строка выборки, либо часть движений вообще не попадает в базу данных. В этой статье мы подробно разберем, почему возникают такие проблемы, проанализируем ошибки в коде и рассмотрим несколько эффективных способов реализации проведения — от классического исправления циклов до профессиональной оптимизации с использованием выгрузки таблиц данных.

Анализ проблемы: почему записывается только последняя запись?

Рассмотрим типичную ситуацию. Разработчик пытается заполнить движения регистра накопления на основе выборки из запроса. На первый взгляд, логика проста: получить данные, создать строки движения, обойти выборку в цикле и заполнить поля. Однако при реализации часто допускается критическая ошибка — инициализация строк движения происходит вне цикла обхода выборки.

Посмотрим на некорректный фрагмент кода, похожий на тот, с которым столкнулся автор темы:


ДвижениеПоТекущемуСценарию = Движения.ИС_ОборотыПоказателейБюджета.Добавить();

Пока ВыборкаПлатежи.Следующий() Цикл                
    ДвижениеПоТекущемуСценарию.Организация = ВыборкаПлатежи.Организация;
    ДвижениеПоТекущемуСценарию.Период = ВыборкаПлатежи.ДатаПланирования;
    ДвижениеПоТекущемуСценарию.Сумма = ВыборкаПлатежи.Сумма;            
КонецЦикла; 

Выясним причину, почему в регистр записывается только одна строка, причем всегда последняя из выборки. Метод Добавить() коллекции набора записей создает один конкретный объект строки в оперативной памяти и возвращает на него ссылку. Когда этот метод вызывается до начала цикла, в наборе записей создается ровно одна строка.

Во время работы цикла Пока ВыборкаПлатежи.Следующий() Цикл программа на каждой итерации не создает новую строку, а лишь перезаписывает свойства одной и той же ранее созданной строки. В итоге, после завершения цикла, эта строка сохраняет значения, переданные ей на самой последней итерации. Именно она и записывается в базу данных.

Для исправления этой ошибки достаточно перенести вызов метода Добавить() внутрь цикла обхода выборки. В этом случае на каждом шаге цикла в оперативной памяти будет создаваться новая чистая строка движения, которая затем заполнится актуальными данными текущей записи выборки:


Пока ВыборкаПлатежи.Следующий() Цикл                
    // Теперь новая строка создается на каждой итерации цикла
    ДвижениеПоТекущемуСценарию = Движения.ИС_ОборотыПоказателейБюджета.Добавить();
    
    ДвижениеПоТекущемуСценарию.Организация = ВыборкаПлатежи.Организация;
    ДвижениеПоТекущемуСценарию.Период = ВыборкаПлатежи.ДатаПланирования;
    ДвижениеПоТекущемуСценарию.Сумма = ВыборкаПлатежи.Сумма;            
КонецЦикла; 

Скрытая ловушка: Повторный обход одной и той же выборки

Проанализируем вторую, более замаскированную и опасную ошибку, которая содержалась в исходном коде автора темы. Код выглядел следующим образом:


ВыборкаПлатежи = РезультатЗапроса.Выбрать();

Пока ВыборкаПлатежи.Следующий() Цикл                
    // Заполнение движений по первому сценарию
КонецЦикла; 

Пока ВыборкаПлатежи.Следующий() Цикл            
    // Заполнение движений по лимитам
КонецЦикла;  

Даже если перенести метод Добавить() внутрь обоих циклов, этот алгоритм все равно отработает неверно: движения по лимитам (второй цикл) вообще не будут сформированы. Разберем по шагам, почему это происходит.

Объект ВыборкаИзРезультатаЗапроса в платформе 1С представляет собой последовательный однонаправленный курсор. Метод Следующий() перемещает этот курсор к следующей записи результата запроса. Когда первый цикл полностью завершается, это означает, что курсор дошел до самого конца выборки, и метод Следующий() вернул значение Ложь.

Когда управление переходит ко второму циклу, курсор по-прежнему находится в самом конце выборки. Повторный вызов метода Следующий() сразу же возвращает Ложь, так как двигаться дальше некуда. Программа просто игнорирует тело второго цикла и переходит к следующим инструкциям. Данные для лимитов не формируются вовсе.

Рассмотрим три способа решения этой проблемы.

Способ 1. Сброс указателя выборки

Самый простой, но наименее производительный способ вернуть выборку в исходное состояние — вызвать метод Сбросить() перед началом второго цикла. Этот метод возвращает курсор в начальное положение (перед первой записью):


ВыборкаПлатежи = РезультатЗапроса.Выбрать();

Пока ВыборкаПлатежи.Следующий() Цикл                
    Движение = Движения.ИС_ОборотыПоказателейБюджета.Добавить();
    // Заполнение первой группы движений...
КонецЦикла; 

// Сбрасываем указатель выборки в начало
ВыборкаПлатежи.Сбросить();

Пока ВыборкаПлатежи.Следующий() Цикл            
    Движение = Движения.ИС_ОборотыПоказателейБюджета.Добавить();
    // Заполнение второй группы движений...
КонецЦикла;  

Способ 2. Объединение логики в один цикл

Гораздо более правильным и эффективным подходом с точки зрения оптимизации кода является объединение логики. Зачем читать одни и те же данные дважды, если можно создать обе записи регистра (и для текущего сценария, и для лимитов бюджета) за один единственный проход выборки?


ВыборкаПлатежи = РезультатЗапроса.Выбрать();

Пока ВыборкаПлатежи.Следующий() Цикл                
    // Формируем движение по первому направлению
    ДвижениеПоТекущемуСценарию = Движения.ИС_ОборотыПоказателейБюджета.Добавить();
    ДвижениеПоТекущемуСценарию.Организация = ВыборкаПлатежи.Организация;
    ДвижениеПоТекущемуСценарию.Период = ВыборкаПлатежи.ДатаПланирования;
    ДвижениеПоТекущемуСценарию.Сумма = ВыборкаПлатежи.Сумма;
    
    // В этом же цикле формируем движение по второму направлению (лимиты)
    ДвижениеПоЛимитамБюджета = Движения.ИС_ОборотыПоказателейБюджета.Добавить();
    ДвижениеПоЛимитамБюджета.Организация = ВыборкаПлатежи.Организация;
    ДвижениеПоЛимитамБюджета.Период = ВыборкаПлатежи.ДатаПланирования;
    ДвижениеПоЛимитамБюджета.Сумма = -ВыборкаПлатежи.Сумма; // Сумма с обратным знаком
КонецЦикла; 

Способ 3. Использование Таблицы Значений

Если логика требует раздельного анализа данных или многократного их обхода, мы можем выгрузить результат запроса в оперативную память в виде объекта ТаблицаЗначений с помощью метода Выгрузить(). В отличие от выборки, таблицу значений можно обходить через цикл Для Каждого неограниченное количество раз без необходимости какого-либо сброса:


ТаблицаПлатежей = РезультатЗапроса.Выгрузить();

Для Каждого СтрокаПлатежа Из ТаблицаПлатежей Цикл                
    Движение = Движения.ИС_ОборотыПоказателейБюджета.Добавить();
    ЗаполнитьЗначенияСвойств(Движение, СтрокаПлатежа);
КонецЦикла; 

Для Каждого СтрокаПлатежа Из ТаблицаПлатежей Цикл            
    Движение = Движения.ИС_ОборотыПоказателейБюджета.Добавить();
    ЗаполнитьЗначенияСвойств(Движение, СтрокаПлатежа);
    Движение.Сумма = -СтрокаПлатежа.Сумма;
КонецЦикла;  

Профессиональное решение: Использование метода Загрузить()

Построчный обход выборки в цикле и ручное добавление каждой записи через Добавить() — это классический учебный подход. Однако при работе с большими объемами данных (сотни и тысячи строк в документе) этот метод работает медленно. Согласно стандартам разработки фирмы "1С", наиболее производительным способом записи движений является использование метода Загрузить() набора записей.

Суть этого метода заключается в том, что мы подготавливаем таблицу значений, структура колонок которой полностью совпадает с полями регистра (измерениями, ресурсами и реквизитами), а затем одной командой загружаем всю таблицу в регистр, избегая медленных построчных операций на встроенном языке.

Для реализации этого метода мы перепишем исходный запрос так, чтобы он сразу возвращал готовую таблицу для обеих групп движений. Для этого применим конструкцию ОБЪЕДИНИТЬ ВСЕ прямо в тексте запроса. Посмотрим, как изящно и быстро будет выглядеть итоговый код проведения:


Запрос = Новый Запрос;
Запрос.УстановитьПараметр("Ссылка", Ссылка);
Запрос.Текст = 
"ВЫБРАТЬ
|    ОМ_ВнесениеЗаявокНаЗакупкуПланирование.ДатаПланирования КАК Период,
|    ОМ_ВнесениеЗаявокНаЗакупку.Организация КАК Организация,
|    ОМ_ВнесениеЗаявокНаЗакупку.СценарийБюджетирования КАК СценарийБюджетирования,
|    ОМ_ВнесениеЗаявокНаЗакупку.ЦФО КАК ЦФО,
|    ОМ_ВнесениеЗаявокНаЗакупку.ПоказательБюджета КАК ПоказательБюджета,
|    ОМ_ВнесениеЗаявокНаЗакупку.Валюта КАК Валюта,
|    ОМ_ВнесениеЗаявокНаЗакупку.Проект КАК Проект,
|    ОМ_ВнесениеЗаявокНаЗакупку.СтатьяДДС КАК Аналитика1,
|    ОМ_ВнесениеЗаявокНаЗакупку.ЗаявкаНаЗакупку.Контрагент КАК Аналитика2,
|    ОМ_ВнесениеЗаявокНаЗакупку.ЗаявкаНаЗакупку КАК Аналитика3,
|    ОМ_ВнесениеЗаявокНаЗакупкуПланирование.Сумма КАК Сумма
|ИЗ
|    Документ.ОМ_ВнесениеЗаявокНаЗакупку.Планирование КАК ОМ_ВнесениеЗаявокНаЗакупкуПланирование
|    ЛЕВОЕ СОЕДИНЕНИЕ Документ.ОМ_ВнесениеЗаявокНаЗакупку КАК ОМ_ВнесениеЗаявокНаЗакупку
|        ПО ОМ_ВнесениеЗаявокНаЗакупкуПланирование.Ссылка = ОМ_ВнесениеЗаявокНаЗакупку.Ссылка
|ГДЕ
|    ОМ_ВнесениеЗаявокНаЗакупку.Ссылка = &Ссылка
|
|ОБЪЕДИНИТЬ ВСЕ
|
|ВЫБРАТЬ
|    ОМ_ВнесениеЗаявокНаЗакупкуПланирование.ДатаПланирования,
|    ОМ_ВнесениеЗаявокНаЗакупку.Организация,
|    ОМ_ВнесениеЗаявокНаЗакупку.СценарийБюджетирования,
|    ОМ_ВнесениеЗаявокНаЗакупку.ЦФО,
|    ОМ_ВнесениеЗаявокНаЗакупку.ПоказательБюджета,
|    ОМ_ВнесениеЗаявокНаЗакупку.Валюта,
|    ОМ_ВнесениеЗаявокНаЗакупку.Проект,
|    ОМ_ВнесениеЗаявокНаЗакупку.СтатьяДДС,
|    НЕОПРЕДЕЛЕНО, // Для лимитов Аналитика2 не нужна
|    НЕОПРЕДЕЛЕНО, // Для лимитов Аналитика3 не нужна
|    -ОМ_ВнесениеЗаявокНаЗакупкуПланирование.Сумма // Сумма со знаком минус
|ИЗ
|    Документ.ОМ_ВнесениеЗаявокНаЗакупку.Планирование КАК ОМ_ВнесениеЗаявокНаЗакупкуПланирование
|    ЛЕВОЕ СОЕДИНЕНИЕ Документ.ОМ_ВнесениеЗаявокНаЗакупку КАК ОМ_ВнесениеЗаявокНаЗакупку
|        ПО ОМ_ВнесениеЗаявокНаЗакупкуПланирование.Ссылка = ОМ_ВнесениеЗаявокНаЗакупку.Ссылка
|ГДЕ
|    ОМ_ВнесениеЗаявокНаЗакупку.Ссылка = &Ссылка";

ТаблицаДвижений = Запрос.Выполнить().Выгрузить();

Если ТаблицаДвижений.Количество() > 0 Тогда
    
    // Заполняем обязательные служебные поля, которых не было в запросе (например, Активность)
    ТаблицаДвижений.ЗаполнитьЗначения(Истина, "Активность");
    
    // Загружаем всю таблицу за один такт и взводим флаг необходимости записи
    Движения.ИС_ОборотыПоказателейБюджета.Загрузить(ТаблицаДвижений);
    Движения.ИС_ОборотыПоказателейБюджета.Записывать = Истина;
    
Иначе
    Отказ = Истина;
КонецЕсли;

Преимущества данного подхода очевидны: код становится коротким, логика полностью переносится на уровень СУБД (что значительно быстрее), а запись в регистр выполняется платформенными методами оптимизированного массового импорта данных.

Важные правила работы с движениями в "ОбработкаПроведения"

Для того чтобы ваши алгоритмы проведения всегда работали стабильно и не вызывали конфликтов блокировок в многопользовательском режиме, придерживайтесь следующих стандартов:

  1. Используйте свойство Записывать: Установка свойства Движения.ИмяРегистра.Записывать = Истина является стандартным и наиболее безопасным решением. Платформа сама запишет все подготовленные движения в базу данных в самом конце транзакции проведения документа.
  2. Избегайте избыточного вызова метода Записать(): Не вызывайте метод Движения.ИмяРегистра.Записать() вручную без явной необходимости. Каждый такой вызов провоцирует лишнее обращение к СУБД, что замедляет проведение. Ручная запись оправдана только тогда, когда вам необходимо прочитать остатки по этому же регистру далее в этой же транзакции с учетом только что сделанных движений текущего документа.
  3. Очищайте движения перед записью: При перепроведении документа платформа по умолчанию перезаписывает движения. Однако если вы используете сложные ручные схемы записи, убедитесь, что перед формированием новых движений старый набор записей очищается, иначе вы рискуете получить дублирование данных в регистре.

Применение этих простых правил в сочетании с оптимизированными запросами позволит вам создавать надежные, быстродействующие и легко поддерживаемые механизмы проведения документов любой сложности.

← На главную