Merge pull request 'parent_code у строк формы ББ/ОФР, общая subline_parents (#118, AURORA-1527)' (#119) from feat/118-subline-parent into main

This commit is contained in:
aep 2026-10-16 18:35:17 +03:00
commit 423929ce64
6 changed files with 176 additions and 23 deletions

View File

@ -568,7 +568,7 @@ S3 (storage); без S3 — предупреждение в логе, цикл
- **`GET /documents/{id}/statement`** — `{document, edition, header, columns[], lines[], checks}`: `header` — `inn`, - **`GET /documents/{id}/statement`** — `{document, edition, header, columns[], lines[], checks}`: `header` — `inn`,
`report_date`, `units` вида `{value, original_value, confidence, verified, modified}` (`confidence` есть у ИНН, у даты и единиц — `null`) и справочные поля формы `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`; поле вычисляется на лету, в БД не хранится; мягкая проверка «сумма подстрок = родитель» по колонке выполняется, только если хотя бы одна подстрока родителя в ней непустая (`amount` не `null`; ноль — значение), иначе не проверяется; `checks` —
`{failed, soft_failed, cross_form, accepted}` (нарушение — `{column_index, relation, expected, actual}`, у межформенной `{failed, soft_failed, cross_form, accepted}` (нарушение — `{column_index, relation, expected, actual}`, у межформенной
проверки `column_index = -1`). Не форма (ОСВ, расшифровка, ББ/ОФР без данных формы — созданные до Среза 3b) — 409 `DOCUMENT_NOT_STATEMENT`. проверки `column_index = -1`). Не форма (ОСВ, расшифровка, ББ/ОФР без данных формы — созданные до Среза 3b) — 409 `DOCUMENT_NOT_STATEMENT`.
Подтверждённая форма читается. Форма достраивается по справочнику строк редакции (`api/rules/statement_lines.yaml`): недостающие строки (`id = null`, `origin = template`) и ячейки (`id = null`); в БД не пишется. Подтверждённая форма читается. Форма достраивается по справочнику строк редакции (`api/rules/statement_lines.yaml`): недостающие строки (`id = null`, `origin = template`) и ячейки (`id = null`); в БД не пишется.

View File

@ -5,7 +5,8 @@
строки); отсутствующий код и пустая сумма в правой части — 0; правая часть считается как записано и с `abs_terms` по строки); отсутствующий код и пустая сумма в правой части — 0; правая часть считается как записано и с `abs_terms` по
модулю, выполнено, если хоть один вариант в пределах `tolerance`; жёсткое нарушение не больше модулю, выполнено, если хоть один вариант в пределах `tolerance`; жёсткое нарушение не больше
`rounding_tolerance_percent` от наибольшего по модулю числа соотношения — мягкое; «сумма пятизначных подстрок = `rounding_tolerance_percent` от наибольшего по модулю числа соотношения — мягкое; «сумма пятизначных подстрок =
четырёхзначный родитель» — мягкая. Результат не хранится (DBML). четырёхзначный родитель» — мягкая и только если в колонке есть хоть одна непустая подстрока (AURORA-1527).
Результат не хранится (DBML).
""" """
import functools import functools
@ -27,6 +28,7 @@ __all__ = [
"cross_form_check", "cross_form_check",
"load_check_rules", "load_check_rules",
"run_checks", "run_checks",
"subline_parents",
] ]
Form = Literal["BALANCE", "INCOME"] Form = Literal["BALANCE", "INCOME"]
@ -136,13 +138,16 @@ def _holds(amounts: Mapping[str, Decimal], rule: RelationRule, tolerance: Decima
return False, expected, plain return False, expected, plain
def _subline_rules(codes: Sequence[str], form: str) -> list[RelationRule]: def subline_parents(codes: Sequence[str]) -> dict[str, str]:
"""«1110 = 11101 + …» для четырёхзначных кодов формы с пятизначными подстроками. Порядок строк в БД не """Родитель каждой пятизначной подстроки: `{код подстроки: код четырёхзначной строки}` (единое правило §7 для
хранится (DBML порядка не знает): «по порядку» — это порядок кодов, в каком их отдаёт `statement_amounts` проверки «сумма подстрок = родитель» и для поля `parent_code` ответа формы; AURORA-1527).
(M-9 2b: подстрока относится к ближайшей четырёхзначной строке выше с теми же тремя цифрами)."""
low, high = CODE_RANGES[form] Подстрока относится к ближайшей четырёхзначной строке выше с теми же тремя цифрами, иначе — к строке с кодом из
parents = {code for code in codes if len(code) == _PARENT_LEN} её первых четырёх цифр, если такая есть; без родителя в результат не попадает. Порядок строк в БД не хранится
children: dict[str, list[str]] = {} (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 last_parent: str | None = None
for code in codes: for code in codes:
if len(code) == _PARENT_LEN: if len(code) == _PARENT_LEN:
@ -151,12 +156,18 @@ def _subline_rules(codes: Sequence[str], form: str) -> list[RelationRule]:
if len(code) != _PARENT_LEN + 1: if len(code) != _PARENT_LEN + 1:
continue continue
if last_parent is not None and last_parent[:_GROUP_LEN] == code[:_GROUP_LEN]: if last_parent is not None and last_parent[:_GROUP_LEN] == code[:_GROUP_LEN]:
parent = last_parent result[code] = last_parent
elif code[:_PARENT_LEN] in parents: elif code[:_PARENT_LEN] in present:
parent = code[:_PARENT_LEN] result[code] = code[:_PARENT_LEN]
else: return result
continue
children.setdefault(parent, []).append(code)
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] = [] rules: list[RelationRule] = []
for parent, kids in children.items(): for parent, kids in children.items():
if parent.isdigit() and low <= int(parent) <= high: if parent.isdigit() and low <= int(parent) <= high:
@ -189,9 +200,14 @@ def run_checks(
# присутствие цели = наличие строки формы; пустая сумма (None) = 0, как `_amounts` в tabular-parser: оба # присутствие цели = наличие строки формы; пустая сумма (None) = 0, как `_amounts` в tabular-parser: оба
# производителя отдают значение на каждую колонку каждой кодированной строки (пустая ячейка — amount=None) # производителя отдают значение на каждую колонку каждой кодированной строки (пустая ячейка — amount=None)
amounts = {code: (values.get(column_index) or _ZERO) for code, values in lines} 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]: for rule in [*explicit, *sublines]:
if rule not in sublines and rule.target not in amounts: if rule not in sublines and rule.target not in amounts:
continue 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) holds, expected, actual = _holds(amounts, rule, rules.tolerance)
if holds: if holds:
continue continue

View File

@ -7,7 +7,7 @@ design D-5, D-14).
import datetime import datetime
import uuid import uuid
from collections.abc import Sequence from collections.abc import Mapping, Sequence
from decimal import Decimal from decimal import Decimal
from typing import Annotated, Literal from typing import Annotated, Literal
@ -81,6 +81,9 @@ class LineOut(BaseModel):
line_code_confidence: Decimal | None line_code_confidence: Decimal | None
line_name: str | None line_name: str | None
line_name_confidence: Decimal | None line_name_confidence: Decimal | None
# код четырёхзначной строки-родителя у пятизначной подстроки (вычисляется по `subline_parents`, в БД не хранится);
# у остальных строк, в т. ч. достроенных из справочника, — None
parent_code: str | None
values: list[ValueOut] values: list[ValueOut]
@ -177,6 +180,7 @@ def _lines_out(
columns: Sequence[FinancialStatementColumn], columns: Sequence[FinancialStatementColumn],
lines: Sequence[tuple[FinancialStatementLine, Sequence[FinancialStatementValue]]], lines: Sequence[tuple[FinancialStatementLine, Sequence[FinancialStatementValue]]],
template: Sequence[TemplateLine], template: Sequence[TemplateLine],
parents: Mapping[str, str],
) -> list[LineOut]: ) -> list[LineOut]:
"""Строки БД с ячейками по всем колонкам + строки справочника, которых нет в форме (design D-3), по коду.""" """Строки БД с ячейками по всем колонкам + строки справочника, которых нет в форме (design D-3), по коду."""
column_index = {column.id: column.column_index for column in columns} 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_code_confidence=line.line_code_confidence,
line_name=line.line_name, line_name=line.line_name,
line_name_confidence=line.line_name_confidence, 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, values=cells,
) )
) )
@ -209,6 +214,7 @@ def _lines_out(
line_code_confidence=None, line_code_confidence=None,
line_name=item.name, line_name=item.name,
line_name_confidence=None, line_name_confidence=None,
parent_code=None,
values=[_empty_value(column) for column in columns], values=[_empty_value(column) for column in columns],
) )
) )
@ -234,6 +240,7 @@ def statement_out( # noqa: PLR0913, PLR0917 - сборка ответа из ч
*, *,
edition: str | None, edition: str | None,
template: Sequence[TemplateLine], template: Sequence[TemplateLine],
parents: Mapping[str, str],
) -> StatementOut: ) -> StatementOut:
"""Ответ экрана проверки: документ, шапка, колонки, строки со значениями и проверки; строки достроены по """Ответ экрана проверки: документ, шапка, колонки, строки со значениями и проверки; строки достроены по
справочнику редакции (D-3).""" справочнику редакции (D-3)."""
@ -278,7 +285,7 @@ def statement_out( # noqa: PLR0913, PLR0917 - сборка ответа из ч
address=meta.address, address=meta.address,
), ),
columns=[ColumnOut.model_validate(column) for column in columns], columns=[ColumnOut.model_validate(column) for column in columns],
lines=_lines_out(columns, lines, template), lines=_lines_out(columns, lines, template, parents),
checks=ChecksOut( checks=ChecksOut(
failed=[_failed(check) for check in report.failed], failed=[_failed(check) for check in report.failed],
soft_failed=[_failed(check) for check in report.soft_failed], soft_failed=[_failed(check) for check in report.soft_failed],

View File

@ -15,7 +15,14 @@ from src.common.clock import utc_now
from src.common.exceptions import ConflictError, DomainValidationError, NotFoundError from src.common.exceptions import ConflictError, DomainValidationError, NotFoundError
from src.domain.osv_mapping import counterparty_check from src.domain.osv_mapping import counterparty_check
from src.domain.policy import VisibilityScope 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_lines import TemplateLine, load_line_catalog
from src.domain.statement_mapping import edition_for, quarter_of, statement_period, statement_status, values_ok from src.domain.statement_mapping import edition_for, quarter_of, statement_period, statement_status, values_ok
from src.models.directories import LegalEntity from src.models.directories import LegalEntity
@ -112,13 +119,16 @@ class StatementService:
async def checks_report( async def checks_report(
self, document: Document, meta: FinancialStatementMeta self, document: Document, meta: FinancialStatementMeta
) -> tuple[ChecksReport, FailedCheck | None]: ) -> tuple[ChecksReport, FailedCheck | None, dict[str, str]]:
"""Проверки §7 по текущим суммам формы и мягкая межформенная проверка (D-5).""" """Проверки §7 по текущим суммам формы, мягкая межформенная проверка (D-5) и родители подстрок
(`parent_code` ответа; AURORA-1527). Родители считаются по тем же кодам и в том же порядке
(`statement_amounts`), что и проверка «сумма подстрок = родитель», — ответ и проверка не расходятся."""
rules = load_check_rules() rules = load_check_rules()
column_indexes, amounts = await repo.statement_amounts(self._session, meta.id) column_indexes, amounts = await repo.statement_amounts(self._session, meta.id)
report = run_checks(document.document_type.value, amounts, column_indexes, rules) 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) 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: async def get(self, document_id: uuid.UUID) -> schemas.StatementOut:
document, meta = await self._visible_statement(document_id) document, meta = await self._visible_statement(document_id)
@ -128,10 +138,12 @@ class StatementService:
"""Ответ формы по текущему состоянию БД, достроенный по справочнику редакции (design D-3; используется и """Ответ формы по текущему состоянию БД, достроенный по справочнику редакции (design D-3; используется и
ответами правки/акцепта). В БД ничего не пишет.""" ответами правки/акцепта). В БД ничего не пишет."""
columns, lines = await repo.load_form(self._session, meta.id) 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) edition = edition_for(document.reporting_date)
template = load_line_catalog().lines(edition, document.document_type.value) 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, ...]: async def hard_failures(self, document: Document, meta: FinancialStatementMeta) -> tuple[FailedCheck, ...]:
"""Нарушенные жёсткие проверки §7; при акцепте — пустой кортеж (используется и подтверждением формы).""" """Нарушенные жёсткие проверки §7; при акцепте — пустой кортеж (используется и подтверждением формы)."""

View File

@ -40,6 +40,8 @@ KA_RF = "ka_rf@armka.local"
RKA = "rka@armka.local" # только DOCUMENT_VIEW RKA = "rka@armka.local" # только DOCUMENT_VIEW
ROSTOV = "Ростовский региональный филиал" ROSTOV = "Ростовский региональный филиал"
MEZH_BALANCE = GOLDEN_STATEMENTS / "mezheninovskaya" / "2025-Q3" / "1_Форма1_бух_09.json" 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_INCOME = GOLDEN_STATEMENTS / "mezheninovskaya" / "2025-Q3" / "1_Форма2_бух_09.json"
MEZH_BALANCE_LINES = 37 MEZH_BALANCE_LINES = 37
MEZH_BALANCE_TEMPLATE_ONLY = {"1215"} # коды fsbu4, которых нет в файле Межениновской (файл по форме 66н) MEZH_BALANCE_TEMPLATE_ONLY = {"1215"} # коды fsbu4, которых нет в файле Межениновской (файл по форме 66н)
@ -167,6 +169,7 @@ async def test_get_statement_shape_and_decimal_strings(
"line_code_confidence", "line_code_confidence",
"line_name", "line_name",
"line_name_confidence", "line_name_confidence",
"parent_code",
"values", "values",
} }
value = next(v for v in line["values"] if v["amount"] is not None) value = next(v for v in line["values"] if v["amount"] is not None)
@ -189,6 +192,58 @@ 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"]) 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)
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")
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_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:
"""Подстрока без четырёхзначной строки в форме остаётся без родителя (и не попадает в проверку)."""
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: async def test_get_income_statement(seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory) -> None:
document = await form(seeded, fixtures_dir, MEZH_INCOME) document = await form(seeded, fixtures_dir, MEZH_INCOME)
client = await auth_client(KA_RF) client = await auth_client(KA_RF)

View File

@ -10,6 +10,7 @@ from src.domain.statement_checks import (
cross_form_check, cross_form_check,
load_check_rules, load_check_rules,
run_checks, run_checks,
subline_parents,
) )
D = Decimal D = Decimal
@ -208,3 +209,65 @@ def test_target_with_empty_cell_counts_as_zero_and_is_checked() -> None:
(0, D("0"), D("900")), (0, D("0"), D("900")),
(1, 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
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 =")] != []