AI Asist
v2
Д
Д

Плейбук: отгрузка (догрузка) на Ozon FBO — Siluetta Ozon (shop 4)

← Заметки  ·  Раздел: Ozon  ·  Файл: data/notes/ozon/supply-playbook.md

Плейбук: отгрузка (догрузка) на Ozon FBO — Siluetta Ozon (shop 4)

Срабатывает, когда пользователь пишет «Хочу сделать отгрузку на Озон», «догрузить товары на озон», «создать поставку озон», «фулфилмент подготовил товары» и т.п.

Это рабочая инструкция под этот стек: данные в корневом mp.db, магазин shop_id = 4 (Siluetta Ozon), интерпретатор .venv/bin/python (Linux) или .venv\Scripts\python.exe (Windows), генератор scripts/ozon_supply_<DATE>.py. Теория Ozon (локальность, наценки, состав кластеров) — в data/knowledge/ozon/ и в Stock-Ozon-Orders-Sales/docs/supply-playbook.md.

Принцип (теория локальности): возим туда, где спрос был, а локального товара нет/мало. Нелокальная продажа всё равно числится в кластере покупателя → cluster_to_name = истинный гео-спрос, даже если товара там не было. Догрузка дефицита поднимает долю локальных продаж и режет переплату за логистику.


Модель данных (mp.db, всё с shop_id = 4)

Таблица Что Ключевые поля
ozon_postings Заказы posting_number, status, created_at, cluster_to_name (кластер покупателя = спрос)
ozon_posting_items SKU в заказах posting_number, offer_id, name, quantity
ozon_stocks_snapshot Остатки FBO по складам snapshot_at, offer_id, warehouse_name, free_to_sell, promised
ozon_warehouses Склад → кластер name (склад), cluster_name
ozon_clusters Справочник кластеров name
  • Join заказов: ozon_postings p JOIN ozon_posting_items pi ON pi.posting_number = p.posting_number AND pi.shop_id = p.shop_id.
  • offer_id = АРТИКУЛ-цвет-Размер, напр. ELS04-black-S, MS-07-black-M. Цвет lowercase, размер UPPERCASE. Регистр обязан совпадать с карточкой Ozon, иначе Excel не подцепится.
  • Склад→кластер берём из ozon_warehouses, НЕ из xlsx-справочника data/knowledge/ozon/ klastery-dostavki_v2.xlsx — тот неполный (нет Ростова, Новосибирска и др.; на прошлой задаче из-за него Ростову/Новосибу ложно показался сток=0). xlsx — только людям для справки.

