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

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

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

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

Подходы к реализации динамической колонки остатка

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

1. Доработка запроса динамического списка

Это наиболее прямой и часто рекомендуемый подход для получения дополнительных данных, включая остатки. Мы используем свойство "Произвольный запрос" динамического списка. Разберем шаги:

  1. Откроем форму динамического списка, который требуется доработать.
  2. Перейдем в свойства элемента формы типа "Динамический список".
  3. В поле "Основная таблица" выберем опцию "Произвольный запрос".
  4. В окне конструктора запросов или напрямую в тексте запроса нам необходимо будет объединить основную таблицу списка (например, таблицу документов) с виртуальной таблицей остатков нужного регистра накопления. Используем левое соединение, чтобы обеспечить вывод всех документов, даже если для них нет остатков.

Пример структуры запроса (схематично):


ВЫБРАТЬ
    Документ.Ссылка КАК Ссылка,
    Документ.Номер КАК Номер,
    Документ.Дата КАК Дата,
    Документ.Контрагент КАК Контрагент,
    ЕСТЬNULL(ОстаткиТоваров.КоличествоОстаток, 0) КАК ОстатокТоваров
ИЗ
    Документ.ПриходнаяНакладная КАК Документ
        ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиТоваров.Остатки(
                Документ.Дата, // Важно: срез на момент времени документа
                Склад = &ОсновнойСклад) КАК ОстаткиТоваров
        ПО Документ.Номенклатура = ОстаткиТоваров.Номенклатура

Важные нюансы при работе с запросом:

  1. Срезы остатков на каждую дату: Получение остатков на момент времени каждого документа является ключевой сложностью. В приведенном примере мы используем Документ.Дата как параметр среза, что позволяет получить остаток на дату каждого конкретного документа. Однако, если требуется более сложный срез, например, на конец дня или с учетом проведения документа, это может потребовать дополнительных условий или подходов. Виртуальные таблицы регистров накопления позволяют передавать в параметры "НачалоПериода" и "КонецПериода" отдельные значения, но не список дат для каждой строки, что требует внимательной проработки соединения.
  2. Производительность: Это критически важный аспект. Добавление сложных соединений, особенно с регистрами остатков, может существенно замедлить работу списка. Для оптимизации используем следующие подходы:
    • Индексы: Убедимся, что все поля, участвующие в соединениях и условиях, проиндексированы.
    • Избегание сложных конструкций: По возможности минимизируем использование вложенных запросов, большого количества таблиц, а также условий «ИЛИ», «НЕ», «ПОДСТРОКА», «МЕСЯЦ», «ВЫБОР КОГДА» в запросах динамического списка, так как они могут снизить производительность.
    • Временные таблицы: В динамических списках рекомендуется использовать временные таблицы только тогда, когда они содержат заведомо небольшое количество записей. Значения временных таблиц в динамическом списке не кэшируются и формируются при каждом считывании данных, что может вызвать замедления.
    • "Динамическое считывание данных": Этот флаг в свойствах запроса динамического списка влияет на то, как платформа загружает данные. Если он установлен, запрос забирает маленькие порции данных (например, по 45 строк), что может вызывать "подвисания" при прокрутке. Если не установлен, порции данных больше (например, по 1000 строк), что может быть комфортнее для прокрутки, но увеличивает время выполнения запроса для первой загрузки. Подбираем оптимальное значение в зависимости от сценария использования.

2. Использование обработчика `ПриПолученииДанныхНаСервере` (для платформы 8.3.10.2168 и выше)

Этот подход является более гибким и часто предпочтительным для версий платформы, начиная с 8.3.10.2168. Он позволяет выполнить дополнительную обработку данных после того, как основной запрос динамического списка уже отработал. Разберем его особенности:

  1. Доступность: Обработчик ПриПолученииДанныхНаСервере доступен только в версиях платформы 1С:Предприятие 8.3.10.2168 и выше. Если ваша платформа ниже этой версии (например, как у автора темы – 8.3.4), этот метод будет недоступен, и придется использовать доработку запроса или другие альтернативы.
  2. Принцип работы:
    • Основной запрос динамического списка остается максимально простым, что положительно сказывается на его производительности.
    • В обработчике ПриПолученииДанныхНаСервере мы можем получить ключи (ссылки) всех строк, которые отображены в текущем окне динамического списка.
    • По этим ключам мы формируем отдельный, более специализированный запрос, который получает необходимые остатки или другие дополнительные данные.
    • Затем мы заполняем значения в созданной нами дополнительной колонке динамического списка, используя полученные данные.

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

