打印模版HTML结合APL实现自定义数据打印

提示说明
1.需申请灰度:【打印模板支持插入APL函数】2.本文档只适用PDF模版

一、场景案例

1.1 打印多级关联一个对象的字段

对于一个订单类、合同类的对象数据,我们在打印明细数据的时候,想要输出一些明细中关联的产品的一些其他属性,比如产品的种类,产品的规格,假设产品是多规格产品可能一部分属性需要关联SPU(标准化产品单元)才能输出,但是在打印模板可能无法直接通过明细数据获取到产品这些的属性的。借助 APL 的查询数据能力,我们可以完成这种多级关联数据的输出。
比如我们想要打印合同产品明细关联SKU(库存单元,比如驱逐舰05 2024款 DM-i 荣耀版 1.5L 120km)关联SPU(标准化产品单元,比如驱逐舰05)的能源类型(比如:插电混动)。即 合同产品明细 lookup SKU lookup SPU。

1.1.1 APL Code

/** * @author admin01 * @codeName 打印合同产品明细 * @description 打印合同产品明细 * @createTime 2024-09-04 */ List details = context.details.SaleContractLineObj as List List skuIds = [] details.each { entry -> Map detail = entry as Map String skuId = detail.get("product_id") skuIds.add(skuId) } def fqa = FQLAttribute.builder().columns(["_id", "name", "price" , "is_giveaway", "picture_path", "spu_id"]).build() def sa = SelectAttribute.builder().build() List prodctList = Fx.object.findByIds("ProductObj", skuIds, fqa, sa).result() as List Map skuId2SpuIdMap = prodctList.collectEntries { [(((Map) it)._id): ((Map) it).spu_id] } Map skuId2Map = prodctList.collectEntries { [(((Map) it)._id): ((Map) it)] } log.info(skuId2Map) def spuIds = skuId2SpuIdMap.values() as List def fqa2 = FQLAttribute.builder().columns(["_id", "name", "is_spec"]).build() def sa2 = SelectAttribute.builder().build() List spuList = Fx.object.findByIds("SPUObj", spuIds, fqa2, sa2).result() as List Map spuMap = spuList.collectEntries { [(((Map) it)._id): ((Map) it)] } log.info(spuMap) details.each { entry -> Map detail = entry as Map String skuId = detail.get("product_id") Map skuData = skuId2Map.get(skuId) String isGiveaway = skuData.get("is_giveaway") String isGiveawayLabel = "" if ("1" == isGiveaway) { isGiveawayLabel = "是" } else if ("0" == isGiveaway) { isGiveawayLabel = "否" } else { isGiveawayLabel = "未知" } skuData.put("isGiveawayLabel", isGiveawayLabel) detail.put("SKUObj", skuData) String spuId = skuId2SpuIdMap.get(skuId) as String Map spu = spuMap.get(spuId) as Map boolean isSpec = spu.get("is_spec") as Boolean Map spuData = [:] if(isSpec){ spuData.put("is_spec", "多规格") } else { spuData.put("is_spec", "单规格") } detail.put("SPUObj", spuData) } Map map = [:] map.put("MySaleContractLineObj", details) return map

1.1.2 Template Source Code

<p> <!-- #set(MyList = pts_ZhFaS__c.MySaleContractLineObj) --> </p> <table> <thead> <tr class="firstRow"> <th> <span class="variable-label">销售合同明细编号</span> </th> <th> <span class="variable-label">是否多规格</span> </th> <th> <span class="variable-label">产品名称</span> </th> <th> <span class="variable-label">是否赠品</span> </th> <th> <span class="variable-label">单价</span> </th> </tr> </thead> <tbody> <!--#for(item : MyList)--> <tr> <td>${item.name}</td> <td>${item.SPUObj.is_spec}</td> <td>${item.SKUObj.name}</td> <td>${item.SKUObj.isGiveawayLabel}</td> <td>${item.SKUObj.price}</td> </tr> <!--#end--> </tbody> <tfoot> <tr> <td>表格行数会根据实际数据量自动加载</td> </tr> </tfoot> </table> <p> <br /> </p>

1.2 打印异形表格形式的审批数据

