Как считается прибыль и аналитика на WB — методичка
Единый подход к расчёту фин-отчёта и аналитики WB в Big-System-MP. Применимо к любому магазину (Мой WB, Siluetta WB, Tote WB и т.д.).
TL;DR — формула прибыли
прибыль = K_выплате − cost × net_qty − Логистика − Хранение − Реклама − Прочее
где (всё из fin_report_detail за окно):
K_выплате = SUM(for_pay) WHERE seller_oper_name='Продажа'
− SUM(for_pay) WHERE seller_oper_name='Возврат'
net_qty = SUM(quantity) WHERE seller_oper_name='Продажа'
− SUM(quantity) WHERE seller_oper_name='Возврат'
Логистика = SUM(delivery_rub)
Хранение = SUM(paid_storage)
Прочее = SUM(paid_acceptance + deduction + penalty + acquiring_fee − additional_payment)
− SUM(deduction WHERE bonus_type_name LIKE '%Продвижение%') ← см. ⚠ ниже
Реклама = SUM(sum_cost) FROM ad_metrics_daily WHERE nm_id IN (sku.nm_ids)
cost = costs.cost WHERE sku = '<sku>' AND marketplace='wb'
⚠ Реклама не должна задваиваться (deduction × ad_metrics_daily)
Реклама WB приходит в данные дважды:
1. ad_metrics_daily.sum_cost — дневной расход кампаний из отчёта по рекламе (/adv/v2/fullstats).
2. fin_report_detail.deduction со строкой bonus_type_name = «Оказание услуг "WB Продвижение", документ №…»
— акт списания за рекламу в отчёте реализации.
Это одна и та же реклама, просто WB показывает её и в рекламном отчёте, и удержанием в финрепе (суммы и периоды не совпадают 1:1 — акт выставляется своими датами/документами).
Правило: берём рекламу из ad_metrics_daily (точнее, по дням и nm_id), а из «Прочего» финрепа
исключаем deduction где bonus_type_name LIKE '%Продвижение%'. Иначе реклама вычитается дважды
и прибыль занижается. (Реальный кейс Tote WB, 01–15.06.2026: с задвоением −6 633 ₽, без — +13 367 ₽.)
⚠ В deduction бывают и другие крупные строки не относящиеся к операционной прибыли —
например «Перевод на баланс заёмщика … по кредиту …» (погашение кредита WB). Это финансовая
операция, а не расход бизнеса; в опер. прибыль её включать тоже не нужно (фильтровать отдельно при появлении).
⚠ Источники WB — два разных API, разные цифры
Это самый частый источник путаницы. WB отдаёт разные числа через разные эндпоинты — это не баг ingest, это дизайн WB.
| Источник | Endpoint | Что значит | Когда использовать |
|---|---|---|---|
| Statistics API | /api/v1/supplier/orders |
заказы которые ушли в логистику (после фильтрации фрода/быстрых отмен) | финансы, склады, прибыль |
| Statistics API | /api/v1/supplier/sales |
реально выкупленные товары (доставлены) | финансы, выкуп % |
| Seller-Analytics | /content/api/v2/analytics (Воронка) |
все нажатия «Заказать» в кабинете (включая мгновенные отмены) | маркетинг, конверсия, реальный спрос |
| Statistics API | /api/v5/supplier/reportDetailByPeriod |
отчёт о реализации — выплаты от WB | точный фин-расчёт |
| Ad API | /adv/v2/fullstats |
реклама per advert/nm | атрибуция рекламы |
| Costs | таблица costs |
себестоимость | вычитание из payout |
Ключевой кейс: 142 vs 166 (Tote WB, skirt-atlas-max-2)
Реальный пример из 06.06.2026, период 27.04-31.05:
| Метрика | Воронка | /supplier/orders |
Δ |
|---|---|---|---|
| Заказов | 166 | 142 | −24 |
| Отменено | 112 | 87 | −25 |
| Не отменено | 54 | 55 | ≈ |
| Выкуплено | 44 | 43 | ≈ |
Корень: /supplier/orders исключает быстро-отменённые заказы (отмена клиентом в первые минуты до отгрузки). Воронка их включает.
На прибыль это не влияет (быстрые отмены не несут логистических расходов). Влияет на: - Метрику «отказы %» - Метрику «конверсия из корзины в заказ» - Стоимость рекламы за заказ
Какие цифры брать для какой цели
| Цель | Источник | Какое поле |
|---|---|---|
| Финансы / ROI / прибыль | fin_report_detail |
for_pay, delivery_rub, paid_storage, etc. |
| Кол-во продаж (=выкупов) | sales или fin_report.Продажа |
sale_id LIKE 'S%' / qty |
| Кол-во возвратов | sales или fin_report.Возврат |
sale_id LIKE 'R%' / qty |
| Реальный спрос / конверсия | funnel_period или funnel_daily |
orders_count, cancel_count |
| Эффективность рекламы | funnel + ad_metrics_daily |
order_count vs sum_cost |
| Сток-планирование | stocks_snapshot |
quantity по складам |
Структура fin_report_detail (главная таблица WB)
Один день / SKU / WB-операция = одна строка.
Ключевые поля:
| Поле | Что |
|---|---|
| rrd_id | PK — ID отчётной строки WB (уникален) |
| period_start / period_end | границы отчёта (обычно неделя) |
| rr_date | дата выплатной транзакции |
| sale_dt / order_dt | даты заказа/продажи |
| sku | артикул продавца |
| tech_size | размер |
| seller_oper_name | тип операции: «Продажа», «Возврат», «Логистика», «Возмещение издержек по перевозке», «Хранение», «Штраф» и т.д. |
| quantity | штук |
| retail_amount | retail цена (то что заплатил покупатель) |
| for_pay | к выплате от WB (retail − комиссия − СПП-удержания) |
| commission_percent | комиссия WB в % |
| delivery_rub | логистика за эту строку |
| paid_storage | хранение |
| paid_acceptance | платная приёмка |
| deduction / penalty | удержания / штрафы |
| additional_payment | дополнительные выплаты (бонусы) |
| acquiring_fee | эквайринг |
Структура orders + sales
Schema:
- orders.srid — PK, sales reference ID (один заказ = один srid)
- orders.is_cancel — отменён ли (boolean)
- orders.date — когда создан
- sales.sale_id — PK, начинается с:
- S* — выкуп (продажа покупателю)
- R* — возврат
- D* — коррекция (доплата/возврат денег)
ИНГЕСТ: подводные камни
/supplier/orders — фильтр по lastChangeDate, не по date
Endpoint принимает dateFrom и возвращает заказы у которых lastChangeDate >= dateFrom. То есть:
- Заказ создан 20.04, выкуплен 05.05 → lastChangeDate=05.05 → попадёт если dateFrom ≤ 05.05
- Заказ создан 30.05, никогда не менялся → lastChangeDate=30.05 → попадёт если dateFrom ≤ 30.05
Рекомендация: при инкрементальном пуле всегда брать dateFrom = now − 60 дней чтобы не упустить заказы которые менялись поздно.
Финреп — квота 2 запроса в 24 часа
/api/v5/supplier/reportDetailByPeriod жёстко лимитирован WB на Базовом тарифе. Если перетянуть — ждать сутки. Безопасное окно: 7 дней раз в неделю (как WB сам присылает отчёты).
Воронка — квота ~2 запроса в час
/content/api/v2/analytics — Seller-Analytics. Чанкуем по 20 nm_ids × 7 дней между паузами 30 мин.
Cost-ключ
В отличие от Ozon, у WB обычно ключ costs.sku совпадает 1:1 с sku из orders/sales/fin_report — потому что у нас один артикул на одной площадке. Размер cost не различает (одна модель = одна цена).
Исключение: если cost зависит от размера — придётся ввести tech_size в costs.
Полный SQL расчёта прибыли по одному SKU
WITH fin AS (
SELECT
tech_size,
SUM(CASE WHEN seller_oper_name='Продажа' THEN quantity ELSE 0 END) AS sold,
SUM(CASE WHEN seller_oper_name='Возврат' THEN quantity ELSE 0 END) AS ret,
SUM(CASE WHEN seller_oper_name='Продажа' THEN retail_amount ELSE 0 END)
− SUM(CASE WHEN seller_oper_name='Возврат' THEN retail_amount ELSE 0 END) AS gross,
SUM(CASE WHEN seller_oper_name='Продажа' THEN for_pay ELSE 0 END)
− SUM(CASE WHEN seller_oper_name='Возврат' THEN for_pay ELSE 0 END) AS for_pay_net,
SUM(delivery_rub) AS lib,
SUM(paid_storage) AS stor,
SUM(paid_acceptance + deduction + penalty + acquiring_fee − additional_payment) AS other
FROM fin_report_detail
WHERE shop_id = :shop AND sku = :sku AND rr_date BETWEEN :since AND :to
GROUP BY tech_size
),
ad AS (
SELECT SUM(sum_cost) AS spend FROM ad_metrics_daily
WHERE shop_id = :shop AND date BETWEEN :since AND :to
AND nm_id IN (SELECT DISTINCT nm_id FROM fin_report_detail
WHERE shop_id = :shop AND sku = :sku)
),
c AS (SELECT cost FROM costs WHERE sku = :sku AND marketplace='wb' ORDER BY valid_from DESC LIMIT 1)
SELECT
fin.tech_size,
fin.sold,
fin.ret,
fin.sold − fin.ret AS net,
fin.gross,
fin.for_pay_net,
fin.lib,
fin.stor,
fin.other,
fin.for_pay_net − fin.lib − fin.stor − fin.other
− (fin.sold − fin.ret) * c.cost
− ad.spend * fin.sold / (SELECT SUM(sold) FROM fin) -- реклама пропорц.
AS profit
FROM fin, ad, c;
Привязка периода — где можно ошибиться
fin_report_detail отдаёт данные по неделям, которые WB закрыл (отправил отчёт). На момент написания (06.06):
- Последний закрытый отчёт: неделя 26.05-01.06
- Текущая неделя 02.06-08.06 — ещё «в работе», цифр в финрепе НЕТ
Если просим финансы за период 27.04 → 31.05 — окно «закрыто». WB уже всё прислал.
Если просим за 27.04 → 06.06 — последние 5 дней в финрепе пустые. Орders/Sales есть, fin_report — нет.
Рекомендация для отчётов: брать окно до конца последнего понедельника прошлой недели — тогда фин данные точно полные.
WB vs Ozon — ключевые отличия в расчёте
| Аспект | WB | Ozon |
|---|---|---|
| Комиссия | 15-25%, зашита в for_pay |
30-46%, зашита в sale_commission |
| Логистика | Отдельной строкой в fin_report | Отдельные операции в transaction_list |
| СПП | Часть for_pay (WB сам считает) |
«Программы партнёров» + «Баллы за скидки» |
| Реклама | ad_metrics_daily.sum_cost per nm_id |
ozon_ad_metrics_daily.money per offer_id |
| Хранение | paid_storage в каждой строке fin_report |
Отдельные операции в transaction_list |
| Источник истины | fin_report_detail (выплаты по неделям) | transaction_list (или Excel «Отчёт по начислениям») |
| Расхождение API vs кабинет | 142 vs 166 (Statistics vs Воронка) | ~0.1% (округление в копейках) |
История находок
| Дата | Что найдено |
|---|---|
| ?? — Siluetta WB | API /supplier/orders возвращает 1090, БД 794 — разница 296. Корень: ingest_orders с малым dateFrom не захватил все lastChangeDate. Fix: тянуть с dateFrom = now − 60 дней. |
| 2026-06-06 — Tote WB | Кабинет (Воронка) 166, БД (Statistics) 142. Разница 24 = быстро-отменённые заказы. Это разница методологий API, не баг ingest. На прибыль не влияет. |
Воспроизведение
# 1. Освежить данные на проде (квоты учитывать)
ssh wbbot 'cd /root/mp-system && .venv/bin/python -c "
from datetime import date, datetime
from app.wb.ingest import ingest_orders, ingest_sales, ingest_stocks, ingest_ad_metrics_history, ingest_funnel
ingest_orders(\"2026-02-01T00:00:00\", shop_id=5)
ingest_sales(\"2026-02-01T00:00:00\", shop_id=5)
ingest_stocks(shop_id=5)
ingest_ad_metrics_history(date(2026,4,7), date(2026,6,6), shop_id=5)
ingest_funnel(datetime(2026,4,27), datetime(2026,5,31), shop_id=5)
"'
# 2. Запустить расчёт прибыли по SKU
python scripts/wb_profit_sku.py # параметры внутри
Скрипты в репозитории:
- scripts/wb_profit_sku.py — фин-отчёт по одному SKU с разбивкой по размерам
- scripts/wb_orders_diagnose.py — диагностика расхождений orders / sales / fin_report