Update google "master" spreadsheet when other sheets are updated - google-apps-script

I have multiple Google spreadsheets with booking data such as booking number, client name, email, booking date etc. The order of these columns is not the same in all sheets.
I would like to update all data from all my "source" sheets in one "master" spreadsheet. Meaning as soon as a new row is added or an existing row is updated, the data will be synced to the master spreadsheet
What would be the best way to achieve that? Javascript or is there some existing Google Sheets addon?
Example sheet 1: Fast boat bookings
Example sheet 2: Airport transfer bookings
Master sheet
Thanks so much to everyone looking into this!
Most people recommend to use "importrange", but I don't think this works for my use case.
I am also aware that it could be achieved by Zapier, but it would become to costly to pay for so many zaps. I believe there is another solution.
I do not have any code yet to start with :-/
I expect the data in the master sheet to be sorted by submission date and time like this:

The general procedure for using formulas is
Use IMPORTRANGE to get the data from the source spreadsheets into the master spreadsheet
Use array notation to put the imported data together
NOTES:
If you are new to using IMPORTRANGE, arrays in Google Sheets and complex formulas, use one sheet for each IMPORTRANGE, and delete the unused columns to save cells because the Google Sheets 5 million cell limit
If you prefer to use scripts, you should get the spreadsheets keys, or URLs then you could use SpreadsheetApp.openById(id) or SpreadsheetApp.openByUrl(url) to open the spreadsheets. Then you could use getValues() / setValues() to read / write the values from source spreadsheets to the master spreadsheet.
References
Using arrays in Google Sheets
Extending Google Sheets with Google Apps Script
Related
Is it possible to do ImportRange in Google Apps Script?
IMPORTRANGE to import multiple Google Sheets into one vertical column?

As a workaround for Apps Script triggers not being fired by automatic sheet updates, you need to use IMPORTRANGE to import your data into a dummy sheet. IMPORTRANGE will detect also through automatic sheet update and simultaneously it is able to fire an onEdit trigger.
Do the following:
Delete all empty rows in MASTER spreadsheet
Create a dummy spreadsheet and import into it the contents of MASTER spreadsheetwith a formula:
From the dummy sheet, open the script editor and insert the following code:
function changed(e) {
var masterSheet = SpreadsheetApp.getActive().getSheetByName("Sheet1");
if(PropertiesService.getScriptProperties().getKeys().length==0){
PropertiesService.getScriptProperties().setProperty('startRow', 5);
}
var startRow=PropertiesService.getScriptProperties().getProperty('startRow');
var lastRow=masterSheet.getLastRow();
var numRows=lastRow-startRow+1;
var startCol=1;
var numCols=6;
var values=masterSheet.getRange(startRow,startCol,numRows,numCols).getValues();
var FBsheet=SpreadsheetApp.openById('1SzBx5Q9rrlcLGSqD8y5eFE5TX304LfZ7D9mxxrHhfKw').getSheetByName('Sheet1');
var ATsheet=SpreadsheetApp.openById('1IRD8wT5Kmx7h_xP807f4ibWRn8g98RjA-dJXxADXAl0').getSheetByName('Sheet1');
FBsheet.getRange(FBsheet.getLastRow()+1,startCol, numRows, numCols).setValues(values);
var ATlastRow=ATsheet.getLastRow();
for(var i=0;i<numRows;i++){
Logger.log(values[i][1]);
ATsheet.getRange(ATlastRow+1+i,1, 1, 1).setValue(values[i][1]);
ATsheet.getRange(ATlastRow+1+i,2, 1, 1).setValue(values[i][0]);
ATsheet.getRange(ATlastRow+1+i,3, 1, 1).setValue(values[i][3]);
ATsheet.getRange(ATlastRow+1+i,4, 1, 1).setValue(values[i][2]);
ATsheet.getRange(ATlastRow+1+i,5, 1, 1).setValue(values[i][5]);
ATsheet.getRange(ATlastRow+1+i,6, 1, 1).setValue(values[i][4]);
}
PropertiesService.getScriptProperties().setProperty('startRow', lastRow+1);
}
Attach to the script an onEdit trigger
Run the script once manually, it is normal that it will throw you an error
Now the script will run automatically each time data update takes place in MASTER spreadsheet.
Make sure you change all spreadsheetIDs and sheet names with your
values.

