В практике разработки на платформе 1С:Предприятие часто возникает задача: при проведении документа (например, «Реализация товаров и услуг») необходимо автоматически изменить статус или какой-либо реквизит в документе-основании (например, в «Заказе клиента»). Несмотря на кажущуюся простоту, эта задача таит в себе множество подводных камней: от блокировок базы данных и проблем с закрытыми периодами до бесконечных циклов перепроведения. Рассмотрим подробнее, как реализовать этот функционал правильно, эффективно и с соблюдением стандартов разработки.
Разберем типичную ситуацию: разработчик прописывает код изменения документа-основания непосредственно в процедуре ОбработкаПроведения текущего документа. Почему это может быть плохим решением? Во-первых, проведение документа всегда происходит внутри транзакции. Если при записи документа-основания произойдет ошибка (например, сработает проверка заполнения или документ окажется заблокирован другим пользователем), транзакция проведения текущего документа также будет отменена. Во-вторых, если документ-основание находится в закрытом для редактирования периоде, обычная запись будет заблокирована платформой.
Проанализируем основные способы решения этой задачи, начиная от самых «чистых» с точки зрения архитектуры и заканчивая практическими приемами для работы с существующими реквизитами.
Наиболее правильным решением является вынос изменяемого реквизита из состава документа в отдельный Регистр Сведений. Вместо того чтобы менять значение внутри объекта документа, мы записываем состояние в регистр, где измерением является ссылка на документ-основание, а ресурсом — необходимое значение.
Преимущества этого метода:
Для того чтобы пользователь мог видеть это значение в списке документов и выполнять по нему отборы, в 1С (особенно в обычных формах) можно использовать механизм Характеристик или соединение с таблицей регистра в динамическом списке (для управляемых форм). Рассмотрим пример кода для записи в регистр сведений при проведении:
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
// Подготовим структуру для записи в регистр
МенеджерЗаписи = РегистрыСведений.СтатусыДокументовОснований.СоздатьМенеджерЗаписи();
МенеджерЗаписи.ДокументОснование = ЭтотОбъект.ДокументОснование;
МенеджерЗаписи.Статус = Перечисления.СтатусыЗаказов.Исполнен;
МенеджерЗаписи.Период = ТекущаяДата();
Попытка
МенеджерЗаписи.Записать(Истина);
Исключение
Отказ = Истина;
Сообщить("Не удалось обновить статус основания: " + ОписаниеОшибки());
КонецПопытки;
КонецПроцедуры
Если архитектура конфигурации не позволяет использовать регистры (например, реквизит уже задействован в десятках отчетов и типовых механизмах), нам придется обновлять именно реквизит объекта. Чтобы избежать проблем с датами запрета и лишними проверками, используем свойство ОбменДанными.Загрузка = Истина.
Разберем по шагам, как это работает. Установка этого флага сообщает платформе, что запись объекта производится в режиме загрузки данных. В этом режиме пропускаются прикладные проверки в модулях (ПередЗаписью, ПриЗаписи), а также игнорируется проверка периода (в большинстве типовых конфигураций).
Посмотрим на пример реализации в подписке на событие или в модуле документа:
ОбъектОснование = ЭтотОбъект.ДокументОснование.ПолучитьОбъект();
// Проверяем, не является ли ссылка пустой и нужно ли вообще менять значение
Если ОбъектОснование <> Неопределено И ОбъектОснование.Статус <> Перечисления.Статусы.Проведено Тогда
ОбъектОснование.Статус = Перечисления.Статусы.Проведено;
// Включаем режим загрузки для обхода проверок и периодов
ОбъектОснование.ОбменДанными.Загрузка = Истина;
Попытка
// Записываем только объект, не вызывая проведение документа-основания
ОбъектОснование.Записать(РежимЗаписиДокумента.Запись);
Исключение
Инфо = ИнформацияОбОшибке();
ЗаписьЖурналаРегистрации("ОбновлениеОснования", УровеньЖурналаРегистрации.Ошибка, , , ПодробноеПредставлениеОшибки(Инфо));
КонецПопытки;
КонецЕсли;
Выясним причину, почему иногда использование записи «в лоб» приводит к конфликтам блокировок. Когда мы проводим документ «А» и внутри него записываем документ «Б», платформа накладывает блокировки на оба объекта. Если в этот момент другой процесс пытается сделать обратное, возникает Deadlock (взаимная блокировка).
Чтобы минимизировать время нахождения в транзакции и передать данные между обработчиками, используем структуру ДополнительныеСвойства. Мы можем пометить документ-основание как «требующий обновления» в модуле объекта, а саму запись произвести в подписке на событие ПриЗаписи или ОбработкаПроведения.
Рассмотрим, как это реализовать:
ОбработкаПроведения добавляем флаг: ЭтотОбъект.ДополнительныеСвойства.Вставить("ОбновитьОснование", Истина);.ПриЗаписи для этого типа документа.Это позволяет четко разделить логику проведения текущего документа и логику влияния на другие объекты системы.
Проанализируем ситуацию, когда моментальное обновление реквизита не критично (например, задержка в 10–30 секунд допустима), но крайне важна стабильность проведения основного документа. В этом случае идеальным вариантом будет использование Очереди задач.
Вместо непосредственной записи документа-основания, мы фиксируем необходимость изменения в специальном регистре (назовем его «ОчередьОбновленияДокументов»).
Алгоритм работы:
Этот подход гарантирует, что пользователь никогда не получит ошибку проведения своего документа из-за того, что кто-то другой в этот момент редактирует документ-основание.
Поскольку в обсуждении упоминались обычные формы, стоит обратить внимание на удобство работы пользователя. Если вы выбрали вариант с Регистром Сведений, для отображения данных в списке документов (журнале) используйте событие ПриВыводеСтроки. Однако помните, что это событие сильно замедляет работу при больших списках.
Более правильный путь для обычных форм — добавить в запрос списка (если используется ДокументСписок с произвольным запросом) левое соединение с вашим регистром сведений. Тогда отборы и сортировки будут работать на уровне СУБД, что обеспечит максимальную производительность.
Подводя итог, можно выстроить следующую иерархию правильности решений:
Никогда не забывайте про обработку исключений (блок Попытка...Исключение) при записи связанных объектов, так как это критическая точка, способная остановить работу пользователей в самый ответственный момент.