0041. Статистика возвратов по контрагентам
Дата: 2026-04-15
Статус
Accepted
Контекст
В ERP Narmak нужен сводный рейтинг контрагентов по возвратам в двух бизнес-ролях:
- Поставщики — насколько «шумны» возвраты при закупках (отношение возвратов поставщику к объёму поступлений).
- Покупатели — насколько часто покупатели возвращают товар относительно объёма отгрузок.
Данные живут в универсальной модели Documents (тип документа задаётся type__key). Нужны быстрые отчёты по отфильтрованному списку контрагентов, периоду и (опционально) закреплённым менеджерам, без дублирования логики в двух почти одинаковых эндпоинтах.
Решение
Разделение направлений (Strategy)
Неизменяемый датакласс ReturnDirection (app/counterparty/services/return_statistics_strategy.py) задаёт пару типов документов для одного направления:
| Направление | return_type_key (возвраты) | base_doc_key (базовый объём) | label |
|---|---|---|---|
| Поставщики | purchasereturn | supply | suppliers |
| Покупатели | salesreturn | demand | customers |
Константы 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-awaredatetime(конец периода — конец суток). Если границы не заданы — последние 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_classA/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)(имена полей — по фактической схеме проекта).
см. также
- Каталог ADR — все архитектурные решения
- 0016–0021 Document — смежная доменная логика документов
- Техническая документация Backend — Django, DRF, REST v2
Ссылки на код (репозиторий backend)
app/counterparty/services/return_statistics_strategy.pyapp/counterparty/services/return_statistics_service.pyapp/counterparty/views/report_views.py(actionsreturn_statistics_*)app/counterparty/serializers.py(ReturnStatistics*)