目前可配置出的样式 VS 希望实现的样式

1.2.1 前置知识

1.2.1.1 HTML 模板引擎语法

HTML 模板引擎语法说明

1.2.1.2 HTML 学习资料

异形表格实现,需要进行合并单元格操作,需要对 HTML 的 table 标签有一定的基础知识,知晓 HTML 的 table 是如何实现合并单元格的,请先学习HTML Table如何进行合并的。

HTML Table Colspan and Rowspan
The Table Data Cell element

1.2.2 案例说明

对于审批数据,需要通过打印模版显示传入,对 APL 中添加一个 instanceId 的参数,类型为 String,即可在执行函数是接收来自打印模版指定的流程实例编号(最终是由Web页面传入的)。
APL 提供了一个 Map,对于列表数据使用表格来进行展示,对每一条记录(objectData或record)都是表格中的一行,每一条记录的一个具体字段值都是要一个APL 查询到审批实例的列表,然后对这列表通过分组计算出每个单元格的 colspan 和 rowspan,如果单元格被合并,那么需要对其的 rowspan 或者 colspan 设置为 0。

1.3 APL 基于某种规则计算出每个值对应单元格的 rowspan 和 colspan

下面就是一个需要对相同的任务名称进行列合并的案例

1.3.1 Data JSON Format

{ "instanceList": [ { "task_name": "单人审批", "reply_user": "lucy", "action_type": "通过", "opinion": "ok", "span": { "task_name": { "colspan": 1, "rowspan": 2 } } }, { "task_name": "单人审批", "reply_user": "tom", "action_type": "通过", "opinion": "Yes", "span": { "task_name": { "colspan": 1, "rowspan": 0 } } }, { "task_name": "会签审批", "reply_user": "scott", "action_type": "通过", "opinion": "", "span": { "task_name": { "colspan": 1, "rowspan": 1 } } } ] }

那么如何进行计算呢,请看下面代码案例:
1.分组,即将任务名称相同的数据放一起。

{ "单人审批": [ { "task_name": "单人审批", "reply_user": "lucy", "action_type": "通过", "opinion": "ok" }, { "task_name": "单人审批", "reply_user": "tom", "action_type": "通过", "opinion": "Yes" } ], "会签审批": [ { "task_name": "会签审批", "reply_user": "scott", "action_type": "通过", "opinion": "done" } ] }

2.给每条记录设置字段 task_name 设置 rowspan,每组第一条设置为整个列表的大小,其他的设置为 rowspan 为 0。

{ "单人审批": [ { "task_name": "单人审批", "reply_user": "lucy", "action_type": "通过", "opinion": "ok", "span": { "task_name": { "rowspan": 2 } } }, { "task_name": "单人审批", "reply_user": "tom", "action_type": "通过", "opinion": "Yes", "span": { "task_name": { "rowspan": 0 } } } ], "会签审批": [ { "task_name": "会签审批", "reply_user": "scott", "action_type": "done", "opinion": "", "span": { "task_name": { "rowspan": 1 } } } ] }

3.再将其恢复为整个列表,并设置到一个 "instanceList"下。(因为本案例,借助了浅拷贝,直接返回最初的 printList即可)

{ "instanceList": [ { "task_name": "单人审批", "reply_user": "lucy", "action_type": "通过", "opinion": "ok", "span": { "task_name": { "rowspan": 2 } } }, { "task_name": "单人审批", "reply_user": "tom", "action_type": "通过", "opinion": "Yes", "span": { "task_name": { "rowspan": 0 } } }, { "task_name": "会签审批", "reply_user": "scott", "action_type": "通过", "opinion": "done", "span": { "task_name": { "rowspan": 1 } } } ] }

1.3.2 APL Code

