diff --git a/src/organizations/resolver.py b/src/organizations/resolver.py index 4fb2f56..3b92ff7 100644 --- a/src/organizations/resolver.py +++ b/src/organizations/resolver.py @@ -85,32 +85,34 @@ class OrganizationDirectoryResolver: @classmethod def resolve(cls, identity: OrganizationIdentity) -> OrganizationResolveResult: # noqa: C901 + directory = cls._directory_queryset() + if identity.rn: - organization = cls._single(Organization.objects.filter(rn=identity.rn)) + organization = cls._single(directory.filter(rn=identity.rn)) if organization is not None: return OrganizationResolveResult("matched", organization) if identity.okpo: - result = cls._resolve_unique(Organization.objects.filter(okpo=identity.okpo)) + result = cls._resolve_unique(directory.filter(okpo=identity.okpo)) if result.status != "unmatched": return result if identity.inn and identity.kpp: result = cls._resolve_unique( - Organization.objects.filter(inn=identity.inn, kpp=identity.kpp), + directory.filter(inn=identity.inn, kpp=identity.kpp), ) if result.status != "unmatched": return result if identity.ogrn and identity.kpp: result = cls._resolve_unique( - Organization.objects.filter(ogrn=identity.ogrn, kpp=identity.kpp), + directory.filter(ogrn=identity.ogrn, kpp=identity.kpp), ) if result.status != "unmatched": return result if identity.ogrip: - result = cls._resolve_unique(Organization.objects.filter(ogrip=identity.ogrip)) + result = cls._resolve_unique(directory.filter(ogrip=identity.ogrip)) if result.status != "unmatched": return result @@ -118,24 +120,28 @@ class OrganizationDirectoryResolver: return cls._resolve_by_inn_ogrn(identity.inn, identity.ogrn) if identity.inn: - result = cls._resolve_unique(Organization.objects.filter(inn=identity.inn)) + result = cls._resolve_unique(directory.filter(inn=identity.inn)) if result.status != "unmatched": return result if identity.ogrn: - result = cls._resolve_unique(Organization.objects.filter(ogrn=identity.ogrn)) + result = cls._resolve_unique(directory.filter(ogrn=identity.ogrn)) if result.status != "unmatched": return result return OrganizationResolveResult("unmatched") + @staticmethod + def _directory_queryset(): + return Organization.objects.filter(directory_imported_at__isnull=False) + @classmethod def _resolve_by_inn_ogrn( cls, inn: str, ogrn: str, ) -> OrganizationResolveResult: - queryset = Organization.objects.filter(inn=inn, ogrn=ogrn) + queryset = cls._directory_queryset().filter(inn=inn, ogrn=ogrn) result = cls._resolve_unique(queryset) if result.status != "ambiguous": return result diff --git a/tests/apps/organizations/test_resolver.py b/tests/apps/organizations/test_resolver.py index 0df8591..5dae493 100644 --- a/tests/apps/organizations/test_resolver.py +++ b/tests/apps/organizations/test_resolver.py @@ -1,6 +1,7 @@ """Tests for organization directory resolver.""" from django.test import TestCase +from django.utils import timezone from organizations.models import Organization from organizations.resolver import OrganizationDirectoryResolver @@ -8,15 +9,20 @@ from organizations.resolver import OrganizationDirectoryResolver class OrganizationDirectoryResolverTest(TestCase): """Checks matching parser rows to the organization directory.""" + @staticmethod + def _directory_organization(**kwargs) -> Organization: + kwargs.setdefault("directory_imported_at", timezone.now()) + return Organization.objects.create(**kwargs) + def test_resolves_by_rn_okpo_and_exact_pairs(self): - by_rn = Organization.objects.create(name="RN", rn=100) - by_okpo = Organization.objects.create(name="OKPO", okpo="001") - by_inn_kpp = Organization.objects.create( + by_rn = self._directory_organization(name="RN", rn=100) + by_okpo = self._directory_organization(name="OKPO", okpo="001") + by_inn_kpp = self._directory_organization( name="INN KPP", inn="7701001001", kpp="770101001", ) - by_ogrn_kpp = Organization.objects.create( + by_ogrn_kpp = self._directory_organization( name="OGRN KPP", ogrn="1027700100001", kpp="770101002", @@ -54,13 +60,13 @@ class OrganizationDirectoryResolverTest(TestCase): ) def test_duplicate_inn_ogrn_uses_single_head(self): - head = Organization.objects.create( + head = self._directory_organization( name="Head", inn="7701001001", ogrn="1027700100001", is_branch=False, ) - Organization.objects.create( + self._directory_organization( name="Branch", inn="7701001001", ogrn="1027700100001", @@ -78,13 +84,13 @@ class OrganizationDirectoryResolverTest(TestCase): self.assertEqual(result.organization, head) def test_duplicate_inn_ogrn_without_single_head_is_ambiguous(self): - Organization.objects.create( + self._directory_organization( name="Head 1", inn="7701001001", ogrn="1027700100001", is_branch=False, ) - Organization.objects.create( + self._directory_organization( name="Head 2", inn="7701001001", ogrn="1027700100001", @@ -100,3 +106,20 @@ class OrganizationDirectoryResolverTest(TestCase): self.assertEqual(result.status, "ambiguous") self.assertIsNone(result.organization) + + def test_ignores_legacy_organization_matches(self): + Organization.objects.create( + name="Legacy", + inn="7701001001", + kpp="770101001", + ) + + result = OrganizationDirectoryResolver.resolve( + OrganizationDirectoryResolver.identity( + inn="7701001001", + kpp="770101001", + ) + ) + + self.assertEqual(result.status, "unmatched") + self.assertIsNone(result.organization) diff --git a/tests/apps/organizations/test_source_backfill.py b/tests/apps/organizations/test_source_backfill.py index 943b3b1..cf1edb9 100644 --- a/tests/apps/organizations/test_source_backfill.py +++ b/tests/apps/organizations/test_source_backfill.py @@ -8,6 +8,7 @@ from apps.parsers.models import ( ) from django.core.management import call_command from django.test import TestCase +from django.utils import timezone from organizations.models import ( DefenseSupplierExtension, FinancialIndicatorsExtension, @@ -28,8 +29,13 @@ from tests.apps.parsers.factories import ( class OrganizationSourceBackfillServiceTest(TestCase): """Checks idempotent migration from legacy source records.""" + @staticmethod + def _directory_organization(**kwargs) -> Organization: + kwargs.setdefault("directory_imported_at", timezone.now()) + return Organization.objects.create(**kwargs) + def test_backfills_inspection_records_into_planned_inspection_extension(self): - organization = Organization.objects.create( + organization = self._directory_organization( name='ООО "Проверка"', inn="7707083801", ogrn="1027700132001", @@ -71,7 +77,7 @@ class OrganizationSourceBackfillServiceTest(TestCase): self.assertEqual(OrganizationSourceRecord.objects.count(), 1) def test_backfill_payload_serializes_registry_organization_fk_as_id(self): - organization = Organization.objects.create( + organization = self._directory_organization( name='ООО "FK Payload"', inn="7707083815", ogrn="1027700132015", @@ -94,7 +100,7 @@ class OrganizationSourceBackfillServiceTest(TestCase): self.assertEqual(record.payload["registry_organization"], str(organization.pk)) def test_backfills_financial_report_lines(self): - organization = Organization.objects.create( + organization = self._directory_organization( name='ООО "Финансы"', inn="7707083802", ogrn="1027700132002", @@ -132,7 +138,7 @@ class OrganizationSourceBackfillServiceTest(TestCase): self.assertEqual(record.financial_lines.get().period_end, 200) def test_backfills_generic_defense_supplier_records(self): - organization = Organization.objects.create( + organization = self._directory_organization( name='ООО "ГОЗ"', inn="7707083803", ) @@ -189,7 +195,7 @@ class OrganizationSourceBackfillServiceTest(TestCase): ) def test_management_command_runs_source_backfill(self): - organization = Organization.objects.create( + organization = self._directory_organization( name='ООО "Команда"', inn="7707083804", ogrn="1027700132004", diff --git a/tests/apps/organizations/test_source_ingestion.py b/tests/apps/organizations/test_source_ingestion.py index a8e9e04..d343360 100644 --- a/tests/apps/organizations/test_source_ingestion.py +++ b/tests/apps/organizations/test_source_ingestion.py @@ -4,6 +4,7 @@ from decimal import Decimal from apps.parsers.models import FinancialReport, GenericParserRecord, ParserLoadLog from django.test import TestCase +from django.utils import timezone from organizations.models import ( DefenseSupplierExtension, FinancialIndicatorsExtension, @@ -22,8 +23,13 @@ from organizations.source_ingestion import ( class OrganizationSourceIngestionServiceTest(TestCase): """Checks runtime parser writes bypass legacy parser record tables.""" + @staticmethod + def _directory_organization(**kwargs) -> Organization: + kwargs.setdefault("directory_imported_at", timezone.now()) + return Organization.objects.create(**kwargs) + def test_save_generic_records_writes_source_records_without_legacy_rows(self): - Organization.objects.create( + self._directory_organization( name='ООО "ГОЗ"', inn="7707083803", ) @@ -70,7 +76,7 @@ class OrganizationSourceIngestionServiceTest(TestCase): self.assertEqual(record.legacy_pk, "") def test_save_records_is_idempotent_by_source_external_id(self): - Organization.objects.create( + self._directory_organization( name='ООО "Идемпотентность"', inn="7707083810", ) @@ -140,7 +146,7 @@ class OrganizationSourceIngestionServiceTest(TestCase): self.assertFalse(name_only.source_extensions.exists()) def test_save_records_does_not_fill_missing_identity_on_resolved_organization(self): - partial = Organization.objects.create( + partial = self._directory_organization( name="Acme Security", inn="7713497980", ogrn="1237700253306", @@ -168,14 +174,14 @@ class OrganizationSourceIngestionServiceTest(TestCase): self.assertEqual(record.extension.organization, partial) def test_save_records_uses_single_head_organization_for_duplicate_inn_ogrn(self): - head = Organization.objects.create( + head = self._directory_organization( name="Head", inn="7713497980", ogrn="1237700253306", filial=".F.", is_branch=False, ) - Organization.objects.create( + self._directory_organization( name="Branch", inn="7713497980", ogrn="1237700253306", @@ -202,14 +208,14 @@ class OrganizationSourceIngestionServiceTest(TestCase): self.assertEqual(record.extension.organization, head) def test_save_records_skips_ambiguous_duplicate_inn_ogrn(self): - Organization.objects.create( + self._directory_organization( name="Head 1", inn="7713497980", ogrn="1237700253306", filial=".F.", is_branch=False, ) - Organization.objects.create( + self._directory_organization( name="Head 2", inn="7713497980", ogrn="1237700253306", @@ -259,7 +265,7 @@ class OrganizationSourceIngestionServiceTest(TestCase): self.assertFalse(Organization.objects.filter(inn="7720525156").exists()) def test_save_records_does_not_add_ogrip_to_existing_legal_entity(self): - organization = Organization.objects.create( + organization = self._directory_organization( name='ООО "Металл-Завод"', inn="7720525156", ogrn="1057746645395", @@ -286,7 +292,7 @@ class OrganizationSourceIngestionServiceTest(TestCase): self.assertEqual(organization.ogrip, "") def test_save_financial_report_writes_financial_lines_without_legacy_report(self): - Organization.objects.create( + self._directory_organization( name='ООО "Финансовый отчет"', ogrn="1027700132002", ) diff --git a/tests/apps/organizations/test_tasks.py b/tests/apps/organizations/test_tasks.py index fee7a8e..37f6d62 100644 --- a/tests/apps/organizations/test_tasks.py +++ b/tests/apps/organizations/test_tasks.py @@ -6,6 +6,7 @@ from apps.parsers.models import ParserLoadLog from django.apps import apps as django_apps from django.core.cache import cache from django.test import TestCase +from django.utils import timezone from django_celery_beat.models import PeriodicTask from organizations.cache import get_organization_api_cache_version from organizations.models import ( @@ -24,8 +25,13 @@ from tests.apps.parsers.factories import IndustrialCertificateRecordFactory class OrganizationSourceBackfillTasksTest(TestCase): """Checks Celery tasks that maintain API v2 organization source extensions.""" + @staticmethod + def _directory_organization(**kwargs) -> Organization: + kwargs.setdefault("directory_imported_at", timezone.now()) + return Organization.objects.create(**kwargs) + def test_backfill_all_task_rebuilds_sources_and_invalidates_api_cache(self): - organization = Organization.objects.create( + organization = self._directory_organization( name='ООО "Источник"', inn="7800000401", ogrn="1027700144401", @@ -57,7 +63,7 @@ class OrganizationSourceBackfillTasksTest(TestCase): ) def test_backfill_parser_batch_task_limits_source_and_batch(self): - organization = Organization.objects.create( + organization = self._directory_organization( name='ООО "Пакет источника"', inn="7800000402", ogrn="1027700144402", diff --git a/tests/apps/parsers/organization_helpers.py b/tests/apps/parsers/organization_helpers.py new file mode 100644 index 0000000..1bf33a4 --- /dev/null +++ b/tests/apps/parsers/organization_helpers.py @@ -0,0 +1,30 @@ +"""Organization helpers for parser tests.""" + +from __future__ import annotations + +from django.utils import timezone +from organizations.models import Organization + + +def create_directory_organization(**kwargs) -> Organization: + """Create an organization row that belongs to the authoritative directory.""" + kwargs.setdefault("directory_imported_at", timezone.now()) + return Organization.objects.create(**kwargs) + + +def get_or_create_directory_organization( + *, + defaults: dict | None = None, + **lookup, +) -> tuple[Organization, bool]: + """Get or create an authoritative directory organization for ingestion tests.""" + directory_defaults = dict(defaults or {}) + directory_defaults.setdefault("directory_imported_at", timezone.now()) + organization, created = Organization.objects.get_or_create( + defaults=directory_defaults, + **lookup, + ) + if not created and organization.directory_imported_at is None: + organization.directory_imported_at = timezone.now() + organization.save(update_fields=["directory_imported_at"]) + return organization, created diff --git a/tests/apps/parsers/test_admin.py b/tests/apps/parsers/test_admin.py index 3f7495e..2c55bb1 100644 --- a/tests/apps/parsers/test_admin.py +++ b/tests/apps/parsers/test_admin.py @@ -45,6 +45,7 @@ from tests.apps.parsers.factories import ( ParserLoadLogFactory, ProxyFactory, ) +from tests.apps.parsers.organization_helpers import create_directory_organization from tests.apps.user.factories import UserFactory from tests.utils.fixtures import fake @@ -80,7 +81,7 @@ def _build_fns_zip_upload() -> SimpleUploadedFile: buffer = io.BytesIO() with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED) as archive: ogrn = _digits(13) - Organization.objects.create( + create_directory_organization( name=f"ФНС {ogrn}", ogrn=ogrn, ) @@ -98,7 +99,7 @@ def _build_fns_zip_upload() -> SimpleUploadedFile: def _build_fns_excel_upload() -> SimpleUploadedFile: ogrn = _digits(13) - Organization.objects.create( + create_directory_organization( name=f"ФНС {ogrn}", ogrn=ogrn, ) diff --git a/tests/apps/parsers/test_direct_ingestion_services.py b/tests/apps/parsers/test_direct_ingestion_services.py index 6d20b19..3202653 100644 --- a/tests/apps/parsers/test_direct_ingestion_services.py +++ b/tests/apps/parsers/test_direct_ingestion_services.py @@ -37,6 +37,8 @@ from organizations.models import ( OrganizationSourceRecord, ) +from tests.apps.parsers.organization_helpers import create_directory_organization + class DirectIngestionParserServicesTest(TestCase): """Parser save services should not write legacy parser record rows.""" @@ -52,7 +54,7 @@ class DirectIngestionParserServicesTest(TestCase): ("7707083809", "1027700132009"), ("7707083810", "1027700132010"), ): - Organization.objects.create( + create_directory_organization( name=f"Справочник {inn or ogrn}", inn=inn, ogrn=ogrn, diff --git a/tests/apps/parsers/test_e2e.py b/tests/apps/parsers/test_e2e.py index 23a2828..f0ea1b3 100644 --- a/tests/apps/parsers/test_e2e.py +++ b/tests/apps/parsers/test_e2e.py @@ -14,6 +14,7 @@ from apps.parsers.services import ParserLoadLogService, ProcurementService from django.test import TestCase, override_settings from organizations.models import Organization, OrganizationSourceRecord +from tests.apps.parsers.organization_helpers import create_directory_organization from tests.utils import TestHTTPServer from tests.utils.fixtures import build_zakupki_xml, build_zip, fake @@ -44,7 +45,7 @@ def _add_zakupki_zip( ) -> int: xml_bytes, rows = build_zakupki_xml(count=count) for row in rows: - Organization.objects.create( + create_directory_organization( name=row.customer_name, inn=row.customer_inn, kpp=row.customer_kpp, diff --git a/tests/apps/parsers/test_fns_parser.py b/tests/apps/parsers/test_fns_parser.py index 9ea0ce8..b159ba1 100644 --- a/tests/apps/parsers/test_fns_parser.py +++ b/tests/apps/parsers/test_fns_parser.py @@ -12,6 +12,7 @@ from apps.parsers.services import ( from django.test import TestCase from organizations.models import Organization +from tests.apps.parsers.organization_helpers import create_directory_organization from tests.utils.fixtures import fake @@ -32,7 +33,7 @@ def _form_code() -> str: def _create_registry_organization(*, inn: str, ogrn: str) -> Organization: - return Organization.objects.create( + return create_directory_organization( name=fake.company(), ogrn=ogrn, inn=inn, diff --git a/tests/apps/parsers/test_fns_upload.py b/tests/apps/parsers/test_fns_upload.py index 9d18a30..0298acd 100644 --- a/tests/apps/parsers/test_fns_upload.py +++ b/tests/apps/parsers/test_fns_upload.py @@ -24,6 +24,7 @@ from rest_framework import status from rest_framework.test import APITestCase from tests.apps.user.factories import UserFactory +from tests.apps.parsers.organization_helpers import create_directory_organization from tests.utils.fixtures import fake @@ -55,7 +56,7 @@ def _build_fns_zip_bytes(file_map: dict[str, bytes]) -> bytes: def _ensure_directory_organization(*, ogrn: str) -> Organization: - return Organization.objects.create( + return create_directory_organization( name=f"ФНС {ogrn}", ogrn=ogrn, ) diff --git a/tests/apps/parsers/test_procurement_service.py b/tests/apps/parsers/test_procurement_service.py index 38c6678..c045a37 100644 --- a/tests/apps/parsers/test_procurement_service.py +++ b/tests/apps/parsers/test_procurement_service.py @@ -11,6 +11,7 @@ from django.test import TestCase from organizations.models import Organization, OrganizationSourceRecord from tests.apps.parsers.factories import ProcurementRecordFactory, fake +from tests.apps.parsers.organization_helpers import create_directory_organization def _digits(length: int) -> str: @@ -35,7 +36,7 @@ def _other_law(law_type: str) -> str: def _create_registry_organization(*, inn: str, ogrn: str) -> Organization: - return Organization.objects.create( + return create_directory_organization( name=fake.company(), ogrn=ogrn, inn=inn, @@ -47,7 +48,7 @@ def _create_registry_organization(*, inn: str, ogrn: str) -> Organization: def _ensure_directory_organization_for_procurement( procurement: Procurement, ) -> Organization: - return Organization.objects.create( + return create_directory_organization( name=procurement.customer_name, ogrn=procurement.customer_ogrn, inn=procurement.customer_inn, diff --git a/tests/apps/parsers/test_service_helpers.py b/tests/apps/parsers/test_service_helpers.py index 4002d22..667625f 100644 --- a/tests/apps/parsers/test_service_helpers.py +++ b/tests/apps/parsers/test_service_helpers.py @@ -25,6 +25,7 @@ from django.test import TestCase from organizations.models import Organization from tests.apps.parsers.factories import ParserLoadLogFactory +from tests.apps.parsers.organization_helpers import create_directory_organization class NormalizeHelpersTest(TestCase): @@ -67,12 +68,12 @@ class RegistryOrganizationResolverTest(TestCase): self.assertEqual(lookup.by_ogrn, {}) def test_resolve_organization_id_by_unique_inn_and_ogrn(self): - org_by_inn = Organization.objects.create( + org_by_inn = create_directory_organization( name="By INN", inn="7701001001", ogrn="1027700100001", ) - org_by_ogrn = Organization.objects.create( + org_by_ogrn = create_directory_organization( name="By OGRN", inn="7701001002", ogrn="1027700100002", @@ -146,12 +147,12 @@ class ParserLoadLogServiceRetryTest(TestCase): class SmallParserServiceQueryTest(TestCase): def test_industrial_product_service_query_helpers(self): - Organization.objects.create( + create_directory_organization( name='ООО "Продукт 1"', inn="7701001001", ogrn="1027700100001", ) - Organization.objects.create( + create_directory_organization( name='ООО "Продукт 2"', inn="7701001001", ogrn="1027700100002", @@ -201,12 +202,12 @@ class SmallParserServiceQueryTest(TestCase): ) def test_inspection_service_has_data_for_period(self): - Organization.objects.create( + create_directory_organization( name='ООО "Проверка 1"', inn="7701002001", ogrn="1027700200001", ) - Organization.objects.create( + create_directory_organization( name='ООО "Проверка 2"', inn="7701002002", ogrn="1027700200002", @@ -263,13 +264,13 @@ class SmallParserServiceQueryTest(TestCase): self.assertTrue(InspectionService.has_data_for_period(2026, 4, True)) def test_procurement_service_find_by_customer_name_with_batch(self): - Organization.objects.create( + create_directory_organization( name="АО Тестовый заказчик 1", inn="7701003001", kpp="770101001", ogrn="1027700300001", ) - Organization.objects.create( + create_directory_organization( name="АО Тестовый заказчик 2", inn="7701003002", kpp="770101002", @@ -332,7 +333,7 @@ class SmallParserServiceQueryTest(TestCase): class FNSReportServiceHelpersTest(TestCase): def test_exists_find_and_status_helpers(self): - Organization.objects.create( + create_directory_organization( name='ООО "ФНС"', ogrn="1027700111111", ) diff --git a/tests/apps/parsers/test_services.py b/tests/apps/parsers/test_services.py index 73d41f1..f4b021b 100644 --- a/tests/apps/parsers/test_services.py +++ b/tests/apps/parsers/test_services.py @@ -50,6 +50,7 @@ from .factories import ( ParserLoadLogFactory, ProxyFactory, ) +from .organization_helpers import create_directory_organization def _digits(length: int) -> str: @@ -61,7 +62,7 @@ def _proxy_address() -> str: def _create_registry_organization(*, inn: str, ogrn: str) -> Organization: - return Organization.objects.create( + return create_directory_organization( name=fake.company(), ogrn=ogrn, inn=inn, diff --git a/tests/apps/parsers/test_source_cards_service.py b/tests/apps/parsers/test_source_cards_service.py index f1d00f2..55d21eb 100644 --- a/tests/apps/parsers/test_source_cards_service.py +++ b/tests/apps/parsers/test_source_cards_service.py @@ -25,6 +25,11 @@ from organizations.source_ingestion import ( ) from rest_framework.exceptions import ValidationError +from tests.apps.parsers.organization_helpers import ( + create_directory_organization, + get_or_create_directory_organization, +) + def _save_source_record( *, @@ -38,7 +43,7 @@ def _save_source_record( payload = dict(payload or {}) if inn: if not Organization.objects.filter(inn=inn).exists(): - Organization.objects.create( + create_directory_organization( name=organization_name or title or external_id, inn=inn, ) @@ -46,7 +51,7 @@ def _save_source_record( rn_key = organization_name or title or external_id rn = str(int(hashlib.sha256(rn_key.encode()).hexdigest()[:12], 16) % 10**12) payload.setdefault("rn", rn) - Organization.objects.get_or_create( + get_or_create_directory_organization( rn=rn, defaults={"name": organization_name or title or external_id}, ) diff --git a/tests/apps/parsers/test_source_cards_views.py b/tests/apps/parsers/test_source_cards_views.py index b2d2362..da040fa 100644 --- a/tests/apps/parsers/test_source_cards_views.py +++ b/tests/apps/parsers/test_source_cards_views.py @@ -20,6 +20,10 @@ from rest_framework import status from rest_framework.test import APITestCase from tests.apps.parsers.factories import ParserLoadLogFactory +from tests.apps.parsers.organization_helpers import ( + create_directory_organization, + get_or_create_directory_organization, +) from tests.apps.user.factories import UserFactory from tests.utils.fixtures import fake @@ -45,7 +49,7 @@ def _save_source_record( if ogrn: query["ogrn"] = ogrn if not Organization.objects.filter(**query).exists(): - Organization.objects.create( + create_directory_organization( name=organization_name or title or external_id, inn=inn, ogrn=ogrn, @@ -54,7 +58,7 @@ def _save_source_record( rn_key = organization_name or title or external_id rn = str(int(hashlib.sha256(rn_key.encode()).hexdigest()[:12], 16) % 10**12) payload["rn"] = rn - Organization.objects.get_or_create( + get_or_create_directory_organization( rn=rn, defaults={"name": organization_name or title or external_id}, ) diff --git a/tests/apps/parsers/test_sources_api_e2e.py b/tests/apps/parsers/test_sources_api_e2e.py index 95a5393..c873d30 100644 --- a/tests/apps/parsers/test_sources_api_e2e.py +++ b/tests/apps/parsers/test_sources_api_e2e.py @@ -19,6 +19,7 @@ from tests.apps.parsers.factories import ( ParserLoadLogFactory, ProcurementRecordFactory, ) +from tests.apps.parsers.organization_helpers import create_directory_organization from tests.apps.user.factories import UserFactory from tests.utils.fixtures import fake @@ -36,12 +37,12 @@ class SourcesApiE2ETest(APITestCase): shared_inn = _digits(10) shared_ogrn = _digits(13) report_ogrn = _digits(13) - Organization.objects.create( + create_directory_organization( name="Shared Organization", inn=shared_inn, ogrn=shared_ogrn, ) - Organization.objects.create( + create_directory_organization( name="Financial Organization", ogrn=report_ogrn, ) diff --git a/tests/apps/parsers/test_tasks.py b/tests/apps/parsers/test_tasks.py index 17419cb..9c773f5 100644 --- a/tests/apps/parsers/test_tasks.py +++ b/tests/apps/parsers/test_tasks.py @@ -70,6 +70,7 @@ from tests.apps.parsers.factories import ( ProcurementRecordFactory, ProxyFactory, ) +from tests.apps.parsers.organization_helpers import create_directory_organization from tests.utils import TestHTTPServer from tests.utils.fixtures import ( build_minpromtorg_certificates_excel, @@ -96,7 +97,7 @@ def _digits(length: int) -> str: def OrganizationFactory(**kwargs) -> Organization: kwargs.setdefault("name", kwargs.get("pn_name") or fake.company()) kwargs.setdefault("okpo", kwargs.get("mn_okpo") or _digits(8)) - return Organization.objects.create(**kwargs) + return create_directory_organization(**kwargs) def RegistryMembershipPeriodFactory( @@ -135,7 +136,7 @@ def _ensure_directory_organization( kpp: str = "", name: str = "", ) -> Organization: - return Organization.objects.create( + return create_directory_organization( name=name or f"Справочник {inn or ogrn}", inn=str(inn or ""), ogrn=str(ogrn or ""), @@ -312,7 +313,7 @@ class GenericSourceFetchTestCase(TestCase): @override_settings(CHECKO_API_KEY="") def test_fstec_enriches_identity_from_local_registry_name_match(self): - Organization.objects.create( + create_directory_organization( pn_name='ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ВЕБЛОК"', mn_ogrn=1237700253306, mn_inn=7713497980, @@ -348,7 +349,7 @@ class GenericSourceFetchTestCase(TestCase): @override_settings(CHECKO_API_KEY="") def test_fstec_restores_leading_zero_for_local_registry_inn(self): - Organization.objects.create( + create_directory_organization( pn_name='ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "БАШНЕФТЬ-ДОБЫЧА"', mn_ogrn=1090280032699, mn_inn=277106840, @@ -487,21 +488,21 @@ class GenericSourceFetchTestCase(TestCase): @override_settings(CHECKO_API_KEY="test-key", FEDRESURS_CHECKO_FALLBACK_LIMIT=10) def test_fedresurs_falls_back_to_checko_for_active_registry_organizations(self): - organization = Organization.objects.create( + organization = create_directory_organization( pn_name='ООО "Тест"', mn_ogrn=1027700000000, mn_inn=7701000001, in_kpp=770101001, mn_okpo="12345678", ) - inactive = Organization.objects.create( + inactive = create_directory_organization( pn_name='ООО "Бывший реестр"', mn_ogrn=1027700000098, mn_inn=7701000098, in_kpp=770101001, mn_okpo="12345678", ) - no_membership = Organization.objects.create( + no_membership = create_directory_organization( pn_name='ООО "Без реестра"', mn_ogrn=1027700000099, mn_inn=7701000099, @@ -610,14 +611,14 @@ class GenericSourceFetchTestCase(TestCase): def test_arbitration_fetches_checko_legal_cases_for_active_registry_organizations( self, ): - organization = Organization.objects.create( + organization = create_directory_organization( pn_name='ООО "Арбитраж"', mn_ogrn=1027700000001, mn_inn=7701000002, in_kpp=770101001, mn_okpo="12345678", ) - inactive = Organization.objects.create( + inactive = create_directory_organization( pn_name='ООО "Бывший арбитраж"', mn_ogrn=1027700000088, mn_inn=7701000088, @@ -743,14 +744,14 @@ class GenericSourceFetchTestCase(TestCase): def test_registry_inspections_fetches_checko_for_active_registry_organizations( self, ): - organization = Organization.objects.create( + organization = create_directory_organization( pn_name='ООО "Проверки"', mn_ogrn=1027700000004, mn_inn=7701000004, in_kpp=770101001, mn_okpo="12345678", ) - inactive = Organization.objects.create( + inactive = create_directory_organization( pn_name='ООО "Старые проверки"', mn_ogrn=1027700000005, mn_inn=7701000005, @@ -822,14 +823,14 @@ class GenericSourceFetchTestCase(TestCase): def test_registry_contracts_fetches_checko_for_active_registry_organizations( self, ): - organization = Organization.objects.create( + organization = create_directory_organization( pn_name='ООО "Контракты"', mn_ogrn=1027700000006, mn_inn=7701000006, in_kpp=770101001, mn_okpo="12345678", ) - inactive = Organization.objects.create( + inactive = create_directory_organization( pn_name='ООО "Старые контракты"', mn_ogrn=1027700000007, mn_inn=7701000007, @@ -2109,7 +2110,7 @@ class FNSFileTasksTestCase(TestCase): content = _build_fns_excel_bytes() external_id = _digits(5) ogrn = _digits(13) - Organization.objects.create(name=f"ФНС {ogrn}", ogrn=ogrn) + create_directory_organization(name=f"ФНС {ogrn}", ogrn=ogrn) filename = f"fin_{external_id}_{ogrn}.xlsx" file_path = os.path.join(watch_dir, filename) with open(file_path, "wb") as handle: @@ -2344,7 +2345,7 @@ class FNSFileTasksTestCase(TestCase): os.makedirs(watch_dir, exist_ok=True) file_path = self._write_fns_file(watch_dir) ogrn = self._ogrn_from_fns_file_path(file_path) - Organization.objects.create(name=f"ФНС филиал {ogrn}", ogrn=ogrn) + create_directory_organization(name=f"ФНС филиал {ogrn}", ogrn=ogrn) with override_settings( FNS_WATCH_DIRECTORY=watch_dir, diff --git a/tests/apps/parsers/test_views.py b/tests/apps/parsers/test_views.py index df67c31..6b8c53e 100644 --- a/tests/apps/parsers/test_views.py +++ b/tests/apps/parsers/test_views.py @@ -35,6 +35,10 @@ from tests.apps.parsers.factories import ( ManufacturerRecordFactory, ParserLoadLogFactory, ) +from tests.apps.parsers.organization_helpers import ( + create_directory_organization, + get_or_create_directory_organization, +) from tests.apps.user.factories import UserFactory from tests.utils.fixtures import fake @@ -46,7 +50,7 @@ def _digits(length: int) -> str: def RegisterOrganizationFactory(**kwargs) -> Organization: kwargs.setdefault("name", kwargs.get("pn_name") or fake.company()) kwargs.setdefault("okpo", kwargs.get("mn_okpo") or _digits(8)) - return Organization.objects.create(**kwargs) + return create_directory_organization(**kwargs) def RegistryMembershipPeriodFactory( @@ -113,7 +117,7 @@ def _save_source_record( if ogrn: query["ogrn"] = ogrn if not Organization.objects.filter(**query).exists(): - Organization.objects.create( + create_directory_organization( name=organization_name, inn=inn, ogrn=ogrn, @@ -122,7 +126,7 @@ def _save_source_record( rn_key = organization_name or title or external_id rn = str(int(hashlib.sha256(rn_key.encode()).hexdigest()[:12], 16) % 10**12) payload.setdefault("rn", rn) - Organization.objects.get_or_create( + get_or_create_directory_organization( rn=rn, defaults={"name": organization_name}, ) @@ -281,7 +285,7 @@ class ParsersViewSetTest(APITestCase): def test_procurements_v1_enriches_missing_customer_fields_from_canonical_organization( self, ): - organization = Organization.objects.create( + organization = create_directory_organization( name='ООО "КАНОНИЧЕСКИЙ ЗАКАЗЧИК"', inn="7701000101", kpp="770101001", @@ -323,13 +327,13 @@ class ParsersViewSetTest(APITestCase): self.assertEqual(detail_row["customer_ogrn"], organization.ogrn) def test_procurements_v1_uses_customer_kpp_for_branch_enrichment(self): - head = Organization.objects.create( + head = create_directory_organization( name='ООО "КАНОНИЧЕСКИЙ ЗАКАЗЧИК"', inn="7701000101", kpp="770101001", ogrn="1027700000001", ) - branch = Organization.objects.create( + branch = create_directory_organization( name='ООО "КАНОНИЧЕСКИЙ ЗАКАЗЧИК" ФИЛИАЛ', inn=head.inn, kpp="780101001", @@ -361,13 +365,13 @@ class ParsersViewSetTest(APITestCase): self.assertEqual(response.data["customer_kpp"], branch.kpp) def test_procurements_v1_does_not_guess_branch_when_customer_kpp_is_missing(self): - head = Organization.objects.create( + head = create_directory_organization( name='ООО "КАНОНИЧЕСКИЙ ЗАКАЗЧИК"', inn="7701000101", kpp="770101001", ogrn="1027700000001", ) - Organization.objects.create( + create_directory_organization( name='ООО "КАНОНИЧЕСКИЙ ЗАКАЗЧИК" ФИЛИАЛ', inn=head.inn, kpp="780101001", @@ -479,7 +483,7 @@ class ParsersViewSetTest(APITestCase): def test_parser_results_v1_enrich_missing_organization_fields_without_contract_change( self, ): - organization = Organization.objects.create( + organization = create_directory_organization( name='ООО "КАНОНИЧЕСКИЙ ПОСТАВЩИК"', inn="7701000102", kpp="770102001", @@ -1199,7 +1203,7 @@ class ParsersViewSetTest(APITestCase): failed_dir = os.path.join(tmpdir, "failed") content = _build_fns_excel_bytes() ogrn = _digits(13) - Organization.objects.create(name=f"ФНС {ogrn}", ogrn=ogrn) + create_directory_organization(name=f"ФНС {ogrn}", ogrn=ogrn) upload = SimpleUploadedFile( f"fin_{_digits(5)}_{ogrn}.xlsx", content, @@ -1226,7 +1230,7 @@ class ParsersViewSetTest(APITestCase): failed_dir = os.path.join(tmpdir, "failed") content = _build_fns_excel_bytes() ogrn = _digits(13) - Organization.objects.create(name=f"ФНС {ogrn}", ogrn=ogrn) + create_directory_organization(name=f"ФНС {ogrn}", ogrn=ogrn) upload = SimpleUploadedFile( f"fin_{_digits(5)}_{ogrn}.xlsx", content, @@ -1250,7 +1254,7 @@ class ParsersViewSetTest(APITestCase): self.client.force_authenticate(self.admin) external_id = _digits(5) ogrn = _digits(13) - Organization.objects.create(name=f"ФНС {ogrn}", ogrn=ogrn) + create_directory_organization(name=f"ФНС {ogrn}", ogrn=ogrn) filename = f"fin_{external_id}_{ogrn}.xlsx" upload = SimpleUploadedFile( "fns_reports.zip",