fix: правки по финальному ревью автоподтверждения (I-1, M-1, M-2, M-4..M-6, T6.x)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e7bef704cf
commit
8122205357
@ -76,7 +76,7 @@
|
||||
advisory-lock, SAVEPOINT на файл, `delete`), `services/file_status.py` (единственное место смены `stored_file.status`
|
||||
по ТЕКУЩЕМУ заданию; `PROCESSED`/`SUPERSEDED` обменом не перезаписываются), `services/result_mapping.py` (цикл маппинга:
|
||||
`pick_candidate` → проверка ключа результата (OQ-70) → чтение S3 вне транзакции → `map_file` в одной транзакции →
|
||||
автозамена `FILE_REPLACE`), `services/documents.py` (карточки, `confirm`), `services/auto_confirm.py` (автоподтверждение чистых документов в транзакции маппинга: `_map_candidate`: `map_file` → `auto_confirm` → `on_documents_mapped`; `confirmed_by` NULL, `DOCUMENT_CONFIRM` без пользователя, переход версии — от `uploaded_by`; OQ-157), `domain/osv_mapping.py` (чистые функции:
|
||||
автозамена `FILE_REPLACE`), `services/documents.py` (карточки, `confirm`), `services/auto_confirm.py` (автоподтверждение чистых документов в транзакции маппинга: `_map_candidate`: `map_file` → автозамена → `auto_confirm` → `on_documents_mapped`; `confirmed_by` NULL, `DOCUMENT_CONFIRM` без пользователя, переход версии — от `uploaded_by`; OQ-157), `domain/osv_mapping.py` (чистые функции:
|
||||
`normalize_account_code`, `build_entries`, `counterparty_check`, `document_status`), `repositories/counterparties.py`
|
||||
(`CounterpartyIndex` — резолв контрагента без policy). **Фоновых циклов три** (запускаются в lifespan при
|
||||
`NATS_ENABLED`; цикл маппинга — ещё и при подключённом S3 (storage)): публикатор outbox, вахтёр сроков и цикл маппинга
|
||||
|
||||
19
README.md
19
README.md
@ -423,6 +423,9 @@ POST /legal-entities/{id}/files → S3 uploads/… + stored_file(UPLOADED) + rec
|
||||
→ автозамена прежних версий (SUPERSEDED) → GET /files/{id}, GET /documents/{id}/entries → POST /documents/{id}/confirm
|
||||
```
|
||||
|
||||
Чистый документ подтверждается самим маппингом (автоподтверждение, OQ-157); `POST /documents/{id}/confirm` нужен для
|
||||
`NEEDS_CHECK` и для форм ББ/ОФР после правки.
|
||||
|
||||
Загрузка — `multipart/form-data`, одно или несколько полей `files`; право `DOCUMENT_EDIT`, ЮЛ в области видимости
|
||||
пользователя (чужое — 404, как и несуществующее). Поддерживаются `xls` (BIFF8), `xlsx`, `xlsb`, `pdf`, `png`, `jpg`
|
||||
(`.jpeg` — как `jpg`): тип определяется по расширению и сверяется с сигнатурой содержимого. Таблицы (`xls/xlsx/xlsb`)
|
||||
@ -466,11 +469,10 @@ curl -s "localhost:8000/api/v1/files/<file_id>" -H "Authorization: Bearer $TOKEN
|
||||
`PROCESSED` и `SUPERSEDED` события обмена не перезаписывают; запоздавший ack не воскрешает `ERROR`.
|
||||
|
||||
**Документ** (`document.status`) создаётся маппером на каждый лист ОСВ, на каждую форму ББ/ОФР (Срез 3b, D-2) и по одному на
|
||||
тип `DISCLOSURE`/`OTHER` результата (D-3; без данных, сразу `RECOGNIZED`). Условия для листа ОСВ:
|
||||
тип `DISCLOSURE`/`OTHER` результата (D-3; без данных, при маппинге `RECOGNIZED`, затем автоподтверждаются системой). Условия для листа ОСВ:
|
||||
|
||||
- `RECOGNIZED` — задание `done`, проверки парсера без нарушений, шапка не противоречит владельцу
|
||||
(`counterparty_check ≠ MISMATCH`), предупреждений маппинга у листа нет;
|
||||
- чистый документ (`RECOGNIZED`) подтверждается системой сразу в маппинге; `RECOGNIZED` остаётся у документов с замечаниями и у форм, не прошедших условия подтверждения (после правки формы автоподтверждения нет);
|
||||
- `NEEDS_CHECK` — иначе: задание `needs_check`, нарушения `checks`, `counterparty_check = MISMATCH`, повтор счёта
|
||||
в файле, строки без родителя, нарастающая по данным Q1;
|
||||
- для формы ББ/ОФР условия `RECOGNIZED`/`NEEDS_CHECK` — правило D-6 (раздел «Формы ББ/ОФР»): проверки §7 по суммам формы вместо
|
||||
@ -478,6 +480,8 @@ curl -s "localhost:8000/api/v1/files/<file_id>" -H "Authorization: Bearer $TOKEN
|
||||
- `CONFIRMED` — аналитик нажал «Подтвердить» (`POST /documents/{id}/confirm`, `confirmed_by/at`, аудит) либо система при автоподтверждении чистого документа (`confirmed_by = null`, аудит `DOCUMENT_CONFIRM` без пользователя; OQ-157);
|
||||
- `ERROR` — зарезервирован: в Срезе 2c ядро документ в `ERROR` не создаёт (результат без документов — «в результате нет документов» — переводит в `ERROR` файл).
|
||||
|
||||
Чистый документ (`RECOGNIZED` при маппинге) подтверждается системой сразу в маппинге. `RECOGNIZED` остаётся у форм, ставших чистыми после правки/акцепта (автоподтверждения после правки нет), у форм, не прошедших условия подтверждения (`form_confirmable`), и у документов, созданных до автоподтверждения; у документов с замечаниями статус `NEEDS_CHECK`.
|
||||
|
||||
`counterparty_check` (`MATCH` / `MISMATCH` / `UNKNOWN`) сверяет шапку (ИНН, иначе имя или алиас) с ЮЛ — владельцем файла;
|
||||
`period_check` — `UNKNOWN`: подходящесть документа версии консолидации — функция (документ, версия), а не свойство документа (Срез 4, OQ-120). Допущения помечены `ASSUMPTION(OQ-nn)` в коде.
|
||||
|
||||
@ -524,7 +528,7 @@ S3 (storage); без S3 — предупреждение в логе, цикл
|
||||
| `DELETE /files/{file_id}` | `DOCUMENT_EDIT` | удалить файл (см. «Удаление и замена»), 204 |
|
||||
| `GET /documents/{document_id}` | `DOCUMENT_VIEW` | документ: статус, проверки, `locator`, `group_schema` листа, `warnings`, `file_id` |
|
||||
| `GET /documents/{document_id}/entries` | `DOCUMENT_VIEW` | строки ОСВ по `row_number` (`limit` ≤ 2000, по умолчанию 500); дерево — по `parent_entry_id` |
|
||||
| `POST /documents/{document_id}/confirm` | `DOCUMENT_EDIT` | `RECOGNIZED`/`NEEDS_CHECK` → `CONFIRMED`, аудит `DOCUMENT_CONFIRM`; для формы ББ/ОФР — ещё `PERIOD_REQUIRED` и `CHECKS_NOT_PASSED` |
|
||||
| `POST /documents/{document_id}/confirm` | `DOCUMENT_EDIT` | `RECOGNIZED`/`NEEDS_CHECK` → `CONFIRMED`, аудит `DOCUMENT_CONFIRM`; для автоподтверждённого документа — 409 `ALREADY_CONFIRMED` (D-6); для формы ББ/ОФР — ещё `PERIOD_REQUIRED` и `CHECKS_NOT_PASSED` |
|
||||
| `GET /documents/{document_id}/statement` | `DOCUMENT_VIEW` | форма ББ/ОФР: шапка, колонки, строки со значениями, редакция, проверки §7 (см. «Формы ББ/ОФР») |
|
||||
| `PATCH /documents/{document_id}/statement` | `DOCUMENT_EDIT` | правка значений и шапки (`inn`, `report_date`, `units`) до подтверждения; аудит `RECOGNIZED_DATA_EDIT` |
|
||||
| `POST /documents/{document_id}/statement/checks/accept` | `DOCUMENT_EDIT` | акцепт непройденных жёстких проверок с комментарием (`{comment}`) |
|
||||
@ -601,9 +605,8 @@ advisory-блокировкой.
|
||||
|
||||
**Удаление.** `DELETE /files/{id}` удаляет формы ББ/ОФР (шапки, колонки, строки, значения), комментарии документов, строки ОСВ, документы, задания и `stored_file`, проставляет объектам S3
|
||||
(`uploads`, `results`) из папок этого файла (`uploads/{file_id}/`, `results/{file_id}/`) `expires_at = now()`
|
||||
(физически объект не удаляется — ретеншн OQ-22, OQ-111) и пишет `FILE_DELETE` с карточкой файла в `before_value`. Нельзя удалить файл с документом, подтверждённым пользователем (`confirmed_by` задан), и с автоподтверждённым документом, привязанным к версии вне статусов «в работе» `INITIATED`/`DOCUMENTS_PROCESSING`/`DATA_CORRECTION` (`409 FILE_HAS_CONFIRMED_DOCUMENTS`); автоподтверждённые привязки к версиям «в работе» снимаются (`FILE_DELETE.after_value.unbound_version_ids`, OQ-157)
|
||||
и файл, который заменил другие (`409 FILE_SUPERSEDES_OTHERS`); `SUPERSEDED` файл удалить можно. Блокировки — в порядке
|
||||
«задания → файл → документы».
|
||||
(физически объект не удаляется — ретеншн OQ-22, OQ-111) и пишет `FILE_DELETE` с карточкой файла в `before_value`. Нельзя удалить файл с документом, подтверждённым пользователем (`confirmed_by` задан), и с автоподтверждённым документом, привязанным к версии вне статусов «в работе» `INITIATED`/`DOCUMENTS_PROCESSING`/`DATA_CORRECTION` (`409 FILE_HAS_CONFIRMED_DOCUMENTS`); автоподтверждённые привязки к версиям «в работе» снимаются (`FILE_DELETE.after_value.unbound_version_ids`, OQ-157). Нельзя также удалить файл, который заменил другие (`409 FILE_SUPERSEDES_OTHERS`); `SUPERSEDED` файл удалить можно. Блокировки — в порядке
|
||||
«задания → файл → документы → advisory ГК → версии по id».
|
||||
|
||||
## Ограничения Среза 2c
|
||||
|
||||
@ -613,7 +616,7 @@ advisory-блокировкой.
|
||||
- Общий размер multipart-запроса `POST /legal-entities/{id}/files` и число файлов в нём приложением не ограничены
|
||||
(Starlette спулит части на диск, `UPLOAD_MAX_BYTES` — лимит одного файла); лимит запроса задаётся ingress платформы.
|
||||
- Маппер Среза 2c разбирал только листы ОСВ (`osvDocument`); с Среза 3b он мапит и формы ББ/ОФР (раздел «Формы ББ/ОФР»).
|
||||
Документы `DISCLOSURE`/`OTHER` результата не игнорируются: на каждый тип создаётся один документ без данных (`RECOGNIZED`,
|
||||
Документы `DISCLOSURE`/`OTHER` результата не игнорируются: на каждый тип создаётся один документ без данных (`RECOGNIZED` при маппинге, затем автоподтверждение;
|
||||
`locator` — перечень листов через запятую; OQ-23). Отдельной обработки по конвейеру (`tabular`/`image`) в маппере нет: разбирается
|
||||
любой результат, проходящий схему `recognition-result`.
|
||||
- Корневой сегмент счёта (`account_root`) — только цифровой (`ASSUMPTION(OQ-103)`): забалансовые «МЦ.04», «ГТД»
|
||||
@ -967,7 +970,7 @@ email без учёта регистра проверяет только сер
|
||||
Тест `api/tests/e2e/test_osv_ingest_e2e.py` (маркеры `integration`, `e2e`, `fixtures`) проверяет весь путь Среза 2c с
|
||||
РЕАЛЬНЫМ tabular-parser: `POST /legal-entities/{id}/files` (ОСВ 66 за 2024 из `../fixtures`) → outbox → NATS →
|
||||
tabular-parser (MinIO) → inbox → маппинг → `GET /files/{id}` = `PROCESSED`, документ 66/2024/Q4 (нарастающий,
|
||||
`RECOGNIZED`, `counterparty_check = MATCH`) и 9 строк ОСВ. Приложение — настоящий `src.app.app` с lifespan на
|
||||
`CONFIRMED` автоподтверждением, `counterparty_check = MATCH`) и 9 строк ОСВ. Приложение — настоящий `src.app.app` с lifespan на
|
||||
отдельной временной БД с seed, NATS и MinIO — из `api/.env.test`; в конце файл удаляется через `DELETE /files/{id}`
|
||||
(объекту S3 ставится `expires_at`), БД снимается.
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@ DELETE_RESPONSES: dict[int | str, dict[str, Any]] = {
|
||||
status.HTTP_409_CONFLICT: {
|
||||
"model": ErrorDetail,
|
||||
"description": "FILE_HAS_CONFIRMED_DOCUMENTS (документ подтверждён пользователем или автоподтверждённый "
|
||||
"документ в версии на согласовании) или FILE_SUPERSEDES_OTHERS "
|
||||
"документ привязан к версии вне корректировки данных) или FILE_SUPERSEDES_OTHERS "
|
||||
"(файл заменил более ранние версии)",
|
||||
},
|
||||
}
|
||||
|
||||
@ -318,7 +318,7 @@ class PrimaryFilesService:
|
||||
if any(status not in IN_WORK_STATUSES for _, status in bound):
|
||||
raise ConflictError(
|
||||
FILE_HAS_CONFIRMED_DOCUMENTS,
|
||||
"файл нельзя удалить: документ уже участвует в версии консолидации на согласовании",
|
||||
"файл нельзя удалить: документ привязан к версии консолидации вне корректировки данных",
|
||||
)
|
||||
unbound_version_ids = sorted({version_id for version_id, _ in bound})
|
||||
await consolidation_documents.unbind_documents(
|
||||
|
||||
@ -59,7 +59,7 @@ async def statement_hard_failures(
|
||||
|
||||
|
||||
async def form_confirmable(session: AsyncSession, document: Document) -> bool:
|
||||
"""Условия подтверждения формы ББ/ОФР без исключений (D-9 Среза 3b; автоподтверждение — design D-3, OQ-157):
|
||||
"""Условия подтверждения формы ББ/ОФР без исключений (§7, D-9 Среза 3b; автоподтверждение — design D-3, OQ-157):
|
||||
период определён и жёстких нарушений §7 нет или они акцептованы. Не-форма и форма без шапки (созданная до Среза 3b)
|
||||
— `True`: условий у них нет. Ручной путь (`DocumentService._check_statement`) проверяет то же, но отвечает 409."""
|
||||
if document.document_type not in FORM_TYPES:
|
||||
|
||||
@ -5,6 +5,7 @@ import uuid
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from loguru import logger
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@ -143,10 +144,16 @@ async def test_file_without_uploader_binds_without_transition(seeded: AsyncSessi
|
||||
version = await _version(seeded, group, entities, ConsolidationStatus.INITIATED)
|
||||
file, [document] = await _file_with(seeded, entities[0], [DocumentStatus.RECOGNIZED], None)
|
||||
|
||||
await auto_confirm(seeded, file=file, documents=[document], now=utc_now())
|
||||
messages: list[str] = []
|
||||
sink = logger.add(lambda message: messages.append(str(message)), level="INFO")
|
||||
try:
|
||||
await auto_confirm(seeded, file=file, documents=[document], now=utc_now())
|
||||
finally:
|
||||
logger.remove(sink)
|
||||
|
||||
assert document.status is DocumentStatus.CONFIRMED
|
||||
assert await _bound(seeded, version.id) == {document.id}
|
||||
assert any("Переход версии по подтверждению пропущен" in m and str(version.id) in m for m in messages)
|
||||
refreshed = await seeded.get(ConsolidationVersion, version.id, populate_existing=True)
|
||||
assert refreshed is not None and refreshed.status is ConsolidationStatus.INITIATED
|
||||
|
||||
|
||||
@ -1562,6 +1562,11 @@ async def test_mixed_file_confirms_clean_and_moves_version_once(
|
||||
assert statuses == [("60", DocumentStatus.CONFIRMED), ("62", DocumentStatus.NEEDS_CHECK)]
|
||||
history = await _history_of(maker, version_id)
|
||||
assert [h.to_status for h in history].count(ConsolidationStatus.DATA_CORRECTION) == 1
|
||||
# переход сделал именно автоподтверждение (T4.2): при перестановке с on_documents_mapped комментарий иной
|
||||
assert history[-1].to_status is ConsolidationStatus.DATA_CORRECTION
|
||||
assert history[-1].comment == "автоподтверждение документа OSV 60"
|
||||
clean = next(d for d in await _documents(maker, file_id) if d.osv_account_code == "60")
|
||||
assert await _bound_ids(maker, version_id) == {clean.id} # NEEDS_CHECK-документ 62 не привязан
|
||||
|
||||
|
||||
async def test_supersede_rebinds_auto_confirmed_document(committed_sessionmaker: async_sessionmaker[AsyncSession]):
|
||||
@ -1657,8 +1662,9 @@ async def _audit_count(maker: async_sessionmaker[AsyncSession]) -> int:
|
||||
|
||||
async def test_exchange_and_mapping_write_no_audit_events(committed_sessionmaker: async_sessionmaker[AsyncSession]):
|
||||
"""spec audit-trail «События файлов и документов»: смена статусов задания и файла (ack, результат, маппинг) и
|
||||
автозамена без заменяемых файлов `audit_event` не пишут: путь `UPLOADED → PROCESSING → PROCESSED` оставляет таблицу
|
||||
аудита неизменной."""
|
||||
автозамена без заменяемых файлов `audit_event` не пишут: путь `UPLOADED → PROCESSING → PROCESSED` для документа
|
||||
`NEEDS_CHECK` (у владельца нет ИНН) оставляет таблицу аудита неизменной. Автоподтверждение чистого документа пишет
|
||||
`DOCUMENT_CONFIRM` — это проверяет отдельный тест."""
|
||||
maker, storage = committed_sessionmaker, FakeStorage()
|
||||
file_id, job_id, _ = await _prepare(
|
||||
maker, storage, _minimal(), job_status=RecognitionStatus.published, file_status=FileStatus.UPLOADED
|
||||
@ -1674,6 +1680,8 @@ async def test_exchange_and_mapping_write_no_audit_events(committed_sessionmaker
|
||||
assert await process_once(maker, storage, _settings()) is True
|
||||
|
||||
assert (await _file(maker, file_id)).status is FileStatus.PROCESSED
|
||||
[document] = await _documents(maker, file_id)
|
||||
assert document.status is DocumentStatus.NEEDS_CHECK
|
||||
assert await _audit_count(maker) == before
|
||||
|
||||
|
||||
|
||||
@ -58,10 +58,10 @@
|
||||
### D-4. Удаление: только автоподтверждённое и только до согласования
|
||||
`PrimaryFilesService.delete` под существующими блокировками (задания → файл → документы) проверяет: если у файла есть
|
||||
`CONFIRMED` документ с `confirmed_by IS NOT NULL` — 409 `FILE_HAS_CONFIRMED_DOCUMENTS` (как сейчас). Если все
|
||||
`CONFIRMED` документы автоподтверждены — дальше блокируются версии ЮЛ (`lock_versions_for_entity` по всем статусам, где
|
||||
есть привязки этих документов; порядок «… → документы → advisory ГК → версии» соблюдён) и проверяется, что каждая
|
||||
`CONFIRMED` документы автоподтверждены — дальше блокируются версии ЮЛ «в работе» (`lock_versions_for_entity` берёт только `IN_WORK_STATUSES`; версии вне
|
||||
«в работе» читаются без блокировки: удаление из них запрещено, возврата в «в работе» нет; порядок «… → документы → advisory ГК → версии» соблюдён) и проверяется, что каждая
|
||||
версия с привязкой в `INITIATED`/`DOCUMENTS_PROCESSING`/`DATA_CORRECTION`; иначе 409 `FILE_HAS_CONFIRMED_DOCUMENTS` с
|
||||
текстом «документ уже участвует в версии на согласовании». Затем привязки снимаются (`consolidation_document`), файл
|
||||
текстом «документ привязан к версии консолидации вне корректировки данных». Затем привязки снимаются (`consolidation_document`), файл
|
||||
удаляется как сейчас; `FILE_DELETE.after_value = {unbound_version_ids: [...]}` (по возрастанию; пусто — пустой список),
|
||||
`before_value` — карточка файла, как сейчас. Статус версии не меняется (свод — чистая функция, пересчитается).
|
||||
`FILE_SUPERSEDES_OTHERS` — без изменений.
|
||||
@ -113,3 +113,6 @@
|
||||
| 9 | 2026-10-08 | Тесты автоподтверждения, маппинга и удаления берут `uploaded_by` у реального пользователя (FK `app_user`), а не случайный uuid (ошибка плана) | случайный uuid не проходит FK | нет |
|
||||
| 10 | 2026-10-08 | В `tests/test_api_files_delete.py` тесты, имитирующие подтверждение пользователем (`test_confirmed_document_blocks_delete`, `test_delete_rereads_state_after_locks`), получили `confirmed_by` | иначе по правилу удаления (D-4) документ считается автоподтверждённым и удаление проходит | нет |
|
||||
| 11 | 2026-10-08 | Импорт `auto_confirm` в `result_mapping._map_candidate` — локальный | цикл `documents` → `result_mapping` → `auto_confirm` → `statements` → `documents` | техдолг: вынести `form_confirmable`/`FORM_TYPES` в модуль без обратной зависимости |
|
||||
| 12 | 2026-10-08 | `make check-full` запускается один раз в конце, после финального ревью и fix-волны (а не внутри Task 6 плана) | не гонять полную проверку дважды | если check-full что-то найдёт — правка после ревью |
|
||||
| 13 | 2026-10-08 | M-3 (гонка в `bound_versions` при будущем маршруте «вернуть на доработку» Среза 7) и вынос `form_confirmable` из цикла импортов оставлены техдолгом (follow-up) | сейчас возврата версии в «в работе» нет, цикл импортов обойдён локальным импортом | при добавлении возврата на доработку надо отвечать 409 для версий, не попавших в множество, заблокированное `lock_versions_for_entity` |
|
||||
| 14 | 2026-10-08 | Отменённая/отклонённая/архивная версия с привязкой автоподтверждённого документа блокирует удаление файла (по спеке «вне INITIATED/DOCUMENTS_PROCESSING/DATA_CORRECTION»), привязки при отмене версии не снимаются | буквальное чтение спеки | файл из отменённой версии удалить нельзя — вопрос владельцу |
|
||||
|
||||
@ -49,7 +49,9 @@ DBML и контракты обмена не меняются.
|
||||
|
||||
- Код: `services/result_mapping.py` (`_map_candidate`), `services/consolidation_documents.py`
|
||||
(`on_document_confirmed` с внешним `changed_by`/`comment`, новый `auto_confirm`), `services/documents.py` (общая
|
||||
проверка условий формы), `services/primary_files.py` (`delete`), `repositories/files.py` (снятие привязок).
|
||||
- Спеки: `osv-result-mapping`, `documents-api`, `primary-files`, `audit-trail`.
|
||||
проверка условий формы), `services/primary_files.py` (`delete`; привязки снимает существующий
|
||||
`consolidation_documents.unbind_documents`).
|
||||
- Спеки (дельты): `osv-result-mapping`, `primary-files`, `audit-trail`. Ручной путь `POST /documents/{id}/confirm` не
|
||||
меняется, поэтому дельты `documents-api` нет.
|
||||
- Поведение для фронта: чистые документы приходят сразу `CONFIRMED` («Загружен»); `confirmed_by = null` — признак
|
||||
автоподтверждения.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user