feat: export organizations to state corp v3
All checks were successful
CI/CD Pipeline / Quality Gate (push) Successful in 30s
CI/CD Pipeline / Build and Push Images (push) Successful in 41s
CI/CD Pipeline / Internal Notify (push) Successful in 0s
CI/CD Pipeline / Deploy Dev via Compose (push) Successful in 19s

This commit is contained in:
2026-06-13 14:07:40 +02:00
parent 90d6bb90bd
commit f95ba3f56a
11 changed files with 513 additions and 218 deletions

View File

@@ -7,6 +7,8 @@ import hashlib
import json
import struct
import zlib
from datetime import date
from decimal import Decimal
from io import BytesIO
from unittest.mock import Mock, patch
from zipfile import ZipFile
@@ -18,9 +20,9 @@ from apps.parsers.models import (
GenericParserRecord,
ParserLoadLog,
)
from apps.registers.models import Register
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from django.test import TestCase, override_settings
from organizations.models import Organization
from tests.apps.parsers.factories import (
IndustrialCertificateRecordFactory,
@@ -29,11 +31,6 @@ from tests.apps.parsers.factories import (
ManufacturerRecordFactory,
ProcurementRecordFactory,
)
from tests.apps.registers.factories import (
OrganizationFactory,
RegisterFactory,
RegistryMembershipPeriodFactory,
)
def _b64url_decode(value: str) -> bytes:
@@ -71,24 +68,59 @@ class StateCorpExchangeServiceTest(TestCase):
"""Verify package compatibility with state-corp receiver contract."""
def test_build_package_contains_expected_payload(self):
registry = Register.objects.get(name="Реестр госкорпорации Росатом")
organization = OrganizationFactory.create(
mn_inn=7707083893,
mn_ogrn=1027700132195,
organization = Organization.objects.create(
rn=1001,
name="АО Альфа",
full_name="Акционерное общество Альфа",
short_name="АО Альфа",
pn_name="АО Альфа",
in_kpp=770701001,
mn_okpo="12345678",
)
RegistryMembershipPeriodFactory.create(
registry=registry,
organization=organization,
started_at="2026-01-01",
ended_at=None,
pn_name_en="Alpha JSC",
inn="7707083893",
ogrn="1027700132195",
kpp="770701001",
okpo="12345678",
gk_code="1",
gk_name="Росатом",
in_korp_code="head",
in_korp_name="Головная организация",
filial=".F.",
is_branch=False,
registration_date=date(2024, 2, 15),
create_date="2024",
organizational_legal_form="12267",
organizational_legal_form1="Акционерное общество",
ownership_form="61",
ownership_form1="Федеральная собственность",
authorized_capital=Decimal("1500000.50"),
legal_address="г. Москва, ул. Тверская, д. 1",
business_act_cod="26.11",
business_activity="Производство электронных компонентов",
general_director="Иванов Иван Иванович",
general_director_tax_id="123456789012",
uk="АО Управляющая компания",
inn_uk="7707000000",
appointment_date=date(2025, 1, 10),
cf_fl_rn="director-001",
akc_fs="100",
akc_sf="0",
re_za=True,
re_zasf=False,
goz_participation=True,
opk_registry_membership=True,
ropk_num="РОПК-001",
ropk_razdel_num="opk-1",
ropk_razdel_name="Росатом ОПК",
min="Минпромторг",
dep="Департамент радиоэлектроники",
otr="Радиоэлектроника",
integrated_structure="Интегрированная структура Альфа",
state_sector_code="10",
state_sector_name="Государственный сектор",
)
IndustrialCertificateRecordFactory.create(
inn=str(organization.mn_inn),
ogrn=str(organization.mn_ogrn),
organisation_name=organization.pn_name,
inn=organization.inn,
ogrn=organization.ogrn,
organisation_name=organization.name,
certificate_number="cert-001",
issue_date="2026-01-10",
issue_date_normalized="2026-01-10",
@@ -97,23 +129,23 @@ class StateCorpExchangeServiceTest(TestCase):
certificate_file_url="https://minpromtorg.gov.ru/cert/001",
)
ManufacturerRecordFactory.create(
inn=str(organization.mn_inn),
ogrn=str(organization.mn_ogrn),
full_legal_name=organization.pn_name,
inn=organization.inn,
ogrn=organization.ogrn,
full_legal_name=organization.name,
address="г. Москва, ул. Тверская, д. 1",
)
IndustrialProductRecordFactory.create(
inn=str(organization.mn_inn),
ogrn=str(organization.mn_ogrn),
full_organisation_name=organization.pn_name,
inn=organization.inn,
ogrn=organization.ogrn,
full_organisation_name=organization.name,
registry_number="prod-001",
product_name="Система связи М-1",
product_model="Связь",
)
InspectionRecordFactory.create(
inn=str(organization.mn_inn),
ogrn=str(organization.mn_ogrn),
organisation_name=organization.pn_name,
inn=organization.inn,
ogrn=organization.ogrn,
organisation_name=organization.name,
registration_number="insp-001",
control_authority="Минпромторг",
legal_basis="294-ФЗ",
@@ -122,9 +154,9 @@ class StateCorpExchangeServiceTest(TestCase):
status="active",
)
ProcurementRecordFactory.create(
customer_inn=str(organization.mn_inn),
customer_ogrn=str(organization.mn_ogrn),
customer_name=organization.pn_name,
customer_inn=organization.inn,
customer_ogrn=organization.ogrn,
customer_name=organization.name,
purchase_number="purchase-001",
purchase_name="Поставка оборудования",
law_type="223-ФЗ",
@@ -137,7 +169,7 @@ class StateCorpExchangeServiceTest(TestCase):
)
financial_report = FinancialReport.objects.create(
external_id="fin-001",
ogrn=str(organization.mn_ogrn),
ogrn=organization.ogrn,
file_name="fin_001_1027700132195.xlsx",
file_hash="f" * 64,
load_batch=1,
@@ -157,8 +189,8 @@ class StateCorpExchangeServiceTest(TestCase):
source=ParserLoadLog.Source.PROCUREMENTS_44FZ,
load_batch=1,
external_id="purchase-generic-001",
inn=str(organization.mn_inn),
ogrn=str(organization.mn_ogrn),
inn=organization.inn,
ogrn=organization.ogrn,
title="Поставка generic-оборудования",
record_date="15.02.2026",
amount="2500000.00",
@@ -173,8 +205,8 @@ class StateCorpExchangeServiceTest(TestCase):
source=ParserLoadLog.Source.ARBITRATION,
load_batch=1,
external_id="case-001",
inn=str(organization.mn_inn),
ogrn=str(organization.mn_ogrn),
inn=organization.inn,
ogrn=organization.ogrn,
title="А40-1/2026",
record_date="2026-03-25",
status="in_progress",
@@ -188,8 +220,8 @@ class StateCorpExchangeServiceTest(TestCase):
source=ParserLoadLog.Source.FEDRESURS_BANKRUPTCY,
load_batch=1,
external_id="fedresurs-001",
inn=str(organization.mn_inn),
ogrn=str(organization.mn_ogrn),
inn=organization.inn,
ogrn=organization.ogrn,
title="Сообщение о намерении",
record_date="2026-03-26",
status="published",
@@ -204,15 +236,15 @@ class StateCorpExchangeServiceTest(TestCase):
source=ParserLoadLog.Source.FAS_GOZ,
load_batch=1,
external_id="fas-goz-001",
inn=str(organization.mn_inn),
ogrn=str(organization.mn_ogrn),
inn=organization.inn,
ogrn=organization.ogrn,
title="Уклонение от заключения контракта",
record_date="2026-02-20",
status="active",
url="https://fas.gov.ru/register/001",
payload={
"Номер реестровой записи": "ГОЗ-001",
"Полное наименование лица": organization.pn_name,
"Полное наименование лица": organization.name,
"Дата вступления постановления": "2026-02-20",
},
)
@@ -220,8 +252,8 @@ class StateCorpExchangeServiceTest(TestCase):
source=ParserLoadLog.Source.FSTEC,
load_batch=1,
external_id="fstec-001",
inn=str(organization.mn_inn),
ogrn=str(organization.mn_ogrn),
inn=organization.inn,
ogrn=organization.ogrn,
title="Реестр лицензий ФСТЭК",
record_date="2026-01-10",
status="present",
@@ -236,8 +268,8 @@ class StateCorpExchangeServiceTest(TestCase):
source=ParserLoadLog.Source.TRUDVSEM,
load_batch=1,
external_id="trudvsem-001",
inn=str(organization.mn_inn),
ogrn=str(organization.mn_ogrn),
inn=organization.inn,
ogrn=organization.ogrn,
title="Инженер-испытатель",
record_date="2026-04-01",
amount="175000.00",
@@ -248,7 +280,7 @@ class StateCorpExchangeServiceTest(TestCase):
package = StateCorpExchangeService.build_package(actual_date="2026-03-15")
self.assertEqual(package.payload_counts["organizations"], 1)
self.assertEqual(package.payload_counts["registry_memberships"], 1)
self.assertNotIn("registry_memberships", package.payload_counts)
self.assertEqual(package.payload_counts["industrial_certificates"], 1)
self.assertEqual(package.payload_counts["manufacturers"], 1)
self.assertEqual(package.payload_counts["industrial_products"], 1)
@@ -264,15 +296,65 @@ class StateCorpExchangeServiceTest(TestCase):
payload = _decode_package_payload(package)
self.assertEqual(payload["format"], StateCorpExchangeService.PAYLOAD_FORMAT)
self.assertEqual(payload["schema_version"], 3)
self.assertEqual(payload["manifest"]["schema_version"], 3)
self.assertEqual(payload["manifest"]["source_system"], "mostovik")
self.assertEqual(payload["data"]["organizations"][0]["inn"], "7707083893")
self.assertNotIn("registry_memberships", payload["data"])
self.assertNotIn("registry_memberships", payload["manifest"]["sections"])
self.assertEqual(
payload["data"]["registry_memberships"][0],
payload["data"]["organizations"][0],
{
"organization_inn": "7707083893",
"registry_name": "Реестр госкорпорации Росатом",
"started_at": "2026-01-01",
"ended_at": None,
"mostovik_uid": str(organization.uid),
"rn": 1001,
"name": "АО Альфа",
"full_name": "Акционерное общество Альфа",
"short_name": "АО Альфа",
"pn_name": "АО Альфа",
"pn_name_en": "Alpha JSC",
"inn": "7707083893",
"kpp": "770701001",
"ogrn": "1027700132195",
"ogrip": "",
"okpo": "12345678",
"identity_status": "complete",
"primary_identity": "inn:7707083893:kpp:770701001",
"gk_code": "1",
"gk_name": "Росатом",
"in_korp_code": "head",
"in_korp_name": "Головная организация",
"filial": ".F.",
"is_branch": False,
"re_za": True,
"re_zasf": False,
"goz_participation": True,
"opk_registry_membership": True,
"ropk_num": "РОПК-001",
"ropk_razdel_num": "opk-1",
"ropk_razdel_name": "Росатом ОПК",
"registration_date": "2024-02-15",
"create_date": "2024",
"organizational_legal_form": "12267",
"organizational_legal_form1": "Акционерное общество",
"ownership_form": "61",
"ownership_form1": "Федеральная собственность",
"authorized_capital": "1500000.50",
"legal_address": "г. Москва, ул. Тверская, д. 1",
"business_act_cod": "26.11",
"business_activity": "Производство электронных компонентов",
"general_director": "Иванов Иван Иванович",
"general_director_tax_id": "123456789012",
"uk": "АО Управляющая компания",
"inn_uk": "7707000000",
"appointment_date": "2025-01-10",
"cf_fl_rn": "director-001",
"akc_fs": "100",
"akc_sf": "0",
"min": "Минпромторг",
"dep": "Департамент радиоэлектроники",
"otr": "Радиоэлектроника",
"integrated_structure": "Интегрированная структура Альфа",
"state_sector_code": "10",
"state_sector_name": "Государственный сектор",
},
)
self.assertEqual(
@@ -320,56 +402,38 @@ class StateCorpExchangeServiceTest(TestCase):
"Инженер-испытатель",
)
def test_build_package_exports_only_active_rosatom_roscosmos_registry_members(self):
target_registry = Register.objects.get(name="Реестр госкорпорации Роскосмос")
non_target_registry = RegisterFactory.create()
target = OrganizationFactory.create(
mn_inn=7707000001,
mn_ogrn=1027700000001,
pn_name="АО Целевая",
def test_build_package_exports_only_rosatom_roscosmos_organizations(self):
target = Organization.objects.create(
inn="7707000001",
ogrn="1027700000001",
name="АО Целевая",
gk_name="Роскосмос",
)
non_target = OrganizationFactory.create(
mn_inn=7707000002,
mn_ogrn=1027700000002,
pn_name="АО Не экспортируется",
non_target = Organization.objects.create(
inn="7707000002",
ogrn="1027700000002",
name="АО Не экспортируется",
gk_name="Иная корпорация",
)
inactive_target = OrganizationFactory.create(
mn_inn=7707000003,
mn_ogrn=1027700000003,
pn_name="АО Бывшая",
)
RegistryMembershipPeriodFactory.create(
registry=target_registry,
organization=target,
started_at="2026-01-01",
ended_at=None,
)
RegistryMembershipPeriodFactory.create(
registry=non_target_registry,
organization=non_target,
started_at="2026-01-01",
ended_at=None,
)
RegistryMembershipPeriodFactory.create(
registry=target_registry,
organization=inactive_target,
started_at="2026-01-01",
ended_at="2026-03-01",
rosatom = Organization.objects.create(
inn="7707000003",
ogrn="1027700000003",
name="АО Росатом",
gk_name="ГК Росатом",
)
IndustrialProductRecordFactory.create(
inn=str(target.mn_inn),
inn=target.inn,
registry_number="target-product",
)
IndustrialProductRecordFactory.create(
inn=str(non_target.mn_inn),
inn=non_target.inn,
registry_number="non-target-product",
)
GenericParserRecord.objects.create(
source=ParserLoadLog.Source.TRUDVSEM,
load_batch=1,
external_id="target-vacancy",
inn=str(target.mn_inn),
inn=target.inn,
title="Целевая вакансия",
record_date="2026-03-10",
)
@@ -377,7 +441,7 @@ class StateCorpExchangeServiceTest(TestCase):
source=ParserLoadLog.Source.TRUDVSEM,
load_batch=1,
external_id="non-target-vacancy",
inn=str(non_target.mn_inn),
inn=non_target.inn,
title="Лишняя вакансия",
record_date="2026-03-10",
)
@@ -386,16 +450,10 @@ class StateCorpExchangeServiceTest(TestCase):
payload = _decode_package_payload(package)
self.assertEqual(
[item["inn"] for item in payload["data"]["organizations"]],
[str(target.mn_inn)],
)
self.assertEqual(
[
(item["organization_inn"], item["registry_name"])
for item in payload["data"]["registry_memberships"]
],
[(str(target.mn_inn), "Реестр госкорпорации Роскосмос")],
sorted(item["inn"] for item in payload["data"]["organizations"]),
sorted([target.inn, rosatom.inn]),
)
self.assertNotIn("registry_memberships", payload["data"])
self.assertEqual(
[
item["registry_number"]
@@ -408,6 +466,16 @@ class StateCorpExchangeServiceTest(TestCase):
["target-vacancy"],
)
package = StateCorpExchangeService.build_package(
organization_inns=[target.inn, non_target.inn],
actual_date="2026-03-15",
)
payload = _decode_package_payload(package)
self.assertEqual(
[item["inn"] for item in payload["data"]["organizations"]],
[target.inn],
)
@patch("apps.exchange.state_corp_services.requests.post")
def test_send_package_posts_multipart_archive(self, post_mock):
response_mock = Mock(status_code=201)

View File

@@ -1,12 +1,17 @@
"""Tests for organizations API v2."""
from io import BytesIO
from apps.parsers.models import ParserLoadLog
from django.core.cache import cache
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db import connection
from django.test import override_settings
from django.test.utils import CaptureQueriesContext
from django.urls import reverse
from openpyxl import Workbook
from organizations.cache import invalidate_organization_api_cache
from organizations.directory_import import DIRECTORY_SHEET, SOURCE_HEADERS
from organizations.filters import OrganizationFilter
from organizations.models import (
FinancialIndicatorsExtension,
@@ -19,6 +24,7 @@ from organizations.models import (
SecurityRegistryExtension,
VacancyExtension,
)
from registers.models import Organization as LegacyRegisterOrganization
from rest_framework import status
from rest_framework.test import APITestCase
@@ -33,6 +39,17 @@ class OrganizationsApiV2Test(APITestCase):
self.user = UserFactory.create_user()
self.client.force_authenticate(self.user)
@staticmethod
def _append_reference_sheet(
workbook: Workbook,
title: str,
values: dict[str, str],
) -> None:
worksheet = workbook.create_sheet(title=title)
worksheet.append(["kod", "value"])
for key, value in values.items():
worksheet.append([key, value])
def test_list_is_paginated_and_available_only_under_v2(self):
Organization.objects.create(
name='ООО "Альфа"',
@@ -60,6 +77,73 @@ class OrganizationsApiV2Test(APITestCase):
v1_response = self.client.get("/api/v1/organizations/")
self.assertEqual(v1_response.status_code, status.HTTP_404_NOT_FOUND)
def test_admin_import_directory_uploads_xlsx_into_canonical_organizations(self):
self.client.force_authenticate(UserFactory.create_superuser())
workbook = Workbook()
worksheet = workbook.active
worksheet.title = DIRECTORY_SHEET
worksheet.append(SOURCE_HEADERS)
worksheet.append(
[
{
"rn": "10",
"_gk": "1",
"in_korp": "1",
"full_name": 'Акционерное общество "Тест"',
"short_name": 'АО "Тест"',
"pn_name": 'АО "Тест"',
"inn": "7701001001",
"ogrn": "1027700100001",
"okpo": "00123456",
"filial": ".F.",
"kpp": "770101001",
"registration_date": "01.02.2020",
"authorized_capita": 1000,
"business_act_cod": "2",
"goz_participation": ".T.",
"opk_registry_membership": ".F.",
"_k": "2",
}.get(header, "")
for header in SOURCE_HEADERS
]
)
self._append_reference_sheet(workbook, "_gk", {"1": "Роскосмос"})
self._append_reference_sheet(workbook, "in_korp", {"1": "Входит в состав"})
self._append_reference_sheet(
workbook,
"business_act_cod",
{"2": "Производственная"},
)
self._append_reference_sheet(
workbook,
"_k - Госектор",
{"2": "Головные исполнители"},
)
buffer = BytesIO()
workbook.save(buffer)
upload = SimpleUploadedFile(
"directory.xlsx",
buffer.getvalue(),
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)
response = self.client.post(
reverse("api_v2:organizations:organizations-import-directory"),
{"file": upload},
format="multipart",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["success"], True)
self.assertEqual(response.data["created"], 1)
self.assertEqual(LegacyRegisterOrganization.objects.count(), 0)
organization = Organization.objects.get(rn=10)
self.assertEqual(organization.name, 'Акционерное общество "Тест"')
self.assertEqual(organization.inn, "7701001001")
self.assertEqual(organization.gk_name, "Роскосмос")
self.assertEqual(organization.business_activity, "Производственная")
self.assertTrue(organization.goz_participation)
def test_openapi_documents_v2_organizations_endpoints(self):
response = self.client.get(
reverse("schema-swagger-ui"),

View File

@@ -0,0 +1,23 @@
"""Regression tests for removed legacy register seed records."""
from apps.registers.models import Register
from django.test import TestCase
LEGACY_REGISTER_NAMES = (
"Реестр предприятий ОПК",
"Реестр госкорпорации Роскосмос",
"Реестр госкорпорации Роскосмос ГОЗ",
"Реестр госкорпорации Роскосмос ОПК",
"Реестр госкорпорации Росатом",
"Реестр госкорпорации Росатом ГОЗ",
"Реестр госкорпорации Росатом ОПК",
)
class LegacyRegistersRemovedTest(TestCase):
"""Old Mostovik corporate registers are no longer seeded."""
def test_legacy_register_seed_records_are_absent(self):
self.assertFalse(
Register.objects.filter(name__in=LEGACY_REGISTER_NAMES).exists()
)