AURORA-1897: лимиты АХР ф.2 — период, НДС, единицы #269

Open
aep wants to merge 4 commits from feat/266-aurora-1897-ahr-limit-period-vat-units into test
Owner

Closes #266.

Стек веток (важно для ревью)

Эта ветка стоит не на test, а поверх цепочки незамерженных PR:
test → PR #263 (AURORA-1906, миграция 0050_form2_ahr_spod_column) →
PR #268 (AURORA-1909, миграция 0051_form2_ahr_satellite_new_line) → эта
ветка (миграция 0052_form2_ahr_limit_period_vat_units).

Диф этого PR временно включает коммиты #263 и #268 (они ещё не в test).
PR создан с --base test по процессу; после мерджа #263/#268 ревьюеру будет
проще смотреть только 4 файла последнего коммита (ab765f0).

Summary

AURORA-1897 (эпик AURORA-1876): лист «АХР лимиты» формы 2 (ПД) закрывает три
пункта разбора заказчика одной миграцией и формулой:

  1. Годовые нормативы ×4 — ДМС (id 24 «стандарт РФ», id 25 «стандарт 1
    РФ») и оргтехника (id 13 «принтеры», 14 «МФУ», 15 «копировальные
    аппараты») дублировали годовую сумму в каждом квартале вместо деления на
    4. Причина — у этих строк в справочнике не было отметки «годовой»
    (unit был NULL для ДМС, формула для принтеров/МФУ/копиров была
    захардкожена по названию). Добавлена колонка v3.limit_template.period
    (YEAR/QUARTER/MONTH), размеченная по факту и backfill'ом по unit LIKE '%/год'/'%/месяц'.
  2. Сумма без НДС — лист лимитов и перенос в АХР считали сумму от
    limit_with_vat, хотя смета формы 2 ведётся без НДС. Переключено на
    limit_without_vat. У SIM-карт (id 7, 8) этот столбец был NULL —
    заполнен по аналогии с id 9 (450 с НДС → 375 без, тот же лимит).
    У принтеров/МФУ/копиров (id 13-15) limit_with_vat был NULL,
    limit_without_vat = 0 — заполнены по вопросу 2 из тикета (0,57 × 10900
    — цена с НДС → годовой норматив 6213 руб./шт., без НДС 5177,50).
  3. Единицы: рубли → тыс. руб. в плане АХР — лист лимитов работает в
    рублях (справочник, подписи фронта), а план АХР и Смета формы 2 — в
    тыс. руб. Перенос в план АХР (sync_form2_ahr_satellite_line(s)) теперь
    делит на 1000 для лимитных строк. Аренда/коммуналка/охрана (тыс. руб. уже
    в источнике) не тронуты.

Одна миграция 0052_form2_ahr_limit_period_vat_units, т.к. все три правки
меняют одну формулу и три вызывающие SQL-функции:

  • v3._form2_ahr_limit_amount(p_period, p_limit, p_qty) — новая 3-аргументная
    сигнатура (была 4-аргументная (name, unit, limit_with_vat, qty) с
    regexp-проверкой по названию для принтеров/МФУ/копиров — убрана полностью).
    Старая сигнатура удалена DROP FUNCTION после того, как переписаны все три
    вызывающие функции.
  • v3.v_form2_view (ветка AHR_LIMIT), v3.sync_form2_ahr_satellite_lines,
    v3.sync_form2_ahr_satellite_line — пересозданы целиком (CREATE OR REPLACE с полным телом, без regexp-патчей, как раньше в 0037). Ветки
    AHR/CAP/OPER/AHR_RENT/AHR_UTILITY/AHR_SECURITY/OTCH9F/SMETA и переносы
    аренды/коммуналки/охраны скопированы без изменений — это зона
    AURORA-1887/AURORA-1909, не трогаю.
  • Лист лимитов теперь отдаёт amount_year (сумма 4 кварталов) и period в
    каждой строке, плюс последнюю строку «Итого» (row_type='ROOT',
    data.id=0, sort_order=999999, суммы по всем LEAF). Тип ROOT
    обязателен — фронт строит дерево по ROW_TYPE_LEVELS
    (web/src/components/RealtimeTable/utils/rowUtils.js), иначе строка
    неизвестного типа легла бы в предыдущую секцию.
  • v3._limit_template_path_set дописывает id=0 к path-set, если вход не
    пуст — иначе строка «Итого» не попадала бы в diff ответа PATCH .../cell
    при изменении любой лимитной ячейки.
  • В конце миграции — PERFORM v3.sync_form2_ahr_satellite_lines(id) по всем
    существующим v3.budget_form WHERE form_type_code = 'FORM_2'.

