fix(statements): откат M-1 — пустая сумма цели считается нулём, как в tabular-parser; описание поля inn (N-1)

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:48:03 +04:00
parent 4a5cb87e63
commit 51d322c8a7
3 changed files with 19 additions and 13 deletions

View File

@ -1,10 +1,11 @@
"""Логические проверки форм ББ/ОФР §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).
в форме есть строка его цели (пустая сумма `None` = 0: оба производителя отдают значение на каждую колонку каждой
строки); отсутствующий код и пустая сумма в правой части — 0; правая часть считается как записано и с `abs_terms` по
модулю, выполнено, если хоть один вариант в пределах `tolerance`; жёсткое нарушение не больше
`rounding_tolerance_percent` от наибольшего по модулю числа соотношения — мягкое; «сумма пятизначных подстрок =
четырёхзначный родитель» — мягкая. Результат не хранится (DBML).
"""
import functools
@ -185,8 +186,9 @@ def run_checks(
hard: list[FailedCheck] = []
soft: list[FailedCheck] = []
for column_index in column_indexes:
# код присутствует в колонке, только если в ней есть сумма (пустая ячейка — кода нет, как в tabular-parser)
amounts = {code: amount for code, values in lines if (amount := values.get(column_index)) is not None}
# присутствие цели = наличие строки формы; пустая сумма (None) = 0, как `_amounts` в tabular-parser: оба
# производителя отдают значение на каждую колонку каждой кодированной строки (пустая ячейка — amount=None)
amounts = {code: (values.get(column_index) or _ZERO) for code, values in lines}
for rule in [*explicit, *sublines]:
if rule not in sublines and rule.target not in amounts:
continue

View File

@ -246,7 +246,7 @@ class HeaderPatch(BaseModel):
model_config = ConfigDict(extra="forbid")
inn: str | None = None
inn: str | None = Field(None, description="10 или 12 цифр; null не допускается (422 INVALID_INN)")
report_date: datetime.date | None = None
units: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=50)] | None = None

View File

@ -197,10 +197,14 @@ def test_broken_rules_file(tmp_path) -> None:
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 — цель есть, и при ненулевых слагаемых это нарушение."""
def test_target_with_empty_cell_counts_as_zero_and_is_checked() -> None:
"""M-1 (откат): цель присутствует, если строка есть в форме; пустая ячейка (`None`) = 0 — как в tabular-parser
(`_amounts`, null -> 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"))]
report = run_checks("BALANCE", _lines(*terms, ("1600", {0: None, 1: "0"})), [0, 1], RULES)
failed = _by_relation(report.failed, "1600 = 1100 + 1200")
assert [(item.column_index, item.expected, item.actual) for item in failed] == [
(0, D("0"), D("900")),
(1, D("0"), D("900")),
]