feat: export organization source records
This commit is contained in:
233
tests/apps/organizations/test_source_record_export.py
Normal file
233
tests/apps/organizations/test_source_record_export.py
Normal 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
|
||||
)
|
||||
Reference in New Issue
Block a user