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

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

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

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

Способ 1. Проверка уникальности по наименованию через метод НайтиПоНаименованию

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

Посмотрим на типичные ошибки начальной реализации:

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

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


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

КонецПроцедуры

Разберем ключевые моменты этого решения:

  1. Проверка загрузки: Условие Если ОбменДанными.Загрузка Тогда Возврат; КонецЕсли; гарантирует, что при синхронизации или выгрузке/загрузке через универсальные механизмы обмен не прервется критической ошибкой.
  2. Очистка пробелов: Функция СокрЛП убирает невидимые случайные пробелы по краям строки, предотвращая рассинхронизацию поиска.
  3. Второй параметр метода: Значение Истина включает точное совпадение строки.
  4. Сообщение пользователю: В транзакции записи на сервере нельзя использовать модальные окна (например, метод Предупреждение). Корректный подход — объект СообщениеПользователю, привязанный к конкретному реквизиту формы, и установка флага Отказ = Истина.

Способ 2. Контроль дублей по ИНН и КПП с помощью запроса

На практике наименования юридических лиц могут незначительно отличаться (например, «ООО Ромашка» и «Общество с ограниченной ответственностью "Ромашка"`»), поэтому наиболее надежным бизнес-критерием уникальности контрагента выступает его налоговый номер — ИНН (а для юридических лиц — связка ИНН + КПП).

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


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

КонецПроцедуры

Важные нюансы и рекомендации при промышленной разработке

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

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