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 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 ./ COPY pyproject.toml uv.lock ./
RUN if [ "${INSTALL_DEV}" = "true" ]; then \ RUN if [ "${INSTALL_DEV}" = "true" ]; then \
@@ -43,17 +30,6 @@ RUN if [ "${INSTALL_DEV}" = "true" ]; then \
FROM base AS runtime-base 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 --from=builder /app/.venv /app/.venv
COPY src/ ./src/ COPY src/ ./src/
COPY docker/scripts/ ./docker/scripts/ COPY docker/scripts/ ./docker/scripts/

View File

@@ -82,7 +82,9 @@ def build_upload_success_payload(
report_quarter=report_quarter, report_quarter=report_quarter,
) )
else: 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: if result is not None:
payload["result"] = result payload["result"] = result

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,11 +7,11 @@
- FormF2ParseResultSerializer - результат парсинга - FormF2ParseResultSerializer - результат парсинга
""" """
from apps.form_2.models import FormF2Record
from apps.core.upload_contracts import ( from apps.core.upload_contracts import (
UploadQuarterSerializer,
UploadParseResultSerializer, UploadParseResultSerializer,
UploadQuarterSerializer,
) )
from apps.form_2.models import FormF2Record
from apps.organization.serializers import OrganizationSerializer from apps.organization.serializers import OrganizationSerializer
from rest_framework import serializers from rest_framework import serializers

View File

@@ -7,11 +7,11 @@
- FormF3ParseResultSerializer - результат парсинга - FormF3ParseResultSerializer - результат парсинга
""" """
from apps.form_3.models import FormF3Record
from apps.core.upload_contracts import ( from apps.core.upload_contracts import (
UploadAnnualSerializer, UploadAnnualSerializer,
UploadParseResultSerializer, UploadParseResultSerializer,
) )
from apps.form_3.models import FormF3Record
from apps.organization.serializers import OrganizationSerializer from apps.organization.serializers import OrganizationSerializer
from rest_framework import serializers from rest_framework import serializers

View File

@@ -1,10 +1,10 @@
"""Сериализаторы формы Ф-4.""" """Сериализаторы формы Ф-4."""
from apps.form_4.models import FormF4Record
from apps.core.upload_contracts import ( from apps.core.upload_contracts import (
UploadHalfYearSerializer, UploadHalfYearSerializer,
UploadParseResultSerializer, UploadParseResultSerializer,
) )
from apps.form_4.models import FormF4Record
from apps.organization.serializers import OrganizationSerializer from apps.organization.serializers import OrganizationSerializer
from rest_framework import serializers from rest_framework import serializers

View File

@@ -1,10 +1,10 @@
"""Сериализаторы формы Ф-5.""" """Сериализаторы формы Ф-5."""
from apps.form_5.models import FormF5Record
from apps.core.upload_contracts import ( from apps.core.upload_contracts import (
UploadAnnualSerializer, UploadAnnualSerializer,
UploadParseResultSerializer, UploadParseResultSerializer,
) )
from apps.form_5.models import FormF5Record
from apps.organization.serializers import OrganizationSerializer from apps.organization.serializers import OrganizationSerializer
from rest_framework import serializers from rest_framework import serializers

View File

@@ -1,10 +1,10 @@
"""Сериализаторы формы Ф-6.""" """Сериализаторы формы Ф-6."""
from apps.form_6.models import FormF6Record
from apps.core.upload_contracts import ( from apps.core.upload_contracts import (
UploadAnnualSerializer, UploadAnnualSerializer,
UploadParseResultSerializer, UploadParseResultSerializer,
) )
from apps.form_6.models import FormF6Record
from apps.organization.serializers import OrganizationSerializer from apps.organization.serializers import OrganizationSerializer
from rest_framework import serializers 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]]: def get_corporation_scope_dictionary() -> list[dict[str, str | int]]:
"""Возвращает справочник корпусов для API-словаря.""" """Возвращает справочник корпусов для API-словаря."""
items: list[dict[str, str | int]] = [] items: list[dict[str, str | int]] = []
for code, sort_order in sorted( for code, sort_order in sorted(SCOPE_SORT_ORDER.items(), key=lambda item: item[1]):
SCOPE_SORT_ORDER.items(), key=lambda item: item[1]
):
label = SCOPE_LABELS.get(code) label = SCOPE_LABELS.get(code)
short_name = SCOPE_SHORT_NAMES.get(code) short_name = SCOPE_SHORT_NAMES.get(code)
if not label or not short_name: if not label or not short_name:

View File

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

View File

