Хук в auto_confirm: на каждый документ, подтверждённый системой, пишется quality_outcome_fact (outcome=AUTO_CONFIRMED, primary_cause=WITHOUT_PARTICIPATION всегда — правок при автоподтверждении не бывает, D-3 блокирует формы с нарушениями). payload строится build_outcome_payload по тому же шаблону, что факт приёма (Task 5): ОСВ — osv_warnings/row_count, формы — lines/values/header_source, issue_count_at_outcome — все нарушения документа без фильтра. Запись в транзакции маппинга, без flush/commit (Task 4 insert_outcome_fact). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
145 lines
7.6 KiB
Python
145 lines
7.6 KiB
Python
"""Автоподтверждение чистых документов при маппинге результата (osv-result-mapping «Автоподтверждение чистого
|
||
документа»; `05-domain-rules.md` §9.3, §9.5; OQ-157; design D-1–D-3 change `auto-confirm-clean-documents`).
|
||
|
||
Документ, получивший при маппинге `RECOGNIZED` (ТЗ 03_9 п. 4: «при отсутствии замечаний — „Загружен“»), становится
|
||
`CONFIRMED` от имени системы: `confirmed_by = NULL`, у формы `reviewed_by = NULL`, `audit_event(DOCUMENT_CONFIRM)` без
|
||
пользователя. Привязка к версиям и переход версии — тот же хук, что у ручного подтверждения, с автором
|
||
`stored_file.uploaded_by`. Вызывается в транзакции маппинга под заданием, файлом и документами (порядок блокировок D-5).
|
||
|
||
§14.3 п. 5 (design `quality-metrics` §4.2, AURORA-1759): на каждый подтверждённый документ пишется факт исхода
|
||
`quality_outcome_fact` с `outcome = AUTO_CONFIRMED`. Правок при автоподтверждении не бывает (D-3 блокирует формы
|
||
с нарушениями) — `primary_cause` всегда `WITHOUT_PARTICIPATION`, без вычисления приоритета §14.3 (он нужен только
|
||
общему случаю `CONFIRMED`, Task 14). Запись — в той же транзакции, без `flush`/`commit`.
|
||
"""
|
||
|
||
import datetime
|
||
from collections.abc import Sequence
|
||
from typing import Any
|
||
|
||
from sqlalchemy import func, select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from src.domain.quality_facts import build_outcome_payload
|
||
from src.models.enums import (
|
||
AuditAction,
|
||
AuditSection,
|
||
DocumentIssueKind,
|
||
DocumentStatus,
|
||
DocumentType,
|
||
IssueSource,
|
||
QualityCause,
|
||
QualityOutcome,
|
||
)
|
||
from src.models.files import Document, StoredFile
|
||
from src.models.osv import OsvEntry
|
||
from src.models.statements import FinancialStatementMeta
|
||
from src.repositories import statements as statements_repo
|
||
from src.repositories.quality_facts import insert_outcome_fact
|
||
from src.services import consolidation_documents
|
||
from src.services.audit import AuditService
|
||
from src.services.statements import FORM_TYPES, form_blocker
|
||
|
||
AUTO_CONFIRM_DESCRIPTION = "автоподтверждение: без замечаний"
|
||
OBJECT_TYPE = "document"
|
||
|
||
|
||
def transition_comment(document: Document) -> str:
|
||
"""Комментарий перехода версии: «автоподтверждение документа <тип> [<счёт>]» (design решение №6)."""
|
||
parts = ["автоподтверждение документа", document.document_type.value]
|
||
if document.osv_account_code:
|
||
parts.append(document.osv_account_code)
|
||
return " ".join(parts)
|
||
|
||
|
||
async def _outcome_fact_fields(
|
||
session: AsyncSession, document: Document, *, meta: FinancialStatementMeta | None
|
||
) -> dict[str, Any]:
|
||
"""`payload` и `issue_count_at_outcome` факта исхода `AUTO_CONFIRMED` (§14.3 п. 5, design §4.2).
|
||
|
||
Строение `payload` повторяет факт приёма (Task 5, `services/result_mapping.py`:
|
||
`_osv_intake_fields`/`_statement_intake_fields`), но как снимок ИСХОДА (`build_outcome_payload`, `fixes=()` —
|
||
правок при автоподтверждении не бывает, D-3). `issue_count_at_outcome` — ВСЕ нарушения документа на этот момент,
|
||
без фильтра по `kind`/`source` (в отличие от `warning_count` факта приёма, который фильтрует только
|
||
предупреждения сервиса)."""
|
||
issues = await statements_repo.issues(session, document.id)
|
||
if document.document_type is DocumentType.OSV:
|
||
row_count = (
|
||
await session.execute(
|
||
select(func.count()).select_from(OsvEntry).where(OsvEntry.document_id == document.id)
|
||
)
|
||
).scalar_one()
|
||
osv_warnings = [
|
||
issue.code
|
||
for issue in issues
|
||
if issue.kind is DocumentIssueKind.warning and issue.source is IssueSource.service
|
||
]
|
||
payload = build_outcome_payload(document, osv_warnings=osv_warnings, osv_row_count=row_count, fixes=())
|
||
elif meta is not None:
|
||
_, pairs = await statements_repo.load_form(session, meta.id)
|
||
lines = [line for line, _ in pairs]
|
||
values = [value for _, line_values in pairs for value in line_values]
|
||
payload = build_outcome_payload(
|
||
document, lines=lines, values=values, issues=issues, header_source=meta.header_source, fixes=()
|
||
)
|
||
else:
|
||
# DISCLOSURE/OTHER и форма без шапки (в текущем `map_file` не возникает — см. Task 5): снимок без построчных
|
||
# данных дешевле, чем прерванная транзакция маппинга
|
||
payload = build_outcome_payload(document, issues=issues, fixes=())
|
||
return {"issue_count_at_outcome": len(issues), "payload": payload}
|
||
|
||
|
||
async def auto_confirm(
|
||
session: AsyncSession, *, file: StoredFile, documents: Sequence[Document], now: datetime.datetime
|
||
) -> list[Document]:
|
||
"""Подтверждает документы `RECOGNIZED` файла (форма — только при `form_blocker is None`, D-3); `NEEDS_CHECK`
|
||
и прочие не трогает. Возвращает подтверждённые документы по возрастанию `id`."""
|
||
audit = AuditService(session)
|
||
confirmed: list[Document] = []
|
||
for document in sorted(documents, key=lambda item: item.id):
|
||
if document.status is not DocumentStatus.RECOGNIZED:
|
||
continue
|
||
meta = None
|
||
if document.document_type in FORM_TYPES:
|
||
meta = await statements_repo.get_meta(session, document.id, refresh=True)
|
||
if meta is not None and await form_blocker(session, document, meta) is not None:
|
||
continue
|
||
document.status = DocumentStatus.CONFIRMED
|
||
document.confirmed_by = None
|
||
document.confirmed_at = now
|
||
document.updated_at = now
|
||
if meta is not None:
|
||
meta.reviewed_by = None
|
||
meta.reviewed_at = now
|
||
await session.flush()
|
||
await consolidation_documents.on_document_confirmed(
|
||
session,
|
||
audit,
|
||
document=document,
|
||
changed_by=file.uploaded_by,
|
||
comment=transition_comment(document),
|
||
now=now,
|
||
)
|
||
await audit.record(
|
||
action=AuditAction.DOCUMENT_CONFIRM,
|
||
section=AuditSection.PRIMARY_DOCUMENTS,
|
||
object_type=OBJECT_TYPE,
|
||
object_id=document.id,
|
||
user_id=None,
|
||
description=AUTO_CONFIRM_DESCRIPTION,
|
||
before_value={"status": DocumentStatus.RECOGNIZED.value},
|
||
after_value={"status": DocumentStatus.CONFIRMED.value},
|
||
)
|
||
confirmed.append(document)
|
||
fields = await _outcome_fact_fields(session, document, meta=meta)
|
||
await insert_outcome_fact(
|
||
session,
|
||
document_id=document.id,
|
||
outcome=QualityOutcome.AUTO_CONFIRMED,
|
||
outcome_at=now,
|
||
primary_cause=QualityCause.WITHOUT_PARTICIPATION,
|
||
report_date=document.reporting_date,
|
||
confirmed_by=None,
|
||
**fields,
|
||
)
|
||
return confirmed
|