openspec: change statement-template-lines — полный состав строк ББ/ОФР (#80)

Спека: достройка формы по справочнику строк редакции в GET …/statement,
создание строки и ячейки при первой правке по line_code, пустые шаблонные
строки вне проверок §7 и статуса.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019qoydUVv8g1qHxc123u2RR
This commit is contained in:
Andrey Epifancev 2026-10-09 10:17:23 +04:00
parent 8be43512af
commit 5c7d5393c5
5 changed files with 358 additions and 0 deletions

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-09

View File

@ -0,0 +1,141 @@
# Design: полный состав строк ББ/ОФР на экране проверки
## Context
Форма ББ/ОФР в ядре — `financial_statement_meta` → `financial_statement_column` (колонки-периоды) →
`financial_statement_line` (только строки с кодом, `(meta_id, line_code)` уникален) → `financial_statement_value`
(`(line_id, column_id)` уникален). Строки и ячейки пишет только маппинг (`repositories/statements.insert_statement`), ровно
то, что пришло в `statementDocument` от scan-gateway или tabular-parser. Экран проверки читает форму
`GET /documents/{id}/statement` (`services/statements.StatementService.build_out`, порядок строк — по коду), правит
`PATCH …/statement` (`_apply_cells`: `lock_cells` по `(line_id, column_id)` под `FOR UPDATE`; чужая или отсутствующая
ячейка — 422 `UNKNOWN_CELL`). Проверки §7 (`domain/statement_checks.run_checks`) считают цель присутствующей, если её
код есть среди строк формы; пустая сумма — 0. Статус D-6 (`statement_status`) учитывает `values_ok` по всем ячейкам и
`has_lines`. Редакция формы — `edition_for(document.reporting_date)`: `66n` (≤ 2024-12-31) / `fsbu4` / `None`.
Раскладку «как на бумаге» по разделам делает фронт (`arm-web/src/widgets/DocumentReview/formLayout.ts`).
Справочник «код → наименование» по редакциям есть только в scan-gateway (`api/rules/statement_lines.yaml`): `66n`
заполнен (из старого ядра), `fsbu4` пуст (OQ-150).
## Goals / Non-Goals
**Goals:** аналитик видит все строки формы своей редакции и может заполнить любую, включая потерянную источником;
уже загруженные документы получают это без миграции; распознанные данные не меняются; пустые строки не меняют статус,
проверки и автоподтверждение.
**Non-Goals:** см. proposal.
## Decisions
### D-1. Достраивать в ответе, а не в БД (подход B, решение владельца 09.10.2026)
БД хранит только пришедшее из источника; достройка — чистая функция `domain/statement_lines.complete_form` поверх
прочитанной формы в `build_out`. Альтернатива «дописать строки при маппинге» отклонена: нужна data-миграция для уже
загруженных документов, в БД появляются строки, которых не было в первичке, и проверки §7 начали бы видеть пустые итоги
(пустая 1700 при непустых 1300–1500 — жёсткое нарушение), то есть изменился бы статус документов.
### D-2. Справочник — данные ядра `api/rules/statement_lines.yaml`
```yaml
editions:
- id: 66n
valid_until: 2024-12-31
forms:
BALANCE: [{code: "1110", name: "Нематериальные активы"}, ...] # порядок формы
INCOME: [...]
- id: fsbu4
valid_from: 2025-01-01
forms: {BALANCE: [...], INCOME: [...]}
```
Список, а не словарь — порядок строк формы сохраняется (пригодится фронту и будущему порядку в ответе; сейчас ответ
сортируется по коду, как раньше). Загрузка — один раз при импорте модуля (как `statement_checks.yaml`), валидация
pydantic: коды — четыре цифры, без повторов внутри формы, обе формы в обеих редакциях. Редакция выбирается существующим
`edition_for` — новой логики выбора нет.
Состав (решение владельца 09.10.2026 — из шаблонов `../fixtures/statements-xlsx/templates/`):
- **66n, ББ** («ББ до 01.01.2025»): 1110, 1120, 1130, 1140, 1150, 1160, 1170, 1180, 1190, 1100, 1210, 1220, 1230, 1240,
1250, 1260, 1200, 1600, 1310, 1320, 1340, 1350, 1360, 1370, 1300, 1410, 1420, 1430, 1450, 1400, 1510, 1520, 1530,
1540, 1550, 1500, 1700 (37).
- **66n, ОФР** («ОФР до 01.01.2025»): 2110, 2120, 2100, 2210, 2220, 2200, 2310, 2320, 2330, 2340, 2350, 2300, 2410,
2411, 2412, 2460, 2400, 2510, 2520, 2500, 2900, 2910 (22).
- **fsbu4, ББ** («ББ после 01.01.2025»): 1110, 1150, 1160, 1170, 1180, 1190, 1100, 1210, 1215, 1220, 1230, 1240, 1250,
1260, 1200, 1600, 1310, 1320, 1340, 1350, 1360, 1370, 1300, 1410, 1420, 1430, 1450, 1400, 1510, 1520, 1540, 1550,
1500, 1700 (34).
- **fsbu4, ОФР** («ОФР после 01.01.2025»): 2110, 2120, 2100, 2210, 2220, 2200, 2310, 2320, 2330, 2340, 2350, 2300,
2410, 2411, 2412, 2420, 2400, 2510, 2520, 2530, 2500, 2900, 2910 (23).
Наименования — из тех же шаблонов; склейки с заголовками разделов и сносками исправляются (шаблон склеил объединённые
ячейки): 1110 «АКТИВ» → «Нематериальные активы», 1210 «II. Оборотные активы» → «Запасы», 1310 «ПАССИВ» → «Уставный
капитал (складочный капитал, уставный фонд, вклады товарищей)», 1410 «IV. Долгосрочные обязательства» и 1510 «V.
Краткосрочные обязательства» → «Заемные средства», 2411 «в том числе:» → «текущий налог на прибыль», хвосты сносок
(«Выручка 5», «Налог на прибыль 7», «( / )») и префикс «Справочно» у 2900 убираются, переносы строк — в пробел.
Строки источника, которых нет в справочнике (пятизначные 11101, коды чужой редакции, 1105 «Гудвил» у OCR), остаются как
есть: справочник только добавляет, ничего не убирает и не переименовывает.
### D-3. Достройка ответа
`complete_form(form_type, edition, columns, lines) -> lines'`:
- для каждой строки из БД — недостающие ячейки по всем `columns` (`id = null`, `amount = null`, `string_value = null`,
`confidence = null`, `is_recognized = null`, `is_modified = false`, `original_amount = null`, `modified_* = null`);
- при `edition ≠ None` — строки справочника, кода которых нет среди строк формы: `id = null`, `line_code`, `line_name` из
справочника, `*_confidence = null`, `origin = "template"`, ячейки по всем колонкам как выше;
- `origin` строки из БД: `template`, если `line_code_original_value IS NULL` (строка создана правкой, D-4), иначе `source`
(маппинг всегда пишет `line_code_original_value = line_code`);
- итог — по `line_code` (как сейчас).
Форма без колонок (OCR без шапки) получает строки без ячеек — заполнить нечего, пока не появятся колонки (их ядро не
создаёт; Non-goal).
### D-4. Правка по коду строки
`CellPatch`: `line_id: uuid | None`, `line_code: str | None` (ровно одно, иначе 422 валидации тела), `column_id`, `amount`.
`_apply_cells` (под существующим порядком блокировок «файл `FOR UPDATE` → документ `FOR NO KEY UPDATE`», шапка с
`refresh`):
1. Колонка должна принадлежать форме, иначе 422 `UNKNOWN_CELL`.
2. `line_id`: строка формы (иначе `UNKNOWN_CELL`); если у неё нет ячейки в колонке — ячейка создаётся.
3. `line_code`: если строка с таким кодом в форме уже есть — 422 `UNKNOWN_CELL` с сообщением «строка уже есть в форме —
передайте line_id» (фронт после ответа знает `id`); если кода нет в справочнике редакции документа (или редакция не
определена) — 422 `UNKNOWN_CELL`. Иначе создаётся `financial_statement_line` (`line_code`, `line_name` из
справочника, `line_code_original_value = NULL`, `line_name_original_value = NULL`, `*_confidence = NULL`,
`*_modified = false`) и ячейка. Две ячейки одной новой строки в одном запросе создают одну строку.
4. Создаваемая ячейка: `original_amount = NULL`, `is_recognized = NULL`, `confidence = NULL`, `string_value = NULL`;
затем общая логика правки: `amount`, `is_modified = (amount ≠ original_amount)`, `modified_by/at`.
5. `amount = null` для несуществующей ячейки — не изменение: ничего не создаётся, в аудит не попадает.
6. Повтор ячейки в запросе (по `(line_id|line_code, column_id)`) — 422 `DUPLICATE_CELL`, как сейчас.
Гонки двух правок одной формы исключены блокировкой документа: второй запрос ждёт и видит строку, созданную первым
(получит 422 «передайте line_id» и перечитает форму). Уникальный индекс `(meta_id, line_code)` — страховка.
Аудит — одно `RECOGNIZED_DATA_EDIT` на запрос, как сейчас; в `before/after` ячейки созданной строки ключ —
`line_code` + `column_index`, `before.amount = null`.
### D-5. Пустые шаблонные строки вне проверок и статуса
Достроенные строки в БД нет — `statement_amounts`, `value_flags`, `has_lines`, `_line_amount` (межформенная) их не
видят. Созданная правкой строка (`line_code_original_value IS NULL`) участвует в `statement_amounts`, только если у неё
есть хотя бы одна непустая сумма (фильтр в запросе). Так очищенная до пустоты строка снова «как будто её нет», а
частично заполненная (1700 есть в текущей колонке, нет в прошлой) проверяется по всем колонкам, как строка источника, —
расхождение в незаполненной колонке подскажет аналитику заполнить и её. `values_ok` её ячейки не тревожат:
`is_recognized = NULL` и `confidence = NULL` — «без сомнений», правленные — `is_modified`.
### D-6. Фронт (arm-web, PR в `test`)
`statementGrid`: ключ строки — `line.id ?? "code:" + line_code`, ключ ячейки — `rowKey:column_id`; `draftToPatch`
отправляет `line_id` или `line_code` по ключу. Достроенная пустая ячейка (`id = null`) без подсветки «требует проверки»
(`is_recognized = null`), введённое значение подсвечивается как правка. `formLayout` уже раскладывает строки по разделам —
меняется только то, что разделы теперь всегда полные. Сверка с макетом [41] (PNG `docs/mockup/screens/`) в Playwright и
ручной прогон сценария: пустая строка → ввод → «Сохранить» → проверки пересчитаны.
## Risks / Trade-offs
- **Две копии справочника (ядро и scan-gateway) расходятся**: в ядре 66n — по шаблону банка (без 1105, 1215, 1330 и с
«Доходные вложения в материальные ценности» у 1160), в scan-gateway — словарь старого ядра. Наименования строк
источника не трогаются, так что расхождение видно только в наименовании достроенных строк. Follow-up: заполнить
`fsbu4` в scan-gateway и свести копии.
- **Шаблоны банка — не нормативный текст.** Если в ФСБУ 4/2023 окажутся строки, которых нет в шаблоне, они не
достроятся; правится данными (`statement_lines.yaml`), без кода.
- **Ответ GET растёт** на 30–40 строк × колонки — несущественно.
## Decisions made without discussion
| # | Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|---|
| R-1 | 09.10.2026 | Состав **66n** в ядре — из шаблонов банка «до 01.01.2025», а не копия словаря scan-gateway (в чате согласовано «копия») | В словаре scan-gateway у 66n коды и имена эпохи ФСБУ (1105 «Гудвил», 1215, 1160 «Инвестиционная недвижимость»), а шаблон банка — тот же источник, что выбран для fsbu4; одна методика для обеих редакций | Неверное наименование или лишняя/недостающая достроенная строка у отчётности до 2025 года; правится данными |
| R-2 | 09.10.2026 | Наименования склеенных строк шаблона исправлены по форме (1110, 1210, 1310, 1410, 1510, 2411, сноски) | Склейка — артефакт объединённых ячеек шаблона, а не наименование строки | Неточное наименование достроенной строки; правится данными |
| R-3 | 09.10.2026 | Созданная правкой строка участвует в проверках §7, только пока у неё есть непустая сумма | Решение владельца «пустая строка не влияет на проверки» должно сохраняться и после очистки | Аналитик очистил итог — нарушение исчезло вместе с ним; так же ведёт себя отсутствующая строка источника |
| R-4 | 09.10.2026 | Без редакции (нет даты периода) строки не достраиваются, недостающие ячейки — достраиваются | Не из чего выбрать состав; дата периода нужна и для подтверждения (`PERIOD_REQUIRED`) — после её правки строки появятся | Аналитик не видит пустых строк, пока не введёт дату отчёта |
| R-5 | 09.10.2026 | `line_code` для кода, уже существующего в форме, — 422 `UNKNOWN_CELL`, а не тихая правка существующей строки | Один способ адресации существующей ячейки (`line_id`), второй запрос в гонке не перезапишет чужую правку молча | Лишний круг фронта: перечитать форму и повторить |
| R-6 | 09.10.2026 | Ячейка, которой нет у существующей строки, создаётся при правке по `line_id` | Та же проблема, что с отсутствующей строкой; отдельного API не требует | — |

View File

@ -0,0 +1,67 @@
# Proposal: полный состав строк ББ/ОФР на экране проверки
**Срез:** доработка Среза 3b (`../docs/context/09-delivery-plan.md`) — экран проверки форм ББ/ОФР.
**Репозитории:** arm-core (этот change, issue RSHB/arm-core#80); arm-web — PR в `test` (раскладка, правка по коду строки);
`../docs` — bounded-PR: правило в `05-domain-rules.md` §7 «Ядро» и §11/§12, OQ-150 в `07-open-questions.md`, гайды
`dev/frontend-api-guide.md`, `dev/qa-guide.md`. Follow-up: словарь `fsbu4` в scan-gateway (OQ-150 для нормализатора).
DBML и контракты обмена не меняются, миграций нет.
## Why
Ядро хранит только строки формы, пришедшие от OCR или разбора Excel (`financial_statement_line`), и правит только
существующую ячейку (`PATCH …/statement` по `line_id` + `column_id`, иначе 422 `UNKNOWN_CELL`). Если источник строку
потерял — пример: «Русский бисквит», 1 кв. 2025, ИС склеила «1500\n1700» и строка 1700 пропала (до OQ-156), — аналитик
не может её заполнить, хотя форма бухгалтерской отчётности обязана её содержать. То же с ячейкой, которой нет у
существующей строки в какой-то колонке. Решение владельца 09.10.2026: на экране проверки всегда показывать все строки
формы своей редакции и давать их заполнять.
## What Changes
- **Справочник строк форм по редакциям** — `api/rules/statement_lines.yaml` (данные, не код): редакции `66n`
(≤ 2024-12-31) и `fsbu4` (с 2025-01-01), для каждой — ББ и ОФР: коды в порядке формы и наименования. Источник обеих
редакций — шаблоны банка `../fixtures/statements-xlsx/templates/` («ББ/ОФР до 01.01.2025», «ББ/ОФР после 01.01.2025»)
с исправленными склейками наименований и заголовков разделов (решение владельца 09.10.2026: данные OQ-150 — из
шаблонов).
- **`GET /documents/{id}/statement` достраивает форму.** К строкам из БД добавляются строки справочника редакции
`edition`, которых в форме нет, и недостающие ячейки у существующих строк по всем колонкам формы. У достроенной строки
`id = null`, `origin = "template"`, наименование — из справочника; у достроенной ячейки `id = null`, `amount = null`,
`is_recognized = null`, `is_modified = false`. Новое поле `LineOut.origin`: `source` — строка пришла из источника,
`template` — строка из справочника (достроенная или созданная правкой аналитика). Без `edition` (нет даты периода)
строки не достраиваются, недостающие ячейки — достраиваются.
- **`PATCH …/statement` создаёт строку и ячейку при первой правке.** Элемент `values[]` адресует ячейку либо
`line_id` + `column_id` (как сейчас; если у строки нет ячейки в этой колонке — ячейка создаётся), либо `line_code` +
`column_id` — для строки справочника редакции, которой нет в форме: ядро создаёт `financial_statement_line`
(`line_code_original_value = NULL` — признак «не из источника», наименование из справочника) и
`financial_statement_value` (`original_amount = NULL`, `is_recognized = NULL`, `confidence = NULL`). Дальше — как любая
правка: `is_modified`, `modified_by/at`, один `audit_event(RECOGNIZED_DATA_EDIT)` с `before = null`, сброс акцепта,
пересчёт статуса.
- **Пустая шаблонная строка не влияет на статус и проверки** (решение владельца 09.10.2026). Достроенные строки в БД
не пишутся и в проверки §7, `values_ok` (D-6) и автоподтверждение не попадают. Созданная правкой строка (`origin =
template`) участвует в проверках §7, только пока у неё есть хотя бы одна непустая сумма; очищенная до пустоты —
снова как будто её нет.
Таблицы DBML v5.17: `financial_statement_line` (`line_code_original_value` NULL — признак строки не из источника),
`financial_statement_value`, `financial_statement_meta` (сброс акцепта), `document` (статус), `audit_event`. Новых полей
нет.
## Non-goals
- Запись достроенных строк в БД при маппинге и миграция данных — нет: БД хранит то, что пришло (первичка).
- Обязательность заполнения: пустая строка не блокирует подтверждение и автоподтверждение; полноту ловят проверки §7.
- Заполнение `fsbu4` в словаре scan-gateway (нормализатор, golden-эталоны) — follow-up issue.
- Строки без кода (OQ-36, OQ-141, OQ-149), пятизначные подстроки в справочнике, отдельные формы для упрощённой
отчётности малых предприятий — нет.
- Порядок строк в ответе — по коду, как сейчас; раскладка по разделам — на фронте (`formLayout.ts`).
- Удаление созданной строки аналитиком — нет (очистка суммы до `null` возвращает её в состояние «пустая шаблонная»).
- Правка кода и наименования строки — нет (решение Среза 3b).
## Impact
- Код: `domain/statement_lines.py` (новый: загрузка справочника, достройка формы — чистые функции),
`api/rules/statement_lines.yaml` (новый), `schemas/statements.py` (`LineOut.id`/`ValueOut.id` nullable,
`LineOut.origin`, `CellPatch` с `line_id | line_code`), `services/statements.py` (`build_out`, `_apply_cells`),
`repositories/statements.py` (создание строки/ячейки, исключение пустых шаблонных строк в `statement_amounts`).
- Спеки (дельты): `documents-api` («Форма ББ/ОФР для экрана проверки», «Правка формы»), `statement-checks`
(«Соотношения §7 по колонкам»).
- Фронт arm-web: ключ шаблонной строки, отправка `line_code`, отсутствие подсветки «требует проверки» у пустых
достроенных ячеек.

View File

@ -0,0 +1,103 @@
## MODIFIED Requirements
### Requirement: Форма ББ/ОФР для экрана проверки
`GET /api/v1/documents/{document_id}/statement` (`DOCUMENT_VIEW`, policy по ЮЛ) SHALL отдавать для документа
`BALANCE`/`INCOME`: `document` (`id`, `document_type`, `status`, `period_year`, `period_quarter`, `reporting_date`,
`counterparty_check`, `locator`, `file_id`); `edition` (`66n` при дате периода ≤ 2024-12-31, `fsbu4` — позже, `null` без
даты); `header` — для `inn`, `report_date`, `units`: `value`, `original_value`, `confidence` (у ИНН), `verified`,
`modified`; плюс `organization_name`, `okud`, `okpo`, `okopf`, `okfs`, `address`; `columns[]` (`id`, `column_index`,
`label`, `period_date`, `is_current`); `lines[]` по `line_code` (`id`, `origin`, `line_code`, `line_code_confidence`,
`line_name`, `line_name_confidence`, `values[]` по `column_index`: `id`, `column_id`, `amount`, `string_value`,
`confidence`, `is_recognized`, `is_modified`, `original_amount`, `modified_by`, `modified_at`); `checks` (`failed`,
`soft_failed`, `cross_form`, `accepted`: `by`, `at`, `comment` или `null`). Документ другого типа — 409
`DOCUMENT_NOT_STATEMENT`; чужой или несуществующий — 404.
Форма SHALL достраиваться в ответе по справочнику строк редакции `edition` (`api/rules/statement_lines.yaml`, свой
список кодов и наименований для `BALANCE` и `INCOME` каждой редакции): строки справочника, кода которых нет в форме,
добавляются с `id = null`, `origin = "template"`, наименованием из справочника и `*_confidence = null`; у каждой строки
SHALL быть значение по каждой колонке формы — недостающее добавляется с `id = null`, `amount = null`,
`string_value = null`, `confidence = null`, `is_recognized = null`, `is_modified = false`, `original_amount = null`,
`modified_by = null`, `modified_at = null`. При `edition = null` строки не достраиваются, недостающие значения —
достраиваются. Строка из БД имеет `origin = "source"`, если пришла из источника, и `"template"`, если создана правкой
аналитика (`line_code_original_value IS NULL`). Строки источника, которых нет в справочнике, отдаются как есть.
Достроенные строки и значения MUST NOT записываться в БД при чтении.
#### Scenario: Форма для экрана
- **WHEN** запрошена форма `BALANCE` из `mezheninovskaya/2025-Q3/1_Форма1_бух_09`
- **THEN** `edition = fsbu4`, все 37 строк файла — `origin = source` с `id`, каждый код справочника `fsbu4` / `BALANCE`
есть среди `lines`, у каждой строки значения по всем колонкам, `checks.failed` пуст
#### Scenario: Строка, потерянная источником
- **WHEN** у формы `BALANCE` за 2025 Q1 в БД нет строки 1700 (OCR склеил «1500\n1700» до OQ-156)
- **THEN** в `lines` есть 1700: `id = null`, `origin = template`, `line_name = "БАЛАНС"`, значения по всем колонкам
`id = null`, `amount = null`, `is_recognized = null`; статус документа и `checks` — те же, что без достройки
#### Scenario: Недостающая ячейка
- **WHEN** у строки 1150 в БД есть значение только для колонки 0, а у формы две колонки
- **THEN** у 1150 два значения; значение колонки 1 — `id = null`, `amount = null`
#### Scenario: Нет даты периода
- **WHEN** у формы OCR не определена дата периода (`edition = null`)
- **THEN** `lines` — только строки из БД, у каждой значения по всем колонкам
#### Scenario: Не форма
- **WHEN** запрошена форма документа `OSV`
- **THEN** 409 `DOCUMENT_NOT_STATEMENT`
### Requirement: Правка формы
`PATCH /api/v1/documents/{document_id}/statement` (`DOCUMENT_EDIT`, policy) SHALL принимать частичное тело `header`
(`inn` — 10 или 12 цифр, иначе 422 `INVALID_INN`; `report_date` — дата; `units` — непустая строка ≤ 50) и `values[]`
(ровно одно из `line_id` | `line_code`, плюс `column_id`, `amount` — число с ≤ 2 знаками или `null`) и в одной
транзакции: при первой правке значения записывать `original_amount` = прежняя сумма; ставить `amount`,
`is_modified = (amount ≠ original_amount)`, `modified_by/at`; для поля шапки — `*_modified = true`, `*_original_value`
не менять; после `inn` — пересчитать `counterparty_check`; после `report_date` — пересчитать `period_year`,
`period_quarter`, `reporting_date`; сбросить акцепт проверок; пересчитать статус; записать одно
`audit_event(RECOGNIZED_DATA_EDIT, RECOGNITION)` с `before/after` изменённых полей и ячеек; вернуть форму, как `GET`.
Документ `CONFIRMED` — 409 `DOCUMENT_READONLY`; файл `SUPERSEDED` — 409 `FILE_SUPERSEDED`; не форма — 409
`DOCUMENT_NOT_STATEMENT`; ячейка не этого документа — 422 `UNKNOWN_CELL`. Запрос без изменений — 200 без аудита.
Блокировки — «файл `FOR UPDATE` → документ `FOR NO KEY UPDATE`».
Ячейка, которой нет в БД, SHALL создаваться при правке:
- по `line_id` строки формы и `column_id` колонки формы, если у строки нет значения в этой колонке;
- по `line_code`, если строки с этим кодом в форме нет, а код есть в справочнике редакции документа для типа формы:
создаётся `financial_statement_line` (`line_code`, `line_name` из справочника, `line_code_original_value = NULL`,
`line_name_original_value = NULL`, `*_confidence = NULL`, `*_modified = false`); несколько ячеек одной новой строки в
запросе создают одну строку.
Созданная ячейка — `original_amount = NULL`, `is_recognized = NULL`, `confidence = NULL`, `string_value = NULL`, далее
общая логика правки. `line_code`, строка с которым в форме уже есть, код вне справочника редакции или редакция не
определена — 422 `UNKNOWN_CELL`; колонка не этой формы — 422 `UNKNOWN_CELL`; одна ячейка дважды в запросе — 422
`DUPLICATE_CELL`. `amount = null` для ячейки, которой нет в БД, — не изменение: ничего не создаётся и не аудируется.
#### Scenario: Правка значения
- **WHEN** аналитик меняет сумму строки 1150 колонки 0 с 100 на 120
- **THEN** `amount = 120`, `original_amount = 100`, `is_modified = true`, аудит `RECOGNIZED_DATA_EDIT` с `before 100`, `after 120`
#### Scenario: Возврат к исходному
- **WHEN** затем та же ячейка меняется обратно на 100
- **THEN** `is_modified = false`, `original_amount = 100`
#### Scenario: Заполнение строки, которой нет в форме
- **WHEN** у формы `BALANCE` редакции `fsbu4` нет строки 1700, аналитик передаёт `{line_code: "1700", column_id: <колонка 0>, amount: 2371333}`
- **THEN** в БД создана строка 1700 (`line_code_original_value = NULL`, `line_name = "БАЛАНС"`) и значение
`amount = 2371333`, `original_amount = NULL`, `is_modified = true`; в ответе у 1700 `id` заполнен, `origin = template`;
аудит `RECOGNIZED_DATA_EDIT` с `before.amount = null`; акцепт сброшен, статус пересчитан
#### Scenario: Код, который уже есть в форме
- **WHEN** передан `line_code: "1150"`, а строка 1150 в форме есть
- **THEN** 422 `UNKNOWN_CELL`, данные не изменились
#### Scenario: Код вне справочника
- **WHEN** передан `line_code: "1105"` для формы редакции `66n`
- **THEN** 422 `UNKNOWN_CELL`
#### Scenario: Пустая сумма в несуществующую ячейку
- **WHEN** передан `{line_code: "1530", column_id: <колонка 0>, amount: null}` и строки 1530 нет
- **THEN** 200, строка не создана, аудита нет
#### Scenario: Правка даты отчёта
- **WHEN** у формы OCR без периода дата отчёта правится на 2025-06-30
- **THEN** `period_year = 2025`, `period_quarter = Q2`, `report_date_modified = true`, `report_date_original_value` не изменился
#### Scenario: Подтверждённая форма
- **WHEN** правится форма `CONFIRMED`
- **THEN** 409 `DOCUMENT_READONLY`, данные не изменились

View File

@ -0,0 +1,45 @@
## MODIFIED Requirements
### Requirement: Соотношения §7 по колонкам
Проверки SHALL считаться чистой функцией по текущим `financial_statement_value.amount` документа для каждой колонки
отдельно по словарю `api/rules/statement_checks.yaml` (копия словаря tabular-parser): соотношение проверяется, только
если присутствует его левая часть; отсутствующий код и пустая сумма — 0; выполнено, если `|левая − правая| ≤ tolerance`
(1, в единицах формы) хотя бы в одном варианте правой части — как записано или с кодами `abs_terms` по модулю. Нарушение
жёсткого (`strict`) соотношения, не превышающее `rounding_tolerance_percent` (0,001 %) от наибольшего по модулю числа
соотношения, SHALL считаться мягким («в пределах округления»). Мягкие: соотношения `strict: false` и «сумма пятизначных
подстрок = четырёхзначный родитель». Результат — `{failed: [...], soft_failed: [...]}`, элемент — `{column_index,
relation, expected, actual}`; MUST NOT храниться в БД.
Присутствующими SHALL считаться строки формы из БД, кроме строк, созданных правкой аналитика
(`line_code_original_value IS NULL`), у которых все суммы пустые; строки, достроенные в ответе по справочнику
(`documents-api`, `id = null`), в проверках MUST NOT участвовать. Созданная правкой строка с хотя бы одной непустой
суммой проверяется по всем колонкам, пустые суммы — 0.
#### Scenario: Сходящийся баланс
- **WHEN** проверяется форма `mezheninovskaya/2025-Q3/1_Форма1_бух_09`
- **THEN** `failed` пуст
#### Scenario: Нарушение жёсткого соотношения
- **WHEN** у баланса 1600 = 1000, 1100 = 300, 1200 = 600 в колонке 0
- **THEN** `failed` содержит `{column_index: 0, relation: "1600 = 1100 + 1200", expected: 1000, actual: 900}`
#### Scenario: Расходы со знаком минус
- **WHEN** ОФР 2110 = 100, 2120 = −60, 2100 = 40
- **THEN** `2100 = 2110 − 2120` выполнено через вариант с `abs_terms`
#### Scenario: Правка исправляет нарушение
- **WHEN** аналитик правит 1200 с 600 на 700 в форме из предыдущего сценария
- **THEN** проверка по текущим суммам: `failed` пуст
#### Scenario: Пустая шаблонная строка не проверяется
- **WHEN** в форме нет строки 1700 (в ответе она достроена пустой), 1300 + 1400 + 1500 = 2 371 333 в колонке 0
- **THEN** `1700 = 1300 + 1400 + 1500` и `1600 = 1700` не проверяются, `failed` — как без достройки
#### Scenario: Заполненная шаблонная строка проверяется
- **WHEN** аналитик заполнил 1700 = 2 371 333 только в колонке 0, а в колонке 1 1300 + 1400 + 1500 = 2 000 000
- **THEN** в колонке 0 соотношение выполнено; в колонке 1 `failed` содержит `1700 = 1300 + 1400 + 1500` с `expected 0`,
`actual 2000000`
#### Scenario: Очищенная шаблонная строка
- **WHEN** затем аналитик очищает 1700 в колонке 0 (`amount = null`)
- **THEN** строка 1700 снова не участвует в проверках