How to get cell value in automated mail via Google Sheets (& Script) - google-apps-script

For some time I'm using a document in Google Sheets to fetch price data from a website. When a change is been made within our fetching sheet we receive an email notification (via Google Scripts). This works, but since there are a lot of changes per day I would like to get the cell location that has been changed within the email (so we know where to look directly). Does anyone know how I can change my script to also receive some data from the sheet itself (like the cell location that has been changed, the old cell data and the new cell data)? Thank you!
Script:
function sendEmailAlert() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var data = ss.getActiveSheet().getActiveCell().getA1Notation();
var sheetname = ss.getActiveSheet().getName();
var user = Session.getActiveUser().getEmail();
var Toemail = 'myemail#gmail.com';
var subject = 'New Entry in ' + data + '.' + ss.getName();
var body = 'Your file has a new entry in - ' + sheetname + ' Updated by - ' + user + data
' check file- ' + ss.getUrl();
if(data.indexOf('K2:K29')!=-1.23456789) {
MailApp.sendEmail(Toemail,subject, body);
}
};

Implementation depend on your workflow.
If you can get and store somewhere the old table and then get the new table you can compare them and get all the changes this way:
// old table
const old_table = [
['a1', 'b1', 'c1'],
['a2', 'b2', 'c2'],
['a3', 'b3', 'c3'],
['a4', 'b4', 'c4'],
];
// new table
const new_table = [
['a1', 'b1', 'c1'],
['a2', '🙂', 'c2'],
['🙂', 'b3', '🙃'],
['a4', 'b4', 'c4'],
];
const abc = 'ABCDEFGHIJKLMNOPQRTUVWXYZ';
// get indexes of changed rows
const changed_rows = old_table.map( (row, i) =>
(row.toString() != new_table[i].toString()) ? i : '' ).filter(String);
// get changes for every changed row
const changes = changed_rows.map( r =>
old_table[r].map( (col, i) =>
(col != new_table[r][i] ) ? {
'cell': abc[i] + (r+1),
'old_value': col,
'new_value': new_table[r][i]
} : '' ) .filter(String) ).filter(String).flat();
console.log(changes);
console.table(changes);
Output:
[
{
"cell": "B2",
"old_value": "b2",
"new_value": "🙂"
},
{
"cell": "A3",
"old_value": "a3",
"new_value": "🙂"
},
{
"cell": "C3",
"old_value": "c3",
"new_value": "🙃"
}
]
┌─────────┬──────┬───────────┬───────────┐
│ (index) │ cell │ old_value │ new_value │
├─────────┼──────┼───────────┼───────────┤
│ 0 │ 'B2' │ 'b2' │ '🙂' │
│ 1 │ 'A3' │ 'a3' │ '🙂' │
│ 2 │ 'C3' │ 'c3' │ '🙃' │
└─────────┴──────┴───────────┴───────────┘
Update
Here is another algorithm with the same output:
// old table
const OLD_TABLE = [
['a1', 'b1', 'c1'],
['a2', 'b2', 'c2'],
['a3', 'b3', 'c3'],
['a4', 'b4', 'c4'],
];
// new table
const NEW_TABLE = [
['a1', 'b1', 'c1'],
['a2', '🙂', 'c2'],
['🙂', 'b3', '🙃'],
['a4', 'b4', 'c4'],
];
function get_obj(row, col) {
var old_value = OLD_TABLE[row][col];
var new_value = NEW_TABLE[row][col];
if (old_value == new_value) return '';
return {
'cell': 'ABCDEFGHIJKLMNOPQRTUVWXYZ'[col] + (row+1),
'old value': old_value,
'new value': new_value
}
}
const changes = OLD_TABLE.map( (row, r) =>
row.map((_, c) => get_obj(r, c)) ).flat().filter(String);
console.log(changes);
console.table(changes);
Finally
Sorry, I can't help... :) One-liner is here:
const changes = (tab1, tab2) => tab1.map((row, r) => row.map((_, c) =>
tab1[r][c] == tab2[r][c] ? '' : ({
'cell' : 'ABCDEFGHIJKLMNOPQRTUVWXYZ'[c] + (r + 1),
'old_value' : tab1[r][c],
'new_value' : tab2[r][c] }))).flat(2).filter(String);
console.log(changes(OLD_TABLE, NEW_TABLE));
The same output.

