Implement exchange imports and frontend reporting APIs
Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Failing after 3m50s
CI/CD Pipeline / Run Tests (push) Successful in 3m57s
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 / Code Quality Checks (push) Failing after 3m50s
CI/CD Pipeline / Run Tests (push) Successful in 3m57s
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:
@@ -1,9 +1,10 @@
|
||||
"""Factories for organization app."""
|
||||
|
||||
import factory
|
||||
from apps.organization.models import Organization
|
||||
from faker import Faker
|
||||
|
||||
from apps.organization.models import IndustryCluster, Organization, OrganizationType
|
||||
|
||||
fake = Faker("ru_RU")
|
||||
|
||||
|
||||
@@ -14,10 +15,47 @@ class OrganizationFactory(factory.django.DjangoModelFactory):
|
||||
model = Organization
|
||||
|
||||
name = factory.LazyAttribute(lambda _: fake.company())
|
||||
short_name = factory.LazyAttribute(lambda _: f"АО «{fake.company()}»")
|
||||
organization_type = factory.LazyAttribute(
|
||||
lambda _: fake.random_element(
|
||||
[OrganizationType.AO, OrganizationType.PAO, OrganizationType.FGUP]
|
||||
)
|
||||
)
|
||||
cluster = factory.LazyAttribute(
|
||||
lambda _: fake.random_element(
|
||||
[
|
||||
IndustryCluster.RADIOELECTRONICS,
|
||||
IndustryCluster.NUCLEAR,
|
||||
IndustryCluster.SPACE,
|
||||
]
|
||||
)
|
||||
)
|
||||
inn = factory.LazyAttribute(lambda _: fake.numerify("##########"))
|
||||
ogrn = factory.LazyAttribute(lambda _: fake.numerify("#############"))
|
||||
kpp = factory.LazyAttribute(lambda _: fake.numerify("#########"))
|
||||
okpo = factory.LazyAttribute(lambda _: fake.numerify("########"))
|
||||
registration_date = factory.LazyAttribute(lambda _: fake.date_this_century())
|
||||
legal_address = factory.LazyAttribute(lambda _: fake.address().replace("\n", ", "))
|
||||
activity_type = factory.LazyAttribute(lambda _: fake.job())
|
||||
founder_name = factory.LazyAttribute(lambda _: fake.company())
|
||||
ownership_type = "Собственность государственных корпораций"
|
||||
legal_form = "Акционерное общество"
|
||||
charter_capital_amount = factory.LazyAttribute(
|
||||
lambda _: fake.pydecimal(left_digits=9, right_digits=2, positive=True)
|
||||
)
|
||||
general_director_name = factory.LazyAttribute(lambda _: fake.name())
|
||||
general_director_inn = factory.LazyAttribute(
|
||||
lambda _: fake.numerify("############")
|
||||
)
|
||||
general_director_appointment_date = factory.LazyAttribute(
|
||||
lambda _: fake.date_this_decade()
|
||||
)
|
||||
executors_count = factory.LazyAttribute(lambda _: fake.random_int(min=20, max=500))
|
||||
financial_reports_available = True
|
||||
tax_reports_available = True
|
||||
in_defense_unreliable_suppliers_registry = False
|
||||
in_275_fz_registry = False
|
||||
bankruptcy_messages_found = False
|
||||
|
||||
@classmethod
|
||||
def create_organization(cls, **kwargs):
|
||||
|
||||
235
tests/apps/organization/test_analytics_api.py
Normal file
235
tests/apps/organization/test_analytics_api.py
Normal file
@@ -0,0 +1,235 @@
|
||||
"""Tests for organization analytics endpoints."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from django.test import override_settings
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from apps.registers.models import Register, RegisterUpload, RegistryMembershipPeriod
|
||||
from tests.apps.form_1.factories import FormF1RecordFactory
|
||||
from tests.apps.form_2.factories import FormF2RecordFactory
|
||||
from tests.apps.form_3.factories import FormF3RecordFactory
|
||||
from tests.apps.form_4.factories import FormF4RecordFactory
|
||||
from tests.apps.form_5.factories import FormF5RecordFactory
|
||||
from tests.apps.form_6.factories import FormF6RecordFactory
|
||||
from tests.apps.organization.factories import OrganizationFactory
|
||||
from tests.apps.user.factories import UserFactory
|
||||
|
||||
|
||||
@override_settings(ROOT_URLCONF="core.urls")
|
||||
class OrganizationAnalyticsApiTest(APITestCase):
|
||||
def setUp(self):
|
||||
self.user = UserFactory.create_user()
|
||||
self.client.force_authenticate(self.user)
|
||||
self.organization = OrganizationFactory.create(
|
||||
cluster="radioelectronics",
|
||||
executors_count=120,
|
||||
financial_reports_available=True,
|
||||
tax_reports_available=True,
|
||||
bankruptcy_messages_found=False,
|
||||
)
|
||||
register = Register.objects.create(name="Реестр госкорпорации Росатом ОПК")
|
||||
upload = RegisterUpload.objects.create(
|
||||
registry=register,
|
||||
actual_date=date(2026, 4, 1),
|
||||
file_name="analytics.xlsx",
|
||||
file_hash="analytics-hash",
|
||||
rows_count=1,
|
||||
)
|
||||
RegistryMembershipPeriod.objects.create(
|
||||
registry=register,
|
||||
organization=self.organization,
|
||||
started_at=date(2026, 4, 1),
|
||||
started_by_upload=upload,
|
||||
)
|
||||
|
||||
FormF1RecordFactory.create(
|
||||
organization=self.organization,
|
||||
report_year=2026,
|
||||
report_quarter=1,
|
||||
payroll_fund=Decimal("1000000.00"),
|
||||
military_output_actual=Decimal("11000000.00"),
|
||||
civilian_output_actual=Decimal("7000000.00"),
|
||||
hightech_output_actual=Decimal("1500000.00"),
|
||||
rd_volume_actual=Decimal("900000.00"),
|
||||
military_domestic_actual=Decimal("9000000.00"),
|
||||
military_export_actual=Decimal("2000000.00"),
|
||||
civilian_domestic_actual=Decimal("5000000.00"),
|
||||
civilian_export_actual=Decimal("2000000.00"),
|
||||
)
|
||||
FormF1RecordFactory.create(
|
||||
organization=self.organization,
|
||||
report_year=2025,
|
||||
report_quarter=1,
|
||||
payroll_fund=Decimal("900000.00"),
|
||||
military_output_actual=Decimal("9000000.00"),
|
||||
civilian_output_actual=Decimal("6000000.00"),
|
||||
hightech_output_actual=Decimal("1200000.00"),
|
||||
rd_volume_actual=Decimal("700000.00"),
|
||||
military_domestic_actual=Decimal("7000000.00"),
|
||||
military_export_actual=Decimal("2000000.00"),
|
||||
civilian_domestic_actual=Decimal("4500000.00"),
|
||||
civilian_export_actual=Decimal("1500000.00"),
|
||||
)
|
||||
FormF2RecordFactory.create(
|
||||
organization=self.organization,
|
||||
report_year=2026,
|
||||
report_quarter=1,
|
||||
revenue=Decimal("1100000000.00"),
|
||||
revenue_prev=Decimal("760000000.00"),
|
||||
net_profit=Decimal("-144600000.00"),
|
||||
net_profit_prev=Decimal("500000000.00"),
|
||||
income_tax=Decimal("18900000.00"),
|
||||
)
|
||||
FormF2RecordFactory.create(
|
||||
organization=self.organization,
|
||||
report_year=2025,
|
||||
report_quarter=1,
|
||||
revenue=Decimal("760000000.00"),
|
||||
net_profit=Decimal("500000000.00"),
|
||||
income_tax=Decimal("6450000.00"),
|
||||
)
|
||||
FormF3RecordFactory.create(
|
||||
organization=self.organization,
|
||||
report_year=2026,
|
||||
avg_employees=1050,
|
||||
production_workers=620,
|
||||
engineering_workers=210,
|
||||
administrative_workers=220,
|
||||
workers_needed=35,
|
||||
total_equipment=187,
|
||||
domestic_equipment=91,
|
||||
imported_equipment=96,
|
||||
equipment_age_under_5=70,
|
||||
equipment_age_5_10=41,
|
||||
equipment_age_10_15=33,
|
||||
equipment_age_15_20=22,
|
||||
equipment_age_over_20=21,
|
||||
physical_wear_percent=Decimal("32.00"),
|
||||
utilization_rate=Decimal("92.00"),
|
||||
avg_shift_work=Decimal("1.80"),
|
||||
equipment_needed=14,
|
||||
)
|
||||
FormF3RecordFactory.create(
|
||||
organization=self.organization,
|
||||
report_year=2025,
|
||||
avg_employees=1020,
|
||||
)
|
||||
FormF4RecordFactory.create(
|
||||
organization=self.organization,
|
||||
report_year=2026,
|
||||
revenue_rsbu=Decimal("1100000000.00"),
|
||||
net_profit_rsbu=Decimal("320000000.00"),
|
||||
ebitda_rsbu=Decimal("480000000.00"),
|
||||
gross_profit_rsbu=Decimal("520000000.00"),
|
||||
operating_profit_rsbu=Decimal("300000000.00"),
|
||||
net_debt_rsbu=Decimal("200000000.00"),
|
||||
loans_rsbu=Decimal("300000000.00"),
|
||||
total_assets_rsbu=Decimal("900000000.00"),
|
||||
capex=Decimal("90000000.00"),
|
||||
rd_expenses=Decimal("40000000.00"),
|
||||
ros=Decimal("23.80"),
|
||||
roa=Decimal("12.10"),
|
||||
roe=Decimal("15.40"),
|
||||
)
|
||||
FormF4RecordFactory.create(
|
||||
organization=self.organization,
|
||||
report_year=2025,
|
||||
revenue_rsbu=Decimal("980000000.00"),
|
||||
net_profit_rsbu=Decimal("250000000.00"),
|
||||
ebitda_rsbu=Decimal("410000000.00"),
|
||||
ros=Decimal("21.00"),
|
||||
roa=Decimal("10.50"),
|
||||
roe=Decimal("14.10"),
|
||||
)
|
||||
FormF5RecordFactory.create(
|
||||
organization=self.organization,
|
||||
report_year=2026,
|
||||
equipment_category="Станочное оборудование",
|
||||
is_domestic=True,
|
||||
physical_wear_percent=Decimal("28.40"),
|
||||
)
|
||||
FormF6RecordFactory.create(
|
||||
organization=self.organization,
|
||||
report_year=2026,
|
||||
category="Станочное оборудование",
|
||||
total_equipment=54,
|
||||
domestic_equipment=31,
|
||||
imported_equipment=23,
|
||||
age_under_5=70,
|
||||
age_5_10=41,
|
||||
age_10_15=33,
|
||||
age_15_20=22,
|
||||
age_over_20=21,
|
||||
physical_wear_percent=Decimal("28.40"),
|
||||
utilization_rate=Decimal("92.00"),
|
||||
avg_shift_work=Decimal("1.80"),
|
||||
)
|
||||
|
||||
def test_financial_summary_endpoint(self):
|
||||
response = self.client.get(
|
||||
f"/api/v1/organizations/{self.organization.id}/analytics/financial-summary/"
|
||||
"?report_year=2026&report_quarter=1"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["revenue"]["amount"], 1100000000)
|
||||
self.assertEqual(response.data["revenue"]["previous_amount"], 760000000)
|
||||
self.assertEqual(response.data["taxes_paid"]["amount"], 18900000)
|
||||
self.assertEqual(response.data["insurance_contributions"]["amount"], 302000)
|
||||
|
||||
def test_personnel_and_equipment_endpoints(self):
|
||||
personnel_response = self.client.get(
|
||||
f"/api/v1/organizations/{self.organization.id}/analytics/personnel/"
|
||||
"?report_year=2026&history_years=2"
|
||||
)
|
||||
equipment_response = self.client.get(
|
||||
f"/api/v1/organizations/{self.organization.id}/analytics/equipment/"
|
||||
"?report_year=2026"
|
||||
)
|
||||
|
||||
self.assertEqual(personnel_response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
personnel_response.data["headcount"]["average_employees"],
|
||||
1050,
|
||||
)
|
||||
self.assertEqual(len(personnel_response.data["history"]), 2)
|
||||
|
||||
self.assertEqual(equipment_response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(equipment_response.data["summary"]["total_equipment"], 187)
|
||||
self.assertEqual(
|
||||
equipment_response.data["categories"][0]["category"],
|
||||
"Станочное оборудование",
|
||||
)
|
||||
|
||||
def test_products_and_risk_profile_endpoints(self):
|
||||
products_response = self.client.get(
|
||||
f"/api/v1/organizations/{self.organization.id}/analytics/products/"
|
||||
"?frequency=quarterly&price_mode=actual&report_year=2026"
|
||||
)
|
||||
risk_response = self.client.get(
|
||||
f"/api/v1/organizations/{self.organization.id}/risk-profile/"
|
||||
)
|
||||
|
||||
self.assertEqual(products_response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
products_response.data["summary"]["military_output_amount"],
|
||||
11000000,
|
||||
)
|
||||
self.assertEqual(risk_response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(risk_response.data["risk_level"], "low")
|
||||
|
||||
def test_dashboard_endpoint(self):
|
||||
response = self.client.get(
|
||||
"/api/v1/analytics/dashboard/?corporation_scope=rosatom"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["corporation_scope"], "rosatom")
|
||||
self.assertEqual(
|
||||
response.data["distribution_by_cluster"][0]["cluster"], "radioelectronics"
|
||||
)
|
||||
@@ -4,11 +4,11 @@ from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
from apps.registers.models import Register, RegisterUpload, RegistryMembershipPeriod
|
||||
from django.test import override_settings
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from apps.registers.models import Register, RegisterUpload, RegistryMembershipPeriod
|
||||
from tests.apps.organization.factories import OrganizationFactory
|
||||
from tests.apps.user.factories import UserFactory
|
||||
|
||||
@@ -22,7 +22,11 @@ class OrganizationApiTest(APITestCase):
|
||||
self.client.force_authenticate(self.user)
|
||||
|
||||
def test_list_includes_only_active_registry_names(self):
|
||||
organization = OrganizationFactory.create(name="АО Альфа")
|
||||
organization = OrganizationFactory.create(
|
||||
name="АО Альфа",
|
||||
short_name="АО «Альфа»",
|
||||
organization_type="ao",
|
||||
)
|
||||
active_register = Register.objects.create(name="Реестр ОПК")
|
||||
closed_register = Register.objects.create(name="Архивный реестр")
|
||||
active_upload = RegisterUpload.objects.create(
|
||||
@@ -58,10 +62,20 @@ class OrganizationApiTest(APITestCase):
|
||||
response = self.client.get("/api/v1/organizations/")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["data"][0]["active_registry_names"], ["Реестр ОПК"])
|
||||
self.assertEqual(
|
||||
response.data["results"][0]["active_registry_names"], ["Реестр ОПК"]
|
||||
)
|
||||
self.assertEqual(response.data["results"][0]["corporation_scope"], ["opk"])
|
||||
self.assertEqual(response.data["results"][0]["short_name"], "АО «Альфа»")
|
||||
|
||||
def test_detail_includes_active_registries(self):
|
||||
organization = OrganizationFactory.create()
|
||||
organization = OrganizationFactory.create(
|
||||
short_name="АО «Бета»",
|
||||
general_director_name="Иванов Иван Иванович",
|
||||
general_director_inn="123456789012",
|
||||
financial_reports_available=True,
|
||||
tax_reports_available=True,
|
||||
)
|
||||
register = Register.objects.create(name="Реестр Роскосмос")
|
||||
upload = RegisterUpload.objects.create(
|
||||
registry=register,
|
||||
@@ -81,13 +95,18 @@ class OrganizationApiTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
response.data["data"]["active_registry_names"],
|
||||
response.data["summary"]["active_registry_names"],
|
||||
["Реестр Роскосмос"],
|
||||
)
|
||||
self.assertEqual(
|
||||
response.data["data"]["active_registries"],
|
||||
response.data["active_registries"],
|
||||
[{"id": str(register.id), "name": "Реестр Роскосмос"}],
|
||||
)
|
||||
self.assertEqual(response.data["corporation_scope"], ["roscosmos"])
|
||||
self.assertEqual(
|
||||
response.data["general_director"]["full_name"],
|
||||
"Иванов Иван Иванович",
|
||||
)
|
||||
|
||||
def test_registry_filter_uses_only_active_memberships(self):
|
||||
active_organization = OrganizationFactory.create(name="АО Актив")
|
||||
@@ -119,5 +138,45 @@ class OrganizationApiTest(APITestCase):
|
||||
response = self.client.get(f"/api/v1/organizations/?registry={register.id}")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data["data"]), 1)
|
||||
self.assertEqual(response.data["data"][0]["id"], str(active_organization.id))
|
||||
self.assertEqual(len(response.data["results"]), 1)
|
||||
self.assertEqual(response.data["results"][0]["id"], str(active_organization.id))
|
||||
|
||||
def test_corporation_scope_filter_uses_active_registers(self):
|
||||
rosatom_org = OrganizationFactory.create(name="АО Росатом")
|
||||
roscosmos_org = OrganizationFactory.create(name="АО Роскосмос")
|
||||
rosatom_register = Register.objects.create(name="Реестр госкорпорации Росатом")
|
||||
roscosmos_register = Register.objects.create(
|
||||
name="Реестр госкорпорации Роскосмос"
|
||||
)
|
||||
rosatom_upload = RegisterUpload.objects.create(
|
||||
registry=rosatom_register,
|
||||
actual_date=date(2026, 4, 1),
|
||||
file_name="rosatom.xlsx",
|
||||
file_hash="rosatom-hash",
|
||||
rows_count=1,
|
||||
)
|
||||
roscosmos_upload = RegisterUpload.objects.create(
|
||||
registry=roscosmos_register,
|
||||
actual_date=date(2026, 4, 1),
|
||||
file_name="roscosmos.xlsx",
|
||||
file_hash="roscosmos-hash",
|
||||
rows_count=1,
|
||||
)
|
||||
RegistryMembershipPeriod.objects.create(
|
||||
registry=rosatom_register,
|
||||
organization=rosatom_org,
|
||||
started_at=date(2026, 4, 1),
|
||||
started_by_upload=rosatom_upload,
|
||||
)
|
||||
RegistryMembershipPeriod.objects.create(
|
||||
registry=roscosmos_register,
|
||||
organization=roscosmos_org,
|
||||
started_at=date(2026, 4, 1),
|
||||
started_by_upload=roscosmos_upload,
|
||||
)
|
||||
|
||||
response = self.client.get("/api/v1/organizations/?corporation_scope=rosatom")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data["results"]), 1)
|
||||
self.assertEqual(response.data["results"][0]["id"], str(rosatom_org.id))
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
from datetime import date
|
||||
|
||||
from apps.registers.models import Register, RegisterUpload, RegistryMembershipPeriod
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.registers.models import Register, RegisterUpload, RegistryMembershipPeriod
|
||||
|
||||
from .factories import OrganizationFactory
|
||||
|
||||
|
||||
@@ -90,3 +91,40 @@ class OrganizationModelTest(TestCase):
|
||||
|
||||
self.assertEqual(self.org.get_active_registry_names(), ["Активный реестр"])
|
||||
self.assertEqual(self.org.active_registry_names_display(), "Активный реестр")
|
||||
|
||||
def test_corporation_scopes_are_derived_from_active_registers(self):
|
||||
rosatom_register = Register.objects.create(name="Реестр госкорпорации Росатом")
|
||||
opk_register = Register.objects.create(name="Реестр предприятий ОПК")
|
||||
upload = RegisterUpload.objects.create(
|
||||
registry=rosatom_register,
|
||||
actual_date=date(2026, 4, 1),
|
||||
file_name="scopes.xlsx",
|
||||
file_hash="scopes-hash",
|
||||
rows_count=2,
|
||||
)
|
||||
opk_upload = RegisterUpload.objects.create(
|
||||
registry=opk_register,
|
||||
actual_date=date(2026, 4, 1),
|
||||
file_name="scopes-opk.xlsx",
|
||||
file_hash="scopes-opk-hash",
|
||||
rows_count=2,
|
||||
)
|
||||
|
||||
RegistryMembershipPeriod.objects.create(
|
||||
registry=rosatom_register,
|
||||
organization=self.org,
|
||||
started_at=date(2026, 4, 1),
|
||||
started_by_upload=upload,
|
||||
)
|
||||
RegistryMembershipPeriod.objects.create(
|
||||
registry=opk_register,
|
||||
organization=self.org,
|
||||
started_at=date(2026, 4, 1),
|
||||
started_by_upload=opk_upload,
|
||||
)
|
||||
|
||||
self.assertEqual(self.org.get_corporation_scopes(), ["rosatom", "opk"])
|
||||
self.assertEqual(
|
||||
self.org.get_corporation_scope_labels(),
|
||||
["Госкорпорация «Росатом»", "Организации ОПК"],
|
||||
)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""Tests for Organization services."""
|
||||
|
||||
from apps.organization.services import OrganizationService
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.organization.services import OrganizationService
|
||||
|
||||
from .factories import OrganizationFactory
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user