报表及文档生成入门

有时需要对 CUBA 应用程序进行增强,使其具备内置的报表、文档生成功能。 CUBA 报表 扩展支持多种文件格式的模板,包括 DOCXXLSXPDFHTML,同时也可以输出这些格式的文件。此外,这个扩展也包含管理界面,并且提供了以编程方式进行报表/文档处理的API。

本指南中,会展示使用报表扩展的各种方式,包括通过界面和以编程的方式使用。

将要构建的内容

本指南对 CUBA 宠物诊所 示例进行了增强,以演示可内嵌到 CUBA 应用程序的报表功能:

  • 病历 以 Word 文档的格式提供,可以包含指定宠物的相关信息(主体数据、就诊数据), 可以从宠物详情界面下载病历。

  • 可以从应该就诊历史浏览界面下载 最近就诊 报表。这个报表包含了最近一个月的就诊记录,可以导出为包含就诊记录、相关宠物及其主人信息的Excel工作表。

开发环境要求

您的开发环境需要满足以下条件:

下载 并解压本指南的源码,或者使用 git 克隆下来:

示例: CUBA 宠物诊所

这个示例是以 CUBA 宠物诊所项目为基础,而这个项目的基础是众所周知的 Spring 宠物诊所项目。CUBA 宠物诊所应用程序涉及到了宠物诊所的领域模型及与管理一家宠物诊所相关的业务流程。

这个应用程序的领域模型如下:

领域模型

主要的实体是 PetVisit。 Pet 到诊所就诊,就诊时(Vist) 会有一名兽医(Vet)负责照顾它。每个宠物都有主人,一个主人可以有多个宠物。一次就诊(Vist)即是一个宠物在主人的帮助下到诊所诊治的活动。

简介

在使用业务应用程序时文档生成是必不可少的功能,我们常常需要从系统中提取一些数据,然后将其导出为通用的格式,这样就可以与其它参与者(比如客户、 合作伙伴或其它系统)共享这些数据。

生成的文档可能是人类可读的格式,比如 PDF,也可能是作为其它系统的输入数据的格式,比如 CSV/Excel 格式或 Word 文档。

针对业务应用程序的这种需求,CUBA 通过 报表 扩展提供了专门的支持。

使用报表扩展可以定义格式化的文档模板,同时为模板定义要使用的数据。定义以声明式的方式完成,不需要写任何代码就可以生成报表/文档。开发人员、管理员和具有中级技能的业务用户都可以使用报表扩展。

示例用例

如上所述,应用程序中有很多处理文档/报表生成的用例。这里有一个示例列表:

  • 创建一个交易额排行前10的客户的Excel表

  • 给应用程序中的订单生成PDF格式的发票

  • 创建CSV格式的支持数据文件,用于与支付系统和BI系统交互

  • 显示一个畅销产品的列表,并且按产品类别分组

  • 创建一个 HTML 格式的最新产品报价文件,用于上传并发布到网站上

上述所有的需求 报表 扩展都可以处理。在下一节中将介绍整体概念,并实现一些用例。

模板 + 数据 = 文档

报表扩展的基本技术结构是基于文档模板和模板数据,模板和数据会被进行合并处理并生成最终的文档/报表。

文档模板

文档模板是一个文件,它用来定义目标文件的外观。模板中会包含占位符和特定的文档片断,报表 扩展可以获取模板文件并为其注入数据。

占位符可以像普通文档一样插入模板文件,这时就需要一个特定字符去标识出哪些文本是占位符。报表 扩展中定义占位符的方式: ${NAME_OF_PLACEHOLDER}

下面是一个文档模板示例。这个示例是一个 Excel工作表,本指南在后面会用到这个工作表。

excel document template example

这个模板中包含了常规的文本和表格中不同列的占位符,比如 ${pet.name}${pet.identificationNumber}

此外,也可以以期望的方式定义样式和调整模板。 对于 Word 和 Excel 文档,程序本身提供的格式化选项都可应用到模板文件。对于 HTML 文档,所有 CSS 功能都可以应用到目标文档。

模板文件的格式不一定要与输出文件的格式一致。比如可以使用WORD模板生成PDF文件。

数据源

报表使用的数据可以有多少来源。 在 CUBA 应用程序中直接从实体层获取数据是最便捷的方式。但是, 报表 扩展不限定于特定的数据源。可以使用以下数据源:

  • CUBA 实体 / JPQL

  • SQL

  • Groovy

  • JSON

多种数据源为给模板提供数据提供高度的灵活性。在一个模板中可以组合使用多种数据源。

报表配置

