Как исправить ошибку «Операции сравнения на больше-меньше допустимы только для значений совпадающих примитивных типов» в 1С?

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

В процессе разработки интеграций, выгрузок и HTTP-сервисов в среде «1С:Предприятие» программисты часто сталкиваются со следующей ошибкой выполнения кода:

«{HTTPСервис.HTTPService1.Модуль(276)}: Операции сравнения на больше-меньше допустимы только для значений совпадающих примитивных типов (Булево, Число, Строка, Дата)»

Чаще всего эта ошибка возникает при сравнении дат, когда одна из переменных неожиданно принимает значение Неопределено, NULL или пустую строку. Особенно остро эта проблема проявляется в устаревших конфигурациях, таких как Зарплата и Управление Персоналом КОРП 2.5, работающих в режиме совместимости с платформой «1С:Предприятие 8.2».

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

---

Выясняем истинную причину появления NULL в выборке

Посмотрим на строку кода, на которую указывает конфигуратор:

Если Выборка.ДатаПриемаНаРаботу > ДатаЗапроса Тогда

Переменная ДатаЗапроса в коде инициализируется как ТекущаяДата(), то есть гарантированно имеет тип Дата. Соответственно, проблема кроется в поле Выборка.ДатаПриемаНаРаботу. На первый взгляд кажется, что у какого-то сотрудника просто не заполнена дата приема. Однако в системе «1С:Предприятие» пустая дата в стандартном реквизите справочника имеет значение 01.01.0001 00:00:00 (тип Дата), а не NULL.

Проанализируем структуру источника данных. Справочник СотрудникиОрганизаций является иерархическим. Это означает, что он содержит как элементы (конкретных сотрудников), так и группы (папки для их логической группировки). При этом:

  1. Запрос автора выбирает данные из справочника без ограничения по иерархии.
  2. Для строк-групп (папок) кадровые реквизиты, включая ДатаПриемаНаРаботу, физически отсутствуют в базе данных.
  3. В результате выполнения запроса для всех групп в поле ДатаПриемаНаРаботу возвращается значение NULL.
  4. При обходе выборки в цикле для группы справочника срабатывает условие сравнения: NULL > ДатаЗапроса. Платформа не может сравнить специальный тип данных NULL с типом Дата, что и приводит к аварийному завершению работы HTTP-сервиса.

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

ГДЕ НЕ СотрудникиОрганизаций.ЭтоГруппа ---

Исправляем синтаксическую ошибку функции ЕСТЬNULL в запросе

В процессе обсуждения проблемы на форуме было предложено использовать конструкцию обработки нулевых значений в тексте запроса:

"| ЕСТЬNULL( СотрудникиОрганизаций.ДатаПриемаНаРаботу, ДАТАВРЕМЯ(1, 1, 1),"

Попытка применить этот совет привела к новой ошибке на этапе выполнения метода Запрос.Выполнить(). Разберем, почему это произошло:

  1. Синтаксическая ошибка в коде: В предложенном выражении после закрытия функции ДАТАВРЕМЯ была оставлена лишняя запятая, и при этом отсутствовала закрывающая скобка для самой функции ЕСТЬNULL.
  2. Неполный формат литерала даты: Конструктор ДАТАВРЕМЯ в запросах 1С требует обязательного указания всех шести параметров: ДАТАВРЕМЯ(Год, Месяц, День, Час, Минута, Секунда). Запись вида ДАТАВРЕМЯ(1, 1, 1) является некорректной и вызывает ошибку синтаксического анализатора запросов.

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


ЕСТЬNULL(СотрудникиОрганизаций.ДатаПриемаНаРаботу, &ПустаяДата) КАК ДатаПриемаНаРаботу

---

Скрытая «мина»: неленивые вычисления логических выражений в 1С

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


EndDate = ?(Выборка.ДатаУвольнения = Дата('00010101'), "", Выборка.ДатаУвольнения);
...
Если EndDate <> "" и EndDate < ДатаЗапроса Тогда

Здесь заложена критическая логическая ошибка, которая гарантированно сработает для любого работающего (не уволенного) сотрудника, у которого EndDate равен пустой строке "". Выясним причину этой уязвимости.

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

Даже если первая часть условия EndDate <> "" возвращает Ложь, система переходит к вычислению второй части выражения: EndDate < ДатаЗапроса. Происходит попытка сравнения пустой строки "" (тип Строка) с переменной ДатаЗапроса (тип Дата). Платформа снова выдаст ошибку несовпадающих примитивных типов.

Для устранения этой уязвимости мы должны разделить проверку на два независимых вложенных блока Если или воспользоваться функцией ЗначениеЗаполнено():


Если ЗначениеЗаполнено(EndDate) Тогда
    Если EndDate < ДатаЗапроса Тогда
        // Логика обработки уволенного сотрудника
    КонецЕсли;
КонецЕсли;

---

Оптимизация запроса и кода для HTTP-сервиса

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

Рассмотрим финальный вариант оптимизированного запроса и кода функции:


