SuiteScript Workflow Action error SSS_USAGE_LIMIT_EXCEEDED and email not attaching to related records - suitescript2.0

I am receiving the following error when I try to process more than approx 15 transactions:
Error: SSS_USAGE_LIMIT_EXCEEDED
{"type":"error.SuiteScriptError","name":"SSS_USAGE_LIMIT_EXCEEDED","message":"Script Execution Usage Limit Exceeded","stack":["createError(N/error)","onAction(/SuiteScripts/sdf_ignore/Send Remittance PFA Workflow Action.js:84)"],"cause":{"type":"internal error","code":"SSS_USAGE_LIMIT_EXCEEDED","details":"Script Execution Usage Limit Exceeded","userEvent":null,"stackTrace":["createError(N/error)","onAction(/SuiteScripts/sdf_ignore/Send Remittance PFA Workflow Action.js:84)"],"notifyOff":false},"id":"","notifyOff":false,"userFacing":false}
The script is a workflow action script that is triggered when the user clicks on a button to send email remittance advice.
This works for the most part unless I exceed a certain number of transactions.
Do I need to use another script type?
Or can I modify the following script to reduce its governance usage?
/**
*#NApiVersion 2.x
*#NScriptType WorkflowActionScript
*/
define([
"N/search",
"N/record",
"N/render",
"N/file",
"N/xml",
"N/email",
], function (search, record, render, file, xml, email) {
function onAction(context) {
var fileObj = [];
var record = context.newRecord;
log.debug("record", record);
var batchId = record.getValue({ fieldId: "name" });
var id = record.id;
log.debug("recordid", record.id);
var vendorpaymentSearchObj = search.create({
type: "vendorpayment",
filters: [
["type", "anyof", "VendPymt"],
"AND",
["custbody_9997_pfa_record", "anyof", id],
],
columns: [
search.createColumn({
name: "transactionnumber",
summary: "GROUP",
label: "Transaction Number",
}),
search.createColumn({
name: "formulatext",
summary: "GROUP",
formula: "{entity}",
label: "Vendor",
}),
search.createColumn({
name: "formulatext",
summary: "GROUP",
formula:
"CASE WHEN {vendor.custentity_2663_email_address_notif} IS NULL THEN {vendor.email} ELSE {vendor.custentity_2663_email_address_notif} END",
label: "Email",
}),
search.createColumn({
name: "total",
summary: "SUM",
label: "Amount (Transaction Total)",
}),
search.createColumn({
name: "currency",
summary: "GROUP",
label: "Currency",
}),
search.createColumn({
name: "trandate",
summary: "GROUP",
sort: search.Sort.ASC,
label: "Date",
}),
search.createColumn({
name: "internalid",
summary: "GROUP",
label: "internalid",
}),
search.createColumn({
name: "internalid",
join: "vendor",
summary: "GROUP",
label: "Internal ID",
}),
search.createColumn({
name: "internalid",
summary: "GROUP",
label: "Internal ID",
}),
],
});
var searchResultCount = vendorpaymentSearchObj.runPaged().count;
log.debug("vendorpaymentSearchObj result count", searchResultCount);
vendorpaymentSearchObj.run().each(function (result) {
var emailAddress = result.getValue(result.columns[2]);
var transactionNumber = result.getValue(result.columns[0]);
var amount = result.getValue(result.columns[3]);
var date = result.getValue(result.columns[5]);
var vendor = result.getValue(result.columns[1]);
var resultId = result.getValue(result.columns[6]);
var vendorId = result.getValue(result.columns[7]);
var transactionId = result.getValue(result.columns[8]);
log.debug(
"emailAddress: ",
emailAddress +
" transaction bumber: " +
transactionNumber +
" amount: " +
amount +
" date: " +
date +
" vendor: " +
vendor +
" resultId " +
resultId +
" transactionId " +
transactionId
);
var pdfFile = render.transaction({
entityId: parseInt(resultId),
printMode: render.PrintMode.PDF,
formId: 109,
inCustLocale: true,
});
pdfFile.folder = 1351;
var fileId = pdfFile.save();
var pdffile2 = file.load({ id: fileId });
// var fileObj = file.load({ id: parseInt(fileId) });
var mergeResult = render.mergeEmail({
templateId: 8,
// entity: {
// type: "employee",
// id: parseInt(recipient),
// },
entity: {
type: "vendor",
id: parseInt(vendorId),
},
recipient: {
type: "vendor",
id: parseInt(vendorId),
},
supportCaseId: null,
transactionId: parseInt(resultId),
customRecord: null,
// {
// type: "customrecord_2663_entity_bank_details",
// id: parseInt(bankDetailsId),
// },
});
var emailSubject = mergeResult.subject;
var emailBody = mergeResult.body;
//create a placeholder in the original HTML with an element called NLID. This will replace that with a value that is part of the script
emailSubject = emailSubject.replace("NLVENDOR", vendor);
// emailBody = emailBody.replace("NLDOCNUMBER", bankDetailsId);
var emailString = JSON.stringify(emailAddress);
email.send({
author: -5,
// recipients: 2020,
recipients: emailAddress,
subject: emailSubject,
body: emailBody,
attachments: [pdffile2],
relatedRecords: {
entity: parseInt(vendorId),
customRecord: {
id: parseInt(id),
recordType: 'customrecord_2663_file_admin', //an integer value
},
// transactionId: 38326,
},
});
return true;
});
/*
vendorpaymentSearchObj.id="customsearch1658554258593";
vendorpaymentSearchObj.title="Bill Payments in a Payment Batch (copy)";
var newSearchId = vendorpaymentSearchObj.save();
*/
}
return {
onAction: onAction,
};
});
Another issue I am having with this script is the email.send method doesn't throw an error for the custom record but doesn't actually attach the email messages to the stated transaction type either. It doesn't allow me to attach the emails to a transaction at all (I get an 'unexpected error' if I do)
I finally got a map reduce working for this:
/**
*#NApiVersion 2.x
*#NScriptType MapReduceScript
*/
define([
"N/search",
"N/record",
"N/render",
"N/file",
"N/xml",
"N/email",
"N/runtime",
], function (search, record, render, file, xml, email, runtime) {
function getInputData(context) {
var scriptObj = runtime.getCurrentScript();
var recordId = scriptObj.getParameter("custscript_recordid");
// var record = context.newRecord;
// log.debug("record", record);
// var batchId = record.getValue({ fieldId: "name" });
var id = recordId;
log.debug("recordid", recordId);
var vendorpaymentSearchObj = search.create({
type: "vendorpayment",
filters: [
["type", "anyof", "VendPymt"],
"AND",
["custbody_9997_pfa_record", "anyof", id],
],
columns: [
//0
search.createColumn({
name: "transactionnumber",
summary: "GROUP",
label: "Transaction Number",
}),
//1
search.createColumn({
name: "formulatext",
summary: "GROUP",
formula: "{entity}",
label: "Vendor",
}),
//2
search.createColumn({
name: "formulatext",
summary: "GROUP",
formula:
"CASE WHEN {vendor.custentity_2663_email_address_notif} IS NULL THEN {vendor.email} ELSE {vendor.custentity_2663_email_address_notif} END",
label: "Email",
}),
//3
search.createColumn({
name: "total",
summary: "SUM",
label: "Amount (Transaction Total)",
}),
//4
search.createColumn({
name: "currency",
summary: "GROUP",
label: "Currency",
}),
//5
search.createColumn({
name: "trandate",
summary: "GROUP",
sort: search.Sort.ASC,
label: "Date",
}),
//6
search.createColumn({
name: "internalid",
join: "vendor",
summary: "GROUP",
label: "vendorId",
}),
//7
search.createColumn({
name: "internalid",
summary: "GROUP",
label: "searchResultId",
}),
],
});
log.debug(
"vendorpaymentSearchObj result count",
vendorpaymentSearchObj.runPaged().count
);
var vendorPayments = [];
vendorpaymentSearchObj.run().each(function (result) {
vendorPayments.push({
emailAddress: result.getValue(result.columns[2]),
transactionNumber: result.getValue(result.columns[0]),
amount: result.getValue(result.columns[3]),
date: result.getValue(result.columns[5]),
vendor: result.getValue(result.columns[1]),
resultId: result.getValue(result.columns[7]),
vendorId: result.getValue(result.columns[6]),
id: id,
// transactionId: result.getValue(result.columns[8]),
});
return true;
});
return vendorPayments;
}
/**
* #param {MapReduceContext.map} context
*/
function map(context) {
try {
log.debug("context", context);
const result = JSON.parse(context.value);
log.debug("result", result);
var emailAddress = result.emailAddress;
var transactionNumber = result.transactionNumber;
var amount = result.amount;
var date = result.date;
var vendor = result.vendor;
var resultId = result.resultId;
var vendorId = result.vendorId;
var id = result.id;
// var transactionId = result.transactionId;
log.debug(
"emailAddress: ",
emailAddress +
" transaction bumber: " +
transactionNumber +
" amount: " +
amount +
" date: " +
date +
" vendor: " +
vendor +
" resultId " +
resultId +
// " transactionId " +
// transactionId +
"vendorId " +
vendorId
);
// for (var i = 0; i < context.value.length; i++) {
var pdfFile = render.transaction({
entityId: parseInt(resultId),
printMode: render.PrintMode.PDF,
formId: 109,
});
pdfFile.folder = 1351;
var fileId = pdfFile.save();
var pdffile2 = file.load({ id: fileId });
// context.write({
// key: context.value[i],
// value: [
// [pdffile2],
// pdfFile,
// fileId,
// emailAddress,
// transactionNumber,
// amount,
// date,
// vendor,
// resultId,
// vendorId,
// transactionId,
// ],
// });
log.debug("fileid: " + fileId + pdfFile + pdffile2);
context.write({
key: resultId,
value: JSON.stringify({
vendorId: vendorId,
vendor: vendor,
fileId: fileId,
emailAddress: emailAddress,
id: id,
}),
});
// }
} catch (ex) {
log.error("Error on map", ex.message + ex.error);
}
}
// var fileObj = file.load({ id: parseInt(fileId) });
/**
* #param {MapReduceContext.reduce} context
*/
function reduce(context) {
try {
var reduceResults = context.values; //note: context.values and not context.value unlike the earlier stages. Also, this is not JSON.Parse
log.debug("reduceResults", reduceResults);
var pdffile2 = [];
for (var i = 0; i < reduceResults.length; i++) {
//note: this is context.value(S)
log.debug("vendorId", JSON.parse(reduceResults[i]).vendorId);
log.debug("key", context.key);
if (reduceResults) {
var mergeResult = render.mergeEmail({
templateId: 8,
// entity: {
// type: "employee",
// id: parseInt(recipient),
// },
entity: {
type: "vendor",
id: parseInt(JSON.parse(reduceResults[i]).vendorId),
},
recipient: {
type: "vendor",
id: parseInt(JSON.parse(reduceResults[i]).vendorId),
},
supportCaseId: null,
transactionId: parseInt(context.key),
customRecord: null,
});
log.debug("mergeResult", mergeResult);
var emailSubject = mergeResult.subject;
var emailBody = mergeResult.body;
log.debug("email body", emailBody);
var pdf = file.load({
id: JSON.parse(reduceResults[i]).fileId,
});
var vendorName = JSON.parse(reduceResults[i]).vendor;
log.debug("vendorname", vendorName);
pdffile2.push(pdf);
//create a placeholder in the original HTML with an element called NLVENDOR. This will replace that with a value that is part of the script
var emailSubjectNew = emailSubject.replace("NLVENDOR", vendorName);
log.debug("email subject", emailSubjectNew);
var emailString = JSON.parse(reduceResults[i]).emailAddress;
log.debug("emailstring", emailString);
email.send({
author: -5,
recipients: JSON.parse(reduceResults[i]).emailAddress,
subject: emailSubjectNew,
body: emailBody,
attachments: pdffile2,
relatedRecords: {
entity: parseInt(JSON.parse(reduceResults[i]).vendorId),
transactionId: parseInt(context.key),
},
});
}
}
} catch (ex) {
log.error("Error on reduce", ex.message + "" + ex.name);
}
}
/**
* #param {MapReduceContext.summarize} context
*/
function summarize(summary) {
log.debug("context", summary);
summary.output.iterator().each(function (key, value) {
contents += key + " " + value + "\n";
return true;
});
}
return {
getInputData: getInputData,
map: map,
reduce: reduce,
summarize: summarize,
};
});

