Как правильно скопировать подчиненные элементы справочника при копировании документа в 1С?

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

В практике разработки на платформе 1С:Предприятие 8.3 часто возникает классическая задача: при копировании пользователем существующего документа необходимо автоматически скопировать и все связанные с ним элементы подчиненного справочника. На первый взгляд, задача кажется тривиальной — достаточно перехватить событие копирования и переписать данные. Однако на деле разработчики сталкиваются с целым рядом архитектурных преград: от некорректного поведения интерфейса до появления "битых" ссылок в базе данных.

В рамках этой статьи мы подробно проанализируем, почему стандартные подходы вроде записи объекта в обработчике ПриКопировании или использования механизма УстановитьСсылкуНового в управляемых формах приводят к ошибкам. Также мы разберем по шагам и реализуем наиболее надежное, изящное и безопасное решение, которое соответствует стандартам разработки фирмы "1С".

Проблема 1. Почему вызов метода Записать() в событии ПриКопировании — это критическая ошибка?

Рассмотрим ситуацию, когда разработчик пытается решить задачу «в лоб»: перехватывает событие ПриКопировании в модуле объекта (или в одноименной подписке на событие) и вызывает внутри него метод Записать() для получения ссылки нового документа.

При интерактивном копировании документа в режиме пользователя платформа 1С выполняет следующие действия:

  1. Создает в оперативной памяти компьютера новый, еще не записанный объект.
  2. Заполняет его реквизиты данными из документа-источника.
  3. Вызывает обработчик ПриКопировании.
  4. Инициирует открытие управляемой формы для этого нового объекта.

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

Проблема 2. Коварство метода УстановитьСсылкуНового в управляемых формах

В попытках обойти преждевременную запись документа разработчики часто прибегают к следующей красивой схеме:

  1. Заранее сгенерировать уникальный идентификатор (UUID).
  2. Получить пустую ссылку на основе этого UUID с помощью метода ПолучитьСсылку().
  3. Применить к новому документу метод УстановитьСсылкуНового().
  4. Создать подчиненные элементы справочника, сразу указав эту зарезервированную ссылку в качестве реквизита Владелец.

В обычных формах (в старых редакциях платформ) этот подход работал отлично. Однако в управляемом приложении этот механизм имеет критическую уязвимость.

Дело в том, что в управляемом интерфейсе данные между клиентом и сервером передаются не в виде полноценных прикладных объектов, а в виде специализированной XML-структуры ДанныеФормыСтруктура. В процессе этой сериализации и конвертации объекта при открытии его формы установленная в обработчике ПриКопировании ссылка нового попросту теряется. При последующей интерактивной записи документа пользователем платформа сгенерирует для него совершенно другой, новый UUID. В результате все созданные вами подчиненные элементы останутся привязаны к старому (несуществующему) UUID, превратившись в базу «битых» ссылок со значением <Объект не найден>.

Решение. Современный стандартный подход для управляемого приложения (Best Practice)

Выясним причину успеха правильного алгоритма: наиболее надежный способ скопировать подчиненные элементы — отложить их создание до момента фактической и осознанной записи родительского документа пользователем. Реализуется это с помощью связки управляемой формы и модуля объекта через временное хранение данных в ДополнительныеСвойства.

Давайте разберем по шагам процесс реализации этой логики.

Шаг 1. Перехват факта копирования в управляемой форме документа

В модуле управляемой формы нашего документа найдем предопределенное событие ПриСозданииНаСервере. Платформа при интерактивном копировании всегда передает в параметры формы ссылку на исходный документ в параметре Параметры.ЗначениеКопирования. Мы сохраним эту ссылку во временном реквизите формы.

Сначала добавим в форму документа новый реквизит с именем ДокументИсточникКопирования (тип значения — ссылка на наш документ).

Затем напишем следующий код в модуле формы:


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

Шаг 2. Передача ссылки на источник в модуль объекта при записи

Когда пользователь заполняет все необходимые поля в форме скопированного документа и нажимает кнопку «Записать», срабатывает серверное событие формы ПередЗаписьюНаСервере. Нам нужно передать ссылку на исходный документ из реквизита формы в специальную структуру ДополнительныеСвойства записываемого прикладного объекта.

Пропишем в модуле формы следующий код:


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

Шаг 3. Копирование подчиненных элементов в событии ПриЗаписи документа

Перейдем в модуль объекта нашего документа (или создадим подписку на событие ПриЗаписи). К моменту вызова обработчика ПриЗаписи новый документ уже гарантированно записан в базу данных, транзакция активна, и у объекта есть постоянная, валидная ссылка ЭтотОбъект.Ссылка.

Напишем процедуру, которая считает переданный параметр, найдет подчиненные элементы исходного документа и скопирует их, привязав к новому владельцу:


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

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

Решение для программного копирования (без интерактивных форм)

Рассмотрим подробнее сценарий, когда копирование документа выполняется не пользователем в интерфейсе, а чисто программным кодом (например, в фоновом задании, при обработке обмена данными или в регламентной процедуре). В таком сценарии события формы задействованы не будут.

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


Процедура ПрограммноеКопированиеДокумента(ДокументИсточникСсылка)
    
    // 1. Создаем копию документа в оперативной памяти
    НовыйДокумент = ДокументИсточникСсылка.ПолучитьОбъект().Скопировать();
    
    // 2. Генерируем постоянную ссылку для нового документа заранее
    НоваяСсылкаДокумента = Документы.ИмяВашегоДокумента.ПолучитьСсылку();
    НовыйДокумент.УстановитьСсылкуНового(НоваяСсылкаДокумента);
    
    // 3. Копируем и сразу записываем подчиненные элементы справочника, привязав к будущей ссылке
    Выборка = Справочники.ПодчиненныйСправочник.Выбрать(, ДокументИсточникСсылка);
    Пока Выборка.Следующий() Цикл
        
        КопияЭлемента = Выборка.Ссылка.Скопировать();
        КопияЭлемента.Владелец = НоваяСсылкаДокумента; // Используем зарезервированную ссылку владельца
        
        Попытка
            КопияЭлемента.Записать();
        Исключение
            ЗаписьЖурналаРегистрации("КопированиеДокументов", УровеньЖурналаРегистрации.Ошибка,,, 
                "Ошибка копирования подчиненного элемента: " + ОписаниеОшибки());
            Возврат;
        КонецПопытки;
        
    КонецЦикла;
    
    // 4. Записываем сам документ. Он запишется с тем UUID, который мы жестко закрепили на шаге 2
    Попытка
        НовыйДокумент.Записать();
    Исключение
        ВызватьИсключение "Не удалось записать скопированный документ: " + ОписаниеОшибки();
    КонецПопытки;
    
КонецПроцедуры

Архитектурный совет: подчиненный справочник или табличная часть?

В завершение разбора темы важно затронуть важный архитектурный аспект. Подчинение справочника документу — это паттерн проектирования метаданных в 1С, который стоит использовать с большой осторожностью.

Если подчиненные данные жестко привязаны к жизненному циклу документа и не используются другими объектами системы независимо, их гораздо правильнее хранить в Табличной части (ТЧ) самого документа.

Почему использование Табличной части эффективнее:

Использовать подчиненный справочник стоит только в тех редких случаях, когда количество подчиненных строк исчисляется десятками тысяч (что может перегрузить документ при открытии), либо когда к элементам подчиненного справочника требуется обращаться напрямую из других разделов системы (например, использовать их в качестве субконто в бухгалтерском учете).

← На главную