From e75ed6d44f40dd3666f658661b64694ddbdf31b2 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 8 Nov 2026 09:36:25 +0400 Subject: [PATCH] =?UTF-8?q?fix(form4):=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D1=83=D0=BC=D0=B5=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=BC?= =?UTF-8?q?=D0=B8=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D1=8E=201938=20=D0=B2=2000?= =?UTF-8?q?55=20=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B5=D1=81=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=5Fapply=5Fform=5Fcell=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D1=85=200050=20(=D0=A1=D0=9F=D0=9E=D0=94,=20?= =?UTF-8?q?AURORA-1906)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Миграция 0050_form4_transfer_econ_calc -> 0055 (down 0054): номер 0050 занят на test. Тело v3._apply_form_cell = актуальное тело из 0050_form2_ahr_spod_column + только дельта AURORA-1938 (запрет ручной записи FORM_4 q*.transfer_econ); раньше копия опиралась на 0023 и откатывала СПОД. v_form4_sheet_sections после 0033 на test не менялась — тело без изменений. Добавлен тест на сохранение обеих правок в итоговом определении функции. Co-Authored-By: Claude Opus 5.5 --- ...lc.py => 0055_form4_transfer_econ_calc.py} | 6 ++-- ....sql => 0055_form4_transfer_econ_calc.sql} | 17 +++++----- .../integration/test_form4_transfer_econ.py | 31 +++++++++++++++++++ 3 files changed, 44 insertions(+), 10 deletions(-) rename api/alembic/versions/{0050_form4_transfer_econ_calc.py => 0055_form4_transfer_econ_calc.py} (58%) rename api/alembic/versions/sql/{0050_form4_transfer_econ_calc.sql => 0055_form4_transfer_econ_calc.sql} (99%) diff --git a/api/alembic/versions/0050_form4_transfer_econ_calc.py b/api/alembic/versions/0055_form4_transfer_econ_calc.py similarity index 58% rename from api/alembic/versions/0050_form4_transfer_econ_calc.py rename to api/alembic/versions/0055_form4_transfer_econ_calc.py index 6ce72dd..78d6c44 100644 --- a/api/alembic/versions/0050_form4_transfer_econ_calc.py +++ b/api/alembic/versions/0055_form4_transfer_econ_calc.py @@ -1,14 +1,14 @@ from sql_migration import run_sql_file_migration -revision = "0050" -down_revision = "0049" +revision = "0055" +down_revision = "0054" branch_labels = None depends_on = None def upgrade() -> None: - run_sql_file_migration("0050_form4_transfer_econ_calc.sql") + run_sql_file_migration("0055_form4_transfer_econ_calc.sql") def downgrade() -> None: diff --git a/api/alembic/versions/sql/0050_form4_transfer_econ_calc.sql b/api/alembic/versions/sql/0055_form4_transfer_econ_calc.sql similarity index 99% rename from api/alembic/versions/sql/0050_form4_transfer_econ_calc.sql rename to api/alembic/versions/sql/0055_form4_transfer_econ_calc.sql index 73e2b20..f4132f3 100644 --- a/api/alembic/versions/sql/0050_form4_transfer_econ_calc.sql +++ b/api/alembic/versions/sql/0055_form4_transfer_econ_calc.sql @@ -11,6 +11,9 @@ -- Буквальный Excel-перенос (ещё и минус transfer_q2 для Q1 / transfer_q3 для Q2) -- задваивал бы вычитание — residual_after_actual_QN уже его учитывает. -- +-- 0055: пересобрано поверх 0050_form2_ahr_spod_column (AURORA-1906): тело _apply_form_cell = актуальное +-- на test + только дельта AURORA-1938 (v_form4_sheet_sections на test после 0033 не менялась). +-- -- 2) v3._apply_form_cell: для FORM_4 q*.transfer_econ больше нельзя писать напрямую — -- поле стало вычисляемым, как residual_after_actual. @@ -900,7 +903,7 @@ BEGIN ELSE RAISE EXCEPTION 'unknown_column: %', p_column; END CASE; - + IF (v_target_type = 'NUMERIC' OR v_target_type = 'INTEGER') AND p_value = '""'::jsonb THEN p_value := '"0"'::jsonb; END IF; @@ -940,7 +943,7 @@ BEGIN IF (v_parts[1]='fact_q1' AND v_parts[2] NOT IN ('jan','feb','mar')) OR (v_parts[1]='fact_q2' AND v_parts[2] NOT IN ('apr','may','jun')) OR (v_parts[1]='fact_q3' AND v_parts[2] NOT IN ('jul','aug','sep')) OR - (v_parts[1]='fact_q4' AND v_parts[2] NOT IN ('oct','nov','dec')) THEN + (v_parts[1]='fact_q4' AND v_parts[2] NOT IN ('oct','nov','dec','spod')) THEN RAISE EXCEPTION 'unknown_column: % (месяц вне квартала)', p_column; END IF; v_target_col := 'actual_' || v_parts[2]; @@ -949,7 +952,7 @@ BEGIN RAISE EXCEPTION 'unknown_column: %', p_column; END IF; - + IF v_target_type = 'NUMERIC' AND p_value = '""'::jsonb THEN p_value := '"0"'::jsonb; END IF; @@ -1228,9 +1231,9 @@ BEGIN ELSIF v_key_kind = 'line_quarter' THEN IF v_q IS NULL THEN RAISE EXCEPTION 'internal: quarter not set for %', p_column; END IF; - + IF v_target_type = 'DATE' THEN - + v_sql := format( 'INSERT INTO v3.%1$I (line_id, quarter, %2$I) VALUES ($1, $2, to_date($3->>0, ''DD.MM.YYYY'')) ' || 'ON CONFLICT (line_id, quarter) DO UPDATE SET %2$I = EXCLUDED.%2$I', @@ -1245,12 +1248,12 @@ BEGIN ); END IF; - + EXECUTE v_sql USING p_line_id, v_q, p_value; ELSIF v_key_kind = 'line_actor' THEN IF v_actor IS NULL THEN RAISE EXCEPTION 'internal: actor not set for %', p_column; END IF; - + IF v_target_type = 'DATE' THEN v_sql := format( 'INSERT INTO v3.%1$I (line_id, actor, %2$I) VALUES ($1, $2, to_date($3->>0, ''DD.MM.YYYY'')) ' diff --git a/api/tests/integration/test_form4_transfer_econ.py b/api/tests/integration/test_form4_transfer_econ.py index a4a06de..3fcd487 100644 --- a/api/tests/integration/test_form4_transfer_econ.py +++ b/api/tests/integration/test_form4_transfer_econ.py @@ -231,3 +231,34 @@ def test_form4_ahr_transfer_econ_cell_is_read_only(client, admin_tokens, auth_he payload = response.json() assert "message" in payload assert "computed_field" in payload["message"] or "transfer_econ" in payload["message"] + + +async def test_apply_form_cell_keeps_aurora_1906_spod_and_aurora_1938_guard(): + """Регрессия на порядок миграций: 0055 (AURORA-1938) пересобирает + v3._apply_form_cell поверх 0050_form2_ahr_spod_column (AURORA-1906), а не + поверх старого тела 0023 — иначе CREATE OR REPLACE тихо откатил бы СПОД. + Проверяем итоговое определение функции в БД: в нём одновременно есть + разрешение fact_q4.spod (1906) и запрет ручной записи transfer_econ (1938).""" + from sqlalchemy import text + from sqlalchemy.ext.asyncio import create_async_engine + from sqlalchemy.pool import NullPool + + from src.core.config import settings + + engine = create_async_engine(settings.DATABASE_URL, poolclass=NullPool) + try: + async with engine.connect() as conn: + definition = ( + await conn.execute( + text( + "SELECT pg_get_functiondef(p.oid) FROM pg_proc p " + "JOIN pg_namespace n ON n.oid = p.pronamespace " + "WHERE n.nspname = 'v3' AND p.proname = '_apply_form_cell'" + ) + ) + ).scalar_one() + finally: + await engine.dispose() + + assert "'oct','nov','dec','spod'" in definition, "AURORA-1906 (СПОД) откачен" + assert "v_field = 'transfer_econ'" in definition, "AURORA-1938 (computed transfer_econ) потерян"