Немного про FastReport, источники данных и отчеты без запросов к БД на конкретном примере

28 3

Многие не понаслышке знают, что создание отчетов в Directum RX это одна из самых трудоемких задач.
Зачастую, тривиальная задача может превратиться в целое испытание из-за неверно выбранного подхода к заполнению отчета данными.

И тут резонно возникает вопрос "А какие подходы вообще есть!?"

Прежде чем ответить этот вопрос, давайте коротко разберем источники данных для отчета:

  • Параметры отчета;
  • Источник данных типа сущность;
  • SQL-источники данных.

Справка Directum RX 3.5 по основным механизмам отчетов.

 

Параметры отчета:

Подходят как для передачи данных в дизайнер отчета, так и в события "До выполнения", "После выполнения".
Поскольку параметры в основном используются для передачи примитивов и изредка сущностей, считается что с ними каши не сварить. Вероятно по это причине, в построении сложных отчетов они не используются, т.к. в базовых решениях нет ни одного отчета с выборкой полученной из параметров, а не из источников данных (Sungero_Connections или тип сущность).

Пример: Отчет "BarcodePageReport" расположенный в Sungero.Docflow.OfficialDocument.

 

Источник данных типа сущность:

Тут все просто, это обычный LINQ по типам сущностей.
Получили по нужным условиям сущности, подключили их к DataSource в отчете, вывели данные. Данные для фильтрации как раз можно передать в параметрах отчета, если это необходимо.
Загрузку всех связанных с сущностью полей можно считать как минусом, так и плюсом, но то что это влияет на скорость построения отчета это факт.
Несомненным плюсом является скорость разработки отчета. Подключили к примеру справочник сотрудников, отфильтровали и работаем. Красота!

Пример: Отчет "OutgoingDocumentsReport" расположенный в Sungero.RecordManagement.

 

SQL-источники данных (далее по тексту Sungero_Connections):

Классические запросы к БД.
Тут и связи таблиц через JOIN, и объединения UNION, и прочие прелести SQL (MS и Postge). 
Выборка данных для отчета ограничена лишь фантазией и навыками разработчика.
Ну и несомненный плюс в виде быстродействия, т.к. получить большой массив данных запросом намного быстрее, чем прикладным кодом (кривые запросы и индексы в расчет не берем).

Пример: Отчет "ApprovalSheetReport" расположенный в Sungero.Docflow.OfficialDocument (95% отчетов в Sungero как раз используют Sungero_Connection).

 

Казалось бы!?
Вот он идеальный кандидат. Подключай таблицы в Sungero_Connections и пользуйся, но дьявол как всегда кроется в деталях!

  1. Sungero_Connections требует как минимум базовых знаний TSQL и PGSQL, что имеют не все разработчики, как бы странно это не звучало;
  2. Требуется создание отдельной таблицы для каждого отчета при инициализации (на худой конец перед выполнением отчета), т.к. данные куда-то писать нужно;
  3. Созданную таблицу обязательно нужно очищать от данных, после выполнения отчета;
  4. Трудоемкость использования Sungero_Connections гораздо выше чем у первых 2-х источников, т.к. нужно писать запросы (желательно сразу с адаптацией под Postgre, если у заказчика MS и наоборот), настраивать соотношения полей и т.д., и .т.п.

Есть ли вариант как-то упростить себе жизнь!? Ну хотя бы изредка!?
FastReport ведь весьма гибкий генератор отчетов и одну и ту же задачу можно решить даже не двумя разными подходами, а больше!

Давайте разберем один из таких примеров, а заодно немного затронем тему коллекций в параметрах, т.к. данных по ним на Club почти нет.

 

Постановка задачи и анализ

Требуется разработать отчет по исполнению поручения (форма отчета ниже).
Вызов отчета поместить на ленту карточки задачи "Задача на исполнение поручения" в группу "Отчеты".

Дополнительные требования:
- Формат страниц отчета А6;
- Поля: нижний отступ - 10 мм, остальные отступы - 5мм;
- Шрифт: Arial;
- Размер шрифта: 12 пунктов (рег. номер и рег. дата - 9 пунктов).


Все имена и роли изменены, любые совпадения с реальным отчетом случайны!

