How to successfully update Original Estimate based on "Start Date" and "Target End" customfields
Asked by Noam Tal:

Hello and thank you in advance. I have been seeing many similar answered questions but none of the suggested scripts were able to be parsed in ScriptRunner Console. Especially when adding EventDispatchOption class for being able to execute issueManager.issueUpdate().  

The goal is listen to a change in one or more of these fields: Original Estimate, "Start Date" and "Target End" (the last two are custom fields)

Since both custom fields are a date picker and the oEstimate isn't I fail to run simple math operations such as minus() and plus().

I wanted to do something like this:

* If oEstimate is empty, calculate it from targetEnd.minus(startDate)
* if startDate or targetEnd were left empty, update them with the current Sprint value
* if startDate is updated, update both targetEnd and oEstimate by the calculating the days changed in startDate
* if targetEnd is changed, calculate a new oEstimate

I only have ScriptRunner. (Automation plugin requires Pro edition that we don't have)


Answers:
author: Bill Sheboy
body: Hi [@Noam Tal](/t5/user/viewprofilepage/user-id/5272413) -- Welcome to the Atlassian Community!

As a reminder, the Atlassian Community is not a free labor pool to implement requests.

Instead when you have a question, please show what you have already done to try to solve a problem (including any images/information of your solution attempts), describe the challenges you are facing, and describe what assistance you are seeking.

When you find you regularly have needs for complicated solutions for Atlassian tools and do not know where to start, I recommend talking to your team about learning more through study / experimentation and hiring a full time Jira administrator.

Kind regards,  
Bill

comments: author: Evgenii
body: Hire admin like me, for example :)


author: Noam Tal
body: Fair enough, as I am rather new to ScriptRunner scripting, and this was my first question I didn't know what are the dos or don't.  

I was able to create several items already, but didn't know why in so many cases 'issue' was showing as not declared.

[@Evgenii](/t5/user/viewprofilepage/user-id/3993514) , I am actually rolling this idea out.


author: Ram Kumar Aravindakshan _Adaptavist_
body: Hi [@Noam Tal](/t5/user/viewprofilepage/user-id/5272413)

In your description, you mentioned:-
> Hello and thank you in advance. I have been seeing many similar answered questions but none of the suggested scripts were able to be parsed in ScriptRunner Console.

From what you have described, you are trying to copy the Listener or Post-Function code into the ScriptRunner console and run it directly. Please confirm.

If this is the case, it will implicitly fail. When using a Listener or Post-Functions, the editor has bound variables such as event or issue; through which you can invoke the project or other variables.

However, in the ScriptRunner console, this is not doable. It is a barebone editor to which you must first declare the variables. The only variable that is bound to the ScriptRunner console is the log variable to do logging.

Could you please share the sample code that you have tested with so I can provide a better solution.

Thank you and Kind regards,

Ram

comments: author: Noam Tal
body: [@Ram Kumar Aravindakshan _Adaptavist_](/t5/user/viewprofilepage/user-id/2988015) Thanks much for this input.

I am trying to create a listener that listens to an update event.

I have got several insights about that and I am in a mood to try fiddling that on my own for a bit. I'll share my code later on.

Thanks again


author: Evgenii
body: Hi, [@Noam Tal](/t5/user/viewprofilepage/user-id/5272413)

Can you add more details about 3 and 4 points from your list? What calculations are needed there?

I understood 2 first points, and can make script for them, but 3 and 4 are still unclear

comments: author: Noam Tal
body: Thanks Evgeniy for reaching out.

In bullet #3, I can see now why it was not understood. this case does not require changing the oEstimate of course :)  
The use case is when an item was planned to start on a DATE but after a few days we realized that it can actually start 10 days later, I would like to update the targetEnd and keep the oEstimate

In bullet #4, there are many occasions where a developer needs more days to work on an item. In those cases, then the targetEnd date is changed, meaning that the gap between startDate has grown. I thought it would make sense to update the oEstimate accordingly


author: Evgenii
body: Ok, understood. I'll make a draft today


author: Evgenii
body: [@Noam Tal](/t5/user/viewprofilepage/user-id/5272413) made draft, adapt it to your environment  
/\*  
\* Created 2023.  
\* @author Evgeniy Isaenkov  
\* @github <https://github.com/Udjin79/SRUtils>  
\*/  

package Examples.Listeners  

import com.atlassian.greenhopper.service.sprint.Sprint  
import com.atlassian.jira.component.ComponentAccessor  
import com.atlassian.jira.event.type.EventDispatchOption  
import com.atlassian.jira.issue.CustomFieldManager  
import com.atlassian.jira.issue.IssueManager  
import com.atlassian.jira.issue.MutableIssue  
import com.atlassian.jira.issue.fields.CustomField  
import com.atlassian.jira.issue.index.IssueIndexingService  
import com.atlassian.jira.user.ApplicationUser  
import com.atlassian.jira.user.util.UserManager  

