Плейбук: «Прибыль 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-реклама есть в двух местах:
ad_metrics_daily.sum_cost— дневной рекламный расход.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, например хранение или общие удержания.
Правило для отчёта по артикулам:
- Строки без SKU, не являющиеся WB-продвижением, распределять пропорционально положительной выручке SKU.
- Если положительной выручки нет — показывать отдельной строкой
нераспределено. неопознанный товароставлять отдельной строкой, если 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 без себеса;
- если есть
неопознанный товарили нераспределённые строки.
Чек-лист
- Проверен
shop_id. - Проверен
MAX(rr_date)поfin_report_detail. - Проверен
MAX(date)поad_metrics_daily. - Период указан абсолютными датами.
- WB-продвижение из
deductionисключено изother_no_ads. - Реклама взята из
ad_metrics_daily. - Себес найден для всех нормальных SKU.
- Есть строка
ИТОГО.
Тайминг пула реализации (разбор 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(уже за вычетом комиссии), но без логистики/хранения/удержаний — полноценная прибыль только из реализации.