Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save ShilGen/c332e5853fed86e1ee1a40f8ed9db770 to your computer and use it in GitHub Desktop.

Select an option

Save ShilGen/c332e5853fed86e1ee1a40f8ed9db770 to your computer and use it in GitHub Desktop.
/**
* ════════════════════════════════════════════════════════════════════════
* WB API - Полная интеграция для Google Sheets
* ════════════════════════════════════════════════════════════════════════
*
* ИНСТРУКЦИЯ ПО УСТАНОВКЕ:
* 1. Откройте Google Sheets → Расширения → Apps Script
* 2. Вставьте этот код в редактор
* 3. Сохраните проект
* 4. Вернитесь в Google Sheets - появится меню "WB API"
*
* ИСПОЛЬЗОВАНИЕ:
* 1. В ячейке A1 введите ваш API токен WB
* 2. Выберите нужный отчёт в меню "WB API"
* 3. Заполните параметры в диалоговом окне
*/
// ═══════════════════════════════════════════════════════════════════════
// КОНФИГУРАЦИЯ
// ═══════════════════════════════════════════════════════════════════════
const CONFIG = {
// Базовые URL для разных API
STATISTICS_API: 'https://statistics-api.wildberries.ru',
CONTENT_API: 'https://content-api.wildberries.ru',
MARKETPLACE_API: 'https://marketplace-api.wildberries.ru',
ANALYTICS_API: 'https://seller-analytics-api.wildberries.ru',
// Настройки повторных попыток
MAX_RETRY_ATTEMPTS: 60,
RETRY_DELAY_MS: 2000,
RATE_LIMIT_WAIT_MS: 61000,
// Названия листов
SHEETS: {
PAID_STORAGE: 'Платное хранение',
DETAIL_REPORT: 'Детализация реализации',
CARDS: 'Карточки товаров',
WAREHOUSES: 'Склады',
SALES_FUNNEL: 'Воронка продаж',
ORDERS: 'Заказы',
SALES: 'Продажи',
ERROR_LOG: 'Логи ошибок'
}
};
// ═══════════════════════════════════════════════════════════════════════
// МЕНЮ В GOOGLE SHEETS
// ═══════════════════════════════════════════════════════════════════════
/**
* Создает меню при открытии документа
*/
function onOpen() {
const ui = SpreadsheetApp.getUi();
const token = getApiToken();
if (!token) {
// Если токен не настроен, показываем только настройку
ui.createMenu('WB API')
.addItem('⚙️ Настроить токен API', 'showTokenSetup')
.addItem('📖 Инструкция', 'showInstructions')
.addToUi();
} else {
// Полное меню
ui.createMenu('WB API')
.addSubMenu(ui.createMenu('📊 Отчёты')
.addItem('Платное хранение', 'showPaidStorageDialog')
.addItem('Детализация реализации', 'showDetailReportDialog')
.addItem('Заказы', 'showOrdersDialog')
.addItem('Продажи и возвраты', 'showSalesDialog'))
.addSubMenu(ui.createMenu('📦 Товары и склады')
.addItem('Карточки товаров', 'showCardsDialog')
.addItem('Данные по складам', 'showWarehousesDialog'))
.addSubMenu(ui.createMenu('📈 Аналитика')
.addItem('Воронка продаж', 'showSalesFunnelDialog'))
.addSeparator()
.addItem('⚙️ Изменить токен API', 'showTokenSetup')
.addItem('📖 Инструкция', 'showInstructions')
.addToUi();
}
}
/**
* Показывает инструкцию
*/
function showInstructions() {
const ui = SpreadsheetApp.getUi();
const token = getApiToken();
const tokenStatus = token ? '✅ Токен настроен' : '❌ Токен не настроен';
const instructions = `
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📖 ИНСТРУКЦИЯ ПО ИСПОЛЬЗОВАНИЮ
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔑 СТАТУС: ${tokenStatus}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📋 БЫСТРЫЙ СТАРТ:
1️⃣ Настройте токен API
→ Меню "WB API" → "Настроить токен API"
2️⃣ Выберите нужный отчёт в меню "WB API"
3️⃣ Заполните параметры (если требуется)
4️⃣ Дождитесь загрузки данных
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 ДОСТУПНЫЕ ОТЧЁТЫ:
📊 Отчёты:
▪ Платное хранение (макс. 8 дней)
▪ Детализация реализации
▪ Заказы
▪ Продажи и возвраты
📦 Товары и склады:
▪ Карточки товаров
▪ Данные по складам
📈 Аналитика:
▪ Воронка продаж
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⏱️ ВРЕМЯ ЗАГРУЗКИ:
⚡ Быстро (до 1 мин):
• Карточки товаров
• Данные по складам
• Платное хранение
⏳ Средне (1-5 мин):
• Детализация реализации
• Воронка продаж (21 сек между запросами)
🕐 Долго (5+ мин):
• Заказы (61 сек между запросами)
• Продажи и возвраты (61 сек между запросами)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔧 ВАЖНО:
• Токен хранится безопасно в настройках
• Можно изменить токен через меню
• Все ошибки логируются в лист "Логи ошибок"
• Прогресс показывается в диалоговых окнах
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
❓ ПОМОЩЬ:
📚 Документация: https://dev.wildberries.ru/
💬 Поддержка: https://seller.wildberries.ru/help-center
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
`;
ui.alert('Инструкция по использованию', instructions, ui.ButtonSet.OK);
}
// ═══════════════════════════════════════════════════════════════════════
// ОБЩИЕ ФУНКЦИИ
// ═══════════════════════════════════════════════════════════════════════
/**
* Получает токен API из Properties Service
*/
function getApiToken() {
const properties = PropertiesService.getUserProperties();
return properties.getProperty('WB_API_TOKEN') || '';
}
/**
* Сохраняет токен API в Properties Service
*/
function saveApiToken(token) {
const properties = PropertiesService.getUserProperties();
properties.setProperty('WB_API_TOKEN', token.trim());
return true;
}
/**
* Показывает диалог настройки токена
*/
function showTokenSetup() {
const currentToken = getApiToken();
const tokenMasked = currentToken ? currentToken.substring(0, 20) + '...' : 'не настроен';
const html = HtmlService.createHtmlOutput(`
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.container {
background: white;
padding: 30px;
border-radius: 12px;
color: #333;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
h2 { margin-top: 0; color: #667eea; text-align: center; }
.info {
background-color: #f0f8ff;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 4px solid #667eea;
}
.form-group { margin-bottom: 20px; }
label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #555;
}
input {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 14px;
box-sizing: border-box;
transition: border-color 0.3s;
}
input:focus {
outline: none;
border-color: #667eea;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 14px 28px;
border: none;
border-radius: 6px;
cursor: pointer;
width: 100%;
font-size: 16px;
font-weight: bold;
transition: transform 0.2s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.current-token {
background: #f8f9fa;
padding: 10px;
border-radius: 6px;
margin-top: 10px;
font-family: monospace;
font-size: 12px;
color: #666;
}
.success-msg {
background: #d4edda;
color: #155724;
padding: 12px;
border-radius: 6px;
margin-top: 15px;
display: none;
border-left: 4px solid #28a745;
}
.error-msg {
background: #f8d7da;
color: #721c24;
padding: 12px;
border-radius: 6px;
margin-top: 15px;
display: none;
border-left: 4px solid #dc3545;
}
.links {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #ddd;
font-size: 13px;
}
.links a {
color: #667eea;
text-decoration: none;
}
.links a:hover {
text-decoration: underline;
}
</style>
<div class="container">
<h2>🔑 Настройка токена WB API</h2>
<div class="info">
<strong>ℹ️ Как получить токен:</strong><br>
1. Откройте <a href="https://seller.wildberries.ru/api-integrations" target="_blank">личный кабинет WB</a><br>
2. Профиль → Интеграции по API<br>
3. Создайте токен с правами: <strong>Statistics, Marketplace, Content, Analytics</strong><br>
4. Скопируйте токен и вставьте ниже
</div>
${currentToken ? `
<div class="current-token">
<strong>Текущий токен:</strong><br>
${tokenMasked}
</div>
` : ''}
<div class="form-group">
<label for="token">Введите токен WB API:</label>
<input
type="text"
id="token"
placeholder="Вставьте сюда ваш токен"
value="${currentToken}"
>
</div>
<button onclick="saveToken()" id="saveBtn">
${currentToken ? '🔄 Обновить токен' : '💾 Сохранить токен'}
</button>
<div id="successMsg" class="success-msg"></div>
<div id="errorMsg" class="error-msg"></div>
<div class="links">
📚 <a href="https://dev.wildberries.ru/" target="_blank">Документация WB API</a><br>
💬 <a href="https://seller.wildberries.ru/help-center" target="_blank">Центр помощи</a>
</div>
</div>
<script>
function saveToken() {
const token = document.getElementById('token').value.trim();
const saveBtn = document.getElementById('saveBtn');
const successMsg = document.getElementById('successMsg');
const errorMsg = document.getElementById('errorMsg');
// Скрываем предыдущие сообщения
successMsg.style.display = 'none';
errorMsg.style.display = 'none';
if (!token) {
errorMsg.textContent = '⚠️ Введите токен!';
errorMsg.style.display = 'block';
return;
}
if (token.length < 50) {
errorMsg.textContent = '⚠️ Токен слишком короткий. Проверьте, что скопировали его полностью.';
errorMsg.style.display = 'block';
return;
}
saveBtn.disabled = true;
saveBtn.textContent = '⏳ Сохранение...';
google.script.run
.withSuccessHandler(function() {
successMsg.textContent = '✅ Токен успешно сохранён! Обновите страницу, чтобы увидеть все отчёты.';
successMsg.style.display = 'block';
saveBtn.textContent = '✅ Сохранено!';
setTimeout(function() {
google.script.host.close();
location.reload();
}, 2000);
})
.withFailureHandler(function(error) {
errorMsg.textContent = '❌ Ошибка: ' + error.message;
errorMsg.style.display = 'block';
saveBtn.disabled = false;
saveBtn.textContent = '${currentToken ? '🔄 Обновить токен' : '💾 Сохранить токен'}';
})
.saveApiToken(token);
}
// Автофокус на поле ввода
document.getElementById('token').focus();
// Enter для сохранения
document.getElementById('token').addEventListener('keypress', function(e) {
if (e.key === 'Enter') saveToken();
});
</script>
`).setWidth(500).setHeight(500);
SpreadsheetApp.getUi().showModalDialog(html, 'Настройка WB API');
}
/**
* Получает или создает лист с указанным именем
*/
function getOrCreateSheet(sheetName) {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
let sheet = spreadsheet.getSheetByName(sheetName);
if (!sheet) {
sheet = spreadsheet.insertSheet(sheetName);
} else {
sheet.clear();
}
return sheet;
}
/**
* Логирует ошибку в специальный лист
*/
function logError(error) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let logSheet = ss.getSheetByName(CONFIG.SHEETS.ERROR_LOG);
if (!logSheet) {
logSheet = ss.insertSheet(CONFIG.SHEETS.ERROR_LOG);
logSheet.appendRow(['Время', 'Функция', 'Сообщение']);
logSheet.getRange(1, 1, 1, 3).setFontWeight('bold').setBackground('#EA4335').setFontColor('#FFFFFF');
}
const caller = error.stack ? error.stack.split('\n')[1] : 'Неизвестно';
logSheet.appendRow([new Date(), caller, error.message || JSON.stringify(error)]);
logSheet.autoResizeColumns(1, 3);
}
/**
* Форматирует лист с заголовками
*/
function formatSheetHeaders(sheet, headers) {
sheet.getRange(1, 1, 1, headers.length)
.setValues([headers])
.setFontWeight('bold')
.setBackground('#4285F4')
.setFontColor('#FFFFFF');
sheet.setFrozenRows(1);
sheet.autoResizeColumns(1, headers.length);
sheet.getRange(1, 1, sheet.getMaxRows(), headers.length).createFilter();
}
/**
* Добавляет временную метку обновления
*/
function addUpdateTimestamp(sheet) {
const lastRow = sheet.getLastRow();
sheet.getRange(lastRow + 2, 1).setValue('Обновлено:');
sheet.getRange(lastRow + 2, 2)
.setValue(new Date())
.setNumberFormat('dd.MM.yyyy HH:mm:ss');
}
/**
* Выполняет API запрос с обработкой rate limits
*/
function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = UrlFetchApp.fetch(url, options);
const statusCode = response.getResponseCode();
const headers = response.getAllHeaders();
if (statusCode === 429) {
const retryHeader = headers['X-Ratelimit-Retry'] || headers['x-ratelimit-retry'];
const retrySec = retryHeader ? parseInt(retryHeader, 10) : 20;
Logger.log(`Rate limit - ожидание ${retrySec + 1} секунд`);
Utilities.sleep((retrySec + 1) * 1000);
continue;
}
if (statusCode === 401) {
throw new Error('401 Unauthorized: проверьте токен API');
}
if (statusCode !== 200) {
throw new Error(`HTTP ${statusCode}: ${response.getContentText()}`);
}
return response;
} catch (error) {
if (attempt === maxRetries) throw error;
Logger.log(`Попытка ${attempt} не удалась, повтор...`);
Utilities.sleep(2000);
}
}
}
// ═══════════════════════════════════════════════════════════════════════
// 1. ПЛАТНОЕ ХРАНЕНИЕ
// ═══════════════════════════════════════════════════════════════════════
function showPaidStorageDialog() {
const ui = SpreadsheetApp.getUi();
const token = getApiToken();
if (!token) {
ui.alert('⚠️ Токен не найден', 'Введите токен WB API в ячейку A1', ui.ButtonSet.OK);
return;
}
const today = new Date();
const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
const dateFromStr = Utilities.formatDate(weekAgo, Session.getScriptTimeZone(), 'yyyy-MM-dd');
const dateToStr = Utilities.formatDate(today, Session.getScriptTimeZone(), 'yyyy-MM-dd');
const html = HtmlService.createHtmlOutput(`
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type="date"] { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
button { background-color: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; width: 100%; }
.info { background-color: #f0f8ff; padding: 10px; border-radius: 4px; margin-bottom: 15px; }
</style>
<div class="info">ℹ️ Максимальный период — 8 дней</div>
<div class="form-group">
<label>Дата начала:</label>
<input type="date" id="dateFrom" value="${dateFromStr}">
</div>
<div class="form-group">
<label>Дата окончания:</label>
<input type="date" id="dateTo" value="${dateToStr}">
</div>
<button onclick="submitForm()">📥 Загрузить отчет</button>
<script>
function submitForm() {
const dateFrom = document.getElementById('dateFrom').value;
const dateTo = document.getElementById('dateTo').value;
if (!dateFrom || !dateTo) { alert('Выберите обе даты'); return; }
const from = new Date(dateFrom); const to = new Date(dateTo);
const daysDiff = Math.ceil((to - from) / (1000 * 60 * 60 * 24));
if (daysDiff > 8) { alert('⚠️ Максимальный период — 8 дней'); return; }
if (daysDiff < 0) { alert('⚠️ Дата начала должна быть раньше'); return; }
google.script.run.withSuccessHandler(() => google.script.host.close())
.withFailureHandler(e => alert('Ошибка: ' + e.message))
.getPaidStorageReport(dateFrom, dateTo);
document.body.innerHTML = '<div style="text-align: center; padding: 40px;"><h2>⏳ Загрузка...</h2></div>';
}
</script>
`).setWidth(400).setHeight(300);
ui.showModalDialog(html, '📊 Платное хранение');
}
function getPaidStorageReport(dateFrom, dateTo) {
const token = getApiToken();
const url = `${CONFIG.ANALYTICS_API}/api/v1/paid_storage?dateFrom=${dateFrom}&dateTo=${dateTo}`;
try {
SpreadsheetApp.getActiveSpreadsheet().toast('Создание задачи...', '⏳ WB API', -1);
// Создаем задачу
const taskResponse = fetchWithRetry(url, {
method: 'get',
headers: { 'Authorization': token },
muteHttpExceptions: true
});
const taskData = JSON.parse(taskResponse.getContentText());
const taskId = taskData.data.taskId;
SpreadsheetApp.getActiveSpreadsheet().toast(`Задача ${taskId}. Ожидание...`, '⏳ WB API', -1);
// Ждем готовности
const statusUrl = `${CONFIG.ANALYTICS_API}/api/v1/paid_storage/tasks/${taskId}/status`;
for (let i = 0; i < CONFIG.MAX_RETRY_ATTEMPTS; i++) {
const statusResp = fetchWithRetry(statusUrl, {
method: 'get',
headers: { 'Authorization': token },
muteHttpExceptions: true
});
const statusData = JSON.parse(statusResp.getContentText());
if (statusData.data.status === 'done') break;
if (statusData.data.status === 'error') throw new Error('Ошибка генерации отчета');
if (i < CONFIG.MAX_RETRY_ATTEMPTS - 1) Utilities.sleep(CONFIG.RETRY_DELAY_MS);
}
// Скачиваем
SpreadsheetApp.getActiveSpreadsheet().toast('Скачивание данных...', '⏳ WB API', -1);
const downloadUrl = `${CONFIG.ANALYTICS_API}/api/v1/paid_storage/tasks/${taskId}/download`;
const reportResp = fetchWithRetry(downloadUrl, {
method: 'get',
headers: { 'Authorization': token },
muteHttpExceptions: true
});
const reportData = JSON.parse(reportResp.getContentText());
// Записываем
const sheet = getOrCreateSheet(CONFIG.SHEETS.PAID_STORAGE);
const headers = ['Дата', 'Склад', 'Коэфф. склада', 'Артикул WB', 'Артикул продавца', 'Бренд', 'Предмет',
'Размер', 'Штрихкод', 'Объем (л)', 'Тип расчета', 'Стоимость (₽)', 'Кол-во ШК'];
formatSheetHeaders(sheet, headers);
const rows = reportData.map(item => [
item.date || '', item.warehouse || '', item.warehouseCoef || 0, item.nmId || '', item.vendorCode || '',
item.brand || '', item.subject || '', item.size || '', item.barcode || '', item.volume || 0,
item.calcType || '', item.warehousePrice || 0, item.barcodesCount || 0
]);
if (rows.length > 0) {
sheet.getRange(2, 1, rows.length, headers.length).setValues(rows);
sheet.getRange(2, 12, rows.length, 1).setNumberFormat('#,##0.00 ₽');
}
addUpdateTimestamp(sheet);
SpreadsheetApp.getActiveSpreadsheet().setActiveSheet(sheet);
SpreadsheetApp.getActiveSpreadsheet().toast(`Загружено ${rows.length} записей`, '✅ Готово!', 5);
} catch (error) {
logError(error);
SpreadsheetApp.getUi().alert('❌ Ошибка', error.message, SpreadsheetApp.getUi().ButtonSet.OK);
throw error;
}
}
// ═══════════════════════════════════════════════════════════════════════
// 2. ДЕТАЛИЗАЦИЯ РЕАЛИЗАЦИИ
// ═══════════════════════════════════════════════════════════════════════
function showDetailReportDialog() {
const ui = SpreadsheetApp.getUi();
const token = getApiToken();
if (!token) {
ui.alert('⚠️ Токен не найден', 'Введите токен WB API в ячейку A1', ui.ButtonSet.OK);
return;
}
const html = HtmlService.createHtmlOutput(`
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
button { background-color: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; width: 100%; }
.info { background-color: #f0f8ff; padding: 10px; border-radius: 4px; margin-bottom: 15px; }
</style>
<div class="info">ℹ️ Данные доступны с 29 января 2024 года</div>
<div class="form-group">
<label>Дата начала:</label>
<input type="date" id="dateFrom" value="2024-01-29">
</div>
<div class="form-group">
<label>Дата окончания:</label>
<input type="date" id="dateTo" value="${new Date().toISOString().split('T')[0]}">
</div>
<button onclick="submitForm()">📥 Загрузить отчет</button>
<script>
function submitForm() {
const dateFrom = document.getElementById('dateFrom').value;
const dateTo = document.getElementById('dateTo').value;
if (!dateFrom || !dateTo) { alert('Выберите обе даты'); return; }
google.script.run.withSuccessHandler(() => google.script.host.close())
.withFailureHandler(e => alert('Ошибка: ' + e.message))
.getDetailReport(dateFrom, dateTo);
document.body.innerHTML = '<div style="text-align: center; padding: 40px;"><h2>⏳ Загрузка...</h2><p>Это может занять несколько минут</p></div>';
}
</script>
`).setWidth(400).setHeight(300);
ui.showModalDialog(html, '📊 Детализация реализации');
}
function getDetailReport(dateFrom, dateTo) {
const token = getApiToken();
const limit = 100000;
const sheet = getOrCreateSheet(CONFIG.SHEETS.DETAIL_REPORT);
try {
SpreadsheetApp.getActiveSpreadsheet().toast('Загрузка детализации...', '⏳ WB API', -1);
let rrdid = 0;
let headers = [];
let allData = [];
let headersSet = false;
while (true) {
const url = `${CONFIG.STATISTICS_API}/api/v5/supplier/reportDetailByPeriod?dateFrom=${dateFrom}&dateTo=${dateTo}&limit=${limit}&rrdid=${rrdid}`;
const response = fetchWithRetry(url, {
method: 'get',
headers: { 'Authorization': token },
muteHttpExceptions: true
});
const data = JSON.parse(response.getContentText());
if (!data || data.length === 0) break;
if (!headersSet) {
headers = Object.keys(data[0]);
formatSheetHeaders(sheet, headers);
headersSet = true;
}
const rows = data.map(row => headers.map(header => row[header]));
allData.push(...rows);
rrdid = data[data.length - 1].rrd_id;
Logger.log(`Загружено записей: ${allData.length}`);
if (data.length < limit) break;
Utilities.sleep(1000);
}
if (allData.length > 0) {
sheet.getRange(2, 1, allData.length, allData[0].length).setValues(allData);
}
addUpdateTimestamp(sheet);
SpreadsheetApp.getActiveSpreadsheet().setActiveSheet(sheet);
SpreadsheetApp.getActiveSpreadsheet().toast(`Загружено ${allData.length} записей`, '✅ Готово!', 5);
} catch (error) {
logError(error);
SpreadsheetApp.getUi().alert('❌ Ошибка', error.message, SpreadsheetApp.getUi().ButtonSet.OK);
throw error;
}
}
// ═══════════════════════════════════════════════════════════════════════
// 3. КАРТОЧКИ ТОВАРОВ
// ═══════════════════════════════════════════════════════════════════════
function showCardsDialog() {
const ui = SpreadsheetApp.getUi();
const token = getApiToken();
if (!token) {
ui.alert('⚠️ Токен не найден', 'Настройте токен через меню: WB API → Настроить токен API', ui.ButtonSet.OK);
return;
}
const html = HtmlService.createHtmlOutput(`
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.info { background-color: #f0f8ff; padding: 15px; border-radius: 8px; margin-bottom: 15px; border-left: 4px solid #4285F4; }
.warning { background-color: #fff3cd; padding: 15px; border-radius: 8px; margin-bottom: 15px; border-left: 4px solid #ffc107; }
button { background-color: #4CAF50; color: white; padding: 14px 28px; border: none; border-radius: 6px; cursor: pointer; width: 100%; font-size: 16px; font-weight: bold; }
button:hover { background-color: #45a049; }
button:disabled { background-color: #cccccc; cursor: not-allowed; }
#progress { display: none; margin-top: 20px; }
.progress-bar { width: 100%; height: 30px; background-color: #f0f0f0; border-radius: 15px; overflow: hidden; }
.progress-fill { height: 100%; background: linear-gradient(90deg, #4285F4, #34A853); width: 0%; transition: width 0.3s; text-align: center; line-height: 30px; color: white; font-weight: bold; }
.status { margin-top: 15px; padding: 10px; background: #f8f9fa; border-radius: 6px; font-size: 14px; }
.success { background: #d4edda; color: #155724; padding: 15px; border-radius: 6px; margin-top: 15px; display: none; }
.error { background: #f8d7da; color: #721c24; padding: 15px; border-radius: 6px; margin-top: 15px; display: none; }
</style>
<div class="info">
<strong>📦 Карточки товаров</strong><br><br>
Будет загружен список всех ваших товаров с информацией о необходимости маркировки (КИЗ).
</div>
<div class="warning">
<strong>⚠️ Важно:</strong><br>
Если у вас много товаров (более 1000), загрузка может занять несколько минут. Пожалуйста, не закрывайте это окно до завершения.
</div>
<button onclick="startLoad()" id="loadBtn">📥 Загрузить карточки</button>
<div id="progress">
<div class="progress-bar">
<div class="progress-fill" id="progressFill">0%</div>
</div>
<div class="status" id="status">Инициализация...</div>
</div>
<div id="successMsg" class="success"></div>
<div id="errorMsg" class="error"></div>
<script>
let progressInterval;
function startLoad() {
document.getElementById('loadBtn').disabled = true;
document.getElementById('loadBtn').textContent = '⏳ Загрузка...';
document.getElementById('progress').style.display = 'block';
updateProgress(5, 'Подключение к API...');
google.script.run
.withSuccessHandler(onSuccess)
.withFailureHandler(onError)
.getProductCards();
// Медленный прогресс для больших объемов данных
let currentProgress = 5;
progressInterval = setInterval(() => {
if (currentProgress < 95) {
currentProgress += 1;
updateProgress(currentProgress, getStatusMessage(currentProgress));
}
}, 2000); // Обновляем каждые 2 секунды
}
function getStatusMessage(progress) {
if (progress < 20) return 'Подключение к API...';
if (progress < 40) return 'Загрузка первой порции данных...';
if (progress < 60) return 'Обработка товаров...';
if (progress < 80) return 'Загрузка продолжается...';
return 'Завершение...';
}
function updateProgress(percent, status) {
document.getElementById('progressFill').style.width = percent + '%';
document.getElementById('progressFill').textContent = percent + '%';
document.getElementById('status').textContent = status;
}
function onSuccess(result) {
clearInterval(progressInterval);
updateProgress(100, 'Готово!');
const msg = document.getElementById('successMsg');
msg.textContent = '✅ Успешно загружено! Данные находятся в листе "Карточки товаров"';
msg.style.display = 'block';
setTimeout(() => google.script.host.close(), 2000);
}
function onError(error) {
clearInterval(progressInterval);
document.getElementById('progress').style.display = 'none';
const msg = document.getElementById('errorMsg');
if (error.message && error.message.includes('time')) {
msg.innerHTML = '<strong>❌ Превышен лимит времени</strong><br><br>' +
'У вас очень много товаров. Рекомендации:<br>' +
'1. Попробуйте еще раз - иногда помогает<br>' +
'2. Используйте фильтры в API (если доступны)<br>' +
'3. Обратитесь в поддержку WB для большого каталога';
} else {
msg.textContent = '❌ Ошибка: ' + error.message;
}
msg.style.display = 'block';
document.getElementById('loadBtn').disabled = false;
document.getElementById('loadBtn').textContent = '🔄 Попробовать снова';
}
</script>
`).setWidth(500).setHeight(450);
ui.showModalDialog(html, '📦 Карточки товаров');
}
function getProductCards() {
const token = getApiToken();
const url = `${CONFIG.CONTENT_API}/content/v2/get/cards/list`;
try {
// Создаем лист сразу
const sheet = getOrCreateSheet(CONFIG.SHEETS.CARDS);
const headers = ['Артикул (nmID)', 'Название товара', 'Нужен КИЗ'];
formatSheetHeaders(sheet, headers);
let cursor = { limit: 1000 }; // Увеличиваем лимит с 100 до 1000
const filter = { withPhoto: -1 };
let totalCards = 0;
let currentRow = 2; // Начинаем со 2-й строки (1-я - заголовки)
while (true) {
const payload = { settings: { cursor, filter } };
const response = fetchWithRetry(url, {
method: 'post',
headers: {
'Authorization': token,
'Content-Type': 'application/json'
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
const data = JSON.parse(response.getContentText());
if (!data.cards || data.cards.length === 0) break;
// Сразу записываем порцию в таблицу, а не накапливаем в памяти
const rows = data.cards.map(card => [
card.nmID,
card.title,
card.needKiz ? 'Да' : 'Нет'
]);
sheet.getRange(currentRow, 1, rows.length, headers.length).setValues(rows);
currentRow += rows.length;
totalCards += rows.length;
Logger.log(`Загружено карточек: ${totalCards}`);
// Проверяем, есть ли ещё данные
if (data.cursor.total < cursor.limit) break;
cursor = {
limit: cursor.limit,
updatedAt: data.cursor.updatedAt,
nmID: data.cursor.nmID
};
// Убрали sleep - для Content API он не нужен
}
addUpdateTimestamp(sheet);
SpreadsheetApp.getActiveSpreadsheet().setActiveSheet(sheet);
return { success: true, count: totalCards };
} catch (error) {
logError(error);
throw error;
}
}
// ═══════════════════════════════════════════════════════════════════════
// 4. СКЛАДЫ
// ═══════════════════════════════════════════════════════════════════════
function showWarehousesDialog() {
const ui = SpreadsheetApp.getUi();
const token = getApiToken();
if (!token) {
ui.alert('⚠️ Токен не найден', 'Настройте токен через меню: WB API → Настроить токен API', ui.ButtonSet.OK);
return;
}
const html = HtmlService.createHtmlOutput(`
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.info { background-color: #f0f8ff; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #4285F4; }
button { background-color: #4CAF50; color: white; padding: 14px 28px; border: none; border-radius: 6px; cursor: pointer; width: 100%; font-size: 16px; font-weight: bold; }
button:hover { background-color: #45a049; }
button:disabled { background-color: #cccccc; cursor: not-allowed; }
#progress { display: none; margin-top: 20px; }
.progress-bar { width: 100%; height: 30px; background-color: #f0f0f0; border-radius: 15px; overflow: hidden; }
.progress-fill { height: 100%; background: linear-gradient(90deg, #4285F4, #34A853); width: 0%; transition: width 0.3s; text-align: center; line-height: 30px; color: white; font-weight: bold; }
.status { margin-top: 15px; padding: 10px; background: #f8f9fa; border-radius: 6px; font-size: 14px; }
.success { background: #d4edda; color: #155724; padding: 15px; border-radius: 6px; margin-top: 15px; display: none; }
.error { background: #f8d7da; color: #721c24; padding: 15px; border-radius: 6px; margin-top: 15px; display: none; }
</style>
<div class="info">
<strong>🏭 Данные по складам</strong><br><br>
Будет загружена информация о всех доступных складах: типы груза, типы доставки и другие характеристики.
</div>
<button onclick="startLoad()" id="loadBtn">📥 Загрузить склады</button>
<div id="progress">
<div class="progress-bar">
<div class="progress-fill" id="progressFill">0%</div>
</div>
<div class="status" id="status">Инициализация...</div>
</div>
<div id="successMsg" class="success"></div>
<div id="errorMsg" class="error"></div>
<script>
function startLoad() {
document.getElementById('loadBtn').disabled = true;
document.getElementById('loadBtn').textContent = '⏳ Загрузка...';
document.getElementById('progress').style.display = 'block';
updateProgress(20, 'Подключение к API...');
google.script.run
.withSuccessHandler(onSuccess)
.withFailureHandler(onError)
.getWarehouses();
// Симуляция прогресса
setTimeout(() => updateProgress(60, 'Получение данных о складах...'), 500);
setTimeout(() => updateProgress(90, 'Сохранение...'), 1500);
}
function updateProgress(percent, status) {
document.getElementById('progressFill').style.width = percent + '%';
document.getElementById('progressFill').textContent = percent + '%';
document.getElementById('status').textContent = status;
}
function onSuccess(result) {
updateProgress(100, 'Готово!');
const msg = document.getElementById('successMsg');
msg.textContent = '✅ Успешно загружено! Данные находятся в листе "Склады"';
msg.style.display = 'block';
setTimeout(() => google.script.host.close(), 2000);
}
function onError(error) {
document.getElementById('progress').style.display = 'none';
const msg = document.getElementById('errorMsg');
msg.textContent = '❌ Ошибка: ' + error.message;
msg.style.display = 'block';
document.getElementById('loadBtn').disabled = false;
document.getElementById('loadBtn').textContent = '🔄 Попробовать снова';
}
</script>
`).setWidth(450).setHeight(350);
ui.showModalDialog(html, '🏭 Данные по складам');
}
function getWarehouses() {
const token = getApiToken();
const url = `${CONFIG.MARKETPLACE_API}/api/v3/warehouses`;
try {
const response = fetchWithRetry(url, {
method: 'get',
headers: { 'Authorization': token },
muteHttpExceptions: true
});
const warehouses = JSON.parse(response.getContentText());
const sheet = getOrCreateSheet(CONFIG.SHEETS.WAREHOUSES);
const headers = ['ID склада', 'Название', 'Тип груза', 'Тип доставки'];
formatSheetHeaders(sheet, headers);
const cargoTypes = { 1: 'Малогабаритный', 2: 'Крупногабаритный', 3: 'Сверхгабаритный' };
const deliveryTypes = { 1: 'FBS', 2: 'FBO', 5: 'Самовывоз' };
const rows = warehouses.map(w => [
w.id,
w.name,
cargoTypes[w.cargoType] || `Неизвестный (${w.cargoType})`,
deliveryTypes[w.deliveryType] || `Неизвестный (${w.deliveryType})`
]);
if (rows.length > 0) {
sheet.getRange(2, 1, rows.length, headers.length).setValues(rows);
}
addUpdateTimestamp(sheet);
SpreadsheetApp.getActiveSpreadsheet().setActiveSheet(sheet);
return { success: true, count: rows.length };
} catch (error) {
logError(error);
throw error;
}
}
// ═══════════════════════════════════════════════════════════════════════
// 5. ВОРОНКА ПРОДАЖ
// ═══════════════════════════════════════════════════════════════════════
function showSalesFunnelDialog() {
const ui = SpreadsheetApp.getUi();
const token = getApiToken();
if (!token) {
ui.alert('⚠️ Токен не найден', 'Настройте токен через меню: WB API → Настроить токен API', ui.ButtonSet.OK);
return;
}
const firstDayOfMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
const dateFromStr = Utilities.formatDate(firstDayOfMonth, 'Europe/Moscow', 'yyyy-MM-dd');
const dateToStr = Utilities.formatDate(new Date(), 'Europe/Moscow', 'yyyy-MM-dd');
const html = HtmlService.createHtmlOutput(`
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
button { background-color: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; width: 100%; }
button:disabled { background-color: #cccccc; }
.info { background-color: #fff3cd; padding: 10px; border-radius: 4px; margin-bottom: 15px; border-left: 4px solid #ffc107; }
#progress { display: none; margin-top: 20px; }
.progress-bar { width: 100%; height: 25px; background-color: #f0f0f0; border-radius: 12px; overflow: hidden; }
.progress-fill { height: 100%; background: linear-gradient(90deg, #4285F4, #34A853); width: 0%; transition: width 0.5s; text-align: center; line-height: 25px; color: white; font-size: 12px; font-weight: bold; }
.status { margin-top: 10px; padding: 8px; background: #f8f9fa; border-radius: 4px; font-size: 13px; }
.success { background: #d4edda; color: #155724; padding: 15px; border-radius: 6px; margin-top: 15px; display: none; }
.error { background: #f8d7da; color: #721c24; padding: 15px; border-radius: 6px; margin-top: 15px; display: none; }
</style>
<div class="info">⚠️ Загрузка может занять несколько минут. Между запросами пауза 21 секунда (ограничение API)</div>
<div id="formSection">
<div class="form-group">
<label>Дата начала:</label>
<input type="date" id="dateFrom" value="${dateFromStr}">
</div>
<div class="form-group">
<label>Дата окончания:</label>
<input type="date" id="dateTo" value="${dateToStr}">
</div>
<button onclick="submitForm()" id="submitBtn">📥 Загрузить отчет</button>
</div>
<div id="progress">
<div class="progress-bar">
<div class="progress-fill" id="progressFill">0%</div>
</div>
<div class="status" id="status">Инициализация...</div>
</div>
<div id="successMsg" class="success"></div>
<div id="errorMsg" class="error"></div>
<script>
function submitForm() {
const dateFrom = document.getElementById('dateFrom').value;
const dateTo = document.getElementById('dateTo').value;
if (!dateFrom || !dateTo) { alert('Выберите обе даты'); return; }
document.getElementById('formSection').style.display = 'none';
document.getElementById('progress').style.display = 'block';
updateProgress(5, 'Подключение к API...');
google.script.run
.withSuccessHandler(onSuccess)
.withFailureHandler(onError)
.getSalesFunnel(dateFrom, dateTo);
// Симуляция долгого прогресса
setTimeout(() => updateProgress(15, 'Запрос данных...'), 2000);
setTimeout(() => updateProgress(25, 'Обработка страницы 1...'), 5000);
setTimeout(() => updateProgress(40, 'Ожидание (21 сек между запросами)...'), 10000);
setTimeout(() => updateProgress(60, 'Обработка данных...'), 30000);
setTimeout(() => updateProgress(80, 'Формирование отчёта...'), 60000);
}
function updateProgress(percent, status) {
document.getElementById('progressFill').style.width = percent + '%';
document.getElementById('progressFill').textContent = percent + '%';
document.getElementById('status').textContent = status;
}
function onSuccess(result) {
updateProgress(100, 'Готово!');
const msg = document.getElementById('successMsg');
msg.textContent = '✅ Успешно загружено! Данные находятся в листе "Воронка продаж"';
msg.style.display = 'block';
setTimeout(() => google.script.host.close(), 2000);
}
function onError(error) {
document.getElementById('progress').style.display = 'none';
document.getElementById('formSection').style.display = 'block';
const msg = document.getElementById('errorMsg');
msg.textContent = '❌ Ошибка: ' + error.message;
msg.style.display = 'block';
}
</script>
`).setWidth(450).setHeight(450);
ui.showModalDialog(html, '📈 Воронка продаж');
}
function getSalesFunnel(dateFrom, dateTo) {
const token = getApiToken();
const url = `${CONFIG.ANALYTICS_API}/api/v2/nm-report/detail`;
try {
const bodyTemplate = {
brandNames: [],
objectIDs: [],
tagIDs: [],
nmIDs: [],
timezone: 'Europe/Moscow',
period: {
begin: `${dateFrom} 00:00:00`,
end: `${dateTo} 23:59:59`
},
orderBy: { field: 'ordersSumRub', mode: 'desc' },
page: 1
};
const sheet = getOrCreateSheet(CONFIG.SHEETS.SALES_FUNNEL);
const headers = ['nmID', 'Артикул', 'Бренд', 'Заказы', 'Продажи (₽)', 'В корзину', 'Открытия'];
formatSheetHeaders(sheet, headers);
const allRows = [];
while (true) {
const response = fetchWithRetry(url, {
method: 'post',
headers: {
'Authorization': token,
'Content-Type': 'application/json'
},
payload: JSON.stringify(bodyTemplate),
muteHttpExceptions: true
});
const json = JSON.parse(response.getContentText());
json.data.cards.forEach(c => {
allRows.push([
c.nmID,
c.vendorCode,
c.brandName,
c.statistics.selectedPeriod.ordersCount,
c.statistics.selectedPeriod.ordersSumRub,
c.statistics.selectedPeriod.addToCartCount,
c.statistics.selectedPeriod.openCardCount
]);
});
Logger.log(`Загружено товаров: ${allRows.length}`);
if (!json.data.isNextPage) break;
bodyTemplate.page += 1;
Utilities.sleep(21000);
}
if (allRows.length > 0) {
sheet.getRange(2, 1, allRows.length, headers.length).setValues(allRows);
sheet.getRange(2, 5, allRows.length, 1).setNumberFormat('#,##0 ₽');
}
addUpdateTimestamp(sheet);
SpreadsheetApp.getActiveSpreadsheet().setActiveSheet(sheet);
return { success: true, count: allRows.length };
} catch (error) {
logError(error);
throw error;
}
}
// ═══════════════════════════════════════════════════════════════════════
// 6. ЗАКАЗЫ
// ═══════════════════════════════════════════════════════════════════════
function showOrdersDialog() {
const ui = SpreadsheetApp.getUi();
const token = getApiToken();
if (!token) {
ui.alert('⚠️ Токен не найден', 'Настройте токен через меню: WB API → Настроить токен API', ui.ButtonSet.OK);
return;
}
const monthAgo = new Date(new Date().getTime() - 30 * 24 * 60 * 60 * 1000);
const dateFromStr = Utilities.formatDate(monthAgo, Session.getScriptTimeZone(), 'yyyy-MM-dd');
const html = HtmlService.createHtmlOutput(`
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
button { background-color: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; width: 100%; }
button:disabled { background-color: #cccccc; }
.info { background-color: #fff3cd; padding: 10px; border-radius: 4px; margin-bottom: 15px; border-left: 4px solid #ffc107; }
#progress { display: none; margin-top: 20px; }
.progress-bar { width: 100%; height: 25px; background-color: #f0f0f0; border-radius: 12px; overflow: hidden; }
.progress-fill { height: 100%; background: linear-gradient(90deg, #4285F4, #34A853); width: 0%; transition: width 0.5s; text-align: center; line-height: 25px; color: white; font-size: 12px; font-weight: bold; }
.status { margin-top: 10px; padding: 8px; background: #f8f9fa; border-radius: 4px; font-size: 13px; }
.success { background: #d4edda; color: #155724; padding: 15px; border-radius: 6px; margin-top: 15px; display: none; }
.error { background: #f8d7da; color: #721c24; padding: 15px; border-radius: 6px; margin-top: 15px; display: none; }
</style>
<div class="info">⚠️ Между запросами пауза 61 секунда (ограничение API). Загрузка может занять продолжительное время.</div>
<div id="formSection">
<div class="form-group">
<label>Дата начала:</label>
<input type="date" id="dateFrom" value="${dateFromStr}">
</div>
<button onclick="submitForm()" id="submitBtn">📥 Загрузить заказы</button>
</div>
<div id="progress">
<div class="progress-bar">
<div class="progress-fill" id="progressFill">0%</div>
</div>
<div class="status" id="status">Инициализация...</div>
</div>
<div id="successMsg" class="success"></div>
<div id="errorMsg" class="error"></div>
<script>
function submitForm() {
const dateFrom = document.getElementById('dateFrom').value;
if (!dateFrom) { alert('Выберите дату'); return; }
document.getElementById('formSection').style.display = 'none';
document.getElementById('progress').style.display = 'block';
updateProgress(5, 'Подключение к API...');
google.script.run
.withSuccessHandler(onSuccess)
.withFailureHandler(onError)
.getOrders(dateFrom);
// Симуляция долгого прогресса
setTimeout(() => updateProgress(15, 'Запрос заказов...'), 2000);
setTimeout(() => updateProgress(30, 'Обработка первой партии...'), 5000);
setTimeout(() => updateProgress(50, 'Ожидание (61 сек между запросами)...'), 10000);
setTimeout(() => updateProgress(70, 'Загрузка продолжается...'), 70000);
}
function updateProgress(percent, status) {
document.getElementById('progressFill').style.width = percent + '%';
document.getElementById('progressFill').textContent = percent + '%';
document.getElementById('status').textContent = status;
}
function onSuccess(result) {
updateProgress(100, 'Готово!');
const msg = document.getElementById('successMsg');
msg.textContent = '✅ Успешно загружено! Данные находятся в листе "Заказы"';
msg.style.display = 'block';
setTimeout(() => google.script.host.close(), 2000);
}
function onError(error) {
document.getElementById('progress').style.display = 'none';
document.getElementById('formSection').style.display = 'block';
const msg = document.getElementById('errorMsg');
msg.textContent = '❌ Ошибка: ' + error.message;
msg.style.display = 'block';
}
</script>
`).setWidth(450).setHeight(450);
ui.showModalDialog(html, '🚚 Заказы');
}
function getOrders(dateFrom) {
const token = getApiToken();
try {
const sheet = getOrCreateSheet(CONFIG.SHEETS.ORDERS);
const headers = ['Дата', 'Последнее изменение', 'Склад', 'Артикул', 'nmId', 'Штрихкод',
'Цена', 'Скидка %', 'СПП', 'Итоговая цена', 'Цена со скидкой', 'Отменён', 'srid'];
formatSheetHeaders(sheet, headers);
let currentDateFrom = `${dateFrom}T00:00:00`;
let allRows = [];
while (true) {
const url = `${CONFIG.STATISTICS_API}/api/v1/supplier/orders?dateFrom=${encodeURIComponent(currentDateFrom)}&flag=0`;
const response = fetchWithRetry(url, {
method: 'get',
headers: { 'Authorization': token },
muteHttpExceptions: true
});
const orders = JSON.parse(response.getContentText());
if (!orders.length) break;
const rows = orders.map(o => [
o.date, o.lastChangeDate, o.warehouseName, o.supplierArticle, o.nmId, o.barcode,
o.totalPrice, o.discountPercent, o.spp, o.finishedPrice, o.priceWithDisc, o.isCancel, o.srid
]);
allRows.push(...rows);
currentDateFrom = orders[orders.length - 1].lastChangeDate;
Logger.log(`Загружено заказов: ${allRows.length}`);
Utilities.sleep(CONFIG.RATE_LIMIT_WAIT_MS);
}
if (allRows.length > 0) {
sheet.getRange(2, 1, allRows.length, headers.length).setValues(allRows);
}
addUpdateTimestamp(sheet);
SpreadsheetApp.getActiveSpreadsheet().setActiveSheet(sheet);
return { success: true, count: allRows.length };
} catch (error) {
logError(error);
throw error;
}
}
// ═══════════════════════════════════════════════════════════════════════
// 7. ПРОДАЖИ И ВОЗВРАТЫ
// ═══════════════════════════════════════════════════════════════════════
function showSalesDialog() {
const ui = SpreadsheetApp.getUi();
const token = getApiToken();
if (!token) {
ui.alert('⚠️ Токен не найден', 'Настройте токен через меню: WB API → Настроить токен API', ui.ButtonSet.OK);
return;
}
const monthAgo = new Date(new Date().getTime() - 30 * 24 * 60 * 60 * 1000);
const dateFromStr = Utilities.formatDate(monthAgo, Session.getScriptTimeZone(), 'yyyy-MM-dd');
const html = HtmlService.createHtmlOutput(`
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
button { background-color: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; width: 100%; }
button:disabled { background-color: #cccccc; }
.info { background-color: #fff3cd; padding: 10px; border-radius: 4px; margin-bottom: 15px; border-left: 4px solid #ffc107; }
#progress { display: none; margin-top: 20px; }
.progress-bar { width: 100%; height: 25px; background-color: #f0f0f0; border-radius: 12px; overflow: hidden; }
.progress-fill { height: 100%; background: linear-gradient(90deg, #4285F4, #34A853); width: 0%; transition: width 0.5s; text-align: center; line-height: 25px; color: white; font-size: 12px; font-weight: bold; }
.status { margin-top: 10px; padding: 8px; background: #f8f9fa; border-radius: 4px; font-size: 13px; }
.success { background: #d4edda; color: #155724; padding: 15px; border-radius: 6px; margin-top: 15px; display: none; }
.error { background: #f8d7da; color: #721c24; padding: 15px; border-radius: 6px; margin-top: 15px; display: none; }
</style>
<div class="info">⚠️ Между запросами пауза 61 секунда (ограничение API). Загрузка может занять продолжительное время.</div>
<div id="formSection">
<div class="form-group">
<label>Дата начала:</label>
<input type="date" id="dateFrom" value="${dateFromStr}">
</div>
<button onclick="submitForm()" id="submitBtn">📥 Загрузить данные</button>
</div>
<div id="progress">
<div class="progress-bar">
<div class="progress-fill" id="progressFill">0%</div>
</div>
<div class="status" id="status">Инициализация...</div>
</div>
<div id="successMsg" class="success"></div>
<div id="errorMsg" class="error"></div>
<script>
function submitForm() {
const dateFrom = document.getElementById('dateFrom').value;
if (!dateFrom) { alert('Выберите дату'); return; }
document.getElementById('formSection').style.display = 'none';
document.getElementById('progress').style.display = 'block';
updateProgress(5, 'Подключение к API...');
google.script.run
.withSuccessHandler(onSuccess)
.withFailureHandler(onError)
.getSales(dateFrom);
// Симуляция долгого прогресса
setTimeout(() => updateProgress(15, 'Запрос продаж и возвратов...'), 2000);
setTimeout(() => updateProgress(30, 'Обработка первой партии...'), 5000);
setTimeout(() => updateProgress(50, 'Ожидание (61 сек между запросами)...'), 10000);
setTimeout(() => updateProgress(70, 'Загрузка продолжается...'), 70000);
}
function updateProgress(percent, status) {
document.getElementById('progressFill').style.width = percent + '%';
document.getElementById('progressFill').textContent = percent + '%';
document.getElementById('status').textContent = status;
}
function onSuccess(result) {
updateProgress(100, 'Готово!');
const msg = document.getElementById('successMsg');
msg.textContent = '✅ Успешно загружено! Данные находятся в листе "Продажи"';
msg.style.display = 'block';
setTimeout(() => google.script.host.close(), 2000);
}
function onError(error) {
document.getElementById('progress').style.display = 'none';
document.getElementById('formSection').style.display = 'block';
const msg = document.getElementById('errorMsg');
msg.textContent = '❌ Ошибка: ' + error.message;
msg.style.display = 'block';
}
</script>
`).setWidth(450).setHeight(450);
ui.showModalDialog(html, '💰 Продажи и возвраты');
}
function getSales(dateFrom) {
const token = getApiToken();
try {
const sheet = getOrCreateSheet(CONFIG.SHEETS.SALES);
const headers = ['Дата продажи', 'Последнее изменение', 'Склад', 'Страна', 'Округ', 'Регион',
'Артикул', 'nmId', 'Штрихкод', 'Категория', 'Предмет', 'Бренд', 'Размер',
'Цена без скидки', 'Скидка продавца', 'Продажа', 'К перечислению', 'Тип документа', 'srid'];
formatSheetHeaders(sheet, headers);
let currentDateFrom = `${dateFrom}T00:00:00`;
let allRows = [];
while (true) {
const url = `${CONFIG.STATISTICS_API}/api/v1/supplier/sales?dateFrom=${encodeURIComponent(currentDateFrom)}&flag=0`;
const response = fetchWithRetry(url, {
method: 'get',
headers: { 'Authorization': token },
muteHttpExceptions: true
});
const sales = JSON.parse(response.getContentText());
if (!sales.length) break;
const rows = sales.map(s => [
s.date, s.lastChangeDate, s.warehouseName, s.countryName, s.oblastOkrugName, s.regionName,
s.supplierArticle, s.nmId, s.barcode, s.category, s.subject, s.brand, s.techSize,
s.totalPrice, s.discountPercent, s.isSupply, s.isRealization, s.promoCodeDiscount, s.srid
]);
allRows.push(...rows);
currentDateFrom = sales[sales.length - 1].lastChangeDate;
Logger.log(`Загружено продаж: ${allRows.length}`);
Utilities.sleep(CONFIG.RATE_LIMIT_WAIT_MS);
}
if (allRows.length > 0) {
sheet.getRange(2, 1, allRows.length, headers.length).setValues(allRows);
}
addUpdateTimestamp(sheet);
SpreadsheetApp.getActiveSpreadsheet().setActiveSheet(sheet);
return { success: true, count: allRows.length };
} catch (error) {
logError(error);
throw error;
}
}
// ═══════════════════════════════════════════════════════════════════════
// ДОПОЛНИТЕЛЬНЫЕ ФУНКЦИИ ДЛЯ БЫСТРОГО ТЕСТИРОВАНИЯ
// ═══════════════════════════════════════════════════════════════════════
/**
* Быстрая загрузка всех основных отчётов
*/
function loadAllReports() {
const ui = SpreadsheetApp.getUi();
const result = ui.alert(
'Загрузить все отчёты?',
'Будут загружены: Карточки товаров, Склады, Заказы за месяц.\nЭто может занять несколько минут.',
ui.ButtonSet.YES_NO
);
if (result === ui.Button.YES) {
try {
getProductCards();
Utilities.sleep(2000);
getWarehouses();
Utilities.sleep(2000);
const monthAgo = Utilities.formatDate(
new Date(new Date().getTime() - 30 * 24 * 60 * 60 * 1000),
Session.getScriptTimeZone(),
'yyyy-MM-dd'
);
getOrders(monthAgo);
ui.alert('✅ Успешно!', 'Все отчёты загружены', ui.ButtonSet.OK);
} catch (error) {
ui.alert('❌ Ошибка', error.message, ui.ButtonSet.OK);
}
}
}
/**
* Очистка всех листов с отчётами
*/
function clearAllReports() {
const ui = SpreadsheetApp.getUi();
const result = ui.alert(
'Очистить все отчёты?',
'Все данные в листах отчётов будут удалены.',
ui.ButtonSet.YES_NO
);
if (result === ui.Button.YES) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
Object.values(CONFIG.SHEETS).forEach(sheetName => {
const sheet = ss.getSheetByName(sheetName);
if (sheet) sheet.clear();
});
ui.alert('✅ Готово', 'Все отчёты очищены', ui.ButtonSet.OK);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment