В практике разработки на платформе «1С:Предприятие» стандартным и наиболее эффективным способом получения сгруппированных данных является использование запросов или системы компоновки данных (СКД). Однако возникают задачи, когда необходимо выполнить группировку (агрегацию) данных непосредственно в оперативной памяти «на лету», используя только встроенный язык. Например, при обработке небольших объемов данных из выборки документов или при работе в простых локальных обработках.
В рамках этой статьи мы детально разберем, как реализовать группировку номенклатуры по количеству и сумме без использования механизмов СУБД, рассмотрим типичные ошибки начинающих разработчиков и покажем сопутствующие приемы фильтрации документов по периодам и статусу проведения.
Для того чтобы сгруппировать данные по номенклатуре, сначала необходимо собрать их в один массив. Лучшим объектом для этой задачи в памяти является ТаблицаЗначений. Мы создадим таблицу, опишем ее структуру и заполним строками из документов.
Важная деталь: Часто разработчики совершают ошибку, пытаясь добавить в таблицу значений колонки со сложными путями (например, через точку). Код вида ТаблицаТоваров.Колонки.Добавить("Ссылка.Дата") вызовет ошибку выполнения «Неверное имя колонки». Имя колонки должно быть простым идентификатором, а значение (например, дату документа) мы будем присваивать непосредственно при обходе выборки.
Рассмотрим пример правильного заполнения таблицы значений на основе выборки документов:
ТаблицаТоваров = Новый ТаблицаЗначений;
ТаблицаТоваров.Колонки.Добавить("Номенклатура");
ТаблицаТоваров.Колонки.Добавить("Количество");
ТаблицаТоваров.Колонки.Добавить("Сумма");
ТаблицаТоваров.Колонки.Добавить("Дата");
// Получаем выборку документов за определенный период
Выборка = Документы.РеализацияТоваровУслуг.Выбрать(Отчет.ПериодНачало, Отчет.ПериодКонец);
Пока Выборка.Следующий() Цикл
// Фильтруем документы: обрабатываем только проведенные
Если Не Выборка.Проведен Тогда
Продолжить;
КонецЕсли;
// Переносим строки табличной части во временную таблицу
Для Каждого Стр Из Выборка.Товары Цикл
НоваяСтрока = ТаблицаТоваров.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, Стр);
НоваяСтрока.Дата = Выборка.Дата; // Заполняем дату из шапки документа
КонецЦикла;
КонецЦикла;
После того как все данные собраны в ТаблицаЗначений, мы можем выполнить их свертку. Для этого у таблицы значений существует стандартный и очень быстрый метод Свернуть(). Он принимает два строковых параметра:
Проанализируем ситуацию с расчетом цены. Нельзя добавлять колонку «Цена» в список суммируемых колонок. При объединении одинаковых товаров их цены просто сложатся, что приведет к некорректным данным. Правильный подход — рассчитывать цену как средневзвешенную (Сумма / Количество) уже после выполнения свертки при выводе данных в отчет.
Посмотрим на пример выполнения свертки и последующего вывода данных в табличный документ:
// Сворачиваем таблицу по номенклатуре, суммируя количество и сумму
ТаблицаТоваров.Свернуть("Номенклатура", "Количество, Сумма");
// Рекомендуется отсортировать результат для удобства чтения отчета
ТаблицаТоваров.Сортировать("Номенклатура Возр");
НомерСтроки = 0;
СуммаВсего = 0;
Для Каждого СтрокаВыборки Из ТаблицаТоваров Цикл
НомерСтроки = НомерСтроки + 1;
ОбластьСтрока.Параметры.НомерСтроки = НомерСтроки;
ОбластьСтрока.Параметры.Товар = СтрокаВыборки.Номенклатура;
ОбластьСтрока.Параметры.Количество = СтрокаВыборки.Количество;
// Рассчитываем цену с проверкой деления на ноль
Если СтрокаВыборки.Количество <> 0 Тогда
ОбластьСтрока.Параметры.Цена = СтрокаВыборки.Сумма / СтрокаВыборки.Количество;
Иначе
ОбластьСтрока.Параметры.Цена = 0;
КонецЕсли;
ОбластьСтрока.Параметры.Сумма = СтрокаВыборки.Сумма;
СуммаВсего = СуммаВсего + СтрокаВыборки.Сумма;
ТабДок.Вывести(ОбластьСтрока);
КонецЦикла;
При интерактивной работе с отчетом пользователи часто хотят получать данные без ограничения периода или указывать только одну из границ. Кроме того, важно правильно обрабатывать время документов, попадающих на самый конец суток (в секунду 23:59:59).
Если использовать простую передачу параметров, документы на границе периода могут не попасть в выборку. Чтобы этого избежать, используем функцию КонецДня() для конечной даты. Также применим тернарный оператор (сокращенное условие ?()), чтобы корректно отрабатывать ситуацию, когда пользователь оставил дату окончания пустой:
ДатаНачала = Отчет.ПериодНачало;
// Если дата окончания не задана, передаем пустую дату, иначе берем самый конец дня
ДатаКонца = ?(ЗначениеЗаполнено(Отчет.PeriodКонец), КонецДня(Отчет.ПериодКонец), Отчет.ПериодКонец);
Выборка = Документы.РеализацияТоваровУслуг.Выбрать(ДатаНачала, ДатаКонца);
Для визуального оформления отчета мы можем проверять, заполнен ли период в форме, и выводить текстовое представление интервала только тогда, когда параметры действительно заданы пользователем:
Если ЗначениеЗаполнено(Отчет.ПериодНачало) Или ЗначениеЗаполнено(Отчет.ПериодКонец) Тогда
ОбластьШапка.Параметры.ПериодОтчета = "Период с " + Формат(Отчет.ПериодНачало, "ДФ=dd.MM.yyyy") + " по " + Формат(Отчет.ПериодКонец, "ДФ=dd.MM.yyyy");
Иначе
ОбластьШапка.Параметры.ПериодОтчета = "За весь период";
КонецЕсли;
Хотя метод Свернуть() для ТаблицыЗначений очень прост в реализации, у него есть архитектурный недостаток: мы вынуждены сначала загрузить абсолютно все строки из всех документов в память (что создает дубли и увеличивает расход ОЗУ), и только потом выполнять свертку.
Если обрабатывается большое количество документов, более производительным решением будет использование объекта Соответствие. В этом случае мы агрегируем данные прямо во время обхода выборки, не создавая промежуточных дублирующихся строк:
// Ключ соответствия — Номенклатура, Значение — Структура с показателями
ДанныеГруппировки = Новый Соответствие;
Пока Выборка.Следующий() Цикл
Если Не Выборка.Проведен Тогда
Продолжить;
КонецЕсли;
Для Каждого Стр Из Выборка.Товары Цикл
НайденныеПоказатели = ДанныеГруппировки.Получить(Стр.Номенклатура);
Если НайденныеПоказатели = Неопределено Тогда
// Товар встретился впервые, инициализируем структуру
Показатели = Новый Структура("Количество, Сумма", Стр.Количество, Стр.Сумма);
ДанныеГруппировки.Вставить(Стр.Номенклатура, Показатели);
Else
// Товар уже есть, увеличиваем показатели
НайденныеПоказатели.Количество = НайденныеПоказатели.Количество + Стр.Количество;
НайденныеПоказатели.Сумма = НайденныеПоказатели.Сумма + Стр.Сумма;
КонецЕсли;
КонецЦикла;
КонецЦикла;
Этот способ работает быстрее при больших объемах данных и потребляет значительно меньше оперативной памяти компьютера, так как коллекция уникальных элементов всегда лаконична.
Необходимо помнить, что обход документов через метод Выбрать() и ручная группировка в памяти — это тяжелая операция для СУБД и сервера приложений 1С. Метод Выбрать() извлекает из базы данных объекты документов целиком (все реквизиты, все табличные части, движения), даже если вам нужны только три поля.
Используйте данный подход взвешенно: он отлично подходит для локальных обработок, заполнения табличных частей документов «на лету» или для работы с небольшими массивами данных. Для построения тяжелых отчетов за длительные периоды всегда рекомендуется использовать запросы с группировкой на уровне СУБД (оператор СГРУППИРОВАТЬ ПО) или СКД.