How to successfully update Original Estimate based on "Start Date" and "Target End" customfields

Noam Tal July 22, 2023

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)

3 answers

1 vote
Bill Sheboy
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.
July 23, 2023

Hi @Noam Tal -- 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

Evgenii
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
July 23, 2023

Hire admin like me, for example :)

Noam Tal July 23, 2023

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 , I am actually rolling this idea out. 

0 votes
Ram Kumar Aravindakshan _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.
July 23, 2023

Hi @Noam Tal

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

Noam Tal July 23, 2023

@Ram Kumar Aravindakshan _Adaptavist_ 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

0 votes
Evgenii
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
July 23, 2023

Hi, @Noam Tal 

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

Noam Tal July 23, 2023

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

Evgenii
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
July 23, 2023

Ok, understood. I'll make a draft today

Evgenii
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
July 23, 2023

@Noam Tal 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)
}

Noam Tal July 23, 2023

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

Evgenii
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
July 23, 2023

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

Evgenii
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
July 23, 2023

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
Evgenii
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
July 23, 2023

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)
}
Like Noam Tal likes this
Noam Tal July 24, 2023

@Evgenii 

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

Like Evgenii likes this

Suggest an answer

Log in or Sign up to answer
TAGS
AUG Leaders

Atlassian Community Events