Модель api/src/db/models/limit_template.py дополнена полем period
(с тем же CHECK, что в БД).

Test plan

  • TDD: новый файл api/tests/integration/test_form2_ahr_limit.py,
    7 интеграционных тестов через API (GET .../sheet/AHR_LIMIT,
    PATCH .../sheet/AHR_LIMIT/cell, GET .../sheet/AHR), числа точно из
    тикета:
    • id 24 («стандарт РФ»), qty_q1=10 → amount_q1=69645,
      amount_year=69645; АХР plan.q1=69.645.
    • id 3 («Директор»), qty_q2=1 → amount_q2=2000; АХР plan.q2=2.0.
    • id 7 (SIM-карта, лимит без НДС был NULL), qty_q1=20 →
      amount_q1=7500 (не 0); АХР plan.q1=7.5.
    • id 13 («принтеры», без проверки по имени), qty_q1=5 →
      amount_q1=6471.875; АХР plan.q1=6.471875.
    • id 17 (квартальный норматив, не годовой), qty_q3=2 →
      amount_q3=7500; АХР plan.q3=7.5 — считается как раньше, поменялся
      только источник НДС.
    • строка «Итого» — последняя в листе, row_type=ROOT, data.id=0,
      попадает в diff PATCH-ответа, сумма равна сумме всех LEAF.
    • регрессия: PATCH AHR_RENT plan.q1=100 переносится в АХР как 100,
      без деления — ветки аренды/коммуналки/охраны не задеты.
  • Тестовая БД — одноразовый postgres:16 в Docker (локальный порт
    55441), не общий perf-pg (он сейчас на 0053, несовместим с цепочкой
    0050→0051→0052 этой ветки). alembic upgrade head с нуля, контейнер удалён
    после прогона.
  • Полный cd api && pytest (245 тестов всего с учётом новых):
    • С миграцией 0052: 231 passed, 3 failed, 4 skipped (238 тестов) +
      7 новых тестов лимитов АХР = 238 passed, 3 failed, 4 skipped итого
      (245 собрано).
    • Без миграции (вторая одноразовая БД на revision 0051, тот же
      контейнер, та же фикстура, без нового тестового файла): 224 passed, 3 failed, 4 skipped (231 тест).
    • Разница — ровно 7 новых тестов (все зелёные), 0 ��зменений в падениях.
    • 3 падения pre-existing, не связаны с этой веткой и подтверждены
      одинаковыми до и после миграции:
      • tests/integration/test_db_functions_integration.py::test_backend_calls_form3_add_project_and_add_line_functions
        (FORM_3, report_type LIMIT — это разные "LIMIT", не
        v3.limit_template/AHR_LIMIT).
      • tests/unit/test_export_service_query_params.py::test_export_full_form_ignores_non_applicable_params_for_other_sheets
      • tests/unit/test_export_service_query_params.py::test_export_form_payload_forwards_ignore_flag
        (оба — ExportService не принимает кварг ignore_non_applicable_query_params,
        который ожидает тест; код экспорта не трогал).

Чеклист для человека перед реальным деплоем

Диагностические запросы «на копии продовой БД» из тикета недоступны в этом
окружении (нет доступа к проду) — пожалуйста, прогоните перед прод-деплоем:

  • Посчитать, у каких существующих v3.form_limit строк (все формы 2)
    qty_qN не пустой для id 23/24/25 (ДМС) и 13/14/15 (принтеры/МФУ/копиры)
    — после миграции их квартальная сумма уменьшится в 4 раза (годовой план
    делится, а не дублируется) и будет учитывать period/новые
    limit_with_vat/limit_without_vat для id 13-15.
  • Посчитать дельту по строкам лимитов АХР между старым и новым планом
    АХР (суммы в тыс. руб. вместо руб.) для всех существующих форм 2 — эта
    миграция делает PERFORM v3.sync_form2_ahr_satellite_lines(id) по всем
    FORM_2, т.е. пересчитывает всё при деплое; имеет смысл сверить дельту
    со Сметой формы 2 и сводами 1/2 до переключения трафика.
  • Проверить id 7/8 (SIM-карты) и id 13-15 (принтеры/МФУ/копиры) — их
    limit_without_vat/limit_with_vat заполнены по умолчанию (375 и
    6213/5177.5 соответственно, см. «Открытые вопросы» ниже); если у
    заказчика другие значения — скорректировать справочник отдельным UPDATE
    до или после деплоя.

