Shared Spreadsheet with two instances of onEdit, deleting wrong row - google-apps-script

I'm trying to create a lead management format using Google Sheets & Apps Script.
The apps script is checking whether the value in column M of sheet Propsect or Interested has changed and depending on the value, moving the row to the respective sheet (Interested, Postponed, Lost, or Booked)
The spreadsheet is shared with my team who'll make changes and with multiple users editing at a time.
Now, the problem is that, as soon as two onEdits are triggered, and if both require rows to be moved, the first instance runs properly but the second one removes the wrong row.
Eg: In sheet Prospect, Row 2 & Row 3 have status changed to Lost & Postponed at the same time. Now, Lost gets triggered properly, however, the Postponed instance deletes the 4th row (now the 3rd row, as row 2 was removed before).
I have tried to add in lockservice to the code so that only one instance is running but that doesn't seem to solve the problem as the event object is still considering the un-updated row number.
Even tried adding flush() at the start & end of the code but didn't work either.
You can access the spreadsheet here.
My code is as follows:
function Master(e) {
var lock = LockService.getScriptLock();
var SS = e.source;
var Sheet = e.source.getActiveSheet();
var Range = e.range;
if(Sheet.getName() == "Prospect" && Range.getColumn() == "13" || Sheet.getName() == "Interested" && Range.getColumn() == "13"){
moveRows(SS,Sheet,Range);
}
lock.releaseLock();
}
function moveRows(SS,Sheet,Range) {
var val1 = Sheet.getRange(Range.getRow(),1,1,10).getDisplayValues();
val1 = String(val1).split(",");
var tar_sheet = SpreadsheetApp.getActive().getSheetByName(Range.getValue());
var row = tar_sheet.getRange(tar_sheet.getLastRow()+1,1,1,val1.length).setValues([val1]);
Sheet.deleteRow(Range.getRow());
}
}
Is there any way for the second onEdit to run only after the first has completed execution? I guess, if that could happen, the problem would be solved?
I Hope I have been able to convey my question properly.

Issue:
Event object e passed to a onEdit(e) is not altered, when two or more edits are done at the same time and the first edit alters the next edit's row number- making e.range.rowStart of the second+ edit unreliable at the time of it's execution.
Possible Solutions:
Do not delete the rows immediately. Mark them for deletion(save the range string in properties service) and delete them later(time trigger), when document is not in use.
Alternatively, Add code guards: Check range.getValue()===e.value. If they're equal, continue to moveRows else keep offseting the range by -1 row until they're both equal.
References:
PropertiesService
Range#offset

I guess you should trigger only one function based on user interaction and then inside that perform conditional operations.
Something like this:
function onEdit(event_object) {
var sheet = event_object.range.getSheet();
var row = event_object.range.getRow();
var column = event_object.range.getColumn();
if (sheet.getName() == "Sheet1") {
// perform operations when Sheet1 is edited
} else if (sheet.getName() == "Sheet2") {
// perform operations when Sheet2 is edited
}
}
Reference :
https://developers.google.com/apps-script/reference/spreadsheet/range
https://developers.google.com/apps-script/guides/triggers/events#edit

Related

How to make a trigger that run when there is changes on specific column of different Spreadsheet

I want to ask about how to make a trigger that run when there are changes on different spreadsheet (Source spreadsheet) and on specific column.
I have 2 different spreadsheet (Source Spreadsheet and Target spreadsheet) and the trigger will working on target spreadsheet when there are changes that happened in the specific column of the source spreadsheet. I've just made the code, but it's not working. This code was made on the target spreadsheet. I've already use the onEdit() and onChange() trigger, but nothing happen or run this script.
This code still show some error like:
TypeError: e.source.openById is not a function
Here is the code that I've been made on the target spreadsheet:
function inChange(e) {
var source_spreadsheet_sheetName = e.source.openById('').getName(); //ID of source spreadsheet
var Row = e.source.getActiveCell().getRow();
var Column = e.source.getActiveCell().getColumn();
if (source_spreadsheet_sheetName == '' && Row >= 2 && Column == 2) { //the name of sheet from source spreadsheet and the specific column that has changes on it.
myFunction(); //It will run the process of this myFunction script that I've been made on target spreadsheet
}
}
Your trigger runs. But it fails on first line of code because you're calling a method that you cannot.
Several problems.
Problem #1
var source_spreadsheet_sheetName = e.source.openById('').getName();
the openById probably does not work because source is already of type Spreadsheet so you can directly call getName on it.
It should be
var source_spreadsheet_sheetName = e.source.getName();
Problem #2
There is no getActiveCell on a Spreadsheet, only on a Sheet.
// if you know the sheet name in advance
let sheet = e.source.getSheetByName(YOUR_SHEET_NAME)
// or if you don't, get the active sheet (and don't use the line before)
let sheet = e.source.getActiveSheet()
let row = sheet.getActiveCell().getRow();
let column = sheet.getActiveCell().getColumn();
Problem #3
if (source_spreadsheet_sheetName == ''
A sheet name cannot be empty so this will never be true
source_spreadsheet == '' cannot be true because it's the name of a spreadsheet

How to set timer for a row in google sheets

I would like to set a timer for an entire row in google sheets where a user can start entering data in the second row only after a certain time after starting row one.
Example: If a user starts filling cells in row 1 then they should be able to fill the data in the second only after the timer ends.
Could anyone suggest me how to get started or suggest me a chrome extension for this use?
You could also suggest me on how to build the chrome extension I can try it along with my colleagues.
This function uses an onEdit trigger to impose a 20 second delay between editing rows. It may not be exactly what you want but perhaps it's a start. It uses PropertiesService to keep state. I think user properties would be a better choice but script properties are easier to develop with since you can modify them directly in the script editor.
function onEdit(e) {
const sh=e.range.getSheet();
const delay=20000;
let ms=Number(new Date().valueOf()).toFixed();
if(sh.getName()=='Sheet10') {
const ps=PropertiesService.getScriptProperties();
let dObj=ps.getProperties();
if(dObj.hasOwnProperty('row') && dObj.hasOwnProperty('delay')) {
if(dObj.row!=e.range.rowStart && Number(ms-dObj.delay)<delay) {
e.range.setValue(e.oldValue);
e.source.toast('Sorry you have ' + (delay-Number(ms-dObj.delay))/1000 + ' seconds left.');
}else{
ps.setProperties({'row':e.range.rowStart,'delay':ms});
}
}else{
ps.setProperties({'row':e.range.rowStart,'delay':ms});
}
}
}
Issue with Protections:
Class Protection is commonly used to protect ranges from being edited. It is not appropriate for your situation, though, because, as specified here, users who are executing the script cannot remove themselves from the list of editors:
Neither the owner of the spreadsheet nor the current user can be removed.
Using oldValue:
Because of this, the best way to go would be to use the parameter oldValue from the onEdit event object.
An onEdit trigger runs every time a user edits a cell. In it, you can use:
PropertiesService to store useful information: (1) whether it is the first time row 1 is edited (isNotFirstTime), and (2) when was last time first row was edited (startTime).
Event object to get information on the edited cell (its row, its old value, etc.).
You can do something along the following lines (check comments):
function onEdit(e) {
var current = new Date(); // Current date
var range = e.range;
var editedRow = range.getRow();
var sheet = range.getSheet();
var props = PropertiesService.getScriptProperties();
var waitingTime = 20 * 1000; // 20 seconds
var isNotFirstTime = props.getProperty("isNotFirstTime"); // Check if first row was previously edited
var startTime = new Date(props.getProperty("startTime")); // Time when first row was first edited
if (editedRow === 1 && !isNotFirstTime) { // Check that (1) edited row is first one, (2) it was not edited before
props.setProperty("startTime", current.toString()); // If it's first time first row was edited, store current time
Utilities.sleep(waitingTime); // Wait for 20 seconds
props.setProperty("isNotFirstTime", true); // Store: first row was previously edited
}
// Check that (1) second row edited, (2) Less than 20 seconds passed since first time first row was edited:
if (editedRow === 2 && (current - startTime) < waitingTime) {
range.setValue(e.oldValue || ""); // Set previous value to edited cell (this avoids editing cells)
}
}
Reference:
onEdit(e)
onEdit Event object
Class PropertiesService

Google Sheets Script - Move Rows from One Tab to Another

I'm trying to modify the code from the below forum post to help fit my needs:
https://productforums.google.com/forum/#!topic/docs/ehoCZjFPBao/discussion
function onEdit() {
// moves a row from a sheet to another when a magic value is entered in a column
// adjust the following variables to fit your needs
// see https://productforums.google.com/d/topic/docs/ehoCZjFPBao/discussion
var sheetNameToWatch = "Transfers";
var columnNumberToWatch = 15; // column A = 1, B = 2, etc.
var valueToWatch = "yes";
var sheetNameToMoveTheRowTo = "Archive";
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getActiveCell();
if (sheet.getName() == sheetNameToWatch && range.getColumn() == columnNumberToWatch && range.getValue() == valueToWatch) {
var targetSheet = ss.getSheetByName(sheetNameToMoveTheRowTo);
var targetRange = targetSheet.getRange(targetSheet.getLastRow() + 1, 1);
sheet.getRange(range.getRow(), 1, 1, sheet.getLastColumn()).moveTo(targetRange);
sheet.deleteRow(range.getRow());
}
}
What this code does is moves a row from one tab to another when a specific value is entered (in the code above, it moves a row from "Transfers" to "Archive" when the word "yes" is entered in column 15 (Column O).
My needs are a little bit different. I cannot have a user moving one row at a time because there may be multiple rows involved and combined together, they will need to meet certain criteria (e.g. do the amounts in all rows balance to 0, is the same account used, etc.). This I believe is a rudimentary data validation.
Therefore, I tried to make a formula so that if I entered the word yes in cell F8, it populates the word "Send" (used to be "yes", changed for clarity) to column O. If the word yes is entered in cell F8, then every row with the word "Send" should be moved to the Archive Column. The problem is, the above code relied on a function called OnEdit, and populating the rows with the word Send via formula does not trigger the script to run. The above code needs the user to manually enter the correct keyword in order to move the rows over.
Can someone help to modify or rewrite the code so that it looks for a user to manually type in the keyword "yes" in cell F8, and then have it move any row that had the word "Send" populated by a formula in Column O?
The below Google Sheet is a simplified example of what I'm trying to do. Additional comments can be found on the "MASTER - DO NOT EDIT" tab.
https://docs.google.com/spreadsheets/d/1iajS90qvwEOGVnl2lpDbVtcI532OO8n4NLZEBDUpVzA/edit#gid=398066315
Thanks for looking. If anyone needs additional info, please let me know.
~~~~~~~~~~~~~~~~~~~~~~~~~~~
EDIT: I tried out Cooper's code below, and it seems to work. I'm a novice at this, but I've managed to add a few things to make it run onEdit, and to delete the initial trigger word.
An example of this slightly tweaked code running can be found below:
function onEdit() {
archiveRows();
clearCells();
}
function archiveRows()
{
/*installable trigger rows 10-13 https://developers.google.com/apps-script/guides/triggers/installable
if(!projectTriggerExists()
{
ScriptApp.newTrigger('myFunction')
.forSpreadsheet(ss)
.onOpen()
.create();
}//by wrapping the trigger creation like this you don't have to worry about creating unwanted triggers.
*/
var ss=SpreadsheetApp.getActive();
var sh0=ss.getSheetByName('Transfers'); //sh0 = Transfers tab
var rg0=sh0.getDataRange(); //rg0 = Range of sh0, This is functionally equivalent to creating a Range bounded by A1 and (Range.getLastColumn(), Range.getLastRow()). https://developers.google.com/apps-script/reference/spreadsheet/sheet#getdatarange
var sh1=ss.getSheetByName('Archive'); //sh1 = Archive tab
var vals=rg0.getValues(); //Returns the rectangular grid of values for this range. Returns a two-dimensional array of values, indexed by row, then by column. The values may be of type Number, Boolean, Date, or String, depending on the value of the cell. Empty cells will be represented by an empty string in the array. Remember that while a range index starts at 1, 1, the JavaScript array will be indexed from [0][0]. https://developers.google.com/apps-script/reference/spreadsheet/range#getvalues
for(var i=vals.length-1;i>11;i--) //When deleting rows it is better to start from the bottom otherwise deleted rows will mess up your loop indexing
{
if(vals[i][14]=='SEND') //If column 14 in the range has the word 'SEND' then run the next lines of code. 14 is column O. Column 0 is 15, but see note above regarding JavaScript array being indexed from [0][0]
{
sh1.appendRow(vals[i]);
sh0.deleteRow(i+1);//indexes start at zero but rows start at one
}
}
}
//This could be accomplished with SpreadsheetApp.getActive().getActiveSheet().clear();
function clearCells()
{
//https://stackoverflow.com/questions/9268570/i-need-a-button-to-clear-cells-in-a-google-spreadsheet
var ss=SpreadsheetApp.getActive();
ss.getRange('A1:A1').clearContent()
}
function projectTriggerExists(functionName)
{
if(functionName)
{
var allTriggers=ScriptApp.getProjectTriggers();
var funcExists=false;
for(var i=0;i<allTriggers.length;i++)
{
var trigger=allTriggers[i];
if(allTriggers[i].getHandlerFunction()==functionName)
{
funcExists=true;
break;
}
}
}
return funcExists;
}
The nature of the spreadsheet is that I will need to duplicate the "Transfers" tab in the original post a dozen times so that it can be used by multiple people at once (data being entered in by the user essentially over 12 "forms"). All the data put into these dozen "Transfers" tabs should rout to the same "Archive" tab. However, I'd also like to build in a rudimentary routing system, so that instead of having everything rout to one tab, different code words send the lines to different "Archive" tabs. For instance, "OK+Send+Staff1" sends the line to a tab called Staff1Archive, "OK+Send+Staff2" sends to a tab called Staff2Archive, "OK+Send+Staff3" sends to a tab called Staff3Archive, etc.
Can anyone help with this code as well?
I think this will do it for you.
function archiveRows()
{
var ss=SpreadsheetApp.getActive();
var sh0=ss.getSheetByName('Transfers');
var rg0=sh0.getDataRange();
var sh1=ss.getSheetByName('Archive');
var vals=rg0.getValues();
for(var i=vals.length-1;i>11;i--)
{
if(vals[i][14]=='SEND')
{
sh1.appendRow(vals[i]);
sh0.deleteRow(i+1)
}
}
}

Google sheets script writing, how do I prevent an auto update or recacluation of my timestamps in googel sheets?

I attached a copy of the spreadsheet I run in google. The concept is when someone begins picking an order, they will for instance type their name in I3 and their time will start. once complete they will type their name in J3 , each causing a time stamp below leading to a total duration time. later it will factor percentages.
The problem is the time stamps seem to randomly update without prompting to do so. it seems to be when it is printed or reopened. This will cause inaccuracies in the times and percentages. Any assistance would be greatly appreciated
It would appear that I should write a script to accommodate this need, but I haven't the slightest on how to do this. I was directed to this forum from a reply in google docs help forum
enter link description here
I don't think you can choose on which cell you update or recalculate (Tell me if I'm wrong).
A non-optimized workaround (Maybe some expert have got better solution):
function onEdit(e){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var i = 3;
while(i <= sheet.getLastRow())
{
var data = sheet.getRange(i, 9, 3, 2).getValues();
if(data[1][0].length == 0)
{
if(data[0][0] != "")
{
var d = new Date();
sheet.getRange(i+1, 9).setValue(d);
}
}
if(data[1][1].length == 0)
{
if(data[0][1] != "")
{
var d = new Date()
sheet.getRange(i+1, 10).setValue(d);
}
}
i=i+3;
}
}
The onEdit(e) function is a simple trigger implement in Google Spreadsheet. It will trigger each time a cell is edit.
On this code above, we iterate to check on each element if a name was added or not. If so, the code will set the date and time below the name just added. Since you set the value on one single moment, no auto update will modify those time.
for this workaround, you need to remove the formula you put on the cell where you want your timestamp (i.e I4 and J4). You can keep the cells which calculate the difference between time, they should work with no problem.
Paste below script in the script editor and save it.
function onEdit(e) {
if (e.range.rowStart % 3 != 0 || [9, 10].indexOf(e.range.columnStart) == -1) return;
var o = e.range.offset(1, 0);
if (!o.getValue()) o.setValue(new Date())
}
Don't run the script by clicking the play button in the script editor. Instead to back to any of the tabs, clear all cells where you now have the =now() formula and enter a name. See if the timestamp appears.

deleteRow based off cell edit on another sheet of same spreadsheet

WHAT I HAVE One google spreadsheet named "Script Test" with two sheets named "delete" and "non delete".
WHAT I NEED If a row in Col B on "non delete" is changed to 'DELETE' via the drop down menu, the row with the same Buy Number on "delete" will be deleted.
WHAT I HAVE TRIED
What has worked = By researching on stack I found an onEdit function that deletes a row on "delete" based on if a cell has a specific value. In this case, that value is 'DELETE'. The problem with this is, I can only get it to work if that cell is on the sheet "delete" rather than the sheet "non delete". If I'm working off of "non delete" and need to go back to "delete" to delete a row of information, I can just right click on the row number and manually delete it. So, this script isn't necessarily saving me time.
This script is as follows:
function onEdit(e) {
try {
var ss = e.source;
var s = ss.getActiveSheet();
if (s.getName() == 'delete' &&
e.range.columnStart == 1 && e.range.columnEnd == 1 && // only look at edits happening in col A which is 1
e.range.rowStart == e.range.rowEnd ) { // only look at single row edits which will equal a single cell
checkCellValue(e);
}
} catch (error) { Logger.log(error); }
};
function checkCellValue(e) {
if (e.value == 'DELETE') {
e.source.getActiveSheet().deleteRow(e.range.rowStart);
}
}
What has not worked = I fiddled with the script a bit to have it read Col F on "delete" and in Col F I have an Index Match of Col B in "non delete". However, this does not delete the row on "delete" when Col F changes to 'DELETE'. Now I'm not 100% on this but I can pretty much deduce that this is happening because Col F isn't being "edited", rather the formula inside of it is "updating". I've also tried fiddling with other scripts that I found on stack but none seem to have gotten me as close as the script above.
THINGS TO THINK ABOUT
First of all, thanks for any help you guys can give me. Just before posting this question, I came across a filter function that I think may be a direction to head in if I'm right about the Index Match. I found one function that hides rows based on a filter but I would need the row to be deleted so I'm assuming that is as simple as switching hideRows with deleteRows. I've tried adding screenshots of what I need done but I don't have enough reputation. I can and will add a link to a copy of the spreadsheet if that helps. Once again, thanks for any tips or guidance.
Copy of Script Test
Use the getSheetByName() method to get the delete sheet.
function checkCellValue(argRowToDelete) {
if (e.value == 'DELETE') {
var toDeltSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("delete");
toDeltSheet.deleteRow(argRowToDelete);
}
}
If you want a separate function just for deleting the row, maybe do the check for the 'DELETE' text in the main function, and pass the row number to the delete function:
I've tested the following code, and it works. When the drop down list is used to select 'DELETE' in the 'non delete' sheet, it deletes the corresponding row in the 'delete' sheet.
I made multiple changes to the code. Even though this code deletes a row in a sheet different from where the edit is taking place, there is still a potential problem. Once a row in the 'delete' sheet is deleted, the rows will shift. If you start deleting rows at the top or middle, then every row below the deleted row is no longer synced with the rows in the 'delete' sheet.
So, this answers your question, but now you have yet another problem.
function onEdit(e) {
try {
var ss = e.source;
var s = ss.getActiveSheet();
var colStart = e.range.columnStart;
var colEnd = e.range.columnEnd;
Logger.log('colStart: ' + colStart);
Logger.log('colEnd: ' + colEnd);
var thisRow = e.range.getRow();
Logger.log('s: ' + s.getName());
//Avoid looking at multi column edits. If column start and column end is same column,
//then not a multi column edit
var editedFromNonDelete = (s.getName() === 'non delete');
Logger.log('editedFromNonDelete: ' + editedFromNonDelete);
var editedFromColB = (colEnd === 2) && (colStart === colEnd);
// only look at edits happening in col B
if (editedFromNonDelete && editedFromColB) {
Logger.log('e.value: ' + e.value);
if (e.value === 'DELETE') {
fncDeleteRow(thisRow);
};
}
} catch (error) {
Logger.log(error);
}
};
function fncDeleteRow(argRowToDelete) {
var toDeltSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("delete");
toDeltSheet.deleteRow(argRowToDelete);
};
After testing out the filter function for a couple minutes, I've pretty much got it to do what I needed. Thanks anyways!