gifts2017

Кросс-таблица из табличной части или запроса

Опубликовал Пишу код как картины (yurii_host) в раздел Программирование - Практика программирования

Данная задача встречалась мне несколько раз. Каждый раз в новой интерпретации, но суть примерно одинаковая: "Хочу, чтобы по строкам были товары, по колонкам склады, а на пересечении - количество". Вместо товары-склады, может быть что угодно: дата-контрагент, номенклатура+цвет - размер и т.д.

Для примера возьмем некоторый документ с табличной частью Остатки на конец смены. Структура полей табличной части представлена на рисунке справа.

Заказчику неудобно работать с ней в таком виде, и он желает просматривать ее и редактировать в виде кросс-таблицы, как на рисунке слева.
Замечание: на картинке приводится две таблицы одновременно для наглядности. В реальных задачах обычно требуется отображать только кросс-таблицу.

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

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

Процедура ТранспорироватьКроссТаблицуВТабличнуюЧасть()
	
	// Обратная трансформация
	ТабличнаяЧасть = ТрансформироватьКроссТаблицуВДанные(
					ОстаткиНаКонецСмены_КроссТаблица.Выгрузить(), 
					ОстаткиНаКонецСмены_Служебная.Выгрузить(), 
					"Количество");			// поле, в которое будут записываться значения
					              			// из пересечения вертикальных и горизонтальных группировок
	ОстаткиНаКонецСмены.Загрузить(ТабличнаяЧасть);
	
КонецПроцедуры

Небольшие пояснения:
1. Дополнительная информация для связи между исходной и конечной таблицами хранится в служебной таблице. Ее необходимо сохранять на протяжении всей работы с кросс-таблицей. Ее структура простая: Имя, Заголовок и Тип колонок, а также имена полей табличной части, по сочетанию которых будут создаваться колонки. В моем примере это одно поле “Стеллаж”, но их может быть и несколько, например “Номенклатура,Характеристика”. Для рассматриваемого примера структура служебной таблицы с данными представлена на рисунке

 

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

Сами процедуры
// Пусть нужно по некоторой таблице значений создать другую таблицу значений
// При этом колонки второй таблицы соответствуют сочетанию полей в первой таблице 
// (например, в первой таблице одна колонка Номенклатура, а во второй должна быть отдельная колонка для каждой номенклатуры)
// Тогда первую таблицу будем для краткости называть ДАННЫЕ, а вторрую КРОССТАБЛИЦА

// для решения этой задачи потребуется дополнительная таблица, которую будем назывть
// ИДЕНТИФИКАТОРЫКОЛОНОК

// Данный алгоритм выполняет преобразования 
//		- ДАННЫЕ (1)-> ИДЕНТИФИКАТОРЫКОЛОНОК (2)-> КРОССТАБЛИЦА
//		- КРОССТАБЛИЦА (3)-> ДАННЫЕ

Функция ПолучитьИдентификаторыДанных(тДанные, стрПоляВертикальныхГруппировок) Экспорт
	
	тИдентификаторыКолонок = тДанные.Скопировать(,стрПоляВертикальныхГруппировок);
	тИдентификаторыКолонок.Свернуть(стрПоляВертикальныхГруппировок);
	
	тИдентификаторыКолонок.Колонки.Добавить("Имя");
	тИдентификаторыКолонок.Колонки.Добавить("Заголовок");
	тИдентификаторыКолонок.Колонки.Добавить("Тип");
	
	Для сч = 1 по тИдентификаторыКолонок.Количество() Цикл
		тИдентификаторыКолонок[сч-1].Имя = "Группировка_" + Формат(сч, "ЧЦ=5; ЧВН=; ЧГ=0");
	КонецЦикла;
	
	Возврат тИдентификаторыКолонок;
	
КонецФункции

