Published at: 2025-10-30
Custom Data Printing with HTML Templates and APL Integration
Notes
- This feature requires beta access: [Print templates now support APL functions]
- This documentation applies to PDF templates only
picture coming soon:
picture coming soon:
1. Use Case Examples
1.1 Printing Multi-level Related Object Fields
For order or Contract records, when printing line item details, you may need to display additional Product Attributes such as product category or specifications. For multi-variant Products, some attributes may require lookup through the SPU (Standard Product Unit). APL’s query capabilities enable outputting these multi-level related data fields.
Example scenario: Printing energy type (e.g., “Plug-in Hybrid”) for Contract line items by tracing:
Contract line item → SKU (Stock Keeping Unit, e.g., “Destroyer 05 2024 DM-i Glory Edition 1.5L 120km”) → SPU (Standard Product Unit, e.g., “Destroyer 05”).
1.1.1 APL Code
```java /** * @author admin01 * @codeName PrintContractLineItems * @description Prints contract product line items * @createTime 09/04/2024 */
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
```html
| Contract Line Item ID | Multi-spec | Product Name | Is Gift | Sales Price |
|---|---|---|---|---|
| ${item.name} | ${item.SPUObj.is_spec} | ${item.SKUObj.name} | ${item.SKUObj.isGiveawayLabel} | ${item.SKUObj.price} |
| Table rows will auto-populate based on actual data volume |
```
1.2 Printing Irregular Approval Data Tables
Current configurable style VS desired implementation style

1.2.1 Prerequisites
1.2.1.1 HTML Template Engine Syntax
HTML template engine syntax documentation
1.2.1.2 HTML Learning Resources
Creating irregular tables requires cell merging operations. Familiarity with HTML table tags and cell merging techniques is essential:
HTML Table Colspan and Rowspan
The Table Data Cell element
picture coming soon:
picture coming soon:
1.2.2 Case Explanation
For approval data display:
1. Add an instanceId parameter (String type) in APL to receive the process instance ID from the print template
2. APL provides a Map structure where:
- List data displays in tables
- Each record (objectData) represents a table row
- Each field value represents a cell
3. Query approval instances and calculate cell colspan/rowspan through grouping
4. Merged cells should have rowspan or colspan set to 0
picture coming soon:
1.3 APL-Based Cell Span Calculation
Example case requiring column merging for identical task names
1.3.1 Data JSON Format
json
{
"instanceList": [
{
"task_name": "Single Approval",
"reply_user": "lucy",
"action_type": "Approved",
"opinion": "ok",
"span": {
"task_name": {
"colspan": 1,
"rowspan": 2
}
}
},
{
"task_name": "Single Approval",
"reply_user": "tom",
"action_type": "Approved",
"opinion": "Yes",
"span": {
"task_name": {
"colspan": 1,
"rowspan": 0
}
}
},
{
"task_name": "Consensus Approval",
"reply_user": "scott",
"action_type": "Approved",
"opinion": "",
"span": {
"task_name": {
"colspan": 1,
"rowspan": 1
}
}
}
]
}
Implementation steps:
1. Group by task name
json
{
"Single Approval": [
{
"task_name": "Single Approval",
"reply_user": "lucy",
"action_type": "Approved",
"opinion": "ok"
},
{
"task_name": "Single Approval",
"reply_user": "tom",
"action_type": "Approved",
"opinion": "Yes"
}
],
"Consensus Approval": [
{
"task_name": "Consensus Approval",
"reply_user": "scott",
"action_type": "Approved",
"opinion": "done"
}
]
}
- Set
rowspanfor task_name field (group size for first record, 0 for others)
json
{
"Single Approval": [
{
"task_name": "Single Approval",
"reply_user": "lucy",
"action_type": "Approved",
"opinion": "ok",
"span": {
"task_name": {
"rowspan": 2
}
}
},
{
"task_name": "Single Approval",
"reply_user": "tom",
"action_type": "Approved",
"opinion": "Yes",
"span": {
"task_name": {
"rowspan": 0
}
}
}
],
"Consensus Approval": [
{
"task_name": "Consensus Approval",
"reply_user": "scott",
"action_type": "done",
"opinion": "",
"span": {
"task_name": {
"rowspan": 1
}
}
}
]
}
- Restore complete list under “instanceList”
json
{
"instanceList": [
{
"task_name": "Single Approval",
"reply_user": "lucy",
"action_type": "Approved",
"opinion": "ok",
"span": {
"task_name": {
"rowspan": 2
}
}
},
{
"task_name": "Single Approval",
"reply_user": "tom",
"action_type": "Approved",
"opinion": "Yes",
"span": {
"task_name": {
"rowspan": 0
}
}
},
{
"task_name": "Consensus Approval",
"reply_user": "scott",
"action_type": "Approved",
"opinion": "done",
"span": {
"task_name": {
"rowspan": 1
}
}
}
]
}
1.3.2 APL Code
```java /** * @author admin01 * @codeName PrintApproval * @description Parameter passing example * @createTime 07/08/2024 */ 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 // Approver 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) } // Approval 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’ } // Comments 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 relational data to calculate task_name row 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 print templates with conditional statements:
- Hide td elements where rowspan = 0
- Display and set rowspan attribute for others
picture coming soon:
```html
| Task Node | Approver | Result | Comments |
|---|---|---|---|
| ${entry.task_name} | ${entry.reply_user} | ${entry.action_type} | ${entry.opinion} |
| Task Node | Approver | Result | Comments |
|---|---|---|---|
| ${entry.task_name} | ${entry.reply_user} | ${entry.action_type} | ${entry.opinion} |
```
2. Summary
Through these examples, print templates + APL can theoretically display:
- Data directly/indirectly related to primary objects
- Sub-object records grouped by Record Type or other option fields
- Aggregated field values (sums, averages) with flexible record filtering
- Multi-level lookup fields (Custom Object A → B → … → Z)
3. Practical Exercises
3.1 Implementing Transposed Data Tables
Example: Convert from this orientation (regions as columns, quarters as rows):
picture coming soon:
To this orientation (quarters as columns, regions as rows):
picture coming soon:
3.2 Column Headers
json
[
"Sales by Region",
"Europe",
"Asia",
"North America"
]
3.3 Data
```json [