import java.sql.Timestamp  

IssueManager issueManager = ComponentAccessor.getIssueManager()  
CustomFieldManager customFieldManager = ComponentAccessor.getCustomFieldManager()  
UserManager userManager = ComponentAccessor.getUserManager()  
IssueIndexingService issueIndexingService = ComponentAccessor.getComponent(IssueIndexingService)  

// Configuring main variables  
def changeItem = event.changeLog.getRelated("ChildChangeItem")  
MutableIssue issue = issueManager.getIssueObject(event.getIssue().id) as MutableIssue  

// Configuring service user, which will make changes  
ApplicationUser user = userManager.getUserByName("serviceAccount")  
// Configuring date string format, which we receive in change data  
String formatPattern = "yyyy-MM-dd'T'HH:mm:ssZ"  
List fieldNames = \["timeoriginalestimate", "Start Date", "Target End"\]  

if (changeItem\['field'\] \&\& changeItem\['field'\].first() in fieldNames) {  
String fieldName = changeItem\['field'\].first()  
CustomField startDate = customFieldManager.getCustomFieldObject(11111)  
CustomField targetEnd = customFieldManager.getCustomFieldObject(11112)  
Long startDateValue = (issue.getCustomFieldValue(startDate) as Timestamp).getTime()  
Long targetEndValue = (issue.getCustomFieldValue(targetEnd) as Timestamp).getTime()  
Sprint sprint = issue.getSprints().find { it.active == true }  
Long originalEstimate = issue.getOriginalEstimate()  

if (sprint \&\& (!startDateValue \|\| !targetEndValue)) {  
issue.setCustomFieldValue(startDate, sprint.getStartDate().getMillis())  
issue.setCustomFieldValue(targetEnd, sprint.getEndDate().getMillis())  
issue.setOriginalEstimate((targetEndValue - startDateValue) / 1000 as Long)  
}  

if (startDateValue \&\& targetEndValue \&\& originalEstimate \&\& fieldName == "Start Date") {  
Long changeOld = (Date.parse(formatPattern, (String) changeItem\['oldvalue'\].first()) as Date).getTime()  
Long changeNew = (Date.parse(formatPattern, (String) changeItem\['newvalue'\].first()) as Date).getTime()  
Long diff = changeNew - changeOld  
issue.setCustomFieldValue(targetEnd, targetEndValue + diff)  
}  

if (startDateValue \&\& targetEndValue \&\& originalEstimate \&\& fieldName == "Target End") {  
Long changeOld = (Date.parse(formatPattern, (String) changeItem\['oldvalue'\].first()) as Date).getTime()  
Long changeNew = (Date.parse(formatPattern, (String) changeItem\['newvalue'\].first()) as Date).getTime()  
Long diff = changeNew - changeOld  
issue.setOriginalEstimate(issue.originalEstimate + (diff / 1000) as Long)  
}  

if (!originalEstimate \&\& startDateValue \&\& targetEndValue) {  
issue.setOriginalEstimate((targetEndValue - originalEstimate) / 1000 as Long)  
}  

issueManager.updateIssue(user, issue, EventDispatchOption.DO_NOT_DISPATCH, false)  
issueIndexingService.reIndex(issue)  
}  


author: Noam Tal
body: Hi Evgeniy,

That is so awesome that you could assist so quickly!

I have a few questions,

1. At lines 37 \& 38, should I change the index 'field' with something?
2. Same question for lines 53, 54, 60, 61 'oldvalue'/'newvalue'?
3. Why did you choose "DO_NOT_DISPATCH" rather than UPDATE_ISSUE?

Lastly, there was a problem running it due to this error in line 43:  
Sprint sprint = issue.getSprints().find { it.active == true } --\>

\[Static type checking\] - Cannot assign value of type java.lang.Object to variable of type com.atlassian.greenhopper.service.sprint.Sprint


author: Evgenii
body: It's very strange, I'm sure I posted answer and my post disappeared. I'll repeat it tomorrow.


author: Evgenii
body: 1. No, it's entry from change item , that contains information about field, that was changed. It looks like:  

```
[[GenericEntity:ChangeItem][newvalue,288000][field,timeoriginalestimate][oldstring,144000][newstring,288000][id,10106][fieldtype,jira][oldvalue,144000][group,10102]]
```

2. The same.  
3. I'm not sure, that it's good idea to send new event ISSUE_UPDATED, that will trigger this listener again :)  
Issue is updated at 70, 71 lines.  
As for error - it's strange. I tested it, it gives Sprint object.  
Try to change:  
def sprint = issue.getSprints().find { it.active == true }  
If it won't help, I'll look for another method of getting sprint of issue