To me this operation seems more suitable for a Map/Reduce if you don't know how many results the search will have. Meaning if it always rendered one PDF and sent it so no need for a MR but if the amount is unknown so a Map/Reduce is the way to go.
If you still want to try and reduce usage on this script you can try:
Not saving and loading the PDF. Instead just generate it and send it. (assuming this is not a requirement)
You can try adding search criteria to narrow down the query
I'm doubtful that either of these will reduce the usage enough to make a real difference. I would look into the N/task module.

Related

Add quantity for expense sublist on line commit

I have an expense report transaction where I am trying to sum the quantity field on the sublist and update a custom field (i.e. basically a running total that is updated every time a line is committed)
This is a part of the client script that I have so far:
function sublistChanged(context) {
var currentRecord = context.currentRecord;
var sublistName = context.sublistId;
var sublistFieldName = context.fieldId;
var op = context.operation;
totalExpQuantity = 0;
if (sublistName === "expense") {
for (var i = 0; i < sublistName.length; i++) {
var quantity = currentRecord.getCurrentSublistValue({
sublistId: "expense",
fieldId: "quantity",
});
log.debug("quantity", quantity);
}
totalExpQuantity += parseInt(quantity);
var lineCount = currentRecord.getLineCount({
sublistId: "expense",
});
console.log(lineCount);
currentRecord.setValue({
fieldId: "custbody_mileage_exp_report",
value:
"Total has changed to " +
currentRecord.getValue({
fieldId: "amount",
}) +
" with operation: " +
op +
" total quantity: " +
totalExpQuantity,
});
}
}
Every time a line is committed with a quantity value, I want the total to increment by the value of the line that was committed
i.e. here, after every line is committed, the total should go from 10,000, to 10,500 to 15,500
I have tried a variation of this code where the following line is part of the 'for loop':
totalExpQuantity += parseInt(quantity);
i.e.
for (var i = 0; i < sublistName.length; i++) {
var quantity = currentRecord.getCurrentSublistValue({
sublistId: "expense",
fieldId: "quantity",
});
totalExpQuantity += parseInt(quantity);
log.debug("quantity", quantity);
}
I get the following result:
Is what I am trying to do possible?
Where am I going wrong in the code? Should it be a different entry point?
I have also tried postsourcing though it didn't retrieve the quantity field and returned a blank
Found the answer. This is the working version of the script:
/**
*#NApiVersion 2.x
*#NScriptType ClientScript
*/
define(["N/search"], function (search) {
function fieldChanged(context) {
try {
var recordObj = context.currentRecord;
var employeeObj = parseInt(
recordObj.getValue({
fieldId: "entity",
})
);
if (context.fieldId == "entity") {
var employeeName = parseInt(employeeObj);
log.debug("employee", employeeName);
var expensereportSearchObj = search.create({
type: "expensereport",
filters: [
["type", "anyof", "ExpRept"],
"AND",
["expensecategory", "anyof", "8"],
"AND",
["expensedate", "within", "thisfiscalyear"],
"AND",
["employee", "anyof", employeeObj],
],
columns: [
search.createColumn({
name: "entityid",
join: "employee",
label: "Name",
}),
search.createColumn({ name: "tranid", label: "Document Number" }),
search.createColumn({
name: "expensecategory",
label: "Expense Category",
}),
search.createColumn({ name: "expensedate", label: "Expense Date" }),
search.createColumn({ name: "currency", label: "Currency" }),
search.createColumn({
name: "quantity",
join: "expenseDetail",
sort: search.Sort.ASC,
label: "Quantity",
}),
],
});
var searchResult = expensereportSearchObj
.run()
.getRange({ start: 0, end: 1000 });
log.debug("result", JSON.stringify(searchResult));
var searchResultCount = expensereportSearchObj.runPaged().count;
log.debug("expensereportSearchObj result count", searchResultCount);
let q = 0;
for (var i = 0; i < searchResult.length; i++) {
var quantity = searchResult[i].getValue(searchResult[i].columns[5]);
log.debug("quantity", quantity);
q += parseInt(quantity);
log.debug("q", q);
}
recordObj.setValue({
fieldId: "custbody_employee_mileage_ytd",
value: q,
});
}
//loop through all results add +- to 0. see video for sub;ists
} catch (error) {
log.debug(
error.name,
"recordObjId: " +
recordObj +
", employee:" +
employeeName +
", message: " +
error.message +
", cause: " +
error.cause
);
}
}
function sublistChanged(context) {
var recordObj = context.currentRecord;
var sublistName = recordObj.getSublist({ sublistId: "expense" });
var lineCount = recordObj.getLineCount({ sublistId: "expense" });
var totalQuantity = 0;
for (i = 0; i < lineCount; i++) {
var expenseCategory = recordObj.getSublistValue({
sublistId: "expense",
fieldId: "category",
line: i,
});
log.debug("expenseCategory", expenseCategory);
var expenseQuantity = recordObj.getSublistValue({
sublistId: "expense",
fieldId: "quantity",
line: i,
});
if (expenseCategory == 8) {
totalQuantity += expenseQuantity;
}
recordObj.setValue({
fieldId: "custbody_mileage_exp_report",
value: totalQuantity,
});
}
}
return {
fieldChanged: fieldChanged,
sublistChanged: sublistChanged,
};
});

