docs: fix round 1 — автоподтверждение: §9.1/§9.3/§9.5, OQ-157, гайды QA и фронта

Правки по ревью: цепочка F3–F10 на форме с нарушением (чистая CONFIRMED),
F11 — ручное подтверждение после правки, DISCLOSURE/OTHER автоподтверждаются,
удаление — условия вне корректировки данных, W17 — файл вне демо-периметра.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-08 16:59:21 +04:00
parent 9e0115c2c7
commit e975f56386
5 changed files with 21 additions and 19 deletions

View File

@ -573,13 +573,13 @@ NEEDS_CHECK`; пройдено/акцептовано + нажато «Подт
(`current_recognition_job_id`): `PROCESSING` — ack сервиса (или результат `done`/`needs_check`, если ack потерян); (`current_recognition_job_id`): `PROCESSING` — ack сервиса (или результат `done`/`needs_check`, если ack потерян);
`ERROR` — задание `error`/`timeout`/`dlq` либо ошибка маппинга; `PROCESSED` — маппинг завершён; `SUPERSEDED` — автозамена. `ERROR` — задание `error`/`timeout`/`dlq` либо ошибка маппинга; `PROCESSED` — маппинг завершён; `SUPERSEDED` — автозамена.
`PROCESSED` и `SUPERSEDED` событиями обмена не перезаписываются. Физическое удаление: ANL-020 запрещает его после `PROCESSED` и `SUPERSEDED` событиями обмена не перезаписываются. Физическое удаление: ANL-020 запрещает его после
INITIATED; в Срезе 2c консолидаций ещё нет, поэтому удаление разрешено, кроме файла с `CONFIRMED` документом и файла, INITIATED; в Срезе 2c консолидаций ещё нет, поэтому удаление разрешено, кроме файла с документом, подтверждённым пользователем, автоподтверждённого документа в версии вне INITIATED/DOCUMENTS_PROCESSING/DATA_CORRECTION и файла,
заменившего другие (§9.5); поведение после появления консолидаций — OQ-110. заменившего другие (§9.5); поведение после появления консолидаций — OQ-110.
### 9.3 Документ (`document_status`): создаётся ядром при маппинге результата, по одному на лист ОСВ: ### 9.3 Документ (`document_status`): создаётся ядром при маппинге результата, по одному на лист ОСВ:
`RECOGNIZED` — задание `done`, `checks` парсера без нарушений, `counterparty_check ≠ MISMATCH` и нет предупреждений `RECOGNIZED` — задание `done`, `checks` парсера без нарушений, `counterparty_check ≠ MISMATCH` и нет предупреждений
маппинга **этого листа** (повтор счёта в файле, строки без родителя, нарастающая по данным Q1); иначе `NEEDS_CHECK` маппинга **этого листа** (повтор счёта в файле, строки без родителя, нарастающая по данным Q1); иначе `NEEDS_CHECK`
(OQ-05 закрыт: `RECOGNIZED` нужен). **Автоподтверждение (OQ-157, решение владельца 08.10.2026; ТЗ 03_9 п. 4, 10_10):** документ, получивший при маппинге `RECOGNIZED`, в той же транзакции становится `CONFIRMED` от имени системы (`confirmed_by` NULL, `audit_event(DOCUMENT_CONFIRM)` с `user_id` NULL и описанием «автоподтверждение: без замечаний»), привязывается к версиям и двигает их, как при ручном подтверждении (§3 п. 1); переход версии — от `stored_file.uploaded_by` с комментарием «автоподтверждение документа …». Форма ББ/ОФР — только при условиях ручного подтверждения (п. 4); `RECOGNIZED` формы после правки или акцепта — только вручную. `NEEDS_CHECK` — только вручную; «Подтвердить данные» → `CONFIRMED`; `ERROR` — зарезервирован (в Срезе 2c ядро (OQ-05 закрыт: `RECOGNIZED` нужен). **Автоподтверждение (OQ-157, решение владельца 08.10.2026; ТЗ 03_9 п. 4, 10_10):** документ, получивший при маппинге `RECOGNIZED`, в той же транзакции становится `CONFIRMED` от имени системы (`confirmed_by` NULL, `audit_event(DOCUMENT_CONFIRM)` с `user_id` NULL и описанием «автоподтверждение: без замечаний»), привязывается к версиям и двигает их, как при ручном подтверждении (§3 п. 1); переход версии — от `stored_file.uploaded_by` с комментарием «автоподтверждение документа …». Форма ББ/ОФР — при тех же условиях, что ручное подтверждение (п. 4: период определён, нет непройденных жёстких проверок §7; мягкие не учитываются); `RECOGNIZED` формы после правки или акцепта — только вручную. `NEEDS_CHECK` — только вручную. `DISCLOSURE`/`OTHER` (без данных) подтверждаются автоматически тоже (в версии не привязываются; удаление — по §9.5 п. 5). «Подтвердить данные» → `CONFIRMED`; `ERROR` — зарезервирован (в Срезе 2c ядро
документ в `ERROR` не создаёт: файл без разбираемой ОСВ получает `ERROR` на уровне файла). `counterparty_check`: ИНН шапки документ в `ERROR` не создаёт: файл без разбираемой ОСВ получает `ERROR` на уровне файла). `counterparty_check`: ИНН шапки
равен ИНН владельца файла → `MATCH`; ИНН нет — нормализованное имя шапки совпало с именем или алиасом владельца → `MATCH`; равен ИНН владельца файла → `MATCH`; ИНН нет — нормализованное имя шапки совпало с именем или алиасом владельца → `MATCH`;
иначе `MISMATCH`; пустая шапка → `UNKNOWN`. `period_check` — `UNKNOWN`: подходящесть документа версии — функция (документ, версия), а один документ входит в несколько версий (§3 п. 1, Срез 4; OQ-120). иначе `MISMATCH`; пустая шапка → `UNKNOWN`. `period_check` — `UNKNOWN`: подходящесть документа версии — функция (документ, версия), а один документ входит в несколько версий (§3 п. 1, Срез 4; OQ-120).
@ -591,7 +591,7 @@ INITIATED; в Срезе 2c консолидаций ещё нет, поэтом
`NEEDS_CHECK`. Статус пересчитывается при маппинге, после каждой правки формы и после акцепта (кроме `CONFIRMED`); при `NEEDS_CHECK`. Статус пересчитывается при маппинге, после каждой правки формы и после акцепта (кроме `CONFIRMED`); при
пересчёте после правки/акцепта предупреждения маппинга не учитываются — на уровне документа они не хранятся пересчёте после правки/акцепта предупреждения маппинга не учитываются — на уровне документа они не хранятся
(`metrics.mapping_warnings` — на уровне файла). Расшифровки (`DISCLOSURE`) и прочие (`OTHER`) — один документ на тип на (`metrics.mapping_warnings` — на уровне файла). Расшифровки (`DISCLOSURE`) и прочие (`OTHER`) — один документ на тип на
файл без данных, статус `RECOGNIZED` (OQ-23). Документ ББ/ОФР, созданный до Среза 3b и не имеющий данных формы файл без данных, статус `RECOGNIZED` при маппинге, затем автоподтверждается → `CONFIRMED` (OQ-23, OQ-157). Документ ББ/ОФР, созданный до Среза 3b и не имеющий данных формы
(`financial_statement_meta` нет), подтверждается без условий п. 9.5.4, а `GET`/`PATCH …/statement` по нему — 409 (`financial_statement_meta` нет), подтверждается без условий п. 9.5.4, а `GET`/`PATCH …/statement` по нему — 409
`DOCUMENT_NOT_STATEMENT`. `DOCUMENT_NOT_STATEMENT`.
@ -658,11 +658,11 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
уверенность значений подтверждение **не блокируют** (как у ОСВ): защита — статус `NEEDS_CHECK` и подсветка. Мягкие проверки уверенность значений подтверждение **не блокируют** (как у ОСВ): защита — статус `NEEDS_CHECK` и подсветка. Мягкие проверки
(в том числе межформенная) не блокируют. `DISCLOSURE`/`OTHER` — без условий. Привязка к версиям и переходы статуса версии — (в том числе межформенная) не блокируют. `DISCLOSURE`/`OTHER` — без условий. Привязка к версиям и переходы статуса версии —
как у ОСВ (§3 п. 1, §9.1): ББ/ОФР за квартал версии (`Q1` — только версии за `Q1`, R-14a Среза 4). как у ОСВ (§3 п. 1, §9.1): ББ/ОФР за квартал версии (`Q1` — только версии за `Q1`, R-14a Среза 4).
5. **Удаление** (`DELETE /files/{id}`, `DOCUMENT_EDIT`): запрещено, если у файла есть документ, подтверждённый пользователем (`confirmed_by` не NULL), или автоподтверждённый документ привязан к версии вне `INITIATED`/`DOCUMENTS_PROCESSING`/`DATA_CORRECTION` (`FILE_HAS_CONFIRMED_DOCUMENTS`; OQ-157, уточняет OQ-110); привязки автоподтверждённых документов снимаются, статус версий не меняется, `FILE_DELETE.after_value.unbound_version_ids` и если файл заменил другие (`FILE_SUPERSEDES_OTHERS`); `SUPERSEDED` файл удалить 5. **Удаление** (`DELETE /files/{id}`, `DOCUMENT_EDIT`): запрещено, если у файла есть документ, подтверждённый пользователем (`confirmed_by` не NULL), или автоподтверждённый документ привязан к версии вне `INITIATED`/`DOCUMENTS_PROCESSING`/`DATA_CORRECTION` (`FILE_HAS_CONFIRMED_DOCUMENTS`; OQ-157, уточняет OQ-110); привязки автоподтверждённых документов снимаются, статус версий не меняется, `FILE_DELETE.after_value.unbound_version_ids`; запрещено также, если файл заменил другие (`FILE_SUPERSEDES_OTHERS`); `SUPERSEDED` файл удалить
можно. Удаляются строки, документы (вместе с данными форм `financial_statement_*` и комментариями документов), задания и `stored_file`; объектам S3 ставится `expires_at = now()` (физически не можно. Удаляются строки, документы (вместе с данными форм `financial_statement_*` и комментариями документов), задания и `stored_file`; объектам S3 ставится `expires_at = now()` (физически не
стираются — OQ-111); аудит `FILE_DELETE` с карточкой файла в `before_value`. **OQ-110 закрыт для Среза 4:** к версии стираются — OQ-111); аудит `FILE_DELETE` с карточкой файла в `before_value`. **OQ-110 закрыт для Среза 4:** к версии
привязываются только `CONFIRMED` документы, а файл с таким документом удалить нельзя — удалить файл с документом в привязываются только `CONFIRMED` документы, а файл с таким документом удалить нельзя — удалить файл с документом в
версии невозможно; расширять удаление не требуется. версии невозможно; расширять удаление не требуется (уточнено OQ-157: для автоподтверждённых документов — см. выше).
6. **Аудит.** `FILE_UPLOAD`, `FILE_REPLACE`, `FILE_DELETE`, `DOCUMENT_CONFIRM` (раздел `PRIMARY_DOCUMENTS`); правка формы и 6. **Аудит.** `FILE_UPLOAD`, `FILE_REPLACE`, `FILE_DELETE`, `DOCUMENT_CONFIRM` (раздел `PRIMARY_DOCUMENTS`); правка формы и
акцепт проверок — `RECOGNIZED_DATA_EDIT` (раздел `RECOGNITION`, `object_type = document`, `before/after` изменённых полей и акцепт проверок — `RECOGNIZED_DATA_EDIT` (раздел `RECOGNITION`, `object_type = document`, `before/after` изменённых полей и
ячеек); смены статусов задания/файла в обмене и в цикле маппинга `audit_event` не пишут (OQ-55). Не аудируются (Срез 3b): ячеек); смены статусов задания/файла в обмене и в цикле маппинга `audit_event` не пишут (OQ-55). Не аудируются (Срез 3b):

View File

@ -10,7 +10,7 @@
| OQ-02 | ОСВ «за 2/3/4 квартал» — квартальная или нарастающая? Как определять автоматически | FNR01 маппинг периода | срез 2, §2 правил | **закрыт решением владельца 05.10.2026:** ОСВ «за N квартал» — **квартальная**; признак сверяется по данным: если есть ОСВ Q1 того же года и `opening` совпадает — нарастающая (парсер ставит `is_cumulative` по тексту, ядро переопределяет при маппинге/в консолидации); **допущение парсера (Срез 2a):** `period_quarter` и `is_cumulative` вычисляются только по тексту периода (`rules/periods.yaml`; Q2–Q4 «N квартал» → `false`), сверка по данным — в ядре, подсрез 2c (`ASSUMPTION(OQ-02)`) | | OQ-02 | ОСВ «за 2/3/4 квартал» — квартальная или нарастающая? Как определять автоматически | FNR01 маппинг периода | срез 2, §2 правил | **закрыт решением владельца 05.10.2026:** ОСВ «за N квартал» — **квартальная**; признак сверяется по данным: если есть ОСВ Q1 того же года и `opening` совпадает — нарастающая (парсер ставит `is_cumulative` по тексту, ядро переопределяет при маппинге/в консолидации); **допущение парсера (Срез 2a):** `period_quarter` и `is_cumulative` вычисляются только по тексту периода (`rules/periods.yaml`; Q2–Q4 «N квартал» → `false`), сверка по данным — в ядре, подсрез 2c (`ASSUMPTION(OQ-02)`) |
| OQ-03 | Приведение квартальных ОСВ к нарастающим: ключ соединения (контрагент vs договор), пропуск промежуточного квартала | DOC-259006 | срез 4 | **закрыт решением владельца 06.10.2026 (Срез 4):** ключ склейки — (счёт, контрагент по сырому имени и ИНН), договоры внутри контрагента (строка COUNTERPARTY уже содержит их сумму); пропуск квартала — **пробел** по паре (ЮЛ, счёт), а не блокирующая ошибка: строк пары в своде нет, пробел отдаётся в `gaps[]` сводки и в листе «Пробелы» экспорта; квартал без строки ключа = нули; дубль ключа внутри квартала суммируется. Правило — `05-domain-rules.md` §2; код — `domain/consolidation_periods.select_sources`, `domain/consolidated_osv.build` | | OQ-03 | Приведение квартальных ОСВ к нарастающим: ключ соединения (контрагент vs договор), пропуск промежуточного квартала | DOC-259006 | срез 4 | **закрыт решением владельца 06.10.2026 (Срез 4):** ключ склейки — (счёт, контрагент по сырому имени и ИНН), договоры внутри контрагента (строка COUNTERPARTY уже содержит их сумму); пропуск квартала — **пробел** по паре (ЮЛ, счёт), а не блокирующая ошибка: строк пары в своде нет, пробел отдаётся в `gaps[]` сводки и в листе «Пробелы» экспорта; квартал без строки ключа = нули; дубль ключа внутри квартала суммируется. Правило — `05-domain-rules.md` §2; код — `domain/consolidation_periods.select_sources`, `domain/consolidated_osv.build` |
| OQ-04 | ОСВ по субсчёту (58.03 вместо 58) закрывает ли обязательный документ «ОСВ 58» | фикстуры | чек-лист | **закрыт решением владельца 05.10.2026:** **нет**, ОСВ по субсчёту (58.03) — только субсчёт, обязательный документ «ОСВ 58» закрывает только полная ОСВ по счёту 58; чек-лист считает документ выполненным при `osv_account_code = код правила` | | OQ-04 | ОСВ по субсчёту (58.03 вместо 58) закрывает ли обязательный документ «ОСВ 58» | фикстуры | чек-лист | **закрыт решением владельца 05.10.2026:** **нет**, ОСВ по субсчёту (58.03) — только субсчёт, обязательный документ «ОСВ 58» закрывает только полная ОСВ по счёту 58; чек-лист считает документ выполненным при `osv_account_code = код правила` |
| OQ-05 | Нужен ли статус `document.RECOGNIZED` или все идут через `NEEDS_CHECK → CONFIRMED` | DBML | срез 2–3 | **закрыт решением владельца 05.10.2026:** статус `RECOGNIZED` **нужен**: документ после успешного распознавания — `RECOGNIZED`, при нарушениях — `NEEDS_CHECK`, после подтверждения аналитиком — `CONFIRMED` Пересмотрено OQ-157 (08.10.2026): после маппинга чистый документ сразу `CONFIRMED`; `RECOGNIZED` остаётся у старых документов и форм после правки. | | OQ-05 | Нужен ли статус `document.RECOGNIZED` или все идут через `NEEDS_CHECK → CONFIRMED` | DBML | срез 2–3 | **закрыт решением владельца 05.10.2026:** статус `RECOGNIZED` **нужен**: документ после успешного распознавания — `RECOGNIZED`, при нарушениях — `NEEDS_CHECK`, после подтверждения аналитиком — `CONFIRMED`. Пересмотрено OQ-157 (08.10.2026): после маппинга чистый документ сразу `CONFIRMED`; `RECOGNIZED` остаётся у старых документов и форм после правки. |
| OQ-06 | Замена одного документа внутри многодокументного файла | DBML | срез 3 | решено: замена файлом целиком | | OQ-06 | Замена одного документа внутри многодокументного файла | DBML | срез 3 | решено: замена файлом целиком |
| OQ-07 | Матчинг ВГО: одна строка = одна позиция (DOC-223044) или агрегировать по (компания, контрагент, счёт) (FNR03)? | DOC-223044 vs FNR03 | срез 5 | допущение: агрегировать по (компания, контрагент, счёт, сторона) с детализацией по `osv_entry.id` | | OQ-07 | Матчинг ВГО: одна строка = одна позиция (DOC-223044) или агрегировать по (компания, контрагент, счёт) (FNR03)? | DOC-223044 vs FNR03 | срез 5 | допущение: агрегировать по (компания, контрагент, счёт, сторона) с детализацией по `osv_entry.id` |
| OQ-08 | Знак порога 5 % в правиле пары (≤ или <), и к чему считать % — к меньшей сумме (Методика сноска 14) | FNR03 (знак потерян) | срез 5 | допущение: `≤ 5 %` к меньшей | | OQ-08 | Знак порога 5 % в правиле пары (≤ или <), и к чему считать % — к меньшей сумме (Методика сноска 14) | FNR03 (знак потерян) | срез 5 | допущение: `≤ 5 %` к меньшей |
@ -115,7 +115,7 @@
| OQ-107 | При `hierarchy=sums` (дерево восстановлено по равенству «сумма детей = родитель») сверка «сумма детей = родитель» тавтологична — она не может не сойтись. Считать ли такой разбор `needs_check` (или отдельное предупреждение достаточно)? | tabular-parser: финальное ревью Среза 2a, M-5 | срез 2a+ (статус результата), срез 2c (UI проверки) | допущение принято: статус — по контрольным суммам, стратегия видна в `metrics.warnings` (`hierarchy=sums`); на реальных файлах стратегия не выбирается (все — `outline`/`trivial`) | | OQ-107 | При `hierarchy=sums` (дерево восстановлено по равенству «сумма детей = родитель») сверка «сумма детей = родитель» тавтологична — она не может не сойтись. Считать ли такой разбор `needs_check` (или отдельное предупреждение достаточно)? | tabular-parser: финальное ревью Среза 2a, M-5 | срез 2a+ (статус результата), срез 2c (UI проверки) | допущение принято: статус — по контрольным суммам, стратегия видна в `metrics.warnings` (`hierarchy=sums`); на реальных файлах стратегия не выбирается (все — `outline`/`trivial`) |
| OQ-108 | Защита фазы чтения xlsx от XML-бомб (entity expansion) — `defusedxml` как новая зависимость сервиса — и иные лимиты памяти чтения. Остаточные риски после R-23/R-25: (1) бомба по объёму самих ячеек в пределах лимита распакованного zip — xlsx в 5 МБ с 2 млн обычных ячеек читается openpyxl 13 с и занимает 1,5 ГБ (замер ревьюера); (2) объём `sharedStrings` и стилей в тех же пределах; (3) entity expansion (от экспоненты «billion laughs» защищает только сам expat ≥ 2.4); (4) **известный обход предсканирования (R-26):** лист в части без `.xml` с ≥ 64 байтами ведущих пробелов и без XML-декларации не попадает в скан (файл 1,8 КБ → 27 с / 1,4 ГБ в openpyxl) — первый follow-up: убрать «sniff» и отдавать expat каждую некаталожную запись; (5) скан сам не ограничен размером чанка: «мусорная» XML-часть с атрибутом в 400 МБ → 1,3 ГБ / 1,2 с, 400 МБ тегов → 8,5 с CPU; (6) xls с очень многими крупными merged-диапазонами (> 10 000 ячеек каждый) — линейный просмотр индекса `Sheet` (≈ 4,4 мс на обращение), останавливает только `PARSE_TIMEOUT_SECONDS`: потолок на число таких диапазонов. Предскан — эшелон защиты, не граница безопасности; настоящая граница — лимит памяти контейнера / подпроцесс с RLIMIT. Рекомендация: жёсткий лимит памяти контейнера сервиса; и/или предпроверка объявленного `<dimension>` и потоковый счётчик ячеек `<c>`/строк `sharedStrings` (тем же expat-сканом, что объединения) с лимитом; и/или разбор книги в подпроцессе с `RLIMIT_AS`/таймаутом, который можно убить. Решение платформы | tabular-parser: финальное ревью Среза 2a, I-1 (хвост), R-23, R-25 | tabular-parser (readers), платформа (зависимости и лимиты образа/пода) | допущение принято: действуют лимиты R-23/R-25 — распакованный zip ≤ 10 × `PARSE_MAX_BYTES`, площадь объединённых ячеек книги ≤ 200 000 (потоковый expat-скан всех XML-частей до openpyxl); объём ячеек, `sharedStrings`/стили и entity expansion не ограничены (README «Известные ограничения») | | OQ-108 | Защита фазы чтения xlsx от XML-бомб (entity expansion) — `defusedxml` как новая зависимость сервиса — и иные лимиты памяти чтения. Остаточные риски после R-23/R-25: (1) бомба по объёму самих ячеек в пределах лимита распакованного zip — xlsx в 5 МБ с 2 млн обычных ячеек читается openpyxl 13 с и занимает 1,5 ГБ (замер ревьюера); (2) объём `sharedStrings` и стилей в тех же пределах; (3) entity expansion (от экспоненты «billion laughs» защищает только сам expat ≥ 2.4); (4) **известный обход предсканирования (R-26):** лист в части без `.xml` с ≥ 64 байтами ведущих пробелов и без XML-декларации не попадает в скан (файл 1,8 КБ → 27 с / 1,4 ГБ в openpyxl) — первый follow-up: убрать «sniff» и отдавать expat каждую некаталожную запись; (5) скан сам не ограничен размером чанка: «мусорная» XML-часть с атрибутом в 400 МБ → 1,3 ГБ / 1,2 с, 400 МБ тегов → 8,5 с CPU; (6) xls с очень многими крупными merged-диапазонами (> 10 000 ячеек каждый) — линейный просмотр индекса `Sheet` (≈ 4,4 мс на обращение), останавливает только `PARSE_TIMEOUT_SECONDS`: потолок на число таких диапазонов. Предскан — эшелон защиты, не граница безопасности; настоящая граница — лимит памяти контейнера / подпроцесс с RLIMIT. Рекомендация: жёсткий лимит памяти контейнера сервиса; и/или предпроверка объявленного `<dimension>` и потоковый счётчик ячеек `<c>`/строк `sharedStrings` (тем же expat-сканом, что объединения) с лимитом; и/или разбор книги в подпроцессе с `RLIMIT_AS`/таймаутом, который можно убить. Решение платформы | tabular-parser: финальное ревью Среза 2a, I-1 (хвост), R-23, R-25 | tabular-parser (readers), платформа (зависимости и лимиты образа/пода) | допущение принято: действуют лимиты R-23/R-25 — распакованный zip ≤ 10 × `PARSE_MAX_BYTES`, площадь объединённых ячеек книги ≤ 200 000 (потоковый expat-скан всех XML-частей до openpyxl); объём ячеек, `sharedStrings`/стили и entity expansion не ограничены (README «Известные ограничения») |
| OQ-109 | Воркер tabular-parser скачивает объект S3 целиком и только потом проверяет `PARSE_MAX_BYTES`; проверять размер по `stat` до скачивания | tabular-parser: финальное ревью Среза 2a, M-7 | tabular-parser (`core/worker.py`) — вне Среза 2a | **закрыт реализацией Среза 2c (06.10.2026, `arm-core/openspec/changes/slice-2c-osv-ingest`):** ядро ограничивает размер файла на загрузке (`UPLOAD_MAX_BYTES` = 50 МиБ, как `PARSE_MAX_BYTES` парсера), поэтому в воркер большие объекты из ядра не попадают; проверка по `stat` до скачивания в `tabular-parser` остаётся как есть (дефенсивная, не требуется) | | OQ-109 | Воркер tabular-parser скачивает объект S3 целиком и только потом проверяет `PARSE_MAX_BYTES`; проверять размер по `stat` до скачивания | tabular-parser: финальное ревью Среза 2a, M-7 | tabular-parser (`core/worker.py`) — вне Среза 2a | **закрыт реализацией Среза 2c (06.10.2026, `arm-core/openspec/changes/slice-2c-osv-ingest`):** ядро ограничивает размер файла на загрузке (`UPLOAD_MAX_BYTES` = 50 МиБ, как `PARSE_MAX_BYTES` парсера), поэтому в воркер большие объекты из ядра не попадают; проверка по `stat` до скачивания в `tabular-parser` остаётся как есть (дефенсивная, не требуется) |
| OQ-110 | Удаление файла после консолидаций: у `document` появятся FK из `financial_statement_meta`, `consolidation_document`, `ex_snapshot_item`, а `DELETE /files/{id}` Среза 2c их не чистит (в 2c строк в этих таблицах нет). ANL-020 запрещает физическое удаление после INITIATED. Что делать: запретить удаление файла, документ которого входит в версию консолидации, и/или каскадно чистить служебные таблицы (`financial_statement_meta` — в Срезе 3) | arm-core Срез 2c: Task 10 (R-23, design №36, №48) | Срезы 3, 4 (расширить удаление или запрет) | **закрыт для Среза 4 (06.10.2026):** привязываются к версии только `CONFIRMED` документы незаменённых файлов, а файл с `CONFIRMED` документом удалить нельзя (`FILE_HAS_CONFIRMED_DOCUMENTS`) — удаление файла с документом в версии невозможно, расширять `DELETE /files/{id}` не нужно; автозамена снимает привязки документов заменённого файла (`FILE_REPLACE.unbound_version_ids`). Остаток: FK из `financial_statement_meta` и `ex_snapshot_item` — Срезы 3 и 7 Уточнено OQ-157: файл с автоподтверждёнными документами удаляется, пока их версии «в работе»; привязки снимаются. | | OQ-110 | Удаление файла после консолидаций: у `document` появятся FK из `financial_statement_meta`, `consolidation_document`, `ex_snapshot_item`, а `DELETE /files/{id}` Среза 2c их не чистит (в 2c строк в этих таблицах нет). ANL-020 запрещает физическое удаление после INITIATED. Что делать: запретить удаление файла, документ которого входит в версию консолидации, и/или каскадно чистить служебные таблицы (`financial_statement_meta` — в Срезе 3) | arm-core Срез 2c: Task 10 (R-23, design №36, №48) | Срезы 3, 4 (расширить удаление или запрет) | **закрыт для Среза 4 (06.10.2026):** привязываются к версии только `CONFIRMED` документы незаменённых файлов, а файл с `CONFIRMED` документом удалить нельзя (`FILE_HAS_CONFIRMED_DOCUMENTS`) — удаление файла с документом в версии невозможно, расширять `DELETE /files/{id}` не нужно; автозамена снимает привязки документов заменённого файла (`FILE_REPLACE.unbound_version_ids`). Остаток: FK из `financial_statement_meta` и `ex_snapshot_item` — Срезы 3 и 7. Уточнено OQ-157: файл с автоподтверждёнными документами удаляется, пока их версии «в работе»; привязки снимаются. |
| OQ-111 | Ретеншн S3-объектов при удалении файла: сейчас `DELETE` лишь ставит `s3_object.expires_at = now()`, объекты `uploads`/`results` физически не стираются (дополняет OQ-22). Кто и когда их удаляет — lifecycle-правило бакета по метаданным, фоновая задача ядра, ручная очистка? Объект, записанный в S3 до сбоя БД, остаётся сиротой | arm-core Срез 2c: design D-10, №36 | платформа (lifecycle), Срез 2 → эксплуатация | допущение принято: бакет растёт; очистка — вне среза | | OQ-111 | Ретеншн S3-объектов при удалении файла: сейчас `DELETE` лишь ставит `s3_object.expires_at = now()`, объекты `uploads`/`results` физически не стираются (дополняет OQ-22). Кто и когда их удаляет — lifecycle-правило бакета по метаданным, фоновая задача ядра, ручная очистка? Объект, записанный в S3 до сбоя БД, остаётся сиротой | arm-core Срез 2c: design D-10, №36 | платформа (lifecycle), Срез 2 → эксплуатация | допущение принято: бакет растёт; очистка — вне среза |
| OQ-112 | Остаточные объекты e2e-прогона: каждый запуск `make e2e` оставляет в MinIO dev-стенда ~2 объекта (`uploads/…`, `results/…` — следствие OQ-111) | arm-core Срез 2c: Task 11 (O-5) | dev-стенд | допущение принято: чистить бакет `arm-ka` вручную (раздел «Сброс стенда» `dev/qa-guide.md`) | | OQ-112 | Остаточные объекты e2e-прогона: каждый запуск `make e2e` оставляет в MinIO dev-стенда ~2 объекта (`uploads/…`, `results/…` — следствие OQ-111) | arm-core Срез 2c: Task 11 (O-5) | dev-стенд | допущение принято: чистить бакет `arm-ka` вручную (раздел «Сброс стенда» `dev/qa-guide.md`) |
| OQ-113 | Полнота плана счетов seed: 19 нормализованных кодов строк-счетов golden ОСВ отсутствуют в `chart_of_account` (`01.01`, `66.01`, `76.01`, `60.АВ`, `76.ОТ` и др.), поэтому у их строк `account_id = NULL`. Достаточно ли «администратор добавит» или seed должен включать полный набор субсчетов (включая буквенные) до Среза ВГО (§4–§5) | arm-core Срез 2c: Task 8 (O-3) | Срезы 4–5 (пары счетов элиминации) | **закрыт: допущение принято — выгрузка плана счетов 1С заказчика (`fixtures/directories/chart-of-accounts-1c.xlsx`, 488 строк, все 122 кода seed в ней есть) полная (arm-core #25);** ранее: частично решён 06.10.2026, перенос в seed — arm-core#23 | | OQ-113 | Полнота плана счетов seed: 19 нормализованных кодов строк-счетов golden ОСВ отсутствуют в `chart_of_account` (`01.01`, `66.01`, `76.01`, `60.АВ`, `76.ОТ` и др.), поэтому у их строк `account_id = NULL`. Достаточно ли «администратор добавит» или seed должен включать полный набор субсчетов (включая буквенные) до Среза ВГО (§4–§5) | arm-core Срез 2c: Task 8 (O-3) | Срезы 4–5 (пары счетов элиминации) | **закрыт: допущение принято — выгрузка плана счетов 1С заказчика (`fixtures/directories/chart-of-accounts-1c.xlsx`, 488 строк, все 122 кода seed в ней есть) полная (arm-core #25);** ранее: частично решён 06.10.2026, перенос в seed — arm-core#23 |

View File

@ -66,7 +66,7 @@ opening_debit?, opening_credit?, turnover_debit?, turnover_credit?, closing_debi
всегда `UNKNOWN` → показывайте «—» (OQ-120). всегда `UNKNOWN` → показывайте «—» (OQ-120).
- «Статус» (подписи — как в чек-листе и его CSV, `context/10-user-journey.md` «Словарь UI»): файл `UPLOADED`/`PROCESSING` — - «Статус» (подписи — как в чек-листе и его CSV, `context/10-user-journey.md` «Словарь UI»): файл `UPLOADED`/`PROCESSING` —
«Обработка»; файл `ERROR` — «Ошибка» (причина — `job.error_message` / `job.mapping_error`; задание `timeout`/`dlq` — тоже «Обработка»; файл `ERROR` — «Ошибка» (причина — `job.error_message` / `job.mapping_error`; задание `timeout`/`dlq` — тоже
ошибка, причина в `job.error_message`); иначе по документу: `RECOGNIZED` и `NEEDS_CHECK` — «Требуется проверка», `CONFIRMED` — ошибка, причина в `job.error_message`); иначе по документу: `NEEDS_CHECK` и `RECOGNIZED` (старые и формы после правки) — «Требуется проверка», `CONFIRMED` (в т.ч. автоподтверждённый) —
«Загружен», `ERROR` — «Ошибка». «Загружен», `ERROR` — «Ошибка».
- Комментарий — у **документа** (`comments_count`, `GET/POST /documents/{id}/comments`, контракт 3b); у файла без документов - Комментарий — у **документа** (`comments_count`, `GET/POST /documents/{id}/comments`, контракт 3b); у файла без документов
комментария нет. комментария нет.

View File

@ -209,7 +209,7 @@ function parseError(status: number, body: unknown): { status: number; code: stri
| `PermissionObject` / `PermissionAction` | `DIRECTORY`, `DOCUMENT`, `CONSOLIDATION`, `KFO`, `EX`, `EXPORT`, `USERS`, `AUDIT` / `VIEW`, `EDIT`, `APPROVE`, `VERIFY`, `DELETE`, `EXPORT`, `ADMIN` | | `PermissionObject` / `PermissionAction` | `DIRECTORY`, `DOCUMENT`, `CONSOLIDATION`, `KFO`, `EX`, `EXPORT`, `USERS`, `AUDIT` / `VIEW`, `EDIT`, `APPROVE`, `VERIFY`, `DELETE`, `EXPORT`, `ADMIN` |
| `OnExisting` (импорт) | `skip`, `update` | | `OnExisting` (импорт) | `skip`, `update` |
**Автоподтверждение документа (OQ-157).** После маппинга чистый документ сразу приходит `CONFIRMED` с `confirmed_by: null`: кнопку «Подтвердить» показывать неактивной; повторный `POST …/confirm` — 409 `ALREADY_CONFIRMED`. Удаление файла с таким документом возможно (204), кроме случаев, когда документ подтвердил пользователь или версия на согласовании: тогда 409 `FILE_HAS_CONFIRMED_DOCUMENTS`. **Автоподтверждение документа (OQ-157).** После маппинга чистый документ сразу приходит `CONFIRMED` с `confirmed_by: null`: кнопку «Подтвердить» показывать неактивной; повторный `POST …/confirm` — 409 `ALREADY_CONFIRMED`. Удаление файла с таким документом возможно (204), кроме случаев, когда документ подтвердил пользователь или документ привязан к версии вне корректировки данных (не `INITIATED`/`DOCUMENTS_PROCESSING`/`DATA_CORRECTION`): тогда 409 `FILE_HAS_CONFIRMED_DOCUMENTS`.
--- ---
@ -350,7 +350,7 @@ UI прячет элементы по `GET /me → permissions` (объедин
|---|---|---| |---|---|---|
| `UPLOADED` | принят, ждёт отправки на распознавание | «Обработка», опрашивать | | `UPLOADED` | принят, ждёт отправки на распознавание | «Обработка», опрашивать |
| `PROCESSING` | распознаётся или маппится; `documents[]` ещё пуст | «Обработка», опрашивать | | `PROCESSING` | распознаётся или маппится; `documents[]` ещё пуст | «Обработка», опрашивать |
| `PROCESSED` | документы созданы | статус — по документу: `RECOGNIZED`/`NEEDS_CHECK` «Требуется проверка», `CONFIRMED` «Загружен», `ERROR` «Ошибка» | | `PROCESSED` | документы созданы | статус — по документу: `NEEDS_CHECK` и `RECOGNIZED` (старые и формы после правки) «Требуется проверка», `CONFIRMED` «Загружен» (в т.ч. автоподтверждённый), `ERROR` «Ошибка» |
| `ERROR` | ошибка распознавания (`job.status` `error`/`timeout`/`dlq`, причина — `job.error_code`/`error_message`) или маппинга (`job.mapping_error`) | «Ошибка» + причина | | `ERROR` | ошибка распознавания (`job.status` `error`/`timeout`/`dlq`, причина — `job.error_code`/`error_message`) или маппинга (`job.mapping_error`) | «Ошибка» + причина |
| `SUPERSEDED` | заменён более новым файлом с теми же документами (автозамена); в списке скрыт без `include_superseded=true` | — | | `SUPERSEDED` | заменён более новым файлом с теми же документами (автозамена); в списке скрыт без `include_superseded=true` | — |
@ -363,7 +363,7 @@ UI прячет элементы по `GET /me → permissions` (объедин
Документ: Документ:
- Один файл = N документов: лист ОСВ по счёту — документ `OSV` (`osv_account_code`, `locator` — лист), форма ББ/ОФР — документ - Один файл = N документов: лист ОСВ по счёту — документ `OSV` (`osv_account_code`, `locator` — лист), форма ББ/ОФР — документ
`BALANCE`/`INCOME`, расшифровки — один документ `DISCLOSURE`/`OTHER` на тип (без данных, сразу `RECOGNIZED`). `BALANCE`/`INCOME`, расшифровки — один документ `DISCLOSURE`/`OTHER` на тип (без данных; статус `RECOGNIZED` при маппинге, затем автоподтверждается → `CONFIRMED`).
- Период: `period_year` + `period_quarter` + `is_cumulative`. Квартальный — «2024 К1», нарастающий — «2024 · 9 мес.»; - Период: `period_year` + `period_quarter` + `is_cumulative`. Квартальный — «2024 К1», нарастающий — «2024 · 9 мес.»;
`period_quarter = null` — «период не определён» (например, ОСВ со скользящим периодом) — не подставлять квартал самим. `period_quarter = null` — «период не определён» (например, ОСВ со скользящим периодом) — не подставлять квартал самим.
- Проверки: `counterparty_check` — сверка шапки с ЮЛ-владельцем; `period_check` **всегда `UNKNOWN`** — показывать «—» - Проверки: `counterparty_check` — сверка шапки с ЮЛ-владельцем; `period_check` **всегда `UNKNOWN`** — показывать «—»
@ -593,7 +593,7 @@ async function download(path: string, fallbackName: string): Promise<void> {
| `DOCUMENT_READONLY` | 409 | правка/акцепт подтверждённой формы | — | перевести экран в просмотр | | `DOCUMENT_READONLY` | 409 | правка/акцепт подтверждённой формы | — | перевести экран в просмотр |
| `UNKNOWN_CELL`, `DUPLICATE_CELL` | 422 | правка формы | — | ошибка клиента (перечитать форму) | | `UNKNOWN_CELL`, `DUPLICATE_CELL` | 422 | правка формы | — | ошибка клиента (перечитать форму) |
| `NOTHING_TO_ACCEPT` | 409 | акцепт без нарушений | — | перечитать форму | | `NOTHING_TO_ACCEPT` | 409 | акцепт без нарушений | — | перечитать форму |
| `FILE_HAS_CONFIRMED_DOCUMENTS`, `FILE_SUPERSEDES_OTHERS` | 409 | удаление файла | — | скрывать «Удалить» у таких файлов (есть документ, подтверждённый пользователем, или автоподтверждённый документ в версии на согласовании, см. §3.7; или на файл ссылаются `superseded_by_file_id`) | | `FILE_HAS_CONFIRMED_DOCUMENTS`, `FILE_SUPERSEDES_OTHERS` | 409 | удаление файла | — | скрывать «Удалить» у таких файлов (есть документ, подтверждённый пользователем, или автоподтверждённый документ в версии вне корректировки данных (не `INITIATED`/`DOCUMENTS_PROCESSING`/`DATA_CORRECTION`), см. §3.7; или на файл ссылаются `superseded_by_file_id`) |
| `FILE_CONTENT_NOT_FOUND` | 404 | первичка | — | «Файл больше недоступен» | | `FILE_CONTENT_NOT_FOUND` | 404 | первичка | — | «Файл больше недоступен» |
| `TOO_MANY_IDS`, `INVALID_IDS` | 422 | `users/brief` | — | ошибка клиента (бейте на пачки ≤ 200) | | `TOO_MANY_IDS`, `INVALID_IDS` | 422 | `users/brief` | — | ошибка клиента (бейте на пачки ≤ 200) |
| `CONSOLIDATION_EXISTS` | 409 | создание консолидации | — | «Консолидация за этот период уже есть» → открыть её | | `CONSOLIDATION_EXISTS` | 409 | создание консолидации | — | «Консолидация за этот период уже есть» → открыть её |

View File

@ -506,6 +506,8 @@ PDF и изображения обрабатывает `scan-gateway`; ядро
### 6.9 Формы ББ/ОФР: просмотр, правка, акцепт, подтверждение (Срез 3b) ### 6.9 Формы ББ/ОФР: просмотр, правка, акцепт, подтверждение (Срез 3b)
Предусловие (OQ-157): чистая форма после маппинга сразу `CONFIRMED` (`confirmed_by` пуст, `reviewed_by` NULL) и правке не поддаётся (`PATCH` — 409 `DOCUMENT_READONLY`). Поэтому F1–F2 — на чистой форме `1_Форма1_бух_09.xlsx`; затем удалите её (`DELETE /files/{file_id}`, 204) и загрузите копию с нарушением: в Excel сумма строки 1600 колонки 0 увеличена на 100 (`1600 = 1100 + 1200` нарушено). F3–F10 ведутся на этой копии (`file_id` новой формы); F3 увеличивает сумму от значения копии, F7 возвращает её на верную сумму `1100 + 1200`.
Предусловия: раздел 4.2 (ядро с `NATS_ENABLED=true`, tabular-parser на `:8001`), `make seed-dev` (dev-ГК «Агрофирма Межениновская», Предусловия: раздел 4.2 (ядро с `NATS_ENABLED=true`, tabular-parser на `:8001`), `make seed-dev` (dev-ГК «Агрофирма Межениновская»,
ЮЛ с ИНН `7016006635`, Ростовский РФ — ИНН совпадает с ИНН шапок форм), пользователь `ka_go` (`DOCUMENT_EDIT`, `DOCUMENT_VIEW`, ЮЛ с ИНН `7016006635`, Ростовский РФ — ИНН совпадает с ИНН шапок форм), пользователь `ka_go` (`DOCUMENT_EDIT`, `DOCUMENT_VIEW`,
видит всё) и для негатива `expert` (только `DOCUMENT_VIEW`), `client_manager` (чужой филиал). В UI правка, акцепт и подтверждение видит всё) и для негатива `expert` (только `DOCUMENT_VIEW`), `client_manager` (чужой филиал). В UI правка, акцепт и подтверждение
@ -530,23 +532,23 @@ curl -s "localhost:8000/api/v1/documents/$DOC/statement" -H "$H" | python3 -m js
| № | Пользователь | Шаги | Ожидание | | № | Пользователь | Шаги | Ожидание |
|---|---|---|---| |---|---|---|---|
| F1 | ka_go | загрузить `1_Форма1_бух_09.xlsx` для ЮЛ с ИНН 7016006635, дождаться `PROCESSED` | один документ: `document_type = BALANCE`, `osv_account_code` пуст, `period_year = 2025`, `period_quarter = Q3`, `counterparty_check = MATCH`, `comments_count = 0` | | F1 | ka_go | загрузить `1_Форма1_бух_09.xlsx` для ЮЛ с ИНН 7016006635, дождаться `PROCESSED` | один документ: `document_type = BALANCE`, `osv_account_code` пуст, `period_year = 2025`, `period_quarter = Q3`, `counterparty_check = MATCH`, `comments_count = 0`, `status = CONFIRMED` (автоподтверждение: `confirmed_by` пуст, `reviewed_by` NULL) |
| F2 | ka_go | `GET /documents/{id}/statement` | 200; `edition = fsbu4`; `document.reporting_date = 2025-09-30`; `columns` — 3 (у колонки 0 `is_current = true`); `lines` — 37, отсортированы по `line_code`, у каждой `values` на все колонки; суммы — строки; `header.inn.value = "7016006635"`, `confidence = "100.00"`, `modified = false`; `checks.failed`, `soft_failed` пусты, `accepted = null` | | F2 | ka_go | `GET /documents/{id}/statement` | 200; `edition = fsbu4`; `document.reporting_date = 2025-09-30`; `columns` — 3 (у колонки 0 `is_current = true`); `lines` — 37, отсортированы по `line_code`, у каждой `values` на все колонки; суммы — строки; `header.inn.value = "7016006635"`, `confidence = "100.00"`, `modified = false`; `checks.failed`, `soft_failed` пусты, `accepted = null` |
| F3 | ka_go | `PATCH …/statement` `{"values":[{"line_id":…,"column_id":…,"amount":"<сумма строки 1600, колонки 0, увеличенная на 100>"}]}` (id — из F2) | 200; у ячейки `amount` новая, `original_amount` = прежняя, `is_modified = true`, `modified_by` = пользователь; `checks.failed` непуст (нарушено `1600 = 1100 + 1200` и/или `1600 = 1700`), `document.status = NEEDS_CHECK`; в `audit_event` одно `RECOGNIZED_DATA_EDIT` (раздел `RECOGNITION`, `object_type = document`) с `before`/`after` ячейки (6.4) | | F3 | ka_go | `PATCH …/statement` `{"values":[{"line_id":…,"column_id":…,"amount":"<сумма строки 1600, колонки 0, увеличенная на 100>"}]}` (id — из F2) | 200; у ячейки `amount` новая, `original_amount` = прежняя, `is_modified = true`, `modified_by` = пользователь; `checks.failed` непуст (нарушено `1600 = 1100 + 1200` и/или `1600 = 1700`), `document.status = NEEDS_CHECK`; в `audit_event` одно `RECOGNIZED_DATA_EDIT` (раздел `RECOGNITION`, `object_type = document`) с `before`/`after` ячейки (6.4) |
| F4 | ka_go | `POST …/confirm` | 409 `CHECKS_NOT_PASSED`, `detail.failures[]` — нарушения (`column_index`, `relation`, `expected`, `actual`) | | F4 | ka_go | `POST …/confirm` | 409 `CHECKS_NOT_PASSED`, `detail.failures[]` — нарушения (`column_index`, `relation`, `expected`, `actual`) |
| F5 | ka_go | `POST …/statement/checks/accept` `{"comment":"округление в форме клиента"}` | 200; `checks.accepted = {by, at, comment}`, `checks.failed` по-прежнему непуст; статус документа пересчитан (`RECOGNIZED`, если других причин `NEEDS_CHECK` нет); `POST …/confirm` теперь проходит (но F11 делайте после возврата значений) | | F5 | ka_go | `POST …/statement/checks/accept` `{"comment":"округление в форме клиента"}` | 200; `checks.accepted = {by, at, comment}`, `checks.failed` по-прежнему непуст; статус документа пересчитан (`RECOGNIZED`, если других причин `NEEDS_CHECK` нет); `POST …/confirm` теперь проходит (но F11 делайте после возврата значений) |
| F6 | ka_go | снова `PATCH` той же ячейки другой суммой | 200; `checks.accepted = null` (акцепт сброшен правкой) | | F6 | ka_go | снова `PATCH` той же ячейки другой суммой | 200; `checks.accepted = null` (акцепт сброшен правкой) |
| F7 | ka_go | `PATCH` той же ячейки обратно на исходную сумму (`original_amount` из F3) | 200; `is_modified = false`, `original_amount` остаётся, `checks.failed` пуст; `PATCH` с пустым телом `{}` — 200, новых записей `RECOGNIZED_DATA_EDIT` нет | | 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`; данные не изменились | | 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`; данные не изменились |
| 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` (поправьте дату — подтверждение проходит) | | 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` | | F10 | ka_go | `POST …/accept` без нарушений | 409 `NOTHING_TO_ACCEPT` |
| F11 | ka_go | `POST …/confirm` для чистой формы (после F7/F9 вернуть исходные значения) | 200 `DocumentOut`, `status = CONFIRMED`, `confirmed_by`/`confirmed_at`; повтор — 409 `ALREADY_CONFIRMED`; `PATCH`/`accept` — 409 `DOCUMENT_READONLY`; в БД у шапки `reviewed_by/at` заполнены | | 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` пуст |
| F12 | ka_go | загрузить `1_Форма2_бух_09.xlsx` тому же ЮЛ, дождаться `PROCESSED`; `GET …/statement` | `document_type = INCOME`, `period_quarter = Q3`, 2 колонки, `edition = fsbu4`; у ББ и ОФР поле `checks.cross_form` — либо `null`, либо нарушение `1370 − 1370(начало года) = 2400` (мягкое, не блокирует подтверждение); статус ОФР — `RECOGNIZED` | | F12 | ka_go | загрузить `1_Форма2_бух_09.xlsx` тому же ЮЛ, дождаться `PROCESSED`; `GET …/statement` | `document_type = INCOME`, `period_quarter = Q3`, 2 колонки, `edition = fsbu4`; у ББ и ОФР поле `checks.cross_form` — либо `null`, либо нарушение `1370 − 1370(начало года) = 2400` (мягкое, не блокирует подтверждение); статус ОФР — `CONFIRMED` (автоподтверждение; мягкие проверки его не отменяют) |
| F13 | ka_go | `GET /files/{file_id}/content?disposition=inline` и без параметра | 200, тело байт в байт равно загруженному `.xlsx`, `Content-Type` xlsx, заголовки `X-Content-Type-Options: nosniff` и `Cache-Control: private, no-store`; `inline` — ещё `Content-Security-Policy: sandbox`, `Content-Disposition: inline; filename="file.xlsx"; filename*=UTF-8''<имя>` (кириллица в percent-кодировке); `disposition=foo` — 422 | | F13 | ka_go | `GET /files/{file_id}/content?disposition=inline` и без параметра | 200, тело байт в байт равно загруженному `.xlsx`, `Content-Type` xlsx, заголовки `X-Content-Type-Options: nosniff` и `Cache-Control: private, no-store`; `inline` — ещё `Content-Security-Policy: sandbox`, `Content-Disposition: inline; filename="file.xlsx"; filename*=UTF-8''<имя>` (кириллица в percent-кодировке); `disposition=foo` — 422 |
| F14 | ka_go | `POST /documents/{id}/comments` `{"text":"нет подписи руководителя"}`; `{"text":" "}`; затем `GET …/comments` и `GET /files/{file_id}` | 201 с `author.full_name`; пустой после обрезки текст — 422; в списке один комментарий; в карточке файла и `GET /documents/{id}` у документа `comments_count = 1`; комментарий к `CONFIRMED` документу разрешён | | F14 | ka_go | `POST /documents/{id}/comments` `{"text":"нет подписи руководителя"}`; `{"text":" "}`; затем `GET …/comments` и `GET /files/{file_id}` | 201 с `author.full_name`; пустой после обрезки текст — 422; в списке один комментарий; в карточке файла и `GET /documents/{id}` у документа `comments_count = 1`; комментарий к `CONFIRMED` документу разрешён |
| F15 | ka_go | `GET /users/brief?ids=<uploaded_by файла>,<случайный uuid>`; с 201 id; с `ids=abc` | 200, один элемент `{id, full_name, is_active}`; 422 `TOO_MANY_IDS`; 422 `INVALID_IDS`; то же под `expert` (без `USERS_ADMIN`) — 200 | | F15 | ka_go | `GET /users/brief?ids=<uploaded_by файла>,<случайный uuid>`; с 201 id; с `ids=abc` | 200, один элемент `{id, full_name, is_active}`; 422 `TOO_MANY_IDS`; 422 `INVALID_IDS`; то же под `expert` (без `USERS_ADMIN`) — 200 |
| F16 | ka_go | `GET /directories/legal-entities?q=7016006635` после F1 и F12 | `documents_count = 2` (ББ и ОФР) | | F16 | ka_go | `GET /directories/legal-entities?q=7016006635` после F1 и F12 | `documents_count = 2` (ББ и ОФР) |
| F17 | ka_go | в ГК «Агрофирма Межениновская» завести второе ЮЛ и ГСК из двух ЮЛ (раздел 7, «Подготовка»), создать версию за 2025 Q3; подтвердить ББ и ОФР (F11, F12); `GET …/checklist` и `GET …/checklist?format=csv` | ячейки «Бух. баланс» и «ОФР» ЮЛ — `CONFIRMED`; CSV: файл начинается с BOM (`EF BB BF`), первая строка «Организация;ИНН;Бух. баланс;ОФР;ОСВ …», разделитель `;`, у ЮЛ «Загружен»; `Content-Disposition` с `filename="checklist.csv"` и `filename*=UTF-8''checklist-<ГК>-2025-Q3.csv` | | F17 | ka_go | в ГК «Агрофирма Межениновская» завести второе ЮЛ и ГСК из двух ЮЛ (раздел 7, «Подготовка»), создать версию за 2025 Q3; убедиться, что ББ и ОФР второго ЮЛ `CONFIRMED` (чистые формы автоподтверждаются; при `NEEDS_CHECK` — проверить значения и подтвердить вручную, F11); `GET …/checklist` и `GET …/checklist?format=csv` | ячейки «Бух. баланс» и «ОФР» ЮЛ — `CONFIRMED`; CSV: файл начинается с BOM (`EF BB BF`), первая строка «Организация;ИНН;Бух. баланс;ОФР;ОСВ …», разделитель `;`, у ЮЛ «Загружен»; `Content-Disposition` с `filename="checklist.csv"` и `filename*=UTF-8''checklist-<ГК>-2025-Q3.csv` |
| F18 | expert | `PATCH …/statement`, `POST …/comments`, `POST …/accept` | 403 (нет `DOCUMENT_EDIT`); `GET …/statement` и `GET …/comments` — 200 | | F18 | expert | `PATCH …/statement`, `POST …/comments`, `POST …/accept` | 403 (нет `DOCUMENT_EDIT`); `GET …/statement` и `GET …/comments` — 200 |
| F19 | client_manager | `GET …/statement`, `GET /files/{id}/content` документа ЮЛ другого филиала | 404 `NOT_FOUND`, тело как у случайного uuid | | F19 | client_manager | `GET …/statement`, `GET /files/{id}/content` документа ЮЛ другого филиала | 404 `NOT_FOUND`, тело как у случайного uuid |
| F20 | ka_go | `GET …/statement` документа `OSV` (раздел 6.6) | 409 `DOCUMENT_NOT_STATEMENT` | | F20 | ka_go | `GET …/statement` документа `OSV` (раздел 6.6) | 409 `DOCUMENT_NOT_STATEMENT` |
@ -766,7 +768,7 @@ docker compose -f ../docs/dev/docker-compose.yml --profile services up -d --buil
| № | Шаги | Ожидание | | № | Шаги | Ожидание |
|---|---|---| |---|---|---|
| W16 | В панели Счастливочной клик по «ОСВ 60» | экран «Просмотр ОСВ»: крошки «Отчётность › ПЕКАРНЯ СЧАСТЛИВОЧНАЯ, ООО › Проверка документа», подзаголовок «<имя файла> · XLS · <дата>», справа статус «Загружен», кнопка «✓ Подтвердить» неактивна; надпись «Суммы в рублях; строки ОСВ только для чтения»; дерево счёт → субсчёт → контрагент, колонки «Счёт / контрагент», «ИНН» и шесть сумм. Корневая строка `60`: «—», «27 923 002,09», «107 554 149,33», «99 188 100,32», «—», «19 556 953,08» (сальдо нач. Кт + обороты Кт − обороты Дт = сальдо кон. Кт) | | W16 | В панели Счастливочной клик по «ОСВ 60» | экран «Просмотр ОСВ»: крошки «Отчётность › ПЕКАРНЯ СЧАСТЛИВОЧНАЯ, ООО › Проверка документа», подзаголовок «<имя файла> · XLS · <дата>», справа статус «Загружен», кнопка «✓ Подтвердить» неактивна; надпись «Суммы в рублях; строки ОСВ только для чтения»; дерево счёт → субсчёт → контрагент, колонки «Счёт / контрагент», «ИНН» и шесть сумм. Корневая строка `60`: «—», «27 923 002,09», «107 554 149,33», «99 188 100,32», «—», «19 556 953,08» (сальдо нач. Кт + обороты Кт − обороты Дт = сальдо кон. Кт) |
| W17 | Загрузить ОСВ Счастливочной под ЮЛ РБ (один файл из `../fixtures/osv/schastl/2024-Q3/`, проверка `Контрагент ✗`), в панели открыть «ОСВ 60» → статус «Требуется проверка» → «✓ Подтвердить» | до подтверждения статус «Требуется проверка»; после — уведомление «Документ подтверждён», статус «Загружен», кнопка неактивна; в панели документ — «Загружен» | | W17 | Загрузить один файл из `../fixtures/osv/schastl/2024-Q3/` в ЮЛ вне демо-периметра (любое ЮЛ стенда вне ГСК демо, без `consolidation_version_id`; проверка `Контрагент ✗` — шапка другой организации; не РБ, чтобы не заменить файлы и не сдвинуть счётчики W18), в панели открыть «ОСВ 60» → статус «Требуется проверка» → «✓ Подтвердить» | до подтверждения статус «Требуется проверка»; после — уведомление «Документ подтверждён», статус «Загружен», кнопка неактивна; в панели документ — «Загружен» |
| W18 | Остальные файлы РБ и Счастливочной 2024 Q1–Q3 (всего 22, список — 7.1): через «Загрузить документы» у каждого ЮЛ и подтверждение каждого ОСВ, либо скриптом из каталога `arm-web`: `scripts/demo-prepare.sh <version_id>` (id — из адреса `?version=`) | скрипт печатает строку на каждый файл (пять файлов из W13 — «уже загружен», это нормально), ждёт распознавания (до 5 мин), подтверждает только неподтверждённые ОСВ (чистые уже «Загружен», автоподтверждение, OQ-157) и в конце печатает `строк: 1385 документов: 22 несопоставленных: 1381 пробелов: 7`. Статус версии — «Коррекция данных» (K9) появляется сразу после распознавания (автоподтверждение чистых документов, OQ-157), в «Организации» в колонке «ОСВ» — счета ЮЛ | | W18 | Остальные файлы РБ и Счастливочной 2024 Q1–Q3 (всего 22, список — 7.1): через «Загрузить документы» у каждого ЮЛ и подтверждение каждого ОСВ, либо скриптом из каталога `arm-web`: `scripts/demo-prepare.sh <version_id>` (id — из адреса `?version=`) | скрипт печатает строку на каждый файл (пять файлов из W13 — «уже загружен», это нормально), ждёт распознавания (до 5 мин), подтверждает только неподтверждённые ОСВ (чистые уже «Загружен», автоподтверждение, OQ-157) и в конце печатает `строк: 1385 документов: 22 несопоставленных: 1381 пробелов: 7`. Статус версии — «Коррекция данных» (K9) появляется сразу после распознавания (автоподтверждение чистых документов, OQ-157), в «Организации» в колонке «ОСВ» — счета ЮЛ |
`demo-prepare.sh` берёт пользователя вторым аргументом (по умолчанию `ka_go@armka.local`), адрес ядра — из `API` `demo-prepare.sh` берёт пользователя вторым аргументом (по умолчанию `ka_go@armka.local`), адрес ядра — из `API`
@ -822,7 +824,7 @@ PDF из `fixtures/ocr` — отчётность других компаний (
«Счёт» принимает только корень (`60`, не `60.01`). «Счёт» принимает только корень (`60`, не `60.01`).
- Таблица консолидаций и статус версии обновляются по F5: после создания консолидации на странице той же ГК новой строки - Таблица консолидаций и статус версии обновляются по F5: после создания консолидации на странице той же ГК новой строки
может не быть до обновления. Панель «Организации» перечитывается при закрытии панели документов. может не быть до обновления. Панель «Организации» перечитывается при закрытии панели документов.
- «Распознан» и «Требуется проверка» в панели документов выглядят одинаково — «Требуется проверка». - «Распознан» (старые `RECOGNIZED` и формы после правки) и «Требуется проверка» в панели документов выглядят одинаково — «Требуется проверка».
- «ГСК» и «Дата верификации» в таблице консолидаций, «Утверждён в РФ» и «Верифицирован в ГО» — всегда «—». - «ГСК» и «Дата верификации» в таблице консолидаций, «Утверждён в РФ» и «Верифицирован в ГО» — всегда «—».
- Панель документов показывает первые 100 файлов ЮЛ; загрузка — только из панели ЮЛ внутри версии. - Панель документов показывает первые 100 файлов ЮЛ; загрузка — только из панели ЮЛ внутри версии.
- Шапку формы (ИНН, дату, единицу) нельзя очистить, только заменить; время принятия нарушений — в часовом поясе - Шапку формы (ИНН, дату, единицу) нельзя очистить, только заменить; время принятия нарушений — в часовом поясе