Sum of custom field values from subtasks in parent issue custom groovy field

Hello,

I am displaying an Issue Matrix on the parent issue and would like to display the Sum total of all subtasks (custom field values for field name 'Total') in a custom field underneath the Issue Matrix on the parent issue in a field named 'Request Total'.  Subtask field 'Total' is defined as a Number Field. And the parent Issue field 'Request Total' is defined as a Scripted Field using Template: Number Field.

I think I need to:
1) query the total number of Subtasks
2) get the value for each subtask 'Total' field and store in an array
3) add the values for each subtask 'Total' field (add array)
4) display the SUM of all 'Total' field values (SUM of array) in the 'Request Total' field on the parent Issue

Can anyone please help me get started with coding this.  I am pretty new to JIRA and Groovy scripts, yet have experience, albeit dated experience, in C and VB.

I appreciate any help!

Thanks much,
Scott

 

5 answers

1 accepted

0 votes
Accepted answer

Use this code for scripted field:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.fields.CustomField

//return number subtasks
issue.getSubTaskObjects().size()

CustomField total = ComponentAccessor.getCustomFieldManager().getCustomFieldObjectByName("Total")
double totalSum = 0;
for(Issue subtask: issue.getSubTaskObjects()){
    if(subtask.getCustomFieldValue(total) != null)
        totalSum += subtask.getCustomFieldValue(total)
}

return totalSum
Vasiliy,
Thank you so much.  Even though it does throw 'cannot find matching method' it works like a charm. I should have just tried to run it a few minutes sooner smile
Just curious as to why it would throw the error...
Thanks again,
Scott

Sometimes these warnings are not correct. It is better to use IDE. I use IDEA to write code.

Hello, how do you deal with the case when fields in subtasks are updated? How do you ask the parent issue to update its total sum?

0 votes

Ok, it's a good starting idea, the principles are right, but I would approach it slightly differently

Scripted fields run code and display the result.   When we're looking at doing this, let's start with the issue you're going to display it on.  Groovy lets you get into the JIRA API, so you can use that directly.

Specifically, let's assume you have the issue object for the issue you're going to display the field on.

You can bypass most of your steps very easily:

def listOfSubtasks = issue.getSubTaskObjects ()

Then grab the values you want:

def result = 0
def customFieldManager = ComponentAccessor.getCustomFieldManager()
def totalField = customFieldManager.getCustomFieldObjectByName("Total")

listOfSubtasks.each {

            if (it.getCustomFieldValue(totalField))
                result += (double) it.getCustomFieldValue(totalField)
        }
return result
There is a massive problem with this though - where/when it gets run.  I've used an issue object to abstract it slightly.  Most people doing this think "it's a scripted field, so it'll display the results when I look at an issue", but it's not true.  It displays the last result it calculated.  The script runs effectively when the issue is updated not when you view.  So, if you put this code into a scripted field on the parent issue, it would give you the right answer at first, and it would go wrong when you updated one of the subtasks.  Because the code only runs when the parent is updated.

There are a couple of ways to fix that, but the easiest one is to have a second scripted listener which listens for changes on the sub-tasks and triggers a re-index on the parent when the Total field is changed on one.

Hi @Nic Brough [Adaptavist] ,  I have the same re-indexing issue.

Can you please help me with re-indexing the parent issue if  one custom field is updated on subtask.

I found this code but it seems to be not working in script runner listener.

public class IssueModifiedListener implements InitializingBean, DisposableBean {

        private static final Logger log = Logger.getLogger(IssueModifiedListener.class);

        private final EventPublisher eventPublisher;
        private final IssueIndexManager issueIndexManager;

        /**
         * Constructor.                                                                                                                                                                                                                      
         * @param eventPublisher injected {@code EventPublisher} implementation.                                                                                                                                                             
         */                                                                                                                                                                                                                                  
        public IssueModifiedListener(EventPublisher eventPublisher, IssueIndexManager issueIndexManager) {                                                                                                                                   
                this.eventPublisher = eventPublisher;                                                                                                                                                                                        
                this.issueIndexManager = issueIndexManager;                                                                                                                                                                                  
        }                                                                                                                                                                                                                                    
                                                                                                                                                                                                                                             
        /**                                                                                                                                                                                                                                  
         * Called when the plugin has been enabled.                                                                                                                                                                                          
         * @throws Exception                                                                                                                                                                                                                 
         */                                                                                                                                                                                                                                  
        @Override                                                                                                                                                                                                                            
                public void afterPropertiesSet() throws Exception {                                                                                                                                                                          
                        // register ourselves with the EventPublisher                                                                                                                                                                        
                        eventPublisher.register(this);                                                                                                                                                                                       
                }                                                                                                                                                                                                                            
                                                                                                                                                                                                                                             
        /**                                                                                                                                                                                                                                  
         * Called when the plugin is being disabled or removed.                                                                                                                                                                              
         * @throws Exception                                                                                                                                                                                                                 
         */                                                                                                                                                                                                                                  
        @Override                                                                                                                                                                                                                            
                public void destroy() throws Exception {                                                                                                                                                                                     
                        // unregister ourselves with the EventPublisher                                                                                                                                                                      
                        eventPublisher.unregister(this);                                                                                                                                                                                     
                }                                                                                                                                                                                                                            
                                                                                                                                                                                                                                             
