Как получить данные движений в регистре накопления ПередЗаписью и избежать пустой таблицы

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

При разработке и кастомизации типовых конфигураций 1С (таких как УПП 1.3, КА 1.1 или УТ 10.3) программисты часто сталкиваются с задачей дублирования или трансформации движений документов. Типичный сценарий: при записи движений по регистру ПартииТоваровНаСкладах необходимо параллельно записать эти же данные (возможно, с небольшими корректировками) в другой, собственный регистр накопления (например, ПартииТоваровНаСкладахЗакупкаДопРасходы).

Однако при попытке перехватить данные в обработчике события ПередЗаписью модуля набора записей регистров накопления разработчики часто обнаруживают, что коллекция движений или переменная мТаблицаДвижений оказывается пустой. В этой статье мы подробно разберем, почему так происходит, какие критические ошибки часто совершают программисты при попытке решить эту задачу, и как правильно реализовать данный механизм без угроз для производительности системы.

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

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

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

Чтобы обойти первый этап очистки и работать только с реальными данными, которые будут записаны в базу данных, в самом начале обработчика событий необходимо установить простую проверку на количество строк:


Если ЭтотОбъект.Количество() = 0 Тогда
    Возврат; 
КонецЕсли;

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

Анализируем критические ошибки в коде

Рассмотрим популярные ошибки, которые часто допускают разработчики при попытке «вытащить» данные из базы во время записи:

Ошибка №1: Чтение набора записей через СоздатьНаборЗаписей() и Прочитать()

Попытка считать данные из базы данных внутри транзакции записи выглядит так:


Движения = РегистрыНакопления.ПартииТоваровНаСкладах.СоздатьНаборЗаписей();
Движения.Отбор.Регистратор.Значение = ЭтотОбъект.Отбор.Регистратор.Значение.Ссылка;
Движения.Прочитать();

Это грубейшая ошибка. Метод Прочитать() обращается к физическим таблицам СУБД. На момент выполнения обработчика ПередЗаписью новые движения еще не зафиксированы в базе данных. Соответственно, этот метод вернет либо старые движения (которые вот-вот будут заменены), либо пустоту. Более того, у нового объекта типа РегистрНакопленияНаборЗаписей экспортная переменная мТаблицаДвижений гарантированно будет пустой, так как она инициализируется только в модуле проведения конкретного документа-источника.

Ошибка №2: Вызов ПолучитьОбъект() для документа в транзакции записи

Обращение к документу через получение его объектной сущности:


ДокСсылка = ЭтотОбъект.Отбор.Регистратор.Value;
ОбъектДок = ДокСсылка.ПолучитьОбъект();

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

Решение А: Копирование данных напрямую через ЭтотОбъект

Рассмотрим наиболее простой и надежный способ решения задачи непосредственно в модуле набора записей регистра-источника. Нам вообще не требуется обращаться к экспортным переменным документов вроде мТаблицаДвижений. К моменту финального вызова ПередЗаписью все необходимые данные уже содержатся в коллекции ЭтотОбъект. Нам достаточно выгрузить их во временную таблицу значений и загрузить в новый регистр.

Разберем по шагам реализацию в модуле набора записей регистра ПартииТоваровНаСкладах:


Процедура ПередЗаписью(Отказ, Замещение)
    
    // Шаг 1. Игнорируем вызов на этапе очистки движений
    Если ЭтотОбъект.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;
    
    // Получаем ссылку на регистратор
    РегистраторСсылка = ЭтотОбъект.Отбор.Регистратор.Значение;
    
    // Шаг 2. Фильтруем документы по типу, если логика нужна не для всех
    Если ТипЗнч(РегистраторСсылка) = Тип("ДокументСсылка.ПоступлениеТоваровУслуг") Тогда
        
        // Шаг 3. Выгружаем подготовленные новые движения во временную таблицу
        ТаблицаНовыхДвижений = ЭтотОбъект.Выгрузить();
        
        // Шаг 4. Создаем набор записей для нашего нового регистра-приемника
        Приемник = РегистрыНакопления.ПартииТоваровНаСкладахЗакупкаДопРасходы.СоздатьНаборЗаписей();
        Приемник.Отбор.Регистратор.Установить(РегистраторСсылка);
        
        // Загружаем скопированные строки
        Приемник.Загрузить(ТаблицаНовыхДвижений);
        
        // Шаг 5. При необходимости модифицируем данные приемника
        Для Каждого СтрокаДвижения Из Приемник Цикл
            // Например, пересчитываем суммы или меняем аналитику
            // СтрокаДвижения.Сумма = СтрокаДвижения.Сумма * 1.1; 
        КонецЦикла;
        
        // Записываем данные в новый регистр
        Приемник.Записать();
        
    КонецЕсли;

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

Решение Б: Использование подписки на событие ПриЗаписи

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

Разберем алгоритм настройки подписки:

  1. Создайте в конфигураторе новую Подписку на событие.
  2. В свойстве Источник укажите класс объектов: РегистрНакопленияНаборЗаписей.ПартииТоваровНаСкладах.
  3. В свойстве Событие выберите: ПриЗаписи.
  4. Укажите общий модуль, в котором будет размещен обработчик (модуль должен быть серверным).

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


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

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

Решение В: Интеграция в общие модули проведения партий

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

В таких системах вызов записи таблиц обычно выглядит так:


ОбщегоНазначения.ЗаписатьТаблицыДокументаВРегистр(Движения.ПартииТоваровНаСкладах, ...);

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


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

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

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

← На главную