Как считается прибыль по моделям на Ozon — методичка
Описание единого подхода к расчёту фин-отчёта Ozon в Big-System-MP.
Применяется к любому магазину shop_id (Мой Ozon, Siluetta Ozon, и т.д.).
TL;DR — формула прибыли по модели
прибыль = K_выплате_нетто_per_offer − cost_per_offer × net_qty
где:
K_выплате_нетто_per_offer = SUM(finance_operations.amount) WHERE posting → offer
− ads_per_offer (из Performance API)
cost_per_offer = costs.cost WHERE sku = offer-color (lowercase)
net_qty = SUM(qty WHERE op="Реализация") − SUM(qty WHERE op="Возврат")
Источники данных
| Что | Откуда | Таблица в БД | Endpoint |
|---|---|---|---|
| К выплате нетто | Ozon Seller API | ozon_finance_operations |
/v3/finance/transaction/list |
| Реклама per offer_id | Ozon Performance API | ozon_ad_metrics_daily |
/api/client/statistics |
| Себестоимость | Ручной импорт costs.xlsx |
costs |
— |
| Атрибуция operation→offer | Через posting_number | ozon_posting_items |
/v3/posting/fbo/list |
Excel «Отчёт по начислениям» НЕ обязателен — но даёт дополнительную детализацию (Программы партнёров / Баллы за скидки отдельными строками, чего в API нет).
Совпадение API с Excel
Сверка за период 07.05-06.06.2026 (Мой Ozon, shop_id=2):
- SUM(amount) через API = 269 608 ₽
- «К выплате итого» в Excel = 269 218 ₽
- «К выплате» в Ozon Кошельке (пользователь) = 269 607,81 ₽
- Расхождение: 390 ₽ (0.1%) — округления в копейках при экспорте Excel.
Performance API vs Excel по рекламе: - Performance: 76 783 ₽ - Excel: 76 873 ₽ - Расхождение: 90 ₽ (0.1%) — те же копейки.
Вывод: API и Excel сходятся в пределах округлений. Можно строить отчёт целиком из API.
Формула — шаг за шагом
1. К выплате нетто
SELECT SUM(amount)
FROM ozon_finance_operations
WHERE shop_id = :shop_id
AND operation_date BETWEEN :since AND :to
Это сразу полная цифра «на руки» за период. Включает:
- Выручка от продаж (operation_type=OperationAgentDeliveredToCustomer, type=orders)
- Минус комиссия Ozon (внутри той же операции, поле sale_commission)
- Минус логистика, обработка возвратов, эквайринг (services внутри той же операции)
- Минус отдельные операции рекламы (Оплата за клик, Продвижение с оплатой за заказ)
- Минус услуги FBO, упаковка, кросс-докинг и др.
- Плюс компенсации от Ozon
2. Атрибуция per offer_id
Финансовая операция привязана к posting_number. Один posting может содержать несколько товаров (items), но обычно 1 posting = 1 item × qty=1.
SELECT i.offer_id, SUM(o.amount) as payout_for_offer
FROM ozon_finance_operations o
JOIN ozon_posting_items i ON i.shop_id = o.shop_id
AND i.posting_number = o.posting_number
WHERE o.shop_id = :shop_id
AND o.operation_date BETWEEN :since AND :to
GROUP BY i.offer_id
Важно: некоторые операции (например Оплата за клик) не имеют posting_number — это «общие» расходы магазина. Их нужно либо распределить пропорционально (плохо), либо взять per offer_id из Performance API (правильно).
3. Реклама per offer_id
Performance API даёт точную атрибуцию:
SELECT offer_id, SUM(money) as ad_spend
FROM ozon_ad_metrics_daily
WHERE shop_id = :shop_id
AND date BETWEEN :since AND :to
GROUP BY offer_id
Полная сумма ≈ сумма «Оплата за клик» + «Продвижение с оплатой за заказ» в finance_operations. Расхождение 0.1%.
Внимание: в finance_operations реклама идёт без posting_number (общие расходы магазина). Если строить расчёт только через finance_operations, надо вычесть рекламу из общего SUM(amount) и затем добавить её обратно per offer_id из Performance. Иначе получится «реклама размазана пропорц. gross» (что было ошибкой в первой версии расчёта).
4. Себестоимость
Таблица costs, ключ — lowercase model-color (без размера):
SELECT cost FROM costs
WHERE sku = LOWER(REGEXP_REPLACE(offer_id, '-[^-]+$', ''))
AND marketplace = 'ozon'
ORDER BY valid_from DESC LIMIT 1
Пример: ELS04-Brown-S → key els04-brown → cost 850 ₽.
5. Количество net_qty
SELECT
SUM(CASE WHEN o.type = 'orders' THEN 1 ELSE 0 END)
− SUM(CASE WHEN o.type = 'returns' AND o.operation_type = 'ClientReturnAgentOperation' THEN 1 ELSE 0 END)
FROM ozon_finance_operations o
JOIN ozon_posting_items i ON …
WHERE o.shop_id = :shop_id
AND o.operation_date BETWEEN :since AND :to
AND i.offer_id = :offer_id
Или через ozon_postings со статусом delivered минус возвраты — но finance_operations надёжнее.
Группировка «по моделям»
Модель = offer_id.split('-')[0]. Например HZ030-Brown-S → модель HZ030.
В UI/отчёте имеет смысл показывать обе разрезки: - по моделям (сводка для принятия решений) - по offer_id (детализация: какой именно цвет/размер тянет)
Что Excel даёт сверх API
«Отчёт по начислениям» в Ozon Seller — это тот же transaction list, но с дополнительной декомпозицией:
| В Excel | В API |
|---|---|
Выручка (что заплатил покупатель деньгами) |
сливается в accruals_for_sale |
Программы партнёров (промокоды/акции, компенсируемые Ozon) |
сливается в accruals_for_sale |
Баллы за скидки (что покупатель оплатил баллами, компенсируется Ozon) |
сливается в accruals_for_sale |
То есть в API accruals_for_sale = Выручка + Программы + Баллы (это retail), а отдельно эти 3 компонента не разделить. Если нужна аналитика «сколько мы продали за баллы / промокоды / живые деньги» — нужен Excel.
Для расчёта прибыли этого не нужно. Прибыль = amount − cost, и она считается из API 1:1.
Ozon vs WB — ключевые различия в расчёте
| Аспект | Ozon | WB |
|---|---|---|
| Комиссия | ~46% от retail (зашита в sale_commission) |
~17-25% (зашита в for_pay) |
| Логистика | Отдельной строкой в transaction, не в комиссии | Часть for_pay, отдельно через service costs |
| СПП/скидки | «Программы партнёров» + «Баллы за скидки» (Ozon доплачивает за свои скидки) | Чистая spp отдельно |
| Реклама | Отдельные операции (services) + Performance API per offer |
ad_spend_daily per offer |
| Хранение | MarketplaceServiceItemFlexibleStorage и др. — реже встречается |
Отдельной таблицей wb_storage_daily |
| Источник истины | Excel «Отчёт по начислениям» = API transaction/list (совпадают) |
API /api/v5/supplier/reportDetailByPeriod |
План для встраивания в Big-System-MP
Сейчас расчёт делается отдельным скриптом scripts/ozon_report_profit.py (читает Excel). Чтобы перевести на API:
- В
app/ozon/queries.pyдобавитьprofit_by_model(shop_id, since, to)— SQL с теми же агрегациями что в скрипте, но изfinance_operations + ad_metrics_daily + costs. - В
app/web/templates/ozon_*.htmlдобавить вкладку «Фин-отчёт» — таблица как в MD, фильтр по периоду. - Scheduler — уже тянет finance_operations и ad_stats ежедневно. Дополнительно ничего не надо.
- Кнопка «Скачать Excel» — генерить наш Excel с теми же 3 колонками что в Ozon, для сверки/архива.
После этого пользователю достаточно зайти в админку → Ozon → выбрать магазин и период → получить таблицу. Excel из Ozon-кабинета не нужен вообще.
Расхождения с предыдущей попыткой через API (что я делал не так)
При первой попытке расчёта через API получился убыток −150к (вместо +67к / реального −21к). Причины:
1. Брал posting_items.payout (прогноз), а не фактический finance_operations.amount.
2. Фильтровал по posting.created_at, не по operation_date → окно сдвигалось.
3. Дублировал рекламу: и в ad_metrics и в operations.
4. Считал postings со статусами delivering, awaiting_* как продажи.
5. Не учитывал что в finance_operations.orders уже включена комиссия и услуги per posting (через services массив в raw_json).
Правильный путь = просто SUM(amount) FROM finance_operations + минус cost. Всё остальное (комиссия / логистика / упаковка / реклама) — уже внутри amount.
История
- 2026-06-06 — первая итерация, расчёт через Excel + проверка совпадения с API
- ?? — встроить в Big-System-MP веб-UI (план)