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