Published at: 2025-10-30

HTML Template with APL for Custom Data Printing


Notice
1. Gray release required: [Print template supports inserting APL functions] 2. This document applies to PDF templates only

image image

1. Use Cases

When printing line-item data for order or Contract records, you might want to display additional attributes of the related Products, such as product category or specifications. For multi-spec Products, some attributes may belong to the SPU (standardized product unit) rather than the SKU (inventory unit). A print template cannot always fetch those attributes directly from line detail data. Using APL’s data query capability, you can output multi-level related fields.

For example, you want to print Contract line items where the Contract Line references an SKU (Inventory unit, e.g., “Destroyer05 2024 DM-i Honor 1.5L 120km”) and that SKU references an SPU (standardized product unit, e.g., “Destroyer05”) whose energy type is “plug-in hybrid”. In other words: Contract line -> lookup SKU (Products) -> lookup SPU.

1.1.1 APL Code

/** * @author admin01 * @codeName Print Contract Line Items * @description Print Contract line items with related product/SPU attributes * @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 = “Yes” } else if (“0” == isGiveaway) { isGiveawayLabel = “No” } else { isGiveawayLabel = “Unknown” } 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”, “Multi-spec”) } else { spuData.put(“is_spec”, “Single-spec”) } detail.put(“SPUObj”, spuData) }

Map map = [:] map.put(“MySaleContractLineObj”, details)

return map

1.1.2 Template Source Code

Contract Line Number Multi-spec Product Name Gift Sales Price
${item.name} ${item.SPUObj.is_spec} ${item.SKUObj.name} ${item.SKUObj.isGiveawayLabel} ${item.SKUObj.price}
Table rows will load according to actual data volume


1.2 Printing irregular-shaped approval tables

Current configurable styles vs. the desired style.

image

1.2.1 Prerequisites

1.2.1.1 HTML template engine syntax

https://lexiangla.com/docs/d36c4e0c6a9111ef9308627909d46e03?company_from=050524eee61811e783175254005b9a60#%E6%9D%A1%E4%BB%B6%E8%AF%AD%E5%8F%A5-if-elseif-else

1.2.1.2 HTML references

Implementing irregular tables requires cell merging; please review how HTML table merging works: https://www.geeksforgeeks.org/html-table-colspan-and-rowspan/ https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td

image image

1.2.2 Case explanation

For approval data that must be displayed by the print template, add an instanceId parameter (String) to the APL function so the function receives the workflow instance ID specified by the print template (passed from the Web page). APL returns a Map. For list data displayed in a table, each record (objectData or record) maps to one table row. For each field that needs merged cells, APL queries the approval instance list, groups the list to compute colspan and rowspan for each cell. When a cell is merged and should not render, set its rowspan or colspan to 0.

image

1.3 APL computes rowspan and colspan for each cell based on rules

Below is an example where identical task names are merged column-wise.

1.3.1 Data JSON Format

{ “instanceList” : [ { “task_name” : “Single Approver”, “reply_user” : “lucy”, “action_type” : “Approved”, “opinion” : “ok”, “span” : { “task_name” : { “colspan” : 1, “rowspan” : 2 } } }, { “task_name” : “Single Approver”, “reply_user” : “tom”, “action_type” : “Approved”, “opinion” : “Yes”, “span” : { “task_name” : { “colspan” : 1, “rowspan” : 0 } } }, { “task_name” : “Joint Approval”, “reply_user” : “scott”, “action_type” : “Approved”, “opinion” : “”, “span” : { “task_name” : { “colspan” : 1, “rowspan” : 1 } } } ] }

How to compute: see code example below. 1. Group records by task_name.

{ “Single Approver”: [ { “task_name”: “Single Approver”, “reply_user”: “lucy”, “action_type”: “Approved”, “opinion”: “ok” }, { “task_name”: “Single Approver”, “reply_user”: “tom”, “action_type”: “Approved”, “opinion”: “Yes” } ], “Joint Approval”: [ { “task_name”: “Joint Approval”, “reply_user”: “scott”, “action_type”: “Approved”, “opinion”: “done” } ] }

  1. Set rowspan for each record’s task_name: the first record in each group gets rowspan equal to the group size; the rest get rowspan = 0.

{ “Single Approver”: [ { “task_name”: “Single Approver”, “reply_user”: “lucy”, “action_type”: “Approved”, “opinion”: “ok”, “span”: { “task_name”: { “rowspan”: 2 } } }, { “task_name”: “Single Approver”, “reply_user”: “tom”, “action_type”: “Approved”, “opinion”: “Yes”, “span”: { “task_name”: { “rowspan”: 0 } } } ], “Joint Approval”: [ { “task_name”: “Joint Approval”, “reply_user”: “scott”, “action_type”: “Approved”, “opinion”: “done”, “span”: { “task_name”: { “rowspan”: 1 } } } ] }

  1. Flatten back to the full list and set under “instanceList” (this example returns the original printList via shallow copy).

{ “instanceList”: [ { “task_name”: “Single Approver”, “reply_user”: “lucy”, “action_type”: “Approved”, “opinion”: “ok”, “span”: { “task_name”: { “rowspan”: 2 } } }, { “task_name”: “Single Approver”, “reply_user”: “tom”, “action_type”: “Approved”, “opinion”: “Yes”, “span”: { “task_name”: { “rowspan”: 0 } } }, { “task_name”: “Joint Approval”, “reply_user”: “scott”, “action_type”: “Approved”, “opinion”: “done”, “span”: { “task_name”: { “rowspan”: 1 } } } ] }

1.3.2 APL Code

/** * @author admin01 * @codeName Print Approval Instances * @description How to pass parameters * @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 = [:] // approval node name newOpin.task_name = ((Map) inst).task_name // handler 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) } // result String type = ((Map) opin).action_type if(type == ‘agree’){ newOpin.action_type = ‘Approved’ } else if(type == ‘reject’){ newOpin.action_type = ‘Rejected’ } else if(type == ‘cancel’){ newOpin.action_type = ‘Canceled’ } // opinion newOpin.opinion = ((Map) opin).opinion // initialize colspan/rowspan for task_name merging Map spanTaskName = [task_name: [colspan: 1, rowspan: 1]] newOpin.span = spanTaskName printList.add(newOpin) } }

// Group and compute rowspan for task_name merging 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

Configure the print template with conditional statements: hide td when rowspan == 0; show td when > 0 and set td rowspan attribute accordingly.

image


Task Node Name Handler Result Opinion
${entry.task_name} ${entry.reply_user} ${entry.action_type} ${entry.opinion}


Task Node Name Handler Result Opinion
${entry.task_name} ${entry.reply_user} ${entry.action_type} ${entry.opinion}


2. Summary

From the above cases, you can use print templates + APL to print data related or unrelated to the primary object. Examples: - Print child (Sub-object) records grouped into multiple tables by Record Type or other picklist fields. - Aggregate a field in a list (sum, average), or hide certain list records (more flexible than template filters). - Retrieve multi-level related field values (custom object A -> custom object B -> … -> custom object Z) and print fields from A through Z.

3. Practice

3.1 Produce a transposed table (rows <-> columns)

For example, given the following data where column headers are “Sales by Region” and the left column is “Quarter”:

image

Make “Quarter” appear as row headers and “Sales by Region” as column headers; the left side shows regions:

image

3.2 Headers

[ “Sales by Region”, “Europe”, “Asia”, “North America” ]

3.3 Data

[ { “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” } ]

Submit Feedback