Google sheets macro only working when I manually trigger it - google-apps-script

I have a spreadsheet that is connected a Hubspot workflow. When a deal is closed in our CRM, Hubspot creates a new row on the sheet with 3 pieces of data from the deal. Due to Hubspot's recommended best practices for working with Google Sheets integration, I have another sheet where I reference data from the sheet that receives the data. The problem arose when I realized that the reference sheet would not automatically refresh when new data was written to the synced sheet. So, I created a macro that copies the formula down using the fill handle every 5 minutes. This rescans the synced sheet and writes any new data to the reference sheet. This morning I woke up and there was some new data that should have been copied over to the reference sheet -- however it wasn't. I checked that the macro is running every 5 minutes, and it is. For the hell of it, I went to the menu and manually ran the macro and, to my surprise, it actually worked! I want this to be fully automated though and don't want to have to manually run a macro everytime I want the data to get copied over.
Data gets synced here
Data is referenced here
Macro Code:
/** #OnlyCurrentDoc */
function RefreshData() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.getRange('A75').activate();
spreadsheet.getActiveRange().autoFill(spreadsheet.getRange('A75:A223'), SpreadsheetApp.AutoFillSeries.DEFAULT_SERIES);
spreadsheet.getRange('B75').activate();
spreadsheet.getActiveRange().autoFill(spreadsheet.getRange('B75:B234'), SpreadsheetApp.AutoFillSeries.DEFAULT_SERIES);
spreadsheet.getRange('C75').activate();
spreadsheet.getActiveRange().autoFill(spreadsheet.getRange('C75:C232'), SpreadsheetApp.AutoFillSeries.DEFAULT_SERIES);
spreadsheet.getRange('C75:C232').activate();
};

The problem is that your macro, doesn't work in the time-driven triggers context. The following "macro" is able to be called by a time-driven trigger:
function RefreshDataByTimeDrivenTrigger() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.getRange('A75:A223')
.setValue(spreadsheet.getRange('A75').getValue());
spreadsheet.getRange('B75:B234')
.setValue(spreadsheet.getRange('B75').getValue());
spreadsheet.getRange('C75:C232')
.setValue(spreadsheet.getRange('C75').getValue());
};
Note:
/** #OnlyCurrentDoc */ was not included in the above code because it applies to the whole project.
I'm intentional using setValue instead of setValues as the same value should be added to all the cells from the specified range.
There are some methods that only works in certain contexts, i.e. SpreadsheetApp.getUi() works then called from a custom menu, but doesn't work when called from a time-driven trigger. The same happens with Range.activate(). Unfortunately the macro recorder adds a lot of Range.activate() and Spreadsheet.getActiveRange() methods and it's not smart enough yet to optimize the recorded user actions by itself, so, in order to make your macro to work when it's called by a time-driven trigger you have to adapt your code:
Remove statements like spreadsheet.getRange('A75').activate();.
Modify statements like spreadsheet.getActiveRange() to use statements like spreadsheet.getRange('A75').
If you are also interested in making your "macro" faster, you might also would like to replace patterns like
spreadsheet.getRange('A75').activate();
spreadsheet.getActiveRange().autoFill(spreadsheet.getRange('A75:A223'), SpreadsheetApp.AutoFillSeries.DEFAULT_SERIES);
by equivalent code that make fewer calls to Google Apps Script methods.
Related
Google sheets macro won't run on trigger

Try it this way:
function RefreshData() {
var ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName("SheetName");
sh.getRange('A75').autoFill(sh.getRange('A75:A223'), SpreadsheetApp.AutoFillSeries.DEFAULT_SERIES);
sh.getRange('B75').autoFill(sh.getRange('B75:B234'), SpreadsheetApp.AutoFillSeries.DEFAULT_SERIES);
sh.getRange('C75').autoFill(sh.getRange('C75:C232'), SpreadsheetApp.AutoFillSeries.DEFAULT_SERIES);
}

Related

Google Sheets - Replace sheet without breaking references to that sheet