Открытые вопросы к заказчику (не блокируют; реализовано по умолчанию)

  1. Годовой план делится на 4: количество вводится в каждом квартале,
    сумма квартала = количество × годовой лимит / 4 (как уже было для
    «бизнес РФ», id 23). Модель ввода не меняется. (реализовано)
  2. Принтеры/МФУ/копиры: 0,57 × 10900 — цена с НДС. По умолчанию
    лимит без НДС = 5177,50 руб./шт./год (6213 / 1.2). (реализовано)
  3. Канцелярские принадлежности (id 21, 500 руб.) — норматив на 1
    человека в квартал, по аналогии с id 20 «Бумага». По умолчанию — да,
    unit = '1 чел/квартал'. (реализовано)

Rulings I made

  • R-1: _form2_ahr_limit_amount новая сигнатура — (period varchar, limit numeric, qty numeric), ровно как в тикете; старая 4-арг. версия
    удаляется DROP FUNCTION после переписывания трёх вызывающих функций
    (порядок важен — иначе осталась бы неиспользуемая старая версия).
  • R-2: тела v3.v_form2_view/sync_form2_ahr_satellite_lines/
    sync_form2_ahr_satellite_line взял из 0050_form2_ahr_spod_column.sql
    (а не из 0037, как в оригинальном тексте тикета) — это актуальная
    версия на голове цепочки 0050→0051 этой ветки; подтвердил grep'ом, что
    0051 эти три функции не трогает (правит только v3.add_budget_line).
  • R-3: строка «Итого» добавлена через UNION ALL с отдельной агрегирующей
    подзапросом (сумма всех LEAF по amount_qN/amount_year), а не как
    ещё одна запись в основном дереве tree/agg — проще и не меняет
    семантику рекурсивного CTE для настоящих секций/групп.
  • R-4: v3._limit_template_path_set при пустом/NULL входе продолжает
    возвращать NULL (поведение не менял) — 0 добавляется только когда
    вход не пуст, как явно написано в тикете.
  • R-5: тестовая БД — одноразовый postgres:16 в Docker на порту 55441 (не
    55432/perf-pg, который сейчас на несовместимой ревизии 0053), контейнер
    удалён после прогона тестов.
  • R-6: для sanity-проверки "без миграции" поднял вторую одноразовую БД в
    том же контейнере на revision 0051 (а не git revert/stash рабочего
    дерева) — чище развязывает "код без миграции" от "БД без миграции" и не
    трогает untracked/staged файлы в worktree.

🤖 Generated with Claude Code