Sure, just add whatever you like to the var body you have going. Here is some sample script for retrieving values from a sheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var activeCell = sheet.getRange("AE13")
var currentCellValue = activeCell.getValue();
You would use the OnChange method
https://developers.google.com/apps-script/reference/script/spreadsheet-trigger-builder
Creating the trigger:
var sheet = SpreadsheetApp.getActive();
ScriptApp.newTrigger("myFunction")
.forSpreadsheet(sheet)
.onChange()
.create();

Related

Can't figure out how to add duplicate count to end of array

I think I'm totally overthinking this but I can't get out of my head. In Google Sheets, I have a list of work orders that look like this:
Work ID Location Start Time Officer Count
123 North 1100
123 North 1100
123 North 1100
222 South 1200
999 North 1400
999 North 1400
333 South 1200
Each work order always has one Officer assigned to it, so I need to count the duplicated work orders and push them to the end of the array under "Officer Count" so it shows how many total Officers needed. For example, 123 would need 3, 222 needs 1, 999 needs 2, and 333 needs 1.
However, the code I have now pops out the right count, just out of order from the original 2D array so I can't push it to the end of the array. Any suggestions?
var rowRange = sheet.getRange(2, 1, 7, 3).getValues();
//Create Work Number array (1D array)
var oneDArr = rowRange.map(function(row){return row[0];});
//Create object to count number of officers to Work Number
var counts = {};
oneDArr.forEach(function(x) { counts[x] = (counts[x] || 0)+1; });
//Create Array from Object
var array = [];
var array = Object.entries(counts);
//Set Values to correct location
// sheet.getRange(11, 1, array.length, array[0].length).setValues(array);
}
Description
Here is an example script to get the count of work orders and place them in the original array.
Code.gs
function test () {
try {
let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
let values = sheet.getDataRange().getValues();
values.shift(); // remove the headers
let ids = values.map( row => row[0] );
ids = [...new Set(ids)]; // create an array of unique ids
console.log(ids);
let count = ids.map( id => 0 ); // initialize count to 0
values.forEach( row => { let index = ids.indexOf(row[0]);
count[index]++;
});
console.log(count);
// now let put back into spreadsheet
ids.forEach( (id,i) => { let j = values.findIndex( row => row[0] === id );
values[j][3] = count[i]; } );
console.log(values);
}
catch(err) {
console.log(err);
}
}
Execution log
8:46:20 PM Notice Execution started
8:46:23 PM Info [ 123, 222, 999, 333 ]
8:46:23 PM Info [ 3, 1, 2, 1 ]
8:46:23 PM Info [ [ 123, 'North', 1100, 3 ],
[ 123, 'North', 1100, '' ],
[ 123, 'North', 1100, '' ],
[ 222, 'South', 1200, 1 ],
[ 999, 'North', 1400, 2 ],
[ 999, 'North', 1400, '' ],
[ 333, 'South', 1200, 1 ] ]
8:46:21 PM Notice Execution completed
References
Array.shift()
Array.map()
Set Object
Array.forEach()
Array.indexOf()
Array.findIndex()

merging 2 arrays into an array with 2 columns

I want to merge 2 arrays in the following format.
array1 = [ "a" , "b" , "c"]
array2 = [ 1 , 2 , 3]
merged_array = [ {"a",1} , {"b",2} , {"c",3}]
The goal is to use this as values of 2 columns and rewrite this back to google sheet.
is my format correct and if yes how should i merge the arrays as said above ?
EDIT:
i decided to use this
var output = [];
for(var a = 0; a <= array1.length; a++)
output.push([array1[a],array2[a]]);
how would this compare to map function, performancewise ?
array1 = [ "a" , "b" , "c"]
array2 = [ 1 , 2 , 3]
merged_array = []
for index, value in enumerate(array1): merged_array.append({value,array2[index]})
print (merged_array)
-> [{'a', 1}, {'b', 2}, {'c', 3}]
Merging two arrays into and array of arrays
function myFunk() {
let array1 = ["a", "b", "c"];
let array2 = [1, 2, 3];
let a = array1.map((e,i) => {return [e,array2[i]];})
Logger.log(JSON.stringify(a));
}
Execution log
4:17:09 PM Notice Execution started
4:17:08 PM Info [["a",1],["b",2],["c",3]]
Array.map()

Google Sheets App Script Merge or Add New Row

