При разработке механизмов автоматического заполнения аналитики в платформе 1С:Предприятие 8 часто используется механизм подписок на события. Это гибкий и удобный способ внедрить общую логику для группы документов без изменения их модулей проведения. Однако начинающие (и даже опытные) разработчики периодически сталкиваются с парадоксальной ситуацией: в режиме отладки код подписки успешно выполняется, свойства движений (например, субконто бухгалтерского учета) заполняются правильными значениями, но после завершения транзакции проведения изменения в базе данных просто исчезают.
В данной статье мы подробно разберем, почему возникает подобный эффект «исчезновения» проводок, детально проанализируем поведение платформы при записи движений и найдем оптимальное решение этой проблемы.
Рассмотрим практический сценарий. Перед нами стоит задача заполнить аналитику по счету 10.6 («Прочие материалы»), а именно субконто Номенклатура и Склады, для документа ПоступлениеДопРасходов. Также для документа ПлатежныйОрдерПоступлениеДенежныхСредств необходимо заполнить субконто взаиморасчетов на счетах расчетов с контрагентами.
Разработчик создал подписку на событие ОбработкаПроведения для указанных документов со следующим кодом:
Если Отказ = Ложь Тогда
Если ТипЗнч(Источник.Ссылка)=Тип("ДокументСсылка.ПлатежныйОрдерПоступлениеДенежныхСредств") Тогда
РасчетыСПоставщикамиИПодрядчиками = ПланыСчетов.Хозрасчетный.РасчетыСПоставщикамиИПодрядчиками;
Для Каждого Проводка Из Источник.Движения.Хозрасчетный Цикл
Если Проводка.СчетДт = РасчетыСПоставщикамиИПодрядчиками ИЛИ Проводка.СчетДт.Родитель = РасчетыСПоставщикамиИПодрядчиками Тогда
БухгалтерскийУчет.УстановитьСубконто(Проводка.СчетДт, Проводка.СубконтоДт, "Контрагенты", Источник.Контрагент);
БухгалтерскийУчет.УстановитьСубконто(Проводка.СчетДт, Проводка.СубконтоДт, "Договоры", Источник.Договор);
БухгалтерскийУчет.УстановитьСубконто(Проводка.СчетДт, Проводка.СубконтоДт, "ДокументыРасчетовСКонтрагентами", Источник.Ссылка);
КонецЕсли;
КонецЦикла;
ИначеЕсли ТипЗнч(Источник.Ссылка)=Тип("ДокументСсылка.ПоступлениеДопРасходов") Тогда
ПрочиеМатериалы = ПланыСчетов.Хозрасчетный.ПрочиеМатериалы;
Для Каждого Проводка Из Источник.Движения.Хозрасчетный Цикл
Если Проводка.СчетКт = ПрочиеМатериалы Тогда
БухгалтерскийУчет.УстановитьСубконто(Проводка.СчетКт, Проводка.СубконтоКт, "Номенклатура", Источник.Номенклатура);
БухгалтерскийУчет.УстановитьСубконто(Проводка.СчетКт, Проводка.СубконтоКт, "Склады", Источник.Склад);
КонецЕсли;
КонецЦикла;
КонецЕсли;
КонецЕсли;
При тестировании выяснилось следующее поведение системы:
ПлатежныйОрдерПоступлениеДенежныхСредств код отрабатывает великолепно — субконто успешно заполняются и сохраняются в базе данных.ПоступлениеДопРасходов в режиме отладки мы четко видим, что коллекция субконто заполняется значениями Источник.Номенклатура и Источник.Склад. Однако после окончания проведения документа движения в регистре бухгалтерии по-прежнему остаются пустыми (без субконто).Давайте разберемся, почему один и тот же алгоритм в рамках одной и той же подписки ведет себя абсолютно по-разному для двух этих документов.
Секрет такого поведения кроется во внутреннем жизненном цикле записи движений документов в платформе «1С:Предприятие 8». Поведение наборов записей регистров при проведении регулируется свойством документа «Запись движений при проведении» (обычно оно установлено в значение Записывать выбранные).
Рассмотрим по шагам, как платформа работает с коллекциями движений в процессе транзакции проведения:
Шаг 1. Инициализация и запуск обработчика проведения. Когда пользователь проводит документ, платформа открывает транзакцию базы данных и запускает процедуру ОбработкаПроведения в модуле объекта самого документа.
Шаг 2. Заполнение движений в модуле документа. Происходит формирование проводок. Разработчики типовых конфигураций могут использовать разные подходы к записи этих движений:
ПлатежныйОрдерПоступлениеДенежныхСредств проводки формируются в памяти, записываются в коллекцию Движения.Хозрасчетный, и для этой коллекции устанавливается свойство Записывать = Истина. Сама физическая запись набора в СУБД происходит автоматически платформой в самом конце транзакции проведения, уже после выполнения всех подписок на события.ПоступлениеДопРасходов разработчики конфигурации применили явный вызов метода записи набора непосредственно в коде модуля документа. То есть внутри обработчика проведения вызывается метод Движения.Хозрасчетный.Записать() (или аналогичный общий метод, выполняющий принудительный сброс буфера в базу данных).Шаг 3. Что происходит со свойством «Записывать» после явного вызова? Это ключевой момент. Как только выполняется метод Движения.Записать() для конкретного регистра, платформа физически сохраняет проводки в СУБД. Сразу после этого платформа автоматически сбрасывает флаг Записывать этого набора записей в значение Ложь. Это делается для оптимизации: платформа «понимает», что движения уже актуализированы в базе данных, и повторно записывать их при завершении транзакции нет никакой необходимости.
Шаг 4. Выполнение подписки на событие. Подписка на событие ОбработкаПроведения выполняется после того, как отработал весь основной код модуля документа. На момент входа в процедуру нашей подписки мы имеем следующую картину:
Движения.Хозрасчетный.Записывать равен Истина.Записать() в модуле) движения уже лежат в базе данных, а в оперативной памяти флаг Движения.Хозрасчетный.Записывать равен Ложь.Шаг 5. Потеря изменений. В коде подписки мы перебираем коллекцию движений в памяти. Поскольку эти движения доступны для чтения и модификации, отладчик без проблем показывает нам успешное заполнение субконто. Однако, когда транзакция завершается, платформа анализирует свойство Записывать каждого регистра. Видя у регистра Хозрасчетный второго документа значение Ложь, платформа решает, что набор записей не изменялся с момента последней физической записи, и просто игнорирует его, завершая транзакцию. Изменения, внесенные в подписке, бесследно теряются.
Чтобы заставить платформу повторно проанализировать и физически обновить измененный нами набор записей в базе данных, необходимо явно сообщить ей о наличии изменений. Для этого достаточно установить свойство Записывать регистра в значение Истина непосредственно перед модификацией или сразу после неё.
Давайте посмотрим на скорректированный и правильный вариант кода нашей подписки на событие:
Если Отказ = Ложь Тогда
Если ТипЗнч(Источник.Ссылка)=Тип("ДокументСсылка.ПлатежныйОрдерПоступлениеДенежныхСредств") Тогда
РасчетыСПоставщикамиИПодрядчиками = ПланыСчетов.Хозрасчетный.РасчетыСПоставщикамиИПодрядчиками;
Для Каждого Проводка Из Источник.Движения.Хозрасчетный Цикл
Если Проводка.СчетДт = РасчетыСПоставщикамиИПодрядчиками ИЛИ Проводка.СчетДт.Родитель = РасчетыСПоставщикамиИПодрядчиками Тогда
БухгалтерскийУчет.УстановитьСубконто(Проводка.СчетДт, Проводка.СубконтоДт, "Контрагенты", Источник.Контрагент);
БухгалтерскийУчет.УстановитьСубконто(Проводка.СчетДт, Проводка.СубконтоДт, "Договоры", Источник.Договор);
БухгалтерскийУчет.УстановитьСубконто(Проводка.СчетДт, Проводка.СубконтоДт, "ДокументыРасчетовСКонтрагентами", Источник.Ссылка);
КонецЕсли;
КонецЦикла;
// На всякий случай гарантируем запись для первого документа
Источник.Движения.Хозрасчетный.Записывать = Истина;
ИначеЕсли ТипЗнч(Источник.Ссылка)=Тип("ДокументСсылка.ПоступлениеДопРасходов") Тогда
ПрочиеМатериалы = ПланыСчетов.Хозрасчетный.ПрочиеМатериалы;
Для Каждого Проводка Из Источник.Движения.Хозрасчетный Цикл
Если Проводка.СчетКт = ПрочиеМатериалы Тогда
БухгалтерскийУчет.УстановитьСубконто(Проводка.СчетКт, Проводка.СубконтоКт, "Номенклатура", Источник.Номенклатура);
БухгалтерскийУчет.УстановитьСубконто(Проводка.СчетКт, Проводка.СубконтоКт, "Склады", Источник.Склад);
КонецЕсли;
КонецЦикла;
// КЛЮЧЕВОЕ ИСПРАВЛЕНИЕ: Информируем платформу о необходимости перезаписать набор записей в БД
Источник.Движения.Хозрасчетный.Записывать = Истина;
КонецЕсли;
КонецЕсли;
Добавление строчки Источник.Движения.Хозрасчетный.Записывать = Истина; полностью решает проблему. Теперь платформа видит, что набор данных был изменен в памяти, и в процессе завершения транзакции принудительно перезаписывает движения с новыми значениями аналитики в СУБД.
В процессе обсуждения на форуме автор выразила опасение, что проблема могла быть вызвана тем, что субконто для счета 10.6 были добавлены непосредственно в режиме «1С:Предприятие» (пользовательская настройка плана счетов), а не в конфигураторе.
Проясним этот технический момент. Напрямую на физическую запись наборов данных в базу способ создания видов субконто не влияет. Механизм сохранения одинаков как для предопределенных аналитик, так и для пользовательских. Тем не менее, здесь есть важный методологический нюанс:
БухгалтерскийУчет.УстановитьСубконто() динамически считывает метаданные счета и распределяет переданные значения по доступным субконто."Номенклатура"), а не по жестко зашитым индексам, так как порядок следования субконто на счете в режиме «1С:Предприятие» может отличаться в разных базах данных или после реструктуризации планов счетов.Хотя предложенное решение с установкой флага Записывать = Истина полностью устраняет проблему незаполненных данных, с точки зрения оптимизации высоконагруженных систем у этого подхода есть скрытый недостаток. Это так называемый эффект «двойной записи».
Проанализируем физический процесс в СУБД:
Движения.Записать(). На уровне СУБД происходит операция DELETE (очистка старых движений) и INSERT (запись новых проводок). На таблицы накладываются эксклюзивные блокировки.Записывать = Истина.На базах данных с небольшим объемом документов эта задержка незаметна. Но если у вас крупная информационная система с сотнями одновременно работающих пользователей и многострочными документами, двойная запись одного и того же регистра может приводить к избыточным блокировкам, ожиданиям на СУБД и ошибкам времени ожидания блокировок (Deadlocks).
Чтобы избежать двойной записи и сделать код более производительным, в современных конфигурациях на платформе 8.3 (особенно использующих БСП — Библиотеку стандартных подсистем) применяются альтернативные архитектурные паттерны.
Вместо подписки на событие, которая выполняется «в самом конце», мы можем использовать расширение конфигурации. Мы перехватываем процедуру, подготавливающую движения конкретного документа, с помощью аннотации &После или &Перед в модуле проведения документа (или в соответствующем общем модуле формирования движений).
Это позволяет нам модифицировать массив или таблицу проводок в памяти до того момента, как будет вызван метод Движения.Записать(). Таким образом, движения запишутся в базу данных только один раз, уже со всеми необходимыми субконто.
В современных конфигурациях (таких как БП 3.0, ERP 2.5, КА 2.5) разработчики ушли от практики хаотичной записи движений в модулях документов. Все движения накапливаются в специальной коллекции в оперативной памяти в течение всего процесса проведения, а их финальная запись происходит централизованно один раз в самом конце транзакции.
Для модификации движений используются специальные программные интерфейсы БСП, такие как регистрация дополнительных реквизитов или встраивание в механизмы заполнения свойств документа через переопределяемые общие модули. Это гарантирует отсутствие двойных транзакционных записей и исключает проблемы с потерей флага Записывать.
Если перед вами стоит задача гарантировать корректное сохранение измененных движений регистра в подписке на событие проведения, руководствуйтесь следующим чек-листом:
Движения.[ИмяРегистра].Записывать в конце вашей процедуры в подписке.[НаборЗаписей].Записывать = Истина;.БухгалтерскийУчет.УстановитьСубконто()), передавая имя вида субконто строкой.ОбработкаПроведения для тяжелых документов, заменяя их расширениями конфигурации, чтобы предотвратить избыточную двойную запись в СУБД.