Как корректно заполнить табличную часть документа 1С из регистров сведений и накопления, избегая пустых выборок?

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

При разработке функционала в 1С, особенно при работе с документами, которые должны автоматически заполняться на основе данных из регистров, мы часто сталкиваемся с необходимостью создания сложных запросов. Это позволяет значительно упростить работу пользователя, но при некорректной логике запроса может приводить к пустым результатам. Рассмотрим ситуацию, когда документ «Пересчёт товаров по помещениям хранения» с табличной частью «Товары» не заполняется данными из регистра сведений и регистра накопления из-за ошибок в условиях запроса.

Постановка проблемы: почему документ остается пустым?

Наш коллега столкнулся с задачей заполнения табличной части документа «Пересчёт товаров по помещениям хранения». Для этого ему требовалось:

  1. Получить список номенклатуры из специального регистра сведений (используемого как перечень для инвентаризации).
  2. Получить данные об остатках по складским ячейкам из регистра накопления.
  3. На основе этих данных заполнить табличную часть документа полями: Номенклатура, ХарактеристикаНоменклатуры, ЕдиницаИзмерения, КоличествоУчетное, КоличествоФактическое.

Основная сложность заключалась в том, чтобы «правильно получить все номенклатуры из регистра сведений и по ним корректно подтянуть остатки, характеристики и единицы измерения из регистра накопления, чтобы табличная часть документа заполнялась без пустых выборок».

Изначальный код, с которым возникла проблема, выглядел следующим образом:


