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

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

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

Попытка решить эту задачу «в лоб» через создание нового набора записей часто приводит к неприятному эффекту: стандартные движения документа полностью исчезают, а в базе данных остается только одна наша новая строка. В этой статье мы подробно разберем, почему так происходит, изучим внутренние механизмы СУБД при записи движений и рассмотрим несколько правильных способов решения этой задачи в зависимости от контекста выполнения кода.

Выясняем причину: почему пропадают существующие движения?

Проанализируем исходный код, который часто пишут начинающие разработчики:


НаборЗаписейУчетная = РегистрыНакопления.Запасы.СоздатьНаборЗаписей();
НаборЗаписейУчетная.Отбор.Регистратор.Установить(Ссылка);

НоваяЗапись = НаборЗаписейУчетная.Добавить();
НоваяЗапись.ВидДвижения = ВидДвиженияНакопления.Приход; 
НоваяЗапись.Период = ЭтотОбъект.Дата;
НоваяЗапись.Организация = Справочники.Организации.НайтиПоКоду("000001");
// Заполнение остальных реквизитов и измерений...

НаборЗаписейУчетная.Записать();

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

  1. Платформа инициирует транзакцию базы данных.
  2. На уровне СУБД выполняется SQL-запрос удаления (DELETE) абсолютно всех строк из таблицы регистра, у которых поле регистратора совпадает с переданной ссылкой.
  3. После очистки старых записей платформа считывает данные из оперативной памяти — из нашего объекта НаборЗаписейУчетная. В данном объекте сейчас находится ровно одна строка, которую мы только что добавили методом Добавить().
  4. Платформа выполняет запрос вставки (INSERT) этой единственной строки в базу данных.
  5. Транзакция фиксируется.

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

Способ 1. Использование метода Прочитать() перед модификацией

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

Разберем этот вариант на примере модифицированного кода:


НаборЗаписейУчетная = РегистрыНакопления.Запасы.СоздатьНаборЗаписей();
НаборЗаписейУчетная.Отбор.Регистратор.Установить(Ссылка);

// Считываем существующие в базе данных движения по этому регистратору
НаборЗаписейУчетная.Прочитать();

// Теперь в наборе содержатся все старые движения. Добавим к ним новое:
НоваяЗапись = НаборЗаписейУчетная.Добавить();
НоваяЗапись.ВидДвижения = ВидДвиженияНакопления.Приход; 
НоваяЗапись.Период = ЭтотОбъект.Дата;
// Заполнение полей...

// Записываем набор обратно в базу данных
НаборЗаписейУчетная.Записать();

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

Затем мы добавляем 10 001-ю строку и вызываем метод Записать(Истина). Система выполнит тяжелую операцию: сначала удалит 10 000 строк из СУБД, а затем вставит уже 10 001 строку. В высоконагруженных системах (например, на крупных предприятиях) это приведет к колоссальной избыточной нагрузке на дисковую подсистему, росту транзакционного лога СУБД и длительным взаимным блокировкам таблиц.

Способ 2. Правильное добавление движений внутри ОбработкиПроведения

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

У любого документа-объекта есть встроенное свойство Движения, которое представляет собой коллекцию наборов записей по всем регистрам, в которых документ может выступать регистратором. Нам достаточно обратиться к этой коллекции напрямую.

Рассмотрим правильный шаблон кода для модуля документа:


Процедура ОбработкаПроведения(Отказ, РежимПроведения)
    
    // ... здесь отрабатывает стандартный алгоритм проведения документа ...
    
    // Добавляем наше движение, не мешая стандартному коду
    Движения.Запасы.Записывать = Истина;
    
    НоваяЗапись = Движения.Запасы.Добавить();
    НоваяЗапись.ВидДвижения = ВидДвиженияНакопления.Приход;
    НоваяЗапись.Период = Дата;
    НоваяЗапись.Организация = Справочники.Организации.НайтиПоКоду("000001");
    НоваяЗапись.СтруктурнаяЕдиница = СтруктурнаяЕдиницаПолучатель;
    // Заполнение остальных реквизитов...
    
КонецПроцедуры

Почему этот способ является наиболее предпочтительным?

Способ 3. Добавление движений через подписки на события

Довольно часто разработчикам приходится дорабатывать типовые конфигурации (например, 1С:ERP, 1С:КА или 1С:УТ) без изменения самого документа, используя механизм подписок на события. В обработчике подписки на событие ОбработкаПроведения нам доступен параметр Источник, содержащий сам документ-объект.

Многие допускают ошибку, пытаясь получить объект заново через ссылку: Источник.Ссылка.ПолучитьОбъект(). Делать так внутри транзакции проведения категорически нельзя — это неизбежно приведет к дедлокам (взаимным блокировкам) СУБД. Вместо этого нужно работать напрямую со свойствами переданного источника.

Рассмотрим пример кода в общем модуле для подписки на событие:


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

КонецПроцедуры

Способ 4. Запись в режиме добавления через Записать(Ложь)

В платформе 1С существует возможность дозаписать новые строки в СУБД без предварительного чтения и без удаления существующих записей. Для этого используется вызов метода Записать(Ложь). При таком вызове платформа не выполняет команду DELETE, а сразу делает команду INSERT для строк, находящихся в наборе.

Однако этот способ таит в себе серьезные опасности, о которых нужно знать заранее:

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

Важный аспект безопасности: управляемые блокировки данных

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

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

Проанализируем пример правильной установки блокировки перед модификацией набора записей:


// Инициализируем объект блокировки
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.Запасы");

// Устанавливаем пространство и отбор по нашему регистратору
ЭлементБлокировки.УстановитьЗначение("Регистратор", СсылкаНаДокумент);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;

// Накладываем блокировку
Блокировка.Заблокировать();

// Только после успешной блокировки выполняем чтение и запись
НаборЗаписей = РегистрыНакопления.Запасы.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(СсылкаНаДокумент);
НаборЗаписей.Прочитать();

НоваяЗапись = НаборЗаписей.Добавить();
// Заполнение полей новой записи...

НаборЗаписей.Записать();

Подведем итоги

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

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