Published at: 2025-10-30
HTML Template with APL for Custom Data Printing

1. Use Cases
1.1 Printing multi-level related object fields
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.

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

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.

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” } ] }
- 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 } } } ] }
- 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.

| 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”:

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

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” } ]