Copy edited values before they are edited - google-apps-script

I'm trying to setup a daily report that will send me an email with only the edited rows from a sheet, but also including the old values before they were edited.
I wrote the following script, where when the specified range is edited, the whole row is transferred to another tab called "new_values".
function onEdit_inRange(e,sheetName,sheetRange){
var sh=e.range.getSheet();
if(sh.getName()===sheetName){
var range = SpreadsheetApp.getActiveSheet().getRange(sheetRange);
var xy=[e.range.getColumn(),e.range.getRow()];
if (xy[0]>=range.getColumn() && xy[0]<=range.getLastColumn() && xy[1]>=range.getRow() && xy[1]<=range.getLastRow()) {
return e.range;
}
}
return false;
}
function onEdit(e){
var range=0;
if((range=onEdit_inRange(e,'sheet','M6:O'))){
// SpreadsheetApp.getUi().alert('You Edited a Cell INSIDE the Range!');
} else if ((range=onEdit_inRange(e,'sheet','Q6:Q'))) {
// SpreadsheetApp.getUi().alert('You Edited a Cell INSIDE the Range!');
}
var ss = e.range.getSheet();
//ss.getRange(e.range.getRow(),49) //set timestamp in column 49
//.setValue(new Date()); // Set time of edit in "G"
var appendSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('new_values');
var archiveLastRow = appendSheet.getLastRow();
Logger.log(archiveLastRow);
var archiveAppendRange = "new_values!" + (appendSheet.getLastRow() + 1) + ":" + (appendSheet.getLastRow() + 1);
Logger.log(archiveAppendRange);
var destRange = ss.getRange(archiveAppendRange);
Logger.log(destRange);
var sourceDataValues = ss.getRange(range.getRow(),1,1,49).copyTo(destRange);
Logger.log(sourceDataValues);
}
What I can't manage to do is to make this script to also copy the old values, when they are edited, and move them to another separate tab (called "old_values").
How can I copy the old value, when cells are edited in the same way I do the new values?
My plan is that I'll merge both tables and send over email.
Here's an example spreadsheet with my code: https://docs.google.com/spreadsheets/d/1xzyVD7ElA0FMVC8sta6beDJCIwOQEMRqjhF5eW4mExI/copy

Related

Have only the rows containing the value in Column B copied into the new sheet

I am trying to accomplish 2 things with Google Sheets. I have a Main sheet with data and I would like to:
Create new sheet with the name based on a cell value in Column B (accomplished)
Copy the rows containing those values in B in that new sheet
Part 1 works fine with this script:
function onOpen() {
var menu = [{
name : "Add",
functionName : "newSheet"
}
];
SpreadsheetApp.getActiveSpreadsheet().addMenu("Sheet", menu);
}
function newSheet() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var templateSheet = ss.getActiveSheet();
var sheet1 = ss.getSheetByName("Sheet1")
var getNames = sheet1.getRange("B2:B").getValues().filter(String).toString().split(",");
for (var i = 0; i < getNames.length; i++) {
var copy = ss.getSheetByName(getNames[i]);
if (copy) {
Logger.log("Sheet already exists");
} else {
templateSheet.copyTo(ss).setName(getNames[i]);
ss.setActiveSheet(ss.getSheetByName(getNames[i]));
ss.moveActiveSheet(ss.getNumSheets());
}
}
}
The problem is when new sheets are created it copies the content of the main sheet. I would like to have only the rows containing the value in Column B copied into the new sheet.
Instead of using copyTo you might use one of the insertSheet methods of Class Spreadsheet, then copy rows having the required value in column B into the new sheet.
The specific code to copy the rows depends on what really need to copy (values, displayed values, formulas, cells formatting, rich text cell content formatting, notes, data validation, conditional formatting)
Let say that you are only interested in passing the values, you could use something like the following in the getNames for loop:
var data = sheet1.getDataRange().getValues().filter(row => row[1] === getNames[i])
var newSheet = ss.insertSheet(getNames[i]);
if( data.length > 0 ) newSheet.getRange(1,1,data.length,data[0].length).setValues(data);
Related
insertSheet advanced options
check for existence of a sheet in google spreadsheets
You can just add one more function to remove redundant rows from any sheet. Something like this:
function main() {
var sheet = SpreadsheetApp.getActiveSheet()
remove_rows("aaa", sheet); // remove all rows, that contain 'aaa' in first column
}
function remove_rows(value, sheet) {
var data = sheet.getDataRange().getValues();
var new_data = data.filter(x => x[0].indexOf(value)>=0)
sheet.getDataRange().clearContent();
sheet.getRange(1,1,new_data.length,new_data[0].length).setValues(new_data);
}

How to get all old values for a mulitple cell range with onEdit trigger