@@ -249,16 +249,19 @@ class AdminUsersManagementView(APIView):
return return
latest_jobs: dict[int, BackgroundJob] = {} latest_jobs: dict[int, BackgroundJob] = {}
jobs = BackgroundJobService.get_queryset().filter( jobs = (
user_id__in=user_ids BackgroundJobService.get_queryset()
).annotate( .filter(user_id__in=user_ids)
.annotate(
_effective_job_ts=Coalesce( _effective_job_ts=Coalesce(
F("completed_at"), F("completed_at"),
F("started_at"), F("started_at"),
F("updated_at"), F("updated_at"),
F("created_at"), F("created_at"),
) )
).order_by("user_id", "-_effective_job_ts") )
.order_by("user_id", "-_effective_job_ts")
)
for job in jobs: for job in jobs:
if job.user_id not in latest_jobs: if job.user_id not in latest_jobs:

View File

@@ -4,9 +4,9 @@ import factory
from apps.external_data.models import ( from apps.external_data.models import (
ArbitrationCase, ArbitrationCase,
IndustrialProduct, IndustrialProduct,
InformationSecurityRegistryEntry,
ProsecutorCheck, ProsecutorCheck,
PublicProcurement, PublicProcurement,
InformationSecurityRegistryEntry,
) )
from faker import Faker from faker import Faker
@@ -71,9 +71,7 @@ class ArbitrationCaseFactory(factory.django.DjangoModelFactory):
decision_date = factory.LazyAttribute(lambda _: fake.date_this_year()) decision_date = factory.LazyAttribute(lambda _: fake.date_this_year())
class InformationSecurityRegistryEntryFactory( class InformationSecurityRegistryEntryFactory(factory.django.DjangoModelFactory):
factory.django.DjangoModelFactory
):
class Meta: class Meta:
model = InformationSecurityRegistryEntry model = InformationSecurityRegistryEntry

View File

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

View File

@@ -193,8 +193,7 @@ class FormUploadContractsApiTest(APITestCase):
def test_upload_processing_error_contract(self): def test_upload_processing_error_contract(self):
for _, case in self.CASES.items(): for _, case in self.CASES.items():
with self.subTest(form=case["form"]): with self.subTest(form=case["form"]), patch(
with patch(
case["parse_target"], case["parse_target"],
side_effect=RuntimeError("parse failed"), side_effect=RuntimeError("parse failed"),
) as parse_mock: ) as parse_mock:

View File

@@ -183,9 +183,18 @@ class OrganizationAnalyticsApiTest(APITestCase):
self.assertEqual(response.data["insurance_contributions"]["amount"], 302000) self.assertEqual(response.data["insurance_contributions"]["amount"], 302000)
self.assertEqual(response.data["organization_id"], str(self.organization.id)) self.assertEqual(response.data["organization_id"], str(self.organization.id))
self.assertEqual(response.data["report_period"], {"year": 2026, "quarter": 1}) self.assertEqual(response.data["report_period"], {"year": 2026, "quarter": 1})
self.assertEqual(set(response.data["revenue"]), {"amount", "previous_amount", "delta_percent"}) self.assertEqual(
self.assertEqual(set(response.data["net_profit"]), {"amount", "previous_amount", "delta_percent"}) set(response.data["revenue"]),
self.assertEqual(set(response.data["taxes_paid"]), {"amount", "previous_amount", "delta_percent"}) {"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( self.assertEqual(
set(response.data["insurance_contributions"]), set(response.data["insurance_contributions"]),
{"amount", "previous_amount", "delta_percent"}, {"amount", "previous_amount", "delta_percent"},
@@ -225,7 +234,9 @@ class OrganizationAnalyticsApiTest(APITestCase):
"?report_year=2026&history_years=2" "?report_year=2026&history_years=2"
) )
self.assertEqual(personnel_response.status_code, status.HTTP_200_OK) 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["report_year"], 2026)
self.assertEqual( self.assertEqual(
personnel_response.data["headcount"]["average_employees"], personnel_response.data["headcount"]["average_employees"],
@@ -285,13 +296,21 @@ class OrganizationAnalyticsApiTest(APITestCase):
"?frequency=quarterly&price_mode=actual&report_year=2026" "?frequency=quarterly&price_mode=actual&report_year=2026"
) )
self.assertEqual(products_response.status_code, status.HTTP_200_OK) 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["report_year"], 2026)
self.assertEqual(products_response.data["frequency"], "quarterly") self.assertEqual(products_response.data["frequency"], "quarterly")
self.assertEqual(products_response.data["price_mode"], "actual") self.assertEqual(products_response.data["price_mode"], "actual")
self.assertEqual(products_response.data["summary"]["military_output_amount"], 11000000) self.assertEqual(
self.assertEqual(products_response.data["summary"]["civilian_output_amount"], 7000000) products_response.data["summary"]["military_output_amount"], 11000000
self.assertEqual(products_response.data["summary"]["hightech_output_amount"], 1500000) )
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"]["rd_volume_amount"], 900000)
self.assertEqual(len(products_response.data["production_series"]), 1) self.assertEqual(len(products_response.data["production_series"]), 1)
self.assertEqual(len(products_response.data["sales_series"]), 1) self.assertEqual(len(products_response.data["sales_series"]), 1)