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