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

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

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

Начинающие разработчики часто решают эту задачу «в лоб» — записывая родительский документ непосредственно на сервере из контекста формы создаваемого документа. Мы подробно разберем, почему такое решение является опасным антипаттерном, к каким критическим ошибкам в многопользовательском режиме оно приводит, и как реализовать эту задачу правильно в соответствии со стандартами разработки фирмы «1С».

Анализ проблемного решения (почему код автора темы использовать нельзя)

Рассмотрим логику, которую предложил автор в сообщении №5. При записи документа Перевозка на сервере в процедуре ПослеЗаписиНаСервере выполняется следующий код:


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

Давайте выясним причину, почему данный подход крайне нежелателен для использования в реальных базах данных с несколькими пользователями:

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

Ниже мы рассмотрим два правильных и безопасных способа решения этой задачи на управляемых формах 1С.

Решение №1: Использование механизма клиентских оповещений (Рекомендуемый стандарт)

Основное правило разработки в управляемом приложении 1С: **интерактивные действия должны происходить на клиенте**, не затрагивая базу данных напрямую до тех пор, пока сам пользователь не выразит явное намерение сохранить изменения.

Для реализации этого подхода мы применим встроенный механизм оповещений платформы. Разберем этот процесс по шагам.

Шаг 1. Передаем основание при открытии новой формы

В форме документа-родителя (Заявка) пишем код команды, которая будет открывать форму создания подчиненного документа Перевозка:


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

Шаг 2. Отправляем оповещение при записи подчиненного документа

В форме документа Перевозка переопределяем стандартный клиентский обработчик события ПослеЗаписи. Когда пользователь нажмет кнопку «Записать» или «Провести», форма отправит глобальное оповещение по всей системе:


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

Шаг 3. Ловим оповещение в форме исходного документа

Возвращаемся в форму документа Заявка. Нам необходимо переопределить стандартный обработчик события управляемой формы ОбработкаОповещения. Эта процедура сработает на клиенте в тот момент, когда подчиненный документ запишется:


&НаКлиенте
Процедура ОбработкаОповещения(ИмяСобытия, Параметр, Источник)
    
    Если ИмяСобытия = "СозданаНоваяПеревозка" Тогда
        
        // Убеждаемся, что оповещение пришло именно для ТЕКУЩЕЙ открытой заявки
        Если Параметр.ЗаявкаОснование = Объект.Ссылка Тогда
            
            // Защита от дублирования: проверяем, нет ли уже этой ссылки в табличной части
            ПараметрыОтбора = Новый Структура("Документ", Параметр.СсылкаПеревозки);
            НайденныеСтроки = Объект.ТЧ_Перевозки.НайтиСтроки(ПараметрыОтбора);
            
            Если НайденныеСтроки.Количество() = 0 Тогда
                
                // Добавляем строку на клиенте прямо в данные формы! Без серверного вызова.
                НоваяСтрока = Объект.ТЧ_Перевозки.Добавить();
                НоваяСтрока.Документ = Параметр.СсылкаПеревозки;
                
                // Устанавливаем признак модифицированности формы, чтобы пользователь сохранил изменения в Заявке
                ЭтаФорма.Модифицированность = Истина;
                
            КонецЕсли;
            
        КонецЕсли;
        
    КонецЕсли;
    
КонецПроцедуры

Преимущества данного решения:

Решение №2: Использование асинхронного описания оповещения о закрытии

Бывают ситуации, когда логика бизнес-процесса требует, чтобы строка добавлялась в табличную часть родительского документа строго в момент закрытия подчиненной формы. Для таких случаев в платформе предусмотрен механизм асинхронного вызова с использованием объекта ОписаниеОповещения.

Рассмотрим, как реализовать этот сценарий по шагам.

Шаг 1. Открытие формы с обратным вызовом

В форме документа Заявка при нажатии кнопки мы подготавливаем специальный объект обратного вызова:


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

Шаг 2. Описание логики в процедуре обратного вызова

В этой же форме Заявки пишем экспортную клиентскую процедуру, которая примет результат закрытия формы Перевозки:


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

Шаг 3. Передача результата из закрывающейся формы

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


&НаКлиенте
Процедура ПередЗакрытием(Отказ, СтандартнаяОбработка)
    
    // Если документ записан и имеет ссылку, передаем ее в качестве результата закрытия
    Если ЗначениеЗаполнено(Объект.Ссылка) Тогда
        Закрыть(Объект.Ссылка);
    КонецЕсли;
    
КонецПроцедуры

Архитектурный совет: как избежать взаимных ссылок?

Как совершенно верно отметил коллега в сообщении №15, ситуация, когда документ Заявка хранит ссылку на Перевозку в табличной части, а Перевозка ссылается на Заявку через реквизит — это архитектурная ловушка.

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

Вместо этого мы рекомендуем использовать независимый или подчиненный Регистр сведений (например, назвать его СвязиДокументов или ПеревозкиПоЗаявкам):

При записи документа Перевозка записи в этот регистр будут создаваться автоматически (через механизм движений или подписку на события). А в формах документов Заявка и Перевозка вместо табличных частей вы сможете вывести динамические списки, настроенные на этот регистр сведений. Это полностью устранит дублирование информации, облегчит процесс пометки на удаление и ускорит работу всей конфигурации.

← На главную