Automatically create new row when checkbox is ticked in Google Sheets - google-sheets

I have a dataset of jobs for a gardening company.
status # ID Date Name Frequency
☐ 4 340 09/06/20 Jack Once Off
☐ 1 543 22/05/20 Sarah Weekly
☐ 3 121 01/05/20 Emily Fortnightly
☐ 3 577 11/06/20 Peter Once Off
When a booking is complete I want to check the box in the first column which will then create a new row with the following conditions
row is created only for jobs that are "weekly" or "fortnighly". Once off jobs wont create a row when the box is checked
The date in the new row is a week or a fortnight in the future of the original row depending on its frequency value
The job # goes up by one from the original row #
The ID, name and frequency rows remain the same
For example if I ticked the checkbox for the 3rd row it would create a new row like this:
☐ 4 121 15/05/20 Emily Fortnightly
Is this possible? Thanks!

Your goal is possible by using a custom script. You can try creating a bound script in your spreadsheet file and copy/paste this sample script below:
[updated]
SCRIPT:
function onEdit() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getActiveSheet();
var checkbox = sheet.getActiveRange().getValue();
var selectedRow = sheet.getActiveRange().getRow();
var selectedFreq = sheet.getRange(selectedRow, 6).getValue();
//Run if selected cell has a checkbox and ticked
while(checkbox == true){
addValues(selectedRow, sheet, selectedFreq);
break;
}
}
//Function to process values based on frequency
function addValues(selectedRow, sheet, selectedFreq){
var number = sheet.getRange(selectedRow,2).getValue();
var date = new Date(sheet.getRange(selectedRow,4).getValue());
if(selectedFreq == "Fortnightly"){
//Insert a new row after the ticked checkbox, setup a new date with 14 days (or 2 weeks), increments the # with 1 & the rest of the data are copied
var newDate = new Date(date.setDate(date.getDate()+14));
sheet.appendRow(["",number+1,sheet.getRange(selectedRow,3).getValue(),newDate,sheet.getRange(selectedRow,5).getValue(),sheet.getRange(selectedRow,6).getValue()]);
sheet.getRange("G"+selectedRow+":S"+selectedRow).copyTo(sheet.getRange("G"+sheet.getLastRow()+":S"+selectedRow)); //If you want to copy the rest of the columns, e.g. from range G:S
sheet.getRange(sheet.getLastRow(),1).insertCheckboxes();
sheet.getRange(sheet.getLastRow(),4).setValue(sheet.getRange(sheet.getLastRow(),4).getValue()).setNumberFormat("dd/MM/yy");
}
if(selectedFreq == "Weekly"){
//Insert a new row after the ticked checkbox, setup a new date with 7 days (1 week), increments the # with 1 & the rest of the data are copied
var newDate = new Date(date.setDate(date.getDate()+7));
sheet.appendRow(["",number+1,sheet.getRange(selectedRow,3).getValue(),newDate,sheet.getRange(selectedRow,5).getValue(),sheet.getRange(selectedRow,6).getValue()]);
sheet.getRange("G"+selectedRow+":S"+selectedRow).copyTo(sheet.getRange("G"+sheet.getLastRow()+":S"+selectedRow)); //If you want to copy the rest of the columns, e.g. from range G:S
sheet.getRange(sheet.getLastRow(),1).insertCheckboxes();
sheet.getRange(sheet.getLastRow(),4).setValue(sheet.getRange(sheet.getLastRow(),4).getValue()).setNumberFormat("dd/MM/yy");
}
}
[updated]
SAMPLE RESULT:
Sample sheet with data
Once script has been saved, ticked some check box on the sheet and new rows were automatically added at the bottom if frequency contains either "Fortnightly" or "Weekly":

Related

Moving cursor to cell below in google sheets automatic when datainput amount = 1

