При создании аналитических отчетов в системе компоновки данных (СКД) разработчики часто сталкиваются с задачей вывода показателей в динамических колонках. Типичный пример — необходимость проанализировать продажи клиентам в разрезе их последовательных сделок: вывести в первой колонке показатели первого контакта (первой продажи), во второй колонке — второй продажи, в третьей — третьей и так далее.
Если мы попытаемся сделать это напрямую, выбрав в качестве группировки колонок поле ссылки на документ (например, ДокументРеализации), то получим на выходе так называемую «шахматку». Поскольку каждый документ уникален, для каждого клиента будет создана своя собственная колонка. В результате отчет растянется на тысячи столбцов, большая часть ячеек которых будет пустой. Наша задача — сгруппировать данные так, чтобы показатели распределялись строго по колонкам «Продажа №1», «Продажа №2», «Продажа №3», независимо от того, какими именно документами они были оформлены.
В этой статье мы подробно разберем, почему стандартные средства оформления СКД здесь не помогут, и рассмотрим три эффективных способа решения этой задачи на уровне подготовки данных.
Первое, что приходит на ум начинающему разработчику, — использовать системное поле СКД НомерПоПорядкуВГруппировке. Однако этот подход заведомо ошибочен. Давайте проанализируем причину.
Системные поля СКД (такие как номер по порядку, уровень и др.) рассчитываются на самом последнем этапе компоновки — во время вывода данных в итоговый табличный документ. На этапе построения группировок компоновщик еще «не знает», какие номера получат записи. Поля группировки колонок должны быть определены на уровне самого набора данных (в запросе или таблице значений). Использовать расчетное системное поле для создания группировки колонок физически невозможно, так как это нарушает логику жизненного цикла формирования отчета СКД.
Следовательно, порядковый номер продажи в разрезе каждого контрагента должен быть рассчитан заранее и передан в СКД в качестве готового числового поля.
Этот способ является классическим и базируется на механизме связи таблицы самой с собой по условию сравнения дат или моментов времени. Мы сопоставим каждую продажу контрагента со всеми его продажами, которые произошли ранее или одновременно с ней.
Рассмотрим по шагам, как реализовать этот алгоритм в тексте запроса схемы компоновки данных:
Посмотрим на пример реализации такого запроса:
ВЫБРАТЬ
РеализацияТоваровУслуг.Контрагент КАК Контрагент,
РеализацияТоваровУслуг.Ссылка КАК Ссылка,
РеализацияТоваровУслуг.МоментВремени КАК МоментВремени,
РеализацияТоваровУслуг.СуммаДокумента КАК Сумма
ПОМЕСТИТЬ ВТ_Продажи
ИЗ
Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг
ГДЕ
РеализацияТоваровУслуг.Проведен = ИСТИНА
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Т1.Контрагент КАК Контрагент,
Т1.Ссылка КАК Ссылка,
Т1.Сумма КАК Сумма,
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Т2.Ссылка) КАК ПорядковыйНомер
ИЗ
ВТ_Продажи КАК Т1
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Продажи КАК Т2
ПО Т1.Контрагент = Т2.Контрагент
И Т1.МоментВремени >= Т2.МоментВремени
ГРУППИРОВАТЬ ПО
Т1.Контрагент,
Т1.Ссылка,
Т1.Сумма
Преимущества метода: Работает на любых версиях платформы 1С:Предприятие 8, не требует написания программного кода на встроенном языке, полностью декларативен.
Недостатки метода: Крайне низкая производительность на больших базах данных. Так как используется соединение «каждый с каждым» в рамках одного контрагента, сложность алгоритма возрастает квадратично ($O(N^2)$). Если у одного контрагента будет несколько тысяч продаж, отчет может вызвать перегрузку процессора или аварийное завершение работы СУБД.
Если вы работаете на современной версии платформы (начиная с 8.3), то можете использовать мощный инструмент оптимизации — функцию АВТОНОМЕРЗАПИСИ() во временных таблицах. Этот способ позволяет пронумеровать записи за линейное время ($O(N)$), избегая тяжелых соединений.
Давайте разберем этот продвинутый алгоритм по шагам:
Контрагент и МоментВремени. Это критически важно: документы одного клиента должны идти строго друг за другом в хронологическом порядке. Результат помещаем во временную таблицу ВТ_Исходная.ВТ_Исходная в новую таблицу ВТ_С_Номерами и добавляем поле с функцией АВТОНОМЕРЗАПИСИ() КАК ГлобальныйНомер. Поскольку исходная таблица была упорядочена, документы получат сквозную нумерацию, сохраняя хронологию по каждому контрагенту.ВТ_Минимумы. В ней группируем данные по контрагенту и находим минимальный глобальный номер для каждого из них: МИНИМУМ(ГлобальныйНомер) КАК МинимальныйНомерВГруппе.ВТ_С_Номерами и ВТ_Минимумы по полю Контрагент. Порядковый номер продажи внутри группы каждого контрагента вычисляем по простой математической формуле: ГлобальныйНомер - МинимальныйНомерВГруппе + 1.Проанализируем пример практической реализации данного запроса:
ВЫБРАТЬ
Реализация.Контрагент КАК Контрагент,
Реализация.Ссылка КАК Ссылка,
Реализация.СуммаДокумента КАК Сумма
ПОМЕСТИТЬ ВТ_Исходная
ИЗ
Документ.РеализацияТоваровУслуг КАК Реализация
ГДЕ
Реализация.Проведен = ИСТИНА
УПОРЯДОЧИТЬ ПО
Контрагент,
Реализация.МоментВремени
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
АВТОНОМЕРЗАПИСИ() КАК ГлобальныйНомер,
Т_Исходная.Контрагент КАК Контрагент,
Т_Исходная.Ссылка КАК Ссылка,
Т_Исходная.Сумма КАК Сумма
ПОМЕСТИТЬ ВТ_С_Номерами
ИЗ
ВТ_Исходная КАК Т_Исходная
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Т_Номера.Контрагент КАК Контрагент,
МИНИМУМ(Т_Номера.ГлобальныйНомер) КАК МинимальныйНомерВГруппе
ПОМЕСТИТЬ ВТ_Минимумы
ИЗ
ВТ_С_Номерами КАК Т_Номера
ГРУППИРОВАТЬ ПО
Т_Номера.Контрагент
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Т_Номера.Контрагент КАК Контрагент,
Т_Номера.Ссылка КАК Ссылка,
Т_Номера.Сумма КАК Сумма,
Т_Номера.ГлобальныйНомер - Т_Минимумы.МинимальныйНомерВГруппе + 1 КАК ПорядковыйНомер
ИЗ
ВТ_С_Номерами КАК Т_Номера
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Минимумы КАК Т_Минимумы
ПО Т_Номера.Контрагент = Т_Минимумы.Контрагент
Преимущества метода: Потрясающая скорость работы на больших объемах данных. Время выполнения запроса растет линейно, что позволяет без проблем обрабатывать миллионы документов продаж.
Иногда логика определения «порядкового номера контакта» бывает слишком сложной для описания на языке запросов (например, когда нужно учитывать не только реализации, но и входящие платежи, закрытия заказов или специфические бизнес-события). В этом случае наиболее надежным и простым в отладке решением будет использование внешнего набора данных СКД.
Выясним причину эффективности этого метода. Нам гораздо проще обойти выборку записей в обычном цикле на встроенном языке 1С и последовательно увеличивать счетчик, нежели пытаться построить громоздкие запросы.
Выполним следующие шаги для реализации программного метода:
НаборДанныхОбъект и добавим в него поля: Контрагент (тип Ссылка), Ссылка (тип Ссылка), Сумма (тип Число) и ПорядковыйНомер (тип Число).Посмотрим на пример программного кода в модуле отчета:
Процедура ПриКомпоновкеРезультата(ДокументРезультат, ДанныеРасшифровки, СтандартнаяОбработка)
СтандартнаяОбработка = Ложь;
// Шаг 1. Получение данных из базы
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Реализация.Контрагент КАК Контрагент,
| Реализация.Ссылка КАК Ссылка,
| Реализация.СуммаДокумента КАК Сумма
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Реализация
|ГДЕ
| Реализация.Проведен = ИСТИНА
|УПОРЯДОЧИТЬ ПО
| Контрагент,
| Реализация.МоментВремени";
ТаблицаДанных = Запрос.Выполнить().Выгрузить();
// Шаг 2. Добавление колонки для порядкового номера
ТаблицаДанных.Колонки.Добавить("ПорядковыйНомер", Новый ОписаниеТипов("Число"));
// Шаг 3. Заполнение порядковых номеров в цикле
ТекущийКонтрагент = Неопределено;
СчетчикСделок = 0;
Для Каждого Строка Из ТаблицаДанных Цикл
Если Строка.Контрагент <> ТекущийКонтрагент Тогда
ТекущийКонтрагент = Строка.Контрагент;
СчетчикСделок = 1;
Иначе
СчетчикСделок = СчетчикСделок + 1;
КонецЕсли;
Строка.ПорядковыйНомер = СчетчикСделок;
КонецЦикла;
// Шаг 4. Передача внешней таблицы в СКД
ВнешниеНаборыДанных = Новый Структура;
ВнешниеНаборыДанных.Вставить("НаборДанныхОбъект", ТаблицаДанных);
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, КомпоновщикНастроек.ПолучитьНастройки(), ДанныеРасшифровки);
ПроцессорКомпоновки = Новый ПроцессорКомпоновкиДанных;
ПроцессорКомпоновки.Инициализировать(МакетКомпоновки, ВнешниеНаборыДанных, ДанныеРасшифровки, Истина);
ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВТабличныйДокумент;
ПроцессорВывода.УстановитьДокумент(ДокументРезультат);
ПроцессорВывода.Вывести(ПроцессорКомпоновки);
КонецПроцедуры
После того как мы получили поле ПорядковыйНомер одним из описанных способов, нам необходимо правильно настроить саму схему компоновки данных, чтобы отчет выглядел презентабельно.
Выполним настройку структуры отчета:
Контрагент.ПорядковыйНомер.Сумма или количество проданного товара).Если мы оставим все как есть, в шапке колонок пользователь увидит простые числа: «1», «2», «3» и так далее. Это выглядит не слишком информативно. Чтобы исправить это и придать отчету профессиональный вид, воспользуемся механизмом выражений представления:
ПорядковыйНомер."Сделка № " + Формат(ПорядковыйНомер, "ЧГ=0")Теперь система автоматически подставит красивое текстовое представление в шапку каждой колонки, и пользователь увидит надписи «Сделка № 1», «Сделка № 2» вместо безликих цифр.
Важная рекомендация по оптимизации интерфейса: Если у ваших постоянных клиентов количество сделок исчисляется сотнями, то отчет снова может разрастись в ширину. Чтобы избежать этого, рекомендуется ограничить вывод колонок, например, показывать только первые 10 сделок. Для этого в настройках отчета (на вкладке «Отбор» для группировки колонок) установите простое условие: ПорядковыйНомер Меньше или равно 10. Все последующие продажи будут аккуратно скрыты, а отчет останется компактным и удобным для чтения.
Мы успешно решили задачу группировки колонок СКД по порядковому номеру события! Использование этих подходов позволит вам создавать быстрые, интерактивные и визуально понятные аналитические отчеты в среде 1С.