How to write SuiteScript for map reduce CSV file or JSON data as an input process it and Create a customer Record

Map Reduce script for Csv file or JSON Data as input process it and create a customer record
Suite Answer 43795 has an excellent "Processing Invoices Example" sample script that you can work from, it is also pasted below. Suite Answer 43795, also has additional information for each map/reduce stage.
/**
* #NApiVersion 2.x
* #NScriptType MapReduceScript
*/
define(['N/search', 'N/record', 'N/email', 'N/runtime', 'N/error'],
function(search, record, email, runtime, error){
function handleErrorAndSendNotification(e, stage){
log.error('Stage: ' + stage + ' failed', e);
var author = -5;
var recipients = 'notify#xxxxxx.com';
var subject = 'Map/Reduce script ' + runtime.getCurrentScript().id + ' failed for stage: ' + stage;
var body = 'An error occurred with the following information:\n' + 'Error code: ' + e.name + '\n' + 'Error msg: ' + e.message;
email.send({
author: author,
recipients: recipients,
subject: subject,
body: body
});
}
function handleErrorIfAny(summary){
var inputSummary = summary.inputSummary;
var mapSummary = summary.mapSummary;
var reduceSummary = summary.reduceSummary;
if (inputSummary.error)
{
var e = error.create({
name: 'INPUT_STAGE_FAILED',
message: inputSummary.error
});
handleErrorAndSendNotification(e, 'getInputData');
}
handleErrorInStage('map', mapSummary);
handleErrorInStage('reduce', reduceSummary);
}
function handleErrorInStage(stage, summary){
var errorMsg = [];
summary.errors.iterator().each(function(key, value){
var msg = 'Failure to accept payment from customer id: ' + key + '. Error was: ' + JSON.parse(value).message + '\n';
errorMsg.push(msg);
return true;
});
if (errorMsg.length > 0)
{
var e = error.create({
name: 'RECORD_TRANSFORM_FAILED',
message: JSON.stringify(errorMsg)
});
handleErrorAndSendNotification(e, stage);
}
}
function createSummaryRecord(summary){
try{
var seconds = summary.seconds;
var usage = summary.usage;
var yields = summary.yields;
var rec = record.create({
type: 'customrecord_summary',
});
rec.setValue({
fieldId : 'name',
value: 'Summary for M/R script: ' + runtime.getCurrentScript().id
});
rec.setValue({
fieldId: 'custrecord_time',
value: seconds
});
rec.setValue({
fieldId: 'custrecord_usage',
value: usage
});
rec.setValue({
fieldId: 'custrecord_yields',
value: yields
});
rec.save();
} catch(e){
handleErrorAndSendNotification(e, 'summarize');
}
}
function applyLocationDiscountToInvoice(recordId){
var invoice = record.load({
type: record.Type.INVOICE,
id: recordId,
isDynamic: true
});
var location = invoice.getText({
fieldId: 'location'
});
var discount;
if (location === 'East Coast')
discount = 'Eight Percent';
else if (location === 'West Coast')
discount = 'Five Percent';
else if (location === 'United Kingdom')
discount = 'Nine Percent';
else
discount = '';
invoice.setText({
fieldId: 'discountitem',
text: discount,
ignoreFieldChange : false
});
log.debug(recordId + ' has been updated with location-based discount.');
invoice.save();
}
function getInputData(){
return search.create({
type: record.Type.INVOICE,
filters: [['status', search.Operator.IS, 'open']],
columns: ['entity'],
title: 'Open Invoice Search'
});
}
function map(context){
var searchResult = JSON.parse(context.value);
var invoiceId = searchResult.id;
var entityId = searchResult.values.entity.value;
applyLocationDiscountToInvoice(invoiceId);
context.write({
key: entityId,
value: invoiceId
});
}
function reduce(context){
var customerId = context.key;
var custPayment = record.transform({
fromType: record.Type.CUSTOMER,
fromId: customerId,
toType: record.Type.CUSTOMER_PAYMENT,
isDynamic: true
});
var lineCount = custPayment.getLineCount('apply');
for (var j = 0; j < lineCount; j++){
custPayment.selectLine({
sublistId: 'apply',
line: j
});
custPayment.setCurrentSublistValue({
sublistId: 'apply',
fieldId: 'apply',
value: true
});
}
var custPaymentId = custPayment.save();
context.write({
key: custPaymentId
});
}
function summarize(summary){
handleErrorIfAny(summary);
createSummaryRecord(summary);
}
return {
getInputData: getInputData,
map: map,
reduce: reduce,
summarize: summarize
};
});