Im making a registration kind of google sheets project, where the amount of input is quite high, however the input in each cell only needs to be 1 number. It would therefore be usefull to have a script, which made the cursor jump to the cell below after input of 1 number.
efunction onEdit(e) {
var sheet = e.source.getActiveSheet();
var activeCell = sheet.getActiveCell();
var col = activeCell.getColumn();
var row = activeCell.getRow();
var value = activeCell.getValue();
if (value.length == 1) {
sheet.getRange(row + 1, col).activate();
}
}
However this only makes the cursor jump to the cell below the one which the input has been made, when another action is made, like clicking on 3rd cell. therefore not making the registrations easier.
Hope you can help.
The Google Sheets onEdit(e) trigger only runs when the user completes their data entry in a cell, usually by pressing Enter or Tab. You cannot watch individual characters as they are being entered.
What you can do, however, is let the user enter longer strings of digits in the cell, and when Enter is finally pressed, put each one of those digits in a cell of its own. You can then move the selection after the last digit that was thus filled down.
To do that, use String.split(), like this:
/**
* Simple trigger that runs each time the user hand edits the spreadsheet.
*
* Watches column Sheet1!A2:A as it is edited and splits digit strings such
* as 321654 or 321 654 into single digits 3, 2, 1, 6, 5, 4, and puts each
* digit in a cell of its own, stacking them vertically, starting at the
* cell where the digit string was entered.
* Moves the selection to the cell after the cell where last digit was put.
* Will happily overwrite values in cells under the cell that was edited.
*
* #param {Object} e The onEdit() event object.
*/
function onEdit(e) {
// version 1.0, written by --Hyde, 2 January 2023
// - see https://stackoverflow.com/a/74986570/13045193
if (!e) throw new Error('Please do not run the onEdit(e) function in the script editor window.');
if (!e.value
|| e.value.match(/[^\d\s]/i)
|| e.range.columnStart !== 1 // column A
|| e.range.rowStart < 2
|| !e.range.getSheet().getName().match(/^(Sheet1)$/i)) {
return;
}
const numbers = e.value
.replace(/\s/g, '')
.split('')
.map(n => [n]);
e.range
.offset(0, 0, numbers.length, 1)
.setValues(numbers)
.offset(numbers.length, 0, 1, 1)
.activate();
}
I am afraid that it is not possible to recognize what the users are typing until the edit is completely made. What you can do instead is to recognize if the value they have introduced is longer than 1 character and edit the cell by taking only the first character of what they introduce.
You can try the following script:
function onEdit(e) {
var ss = e.source;
var val = e.value;
var r = e.range;
if(val.length>1)
{
var x = val.substring(0,1);
}
r.setValue(x);
}
References:
Simple Triggers
Event Objects
setValue()

Google Spreadsheet dynamic conditional formatting with merged dropdown

