Как избежать дублирования при копировании контрагентов из одного справочника в другой в 1С?

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

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

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

Анализ исходной проблемы и базового кода

Рассмотрим исходный алгоритм, предложенный автором темы:


Процедура ОсновныеДействияФормыЗаолнитьСпрКонтрагенты(Кнопка) 
    Запрос = Новый Запрос; 
    Запрос.Текст = 
        "ВЫБРАТЬ 
        |   Клиенты.Ссылка КАК Ссылка, 
        |   Клиенты.Наименование КАК Наименование 
        |ИЗ 
        |   Справочник.Клиенты КАК Клиенты"; 
        
    Выборка = Запрос.Выполнить.Выбрать(); 
    Пока Выборка.Следующий() Цикл 
        ОбКонтрагенты = Справочники.Контрагенты.СоздатьЭлемент(); 
        ЗаполнитьЗначенияСвойств(ОбКонтрагенты, Выборка); 
        ОбКонтрагенты.ТипКонтрагента = Перечисления.ТипКонтрагента.Клиент; 
        ОбКонтрагенты.Записать(); 
    КонецЦикла; 
КонецПроцедуры

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

Решение 1. Фильтрация на уровне запроса через ЛЕВОЕ СОЕДИНЕНИЕ (оптимально по скорости)

Вместо того чтобы выбирать всех клиентов, а затем проверять каждого из них в цикле на стороне сервера приложений, гораздо эффективнее отсечь уже перенесенных контрагентов на уровне СУБД с помощью SQL-запроса. Для этого мы свяжем две таблицы по какому-то ключевому полю (например, по коду или наименованию) с помощью левого соединения.

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


Процедура ЗаполнитьКонтрагентовБезДублей(Кнопка)
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |   Клиенты.Ссылка КАК Ссылка,
        |   Клиенты.Наименование КАК Наименование,
        |   Клиенты.Код КАК Код
        |ИЗ
        |   Справочник.Клиенты КАК Клиенты
        |   ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Contragents КАК Контрагенты
        |       ПО Клиенты.Код = Контрагенты.Код
        |ГДЕ
        |   Контрагенты.Ссылка ЕСТЬ NULL";
        
    Выборка = Запрос.Выполнить.Выбрать();
    
    Пока Выборка.Следующий() Цикл
        НовыйКонтрагент = Справочники.Контрагенты.СоздатьЭлемент();
        ЗаполнитьЗначенияСвойств(НовыйКонтрагент, Выборка);
        НовыйКонтрагент.ТипКонтрагента = Перечисления.ТипКонтрагента.Клиент;
        
        Попытка
            НовыйКонтрагент.Записать();
        Исключение
            ЗаписьЖурналаРегистрации("КопированиеДанных", УровеньЖурналаРегистрации.Ошибка, , , 
                "Не удалось записать контрагента: " + ОписаниеОшибки());
        КонецПопытки;
    КонецЦикла;
КонецПроцедуры

Как это работает: Мы используем конструкцию ЛЕВОЕ СОЕДИНЕНИЕ. Она сопоставляет каждую строку таблицы Клиенты со строкой из таблицы Контрагенты по равенству кодов. Если для какого-то клиента соответствие в таблице контрагентов не найдено, то поле Контрагенты.Ссылка примет значение NULL (пустое значение базы данных). С помощью условия ГДЕ Контрагенты.Ссылка ЕСТЬ NULL мы отфильтровываем выборку, оставляя только новые элементы. В цикл попадают исключительно те клиенты, которых гарантированно нет в приемнике.

Решение 2. Синхронизация по Уникальному Идентификатору (GUID)

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

Разберем реализацию этого механизма:


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

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

Решение 3. Использование независимого регистра сведений «Соответствие объектов»

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

Проанализируем программную реализацию этого подхода:


