Почему таблица значений не заполняется номенклатурой с нулевым остатком при использовании запросов в 1С?

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

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

Постановка проблемы: почему номенклатура с нулевым остатком не отображается?

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

Вот исходный запрос, который мы попытались использовать:


НачалоПериода = НачалоПериода;
КонецПериода = КонецПериода;
КоличествоОстаток = 0 ; // Это наш параметр для фильтрации

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

ТаблицаЗначений = Запрос.Выполнить().Выгрузить();
// ЭлементыФормы.ТаблицаЗначений.СоздатьКолонки();

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

Анализируем исходный запрос и особенности работы с NULL

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

  1. Первая временная таблица (ВТ) ТоварыНаСкладах:

    Мы корректно отбираем номенклатуру и ее сроки годности из регистра сведений СрокиГодностиНоменклатуры, фильтруя их по заданному диапазону дат.

    
    ВЫБРАТЬ
        |    СрокиГодностиНоменклатуры.Номенклатура,
        |    СрокиГодностиНоменклатуры.СрокГодности
        |ПОМЕСТИТЬ ТоварыНаСкладах
        |ИЗ
        |    РегистрСведений.СрокиГодностиНоменклатуры КАК СрокиГодностиНоменклатуры
        |ГДЕ
        |    СрокиГодностиНоменклатуры.СрокГодности МЕЖДУ &НачалоПериода И &КонецПетода
    

    На этом этапе все работает правильно, и временная таблица содержит ожидаемые данные о номенклатуре со сроками годности.

  2. Основной запрос и проблема с остатками:

    Мы используем ЛЕВОЕ СОЕДИНЕНИЕ нашей временной таблицы ТоварыНаСкладах с виртуальной таблицей остатков РегистрНакопления.ТоварыНаСкладах.Остатки. Этот тип соединения выбран верно, так как мы хотим получить *все* записи из левой таблицы (нашей ВТ `ТоварыНаСкладах`), даже если для некоторых из них нет соответствующих остатков в регистре.

    Далее в списке ВЫБРАТЬ мы видим функцию ЕСТЬNULL(ТоварыНаСкладахОстатки.КоличествоОстаток, 0) КАК Остаток. Она предназначена для того, чтобы, если для какой-то номенклатуры нет записи в регистре остатков (и, следовательно, поле КоличествоОстаток будет содержать NULL), вместо NULL в колонку Остаток выводился 0. Это правильное решение для отображения данных.

    Однако, главная проблема кроется в условии ГДЕ:

    
    |ГДЕ
    |    ТоварыНаСкладахОстатки.КоличествоОстаток <= &КоличествоОстаток
    

    Здесь мы напрямую сравниваем поле ТоварыНаСкладахОстатки.КоличествоОстаток с нашим параметром &КоличествоОстаток (который равен 0). Но что происходит, если для номенклатуры нет записей об остатках, и ТоварыНаСкладахОстатки.КоличествоОстаток содержит NULL?

    Согласно правилам работы с NULL в базах данных (и в запросах 1С в частности), любое сравнение значения NULL с любым другим значением (включая 0, отрицательные числа или даже другой NULL) всегда приводит к логическому "ЛОЖЬ". Это означает, что условие NULL <= 0 будет расценено как ложное, и строки, где остаток равен NULL (то есть товара нет в регистре остатков), не попадут в итоговую выборку.

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

Причина возникновения NULL-значений и их обработка

Давайте подробнее остановимся на понятии NULL в запросах 1С, поскольку его понимание критически важно для решения подобных задач.

  1. Что такое NULL?

    NULL означает отсутствие данных, а не нулевое значение, пустую строку или значение "Неопределено". Он не равен ни числу, ни пробелу, ни пустой ссылке. NULL является типообразующим значением, то есть существует тип NULL и единственное значение этого типа.

  2. Когда возникают NULL-значения?

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

  3. Функция ЕСТЬNULL() для обработки NULL:

    Функция ЕСТЬNULL(<Проверяемое выражение>, <Выражение замены>) специально предназначена для замены значения NULL на другое значение. Она возвращает значение первого параметра, если оно не равно NULL, и значение второго параметра в противном случае. Это более предпочтительная и читаемая альтернатива конструкции ВЫБОР КОГДА ... ЕСТЬ NULL ТОГДА ... ИНАЧЕ ... КОНЕЦ.

    При использовании ЕСТЬNULL() выражение замены приводится к типу проверяемого выражения, если оно является строкой или числом.

  4. Критическое правило для фильтрации с NULL:

    Критически важно использовать ЕСТЬNULL() или явную проверку ЕСТЬ NULL при работе с полями из присоединяемых таблиц в условиях ГДЕ или ИМЕЮЩИЕ. Как мы уже отметили, любое сравнение значения NULL с любым другим выражением (даже NULL с NULL) всегда ложно. Это может привести к некорректным данным или ошибкам, если мы не учтем эту особенность.

Корректируем запрос: правильное применение ЕСТЬNULL в условии ГДЕ

Чтобы корректно учесть номенклатуру как с фактическим нулевым остатком, так и с полным отсутствием записей об остатках (которые мы интерпретируем как 0), необходимо применить функцию ЕСТЬNULL() непосредственно в условии фильтрации ГДЕ. Это гарантирует, что NULL-значения будут преобразованы в 0 *до* выполнения сравнения.

Давайте рассмотрим скорректированный запрос:


НачалоПериода = НачалоПериода;
КонецПериода = КонецПериода;
КоличествоОстаток = 0 ; // Параметр для фильтрации

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

ТаблицаЗначений = Запрос.Выполнить().Выгрузить();
// ЭлементыФормы.ТаблицаЗначений.СоздатьКолонки();

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


|ГДЕ
|    ЕСТЬNULL(ТоварыНаСкладахОстатки.КоличествоОстаток, 0) <= &КоличествоОстаток

Теперь, если для номенклатуры в регистре остатков нет записи (и ТоварыНаСкладахОстатки.КоличествоОстаток будет NULL), функция ЕСТЬNULL сначала преобразует этот NULL в 0. После этого условие 0 <= 0 будет истинным, и такая номенклатура будет включена в выборку. Таким образом, мы получаем полный список номенклатуры, соответствующей нашим критериям, включая ту, по которой отсутствуют записи об остатках (что мы интерпретируем как нулевой остаток).

Итоги и рекомендации

Таким образом, мы выяснили, что основная причина некорректного заполнения таблицы значений заключалась в неверном использовании условия фильтрации ГДЕ при работе с NULL-значениями из присоединенных таблиц. Функция ЕСТЬNULL(), хотя и применялась для вывода данных, не влияла на логику фильтрации, поскольку сравнение с NULL всегда давало ложный результат.

Всегда помните о поведении NULL-значений в запросах 1С, особенно при использовании внешних соединений. Применяйте функцию ЕСТЬNULL() или явную проверку IS NULL не только для вывода данных в списке ВЫБРАТЬ, но и для корректной работы условий фильтрации в ГДЕ или ИМЕЮЩИЕ, чтобы избежать непредвиденных результатов и потери важных данных.

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

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

Надеемся, что этот детальный разбор поможет вам в дальнейшей работе с запросами в 1С и предотвратит подобные ошибки.

← На главную