arm-core/api/src/models/directories.py
Andrey Epifancev 4abfa03539 fix-волна ревью: delta статусов, блокировка версий консолидации, тесты (#96)
I-2: delta MODIFIED consolidations-api «Статусы Среза 4» (загрузка в консолидацию).
T1.1: шапки моделей — DBML v5.18.
T3.1: lock_versions_of_consolidation — двухшаговая блокировка (FOR UPDATE без членства, затем выборка членов);
тесты гонки и выборки.
M-1: тест миграции 0006 — порядок кварталов внутри года.
M-2: тест 404 для ЮЛ чужого филиала на столе своей консолидации.
T4.1: docstring supersede_previous — перенос строк.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 02:23:09 +04:00

180 lines
9.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Часть 01 DBML v5.18 «Справочники»: филиалы, ГК и ЮЛ (SCD4), алиасы, ГСК/ГСЗ (SCD2)."""
import datetime
import uuid
from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Index, Sequence, String, text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from .base import SCHEMA, Base, UUIDPrimaryMixin
from .enums import EntityType, LeHistoryEvent, RelatedGroupKind, pg_enum
#: Последовательности кодов ГК и ГСК/ГСЗ (design D-5): `GK-000001`, `GSK-000001`/`GSZ-000001`. В DBML не описываются
#: (не таблицы); создаются миграцией 0002 и `Base.metadata.create_all` (привязаны к metadata).
COMPANY_GROUP_CODE_SEQ = Sequence("company_group_code_seq", schema=SCHEMA, metadata=Base.metadata)
RELATED_GROUP_CODE_SEQ = Sequence("related_group_code_seq", schema=SCHEMA, metadata=Base.metadata)
class RegionalOffice(UUIDPrimaryMixin, Base):
"""Филиал: ГО и региональные филиалы (DECISION-3: ГО — обычная запись с `is_head_office`)."""
__tablename__ = "regional_office"
name: Mapped[str] = mapped_column(String, nullable=False, unique=True)
is_head_office: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("false"))
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("true"))
class CompanyGroup(UUIDPrimaryMixin, Base):
"""ГК — текущее состояние (SCD4); история — `company_group_history`."""
__tablename__ = "company_group"
code: Mapped[str] = mapped_column(String, nullable=False, unique=True)
crm_id: Mapped[str | None] = mapped_column(String, unique=True)
name: Mapped[str] = mapped_column(String, nullable=False)
base_regional_office_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("regional_office.id")
)
is_archived: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("false"))
valid_from: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), nullable=False)
updated_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id"))
created_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True))
updated_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True))
class CompanyGroupHistory(UUIDPrimaryMixin, Base):
"""Прошлые снимки ГК (SCD4)."""
__tablename__ = "company_group_history"
__table_args__ = (
Index("uq_company_group_history_company_group_id_valid_from", "company_group_id", "valid_from", unique=True),
)
company_group_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("company_group.id"), nullable=False
)
crm_id: Mapped[str | None] = mapped_column(String)
name: Mapped[str | None] = mapped_column(String)
base_regional_office_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True))
valid_from: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True))
valid_to: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True))
changed_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id"))
class LegalEntity(UUIDPrimaryMixin, Base):
"""Организация (ЮЛ/ИП) — текущее состояние (SCD4); `company_group_id IS NULL` = монозаёмщик."""
__tablename__ = "legal_entity"
inn: Mapped[str] = mapped_column(String, nullable=False, unique=True)
crm_id: Mapped[str | None] = mapped_column(String, unique=True)
entity_type: Mapped[EntityType | None] = mapped_column(pg_enum("entity_type"))
name: Mapped[str] = mapped_column(String, nullable=False)
company_group_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("company_group.id"))
regional_office_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("regional_office.id"))
close_date: Mapped[datetime.date | None] = mapped_column(Date)
valid_from: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), nullable=False)
updated_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id"))
created_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True))
updated_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True))
# v5.17, последней колонкой (как её добавляет миграция 0005): КПП, nullable; у ИП запрещён — проверяет ядро
kpp: Mapped[str | None] = mapped_column(String)
class LegalEntityHistory(UUIDPrimaryMixin, Base):
"""Прошлые снимки ЮЛ (SCD4) и события для архива ГК."""
__tablename__ = "legal_entity_history"
__table_args__ = (
Index("uq_legal_entity_history_legal_entity_id_valid_from", "legal_entity_id", "valid_from", unique=True),
)
legal_entity_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("legal_entity.id"), nullable=False
)
inn: Mapped[str | None] = mapped_column(String)
crm_id: Mapped[str | None] = mapped_column(String)
entity_type: Mapped[EntityType | None] = mapped_column(pg_enum("entity_type"))
name: Mapped[str | None] = mapped_column(String)
company_group_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True))
regional_office_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True))
close_date: Mapped[datetime.date | None] = mapped_column(Date)
event_type: Mapped[LeHistoryEvent | None] = mapped_column(pg_enum("le_history_event"))
effective_date: Mapped[datetime.date | None] = mapped_column(Date)
valid_from: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True))
valid_to: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True))
changed_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id"))
kpp: Mapped[str | None] = mapped_column(String) # v5.17, последней колонкой: снимок КПП
class LegalEntityAlias(UUIDPrimaryMixin, Base):
"""Словарь соответствий наименований контрагентов (ТЗ 6.5)."""
__tablename__ = "legal_entity_alias"
__table_args__ = (
Index(
"uq_legal_entity_alias_legal_entity_id_normalized_alias", "legal_entity_id", "normalized_alias", unique=True
),
Index("ix_legal_entity_alias_normalized_alias", "normalized_alias"),
)
legal_entity_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("legal_entity.id"), nullable=False
)
alias: Mapped[str] = mapped_column(String, nullable=False)
normalized_alias: Mapped[str] = mapped_column(String, nullable=False)
created_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id"))
created_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True))
class RelatedGroup(UUIDPrimaryMixin, Base):
"""ГСК/ГСЗ — текущее состояние; иерархия групп не поддерживается (ТЗ 6.1)."""
__tablename__ = "related_group"
code: Mapped[str] = mapped_column(String, nullable=False, unique=True)
company_group_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("company_group.id"), nullable=False
)
kind: Mapped[RelatedGroupKind] = mapped_column(pg_enum("related_group_kind"), nullable=False)
name: Mapped[str] = mapped_column(String, nullable=False)
created_date: Mapped[datetime.date | None] = mapped_column(Date)
class RelatedGroupMember(UUIDPrimaryMixin, Base):
"""Состав ГСК/ГСЗ — SCD2; исключение = закрытие версии (`is_current=false`)."""
__tablename__ = "related_group_member"
__table_args__ = (
# (related_group_id, legal_entity_id): partial UNIQUE WHERE is_current
Index(
"uq_related_group_member_group_entity_current",
"related_group_id",
"legal_entity_id",
unique=True,
postgresql_where=text("is_current"),
),
# (legal_entity_id, kind): partial UNIQUE WHERE is_current — v5.14: ЮЛ в одной ГСК И одной ГСЗ одновременно
Index(
"uq_related_group_member_entity_kind_current",
"legal_entity_id",
"kind",
unique=True,
postgresql_where=text("is_current"),
),
)
related_group_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("related_group.id"), nullable=False
)
legal_entity_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("legal_entity.id"), nullable=False
)
kind: Mapped[RelatedGroupKind] = mapped_column(pg_enum("related_group_kind"), nullable=False)
valid_from: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), nullable=False)
valid_to: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True))
is_current: Mapped[bool] = mapped_column(Boolean, nullable=False)
created_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id"))