134 lines
10 KiB
Markdown
134 lines
10 KiB
Markdown
# Commerce, МойСклад и маркетплейсы
|
||
|
||
Сводные отчёты продаж, сотрудников и производства описаны в
|
||
[руководстве по аналитике компании](COMPANY_ANALYTICS.md).
|
||
|
||
Commerce-контур хранит нормализованные товары, внешние привязки, продажи,
|
||
возвраты, расходы, остатки и локальные изображения товаров. Источником истины
|
||
для структуры данных служат модели `backend/commerce/models/`, для HTTP API —
|
||
`backend/api/v1/commerce/`, для импорта — provider-specific services в
|
||
`backend/commerce/services/`.
|
||
|
||
## Импорт
|
||
|
||
Пользовательский раздел «Товары» временно скрыт для дальнейшей разработки:
|
||
пункт меню и выдача товаров в глобальном поиске отключены, адреса `/products`
|
||
и вложенные страницы перенаправляют в ленту. Код экранов, данные и HTTP API
|
||
сохранены; финансовая аналитика продолжает использовать импортированные факты.
|
||
|
||
Управление доступно в Django admin → «Подключения интеграций»
|
||
(`/admin/commerce/integrationconnection/`). Здесь можно добавить поддерживаемый
|
||
сервис, заменить реквизиты, настроить автосинхронизацию и интервал. Пустые поля
|
||
реквизитов сохраняют прежние значения; сохранённые секреты не показываются.
|
||
Для проверки и ручного импорта отметьте подключение в списке и выберите
|
||
«Проверить выбранные подключения» или «Синхронизировать выбранные подключения».
|
||
Эти действия требуют права изменения подключений. Повторный импорт во время
|
||
активного запуска не создаёт новую задачу.
|
||
|
||
Прогресс и ошибки доступны в «Запусках синхронизации»
|
||
(`/admin/commerce/integrationsyncrun/`). Проекты и выплаты по проектам также
|
||
доступны в админке для просмотра; исправления импортированных документов
|
||
вносятся в сервисе-источнике. Новые провайдеры требуют реализации коннектора;
|
||
добавление одной записи источника не создаёт интеграцию автоматически.
|
||
|
||
Синхронизация любого подключения запускается Celery-задачей
|
||
`commerce.sync_connection_run`; сервис выбирается по `connection.provider`.
|
||
Задача публикуется только после commit транзакции, а заранее сформированный
|
||
`task_id` до публикации сохраняется и в подключении, и в истории запуска.
|
||
Долгие импорты направляются в очередь `commerce`: worker должен слушать её
|
||
отдельно либо вместе с очередью `celery`. Для работы ручного импорта нужен
|
||
Celery worker, для расписания и восстановления зависших запусков — также Celery
|
||
Beat.
|
||
|
||
Импорт МойСклад выполняет следующие этапы:
|
||
|
||
1. `catalog` — загружает и идемпотентно нормализует ассортимент.
|
||
2. `images` — синхронизирует локальные изображения.
|
||
3. `variant_links` — восстанавливает связи модификаций с родителями.
|
||
4. `stocks` — пакетно обновляет остатки по складам.
|
||
5. `sales` и `returns` — импортирует отгрузки и возвраты с позициями.
|
||
6. `finalizing` — сохраняет итоговую статистику и состояние подключения.
|
||
|
||
На каждом длительном этапе сохраняются `stage`, `stage_processed`,
|
||
`stage_total` и `last_progress_at`; обновление отправляется frontend через
|
||
общий WebSocket. Ошибка отдельного изображения учитывается в `image_errors`,
|
||
но не отменяет импорт товарных и финансовых данных.
|
||
|
||
Celery Beat раз в минуту выполняет reconciliation. Запуски, слишком долго
|
||
оставшиеся в `queued`, и `running` без свежего heartbeat переводятся в `error`
|
||
без удаления истории и без автоматического повторного выполнения. Таймауты
|
||
задаются `COMMERCE_QUEUED_STALE_SECONDS` и
|
||
`COMMERCE_RUNNING_STALE_SECONDS`. Ручная безопасная проверка:
|
||
|
||
Долгие импорты имеют отдельные лимиты Celery:
|
||
`COMMERCE_TASK_SOFT_TIME_LIMIT` (по умолчанию 90 минут) и
|
||
`COMMERCE_TASK_TIME_LIMIT` (по умолчанию 120 минут). Мягкий лимит всегда должен
|
||
быть меньше жёсткого. `SoftTimeLimitExceeded` не считается локальной ошибкой
|
||
изображения: запуск завершается с явным сообщением о превышении времени.
|
||
|
||
```bash
|
||
python manage.py reconcile_commerce_sync_runs --dry-run
|
||
python manage.py reconcile_commerce_sync_runs
|
||
```
|
||
|
||
## Wildberries
|
||
|
||
Подключение принимает API-токен с категорией «Финансы». Проверка токена читает
|
||
`GET https://common-api.wildberries.ru/api/v1/seller-info`, сохраняет ID,
|
||
название магазина и ИНН без сохранения токена в открытом виде, а затем вызывает
|
||
`GET https://finance-api.wildberries.ru/ping`, чтобы подтвердить доступ именно
|
||
к финансовой категории.
|
||
|
||
Финансовая синхронизация использует актуальный
|
||
`POST https://finance-api.wildberries.ru/api/finance/v1/sales-reports/detailed`.
|
||
Отключённый 15 июля 2026 года legacy-метод `reportDetailByPeriod` не используется.
|
||
Каждая строка отчёта сохраняется как `SourceRecord`, затем идемпотентно создаёт
|
||
или обновляет `Sale`, `SaleLine` и связанные `Cost`. Поддерживаются продажи,
|
||
возвраты, комиссия, эквайринг, доставка, хранение, приёмка, удержания, штрафы и
|
||
расходы программы лояльности.
|
||
|
||
Товар сопоставляется сначала по существующей WB-привязке, затем по артикулу и
|
||
штрихкоду. Неоднозначные строки сохраняются, но не привязываются к случайному
|
||
товару; их количество видно в `unmatched_products`. Повторный импорт после
|
||
ручной привязки дополняет аналитику.
|
||
|
||
Один ответ ограничен 100 000 строками. После каждой непустой страницы `rrdId` и
|
||
границы периода сохраняются в metadata источника, а следующий запуск продолжает
|
||
импорт. Завершённым отчёт считается только после ответа WB `204`; длина страницы
|
||
не используется как признак конца. После завершения первичной истории каждый
|
||
запуск повторно проверяет последние 30 дней, чтобы корректировки финансового
|
||
отчёта обновили существующие факты.
|
||
|
||
## Изображения
|
||
|
||
`ProductImage` хранит оригинал и миниатюру в `MEDIA_ROOT/commerce/products/`.
|
||
Временные внешние ссылки МойСклад не возвращаются клиенту и не используются как
|
||
публичные URL. Повторная синхронизация сравнивает внешний размер и время
|
||
изменения, поэтому неизменённое изображение не скачивается повторно. Удалённые
|
||
в источнике изображения удаляются локально после успешного чтения списка.
|
||
Изображения сверх безопасного лимита декодирования уменьшаются изолированным
|
||
процессом ImageMagick до допустимого размера, не увеличивая лимит Pillow в
|
||
процессе worker. Ошибка одного файла оставляет остальные изображения товара и
|
||
весь импорт работоспособными.
|
||
|
||
REST `ProductSerializer` возвращает `images[]` с локальными `url` и
|
||
`thumbnail_url`. Каталог использует миниатюру, карточка товара — оригинал для
|
||
полноэкранного просмотра.
|
||
|
||
## Realtime и fallback
|
||
|
||
Событие `commerce_connection_updated` содержит состояние подключения и
|
||
последнего запуска, включая этап, heartbeat, прогресс и счётчики. При рабочем
|
||
WebSocket frontend не опрашивает connections endpoint. Если WebSocket
|
||
недоступен во время активного импорта, включается REST fallback раз в 30 секунд.
|
||
|
||
## Проверка
|
||
|
||
```bash
|
||
cd backend
|
||
pytest tests/commerce -q
|
||
python manage.py makemigrations --check --dry-run
|
||
python manage.py check
|
||
ruff check commerce api/v1/commerce tests/commerce
|
||
```
|