I have the following code which gets data from two sheets but can't figure out how update or add a new row in sheet "sumTransaction" where Category, Month & Year are equal.
For example in this example Expense 1, January, 2019 exists in the sumTransaction sheet so it should update the amount value by -3. Where Source A, January, 2019 and Other 1, March, 2019 do not exist in sumTransaction so they should be added to a new row.
function tableToObject() {
const ss = SpreadsheetApp.getActiveSpreadsheet()
const transactionSheet = ss.getSheetByName('Transactions')
const lastRow = transactionSheet.getLastRow()
const lastColumn = transactionSheet.getLastColumn()
const values = transactionSheet.getRange(1, 1, lastRow, lastColumn).getValues()
const [headers, ...originalData] = values.map(([,b,,d,e,,,,,,,,,,,p,q,r,s]) => [b,d,e,p,q,r,s])
const res = originalData.map(r => headers.reduce((o, h, j) => Object.assign(o, { [h]: r[j] }), {}))
console.log(res)
// GroupBy and Sum
const transactionGroup = [...res.reduce((r, o) => {
const key = o.Category + '_' + o.Month + '_' + o.Year
const item = r.get(key) || Object.assign({}, o, {
Amount: 0,
})
item.Amount += o.Amount
item.Key = key
return r.set(key, item)
}, new Map).values()]
console.log(transactionGroup)
const budgetValues = getBudget()
console.log(budgetValues)
// merge or add row
}
function getBudget(){
const ss = SpreadsheetApp.getActiveSpreadsheet()
const sumSheet = ss.getSheetByName('sumTransacation')
const lastRow = sumSheet.getLastRow()
const lastColumn = sumSheet.getLastColumn()
const values = sumSheet.getRange(1, 1, lastRow, lastColumn).getValues()
const [headers, ...originalData] = values.map(([a,b,c,d,e,f]) => [a,b,c,d,e,f])
const res = originalData.map(r => headers.reduce((o, h, j) => Object.assign(o, { [h]: r[j] }), {}))
return res
}
transactionGroup Data
[ { Date: Fri Jan 04 2019 00:00:00 GMT-0700 (Mountain Standard Time),
Category: 'Source A',
Amount: 85,
Month: 'January',
Year: 2019,
Group: 'COGS',
Debit: 'Credit',
Key: 'Source A_January_2019' },
{ Date: Mon Feb 25 2019 00:00:00 GMT-0700 (Mountain Standard Time),
Category: 'Expense 1',
Amount: -3,
Month: 'February',
Year: 2019,
Group: 'Expense',
Debit: 'Debit',
Key: 'Expense 1_February_2019' },
{ Date: Tue Mar 26 2019 00:00:00 GMT-0600 (Mountain Daylight Time),
Category: 'Other 1',
Amount: -4,
Month: 'March',
Year: 2019,
Group: 'Other',
Debit: 'Debit',
Key: 'Other 1_March_2019'
} ]
budgetValues Data
[ { Category: 'Expense 1',
Month: 'January',
Year: 2019,
Group: 'COGS',
Amount: 10,
'Debit/Credit': '' },
{ Category: 'Expense 2',
Month: 'January',
Year: 2019,
Group: 'COGS',
Amount: 10,
'Debit/Credit': '' } ]
Sample Image of sumTransactions Sheet ( i.e. before script )
Sample Image of sumTransactions Sheet ( i.e. after script )
I believe your goal is as follows.
There are 2 sheets which are the source sheet Transactions and the destination sheet sumTransacation.
You want to check the duplicated values between the source and destination sheets. At that time, you want to check the columns "A" to "C" on the destination sheet. So from your sample values, you want to check the values of Category, Month, and Year.
From your explanation,
When I saw your values of transactionGroup Data and budgetValues Data, Category: 'Expense 1' of transactionGroup Data is Month: 'February',. And Category: 'Expense 1' of budgetValues Data is Month: 'January',. When I saw the images of i.e. before script and i.e. after script, Category: 'Expense 1' of transactionGroup Data is removed. In this case, I thought that you might want to add the value of Category: 'Expense 1' of transactionGroup Data to the destination sheet.
When my understanding is correct, how about the following sample script?
Sample script:
I added the script's flow in the script as the comment.
function myFunction() {
const ss = SpreadsheetApp.getActiveSpreadsheet()
// 1. Retrieve values from destination sheet.
const dst = ss.getSheetByName('sumTransacation');
const [headers, ...dstVal] = dst.getDataRange().getValues();
// 2. Retrieve values from source sheet.
const src = ss.getSheetByName('Transactions');
const [srcHead, ...srcVal] = src.getDataRange().getValues();
const srcIdx = headers.reduce((ar, h) => {
const temp = srcHead.indexOf(h);
if (temp > -1) ar.push(temp);
return ar
}, []);
const srcValues = srcVal.map(r => srcIdx.map(i => r[i]));
// 3. Update values of destination sheet.
const obj1 = srcValues.reduce((o, r) => Object.assign(o, {[`${r[0] + r[1] + r[2]}`]: r}), {});
const values1 = dstVal.map(r => {
const temp = obj1[r[0] + r[1] + r[2]];
if (temp) {
return r.slice(0, 4).concat([r[4] + temp[4], r[5]]);
}
return r;
});
// 4. Added new values of source sheet.
const obj2 = dstVal.reduce((o, r) => Object.assign(o, {[`${r[0] + r[1] + r[2]}`]: r}), {});
const values2 = srcValues.reduce((ar, r) => {
if (!obj2[r[0] + r[1] + r[2]]) ar.push(r);
return ar;
}, []);
const values = [headers, ...values1, ...values2];
// 5. Update the destination sheet using new values.
dst.clearContents().getRange(1, 1, values.length, values[0].length).setValues(values);
}
References:
reduce()
map()
Edit:
When I saw your sample Spreadsheet, I noticed that your spreadsheet is different from your sample images. I think that this is the reason of your issue. So for your sample Spreadsheet, I added one more sample script as follows.
Sample script:
function sample2() {
const ss = SpreadsheetApp.getActiveSpreadsheet()
// 1. Retrieve values from destination sheet.
const dst = ss.getSheetByName('sumTransacation');
const [headers, ...dstVal] = dst.getDataRange().getValues();
// 2. Retrieve values from source sheet.
const src = ss.getSheetByName('Transactions');
const [srcHead, ...srcVal] = src.getDataRange().getValues().map(([,b,,d,e,,,,,,,,,,,p,q,r,s]) => [b,d,e,p,q,r,s])
const srcIdx = headers.reduce((ar, h) => {
const temp = srcHead.indexOf(h);
if (temp > -1) {
ar.push(temp);
} else {
ar.push("");
}
return ar
}, []);
const srcValues = srcVal.map(r => srcIdx.map(i => r[i]));
// 3. Update values of destination sheet.
const obj1 = srcValues.reduce((o, r) => Object.assign(o, {[`${r[0] + r[1] + r[2]}`]: r}), {});
const values1 = dstVal.map(r => {
const temp = obj1[r[0] + r[1] + r[2]];
if (temp) {
return r.slice(0, 4).concat([r[4] + temp[4], r[5]]);
}
return r;
});
// 4. Added new values of source sheet.
const obj2 = dstVal.reduce((o, r) => Object.assign(o, {[`${r[0] + r[1] + r[2]}`]: r}), {});
const values2 = srcValues.reduce((ar, r) => {
if (!obj2[r[0] + r[1] + r[2]]) ar.push(r);
return ar;
}, []);
const values = [headers, ...values1, ...values2];
dst.clearContents().getRange(1, 1, values.length, values[0].length).setValues(values);
}
In your sample Spreadsheet, the values of "Month" of "Transactions" is different from that of "sumTransacation" sheet. But, unfortunately, I cannot know your actual format. So, when you want to compare the values, how about changing the format as the same format? Please be careful this.

