Вводные: от рутины к инструменту

Я работаю аналитиком по потерям на складе одного крупного маркетплейса. Типовая задача: быстро понять, какой товар “завис”, какой скоро уйдёт на списание, и куда смотреть в первую очередь, чтобы снижать потери.

До автоматизации процесс выглядел так:

  1. Берём Excel-выгрузки из внутренних систем.

  2. Собираем сводные таблицы в Excel.

  3. Приводим формат, фильтруем, считаем суммы/количество.

  4. Выгружаем результат в Google Sheets (чтобы коллегам было удобно смотреть).

  5. Руками проверяем повторы и корректность группировок.

На один цикл уходило около двух часов времени, а ещё периодически всплывали “мелкие” ошибки: повторяющиеся позиции, неверные фильтры, случайные сдвиги диапазонов, забытый формат даты.

Я поймал себя на мысли: мы не решаем задачу аналитики — мы обслуживаем процесс выгрузки.

Так родилась идея: сделать бота, который берёт выгрузки, сам считает нужные агрегации и кладёт результат в Google Sheets в готовом виде.

Как раньше выглядела сводная Excel (некоторые данные скрыты)
Как раньше выглядела сводная Excel (некоторые данные скрыты)

Я сформулировал “MVP-требования”, чтобы не закопаться:

  • В Telegram выбираю режим (какой файл обрабатываем).

  • Бот принимает Excel, обрабатывает и выгружает в Google Sheets.

  • Всё логируется: кто отправил, какой файл, что получилось.

  • Никаких ручных сводных таблиц.

  • Дополнительная ценность: отдельный блок “что спишется в течение 24 часов”, чтобы контролировать срочные потери.

Проект оказался не только автоматизацией одной рутины, но и новым инструментом: он помогает сразу видеть приоритет по списаниям и быстрее принимать решения.


Архитектура: почему Telegram и Google Sheets

Почему Telegram:

  • Он уже у всех есть.

  • Не нужно UI/веб-приложение.

  • Можно встроить “процесс” в привычный мессенджер.

Почему Google Sheets:

  • Удобно для совместного доступа.

  • Не нужно поднимать BI, если задача — оперативный контроль.

  • Таблицу можно расширять (дашборды, фильтры, условное форматирование).


Первая версия: один файл, один сценарий

Начинал я с одного режима: “выгрузка товара без движения”.

Выгрузка содержит записи по товарам. Нам было важно получить таблицу примерно в таком виде:

  • Гофра / ID тары

  • Идентификатор товара

  • Количество

  • Стоимость

Причём “Идентификатор товара” внутри одной гофры хотелось видеть списком (через переносы строк), чтобы можно было быстро копировать и искать повторяющиеся позиции.

Бизнес-логика агрегирования (упрощённо)

  • Группируем по ID тары.

  • Для каждой тары:

    • список идентификаторов товара (join через \n)

    • количество строк (count)

    • сумма стоимости (sum)


Быстро выяснилось: всё упирается в доставку файла

Telegram хорош, пока файлы маленькие. Но в реальности Excel-выгрузки иногда бывают > 20 МБ и Telegram отказывает: BadRequest: File is too big.

Сначала я хотел сделать скачивание по ссылке с Яндекс.Диска — и тут вылезла проблема SmartCaptcha/ограничений для публичных ссылок.

Решение оказалось инженерно простым и надёжным: OAuth и доступ к Диску через API.


Итерация 2: Яндекс.Диск OAuth и “берём последний файл из папки”

Я сделал сценарий “бот как инструмент”, а не “бот как файлообменник”:

  • На Яндекс.Диске есть две папки:

    • /BOT_UPLOADS/no_move/

    • /BOT_UPLOADS/24h/

  • Сотрудники загружают файлы туда (через общий доступ по приглашению).

  • Бот по OAuth берёт последний загруженный файл из нужной папки и обрабатывает.

Плюсы:

  • Telegram больше не лимитирует размер файлов.

  • Не нужно пересылать файлы в чат (а с РФ это иногда и правда проблемно).

  • Процесс становится предсказуемым: “закинул на диск → нажал кнопку → получил результат”.

Структура Я.Диска
Структура Я.Диска

Интерфейс бота: минимум кнопок — максимум ясности

В Telegram я сделал постоянную клавиатуру:

  • 📦 Без движения

  • ⏱ 24 часа (обновить)

  • ☁️ Взять с Я.Диска (последний файл)

  • 🛠 Админ-панель (только для admin user_id)