&НаСервере
Процедура ЗаполнитьПоОтборуДляПересчетаНаСервере()
    Объект.Товары.Очистить();
    
    ОтборСклад = Объект.КТ_ОтборДляПересчетаТоваров.Склад;
    ОтборПомещение = Объект.КТ_ОтборДляПересчетаТоваров.Помещение;
    ОтборСкладскаяЯчейка = Объект.КТ_ОтборДляПересчетаТоваров.СкладскаяЯчейка;
    ОтборГруппаНоменклатуры = Объект.КТ_ОтборДляПересчетаТоваров.НоменклатурнаяГруппа;
    
    Запрос = Новый Запрос;
    Запрос.Текст = "ВЫБРАТЬ
                    | КТ_НоменклатураВСправочнике_КТ_ОтборыДляДокументоаИнвентаризация.Номенклатура КАК Номенклатура
                    |ИЗ
                    | РегистрСведений.КТ_НоменклатураВСправочнике_КТ_ОтборыДляДокументоаИнвентаризация КАК КТ_НоменклатураВСправочнике_КТ_ОтборыДляДокументоаИнвентаризация
                    |ГДЕ
                    | КТ_НоменклатураВСправочнике_КТ_ОтборыДляДокументоаИнвентаризация.КТ_Отбор = &КТ_Отбор";
    Запрос.УстановитьПараметр("КТ_Отбор", Объект.КТ_ОтборДляПересчетаТоваров);
    МассивНоменклатуры = Запрос.Выполнить().Выгрузить().ВыгрузитьКолонку("Номенклатура");
    
    Запрос = Новый Запрос;
    ТекстЗапроса = "ВЫБРАТЬ
                    | КТ_СкладскиеЯчейкиОстатки.КоличествоОстаток КАК КоличествоОстаток,
                    | КТ_СкладскиеЯчейкиОстатки.Номенклатура КАК Номенклатура,
                    | КТ_СкладскиеЯчейкиОстатки.СкладскаяЯчейка КАК СкладскаяЯчейка,
                    | КТ_СкладскиеЯчейкиОстатки.ХарактеристикиНоменклатуры КАК ХарактеристикиНоменклатуры,
                    | КТ_СкладскиеЯчейкиОстатки.ЕдиницаИзмерения КАК ЕдиницаИзмерения
                    |ПОМЕСТИТЬ ВТ_Остатки
                    |ИЗ
                    | РегистрНакопления.КТ_СкладскиеЯчейки.Остатки(
                    | &ДатаОстатка,
                    | Склад = &Склад
                    | И Помещение = &Помещение
                    | И СкладскаяЯчейка = &СкладскаяЯчейка
                    | И Номенклатура.Родитель = &ГруппаНоменклатуры
                    | И Номенклатура В (&МассивНоменклатуры)) КАК КТ_СкладскиеЯчейкиОстатки
                    |;
                    |
                    |////////////////////////////////////////////////////////////////////////////////
                    |ВЫБРАТЬ
                    | КТ_ПересчетТоваровПоЗонамХраненияТовары.Номенклатура КАК Номенклатура
                    |ПОМЕСТИТЬ ВТ_УжеИмеющаясяНоменклатура
                    |ИЗ
                    | Документ.КТ_ПересчетТоваровПоПомещениямХранения.Товары КАК КТ_ПересчетТоваровПоЗонамХраненияТовары
                    |ГДЕ
                    | КТ_ПересчетТоваровПоЗонамХраненияТовары.Ссылка.КТ_СтатусДокумента <> &КТ_СтатусДокумента
                    | И КТ_ПересчетТоваровПоЗонамХраненияТовары.Ссылка.ДокументОснование = &ДокументОснование
                    |;
                    |
                    |////////////////////////////////////////////////////////////////////////////////
                    |ВЫБРАТЬ
                    | ЕСТЬNULL(ВТ_Остатки.КоличествоОстаток, 0) КАК КоличествоУчетное,
                    | ЕСТЬNULL(ВТ_Остатки.КоличествоОстаток, 0) КАК КоличествоФактическое,
                    | ВТ_Остатки.СкладскаяЯчейка КАК КТ_СкладскаяЯчейка,
                    | ВТ_Остатки.ХарактеристикиНоменклатуры КАК КТ_ХарактеристикиНоменклатуры,
                    | ВТ_Остатки.ЕдиницаИзмерения КАК ЕдиницаИзмерения,
                    | ВТ_Остатки.Номенклатура КАК Номенклатура
                    |ИЗ
                    | ВТ_Остатки КАК ВТ_Остатки
                    |ГДЕ
                    | НЕ ВТ_Остатки.Номенклатура В
                    | (ВЫБРАТЬ
                    | ВТ_УжеИмеющаясяНоменклатура.Номенклатура КАК Номенклатура
                    | ИЗ
                    | ВТ_УжеИмеющаясяНоменклатура КАК ВТ_УжеИмеющаясяНоменклатура)";
                    
    Запрос.УстановитьПараметр("ДатаОстатка", Объект.Дата);
    Запрос.УстановитьПараметр("ГруппаНоменклатуры",ОтборГруппаНоменклатуры);
    Запрос.УстановитьПараметр("Помещение" , ОтборПомещение);
    Запрос.УстановитьПараметр("Склад" , ОтборСклад);
    Запрос.УстановитьПараметр("СкладскаяЯчейка", ОтборСкладскаяЯчейка);
    Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
    Запрос.УстановитьПараметр("КТ_СтатусДокумента", Перечисления.КТ_СтатусыДокументов.Аннулирован);
    Запрос.УстановитьПараметр("ДокументОснование", Объект.ДокументОснование);
    
    Если НЕ ЗначениеЗаполнено(ОтборПомещение) Тогда
        ТекстЗапроса = СтрЗаменить(ТекстЗапроса,"И Помещение = &Помещение","");
    КонецЕсли;
    Если НЕ ЗначениеЗаполнено(ОтборСкладскаяЯчейка) Тогда
        ТекстЗапроса = СтрЗаменить(ТекстЗапроса,"И СкладскаяЯчейка = &СкладскаяЯчейка","");
    КонецЕсли;
    Если НЕ ЗначениеЗаполнено(ОтборГруппаНоменклатуры) Тогда
        ТекстЗапроса = СтрЗаменить(ТекстЗапроса,"И Номенклатура.Родитель = &ГруппаНоменклатуры","");
    КонецЕсли;
    Если МассивНоменклатуры.Количество = 0 Тогда
        ТекстЗапроса = СтрЗаменить(ТекстЗапроса,"И Номенклатура В (&МассивНоменклатуры)","");
    КонецЕсли;
    
    Запрос.Текст = ТекстЗапроса;
    РезультатЗапроса = Запрос.Выполнить();
    ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
    
    Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
        НоваяСтрокаТовары = Объект.Товары.Добавить();
        ЗаполнитьЗначенияСвойств(НоваяСтрокаТовары,ВыборкаДетальныеЗаписи);
        НоваяСтрокаТовары.КоличествоРазница = НоваяСтрокаТовары.КоличествоУчетное - НоваяСтрокаТовары.КоличествоФактическое;
    КонецЦикла;
