Skip to content

0041. Статистика возвратов по контрагентам

Дата: 2026-04-15

Статус

Accepted

Контекст

В ERP Narmak нужен сводный рейтинг контрагентов по возвратам в двух бизнес-ролях:

  1. Поставщики — насколько «шумны» возвраты при закупках (отношение возвратов поставщику к объёму поступлений).
  2. Покупатели — насколько часто покупатели возвращают товар относительно объёма отгрузок.

Данные живут в универсальной модели Documents (тип документа задаётся type__key). Нужны быстрые отчёты по отфильтрованному списку контрагентов, периоду и (опционально) закреплённым менеджерам, без дублирования логики в двух почти одинаковых эндпоинтах.

Решение

Разделение направлений (Strategy)

Неизменяемый датакласс ReturnDirection (app/counterparty/services/return_statistics_strategy.py) задаёт пару типов документов для одного направления:

Направлениеreturn_type_key (возвраты)base_doc_key (базовый объём)label
Поставщикиpurchasereturnsupplysuppliers
Покупателиsalesreturndemandcustomers

Константы SUPPLIER_DIRECTION и CUSTOMER_DIRECTION, словарь DIRECTION_BY_QUERY для детального API (direction=suppliers|customers).

Фасад расчёта (Facade)

Вся бизнес-логика — в ReturnStatisticsService (app/counterparty/services/return_statistics_service.py):

  • Парсинг периода periods__gte / periods__lte: строки из query, date, datetime приводятся к timezone-aware datetime (конец периода — конец суток). Если границы не заданы — последние 365 суток от timezone.now().
  • В выборку контрагентов попадают только те, у кого есть хотя бы один «ролевой» документ по направлению: Exists на Documents с type__key__in:
    • поставщики: purchaseorder, invoicein, supply;
    • покупатели: customerorder, invoiceout, demand.
  • Агрегаты по периоду для каждого контрагента: сумма и количество базовых документов, сумма и количество возвратов, дата последнего возврата — через Subquery к Documents с фильтром applicable=True, is_deleted=False, moment в интервале. У подзапросов сумм/счётчиков в конце цепочки [:1], чтобы PostgreSQL всегда получал скаляр (избежание CardinalityViolation).
  • После получения строк списка менеджеры assigned_to_employee подгружаются отдельным запросом (ArrayAgg по through-модели, группировка по counterparty_id), а не вложенным Subquery(ArrayAgg(...)) в annotate внешнего queryset — иначе в Postgres легко получить подзапрос «не ровно одна строка».
  • Тренд (up / down / stable): период делится пополам по времени; для каждой половины считается отношение суммы возвратов к сумме базы; сравнение с порогом ±5%.
  • Рейтинг надёжности reliability_score (0–100) и класс reliability_class A / B / C — весовая сумма пяти подбаллов (W_RATE, W_FREQ, W_TREND, W_AVG, W_DAYS в сервисе), с ограничениями на долю возвратов, частоту, тренд, соотношение средних чеков возврата и базы, «свежесть» последнего возврата. Класс: ≥80 → A, ≥50 → B, иначе C.
  • Список в ответе сортируется по reliability_score по убыванию.

Детализация по одному контрагенту — get_counterparty_return_detail: те же агрегаты плюс помесячная разбивка (TruncMonth), топ позиций возврата по сумме, те же менеджеры через bulk-загрузку.

REST API

ViewSet CounterPartyReportViewSet, базовый путь API v2: counterparties-reports (narmak/api_url_v2.py).

МетодURL (относительно префикса v2)Назначение
GET.../counterparties-reports/return_statistics_suppliers/Список по поставщикам
GET.../counterparties-reports/return_statistics_customers/Список по покупателям
GET.../counterparties-reports/{id}/return_statistics_detail/?direction=...Деталь по контрагенту

Фильтрация списка контрагентов — ReportCounterPartyFilter: в т.ч. id, periods (границы передаются в сервис через filterset.data), assigned_to_employee.

Сериализаторы ответа: ReturnStatisticsSerializer, ReturnStatisticsDetailSerializer (app/counterparty/serializers.py).

Клиент (main-office)

Страница отчёта, SDK-модель и вызовы API — в репозитории narmak-nx-angular-main (маршрут контрагентов, пункт меню с правом counterparty_return_statistics, опционально виджет в карточке контрагента). Параметры периода на клиенте мапятся в periods__gte / periods__lte.

Последствия

  • Изменение весов или порогов классов — только в ReturnStatisticsService (и при необходимости подстройка UI/легенды).
  • Новое направление (например, отдельная ветка по маркетплейсу) — новый экземпляр ReturnDirection + отдельный action или query-параметр, без копирования SQL.
  • Типы документов жёстко завязаны на type__key в справочнике типов; переименование ключей потребует правки стратегии.
  • Производительность на больших периодах зависит от индексов по Documents(conterparty_id, type_id, moment, applicable, is_deleted) (имена полей — по фактической схеме проекта).

см. также

Ссылки на код (репозиторий backend)

  • app/counterparty/services/return_statistics_strategy.py
  • app/counterparty/services/return_statistics_service.py
  • app/counterparty/views/report_views.py (actions return_statistics_*)
  • app/counterparty/serializers.py (ReturnStatistics*)