feat(parsers): implement arbitration and upload fixes
This commit is contained in:
@@ -802,6 +802,9 @@ class GenericParserRecordService(BulkOperationsMixin, BaseService[GenericParserR
|
||||
external_id__in=unique_records.keys(),
|
||||
).values_list("external_id", flat=True)
|
||||
)
|
||||
registry_lookup = RegistryOrganizationResolver.build_lookup(
|
||||
[(record.inn, record.ogrn) for record in unique_records.values()]
|
||||
)
|
||||
instances = [
|
||||
cls.model(
|
||||
load_batch=batch_id,
|
||||
@@ -816,6 +819,11 @@ class GenericParserRecordService(BulkOperationsMixin, BaseService[GenericParserR
|
||||
status=record.status,
|
||||
url=record.url,
|
||||
payload=record.payload,
|
||||
registry_organization_id=RegistryOrganizationResolver.resolve_organization_id(
|
||||
lookup=registry_lookup,
|
||||
inn=record.inn,
|
||||
ogrn=record.ogrn,
|
||||
),
|
||||
)
|
||||
for external_id, record in unique_records.items()
|
||||
if external_id not in existing_external_ids
|
||||
|
||||
@@ -234,7 +234,12 @@ PARSER_SOURCES: dict[str, ParserSourceDescriptor] = {
|
||||
status="implemented",
|
||||
upstream_url="https://kad.arbitr.ru/",
|
||||
access_method="official_search_api",
|
||||
parser_strategy="kad_arbitr_search",
|
||||
parser_strategy="checko_legal_cases_by_inn_ogrn",
|
||||
source_notes=(
|
||||
"Поиск дел выполняется по ИНН/ОГРН организаций из реестров; "
|
||||
"если реестр пустой, используются ИНН из уже загруженных источников. "
|
||||
"Checko отдаёт карточки со ссылками на КАД Арбитр."
|
||||
),
|
||||
api_route="arbitration/cases",
|
||||
),
|
||||
"fedresurs_bankruptcy": ParserSourceDescriptor(
|
||||
|
||||
@@ -10,13 +10,15 @@ import logging
|
||||
import shutil
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
from apps.core.services import BackgroundJobService
|
||||
from apps.core.tasks import PeriodicTask as CorePeriodicTask
|
||||
from apps.parsers.clients.base import HTTPClientError
|
||||
from apps.parsers.clients.checko import CheckoClient, CompanyRequest
|
||||
from apps.parsers.clients.checko import CheckoClient, CompanyRequest, LegalCasesRequest
|
||||
from apps.parsers.clients.checko.exceptions import CheckoError
|
||||
from apps.parsers.clients.common import GenericParserItem, StructuredDataClient
|
||||
from apps.parsers.clients.minpromtorg import (
|
||||
@@ -27,7 +29,16 @@ from apps.parsers.clients.minpromtorg import (
|
||||
from apps.parsers.clients.proverki import ProverkiClient
|
||||
from apps.parsers.clients.trudvsem import TrudvsemClient
|
||||
from apps.parsers.clients.zakupki import ZakupkiClient
|
||||
from apps.parsers.models import ParserLoadLog
|
||||
from apps.parsers.models import (
|
||||
FinancialReport,
|
||||
GenericParserRecord,
|
||||
IndustrialCertificateRecord,
|
||||
IndustrialProductRecord,
|
||||
InspectionRecord,
|
||||
ManufacturerRecord,
|
||||
ParserLoadLog,
|
||||
ProcurementRecord,
|
||||
)
|
||||
from apps.parsers.services import (
|
||||
FNSReportService,
|
||||
GenericParserRecordService,
|
||||
@@ -56,6 +67,7 @@ STRUCTURED_SOURCE_OPTIONS = {
|
||||
"fedresurs_bankruptcy": {"timeout": 30},
|
||||
}
|
||||
FEDRESURS_CHECKO_FALLBACK_LIMIT = 100
|
||||
ARBITRATION_CHECKO_LIMIT = 100
|
||||
PARSER_STALE_LOAD_MAX_AGE_MINUTES = 90
|
||||
PARSER_SOFT_TIME_LIMIT_SECONDS = 15 * 60
|
||||
PARSER_TIME_LIMIT_SECONDS = 20 * 60
|
||||
@@ -392,6 +404,320 @@ def _fedresurs_external_id(
|
||||
return f"checko-fedresurs:{digest}"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ArbitrationSubject:
|
||||
"""Организация, по которой можно запросить арбитражные дела."""
|
||||
|
||||
inn: str
|
||||
ogrn: str
|
||||
name: str
|
||||
registry_organization_id: int | None = None
|
||||
|
||||
|
||||
def _normalize_identifier(value) -> str:
|
||||
"""Вернуть только цифровой идентификатор или пустую строку."""
|
||||
if value is None:
|
||||
return ""
|
||||
value = str(value).strip()
|
||||
return value if value.isdigit() else ""
|
||||
|
||||
|
||||
def _arbitration_subject_key(subject: ArbitrationSubject) -> str:
|
||||
"""Стабильный ключ субъекта для дедупликации запросов."""
|
||||
return subject.inn or subject.ogrn
|
||||
|
||||
|
||||
def _add_arbitration_subject(
|
||||
subjects: dict[str, ArbitrationSubject],
|
||||
*,
|
||||
inn,
|
||||
ogrn,
|
||||
name,
|
||||
registry_organization_id: int | None = None,
|
||||
limit: int,
|
||||
) -> None:
|
||||
"""Добавить subject, если по нему есть ИНН/ОГРН и лимит не исчерпан."""
|
||||
if len(subjects) >= limit:
|
||||
return
|
||||
|
||||
subject = ArbitrationSubject(
|
||||
inn=_normalize_identifier(inn),
|
||||
ogrn=_normalize_identifier(ogrn),
|
||||
name=str(name or "").strip(),
|
||||
registry_organization_id=registry_organization_id,
|
||||
)
|
||||
key = _arbitration_subject_key(subject)
|
||||
if key:
|
||||
subjects.setdefault(key, subject)
|
||||
|
||||
|
||||
def _extend_arbitration_subjects_from_model(
|
||||
subjects: dict[str, ArbitrationSubject],
|
||||
*,
|
||||
model,
|
||||
limit: int,
|
||||
inn_field: str | None,
|
||||
ogrn_field: str | None,
|
||||
name_field: str | None,
|
||||
) -> None:
|
||||
"""Добрать организации из уже загруженных parser-таблиц."""
|
||||
if len(subjects) >= limit:
|
||||
return
|
||||
|
||||
fields = [
|
||||
field
|
||||
for field in (inn_field, ogrn_field, name_field, "registry_organization_id")
|
||||
if field
|
||||
]
|
||||
queryset = model.objects.all().order_by("-updated_at")
|
||||
if inn_field:
|
||||
queryset = queryset.exclude(**{inn_field: ""})
|
||||
elif ogrn_field:
|
||||
queryset = queryset.exclude(**{ogrn_field: ""})
|
||||
|
||||
# Берем небольшой запас: в источниках часто несколько строк на один ИНН.
|
||||
row_limit = max((limit - len(subjects)) * 5, 10)
|
||||
for row in queryset.values(*fields)[:row_limit]:
|
||||
_add_arbitration_subject(
|
||||
subjects,
|
||||
inn=row.get(inn_field) if inn_field else "",
|
||||
ogrn=row.get(ogrn_field) if ogrn_field else "",
|
||||
name=row.get(name_field) if name_field else "",
|
||||
registry_organization_id=row.get("registry_organization_id"),
|
||||
limit=limit,
|
||||
)
|
||||
if len(subjects) >= limit:
|
||||
return
|
||||
|
||||
|
||||
def _arbitration_subjects(limit: int) -> list[ArbitrationSubject]:
|
||||
"""Собрать организации для арбитражного lookup."""
|
||||
subjects: dict[str, ArbitrationSubject] = {}
|
||||
|
||||
for organization in Organization.objects.order_by("pn_name").values(
|
||||
"id",
|
||||
"mn_inn",
|
||||
"mn_ogrn",
|
||||
"pn_name",
|
||||
)[:limit]:
|
||||
_add_arbitration_subject(
|
||||
subjects,
|
||||
inn=organization["mn_inn"],
|
||||
ogrn=organization["mn_ogrn"],
|
||||
name=organization["pn_name"],
|
||||
registry_organization_id=organization["id"],
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
fallbacks = (
|
||||
(ManufacturerRecord, "inn", "ogrn", "full_legal_name"),
|
||||
(IndustrialCertificateRecord, "inn", "ogrn", "organisation_name"),
|
||||
(IndustrialProductRecord, "inn", "ogrn", "full_organisation_name"),
|
||||
(InspectionRecord, "inn", "ogrn", "organisation_name"),
|
||||
(ProcurementRecord, "customer_inn", "customer_ogrn", "customer_name"),
|
||||
(GenericParserRecord, "inn", "ogrn", "organisation_name"),
|
||||
(FinancialReport, None, "ogrn", None),
|
||||
)
|
||||
for model, inn_field, ogrn_field, name_field in fallbacks:
|
||||
_extend_arbitration_subjects_from_model(
|
||||
subjects,
|
||||
model=model,
|
||||
inn_field=inn_field,
|
||||
ogrn_field=ogrn_field,
|
||||
name_field=name_field,
|
||||
limit=limit,
|
||||
)
|
||||
if len(subjects) >= limit:
|
||||
break
|
||||
|
||||
return list(subjects.values())
|
||||
|
||||
|
||||
def _resolve_arbitration_limit(limit: int | None) -> int:
|
||||
if limit is None:
|
||||
limit = getattr(settings, "ARBITRATION_CHECKO_LIMIT", ARBITRATION_CHECKO_LIMIT)
|
||||
try:
|
||||
resolved = int(limit)
|
||||
except (TypeError, ValueError):
|
||||
resolved = ARBITRATION_CHECKO_LIMIT
|
||||
return max(resolved, 0)
|
||||
|
||||
|
||||
def _fetch_checko_arbitration_records(
|
||||
*,
|
||||
limit: int | None,
|
||||
proxies: list[str] | None,
|
||||
) -> list[GenericParserItem]:
|
||||
"""Получить арбитражные дела по ИНН/ОГРН через Checko legal-cases API."""
|
||||
api_key = getattr(settings, "CHECKO_API_KEY", "")
|
||||
if not api_key:
|
||||
raise ParserSourceSkipped("CHECKO_API_KEY is empty; arbitration parser skipped")
|
||||
|
||||
resolved_limit = _resolve_arbitration_limit(limit)
|
||||
if resolved_limit <= 0:
|
||||
logger.info("Arbitration Checko parser is disabled by limit=%s", limit)
|
||||
return []
|
||||
|
||||
subjects = _arbitration_subjects(resolved_limit)
|
||||
if not subjects:
|
||||
raise ParserSourceSkipped(
|
||||
"no registry organizations or parser identifiers found for arbitration"
|
||||
)
|
||||
|
||||
checko_proxies = (
|
||||
proxies if getattr(settings, "CHECKO_USE_RUNTIME_PROXIES", False) else None
|
||||
)
|
||||
client = CheckoClient(api_key=api_key, proxies=checko_proxies, timeout=30)
|
||||
records: list[GenericParserItem] = []
|
||||
failed_lookups = 0
|
||||
for subject in subjects:
|
||||
try:
|
||||
request = LegalCasesRequest(
|
||||
inn=subject.inn or None,
|
||||
ogrn=subject.ogrn or None,
|
||||
limit=100,
|
||||
)
|
||||
for legal_case in client.iter_legal_cases(request):
|
||||
records.append(_checko_arbitration_item(legal_case, subject=subject))
|
||||
except CheckoError as exc:
|
||||
failed_lookups += 1
|
||||
logger.info(
|
||||
"Checko arbitration lookup skipped for subject=%s: %s",
|
||||
_arbitration_subject_key(subject),
|
||||
exc,
|
||||
)
|
||||
|
||||
if failed_lookups == len(subjects) and not records:
|
||||
raise ParserSourceSkipped("Checko arbitration lookups failed for all subjects")
|
||||
|
||||
logger.info(
|
||||
"Fetched %d arbitration records through Checko for %d subjects",
|
||||
len(records),
|
||||
len(subjects),
|
||||
)
|
||||
return records
|
||||
|
||||
|
||||
def _checko_arbitration_item(case, *, subject: ArbitrationSubject) -> GenericParserItem:
|
||||
"""Преобразовать дело Checko в generic record."""
|
||||
case_number = getattr(case, "case_number", "") or ""
|
||||
filing_date = getattr(case, "filing_date", "") or ""
|
||||
role = _case_role_for_subject(case, subject)
|
||||
external_id = _arbitration_external_id(
|
||||
subject_key=_arbitration_subject_key(subject),
|
||||
case_number=case_number,
|
||||
filing_date=filing_date,
|
||||
role=role,
|
||||
)
|
||||
title = case_number or getattr(case, "category", "") or "Арбитражное дело"
|
||||
amount = _decimal_or_none(getattr(case, "claim_amount", None))
|
||||
return GenericParserItem(
|
||||
source="arbitration",
|
||||
external_id=external_id,
|
||||
inn=subject.inn,
|
||||
ogrn=subject.ogrn,
|
||||
organisation_name=subject.name,
|
||||
title=title,
|
||||
record_date=filing_date or getattr(case, "result_date", "") or "",
|
||||
amount=amount,
|
||||
status=getattr(case, "status", "") or "",
|
||||
url=getattr(case, "url", "") or "",
|
||||
payload={
|
||||
"provider": "checko",
|
||||
"declared_source": "КАД Арбитр",
|
||||
"target": {
|
||||
"inn": subject.inn,
|
||||
"ogrn": subject.ogrn,
|
||||
"name": subject.name,
|
||||
"registry_organization_id": subject.registry_organization_id,
|
||||
"role": role,
|
||||
},
|
||||
"case_number": case_number,
|
||||
"court_name": getattr(case, "court_name", None),
|
||||
"type": getattr(case, "type", None),
|
||||
"category": getattr(case, "category", None),
|
||||
"status": getattr(case, "status", None),
|
||||
"filing_date": filing_date,
|
||||
"result_date": getattr(case, "result_date", None),
|
||||
"claim_amount": getattr(case, "claim_amount", None),
|
||||
"awarded_amount": getattr(case, "awarded_amount", None),
|
||||
"plaintiffs": _case_parties_payload(getattr(case, "plaintiffs", ())),
|
||||
"defendants": _case_parties_payload(getattr(case, "defendants", ())),
|
||||
"third_parties": _case_parties_payload(getattr(case, "third_parties", ())),
|
||||
"instances": _case_instances_payload(getattr(case, "instances", ())),
|
||||
"url": getattr(case, "url", None),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _decimal_or_none(value) -> Decimal | None:
|
||||
if value in (None, ""):
|
||||
return None
|
||||
try:
|
||||
return Decimal(str(value))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _case_role_for_subject(case, subject: ArbitrationSubject) -> str:
|
||||
party_groups = (
|
||||
("plaintiff", getattr(case, "plaintiffs", ())),
|
||||
("defendant", getattr(case, "defendants", ())),
|
||||
("third_party", getattr(case, "third_parties", ())),
|
||||
)
|
||||
for role, parties in party_groups:
|
||||
for party in parties or ():
|
||||
if _party_matches_subject(party, subject):
|
||||
return role
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _party_matches_subject(party, subject: ArbitrationSubject) -> bool:
|
||||
party_inn = _normalize_identifier(getattr(party, "inn", None))
|
||||
party_ogrn = _normalize_identifier(getattr(party, "ogrn", None))
|
||||
return bool(
|
||||
(subject.inn and party_inn == subject.inn)
|
||||
or (subject.ogrn and party_ogrn == subject.ogrn)
|
||||
)
|
||||
|
||||
|
||||
def _case_parties_payload(parties) -> list[dict]:
|
||||
return [
|
||||
{
|
||||
"name": getattr(party, "name", None),
|
||||
"inn": getattr(party, "inn", None),
|
||||
"ogrn": getattr(party, "ogrn", None),
|
||||
"role": getattr(party, "role", None),
|
||||
}
|
||||
for party in parties or ()
|
||||
]
|
||||
|
||||
|
||||
def _case_instances_payload(instances) -> list[dict]:
|
||||
return [
|
||||
{
|
||||
"number": getattr(instance, "number", None),
|
||||
"court_name": getattr(instance, "court_name", None),
|
||||
"judge": getattr(instance, "judge", None),
|
||||
"result": getattr(instance, "result", None),
|
||||
"date": getattr(instance, "date", None),
|
||||
}
|
||||
for instance in instances or ()
|
||||
]
|
||||
|
||||
|
||||
def _arbitration_external_id(
|
||||
*,
|
||||
subject_key: str,
|
||||
case_number: str,
|
||||
filing_date: str,
|
||||
role: str,
|
||||
) -> str:
|
||||
raw = f"{subject_key}:{case_number}:{filing_date}:{role}"
|
||||
digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()[:24]
|
||||
return f"checko-arbitration:{digest}"
|
||||
|
||||
|
||||
@shared_task(bind=True, base=CorePeriodicTask)
|
||||
def sync_ru_proxies(self) -> dict[str, int | str]: # noqa: ARG001
|
||||
"""Периодически загружать RU-прокси из Proxy-Tools."""
|
||||
@@ -1860,23 +2186,37 @@ def parse_arbitration_cases(
|
||||
*,
|
||||
file_url: str | None = None,
|
||||
file_path: str | None = None,
|
||||
limit: int | None = None,
|
||||
proxies: list[str] | None = None,
|
||||
requested_by_id: int | None = None,
|
||||
) -> dict:
|
||||
"""Парсинг выдачи КАД/арбитража в generic storage."""
|
||||
"""Парсинг арбитражных дел по организациям в generic storage."""
|
||||
proxies = _resolve_proxies(proxies)
|
||||
if file_url or file_path:
|
||||
|
||||
def fetch_records():
|
||||
return _fetch_structured_records(
|
||||
source_key="arbitration",
|
||||
file_url=file_url,
|
||||
file_path=file_path,
|
||||
proxies=proxies,
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
def fetch_records():
|
||||
return _fetch_checko_arbitration_records(
|
||||
limit=limit,
|
||||
proxies=proxies,
|
||||
)
|
||||
|
||||
return _run_generic_parser(
|
||||
self,
|
||||
source_key="arbitration",
|
||||
source=ParserLoadLog.Source.ARBITRATION,
|
||||
task_name="apps.parsers.tasks.parse_arbitration_cases",
|
||||
requested_by_id=requested_by_id,
|
||||
fetch_records=lambda: _fetch_structured_records(
|
||||
source_key="arbitration",
|
||||
file_url=file_url,
|
||||
file_path=file_path,
|
||||
proxies=proxies,
|
||||
),
|
||||
fetch_records=fetch_records,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ from apps.parsers.serializers import (
|
||||
FinancialReportSerializer,
|
||||
FNSFileUploadSerializer,
|
||||
FNSFileUploadSuccessSerializer,
|
||||
FNSZipUploadSerializer,
|
||||
GenericParserRecordSerializer,
|
||||
IndustrialCertificateSerializer,
|
||||
IndustrialProductSerializer,
|
||||
@@ -172,6 +173,13 @@ TRUDVSEM_PARAMS = {
|
||||
"requested_by_id",
|
||||
}
|
||||
GENERIC_FILE_PARAMS = {"file_url", "file_path", "proxies", "requested_by_id"}
|
||||
ARBITRATION_PARAMS = {
|
||||
"file_url",
|
||||
"file_path",
|
||||
"limit",
|
||||
"proxies",
|
||||
"requested_by_id",
|
||||
}
|
||||
PAGE_PARAM = openapi.Parameter(
|
||||
"page",
|
||||
openapi.IN_QUERY,
|
||||
@@ -936,6 +944,28 @@ class FNSReportUploadView(APIView):
|
||||
},
|
||||
)
|
||||
def post(self, request): # noqa
|
||||
uploaded_file = request.FILES.get("file")
|
||||
if uploaded_file and uploaded_file.name.lower().endswith(".zip"):
|
||||
serializer = FNSZipUploadSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
try:
|
||||
result = FNSUploadService.queue_zip_archive(
|
||||
archive_file=serializer.validated_data["file"],
|
||||
requested_by_id=request.user.id,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise ValidationError({"file": str(exc)}) from exc
|
||||
|
||||
return Response(
|
||||
{
|
||||
"queued": result.queued,
|
||||
"skipped": result.skipped,
|
||||
"invalid": result.invalid,
|
||||
"task_ids": result.task_ids,
|
||||
},
|
||||
status=status.HTTP_202_ACCEPTED,
|
||||
)
|
||||
|
||||
serializer = FNSFileUploadSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
@@ -1307,6 +1337,8 @@ class ParserLoadLogExportView(APIView):
|
||||
def _allowed_task_params(source_key: str) -> set[str]:
|
||||
if source_key in EXISTING_TASK_PARAMS:
|
||||
return EXISTING_TASK_PARAMS[source_key]
|
||||
if source_key == "arbitration":
|
||||
return ARBITRATION_PARAMS
|
||||
if source_key == "trudvsem":
|
||||
return TRUDVSEM_PARAMS
|
||||
return GENERIC_FILE_PARAMS
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
</div>
|
||||
<span id="uploadCount" class="badge"></span>
|
||||
</div>
|
||||
<div id="manualUploadStatus" class="empty-state hidden"></div>
|
||||
<div id="uploadSources" class="source-list"></div>
|
||||
</section>
|
||||
|
||||
@@ -870,6 +871,7 @@
|
||||
<div class="row">
|
||||
<button type="submit">Загрузить организации ОПК</button>
|
||||
<span class="muted">Запрос уйдет в <code>/api/v1/registers/upload/</code></span>
|
||||
<span id="registryUploadStatus" class="muted"></span>
|
||||
</div>
|
||||
</form>
|
||||
<div id="registrySummary" class="source-list"></div>
|
||||
@@ -1987,9 +1989,16 @@
|
||||
const formData = new FormData(event.target);
|
||||
const file = formData.get("file");
|
||||
if (!file || !file.name) return;
|
||||
$("registryUploadStatus").textContent = "Загрузка...";
|
||||
try {
|
||||
await apiUpload("/api/v1/registers/upload/", formData);
|
||||
event.target.querySelector('input[type="file"]').value = "";
|
||||
$("registryUploadStatus").textContent = "Файл загружен";
|
||||
await refreshDashboard();
|
||||
showMainTab("uploads");
|
||||
} catch (error) {
|
||||
$("registryUploadStatus").textContent = errorMessage(error);
|
||||
}
|
||||
});
|
||||
|
||||
$("backupExportForm").addEventListener("submit", async (event) => {
|
||||
@@ -2121,9 +2130,20 @@
|
||||
formData.append("file", fileInput.files[0]);
|
||||
const source = sourceByKey(target.dataset.upload);
|
||||
const uploadUrl = source?.upload_url || `/api/v1/parsers/upload/${target.dataset.upload}/`;
|
||||
await apiUpload(uploadUrl, formData);
|
||||
$("manualUploadStatus").classList.remove("hidden");
|
||||
$("manualUploadStatus").textContent = "Загрузка...";
|
||||
try {
|
||||
const result = await apiUpload(uploadUrl, formData);
|
||||
fileInput.value = "";
|
||||
const queued = result.queued ?? result.data?.queued;
|
||||
$("manualUploadStatus").textContent = queued !== undefined
|
||||
? `Файл принят: queued=${queued}, skipped=${result.skipped ?? 0}, invalid=${result.invalid ?? 0}`
|
||||
: "Файл загружен";
|
||||
await refreshDashboard();
|
||||
showMainTab("uploads");
|
||||
} catch (error) {
|
||||
$("manualUploadStatus").textContent = errorMessage(error);
|
||||
}
|
||||
}
|
||||
const recordRow = target.closest("[data-record-id]");
|
||||
if (recordRow) {
|
||||
|
||||
@@ -31,6 +31,7 @@ from apps.parsers.clients.proverki.client import ProverkiClientError
|
||||
from apps.parsers.clients.zakupki import ZakupkiClientError
|
||||
from apps.parsers.models import (
|
||||
FinancialReport,
|
||||
GenericParserRecord,
|
||||
IndustrialCertificateRecord,
|
||||
IndustrialProductRecord,
|
||||
InspectionRecord,
|
||||
@@ -66,6 +67,7 @@ from registers.models import Organization
|
||||
|
||||
from tests.apps.parsers.factories import (
|
||||
InspectionRecordFactory,
|
||||
ManufacturerRecordFactory,
|
||||
ParserLoadLogFactory,
|
||||
ProcurementRecordFactory,
|
||||
ProxyFactory,
|
||||
@@ -246,6 +248,124 @@ class GenericSourceFetchTestCase(TestCase):
|
||||
self.assertEqual(records[0].record_date, "2026-04-01")
|
||||
self.assertEqual(records[0].payload["provider"], "checko")
|
||||
|
||||
@override_settings(CHECKO_API_KEY="test-key", ARBITRATION_CHECKO_LIMIT=10)
|
||||
def test_arbitration_fetches_checko_legal_cases_for_registry_organizations(self):
|
||||
organization = Organization.objects.create(
|
||||
pn_name='ООО "Арбитраж"',
|
||||
mn_ogrn=1027700000001,
|
||||
mn_inn=7701000002,
|
||||
in_kpp=770101001,
|
||||
mn_okpo="12345678",
|
||||
)
|
||||
|
||||
class _CheckoClient:
|
||||
instances = []
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
self.requests = []
|
||||
self.instances.append(self)
|
||||
|
||||
def iter_legal_cases(self, request):
|
||||
self.requests.append(request)
|
||||
return iter(
|
||||
[
|
||||
SimpleNamespace(
|
||||
case_number="А40-1/2026",
|
||||
court_name="АС города Москвы",
|
||||
type="civil",
|
||||
category="Взыскание задолженности",
|
||||
status="Рассмотрено",
|
||||
filing_date="2026-04-01",
|
||||
result_date="2026-04-20",
|
||||
claim_amount=150000,
|
||||
awarded_amount=100000,
|
||||
plaintiffs=(
|
||||
SimpleNamespace(
|
||||
name=organization.pn_name,
|
||||
inn=str(organization.mn_inn),
|
||||
ogrn=str(organization.mn_ogrn),
|
||||
role="plaintiff",
|
||||
),
|
||||
),
|
||||
defendants=(),
|
||||
third_parties=(),
|
||||
instances=(),
|
||||
url="https://kad.arbitr.ru/Card/1",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
with patch.object(parser_tasks, "CheckoClient", _CheckoClient):
|
||||
result = parser_tasks.parse_arbitration_cases(limit=10, proxies=[])
|
||||
|
||||
self.assertEqual(result["status"], "success")
|
||||
self.assertEqual(result["saved"], 1)
|
||||
record = GenericParserRecord.objects.get(
|
||||
source=ParserLoadLog.Source.ARBITRATION
|
||||
)
|
||||
self.assertEqual(record.registry_organization_id, organization.id)
|
||||
self.assertEqual(record.inn, str(organization.mn_inn))
|
||||
self.assertEqual(record.ogrn, str(organization.mn_ogrn))
|
||||
self.assertEqual(record.amount, 150000)
|
||||
self.assertEqual(record.payload["provider"], "checko")
|
||||
self.assertEqual(record.payload["target"]["role"], "plaintiff")
|
||||
self.assertEqual(_CheckoClient.instances[0].requests[0].inn, record.inn)
|
||||
|
||||
@override_settings(CHECKO_API_KEY="test-key")
|
||||
def test_arbitration_uses_loaded_parser_identifiers_when_registry_empty(self):
|
||||
manufacturer = ManufacturerRecordFactory(
|
||||
inn="7701000003",
|
||||
ogrn="1027700000002",
|
||||
full_legal_name='ООО "Источник"',
|
||||
)
|
||||
|
||||
class _CheckoClient:
|
||||
def __init__(self, **_kwargs):
|
||||
return
|
||||
|
||||
def iter_legal_cases(self, request):
|
||||
self.request = request
|
||||
return iter(
|
||||
[
|
||||
SimpleNamespace(
|
||||
case_number="А40-2/2026",
|
||||
court_name="АС города Москвы",
|
||||
type=None,
|
||||
category=None,
|
||||
status="Принято",
|
||||
filing_date="2026-04-02",
|
||||
result_date=None,
|
||||
claim_amount=None,
|
||||
awarded_amount=None,
|
||||
plaintiffs=(),
|
||||
defendants=(
|
||||
SimpleNamespace(
|
||||
name=manufacturer.full_legal_name,
|
||||
inn=manufacturer.inn,
|
||||
ogrn=manufacturer.ogrn,
|
||||
role="defendant",
|
||||
),
|
||||
),
|
||||
third_parties=(),
|
||||
instances=(),
|
||||
url="https://kad.arbitr.ru/Card/2",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
with patch.object(parser_tasks, "CheckoClient", _CheckoClient):
|
||||
records = parser_tasks._fetch_checko_arbitration_records(
|
||||
limit=1,
|
||||
proxies=[],
|
||||
)
|
||||
|
||||
self.assertEqual(len(records), 1)
|
||||
self.assertEqual(records[0].source, "arbitration")
|
||||
self.assertEqual(records[0].inn, manufacturer.inn)
|
||||
self.assertEqual(records[0].ogrn, manufacturer.ogrn)
|
||||
self.assertEqual(records[0].payload["target"]["role"], "defendant")
|
||||
|
||||
@override_settings(CHECKO_API_KEY="")
|
||||
def test_fedresurs_skips_when_official_blocked_and_fallback_empty(self):
|
||||
with patch.object(
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import io
|
||||
import os
|
||||
import tempfile
|
||||
import zipfile
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from apps.parsers.models import (
|
||||
@@ -50,6 +51,14 @@ def _build_fns_excel_bytes() -> bytes:
|
||||
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),
|
||||
@@ -411,6 +420,34 @@ class ParsersViewSetTest(APITestCase):
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user