Яндекс Маркет: как строится таблица заказов

Проверено 16 июля 2026 года по текущим локальным рабочим деревьям wolle-economy-viewer, ya-reports, ya-updater и ya-order-notify, а также read-only данным PostgreSQL. Пример во всех разделах — заказ 55830644865.

Назначение и границы

Гайд описывает вкладку «Яндекс Маркет» страницы «Заказы» проекта wolle-economy-viewer при включённом переключателе «Показать все колонки». Авторитетный список — DISPLAY_COLUMNS в src/wolle_economy/ui/columns.py; на дату проверки в нём 43 колонки.

Здесь описано текущее фактическое поведение. Даже если название колонки допускает другое толкование, формула приводится именно такая, какую выполняет код.

Не входят в область гайда: фильтры и верхние KPI страницы, вкладки других маркетплейсов, конкурентное ценообразование после создания заказа и бухгалтерское признание доходов.

Короткая модель

Одна строка на экране — одна позиция ya_order_items, а не один заказ. Основная финансовая модель такова:

Цена продажи                    sell_price
− комиссии ЯМ                   market_services
= ожидаемая выплата             expected_payout
+ наши промо-списания           promo_discounts  (они отрицательные)
− наши затраты                  our_costs
= прибыль с учётом промо        profit

Где:

our_costs = фактическая закупка доставленных единиц
          + упаковка доставленных единиц
          + переходник доставленных единиц
          + доставка из Китая доставленных единиц (только seller.location = 'CN')

Субсидия Яндекс Маркета (bonus_points) уже входит в sell_price. Повторно прибавлять её к прибыли нельзя.

Поток данных

Yandex Partner API /v1/businesses/{businessId}/orders
  └─ ya-order-notify → ya_orders + ya_order_items
       ├─ снимок закупочной цены, минимальной цены и плановых комиссий
       └─ ссылки на продавца, фид и товар

Отчёт «Заказы» (united-orders)
  ├─ лист «Услуги и маржа по заказам» → ya_order_margin_report
  └─ лист «Транзакции по заказам и товарам» → ya_order_transactions_report

Отчёт «Платежи» (united-netting)
  └─ лист «Отчёт о платежах» → ya_payments_reports

Складская цепочка
  └─ all_split_orders + stock_movement_transactions + order_to_supplier
       → фактическая закупочная цена

wolle-economy-viewer
  ├─ queries.py: три SQL-запроса
  ├─ loader.py: merge по item_id и ya_order_id
  ├─ economics.py: производные показатели
  └─ columns.py + table.py: порядок, подписи и форматирование

ya-updater рассчитывает минимальную цену предложения по очень близкой формуле и ведёт историю цен. Однако значения конкретного заказа фиксируются при создании заказа проектом ya-order-notify; именно его снимок находится в ya_order_items и читается таблицей.

Гранулярность и опасность двойного счёта

Базовая строка — e_com.ya_order_items.id. К ней присоединяются поля разных уровней:

Уровень Примеры Что происходит при заказе с несколькими позициями
Заказ sell_price, market_services, payment_status Одна и та же сумма повторяется в каждой позиции после JOIN. Суммировать строки нельзя.
Позиция offer_id, quantity, base_price, расчётные комиссии Свои значения для каждой позиции.
Единица товара base_price, ff_fee, socket_adapter_fee Python умножает на количество.
Платёжная транзакция строки ya_payments_reports SQL агрегирует все строки заказа.

Для multi-item заказа текущий код не распределяет sell_price и market_services между позициями. Следовательно, expected_payout, profit и часть производных также повторяют заказные суммы в каждой позиции, но вычитают затраты конкретной позиции. Это ограничение нужно учитывать при экспорте и агрегации.

Какие отчёты и столбцы используются

Отчёт «Заказы» (united-orders)

Файл скачивает ya-reports через POST /reports/united-orders/generate.

Лист «Услуги и маржа по заказам» парсится позиционно:

Excel iloc Назначение в ya-reports Таблица БД
H 7 номер заказа → order_id для связи ya_orders.order_id
J 9 статус → status ya_order_margin_report.status
M 12 услуги Маркета → market_services ya_order_margin_report.market_services
N 13 цена продажи → sell_price ya_order_margin_report.sell_price
Q 16 статус платежа → payment_status ya_order_margin_report.payment_status

Строки читаются с динамически найденной строкой заголовка: код ищет маркер «Номер заказа» в Excel-строке 4 или 7.

Лист «Транзакции по заказам и товарам» читается с header=9 (Excel-строка 10). Для этой таблицы важны:

Excel iloc Поле БД Использование
H 7 связь через номер заказа поиск ya_orders
L 11 связь через Offer ID поиск ya_order_items
O 14 sell_price цена возвращённой транзакции
S 18 other_market_discounts диагностика скидок
Y 24 status статус позиции и распознавание возврата
AG 32 customer_payment_date приоритетная дата выплаты
AJ 35 bonuses справочная субсидия ЯМ
BA 52 customer_refund_amount диагностика возврата
BC 54 refund_payment_date дата возвратного платежа

Отчёт «Платежи» (united-netting)

Файл скачивается через POST /reports/united-netting/generate. Лист «Отчёт о платежах» читается с header=1 (Excel-строка 2):

