From ed5ed43fa62e72619b9733d92bafa2088cc0fed8 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 20:27:23 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=200001=5Fdbml=5Fv5=5F13=20=E2=80=94=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D0=BD=D1=8B=D0=B9=20DDL=20DBML=20v5.13=20=D1=81=20=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=D1=82=D0=BE=D0=BC;=20=D1=82=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D1=8B=20=D1=86=D0=B8=D0=BA=D0=BB=D0=B0=20=D0=B8=20make?= =?UTF-8?q?=20migration-cycle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Явный DDL без импорта src.models: схема dkb, 27 enum-типов (явный CREATE/DROP TYPE), 43 таблицы с индексами (partial unique, NULLS NOT DISTINCT), FK цикла stored_file -> recognition_job отдельным шагом; downgrade — FK цикла, таблицы в обратном порядке, типы. Тесты (integration, scratch-БД, alembic подпроцессом): upgrade -> downgrade -> upgrade, пустой compare_metadata и alembic check, каталог = create_all ORM, поведение partial unique/NULLS NOT DISTINCT/FK-цикла; офлайн-SQL — CREATE SCHEMA первым. Co-Authored-By: Claude Fable 5.1 --- Makefile | 7 +- api/alembic/versions/0001_dbml_v5_13.py | 1481 +++++++++++++++++++++++ api/pyproject.toml | 4 + api/tests/test_migrations.py | 284 +++++ 4 files changed, 1775 insertions(+), 1 deletion(-) create mode 100644 api/alembic/versions/0001_dbml_v5_13.py create mode 100644 api/tests/test_migrations.py diff --git a/Makefile b/Makefile index 34f3984..b0d16b5 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ COMPOSE_FILE ?= ../docs/dev/docker-compose.yml # DBML относительно api/: и в основном workspace, и в worktree .wt/<срез>/ это соседний docs/ DBML ?= ../../docs/model/data-model-v5.13.dbml -.PHONY: venv lint test test-integration dbml-check check check-full up migrate seed seed-dev +.PHONY: venv lint test test-integration dbml-check migration-cycle check check-full up migrate seed seed-dev # Python 3.11 (версия платформы); внутренние пакеты raisa_* вне банка недоступны — пропускаем их venv: @@ -28,6 +28,11 @@ test-integration: dbml-check: cd api && $(BIN)/python scripts/check_dbml_vs_orm.py $(DBML) +# Миграции на временной БД (spec data-model): upgrade head -> downgrade base -> upgrade head, пустой autogenerate, +# схема после миграции = create_all ORM. Подключается в check-full (задача 25) +migration-cycle: + cd api && $(BIN)/pytest -m integration tests/test_migrations.py + check: lint test # TODO (задача 25): добавить фронт (lint/test/build web/), проверку миграций с нуля и откат, актуальность OpenAPI diff --git a/api/alembic/versions/0001_dbml_v5_13.py b/api/alembic/versions/0001_dbml_v5_13.py new file mode 100644 index 0000000..5a68308 --- /dev/null +++ b/api/alembic/versions/0001_dbml_v5_13.py @@ -0,0 +1,1481 @@ +"""DBML v5.13: вся модель данных АРМ КА с нуля (схема dkb, 27 enum-типов, 43 таблицы, ограничения, индексы). + +Ревизия заморожена: явный DDL, модели `src.models` не импортируются. Имена ограничений и индексов совпадают с ORM +(naming_convention `src/models/base.py` + явные имена); `alembic check` после upgrade не находит расхождений. +Черновик — autogenerate, вручную: явное создание/удаление enum-типов, FK цикла stored_file <-> recognition_job +отдельным шагом, полный downgrade (design D-2). + +Revision ID: 0001_dbml_v5_13 +Revises: +Create Date: 2026-10-03 +""" + +from collections.abc import Sequence + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +from alembic import op + +revision: str = "0001_dbml_v5_13" +down_revision: str | Sequence[str] | None = None +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + +SCHEMA = "dkb" +FK_CYCLE = "fk_stored_file_current_recognition_job_id_recognition_job" + + +def _enum(name: str, *values: str) -> postgresql.ENUM: + """Нативный PG-enum `dkb.`; тип создаётся явно в upgrade(), а не неявно из create_table.""" + return postgresql.ENUM(*values, name=name, schema=SCHEMA, create_type=False) + + +#: 27 enum-типов DBML v5.13 в порядке DBML; значения — в порядке DBML (включая кириллицу `ЮЛ`/`ИП`). +ENUMS: dict[str, postgresql.ENUM] = { + enum_type.name: enum_type + for enum_type in ( + _enum("entity_type", "ЮЛ", "ИП"), + _enum("related_group_kind", "gsk", "gsz"), + _enum("account_kind", "A", "P", "AP"), + _enum("account_row_type", "account", "subaccount", "analytic"), + _enum("business_segment", "DKB", "DMSB", "DM"), + _enum("document_type", "BALANCE", "INCOME", "OSV", "DISCLOSURE", "OTHER"), + _enum("file_type", "XLS", "XLSX", "XLSB", "PDF", "PNG", "JPG"), + _enum("recognition_pipeline", "tabular", "image"), + _enum("period_quarter", "Q1", "Q2", "Q3", "Q4"), + _enum("file_status", "UPLOADED", "PROCESSING", "PROCESSED", "ERROR", "SUPERSEDED"), + _enum("document_status", "RECOGNIZED", "NEEDS_CHECK", "CONFIRMED", "ERROR"), + _enum("s3_bucket_role", "uploads", "results", "exports", "archive"), + _enum( + "recognition_status", "queued", "published", "processing", "done", "needs_check", "error", "timeout", "dlq" + ), + _enum("statement_type", "balance", "income"), + _enum("osv_group_type", "ACCOUNT", "DEPARTMENT", "COUNTERPARTY", "CONTRACT", "OTHER"), + _enum("osv_data_type", "BU", "CURRENCY", "QUANTITY", "SUM"), + _enum("consolidation_scenario", "CREATE", "CONFIRM"), + _enum("consolidation_approach", "BASIC", "EXTENDED"), + _enum( + "consolidation_status", + "INITIATED", + "DOCUMENTS_PROCESSING", + "DATA_CORRECTION", + "CONSOLIDATION_RF", + "ADJUSTMENT", + "VERIFICATION_RF", + "CONSOLIDATION_GO", + "VERIFICATION_GO", + "VERIFIED", + "CANCELLED", + "REJECTED", + "ARCHIVED", + ), + _enum("correction_status", "DRAFT", "APPLIED", "REVOKED"), + _enum("document_check_result", "MATCH", "MISMATCH", "UNKNOWN"), + _enum("le_history_event", "CREATE", "UPDATE", "MOVE_TO_GROUP", "CLOSE", "REMOVE_FROM_GROUP"), + _enum("ex_snapshot_status", "ACTUAL", "ARCHIVE"), + _enum("permission_object", "DIRECTORY", "DOCUMENT", "CONSOLIDATION", "KFO", "EX", "EXPORT", "USERS", "AUDIT"), + _enum("permission_action", "VIEW", "EDIT", "APPROVE", "VERIFY", "DELETE", "EXPORT", "ADMIN"), + _enum( + "audit_section", + "DIRECTORIES", + "CONSOLIDATION", + "PRIMARY_DOCUMENTS", + "RECOGNITION", + "KFO", + "EXPORT", + "ACCESS", + "SESSION", + "USERS", + ), + _enum( + "audit_action", + "LOGIN", + "LOGIN_FAILED", + "LOGOUT", + "CONSOLIDATION_CREATE", + "CONSOLIDATION_STATUS_CHANGE", + "CONSOLIDATION_APPROVE", + "CONSOLIDATION_VERIFY", + "GROUP_CHANGE", + "MEMBERSHIP_CHANGE", + "ASSIGNMENT_CHANGE", + "FILE_UPLOAD", + "FILE_DELETE", + "FILE_REPLACE", + "DOCUMENT_CONFIRM", + "CONSOLIDATION_LOCK", + "CONSOLIDATION_LOCK_TAKEOVER", + "DISCLOSURE_EDIT", + "RECOGNIZED_DATA_EDIT", + "OSV_COUNTERPARTY_MAP", + "OSV_TYPE_OVERRIDE", + "CORRECTION_CREATE", + "CORRECTION_EDIT", + "CORRECTION_REVOKE", + "GOODWILL_EDIT", + "EXPORT", + "DIRECTORY_IMPORT", + "USER_ROLE_CHANGE", + "ROLE_PERMISSION_CHANGE", + ), + ) +} + +#: 43 таблицы в порядке создания (зависимости FK); downgrade удаляет их в обратном порядке. +TABLES: tuple[str, ...] = ( + "chart_of_account", + "elimination_rule", + "permission", + "regional_office", + "role", + "s3_object", + "app_user", + "elimination_rule_account", + "required_document_rule", + "comment", + "company_group", + "role_permission", + "user_role", + "company_group_history", + "legal_entity", + "related_group", + "consolidation", + "ex_snapshot", + "legal_entity_alias", + "legal_entity_history", + "related_group_member", + "stored_file", + "consolidation_version", + "document", + "ex_snapshot_member", + "recognition_job", + "audit_event", + "client_kfo_line", + "consolidation_document", + "consolidation_lock", + "consolidation_member", + "consolidation_status_history", + "correction", + "ex_snapshot_item", + "financial_statement_meta", + "goodwill", + "kfo_result", + "osv_entry", + "financial_statement_column", + "financial_statement_line", + "kfo_result_line", + "financial_statement_value", + "kfo_result_line_company", +) + + +def upgrade() -> None: + """Схема dkb (идемпотентно) -> 27 enum-типов -> 43 таблицы с их индексами в порядке зависимостей FK. + + FK цикла `stored_file.current_recognition_job_id` добавляется сразу после создания `recognition_job`. + """ + op.execute(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}") + bind = op.get_bind() + for enum_type in ENUMS.values(): + enum_type.create(bind, checkfirst=False) + op.create_table( + "chart_of_account", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("code", sa.String(), nullable=False), + sa.Column("parent_id", sa.UUID(), nullable=True), + sa.Column("name", sa.String(), nullable=False), + sa.Column("kind", ENUMS["account_kind"], nullable=True), + sa.Column("row_type", ENUMS["account_row_type"], nullable=False), + sa.Column("is_off_balance", sa.Boolean(), server_default=sa.text("false"), nullable=False), + sa.ForeignKeyConstraint( + ["parent_id"], ["dkb.chart_of_account.id"], name=op.f("fk_chart_of_account_parent_id_chart_of_account") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_chart_of_account")), + sa.UniqueConstraint("code", name=op.f("uq_chart_of_account_code")), + schema=SCHEMA, + ) + op.create_table( + "elimination_rule", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("statement_type", ENUMS["statement_type"], nullable=False), + sa.Column("rule_no", sa.SmallInteger(), nullable=False), + sa.Column("article_1_line", sa.String(), nullable=False), + sa.Column("article_2_line", sa.String(), nullable=False), + sa.Column("side_1", sa.String(), nullable=False), + sa.Column("measure", sa.String(), nullable=False), + sa.Column("elimination_mode", sa.String(), nullable=False), + sa.Column("vat_handling", sa.String(), nullable=True), + sa.Column("excise_adjust", sa.Boolean(), server_default=sa.text("false"), nullable=False), + sa.Column("comment", sa.Text(), nullable=True), + sa.Column("version_no", sa.Integer(), nullable=False), + sa.Column("valid_from", sa.Date(), nullable=False), + sa.Column("valid_to", sa.Date(), nullable=True), + sa.PrimaryKeyConstraint("id", name=op.f("pk_elimination_rule")), + schema=SCHEMA, + ) + op.create_index( + "uq_elimination_rule_statement_type_rule_no_version_no", + "elimination_rule", + ["statement_type", "rule_no", "version_no"], + unique=True, + schema=SCHEMA, + ) + op.create_table( + "permission", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("code", sa.String(), nullable=False), + sa.Column("object_type", ENUMS["permission_object"], nullable=False), + sa.Column("action", ENUMS["permission_action"], nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.Text(), nullable=True), + sa.PrimaryKeyConstraint("id", name=op.f("pk_permission")), + sa.UniqueConstraint("code", name=op.f("uq_permission_code")), + schema=SCHEMA, + ) + op.create_index( + "uq_permission_object_type_action", "permission", ["object_type", "action"], unique=True, schema=SCHEMA + ) + op.create_table( + "regional_office", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("is_head_office", sa.Boolean(), server_default=sa.text("false"), nullable=False), + sa.Column("is_active", sa.Boolean(), server_default=sa.text("true"), nullable=False), + sa.PrimaryKeyConstraint("id", name=op.f("pk_regional_office")), + sa.UniqueConstraint("name", name=op.f("uq_regional_office_name")), + schema=SCHEMA, + ) + op.create_table( + "role", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("code", sa.String(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("is_system", sa.Boolean(), server_default=sa.text("false"), nullable=False), + sa.Column("is_active", sa.Boolean(), server_default=sa.text("true"), nullable=False), + sa.PrimaryKeyConstraint("id", name=op.f("pk_role")), + sa.UniqueConstraint("code", name=op.f("uq_role_code")), + schema=SCHEMA, + ) + op.create_table( + "s3_object", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("bucket_role", ENUMS["s3_bucket_role"], nullable=False), + sa.Column("bucket", sa.String(), nullable=False), + sa.Column("key", sa.String(), nullable=False), + sa.Column("content_type", sa.String(), nullable=True), + sa.Column("size_bytes", sa.BigInteger(), nullable=True), + sa.Column("sha256", sa.String(), nullable=True), + sa.Column("version_id", sa.String(), nullable=True), + sa.Column("created_by", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("expires_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("archived_to", sa.String(), nullable=True), + sa.PrimaryKeyConstraint("id", name=op.f("pk_s3_object")), + schema=SCHEMA, + ) + op.create_index("uq_s3_object_bucket_key", "s3_object", ["bucket", "key"], unique=True, schema=SCHEMA) + op.create_table( + "app_user", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("external_id", sa.String(), nullable=True), + sa.Column("email", sa.String(), nullable=True), + sa.Column("full_name", sa.String(), nullable=False), + sa.Column("employee_no", sa.String(), nullable=True), + sa.Column("regional_office_id", sa.UUID(), nullable=False), + sa.Column("business_segment", ENUMS["business_segment"], nullable=True), + sa.Column("is_active", sa.Boolean(), server_default=sa.text("true"), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint( + ["regional_office_id"], + ["dkb.regional_office.id"], + name=op.f("fk_app_user_regional_office_id_regional_office"), + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_app_user")), + sa.UniqueConstraint("email", name=op.f("uq_app_user_email")), + sa.UniqueConstraint("external_id", name=op.f("uq_app_user_external_id")), + schema=SCHEMA, + ) + op.create_table( + "elimination_rule_account", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("rule_id", sa.UUID(), nullable=False), + sa.Column("company_no", sa.SmallInteger(), nullable=False), + sa.Column("account_id", sa.UUID(), nullable=False), + sa.Column("is_excluded", sa.Boolean(), server_default=sa.text("false"), nullable=False), + sa.ForeignKeyConstraint( + ["account_id"], + ["dkb.chart_of_account.id"], + name=op.f("fk_elimination_rule_account_account_id_chart_of_account"), + ), + sa.ForeignKeyConstraint( + ["rule_id"], ["dkb.elimination_rule.id"], name=op.f("fk_elimination_rule_account_rule_id_elimination_rule") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_elimination_rule_account")), + schema=SCHEMA, + ) + op.create_table( + "required_document_rule", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("approach", ENUMS["consolidation_approach"], nullable=False), + sa.Column("document_type", ENUMS["document_type"], nullable=False), + sa.Column("account_id", sa.UUID(), nullable=True), + sa.Column("is_mandatory", sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint( + ["account_id"], + ["dkb.chart_of_account.id"], + name=op.f("fk_required_document_rule_account_id_chart_of_account"), + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_required_document_rule")), + schema=SCHEMA, + ) + op.create_index( + "uq_required_document_rule_approach_type_account", + "required_document_rule", + ["approach", "document_type", "account_id"], + unique=True, + schema=SCHEMA, + ) + op.create_table( + "comment", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("object_type", sa.String(), nullable=False), + sa.Column("object_id", sa.UUID(), nullable=False), + sa.Column("text", sa.Text(), nullable=False), + sa.Column("author_id", sa.UUID(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(["author_id"], ["dkb.app_user.id"], name=op.f("fk_comment_author_id_app_user")), + sa.PrimaryKeyConstraint("id", name=op.f("pk_comment")), + schema=SCHEMA, + ) + op.create_index( + "ix_comment_object_type_object_id", "comment", ["object_type", "object_id"], unique=False, schema=SCHEMA + ) + op.create_table( + "company_group", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("code", sa.String(), nullable=False), + sa.Column("crm_id", sa.String(), nullable=True), + sa.Column("name", sa.String(), nullable=False), + sa.Column("base_regional_office_id", sa.UUID(), nullable=True), + sa.Column("is_archived", sa.Boolean(), server_default=sa.text("false"), nullable=False), + sa.Column("valid_from", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_by", sa.UUID(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint( + ["base_regional_office_id"], + ["dkb.regional_office.id"], + name=op.f("fk_company_group_base_regional_office_id_regional_office"), + ), + sa.ForeignKeyConstraint(["updated_by"], ["dkb.app_user.id"], name=op.f("fk_company_group_updated_by_app_user")), + sa.PrimaryKeyConstraint("id", name=op.f("pk_company_group")), + sa.UniqueConstraint("code", name=op.f("uq_company_group_code")), + sa.UniqueConstraint("crm_id", name=op.f("uq_company_group_crm_id")), + schema=SCHEMA, + ) + op.create_table( + "role_permission", + sa.Column("role_id", sa.UUID(), nullable=False), + sa.Column("permission_id", sa.UUID(), nullable=False), + sa.Column("granted_by", sa.UUID(), nullable=True), + sa.Column("granted_at", sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint( + ["granted_by"], ["dkb.app_user.id"], name=op.f("fk_role_permission_granted_by_app_user") + ), + sa.ForeignKeyConstraint( + ["permission_id"], ["dkb.permission.id"], name=op.f("fk_role_permission_permission_id_permission") + ), + sa.ForeignKeyConstraint(["role_id"], ["dkb.role.id"], name=op.f("fk_role_permission_role_id_role")), + sa.PrimaryKeyConstraint("role_id", "permission_id", name=op.f("pk_role_permission")), + schema=SCHEMA, + ) + op.create_table( + "user_role", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("user_id", sa.UUID(), nullable=False), + sa.Column("role_id", sa.UUID(), nullable=False), + sa.ForeignKeyConstraint(["role_id"], ["dkb.role.id"], name=op.f("fk_user_role_role_id_role")), + sa.ForeignKeyConstraint(["user_id"], ["dkb.app_user.id"], name=op.f("fk_user_role_user_id_app_user")), + sa.PrimaryKeyConstraint("id", name=op.f("pk_user_role")), + schema=SCHEMA, + ) + op.create_index("uq_user_role_user_id_role_id", "user_role", ["user_id", "role_id"], unique=True, schema=SCHEMA) + op.create_table( + "company_group_history", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("company_group_id", sa.UUID(), nullable=False), + sa.Column("crm_id", sa.String(), nullable=True), + sa.Column("name", sa.String(), nullable=True), + sa.Column("base_regional_office_id", sa.UUID(), nullable=True), + sa.Column("valid_from", sa.DateTime(timezone=True), nullable=True), + sa.Column("valid_to", sa.DateTime(timezone=True), nullable=True), + sa.Column("changed_by", sa.UUID(), nullable=True), + sa.ForeignKeyConstraint( + ["changed_by"], ["dkb.app_user.id"], name=op.f("fk_company_group_history_changed_by_app_user") + ), + sa.ForeignKeyConstraint( + ["company_group_id"], + ["dkb.company_group.id"], + name=op.f("fk_company_group_history_company_group_id_company_group"), + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_company_group_history")), + schema=SCHEMA, + ) + op.create_index( + "uq_company_group_history_company_group_id_valid_from", + "company_group_history", + ["company_group_id", "valid_from"], + unique=True, + schema=SCHEMA, + ) + op.create_table( + "legal_entity", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("inn", sa.String(), nullable=False), + sa.Column("crm_id", sa.String(), nullable=True), + sa.Column("entity_type", ENUMS["entity_type"], nullable=True), + sa.Column("name", sa.String(), nullable=False), + sa.Column("company_group_id", sa.UUID(), nullable=True), + sa.Column("regional_office_id", sa.UUID(), nullable=True), + sa.Column("close_date", sa.Date(), nullable=True), + sa.Column("valid_from", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_by", sa.UUID(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint( + ["company_group_id"], ["dkb.company_group.id"], name=op.f("fk_legal_entity_company_group_id_company_group") + ), + sa.ForeignKeyConstraint( + ["regional_office_id"], + ["dkb.regional_office.id"], + name=op.f("fk_legal_entity_regional_office_id_regional_office"), + ), + sa.ForeignKeyConstraint(["updated_by"], ["dkb.app_user.id"], name=op.f("fk_legal_entity_updated_by_app_user")), + sa.PrimaryKeyConstraint("id", name=op.f("pk_legal_entity")), + sa.UniqueConstraint("crm_id", name=op.f("uq_legal_entity_crm_id")), + sa.UniqueConstraint("inn", name=op.f("uq_legal_entity_inn")), + schema=SCHEMA, + ) + op.create_table( + "related_group", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("code", sa.String(), nullable=False), + sa.Column("company_group_id", sa.UUID(), nullable=False), + sa.Column("kind", ENUMS["related_group_kind"], nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("created_date", sa.Date(), nullable=True), + sa.ForeignKeyConstraint( + ["company_group_id"], ["dkb.company_group.id"], name=op.f("fk_related_group_company_group_id_company_group") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_related_group")), + sa.UniqueConstraint("code", name=op.f("uq_related_group_code")), + schema=SCHEMA, + ) + op.create_table( + "consolidation", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("company_group_id", sa.UUID(), nullable=False), + sa.Column("related_group_id", sa.UUID(), nullable=True), + sa.Column("year", sa.SmallInteger(), nullable=False), + sa.Column("quarter", ENUMS["period_quarter"], nullable=False), + sa.Column("reporting_date", sa.Date(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint( + ["company_group_id"], ["dkb.company_group.id"], name=op.f("fk_consolidation_company_group_id_company_group") + ), + sa.ForeignKeyConstraint( + ["related_group_id"], ["dkb.related_group.id"], name=op.f("fk_consolidation_related_group_id_related_group") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_consolidation")), + schema=SCHEMA, + ) + op.create_index( + "uq_consolidation_group_year_quarter", + "consolidation", + ["company_group_id", "year", "quarter"], + unique=True, + schema=SCHEMA, + ) + op.create_table( + "ex_snapshot", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("related_group_id", sa.UUID(), nullable=False), + sa.Column("company_group_id", sa.UUID(), nullable=False), + sa.Column("reporting_date", sa.Date(), nullable=False), + sa.Column("year", sa.SmallInteger(), nullable=False), + sa.Column("quarter", ENUMS["period_quarter"], nullable=False), + sa.Column("version_no", sa.Integer(), nullable=False), + sa.Column("status", ENUMS["ex_snapshot_status"], nullable=False), + sa.Column("source_version_id", sa.UUID(), nullable=False), + sa.Column("source_version_no", sa.Integer(), nullable=False), + sa.Column("superseded_by_snapshot_id", sa.UUID(), nullable=True), + sa.Column("verified_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("written_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("written_by", sa.UUID(), nullable=True), + sa.Column("archived_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("data_lake_ref", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["company_group_id"], ["dkb.company_group.id"], name=op.f("fk_ex_snapshot_company_group_id_company_group") + ), + sa.ForeignKeyConstraint( + ["related_group_id"], ["dkb.related_group.id"], name=op.f("fk_ex_snapshot_related_group_id_related_group") + ), + sa.ForeignKeyConstraint( + ["superseded_by_snapshot_id"], + ["dkb.ex_snapshot.id"], + name=op.f("fk_ex_snapshot_superseded_by_snapshot_id_ex_snapshot"), + ), + sa.ForeignKeyConstraint(["written_by"], ["dkb.app_user.id"], name=op.f("fk_ex_snapshot_written_by_app_user")), + sa.PrimaryKeyConstraint("id", name=op.f("pk_ex_snapshot")), + sa.UniqueConstraint("source_version_id", name=op.f("uq_ex_snapshot_source_version_id")), + schema=SCHEMA, + ) + op.create_index( + "ix_ex_snapshot_group_year_quarter_status", + "ex_snapshot", + ["related_group_id", "year", "quarter", "status"], + unique=False, + schema=SCHEMA, + ) + op.create_index( + "uq_ex_snapshot_group_year_quarter_version", + "ex_snapshot", + ["related_group_id", "year", "quarter", "version_no"], + unique=True, + schema=SCHEMA, + ) + op.create_table( + "legal_entity_alias", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("legal_entity_id", sa.UUID(), nullable=False), + sa.Column("alias", sa.String(), nullable=False), + sa.Column("normalized_alias", sa.String(), nullable=False), + sa.Column("created_by", sa.UUID(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint( + ["created_by"], ["dkb.app_user.id"], name=op.f("fk_legal_entity_alias_created_by_app_user") + ), + sa.ForeignKeyConstraint( + ["legal_entity_id"], + ["dkb.legal_entity.id"], + name=op.f("fk_legal_entity_alias_legal_entity_id_legal_entity"), + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_legal_entity_alias")), + schema=SCHEMA, + ) + op.create_index( + "ix_legal_entity_alias_normalized_alias", + "legal_entity_alias", + ["normalized_alias"], + unique=False, + schema=SCHEMA, + ) + op.create_index( + "uq_legal_entity_alias_legal_entity_id_normalized_alias", + "legal_entity_alias", + ["legal_entity_id", "normalized_alias"], + unique=True, + schema=SCHEMA, + ) + op.create_table( + "legal_entity_history", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("legal_entity_id", sa.UUID(), nullable=False), + sa.Column("inn", sa.String(), nullable=True), + sa.Column("crm_id", sa.String(), nullable=True), + sa.Column("entity_type", ENUMS["entity_type"], nullable=True), + sa.Column("name", sa.String(), nullable=True), + sa.Column("company_group_id", sa.UUID(), nullable=True), + sa.Column("regional_office_id", sa.UUID(), nullable=True), + sa.Column("close_date", sa.Date(), nullable=True), + sa.Column("event_type", ENUMS["le_history_event"], nullable=True), + sa.Column("effective_date", sa.Date(), nullable=True), + sa.Column("valid_from", sa.DateTime(timezone=True), nullable=True), + sa.Column("valid_to", sa.DateTime(timezone=True), nullable=True), + sa.Column("changed_by", sa.UUID(), nullable=True), + sa.ForeignKeyConstraint( + ["changed_by"], ["dkb.app_user.id"], name=op.f("fk_legal_entity_history_changed_by_app_user") + ), + sa.ForeignKeyConstraint( + ["legal_entity_id"], + ["dkb.legal_entity.id"], + name=op.f("fk_legal_entity_history_legal_entity_id_legal_entity"), + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_legal_entity_history")), + schema=SCHEMA, + ) + op.create_index( + "uq_legal_entity_history_legal_entity_id_valid_from", + "legal_entity_history", + ["legal_entity_id", "valid_from"], + unique=True, + schema=SCHEMA, + ) + op.create_table( + "related_group_member", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("related_group_id", sa.UUID(), nullable=False), + sa.Column("legal_entity_id", sa.UUID(), nullable=False), + sa.Column("valid_from", sa.DateTime(timezone=True), nullable=False), + sa.Column("valid_to", sa.DateTime(timezone=True), nullable=True), + sa.Column("is_current", sa.Boolean(), nullable=False), + sa.Column("created_by", sa.UUID(), nullable=True), + sa.ForeignKeyConstraint( + ["created_by"], ["dkb.app_user.id"], name=op.f("fk_related_group_member_created_by_app_user") + ), + sa.ForeignKeyConstraint( + ["legal_entity_id"], + ["dkb.legal_entity.id"], + name=op.f("fk_related_group_member_legal_entity_id_legal_entity"), + ), + sa.ForeignKeyConstraint( + ["related_group_id"], + ["dkb.related_group.id"], + name=op.f("fk_related_group_member_related_group_id_related_group"), + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_related_group_member")), + schema=SCHEMA, + ) + op.create_index( + "uq_related_group_member_entity_current", + "related_group_member", + ["legal_entity_id"], + unique=True, + schema=SCHEMA, + postgresql_where=sa.text("is_current"), + ) + op.create_index( + "uq_related_group_member_group_entity_current", + "related_group_member", + ["related_group_id", "legal_entity_id"], + unique=True, + schema=SCHEMA, + postgresql_where=sa.text("is_current"), + ) + op.create_table( + "stored_file", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("legal_entity_id", sa.UUID(), nullable=False), + sa.Column("file_type", ENUMS["file_type"], nullable=False), + sa.Column("original_name", sa.String(), nullable=True), + sa.Column("s3_object_id", sa.UUID(), nullable=False), + sa.Column("content_hash", sa.String(), nullable=True), + sa.Column("size_bytes", sa.BigInteger(), nullable=True), + sa.Column("status", ENUMS["file_status"], nullable=False), + sa.Column("version_no", sa.Integer(), server_default=sa.text("1"), nullable=False), + sa.Column("superseded_by_file_id", sa.UUID(), nullable=True), + sa.Column("current_recognition_job_id", sa.UUID(), nullable=True), + sa.Column("uploaded_by", sa.UUID(), nullable=True), + sa.Column("uploaded_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint( + ["legal_entity_id"], ["dkb.legal_entity.id"], name=op.f("fk_stored_file_legal_entity_id_legal_entity") + ), + sa.ForeignKeyConstraint( + ["s3_object_id"], ["dkb.s3_object.id"], name=op.f("fk_stored_file_s3_object_id_s3_object") + ), + sa.ForeignKeyConstraint( + ["superseded_by_file_id"], + ["dkb.stored_file.id"], + name=op.f("fk_stored_file_superseded_by_file_id_stored_file"), + ), + sa.ForeignKeyConstraint(["uploaded_by"], ["dkb.app_user.id"], name=op.f("fk_stored_file_uploaded_by_app_user")), + sa.PrimaryKeyConstraint("id", name=op.f("pk_stored_file")), + schema=SCHEMA, + ) + op.create_index( + "ix_stored_file_legal_entity_id_status", + "stored_file", + ["legal_entity_id", "status"], + unique=False, + schema=SCHEMA, + ) + op.create_table( + "consolidation_version", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("consolidation_id", sa.UUID(), nullable=False), + sa.Column("version_no", sa.Integer(), nullable=False), + sa.Column("status", ENUMS["consolidation_status"], nullable=False), + sa.Column("scenario", ENUMS["consolidation_scenario"], nullable=False), + sa.Column("approach", ENUMS["consolidation_approach"], server_default=sa.text("'BASIC'"), nullable=False), + sa.Column("goodwill_enabled", sa.Boolean(), server_default=sa.text("false"), nullable=False), + sa.Column("initiator_id", sa.UUID(), nullable=False), + sa.Column("initiator_office_id", sa.UUID(), nullable=False), + sa.Column("approved_in_rf_by", sa.UUID(), nullable=True), + sa.Column("approved_in_rf_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("verified_in_go_by", sa.UUID(), nullable=True), + sa.Column("verified_in_go_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("uob_decision_no", sa.String(), nullable=True), + sa.Column("uob_decision_date", sa.Date(), nullable=True), + sa.Column("methodology_deviations", sa.Text(), nullable=True), + sa.Column("elimination_rules_version", sa.Integer(), nullable=True), + sa.Column("is_in_work", sa.Boolean(), server_default=sa.text("true"), nullable=False), + sa.Column("previous_version_id", sa.UUID(), nullable=True), + sa.Column("cancelled_reason", sa.Text(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("created_by", sa.UUID(), nullable=True), + sa.Column("updated_by", sa.UUID(), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint( + ["approved_in_rf_by"], ["dkb.app_user.id"], name=op.f("fk_consolidation_version_approved_in_rf_by_app_user") + ), + sa.ForeignKeyConstraint( + ["consolidation_id"], + ["dkb.consolidation.id"], + name=op.f("fk_consolidation_version_consolidation_id_consolidation"), + ), + sa.ForeignKeyConstraint( + ["created_by"], ["dkb.app_user.id"], name=op.f("fk_consolidation_version_created_by_app_user") + ), + sa.ForeignKeyConstraint( + ["initiator_id"], ["dkb.app_user.id"], name=op.f("fk_consolidation_version_initiator_id_app_user") + ), + sa.ForeignKeyConstraint( + ["initiator_office_id"], + ["dkb.regional_office.id"], + name=op.f("fk_consolidation_version_initiator_office_id_regional_office"), + ), + sa.ForeignKeyConstraint( + ["previous_version_id"], + ["dkb.consolidation_version.id"], + name="fk_consolidation_version_previous_version_id", + ), + sa.ForeignKeyConstraint( + ["updated_by"], ["dkb.app_user.id"], name=op.f("fk_consolidation_version_updated_by_app_user") + ), + sa.ForeignKeyConstraint( + ["verified_in_go_by"], ["dkb.app_user.id"], name=op.f("fk_consolidation_version_verified_in_go_by_app_user") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_consolidation_version")), + schema=SCHEMA, + ) + op.create_index( + "uq_consolidation_version_consolidation_id_version_no", + "consolidation_version", + ["consolidation_id", "version_no"], + unique=True, + schema=SCHEMA, + ) + op.create_table( + "document", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("stored_file_id", sa.UUID(), nullable=False), + sa.Column("legal_entity_id", sa.UUID(), nullable=False), + sa.Column("document_type", ENUMS["document_type"], nullable=False), + sa.Column("osv_account_code", sa.String(), nullable=True), + sa.Column("period_year", sa.SmallInteger(), nullable=True), + sa.Column("period_quarter", ENUMS["period_quarter"], nullable=True), + sa.Column("is_cumulative", sa.Boolean(), nullable=True), + sa.Column("reporting_date", sa.Date(), nullable=True), + sa.Column("locator", sa.String(), nullable=True), + sa.Column("status", ENUMS["document_status"], nullable=False), + sa.Column("period_check", ENUMS["document_check_result"], nullable=True), + sa.Column("counterparty_check", ENUMS["document_check_result"], nullable=True), + sa.Column("confirmed_by", sa.UUID(), nullable=True), + sa.Column("confirmed_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(["confirmed_by"], ["dkb.app_user.id"], name=op.f("fk_document_confirmed_by_app_user")), + sa.ForeignKeyConstraint( + ["legal_entity_id"], ["dkb.legal_entity.id"], name=op.f("fk_document_legal_entity_id_legal_entity") + ), + sa.ForeignKeyConstraint( + ["stored_file_id"], ["dkb.stored_file.id"], name=op.f("fk_document_stored_file_id_stored_file") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_document")), + schema=SCHEMA, + ) + op.create_index( + "ix_document_legal_entity_period_status", + "document", + ["legal_entity_id", "document_type", "osv_account_code", "period_year", "period_quarter", "status"], + unique=False, + schema=SCHEMA, + ) + op.create_index( + "uq_document_file_type_account", + "document", + ["stored_file_id", "document_type", "osv_account_code"], + unique=True, + schema=SCHEMA, + postgresql_nulls_not_distinct=True, + ) + op.create_table( + "ex_snapshot_member", + sa.Column("ex_snapshot_id", sa.UUID(), nullable=False), + sa.Column("legal_entity_id", sa.UUID(), nullable=False), + sa.Column("inn", sa.String(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["ex_snapshot_id"], ["dkb.ex_snapshot.id"], name=op.f("fk_ex_snapshot_member_ex_snapshot_id_ex_snapshot") + ), + sa.ForeignKeyConstraint( + ["legal_entity_id"], + ["dkb.legal_entity.id"], + name=op.f("fk_ex_snapshot_member_legal_entity_id_legal_entity"), + ), + sa.PrimaryKeyConstraint("ex_snapshot_id", "legal_entity_id", name=op.f("pk_ex_snapshot_member")), + schema=SCHEMA, + ) + op.create_table( + "recognition_job", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("file_id", sa.UUID(), nullable=False), + sa.Column("pipeline", ENUMS["recognition_pipeline"], nullable=False), + sa.Column("request_subject", sa.String(), nullable=False), + sa.Column("request_msg_id", sa.String(), nullable=True), + sa.Column("result_msg_id", sa.String(), nullable=True), + sa.Column("schema_version", sa.String(), nullable=False), + sa.Column("service_name", sa.String(), nullable=True), + sa.Column("service_version", sa.String(), nullable=True), + sa.Column("status", ENUMS["recognition_status"], nullable=False), + sa.Column("attempt", sa.SmallInteger(), server_default=sa.text("1"), nullable=False), + sa.Column("max_attempts", sa.SmallInteger(), server_default=sa.text("1"), nullable=False), + sa.Column("requested_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("published_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("started_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("finished_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("reply_deadline", sa.DateTime(timezone=True), nullable=True), + sa.Column("external_task_id", sa.String(), nullable=True), + sa.Column("error_code", sa.String(), nullable=True), + sa.Column("error_message", sa.Text(), nullable=True), + sa.Column("result_s3_object_id", sa.UUID(), nullable=True), + sa.Column("result_schema_version", sa.String(), nullable=True), + sa.Column("group_schema", postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column("metrics", postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column("fields_total", sa.Integer(), nullable=True), + sa.Column("fields_low_confidence", sa.Integer(), nullable=True), + sa.Column("fields_unrecognized", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["file_id"], ["dkb.stored_file.id"], name=op.f("fk_recognition_job_file_id_stored_file") + ), + sa.ForeignKeyConstraint( + ["result_s3_object_id"], ["dkb.s3_object.id"], name=op.f("fk_recognition_job_result_s3_object_id_s3_object") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_recognition_job")), + sa.UniqueConstraint("request_msg_id", name=op.f("uq_recognition_job_request_msg_id")), + sa.UniqueConstraint("result_msg_id", name=op.f("uq_recognition_job_result_msg_id")), + schema=SCHEMA, + ) + op.create_index( + "uq_recognition_job_file_id_attempt", "recognition_job", ["file_id", "attempt"], unique=True, schema=SCHEMA + ) + # цикл stored_file <-> recognition_job: FK на nullable-стороне добавляется после создания обеих таблиц + op.create_foreign_key( + FK_CYCLE, + "stored_file", + "recognition_job", + ["current_recognition_job_id"], + ["id"], + source_schema=SCHEMA, + referent_schema=SCHEMA, + ) + op.create_table( + "audit_event", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("occurred_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("user_id", sa.UUID(), nullable=True), + sa.Column("action", ENUMS["audit_action"], nullable=False), + sa.Column("section", ENUMS["audit_section"], nullable=False), + sa.Column("object_type", sa.String(), nullable=False), + sa.Column("object_id", sa.UUID(), nullable=True), + sa.Column("consolidation_version_id", sa.UUID(), nullable=True), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("before_value", postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column("after_value", postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column("comment", sa.Text(), nullable=True), + sa.Column("result", sa.String(), nullable=True), + sa.Column("changed_field", sa.String(), nullable=True), + sa.Column("ip_address", sa.String(), nullable=True), + sa.Column("user_agent", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["consolidation_version_id"], + ["dkb.consolidation_version.id"], + name=op.f("fk_audit_event_consolidation_version_id_consolidation_version"), + ), + sa.ForeignKeyConstraint(["user_id"], ["dkb.app_user.id"], name=op.f("fk_audit_event_user_id_app_user")), + sa.PrimaryKeyConstraint("id", name=op.f("pk_audit_event")), + schema=SCHEMA, + ) + op.create_index( + "ix_audit_event_object_type_object_id", "audit_event", ["object_type", "object_id"], unique=False, schema=SCHEMA + ) + op.create_index( + "ix_audit_event_user_occurred_at", "audit_event", ["user_id", "occurred_at"], unique=False, schema=SCHEMA + ) + op.create_index( + "ix_audit_event_version_occurred_at", + "audit_event", + ["consolidation_version_id", "occurred_at"], + unique=False, + schema=SCHEMA, + ) + op.create_table( + "client_kfo_line", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("consolidation_version_id", sa.UUID(), nullable=False), + sa.Column("statement_type", ENUMS["statement_type"], nullable=False), + sa.Column("line_code", sa.String(), nullable=False), + sa.Column("client_amount", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("client_vgo_amount", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("bank_amount", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("bank_vgo_amount", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("deviation_amount", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("deviation_percent", sa.Numeric(precision=9, scale=4), nullable=True), + sa.Column("deviation_reason", sa.Text(), nullable=True), + sa.Column("comment", sa.Text(), nullable=True), + sa.Column("entered_by", sa.UUID(), nullable=True), + sa.Column("entered_at", sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint( + ["consolidation_version_id"], + ["dkb.consolidation_version.id"], + name="fk_client_kfo_line_consolidation_version_id", + ), + sa.ForeignKeyConstraint( + ["entered_by"], ["dkb.app_user.id"], name=op.f("fk_client_kfo_line_entered_by_app_user") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_client_kfo_line")), + schema=SCHEMA, + ) + op.create_index( + "uq_client_kfo_line_version_type_code", + "client_kfo_line", + ["consolidation_version_id", "statement_type", "line_code"], + unique=True, + schema=SCHEMA, + ) + op.create_table( + "consolidation_document", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("consolidation_version_id", sa.UUID(), nullable=False), + sa.Column("document_id", sa.UUID(), nullable=False), + sa.ForeignKeyConstraint( + ["consolidation_version_id"], + ["dkb.consolidation_version.id"], + name="fk_consolidation_document_consolidation_version_id", + ), + sa.ForeignKeyConstraint( + ["document_id"], ["dkb.document.id"], name=op.f("fk_consolidation_document_document_id_document") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_consolidation_document")), + schema=SCHEMA, + ) + op.create_index( + "uq_consolidation_document_version_document", + "consolidation_document", + ["consolidation_version_id", "document_id"], + unique=True, + schema=SCHEMA, + ) + op.create_table( + "consolidation_lock", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("consolidation_version_id", sa.UUID(), nullable=False), + sa.Column("locked_by", sa.UUID(), nullable=False), + sa.Column("locked_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("released_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("taken_over_from", sa.UUID(), nullable=True), + sa.Column("takeover_reason", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["consolidation_version_id"], + ["dkb.consolidation_version.id"], + name="fk_consolidation_lock_consolidation_version_id", + ), + sa.ForeignKeyConstraint( + ["locked_by"], ["dkb.app_user.id"], name=op.f("fk_consolidation_lock_locked_by_app_user") + ), + sa.ForeignKeyConstraint( + ["taken_over_from"], ["dkb.app_user.id"], name=op.f("fk_consolidation_lock_taken_over_from_app_user") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_consolidation_lock")), + schema=SCHEMA, + ) + op.create_index( + "uq_consolidation_lock_version_active", + "consolidation_lock", + ["consolidation_version_id"], + unique=True, + schema=SCHEMA, + postgresql_where=sa.text("released_at IS NULL"), + ) + op.create_table( + "consolidation_member", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("consolidation_version_id", sa.UUID(), nullable=False), + sa.Column("legal_entity_id", sa.UUID(), nullable=False), + sa.Column("is_head_entity", sa.Boolean(), server_default=sa.text("false"), nullable=False), + sa.ForeignKeyConstraint( + ["consolidation_version_id"], + ["dkb.consolidation_version.id"], + name="fk_consolidation_member_consolidation_version_id", + ), + sa.ForeignKeyConstraint( + ["legal_entity_id"], + ["dkb.legal_entity.id"], + name=op.f("fk_consolidation_member_legal_entity_id_legal_entity"), + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_consolidation_member")), + schema=SCHEMA, + ) + op.create_index( + "uq_consolidation_member_version_entity", + "consolidation_member", + ["consolidation_version_id", "legal_entity_id"], + unique=True, + schema=SCHEMA, + ) + op.create_table( + "consolidation_status_history", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("version_id", sa.UUID(), nullable=False), + sa.Column("from_status", ENUMS["consolidation_status"], nullable=True), + sa.Column("to_status", ENUMS["consolidation_status"], nullable=False), + sa.Column("changed_by", sa.UUID(), nullable=False), + sa.Column("changed_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("comment", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["changed_by"], ["dkb.app_user.id"], name=op.f("fk_consolidation_status_history_changed_by_app_user") + ), + sa.ForeignKeyConstraint( + ["version_id"], ["dkb.consolidation_version.id"], name="fk_consolidation_status_history_version_id" + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_consolidation_status_history")), + schema=SCHEMA, + ) + op.create_table( + "correction", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("consolidation_version_id", sa.UUID(), nullable=False), + sa.Column("statement_type", ENUMS["statement_type"], nullable=False), + sa.Column("rule_id", sa.UUID(), nullable=True), + sa.Column("parent_correction_id", sa.UUID(), nullable=True), + sa.Column("legal_entity_id", sa.UUID(), nullable=True), + sa.Column("counterparty_id", sa.UUID(), nullable=True), + sa.Column("debit_line_code", sa.String(), nullable=True), + sa.Column("debit_account_id", sa.UUID(), nullable=True), + sa.Column("debit_adjustment", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("credit_line_code", sa.String(), nullable=True), + sa.Column("credit_account_id", sa.UUID(), nullable=True), + sa.Column("credit_adjustment", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("deviation_amount", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("deviation_percent", sa.Numeric(precision=9, scale=4), nullable=True), + sa.Column("reason_code", sa.String(), nullable=True), + sa.Column("comment", sa.Text(), nullable=False), + sa.Column("status", ENUMS["correction_status"], server_default=sa.text("'DRAFT'"), nullable=False), + sa.Column("version_no", sa.Integer(), server_default=sa.text("1"), nullable=False), + sa.Column("created_by", sa.UUID(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("updated_by", sa.UUID(), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint( + ["consolidation_version_id"], + ["dkb.consolidation_version.id"], + name=op.f("fk_correction_consolidation_version_id_consolidation_version"), + ), + sa.ForeignKeyConstraint( + ["counterparty_id"], ["dkb.legal_entity.id"], name=op.f("fk_correction_counterparty_id_legal_entity") + ), + sa.ForeignKeyConstraint(["created_by"], ["dkb.app_user.id"], name=op.f("fk_correction_created_by_app_user")), + sa.ForeignKeyConstraint( + ["credit_account_id"], + ["dkb.chart_of_account.id"], + name=op.f("fk_correction_credit_account_id_chart_of_account"), + ), + sa.ForeignKeyConstraint( + ["debit_account_id"], + ["dkb.chart_of_account.id"], + name=op.f("fk_correction_debit_account_id_chart_of_account"), + ), + sa.ForeignKeyConstraint( + ["legal_entity_id"], ["dkb.legal_entity.id"], name=op.f("fk_correction_legal_entity_id_legal_entity") + ), + sa.ForeignKeyConstraint( + ["parent_correction_id"], ["dkb.correction.id"], name=op.f("fk_correction_parent_correction_id_correction") + ), + sa.ForeignKeyConstraint( + ["rule_id"], ["dkb.elimination_rule.id"], name=op.f("fk_correction_rule_id_elimination_rule") + ), + sa.ForeignKeyConstraint(["updated_by"], ["dkb.app_user.id"], name=op.f("fk_correction_updated_by_app_user")), + sa.PrimaryKeyConstraint("id", name=op.f("pk_correction")), + schema=SCHEMA, + ) + op.create_index( + "ix_correction_consolidation_version_id", + "correction", + ["consolidation_version_id"], + unique=False, + schema=SCHEMA, + ) + op.create_index( + "ix_correction_version_rule_entities", + "correction", + ["consolidation_version_id", "rule_id", "legal_entity_id", "counterparty_id"], + unique=False, + schema=SCHEMA, + ) + op.create_table( + "ex_snapshot_item", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("ex_snapshot_id", sa.UUID(), nullable=False), + sa.Column("item_type", sa.String(), nullable=False), + sa.Column("legal_entity_id", sa.UUID(), nullable=True), + sa.Column("document_id", sa.UUID(), nullable=True), + sa.Column("payload", postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column("payload_schema_version", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["document_id"], ["dkb.document.id"], name=op.f("fk_ex_snapshot_item_document_id_document") + ), + sa.ForeignKeyConstraint( + ["ex_snapshot_id"], ["dkb.ex_snapshot.id"], name=op.f("fk_ex_snapshot_item_ex_snapshot_id_ex_snapshot") + ), + sa.ForeignKeyConstraint( + ["legal_entity_id"], ["dkb.legal_entity.id"], name=op.f("fk_ex_snapshot_item_legal_entity_id_legal_entity") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_ex_snapshot_item")), + schema=SCHEMA, + ) + op.create_index( + "ix_ex_snapshot_item_snapshot_item_type", + "ex_snapshot_item", + ["ex_snapshot_id", "item_type"], + unique=False, + schema=SCHEMA, + ) + op.create_table( + "financial_statement_meta", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("document_id", sa.UUID(), nullable=False), + sa.Column("recognition_job_id", sa.UUID(), nullable=False), + sa.Column("statement_type", ENUMS["statement_type"], nullable=False), + sa.Column("inn", sa.String(), nullable=True), + sa.Column("inn_original_value", sa.String(), nullable=True), + sa.Column("inn_confidence", sa.Numeric(precision=5, scale=2), nullable=True), + sa.Column("inn_verified", sa.Boolean(), nullable=True), + sa.Column("inn_modified", sa.Boolean(), nullable=True), + sa.Column("organization_name", sa.String(), nullable=True), + sa.Column("report_date", sa.Date(), nullable=True), + sa.Column("report_date_original_value", sa.String(), nullable=True), + sa.Column("report_date_verified", sa.Boolean(), nullable=True), + sa.Column("report_date_modified", sa.Boolean(), nullable=True), + sa.Column("units", sa.String(), nullable=True), + sa.Column("units_original_value", sa.String(), nullable=True), + sa.Column("units_verified", sa.Boolean(), nullable=True), + sa.Column("units_modified", sa.Boolean(), nullable=True), + sa.Column("currency", sa.String(), server_default=sa.text("'RUB'"), nullable=True), + sa.Column("okpo", sa.String(), nullable=True), + sa.Column("okopf", sa.String(), nullable=True), + sa.Column("okfs", sa.String(), nullable=True), + sa.Column("okud", sa.String(), nullable=True), + sa.Column("okei", sa.String(), nullable=True), + sa.Column("legal_form", sa.String(), nullable=True), + sa.Column("ownership_form", sa.String(), nullable=True), + sa.Column("address", sa.String(), nullable=True), + sa.Column("subject_to_audit", sa.Boolean(), nullable=True), + sa.Column("reviewed_by", sa.UUID(), nullable=True), + sa.Column("reviewed_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("checks_accepted_by", sa.UUID(), nullable=True), + sa.Column("checks_accepted_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("checks_accepted_comment", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["checks_accepted_by"], + ["dkb.app_user.id"], + name=op.f("fk_financial_statement_meta_checks_accepted_by_app_user"), + ), + sa.ForeignKeyConstraint( + ["document_id"], ["dkb.document.id"], name=op.f("fk_financial_statement_meta_document_id_document") + ), + sa.ForeignKeyConstraint( + ["recognition_job_id"], + ["dkb.recognition_job.id"], + name=op.f("fk_financial_statement_meta_recognition_job_id_recognition_job"), + ), + sa.ForeignKeyConstraint( + ["reviewed_by"], ["dkb.app_user.id"], name=op.f("fk_financial_statement_meta_reviewed_by_app_user") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_financial_statement_meta")), + sa.UniqueConstraint("document_id", name=op.f("uq_financial_statement_meta_document_id")), + schema=SCHEMA, + ) + op.create_table( + "goodwill", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("consolidation_version_id", sa.UUID(), nullable=False), + sa.Column("head_legal_entity_id", sa.UUID(), nullable=False), + sa.Column("subsidiary_legal_entity_id", sa.UUID(), nullable=False), + sa.Column("ownership_share", sa.Numeric(precision=7, scale=4), nullable=True), + sa.Column("financial_investment_amount", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("charter_capital_amount", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("additional_capital_amount", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("participation_amount", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("goodwill_amount", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("include_in_kfo", sa.Boolean(), server_default=sa.text("false"), nullable=False), + sa.Column("comment", sa.Text(), nullable=True), + sa.Column("created_by", sa.UUID(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("updated_by", sa.UUID(), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint( + ["consolidation_version_id"], + ["dkb.consolidation_version.id"], + name=op.f("fk_goodwill_consolidation_version_id_consolidation_version"), + ), + sa.ForeignKeyConstraint(["created_by"], ["dkb.app_user.id"], name=op.f("fk_goodwill_created_by_app_user")), + sa.ForeignKeyConstraint( + ["head_legal_entity_id"], + ["dkb.legal_entity.id"], + name=op.f("fk_goodwill_head_legal_entity_id_legal_entity"), + ), + sa.ForeignKeyConstraint( + ["subsidiary_legal_entity_id"], + ["dkb.legal_entity.id"], + name=op.f("fk_goodwill_subsidiary_legal_entity_id_legal_entity"), + ), + sa.ForeignKeyConstraint(["updated_by"], ["dkb.app_user.id"], name=op.f("fk_goodwill_updated_by_app_user")), + sa.PrimaryKeyConstraint("id", name=op.f("pk_goodwill")), + schema=SCHEMA, + ) + op.create_index( + "uq_goodwill_version_head_subsidiary", + "goodwill", + ["consolidation_version_id", "head_legal_entity_id", "subsidiary_legal_entity_id"], + unique=True, + schema=SCHEMA, + ) + op.create_table( + "kfo_result", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("consolidation_version_id", sa.UUID(), nullable=False), + sa.Column("computed_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("is_frozen", sa.Boolean(), server_default=sa.text("false"), nullable=False), + sa.Column("net_assets", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("balance_total", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("revenue", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("sales_profit", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("total_deviation_bb", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("total_deviation_ofr", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("threshold_bb_exceeded", sa.Boolean(), nullable=True), + sa.Column("threshold_ofr_exceeded", sa.Boolean(), nullable=True), + sa.Column("threshold_justification", sa.Text(), nullable=True), + sa.Column("goodwill_total", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("has_deviations", sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint( + ["consolidation_version_id"], + ["dkb.consolidation_version.id"], + name=op.f("fk_kfo_result_consolidation_version_id_consolidation_version"), + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_kfo_result")), + sa.UniqueConstraint("consolidation_version_id", name=op.f("uq_kfo_result_consolidation_version_id")), + schema=SCHEMA, + ) + op.create_table( + "osv_entry", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("recognition_job_id", sa.UUID(), nullable=False), + sa.Column("document_id", sa.UUID(), nullable=False), + sa.Column("owner_legal_entity_id", sa.UUID(), nullable=False), + sa.Column("parent_entry_id", sa.UUID(), nullable=True), + sa.Column("group_type", ENUMS["osv_group_type"], nullable=True), + sa.Column("group_name", sa.String(), nullable=True), + sa.Column("row_number", sa.Integer(), nullable=True), + sa.Column("level", sa.SmallInteger(), nullable=True), + sa.Column("row_value", sa.String(), nullable=True), + sa.Column("data_type", ENUMS["osv_data_type"], nullable=True), + sa.Column("account_code", sa.String(), nullable=True), + sa.Column("account_id", sa.UUID(), nullable=True), + sa.Column("account_root", sa.String(), nullable=True), + sa.Column("counterparty_name", sa.String(), nullable=True), + sa.Column("counterparty_inn", sa.String(), nullable=True), + sa.Column("counterparty_id", sa.UUID(), nullable=True), + sa.Column("opening_debit", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("opening_credit", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("turnover_debit", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("turnover_credit", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("closing_debit", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("closing_credit", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("is_modified", sa.Boolean(), server_default=sa.text("false"), nullable=False), + sa.Column("original_values", postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column("modified_by", sa.UUID(), nullable=True), + sa.Column("modified_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("is_excisable", sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint( + ["account_id"], ["dkb.chart_of_account.id"], name=op.f("fk_osv_entry_account_id_chart_of_account") + ), + sa.ForeignKeyConstraint( + ["counterparty_id"], ["dkb.legal_entity.id"], name=op.f("fk_osv_entry_counterparty_id_legal_entity") + ), + sa.ForeignKeyConstraint(["document_id"], ["dkb.document.id"], name=op.f("fk_osv_entry_document_id_document")), + sa.ForeignKeyConstraint(["modified_by"], ["dkb.app_user.id"], name=op.f("fk_osv_entry_modified_by_app_user")), + sa.ForeignKeyConstraint( + ["owner_legal_entity_id"], + ["dkb.legal_entity.id"], + name=op.f("fk_osv_entry_owner_legal_entity_id_legal_entity"), + ), + sa.ForeignKeyConstraint( + ["parent_entry_id"], ["dkb.osv_entry.id"], name=op.f("fk_osv_entry_parent_entry_id_osv_entry") + ), + sa.ForeignKeyConstraint( + ["recognition_job_id"], + ["dkb.recognition_job.id"], + name=op.f("fk_osv_entry_recognition_job_id_recognition_job"), + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_osv_entry")), + schema=SCHEMA, + ) + op.create_index( + "ix_osv_entry_owner_account_counterparty", + "osv_entry", + ["owner_legal_entity_id", "account_id", "counterparty_id"], + unique=False, + schema=SCHEMA, + ) + op.create_index( + "uq_osv_entry_document_id_row_number", "osv_entry", ["document_id", "row_number"], unique=True, schema=SCHEMA + ) + op.create_table( + "financial_statement_column", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("meta_id", sa.UUID(), nullable=False), + sa.Column("column_index", sa.SmallInteger(), nullable=False), + sa.Column("label", sa.String(), nullable=True), + sa.Column("period_date", sa.Date(), nullable=True), + sa.Column("is_current", sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint( + ["meta_id"], + ["dkb.financial_statement_meta.id"], + name=op.f("fk_financial_statement_column_meta_id_financial_statement_meta"), + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_financial_statement_column")), + schema=SCHEMA, + ) + op.create_index( + "uq_financial_statement_column_meta_id_column_index", + "financial_statement_column", + ["meta_id", "column_index"], + unique=True, + schema=SCHEMA, + ) + op.create_table( + "financial_statement_line", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("meta_id", sa.UUID(), nullable=False), + sa.Column("line_code", sa.String(), nullable=True), + sa.Column("line_code_original_value", sa.String(), nullable=True), + sa.Column("line_code_confidence", sa.Numeric(precision=5, scale=2), nullable=True), + sa.Column("line_code_modified", sa.Boolean(), nullable=True), + sa.Column("line_name", sa.String(), nullable=True), + sa.Column("line_name_original_value", sa.String(), nullable=True), + sa.Column("line_name_confidence", sa.Numeric(precision=5, scale=2), nullable=True), + sa.Column("line_name_modified", sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint( + ["meta_id"], + ["dkb.financial_statement_meta.id"], + name=op.f("fk_financial_statement_line_meta_id_financial_statement_meta"), + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_financial_statement_line")), + schema=SCHEMA, + ) + op.create_index( + "uq_financial_statement_line_meta_id_line_code", + "financial_statement_line", + ["meta_id", "line_code"], + unique=True, + schema=SCHEMA, + ) + op.create_table( + "kfo_result_line", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("kfo_result_id", sa.UUID(), nullable=False), + sa.Column("statement_type", ENUMS["statement_type"], nullable=False), + sa.Column("line_code", sa.String(), nullable=False), + sa.Column("line_name", sa.String(), nullable=True), + sa.Column("sum_amount", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("eliminated_auto", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("eliminated_manual", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("goodwill_adjust", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("consolidated_amount", sa.Numeric(precision=18, scale=2), nullable=False), + sa.Column("vgo_amount", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("source_kind", sa.String(), nullable=True), + sa.Column("deviation_amount", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("comment", sa.Text(), nullable=True), + sa.Column("ordinal", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["kfo_result_id"], ["dkb.kfo_result.id"], name=op.f("fk_kfo_result_line_kfo_result_id_kfo_result") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_kfo_result_line")), + schema=SCHEMA, + ) + op.create_index( + "uq_kfo_result_line_result_type_code", + "kfo_result_line", + ["kfo_result_id", "statement_type", "line_code"], + unique=True, + schema=SCHEMA, + ) + op.create_table( + "financial_statement_value", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("line_id", sa.UUID(), nullable=False), + sa.Column("column_id", sa.UUID(), nullable=False), + sa.Column("amount", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("string_value", sa.String(), nullable=True), + sa.Column("confidence", sa.Numeric(precision=5, scale=2), nullable=True), + sa.Column("is_recognized", sa.Boolean(), nullable=True), + sa.Column("is_modified", sa.Boolean(), server_default=sa.text("false"), nullable=False), + sa.Column("original_amount", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("modified_by", sa.UUID(), nullable=True), + sa.Column("modified_at", sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint( + ["column_id"], + ["dkb.financial_statement_column.id"], + name="fk_financial_statement_value_column_id_fs_column", + ), + sa.ForeignKeyConstraint( + ["line_id"], + ["dkb.financial_statement_line.id"], + name=op.f("fk_financial_statement_value_line_id_financial_statement_line"), + ), + sa.ForeignKeyConstraint( + ["modified_by"], ["dkb.app_user.id"], name=op.f("fk_financial_statement_value_modified_by_app_user") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_financial_statement_value")), + schema=SCHEMA, + ) + op.create_index( + "uq_financial_statement_value_line_id_column_id", + "financial_statement_value", + ["line_id", "column_id"], + unique=True, + schema=SCHEMA, + ) + op.create_table( + "kfo_result_line_company", + sa.Column("kfo_result_line_id", sa.UUID(), nullable=False), + sa.Column("legal_entity_id", sa.UUID(), nullable=False), + sa.Column("amount", sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column("share_percent", sa.Numeric(precision=7, scale=4), nullable=True), + sa.ForeignKeyConstraint( + ["kfo_result_line_id"], + ["dkb.kfo_result_line.id"], + name=op.f("fk_kfo_result_line_company_kfo_result_line_id_kfo_result_line"), + ), + sa.ForeignKeyConstraint( + ["legal_entity_id"], + ["dkb.legal_entity.id"], + name=op.f("fk_kfo_result_line_company_legal_entity_id_legal_entity"), + ), + sa.PrimaryKeyConstraint("kfo_result_line_id", "legal_entity_id", name=op.f("pk_kfo_result_line_company")), + schema=SCHEMA, + ) + + +def downgrade() -> None: + """Полный откат: FK цикла -> таблицы в обратном порядке (с индексами) -> enum-типы. Схема dkb остаётся.""" + op.drop_constraint(FK_CYCLE, "stored_file", type_="foreignkey", schema=SCHEMA) + for table in reversed(TABLES): + op.drop_table(table, schema=SCHEMA) + bind = op.get_bind() + for enum_type in reversed(ENUMS.values()): + enum_type.drop(bind, checkfirst=False) diff --git a/api/pyproject.toml b/api/pyproject.toml index d9ed6ed..f7ede09 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -7,6 +7,10 @@ target-version = "py311" [tool.ruff.lint] select = ["E", "F", "I", "UP", "B", "PL"] +# Ревизии alembic — линейный DDL: одна функция upgrade() на десятки create_table/create_index +[tool.ruff.lint.per-file-ignores] +"alembic/versions/*" = ["PLR0915"] + [tool.ruff.lint.isort] known-first-party = ["src", "scripts"] diff --git a/api/tests/test_migrations.py b/api/tests/test_migrations.py new file mode 100644 index 0000000..223304e --- /dev/null +++ b/api/tests/test_migrations.py @@ -0,0 +1,284 @@ +"""Миграция 0001 (DBML v5.13) на PostgreSQL 16: полный цикл, пустой autogenerate, схема = схеме ORM. + +Spec data-model, требование «Миграции применяются с нуля и откатываются» (сценарии «Полный цикл» и «Автогенерация +ничего не находит»), design D-2. Alembic запускается отдельным процессом (`python -m alembic -x dburl=...`), как из +CLI: `env.py` вызывает `asyncio.run(...)`, а внутри event loop pytest-asyncio это невозможно; заодно процесс не +перенастраивает логирование тестов через `fileConfig`. Каждая проверка идёт на своей временной БД (`scratch_db_url`). +""" + +import asyncio +import subprocess +import sys +import uuid +from pathlib import Path +from typing import Any + +import pytest +from alembic.autogenerate import compare_metadata +from alembic.runtime.migration import MigrationContext +from sqlalchemy import Connection, text +from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, AsyncSession, create_async_engine + +from src.models import Base +from src.models.enums import ENUMS +from tests.pg_helpers import create_scratch_db, drop_scratch_db +from tests.test_models_part_01_03 import _check_related_group_member_uniqueness +from tests.test_models_part_04_06 import _check_nulls_not_distinct, _make_file_with_job +from tests.test_models_part_07_09 import ( + ALL_TABLES, + EXPECTED_ENUM_COUNT, + EXPECTED_TABLE_COUNT, + _check_active_lock_is_unique, +) + +API_DIR = Path(__file__).resolve().parents[1] +HEAD = "0001_dbml_v5_13" +VERSION_TABLE = "alembic_version" +FK_CYCLE = "fk_stored_file_current_recognition_job_id_recognition_job" + + +def _run_alembic(url: str, *args: str) -> str: + """`alembic -x dburl= ` из каталога api/; вернуть stdout+stderr, упасть с выводом при ошибке.""" + result = subprocess.run( + [sys.executable, "-m", "alembic", "-x", f"dburl={url}", *args], + cwd=API_DIR, + capture_output=True, + text=True, + check=False, + ) + output = result.stdout + result.stderr + assert result.returncode == 0, f"alembic {' '.join(args)} завершился с кодом {result.returncode}:\n{output}" + return output + + +async def alembic(url: str, *args: str) -> str: + return await asyncio.to_thread(_run_alembic, url, *args) + + +async def _scalars(conn: AsyncConnection, sql: str) -> list[Any]: + return list((await conn.execute(text(sql))).scalars()) + + +async def tables_in_dkb(conn: AsyncConnection) -> set[str]: + return set( + await _scalars( + conn, + "SELECT table_name FROM information_schema.tables WHERE table_schema = 'dkb'", + ) + ) + + +async def enums_in_dkb(conn: AsyncConnection) -> set[str]: + return set( + await _scalars( + conn, + "SELECT t.typname FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace " + "WHERE n.nspname = 'dkb' AND t.typtype = 'e'", + ) + ) + + +async def schema_snapshot(conn: AsyncConnection) -> dict[str, Any]: + """Каталог схемы dkb без служебной таблицы alembic: колонки, ограничения, индексы, enum'ы (с порядком значений).""" + columns = ( + await conn.execute( + text( + "SELECT table_name, ordinal_position, column_name, data_type, udt_schema, udt_name, is_nullable, " + "column_default, numeric_precision, numeric_scale FROM information_schema.columns " + "WHERE table_schema = 'dkb' AND table_name <> :vt ORDER BY table_name, ordinal_position" + ), + {"vt": VERSION_TABLE}, + ) + ).all() + constraints = ( + await conn.execute( + text( + "SELECT c.conname, c.conrelid::regclass::text, pg_get_constraintdef(c.oid) FROM pg_constraint c " + "WHERE c.connamespace = 'dkb'::regnamespace AND c.conrelid::regclass::text <> 'dkb.alembic_version' " + "AND c.contype <> 'n'" + ) + ) + ).all() + indexes = ( + await conn.execute( + text("SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = 'dkb' AND tablename <> :vt"), + {"vt": VERSION_TABLE}, + ) + ).all() + enums = ( + await conn.execute( + text( + "SELECT t.typname, array_agg(e.enumlabel ORDER BY e.enumsortorder) FROM pg_type t " + "JOIN pg_namespace n ON n.oid = t.typnamespace JOIN pg_enum e ON e.enumtypid = t.oid " + "WHERE n.nspname = 'dkb' GROUP BY t.typname" + ) + ) + ).all() + return { + "columns": [tuple(row) for row in columns], + "constraints": {row[0]: (row[1], row[2]) for row in constraints}, + "indexes": {row[0]: row[1] for row in indexes}, + "enums": {row[0]: list(row[1]) for row in enums}, + } + + +async def _assert_full_model(conn: AsyncConnection) -> None: + tables = await tables_in_dkb(conn) + assert tables - {VERSION_TABLE} == ALL_TABLES + assert len(tables - {VERSION_TABLE}) == EXPECTED_TABLE_COUNT + enums = await enums_in_dkb(conn) + assert enums == set(ENUMS) + assert len(enums) == EXPECTED_ENUM_COUNT + assert await _scalars(conn, "SELECT version_num FROM dkb.alembic_version") == [HEAD] + + +async def _truncate_model_tables(engine: AsyncEngine) -> None: + async with engine.begin() as conn: + await conn.execute(text("TRUNCATE " + ", ".join(f"dkb.{table}" for table in sorted(ALL_TABLES)) + " CASCADE")) + + +@pytest.mark.integration +async def test_full_cycle_upgrade_downgrade_upgrade(scratch_db_url: str) -> None: + """Сценарий «Полный цикл»: upgrade head → downgrade base → upgrade head; после отката схема dkb пуста.""" + engine = create_async_engine(scratch_db_url) + try: + await alembic(scratch_db_url, "upgrade", "head") + async with engine.connect() as conn: + await _assert_full_model(conn) + + await alembic(scratch_db_url, "downgrade", "base") + async with engine.connect() as conn: + assert await tables_in_dkb(conn) == {VERSION_TABLE} # служебная таблица alembic остаётся, она пуста + assert await _scalars(conn, "SELECT count(*) FROM dkb.alembic_version") == [0] + assert await enums_in_dkb(conn) == set() + # в схеме не осталось других объектов (последовательностей, типов, индексов) кроме alembic_version + leftovers = await _scalars( + conn, + "SELECT relname FROM pg_class WHERE relnamespace = 'dkb'::regnamespace " + "AND relname NOT IN ('alembic_version', 'alembic_version_pkc')", + ) + assert leftovers == [] + types = await _scalars( + conn, + "SELECT typname FROM pg_type WHERE typnamespace = 'dkb'::regnamespace " + "AND typname NOT IN ('alembic_version', '_alembic_version')", + ) + assert types == [] + + await alembic(scratch_db_url, "upgrade", "head") + async with engine.connect() as conn: + await _assert_full_model(conn) + finally: + await engine.dispose() + + +@pytest.mark.integration +async def test_autogenerate_finds_nothing_after_upgrade(scratch_db_url: str) -> None: + """Сценарий «Автогенерация ничего не находит»: compare_metadata пуст, `alembic check` не видит операций.""" + await alembic(scratch_db_url, "upgrade", "head") + + def diff(sync_conn: Connection) -> list[Any]: + context = MigrationContext.configure( + sync_conn, + opts={ + # те же опции сравнения, что в alembic/env.py + "compare_type": True, + "compare_server_default": True, + "include_schemas": True, + "include_name": lambda name, type_, _parents: name == "dkb" if type_ == "schema" else True, + "version_table_schema": "dkb", + }, + ) + return compare_metadata(context, Base.metadata) + + engine = create_async_engine(scratch_db_url) + try: + async with engine.connect() as conn: + assert await conn.run_sync(diff) == [] + finally: + await engine.dispose() + + output = await alembic(scratch_db_url, "check") + assert "No new upgrade operations detected" in output + + +@pytest.mark.integration +async def test_migrated_schema_equals_orm_create_all(scratch_db_url: str) -> None: + """Каталог после миграции = каталог `create_all` ORM: колонки, default, ограничения, индексы, enum (имена и DDL).""" + await alembic(scratch_db_url, "upgrade", "head") + reference_name = f"armcore_scratch_{uuid.uuid4().hex[:12]}" + reference_url = await create_scratch_db(reference_name) + migrated = create_async_engine(scratch_db_url) + reference = create_async_engine(reference_url) + try: + async with reference.begin() as conn: + await conn.execute(text("CREATE SCHEMA IF NOT EXISTS dkb")) + await conn.run_sync(Base.metadata.create_all) + async with migrated.connect() as conn: + got = await schema_snapshot(conn) + async with reference.connect() as conn: + expected = await schema_snapshot(conn) + assert got["enums"] == expected["enums"] + assert got["columns"] == expected["columns"] + assert got["constraints"] == expected["constraints"] + assert got["indexes"] == expected["indexes"] + finally: + await migrated.dispose() + await reference.dispose() + await drop_scratch_db(reference_name) + + +@pytest.mark.integration +async def test_migrated_schema_behaviour(scratch_db_url: str) -> None: + """После миграции: FK-цикл, partial unique, NULLS NOT DISTINCT и server_default работают на реальной БД.""" + await alembic(scratch_db_url, "upgrade", "head") + engine = create_async_engine(scratch_db_url) + try: + async with engine.connect() as conn: + fk = ( + await conn.execute( + text( + "SELECT conrelid::regclass::text, pg_get_constraintdef(oid) FROM pg_constraint " + "WHERE conname = :name AND connamespace = 'dkb'::regnamespace" + ), + {"name": FK_CYCLE}, + ) + ).one() + assert fk[0] == "dkb.stored_file" + assert fk[1] == "FOREIGN KEY (current_recognition_job_id) REFERENCES dkb.recognition_job(id)" + rows = await conn.execute( + text( + "SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = 'dkb' AND indexname IN " + "('uq_document_file_type_account', 'uq_consolidation_lock_version_active', " + "'uq_related_group_member_entity_current', 'uq_related_group_member_group_entity_current')" + ) + ) + indexdefs = {row[0]: row[1] for row in rows} + assert "NULLS NOT DISTINCT" in indexdefs["uq_document_file_type_account"] + assert "WHERE (released_at IS NULL)" in indexdefs["uq_consolidation_lock_version_active"] + for name in ("uq_related_group_member_entity_current", "uq_related_group_member_group_entity_current"): + assert indexdefs[name].startswith("CREATE UNIQUE INDEX") + assert "WHERE is_current" in indexdefs[name] + + # поведенческие проверки из тестов ORM (create_all) — теперь на схеме из миграции; данные между ними + # очищаются: помощники создают одинаковые справочные записи (ГО, группа G1) + await _check_related_group_member_uniqueness(engine) + await _truncate_model_tables(engine) + async with AsyncSession(engine, expire_on_commit=False) as session: + chain = await _make_file_with_job(session) # замыкает FK-цикл, проверяет server_default + await session.commit() + await _check_nulls_not_distinct(session, chain) + await _truncate_model_tables(engine) + async with AsyncSession(engine, expire_on_commit=False) as session: + await _check_active_lock_is_unique(session) + finally: + await engine.dispose() + + +def test_offline_sql_creates_schema_before_version_table() -> None: + """Офлайн-режим (`upgrade head --sql`): CREATE SCHEMA dkb идёт до таблицы версий и до enum-типов.""" + output = _run_alembic("postgresql+asyncpg://user:pass@localhost/offline", "upgrade", "head", "--sql") + schema_at = output.index("CREATE SCHEMA IF NOT EXISTS dkb") + assert schema_at < output.index("CREATE TABLE dkb.alembic_version") + assert schema_at < output.index("CREATE TYPE dkb.entity_type AS ENUM ('ЮЛ', 'ИП')") + assert f"INSERT INTO dkb.alembic_version (version_num) VALUES ('{HEAD}')" in output