AI Asist
v2
Д
Д

Плейбук: «Прибыль WB» за 7 дней / 1 день

← Заметки  ·  Раздел: Wildberries  ·  Файл: data/notes/wb/profit-week-day-playbook.md

Плейбук: «Прибыль WB» за 7 дней / 1 день

Срабатывает, когда пользователь просит:

  • «Прибыль WB за 7 дней»;
  • «Прибыль WB за неделю»;
  • «Прибыль WB за день»;
  • «Tote WB прибыль за вчера»;
  • «Siluetta WB прибыль сравнение»;
  • «прибыль WB сравнение за 7 дней / за 1 день».

Формат отчёта такой же, как для Ozon:

Артикул | Продажи net | Выручка | Себес | Лог/шт | Реклама | ДРР | Прибыль | Приб/шт | ROI

Размеры на WB объединяются в один артикул по sku. Если один SKU встречается в разных tech_size, в отчёте показываем одну строку SKU.


Магазины

Магазин shop_id marketplace
Siluetta WB 3 wb
Tote WB 5 wb

Если магазин не указан, брать из контекста последнего обсуждения. Если контекста нет — уточнить магазин.


Периоды

7 дней

Прибыль WB за 7 дней = последняя закрытая календарная неделя Пн–Вс.

Пример: если текущая дата 2026-06-29, период:

2026-06-22 … 2026-06-28

Проверять, что в fin_report_detail есть данные по rr_date до конца периода:

SELECT MAX(rr_date)
FROM fin_report_detail
WHERE shop_id = :shop_id;

Если финрепа за период нет, не придумывать цифры. Сказать, что отчёт ещё не закрыт/не загружен, и при необходимости запустить ingest финрепа с учётом лимита WB.

1 день

Прибыль WB за день = один календарный день. По умолчанию — вчера по Europe/Moscow.

Пример: если текущая дата 2026-06-29, период:

2026-06-28 … 2026-06-28

Перед расчётом проверить наличие строк:

SELECT COUNT(*)
FROM fin_report_detail
WHERE shop_id = :shop_id
  AND rr_date BETWEEN :since AND :to;

Дневная прибыль СОБИРАЕТСЯ нормально через реализацию — реализация НЕ отстаёт. Вопрос только во времени пула: WB отдаёт вчерашнюю дневную реализацию (period=daily) с логистикой/удержаниями только к полудню МСК. Если за вчера COUNT(*)=0 или есть только реклама (revenue=0) — это не «нет данных», а «финреп ещё не дёрнут за этот день». Решение — пул в 12:00 МСК, см. раздел «Тайминг пула реализации» ниже.


Сравнение

Если пользователь пишет «сравнение», «сравни», «сравни данные», «сравни отчёты»:

7 дней сравнение

Текущий период = последняя закрытая неделя Пн–Вс.

Предыдущий период = предыдущая закрытая неделя такой же длины.

Пример для текущей даты 2026-06-29:

текущий:    2026-06-22 … 2026-06-28
предыдущий: 2026-06-15 … 2026-06-21

1 день сравнение

Текущий период = выбранный день.

Предыдущий период = среднее за предыдущие 7 календарных дней, не включая выбранный день.

Пример:

день:       2026-06-28
сравнение:  среднее за 2026-06-21 … 2026-06-27

Для среднего за 7 дней каждую абсолютную метрику делить на 7:

  • Продажи net;
  • Выручка;
  • Себес;
  • Логистика;
  • Реклама;
  • Прибыль.

Проценты и метрики на штуку считать уже от усреднённых чисел, а не усреднять проценты построчно.


Источники данных

Метрика Источник
Продажи net fin_report_detail: продажи минус возвраты
Выручка retail_amount по продажам минус возвраты
К перечислению for_pay по продажам минус возвраты
Себес costs, marketplace='wb', ключ = sku
Логистика delivery_rub + распределённое paid_storage без SKU
Реклама ad_metrics_daily.sum_cost по nm_id
Прочие удержания paid_acceptance + deduction + penalty + acquiring_fee - additional_payment, без WB-продвижения