Процедура КопированиеЧерезРегистрСоответствий()
    Запрос = Новый Запрос;
    // Выбираем клиентов, которых нет в регистре соответствия объектов
    Запрос.Текст = 
        "ВЫБРАТЬ
        |   Клиенты.Ссылка КАК Ссылка,
        |   Клиенты.Наименование КАК Наименование
        |ИЗ
        |   Справочник.Клиенты КАК Клиенты
        |   ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СоответствиеОбъектов КАК Соответствие
        |       ПО Клиенты.Ссылка = Соответствие.ОбъектИсточник
        |ГДЕ
        |   Соответствие.ОбъектИсточник ЕСТЬ NULL";
        
    Выборка = Запрос.Выполнить.Выбрать();
    
    Пока Выборка.Следующий() Цикл
        // Создаем контрагента
        НовыйКонтрагент = Справочники.Contragents.СоздатьЭлемент();
        ЗаполнитьЗначенияСвойств(НовыйContragent, Выборка);
        НовыйКонтрагент.ТипКонтрагента = Перечисления.ТипКонтрагента.Клиент;
        
        НачатьТранзакцию();
        Попытка
            НовыйКонтрагент.Записать();
            
            // Фиксируем связь в регистре сведений
            МенеджерЗаписи = РегистрыСведений.СоответствиеОбъектов.СоздатьМенеджерЗаписи();
            МенеджерЗаписи.ОбъектИсточник = Выборка.Ссылка;
            МенеджерЗаписи.ОбъектПриемник = НовыйКонтрагент.Ссылка;
            МенеджерЗаписи.Записать(Истина);
            
            ЗафиксироватьТранзакцию();
        Исключение
            ОтменитьТранзакцию();
            ЗаписьЖурналаРегистрации("Синхронизация", УровеньЖурналаРегистрации.Предупреждение, , , 
                "Ошибка при записи данных: " + ОписаниеОшибки());
        КонецПопытки;
    КонецЦикла;
КонецПроцедуры

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

Решение 4. Сопоставление по бизнес-ключам (ИНН и КПП)

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

Рассмотрим, как реализовать сопоставление по ИНН:


Процедура КопированиеСПроверкойПоИНН()
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |   Клиенты.Ссылка КАК Ссылка,
        |   Клиенты.Наименование КАК Наименование,
        |   Клиенты.ИНН КАК ИНН
        |ИЗ
        |   Справочник.Клиенты КАК Клиенты";
        
    Выборка = Запрос.Выполнить.Выбрать();
    
    Пока Выборка.Следующий() Цикл
        // Проверяем заполненность ИНН у источника
        Если ПустаяСтрока(Выборка.ИНН) Тогда
            // Если ИНН пустой, сопоставить надежно не удастся, можно выдать предупреждение или пропустить
            ЗаписьЖурналаРегистрации("Синхронизация", УровеньЖурналаРегистрации.Предупреждение, , Выборка.Ссылка, 
                "У клиента '" + Выборка.Наименование + "' не заполнен ИНН!");
            Продолжить;
        КонецЕсли;
        
        // Проверяем существование контрагента с таким ИНН в базе данных
        СсылкаНаСуществующего = Справочники.Контрагенты.НайтиПоРеквизиту("ИНН", Выборка.ИНН);
        
        Если СсылкаНаСуществующего.Пустая() Тогда
            // Создаем нового, если не нашли
            НовыйКонтрагент = Справочники.Контрагенты.СоздатьЭлемент();
            ЗаполнитьЗначенияСвойств(НовыйКонтрагент, Выборка);
            НовыйКонтрагент.ТипКонтрагента = Перечисления.ТипКонтрагента.Клиент;
            НовыйКонтрагент.Записать();
        Иначе
            // Элемент найден. Обновим его наименование, если оно отличается
            ОбъектКонтрагент = СсылкаНаСуществующего.ПолучитьОбъект();
            Если ОбъектКонтрагент.Наименование <> Выборка.Наименование Тогда
                ОбъектКонтрагент.Наименование = Выборка.Наименование;
                ОбъектКонтрагент.Записать();
            КонецЕсли;
        КонецЕсли;
    КонецЦикла;
КонецПроцедуры

Решение 5. Самый простой вариант автора (реквизит «Скопировано») и его недостатки

В процессе обсуждения на форуме автор темы выбрал самое очевидное и простое для реализации решение: он добавил в справочник Клиенты реквизит типа Булево с именем Скопировано. При переносе элемента этот признак устанавливается в значение Истина, а в запросе на выгрузку ставится фильтр ГДЕ НЕ Клиенты.Скопировано.

Давайте посмотрим на этот код:


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

Почему этот подход имеет серьезные архитектурные минусы:

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

Резюме и рекомендации разработчику

При выборе оптимального алгоритма синхронизации руководствуемся следующими правилами:

← На главную