I am Unit Testing one of my functions. Here is my code:
public void TestLabels()
{
//Step 1: Creating a mock table with columns exactly like in the real table.
DataTable table = new DataTable();
DataRow mydatarow;
mydatarow = table.NewRow();
//Step 2: Adding the row as same as the Real Data!
mydatarow["Name"] = "Test";
mydatarow["Address"] = "00000 ST.";
mydatarow["ZipCode"] = "77665";
mydatarow["Tracking#"] = "";
table.Rows.Add(mydatarow);
foreach (DataColumn column in table.Columns)
Console.WriteLine(column.ColumnName);
//Step 3: Call method we are testing.
var updateTable = IceTechUPSClient.Instance.CreateLabels(table);
foreach (DataRow row in updateTable.Rows)
{
var trackingNumber = row["Tracking#"].ToString();
Assert.IsFalse(String.IsNullOrWhiteSpace(trackingNumber), "Expecting tracking number generated for every row!");
Assert.IsTrue(File.Exists(trackingNumber + ".gif"));
}
}
Now I am getting an error: Column 'Name' does not belong to table. As you can see I have specified column name "Name" here and also added that particular row. Then why I am getting this error? Any help?
Thanks!
You haven't set up your columns (unless you've missed out some code in your example).
You need to create the columns with the required names before you can access them like this:
var columnSpec = new DataColumn
{
DataType = typeof(string),
ColumnName = "Name"
};
this.table.Columns.Add(columnSpec);
When you read data from the database if you've set AutoGenerateColumns to true (the default) you don't need to do this explicitly as it's done for you behind the scenes.
Related
I'm designing a script that takes an object (jsonData[data]) and inputs its values into a different sheet based on which product it is.
Currently the script inputs all the data into a new row each time the form reaches a new stage, however the form goes through 4 stages of approval and so I'm finding each submission being entered into 4 different rows. Each submission has an "Id" value within the object which remains the same (but each submission could also be on any row in the sheet as it's used a lot).
I'm checking whether the ID exists in the sheet and using iteration to find the row number:
function updatePlatformBulkInfo(jsonData) {
var sheetUrl = "https://docs.google.com/spreadsheets/d/13U9r9Lu2Fq1WTT8pQ128heCm6_gMmH1R4O6u8e7kvBo/edit#gid=0";
var sheetName = "PlatformBulkSetup";
var doc = SpreadsheetApp.openByUrl(sheetUrl);
var sheet = doc.getSheetByName(sheetName);
var rowList = [];
var formId = jsonData["Id"];
var allSheetData = sheet.getDataRange().getValues();
setLog("AllSheetData = " + allSheetData[1][11]) //Logs to ensure data is collected correctly
var rowEdited = false;
var rowNumber = 0;
//Check whether ID exists in the sheet
for (var i = 0; i < allSheetData.length; i++) {
if(allSheetData[i][11] == formId) {
rowEdited = true;
} else {
rowNumber += 1;
}
}
My issue is with the next part:
//Append row if ID isn't duplicate or update row if duplicate found
if (rowEdited == false) {
for (var data in jsonData) {
rowList.push(jsonData[data])
}
setLog("***Row List = " + rowList + " ***");
setLog("***Current Row Number = " + rowNumber + " ***");
sheet.appendRow(rowList);
} else if(rowEdited == true){
var newRowValue = jsonData[data];
sheet.getRange(rowNumber, 1).setValues(newRowValue);
}
Everything works fine if the duplicate isn't found (the objects values are appended to the sheet). But if a duplicate is found I'm getting the error:
Cannot find method setValues(string)
This looks to me like i'm passing a string instead of an object, but as far as I'm aware I've already converted the JSON string into an object:
var jsonString = e.postData.getDataAsString();
var jsonData = JSON.parse(jsonString);
How can I modify my script to write the updated data to the matched row?
It's unclear based on your code whether or not you will actually write to the correct cell in the case of a duplicate. As presented, it looks as though you loop over the sheet data, incrementing a row number if the duplicate is not found. Then, after completing the loop, you write to the sheet, in the row described by rowNumber, even though your code as written changes rowNumber after finding a duplicate.
To address this, your loop needs to exit upon finding a duplicate:
var duplicateRow = null, checkedCol = /* your column to check */;
for(var r = 0, rows = allSheetData.length; r < rows; ++r) {
if(allSheetData[r][checkedCol] === formId) {
// Convert from 0-base Javascript index to 1-base Range index.
duplicateRow = ++r;
// Stop iterating through allSheetData, since we found the row.
break;
}
}
In both cases (append vs modify), you seem to want the same output. Rather than write the code to build the output twice, do it outside the loop. Note that the order of enumeration specified by the for ... in ... pattern is not dependable, so if you need the elements to appear in a certain order in the output, you should explicitly place them in their desired order.
If a duplicate ID situation is supposed to write different data in different cells, then the following two snippets will need to be adapted to suit. The general idea and instructions still apply.
var dataToWrite = [];
/* add items to `dataToWrite`, making an Object[] */
Then, to determine whether to append or modify, test if duplicateRow is null:
if(dataToWrite.length) {
if(duplicateRow === null) {
sheet.appendRow(dataToWrite);
} else {
// Overwriting a row. Select as many columns as we have data to write.
var toEdit = sheet.getRange(duplicateRow, 1, 1, dataToWrite.length);
// Because setValues requires an Object[][], wrap `dataToWrite` in an array.
// This creates a 1 row x N column array. If the range to overwrite was not a
// single row, a different approach would be needed.
toEdit.setValues( [dataToWrite] );
}
}
Below is the most basic solution. At the end of this post, I'll expand on how this can be improved. I don't know how your data is organized, how exactly you generate new unique ids for your records, etc., but let's assume it looks something like this.
Suppose we need to update the existing record with new data. I assume your JSON contains key-value pairs for each field:
var chris = {
id:2,
name: "Chris",
age: 29,
city: "Amsterdam"
};
Updating a record breaks down into several steps:
1) Creating a row array from your object. Note that the setValues() method accepts a 2D array as an argument, while the appendRow() method of the Sheet class accepts a single-dimension array.
2) Finding the matching id in your table if it exists. The 'for' loop is not very well-suited for this idea unless you put 'break' after the matching id value is found. Otherwise, it will loop over the entire array of values, which is redundant. Similarly, there's no need to retrieve the entire data range as the only thing you need is the "id" column.
IMPORTANT: to get the row number, you must increment the array index of the matching value by 1 as array indices start from 0. Also, if your spreadsheet contains 1 or more header rows (mine does), you must also factor in the offset and increment the value by the number of headers.
3) Based on the matching row number, build the range object for that row and update values. If no matching row is found, call appendRow() method of the Sheet class.
function updateRecord(query) {
rowData = [];
var keys = Object.keys(query);
keys.forEach(function(key){
rowData.push(query[key]);
})
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheets()[0];
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
var idColumn = 1;
var ids = sheet.getRange(2, idColumn, sheet.getLastRow() - 1, 1).getValues();
var i = 0;
var matchedRow;
do {
if (ids[i] == query.id) { matchedRow = i + 2; }
i++;
} while (!matchedRow && i < ids.length);
if (matchedRow) {
var row = sheet.getRange(matchedRow, idColumn, 1, rowData.length);
row.setValues([rowData]);
} else {
sheet.appendRow(rowData);
}
}
NOTE: if your query contains only some fields that need to be updated (say, the 'id' and the 'name' field), the corresponding columns for these fields will be
headers.indexOf(query[key]) + 1;
Possible improvements
If the goal is to use the spreadsheet as a database and define all CRUD (Create, Read, Write, Delete) operations. While the exact steps are beyond the scope of the answer, here's the gist of it.
1) Deploy and publish the spreadsheet-bound script as a web app, with the access set to "anyone, even anonymous".
function doGet(e) {
handleResponse(e);
}
function doPost(e) {
handleRespone(e);
}
function handleResponse(e) {
if (e.contentLength == -1) {
//handle GET request
} else {
//handle POST request
}
}
2) Define the structure of your queries. For example, getting the list of values and finding a value by id can be done via GET requests and passing parameters in the url. Queries that add, remove, or modify data can be sent as payload via POST request. GAS doesn't support other methods besides GET and POST, but you can simulate this by including relevant methods in the body of your query and then selecting corresponding actions inside handleResponse() function.
3) Make requests to the spreadsheet URL via UrlFetchApp. More details on web apps https://developers.google.com/apps-script/guides/web
I am populating data using 2 views inside a data delegate. I have to calculate running balance by sorting the based on transaction date. How do i sort the data and populate the running balance.
The delegate code is given below.
[PXVirtualDAC]
public PXSelectOrderBy<FundBalanceData, OrderBy<Asc<FundBalanceData.tranDate>>> fbdata;
protected virtual IEnumerable FBData()
{
List<FundBalanceData> importlist = new List<FundBalanceData>();
PXSelectBase<ARPayment> cmd = new PXSelectJoin<ARPayment, InnerJoin<GLTran, On<ARPayment.batchNbr, Equal<GLTran.batchNbr>>,
InnerJoin<PMProject, On<GLTran.projectID, Equal<PMProject.contractID>>>>,
Where<GLTran.projectID, Equal<Current<PMProject.contractID>>,
And<ARPayment.docType, Equal<ARPaymentType.prepayment>>>>(Base);
foreach(PXResult<ARPayment,GLTran,PMProject> line in cmd.Select())
{
FundBalanceData data = new FundBalanceData();
ARPayment arp = line;
PMProject pmp = line;
GLTran glt = line;
data.BAccountID = arp.CustomerID;
data.CreditAmount = glt.CreditAmt;
data.DebitAmount = glt.DebitAmt;
data.RefNbr = arp.RefNbr;
data.DocType = arp.DocType;
data.Desc = arp.DocDesc;
data.TranDate = arp.DocDate;
importlist.Add(data);
}
PXSelectBase<APAdjust> cmd2 = new PXSelectJoin<APAdjust, LeftJoin<GLTran, On<APAdjust.adjBatchNbr, Equal<GLTran.batchNbr>, And<APAdjust.adjdAPAcct, Equal<GLTran.accountID>>>,
InnerJoin<APPayment, On<APAdjust.adjgRefNbr, Equal<APPayment.refNbr>, And<APAdjust.adjgDocType, Equal<APPayment.docType>>>,
InnerJoin<APTran,On<APTran.refNbr,Equal<APAdjust.adjdRefNbr>,And<APTran.tranType,Equal<APAdjust.adjdDocType>>>>>>,
//InnerJoin<GLTran, On<APTran.projectID, Equal<GLTran.projectID>>>>>,
Where<APAdjust.adjgDocType, Equal<APPaymentType.prepayment>, And<APTran.projectID, Equal<Current<PMProject.contractID>>, And<GLTran.accountID, NotEqual<APPayment.aPAccountID>>>>>(Base);
foreach (PXResult<APAdjust,GLTran,APPayment,APTran> line in cmd2.Select())
{
FundBalanceData data = new FundBalanceData();
APPayment arp = line;
GLTran glt = line;
APAdjust apd = line;
data.BAccountID = arp.VendorID;
data.CreditAmount = glt.CreditAmt;
data.DebitAmount = glt.DebitAmt;
data.RefNbr = arp.RefNbr;
data.DocType = arp.DocType;
data.Desc = arp.DocDesc;
data.TranDate = arp.DocDate;
importlist.Add(data);
}
decimal? balance = decimal.Zero;
foreach(FundBalanceData data in importlist)
{
balance = balance + (data.CreditAmount - data.DebitAmount);
data.Balance = balance;
}
return importlist;
}
The array is not sorted by TranDate and the balance calculated is wrong
The result after implementing Samvel Petrosov suggestion
I was trying to sort the array on DateTime? and missed used the Value property. I have tried Samvel Petrosov suggestion and it worked.
Here is the part about sorting of the Result set of data views from T200 Acumatica Framework Fundamentals Course:
A data view executes the delegate by the following rules:
If a delegate is defined, invoke the delegate
If the delegate returns null, execute the BQL command
If the delegate returns an object, reorder the result according to the OrderBy clause of the BQL command
If a delegate is not defined, execute the BQL command
The result set returned by the data view is always sorted by the ORDER
BY clause specified in the type of the data view object. If you sort
data records in a different way within the delegate, the result set
will be reordered before it is returned by the data view.
The calculation of the Balance in your code is done before the return of the result set. That is way it is not calculated as you are waiting. The sorting is taking place after you return the result set.
UPDATE 1
Change your cmd and cmd2 to the following queries:
PXSelectBase<ARPayment> cmd = new PXSelectJoin<ARPayment, InnerJoin<GLTran, On<ARPayment.batchNbr, Equal<GLTran.batchNbr>>,
InnerJoin<PMProject, On<GLTran.projectID, Equal<PMProject.contractID>>>>,
Where<GLTran.projectID, Equal<Current<PMProject.contractID>>,
And<ARPayment.docType, Equal<ARPaymentType.prepayment>>>,OrderBy<Asc<ARPayment.DocDate>>>(Base);
PXSelectBase<APAdjust> cmd2 = new PXSelectJoin<APAdjust, LeftJoin<GLTran, On<APAdjust.adjBatchNbr, Equal<GLTran.batchNbr>, And<APAdjust.adjdAPAcct, Equal<GLTran.accountID>>>,
InnerJoin<APPayment, On<APAdjust.adjgRefNbr, Equal<APPayment.refNbr>, And<APAdjust.adjgDocType, Equal<APPayment.docType>>>,
InnerJoin<APTran,On<APTran.refNbr,Equal<APAdjust.adjdRefNbr>,And<APTran.tranType,Equal<APAdjust.adjdDocType>>>>>>,
//InnerJoin<GLTran, On<APTran.projectID, Equal<GLTran.projectID>>>>>,
Where<APAdjust.adjgDocType, Equal<APPaymentType.prepayment>, And<APTran.projectID, Equal<Current<PMProject.contractID>>, And<GLTran.accountID, NotEqual<APPayment.aPAccountID>>>>,OrderBy<Asc<APPayment.DocDate>>>(Base);
UPDATE 2
Try to add additional sorting by transaction date before the last loop:
importlist= importlist.OrderBy(x => x.TranDate.Value).ToList();
or if TranDate is not DateTime?
importlist= importlist.OrderBy(x => x.TranDate).ToList();
I am trying to copy data from one list to other list (both lists are on different sites) along with lookup columns. But, I am getting an error for lookup field as:
Value does not fall within the expected range
Code works and data gets copied for other non-lookup fields. I tried every possible way including increasing List View Lookup Threshold and all possible ways of code but still error persists at ExecuteQuery().
Below is my code for lookup field:
if (field is FieldLookup && field.InternalName == "Country")
{
var CountryLookup = (item.FieldValues["Country"] as FieldLookupValue).LookupValue.ToString();
var CountryLookupId = (item.FieldValues["Country"] as FieldLookupValue).LookupId.ToString();
FieldLookupValue flvRDS = new FieldLookupValue();
flvRDS.LookupId = int.Parse(CountryLookupId);
itemToCreate["Country"] = flvRDS;
itemToCreate.Update();
destContext.ExecuteQuery();
}
Help is really appreciated.
I assume item is the new ListItem you're trying to create on your target list.
But you're never in fact reading any value from field here! So basically, you're trying to set your new FieldLookup.LookupId with the item["Country"].LookupId, which should logically be empty at this moment.
Here's a method I use to retrieve a lookup field ListItem from a value, feel free to modify it to fit your need, since I don't know how you want to retrieve it (SPList is an alias for Microsoft.SharePoint.Client.List).
private ListItem GetLookupItem(FieldLookup lookupField, string lookupValue)
{
string mappingField = lookupField.LookupField;
Microsoft.SharePoint.Client.List lookupList = Context.Web.Lists.GetById(new Guid(lookupField.LookupList));
Context.Load(lookupList);
Context.ExecuteQuery();
ListItemCollection libListItems = lookupList.GetItems(CamlQuery.CreateAllItemsQuery());
Context.Load(libListItems, items => items.Include(
itemlookup => itemlookup.Id,
itemlookup => itemlookup[mappingField]));
Context.ExecuteQuery();
foreach (ListItem mappedItem in libListItems)
{
object mappedField = mappedItem[mappingField];
if (mappedField != null && mappedField.ToString().Equals(lookupValue))
return mappedItem;
}
return null;
}
Now that you have the corresponding ListItem, you can set your item.LookupId with its Id:
if (field is FieldLookup && field.InternalName == "Country")
{
FieldLookupValue flvRDS = new FieldLookupValue();
flvRDS.LookupId = GetLookupItem(field as FieldLookup, "France").Id; // here, dunno how you get your country's name
itemToCreate["Country"] = flvRDS;
itemToCreate.Update();
destContext.ExecuteQuery();
}
Feel free to add some more previous code if you want an answer more suited for your specific issue.
I am iterating through a collection of KeyValuePair, then copying the Key and Value to a newly-created class as follows :
Classes.MemberHierarchy membHier = new Classes.MemberHierarchy();
List<Classes.MemberHierarchy> membHierList = new List<Classes.MemberHierarchy>();
foreach (KeyValuePair<string, string[]> acct in acctData)
{
membHier.entityName = acct.Key;
membHier.Accounts = acct.Value;
membHierList.Add(membHier);
}
Problem is that upon the 2nd iteration, membHierList properties are immediately getting overwritten by the values in the first iteration. It's very strange.
So upon first iteration, membHier.entityName is "ABC Member" and the Accounts get populated with the string array no problem.
Then upon 2nd iteration, membHier.entityName is "XYZ Member".
Now "XYZ Member" occupies both slots as follows
membHierList[0].base.entityName = "XYZ Member"
membHierList[1].base.entityName = "XYZ Member"
Do I have an object conflict up above ?
Thank you in advance....
Bob
No it is not strange because you declare and initialize the object membHier just one time before the loop and then try to insert in the new list the same object.
This will resolve your problem
List<Classes.MemberHierarchy> membHierList = new List<Classes.MemberHierarchy>();
foreach (KeyValuePair<string, string[]> acct in acctData)
{
Classes.MemberHierarchy membHier = new Classes.MemberHierarchy();
membHier.entityName = acct.Key;
membHier.Accounts = acct.Value;
membHierList.Add(membHier);
}
Declaring a NEW instance of the object MemberHierarchy at every loop will populate the list with different instances and every instance has its own values.
Instead, with the initialization outside the loop, at every iteration you update the same instance with the extracted values. But membHier is a reference type, so if not reinitialized, it points at the same memory where are stored the values of the first entity and you effectively overwrite these values with the second entity. At the end, all the elements in the list points to the same memory location and these memory locations contains the data of the last entity
You need to move the construction of membHier to the inside of the foreach loop. The way your code is written now, only one instance of the MemberHierarchy class is created.
List<Classes.MemberHierarchy> membHierList = new List<Classes.MemberHierarchy>();
foreach (KeyValuePair<string, string[]> acct in acctData)
{
Classes.MemberHierarchy membHier = new Classes.MemberHierarchy();
membHier.entityName = acct.Key;
membHier.Accounts = acct.Value;
membHierList.Add(membHier);
}
using current C# 4.0 syntax it would look like:
var membHierList = new List<Classes.MemberHierarchy>();
foreach (var acct in acctData)
{
membHierList.Add(new Classes.MemberHierarchy
{
entityName = acct.Key;
Accounts = acct.Value;
});
}
I am not sure if I am going about this the correct way but I have a c# method which loads an excel sheet into a 2 dimentional object array. In this array item 1,1 - 1,16 contain headers, then 2-1 - 2-16 contain data that match up with those headers as do x-1 - x-16 from there on in. I would like to turn this array into a data table so ultimately I can have it in a format I will then import into an access or SQL server db depending on a clients needs. I have tried using the following code to no avail, but I have a feeling I am way off. Any help on this would be very much appreciated.
private void ProcessObjects(object[,] valueArray)
{
DataTable holdingTable = new DataTable();
DataRow holdingRow;
holdingTable.BeginLoadData();
foreach(int row in valueArray)
{
holdingRow = holdingTable.LoadDataRow(valueArray[row], true);
}
}
Any chance you're using a repository pattern (like subsonic or EF) or using LinqToSql?
You could do this (LinqToSql for simplicity):
List<SomeType> myList = valueArray.ToList().Skip([your header rows]).ConvertAll(f => Property1 = f[0] [the rest of your convert statement])
DataContext dc = new DataContext();
dc.SomeType.InsertAllOnSubmit(myList);
dc.SubmitChanges();