diff --git a/docker/scripts/start-web.sh b/docker/scripts/start-web.sh index ef5599d..5b32530 100755 --- a/docker/scripts/start-web.sh +++ b/docker/scripts/start-web.sh @@ -12,6 +12,6 @@ esac exec gunicorn core.wsgi:application \ --bind "0.0.0.0:${PORT:-8000}" \ --workers "${GUNICORN_WORKERS:-3}" \ - --timeout "${GUNICORN_TIMEOUT:-60}" \ + --timeout "${GUNICORN_TIMEOUT:-300}" \ --access-logfile "-" \ --error-logfile "-" diff --git a/src/apps/core/upload_contracts.py b/src/apps/core/upload_contracts.py index eb0e669..933d783 100644 --- a/src/apps/core/upload_contracts.py +++ b/src/apps/core/upload_contracts.py @@ -82,7 +82,9 @@ def build_upload_success_payload( report_quarter=report_quarter, ) else: - payload["report_period_display"] = report_annual_display(report_year=report_year) + payload["report_period_display"] = report_annual_display( + report_year=report_year + ) if result is not None: payload["result"] = result diff --git a/src/apps/exchange/admin.py b/src/apps/exchange/admin.py index 3915776..7053d86 100644 --- a/src/apps/exchange/admin.py +++ b/src/apps/exchange/admin.py @@ -163,6 +163,12 @@ class ExchangePackageImportAdmin(admin.ModelAdmin): prosecutor_checks = result.get("prosecutor_checks", {}) public_procurements = result.get("public_procurements", {}) arbitration_cases = result.get("arbitration_cases", {}) + bankruptcy_procedures = result.get("bankruptcy_procedures", {}) + defense_unreliable_suppliers = result.get("defense_unreliable_suppliers", {}) + information_security_registries = result.get( + "information_security_registries", {} + ) + labor_vacancies = result.get("labor_vacancies", {}) self.message_user( request, ( @@ -172,7 +178,11 @@ class ExchangePackageImportAdmin(admin.ModelAdmin): f"продукция {industrial_products.get('created', 0)}, " f"проверки {prosecutor_checks.get('created', 0)}, " f"закупки {public_procurements.get('created', 0)}, " - f"арбитраж {arbitration_cases.get('created', 0)}." + f"арбитраж {arbitration_cases.get('created', 0)}, " + f"банкротства {bankruptcy_procedures.get('created', 0)}, " + f"РНП/ГОЗ {defense_unreliable_suppliers.get('created', 0)}, " + f"ИБ-реестры {information_security_registries.get('created', 0)}, " + f"вакансии {labor_vacancies.get('created', 0)}." ), level=messages.SUCCESS, ) diff --git a/src/apps/exchange/services.py b/src/apps/exchange/services.py index 6c5b203..8089523 100644 --- a/src/apps/exchange/services.py +++ b/src/apps/exchange/services.py @@ -21,7 +21,11 @@ from apps.exchange.models import ( ) from apps.external_data.models import ( ArbitrationCase, + BankruptcyProcedure, + DefenseUnreliableSupplier, IndustrialProduct, + InformationSecurityRegistryEntry, + LaborVacancy, ProsecutorCheck, PublicProcurement, ) @@ -93,6 +97,10 @@ class ExchangePackageImportService: "prosecutor_checks", "public_procurements", "arbitration_cases", + "bankruptcy_procedures", + "defense_unreliable_suppliers", + "information_security_registries", + "labor_vacancies", ) @classmethod @@ -458,6 +466,18 @@ class ExchangePackageImportService: arbitration_summary = cls._upsert_arbitration_cases( cls._extract_rows(data, "arbitration_cases"), ) + bankruptcy_summary = cls._upsert_bankruptcy_procedures( + cls._extract_rows(data, "bankruptcy_procedures"), + ) + defense_supplier_summary = cls._upsert_defense_unreliable_suppliers( + cls._extract_rows(data, "defense_unreliable_suppliers"), + ) + information_security_summary = cls._upsert_information_security_registries( + cls._extract_rows(data, "information_security_registries"), + ) + labor_vacancy_summary = cls._upsert_labor_vacancies( + cls._extract_rows(data, "labor_vacancies"), + ) return { "organizations": organization_summary, @@ -465,6 +485,10 @@ class ExchangePackageImportService: "prosecutor_checks": prosecutor_summary, "public_procurements": procurement_summary, "arbitration_cases": arbitration_summary, + "bankruptcy_procedures": bankruptcy_summary, + "defense_unreliable_suppliers": defense_supplier_summary, + "information_security_registries": information_security_summary, + "labor_vacancies": labor_vacancy_summary, } @classmethod @@ -798,6 +822,209 @@ class ExchangePackageImportService: "skipped": skipped_count, } + @classmethod + def _upsert_bankruptcy_procedures( + cls, + rows: list[dict[str, Any]], + ) -> dict[str, int]: + created_count = 0 + updated_count = 0 + skipped_count = 0 + + for row in rows: + organization = cls._resolve_organization(row) + external_id = cls._clean_string(row.get("external_id")) + if not external_id: + skipped_count += 1 + continue + + defaults = { + "message_type": cls._clean_string(row.get("message_type")), + "message_date": cls._parse_date_value( + row.get("message_date"), + field_name="message_date", + allow_null=True, + ), + "case_number": cls._clean_string(row.get("case_number")), + "status": cls._clean_string(row.get("status")), + "source_url": cls._clean_string(row.get("source_url")), + } + state = cls._upsert_external_row( + model=BankruptcyProcedure, + lookup={ + "organization": organization, + "external_id": external_id, + }, + defaults=defaults, + ) + if state == "created": + created_count += 1 + elif state == "updated": + updated_count += 1 + + return { + "created": created_count, + "updated": updated_count, + "skipped": skipped_count, + } + + @classmethod + def _upsert_defense_unreliable_suppliers( + cls, + rows: list[dict[str, Any]], + ) -> dict[str, int]: + created_count = 0 + updated_count = 0 + skipped_count = 0 + + for row in rows: + organization = cls._resolve_organization(row) + external_id = cls._clean_string(row.get("external_id")) + if not external_id: + skipped_count += 1 + continue + + defaults = { + "registry_source": cls._clean_string(row.get("registry_source")), + "registry_number": cls._clean_string(row.get("registry_number")), + "supplier_name": cls._clean_string(row.get("supplier_name")), + "reason": cls._clean_string(row.get("reason")), + "included_at": cls._parse_date_value( + row.get("included_at"), + field_name="included_at", + allow_null=True, + ), + "status": cls._clean_string(row.get("status")), + "source_url": cls._clean_string(row.get("source_url")), + } + state = cls._upsert_external_row( + model=DefenseUnreliableSupplier, + lookup={ + "organization": organization, + "external_id": external_id, + }, + defaults=defaults, + ) + if state == "created": + created_count += 1 + elif state == "updated": + updated_count += 1 + + return { + "created": created_count, + "updated": updated_count, + "skipped": skipped_count, + } + + @classmethod + def _upsert_information_security_registries( + cls, + rows: list[dict[str, Any]], + ) -> dict[str, int]: + created_count = 0 + updated_count = 0 + skipped_count = 0 + + for row in rows: + organization = cls._resolve_organization(row) + external_id = cls._clean_string(row.get("external_id")) + registry_name = cls._clean_string(row.get("registry_name")) + entry_number = cls._clean_string(row.get("entry_number")) + if not external_id and not (registry_name and entry_number): + skipped_count += 1 + continue + + lookup = {"organization": organization} + if external_id: + lookup["external_id"] = external_id + else: + lookup.update( + { + "registry_name": registry_name, + "entry_number": entry_number, + } + ) + + defaults = { + "registry_name": registry_name, + "presence_status": cls._clean_string(row.get("presence_status")) + or InformationSecurityRegistryEntry.PresenceStatus.PRESENT, + "entry_number": entry_number, + "issued_at": cls._parse_date_value( + row.get("issued_at"), + field_name="issued_at", + allow_null=True, + ), + "expires_at": cls._parse_date_value( + row.get("expires_at"), + field_name="expires_at", + allow_null=True, + ), + } + state = cls._upsert_external_row( + model=InformationSecurityRegistryEntry, + lookup=lookup, + defaults=defaults, + ) + if state == "created": + created_count += 1 + elif state == "updated": + updated_count += 1 + + return { + "created": created_count, + "updated": updated_count, + "skipped": skipped_count, + } + + @classmethod + def _upsert_labor_vacancies(cls, rows: list[dict[str, Any]]) -> dict[str, int]: + created_count = 0 + updated_count = 0 + skipped_count = 0 + + for row in rows: + organization = cls._resolve_organization(row) + external_id = cls._clean_string(row.get("external_id")) + if not external_id: + skipped_count += 1 + continue + + defaults = { + "vacancy_source": cls._clean_string(row.get("vacancy_source")), + "title": cls._clean_string(row.get("title")), + "status": cls._clean_string(row.get("status")), + "published_at": cls._parse_date_value( + row.get("published_at"), + field_name="published_at", + allow_null=True, + ), + "salary_amount": cls._parse_decimal_value( + row.get("salary_amount"), + field_name="salary_amount", + allow_null=True, + ), + "source_url": cls._clean_string(row.get("source_url")), + } + state = cls._upsert_external_row( + model=LaborVacancy, + lookup={ + "organization": organization, + "external_id": external_id, + }, + defaults=defaults, + ) + if state == "created": + created_count += 1 + elif state == "updated": + updated_count += 1 + + return { + "created": created_count, + "updated": updated_count, + "skipped": skipped_count, + } + @classmethod def _upsert_external_row( cls, diff --git a/src/apps/external_data/api.py b/src/apps/external_data/api.py index 421c46b..a45d783 100644 --- a/src/apps/external_data/api.py +++ b/src/apps/external_data/api.py @@ -3,17 +3,23 @@ from apps.core.viewsets import ClassicReadOnlyViewSet from apps.external_data.models import ( ArbitrationCase, + BankruptcyProcedure, + DefenseUnreliableSupplier, IndustrialProduct, + InformationSecurityRegistryEntry, + LaborVacancy, ProsecutorCheck, PublicProcurement, - InformationSecurityRegistryEntry, ) from apps.external_data.serializers import ( ArbitrationCaseSerializer, + BankruptcyProcedureSerializer, + DefenseUnreliableSupplierSerializer, IndustrialProductSerializer, + InformationSecurityRegistryEntrySerializer, + LaborVacancySerializer, ProsecutorCheckSerializer, PublicProcurementSerializer, - CorporationMembershipSerializer, ) from django_filters import rest_framework as filters from rest_framework.permissions import IsAuthenticated @@ -79,6 +85,48 @@ class CorporationMembershipFilter(filters.FilterSet): fields = ["organization", "presence_status"] +class BankruptcyProcedureFilter(filters.FilterSet): + organization = filters.UUIDFilter(field_name="organization_id") + message_date_from = filters.DateFilter(field_name="message_date", lookup_expr="gte") + message_date_to = filters.DateFilter(field_name="message_date", lookup_expr="lte") + + class Meta: + model = BankruptcyProcedure + fields = ["organization", "message_date_from", "message_date_to"] + + +class DefenseUnreliableSupplierFilter(filters.FilterSet): + organization = filters.UUIDFilter(field_name="organization_id") + registry_source = filters.CharFilter(lookup_expr="exact") + included_at_from = filters.DateFilter(field_name="included_at", lookup_expr="gte") + included_at_to = filters.DateFilter(field_name="included_at", lookup_expr="lte") + + class Meta: + model = DefenseUnreliableSupplier + fields = [ + "organization", + "registry_source", + "included_at_from", + "included_at_to", + ] + + +class LaborVacancyFilter(filters.FilterSet): + organization = filters.UUIDFilter(field_name="organization_id") + vacancy_source = filters.CharFilter(lookup_expr="exact") + published_at_from = filters.DateFilter(field_name="published_at", lookup_expr="gte") + published_at_to = filters.DateFilter(field_name="published_at", lookup_expr="lte") + + class Meta: + model = LaborVacancy + fields = [ + "organization", + "vacancy_source", + "published_at_from", + "published_at_to", + ] + + class IndustrialProductViewSet(ClassicReadOnlyViewSet[IndustrialProduct]): queryset = IndustrialProduct.objects.select_related("organization").all() serializer_class = IndustrialProductSerializer @@ -122,10 +170,48 @@ class ArbitrationCaseViewSet(ClassicReadOnlyViewSet[ArbitrationCase]): class CorporationMembershipViewSet( ClassicReadOnlyViewSet[InformationSecurityRegistryEntry] ): - queryset = InformationSecurityRegistryEntry.objects.select_related("organization").all() - serializer_class = CorporationMembershipSerializer + queryset = InformationSecurityRegistryEntry.objects.select_related( + "organization" + ).all() + serializer_class = InformationSecurityRegistryEntrySerializer permission_classes = [IsAuthenticated] filterset_class = CorporationMembershipFilter search_fields = ["registry_name", "entry_number"] ordering_fields = ["issued_at", "expires_at", "created_at"] ordering = ["-issued_at"] + + +class InformationSecurityRegistryEntryViewSet(CorporationMembershipViewSet): + pass + + +class BankruptcyProcedureViewSet(ClassicReadOnlyViewSet[BankruptcyProcedure]): + queryset = BankruptcyProcedure.objects.select_related("organization").all() + serializer_class = BankruptcyProcedureSerializer + permission_classes = [IsAuthenticated] + filterset_class = BankruptcyProcedureFilter + search_fields = ["message_type", "case_number"] + ordering_fields = ["message_date", "created_at"] + ordering = ["-message_date"] + + +class DefenseUnreliableSupplierViewSet( + ClassicReadOnlyViewSet[DefenseUnreliableSupplier] +): + queryset = DefenseUnreliableSupplier.objects.select_related("organization").all() + serializer_class = DefenseUnreliableSupplierSerializer + permission_classes = [IsAuthenticated] + filterset_class = DefenseUnreliableSupplierFilter + search_fields = ["registry_number", "supplier_name", "reason"] + ordering_fields = ["included_at", "created_at"] + ordering = ["-included_at"] + + +class LaborVacancyViewSet(ClassicReadOnlyViewSet[LaborVacancy]): + queryset = LaborVacancy.objects.select_related("organization").all() + serializer_class = LaborVacancySerializer + permission_classes = [IsAuthenticated] + filterset_class = LaborVacancyFilter + search_fields = ["title", "status"] + ordering_fields = ["published_at", "created_at", "salary_amount"] + ordering = ["-published_at"] diff --git a/src/apps/external_data/migrations/0003_exchange_additional_external_data.py b/src/apps/external_data/migrations/0003_exchange_additional_external_data.py new file mode 100644 index 0000000..24d8da3 --- /dev/null +++ b/src/apps/external_data/migrations/0003_exchange_additional_external_data.py @@ -0,0 +1,340 @@ +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("external_data", "0002_information_security_registry_entry"), + ] + + operations = [ + migrations.AddField( + model_name="informationsecurityregistryentry", + name="external_id", + field=models.CharField( + blank=True, + db_index=True, + default="", + max_length=255, + verbose_name="внешний ID", + ), + ), + migrations.CreateModel( + name="LaborVacancy", + fields=[ + ( + "created_at", + models.DateTimeField( + auto_now_add=True, + db_index=True, + help_text="Дата и время создания записи", + verbose_name="создано", + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, + help_text="Дата и время последнего обновления", + verbose_name="обновлено", + ), + ), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "external_id", + models.CharField( + db_index=True, + max_length=255, + verbose_name="внешний ID", + ), + ), + ( + "vacancy_source", + models.CharField( + db_index=True, + max_length=50, + verbose_name="источник вакансии", + ), + ), + ( + "title", + models.CharField( + db_index=True, + max_length=500, + verbose_name="название вакансии", + ), + ), + ( + "status", + models.CharField( + blank=True, + db_index=True, + default="", + max_length=64, + verbose_name="статус", + ), + ), + ( + "published_at", + models.DateField( + blank=True, + db_index=True, + null=True, + verbose_name="дата публикации", + ), + ), + ( + "salary_amount", + models.DecimalField( + blank=True, + decimal_places=2, + max_digits=20, + null=True, + verbose_name="зарплата", + ), + ), + ( + "source_url", + models.TextField( + blank=True, + default="", + verbose_name="ссылка на источник", + ), + ), + ( + "organization", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="labor_vacancies", + to="organization.organization", + verbose_name="организация", + ), + ), + ], + options={ + "ordering": ["-published_at", "title"], + }, + ), + migrations.CreateModel( + name="DefenseUnreliableSupplier", + fields=[ + ( + "created_at", + models.DateTimeField( + auto_now_add=True, + db_index=True, + help_text="Дата и время создания записи", + verbose_name="создано", + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, + help_text="Дата и время последнего обновления", + verbose_name="обновлено", + ), + ), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "external_id", + models.CharField( + db_index=True, + max_length=255, + verbose_name="внешний ID", + ), + ), + ( + "registry_source", + models.CharField( + db_index=True, + max_length=50, + verbose_name="источник реестра", + ), + ), + ( + "registry_number", + models.CharField( + blank=True, + db_index=True, + default="", + max_length=128, + verbose_name="номер записи", + ), + ), + ( + "supplier_name", + models.CharField( + blank=True, + default="", + max_length=500, + verbose_name="наименование поставщика", + ), + ), + ( + "reason", + models.TextField( + blank=True, + default="", + verbose_name="основание", + ), + ), + ( + "included_at", + models.DateField( + blank=True, + db_index=True, + null=True, + verbose_name="дата включения", + ), + ), + ( + "status", + models.CharField( + blank=True, + db_index=True, + default="", + max_length=64, + verbose_name="статус", + ), + ), + ( + "source_url", + models.TextField( + blank=True, + default="", + verbose_name="ссылка на источник", + ), + ), + ( + "organization", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="defense_unreliable_suppliers", + to="organization.organization", + verbose_name="организация", + ), + ), + ], + options={ + "ordering": ["-included_at", "registry_source", "registry_number"], + }, + ), + migrations.CreateModel( + name="BankruptcyProcedure", + fields=[ + ( + "created_at", + models.DateTimeField( + auto_now_add=True, + db_index=True, + help_text="Дата и время создания записи", + verbose_name="создано", + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, + help_text="Дата и время последнего обновления", + verbose_name="обновлено", + ), + ), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "external_id", + models.CharField( + db_index=True, + max_length=255, + verbose_name="внешний ID", + ), + ), + ( + "message_type", + models.CharField( + db_index=True, + max_length=255, + verbose_name="тип сообщения", + ), + ), + ( + "message_date", + models.DateField( + blank=True, + db_index=True, + null=True, + verbose_name="дата сообщения", + ), + ), + ( + "case_number", + models.CharField( + blank=True, + db_index=True, + default="", + max_length=128, + verbose_name="номер дела", + ), + ), + ( + "status", + models.CharField( + blank=True, + db_index=True, + default="", + max_length=64, + verbose_name="статус", + ), + ), + ( + "source_url", + models.TextField( + blank=True, + default="", + verbose_name="ссылка на источник", + ), + ), + ( + "organization", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="bankruptcy_procedures", + to="organization.organization", + verbose_name="организация", + ), + ), + ], + options={ + "ordering": ["-message_date", "case_number", "message_type"], + }, + ), + ] diff --git a/src/apps/external_data/models.py b/src/apps/external_data/models.py index fc28e4f..262d3e6 100644 --- a/src/apps/external_data/models.py +++ b/src/apps/external_data/models.py @@ -112,7 +112,69 @@ class ArbitrationCase(UUIDPrimaryKeyMixin, TimestampMixin, models.Model): return f"{self.case_number} ({self.organization_id})" -class InformationSecurityRegistryEntry(UUIDPrimaryKeyMixin, TimestampMixin, models.Model): +class BankruptcyProcedure(UUIDPrimaryKeyMixin, TimestampMixin, models.Model): + organization = models.ForeignKey( + Organization, + on_delete=models.CASCADE, + related_name="bankruptcy_procedures", + verbose_name=_("организация"), + ) + external_id = models.CharField(_("внешний ID"), max_length=255, db_index=True) + message_type = models.CharField(_("тип сообщения"), max_length=255, db_index=True) + message_date = models.DateField( + _("дата сообщения"), null=True, blank=True, db_index=True + ) + case_number = models.CharField( + _("номер дела"), max_length=128, blank=True, default="", db_index=True + ) + status = models.CharField( + _("статус"), max_length=64, blank=True, default="", db_index=True + ) + source_url = models.TextField(_("ссылка на источник"), blank=True, default="") + + class Meta: + ordering = ["-message_date", "case_number", "message_type"] + + def __str__(self) -> str: + return f"{self.message_type} ({self.organization_id})" + + +class DefenseUnreliableSupplier(UUIDPrimaryKeyMixin, TimestampMixin, models.Model): + organization = models.ForeignKey( + Organization, + on_delete=models.CASCADE, + related_name="defense_unreliable_suppliers", + verbose_name=_("организация"), + ) + external_id = models.CharField(_("внешний ID"), max_length=255, db_index=True) + registry_source = models.CharField( + _("источник реестра"), max_length=50, db_index=True + ) + registry_number = models.CharField( + _("номер записи"), max_length=128, blank=True, default="", db_index=True + ) + supplier_name = models.CharField( + _("наименование поставщика"), max_length=500, blank=True, default="" + ) + reason = models.TextField(_("основание"), blank=True, default="") + included_at = models.DateField( + _("дата включения"), null=True, blank=True, db_index=True + ) + status = models.CharField( + _("статус"), max_length=64, blank=True, default="", db_index=True + ) + source_url = models.TextField(_("ссылка на источник"), blank=True, default="") + + class Meta: + ordering = ["-included_at", "registry_source", "registry_number"] + + def __str__(self) -> str: + return f"{self.registry_source}:{self.registry_number} ({self.organization_id})" + + +class InformationSecurityRegistryEntry( + UUIDPrimaryKeyMixin, TimestampMixin, models.Model +): class PresenceStatus(models.TextChoices): PRESENT = "present", _("В реестре") ABSENT = "absent", _("Не в реестре") @@ -123,6 +185,9 @@ class InformationSecurityRegistryEntry(UUIDPrimaryKeyMixin, TimestampMixin, mode related_name="information_security_registry_entries", verbose_name=_("организация"), ) + external_id = models.CharField( + _("внешний ID"), max_length=255, blank=True, default="", db_index=True + ) registry_name = models.CharField( _("название реестра"), max_length=255, db_index=True ) @@ -148,3 +213,37 @@ class InformationSecurityRegistryEntry(UUIDPrimaryKeyMixin, TimestampMixin, mode def __str__(self) -> str: return f"{self.registry_name} ({self.organization_id})" + + +class LaborVacancy(UUIDPrimaryKeyMixin, TimestampMixin, models.Model): + organization = models.ForeignKey( + Organization, + on_delete=models.CASCADE, + related_name="labor_vacancies", + verbose_name=_("организация"), + ) + external_id = models.CharField(_("внешний ID"), max_length=255, db_index=True) + vacancy_source = models.CharField( + _("источник вакансии"), max_length=50, db_index=True + ) + title = models.CharField(_("название вакансии"), max_length=500, db_index=True) + status = models.CharField( + _("статус"), max_length=64, blank=True, default="", db_index=True + ) + published_at = models.DateField( + _("дата публикации"), null=True, blank=True, db_index=True + ) + salary_amount = models.DecimalField( + _("зарплата"), + max_digits=20, + decimal_places=2, + null=True, + blank=True, + ) + source_url = models.TextField(_("ссылка на источник"), blank=True, default="") + + class Meta: + ordering = ["-published_at", "title"] + + def __str__(self) -> str: + return f"{self.title} ({self.organization_id})" diff --git a/src/apps/external_data/serializers.py b/src/apps/external_data/serializers.py index c9cbb44..3b48bd3 100644 --- a/src/apps/external_data/serializers.py +++ b/src/apps/external_data/serializers.py @@ -2,10 +2,13 @@ from apps.external_data.models import ( ArbitrationCase, + BankruptcyProcedure, + DefenseUnreliableSupplier, IndustrialProduct, + InformationSecurityRegistryEntry, + LaborVacancy, ProsecutorCheck, PublicProcurement, - InformationSecurityRegistryEntry, ) from rest_framework import serializers @@ -78,7 +81,43 @@ class ArbitrationCaseSerializer(serializers.ModelSerializer): ] -class CorporationMembershipSerializer(serializers.ModelSerializer): +class BankruptcyProcedureSerializer(serializers.ModelSerializer): + organization = serializers.UUIDField(source="organization_id", read_only=True) + + class Meta: + model = BankruptcyProcedure + fields = [ + "id", + "organization", + "external_id", + "message_type", + "message_date", + "case_number", + "status", + "source_url", + ] + + +class DefenseUnreliableSupplierSerializer(serializers.ModelSerializer): + organization = serializers.UUIDField(source="organization_id", read_only=True) + + class Meta: + model = DefenseUnreliableSupplier + fields = [ + "id", + "organization", + "external_id", + "registry_source", + "registry_number", + "supplier_name", + "reason", + "included_at", + "status", + "source_url", + ] + + +class InformationSecurityRegistryEntrySerializer(serializers.ModelSerializer): organization = serializers.UUIDField(source="organization_id", read_only=True) class Meta: @@ -86,9 +125,31 @@ class CorporationMembershipSerializer(serializers.ModelSerializer): fields = [ "id", "organization", + "external_id", "registry_name", "presence_status", "entry_number", "issued_at", "expires_at", ] + + +class LaborVacancySerializer(serializers.ModelSerializer): + organization = serializers.UUIDField(source="organization_id", read_only=True) + + class Meta: + model = LaborVacancy + fields = [ + "id", + "organization", + "external_id", + "vacancy_source", + "title", + "status", + "published_at", + "salary_amount", + "source_url", + ] + + +CorporationMembershipSerializer = InformationSecurityRegistryEntrySerializer diff --git a/src/apps/external_data/urls.py b/src/apps/external_data/urls.py index 2920190..4f69b05 100644 --- a/src/apps/external_data/urls.py +++ b/src/apps/external_data/urls.py @@ -2,10 +2,14 @@ from apps.external_data.api import ( ArbitrationCaseViewSet, + BankruptcyProcedureViewSet, + CorporationMembershipViewSet, + DefenseUnreliableSupplierViewSet, IndustrialProductViewSet, + InformationSecurityRegistryEntryViewSet, + LaborVacancyViewSet, ProsecutorCheckViewSet, PublicProcurementViewSet, - CorporationMembershipViewSet, ) from django.urls import include, path from rest_framework.routers import DefaultRouter @@ -30,6 +34,26 @@ router.register( CorporationMembershipViewSet, basename="corporation-memberships", ) +router.register( + "information-security-registries", + InformationSecurityRegistryEntryViewSet, + basename="information-security-registries", +) +router.register( + "bankruptcy-procedures", + BankruptcyProcedureViewSet, + basename="bankruptcy-procedures", +) +router.register( + "defense-unreliable-suppliers", + DefenseUnreliableSupplierViewSet, + basename="defense-unreliable-suppliers", +) +router.register( + "labor-vacancies", + LaborVacancyViewSet, + basename="labor-vacancies", +) urlpatterns = [ path("", include(router.urls)), diff --git a/src/apps/form_2/serializers.py b/src/apps/form_2/serializers.py index 3da59c3..a218b67 100644 --- a/src/apps/form_2/serializers.py +++ b/src/apps/form_2/serializers.py @@ -7,11 +7,11 @@ - FormF2ParseResultSerializer - результат парсинга """ -from apps.form_2.models import FormF2Record from apps.core.upload_contracts import ( - UploadQuarterSerializer, UploadParseResultSerializer, + UploadQuarterSerializer, ) +from apps.form_2.models import FormF2Record from apps.organization.serializers import OrganizationSerializer from rest_framework import serializers diff --git a/src/apps/form_3/serializers.py b/src/apps/form_3/serializers.py index 1d2fd05..3a27cd8 100644 --- a/src/apps/form_3/serializers.py +++ b/src/apps/form_3/serializers.py @@ -7,11 +7,11 @@ - FormF3ParseResultSerializer - результат парсинга """ -from apps.form_3.models import FormF3Record from apps.core.upload_contracts import ( UploadAnnualSerializer, UploadParseResultSerializer, ) +from apps.form_3.models import FormF3Record from apps.organization.serializers import OrganizationSerializer from rest_framework import serializers diff --git a/src/apps/form_4/serializers.py b/src/apps/form_4/serializers.py index 4c22e08..22db8c5 100644 --- a/src/apps/form_4/serializers.py +++ b/src/apps/form_4/serializers.py @@ -1,10 +1,10 @@ """Сериализаторы формы Ф-4.""" -from apps.form_4.models import FormF4Record from apps.core.upload_contracts import ( UploadHalfYearSerializer, UploadParseResultSerializer, ) +from apps.form_4.models import FormF4Record from apps.organization.serializers import OrganizationSerializer from rest_framework import serializers diff --git a/src/apps/form_5/serializers.py b/src/apps/form_5/serializers.py index 5bcd954..6d1cfca 100644 --- a/src/apps/form_5/serializers.py +++ b/src/apps/form_5/serializers.py @@ -1,10 +1,10 @@ """Сериализаторы формы Ф-5.""" -from apps.form_5.models import FormF5Record from apps.core.upload_contracts import ( UploadAnnualSerializer, UploadParseResultSerializer, ) +from apps.form_5.models import FormF5Record from apps.organization.serializers import OrganizationSerializer from rest_framework import serializers diff --git a/src/apps/form_6/serializers.py b/src/apps/form_6/serializers.py index 8a647ab..464a9c7 100644 --- a/src/apps/form_6/serializers.py +++ b/src/apps/form_6/serializers.py @@ -1,10 +1,10 @@ """Сериализаторы формы Ф-6.""" -from apps.form_6.models import FormF6Record from apps.core.upload_contracts import ( UploadAnnualSerializer, UploadParseResultSerializer, ) +from apps.form_6.models import FormF6Record from apps.organization.serializers import OrganizationSerializer from rest_framework import serializers diff --git a/src/apps/organization/analytics_services.py b/src/apps/organization/analytics_services.py index 4bdd093..cee0a37 100644 --- a/src/apps/organization/analytics_services.py +++ b/src/apps/organization/analytics_services.py @@ -746,11 +746,16 @@ class OrganizationAnalyticsService: return base_rows if frequency == "annual": - return [cls._aggregate_product_metrics(base_rows, str(records[0].report_year))] + return [ + cls._aggregate_product_metrics(base_rows, str(records[0].report_year)) + ] if frequency == "semiannual": grouped_rows: list[dict[str, object]] = [] - periods = ((1, 2, f"{records[0].report_year}-H1"), (3, 4, f"{records[0].report_year}-H2")) + periods = ( + (1, 2, f"{records[0].report_year}-H1"), + (3, 4, f"{records[0].report_year}-H2"), + ) for quarter_start, quarter_end, period in periods: half_rows = [ row @@ -792,10 +797,11 @@ class OrganizationAnalyticsService: @staticmethod def _product_row_shipped_goods_amount(row: dict[str, object]) -> int: metrics = row["metrics"] - return _amount(metrics["military_domestic_amount"]) + _amount( - metrics["military_export_amount"] - ) + _amount(metrics["civilian_domestic_amount"]) + _amount( - metrics["civilian_export_amount"] + return ( + _amount(metrics["military_domestic_amount"]) + + _amount(metrics["military_export_amount"]) + + _amount(metrics["civilian_domestic_amount"]) + + _amount(metrics["civilian_export_amount"]) ) @classmethod diff --git a/src/apps/organization/contract_serializers.py b/src/apps/organization/contract_serializers.py index 6e083fd..e739d5d 100644 --- a/src/apps/organization/contract_serializers.py +++ b/src/apps/organization/contract_serializers.py @@ -1,12 +1,11 @@ """Serializer contracts for OpenAPI documentation.""" -from rest_framework import serializers - from apps.organization.serializers import ( CorporationScopeDictionarySerializer, OrganizationCatalogDetailSerializer, OrganizationCatalogListSerializer, ) +from rest_framework import serializers class OrganizationCatalogListResponseSerializer(serializers.Serializer): diff --git a/src/apps/organization/scope_utils.py b/src/apps/organization/scope_utils.py index aaf6ccd..1c96e33 100644 --- a/src/apps/organization/scope_utils.py +++ b/src/apps/organization/scope_utils.py @@ -67,9 +67,7 @@ def scope_labels(scope_codes: Iterable[str]) -> list[str]: def get_corporation_scope_dictionary() -> list[dict[str, str | int]]: """Возвращает справочник корпусов для API-словаря.""" items: list[dict[str, str | int]] = [] - for code, sort_order in sorted( - SCOPE_SORT_ORDER.items(), key=lambda item: item[1] - ): + for code, sort_order in sorted(SCOPE_SORT_ORDER.items(), key=lambda item: item[1]): label = SCOPE_LABELS.get(code) short_name = SCOPE_SHORT_NAMES.get(code) if not label or not short_name: diff --git a/src/apps/organization/serializers.py b/src/apps/organization/serializers.py index 1eadd5a..b37114b 100644 --- a/src/apps/organization/serializers.py +++ b/src/apps/organization/serializers.py @@ -145,7 +145,6 @@ class OrganizationCatalogBaseSerializer(serializers.ModelSerializer): return SCOPE_LABELS.get(scope_code, "") - class OrganizationCatalogListSerializer(OrganizationCatalogBaseSerializer): """Сериализатор списка организаций для `/api/v1/organizations/`.""" diff --git a/src/apps/user/views.py b/src/apps/user/views.py index b078c76..b613835 100644 --- a/src/apps/user/views.py +++ b/src/apps/user/views.py @@ -249,16 +249,19 @@ class AdminUsersManagementView(APIView): return latest_jobs: dict[int, BackgroundJob] = {} - jobs = BackgroundJobService.get_queryset().filter( - user_id__in=user_ids - ).annotate( - _effective_job_ts=Coalesce( - F("completed_at"), - F("started_at"), - F("updated_at"), - F("created_at"), + jobs = ( + BackgroundJobService.get_queryset() + .filter(user_id__in=user_ids) + .annotate( + _effective_job_ts=Coalesce( + F("completed_at"), + F("started_at"), + F("updated_at"), + F("created_at"), + ) ) - ).order_by("user_id", "-_effective_job_ts") + .order_by("user_id", "-_effective_job_ts") + ) for job in jobs: if job.user_id not in latest_jobs: diff --git a/tests/apps/exchange/test_api.py b/tests/apps/exchange/test_api.py index 86268bf..765bd3c 100644 --- a/tests/apps/exchange/test_api.py +++ b/tests/apps/exchange/test_api.py @@ -16,7 +16,11 @@ from apps.exchange.models import ExchangeDeliveryChannel, ExchangePackageImport from apps.exchange.services import ExchangePackageImportService from apps.external_data.models import ( ArbitrationCase, + BankruptcyProcedure, + DefenseUnreliableSupplier, IndustrialProduct, + InformationSecurityRegistryEntry, + LaborVacancy, ProsecutorCheck, PublicProcurement, ) @@ -194,6 +198,53 @@ def build_exchange_payload() -> dict[str, list[dict[str, object]]]: "decision_date": "2026-03-25", } ], + "bankruptcy_procedures": [ + { + "organization_inn": "7707083893", + "external_id": "fedresurs:001", + "message_type": "Сообщение о намерении", + "message_date": "2026-03-26", + "case_number": "А40-555/2026", + "status": "published", + "source_url": "https://fedresurs.ru/message/001", + } + ], + "defense_unreliable_suppliers": [ + { + "organization_inn": "7707083893", + "external_id": "fas-goz:001", + "registry_source": "fas_goz", + "registry_number": "ГОЗ-001", + "supplier_name": "АО Альфа Обновленная", + "reason": "Уклонение от заключения контракта", + "included_at": "2026-02-20", + "status": "active", + "source_url": "https://fas.gov.ru/register/001", + } + ], + "information_security_registries": [ + { + "organization_inn": "7707083893", + "external_id": "fstec:001", + "registry_name": "Реестр лицензий ФСТЭК", + "presence_status": "present", + "entry_number": "77-001234", + "issued_at": "2026-01-10", + "expires_at": "2027-01-10", + } + ], + "labor_vacancies": [ + { + "organization_inn": "7707083893", + "external_id": "trudvsem:001", + "vacancy_source": "trudvsem", + "title": "Инженер-испытатель", + "status": "open", + "published_at": "2026-04-01", + "salary_amount": "175000.00", + "source_url": "https://trudvsem.ru/vacancy/001", + } + ], } @@ -234,6 +285,23 @@ class ExchangePackageApiTest(APITestCase): self.assertEqual(ProsecutorCheck.objects.count(), 1) self.assertEqual(PublicProcurement.objects.count(), 1) self.assertEqual(ArbitrationCase.objects.count(), 1) + self.assertEqual(BankruptcyProcedure.objects.count(), 1) + self.assertEqual(DefenseUnreliableSupplier.objects.count(), 1) + self.assertEqual(InformationSecurityRegistryEntry.objects.count(), 1) + self.assertEqual(LaborVacancy.objects.count(), 1) + self.assertEqual( + response.data["result"]["bankruptcy_procedures"]["created"], + 1, + ) + self.assertEqual( + response.data["result"]["defense_unreliable_suppliers"]["created"], + 1, + ) + self.assertEqual( + response.data["result"]["information_security_registries"]["created"], + 1, + ) + self.assertEqual(response.data["result"]["labor_vacancies"]["created"], 1) organization = Organization.objects.get(inn="7707083893") self.assertEqual(organization.name, "АО Альфа Обновленная") diff --git a/tests/apps/external_data/factories.py b/tests/apps/external_data/factories.py index fc2e9f0..141d827 100644 --- a/tests/apps/external_data/factories.py +++ b/tests/apps/external_data/factories.py @@ -3,10 +3,13 @@ import factory from apps.external_data.models import ( ArbitrationCase, + BankruptcyProcedure, + DefenseUnreliableSupplier, IndustrialProduct, + InformationSecurityRegistryEntry, + LaborVacancy, ProsecutorCheck, PublicProcurement, - InformationSecurityRegistryEntry, ) from faker import Faker @@ -71,9 +74,7 @@ class ArbitrationCaseFactory(factory.django.DjangoModelFactory): decision_date = factory.LazyAttribute(lambda _: fake.date_this_year()) -class InformationSecurityRegistryEntryFactory( - factory.django.DjangoModelFactory -): +class InformationSecurityRegistryEntryFactory(factory.django.DjangoModelFactory): class Meta: model = InformationSecurityRegistryEntry @@ -83,3 +84,45 @@ class InformationSecurityRegistryEntryFactory( entry_number = "77-001234" issued_at = factory.LazyAttribute(lambda _: fake.date_this_year()) expires_at = factory.LazyAttribute(lambda _: fake.date_this_year()) + + +class BankruptcyProcedureFactory(factory.django.DjangoModelFactory): + class Meta: + model = BankruptcyProcedure + + organization = factory.SubFactory(OrganizationFactory) + external_id = factory.Sequence(lambda n: f"fedresurs:{n}") + message_type = "Сообщение о намерении" + message_date = factory.LazyAttribute(lambda _: fake.date_this_year()) + case_number = factory.Sequence(lambda n: f"А40-{20_000 + n}/2026") + status = "published" + source_url = factory.Sequence(lambda n: f"https://fedresurs.ru/message/{n}") + + +class DefenseUnreliableSupplierFactory(factory.django.DjangoModelFactory): + class Meta: + model = DefenseUnreliableSupplier + + organization = factory.SubFactory(OrganizationFactory) + external_id = factory.Sequence(lambda n: f"fas-goz:{n}") + registry_source = "fas_goz" + registry_number = factory.Sequence(lambda n: f"ГОЗ-{n:04d}") + supplier_name = factory.LazyAttribute(lambda obj: obj.organization.name) + reason = "Уклонение от заключения контракта" + included_at = factory.LazyAttribute(lambda _: fake.date_this_year()) + status = "active" + source_url = factory.Sequence(lambda n: f"https://fas.gov.ru/register/{n}") + + +class LaborVacancyFactory(factory.django.DjangoModelFactory): + class Meta: + model = LaborVacancy + + organization = factory.SubFactory(OrganizationFactory) + external_id = factory.Sequence(lambda n: f"trudvsem:{n}") + vacancy_source = "trudvsem" + title = "Инженер-испытатель" + status = "open" + published_at = factory.LazyAttribute(lambda _: fake.date_this_year()) + salary_amount = "175000.00" + source_url = factory.Sequence(lambda n: f"https://trudvsem.ru/vacancy/{n}") diff --git a/tests/apps/external_data/test_api.py b/tests/apps/external_data/test_api.py index b950023..de4b9a0 100644 --- a/tests/apps/external_data/test_api.py +++ b/tests/apps/external_data/test_api.py @@ -10,10 +10,13 @@ from rest_framework.test import APITestCase from tests.apps.external_data.factories import ( ArbitrationCaseFactory, + BankruptcyProcedureFactory, + DefenseUnreliableSupplierFactory, IndustrialProductFactory, + InformationSecurityRegistryEntryFactory, + LaborVacancyFactory, ProsecutorCheckFactory, PublicProcurementFactory, - InformationSecurityRegistryEntryFactory, ) from tests.apps.organization.factories import OrganizationFactory from tests.apps.user.factories import UserFactory @@ -116,3 +119,47 @@ class ExternalDataApiTest(APITestCase): self.assertEqual(result["presence_status"], "present") self.assertIn("registry_name", result) self.assertIn("entry_number", result) + + def test_additional_exchange_sections_are_exposed(self): + BankruptcyProcedureFactory.create( + organization=self.organization, + message_type="Сообщение о намерении", + message_date=date(2026, 3, 26), + ) + DefenseUnreliableSupplierFactory.create( + organization=self.organization, + registry_source="fas_goz", + included_at=date(2026, 2, 20), + ) + LaborVacancyFactory.create( + organization=self.organization, + vacancy_source="trudvsem", + published_at=date(2026, 4, 1), + ) + BankruptcyProcedureFactory.create(organization=self.other_organization) + DefenseUnreliableSupplierFactory.create(organization=self.other_organization) + LaborVacancyFactory.create(organization=self.other_organization) + + bankruptcy_response = self.client.get( + f"/api/v1/bankruptcy-procedures/?organization={self.organization.id}" + "&message_date_from=2026-01-01&message_date_to=2026-12-31" + ) + defense_response = self.client.get( + f"/api/v1/defense-unreliable-suppliers/?organization={self.organization.id}" + "®istry_source=fas_goz" + ) + vacancies_response = self.client.get( + f"/api/v1/labor-vacancies/?organization={self.organization.id}" + "&vacancy_source=trudvsem" + ) + + self.assertEqual(bankruptcy_response.status_code, status.HTTP_200_OK) + self.assertEqual(defense_response.status_code, status.HTTP_200_OK) + self.assertEqual(vacancies_response.status_code, status.HTTP_200_OK) + self.assertEqual(bankruptcy_response.data["count"], 1) + self.assertEqual(defense_response.data["count"], 1) + self.assertEqual(vacancies_response.data["count"], 1) + self.assertEqual( + vacancies_response.data["results"][0]["title"], + "Инженер-испытатель", + ) diff --git a/tests/apps/forms/test_upload_contracts_api.py b/tests/apps/forms/test_upload_contracts_api.py index cbb4d27..1a36541 100644 --- a/tests/apps/forms/test_upload_contracts_api.py +++ b/tests/apps/forms/test_upload_contracts_api.py @@ -193,19 +193,18 @@ class FormUploadContractsApiTest(APITestCase): def test_upload_processing_error_contract(self): for _, case in self.CASES.items(): - with self.subTest(form=case["form"]): - with patch( - case["parse_target"], - side_effect=RuntimeError("parse failed"), - ) as parse_mock: - response = self.client.post( - case["url"], - self._build_payload(case["payload"], file_size=256), - format="multipart", - ) + with self.subTest(form=case["form"]), patch( + case["parse_target"], + side_effect=RuntimeError("parse failed"), + ) as parse_mock: + response = self.client.post( + case["url"], + self._build_payload(case["payload"], file_size=256), + format="multipart", + ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data["error_code"], "processing_error") - self.assertEqual(response.data["error_message"], "parse failed") - self.assertEqual(response.data["details"], []) - parse_mock.assert_called_once() + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data["error_code"], "processing_error") + self.assertEqual(response.data["error_message"], "parse failed") + self.assertEqual(response.data["details"], []) + parse_mock.assert_called_once() diff --git a/tests/apps/organization/test_analytics_api.py b/tests/apps/organization/test_analytics_api.py index e6dff33..607b7b2 100644 --- a/tests/apps/organization/test_analytics_api.py +++ b/tests/apps/organization/test_analytics_api.py @@ -188,9 +188,18 @@ class OrganizationAnalyticsApiTest(APITestCase): self.assertEqual(response.data["insurance_contributions"]["amount"], 302000) self.assertEqual(response.data["organization_id"], str(self.organization.id)) self.assertEqual(response.data["report_period"], {"year": 2026, "quarter": 1}) - self.assertEqual(set(response.data["revenue"]), {"amount", "previous_amount", "delta_percent"}) - self.assertEqual(set(response.data["net_profit"]), {"amount", "previous_amount", "delta_percent"}) - self.assertEqual(set(response.data["taxes_paid"]), {"amount", "previous_amount", "delta_percent"}) + self.assertEqual( + set(response.data["revenue"]), + {"amount", "previous_amount", "delta_percent"}, + ) + self.assertEqual( + set(response.data["net_profit"]), + {"amount", "previous_amount", "delta_percent"}, + ) + self.assertEqual( + set(response.data["taxes_paid"]), + {"amount", "previous_amount", "delta_percent"}, + ) self.assertEqual( set(response.data["insurance_contributions"]), {"amount", "previous_amount", "delta_percent"}, @@ -228,7 +237,12 @@ class OrganizationAnalyticsApiTest(APITestCase): set(response.data["ratio_normatives"]), {"ros", "roa", "roe", "ebitda_margin"}, ) - self.assertTrue(all(value is not None for value in response.data["ratio_normatives"].values())) + self.assertTrue( + all( + value is not None + for value in response.data["ratio_normatives"].values() + ) + ) def test_personnel_contract(self): personnel_response = self.client.get( @@ -236,7 +250,9 @@ class OrganizationAnalyticsApiTest(APITestCase): "?report_year=2026&history_years=2" ) self.assertEqual(personnel_response.status_code, status.HTTP_200_OK) - self.assertEqual(personnel_response.data["organization_id"], str(self.organization.id)) + self.assertEqual( + personnel_response.data["organization_id"], str(self.organization.id) + ) self.assertEqual(personnel_response.data["report_year"], 2026) self.assertEqual( personnel_response.data["headcount"]["average_employees"], @@ -312,15 +328,25 @@ class OrganizationAnalyticsApiTest(APITestCase): "?frequency=quarterly&price_mode=actual&report_year=2026" ) self.assertEqual(products_response.status_code, status.HTTP_200_OK) - self.assertEqual(products_response.data["organization_id"], str(self.organization.id)) + self.assertEqual( + products_response.data["organization_id"], str(self.organization.id) + ) self.assertEqual(products_response.data["report_year"], 2026) self.assertEqual(products_response.data["frequency"], "quarterly") self.assertEqual(products_response.data["price_mode"], "actual") - self.assertEqual(products_response.data["summary"]["military_output_amount"], 11000000) - self.assertEqual(products_response.data["summary"]["civilian_output_amount"], 7000000) - self.assertEqual(products_response.data["summary"]["hightech_output_amount"], 1500000) + self.assertEqual( + products_response.data["summary"]["military_output_amount"], 11000000 + ) + self.assertEqual( + products_response.data["summary"]["civilian_output_amount"], 7000000 + ) + self.assertEqual( + products_response.data["summary"]["hightech_output_amount"], 1500000 + ) self.assertEqual(products_response.data["summary"]["rd_volume_amount"], 900000) - self.assertEqual(products_response.data["summary"]["shipped_goods_amount"], 18000000) + self.assertEqual( + products_response.data["summary"]["shipped_goods_amount"], 18000000 + ) self.assertEqual(len(products_response.data["production_series"]), 1) self.assertEqual(len(products_response.data["sales_series"]), 1) self.assertEqual(len(products_response.data["rd_volume_series"]), 1) @@ -381,7 +407,9 @@ class OrganizationAnalyticsApiTest(APITestCase): self.assertEqual(monthly_response.status_code, status.HTTP_200_OK) self.assertEqual(monthly_response.data["frequency"], "monthly") self.assertEqual(len(monthly_response.data["production_series"]), 6) - self.assertEqual(monthly_response.data["production_series"][0]["period"], "2026-01") + self.assertEqual( + monthly_response.data["production_series"][0]["period"], "2026-01" + ) self.assertEqual( monthly_response.data["production_series"][0]["military_output_amount"], 3666666, diff --git a/tests/apps/organization/test_api.py b/tests/apps/organization/test_api.py index bab26b2..dbb817a 100644 --- a/tests/apps/organization/test_api.py +++ b/tests/apps/organization/test_api.py @@ -230,15 +230,21 @@ class OrganizationApiTest(APITestCase): response = self.client.get("/api/v1/organizations/?registry_category=goz") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual([item["id"] for item in response.data["results"]], [str(goz_org.id)]) + self.assertEqual( + [item["id"] for item in response.data["results"]], [str(goz_org.id)] + ) response = self.client.get("/api/v1/organizations/?registryCategory=opk") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual([item["id"] for item in response.data["results"]], [str(opk_org.id)]) + self.assertEqual( + [item["id"] for item in response.data["results"]], [str(opk_org.id)] + ) response = self.client.get("/api/v1/organizations/?registry_category=other") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIn(str(other_org.id), [item["id"] for item in response.data["results"]]) + self.assertIn( + str(other_org.id), [item["id"] for item in response.data["results"]] + ) class OrganizationDictionaryApiTest(APITestCase):