I have developed a web app to manage the bonuses of a company.
It has two pages, the first one to fill a form and declare a "no-bonus" reason and the second one to consult who will be entitled to a bonus at the end of the month.
The main rule is that for two no-bonus reasons filled in for one person, the bonus will not be paid. Each employee's counter is reset to zero each month.
So I was able to make the application with the form without any problems. I was also able to upload the different "no-bonus" in the consultation page. However, I would like to add a table with the list of employees, the number of "no-bonus" of the month and if they are entitled to their bonus or not.
To do this, I made a specific tab in my Google Sheets with a column for "employee", one for the number of "no-bonus" (with a CountIfs formula) and one to know if the bonus is granted or not. I added the onEdit(e) function in my code to be able to display "No" if the number of "no-bonus" is greater than 1 and "Yes" in the opposite case.
The problem I have is that my onEdit(e) acts when I make a modification in a cell of the no-bonus column. It does not act when I fill in the form. And I don't see how to counter this.
Here is the code of my onEdit(e) function:
function onEdit(e) {
var ss = e.source;
var activeSheet = ss.getActiveSheet();
var cell = e.range;
if (activeSheet.getName() == "Sheet1" && cell.getColumn() == 2 && cell.getRow() > 1) {
var choice1 = cell.getValue();
if (choice1 > 1){
cell.offset(0,1).clearDataValidations()
cell.offset(0,1).setValue("No");
}
else{
cell.offset(0,1).clearDataValidations()
cell.offset(0,1).setValue("Yes");
}
}
}
Here is the link of the Sheets in question: link
For those who don't want to access the link, here is a screenshot of the tab where the fromulaire requests are saved :
And here is the table where the onEdit(e) function is used:
Sorry for my English, it is quite average. Thanks in advance for your help.
Related
NOTE: my question is NOT answered by Protecting/unprotecting range based on another cell value
My question asks about a range, with only part of the rows selected, NOT entire rows as in that question. I have extensively searched the site and there isn't anything that answers my question.
Please read my query properly prior to linking it to something else that does not answer it.
QUESTION
I have created a shared test Google Sheets workbook (TestWBook) to illustrate what I am trying to do. The 'Outgoing' sheet is where I need the restrictions to be applied; it is used to store order data.
The whole 'Outgoing' sheet (A1:AS), except range A2:M882, is currently protected so that only the owner and two other editors can make changes to it (let's call them editor1#email.com and editor2#email.com).
Other editors enter data in A2:M882 to specify new order requirements (which is why this range started by not being protected).
I now need to protect each row between columns A and M(active_row) from all editors but the owner and two other named editors (editor1#email.com and editor2#email.com) whenever a date is entered in column O (Date Sent) (or if that cell is not blank, either would work).
I don't want to protect an entire single row every time the script runs, as I don't want to end up with hundreds of protected ranges.
I want the protection to be applied dynamically to the range from A2 to M(active_row), so that effectively that one protected range is updated every time a date/value is entered in column O.
I have been looking through scripts for the last two days and cannot find a way to specify this dynamic range (sorry, I can adapt scripts but cannot write them up from scratch!)
Can anyone help?
Try
function onEdit(event) {
var sh = event.source.getActiveSheet();
var rng = event.source.getActiveRange();
if (sh.getName() != 'Outgoing') return;
if (rng.getColumn() != 15) return;
// save the protection parameters
var p1 = sh.getProtections(SpreadsheetApp.ProtectionType.SHEET)[0];
// delete actual protection
let protection = sh.getProtections(SpreadsheetApp.ProtectionType.SHEET)[0];
if (protection) { if (protection.canEdit()) { protection.remove(); } }
// rebuild protection
var p2 = sh.protect();
p2.setDescription(p1.getDescription());
p2.setWarningOnly(p1.isWarningOnly());
if (!p1.isWarningOnly()) {
p2.removeEditors(p2.getEditors());
p2.addEditors(p1.getEditors());
}
// define new uprotected area
p2.setUnprotectedRanges([sh.getRange('A' + rng.getRow() + ':M882')]);
}
My first issue is this, I have an items log sheet where I want to add and manage individual unique items in our inventory. I created a data validation dependent dropdown list for a main category and found out how to build a script to dynamically create a secondary category dropdown list based on the selected main category.
For Example:
If cell B2 (Main Category) is set to Carabiner (based on data validation range on another sheet) THEN cell C2 (secondary Category) will dynamically create a dropdown list relative to the Carabiner main category (i.e. locking, non-locking)
That is simple enough if you only have one row to create the dropdown lists, but I wanted to be able to pick from a secondary category list in each row dependent on which was picked in the main category cell.
I found a video of a script that did just that and got it working just fine.
Now the problem is that the script runs data validation on every other sheet. How can I limit the script to only run on a specific sheet?
Here is the script:
function onEdit() {
// this line just refers to the current file var start = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); var current = start.getActiveCell()
// var to refer to the worksheets -lists is where the data validation range will come from, and main is where we want to use that data validation range var list = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Indirect_Categ_Ranges") var main = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Items_Log");
// has the user selected a category? Refers to the column number in the Items_Log sheet where the user has picked the main category
if (current.getColumn()==2)
{
//to copy the selected sub-category -the 2,1 is the row and column on the Indirect_Categ_Ranges sheet where this script will dynamically update the main category picked to define what the indirect function will display in the next column
var choice = current.getValue()
list.getRange(2,1).setValue(choice)
//clear any validation -the 2,3,1000 looks to start clearing validation at row 2, column 3 and down for up to 1000 entries
main.getRange(2,3,5000).clearDataValidations()
// create the rule - var_point defines the offset number of rows and columns to shift to initiate the dynamic dependent dropdown list, the var_items defines where the to look for the range to build the dropdown list
var point = current.offset(0,1)
var items = list.getRange(2,2,50)
var rule = SpreadsheetApp.newDataValidation().requireValueInRange(items,true).build();
point.setDataValidation(rule)
}
}
Also, is there a way to have the clear validations to run no matter how many rows there are?
The function will run anytime there's an edit and there's nothing you can do to stop that. You can, instead, terminate execution preemptively if it's not the sheet you care about.
The event object tells you which range was edited. You can get that range's sheet to know which sheet was edited. If the name matches, then execute the other stuff.
function onEdit(e) {
if (e.range.getSheet().getName() === 'Items_Log') {
// Data validation
}
}
It's not great practice to use .getActiveRange() or .getActiveSheet() when you want what was actually edited because there is a chance, however small, that the edited range may differ from the active range at the time of function execution.
Explanation:
You need to take advantage of the event object.
That object contains relevant information to the edits you make.
For example:
e.source is equivalent to SpreadsheetApp.getActive()
e.range is equivalent to .getActiveCell().
To run the code only for a particular sheet, in this case Items_Log, add a condition to check if the name of the active sheet matches that name:
if (current.getColumn()==2 && start.getName()=="Items_Log")
where start is the active sheet:
var start = e.source.getActiveSheet();
Solution:
function onEdit(e) {
var start = e.source.getActiveSheet();
var current = e.range;
var list = e.source.getSheetByName("Indirect_Categ_Ranges")
var main = e.source.getSheetByName("Items_Log");
if (current.getColumn()==2 && start.getName()=="Items_Log")
{
var choice = current.getValue()
list.getRange(2,1).setValue(choice)
main.getRange(2,3,5000).clearDataValidations()
var point = current.offset(0,1)
var items = list.getRange(2,2,50)
var rule = SpreadsheetApp.newDataValidation().requireValueInRange(items,true).build();
point.setDataValidation(rule)
}
}
Thanks for the help. after looking at your suggestions and trying a couple things, I found that simply adding the code:
&& start.getName()=="Items_Log")
To the end of the line:
if (current....
Worked and solved the issue.
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
We are using Google sheets for tracking attendance. Previously, the teachers were entering P, T, or A (for present, tardy, absent) for each period. I would still like users to have the option to enter a value for each period in a week, however it would be a great time saver if they could enter one value for the whole day.
What I'd like is that if a value is entered into any one of the "0" periods (green columns) with a "P" or "A" (data validation limits those options) an OnEdit function would copy that same letter ("P" or "A") to the following 8 columns and then delete the original value. (without the deletion the totals on the far right columns will be off). I would not want the OnEdit to be activitated based on edits in any of the non-green columns.
I will eventually have several tabs, each one a different week, but each exactly the same... so I'm thinking the function should work within whatever the activesheet is.
https://docs.google.com/spreadsheets/d/1NKIdNY4k66r0zhJeFv8jYYoIwuTq0tCWlWin5GO_YtM/edit?usp=sharing
Thank you for your help,
I wrote some code to get you started with your project. (I am also a teacher) You will have to make some changes based on what you are going for and it can probably be optimised to run faster. Good luck!
function onEdit(e) {
//create an array of the columns that will be affected
var allColumns = [2, 10];
//get the number values of the column and row
var col = e.range.getColumn();
var row = e.range.getRow();
//get the A1 notation of the editted cell for clearing it out
var cell = e.range.getA1Notation();
//only run if the cell is in a column in the allColumns array
if(allColumns.indexOf(col) > -1) {
//run the for loop for the next 8 cells
for(var i = col + 1; i < col + 9; i++) {
SpreadsheetApp.getActiveSheet().getRange(row, i).setValue(e.value);
SpreadsheetApp.getActiveSheet().getRange(cell).setValue('');
}
}
}
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.