fix: revive main dashboard analytics
This commit is contained in:
@@ -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
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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/"
|
||||||
|
|||||||
Reference in New Issue
Block a user