Актуальные правила учёта (проверено 14.07.2026)

  • Основной источник для действующего SKU — Seller API POST /v1/analytics/stocks: он даёт ads_cluster за 28 дней, доступный и готовящийся товар, заявки и FBO-транзит. Локальная БД — контрольный источник и fallback.
  • Ответ /v1/analytics/stocks может содержать несколько строк одного SKU × кластер по складам. Остатки суммируем, а ads_cluster берём один раз (MAX), не суммируем: ADS повторяется в строках складов.
  • Текущий учитываемый запас: available_stock_count + valid_stock_count. Входящий FBO-транзит: transit_stock_count; requested_stock_count учитываем только после подтверждения активной заявки через /v3/supply-order/list/v3/supply-order/get/v1/supply-order/bundle.
  • Analytics и supply-order показывают один и тот же inbound с разных сторон: supply-order используем для проверки статуса, состава и кластера, но не прибавляем его второй раз поверх уже учтённых requested_stock_count / transit_stock_count.
  • promised, reserved и клиентские статусы awaiting_* / deliveringне поставка продавца на FBO. Их нельзя прибавлять как входящий товар.
  • REPORTS_CONFIRMATION_AWAITING означает согласование актов после физической приёмки. Если снимок остатков сделан позже приёмки, такую поставку второй раз не прибавляем.
  • Не использовать для нового кода устаревающий /v2/analytics/stock_on_warehouses и старые /v1/draft/create/info, /v1/draft/timeslot/info, /v1/draft/supply/create (отключение было назначено на 16.03.2026). Актуальный flow: отдельные create-методы черновика + /v2/draft/*.

Шаги

Шаг 1. Вход — что подготовил фулфилмент

Получить от пользователя таблицу артикул + размер → количество (это объём к распределению, обычно весь отгружаем). Зафиксировать суммы по каждому SKU — это контроль в конце. Отметить новые артикулы (нет истории на Ozon) — для них шаг 6 особый.

Шаг 2. Спрос — строго offer_id × кластер

Для существующих SKU основной сигнал — ads_cluster из /v1/analytics/stocks. Агрегат всей категории нужен только для нового SKU без своей истории. Нельзя распределять размер S по общему спросу всех размеров: один размер может быть затоварен, другой — отсутствовать.

Если API недоступен, строим fallback по неотменённым заказам. Период уточняем у пользователя (типично 28–30 дней), окна блогеров/разовых акций исключаем или считаем отдельным сценарием.

SELECT pi.offer_id, p.cluster_to_name, SUM(pi.quantity) AS qty
FROM ozon_postings p
JOIN ozon_posting_items pi
  ON pi.posting_number = p.posting_number AND pi.shop_id = p.shop_id
WHERE p.shop_id = 4
  AND p.created_at >= '2026-05-25'              -- период/отсечка блогеров
  AND p.status NOT IN ('cancelled','canceled')
  AND pi.offer_id IN (...)                      -- только SKU текущей поставки
GROUP BY pi.offer_id, p.cluster_to_name
ORDER BY pi.offer_id, qty DESC;

Комбинезоны Siluetta = ELS04-* и MS-07-* (name «Комбинезон спортивный…»). skirt-* — юбка.

Шаг 3. Остатки и уже едущие поставки — offer_id × кластер

В API суммируем складские строки до уровня offer_id × cluster_name по правилам выше. Если используем локальный fallback, берём последний снимок и маппим склад через ozon_warehouses.

WITH snap AS (SELECT MAX(snapshot_at) m FROM ozon_stocks_snapshot WHERE shop_id=4)
SELECT s.offer_id, w.cluster_name, SUM(s.free_to_sell) AS stock
FROM ozon_stocks_snapshot s
JOIN ozon_warehouses w ON w.name = s.warehouse_name AND w.shop_id = s.shop_id
WHERE s.shop_id=4 AND s.snapshot_at=(SELECT m FROM snap)
  AND s.offer_id IN (...)
GROUP BY s.offer_id, w.cluster_name
ORDER BY s.offer_id, w.cluster_name;

Перед расчётом проверить время обновления. Для локального fallback сначала освежить снимок: .venv/bin/python scripts/ingest_ozon.py --shop-id 4 stocks.

Шаг 4. Целевой запас и дефицит

Считаем отдельно для каждого offer_id × кластер:

target = ads_cluster × (дни покрытия + lead time) × safety_factor
deficit = max(0, target − available − valid − подтверждённый inbound)

Рабочий default при отсутствии указаний: 30 дней покрытия + 10 дней на создание/доставку/ приёмку, safety_factor = 1.0. Параметры фиксируем в SUMMARY.md. Нельзя сначала сложить весь спрос и весь запас категории, а потом взять один общий max(...): избыток размера S не должен скрывать дефицит M.

Шаг 5. Выбор кластеров

До ранжирования исключаем направления, которые пользователь не готов обслуживать: закрытия, слишком долгий/дорогой рейс, ограничения перевозчика. Затем ранжируем по сумме положительных SKU-дефицитов, обычно берём 5–7 кластеров. Минимум на кластер уточняем (рабочий default — 20 шт). Склад хранения внутри кластера окончательно предлагает Ozon.

Если пользователь исключил кластер или Ozon не дал слот, запускаем расчёт заново без него. Нельзя просто перенести весь готовый файл в соседний кластер: размерная потребность отличается.

Шаг 6. Раскладка фактического прихода

  • Для каждого SKU распределяем только реально подготовленное количество. Если его меньше суммарного дефицита — делим по положительным дефицитам (Hamilton). Если больше — сначала закрываем дефицит, затем либо оставляем избыток на ФФ, либо, если весь объём надо отгрузить, выравниваем итоговые дни покрытия между выбранными кластерами (water-filling).
  • После округления сумма по SKU обязана точно совпасть с подготовленным количеством.
  • Новый артикул (истории нет): дефицит не посчитать → раскладываем по гео-паттерну спроса категории со сдвигом в крупнейший рынок (Москва — присутствие нового товара там, где спроса больше всего). Дальние/мелкие кластеры (Красноярск, ДВ) — по минимуму.
  • После округления проверяем минимальную партию по кластеру. Если она не выдержана, удаляем слабый кластер и пересчитываем всю матрицу.

Шаг 7. Проверка возможности поставки

  • Справочно проверить capacity через GET /v1/supplier/available_warehouses.
  • Точный принимающий склад и bookable-слот подтверждаются только свежим черновиком. Capacity не гарантирует слот.
  • Для чтения существующих заявок безопасны /v3/supply-order/list, /v3/supply-order/get, /v1/supply-order/bundle. Создание/изменение черновика или заявки — внешняя мутация; без явного указания пользователя её не выполнять.
  • Локальное зеркало Dev-API-Ozon проверять по LAST_UPDATE.txt: версии методов могли измениться позже даты зеркала.

Шаг 8. Генерация Excel — один файл = один кластер

Формат Ozon (строго): заголовки lowercase артикул | имя (необязательно) | количество, колонка «имя» пустая (Ozon подтянет из карточки), количество — целое > 0. Скопировать прошлый генератор scripts/ozon_supply_2026-06-16.py, заменить ALLOC / SUPPLY_TOTALS / DATE. Встроенный validate() упадёт, если суммы по колонкам ≠ заявке.

.venv/bin/python scripts/ozon_supply_<DATE>.py

Выход: data/ozon_supply/Siluetta_ozon_supply_<DATE>/*.xlsx + ZIP + консольная сводка.

Шаг 9. Контроль и выдача

  • Σ по строкам = заявке (по каждому SKU). ✔ автоматически в validate().
  • Σ по столбцам = объём кластера; общий ИТОГО = весь приход.
  • Регистр offer_id новых артикулов сверен с карточкой ЛК.
  • В ответе пользователю: таблица раскладки + кликабельные ссылки на файлы, SUMMARY.md рядом.

Стартовый слепок кластеров (новый товар / нет своих данных)

Когда у магазина/артикула нет своей истории на Ozon (новый товар, новый магазин), гео-спрос берём по родственному магазину-референсу. Референс — shop 2 «Мой Ozon» (Shakti), у него есть история заказов.

Слепок ниже = доли кластеров в спросе Shakti за 2026-03-19 … 2026-05-23 (1034 ед, 24 кластера; в БД пока только этот период — не с декабря). Обновлять при накоплении данных или при ингесте более длинной истории.

# Кластер доля накопл. наценка* Замена, если не даёт слот
1 Москва, МО и Дальние регионы 20.7% 20.7% 8% Тверь / Ярославль (сам почти не закрывается)
2 Санкт-Петербург и СЗО 11.7% 32.4% 8% Ярославль / Калининград
3 Ростов 9.5% 41.9% 0% Краснодар / Саратов
4 Дальний Восток 9.1% 51.0% 0% Новосибирск / Красноярск
5 Новосибирск 5.7% 56.7% 0% Екатеринбург / Красноярск
6 Красноярск 4.6% 61.3% 0% Новосибирск / Екатеринбург
7 Казань 4.4% 65.7% 8% Самара / Уфа

* ориентировочная наценка за нелокальность (см. Stock-Ozon-Orders-Sales/docs/supply-playbook.md).

Замены — географически близкий, НЕ закрытый кластер с похожей нишей покупателей. Берём, если основной кластер в исключениях или Ozon не даёт слот. Способ: либо отдельный Excel на замену, либо файл с несколькими вкладками (как Замена_Екатеринбурга_Уфа_Самара_Тюмень.xlsx). Прим.: Иркутск/Бурятия/Хакасия отдельным кластером не идут — они внутри «Красноярска».

Рекомендация для нового товара: брать топ-5 (Москва, СПб, Ростов, ДВ, Новосибирск = 57% спроса) или топ-7 (+Красноярск, Казань = 66%). Москва+СПб — всегда; Ростов/ДВ/ Новосибирск/Красноярск — 0%-наценка, хороши для присутствия. Распределять объём по долям (Hamilton). Когда у самого товара появится история — переходить на его дефицитную раскладку (шаги 2–4), слепок нужен только для холодного старта.

Пересчёт слепка:

SELECT p.cluster_to_name, SUM(pi.quantity) qty
FROM ozon_postings p JOIN ozon_posting_items pi
  ON pi.posting_number=p.posting_number AND pi.shop_id=p.shop_id
WHERE p.shop_id=2 AND p.status NOT IN ('cancelled','canceled')
GROUP BY p.cluster_to_name ORDER BY qty DESC;

Чек-лист

  1. /v1/analytics/stocks или локальный fallback свежий?
  2. ☐ ADS взят один раз, а остатки сложены по складам на уровне SKU × кластер?
  3. promised/reserved не приняты за inbound; активные supply orders проверены?
  4. ☐ Горизонт, lead time и safety factor зафиксированы?
  5. ☐ Пользовательские исключения применены до ранжирования?
  6. ☐ Минимум на кластер выдержан после полного пересчёта?
  7. ☐ Новые артикулы — по гео-спросу категории, существующие — по своему SKU-дефициту?
  8. ☐ Excel: 3 колонки lowercase, имя пустое, регистр offer_id как в карточке?
  9. ☐ Суммы по строкам/столбцам сходятся (validate прошёл)?
  10. ☐ SUMMARY.md содержит источник, дату, формулу, исключения и входящий транзит?

Вопросы, которые задаём пользователю ДО расчёта

  1. Что и сколько пришло на ФФ (артикул+размер→кол-во)? Какие из них новые?
  2. Какой горизонт покрытия и были ли блогеры/акции, искажающие обычный спрос?
  3. Минимум на кластер и сколько кластеров (5/6/7)?
  4. Какие направления исключить из-за срока/цены доставки или других ограничений?
  5. Весь приход обязательно отправить на FBO или избыток можно оставить на ФФ?
  6. Есть ли уже созданные поставки вне API/другим подрядчиком или особые смещения под акции?
Фильтры