Generate cycle time report for kanban board states

I am creating custom HTML page in rally tool to write the code to get the total number of days each story stayed in each state in the kanban board from the day it entered the state and till it leaves the state as shown below:[In the image, uid12 stayed in "ready state" State for 10 days and currently staying in "development state " state from last 2 days.. In story uid34,total number of days it took to complete all states is 32]. Can anyone please help me with this as i am new to rally.
1
Here is a js code based on AppSDK2 that can be compiled to html using rally-app-builder
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function(){
var context = this.getContext();
var project = context.getProject()['ObjectID'];
console.log(project);
var that = this;
var panel = Ext.create('Ext.panel.Panel', {
layout: 'hbox',
itemId: 'parentPanel',
componentCls: 'panel',
items: [
{
xtype: 'panel',
width: 600,
itemId: 'childPanel1'
},
{
xtype: 'panel',
width: 600,
itemId: 'childPanel2'
}
],
});
this.add(panel);
Ext.create('Rally.data.lookback.SnapshotStore', {
fetch : ['Name','c_KanbanState','_UnformattedID', '_TypeHierarchy'],
filters : [{
property : '__At',
value : 'current'
},
{
property : '_TypeHierarchy',
value : 'HierarchicalRequirement'
},
{
property : '_ProjectHierarchy',
value: project
},
{
property : 'c_KanbanState',
operator : 'exists',
value : true
}
],
hydrate: ['_TypeHierarchy', 'c_KanbanState'],
listeners: {
load: this.onStoriesLoaded,
scope: this
}
}).load({
params : {
compress : true,
removeUnauthorizedSnapshots : true
}
});
},
onStoriesLoaded: function(store, data){
var that = this;
var stories = [];
var id;
_.each(data, function(record) {
var artifactType = record.get('_TypeHierarchy');
if (artifactType[artifactType.length - 1] == "HierarchicalRequirement") {
id = 'US' + record.get('_UnformattedID');
} else if (artifactType[artifactType.length - 1] == "Defect") {
id = 'DE' + record.get('_UnformattedID');
}
stories.push({
Name: record.get('Name'),
FormattedID: id,
UnformattedID: record.get('_UnformattedID'),
c_KanbanState: record.get('c_KanbanState')
});
console.log(stories);
});
var myStore = Ext.create('Rally.data.custom.Store', {
data: stories
});
if (!this.down('#allStoriesGrid')) {
this.down('#childPanel1').add({
xtype: 'rallygrid',
id: 'allStoriesGrid',
store: myStore,
columnCfgs: [
{
text: 'Formatted ID', dataIndex: 'FormattedID',
},
{
text: 'Name', dataIndex: 'Name', flex: 1,
},
{
text: 'Current Kanban State', dataIndex: 'c_KanbanState'
}
],
listeners: {
cellclick: function( grid, td, cellIndex, record, tr, rowIndex){
id = grid.getStore().getAt(rowIndex).get('UnformattedID');
console.log('id', id);
that.getStoryModel(id);//to build a grid of Kanban allowed values
}
}
});
}else{
this.down('#allStoriesGrid').reconfigure(myStore);
}
},
getStoryModel:function(id){
var workspace = this.getContext().getWorkspaceRef();
var project = this.getContext().getProjectRef();
console.log('workspace',workspace);
console.log('project',project);
console.log('get story model');
var that = this;
this.arr=[];
Rally.data.ModelFactory.getModel({
type: 'User Story',
success: function(model){
var allowedValuesStore = model.getField('c_KanbanState').getAllowedValueStore( );
that.getDropdownValues(allowedValuesStore, id);
}
});
},
getDropdownValues:function(allowedValuesStore, id){
var that = this;
allowedValuesStore.load({
scope: this,
callback: function(records, operation, success){
_.each(records, function(val){
//AllowedAttributeValue object in WS API has StringValue
var v = val.get('StringValue');
that.arr.push(v);
});
console.log('arr', this.arr);
that.getStoryById(id);
}
});
},
getStoryById:function(id){
var that = this;
var snapStore = Ext.create('Rally.data.lookback.SnapshotStore', {
fetch: ['c_KanbanState', 'Blocked'],
hydrate:['c_KanbanState','Blocked'],
filters : [
{
property : '_UnformattedID',
value : id
}
],
sorters:[
{
property : '_ValidTo',
direction : 'ASC'
}
]
});
snapStore.load({
params: {
compress: true,
removeUnauthorizedSnapshots : true
},
callback : function(records, operation, success) {
that.onDataLoaded(records, id);
}
});
},
onDataLoaded:function(records, id){
var times = [];
var measure = 'second';
//-----------------------ready
var ready = _.filter(records, function(record) {
return record.get('c_KanbanState') === 'ready';
});
var cycleTimeFromReadyToDev = '';
if (_.size(ready) > 0) {
var ready1 = _.first(ready);
var ready2 = _.last(ready);
var readyDate1 = new Date(ready1.get('_ValidFrom'));
if (ready2.get('_ValidTo') === "9999-01-01T00:00:00.000Z") { //infinity
readyDate2 = new Date(); //now
}
else{
var readyDate2 = new Date(ready2.get('_ValidTo'));
}
cycleTimeFromReadyToDev = Rally.util.DateTime.getDifference(readyDate2,readyDate1, measure );
}
times.push(cycleTimeFromReadyToDev);
//----------------------dev
var dev = _.filter(records, function(record) {
return record.get('c_KanbanState') === 'dev';
});
var cycleTimeFromDevToDone = '';
if (_.size(dev) > 0) {
var dev1 = _.first(dev);
var dev2 = _.last(dev);
var devDate1 = new Date(dev1.get('_ValidFrom'));
if (dev2.get('_ValidTo') === "9999-01-01T00:00:00.000Z") { //infinity
devDate2 = new Date(); //now
}
else{
var devDate2 = new Date(dev2.get('_ValidTo'));
}
cycleTimeFromInProgressToDone = Rally.util.DateTime.getDifference(devDate2,devDate1, measure );
}
times.push(cycleTimeFromDevToDone);
//------------------------done
var done = _.filter(records, function(record) {
return record.get('c_KanbanState') === 'done';
});
console.log('done',done);
var cycleTimeFromDoneToReleased = '';
if (_.size(done) > 0) {
var done1 = _.first(done);
var done2 = _.last(done);
var doneDate1 = new Date(done1.get('_ValidFrom'));
if (done2.get('_ValidTo') === "9999-01-01T00:00:00.000Z") { //infinity
doneDate2 = new Date(); //now
}
else{
var doneDate2 = new Date(done2.get('_ValidTo'));
}
cycleTimeFromDoneToReleased = Rally.util.DateTime.getDifference(doneDate2,doneDate1, measure );
}
times.push(cycleTimeFromDoneToReleased);
//skip first '' element of the this.arr and last 'released' element of this.arr because
//do not care for cycle times in first and last kanban states
this.arrShortened = _.without(this.arr, _.first(this.arr),_.last(this.arr)) ;
cycleTimes = _.zip(this.arrShortened, times);
cycleTimes = _.object(cycleTimes);
var cycleTimesArray = [];
cycleTimesArray.push(cycleTimes);
var store = Ext.create('Rally.data.custom.Store',{
data: cycleTimesArray,
pageSize: 100
});
var columnConfig = [];
_.each(cycleTimes,function(c,key){
var columnConfigElement = _.object(['text', 'dataIndex', 'flex'], ['time spent in ' + key, key, 1]);
columnConfig.push(columnConfigElement);
});
var title = 'KanbanState cycle time for US' + id + ' in ' + measure + 's'
if (!this.grid) {
this.grid = this.down('#childPanel2').add({
xtype: 'rallygrid',
title: title,
itemId: 'grid2',
store: store,
columnCfgs: columnConfig
});
}
else{
this.down('#grid2').reconfigure(store);
}
}
});
You can use this example as a starting point. It builds a second grid of cycle times when a row in a first grid is clicked. The KanbanState field in my example has 'ready','dev' and 'done' allowed values

