Как правильно изменить реквизит исходного документа при создании на основании в 1С?

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

В практике разработки на платформе 1С:Предприятие 8 часто возникает задача: при создании одного документа на основании другого (например, документа «Реализация товаров и услуг» на основании «Заказа клиента») нужно изменить состояние или какой-либо реквизит исходного (родительского) документа. Первое интуитивное решение, которое приходит в голову начинающему разработчику — это просто перезаписать родительский документ «на лету» в момент заполнения или проведения нового документа. Однако такое решение является грубой архитектурной ошибкой и неизбежно приводит к проблемам в многопользовательской среде.

В этой статье мы подробно разберем, почему стандартные «простые» варианты изменения родительского документа не работают стабильно, к каким тяжелым последствиям для базы данных они приводят, и рассмотрим правильные, профессиональные архитектурные подходы (Best Practices) для решения этой задачи.

Начнем с детального анализа проблемных подходов, которые часто пытаются применить на практике.

---

Почему нельзя изменять родительский документ в обработчике ОбработкаЗаполнения

Рассмотрим ситуацию: пользователь нажимает кнопку «Создать на основании». В созданном Документе 2 вызывается стандартный обработчик ОбработкаЗаполнения. Разработчик решает в этот момент получить объект Документа 1, поменять его реквизит (например, статус «В работе» на «Оформлен») и записать его.

Посмотрим на пример такого опасного кода:


// В модуле объекта Документа 2 (подчиненного)
Процедура ОбработкаЗаполнения(ДанныеЗаполнения, ТекстЗаполнения, СтандартнаяОбработка)
    
    Если ТипЗнч(ДанныеЗаполнения) = Тип("ДокументСсылка.Документ1") Тогда
        // Заполняем реквизиты Документа 2
        ЭтотОбъект.ДокументОснование = ДанныеЗаполнения;
        ЭтотОбъект.Организация = ДанныеЗаполнения.Организация;
        
        // ОПАСНО! Попытка изменить и записать Документ 1
        ОбъектОснования = ДанныеЗаполнения.ПолучитьОбъект();
        ОбъектОснования.СтатусДокумента = Перечисления.Статусы.Оформлен;
        Попытка
            ОбъектОснования.Записать(РежимЗаписиДокумента.Запись);
        Исключение
            ЗаписьЖурналаРегистрации("Ошибка записи основания",,,, ОписаниеОшибки());
        КонецПопытки;
    КонецЕсли;
    
КонецПроцедуры

Выясним причину, почему этот код крайне опасен для стабильной работы системы:

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

---

Почему нельзя изменять родительский документ в обработчике ОбработкаПроведения

Поняв бесперспективность изменения реквизитов при заполнении формы, разработчики часто переносят код в обработчик проведения подчиненного документа (Документ 2). Логика кажется разумной: Документ 2 гарантированно проводится и записывается, значит, именно в этот момент можно изменить статус Документа 1.

Однако и этот подход таит в себе огромные риски для производительности базы данных:

  1. Существенное увеличение времени транзакции. Проведение документа в 1С всегда выполняется внутри неявной транзакции базы данных. Если внутри этой транзакции мы начинаем изменять и записывать другой ссылочный объект (Документ 1), СУБД вынуждена заблокировать таблицы обоих документов. Время проведения увеличивается в разы.
  2. Взаимные блокировки (Deadlocks). В условиях многопользовательской работы это прямой путь к ошибкам совместного доступа. Например, если один пользователь проводит Документ 2, который пытается заблокировать и перезаписать Документ 1, а в этот же самый момент другой процесс (или регламентное задание) обращается к Документу 1 и пытается обновить связанные данные, СУБД зафиксирует взаимную блокировку и аварийно завершит одну из транзакций. Пользователи начнут массово получать сообщения о превышении времени ожидания блокировки.
  3. Риск зацикливания транзакций. Если у Документа 1 при его записи также прописана сложная логика в обработчиках проведения или записи (например, пересчет каких-то итогов, создание третьих документов), это может привести к бесконечной рекурсии или непредсказуемым ошибкам вида «В данной транзакции уже происходили ошибки». Источник такой ошибки в логах будет найти крайне тяжело.

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

Давайте теперь разберем по шагам правильные архитектурные решения этой задачи.

---

Решение 1: Динамическое вычисление состояния (Отказ от хранения реквизита)

Самое правильное и элегантное решение с точки зрения архитектуры СУБД — это полный отказ от хранения избыточного реквизита «Статус» или «Оформлен» в самом Документе 1. Вместо того чтобы физически записывать признак «Оформлен» в Документ 1, мы можем вычислять этот статус динамически в любой момент времени.

Рассмотрим, как это реализовать на практике. Документ 2 в любом случае хранит в себе ссылку на Документ 1 в качестве документа-основания (например, в реквизите ДокументОснование или в табличной части). Мы можем в реальном времени определять, создан ли подчиненный документ.

