Správa rozpočtů napříč Google Ads, Sklikem a Facebookem je časově náročná, zvláště pokud spravujete více účtů. V tomto článku ukážu, jak jsem postupně vylepšil původní „MCC Skript na kontrolu rozpočtů“ (autor: Stanislav Jílek), až vznikla verze zaměřená na YTD (Year-To-Date) porovnání letošního roku a loňska, bez zahrnutí app konverzí, a přizpůsobená pro ranní běh „do včerejška“. Použití chatGPT na scriptování dostává nový rozměr, protože neumím scriptovat.
1. Z čeho jsme vycházeli
Původní skript nabízel:
-
Centralizovanou konfiguraci přes Google Sheets (včetně e-mailu, rozpočtu),
-
Kontrolu Google Ads, Sklik API „Drak“ a Facebook,
-
Měřil procento času v měsíci, využití rozpočtu a doporučený denní budget, vše pro aktuální a předchozí období (např. měsíc, včera),
-
V roce 2025 rozšířen o komunikaci s Facebook budgetem a API Drak.
2. Kam jsem to dotáhl: rychlý YTD přehled
Po iteracích a optimalizacích vznikla zrychlená verze s několika klíčovými vylepšeními:
|
Funkce |
Původní skript |
Rychlá YTD verze |
|---|---|---|
|
Období |
více časových segmentů (měsíce, včera) |
„Letošek k včerejšku“ a „Loňsko k včerejšku“ |
|
Výkon |
Sleep mezi API voláními → dlouhé běhy |
Žádné sleepy, přímý agregát |
|
|
detaillní data |
agregát jedním výstupem |
3. Přínosy téhle verze
-
Rychlejší běh skriptu — žádné zbytečné pauzy mezi API dotazy.
-
Srozumitelnost — porovnání YTD let vs. YTD loňska zachycuje trendy.
-
Flexibilita — můžeš snadno modulárně rozdělit platformy (GA, Sklik, FB) a případně rozšířit o souhrnné řádky.
4. Jak implementovat
-
Kopie config spreadsheetu – jako ve scriptu od Standy, vlož URL do ss_config.
-
Nastav tokeny pro Sklik (API Drak) a Facebook (Graph API token + účet).
-
Vlož hlavní funkci main(), která obsahuje zrychlený YTD výpočet:
-
Výpočet dat „do včerejška“
-
Agregace Sklik a Facebook dat
-
Odeslání formátovaného HTML e-mailu
-
-
Naplánuj spouštění — ideálně ráno, po dokončení datového zpracování.
- Pokud chcete takto sledovat pouze nějaké účty, tak si list budget_control zduplikujte a nechte pouze účty, které vám dávají smysl, změna názvu listu je v části ss_config.getSheetByName
5. Co skript neřeší
-
Detaily kampaní / sestav / reklam — jde hlavně o účetní přehled.
-
Alerty — nehlásí anomálie nebo nutnost úpravy rozpočtu (může se doplnit).
- SKLIK zboží – tyto data nejsou v API a nejsou ani v původním scriptu od Standy
-
Historické trendy — YTD je užitečné porovnání, ale trendový report je zase jiné narativní vyprávění.
6. Závěr
Automatizace hlídání rozpočtů je RTL (real-time labor-saving). Tato verze pomáhá rychle porovnat hlavní metriky mezi jednotlivými roky. Slouží primárně k trackování celého roku.
Celý upravený script: nic složitého, ale přišlo mi fajn se podělit.
// FAST YTD kontrola (GA, Sklik, Facebook) — do včerejška
var ss_config = SpreadsheetApp.openByUrl("URL souboru s daty");
/*********************************************************************************************************
Skript: MCC Skript na kontrolu rozpočtů pro Google Ads, Sklik a Facebook
Verze: Budget control 09.04.2025
Vytvořil: Stanislav Jílek [standajilek.cz]
Navrhli a testovali: Karel Rujzl [rujzl.cz] a Petra Větrovská [vetrovka.cz]
Prvotní myšlenka na kontrolu rozpočtů: Hana Kobzová [hanakobzova.cz]
EDIT na YTD: Tomáš Bzirský [bzirsky.cz]
/********************************************************************************************************/
function main() {
var settings_sheet = ss_config.getSheetByName("budget_control_year");
var mail = settings_sheet.getRange("B2").getValue();
var subject = settings_sheet.getRange("B3").getValue();
// --- Datum: do včerejška, časová zóna účtu (fallback Praha) ---
var TZ = (typeof AdsApp !== "undefined" && AdsApp.currentAccount && AdsApp.currentAccount().getTimeZone)
? (AdsApp.currentAccount().getTimeZone() || "Europe/Prague")
: "Europe/Prague";
var now = new Date();
now.setDate(now.getDate() - 1); // včera
var thisYear = now.getFullYear();
var lastYear = thisYear - 1;
var startTY = new Date(thisYear, 0, 1);
var endTY = new Date(thisYear, now.getMonth(), now.getDate());
var startLY = new Date(lastYear, 0, 1);
var endLY = new Date(lastYear, now.getMonth(), now.getDate());
var periods = [
[startTY, endTY, "Letošek (YTD TY)"],
[startLY, endLY, "Loňský rok (YTD LY)"]
];
// --- Tabulka ---
var table_header = "<tr bgcolor='#ffd75d'><th>Účet</th><th>Období</th><th>Náklady</th><th>Obrat</th><th>Konverze</th><th>CPA</th><th>PNO</th></tr>";
var table = "<table border='1' style='border-collapse:collapse' cellpadding='5'>";
// ===== GOOGLE ADS =====
try {
var lastRow = settings_sheet.getLastRow();
var adwords_settings = lastRow >= 6 ? settings_sheet.getRange("D6:D" + lastRow).getValues() : [];
if (settings_sheet.getRange("D6").getValue() != "") {
table += "<tr><td colspan='7' bgcolor='#4fabe5'><strong>GOOGLE ADS</strong></td></tr>" + table_header;
}
for (var i = 0; i < adwords_settings.length; i++) {
var accId = adwords_settings[i][0];
if (!accId) continue;
try {
var it = MccApp.accounts().withIds([accId]).get();
if (it.hasNext()) MccApp.select(it.next());
var account_name = AdsApp.currentAccount().getName();
var currency = AdsApp.currentAccount().getCurrencyCode();
for (var p = 0; p < periods.length; p++) {
var ds = Utilities.formatDate(periods[p][0], TZ, 'yyyyMMdd');
var de = Utilities.formatDate(periods[p][1], TZ, 'yyyyMMdd');
var rows = AdsApp.report(
"SELECT Cost, ConversionValue, Conversions " +
"FROM ACCOUNT_PERFORMANCE_REPORT DURING " + ds + "," + de
).rows();
var cost = 0, convValue = 0, conv = 0;
if (rows.hasNext()) {
var r = rows.next();
cost = toFixed0(r.Cost);
convValue = toFixed0(r.ConversionValue);
conv = toFixed0(r.Conversions);
}
var pno = pctOrZero(cost, convValue);
var cpa = divOrZero(cost, conv);
table += add_html_simple(i, p, periods[p][2], account_name, currency, cost, convValue, conv, cpa, pno);
}
} catch (e) {
Logger.log("GA ERR: " + e);
}
}
} catch (e) {
Logger.log("GA BLOCK ERR: " + e);
}
// ===== SKLIK =====
try {
var sklik_settings = lastRow >= 6 ? settings_sheet.getRange("A6:A" + lastRow).getValues() : [];
if (settings_sheet.getRange("A6").getValue() != "") {
table += "<tr><td colspan='7' bgcolor='#ff4646'><strong>SKLIK</strong></td></tr>" + table_header;
}
var token = settings_sheet.getRange("B4").getValue();
var client_login = sklik_api([token], 'client.loginByToken');
var client_get = sklik_api([{ session: client_login.session }], 'client.get');
// map uživatelů
var userMap = {};
userMap[client_get.user.username.toLowerCase()] = [client_get.user.userId, client_get.user.username];
if (client_get.foreignAccounts && client_get.foreignAccounts.length) {
for (var fa = 0; fa < client_get.foreignAccounts.length; fa++) {
userMap[client_get.foreignAccounts[fa].username.toLowerCase()] = [client_get.foreignAccounts[fa].userId, client_get.foreignAccounts[fa].username];
}
}
for (var s = 0; s < sklik_settings.length; s++) {
var uname = (sklik_settings[s][0] || "").toLowerCase();
if (!uname || !userMap[uname]) continue;
var uid = userMap[uname][0];
var accountName = userMap[uname][1];
for (var p2 = 0; p2 < periods.length; p2++) {
var ds2 = Utilities.formatDate(periods[p2][0], TZ, 'yyyy-MM-dd');
var de2 = Utilities.formatDate(periods[p2][1], TZ, 'yyyy-MM-dd');
var stats = sklik_api([
{ session: client_login.session, userId: uid },
{ dateFrom: ds2, dateTo: de2, granularity: 'total' }
], 'client.stats');
var cost2 = 0, convVal2 = 0, conv2 = 0;
if (stats && stats.report && stats.report.length) {
cost2 = toFixed0(stats.report[0].price / 100);
convVal2 = toFixed0(stats.report[0].conversionValue / 100);
conv2 = toFixed0(stats.report[0].conversions);
}
var pno2 = pctOrZero(cost2, convVal2);
var cpa2 = divOrZero(cost2, conv2);
table += add_html_simple(s, p2, periods[p2][2], accountName, "CZK", cost2, convVal2, conv2, cpa2, pno2);
}
}
sklik_api([{ session: client_login.session }], 'client.logout');
} catch (e) {
Logger.log("SKLIK ERR: " + e);
}
// ===== FACEBOOK =====
try {
var facebook_settings = lastRow >= 6 ? settings_sheet.getRange("G6:H" + lastRow).getValues() : [];
if (settings_sheet.getRange("G6").getValue() != "") {
table += "<tr><td colspan='7' bgcolor='#3b5998'><strong>FACEBOOK</strong></td></tr>" + table_header;
}
var FB_API = 'v22.0';
for (var f = 0; f < facebook_settings.length; f++) {
var tokenFB = facebook_settings[f][0];
var accFB = facebook_settings[f][1];
if (!tokenFB || !accFB) continue;
for (var p3 = 0; p3 < periods.length; p3++) {
var ds3 = Utilities.formatDate(periods[p3][0], TZ, 'yyyy-MM-dd');
var de3 = Utilities.formatDate(periods[p3][1], TZ, 'yyyy-MM-dd');
// požádáme o agregát za celé období (time_increment=all_days) + jen potřebná pole
var url = "/" + FB_API + "/act_" + accFB + "/insights?" +
"fields=account_name,account_currency,spend,actions,action_values" +
"&level=account" +
"&time_range[since]=" + ds3 + "&time_range[until]=" + de3 +
"&time_increment=all_days" +
"&filtering=" + encodeURIComponent('[{"field":"action_type","operator":"IN","value":["offsite_conversion.fb_pixel_purchase"]}]') +
"&access_token=" + encodeURIComponent(tokenFB);
var resp = fb_api(url);
var hasData = resp && resp.data && resp.data.length;
var accNameFB = hasData ? resp.data[0].account_name : ("act_" + accFB);
var currFB = hasData ? resp.data[0].account_currency : "CZK";
var costFB = hasData ? toFixed0(parseFloat(resp.data[0].spend)) : 0;
var convFB = 0, convValFB = 0;
if (hasData && resp.data[0].actions) {
for (var a = 0; a < resp.data[0].actions.length; a++) {
var ia = resp.data[0].actions[a];
if (ia.action_type == "offsite_conversion.fb_pixel_purchase") { convFB = toFixed0(parseFloat(ia.value)); break; }
}
}
if (hasData && resp.data[0].action_values) {
for (var v = 0; v < resp.data[0].action_values.length; v++) {
var iv = resp.data[0].action_values[v];
if (iv.action_type == "offsite_conversion.fb_pixel_purchase") { convValFB = toFixed0(parseFloat(iv.value)); break; }
}
}
var pnoFB = pctOrZero(costFB, convValFB);
var cpaFB = divOrZero(costFB, convFB);
table += add_html_simple(f, p3, periods[p3][2], accNameFB, currFB, costFB, convValFB, convFB, cpaFB, pnoFB);
}
}
} catch (e) {
Logger.log("FB ERR: " + e);
}
table += "</table>";
MailApp.sendEmail({ to: mail, subject: subject, htmlBody: table });
}
/*** UTILITIES ***/
function toFixed0(val) {
var num = parseFloat(String(val).split(",").join(""));
if (isNaN(num)) return 0;
return Number(num.toFixed(0));
}
function pctOrZero(cost, value) {
if (value == 0) return 0;
return Number(((cost / value) * 100).toFixed(2));
}
function divOrZero(a, b) {
if (b == 0) return 0;
return Number((a / b).toFixed(0));
}
function number_format(number) {
number = number.toString();
number = number.split("").reverse().join("");
number = number.substr(0, 3) + " " + number.substr(3, 3) + " " + number.substr(6, 3) + " " + number.substr(9, 3) + " " + number.substr(12, 3);
number = number.split("").reverse().join("");
return number.trim();
}
function row_color(row) {
return (row % 2 == 0) ? "#ffffff" : "#d5d5d5";
}
function add_html_simple(i, j, label, account_name, currency, cost, convVal, conv, cpa, pno) {
var tr1 = "<tr bgcolor='" + row_color(i) + "'>";
if (j == 0) {
return tr1 +
"<td nowrap rowspan='2'><strong>" + account_name + "</strong></td>" +
"<td nowrap><strong>" + label + "</strong></td>" +
"<td nowrap align='right'><strong>" + number_format(cost) + " " + currency + "</strong></td>" +
"<td nowrap align='right'><strong>" + number_format(convVal) + " " + currency + "</strong></td>" +
"<td nowrap align='right'><strong>" + number_format(conv) + "</strong></td>" +
"<td nowrap align='right'><strong>" + number_format(cpa) + " " + currency + "</strong></td>" +
"<td nowrap align='right'><strong>" + pno + " %</strong></td></tr>";
} else {
return tr1 +
"<td nowrap>" + label + "</td>" +
"<td nowrap align='right'>" + number_format(cost) + " " + currency + "</td>" +
"<td nowrap align='right'>" + number_format(convVal) + " " + currency + "</td>" +
"<td nowrap align='right'>" + number_format(conv) + "</td>" +
"<td nowrap align='right'>" + number_format(cpa) + " " + currency + "</td>" +
"<td nowrap align='right'>" + pno + " %</td></tr>";
}
}
function sklik_api(parameters, method) {
var url = 'https://api.sklik.cz/drak/json/' + method;
var options = { method: 'post', contentType: 'application/json', muteHttpExceptions: true, payload: JSON.stringify(parameters) };
try { return JSON.parse(UrlFetchApp.fetch(url, options)); }
catch (e1) { try { return JSON.parse(UrlFetchApp.fetch(url, options)); } catch (e2) { return JSON.parse(UrlFetchApp.fetch(url, options)); } }
}
function fb_api(path) {
var url = 'https://graph.facebook.com' + path;
var options = { method: 'get', contentType: 'application/json', muteHttpExceptions: true };
try { return JSON.parse(UrlFetchApp.fetch(url, options)); }
catch (e1) { try { return JSON.parse(UrlFetchApp.fetch(url, options)); } catch (e2) { return JSON.parse(UrlFetchApp.fetch(url, options)); } }
}