We are building a google sheets database where each user has their own spreadsheet that accesses a central sheet for information using apps script.
This means that with 50 employees, we have 50 spreadsheets to maintain. I am trying to find a way to push updates to all 50 spreadsheets without having to update each one manually. I have all the apps script code in a library that each user's sheet references, so I have the coding maintenance figured out. But keeping each users actual spreadsheet up to date with the latest features is proving difficult.
One way I'm figuring to do that is have a "Template" user sheet that gets updated with the changes/new features. Then when each user opens their spreadsheet, it cross references all of its sheets to the template sheet, and checks if it needs to replace it's sheet with the latest sheet based on time that it was updated in the template sheet. For example, when the sheet "Project Report" in the template is newer than the "Project Report" sheet in the user's spreadsheet, the user SS deletes it's current "Project Report" and copies the template "Project Report" sheet to it's own via the copyTo() method.
I have this all working with apps script, but the issue now is that when the user's local sheet is deleted and replaced with the new updated seet, all formula references to that sheet in other sheets break and replace the reference with #REF. I had planned on overcoming this by using only Named Ranges, but even the named ranges break when the sheet is replaced to the point where even the apps script can no longer find the named range because the named range it is looking for was automatically renamed when the new version of the sheet was imported (aka, "CustomNamedRange" in the template SS was renamed to "'SheetName'!CustomNamedRange" in the user SS).
The only way I know to overcome this issue at this point is to create a centralized "Range Index" spreadsheet that has all the named ranges with their destination sheet and range. I would have to create a custom function that filters through the range index and finds the address it needs based on the name given. For example, instead of calling "CustomNamedRange" in a sheet formula, I would call custom function: getNamedRange("CustomNamedRange"), and apps script would return the range found in the range index. And when a sheet is replaced with the newer version, no references would break because all references go through the apps script filter function.
The only problem with this is that I can foresee this method (calling every range needed in the script through a custom function) slowing down my spreadsheet A LOT because every time a range is called for, it will have to go search through the range index to find it and return it.
Does anyone have any other ideas on how to accomplish what I'm looking for? As in keeping 50+ individual spreadsheets updated with new features without having to do it manually and without breaking all the references?
Sorry for the long post, but I appreciate any ideas!
I had a similar problem and was able to resolve it by using SheetAPI to replace text. I have a template called Sheet1_Template and its hidden. I delete Sheet1, copy Sheet1_Template, show it and then replace all occurances of "Sheet1" in formulas to "Sheet1". Sheet API has to be enabled in the Resources and Google API Console.
function copyTemplate() {
try {
var spread = SpreadsheetApp.getActiveSpreadsheet();
var sheet = spread.getSheetByName("Sheet1");
if( sheet !== null ) spread.deleteSheet(sheet);
sheet = spread.getSheetByName("Sheet1_Template");
sheet = sheet.copyTo(spread);
sheet.setName("Sheet1");
sheet.showSheet();
sheet.activate();
spread.moveActiveSheet(0);
var requests = {"requests":[{"findReplace":{"allSheets":true,"find":"Sheet1","replacement":"Sheet1","includeFormulas":true}}]};
Sheets.Spreadsheets.batchUpdate(requests, spread.getId());
}
catch(err) {
Logger.log("error in copyTemplate: "+err);
}
}
I haven't been able to test implementation of it yet, but I believe the answer above is what I was originally looking for.
I haven't spent any time messing with the API yet, so in the meantime I have found another solution:
Google Sheets recently added macros to it's feature set. The beauty of this is that You can see and edit the macro code after you've recorded your actions in the sheet. For now, I plan on recording a macro when I make updates to the template sheet, then copying the script for that macro into a custom function in my library that will run every time a user opens their spreadsheet. When they open their SS, apps script will check to see if the library's macro function has a later date than the last time the sheet was opened. If it does have a new date, then it will run the macro script, and that user's SS should get updated to the same state as the template.
Also if you are seeing that you cannot run the query from #TheWizEd
It may be due to "Sheets API" not being enabled at Advanced Google services. Please enable>
In the script editor, select Resources > Advanced Google services In the dialog that appears, click the on/off switch for Google Sheets API v4. Please turn on. Click OK button.
Thank you so much to TheWizEd for getting me started (please vote for that post too).
This is what I needed:
function replaceFormulasInSheet(sheet, searchFor, replaceWith) {
// https://stackoverflow.com/a/67151030/470749
// First you need to do this to enable the feature: https://developers.google.com/apps-script/guides/services/advanced#enabling_advanced_services
// https://developers.google.com/sheets/api/quickstart/apps-script
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#findreplacerequest
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/batchUpdate
const spread = SpreadsheetApp.getActiveSpreadsheet();
const requests = {
"requests": [
{
"findReplace": {
// "allSheets": true, Omitting this property and instead setting the sheetId property is the only way to effectively set allSheets as false.
"sheetId": sheet.getSheetId(),
"find": searchFor,
"replacement": replaceWith,
"includeFormulas": true
}
}
]
};
return Sheets.Spreadsheets.batchUpdate(requests, spread.getId());
}
Also note that it does not work for sheets with hyphens in their names. If you need hyphens in their names, remove the hyphens beforehand and re-add them after.

Google Spreadsheet run script slow

Let's say I have a spreadsheet called A. When i run a script on spreadsheet A it will load very slowly and may cause timeout sometime. (I thought something was wrong with the function I created so i improved it - but still causing same result). However, once I make a copy of spreadsheet A (name it B). With the exact same code and functions spreadsheet B runs script rather fast. How is that possible when spreadsheet B is the exact same as spreadsheet A?
I found out what was the problem. It is actually because of getScriptProperties in my google spreadsheet. So, in my old spreadsheet the getScriptProperties will take old data (whereby, when i create a new one, i does not exist). On the other hand, when I duplicate a new spreadsheet, it is running for the first time so it will take everything. What i did was deleteProperty of the old spreadsheet so that it can create a new Script Property (which will take new additions).
The code is as below:
var scriptProperties = PropertiesService.getScriptProperties();
scriptProperties.deleteProperty('test');

