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