Excel iloc Поле БД Использование
J 9 order_id связь с заказом
P 15 offer_id необязательная связь с позицией
S 18 item_name_or_service_name распознавание штрафов
T 19 quantity сохраняется для диагностики
U 20 transaction_amount комиссии, промо, штрафы
V 21 transaction_type сохраняется как источник отчёта
W 22 transaction_source различение нового формата отчёта
X 23 payment_status различение старого формата и выплат
AA 26 payment_date fallback даты выплаты
AB 27 payment_id идентификатор платежа
AC 28 payment_or_withheld_amount сохраняется, но таблица заказов не использует

Парсер ориентируется на позиции столбцов, а не на текст заголовка. При изменении формата Excel индексы требуется сверить в ya-reports/backend/src/services/orders_to_db.py и payments_to_db.py.

Сквозной пример YM-55830644865

Заказ содержит одну позицию (ya_order_items.id = 27032, quantity = 1), поэтому заказные суммы не дублируются между товарами.

Параметр Значение
Магазин ТехноПравда ЯНД
Offer ID BRITVS100
Плановая закупка 900,00 ₽
Фактическая закупка 820,00 ₽
Упаковка 50,00 ₽
Наши учитываемые затраты 870,00 ₽
Минимальная цена / цена продажи 2 340,00 ₽
Субсидия ЯМ, уже в цене продажи 739,00 ₽
Комиссии из margin report 382,01 ₽
Ожидаемая выплата 1 957,99 ₽
Наши промо-списания −798,06 ₽
Прибыль с промо 289,93 ₽
Прибыль без промо 1 087,99 ₽

Проверка основной формулы:

expected_payout = 2 340,00 − 382,01 = 1 957,99 ₽
our_costs       = 820,00 + 50,00 + 0,00 + 0,00 = 870,00 ₽
profit          = 1 957,99 − 798,06 − 870,00 = 289,93 ₽
profit_no_promo = 1 957,99 − 870,00 = 1 087,99 ₽

Сводный список колонок (UI-43)

Заголовок в UI Техническое имя Тип источника
1 Заказ создан created_at БД/API
2 Дата отгрузки shipment_date БД/API
3 Магазин seller_name БД
4 Номер заказа order_id_str Вычисление
5 Offer ID offer_id БД/API
6 Наименование product_name БД
7 Статус заказа order_status Отчёт + fallback БД
8 Статус заказа 2 fulfillment_status Отчёт + fallback БД
9 Статус платежа payment_status Отчёт «Заказы»
10 Количество quantity БД/API
11 Отправлено от/из supplier_name Снимок расчёта заказа
12 Цена закупки base_price_total Вычисление
13 Факт. закупка effective_purchase_total Складская цепочка + вычисление
14 Цена упаковки ff_fee_total БД + вычисление
15 Цена переходника socket_adapter_total БД + вычисление
16 Доставка из Китая custom_delivery_fee_total Снимок заказа + вычисление
17 Цена + маржа price_with_margin Вычисление
18 Маржа план ₽ our_margin Вычисление
19 Маржа факт ₽ margin_fact_rub Вычисление
20 Маржа план % (от цены продажи) margin_plan_pct Вычисление
21 Маржа факт % (от цены продажи) margin_fact_pct Вычисление
22 Маржа план % (от закупки) margin_plan_on_cost_pct Вычисление
23 Маржа факт % (от закупки) margin_fact_on_cost_pct Вычисление
24 Минимальная цена min_sell_price_total Снимок расчёта + вычисление
25 Ожидаемая прибыль expected_profit Вычисление
26 Цена продажи sell_price Отчёт «Заказы» + fallback
27 Субсидия ЯМ (справочно, в sell_price) bonus_points Отчёт «Заказы», транзакции
28 Промо-расходы (наши баллы) promo_discounts Отчёт «Платежи»
29 Разница от мин. цены diff_from_min_price Вычисление
30 Расчётные комиссии calc_commissions Снимок расчёта + вычисление
31 Комиссии (margin_report) market_services Отчёт «Заказы»
32 Факт. комиссии (ЛК) fact_commissions Отчёт «Платежи»
33 Доход за вычетом комиссий income_after_fees Вычисление
34 Прибыль profit Вычисление
35 Разница Прибыли Факт/Ожид profit_vs_expected Вычисление
36 Доход за вычетом комиссий и промо income_after_fees_promo Вычисление
37 Прибыль без учёта промо-расходов profit_no_promo Вычисление
38 Штраф за отмену заказа seller_cancel_penalty Отчёт «Платежи»
39 Штраф за позднюю отгрузку late_ship_penalty Отчёт «Платежи»
40 Нам перевели за заказ payout_if_paid Статус отчёта + вычисление
41 Сумма выплаты (если нет даты, то ожидаемая) expected_payout Вычисление
42 Дата выплаты last_payment_date Два отчёта
43 Фактическая прибыль actual_profit Вычисление
Авторитетный программный список содержит 43 уникальные позиции. Далее описаны все 43 в том же порядке.

Подробное описание колонок

YM-01. Заказ создан — created_at

Источник. e_com.ya_orders.created_at. Поле заполняет ya-order-notify из creationDate ответа Yandex Partner API POST /v1/businesses/{businessId}/orders. Схема Order приводит время к UTC и убирает timezone перед записью; PostgreSQL в текущей базе возвращает timezone-aware значение.

