Как установить отбор по расчетным полям (остаткам) в табличном поле на обычной форме в 1С?

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

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

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

В чем сложность фильтрации динамических списков в обычных формах?

Динамический список в обычных формах (будь то СправочникСписок, ДокументСписок или РегистрСведенийСписок) жестко привязан к конкретной физической таблице базы данных СУБД. Платформа считывает данные порциями, оптимизируя потребление памяти. Когда мы программно добавляем колонки в такое табличное поле и заполняем их в событии отрисовки интерфейса, эти данные существуют только «на экране» — база данных о них ничего не знает. Следовательно, стандартный механизм ТабличноеПоле.Значение.Отбор не может отфильтровать строки по этим колонкам.

---

Анализ опасного решения: почему код с форумов может «уронить» базу данных

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


// ПРИМЕР ТОГО, КАК ДЕЛАТЬ НЕЛЬЗЯ!
Процедура ТабличноеПолеАналогиПриПолученииДанных(Элемент, ОформленияСтрок)
    // Формирование массива аналогов на экране...
    // Выполнение запроса к остаткам...
    
    // Попытка установить отбор прямо в событии отрисовки:
    ОтборОстатка = ЭлементыФормы.ТабличноеПолеАналоги.Значение;
    ОтборОстатка.Отбор.Аналог.Установить(СписокАналоговСБолееЧемНольОстатками); 
КонецПроцедуры

Разберем основные проблемы этого кода по шагам:

  1. Критическая ошибка: Бесконечная рекурсия (Зацикливание интерфейса). Событие ПриПолученииДанных срабатывает автоматически каждый раз, когда табличное поле выводится на экран, прокручивается или обновляется. Метод Отбор...Установить() принудительно заставляет платформу перечитать данные из СУБД и перерисовать таблицу. Перерисовка таблицы снова вызывает событие ПриПолученииДанных, которое опять меняет отбор и снова инициирует отрисовку. В результате приложение намертво зависает или аварийно завершает работу из-за переполнения стека.
  2. Проблема производительности: Запросы в цикле отрисовки. Выполнение SQL-запроса внутри ПриПолученииДанных — это грубый антипаттерн (так называемая проблема N+1 запроса). При быстром скроллинге списка мышкой СУБД будет перегружена десятками тяжелых запросов в секунду.
  3. Неоптимальное соединение таблиц. Если в запросе соединяться с таблицей Остатки без параметров, система сначала рассчитает остатки по абсолютно всем товарам в базе данных, а лишь затем отфильтрует их. На реальной многотысячной базе это приведет к колоссальным задержкам.

---

Вариант №1 (Рекомендуемый): Полный переход на Таблицу Значений

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

Рассмотрим этот алгоритм подробнее по шагам:

  1. В окне свойств табличного поля на форме очищаем свойство Данные (убираем привязку к регистру сведений).
  2. В модуле формы объявляем переменную или создаем реквизит формы с типом ТаблицаЗначений.
  3. Управляем выводом данных при активизации строки основного списка товаров (например, списка номенклатуры).

Рассмотрим пример реализации кода в модуле формы:


// Процедура обновляет список аналогов с остатками для выбранной номенклатуры
Процедура ОбновитьТаблицуАналогов(ТекущаяНоменклатура)
    
    // Если номенклатура не выбрана, очищаем таблицу аналогов
    Если Не ЗначениеЗаполнено(ТекущаяНоменклатура) Тогда
        ЭлементыФормы.ТабличноеПолеАналоги.Значение = Новый ТаблицаЗначений;
        ЭлементыФормы.ТабличноеПолеАналоги.СоздатьКолонки();
        Возврат;
    КонецЕсли;
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |   РегАналоги.Аналог КАК Аналог,
        |   РегАналоги.Аналог.Артикул КАК Артикул,
        |   ЕСТЬNULL(ОстаткиТоваров.КоличествоОстаток, 0) КАК Остаток
        |ИЗ
        |   РегистрСведений.Аналоги КАК РегАналоги
        |   // Соединяем с виртуальной таблицей остатков, передавая параметры для оптимизации
        |   ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки(, Номенклатура В (
        |       ВЫБРАТЬ ВЫРАЗИТЬ(АналогиВнутр.Аналог КАК Справочник.Номенклатура)
        |       ИЗ РегистрСведений.Аналоги КАК АналогиВнутр
        |       ГДЕ АналогиВнутр.Номенклатура = &Номенклатура
        |   )) КАК ОстаткиТоваров
        |   ПО РегАналоги.Аналог = ОстаткиТоваров.Номенклатура
        |ГДЕ
        |   РегАналоги.Номенклатура = &Номенклатура
        |   И ЕСТЬNULL(ОстаткиТоваров.КоличествоОстаток, 0) > 0"; // Исключаем аналоги без остатков
        
    Запрос.УстановитьПараметр("Номенклатура", ТекущаяНоменклатура);
    
    // Выгружаем результат напрямую в табличное поле
    ТаблицаРезультат = Запрос.Выполнить().Выгрузить();
    ЭлементыФормы.ТабличноеПолеАналоги.Значение = ТаблицаРезультат;
    ЭлементыФормы.ТабличноеПолеАналоги.СоздатьКолонки();
    
    // При желании можно настроить оформление колонок (ширину, заголовки)
    ЭлементыФормы.TabличнаяЧастьАналоги.Колонки.Остаток.Ширина = 10;
    
