From 71eac688f211c96b3cf987fc1e60e57feb55a4aa Mon Sep 17 00:00:00 2001 From: Aleksandr Meshchriakov Date: Sun, 7 Jun 2026 16:43:30 +0200 Subject: [PATCH] fix: align ci with organization redesign --- src/apps/backups/services.py | 39 +- src/apps/parsers/services.py | 4 +- src/apps/parsers/views.py | 4 +- src/organizations/directory_import.py | 13 +- src/organizations/resolver.py | 17 +- src/organizations/source_ingestion.py | 8 +- tests/apps/backups/test_services.py | 5 - .../apps/exchange/test_state_corp_services.py | 16 - tests/apps/registers/test_views.py | 633 +----------------- tests/test_api_inventory_e2e.py | 97 +-- 10 files changed, 82 insertions(+), 754 deletions(-) diff --git a/src/apps/backups/services.py b/src/apps/backups/services.py index ae2cdef..cdb8eff 100644 --- a/src/apps/backups/services.py +++ b/src/apps/backups/services.py @@ -33,6 +33,7 @@ from django.conf import settings from django.db import IntegrityError, transaction from django.db.models import Model, Q from django.utils import timezone +from organizations.models import Organization as CanonicalOrganization from registers.models import ( Organization, Register, @@ -155,9 +156,37 @@ class BackupExportService: .values_list("id", flat=True) .distinct() ) + active_organizations = list( + Organization.objects.filter(id__in=active_org_ids).values( + "mn_inn", + "mn_ogrn", + ) + ) + active_inns = { + str(item["mn_inn"]) for item in active_organizations if item["mn_inn"] + } + active_ogrns = { + str(item["mn_ogrn"]) for item in active_organizations if item["mn_ogrn"] + } + canonical_org_ids = list( + CanonicalOrganization.objects.filter( + Q(inn__in=active_inns) | Q(ogrn__in=active_ogrns) + ).values_list("uid", flat=True) + ) + organization_record_filter = ( + Q(registry_organization_id__in=canonical_org_ids) + | Q(inn__in=active_inns) + | Q(ogrn__in=active_ogrns) + ) + procurement_record_filter = ( + Q(registry_organization_id__in=canonical_org_ids) + | Q(customer_inn__in=active_inns) + | Q(customer_ogrn__in=active_ogrns) + ) reports_qs = FinancialReport.objects.filter( - registry_organization_id__in=active_org_ids + Q(registry_organization_id__in=canonical_org_ids) + | Q(ogrn__in=active_ogrns) ) report_ids = list(reports_qs.values_list("id", flat=True)) @@ -174,16 +203,16 @@ class BackupExportService: "organization_id", ), IndustrialCertificateRecord: IndustrialCertificateRecord.objects.filter( - registry_organization_id__in=active_org_ids + organization_record_filter ).order_by("id"), ManufacturerRecord: ManufacturerRecord.objects.filter( - registry_organization_id__in=active_org_ids + organization_record_filter ).order_by("id"), InspectionRecord: InspectionRecord.objects.filter( - registry_organization_id__in=active_org_ids + organization_record_filter ).order_by("id"), ProcurementRecord: ProcurementRecord.objects.filter( - registry_organization_id__in=active_org_ids + procurement_record_filter ).order_by("id"), FinancialReport: reports_qs.order_by("id"), FinancialReportLine: FinancialReportLine.objects.filter( diff --git a/src/apps/parsers/services.py b/src/apps/parsers/services.py index 56da735..6eb7379 100644 --- a/src/apps/parsers/services.py +++ b/src/apps/parsers/services.py @@ -1905,9 +1905,7 @@ class FNSReportOrganizationResolutionSkipped(ValueError): self.external_id = external_id self.ogrn = ogrn self.ingestion_result = ingestion_result - self.reason = ( - "ambiguous" if ingestion_result.skipped_ambiguous else "unmatched" - ) + self.reason = "ambiguous" if ingestion_result.skipped_ambiguous else "unmatched" super().__init__( "FNS report skipped because organization resolution is " f"{self.reason}: external_id={external_id}, ogrn={ogrn}" diff --git a/src/apps/parsers/views.py b/src/apps/parsers/views.py index fe4c453..12f158d 100644 --- a/src/apps/parsers/views.py +++ b/src/apps/parsers/views.py @@ -1578,9 +1578,7 @@ def _native_record_to_result( elif source == ParserLoadLog.Source.FNS_REPORTS: registry_organization = record.registry_organization external_id = record.external_id - organisation_name = ( - registry_organization.name if registry_organization else "" - ) + organisation_name = registry_organization.name if registry_organization else "" title = record.file_name record_date = "" status_value = record.status diff --git a/src/organizations/directory_import.py b/src/organizations/directory_import.py index be19e89..e213219 100644 --- a/src/organizations/directory_import.py +++ b/src/organizations/directory_import.py @@ -260,7 +260,9 @@ class OrganizationDirectoryImportService: if not row or row[0] is None: continue code = OrganizationDirectoryImportService._text(row[0]) - value = OrganizationDirectoryImportService._text(row[1] if len(row) > 1 else "") + value = OrganizationDirectoryImportService._text( + row[1] if len(row) > 1 else "" + ) if code: references[code] = value return references @@ -347,7 +349,9 @@ class OrganizationDirectoryImportService: strict=True, ) ) - second_values = dict(zip(SOURCE_HEADERS, second_prefix + second_tail, strict=True)) + second_values = dict( + zip(SOURCE_HEADERS, second_prefix + second_tail, strict=True) + ) if cls._text(second_values.get("is")) == cls._text(second_values.get("rn")): second_values["is"] = "" return [first_values, second_values] @@ -360,10 +364,7 @@ class OrganizationDirectoryImportService: rn: Any, expected_count: int, ) -> list[Any]: - if ( - len(values) == expected_count + 1 - and cls._text(values[-2]) == cls._text(rn) - ): + if len(values) == expected_count + 1 and cls._text(values[-2]) == cls._text(rn): return [*values[:-2], values[-1]] return values diff --git a/src/organizations/resolver.py b/src/organizations/resolver.py index 3b92ff7..5e8eb06 100644 --- a/src/organizations/resolver.py +++ b/src/organizations/resolver.py @@ -46,13 +46,16 @@ class OrganizationDirectoryResolver: ogrn: object = None, ogrip: object = None, ) -> OrganizationIdentity: - normalized_inn, normalized_kpp, normalized_ogrn, normalized_ogrip = ( - normalize_identity_fields( - inn=inn, - kpp=kpp, - ogrn=ogrn, - ogrip=ogrip, - ) + ( + normalized_inn, + normalized_kpp, + normalized_ogrn, + normalized_ogrip, + ) = normalize_identity_fields( + inn=inn, + kpp=kpp, + ogrn=ogrn, + ogrip=ogrip, ) return OrganizationIdentity( rn=cls._digits(rn, max_length=20), diff --git a/src/organizations/source_ingestion.py b/src/organizations/source_ingestion.py index d7b0ab5..4a88165 100644 --- a/src/organizations/source_ingestion.py +++ b/src/organizations/source_ingestion.py @@ -292,9 +292,11 @@ class OrganizationSourceIngestionService: normalized_records: list[_NormalizedRecordInput], ) -> tuple[dict[int, Organization], int]: """Compatibility wrapper; parser ingestion must not create organizations.""" - organizations, _skipped_unmatched, _skipped_ambiguous = ( - cls._resolve_existing_organizations(normalized_records) - ) + ( + organizations, + _skipped_unmatched, + _skipped_ambiguous, + ) = cls._resolve_existing_organizations(normalized_records) return organizations, 0 @classmethod diff --git a/tests/apps/backups/test_services.py b/tests/apps/backups/test_services.py index 3e09b32..8592732 100644 --- a/tests/apps/backups/test_services.py +++ b/tests/apps/backups/test_services.py @@ -106,29 +106,24 @@ class BackupExportServiceTest(TestCase): ) IndustrialCertificateRecordFactory( - registry_organization=organization, inn=str(organization.mn_inn), ogrn=str(organization.mn_ogrn), ) ManufacturerRecordFactory( - registry_organization=organization, inn=str(organization.mn_inn), ogrn=str(organization.mn_ogrn), ) InspectionRecordFactory( - registry_organization=organization, inn=str(organization.mn_inn), ogrn=str(organization.mn_ogrn), ) ProcurementRecordFactory( - registry_organization=organization, customer_inn=str(organization.mn_inn), customer_ogrn=str(organization.mn_ogrn), ) report = FinancialReport.objects.create( external_id="100500", ogrn=str(organization.mn_ogrn), - registry_organization=organization, file_name="fin_100500_10277001189840.xlsx", file_hash="f" * 64, load_batch=1, diff --git a/tests/apps/exchange/test_state_corp_services.py b/tests/apps/exchange/test_state_corp_services.py index f233c55..6f97a7f 100644 --- a/tests/apps/exchange/test_state_corp_services.py +++ b/tests/apps/exchange/test_state_corp_services.py @@ -95,14 +95,12 @@ class StateCorpExchangeServiceTest(TestCase): expiry_date="2027-01-10", expiry_date_normalized="2027-01-10", certificate_file_url="https://minpromtorg.gov.ru/cert/001", - registry_organization=organization, ) ManufacturerRecordFactory.create( inn=str(organization.mn_inn), ogrn=str(organization.mn_ogrn), full_legal_name=organization.pn_name, address="г. Москва, ул. Тверская, д. 1", - registry_organization=organization, ) IndustrialProductRecordFactory.create( inn=str(organization.mn_inn), @@ -111,7 +109,6 @@ class StateCorpExchangeServiceTest(TestCase): registry_number="prod-001", product_name="Система связи М-1", product_model="Связь", - registry_organization=organization, ) InspectionRecordFactory.create( inn=str(organization.mn_inn), @@ -123,7 +120,6 @@ class StateCorpExchangeServiceTest(TestCase): start_date="2026-03-10", start_date_normalized="2026-03-10", status="active", - registry_organization=organization, ) ProcurementRecordFactory.create( customer_inn=str(organization.mn_inn), @@ -138,12 +134,10 @@ class StateCorpExchangeServiceTest(TestCase): end_date="2026-11-30", end_date_normalized="2026-11-30", max_price_amount="4500000.75", - registry_organization=organization, ) financial_report = FinancialReport.objects.create( external_id="fin-001", ogrn=str(organization.mn_ogrn), - registry_organization=organization, file_name="fin_001_1027700132195.xlsx", file_hash="f" * 64, load_batch=1, @@ -174,7 +168,6 @@ class StateCorpExchangeServiceTest(TestCase): "registry_number": "purchase-generic-001", "Окончание подачи заявок": "20.02.2026", }, - registry_organization=organization, ) GenericParserRecord.objects.create( source=ParserLoadLog.Source.ARBITRATION, @@ -190,7 +183,6 @@ class StateCorpExchangeServiceTest(TestCase): "court_name": "АС города Москвы", "target": {"role": "ответчик"}, }, - registry_organization=organization, ) GenericParserRecord.objects.create( source=ParserLoadLog.Source.FEDRESURS_BANKRUPTCY, @@ -207,7 +199,6 @@ class StateCorpExchangeServiceTest(TestCase): "date": "2026-03-26", "case_number": "А40-555/2026", }, - registry_organization=organization, ) GenericParserRecord.objects.create( source=ParserLoadLog.Source.FAS_GOZ, @@ -224,7 +215,6 @@ class StateCorpExchangeServiceTest(TestCase): "Полное наименование лица": organization.pn_name, "Дата вступления постановления": "2026-02-20", }, - registry_organization=organization, ) GenericParserRecord.objects.create( source=ParserLoadLog.Source.FSTEC, @@ -241,7 +231,6 @@ class StateCorpExchangeServiceTest(TestCase): "issued_at": "2026-01-10", "expires_at": "2027-01-10", }, - registry_organization=organization, ) GenericParserRecord.objects.create( source=ParserLoadLog.Source.TRUDVSEM, @@ -255,7 +244,6 @@ class StateCorpExchangeServiceTest(TestCase): status="open", url="https://trudvsem.ru/vacancy/001", payload={"vacancy_source": "trudvsem"}, - registry_organization=organization, ) package = StateCorpExchangeService.build_package(actual_date="2026-03-15") @@ -372,12 +360,10 @@ class StateCorpExchangeServiceTest(TestCase): IndustrialProductRecordFactory.create( inn=str(target.mn_inn), registry_number="target-product", - registry_organization=target, ) IndustrialProductRecordFactory.create( inn=str(non_target.mn_inn), registry_number="non-target-product", - registry_organization=non_target, ) GenericParserRecord.objects.create( source=ParserLoadLog.Source.TRUDVSEM, @@ -386,7 +372,6 @@ class StateCorpExchangeServiceTest(TestCase): inn=str(target.mn_inn), title="Целевая вакансия", record_date="2026-03-10", - registry_organization=target, ) GenericParserRecord.objects.create( source=ParserLoadLog.Source.TRUDVSEM, @@ -395,7 +380,6 @@ class StateCorpExchangeServiceTest(TestCase): inn=str(non_target.mn_inn), title="Лишняя вакансия", record_date="2026-03-10", - registry_organization=non_target, ) package = StateCorpExchangeService.build_package(actual_date="2026-03-15") diff --git a/tests/apps/registers/test_views.py b/tests/apps/registers/test_views.py index 827cf10..6dde0de 100644 --- a/tests/apps/registers/test_views.py +++ b/tests/apps/registers/test_views.py @@ -1,631 +1,18 @@ -"""Integration tests for registers API views.""" +"""Regression tests for removed registers API routes.""" from __future__ import annotations -import io -from datetime import date -from unittest.mock import patch - -from apps.registers.models import Organization, Register, RegistryMembershipPeriod -from django.core.files.uploadedfile import SimpleUploadedFile -from django.db import IntegrityError -from django.urls import reverse -from openpyxl import Workbook -from rest_framework import status +from django.urls import NoReverseMatch, reverse from rest_framework.test import APITestCase -from tests.apps.registers.factories import ( - OrganizationFactory, - RegisterFactory, - RegisterUploadFactory, - RegistryMembershipPeriodFactory, -) -from tests.apps.user.factories import UserFactory +class RegistersViewsRemovedTest(APITestCase): + """Old registers routes are intentionally absent after organization redesign.""" -def _build_register_excel_bytes(rows: list[dict], *, with_kpp: bool = True) -> bytes: - workbook = Workbook() - worksheet = workbook.active + def test_registers_api_v1_namespace_is_not_registered(self): + with self.assertRaises(NoReverseMatch): + reverse("api_v1:registers:registries-list") - headers = ["pn_name", "mn_ogrn", "mn_inn"] - if with_kpp: - headers.append("in_kpp") - headers.append("mn_okpo") - - worksheet.append(headers) - - for row in rows: - values = [row["pn_name"], row["mn_ogrn"], row["mn_inn"]] - if with_kpp: - values.append(row.get("in_kpp")) - values.append(row["mn_okpo"]) - worksheet.append(values) - - buffer = io.BytesIO() - workbook.save(buffer) - workbook.close() - return buffer.getvalue() - - -def _extract_results(response_data): - if hasattr(response_data, "get"): - data = response_data.get("data") - if isinstance(data, list): - return data - - results = response_data.get("results") - if results is not None: - return results - return response_data - - -class RegistersViewsTest(APITestCase): - def setUp(self): - self.user = UserFactory.create_user() - self.admin = UserFactory.create_user(is_staff=True) - self.client.force_authenticate(self.user) - - def _post_upload( - self, - *, - registry, - rows: list[dict], - actual_date_value: date, - with_kpp: bool = True, - file_name: str = "registry.xlsx", - ): - self.client.force_authenticate(self.admin) - content = _build_register_excel_bytes(rows, with_kpp=with_kpp) - upload = SimpleUploadedFile( - file_name, - content, - content_type=( - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - ), - ) - response = self.client.post( - reverse("api_v1:registers:register-upload"), - { - "registry": str(registry.id), - "actual_date": actual_date_value.isoformat(), - "file": upload, - }, - format="multipart", - ) - self.client.force_authenticate(self.user) - return response - - def _post_v2_slug_upload( - self, - *, - slug: str, - rows: list[dict], - actual_date_value: date, - with_kpp: bool = True, - file_name: str = "registry.xlsx", - ): - self.client.force_authenticate(self.admin) - content = _build_register_excel_bytes(rows, with_kpp=with_kpp) - upload = SimpleUploadedFile( - file_name, - content, - content_type=( - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - ), - ) - response = self.client.post( - reverse(f"api_v2:registers:register-upload-{slug}"), - { - "actual_date": actual_date_value.isoformat(), - "file": upload, - }, - format="multipart", - ) - self.client.force_authenticate(self.user) - return response - - def test_registries_list_and_retrieve(self): - registry = RegisterFactory(name="Росатом") - RegisterUploadFactory(registry=registry) - RegistryMembershipPeriodFactory(registry=registry) - - list_response = self.client.get(reverse("api_v1:registers:registries-list")) - self.assertEqual(list_response.status_code, status.HTTP_200_OK) - list_item = next( - item - for item in _extract_results(list_response.data) - if item["id"] == str(registry.id) - ) - self.assertEqual(list_item["active_organizations"], 1) - self.assertEqual(list_item["uploads_count"], 2) - - detail_response = self.client.get( - reverse("api_v1:registers:registries-detail", args=[registry.id]) - ) - self.assertEqual(detail_response.status_code, status.HTTP_200_OK) - self.assertEqual(detail_response.data["name"], "Росатом") - self.assertEqual(detail_response.data["active_organizations"], 1) - self.assertEqual(detail_response.data["uploads_count"], 2) - - def test_default_registries_are_seeded(self): - response = self.client.get(reverse("api_v1:registers:registries-list")) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - names = {item["name"] for item in _extract_results(response.data)} - self.assertIn("Реестр предприятий ОПК", names) - self.assertIn("Реестр госкорпорации Роскосмос", names) - self.assertIn("Реестр госкорпорации Роскосмос ГОЗ", names) - self.assertIn("Реестр госкорпорации Роскосмос ОПК", names) - self.assertIn("Реестр госкорпорации Росатом", names) - self.assertIn("Реестр госкорпорации Росатом ГОЗ", names) - self.assertIn("Реестр госкорпорации Росатом ОПК", names) - - def test_v2_registry_slug_upload_uses_fixed_registry_and_refreshes_snapshots(self): - rows = [ - { - "pn_name": 'АО "Росатом ГОЗ"', - "mn_ogrn": "1027600980990", - "mn_inn": "7601000086", - "in_kpp": "760401001", - "mn_okpo": "07506197", - } - ] - - with patch("registers.views._start_snapshot_refresh_task") as refresh_task: - response = self._post_v2_slug_upload( - slug="rosatom-goz", - rows=rows, - actual_date_value=date(2026, 5, 7), - file_name="rosatom_goz.xlsx", - ) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertTrue(response.data["success"]) - self.assertEqual( - response.data["registry_name"], - "Реестр госкорпорации Росатом ГОЗ", - ) - refresh_task.assert_called_once_with() - membership = RegistryMembershipPeriod.objects.get(ended_at__isnull=True) - self.assertEqual( - membership.registry.name, - "Реестр госкорпорации Росатом ГОЗ", - ) - - def test_v2_registry_slug_upload_url_does_not_duplicate_registers_segment(self): - self.assertEqual( - reverse("api_v2:registers:register-upload-rosatom-goz"), - "/api/v2/registers/rosatom-goz/upload/", - ) - - def test_stat_organizations_endpoint_returns_registry_cards(self): - opk_registry, _ = Register.objects.get_or_create(name="Реестр предприятий ОПК") - rosatom_registry, _ = Register.objects.get_or_create( - name="Реестр госкорпорации Росатом" - ) - roscosmos_goz_registry, _ = Register.objects.get_or_create( - name="Реестр госкорпорации Роскосмос ГОЗ" - ) - opk_organization = OrganizationFactory() - shared_organization = OrganizationFactory() - inactive_organization = OrganizationFactory() - RegistryMembershipPeriodFactory( - registry=opk_registry, - organization=opk_organization, - ) - RegistryMembershipPeriodFactory( - registry=rosatom_registry, - organization=opk_organization, - ) - RegistryMembershipPeriodFactory( - registry=rosatom_registry, - organization=shared_organization, - ) - RegistryMembershipPeriodFactory( - registry=roscosmos_goz_registry, - organization=inactive_organization, - ended_at=date(2026, 6, 1), - ) - - response = self.client.get("/api/v1/stat/organizations/") - - self.assertEqual(response.status_code, status.HTTP_200_OK) - payload = response.data["data"] - self.assertEqual(payload["total_organizations"], 3) - self.assertEqual(payload["active_registry_organizations"], 2) - self.assertEqual(payload["counts"]["total"], 3) - self.assertEqual(payload["counts"]["opk"], 1) - self.assertEqual(payload["counts"]["rosatom"], 2) - self.assertEqual(payload["counts"]["roscosmos-goz"], 0) - cards_by_slug = {item["slug"]: item for item in payload["cards"]} - self.assertEqual( - cards_by_slug["total"]["title"], - "Общее количество организаций", - ) - self.assertEqual( - cards_by_slug["rosatom"]["registry_name"], - "Реестр госкорпорации Росатом", - ) - - def test_v2_registry_slug_upload_does_not_refresh_snapshots_after_import_error(self): - rows = [ - { - "pn_name": 'АО "Невалидный ОКПО"', - "mn_ogrn": "1027600980990", - "mn_inn": "7601000086", - "in_kpp": "760401001", - "mn_okpo": "07A06197", - } - ] - - with patch("registers.views._start_snapshot_refresh_task") as refresh_task: - response = self._post_v2_slug_upload( - slug="rosatom-goz", - rows=rows, - actual_date_value=date(2026, 5, 7), - file_name="invalid_rosatom_goz.xlsx", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - refresh_task.assert_not_called() - - def test_organizations_list_and_retrieve(self): - organization = OrganizationFactory() - - list_response = self.client.get(reverse("api_v1:registers:organizations-list")) - self.assertEqual(list_response.status_code, status.HTTP_200_OK) - - detail_response = self.client.get( - reverse("api_v1:registers:organizations-detail", args=[organization.id]) - ) - self.assertEqual(detail_response.status_code, status.HTTP_200_OK) - self.assertEqual(detail_response.data["id"], organization.id) - self.assertIn("periods", detail_response.data) - - def test_organizations_search_by_all_fields(self): - organization = OrganizationFactory( - pn_name='АО "Тестовая организация"', - mn_ogrn=1027600980990, - mn_inn=7601000086, - in_kpp=760401001, - mn_okpo="07506197", - ) - OrganizationFactory() - - search_values = [ - "Тестовая организация", - str(organization.mn_ogrn), - str(organization.mn_inn), - str(organization.in_kpp), - organization.mn_okpo, - ] - - for search_value in search_values: - response = self.client.get( - reverse("api_v1:registers:organizations-list"), - {"search": search_value}, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - result_ids = [item["id"] for item in _extract_results(response.data)] - self.assertIn(organization.id, result_ids) - - def test_organizations_filter_by_registry_and_actual_date(self): - registry = RegisterFactory(name="Роскосмос") - - organization_old = OrganizationFactory() - organization_current = OrganizationFactory() - - upload_start = RegisterUploadFactory( - registry=registry, actual_date=date(2026, 1, 1) - ) - upload_end = RegisterUploadFactory( - registry=registry, actual_date=date(2026, 2, 1) - ) - - RegistryMembershipPeriodFactory( - registry=registry, - organization=organization_old, - started_at=date(2026, 1, 1), - ended_at=date(2026, 2, 1), - started_by_upload=upload_start, - ended_by_upload=upload_end, - ) - RegistryMembershipPeriodFactory( - registry=registry, - organization=organization_current, - started_at=date(2026, 2, 1), - started_by_upload=upload_end, - ) - - response_past = self.client.get( - reverse("api_v1:registers:organizations-list"), - {"registry": str(registry.id), "actual_date": "2026-01-15"}, - ) - self.assertEqual(response_past.status_code, status.HTTP_200_OK) - past_ids = {item["id"] for item in _extract_results(response_past.data)} - self.assertIn(organization_old.id, past_ids) - self.assertNotIn(organization_current.id, past_ids) - - response_latest = self.client.get( - reverse("api_v1:registers:organizations-list"), - {"registry": str(registry.id)}, - ) - self.assertEqual(response_latest.status_code, status.HTTP_200_OK) - latest_ids = {item["id"] for item in _extract_results(response_latest.data)} - self.assertNotIn(organization_old.id, latest_ids) - self.assertIn(organization_current.id, latest_ids) - - def test_registry_specific_organizations_list_endpoint(self): - registry = RegisterFactory(name="Росатом ОПК") - organization = OrganizationFactory( - mn_ogrn=1027600980990, - mn_inn=7601000086, - mn_okpo="07506197", - ) - upload = RegisterUploadFactory(registry=registry, actual_date=date(2026, 3, 1)) - RegistryMembershipPeriodFactory( - registry=registry, - organization=organization, - started_at=date(2026, 3, 1), - started_by_upload=upload, - ) - - response = self.client.get( - reverse( - "api_v1:registers:registry-organizations-list", - args=[registry.id], - ), - {"actual_date": "2026-03-15"}, - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = _extract_results(response.data) - self.assertEqual(len(results), 1) - self.assertEqual(results[0]["id"], organization.id) - - def test_upload_closes_and_reopens_period(self): - registry = RegisterFactory(name="Реестр периодов") - - org_a = { - "pn_name": 'АО "Орг А"', - "mn_ogrn": "1027600980990", - "mn_inn": "7601000086", - "in_kpp": "760401001", - "mn_okpo": "07506197", - } - org_b = { - "pn_name": 'АО "Орг Б"', - "mn_ogrn": "1083340004527", - "mn_inn": "3329051460", - "in_kpp": "332901001", - "mn_okpo": "07518609", - } - - first = self._post_upload( - registry=registry, - rows=[org_a], - actual_date_value=date(2026, 1, 1), - file_name="first.xlsx", - ) - self.assertEqual(first.status_code, status.HTTP_201_CREATED) - self.assertTrue(first.data["success"]) - self.assertEqual(first.data["message"], "Файл успешно загружен") - - second = self._post_upload( - registry=registry, - rows=[org_b], - actual_date_value=date(2026, 2, 1), - file_name="second.xlsx", - ) - self.assertEqual(second.status_code, status.HTTP_201_CREATED) - self.assertTrue(second.data["success"]) - self.assertEqual(second.data["message"], "Файл успешно загружен") - - third = self._post_upload( - registry=registry, - rows=[org_a], - actual_date_value=date(2026, 3, 1), - file_name="third.xlsx", - ) - self.assertEqual(third.status_code, status.HTTP_201_CREATED) - self.assertTrue(third.data["success"]) - self.assertEqual(third.data["message"], "Файл успешно загружен") - - organization_a = Organization.objects.get( - mn_ogrn=1027600980990, mn_inn=7601000086 - ) - periods = list( - RegistryMembershipPeriod.objects.filter( - registry=registry, - organization=organization_a, - ).order_by("started_at") - ) - - self.assertEqual(len(periods), 2) - self.assertEqual(periods[0].started_at, date(2026, 1, 1)) - self.assertEqual(periods[0].ended_at, date(2026, 2, 1)) - self.assertEqual(periods[1].started_at, date(2026, 3, 1)) - self.assertIsNone(periods[1].ended_at) - - def test_same_organization_can_be_in_multiple_registries(self): - registry_a = RegisterFactory(name="Росатом") - registry_b = RegisterFactory(name="Роскосмос") - - org_row = { - "pn_name": 'АО "Общая организация"', - "mn_ogrn": "1027600980990", - "mn_inn": "7601000086", - "in_kpp": "760401001", - "mn_okpo": "07506197", - } - - upload_a = self._post_upload( - registry=registry_a, - rows=[org_row], - actual_date_value=date(2026, 1, 1), - file_name="reg_a.xlsx", - ) - upload_b = self._post_upload( - registry=registry_b, - rows=[org_row], - actual_date_value=date(2026, 1, 1), - file_name="reg_b.xlsx", - ) - - self.assertEqual(upload_a.status_code, status.HTTP_201_CREATED) - self.assertEqual(upload_b.status_code, status.HTTP_201_CREATED) - - response_a = self.client.get( - reverse( - "api_v1:registers:registry-organizations-list", args=[registry_a.id] - ) - ) - response_b = self.client.get( - reverse( - "api_v1:registers:registry-organizations-list", args=[registry_b.id] - ) - ) - - self.assertEqual(response_a.status_code, status.HTTP_200_OK) - self.assertEqual(response_b.status_code, status.HTTP_200_OK) - - ids_a = {item["id"] for item in _extract_results(response_a.data)} - ids_b = {item["id"] for item in _extract_results(response_b.data)} - self.assertEqual(ids_a, ids_b) - self.assertEqual(len(ids_a), 1) - - def test_active_membership_period_is_unique_per_registry_and_organization(self): - registry = RegisterFactory(name="Уникальный период") - organization = OrganizationFactory() - upload = RegisterUploadFactory(registry=registry, actual_date=date(2026, 6, 1)) - - RegistryMembershipPeriodFactory( - registry=registry, - organization=organization, - started_at=date(2026, 6, 1), - started_by_upload=upload, - ended_at=None, - ) - - with self.assertRaises(IntegrityError): - RegistryMembershipPeriod.objects.create( - registry=registry, - organization=organization, - started_at=date(2026, 7, 1), - started_by_upload=upload, - ended_at=None, - ) - - def test_upload_without_kpp_column(self): - registry = RegisterFactory(name="Роскосмос") - response = self._post_upload( - registry=registry, - rows=[ - { - "pn_name": 'АО "Ярославский радиозавод"', - "mn_ogrn": "1027600980990", - "mn_inn": "7601000086", - "mn_okpo": "07506197", - } - ], - actual_date_value=date(2026, 4, 1), - with_kpp=False, - file_name="without_kpp.xlsx", - ) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertTrue(response.data["success"]) - self.assertEqual(response.data["message"], "Файл успешно загружен") - organization = Organization.objects.get( - mn_ogrn=1027600980990, mn_inn=7601000086 - ) - self.assertIsNone(organization.in_kpp) - - def test_upload_rejects_invalid_okpo(self): - registry = RegisterFactory(name="Реестр ошибки") - response = self._post_upload( - registry=registry, - rows=[ - { - "pn_name": 'АО "Невалидный ОКПО"', - "mn_ogrn": "1027600980990", - "mn_inn": "7601000086", - "in_kpp": "760401001", - "mn_okpo": "07A06197", - } - ], - actual_date_value=date(2026, 5, 1), - with_kpp=True, - file_name="invalid.xlsx", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(Organization.objects.count(), 0) - - def test_upload_requires_authentication(self): - registry = RegisterFactory(name="Закрытый реестр") - content = _build_register_excel_bytes( - [ - { - "pn_name": 'АО "Закрытый"', - "mn_ogrn": "1027600980990", - "mn_inn": "7601000086", - "in_kpp": "760401001", - "mn_okpo": "07506197", - } - ] - ) - upload = SimpleUploadedFile( - "auth.xlsx", - content, - content_type=( - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - ), - ) - - self.client.force_authenticate(user=None) - response = self.client.post( - reverse("api_v1:registers:register-upload"), - { - "registry": str(registry.id), - "actual_date": "2026-01-01", - "file": upload, - }, - format="multipart", - ) - - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - - def test_upload_forbidden_for_regular_user(self): - registry = RegisterFactory(name="Только для админа") - content = _build_register_excel_bytes( - [ - { - "pn_name": 'АО "Ограниченный доступ"', - "mn_ogrn": "1027600980990", - "mn_inn": "7601000086", - "in_kpp": "760401001", - "mn_okpo": "07506197", - } - ] - ) - upload = SimpleUploadedFile( - "viewer.xlsx", - content, - content_type=( - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - ), - ) - - self.client.force_authenticate(self.user) - response = self.client.post( - reverse("api_v1:registers:register-upload"), - { - "registry": str(registry.id), - "actual_date": "2026-01-01", - "file": upload, - }, - format="multipart", - ) - - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_registers_api_v2_namespace_is_not_registered(self): + with self.assertRaises(NoReverseMatch): + reverse("api_v2:registers:register-upload-rosatom-goz") diff --git a/tests/test_api_inventory_e2e.py b/tests/test_api_inventory_e2e.py index 49fe322..c83a93d 100644 --- a/tests/test_api_inventory_e2e.py +++ b/tests/test_api_inventory_e2e.py @@ -19,7 +19,7 @@ from apps.parsers.models import ( ) from apps.user.services import UserService from django.core.files.uploadedfile import SimpleUploadedFile -from django.urls import reverse +from django.urls import NoReverseMatch, reverse from django.utils import timezone from django_celery_beat.models import PeriodicTask from openpyxl import Workbook @@ -33,7 +33,6 @@ from tests.apps.parsers.factories import ( ManufacturerRecordFactory, ParserLoadLogFactory, ) -from tests.apps.registers.factories import RegisterFactory from tests.apps.user.factories import UserFactory from tests.utils.fixtures import fake @@ -57,26 +56,6 @@ def _build_fns_excel_bytes() -> bytes: return buffer.getvalue() -def _build_register_excel_bytes(rows: list[dict[str, str]]) -> bytes: - workbook = Workbook() - worksheet = workbook.active - worksheet.append(["pn_name", "mn_ogrn", "mn_inn", "in_kpp", "mn_okpo"]) - for row in rows: - worksheet.append( - [ - row["pn_name"], - row["mn_ogrn"], - row["mn_inn"], - row["in_kpp"], - row["mn_okpo"], - ] - ) - buffer = io.BytesIO() - workbook.save(buffer) - workbook.close() - return buffer.getvalue() - - def _extract_results(response_data): if hasattr(response_data, "get"): data = response_data.get("data") @@ -473,10 +452,13 @@ class ParsersApiInventoryE2ETest(AuthenticatedApiMixin, APITestCase): self.assertEqual(sources_statuses.status_code, status.HTTP_200_OK) self.assertEqual(source_detail.status_code, status.HTTP_200_OK) - main_dashboard = self.client.get(reverse("api_v1:stat:main-dashboard")) + with self.assertRaises(NoReverseMatch): + reverse("api_v1:stat:main-dashboard") + + main_dashboard = self.client.get(reverse("api_v1:parsers:dashboard-data")) self.assertEqual(main_dashboard.status_code, status.HTTP_200_OK) - self.assertIn("source_cards", main_dashboard.data["data"]) - self.assertIn("organization_stats", main_dashboard.data["data"]) + self.assertIn("sources", main_dashboard.data["data"]) + self.assertIn("source_counts", main_dashboard.data["data"]) self.authenticate(self.admin) parsing_url = reverse("api_v1:parsing:parsing-settings") @@ -544,64 +526,13 @@ class RegistersApiInventoryE2ETest(AuthenticatedApiMixin, APITestCase): self.user = UserFactory.create_user() self.admin = UserFactory.create_superuser() - def test_registers_endpoints(self): - registry = RegisterFactory(name="Inventory Registry") - rows = [ - { - "pn_name": "Inventory Org", - "mn_ogrn": "1027600980990", - "mn_inn": "7601000086", - "in_kpp": "760401001", - "mn_okpo": "07506197", - } - ] - - self.authenticate(self.admin) - upload = SimpleUploadedFile( - "inventory-registry.xlsx", - _build_register_excel_bytes(rows), - content_type=( - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - ), - ) - upload_response = self.client.post( - reverse("api_v1:registers:register-upload"), - { - "registry": str(registry.id), - "actual_date": date(2026, 1, 1).isoformat(), - "file": upload, - }, - format="multipart", - ) - self.assertEqual(upload_response.status_code, status.HTTP_201_CREATED) - - self.authenticate(self.user) - registries_list = self.client.get(reverse("api_v1:registers:registries-list")) - registries_detail = self.client.get( - reverse("api_v1:registers:registries-detail", args=[registry.id]) - ) - organizations_list = self.client.get( - reverse("api_v1:registers:organizations-list") - ) - organization_id = _extract_results(organizations_list.data)[0]["id"] - organization_detail = self.client.get( - reverse("api_v1:registers:organizations-detail", args=[organization_id]) - ) - registry_organizations = self.client.get( - reverse( - "api_v1:registers:registry-organizations-list", - args=[registry.id], - ) - ) - - for response in ( - registries_list, - registries_detail, - organizations_list, - organization_detail, - registry_organizations, - ): - self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_registers_endpoints_are_removed(self): + with self.assertRaises(NoReverseMatch): + reverse("api_v1:registers:register-upload") + with self.assertRaises(NoReverseMatch): + reverse("api_v1:registers:registries-list") + with self.assertRaises(NoReverseMatch): + reverse("api_v2:registers:register-upload-rosatom-goz") class ExchangeApiInventoryE2ETest(AuthenticatedApiMixin, APITestCase):