feat: export organization source records
Some checks failed
CI/CD Pipeline / Quality Gate (push) Failing after 9s
CI/CD Pipeline / Build and Push Images (push) Has been skipped
CI/CD Pipeline / Internal Notify (push) Successful in 1s
CI/CD Pipeline / Deploy Dev via Compose (push) Has been skipped

This commit is contained in:
2026-06-09 12:38:18 +02:00
parent 71eac688f2
commit 7aa67fc192
4 changed files with 613 additions and 1 deletions

View File

@@ -0,0 +1,233 @@
"""Tests for organization source-record ZIP export."""
import csv
import json
import zipfile
from io import BytesIO, StringIO
from django.urls import reverse
from openpyxl import load_workbook
from organizations.models import (
FinancialIndicatorsExtension,
Organization,
OrganizationSourceFinancialLine,
OrganizationSourceRecord,
PlannedInspectionExtension,
SourceGroup,
)
from rest_framework import status
from rest_framework.test import APITestCase
from tests.apps.user.factories import UserFactory
class OrganizationSourceRecordExportApiV2Test(APITestCase):
"""Checks admin-only source-record export contract."""
def setUp(self):
self.url = reverse(
"api_v2:organizations:organization-source-records-export",
)
def test_admin_exports_selected_sources_to_zip(self):
self.client.force_authenticate(UserFactory.create_superuser())
organization = Organization.objects.create(
name='ООО "Экспорт"',
full_name='Общество с ограниченной ответственностью "Экспорт"',
inn="7707083810",
ogrn="1027700132010",
kpp="770701001",
)
inspection_extension = PlannedInspectionExtension.objects.create(
organization=organization,
title="Плановые проверки Генпрокуратуры России",
)
OrganizationSourceRecord.objects.create(
extension=inspection_extension,
record_type="inspection",
source="inspections",
external_id="INSP-1",
title="Проверка экспорт",
payload={
"risk": {"score": 7},
"tags": ["плановая", "надзор"],
},
)
financial_extension = FinancialIndicatorsExtension.objects.create(
organization=organization,
title="Финансово-экономические показатели",
)
financial_record = OrganizationSourceRecord.objects.create(
extension=financial_extension,
record_type="financial_report",
source="fns_reports",
external_id="FIN-1",
title="Финансовый отчет",
payload={"period": 2025},
)
OrganizationSourceFinancialLine.objects.create(
source_record=financial_record,
form_code="1",
line_code="1600",
line_name="Баланс",
year=2025,
period_start=100,
period_end=200,
)
response = self.client.post(
self.url,
{
"sources": [
SourceGroup.PLANNED_INSPECTIONS.value,
SourceGroup.FINANCIAL_INDICATORS.value,
],
"format": "xlsx",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response["Content-Type"], "application/zip")
self.assertIn(
'filename="organization_source_records_export_',
response["Content-Disposition"],
)
with zipfile.ZipFile(BytesIO(response.content)) as archive:
self.assertEqual(
set(archive.namelist()),
{"planned-inspections.xlsx", "financial-indicators.json"},
)
workbook = load_workbook(
BytesIO(archive.read("planned-inspections.xlsx")),
read_only=True,
)
worksheet = workbook["data"]
rows = list(worksheet.iter_rows(values_only=True))
self.assertEqual(
rows[0][:4],
("Наименование", "ИНН", "ОГРН", "КПП"),
)
self.assertEqual(
rows[1][:4],
(
'Общество с ограниченной ответственностью "Экспорт"',
"7707083810",
"1027700132010",
"770701001",
),
)
self.assertIn("payload.risk.score", rows[0])
self.assertIn("payload.tags", rows[0])
financial_rows = json.loads(
archive.read("financial-indicators.json").decode("utf-8"),
)
self.assertEqual(financial_rows[0]["Наименование"], organization.full_name)
self.assertEqual(
financial_rows[0]["financial_lines"][0]["line_code"], "1600"
)
self.assertEqual(financial_rows[0]["financial_lines"][0]["period_end"], 200)
def test_csv_export_uses_bom_and_canonical_columns_before_payload(self):
self.client.force_authenticate(UserFactory.create_superuser())
organization = Organization.objects.create(
name='ООО "CSV"',
short_name='ООО "CSV"',
inn="7707083811",
ogrn="1027700132011",
kpp="770701002",
)
extension = PlannedInspectionExtension.objects.create(
organization=organization,
title="Плановые проверки Генпрокуратуры России",
)
OrganizationSourceRecord.objects.create(
extension=extension,
record_type="inspection",
source="inspections",
external_id="INSP-CSV",
title="CSV проверка",
payload={"nested": {"value": "данные"}},
)
response = self.client.post(
self.url,
{
"sources": [SourceGroup.PLANNED_INSPECTIONS.value],
"format": "csv",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
with zipfile.ZipFile(BytesIO(response.content)) as archive:
csv_bytes = archive.read("planned-inspections.csv")
self.assertTrue(csv_bytes.startswith(b"\xef\xbb\xbf"))
csv_text = csv_bytes.decode("utf-8-sig")
csv_rows = list(csv.reader(StringIO(csv_text)))
self.assertEqual(csv_rows[0][:4], ["Наименование", "ИНН", "ОГРН", "КПП"])
self.assertEqual(
csv_rows[1][:4], ['ООО "CSV"', "7707083811", "1027700132011", "770701002"]
)
self.assertIn("payload.nested.value", csv_rows[0])
def test_export_rejects_non_admin_user(self):
self.client.force_authenticate(UserFactory.create_user())
response = self.client.post(
self.url,
{
"sources": [SourceGroup.PLANNED_INSPECTIONS.value],
"format": "json",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_export_rejects_empty_duplicate_and_unknown_values(self):
self.client.force_authenticate(UserFactory.create_superuser())
empty_response = self.client.post(
self.url,
{"sources": [], "format": "json"},
format="json",
)
duplicate_response = self.client.post(
self.url,
{
"sources": [
SourceGroup.PLANNED_INSPECTIONS.value,
SourceGroup.PLANNED_INSPECTIONS.value,
],
"format": "json",
},
format="json",
)
unknown_source_response = self.client.post(
self.url,
{"sources": ["unknown"], "format": "json"},
format="json",
)
invalid_format_response = self.client.post(
self.url,
{
"sources": [SourceGroup.PLANNED_INSPECTIONS.value],
"format": "xml",
},
format="json",
)
self.assertEqual(empty_response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(duplicate_response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
unknown_source_response.status_code, status.HTTP_400_BAD_REQUEST
)
self.assertEqual(
invalid_format_response.status_code, status.HTTP_400_BAD_REQUEST
)