При разработке отчетов, обработок заполнения или механизмов подбора в платформе 1С:Предприятие разработчики часто сталкиваются с необходимостью получения остатков товаров на складах. В процессе написания запросов к виртуальным таблицам регистров накопления легко совершить типичные ошибки, которые приводят либо к аварийному завершению работы системы с ошибкой несоответствия типов, либо к получению пустого результата запроса.
В данной статье мы подробно разберем, почему возникает ошибка несовместимости типов при работе со справочниками в запросах, выясним разницу между менеджером справочника и ссылкой, научимся правильно отключать фильтры для получения полных остатков, а также рассмотрим лучшие практики оптимизации таких запросов.
Проанализируем исходную ошибку, с которой столкнулся автор темы. В своем коде он пытается выполнить следующее присвоение и передать его в запрос:
Номенклатура = Справочники.Номенклатура;
Запрос.УстановитьПараметр("Номенклатура", Номенклатура);
При попытке выполнить запрос, содержащий условие Номенклатура = &Номенклатура, система выдает ошибку совместимости типов данных. Выясним, почему это происходит.
Объект Справочники.Номенклатура возвращает тип данных СправочникМенеджер.Номенклатура. Менеджер справочника — это специальный программный класс на платформе 1С, предназначенный для управления справочником в целом (например, создания новых элементов, поиска по коду или наименованию, получения макетов). Менеджер существует только в контексте выполнения кода 1С и не имеет представления в таблицах базы данных SQL.
В то же время, поля регистров (такие как измерение Номенклатура в регистре накопления ТоварыНаСкладах) хранят тип данных СправочникСсылка.Номенклатура. Это уникальный идентификатор (GUID) конкретной записи в таблице базы данных.
Когда СУБД (например, MS SQL Server или PostgreSQL) пытается сравнить поле ссылки (GUID) с переданным в параметр менеджером справочника, это вызывает конфликт типов данных, так как платформа не может преобразовать программный менеджер класса в конкретный GUID записи.
Правило: В параметры запроса всегда необходимо передавать конкретную ссылку на элемент справочника (СправочникСсылка), а не менеджер справочника (СправочникМенеджер).
Для передачи конкретного элемента мы можем использовать поиск по наименованию (хотя в реальном коде этого стоит избегать в пользу поиска по коду или реквизитам):
// Возвращает тип "СправочникСсылка.Номенклатура"
СсылкаНаТовар = Справочники.Номенклатура.НайтиПоНаименованию("Мит 160 литр бойлер");
Запрос.УстановитьПараметр("Продукция", СсылкаНаТовар);
Если мы хотим получить пустую ссылку данного справочника (например, для проверки на незаполненные значения):
ПустаяСсылкаНоменклатуры = Справочники.Номенклатура.ПустаяСсылка();
Запрос.УстановитьПараметр("Номенклатура", ПустаяСсылкаНоменклатуры);
---
Разберем ситуацию, когда нам необходимо заполнить табличную часть документа всеми остатками товаров на складе без отбора по конкретной позиции. Начинающие разработчики часто пытаются решить эту задачу "в лоб", передавая в параметр пустую ссылку:
Номенклатура = Справочники.Номенклатура.ПустаяСсылка();
Запрос.УстановитьПараметр("Номенклатура", Номенклатура);
При этом текст запроса выглядит следующим образом:
ВЫБРАТЬ
ТоварыНаСкладахОстатки.Номенклатура КАК Номенклатура,
ТоварыНаСкладахОстатки.КоличествоОстаток КАК Количество
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(
&Период,
Номенклатура = &Номенклатура И Склад = &Склад
) КАК ТоварыНаСкладахОстатки
В результате выполнения такого кода табличная часть останется пустой (или вернет некорректные данные). Разберем причину этого поведения.
Условие Номенклатура = &Номенклатура в параметрах виртуальной таблицы жестко ограничивает выборку. Передавая туда пустую ссылку, мы принудительно заставляем СУБД искать остатки по "пустому" товару. Разумеется, остатков по пустой ссылке на складе нет, и запрос возвращает пустой результат.
Для того чтобы получить остатки по всей номенклатуре, необходимо полностью исключить условие фильтрации из параметров виртуальной таблицы остатков и секции ГДЕ:
ВЫБРАТЬ
ТоварыНаСкладахОстатки.Номенклатура КАК Номенклатура,
ТоварыНаСкладахОстатки.КоличествоОстаток КАК ОстатокНаСкладе
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(&Период, Склад = &Склад) КАК ТоварыНаСкладахОстатки
В этом случае платформа наложит фильтр только по складу, а по измерению номенклатуры вернет абсолютно все имеющиеся на остатках позиции.
---
На практике часто требуется создать универсальный алгоритм заполнения. Например, если пользователь выбрал конкретный товар на форме — заполнить остатки только по нему, а если поле выбора осталось пустым — получить остатки по всем товарам склада. Писать для этого два разных жестких запроса неэффективно.
Рассмотрим два профессиональных подхода к реализации этой задачи.
Этот способ является наиболее производительным с точки зрения СУБД. Мы анализируем, заполнен ли фильтр на форме, и динамически добавляем условие в текст запроса:
ТекстОтбораНоменклатуры = "";
// Проверяем, заполнено ли значение отбора на форме/в реквизитах
Если ЗначениеЗаполнено(ВыбраннаяНоменклатура) Тогда
ТекстОтбораНоменклатуры = " И Номенклатура = &Номенклатура";
КонецЕсли;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТоварыНаСкладахОстатки.Номенклатура КАК Номенклатура,
| ТоварыНаСкладахОстатки.КоличествоОстаток КАК ОстатокНаСкладе
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(&Период, Склад = &Склад " + ТекстОтбораНоменклатуры + ") КАК ТоварыНаСкладахОстатки";
Запрос.УстановитьПараметр("Период", Новый Граница(ДатаОкончания, ВидГраницы.Включая));
Запрос.УстановитьПараметр("Склад", ВыбранныйСклад);
Если ЗначениеЗаполнено(ВыбраннаяНоменклатура) Тогда
Запрос.УстановитьПараметр("Номенклатура", ВыбраннаяНоменклатура);
КонецЕсли;
Результат = Запрос.Выполнить().Выгрузить();
Преимущество этого метода в том, что в базу данных уходит максимально простой и оптимизированный SQL-запрос. СУБД строит оптимальный план выполнения (Query Plan), используя индексы по измерению Номенклатура.
Если мы не хотим собирать текст запроса динамически из кусочков строк, можно применить универсальное логическое выражение непосредственно в параметрах виртуальной таблицы:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТоварыНаСкладахОстатки.Номенклатура КАК Номенклатура,
| ТоварыНаСкладахОстатки.КоличествоОстаток КАК ОстатокНаСкладе
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(
| &Период,
| Склад = &Склад
| И (Номенклатура = &Номенклатура ИЛИ &Номенклатура = ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка))
| ) КАК ТоварыНаСкладахОстатки";
Запрос.УстановитьПараметр("Период", Новый Граница(ДатаОкончания, ВидГраницы.Включая));
Запрос.УстановитьПараметр("Склад", ВыбранныйСклад);
Запрос.УстановитьПараметр("Номенклатура", ВыбраннаяНоменклатура); // Может быть пустой ссылкой
Как это работает: Если параметр &Номенклатура содержит конкретную ссылку, условие отрабатывает как обычный фильтр. Если же мы передаем туда пустую ссылку, то правая часть выражения &Номенклатура = ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка) возвращает ИСТИНА, тем самым отключая фильтрацию по номенклатуре для всех записей.
Внимание! Использование конструкции ИЛИ в условиях запроса на больших объемах данных (сотни тысяч или миллионы строк в регистре) может привести к ухудшению производительности СУБД (операция Index Scan вместо Index Seek). Для высоконагруженных систем всегда используйте Способ А.
---
Обратим внимание на строку из сообщения автора: Склад = Справочники.Склады.Склад;. Если склад является предопределенным элементом конфигурации, к нему можно обратиться напрямую из кода. Однако, если данный склад нужен исключительно для фильтрации внутри запроса, хорошим тоном разработки считается обращение к нему через специальную функцию запроса ПредопределенноеЗначение. Это позволяет разгрузить код от лишней установки параметров:
ВЫБРАТЬ
ТоварыНаСкладахОстатки.Номенклатура КАК Номенклатура,
ТоварыНаСкладахОстатки.КоличествоОстаток КАК ОстатокНаСкладе
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(
&Период,
Склад = ПредопределенноеЗначение("Справочник.Склады.Склад")
) КАК ТоварыНаСкладахОстатки
Использование функции ПредопределенноеЗначение делает структуру запроса более прозрачной и исключает необходимость вызова метода Запрос.УстановитьПараметр() для предопределенных элементов метаданных.
---
Для корректного расчета остатков критически важно правильно указывать параметр &Период. В сообщениях темы автор использует объект Граница:
Запрос.УстановитьПараметр("Период", Новый Граница(ДатаОкончания, ВидГраницы.Включая));
Разберем три основных сценария работы со временем при получении остатков в 1С:
ТекущаяДата()):
Если передать в параметры виртуальной таблицы простую дату, расчет остатков будет произведен на начало указанной секунды. Любые документы, проведенные ровно в эту же секунду, не будут учтены при расчете остатков. Это частая причина ошибок, когда только что проведенный документ "не видит" остатки.
Граница с видом Включая:
Позволяет получить остатки на указанную дату/время включительно (с учетом всех движений документов, зарегистрированных на эту секунду). Именно этот способ продемонстрирован в решении автора.
МоментВремени:
Используется при проведении документов в модуле объекта. Он уникально идентифицирует положение документа на временной шкале базы данных (сочетание даты, времени и ссылки на документ).
Новый Граница(ЭтотОбъект.МоментВремени(), ВидГраницы.Исключая) — остатки товаров непосредственно до проведения текущего документа. Используется для контроля остатков перед списанием.Новый Граница(ЭтотОбъект.МоментВремени(), ВидГраницы.Включая) — остатки товаров после проведения текущего документа.---
Для отображения полученных данных в табличной части формы автор использует метод Загрузить():
Объект.ПрайсЛист.Загрузить(РезультатЗапроса);
Рассмотрим правила успешного применения этого метода, чтобы избежать ситуации, когда "ошибки нет, но данные не отображаются":
Загрузить() автоматически сопоставляет имена колонок в таблице значений (результате запроса) с именами реквизитов табличной части объекта. Регистр букв при этом не имеет значения. Если в запросе колонка называется КоличествоОстаток, а в табличной части реквизит называется Количество, то колонка заполнена не будет. Нам обязательно нужно использовать псевдонимы (конструкцию КАК) в тексте запроса:
| ТоварыНаСкладахОстатки.КоличествоОстаток КАК Количество
Номенклатура в запросе должен строго соответствовать типу реквизита табличной части СправочникСсылка.Номенклатура.
Загрузить() полностью перезаписывает табличную часть. Все ранее добавленные пользователем строки будут удалены. Если требуется добавить строки к уже существующим, следует использовать метод ЗаполнитьЗначенияСвойств() в цикле обхода выборки запроса.
---
Подведем итог и соберем идеальную серверную процедуру для заполнения табличной части остатками всей номенклатуры по складу без "мусорного" неиспользуемого кода (о котором справедливо предупреждали участники форума в сообщениях 17 и 21):
&НаСервере
Процедура ЗаполнитьОстаткиНаСервере()
// Получаем склад из реквизитов документа на форме
ВыбранныйСклад = Объект.Склад;
Если Не ЗначениеЗаполнено(ВыбранныйСклад) Тогда
ВызватьИсключение "Не выбран склад для заполнения остатков!";
КонецЕсли;
ДатаОкончания = ТекущаяДата();
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТоварыНаСкладахОстатки.Номенклатура КАК Номенклатура,
| ТоварыНаСкладахОстатки.КоличествоОстаток КАК Количество
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(
| &Период,
| Склад = &Склад
| ) КАК ТоварыНаСкладахОстатки";
// Накладываем фильтр по складу и дате (включая текущую секунду)
Запрос.УстановитьПараметр("Период", Новый Граница(ДатаОкончания, ВидГраницы.Включая));
Запрос.УстановитьПараметр("Склад", ВыбранныйСклад);
РезультатЗапроса = Запрос.Выполнить().Выгрузить();
// Загружаем данные в табличную часть ПрайсЛист
Объект.ПрайсЛист.Загрузить(РезультатЗапроса);
КонецПроцедуры
Изучив эти правила и механизмы, вы сможете писать надежные, быстродействующие запросы для работы с остатками товаров в любых конфигурациях на платформе 1С:Предприятие 8.