Google Apps Script Email Automation Errors - google-apps-script

I asked this question and was able to have emails auto-send using the modified script shared with me in the answer...however I'm running into a couple of issues.
The Google Sheet has one tab with data imported via a Google Sheets add-on called Data Connector. It auto-refreshes data connected from Salesforce, to Sheets, every 24 hours. In the "First Time Users" tab I'm using a query to pull all new users from the imported data tab and running the script off of this tab. Newest users are always added to the top of the sheet, so rows of data which the script has already sent an email based on, will move down as new data is added to the top of the sheet.
I want this script to run anytime the sheet changes (this will typically happen when the imported data refreshes) and I want an email to be sent out to a specific email address, if there's a new user (one email per new user and multiple new users may be added at one time).
This is the script I got from my last posted question:
function email() {
var ActiveSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("First Time Users");
var StartRow = 2;
var LastRow = ActiveSheet.getLastRow();
var RowRange = LastRow - StartRow + 1;
var WholeRange = ActiveSheet.getRange(StartRow, 1, RowRange, 9);
var AllValues = WholeRange.getValues();
var ranges = [];
for (var i = 0; i < AllValues.length; i++) {
var CurrentRow = AllValues[i];
var EmailSent = CurrentRow[8];
if (CurrentRow[7] == "Y" && EmailSent != "Y") {
var message =
"<p><b>Request: </b>" + CurrentRow[0] + "</p>" +
"<p><b>Account: </b>" + CurrentRow[1] + "</p>" +
"<p><b>Appointment Created Date: </b>" + CurrentRow[4] + "</p>" +
"<p><b>User: </b>" + CurrentRow[5] + "</p>";
var setRow = i + StartRow;
var SendTo = "testemail#gmail.com";
var Subject = "First Time User Submitted Ask: " + CurrentRow[1];
MailApp.sendEmail
({
to: SendTo,
cc: "",
subject: Subject,
htmlBody: message,
});
ranges.push("I" + setRow);
}
}
ActiveSheet.getRangeList(ranges).setValue("Y");
}
I added the following trigger, hoping it would execute the script every time new users are added to the "First Time Users" tab:
My issues are the following:
Trigger/execution error: Exception: Ranges must have at least one range.
at email(Code:103:15)
The email notification contains information from the last row of the sheet, moving up, rather than the row with the new user which is the row(s) where there's no "Y" under the "Sent" column, Column I. The "Y" is added to this column once the script executes and sends out an email. (Ex., if I receive 3 notification emails for 3 new users, the data in the email has data from the last row, the next email has data from the second to last row, and the third email has data from the third to last row in the sheet).
Here's a sample sheet of what the "First Time Users" tab looks like. Any help is greatly appreciated.
Thanks!

I just tried the script and it's working flawlessly, I used a Time-Driven trigger instead of an OnChange trigger, in your situation as you mentioned that the Salesforce connector is refreshing data every 24 hours it's best for you to have the trigger run every couple of minutes.
You may want to try using it as a Time driven trigger instead. This is how the latest test emails were received:
This is how the emails were received in my mailbox:
This is how the execution is displayed in the logs:
As you may notice there are no errors shown in the logs so I would recommend using Time-driven triggers instead.

Related

Google sheet script linked sending duplicate emails when form is submitted