Функция GetEmployeeList(Запрос)

    // Инициализация результирующего массива
    ResultList = Новый Массив;
    
    // Получение параметров запроса
    ContractTypeLine = Запрос.ПараметрыЗапроса.Получить("contractType");
    ContractTypeLine = ?(ContractTypeLine = НеОпределено, "", ContractTypeLine);
    
    ParentOrgCodesLine = Запрос.ПараметрыЗапроса.Получить("parentOrgCode");
    ParentOrgCodesLine = ?(ParentOrgCodesLine = НеОпределено, "", ParentOrgCodesLine);
    
    PermittedOrgCodesArr = GetCutDepartmetnsWithChildView(ParentOrgCodesLine);
    
    // Создание и настройка запроса
    НовыйЗапрос = Новый Запрос;
    НовыйЗапрос.Текст = 
        "ВЫБРАТЬ
        |   ФИОФизЛицСрезПоследних.Фамилия КАК Фамилия,
        |   ФИОФизЛицСрезПоследних.Имя КАК Имя,
        |   ФИОФизЛицСрезПоследних.Отчество КАК Отчество,
        |   СписокРуководителей.Ссылка КАК Руководитель,
        |   СписокРуководителей.Ссылка.Код КАК ТабНомРуководителя,
        |   СотрудникиОрганизаций.Ссылка КАК Сотрудник,
        |   СотрудникиОрганизаций.Организация.Код КАК КодОрганизации,
        |   СотрудникиОрганизаций.ТекущееПодразделениеОрганизации.Код КАК КодПодразделения,
        |   СотрудникиОрганизаций.ТекущееПодразделениеОрганизации,
        |   СотрудникиОрганизаций.ТекущаяДолжностьОрганизации,
        |   СотрудникиОрганизаций.Код,
        |   СотрудникиОрганизаций.ДатаПриемаНаРаботу КАК ДатаПриемаНаРаботу,
        |   ЕСТЬNULL(СотрудникиОрганизаций.ДатаУвольнения, &ПустаяДата) КАК ДатаУвольнения,
        |   СостояниеРаботниковОрганизацийСрезПоследних.Период,
        |   СостояниеРаботниковОрганизацийСрезПоследних.Состояние,
        |   СостояниеРаботниковОрганизацийСрезПоследних.ПериодЗавершения
        |ИЗ
        |   Справочник.СотрудникиОрганизаций КАК СотрудникиОрганизаций
        |   ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ФИОФизЛиц.СрезПоследних(&ДатаЗапроса, ) КАК ФИОФизЛицСрезПоследних
        |       ПО СотрудникиОрганизаций.Физлицо = ФИОФизЛицСрезПоследних.ФизЛицо
        |   ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СостояниеРаботниковОрганизаций.СрезПоследних(&ДатаЗапроса, ) КАК СостояниеРаботниковОрганизацийСрезПоследних
        |       ПО СостояниеРаботниковОрганизацийСрезПоследних.Сотрудник = СотрудникиОрганизаций.Ссылка
        |   ЛЕВОЕ СОЕДИНЕНИЕ (
        |       ВЫБРАТЬ 
        |           СотрудникиОрганизаций.Ссылка,
        |           ОтветственныеЛицаОрганизацийСрезПоследних.СтруктурнаяЕдиница
        |       ИЗ 
        |           РегистрСведений.ОтветственныеЛицаОрганизаций.СрезПоследних(&ДатаЗапроса, ) КАК ОтветственныеЛицаОрганизацийСрезПоследних
        |           ЛЕВОЕ СОЕДИНЕНИЕ Справочник.СотрудникиОрганизаций КАК СотрудникиОрганизаций
        |               ПО ОтветственныеЛицаОрганизацийСрезПоследних.ФизическоеЛицо = СотрудникиОрганизаций.Физлицо
        |               И ОтветственныеЛицаОрганизацийСрезПоследних.СтруктурнаяЕдиница ССЫЛКА Справочник.ПодразделенияОрганизаций
        |   ) КАК СписокРуководителей
        |       ПО СотрудникиОрганизаций.ТекущееПодразделениеОрганизации = СписокРуководителей.СтруктурнаяЕдиница
        |ГДЕ
        |   НЕ СотрудникиОрганизаций.ЭтоГруппа
        |   И (СотрудникиОрганизаций.ДатаПриемаНаРаботу <= &ДатаЗапроса 
        |       ИЛИ СотрудникиОрганизаций.ДатаПриемаНаРаботу = &ПустаяДата)
        |УПОРЯДОЧИТЬ ПО
        |   Фамилия,
        |   Имя,
        |   Отчество";
        
    ДатаЗапроса = ТекущаяДата();
    ПустаяДата = Дата('00010101');
    
    НовыйЗапрос.УстановитьПараметр("ДатаЗапроса", ДатаЗапроса);
    НовыйЗапрос.УстановитьПараметр("ПустаяДата", ПустаяДата);
    
    Результат = НовыйЗапрос.Выполнить();
    Выборка = Результат.Выбрать();
    
    Пока Выборка.Следующий() Цикл
    
        // Проверка вида занятости
        ContractType = обИмяПеречисления(Выборка.Сотрудник.ВидЗанятости);
        Если ЗначениеЗаполнено(ContractTypeLine) Тогда
            ContractTypesArr = Split(ContractTypeLine, ",");
            флагНаВидЗанятости = 0;
            Для Каждого contractTypePermitted Из ContractTypesArr Цикл
                Если ContractType = contractTypePermitted Тогда
                    флагНаВидЗанятости = 1;
                    Прервать;
                КонецЕсли;
            КонецЦикла;
            Если флагНаВидЗанятости = 0 Тогда
                Продолжить;
            КонецЕсли;
        КонецЕсли;
        
        // Формирование структуры данных
        Guid = Строка(Выборка.Сотрудник.УникальныйИдентификатор);
        Lastname = Выборка.Фамилия;
        Firstname = Выборка.Имя;
        Middlename = Выборка.Отчество;
        Department = СокрЛП(Выборка.КодОрганизации) + ?(Выборка.КодПодразделения = Null, "", "~" + СокрЛП(Выборка.КодПодразделения));
        Position = Выборка.ТекущаяДолжностьОрганизации.Наименование;
        Tabnum = СокрЛП(Выборка.КодОрганизации) + "~" + СокрЛП(Выборка.Код);
        StartDate = Выборка.ДатаПриемаНаРаботу;
        
        // Корректная обработка даты увольнения
        EndDate = ?(Выборка.ДатаУвольнения = ПустаяДата, "", Выборка.ДатаУвольнения);
        
        StartLeaveDate = "";
        EndLeaveDate = "";
        ManagerGuid = ?(Выборка.Руководитель = Null, "", Строка(Выборка.Руководитель.УникальныйИдентификатор));
        ManagerTabNum = ?(Выборка.ТабНомРуководителя = Null, "", Выборка.КодОрганизации + "~" + Выборка.ТабНомРуководителя);
        
        // Установка состояний
        Состояние = "Работает";
        Период = Выборка.Период;
        ПериодЗавершения = Выборка.ПериодЗавершения;
        
        Если Выборка.Состояние <> Null Тогда
            Состояние = Строка(Выборка.Состояние);
        КонецЕсли;
        
        // Избегаем сравнения разных типов через вложенные условия
        Если ЗначениеЗаполнено(EndDate) Тогда
            Если EndDate < ДатаЗапроса Тогда
                Состояние = "Уволен";
            КонецЕсли;
        КонецЕсли;
        
        DisableReason = "Работает";
        Если Состояние <> "Работает" Тогда
            Если ПериодЗавершения = Null Тогда
                StartLeaveDate = Период;
            Иначе
                StartLeaveDate = "";
            КонецЕсли;
            
            Если ПериодЗавершения <> Null Тогда
                EndLeaveDate = ПериодЗавершения;
            Иначе
                EndLeaveDate = "";
            КонецЕсли;
            DisableReason = "Работает";
        ИначеЕсли Состояние <> "Работает" Тогда
            // Проверка на корректность типов перед сравнением
            Если ТипЗнч(ПериодЗавершения) = Тип("Дата") И ПериодЗавершения >= ДатаЗапроса Тогда
                StartLeaveDate = Период;
                EndLeaveDate = ПериодЗавершения;
                DisableReason = Состояние;
            КонецЕсли;
        КонецЕсли;
        
        // Заполнение результирующей структуры
        Структура = Новый Структура;
        Структура.Вставить("Guid", Guid);
        Структура.Вставить("Lastname", Lastname);
        Структура.Вставить("Firstname", Firstname);
        Структура.Вставить("Middlename", Middlename);
        Структура.Вставить("UserOrg", Department);
        Структура.Вставить("Position", Position);
        Структура.Вставить("Tabnum", Tabnum);
        Структура.Вставить("StartDate", StartDate);
        Структура.Вставить("EndDate", EndDate);
        Структура.Вставить("StartLeaveDate", StartLeaveDate);
        Структура.Вставить("EndLeaveDate", EndLeaveDate);
        Структура.Вставить("ManagerGuid", ManagerGuid);
        Структура.Вставить("ManagerTabNum", ManagerTabNum);
        Структура.Вставить("DisableReason", DisableReason);
        Структура.Вставить("СontractType", ContractType);
        
        ResultList.Добавить(Структура);
        
    КонецЦикла;
    
    // Формирование JSON-ответа
    СтрокаДляОтвета = СформироватьJSON(ResultList);
    Ответ = Новый HTTPСервисОтвет(200);
    Ответ.Заголовки.Вставить("Content-type", "application/json; charset=utf-8");
    Ответ.УстановитьТелоИзСтроки(СтрокаДляОтвета, КодировкаТекста.UTF8, ИспользованиеByteOrderMark.НеИспользовать);
    
    Возврат Ответ;

КонецФункции

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

← На главную