Преобразование. SQL передаёт значение без вычислений. В таблице Streamlit применяется формат DD.MM.YYYY HH:mm. Конкретное отображаемое часовое смещение может зависеть от клиента; для сверки используйте значение БД в UTC.

Пример. Для заказа 55830644865: 2026-04-06 21:24:03.930000+00:00.

YM-02. Дата отгрузки — shipment_date

Источник. e_com.ya_orders.shipment_date, исходно delivery.shipment.shipmentDate заказа из Yandex Partner API. ya-order-notify также обновляет поле при изменении даты в API.

Преобразование. Расчёта нет; UI показывает только дату в формате DD.MM.YYYY. Пустое значение остаётся пустым.

Пример. 2026-04-09 00:00:00+00:0009.04.2026.

YM-03. Магазин — seller_name

Источник. e_com.platform_sellers.seller_name. Связь: ya_orders.seller_id = platform_sellers.id через обязательный JOIN.

Следствие JOIN. Если связанного продавца нет, позиция вообще не попадёт в таблицу, а не получит пустой магазин.

Пример. seller_id = 2ТехноПравда ЯНД.

YM-04. Номер заказа — order_id_str

Источник. Число e_com.ya_orders.order_id, пришедшее из orderId Yandex Partner API.

Формула. После загрузки Python выполняет:

df["order_id_str"] = df["order_id"].astype(str)

Преобразование в строку предотвращает научную запись и форматирование длинного идентификатора как числа.

Пример. 55830644865 → строка "55830644865".

YM-05. Offer ID — offer_id

Источник. e_com.ya_order_items.offer_id; исходное поле API — items[].offerId. Это идентификатор предложения магазина, а не marketSku и не внутренний item_id Яндекса.

Пример. BRITVS100.

YM-06. Наименование — product_name

Источник. Не ya_order_items.offer_name. Используется текущее внутреннее название группы товара:

ya_order_items.feed_item_id
→ yandex_feed_items.id
→ yandex_feed_items.unique_product_group_id
→ unique_product_groups.id
→ unique_product_groups.name

Оба соединения обязательные. Если фид или группа удалены физически, позиция исчезнет из результата. Переименование unique_product_groups.name изменит отображение старых заказов, потому что это не снимок на дату заказа.

Пример. Электробритва Xiaomi Mijia Electric Shaver S100 (MSX201/NUN4062CN) CN.

YM-07. Статус заказа — order_status

Это наиболее подробный статус позиции из транзакционного отчёта с fallback на технический API-статус заказа.

Источники. e_com.ya_order_transactions_report.status и e_com.ya_orders.status.

Агрегация транзакций по ya_order_items_id. У одной позиции может быть несколько транзакций. SQL формирует tr.status:

  1. если есть хотя бы одна возвратная строка со статусом Возврат оформлен или Невыкуп передан вам и хотя бы одна невозвратная — Частично возвращён;
  2. иначе берётся максимальный возвратный статус;
  3. если возвратных нет — MAX(status).

Итоговая формула:

COALESCE(tr.status, o.status)

В отчёте это поле берётся с листа «Транзакции по заказам и товарам», Excel-столбец Y (iloc[24]). Если транзакционный отчёт ещё не загружен, показывается код API вроде DELIVERED.

Пример. ya_order_transactions_report.status = 'Доставлен покупателю', поэтому UI показывает Доставлен покупателю, а не ya_orders.status = 'DELIVERED'.

YM-08. Статус заказа 2 — fulfillment_status

Это нормализованный финансовый статус заказа.

Приоритет:

COALESCE(
  CASE WHEN tr.status = 'Частично возвращён'
       THEN 'Частично возвращён' END,
  ya_order_margin_report.status,
  русская_расшифровка(ya_orders.status)
)

Частичный возврат намеренно сильнее margin_report: отчёт о марже иногда продолжает показывать Доставлен. Если margin_report отсутствует, коды API преобразуются: DELIVERED → Доставлен, CANCELLED → Отменён, DELIVERY → В доставке, PROCESSING → В обработке, PICKUP → Самовывоз; неизвестный код остаётся как есть.

Отчёт. ya_order_margin_report.status приходит с листа «Услуги и маржа по заказам», Excel-столбец J (iloc[9]).

Пример. margin_report.status = 'Доставлен', возврата нет → Доставлен.

YM-09. Статус платежа — payment_status

Источник. e_com.ya_order_margin_report.payment_status, лист «Услуги и маржа по заказам», Excel-столбец Q (iloc[16]).

Преобразование. Нет. Если отчёт отсутствует, значение NULL. Статусы Переведён и Удержан из платежей покупателей считаются оплаченными при расчёте payout_if_paid и actual_profit.

Пример. Переведён.

YM-10. Количество — quantity

Источник. e_com.ya_order_items.count, исходное items[].count Yandex Partner API.

Использование. Количество не только отображается: им умножаются плановая закупка, упаковка, переходник, китайская доставка и минимальная цена. Для затрат при частичном возврате вместо него может использоваться tr_delivered_quantity.

Пример. 1.

YM-11. Отправлено от/из — supplier_name

Источник. e_com.ya_order_items.supplier_name. Это снимок выбора источника товара в момент создания заказа, сохранённый ya-order-notify, а не вычисление wolle-economy-viewer и не фактический поставщик из order_to_supplier.

ItemsCalculator выбирает склад либо предложение поставщика. Для внешнего поставщика выбирается минимальная доступная цена; при отсутствии доступных остатков может использоваться последнее известное предложение. Вместе с ценой в позицию записывается имя поставщика.

