Business description
In our company, we have a simple decision process. Each expenditure needs to be accepted by a responsible person or member of some group. Who will be that person? It depends on the type of that cost, cost center and its value:
In that case, there are two possible cost types: investment and operational. InvestAdmin approves each investment, so there is only one condition and suited result in the first row. In the next rows, cost types are blank, so that parameter is ignored. For IT Department there are two possibilities - costs with higher amount (equal or higher than 20000) are approved by one of the IT Directors, any IT Manager approves cheaper. In the end - every non-investment cost created by Board is approved by one of the board members.
All expenditures are declared via JIRA. To report costs, the concerned create new issues. After checking all data, he can proceed with a task and the approver should be designated automatically.
Solution Brief
In a workflow, we need to add a new scriptrunner post-function. It will read a dedicated decision table and, based on it, assign to the responsible people.
Example Decision Table
Script
Note
In this script, we use some superclasses, which help us in code management. For more information, see the Usage of base groovy classes and utilities page.
import com.onresolve.scriptrunner.runner.customisers.WithPlugin import global.fields.CustomFieldCode import global.fields.FieldUtils import global.fields.SystemFieldId @WithPlugin([ "eu.rivetgroup.atlas.rvg-jira-app-base-plugin", "eu.rivetgroup.atlas.rvg-jira-decision-tables-plugin" ]) class TestCostDecisionTablePostFunction extends BaseDecisionTableWorkflowFunction { TestCostDecisionTablePostFunction(Map<String, Object> transientVars, boolean validatorMode) { super(transientVars, validatorMode, false) } @Override protected boolean executeInternal() { def costTypeField = issueFieldOperator.forField(FieldUtils.getCustomField(CustomFieldCode.COST_TYPE)) def costCentreField = issueFieldOperator.forField(FieldUtils.getCustomField(CustomFieldCode.COST_CENTRE)) def amountField = issueFieldOperator.forField(FieldUtils.getCustomField(CustomFieldCode.AMOUNT)) def groupAssigneeField = issueFieldOperator.forField(FieldUtils.getCustomField(CustomFieldCode.GROUP_ASSIGNEE)) def assigneeField = issueFieldOperator.forField(FieldUtils.getSystemField(SystemFieldId.ASSIGNEE)) issueFieldOperator.checkRequired(costTypeField, costCentreField, amountField); def query = [ "costType": costTypeField.getBusinessKey().first(), "costCentre": costCentreField.getBusinessKey().first(), "amount": amountField.getObjectValue() def dtResult = decisionTableManager.getDMNDecisionTableOperations( "costDecisions", "costDecision" ).executeQuery(query); issueFieldOperator.getConfig().setCurrentExecutionInfo(dtResult.executionContextInfo) Map<String, ?> dtResEntry = dtResult.singleNonEmpty(); String approverGroupName = dtResEntry.get("approverGroup") String approverUser = dtResEntry.get("approver") groupAssigneeField.setValueByBusinessKey(approverGroupName) if (approverUser != null) { assigneeField.setValueByBusinessKey(approverUser) } return true; } }
Explanation
Let's look at the script line by line.
@WithPlugin([ "eu.rivetgroup.atlas.rvg-jira-app-base-plugin", "eu.rivetgroup.atlas.rvg-jira-decision-tables-plugin" ])
Firstly, there is an import of required functionalities from add-ons.
class TestCostDecisionTablePostFunction extends BaseDecisionTableWorkflowFunction
Begin of PostFunction class. As you see, it extends BaseDecisionTableWorkflowFunction
, which is described later.
TestCostDecisionTablePostFunction(Map<String, Object> transientVars, boolean validatorMode) { super(transientVars, validatorMode, false) }
A constructor provides required data about workflow.
def costTypeField = issueFieldOperator.forField(FieldUtils.getCustomField(CustomFieldCode.COST_TYPE)) def costCentreField = issueFieldOperator.forField(FieldUtils.getCustomField(CustomFieldCode.COST_CENTRE)) def amountField = issueFieldOperator.forField(FieldUtils.getCustomField(CustomFieldCode.AMOUNT)) def groupAssigneeField = issueFieldOperator.forField(FieldUtils.getCustomField(CustomFieldCode.GROUP_ASSIGNEE)) def assigneeField = issueFieldOperator.forField(FieldUtils.getSystemField(SystemFieldId.ASSIGNEE))
References to related fields. We strongly suggest excluding such relations to the external file, which will help you get a higher quality.
Thanks to them, we can get possible operations for all fields needed to call decision tables and process the result. Field operations allow taking no care about the particular field type in many scenarios - code is much less complex and easier to read.
issueFieldOperator.checkRequired(costTypeField, costCentreField, amountField)
Next, there is added validation, if necessary fields have values. Without them, operations could be impossible.
def query = [ "costType": costTypeField.getBusinessKey().first(), "costCentre": costCentreField.getBusinessKey().first(), "amount": amountField.getObjectValue() ]
Build the decision table query - to declared fields in the decision table, we add values. Cost Type and Cost Center are the single choice options, so we provide the first BusinessKey (current option value). The amount is numerical, so we use ObjectValue.
def dtResult = decisionTableManager.getDMNDecisionTableOperations( "costDecisions", "costDecision" ).executeQuery(query);
Execution of query on the decision table. To get the Decision Table Operation instance, we use "costDecisions" as a name of the Decision Table file (with .dmn extension) and "costDecision" as an id of decision table element inside the DMN model because DMN can contain multiple decision tables.
issueFieldOperator.getConfig().setCurrentExecutionInfo(dtResult.getExecutionContextInfo())
Set the context related to the executed decision table. Thanks to that, we have info about the decision table name or query, which helps handle any errors.
Map<String, ?> dtResEntry = dtResult.singleNonEmpty()
Next, we get an entry as a map with output parameters from the result. Here we use singleNonEmpty
because we expect a single entry from the table. Other cases should be reported as incorrect.
String approverGroupName = dtResEntry.get("approverGroup") String approverUser = dtResEntry.get("approver") groupAssigneeField.setValueByBusinessKey(approverGroupName) if (approverUser != null) { assigneeField.setValueByBusinessKey(approverUser) } return true
At the finish, we fill related fields in the issue with obtained values. We use the method setValueByBusinessKey because it ensures us if important conditions are checked. For more information, see Technical documentation.
Additionally, we return true
because the post-function works properly.
Attachments