И так, перед нами постановка задачи на реализацию отчета по поручению для руководителя.
Если блоки 2, 5, 6 вопросов реализации не вызывают, то по остальным блокам есть разные варианты реализации.

Блок 1: 

Б) Скрывать логотип в зависимости от условия.

Думаю не нужно объяснять, что вариант А является далеко не самым оптимальным, поэтому без вопросов выбираем Б.

Блок 3:

А) Использовать простой строковый параметр отчета, записать туда форматированный текст с HTML тегами и вывести в отчете;
Б) Подключить источник Sungero_Connections, написать запрос который будет выбирать нужных исполнителей;
В) Подключить источник типа сущность IEmployee и фильтровать его по заданным параметрам;
Г) Использовать строковый параметр отчета со свойством "IsCollection". 

Вариант А ограничен длинной строки и в данном случае его лучше не применять, вариант Б довольно трудоемкий по реализации, а при варианте Г не понятно, как без бубнов подставлять (отв.) у основных исполнителей. В данном случае коллекция наше все, на мой взгляд.

Блок 4:

Тут, в теории, по сути те же варианты, что и в блоке 3, за исключением варианта А, как самого неоптимального из-за возможных объемов текста. Вообще передача больших кусков данных в параметрах отчета это крайне сомнительное и плохое решение на мой взгляд.
Вариант Г отпадает по тем же причинам что и А, т.к. некоторые могут и поэму написать в тексте поручения (правда ограниченную 1000 символами) и когда отчет сломается это лишь вопрос времени.
Вариант Б мы в данном примере не используем, т.к. долго и трудоемко.
Остается подключить коллекцию поручения с исполнителями ActionItemParts, как источник данных. В случае необходимости, отчет можно будет быстро поправить, если на этапе проектирования что-то упустили.

Где создавать отчет:

A) Создать новый скрытый модуль и создать отчет в нем, без привязки к сущности задачи;
Б) Перекрыть задачу ActionItemExecutionTask (Задача на исполнение поручения) и создать отчет в ней.

Т.к. отчет логически относится к исполнению поручения и будет вызываться только из задачи, то правильнее выбрать вариант Б, да и создание лишних модулей ради одного отчета можно будет избежать.
Плюсом в отчете автоматически появится параметр Entity, который будет иметь интерфейс Sungero.RecordManagement.IActionItemExecutionTask

 

Реализация

Анализ вариантов реализации провели, можно выполнять поставленную задачу.

1) Перекроем задачу ActionItemExecutionTask и создадим отчет ResolutionReport.

 

 

 

 

 

 

 

 

 

2) Займемся параметрами отчета и источниками данных. 

  • Рег. номер и рег. дату документы мы будем передавать через параметры отчета, поэтому создадим параметры с именами RegistrationNumber и RegistrationDate.
  • Для отслеживания вхождения автора поручения в роль ... пусть будет "Аудиторы", создадим логический параметр IsAuditors. На основе состояния этого параметра будем скрывать логотип.
  • Блок 3 с исполнителями поручения мы решили передавать через коллекцию строк. Назовем этот параметр AssigneesName
  • Блок 4 мы решили получать из источника данных (тип сущности), поэтому подключим тип finex.SolutionsForTests.ActionItemExecutionTaskActionItemParts и назовем источник ActionItemParts.

Остальные данные поручения мы будем получать из параметра Entity.


 

3) Займемся заполнением параметров отчета данными в событии отчета "До выполнения":

public override void BeforeExecute(Sungero.Reporting.Server.BeforeExecuteEventArgs e)
{
    var task = ResolutionReport.Entity;
			
    //Заполним рег № и дату регистрации, если в поручении есть документ			
    if (task.DocumentsGroup.OfficialDocuments.Any())
    {
	   var document = task.DocumentsGroup.OfficialDocuments.First();
	   ResolutionReport.RegistrationNumber = string.Format("№ {0}", document.RegistrationNumber);
	   ResolutionReport.RegistrationDate = document.RegistrationDate;
    }
			
    //Заполним исполнителя или исполнителей поручения, если оно составное
    if (task.IsCompoundActionItem.HasValue && task.IsCompoundActionItem.Value)
    {
	   foreach (var item in task.ActionItemParts)
	      ResolutionReport.AssigneesName.Add(string.Format("{0} (отв.)", item.Assignee.Person.ShortName));
    }
    else
       ResolutionReport.AssigneesName.Add(string.Format("{0} (отв.)", task.Assignee.Person.ShortName));

    //Заполним соисполнителей поручения
    foreach (var item in task.CoAssignees)
	   ResolutionReport.AssigneesName.Add(item.Assignee.Person.ShortName);
			
    //Сотрудник выдавший поручение входит в роль Аудиторы ?
    ResolutionReport.IsAuditors = task.AssignedBy.IncludedIn(Roles.Auditors);
}

