В процессе разработки на платформе 1С:Предприятие 7.7 часто возникает задача дублирования данных. Обычное копирование элемента справочника штатными средствами платформы (клавиша F9) создает лишь копию самого элемента, но не затрагивает записи подчиненных ему справочников. Это создает массу неудобств для пользователя, которому приходится вручную переносить данные, например, технологические операции материала или спецификации. Рассмотрим, как решить эту задачу программно, разберем типичные ошибки и создадим универсальный инструмент для копирования.
Давайте проанализируем ситуацию. Когда мы копируем элемент (владельца), система создает новый объект и заполняет его реквизиты. Однако связи в 1С 7.7 работают по ссылочному принципу. Подчиненные записи справочника «хранят» ссылку на своего владельца. При создании копии владельца его внутренний идентификатор (ID) меняется, а старые подчиненные записи продолжают ссылаться на исходный элемент. Чтобы решить проблему, нам нужно программно найти все записи, привязанные к «старому» объекту, и создать их точные копии, указав в качестве владельца «новый» объект.
Рассмотрим распространенную ошибку, с которой сталкиваются разработчики при попытке реализовать копирование в модуле формы. Основная проблема заключается в моменте записи. Если мы попытаемся создать подчиненные записи до того, как основной элемент был физически записан в базу данных, мы получим ошибку: «Не выбран элемент справочника-хозяина!». Это происходит потому, что у нового элемента еще нет ссылки (он еще не существует в таблице базы данных).
Рассмотрим первый способ реализации — непосредственно в модуле формы элемента справочника. Нам необходимо «запомнить» объект-источник в момент копирования и выполнить перенос данных после фиксации нового элемента в базе.
Для начала определим переменную в начале модуля формы:
Перем ОбъектКопирования;
Затем используем предопределенную процедуру ВводНового, которая срабатывает при создании нового элемента, в том числе и при копировании (если второй параметр равен 1):
Процедура ВводНового(ФлагКопирования, ЭлементИсточник)
Если ФлагКопирования = 1 Тогда
// Запоминаем ссылку на оригинал, чтобы потом прочитать его подчиненные справочники
ОбъектКопирования = ЭлементИсточник;
КонецЕсли;
КонецПроцедуры
Теперь перейдем к самому важному этапу — переносу данных. Это лучше всего делать в процедуре ПриЗаписи. Разберем логику по шагам: сначала мы проверяем, было ли это копированием, затем убеждаемся, что новый элемент записан, и только потом запускаем цикл по подчиненному справочнику.
Процедура ПриЗаписи()
Если ПустоеЗначение(ОбъектКопирования) = 0 Тогда
// Сначала сохраняем самого владельца, чтобы получить ссылку
// В 1С 7.7 в процедуре ПриЗаписи() элемент уже считается записанным,
// но для надежности можно проверить ТекущийЭлемент().Выбран()
СпрПодч_Источник = СоздатьОбъект("Справочник.МатериалыОперации");
СпрПодч_Приемник = СоздатьОбъект("Справочник.МатериалыОперации");
// Устанавливаем фильтр по владельцу-источнику
СпрПодч_Источник.ИспользоватьВладельца(ОбъектКопирования);
СпрПодч_Источник.ВыбратьЭлементы();
Пока СпрПодч_Источник.ПолучитьЭлемент() = 1 Цикл
СпрПодч_Приемник.Новый();
// Устанавливаем владельцем наш новый (только что записанный) элемент
СпрПодч_Приемник.ИспользоватьВладельца(ТекущийЭлемент());
// Копируем реквизиты
СпрПодч_Приемник.ВидРабот = СпрПодч_Источник.ВидРабот;
СпрПодч_Приемник.НормаВремени = СпрПодч_Источник.НормаВремени;
// Записываем новую подчиненную запись
СпрПодч_Приемник.Записать();
КонецЦикла;
// Очищаем переменную, чтобы не скопировать повторно при следующем нажатии "Записать"
ОбъектКопирования = "";
КонецЕсли;
КонецПроцедуры
Если в вашей конфигурации много справочников с подчиненными структурами, прописывать код в каждой форме нерационально. Проанализируем более элегантный подход — создание универсальной функции в Глобальном модуле, которая будет автоматически определять структуру подчиненности через метаданные.
Посмотрим на пример такой функции, которая рекурсивно обходит все подчиненные справочники:
Функция глСкопироватьСправочник(ОбъектИсточник, НовыйВладелец = 0) Экспорт
Если ПустоеЗначение(ОбъектИсточник) = 1 Тогда
Возврат "";
КонецЕсли;
ВидСпр = ОбъектИсточник.Вид();
НовыйОбъект = СоздатьОбъект("Справочник." + ВидСпр);
// Обработка родителя для иерархических справочников
Если ОбъектИсточник.Уровень() > 1 Тогда
НовыйОбъект.ИспользоватьРодителя(ОбъектИсточник.Родитель);
КонецЕсли;
// Установка владельца
Если ПустоеЗначение(НовыйВладелец) = 0 Тогда
НовыйОбъект.ИспользоватьВладельца(НовыйВладелец);
ИначеЕсли ПустоеЗначение(ОбъектИсточник.Владелец) = 0 Тогда
НовыйОбъект.ИспользоватьВладельца(ОбъектИсточник.Владелец);
КонецЕсли;
НовыйОбъект.Новый();
// Копирование всех реквизитов через метаданные
МетаОбъект = Метаданные.Справочник(ВидСпр);
Для и = 1 По МетаОбъект.Реквизит() Цикл
ИдРеквизита = МетаОбъект.Реквизит(и).Идентификатор;
НовыйОбъект.УстановитьАтрибут(ИдРеквизита, ОбъектИсточник.ПолучитьАтрибут(ИдРеквизита));
КонецЦикла;
НовыйОбъект.Наименование = ОбъектИсточник.Наименование;
// Если код числовой и автонумеруемый, лучше не копировать его, чтобы избежать дублей
// НовыйОбъект.Код = ОбъектИсточник.Код;
НовыйОбъект.Записать();
// Рекурсивный поиск и копирование всех подчиненных справочников
Для к = 1 По Метаданные.Справочник() Цикл
МетаПодч = Метаданные.Справочник(к);
Если МетаПодч.Владелец.Выбран() = 1 Тогда
Если МетаПодч.Владелец.Идентификатор = ВидСпр Тогда
// Нашли подчиненный справочник, копируем его элементы
СпрПодч = СоздатьОбъект("Справочник." + МетаПодч.Идентификатор);
СпрПодч.ИспользоватьВладельца(ОбъектИсточник.ТекущийЭлемент());
СпрПодч.ВыбратьЭлементы();
Пока СпрПодч.ПолучитьЭлемент() = 1 Цикл
глСкопироватьСправочник(СпрПодч.ТекущийЭлемент(), НовыйОбъект.ТекущийЭлемент());
КонецЦикла;
КонецЕсли;
КонецЕсли;
КонецЦикла;
Возврат НовыйОбъект.ТекущийЭлемент();
КонецФункции
Проанализируем критические моменты, которые часто упускают из виду при реализации подобного функционала:
УстановитьАтрибут копирует только значение на текущую дату. Если у справочника есть история цен или ставок, ее нужно копировать через объект Периодический. Выясним причину: данные истории хранятся в отдельной таблице, и для их переноса требуется выборка всех значений по датам и их последовательная запись для нового объекта.НачатьТранзакцию() перед началом процесса и ЗафиксироватьТранзакцию() после успешного завершения. Если на середине процесса произойдет сбой (например, отключится питание), база данных не останется в «полускопированном» состоянии.Новый() система 1С 7.7 автоматически генерирует следующий код. Если вы вручную копируете код (НовыйОбъект.Код = ОбъектИсточник.Код), будьте готовы к ошибкам уникальности, если в справочнике стоит соответствующий запрет.Разберем, куда лучше поместить вызов функции копирования. Наиболее удобным вариантом для пользователя будет размещение кнопки «Копировать с подчиненными» в форме списка справочника. В формуле кнопки достаточно прописать:
глСкопироватьСправочник(ТекущийЭлемент());
Предупреждение("Копирование успешно завершено!");
Таким образом, мы создали надежный механизм, который избавляет пользователей от рутины и гарантирует целостность данных при дублировании сложных объектов. Использование метаданных делает решение гибким: даже если вы добавите новый подчиненный справочник в конфигураторе, функция глСкопироватьСправочник подхватит его автоматически без правки кода.