fix: align reporting forms and demo analytics
All checks were successful
All checks were successful
This commit is contained in:
41
tests/apps/core/test_demo_period_workbooks.py
Normal file
41
tests/apps/core/test_demo_period_workbooks.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""Integration checks for the versioned Excel demo-period fixtures."""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from apps.form_3.services import FormF3Parser
|
||||
from apps.form_4.services import FormF4Parser
|
||||
from apps.form_6.services import FormF6Parser
|
||||
from django.test import TestCase
|
||||
|
||||
DEMO_DIR = Path(__file__).resolve().parents[3] / "input" / "demo-periods"
|
||||
|
||||
|
||||
class DemoPeriodWorkbooksTest(TestCase):
|
||||
def test_form_f3_workbooks_match_current_schema(self):
|
||||
for path in sorted(DEMO_DIR.glob("Ф-3_*.xlsx")):
|
||||
year = int(re.search(r"_(\d{4})", path.name).group(1))
|
||||
with self.subTest(path=path.name), path.open("rb") as file:
|
||||
result = FormF3Parser(report_year=year).parse(file)
|
||||
self.assertEqual(result.loaded_count, 20)
|
||||
self.assertEqual(result.skipped_count, 0)
|
||||
|
||||
def test_form_f4_workbooks_match_current_schema(self):
|
||||
for path in sorted(DEMO_DIR.glob("Ф-4_*.xlsx")):
|
||||
match = re.search(r"_(\d{4})-H([12])", path.name)
|
||||
year, half_year = map(int, match.groups())
|
||||
with self.subTest(path=path.name), path.open("rb") as file:
|
||||
result = FormF4Parser(
|
||||
report_year=year,
|
||||
report_half_year=half_year,
|
||||
).parse(file)
|
||||
self.assertEqual(result.loaded_count, 20)
|
||||
self.assertEqual(result.skipped_count, 0)
|
||||
|
||||
def test_form_f6_workbooks_are_structurally_consistent(self):
|
||||
for path in sorted(DEMO_DIR.glob("Ф-6_*.xlsx")):
|
||||
year = int(re.search(r"_(\d{4})", path.name).group(1))
|
||||
with self.subTest(path=path.name), path.open("rb") as file:
|
||||
result = FormF6Parser(report_year=year).parse(file)
|
||||
self.assertEqual(result.loaded_count, 20)
|
||||
self.assertEqual(result.skipped_count, 0)
|
||||
@@ -59,6 +59,51 @@ class GenerateTestReportsCommandTest(TestCase):
|
||||
self.assertEqual(PublicProcurement.objects.count(), 3)
|
||||
self.assertEqual(ArbitrationCase.objects.count(), 3)
|
||||
|
||||
for record in FormF3Record.objects.all():
|
||||
self.assertEqual(
|
||||
sum(
|
||||
(
|
||||
record.employees_under_20,
|
||||
record.employees_20_29,
|
||||
record.employees_30_39,
|
||||
record.employees_40_49,
|
||||
record.employees_50_59,
|
||||
record.employees_over_60,
|
||||
)
|
||||
),
|
||||
int(record.avg_employees),
|
||||
)
|
||||
self.assertEqual(
|
||||
record.machine_tools_and_equipment,
|
||||
record.total_equipment,
|
||||
)
|
||||
|
||||
for record in FormF6Record.objects.all():
|
||||
self.assertEqual(
|
||||
sum(
|
||||
(
|
||||
record.age_under_5,
|
||||
record.age_5_10,
|
||||
record.age_10_15,
|
||||
record.age_15_20,
|
||||
record.age_over_20,
|
||||
)
|
||||
),
|
||||
record.total_equipment,
|
||||
)
|
||||
self.assertEqual(
|
||||
sum(
|
||||
(
|
||||
record.age_under_5_imported,
|
||||
record.age_5_10_imported,
|
||||
record.age_10_15_imported,
|
||||
record.age_15_20_imported,
|
||||
record.age_over_20_imported,
|
||||
)
|
||||
),
|
||||
record.imported_equipment,
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"Ф-1: создано 12 записей, активных 9, архивных 3", stdout.getvalue()
|
||||
)
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
"""Tests for FormF3 services."""
|
||||
|
||||
from decimal import Decimal
|
||||
from io import BytesIO
|
||||
|
||||
from apps.form_3.models import FormF3Record
|
||||
from apps.form_3.services import FormF3Parser, FormF3Service
|
||||
from django.test import TestCase
|
||||
from openpyxl import Workbook
|
||||
|
||||
from tests.apps.organization.factories import OrganizationFactory
|
||||
|
||||
@@ -48,6 +53,72 @@ class FormF3ServiceTest(TestCase):
|
||||
class FormF3ParserTest(TestCase):
|
||||
"""Tests for FormF3Parser."""
|
||||
|
||||
@staticmethod
|
||||
def _current_template_file() -> BytesIO:
|
||||
headers = [
|
||||
"Наименование организации",
|
||||
"ОКПО",
|
||||
"ОГРН",
|
||||
"ИНН",
|
||||
"Отгруженные товары собственного производства, выполненные работы и услуги собственными силами в фактических ценах - всего",
|
||||
"Отгруженные товары собственного производства, выполненные работы и услуги собственными силами в фактических ценах, из них инновационные товары, работы, услуги",
|
||||
"Всего основных фондов (без незавершенных активов и не включая земельные участки и объекты природопользования)",
|
||||
"Станочный парк и оборудование, всего по предприятию",
|
||||
"Станочный парк и оборудование - импортное оборудование",
|
||||
"Оборудование с возрастом от 5 до 10 лет",
|
||||
"Оборудование с возрастом до 5 лет",
|
||||
"Фактический (физический) износ оборудования, %",
|
||||
"Средний уровень загрузки производственных мощностей (всего)",
|
||||
"Средний уровень загрузки производственных мощностей военного производства",
|
||||
"Средний возраст сотрудников",
|
||||
"Количество работников предприятия до 20 лет",
|
||||
"Количество работников предприятия 20-29 лет",
|
||||
"Количество работников предприятия 30-39 лет",
|
||||
"Количество работников предприятия 40-49 лет",
|
||||
"Количество работников предприятия 50-59 лет",
|
||||
"Количество работников предприятия старше 60 лет",
|
||||
]
|
||||
workbook = Workbook()
|
||||
sheet = workbook.active
|
||||
sheet.append(headers)
|
||||
sheet.append(
|
||||
[None, None, None, None]
|
||||
+ ["тыс. руб."] * 3
|
||||
+ ["ед."] * 4
|
||||
+ ["%"] * 3
|
||||
+ ["лет"]
|
||||
+ ["чел."] * 6
|
||||
)
|
||||
sheet.append(
|
||||
[
|
||||
"Тестовая организация Ф-3",
|
||||
"90000001",
|
||||
"1267700000017",
|
||||
"3234567890",
|
||||
352,
|
||||
222,
|
||||
85,
|
||||
46,
|
||||
17,
|
||||
13,
|
||||
14,
|
||||
21,
|
||||
79,
|
||||
65,
|
||||
42,
|
||||
4,
|
||||
18,
|
||||
28,
|
||||
26,
|
||||
17,
|
||||
7,
|
||||
]
|
||||
)
|
||||
file = BytesIO()
|
||||
workbook.save(file)
|
||||
file.seek(0)
|
||||
return file
|
||||
|
||||
def test_get_column_mappings_returns_mappings(self):
|
||||
"""Test get_column_mappings returns correct mappings."""
|
||||
parser = FormF3Parser(report_year=2026, report_quarter=1)
|
||||
@@ -57,8 +128,24 @@ class FormF3ParserTest(TestCase):
|
||||
self.assertTrue(len(mappings) > 0)
|
||||
|
||||
field_names = [m.field_name for m in mappings]
|
||||
self.assertIn("avg_employees", field_names)
|
||||
self.assertIn("total_equipment", field_names)
|
||||
self.assertIn("shipped_goods_total", field_names)
|
||||
self.assertIn("machine_tools_and_equipment", field_names)
|
||||
self.assertIn("average_employee_age", field_names)
|
||||
self.assertNotIn("avg_employees", field_names)
|
||||
|
||||
def test_parse_current_template_maps_equipment_and_personnel_columns(self):
|
||||
OrganizationFactory.create(inn="3234567890")
|
||||
parser = FormF3Parser(report_year=2026, report_quarter=1)
|
||||
|
||||
result = parser.parse(self._current_template_file())
|
||||
|
||||
self.assertEqual(result.loaded_count, 1)
|
||||
record = FormF3Record.objects.get()
|
||||
self.assertEqual(record.shipped_goods_total, Decimal("352"))
|
||||
self.assertEqual(record.machine_tools_and_equipment, 46)
|
||||
self.assertEqual(record.imported_equipment, 17)
|
||||
self.assertEqual(record.average_employee_age, Decimal("42"))
|
||||
self.assertEqual(record.employees_30_39, 28)
|
||||
|
||||
def test_create_record_uses_existing_organization(self):
|
||||
"""Report imports reuse existing organizations when available."""
|
||||
@@ -69,8 +156,8 @@ class FormF3ParserTest(TestCase):
|
||||
row_data = {
|
||||
"inn": "3234567890",
|
||||
"name": "Тестовая организация Ф-3",
|
||||
"avg_employees": 100,
|
||||
"total_equipment": 50,
|
||||
"machine_tools_and_equipment": 50,
|
||||
"average_employee_age": 42,
|
||||
}
|
||||
|
||||
record = parser.create_record(row_data)
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
"""Tests for FormF4 services."""
|
||||
|
||||
from decimal import Decimal
|
||||
from io import BytesIO
|
||||
|
||||
from apps.form_4.models import FormF4Record
|
||||
from apps.form_4.services import FormF4Parser, FormF4Service
|
||||
from django.test import TestCase
|
||||
from openpyxl import Workbook
|
||||
|
||||
from tests.apps.organization.factories import OrganizationFactory
|
||||
|
||||
@@ -48,6 +53,47 @@ class FormF4ServiceTest(TestCase):
|
||||
class FormF4ParserTest(TestCase):
|
||||
"""Tests for FormF4Parser."""
|
||||
|
||||
@staticmethod
|
||||
def _current_template_file() -> BytesIO:
|
||||
headers = [
|
||||
"Наименование организации",
|
||||
"ОКПО",
|
||||
"ОГРН",
|
||||
"ИНН",
|
||||
"Выручка, в соответствии с РСБУ",
|
||||
"Выручка, в соответствии с МСФО",
|
||||
"Чистая прибыль (убыток), в соответствии с РСБУ",
|
||||
"Чистая прибыль (убыток), в соответствии с МСФО",
|
||||
"Кредиты и займы, в соответствии с РСБУ",
|
||||
"Кредиты и займы, в соответствии с МСФО",
|
||||
"EBITDA, в соответствии с РСБУ",
|
||||
"EBITDA, в соответствии с МСФО",
|
||||
]
|
||||
workbook = Workbook()
|
||||
sheet = workbook.active
|
||||
sheet.append(headers)
|
||||
sheet.append([None, None, None, None] + ["тыс. руб."] * 8)
|
||||
sheet.append(
|
||||
[
|
||||
"Тестовая организация Ф-4",
|
||||
"90000001",
|
||||
"1267700000017",
|
||||
"4234567890",
|
||||
477807,
|
||||
499308,
|
||||
442414,
|
||||
462322,
|
||||
42047,
|
||||
43729,
|
||||
138564,
|
||||
62115,
|
||||
]
|
||||
)
|
||||
file = BytesIO()
|
||||
workbook.save(file)
|
||||
file.seek(0)
|
||||
return file
|
||||
|
||||
def test_get_column_mappings_returns_mappings(self):
|
||||
"""Test get_column_mappings returns correct mappings."""
|
||||
parser = FormF4Parser(report_year=2026, report_half_year=1)
|
||||
@@ -59,6 +105,20 @@ class FormF4ParserTest(TestCase):
|
||||
field_names = [m.field_name for m in mappings]
|
||||
self.assertIn("revenue_rsbu", field_names)
|
||||
self.assertIn("net_profit_rsbu", field_names)
|
||||
self.assertIn("ebitda_rsbu", field_names)
|
||||
|
||||
def test_parse_current_template_maps_profit_loans_and_ebitda_columns(self):
|
||||
OrganizationFactory.create(inn="4234567890")
|
||||
parser = FormF4Parser(report_year=2025, report_half_year=2)
|
||||
|
||||
result = parser.parse(self._current_template_file())
|
||||
|
||||
self.assertEqual(result.loaded_count, 1)
|
||||
record = FormF4Record.objects.get()
|
||||
self.assertEqual(record.revenue_rsbu, Decimal("477807"))
|
||||
self.assertEqual(record.net_profit_rsbu, Decimal("442414"))
|
||||
self.assertEqual(record.loans_rsbu, Decimal("42047"))
|
||||
self.assertEqual(record.ebitda_rsbu, Decimal("138564"))
|
||||
|
||||
def test_create_record_uses_existing_organization(self):
|
||||
"""Report imports reuse existing organizations when available."""
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
"""Tests for FormF6 services."""
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
from apps.core.excel import ExcelValidationError
|
||||
from apps.form_6.models import FormF6Record
|
||||
from apps.form_6.services import FormF6Parser, FormF6Service
|
||||
from django.test import TestCase
|
||||
from openpyxl import Workbook
|
||||
|
||||
from tests.apps.organization.factories import OrganizationFactory
|
||||
|
||||
@@ -48,6 +53,82 @@ class FormF6ServiceTest(TestCase):
|
||||
class FormF6ParserTest(TestCase):
|
||||
"""Tests for FormF6Parser."""
|
||||
|
||||
@staticmethod
|
||||
def _current_template_file(
|
||||
*,
|
||||
total_equipment: int = 100,
|
||||
age_under_5: int | float = 20,
|
||||
total_header: str = "Всего",
|
||||
) -> BytesIO:
|
||||
workbook = Workbook()
|
||||
sheet = workbook.active
|
||||
row_1 = [None] * 38
|
||||
row_2 = [None] * 38
|
||||
row_3 = [None] * 38
|
||||
row_4 = [None] * 38
|
||||
row_1[:6] = [
|
||||
"Наименование организации",
|
||||
"ОКПО",
|
||||
"ОГРН",
|
||||
"ИНН",
|
||||
"Код строки",
|
||||
"Категория",
|
||||
]
|
||||
row_1[6] = "Количество оборудования на конец года ед."
|
||||
row_1[10] = "Из установленного оборудования – оборудование в возрасте, единиц"
|
||||
row_1[35:38] = [
|
||||
"Средняя сменность работы",
|
||||
"Количество рабочих мест без оборудования",
|
||||
"Примечание",
|
||||
]
|
||||
row_2[10], row_2[15], row_2[20], row_2[25], row_2[30] = (
|
||||
"До 5 лет",
|
||||
"От 5 до 10 лет",
|
||||
"От 10 до 15 лет",
|
||||
"От 15 до 20 лет",
|
||||
"Свыше 20 лет",
|
||||
)
|
||||
row_3[6:10] = [
|
||||
total_header,
|
||||
"Введенное в эксплуатацию в отчетном году",
|
||||
"Выведенное из эксплуатации в отчетном году",
|
||||
"Импортное оборудование",
|
||||
]
|
||||
for start in (10, 15, 20, 25, 30):
|
||||
row_3[start : start + 5] = [
|
||||
"Всего",
|
||||
"Импортного оборудования",
|
||||
"Коэффициент использования оборудования в производстве",
|
||||
"Доля фактического времени работы оборудования, использованного для производства ПВН, %",
|
||||
"Средневзвешенный коэффициент загрузки",
|
||||
]
|
||||
for row in (row_1, row_2, row_3, row_4):
|
||||
sheet.append(row)
|
||||
|
||||
ages = [age_under_5, 25, 20, 20, 15]
|
||||
imported = [4, 5, 4, 4, 3]
|
||||
values = [
|
||||
"Тестовая организация Ф-6",
|
||||
"90000001",
|
||||
"1267700000017",
|
||||
"6234567890",
|
||||
"101",
|
||||
"Основное технологическое оборудование",
|
||||
total_equipment,
|
||||
12,
|
||||
7,
|
||||
sum(imported),
|
||||
]
|
||||
for total, imported_total in zip(ages, imported, strict=True):
|
||||
values.extend([total, imported_total, 75, 20, 0.8])
|
||||
values.extend([1.5, 4, "Тестовые согласованные данные"])
|
||||
sheet.append(values)
|
||||
|
||||
file = BytesIO()
|
||||
workbook.save(file)
|
||||
file.seek(0)
|
||||
return file
|
||||
|
||||
def test_get_column_mappings_returns_mappings(self):
|
||||
"""Test get_column_mappings returns correct mappings."""
|
||||
parser = FormF6Parser(report_year=2026, report_quarter=1)
|
||||
@@ -59,6 +140,41 @@ class FormF6ParserTest(TestCase):
|
||||
field_names = [m.field_name for m in mappings]
|
||||
self.assertIn("row_code", field_names)
|
||||
self.assertIn("total_equipment", field_names)
|
||||
self.assertIn("commissioned_equipment", field_names)
|
||||
self.assertIn("age_over_20_imported", field_names)
|
||||
|
||||
def test_parse_current_template_maps_all_age_buckets(self):
|
||||
OrganizationFactory.create(inn="6234567890")
|
||||
parser = FormF6Parser(report_year=2026, report_quarter=1)
|
||||
|
||||
result = parser.parse(self._current_template_file())
|
||||
|
||||
self.assertEqual(result.loaded_count, 1)
|
||||
record = FormF6Record.objects.get()
|
||||
self.assertEqual(record.total_equipment, 100)
|
||||
self.assertEqual(record.commissioned_equipment, 12)
|
||||
self.assertEqual(record.decommissioned_equipment, 7)
|
||||
self.assertEqual(record.age_under_5, 20)
|
||||
self.assertEqual(record.age_over_20, 15)
|
||||
self.assertEqual(record.age_over_20_imported, 3)
|
||||
|
||||
def test_rejects_non_integral_equipment_counts_before_creating_records(self):
|
||||
OrganizationFactory.create(inn="6234567890")
|
||||
parser = FormF6Parser(report_year=2026, report_quarter=1)
|
||||
|
||||
with self.assertRaises(ExcelValidationError):
|
||||
parser.parse(self._current_template_file(age_under_5=20.5))
|
||||
|
||||
self.assertFalse(FormF6Record.objects.exists())
|
||||
|
||||
def test_rejects_incompatible_template_before_creating_records(self):
|
||||
OrganizationFactory.create(inn="6234567890")
|
||||
parser = FormF6Parser(report_year=2026, report_quarter=1)
|
||||
|
||||
with self.assertRaises(ExcelValidationError):
|
||||
parser.parse(self._current_template_file(total_header="Импортное"))
|
||||
|
||||
self.assertFalse(FormF6Record.objects.exists())
|
||||
|
||||
def test_create_record_uses_existing_organization(self):
|
||||
"""Report imports reuse existing organizations when available."""
|
||||
|
||||
@@ -41,6 +41,8 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
organization=self.organization,
|
||||
report_year=2026,
|
||||
report_quarter=1,
|
||||
report_month=6,
|
||||
avg_employees=1050,
|
||||
avg_payroll_employees=995,
|
||||
payroll_fund=Decimal("1000000.00"),
|
||||
military_output_actual=Decimal("11000000.00"),
|
||||
@@ -56,6 +58,8 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
organization=self.organization,
|
||||
report_year=2025,
|
||||
report_quarter=1,
|
||||
report_month=12,
|
||||
avg_employees=1020,
|
||||
avg_payroll_employees=970,
|
||||
payroll_fund=Decimal("900000.00"),
|
||||
military_output_actual=Decimal("9000000.00"),
|
||||
@@ -88,11 +92,19 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
FormF3RecordFactory.create(
|
||||
organization=self.organization,
|
||||
report_year=2026,
|
||||
avg_employees=1050,
|
||||
production_workers=620,
|
||||
engineering_workers=210,
|
||||
administrative_workers=220,
|
||||
workers_needed=35,
|
||||
avg_employees=None,
|
||||
production_workers=None,
|
||||
engineering_workers=None,
|
||||
administrative_workers=None,
|
||||
workers_needed=None,
|
||||
average_employee_age=Decimal("42.00"),
|
||||
employees_under_20=4,
|
||||
employees_20_29=18,
|
||||
employees_30_39=28,
|
||||
employees_40_49=26,
|
||||
employees_50_59=17,
|
||||
employees_over_60=7,
|
||||
machine_tools_and_equipment=46,
|
||||
total_equipment=187,
|
||||
domestic_equipment=91,
|
||||
imported_equipment=96,
|
||||
@@ -109,14 +121,16 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
FormF3RecordFactory.create(
|
||||
organization=self.organization,
|
||||
report_year=2025,
|
||||
avg_employees=1020,
|
||||
avg_employees=None,
|
||||
average_employee_age=Decimal("41.00"),
|
||||
)
|
||||
FormF4RecordFactory.create(
|
||||
organization=self.organization,
|
||||
report_year=2026,
|
||||
revenue_rsbu=Decimal("1100000000.00"),
|
||||
net_profit_rsbu=Decimal("320000000.00"),
|
||||
ebitda_rsbu=Decimal("480000000.00"),
|
||||
report_half_year=2,
|
||||
revenue_rsbu=Decimal("477807.00"),
|
||||
net_profit_rsbu=Decimal("442414.00"),
|
||||
ebitda_rsbu=Decimal("138564.00"),
|
||||
gross_profit_rsbu=Decimal("520000000.00"),
|
||||
operating_profit_rsbu=Decimal("300000000.00"),
|
||||
net_debt_rsbu=Decimal("200000000.00"),
|
||||
@@ -131,9 +145,10 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
FormF4RecordFactory.create(
|
||||
organization=self.organization,
|
||||
report_year=2025,
|
||||
revenue_rsbu=Decimal("980000000.00"),
|
||||
net_profit_rsbu=Decimal("250000000.00"),
|
||||
ebitda_rsbu=Decimal("410000000.00"),
|
||||
report_half_year=2,
|
||||
revenue_rsbu=Decimal("450000.00"),
|
||||
net_profit_rsbu=Decimal("400000.00"),
|
||||
ebitda_rsbu=Decimal("120000.00"),
|
||||
ros=Decimal("21.00"),
|
||||
roa=Decimal("10.50"),
|
||||
roe=Decimal("14.10"),
|
||||
@@ -152,14 +167,31 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
organization=self.organization,
|
||||
report_year=2026,
|
||||
category="Станочное оборудование",
|
||||
total_equipment=54,
|
||||
domestic_equipment=31,
|
||||
imported_equipment=23,
|
||||
total_equipment=187,
|
||||
domestic_equipment=None,
|
||||
imported_equipment=96,
|
||||
commissioned_equipment=12,
|
||||
decommissioned_equipment=7,
|
||||
age_under_5=70,
|
||||
age_under_5_imported=35,
|
||||
age_under_5_utilization_rate=Decimal("92.00"),
|
||||
age_under_5_lease_share_itn_percent=Decimal("20.00"),
|
||||
age_5_10=41,
|
||||
age_5_10_imported=21,
|
||||
age_5_10_utilization_rate=Decimal("92.00"),
|
||||
age_5_10_lease_share_itn_percent=Decimal("20.00"),
|
||||
age_10_15=33,
|
||||
age_10_15_imported=17,
|
||||
age_10_15_utilization_rate=Decimal("92.00"),
|
||||
age_10_15_lease_share_itn_percent=Decimal("20.00"),
|
||||
age_15_20=22,
|
||||
age_15_20_imported=12,
|
||||
age_15_20_utilization_rate=Decimal("92.00"),
|
||||
age_15_20_lease_share_itn_percent=Decimal("20.00"),
|
||||
age_over_20=21,
|
||||
age_over_20_imported=11,
|
||||
age_over_20_utilization_rate=Decimal("92.00"),
|
||||
age_over_20_lease_share_itn_percent=Decimal("20.00"),
|
||||
physical_wear_percent=Decimal("28.40"),
|
||||
utilization_rate=Decimal("92.00"),
|
||||
avg_shift_work=Decimal("1.80"),
|
||||
@@ -231,14 +263,24 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
self.assertEqual(response.data["organization_id"], str(self.organization.id))
|
||||
self.assertEqual(response.data["group"], "efficiency")
|
||||
self.assertEqual(response.data["periods"], [2025, 2026])
|
||||
self.assertEqual(
|
||||
response.data["report_periods"],
|
||||
[
|
||||
{"year": 2025, "report_half_year": 2},
|
||||
{"year": 2026, "report_half_year": 2},
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
response.data["kpis"].keys(),
|
||||
{"revenue", "ebitda", "net_profit", "revenue_per_employee"},
|
||||
)
|
||||
self.assertGreater(response.data["kpis"]["revenue_per_employee"]["value"], 0)
|
||||
self.assertEqual(response.data["kpis"]["revenue"]["value"], 477807)
|
||||
self.assertEqual(response.data["kpis"]["net_profit"]["value"], 442414)
|
||||
self.assertEqual(response.data["kpis"]["ebitda"]["value"], 138564)
|
||||
self.assertEqual(response.data["kpis"]["revenue_per_employee"]["value"], 455)
|
||||
self.assertEqual(
|
||||
response.data["kpis"]["revenue_per_employee"]["unit"],
|
||||
"rub_per_employee",
|
||||
"rub_thousands_per_employee",
|
||||
)
|
||||
for kpi in response.data["kpis"].values():
|
||||
self.assertIn("previous_value", kpi)
|
||||
@@ -251,7 +293,7 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
self.assertIn("unit", series)
|
||||
self.assertIn("points", series)
|
||||
expected_unit = (
|
||||
"rub_per_employee"
|
||||
"rub_thousands_per_employee"
|
||||
if series["metric"] == "revenue_per_employee"
|
||||
else "rub_thousands"
|
||||
)
|
||||
@@ -283,10 +325,43 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["periods"], [])
|
||||
self.assertEqual(response.data["report_periods"], [])
|
||||
self.assertEqual(response.data["kpis"], {})
|
||||
self.assertFalse(response.data["data_available"])
|
||||
self.assertEqual(response.data["message"], "Данные отсутствуют")
|
||||
|
||||
def test_economics_normalizes_f2_rubles_to_thousands_without_f4(self):
|
||||
organization = OrganizationFactory.create()
|
||||
FormF1RecordFactory.create(
|
||||
organization=organization,
|
||||
report_year=2026,
|
||||
report_month=6,
|
||||
avg_employees=100,
|
||||
)
|
||||
FormF2RecordFactory.create(
|
||||
organization=organization,
|
||||
report_year=2026,
|
||||
revenue=Decimal("1200000.00"),
|
||||
net_profit=Decimal("300000.00"),
|
||||
ebitda=Decimal("480000.00"),
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
f"/api/v1/organizations/{organization.id}/analytics/economics/"
|
||||
"?group=efficiency&from_year=2026&to_year=2026"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["kpis"]["revenue"]["value"], 1200)
|
||||
self.assertEqual(
|
||||
response.data["kpis"]["revenue_per_employee"]["value"],
|
||||
12,
|
||||
)
|
||||
self.assertEqual(
|
||||
response.data["kpis"]["revenue"]["unit"],
|
||||
"rub_thousands",
|
||||
)
|
||||
|
||||
def test_personnel_contract(self):
|
||||
personnel_response = self.client.get(
|
||||
f"/api/v1/organizations/{self.organization.id}/analytics/personnel/"
|
||||
@@ -297,6 +372,7 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
personnel_response.data["organization_id"], str(self.organization.id)
|
||||
)
|
||||
self.assertEqual(personnel_response.data["report_year"], 2026)
|
||||
self.assertEqual(personnel_response.data["report_month"], 6)
|
||||
self.assertEqual(
|
||||
personnel_response.data["headcount"]["average_employees"],
|
||||
1050,
|
||||
@@ -306,6 +382,7 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
set(personnel_response.data["history"][0]),
|
||||
{
|
||||
"year",
|
||||
"report_month",
|
||||
"average_employees",
|
||||
"avg_payroll_employees",
|
||||
"average_age",
|
||||
@@ -313,15 +390,22 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
},
|
||||
)
|
||||
self.assertEqual(len(personnel_response.data["age_distribution"]), 3)
|
||||
self.assertIn("average_age", personnel_response.data)
|
||||
self.assertEqual(personnel_response.data["average_age"], 42.0)
|
||||
self.assertEqual(
|
||||
personnel_response.data["headcount"]["avg_payroll_employees"], 995
|
||||
)
|
||||
self.assertEqual(personnel_response.data["headcount"]["payroll_fund"], 1000000)
|
||||
self.assertEqual(
|
||||
personnel_response.data["headcount"]["payroll_fund"], 1000000000
|
||||
)
|
||||
self.assertIsNone(personnel_response.data["headcount"]["production_workers"])
|
||||
self.assertEqual(
|
||||
personnel_response.data["age_distribution"][0]["age_group"],
|
||||
"under_30",
|
||||
)
|
||||
self.assertEqual(
|
||||
personnel_response.data["age_distribution"][0]["employees_count"],
|
||||
22,
|
||||
)
|
||||
self.assertIn("employees_count", personnel_response.data["age_distribution"][0])
|
||||
|
||||
def test_yearly_analytics_use_latest_available_year_when_requested_year_is_empty(
|
||||
@@ -374,15 +458,19 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.data["summary"]["total_equipment"], 187)
|
||||
self.assertEqual(response.data["summary"]["machine_tools_and_equipment"], 46)
|
||||
self.assertIsNone(response.data["summary"]["domestic_equipment"])
|
||||
self.assertEqual(response.data["summary"]["imported_equipment"], 96)
|
||||
self.assertEqual(response.data["summary"]["physical_wear_percent"], 32.0)
|
||||
self.assertEqual(response.data["summary"]["weighted_wear_percent"], 32.0)
|
||||
self.assertIsNone(response.data["summary"]["weighted_wear_percent"])
|
||||
self.assertEqual(response.data["summary"]["utilization_rate"], 0.92)
|
||||
self.assertEqual(response.data["summary"]["commissioned_equipment"], 1)
|
||||
self.assertEqual(response.data["summary"]["decommissioned_equipment"], 1)
|
||||
self.assertEqual(response.data["summary"]["commissioned_equipment"], 12)
|
||||
self.assertEqual(response.data["summary"]["decommissioned_equipment"], 7)
|
||||
self.assertEqual(
|
||||
response.data["age_distribution"][0]["bucket"], "under_5_years"
|
||||
)
|
||||
self.assertEqual(len(response.data["age_distribution"]), 5)
|
||||
self.assertEqual(response.data["age_distribution"][0]["imported_equipment"], 35)
|
||||
self.assertEqual(
|
||||
set(response.data["age_distribution"][0]),
|
||||
{
|
||||
@@ -400,13 +488,13 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
response.data["categories"][0],
|
||||
{
|
||||
"category": "Станочное оборудование",
|
||||
"total_equipment": 54,
|
||||
"domestic_equipment": 31,
|
||||
"imported_equipment": 23,
|
||||
"physical_wear_percent": 28.4,
|
||||
"weighted_wear_percent": 28.4,
|
||||
"total_equipment": 187,
|
||||
"domestic_equipment": None,
|
||||
"imported_equipment": 96,
|
||||
"physical_wear_percent": None,
|
||||
"weighted_wear_percent": None,
|
||||
"utilization_rate": 0.92,
|
||||
"lease_share_itn_percent": None,
|
||||
"lease_share_itn_percent": 20.0,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user