How my sheet works
I'm making a spreadsheet to show how much parts I have. By using a dropdown, am I able to show that I created a product. With conditional formatting I am showing that having 0 items isn't an issue when the product is created. Created products with 0 items change from red to purple. Purple means it doesn't matter to have 0 items from this product.
My issue
My issue starts with my dropdown. If I merge cells, The value will go into the upperleft cell. This means other cells inside the merged cell are blank. This gives me a problem with conditional formatting.
My conditional formatting code example:
=if($D2=0;$E2="Created")
I have to change this code for every cell because of the condition combined with a dropdown. Having more than 250 rows would be inhumanly hard to do by hand.
My questions
Are there ways to give all cells of a merged cell the value of the combined cell in an efficient way?
Is there a better way to make my conditional formatting code applyable to merged cells?
This is my sheet
Product items collected sheet link (Shows the problem and solution!)
Product items collected sheet image (Version 1)
Product items collected sheet image (Version 2)
At the heart of this question is the operation of merged cells. When a cell is merged, say over several rows, only the cell at the top left of the merged cell can contain data, respond to conditional formatting, and so on. In a manner of speaking the other cells cease to exist and values CANNOT be assign to them.
The questioner asks:
Q: Are there ways to give all cells of a merged cell the value of the combined cell in an efficient way?
A: No. Not just in an "efficient" way; it's just not possible.
Q: Is there a better way to make my conditional formatting code applicable to merged cells?
A: No and yes ;)
No. In so far as a merged cell is concerned, everything is driven by the value in the top cell of the merged range. There are no other options for the "rest" of the merged cell.
Yes. I'd create a "helper" cells in Column F as in this screenshot
The code to achieve this is dynamic - it will automatically adapt to adding more products, more items, etc.
The logic is fairly simple: Start in F2, test whether E2 has a value (that is, is it the top of the merged cell?). If yes, then assign the value of E2 to F2 AND put that value in a variable for the following cells. If no, the cell in Column E must be part of a merged cell, so assign the value for Column F to the variable that was saved earlier.
function so5270705902() {
// basic declarations
var ss = SpreadsheetApp.getActiveSpreadsheet();
// note this is going to work on the second sheet in the spreadsheet - this can be edited.
var sheet = ss.getSheets()[1];
// Column B contains no merged cells, and always contains data (it is the BOM for the Products).
// so we'll use it to established the last row of data.
var Bvals = sheet.getRange("B1:B").getValues();
var Blast = Bvals.filter(String).length;
// Row 1 is a header row, so data commences in Row 2 - this can be edited
var dataStart = 2;
// Logger.log("the last row in column D = "+Blast);// DEBUG
// set up to loop through the rows of Column F
var mergedcellvalue = "";
for (i = dataStart; i < (Blast + 1); i++) {
// set the range for the row
var range = sheet.getRange(i, 6);
//Logger.log("row#"+i+" = "+range.getA1Notation()); DEBUG
// get the value in column E
var ECell = range.offset(0, -1);
var ECellVal = ECell.getValue();
//Logger.log("offsetrange#"+i+" range value = "+ECellVal);
//Logger.log("Column E, row#"+i+", value = "+ECell.getA1Notation()+" range value = "+ECellVal);//DEBUG
// when a row is merged, on the top row contains any data
// so we'll evaluate to see whether there is any value in this row in Column E
if (ECell.isBlank()) {
//Logger.log("ECell is blank. We're in the middle of the Merged Cell"); ??DEBUG
// Set the value to the lastes value of "mergedcellvalue"
range.setValue(mergedcellvalue);
} else {
//Logger.log("ECell has a value. We're at the top of the merged cell");//DEBUG
// paste the ECellVal into this range
range.setValue(ECellVal);
// Update the "mergedcellvalue" variable so that it can be applied against lower cells of this merged cell
mergedcellvalue = ECellVal;
} // end of the if isblank
} // end of the loop through column F
}
UPDATE 22 October 2018
For development purposes, I used a small range of only 14 rows in Column E. However the questioner's data covers over 250 rows, so I expanded development testing to cover 336 rows (yeah, I know, but I was copy/pasting and I ended up with 336 and was too lazy to delete any rows. OK?). I found that the code took over 81 seconds to process. Not good.
The primary reason (about 80 seconds worth) for the long processing time is that there is a getValue statement within the loop - var ECellVal = ECell.getValue();. This costs about 0.2 seconds per instance. Including getValue in a loop is a classic performance mistake. My bad. So I modified the code to get the values of Column E BEFORE the loop
var Evals = sheet.getRange("e2:E").getValues();.
I was surprised when the execution time stayed around the same mark. The reason was that the isBlank evaluation - if (ECell.isBlank()) { which previously took no time at all, was now consuming #0.2 second per instance. Not good++. So after searching Stack Overflow, I modified this line as follows:
if (!Evals[(i-dataStart)][0]) {.
Including setValues in a loop is also asking for trouble. An option would have been to write the values to an array and then, after the loop, update the Column E values with the array. However in this case, the execution time doesn't seem to have suffered and I'm leaving the setValues inside the loop.
With these two changes, total execution time is now 1.158 seconds. That's a percentage reduction of , um, a LOT.
function so5270705903() {
// basic declarations
var ss = SpreadsheetApp.getActiveSpreadsheet();
// note this is going to work on the second sheet in the spreadsheet - this can be edited.
var sheet = ss.getSheets()[2];
// Column B contains no merged cells, and always contains data (it is the BOM for the Products).
// so we'll use it to established the last row of data.
var Bvals = sheet.getRange("B1:B").getValues();
var Blast = Bvals.filter(String).length;
// Row 1 is a header row, so data commences in Row 2 - this can be edited
var dataStart = 2;
// Logger.log("the last row in column D = "+Blast);// DEBUG
// set up to loop through the rows of Column F
var mergedcellvalue = "";
// get the values for Column E BEFORE the loop
var Evals = sheet.getRange("e2:E").getValues();
for (i = dataStart; i < (Blast + 1); i++) {
// set the range for the row
var range = sheet.getRange(i, 6);
//Logger.log("row#"+i+" = "+range.getA1Notation()); DEBUG
// get the value in column E
var ECell = range.offset(0, -1);
var ECellVal = Evals[(i - dataStart)][0];
//Logger.log("Column E, row#"+i+", value = "+ECell.getA1Notation()+" range value = "+ECellVal);//DEBU
// when a row is merged, on the top row contains any data
// so we'll evaluate to see whether there is any value in this row in Column E
// instead is isblank, which was talking 0.2 seconds to evaluate, this if is more simple
if (!Evals[(i - dataStart)][0]) {
//Logger.log("ECell is blank. We're in the middle of the Merged Cell"); //DEBUG
// Set the value to the lastes value of "mergedcellvalue"
range.setValue(mergedcellvalue);
} else {
//Logger.log("ECell has a value. We're at the top of the merged cell");//DEBUG
// paste the ECellVal into this range
range.setValue(ECellVal);
// Update the "mergedcellvalue" variable so that it can be applied against lower cells of this merged cell
mergedcellvalue = ECellVal;
} // end of the if isblank
} // end of the loop through column F
}
UPDATE 3 March 2019
The questioner made his final changes to the code. This code is the final solution.
function reloadCreatedCells() {
// Basic declarations.
var ss = SpreadsheetApp.getActiveSpreadsheet();
// Note this is going to work on the second sheet in the spreadsheet - this can be edited.
var sheet = ss.getSheets()[1];
// Column B contains no merged cells, and always contains data (it is the BOM for the Products).
// so we'll use it to established the last row of data.
var D_vals = sheet.getRange("D1:D").getValues();
var D_last = D_vals.filter(String).length;
// First row with data.
var dataStart = 2;
// Set up to loop through the rows of Column H - K.
var mergedcellvalue = "";
// Get the values for Column H - K BEFORE the loop.
var H_K_vals = sheet.getRange("H2:K").getValues();
// How many people we have.
var people = 4;
// The first vertical row.
var rowStart = 12;
// Horizontal rows.
for (var h = 0; h < people; h++) {
// Vertical rows.
for (var v = dataStart; v < D_last; v++) {
// Set the range for the row.
var range = sheet.getRange(v, rowStart + h);
// Logger.log(range.getA1Notation()); //DEBUG
// Get the value in column H - K.
var H_K_Cell = range.offset(0, -people);
// Adding Created and not created values inside L - O.
var H_K_CellVal = H_K_vals[(v - dataStart)][h];
// Logger.log(H_K_Cell.getA1Notation() + ': ' + H_K_CellVal); //DEBUG
// When a row is merged, the value is only inside the top row.
// Therefore, you need to check if the value is empty or not.
// If the value is empty. Place the top value of the merged cell inside the empty cell.
if (!H_K_vals[(v - dataStart)][h]) {
// Logger.log(H_K_Cell.getA1Notation() + ": is blank. We're below the top cell of the merged cell."); //DEBUG
// Set the value to the top cell of the merged cell with "mergedcellvalue".
range.setValue(mergedcellvalue);
} else {
// Logger.log(H_K_Cell.getA1Notation() + ": has a value. We're at the top of the merged cell."); //DEBUG
// Paste the H_K_CellVal into this range.
range.setValue(H_K_CellVal);
// Update the "mergedcellvalue" variable, so that it can be applied against lower cells of this merged cell.
mergedcellvalue = H_K_CellVal;
} // end of the if isblank.
} // End of the vertical row loop.
} // End of the horizontal row loop.
}

Google Spreadsheet move row to new sheet based on cell value

I am new at this so still trying to figure how everything works.
I have a sheet that collects responses from a Google Form. Based on the answer to one of those questions I would like the row from that sheet to move to a different sheet document all together (not a sheet in the same document).
I have it set on a time based trigger every minute so as new responses come in it would kick them over to the correct document and then delete the row from the original spreadsheet.
I have been able to get part of the way there. I would like for it to take the row, columns A through E, move those to the correct document, find where the next open row is and place the data in columns A through E on the new document.
Where my issue is coming in at the moment is when the row is moved to the new document. I have formulas saved in columns G - Z on the destination page. It is finding the last row with a formula and placing the row after that (which is at the very bottom of the page). I am pretty sure this has to do with using an array? But I may be wrong. Is there a way to just have that look at the destination page column A-E, find the next blank row, and copy A-E from the original file to the new page?
arr = [],
values = sheetOrg.getDataRange().getValues(),
i = values.length;
while (--i) {
if (value1ToWatch.indexOf(values[i][1]) > -1) {
arr.unshift(values[i])
sheetOrg.deleteRow(i + 1)
sheet1in.getRange(sheet1in.getLastRow()+1, 1, arr.length, arr[0].length).setValues(arr);
};
I have multiple If statements each with some changes to the "valueToWatch" and the "Sheet1in" for different values and destination pages. If that information helps at all.
You can find the last cell in a column with data in it like this:
function findLastValueInColumn() {
var column = "A"; // change to whatever column you want to check
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
var lastDataRow = sheet.getDataRange().getLastRow();
for (var currentRow = lastDataRow; currentRow > 0; currentRow--) {
var cellName = column + currentRow;
var cellval = sheet.getRange( cellName ).getValue();
if (cellval != "") {
Logger.log("Last value in Column " + column + " is in cell " + currentRow);
break;
}
}
}
You can then use this function to figure out where to start your new data.

Google spreadsheet script for insert new row based on value of cell

Spreadsheet data looks like this
function myFunction()
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName('Active Listeners');
sh.insertRowBefore(15551)
}
As i have large range of rows that could be work on. If the value of the range matches with "Apr 9" then insert row before to that. Could anyone help me to get that.
A 'for loop' to cycle through your rows from the bottom would almost do the trick. The loop inserts a row after each row specified by i. Keep in mind you'll need a different solution if your Apr 9 column is formatted as a date. This works for plain text only. You can select the column and change to plain text with "Format > Number > Plain Text" on the menu.
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName('Active Listeners');
//var shlen = sh.getDataRange().getLastRow(); //returns integer last row
var shlen = Browser.inputBox("Enter Last Row of Preferred Range", Browser.Buttons.OK_CANCEL);
var ecell = sh.getActiveCell().getA1Notation();
You may need a different dataRange (below), I've just grabbed the parameters of data in your whole sheet (above), then grabbed a range specified in A1 notation of "A1:B[number reference of bottom row]" The modification may be that you need "B1:C" + [shlen] or whichever other range.
if (shlen >= 1) {
var dataRange = sh.getRange("A1:A" + shlen).getValues();
for (var i = shlen; i > 0; i--) {
var row = dataRange[i-1];
if (row[0] == "Apr 9") {
sh.insertRowAfter(i-1)
}
}}
}
Someone more knowledgeable than me can pitch in if they have a better answer, but my only solution (which should be ok if it's a one-of) would be to just repeat the script a few times, starting at the row of your choice each time. Select cell A1 and then press "control (or command) + down arrow". It will take you to the first gap, which should be where the previous script ended. Remember the row number you're up to and plug that in the input box when you run the script again. Might take a few iterations but you'll get there.
If this process is going to be done repeatedly then best of luck in finding a solution :)

Increment a Google Sheet cell by 1 via App Scripts

I'm trying to increment a number in a Google sheet cell by one, when I custom menu item is clicked. But the result in the spreadsheet is either #NUM! or 1range. I've tried a number of different methods, as shown in the comments in my code.
Note that the number is stored as a custom number format so that it has 4 leading zeros, eg: 00001
function onOpen() {
var ui = SpreadsheetApp.getUi();
// Or DocumentApp or FormApp.
ui.createMenu('OrderNumber')
.addItem('Generate next order number', 'menuItem1')
.addToUi();
}
function menuItem1() {
SpreadsheetApp.getUi() // Or DocumentApp or FormApp.
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var range = sheet.getRange("N11");
range = 1 + +range;
// For the previous line, I've also tried: range = 1 + parseInt(range)
// For the previous line, I've also tried: range = 1 + Number(range)
SpreadsheetApp.getActiveSheet().getRange('N11').setValue(range);
}
As a test if it just set it to a number it shows in the spreadsheet correctly, e.g:
range = 5
SpreadsheetApp.getActiveSheet().getRange('N11').setValue(range);
Any help on this would be greatly appreciated. Thanks.
A Range is an object containing a lot of information: values but also formatting, data validation, font size etc.
You need to call getValue() on range first to extract the number:
var range = sheet.getRange("N11").getValue();

Resources