定义了数据源后并且使数据源与特定模板占位符通过称作 带区(band) 的概念连接起来,这时输出文档需要的配置就完成了。报表 扩展将所有的配置存储在 Report 实体,其中包含了数据源配置、模板定义以及其它配置信息。

report entity configuration screen

创建报表(Report)记录有两种主要的方式。

第一种方式是手动创建一个实例并且定义数据源及相关的 JPQL / SQL 查询 / groovy 脚本。

这种方式常常在需要定义非常复杂报表时用到,用这种方式定义报表需要开发人员具有编写 SQL/JPQL 语句,甚至编写 Groovy脚本的技能。所以这种方式适合于对应用程序的内部处理机制具有很深的理解的高级管理员。 这种方式也需要开发人员具备相关的领域模型知识和报表扩展的相关的技术概念,包括报表带区、报表参数等。

报表向导

第二个选项是使用可以视化的报表向导,向导可以指引用户选择各种选项。 报表向导中可以定义报表中要选择的实体的条件、要包含的属性等。另外它也可以根据选择的属性生成一个基本的模板,会自动为属性在模板中添加占位符。

report entity wizard 1
report entity wizard 2
report entity wizard 3

这种方式更适合于业务用户,因为它隐藏了复杂的技术细节,比如可以使用简单的配置界面来定义查询。对于简单的报表,报表向导可作为业务人员直接创建报表的主要 UI。

生成了带有合适的占位符的文档模板后,业务人员可以对这个生成的模板进行输出格式方面的调整。能熟练使用 Word 或Excel 的业务人员对这些格式应该很熟悉。 这种方式为非技术人员制作报表提供了可能性。

当然,对于技术精湛的开发人员,也可以使用这种方式来创建报表,或者基于自动生成的报表再进行手动调整,这会大大提高报表开发效率。

报表向导的一个限制是它只能生成基于CUBA实体层的报表/文档。不能 直接 创建基于 Groovy / SQL 脚本的报表。但是可以使用向导先生成一个报表,然后使用常规的管理界面来调整报表带区。

外部参数

可以给报表定义外部参数,这些外部参数可以由用户交互地方式设置,也可以由应用程序代码使用。这些参数常用于数据获取阶段。在 基于 JPQL/SQL 语句和 Groovy 脚本的数据带区中, 可以将参数值动态地注入查询语句或Groovy脚本。

报表配置中外部参数配置界面看起来是这样:

report entity configuration external parameters

有两种方式为外部参数输入值。第一种方式是使用用于收集参数的通用表单。在使用通用报表执行UI调用报表时会出现这种参数输入界面。下图是这种UI的一个示例:

report external parameters dynamic ui

病历

第一个要生成的报表是指定宠物的病历。这个报表上包含了宠物的基础信息及其诊疗信息。

这种情况下,文档模板应该是一个 Word 文档。但是,最终要输出PDF文件。

这个报表具有以下以下默认设置:

  • Report Entity(报表实体): Pet (petclinic_Pet)

  • Template Type(模板类型): DOCX

  • Report Name(报表名称): Patient Record

  • Report Type (报表类型): Report for single entity

定义属性

这个报表中要选择的宠物直接主数据与 recent-visits 报表相似,包括宠物(Pet)实体及关联的主人(Owner)实体的基础信息。

为了显示宠物实体相关的诊疗记录,需要创建另外一个数据带区。这个操作可以在报表向导中完成。

在定义了实体的直接属性后的第二步,就可以为报表创建额外的“区域(regions)”。这些区域会被转换成特定的数据带区(Data band)。

report entity wizard add tabular section

配置输出文档

这个报表的输出文件包含了一个通过报表向导默认生成的模板。因此可以在报表配置界会有一个对应的模板条目。

模板文件是 DOCX 时,输出类型被设置为 PDF

report configuration default template

另外,也可定义输出文档的文件名。文件名可以是静态常量文件名,也可以是编程方式配置的动态文件名。

在这里,对于名称为 "Horsea" 的宠物,文件名应该是: Patient Record - Horsea.pdf

要实现这一点,可以配置文件名模板,在模板中引用特定带区:${Root.title}.pdf

Root.title 指的是 Root 带区的 title 值。这里使用了 Groovy 脚本定义了一个数据集,其中包含了 title 属性值 :

  1. Root 数据带区中定义的数据集

def petName = params["entity"]["name"] (1)

return [
    ["title" : "Patient Record - $petName"] (2)
]
1 params 变量可以用来访问各种外部参数。params["entity"] 指向选择的宠物实例。
2 这段Groovy 脚本返回了一个字典列表,目标文件名使用 title 键名存储在字典中

