From 1ab7f2238d6ce86ee2010ff602a0038cfb4156f9 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 18 Oct 2026 18:05:28 +0400 Subject: [PATCH] =?UTF-8?q?chore(statements):=20=D0=BA=D0=BE=D0=BC=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=B0=D1=80=D0=B8=D0=B8,=20README,=20=D1=82?= =?UTF-8?q?=D0=B8=D0=BF=D1=8B,=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=B8=D1=82=D0=BE=D0=B3=D0=B0=D0=BC=20=D1=84=D0=B8?= =?UTF-8?q?=D0=BD=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B2=D1=8C=D1=8E=20(M-1,=20M-2,=20M-3,=20M-5,=20M-7,=20M-9,?= =?UTF-8?q?=20T2.1,=20T4.5,=20T10.1,=20T11.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W --- api/alembic/versions/0007_dbml_v5_19.py | 1 + api/src/domain/statement_status.py | 5 +-- api/src/repositories/statements.py | 3 +- api/tests/test_auto_confirm.py | 39 +++++++++++++++++++++++ api/tests/test_domain_statement_status.py | 5 +++ 5 files changed, 50 insertions(+), 3 deletions(-) diff --git a/api/alembic/versions/0007_dbml_v5_19.py b/api/alembic/versions/0007_dbml_v5_19.py index c293f8d..ac7e39d 100644 --- a/api/alembic/versions/0007_dbml_v5_19.py +++ b/api/alembic/versions/0007_dbml_v5_19.py @@ -262,6 +262,7 @@ def _log_lost_values() -> None: def downgrade() -> None: + """Откат v5.19. Нужно онлайн-соединение (проверки читают данные): offline-режим `--sql` не поддерживается (T2.1).""" _check_downgrade_allowed() _log_lost_values() op.execute(f"DROP TABLE {S}.document_issue") diff --git a/api/src/domain/statement_status.py b/api/src/domain/statement_status.py index 0181b30..ece8a03 100644 --- a/api/src/domain/statement_status.py +++ b/api/src/domain/statement_status.py @@ -51,13 +51,14 @@ class NameCheckLine: def name_mismatches(lines: Iterable[NameCheckLine], catalog: LineCatalog, form: str) -> list[Hashable]: """Ключи строк `ok` с неуверенным кодом (`line_code_confidence < 100`), без закреплённого кода (`code_fixed`), - чьё наименование не совпало ни с одним справочным именем кода (Р-16). Кода нет в словаре — несовпадение.""" + чьё наименование не совпало ни с одним справочным именем кода (Р-16). Кода нет в словаре — несовпадение. + Строка без уверенности (`None`) считается уверенной и не проверяется: спека требует `confidence < 100`.""" result: list[Hashable] = [] for line in lines: if line.line_status != StatementLineStatus.ok or line.code_fixed: continue confidence = line.line_code_confidence - if confidence is None or confidence >= _CONFIDENT: + if confidence is None or confidence >= _CONFIDENT: # None — уверенной (T10.1) continue if not line.line_code or not names_match(line.line_name, catalog.names(form, line.line_code)): result.append(line.key) diff --git a/api/src/repositories/statements.py b/api/src/repositories/statements.py index ecd4c71..d72c52f 100644 --- a/api/src/repositories/statements.py +++ b/api/src/repositories/statements.py @@ -10,6 +10,7 @@ from typing import Any from sqlalchemy import delete, exists, func, insert, or_, select, update from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.sql.expression import ScalarSelect from src.domain.statement_aggregate import AggLine, CodeAggregate, aggregate_by_code from src.domain.statement_issues import IssueDraft @@ -213,7 +214,7 @@ async def delete_documents_statements(session: AsyncSession, document_ids: Seque await _delete_statements(session, list(document_ids)) -async def _delete_statements(session: AsyncSession, documents: Any) -> None: +async def _delete_statements(session: AsyncSession, documents: ScalarSelect[uuid.UUID] | list[uuid.UUID]) -> None: await session.execute(delete(DocumentIssue).where(DocumentIssue.document_id.in_(documents))) metas = select(FinancialStatementMeta.id).where(FinancialStatementMeta.document_id.in_(documents)).scalar_subquery() lines = select(FinancialStatementLine.id).where(FinancialStatementLine.meta_id.in_(metas)).scalar_subquery() diff --git a/api/tests/test_auto_confirm.py b/api/tests/test_auto_confirm.py index 05ebed8..45c5e07 100644 --- a/api/tests/test_auto_confirm.py +++ b/api/tests/test_auto_confirm.py @@ -259,3 +259,42 @@ async def test_manual_confirm_of_auto_confirmed_is_409(seeded: AsyncSession, aut assert response.status_code == HTTP_CONFLICT, response.text assert response.json()["detail"]["code"] == "ALREADY_CONFIRMED" assert len(await _confirm_events(seeded)) == 1 + + +async def _synthetic_disclosure(session: AsyncSession, fixtures_dir: Path, *, office: RegionalOffice) -> Document: + """RECOGNIZED `DISCLOSURE` из синтетического снимка st06 (T11.1: форм-проверки к не-форме не применяются).""" + import json # noqa: PLC0415 - нужен только здесь + + path = fixtures_dir / GOLDEN_STATEMENTS / "synthetic" / "st06_header_variants.json" + data = json.loads(path.read_text("utf-8")) + inn = next((d["header"].get("inn") for d in data["documents"] if d.get("header", {}).get("inn")), None) + owner = await make_legal_entity(session, name="ООО Синтетика", inn=inn, office=office) + documents = await map_golden_snapshot(session, entity=owner, path=path, uploaded_by=None) + disclosure = next(d for d in documents if d.document_type is DocumentType.DISCLOSURE) + assert disclosure.status is DocumentStatus.RECOGNIZED + return disclosure + + +@pytest.mark.fixtures +async def test_recognized_disclosure_is_auto_confirmed(seeded: AsyncSession, fixtures_dir: Path) -> None: + office = (await seeded.execute(select(RegionalOffice).where(RegionalOffice.name == ROSTOV))).scalar_one() + disclosure = await _synthetic_disclosure(seeded, fixtures_dir, office=office) + file = await seeded.get(StoredFile, disclosure.stored_file_id) + assert file is not None + + assert await auto_confirm(seeded, file=file, documents=[disclosure], now=utc_now()) == [disclosure] + assert disclosure.status is DocumentStatus.CONFIRMED + + +@pytest.mark.fixtures +async def test_recognized_disclosure_is_manually_confirmable( + seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory +) -> None: + office = (await seeded.execute(select(RegionalOffice).where(RegionalOffice.name == ROSTOV))).scalar_one() + disclosure = await _synthetic_disclosure(seeded, fixtures_dir, office=office) + client = await auth_client(KA_RF) + + response = await client.post(f"/documents/{disclosure.id}/confirm") + + assert response.status_code == 200, response.text # noqa: PLR2004 + assert response.json()["status"] == "CONFIRMED" diff --git a/api/tests/test_domain_statement_status.py b/api/tests/test_domain_statement_status.py index 4c7d758..603cfde 100644 --- a/api/tests/test_domain_statement_status.py +++ b/api/tests/test_domain_statement_status.py @@ -120,3 +120,8 @@ def test_name_mismatches() -> None: _line(10, None, None, "Без кода", status="unreadable"), ] assert name_mismatches(lines, CATALOG, "BALANCE") == [1, 3, 8, 9] + + +def test_name_mismatches_missing_confidence_is_confident() -> None: + """T10.1: `line_code_confidence = None` не даёт `NAME_MISMATCH` (спека: `confidence < 100`).""" + assert name_mismatches([_line(1, "1120", None, "Чужое имя")], CATALOG, "BALANCE") == []