Closes #266. ## Стек веток (важно для ревью) Эта ветка стоит не на `test`, а поверх цепочки незамерженных PR: `test` → PR #263 (AURORA-1906, миграция `0050_form2_ahr_spod_column`) → PR #268 (AURORA-1909, миграция `0051_form2_ahr_satellite_new_line`) → эта ветка (миграция `0052_form2_ahr_limit_period_vat_units`). Диф этого PR **временно включает коммиты #263 и #268** (они ещё не в `test`). PR создан с `--base test` по процессу; после мерджа #263/#268 ревьюеру будет проще смотреть только 4 файла последнего коммита (`ab765f0`). ## Summary AURORA-1897 (эпик AURORA-1876): лист «АХР лимиты» формы 2 (ПД) закрывает три пункта разбора заказчика одной миграцией и формулой: 1. **Годовые нормативы ×4** — ДМС (id 24 «стандарт РФ», id 25 «стандарт 1 РФ») и оргтехника (id 13 «принтеры», 14 «МФУ», 15 «копировальные аппараты») дублировали годовую сумму в каждом квартале вместо деления на 4. Причина — у этих строк в справочнике не было отметки «годовой» (`unit` был `NULL` для ДМС, формула для принтеров/МФУ/копиров была захардкожена по названию). Добавлена колонка `v3.limit_template.period` (`YEAR`/`QUARTER`/`MONTH`), размеченная по факту и backfill'ом по `unit LIKE '%/год'`/`'%/месяц'`. 2. **Сумма без НДС** — лист лимитов и перенос в АХР считали сумму от `limit_with_vat`, хотя смета формы 2 ведётся без НДС. Переключено на `limit_without_vat`. У SIM-карт (id 7, 8) этот столбец был `NULL` — заполнен по аналогии с id 9 (`450` с НДС → `375` без, тот же лимит). У принтеров/МФУ/копиров (id 13-15) `limit_with_vat` был `NULL`, `limit_without_vat` = 0 — заполнены по вопросу 2 из тикета (`0,57 × 10900` — цена с НДС → годовой норматив 6213 руб./шт., без НДС 5177,50). 3. **Единицы: рубли → тыс. руб. в плане АХР** — лист лимитов работает в рублях (справочник, подписи фронта), а план АХР и Смета формы 2 — в тыс. руб. Перенос в план АХР (`sync_form2_ahr_satellite_line(s)`) теперь делит на 1000 для лимитных строк. Аренда/коммуналка/охрана (тыс. руб. уже в источнике) не тронуты. Одна миграция `0052_form2_ahr_limit_period_vat_units`, т.к. все три правки меняют одну формулу и три вызывающие SQL-функции: - `v3._form2_ahr_limit_amount(p_period, p_limit, p_qty)` — новая 3-аргументная сигнатура (была 4-аргументная `(name, unit, limit_with_vat, qty)` с regexp-проверкой по названию для принтеров/МФУ/копиров — убрана полностью). Старая сигнатура удалена `DROP FUNCTION` после того, как переписаны все три вызывающие функции. - `v3.v_form2_view` (ветка `AHR_LIMIT`), `v3.sync_form2_ahr_satellite_lines`, `v3.sync_form2_ahr_satellite_line` — пересозданы целиком (`CREATE OR REPLACE` с полным телом, без regexp-патчей, как раньше в 0037). Ветки AHR/CAP/OPER/AHR_RENT/AHR_UTILITY/AHR_SECURITY/OTCH9F/SMETA и переносы аренды/коммуналки/охраны скопированы без изменений — это зона AURORA-1887/AURORA-1909, не трогаю. - Лист лимитов теперь отдаёт `amount_year` (сумма 4 кварталов) и `period` в каждой строке, плюс последнюю строку «Итого» (`row_type='ROOT'`, `data.id=0`, `sort_order=999999`, суммы по всем `LEAF`). Тип `ROOT` обязателен — фронт строит дерево по `ROW_TYPE_LEVELS` (`web/src/components/RealtimeTable/utils/rowUtils.js`), иначе строка неизвестного типа легла бы в предыдущую секцию. - `v3._limit_template_path_set` дописывает `id=0` к path-set, если вход не пуст — иначе строка «Итого» не попадала бы в diff ответа `PATCH .../cell` при изменении любой лимитной ячейки. - В конце миграции — `PERFORM v3.sync_form2_ahr_satellite_lines(id)` по всем существующим `v3.budget_form WHERE form_type_code = 'FORM_2'`. Модель `api/src/db/models/limit_template.py` дополнена полем `period` (с тем же `CHECK`, что в БД). ## Test plan - [x] TDD: новый файл `api/tests/integration/test_form2_ahr_limit.py`, 7 интеграционных тестов через API (`GET .../sheet/AHR_LIMIT`, `PATCH .../sheet/AHR_LIMIT/cell`, `GET .../sheet/AHR`), числа точно из тикета: - id 24 («стандарт РФ»), `qty_q1=10` → `amount_q1=69645`, `amount_year=69645`; АХР `plan.q1=69.645`. - id 3 («Директор»), `qty_q2=1` → `amount_q2=2000`; АХР `plan.q2=2.0`. - id 7 (SIM-карта, лимит без НДС был `NULL`), `qty_q1=20` → `amount_q1=7500` (не 0); АХР `plan.q1=7.5`. - id 13 («принтеры», без проверки по имени), `qty_q1=5` → `amount_q1=6471.875`; АХР `plan.q1=6.471875`. - id 17 (квартальный норматив, не годовой), `qty_q3=2` → `amount_q3=7500`; АХР `plan.q3=7.5` — считается как раньше, поменялся только источник НДС. - строка «Итого» — последняя в листе, `row_type=ROOT`, `data.id=0`, попадает в diff `PATCH`-ответа, сумма равна сумме всех `LEAF`. - регрессия: `PATCH AHR_RENT plan.q1=100` переносится в АХР как `100`, без деления — ветки аренды/коммуналки/охраны не задеты. - [x] Тестовая БД — одноразовый `postgres:16` в Docker (локальный порт 55441), не общий `perf-pg` (он сейчас на 0053, несовместим с цепочкой 0050→0051→0052 этой ветки). `alembic upgrade head` с нуля, контейнер удалён после прогона. - [x] Полный `cd api && pytest` (245 тестов всего с учётом новых): - **С миграцией 0052**: `231 passed, 3 failed, 4 skipped` (238 тестов) + 7 новых тестов лимитов АХР = **238 passed, 3 failed, 4 skipped** итого (245 собрано). - **Без миграции** (вторая одноразовая БД на revision `0051`, тот же контейнер, та же фикстура, без нового тестового файла): `224 passed, 3 failed, 4 skipped` (231 тест). - Разница — ровно 7 новых тестов (все зелёные), 0 ��зменений в падениях. - 3 падения **pre-existing**, не связаны с этой веткой и подтверждены одинаковыми до и после миграции: - `tests/integration/test_db_functions_integration.py::test_backend_calls_form3_add_project_and_add_line_functions` (FORM_3, report_type `LIMIT` — это разные "LIMIT", не `v3.limit_template`/`AHR_LIMIT`). - `tests/unit/test_export_service_query_params.py::test_export_full_form_ignores_non_applicable_params_for_other_sheets` - `tests/unit/test_export_service_query_params.py::test_export_form_payload_forwards_ignore_flag` (оба — `ExportService` не принимает кварг `ignore_non_applicable_query_params`, который ожидает тест; код экспорта не трогал). ## Чеклист для человека перед реальным деплоем Диагностические запросы «на копии продовой БД» из тикета недоступны в этом окружении (нет доступа к проду) — пожалуйста, прогоните перед прод-деплоем: - [ ] Посчитать, у каких существующих `v3.form_limit` строк (все формы 2) `qty_qN` не пустой для id 23/24/25 (ДМС) и 13/14/15 (принтеры/МФУ/копиры) — после миграции их квартальная сумма уменьшится в 4 раза (годовой план делится, а не дублируется) и будет учитывать `period`/новые `limit_with_vat`/`limit_without_vat` для id 13-15. - [ ] Посчитать дельту по строкам лимитов АХР между старым и новым планом АХР (суммы в тыс. руб. вместо руб.) для всех существующих форм 2 — эта миграция делает `PERFORM v3.sync_form2_ahr_satellite_lines(id)` по всем `FORM_2`, т.е. пересчитывает всё при деплое; имеет смысл сверить дельту со Сметой формы 2 и сводами 1/2 до переключения трафика. - [ ] Проверить id 7/8 (SIM-карты) и id 13-15 (принтеры/МФУ/копиры) — их `limit_without_vat`/`limit_with_vat` заполнены по умолчанию (375 и 6213/5177.5 соответственно, см. «Открытые вопросы» ниже); если у заказчика другие значения — скорректировать справочник отдельным UPDATE до или после деплоя. ## Открытые вопросы к заказчику (не блокируют; реализовано по умолчанию) 1. **Годовой план делится на 4**: количество вводится в каждом квартале, сумма квартала = количество × годовой лимит / 4 (как уже было для «бизнес РФ», id 23). Модель ввода не меняется. *(реализовано)* 2. **Принтеры/МФУ/копиры**: `0,57 × 10900` — цена **с НДС**. По умолчанию лимит без НДС = 5177,50 руб./шт./год (`6213 / 1.2`). *(реализовано)* 3. **Канцелярские принадлежности** (id 21, 500 руб.) — норматив на 1 человека в квартал, по аналогии с id 20 «Бумага». По умолчанию — да, `unit = '1 чел/квартал'`. *(реализовано)* ## Rulings I made - R-1: `_form2_ahr_limit_amount` новая сигнатура — `(period varchar, limit numeric, qty numeric)`, ровно как в тикете; старая 4-арг. версия удаляется `DROP FUNCTION` после переписывания трёх вызывающих функций (порядок важен — иначе осталась бы неиспользуемая старая версия). - R-2: тела `v3.v_form2_view`/`sync_form2_ahr_satellite_lines`/ `sync_form2_ahr_satellite_line` взял из `0050_form2_ahr_spod_column.sql` (а не из `0037`, как в оригинальном тексте тикета) — это актуальная версия на голове цепочки 0050→0051 этой ветки; подтвердил grep'ом, что 0051 эти три функции не трогает (правит только `v3.add_budget_line`). - R-3: строка «Итого» добавлена через `UNION ALL` с отдельной агрегирующей подзапросом (сумма всех `LEAF` по `amount_qN`/`amount_year`), а не как ещё одна запись в основном дереве `tree`/`agg` — проще и не меняет семантику рекурсивного CTE для настоящих секций/групп. - R-4: `v3._limit_template_path_set` при пустом/`NULL` входе продолжает возвращать `NULL` (поведение не менял) — `0` добавляется только когда вход не пуст, как явно написано в тикете. - R-5: тестовая БД — одноразовый `postgres:16` в Docker на порту 55441 (не 55432/`perf-pg`, который сейчас на несовместимой ревизии 0053), контейнер удалён после прогона тестов. - R-6: для sanity-проверки "без миграции" поднял вторую одноразовую БД в том же контейнере на revision `0051` (а не `git revert`/stash рабочего дерева) — чище развязывает "код без миграции" от "БД без миграции" и не трогает untracked/staged файлы в worktree. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
aep added 4 commits 2026-10-31 14:36:54 +03:00
Добавлен столбец «СПОД» между «Декабрь» и «Итог IV кв.» на листах
AHR_RENT/AHR_UTILITY/AHR_SECURITY формы 2, с переносом значения в
budget_line_quarter.actual_spod синхронизируемой строки АХР (Support)
и, через уже существующую логику, в итог 4 кв. и года АХР/Смет/сводов.