        /**                                                                                                                                                                                                                                  
         * Receives any {@code IssueEvent}s sent by JIRA.                                                                                                                                                                                    
         * @param issueEvent the IssueEvent passed to us                                                                                                                                                                                     
         */                                                                                                                                                                                                                                  
        @EventListener                                                                                                                                                                                                                       
        public void onIssueEvent(IssueEvent issueEvent) {
                Long eventTypeId = issueEvent.getEventTypeId();
                Issue issue = issueEvent.getIssue();
                Issue parent = issue.getParentObject();

                if( parent != null ) {
                        reindexIssue( parent );
                }

        }

        /**
         * Called a parent issue is found that needs to be reindexed.
         * @param issue The issue to be reindexed
         */
        private void reindexIssue(Issue issue) {
                try {
                        boolean origVal = ImportUtils.isIndexIssues();
                        ImportUtils.setIndexIssues(true);
                        issueIndexManager.reIndex(issue);
                        ImportUtils.setIndexIssues(origVal);
                } catch (IndexException ie) {
                        log.error("Unable to reindex issue: " + issue.getString("key")
                                        + ", [id=" + issue.getLong("id") + "].", ie);
                }
        }

}

Hey Nic,

Thank you so much for your response. I did not pursue your answer as Vasily's answer works. Yet I do thank you!

Scott

 

His code is much better than mine (as usual), but does the same thing in much the same way.

Did you pick up the bit about causing the parent issue to re-index?

I may be wrong but I think that in this case it may not be an issue.  The project only has one issue type (IT Sourcing Intake Request Form) and a subtask (IT Sourcing Intake Request Form Subtask). Create only provides the IT Sourcing Intake Request Form as an option.  Users will be trained to create subtask to represent orders under the parent issue (requisition). As of now the scripted field is only displayed on the View Issue intake form, as I thought if I placed it there it would force the field to trigger.  So far in my testing when I update a subtask, the view issue screen updates the Total.  I'll keep pocking at it.  Thanks again!  -Scott 

Hi folks,

I think I'm trying to do a similar thing, but I need the value I'm pulling to be the remaining time estimate. I know JIRA does this automatically on the parent issues but I need that "remaining sum" value in an explicit field so I can export those values to our project management tool.

Basically, I need a scripted field that adds up the remaining time estimates of the subtasks.

I'm not a developer and I can't figure out how to adjust this script to make this work - any help would be very appreciated!

Thanks,

Kelly

Kelly, hi!

Try this script:

import com.atlassian.jira.issue.Issue

long totalSum = 0;
for(Issue subtask: issue.getSubTaskObjects()){
if(subtask.getCustomFieldValue(total) != null)
totalSum += subtask.getEstimate()
}

return totalSum / (1000 * 60) //return result in hours

Thanks for the super fast response and script! I apologize if I'm making a completely amateur mistake, but it's not working in the preview (returning several errors and a "null" when I know it shouldn't be). I've attached a screenshot that shows the errors. Any pointers?

I assume "total" is referring to the field I'm creating, but I've renamed it in various combinations and can't figure it out :(

Thank you for any additional help! This bit of automation will save our team lots of time.

 

Screen Shot 2018-12-26 at 11.24.30 PM.png

Actually nevermind, I figured out a way to make it work like so, returning minutes:

import com.atlassian.jira.issue.Issue

long totalSum = 0;
for(Issue subtask: issue.getSubTaskObjects()){
if(subtask.getEstimate() != null)
totalSum += subtask.getEstimate()
}

return totalSum / 60

Thank you again for your help!! 

FWIW for future folks: I ended up tweaking the code so it now adds remaining estimates of all the subtasks as well as the remaining estimate on the story level ticket, or returns null if neither story nor subtasks have estimates. Final code below:

import com.atlassian.jira.issue.Issue

long totalSum = 0;
boolean noEstimates = true;
def totalSubTasks = issue.getSubTaskObjects().size()

for(Issue subtask: issue.getSubTaskObjects()){
if(subtask.getEstimate() != null){
totalSum += subtask.getEstimate()
noEstimates = false
}
}
if (issue.getEstimate() != null){
totalSum += issue.getEstimate()
noEstimates = false
}

if (noEstimates == true){
return null
}
else {
return totalSum / 60
}

Suggest an answer

Log in or Sign up to answer
Community showcase
Published Thursday in Agile

How Scrum works? It starts with training and education

To answer “How scrum works,” most of the teams I've worked with first addressed the question: “where to start?”  That question applies to both implementation and improvements on the Scrum framew...

229 views 3 7
Read article

Atlassian User Groups

Connect with like-minded Atlassian users at free events near you!

Find a group

Connect with like-minded Atlassian users at free events near you!

Find my local user group

Unfortunately there are no AUG chapters near you at the moment.

Start an AUG

You're one step closer to meeting fellow Atlassian users at your local meet up. Learn more about AUGs

Groups near you