Пример использования (схематично):


&НаСервере
Процедура СписокПриПолученииДанныхНаСервере(Элемент, Данные, Отказ)
    МассивСсылок = Новый Массив;
    Для Каждого СтрокаДанных Из Данные Цикл
        МассивСсылок.Добавить(СтрокаДанных.Ссылка);
    КонецЦикла;

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

        РезультатЗапроса = Запрос.Выполнить().Выгрузить();
        ИндексированныеДанные = Новый Соответствие;
        Для Каждого СтрокаРезультата Из РезультатЗапроса Цикл
            ИндексированныеДанные.Вставить(СтрокаРезультата.Ссылка, СтрокаРезультата.ОстатокТоваров);
        КонецЦикла;

        Для Каждого СтрокаДанных Из Данные Цикл
            Если СтрокаДанных.Свойство("Остаток") И ИндексированныеДанные.Содержит(СтрокаДанных.Ссылка) Тогда
                СтрокаДанных.Остаток = ИндексированныеДанные.Получить(СтрокаДанных.Ссылка);
            КонецЕсли;
        КонецЦикла;
    КонецЕсли;
КонецПроцедуры

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

Расчет и вывод итогов по колонке в динамическом списке

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

Рассмотрим по шагам, как это можно реализовать, опираясь на пример из обсуждения:

1. Подготовка формы и подключение обработчика ожидания

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


&НаКлиенте
Процедура ПриОткрытии(Отказ)
    // При первом вызове запомним текущие настройки списка.
    // Это будет строка, содержащая ключевые параметры текущих настроек.
    // Мы объявляем ее как реквизит формы или глобальную переменную на клиенте.
    // Например, НастройкиСпискаФормы = "";
    НастройкиСпискаФормы = ""; 
    ПодключитьОбработчикОжидания("ПроверкаИзменения", 1); // Проверять изменения каждую секунду
КонецПроцедуры

Мы инициализируем переменную НастройкиСпискаФормы (которая должна быть реквизитом формы или клиентской переменной) и подключаем обработчик ожидания ПроверкаИзменения, который будет запускаться каждую секунду.

2. Отслеживание изменений настроек списка

Функция ПроверкаИзменения будет собирать текущие настройки динамического списка и сравнивать их с сохраненными. Если настройки изменились, запускается пересчет итогов.


&НаКлиенте
Процедура ПроверкаИзменения()
    
    // Создаем строку, отражающую текущие настройки списка
    НастройкиСпискаТек = "";
    Для Каждого ЭлементПользНастроек Из Список.КомпоновщикНастроек.ПользовательскиеНастройки.Элементы Цикл
        // Добавляем в строку отборы и сортировки
        НастройкиСпискаТек = НастройкиСпискаТек + СокрЛП(ЭлементПользНастроек);
    КонецЦикла;
    // Добавляем в ловушку период списка, если это список документов
    НастройкиСпискаТек = НастройкиСпискаТек + СокрЛП(Элементы.Список.Период.ДатаНачала) + СокрЛП(Элементы.Список.Период.ДатаОкончания);
    
    // Сравниваем текущие настройки с ранее сохраненными
    Если НастройкиСпискаФормы <> НастройкиСпискаТек Тогда    
        // Если что-то поменялось, обновляем сохраненные настройки
        НастройкиСпискаФормы = НастройкиСпискаТек;
        
        СчитаемИтогиНаСервере(); // Вызываем серверную функцию для пересчета итогов
        
    КонецЕсли;
    
КонецПроцедуры

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

3. Расчет итогов на сервере с использованием СКД

Основная логика расчета итогов выполняется на сервере, используя возможности системы компоновки данных. Мы используем схему и настройки СКД динамического списка для "виртуального" формирования данных и получения итогового значения.


