diff --git a/openspec/changes/statement-template-lines/.openspec.yaml b/openspec/changes/statement-template-lines/.openspec.yaml new file mode 100644 index 0000000..8126432 --- /dev/null +++ b/openspec/changes/statement-template-lines/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-09 diff --git a/openspec/changes/statement-template-lines/design.md b/openspec/changes/statement-template-lines/design.md new file mode 100644 index 0000000..1852287 --- /dev/null +++ b/openspec/changes/statement-template-lines/design.md @@ -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 не требует | — | diff --git a/openspec/changes/statement-template-lines/proposal.md b/openspec/changes/statement-template-lines/proposal.md new file mode 100644 index 0000000..dcaaa72 --- /dev/null +++ b/openspec/changes/statement-template-lines/proposal.md @@ -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`, отсутствие подсветки «требует проверки» у пустых + достроенных ячеек. diff --git a/openspec/changes/statement-template-lines/specs/documents-api/spec.md b/openspec/changes/statement-template-lines/specs/documents-api/spec.md new file mode 100644 index 0000000..98b9e80 --- /dev/null +++ b/openspec/changes/statement-template-lines/specs/documents-api/spec.md @@ -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`, данные не изменились diff --git a/openspec/changes/statement-template-lines/specs/statement-checks/spec.md b/openspec/changes/statement-template-lines/specs/statement-checks/spec.md new file mode 100644 index 0000000..6d38e0a --- /dev/null +++ b/openspec/changes/statement-template-lines/specs/statement-checks/spec.md @@ -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 снова не участвует в проверках