feat: export state corp package from backup endpoint
This commit is contained in:
@@ -19,14 +19,17 @@ from zipfile import ZIP_DEFLATED, ZipFile
|
||||
|
||||
import requests
|
||||
from apps.parsers.models import (
|
||||
GenericParserRecord,
|
||||
IndustrialProductRecord,
|
||||
InspectionRecord,
|
||||
ParserLoadLog,
|
||||
ProcurementRecord,
|
||||
)
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from registers.models import Organization
|
||||
from registers.models import Organization, RegistryMembershipPeriod
|
||||
|
||||
|
||||
class StateCorpExchangeError(ValueError):
|
||||
@@ -52,6 +55,14 @@ class StateCorpExchangeService:
|
||||
AAD = b"state-corp-exchange-v1"
|
||||
PAYLOAD_FORMAT = "state-corp-exchange-payload"
|
||||
BIN_FORMAT = "state-corp-exchange-bin"
|
||||
ROSATOM_ROSCOSMOS_REGISTRY_NAMES = (
|
||||
"Реестр госкорпорации Роскосмос",
|
||||
"Реестр госкорпорации Роскосмос ГОЗ",
|
||||
"Реестр госкорпорации Роскосмос ОПК",
|
||||
"Реестр госкорпорации Росатом",
|
||||
"Реестр госкорпорации Росатом ГОЗ",
|
||||
"Реестр госкорпорации Росатом ОПК",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def build_package(
|
||||
@@ -60,18 +71,32 @@ class StateCorpExchangeService:
|
||||
package_id: str | None = None,
|
||||
source_system: str = "mostovik",
|
||||
organization_inns: list[str] | None = None,
|
||||
actual_date: date | str | None = None,
|
||||
) -> StateCorpExchangePackage:
|
||||
"""Build encrypted archive for state-corp import."""
|
||||
produced_at = timezone.now()
|
||||
snapshot_date = cls._coerce_actual_date(actual_date)
|
||||
normalized_inns = cls._normalize_inn_list(organization_inns)
|
||||
organizations = cls._get_organizations(normalized_inns)
|
||||
organizations = (
|
||||
cls._get_organizations(normalized_inns)
|
||||
if normalized_inns
|
||||
else cls._get_rosatom_roscosmos_organizations(snapshot_date)
|
||||
)
|
||||
allowed_inns = {str(item.mn_inn) for item in organizations}
|
||||
data = {
|
||||
"organizations": cls._serialize_organizations(organizations),
|
||||
"industrial_products": cls._serialize_industrial_products(allowed_inns),
|
||||
"prosecutor_checks": cls._serialize_prosecutor_checks(allowed_inns),
|
||||
"public_procurements": cls._serialize_public_procurements(allowed_inns),
|
||||
"arbitration_cases": [],
|
||||
"arbitration_cases": cls._serialize_arbitration_cases(allowed_inns),
|
||||
"bankruptcy_procedures": cls._serialize_bankruptcy_procedures(allowed_inns),
|
||||
"defense_unreliable_suppliers": (
|
||||
cls._serialize_defense_unreliable_suppliers(allowed_inns)
|
||||
),
|
||||
"information_security_registries": (
|
||||
cls._serialize_information_security_registries(allowed_inns)
|
||||
),
|
||||
"labor_vacancies": cls._serialize_labor_vacancies(allowed_inns),
|
||||
}
|
||||
payload_counts = {key: len(value) for key, value in data.items()}
|
||||
package_id = package_id or cls._build_package_id()
|
||||
@@ -248,6 +273,23 @@ class StateCorpExchangeService:
|
||||
)
|
||||
return list(queryset)
|
||||
|
||||
@classmethod
|
||||
def _get_rosatom_roscosmos_organizations(
|
||||
cls,
|
||||
actual_date: date,
|
||||
) -> list[Organization]:
|
||||
organization_ids = (
|
||||
RegistryMembershipPeriod.objects.filter(
|
||||
registry__name__in=cls.ROSATOM_ROSCOSMOS_REGISTRY_NAMES,
|
||||
started_at__lte=actual_date,
|
||||
)
|
||||
.filter(Q(ended_at__isnull=True) | Q(ended_at__gt=actual_date))
|
||||
.order_by("organization_id")
|
||||
.values_list("organization_id", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
return list(Organization.objects.filter(id__in=organization_ids).order_by("id"))
|
||||
|
||||
@classmethod
|
||||
def _serialize_organizations(
|
||||
cls,
|
||||
@@ -350,6 +392,249 @@ class StateCorpExchangeService:
|
||||
)
|
||||
return items
|
||||
|
||||
@classmethod
|
||||
def _serialize_arbitration_cases(
|
||||
cls,
|
||||
allowed_inns: set[str],
|
||||
) -> list[dict[str, str]]:
|
||||
items: list[dict[str, str]] = []
|
||||
queryset = GenericParserRecord.objects.filter(
|
||||
source=ParserLoadLog.Source.ARBITRATION,
|
||||
inn__in=allowed_inns,
|
||||
).order_by("id")
|
||||
for record in queryset:
|
||||
payload = record.payload if isinstance(record.payload, dict) else {}
|
||||
target = payload.get("target")
|
||||
if not isinstance(target, dict):
|
||||
target = {}
|
||||
|
||||
decision_date = cls._coerce_date(
|
||||
None,
|
||||
payload.get("result_date")
|
||||
or payload.get("decision_date")
|
||||
or payload.get("filing_date")
|
||||
or record.record_date,
|
||||
)
|
||||
case_number = str(
|
||||
payload.get("case_number") or record.title or record.external_id or ""
|
||||
).strip()
|
||||
if not case_number or decision_date is None:
|
||||
continue
|
||||
|
||||
items.append(
|
||||
{
|
||||
"organization_inn": cls._digits(record.inn),
|
||||
"case_number": case_number,
|
||||
"court_name": str(payload.get("court_name") or "").strip(),
|
||||
"party_role": str(
|
||||
target.get("role") or payload.get("party_role") or ""
|
||||
).strip(),
|
||||
"status": str(payload.get("status") or record.status or "").strip(),
|
||||
"decision_date": decision_date.isoformat(),
|
||||
}
|
||||
)
|
||||
return items
|
||||
|
||||
@classmethod
|
||||
def _serialize_bankruptcy_procedures(
|
||||
cls,
|
||||
allowed_inns: set[str],
|
||||
) -> list[dict[str, str | None]]:
|
||||
items: list[dict[str, str | None]] = []
|
||||
for record in cls._generic_records(
|
||||
allowed_inns,
|
||||
sources=[ParserLoadLog.Source.FEDRESURS_BANKRUPTCY],
|
||||
):
|
||||
payload = cls._record_payload(record)
|
||||
message_date = cls._coerce_date(
|
||||
None,
|
||||
cls._payload_lookup(payload, ["message_date", "date", "Дата"])
|
||||
or record.record_date,
|
||||
)
|
||||
message_type = cls._payload_lookup(payload, ["message_type", "type"])
|
||||
items.append(
|
||||
{
|
||||
"organization_inn": cls._digits(record.inn),
|
||||
"external_id": record.external_id,
|
||||
"message_type": message_type or record.title or record.status,
|
||||
"message_date": message_date.isoformat() if message_date else None,
|
||||
"case_number": cls._payload_lookup(payload, ["case_number"]),
|
||||
"status": record.status,
|
||||
"source_url": record.url,
|
||||
}
|
||||
)
|
||||
return items
|
||||
|
||||
@classmethod
|
||||
def _serialize_defense_unreliable_suppliers(
|
||||
cls,
|
||||
allowed_inns: set[str],
|
||||
) -> list[dict[str, str | None]]:
|
||||
items: list[dict[str, str | None]] = []
|
||||
for record in cls._generic_records(
|
||||
allowed_inns,
|
||||
sources=[
|
||||
ParserLoadLog.Source.UNFAIR_SUPPLIERS,
|
||||
ParserLoadLog.Source.FAS_GOZ,
|
||||
],
|
||||
):
|
||||
payload = cls._record_payload(record)
|
||||
included_at = cls._coerce_date(
|
||||
None,
|
||||
cls._payload_lookup(
|
||||
payload,
|
||||
[
|
||||
"included_at",
|
||||
"date",
|
||||
"Дата вступления постановления",
|
||||
"Включено",
|
||||
],
|
||||
)
|
||||
or record.record_date,
|
||||
)
|
||||
items.append(
|
||||
{
|
||||
"organization_inn": cls._digits(record.inn),
|
||||
"external_id": record.external_id,
|
||||
"registry_source": record.source,
|
||||
"registry_number": cls._payload_lookup(
|
||||
payload,
|
||||
[
|
||||
"registry_number",
|
||||
"number",
|
||||
"Номер реестровой записи",
|
||||
"Номер реестровой записи в ЕРУЗ",
|
||||
],
|
||||
),
|
||||
"supplier_name": cls._payload_lookup(
|
||||
payload,
|
||||
[
|
||||
"supplier_name",
|
||||
"organisation_name",
|
||||
"Полное наименование лица",
|
||||
"Фирменное наименование лица",
|
||||
"Наименование ФИО недобросовестного поставщика",
|
||||
],
|
||||
)
|
||||
or record.organisation_name,
|
||||
"reason": record.title,
|
||||
"included_at": included_at.isoformat() if included_at else None,
|
||||
"status": record.status,
|
||||
"source_url": record.url,
|
||||
}
|
||||
)
|
||||
return items
|
||||
|
||||
@classmethod
|
||||
def _serialize_information_security_registries(
|
||||
cls,
|
||||
allowed_inns: set[str],
|
||||
) -> list[dict[str, str | None]]:
|
||||
items: list[dict[str, str | None]] = []
|
||||
for record in cls._generic_records(
|
||||
allowed_inns,
|
||||
sources=[ParserLoadLog.Source.FSTEC],
|
||||
):
|
||||
payload = cls._record_payload(record)
|
||||
issued_at = cls._coerce_date(
|
||||
None,
|
||||
cls._payload_lookup(
|
||||
payload,
|
||||
["issued_at", "date", "Дата предоставления лицензии"],
|
||||
)
|
||||
or record.record_date,
|
||||
)
|
||||
expires_at = cls._coerce_date(
|
||||
None,
|
||||
cls._payload_lookup(
|
||||
payload,
|
||||
["expires_at", "Срок действия сертификата", "res_valid_till"],
|
||||
),
|
||||
)
|
||||
items.append(
|
||||
{
|
||||
"organization_inn": cls._digits(record.inn),
|
||||
"external_id": record.external_id,
|
||||
"registry_name": cls._payload_lookup(payload, ["registry_name"])
|
||||
or record.title
|
||||
or "Реестр ФСТЭК",
|
||||
"presence_status": "present",
|
||||
"entry_number": cls._payload_lookup(
|
||||
payload,
|
||||
[
|
||||
"entry_number",
|
||||
"registration_number",
|
||||
"Регистрационный номер лицензии",
|
||||
"№ сертификата",
|
||||
],
|
||||
)
|
||||
or record.external_id,
|
||||
"issued_at": issued_at.isoformat() if issued_at else None,
|
||||
"expires_at": expires_at.isoformat() if expires_at else None,
|
||||
}
|
||||
)
|
||||
return items
|
||||
|
||||
@classmethod
|
||||
def _serialize_labor_vacancies(
|
||||
cls,
|
||||
allowed_inns: set[str],
|
||||
) -> list[dict[str, str | None]]:
|
||||
items: list[dict[str, str | None]] = []
|
||||
for record in cls._generic_records(
|
||||
allowed_inns,
|
||||
sources=[ParserLoadLog.Source.TRUDVSEM],
|
||||
):
|
||||
payload = cls._record_payload(record)
|
||||
published_at = cls._coerce_date(
|
||||
None,
|
||||
cls._payload_lookup(
|
||||
payload,
|
||||
["published_at", "creation-date", "date", "published_at"],
|
||||
)
|
||||
or record.record_date,
|
||||
)
|
||||
items.append(
|
||||
{
|
||||
"organization_inn": cls._digits(record.inn),
|
||||
"external_id": record.external_id,
|
||||
"vacancy_source": cls._payload_lookup(payload, ["vacancy_source"])
|
||||
or "trudvsem",
|
||||
"title": record.title,
|
||||
"status": record.status,
|
||||
"published_at": published_at.isoformat() if published_at else None,
|
||||
"salary_amount": cls._serialize_decimal(record.amount),
|
||||
"source_url": record.url,
|
||||
}
|
||||
)
|
||||
return items
|
||||
|
||||
@staticmethod
|
||||
def _record_payload(record: GenericParserRecord) -> dict[str, Any]:
|
||||
return record.payload if isinstance(record.payload, dict) else {}
|
||||
|
||||
@classmethod
|
||||
def _generic_records(
|
||||
cls,
|
||||
allowed_inns: set[str],
|
||||
*,
|
||||
sources: list[str],
|
||||
):
|
||||
if not allowed_inns:
|
||||
return GenericParserRecord.objects.none()
|
||||
return GenericParserRecord.objects.filter(
|
||||
source__in=sources,
|
||||
inn__in=allowed_inns,
|
||||
).order_by("id")
|
||||
|
||||
@staticmethod
|
||||
def _payload_lookup(payload: dict[str, Any], candidates: list[str]) -> str:
|
||||
for key in candidates:
|
||||
value = payload.get(key)
|
||||
if value not in (None, ""):
|
||||
return str(value).strip()
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _normalize_inn_list(organization_inns: list[str] | None) -> list[str]:
|
||||
normalized_items: list[str] = []
|
||||
@@ -359,6 +644,20 @@ class StateCorpExchangeService:
|
||||
normalized_items.append(normalized)
|
||||
return normalized_items
|
||||
|
||||
@staticmethod
|
||||
def _coerce_actual_date(value: date | str | None) -> date:
|
||||
if value is None:
|
||||
return timezone.localdate()
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
raw_text = str(value).strip()
|
||||
try:
|
||||
return date.fromisoformat(raw_text)
|
||||
except ValueError as exc:
|
||||
raise StateCorpExchangeError(
|
||||
"actual_date должен быть датой в формате YYYY-MM-DD"
|
||||
) from exc
|
||||
|
||||
@staticmethod
|
||||
def _digits(value: str | int) -> str:
|
||||
return "".join(char for char in str(value).strip() if char.isdigit())
|
||||
|
||||
Reference in New Issue
Block a user