基于 Groovy 脚本的数据集的最终用法如下所示:

report configuration report output title

使用 LibreOffice 将 Word 精确地转换为 PDF

要输出 PDF 文档,需要对应用程序配置进行一些调整。CUBA 利用了 LibreOffice 的转换功能来生成高质量的 PDF 文件。

LibreOffice 提供了一个用于转换各种文件格式(特别是 PDF文件生成)的API供其它程序调用。 如果操作系统安装了 LibreOffice,并且在 CUBA 应用程序中进行了正确的配置, 报表 扩展将使用这个 API来生成 PDF文件。

app.properties
reporting.openoffice.docx.useOfficeForDocumentConversion = true

## Unix:
# reporting.openoffice.path = /usr/lib/libreoffice/program

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

## MacOS:
reporting.openoffice.path = /Applications/LibreOffice.app/Contents/MacOS

首先需要启用 LibreOffice 的文档转换功能,另外需要配置 LibreOffice 的二进制文件路径。这些配置完成后,CUBA 就可以将 DOCXXSLX 模板渲染为 PDF 文件。

More information on the configuration can be found in the official Reporting docs: Appendix A: Installing and Configuring OpenOffice.

在官方报表文档中可以找到更多关于配置的说明: Appendix A: 安装和配置 OpenOffice

从宠物详情界面运行报表

有两种方式可以使用编程的方式从界面上运行报表。

这里介绍第一种。首先报表本身的配置决定它可以从哪些界面被调用,剩下的工作就是在控制器中仅仅声明一下要打印报表,报表扩展将确定在当前上下文中可用的报表。

报表本身的对应的配置部分是 “Roles and Screens ”。在这部分可以配置一个允许使用当前报表的界面列表。

report configuration roles and screens

在配置界面右侧的列表中加入了界面: petclinic_Pet.edit

在目标界面中加入下列代码用于调用报表:

pet-edit.xml
<hbox id="editActions" spacing="true">
    <button action="windowCommitAndClose"/>
    <button action="windowClose"/>
    <button id="patientRecordBtn"
            caption="msg://patientRecord"/> (1)
</hbox>
1 在默认的 save & close 按钮旁边添加了一个id为 patientRecordBtn 按钮。
PetEdit.java
import com.haulmont.reports.gui.actions.EditorPrintFormAction;

@UiController("petclinic_Pet.edit")
public class PetEdit extends StandardEditor<Pet> {

    @Inject
    protected Button patientRecordBtn;

    @Subscribe
    protected void onInit(InitEvent event) {
        patientRecordBtn.setAction( (1)
                new EditorPrintFormAction(this, null) (2)
        );
    }

}
1 在界面的 init 事件中配置打印按钮的操作
2 使用了 报表 扩展中的 EditorPrintFormAction 操作,它会自动搜索在当前界面可用的报表。

如果有多个可用的报表,将会弹出一个用于选择报表的窗口,用户可以选择要运行的报表。如果只有一个可用的报表,那么系统会自动运行这个报表,不需要用户再去选择。

最近就诊记录报表

第二个示例报表是 recent-visits 报表。这个报表需要可以直接从就诊记录浏览界面下载。这个报表中包含最近一个月的就诊记录,以 Excel 文件的方式导出,应该包含宠物及其主人的信息。

要满足这个需求,第一步通过报表向导创建一个报表。后续的主要设置是:

  • Report Entity (报表实体): Visit (petclinic_Visit)

  • Template Type (模板类型): XLSX

  • Report Name (报表名称): Vists by Period

  • Report Type (报表类型): Report for list of entities, selected by query

下一步是定义查询。报表向导允许用户通过界面以定义条件的方式设置查询。

在这个报表中应该查询最近一个月的就诊记录。但是报表扩展中没有“相对日期”的概念。因此,这段逻辑必须在调用报表的外部代码中实现。

这个报表本身允许指定日期范围参数也使得其更具通用性,这样带来的好处是此报表可以在其它场景中重用。

这个查询由 visitDate 属性的两个条件组成。属性值应该输出两个日期之间:"Visit Date Range Start(就诊起始日期)" and "Visit Date Range End(就诊结束日期)"。两个条件只有一点不同之处,对于第一个条件,“operation(比较操作符)” 是 greater or equal than: >=(大于或等于) ,第二个条件的 “operation(比较操作符)” 是 ‘smaller than: <(小于)’ 。With that, the boundaries of the range are expressed as conditions.

visit by period date range creteria

报表引擎对每个条件都会创建一个外部参数,这样就可以以编程的方式提供参数值,也可以由用户通过UI提供。