Ограничение. Фактический товар позднее может быть взят с другого складского прихода; колонка всё равно показывает первоначальный снимок. Для фактической цены используется отдельная складская цепочка в следующей колонке.

Пример. Opt-instock.

YM-12. Цена закупки — base_price_total

Это плановая закупка всей позиции по снимку на момент заказа.

Исходное поле. e_com.ya_order_items.base_price — цена одной единицы, выбранная ya-order-notify при создании заказа. Для рублёвого предложения это цена поставщика; для валютного предложения ItemsCalculator переводит цену в рубли по используемым им курсам и сохраняет результат. Значение приводится к целому перед записью.

Формула:

base_price_total = ya_order_items.base_price × quantity

Эта колонка не заменяется фактической закупкой и используется как база плановой маржи.

Пример. 900 × 1 = 900,00 ₽.

YM-13. Факт. закупка — effective_purchase_total

Назначение. Максимально близкая к реально понесённой закупочной стоимости сумма всей позиции.

Источник фактической цены. e_com.order_to_supplier.ru_custom_price или ru_price, найденные через all_split_orders и движения склада:

  1. для каждой позиции берутся её all_split_orders;
  2. определяется последнее stock_movement_transactions для split-заказа;
  3. если движений нет или последнее движение имеет type = 'lost', источником остаётся текущий all_split_orders;
  4. иначе выбирается первый split-заказ, который привёз товар на соответствующий warehouse_new_id;
  5. по его order_to_supplier_id находится цена;
  6. обычно применяется COALESCE(ru_custom_price, ru_price, 0);
  7. защита исторических данных: если валюта не RUB/RUR и ru_custom_price = custom_price, используется корректно пересчитанный ru_price.

Из-за нескольких split-заказов SQL использует DISTINCT ON (ya_order_items.id).

Формула отображаемой суммы:

если supplier_price_fact > 0:
    effective_purchase_total = supplier_price_fact × quantity
иначе:
    effective_purchase_total = base_price_total

Важно для возвратов. Сама отображаемая колонка всегда умножается на полное quantity. Но our_costs и прибыль при частичном возврате считают закупку только доставленных единиц.

Пример. Для split-заказа 58908 найден order_to_supplier.id = 56074: ru_custom_price = NULL, ru_price = 820. Поэтому 820 × 1 = 820,00 ₽, а плановые 900 ₽ не используются в фактических затратах.

YM-14. Цена упаковки — ff_fee_total

Источник за единицу. Текущая справочная ставка e_com.ff_fees.value через цепочку:

ya_order_items.feed_item_id
→ yandex_feed_items.market_modifier_yandex_id
→ market_modifier_yandex.ff_fees_id
→ ff_fees.value

Соединения с модификатором и ставкой — LEFT JOIN, пустое значение превращается в 0.

Формула:

ff_fee_total = COALESCE(ff_fees.value, 0) × quantity

Нюанс истории. В ya_order_items.markup_ff_fees_amount также хранится снимок упаковки при создании заказа, но wolle-economy-viewer его для этой колонки не читает. Если справочник изменился, старый заказ на экране может пересчитаться по новой ставке.

Пример. Текущая ставка 50 ₽; снимок заказа тоже 50 ₽: 50 × 1 = 50,00 ₽.

YM-15. Цена переходника — socket_adapter_total

Источник за единицу. e_com.socket_adapter_fee.value через yandex_feed_items → market_modifier_yandex.socket_adapter_fee_id. Пустое значение становится нулём.

Формула:

socket_adapter_total = COALESCE(socket_adapter_fee.value, 0) × quantity

Как и упаковка, это текущая справочная ставка, хотя в ya_order_items.markup_socket_adapter_fee_amount существует исторический снимок.

Пример. Ссылка на ставку отсутствует, снимок равен 0: 0 × 1 = 0,00 ₽.

YM-16. Доставка из Китая — custom_delivery_fee_total

Исходное значение за единицу. e_com.ya_order_items.markup_custom_delivery_fee_value_amount, сохранённое ya-order-notify из market_modifier_yandex.delivery_fee_value, когда включён is_need_custom_delivery_value.

Условие:

если platform_sellers.location = 'CN':
    custom_delivery_fee_total = markup_custom_delivery_fee_value_amount × quantity
иначе:
    custom_delivery_fee_total = 0

Для RU-магазина эта надбавка трактуется как доставка ЯМ, уже содержащаяся в market_services, и не считается нашим отдельным расходом. Для CN-магазина это собственная доставка из Китая и она входит в our_costs.

Пример. В позиции сохранено 410 ₽, но platform_sellers.location = 'RU', поэтому UI показывает 0,00 ₽. Это намеренное обнуление, а не отсутствие исходного значения.

YM-17. Цена + маржа — price_with_margin

Плановая закупка с нашей процентной маржой, без комиссий ЯМ, упаковки и прочих надбавок.

price_with_margin = base_price_total × (1 + margin_percent / 100)

margin_percent берётся из e_com.ya_order_items.margin_percent. При создании заказа это сумма базового множителя, рассчитанного по категории/магазину/остаткам, и дополнительной маржинальности группы товара; для товара не со склада дополнительная маржинальность обнуляется.

Пример. 900 × (1 + 29 / 100) = 1 161,00 ₽.

