前言

本文档提供在基于 CUBA 框架构建的应用程序中创建报表的指导。

目标读者

此手册适用于 CUBA 应用程序的开发人员和管理员。要顺利地使用报表生成器,需要对关系数据库原理有一些了解,并且能够编写 SQL 查询。熟悉 JPQL 和 Groovy 也会很有帮助,因为在某些情况下使用这些语言提取报表数据会更容易。

更多阅读材料

可以在 https://www.cuba-platform.com/documentation 找到与 CUBA 框架相关的手册及附属文档。

报表生成器的核心是 YARG 框架,是在免费的 Apache 2.0 许可下发布的。有关更多详细信息,请参阅 blog 中的文章。该框架文档可从 https://github.com/cuba-platform/yarg/wiki 获取。

反馈

如果有任何改进本手册的建议,请随时在 GitHub 上的源码仓库报告问题。如果看到拼写或措辞错误、bug 或不一致,请毫不犹豫地 fork 并修复。谢谢!

1. 简介

报表扩展旨在简化 CUBA 应用程序中报表的生成。它允许在大多数流行的编辑器(如 Microsoft OfficeLibreOffice/OpenOffice)中创建报表模板,并在运行时使用 CUBA 数据模型、SQL、JPQL 或脚本定义数据源

报表扩展支持的功能:

  • 使用逐步向导在运行时可视化地构建报表模板;

  • 以 DOC、DOCX、ODT、XLS、XLSX、HTML 或任意文本格式生成报表;

  • 创建复杂的 XLS(X)报表:多层报表、数据聚合报表、交叉表报表;

  • 在 XLS(X)报表中使用图表和公式;

  • 将 office 格式或 HTML 格式的报表转换为 PDF 格式。

2. 快速开始

本章提供了使用报表生成器的实际操作示例。大部分示例都基于示例 Library 应用程序,该应用程序可在 CUBA Studio 示例项目 GitHub 中获取。

首先,在 CUBA Studio 中打开 Project Properties 编辑器:点击 CUBAProject Properties 主菜单项。在 App components 列表中添加 reports 应用程序组件。

为了快速可以开始,报表生成器附带有报表向导 - 一种用于快速创建报表的可视化工具,包括数据结构和模板设计。使用向导创建报表后,可以再对生成的报表进行进行分析,了解数据集如何创建、查看报表参数、修改模板输出类型,这些内容在本手册的其它部分描述。

要运行向导,请单击 Reports 界面中的 CreateUsing wizard

reports wizard main
Figure 1. 调用报表向导

使用向导可以创建三种类型的报表:

  1. 单个实体的报表。

  2. 给定实体列表的报表。

  3. 通过查询过滤的实体列表的报表。

报表设计分为三个步骤:

  1. 创建报表的数据结构。

  2. 编辑报表区域。

  3. 保存报表。

可以使用报表编辑器以常规方式修改创建的报表,并通过通用报表浏览界面运行,或使用 TablePrintFormActionEditorPrintFormAction 操作来调用。

2.1. 单实体报表

假设我们想获取有关图书的详细出版信息,即 library$BookPublication 实体的实例。

首先,运行报表向导并指定报表详细信息:

  • Entity - 要在报表中展示的实体 - library$BookPublication.

  • Template type - 报表模板格式 - 定义报表输出的格式 - DOCX。注意,还可以使用XSLXHTMLCSVChart格式。

  • Report name - Publication details

接下来,指定报表类型: Report for single entity

single entity step 1
Figure 2. 单实体报表:第一步

然后单击 Next 按钮; 将出现 Select attributes for the simple report region 窗口。指定 BookPublication 实体和应该在体现在报表中的相关实体的属性(Publication.Book.NamePublication.Publisher.NamePublication.YearPublication.City.Name)。要执行此操作,请在左列中选择它们并通过单击 attributes_selection_arrow 或双击将它们移动到右侧。

报表中属性的顺序将对应于右侧列表中指定的顺序。要更改显示顺序,请单击 attributes_selection_up/attributes_selection_down 上下移动属性。

single entity attributes
Figure 3. 单实体报表:选择实体属性

单击 ОК 进入第二步 - 报表区域编辑。

出现的界面包含一个命名区域列表,这些区域是用来显示相关数据。向导允许向模板添加多个纯文本区域,以显示不同的数据集。

加载到特定区域的实体属性列表可以通过单击选中的属性列表链接来修改。还可以通过单击 Add simple region 来添加新区域。

如果实体包含集合属性,则会出现 Add tabulated region 按钮。它可以添加显示表格数据的区域。

在这两种情况下,选择对话框将显示 library$BookPublication 实体的属性列表,允许添加或删除集合中的属性。

single entity step 2
Figure 4. 单实体报表:第二步

在此步骤,我们已经可以运行报表并查看报表的样式。单击 Run 按钮,选择 library$BookPublication 实例并查看结果。

single entity test running
Figure 5. 测试运行

配置完所有报表区域后,可以进入第三步:保存报表。此时,可以查看完整的报表模板,或将输出文件的名称和格式更改为任意一种可用的类型。

single entity step 3
Figure 6. 报表输出类型

单击 Save 按钮后,将出现标准报表编辑界面。现在,可以按常规方式微调报表。编辑完成后,在报表编辑界面单击 Save and close

该报表现已添加到报表浏览界面中的 General 报表组,可以通过单击 Run 按钮来运行该报表。

single entity reports list
Figure 7. 报表浏览

此外,我们可以在出版物浏览界面上调用报表运行功能。为此,我们将在 bookpublication-browse.xml 界面描述添加 Print details 按钮:

<groupTable id="bookPublicationsTable"
    ...
    <buttonsPanel>
        ...
        <button id="printDetails"
                caption="msg://printDetails"/>

然后我们需要在控制器中实现 TablePrintFormAction

@Inject
private Button printDetails;

@Subscribe
private void onInit(InitEvent event) {
    TablePrintFormAction action = new TablePrintFormAction("report", bookPublicationsTable);
    bookPublicationsTable.addAction(action);
    printDetails.setAction(action);
}

最后,我们应该将报表与 BookPublication 浏览界面关联起来。打开报表编辑界面,切换到 Roles and Screens 标签页,并从下拉列表中将 library$BookPublication.lookup 界面添加到下表:

single entity screens
Figure 8. 添加界面

现在,可以通过在网格中选中出版物并单击 Print details 按钮来运行任何出版物的报表

single entity running
Figure 9. 打印详情

输出如下:

single entity result
Figure 10. Report result

2.2. 实体列表报表

报表向导允许为实体实例列表创建两种类型的报表:

  1. 手动选择特定实体实例的报表

  2. 由特定请求筛选的实体实例报表。

来看看第一种报表类型。假设想要获取图书馆中所有书籍实例(library$BookInstance 实体)的列表,列表项中包含书籍的名称和所属类目。

第一步,指定报表详细信息:

  • Entity - 报表实体 - library$BookInstance.

  • Template type - 输出格式 - XSLX.

  • Report name - 报表名称 - Book items location.

然后,选择报表的类型(Report for list of entities),然后单击 Next

list of entities step 1
Figure 11. 实体列表报表:第一步

按任务要求,在属性选择窗口中选择 BookItem.Publication.Book.NameBookItem.LibraryDepartment.Name

list of entities attributes
Figure 12. 实体列表报表:选择实体属性

单击 ОК 并进入第二步,进行报表带区编辑。

用于实体列表的报表模板被限制为只能有一个以表格形式显示数据的区域。虽然不允许添加新区域,但可以通过单击包含属性列表的链接来编辑现有数据集,或者删除现有区域并重新创建。

目前,不需要进行任何更改。单击 NextSave 保存报表。该报表在报表编辑界面中显示如下:

list of entities editor
Figure 13. 报表数据构成

一旦报表被保存,就可以从通用报表浏览器运行报表。

此外,可以添加一个按钮来从书籍条目浏览界面运行报表,可以通过单击出版物浏览界面中的 Show items 按钮打开该界面。为此,将书籍实例表格的 multiselect 属性设置为 true,以便能够为报表指定一组记录,然后添加按钮的源代码:

<groupTable id="bookInstancesTable"
            multiselect="true">
            ...
    <buttonsPanel>
    ...
        <button id="printList"
                caption="msg://printList"/>

之后,在界面控制器中注入 Button 组件:

@Inject
private Button printList;

接下来,订阅 InitEvent 事件并添加以下代码:

@Subscribe
private void onInit(InitEvent event) {
    TablePrintFormAction action = new TablePrintFormAction("report", bookInstancesTable);
    bookInstancesTable.addAction(action);
    printList.setAction(action);
}

最后,应该将 Book items location 报表与图书条目浏览界面相关联。打开报表编辑界面,切换到 Roles and Screens 标签页,然后从界面下拉列表中添加 library$BookInstance.lookup 界面到下表:

list of entities screens
Figure 14. 添加界面

现在,可以从书籍条目浏览界面的表格中选中要打印的条目并点击 Print list 按钮来运行报表。Print selected 选项导出所选条目,Print all 选项打印当前过滤器选择的所有实例。

list of entities running
Figure 15. 打印选中的

输出如下:

list of entities result
Figure 16. 报表输出

2.3. 查询报表

现在看一下向导提供的最后一种报表类型:通过查询过滤出的实体列表的报表。要演示这种报表类型,可以在前一个示例的基础上进行修改。和前一个示例一样,报表包含一个书籍列表(包括其标题和归属类目),但仅输出特定日期之后添加的书籍。

像上一个示例一样设置报表的详细信息:

  • Entity - 报表实体 - library$BookInstance.

  • Template type - 输出文件格式 - XSLX.

  • Report name - Recently added book items.

然后选择 Report for list of entities, selected by query 报表类型。

query step 1
Figure 17. 查询语句过滤的实体列表报表:第一步

选定的报表类型允许我们选择与指定条件匹配的实体列表。要设置查询,请单击下面的 Set query 链接。

出现 Define query 窗口。正如所见,窗口类似于通用过滤器窗口。在此处指定条件,可将多个条件使用 AND/OR 进行组合。

要添加新查询条件,请单击 Add。在出现的窗口中选择 Created at 属性。现在该属性被添加到查询条件树中,右侧面板将显示其属性。

在属性面板中,可以设置默认参数值。如果不允许更改报表逻辑,则可以通过选中 Hidden 复选框来隐藏此属性。在这种情况下,运行报表时不会要求用户输入此参数。

选择一个运算符(>=)。

query parameter
Figure 18. 查询参数

保存查询后,单击 Next 移至 library$BookInstance 属性选择界面。我们将 BookItem.Publication.Book.NameBookItem.LibraryDepartment.Name 属性移到右边。单击 OK 完成第一步。

list of entities attributes
Figure 19. 选择实体属性

点击 NextSave 以保存报表。

query step 2
Figure 20. 实体属性

该报表如下所示:

query editor
Figure 21. 报表数据构成

在报表编辑器中,可以通过添加新的带区和数据集,以及配置报表模板、本地化和访问权限来创建更复杂的报表。

例如,可以切换到 Parameters and Formats 标签页并修改 Parameters 列表中的查询参数:使用 Date 替换标准的 CreateTs1。保存更改并关闭报表编辑界面。

query parameter rename
Figure 22. 参数和格式标签页

执行完上述操作后,添加可直接从图书馆图书类目浏览界面运行报表的 Report 按钮。

为此,需要在 librarydepartment-browse.xml 界面描述中定义一个按钮:

