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 import defaultdict
from collections.abc import Iterable from collections.abc import Iterable
from datetime import date
from decimal import Decimal from decimal import Decimal
from apps.core.exceptions import NotFoundError from apps.core.exceptions import NotFoundError
@@ -20,7 +19,11 @@ from apps.organization.availability import (
risk_level_for_availability, risk_level_for_availability,
) )
from apps.organization.models import IndustryCluster, Organization 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 from django.db.models import Avg, Case, Count, IntegerField, Q, Sum, When
ZERO = Decimal("0") ZERO = Decimal("0")
@@ -1145,6 +1148,37 @@ class OrganizationAnalyticsService:
class DashboardAnalyticsService: class DashboardAnalyticsService:
"""Cross-organization dashboard aggregations.""" """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 @classmethod
def get_dashboard( def get_dashboard(
cls, *, corporation_scope: str | None = None cls, *, corporation_scope: str | None = None
@@ -1165,32 +1199,27 @@ class DashboardAnalyticsService:
totals_by_cluster: dict[str, list[Organization]] = defaultdict(list) totals_by_cluster: dict[str, list[Organization]] = defaultdict(list)
for organization in organizations: for organization in organizations:
cluster = organization.cluster or IndustryCluster.OTHER cluster = cls._resolve_dashboard_cluster(organization)
totals_by_cluster[cluster].append(organization) totals_by_cluster[cluster].append(organization)
total_organizations = len(organizations)
current_year = date.today().year
previous_year = current_year - 1
f3_records = ( f3_records = (
FormF3Record.objects.filter( FormF3Record.objects.filter(
organization__in=organizations, organization__in=organizations,
is_active_version=True, is_active_version=True,
report_year__in=[previous_year, current_year],
) )
.select_related("organization") .select_related("organization")
.order_by( .order_by(
"organization_id", "report_year", "-report_quarter", "-created_at" "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: for record in f3_records:
key = (str(record.organization_id), record.report_year) organization_key = str(record.organization_id)
f3_best.setdefault(key, record) f3_best_by_organization[organization_key].setdefault(
record.report_year, record
def cluster_label(cluster_code: str) -> str: )
return dict(IndustryCluster.choices).get(cluster_code, "Иная")
total_organizations = len(organizations)
distribution_by_cluster = [] distribution_by_cluster = []
executors_by_cluster = [] executors_by_cluster = []
headcount_growth_by_cluster = [] headcount_growth_by_cluster = []
@@ -1198,15 +1227,21 @@ class DashboardAnalyticsService:
for cluster_code, cluster_organizations in sorted(totals_by_cluster.items()): for cluster_code, cluster_organizations in sorted(totals_by_cluster.items()):
cluster_total = len(cluster_organizations) 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( bankruptcy_free = sum(
1 for org in cluster_organizations if not org.bankruptcy_messages_found 1 for org in cluster_organizations if not org.bankruptcy_messages_found
) )
growth_values = [] growth_values = []
for organization in cluster_organizations: for organization in cluster_organizations:
current_record = f3_best.get((str(organization.id), current_year)) organization_f3_by_year = f3_best_by_organization.get(
previous_record = f3_best.get((str(organization.id), previous_year)) 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: if current_record is None or previous_record is None:
continue continue
growth_values.append( growth_values.append(
@@ -1218,7 +1253,7 @@ class DashboardAnalyticsService:
distribution_by_cluster.append( distribution_by_cluster.append(
{ {
"cluster": cluster_code, "cluster": cluster_code,
"cluster_label": cluster_label(cluster_code), "cluster_label": cls._dashboard_cluster_label(cluster_code),
"organizations_share_percent": round( "organizations_share_percent": round(
(cluster_total / total_organizations) * 100, 1 (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.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["corporation_scope"], "roscosmos") 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): def test_analytics_query_validation(self):
response = self.client.get( response = self.client.get(
f"/api/v1/organizations/{self.organization.id}/analytics/economics/" f"/api/v1/organizations/{self.organization.id}/analytics/economics/"