Skrypt Google Ads – kalkulacja budżetów od wskazanej daty na określoną liczbę dni – poziom MCK

Skrypt do wdrożenia na poziomie MCK. Możecie śledzić wydatki na x kontach Google Ads w hierarchii tego konta MCK.

Główne zadanie skryptu, to przeliczenie jaki ma być obecny budżet dzienny biorąc pod uwagę dotychczasowe wydatki od wskazanej przez Was daty i liczby dni, w których ma działać dany budżet całościowy.

Ciekawe rozwiązanie, to jeśli system wykryje dni z kosztem 0 zł, to doliczy te dni do liczby dni w jakie miał działać dany budżet, czyli jeśli początkowe ustawienie było 30 dni, a system wykrył, że były 3 dni z 0zł kosztu, to doliczy te dni do obliczeń i sumarycznie będzie wyliczał pozostały budżet dla 33 dni.

Tak wygląda przykładowy arkusz z przeliczeniami.

Poniżej znajdziecie szczegółową instrukcję.

UWAGA! Nie napisałem ani jednej linijki tego kodu. Kod powstał z użyciem ChatGPT. Skryptu używasz na własną odpowiedzialność.

// Copyright 2025. All Rights Reserved. Skrypt wygenerowany za pomocą ChatGPT.
// Kalkulacja budżetów w miesiącu kalendarzowym.
// Masz pytania? Napisz dominik@myslak.pl
//
//
// O skrypcie
// 
// Skrypt do wdrożenia na poziomie MCK (Moje Centrum Klienta).
// 
// Skrypt analizuje wydatki na wskazanych kontach Google Ads. 
// 
// GŁÓWNE DZIAŁANIE SKRYPTU: Skrypt wyliczy szacowany budżet dzienny danego konta Google Ads na pozostałe dni wskazanego okresu (daty i liczby dni).
// 
// Skrypt dodatkowo wysyła alert na Twojego maila, gdy wartość w kolumnie Budżet dzienny (pozostałe dni) jest mniejsza od wartości w kolumnie Budżet dzienny (szacowany) (o zadany procent w skrypcie) oraz, gdy wydatki z wczoraj były wyższe od wartości w kolumnie Budżet dzienny (pozostałe dni) (o zadany procent w skrypcie). 
// 
// Instrukcja
// Skopiuj cały skrypt i wklej w odpowiednie pole podczas dodawania skryptu na koncie Google Ads.
// Skopiuj adres z pola SPREADSHEET_URL i wklej w okno przeglądarki. Utwórz Swój arkusz.
// Na karcie Budżety wypełnij kolumnę Account ID oraz Monthly Budget. Skopiuj adres url Swojego szablonu pliku i wklej z powrotem do pola SPREADSHEET_URL pomiędzy ''.
// W polu ALERT_EMAIL uzupełnij email do alertów, a w polu THRESHOLD_PERCENT, YESTERDAY_COST_THRESHOLD_PERCENT i REMAINING_BUDGET_ALERT_PERCENT wartości procentowe, po przekroczeniu, których skrypt wyśle maila z alertami.
//
// Wartość procentowa w polu THRESHOLD_PERCENT to różnica pomiędzy kolumnami Budżet dzienny (szacowany) a Budżet dzienny (pozostałe dni).
//
// Wartość procentowa w polu YESTERDAY_COST_THRESHOLD_PERCENT to różnica pomiędzy kolumnami kosztów z dnia wczorajszego a Budżet dzienny (pozostałe dni).
//
// Wartość procentowa w polu REMAINING_BUDGET_ALERT_PERCENT to informacja dla skryptu, kiedy ma poinformować o zbliżającym się wyklikaniu budżetu.
//
// W dolnej części skryptu możesz zmienić tytuł otrzymywanego maila z alertami.
//
// Zapisz skrypt, autoryzuj i uruchom. Nie zapomnij ustawić harmonogramu. Zalecam codziennie, ale to od Ciebie zależy częstotliwość jego uruchamiania.
// 
// Wyniki przeliczeń pojawią się w Twoim Dokumencie Google na karcie Analiza Budżetu.
// Skrypt będzie nadpisywał poprzednie wyniki z poprzednich uruchomień skryptu.
//
// Objaśnienie kilku kolumn. 
// % budżetu (szacowany) - tyle procentowo powinniśmy wydać dziennie do tej pory, licząc Budżet miesięczny/liczbę dni w miesiącu.
// % budżetu (faktyczny) - tyle procentowo faktycznie wydaliśmy uwzględniając dotychczasowe wydatki. Tło komórek wypełnia się odcieniami czerwonego i zielonego w zależności od tego, czy wydatki są pod, czy nad normą % budżetu (szacowany).
// Budżet dzienny (szacowany) - tyle powinniśmy wydawać dziennie, licząc Budżet miesięczny/liczbę dni w miesiącu.
// Budżet dzienny (pozostałe dni) - tyle powinniśmy wydawać dziennie po uwzględnieniu dotychczasowych wydatków. Tło komórek wypełnia się kolorem czerwonym lub zielonym w zależności od tego, czy musimy zmniejszyć lub zwiększyć budżet dzienny.
// UWAGA! Nie napisałem ani jednej linijki tego skryptu. Skrypt powstał z użyciem ChatGPT. Skryptu używasz na własną odpowiedzialność.
//
////////////////////////////////////////////////////////////////////