<table id="libraryDepartmentsTable"
    ...
    <buttonsPanel id="buttonsPanel">
        ...
        <button id="reportBtn"
                caption="msg://reportBtn"/>
    </buttonsPanel>
</table>

之后,在界面控制器中注入按钮:

@Inject
private Button reportBtn;

并在 onInit() 方法中将 RunReportAction 分配给按钮:

reportBtn.setAction(new RunReportAction("report"));

如同前面的示例,对于每个报表,需要将 library$LibraryDepartment.browse 添加到报表编辑界面的 Roles and Screens 标签页上的界面列表中。

Report 按钮将显示在图书馆图书类目浏览界面中,只需单击一下即可显示系统中所有可用报表的列表。要运行报表,请在列表中选择 Recently added book items,指定日期并单击 Run report

query running
Figure 23. 运行报表

输出如下:

query result
Figure 24. 报表输出

2.4. 带图表输出的报表

使用报表向导,可以使用跟创建其它类型的报表一样的方式创建带图表输出的报表。唯一的不同是在向导中完成了报表之后需要配置图表模板。

该示例程序是基于 petclinic - 宠物诊所 应用程序,源码可以在 GitHub 找到。

  1. 如前一章所述开始创建报表。

    chart wizard
    Figure 25. 向导中的图表模板 - 第一步
  2. 对于图表报表,要选择可进行数值计算的实体属性,将来用这些属性作为图表的数值轴。

    chart wizard 2
    Figure 26. 向导中的图表模板 - 选择属性
  3. 完成创建报表的下一步。

    chart wizard 3
    Figure 27. 向导中的图表模板 - 第二步
  4. 最后一步中,选择图表类型,饼图或序列图,然后保存报表。

    chart wizard 4
    Figure 28. 向导中的图表模板 - 第三步
  5. 最后,在报表编辑界面的 Templates 标签页配置图表的轴。

    chart wizard 5
    Figure 29. 图标模板配置

    要了解图表配置的更多细节,请参阅 图表文档

3. 创建报表

在系统中创建报表涉及两个相关元素:可视化展现模板和为报表提取的数据的描述。使用外部工具以 XLS(X)、DOC(X)、HTML 格式创建模板,并在报表设计界面中创建报表数据的描述。

根据模板和报表参数,生成的报表可以是 PDF、XLS(X)、CSV、DOC(X)、HTML、Chart、Table 或 Pivot table 格式。

报表数据结构既可以在报表设计器中通过创建带区、查询和其它元素来描述,也可以通过实现特定接口的 Java 类来实现。报表可以从用户或调用代码中获取参数。可以指定有权访问报表的用户,以及报表可以出现在哪些系统界面中。

报表生成器的主要组件如下图所示:

reporting
Figure 30. 报表生成器组件
  • YARG - 框架,这是报表生成器的核心。

  • Report Engine 将 YARG 集成到 CUBA 框架中,并提供其它功能,如报表访问权限和界面集成。

  • Report Designer 是用于描述和存储报表的工具。它包括用于存储报表描述和模板的基础设施,以及用于创建和管理报表的界面。

  • Report - 报表数据结构描述,包括 Band(报表带区)Dataset(输出到带区的数据集)

  • Report Template - 报表可视化展示模板。

3.1. 报表数据结构

报表编辑界面的 Report structure 标签页如下所述:

report structure
Figure 31. 报表数据结构

顶部包含用于输入常规报表属性的字段:

  • Name - 报表名称。该名称可以在 Localization 标签页中本地化。

  • Group - 报表组,用于在标准报表浏览界面中进行分组。

  • Default template - 报表输出模板.

  • System code - 可选代码,可用于在应用程序代码中标识报表。

报表数据结构的主要元素是带区树(band hierarchy) - Report bands

报表区有以下参数:

  • Band name - 报表中的唯一报带区名称。它必须只包含拉丁字母、数字和下划线。此外,如果带区名称以 header 开头,则其数据不会在表格输出中输出。

  • Orientation - 报表带区方向:HorizontalVertical交叉表。水平报表带区向下复制,垂直 - 向右,交叉区域 - 向右和向下复制为矩阵。水平报表带区可以包含子带区。

  • Parent band - 父带区。

每个报表带区包括一个或多个数据集。在运行报表时,数据集将转换为行的列表,其中每行都是包含键-值对的 map。报表中出现的带区的次数与其最长数据集中的行数一样多。字段名称在报表模板中指定,并在生成报表时替换为数据集中的相应值。在描述数据集时,可以使用报表的外部参数以及其它报表带区的字段 - 这样可以创建链接带区。

每个报表都有 Root 带区。可以在其中创建数据集并从其它带区引用字段,但不能在报表模板中使用 Root 带区。

Dataset name 列的值仅为方便用户。

Link field 用于合并来自一个带区内的多个数据集的数据。当单个查询或 Groovy 脚本无法满足报表行的整个数据时,可以使用它。

支持的数据集类型如下所述。

3.1.1. SQL 数据集

SQL 数据集是通过执行 SQL 查询而生成的。建议使用 as 运算符为查询结果字段使用别名。也建议将别名用双引号括起来,以防止 DBMS 进行可能的大小写转换:

select u.name as "userName", u.login as "userLogin"
from sec_user u

可以在查询中使用报表输入参数和父带区字段。参数应该用 ${} 中的名称来处理,例如 ${dateFrom}。父带区字段也可以类似地处理,通过在字段名称前面添加带区名称:${band1.field1}

下面是一个使用从 group 父带区获取的 groupId 参数和外部 active 参数的 SQL 查询示例:

select u.name as "userName", u.login as "userLogin"
from sec_user u
where u.group_id = ${group.groupId}
    and u.active = ${active}
    and u.delete_ts is null
Warning

SQL 查询中应手动包含条件以过滤掉软删除的记录。

默认情况下,SQL 查询在主数据库上执行。如果要查询 附加数据存储,请在 Data store 字段中设置其名称。

报表带区中的查询预处理

如果需要根据报表输入参数或父带区中的参数值动态修改 SQL/JPQL 查询,则可以使用 SQL 预处理。模板引擎允许使用 Groovy 修改 SQL/JPQ 查询。要激活它,请在报表带区编辑界面下方选中 Preprocess query as Groovy template 复选框。生成的查询将由 GStringTemplateEngine 处理,它可以访问:

  • 报表参数: ${<parameter_name>},

  • 父带区中的值: ${<band_name>.<parameter_name>}.

考虑以下示例:根据是否传递 createTs2 报表参数选择一个查询条件:e.create_ts < ${createTs2}e.create_ts < current_timestamp

在这种情况下,查询应如下所示:

select e.create_ts, e.id, e.vin from ref_car e
where
e.create_ts >= \${createTs1}
and
<% out << (createTs2 != null  ? 'e.create_ts < ${createTs2}' : 'e.create_ts < current_timestamp')%>

因此,如果未传递 createTs2 参数,则初始查询将转换为以下查询:

select e.create_ts, e.id, e.vin from ref_car e
where
e.create_ts >= \${createTs1}
and
e.create_ts < current_timestamp

如果传递 createTs2,则报表带区将使用以下查询:

select e.create_ts, e.id, e.vin from ref_car e
where
e.create_ts >= \${createTs1}
and
e.create_ts < ${createTs2}

3.1.2. JPQL 数据集

JPQL 数据集是通过执行 JPQL 查询而生成的。生成的查询字段必须使用 as 运算符提供别名。可以在 JPQL 查询中使用报表输入参数和父带区字段,类似于 SQL 查询。

下面是一个使用从 group 父带区获取的 groupId 参数和外部 active 参数的 JPQL 查询示例:

select u.name as userName, u.login as userLogin
from sec$User u
where u.group.id = ${group.groupId}
    and u.active = ${active}

JPQL 查询自动支持软删除并仅返回未删除的记录。

还可以通过选中报表区编辑界面下方的 Preprocess query as Groovy template 复选框来激活查询预处理

默认情况下,JPQL 查询使用映射到主数据库的实体。如果要查询 附加数据存储 中的实体,请在 Data store 字段中设置其名称。

3.1.3. Groovy 数据集

Groovy 数据集是通过执行 Groovy 脚本而生成的。该脚本返回 List<Map<String, Object>> 类型的对象。此列表的每个元素都是 Map<String, Object> 类型的对象 - 对应于一个数据集记录。

