СПП на WB — где брать и как считаем (варианты 1 и 3)
СПП = Скидка Постоянного Покупателя — скидка, которую WB даёт конечному покупателю за свой счёт (не за счёт продавца). Продавцу платят от ЦЕНЫ ПРОДАВЦА, а СПП — расход WB.
Цена покупателя = Цена продавца − (скидка продавца) − СПП − (скидка WB-кошелёк, если есть)
Выплата продавцу = f(Цена продавца, комиссия, логистика) — СПП на выплату НЕ влияет
СПП разный у каждого артикула и даже у каждой продажи (зависит от покупателя/времени); на Ozon — ещё и по размерам. Поэтому считаем СПП всегда по nm_id / размеру / дню, не «одним числом».
Вариант 1 — РЕАЛИЗОВАННЫЙ СПП из финотчёта (постфактум)
Источник: /api/v5/supplier/reportDetailByPeriod → fin_report_detail.
Маппинг полей (raw_json → колонка БД → графа кабинета):
| raw_json | колонка | графа WB |
|---|---|---|
retailPrice |
retail_price |
«Цена розничная» = цена продавца |
spp |
spp (добавлена) |
СПП, % |
retailAmount |
retail_amount |
«Вайлдберриз реализовал Товар (Пр)» = что заплатил покупатель |
Проверка: retail_amount ≈ retail_price × (1 − spp/100).
Что сделано:
- В модель FinReportDetail (app/models.py) добавлена колонка spp.
- В нормализатор _normalize_finreport_row (app/wb/endpoints.py) добавлен "spp": _to_float(item.get("spp"))
— будущие пуллы финотчёта заполняют поле автоматически.
- Разовый бэкфилл по всей таблице: UPDATE fin_report_detail SET spp = json_extract(raw_json,'$.spp')
(≈108k строк, в т.ч. 4970 строк «Продажа»).
- Отчёт: scripts/wb_spp_report.py — СПП по артикулам/размерам за период (min/avg/max, цена продавца, цена покупателя).
- python scripts/wb_spp_report.py --shop 5 --sku skirt-atlas-max-2 --by-size --from 2026-06-01 --to 2026-06-15
Ограничение: это СПП только по проданным товарам и по закрытым неделям финотчёта (с лагом, лимит finance-api 2 req/24ч). Для «СПП по дням на лету по всем SKU» → вариант 3.
Вариант 3 — ЕЖЕДНЕВНЫЙ снимок цен/СПП, БЕЗ финотчёта
Файл: scripts/ingest_wb_spp_daily.py (самодостаточный, своя таблица wb_price_daily).
Зачем: снимать СПП каждый день по всем SKU, чтобы строить корреляцию «заказы ↔ СПП по дням».
Два источника на каждый размер:
- Цена продавца + скидки —
discounts-prices-api/api/v2/list/goods/filter(авторизованный, токен магазина): -price— цена до скидки продавца -discountedPrice— после скидки ПРОДАВЦА (⚠ СПП тут ещё НЕ применён) -discount/clubDiscount— % скидки продавца и WB Клуба - Цена покупателя (со СПП) — ВИТРИНА
wildberries.ruчерез реальный браузер (Playwright + stealth, persistent-профиль_wbprofile). Со страницы товара берём из DOM: -buyer_price— обычная цена покупателя (цена продавца − СПП), БЕЗ кошелька →ins[class*="priceBlockFinalPrice"]-buyer_wallet_price— та же − скидка WB-кошелька →[class*="priceBlockWalletPrice"]-СПП% = (discountedPrice − buyer_price) / discountedPrice × 100(база — цена БЕЗ кошелька).
Таблица wb_price_daily, PK (snapshot_date, shop_id, nm_id, tech_size) — один снимок/день/размер.
Поля: price, discounted_price, discount_pct, club_discount_pct, club_discounted_price,
buyer_price, buyer_wallet_price, spp_pct, snapshot_at. (buyer_wallet_price добавляется авто-миграцией.)
Запуск (раз в день, вручную — см. ниже про окно браузера):
python scripts/ingest_wb_spp_daily.py # оба WB-магазина (3,5), с витриной
python scripts/ingest_wb_spp_daily.py --shop 5 # один магазин
python scripts/ingest_wb_spp_daily.py --no-public # только цена продавца, БЕЗ браузера
python scripts/ingest_wb_spp_daily.py --show # последний снимок
⚠ Почему витрина через браузер, а не card.wb.ru
Сырой запрос к card.wb.ru/cards/v2/detail (httpx, любые заголовки/хосты/dest) → 403 text/html
(антибот режет по TLS/JA3). Headless-браузер → антибот-челлендж «Почти готово...». Работает только
headed-браузер: один раз проходит антибот, профиль _wbprofile хранит куки, дальше цены снимаются с DOM.
Следствия:
- Только локально, с открывающимся окном. Тихим/headless cron'ом НЕ сделать → прод-cron сознательно
не ставим. Зависимости: pip install playwright playwright-stealth (браузер chromium ставится
python -m playwright install chromium, кэш общий с другими проектами).
- Распроданные товары пропускаются. Перед обходом витрины скрипт сам снимает свежие остатки
(ingest_stocks); если сток = 0 — витрину не дёргаем, у распроданного цены нет, buyer_price/spp_pct
= NULL, это норма (не сбой). Отключить авто-обновление: --no-fresh-stocks (тогда берётся последний
снимок из БД, возможны ложные «сбои» по уже распроданным).
Итог прогона печатается честно: витрина: ok=N нет_в_наличии=M сбой=K.
сбой>0 = товар в наличии, но цену снять не удалось → разбираться (изменился селектор / антибот).
Проверено 2026-06-16: Tote WB — 10 товаров, в наличии 4, распродано 6, сбоев 0.
skirt-atlas-max-2: цена продавца 3376 → витрина 2424 (без кошелька) / 2375 (кошелёк) → СПП 28,2%
(в коридоре финотчёта 11,6–36,7%, ср. 25,4%). ELS04 → 26,7%.
Корреляция «заказы ↔ СПП по дням» (когда накопятся снимки)
SELECT d.snapshot_date, d.vendor_code, d.spp_pct,
(SELECT COUNT(*) FROM orders o
WHERE o.shop_id=d.shop_id AND o.sku=d.vendor_code
AND substr(o.date,1,10)=d.snapshot_date) AS orders_cnt
FROM wb_price_daily d
WHERE d.shop_id=5 AND d.vendor_code='skirt-atlas-max-2' AND d.tech_size='S'
ORDER BY d.snapshot_date;
(СПП пишется в финотчёте по продажам; здесь — снимок на день, поэтому это два независимых ряда для корреляции.)