YM-18. Маржа план ₽ — our_margin

Плановая рублёвая маржа на плановой закупке:

our_margin = base_price_total × margin_percent / 100

Если margin_percent пуст, Python подставляет 0.

Пример. 900 × 29% = 261,00 ₽.

YM-19. Маржа факт ₽ — margin_fact_rub

Это алиас уже рассчитанной колонки profit:

df["margin_fact_rub"] = df["profit"]

То есть «фактическая маржа» здесь учитывает market_services, наши промо-списания и our_costs, но не использует fact_commissions. Подробная формула приведена в разделе «Прибыль».

Пример. 289,93 ₽.

YM-20. Маржа план % (от цены продажи) — margin_plan_pct

margin_plan_pct = round(our_margin / sell_price × 100, 2)

При sell_price = 0 знаменатель заменяется на NaN, поэтому результат пустой, а не бесконечность.

Пример. 261 / 2 340 × 100 = 11,1538…% → в данных 11,15%, UI форматирует одним знаком как 11,2 %.

YM-21. Маржа факт % (от цены продажи) — margin_fact_pct

Сначала вычисляется техническая margin_pct, затем копируется:

margin_fact_pct = round(profit / sell_price × 100, 2)

Пример. 289,93 / 2 340 × 100 = 12,390…%12,39%, в UI 12,4 %.

YM-22. Маржа план % (от закупки) — margin_plan_on_cost_pct

margin_plan_on_cost_pct = round(our_margin / base_price_total × 100, 2)

При ненулевой закупке это восстанавливает ya_order_items.margin_percent. База — плановая, а не фактическая закупка.

Пример. 261 / 900 × 100 = 29,00%.

YM-23. Маржа факт % (от закупки) — margin_fact_on_cost_pct

margin_fact_on_cost_pct = round(profit / base_price_total × 100, 2)

Название «от закупки» означает деление именно на плановую base_price_total, даже когда для прибыли применяется более низкая фактическая закупка.

Пример. 289,93 / 900 × 100 = 32,214…%32,21%, в UI 32,2 %.

YM-24. Минимальная цена — min_sell_price_total

Источник за единицу. e_com.ya_order_items.final_price. Это снимок минимально допустимой цены, рассчитанный ya-order-notify в момент создания заказа. Поле modifier_price в текущей записи содержит то же значение, но viewer читает именно final_price.

Упрощённо расчёт снимка включает закупку, плановую маржу, упаковку, обработку, приём платежа, переходник/доставку при необходимости, категорийную комиссию, перевод платежа и ограниченную минимумом/максимумом доставку. Затем итог приводится к целому. ya-updater использует родственную логику для последующих обновлений цены.

Формула таблицы:

min_sell_price_total = ya_order_items.final_price × quantity

Пример. 2 340 × 1 = 2 340,00 ₽.

YM-25. Ожидаемая прибыль — expected_profit

expected_profit = expected_payout − our_costs

promo_discounts в ожидаемую прибыль не входит. our_costs считается по доставленному количеству:

delivered_q = tr_delivered_quantity, если транзакционный отчёт существует
              иначе quantity

our_costs = effective_purchase_price_per_unit × delivered_q
          + ff_fee × delivered_q
          + socket_adapter_fee × delivered_q
          + custom_delivery_fee × delivered_q, только для CN

Для частичного возврата возвращённые единицы не считаются потерянной закупкой: предполагается, что товар вернулся на склад.

Пример. 1 957,99 − 870,00 = 1 087,99 ₽.

YM-26. Цена продажи — sell_price

Основной источник. e_com.ya_order_margin_report.sell_price, лист «Услуги и маржа по заказам», Excel-столбец N (iloc[13]). Это сумма, которую ЯМ признаёт продажей: платёж покупателя плюс субсидия Маркета.

Fallback при отсутствии margin report:

sell_price = (ya_order_items.buyer_price + ya_order_items.subsidy) × quantity

После этого для частичного возврата вычитается сумма sell_price возвратных строк ya_order_transactions_report. Возвратные строки определяются по статусам Возврат оформлен и Невыкуп передан вам. Нельзя вычитать только customer_refund_amount: оно не включает возвращаемую субсидию.

sell_price_final = sell_price − SUM(transaction.sell_price для возвратов)

Гранулярность. margin_report.sell_price относится ко всему заказу и повторяется у каждой позиции multi-item заказа.

Пример. Margin report содержит 2 340,00 ₽, возвратов нет. В транзакционном отчёте эта сумма раскладывается как customer_payment_amount = 1 601 ₽ и bonuses = 739 ₽: 1 601 + 739 = 2 340 ₽.

YM-27. Субсидия ЯМ (справочно, в sell_price) — bonus_points

Источник. Сумма e_com.ya_order_transactions_report.bonuses по позиции. В Excel это лист «Транзакции по заказам и товарам», столбец AJ (iloc[35]).

bonus_points = SUM(transactions.bonuses по ya_order_items_id)

Это скидка/субсидия за счёт ЯМ. Она уже включена в sell_price и показывается только для объяснения состава цены. Прибавление к доходу или прибыли создаст двойной счёт.

Пример. 739,00 ₽; проверка: 1 601 ₽ покупателя + 739 ₽ субсидии = 2 340 ₽ продажи.

YM-28. Промо-расходы (наши баллы) — promo_discounts

