При разработке и доработке конфигураций на платформе 1С:Предприятие 8.3 мы часто сталкиваемся с необходимостью отображения итогов по колонкам табличных частей документов. Эта задача становится более сложной, когда речь идет о так называемых «расчетных колонках», то есть колонках, которые не являются прямыми реквизитами табличной части, а вычисляются на форме, и при этом требуется, чтобы итоги учитывали фильтры, установленные пользователем в табличном поле.
Стандартный функционал платформы 1С предоставляет свойство «Считать итоги» для колонок табличного поля, а также метод Итог() для коллекций данных (например, ТабличнаяЧастьДокумента.Итог("Сумма")). Однако эти механизмы имеют существенное ограничение: они, как правило, не учитывают интерактивные фильтры, которые пользователь устанавливает непосредственно в интерфейсе табличного поля. Это означает, что если пользователь отфильтровал строки по какому-либо условию, стандартные итоги все равно будут рассчитываться по всем строкам, а не только по видимым. Для расчетных колонок задача усложняется тем, что их значения вообще не хранятся в данных объекта и их нужно рассчитывать.
В этой статье мы подробно разберем, как эффективно решить данную проблему, обеспечивая корректный расчет и отображение итогов для расчетных колонок с учетом всех активных пользовательских фильтров. Мы рассмотрим несколько подходов, начиная от прямого перебора строк с проверкой их видимости, до более современных и мощных методов с использованием динамических списков и Системы Компоновки Данных.
Один из наиболее распространенных и относительно простых способов решения задачи — это программный перебор всех строк табличной части и проверка каждой строки на соответствие текущим фильтрам, установленным в табличном поле. Платформа 1С предоставляет специальный метод для этой цели: ТабличноеПоле.ПроверитьСтроку(). Этот метод чрезвычайно полезен, поскольку он инкапсулирует в себе всю сложную логику проверки строки на соответствие произвольному набору фильтров (включая равенство, неравенство, списки, интервалы, вхождения подстрок и так далее), избавляя нас от необходимости вручную разбирать каждый элемент отбора.
Суть подхода:
ПроверитьСтроку(), передавая ему идентификатор этой строки. Этот метод возвращает Истина, если строка соответствует всем установленным фильтрам и должна быть видима, и Ложь в противном случае.Пример реализации на клиенте:
Предположим, у нас есть табличное поле с именем Товары, связанное с табличной частью объекта Объект.Товары. И есть расчетная колонка, например, СуммаСНДС, которая не является реквизитом табличной части, а вычисляется на форме.
&НаКлиенте
Процедура РассчитатьИтогиСУчетомФильтра()
// Переменная для накопления итоговой суммы
ИтоговаяСумма = 0;
// Получаем ссылку на элемент табличного поля на форме
ЭлементыТабличногоПоля = Элементы.Товары;
// Получаем ссылку на данные табличной части объекта
ДанныеТабличнойЧасти = Объект.Товары;
// Перебираем все строки данных табличной части
Для Каждого СтрокаДанных Из ДанныеТабличнойЧасти Цикл
// Получаем идентификатор текущей строки данных.
// Это необходимо, чтобы метод ПроверитьСтроку() мог найти соответствующую строку в отображаемых данных формы.
ИдентификаторСтроки = СтрокаДанных.ПолучитьИдентификатор();
// Проверяем, видима ли текущая строка в табличном поле с учетом всех установленных фильтров.
Если ЭлементыТабличногоПоля.ПроверитьСтроку(ИдентификаторСтроки) Тогда
// Если строка видима, мы добавляем значение ее расчетной колонки к итогу.
// Предполагается, что значение расчетной колонки 'СуммаСНДС' уже как-то рассчитано
// и доступно через объект 'СтрокаДанных' (например, если оно было предварительно записано
// во временный реквизит строки или вычислено при выводе на форму).
// В случае, если колонка чисто 'виртуальная' и не записана в СтрокаДанных,
// ее нужно будет пересчитать здесь на основе других полей СтрокаДанных.
ИтоговаяСумма = ИтоговаяСумма + СтрокаДанных.СуммаСНДС; // Замените на имя вашей расчетной колонки
КонецЕсли;
КонецЦикла;
// Теперь мы можем вывести полученную ИтоговуюСумму.
// Если нужно вывести в подвал табличного поля:
// Найдем элемент подвала колонки, например, 'ТоварыСуммаСНДСПодвал'.
// Имя элемента подвала обычно формируется как ИмяЭлементаТабличногоПоля + ИмяКолонки + "Подвал".
ЭлементыТабличногоПоля.ПодчиненныеЭлементы.ТоварыСуммаСНДСПодвал.Заголовок = Формат(ИтоговаяСумма, "ЧДЦ=2");
// Или, если нужно вывести в отдельное поле формы:
// Элементы.ПолеИтоговаяСумма.Значение = ИтоговаяСумма;
КонецПроцедуры
Когда вызывать эту процедуру?
Для того чтобы итоги всегда были актуальными, процедуру РассчитатьИтогиСУчетомФильтра() необходимо вызывать в следующих случаях:
ПриОткрытии формы).ПриИзменении для колонок, влияющих на расчетную сумму, или в ПриОкончанииРедактирования строки).ПриИзменении у объекта отбора табличного поля. Допустим, ваше табличное поле называется Товары, тогда его отбор доступен как Элементы.Товары.ОтборСтрок. Подписку на событие можно оформить в событии формы ПриСозданииНаСервере или ПриОткрытии:
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
// ... ваш код инициализации формы ...
// Устанавливаем обработчик события ПриИзменении для отбора строк табличного поля
Элементы.Товары.ОтборСтрок.УстановитьДействие("ПриИзменении", Новый Действие("ТоварыОтборСтрокПриИзменении"));
// ... другой код ...
КонецПроцедуры
&НаКлиенте
Процедура ТоварыОтборСтрокПриИзменении(Элемент)
РассчитатьИтогиСУчетомФильтра();
КонецПроцедуры
Таким образом, мы гарантируем, что итоги будут пересчитываться всякий раз, когда пользователь изменяет условия фильтрации.
Ограничения и важные моменты:
ПроверитьСтроку() работает только на клиенте или в процедурах, вызываемых с клиента (например, из событий формы).ПолучитьИдентификатор(), уникален только в рамках текущей коллекции.Этот метод является критически важным, поскольку он эффективно решает проблему, избегая сложной ручной обработки всех видов сравнения, как это было предложено в одном из менее оптимальных вариантов на форуме (сообщение 5). Сравним его с тем, что было предложено изначально:
// Пример из сообщения 5 (для сравнения)
Функция ПолучитьМассивСтрокТабличнойЧастиПоТекущимОтборам(ТабличнаяЧасть, ТекущийЭлементФормы)
СтруктураОтбора = Новый Структура;
// заполним СтруктуруОтбора согласно отборам с ВидСравнения.Равно
Для каждого ЭлементОтбора из ТекущийЭлементФормы.ОтборСтрок Цикл
Если ЭлементОтбора.Использование И ЭлементОтбора.ВидСравнения = ВидСравнения.Равно Тогда
СтруктураОтбора.Вставить(ЭлементОтбора.Имя, ЭлементОтбора.Значение);
КонецЕсли;
КонецЦикла;
// получим массив строк табличной части, соответствующих отборам с ВидСравнения.Равно
МассивНайденныхСтрок = ТабличнаяЧасть.НайтиСтроки(СтруктураОтбора);
// объявим массив, в который будем записывать номера строк МассивНайденныхСтрок, которые не удовлетворяют отбору
МассивНомераУдаляемыхСтрок = Новый Массив;
// Обходим остальные отборы с Не ВидСравнения.Равно и исключаем из массива строки, которые не устраивают отбору
Для каждого Строка из МассивНайденныхСтрок Цикл
УдалитьСтроку = Ложь;
Для каждого ЭлементОтбора из ТекущийЭлементФормы.ОтборСтрок Цикл
// прекращаем обработку текущего элемента, если не установлено его использование
Если Не ЭлементОтбора.Использование Тогда Продолжить КонецЕсли;
//Обработаем ВидСравнения.НеРавно
Если ЭлементОтбора.ВидСравнения = ВидСравнения.НеРавно Тогда
Если Не(Строка[ЭлементОтбора.Имя] <> ЭлементОтбора.Значение) Тогда
УдалитьСтроку = Истина;
Перейти ~ПолучитьМассивСтрокТабличнойЧастиПоТекущимОтборамКонецЦикла;
КонецЕсли;
КонецЕсли;
//Обработаем ВидСравнения.ВСписке
Если ЭлементОтбора.ВидСравнения = ВидСравнения.ВСписке Тогда
ЭлементНайденВСписке = Ложь;
Для каждого ЭлементСписка из ЭлементОтбора.Значение Цикл
Если Строка[ЭлементОтбора.Имя] = ЭлементСписка.Значение Тогда
ЭлементНайденВСписке = Истина;
Прервать;
КонецЕсли;
КонецЦикла;
Если Не ЭлементНайденВСписке Тогда
УдалитьСтроку = Истина;
Перейти ~ПолучитьМассивСтрокТабличнойЧастиПоТекущимОтборамКонецЦикла;
КонецЕсли;
КонецЕсли;
// и так далее для всех видов сравнения...
КонецЦикла;
~ПолучитьМассивСтрокТабличнойЧастиПоТекущимОтборамКонецЦикла:
// удаляем текущую строку из массива, если она не удовлетворила одному из условий
Если УдалитьСтроку Тогда
МассивНомераУдаляемыхСтрок.Добавить(Строка.НомерСтроки);
КонецЕсли;
КонецЦикла;
// Удалим из МассивНайденныхСтрок строки, номера которых занесены в массив МассивНомеровСтрокДляУдаления
// ... (дальнейший код для удаления элементов из массива)
Возврат МассивНайденныхСтрок;
КонецФункции
Как мы видим, ручная обработка всех видов сравнения требует значительно больше кода, более сложна в поддержке и подвержена ошибкам. Метод ПроверитьСтроку() скрывает эту сложность, делая код расчета итогов чистым и лаконичным, как было справедливо отмечено в сообщении 6.
В современных конфигурациях 1С, основанных на управляемых формах, вместо прямой работы с табличными частями документа на форме чаще используются динамические списки. Динамические списки представляют собой мощный механизм для отображения данных из базы данных, который работает на основе запросов, выполняемых на сервере. Этот подход обладает существенными преимуществами при работе с фильтрацией и итогами.
Преимущества динамических списков:
Как реализовать итоги в динамическом списке:
Для реализации итогов по расчетным колонкам в динамическом списке нам понадобится модифицировать основной запрос списка. Если расчетные колонки зависят от данных, которые можно получить запросом (например, некие агрегаты или вычисляемые поля), то мы можем использовать пакетные запросы или временные таблицы.
Пример схемы запроса для динамического списка:
Допустим, наша расчетная колонка СуммаСНДС вычисляется как Сумма * (1 + ПроцентНДС / 100). В основном запросе динамического списка мы можем:
СуммаСНДС.
// Пример запроса для динамического списка (Основной запрос)
ВЫБРАТЬ
Товары.Ссылка,
Товары.Номенклатура,
Товары.Количество,
Товары.Цена,
Товары.Сумма,
Товары.ПроцентНДС,
Товары.Сумма * (1 + Товары.ПроцентНДС / 100) КАК СуммаСНДС
ИЗ
Документ.МойДокумент.Товары КАК Товары
ГДЕ
// Здесь платформа автоматически добавляет условия отбора из пользовательских фильтров
Чтобы получить итог по отфильтрованным данным, мы можем добавить второй пакет запросов, который будет рассчитывать агрегированное значение:
// Пример запроса для динамического списка с расчетом итогов (используя временные таблицы)
// Пакет 1: Получаем данные, которые будут отображаться в списке (с учетом фильтров)
ВЫБРАТЬ
Товары.Ссылка КАК Ссылка,
Товары.Номенклатура КАК Номенклатура,
Товары.Количество КАК Количество,
Товары.Цена КАК Цена,
Товары.Сумма КАК Сумма,
Товары.ПроцентНДС КАК ПроцентНДС,
Товары.Сумма * (1 + Товары.ПроцентНДС / 100) КАК СуммаСНДС
ПОМЕСТИТЬ ВТ_ОтфильтрованныеСтроки
ИЗ
Документ.МойДокумент.Товары КАК Товары
ГДЕ
// Здесь платформа автоматически добавляет условия отбора из пользовательских фильтров
// (мы не пишем их вручную, платформа делает это за нас)
;
// Пакет 2: Рассчитываем итоги по отфильтрованным данным
ВЫБРАТЬ
СУММА(ВТ_ОтфильтрованныеСтроки.СуммаСНДС) КАК ИтогПоСуммеСНДС
ПОМЕСТИТЬ ВТ_Итоги
ИЗ
ВТ_ОтфильтрованныеСтроки КАК ВТ_ОтфильтрованныеСтроки
;
// Пакет 3: Выбираем отображаемые данные и присоединяем к ним итоги
ВЫБРАТЬ
ОтфильтрованныеСтроки.Ссылка,
ОтфильтрованныеСтроки.Номенклатура,
ОтфильтрованныеСтроки.Количество,
ОтфильтрованныеСтроки.Цена,
ОтфильтрованныеСтроки.Сумма,
ОтфильтрованныеСтроки.ПроцентНДС,
ОтфильтрованныеСтроки.СуммаСНДС,
Итоги.ИтогПоСуммеСНДС
ИЗ
ВТ_ОтфильтрованныеСтроки КАК ОтфильтрованныеСтроки,
ВТ_Итоги КАК Итоги
После того как запрос динамического списка будет настроен таким образом, мы сможем использовать поле ИтогПоСуммеСНДС в форме для отображения общего итога. Этот подход идеально подходит для работы с большими объемами данных, так как вся тяжелая работа выполняется на стороне сервера базы данных.
Настройка отображения итогов в динамическом списке:
В свойствах элемента управления "Табличное поле" на форме, которое связано с динамическим списком, мы можем включить отображение подвала. Однако, чтобы динамически обновлять значение в подвале, потребуется:
ИтогПоСуммеСНДС (возможно, сделать ее невидимой или просто использовать как источник данных).ПриОбновлении или ПриЧтенииНаСервере динамического списка, чтобы извлечь значение итога из первой строки (или из специально предназначенного для этого поля) и вывести его в подвал или отдельное поле формы.
// В обработчике ПриЧтенииНаСервере для динамического списка
&НаСервере
Процедура СписокПриЧтенииНаСервере(Список, Отказ)
Если Список.Количество() > 0 Тогда
ПерваяСтрока = Список.Получить(0);
ЗначениеИтога = ПерваяСтрока.ИтогПоСуммеСНДС;
// Передать значение на клиент для отображения
// Например, в реквизит формы:
ЗначениеВРеквизитФормы("ОбщийИтогСуммаСНДС", ЗначениеИтога);
Иначе
ЗначениеВРеквизитФормы("ОбщийИтогСуммаСНДС", 0);
КонецЕсли;
КонецПроцедуры
// На клиенте для обновления подвала
&НаКлиенте
Процедура ОбновитьПодвал()
// Предполагаем, что у вас есть реквизит формы ОбщийИтогСуммаСНДС
// и элемент подвала 'ТоварыСуммаСНДСПодвал'
Элементы.Товары.ПодчиненныеЭлементы.ТоварыСуммаСНДСПодвал.Заголовок = Формат(ОбщийИтогСуммаСНДС, "ЧДЦ=2");
КонецПроцедуры
Не забываем вызывать ОбновитьПодвал() после каждого обновления списка или в ПриОтображении формы.
Для наиболее сложных случаев, когда логика расчета итогов является нетривиальной, включает в себя несколько уровней агрегации, использование сложных группировок, или когда расчетные колонки требуют выполнения произвольного алгоритма, который не может быть выражен в простом запросе, мы можем применить Систему Компоновки Данных (СКД). СКД — это гибкий и мощный инструмент 1С, предназначенный для построения отчетов, но его можно успешно использовать и для программной обработки данных "на лету", без формирования полноценного отчета для пользователя.
Алгоритм использования СКД для расчета итогов:
ТаблицаЗначений. Если расчетные колонки требуют сложных вычислений, эти вычисления выполняются для каждой строки до передачи данных в СКД, и результат записывается в дополнительные колонки ТаблицыЗначений.СхемаКомпоновкиДанных. В качестве источника данных для этой схемы указывается наша ТаблицаЗначений.СУММА, СРЕДНЕЕ, МИНИМУМ, МАКСИМУМ, КОЛИЧЕСТВО и т.д.) необходимо рассчитать итоги. Именно здесь мы определяем, как будет рассчитываться итоговая сумма для нашей расчетной колонки.ТаблицеЗначений.ТаблицуЗначений, применяет отборы, вычисляет поля и ресурсы, формируя результат.Пример программного использования СКД (упрощенно):
&НаСервере
Функция РассчитатьИтогиСКП(ТабличнаяЧастьДокумента, ЭлементыТабличногоПоля)
// 1. Подготовка данных: выгружаем табличную часть в ТаблицуЗначений
ТаблицаДанных = ТабличнаяЧастьДокумента.Выгрузить();
// Добавляем расчетную колонку, если ее нет, и заполняем ее значениями
// Это критично, если СуммаСНДС - чисто расчетное поле, не реквизит ТЧ
Если НЕ ТаблицаДанных.Колонки.Содержит("СуммаСНДС") Тогда
ТаблицаДанных.Колонки.Добавить("СуммаСНДС", Новый ОписаниеТипов("Число"));
КонецЕсли;
Для Каждого СтрокаТД Из ТаблицаДанных Цикл
// Производим расчет для каждой строки. Здесь может быть сложная логика.
СтрокаТД.СуммаСНДС = СтрокаТД.Сумма * (1 + СтрокаТД.ПроцентНДС / 100);
КонецЦикла;
// 2. Создание схемы компоновки данных
СхемаКомпоновкиДанных = Новый СхемаКомпоновкиДанных;
// Создаем набор данных - объект
НаборДанныхОбъект = СхемаКомпоновкиДанных.НаборыДанных.Добавить(Тип("НаборДанныхОбъект"));
НаборДанныхОбъект.Имя = "ДанныеТЧ";
НаборДанныхОбъект.ИмяОбъекта = "ТаблицаДанных"; // Имя объекта, который мы передадим
// Добавляем поля в набор данных
Для Каждого КолонкаТД Из ТаблицаДанных.Колонки Цикл
Поле = НаборДанныхОбъект.Поля.Добавить(Тип("ПолеНабораДанных"));
Поле.Поле = КолонкаТД.Имя;
Поле.ПутьКДанным = КолонкаТД.Имя;
КонецЦикла;
// Добавляем поле-ресурс для расчета суммы
РесурсСуммаСНДС = СхемаКомпоновкиДанных.Ресурсы.Добавить();
РесурсСуммаСНДС.Поле = Новый ПолеКомпоновкиДанных("СуммаСНДС");
РесурсСуммаСНДС.Выражение = "Сумма(СуммаСНДС)";
// 3. Настройка компоновщика настроек
НастройкиКомпоновщика = СхемаКомпоновкиДанных.НастройкиПоУмолчанию;
// Добавляем группировку для расчета общего итога
// Если нам нужен только общий итог, группировка не обязательна, достаточно ресурсов
// Но для более сложной структуры можно добавить группировку по пустым полям
// НастройкиКомпоновщика.Структура.Добавить(Тип("ГруппировкаКомпоновкиДанных"));
// 4. Преобразование отборов табличного поля в отборы СКД
// Этот шаг более сложный и требует написания функции-конвертера
// Для простоты здесь мы предполагаем, что отбор применяется к исходной ТаблицеДанных
// или можем использовать метод "ПроверитьСтроку" до компоновки, как в первом варианте.
// Если же отборы применяются именно к СКД, нам нужно разобрать ЭлементыТабличногоПоля.ОтборСтрок
// и для каждого ЭлементОтбора создать соответствующий ОтборКомпоновкиДанных.
// Пример Отбора для СКД (если бы мы его строили вручную)
// ОтборСКД = НастройкиКомпоновщика.Отбор;
// ЭлементОтбора = ОтборСКД.Элементы.Добавить(Тип("ЭлементОтбораКомпоновкиДанных"));
// ЭлементОтбора.ЛевоеЗначение = Новый ПолеКомпоновкиДанных("Номенклатура");
// ЭлементОтбора.ВидСравнения = ВидСравненияКомпоновкиДанных.Равно;
// ЭлементОтбора.ПравоеЗначение = Справочники.Номенклатура.НайтиПоКоду("00001");
// ЭлементОтбора.Использование = Истина;
// 5. Компоновка данных
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, НастройкиКомпоновщика);
ПроцессорКомпоновки = Новый ПроцессорКомпоновкиДанных;
ПроцессорКомпоновки.Инициализировать(МакетКомпоновки, , , Истина); // Истина - для получения ресурсов без детальных записей
// Передаем нашу таблицу значений в качестве внешних данных для набора данных объект
ВнешниеНаборыДанных = Новый Структура("ТаблицаДанных", ТаблицаДанных);
ПроцессорКомпоновки.УстановитьВнешниеНаборыДанных(ВнешниеНаборыДанных);
// 6. Извлечение итоговых значений
ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений;
РезультатКомпоновки = Новый ТаблицаЗначений;
ПроцессорВывода.УстановитьОбъект(РезультатКомпоновки);
ПроцессорВывода.НачатьВывод();
ЭлементРезультата = ПроцессорКомпоновки.Следующий();
Если ЭлементРезультата.Тип = Тип("РезультатКомпоновкиДанныхРасшифровка") Тогда
// Обычно итоги находятся в первой строке группировки верхнего уровня или в общем итоге
Если РезультатКомпоновки.Количество() > 0 Тогда
Возврат РезультатКомпоновки[0].СуммаСНДС; // Наше итоговое значение
КонецЕсли;
КонецЕсли;
Возврат 0; // Если итогов нет
КонецФункции
Когда использовать СКД?
Метод СКД является наиболее универсальным и мощным, но и самым сложным в реализации. Он требует глубокого понимания принципов работы СКД и её программного интерфейса. Однако, один раз реализованный, он предоставляет исключительную гибкость.
Мы рассмотрели три основных подхода к расчету итогов по расчетным колонкам табличных частей документа с учетом пользовательских фильтров в 1С:Предприятие 8.3. Выбор конкретного метода зависит от версии платформы, сложности расчетов, объема данных и требований к производительности:
ПроверитьСтроку() является наиболее простым и эффективным для обычных форм и управляемых форм, где табличное поле непосредственно связано с табличной частью объекта и объем данных не слишком велик. Он позволяет быстро и корректно учесть все интерактивные фильтры.Внимательно анализируйте свою задачу и выбирайте тот подход, который наилучшим образом соответствует вашим требованиям и возможностям платформы. В большинстве случаев для решения описанной проблемы будет достаточно метода ПроверитьСтроку(), как наиболее сбалансированного по сложности реализации и функциональности.