Важно: рекламу WB не задваивать.

WB-реклама есть в двух местах:

  1. ad_metrics_daily.sum_cost — дневной рекламный расход.
  2. fin_report_detail.deduction со строками bonus_type_name LIKE '%Продвижение%' — удержание в финрепе.

В отчёте берём рекламу из ad_metrics_daily, а из прочих удержаний исключаем deduction по WB-продвижению:

SUM(paid_acceptance + deduction + penalty + acquiring_fee - additional_payment)
- SUM(CASE WHEN bonus_type_name LIKE '%Продвижение%' THEN deduction ELSE 0 END)

Формулы

Продажи net =
    SUM(quantity WHERE seller_oper_name='Продажа')
  - SUM(quantity WHERE seller_oper_name='Возврат')

Выручка =
    SUM(retail_amount WHERE seller_oper_name='Продажа')
  - SUM(retail_amount WHERE seller_oper_name='Возврат')

К перечислению =
    SUM(for_pay WHERE seller_oper_name='Продажа')
  - SUM(for_pay WHERE seller_oper_name='Возврат')

Себес = max(Продажи net, 0) × cost

Логистика = SUM(delivery_rub) + распределённое paid_storage без SKU

Реклама = SUM(ad_metrics_daily.sum_cost)

ДРР = Реклама / Выручка × 100

Прибыль =
    К перечислению
  - Логистика
  - Прочие удержания без WB-продвижения
  - Реклама
  - Себес

Лог/шт = Логистика / Продажи net

Приб/шт = Прибыль / Продажи net

ROI = Прибыль / Себес × 100

Если Продажи net <= 0, Лог/шт и Приб/шт показывать .

Если Себес = 0, ROI показывать .


Распределение общих строк без SKU

В WB бывают строки без SKU, например хранение или общие удержания.

Правило для отчёта по артикулам:

  1. Строки без SKU, не являющиеся WB-продвижением, распределять пропорционально положительной выручке SKU.
  2. Если положительной выручки нет — показывать отдельной строкой нераспределено.
  3. неопознанный товар оставлять отдельной строкой, если WB отдал такой SKU.

SQL-скелет

WITH fin AS (
  SELECT
    sku,
    nm_id,
    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 revenue,
    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 delivery,
    SUM(paid_storage) AS storage,
    SUM(paid_acceptance + deduction + penalty + acquiring_fee - additional_payment)
      - SUM(CASE WHEN bonus_type_name LIKE '%Продвижение%' THEN deduction ELSE 0 END) AS other_no_ads
  FROM fin_report_detail
  WHERE shop_id = :shop_id
    AND rr_date BETWEEN :since AND :to
  GROUP BY sku, nm_id
),
ads AS (
  SELECT
    nm_id,
    SUM(sum_cost) AS ads
  FROM ad_metrics_daily
  WHERE shop_id = :shop_id
    AND date BETWEEN :since AND :to
  GROUP BY nm_id
)
SELECT
  fin.sku,
  fin.sold - fin.ret AS net_qty,
  fin.revenue,
  fin.for_pay_net,
  fin.delivery,
  fin.storage,
  fin.other_no_ads,
  COALESCE(ads.ads, 0) AS ads
FROM fin
LEFT JOIN ads ON ads.nm_id = fin.nm_id;

Себес подтягивать отдельно из:

SELECT sku, cost
FROM costs
WHERE marketplace='wb'
ORDER BY valid_from DESC;

Для каждого SKU брать самую свежую запись.


Формат ответа

Выводить компактную таблицу:

Артикул              Прод net   Выручка    Себес  Лог/шт  Реклама    ДРР   Прибыль  Приб/шт    ROI
skirt-atlas-max-2          15    34 437   10 500     687    6 189  18.0%     2 946      196   28.1%
...
ИТОГО                      29    66 989   23 250     583   16 669  24.9%       655       23    2.8%