Источник. Агрегат e_com.ya_payments_reports.transaction_amount по заказу. Значение намеренно остаётся отрицательным.

Поддерживаются два формата отчёта:

SUM(CASE
  WHEN transaction_source = 'Скидка за участие в совместных акциях'
    OR (transaction_source IS NULL AND payment_status = 'Списание')
  THEN transaction_amount
  ELSE 0
END)

В Excel используются лист «Отчёт о платежах», сумма из U (iloc[20]), источник W (iloc[22]) и статус X (iloc[23]).

Пример. Старый формат содержит два списания: −126,25 ₽ («Буст продаж») и −671,81 ₽ («Размещение товарных предложений»). Сумма: −798,06 ₽.

YM-29. Разница от мин. цены — diff_from_min_price

diff_from_min_price = COALESCE(sell_price, 0) − min_sell_price_total

Положительное значение означает продажу выше снимка минимальной цены, отрицательное — ниже. При отсутствующей цене продажи код подставляет ноль, поэтому получится отрицательная минимальная цена, а не пустое значение.

Пример. 2 340 − 2 340 = 0,00 ₽.

YM-30. Расчётные комиссии — calc_commissions

Это плановые суммы, рассчитанные ya-order-notify при создании заказа и сохранённые в позиции:

calc_commissions =
    ya_order_items.commission_yandex_category_fee_amount
  + ya_order_items.commission_yandex_transfer_payments_fee_amount
  + ya_order_items.commission_yandex_delivery_fee_actual_amount

В каждое пустое поле SQL подставляет 0. Надбавки markup_* сюда не входят: это состав цены, а не комиссии ЯМ.

Расчёт снимка:

  • категорийная комиссия = рассчитанная минимальная цена × ставка категории;
  • комиссия за перевод = минимальная цена × ставка перевода платежа;
  • доставка = процент от предварительной цены, ограниченный настроенными минимумом и максимумом; для ch_mapping равна нулю.

Пример. 481,2972258917 + 50,3174372523 + 0 = 531,614663144 ₽; UI показывает 531,61 ₽.

YM-31. Комиссии (margin_report) — market_services

Источник. e_com.ya_order_margin_report.market_services, лист «Услуги и маржа по заказам», Excel-столбец M (iloc[12]). Расчёта в viewer нет; пустое значение становится нулём только при последующих формулах.

Это основной источник комиссий для экономики заказа. В отличие от расчётных комиссий, сумма пришла из итогового отчёта ЯМ.

Гранулярность. Сумма относится ко всему заказу и повторяется у всех его позиций.

Пример. 382,01 ₽.

YM-32. Факт. комиссии (ЛК) — fact_commissions

Источник. Фактические отрицательные строки e_com.ya_payments_reports.transaction_amount, агрегированные по заказу. Сумма превращается в положительное число:

SUM(CASE
  WHEN transaction_amount < 0
   AND (
     transaction_source = 'Оплата услуг Яндекс.Маркета'
     OR (transaction_source IS NULL AND payment_status = 'Удержание')
   )
  THEN -transaction_amount
  ELSE 0
END)

Новый формат определяется по transaction_source, старый — по payment_status. Промо-строки со статусом Списание исключаются.

Не участвует в прибыли. Колонка диагностическая. expected_payout использует market_services, а не fact_commissions.

Пример и обнаруженное расхождение. У заказа один полный набор удержаний составляет:

0,12 + 320,00 + 53,82 + 1,28 + 6,79 = 382,01 ₽

В ya_payments_reports этот набор записан дважды (десять строк вместо пяти), поэтому SQL честно возвращает 764,02 ₽. Это объясняет отличие от market_services = 382,01 ₽ и указывает на дубль исходных строк/слияние файлов отчёта, а не на формулу прибыли.

YM-33. Доход за вычетом комиссий — income_after_fees

В текущем коде это прямой алиас ожидаемой выплаты:

income_after_fees = expected_payout
                  = max(sell_price − market_services, 0)

fact_commissions не используется.

Пример. 2 340 − 382,01 = 1 957,99 ₽.

YM-34. Прибыль — profit

Основная фактическая экономика с учётом наших промо-расходов:

profit = expected_payout + promo_discounts − our_costs

Так как promo_discounts отрицательные, операция + уменьшает прибыль. Затраты при возврате считаются только по доставленным единицам.

Пример. 1 957,99 + (−798,06) − 870 = 289,93 ₽.

YM-35. Разница Прибыли Факт/Ожид — profit_vs_expected

Несмотря на название, «факт» здесь — actual_profit, а не колонка profit:

profit_vs_expected = actual_profit − expected_profit

actual_profit не учитывает promo_discounts, поэтому у обычного оплаченного заказа оно совпадает с expected_profit, а разница равна нулю. Эта колонка не показывает влияние промо; для него сравнивайте profit и profit_no_promo.

Пример. 1 087,99 − 1 087,99 = 0,00 ₽, хотя profit = 289,93 ₽.

YM-36. Доход за вычетом комиссий и промо — income_after_fees_promo

income_after_fees_promo = expected_payout + promo_discounts

Закупка и упаковка здесь ещё не вычтены.

Пример. 1 957,99 − 798,06 = 1 159,93 ₽.

YM-37. Прибыль без учёта промо-расходов — profit_no_promo

profit_no_promo = income_after_fees − our_costs
                = expected_payout − our_costs