КонецПроцедуры

После выполнения этого кода документ оставался пустым. Отладка показала, что МассивНоменклатуры заполняется корректно, однако в ВыборкаДетальныеЗаписи (результат второго запроса) данные отсутствуют, а значения полей NULL.

Пошаговый анализ и выявление причины

  1. Изоляция проблемы: Мы выяснили, что первый запрос, получающий список номенклатуры из регистра сведений, работает правильно. Проблема кроется во втором запросе, который формирует временную таблицу ВТ_Остатки из виртуальной таблицы Остатки() регистра накопления КТ_СкладскиеЯчейки.

  2. Строгие условия оператора «И»: Изначально условия внутри виртуальной таблицы Остатки() были объединены оператором И (AND):

    
    ...Остатки(
    &ДатаОстатка,
    Склад = &Склад
    И Помещение = &Помещение
    И СкладскаяЯчейка = &СкладскаяЯчейка
    И Номенклатура.Родитель = &ГруппаНоменклатуры
    И Номенклатура В (&МассивНоменклатуры))
    

    Оператор И требует, чтобы все перечисленные условия были истинными для того, чтобы строка попала в выборку. Это означает, что если хотя бы одно из полей (например, Помещение или СкладскаяЯчейка) в регистрах не заполнено или не соответствует переданному параметру, то строка не будет выбрана. Даже если мы динамически удаляли условия с помощью СтрЗаменить для незаполненных параметров, жесткое «И» между оставшимися параметрами могло приводить к отсутствию результатов, если номенклатура не удовлетворяла всем этим критериям одновременно.

  3. Необходимость более мягкого отбора: В данном случае, вероятно, требовалось получить остатки по номенклатуре, которая удовлетворяет хотя бы одному из дополнительных условий отбора (кроме основного отбора по складу и списку номенклатуры). Например, если указано Помещение, то мы ищем по нему, но если Помещение не указано, а указана СкладскаяЯчейка, то ищем по ячейке. Использование И во всех случаях делало запрос слишком ограничивающим.

Решение проблемы: корректировка логических операторов

Ключевым шагом в решении проблемы стала замена оператора И на ИЛИ (OR) для тех условий, которые должны действовать как взаимоисключающие или альтернативные фильтры внутри виртуальной таблицы Остатки(). Это позволило расширить выборку и включить номенклатуру, которая удовлетворяет хотя бы одному из критериев отбора, а не всем сразу.

Исправленная часть запроса для виртуальной таблицы КТ_СкладскиеЯчейки.Остатки стала выглядеть так:


РегистрНакопления.КТ_СкладскиеЯчейки.Остатки(
    &ДатаОстатка,
    Склад = &Склад
    ИЛИ Помещение = &Помещение
    ИЛИ СкладскаяЯчейка = &СкладскаяЯчейка
    ИЛИ Номенклатура.Родитель = &ГруппаНоменклатуры
    ИЛИ Номенклатура В (&МассивНоменклатуры)) КАК КТ_СкладскиеЯчейкиОстатки

