Как правильно использовать метод Скопировать() и почему возникают «битые ссылки» при проведении документов в 1С?

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

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

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

---

1. Как метод Скопировать() работает «под капотом»

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

Рассмотрим пример ручного заполнения:


НовыйЗаказ = Документы.ЗаказКлиента.СоздатьДокумент();
НовыйЗаказ.Дата = ТекущаяДата();
НовыйЗаказ.Контрагент = ИсточникЗаказ.Контрагент;
НовыйЗаказ.Договор = ИсточникЗаказ.Договор;
// И так далее для всех реквизитов и табличных частей...

А теперь посмотрим на платформенный метод:

НовыйЗаказ = ИсточникЗаказ.Скопировать();

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

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

---

2. Почему возникает «битая ссылка» (Разбор транзакционной ошибки)

Рассмотрим классический сценарий, приводящий к сбою: во время проведения первого заказа (Заказ А) срабатывает подписка на событие. Внутри этой подписки программно создается копия этого заказа (Заказ Б) с изменением пары реквизитов, после чего Заказ Б записывается и проводится. В результате в коде ссылка на Заказ А вроде бы существует, но на клиенте она превращается в <Объект не найден>.

Выясним причину этого поведения, проанализировав транзакционную модель 1С и СУБД:

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

---

3. Как правильно решать эту задачу: три надежных способа

Чтобы избежать транзакционных ошибок и появления «битых ссылок», нам необходимо гарантировать, что создание и проведение копии документа (Заказа Б) будет происходить вне транзакции проведения основного документа (Заказа А).

Рассмотрим три наиболее эффективных архитектурных подхода.

Вариант А. Использование фоновых заданий (асинхронный подход)

Это самый надежный и масштабируемый способ для серверного кода, особенно при автоматической обработке данных или интеграции.

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


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

Метод фонового задания работает уже в своей собственной, полностью независимой транзакции:


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

Вариант Б. Перенос логики на клиентскую сторону (для интерактивной работы)

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

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


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

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

Вариант В. Использование метода УстановитьСсылкуНового()

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

Давайте посмотрим, как это реализовать:


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

---

Резюме

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

← На главную