Page tree
Skip to end of metadata
Go to start of metadata

Business description

In our company we have a simple decision process. Each expenditure needs to be accepted by responsible person or member of some group. Who will be that person? It depends on type of that cost, cost center and its value:

In that case there are two possible cost types: investment and operational. Each investment is approved by InvestAdmin, so at first row there is only one condition and suited result. In the next rows cost type 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, cheapers are approved by any IT Manager. At the end - every non-investment costs created by Board are approved by one of the board members.

All expenditure are declared via JIRA. To report costs, the concerned create new issue. After checking all data he can proceed a task and the approver should be designated automatically.

Solution Brief

In workflow, we need to add new scriptrunner postfunction. It will read 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 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 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, it 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 to exclude such relations to external file, what 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 allows to 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 decision table we add values. Cost Type and Cost Center are the single choice options, so we provide the first BusinessKey (current option value), amount is a numerical, so we use ObjectValue.

def dtResult = decisionTableManager.getDMNDecisionTableOperations(
	"costDecisions",
	"costDecision"
).executeQuery(query);

Execution of query on the decision table. To get Decision Table Operation instance we use "costDecisions" as a name of Decision Table file (with .dmn extension) and "costDecision" as an id of decision table element inside DMN model, because DMN can contain multiple decision tables.

issueFieldOperator.getConfig().setCurrentExecutionInfo(dtResult.getExecutionContextInfo())

Set the context related to executed decision table. Thanks to that we have an info about decision table name or query, which helps to handle many 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 finish, we fill related fields in issue with obtained values. We use method setValueByBusinessKey, because it ensures us if important conditions are checked. For more information, see Technical documentation.

Additionally, we return true, because the postfunction works properly.

Attachments

TestCostDecisionTablePostFunction.groovycostApprover.dmn

  • No labels