Логика работы простая:

  1. выбираешь режим (какой файл мы ждём)

  2. либо отправляешь Excel (если маленький), либо жмёшь “взять с диска”

Важный UX момент: если пользователь отправляет файл “не выбрав режим”, бот не падает и не пытается угадывать — он просит выбрать режим.

Telegram интерфейс
Telegram интерфейс

Две выгрузки на одном листе: как я спроектировал Google Sheets

Изначально хотелось, чтобы всё было “на одном экране”:

  • слева: “Без движения”

  • справа: “Списание в течение 24 часов”

  • сверху: аккуратные заголовки

  • справа ещё блок “Актуальность файла 24ч” (дата/время последнего обновления)

Схематично:

  • Левая зона: B2:E…

  • Правая зона: K2:O…

  • Метаданные актуальности: P3:P4

Так таблица превращается в инструмент: зашёл — сразу видишь “что висит” и “что срочно спишется”.

Вид итоговой таблицы (некоторые данные скрыты)
Вид итоговой таблицы (некоторые данные скрыты)

Итерация 3: второй файл — “спишется в течение 24 часов”

С файлом “24 часа” были нюансы:

  1. Там встречаются пустые ячейки.

  2. В нём данные по всем складам, а мне нужно только определённые блоки (whitelist по “ID Блока”).

  3. В нём почти всегда пустая “гофра/ID тары”.

Третий пункт особенно важный: если в файле 24ч ID тары пустой, то группировать по нему бессмысленно — получается одна гигантская группа “—”.

Значит ID тары нужно брать из выгрузки “без движения”.


Главная идея правого блока: “соединить два мира”

Получилась логика join’а:

  • “Без движения” даёт нам:

    • ID тары

    • список идентификаторов товара

  • “24 часа” даёт нам:

    • прогноз списания (дата/время)

    • стоимость

    • идентификатор товара (в этом файле он называется “Идентификатор товара”)

Что делаем:

  1. строим карту идентификатор товара -> ID тары из левой таблицы
    (там идентификаторы хранятся списком через \n, поэтому мы их разбиваем)

  2. берём snapshot “24 часа” (после whitelist фильтра по ID Блока)

  3. оставляем только те строки “24 часа”, чей идентификатор есть в карте

  4. присваиваем каждой строке “24 часа” ID тары из карты

  5. группируем по ID тары и считаем:

    • список идентификаторов товара

    • количество

    • сумма стоимости

    • когда начнёт списываться? — это MIN(прогноз) в группе
      (самый ранний товар, который уйдёт на списание)

И вот тут появилось то, чего в Excel сводными было достигать долго:
приоритизация по ближайшему списанию стала естественной.

Списания в течении 24ч. (некоторые данные скрыты)
Списания в течении 24ч. (некоторые данные скрыты)

Snapshot’ы: чтобы не зависеть от “сегодня загрузил / не загрузил”

Чтобы правый блок можно было строить даже если “без движения” обновляли не прямо сейчас, я добавил хранение:

  • last_24h_snapshot — обработанные данные по 24 часам (после whitelist)

  • last_no_move_map — карта идентификатор -> ID тары

В итоге процесс стал устойчивее: обновил 24ч — он запомнился. Обновил “без движения” — карта обновилась. А правый блок строится на пересечении.


Логи и админка: почему это важно

Любой бот, который используют несколько людей, должен объяснять, “что произошло”:

  • кто запросил выгрузку

  • какой режим

  • откуда взят файл (Telegram / Я.Диск)

  • имя файла, размер, время обработки

  • сколько строк/групп получилось

  • какие ошибки

Это сильно ускоряет поддержку: вместо “что-то не работает” есть конкретная точка.

Также важно: не логировать секреты (токены, ключи), максимум — префиксы, чтобы понять “какой токен подхватился”.