Visit 实体属性的选择

下一步是在向导中定义基础实体(Visit)的属性和其相关的需要显示在报表上的实体属性。选择的属性会产生两个影响,一个是影响用于获取数据的 JPQL 语句的 SELECT 部分。另一个是影响向导生成的模板中的列。

对于 recent-visits 报表,应该选择下列属性:

  • Visit Date (就诊日期): visitDate

  • Visit Description (就诊描述): description

  • 宠物相关信息:

    • 宠物名称: pet.name

    • 宠物身份编号: pet.identificationNumber

    • 宠物类型: pet.type.name

    • 宠物生活的城市 (即其主人生活的城市): pet.owner.city

通过向导界面选择了属性后,就可以配置输出模板的文件类型。在这个示例中,默认配置就可以。

使用报表向导完成报表创建后会显示出报表配置界面,这时可以进行进一步调整。

以编程的方式提供外部参数

recent-visits 报表用例中,没有给用户暴露设置参数的通用 UI 。而是通过调用 API 以编程的方式传递参数。

patient-record 报表中,报表与界面的关联是在报表配置中进行的。

之前讲过有两种运行报表的方式,这个示例中将演示第二种方式。 在这个示例中,会以编程的方式选择报表实例(通过 system code 属性)然后直接传递给报表 API,以此来执行报表并下载文档。

执行 recent-visits 报表的主要步骤:

  1. 标识出最近一个月 (通过外部代码来计算相对日期)

  2. 加载要执行的报表实例

  3. 执行报表实例并传递外部参数

VisitBrowse.java
public class VisitBrowse extends StandardLookup<Visit> {

    @Inject
    protected ReportGuiManager reportGuiManager;

    @Inject
    protected DataManager dataManager;

    @Inject
    protected TimeSource timeSource;

    @Subscribe("visitsTable.lastMonthReport")
    protected void onVisitsTableLastMonthReport(
            Action.ActionPerformedEvent event
    ) {

        TimeRange visitDateTimeRange =
                MonthYearValue
                        .fromDate(today())
                        .minusMonths(1); (1)

        reportGuiManager.printReport( (2)
                loadReportByCode("visits-by-period"), (3)
                ParamsMap.of( (4)
                        "visitDateRangeStart", visitDateTimeRange.getStart(),
                        "visitDateRangeEnd", visitDateTimeRange.getEnd()
                        )
        );
    }

    private LocalDate today() {
        return timeSource.now().toLocalDate();
    }

    private Report loadReportByCode(String reportCode) {
        return dataManager.load(Report.class)
                .query("select e from report$Report e where e.code = :reportCode")
                .parameter("reportCode", reportCode)
                .one();
    }

}
1 标识出基于当前日期的最近一个月
2 reportGuiManager.printReport API 用于启动报表执行并立即下载文档
3 需要从数据库中加载正确的报表实例。这里根据报表实例的 code 属性值来查找
4 传递一个参数字典给报表,其中包含了起始日期和结束日期

这个报表中的外部参数传递中主要的部分是对最近一个月的定义:

`TimeRange visitDateTimeRange = MonthYearValue.fromDate(today()).minusMonths(1);`

这里的 TimeRange 类是这个示例程序中的一个自定义类,提供了定义一个日期范围 (可以是年月范围和季度范围)的 API ,也包含了获取范围值的方法。

定义了参数值后,可以将参数值传递给 reportGuiManager.printReport ,它可以接收一个字典类型的参数。

字典中的参数键名必须与报表实体中配置的参数的 parameterAlias 值匹配。

总结

在这个关于报表和文档生成的介绍中,介绍了CUBA的 报表 扩展的两个主要组成部分:文档模板和数据。报表允许将文档模板和来自数据源的数据进行合并,并产生输出文档。

文档模板和数据源都支持多种类型,这样在配置报表运行输出文档时就有具有了高度的灵活性。

所有的配置保存在主实体: Report 中,报表配置可以通过可视化的向导创建,也可以通过配置UI手动创建。两种创建方式可以支持不同技术水平的用户。

报表可以通过通用用户界面直接运行,也可以通过代码以编程的方式调用。可以给报表定义外部参数,外部参数需要传递给报表。有了参数,报表可以配置地更通用以适应多种用例。

本指南中创建了两个报表。第一个报表使用了PDF生成,用于创建宠物病历。这个报表使用了界面来声明性地配置报表可以在哪些界面中使用。

第二个报表生成了一个包含最近一个月就诊记录的Excel工作表。在这个示例中使用了外部参数支持动态地指定报表数据的日期范围。