const SPREADSHEET_URL = 'https://docs.google.com/spreadsheets/d/1-87AC42Ch-WhD_m12c0Qq0409v6t3MUfyCymQXLrKr8/copy'; // <-- Skopiuj ten link. Utwórz Swój arkusz i wklej adres do niego.
const INPUT_SHEET_NAME = 'Budżety';
const OUTPUT_SHEET_NAME = 'Analiza Budżetu';
const ALERT_EMAIL = 'twój@email.pl'; // <-- Tutaj wpisz adres e-mail do alertów

const THRESHOLD_PERCENT = 10; // <-- Tutaj wpisz próg procentowy różnicy kolumn Budżet dzienny (pozostałe dni) a Budżet dzienny (szacowany) (np. 10%)
const YESTERDAY_COST_THRESHOLD_PERCENT = 10; // <-- Tutaj wpisz próg procentowy różnicy kolumn kosztów z dnia wczorajszego a Budżet dzienny (pozostałe dni) (np. 10%)
const REMAINING_BUDGET_ALERT_PERCENT = 20; // Próg procentowy pozostałego budżetu (np. 20%)


////// Koniec konfiguracji w skrypcie. Poniżej w kodzie znajdziesz kilka sformułowań, które możesz zmienić na Swoje potrzeby, jak np. komunikaty w Alertach email - na własną odpowiedzialność.