Rally Kanban # of days since first state?

Is there a way to calculate the number of days since the card has been in the first state? Lets use say I use a custom field \for the kanban states. 1,2,3,4 If a card is in state 3 then how long has it been since # 1?
I am not sure of a way to automate it or flag items but if you review the US/DE in question just take a quick look at the revision history.
Any changes in state should be logged in the history.
Use Lookback API to access historic data.
Lookback API allows to see what any work item or collection of work items looked like in the past. This is different from using WS API directly, which can provide you with the current state of objects, but does not have historical data.
LBAPI documentation is available here
Here is an example that builds a grid of stories with a Kanban state. When a story's row is double-clicked, the time this story has spent in three Kanban states is calculated and a grid is built to show the values:
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function(){
var x = Ext.create('Rally.data.lookback.SnapshotStore', {
fetch : ['Name','c_Kanban','_UnformattedID', '_TypeHierarchy'],
filters : [{
property : '__At',
value : 'current'
},
{
property : '_TypeHierarchy',
value : 'HierarchicalRequirement'
},
{
property : '_ProjectHierarchy',
value : 22222
},
{
property : 'c_Kanban', //get stories with Kanban state
operator : 'exists',
value : true
}
],
hydrate: ['_TypeHierarchy', 'c_Kanban'],
listeners: {
load: this.onStoriesLoaded,
scope: this
}
}).load({
params : {
compress : true,
removeUnauthorizedSnapshots : true
}
});
},
//make grid of stories with Kanban state
onStoriesLoaded: function(store, data){
var that = this;
var stories = [];
var id;
Ext.Array.each(data, function(record) {
var artifactType = record.get('_TypeHierarchy');
if (artifactType[artifactType.length - 1] == "HierarchicalRequirement") {
id = 'US' + record.get('_UnformattedID');
} else if (artifactType[artifactType.length - 1] == "Defect") {
id = 'DE' + record.get('_UnformattedID');
}
stories.push({
Name: record.get('Name'),
FormattedID: id,
UnformattedID: record.get('_UnformattedID'),
c_Kanban: record.get('c_Kanban')
});
console.log(stories);
});
var myStore = Ext.create('Rally.data.custom.Store', {
data: stories
});
if (!this.down('#allStoriesGrid')) {
this.add({
xtype: 'rallygrid',
id: 'allStoriesGrid',
store: myStore,
width: 500,
columnCfgs: [
{
text: 'Formatted ID', dataIndex: 'FormattedID',
},
{
text: 'Name', dataIndex: 'Name', flex: 1,
},
{
text: 'Kanban', dataIndex: 'c_Kanban'
}
],
listeners: {
celldblclick: function( grid, td, cellIndex, record, tr, rowIndex){
id = grid.getStore().getAt(rowIndex).get('UnformattedID');
console.log('id', id);
that.getStoryModel(id); //to eventually build a grid of Kanban allowed values
}
}
});
}else{
this.down('#allStoriesGrid').reconfigure(myStore);
}
},
getStoryModel:function(id){
console.log('get story model');
var that = this;
this.arr=[];
//get a model of user story
Rally.data.ModelFactory.getModel({
type: 'User Story',
context: {
workspace: '/workspace/11111',
project: 'project/22222'
},
success: function(model){
//Get store instance for the allowed values
var allowedValuesStore = model.getField('c_Kanban').getAllowedValueStore( );
that.getDropdownValues(allowedValuesStore, id);
}
});
},
getDropdownValues:function(allowedValuesStore, id){
var that = this;
//load data into the store
allowedValuesStore.load({
scope: this,
callback: function(records, operation, success){
_.each(records, function(val){
//AllowedAttributeValue object in WS API has StringValue
var v = val.get('StringValue');
that.arr.push(v);
});
console.log('arr', this.arr);
that.getStoryById(id); //former makeStore
}
});
},
getStoryById:function(id){
var that = this;
var snapStore = Ext.create('Rally.data.lookback.SnapshotStore', {
fetch: ['c_Kanban', 'Blocked'],
hydrate:['c_Kanban','Blocked'],
filters : [
{
property : '_UnformattedID',
value : id //15
}
],
sorters:[
{
property : '_ValidTo',
direction : 'ASC'
}
]
});
snapStore.load({
params: {
compress: true,
removeUnauthorizedSnapshots : true
},
callback : function(records, operation, success) {
that.onDataLoaded(records, id);
}
});
},
onDataLoaded:function(records, id){
var times = [];
var measure = 'second';
//-----------------------backlog
var backlog = _.filter(records, function(record) {
return record.get('c_Kanban') === 'backlog';
});
console.log('backlog',backlog);
var cycleTimeFromBacklogToInProgress = '';
if (_.size(backlog) > 0) {
var backlog1 = _.first(backlog);
var backlog2 = _.last(backlog);
var backlogDate1 = new Date(backlog1.get('_ValidFrom'));
if (backlog2.get('_ValidTo') === "9999-01-01T00:00:00.000Z") { //infinity
backlogDate2 = new Date(); //now
}
else{
var backlogDate2 = new Date(backlog2.get('_ValidTo'));
}
cycleTimeFromBacklogToInProgress = Rally.util.DateTime.getDifference(backlogDate2,backlogDate1, measure );
}
times.push(cycleTimeFromBacklogToInProgress);
//console.log(cycleTimeFromBacklogToInProgress);
//----------------------in progress
var inProgress = _.filter(records, function(record) {
return record.get('c_Kanban') === 'in-progress';
});
console.log('in-progress',inProgress);
var cycleTimeFromInProgressToDone = '';
if (_.size(inProgress) > 0) {
var inProgress1 = _.first(inProgress);
var inProgress2 = _.last(inProgress);
var inProgressDate1 = new Date(inProgress1.get('_ValidFrom'));
if (inProgress2.get('_ValidTo') === "9999-01-01T00:00:00.000Z") { //infinity
inProgressDate2 = new Date(); //now
}
else{
var inProgressDate2 = new Date(inProgress2.get('_ValidTo'));
}
cycleTimeFromInProgressToDone = Rally.util.DateTime.getDifference(inProgressDate2,inProgressDate1, measure );
}
times.push(cycleTimeFromInProgressToDone);
//console.log(cycleTimeFromInProgressToDone);
//------------------------done
var done = _.filter(records, function(record) {
return record.get('c_Kanban') === 'done';
});
console.log('done',done);
var cycleTimeFromDoneToReleased = '';
if (_.size(done) > 0) {
var done1 = _.first(done);
var done2 = _.last(done);
var doneDate1 = new Date(done1.get('_ValidFrom'));
if (done2.get('_ValidTo') === "9999-01-01T00:00:00.000Z") { //infinity
doneDate2 = new Date(); //now
}
else{
var doneDate2 = new Date(done2.get('_ValidTo'));
}
cycleTimeFromDoneToReleased = Rally.util.DateTime.getDifference(doneDate2,doneDate1, measure );
}
times.push(cycleTimeFromDoneToReleased);
//console.log(cycleTimeFromDoneToReleased);
/**********
skip first '' element of the this.arr and last 'released' element of this.arr because
do not care for cycle times in first and last kanban states
Originally: arr ["", "backlog", "in-progress", "done", "released"] ,shorten to: ["backlog", "in-progress", "done"]
**********/
this.arrShortened = _.without(this.arr, _.first(this.arr),_.last(this.arr)) ;
console.log('this.arrShortened with first and last skipped', this.arrShortened); //["backlog", "in-progress", "done"]
cycleTimes = _.zip(this.arrShortened, times);
//console.log('cycleTimes as multi-dimentional array', cycleTimes);
cycleTimes = _.object(cycleTimes);
//console.log('cycleTimes as object', cycleTimes); //cycleTimes as object Object {backlog: 89, in-progress: 237, done: 55}
var cycleTimesArray = [];
cycleTimesArray.push(cycleTimes);
console.log('cycleTimesArray',cycleTimesArray);
var store = Ext.create('Rally.data.custom.Store',{
data: cycleTimesArray,
pageSize: 100
});
var columnConfig = [];
_.each(cycleTimes,function(c,key){
var columnConfigElement = _.object(['text', 'dataIndex', 'flex'], ['time spent in ' + key, key, 1]);
columnConfig.push(columnConfigElement);
});
var title = 'Kanban cycle time for US' + id + ' in ' + measure + 's'
if (!this.grid) {
this.grid = this.add({
xtype: 'rallygrid',
title: title,
width: 500,
itemId: 'grid2',
store: store,
columnCfgs: columnConfig
});
}
else{
this.down('#grid2').reconfigure(store);
}
}
});