Функция ТрансформироватьДанныеВКроссТаблицу(тДанные, тИдентификаторыКолонок, стрГоризонтальныеГруппировки, ИмяКолонкиРесурс) Экспорт
	
	тКроссТаблица = тДанные.Скопировать(,стрГоризонтальныеГруппировки);
	тКроссТаблица.Свернуть(стрГоризонтальныеГруппировки);
	
	Для каждого Строка из тИдентификаторыКолонок Цикл
		тКроссТаблица.Колонки.Добавить(Строка.Имя, Строка.Тип, Строка.Заголовок);
	КонецЦикла;
	
	СтруктураПоискаВертикальныхГруппировок = Новый Структура;
	Для каждого Колонка из тИдентификаторыКолонок.Колонки Цикл
		Если Колонка.Имя = "Имя" Тогда
			Продолжить;
		ИначеЕсли Колонка.Имя = "Заголовок" Тогда
			Продолжить;
		ИначеЕсли Колонка.Имя = "Тип" Тогда
			Продолжить;
		КонецЕсли;
		
		СтруктураПоискаВертикальныхГруппировок.Вставить(Колонка.Имя);
		
	КонецЦикла;
	
	СтруктураПоискаГоризонтальныхГруппировок = Новый Структура(стрГоризонтальныеГруппировки);
	
	Для каждого Строка из тДанные Цикл
		
		ЗаполнитьЗначенияСвойств(СтруктураПоискаГоризонтальныхГруппировок, Строка);
		НайденныеСтроки = тКроссТаблица.НайтиСтроки(СтруктураПоискаГоризонтальныхГруппировок);
		Если НайденныеСтроки.Количество() Тогда
			СтрокаОтображения = НайденныеСтроки[0];
		Иначе
			СтрокаОтображения = тКроссТаблица.Добавить();
			ЗаполнитьЗначенияСвойств(СтрокаОтображения, СтруктураПоискаГоризонтальныхГруппировок);
		КонецЕсли;
		
		ЗаполнитьЗначенияСвойств(СтруктураПоискаВертикальныхГруппировок, Строка);
		НайденныеСтроки = тИдентификаторыКолонок.НайтиСтроки(СтруктураПоискаВертикальныхГруппировок);
		ИмяКолонкиОтображения = НайденныеСтроки[0].Имя; // найденное значение ВСЕГДА будет. Если нет, то это несоблюдение требований алгоритма. Выдаем программную ошибку
		
		СтрокаОтображения[ИмяКолонкиОтображения] = Строка[ИмяКолонкиРесурс];
		
	КонецЦикла;
	
	Возврат тКроссТаблица;
	
КонецФункции

Функция ТрансформироватьКроссТаблицуВДанные(тКроссТаблица, тИдентификаторыКолонок, ИмяКолонкиРесурс) Экспорт
	
	тДанные = Новый ТаблицаЗначений;
	тДанные.Колонки.Добавить(ИмяКолонкиРесурс);
	
	Для каждого Колонка из тКроссТаблица.Колонки Цикл
		Если Найти(Колонка.Имя, "Группировка_") = 1 Тогда
			Продолжить;
		КонецЕсли;
		
		тДанные.Колонки.Добавить(Колонка.Имя, Колонка.ТипЗначения);
	КонецЦикла;
	
	Для каждого Колонка из тИдентификаторыКолонок.Колонки Цикл
		Если Колонка.Имя = "Имя" Тогда
			Продолжить;
		ИначеЕсли Колонка.Имя = "Заголовок" Тогда
			Продолжить;
		КонецЕсли;
		
		тДанные.Колонки.Добавить(Колонка.Имя);
	КонецЦикла;
	
	Для каждого КолонкаОтображения из тКроссТаблица.Колонки Цикл
		Если НЕ Найти(КолонкаОтображения.Имя, "Группировка_") = 1 Тогда
			Продолжить;
		КонецЕсли;
		
		СтрокаСоответствияКолонок = тИдентификаторыКолонок.Найти(КолонкаОтображения.Имя, "Имя"); // Строка должна быть найдена ВСЕГДА. Иначе это несоблюдение требований алгоритма - будет выдана программная ошибка
		
		Для каждого СтрокаОтображения из тКроссТаблица Цикл
			
			Значение = СтрокаОтображения[КолонкаОтображения.Имя];
			Если НЕ ЗначениеЗаполнено(Значение) Тогда
				Продолжить;
			КонецЕсли;
			
			НоваяСтрока = тДанные.Добавить();
			ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаОтображения);
			ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаСоответствияКолонок);
			НоваяСтрока[ИмяКолонкиРесурс] = Значение;
			
		КонецЦикла;
		
	КонецЦикла;
	
	Возврат тДанные;
	
КонецФункции

Отмечу также, что данные методы можно использовать не только для разворачивания табличной части, но и таблицы из запроса (например, в АРМе)

Процедуры тестировались на платформе 8.3 в режиме совместимости с 8.2. Однако скорее всего будут работать и на платформе 8.1

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

Скачать файлы

Наименование Файл Версия Размер
Пример Кросс-таблица (ОФ, УФ, 8.3) 9
.epf 20,23Kb
09.09.16
9
.epf 20,23Kb Скачать

См. также

Подписаться Добавить вознаграждение
Комментарии
1. Василий Воробьев (Hatson) 19.09.16 11:43
Хм, ну надо же))
А через неделю я опубликовал свою статью про СКД.

http://infostart.ru/public/549297/

Задам провокационный вопрос:
А что делать если в одной кросс-таблице нужно свести данные из нескольких реальных таблиц (табличных частей) с разной детализацией?
2. Пишу код как картины (yurii_host) 19.09.16 23:30
(1) Hatson, оба способа имеют право на существование. Все зависит от предпочтений программиста и требований клиента.
Я изначально тоже хотел сделать через СКД, когда у меня встала такая задача. Но ключевому пользователю показалось неудобно редактировать в табличном документе. В итоге родился данный способ, который был уже применен несколько раз мной и коллегами.

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

В схеме с СКД экономятся силы программиста на выводе и тратятся на редактировании. В схеме с табличным полем - наоборот. Все равно думать надо и в том и другом случае.
3. topasha (topasha) 13.10.16 00:45
Вот спасибо, хороший человек! Кучу времени мне сэкономил.