Важное примечание: Переход от И к ИЛИ кардинально меняет логику отбора. Если вы изначально предполагали, что все указанные условия должны быть выполнены (например, "Номенклатура на Складе А И в Помещении Б И в Ячейке В"), то оператор И был бы правильным. Однако, если цель была "Номенклатура на Складе А И (в Помещении Б ИЛИ в Ячейке В ИЛИ принадлежит Группе Г)", то такой комбинированный подход с использованием скобок и разных операторов был бы необходим. В нашем случае, переход на ИЛИ для большинства условий позволил получить более широкую выборку, соответствующую фактическим потребностям.

Дополнительное замечание по динамическому изменению запроса: Код уже содержал блоки с СтрЗаменить, которые предназначались для удаления условий из текста запроса, если соответствующие параметры отбора были не заполнены:


Если НЕ ЗначениеЗаполнено(ОтборПомещение) Тогда
    ТекстЗапроса = СтрЗаменить(ТекстЗапроса,"И Помещение = &Помещение","");
КонецЕсли;
// ... аналогично для других отборов

Этот подход является правильным и рекомендуемым для обработки необязательных условий, которые должны действовать как "И" (AND). Если параметр не заполнен, условие по нему полностью удаляется из запроса, не мешая остальным. В данном конкретном случае, даже при использовании динамического удаления, если какие-либо условия оставались активными (т.е. параметры были заполнены), оператор И делал их слишком строгими. Поэтому окончательное решение с ИЛИ указывает на то, что для автора требовался более широкий, "объединяющий" отбор по этим параметрам.

Рекомендации и лучшие практики при работе с запросами

