diff --git a/CLAUDE.md b/CLAUDE.md index 644c08e..5fe14f9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -98,7 +98,10 @@ документа. **Порядок блокировок «файл → документ»**, шапка (`meta`) читается ПОД блокировкой с `refresh=True` — иначе в сессии остаётся устаревший акцепт. Подтверждение формы (`services/documents._check_statement`) требует период (`PERIOD_REQUIRED`) и пройденные/акцептованные жёсткие проверки (`CHECKS_NOT_PASSED`, `detail.failures`); мягкие (в т. ч. межформенная 1370↔2400) - не блокируют. Комментарии к документу — `services/documents.py` (`comments`/`add_comment`, `comment(object_type='document')`, + не блокируют. Полный состав формы — `domain/statement_lines.py` по справочнику `api/rules/statement_lines.yaml` (редакции `66n`/`fsbu4` + из шаблонов банка; копия словаря scan-gateway с ним не совпадает): `GET …/statement` достраивает строки и ячейки с + `id = null`, `PATCH` по `line_code` создаёт строку (`line_code_original_value = NULL` — «не из источника»); пустые + созданные строки не участвуют в §7 (`statement_amounts`). Решения — `openspec/changes/statement-template-lines/design.md`. Комментарии к документу — `services/documents.py` (`comments`/`add_comment`, `comment(object_type='document')`, без аудита; `comments_count` в карточках). `GET /files/{id}/content` — `services/primary_files.content` + `ObjectStorage.stream` (поток из S3, первая порция до заголовков → `FILE_CONTENT_NOT_FOUND`; сессию тело потока не держит; имя — RFC 5987, `X-Content-Type-Options: nosniff`; освобождение потока — `common/streaming.py`: `ClosingStreamingResponse` + diff --git a/README.md b/README.md index d7cf77a..1bd018f 100644 --- a/README.md +++ b/README.md @@ -568,7 +568,7 @@ S3 (storage); без S3 — предупреждение в логе, цикл (`organization_name`, `okud`, `okpo`, …); `lines[].values[]` — суммы строкой, `is_modified`, `original_amount`; `checks` — `{failed, soft_failed, cross_form, accepted}` (нарушение — `{column_index, relation, expected, actual}`, у межформенной проверки `column_index = -1`). Не форма (ОСВ, расшифровка, ББ/ОФР без данных формы — созданные до Среза 3b) — 409 `DOCUMENT_NOT_STATEMENT`. - Подтверждённая форма читается. + Подтверждённая форма читается. Форма достраивается по справочнику строк редакции (`api/rules/statement_lines.yaml`): недостающие строки (`id = null`, `origin = template`) и ячейки (`id = null`); в БД не пишется. - **`PATCH /documents/{id}/statement`** — `{header?: {inn?, report_date?, units?}, values?: [{line_id, column_id, amount}]}` (до 5000 ячеек; `amount: null` очищает ячейку; не более двух знаков; `units` — непустая строка после обрезки пробелов до 50 символов, пробельная строка — 422, `null` — «не менять»; `inn: null` — 422 `INVALID_INN`, ИНН не очищается). Правится только неподтверждённая форма @@ -577,6 +577,7 @@ S3 (storage); без S3 — предупреждение в логе, цикл одна ячейка дважды — 422 `DUPLICATE_CELL`, ИНН не из 10 или 12 цифр — 422 `INVALID_INN`. Один запрос — одна транзакция и одно событие `RECOGNIZED_DATA_EDIT` с `before/after`; возврат ячейки к исходному значению снимает `is_modified`; правка ИНН пересчитывает `counterparty_check`, даты отчёта — период документа; любая правка сбрасывает акцепт и пересчитывает статус. + `values[]` адресует ячейку `line_id` или `line_code` (ровно одно): ячейка, которой нет, создаётся при первой непустой правке; строка по `line_code` — только код справочника редакции, которого нет в форме. Запрос без изменений — 200 без аудита. Ответ — та же форма, что у `GET`. - **`POST /documents/{id}/statement/checks/accept`** — `{comment}` (1–2000 символов): акцепт нарушенных жёстких проверок (`checks_accepted_by/at/comment`). Нарушений нет — 409 `NOTHING_TO_ACCEPT`. Акцепт сбрасывается любой правкой формы (в `before_value` события аудита сброса остаётся комментарий сброшенного акцепта). @@ -855,7 +856,7 @@ email без учёта регистра проверяет только сер | `FILE_CONTENT_NOT_FOUND` | 404 | `GET /files/{id}/content`: файл есть, объекта нет в S3 (истёк или пропал) | | `DOCUMENT_NOT_STATEMENT` | 409 | `…/statement`: документ — не форма ББ/ОФР (ОСВ, расшифровка, ББ/ОФР без данных формы — созданные до Среза 3b) | | `DOCUMENT_READONLY` | 409 | правка/акцепт формы: форма уже `CONFIRMED` (исправление — новым файлом) | -| `UNKNOWN_CELL` | 422 | `PATCH …/statement`: `line_id`/`column_id` не принадлежат этой форме | +| UNKNOWN_CELL | 422 | PATCH …/statement: line_id/column_id не этой формы; line_code уже есть в форме (передайте line_id), нет в справочнике редакции или редакция не определена | | `DUPLICATE_CELL` | 422 | `PATCH …/statement`: одна ячейка указана в запросе дважды | | `NOTHING_TO_ACCEPT` | 409 | акцепт: непройденных жёстких проверок нет (в том числе уже акцептованы) | | `PERIOD_REQUIRED` | 409 | подтверждение формы: период не определён — поправьте дату отчёта | diff --git a/openspec/changes/statement-template-lines/design.md b/openspec/changes/statement-template-lines/design.md index 560ad7f..c90cfd2 100644 --- a/openspec/changes/statement-template-lines/design.md +++ b/openspec/changes/statement-template-lines/design.md @@ -71,7 +71,7 @@ pydantic: коды — четыре цифры, без повторов внут есть: справочник только добавляет, ничего не убирает и не переименовывает. ### D-3. Достройка ответа -`complete_form(form_type, edition, columns, lines) -> lines'`: +Достройка (по факту — `_lines_out` в `schemas/statements.py`, R-9; логика та же): - для каждой строки из БД — недостающие ячейки по всем `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` из @@ -113,7 +113,7 @@ pydantic: коды — четыре цифры, без повторов внут `is_recognized = NULL` и `confidence = NULL` — «без сомнений», правленные — `is_modified`. ### D-6. Фронт (arm-web, PR в `test`) -`statementGrid`: ключ строки — `line.id ?? "code:" + line_code`, ключ ячейки — `rowKey:column_id`; `draftToPatch` +`statementGrid`: ключ строки — `line.id ?? "code-" + line_code` (R-10), ключ ячейки — `rowKey:column_id`; `draftToPatch` отправляет `line_id` или `line_code` по ключу. Достроенная пустая ячейка (`id = null`) без подсветки «требует проверки» (`is_recognized = null`), введённое значение подсвечивается как правка. `formLayout` уже раскладывает строки по разделам — меняется только то, что разделы теперь всегда полные. **Строки «БАЛАНС» (1600 по активу, 1700 по пассиву) выделяются @@ -146,3 +146,6 @@ pydantic: коды — четыре цифры, без повторов внут | R-6 | 09.10.2026 | Ячейка, которой нет у существующей строки, создаётся при правке по `line_id` | Та же проблема, что с отсутствующей строкой; отдельного API не требует | — | | R-7 | 09.10.2026 | Цвет строк «БАЛАНС» — светло-фиолетовый `#f9f0ff` + жирный 700; подсветка ячейки перекрывает фон строки; ОФР не выделяется | Владелец просил выделить баланс по активу и пассиву, цвет не задал; в макете [41] строки «БАЛАНС» не видно; фиолетовый не занят: серый — разделы, синий — «исправлено», оранжевый — «требует проверки» | Цвет не понравится — правка одной константы | | R-8 | 09.10.2026 | `has_lines` (условие D-6 «в форме есть строки») учитывает и строки, созданные правкой | Форму, у которой источник не дал строк (не найдена колонка кодов), аналитик может заполнить вручную — это его данные, как любая правка; статус всё равно требует пройденных проверок и завершённого задания | Документ без строк источника станет `RECOGNIZED` после ручного заполнения — ожидаемо | +| R-9 | 09.10.2026 | Достройка формы реализована функцией `_lines_out` в `schemas/statements.py`, а не `domain/statement_lines.complete_form` из D-3 | План конкретнее, поведение то же | Рефакторинг в domain позже | +| R-10 | 09.10.2026 | Ключ достроенной строки на фронте `code-`, а не `code:` из D-6 | ':' разделяет rowKey и columnId в cellKey | Нет | +| R-11 | 09.10.2026 | Адресация ячеек PATCH вынесена из `_apply_cells` в `_resolve_cells` (отклонение от кода плана ради ruff PLR0912/PLW2901); поведение и порядок проверок сохранены | Линтер | Нет |