function main() {
  const spreadsheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL);
  const inputSheet = spreadsheet.getSheetByName(INPUT_SHEET_NAME);
  const outputSheet = getOrCreateOutputSheet(spreadsheet, OUTPUT_SHEET_NAME);
  outputSheet.clearContents();

  const today = new Date();
  const timeZone = AdsApp.currentAccount().getTimeZone();
  
  const rows = inputSheet.getDataRange().getValues();
  const header = rows[0];

  const accountIdIndex = header.indexOf('Account ID');
  const budgetIndex = header.indexOf('Monthly Budget');
  const startDateIndex = header.indexOf('Start Date');
  const daysToSpendIndex = header.indexOf('Days to Spend');

  const baseHeaders = [
    'Data',
    'ID konta',
    'Nazwa konta',
    'Budżet (cały okres)',
    'Wydano dotychczas',
    '% budżetu (szacowany)',
    '% budżetu (faktyczny)',
    'Budżet dzienny (szacowany)',
    'Budżet dzienny (pozostałe dni)',
    'Zakres wydatków (szacowany)',
    'Zakres wydatków (pozostałe dni)',
    'Liczba dni okresu',
    'Minęło dni',
    'Pozostałe dni'
  ];
  
  let alertRows = [];

  // Zbieramy dane, by zbudować nagłówek po pętli
  let maxDateColumns = [];                    
  let rowObjects = [];                        

  for (let i = 1; i < rows.length; i++) {
    const accountId = rows[i][accountIdIndex];
    const totalBudget = parseFloat(rows[i][budgetIndex]);
    const startDateStr = rows[i][startDateIndex];
    const baseDaysToSpend = parseInt(rows[i][daysToSpendIndex], 10);

    if (!accountId || isNaN(totalBudget) || !startDateStr || isNaN(baseDaysToSpend)) continue;

    const startDate = new Date(startDateStr);
    let yesterday = new Date(today);
    yesterday.setDate(today.getDate() - 1);
    if (yesterday < startDate) {
      continue; 
    }

    // === Przełączamy się na konto ===
    const accountIterator = AdsManagerApp.accounts().withIds([accountId]).get();
    if (!accountIterator.hasNext()) continue;
    const account = accountIterator.next();
    MccApp.select(account);

    // === Liczymy dni z kosztami 0 zł ===
    let zeroCostDays = 0;
    for (let d = new Date(startDate); d <= yesterday; d.setDate(d.getDate() + 1)) {
      const singleDay = Utilities.formatDate(new Date(d), timeZone, 'yyyyMMdd');
      const cost = AdsApp.currentAccount().getStatsFor(singleDay, singleDay).getCost();
      if (cost === 0) zeroCostDays++;
    }

    let totalDays = baseDaysToSpend + zeroCostDays;
    let elapsedDays = Math.floor((yesterday - startDate) / (1000 * 60 * 60 * 24)) + 1;
    let remainingDays = totalDays - elapsedDays;

    // Koszty całkowite
    const dateStart = Utilities.formatDate(startDate, timeZone, 'yyyyMMdd');
    const dateEnd = Utilities.formatDate(yesterday, timeZone, 'yyyyMMdd');
    let spent = 0;
    if (elapsedDays === 1) {
      spent = AdsApp.currentAccount().getStatsFor(dateStart, dateStart).getCost();
    } else {
      spent = AdsApp.currentAccount().getStatsFor(dateStart, dateEnd).getCost();
    }

    // === Wydłużanie okresu jeśli budżet nie został jeszcze wydany ===
    if (spent < totalBudget && elapsedDays > totalDays) {
      totalDays = elapsedDays;
      remainingDays = totalDays - elapsedDays;
    }

    // Zakresy dat w prezentacji
    const estimatedEndDate = new Date(startDate);
    estimatedEndDate.setDate(startDate.getDate() + totalDays - 1);
    const displayEstimatedStart = Utilities.formatDate(startDate, timeZone, 'yyyy-MM-dd');
    const displayEstimatedEnd = Utilities.formatDate(estimatedEndDate, timeZone, 'yyyy-MM-dd');

    const displayRemainingStart = displayEstimatedStart;
    const displayRemainingEnd = Utilities.formatDate(yesterday, timeZone, 'yyyy-MM-dd');

    // Metryki budżetowe
    const estimatedDaily = totalBudget / totalDays;
    const adjustedDaily = remainingDays > 0 ? (totalBudget - spent) / remainingDays : 0;
    const accountName = account.getName();

    const percentBudgetExpected = ((estimatedDaily * elapsedDays) / totalBudget) * 100;
    const percentBudgetActual = (spent / totalBudget) * 100;
    const remainingBudgetPercent = ((totalBudget - spent) / totalBudget) * 100;

    // Koszty dzienne
    const costMap = {};         
    const dateColumns = [];     
    let costYesterday = 0;

    for (let d = new Date(yesterday); d >= startDate; d.setDate(d.getDate() - 1)) {
      const displayDate = Utilities.formatDate(new Date(d), timeZone, 'yyyy-MM-dd');
      const singleDay = Utilities.formatDate(new Date(d), timeZone, 'yyyyMMdd');
      const cost = AdsApp.currentAccount().getStatsFor(singleDay, singleDay).getCost();
      if (singleDay === dateEnd) costYesterday = cost;

      costMap[displayDate] = cost.toFixed(2);
      dateColumns.push(displayDate);
    }

    if (dateColumns.length > maxDateColumns.length) {
      maxDateColumns = dateColumns.slice(); 
    }

    const baseRow = [
      Utilities.formatDate(today, timeZone, 'yyyy-MM-dd'),
      accountId,
      accountName,
      totalBudget,
      spent.toFixed(2),
      percentBudgetExpected.toFixed(2) + '%',
      percentBudgetActual.toFixed(2) + '%',
      estimatedDaily.toFixed(2),
      adjustedDaily.toFixed(2),
      `${displayEstimatedStart} – ${displayEstimatedEnd}`,
      `${displayRemainingStart} – ${displayRemainingEnd}`,
      totalDays,
      elapsedDays,
      remainingDays
    ];

    rowObjects.push({ base: baseRow, costMap: costMap });

    // === ALERTY ===
    if (adjustedDaily < estimatedDaily) {
      const percentDiff = ((estimatedDaily - adjustedDaily) / estimatedDaily) * 100;
      if (percentDiff >= THRESHOLD_PERCENT) {
        alertRows.push(makeAlertRow(accountId, accountName, totalBudget, spent, estimatedDaily, adjustedDaily,
          displayEstimatedStart, displayEstimatedEnd, displayRemainingEnd, totalDays, elapsedDays, remainingDays, costYesterday,
          `Pozostały budżet dzienny jest niższy o ${percentDiff.toFixed(2)}%`));
      }
    }

    if (costYesterday > adjustedDaily) {
      const percentDiff = ((costYesterday - adjustedDaily) / adjustedDaily) * 100;
      if (percentDiff >= YESTERDAY_COST_THRESHOLD_PERCENT) {
        alertRows.push(makeAlertRow(accountId, accountName, totalBudget, spent, estimatedDaily, adjustedDaily,
          displayEstimatedStart, displayEstimatedEnd, displayRemainingEnd, totalDays, elapsedDays, remainingDays, costYesterday,
          'Koszt z wczoraj przekroczył budżet dzienny'));
      }
    }

    if (remainingBudgetPercent < REMAINING_BUDGET_ALERT_PERCENT) {
      alertRows.push(makeAlertRow(accountId, accountName, totalBudget, spent, estimatedDaily, adjustedDaily,
        displayEstimatedStart, displayEstimatedEnd, displayRemainingEnd, totalDays, elapsedDays, remainingDays, costYesterday,
        `Pozostało mniej niż ${REMAINING_BUDGET_ALERT_PERCENT}% budżetu`));
    }

    if (spent > totalBudget) {
      alertRows.push(makeAlertRow(accountId, accountName, totalBudget, spent, estimatedDaily, adjustedDaily,
        displayEstimatedStart, displayEstimatedEnd, displayRemainingEnd, totalDays, elapsedDays, remainingDays, costYesterday,
        'Przekroczono budżet całkowity'));
    }

    // === Alert 5: Koszty z wczoraj = 0 zł ===
    if (costYesterday === 0) {
      alertRows.push(makeAlertRow(accountId, accountName, totalBudget, spent, estimatedDaily, adjustedDaily,
        displayEstimatedStart, displayEstimatedEnd, displayRemainingEnd, totalDays, elapsedDays, remainingDays, costYesterday,
        'Koszty z dnia wczorajszego wyniosły 0 zł'));
    }
  }

  if (rowObjects.length === 0) {
    outputSheet.appendRow(baseHeaders);
    return;
  }

  outputSheet.appendRow(baseHeaders.concat(maxDateColumns));

  const fullWidth = baseHeaders.length + maxDateColumns.length;
  const allDataRows = rowObjects.map(ro => {
    const costArray = maxDateColumns.map(date => (date in ro.costMap ? ro.costMap[date] : ''));
    const row = ro.base.concat(costArray);
    if (row.length < fullWidth) {
      const missing = new Array(fullWidth - row.length).fill('');
      return row.concat(missing);
    } else if (row.length > fullWidth) {
      return row.slice(0, fullWidth);
    }
    return row;
  });

  outputSheet
    .getRange(2, 1, allDataRows.length, fullWidth)
    .setValues(allDataRows);

  for (let r = 0; r < allDataRows.length; r++) {
    const rowNum = r + 2; 

    const adjustedDailyCell = outputSheet.getRange(rowNum, 9);
    const estDailyCell = outputSheet.getRange(rowNum, 8);
    const adjustedDailyVal = parseFloat(adjustedDailyCell.getValue());
    const estDailyVal = parseFloat(estDailyCell.getValue());
    if (!isNaN(adjustedDailyVal) && !isNaN(estDailyVal)) {
      if (adjustedDailyVal < estDailyVal) {
        adjustedDailyCell.setBackground('#f8d7da'); 
      } else {
        adjustedDailyCell.setBackground('#d4edda'); 
      }
    }

    const percentExpectedCell = outputSheet.getRange(rowNum, 6);
    const percentActualCell = outputSheet.getRange(rowNum, 7);
    const valExpected = parseFloat(percentExpectedCell.getValue());
    const valActual = parseFloat(percentActualCell.getValue());
    if (!isNaN(valExpected) && !isNaN(valActual)) {
      let color = '';
      const diffPercent = valActual - valExpected;
      if (diffPercent > 0) {
        if (diffPercent >= 30) color = '#a94442';
        else if (diffPercent >= 20) color = '#c9302c';
        else color = '#f2dede';
      } else {
        const diff = Math.abs(diffPercent);
        if (diff >= 30) color = '#2b542c';
        else if (diff >= 20) color = '#449d44';
        else color = '#dff0d8';
      }
      percentActualCell.setBackground(color);
    }
  }

  if (alertRows.length > 0) {
    sendEmailAlert(alertRows);
  }
}

