AI Asist
v2
Д
Д

Как считается прибыль по моделям на Ozon — методичка

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

Как считается прибыль по моделям на 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:

  1. В app/ozon/queries.py добавить profit_by_model(shop_id, since, to) — SQL с теми же агрегациями что в скрипте, но из finance_operations + ad_metrics_daily + costs.
  2. В app/web/templates/ozon_*.html добавить вкладку «Фин-отчёт» — таблица как в MD, фильтр по периоду.
  3. Scheduler — уже тянет finance_operations и ad_stats ежедневно. Дополнительно ничего не надо.
  4. Кнопка «Скачать 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 (план)
Фильтры