1037 lines
41 KiB
Python
1037 lines
41 KiB
Python
"""Integration tests for parsers API views (no mocks)."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import csv
|
||
import io
|
||
import os
|
||
import tempfile
|
||
import zipfile
|
||
from unittest.mock import Mock, patch
|
||
|
||
from apps.parsers.models import (
|
||
FinancialReport,
|
||
FinancialReportLine,
|
||
GenericParserRecord,
|
||
ParserLoadLog,
|
||
ProcurementRecord,
|
||
)
|
||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||
from django.urls import reverse
|
||
from openpyxl import Workbook
|
||
from organizations.models import Organization
|
||
from rest_framework import status
|
||
from rest_framework.test import APITestCase
|
||
|
||
from tests.apps.parsers.factories import (
|
||
IndustrialCertificateRecordFactory,
|
||
IndustrialProductRecordFactory,
|
||
InspectionRecordFactory,
|
||
ManufacturerRecordFactory,
|
||
ParserLoadLogFactory,
|
||
)
|
||
from tests.apps.registers.factories import (
|
||
OrganizationFactory as RegisterOrganizationFactory,
|
||
)
|
||
from tests.apps.registers.factories import (
|
||
RegistryMembershipPeriodFactory,
|
||
)
|
||
from tests.apps.user.factories import UserFactory
|
||
from tests.utils.fixtures import fake
|
||
|
||
|
||
def _digits(length: int) -> str:
|
||
return "".join(str(fake.random_int(0, 9)) for _ in range(length))
|
||
|
||
|
||
def _build_fns_excel_bytes() -> bytes:
|
||
wb = Workbook()
|
||
ws = wb.active
|
||
year = fake.random_int(min=2020, max=2025)
|
||
ws.append(["Form", None, year, None])
|
||
ws.append([None, "Code", "Start", "End"])
|
||
ws.append(
|
||
[fake.word(), _digits(4), fake.random_int(10, 999), fake.random_int(10, 999)]
|
||
)
|
||
buf = io.BytesIO()
|
||
wb.save(buf)
|
||
wb.close()
|
||
return buf.getvalue()
|
||
|
||
|
||
def _build_fns_zip_bytes(file_map: dict[str, bytes]) -> bytes:
|
||
buf = io.BytesIO()
|
||
with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as archive:
|
||
for file_name, content in file_map.items():
|
||
archive.writestr(file_name, content)
|
||
return buf.getvalue()
|
||
|
||
|
||
def _create_procurement_record() -> ProcurementRecord:
|
||
return ProcurementRecord.objects.create(
|
||
load_batch=fake.random_int(min=1, max=1000),
|
||
purchase_number=_digits(19),
|
||
purchase_name=fake.sentence(nb_words=6),
|
||
customer_inn=_digits(10),
|
||
customer_kpp=_digits(9),
|
||
customer_ogrn=_digits(13),
|
||
customer_name=fake.company(),
|
||
max_price=str(fake.pydecimal(left_digits=7, right_digits=2, positive=True)),
|
||
status=fake.word(),
|
||
law_type="44-FZ",
|
||
href=fake.url(),
|
||
region_code=f"{fake.random_int(min=1, max=99):02d}",
|
||
)
|
||
|
||
|
||
class ParsersViewSetTest(APITestCase):
|
||
def setUp(self):
|
||
self.user = UserFactory.create_user()
|
||
self.admin = UserFactory.create_superuser()
|
||
|
||
def test_certificates_list_and_retrieve(self):
|
||
record = IndustrialCertificateRecordFactory()
|
||
self.client.force_authenticate(self.user)
|
||
url = reverse("api_v1:minpromtorg:certificates-list")
|
||
response = self.client.get(url)
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||
detail = self.client.get(
|
||
reverse("api_v1:minpromtorg:certificates-detail", args=[record.id])
|
||
)
|
||
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()
|
||
self.client.force_authenticate(self.user)
|
||
url = reverse("api_v1:minpromtorg:manufacturers-list")
|
||
response = self.client.get(url)
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||
detail = self.client.get(
|
||
reverse("api_v1:minpromtorg:manufacturers-detail", args=[record.id])
|
||
)
|
||
self.assertEqual(detail.status_code, status.HTTP_200_OK)
|
||
first_page_response = self.client.get(
|
||
"/api/v1/parsers/results/manufactures/",
|
||
{"page": 1, "page_size": 1},
|
||
)
|
||
second_page_response = self.client.get(
|
||
"/api/v1/parsers/results/manufactures/",
|
||
{"page": 2, "page_size": 1},
|
||
)
|
||
self.assertEqual(first_page_response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(second_page_response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(first_page_response.data["meta"]["pagination"]["count"], 2)
|
||
self.assertEqual(second_page_response.data["meta"]["pagination"]["count"], 2)
|
||
self.assertNotEqual(
|
||
first_page_response.data["data"][0]["id"],
|
||
second_page_response.data["data"][0]["id"],
|
||
)
|
||
self.assertEqual(
|
||
{
|
||
first_page_response.data["data"][0]["id"],
|
||
second_page_response.data["data"][0]["id"],
|
||
},
|
||
{record.id, second_record.id},
|
||
)
|
||
|
||
def test_products_list_and_retrieve(self):
|
||
record = IndustrialProductRecordFactory()
|
||
self.client.force_authenticate(self.user)
|
||
url = reverse("api_v1:minpromtorg:industrial-products-list")
|
||
response = self.client.get(url)
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||
detail = self.client.get(
|
||
reverse("api_v1:minpromtorg:industrial-products-detail", args=[record.id])
|
||
)
|
||
self.assertEqual(detail.status_code, status.HTTP_200_OK)
|
||
|
||
def test_inspections_list_and_retrieve(self):
|
||
record = InspectionRecordFactory()
|
||
self.client.force_authenticate(self.user)
|
||
url = reverse("api_v1:proverki:inspections-list")
|
||
response = self.client.get(url)
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||
detail = self.client.get(
|
||
reverse("api_v1:proverki:inspections-detail", args=[record.id])
|
||
)
|
||
self.assertEqual(detail.status_code, status.HTTP_200_OK)
|
||
|
||
def test_procurements_list_and_retrieve(self):
|
||
record = _create_procurement_record()
|
||
self.client.force_authenticate(self.user)
|
||
url = reverse("api_v1:zakupki:procurements-list")
|
||
response = self.client.get(url)
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||
detail = self.client.get(
|
||
reverse("api_v1:zakupki:procurements-detail", args=[record.id])
|
||
)
|
||
self.assertEqual(detail.status_code, status.HTTP_200_OK)
|
||
|
||
def test_procurements_v1_enriches_missing_customer_fields_from_canonical_organization(
|
||
self,
|
||
):
|
||
organization = Organization.objects.create(
|
||
name='ООО "КАНОНИЧЕСКИЙ ЗАКАЗЧИК"',
|
||
inn="7701000101",
|
||
kpp="770101001",
|
||
ogrn="1027700000001",
|
||
)
|
||
record = ProcurementRecord.objects.create(
|
||
load_batch=1,
|
||
purchase_number="0320100000126000001",
|
||
purchase_name="Поставка оборудования",
|
||
customer_inn="",
|
||
customer_kpp="",
|
||
customer_ogrn=organization.ogrn,
|
||
customer_name="",
|
||
max_price="1000.00",
|
||
status="published",
|
||
law_type="44-FZ",
|
||
href="https://zakupki.gov.ru/example",
|
||
region_code="77",
|
||
)
|
||
|
||
self.client.force_authenticate(self.user)
|
||
|
||
list_response = self.client.get(reverse("api_v1:zakupki:procurements-list"))
|
||
detail_response = self.client.get(
|
||
reverse("api_v1:zakupki:procurements-detail", args=[record.id])
|
||
)
|
||
|
||
self.assertEqual(list_response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(detail_response.status_code, status.HTTP_200_OK)
|
||
list_row = list_response.data["data"][0]
|
||
detail_row = detail_response.data
|
||
self.assertEqual(list_row["customer_name"], organization.name)
|
||
self.assertEqual(list_row["customer_inn"], organization.inn)
|
||
self.assertEqual(list_row["customer_kpp"], organization.kpp)
|
||
self.assertEqual(list_row["customer_ogrn"], organization.ogrn)
|
||
self.assertEqual(detail_row["customer_name"], organization.name)
|
||
self.assertEqual(detail_row["customer_inn"], organization.inn)
|
||
self.assertEqual(detail_row["customer_kpp"], organization.kpp)
|
||
self.assertEqual(detail_row["customer_ogrn"], organization.ogrn)
|
||
|
||
def test_procurements_v1_uses_customer_kpp_for_branch_enrichment(self):
|
||
head = Organization.objects.create(
|
||
name='ООО "КАНОНИЧЕСКИЙ ЗАКАЗЧИК"',
|
||
inn="7701000101",
|
||
kpp="770101001",
|
||
ogrn="1027700000001",
|
||
)
|
||
branch = Organization.objects.create(
|
||
name='ООО "КАНОНИЧЕСКИЙ ЗАКАЗЧИК" ФИЛИАЛ',
|
||
inn=head.inn,
|
||
kpp="780101001",
|
||
ogrn=head.ogrn,
|
||
)
|
||
record = ProcurementRecord.objects.create(
|
||
load_batch=1,
|
||
purchase_number="0320100000126000002",
|
||
purchase_name="Поставка оборудования",
|
||
customer_inn=head.inn,
|
||
customer_kpp=branch.kpp,
|
||
customer_ogrn=head.ogrn,
|
||
customer_name="",
|
||
max_price="1000.00",
|
||
status="published",
|
||
law_type="44-FZ",
|
||
href="https://zakupki.gov.ru/example",
|
||
region_code="77",
|
||
)
|
||
|
||
self.client.force_authenticate(self.user)
|
||
|
||
response = self.client.get(
|
||
reverse("api_v1:zakupki:procurements-detail", args=[record.id])
|
||
)
|
||
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(response.data["customer_name"], branch.name)
|
||
self.assertEqual(response.data["customer_kpp"], branch.kpp)
|
||
|
||
def test_procurements_v1_does_not_guess_branch_when_customer_kpp_is_missing(self):
|
||
head = Organization.objects.create(
|
||
name='ООО "КАНОНИЧЕСКИЙ ЗАКАЗЧИК"',
|
||
inn="7701000101",
|
||
kpp="770101001",
|
||
ogrn="1027700000001",
|
||
)
|
||
Organization.objects.create(
|
||
name='ООО "КАНОНИЧЕСКИЙ ЗАКАЗЧИК" ФИЛИАЛ',
|
||
inn=head.inn,
|
||
kpp="780101001",
|
||
ogrn=head.ogrn,
|
||
)
|
||
record = ProcurementRecord.objects.create(
|
||
load_batch=1,
|
||
purchase_number="0320100000126000003",
|
||
purchase_name="Поставка оборудования",
|
||
customer_inn=head.inn,
|
||
customer_kpp="",
|
||
customer_ogrn=head.ogrn,
|
||
customer_name="",
|
||
max_price="1000.00",
|
||
status="published",
|
||
law_type="44-FZ",
|
||
href="https://zakupki.gov.ru/example",
|
||
region_code="77",
|
||
)
|
||
|
||
self.client.force_authenticate(self.user)
|
||
|
||
response = self.client.get(
|
||
reverse("api_v1:zakupki:procurements-detail", args=[record.id])
|
||
)
|
||
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(response.data["customer_name"], "")
|
||
self.assertEqual(response.data["customer_kpp"], "")
|
||
|
||
def test_eis_result_endpoint_uses_generic_records_without_breaking_old_zakupki_api(
|
||
self,
|
||
):
|
||
old_record = _create_procurement_record()
|
||
generic_record = GenericParserRecord.objects.create(
|
||
load_batch=1,
|
||
source=ParserLoadLog.Source.PROCUREMENTS_44FZ,
|
||
external_id="eis-44fz-1",
|
||
inn=_digits(10),
|
||
organisation_name="EIS Customer",
|
||
title="EIS 44-FZ notice",
|
||
status="published",
|
||
payload={"registry": "44fz"},
|
||
)
|
||
self.client.force_authenticate(self.user)
|
||
|
||
old_response = self.client.get(reverse("api_v1:zakupki:procurements-list"))
|
||
new_response = self.client.get("/api/v1/eis/procurements-44fz/")
|
||
detail_response = self.client.get(
|
||
f"/api/v1/eis/procurements-44fz/{generic_record.id}/"
|
||
)
|
||
|
||
self.assertEqual(old_response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(old_response.data["data"][0]["id"], old_record.id)
|
||
self.assertEqual(new_response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(new_response.data["meta"]["pagination"]["count"], 1)
|
||
self.assertEqual(new_response.data["data"][0]["id"], generic_record.id)
|
||
self.assertEqual(detail_response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(detail_response.data["data"]["payload"], {"registry": "44fz"})
|
||
|
||
def test_parser_results_v1_enrich_missing_organization_fields_without_contract_change(
|
||
self,
|
||
):
|
||
organization = Organization.objects.create(
|
||
name='ООО "КАНОНИЧЕСКИЙ ПОСТАВЩИК"',
|
||
inn="7701000102",
|
||
kpp="770102001",
|
||
ogrn="1027700000002",
|
||
)
|
||
generic_record = GenericParserRecord.objects.create(
|
||
load_batch=1,
|
||
source=ParserLoadLog.Source.PROCUREMENTS_44FZ,
|
||
external_id="eis-44fz-enrichment",
|
||
inn="",
|
||
ogrn=organization.ogrn,
|
||
organisation_name="",
|
||
title="EIS notice",
|
||
status="published",
|
||
payload={"registry": "44fz"},
|
||
)
|
||
self.client.force_authenticate(self.user)
|
||
|
||
response = self.client.get("/api/v1/parsers/results/procurements_44fz/")
|
||
detail_response = self.client.get(
|
||
f"/api/v1/parsers/results/procurements_44fz/{generic_record.id}/"
|
||
)
|
||
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(detail_response.status_code, status.HTTP_200_OK)
|
||
row = response.data["data"][0]
|
||
detail = detail_response.data["data"]
|
||
self.assertEqual(row["organisation_name"], organization.name)
|
||
self.assertEqual(row["inn"], organization.inn)
|
||
self.assertEqual(row["ogrn"], organization.ogrn)
|
||
self.assertNotIn("kpp", row)
|
||
self.assertEqual(detail["organisation_name"], organization.name)
|
||
self.assertEqual(detail["inn"], organization.inn)
|
||
self.assertEqual(detail["ogrn"], organization.ogrn)
|
||
self.assertNotIn("kpp", detail)
|
||
|
||
def test_dashboard_data_exposes_source_groups_for_page(self):
|
||
GenericParserRecord.objects.create(
|
||
load_batch=1,
|
||
source=ParserLoadLog.Source.PROCUREMENTS_44FZ,
|
||
external_id="eis-44fz-1",
|
||
title="EIS 44-FZ notice 1",
|
||
payload={"registry": "44fz"},
|
||
)
|
||
GenericParserRecord.objects.create(
|
||
load_batch=1,
|
||
source=ParserLoadLog.Source.PROCUREMENTS_44FZ,
|
||
external_id="eis-44fz-2",
|
||
title="EIS 44-FZ notice 2",
|
||
payload={"registry": "44fz"},
|
||
)
|
||
GenericParserRecord.objects.create(
|
||
load_batch=1,
|
||
source=ParserLoadLog.Source.TRUDVSEM,
|
||
external_id="trudvsem-1",
|
||
title="Vacancy",
|
||
payload={"registry": "trudvsem"},
|
||
)
|
||
FinancialReport.objects.create(
|
||
external_id=_digits(5),
|
||
ogrn=_digits(13),
|
||
file_name=f"fin_{_digits(5)}_{_digits(13)}.xlsx",
|
||
file_hash=fake.sha256(raw_output=False),
|
||
load_batch=1,
|
||
status=FinancialReport.Status.SUCCESS,
|
||
source=FinancialReport.SourceType.API,
|
||
)
|
||
self.client.force_authenticate(self.user)
|
||
|
||
response = self.client.get("/api/v1/parsers/dashboard/")
|
||
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||
payload = response.data["data"]
|
||
self.assertIn("groups", payload)
|
||
self.assertIn("api", payload["groups"])
|
||
self.assertIn("uploads", payload["groups"])
|
||
self.assertEqual(payload["api_sources"], payload["groups"]["api"])
|
||
self.assertEqual(payload["file_sources"], payload["groups"]["uploads"])
|
||
sources = {item["key"]: item for item in payload["sources"]}
|
||
self.assertEqual(
|
||
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/",
|
||
)
|
||
self.assertEqual(
|
||
sources["contracts"]["result_list_url"],
|
||
"/api/v1/parsers/results/contracts/",
|
||
)
|
||
self.assertEqual(
|
||
payload["source_counts"][ParserLoadLog.Source.PROCUREMENTS_44FZ],
|
||
2,
|
||
)
|
||
self.assertEqual(payload["source_counts"][ParserLoadLog.Source.TRUDVSEM], 1)
|
||
self.assertEqual(payload["source_counts"][ParserLoadLog.Source.FNS_REPORTS], 1)
|
||
|
||
def test_dashboard_data_exposes_registry_data_coverage(self):
|
||
registry_organization = RegisterOrganizationFactory(
|
||
mn_inn=7720699480,
|
||
mn_ogrn=1107746880031,
|
||
)
|
||
RegistryMembershipPeriodFactory(organization=registry_organization)
|
||
FinancialReport.objects.create(
|
||
external_id=_digits(5),
|
||
ogrn=str(registry_organization.mn_ogrn),
|
||
file_name=f"fin_{_digits(5)}_{registry_organization.mn_ogrn}.xlsx",
|
||
file_hash=fake.sha256(raw_output=False),
|
||
load_batch=1,
|
||
status=FinancialReport.Status.SUCCESS,
|
||
source=FinancialReport.SourceType.API,
|
||
)
|
||
GenericParserRecord.objects.create(
|
||
load_batch=1,
|
||
source=ParserLoadLog.Source.UNFAIR_SUPPLIERS,
|
||
external_id="unfair-1",
|
||
title="Unfair supplier record",
|
||
inn=str(registry_organization.mn_inn),
|
||
ogrn=str(registry_organization.mn_ogrn),
|
||
)
|
||
self.client.force_authenticate(self.user)
|
||
|
||
response = self.client.get("/api/v1/parsers/dashboard/")
|
||
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||
coverage = response.data["data"]["registry_data_coverage"]
|
||
self.assertEqual(coverage["total_organizations"], 1)
|
||
items = {item["source"]: item for item in coverage["items"]}
|
||
self.assertEqual(items[ParserLoadLog.Source.FNS_REPORTS]["organizations_count"], 1)
|
||
self.assertEqual(items[ParserLoadLog.Source.FNS_REPORTS]["coverage_percent"], 100)
|
||
self.assertNotIn(ParserLoadLog.Source.UNFAIR_SUPPLIERS, items)
|
||
|
||
def test_dashboard_data_exposes_registry_enrichment_analytics(self):
|
||
rosatom_organization = RegisterOrganizationFactory(
|
||
mn_inn=7720699480,
|
||
mn_ogrn=1107746880031,
|
||
)
|
||
roscosmos_organization = RegisterOrganizationFactory(
|
||
mn_inn=7730239877,
|
||
mn_ogrn=1087302001797,
|
||
)
|
||
rosatom_membership = RegistryMembershipPeriodFactory(
|
||
organization=rosatom_organization
|
||
)
|
||
roscosmos_membership = RegistryMembershipPeriodFactory(
|
||
organization=roscosmos_organization
|
||
)
|
||
FinancialReport.objects.create(
|
||
external_id=_digits(5),
|
||
ogrn=str(rosatom_organization.mn_ogrn),
|
||
file_name=f"fin_{_digits(5)}_{rosatom_organization.mn_ogrn}.xlsx",
|
||
file_hash=fake.sha256(raw_output=False),
|
||
load_batch=1,
|
||
status=FinancialReport.Status.SUCCESS,
|
||
source=FinancialReport.SourceType.API,
|
||
)
|
||
IndustrialCertificateRecordFactory(
|
||
inn=str(rosatom_organization.mn_inn),
|
||
ogrn=str(rosatom_organization.mn_ogrn),
|
||
)
|
||
GenericParserRecord.objects.create(
|
||
load_batch=1,
|
||
source=ParserLoadLog.Source.UNFAIR_SUPPLIERS,
|
||
external_id="unfair-registry-analytics",
|
||
title="Risk signal",
|
||
inn=str(roscosmos_organization.mn_inn),
|
||
ogrn=str(roscosmos_organization.mn_ogrn),
|
||
)
|
||
GenericParserRecord.objects.create(
|
||
load_batch=1,
|
||
source=ParserLoadLog.Source.FEDRESURS_BANKRUPTCY,
|
||
external_id="bankruptcy-registry-analytics",
|
||
title="Bankruptcy risk signal",
|
||
inn=str(roscosmos_organization.mn_inn),
|
||
ogrn=str(roscosmos_organization.mn_ogrn),
|
||
)
|
||
InspectionRecordFactory(
|
||
inn=str(rosatom_organization.mn_inn),
|
||
ogrn=str(rosatom_organization.mn_ogrn),
|
||
)
|
||
self.client.force_authenticate(self.user)
|
||
|
||
response = self.client.get("/api/v1/parsers/dashboard/")
|
||
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||
analytics = response.data["data"]["registry_enrichment_analytics"]
|
||
self.assertEqual(
|
||
analytics["population"]["active_registry_organizations"],
|
||
2,
|
||
)
|
||
self.assertEqual(analytics["coverage_summary"]["with_any_enrichment"], 1)
|
||
self.assertEqual(analytics["coverage_summary"]["core_profile_complete"], 1)
|
||
self.assertEqual(analytics["coverage_summary"]["requires_attention"], 1)
|
||
source_coverage = {
|
||
item["source"]: item for item in analytics["source_coverage"]
|
||
}
|
||
self.assertEqual(
|
||
source_coverage[ParserLoadLog.Source.FNS_REPORTS][
|
||
"organizations_count"
|
||
],
|
||
1,
|
||
)
|
||
self.assertEqual(
|
||
source_coverage[ParserLoadLog.Source.INDUSTRIAL][
|
||
"organizations_count"
|
||
],
|
||
1,
|
||
)
|
||
self.assertNotIn(ParserLoadLog.Source.UNFAIR_SUPPLIERS, source_coverage)
|
||
self.assertNotIn(ParserLoadLog.Source.FEDRESURS_BANKRUPTCY, source_coverage)
|
||
self.assertNotIn(ParserLoadLog.Source.INSPECTIONS, source_coverage)
|
||
risk_signals = {item["source"]: item for item in analytics["risk_signals"]}
|
||
self.assertEqual(
|
||
risk_signals[ParserLoadLog.Source.UNFAIR_SUPPLIERS][
|
||
"organizations_count"
|
||
],
|
||
1,
|
||
)
|
||
self.assertEqual(
|
||
risk_signals[ParserLoadLog.Source.UNFAIR_SUPPLIERS]["risk_severity"],
|
||
"critical",
|
||
)
|
||
self.assertEqual(
|
||
risk_signals[ParserLoadLog.Source.FEDRESURS_BANKRUPTCY][
|
||
"risk_severity"
|
||
],
|
||
"critical",
|
||
)
|
||
self.assertEqual(
|
||
risk_signals[ParserLoadLog.Source.INSPECTIONS]["risk_severity"],
|
||
"soft",
|
||
)
|
||
self.assertEqual(
|
||
analytics["risk_summary"]["critical"]["organizations_count"],
|
||
1,
|
||
)
|
||
self.assertEqual(
|
||
analytics["risk_summary"]["soft"]["organizations_count"],
|
||
1,
|
||
)
|
||
matrix = {
|
||
item["registry_id"]: item
|
||
for item in analytics["registry_source_matrix"]
|
||
}
|
||
rosatom_matrix = matrix[str(rosatom_membership.registry_id)]
|
||
roscosmos_matrix = matrix[str(roscosmos_membership.registry_id)]
|
||
self.assertEqual(rosatom_matrix["active_organizations"], 1)
|
||
self.assertEqual(
|
||
rosatom_matrix["sources"][ParserLoadLog.Source.FNS_REPORTS][
|
||
"organizations_count"
|
||
],
|
||
1,
|
||
)
|
||
self.assertEqual(
|
||
roscosmos_matrix["sources"][ParserLoadLog.Source.FNS_REPORTS][
|
||
"organizations_count"
|
||
],
|
||
0,
|
||
)
|
||
total_matrix = analytics["registry_source_matrix"][-1]
|
||
self.assertEqual(total_matrix["registry_id"], "__all__")
|
||
self.assertEqual(total_matrix["registry_name"], "Все организации")
|
||
self.assertEqual(total_matrix["active_organizations"], 2)
|
||
self.assertEqual(
|
||
total_matrix["sources"][ParserLoadLog.Source.FNS_REPORTS][
|
||
"organizations_count"
|
||
],
|
||
1,
|
||
)
|
||
self.assertEqual(
|
||
total_matrix["sources"][ParserLoadLog.Source.FNS_REPORTS][
|
||
"coverage_percent"
|
||
],
|
||
50,
|
||
)
|
||
self.assertEqual(
|
||
total_matrix["sources"][ParserLoadLog.Source.INDUSTRIAL][
|
||
"organizations_count"
|
||
],
|
||
1,
|
||
)
|
||
self.assertEqual(
|
||
total_matrix["sources"][ParserLoadLog.Source.INDUSTRIAL][
|
||
"coverage_percent"
|
||
],
|
||
50,
|
||
)
|
||
|
||
def test_financial_reports_list_and_retrieve(self):
|
||
report = FinancialReport.objects.create(
|
||
external_id=_digits(5),
|
||
ogrn=_digits(13),
|
||
file_name=f"fin_{_digits(5)}_{_digits(13)}.xlsx",
|
||
file_hash=fake.sha256(raw_output=False),
|
||
load_batch=fake.random_int(min=1, max=1000),
|
||
status=FinancialReport.Status.SUCCESS,
|
||
source=FinancialReport.SourceType.API,
|
||
)
|
||
FinancialReportLine.objects.create(
|
||
report=report,
|
||
form_code="1",
|
||
line_code=_digits(4),
|
||
line_name=fake.word(),
|
||
year=fake.random_int(min=2020, max=2025),
|
||
period_start=fake.random_int(min=1, max=999),
|
||
period_end=fake.random_int(min=1, max=999),
|
||
)
|
||
self.client.force_authenticate(self.user)
|
||
url = reverse("api_v1:fns:fns-reports-list")
|
||
response = self.client.get(url)
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(response.data["data"][0]["lines_count"], 1)
|
||
alias_response = self.client.get(url, {"source": "fns_reports"})
|
||
self.assertEqual(alias_response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(alias_response.data["data"][0]["id"], report.id)
|
||
source_type_response = self.client.get(
|
||
url,
|
||
{"source": FinancialReport.SourceType.API},
|
||
)
|
||
self.assertEqual(source_type_response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(source_type_response.data["data"][0]["id"], report.id)
|
||
invalid_source_response = self.client.get(url, {"source": "unknown"})
|
||
self.assertEqual(invalid_source_response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(invalid_source_response.data["data"], [])
|
||
detail = self.client.get(
|
||
reverse("api_v1:fns:fns-reports-detail", args=[report.id])
|
||
)
|
||
self.assertEqual(detail.status_code, status.HTTP_200_OK)
|
||
self.assertIn("lines", detail.data)
|
||
unified_response = self.client.get("/api/v1/parsers/results/fns_financial/")
|
||
self.assertEqual(unified_response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(unified_response.data["meta"]["pagination"]["count"], 1)
|
||
self.assertEqual(unified_response.data["data"][0]["id"], report.id)
|
||
self.assertEqual(unified_response.data["data"][0]["title"], report.file_name)
|
||
|
||
def test_fns_financial_results_searches_and_orders_by_registry_organization(self):
|
||
alpha_org = RegisterOrganizationFactory(
|
||
pn_name='alpha "ФНС"',
|
||
mn_ogrn=1027700000001,
|
||
mn_inn=7701000001,
|
||
)
|
||
beta_org = RegisterOrganizationFactory(
|
||
pn_name='Beta "ФНС"',
|
||
mn_ogrn=1027700000002,
|
||
mn_inn=7701000002,
|
||
)
|
||
alpha_report = FinancialReport.objects.create(
|
||
external_id="fns-alpha",
|
||
ogrn=str(alpha_org.mn_ogrn),
|
||
registry_organization=alpha_org,
|
||
file_name=f"fin_fns-alpha_{alpha_org.mn_ogrn}.xlsx",
|
||
file_hash=fake.sha256(raw_output=False),
|
||
load_batch=1,
|
||
status=FinancialReport.Status.SUCCESS,
|
||
source=FinancialReport.SourceType.API,
|
||
)
|
||
beta_report = FinancialReport.objects.create(
|
||
external_id="fns-beta",
|
||
ogrn=str(beta_org.mn_ogrn),
|
||
registry_organization=beta_org,
|
||
file_name=f"fin_fns-beta_{beta_org.mn_ogrn}.xlsx",
|
||
file_hash=fake.sha256(raw_output=False),
|
||
load_batch=1,
|
||
status=FinancialReport.Status.SUCCESS,
|
||
source=FinancialReport.SourceType.API,
|
||
)
|
||
self.client.force_authenticate(self.user)
|
||
|
||
name_response = self.client.get(
|
||
"/api/v1/parsers/results/fns_financial/",
|
||
{"search": "alpha"},
|
||
)
|
||
inn_response = self.client.get(
|
||
"/api/v1/parsers/results/fns_financial/",
|
||
{"search": str(beta_org.mn_inn)},
|
||
)
|
||
ordered_response = self.client.get(
|
||
"/api/v1/parsers/results/fns_financial/",
|
||
{"ordering": "organisation_name"},
|
||
)
|
||
|
||
self.assertEqual(name_response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(
|
||
[item["id"] for item in name_response.data["data"]],
|
||
[alpha_report.id],
|
||
)
|
||
self.assertEqual(
|
||
name_response.data["data"][0]["organisation_name"],
|
||
alpha_org.pn_name,
|
||
)
|
||
self.assertEqual(name_response.data["data"][0]["inn"], str(alpha_org.mn_inn))
|
||
self.assertEqual(inn_response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(
|
||
[item["id"] for item in inn_response.data["data"]],
|
||
[beta_report.id],
|
||
)
|
||
self.assertEqual(ordered_response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(
|
||
[item["id"] for item in ordered_response.data["data"]],
|
||
[alpha_report.id, beta_report.id],
|
||
)
|
||
|
||
def test_eis_results_order_text_case_insensitively_and_by_amount(self):
|
||
low_amount = GenericParserRecord.objects.create(
|
||
load_batch=1,
|
||
source=ParserLoadLog.Source.PROCUREMENTS_44FZ,
|
||
external_id="amount-low",
|
||
organisation_name="alpha customer",
|
||
title="Low amount",
|
||
amount="10.00",
|
||
)
|
||
high_amount = GenericParserRecord.objects.create(
|
||
load_batch=1,
|
||
source=ParserLoadLog.Source.PROCUREMENTS_44FZ,
|
||
external_id="amount-high",
|
||
organisation_name="Beta customer",
|
||
title="High amount",
|
||
amount="20.00",
|
||
)
|
||
self.client.force_authenticate(self.user)
|
||
|
||
amount_response = self.client.get(
|
||
"/api/v1/eis/procurements-44fz/",
|
||
{"ordering": "amount"},
|
||
)
|
||
name_response = self.client.get(
|
||
"/api/v1/eis/procurements-44fz/",
|
||
{"ordering": "-organisation_name"},
|
||
)
|
||
|
||
self.assertEqual(amount_response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(
|
||
[item["id"] for item in amount_response.data["data"]],
|
||
[low_amount.id, high_amount.id],
|
||
)
|
||
self.assertEqual(name_response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(
|
||
[item["id"] for item in name_response.data["data"]],
|
||
[high_amount.id, low_amount.id],
|
||
)
|
||
|
||
def test_system_logs_admin_only(self):
|
||
log = ParserLoadLogFactory()
|
||
url_logs = reverse("api_v1:system:parser-logs-list")
|
||
|
||
response = self.client.get(url_logs)
|
||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||
|
||
self.client.force_authenticate(self.user)
|
||
response = self.client.get(url_logs)
|
||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||
|
||
self.client.force_authenticate(self.admin)
|
||
response = self.client.get(url_logs)
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||
detail = self.client.get(
|
||
reverse("api_v1:system:parser-logs-detail", args=[log.id])
|
||
)
|
||
self.assertEqual(detail.status_code, status.HTTP_200_OK)
|
||
|
||
def test_system_logs_support_search_and_organizations_count(self):
|
||
search_marker = "manufactures-unique-search-marker"
|
||
first_log = ParserLoadLogFactory(
|
||
source="manufactures",
|
||
batch_id=101,
|
||
status="success",
|
||
error_message=search_marker,
|
||
)
|
||
ParserLoadLogFactory(
|
||
source="inspections",
|
||
batch_id=202,
|
||
status="failed",
|
||
error_message="timeout",
|
||
)
|
||
ManufacturerRecordFactory(load_batch=101, inn="7701000001")
|
||
ManufacturerRecordFactory(load_batch=101, inn="7701000002")
|
||
|
||
self.client.force_authenticate(self.admin)
|
||
response = self.client.get(
|
||
reverse("api_v1:system:parser-logs-list"),
|
||
{"search": search_marker},
|
||
)
|
||
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||
rows = response.data["results"]
|
||
self.assertEqual(len(rows), 1)
|
||
self.assertEqual(rows[0]["id"], first_log.id)
|
||
self.assertEqual(rows[0]["organizations_count"], 2)
|
||
|
||
def test_system_logs_export_returns_csv(self):
|
||
ParserLoadLogFactory(
|
||
source="manufactures",
|
||
batch_id=333,
|
||
status="success",
|
||
records_count=4,
|
||
)
|
||
ManufacturerRecordFactory(load_batch=333, inn="7701000001")
|
||
|
||
self.client.force_authenticate(self.admin)
|
||
response = self.client.get(
|
||
reverse("api_v1:system:parser-logs-export"),
|
||
{"search": "333"},
|
||
)
|
||
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(response["Content-Type"], "text/csv; charset=utf-8")
|
||
content = response.content.decode("utf-8")
|
||
self.assertIn("organizations_count", content)
|
||
self.assertIn("333", content)
|
||
|
||
def test_system_logs_support_search_by_source_label(self):
|
||
ParserLoadLogFactory(
|
||
source="fns_reports",
|
||
batch_id=909,
|
||
status="success",
|
||
)
|
||
|
||
self.client.force_authenticate(self.admin)
|
||
response = self.client.get(
|
||
reverse("api_v1:system:parser-logs-list"),
|
||
{"search": "финансово"},
|
||
)
|
||
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(len(response.data["results"]), 1)
|
||
self.assertEqual(response.data["results"][0]["source"], "financial-indicators")
|
||
|
||
def test_fns_upload_invalid_filename(self):
|
||
self.client.force_authenticate(self.admin)
|
||
with tempfile.TemporaryDirectory() as tmpdir:
|
||
watch_dir = os.path.join(tmpdir, "watch")
|
||
processed_dir = os.path.join(tmpdir, "processed")
|
||
failed_dir = os.path.join(tmpdir, "failed")
|
||
content = _build_fns_excel_bytes()
|
||
upload = SimpleUploadedFile(
|
||
f"bad_{fake.random_int()}.xlsx",
|
||
content,
|
||
content_type=(
|
||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||
),
|
||
)
|
||
with self.settings(
|
||
FNS_WATCH_DIRECTORY=watch_dir,
|
||
FNS_PROCESSED_DIRECTORY=processed_dir,
|
||
FNS_FAILED_DIRECTORY=failed_dir,
|
||
):
|
||
url = reverse("api_v1:fns:fns-upload")
|
||
response = self.client.post(
|
||
url, {"files": [upload]}, format="multipart"
|
||
)
|
||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||
|
||
def test_fns_upload_forbidden_for_regular_user(self):
|
||
self.client.force_authenticate(self.user)
|
||
with tempfile.TemporaryDirectory() as tmpdir:
|
||
watch_dir = os.path.join(tmpdir, "watch")
|
||
processed_dir = os.path.join(tmpdir, "processed")
|
||
failed_dir = os.path.join(tmpdir, "failed")
|
||
content = _build_fns_excel_bytes()
|
||
upload = SimpleUploadedFile(
|
||
f"fin_{_digits(5)}_{_digits(13)}.xlsx",
|
||
content,
|
||
content_type=(
|
||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||
),
|
||
)
|
||
with self.settings(
|
||
FNS_WATCH_DIRECTORY=watch_dir,
|
||
FNS_PROCESSED_DIRECTORY=processed_dir,
|
||
FNS_FAILED_DIRECTORY=failed_dir,
|
||
):
|
||
url = reverse("api_v1:fns:fns-upload")
|
||
response = self.client.post(
|
||
url, {"files": [upload]}, format="multipart"
|
||
)
|
||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||
|
||
def test_fns_upload_accepts_single_file_payload(self):
|
||
self.client.force_authenticate(self.admin)
|
||
with tempfile.TemporaryDirectory() as tmpdir:
|
||
watch_dir = os.path.join(tmpdir, "watch")
|
||
processed_dir = os.path.join(tmpdir, "processed")
|
||
failed_dir = os.path.join(tmpdir, "failed")
|
||
content = _build_fns_excel_bytes()
|
||
upload = SimpleUploadedFile(
|
||
f"fin_{_digits(5)}_{_digits(13)}.xlsx",
|
||
content,
|
||
content_type=(
|
||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||
),
|
||
)
|
||
with self.settings(
|
||
FNS_WATCH_DIRECTORY=watch_dir,
|
||
FNS_PROCESSED_DIRECTORY=processed_dir,
|
||
FNS_FAILED_DIRECTORY=failed_dir,
|
||
):
|
||
url = reverse("api_v1:fns:fns-upload")
|
||
response = self.client.post(url, {"file": upload}, format="multipart")
|
||
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(response.data["success"], True)
|
||
self.assertIn("message", response.data)
|
||
|
||
def test_fns_upload_accepts_zip_payload(self):
|
||
self.client.force_authenticate(self.admin)
|
||
external_id = _digits(5)
|
||
ogrn = _digits(13)
|
||
filename = f"fin_{external_id}_{ogrn}.xlsx"
|
||
upload = SimpleUploadedFile(
|
||
"fns_reports.zip",
|
||
_build_fns_zip_bytes({filename: _build_fns_excel_bytes()}),
|
||
content_type="application/zip",
|
||
)
|
||
with tempfile.TemporaryDirectory() as tmpdir:
|
||
watch_dir = os.path.join(tmpdir, "watch")
|
||
processed_dir = os.path.join(tmpdir, "processed")
|
||
failed_dir = os.path.join(tmpdir, "failed")
|
||
with self.settings(
|
||
FNS_WATCH_DIRECTORY=watch_dir,
|
||
FNS_PROCESSED_DIRECTORY=processed_dir,
|
||
FNS_FAILED_DIRECTORY=failed_dir,
|
||
):
|
||
url = reverse("api_v1:fns:fns-upload")
|
||
response = self.client.post(url, {"file": upload}, format="multipart")
|
||
|
||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||
self.assertEqual(response.data["queued"], 1)
|
||
self.assertEqual(response.data["skipped"], 0)
|
||
self.assertEqual(response.data["invalid"], 0)
|
||
self.assertEqual(FinancialReport.objects.count(), 1)
|
||
|
||
def test_parsing_settings_get_and_patch(self):
|
||
self.client.force_authenticate(self.admin)
|
||
url = reverse("api_v1:parsing:parsing-settings")
|
||
|
||
initial = self.client.get(url)
|
||
self.assertEqual(initial.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(initial.data["planned_inspections"], "monthly")
|
||
|
||
updated = self.client.patch(
|
||
url,
|
||
{"planned_inspections": "weekly"},
|
||
format="json",
|
||
)
|
||
|
||
self.assertEqual(updated.status_code, status.HTTP_200_OK)
|
||
self.assertEqual(updated.data["planned_inspections"], "weekly")
|
||
|
||
def test_run_sync_inspections_accepts_limited_sync_params(self):
|
||
self.client.force_authenticate(self.user)
|
||
url = reverse("api_v1:parsers:run-parser", args=["sync_inspections"])
|
||
payload = {
|
||
"max_months_per_law": 1,
|
||
"start_year": 2026,
|
||
"start_month": 4,
|
||
"include_fz294": True,
|
||
"include_fz248": False,
|
||
"current_year": 2026,
|
||
"current_month": 4,
|
||
}
|
||
|
||
with patch(
|
||
"apps.parsers.views.tasks.sync_inspections.apply_async",
|
||
return_value=Mock(id="task-123"),
|
||
) as apply_async_mock:
|
||
response = self.client.post(url, payload, format="json")
|
||
|
||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||
task_kwargs = apply_async_mock.call_args.kwargs["kwargs"]
|
||
for key, value in payload.items():
|
||
self.assertEqual(task_kwargs[key], value)
|
||
self.assertEqual(task_kwargs["requested_by_id"], self.user.id)
|