How to calculate two variables to sum the total from XML element Data then write to sheet

So I have the below code with help from #Tanaike but now I would like to know how to calculate the variables 'qty' minus 'availqty' then write the result to a column by replacing this code: qty ? qty.getText() : "",
Thanks in advance
var totlistings = root.getChild('ActiveList', NS).getChild('PaginationResult', NS).getChild('TotalNumberOfEntries', NS).getValue();
//Logger.log(totlistings);
var itms = root.getChild('ActiveList', NS).getChild('ItemArray', NS).getChildren();
var values = itms.map(e => {
var item = e.getChild('ItemID', NS);
var title = e.getChild('Title', NS);
var url = e.getChild('ListingDetails', NS).getChild('ViewItemURLForNaturalSearch', NS);
var imgurl = e.getChild('PictureDetails', NS).getChild('GalleryURL', NS);
var qty = e.getChild('Quantity', NS);
var availqty = e.getChild('QuantityAvailable', NS)
var sku = e.getChild('SKU', NS);
//Logger.log(sum);
return [sku ? sku.getText(): "",
availqty ? availqty.getText() : "",
qty ? qty.getText() : "",
item ? item.getText() : "",
title ? title.getText() : "",
url ? url.getText() : "",
imgurl ? imgurl.getText() : ""];
});
sheet.getRange(sheet.getLastRow() + 1, 1, values.length, values[0].length).setValues(values);
After trying some more I have manged to do it but for 3 days previous I struggled. Strange how I could see it once I had sent my question on here!
I swopped qty ? qty.getText() : "", with qty ? qty.getText() - availqty.getText() : "",

