From 48525731ff136437fd3161efea76a9a2792c150c Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 19:32:48 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20ORM-=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=20=D1=87=D0=B0=D1=81=D1=82=D0=B5=D0=B9=2007=E2=80=9309?= =?UTF-8?q?=20DBML=20v5.13=20(=D0=BA=D0=BE=D0=BD=D1=81=D0=BE=D0=BB=D0=B8?= =?UTF-8?q?=D0=B4=D0=B0=D1=86=D0=B8=D1=8F,=20=D0=9A=D0=A4=D0=9E,=20=D0=B0?= =?UTF-8?q?=D1=83=D0=B4=D0=B8=D1=82/=D0=95=D0=A5,=20required=5Fdocument=5F?= =?UTF-8?q?rule)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- api/src/models/__init__.py | 5 +- api/src/models/audit_ex.py | 156 +++++++++++++++++++++++++ api/src/models/consolidation.py | 158 +++++++++++++++++++++++++ api/src/models/kfo.py | 197 ++++++++++++++++++++++++++++++++ 4 files changed, 515 insertions(+), 1 deletion(-) create mode 100644 api/src/models/audit_ex.py create mode 100644 api/src/models/consolidation.py create mode 100644 api/src/models/kfo.py diff --git a/api/src/models/__init__.py b/api/src/models/__init__.py index dc6ac3d..fcbbb72 100644 --- a/api/src/models/__init__.py +++ b/api/src/models/__init__.py @@ -3,7 +3,7 @@ Импорт всех модулей моделей обязателен: он регистрирует таблицы в `Base.metadata` (create_all, alembic). """ -from . import access, accounting, directories, files, osv, statements +from . import access, accounting, audit_ex, consolidation, directories, files, kfo, osv, statements from .base import Base, UUIDPrimaryMixin #: Версия DBML, которой соответствуют модели (сверяется с шапкой DBML в check_dbml_vs_orm). @@ -15,8 +15,11 @@ __all__ = [ "UUIDPrimaryMixin", "access", "accounting", + "audit_ex", + "consolidation", "directories", "files", + "kfo", "osv", "statements", ] diff --git a/api/src/models/audit_ex.py b/api/src/models/audit_ex.py new file mode 100644 index 0000000..04af415 --- /dev/null +++ b/api/src/models/audit_ex.py @@ -0,0 +1,156 @@ +"""Часть 09 DBML v5.13 «Комментарии, аудит, ЕХФД, прочее»: комментарии, журнал аудита, срезы ЕХ, чек-лист документов. + +`audit_event` — append-only, высокообъёмная (before/after — jsonb). Срезы ЕХ самодостаточны (v5.12): FK на +консолидацию и её результаты отсутствуют (их удаляют по сроку, срез хранится «вечно»): `ex_snapshot.source_version_id` +— uuid без FK. `required_document_rule` (секция DBML «ПРОЧЕЕ») лежит здесь по рулингу R-4. +""" + +import datetime +import uuid +from typing import Any + +from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Index, Integer, SmallInteger, String, Text +from sqlalchemy.dialects.postgresql import JSONB, UUID +from sqlalchemy.orm import Mapped, mapped_column + +from .base import Base, UUIDPrimaryMixin +from .enums import ( + AuditAction, + AuditSection, + ConsolidationApproach, + DocumentType, + ExSnapshotStatus, + PeriodQuarter, + pg_enum, +) + + +class Comment(UUIDPrimaryMixin, Base): + """Комментарий к объекту (документ, версия, строка КФО и т.п.; ТЗ 6.3/6.4); связь с объектом — полиморфная.""" + + __tablename__ = "comment" + __table_args__ = (Index("ix_comment_object_type_object_id", "object_type", "object_id"),) + + object_type: Mapped[str] = mapped_column(String, nullable=False) + object_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False) + text: Mapped[str] = mapped_column(Text, nullable=False) + author_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id"), nullable=False) + created_at: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), nullable=False) + + +class AuditEvent(UUIDPrimaryMixin, Base): + """Журнал изменений (ТЗ 8.2). Append-only; `occurred_at` проставляет приложение (default в DBML нет).""" + + __tablename__ = "audit_event" + __table_args__ = ( + Index("ix_audit_event_version_occurred_at", "consolidation_version_id", "occurred_at"), + Index("ix_audit_event_object_type_object_id", "object_type", "object_id"), + Index("ix_audit_event_user_occurred_at", "user_id", "occurred_at"), + ) + + occurred_at: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), nullable=False) + user_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id")) + action: Mapped[AuditAction] = mapped_column(pg_enum("audit_action"), nullable=False) + section: Mapped[AuditSection] = mapped_column(pg_enum("audit_section"), nullable=False) + object_type: Mapped[str] = mapped_column(String, nullable=False) + object_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True)) + consolidation_version_id: Mapped[uuid.UUID | None] = mapped_column( + UUID(as_uuid=True), ForeignKey("consolidation_version.id") + ) + description: Mapped[str | None] = mapped_column(Text) + before_value: Mapped[Any | None] = mapped_column(JSONB) + after_value: Mapped[Any | None] = mapped_column(JSONB) + comment: Mapped[str | None] = mapped_column(Text) + result: Mapped[str | None] = mapped_column(String) + changed_field: Mapped[str | None] = mapped_column(String) + ip_address: Mapped[str | None] = mapped_column(String) + user_agent: Mapped[str | None] = mapped_column(String) + + +class ExSnapshot(UUIDPrimaryMixin, Base): + """Срез ЕХ: одна ГСК на одну отчётную дату (ЕХ 6.2). Неизменяем и самодостаточен — без FK на консолидацию.""" + + __tablename__ = "ex_snapshot" + __table_args__ = ( + Index( + "uq_ex_snapshot_group_year_quarter_version", + "related_group_id", + "year", + "quarter", + "version_no", + unique=True, + ), + Index("ix_ex_snapshot_group_year_quarter_status", "related_group_id", "year", "quarter", "status"), + ) + + related_group_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("related_group.id"), nullable=False + ) + company_group_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("company_group.id"), nullable=False + ) + reporting_date: Mapped[datetime.date] = mapped_column(Date, nullable=False) + year: Mapped[int] = mapped_column(SmallInteger, nullable=False) + quarter: Mapped[PeriodQuarter] = mapped_column(pg_enum("period_quarter"), nullable=False) + version_no: Mapped[int] = mapped_column(Integer, nullable=False) + status: Mapped[ExSnapshotStatus] = mapped_column(pg_enum("ex_snapshot_status"), nullable=False) + # id consolidation_version-источника БЕЗ FK (v5.12): источник удаляется по сроку, срез — нет + source_version_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False, unique=True) + source_version_no: Mapped[int] = mapped_column(Integer, nullable=False) + superseded_by_snapshot_id: Mapped[uuid.UUID | None] = mapped_column( + UUID(as_uuid=True), ForeignKey("ex_snapshot.id") + ) + verified_at: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), nullable=False) + written_at: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), nullable=False) + written_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id")) + archived_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True)) + data_lake_ref: Mapped[str | None] = mapped_column(String) + + +class ExSnapshotMember(Base): + """Периметр среза на дату (ЕХ 6.3); `inn`/`name` денормализованы; составной PK (ex_snapshot_id, legal_entity_id).""" + + __tablename__ = "ex_snapshot_member" + + ex_snapshot_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("ex_snapshot.id"), primary_key=True + ) + legal_entity_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("legal_entity.id"), primary_key=True + ) + inn: Mapped[str] = mapped_column(String, nullable=False) + name: Mapped[str] = mapped_column(String, nullable=False) + + +class ExSnapshotItem(UUIDPrimaryMixin, Base): + """Состав среза: `primary_document` -> `document` (исходники хранятся весь срок), всё остальное -> `payload`.""" + + __tablename__ = "ex_snapshot_item" + __table_args__ = (Index("ix_ex_snapshot_item_snapshot_item_type", "ex_snapshot_id", "item_type"),) + + ex_snapshot_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("ex_snapshot.id"), nullable=False) + item_type: Mapped[str] = mapped_column(String, nullable=False) + legal_entity_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("legal_entity.id")) + document_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("document.id")) + payload: Mapped[Any | None] = mapped_column(JSONB) + payload_schema_version: Mapped[str | None] = mapped_column(String) + + +class RequiredDocumentRule(UUIDPrimaryMixin, Base): + """Состав обязательных документов по подходу — основа чек-листа (ТЗ 5.1/6.2, сноска 12).""" + + __tablename__ = "required_document_rule" + __table_args__ = ( + Index( + "uq_required_document_rule_approach_type_account", + "approach", + "document_type", + "account_id", + unique=True, + ), + ) + + approach: Mapped[ConsolidationApproach] = mapped_column(pg_enum("consolidation_approach"), nullable=False) + document_type: Mapped[DocumentType] = mapped_column(pg_enum("document_type"), nullable=False) + account_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("chart_of_account.id")) + is_mandatory: Mapped[bool] = mapped_column(Boolean, nullable=False) diff --git a/api/src/models/consolidation.py b/api/src/models/consolidation.py new file mode 100644 index 0000000..53135a6 --- /dev/null +++ b/api/src/models/consolidation.py @@ -0,0 +1,158 @@ +"""Часть 07 DBML v5.13 «Консолидация»: консолидация ГК за период, версии, маршрут, блокировка, периметр, документы. + +Производные консолидации (сводная ОСВ, пары ВГО, шахматка) НЕ хранятся (DECISION-5): считаются запросом из +`osv_entry` × `consolidation_member` × `elimination_rule`. Версия после VERIFIED — read-only (ТЗ 6.10). +Длинные имена FK на `consolidation_version` заданы явно (по конвенции R-9 > 63 символов PostgreSQL). +""" + +import datetime +import uuid + +from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Index, Integer, SmallInteger, String, Text, text +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column + +from .base import Base, UUIDPrimaryMixin +from .enums import ( + ConsolidationApproach, + ConsolidationScenario, + ConsolidationStatus, + PeriodQuarter, + pg_enum, +) + + +class Consolidation(UUIDPrimaryMixin, Base): + """Логическая консолидация ГК за период (ТЗ 6.10); версии различаются периметром `consolidation_member`.""" + + __tablename__ = "consolidation" + __table_args__ = (Index("uq_consolidation_group_year_quarter", "company_group_id", "year", "quarter", unique=True),) + + company_group_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("company_group.id"), nullable=False + ) + related_group_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("related_group.id")) + year: Mapped[int] = mapped_column(SmallInteger, nullable=False) + quarter: Mapped[PeriodQuarter] = mapped_column(pg_enum("period_quarter"), nullable=False) + reporting_date: Mapped[datetime.date] = mapped_column(Date, nullable=False) + created_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True)) + + +class ConsolidationVersion(UUIDPrimaryMixin, Base): + """Версия консолидации: сценарий, подход, статус, маршрут согласования, решение УОБ (ТЗ 5.1, 6.10).""" + + __tablename__ = "consolidation_version" + __table_args__ = ( + Index("uq_consolidation_version_consolidation_id_version_no", "consolidation_id", "version_no", unique=True), + ) + + consolidation_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("consolidation.id"), nullable=False + ) + version_no: Mapped[int] = mapped_column(Integer, nullable=False) + status: Mapped[ConsolidationStatus] = mapped_column(pg_enum("consolidation_status"), nullable=False) + scenario: Mapped[ConsolidationScenario] = mapped_column(pg_enum("consolidation_scenario"), nullable=False) + approach: Mapped[ConsolidationApproach] = mapped_column( + pg_enum("consolidation_approach"), nullable=False, server_default=text("'BASIC'") + ) + goodwill_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("false")) + initiator_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id"), nullable=False) + initiator_office_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("regional_office.id"), nullable=False + ) + approved_in_rf_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id")) + approved_in_rf_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True)) + verified_in_go_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id")) + verified_in_go_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True)) + uob_decision_no: Mapped[str | None] = mapped_column(String) + uob_decision_date: Mapped[datetime.date | None] = mapped_column(Date) + methodology_deviations: Mapped[str | None] = mapped_column(Text) + elimination_rules_version: Mapped[int | None] = mapped_column(Integer) + is_in_work: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("true")) + previous_version_id: Mapped[uuid.UUID | None] = mapped_column( + UUID(as_uuid=True), + ForeignKey("consolidation_version.id", name="fk_consolidation_version_previous_version_id"), + ) + cancelled_reason: Mapped[str | None] = mapped_column(Text) + created_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True)) + created_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id")) + updated_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id")) + updated_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True)) + + +class ConsolidationStatusHistory(UUIDPrimaryMixin, Base): + """Маршрут согласования: журнал смен статуса версии (ТЗ 6.10, 8.2).""" + + __tablename__ = "consolidation_status_history" + + version_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), + ForeignKey("consolidation_version.id", name="fk_consolidation_status_history_version_id"), + nullable=False, + ) + from_status: Mapped[ConsolidationStatus | None] = mapped_column(pg_enum("consolidation_status")) + to_status: Mapped[ConsolidationStatus] = mapped_column(pg_enum("consolidation_status"), nullable=False) + changed_by: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id"), nullable=False) + changed_at: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), nullable=False) + comment: Mapped[str | None] = mapped_column(Text) + + +class ConsolidationLock(UUIDPrimaryMixin, Base): + """Взятие версии в работу: ответственный сейчас = активная блокировка (`released_at IS NULL`).""" + + __tablename__ = "consolidation_lock" + __table_args__ = ( + # не более одной активной блокировки на версию + Index( + "uq_consolidation_lock_version_active", + "consolidation_version_id", + unique=True, + postgresql_where=text("released_at IS NULL"), + ), + ) + + consolidation_version_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), + ForeignKey("consolidation_version.id", name="fk_consolidation_lock_consolidation_version_id"), + nullable=False, + ) + locked_by: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id"), nullable=False) + locked_at: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), nullable=False) + released_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True)) + taken_over_from: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id")) + takeover_reason: Mapped[str | None] = mapped_column(Text) + + +class ConsolidationMember(UUIDPrimaryMixin, Base): + """Зафиксированный периметр версии: изменение `related_group_member` существующие версии не меняет.""" + + __tablename__ = "consolidation_member" + __table_args__ = ( + Index("uq_consolidation_member_version_entity", "consolidation_version_id", "legal_entity_id", unique=True), + ) + + consolidation_version_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), + ForeignKey("consolidation_version.id", name="fk_consolidation_member_consolidation_version_id"), + nullable=False, + ) + legal_entity_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("legal_entity.id"), nullable=False + ) + is_head_entity: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("false")) + + +class ConsolidationDocument(UUIDPrimaryMixin, Base): + """Явная привязка логических документов к версии (Прил. 2 ТЗ); чек-лист = required_document_rule × периметр.""" + + __tablename__ = "consolidation_document" + __table_args__ = ( + Index("uq_consolidation_document_version_document", "consolidation_version_id", "document_id", unique=True), + ) + + consolidation_version_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), + ForeignKey("consolidation_version.id", name="fk_consolidation_document_consolidation_version_id"), + nullable=False, + ) + document_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("document.id"), nullable=False) diff --git a/api/src/models/kfo.py b/api/src/models/kfo.py new file mode 100644 index 0000000..dc0df0f --- /dev/null +++ b/api/src/models/kfo.py @@ -0,0 +1,197 @@ +"""Часть 08 DBML v5.13 «КФО»: корректировки, гудвилл, результат КФО (шапка, строки, разрез по компаниям), КФО клиента. + +Корректировки и гудвилл — ручные решения аналитика, хранятся поверх первички (overlay). Результат КФО +замораживается при VERIFIED. Деньги — `numeric(18,2)`, проценты — `numeric(7,4)` / `numeric(9,4)`. +""" + +import datetime +import decimal +import uuid + +from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, Numeric, String, Text, text +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column + +from .base import Base, UUIDPrimaryMixin +from .enums import CorrectionStatus, StatementType, pg_enum + + +class Correction(UUIDPrimaryMixin, Base): + """Корректировка ВГО: итог = свод первички − Σ применённых корректировок; пара — по (rule_id, компании).""" + + __tablename__ = "correction" + __table_args__ = ( + Index("ix_correction_consolidation_version_id", "consolidation_version_id"), + Index( + "ix_correction_version_rule_entities", + "consolidation_version_id", + "rule_id", + "legal_entity_id", + "counterparty_id", + ), + ) + + consolidation_version_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("consolidation_version.id"), nullable=False + ) + statement_type: Mapped[StatementType] = mapped_column(pg_enum("statement_type"), nullable=False) + rule_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("elimination_rule.id")) + parent_correction_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("correction.id")) + legal_entity_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("legal_entity.id")) + counterparty_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("legal_entity.id")) + debit_line_code: Mapped[str | None] = mapped_column(String) + debit_account_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("chart_of_account.id")) + debit_adjustment: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + credit_line_code: Mapped[str | None] = mapped_column(String) + credit_account_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("chart_of_account.id")) + credit_adjustment: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + deviation_amount: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + deviation_percent: Mapped[decimal.Decimal | None] = mapped_column(Numeric(9, 4)) + reason_code: Mapped[str | None] = mapped_column(String) + comment: Mapped[str] = mapped_column(Text, nullable=False) + status: Mapped[CorrectionStatus] = mapped_column( + pg_enum("correction_status"), nullable=False, server_default=text("'DRAFT'") + ) + version_no: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("1")) + 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)) + updated_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id")) + updated_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True)) + + +class Goodwill(UUIDPrimaryMixin, Base): + """Гудвилл по паре «головная организация — дочерняя» (ТЗ 6.7); доля участия вводится вручную, %.""" + + __tablename__ = "goodwill" + __table_args__ = ( + Index( + "uq_goodwill_version_head_subsidiary", + "consolidation_version_id", + "head_legal_entity_id", + "subsidiary_legal_entity_id", + unique=True, + ), + ) + + consolidation_version_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("consolidation_version.id"), nullable=False + ) + head_legal_entity_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("legal_entity.id"), nullable=False + ) + subsidiary_legal_entity_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("legal_entity.id"), nullable=False + ) + ownership_share: Mapped[decimal.Decimal | None] = mapped_column(Numeric(7, 4)) + financial_investment_amount: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + charter_capital_amount: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + additional_capital_amount: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + participation_amount: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + goodwill_amount: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + include_in_kfo: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("false")) + comment: Mapped[str | None] = mapped_column(Text) + 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)) + updated_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id")) + updated_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True)) + + +class KfoResult(UUIDPrimaryMixin, Base): + """Шапка результата КФО версии + контрольные пороги (ТЗ 6.8); `is_frozen = true` после VERIFIED.""" + + __tablename__ = "kfo_result" + + consolidation_version_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("consolidation_version.id"), nullable=False, unique=True + ) + computed_at: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), nullable=False) + is_frozen: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("false")) + net_assets: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + balance_total: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + revenue: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + sales_profit: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + total_deviation_bb: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + total_deviation_ofr: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + threshold_bb_exceeded: Mapped[bool | None] = mapped_column(Boolean) + threshold_ofr_exceeded: Mapped[bool | None] = mapped_column(Boolean) + threshold_justification: Mapped[str | None] = mapped_column(Text) + goodwill_total: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + has_deviations: Mapped[bool | None] = mapped_column(Boolean) + + +class KfoResultLine(UUIDPrimaryMixin, Base): + """Строка КФО (ФСБУ 4/2023): сумма по компаниям -> авто-элиминация -> корректировки -> итог.""" + + __tablename__ = "kfo_result_line" + __table_args__ = ( + Index( + "uq_kfo_result_line_result_type_code", + "kfo_result_id", + "statement_type", + "line_code", + unique=True, + ), + ) + + kfo_result_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("kfo_result.id"), nullable=False) + statement_type: Mapped[StatementType] = mapped_column(pg_enum("statement_type"), nullable=False) + line_code: Mapped[str] = mapped_column(String, nullable=False) + line_name: Mapped[str | None] = mapped_column(String) + sum_amount: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + eliminated_auto: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + eliminated_manual: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + goodwill_adjust: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + consolidated_amount: Mapped[decimal.Decimal] = mapped_column(Numeric(18, 2), nullable=False) + vgo_amount: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + source_kind: Mapped[str | None] = mapped_column(String) + deviation_amount: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + comment: Mapped[str | None] = mapped_column(Text) + ordinal: Mapped[int | None] = mapped_column(Integer) + + +class KfoResultLineCompany(Base): + """Разрез статьи КФО по компаниям (ЕХ 6.6); составной PK (kfo_result_line_id, legal_entity_id).""" + + __tablename__ = "kfo_result_line_company" + + kfo_result_line_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("kfo_result_line.id"), primary_key=True + ) + legal_entity_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("legal_entity.id"), primary_key=True + ) + amount: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + share_percent: Mapped[decimal.Decimal | None] = mapped_column(Numeric(7, 4)) + + +class ClientKfoLine(UUIDPrimaryMixin, Base): + """Сравнение с КФО клиента: ручной ввод сумм клиента и расхождение относительно наименьшего (Методика, сн. 14).""" + + __tablename__ = "client_kfo_line" + __table_args__ = ( + Index( + "uq_client_kfo_line_version_type_code", + "consolidation_version_id", + "statement_type", + "line_code", + unique=True, + ), + ) + + consolidation_version_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), + ForeignKey("consolidation_version.id", name="fk_client_kfo_line_consolidation_version_id"), + nullable=False, + ) + statement_type: Mapped[StatementType] = mapped_column(pg_enum("statement_type"), nullable=False) + line_code: Mapped[str] = mapped_column(String, nullable=False) + client_amount: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + client_vgo_amount: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + bank_amount: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + bank_vgo_amount: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + deviation_amount: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2)) + deviation_percent: Mapped[decimal.Decimal | None] = mapped_column(Numeric(9, 4)) + deviation_reason: Mapped[str | None] = mapped_column(Text) + comment: Mapped[str | None] = mapped_column(Text) + entered_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id")) + entered_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True))