In google apps script, with onEdit trigger, how do I get the old values of all the cells if the edited range has multiple cells? In the Event object documentation https://developers.google.com/apps-script/guides/triggers/events it is specified that the oldValue attribute is "Only available if the edited range is a single cell".
In my case, I use onEdit from the Event object to run a function (that needs oldValue and newValue) only when a specific column is edited. It works fine when the user selects only one cell in my specific column, but if the user selects a few cells or the entire row for example, only data from the first selected cell is retrieved but I need to access the oldValue of my specific column.
You want to retrieve old values when the multiple cells are edited.
If my understanding is correct, how about this answer?
Issue:
Unfortunately, in the current stage, when the multiple cells are edited, there are no methods for retrieving all old values from the event object.
Workaround:
So as the current workaround, how about this flow?
Copy the active Spreadsheet.
This is run only one time.
When the cells are edited, the old values are retrieved by comparing the active Spreadsheet and copied Spreadsheet.
Update the copied Spreadsheet.
By above flow, the cycle for retrieving old values when the cells are edited can be created. When this flow is reflected to the script, it becomes as follows. Please think of this as just one of several answers.
Sample script:
When you use this script, please install the OnEdit event trigger to the function of onEditByTrigger() after copy and paste this script to the script editor of the container-bound script. By this, when the cells are edited, you can see the current and old values at the log.
var backupfilename = "backupfile";
function copyToo(srcrange, dstrange) {
var dstSS = dstrange.getSheet().getParent();
var copiedsheet = srcrange.getSheet().copyTo(dstSS);
copiedsheet.getRange(srcrange.getA1Notation()).copyTo(dstrange);
dstSS.deleteSheet(copiedsheet);
}
// This is run only one time.
function init() {
// Source
var srcss = SpreadsheetApp.getActiveSheet();
var range = srcss.getDataRange().getA1Notation();
var srcrange = srcss.getRange(range);
var srcsheetname = srcss.getName();
// Destination
var backupfile = DriveApp.getFilesByName(backupfilename);
var dstid = backupfile.hasNext()
? backupfile.next().getId()
: SpreadsheetApp.create(backupfilename).getId();
var dstss = SpreadsheetApp.openById(dstid).getSheets()[0]
var dstrange = dstss.getRange(range);
dstss.setName(srcsheetname);
copyToo(srcrange, dstrange);
PropertiesService.getScriptProperties().setProperty('backupfileid', dstid);
return dstid;
}
function onEditByTrigger(e) {
var columnNumber = 1; // If you want to retrieve the old values when the column "A" is edited, it's 1.
var source = e.source;
var range = e.range;
var dstid = PropertiesService.getScriptProperties().getProperty('backupfileid');
if (!dstid) {
dstid = init();
}
if (e.range.columnStart == columnNumber) {
var range = source.getSheetName() + "!" + range.getA1Notation();
var fields = "sheets(data(rowData(values(formattedValue,userEnteredFormat,userEnteredValue))))";
var currentValue = source.getRange(range).getValues();
var oldValue = SpreadsheetApp.openById(dstid).getRange(range).getValues();
Logger.log("currentValue %s", currentValue)
Logger.log("oldValue %s", oldValue)
}
// Update backup file
var range = e.source.getDataRange().getA1Notation();
var srcrange = e.source.getRange(range);
var dstrange = SpreadsheetApp.openById(dstid).getSheets()[0].getRange(range);
copyToo(srcrange, dstrange);
}
Note:
This is a sample script for retrieving the old values when the multiple cells were edited. So if you use this script, please modify this to your situation.
References:
Event Objects
Installable Triggers
If this was not the direction you want, I apologize.

Google App Script : How to copy and paste data from one sheet to another?

