Triggering function from one master spreadsheet in other spreadsheet documents - javascript

I have a bunch of spreadsheets containing financial forecast. At the end of each month I make a copy to lock in the months forecast in every sheet. This works well. The problem is that I have to open all the sheets separately in order to activate the script in each sheet. So I was wondering if there is a way to trigger the script via a button from a master spreadsheet that looks something like this: https://docs.google.com/spreadsheets/d/1YA6twVP3_oSJttcLx5JRdm8TO01zKFr2k_D9lrlc1eE/edit#gid=0
The script I'd like to trigger looks like this:
function CopyMonth() {
const sh = SpreadsheetApp.getActive();
let ss = sh.duplicateActiveSheet();
let name = ss.getRange("a26").getDisplayValue();
ss.setName(name);
var sheet = SpreadsheetApp.getActiveSheet();
var rangeToCopy = sheet.getRange(1, 1, sheet.getMaxRows(), sheet.getMaxColumns());
rangeToCopy.copyTo(sheet.getRange(1, 1), {contentsOnly: true});
}
The time and date I lock the forecast varies so it cannot be activated by a specific time or something like that.
The marked duplicate does not address my issue. The subject referenced to explains how to create a button to run a script. I'm asking how to run a script I've made in one document from an entirely different document. My problem is that I have 8 document I have to open and press a button today to run my script. I want to be able to run the scrip in all 8 documents from a master document instead.

Provided that all the spreadsheets you want to process are in the same folder, with no other spreadsheets in that folder or its subfolders, you can use something like this:
/**
* Makes a values only copy of the first tab in all
* spreadsheets in a folder and its subfolders.
*/
function insertValuesOnlyMonthTabInManySpreadsheets() {
// version 1.0, written by --Hyde, 14 December 2022
// - see https://stackoverflow.com/q/74801764/13045193
const folderName = '...put folder name here...';
const _action = (file) => {
const ss = getSpreadsheetFromFile_(file);
if (!ss) {
return;
}
const firstTab = ss.getSheets()[0];
const tabName = firstTab.getRange('A26').getDisplayValue();
let newTab;
try {
newTab = firstTab.copyTo(ss);
newTab.setName(tabName);
} catch (error) {
handleError_(error);
if (!newTab) {
return;
}
}
const rangeToCopyAsValuesOnly = firstTab.getDataRange();
rangeToCopyAsValuesOnly.copyTo(newTab.getRange(1, 1), { contentsOnly: true });
console.log(`Processed '${ss.getName()}'.`);
};
const folder = DriveApp.getFoldersByName(folderName).next();
processFilesInFolderRecursively_(folder, _action, handleError_);
}
For that to work, you will need to paste the getSpreadsheetFromFile_(), processFilesInFolderRecursively_() and handleError_() functions in the script project. You can get them from the processFilesInFolderRecursively_ script.

Related

Why are editor Apps Script executions different from trigger executions?

