При разработке сложных систем на платформе «1С:Предприятие 8» часто возникает необходимость разгрузить модули проведения документов или реализовать сквозной учет по нескольким документам без изменения их типовой логики. Для этих целей идеально подходят подписки на события. Однако запись в регистры накопления из подписки имеет ряд неочевидных нюансов, связанных с транзакционной целостностью, блокировками данных и очисткой движений при отмене проведения.
В этой статье мы подробно разберем, как правильно спроектировать и закодировать запись в регистр накопления из подписки на событие, проанализируем распространенные заблуждения разработчиков и рассмотрим два основных технических подхода к решению этой задачи.
Среди начинающих (а иногда и опытных) разработчиков 1С бытует мнение, что документ может делать движения только по тем регистрам, которые явно отмечены «галочками» на вкладке «Движения» в свойствах этого документа в конфигураторе. Это технически неверно.
Платформа 1С позволяет программно записать набор записей в абсолютно любой регистр накопления (или сведений), даже если этот регистр не отмечен во вкладке «Движения» документа. Однако для реализации такого подхода на уровне метаданных должны выполняться обязательные условия:
Регистратор жестко контролируется СУБД на уровне ссылочной целостности.Источник.Движения.ИмяРегистра. Запись необходимо выполнять напрямую через менеджер регистра, создавая объект НаборЗаписей.Рассмотрим детально оба варианта реализации подписки на событие.
Это классический и наиболее безопасный вариант. В свойствах документов-источников на вкладке «Движения» ваш регистр накопления отмечен флагом. При этом сама логика заполнения движений выносится из модуля документа в общий модуль, вызываемый подпиской.
Шаг 1. Создание подписки на событие
В ветке конфигурации «Общие -> Подписки на события» создаем новую подписку:
ДокументОбъект.РеализацияТоваровУслуг, ДокументОбъект.ОтчетОРозничныхПродажах).ОбработкаПроведения.Шаг 2. Написание программного кода
В процедуре-обработчике общего модуля мы используем стандартную коллекцию Движения объекта-источника. Проанализируем пример кода:
Процедура ОбработкаПроведенияДвиженияРН(Источник, Отказ, РежимПроведения) Экспорт
// Получаем коллекцию движений по нашему регистру
ДвиженияРегистра = Источник.Движения.ИмяВашегоРегистра;
// Создаем новую запись движения
НовоеДвижение = ДвиженияРегистра.Добавить();
НовоеДвижение.ВидДвижения = ВидДвиженияНакопления.Приход; // Или ВидДвиженияНакопления.Расход
НовоеДвижение.Период = Источник.Date; // Важно использовать актуальную дату документа
НовоеДвижение.Активность = Истина;
// Заполняем измерения
НовоеДвижение.Номенклатура = Источник.Товары.Получить(0).Номенклатура; // Пример заполнения
НовоеДвижение.Склад = Источник.Склад;
// Заполняем ресурсы
НовоеДвижение.Количество = Источник.Товары.Итог("Количество");
// Обязательно взводим флаг Записывать, иначе платформа проигнорирует этот набор записей!
ДвиженияРегистра.Записывать = Истина;
КонецПроцедуры
Плюсы данного подхода:
ОбработкаУдаленияПроведения.Этот способ часто применяется при разработке в расширениях конфигурации, когда мы не хотим или не имеем возможности изменять свойства типовых документов (снимать их с поддержки ради добавления регистра в состав движений). Запись в этом случае производится через создание явного набора записей регистра.
Программный код в обработчике подписки на проведение:
Процедура ОбработкаПроведенияДвиженияРН(Источник, Отказ, РежимПроведения) Экспорт
// Создаем набор записей регистра напрямую
НаборЗаписей = РегистрыНакопления.ИмяВашегоРегистра.СоздатьНаборЗаписей();
// Устанавливаем жесткий отбор по регистратору
НаборЗаписей.Отбор.Регистратор.Установить(Источник.Ссылка);
// Формируем записи
НоваяЗапись = НаборЗаписей.Добавить();
НоваяЗапись.Регистратор = Источник.Ссылка;
НоваяЗапись.Период = Источник.Дата;
НоваяЗапись.ВидДвижения = ВидДвиженияНакопления.Приход;
НоваяЗапись.Активность = Истина;
// Заполняем измерения и ресурсы
НоваяЗапись.Номенклатура = ...;
НоваяЗапись.Количество = ...;
// Записываем набор. Так как мы находимся внутри транзакции проведения,
// метод Записать() отработает строго в рамках этой транзакции.
НаборЗаписей.Записать();
КонецПроцедуры
Важное предупреждение: При использовании Варианта Б стандартные автоматические механизмы платформы перестают работать. Вам придется вручную компенсировать их отсутствие, иначе вы рискуете нарушить целостность данных в информационной базе.
Если вы выбрали путь прямой записи набора данных, вам обязательно нужно решить три критические проблемы:
Поскольку документ «не знает», что он делает движения по вашему регистру (ведь в метаданных документа этот регистр не отмечен), то при нажатии кнопки «Отменить проведение» платформа автоматически эти движения не удалит. Движения останутся в базе данных «зависшими», что приведет к грубому искажению отчетов.
Решение: Вам необходимо создать вторую подписку на событие — ОбработкаУдаленияПроведения для тех же документов. В обработчике этой подписки нужно программно очищать движения, записывая пустой набор записей по регистратору:
Процедура ОбработкаУдаленияПроведенияДвижений(Источник, Отказ) Экспорт
НаборЗаписей = РегистрыНакопления.ИмяВашегоРегистра.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(Источник.Ссылка);
// Запись пустого набора автоматически удаляет все существующие движения по данному регистратору
НаборЗаписей.Записать();
КонецПроцедуры
При использовании Источник.Движения платформа автоматически накладывает управляемые блокировки на таблицы СУБД, защищая данные от одновременного изменения другими сессиями пользователей. При ручном вызове метода НаборЗаписей.Записать() автоматическая блокировка по умолчанию не ставится.
В условиях многопользовательской работы это гарантированно приведет к ошибкам совместного доступа или получению неактуальных остатков при расчетах.
Решение: Перед записью набора данных в коде проведения необходимо явно объявлять объект БлокировкаДанных, накладывая исключительную блокировку на регистр по нашему регистратору или ключевым измерениям (номенклатуре, складу).
Обратите внимание, что в обработчике события ОбработкаПроведения у объекта Источник гарантированно заполнена ссылка Источник.Ссылка, так как документ уже сохранен в базе данных перед началом транзакции проведения.
Однако если вы попытаетесь использовать подписки на более ранних этапах жизненного цикла документа (например, ПередЗаписью или ПриЗаписи), у нового (еще ни разу не сохраненного) документа ссылки в свойстве Источник.Ссылка не будет. Попытка установить отбор по пустой ссылке вызовет ошибку времени выполнения. Запись движений возможна только тогда, когда ссылка физически существует в базе данных.
Подведем итог нашему анализу: