fix: revive main dashboard analytics
Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Failing after 2m38s
CI/CD Pipeline / Run Tests (push) Successful in 2m44s
CI/CD Pipeline / Build and Push Dev Images (push) Has been skipped
CI/CD Pipeline / Deploy Dev via Compose (push) Has been skipped

This commit is contained in:
2026-06-16 00:00:14 +02:00
parent f93663b2a4
commit 94aa22ce35
2 changed files with 136 additions and 18 deletions

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
from collections import defaultdict
from collections.abc import Iterable
from datetime import date
from decimal import Decimal
from apps.core.exceptions import NotFoundError
@@ -20,7 +19,11 @@ from apps.organization.availability import (
risk_level_for_availability,
)
from apps.organization.models import IndustryCluster, Organization
from apps.organization.scope_utils import filter_queryset_by_scopes
from apps.organization.scope_utils import (
SCOPE_LABELS,
filter_queryset_by_scopes,
scopes_from_organization_fields,
)
from django.db.models import Avg, Case, Count, IntegerField, Q, Sum, When
ZERO = Decimal("0")
@@ -1145,6 +1148,37 @@ class OrganizationAnalyticsService:
class DashboardAnalyticsService:
"""Cross-organization dashboard aggregations."""
@staticmethod
def _resolve_dashboard_cluster(organization: Organization) -> str:
if organization.cluster:
return organization.cluster
scope_codes = scopes_from_organization_fields(
gk_code=organization.gk_code,
gk_name=organization.gk_name,
opk_registry_membership=organization.opk_registry_membership,
)
for scope_code in scope_codes:
if scope_code in {"rosatom", "roscosmos"}:
return scope_code
return scope_codes[0] if scope_codes else IndustryCluster.OTHER
@staticmethod
def _dashboard_cluster_label(cluster_code: str) -> str:
industry_cluster_label = dict(IndustryCluster.choices).get(cluster_code)
if industry_cluster_label:
return industry_cluster_label
return SCOPE_LABELS.get(cluster_code, "Иная")
@staticmethod
def _resolve_executors_total(organizations: list[Organization]) -> int:
executors_total = sum(
organization.executors_count for organization in organizations
)
if executors_total:
return executors_total
return sum(1 for organization in organizations if organization.goz_participation)
@classmethod
def get_dashboard(
cls, *, corporation_scope: str | None = None
@@ -1165,32 +1199,27 @@ class DashboardAnalyticsService:
totals_by_cluster: dict[str, list[Organization]] = defaultdict(list)
for organization in organizations:
cluster = organization.cluster or IndustryCluster.OTHER
cluster = cls._resolve_dashboard_cluster(organization)
totals_by_cluster[cluster].append(organization)
total_organizations = len(organizations)
current_year = date.today().year
previous_year = current_year - 1
f3_records = (
FormF3Record.objects.filter(
organization__in=organizations,
is_active_version=True,
report_year__in=[previous_year, current_year],
)
.select_related("organization")
.order_by(
"organization_id", "report_year", "-report_quarter", "-created_at"
)
)
f3_best: dict[tuple[str, int], FormF3Record] = {}
f3_best_by_organization: dict[str, dict[int, FormF3Record]] = defaultdict(dict)
for record in f3_records:
key = (str(record.organization_id), record.report_year)
f3_best.setdefault(key, record)
def cluster_label(cluster_code: str) -> str:
return dict(IndustryCluster.choices).get(cluster_code, "Иная")
organization_key = str(record.organization_id)
f3_best_by_organization[organization_key].setdefault(
record.report_year, record
)
total_organizations = len(organizations)
distribution_by_cluster = []
executors_by_cluster = []
headcount_growth_by_cluster = []
@@ -1198,15 +1227,21 @@ class DashboardAnalyticsService:
for cluster_code, cluster_organizations in sorted(totals_by_cluster.items()):
cluster_total = len(cluster_organizations)
executors_total = sum(org.executors_count for org in cluster_organizations)
executors_total = cls._resolve_executors_total(cluster_organizations)
bankruptcy_free = sum(
1 for org in cluster_organizations if not org.bankruptcy_messages_found
)
growth_values = []
for organization in cluster_organizations:
current_record = f3_best.get((str(organization.id), current_year))
previous_record = f3_best.get((str(organization.id), previous_year))
organization_f3_by_year = f3_best_by_organization.get(
str(organization.id), {}
)
report_years = sorted(organization_f3_by_year)
if len(report_years) < 2:
continue
previous_record = organization_f3_by_year[report_years[-2]]
current_record = organization_f3_by_year[report_years[-1]]
if current_record is None or previous_record is None:
continue
growth_values.append(
@@ -1218,7 +1253,7 @@ class DashboardAnalyticsService:
distribution_by_cluster.append(
{
"cluster": cluster_code,
"cluster_label": cluster_label(cluster_code),
"cluster_label": cls._dashboard_cluster_label(cluster_code),
"organizations_share_percent": round(
(cluster_total / total_organizations) * 100, 1
),

View File

@@ -528,6 +528,89 @@ class OrganizationAnalyticsApiTest(APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["corporation_scope"], "roscosmos")
def test_dashboard_endpoint_groups_blank_clusters_by_corporation_scope(self):
OrganizationFactory.create(
cluster="",
executors_count=0,
gk_code="1",
gk_name='Госкорпорация "Роскосмос"',
)
response = self.client.get(
"/api/v1/analytics/dashboard/?corporation_scope=roskosmos"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data["distribution_by_cluster"][0]["cluster"], "roscosmos"
)
self.assertEqual(
response.data["distribution_by_cluster"][0]["cluster_label"],
"Госкорпорация «Роскосмос»",
)
def test_dashboard_endpoint_counts_goz_organizations_when_executors_are_missing(
self,
):
OrganizationFactory.create(
cluster="",
executors_count=0,
gk_code="1",
gk_name='Госкорпорация "Роскосмос"',
goz_participation=True,
)
OrganizationFactory.create(
cluster="",
executors_count=0,
gk_code="1",
gk_name='Госкорпорация "Роскосмос"',
goz_participation=False,
)
response = self.client.get(
"/api/v1/analytics/dashboard/?corporation_scope=roskosmos"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data["executors_by_cluster"][0]["cluster"], "roscosmos"
)
self.assertEqual(
response.data["executors_by_cluster"][0]["executors_count"], 1
)
def test_dashboard_endpoint_uses_latest_available_f3_years_for_growth(self):
organization = OrganizationFactory.create(
cluster="space",
executors_count=0,
gk_code="1",
gk_name='Госкорпорация "Роскосмос"',
)
FormF3RecordFactory.create(
organization=organization,
report_year=2023,
report_quarter=4,
avg_employees=100,
)
FormF3RecordFactory.create(
organization=organization,
report_year=2024,
report_quarter=4,
avg_employees=150,
)
response = self.client.get(
"/api/v1/analytics/dashboard/?corporation_scope=roskosmos"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data["headcount_growth_by_cluster"][0]["cluster"], "space"
)
self.assertEqual(
response.data["headcount_growth_by_cluster"][0]["growth_percent"], 50.0
)
def test_analytics_query_validation(self):
response = self.client.get(
f"/api/v1/organizations/{self.organization.id}/analytics/economics/"