author: Evgenii
body: Changed way of getting issue sprint, added some validators:

```
?/*
 * Created 2023.
 * @author Evgeniy Isaenkov
 * @github https://github.com/Udjin79/SRUtils
 */

package Examples.Listeners

import com.atlassian.greenhopper.service.sprint.Sprint
import com.atlassian.greenhopper.service.sprint.SprintIssueService
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.issue.index.IssueIndexingService
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.user.util.UserManager
import com.onresolve.scriptrunner.runner.customisers.JiraAgileBean
import com.onresolve.scriptrunner.runner.customisers.WithPlugin

import java.sql.Timestamp

@WithPlugin('com.pyxis.greenhopper.jira')
@JiraAgileBean
SprintIssueService sprintIssueService

IssueManager issueManager = ComponentAccessor.getIssueManager()
CustomFieldManager customFieldManager = ComponentAccessor.getCustomFieldManager()
UserManager userManager = ComponentAccessor.getUserManager()
IssueIndexingService issueIndexingService = ComponentAccessor.getComponent(IssueIndexingService)

// Configuring main variables
def changeItem = event.changeLog.getRelated("ChildChangeItem")
MutableIssue issue = issueManager.getIssueObject(event.getIssue().id) as MutableIssue
Sprint sprint
// Configuring service user, which will make changes
ApplicationUser user = userManager.getUserByName("serviceAccount")
// Configuring date string format, which we receive in change data
String formatPattern = "yyyy-MM-dd'T'HH:mm:ssZ"
List fieldNames = ["timeoriginalestimate", "Start Date", "Target End"]

if (changeItem['field'] && changeItem['field'].first() in fieldNames) {
String fieldName = changeItem['field'].first()
// set ID's of your custom fields
 CustomField startDate = customFieldManager.getCustomFieldObject(11111)
CustomField targetEnd = customFieldManager.getCustomFieldObject(11112)
Long startDateValue = (issue.getCustomFieldValue(startDate) as Timestamp).getTime()
Long targetEndValue = (issue.getCustomFieldValue(targetEnd) as Timestamp).getTime()
def sprintData = sprintIssueService.getActiveSprintForIssue(user, issue)
if (sprintData) {
sprint = sprintData.getValue().first()
}
Long originalEstimate = issue.getOriginalEstimate()

if (sprint && (!startDateValue || !targetEndValue)) {
issue.setCustomFieldValue(startDate, sprint.getStartDate().getMillis())
issue.setCustomFieldValue(targetEnd, sprint.getEndDate().getMillis())
issue.setOriginalEstimate((targetEndValue - startDateValue) / 1000 as Long)
}

if (startDateValue && targetEndValue && originalEstimate && fieldName == "Start Date") {
Long changeOld = (Date.parse(formatPattern, (String) changeItem['oldvalue'].first()) as Date).getTime()
Long changeNew = (Date.parse(formatPattern, (String) changeItem['newvalue'].first()) as Date).getTime()
Long diff = changeNew - changeOld
issue.setCustomFieldValue(targetEnd, targetEndValue + diff)
}

if (startDateValue && targetEndValue && originalEstimate && fieldName == "Target End") {
Long changeOld = (Date.parse(formatPattern, (String) changeItem['oldvalue'].first()) as Date).getTime()
Long changeNew = (Date.parse(formatPattern, (String) changeItem['newvalue'].first()) as Date).getTime()
Long diff = changeNew - changeOld
issue.setOriginalEstimate(issue.originalEstimate + (diff / 1000) as Long)
}

if (!originalEstimate && startDateValue && targetEndValue) {
issue.setOriginalEstimate((targetEndValue - originalEstimate) / 1000 as Long)
}

issueManager.updateIssue(user, issue, EventDispatchOption.DO_NOT_DISPATCH, false)
issueIndexingService.reIndex(issue)
}
```


author: Noam Tal
body: [@Evgenii](/t5/user/viewprofilepage/user-id/3993514)   

That too doesn't work out of the box but that is really OK with me as it lays a nice surface to work and play with.

I already used log.warn() to see what went wrong, and saw that Sprint was null, than when I assigned a value to it I still got null, so I checked the field and it appears that our project's Sprint is a custom filed. For the test I was able to read all the attributes of this field and they contain all the Sprint data.

Anyway, please let's leave it for now as I wish to resolve that on my own as much as I can. That is like a riddle that challenges me.

I really appreciate your goodwill. In the meanwhile I have put the listener in Disabled mode until I have time to get back to it.   

Thanks again!!


Tags: data-center

script-listener

scriptrunner