1

I have a budget spreadsheet with tabs for every pay period. These tabs are created as needed and don't have names I can easily know in advance. For instance, one will be "10/15 - 10/28" because that's the pay period. Next month I create a new one with "10/29 - 11/11." I'd like to be able to sum a value across all sheets. For example, every sheet has a row named "Save," some sheets have a row named "Rent", but not every sheet will contain rows with those names and when they do they won't always be in the same cell number.

Sample sheet

I've seen some examples where there's a bunch of SUMIFs and every sheet is manually named but I'd much rather not have to do that because this sheet gets copied fairly often and the sheet names will never be the same.

=SUMIFS('Tab 1' !A1:A10, 'Tab 1'!B1:B10, "Rent")
+SUMIFS('Tab 2' !A1:A10, 'Tab 2'!B1:B10, "Rent")
+SUMIFS('Tab 3' !A1:A10, 'Tab 3'!B1:B10, "Rent")

Is this possible with either a standard formula or a script?

Sample Data

Tab 1

Tab 2

Tab 3

Desired final tab

Summaries Tab

Column 1's values are known in advance so those can be hardcoded. For instance, there will never be a random "yet more stuff" appear which I wouldn't sum up by adding a new row to the final tab.

Ginger McMurray
  • 1,275
  • 2
  • 20
  • 42
  • 1
    If you rename a tab, the function's tab name string also gets renamed automatically. – TheMaster Oct 16 '21 at 19:12
  • 1
    https://stackoverflow.com/a/60364928/ – TheMaster Oct 16 '21 at 19:19
  • Can you provide a copy of the spreadsheet you are working on, indicating the formulas used on what can change in a sheet? (name, cell values, etc.) – Iamblichus Oct 18 '21 at 07:25
  • Why create a new tab every pay period? Why not one tab for an entire year's worth of pay periods? – MattKing Oct 18 '21 at 14:52
  • @TheMaster I'm adding tabs so hardcoding them in the function doesn't work. – Ginger McMurray Oct 18 '21 at 16:04
  • @Iamblichus I edited in a link. – Ginger McMurray Oct 18 '21 at 16:04
  • @MattKing there's a ton more information on each sheet than the basic sample here. Data is divided up amongst accounts with line items for each expense, sections for investments, and more. It's a one-stop shop for all of my financial data and could never fit everything on a single tab. – Ginger McMurray Oct 18 '21 at 16:06
  • @GingerMcMurray Understood. The pros of many tabs is a "one stop shop", the con is Spreadsheets are not designed to aggregate information from dozens of tabs. – MattKing Oct 18 '21 at 16:50

2 Answers2

2

While there's another answer that works for this, I think the use of text finders and getRange, getValue and setFormula in loops is not the best approach, since it greatly increases the amount of calls to the spreadsheet service, slowing down the script (see Minimize calls to other services).

Method 1. onEdit trigger:

An option would be to use an onEdit trigger to do the following whenever a user edits the spreadsheet:

  • Loop through all sheets (excluding Totals).
  • For each sheet, loop through all data.
  • For each row, check if the category has been found previously.
  • If it has not been found, add it (and the corresponding amount) to an array storing the totals (called items in the function below).
  • If it has been found, add the current amount to the previous total.
  • Write the resulting data to Totals.

It could be something like this (check inline comments for more details):

const TOTAL_SHEET_NAME = "Totals";
const FIRST_ROW = 4;

function onEdit(e) {
  const ss = e.source;
  const targetSheet = ss.getSheetByName(TOTAL_SHEET_NAME);
  const sourceSheets = ss.getSheets().filter(sheet => sheet.getName() !== TOTAL_SHEET_NAME);
  let items = [["Category", "Amount"]];
  sourceSheets.forEach(sheet => { // Loop through all source sheets
    const values = sheet.getRange(FIRST_ROW, 1, sheet.getLastRow()-FIRST_ROW+1, 2).getValues();
    values.forEach(row => { // Loop through data in a sheet
      const [category, amount] = row;
      const item = items.find(item => item[0] === category); // Find category
      if (!item) { // If category doesn't exist, create it
        items.push([category, amount]);
      } else { // If category exists, update the amount
        item[1] += amount;
      }
    });
  });
  targetSheet.getRange(FIRST_ROW-1, 1, items.length, items[0].length).setValues(items);
}

Method 2. Custom function:

Another option would be to use an Apps Script Custom Function.

In this case, writing the data via setValues is not necessary, returning the results would be enough:

const TOTAL_SHEET_NAME = "Totals";
const FIRST_ROW = 4;

function CALCULATE_TOTALS() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sourceSheets = ss.getSheets().filter(sheet => sheet.getName() !== TOTAL_SHEET_NAME);
  let items = [["Category", "Amount"]];
  sourceSheets.forEach(sheet => { // Loop through all source sheets
    const values = sheet.getRange(FIRST_ROW, 1, sheet.getLastRow()-FIRST_ROW+1, 2).getValues();
    values.forEach(row => { // Loop through data in a sheet
      const [category, amount] = row;
      const item = items.find(item => item[0] === category); // Find category
      if (!item) { // If category doesn't exist, create it
        items.push([category, amount]);
      } else { // If category exists, update the amount
        item[1] += amount;
      }
    });
  });
  return items;
}

Once the script is saved, you can use this function the same you would use any sheets built-in function:

enter image description here

The problem with this approach is that the formula won't recalculate automatically when changing any of the source data. In order to do that, see the above method.

