2 Commits

18 changed files with 76 additions and 78 deletions

View File

@@ -19,19 +19,6 @@ FROM base AS builder
ARG INSTALL_DEV=false
# hadolint ignore=DL3008
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
gcc \
libpq-dev \
libffi-dev \
libxml2-dev \
libxslt1-dev \
zlib1g-dev \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
COPY pyproject.toml uv.lock ./
RUN if [ "${INSTALL_DEV}" = "true" ]; then \
@@ -43,17 +30,6 @@ RUN if [ "${INSTALL_DEV}" = "true" ]; then \
FROM base AS runtime-base
# hadolint ignore=DL3008
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
curl \
libpq5 \
libffi8 \
libxml2 \
libxslt1.1 \
zlib1g \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/.venv /app/.venv
COPY src/ ./src/
COPY docker/scripts/ ./docker/scripts/

View File

@@ -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

View File

@@ -4,16 +4,16 @@ from apps.core.viewsets import ClassicReadOnlyViewSet
from apps.external_data.models import (
ArbitrationCase,
IndustrialProduct,
InformationSecurityRegistryEntry,
ProsecutorCheck,
PublicProcurement,
InformationSecurityRegistryEntry,
)
from apps.external_data.serializers import (
ArbitrationCaseSerializer,
CorporationMembershipSerializer,
IndustrialProductSerializer,
ProsecutorCheckSerializer,
PublicProcurementSerializer,
CorporationMembershipSerializer,
)
from django_filters import rest_framework as filters
from rest_framework.permissions import IsAuthenticated
@@ -122,7 +122,9 @@ class ArbitrationCaseViewSet(ClassicReadOnlyViewSet[ArbitrationCase]):
class CorporationMembershipViewSet(
ClassicReadOnlyViewSet[InformationSecurityRegistryEntry]
):
queryset = InformationSecurityRegistryEntry.objects.select_related("organization").all()
queryset = InformationSecurityRegistryEntry.objects.select_related(
"organization"
).all()
serializer_class = CorporationMembershipSerializer
permission_classes = [IsAuthenticated]
filterset_class = CorporationMembershipFilter

View File

@@ -112,7 +112,9 @@ class ArbitrationCase(UUIDPrimaryKeyMixin, TimestampMixin, models.Model):
return f"{self.case_number} ({self.organization_id})"
class InformationSecurityRegistryEntry(UUIDPrimaryKeyMixin, TimestampMixin, models.Model):
class InformationSecurityRegistryEntry(
UUIDPrimaryKeyMixin, TimestampMixin, models.Model
):
class PresenceStatus(models.TextChoices):
PRESENT = "present", _("В реестре")
ABSENT = "absent", _("Не в реестре")

View File

@@ -3,9 +3,9 @@
from apps.external_data.models import (
ArbitrationCase,
IndustrialProduct,
InformationSecurityRegistryEntry,
ProsecutorCheck,
PublicProcurement,
InformationSecurityRegistryEntry,
)
from rest_framework import serializers

View File

@@ -2,10 +2,10 @@
from apps.external_data.api import (
ArbitrationCaseViewSet,
CorporationMembershipViewSet,
IndustrialProductViewSet,
ProsecutorCheckViewSet,
PublicProcurementViewSet,
CorporationMembershipViewSet,
)
from django.urls import include, path
from rest_framework.routers import DefaultRouter

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -56,9 +56,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:

View File

@@ -129,7 +129,6 @@ class OrganizationCatalogBaseSerializer(serializers.ModelSerializer):
return SCOPE_LABELS.get(scope_code, "")
class OrganizationCatalogListSerializer(OrganizationCatalogBaseSerializer):
"""Сериализатор списка организаций для `/api/v1/organizations/`."""

View File

@@ -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:

View File

@@ -4,9 +4,9 @@ import factory
from apps.external_data.models import (
ArbitrationCase,
IndustrialProduct,
InformationSecurityRegistryEntry,
ProsecutorCheck,
PublicProcurement,
InformationSecurityRegistryEntry,
)
from faker import Faker
@@ -71,9 +71,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

View File

@@ -11,9 +11,9 @@ from rest_framework.test import APITestCase
from tests.apps.external_data.factories import (
ArbitrationCaseFactory,
IndustrialProductFactory,
InformationSecurityRegistryEntryFactory,
ProsecutorCheckFactory,
PublicProcurementFactory,
InformationSecurityRegistryEntryFactory,
)
from tests.apps.organization.factories import OrganizationFactory
from tests.apps.user.factories import UserFactory

View File

@@ -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()

View File

@@ -183,9 +183,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"},
@@ -225,7 +234,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"],
@@ -285,13 +296,21 @@ 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(len(products_response.data["production_series"]), 1)
self.assertEqual(len(products_response.data["sales_series"]), 1)