При разработке внешних обработок или отчетов в 1С часто возникает задача получить список номенклатуры, которая соответствует определенным условиям, например, по сроку годности, и при этом имеет нулевой или отрицательный остаток на складе. Нередко в таких ситуациях мы сталкиваемся с тем, что часть данных, а именно позиции без фактического остатка, но с важными для нас характеристиками (вроде срока годности), не попадает в итоговую выборку. Давайте разберем эту проблему на примере и найдем эффективное решение.
Представим, что мы создаем внешнюю обработку в конфигурации 1С:Управление торговлей (УТ Кз 2.2), цель которой — вывести номенклатуру, у которой срок годности находится в определенном диапазоне, а остаток на складе равен нулю или меньше. Мы используем запрос с ЛЕВОЕ СОЕДИНЕНИЕ и функцию ЕСТЬNULL для обработки возможных отсутствующих значений остатка.
Вот исходный запрос, который мы попытались использовать:
НачалоПериода = НачалоПериода;
КонецПериода = КонецПериода;
КоличествоОстаток = 0 ; // Это наш параметр для фильтрации
Запрос = Новый Запрос;
Запрос.Текст ="ВЫБРАТЬ
| СрокиГодностиНоменклатуры.Номенклатура,
| СрокиГодностиНоменклатуры.СрокГодности
|ПОМЕСТИТЬ ТоварыНаСкладах
|ИЗ
| РегистрСведений.СрокиГодностиНоменклатуры КАК СрокиГодностиНоменклатуры
|ГДЕ
| СрокиГодностиНоменклатуры.СрокГодности МЕЖДУ &НачалоПериода И &КонецПериода
|
|СГРУППИРОВАТЬ ПО
| СрокиГодностиНоменклатуры.Номенклатура,
| СрокиГодностиНоменклатуры.СрокГодности
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ТоварыНаСкладах.Номенклатура,
| МИНИМУМ(ТоварыНаСкладах.СрокГодности) КАК СрокГодности,
| ЕСТЬNULL(ТоварыНаСкладахОстатки.КоличествоОстаток, 0) КАК Остаток
|ИЗ
| ТоварыНаСкладах КАК ТоварыНаСкладах
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки КАК ТоварыНаСкладахОстатки
| ПО ТоварыНаСкладах.Номенклатура = ТоварыНаСкладахОстатки.Номенклатура
|ГДЕ
| ТоварыНаСкладахОстатки.КоличествоОстаток <= &КоличествоОстаток
|
|СГРУППИРОВАТЬ ПО
| ТоварыНаСкладах.Номенклатура,
| ЕСТЬNULL(ТоварыНаСкладахОстатки.КоличествоОстаток, 0)";
Запрос.УстановитьПараметр("КоличествоОстаток", КоличествоОстаток);
Запрос.УстановитьПараметр("КонецПериода", КонецПериода);
Запрос.УстановитьПараметр("НачалоПериода", НачалоПериода);
ТаблицаЗначений = Запрос.Выполнить().Выгрузить();
// ЭлементыФормы.ТаблицаЗначений.СоздатьКолонки();
В результате выполнения этого запроса мы обнаружили, что в итоговую таблицу значений попадает номенклатура, у которой остаток действительно меньше нуля, но позиции с нулевым остатком, а также те, для которых остатков нет вовсе (отсутствуют записи в регистре), не отображаются. Почему так происходит?
Давайте разберем логику нашего запроса шаг за шагом, чтобы выяснить причину такого поведения.
ТоварыНаСкладах:
Мы корректно отбираем номенклатуру и ее сроки годности из регистра сведений СрокиГодностиНоменклатуры, фильтруя их по заданному диапазону дат.
ВЫБРАТЬ
| СрокиГодностиНоменклатуры.Номенклатура,
| СрокиГодностиНоменклатуры.СрокГодности
|ПОМЕСТИТЬ ТоварыНаСкладах
|ИЗ
| РегистрСведений.СрокиГодностиНоменклатуры КАК СрокиГодностиНоменклатуры
|ГДЕ
| СрокиГодностиНоменклатуры.СрокГодности МЕЖДУ &НачалоПериода И &КонецПетода
На этом этапе все работает правильно, и временная таблица содержит ожидаемые данные о номенклатуре со сроками годности.
Мы используем ЛЕВОЕ СОЕДИНЕНИЕ нашей временной таблицы ТоварыНаСкладах с виртуальной таблицей остатков РегистрНакопления.ТоварыНаСкладах.Остатки. Этот тип соединения выбран верно, так как мы хотим получить *все* записи из левой таблицы (нашей ВТ `ТоварыНаСкладах`), даже если для некоторых из них нет соответствующих остатков в регистре.
Далее в списке ВЫБРАТЬ мы видим функцию ЕСТЬNULL(ТоварыНаСкладахОстатки.КоличествоОстаток, 0) КАК Остаток. Она предназначена для того, чтобы, если для какой-то номенклатуры нет записи в регистре остатков (и, следовательно, поле КоличествоОстаток будет содержать NULL), вместо NULL в колонку Остаток выводился 0. Это правильное решение для отображения данных.
Однако, главная проблема кроется в условии ГДЕ:
|ГДЕ
| ТоварыНаСкладахОстатки.КоличествоОстаток <= &КоличествоОстаток
Здесь мы напрямую сравниваем поле ТоварыНаСкладахОстатки.КоличествоОстаток с нашим параметром &КоличествоОстаток (который равен 0). Но что происходит, если для номенклатуры нет записей об остатках, и ТоварыНаСкладахОстатки.КоличествоОстаток содержит NULL?
Согласно правилам работы с NULL в базах данных (и в запросах 1С в частности), любое сравнение значения NULL с любым другим значением (включая 0, отрицательные числа или даже другой NULL) всегда приводит к логическому "ЛОЖЬ". Это означает, что условие NULL <= 0 будет расценено как ложное, и строки, где остаток равен NULL (то есть товара нет в регистре остатков), не попадут в итоговую выборку.
Таким образом, несмотря на то, что мы корректно обрабатываем NULL для отображения в выборке, мы неправильно фильтруем данные, исключая из рассмотрения номенклатуру без записей об остатках.
Давайте подробнее остановимся на понятии NULL в запросах 1С, поскольку его понимание критически важно для решения подобных задач.
NULL означает отсутствие данных, а не нулевое значение, пустую строку или значение "Неопределено". Он не равен ни числу, ни пробелу, ни пустой ссылке. NULL является типообразующим значением, то есть существует тип NULL и единственное значение этого типа.
NULL-значения чаще всего появляются в запросах при внешних соединениях (ЛЕВОЕ СОЕДИНЕНИЕ, ПРАВОЕ СОЕДИНЕНИЕ, ПОЛНОЕ СОЕДИНЕНИЕ), когда для записи из одной таблицы не находится соответствующая запись во второй таблице. Также они могут возникнуть при обращении к реквизитам элементов для группы и наоборот, или при обращении к реквизитам для "битой" ссылки.
Функция ЕСТЬNULL(<Проверяемое выражение>, <Выражение замены>) специально предназначена для замены значения NULL на другое значение. Она возвращает значение первого параметра, если оно не равно NULL, и значение второго параметра в противном случае. Это более предпочтительная и читаемая альтернатива конструкции ВЫБОР КОГДА ... ЕСТЬ 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С и предотвратит подобные ошибки.