How to get the values of a ID column in a jqGrid from VB.NET code behind page?

My .ascx page has two jqGrids
$(document).ready(function () {
var searchText = "";
$("#cclist").jqGrid({
//url: ResolveUrl() + '/CC_DoctorList',
datatype: 'local',
// postData: { "CaseNo": CaseNo },
mtype: 'POST',
ajaxGridOptions: { contentType: 'application/json; charset=utf-8' },
serializeGridData: function (jsondata) { return JSON.stringify(jsondata); },
jsonReader: { repeatitems: false, root: "d.rows", page: "d.page", total: "d.total", records: "d.records" },
colNames: ['Remove', 'DoctorID', 'Last Name', 'First Name', 'Address'],
colModel: [
{ name: 'RemoveAction', width: 80, fixed: true, sortable: false, resize: false, align: "center" },
{ name: 'ID', index: 'ID', width: 50, sortable: false, hidden: false },
{ name: 'LastName', index: 'LastName', width: 100, hidden: false, sortable: false },
{ name: 'FirstName', index: 'FirstName', width: 100, hidden: false, sortable: false },
{ name: 'Address', width: 420, hidden: false, sortable: false,
jsonmap: function (obj) {
var street = obj.Address.Address1
var city = obj.Address.City
var state = obj.Address.StateProvince
var zip = obj.Address.PostalCode
if (street != '') { street = street + ', ' }
if (city != '') { city = city + ', ' }
if (state != '') { state = state + ', ' }
var Address = street + city + state + zip
return Address
}
}
],
gridComplete: function () { addDeleteIcon(); },
pager: '#ccpager',
rowNum: 100,
rowList: [100, 200],
sortname: 'LastName',
sortorder: 'desc',
viewrecords: true,
gridview: true,
height: "100%",
caption: 'Send Copy of Report To:',
multiselect: false,
shrinkToFit: false,
loadui: "disabled"
})//.jqGrid('navGrid', '#ccpager', { edit: false, add: true, del: false, search: true });
$("#list").jqGrid({
url: ResolveUrl() + '/DoctorList',
datatype: 'local',
postData: { "searchText": searchText },
mtype: 'POST',
ajaxGridOptions: { contentType: 'application/json; charset=utf-8' },
serializeGridData: function (jsondata) { return JSON.stringify(jsondata); },
jsonReader: { repeatitems: false, root: "d.rows", page: "d.page", total: "d.total", records: "d.records" },
colNames: ['Add', 'DoctorID', 'Last Name', 'First Name', 'Address'],
colModel: [
{ name: 'AddAction', width: 80, fixed: true, sortable: false, resize: false, align: "center" },
{ name: 'ID', index: 'ID', width: 50, sortable: false, hidden: false },
{ name: 'LastName', index: 'LastName', width: 100, hidden: false, frozen: true, sortable: false },
{ name: 'FirstName', index: 'FirstName', width: 100, hidden: false, frozen: true, sortable: false },
{ name: 'Address', width: 420, hidden: false, sortable: false,
jsonmap: function (obj) {
var street = obj.Address.Address1
var city = obj.Address.City
var state = obj.Address.StateProvince
var zip = obj.Address.PostalCode
if (street != '') { street = street + ', ' }
if (city != '') { city = city + ', ' }
if (state != '') { state = state + ', ' }
var Address = street + city + state + zip
return Address
}
}],
gridComplete: function () {
var ids = jQuery("#list").jqGrid('getDataIDs');
for (var i = 0; i < ids.length; i++) {
var cl = ids[i];
var rd = $("#list").getRowData(cl);
var imageid = 'addImg_' + rd['ID']
be = "<div><image style='height:22px;width:20px;' alt='' src='" + ResolveUrl('//img/icons/add_black.png') + "'></image></div>"//"<input type='button' value='Remove' onclick=\"jQuery('#rowed2').editRow('" + cl + "');\" />";
jQuery("#list").jqGrid('setRowData', ids[i], { AddAction: be });
}
},
pager: '#pager',
rowNum: 5,
rowList: [5, 10, 15, 20],
sortname: 'LastName',
sortorder: 'desc',
viewrecords: true,
gridview: true,
caption: 'Search Result',
multiselect: false,
height: "100%",
shrinkToFit: false
})
jQuery("#list").click(function (e) {
var el = e.target;
if (el.nodeName !== "TD") {
el = $(el, this.rows).closest("td");
}
var iCol = $(el).index();
var row = $(el, this.rows).closest("tr.jqgrow");
var rowId = row[0].id;
var noMatch = 0;
if (iCol == 0) {
var rd = $("#list").getRowData(rowId);
var DoctorID = rd['ID'];
//check if the doc already exists in the cc doc list
var ids = jQuery("#cclist").jqGrid('getDataIDs');
if (ids.length == 0) {
ids.length = ids.length + 1;
}
for (var i = 0; i < ids.length; i++) {
var cl = ids[i];
var ccrd = $("#cclist").getRowData(cl);
var newrowid = ids.length + 1;
var ccDoctorID = ccrd['ID'];
if (DoctorID != ccDoctorID) {
noMatch = noMatch + 1;
if (noMatch == ids.length) {
var deleteImageIcon = "<div><image style='height:22px;width:20px;' alt='' src='" + ResolveUrl('//img/icons/trashbin.png') + "'></image></div>"; // onclick=\"jQuery('#cclist').delRowData('" + rowId + "');\"
jQuery("#cclist").jqGrid('addRowData', newrowid, { RemoveAction: deleteImageIcon, ID: rd['ID'], LastName: rd['LastName'], FirstName: rd['FirstName'], Number: rd['Number'], Address: rd['Address'] });
// alert(ids);
// var hdnids = document.getElementById('hdnDocIDs').value;
// hdnids.value = rd['ID'];
//var hdnids = jQuery("#<%=hdnDocIds.ClientID %>").val();
//alert(hdnids);
//hdnids = rd['ID'];
//alert('hdnvalue :' + hdnids);
//$("#<%=hdnlbldocs.ClientID%>").val(rd['ID'].toString() + ',' + $("#<%=hdnlbldocs.ClientID%>").val())
//$("#<%=hdnlbldocs.ClientID%>").val(rd['ID']);
//alert($("#<%=hdnlbldocs.ClientID%>").val());
//alert($("#<%=hdnlbldocs.ClientID%>").val(rd['ID'] + ','));
//alert($("#<%=hdnlbldocs.ClientID%>").val());
//jQuery("#<%=hdnDocIDs.ClientID %>").val(rd['ID']);
//The below works as expected - working.
jQuery("#<%=hdnDocIDs.ClientID %>").val(jQuery("#<%=hdnDocIDs.ClientID%>").val() + ',' + rd['ID']);
alert('All hdn ids : ' + jQuery("#<%=hdnDocIDs.ClientID%>").val());
//Using hidden fields it concatenates the doc ids - working
//alert('in side the for loop ID 2:' + rd['ID'] + ' DoctorID : ' + DoctorID);
//var furl = ResolveUrl() + '/AddCCDoctor';
//var furl = '';
//var param = '{"CaseNo":"' + CaseNo + '", "DoctorID":"' + DoctorID + '" }';
//var param = '{ "DoctorID":"' + DoctorID + '" }';
//var callback = function (msg) { dataHasChanged(); jQuery("#cclist").trigger("reloadGrid"); };
// ajaxJsonCall(furl, param, callback);
//jQuery("#cclist").jqGrid('setGridParam', {datatype: 'json'}).trigger('reloadGrid');
function (msg) { dataHasChanged(); jQuery("#cclist").trigger("reloadGrid"); };
}
}
}
}
});
The #list grid gets loaded by clicking on the Search button that I have not posted in the above code. Once the #list jqGrid is loaded users can click on the rows they are interested in and those rows are added to the #cclist jqgrid.
Now, to make the .ascx more generic I need to be able to write a public method in the code behind to return all the rows IDs (doctorIDs) that are added to the #cclist jqGrid and save the IDs to the database.
Can someone help me out on how to do this?
Thanks for taking time to read my post and posting your comments.
I put the code in a function as below and it is working as expected.
var hiddenField = $("#<%= hdnDocIDs.ClientID %>");
var selectedValues = "";
function updateSelectedRowsHidden() {
var selectedRows = [];
selectedRows = $('#cclist').jqGrid('getDataIDs');
selectedValues = "";
hiddenField.val("");
for (var i = 0; i < selectedRows.length; i++) {
var myRow = $('#cclist').jqGrid('getRowData', selectedRows[i]);
selectedValues = selectedValues + myRow.ID + ",";
}
hiddenField.val(selectedValues);
}
You see this answer, here I'm getting data out of selected rows and sending that to server method. Have a save button in your page like the one in answer
Now in your case you want data from entire grid. So for you code will be like this.
$('#clickMe').click(function() {
var rowsOnPage=[];
var docId=[];
rowsOnPage=$('#cclist').jqGrid('getDataIDs');
for(var i=0;i<rowsOnPage.length;i++)
{
docId.push(rowsOnPage[i].ID);
}
$.ajax({
type: 'POST',
url: '#Url.Action("editMe")',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify({DoctorID:docID })
dataType: "json",
success: function() {
$('#grid').trigger("reloadGrid");
},
error: function() {
alert("error");
}
});
});
and the server method(in ASP.NET MVC) which will be your code behind method in your approach will be like this.
//depends upon dataType of DoctorID(int or string)
public ActionResult editMe(string[] DoctorID)
{
}