- rent_detail/utility_detail/security_detail: колонка actual_spod
  (ALTER TABLE + ORM).
- v3.v_form2_view: fact_q4.spod и fact_q4.total с учётом spod в ветках
  AHR_RENT/AHR_UTILITY/AHR_SECURITY.
- v3._apply_form_cell: fact_q4.spod разрешён как обычное поле месяца.
- v3._upsert_form2_ahr_detail_sync_values: DROP+CREATE с новым
  параметром p_actual_spod (DROP обязателен из-за неоднозначности
  перегрузки с DEFAULT).
- v3.sync_form2_ahr_satellite_lines/line: actual_spod передаётся из
  rd/ud/sd в апсерт.
- v3.v_svod2_go: строка аренды также считается заполненной по
  actual_spod.
- Data-миграция: 'fact_q4.spod' добавлен в column_keys этапов, где уже
  есть 'fact_q4.dec' (phase_template/form_phase), идемпотентно.
- Тело CREATE OR REPLACE для v_form2_view/sync_form2_ahr_satellite_*
  собрано вручную по миграциям 0033/0034 + regexp-патчам из 0037
  (pg_get_functiondef на живой БД недоступен в этой среде — см.
  Rulings в PR).

Тесты: node --test на новых accessorKey в конфигах фронта + unit-тест
на 0049/0050 (v_svod2_go теперь засчитывает actual_spod для аренды).
Интеграционные тесты добавлены, но не выполнены — нет доступа к
Postgres в этой рабочей среде (см. Rulings в PR).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Прогнал api/tests/integration/test_form2_ahr_satellite_spod_integration.py
против живого Postgres (perf/perf/perf на localhost:55432, БД dfip_1906,
alembic upgrade head + фикстура) — нашёл и поправил два бага самого теста
(фичу/миграцию не трогал):

