Почему не работает поиск договора по наименованию в 1С при наличии дубликатов и как правильно проверять категории объектов?

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

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

В чем суть проблемы: анализируем исходную ситуацию

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

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


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

Далее в процедуре печати логика обрабатывалась следующим образом:


ОбластьМакета = Макет.ПолучитьОбласть("Дополнение");
Если ПолучитьПредоплату(ПараметрыПечати.Договор) <> Неопределено Тогда
    ОбластьМакета.Параметры.СДаты = Формат(НачалоМесяца(ДобавитьМесяц(Объект.ДатаДокументов, 1)), "ДЛФ=D");
    ОбластьМакета.Параметры.ПоДату = Формат(КонецМесяца(ДобавитьМесяц(Объект.ДатаДокументов, 1)), "ДЛФ=D");
Иначе
    ОбластьМакета.Параметры.СДаты = Формат(НачалоМесяца(ДобавитьМесяц(Объект.ДатаДокументов, 0)), "ДЛФ=D");
    ОбластьМакета.Параметры.ПоДату = Формат(КонецМесяца(ДобавитьМесяц(Объект.ДатаДокументов, 0)), "ДЛФ=D");
КонецЕсли;

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

Выявление архитектурных и логических ошибок

Проанализируем детально, почему этот код работает нестабильно и нарушает стандарты разработки платформы 1С:Предприятие 8.

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

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

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

Когда у контрагента в базе присутствуют два договора «Основной»:

  1. Первый договор (без категории «Предоплата»).
  2. Второй договор (с установленной категорией «Предоплата»).

Метод НайтиПоНаименованию("Основной") возвращает ссылку на первый попавшийся договор (без категории). Запрос проверяет наличие записи в регистре сведений для этого договора, не находит её и возвращает Неопределено. Как только один из договоров переименовывается в «Основной1», его наименование становится уникальным, метод поиска находит именно его, и алгоритм срабатывает. Но полагаться на ручное переименование пользователями в реальной базе категорически нельзя.

Пошаговое исправление и оптимизация алгоритма

Для исправления ситуации нам необходимо полностью отказаться от строкового поиска и переписать логику проверки. Ссылка на объект (UUID) уникальна в пределах всей базы данных, и СУБД выполняет выборку по ней мгновенно.

Шаг 1. Исправление функции проверки категории

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


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

Шаг 2. Оптимизация вывода параметров в печатную форму

В первоначальном коде автора также содержалась неоптимальная конструкция ДобавитьМесяц(Объект.ДатаДокументов, 0). Прибавление нуля месяцев не несет никакой полезной нагрузки и лишь тратит процессорное время. Избавимся от этой конструкции.

Посмотрим на исправленный вариант вызова функции в модуле печати:


ОбластьМакета = Макет.ПолучитьОбласть("Дополнение");

// Используем нашу оптимизированную функцию
Если ЕстьПредоплатаПоДоговору(ПараметрыПечати.Договор) Тогда
    // Если предоплата есть, сдвигаем период на +1 месяц
    ДатаСмещения = ДобавитьМесяц(Объект.ДатаДокументов, 1);
    ОбластьМакета.Параметры.СДаты = Формат(НачалоМесяца(ДатаСмещения), "ДЛФ=D");
    ОбластьМакета.Параметры.ПоДату = Формат(КонецМесяца(ДатаСмещения), "ДЛФ=D");
Иначе
    // Если предоплаты нет, выводим текущий месяц документа
    ОбластьМакета.Параметры.СДаты = Формат(НачалоМесяца(Объект.ДатаДокументов), "ДЛФ=D");
    ОбластьМакета.Параметры.ПоДату = Formат(КонецМесяца(Объект.ДатаДокументов), "ДЛФ=D");
КонецЕсли;

Архитектурный контекст: категории в старых и новых конфигурациях

Рассматриваемый в теме регистр сведений КатегорииОбъектов — это классический механизм классификации из старых конфигураций 1С (таких как *Управление торговлей 10.3*, *Управление производственным предприятием 1.3*, *Комплексная автоматизация 1.1*).

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

Важное примечание для современных конфигураций: если вы работаете в современных конфигурациях на базе БСП (например, *ERP 2*, *Управление торговлей 11*, *Бухгалтерия предприятия 3.0*), механизм категорий полностью заменен подсистемой «Свойства». Для проверки дополнительных реквизитов и сведений в них не следует писать прямые запросы к регистрам. Вместо этого используются функции стандартных общих модулей, например:

УправлениеСвойствами.ЗначениеСвойства(Объект, Свойство)

Применение стандартных методов БСП гарантирует, что ваш код не сломается при очередном обновлении конфигурации платформы.

Основные правила работы со ссылками для предотвращения ошибок

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

← На главную