КонецПроцедуры

// Обработчик события смены строки в основном списке номенклатуры
Процедура СправочникСписокПриАктивизацииСтроки(Элемент)
    ТекущиеДанные = Элемент.ТекущиеДанные;
    Если ТекущиеДанные <> Неопределено Тогда
        // Вызываем обновление таблицы аналогов для новой строки
        ОбновитьТаблицуАналогов(ТекущиеДанные.Ссылка);
    Иначе
        ОбновитьТаблицуАналогов(Неопределено);
    КонецЕсли;
КонецПроцедуры

Преимущества данного метода:

---

Вариант №2: Использование Построителя Запроса

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

Построитель запроса выступает посредником между базой данных и табличным полем. Мы можем программно задать ему сложный текст запроса с левыми соединениями, а платформа сама будет накладывать пользовательские отборы на итоговый SQL-запрос.

Посмотрим на пример инициализации табличного поля через построитель запроса в событии открытия формы:


Процедура ПриОткрытии()
    
    // Настраиваем Построитель Запроса
    Построитель = Новый ПостроительЗапроса;
    Построитель.Текст = 
        "ВЫБРАТЬ
        |   РегАналоги.Номенклатура КАК Номенклатура,
        |   РегАналоги.Аналог КАК Аналог,
        |   РегАналоги.Аналог.Артикул КАК Артикул,
        |   ЕСТЬNULL(ОстаткиТоваров.КоличествоОстаток, 0) КАК Остаток
        |{ВЫБРАТЬ
        |   Номенклатура.*,
        |   Аналог.*,
        |   Артикул,
        |   Остаток}
        |ИЗ
        |   РегистрСведений.Аналоги КАК РегАналоги
        |   ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки КАК ОстаткиТоваров
        |   ПО РегАналоги.Аналог = ОстаткиТоваров.Номенклатура
        |{ГДЕ
        |   РегАналоги.Номенклатура.* КАК Номенклатура,
        |   РегАналоги.Аналог.* КАК Аналог,
        |   (ЕСТЬNULL(ОстаткиТоваров.КоличествоОстаток, 0)) КАК Остаток}";
        
    // Связываем Построитель Запроса с табличным полем
    ТабличноеПолеАналоги.ИсточникДанных = Новый ИсточникДанныхПостроителяЗапроса(Построитель);
    
    // Устанавливаем фиксированный отбор (например, показывать только аналоги с остатками > 0)
    Построитель.Отбор.Добавить("Остаток");
    Построитель.Отбор.Остаток.ВидСравнения = ВидСравнения.Больше;
    Построитель.Отбор.Остаток.Значение = 0;
    Построитель.Отбор.Остаток.Использование = Истина;
    
КонецПроцедуры

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

---

Резюме

Мы проанализировали особенности работы с отборами в обычных формах 1С и выяснили, почему попытки установить фильтры в обработчиках отрисовки (таких как ПриПолученииДанных) приводят к фатальным сбоям программы. Использование Таблицы Значений (Вариант №1) решает проблему несовместимости динамического списка с расчетными данными в 95% случаев и гарантирует стабильную, быструю работу интерфейса без лишней нагрузки на сервер.

← На главную