2026-03-03 01:39:56 INFO httpx: HTTP Request: POST https://api.telegram.org/bot[REDACTED]/getUpdates "HTTP/1.1 200 OK"
2026-03-03 01:39:56 INFO bot.handlers: Получен файл от user_id=*** username=*** filename=ШК без движения (2).xlsx size=1658962
2026-03-03 01:39:56 INFO httpx: HTTP Request: POST https://api.telegram.org/bot[REDACTED]/getFile "HTTP/1.1 200 OK"
2026-03-03 01:39:56 INFO httpx: HTTP Request: GET https://api.telegram.org/file/***/documents/file_68.xlsx "HTTP/1.1 200 OK"
2026-03-03 01:39:57 INFO httpx: HTTP Request: POST https://api.telegram.org/bot[REDACTED]/sendMessage "HTTP/1.1 200 OK"
2026-03-03 01:39:59 INFO httpx: HTTP Request: POST https://api.telegram.org/bot[REDACTED]/getUpdates "HTTP/1.1 200 OK"
2026-03-03 01:39:59 INFO httpx: HTTP Request: POST https://api.telegram.org/bot[REDACTED]/answerCallbackQuery "HTTP/1.1 200 OK"
2026-03-03 01:39:59 INFO httpx: HTTP Request: POST https://api.telegram.org/bot[REDACTED]/editMessageReplyMarkup "HTTP/1.1 200 OK"
2026-03-03 01:39:59 INFO httpx: HTTP Request: POST https://api.telegram.org/bot[REDACTED]/sendMessage "HTTP/1.1 200 OK"
2026-03-03 01:40:00 INFO httpx: HTTP Request: POST https://api.telegram.org/bot[REDACTED]/editMessageText "HTTP/1.1 200 OK"
2026-03-03 01:40:07 INFO root: Данные успешно загружены в Google Sheets. Строк: 56. Диапазон: D1:G56. Лист: Выгрузка ***
2026-03-03 01:40:07 INFO httpx: HTTP Request: POST https://api.telegram.org/bot[REDACTED]/editMessageText "HTTP/1.1 200 OK"
2026-03-03 01:40:07 INFO bot.handlers: Успешная загрузка: user_id=*** username=*** mode=export_with_transfers rows=56 source_rows=2320 duration=7.109s processing=0.575s upload=6.397s file=ШК без движения (2).xlsx size=1658962 unknown=1
2026-03-03 01:40:07 INFO bot.handlers: Удален временный файл: C:\Users\user\PyCharmMiscProject\Bot_Mont_SHK\uploaded_710953844_1772491196.xlsx
2026-03-03 01:40:09 INFO httpx: HTTP Request: POST https://api.telegram.org/bot[REDACTED]/getUpdates "HTTP/1.1 200 OK"

CI: чтобы не ломать всё одним коммитом

Проект живёт и меняется. Чтобы случайно не сломать зависимость/синтаксис, я сделал простой CI в GitHub Actions:

  • установка requirements.txt

  • python -m compileall .

  • ruff check по критичным ошибкам

Это минимально, но уже ловит “упал импорт”, “ошибка синтаксиса”, “битая зависимость”.


Результаты: что изменилось в работе

Самое заметное:

  • Ушёл час рутины на сводные и ручные проверки.

  • Процесс выгрузки стал “одна кнопка”.

  • Появился новый инструмент контроля:

    • “что зависло”

    • “что спишется в ближайшие 24 часа”

    • какой товар “горит” (min по времени списания)

Это не просто автоматизация: это улучшение управления потерями


С какими проблемами столкнулся по дороге

1) Лимиты Telegram на размер файлов

Решение: Яндекс.Диск + OAuth + “берём последний файл”.

2) SmartCaptcha на публичных ссылках Яндекс.Диска

Решение: не использовать публичные ссылки, только OAuth API.

3) Плавающие форматы Excel и пустые ячейки

Решение: аккуратное чтение, приведение типов, пропуски, логирование отброшенных строк.

4) “Токены внезапно перестали работать”

Решение: строгая проверка токена через GET /v1/disk, хранение токенов без кавычек, в идеале — авто-refresh по refresh_token.


Что можно улучшить дальше

Несколько идей “на будущее”:

  1. Дашборд в Google Sheets:

    • сколько и на какую сумму спишется по часам

    • график “нагрузка списаний” на ближайшие 24 часа

  2. Авто-refresh OAuth токена по refresh_token (чтобы не ловить 401).

  3. Архив выгрузок по датам (история изменений).

  4. Роли/клиенты (если делать “бот как сервис” для нескольких складов).


Вывод

Я начал с простой боли — «слишком много времени на сводные и ручную выгрузку».
А закончил инструментом, который:

  • автоматизирует сбор и обработку данных,

  • снижает ошибки,

  • делает контроль списаний прозрачнее,

  • и реально помогает эффективнее работать с потерями.

Код проекта