Have found nothing online on this:
I have built a script that repeatedly populates a sheet before copying it to a separate spreadsheet. The issue is my script behaves differently if executed from the editor versus when triggered from inside the spreadsheet.
When run from the Apps Script editor, the script runs as expected whereas when run from a custom menu item or a trigger in an embedded photo, my function populating my original sheet doesn't have a chance to finish execution before it is copied to a separate spreadsheet.
When executed from a trigger, my separate spreadsheet's copied sheet resembles my original sheet mid-execution. It gets copied before the function populating the original sheet is done executing. This is a problem since my code is meant for another person to use it directly from the spreadsheet.
edit (here's the code):
//loop thru supervisor, create new page
for (let i = 0; i < swConcise.length; i++){
let swName = swConcise[i];
//draw social worker report function
batchSocialWorker(swName);
//copy to alt ss
const sheet = baseSS.getSheetByName('SocialWorkersReport');
sheet.copyTo(newSS);
//rename sheet
newSS.getSheets()[i+1].setName(swName);
}
Edit (longer code if you need):
function batchSocialWorkers(){
/*
1. create new ss, set baseSS, newSS vars
2. create directors array
3. loop directors, copy sheet to newSS and rename
4. create link in new sheet
*/
const baseSS = SpreadsheetApp.getActiveSheet();
//create new ss titled date, sw Report
const today = date();
const title = "Social Workers Reports " + today;
const id = createSS (title);
const newSS = SpreadsheetApp.openById(id);
//scrape social workers names
const swAll = baseSS.getSheetByName('Relationships').getRange('F11:F').getValues();
//create concise s0cial workers array
const swConcise = cleanupArray(swAll);
//loop thru supervisor, create new page
for (let i = 0; i < swConcise.length; i++){
let swName = swConcise[i];
//draw social worker report function
batchSocialWorker(swName);
//copy to alt ss
const sheet = baseSS.getSheetByName('SocialWorkersReport');
sheet.copyTo(newSS);
//rename sheet
newSS.getSheets()[i+1].setName(swName);
}
//delete first 2 sheets
newSS.deleteSheet(newSS.getSheets()[0]);
newSS.deleteSheet(newSS.getSheets()[0]);
//provide link to new sheet
const link = 'https://docs.google.com/spreadsheets/d/' + id;
var ui = SpreadsheetApp.getUi();
ui.alert('Batch Social Workers Reports Created', link, ui.ButtonSet.OK);
}
function createSS (title) {
// This code uses the Sheets Advanced Service, but for most use cases
// the built-in method SpreadsheetApp.create() is more appropriate.
try {
let sheet = Sheets.newSpreadsheet();
sheet.properties = Sheets.newSpreadsheetProperties();
sheet.properties.title = title;
const spreadsheet = Sheets.Spreadsheets.create(sheet);
return spreadsheet.spreadsheetId;
} catch (err) {
// TODO (developer) - Handle exception
console.log('Failed with error %s', err.message);
}
}
function cleanupArray (array){
let newArray = new Array();
for (let i = 0; i < array.length; i++){
if (array[i][0] != '') newArray.push(array[i][0]);
}
return newArray;
}
This is partly a guess because you have not provided all of the code:
But try this. I still don't know which functions you are calling by a trigger. But I did see several errors in your code. I tried to repair what I can.
function batchSocialWorkers() {
const bss = SpreadsheetApp.getActive();
const title = "Social Workers Reports " + Utilities.formatDate(new Date(),bss.getSpreadsheetTimeZone(),"MM/dd/yyyy");//guess at the format
const ss = SpreadsheetApp.create(title);
const sh = bss.getSheetByName('Relationships')
const vs = sh.getRange('F11:F' + sh.getLastRow()).getValues();
for (let i = 0; i < vs.length; i++) {
let swName = vs[i];
batchSocialWorker(swName);
const sheet = bss.getSheetByName('SocialWorkersReport');
sheet.copyTo(ss);
ss.getSheets()[i + 1].setName(swName);
}
ss.deleteSheet(ss.getSheets()[0]);
ss.deleteSheet(ss.getSheets()[0]);
const link = 'https://docs.google.com/spreadsheets/d/' + id;
var ui = SpreadsheetApp.getUi();
ui.alert('Batch Social Workers Reports Created', link, ui.ButtonSet.OK);
}

How to run a function based on an answer

I am trying to figure out how to make a script to run a function based on an answer provided, at the moment my script creates and populates a word doc with answers provided from a google form. I want to streamline it so that I can use one google form to create different documents based on an answer provided rather than creating multiple google forms. I am very new to JavaScript and I am pretty sure I need an if statement at the beginning of everything, but I don't know what I should write or where it should go. This is my script:
function onOpen() {
const ui = SpreadsheetApp.getUi();
const menu = ui.createMenu('AutoFill Docs');
menu.addItem('Create New Docs', 'createNewGoogleDocs');
menu.addToUi();
}
function createNewGoogleDocs(){
const googleDocTemplate = DriveApp.getFileById('google file goes here');
const destinationFolder = DriveApp.getFolderById('google folder goes here');
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Response');
const rows = sheet.getDataRange().getValues();
rows.forEach(function(row,index) {
if (index === 0) return;
if (row[9]) return;
const copy = googleDocTemplate.makeCopy(`${row[5]} document name goes here`, destinationFolder);
const doc = DocumentApp.openById(copy.getId())
const body = doc.getBody();
body.replaceText('{{Name}}', row[5]);
body.replaceText('{{To}}', row[6]);
body.replaceText('{{Items}}', row[7]);
body.replaceText('{{Reasoning}}', row[8])
body.replaceText('{{Submitter}}', row[1]);
body.replaceText('{{Role}}', row[2]);
body.replaceText('{{Time}}', row[3]);
body.replaceText('{{Discord}}', row[4]);
doc.saveAndClose();
const url = doc.getUrl();
sheet.getRange(index + 1, 10).setValue(url)
})
}

Restrict Editors to Specific Ranges Script

Thanks to the help of someone from Stack, the following script was provided to make a protection script I had written run faster. While the new script does apply protections except the specified ranges, users who are provided editor access are able to edit outside of the desired ranges.
My hope is to ensure that users are only able to enter data in specific ranges, but in order to enter data, they need editor access. Is it possible to restrict editors to only edit the desired ranges?
// This script is from https://tanaikech.github.io/2017/07/31/converting-a1notation-to-gridrange-for-google-sheets-api/
function a1notation2gridrange1(a1notation) {
var data = a1notation.match(/(^.+)!(.+):(.+$)/);
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(data[1]);
var range = ss.getRange(data[2] + ":" + data[3]);
var gridRange = {
sheetId: ss.getSheetId(),
startRowIndex: range.getRow() - 1,
endRowIndex: range.getRow() - 1 + range.getNumRows(),
startColumnIndex: range.getColumn() - 1,
endColumnIndex: range.getColumn() - 1 + range.getNumColumns(),
};
if (!data[2].match(/[0-9]/)) delete gridRange.startRowIndex;
if (!data[3].match(/[0-9]/)) delete gridRange.endRowIndex;
return gridRange;
}
// Please run this function.
function myFunction() {
// Please set your sheet names and unprotected ranges you want to use.
const obj = [
{ sheetName: "Ordering", unprotectedRanges: ["O5:P", "C2:E2"] },
{ sheetName: "Accessory INV", unprotectedRanges: ["E5:H"] },
{ sheetName: "Apparel INV", unprotectedRanges: ["E5:F"] },
{sheetName: "Pending TOs", unprotectedRanges: ["E6:H"] },
{sheetName: "INV REF", unprotectedRanges: ["C6:C"] },
];
// 1. Retrieve sheet IDs and protected range IDs.
const spreadsheetId = SpreadsheetApp.getActiveSpreadsheet().getId();
const sheets = Sheets.Spreadsheets.get(spreadsheetId, { ranges: obj.map(({ sheetName }) => sheetName), fields: "sheets(protectedRanges(protectedRangeId),properties(sheetId))" }).sheets;
const { protectedRangeIds, sheetIds } = sheets.reduce((o, { protectedRanges, properties: { sheetId } }) => {
if (protectedRanges && protectedRanges.length > 0) o.protectedRangeIds.push(protectedRanges.map(({ protectedRangeId }) => protectedRangeId));
o.sheetIds.push(sheetId);
return o;
}, { protectedRangeIds: [], sheetIds: [] });
// 2. Convert A1Notation to Gridrange.
const gridranges = obj.map(({ sheetName, unprotectedRanges }, i) => unprotectedRanges.map(f => a1notation2gridrange1(`${sheetName}!${f}`)));
// 3. Create request body.
const deleteProptectedRanges = protectedRangeIds.flatMap(e => e.map(id => ({ deleteProtectedRange: { protectedRangeId: id } })));
const protects = sheetIds.map((sheetId, i) => ({ addProtectedRange: { protectedRange: { range: { sheetId }, unprotectedRanges: gridranges[i] } } }));
// 4. Request to Sheets API with the created request body.
Sheets.Spreadsheets.batchUpdate({ requests: [...deleteProptectedRanges, ...protects] }, spreadsheetId);
}
Edit: The solution provided by Tanaike works to restrict editors to me (the owner), but the script will be run by other users when they insert a row using the following:
function addNewApparelSKU() {
const ss = SpreadsheetApp.getActive();
const ui = SpreadsheetApp.getUi();
const sheet = ss.getSheetByName('Apparel INV');
const response = ui.prompt('Enter New SKU', ui.ButtonSet.OK_CANCEL);
if (response.getSelectedButton() === ui.Button.OK) {
const text = response.getResponseText();
sheet.appendRow([text]);
sheet.sort(1);
myFunction(); //references the Protection script
}
}
When this script is used by another editor, it gives an error because the user can't insert a row due to the restrictions.
I believe your goal is as follows.
You want to protect the specific ranges in each sheet and want to make users edit only the specific ranges.
From your updated question, the script of addNewApparelSKU is run by clicking a button on Spreadsheet.
About the following script was provided to make a protection script I had written run faster., if the script of your previous question is used, how about the following modified script?
And, in this case, it is required to run the script (in this case, it's myFunction().) by the owner (you). For this, I would like to run this script using Web Apps. By this, the script can be run by the owner.
Usage:
1. Sample script:
Please copy and paste the following script to the script editor of Spreadsheet. And pleaes enable Sheets API at Advanced Google services.
And, please set your email address to const email = "###"; in myFunction.
function addNewApparelSKU() {
// This is from addNewApparelSKU().
const ss = SpreadsheetApp.getActive();
const ui = SpreadsheetApp.getUi();
const response = ui.prompt('Enter New SKU', ui.ButtonSet.OK_CANCEL);
if (response.getSelectedButton() === ui.Button.OK) {
const text = response.getResponseText();
const webAppsUrl = "https://script.google.com/macros/s/###/exec"; // Pleas set your Web Apps URL.
const url = webAppsUrl + "?text=" + text;
const res = UrlFetchApp.fetch(url, {muteHttpExceptions: true});
// ui.alert(res.getContentText()); // You can see the response value using this line.
}
}
function doGet(e) {
const text = e.parameter.text;
const sheet = SpreadsheetApp.getActive().getSheetByName('Apparel INV');
sheet.appendRow([text]);
sheet.sort(1);
myFunction();
return ContentService.createTextOutput(text);
}
// This script is from https://tanaikech.github.io/2017/07/31/converting-a1notation-to-gridrange-for-google-sheets-api/
function a1notation2gridrange1(a1notation) {
var data = a1notation.match(/(^.+)!(.+):(.+$)/);
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(data[1]);
var range = ss.getRange(data[2] + ":" + data[3]);
var gridRange = {
sheetId: ss.getSheetId(),
startRowIndex: range.getRow() - 1,
endRowIndex: range.getRow() - 1 + range.getNumRows(),
startColumnIndex: range.getColumn() - 1,
endColumnIndex: range.getColumn() - 1 + range.getNumColumns(),
};
if (!data[2].match(/[0-9]/)) delete gridRange.startRowIndex;
if (!data[3].match(/[0-9]/)) delete gridRange.endRowIndex;
return gridRange;
}
// Please run this function.
function myFunction() {
const email = "###"; // <--- Please set your email address.
// Please set your sheet names and unprotected ranges you want to use.
const obj = [
{ sheetName: "Ordering", unprotectedRanges: ["O5:P", "C2:E2"] },
{ sheetName: "Accessory INV", unprotectedRanges: ["E5:H"] },
{ sheetName: "Apparel INV", unprotectedRanges: ["E5:F"] },
{sheetName: "Pending TOs", unprotectedRanges: ["E6:H"] },
{sheetName: "INV REF", unprotectedRanges: ["C6:C"] },
];
// 1. Retrieve sheet IDs and protected range IDs.
const spreadsheetId = SpreadsheetApp.getActiveSpreadsheet().getId();
const sheets = Sheets.Spreadsheets.get(spreadsheetId, { ranges: obj.map(({ sheetName }) => sheetName), fields: "sheets(protectedRanges(protectedRangeId),properties(sheetId))" }).sheets;
const { protectedRangeIds, sheetIds } = sheets.reduce((o, { protectedRanges, properties: { sheetId } }) => {
if (protectedRanges && protectedRanges.length > 0) o.protectedRangeIds.push(protectedRanges.map(({ protectedRangeId }) => protectedRangeId));
o.sheetIds.push(sheetId);
return o;
}, { protectedRangeIds: [], sheetIds: [] });
// 2. Convert A1Notation to Gridrange.
const gridranges = obj.map(({ sheetName, unprotectedRanges }, i) => unprotectedRanges.map(f => a1notation2gridrange1(`${sheetName}!${f}`)));
// 3. Create request body.
const deleteProptectedRanges = protectedRangeIds.flatMap(e => e.map(id => ({ deleteProtectedRange: { protectedRangeId: id } })));
const protects = sheetIds.map((sheetId, i) => ({ addProtectedRange: { protectedRange: { editors: {users: [email]}, range: { sheetId }, unprotectedRanges: gridranges[i] } } }));
// 4. Request to Sheets API with the created request body.
Sheets.Spreadsheets.batchUpdate({ requests: [...deleteProptectedRanges, ...protects] }, spreadsheetId);
}
2. Deploy Web Apps.
The detailed information can be seen at the official document.
On the script editor, at the top right of the script editor, please click "click Deploy" -> "New deployment".
Please click "Select type" -> "Web App".
Please input the information about the Web App in the fields under "Deployment configuration".
Please select "Me" for "Execute as".
This is the importance of this workaround.
Please select "Anyone" for "Who has access".
In your situation, I thought that this setting might be suitable.
Please click "Deploy" button.
Copy the URL of the Web App. It's like https://script.google.com/macros/s/###/exec.
When you modified the Google Apps Script, please modify the deployment as a new version. By this, the modified script is reflected in Web Apps. Please be careful this.
You can see the detail of this in the report of "Redeploying Web Apps without Changing URL of Web Apps for new IDE".
Please set the Web Apps URL to const url = "https://script.google.com/macros/s/###/exec"; in the above script.
Please modify the deployment as a new version. By this, the modified script is reflected in Web Apps. Please be careful this. You can see this flow at here.
3. Testing.
Please run addNewApparelSKU() by clicking the button. By this, the script is run by the owner.
Note:
When you modified the Google Apps Script, please modify the deployment as a new version. By this, the modified script is reflected in Web Apps. Please be careful this.
You can see the detail of this in the report of "Redeploying Web Apps without Changing URL of Web Apps for new IDE".
My proposed script is a simple script. So please modify it for your actual situation.
References:
Web Apps
Taking advantage of Web Apps with Google Apps Script

Trying to import all google sheets files within a folder to a spreadsheet, all column headers are the same

I need to be able to take all files from a a folder within drive and input the data into the spreadsheet. I am also creating a Menu Tab so I can just Run the script without going to editor. It would be great if I can create a way enter names of existing folder name without always going to the script in order to take out that extra step. This is the script I am using. I really need assistance with this.
function importTimesheets() {
var spreadsheets = DriveApp.
getFolderById("").
getFilesByType(MimeType.GOOGLE_SHEETS);
var data = [];
while (spreadsheets.hasNext()) {
var currentSpreadsheet = SpreadsheetApp.openById(spreadsheets.next().getId());
data = data.concat(currentSpreadsheet
.getSheetByName('Timesheet')
.getRange("A3:L10")
.getValues()
);
}
SpreadsheetApp.
getActiveSheet().
getRange(1, 1, data.length, data[0].length).
setValues(data);
}
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Generate Timesheets')
.addItem('Generate', 'importTimesheets')
As far as I understand you are trying to find a solution for this line of code, whereby you currently have to enter the Id manually.
DriveApp.getFolderById("")
My suggestions would be to prompt the user for the folder Id, because prompting for the folder name may cause errors if more than one folder has the same name. My suggestion is implemented as follows:
const folderId = SpreadsheetApp.getUi().prompt("Please enter the Folder Id").getResponseText()
const folder = DriveApp.getFolderById(folderId)
You could also use the Google File/Folder picker, as described here to search and select the folder.
Try this:
var level=0;
function getFnF(folder = DriveApp.getRootFolder()) {
const ss=SpreadsheetApp.getActive();
const sh=ss.getSheetByName('Sheet1')
const files=folder.getFilesByType(MimeType.GOOGLE_SHEETS);
while(files.hasNext()) {
let file=files.next();
let firg=sh.getRange(sh.getLastRow() + 1,level + 1);
firg.setValue(Utilities.formatString('File: %s', file.getName()));//need editing
}
const subfolders=folder.getFolders()
while(subfolders.hasNext()) {
let subfolder=subfolders.next();
let forg=sh.getRange(sh.getLastRow() + 1,level + 1);
forg.setValue(Utilities.formatString('Fldr: %s', subfolder.getName()));//needs editing
level++;
getFnF(subfolder);
}
level--;
}
function runThisFirst() {
let r = SpreadsheetApp.getUi().prompt('Folder id','Enter Folder Id',SpreadsheetApp.getUi().ButtonSet.OK);
let folder = DriveApp.getFolderById(r.getResponseText())
getFnF(folder);
}

How to make sure a block of Script runs first, before the next block?

sorry if this is an elementary question, but I just can't get this to work the way I need.
I have a script that essentially consists of 3 parts:
1). Removes all protection in a sheet
2). Executes some copying functions (since ranges are protected I need to remove the protection first #1)
3). Sets the protection back up after #2 is finished.
Here's my code:
First clears protection
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('COST REPORT');
var protections = ss.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (var i = 0; i < protections.length; i++) {
var protection = protections[i];
if (protection.canEdit()) {
protection.remove();
}
}
Second clears data in cells
var costReport = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(
'COST REPORT');
costReport.getRange('F12:F16').clearContent(); //Theoreticals
costReport.getRange('D20:D20').clearContent(); //Week Ending Date
Third sets protection
var ss = SpreadsheetApp.getActive().getSheetByName('COST REPORT');
var costReportCOGS = ss.getRange('G11:G16');
var protection = costReportCOGS.protect().setDescription('costReportCOGS');
var me = Session.getEffectiveUser();
protection.addEditor(me);
protection.removeEditors(protection.getEditors());
if (protection.canDomainEdit()) {
protection.setDomainEdit(false);
}
var costReportPurchaseEnding = ss.getRange('D11:E16');
var protection = costReportPurchaseEnding.protect().setDescription(
'costReportPurchaseEnding');
var me = Session.getEffectiveUser();
protection.addEditor(me);
protection.removeEditors(protection.getEditors());
if (protection.canDomainEdit()) {
protection.setDomainEdit(false);
}
I've cut some of the script down for ease of debugging, but basically I need the script to Execute & Finish in this order, one by one. If you just try running the script the way it is, the protection doesn't get removed and I get the error "trying to edit protected range...."
If I run each block by itself then it works perfect, but that consists of 3 different scripts the user has to run and I need it all in one.
Thanks in advance!
Sean.
Something like this?
function removeProtection() {
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('COST REPORT');
var protections = ss.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (var i = 0; i < protections.length; i++) {
var protection = protections[i];
if (protection.canEdit()) {
protection.remove();
}
}
};
function clearRangeData() {
var costReport = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(
'COST REPORT');
costReport.getRange('F12:F16').clearContent(); //Theoreticals
costReport.getRange('D20:D20').clearContent(); //Week Ending Date
};
function weeklyFileRangeProtection() {
//COST REPORT
var ss = SpreadsheetApp.getActive().getSheetByName('COST REPORT');
var costReportCOGS = ss.getRange('G11:G16');
var protection = costReportCOGS.protect().setDescription('costReportCOGS');
var me = Session.getEffectiveUser();
protection.addEditor(me);
protection.removeEditors(protection.getEditors());
if (protection.canDomainEdit()) {
protection.setDomainEdit(false);
}
};
You are having issues because for each function you are calling SpreadsheetApp.getActiveSpreadsheet . Each time you make this call you create a virtual "copy" of the spreadhseet, and the changes you make to this copy are only passed to the version in Google's servers once the whole script is finished. Hence, if you manually run each of the 3 function that the workflow:
Run function 1 -> script finished -> update the spreadsheet in the server -> run function 2 (which now gets the updated spreadsheet) -> script finished -> update the spreadsheet in the server -> run function 3 (which now gets the re-updated spreadsheet) -> script finished -> update the spreadsheet in the server
Now, if you run the three functions, the way the script is here is what happens:
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('COST REPORT'); this creates a virtual copy of the spreadsheet -> your code removes the protection from this copy and the server spreadsheet is not modified -> you call again var costReport = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('COST REPORT'); which create a new copy of the server spreadsheet, which hadn't its protections removed yet -> your code tries to clear the data on this copy, which triggers the error.
As #Cameron Roberts suggested in his answer Spreadsheet.flush() between the calls will solve the issue, because if forces the changes to be synced to the spreadsheet in the server. But you will have another "problem", which is the amount of copies you are calling, the .getActiveSpreadsheet() is very time consuming! It is better if you make only one call, store in a variable (you already do that, it is your variable ss) and make all the edits to that.
Your code will end up looking like this:
var ss = SpreadsheetApp.getActiveSpreadsheet();
var costReport = ss.getSheetByName('COST REPORT');
//First clear protection
var protections = costReport.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (var i = 0; i < protections.length; i++) {
var protection = protections[i];
if (protection.canEdit()) {
protection.remove();
};
};
//Second clears data in cells
costReport.getRange('F12:F16').clearContent(); //Theoreticals
costReport.getRange('D20:D20').clearContent(); //Week Ending Date
//Third sets protection
var costReportCOGS = costReport.getRange('G11:G16');
var protection = costReportCOGS.protect().setDescription('costReportCOGS');
var me = Session.getEffectiveUser();
protection.addEditor(me);
protection.removeEditors(protection.getEditors());
if (protection.canDomainEdit()) {
protection.setDomainEdit(false);
};
var costReportPurchaseEnding = costReport.getRange('D11:E16');
var protection = costReportPurchaseEnding.protect().setDescription(
'costReportPurchaseEnding');
var me = Session.getEffectiveUser();
protection.addEditor(me);
protection.removeEditors(protection.getEditors());
if (protection.canDomainEdit()) {
protection.setDomainEdit(false);
};
This method also applies to Google Docs, which does NOT have a similar .flush() method for updating the server version.
I believe you have misdiagnosed the issue slightly. The code is already running in the correct order, but the protection is simply not being removed before the write calls are executed, due to the nature of Google's underlying architecture.
The comments steering you towards asynchronous behaviour are not helpful in this case, they do make sense from a Javascript perspective but are not the issue here, this is an Apps Script / Google Sheets issue, none of the functions you are calling are asynchronous.
I have two suggestions, one is to try calling SpreadsheetApp.flush() after the protections are removed. The other is to use Utilities.sleep() to artificially pause the script for a brief period after executing the remove() calls.
https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet-app#flush()
https://developers.google.com/apps-script/reference/utilities/utilities#sleep(Integer)

Categories

Resources