Как правильно обработать услуги и избежать ошибок с NULL при контроле остатков в запросах 1С?

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

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

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

Анализ проблемы: почему в выборке по группировкам возникает NULL?

Рассмотрим типичную ситуацию. Разработчик строит запрос по временной таблице документа, соединяя ее с виртуальной таблицей остатков регистра партий. Для обхода результатов используется конструкция ИТОГИ... ПО Номенклатура. При попытке проверить вид номенклатуры на уровне группировочной выборки код выдает ошибку или ведет себя непредсказуемо.

Давайте посмотрим на исходный (проблемный) фрагмент кода:


ВыборкаНоменклатура = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаНоменклатура.Следующий() Цикл
    // Попытка проверить вид номенклатуры на уровне группировки
    Если ВыборкаНоменклатура.ВидНоменклатуры <> Перечисления.ВидыНоменклатуры.Услуга 
        И ВыборкаНоменклатура.Количество > ВыборкаНоменклатура.КоличествоОстаток Тогда
        
        Сообщить("Не хватает товара...");
    КонецЕсли;
КонецЦикла;

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

  1. Являются измерениями группировки (в нашем случае поле Номенклатура).
  2. Являются результатами агрегатных функций, описанных в секции ИТОГИ (например, СУММА(КоличествоОстаток) или МАКСИМУМ(Количество)).

Любые другие поля (такие как ВТ.ВидНоменклатуры или текстовое представление номенклатуры), не включенные в агрегаты и не являющиеся полями группировки, на уровне группировочной записи будут содержать значение NULL. Попытка выполнить сравнение ВыборкаНоменклатура.ВидНоменклатуры <> Перечисления.ВидыНоменклатуры.Услуга приводит к ошибке, так как мы пытаемся сравнить значение NULL со значением перечисления.

Решение проблемы на уровне итогов запроса

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

Вариант 1. Добавление поля в список группировок итогов

Поскольку у одной номенклатурной позиции может быть только один вид номенклатуры, мы можем безболезненно добавить это поле в разрез итогов. Это не приведет к дублированию строк, но сделает поле доступным для анализа в выборке по группировкам:


ИТОГИ
    МАКСИМУМ(Количество),
    СУММА(КоличествоОстаток)
ПО
    Номенклатура,
    ВидНоменклатуры

Вариант 2. Применение агрегатной функции в секции ИТОГИ

Если мы не хотим добавлять поле в разрез группировки, мы можем вычислить его значение с помощью агрегатной функции МАКСИМУМ в секции итогов. Для уникальных реквизитов номенклатуры функция МАКСИМУМ вернет их единственное реальное значение:


ИТОГИ
    МАКСИМУМ(Количество),
    СУММА(КоличествоОстаток),
    МАКСИМУМ(ВидНоменклатуры) КАК ВидНоменклатуры
ПО
    Номенклатура

Борьба с NULL во встроенном языке и функция ЕСТЬNULL

Другая важная проблема заключается в том, что при левом соединении с таблицей остатков для услуг (или для товаров, которых вообще нет на складе) поле КоличествоОстаток вернет значение NULL. Если во встроенном языке попытаться выполнить математическое сравнение или вычитание с участием NULL (например, ВыборкаНоменклатура.Количество > ВыборкаНоменклатура.КоличествоОстаток), система вызовет ошибку несовместимости типов.

Некоторые разработчики пытаются делать проверку типов на стороне встроенного языка:


Если ТипЗнч(ВыборкаНоменклатура.ВидНоменклатуры) <> Тип("Null") Тогда
    // Костыль для обхода ошибки
КонецЕсли;

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

Отредактируем выбор полей в запросе:


ВЫБРАТЬ
    ВТ.Номенклатура КАК Номенклатура,
    ЕСТЬNULL(ОстаткиПоПартиямОстатки.КоличествоОстаток, 0) КАК КоличествоОстаток,
    ЕСТЬNULL(ОстаткиПоПартиямОстатки.СебестоимостьОстаток, 0) КАК СебестоимостьОстаток,
    ЕСТЬNULL(ВТ.ВидНоменклатуры, ЗНАЧЕНИЕ(Перечисление.ВидыНоменклатуры.ПустаяСсылка)) КАК ВидНоменклатуры

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

Архитектурная оптимизация: исключение услуг из соединения

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

