From 11b0216cffbb867c2228be728620771ecefd85bb Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Fri, 16 Oct 2026 18:48:50 +0400 Subject: [PATCH 1/2] =?UTF-8?q?feat(statements):=20parent=5Fcode=20=D1=83?= =?UTF-8?q?=20=D1=81=D1=82=D1=80=D0=BE=D0=BA=20=D1=84=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D1=8B=20=D0=91=D0=91/=D0=9E=D0=A4=D0=A0,=20=D0=BE=D0=B1=D1=89?= =?UTF-8?q?=D0=B0=D1=8F=20subline=5Fparents=20(#118,=20AURORA-1527)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Правило «подстрока относится к ближайшей четырёхзначной строке выше с теми же тремя цифрами, иначе к строке из первых четырёх цифр» вынесено в subline_parents; им пользуются проверка «сумма подстрок = родитель» и ответ GET /documents/{id}/statement (поле lines[].parent_code, вычисляется, в БД не хранится). Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- api/src/domain/statement_checks.py | 36 ++++++++++++------- api/src/schemas/statements.py | 11 ++++-- api/src/services/statements.py | 24 +++++++++---- api/tests/test_api_statements.py | 43 +++++++++++++++++++++++ api/tests/test_domain_statement_checks.py | 37 +++++++++++++++++++ 6 files changed, 131 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 051b398..79821a7 100644 --- a/README.md +++ b/README.md @@ -568,7 +568,7 @@ S3 (storage); без S3 — предупреждение в логе, цикл - **`GET /documents/{id}/statement`** — `{document, edition, header, columns[], lines[], checks}`: `header` — `inn`, `report_date`, `units` вида `{value, original_value, confidence, verified, modified}` (`confidence` есть у ИНН, у даты и единиц — `null`) и справочные поля формы - (`organization_name`, `okud`, `okpo`, …); `lines[].values[]` — суммы строкой, `is_modified`, `original_amount`; `checks` — + (`organization_name`, `okud`, `okpo`, …); `lines[].values[]` — суммы строкой, `is_modified`, `original_amount`; `lines[].parent_code` — у пятизначной подстроки код её четырёхзначной строки-родителя (правило проверки «сумма подстрок = родитель», `domain/statement_checks.subline_parents`: ближайшая выше строка с теми же тремя цифрами, иначе строка с кодом из первых четырёх цифр; родитель считается по строкам, участвующим в проверках, — достроенные из справочника пустые строки родителями не бывают), у остальных строк `null`; поле вычисляется на лету, в БД не хранится; `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`); в БД не пишется. diff --git a/api/src/domain/statement_checks.py b/api/src/domain/statement_checks.py index 3c6dca8..bc3be34 100644 --- a/api/src/domain/statement_checks.py +++ b/api/src/domain/statement_checks.py @@ -27,6 +27,7 @@ __all__ = [ "cross_form_check", "load_check_rules", "run_checks", + "subline_parents", ] Form = Literal["BALANCE", "INCOME"] @@ -136,13 +137,16 @@ def _holds(amounts: Mapping[str, Decimal], rule: RelationRule, tolerance: Decima return False, expected, plain -def _subline_rules(codes: Sequence[str], form: str) -> list[RelationRule]: - """«1110 = 11101 + …» для четырёхзначных кодов формы с пятизначными подстроками. Порядок строк в БД не - хранится (DBML порядка не знает): «по порядку» — это порядок кодов, в каком их отдаёт `statement_amounts` - (M-9 2b: подстрока относится к ближайшей четырёхзначной строке выше с теми же тремя цифрами).""" - low, high = CODE_RANGES[form] - parents = {code for code in codes if len(code) == _PARENT_LEN} - children: dict[str, list[str]] = {} +def subline_parents(codes: Sequence[str]) -> dict[str, str]: + """Родитель каждой пятизначной подстроки: `{код подстроки: код четырёхзначной строки}` (единое правило §7 для + проверки «сумма подстрок = родитель» и для поля `parent_code` ответа формы; AURORA-1527). + + Подстрока относится к ближайшей четырёхзначной строке выше с теми же тремя цифрами, иначе — к строке с кодом из + её первых четырёх цифр, если такая есть; без родителя в результат не попадает. Порядок строк в БД не хранится + (DBML порядка не знает): «выше» — порядок `codes`, и вызывающий передаёт его тот же, в каком `statement_amounts` + отдаёт коды проверке (по возрастанию кода; M-9 2b). Порядок результата — порядок подстрок в `codes`.""" + present = {code for code in codes if len(code) == _PARENT_LEN} + result: dict[str, str] = {} last_parent: str | None = None for code in codes: if len(code) == _PARENT_LEN: @@ -151,12 +155,18 @@ def _subline_rules(codes: Sequence[str], form: str) -> list[RelationRule]: if len(code) != _PARENT_LEN + 1: continue if last_parent is not None and last_parent[:_GROUP_LEN] == code[:_GROUP_LEN]: - parent = last_parent - elif code[:_PARENT_LEN] in parents: - parent = code[:_PARENT_LEN] - else: - continue - children.setdefault(parent, []).append(code) + result[code] = last_parent + elif code[:_PARENT_LEN] in present: + result[code] = code[:_PARENT_LEN] + return result + + +def _subline_rules(codes: Sequence[str], form: str) -> list[RelationRule]: + """«1110 = 11101 + …» для четырёхзначных кодов формы с пятизначными подстроками; родители — `subline_parents`.""" + low, high = CODE_RANGES[form] + children: dict[str, list[str]] = {} + for kid, parent in subline_parents(codes).items(): + children.setdefault(parent, []).append(kid) rules: list[RelationRule] = [] for parent, kids in children.items(): if parent.isdigit() and low <= int(parent) <= high: diff --git a/api/src/schemas/statements.py b/api/src/schemas/statements.py index 6ca6546..5e4fb02 100644 --- a/api/src/schemas/statements.py +++ b/api/src/schemas/statements.py @@ -7,7 +7,7 @@ design D-5, D-14). import datetime import uuid -from collections.abc import Sequence +from collections.abc import Mapping, Sequence from decimal import Decimal from typing import Annotated, Literal @@ -81,6 +81,9 @@ class LineOut(BaseModel): line_code_confidence: Decimal | None line_name: str | None line_name_confidence: Decimal | None + # код четырёхзначной строки-родителя у пятизначной подстроки (вычисляется по `subline_parents`, в БД не хранится); + # у остальных строк, в т. ч. достроенных из справочника, — None + parent_code: str | None values: list[ValueOut] @@ -177,6 +180,7 @@ def _lines_out( columns: Sequence[FinancialStatementColumn], lines: Sequence[tuple[FinancialStatementLine, Sequence[FinancialStatementValue]]], template: Sequence[TemplateLine], + parents: Mapping[str, str], ) -> list[LineOut]: """Строки БД с ячейками по всем колонкам + строки справочника, которых нет в форме (design D-3), по коду.""" column_index = {column.id: column.column_index for column in columns} @@ -194,6 +198,7 @@ def _lines_out( line_code_confidence=line.line_code_confidence, line_name=line.line_name, line_name_confidence=line.line_name_confidence, + parent_code=parents.get(line.line_code) if line.line_code is not None else None, values=cells, ) ) @@ -209,6 +214,7 @@ def _lines_out( line_code_confidence=None, line_name=item.name, line_name_confidence=None, + parent_code=None, values=[_empty_value(column) for column in columns], ) ) @@ -234,6 +240,7 @@ def statement_out( # noqa: PLR0913, PLR0917 - сборка ответа из ч *, edition: str | None, template: Sequence[TemplateLine], + parents: Mapping[str, str], ) -> StatementOut: """Ответ экрана проверки: документ, шапка, колонки, строки со значениями и проверки; строки достроены по справочнику редакции (D-3).""" @@ -278,7 +285,7 @@ def statement_out( # noqa: PLR0913, PLR0917 - сборка ответа из ч address=meta.address, ), columns=[ColumnOut.model_validate(column) for column in columns], - lines=_lines_out(columns, lines, template), + lines=_lines_out(columns, lines, template, parents), checks=ChecksOut( failed=[_failed(check) for check in report.failed], soft_failed=[_failed(check) for check in report.soft_failed], diff --git a/api/src/services/statements.py b/api/src/services/statements.py index 916dd8f..bd9ab93 100644 --- a/api/src/services/statements.py +++ b/api/src/services/statements.py @@ -15,7 +15,14 @@ from src.common.clock import utc_now from src.common.exceptions import ConflictError, DomainValidationError, NotFoundError from src.domain.osv_mapping import counterparty_check from src.domain.policy import VisibilityScope -from src.domain.statement_checks import ChecksReport, FailedCheck, cross_form_check, load_check_rules, run_checks +from src.domain.statement_checks import ( + ChecksReport, + FailedCheck, + cross_form_check, + load_check_rules, + run_checks, + subline_parents, +) from src.domain.statement_lines import TemplateLine, load_line_catalog from src.domain.statement_mapping import edition_for, quarter_of, statement_period, statement_status, values_ok from src.models.directories import LegalEntity @@ -112,13 +119,16 @@ class StatementService: async def checks_report( self, document: Document, meta: FinancialStatementMeta - ) -> tuple[ChecksReport, FailedCheck | None]: - """Проверки §7 по текущим суммам формы и мягкая межформенная проверка (D-5).""" + ) -> tuple[ChecksReport, FailedCheck | None, dict[str, str]]: + """Проверки §7 по текущим суммам формы, мягкая межформенная проверка (D-5) и родители подстрок + (`parent_code` ответа; AURORA-1527). Родители считаются по тем же кодам и в том же порядке + (`statement_amounts`), что и проверка «сумма подстрок = родитель», — ответ и проверка не расходятся.""" rules = load_check_rules() column_indexes, amounts = await repo.statement_amounts(self._session, meta.id) report = run_checks(document.document_type.value, amounts, column_indexes, rules) + parents = subline_parents([code for code, _ in amounts]) balance_1370, income_2400 = await repo.pair_amounts(self._session, document) - return report, cross_form_check(balance_1370, income_2400, rules.tolerance) + return report, cross_form_check(balance_1370, income_2400, rules.tolerance), parents async def get(self, document_id: uuid.UUID) -> schemas.StatementOut: document, meta = await self._visible_statement(document_id) @@ -128,10 +138,12 @@ class StatementService: """Ответ формы по текущему состоянию БД, достроенный по справочнику редакции (design D-3; используется и ответами правки/акцепта). В БД ничего не пишет.""" columns, lines = await repo.load_form(self._session, meta.id) - report, cross = await self.checks_report(document, meta) + report, cross, parents = await self.checks_report(document, meta) edition = edition_for(document.reporting_date) template = load_line_catalog().lines(edition, document.document_type.value) - return schemas.statement_out(document, meta, columns, lines, report, cross, edition=edition, template=template) + return schemas.statement_out( + document, meta, columns, lines, report, cross, edition=edition, template=template, parents=parents + ) async def hard_failures(self, document: Document, meta: FinancialStatementMeta) -> tuple[FailedCheck, ...]: """Нарушенные жёсткие проверки §7; при акцепте — пустой кортеж (используется и подтверждением формы).""" diff --git a/api/tests/test_api_statements.py b/api/tests/test_api_statements.py index 03ece4a..b89b3fd 100644 --- a/api/tests/test_api_statements.py +++ b/api/tests/test_api_statements.py @@ -40,6 +40,8 @@ KA_RF = "ka_rf@armka.local" RKA = "rka@armka.local" # только DOCUMENT_VIEW ROSTOV = "Ростовский региональный филиал" MEZH_BALANCE = GOLDEN_STATEMENTS / "mezheninovskaya" / "2025-Q3" / "1_Форма1_бух_09.json" +# ББ с пятизначными строками детализации +UNAGRANDE_BALANCE = GOLDEN_STATEMENTS / "unagrande-group" / "2025-Q3" / "ББ. Лист 1 3кв.25.json" MEZH_INCOME = GOLDEN_STATEMENTS / "mezheninovskaya" / "2025-Q3" / "1_Форма2_бух_09.json" MEZH_BALANCE_LINES = 37 MEZH_BALANCE_TEMPLATE_ONLY = {"1215"} # коды fsbu4, которых нет в файле Межениновской (файл по форме 66н) @@ -167,6 +169,7 @@ async def test_get_statement_shape_and_decimal_strings( "line_code_confidence", "line_name", "line_name_confidence", + "parent_code", "values", } value = next(v for v in line["values"] if v["amount"] is not None) @@ -189,6 +192,46 @@ async def test_get_statement_shape_and_decimal_strings( assert [v["column_index"] for v in line["values"]] == sorted(v["column_index"] for v in line["values"]) +async def test_get_statement_parent_code_of_sublines( + seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory +) -> None: + """AURORA-1527: у пятизначной подстроки `parent_code` — её четырёхзначная строка, у остальных (в т. ч. шаблонных) — + `null`; родитель тот же, что у проверки «сумма подстрок = родитель».""" + document = await form(seeded, fixtures_dir, UNAGRANDE_BALANCE) + client = await auth_client(KA_RF) + body = (await client.get(f"/documents/{document.id}/statement")).json() + parents = {line["line_code"]: line["parent_code"] for line in body["lines"]} + assert parents["11101"] == "1110" and parents["11201"] == "1120" and parents["11510"] == "1150" + assert parents["11904"] == "1190" and parents["12101"] == "1210" + assert all((parent is not None) == (len(code) == 5) for code, parent in parents.items()) # noqa: PLR2004 + assert all(line["parent_code"] is None for line in body["lines"] if line["origin"] == "template") + + +async def test_get_statement_parent_code_matches_subline_check( + seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory +) -> None: + """Нарушенная «1110 = 11101 + …» строится по тем же подстрокам, что отдаёт `parent_code`.""" + document = await form(seeded, fixtures_dir, UNAGRANDE_BALANCE) + await set_amount(seeded, document, "1110", Decimal("999999"), current=True) + client = await auth_client(KA_RF) + body = (await client.get(f"/documents/{document.id}/statement")).json() + kids = sorted(line["line_code"] for line in body["lines"] if line["parent_code"] == "1110") + assert kids == ["11101", "11102", "11103"] + assert f"1110 = {' + '.join(kids)}" in {item["relation"] for item in body["checks"]["soft_failed"]} + + +async def test_get_statement_orphan_subline_has_no_parent( + seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory +) -> None: + """Подстрока без четырёхзначной строки в форме остаётся без родителя (и не попадает в проверку).""" + document = await form(seeded, fixtures_dir, UNAGRANDE_BALANCE) + await drop_line(seeded, document, "1170") + client = await auth_client(KA_RF) + body = (await client.get(f"/documents/{document.id}/statement")).json() + parents = {line["line_code"]: line["parent_code"] for line in body["lines"] if line["origin"] == "source"} + assert parents["11701"] is None + + async def test_get_income_statement(seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory) -> None: document = await form(seeded, fixtures_dir, MEZH_INCOME) client = await auth_client(KA_RF) diff --git a/api/tests/test_domain_statement_checks.py b/api/tests/test_domain_statement_checks.py index 295de02..a4875ee 100644 --- a/api/tests/test_domain_statement_checks.py +++ b/api/tests/test_domain_statement_checks.py @@ -10,6 +10,7 @@ from src.domain.statement_checks import ( cross_form_check, load_check_rules, run_checks, + subline_parents, ) D = Decimal @@ -208,3 +209,39 @@ def test_target_with_empty_cell_counts_as_zero_and_is_checked() -> None: (0, D("0"), D("900")), (1, D("0"), D("900")), ] + + +def test_subline_parents_nearest_parent_above_with_same_group() -> None: + # 12310 и 12320 относятся к 1230 (ближайшая выше с теми же тремя цифрами), а не к 1231 + codes = ["1231", "1230", "12310", "12320"] + assert subline_parents(codes) == {"12310": "1230", "12320": "1230"} + + +def test_subline_parents_fallback_to_four_digit_prefix() -> None: + # выше подстроки нет родителя той же группы — родитель по первым четырём цифрам, если такая строка есть + assert subline_parents(["11101", "1110"]) == {"11101": "1110"} + assert subline_parents(["1100", "1110", "1120", "11101"]) == {"11101": "1110"} + + +def test_subline_parents_orphans_and_non_sublines_are_absent() -> None: + assert subline_parents(["1100", "11101", "1700", "123"]) == {} + assert subline_parents([]) == {} + + +def test_subline_parents_follow_given_order() -> None: + # порядок задаёт вызывающий (порядок кодов `statement_amounts`): «ближайшая выше» считается по нему + assert subline_parents(["1230", "1231", "12310"]) == {"12310": "1231"} + assert subline_parents(["1231", "1230", "12310"]) == {"12310": "1230"} + + +def test_run_checks_and_subline_parents_agree() -> None: + # проверка «сумма подстрок = родитель» строится по тем же родителям, что и `subline_parents` + codes = ["1231", "1230", "12310", "12320", "1110", "11101"] + amounts = {"11101": "5", "1230": "10"} + report = run_checks("BALANCE", _lines(*((code, {0: amounts.get(code, "1")}) for code in codes)), [0], RULES) + parents = subline_parents(codes) + expected = { + f"{parent} = {' + '.join(kid for kid, owner in parents.items() if owner == parent)}" + for parent in set(parents.values()) + } + assert {item.relation for item in report.soft_failed} == expected From 7ab16388f20d4a57a835bad43dfec592b5b76d0d Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Fri, 16 Oct 2026 19:26:36 +0400 Subject: [PATCH 2/2] =?UTF-8?q?feat(statement=5Fchecks):=20=C2=AB=D1=80?= =?UTF-8?q?=D0=BE=D0=B4=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=20=3D=20=CE=A3=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D1=81=D1=82=D1=80=D0=BE=D0=BA=C2=BB=20=D0=BD?= =?UTF-8?q?=D0=B5=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D1=8F=D0=B5=D1=82?= =?UTF-8?q?=D1=81=D1=8F=20=D0=BF=D1=80=D0=B8=20=D0=BF=D1=83=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D0=B9=20=D0=B4=D0=B5=D1=82=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20(#118,=20AURORA-1527)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit По колонке соотношение считается, только если хотя бы одна подстрока родителя имеет значение (amount не null; ноль — значение). Прочие соотношения и «пустая сумма = 0» не менялись. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- api/src/domain/statement_checks.py | 8 ++++++- api/tests/test_api_statements.py | 12 +++++++++++ api/tests/test_domain_statement_checks.py | 26 +++++++++++++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 79821a7..b4dc3d7 100644 --- a/README.md +++ b/README.md @@ -568,7 +568,7 @@ S3 (storage); без S3 — предупреждение в логе, цикл - **`GET /documents/{id}/statement`** — `{document, edition, header, columns[], lines[], checks}`: `header` — `inn`, `report_date`, `units` вида `{value, original_value, confidence, verified, modified}` (`confidence` есть у ИНН, у даты и единиц — `null`) и справочные поля формы - (`organization_name`, `okud`, `okpo`, …); `lines[].values[]` — суммы строкой, `is_modified`, `original_amount`; `lines[].parent_code` — у пятизначной подстроки код её четырёхзначной строки-родителя (правило проверки «сумма подстрок = родитель», `domain/statement_checks.subline_parents`: ближайшая выше строка с теми же тремя цифрами, иначе строка с кодом из первых четырёх цифр; родитель считается по строкам, участвующим в проверках, — достроенные из справочника пустые строки родителями не бывают), у остальных строк `null`; поле вычисляется на лету, в БД не хранится; `checks` — + (`organization_name`, `okud`, `okpo`, …); `lines[].values[]` — суммы строкой, `is_modified`, `original_amount`; `lines[].parent_code` — у пятизначной подстроки код её четырёхзначной строки-родителя (правило проверки «сумма подстрок = родитель», `domain/statement_checks.subline_parents`: ближайшая выше строка с теми же тремя цифрами, иначе строка с кодом из первых четырёх цифр; родитель считается по строкам, участвующим в проверках, — достроенные из справочника пустые строки родителями не бывают), у остальных строк `null`; поле вычисляется на лету, в БД не хранится; мягкая проверка «сумма подстрок = родитель» по колонке выполняется, только если хотя бы одна подстрока родителя в ней непустая (`amount` не `null`; ноль — значение), иначе не проверяется; `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`); в БД не пишется. diff --git a/api/src/domain/statement_checks.py b/api/src/domain/statement_checks.py index bc3be34..7b1125d 100644 --- a/api/src/domain/statement_checks.py +++ b/api/src/domain/statement_checks.py @@ -5,7 +5,8 @@ строки); отсутствующий код и пустая сумма в правой части — 0; правая часть считается как записано и с `abs_terms` по модулю, выполнено, если хоть один вариант в пределах `tolerance`; жёсткое нарушение не больше `rounding_tolerance_percent` от наибольшего по модулю числа соотношения — мягкое; «сумма пятизначных подстрок = -четырёхзначный родитель» — мягкая. Результат не хранится (DBML). +четырёхзначный родитель» — мягкая и только если в колонке есть хоть одна непустая подстрока (AURORA-1527). +Результат не хранится (DBML). """ import functools @@ -199,9 +200,14 @@ def run_checks( # присутствие цели = наличие строки формы; пустая сумма (None) = 0, как `_amounts` в tabular-parser: оба # производителя отдают значение на каждую колонку каждой кодированной строки (пустая ячейка — amount=None) amounts = {code: (values.get(column_index) or _ZERO) for code, values in lines} + filled = {code: values.get(column_index) is not None for code, values in lines} for rule in [*explicit, *sublines]: if rule not in sublines and rule.target not in amounts: continue + # AURORA-1527: детализации нет (все подстроки родителя в колонке пусты, `None`) — «родитель = Σ подстрок» + # по колонке не проверяется; ноль — значение. Правило кода, словарь не меняется. + if rule in sublines and all(filled.get(code) is not True for _, code in rule.terms): + continue holds, expected, actual = _holds(amounts, rule, rules.tolerance) if holds: continue diff --git a/api/tests/test_api_statements.py b/api/tests/test_api_statements.py index b89b3fd..7254481 100644 --- a/api/tests/test_api_statements.py +++ b/api/tests/test_api_statements.py @@ -213,6 +213,7 @@ async def test_get_statement_parent_code_matches_subline_check( """Нарушенная «1110 = 11101 + …» строится по тем же подстрокам, что отдаёт `parent_code`.""" document = await form(seeded, fixtures_dir, UNAGRANDE_BALANCE) await set_amount(seeded, document, "1110", Decimal("999999"), current=True) + await set_amount(seeded, document, "11101", Decimal("1"), current=True) # детализация есть — проверка работает client = await auth_client(KA_RF) body = (await client.get(f"/documents/{document.id}/statement")).json() kids = sorted(line["line_code"] for line in body["lines"] if line["parent_code"] == "1110") @@ -220,6 +221,17 @@ async def test_get_statement_parent_code_matches_subline_check( assert f"1110 = {' + '.join(kids)}" in {item["relation"] for item in body["checks"]["soft_failed"]} +async def test_get_statement_no_subline_warnings_without_detail( + seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory +) -> None: + """AURORA-1527: все подстроки родителя пусты — «1150 = 11501 + …», «1170 = 11701», «1210 = 12101» не выдаются.""" + document = await form(seeded, fixtures_dir, UNAGRANDE_BALANCE) + client = await auth_client(KA_RF) + body = (await client.get(f"/documents/{document.id}/statement")).json() + soft = [item["relation"] for item in body["checks"]["soft_failed"]] + assert not any(relation.startswith(("1150 =", "1170 =", "1210 =")) for relation in soft), soft + + async def test_get_statement_orphan_subline_has_no_parent( seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory ) -> None: diff --git a/api/tests/test_domain_statement_checks.py b/api/tests/test_domain_statement_checks.py index a4875ee..ca77c8e 100644 --- a/api/tests/test_domain_statement_checks.py +++ b/api/tests/test_domain_statement_checks.py @@ -245,3 +245,29 @@ def test_run_checks_and_subline_parents_agree() -> None: for parent in set(parents.values()) } assert {item.relation for item in report.soft_failed} == expected + + +def test_subline_check_skipped_when_all_sublines_empty_in_column() -> None: + # AURORA-1527: детализации нет (все подстроки пусты) — «1150 = 11501 + …» по колонке не проверяется + lines = _lines(("1150", {0: "90", 1: "90"}), ("11501", {0: None, 1: None}), ("11502", {0: None, 1: None})) + assert _by_relation(run_checks("BALANCE", lines, [0, 1], RULES).soft_failed, "1150 = 11501 + 11502") == [] + + +def test_subline_check_is_per_column() -> None: + # в колонке 0 подстроки пусты (не проверяется), в колонке 1 заполнены и не сходятся (нарушение) + lines = _lines(("1150", {0: "90", 1: "90"}), ("11501", {0: None, 1: "10"}), ("11502", {0: None, 1: None})) + failed = _by_relation(run_checks("BALANCE", lines, [0, 1], RULES).soft_failed, "1150 = 11501 + 11502") + assert [(f.column_index, f.expected, f.actual) for f in failed] == [(1, D("90"), D("10"))] + + +def test_subline_zero_is_a_value() -> None: + # ноль — значение: подстроки с нулями проверяются как раньше + lines = _lines(("1150", {0: "90"}), ("11501", {0: "0"}), ("11502", {0: None})) + assert len(_by_relation(run_checks("BALANCE", lines, [0], RULES).soft_failed, "1150 = 11501 + 11502")) == 1 + + +def test_empty_sublines_do_not_affect_other_relations() -> None: + # прочие соотношения §7 не затронуты: пустая цель 1200 при ненулевых слагаемых — по-прежнему жёсткое нарушение + lines = _lines(("1210", {0: "5"}), ("12101", {0: None}), ("1200", {0: None}), ("1600", {0: "5"})) + report = run_checks("BALANCE", lines, [0], RULES) + assert [f.relation for f in report.failed if f.relation.startswith("1200 =")] != []