Для текущей модели это то же арифметическое значение, что expected_profit.

Пример. 1 957,99 − 870 = 1 087,99 ₽.

YM-38. Штраф за отмену заказа — seller_cancel_penalty

Источник. ya_payments_reports, строки с точным именем услуги Отмена заказа по вине продавца и отрицательной суммой:

SUM(CASE
  WHEN item_name_or_service_name = 'Отмена заказа по вине продавца'
   AND transaction_amount < 0
  THEN -transaction_amount ELSE 0
END)

Excel-поля: название S (iloc[18]), сумма U (iloc[20]). Результат показывается положительным расходом.

Пример. Подходящих строк нет → 0,00 ₽.

YM-39. Штраф за позднюю отгрузку — late_ship_penalty

Аналогичный агрегат платёжного отчёта для точного названия Отгрузка или доставка не вовремя:

SUM(-transaction_amount для отрицательных подходящих строк)

Пример. Подходящих строк нет → 0,00 ₽.

YM-40. Нам перевели за заказ — payout_if_paid

Это не сумма строк платёжного отчёта. Viewer проверяет ya_order_margin_report.payment_status:

если payment_status ∈ {'Переведён', 'Удержан из платежей покупателей'}:
    payout_if_paid = expected_payout
иначе:
    payout_if_paid = 0

Пример. Статус Переведён1 957,99 ₽.

YM-41. Сумма выплаты (если нет даты, то ожидаемая) — expected_payout

Название историческое: формула не зависит от даты выплаты.

expected_payout = max(COALESCE(sell_price, 0) − COALESCE(market_services, 0), 0)

Ограничение снизу не позволяет показать отрицательную выплату. Фактические строки ya_payments_reports не суммируются: отчёт смешивает перечисления, комиссии и промо, а старые выгрузки могут содержать дубли.

Пример. max(2 340 − 382,01, 0) = 1 957,99 ₽.

YM-42. Дата выплаты — last_payment_date

Два источника с приоритетом транзакционного отчёта.

  1. MAX(ya_order_transactions_report.customer_payment_date) по позиции — лист «Транзакции по заказам и товарам», Excel AG (iloc[32]).
  2. Fallback из ya_payments_reports.payment_date:
  3. новый формат: transaction_source IS NOT NULL и статус Удержан из платежей покупателей или Переведён по графику выплат;
  4. старый формат: transaction_source IS NULL и payment_status = 'Начисление'.

Python приводит обе серии к UTC и применяет transaction_date.fillna(payment_report_date). UI показывает DD.MM.YYYY.

Пример. Платёжный отчёт старого формата даёт максимум 01.04.2026, но транзакционный отчёт содержит 20.04.2026 и имеет приоритет. UI показывает 20.04.2026.

YM-43. Фактическая прибыль — actual_profit

Сначала определяется факт оплаты:

has_payment = last_payment_date заполнена
              ИЛИ payment_status считается оплаченным

Затем:

если оплаты нет:
    actual_profit = 0
иначе если заказ отменён до отгрузки:
    actual_profit = expected_payout
                  − seller_cancel_penalty
                  − late_ship_penalty
иначе:
    actual_profit = expected_payout − our_costs

Отмена «до отгрузки» распознаётся по статусам Заказ отменен до обработки, Отменен при обработке и Отменён; закупочные затраты в этой ветке считаются непонесёнными. Для доставленных/возвращённых заказов используется полный расчёт затрат доставленного количества.

Критически важно. actual_profit не вычитает promo_discounts, поэтому не равно основной колонке profit при наличии промо.

Пример. Оплата есть, заказ доставлен: 1 957,99 − 870 = 1 087,99 ₽.

Исходные строки примера

Ниже минимальный набор фактов, достаточный для ручного воспроизведения результата.

ya_orders

id=43367
order_id=55830644865
seller_id=2
status=DELIVERED
sub_status=DELIVERY_SERVICE_DELIVERED
created_at=2026-04-06T21:24:03.930000+00:00
shipment_date=2026-04-09T00:00:00+00:00

ya_order_items

id=27032, feed_item_id=1812, offer_id=BRITVS100, count=1
base_price=900, margin_percent=29, final_price=2340
supplier_name=Opt-instock, subsidy=739
markup_ff_fees_amount=50
markup_socket_adapter_fee_amount=0
markup_custom_delivery_fee_value_amount=410
commission_yandex_category_fee_amount=481.29722589167767
commission_yandex_transfer_payments_fee_amount=50.317437252311755
commission_yandex_delivery_fee_actual_amount=0

ya_order_margin_report

sell_price=2340.00
market_services=382.01
status=Доставлен
payment_status=Переведён

ya_order_transactions_report

status=Доставлен покупателю
sell_price=2340.00
customer_payment_amount=1601.00
bonuses=739.00
other_market_discounts=739.00
customer_payment_date=2026-04-20
customer_refund_amount=0

Складская фактическая цена

all_split_orders.id=58908
order_to_supplier.id=56074
currency=RUB
ru_custom_price=NULL
ru_price=820.00

ya_payments_reports

Промо-строки:

Услуга Статус Сумма
Буст продаж, оплата за продажи Списание −126,25 ₽
Размещение товарных предложений Списание −671,81 ₽

Один набор комиссий:

