Google Sheets - автоматизируем отчетность для маркетплейсов: бартер, раздачи, коммерция
Многие до сих пор ведут свою отчетность в Гугл таблицах - и это окей.
Так давайте автоматизируем таблицы, да ещё и так, чтоб сотрудники сломать ничего не смогли. Настроим передачу данных в таблицу руководителя, сделаем автоматический подсчет план/факта и будем отправлять эту отчетность каждый день в ТГ.
Так давайте автоматизируем таблицы, да ещё и так, чтоб сотрудники сломать ничего не смогли. Настроим передачу данных в таблицу руководителя, сделаем автоматический подсчет план/факта и будем отправлять эту отчетность каждый день в ТГ.
Вот и у одного моего клиента-селлера на маркетплейсах была такая же ситуация: хотелось держать руку на пульсе, да так чтоб просто каждый день в Телеграм прилетал результат работы отделов за вчера и общее саммари по месяцу в разрезе отдела.
Я пораскинул головой и понял, что не справлюсь в одиночку, но не зря же я облачаю ChatGPT каждый месяц, пошел за помощью туда.
Дальше расскажу, как делал, что получилось и что это дало.
Меня зовут Никита - я директор по маркетингу на удаленке: выстраиваю аналитику, стратегии, команды и привожу их к результату, заходите в мой телеграм, чтобы быть на связи: t.me/svestudio
Шаг 1 - подготовка таблиц
1. В первую очередь я пошёл смотреть таблицы, которые ведут мои подчиненные: выделил все действительно необходимые данные, убрав ненужные.
2. Создал универсальную таблицу отчетности для всех сотрудников, далее, в зависимости от подразделения, скрыл ненужные столбцы, чтобы не мешали
3. Поставил несколько формул с защитой диапазона, чтобы сотрудники не могли их сломать.
Например, присвоение порядкового номера, автозаполнение имени сотрудника, автоматическое проставление кол-ва интеграций и т.д
Например, присвоение порядкового номера, автозаполнение имени сотрудника, автоматическое проставление кол-ва интеграций и т.д
4. Создал базу данных для раскрывающихся списков на отдельной вкладке
5. Создал таблицу руководителя, добавив туда листы с подразделениями, которые полностью соответствовали листам сотрудников.