LINQ-to-SQL GroupBy across Many-to-Many Divide

I have the following issue to solve.
I have an IQueryable list of Invoices, each tied to an Account. Each Account can have multiple Structures, and Accounts and Structures are tied together by a table called StructureAccount. It looks like this:
Invoice ---> Account <-----StructureAccount ----> Structure
I want to query my IQueryable list of Invoices and group by Structure.StructureID or StructureAccount.StructureID. But, because any given invoice can be tied to multiple Structures the best I can get is a LIST of StructureIDs, and therefore my GroupBy is not working.
I feel like I am missing an obvious solution to this.
I should note that I understand that the data in any one Invoice would be counted multiple times if the Invoice were tied to more than one Structure, and this is "solved" by a "PercentAllocationtoStructure" value in the table StructureAccount.
I hope I did a good enough job explaining this problem. Let me know if not.
Hmmm...I might be missing something, but doesn't the following work?
var q = from i in Invoice
join a in Account
on i.AccountID equals a.AccountID
join sa in StructureAccount
on i.AccountID equals sa.AccountID
join s in Structure
on sa.StructureID equals s.StructureID
group i by s.StructureID;
I tested it on the following dummy data:
var Invoice = new [] {
new { InvoiceID = 1, AccountID = 1 },
new { InvoiceID = 2, AccountID = 2 },
new { InvoiceID = 3, AccountID = 3 },
new { InvoiceID = 4, AccountID = 1 },
new { InvoiceID = 5, AccountID = 2 },
new { InvoiceID = 6, AccountID = 3 }
};
var Account = new [] {
new { AccountID = 1 },
new { AccountID = 2 },
new { AccountID = 3 },
};
var StructureAccount = new [] {
new { AccountID = 1, StructureID = 2 },
new { AccountID = 1, StructureID = 3 },
new { AccountID = 2, StructureID = 2 },
new { AccountID = 3, StructureID = 1 },
new { AccountID = 3, StructureID = 2 },
};
var Structure = new [] {
new { StructureID = 1 },
new { StructureID = 2 },
new { StructureID = 3 }
};
And it returns:
StructureID = 2:
InvoiceID's: 1,2,3,4,5,6
StructureID = 3:
InvoiceID's: 1,4
StructureID = 1:
InvoiceID's: 3,6
I'll assume you have the following starting point:
IQueryable<Invoice> _invoices;
First, you need to get a list of all the items that you will be iterating over:
IQueryable<Account> _accounts = _invoices.Select(myInvoice => myInvoice.Account).Distinct();
IQueryable<StructuredAccount> _structuredAccounts = _accounts.SelectMany(myAccount => myAccount.StructuredAccounts);
IQueryable<Structure> _structures = _structuredAccounts.Select(myStructuredAccount => myStructuredAccount.Structure).Distinct();
Next, you need to go back and join your Structure objects to the respective Invoice objects.
For this, you'll:
Get a set of {Structure, Account} pairs:
var structureAccountJoin = _structures.Join(_structuredAccounts, _structure => structure.StructuredID, _structuredAccount => _structuredAccount.StructuredID, (structure, structuredAccount) => new { Structure = structure, Account = structuredAccount.Account });
Get a set of {Structure, Invoice} pairs:
var structureInvoiceJoin = structureAccountJoin.Join(_invoices, myObj => myObj.Account.AccountID, invoice => invoice.AccountID, (myObj, invoice) => new { Structure = myObj.Structure, Invoice = invoice});
Finally, you can group everything by the Structure object:
IQueryable<IGrouping<Structure, Invoice>> groupByStructure = structureInvoiceJoin.GroupBy(obj => obj.Structure, result => result.Invoice);
(GroupBy documentation: http://msdn.microsoft.com/en-us/library/bb534304.aspx)
Now, you can access everything as follows:
foreach(IGrouping<Structure, Invoice> groupEntry in groupByStructure)
{
Structure currentGrouping = groupEntry.Key;
foreach(Invoice inv in groupEntry)
{
// do something;
}
}
As a note, this is a very complex script that requires a lot of steps if you don't have access to the tables directly. You may want to look into creating a StoredProcedure for this instead, as it will be more efficient and you'll be able to use SQL Joins instead. If you have only an IQueryable<Invoice> to work with and access to nothing else, there is probably a design problem somewhere in your architecture.
Nevertheless, this is the way to make it work based on your requirements, if I read them correctly.