Предыстория, которой не было, но которая вполне могла случиться
Очень Важный Заказчик (ОВЗ) поставил задачу: сделать отчет по начислениям и удержаниям сотрудников с группировками по организациям и подразделениям в разрезе кварталов.
Отчет должен быть реализован на основе вот такого регистра:
Что может быть проще? Создаем схему компоновки:
и с чувством выполненного долга показываем результат ОВЗ:
Но к нашему удивлению...
ОВЗ: Что это за $&^*%$% ?!. У меня раньше стояла 7.7, и там мои программисты за полчаса-час сделали такую форму:
ОВЗ: нужна такая же.
Мы: но у нас СКД - современный стандарт построения отчетов нового поколения...
ОВЗ: Да? почему же получилась такая $&^*%$% ?
Мы: Ну как же, смотрите - вот отборы, вот структура, варианты и даже условное оформление!
ОВЗ: $#%! *&$%! &*$!
ОВЗ: На %#& мне все это надо?
ОВЗ: Мне надо завтра на стол Генеральному положить отчет, а ваш даже на страницу не помещается!
ОВЗ: Так, сроку вам до вечера. Напомнить, сколько мы вам платим? Все, свободны.
Итак, получив такой мотивирующий пинок импульс, команда приступила к работе.
Тимлид: какие у нас есть варианты?
Разработчик1: Может отказаться от СКД? Сделаем через запрос с итогами и выборкой с группировками.
Тимлид: Как крайний вариант подойдет, но не хотелось бы отказываться от СКД, надо же ОВЗ как-то приучать к новым технологиям...
Разработчик2: Можно результат компоновки выгрузить в ТЗ или в ДЗ, и потом вывести в документ.
Тимлид: Можно, но таблицу в структуре отчета так выгрузить нельзя. А без неё сложно - надо как-то решать вопрос с разреженностью данных в колонках. Что там говорит коллективный разум Инфостарта?
Оставим эту команду, и посмотрим, какие у нас есть вообще способы кастомизировать отчеты?
Способы кастомизации отчетов:
- Можно посмотреть в сторону макетов полей и группировок СКД. При их использовании может потребоваться ввести новые элементы в структуру отчета или добавить в набор данных дополнительные строки (изменив текст запроса) или дополнительные поля, например для итогов, а также сделать дополнительные настройки для каждого элемента структуры. Примеры.
- Можно выполнить постобработку полученного табличного документа - в цикле перебрать строки/ячейки, при необходимости их добавить/удалить/объединить/оформить.
- Можно на лету менять макеты в объекте МакетКомпоновкиДанных или ЭлементРезультатаКомпоновкиДанных. Очень хорошо такой подход раскрыт в этой статье.
- Можно применять комбинацию этих способов.
Но есть и другой путь
Как у нас обычно происходит программное формирование отчета?
Процедура ПриКомпоновкеРезультата(ДокументРезультат, ДанныеРасшифровки, СтандартнаяОбработка)
Настройки = КомпоновщикНастроек.ПолучитьНастройки();
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, Настройки, ДанныеРасшифровки ,, Тип("ГенераторМакетаКомпоновкиДанных"));
ПроцессорКомпоновки = Новый ПроцессорКомпоновкиДанных;
ПроцессорКомпоновки.Инициализировать(МакетКомпоновки,, ДанныеРасшифровки);
ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВТабличныйДокумент;
ПроцессорВывода.УстановитьДокумент(ДокументРезультат);
// 1 вариант
ПроцессорВывода.Вывести(ПроцессорКомпоновки);
// 2 вариант
ПроцессорВывода.НачатьВывод();
ЭлементРезультата = ПроцессорКомпоновки.Следующий();
Пока ЭлементРезультата <> Неопределено Цикл
ПроцессорВывода.ВывестиЭлемент(ЭлементРезультата);
ЭлементРезультата = ПроцессорКомпоновки.Следующий();
КонецЦикла;
ПроцессорВывода.ЗакончитьВывод();
КонецПроцедуры
И вот во втором варианте, внутри цикла вывода элемента результата компоновки, мы можем использовать не стандартный вывод через ПроцессорВывода, а старый добрый вывод в табличный документ на основе макета с именованными секциями.
ЭлементРезультата = ПроцессорКомпоновки.Следующий();
Пока ЭлементРезультата <> Неопределено Цикл
Секция = МакетОтчета.ПолучитьОбласть(ИмяСекции);
Секция.Параметры.Заполнить(ЗначенияПараметров);
// 1
ДокументРезультат.Вывести(Секция);
// или 2
ДокументРезультат.Присоединить(Секция);
ЭлементРезультата = ПроцессорКомпоновки.Следующий();
КонецЦикла;
Дело за малым - определить, какие секции в какой момент выводить и какими значениями заполнять их параметры.
Чем нам здесь может помочь ЭлементРезультата?
Макет содержит внутреннее имя макета, выводимого в табличный документ, с его помощью мы можем определить ту часть отчета, которая в данный момент выводится. ПроцентВывода нас пока не интересует, а ТипЭлемента и РасположениеВложенныхЭлементов пригодятся - с их помощью определим, надо ли секцию выводить с новой строки, или же присоединить к текущей. ЗначенияПараметров, как несложно догадаться, содержит коллекцию параметров для вывода элемента отчета:
В нашем случае "П1" - сумма по конкретному сотруднику за определенный период, "П2" - ИдентификаторРасшифровкиКомпоновкиДанных.
Код при этом может выглядеть примерно так:
Если ЭлементРезультата.Макет = "Макет38" Тогда
Секция = МакетОтчета.ПолучитьОбласть("Сотрудник|Период");
Секция.Параметры.Сумма = ЗначениеПараметра(ЭлементРезультата, "П1");
Секция.Параметры.Расшифровка = ЗначениеПараметра(ЭлементРезультата, "П2");
ДокументРезультат.Присоединить(Секция);
КонецЕсли;
где функция ЗначениеПараметра:
Функция ЗначениеПараметра(ЭлементРезультата, ИмяПараметра)
Результат = Неопределено;
Параметр = ЭлементРезультата.ЗначенияПараметров.Найти(ИмяПараметра);
Если ТипЗнч(Параметр) = Тип("ЗначениеПараметраМакетаКомпоновкиДанных") Тогда
Результат = Параметр.Значение;
КонецЕсли;
Возврат Результат;
КонецФункции
Чтобы не делать каскад условий для проверки имен макета, можно заранее определить список соответствий имен макетов СКД ("Макет38") и имен секций макета отчета ("Сотрудник|Период"). Заодно там же можно указать и признак вывода с новой строки. А чтобы не прописывать установку отдельных параметров секции макета, можно эти параметры заполнять из заранее созданной структуры (конечно же в этом случае параметры секций макета должны называться "П1", "П2" и т.д.).
Макеты = Новый Соответствие;
Макеты.Вставить("Макет38", Новый Структура("ИмяСекции,НоваяСтрока", "Сотрудник|Период", Ложь));
// ... добавляем другие соответствия
ЗначенияПараметров = Новый Структура;
ЭлементРезультата = ПроцессорКомпоновки.Следующий();
Пока ЭлементРезультата <> Неопределено Цикл
ДанныеМакета = Макеты.Получить(ЭлементРезультата.Макет);
Если ТипЗнч(ДанныеМакета) = Тип("Структура") Тогда
Секция = МакетОтчета.ПолучитьОбласть(ДанныеМакета.ИмяСекции);
Для каждого Параметр Из ЭлементРезультата.ЗначенияПараметров Цикл
ЗначенияПараметров.Вставить(Параметр.Имя, Параметр.Значение);
КонецЦикла;
Секция.Параметры.Заполнить(ЗначенияПараметров);
Если ДанныеМакета.НоваяСтрока Тогда
ДокументРезультат.Вывести(Секция);
Иначе
ДокументРезультат.Присоединить(Секция);
КонецЕсли;
КонецЕсли;
ЭлементРезультата = ПроцессорКомпоновки.Следующий();
КонецЦикла;
Вот тот минимальный объем кода, который сформирует нам табличный документ с отчетом, и который можно в дальнейшем дорабатывать и усложнять.
Значения расшифровки
При работе со значениями параметров макета компоновки данных выясняется одна неожиданная, но вполне объяснимая особенность. Если со значениями примитивных типов все нормально, то данные ссылочных типов хранятся в виде представления:
Для вывода в отчет этого вполне достаточно, а если нам надо как-то эти данные дополнительно обработать? Казалось бы, можно получить ссылку по представлению (найти элемент справочника по наименованию или коду), но к счастью есть другой, более цивилизованный способ. Дело в том, что необходимые нам данные хранятся в данных расшифровки, осталось их оттуда только достать. Ниже приведена функция, которая это делает:
Сотрудник = ЗначениеРасшифровки(ДанныеРасшифровки, ЭлементРезультата, "П2", "Сотрудник");
Функция ЗначениеРасшифровки(ДанныеРасшифровки, ЭлементРезультата, ИмяПараметра, ИмяПараметраРасшифровки)
Результат = Неопределено;
Параметр = ЭлементРезультата.ЗначенияПараметров.Найти(ИмяПараметра);
Если ТипЗнч(Параметр) = Тип("ЗначениеПараметраМакетаКомпоновкиДанных") Тогда
Идентификатор = Параметр.Значение;
Если ТипЗнч(Идентификатор) = Тип("ИдентификаторРасшифровкиКомпоновкиДанных") Тогда
ЭлементРасшифровки = ДанныеРасшифровки.Элементы.Получить(Идентификатор);
Если ТипЗнч(ЭлементРасшифровки) = Тип("ЭлементРасшифровкиКомпоновкиДанныхПоля") Тогда
Поля = ЭлементРасшифровки.ПолучитьПоля();
Значение = Поля.Найти(ИмяПараметраРасшифровки);
Если ТипЗнч(Значение) = Тип("ЗначениеПоляРасшифровкиКомпоновкиДанных") Тогда
Результат = Значение.Значение;
КонецЕсли;
КонецЕсли;
КонецЕсли;
КонецЕсли;
Возврат Результат;
КонецФункции
Условное оформление
А как у нас обстоят дела с условным оформлением? Элементы условного оформления, например ЦветФона (при их наличии конечно же), тоже имеются в составе ЭлементРезультата.ЗначенияПараметров, откуда их можно извлечь и применить к оформлению ячейки.
Вот только сложно понять к оформлению чего(фона, текста...) относится этот цвет. Не самый эффективный, но вполне рабочий способ решения этой проблемы заключается в следующем:
Параллельно с формированием отчета на основе своего макета, можно формировать другой табличный документ стандартным способом. В этом случае ПроцессорВывода сам применяет условное оформление к выводимым макетам, и мы можем подсмотреть оформление этих ячеек. Некоторая сложность заключается в том, что выводимая на каждом шаге область может иметь произвольный размер, и в этой области нам надо взять оформление конкретной ячейки.
Ячейка = ПолучитьЯчейкуОбласти(ДокументРезультатДляУО, 3, 2, 1, 1);
Секция.Область().ЦветФона = Ячейка.ЦветФона;
Функция ПолучитьЯчейкуОбласти(Таблица, ВысотаМакета, ШиринаМакета, СтрокаЯчейки, КолонкаЯчейки)
Результат = Неопределено;
Если Таблица.ВысотаТаблицы > 0 Тогда
Область = Таблица.ПолучитьОбласть(Таблица.ВысотаТаблицы - ВысотаМакета + 1,, Таблица.ВысотаТаблицы);
Результат = Область.Область(СтрокаЯчейки, Область.ШиринаТаблицы - ШиринаМакета + КолонкаЯчейки);
КонецЕсли;
Возврат Результат;
КонецФункции
Обработка расшифровки в модуле формы
Что мы еще упустили? Очевидно, что наш отчет настроен под определенную структуру. А пользователь при работе с отчетом вполне может "расшифровать" ячейку и получить непредсказуемый результат. Надо бы лишить его этой возможности.
&НаКлиенте
Процедура РезультатОбработкаДополнительнойРасшифровки(Элемент, Расшифровка, СтандартнаяОбработка, ДополнительныеПараметры)
Если ТипЗнч(Расшифровка) = Тип("ИдентификаторРасшифровкиКомпоновкиДанных") Тогда
// Оставляем типовое меню без пункта "Расшифровать", т.к. этот пункт меняет структуру отчета
СтандартнаяОбработка = Ложь;
ОбработкаРасшифровки = Новый ОбработкаРасшифровкиКомпоновкиДанных(ДанныеРасшифровки, Новый ИсточникДоступныхНастроекКомпоновкиДанных(Отчет));
МассивДоступныхДействий = Новый Массив();
МассивДоступныхДействий.Добавить(ДействиеОбработкиРасшифровкиКомпоновкиДанных.ОткрытьЗначение);
МассивДоступныхДействий.Добавить(ДействиеОбработкиРасшифровкиКомпоновкиДанных.Отфильтровать);
МассивДоступныхДействий.Добавить(ДействиеОбработкиРасшифровкиКомпоновкиДанных.Упорядочить);
МассивДоступныхДействий.Добавить(ДействиеОбработкиРасшифровкиКомпоновкиДанных.Оформить);
ОбработкаРасшифровки.ПоказатьВыборДействия(Новый ОписаниеОповещения("ПослеВыбораДействияРасшифровки", ЭтаФорма, Расшифровка), Расшифровка, МассивДоступныхДействий,,, Элементы.Результат);
Иначе
// Если передали в расшифровку какое-то свое значение, обработаем отдельно
КонецЕсли;
КонецПроцедуры
&НаКлиенте
Процедура ПослеВыбораДействияРасшифровки(ВыбранноеДействие, ПараметрВыбранногоДействия, Расшифровка)Экспорт
Если ВыбранноеДействие <> Неопределено Тогда
Если ВыбранноеДействие <> ДействиеОбработкиРасшифровкиКомпоновкиДанных.Нет Тогда
Если ВыбранноеДействие = ДействиеОбработкиРасшифровкиКомпоновкиДанных.ОткрытьЗначение Тогда
ПоказатьЗначение(, ПараметрВыбранногоДействия);
Иначе
ОтработатьРасшифровку(Новый ОписаниеОбработкиРасшифровкиКомпоновкиДанных(ДанныеРасшифровки, Расшифровка, ПараметрВыбранногоДействия));
КонецЕсли;
КонецЕсли;
КонецЕсли;
КонецПроцедуры //
&НаСервере
Процедура ОтработатьРасшифровку(ОписаниеОбработкиРасшифровки)
ДанныеРасшифровкиОбъект = ПолучитьИзВременногоХранилища(ДанныеРасшифровки);
ОбработкаРасшифровки = Новый ОбработкаРасшифровкиКомпоновкиДанных(ДанныеРасшифровкиОбъект, Новый ИсточникДоступныхНастроекКомпоновкиДанных(Отчет));
РезультирующиеНастройки = ОбработкаРасшифровки.ПрименитьНастройки(ОписаниеОбработкиРасшифровки.Идентификатор, ОписаниеОбработкиРасшифровки.ПрименяемыеНастройки);
Если ТипЗнч(РезультирующиеНастройки) = Тип("НастройкиКомпоновкиДанных") Тогда
Отчет.КомпоновщикНастроек.ЗагрузитьНастройки(РезультирующиеНастройки);
ИначеЕсли ТипЗнч(РезультирующиеНастройки) = Тип("ПользовательскиеНастройкиКомпоновкиДанных") Тогда
Отчет.КомпоновщикНастроек.ЗагрузитьПользовательскиеНастройки(РезультирующиеНастройки);
КонецЕсли;
СкомпоноватьРезультат();
КонецПроцедуры
Инструмент анализа процесса компоновки
Вроде бы мы рассмотрели все аспекты, кроме одного: а как нам получить соответствие имен макетов СКД и имен секций нашего макета отчета?
Для этого был создан специальный инструмент в виде внешней обработки.
Здесь надо упомянуть о том, что при первой итерации цикла получения элементов результата, в ЭлементРезультата.Макеты содержится список описаний макетов областей компоновки данных. Из них можно взять и список параметров, и свойства самого макета, а потом связать все это с именем.
Для облегчения этого процесса в обработке предусмотрен вывод списка макетов и их параметров:
Понятно, что результат - табличный документ - состоит из этих областей макетов. А как они располагаются в отчете относительно друг друга? Для просмотра этого тоже есть средство:
Таким образом с помощью данной обработки легко понять, как связать имена макетов компоновки данных с именами секций своего макета и заполнить вышеупомянутое соответствие Макеты.
Дополнительно данный инструмент генерирует шаблон кода построения отчета для модуля объекта. В полученном шаблоне необходимо заполнить имена секций и, при необходимости, дописать содержимое цикла обработки ЭлементРезультата. Это необходимо сделать если вид полученного отчета посложнее простой таблицы (как в примере в начале статьи).
Подведем итоги
Рассмотренный способ обладает целым рядом преимуществ:
- Максимальное соответствие внешнего вида шаблона и полученной формы отчета, WYSIWYG - наше всё! И даже можно устанавливать ширину колонок и она будет такой же в отчете!)).
- Относительно небольшой объем кода, его простота, концентрация кода в одном месте (в отличие от настроек СКД, разбросанных по разным разделам)
- Возможность пошаговой отладки.
- Четкое разделение "зон ответственности" - вот здесь мы получаем некоторый объем данных, а вот здесь - выводим данные в отчет. (Добавление в запрос генерации названий колонок - зло!).
- Полный контроль за выводом отдельных ячеек отчета - легко реализовать вывод разных секций в зависимости от выводимых данных (например, для контрагентов - юр.лиц выводим одну секцию, для физ.лиц - другую). Некоторые строки можно вообще пропускать (вспомним про знаменитую проблему вывода иерархического справочника).
- Объединение произвольной группы ячеек, как горизонтальное, так и вертикальное - так же, как в примере отчета в статье.
- Возможность проведения расчетов, которые нельзя провести с помощью языка запросов и выражений СКД.
- Возможность реализации нестандартного условного оформления, например, которое зависит не только от данных строки.
- Простой и логичный способ получения данных накопления - нарастающий итог, иерархическая нумерация, разница текущего значения с последним из непустых предыдущих.
- Такой же простой способ вывести картинки строк или другие внедренные в табличный документ объекты, например, диаграммы.
- Возможность использовать ПроверитьВывод() и ПроверитьПрисоединение(), например для расчета итогов по странице при переменной высоте строк отчета (привет, товарная накладная!).
- Вывод отчета в несколько "колонок". Пример из жизни: прайс-лист компьютерной фирмы: материнские платы выводятся одной таблицей на всю ширину страницы, память - двумя (по половине ширины страницы), манипуляторы - тремя.
- Возможность использовать свою расшифровку произвольного вида - хоть ссылки, хоть структуру, хоть текст с подсказкой. Также можно убрать расшифровку из тех ячеек, где она не нужна.
- Возможность использовать свою систему групп строк и колонок, определять свой список свернутых групп при открытии отчета
Конечно же есть и недостатки (я насчитал аж 3: один весомый и два малозначимых):
- Самый главный недостаток - "заточенность" под конкретную структуру отчета. Хотя недостатком это является весьма условно - есть много отчетов, для которых менять структуру пользователям просто не нужно.
- Поскольку при построении отчета мы ориентируемся на внутренние имена макетов СКД, возможна ситуация, когда при смене версии платформы механизм именования изменится и наше соответствие имен макетов "испортится". Но мне кажется этот риск имеет достаточно малое значение, поскольку он, во-первых - маловероятен, а во-вторых - его последствия легко устраняются.
- Чисто теоретически алгоритм вывода отчета на языке 1С может работать медленнее, чем платформенный механизм вывода. Но, как правило, основные временные затраты идут на выполнение запроса и на клиент-серверный обмен, а не на формирование табличного документа.
Методика, описанная в статье, была протестирована на платформе 8.3.18.1363.
На этом все. Как обычно приветствуются замечания / дополнения / комментарии.
К публикации приложены файлы: внешняя обработка - АнализПроцессаКомпоновки.epf и архив с этой обработкой и выгрузкой информационной базы с рассмотренным в статье примером.