Related

Duplicate Sheet in Google Sheets leaving Custom Script behind

I recently added a script within the Script Editor of Google Sheets to auto-sort my page based on values in Column D. It was set up to apply to the active sheet – Let's call it 'Sheet A'
Sheet A is used so people can view the data sort in descending order, which is great. Every time a change/edit is made to Sheet A, the data automatically resorts itself.
I did plenty of cell merging, styling, conditional formatting, etc in Sheet A that I don't want to have to recreate it. So I duplicated Sheet A – let's call it 'Sheet B' and applied Data > Sort Range alphabetical (Column E is names) so that people can have to option to view this way and find themselves much quicker than they would in Sheet A.
THE ISSUE: The custom script I added in Sheet A is being carried over into Sheet B – so anytime I make an edit, Sheet B auto-sorts by descending order from values in Column D.
Is it possible to duplicate my Sheet A but not keep the script in the copy (Sheet B)? Or is there a better way to migrate Sheet A to Sheet B that ignores the script coming with it all together?
There is no simple way to do copy a spreadsheet without the linked files, either a form or a bounded script.
In your case that the spreadsheet includes simple triggers perhaps the simplest solution is to include a condition to check if the id of the active spreadsheet is equal to the id of the "original" spreadsheet:
function onEdit(e){
if( e.source.getId() !== 'put_here_the_id_of_the_original_spreadsheet') return;
// put here the things to do
}
NOTE: Bounded scripts are bounded to the container (in this case the spreadsheet) not to an specific sheet (page in Google Docs or slide in Google Slides), so leaving a script behind when making a a copy of one sheet doesn't make sense.
If you want that a function only make changes to certain sheet then you should add a condition to compare the name of the active sheet with the name of the "original" sheet
function onEdit(e){
// Get the active sheet
var sheet = e.range.getSheet();
var name = sheet.getName();
if(name !== 'SheetA') return;
// put here the things to do
}
Related
Copying a spreadsheet copies all linked files as well
How can I delete container-bound scripts from multiple Google Documents or Sheets simultaneously without deleting the containers/files?

how do I reset the google sheets counter?

Current function: Using apps script, I delete a few tabs then add a few tabs. So new a new sheet is like 'Sheet 467'. The number is getting big very quickly even though there are only 8 sheets in the file.
Desired function: I'd like to reset the sheet counter so the next sheet is 9 (number of sheets currently in the file +1) rather than 468 (number of sheets there have ever been in the file +1).
I tried to google this but I think the search terms are too generic. I couldn't find it anywhere.
Explanation
Unfortunately, this is the default behaviour of how sheets are created. You can request it as a feature but I don't think this will ever be implemented.
However, I would like to propose the following two workarounds:
User inserts a new sheet:
Take advantage of the onChange trigger and especially the event
object property changeType. When you add a new sheet, the latter
takes the value of INSERT_GRID.
This allows you to trigger a piece
of code when a new sheet is added by the user.
The following script will check whether you created a new sheet. If you did, it will rename the last created sheet as Sheet plus the total number of sheets.
Script inserts a new sheet:
If the new sheets are created by a script the onChange trigger can't be used. Then use the second solution I propose.
Workarounds:
Solution when the user inserts a new sheet:
function nameNewSheet(e) {
if (e.changeType == 'INSERT_GRID') {
const sheets = SpreadsheetApp.getActive().getSheets();
sheets[sheets.length-1].setName(`Sheet${sheets.length}`)
}
}
In order to use this solution you have to create an installable onChange trigger for the nameNewSheet function. Please read this answer on how to do that.
Solution when the script inserts a new sheet:
If the script itself adds new sheets, then onChange won't be triggered. But you can incorporate the following two lines into your existing script that inserts new sheets in order to rename the last created sheet.
function myFunction() {
//
// the code of the script that adds a new sheet
//
const sheets = SpreadsheetApp.getActive().getSheets();
sheets[sheets.length-1].setName(`Sheet${sheets.length}`);
}
Illustration:

How to have a reference to a sheet bound to the name instead of the sheet

