docs: полный состав строк ББ/ОФР — §7, OQ-150, гайды (#81)

- 05-domain-rules: §7 абзац «Полный состав формы», §11 пометка про достройку строк
- 07-open-questions: OQ-150 — решение владельца 09.10.2026
- frontend-api-guide, api-contract-slice-3b: values[] с line_code, origin, id = null, новые случаи UNKNOWN_CELL
- qa-guide: F8a, F8b, W28a, W28b

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-09 12:21:41 +04:00
parent 045604062e
commit 1851c248fa
5 changed files with 28 additions and 9 deletions

View File

@ -492,6 +492,15 @@ variance = |amount_a − amount_b|; variance_pct = variance / min(amount_a, amou
без даты; решение владельца 07.10.2026 по OQ-145 — редакция в контракт не передаётся, досрочный ФСБУ 4/2023 — новый OQ при
первом случае); раздельные словари и формулы по редакциям — вместе с составом `fsbu4` (OQ-146, OQ-150).
**Полный состав формы (arm-core#80, решение владельца 09.10.2026).** Экран проверки показывает все строки формы
редакции документа (`edition`): справочник `arm-core/api/rules/statement_lines.yaml` (66н и ФСБУ 4/2023, ББ и ОФР, из
шаблонов банка `fixtures/statements-xlsx/templates/`). Строки и ячейки, которых нет в источнике, ядро достраивает в
ответе (`id = null`, `origin = template`) и в БД не пишет; первая непустая правка создаёт строку
(`line_code_original_value = NULL` — признак «не из источника») и ячейку как правку аналитика (аудит
`RECOGNIZED_DATA_EDIT`). Пустая добавленная строка не влияет на статус, проверки §7 и автоподтверждение; созданная
правкой строка участвует в проверках, пока у неё есть хоть одна сумма. Без редакции (нет даты периода) строки не
достраиваются. Строки «БАЛАНС» (1600, 1700) на экране выделяются цветом.
Когда: после распознавания; после «Сохранить» правок; при «Подтвердить данные». Непройдено → `document.status =
NEEDS_CHECK`; пройдено/акцептовано + нажато «Подтвердить» → `CONFIRMED` + `audit_event(DOCUMENT_CONFIRM)`.
Отрицательные значения в формах — в скобках `(2100)` → `-2100`. Значения с `confidence < 100` подсвечиваются.
@ -873,7 +882,7 @@ PDF-пути, решение №1 спеки 3a; `pages:…` — только е
(ИНН, имя, units, report_date, okud — из `organization/inn/units/date.form_name`, с низкой уверенностью → `NEEDS_CHECK`) +
`financial_statement_column` (из шапки `rows[0]`, если она есть; `period_date` распарсить из «На 30 июня 2025 г», `is_current` =
первая; без шапки — колонки без дат, документ `NEEDS_CHECK`) + `financial_statement_line` (`cells[1]` код, `cells[0]` имя;
только строки с кодом) + `financial_statement_value` (число: убрать пробелы/`-`, скобки → минус; `confidence`; `is_recognized`).
только строки с кодом); недостающие строки формы ядро достраивает на экране по справочнику редакции, §7 + `financial_statement_value` (число: убрать пробелы/`-`, скобки → минус; `confidence`; `is_recognized`).
Сверки: ИНН/имя vs `legal_entity` → `document.counterparty_check`; дата vs период версии → `period_check`.
### §11.1 Очистка OCR-текста (перенос из старого ядра, Срез 3)

View File

@ -155,7 +155,7 @@
| OQ-147 | Закрывающая скобка, распознанная OCR как `}` или `]` (`(7744536}` в `aep_3.json`): дословный перенос `parse_amount` минус не ставит — сумма положительная, `is_recognized = false`. Расширить ли множество закрывающих скобок в §11.1 п. 7 после проверки на стенде | change `slice-3a-ocr-normalizer`, решение №12; владелец 06.10.2026: «пока устроит как есть» | scan-gateway `normalizer/ocr/amounts.py`, §11.1 п. 7 | открыт: допущение — перенос дословный; знак теряется, строка подсвечена `is_recognized = false`, исходный текст — в `raw/` |
| OQ-148 | Длинное/среднее тире «—»/«–» как прочерк: дословный `parse_amount` вырезает тире как «лишний» символ → сумма `null`, но `is_recognized = false` даже при `confidence = 100` (дефис «-» даёт `true`). Лишний `needs_check` на формах с длинным тире. Считать ли прочерк любого вида распознанным | change `slice-3a-ocr-normalizer`, решение №20 | scan-gateway `amounts.py`, §11.1 п. 7, доля `done` на реальных сканах | открыт: допущение — перенос дословный; правило уточняется после прогона на стенде |
| OQ-149 | Строки таблицы OCR, отброшенные классификацией (поведение источника, §11.1 п. 4), не восстанавливаются: с непустым нечисловым кодом (`1150 :`) и с пустым кодом, но с суммами (в `aep_3.json` — «БАЛАНС» и «Выручка»: код пуст, мусор `211(1 \|` стоит в колонке значения). Нормализатор выдаёт предупреждения `строк с нечисловым кодом пропущено: N` и `строк без кода с суммами пропущено: N` (как OQ-141 для Excel), документ — `needs_check`, содержимое строк остаётся в `raw/`. Восстанавливать ли такие строки по наименованию (словарь редакции) или по структуре формы | `fixtures/ocr/aep_3.json`, `aep_6.json`, change `slice-3a-ocr-normalizer`, решение №29, §11.1 п. 4 | scan-gateway `rows.py`/`document.py`, полнота форм, OQ-36, OQ-139, OQ-141 | открыт: допущение — строки пропускаются с предупреждением; аналитик видит расхождение итогов в проверках 3b |
| OQ-150 | Состав редакции `fsbu4` (ФСБУ 4/2023, отчётность с 2025 года) в `api/rules/statement_lines.yaml` scan-gateway: коды и наименования строк ББ и ОФР; источник — Приложения 3–4 к ФСБУ 4/2023 (черновой перечень кодов — шаблоны `fixtures/statements-xlsx/templates/`, OQ-142). Пока `lines: {}` | change `slice-3a-ocr-normalizer`, D-7 | scan-gateway `api/rules/`, эталоны `fixtures/ocr/expected_normalize/` для документов 2025 года, статус `done` | открыт: **данные даёт владелец**, код нормализатора не меняется; до заполнения документы 2025 года идут с именами из OCR, предупреждением «редакция fsbu4 не заполнена» и `needs_check`. Порядок заполнения — `scan-gateway/README.md` |
| OQ-150 | Состав редакции `fsbu4` (ФСБУ 4/2023, отчётность с 2025 года) в `api/rules/statement_lines.yaml` scan-gateway: коды и наименования строк ББ и ОФР; источник — Приложения 3–4 к ФСБУ 4/2023 (черновой перечень кодов — шаблоны `fixtures/statements-xlsx/templates/`, OQ-142). Пока `lines: {}` | change `slice-3a-ocr-normalizer`, D-7 | scan-gateway `api/rules/`, эталоны `fixtures/ocr/expected_normalize/` для документов 2025 года, статус `done` | открыт: **данные даёт владелец**, код нормализатора не меняется; до заполнения документы 2025 года идут с именами из OCR, предупреждением «редакция fsbu4 не заполнена» и `needs_check`. Порядок заполнения — `scan-gateway/README.md` **09.10.2026, решение владельца:** состав `fsbu4` (и `66n`) для ядра принят из шаблонов банка — `arm-core/api/rules/statement_lines.yaml` (arm-core#80); словарь scan-gateway — follow-up, копии расходятся (66н scan-gateway — словарь старого ядра). |
| OQ-151 | Эмулятор ИС Сканирование: файл, совпавший по sha256 с PDF из `SCAN_FIXTURES_DIR`, у которого нет JSON-набора (`<имя>.json`), отвечает «нет пары» (`INVALID_DOCUMENT`) и **не** уходит на `SCAN_EMULATOR_DEFAULT`. Пробел спеки эмулятора: считать ли такой файл промахом и отдавать набор по умолчанию | change `slice-3a-ocr-normalizer`, спека `scan-emulator` | scan-gateway `scan_emulator`, dev-стенд (`dev/qa-guide.md` §6) | открыт: допущение — поведение как есть; PDF без JSON-пары в `fixtures/ocr` не класть |
| OQ-152 | Повтор кода строки в таблице OCR: остаётся **первая** строка (паритет с Excel-путём, §12 п. 5; решение №15), но в `aep_6.json` (ОФР) код 2410 повторён, оставленная первая строка пуста, а отброшенная содержит `(649)`. Выбрать правило «первая строка с суммой» вместо «первая» (для `aep_3` оно даёт иной результат, чем для `aep_6` — нужна проверка на стенде) | change `slice-3a-ocr-normalizer`, решение №34 (R-16), финальное ревью I-2 | scan-gateway `document.py`, §11.1 п. 4, Excel-путь §12 п. 5 | открыт: допущение — первая строка; предупреждение «код N повторяется» сигнализирует аналитику, сумма отброшенной строки остаётся в `raw/` |
| OQ-153 | Журналировать ли скачивание первички (`GET /files/{id}/content`) и CSV чек-листа (ТЗ 8.2: перечень событий журнала включает «выгрузку»)? Экспорт сводной ОСВ в xlsx пишет `audit_event(EXPORT)` (Срез 4), а скачивание исходного файла и CSV чек-листа в Срезе 3b — нет; комментарии к документу тоже не аудируются (`comment` хранит автора и время) | change `slice-3b-statements-api`, design D-10, D-11, D-13, решение №8; ТЗ 8.2 | `audit_event`, `GET /files/{id}/content`, `GET …/checklist?format=csv`, §9.5 п. 6 | **закрыт решением владельца 07.10.2026 (вариант А):** скачивание исходного файла (`GET /files/{id}/content`, это же и просмотр PDF) и CSV чек-листа в `audit_event` не пишутся; `EXPORT` пишет только экспорт сводной ОСВ в xlsx (Срез 4) |

View File

@ -32,7 +32,7 @@ D-1…D-14, «Decisions made without discussion» №1–№22). Правила
| Метод и путь | Право | Запрос | Ответ | Ошибки | Экран |
|---|---|---|---|---|---|
| `GET /documents/{document_id}/statement` | VIEW | — | 200 `StatementOut` | 404 `NOT_FOUND`, 409 `DOCUMENT_NOT_STATEMENT` | [41] |
| `PATCH /documents/{document_id}/statement` | EDIT | `StatementPatch {header?, values[]}` | 200 `StatementOut` (состояние после правки) | 404, 409 `FILE_SUPERSEDED` / `DOCUMENT_READONLY` / `DOCUMENT_NOT_CONFIRMABLE` / `DOCUMENT_NOT_STATEMENT`, 422 `INVALID_INN` / `UNKNOWN_CELL` / `DUPLICATE_CELL`, 422 (pydantic-массив) | [41-02] «Сохранить» |
| `PATCH /documents/{document_id}/statement` | EDIT | `StatementPatch {header?, values[]}` (`values[]` — `{line_id | line_code, column_id, amount}`) | 200 `StatementOut` (состояние после правки) | 404, 409 `FILE_SUPERSEDED` / `DOCUMENT_READONLY` / `DOCUMENT_NOT_CONFIRMABLE` / `DOCUMENT_NOT_STATEMENT`, 422 `INVALID_INN` / `UNKNOWN_CELL` / `DUPLICATE_CELL`, 422 (pydantic-массив) | [41-02] «Сохранить» |
| `POST /documents/{document_id}/statement/checks/accept` | EDIT | `ChecksAcceptIn {comment}` (1–2000 символов после обрезки) | 200 `StatementOut` | 404, 409 `NOTHING_TO_ACCEPT` и те же 409, что у `PATCH`, 422 (pydantic-массив) | [41] блок «Логические ошибки» |
| `POST /documents/{document_id}/confirm` | EDIT | — | 200 `DocumentOut` (как в Срезе 2c) | 404, 409 `FILE_SUPERSEDED` / `ALREADY_CONFIRMED` / `DOCUMENT_NOT_CONFIRMABLE` / **`PERIOD_REQUIRED`** / **`CHECKS_NOT_PASSED`** | [41] «Подтвердить данные» |
| `POST /documents/{document_id}/comments` | EDIT | `CommentIn {text}` (1–4000 символов после обрезки пробелов) | 201 `CommentOut` | 404, 422 (pydantic-массив) | [41] «Добавить комментарий» |
@ -76,8 +76,8 @@ D-1…D-14, «Decisions made without discussion» №1–№22). Правила
**`StatementPatch`** `{header?: {inn?, report_date?, units?}, values?: [{line_id, column_id, amount}]}` — частичное тело, лишние поля
запрещены (422). `header.inn` — строка из 10 или 12 цифр (любое другое значение, включая `null`, — 422 `INVALID_INN`: ИНН очистить нельзя); `header.report_date` — дата, `null` очищает; `header.units` — непустая
строка после обрезки пробелов, ≤ 50 (`null` — «не менять», единицы очистить нельзя; пробельная строка — 422 pydantic-массивом). Поле шапки, которого нет в теле, не меняется. `values` — до 5000 ячеек; одна ячейка дважды в запросе — 422
`DUPLICATE_CELL`; `line_id`/`column_id` не этого документа — 422 `UNKNOWN_CELL`. **`ChecksAcceptIn`** `{comment}`.
строка после обрезки пробелов, ≤ 50 (`null` — «не менять», единицы очистить нельзя; пробельная строка — 422 pydantic-массивом). Поле шапки, которого нет в теле, не меняется. `values` — до 5000 ячеек: `{line_id | line_code, column_id, amount}` (ровно одно из `line_id`/`line_code`); одна ячейка дважды в запросе — 422
`DUPLICATE_CELL`; `line_id`/`column_id` не этого документа — 422 `UNKNOWN_CELL`. **Полный состав формы (arm-core#80):** в `StatementOut` `lines[].origin` (`source` | `template`), у достроенных по справочнику редакции строк и ячеек `id = null`; `line_code` создаёт строку (`line_code_original_value = NULL`) при первой непустой сумме. Новые случаи `UNKNOWN_CELL`: код уже есть в форме (передайте `line_id`), код вне справочника, нет редакции. **`ChecksAcceptIn`** `{comment}`.
**`CommentIn`** `{text}`. **`CommentOut`** `{id, text, author: {id, full_name}, created_at}`. **`UserBriefOut`** `{id, full_name,
is_active}`.

View File

@ -377,13 +377,19 @@ UI прячет элементы по `GET /me → permissions` (объедин
| Метод и путь | Право | Запрос | Ответ | Ошибки |
|---|---|---|---|---|
| `GET /documents/{document_id}/statement` | VIEW | — | `StatementOut` | 409 `DOCUMENT_NOT_STATEMENT` (ОСВ, расшифровка, ББ/ОФР без данных формы) |
| `PATCH /documents/{document_id}/statement` | EDIT | `{header?: {inn?, report_date?, units?}, values?: [{line_id, column_id, amount}]}` (до 5000 ячеек) | `StatementOut` — состояние после правки | 409 `FILE_SUPERSEDED`, `DOCUMENT_READONLY` (уже `CONFIRMED`), `DOCUMENT_NOT_CONFIRMABLE` (статус `ERROR`), `DOCUMENT_NOT_STATEMENT`; 422 `INVALID_INN`, `UNKNOWN_CELL`, `DUPLICATE_CELL`; 422 массивом |
| `PATCH /documents/{document_id}/statement` | EDIT | `{header?: {inn?, report_date?, units?}, values?: [{line_id | line_code, column_id, amount}]}` (до 5000 ячеек; для достроенной строки с `id = null` — `line_code`) | `StatementOut` — состояние после правки | 409 `FILE_SUPERSEDED`, `DOCUMENT_READONLY` (уже `CONFIRMED`), `DOCUMENT_NOT_CONFIRMABLE` (статус `ERROR`), `DOCUMENT_NOT_STATEMENT`; 422 `INVALID_INN`, `UNKNOWN_CELL`, `DUPLICATE_CELL`; 422 массивом |
| `POST /documents/{document_id}/statement/checks/accept` | EDIT | `{comment}` (1–2000) | `StatementOut` | 409 `NOTHING_TO_ACCEPT` и те же 409 |
Главное (подробно — `api-contract-slice-3b.md` §3–§5):
- `StatementOut = {document, edition, header, columns[], lines[], checks}`. У **каждой** пары «строка × колонка» есть `ValueOut`
со своим `id` — для правки передавайте `line_id` и `column_id` существующей ячейки, создавать значения не нужно.
- `StatementOut = {document, edition, header, columns[], lines[], checks}`. У **каждой** пары «строка × колонка» есть `ValueOut`.
Ядро достраивает полный состав формы редакции (справочник `statement_lines.yaml`, arm-core#80): `lines[].origin` —
`source` (из источника) или `template` (достроена); у достроенных строк и ячеек `id = null`, в БД их нет. Для правки
передавайте `{line_id | line_code, column_id, amount}`: у существующей строки — `line_id`; у достроенной (`id = null`) —
`line_code`, первая непустая сумма создаёт строку и ячейку (`is_modified = true`). Очистка (`amount: null`) возвращает
созданную ячейку в пустую. `UNKNOWN_CELL` — и новые случаи: `line_code` уже есть в форме (передайте `line_id`), код вне
справочника редакции, у документа нет редакции (нет даты периода — строки не достраиваются). Пустые достроенные строки
не участвуют в проверках §7 и статусе.
- Подсветка: `confidence < 100`, `is_recognized = false`, `is_modified = true` (исходное — `original_amount`). Возврат ячейки
к исходному значению снимает `is_modified`.
- Правка даты отчёта пересчитывает `document.period_*`, `reporting_date` и `edition`; правка ИНН — `counterparty_check`;
@ -592,7 +598,7 @@ async function download(path: string, fallbackName: string): Promise<void> {
| `CHECKS_NOT_PASSED` | 409 | подтверждение формы | `failures[] {column_index, relation, expected, actual}` | показать нарушения, предложить акцепт |
| `DOCUMENT_NOT_STATEMENT` | 409 | `…/statement` не по форме ББ/ОФР | — | не открывать экран [41] для такого документа |
| `DOCUMENT_READONLY` | 409 | правка/акцепт подтверждённой формы | — | перевести экран в просмотр |
| `UNKNOWN_CELL`, `DUPLICATE_CELL` | 422 | правка формы | — | ошибка клиента (перечитать форму) |
| `UNKNOWN_CELL`, `DUPLICATE_CELL` | 422 | правка формы | — | ошибка клиента (перечитать форму); `UNKNOWN_CELL` также: `line_code` уже есть в форме (слать `line_id`), код вне справочника редакции, нет редакции |
| `NOTHING_TO_ACCEPT` | 409 | акцепт без нарушений | — | перечитать форму |
| `FILE_HAS_CONFIRMED_DOCUMENTS`, `FILE_SUPERSEDES_OTHERS` | 409 | удаление файла | — | скрывать «Удалить» у таких файлов (есть документ, подтверждённый пользователем, или автоподтверждённый документ в версии вне корректировки данных (не `INITIATED`/`DOCUMENTS_PROCESSING`/`DATA_CORRECTION`), см. §3.7; или на файл ссылаются `superseded_by_file_id`) |
| `FILE_CONTENT_NOT_FOUND` | 404 | первичка | — | «Файл больше недоступен» |

View File

@ -540,6 +540,8 @@ curl -s "localhost:8000/api/v1/documents/$DOC/statement" -H "$H" | python3 -m js
| F6 | ka_go | снова `PATCH` той же ячейки другой суммой | 200; `checks.accepted = null` (акцепт сброшен правкой) |
| F7 | ka_go | `PATCH` той же ячейки на верную сумму (`1100 + 1200` по строкам 1100 и 1200 этой формы) | 200; `checks.failed` пуст; `document.status = RECOGNIZED` (ручное подтверждение — F11); `original_amount` остаётся сломанной суммой из копии, `is_modified = true`; `PATCH` с пустым телом `{}` — 200, новых записей `RECOGNIZED_DATA_EDIT` нет |
| F8 | ka_go | `PATCH` `{"header":{"inn":"123"}}` и `{"header":{"inn":null}}`; `{"values":[{"line_id":"<случайный uuid>","column_id":"<случайный uuid>","amount":"1"}]}` | 422 `INVALID_INN` (оба случая); 422 `UNKNOWN_CELL`; данные не изменились |
| F8a | ka_go | `PATCH` `{"values":[{"line_code":"1105","column_id":"<колонка 0>","amount":"1"}]}` (код 1105 в форме уже есть — взять из F2); затем `line_code` вне справочника (`"9999"`) | 422 `UNKNOWN_CELL` (оба случая); данные не изменились |
| F8b | ka_go | Новая строка: удалить строку 1700 нельзя через API — проверить на документе без 1700 (РБ 1 кв. 2025 до OQ-156 или вручную): `GET …/statement` — строка 1700 есть, `origin = template`, `id = null`; `PATCH` `line_code = "1700"` с суммой | 200; строка получила `id`, `line_code_original_value = null`, проверки пересчитаны; очистка (`amount: null`) — строка снова пустая, нарушений по ней нет |
| F9 | ka_go | `PATCH` `{"header":{"report_date":"2025-06-30"}}`, затем вернуть `"2025-09-30"` | период пересчитывается: `period_quarter = Q2` (`reporting_date = 2025-06-30`), `header.report_date.modified = true`, `original_value` не меняется (после возврата на `2025-09-30` — `Q3`, `modified` остаётся `true`); дата не на конец квартала (`2025-09-15`) — 200, `period_quarter = null`, статус `NEEDS_CHECK`, `POST …/confirm` — 409 `PERIOD_REQUIRED` (поправьте дату — подтверждение проходит) |
| F10 | ka_go | `POST …/accept` без нарушений | 409 `NOTHING_TO_ACCEPT` |
| F11 | ka_go | `POST …/confirm` для формы в `RECOGNIZED` после F7 и F9 (ручное подтверждение) | 200 `DocumentOut`, `status = CONFIRMED`, `confirmed_by`/`confirmed_at`; повтор — 409 `ALREADY_CONFIRMED`; `PATCH`/`accept` — 409 `DOCUMENT_READONLY`; в БД у шапки `reviewed_by/at` заполнены. Для автоподтверждённой чистой формы (F1, до удаления) `POST …/confirm` — 409 `ALREADY_CONFIRMED`, `confirmed_by` пуст |
@ -792,6 +794,8 @@ PDF из `fixtures/ocr` — отчётность других компаний (
| W26 | В шапке «Дата отчёта» ввести `2025-09-15` → «✓ Сохранить» → «✓ Подтвердить данные» | красный блок «<текст сервера>. Исправьте дату отчёта в шапке и сохраните.» (409 `PERIOD_REQUIRED`: дата не на конец квартала, F9). Дата вводится строго `ГГГГ-ММ-ДД` |
| W27 | Дата `2025-06-30` → «✓ Сохранить» → «✓ Подтвердить данные» | уведомление «Документ подтверждён», статус «Загружен», «Редактировать» и «Подтвердить данные» неактивны |
| W28 | Открыть «ОФР» того же файла [41-02] | заголовок «Отчёт о финансовых результатах», колонки периодов ОФР; остальное — как W20 |
| W28a | В «Бух. балансе» посмотреть строки 1600 и 1700 | строки выделены фиолетовым фоном и жирным шрифтом |
| W28b | «✎ Редактировать» на форме, где есть достроенные (в источнике отсутствующие) строки | пустые достроенные строки видны, редактируются и не подсвечены как ошибки; ввод суммы → «✓ Сохранить» создаёт строку, проверки пересчитываются |
Формы из Excel (`fixtures/statements-xlsx`, раздел 6.9) открываются тем же экраном, но слева вместо скана — «Превью
недоступно для этого формата» и кнопка «Скачать исходник». Документы «Прочий документ» (расшифровки) в панели не