Corp/docs/guides/COMMERCE.md

134 lines
10 KiB
Markdown
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.

# 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
```