Для вывода статуса в списке документов или на форме Документа 1 мы используем запрос. Пример такого запроса:


ВЫБРАТЬ
    ДокОснование.Ссылка КАК Ссылка,
    ДокОснование.Номер КАК Номер,
    ДокОснование.Дата КАК Дата,
    ВЫБОР
        КОГДА ПодчиненныйДок.Ссылка ИС О КЛАССУ(Документ.Документ2) ТОГДА "Оформлен"
        ИНАЧЕ "Не оформлен"
    КОНЕЦ КАК СтатусОформления
ИЗ
    Документ.Документ1 КАК ДокОснование
    ЛЕВОЕ СОЕДИНЕНИЕ Документ.Документ2 КАК ПодчиненныйДок
        ПО ПодчиненныйДок.ДокументОснование = ДокОснование.Ссылка
        И ПодчиненныйДок.Проведен = ИСТИНА

Этот запрос можно легко интегрировать в настройки динамического списка формы списка Документа 1.

Преимущества этого подхода:

---

Решение 2: Использование независимого регистра сведений

Если бизнес-логика все же требует жестко фиксировать факт создания подчиненного документа (например, нужно хранить дату оформления, автора или дополнительные параметры связи), и простого запроса к подчиненным документам недостаточно, лучшим решением будет использование независимого регистра сведений.

Создадим новый метаданный — независимый Регистр Сведений СостоянияДокументов со следующей структурой:

Теперь рассмотрим, как правильно выполнять запись в этот регистр при проведении Документа 2. Нам не нужно открывать и перезаписывать тяжелый объект Документа 1. Запись в независимый регистр происходит очень быстро и не вызывает конфликтов блокировки объектных таблиц.

В модуле проведения Документа 2 пишем следующий код:


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

Не забудьте также очищать эту запись в обработчике ОбработкаУдаленияПроведения Документа 2, чтобы при отмене проведения статус корректно сбрасывался.

Преимущества использования регистра сведений:

---

Решение 3: Интерактивное обновление интерфейса (Оповещение на клиенте)

Иногда требование изменить реквизит родительского документа продиктовано исключительно желанием пользователя «сразу увидеть изменения на экране». Например, у пользователя на одном мониторе открыта форма Документа 1, на другом — форма Документа 2. Он проводит Документ 2 и хочет, чтобы на форме Документа 1 статус сразу же визуально поменялся.

Для реализации этой логики не нужно делать тяжелых серверных транзакционных записей. Мы можем решить эту задачу интерактивно на стороне клиента с помощью механизма оповещений платформы 1С.

Разберем по шагам, как настроить такую связку:

Шаг 1. В форме Документа 2 (подчиненного) после успешной записи или проведения отправляем глобальное клиентское оповещение:


// В модуле формы Документа 2
&НаКлиенте
Процедура ПослеЗаписи(ПараметрыЗаписи)
    
    Если ЗначениеЗаполнено(Объект.ДокументОснование) Тогда
        // Отправляем оповещение всей системе
        ПараметрыОповещения = Новый Структура;
        ПараметрыОповещения.Вставить("ДокументОснование", Объект.ДокументОснование);
        ПараметрыОповещения.Вставить("Статус", "Проведен");
        
        Оповестить("ИзменениеСтатусаОснования", ПараметрыОповещения, ЭтотОбъект);
    КонецЕсли;
    
КонецПроцедуры

Шаг 2. В форме Документа 1 (родительского) подписываемся на это оповещение и динамически обновляем форму на экране пользователя без блокировки объекта в базе данных:


// В модуле формы Документа 1
&НаКлиенте
Процедура ОбработкаОповещения(ИмяСобытия, Параметр, Источник)
    
    Если ИмяСобытия = "ИзменениеСтатусаОснования" Тогда
        // Проверяем, что событие относится именно к нашему текущему документу
        Если Параметр.ДокументОснование = Объект.Ссылка Тогда
            // Вызываем серверный метод для обновления отображения данных на форме
            ОбновитьСтатусНаФормеСервер();
        КонецЕсли;
    КонецЕсли;
    
КонецПроцедуры

&НаСервере
Процедура ObnovitStatusNaFormeServer()
    // Здесь мы можем, например, перечитать данные формы с сервера
    // или программно изменить реквизит формы (но не сам объект в базе!)
    ЭтаФорма.Прочитать(); 
КонецПроцедуры

Такой подход обеспечивает идеальный пользовательский опыт (UX) — данные на экранах пользователей обновляются мгновенно и динамически, но при этом серверная часть базы данных работает максимально быстро и надежно, без лишних транзакций и блокировок.

---

Резюме: какую стратегию выбрать?

Подведем итог нашему анализу и определим лучшее решение в зависимости от условий вашей задачи:

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

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

← На главную