При разработке и кастомизации типовых конфигураций 1С (таких как УПП 1.3, КА 1.1 или УТ 10.3) программисты часто сталкиваются с задачей дублирования или трансформации движений документов. Типичный сценарий: при записи движений по регистру ПартииТоваровНаСкладах необходимо параллельно записать эти же данные (возможно, с небольшими корректировками) в другой, собственный регистр накопления (например, ПартииТоваровНаСкладахЗакупкаДопРасходы).
Однако при попытке перехватить данные в обработчике события ПередЗаписью модуля набора записей регистров накопления разработчики часто обнаруживают, что коллекция движений или переменная мТаблицаДвижений оказывается пустой. В этой статье мы подробно разберем, почему так происходит, какие критические ошибки часто совершают программисты при попытке решить эту задачу, и как правильно реализовать данный механизм без угроз для производительности системы.
Главная причина, по которой в точке останова внутри метода ПередЗаписью коллекция записей кажется пустой — это особенности транзакционной модели платформы 1С:Предприятие при проведении документов. Запись набора движений по регистру выполняется платформой дважды в рамках одной транзакции:
ПередЗаписью, где количество записей в коллекции ЭтотОбъект.Количество() равно 0, а вспомогательные таблицы пусты. Если ваша точка останова сработала именно здесь, вы увидите пустой набор.ПередЗаписью вызывается повторно, и коллекция ЭтотОбъект уже содержит заполненные строки, готовые к проведению.Чтобы обойти первый этап очистки и работать только с реальными данными, которые будут записаны в базу данных, в самом начале обработчика событий необходимо установить простую проверку на количество строк:
Если ЭтотОбъект.Количество() = 0 Тогда
Возврат;
КонецЕсли;
Эта строчка кода позволит игнорировать вызов очистки движений и переходить к основной логике только тогда, когда набор действительно заполнен новыми данными.
Рассмотрим популярные ошибки, которые часто допускают разработчики при попытке «вытащить» данные из базы во время записи:
Ошибка №1: Чтение набора записей через СоздатьНаборЗаписей() и Прочитать()
Попытка считать данные из базы данных внутри транзакции записи выглядит так:
Движения = РегистрыНакопления.ПартииТоваровНаСкладах.СоздатьНаборЗаписей();
Движения.Отбор.Регистратор.Значение = ЭтотОбъект.Отбор.Регистратор.Значение.Ссылка;
Движения.Прочитать();
Это грубейшая ошибка. Метод Прочитать() обращается к физическим таблицам СУБД. На момент выполнения обработчика ПередЗаписью новые движения еще не зафиксированы в базе данных. Соответственно, этот метод вернет либо старые движения (которые вот-вот будут заменены), либо пустоту. Более того, у нового объекта типа РегистрНакопленияНаборЗаписей экспортная переменная мТаблицаДвижений гарантированно будет пустой, так как она инициализируется только в модуле проведения конкретного документа-источника.
Ошибка №2: Вызов ПолучитьОбъект() для документа в транзакции записи
Обращение к документу через получение его объектной сущности:
ДокСсылка = ЭтотОбъект.Отбор.Регистратор.Value;
ОбъектДок = ДокСсылка.ПолучитьОбъект();
Подобный код внутри обработчиков событий проведения категорически запрещен. Метод ПолучитьОбъект() инициирует повторное чтение данных документа из СУБД. В высоконагруженных системах это приводит к возникновению избыточных блокировок (Shared Locks) и взаимных блокировок СУБД (Deadlocks) при параллельной работе пользователей. Кроме того, вы получите состояние объекта из базы, а не текущее оперативное состояние из оперативной памяти транзакции.
Рассмотрим наиболее простой и надежный способ решения задачи непосредственно в модуле набора записей регистра-источника. Нам вообще не требуется обращаться к экспортным переменным документов вроде мТаблицаДвижений. К моменту финального вызова ПередЗаписью все необходимые данные уже содержатся в коллекции ЭтотОбъект. Нам достаточно выгрузить их во временную таблицу значений и загрузить в новый регистр.
Разберем по шагам реализацию в модуле набора записей регистра ПартииТоваровНаСкладах:
Процедура ПередЗаписью(Отказ, Замещение)
// Шаг 1. Игнорируем вызов на этапе очистки движений
Если ЭтотОбъект.Количество() = 0 Тогда
Возврат;
КонецЕсли;
// Получаем ссылку на регистратор
РегистраторСсылка = ЭтотОбъект.Отбор.Регистратор.Значение;
// Шаг 2. Фильтруем документы по типу, если логика нужна не для всех
Если ТипЗнч(РегистраторСсылка) = Тип("ДокументСсылка.ПоступлениеТоваровУслуг") Тогда
// Шаг 3. Выгружаем подготовленные новые движения во временную таблицу
ТаблицаНовыхДвижений = ЭтотОбъект.Выгрузить();
// Шаг 4. Создаем набор записей для нашего нового регистра-приемника
Приемник = РегистрыНакопления.ПартииТоваровНаСкладахЗакупкаДопРасходы.СоздатьНаборЗаписей();
Приемник.Отбор.Регистратор.Установить(РегистраторСсылка);
// Загружаем скопированные строки
Приемник.Загрузить(ТаблицаНовыхДвижений);
// Шаг 5. При необходимости модифицируем данные приемника
Для Каждого СтрокаДвижения Из Приемник Цикл
// Например, пересчитываем суммы или меняем аналитику
// СтрокаДвижения.Сумма = СтрокаДвижения.Сумма * 1.1;
КонецЦикла;
// Записываем данные в новый регистр
Приемник.Записать();
КонецЕсли;
КонецПроцедуры
Вместо того чтобы изменять модуль типового регистра накопления (что усложнит последующие обновления конфигурации 1С), мы рекомендуем использовать механизм подписок на события. Это более современный и гибкий подход.
Разберем алгоритм настройки подписки:
РегистрНакопленияНаборЗаписей.ПартииТоваровНаСкладах.ПриЗаписи.В событии ПриЗаписи данные уже физически подготовлены и гарантированно находятся в транзакции базы данных. Код обработчика в общем модуле будет выглядеть следующим образом:
Процедура ИмяПодпискиПриЗаписи(Источник, Отказ) Экспорт
// На этапе удаления движений Источник будет пуст, игнорируем его
Если Источник.Количество() = 0 Тогда
Возврат;
КонецЕсли;
РегистраторСсылка = Источник.Отбор.Регистратор.Значение;
// Проверяем тип документа-регистратора
Если ТипЗнч(РегистраторСсылка) = Тип("ДокументСсылка.ПоступлениеТоваровУслуг") Тогда
// Выгружаем данные из записанного источника
ТаблицаДанных = Источник.Выгрузить();
Приемник = РегистрыНакопления.ПартииТоваровНаСкладахЗакупкаДопРасходы.СоздатьНаборЗаписей();
Приемник.Отбор.Регистратор.Установить(РегистраторСсылка);
Приемник.Загрузить(ТаблицаДанных);
// Выполняем запись во второй регистр
Приемник.Записать();
КонецЕсли;
КонецПроцедуры
Преимущества использования подписки: код полностью изолирован от типовых объектов метаданных, что существенно упрощает поддержку и аудит конфигурации, а также минимизирует риски случайного изменения типовой логики проведения.
Если в вашей конфигурации используется классический партионный учет (например, в УПП 1.3), движения по регистрам формируются не напрямую в модулях документов, а через специализированные централизованные процедуры в общих модулях (например, ДвиженияПартийТоваров или УправлениеЗапасамиПартионныйУчет).
В таких системах вызов записи таблиц обычно выглядит так:
ОбщегоНазначения.ЗаписатьТаблицыДокументаВРегистр(Движения.ПартииТоваровНаСкладах, ...);
Проанализируем этот сценарий. Если вам необходимо, чтобы распределение партий во втором регистре происходило строго по алгоритмам первого, имеет смысл найти место вызова этой процедуры в общем модуле проведения и добавить вызов формирования аналогичной таблицы для нового регистра:
// Пример интеграции в модуль проведения партий
ОбщегоНазначения.ЗаписатьТаблицыДокументаВРегистр(Движения.ПартииТоваровНаСкладах, ВидДвиженияНакопления.Приход, ТаблицыДанныхДокумента, м_Дата);
// Дублируем движения в наш регистр с использованием той же таблицы данных
ОбщегоНазначения.ЗаписатьТаблицыДокументаВРегистр(Движения.ПартииТоваровНаСкладахЗакупкаДопРасходы, ВидДвиженияНакопления.Приход, ТаблицыДанныхДокумента, м_Дата);
Этот способ является наиболее органичным для конфигураций, использующих сложную логику распределения партий, так как он исключает необходимость «ловить» движения на уровне СУБД и повторно вызывать методы записи наборов записей.