Чтобы избежать подобных проблем в будущем и сделать разработку более эффективной, рассмотрим ряд рекомендаций:

  1. Использование Консоли запросов для отладки

    Всегда проверяйте свои запросы в "Консоли запросов" – это незаменимый инструмент разработчика 1С. Он позволяет:

    • Видеть синтаксические ошибки сразу же.
    • Задавать параметры запроса вручную и выполнять его, чтобы мгновенно получить результат.
    • Анализировать структуру и состав полей выборки, прежде чем интегрировать запрос в код.
    • Быстро проверять различные комбинации условий и их влияние на результат.

    Если бы начальный запрос был проверен в консоли, было бы сразу видно, что ВТ_Остатки пуста или содержит NULL, что позволило бы быстро сосредоточиться на условиях внутри этой виртуальной таблицы.

  2. Пошаговая отладка кода на сервере

    При выполнении кода на сервере (процедуры с директивой &НаСервере) активно используйте отладчик 1С. Отладчик позволяет:

    • Пошагово проходить по коду, контролируя поток выполнения.
    • Проверять значения всех переменных на любом этапе, включая содержимое массивов (МассивНоменклатуры) и объектов (Объект.КТ_ОтборДляПересчетаТоваров).
    • Анализировать ТекстЗапроса после всех модификаций СтрЗаменить, чтобы убедиться, что он сформирован правильно.
    • Исследовать РезультатЗапроса и ВыборкаДетальныеЗаписи непосредственно после выполнения запроса, чтобы понять, какие данные были возвращены.

    Отладчик помогает увидеть "почему" выборка пуста, показывая промежуточные состояния и значения.

  3. Динамическое формирование условий запроса

    Для обработки необязательных параметров отбора (как в нашем случае Помещение, СкладскаяЯчейка, ГруппаНоменклатуры) существуют два основных подхода:

    1. Модификация текста запроса через СтрЗаменить: Этот метод, уже использованный в примере, эффективен для условий, которые должны быть объединены оператором И. Если параметр не заполнен, соответствующее условие полностью удаляется из строки запроса.

      
      Если НЕ ЗначениеЗаполнено(ОтборПомещение) Тогда
          ТекстЗапроса = СтрЗаменить(ТекстЗапроса,"И Помещение = &Помещение","");
      КонецЕсли;
      

      Необходимо убедиться, что после удаления условия не остается лишних И или ИЛИ в начале или конце списка условий.

    2. Использование конструкций ВЫБОР КОГДА в запросе: Для более сложных динамических условий, особенно если нежелательно модифицировать текст запроса строковыми методами, можно использовать конструкции ВЫБОР КОГДА:

      
      ...ГДЕ
      (ВЫБОР
          КОГДА &ПараметрПомещениеЗаполнен ТОГДА КТ_СкладскиеЯчейкиОстатки.Помещение = &Помещение
          ИНАЧЕ ИСТИНА
      КОНЕЦ)
      И
      (ВЫБОР
          КОГДА &ПараметрГруппаНоменклатурыЗаполнен ТОГДА КТ_СкладскиеЯчейкиОстатки.Номенклатура.Родитель = &ГруппаНоменклатуры
          ИНАЧЕ ИСТИНА
      КОНЕЦ)
      

      В этом случае мы передаем дополнительные параметры-флаги (например, &ПараметрПомещениеЗаполнен) и само значение параметра. Если флаг истинен, то условие применяется; иначе условие всегда истинно (ИНАЧЕ ИСТИНА), что не влияет на выборку.

  4. Оптимизация работы с виртуальными таблицами регистров накопления

    Виртуальная таблица Остатки() (или ОстаткиИОбороты()) регистра накопления – это мощный инструмент, но он требует внимательного отношения к параметрам:

    • Дата: Параметр &ДатаОстатка всегда обязателен для указания, на какую дату формируются остатки.
    • Измерения: Все условия, указанные в скобках после Остатки() (например, Склад = &Склад), применяются как фильтры к измерениям регистра до расчета остатков. Это сужает исходный набор данных, по которым будет произведен расчет.
    • Логика И / ИЛИ: Помните, что И сужает выборку, требуя одновременного выполнения всех условий, тогда как ИЛИ расширяет ее, требуя выполнения хотя бы одного условия. Тщательно анализируйте, какая логика необходима в вашем конкретном случае. Если вы хотите, чтобы необязательные фильтры действовали независимо, ИЛИ может быть оправданным, но это редкость для типовых отборов. Чаще для опциональных фильтров используется И с динамическим удалением условий, как описано выше.
  5. Использование метода ЗаполнитьЗначенияСвойств

    Метод ЗаполнитьЗначенияСвойств(Приемник, Источник) – это удобный способ быстро перенести значения одноименных свойств из одного объекта в другой. В нашем случае он эффективно используется для заполнения новой строки табличной части документа данными из выборки запроса:

    
    НоваяСтрокаТовары = Объект.Товары.Добавить();
    ЗаполнитьЗначенияСвойств(НоваяСтрокаТовары,ВыборкаДетальныеЗаписи);
    

    Убедитесь, что имена полей в выборке запроса точно соответствуют именам реквизитов табличной части документа.

  6. Проверка на пустоту результата запроса

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

    
    РезультатЗапроса = Запрос.Выполнить();
    Если РезультатЗапроса.Пустой() Тогда
        Сообщить("По заданным отборам данные не найдены.");
        Возврат;
    КонецЕсли;
    ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
    // ... далее обход выборки
    
  7. Оптимизация запросов с большими массивами

    Если МассивНоменклатуры может содержать очень большое количество элементов (например, тысячи и более), передача его в условие Номенклатура В (&МассивНоменклатуры) может негативно сказаться на производительности запроса. Для таких случаев более эффективным подходом может быть:

    • Помещение МассивНоменклатуры во временную таблицу перед выполнением основного запроса.
    • Затем соединение основной выборки с этой временной таблицей.

    Это позволяет СУБД более эффективно использовать индексы и план выполнения запроса.

Таким образом, наша ситуация показала, насколько важна правильная логика в запросах 1С. Анализ использования операторов И и ИЛИ, а также применение инструментов отладки, являются ключевыми для успешной разработки и корректного заполнения документов из регистров.

← На главную