Created
October 16, 2025 21:37
-
-
Save daniil-burdygin/d101d9723f30ab347b05aaffed9b6daf to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * ════════════════════════════════════════════════════════════════════════ | |
| * 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); | |
| } | |
| } |
Начиная со строчки Максимальное количество попыток выдает Unexpected idetnifier "КОЛИЧЕСТВО" и всё, ничего не сохраняется
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
собственно без проблем не обошлось. при желании догрузить некоторые данные, скрипт перезаписывает данные затирая предыдущие и жалуется на наличие фильтров....
api от 29 января но некоторые данные отдаются от 4 мая 25 года?!?!