913 lines
30 KiB
Python
913 lines
30 KiB
Python
"""Models for the canonical organizations directory."""
|
||
|
||
import uuid
|
||
|
||
from django.db import models
|
||
from django.db.models import Q
|
||
from django.utils.translation import gettext_lazy as _
|
||
from polymorphic.models import PolymorphicModel
|
||
|
||
from organizations.data_sources import snapshot_data_source_summary
|
||
from organizations.name_normalization import normalize_organization_name
|
||
|
||
|
||
class SourceGroup(models.TextChoices):
|
||
"""Product-level organization source groups."""
|
||
|
||
FINANCIAL_INDICATORS = (
|
||
"financial_indicators",
|
||
_("Финансово-экономические показатели"),
|
||
)
|
||
GOVERNMENT_PROCUREMENTS = "government_procurements", _("Государственные закупки")
|
||
INDUSTRIAL_PRODUCTION = (
|
||
"industrial_production",
|
||
_("Производители и продукция России"),
|
||
)
|
||
PLANNED_INSPECTIONS = "planned_inspections", _("Плановые проверки")
|
||
BANKRUPTCY = "bankruptcy", _("Сведения о процедурах банкротства")
|
||
DEFENSE_SUPPLIERS = "defense_suppliers", _("Недобросовестные поставщики ГОЗ")
|
||
ARBITRATION = "arbitration", _("Арбитражные дела")
|
||
SECURITY_REGISTRIES = (
|
||
"security_registries",
|
||
_("Реестры по информационной безопасности"),
|
||
)
|
||
VACANCIES = "vacancies", _("Вакансии")
|
||
|
||
|
||
class SourceExtensionStatus(models.TextChoices):
|
||
"""Lifecycle status for an organization source extension."""
|
||
|
||
ACTIVE = "active", _("Активно")
|
||
INACTIVE = "inactive", _("Неактивно")
|
||
ERROR = "error", _("Ошибка")
|
||
|
||
|
||
class Organization(models.Model):
|
||
"""Canonical organization without source-specific relations."""
|
||
|
||
class IdentityStatus(models.TextChoices):
|
||
"""Completeness of legal identity identifiers."""
|
||
|
||
COMPLETE = "complete", _("Полная")
|
||
PARTIAL = "partial", _("Частичная")
|
||
MISSING = "missing", _("Отсутствует")
|
||
|
||
uid = models.UUIDField(
|
||
_("UID"),
|
||
primary_key=True,
|
||
default=uuid.uuid4,
|
||
editable=False,
|
||
)
|
||
rn = models.PositiveBigIntegerField(
|
||
_("регистрационный номер"),
|
||
null=True,
|
||
blank=True,
|
||
unique=True,
|
||
db_index=True,
|
||
help_text=_("Регистрационный номер организации из перечня"),
|
||
)
|
||
gk_code = models.CharField(
|
||
_("код государственной корпорации"),
|
||
max_length=16,
|
||
blank=True,
|
||
db_column="_gk",
|
||
db_index=True,
|
||
help_text=_("Отношение к ГК Росатом и ГК Роскосмос"),
|
||
)
|
||
gk_name = models.CharField(
|
||
_("государственная корпорация"),
|
||
max_length=255,
|
||
blank=True,
|
||
help_text=_("Расшифровка кода государственной корпорации"),
|
||
)
|
||
in_korp_code = models.CharField(
|
||
_("код положения внутри ГК"),
|
||
max_length=16,
|
||
blank=True,
|
||
db_index=True,
|
||
help_text=_("Положение внутри ГК Росатом и ГК Роскосмос"),
|
||
)
|
||
in_korp_name = models.CharField(
|
||
_("положение внутри ГК"),
|
||
max_length=255,
|
||
blank=True,
|
||
help_text=_("Расшифровка положения внутри ГК"),
|
||
)
|
||
name = models.CharField(
|
||
_("наименование"),
|
||
max_length=1024,
|
||
db_index=True,
|
||
help_text=_("Наименование организации или ИП"),
|
||
)
|
||
full_name = models.TextField(
|
||
_("полное наименование"),
|
||
blank=True,
|
||
)
|
||
short_name = models.TextField(
|
||
_("краткое наименование"),
|
||
blank=True,
|
||
)
|
||
pn_name = models.TextField(
|
||
_("приведенное наименование"),
|
||
blank=True,
|
||
)
|
||
pn_name_en = models.TextField(
|
||
_("приведенное наименование на английском"),
|
||
blank=True,
|
||
)
|
||
inn = models.CharField(
|
||
_("ИНН"),
|
||
max_length=12,
|
||
blank=True,
|
||
db_index=True,
|
||
help_text=_("ИНН ЮЛ или ИП"),
|
||
)
|
||
kpp = models.CharField(
|
||
_("КПП"),
|
||
max_length=9,
|
||
blank=True,
|
||
db_index=True,
|
||
help_text=_("КПП только для юридических лиц"),
|
||
)
|
||
ogrn = models.CharField(
|
||
_("ОГРН"),
|
||
max_length=13,
|
||
blank=True,
|
||
db_index=True,
|
||
help_text=_("ОГРН только для юридических лиц"),
|
||
)
|
||
okpo = models.CharField(
|
||
_("ОКПО"),
|
||
max_length=32,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
filial = models.CharField(
|
||
_("признак филиала из источника"),
|
||
max_length=64,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
is_branch = models.BooleanField(
|
||
_("филиал"),
|
||
null=True,
|
||
blank=True,
|
||
db_index=True,
|
||
help_text=_("Нормализованный признак filial: .T. = филиал, .F. = головная"),
|
||
)
|
||
ogrip = models.CharField(
|
||
_("ОГРИП"),
|
||
max_length=15,
|
||
blank=True,
|
||
db_index=True,
|
||
help_text=_("ОГРИП только для индивидуальных предпринимателей"),
|
||
)
|
||
registration_date = models.DateField(
|
||
_("дата регистрации"),
|
||
null=True,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
create_date = models.CharField(
|
||
_("год или дата создания"),
|
||
max_length=32,
|
||
blank=True,
|
||
)
|
||
organizational_legal_form = models.CharField(
|
||
_("код организационно-правовой формы"),
|
||
max_length=32,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
organizational_legal_form1 = models.CharField(
|
||
_("организационно-правовая форма"),
|
||
max_length=512,
|
||
blank=True,
|
||
)
|
||
ownership_form = models.CharField(
|
||
_("код формы собственности"),
|
||
max_length=32,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
ownership_form1 = models.CharField(
|
||
_("форма собственности"),
|
||
max_length=512,
|
||
blank=True,
|
||
)
|
||
authorized_capital = models.DecimalField(
|
||
_("уставный капитал"),
|
||
max_digits=24,
|
||
decimal_places=2,
|
||
null=True,
|
||
blank=True,
|
||
)
|
||
legal_address = models.TextField(
|
||
_("юридический адрес"),
|
||
blank=True,
|
||
)
|
||
business_act_cod = models.CharField(
|
||
_("код вида деятельности"),
|
||
max_length=32,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
business_activity = models.CharField(
|
||
_("вид деятельности"),
|
||
max_length=255,
|
||
blank=True,
|
||
)
|
||
general_director = models.TextField(
|
||
_("руководитель"),
|
||
blank=True,
|
||
)
|
||
general_director_tax_id = models.CharField(
|
||
_("ИНН руководителя"),
|
||
max_length=32,
|
||
blank=True,
|
||
)
|
||
uk = models.TextField(
|
||
_("управляющая компания"),
|
||
blank=True,
|
||
)
|
||
inn_uk = models.CharField(
|
||
_("ИНН управляющей компании"),
|
||
max_length=12,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
appointment_date = models.DateField(
|
||
_("дата назначения руководителя"),
|
||
null=True,
|
||
blank=True,
|
||
)
|
||
cf_fl_rn = models.CharField(
|
||
_("регистрационный номер руководителя"),
|
||
max_length=64,
|
||
blank=True,
|
||
)
|
||
akc_fs = models.CharField(
|
||
_("акционерный капитал федеральная собственность"),
|
||
max_length=64,
|
||
blank=True,
|
||
)
|
||
akc_sf = models.CharField(
|
||
_("акционерный капитал собственность субъекта РФ"),
|
||
max_length=64,
|
||
blank=True,
|
||
)
|
||
re_za = models.BooleanField(
|
||
_("реестр организаций-заказчиков"),
|
||
null=True,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
re_zasf = models.BooleanField(
|
||
_("реестр организаций-заказчиков субъекта РФ"),
|
||
null=True,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
goz_participation = models.BooleanField(
|
||
_("участие в ГОЗ"),
|
||
null=True,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
opk_registry_membership = models.BooleanField(
|
||
_("членство в реестре ОПК"),
|
||
null=True,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
ropk_num = models.CharField(
|
||
_("номер в реестре ОПК"),
|
||
max_length=64,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
ropk_razdel_num = models.CharField(
|
||
_("номер раздела реестра ОПК"),
|
||
max_length=64,
|
||
blank=True,
|
||
)
|
||
ropk_razdel_name = models.TextField(
|
||
_("раздел реестра ОПК"),
|
||
blank=True,
|
||
)
|
||
min = models.TextField(
|
||
_("министерство"),
|
||
blank=True,
|
||
)
|
||
dep = models.TextField(
|
||
_("департамент"),
|
||
blank=True,
|
||
)
|
||
otr = models.TextField(
|
||
_("отрасль"),
|
||
blank=True,
|
||
)
|
||
integrated_structure = models.TextField(
|
||
_("интегрированная структура"),
|
||
blank=True,
|
||
db_column="is",
|
||
)
|
||
state_sector_code = models.CharField(
|
||
_("код госсектора"),
|
||
max_length=16,
|
||
blank=True,
|
||
db_column="_k",
|
||
db_index=True,
|
||
)
|
||
state_sector_name = models.CharField(
|
||
_("госсектор"),
|
||
max_length=255,
|
||
blank=True,
|
||
)
|
||
directory_source_file_hash = models.CharField(
|
||
_("хеш файла перечня"),
|
||
max_length=64,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
directory_source_row_number = models.PositiveIntegerField(
|
||
_("номер строки файла перечня"),
|
||
null=True,
|
||
blank=True,
|
||
)
|
||
directory_imported_at = models.DateTimeField(
|
||
_("дата импорта перечня"),
|
||
null=True,
|
||
blank=True,
|
||
)
|
||
identity_status = models.CharField(
|
||
_("полнота реквизитов"),
|
||
max_length=16,
|
||
choices=IdentityStatus.choices,
|
||
default=IdentityStatus.MISSING,
|
||
db_index=True,
|
||
help_text=_("Оценка полноты идентификационных реквизитов"),
|
||
)
|
||
primary_identity = models.CharField(
|
||
_("основной идентификатор"),
|
||
max_length=255,
|
||
blank=True,
|
||
db_index=True,
|
||
editable=False,
|
||
help_text=_("Нормализованный ключ для диагностики и дедупликации"),
|
||
)
|
||
|
||
class Meta:
|
||
db_table = "organizations_organization"
|
||
verbose_name = _("организация")
|
||
verbose_name_plural = _("организации")
|
||
ordering = ["name"]
|
||
indexes = [
|
||
models.Index(fields=["inn", "kpp"]),
|
||
models.Index(fields=["ogrn", "kpp"]),
|
||
models.Index(fields=["inn", "ogrn"]),
|
||
models.Index(fields=["inn", "ogrip"]),
|
||
models.Index(fields=["okpo"]),
|
||
models.Index(fields=["is_branch", "inn", "ogrn"]),
|
||
]
|
||
constraints = [
|
||
models.UniqueConstraint(
|
||
fields=["ogrip"],
|
||
condition=~Q(ogrip=""),
|
||
name="unique_organizations_ogrip_not_blank",
|
||
),
|
||
models.CheckConstraint(
|
||
check=Q(ogrip="") | (Q(kpp="") & Q(ogrn="")),
|
||
name="check_entrepreneur_has_no_kpp_ogrn",
|
||
),
|
||
]
|
||
|
||
def __str__(self) -> str:
|
||
identifier = self.inn or self.ogrn or self.ogrip
|
||
if identifier:
|
||
return f"{self.name} ({identifier})"
|
||
return self.name
|
||
|
||
@property
|
||
def id(self):
|
||
"""Compatibility alias for code paths that expect a model id attribute."""
|
||
return self.pk
|
||
|
||
@staticmethod
|
||
def _legacy_digits(value, *, max_length: int, zfill: int | None = None) -> str:
|
||
digits = "".join(char for char in str(value or "") if char.isdigit())
|
||
if zfill and 0 < len(digits) < zfill:
|
||
digits = digits.zfill(zfill)
|
||
return digits[:max_length]
|
||
|
||
@property
|
||
def mn_inn(self) -> str:
|
||
"""Compatibility alias for the removed registers.Organization field."""
|
||
return self.inn
|
||
|
||
@mn_inn.setter
|
||
def mn_inn(self, value) -> None:
|
||
self.inn = self._legacy_digits(value, max_length=12, zfill=10)
|
||
|
||
@property
|
||
def mn_ogrn(self) -> str:
|
||
"""Compatibility alias for the removed registers.Organization field."""
|
||
return self.ogrn
|
||
|
||
@mn_ogrn.setter
|
||
def mn_ogrn(self, value) -> None:
|
||
self.ogrn = self._legacy_digits(value, max_length=13)
|
||
|
||
@property
|
||
def in_kpp(self) -> str:
|
||
"""Compatibility alias for the removed registers.Organization field."""
|
||
return self.kpp
|
||
|
||
@in_kpp.setter
|
||
def in_kpp(self, value) -> None:
|
||
self.kpp = self._legacy_digits(value, max_length=9, zfill=9)
|
||
|
||
@property
|
||
def mn_okpo(self) -> str:
|
||
"""Compatibility alias for the removed registers.Organization field."""
|
||
return self.okpo
|
||
|
||
@mn_okpo.setter
|
||
def mn_okpo(self, value) -> None:
|
||
self.okpo = self._legacy_digits(value, max_length=32)
|
||
|
||
@property
|
||
def normalized_name(self) -> str:
|
||
return normalize_organization_name(self.name)
|
||
|
||
def save(self, *args, **kwargs) -> None:
|
||
if not self.name:
|
||
self.name = self.pn_name or self.short_name or self.full_name
|
||
self.identity_status = self._resolve_identity_status()
|
||
self.primary_identity = self._resolve_primary_identity()
|
||
update_fields = kwargs.get("update_fields")
|
||
if update_fields is not None:
|
||
kwargs["update_fields"] = list(
|
||
dict.fromkeys(
|
||
[*update_fields, "name", "identity_status", "primary_identity"]
|
||
)
|
||
)
|
||
super().save(*args, **kwargs)
|
||
|
||
def _resolve_identity_status(self) -> str:
|
||
if self.inn and (self.ogrn or self.ogrip):
|
||
return self.IdentityStatus.COMPLETE
|
||
if self.inn or self.ogrn or self.ogrip:
|
||
return self.IdentityStatus.PARTIAL
|
||
return self.IdentityStatus.MISSING
|
||
|
||
def _resolve_primary_identity(self) -> str:
|
||
if self.inn and self.kpp:
|
||
return f"inn:{self.inn}:kpp:{self.kpp}"
|
||
if self.ogrn:
|
||
return f"ogrn:{self.ogrn}"
|
||
if self.ogrip:
|
||
return f"ogrip:{self.ogrip}"
|
||
if self.inn:
|
||
return f"inn:{self.inn}"
|
||
normalized_name = normalize_organization_name(self.name)
|
||
if normalized_name:
|
||
return f"name:{normalized_name}"[:255]
|
||
return ""
|
||
|
||
|
||
class OrganizationSourceExtension(PolymorphicModel):
|
||
"""Base source group extension for one canonical organization."""
|
||
|
||
uid = models.UUIDField(
|
||
_("UID"),
|
||
primary_key=True,
|
||
default=uuid.uuid4,
|
||
editable=False,
|
||
)
|
||
organization = models.ForeignKey(
|
||
Organization,
|
||
on_delete=models.CASCADE,
|
||
related_name="source_extensions",
|
||
verbose_name=_("организация"),
|
||
)
|
||
source_group = models.CharField(
|
||
_("группа источников"),
|
||
max_length=64,
|
||
choices=SourceGroup.choices,
|
||
db_index=True,
|
||
)
|
||
title = models.CharField(
|
||
_("название"),
|
||
max_length=255,
|
||
help_text=_("Человекочитаемое название блока источника"),
|
||
)
|
||
status = models.CharField(
|
||
_("статус"),
|
||
max_length=16,
|
||
choices=SourceExtensionStatus.choices,
|
||
default=SourceExtensionStatus.ACTIVE,
|
||
db_index=True,
|
||
)
|
||
records_count = models.PositiveIntegerField(
|
||
_("количество записей"),
|
||
default=0,
|
||
)
|
||
first_seen_at = models.DateTimeField(
|
||
_("первая запись"),
|
||
null=True,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
last_seen_at = models.DateTimeField(
|
||
_("последняя запись"),
|
||
null=True,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
last_load_batch = models.PositiveIntegerField(
|
||
_("последний пакет загрузки"),
|
||
null=True,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
metadata = models.JSONField(
|
||
_("метаданные"),
|
||
default=dict,
|
||
blank=True,
|
||
)
|
||
created_at = models.DateTimeField(_("дата создания"), auto_now_add=True)
|
||
updated_at = models.DateTimeField(_("дата обновления"), auto_now=True)
|
||
|
||
class Meta:
|
||
db_table = "organizations_source_extension"
|
||
verbose_name = _("расширение источника организации")
|
||
verbose_name_plural = _("расширения источников организаций")
|
||
ordering = ["organization__name", "source_group"]
|
||
constraints = [
|
||
models.UniqueConstraint(
|
||
fields=["organization", "source_group"],
|
||
name="unique_organization_source_group_extension",
|
||
),
|
||
]
|
||
indexes = [
|
||
models.Index(fields=["source_group", "status"]),
|
||
models.Index(fields=["organization", "source_group"]),
|
||
]
|
||
|
||
def __str__(self) -> str:
|
||
return f"{self.organization}: {self.title}"
|
||
|
||
def save(self, *args, **kwargs) -> None:
|
||
source_group = getattr(self, "source_group_value", "")
|
||
if source_group:
|
||
self.source_group = source_group
|
||
super().save(*args, **kwargs)
|
||
|
||
|
||
class FinancialIndicatorsExtension(OrganizationSourceExtension):
|
||
"""Financial and accounting indicators source group."""
|
||
|
||
source_group_value = SourceGroup.FINANCIAL_INDICATORS
|
||
|
||
class Meta:
|
||
db_table = "organizations_financial_indicators_extension"
|
||
verbose_name = _("финансово-экономические показатели")
|
||
verbose_name_plural = _("финансово-экономические показатели")
|
||
|
||
|
||
class GovernmentProcurementExtension(OrganizationSourceExtension):
|
||
"""Government procurement source group."""
|
||
|
||
source_group_value = SourceGroup.GOVERNMENT_PROCUREMENTS
|
||
|
||
class Meta:
|
||
db_table = "organizations_government_procurement_extension"
|
||
verbose_name = _("государственные закупки")
|
||
verbose_name_plural = _("государственные закупки")
|
||
|
||
|
||
class IndustrialProductionExtension(OrganizationSourceExtension):
|
||
"""Russian manufacturers and products source group."""
|
||
|
||
source_group_value = SourceGroup.INDUSTRIAL_PRODUCTION
|
||
|
||
class Meta:
|
||
db_table = "organizations_industrial_production_extension"
|
||
verbose_name = _("производители и продукция России")
|
||
verbose_name_plural = _("производители и продукция России")
|
||
|
||
|
||
class PlannedInspectionExtension(OrganizationSourceExtension):
|
||
"""Planned inspections source group."""
|
||
|
||
source_group_value = SourceGroup.PLANNED_INSPECTIONS
|
||
|
||
class Meta:
|
||
db_table = "organizations_planned_inspection_extension"
|
||
verbose_name = _("плановые проверки")
|
||
verbose_name_plural = _("плановые проверки")
|
||
|
||
|
||
class BankruptcyExtension(OrganizationSourceExtension):
|
||
"""Bankruptcy procedures source group."""
|
||
|
||
source_group_value = SourceGroup.BANKRUPTCY
|
||
|
||
class Meta:
|
||
db_table = "organizations_bankruptcy_extension"
|
||
verbose_name = _("сведения о процедурах банкротства")
|
||
verbose_name_plural = _("сведения о процедурах банкротства")
|
||
|
||
|
||
class DefenseSupplierExtension(OrganizationSourceExtension):
|
||
"""Defense supplier risk source group."""
|
||
|
||
source_group_value = SourceGroup.DEFENSE_SUPPLIERS
|
||
|
||
class Meta:
|
||
db_table = "organizations_defense_supplier_extension"
|
||
verbose_name = _("недобросовестные поставщики ГОЗ")
|
||
verbose_name_plural = _("недобросовестные поставщики ГОЗ")
|
||
|
||
|
||
class ArbitrationExtension(OrganizationSourceExtension):
|
||
"""Arbitration cases source group."""
|
||
|
||
source_group_value = SourceGroup.ARBITRATION
|
||
|
||
class Meta:
|
||
db_table = "organizations_arbitration_extension"
|
||
verbose_name = _("арбитражные дела")
|
||
verbose_name_plural = _("арбитражные дела")
|
||
|
||
|
||
class SecurityRegistryExtension(OrganizationSourceExtension):
|
||
"""Information security registries source group."""
|
||
|
||
source_group_value = SourceGroup.SECURITY_REGISTRIES
|
||
|
||
class Meta:
|
||
db_table = "organizations_security_registry_extension"
|
||
verbose_name = _("реестры по информационной безопасности")
|
||
verbose_name_plural = _("реестры по информационной безопасности")
|
||
|
||
|
||
class VacancyExtension(OrganizationSourceExtension):
|
||
"""Vacancies source group."""
|
||
|
||
source_group_value = SourceGroup.VACANCIES
|
||
|
||
class Meta:
|
||
db_table = "organizations_vacancy_extension"
|
||
verbose_name = _("вакансии")
|
||
verbose_name_plural = _("вакансии")
|
||
|
||
|
||
class OrganizationSourceRecord(models.Model):
|
||
"""Subordinate source record stored under a source extension."""
|
||
|
||
uid = models.UUIDField(
|
||
_("UID"),
|
||
primary_key=True,
|
||
default=uuid.uuid4,
|
||
editable=False,
|
||
)
|
||
extension = models.ForeignKey(
|
||
OrganizationSourceExtension,
|
||
on_delete=models.CASCADE,
|
||
related_name="records",
|
||
verbose_name=_("расширение источника"),
|
||
)
|
||
record_type = models.CharField(
|
||
_("тип записи"),
|
||
max_length=64,
|
||
db_index=True,
|
||
)
|
||
source = models.CharField(
|
||
_("источник"),
|
||
max_length=64,
|
||
db_index=True,
|
||
)
|
||
external_id = models.CharField(
|
||
_("внешний ID"),
|
||
max_length=255,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
title = models.TextField(
|
||
_("заголовок"),
|
||
blank=True,
|
||
)
|
||
record_date = models.CharField(
|
||
_("дата записи"),
|
||
max_length=255,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
amount = models.DecimalField(
|
||
_("сумма"),
|
||
max_digits=20,
|
||
decimal_places=2,
|
||
null=True,
|
||
blank=True,
|
||
)
|
||
status = models.CharField(
|
||
_("статус"),
|
||
max_length=255,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
url = models.TextField(
|
||
_("URL"),
|
||
blank=True,
|
||
)
|
||
payload = models.JSONField(
|
||
_("исходные данные"),
|
||
default=dict,
|
||
blank=True,
|
||
)
|
||
legacy_model = models.CharField(
|
||
_("legacy model"),
|
||
max_length=255,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
legacy_pk = models.CharField(
|
||
_("legacy pk"),
|
||
max_length=64,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
load_batch = models.PositiveIntegerField(
|
||
_("ID пакета загрузки"),
|
||
null=True,
|
||
blank=True,
|
||
db_index=True,
|
||
)
|
||
created_at = models.DateTimeField(_("дата создания"), auto_now_add=True)
|
||
updated_at = models.DateTimeField(_("дата обновления"), auto_now=True)
|
||
|
||
class Meta:
|
||
db_table = "organizations_source_record"
|
||
verbose_name = _("запись источника организации")
|
||
verbose_name_plural = _("записи источников организаций")
|
||
ordering = ["-created_at"]
|
||
constraints = [
|
||
models.UniqueConstraint(
|
||
fields=["source", "external_id"],
|
||
condition=~Q(external_id=""),
|
||
name="unique_source_record_external_id",
|
||
),
|
||
models.UniqueConstraint(
|
||
fields=["legacy_model", "legacy_pk"],
|
||
condition=~Q(legacy_model="") & ~Q(legacy_pk=""),
|
||
name="unique_source_record_legacy_identity",
|
||
),
|
||
]
|
||
indexes = [
|
||
models.Index(fields=["extension", "source"]),
|
||
models.Index(fields=["source", "record_type"]),
|
||
models.Index(fields=["load_batch", "source"]),
|
||
]
|
||
|
||
def __str__(self) -> str:
|
||
return self.title or self.external_id or str(self.uid)
|
||
|
||
@property
|
||
def id(self):
|
||
"""Compatibility alias for legacy parser services that exposed integer id."""
|
||
return self.pk
|
||
|
||
@property
|
||
def lines(self):
|
||
"""Compatibility alias for financial reports stored as source records."""
|
||
return self.financial_lines
|
||
|
||
@property
|
||
def inn(self) -> str:
|
||
"""Return the canonical organization INN or source payload INN."""
|
||
organization = self.extension.organization
|
||
return organization.inn or str((self.payload or {}).get("inn") or "")
|
||
|
||
@property
|
||
def kpp(self) -> str:
|
||
"""Return the canonical organization KPP or source payload KPP."""
|
||
organization = self.extension.organization
|
||
return organization.kpp or str((self.payload or {}).get("kpp") or "")
|
||
|
||
@property
|
||
def ogrn(self) -> str:
|
||
"""Return the canonical organization OGRN or source payload OGRN."""
|
||
organization = self.extension.organization
|
||
return organization.ogrn or str((self.payload or {}).get("ogrn") or "")
|
||
|
||
@property
|
||
def ogrip(self) -> str:
|
||
"""Return the canonical organization OGRIP or source payload OGRIP."""
|
||
organization = self.extension.organization
|
||
return organization.ogrip or str((self.payload or {}).get("ogrip") or "")
|
||
|
||
@property
|
||
def registry_organization(self):
|
||
"""Compatibility alias for callers that still use registry wording."""
|
||
return self.extension.organization
|
||
|
||
@property
|
||
def registry_organization_id(self):
|
||
return self.extension.organization_id
|
||
|
||
|
||
class OrganizationSourceFinancialLine(models.Model):
|
||
"""Structured financial report line under a source record."""
|
||
|
||
source_record = models.ForeignKey(
|
||
OrganizationSourceRecord,
|
||
on_delete=models.CASCADE,
|
||
related_name="financial_lines",
|
||
verbose_name=_("запись источника"),
|
||
)
|
||
form_code = models.CharField(_("код формы"), max_length=10, db_index=True)
|
||
line_code = models.CharField(_("код строки"), max_length=10, db_index=True)
|
||
line_name = models.CharField(_("наименование строки"), max_length=255)
|
||
year = models.PositiveSmallIntegerField(_("год"), db_index=True)
|
||
period_start = models.BigIntegerField(
|
||
_("на начало периода"),
|
||
null=True,
|
||
blank=True,
|
||
)
|
||
period_end = models.BigIntegerField(
|
||
_("на конец периода"),
|
||
null=True,
|
||
blank=True,
|
||
)
|
||
|
||
class Meta:
|
||
db_table = "organizations_source_financial_line"
|
||
verbose_name = _("строка финансового источника")
|
||
verbose_name_plural = _("строки финансовых источников")
|
||
constraints = [
|
||
models.UniqueConstraint(
|
||
fields=["source_record", "form_code", "line_code", "year"],
|
||
name="unique_source_financial_line_year",
|
||
),
|
||
]
|
||
indexes = [
|
||
models.Index(fields=["source_record", "form_code", "line_code"]),
|
||
models.Index(fields=["year", "line_code"]),
|
||
]
|
||
|
||
def __str__(self) -> str:
|
||
return f"{self.line_code} ({self.line_name[:30]}) - {self.year}"
|
||
|
||
|
||
class OrganizationDataSnapshot(models.Model):
|
||
"""Precomputed API v2 data payload for one canonical organization."""
|
||
|
||
organization = models.OneToOneField(
|
||
Organization,
|
||
on_delete=models.CASCADE,
|
||
primary_key=True,
|
||
related_name="data_snapshot",
|
||
verbose_name=_("организация"),
|
||
)
|
||
data = models.JSONField(
|
||
_("данные источников"),
|
||
default=dict,
|
||
help_text=_("Готовый JSON data для API v2"),
|
||
)
|
||
registries = models.JSONField(
|
||
_("реестры"),
|
||
default=list,
|
||
help_text=_("Готовый JSON registries для API v2"),
|
||
)
|
||
data_source_counts = models.JSONField(
|
||
_("счетчики источников"),
|
||
default=list,
|
||
help_text=_("Готовый JSON data_sources для API v2"),
|
||
)
|
||
updated_at = models.DateTimeField(
|
||
_("дата обновления"),
|
||
auto_now=True,
|
||
db_index=True,
|
||
)
|
||
|
||
class Meta:
|
||
db_table = "organizations_data_snapshot"
|
||
verbose_name = _("снапшот данных организации")
|
||
verbose_name_plural = _("снапшоты данных организаций")
|
||
|
||
def __str__(self) -> str:
|
||
return f"Snapshot for {self.organization_id}"
|
||
|
||
def save(self, *args, **kwargs) -> None:
|
||
update_fields = kwargs.get("update_fields")
|
||
if update_fields is None or "data" in update_fields:
|
||
self.data_source_counts = snapshot_data_source_summary(self.data)
|
||
if update_fields is not None:
|
||
kwargs["update_fields"] = list(
|
||
dict.fromkeys([*update_fields, "data_source_counts"])
|
||
)
|
||
|
||
super().save(*args, **kwargs)
|