/** * @author admin01 * @codeName 打印打印 * @description 怎么传参数 * @createTime 2024-07-08 */ Map map = [:] def (Boolean err, List instanceList, String errMsg) = Fx.approval.findTasks(instanceId) List printList = [] instanceList.each { inst -> List opinions = ((Map) inst).opinions as List opinions.eachWithIndex { opin, i -> Map newOpin = [:] // 审批节点名称 newOpin.task_name = ((Map) inst).task_name // 处理人 String replyUser = ((List) ((Map) opin).reply_user)[0] def (Boolean err1, Map userInfo, String errMsg1) = Fx.org.findUserById(replyUser) if(!err1){ newOpin.reply_user = userInfo.full_name } else { log.info(errMsg1) } // 处理结果 String type = ((Map) opin).action_type if(type == 'agree'){ newOpin.action_type = '通过' } else if(type == 'reject'){ newOpin.action_type = '驳回' } else if(type == 'cancel'){ newOpin.action_type = '取消' } // 处理意见 newOpin.opinion = ((Map) opin).opinion // 审批节点名称需要进行合并,初始化 colspan、rowspan Map spanTaskName = [task_name: [colspan: 1, rowspan: 1]] newOpin.span = spanTaskName printList.add(newOpin) } } // 上面生成了一个标准的关系性数据,需要将其分组计算得出需要对 task_name 合并行 Map groupedByTaskName = printList.groupBy { ((Map) it).task_name }.collectEntries { [(it.key): (it.value)] } groupedByTaskName.each { entry -> def taskList = entry.value as List<Map> if (taskList.size() <= 1) return taskList.eachWithIndex { task, i -> Map span = ((Map) task).span as Map Map taskNameSpan = span.task_name as Map if (i == 0) { taskNameSpan.rowspan = taskList.size() } else { taskNameSpan.rowspan = 0 } } } map.put('instanceList', printList) return map

1.3.3 Template Source Code

打印模版配置,结合 if 语句,对 rowspan 为 0 的 td 进行隐藏,大于 0 的显示,并为 td 设置 属性 rowspan 对应的值。

<p> <br /> </p> <table width="100%"> <thead> <tr class="firstRow"> <th>任务节点名称</th> <th>处理人</th> <th>处理结果</th> <th>审批意见</th> </tr> </thead> <tbody> <!--#for(entry:pts_sNsPA__c.instanceList)--> <tr> <td>${entry.task_name}</td> <td>${entry.reply_user}</td> <td>${entry.action_type}</td> <td>${entry.opinion}</td> </tr> <!--#end--> </tbody> </table> <p> <br /> </p> <table width="100%"> <thead> <tr class="firstRow"> <th>任务节点名称</th> <th>处理人</th> <th>处理结果</th> <th>审批意见</th> </tr> </thead> <tbody> <!--#for(entry:pts_sNsPA__c.instanceList)--> <tr> <!-- #if (entry.span.task_name.rowspan != 0) --> <td rowspan="${entry.span.task_name.rowspan}">${entry.task_name}</td> <!-- #end --> <td>${entry.reply_user}</td> <td>${entry.action_type}</td> <td>${entry.opinion}</td> </tr> <!--#end--> </tbody> </table> <p> <br /> </p>

二、总结

通过学习以上的案例,理论上可以使用打印模版 + APL 可以打印任何的与本体对象数据直接有关的和无关的数据,比如打印从对象记录时根据不同的业务类型(record_type)、或者其他的选项类型字段分组展示到多个表格中,比如对列表记录的某一个字段进行聚合(求和、求平均)设置隐藏一些列表中记录(这远比使用打印模版的筛选条件更加灵活),比如多级关联的字段值(自定义对象A -> 自定义对象B -> ... -> 自定义对象Z,打印 A - Z的字段值都可以)。

三、实践

3.1 实现输出一个行列倒置的数据

例如,如果数据如下所示,则列标题为“销售区域”,左侧为“季度”:

使“季度”显示在列标题中,并在左侧可以看到“销售区域”,如下所示:

3.2 标题头

[ "Sales by Region", "Europe", "Asia", "North America" ]

3.3 数据

[ { "quarter": "Qt 1", "eu": "21704714", "as": "8774099", "na": "12094215" }, { "quarter": "Qt 2", "eu": "17987034", "as": "12214447", "na": "10873099" }, { "quarter": "Qt 3", "eu": "19485029", "as": "14536879", "na": "15689543" }, { "quarter": "Qt 4", "eu": "22567894", "as": "15763492", "na": "17456723" } ]
2025-05-26
0 0