- _find_row не учитывал служебную META-строку (editable/role/user_id без
  id/header), которую v3.v_form_view добавляет первой в любой лист —
  KeyError: 'id' на первой же выборке. Теперь ищет только среди INPUT-строк.
- test_satellite_spod_phase_gates_non_admin_user делал один тест из
  "запрет без fact_q4.spod → добавили ключ → разрешено": неадминский
  403/400 от isp1 внутри теста вызывает db_ref.rollback() в
  tests/integration/conftest.py (per-request откат на ошибке), который
  стирает все несохранённые изменения этого же теста — включая только
  что созданные строку сателлита и form_phase. Разбил на два независимых
  теста (test_satellite_spod_rejected_before_phase_includes_spod_column /
  test_satellite_spod_allowed_once_phase_includes_spod_column), в каждом
  ровно один запрос, которому разрешено провалиться.

pytest tests/integration — 157 passed, 1 failed (pre-existing,
не связан: test_backend_calls_form3_add_project_and_add_line_functions,
FORM_3/add_project, 400 на /api/v1/projects/.../report/.../line), 4 skipped.
Новый файл — 15/15.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v3.add_budget_line (ветка сателлитов AHR_RENT/AHR_UTILITY/AHR_SECURITY)
искала любую существующую строку статьи без ORDER BY и без исключения
ручных/уже привязанных строк, и переиспользовала её как заготовку для
новой записи. Если у статьи ещё не было строк в
v3.form2_ahr_satellite_line_source (типично — первая запись при наличии
ручной строки той же статьи), sync переименовывал и перезаписывал план/факт
чужой ручной строки, блокируя её для правки.

