fix(statements): цель проверки присутствует в колонке только при наличии суммы; docstring порядка строк (M-1, M-3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
Andrey Epifancev 2026-10-07 14:28:49 +04:00
parent f9500d3f09
commit e851445866
3 changed files with 23 additions and 11 deletions

View File

@ -1,10 +1,10 @@
"""Логические проверки форм ББ/ОФР §7 в ядре (spec statement-checks; design D-4, D-5).
Перенос `tabular-parser/api/src/parser/statements/checks.py` по семантике: соотношение по каждой колонке, только если в
колонке есть его цель; отсутствующий код и пустая сумма — 0; правая часть считается как записано и с `abs_terms` по
модулю, выполнено, если хоть один вариант в пределах `tolerance`; жёсткое нарушение не больше
`rounding_tolerance_percent` от наибольшего по модулю числа соотношения — мягкое; «сумма пятизначных подстрок =
четырёхзначный родитель» — мягкая. Результат не хранится (DBML).
Перенос `tabular-parser/api/src/parser/statements/checks.py` по семантике: соотношение по каждой колонке, только если
в колонке есть его цель (есть сумма: пустая ячейка — цели нет); отсутствующий код и пустая сумма в правой части — 0;
правая часть считается как записано и с `abs_terms` по модулю, выполнено, если хоть один вариант в пределах
`tolerance`; жёсткое нарушение не больше `rounding_tolerance_percent` от наибольшего по модулю числа соотношения —
мягкое; «сумма пятизначных подстрок = четырёхзначный родитель» — мягкая. Результат не хранится (DBML).
"""
import functools
@ -136,8 +136,9 @@ def _holds(amounts: Mapping[str, Decimal], rule: RelationRule, tolerance: Decima
def _subline_rules(codes: Sequence[str], form: str) -> list[RelationRule]:
"""«1110 = 11101 + …» для четырёхзначных кодов формы с пятизначными подстроками (по
порядку строк, M-9 2b)."""
"""«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]] = {}
@ -177,14 +178,15 @@ def run_checks(
column_indexes: Sequence[int],
rules: CheckRules,
) -> ChecksReport:
"""Проверки §7 формы `form` по колонкам `column_indexes`; `lines` — `(код, {колонка:
сумма})` по порядку строк."""
"""Проверки §7 формы `form` по колонкам `column_indexes`; `lines` — `(код, {колонка: сумма})` в порядке кодов
(порядок строк формы в БД не хранится; от него зависит только привязка пятизначных подстрок)."""
explicit = list(rules.relations_for(form))
sublines = _subline_rules([code for code, _ in lines], form) if rules.subline_check else []
hard: list[FailedCheck] = []
soft: list[FailedCheck] = []
for column_index in column_indexes:
amounts = {code: (values.get(column_index) or _ZERO) for code, values in lines}
# код присутствует в колонке, только если в ней есть сумма (пустая ячейка — кода нет, как в tabular-parser)
amounts = {code: amount for code, values in lines if (amount := values.get(column_index)) is not None}
for rule in [*explicit, *sublines]:
if rule not in sublines and rule.target not in amounts:
continue

View File

@ -116,7 +116,8 @@ async def delete_file_statements(session: AsyncSession, file_id: uuid.UUID) -> N
async def statement_amounts(
session: AsyncSession, meta_id: uuid.UUID
) -> tuple[list[int], list[tuple[str, dict[int, Decimal | None]]]]:
"""Индексы колонок и `(код строки, {колонка: сумма})` по возрастанию кода — вход `run_checks`."""
"""Индексы колонок и `(код строки, {колонка: сумма})` по возрастанию кода — вход `run_checks`. Порядок строк
формы в БД не хранится (DBML не хранит), поэтому порядок строк — лексикографический порядок кодов."""
columns = (
await session.execute(
select(FinancialStatementColumn.id, FinancialStatementColumn.column_index)

View File

@ -195,3 +195,12 @@ def test_broken_rules_file(tmp_path) -> None:
bad.write_text("tolerance: 1\nrelations:\n - {form: BALANCE, relation: '16A0 = 1100', strict: true}\n", "utf-8")
with pytest.raises(ValueError, match="statement_checks.yaml"):
load_check_rules(bad)
def test_target_with_empty_cell_in_column_is_skipped_but_zero_is_checked() -> None:
"""M-1: цель присутствует в колонке, только если в ней есть сумма: пустая ячейка (`None`) — цели нет, соотношение
пропущено (как в tabular-parser); явный 0 — цель есть, и при ненулевых слагаемых это нарушение."""
terms = (("1100", {0: "300", 1: "300"}), ("1200", {0: "600", 1: "600"}))
empty = run_checks("BALANCE", _lines(*terms, ("1600", {0: None, 1: "0"})), [0, 1], RULES)
failed = _by_relation(empty.failed, "1600 = 1100 + 1200")
assert [(item.column_index, item.expected, item.actual) for item in failed] == [(1, D("0"), D("900"))]