以下对象将被传递到脚本中:

  • dataManager - 提供 CRUD 功能的 com.haulmont.cuba.core.global.DataManager 类型的对象。例如:

    def book = dataManager.load(Book.class).id(bookId).view("book.edit").one;
  • metadata - com.haulmont.cuba.core.global.Metadata 类型的对象,提供对应用程序元数据的访问。例如:

    def metaClass = metadata.getClassNN('sec$User')
  • params - 外部报表参数 map。以下是获取参数值的示例:

    def active = params['active']
  • parentBand - 父带区是 com.haulmont.yarg.structure.BandData 类型的对象。此对象允许通过调用 getParameterValue() 方法获取父带区字段值,例如:

    def groupId = parentBand.getParameterValue('groupId')
  • persistence - com.haulmont.cuba.core.Persistence 类型的对象,允许获取数据源。

    默认情况下,使用主数据存储。要使用其它数据存储,请将其名称作为参数传递给 getDataSource() 方法:

    def sql = new Sql(persistence.getDataSource('myStore'))
    def rows = sql.rows('select e.name from SEC_GROUP e')
  • security - com.haulmont.cuba.core.global.Security 类型的对象,用于检查用户对系统中不同对象的访问权限。例如:

    if (security.isEntityOpPermitted(Book.class, EntityOp.READ) {
        ...
    }
  • timeSource - 用于获取当前时间的 com.haulmont.cuba.core.global.TimeSource 类型的对象。例如:

    def currentDate = timeSource.currentTimestamp()
  • transactional - 将一个应该在新事务中执行的闭包作为参数的方法。当前的 EntityManager 成为闭包参数。例如:

    transactional { em ->
        def query = em.createQuery('select g from sec$Group g')
        ...
    }

    下面是一个 Groovy 脚本的示例,该脚本通过父带区输出的组和 active 外部参数提取用户:

    def result = []
    transactional { em ->
        def query = em.createQuery('select u from sec$User u where u.group.id = ?1 and u.active = ?2')
        query.setParameter(1, parentBand.getParameterValue('groupId'))
        query.setParameter(2, params['active'])
        query.resultList.each { user ->
            result.add(['userLogin': user.login, 'userName': user.name])
        }
    }
    return result
  • userSession - 与当前通过身份验证的用户关联的 com.haulmont.cuba.security.global.UserSession 类型的对象。例如:

    def user = userSession.currentOrSubstitutedUser
  • userSessionSource - com.haulmont.cuba.core.global.UserSessionSource 类型的对象,用于获取当前用户会话对象。例如:

    def locale = userSessionSource.locale
Tip

可以使用 AppBeans 类的静态方法来访问中间层的任何 Spring bean,例如:

def myService = com.haulmont.cuba.core.global.AppBeans.get('sample_MyService')

3.1.4. 实体数据集

Entity 数据集由单行组成,使用单个实体实例的属性和与之相关的实体生成。

数据源由 Entity 类型的外部参数生成,必须在 Parameters and Formats 标签页中进行描述。Entity parameter name 字段中的值必须与参数名称匹配。

报表模板必须包含具有实体属性名称的字段。模板中使用的属性应列在特定的窗口中,使用 Select entity attributes 按钮打开此窗口。

3.1.5. 实体列表数据集

实体列表 数据集是使用实体实例列表生成的。

数据源是使用 实体列表 类型的外部参数生成,必须在 Parameters and Formats 标签页中进行描述。Entity parameter name 字段中的值必须与参数别名匹配。

报表模板必须包含具有实体属性名称的字段。模板中使用的属性应列在特定窗口中,使用 Entity attributes 按钮打开该窗口。

3.1.6. JSON 数据集

JSON 数据集是通过 JSON 数据生成的。可以从以下来源获取此数据:

  1. Groovy script

    用户提供的脚本应该以字符串形式返回 JSON 数据。

    例如:

    return '''
            {
              "items": [
                {
                  "name": "Java Concurrency in practice",
                  "price": 15000
                },
                {
                  "name": "Clear code",
                  "price": 13000
                },
                {
                  "name": "Scala in action",
                  "price": 12000
                }
              ]
            }
            '''
  2. URL

    报表引擎将对 URL 执行 GET HTTP 查询。

    例如:

    https://jsonplaceholder.typicode.com/users
  3. 字符串类型的参数

    必须在 Parameters and Formats 标签页中描述包含 JSON 数据的 String 类型的报表外部参数。

使用 JsonPath 查询获取 JSON 树。例如,可以使用 $.store.book[*] JsonPath 返回以下 JSON 树中的所有书籍:

{
    "store": {
    "book": [
            {
                "category": "reference",
                "author": "Nigel Rees",
                "title": "Sayings of the Century",
                "price": 8.95
            },
            {
                "category": "fiction",
                "author": "Evelyn Waugh",
                "title": "Sword of Honour",
                "price": 12.99,
                "isbn": "0-553-21311-3"
            }
    ],
    "bicycle": {
        "color": "red",
        "price": 19.95
    }
}
}

有关 JsonPath 表达式的更多详细信息,请参阅 http://goessner.net/articles/JsonPath/

Warning

报表输出的字段如果是 DateDateTimeTime 数据类型,则不支持 java.text.SimpleDateFormat 定义的格式。如果要设置正确的格式,需要写一个 Groovy 脚本。

如需这么做,可以切换到报表编辑器的 Parameters and Formats 标签页并打开formatter编辑器。比如,对于 bookPublication.dateTime 字段,Groovy 脚本如下:

import java.text.SimpleDateFormat

def simpleOldDateFormat = new SimpleDateFormat('yyyy-MM-dd HH:mm')
def simpleNewDateFormat = new SimpleDateFormat('dd/MM/yyyy HH:mm')
def oldDate = simpleOldDateFormat.parse(value)

return simpleNewDateFormat.format(oldDate)

3.2. 报表模板

可以在报表编辑界面的 Templates 标签页中为一个报表创建多个模板。必须在 Report structure 标签页中选择其中一个作为默认模板。

下面是添加模板的表单:

report template
Figure 32. 模板编辑器
  • Template code - 作为标识的模板代码。

  • Template file - 模板文件,从文件系统加载并与报表结构描述一起保存到数据库中。

  • Output type -报表输出类型。它应该根据输出格式对应表中描述的规则与模板文件类型相一致。

  • Output name pattern - 可选的文件名模板,将用于生成的报表的下载文件名。它可以是常量字符串,也可以包含报表参数变量,例如 ${header.authorName}.xlsx。可以创建具有若干参数和字符串拼接的更复杂的模板,模板中用到的参数可以在任何报表结构带区使用脚本创建,例如,${Root.title}.xlsx,其中 title 是在脚本中定义:

    [['title' : ('Report for '+params['author'].firstName+' '+params['author'].lastName)]]
  • Is custom - 表示格式化逻辑是自定义的,不是系统提供的格式化器。

  • Defined by - 自定义模板的定义方式:可以是class、脚本或 URL。

  • Custom definition - Java 类的完全限定名称、core 模块中 Groovy 脚本的路径或用于创建模板的 URL。

  • Is alterable output - 定义是否允许用户在运行报表时在对话框窗口中选择报表输出类型。

    如果此标志启用,则在运行报表时将显示输出类型选择对话框。如果报表包含多个模板,则还会显示模板选择的下拉列表。

    report template alterable
    Figure 33. 选择输出类型及模板

3.2.1. XLSX 和 XLS 模板

可以使用 Microsoft OfficeLibreOffice 创建 XLSX 和 XLS 模板。

每个报表带区必须在模板中具有相应的区域,该区域被命名为带区。例如,报表有两个区 - Header 和 Data。这意味着该模板还应具有 Header 和 Data 命名区域。要创建命名区域,请选择所需的单元格区域,然后在应用程序左上角的字段中输入名称。要编辑现有命名区域,请在 Microsoft Office 中使用 FormulasName Manager 菜单命令,在 OpenOffice 中使用 InsertNamesManage 命令。 反之亦然,每个要显示的工作表上的区域应该是报表中的一个带区(至少是一个空带区)。

报表带区以报表数据结构中指定的顺序输出。

报表带区可以是水平或垂直的。如果报表区是水平的,相应的命名区域将向下扩展,垂直,将向右扩展。水平报表带区可以以树状结构组织并包含子报表带区(嵌套或子报表带区)。因此,对于子报表带区,需要直接在与父报带表区对应的区域下创建命名区域。 XLSX 格式化器使用以下算法渲染子报表带区:

  • 渲染父报表带区的第一行 →

  • 渲染第一行的所有子行 →

  • 渲染父报表区的下一行。

带区数据集字段以 ${field_name} 格式放置在模板中,其中 field_name 是相应的报表带区字段名称。例如:

report template xls
Figure 34. XLS 文件模板

可以将变量添加到报表模板。变量应该以格式 ${<BandName>.<variableName>} 插入到 XLSX 模板的工作表名称或的页眉/页脚中。

单元格中可能包含格式以及多个字段。要输出图像或公式,需要将它们完全放入与报表带区链接的相应命名区域。

公式可以引用相同报表带区或另一报表带区的单元格。要由格式化程序处理,公式应该使用报表区中的单元格范围,或直接使用单元格坐标,例如,(A1*B1)($B:$B)

要将数据作为 Excel 图表处理,在报表结构中创建一个空报表区,并在模板中创建一个具有相同名称的命名区域。然后在此命名区域内创建一个图表,并使用图表右键菜单中的 Select data 按钮引用关联的报表数据区域。如果图表数据位于连续的单元格范围内,请选择该范围内的所有单元格。图表将包含该范围内的所有数据。如果数据不在连续范围内,请选择不相邻的单元格或范围。

将 XLSX 转换为 PDF 和 CSV

XLSX 报表可以自动转换为 CSV 和 PDF 格式。需要为 PDF 转换安装OpenOffice/LibreOffice

csv output
Figure 35. CSV Output

3.2.2. CSV 模板

可以使用 Microsoft OfficeLibreOffice 创建 CSV 模板。

CSV 模板中的报表区应该是水平方向的,因此相应的命名区域将向下扩展。此外,报表区应属于第一级数据,即根区的第一级子区。在所有其它方面,应使用与XLS/XLSX 模板相同的规则。

csv template
Figure 36. CSV 模板示例
csv report
Figure 37. CSV 输出

Inline editor

CSV 模板支持内联编辑。可以直接在 Template editor 窗口中编辑模板,无需重新上传模板文件即可查看更改。

csv report editor
Figure 38. CSV 内联编辑器

3.2.3. DOCX 和 DOC 模板

可以使用 Microsoft OfficeOpenOffice/LibreOffice 创建 DOC 和 DOCX 模板。

这些类型的模板可以包括文档文本和可选的一个或多个表格。文档文本输出任意区的第一行数据。在表格中,可以输出任意数量的报表带区行。

要在文档文本中放置字段,应该使用 ${band_name.field_name} 格式的字符串,其中 band_name 是带区名称,field_name - 带区字段的名称。

要将数据输出到表格中,应该将其链接到一个报表带区。这是通过在表格的第一个单元格中指定 ##band=band_name 来完成的,其中 band_name 是带区名。表格字段以 ${field_name} 格式被放置,其中 field_name 是与表格关联的带区的字段名称。可以使用带区名称前缀来访问其它带区的字段,与文档文本字段的相同。可以在单个表格单元格中输出多个字段。

DOCX 和 DOC 中的水平报表带区不能包含子区。如果需要子区,请使用 XLS(X)格式。

Warning

模板中表格必须包含一行或两行。如果表格有两行,则相应的带区字段必须在第二行。第一行应包含带有相应报表区名称的标记,如果需要,还可以包含静态文本或其它报表带区字段。

下面是一个模板示例,它输出一个由两个带区( BookAuthors )组成的报表。第一个带区输出书名和分类,第二个带区输出本书的作者列表。

report template doc
Figure 39. DOCX 模板
Warning

DOCX 和 DOC 模板不支持单元格数据格式化。要避免由于用户的语言环境而导致的数字或日期格式问题,例如不必要的数字分隔符,请尝试将数据转换为字符串。例如, 将

select e.year as "year"

转换为

select cast(e.year as varchar(4)) as "year"

3.2.4. HTML 模板

HTML 模板在 .html 文件(无 BOMUTF-8 编码)中定义。可以使用 Flying Saucer 库的 HTML/CSS 功能;其主要指南参考 http://flyingsaucerproject.github.io/flyingsaucer/r8/guide/users-guide-R8.html

要控制页面尺寸、页眉和页脚,请使用特殊的 CSS 规则和属性。可以在示例报表中找到每页都显示页面/页脚的报表示例。

有两种方法可以在模板中放入数据:

  • 使用 FreeMarker 标签。

  • 使用 Groovy 模板引擎。

默认情况下,报表向导会生成带有 FreeMarker 标签的 HTML 模板。

在模板编辑器中使用 Template type 单选按钮切换这两种方法。

html template editor
Figure 40. HTML 模板编辑器
Groovy 模板引擎

可以将 HTML 报表模板作为 Groovy 模板进行预处理,将使用 GStringTemplateEngine 来处理模板。

模板引擎使用 JSP 格式的 <% %> 脚本和 <%= %> 表达式语法或者 GString 格式的表达式。out 变量用来绑定模板将要被写入的 writer。因此,如果定义没有问题的话,模板可以使用任何 Groovy 代码。GStringTemplateEngine 能访问:

  • 外部参数:BandName.fields.ParamName

  • 区域:BandName.bands.ChildBandName

  • 字段:BandName.fields.FieldName

方便起见,也可以使用变量,例如:

<% def headerRow = Root.bands.HeaderRow %>
<p>Date: ${headerRow.fields.reportDate}</p>

下面是一个模板的示例,输出单一用户的报表。

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru">
    <head>
        <title> Report User </title>
        <style type="text/css">
 body {font-family: 'Charis SIL', sans-serif;} tbody tr {height:20px; min-height:20px}
        </style>
    </head>
    <body>
        <% def user = Root.bands.User %>
        <p>Login: ${user.fields.login.first()}</p>
        <p>Active: ${user.fields.active.first()}</p>
    </body>
</html>

可以在报表示例部分找到使用 Groovy 模板的示例。

FreeMarker

FreeMarker 文档可以参阅 https://freemarker.apache.org/docs/

FreeMarker 文档模型的结构如下:

Band {
      bands [ bandName : [ band, .. ], .. ]
      fields [ fieldName : fieldValue, .. ]
}

例如,应该使用以下表达式访问 band 带区中第一行数据的 name 字段:

Root.bands.band[0].fields.name

为方便起见,可以使用变量,例如:

<#assign headerRow = Root.bands.Header[0]>
<p>Date: ${headerRow.fields.reportDate}</p>

下面是一个模板示例,它输出一个由两个带区(BookAuthors)组成的报表。第一个区输出书名和分类,第二个区输出本书的作者列表。

<!doctype html>
<html>
<head></head>
<body>
    <#assign book = Root.bands.Book[0] />
    <#assign authors = Root.bands.Authors />

    <p>Name: ${book.fields.name}</p>
    <p>Genre: ${book.fields.literatureType.name}</p>
    <table border="1" cellpadding="5" cellspacing="0" width="200">
        <thead>
        <tr>
            <td>First name</td>
            <td>Last name</td>
        </tr>
        </thead>
        <tbody>
        <#list authors as author>
            <tr>
                <td>${author.fields.firstName}</td>
                <td>${author.fields.lastName}</td>
            </tr>
        </#list>
        </tbody>
    </table>
</body>
</html>

下面是一个更复杂的例子。假设我们有以下报表带区结构:

Root {
    HeaderBand {
        query = return [[ "name" : "Column1" ],[ "name" : "Column2" ]]
    }
    Band1 {
        query = return [
                ["field1" : "Value 11", "field2" : "Value 12"],
                ["field1" : "Value 21" , "field2" : "Value 22"]
        ]
    }
    Band2 {
        query = return [[ "header" : "Header1" ], [ "header" : "Header2" ]]
        SubBand1 {
            query = return [["header" : 'SubHeader1'] , [ "header" : 'SubHeader2' ]]
        }
    }
}
  • 插入字段:

<!doctype html>
<html>
    <head>
        <title> Simple template </title>
    </head>
    <body>
    <#assign Tree1 = Root.bands.Band2>
        <h1> Header </h1>
        <p>
            ${Tree1[1].bands.SubBand1[0].fields.header}
        </p>
    </body>
</html>
  • 插入列表:

<!doctype html>
<html>
    <head>
        <title> List </title>
    </head>
    <body>
    <#assign Table1Header = Root.bands.HeaderBand>

        <#if Table1Header?has_content>
            <ol>
                <#list Table1Header as header>
                    <li> ${header.fields.name} </li>
                </#list>
            </ol>
        </#if>
    </body>
</html>
  • 插入表格:

<!doctype html>
<html>
<head>
    <title> Table </title>
</head>
<body>
<#assign Table1Header = Root.bands.HeaderBand>
    <#assign Table1 = Root.bands.Band1>
        <table border="1" cellpadding="5" cellspacing="0" width="200">
            <thead>
            <tr>
                <#list Table1Header as header>
                    <td> ${header.fields.name} </td>
                </#list>
            </tr>
            </thead>
            <tbody>
            <#list Table1 as row>
                <tr>
                    <td>
                        ${row.fields.field1}
                    </td>
                    <td>
                        ${row.fields.field2}
                    </td>
                </tr>
            </#list>
            </tbody>
        </table>
</body>
</html>
  • 插入多级列表:

<!doctype html>
<html>
<head>
    <title> Multi-level list </title>
</head>
<body>
<#assign Tree1 = Root.bands.Band2>
    <ul>
        <#list Tree1 as item>
            <li>
                <h2> ${item.fields.header} </h2>
                <#if item.bands.SubBand1?has_content>
                    <ul>
                        <#list item.bands.SubBand1 as subitem>
                            <li>
                                <h3> ${subitem.fields.header} </h3>
                            </li>
                        </#list>
                    </ul>
                </#if>
            </li>
        </#list>
    </ul>
</body>
</html>
嵌入图片

目前,CUBA 报表扩展不提供类似于 DOCX/XLSX 报表将图片插入 HTML 报表的方法。图片仍然可以使用 img 标签并在 src 属性中指定图片链接的方式嵌入。有两种方法可以将图片添加到 HTML 报表中:

  • 通过 URL

    图片可以托管在 Tomcat 服务或者其它地方,以便用做本地文件引用。例如,托管在 deploy\tomcat\webapps\ROOT\images 文件夹中的图片可以被插入:

<img src="http://localhost:8080/images/SomeImage.jpg" height="68" width="199" border="0" align="right"/>
  • 通过 Bitmap

    图片在 src 属性中以字节数组形式添加。此方法可以使用实体的 FileDescriptor 属性变量。甚至可以将字节数组直接添加到模板中,虽然不建议使用此方法:

<img alt="SomePicture.png" src="data:image/png;base64,iVBORw0K ..... AcEP9PwxD0hNKK1FCAAAAAElFTkSuQmCC"/>
内联编辑器

HTML 模板支持内联编辑器。可以直接在 Template editor 窗口中编辑模板,无需重新上传模板文件即可查看更改。

html report editor
Figure 41. HTML 内联编辑器
3.2.4.1. 将 HTML 转换为 PDF

具有 HTML 格式和 PDF 输出格式的模板的报表容易显示出错误的字体。要解决此问题,请将带有所需 .ttf 字体的 cuba/fonts 子目录添加到中间层配置目录(默认部署配置中的 tomcat/conf/app-core)。此外,可以通过在reporting.fontsDir应用程序属性中指定其路径来使用现有的操作系统字体。

要解决 Ubuntu 服务器上的字体问题,应该执行以下操作:

  • 安装 ttf-mscorefonts-installer 包:

    $ sudo apt-get install ttf-mscorefonts-installer
  • 设置 reporting.fontsDir 应用程序属性:

    reporting.fontsDir = /usr/share/fonts/truetype/msttcorefonts
  • 在 HTML 模板中明确指定字体,例如:

<html>
<head>
    <style type="text/css">
 * { font-family: Times New Roman; }
    </style>

另外要提到的是解析特殊字符。为避免将 HTML 转换为 PDF 时出错,建议将字段封装在 HTML 模板文件的 <![CDATA[ ]]> 标签中:

<tr>
        <td> <![CDATA[${(row.fields('book_name'))!?string!}]]> </td>
        <td> <![CDATA[${(row.fields('author'))!?string!}]]> </td>
</tr>

3.2.5. JasperReports 模板

JasperReports 格式化器允许使用 JasperReports 模板输出 CUBA 报表提取的信息。模板将由 CUBA 报表引擎处理,支持定义的几种输出类型,请参阅输出格式对应表

可以使用 JasperReports 工具(例如,Jaspersoft Studio)或在简单的文本编辑器中创建 JRXML 模板。报表数据结构中定义的每个报表带区必须在模板 中有相应的 band 元素,模板中的 band 元素(在 JasperReports 术语中也称为带区)被放置在标准 JasperReports 报表区内:titlepageHeadercolumnHeaderdetail 等。

报表引擎将所有报表带区数据放在一个数据源中:JRBandDataDataSource,它以树形结构组织数据,Root 带区为根带区,并将 CubaJRFunction 实例作为主数据源传递给模板,可以通过参数 REPORTING 引用 CubaJRFunction。在报表模板中可以不声明此参数,这时它会被自动添加,但如果要在 JasperReports IDE 中编译模板,则需要显式声明此参数。

REPORTING 参数提供两个功能:

  • dataset - 从使用的主数据源获取子数据源,例如,在表格或子报表中作为子数据集。此方法在根带区的子节点中搜索具有指定名称的报表带区,并使用搜索到的报表带区数据作为新的根创建新数据源。例如:

    <subDataset name="Product">
            <field name="name" class="java.lang.String"/>
            <field name="price" class="java.lang.Long"/>
    </subDataset>
    ...
    <dataSourceExpression><![CDATA[$P{REPORTING}.dataset("Product")]]></dataSourceExpression>
  • bitmap - 将给定的字节数组转换为 ByteArrayInputStream,可用于将图片嵌入到报表中。例如:

<field name="Main.image" class="java.lang.Object"/> //image from DB as byte array
...
<imageExpression><![CDATA[$P{REPORTING}.bitmap($F{Main.image})]]></imageExpression>

每个报表带区只能在模板中使用一次,因此如果需要在一个报表中以不同的形式表示相同的数据(例如,作为表格和图表),需要创建与模板中 band 元素一样多的报表带区。不支持嵌套报表带区,所有带区都应该是 Root 区的直接子节点。

可以使用以下语法从数据源获取数据: $F{<field name>}。例如:

<textField>
    <textFieldExpression><![CDATA[$F{library_department_name}]]></textFieldExpression>
</textField>

可以在示例报表章节中找到使用 JasperReports 模板的报表示例。

Tip

如果应用程序使用 UberJAR 部署选项,请执行以下操作以使用 UberJAR 运行 JasperReports:

  • jasperreports-.jaryarg-.jar 复制到 Uber JAR 目录,

  • 在 Uber JAR 目录中创建一个文件 jasperreports.properties

  • 在此文件中添加 net.sf.jasperreports.compiler.classpath 属性,将复制的 JAR 的名称作为属性值,例如:

net.sf.jasperreports.compiler.classpath = jasperreports-6.4.1.jar;yarg-2.0-SNAPSHOT.jar

3.2.6. 类定义模板

当很难或不可能使用 SQL、JPQL 或 Groovy 选择数据时,可以使用类定义模板。例如,当报表是组合其它几个报表的结果时,就可以使用它们。

将定义模板的类放在 core 模块中,并实现 com.haulmont.yarg.formatters.CustomReport 接口。在类中,需要定义 createReport() 方法,该方法返回一个字节数组并接收以下输入参数:

  • report - com.haulmont.yarg.structure.Report 类型的报表描述。

  • rootBand - com.haulmont.yarg.structure.BandData 类型的根带区数据。

  • params - 外部报表参数的 map。

下面是一个简单的类定义模板的示例。它会创建一个 HTML 文档,显示从参数中获取的书籍的名称:

package com.sample.library.report;

import com.haulmont.yarg.formatters.CustomReport;
import com.haulmont.yarg.structure.BandData;
import com.haulmont.yarg.structure.Report;
import com.sample.library.entity.Book;
import java.util.Map;

public class BookReport implements CustomReport {
    @Override
    public byte[] createReport(Report report, BandData rootBand, Map<String, Object> params) {
        Book book = (Book) params.get("book");
        String html = "<html><body>";
        html += "<p>Name: " + book.getName() + "</p>";
        html += "</body></html>";
        return html.getBytes();
    }
}

在模板编辑界面中,选中 Is custom 复选框,在 Defined by 字段中选择 Class,并设置 custom definition 为 Java 类的完全限定名:

class defined template
Figure 42. 类定义模板

3.2.7. 图表模板

如果应用程序项目包含 charts 组件,则可以使用图表输出类型。生成的图表显示在 Web 应用程序的 ReportsShow Charts 界面中。

支持两种类型的图表:饼图和序列图,每种类型都有自己的一组参数。

饼图:

chart template pie
Figure 43. 饼图模板
  • Band name - 为图表提供数据的报表带区。

  • Title field - 将从中获取分段名称的字段。

  • Value field - 将从中获取分段值的字段。

  • Color field - 将从中获取分段颜色的字段。颜色值应在 web 格式中指定。如果未定义,将自动选择颜色。

  • Units -此文本将被追加到图例值后面。

序列图:

chart template serial
Figure 44. 序列图模板
  • Band name - 为图表提供数据的报表带区。

  • Category field - 将从中获取类别名称的字段。

  • Category axis caption - 横轴的标题。

  • Value axis caption - 纵轴的标题。

  • Value axis units - 纵轴值的单位。

必须为序列图添加至少一个行定义:

  • Value field - 将从中获取行值的字段。

  • Type - 行显示类型。

  • Color field - 将从中获取分段颜色的字段。颜色值应在 web 格式中指定。如果未定义,将自动选择颜色。

3.2.8. 数据透视表格式化器

如果应用程序项目包含 charts 组件,则可以使用数据透视表输出类型。有关数据透视表的更多信息,请参阅 Charts 手册

透视表输出只能为报表输出锦上添花,这也就是为什么在报表向导中没有透视表。要使用透视表格式化器,在报表编辑页面切换到 Templates 标签页,点击 Create 然后选择透视表作为新模板的输出类型。之后,按照下面的描述配置模板。

生成的表格会显示在 Web 应用程序的 ReportsShow Pivot Tables 界面中。

pivot template result
Figure 45. 透视表格式化器

报表生成器将获取报表带区数据并绘制具有拖放功能、聚合数据和汇总数据的表格。在数据透视报表中只能使用一个报表带区,不支持嵌套带区。

渲染器选项

此标签页上列出了可用的渲染器,选择一个默认渲染器。

pivot template renderer
Figure 46. Renderer Options 标签页
聚合选项

Aggregation options 标签页允许定义表格聚合器列表。聚合属性:

  • Mode 可以设置一个预定义的聚合函数,

  • Caption 要在 UI 中显示的本地化名称,

  • Custom function - 如果不为空,则忽略 Mode 值而使用自定义的 JavaScript 代码。

pivot template aggregation
Figure 47. Aggregation options 标签页
属性选项

数据透视表显示所有类型的数据集的的所有属性。 需要注意的是,SQL、JPQL 和 Groovy 数据集中的引用属性的别名不能包含句点,例如, `select u.name as "userName" `。所选属性应在模板编辑界面的 Properties options 标签页上被设置为数据透视表的属性:

  • RowColumn - 都是键值对字典,其中包含要在数据透视表列和行中使用的属性,其中键是数据集中属性的名称,值是其本地化标题。

  • Aggregation - 要对其值进行聚合的属性,

  • Derived property - 可用于向原始数据集添加新属性,这些属性派生自现有数据集。此元素是键值对,其中键是生成的属性的名称,值是生成此属性的 JavaScript 函数。

pivot template properties
Figure 48. Properties Options 标签页
自定义选项
  • Filter function - 用于过滤的 JavaScript 函数。

  • Sorters function - 用于行和列标题排序的 JavaScript 函数。

  • 渲染器配置(可配置内容取决于在渲染器选项中选择的渲染器)- 允许设置用于自定义渲染器渲染效果的 JavaScript 函数。实际上,只有两种类型的渲染器可以定制:

    • 各种 heatmap:可以通过 Javascript 代码设置单元格颜色,

    • 各种图表:选项可用于设置图表的尺寸。

pivot template custom properties
Figure 49. Custom Options

3.2.9. 表格格式化器(Formatter)

表格输出不需要报表模板,因为数据将显示在专用的应用程序界面。

要使用表格格式化器,请在报表模板编辑界面中选择 Table 作为输出类型。

report table output
Figure 50. 表格格式化器

报表生成器会获取报表带区数据,并从报表带区树的第一级为每个带区绘制一个可排序表格。

表格在列上显示 SQL、JPQL 数据集的所有属性。如果使用实体/实体列表数据集,则表格仅为选择的属性显示列。

结果表格可通过 Web 应用程序的 Reports > Show Report Table 界面查看。使用 Excel 按钮可以将显示的表格导出为 Excel 文件。

show report table
Figure 51. 查看报表表格

数据透视表显示所有类型的

3.2.10. 输出格式对应表

Template / Output XLSX XLS CSV DOCX DOC PDF HTML Chart

XLSX

+

+

+ 1

+ 1

XLS

+

+ 1

CSV

+

DOCX

+

+ 2

+ 2

DOC

+

+ 1

HTML

+

+

Chart

+

JRXML

+

+

+

+

+

+

+

1 - 必须安装 OpenOffice/LibreOffice 才能输出。

2 - 根据reporting.openoffice.docx.useOfficeForDocumentConversion应用程序属性,可以选择使用或不使用 OpenOffice/LibreOffice 执行输出。在后一种情况下,需要提供所需的字体,如将 HTML 转换为 PDF中所述。

3.3. 外部报表参数

外部参数在运行报表时从外部传递,可用作数据集中的条件。所有外部参数都会成为每个报表带区的字段,因此在模板中可像数据集字段一样直接使用外部参数。在数据集的字段名与参数名相同时,数据集字段值优先,报表将使用数据集字段值。

可以在报表编辑界面的 Parameters and Formats 标签页中定义外部参数。添加参数的表单如下:

report parameter
Figure 52. 外部报表参数

Properties 标签页:

  • Caption - 参数名称,在运行报表时这个名称显示在参数输入表单中。

  • Parameter alias - 用于在数据集中访问的参数别名。

  • Parameter type - 参数类型。

  • Hidden - 用于定义是否对用户隐藏此参数,隐藏后将不提示用户输入此参数。

  • Required parameter? - 是否是必须的参数。

  • Entity - 如果参数类型是 EntityList of entities ,则需要在此字段中选择一个实体类型。

  • Entity selection screen - 可选的界面标识符,用于选择实体实例。如果未指定界面,将使用通用的实体选择界面。

  • Enumeration - 如果指定了 Enumeration 参数类型,则需要在此字段中选择枚举类型。

  • Default value - 定义用户未选择值的情况下将使用的默认参数值。

  • Default date(time) is current - 如果指定了时间参数类型(DateTimeDate and time),此字段定义是否将当前时间戳用作默认参数值。

Localization 标签页中,可以为不同的区域设置定义参数名称。为此,应该使用 locale_name = parameter_name 键值对,例如:

ru = Книга
输入参数转换

Transformation 标签页中可以定义对参数进行处理的 Groovy 脚本,在报表中使用的将是经过处理的参数。

Groovy 脚本应该返回新的参数值。脚本中的当前参数值可以通过 paramValue 别名获取,参数 map 可以通过别名 params 获取。例如:

return "%" + paramValue + "%"

还可以使用预定义转换为文本(String)参数添加通配符:

  • Starts with,

  • Ends with,

  • Contains.

report parameter transformation
Figure 53. 输入参数转换

Validation 标签页中,可以使用 Groovy 脚本定义一些参数验证条件,请参阅下面的详细说明。

输入参数验证

可以验证输入参数和(或)定义交叉参数验证。

  1. 可以通过选中 Validate 复选框,在参数编辑界面的 Validation 标签页中启用参数验证。验证逻辑由 Groovy 脚本指定。脚本应检查参数值,如果值无效调用就 invalid() 方法。此方法会向用户显示一条警告信息,提示用户参数验证失败。

    以下变量会传递到脚本中:

    • value - 用户输入的参数值。

    • dataManager - 提供 CRUD 功能的 DataManager 类型的对象。

    • metadata - 提供对应用程序元数据的访问的 Metadata 类型的对象。

    • security - Security 类型的对象,用于检查用户对系统中不同对象的访问权限。

    • userSession - 与当前经过身份验证的用户关联的 UserSession 类型的对象。

  2. 输入参数验证 image::report_parameter_validation.png[align="center"]

  3. 通过选中 Parameters and Formats 标签页的 Cross parameters validation 部分的 Validate 复选框,可以启用交叉参数验证。验证逻辑由 Groovy 脚本指定。这个脚本应检查参数值之间是否合乎逻辑, 检查不通过时调用 invalid() 方法。此方法将向用户显示一条警告信息,其中包含在脚本中指定的提示内容。

    除了上面列出的变量之外,params 变量也会传递到脚本中以访问外部报表参数字典。

    cross parameter validation
    Figure 54. Cross-parameter validation

3.4. 字段值格式化

可以在报表编辑界面的 Parameters and Formats 标签页中为报表输出的任何字段指定格式。下面是添加格式的表单:

report formatter
Figure 55. 字段值格式
  • Value name - 带有报表带区前缀的报表字段名称,例如,Book.year

  • Format string - 格式化字符串。对于数字值,根据 java.text.DecimalFormat 的规则指定格式,对于日期值 - 根据 java.text.SimpleDateFormat 的规则指定格式。

  • Groovy script - 复选框。可以使用 Groovy 脚本来格式化参数。使用 value 别名,会将当前参数值传递给脚本,可以用来格式化或者转换成需要的格式。Groovy 脚本需要返回字符串类型的新值。

使用格式,可以将图像和 HTML 块插入到文档中。

  • 要插入图像,将图像 URL 指定为字段值,格式字符串必须是:${image:<Width>x<Height>},例如 ${image:200x300}

    要使用 FileDescriptor,可以使用 ${imageFileId:WxH} 格式,它接受 FileDescriptor idFileDecriptor 实例本身的链接。

  • 要插入 HTML 片段,应该在字段中返回 HTML 标记,并选择 ${html} 作为格式字符串。在输出值中,可以省略 <body> 以外的 的顶层标记。如有必要,将自动添加所有缺少的顶层标记。所有片段都应该用 UTF-8 编码。不支持 CSS 和 style 属性。

也可以指定自定义的格式。直接在控件中输入新的值而不必打开下拉列表,然后敲回车。也可以从下拉列表中选择任何格式,在控件中编辑名称然后敲回车。两种情况都会保存自定义的格式。

3.5. 报表访问权限

可以在报表编辑界面的 Roles and Screens 标签页中定义哪些用户可以访问报表,以及哪些界面可以使用报表。

如果报表角色列表中含有角色,则该报表仅对具有此角色的用户可用。如果未指定任何角色,则所有人都可以使用该报表。

Tip

注意,在报表查看器(Reports→Reports 菜单项)中,能看到所有的系统报表,因为该菜单是管理员菜单。

界面列表允许指定在调用 RunReportActionTablePrintFormActionEditorPrintFormAction actions时报表可用的界面。如果未指定任何界面,则报表在任何界面都不可用。

3.6. 报表名称本地化

可以本地化报表名称 - 在报表列表中以用户登录的语言显示报表名称。要做到这点,需要转到 Localization 标签页并输入键值对 locale_name = report_name ,例如:

en = Books by author
ru = Книги по автору

4. 运行报表

本节介绍如何运行创建的报表。

4.1. 从通用报表浏览界面运行

运行报表最简单的方式是使用通用报表浏览界面,通过 ReportsRun Reports 打开该界面。用户必须有权访问此界面。在报表浏览界面列出了对当前用户可用的所有报表。如果报表有外部参数,则在运行报表会显示一个表单来获取这些参数。

4.2. 从界面运行报表

可以使用特定操作(action)及相关的按钮或组件右键菜单从任意界面运行报表。在这种情况下,除了用户角色外,还将检查报表在此界面的可用性

下面提供操作(action)类型及其使用示例。

  • com.haulmont.reports.gui.actions.RunReportAction - 在列表中显示所有可用报表的操作。当用户从列表中选择报表时,将显示参数(如果已定义)输入表单并运行报表。

    示例:在界面 XML 描述声明了一个按钮,在按钮中调用此操作

    • XML-描述

      <layout>
          <groupTable id="booksTable">
              ...
              <buttonsPanel id="buttonsPanel">
                  ...
                  <button id="reportButton"
                          icon="PRINT"/>
              </buttonsPanel>
          </groupTable>
    • 控制器

      @Inject
      private Button reportButton;
      
      @Subscribe
      private void onInit(InitEvent event) {
          reportButton.setAction(new RunReportAction("report"));
      }
    • messages.properties

      report = Report
  • com.haulmont.reports.gui.actions.TablePrintFormAction - 这个操作可用在一个实体列表表格上。该操作仅选择具有 EntityList of entities 类型的外部参数的报表,并且参数的实体类型要与表格中显示的实体类型一样。如果只有一个这样的报表可用,会立即调用这个报表。如果多个报表可用,会显示一个列表供用户选择。

    使用以下规则将外部参数值传递给报表:

    • 如果参数是 List of entities 类型,会将表格中当前选择的实例列表传递给它。

    • 如果参数是 Entity 类型,并且在表格选择了单个实例(突出显示一行),则选中实例会传递到报表中。

    • 如果是 Entity 类型的参数,并且表中选择了多行,则报表会根据选择的实例数量运行多次。执行之后,用户将得到一个包含所有生成的报表的 ZIP 文件。

      下面是在表格按钮和右键菜单中使用操作(action)的示例:

    • XML 描述

      <layout>
          <groupTable id="booksTable">
              ...
              <buttonsPanel id="buttonsPanel">
                  ...
                  <button id="reportButton"
                          icon="PRINT"/>
              </buttonsPanel>
          </groupTable>
    • 控制器

      @Inject
      private Button reportButton;
      
      @Inject
      private GroupTable<Book> booksTable;
      
      @Subscribe
      private void onInit(InitEvent event) {
          TablePrintFormAction action = new TablePrintFormAction("report", booksTable);
          booksTable.addAction(action);
          reportButton.setAction(action);
      }
    • messages.properties

      report = Report
  • com.haulmont.reports.gui.actions.EditorPrintFormAction - 与实体编辑器界面关联的操作。该操作仅选择外部参数类型是 EntityList of entities 的报表,并且参数实体类型要与编辑的实体类型一样。如果只有一个这样的报表,则会立即调用它。如果多个,则会显示一个列表供用户选择。

    外部参数值 - 被编辑的实体实例被传递到报表中。如果参数是 List of entities 类型,则会传递一个包含单个条目的列表。

    下面是在一个按钮中使用这个操作的示例,这个按钮位于标准的 OKCancel 按钮旁边:

    • XML 描述

      <layout expand="editActions" spacing="true">
          ...
          <hbox id="editActions" spacing="true">
              <button action="windowCommitAndClose"/>
              <button action="windowClose"/>
              <button id="reportButton" icon="PRINT"/>
          </hbox>
      </layout>
    • 控制器

      @Inject
      private Button reportButton;
      
      @Subscribe
      private void onBeforeShow(BeforeShowEvent event) {
          reportButton.setAction(new EditorPrintFormAction("report", this, null));
      }
    • messages.properties

      report = Report

4.3. 取消报表

如果报表以后台任务的方式运行,那么用户就可以中断报表的执行。

要添加取消选项,请在 Administration > Application Properties 界面中设置 reporting.useBackgroundReportProcessing 属性。

reporting.useBackgroundReportProcessing = true

这样,报表在执行时会显示带有进度条和 Cancel 按钮的窗口:

run cancel
Figure 56. 取消报表

还可以使用 reporting.backgroundReportProcessingTimeoutMs 属性设置处理超时:

reporting.backgroundReportProcessingTimeoutMs = 30000

时间到了,无论结果如何任务都将被取消,用户会接收到错误信息:

run cancel 2
Figure 57. 报表错误

要以编程方式取消报表执行,可以使用 ReportService 接口的 cancelReportExecution() 方法,该方法需要用户会话和报表的标识:

reportService.cancelReportExecution(userSessionId, report.getId());

5. 示例报表

5.1. XLS 报表示例

在本章中,了解 Library 示例应用程序中的一个报表的结构,源代码可从 GitHub 获取。

首先,在 CUBA Studio 中打开 Project Properties 编辑器:点击 CUBAProject Properties 主菜单项。在页面上的 App components 列表中添加 reports 应用程序组件。然后启动应用。

打开 ReportsReports 界面,然后单击 Import 按钮导入报表。在项目根目录中选择 Reports.zip。在表格中将出现两个报表,其中一个是 "Books by author"。此报表显示所选作者的书籍出版物列表;书籍将按书名和出版商分组。输出格式为 XLS。

  1. 报表数据结构

    sample1 structure

    我们看看报表带区。

    • header 带区 - 报表标题。它包含带有 Groovy 脚本的数据集,该脚本输出报表外部参数值:

    [['authorName' : (params['author'].firstName + ' ' + params['author'].lastName)]]
    • book 带区通过执行以下 SQL 查询输出书籍列表:

    select b.name as book_name, b.id as book_id
    from library_book b
        join library_book_author_link ba on ba.book_id = b.id
        join library_author a on a.id = ba.author_id
    where a.id = ${author}

    此查询使用外部报表参数 - author。该参数是 Entity 类型,但在 SQL 查询中,可以直接将其与实体标识符字段进行比较;类型转换会自动完成。

    • publisher 带区是 book 的子带区,通过执行以下 SQL 查询输出图书出版商:

    select p.name as publisher, bp.year, p.id as publisher_id
    from library_book_publication bp
        join library_publisher p on p.id = bp.publisher_id
    where bp.book_id = ${book.book_id}

    此查询使用父带区字段 book_id 作为参数。以这种方式指定了父带区和子带区之间的依赖关系。

    • publication 带区是 publisher 带区的子项,通过执行以下 SQL 查询输出图书出版物:

    select ld.name as department, count(bi.id) as amount
    from library_book_instance bi
        join library_book_publication bp on bp.id = bi.book_publication_id
        join library_library_department ld on ld.id = bi.library_department_id
    where bp.publisher_id = ${publisher.publisher_id} and bp.book_id = ${book.book_id}
    group by ld.name

    此查询使用父带区字段作为参数 - book_idpublisher_id

  2. 报表参数

    Parameters and Formats 标签页包含一个声明的报表外部参数 - Author

    sample1 param

    运行报表时,用户必须输入此参数。作者是通过应用程序中的 library$Author.lookup 界面进行选择的。

  3. 报表模板

    Templates 标签页包含一个定义的 XLS 模板,从 BooksByAuthor.xls 加载

    sample1 template
  4. 报表名称本地化

    Localization 标签页包含俄语语言环境的报表名称:

    ru = Книги по автору

可以在 ReportsRun Reports 通用浏览界面中运行报表。

5.2. 交叉报表示例

要创建交叉报表,请在报表编辑界面的 Report structure 标签页上选择 Crosstab 带区方向。此方向将自动添加三个数据集到报表带区:

  1. <band_name>_dynamic_header - 这个数据集的数据将向右复制,类似于包含表格列标题的垂直带区。

  2. <band_name>_master_data - 这个数据集的数据向下复制,类似于包含表格行标题的水平带区。

  3. <band_name> - 与其所属带区名称相同的数据集。它是实现单元格矩阵的主内容带区。

这些数据集可以是任何可用的数据集类型: SQLJPQLGroovy 等等。

例如, Sales 示例应用程序的 Order 实体的交叉报表可能具有以下结构:

crosstab structure
Figure 58. 交叉报表
  • 这里,orders_dynamic_header 数据集将返回月份名称列表:

    orders_dynamic_header 数据集
    import java.text.DateFormatSymbols
    
    List result = new ArrayList()
    DateFormatSymbols dateFormatSymbols = DateFormatSymbols.getInstance(Locale.ENGLISH)
    for (i in 0..dateFormatSymbols.months.length - 1) {
        result.add(["header_id" : i + 1, "month_name" : dateFormatSymbols.months[i]])
    }
    return result
  • orders_master_data 数据集返回根据外部 user 参数选择的客户名称和标识符:

    orders_master_data 数据集
    select name as name, id as customer_id
    from SALES_CUSTOMER
    where id in (${selected_customers})
  • orders 数据集将提供单元格矩阵的数据,这个数据统计出特定用户在特定月份的订单金额总额。它将 orders_master_data@customer_id (客户 id)作为单元格的 Y-坐标,并将 orders_dynamic_header@header_id (月份名称)作为 X-坐标,使用 amount 值填充单元格矩阵。

    在下面的示例中,报表还有两个外部参数:start_dateend_date 用于定义订单日期的范围。通过 参数交叉验证 来确保参数值在合理的范围是一个很好的主意。

    orders dataset
    select
            o.customer_id as orders_master_data@customer_id,
            month(o.date_) as orders_dynamic_header@header_id,
            sum(o.amount) as "amount"
    from sales_order o
    where o.date_ >= ${start_date} and o.date_ <= ${end_date}
    and o.customer_id in (${orders_master_data@customer_id})
    and month(o.date_) in (${orders_dynamic_header@header_id})
    group by o.customer_id, month(o.date_)
    order by o.customer_id, month(o.date_)

最后可以使用 Microsoft OfficeLibreOffice 创建报表模板。

报表模板应包含交叉带区的所有三个数据集的命名区域以及列标题的命名区域: <band_name>_header。在这个例子中,它是 orders_header

下面是一个模板示例,它在垂直方向输出 客户 列表,在水平方向输出按下单日期汇总的每个月的 订单 金额。

crosstab template 2
Figure 59. 交叉表模板
crosstab names regions
Figure 60. 命名区域

这样,报表可以纵向和横向扩展,并汇总每个客户和每个月的订单金额:

crosstab result
Figure 61. 交叉表输出

如果想为报表添加总计,应该在单独带区中执实现,并且为这个带区定义汇总数据集。

5.3. JRXML 报表示例

该示例也是基于示例 Library 应用程序,源代码可在 GitHub 上获取。

首先,在 CUBA Studio 中打开 Project Properties 编辑器:点击 CUBAProject Properties 主菜单项。在页面上的 App components 列表中添加 reports 应用程序组件。然后启动应用。

打开 ReportsReports 界面,然后单击 Import 按钮导入报表。在项目根目录中选择 Reports.zip。在表格中将显示两个报表,其中一个报表显示每个图书类目中可用的书籍。这个报表显示选中的类目下的图书出版物列表;默认输出格式为 XLS。我们为此报表创建新的 JasperReports 模板。

  1. 报表数据结构.

    sample jasper

    报表带区:

    • Header 带区 - 报表标题。它是包含 Groovy 脚本的数据集,该脚本输出报表外部参数值:

      [['library_department_name' : params['library_department'].name]]
    • Data 带区通过运行以下 Groovy 脚本输出指定类目下的实例列表,这个类目根据传递的参数确定:

      import com.haulmont.cuba.core.global.AppBeans
      
      def persistence = AppBeans.get('cuba_Persistence')
      def tx = persistence.createTransaction()
      try {
          def result = []
      
          def em = persistence.getEntityManager()
          def ltList = em.createQuery('select lt from library$LiteratureType lt').getResultList()
          ltList.each { lt ->
              def count = em.createQuery(''' select count(bi) from library$BookInstance bi where bi.libraryDepartment = ?1 and bi.bookPublication.book.literatureType = ?2 ''')
                  .setParameter(1, params['library_department'])
                  .setParameter(2, lt)
                  .getSingleResult()
              def refCount = em.createQuery(''' select count(bi) from library$BookInstance bi where bi.libraryDepartment = ?1 and bi.bookPublication.book.literatureType = ?2 and bi.isReference = true''')
                  .setParameter(1, params['library_department'])
                  .setParameter(2, lt)
                  .getSingleResult()
      
              result.add(['literature_type_name': lt.name,
                  'books_instances_amount': count,
                  'reference_books_instances_amount': refCount])
          }
          return result
      } finally {
          tx.end()
      }

      此查询使用外部报表参数 - library_department。这个参数是 Entity 类型,但可以直接将其与实体标识符字段进行比较,类型转换会自动完成。

  2. 报表参数.

    Parameters and Formats 标签页包含一个声明的报表外部参数 - Department

    sample jasper 2

    运行报表时,用户必须输入此参数。部门是通过应用程序中的 library$LibraryDepartment.lookup 界面选择的。

  3. 报表模板

    Templates 标签页包含一个定义好的 XLS 模板,从 BookAvailability.xls 加载。

    使用以下内容创建新的 JRXML 文件:

    BookAvailability.jrxml
    <?xml version="1.0" encoding="UTF-8"?>
    <!-- Created with Jaspersoft Studio version 6.4.0.final using JasperReports Library version 6.4.1 -->
    <jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="books" pageWidth="595" pageHeight="842" whenNoDataType="AllSectionsNoDetail" columnWidth="535" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20">
        <property name="template.engine" value="tabular_template"/>
        <property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
        <style name="Table_TH" mode="Opaque" backcolor="#066990">
     <box> <topPen lineWidth="0.5" lineColor="#000000"/> <bottomPen lineWidth="0.5" lineColor="#000000"/> </box>
        </style>
        <style name="Table_CH" mode="Opaque" forecolor="#FFFFFF" backcolor="#06618F" hTextAlign="Center" fontSize="12">
     <box> <topPen lineWidth="0.5" lineColor="#000000"/> <bottomPen lineWidth="0.5" lineColor="#000000"/> </box>
        </style>
        <style name="Table_TD" mode="Opaque" backcolor="#FFFFFF" hTextAlign="Center">
     <box> <topPen lineWidth="0.5" lineColor="#000000"/> <bottomPen lineWidth="0.5" lineColor="#000000"/> </box>
        </style>
        <subDataset name="Data">
            <field name="literature_type_name" class="java.lang.String"/>
            <field name="books_instances_amount" class="java.lang.Long"/>
            <field name="reference_books_instances_amount" class="java.lang.Long"/>
        </subDataset>
        <field name="library_department_name" class="java.lang.String"/>
        <title>
            <band height="72">
                <frame>
                    <reportElement mode="Opaque" x="-20" y="-20" width="595" height="92" backcolor="#006699"/>
                    <staticText>
                        <reportElement x="20" y="10" width="555" height="30" forecolor="#FFFFFF"/>
                        <textElement textAlignment="Center">
                            <font size="20" isBold="true"/>
                        </textElement>
                        <text><![CDATA[Book availability in department]]></text>
                    </staticText>
                    <textField>
                        <reportElement x="20" y="50" width="555" height="30" forecolor="#FFFFFF"/>
                        <box>
                            <pen lineWidth="1.0" lineColor="#FFFFFF"/>
                            <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
                            <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
                            <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
                            <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
                        </box>
                        <textElement textAlignment="Center" verticalAlignment="Middle">
                            <font fontName="SansSerif" size="20" isBold="true"/>
                        </textElement>
                        <textFieldExpression><![CDATA[$F{library_department_name}]]></textFieldExpression>
                    </textField>
                </frame>
            </band>
        </title>
        <detail>
            <band height="204">
                <componentElement>
                    <reportElement x="0" y="4" width="555" height="200" forecolor="#FFFFFF">
                        <property name="com.jaspersoft.studio.layout" value="com.jaspersoft.studio.editor.layout.VerticalRowLayout"/>
                        <property name="com.jaspersoft.studio.table.style.table_header" value="Table_TH"/>
                        <property name="com.jaspersoft.studio.table.style.column_header" value="Table_CH"/>
                        <property name="com.jaspersoft.studio.table.style.detail" value="Table_TD"/>
                        <property name="net.sf.jasperreports.export.headertoolbar.table.name" value=""/>
                        <property name="com.jaspersoft.studio.components.autoresize.proportional" value="true"/>
                    </reportElement>
                    <jr:table xmlns:jr="http://jasperreports.sourceforge.net/jasperreports/components" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports/components http://jasperreports.sourceforge.net/xsd/components.xsd">
                        <datasetRun subDataset="Data">
                            <dataSourceExpression><![CDATA[$P{REPORTING}.dataset("Data")]]></dataSourceExpression>
                        </datasetRun>
                        <jr:column width="188">
                            <jr:columnHeader style="Table_CH" height="30">
                                <staticText>
                                    <reportElement x="0" y="0" width="188" height="30" forecolor="#FFFFFF"/>
                                    <box>
                                        <pen lineColor="#FFFFFF"/>
                                    </box>
                                    <textElement textAlignment="Center" verticalAlignment="Middle">
                                        <font fontName="SansSerif" size="12" isBold="true"/>
                                    </textElement>
                                    <text><![CDATA[Literature Type]]></text>
                                </staticText>
                            </jr:columnHeader>
                            <jr:detailCell style="Table_TD" height="30">
                                <textField>
                                    <reportElement x="0" y="0" width="188" height="30"/>
                                    <textElement textAlignment="Center" verticalAlignment="Middle">
                                        <font fontName="SansSerif" size="12"/>
                                    </textElement>
                                    <textFieldExpression><![CDATA[$F{literature_type_name}]]></textFieldExpression>
                                </textField>
                            </jr:detailCell>
                        </jr:column>
                        <jr:column width="186">
                            <jr:columnHeader style="Table_CH" height="30">
                                <staticText>
                                    <reportElement x="0" y="0" width="186" height="30" forecolor="#FFFFFF"/>
                                    <textElement textAlignment="Center" verticalAlignment="Middle">
                                        <font fontName="SansSerif" size="12" isBold="true"/>
                                    </textElement>
                                    <text><![CDATA[Book Amount]]></text>
                                </staticText>
                            </jr:columnHeader>
                            <jr:detailCell style="Table_TD" height="30">
                                <textField>
                                    <reportElement x="0" y="0" width="186" height="30"/>
                                    <textElement textAlignment="Center" verticalAlignment="Middle">
                                        <font size="12"/>
                                    </textElement>
                                    <textFieldExpression><![CDATA[$F{books_instances_amount}]]></textFieldExpression>
                                </textField>
                            </jr:detailCell>
                        </jr:column>
                        <jr:column width="181">
                            <jr:columnHeader style="Table_CH" height="30">
                                <staticText>
                                    <reportElement x="0" y="0" width="181" height="30" forecolor="#FFFFFF"/>
                                    <textElement textAlignment="Center" verticalAlignment="Middle">
                                        <font fontName="SansSerif" size="12" isBold="true"/>
                                    </textElement>
                                    <text><![CDATA[Reference Book Amount]]></text>
                                </staticText>
                            </jr:columnHeader>
                            <jr:detailCell style="Table_TD" height="30">
                                <textField isBlankWhenNull="false">
                                    <reportElement x="0" y="0" width="181" height="30" forecolor="#000000"/>
                                    <textElement textAlignment="Center" verticalAlignment="Middle">
                                        <font size="12"/>
                                    </textElement>
                                    <textFieldExpression><![CDATA[$F{reference_books_instances_amount}]]></textFieldExpression>
                                </textField>
                            </jr:detailCell>
                        </jr:column>
                    </jr:table>
                </componentElement>
            </band>
        </detail>
        <pageFooter>
            <band height="17">
                <textField>
                    <reportElement mode="Opaque" x="0" y="4" width="515" height="13" backcolor="#E6E6E6"/>
                    <textElement textAlignment="Right"/>
                    <textFieldExpression><![CDATA["Page "+$V{PAGE_NUMBER}+" of"]]></textFieldExpression>
                </textField>
                <textField evaluationTime="Report">
                    <reportElement mode="Opaque" x="515" y="4" width="40" height="13" backcolor="#E6E6E6"/>
                    <textFieldExpression><![CDATA[" " + $V{PAGE_NUMBER}]]></textFieldExpression>
                </textField>
                <textField pattern="M/d/yy">
                    <reportElement x="0" y="4" width="280" height="13"/>
                    <textFieldExpression><![CDATA[new java.util.Date()]]></textFieldExpression>
                </textField>
            </band>
        </pageFooter>
    </jasperReport>

    这个模板中的表格绑定到子数据集(subDataset)。title 元素直接使用 Header 区数据。可以在 JasperReports 可视化设计器中打开模板文件来查看报表布局。

    将新模板上传到应用程序,选择任何一种输出类型,并将其设置为默认值:

    sample jasper 3

运行一下报表以确定报表定义正确:

sample jasper 4
Figure 62. 报表结果

5.4. 带分页、页眉和页脚的 HTML/PDF 报表示例

假设要创建一个这样的报表:横向展示、每页上都显示页码和固定的页眉页脚,使用特殊的 CSS 规则和属性进行配置。输出格式是 HTML 并可以导出为 PDF。

此报表示例及其演示项目也可在 CUBA GitHub 上找到。

  1. 数据模型

    报表将显示有关 Client 实体的信息。它包含两个 String 属性:titlesummary,在报表结构中会使用它们。

    public class Client extends StandardEntity {
    
        @NotNull
        @Column(name = "TITLE", nullable = false)
        protected String title;
    
        @Lob
        @Column(name = "SUMMARY")
        protected String summary;
    
        ...
    }
  2. 创建报表

    创建一个没有参数的简单报表。使用 JPQL 查询所有 Client 实体的本地属性:titlesummary

    example html 1
  3. 报表模板.

    现在创建报表模板文件。在这里定义页眉和页脚块,页眉和页脚会在每页 PDF 都打印 。这需要使用特殊的 page-break-before: always CSS 属性。它会在每个 Client 信息块之前生成分页符。

    如下所示,使用 FreeMarker 语法将数据插入到模板中。请在此处查看完整的 FreeMarker 参考: https://freemarker.apache.org/docs/

    <body>
    <h1>Clients report</h1>
    
    <!-- Custom HTML header -->
    <div class="header">
        Annual Report of our Company
    </div>
    
    <!-- Custom HTML footer -->
    <div class="footer">
        Address: William Road
    </div>
    
    <#assign clients = Root.bands.Clients />
    
    <#list clients as client>
    <div class="custom-page-start" style="page-break-before: always;">
        <h2>Client</h2>
    
        <p>Name: ${client.fields.title}</p>
        <p>Summary: ${client.fields.summary}</p>
    </div>
    </#list>
    </body>
  4. CSS 规则

    将使用以下 CSS 代码来调整 PDF 页面显示:

    body {
        font: 12pt Georgia, "Times New Roman", Times, serif;
        line-height: 1.3;
    }
    @page {
        /* switch to landscape */
        size: landscape;
        /* set page margins */
        margin: 0.5cm;
        @top-center {
            content: element(header);
        }
        @bottom-center {
            content: element(footer);
        }
        @bottom-right{
            content: counter(page) " of " counter(pages);
        }
    }

    此 CSS 代码将设置页眉/页脚位置:

    div.header {
        display: block;
        text-align: center;
        position: running(header);
        width: 100%;
    }
    
    div.footer {
        display: block;
        text-align: center;
        position: running(footer);
        width: 100%;
    }

    之后,需要填充主要内容的边距以防止内容和页眉/页脚重叠:

    /* Fix overflow of headers and content */
    body {
        padding-top: 50px;
    }
    .custom-page-start {
        margin-top: 50px;
    }

    最终,完整的 paging-template.html 文件如下所示:

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>Invoice</title>
        <style type="text/css">
     body { font: 12pt Georgia, "Times New Roman", Times, serif; line-height: 1.3; padding-top: 50px; } div.header { display: block; text-align: center; position: running(header); width: 100%; } div.footer { display: block; text-align: center; position: running(footer); width: 100%; } @page { /* switch to landscape */ size: landscape; /* set page margins */ margin: 0.5cm; @top-center { content: element(header); } @bottom-center { content: element(footer); } @bottom-right { content: counter(page) " of " counter(pages); } } .custom-page-start { margin-top: 50px; }
      </style>
    </head>
    <body>
    <h1>Clients report</h1>
    
    <!-- Custom HTML header -->
    <div class="header">
        Annual Report of our Company
    </div>
    
    <!-- Custom HTML footer -->
    <div class="footer">
        Address: William Road
    </div>
    
    <#assign clients = Root.bands.Clients />
    
    <#list clients as client>
    <div class="custom-page-start" style="page-break-before: always;">
        <h2>Client</h2>
    
        <p>Name: ${client.fields.title}</p>
        <p>Summary: ${client.fields.summary}</p>
    </div>
    </#list>
    </body>
    </html>
  5. 上传模板文件并运行报表。

    example html 3

    正如所见,报表包含标题页、每个 Client 信息前都分页、每页都显示页眉和页脚。

    example html 2

5.5. 使用 Groovy 模板引擎的 HTML 报表

该示例是基于 Library 应用程序,其源码可以在 GitHub 找到。我们创建一个报表展示选中城市的图书出版物。输出格式是 HTML。

  1. 使用 JPQL 数据集创建报表:

    html groovy template structure
    Figure 63. 报表数据结构

    BookPublications 区域用来输出图书出版物列表,使用如下 JPQL 查询语句:

    BookPublications 数据集
    select
    book.name as "book",
    publisher.name as "publisher"
    from library$BookPublication e
    left join e.book book
    left join e.publisher publisher
     where e.town.id = ${city}

    这个查询使用了外部的报表参数 - city。该参数是 Entity 类型;但是,在 JPQL 查询语句中可以直接用来跟实体标识符进行比较;后台会自动做转换。

  2. 报表参数描述:

    Parameters and Formats 标签页声明了一个报表外部参数 – City

    html groovy template parameter
    Figure 64. 报表参数

    当运行报表时,用户必须输入该参数。城市的选择会通过 library$Town.browse 界面进行,这个界面在应用程序内是可用的。

  3. 创建一个报表模板

    Templates 标签页定义了一个 HTML 模板,使用 FreeMarker 标签默认生成。

    使用下面内容创建新的 HTML 文件:

    PublicationsTemplate
    <?xml version="1.0" encoding="utf-8"?>
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru">
       <head>
          <title> Publications by city </title>
          <style type="text/css">
     body {font: 12pt Georgia, "Times New Roman", Times, serif; line-height: 1.3; padding-top: 30px;} tbody tr {height:40px; min-height:20px}
          </style>
       </head>
       <body>
          <h1>Publications, published in <% out << "${Root.fields.city.name}"%></h1>
              <% def bookPublications = Root.bands.BookPublications.fields %>
          <table class="report-table" border="1" cellspacing="2" >
             <thead>
             <tr>
                <th>Book</th>
                <th>Publisher</th>
             </tr>
             </thead>
             <tbody>
                <% bookPublications.title.eachWithIndex{elem, index -> out << "<tr><td> ${bookPublications.book[index]} </td><td> ${bookPublications.publisher[index]} </td></tr>"}%>
             </tbody>
          </table>
       </body>
    </html>

    输入参数的值用来生成报表标题:${Root.fields.city.name}

    bookPublications 变量定义如下:

    <% def bookPublications = Root.bands.BookPublications.fields %>

    该变量在表格体中用来显示报表字段。

    <% bookPublications.title.eachWithIndex{elem, index -> out << "<tr><td> ${bookPublications.book[index]} </td><td> ${bookPublications.publisher[index]} </td></tr>"}%>

    上传新的模板,然后选择 HTML 输出类型,在 Template type 单选按钮组选择 Groovy template 并设置为默认:

    publicationsTemplate editor
    Figure 65. 报表模板编辑器

运行模板,确保其工作正常:

publications report
Figure 66. 报表结果

6. 报表 REST API

通用 REST API 为报表扩展提供以下功能:

  • 获取报表列表。

  • 获取特定报表的详细信息。

  • 运行报表并获取结果。

  • 获取 Swagger 文档。

REST API 使用 OAuth2 协议进行身份验证,并支持匿名访问。

要通过 REST API 提供报表,请在 Roles and Screens 标签页上选中 Visible for REST API 复选框:

visible for rest
Figure 67. Visible for REST API 复选框

下面提供了一些报表特定功能的一般描述。有关如何获取 OAuth 令牌和其它 REST API 功能的更多信息,请参阅 REST API 扩展文档

获取现有报表列表

可以使用以下 GET 请求获取现有报表列表:

/rest/reports/v1/report

例如:

GET http://localhost:8080/app/rest/reports/v1/report HTTP/1.1

Authorization: Bearer f5a2b4b1-a121-4563-9519-dd3c0b116689
Content-Type: application/json

响应体将包含标记为 Visible for REST API 的报表的简要信息:

{
    "id": "2dd27fbf-8830-416a-899f-339543f8f27a",
    "name": "Books by author"
},
{
    "id": "2f07c9fe-5d6d-48cf-876f-8c02ac1f6c3c",
    "name": "Book availability in department"
}
获取报表信息

使用以下 GET 请求获取指定报表的详细信息:

/rest/reports/v1/report/{id}

这里查询的最后一部分是报表标识符,例如:

GET http://localhost:8080/app/rest/reports/v1/report/2dd27fbf-8830-416a-899f-339543f8f27a HTTP/1.1

返回的 JSON 对象将包含报表的以下信息:

{
    "id": "2dd27fbf-8830-416a-899f-339543f8f27a",
    "name": "Books by author",
    "templates": [
        {
            "code": "DEFAULT",
            "outputType": "XLS"
        }
    ],
    "inputParameters": [
        {
            "name": "Author",
            "alias": "author",
            "type": "ENTITY",
            "required": true,
            "hidden": false,
            "entityMetaClass": "library$Author"
        }
    ]
}
运行报表

要运行报表,请发送以下 POST 请求:

/rest/reports/v1/run/{id}

这里查询的最后一部分是报表标识符,例如:

POST http://localhost:8080/app/rest/reports/v1/run/2dd27fbf-8830-416a-899f-339543f8f27a HTTP/1.1

报表参数在请求体中被传递:

{parameters: [{name: 'author',value: '4b3a21b0-d6b7-4161-b0b6-55f118fbaac5'}]}

要使用非默认模板打印报表,请在请求体中传递模板代码:

{template: 'Template_1', parameters: [{name: 'author',value: '4b3a21b0-d6b7-4161-b0b6-55f118fbaac5'}]}
获取 Swagger 文档

可以通过 GET 请求此地址获取报表扩展的完整 Swagger 文档:

http://localhost:8080/app/rest/reports/v1/docs/swagger.json

Appendix A: 安装和配置 OpenOffice

报表生成器使用 OpenOffice / LibreOffice 程序包来输出 PDF 和 DOC 格式的报表。需要在计算机上安装应用服务并进行配置。

在 Windows 上安装和配置 Openoffice

reporting.openoffice.path = C:/Program Files (x86)/OpenOffice.org 3/program

在 Windows 上安装和配置 LibreOffice

reporting.openoffice.path = C:/Program Files (x86)/LibreOffice 5/program

在 Ubuntu Server 上安装和配置 LibreOffice

  • 通过运行以下命令来安装 libreoffice 包,例如:

    $ sudo apt-get install libreoffice`
  • core 模块的 app.properties 文件中的reporting.openoffice.path应用程序属性中,指定 LibreOffice 的路径:

    reporting.openoffice.path = /usr/lib/libreoffice/program
  • 如果服务不是通过界面操作的方式安装,LibreOffice 启动时将出现错误,Caused by: java.awt.HeadlessException: No X11 DISPLAY variable was set, but this program performed an operation which requires it,或者只是停止运行而没有错误消息。要解决此问题,请设置reporting.displayDeviceUnavailable应用程序属性:

    reporting.displayDeviceUnavailable = true
  • 启动 LibreOffice 时,可以运行以下命令来诊断错误:

    $ strace -e trace=signal /usr/lib/libreoffice/program/soffice.bin --headless --accept="socket,host=localhost,port=8100;urp" --nologo --nolockcheck
Tip

对于使用 apt 安装 tomcat 的 Ubuntu 用户,需要将 ~/.config/libreoffice 复制到 $CATALINA_HOME。对于 tomcat8,这个目录是 /usr/share/tomcat8

之后,应该更改此文件夹的所有者:

sudo mkdir /usr/share/tomcat8/.config
sudo cp -pr ~/.config/libreoffice /usr/share/tomcat8/.config/
sudo chown -R tomcat8.tomcat8 /usr/share/tomcat8/.config/

在 macOS 上安装和配置 LibreOffice

reporting.openoffice.path = /Applications/LibreOffice.app

Appendix B: 应用程序属性

本节按字母顺序描述与报表生成器相关的应用程序属性。

reporting.backgroundReportProcessingTimeoutMs

reporting.useBackgroundReportProcessing 设置为 true 的情况下,用该参数定义报表执行过程超时的时限,单位为毫秒。

默认值: 10000

保存在数据库

用于 Middleware block。

reporting.displayDeviceUnavailable

允许在没有窗口界面的服务器操作系统中运行 OpenOffice/LibreOffice。

默认值: false

用于 Middleware block。

reporting.enableTabSymbolInDataSetEditor

定义是否应将 TAB 键作为 \t 符号处理,而不是在报表编辑界面的脚本字段中进行焦点切换。

默认值: false

用于客户端 block。

reporting.fontsDir

指定 HTML 转换为 PDF 时使用的字体目录的路径。

例如: reporting.fontsDir = C:/Windows/Fonts

用于 Middleware block。

reporting.docFormatterTimeout

设置 LibreOffice 转换 DOCX/XLSX 为 HTML/PDF 的超时时限,单位是秒。

当时间用完的时候,用户会收到一个错误消息。

默认值: 20

用于 Middleware block。

reporting.openoffice.docx.useOfficeForDocumentConversion

开启使用 OpenOffice 将含有 DOCX 模板的报表转换为 HTML/PDF 的功能,这样可以显著提高转换的准确性。

默认值: false

用于 Middleware block。

reporting.openoffice.path

设置 OpenOffice 的路径。

默认值: /

用于 Middleware block。

reporting.openoffice.ports

为 OpenOffice/LibreOffice 指定用逗号或竖直分隔符隔开的可用端口列表。

例如: reporting.openoffice.ports = 8100|8101|8102|8103|8104|8105.

默认值: 8100, 8101, 8102, 8103.

用于 Middleware block。

reporting.putEmptyRowIfNoDataSelected

设置当报表带区的数据集没有返回记录时是否仍然将带区显示一次。

默认值: true

用于 Middleware block。

reporting.useBackgroundReportProcessing

允许在后台运行报表执行程序。该属性是为了实现取消操作选项。

默认值: false

保存在数据库

用于 Middleware block。

. . .