Ну вот так она в тестовом формате выглядела - там складочки с направлениями и планы, но о них чуть позже
Что я по итогу получил:
подготовил фундамент для будущей автоматизации, благодаря чему у меня во всех таблицах "стоимость" - всегда находить в столбце "О", так же как и все остальные значения, всегда находятся на своих местах, что позволяет создать логику.
подготовил фундамент для будущей автоматизации, благодаря чему у меня во всех таблицах "стоимость" - всегда находить в столбце "О", так же как и все остальные значения, всегда находятся на своих местах, что позволяет создать логику.
Шаг 2 - перенос данных с таблиц подчиненных
Самое оптимальное решение - сделать с помощью Google apps script (откройте таблицу и выберите из пунктов меню: Расширения, далее перейдите в Apps Script).
Да там все на JavaScript надо делать, навыки в разработке отсутствуют, но нейроночки нас выручат, идем в ChatGPT - описываем задачу и получаем первую часть кода:
// Массивы ID таблиц для каждого листа
const BARTER_FILE_IDS = ['ID таблицы из адресной строки', 'ID таблицы из адресной строки'];
const COMMERCIAL_FILE_IDS = ['ID таблицы из адресной строки'];
const DISTRIBUTION_FILE_IDS = ['ID таблицы из адресной строки', 'ID таблицы из адресной строки', 'ID таблицы из адресной строки'];
// ID вашей таблицы руководителя
const MANAGER_SHEET_ID = 'ID таблицы из адресной строки';
// Карта листов с соответствующими ID таблиц
const SHEETS_MAP = {
'Бартер': BARTER_FILE_IDS,
'Коммерция': COMMERCIAL_FILE_IDS,
'Раздачи': DISTRIBUTION_FILE_IDS
};
Это просто дорожная карта для скрипта, в ней указываются ID таблицы руководителя и ID таблиц сотрудников ответственных за направление.
Карту сделали, пойдём дальше и говорим о том, что хотим данные получать со всех этих таблиц в таблицу руководителя. И вот перед нами рождается первая функция:
Карту сделали, пойдём дальше и говорим о том, что хотим данные получать со всех этих таблиц в таблицу руководителя. И вот перед нами рождается первая функция:
// Функция для обновления данных
function updateManagerSheet() {
const managerSpreadsheet = SpreadsheetApp.openById(MANAGER_SHEET_ID);
for (let sheetName in SHEETS_MAP) {
const sourceFileIds = SHEETS_MAP[sheetName];
const targetSheet = managerSpreadsheet.getSheetByName(sheetName);
if (!targetSheet) {
console.log('Лист ${sheetName} не найден в таблице руководителя');
continue;
}
const targetData = targetSheet.getDataRange().getValues();
const targetDict = {};
// Создаем словарь для существующих записей на основе уникального ключа в первом столбце
targetData.forEach((row, index) => {
const uniqueKey = row[0]; // Первый столбец — уникальный ключ
if (uniqueKey) {
targetDict[uniqueKey] = index + 1; // rowIndex для обновления данных
}
});
sourceFileIds.forEach(fileId => {
const sourceSpreadsheet = SpreadsheetApp.openById(fileId);
const sourceSheet = sourceSpreadsheet.getSheets()[0];
const sourceData = sourceSheet.getDataRange().getValues();
for (let i = 1; i < sourceData.length; i++) { // Пропускаем заголовок
const sourceRow = sourceData[i];
// Проверяем, что в 40-м столбце (AN) есть значение
if (sourceRow[39] !== "" && sourceRow[39] !== null) {
const uniqueKey = `${fileId}_${sourceRow[0]}_${sourceRow[3]}`; // Уникальный ключ на основе файла и A, D столбцов
if (targetDict[uniqueKey]) {
try {
const rowIndex = targetDict[uniqueKey];
// Проверяем существование строки
if (rowIndex > targetSheet.getLastRow()) {
Logger.log(`Ошибка: строки с индексом ${rowIndex} нет в таблице.`);
continue;
}
// Очистка данных перед записью
const cleanedRow = sourceRow.map(value => {
if (value instanceof Date) {
// Форматируем дату в дд.мм.гггг
return Utilities.formatDate(value, Session.getScriptTimeZone(), 'dd.MM.yyyy');
}
return value !== null && value !== undefined ? value.toString() : '';
});
// Проверяем, хватает ли столбцов
const currentCols = targetSheet.getLastColumn();
const neededCols = cleanedRow.length;
if (currentCols < neededCols + 1) { // +1 для учета уникального ключа
targetSheet.insertColumnsAfter(currentCols, neededCols - currentCols);
}
// Обновляем строку
Logger.log(`Обновляем строку: ключ=${uniqueKey}, строка=${rowIndex}, столбцы=${cleanedRow.length}`);
targetSheet.getRange(rowIndex, 2, 1, cleanedRow.length).setValues([cleanedRow]);
} catch (e) {
Logger.log(`Ошибка при обновлении строки с ключом ${uniqueKey}: ${e.message}`);
}
} else {
try {
// Очистка данных перед добавлением
const cleanedRow = sourceRow.map(value => {
if (value instanceof Date) {
// Форматируем дату в дд.мм.гггг
return Utilities.formatDate(value, Session.getScriptTimeZone(), 'dd.MM.yyyy');
}
return value !== null && value !== undefined ? value.toString() : '';
});
// Логируем добавление новой строки
Logger.log(`Добавляем новую строку для ключа: ${uniqueKey}`);
// Добавляем новую строку
targetSheet.appendRow([uniqueKey, ...cleanedRow]); // Уникальный ключ в первый столбец
} catch (e) {
Logger.log(`Ошибка при добавлении строки с ключом ${uniqueKey}: ${e.message}`);
}
}
}
}
});
}
}
Да, это все мой нейро-помощник написал. Что она делает - проходится по таблицам сотрудников, если в 40 столбце "AN" - указан статус, то это запись переносит в таблицу руководителя и распределяется по нужным листам, которые я указал в самом начале, дополнительно создавая уникальный ключ для этой записи, чтобы уже существующие записи не дублировались.
Готово, осталось только триггер установить, чтобы раз в час функция автоматически запускалась. Делается это там же где и код вставляется, там есть вкладка "триггеры" - выберите функцию "updateManagerSheet" и установите триггер по времени.
Я подумал, что было бы неплохо сделать еще и автоматический подсчёт фактических значений. Для этого понадобились листы с планом, т.к. мало подписать просто факт - он ещё и должен отталкиваться от плана.
Шаг 3 - создание плана для сотрудников

