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