feat: use authoritative organization directory
Some checks failed
CI/CD Pipeline / Quality Gate (push) Failing after 9s
CI/CD Pipeline / Build and Push Images (push) Has been skipped
CI/CD Pipeline / Deploy Dev via Compose (push) Has been skipped
CI/CD Pipeline / Internal Notify (push) Successful in 0s

This commit is contained in:
2026-06-07 16:04:43 +02:00
parent 79f0f8ebf7
commit 6ba8fa8d88
41 changed files with 2781 additions and 1058 deletions

View File

@@ -58,12 +58,63 @@ class Organization(models.Model):
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,
@@ -85,6 +136,25 @@ class Organization(models.Model):
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,
@@ -92,6 +162,184 @@ class Organization(models.Model):
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,
@@ -116,20 +364,13 @@ class Organization(models.Model):
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=["inn", "kpp"],
condition=~Q(inn="") & ~Q(kpp=""),
name="unique_org_inn_kpp_not_blank",
),
models.UniqueConstraint(
fields=["inn"],
condition=~Q(inn="") & Q(kpp="") & Q(ogrip=""),
name="unique_org_inn_without_kpp",
),
models.UniqueConstraint(
fields=["ogrip"],
condition=~Q(ogrip=""),
@@ -147,17 +388,69 @@ class Organization(models.Model):
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, "identity_status", "primary_identity"])
dict.fromkeys(
[*update_fields, "name", "identity_status", "primary_identity"]
)
)
super().save(*args, **kwargs)
@@ -517,63 +810,12 @@ class OrganizationSourceRecord(models.Model):
@property
def registry_organization(self):
"""Best-effort active registry organization matched by canonical identity."""
from django.db.models import CharField
from django.db.models.functions import Cast
from registers.models import Organization as RegistryOrganization
from registers.models import RegistryMembershipPeriod
def _registry_numeric_values(value: str) -> list[str]:
stripped = str(value or "").lstrip("0")
return [value, stripped] if stripped and stripped != value else [value]
identity_filter = Q()
inn = self.inn
ogrn = self.ogrn
ogrip = self.ogrip
if inn:
identity_filter |= Q(registry_inn_text__in=_registry_numeric_values(inn))
if ogrn:
identity_filter |= Q(registry_ogrn_text__in=_registry_numeric_values(ogrn))
if ogrip:
identity_filter |= Q(registry_ogrn_text__in=_registry_numeric_values(ogrip))
if not identity_filter:
return None
membership = (
RegistryMembershipPeriod.objects.filter(ended_at__isnull=True)
.select_related("organization")
.annotate(
registry_inn_text=Cast(
"organization__mn_inn",
output_field=CharField(),
),
registry_ogrn_text=Cast(
"organization__mn_ogrn",
output_field=CharField(),
),
)
.filter(identity_filter)
.order_by("organization__pn_name", "organization_id")
.first()
)
if membership is not None:
return membership.organization
return (
RegistryOrganization.objects.annotate(
registry_inn_text=Cast("mn_inn", output_field=CharField()),
registry_ogrn_text=Cast("mn_ogrn", output_field=CharField()),
)
.filter(identity_filter)
.order_by("pn_name", "id")
.first()
)
"""Compatibility alias for callers that still use registry wording."""
return self.extension.organization
@property
def registry_organization_id(self):
registry_organization = self.registry_organization
return registry_organization.id if registry_organization is not None else None
return self.extension.organization_id
class OrganizationSourceFinancialLine(models.Model):