fix: serve latest organization analytics data
Some checks failed
CI/CD Pipeline / Run Tests (push) Failing after 2m53s
CI/CD Pipeline / Code Quality Checks (push) Successful in 3m12s
CI/CD Pipeline / Build Docker Images (push) Has been skipped
CI/CD Pipeline / Push to Gitea Registry (push) Has been skipped
CI/CD Pipeline / Deploy to Server (push) Has been skipped
Some checks failed
CI/CD Pipeline / Run Tests (push) Failing after 2m53s
CI/CD Pipeline / Code Quality Checks (push) Successful in 3m12s
CI/CD Pipeline / Build Docker Images (push) Has been skipped
CI/CD Pipeline / Push to Gitea Registry (push) Has been skipped
CI/CD Pipeline / Deploy to Server (push) Has been skipped
This commit is contained in:
@@ -14,6 +14,11 @@ from apps.form_3.models import FormF3Record
|
||||
from apps.form_4.models import FormF4Record
|
||||
from apps.form_5.models import FormF5Record
|
||||
from apps.form_6.models import FormF6Record
|
||||
from apps.organization.availability import (
|
||||
has_financial_reports,
|
||||
has_tax_reports,
|
||||
risk_level_for_availability,
|
||||
)
|
||||
from apps.organization.models import IndustryCluster, Organization
|
||||
from apps.organization.scope_utils import filter_queryset_by_scopes
|
||||
from django.db.models import Avg, Case, Count, IntegerField, Q, Sum, When
|
||||
@@ -103,6 +108,26 @@ def _best_records_by_year(
|
||||
return resolved
|
||||
|
||||
|
||||
def _latest_report_year(records: Iterable) -> int | None:
|
||||
years = [
|
||||
record.report_year
|
||||
for record in records
|
||||
if getattr(record, "report_year", None) is not None
|
||||
]
|
||||
return max(years, default=None)
|
||||
|
||||
|
||||
def _resolve_report_year(records: Iterable, requested_year: int) -> int:
|
||||
years = {
|
||||
record.report_year
|
||||
for record in records
|
||||
if getattr(record, "report_year", None) is not None
|
||||
}
|
||||
if requested_year in years:
|
||||
return requested_year
|
||||
return max(years, default=requested_year)
|
||||
|
||||
|
||||
def _weighted_average_age(age_distribution: list[dict[str, int]]) -> float:
|
||||
bucket_midpoints = {
|
||||
"under_30": 25,
|
||||
@@ -331,8 +356,27 @@ class OrganizationAnalyticsService:
|
||||
|
||||
periods = sorted(set(f2_by_year) | set(f4_by_year))
|
||||
if not periods:
|
||||
latest_year = max(
|
||||
filter(
|
||||
None,
|
||||
(
|
||||
_latest_report_year(cls._f2_records(organization)),
|
||||
_latest_report_year(cls._f4_records(organization)),
|
||||
),
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
if latest_year is None:
|
||||
raise NotFoundError(message="Economics data is not available")
|
||||
|
||||
f2_by_year = _best_records_by_year(
|
||||
cls._f2_records(organization), latest_year, latest_year
|
||||
)
|
||||
f4_by_year = _best_records_by_year(
|
||||
cls._f4_records(organization), latest_year, latest_year
|
||||
)
|
||||
periods = sorted(set(f2_by_year) | set(f4_by_year))
|
||||
|
||||
metric_units = cls._economics_metric_units()
|
||||
selected_metrics = cls._economics_metric_groups()[group]
|
||||
last_period = periods[-1]
|
||||
@@ -407,6 +451,7 @@ class OrganizationAnalyticsService:
|
||||
history_years: int,
|
||||
) -> dict[str, object]:
|
||||
f3_records = cls._f3_records(organization)
|
||||
report_year = _resolve_report_year(f3_records, report_year)
|
||||
current_f3 = cls._require_record(
|
||||
_pick_record(f3_records, report_year),
|
||||
entity="Personnel",
|
||||
@@ -460,6 +505,7 @@ class OrganizationAnalyticsService:
|
||||
report_year: int,
|
||||
) -> dict[str, object]:
|
||||
f3_records = cls._f3_records(organization)
|
||||
report_year = _resolve_report_year(f3_records, report_year)
|
||||
current_f3 = cls._require_record(
|
||||
_pick_record(f3_records, report_year),
|
||||
entity="Equipment",
|
||||
@@ -813,11 +859,9 @@ class OrganizationAnalyticsService:
|
||||
frequency: str,
|
||||
price_mode: str,
|
||||
) -> dict[str, object]:
|
||||
records = [
|
||||
record
|
||||
for record in cls._f1_records(organization)
|
||||
if record.report_year == report_year
|
||||
]
|
||||
f1_records = cls._f1_records(organization)
|
||||
report_year = _resolve_report_year(f1_records, report_year)
|
||||
records = [record for record in f1_records if record.report_year == report_year]
|
||||
if not records:
|
||||
raise NotFoundError(message="Products data is not available")
|
||||
|
||||
@@ -917,14 +961,21 @@ class OrganizationAnalyticsService:
|
||||
|
||||
@classmethod
|
||||
def get_risk_profile(cls, *, organization: Organization) -> dict[str, object]:
|
||||
financial_reports_available = has_financial_reports(organization)
|
||||
tax_reports_available = has_tax_reports(organization)
|
||||
|
||||
return {
|
||||
"organization_id": str(organization.id),
|
||||
"financial_reports_available": organization.financial_reports_available,
|
||||
"tax_reports_available": organization.tax_reports_available,
|
||||
"financial_reports_available": financial_reports_available,
|
||||
"tax_reports_available": tax_reports_available,
|
||||
"in_defense_unreliable_suppliers_registry": organization.in_defense_unreliable_suppliers_registry,
|
||||
"in_275_fz_registry": organization.in_275_fz_registry,
|
||||
"bankruptcy_messages_found": organization.bankruptcy_messages_found,
|
||||
"risk_level": organization.risk_level,
|
||||
"risk_level": risk_level_for_availability(
|
||||
organization,
|
||||
financial_reports_available=financial_reports_available,
|
||||
tax_reports_available=tax_reports_available,
|
||||
),
|
||||
"updated_at": organization.updated_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
48
src/apps/organization/availability.py
Normal file
48
src/apps/organization/availability.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""Availability helpers for organization-facing contracts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from apps.organization.models import Organization
|
||||
|
||||
|
||||
def has_financial_reports(organization: Organization) -> bool:
|
||||
"""Return whether financial data is effectively available for an organization."""
|
||||
if organization.financial_reports_available:
|
||||
return True
|
||||
|
||||
return (
|
||||
organization.financial_reports.filter(status="success").exists()
|
||||
or organization.form_f2_records.filter(is_active_version=True).exists()
|
||||
)
|
||||
|
||||
|
||||
def has_tax_reports(organization: Organization) -> bool:
|
||||
"""Return whether tax-related report data is effectively available."""
|
||||
if organization.tax_reports_available:
|
||||
return True
|
||||
|
||||
return organization.form_f2_records.filter(
|
||||
is_active_version=True,
|
||||
income_tax__isnull=False,
|
||||
).exists()
|
||||
|
||||
|
||||
def risk_level_for_availability(
|
||||
organization: Organization,
|
||||
*,
|
||||
financial_reports_available: bool,
|
||||
tax_reports_available: bool,
|
||||
) -> str:
|
||||
"""Calculate risk level with effective availability flags."""
|
||||
risk_score = 0
|
||||
risk_score += 3 if organization.in_defense_unreliable_suppliers_registry else 0
|
||||
risk_score += 2 if organization.in_275_fz_registry else 0
|
||||
risk_score += 2 if organization.bankruptcy_messages_found else 0
|
||||
risk_score += 1 if not financial_reports_available else 0
|
||||
risk_score += 1 if not tax_reports_available else 0
|
||||
|
||||
if risk_score >= 5:
|
||||
return "high"
|
||||
if risk_score >= 2:
|
||||
return "medium"
|
||||
return "low"
|
||||
@@ -6,6 +6,7 @@
|
||||
- frontend-facing serializers for organization catalog endpoints.
|
||||
"""
|
||||
|
||||
from apps.organization.availability import has_financial_reports, has_tax_reports
|
||||
from apps.organization.models import Organization
|
||||
from apps.organization.scope_utils import (
|
||||
SCOPE_LABELS,
|
||||
@@ -192,8 +193,8 @@ class OrganizationCatalogDetailSerializer(OrganizationCatalogBaseSerializer):
|
||||
@staticmethod
|
||||
def get_summary(obj: Organization) -> dict[str, object]:
|
||||
return {
|
||||
"financial_reports_available": obj.financial_reports_available,
|
||||
"tax_reports_available": obj.tax_reports_available,
|
||||
"financial_reports_available": has_financial_reports(obj),
|
||||
"tax_reports_available": has_tax_reports(obj),
|
||||
"active_registry_names": obj.get_active_registry_names(),
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from django.test import override_settings
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from tests.apps.external_data.factories import FinancialReportFactory
|
||||
from tests.apps.form_1.factories import FormF1RecordFactory
|
||||
from tests.apps.form_2.factories import FormF2RecordFactory
|
||||
from tests.apps.form_3.factories import FormF3RecordFactory
|
||||
@@ -244,6 +245,16 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
)
|
||||
)
|
||||
|
||||
def test_economics_uses_latest_available_year_when_requested_range_is_empty(self):
|
||||
response = self.client.get(
|
||||
f"/api/v1/organizations/{self.organization.id}/analytics/economics/"
|
||||
"?group=efficiency&from_year=2022&to_year=2024"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["periods"], [2026])
|
||||
self.assertEqual(response.data["series"][0]["points"][0]["period"], 2026)
|
||||
|
||||
def test_personnel_contract(self):
|
||||
personnel_response = self.client.get(
|
||||
f"/api/v1/organizations/{self.organization.id}/analytics/personnel/"
|
||||
@@ -271,6 +282,29 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
)
|
||||
self.assertIn("employees_count", personnel_response.data["age_distribution"][0])
|
||||
|
||||
def test_yearly_analytics_use_latest_available_year_when_requested_year_is_empty(
|
||||
self,
|
||||
):
|
||||
personnel_response = self.client.get(
|
||||
f"/api/v1/organizations/{self.organization.id}/analytics/personnel/"
|
||||
"?report_year=2024&history_years=2"
|
||||
)
|
||||
equipment_response = self.client.get(
|
||||
f"/api/v1/organizations/{self.organization.id}/analytics/equipment/"
|
||||
"?report_year=2024"
|
||||
)
|
||||
products_response = self.client.get(
|
||||
f"/api/v1/organizations/{self.organization.id}/analytics/products/"
|
||||
"?frequency=quarterly&price_mode=actual&report_year=2024"
|
||||
)
|
||||
|
||||
self.assertEqual(personnel_response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(equipment_response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(products_response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(personnel_response.data["report_year"], 2026)
|
||||
self.assertEqual(equipment_response.data["report_year"], 2026)
|
||||
self.assertEqual(products_response.data["report_year"], 2026)
|
||||
|
||||
def test_equipment_contract(self):
|
||||
response = self.client.get(
|
||||
f"/api/v1/organizations/{self.organization.id}/analytics/equipment/"
|
||||
@@ -457,6 +491,23 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
self.assertIn("risk_level", response.data)
|
||||
self.assertIn("updated_at", response.data)
|
||||
|
||||
def test_risk_profile_uses_available_related_report_data(self):
|
||||
organization = OrganizationFactory.create(
|
||||
financial_reports_available=False,
|
||||
tax_reports_available=False,
|
||||
)
|
||||
FinancialReportFactory.create(organization=organization, status="success")
|
||||
FormF2RecordFactory.create(organization=organization, income_tax="1000.00")
|
||||
|
||||
response = self.client.get(
|
||||
f"/api/v1/organizations/{organization.id}/risk-profile/"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(response.data["financial_reports_available"])
|
||||
self.assertTrue(response.data["tax_reports_available"])
|
||||
self.assertEqual(response.data["risk_level"], "low")
|
||||
|
||||
def test_dashboard_endpoint(self):
|
||||
response = self.client.get(
|
||||
"/api/v1/analytics/dashboard/?corporation_scope=rosatom"
|
||||
|
||||
@@ -9,6 +9,8 @@ from django.test import override_settings
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from tests.apps.external_data.factories import FinancialReportFactory
|
||||
from tests.apps.form_2.factories import FormF2RecordFactory
|
||||
from tests.apps.organization.factories import OrganizationFactory
|
||||
from tests.apps.user.factories import UserFactory
|
||||
|
||||
@@ -120,6 +122,20 @@ class OrganizationApiTest(APITestCase):
|
||||
"Иванов Иван Иванович",
|
||||
)
|
||||
|
||||
def test_detail_summary_uses_available_related_report_data(self):
|
||||
organization = OrganizationFactory.create(
|
||||
financial_reports_available=False,
|
||||
tax_reports_available=False,
|
||||
)
|
||||
FinancialReportFactory.create(organization=organization, status="success")
|
||||
FormF2RecordFactory.create(organization=organization, income_tax="1200.00")
|
||||
|
||||
response = self.client.get(f"/api/v1/organizations/{organization.id}/")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(response.data["summary"]["financial_reports_available"])
|
||||
self.assertTrue(response.data["summary"]["tax_reports_available"])
|
||||
|
||||
def test_registry_filter_uses_only_active_memberships(self):
|
||||
active_organization = OrganizationFactory.create(name="АО Актив")
|
||||
closed_organization = OrganizationFactory.create(name="АО Закрыт")
|
||||
|
||||
Reference in New Issue
Block a user