&НаСервере
Процедура СчитаемИтогиНаСервере()
    
    // Получаем исполняемую схему компоновки данных списка
    СКД              = Элементы.Список.ПолучитьИсполняемуюСхемуКомпоновкиДанных();
    // Получаем исполняемые настройки компоновки данных списка (включая пользовательские отборы и сортировки)
    НастройкаСКД     = Элементы.Список.ПолучитьИсполняемыеНастройкиКомпоновкиДанных();    
    
    // Создаем компоновщик макета компоновки данных
    КомпоновщикМакетаКомпоновкиДанных = Новый КомпоновщикМакетаКомпоновкиДанных;
    // Выполняем макет компоновки данных. Важно использовать Тип("ГенераторМакетаКомпоновкиДанныхДляКоллекцииЗначений"),
    // чтобы получить результат, который можно будет обработать ПроцессоромВыводаВКоллекциюЗначений.
    МакетКомпоновкиДанных = КомпоновщикМакетаКомпоновкиДанных.Выполнить(СКД, НастройкаСКД,,,Тип("ГенераторМакетаКомпоновкиДанныхДляКоллекцииЗначений")) ;
    
    // Создаем процессор компоновки данных и инициализируем его полученным макетом
    ПроцессорКомпоновкиДанных = Новый ПроцессорКомпоновкиДанных;
    ПроцессорКомпоновкиДанных.Инициализировать(МакетКомпоновкиДанных);
    
    // Создаем процессор вывода результата компоновки данных в коллекцию значений
    ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений;
    
    // Выводим результат и получаем итог по нужной колонке.
    // Предполагается, что в запросе списка или в его пользовательских настройках есть поле с именем "СуммаУч"
    // или "Остаток", по которому нужно посчитать итог.
    ИтогСуммаУч = ПроцессорВывода.Вывести(ПроцессорКомпоновкиДанных).Итог("СуммаУч");    
    
    // Теперь ИтогСуммаУч содержит нужное нам значение.
    // Его можно передать на клиент и отобразить, например, в поле надписи в подвале формы.
    // Например: Элементы.ПолеИтога.Заголовок = "Итого: " + Формат(ИтогСуммаУч, "ЧДЦ=2");
    
    // Пример вывода на клиент (для отображения в поле надписи)
    ВывестиИтогНаКлиент(ИтогСуммаУч);
    
КонецПроцедуры

&НаКлиенте
Процедура ВывестиИтогНаКлиент(СуммаИтога)
    // Предполагаем, что на форме есть элемент "ПолеИтога", куда вы хотите вывести значение.
    // Это может быть Поле надписи или любой другой элемент, способный отобразить текст.
    Если Элементы.Найти("ПолеИтога") Тогда
        Элементы.ПолеИтога.Заголовок = "Итоговая сумма: " + Формат(СуммаИтога, "ЧЦ=15; ЧДЦ=2");
    КонецЕсли;
КонецПроцедуры

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

Полученное итоговое значение (например, ИтогСуммаУч) затем может быть передано на клиент (с помощью отдельной клиентской процедуры или реквизита формы) и выведено в любой подходящий элемент формы, например, в ПолеНадписи в подвале формы.

Общие рекомендации по производительности динамических списков

При работе с динамическими списками в управляемых формах всегда следует помнить о производительности. Вот несколько дополнительных советов:

  1. Удаление ненужных колонок: В пользовательском режиме удаление ненужных колонок из списка значительно улучшает производительность, поскольку платформа тратит меньше ресурсов на их загрузку и отображение.
  2. Избегание вывода дополнительных полей/характеристик через точку: Например, вместо Номенклатура.ЕдиницаИзмерения.Наименование лучше получить НаименованиеЕдиницыИзмерения напрямую в запросе, если это возможно. Чрезмерное использование точек может сильно снизить производительность, так как это вызывает дополнительные обращения к базе данных или сложную обработку.
  3. Отложенное формирование списков на вкладках: Если на форме имеется несколько динамических списков, расположенных на разных вкладках, рассмотрите возможность их формирования только в момент активации соответствующей вкладки. Это можно реализовать, используя параметр в условии запроса списка и управляя его значением при смене вкладок. Например, установить условие ГДЕ 1 = &ЗагружатьСписок и менять значение параметра с 0 на 1 при активации вкладки.
  4. Ограничение данных: Старайтесь всегда использовать максимально возможные отборы в динамическом списке, чтобы загружать только необходимые данные.

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

← На главную