zov-tech/backend-py/app/drive.py
wasrusgen f5ee9e5b33 feat: warehouse module — ОТГРУЗКИ.xlsx в дашборде менеджера
- backend: новый модуль drive.py (Google Drive download + 5-мин кэш)
- backend: /api/shipments — читает xlsx из Drive, парсит листы «ЗОВ ДД.ММ.ГГ»,
  возвращает позиции (Заказ/Дозаказ) сгруппированные по дате отгрузки с завода
- config: поле shipments_file_id (SHIPMENTS_FILE_ID env; дефолт = ID ОТГРУЗКИ.xlsx)
- frontend: секция «📦 Отгрузки» на главной менеджера (после активных проектов),
  загружается параллельно с замерами и pending; показывает последние 3 партии
- CSS: стили .ship-group / .ship-row / .ship-badge / .ship-check
- deps: добавлен openpyxl>=3.1.0

ВАЖНО после деплоя: добавить сервис-аккаунт как Viewer к ОТГРУЗКИ.xlsx в Drive
и прописать SHIPMENTS_FILE_ID в /opt/zov-tech/deploy/.env на сервере.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 07:21:23 +03:00

65 lines
2.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Загрузка файлов из Google Drive через service account.
Использует google-api-python-client (уже в requirements.txt).
Scope drive.readonly — только чтение файлов, к которым у сервисного аккаунта есть доступ.
"""
from __future__ import annotations
import io
import threading
import time
from typing import Any
from googleapiclient.discovery import build # type: ignore
from googleapiclient.http import MediaIoBaseDownload # type: ignore
from google.oauth2.service_account import Credentials # type: ignore
from .config import get_config
_SCOPES = [
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/drive.readonly",
]
_lock = threading.Lock()
_service: Any = None
# Простой in-memory кэш: {file_id: (bytes, timestamp)}
_cache: dict[str, tuple[bytes, float]] = {}
_CACHE_TTL = 300 # 5 минут
def _get_service() -> Any:
global _service
with _lock:
if _service is None:
cfg = get_config()
creds = Credentials.from_service_account_file(
cfg.google_credentials_path, scopes=_SCOPES
)
_service = build("drive", "v3", credentials=creds, cache_discovery=False)
return _service
def download_file_bytes(file_id: str) -> bytes:
"""Скачивает файл из Google Drive по его ID и возвращает байты.
Кэширует результат на 5 минут, чтобы не качать xlsx на каждый запрос дашборда.
"""
now = time.monotonic()
cached = _cache.get(file_id)
if cached and (now - cached[1]) < _CACHE_TTL:
return cached[0]
service = _get_service()
request = service.files().get_media(fileId=file_id)
fh = io.BytesIO()
downloader = MediaIoBaseDownload(fh, request)
done = False
while not done:
_, done = downloader.next_chunk()
data = fh.getvalue()
_cache[file_id] = (data, now)
return data