I'm having an issue where a script I wrote is sending duplicate emails when a form is submitted. The script is also executing on occasion when I simply open the spreadsheet. I only have one trigger set up to run the script when the form is submitted, and I'm the only one with edit access to the sheet. I tried deleting the script project altogether and creating a new one, which didn't resolve the issue. I'm not sure if there's anything wonky with my script, but here it is:
function sendEmails() {
var sheet = SpreadsheetApp.getActive().getSheetByName('Raw Data'); // Gets Raw Data Sheet
var lastRow = sheet.getLastRow(); // Gets last row of sheet everytime the form is submitted
var lastColumn = sheet.getLastColumn(); // Gets last column of sheet everytime the form is submitted
var value = sheet.getRange(lastRow,1,lastRow,lastColumn).getValues().toString();
var comments = sheet.getRange(lastRow, 41).getValue().toString(); // Gets additional comments from inspection form submitted
if (value.indexOf("NOT OK") > -1) {
MailApp.sendEmail({
to: "test#test.com",
subject: 'Machine Issue',
htmlBody: "An inspection of the xyz machine has returned issues: " + "<br/><br/>"
+ "<b>" + comments + "</b>" + "<br/><br/>" +
" Click " + ' <b>HERE</b>'
+ " to see the last inspection report.",
});
} // Produces email based on defined parameters.
}
I've also tried deleting the trigger and setting up a new one, which hasn't worked either.
Check your executions log. If you have multiple executions but only one Form Responses line, this is a known bug with the form submit trigger. The only way to get around it is to use a script lock.
Like this:
SpreadsheetApp.flush();
var lock = LockService.getScriptLock();
try {
lock.waitLock(15000); // wait 15 seconds for others' use of the code section and lock to stop and then proceed
} catch (e) {
Logger.log('Could not obtain lock after 30 seconds.');
return HtmlService.createHtmlOutput("<b> Server Busy please try after some time <p>")
// In case this a server side code called asynchronously you return a error code and display the appropriate message on the client side
return "Error: Server busy try again later... Sorry :("
}
START NORMAL CODE HERE
I've only got one script where this is really a problem but it was a really awful problem, up to six executions per form submission and the scriptlock is the tidiest way to lock it down. If your code itself takes less than 15 seconds reduct your waitlock time so the extra copies give up faster. If you use this method you'll still see the extra copies in the executions log, but they'll only be 15 seconds long. It's very satisfying to watch them caught and killed in this way.
I had an issue with multiple emails on form submission too. I noticed that my code was sending an email for every send email function i had. Like, it was based on score email, so what i did is, i closed function for every alternative, so, it would check points, if it didnt met the requirements, then it just wouldnt send email, and go for the next function, until it found a the function that met requirementes.
I think that in your code, its reading send emails twice, so he reads on the send emails, look for requirements, and sends. Then, it reads the next send emails, looks again for requirements and send email again.
Its like your giving an order to send emails twice.
This line has a problem:
var value = sheet.getRange(lastRow,1,lastRow,lastColumn).getValues().toString();
Let's say lastRow is 20. Then this code with get the last row plus the next 19 rows of values which presumably are all blank. The third parameter is the number of rows and the fourth is the number of columns.
It's better to pass the event object into the function and use e.values rather than having to go get the last line. If you get multiple form submissions one after another you may in fact be getting the wrong data.
This line also has a problem:
htmlBody: "An inspection of the xyz machine has returned issues: " + "<br/><br/>"
+ "<b>" + comments + "</b>" + "<br/><br/>" +
" Click " + ' <b>HERE</b>'
+ " to see the last inspection report.",
});
The comma at the end of the htmlBody parameter should be removed.
Try this code:
function sendEmails(e) {
var value=e.values.toString();
var comments=e.values[40];
if (value.indexOf("NOT OK") > -1) {
var html="An inspection of the xyz machine has returned issues: ";
html+="<br/><br/>" + "<b>" + comments + "</b>" + "<br/><br/>" + " Click "
html+=' <b>HERE</b>' + " to see the last inspection report.";
MailApp.sendEmail({to: "test#test.com",subject: 'Machine Issue',htmlBody: html});
//Logger.log(html);
}
}
Form Submit Event Object
I played around with this a little more and according to #J. G. there is a problem with onFormSubmit triggers returning multiple triggers. I solved the situation for my testing by using the following code which I was using to log onFormSubmit triggers.
function testFormSubmission(ev) {
var lock=LockService.getUserLock();
try{
if(ev.values && !ev.values[1]){throw('Spurious Returns Error');}
if(lock.tryLock(10000)) {
var ss=SpreadsheetApp.getActive();
var sh=ss.getSheetByName('LogSheet');
var tA=[Utilities.formatDate(new Date(), Session.getScriptTimeZone(),"d/M/yyyy HH:mm:ss")];
tA=tA.concat(ev.values);
tA.splice(tA.length-1,1,ev.triggerUid,ev.range.rowStart,ev.range.columnEnd,JSON.stringify(ev.values));
sh.appendRow(tA);
lock.releaseLock();
}
}
catch(error){
console.error(error);
return;
}
}