Рассмотрим, как правильно организовать работу с услугами:

  1. При формировании временной таблицы (ВТ) данных документа необходимо сразу разделять товары и услуги. Во временную таблицу для анализа остатков должны попадать только те строки, у которых Номенклатура.ВидНоменклатуры = Значение(Перечисление.ВидыНоменклатуры.Товар).
  2. Все запросы по остаткам и списанию партий выполняются исключительно для этой "очищенной" таблицы товаров.
  3. Запись движений по услугам в сопутствующие регистры (например, "Продажи") производится напрямую из данных документа (или из отдельной ВТ услуг), минуя тяжелые запросы по остаткам.

Исправление скрытой ошибки динамической смены ЛИФО / ФИФО

В исходном коде автора темы присутствует еще одна критическая ошибка, связанная со сменой метода списания партий (LIFO/FIFO) с помощью автозамены текста запроса:


Если УчётнаяПолитика = Перечисления.УчетнаяПолитика.ЛИФО Тогда 
    Запрос.Текст = СтрЗаменить(Запрос.Текст, "ОстаткиПоПартиямОстатки.Партия.МоментВремени ВОЗР", "ОстаткиПоПартиямОстатки.Партия.МоментВремени УБЫВ"); 
КонецЕсли;

Однако в самом запросе автора направление сортировки по умолчанию не указано:

УПОРЯДОЧИТЬ ПО ОстаткиПоПартиямОстатки.Партия.МоментВремени

Поскольку ключевое слово ВОЗР отсутствует в исходном тексте запроса, функция СтрЗаменить просто не найдет подстроку для замены. Запрос всегда будет выполняться по методу FIFO (сортировка по умолчанию идет по возрастанию). Чтобы исправить это, в исходном тексте запроса нужно обязательно явно прописать ключевое слово ВОЗР.

Итоговый правильный шаблон решения

Объединим все исправления и рекомендации в единый оптимальный шаблон кода. Мы добавим ЕСТЬNULL в поля остатков, включим вид номенклатуры в агрегаты итогов, исправим сортировку для динамической замены и добавим правильный обход результата.


Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;

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

// Реализуем динамическую смену учетной политики (ФИФО/ЛИФО)
Если УчетнаяПолитика = Перечисления.УчетнаяПолитика.ЛИФО Тогда
    Запрос.Текст = СтрЗаменить(Запрос.Текст, "Партия.МоментВремени ВОЗР", "Партия.МоментВремени УБЫВ");
КонецЕсли;

Запрос.УстановитьПараметр("МоментВремени", МоментВремени);
РезультатЗапроса = Запрос.Выполнить();

// Выборка по группировкам (Номенклатура)
ВыборкаНоменклатура = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);

Пока ВыборкаНоменклатура.Следующий() Цикл
    
    // Теперь это условие отработает корректно и без ошибок на NULL
    Если ВыборкаНоменклатура.ВидНоменклатуры = Перечисления.ВидыНоменклатуры.Услуга Тогда
        // Услуги просто пропускаем при контроле остатков партий
        Продолжить;
    КонецЕсли;
    
    // Контроль остатка товара
    Если ВыборкаНоменклатура.Количество > ВыборкаНоменклатура.КоличествоОстаток Тогда
        Нехватка = ВыборкаНоменклатура.Количество - ВыборкаНоменклатура.КоличествоОстаток;
        Сообщить("Не хватает товара " + ВыборкаНоменклатура.НоменклатураПредставление + " в количестве " + Нехватка);
        Отказ = Истина;
    КонецЕсли;
    
    Если Отказ Тогда
        Продолжить;
    КонецЕсли;
    
    // Вложенный обход детальных записей для списания партий
    ВыборкаДетали = ВыборкаНоменклатура.Выбрать();
    ОсталосьСписать = ВыборкаНоменклатура.Количество;
    
    Пока ВыборкаДетали.Следующий() И ОсталосьСписать > 0 Цикл
        // Алгоритм списания партий...
    КонецЦикла;
    
КонецЦикла;

Разбор этого примера показывает, что совмещение грамотного проектирования запросов (использование ЕСТЬNULL, фильтрация услуг при соединении таблиц) и понимание принципов работы итогов в 1С позволяют создавать быстрый, отказоустойчивый код проведения документов.

← На главную