Теперь для каждой новой записи сателлита всегда создаётся отдельная
пустая строка-заготовка — sync забирает её как строку именно этой записи.
Заодно удалён мёртвый код (_ensure_form2_ahr_sync_line,
_upsert_form2_ahr_sync_values), не имевший вызывающих мест.

AURORA-1907 (лимитная часть) и AURORA-1887 (статьи 314/24/779 в коде) —
отдельные задачи, не затронуты.

Closes #265.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AURORA-1897: лист «АХР лимиты» формы 2 дублировал годовой норматив ДМС/оргтехники
в каждом квартале вместо деления на 4, считал сумму от лимита с НДС (смета
ведётся без НДС) и переносил рубли в план АХР, где всё остальное в тыс. руб.

Миграция 0052_form2_ahr_limit_period_vat_units:
- v3.limit_template.period (YEAR/QUARTER/MONTH) + точечные правки справочника
  (id 24/25 ДМС и id 13-15 принтеры/МФУ/копиры — YEAR; id 7/8 SIM-карты —
  заполнен limit_without_vat; id 21 — unit по аналогии с бумагой).
- v3._form2_ahr_limit_amount(period, limit, qty) — рубли без НДС, без
  проверки по названию; старая 4-арг. сигнатура удалена.
- v3.v_form2_view (ветка AHR_LIMIT), v3.sync_form2_ahr_satellite_lines(_line)
  пересозданы: amount/plan считаются от limit_without_vat, план АХР делится
  на 1000, добавлены amount_year и строка «Итого» (id=0, row_type ROOT).
- v3._limit_template_path_set дописывает id=0 в path-set, чтобы «Итого»
  попадала в diff PATCH-ответа.
- Пересинхронизированы все существующие формы 2.

Ветки аренды/коммуналки/охраны и add_budget_line (AURORA-1887/1909) не тронуты.

Тесты: api/tests/integration/test_form2_ahr_limit.py (7 тестов, числа из
тикета: id24 qty10→69645/69.645, id3→2000/2.0, id7→7500/7.5, id13→6471.875/
6.471875, id17→7500/7.5, «Итого», регрессия АХР_RENT без деления).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This pull request can be merged automatically.
You are not authorized to merge this pull request.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/266-aurora-1897-ahr-limit-period-vat-units:feat/266-aurora-1897-ahr-limit-period-vat-units
git checkout feat/266-aurora-1897-ahr-limit-period-vat-units
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RSHB/DFiP_Budget_planing#269
No description provided.