Google Scripts onEdit not recognising data being edited on a sheet during a sync from a mobile device

I have a Google sheet that is updated by a mobile app created on AppSheet.
I have a column of data that I need to keep a history of so wrote a script to copy the column to a fblank column in another sheet.
function readdailyChecks() {
var sheetFrom = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Todays Checks");
var sheetTo = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("History");
// Copy from 5th column, all rows for one column
var valuesToCopy = sheetFrom.getRange("D2:D100").getValues()
//Paste to another sheet from first cell onwards
sheetTo.getRange(1,sheetTo.getLastColumn()+1,valuesToCopy.length,1).setValues(valuesToCopy);
}
I then wrote another script to do this task when the sheet was edited thinking the sync would edit the sheet data.
function onEdit(e) {
if(e);
readdailyChecks(e)
}
The idea being that the app would sync once the form was completed, update the sheet and trigger the onEdit code to do it's stuff.
The problem is that the sync changes the sheets data without editing it so the historical data is not created!
Is there an onSync code or a way that when the data changes the script can be triggered?
I work at AppSheet. When updates are made to Google sheets via the Google Sheets backend API, they do not fire the onEdit trigger. I'm not sure why exactly this is, but it is a limitation imposed by Google. So that is what you are observing.
The AppSheet documentation suggests that you try a timed trigger instead, polling for changes. https://appsheethelp.zendesk.com/hc/en-us/articles/206483017-Google-Drive
Not the greatest, but it does work. Some AppSheet users have reported success with the onChange trigger instead of the onEdit trigger. To me, this defies logic based on the documented meaning on an onChange trigger, but it appears to work for these users, so worth a shot.

Sync sheet data with calendar after making changes to it locally (thru gDrive)

Is it possible after making some changes to sheet document on PC and successful sync to google drive, to run specific script (on Google side, kind of cron in unix) to import changes from this sheet to Google Calendar service?
P.S. I already know how to import changes from Sheets to Calendar, the question is about automatic run of this import process on document change(edit).
You can check this documentation about Installable Triggers which let Apps Script run a function automatically when a certain event occurs. It is similar to simple triggers for Google Apps like onOpen(), but they can respond to additional events, and they behave differently.
You can use an installable edit trigger that runs when a user modifies a value in a spreadsheet.
Based from this forum:
The onEdit() trigger is the way to go. Here's a simple example
pulled straight from the documentation:
function onEdit(event)
{
var ss = event.source.getActiveSheet();
var r = event.source.getActiveRange();
r.setComment("Last modified: " + (new Date()));
}
Inside the body of the function you can do a variety of things. If you
need access to authorized services (such as Gmail, Calendar, etc) then
you'll need to create an installable trigger instead.
Hope this helps!

Copy last row to another spreadsheet upon form submission

I have a Google spreadsheet with an Add-on that takes data from a form and runs on form submission. I also have another Add-on that pushes the data from this spreadsheet to another spreadsheet - let's call it spreadheet2 here. In spreadsheet2 I have my own script with a function copyLastRow() that copies the last row from this spreadsheet to another spreadsheet - let's call it spreadsheet3. My script is supposed to append a new row from spreadsheet2 to spreadsheet3. It runs OK when I run it manually, but it is not running via the project trigger - which I installed for Script editor's Resources - I tried both on Edit and on Change triggers, but they are simply not firing up when data is pushed from spreadsheet2. The script is working when I actually edit spreadsheet2. However, this is not good for what I need - I really need the script to work without manual intervention. Can you, please, help?
function copyLastRow() {
var target = SpreadsheetApp.openById('xxxxxxxxx').getSheetByName('Sheet1');
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1');
var lastrow = sheet.getLastRow();
var sourceData = sheet.getRange(lastrow, 1, 1, 53).getValues();
target.appendRow(sourceData[0]);
EDIT: I updated the code - I realized I left the previous version of code here.
You're right to worry about whether this function will be effective when multiple users are submitting forms... it won't be. But it's easily improved.
What's the problem? When copyLastRow() runs, it assumes that the last row of the source spreadsheet contains the response that also triggered the function. However, before it gets around to reading that row, another user might submit a form. (Eventually, the function will be triggered by that submission as well, and could process the same row a second time.)
The simplest improvement in this situation is to take advantage of the event object that is provided to the trigger function as a parameter. See Google Sheet Events for some background details.
The newly submitted responses are in event.values, which is an array - exactly what is needed for .appendRow(). Here's how we can update your copyLastRow function:
function copyLastRow(event) {
var target = SpreadsheetApp.openById('xxxxxxxxx').getSheetByName('Sheet1');
target.appendRow(event.values);
}
Now it doesn't matter how many users submit forms - each will be handled uniquely by this function.