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

Welborn Scott Smith January 9, 2017

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

 

6 answers

1 accepted

5 votes
Answer accepted
Vasiliy Zverev
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
January 9, 2017

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
Welborn Scott Smith January 9, 2017
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
Vasiliy Zverev
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
January 9, 2017

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

Chun-Chi Su September 10, 2018

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?

Helen Porter May 8, 2020

,

0 votes
Priyank March 3, 2021

has anyone attempted this with the new jira automation rules ?

0 votes
Vasiliy Zverev
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
December 26, 2018

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
Kelly LaMar December 26, 2018

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

Kelly LaMar December 27, 2018

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!! 

Kelly LaMar December 28, 2018

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
}
0 votes
Kelly LaMar December 26, 2018

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

0 votes
Welborn Scott Smith January 9, 2017

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

 

Nic Brough -Adaptavist-
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
January 9, 2017

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?

Welborn Scott Smith January 9, 2017

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 

0 votes
Nic Brough -Adaptavist-
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
January 9, 2017

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.
NT May 23, 2018

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);
                }
        }

}

Suggest an answer

Log in or Sign up to answer