I am trying to copy and paste some data one after the others from one sheet to another with a google app script. The thing is that my script doesn't paste the data on the correct row. I would like it to paste the first data in cell A2. Then, when I run the script again, in cell A3 etc. But when I run the script, it pastes the data in cell A266
function Agregation () {
var spreadsheet = SpreadsheetApp.getActive();
var ss = spreadsheet.getSheetByName('Données');
var ts = spreadsheet.getSheetByName('Classement');
var lr = ts.getLastRow();
Logger.log(lr);
ss.getRange('A2:E7').copyTo(ts.getRange('A0'+(lr+1)), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
};
For documentation purposes.
The code was good, but the "Classement" sheet was not empty, since it had a non-empty cell at 'A265', the call to ts.getLastRow(); was returning 265. After clearing the sheet and running the code it was working as intended.
A way of moving forward could be to call ts.clear() before the copy is done to ensure the sheet is empty.
UPDATE
After testing, updated the code in the following way:
function Agregation() {
var spreadsheet = SpreadsheetApp.getActive();
var ss = spreadsheet.getSheetByName('Test données');
var ts = spreadsheet.getSheetByName('Test script');
var lr = ts.getLastRow();
Logger.log(lr);
ss.getRange('A2:E7').copyTo(ts.getRange('A'+(lr+1)), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
var toDelete = ts.getRange("A" + ts.getLastRow() - 3 + ":A" + ts.getLastRow()).getValues();
for (var i = 0; i < 4; i++){
if (toDelete[i][0] == ""){
Logger.log(i);
ts.deleteRow((ts.getLastRow() - 3) + i);
}
}
};
It adds the first line and then checks the next 3 to see if it is "", if so, it deletes the rows, so on the next run there wont be spaces

Go to last line of data in Google Sheets

I have poked around and found the following code that will advance to the last line on data in our Google Spreadsheet- at least until we add more lines beyond row 297.
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var range = ss.getSheets()[0].getRange("B297");
SpreadsheetApp.setActiveRange(range);
}
I am trying to figure out how to write this script so that it will go to the last line of data, regardless of the line number.
Is there a change that I can make to this code to accomplish this?
The method getLastRow() tells you the last row of a sheet that has data. This can be used to move the selection to that row. Another thing I changed in the sheet selection: your script always operates on the first sheet; it makes more sense to operate on whichever sheet is active currently.
function myFunction() {
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange(sheet.getLastRow(), 1);
SpreadsheetApp.setActiveRange(range);
}
This can be placed into the spreadsheet menu using onOpen.
By the way, pressing Ctrl + ArrowDown does the same thing, if you do it in a column that has some data in every row (like the date or Id column).
The script below allows you to go to the last cell with the content of column A. It works even if some cells in the column A contain formulas.
Modifying the number in parentheses in lastRowOfCol(1) allows you to reach the last cell with content from another column.
Additionally, you can also change the focus to the first empty cell after the last one with content.
function onOpen(){
lastRowOfCol(1); //Enter the column number you want to use as the base of the search
}
function lastRowOfCol(column){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var total = sheet.getMaxRows();
var values = SpreadsheetApp.getActiveSheet().getRange(1,column,total).getValues();
for(var i=total-1;values[i]=="" && i>0; i--){}
var last = sheet.getRange(i+1,column);
//var last = sheet.getRange(i+1,1); //Option to fetch the last row of a given column, but to position in column 1
//var last = sheet.getRange(i+2,column); //Option to go to the cell below the last one with content
sheet.setActiveSelection(last);
}
Script from Marcelo Camargo in this forum
The currently relevant option that works on all non-hidden sheets, and not just on the active one:
function onOpen() {
const ss = SpreadsheetApp.getActiveSpreadsheet()
const sheets = ss.getSheets()
const properties = PropertiesService.getDocumentProperties()
const lastActiveSheetName = properties.getProperty("lastActiveSheetName")
let lastActiveSheet
for (let sheet of sheets) {
if (!sheet.isSheetHidden()) {
const sheetName = sheet.getName()
const lastEdit = properties.getProperty(sheetName)
if (lastEdit) {
if (sheetName !== lastActiveSheetName){
sheet.getLastRow() // Without this magic does not work - I could not figure out the reasons
sheet.getLastColumn() // Without this magic does not work - I could not figure out the reasons
const [lastRow, lastCol] = lastEdit.split(',')
sheet.getRange(Number(lastRow), Number(lastCol)).activate() // With focus set to this cell
//sheet.setActiveSelection(sheet.getRange(Number(lastRow), Number(lastCol))) // Without setting focus to this cell
}
else {
lastActiveSheet = sheet
}
}
}
}
if(lastActiveSheet){
lastActiveSheet.getLastRow()
lastActiveSheet.getLastColumn()
const [lastRow, lastCol] = properties.getProperty(lastActiveSheetName).split(',')
lastActiveSheet.getRange(Number(lastRow), Number(lastCol)).activate()
}
}
function onEdit() {
const ss = SpreadsheetApp.getActiveSpreadsheet()
const sheet = ss.getActiveSheet()
if (!sheet.isSheetHidden()) {
const cell = ss.getActiveCell()
const row = cell.getRow()
const column = cell.getColumn()
if (row !== 1 || column !== 1) { // Protection from the evil magic of "self-editing" the first cell
const sheetName = sheet.getName()
PropertiesService.getDocumentProperties().setProperty(sheetName, `${row},${column}`)
PropertiesService.getDocumentProperties().setProperty("lastActiveSheetName", sheetName)
}
}
}
PS: Please note that in the code I do not use a semicolon separator - it's more convenient for me.

Google Spreadsheet function gets data from all sheets instead of just one

This is the function I have written, which sends an email to notify someone whenever a specified cell/row/column is edited, and I have it set to trigger onEdit. It works as is.
/* This function send an email when a specified range is edited
* The spreadsheets triggers must be set to onEdit for the function
*/
function sendNotification() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
//Get Active cell
var mycell = ss.getActiveSelection();
var cellcol = mycell.getColumn();
var cellrow = mycell.getRow();
//Define Notification Details
var recipients = "me#email.com";
var subject = "Update to "+ss.getName();
var body = ss.getName() + "has been updated. Visit " + ss.getUrl() + " to view the changes.";
//Check to see if column is A or B to trigger
if (cellcol == 1, 2)
{
//check for row to trigger
if (cellrow == 1)
{
//Send the Email
MailApp.sendEmail(recipients, subject, body);
}
//End sendNotification
}
}
My issue is that I need this to work only when a column or row on a certain sheet(page) of the spreadsheet is edited, rather than column X in any of the sheets within the spreadsheet.
Any ideas? I'm hoping it's something simple that I missed, but I have been unable to find a solution.
You could interrupt the function after assigning the sheet variable:
if (sheet.getSheetName() != 'SheetIWant') return;
You need to look at the parameters that onEdit provides. It will tell you where the change happened.