Для сравнения:

  • в обычном чате можно показывать текущее (предыдущее);
  • в Telegram лучше показывать второй строкой сравн., чтобы таблица не плыла.

В конце кратко указать:

  • магазин;
  • период;
  • источник: fin_report_detail + ad_metrics_daily;
  • если есть SKU без себеса;
  • если есть неопознанный товар или нераспределённые строки.

Чек-лист

  1. Проверен shop_id.
  2. Проверен MAX(rr_date) по fin_report_detail.
  3. Проверен MAX(date) по ad_metrics_daily.
  4. Период указан абсолютными датами.
  5. WB-продвижение из deduction исключено из other_no_ads.
  6. Реклама взята из ad_metrics_daily.
  7. Себес найден для всех нормальных SKU.
  8. Есть строка ИТОГО.

Тайминг пула реализации (разбор 2026-07-09)

Исходный запрос пользователя: «прибыль за вчера по WB не показывается в веб-версии (в боте показывается), по Ozon — да; на WB есть ежедневный фин отчёт, дерни через fin_report_detail». Итог разбора: реализация не отстаёт, отставал только тайминг ночного пула.

Факты (проверено на проде):

  • Дневная реализация WB собирается штатно: эндпоинт POST /api/finance/v1/sales-reports/detailed, period="daily" (не устаревший reportDetailByPeriod). Код уже так и тянет (app/wb/endpoints.py:fetch_finreport_detail, дефолт period="daily").
  • Свежедёрнутый днём финреп сразу отдаёт вчерашний день с логистикой, удержаниями и Продажа-строками. Пример (Tote WB, вчера): 144 строки, продажи 10 093 ₽, логистика 2 816 ₽, MAX(rr_date) = вчера.
  • Проблема была: ночной сбор mp-collect@ (01:00 МСК) дёргал финреп слишком рано. WB публикует вчерашнюю дневную реализацию только к ~полудню МСК, поэтому в 01:00 за вчера ещё пусто → в БД висел позавчерашний MAX(rr_date) → живой расчёт веба (aggregate.wb_profit_rows, фильтр rr_date BETWEEN) давал revenue=0 (выживала только реклама из ad_metrics_daily → прибыль = −реклама).
  • Ozon так не страдает: его финоперации (ozon_finance_operations.operation_date) полны уже к T+1.

Почему нельзя просто «дёргать чаще»: finance-api лимит — 2 запроса/24ч, интервал 12ч на аккаунт. Два пула ближе 12ч → 429.

Решение (внедрено, коммит 38d692a):

  • finreport вынесен из ночного сбора в отдельный таймер mp-finreport@{3,5} на 12:00 МСК. Ночной mp-collect@ идёт с --no-finreport (daily_refresh(include_finreport=False)).
  • run_collect.py --only-finreport — прогон только шага реализации (для полуденного таймера).
  • Persistent=false у таймера: пропущенный слот не гнать, чтобы не нарушить интервал 12ч.

Следствия для этого плейбука:

  • «Прибыль WB за день/вчера» через fin_report_detailвалидна, но за самый свежий день данные появляются только после ~12:00 МСК. До полудня COUNT(*)=0 за вчера — норма, не баг; можно дёрнуть вручную: ingest_wb.py --shop-id N finreport --from … --to … --period daily.
  • Бот (telegram_report_cache) кэшируется дважды: 02:30 МСК (свежие заказы/реклама, но дневная WB-прибыль ещё пустая) и 12:30 МСК — ПОСЛЕ полуденного finreport, где дневная WB-прибыль становится полной. Между 02:30 и 12:30 МСК дневная WB-прибыль в боте и вебе неполная — это норма (WB публикует реализацию к полудню). Кэш пишет scripts/report_cache.py (только локальная БД, без API); бот читает его вживую.
  • sales (statistics-api, выкупы) даёт выручку и for_pay (уже за вычетом комиссии), но без логистики/хранения/удержаний — полноценная прибыль только из реализации.
Фильтры