I'm making a spreadsheet to format the responses of a google form. However, this form will be given every week; ideally, I would like to import the form responses into this spreadsheet, have the formatted data sheet now use this new responses sheet instead of the older one, without having to update the formulae in the data sheet. But if I try to do this by renaming the new data sheet to what the old one was called, it keeps the old sheet reference and updates its name in all the formulae.
I haven't found any solutions from a few google searches. Is there an easy way to get around this?
EDIT: here is an example spreadsheet. Ideally, I want the data in 'Formatting Sheet' to be retrieved from 'New Sheet' instead of old sheet. However, I don't want to have to change all of the formulae in 'Formatting Sheet' to reflect this, as this will be a transition I do regularly. Unfortunately renaming the sheets preserves the reference.
You want to use the values at the sheet Formatting Sheet by replacing the values in the sheet Old Sheet with new values in the sheet New Sheet.
In your current issue, even when the sheet name is changed, the values cannot be updated.
If my understanding is correct, how about this answer? Please think of this as just one of several possible answers.
In your case, even when the sheet name is changed, the sheet ID is not changed. By this, the formula uses old values. I think that the reason of your issue is this. In this answer, in order to achieve your goal, Google Apps Script is used.
Flow:
The flow of the sample script is as follows.
Retrieve the sheet object of New Sheet and Old Sheet, which are the source sheet and destination sheet, respectively.
Backup Old Sheet.
In you don't want to backup it, please remove destinationSheet.copyTo(ss).setName(destinationSheet.getName() + "_backup_" + new Date().toISOString());.
Copy the values from New Sheet to Old Sheet. By this, the values are replaced. And at Formatting Sheet, the new values are used.
Sample script:
Please copy and paste the following sample script to the script editor of your shared Spreadsheet. And run myFunction().
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sourceSheet = ss.getSheetByName("New Sheet");
var destinationSheet = ss.getSheetByName("Old Sheet");
// Backup "Old Sheet".
destinationSheet.copyTo(ss).setName(destinationSheet.getName() + "_backup_" + new Date().toISOString());
// Put values from "New Sheet" to "Old Sheet"
sourceSheet.getDataRange().copyTo(destinationSheet.getRange(1, 1));
}
When you run the script of myFunction(), you can see the result calculated by new values at the sheet of Formatting Sheet.
References:
copyTo() in Class Sheet
copyTo() in Class Range

Apps Script: how do you copy values from specific columns to a different spreadsheet via a time trigger, button press or sheet update?