Услуга Статус Сумма
Приём платежа Удержание −0,12 ₽
Начисления за доставку Удержание −320,00 ₽
Перевод платежа Удержание −53,82 ₽
Буст продаж, оплата за продажи Удержание −1,28 ₽
Размещение товарных предложений Удержание −6,79 ₽

Этот набор удержаний присутствует в таблице дважды; поэтому диагностическая сумма fact_commissions удваивается до 764,02 ₽.

Известные ограничения и ловушки

Заказные деньги повторяются у каждой позиции

sell_price, market_services, агрегаты платежей и их производные присоединяются по ya_order_id. В multi-item заказе они повторяются. Нельзя получать выручку простым SUM по строкам таблицы; сначала дедуплицируйте заказ либо разработайте правило распределения.

Основная прибыль не использует «Факт. комиссии (ЛК)»

profit строится от margin_report.market_services. fact_commissions служит контролем. Поэтому дубль платежных строк не портит основную прибыль примера, но сигнализирует о качестве импорта.

«Фактическая прибыль» не учитывает промо

actual_profit подтверждает наличие выплаты и вычитает затраты, но не promo_discounts. Для итоговой экономики с промо смотрите profit / margin_fact_rub.

Упаковка и переходник берутся из текущих справочников

Хотя заказ хранит markup_ff_fees_amount и markup_socket_adapter_fee_amount, запрос viewer берёт ff_fees.value и socket_adapter_fee.value через текущий модификатор товара. Изменение справочника способно ретроспективно изменить старые строки.

Китайская доставка зависит от локации магазина

Ненулевой markup_custom_delivery_fee_value_amount у RU-магазина отображается как ноль и не входит в затраты. Это видно на заказе примера: исходно 410 ₽, итог 0 ₽.

Дата выплаты — гибрид двух отчётов

Дата транзакционного отчёта приоритетнее платёжного, даже если платёжный отчёт содержит другую дату. При расследовании всегда смотрите оба источника.

Кэш Streamlit

Загрузчики помечены @st.cache_data с TTL из CACHE_TTL (по умолчанию 3600 секунд). Только что импортированный отчёт может не появиться до истечения TTL или очистки кэша.

Округление

Деньги рассчитываются в pandas как числовые значения и только форматируются в UI до двух знаков. Расчётные комиссии могут хранить длинную дробную часть. Процентные колонки округляются до двух знаков в данных, а UI показывает один.

Диагностика расхождения

Проверяйте сверху вниз:

  1. найдите ya_orders.id по внешнему order_id;
  2. посчитайте позиции и убедитесь, что понимаете их гранулярность;
  3. сравните ya_order_margin_report.sell_price и market_services;
  4. проверьте возвратные строки ya_order_transactions_report;
  5. сгруппируйте ya_payments_reports по transaction_source, payment_status, названию и сумме; найдите дубли;
  6. проследите all_split_orders → stock_movement_transactions → order_to_supplier;
  7. воспроизведите our_costs, затем expected_payout, profit_no_promo и profit;
  8. учитывайте TTL кэша.

Безопасный базовый запрос для поиска сущностей:

SELECT o.id AS ya_order_id, i.id AS item_id, i.offer_id, i.count
FROM e_com.ya_orders o
JOIN e_com.ya_order_items i ON i.order_id = o.id
WHERE o.order_id = 55830644865;

Проверка дублей платёжного отчёта:

SELECT
  item_name_or_service_name,
  transaction_source,
  payment_status,
  transaction_amount,
  COUNT(*) AS rows_count
FROM e_com.ya_payments_reports
WHERE order_id = 55830644865
GROUP BY 1, 2, 3, 4
ORDER BY rows_count DESC, item_name_or_service_name;

Где менять поведение

Задача Проект и файл
Добавить/убрать/переименовать колонку wolle-economy-viewer/src/wolle_economy/ui/columns.py
Изменить основной SQL и JOIN wolle-economy-viewer/src/wolle_economy/db/queries.py
Изменить порядок объединения запросов wolle-economy-viewer/src/wolle_economy/domain/loader.py
Изменить финансовую формулу wolle-economy-viewer/src/wolle_economy/domain/economics.py
Изменить формат/скрытие UI wolle-economy-viewer/src/wolle_economy/ui/components/orders/table.py
Изменить импорт united-orders ya-reports/backend/src/services/orders_to_db.py
Изменить импорт united-netting ya-reports/backend/src/services/payments_to_db.py
Изменить снимок цены и комиссий нового заказа ya-order-notify/backend/src/services/items_calculator.py и repository/ya_orders.py
Изменить последующее ценообразование предложения ya-updater/backend/src/services/items_calculator.py и services/ya_sync.py

После изменения логики обновите этот гайд, пример и dist/ в одном коммите.

Зафиксированные ревизии источников

Проект Ветка Commit
wolle-economy-viewer main ed9619c85b97f9f0820f537db30ae9c4c195a51f
ya-reports master ed2c18c393f9bfc80a5c0fe936cfc33857f31acb
ya-updater master 12a5d76e85622df34837e6158809712fa834d530
ya-order-notify main 931294ecf2cef809cc07f71db29ca7ef81b2f8ea

wolle-economy-viewer, ya-reports и ya-order-notify были без незакоммиченных изменений. В ya-updater присутствовали два неотслеживаемых Markdown-файла (calculation_2526849.md, example.md); они использовались только как дополнительный контекст и не меняли исполняемый код.