Add v2 registry uploads and source CSV exports
All checks were successful
CI/CD Pipeline / Quality Gate (push) Successful in 20s
CI/CD Pipeline / Build and Push Images (push) Successful in 10s
CI/CD Pipeline / Internal Notify (push) Successful in 0s
CI/CD Pipeline / Deploy Dev in Dokploy (push) Successful in 1s

This commit is contained in:
2026-05-07 14:39:20 +02:00
parent 507ae2063a
commit 15360a3c8e
13 changed files with 639 additions and 8 deletions

View File

@@ -151,3 +151,25 @@ class ParserDashboardPageTest(TestCase):
self.assertIn("На конец</th>", content)
self.assertIn("На начало</th>", content)
self.assertIn("Покрытие доп. данными", content)
def test_dashboard_uses_v2_registry_upload_routes(self):
response = self.client.get("/dashboard")
self.assertEqual(response.status_code, 200)
content = response.content.decode()
self.assertIn("REGISTRY_UPLOAD_SLUGS_BY_NAME", content)
self.assertIn("/api/v2/registers/${registrySlug}/upload/", content)
self.assertIn("/api/v2/registers/opk/upload/", content)
self.assertIn("registryUploadUrlForSelectedRegistry", content)
def test_dashboard_exposes_v2_source_csv_downloads(self):
response = self.client.get("/dashboard")
self.assertEqual(response.status_code, 200)
content = response.content.decode()
self.assertIn("function sourceCsvDownloadUrl", content)
self.assertIn("/api/v2/sources/${source.api_route}/download/", content)
self.assertIn("data-source-download", content)
self.assertIn("downloadSourceCsv", content)
self.assertIn("CSV", content)
self.assertNotIn("/api/v2/sources/fns/reports/download/", content)

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import csv
import io
import os
import tempfile
@@ -99,6 +100,41 @@ class ParsersViewSetTest(APITestCase):
)
self.assertEqual(detail.status_code, status.HTTP_200_OK)
def test_v2_source_csv_download_exports_source_rows(self):
record = IndustrialCertificateRecordFactory(
certificate_number="CERT-CSV-1",
organisation_name='АО "CSV"',
inn="7701000101",
ogrn="1027700000001",
)
self.client.force_authenticate(self.user)
response = self.client.get(
reverse("api_v2:parser_sources:minpromtorg-certificates-download")
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response["Content-Type"], "text/csv; charset=utf-8")
self.assertIn(
'attachment; filename="minpromtorg-certificates.csv"',
response["Content-Disposition"],
)
rows = list(csv.DictReader(io.StringIO(response.content.decode("utf-8"))))
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0]["id"], str(record.id))
self.assertEqual(rows[0]["source"], ParserLoadLog.Source.INDUSTRIAL)
self.assertEqual(rows[0]["external_id"], "CERT-CSV-1")
self.assertEqual(rows[0]["organisation_name"], 'АО "CSV"')
self.assertEqual(rows[0]["inn"], "7701000101")
self.assertIn("CERT-CSV-1", rows[0]["payload"])
def test_v2_source_csv_download_is_not_registered_for_financial_reports(self):
self.client.force_authenticate(self.user)
response = self.client.get("/api/v2/sources/fns/reports/download/")
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_manufacturers_list_and_retrieve(self):
record = ManufacturerRecordFactory()
second_record = ManufacturerRecordFactory()
@@ -404,6 +440,10 @@ class ParsersViewSetTest(APITestCase):
sources["procurements_44fz"]["result_list_url"],
"/api/v1/parsers/results/procurements_44fz/",
)
self.assertEqual(
sources["procurements_44fz"]["api_route"],
"eis/procurements-44fz",
)
self.assertEqual(
sources["procurements_223fz"]["result_list_url"],
"/api/v1/parsers/results/procurements_223fz/",

View File

@@ -160,6 +160,100 @@ class RegisterImportServiceTest(TestCase):
self.assertEqual(existing.in_kpp, 444)
self.assertEqual(existing.mn_okpo, "87654321")
def test_parse_xlsx_accepts_opk_source_header_aliases(self):
upload = _upload(
"opk.xlsx",
[
[
"rn",
"okpo",
"ogrn",
"inn",
"filial",
"ropk_num",
"ropk_razdel_num",
"ropk_razdel_name",
"short_name",
"full_name",
],
[
1,
"07506197",
"1027600980990",
"7601000086",
"",
"1",
"1",
"Раздел",
'АО "ЯРЗ"',
'АКЦИОНЕРНОЕ ОБЩЕСТВО "ЯРОСЛАВСКИЙ РАДИОЗАВОД"',
],
],
)
rows = RegisterImportService.parse_xlsx(upload)
self.assertEqual(len(rows), 1)
self.assertEqual(
rows[0].pn_name,
'АКЦИОНЕРНОЕ ОБЩЕСТВО "ЯРОСЛАВСКИЙ РАДИОЗАВОД"',
)
self.assertEqual(rows[0].mn_ogrn, 1027600980990)
self.assertEqual(rows[0].mn_inn, 7601000086)
self.assertIsNone(rows[0].in_kpp)
self.assertEqual(rows[0].mn_okpo, "07506197")
def test_parse_xlsx_skips_opk_branch_rows_without_identity(self):
upload = _upload(
"opk.xlsx",
[
[
"rn",
"okpo",
"ogrn",
"inn",
"filial",
"ropk_num",
"ropk_razdel_num",
"ropk_razdel_name",
"short_name",
"full_name",
],
[
100,
"52511425",
None,
None,
True,
48,
1,
"Минпромторг России",
'Филиал ПАО "Ил" - ВАСО',
(
'Филиал публичного акционерного общества "Авиационный '
'комплекс им. С.В. Ильюшина" - ВАСО'
),
],
[
1,
"07506197",
"1027600980990",
"7601000086",
False,
1,
1,
"Минпромторг России",
'АО "ЯРЗ"',
'АКЦИОНЕРНОЕ ОБЩЕСТВО "ЯРОСЛАВСКИЙ РАДИОЗАВОД"',
],
],
)
rows = RegisterImportService.parse_xlsx(upload)
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0].mn_ogrn, 1027600980990)
def test_get_active_periods_by_org_returns_mapping(self):
registry = RegisterFactory()
active_period = RegistryMembershipPeriodFactory(

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import io
from datetime import date
from unittest.mock import patch
from apps.registers.models import Organization, RegistryMembershipPeriod
from django.core.files.uploadedfile import SimpleUploadedFile
@@ -94,6 +95,35 @@ class RegistersViewsTest(APITestCase):
self.client.force_authenticate(self.user)
return response
def _post_v2_slug_upload(
self,
*,
slug: str,
rows: list[dict],
actual_date_value: date,
with_kpp: bool = True,
file_name: str = "registry.xlsx",
):
self.client.force_authenticate(self.admin)
content = _build_register_excel_bytes(rows, with_kpp=with_kpp)
upload = SimpleUploadedFile(
file_name,
content,
content_type=(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
),
)
response = self.client.post(
reverse(f"api_v2:registers:register-upload-{slug}"),
{
"actual_date": actual_date_value.isoformat(),
"file": upload,
},
format="multipart",
)
self.client.force_authenticate(self.user)
return response
def test_registries_list_and_retrieve(self):
registry = RegisterFactory(name="Росатом")
RegisterUploadFactory(registry=registry)
@@ -130,6 +160,66 @@ class RegistersViewsTest(APITestCase):
self.assertIn("Реестр госкорпорации Росатом ГОЗ", names)
self.assertIn("Реестр госкорпорации Росатом ОПК", names)
def test_v2_registry_slug_upload_uses_fixed_registry_and_refreshes_snapshots(self):
rows = [
{
"pn_name": 'АО "Росатом ГОЗ"',
"mn_ogrn": "1027600980990",
"mn_inn": "7601000086",
"in_kpp": "760401001",
"mn_okpo": "07506197",
}
]
with patch("registers.views._start_snapshot_refresh_task") as refresh_task:
response = self._post_v2_slug_upload(
slug="rosatom-goz",
rows=rows,
actual_date_value=date(2026, 5, 7),
file_name="rosatom_goz.xlsx",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertTrue(response.data["success"])
self.assertEqual(
response.data["registry_name"],
"Реестр госкорпорации Росатом ГОЗ",
)
refresh_task.assert_called_once_with()
membership = RegistryMembershipPeriod.objects.get(ended_at__isnull=True)
self.assertEqual(
membership.registry.name,
"Реестр госкорпорации Росатом ГОЗ",
)
def test_v2_registry_slug_upload_url_does_not_duplicate_registers_segment(self):
self.assertEqual(
reverse("api_v2:registers:register-upload-rosatom-goz"),
"/api/v2/registers/rosatom-goz/upload/",
)
def test_v2_registry_slug_upload_does_not_refresh_snapshots_after_import_error(self):
rows = [
{
"pn_name": 'АО "Невалидный ОКПО"',
"mn_ogrn": "1027600980990",
"mn_inn": "7601000086",
"in_kpp": "760401001",
"mn_okpo": "07A06197",
}
]
with patch("registers.views._start_snapshot_refresh_task") as refresh_task:
response = self._post_v2_slug_upload(
slug="rosatom-goz",
rows=rows,
actual_date_value=date(2026, 5, 7),
file_name="invalid_rosatom_goz.xlsx",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
refresh_task.assert_not_called()
def test_organizations_list_and_retrieve(self):
organization = OrganizationFactory()