fix(form4): перенумеровать миграцию 1938 в 0055 и пересобрать _apply_form_cell поверх 0050 (СПОД, AURORA-1906)

Миграция 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 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-11-08 09:36:25 +04:00
parent 37c2b96d61
commit e75ed6d44f
3 changed files with 44 additions and 10 deletions

View File

@ -1,14 +1,14 @@
from sql_migration import run_sql_file_migration from sql_migration import run_sql_file_migration
revision = "0050" revision = "0055"
down_revision = "0049" down_revision = "0054"
branch_labels = None branch_labels = None
depends_on = None depends_on = None
def upgrade() -> 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: def downgrade() -> None:

View File

@ -11,6 +11,9 @@
-- Буквальный Excel-перенос (ещё и минус transfer_q2 для Q1 / transfer_q3 для Q2) -- Буквальный Excel-перенос (ещё и минус transfer_q2 для Q1 / transfer_q3 для Q2)
-- задваивал бы вычитание — residual_after_actual_QN уже его учитывает. -- задваивал бы вычитание — 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 больше нельзя писать напрямую — -- 2) v3._apply_form_cell: для FORM_4 q*.transfer_econ больше нельзя писать напрямую —
-- поле стало вычисляемым, как residual_after_actual. -- поле стало вычисляемым, как residual_after_actual.
@ -940,7 +943,7 @@ BEGIN
IF (v_parts[1]='fact_q1' AND v_parts[2] NOT IN ('jan','feb','mar')) OR 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_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_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; RAISE EXCEPTION 'unknown_column: % (месяц вне квартала)', p_column;
END IF; END IF;
v_target_col := 'actual_' || v_parts[2]; v_target_col := 'actual_' || v_parts[2];

View File

@ -231,3 +231,34 @@ def test_form4_ahr_transfer_econ_cell_is_read_only(client, admin_tokens, auth_he
payload = response.json() payload = response.json()
assert "message" in payload assert "message" in payload
assert "computed_field" in payload["message"] or "transfer_econ" in payload["message"] 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) потерян"