function makeAlertRow(id, name, monthly, spent, estimated, remaining, estStart, estEnd, remEnd, totalDays, elapsedDays, remainingDays, yesterday, alertType) {
  return {
    id: id,
    name: name,
    monthly: monthly.toFixed(2),
    spent: spent.toFixed(2),
    estimated: estimated.toFixed(2),
    remaining: remaining.toFixed(2),
    rangeEstimated: `${estStart} – ${estEnd}`,
    rangeRemaining: `${estStart} – ${remEnd}`,
    totalDays: totalDays,
    elapsedDays: elapsedDays,
    remainingDays: remainingDays,
    yesterday: yesterday.toFixed(2),
    alertType: alertType
  };
}

function getOrCreateOutputSheet(spreadsheet, name) {
  let sheet = spreadsheet.getSheetByName(name);
  if (!sheet) {
    sheet = spreadsheet.insertSheet(name);
  }
  return sheet;
}

function sendEmailAlert(rows) {
  let html = `
    <p><strong>Alerty budżetowe</strong></p>
    <table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse;">
      <tr>
        <th>ID konta</th>
        <th>Nazwa konta</th>
        <th>Budżet (cały okres)</th>
        <th>Wydano dotychczas</th>
        <th>Budżet dzienny (szacowany)</th>
        <th>Budżet dzienny (pozostałe dni)</th>
        <th>Zakres wydatków (szacowany)</th>
        <th>Zakres wydatków (pozostałe dni)</th>
        <th>Liczba dni okresu</th>
        <th>Minęło dni</th>
        <th>Pozostałe dni</th>
        <th>Koszt z dnia wczorajszego</th>
        <th>Typ alertu</th>
      </tr>
  `;

  rows.forEach(r => {
    html += `
      <tr>
        <td>${r.id}</td>
        <td>${r.name}</td>
        <td>${r.monthly} zł</td>
        <td>${r.spent} zł</td>
        <td>${r.estimated} zł</td>
        <td>${r.remaining} zł</td>
        <td>${r.rangeEstimated}</td>
        <td>${r.rangeRemaining}</td>
        <td>${r.totalDays}</td>
        <td>${r.elapsedDays}</td>
        <td>${r.remainingDays}</td>
        <td>${r.yesterday} zł</td>
        <td>${r.alertType}</td>
      </tr>
    `;
  });

  html += `</table>
    <p>Pełny raport w arkuszu: <a href="${SPREADSHEET_URL}" target="_blank">Otwórz arkusz</a></p>
  `;

  MailApp.sendEmail({
    to: ALERT_EMAIL,
    subject: 'Alerty budżetowe – Google Ads',
    htmlBody: html
  });
}