2
votes

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!

4
I have also the same situation with you. For this situation, I overwrite the values and formulas of the copied sheet after the template sheet was copied. By this, #REF! can be avoided. I think that in this method, the process cost becomes low, because the values and formulas of data range are retrieved and overwritten. Also I think that there are several methods for such situation. So please think of this as one of them.Tanaike
@Tanaike, how are you overwriting the formulas that reference the copied sheet? Are you doing that manually? Or have you found a way to use apps script to search the spreadsheet for all formulas that use that reference and overwrite them?Alex Libengood
Yes. There is a script. But the answer has already been posted. I'm really sorry I couldn't help.Tanaike

4 Answers

2
votes

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);
  }
}
1
votes

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.

0
votes

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.

0
votes

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.