I need to copy the values of multiple ranges of data from my main spreadsheet into another spreadsheet and then copy a different range of data back to the main spreadsheet.
Here are my example spreadsheets:
The main spreadsheet (two sheets: 1 - A sheet of pasted values alongside columns of user comments. 2 - A sheet with a button that runs the Apps Script)
The live data spreadsheet (pulls and formats the required data from other sheets).
Previously, I’ve used .copyTo() to copy the values of data and paste them elsewhere on the same spreadsheet. However, this method can’t be used to copy data to a different spreadsheet. Copying data from multiple ranges is also causing me issues. Here is my code:
function RefreshSheetData() {
// 1a. Run the script when a button is pressed (Main Spreadsheet - ‘Update report button!A3’)
// 1b. Run the script at a certain time (every Monday at 10 AM)
// 1c. Run the script when data in a sheet is replaced with new data via a formula – NOT POSSIBLE.
// 2. Copy email address and user comments (Main Spreadsheet - Editable report - J3:J & AM3:AR)
// 3. Clear the “Updated user comments” sheet below the header row (Live data Spreadsheet - Updated user comments - A2:G)
// 4. Paste the values (Live data Spreadsheet - Updated user comments - A2:G)
// 5. Clear the “Editable report” below the header rows (Main Spreadsheet - Editable report – B3:AR)
// 6. Copy the Live sheet (which should now include the most recent user comments via array vlookup) (Live data Spreadsheet - live data – A3:AQ)
// 7. Paste the values (Main Spreadsheet - Editable report - B3:AR)
// 8. Add the (United Kingdom) time and date (Main Spreadsheet - Update report button - A10)
// 9. Add the time and date (Live data Spreadsheet - Updated user comments - J1)
ScriptApp.newTrigger('RefreshSheetData')
.timeBased()
.onWeekDay(ScriptApp.WeekDay.MONDAY)
.atHour(10)
.create();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var startSheet = ss.getSheetByName('Editable report');
var sourceRange = startSheet.getRangeList(['J3:J', 'AM3:AR']);
var sourceValues = sourceRange.getValues();
var target = SpreadsheetApp.openById('1OHQHefYvE4vZZPr8jgziy_L3-UBf1WSoKzMWQ8LUz6w');
var targetSheet = target.getSheetByName('Updated user comments');
var clearTargetRange = targetSheet.getRange('A2:G').clearContent();
var targetRange = targetSheet.getRange('A2').setValues(sourceValues);
var liveTargetSheet = target.getSheetByName('Live data');
var liveSourceRange = liveTargetSheet.getRange('A3:AQ').getValues();
var clearMainRange = startSheet.getRange('B3:AR').clearContent();
var startRange = startSheet.getRange('B3').setValues(liveSourceRange);
SpreadsheetApp.getActive().getRange('A10').setValue(new Date());
targetSheet.getRange('J1').setValue(new Date())
}
Any tips would be appreciated. Thank you.
Part of my question was how to trigger a script when a formula output changes. That’s not possible. Neither .onEdit or .onChange triggers work as they only respond to user actions. They won't run when the value of an IMPORTRANGE or alternative formula changes.
Here some suggestions referring to each of the steps mentioned in your Apps Script file
1b. To run the script at a certain time: please use the installable trigger "Time driven"
1c. To run the script when data in the sheet is updated: please use the onEdit trigger
1d. You can combine all triggers by simply adding as many, as required.
To copy and paste data, you just need the methods getValues() and setValues(), which you were using already, the important thing is that you chose the "to copy" range correctly.
You can clear a range with clear()
See 3.
You can copy a sheet with copyTo(), however keep in mind that if using this method, your data in the copied sheet will automatically be updated if there is a change in the original sheet. If you want the values to remain static, you have to copy and paste them with copyValues() and setValues().
See 2. and 5.
See here how to get and format the date in Apps Script
Assign the date to a variable and use setValue()
I encourage you to try and build the script based on those steps
yourself, the Apps Script documentation provides you good reference
and guidance, how to do so. If you encounter specific problems during
one of the steps which you cannot solve with the documentation, feel
free to ask!

How to create Google Scripts loop to activate and copy data from each tab in the workbook

I have a Spreadsheet that has X number of sheets (the number of sheets will change constantly). The format of the data in each sheet is the same. I want to create a macro to activate each sheet in the Spreadsheet and pull data from specific cells into a new sheet (essentially I'm consolidating info from each sheet in one place). I see how to activate a sheet using getSheetByName(), but the name and number of sheets will change regularly. I was hoping to create a loop that does the following:
Activate the first sheet.
Copy data from a few cells in that sheet to a "Dashboard" sheet.
Activate the next sheet.
Copy data from a few cells in that sheet to a "Dashboard" sheet.
Repeat until all sheets have been reviewed.
I can use the following code to activate the next sheet, but how do I make a loop that stops after the last sheet?
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.getRange('A2').activate();
var nextSheetIndex = spreadsheet.getActiveSheet().getIndex() + 1;
if (nextSheetIndex > spreadsheet.getSheets().length) { nextSheetIndex = 1; }
spreadsheet.setActiveSheet(spreadsheet.getSheets()[nextSheetIndex - 1], true);
spreadsheet.getRange('A2').activate();
nextSheetIndex = spreadsheet.getActiveSheet().getIndex() + 1;
Any tips would be greatly appreciated!
First of all, you don't need to activate Sheet to perform changes. Since you want to iterate over each Sheet in a Spreadsheet, use the getSheets() method to retrieve an Array of Sheet instances.
Then, iterate over each Sheet with a loop of your liking (for,for...in,forEach(),etc) and, during each iteration of the loop, access the Range value you want to pull data from via getRange(yourRangeReference).getValue() and copy it to the destination Sheet.
If you need to check and account for number of sheets changing dynamically, while you are iterating over them, getNumSheets() method on each iteration step and, if its result is greater than initial, access and then push() this sheet into the Array.
Useful links
MDN documentation on for loop (the simplest one);
SpreadsheetApp reference for Spreadsheet service;
Developer guide on extending Google Sheets;