Copy Paste Script Google Sheets

I work for a major Motor Manufacturer and I need some help with a google sheets script that I need to write (I assume a script is the way forward?)
I have a google sheets file that is used to monitor issues and planned improvements for multiple departments. The workbook has multiple tabs that I need to somehow copy and paste data around within it.
I have included a sample file to try and help explain it, it's quite a complicated file to try and explain...I will do my best. It sounds like I need to use a script but I am new to using scripts.
Link to Sheet
Explanation of file:
On a weekly basis I update the sheet with latest data into the "Master data" sheet, this sheet feeds the "electrical" sheet using a query.The dept owner for electrical updates updates the "electricalinput" sheet with his actions and timing and this feeds into the "electrical" sheet also. The "electrical" sheet in turn feeds the "improvement data" sheet which feeds the chart.
It is arranged like this so what when new issues are added and the order changes the comments follow on the "electrical" sheet, and the owner only needs to update the "electricalinput" sheet with comments that he hasn't already done.
What I would like try and do:
I would like to be able to run a script that filters or extracts anything in the electrical sheet with #N/A against it (which means the 3 cell combination hasn't been found in the input sheet) and copy and paste just those items into the "electricalinput" sheet at the next available line that isn't populated. In the actual file there are multiple depts so variations of the script will have to run to cover the different depts.
I've updated the spreadsheet that you shared with relevant code.
You can run the script via Add Ons->Master Data Utils->Clean Up Keys menu. The menu was created in the OnOpen function from this script.
Note that there are a couple of requirements as below for the script to work.(already taken care of in the shared ss)
Departments lists and details helpful for processing all the data as shown below.
There should be a NamedRange called Department_List which would be top left cell of the table shown above.
Column to Clean -> Action Summary
is the column we would be using to search of #N/A
Display Sheet Name -> Electrical
is where the #N/A would be.
Input Sheet Name -> Electricalinput
is the sheet name that would be updated.
Copy Cols From(Disp) -> B:D
the columns that would be copied over
Similar to d but this is where sata would be copied to
But at the moment the code doesn't use this and just pastes into first available cell in col B
Here's the code I attempted, let me know of any suggestions or alterations that would make it easier to use.
function cleanComponentList(curSS) {
Logger.log("Init script cleanComponentList 'Department_List'")
curSS = curSS || SpreadsheetApp.getActiveSpreadsheet()
var deptListStart = curSS.getRangeByName("Department_List")
if(deptListStart==null){
Logger.log("This script cannot be used without the named range 'Department_List'")
return
}
var dept = deptListStart.offset(1,0)
while( ! dept.isBlank() ){
Logger.log("Started processing " + dept.getDisplayValue() + " Dept.")
//Get Department Sheet
var deptSht = curSS.getSheetByName(dept.offset(0,1).getValue())
//Get Column that has Action Summary in department sheet
var actSummCol=deptSht.getRange(2, 1)
//Get Department Input Sheet (Target Sheet where we need to copy the final values to
var deptInputSht = curSS.getSheetByName(dept.offset(0,2).getValue())
//Find column with the "Column Header"/"Summary" in which we search #N/A
while( ! (actSummCol.getDisplayValue() === dept.offset(0,3).getDisplayValue())){
var tmp = actSummCol.getDisplayValue()
var actColRng = actSummCol.getA1Notation()
actSummCol=actSummCol.offset(0,1)
}
var actSummColAddress = actSummCol.getA1Notation();
Logger.log("Found key \"" + dept.offset(0,3).getDisplayValue() + "\" for " + dept.getDisplayValue() + " # " + actSummColAddress)
var errRows = []
//Get all rows that have error Action Summary
var lastRow = deptSht.getLastRow()
for(nRow=1;nRow<lastRow;nRow++){
//There should be a better of checking #N/A
if(actSummCol.offset(nRow,0).getDisplayValue().equals("#N/A")){
errRows.push(nRow+actSummCol.getRow())
}
}
Logger.log("Got " + errRows.length + " error rows for " + dept.getDisplayValue() + " Dept.")
//Get Cell where data append should start from.
var deptInputLastAvailableRow = deptInputSht.getRange("B1")
while(!deptInputLastAvailableRow.isBlank() || deptInputLastAvailableRow.isPartOfMerge()){
deptInputLastAvailableRow=deptInputLastAvailableRow.offset(1,0)
}
Logger.log(dept.getDisplayValue() + " Dept." + " Input will be updated from " + deptInputLastAvailableRow.getA1Notation())
//Copy CDE from the filtered rows to Department Input Sheet
var srcCols=deptSht.getRange(dept.offset(0,4).getDisplayValue())
//There should be a better way of iterating. for-of throws syntax error!!!
for(idx in errRows){
var row = errRows[idx];
var lc = srcCols.getLastColumn()
var fc = srcCols.getColumn()
var errKeyRangeAddress=srcCols.getCell(row,1).getA1Notation() + ":" + srcCols.getCell(row,lc-fc + 1).getA1Notation()
var errKeyRange=deptSht.getRange(errKeyRangeAddress)
errKeyRange.copyTo(deptInputLastAvailableRow)
deptInputLastAvailableRow=deptInputLastAvailableRow.offset(1,0)
}
Logger.log("Copied " + errRows.length + " entries to Sheet \"" + deptInputSht.getName() + "\"")
Logger.log("Finished Processing " + dept.getDisplayValue() + " Dept.")
dept=dept.offset(1,0)
}
}
function onOpen(){
//Choose which way you want your menu to appear.
//SpreadsheetApp.getActiveSpreadsheet().addMenu("Monitor", [{name:"Clean Up Keys",functionName:"cleanComponentList"}])
SpreadsheetApp.getUi().createAddonMenu().addItem("Clean Up Keys", "cleanComponentList").addToUi()
}
This is a simple copy script to help you get started. copyTo is documented here. The rest of the commands can be found in the same documentation.
After you've done a few of your own scripts you'll find it a lot easier to make up your own rather than spending all of your time copying scripts.
function copyPasteSelectedRows()
{
var ss=SpreadsheetApp.getActive();
var sh1=ss.getSheetByName('Sheet2');
var sh0=ss.getSheetByName('Sheet1');
var rg0=sh0.getDataRange();
var vA=rg0.getValues();
for(var i=1;i<vA.length;i++)
{
if(vA[i][10]==1)//condition to meet to copy
{
var src=sh0.getRange(i+1,1,1,vA[i].length);//source range
var tar=sh1.getRange(sh1.getLastRow() + 1,1,1,vA[i].length);//target range
src.copyTo(tar);
}
}
}
This is Sheet1:
This is Sheet2 after the copy:

Sending Email Notification Google Sheets

My company has created a Google Form set up to make one of our processes a lot easier. The Google Form is based off of the Master Spreadsheet that contains all of the data inputted from the Form. This spreadsheet then filters out the the form submission and sends the data to each department’s spreadsheet, which as previously stated before, gets all of the information from the "Master Spreadsheet."
We previously had it set up so when employees would go in and approve or deny these requests in their spreadsheet, we would receive an email notification if someone entered "Approved" or "Denied." Recently we changed it so if a certain person submitted a request for a customer, it would be automatically approved, but when we did this the email notification stopped working because no one is manually entering in "Approved" or "Denied" for these requests. It still works when it's manually typed in, but when the cell is automatically filled in, the sendNotification does not work.
Since no actual data is being input into the individual department sheets, we wanted to put the notification trigger on the "Master Sheet," but we are having a heck of a time getting the email notification to send. Basically we want it so if any cell in "Column F" contains a certain list of email addresses it will send an email to a third party notifying them to actually go ahead and make the changes.
Here is what we have so far. Keep in mind this is the code that worked originally. I've tried many different variations of things and have had no luck whatsoever, but I'm not the most educated coder:
function sendNotification() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Form Responses 3");
//Get Active cell
var mycell = ss.getActiveSelection();
var cellcol = mycell.getColumn();
var cellrow = mycell.getRow();
var cellValue = mycell.getValue();
var activeUser = Session.getActiveUser();
var recipients = "xxxx#xxxxxxxxxx.com";
var subject = "Update to "+ss.getName();
var body = activeUser + " has marked row " + cellrow + " \"" + cellValue + "\" in \"" + ss.getName() + "\". Visit " + ss.getUrl() + " to view the changes.";
if (cellcol == 2) {
if (cellValue.indexOf('test1#test.com') >= 0) {
var subject = "Lunch Is Served";
Logger.log('Sending approval notification of row ' + cellrow + ' to ' + recipients);
MailApp.sendEmail(recipients, subject, body);
}
}
}
Please keep in mind that we can't use lastRowNumber (at least I didn't think we could) because we already have over one thousand rows listed so the information will fill in to the array automatically.Lastly, our current trigger is set to "On Form Submission" because we want these emails to come in as the forms are submitted.
I have included a sample spreadsheet for you guys to look at. Please use test1#test.com as your email address when completing the form.
The Google Sheet can be found at the following site:
Test Sheet!
Thank you so much and I look forward to reading your responses!
You can't use the line:
var mycell = ss.getActiveSelection();
If that function is running from an "On Form Submit" trigger, there is no active selection. Although, there is a property available to the "On Form Submit" event object that gives the currently edited range. You must get the event object from the form submission. Then you have 3 options. 1) Just get the values 2) Get an object of questions and their values 3) Get the range of the range edited. First, you get the event object that is passed into the function. When the form is submitted, data is automatically made available to the function associated with the On Form Submit trigger. The letter e is typically used as the variable name to get the event object:
function sendNotification(e) {
But you can use any variable name:
function sendNotification(objOfData) {
Apps Script Documentation - Spreadsheet - On Form Submit
function sendNotification(e) {
var cellValue = e.values[4];//Get the value in column 5

Why do e.values[] on formSubmitReply in Google Apps script has NaN?

I am trying to make a simple tool which stores ticket number and some details that are handovered to other team members. In that process i used a Form to submit a handover and store in Google Spreasheet. But i also want to notify the team members, so i wrote a script as below.
function formSubmitReply(e)
{
var timeStamp = e.values[0];
var sendTo = e.values[1];
var ticket = e.values[2];
var description = e.values[3];
MailApp.sendEmail(sendTo,
"New Handover",
+ ticket +" handovered to you at "+ timeStamp +"\n\nDescription:"+ description +
"\n\nKindly check it using the below link\n\nhttp://goo.gl/p16qdN",
{name:"Handover Mail"});
}
In the above code e.values[2] has the ticket number through form and in forms i have used a textbox with a regex validation [c.i][r,n][q,c] which means the first three words should be C/I,R/N/Q/C. for eg CRQ, IRC, INC..etc. Everything was working alright but suddenly one day NaN starting coming in the notification mail(below output). Details in Spreadsheet is fine.
NaN handovered to you at 12/4/2014 12:48:23
Description:Device Check after 4 hours
Kindly check it using the below link
http://goo.gl/p16qdN
The above is my primary issue that i want to solve. But also as a add on i need some advice or guide on how to set some value on a cell in spreadsheet which has a fixed column and variable row when a handover is submitted. I am referring http://goo.gl/wkD0Y7 for my work. But in that sheet.getRange(lastRow, getColIndexByName("Status")).setValue("New"); does not seem to work throwing some reference error.
Your error may have to do with the preceding "+" sign in the email body string and the GAS compiler interpreting the string as a number. Try this code instead:
function formSubmitReply(e){
var timeStamp = e.values[0];
var sendTo = e.values[1];
var ticket = e.values[2];
var description = e.values[3];
MailApp.sendEmail(
sendTo,
"New Handover",
ticket + " handovered to you at " +
timeStamp + "\n\nDescription:" +
description + "\n\nKindly check it using the below link\n\nhttp://goo.gl/p16qdN",
{name:"Handover Mail"}
);
}
As for your other question, there's no built-in getColIndexByName() method, so you'll either need to copy over the function from the tutorial you referenced or just set it manually like this:
var statusCol = 1;
sheet.getRange(sheet.getLastRow() + 1, statusCol).setValue("New");

Getting active spreadsheet row after form submission

I am relatively new to Google Apps Script and have been using a very simple script that I created about a year ago that is triggered when a user submits inputs via a Google Form. The script has been working perfectly until approximately this past May and I've been scrambling trying to figure out what happened when I haven't made any changes to my code. I have searched many different places and cannot seem to find what is wrong.
Basically, I have a form that users complete and then submit. Upon submission, the script takes the inputs from the most recent row and stores them in variables that would then be assembled into an email message confirmation that acknowledges each user's submitted input.
Here is my code:
function acknowledgement() {
var ActiveSheet = SpreadsheetApp.getActiveSheet();
var ActiveRow = ActiveSheet.getActiveRange().getRow();
var emailAddy = ActiveSheet.getRange("E"+ActiveRow).getValue();
var locvar = ActiveSheet.getRange("C"+ActiveRow).getValue();
var employeevar = ActiveSheet.getRange("B"+ActiveRow).getValue();
var sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Variables");
var contactvar = sh.getRange("A2").getValue();
var subject = sh.getRange("B2").getValue();
var contactvar2 = sh.getRange("C2").getValue();
var linebrk = "<br />";
var msg = "Dear " + employeevar + ","
+ linebrk + linebrk
+ "This confirms that you have completed your review of the latest security presentation."
+ linebrk + linebrk
+ "Your location number is " + locvar + "."
+ "Thank you very much for your participation."
+ linebrk + linebrk
+ contactvar;
var msghtml = '<p>'+msg+'</p>';
var advancedargs = {cc:contactvar2, htmlBody:msghtml};
MailApp.sendEmail(emailAddy, subject, msg, advancedargs);
}
What is currently happening is that my code is no longer grabbing the current row number (i.e. the active row that was just submitted by a user). Instead, it is simply grabbing the top row of the sheet (i.e. my row headings like 'Employee Name', 'Email Address', etc.) and assigning those row headings to the variables thus producing an error when trying to send the email confirmation. For instance my variable emailAddy would contain "Email Address" causing sendEmail to fail.
Any feedback would be appreciated!
Using getActiveRow in the context of a form submission is somewhat strange as one cannot consider that a user is actually active on the sheet... I don't know why you did choose that approach and I'm actually wondering how it happened to work for so long...
There are other possibilities to handle form submissions but the one that will need the fewest changes in your code is to simply use getLastRow() instead of getActiveRange().getRow()
There are a few risks to use that simple "strategy" as there might be concurrency issues when 2 or more people send a form simultaneously.
The other solution is to get all the field values directly from the event properties that comes on form submission as in that case each event is unique, no matter how it comes into the spreadsheet but your script will have to be rewritten more deeply.
I believe that Google Forms has well covered the case you mention, the trigger "onFormSubmit" the spreadsheet, receives an object as a parameter with all the information you need.
I agree with Serge that the script be rewritten deeply, but definitely, it will save many problems.
Go to the documentation, specifically in "Spreadsheet Form Submit Events" https://developers.google.com/apps-script/understanding_events.
You are looking for the code e.range.getRow().
So we have:
function onFormSubmit(e) {
const row = e.range.getRow()
}
You'll also need to setup a trigger to the onFormSubmit function.
Edit > Current project's triggers
Now (finally) some fun: everytime a form is submitted, you'll know the row it corresponds to. Have fun!
Note that the onFormSubmit function could have any name - the trigger will map to any named function and pass to it an "e" argument that contains range.getRow() among other information.