Method 3. onSelectionChange trigger:

From your comment:

I'd love to be able to trigger it when the totals sheet is opened but that doesn't appear to be possible

You can do this by using an onSelectionChange trigger in combination with PropertiesService.

The idea would be that, every time a user changes cell selection, the function should check whether current sheet is Totals and whether the previously active sheet is not Totals. If that's the case, this means the user just opened the Totals sheet, and the results should update.

It could be something like this:

function onSelectionChange(e) {
  const range = e.range;
  const sheet = range.getSheet();
  const sheetName = sheet.getName();
  const previousSheetName = PropertiesService.getUserProperties().getProperty("PREVIOUS_SHEET");
  if (sheetName === TOTAL_SHEET_NAME && previousSheetName !== TOTAL_SHEET_NAME) {
    updateTotals(e);
  }
  PropertiesService.getUserProperties().setProperty("PREVIOUS_SHEET", sheetName);
}

function updateTotals(e) {
  const ss = e.source;
  const targetSheet = ss.getSheetByName(TOTAL_SHEET_NAME);
  const sourceSheets = ss.getSheets().filter(sheet => sheet.getName() !== TOTAL_SHEET_NAME);
  let items = [["Category", "Amount"]];
  sourceSheets.forEach(sheet => { // Loop through all source sheets
    const values = sheet.getRange(FIRST_ROW, 1, sheet.getLastRow()-FIRST_ROW+1, 2).getValues();
    values.forEach(row => { // Loop through data in a sheet
      const [category, amount] = row;
      const item = items.find(item => item[0] === category); // Find category
      if (!item) { // If category doesn't exist, create it
        items.push([category, amount]);
      } else { // If category exists, update the amount
        item[1] += amount;
      }
    });
  });
  targetSheet.getRange(FIRST_ROW-1, 1, items.length, items[0].length).setValues(items);
}

Note: Please notice that, in order for this trigger to work, you need to refresh the spreadsheet once the trigger is added and every time the spreadsheet is opened (ref).

Reference:

Iamblichus
  • 18,540
  • 2
  • 11
  • 27
  • Thanks! Method #1 is similar to how I implemented it but I'm tying it to the button as mentioned below. I'd love to be able to trigger it when the totals sheet is opened but that doesn't appear to be possible. – Ginger McMurray Oct 21 '21 at 17:43
  • @GingerMcMurray `I'd love to be able to trigger it when the totals sheet is opened but that doesn't appear to be possible` You could use an [onSelectionChange](https://developers.google.com/apps-script/guides/triggers#onselectionchangee) that would store the current sheet and would compare that to the previously stored. If the new one is `Totals` and the old one is not, update the data. If you think that could be useful to you, I'll update my answer explaining this. – Iamblichus Oct 25 '21 at 07:05
  • @lamblichus that would be great! – Ginger McMurray Oct 28 '21 at 14:20
  • @GingerMcMurray I added a `Method 3` to my answer explaining this. I hope this is useful to you. – Iamblichus Oct 29 '21 at 08:46
  • Perfect, thanks! – Ginger McMurray Oct 31 '21 at 22:47
  • Oops! I upvoted but forgot to hit the check mark. It's accepted now. – Ginger McMurray Nov 02 '21 at 13:40
0

I wrote 2 scripts:

  1. budgetTotal which takes a budgetCategory parameter, for example "Rent", and loops through all the sheets in the file to sum up the amounts listed on each sheet for that category.
  2. budgetCreation which looks at your Totals sheet and writes these budgetTotal formulas in for each category you have listed.

I ran into a challenge which was, as I added new sheets the formulas wouldn't be aware and update the totals. So, what I did was create a simple button that executes the budgetCreation script. This way, as you add new payroll weeks you just need to press the button and - voila! - the totals update.

There might be a better way to do this using onEdit or onChange triggers but this felt like a decent starting place.

Here's a copy of the sheet with the button in place.

const ws=SpreadsheetApp.getActiveSpreadsheet()
const ss=ws.getActiveSheet()
const totals=ws.getSheetByName("Totals")

function budgetCreation(){
  var budgetStart = totals.createTextFinder("Category").findNext()
  var budgetStartRow = budgetStart.getRow()+1
  var budgetEndRow = ss.getRange(budgetStart.getA1Notation()).getDataRegion().getLastRow()
  var budgetCategoies = budgetEndRow - budgetStartRow + 1
  ss.getRange(budgetStartRow,2,budgetCategoies,1).clear()

  for (i=0; i<budgetCategoies; i++){
    var budCat = ss.getRange(budgetStartRow+i,1).getValue()
    var budFormula = `=budgetTotal(\"${budCat}\")`
    ss.getRange(budgetStartRow+i,2).setFormula(budFormula)
  }
}

function budgetTotal(budgetCategory) {
  var sheets = ws.getSheets()
  var total = 0
  for (i=0; i<sheets.length; i++){
    if (sheets[i].getName() != totals.getName()){
      var totalFinder = sheets[i].createTextFinder(budgetCategory).findNext()
      if (totalFinder == null){
        total = 0
      } else {
        var totalValueFinder = sheets[i].getRange(totalFinder.getRow(),totalFinder.getColumn()+1).getValue()
        total += totalValueFinder
      }
    }
  }
  return total
}

Dharman
  • 30,962
  • 25
  • 85
  • 135
Marcelo Guerra
  • 123
  • 1
  • 8