Как видим код тут совсем не сложный. 
Коллекция отчета ResolutionReport.AssigneesName без проблем заполняется, через Add().
Если при заполнении возникают ошибки, то используйте AddRange().

 

4) Для получения составных получений в источнике данных ActionItemParts перейдем к коду и напишем условие выборки данных:

public virtual IQueryable<finex.SolutionsForTests.IActionItemExecutionTaskActionItemParts> GetActionItemParts()
{
   return finex.SolutionsForTests.ActionItemExecutionTasks
				.GetAll(t => t.Equals(ResolutionReport.Entity))
				.SelectMany(_ => _.ActionItemParts)
				.Cast<finex.SolutionsForTests.IActionItemExecutionTaskActionItemParts>();
}

Т.к. при создании источника я "намеренно" выбрал тип из модуля finex.SolutionsForTests, а результат имеет интерфейс Sungero.RecordManagement, то нужно привести данные к перекрытой сущности через Cast<>().

 

5) Данные в отчете есть, можно приступать к визуальной составляющей. Настроим размер страниц и поля.


 

6) Настроим бэнды отчета.
Нам точно понадобятся бэнды "Заголовок отчета" и "Подвал отчета" для блоков  1, 2 и 6.
Для блоков 3, 4, 5 добавим 3 источника данных.


 

7) Для удобства дальнейшего повествования я накидал и подписал элементы отчета, чтобы можно было ссылаться на имена элементов в дереве отчета.
Так же добавил логотип в элемент LogoPicture


 

8) Подключим источник данных ActionItemParts к бэнду ActionItemPartsData (бэнд данных тот что посередине).
Остальные бэнды данных останутся без источников.

 

9) Займемся наполнением элементов отчета кодом.

JobTitle - заполняем должностью сотрудника

[[Entity].AssignedBy.JobTitle]

ShortName - заполняем фамилией и инициалами из персоны 

[[Entity].AssignedBy.Person.ShortName]

RegNum и RegDate - заполняем данными из соответствующих параметров

AssigneesText - заполняем данными из параметра AssigneesName. Для этого преобразуем коллекцию CollectionAdapter<string> в массив, чтобы потом преобразовать ее в строку с разделителями используя string.Join().

[string.Join("\n", [AssigneesName].ToArray())]

ActionItemPartsText - заполним пункты поручения из источника ActionItemParts.
Формат: <Фамилия И.О.> <текст личного поручения> до <инд. срок>
Если индивидуальный срок установлен, то выведем "до <инд. срок>", иначе ничего не выводим.

[ActionItemParts.Assignee.Person.ShortName] [ActionItemParts.ActionItemPart] [[ActionItemParts.Deadline].HasValue ? "до " + Format("{0:dd/MM/yyyy}", [ActionItemParts.Deadline]) : ""]

ActionItemText - тест поручения возьмем из самого поручения.

[[Entity].ActionItem]

DeadlineText - Если поручение составное, то возьмем "Общий срок" поручения, иначе возьмем "Срок".

[[Entity].IsCompoundActionItem == true ? [Entity].FinalDeadline : [Entity].Deadline]

LowerText - В подвал отчета выведем дату создания поручения в формате __.Месяц.Год

[Format("{0:__/MM/yyyy}", [Entity].Created)]
Отметки исполнителя ________________________________________________________________________________________________________________________________________

 

Итоговый результат выглядит так:


 

10) Скроем логотип, если автор поручения не входит в заданную роль.

Многие наверное замечали вкладку "Код" в нижней части дизайнера отчетов.
Т.к. не все знают как этой вкладкой пользоваться, мы будем этот пробел устранять.

