From 51d322c8a79fff648070b736446f5e0a30bd956c Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 7 Oct 2026 14:48:03 +0400 Subject: [PATCH] =?UTF-8?q?fix(statements):=20=D0=BE=D1=82=D0=BA=D0=B0?= =?UTF-8?q?=D1=82=20M-1=20=E2=80=94=20=D0=BF=D1=83=D1=81=D1=82=D0=B0=D1=8F?= =?UTF-8?q?=20=D1=81=D1=83=D0=BC=D0=BC=D0=B0=20=D1=86=D0=B5=D0=BB=D0=B8=20?= =?UTF-8?q?=D1=81=D1=87=D0=B8=D1=82=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D0=BD?= =?UTF-8?q?=D1=83=D0=BB=D1=91=D0=BC,=20=D0=BA=D0=B0=D0=BA=20=D0=B2=20tabul?= =?UTF-8?q?ar-parser;=20=D0=BE=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BB=D1=8F=20inn=20(N-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- api/src/domain/statement_checks.py | 14 ++++++++------ api/src/schemas/statements.py | 2 +- api/tests/test_domain_statement_checks.py | 16 ++++++++++------ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/api/src/domain/statement_checks.py b/api/src/domain/statement_checks.py index 81e97ca..3c6dca8 100644 --- a/api/src/domain/statement_checks.py +++ b/api/src/domain/statement_checks.py @@ -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 diff --git a/api/src/schemas/statements.py b/api/src/schemas/statements.py index 614ff5f..a359f71 100644 --- a/api/src/schemas/statements.py +++ b/api/src/schemas/statements.py @@ -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 diff --git a/api/tests/test_domain_statement_checks.py b/api/tests/test_domain_statement_checks.py index 1838024..295de02 100644 --- a/api/tests/test_domain_statement_checks.py +++ b/api/tests/test_domain_statement_checks.py @@ -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")), + ]