AURORA-1938: расчёт переноса в фонд экономии на листах АХР/КВ/Операц формы РФ #262
Open
aep
wants to merge 1 commits from
feat/255-aurora-1938-transfer-econ-calc into test
pull from: feat/255-aurora-1938-transfer-econ-calc
merge into: RSHB:test
RSHB:main
RSHB:feat/266-aurora-1897-ahr-limit-period-vat-units
RSHB:feat/265-aurora-1909-ahr-manual-row-capture
RSHB:feat/264-aurora-1895-formula-localstorage
RSHB:feat/254-aurora-1906-spod-column
RSHB:feat/250-aurora-1888-stages-role-filter
RSHB:feat/253-aurora-1905-form1-form4-export-layout
RSHB:feat/252-aurora-1902-form2-export-layout
RSHB:feat/251-aurora-1901-smeta-rf-column-order
RSHB:feat/249-aurora-1936-row-delete-confirm
RSHB:feat/248-aurora-1877-token-refresh
RSHB:feat/246-aurora-1944-wheel-scroll-step
RSHB:feat/236-aurora-1843-svod2-server-paging-perf
RSHB:feat/236-aurora-1843-svod2-server-paging-web
RSHB:feat/236-aurora-1843-svod2-server-paging
RSHB:feat/214-aurora-1841-dict-vsp-virtual
RSHB:feat/220-aurora-1842-svod2-column-virtual
RSHB:feat/216-aurora-1846-form4-sheet-sum
RSHB:feat/235-aurora-1850-upd-form-cells-subtree
RSHB:feat/233-aurora-1848-sheet-json-no-pydantic-b
RSHB:feat/234-aurora-1839-cell-memo
RSHB:feat/233-aurora-1848-sheet-json-no-pydantic
RSHB:feat/237-aurora-1845-ws-connection-cache
RSHB:feat/215-aurora-1844-upd-form-cells-full-rows
RSHB:feat/226-aurora-1837-cell-styles-css
RSHB:feat/207-aurora-1831-project-tn-race
RSHB:feat/208-aurora-1832-phase-dates-tz
RSHB:feat/209-aurora-1833-greenlet-dev
RSHB:feat/206-aurora-1830-users-me-local-email
RSHB:feat/212-aurora-1838-ws-immutable-update
RSHB:feat/211-aurora-1836-row-measure
RSHB:feat/205-aurora-1922-pg15-migration-0012
RSHB:feat/213-aurora-1840-sheet-modals
RSHB:feat/210-aurora-1835-svod1-formatter
RSHB:test
RSHB:svod2-filter-fix2
RSHB:indent-export
RSHB:export-hierarchy
RSHB:svod2-filter-fix
RSHB:fix-id-column
RSHB:export-year-fix
RSHB:form3-source
RSHB:columns-svod-2
RSHB:export-optimize-refresh
RSHB:last-column
RSHB:svod-fix
RSHB:fix-columns
RSHB:form3-expense
RSHB:sattelite-fix
RSHB:delete-sattelite-rows
RSHB:form3-pages
RSHB:form1-sub_sub-fix
RSHB:sort-fix
RSHB:fixes-form
RSHB:new-expenses
RSHB:new-rows
RSHB:fix-style
RSHB:phase-template-edit
RSHB:fixes
RSHB:add-btn-reset-filter
RSHB:vsp-choice-fix
RSHB:project-filters-fix
RSHB:width-column-header-name
RSHB:task-filter-navigation
RSHB:auth
RSHB:icons-program-project
RSHB:export-optimize
RSHB:org-unit-limits
RSHB:auth-v2
RSHB:tasks-filters
RSHB:insert-row-fix
RSHB:svod-roles
RSHB:new-role-fix
RSHB:phase-edit-fix
RSHB:new-role
RSHB:delete-from-cell
RSHB:svod-cahe-and-table-direction
RSHB:direction-stage-fix
RSHB:backslash-fix
RSHB:svod
RSHB:front-svod-form-1
RSHB:projects-filters
RSHB:remove-row-fix
RSHB:computed-fix
RSHB:modal-add-year-project
RSHB:name-fix
RSHB:audit-logs-projects
RSHB:meta-form-3
RSHB:project-back-fix
RSHB:fix-sticky-columns
RSHB:fixies-3-form
RSHB:tech-num-fix
RSHB:fix-logic-limitation
RSHB:projects-fix
RSHB:3-form-project
RSHB:project-add-year-handler
RSHB:projects-summary-handler
RSHB:AURORA-1247
RSHB:projects-handlers
RSHB:vsp-choose-form2-2
RSHB:directions-fix
RSHB:add-validation
RSHB:add-checkbox-export-vsp
RSHB:fix-update-column-config
RSHB:fix-editable-columns
RSHB:add-biome-front
RSHB:vsp-choose-form2
RSHB:export-form-4
RSHB:booking-fix
RSHB:fix-front-tables
RSHB:phase-directions
RSHB:AURORA-1225
RSHB:fix-mapping-form-2-oper
RSHB:AURORA-1076-fix
RSHB:fix-add-row-program-4-form
RSHB:form2-limit-migrations
RSHB:delete-projects
RSHB:AURORA-1076
RSHB:form-4-add-row
RSHB:fix-logs
RSHB:AURORA-1206
RSHB:AURORA-894
RSHB:add-line-log-hotfix
RSHB:form-export-fix
RSHB:form-4-sql
RSHB:table-optimization-fix
RSHB:header_forms
RSHB:form-4-expense-item-filters
RSHB:entity-type-hotfix
RSHB:form-3-phases-timezones
RSHB:form-4-handlers
RSHB:export-fix
RSHB:close-date-vsp-fix
RSHB:deleted-user-auth-fix
RSHB:smeta-fix
RSHB:front-fixes
RSHB:line_id_code_full
RSHB:line_id_code
RSHB:timezones
RSHB:filenames-fix
RSHB:AURORA-1174-73
RSHB:project-sheets
RSHB:AURORA-1166
RSHB:websocket-projects
RSHB:project-front
RSHB:form3-permissions
RSHB:location_from_to_placement_type
RSHB:fix-vsp-filters
RSHB:fix-tests-and-functions
RSHB:mig2-fix
RSHB:vsp_fixes
RSHB:fix-ssp-title
RSHB:AURORA-1148
RSHB:fix-vsp-form-edit
RSHB:sql-funcs2-fix
RSHB:AURORA-1132
RSHB:logs
RSHB:AURORA-1147
RSHB:export-optimization
RSHB:AURORA-1131
RSHB:sql-funcs-fix
RSHB:vsp-dropdown-form
RSHB:form_2-back
RSHB:migrations
RSHB:websocket-self-flag
RSHB:reload-fix
RSHB:test-stand-fix
RSHB:excel-empty-fix
RSHB:requirements-fix
RSHB:AURORA-1096-back
RSHB:AURORA-1098
RSHB:AURORA-1096
RSHB:ws-user-id-fix
RSHB:expense-item-handler
RSHB:fix-date-phase
RSHB:excel-fix
RSHB:vsp-edit-field-fix
RSHB:vsp-fix-back
RSHB:vsp-back
RSHB:create-form-back
RSHB:export-direction-fix
RSHB:change-role-fix
RSHB:org-unit-users-fix
RSHB:audit-fix
RSHB:org-archive
RSHB:del_row_fix
RSHB:websocket_locks
RSHB:new_line_id
RSHB:admin-back
RSHB:form-fix
RSHB:ws
RSHB:ssp-fix
RSHB:stages-back
RSHB:form_124_write
RSHB:form_124
RSHB:new-model
RSHB:test-tsygankov
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
No description provided.
Delete Branch "feat/255-aurora-1938-transfer-econ-calc"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #255.
Проблема
Форма РФ (FORM_4 «Смета РФ поддержка»), листы АХР/КВ (CAP)/Операц (OPER): поле
«перенос в фонд экономии» (столбцы 75/107/137 Excel-эталона, внутренние имена
q1.transfer_econ/q2.transfer_econ/q3.transfer_econ, аналогичноq4) былоraw passthrough-полем
budget_line_quarter.transfer_to_economy— ничего его несчитало. Репро: добавить строку, заполнить план I–III кв. = 1000/2000/3000,
переносы/факт/секвестр пустыми, сохранить — поля переноса в фонд экономии
остаются пустыми, «ВСЕГО» блоков показывает прочерк вместо суммы, хотя остатки
после факта (1000/2000/3000) посчитаны верно.
Что сделано
api/alembic/versions/sql/0050_form4_transfer_econ_calc.sql:v3.v_form4_sheet_sections(единственная функция, которая отдаёт данныевсех трёх листов АХР/КВ/Операц —
v3.v_form4_viewдиспетчеризуетp_sheet IN ('AHR','CAP','OPER')в неё черезv_form4_sheet_jsonb→v_form4_sheet_lines_jsonb, так что один фикс в общей функции закрываетвсе три листа; подтверждено интеграционным тестом на CAP/OPER). STRUCTURE —
отдельный read-only агрегирующий свод по AHR+CAP+OPER без собственных
INPUT-строк, этой правки не касается.
col_qN_transfer_econ = f.qN_te(и его копию вветке агрегации
tb_N.te) на вычисление из уже имеющихся в схемеcpN/aqN(residual_after_actual_QN), см. Rulings — формула неочевидна,поэтому отдельная секция ниже.
v3._apply_form_cell: для FORM_4 прямая запись вq*.transfer_econтеперь отклоняется с
computed_field: ...(как уже было дляresidual_after_actual), поле убрано из allowlist записи (былосмаплено на
budget_line_quarter.transfer_to_economyв общей таблицеcolumn-mapping, используемой также FORM_2 — проверка намеренно ограничена
form_type_code = 'FORM_4', FORM_2 это поле не трогает).api/tests/integration/test_form4_transfer_econ.py— 4 теста:transfer_q2/q3/q4— именно он отличает верную формулу от наивногобуквального Excel-порта (при нулевых переносах обе формулы дают одинаковый
результат и не могли бы поймать регрессию);
q1.transfer_econотклоняется (стало computed_field).— не только что она не падает, но что считает так же, как INPUT-строка.
Rulings (решения без обсуждения)
Формула — не буквальный перенос из Excel, а с поправкой на уже
заложенный перенос. Эталон (Шаблон_РФ_поддержка.xlsm, АХР_П!CB9) —
CB9=BR9-BT9-BY9-BZ9, т.е. «остаток после факта минус перенос во II/III/IVкв.». Но в нашей схеме
residual_after_actual_QNуже вычитает переносв СЛЕДУЮЩИЙ квартал один раз через промежуточные
cp1/cp2/cp3:cp1 = ap1 + корректировки_Q1 − transfer_q2, аналогичноcp2минусуетtransfer_q3,cp3минусуетtransfer_q4. Буквальный перенос формулыExcel поверх
residual_after_actual_QNзадвоил бы это вычитание.Реализованные формулы:
residual_after_actual_Q1 − transfer_q2 − transfer_q3 − transfer_q4residual_after_actual_Q1 − transfer_q3 − transfer_q4residual_after_actual_Q2 − transfer_q3 − transfer_q4residual_after_actual_Q2 − transfer_q4residual_after_actual_Q3 − transfer_q4residual_after_actual_Q3residual_after_actual_Q4Почему
transfer_q2из формулы Q1 убран: он уже вычтен один раз внутриcp1/residual_after_actual_Q1; вычесть его снова — задвоить вычитание изанизить экономию ровно на сумму
transfer_q2. Аналогичноtransfer_q3вQ2 и
transfer_q4в Q3. Проверочное тождество (и то, что проверяет тест):transfer_q2 + transfer_q3 + transfer_q4 + transfer_econ_Q1(= «ВСЕГО»блока Q1) равно нераспределённому остатку после факта Q1 (до вычета
любых переносов) — с наивной формулой это тождество не выполняется.
Q4 тоже сделан вычисляемым, хотя в тикете и Excel-эталоне колонки для
Q4 нет (у Q4 просто нет «следующего квартала», переносить некуда) — для
единообразия с Q1–Q3 и потому, что
q4.transfer_econзамаплен в той жезаписи column-mapping в
_apply_form_cell, что и Q1–Q3: оставлять егоредактируемым, когда Q1–Q3 стали вычисляемыми, было бы непоследовательно.
Формула:
transfer_econ_Q4 = residual_after_actual_Q4(вычитать нечего).Проверка
FORM_4-only в_apply_form_cellсделана поbudget_form.form_type_code, а не глобально. Полеq*.transfer_econзамаплено в общей для FORM_2/FORM_4 column-mapping таблице внутри
_apply_form_cell. При ревью кода обнаружил, чтоv3.v_form2_sheet_sections(своя, отдельная функция для FORM_2 «ПД») содержит точно такой же
raw-passthrough баг для
transfer_econ— но тикет и переданные инструкциикасаются только FORM_4 («Форма РФ»). Если бы я сделал запись в
transfer_econнеразрешённой глобально, это сломало бы FORM_2 ещёсильнее: поле там и так всегда пустое (баг не чинился), а стало бы ещё и
нередактируемым вручную — чистый регресс без всякой пользы. Поэтому правка
ограничена
form_type_code = 'FORM_4'; баг FORM_2 я НЕ чинил (не входит втикет) — оставляю как есть и выношу отдельной строкой ниже.
Фронтенд НЕ трогал. Инструкция просила найти, где
residual_after_actualисключён из допустимых для записи полей нафронтенде (
AHR.js/CAP.js/STRUCTURE.js), и сделать то же самое дляtransfer_econ. Прочиталweb/src/components/RealtimeTable/tableColumns.jsx(
isEditable()) и все 4 файлаconstants/FORM_4/*.js— такогоper-field флага (editable/computed) для
residual_after_actualНЕсуществует вообще: для роли ADMIN
isEditable()всегда возвращаетtrueнезависимо от поля,
editTypeне используется ни для одной числовойколонки, а цветовая схема (
color_type) — чисто декоративная группировкапо разделам формы, не связанная с правом записи (у
total, который тожевычисляемый, тот же
redColumn, что у заведомо editabletransfer_q2).Единственная защита вычисляемых полей — серверная (
computed_field:в_apply_form_cell), которую UI уже обрабатывает универсально черезсуществующий
onCellUpdateError/handleCellUpdateError. После этого PRtransfer_econведёт себя на фронтенде ТОЧНО так же, какresidual_after_actualуже вёл себя всегда (клик возможен, сохранение сервер отклоняет) — то есть
зеркалирование уже достигнуто без единой правки в
web/.Тестовые годы форм (
90101–90200) выбраны заведомо далёкими от реальныхлет, чтобы не пересекаться по
(org_unit_id, form_type, year)с чужимиданными в общей тестовой БД (интеграционные тесты в проекте не
изолированы, см. ARCHITECTURE.md «Открытые вопросы»); каждый тест всё
равно откатывается (
client-фикстура с rollback-сессией).Тесты
pytest tests/unit— 27 passed (все проходят; 2 несвязанных паденияtest_export_service_query_params.pyотносятся кtests/unit, см. ниже).pytest(полный набор, живой Postgres черезDATABASE_URL, миграцииalembic upgrade head0001→0050 применены без ошибок,--init_dbодин разна чистую БД) — 201 passed, 4 skipped, 6 failed. Все 6 падений
пред-существующие и не связаны с этой веткой — перепроверено на
полностью чистой БД одним проходом (чтобы исключить накопленное состояние
между запусками):
test_backend_calls_form3_add_project_and_add_line_functions,test_delete_project_smoke,test_delete_project_verify_gone,test_delete_seed_project_smoke(все про FORM_3/проекты, я не трогал этоткод),
test_export_full_form_ignores_non_applicable_params_for_other_sheetsи
test_export_form_payload_forwards_ignore_flag(unit-тесты экспорт-сервиса,падают с
TypeError: unexpected keyword argument 'ignore_non_applicable_query_params'— рассинхрон теста и текущей сигнатуры
ExportService, тоже не моя область).Проверено дважды на двух разных чистых контейнерах Postgres 15 — результат
идентичен с правкой и без (те же 6 падений на базовой ветке
testдоприменения миграции 0050).
test_form4_transfer_econ.py— все 4 проходят,включая регрессионный тест с ненулевыми переносами (ключевое доказательство
правильности формулы, не наивного Excel-порта) и проверку ветки агрегации.
alembic upgrade head— чисто применяется поверх 0049 (0001...0050, безошибок).
Окружение для живого Postgres
В предоставленном окружении не было
DATABASE_URL/доступа к ужесуществующему контейнеру БД проекта (креды недоступны, а подбор/просмотр
чужих кредов — запрещённое действие в песочнице). Поднял одноразовый
Postgres 15 в Docker со своими кредами специально для этой проверки,
прогнал
alembic upgrade headи полныйpytest --init_dbна нём, контейнерудалил после проверки.
pg_get_functiondefна живой БД подтвердил, чтоv3.v_form4_sheet_sectionsиv3._apply_form_cellдо этого PR действительноопределялись последними в
0033_form2_old_sort.sqlи0023_apply_cell_empty_string.sqlсоответственно (как и предполагалось взадаче) — переопределены корректно.
Возможная коллизия номера миграции
PR #257 (другая ветка того же батча,
feat/249-...) тоже использует номер0050(0050_budget_line_delete_owner_guard.sql) с тем жеdown_revision = "0049".Если обе ветки смержатся в
test, вторая смерженная ветка получитдублирующийся revision (два alembic-head'а на один
down_revision) —потребуется переномеровать один из файлов 0050→0051 и поправить
down_revisionпри разрешении конфликта мержа. Не трогал #257 (чужая ветка),просто фиксирую риск для мержащего.
Перенос в фонд экономии (q1/q2/q3/q4.transfer_econ, столбцы 75/107/137 и их Q4-аналог формы РФ) был raw passthrough-полем budget_line_quarter. transfer_to_economy: ничего его не считало, поэтому после сохранения строки с планом без переносов поле оставалось пустым, а "ВСЕГО" блоков показывало прочерк вместо суммы. v3.v_form4_sheet_sections теперь считает transfer_econ_QN из уже имеющегося residual_after_actual_QN с поправкой на то, что тот уже вычитает перенос в СЛЕДУЮЩИЙ квартал один раз (через cp1/cp2/cp3): transfer_econ_Q1 = residual_after_actual_Q1 - transfer_q3 - transfer_q4 transfer_econ_Q2 = residual_after_actual_Q2 - transfer_q4 transfer_econ_Q3 = residual_after_actual_Q3 transfer_econ_Q4 = residual_after_actual_Q4 Буквальный перенос формулы Excel (ещё и минус transfer_q2 для Q1 / transfer_q3 для Q2) задвоил бы вычитание. АХР, КВ (CAP) и Операц (OPER) используют одну и ту же функцию, поэтому один фикс закрывает все три листа; STRUCTURE — агрегирующий read-only свод без INPUT-строк, его не касается. v3._apply_form_cell для FORM_4 больше не принимает прямую запись в q*.transfer_econ (ValueError computed_field, как у residual_after_actual). Тесты (api/tests/integration/test_form4_transfer_econ.py): - репро тикета (переносы = 0); - регрессия на двойное вычитание с ненулевыми transfer_q2/q3/q4, отличающая верную формулу от наивного Excel-порта; - КВ и Операц дают те же числа, что АХР; - запись в transfer_econ отклоняется. Closes #255. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>Checkout
From your project repository, check out a new branch and test the changes.