Вот так выглядит постановка плана, все очень просто.
Проставляем артикул, тип интеграции и плановые показатели.
% выполнения считается по формуле, а факт вносится автоматически, дальше расскажу как это сделать.
Вот мы создали табличку с планом и установили его. Что делать дальше?
Настроим проверку по Листам, суммирования показателей на основе типа интеграции и артикула.
Снова идем за помощью к нейро - помощнику вот что получаем:
Проставляем артикул, тип интеграции и плановые показатели.
% выполнения считается по формуле, а факт вносится автоматически, дальше расскажу как это сделать.
Вот мы создали табличку с планом и установили его. Что делать дальше?
Настроим проверку по Листам, суммирования показателей на основе типа интеграции и артикула.
Снова идем за помощью к нейро - помощнику вот что получаем:
// Функция обновления фактов в планах
function updatePlanFacts() {
const spreadsheet = SpreadsheetApp.openById(MANAGER_SHEET_ID);
// Русские названия месяцев
const months = [
'январь', 'февраль', 'март', 'апрель', 'май', 'июнь',
'июль', 'август', 'сентябрь', 'октябрь', 'ноябрь', 'декабрь'
];
const integrationSheets = {
'Бартер': spreadsheet.getSheetByName('Бартер'),
'Коммерция': spreadsheet.getSheetByName('Коммерция'),
'Раздачи': spreadsheet.getSheetByName('Раздачи')
};
// Проверяем наличие листов
for (let type in integrationSheets) {
if (!integrationSheets[type]) {
console.log(`Лист "${type}" для интеграций не найден.`);
return;
}
}
// Кэш для планов, чтобы собирать суммы перед записью
const planUpdates = {};
// Читаем данные из каждого листа интеграции
for (let type in integrationSheets) {
const data = integrationSheets[type].getDataRange().getValues();
for (let i = 1; i < data.length; i++) { // Пропускаем заголовок
const purchaseDateRaw = data[i][2]; // Дата закупки (столбец C)
const releaseDateRaw = data[i][3]; // Дата выхода (столбец D)
const article = data[i][5]; // Рекламируемый артикул (столбец F)
const itemName = data[i][6]; // Наименование товара (столбец G)
const quantity = Number(data[i][8]) || 0; // Количество (столбец I)
if (!article || !itemName || quantity <= 0) continue;
// Определяем целевую дату на основе типа листа
let targetDate;
if (type === 'Бартер' || type === 'Раздачи') {
// Для "Бартер" и "Раздачи" всегда используем столбец C
targetDate = new Date(purchaseDateRaw);
} else {
// Для "Коммерция" оставляем прежнюю логику
targetDate = releaseDateRaw ? new Date(releaseDateRaw) : new Date(purchaseDateRaw);
}
// Проверяем корректность даты
if (!targetDate || isNaN(targetDate.getTime())) {
console.log(`Некорректная дата для строки ${i + 1} на листе "${type}": Дата закупки=${purchaseDateRaw}, Дата выхода=${releaseDateRaw}`);
continue;
}
const targetMonth = targetDate.getMonth();
const targetYear = targetDate.getFullYear();
const planSheetName = `План ${months[targetMonth]} ${targetYear}`;
if (!planUpdates[planSheetName]) {
planUpdates[planSheetName] = {};
}
// Суммируем значения в кэше
const key = `${article}_${itemName}_${type}`;
if (!planUpdates[planSheetName][key]) {
planUpdates[planSheetName][key] = 0;
}
planUpdates[planSheetName][key] += quantity;
}
}
// Записываем суммы из кэша в соответствующие листы "План"
for (let planSheetName in planUpdates) {
const planSheet = spreadsheet.getSheetByName(planSheetName);
if (!planSheet) {
console.log(`Лист "${planSheetName}" не найден, пропускаем.`);
continue;
}
const planData = planSheet.getDataRange().getValues();
const updates = planUpdates[planSheetName];
for (let key in updates) {
const [article, itemName, type] = key.split('_');
const totalQuantity = updates[key];
for (let i = 1; i < planData.length; i++) { // Пропускаем заголовок
const planArticle = planData[i][0]; // Рекламируемый артикул (столбец A)
const planItemName = planData[i][1]; // Наименование товара (столбец B)
const planIntegrationType = planData[i][2]; // Тип интеграции (столбец C)
if (planIntegrationType === type &&
(planArticle === article || planItemName === itemName)) {
planSheet.getRange(i + 1, 5).setValue(totalQuantity); // Записываем итоговое значение
console.log(`Обновлено: Лист "${planSheetName}", Артикул "${article}", Тип "${type}", Факт "${totalQuantity}".`);
}
}
}
}
console.log(`Факты обновлены для всех листов.`);
}
Эта функция, проходится по листам, поочередно определяет артикул, тип интеграции, а так же ее дату, чтобы занести факт в правильный месяц.
Например, если коммерческая интеграция закуплена в ноябре, но выйдет в декабре, то она пойдет в план декабря.
Например, если коммерческая интеграция закуплена в ноябре, но выйдет в декабре, то она пойдет в план декабря.
Что мы получаем:
автоматический подсчет факта по типу интеграции и артикулу, а так же занесения факта в план соответствующего месяца.
Стоит обратить внимание, что если план на артикул не установлен, то факт передан не будет.
Осталось только добавить триггер для срабатывания, по аналогии с предыдущим.
автоматический подсчет факта по типу интеграции и артикулу, а так же занесения факта в план соответствующего месяца.
Стоит обратить внимание, что если план на артикул не установлен, то факт передан не будет.
Осталось только добавить триггер для срабатывания, по аналогии с предыдущим.
Так, вроде неплохо, мы видим все результаты работы сотрудников, можем установить план и автоматически подсчитать факт, но мы ведь не хотим ковыряться в таблицах, так? Так.
Было бы гораздо удобней получать информацию просто в Телеграм. Давайте займемся этим...
Было бы гораздо удобней получать информацию просто в Телеграм. Давайте займемся этим...
Шаг 4 - указываем куда передавать данные, настраиваем бота
Для начала нам нужно создать Телеграм бота и получить API ключ, ну и группу, куда будет отправляться сообщение, если ее у вас еще нет.
Как создать бота - есть куча инструкций в интернете, вот даже нашел для вас:
Создание и настройка бота
Для начала создадим бота через @BotFather:
- Откройте Телеграм и найдите @BotFather.
- Нажмите /start и затем команду /newbot.
- Придумайте имя для вашего бота (например, ShopBot) и уникальный юзернейм (например, MyShopBot).
- Сохраните API токен, который будет использован для дальнейшей настройки.
Ну как группу создать, думаю, разберётесь, я выбрал для себя группу с темами, то есть в группе у меня были разделы: раздачи / бартер / коммерция.
Добавьте вашего бота туда и дайте права администратора.
Теперь в наш код добавляем сведения о боте и группе:
// Telegram Bot API
const TOKEN = 'API токен телеграм бота'; // Токен вашего бота
const CHAT_ID = 'ID группы телеграм - узнается путем API запроса'; // ID группы или чата
const TOPIC_MAP = { // Соответствие ID тем и вкладок таблицы, узнаем так же темы чата, указываем актуальные вместо '15'/'13'/'16'.
'15': 'Бартер',
'13': 'Коммерция',
'16': 'Раздачи'
};
1. Где взять Thread ID и Group ID (ID темы и ID группы):
ID темы и группы можно получить через Telegram Bot API. Вот как это сделать:
1. Отправьте сообщение в тему группы, куда бот будет отправлять данные.
• Перейдите в вашу группу с темами.
• Выберите тему.
• Напишите сообщение.
2. Запросите обновления через Bot API:
• Откройте браузер и перейдите по ссылке, где вместо TOKEN - подставьте ваш API ключ, который сгенерировали при создании бота:
• Вы увидите ответ в формате JSON, например:
ID темы и группы можно получить через Telegram Bot API. Вот как это сделать:
1. Отправьте сообщение в тему группы, куда бот будет отправлять данные.
• Перейдите в вашу группу с темами.
• Выберите тему.
• Напишите сообщение.
2. Запросите обновления через Bot API:
• Откройте браузер и перейдите по ссылке, где вместо TOKEN - подставьте ваш API ключ, который сгенерировали при создании бота:
• Вы увидите ответ в формате JSON, например:
{
"ok": true,
"result": [
{
"update_id": 123456789,
"message": {
"message_thread_id": 12345,
"chat": {
"id": -987654321,
"type": "supergroup"
},
"text": "Test message"
}
}
]
}
"id": -987654321: Это ваш ID группы
"message_thread_id": 12345: это ваш ID темы.
Если тем несколько, как у меня, просто проделайте это несколько раз.
Вы молодец, теперь для вашего скрипта у вас есть значения, куда передавать нужную информацию, что же пойдемте эту информацию собирать, группировать и отправлять в телеграм
Шаг 5 - чтение данных, их группировка и отправка.
Вот функция, которая базово просто позволяет отправить данные в Тележеньку:
// Функция для отправки сообщений в Telegram
function sendToTelegram(message, threadId) {
const url = `https://api.telegram.org/bot${TOKEN}/sendMessage`;
const payload = {
chat_id: CHAT_ID,
text: message,
parse_mode: 'Markdown',
message_thread_id: threadId // Если нет тем, удалите эту строку
};
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
};
UrlFetchApp.fetch(url, options); // Отправка запроса
}
Тут все просто, мы говорим о том, как, куда и с помощью какого метода обращаться к Телеграм для передачи данных.
Давайте лучше данные будем собирать.
Мне нужно, чтобы я видел все записи, сделанные сотрудниками в подразделениях за вчера.
Этот скрипт проходит и в каждой вкладке анализирует дату: убеждается что запись была сделана вчера. Суммирует данные по артикулу.
Данные которые он собирает: артикул, кол-во интеграций, стоимость этих интеграций.
Давайте лучше данные будем собирать.
Мне нужно, чтобы я видел все записи, сделанные сотрудниками в подразделениях за вчера.
Этот скрипт проходит и в каждой вкладке анализирует дату: убеждается что запись была сделана вчера. Суммирует данные по артикулу.
Данные которые он собирает: артикул, кол-во интеграций, стоимость этих интеграций.
// Функция для фильтрации данных за предыдущий день
function getPreviousDayData(sheet) {
if (!sheet) {
console.log(`Ошибка: Лист не передан или не существует.`);
return {};
}
if (sheet.getLastRow() === 0) {
console.log(`Лист ${sheet.getName()} пуст.`);
return {};
}
const data = sheet.getDataRange().getValues(); // Чтение всех данных
const today = new Date();
const previousDay = new Date(today.setDate(today.getDate() - 1));
const previousDayStr = Utilities.formatDate(previousDay, Session.getScriptTimeZone(), 'dd.MM.yyyy');
const result = {};
for (let i = 1; i < data.length; i++) { // Пропускаем заголовок
const rawDate = data[i][2]; // "Дата закупа"
const dateStr = Utilities.formatDate(new Date(rawDate), Session.getScriptTimeZone(), 'dd.MM.yyyy'); // Преобразуем в строку
const itemName = data[i][6]; // "Наименование товара"
const quantity = Number(data[i][8]) || 0; // "Кол-во"
const price = Number(data[i][14]) || 0; // "Стоимость"
if (dateStr === previousDayStr) { // Сравниваем строки
const key = `${itemName}`; // Уникальный ключ только по артикулу
if (!result[key]) {
result[key] = { itemName, totalQuantity: 0, totalPrice: 0 };
}
result[key].totalQuantity += quantity; // Суммируем количество
result[key].totalPrice += price; // Суммируем стоимость
}
}
return result;
}
Дальше данные необходимо отправить в Телеграм, для этого нам поможет вот эта часть кода:
function sendReportsToTopics() {
const spreadsheet = SpreadsheetApp.openById(MANAGER_SHEET_ID);
// Сначала обновляем планы
updatePlanFacts();
const today = new Date();
const previousDay = new Date(today.setDate(today.getDate() - 1));
const previousDayStr = Utilities.formatDate(previousDay, Session.getScriptTimeZone(), 'dd.MM.yyyy');
// Русские названия месяцев
const months = [
'январь', 'февраль', 'март', 'апрель', 'май', 'июнь',
'июль', 'август', 'сентябрь', 'октябрь', 'ноябрь', 'декабрь'
];
// Формируем название текущего месяца на русском
const currentMonth = months[today.getMonth()];
const currentYear = today.getFullYear();
const planSheetName = `План ${currentMonth} ${currentYear}`; // Пример: "План ноябрь 2024"
const sheetsToProcess = {
'Бартер': 15,
'Коммерция': 13,
'Раздачи': 16
};
for (let sheetName in sheetsToProcess) {
const threadId = sheetsToProcess[sheetName];
const sheet = spreadsheet.getSheetByName(sheetName);
if (!sheet) {
console.log(`Лист ${sheetName} не найден.`);
continue;
}
const data = getPreviousDayData(sheet);
if (Object.keys(data).length === 0) {
const noDataMessage = `*Нет данных за предыдущий день для вкладки ${sheetName}.*`;
sendToTelegram(noDataMessage, threadId);
console.log(noDataMessage);
continue;
}
// Формируем сообщение с отчетом за предыдущий день
let report = `*Отчет по ${sheetName} за ${previousDayStr}:*\n\n`;
for (let key in data) {
const { itemName, totalQuantity, totalPrice } = data[key];
report += `*Артикул:* ${itemName}\n*Кол-во:* ${totalQuantity}\n*Общая стоимость:* ${totalPrice.toFixed(2)}\n\n`;
}
// Добавляем данные из "План [месяц год]"
const planSheet = spreadsheet.getSheetByName(planSheetName);
if (!planSheet) {
console.log(`Лист "${planSheetName}" не найден.`);
sendToTelegram(report, threadId);
continue;
}
const planData = planSheet.getDataRange().getValues();
let planReport = `*Общее за текущий месяц:*\n\n`;
for (let i = 1; i < planData.length; i++) { // Пропускаем заголовок
const itemName = planData[i][1]; // Наименование товара (столбец B)
const integrationType = planData[i][2]; // Тип интеграции (столбец C)
const plan = Number(planData[i][3]) || 0; // План (столбец D)
const fact = Number(planData[i][4]) || 0; // Факт (столбец E)
const percent = Number(planData[i][5]) || 0; // % выполнения плана (столбец F)
if (integrationType === sheetName) { // Сравниваем с текущим типом интеграции
planReport += `*Артикул:* ${itemName}\n*План:* ${plan}\n*Факт:* ${fact}\n*% выполнения плана:* ${percent.toFixed(2)}%\n\n`;
}
}
// Отправляем сообщение
sendToTelegram(report + planReport, threadId);
}
}
Сначала он обновляет факты, затем отправляет данные за день, которые собрал предыдущий скрипт и добавляет данные с листа "План [месяц] [год]"
В итоге мы получаем вот такое сообщение в Телеграм в нужном разделе:
В итоге мы получаем вот такое сообщение в Телеграм в нужном разделе:

Вы молодец!
Осталось установить триггер по времени и вы каждый день будете в Телеграм получать вот такое уведомление по всем подразделениям.
Это позволит вам:
1. Хранить все данные в одной таблице и не теряться между таблицами сотрудников
2. Автоматически подсчитывать факт в отношении к плану, без вашего участия
3. Получать уведомления в Телеграм о работе сотрудников и выполнении плана ежедневно
Ну и при наличии желания, можно все это сделать самому и даже подписок никаких платить не придётся! Все абсолютно бесплатно.
Если у вас остались вопросы, или хотите что-то подобное для своей компании - пишите мне, договоримся t.me/svestudio
Осталось установить триггер по времени и вы каждый день будете в Телеграм получать вот такое уведомление по всем подразделениям.
Это позволит вам:
1. Хранить все данные в одной таблице и не теряться между таблицами сотрудников
2. Автоматически подсчитывать факт в отношении к плану, без вашего участия
3. Получать уведомления в Телеграм о работе сотрудников и выполнении плана ежедневно
Ну и при наличии желания, можно все это сделать самому и даже подписок никаких платить не придётся! Все абсолютно бесплатно.
Если у вас остались вопросы, или хотите что-то подобное для своей компании - пишите мне, договоримся t.me/svestudio