Выделяем наш элемент LogoPicture с логотипом и в свойствах элемента решительно жмем на значок с молнией.

Нам нужно событие BeforePrint (До печати), жмем в него двойным кликом мыши и в классе ReportScript у нас появляемся пустой метод LogoPicture_BeforePrint

namespace FastReport
{
  public class ReportScript
  {
   
    private void LogoPicture_BeforePrint(object sender, EventArgs e)
    {
      
    }
    
  }
}

 

Для выполнения задуманного нам нужно:
1) Получить параметр отчета IsAuditors в методе LogoPicture_BeforePrint
2) Если он True, то скрыть логотип (LogoPicture) и пустой разделитель (SeparatorLogo), а остальные элементы заголовка сдвинуть вверх к началу страницы.

Для реализации напишем код: 

private void LogoPicture_BeforePrint(object sender, EventArgs e)
{
      //Получаем параметр IsAuditors из отчета
      //если он True, то скроем логитип и сдвинем элементы формы вверх 
      if (!(Boolean)Report.GetParameterValue("IsAuditors"))
      {
        float logoHeight = LogoPicture.Height + SeparatorLogo.Height;
        
        LogoPicture.Visible = false; 
        SeparatorLogo.Visible = false;
        
        JobTitle.Top = (float)(JobTitle.Top - logoHeight);
        ShortName.Top = (float)(ShortName.Top - logoHeight);
        Line1.Top = (float)(Line1.Top - logoHeight);
        Line2.Top = (float)(Line2.Top - logoHeight);
        RegNum.Top = (float)(RegNum.Top - logoHeight);
        RegDate.Top = (float)(RegDate.Top - logoHeight);
        SeparatorHeader.Top = (float)(SeparatorHeader.Top - logoHeight);
      }
}

Дело осталось за малым.

 

11) Настроим свойства бэндов и элементов.

ReportTitleLogo (заголовок отчета) - может расти, может сжиматься.
AssigneesData (Данные) - может расти, может сжиматься, может разрываться.
ActionItemPartsData (Данные) - может расти, может сжиматься, может разрываться.
ActionItemExecutionData (Данные) - может расти, может сжиматься, может разрываться.
ReportSummary (Подвал отчета) - Печать внизу страницы.

Элементы на странице настраиваем похожим образом: 
- где текста может быть много ставим "Может расти";
- где текста может и не быть, ставим "Может сжиматься";
- где текст может перенестись на другую страницу, ставим "Может разрываться".

 

12) Создаем новое действие в поручении и выносим кнопку на форму задачи.

public virtual void ResolutionReportfinex(Sungero.Domain.Client.ExecuteActionArgs e)
{
   var report = Reports.GetResolutionReport();
   report.Entity = _obj;
   report.Open();
}

Готово, можно проверять результат трудов!

 

Результат

Формируем тестовое поручение для проверки корректности работы отчета:

 

Формируем отчет:

 

Добавляем автора поручения в роль Аудиторы и переформировываем отчет для проверки отображения логотипа:

 

Отлично, создадим еще одно поручение, но уже с соисполнителями:

 

Все работает как и ожидалось.
Логотип скрывается, бэнд с пунктами поручения сжимается при отсутствии данных.

 

Итог

С одной стороны пример отчета был не сложным в реализации, но это только с одной стороны.
Если опыта в FastReport мало или отчеты приходится делать редко, то даже такая разработка может превратиться в целый квест.

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

Приведенный пример не является единственно верным вариантом реализации и если у Вас есть замечания или дополнения, то пишите об этом в комментариях. Будет интересно.

28
Авторизуйтесь, чтобы оценить материал.
2
Mikhail Popkov

Добрый день.
как в запросе корректно передать параметр для in
and wf.performer in @performerIds где @performerIds - (12,147,458) набор ИД - в таком варианте не работает, как и не работает если (@performerIds) где @performerIds - 12,147,458 набор ИД

Сергей Беляков

Mikhail, Он и не будет работать, т.к. @performerIds это строковый параметр и вставляется он в запрос как строка.
На Вашем месте я бы просто заменил источник данных с Sungero_Connection на тип сущности.

Денис Зайцев

Mikhail, вот здесь разбирался данный вопрос

https://club.directum.ru/question/364899

Авторизуйтесь, чтобы написать комментарий