refactor(statements): удалена копия словаря §7 ядра — проверки копией arm_statements
Удалены api/rules/statement_checks.yaml, load_check_rules, run_checks и правила соотношений; в domain/statement_checks.py остались межформенная проверка 1370/2400 и subline_parents. Тест test_name_dictionary_codes_in_code_orders сверяет справочник имён с перечнями редакций пакета. CLAUDE.md и README.md: §7 — копия arm_statements, нарушения хранятся в document_issue. Решения P3-50…P3-52. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2b8fafc3b3
commit
570e82b5d3
14
CLAUDE.md
14
CLAUDE.md
@ -26,7 +26,7 @@
|
||||
ОСВ (§1.3, §1.5, §9.3); Срез 4 — `consolidation_periods.py` (выбор источников и приведение периодов, §2, Q1-эквивалентность ОСВ,
|
||||
R-14), `consolidated_osv.py` (чистая сборка свода: склейка кварталов, пробелы, дорезолв, фильтры, §2–§3), `checklist.py`
|
||||
(чек-лист комплекта, ячейки `CellStatus`); Срез 3b — `statement_mapping.py` (формы ББ/ОФР: период, черновик, статус D-6),
|
||||
`statement_checks.py` (проверки §7 по словарю `api/rules/statement_checks.yaml`), `checklist_csv.py` (CSV чек-листа);
|
||||
`statement_checks.py` (остатки §7 в ядре: межформенная 1370↔2400 и `subline_parents`; соотношения §7 считает `arm_statements`), `checklist_csv.py` (CSV чек-листа);
|
||||
`policy.py` дополнен `IN_WORK_STATUSES`/`ensure_version_editable`, §10; для шахматки
|
||||
и КФО — Срезы 5–6), `contract/` (модели контракта обмена), `api/scripts/` (`check_dbml_vs_orm.py`,
|
||||
`seed.py` + `seed_lib/`, `check_infra.py`), данные seed — `api/seed/`.
|
||||
@ -89,9 +89,12 @@
|
||||
функции) + `repositories/statements.py` (`financial_statement_meta/column/line/value`); форма создаётся с тем, что есть в результате (колонки могут отсутствовать);
|
||||
второй `BALANCE`/`INCOME` того же файла пропускается с предупреждением (OQ-77), `DISCLOSURE`/`OTHER` — один документ
|
||||
без данных на тип (`RECOGNIZED`, D-3); документ `BALANCE`/`INCOME` без `meta` (создан до Среза 3b) подтверждается без условий D-9, `…/statement` по нему — 409
|
||||
`DOCUMENT_NOT_STATEMENT` (решение №16). Проверки §7 — `domain/statement_checks.py` по словарю `api/rules/statement_checks.yaml`; **файл —
|
||||
копия словаря tabular-parser, при изменении правится в обоих репозиториях** (расхождение молчаливо меняет результат проверок).
|
||||
Проверки считаются на лету и не хранятся; хранится только акцепт (`checks_accepted_by/at/comment` в шапке). Правка формы и акцепт —
|
||||
`DOCUMENT_NOT_STATEMENT` (решение №16). Проверки §7 — словарь и расчёт в пакете `arm_statements` (`api/arm_statements/`, копия, напрямую не править —
|
||||
`../docs/scripts/sync-statements.sh`); **копии словаря §7 в ядре нет** (удалены `api/rules/statement_checks.yaml`,
|
||||
`load_check_rules`, `run_checks`). Нарушения при приёме результата и после правки пишет пакет через
|
||||
`services/statements_engine.py` и они **хранятся в `document_issue`** (`relation` / `soft`, `codes`, `line_ids`); ответ
|
||||
формы читает их оттуда. В `domain/statement_checks.py` остались только межформенная проверка 1370↔2400 (на лету, допуск —
|
||||
`engine.tolerance()`) и `subline_parents` (`parent_code` ответа). Акцепт — `checks_accepted_by/at/comment` в шапке. Правка формы и акцепт —
|
||||
`services/statements.py` (`PATCH …/statement`, `POST …/statement/checks/accept`): **только до подтверждения** (`CONFIRMED` →
|
||||
`DOCUMENT_READONLY`), аудит `RECOGNIZED_DATA_EDIT` (раздел `RECOGNITION`, `object_type=document`; один запрос — одно событие), первичка
|
||||
рядом (`original_amount`, `*_original_value`, `is_modified`/`*_modified`); любая правка сбрасывает акцепт и пересчитывает статус
|
||||
@ -99,7 +102,8 @@
|
||||
остаётся устаревший акцепт. Подтверждение формы (`services/documents._check_statement`) требует период (`PERIOD_REQUIRED`) и
|
||||
пройденные/акцептованные жёсткие проверки (`CHECKS_NOT_PASSED`, `detail.failures`); мягкие (в т. ч. межформенная 1370↔2400)
|
||||
не блокируют. Полный состав формы — `domain/statement_lines.py` по справочнику `api/rules/statement_lines.yaml` (редакции `66n`/`fsbu4`
|
||||
из шаблонов банка; копия словаря scan-gateway с ним не совпадает): `GET …/statement` достраивает строки и ячейки с
|
||||
из шаблонов банка; это справочник имён и состава экрана — перечни кодов редакций живут в `arm_statements`
|
||||
(`Rules.code_orders`, `engine.editions()`/`engine.code_order(...)`), согласованность проверяет `test_name_dictionary_codes_in_code_orders`): `GET …/statement` достраивает строки и ячейки с
|
||||
`id = null`, `PATCH` по `line_code` создаёт строку (`line_code_original_value = NULL` — «не из источника»); пустые
|
||||
созданные строки не участвуют в §7 (`statement_amounts`). Решения — `openspec/changes/statement-template-lines/design.md`. Комментарии к документу — `services/documents.py` (`comments`/`add_comment`, `comment(object_type='document')`,
|
||||
без аудита; `comments_count` в карточках). `GET /files/{id}/content` — `services/primary_files.content` +
|
||||
|
||||
14
README.md
14
README.md
@ -563,9 +563,10 @@ S3 (storage); без S3 — предупреждение в логе, цикл
|
||||
`modified_by/at`; у полей шапки — `*_modified`, `*_original_value`). Период формы — по дате отчёта, а при её отсутствии по самой
|
||||
поздней дате колонок; ББ/ОФР всегда нарастающие. Статус документа формы — `RECOGNIZED` только если задание `done`, нет нарушений
|
||||
проверок §7 (или они акцептованы), контрагент не `MISMATCH`, период определён, значения разобраны, есть строки и нет
|
||||
предупреждений; иначе `NEEDS_CHECK` (правило D-6 спеки). Проверки §7 (`domain/statement_checks.py`) считаются на лету по
|
||||
текущим суммам формы и не хранятся; словарь — `api/rules/statement_checks.yaml` (копия словаря tabular-parser — правится
|
||||
в обоих репозиториях).
|
||||
предупреждений; иначе `NEEDS_CHECK` (правило D-6 спеки). Проверки §7 считает пакет `arm_statements` (копия в `api/arm_statements/`,
|
||||
обновляется `sync-statements`) при приёме результата и после каждой правки; нарушения хранятся в `document_issue`
|
||||
(`relation` / `soft`). Копии словаря §7 в ядре нет; в `domain/statement_checks.py` остались межформенная проверка 1370↔2400
|
||||
(на лету, допуск — из правил пакета) и `subline_parents`.
|
||||
|
||||
- **`GET /documents/{id}/statement`** — `{document, edition, header, columns[], lines[], checks, issues[], edition_lines[]}`: `header` — `inn`,
|
||||
`report_date`, `units` вида `{value, original_value, confidence, verified, modified}` (`confidence` есть у ИНН, у даты и единиц — `null`) и справочные поля формы
|
||||
@ -619,7 +620,8 @@ S3 (storage); без S3 — предупреждение в логе, цикл
|
||||
Комментарии аудит не пишут; при удалении файла комментарии его документов удаляются вместе с документами
|
||||
(полиморфная связь без FK; решение №18 закрыто).
|
||||
|
||||
Расчёт — `src/domain/statement_mapping.py` и `src/domain/statement_checks.py` (чистые функции), доступ к данным —
|
||||
Расчёт — `src/domain/statement_mapping.py`, `src/domain/statement_checks.py` (межформенная проверка) и пакет
|
||||
`arm_statements` через `src/services/statements_engine.py` (чистые функции), доступ к данным —
|
||||
`src/repositories/statements.py`, правка и акцепт — `src/services/statements.py`.
|
||||
|
||||
## Удаление и замена
|
||||
@ -1120,7 +1122,7 @@ make migration-cycle # проверить цикл upgr
|
||||
api/
|
||||
run_server.py точка входа (uvicorn; хост и порт — из настроек)
|
||||
alembic/ миграции (0001_dbml_v5_13.py — модель v5.13, 0002_dbml_v5_14.py — v5.14, 0003_dbml_v5_15.py — v5.15, 0004_dbml_v5_16.py — v5.16, 0005_dbml_v5_17.py — v5.17, 0006_dbml_v5_18.py — v5.18, 0007_dbml_v5_19.py — v5.19)
|
||||
rules/ statement_checks.yaml — словарь проверок §7 формы ББ/ОФР (копия словаря tabular-parser; меняется в обоих репозиториях)
|
||||
rules/ statement_lines.yaml — справочник имён строк форм ББ/ОФР по редакциям (словарь проверок §7 — в пакете arm_statements)
|
||||
seed/core/ справочники продукта (роли, права, филиалы, план счетов, правила элиминации, чек-лист)
|
||||
seed/dev/ dev-данные: филиалы и пользователи по ролям
|
||||
scripts/ check_dbml_vs_orm.py (ORM <-> DBML), seed.py и seed_lib/ (загрузчик справочников)
|
||||
@ -1143,7 +1145,7 @@ api/
|
||||
common/ доменные ошибки (`{detail: {code, message}}`), часы
|
||||
domain/ чистые функции по §§ правил, без БД: policy (видимость), directory_import (§13), aliases (§3), inn,
|
||||
file_types (тип файла по расширению и сигнатуре), osv_mapping (результат парсера → документы/строки, §1.3, §1.5, §9.3);
|
||||
Срез 3b — statement_mapping (формы ББ/ОФР: период, статус, §9.3, §9.5), statement_checks (проверки §7),
|
||||
Срез 3b — statement_mapping (формы ББ/ОФР: период, статус, §9.3, §9.5), statement_checks (межформенная проверка §7, родители подстрок),
|
||||
checklist_csv (CSV чек-листа);
|
||||
Срез 4 — consolidation_periods (выбор источников, §2), consolidated_osv (свод), checklist (чек-лист), policy
|
||||
(`ensure_version_editable`, §10)
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
# КОПИЯ tabular-parser/api/rules/statement_checks.yaml (Срез 3b, design D-4): меняется в обоих репозиториях одновременно.
|
||||
# Межрепозиторного теста идентичности нет сознательно (решение №10); расхождение копий — известный риск (03-architecture).
|
||||
# Соотношения §7 (05-domain-rules.md), ББ и ОФР. Данные, не код: новое соотношение — строка здесь.
|
||||
# relation: «<код> = <код> [+|-] <код> …», только четырёхзначные коды; strict: нарушение переводит документ в needs_check
|
||||
# (false — только предупреждение, ASSUMPTION(OQ-12)); abs_terms: коды, которые 1С выгружает со знаком минус, а 66н — в
|
||||
# скобках: правая часть считается и как записано, и с этими членами по модулю (design D-7).
|
||||
tolerance: 1 # допуск в единицах формы (тыс. руб. округляются построчно)
|
||||
# OQ-138 (решение владельца 06.10.2026): единицы шапки верны, расхождение до 0,001 % итоговой строки — округление.
|
||||
# Жёсткое соотношение с расхождением больше `tolerance`, но не больше этой доли от наибольшего по модулю числа
|
||||
# соотношения (итог или слагаемое: итог бывает малой разностью больших слагаемых), не портит статус:
|
||||
# уходит в soft_relations_failed с предупреждением «в пределах округления».
|
||||
rounding_tolerance_percent: 0.001
|
||||
subline_check: true # «сумма пятизначных подстрок = родитель» — мягкая проверка, генерируется по коду
|
||||
relations:
|
||||
- {form: BALANCE, relation: "1100 = 1105 + 1110 + 1120 + 1130 + 1140 + 1150 + 1160 + 1170 + 1180 + 1190", strict: true}
|
||||
- {form: BALANCE, relation: "1200 = 1210 + 1215 + 1220 + 1230 + 1240 + 1250 + 1260", strict: true}
|
||||
- {form: BALANCE, relation: "1300 = 1310 + 1320 + 1340 + 1350 + 1360 + 1370", strict: true}
|
||||
- {form: BALANCE, relation: "1400 = 1410 + 1420 + 1430 + 1450", strict: true}
|
||||
- {form: BALANCE, relation: "1500 = 1510 + 1520 + 1530 + 1540 + 1550", strict: true}
|
||||
- {form: BALANCE, relation: "1600 = 1100 + 1200", strict: true}
|
||||
- {form: BALANCE, relation: "1700 = 1300 + 1400 + 1500", strict: true}
|
||||
- {form: BALANCE, relation: "1600 = 1700", strict: true}
|
||||
- {form: INCOME, relation: "2100 = 2110 - 2120", strict: true, abs_terms: ["2120"]}
|
||||
- {form: INCOME, relation: "2200 = 2100 - 2210 - 2220", strict: true, abs_terms: ["2210", "2220"]}
|
||||
- {form: INCOME, relation: "2300 = 2200 + 2310 + 2320 - 2330 + 2340 - 2350", strict: true, abs_terms: ["2330", "2350"]}
|
||||
- {form: INCOME, relation: "2400 = 2300 - 2410 + 2420", strict: false, abs_terms: ["2410"]}
|
||||
- {form: INCOME, relation: "2400 = 2300 + 2410 + 2460", strict: false}
|
||||
- {form: INCOME, relation: "2410 = 2411 + 2412", strict: false}
|
||||
- {form: INCOME, relation: "2500 = 2400 + 2510 + 2520 + 2530", strict: false}
|
||||
@ -172,7 +172,7 @@ class FailedRelation(ContractModel):
|
||||
"""
|
||||
|
||||
column_index: IntLike
|
||||
relation: str # «1600 = 1100 + 1200» — текст из словаря statement_checks.yaml
|
||||
relation: str # «1600 = 1100 + 1200» — текст из словаря §7 пакета arm_statements
|
||||
expected: Money
|
||||
actual: Money
|
||||
|
||||
|
||||
@ -74,7 +74,8 @@ class _Column(Protocol):
|
||||
def checks_input(
|
||||
columns: Sequence[_Column], aggregates: Mapping[str, CodeAggregate]
|
||||
) -> tuple[list[int], list[tuple[str, dict[int, Decimal | None]]]]:
|
||||
"""Вход `run_checks`: индексы колонок (по возрастанию) и `(код, {column_index: сумма})` по возрастанию кода.
|
||||
"""Коды формы для `subline_parents` и межформенной проверки: индексы колонок (по возрастанию) и
|
||||
`(код, {column_index: сумма})` по возрастанию кода.
|
||||
|
||||
`columns` — колонки формы (`id`, `column_index`). Код без строки источника и без единой суммы (пустая шаблонная
|
||||
строка) пропускается (design D-5 шаблонных строк)."""
|
||||
|
||||
@ -1,152 +1,46 @@
|
||||
"""Логические проверки форм ББ/ОФР §7 в ядре (spec statement-checks; design D-4, D-5).
|
||||
"""Остатки проверок §7 в ядре: межформенная проверка и родители подстрок (spec statement-checks; design D-5).
|
||||
|
||||
Перенос `tabular-parser/api/src/parser/statements/checks.py` по семантике: соотношение по каждой колонке, только если
|
||||
в форме есть строка его цели (пустая сумма `None` = 0: оба производителя отдают значение на каждую колонку каждой
|
||||
строки); отсутствующий код и пустая сумма в правой части — 0; правая часть считается как записано и с `abs_terms` по
|
||||
модулю, выполнено, если хоть один вариант в пределах `tolerance`; жёсткое нарушение не больше
|
||||
`rounding_tolerance_percent` от наибольшего по модулю числа соотношения — мягкое; «сумма пятизначных подстрок =
|
||||
четырёхзначный родитель» — мягкая и только если в колонке есть хоть одна непустая подстрока (AURORA-1527).
|
||||
Результат не хранится (DBML).
|
||||
Соотношения §7 по колонкам формы (в т. ч. «сумма пятизначных подстрок = четырёхзначный родитель») считает пакет
|
||||
`arm_statements` при приёме и пересчёте; нарушения хранятся в `document_issue`. Копии словаря в ядре нет (часть Б, B6).
|
||||
Здесь остались то, чего пакет не знает: межформенная проверка 1370 (ББ) ↔ 2400 (ОФР) — она по двум документам — и
|
||||
привязка пятизначных подстрок к родителям для поля `parent_code` ответа формы (AURORA-1527).
|
||||
"""
|
||||
|
||||
import functools
|
||||
import re
|
||||
from collections.abc import Mapping, Sequence
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from pathlib import Path
|
||||
from typing import Literal, cast
|
||||
|
||||
import yaml
|
||||
from decimal import Decimal
|
||||
|
||||
__all__ = [
|
||||
"CROSS_FORM_RELATION",
|
||||
"CheckRules",
|
||||
"ChecksReport",
|
||||
"FailedCheck",
|
||||
"RelationRule",
|
||||
"cross_form_check",
|
||||
"load_check_rules",
|
||||
"run_checks",
|
||||
"subline_parents",
|
||||
]
|
||||
|
||||
Form = Literal["BALANCE", "INCOME"]
|
||||
RULES_PATH = Path(__file__).resolve().parents[2] / "rules" / "statement_checks.yaml"
|
||||
CROSS_FORM_RELATION = "1370 − 1370(начало года) = 2400"
|
||||
# диапазоны четырёхзначных кодов форм для генерации подстрок (§12 п. 2; в tabular-parser —
|
||||
# statement_headers.yaml)
|
||||
CODE_RANGES: dict[str, tuple[int, int]] = {"BALANCE": (1100, 1799), "INCOME": (2100, 2999)}
|
||||
_ZERO = Decimal("0")
|
||||
_HUNDRED = Decimal("100")
|
||||
_PARENT_LEN = 4
|
||||
_GROUP_LEN = 3
|
||||
_RELATION = re.compile(r"^\s*(?P<target>\d{4})\s*=\s*(?P<rhs>\d{4,5}(\s*[+\-]\s*\d{4,5})*)\s*$")
|
||||
_TERM = re.compile(r"([+\-])?\s*(\d{4,5})")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RelationRule:
|
||||
form: Form
|
||||
relation: str
|
||||
target: str
|
||||
terms: tuple[tuple[int, str], ...]
|
||||
strict: bool
|
||||
abs_terms: frozenset[str] = frozenset()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CheckRules:
|
||||
tolerance: Decimal
|
||||
rounding_tolerance_percent: Decimal
|
||||
subline_check: bool
|
||||
relations: tuple[RelationRule, ...]
|
||||
|
||||
def relations_for(self, form: str) -> tuple[RelationRule, ...]:
|
||||
return tuple(rule for rule in self.relations if rule.form == form)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FailedCheck:
|
||||
"""Межформенное нарушение (`column_index = -1`); `codes` — коды для подсветки строк ответа (1370 и 2400)."""
|
||||
|
||||
column_index: int
|
||||
relation: str
|
||||
expected: Decimal
|
||||
actual: Decimal
|
||||
codes: tuple[str, ...] = () # цель и члены правила (P3-14); у межформенной — 1370 и 2400
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChecksReport:
|
||||
failed: tuple[FailedCheck, ...]
|
||||
soft_failed: tuple[FailedCheck, ...]
|
||||
|
||||
|
||||
def _parse_relation(form: Form, relation: str, strict: bool, abs_terms: Sequence[str]) -> RelationRule:
|
||||
found = _RELATION.match(relation)
|
||||
if found is None:
|
||||
raise ValueError(f"соотношение «{relation}»: ожидается «<код> = <код> [+|-] <код> …»")
|
||||
terms = tuple((-1 if sign == "-" else 1, code) for sign, code in _TERM.findall(found.group("rhs")))
|
||||
unknown = set(abs_terms) - {code for _, code in terms}
|
||||
if unknown:
|
||||
raise ValueError(f"abs_terms {sorted(unknown)} нет в правой части «{relation}»")
|
||||
return RelationRule(form, relation, found.group("target"), terms, strict, frozenset(abs_terms))
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=4)
|
||||
def load_check_rules(path: Path | None = None) -> CheckRules:
|
||||
"""Словарь §7 из yaml; любая ошибка — `ValueError` с именем файла."""
|
||||
source = path or RULES_PATH
|
||||
try:
|
||||
data = yaml.safe_load(source.read_text(encoding="utf-8"))
|
||||
relations = tuple(
|
||||
_parse_relation(item["form"], item["relation"], bool(item["strict"]), item.get("abs_terms", ()))
|
||||
for item in data["relations"]
|
||||
)
|
||||
for rule in relations:
|
||||
if rule.form not in CODE_RANGES:
|
||||
raise ValueError(f"форма {rule.form!r}: ожидается BALANCE или INCOME")
|
||||
return CheckRules(
|
||||
tolerance=Decimal(str(data["tolerance"])),
|
||||
rounding_tolerance_percent=Decimal(str(data.get("rounding_tolerance_percent", 0))),
|
||||
subline_check=bool(data.get("subline_check", True)),
|
||||
relations=relations,
|
||||
)
|
||||
except (OSError, KeyError, TypeError, ValueError, InvalidOperation, yaml.YAMLError) as exc:
|
||||
raise ValueError(f"словарь проверок {source.name}: {exc}") from exc
|
||||
|
||||
|
||||
def _rhs(amounts: Mapping[str, Decimal], rule: RelationRule, *, absolute: bool) -> Decimal:
|
||||
total = _ZERO
|
||||
for sign, code in rule.terms:
|
||||
value = amounts.get(code, _ZERO)
|
||||
if absolute and code in rule.abs_terms:
|
||||
value = abs(value)
|
||||
total += sign * value
|
||||
return total
|
||||
|
||||
|
||||
def _holds(amounts: Mapping[str, Decimal], rule: RelationRule, tolerance: Decimal) -> tuple[bool, Decimal, Decimal]:
|
||||
expected = amounts.get(rule.target, _ZERO)
|
||||
plain = _rhs(amounts, rule, absolute=False)
|
||||
if abs(expected - plain) <= tolerance:
|
||||
return True, expected, plain
|
||||
if rule.abs_terms:
|
||||
with_abs = _rhs(amounts, rule, absolute=True)
|
||||
if abs(expected - with_abs) <= tolerance:
|
||||
return True, expected, with_abs
|
||||
if abs(expected - with_abs) < abs(expected - plain):
|
||||
return False, expected, with_abs
|
||||
return False, expected, plain
|
||||
codes: tuple[str, ...] = ()
|
||||
|
||||
|
||||
def subline_parents(codes: Sequence[str]) -> dict[str, str]:
|
||||
"""Родитель каждой пятизначной подстроки: `{код подстроки: код четырёхзначной строки}` (единое правило §7 для
|
||||
проверки «сумма подстрок = родитель» и для поля `parent_code` ответа формы; AURORA-1527).
|
||||
"""Родитель каждой пятизначной подстроки: `{код подстроки: код четырёхзначной строки}` (поле `parent_code` ответа
|
||||
формы; AURORA-1527).
|
||||
|
||||
Подстрока относится к ближайшей четырёхзначной строке выше с теми же тремя цифрами, иначе — к строке с кодом из
|
||||
её первых четырёх цифр, если такая есть; без родителя в результат не попадает. Порядок строк в БД не хранится
|
||||
(DBML порядка не знает): «выше» — порядок `codes`, и вызывающий передаёт его тот же, в каком `checks_input`
|
||||
отдаёт коды проверке (по возрастанию кода; M-9 2b). Порядок результата — порядок подстрок в `codes`."""
|
||||
отдаёт коды (по возрастанию кода; M-9 2b). Порядок результата — порядок подстрок в `codes`."""
|
||||
present = {code for code in codes if len(code) == _PARENT_LEN}
|
||||
result: dict[str, str] = {}
|
||||
last_parent: str | None = None
|
||||
@ -163,66 +57,6 @@ def subline_parents(codes: Sequence[str]) -> dict[str, str]:
|
||||
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:
|
||||
relation = f"{parent} = {' + '.join(kids)}"
|
||||
rules.append(
|
||||
RelationRule(
|
||||
cast(Form, form),
|
||||
relation,
|
||||
parent,
|
||||
tuple((1, kid) for kid in kids),
|
||||
False,
|
||||
)
|
||||
)
|
||||
return rules
|
||||
|
||||
|
||||
def run_checks(
|
||||
form: str,
|
||||
lines: Sequence[tuple[str, Mapping[int, Decimal | None]]],
|
||||
column_indexes: Sequence[int],
|
||||
rules: CheckRules,
|
||||
) -> ChecksReport:
|
||||
"""Проверки §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:
|
||||
# присутствие цели = наличие строки формы; пустая сумма (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
|
||||
codes = (rule.target, *(code for _, code in rule.terms))
|
||||
failed = FailedCheck(column_index, rule.relation, expected, actual, codes)
|
||||
scale = max([abs(expected), *(abs(amounts.get(code, _ZERO)) for _, code in rule.terms)])
|
||||
rounding = scale * rules.rounding_tolerance_percent / _HUNDRED
|
||||
if rule.strict and abs(expected - actual) > rounding:
|
||||
hard.append(failed)
|
||||
else:
|
||||
soft.append(failed)
|
||||
return ChecksReport(failed=tuple(hard), soft_failed=tuple(soft))
|
||||
|
||||
|
||||
def cross_form_check(
|
||||
balance_1370: tuple[Decimal | None, Decimal | None],
|
||||
income_2400: Decimal | None,
|
||||
|
||||
@ -1,175 +1,27 @@
|
||||
"""Проверки §7 ядра (spec statement-checks; design D-4, D-5): перенос проверок tabular-parser."""
|
||||
"""Остатки проверок §7 в ядре: межформенная проверка и родители подстрок.
|
||||
|
||||
Остальные проверки §7 считает пакет `arm_statements` (тесты пакета и `test_statements_recompute*`); копии словаря
|
||||
в ядре нет (часть Б, B6)."""
|
||||
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.domain.statement_checks import (
|
||||
ChecksReport,
|
||||
FailedCheck,
|
||||
cross_form_check,
|
||||
load_check_rules,
|
||||
run_checks,
|
||||
subline_parents,
|
||||
)
|
||||
from src.domain.statement_checks import FailedCheck, cross_form_check, subline_parents
|
||||
|
||||
D = Decimal
|
||||
RULES = load_check_rules()
|
||||
API_DIR = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _lines(*items: tuple[str, dict[int, str | None]]) -> list[tuple[str, dict[int, Decimal | None]]]:
|
||||
return [(code, {i: (D(v) if v is not None else None) for i, v in values.items()}) for code, values in items]
|
||||
|
||||
|
||||
def test_rules_loaded_from_copy() -> None:
|
||||
assert RULES.tolerance == D("1")
|
||||
assert RULES.rounding_tolerance_percent == D("0.001")
|
||||
relations = [rule.relation for rule in RULES.relations_for("BALANCE")]
|
||||
assert "1600 = 1100 + 1200" in relations and "1600 = 1700" in relations
|
||||
income = {rule.relation: rule for rule in RULES.relations_for("INCOME")}
|
||||
assert income["2100 = 2110 - 2120"].abs_terms == frozenset({"2120"})
|
||||
assert income["2400 = 2300 - 2410 + 2420"].strict is False
|
||||
|
||||
|
||||
def test_hard_violation() -> None:
|
||||
report = run_checks(
|
||||
"BALANCE", _lines(("1100", {0: "300"}), ("1200", {0: "600"}), ("1600", {0: "1000"})), [0], RULES
|
||||
)
|
||||
assert FailedCheck(0, "1600 = 1100 + 1200", D("1000"), D("900"), ("1600", "1100", "1200")) in report.failed
|
||||
|
||||
|
||||
def test_relation_skipped_without_target() -> None:
|
||||
report = run_checks("BALANCE", _lines(("1110", {0: "5"}), ("1150", {0: "7"})), [0], RULES)
|
||||
assert report == ChecksReport(failed=(), soft_failed=())
|
||||
|
||||
|
||||
def test_tolerance_one_unit() -> None:
|
||||
report = run_checks("BALANCE", _lines(("1100", {0: "300"}), ("1200", {0: "600"}), ("1600", {0: "901"})), [0], RULES)
|
||||
assert all(item.relation != "1600 = 1100 + 1200" for item in report.failed)
|
||||
|
||||
|
||||
def test_abs_terms_minus_sign() -> None:
|
||||
report = run_checks("INCOME", _lines(("2110", {0: "100"}), ("2120", {0: "-60"}), ("2100", {0: "40"})), [0], RULES)
|
||||
assert all(item.relation != "2100 = 2110 - 2120" for item in report.failed)
|
||||
|
||||
|
||||
def test_rounding_goes_soft() -> None:
|
||||
# расхождение 5 при числах ~1 000 000: 5 ≤ 1 000 000 × 0,001 % = 10 → мягкое «в пределах округления»
|
||||
report = run_checks(
|
||||
"BALANCE", _lines(("1100", {0: "500000"}), ("1200", {0: "500000"}), ("1600", {0: "1000005"})), [0], RULES
|
||||
)
|
||||
assert all(item.relation != "1600 = 1100 + 1200" for item in report.failed)
|
||||
assert any(item.relation == "1600 = 1100 + 1200" for item in report.soft_failed)
|
||||
|
||||
|
||||
def test_soft_relation_not_hard() -> None:
|
||||
report = run_checks("INCOME", _lines(("2300", {0: "100"}), ("2410", {0: "20"}), ("2400", {0: "50"})), [0], RULES)
|
||||
assert all(item.relation != "2400 = 2300 - 2410 + 2420" for item in report.failed)
|
||||
assert any(item.relation == "2400 = 2300 - 2410 + 2420" for item in report.soft_failed)
|
||||
|
||||
|
||||
def test_sublines_soft() -> None:
|
||||
report = run_checks("BALANCE", _lines(("1150", {0: "10"}), ("11501", {0: "6"}), ("11502", {0: "2"})), [0], RULES)
|
||||
assert any(item.relation == "1150 = 11501 + 11502" for item in report.soft_failed)
|
||||
assert report.failed == ()
|
||||
|
||||
|
||||
def test_target_present_no_terms_hard_failure() -> None:
|
||||
# Спека: отсутствующий код = 0; если цель присутствует но членов нет, это нарушение
|
||||
# Правило 1600 = 1100 + 1200, в колонке только 1600=1000 → 1600 ≠ 0, жёсткое нарушение
|
||||
report = run_checks("BALANCE", _lines(("1600", {0: "1000"})), [0], RULES)
|
||||
assert any(
|
||||
item.relation == "1600 = 1100 + 1200" and item.expected == D("1000") and item.actual == D("0")
|
||||
for item in report.failed
|
||||
)
|
||||
|
||||
|
||||
def test_target_without_self_sum_hard_failure() -> None:
|
||||
# Спека: пропуск только если цель отсутствует; если цель есть, правило проверяется
|
||||
# Правило 1600 = 1700, в колонке только 1600=1000 (нет 1300, 1400, 1500) → нарушение
|
||||
report = run_checks("BALANCE", _lines(("1600", {0: "1000"})), [0], RULES)
|
||||
assert any(
|
||||
item.relation == "1600 = 1700" and item.expected == D("1000") and item.actual == D("0")
|
||||
for item in report.failed
|
||||
)
|
||||
|
||||
|
||||
def _by_relation(items: tuple[FailedCheck, ...], relation: str) -> list[FailedCheck]:
|
||||
return [item for item in items if item.relation == relation]
|
||||
|
||||
|
||||
def test_relation_checked_per_column_and_none_is_zero() -> None:
|
||||
# колонка 0: 900 = 300 + 600; колонка 1: 1000 ≠ 900; колонка 2: 300 = 300 + None(=0)
|
||||
lines = _lines(
|
||||
("1100", {0: "300", 1: "300", 2: "300"}),
|
||||
("1200", {0: "600", 1: "600", 2: None}),
|
||||
("1600", {0: "900", 1: "1000", 2: "300"}),
|
||||
)
|
||||
report = run_checks("BALANCE", lines, [0, 1, 2], RULES)
|
||||
assert _by_relation(report.failed, "1600 = 1100 + 1200") == [
|
||||
FailedCheck(1, "1600 = 1100 + 1200", D("1000"), D("900"), ("1600", "1100", "1200"))
|
||||
]
|
||||
assert _by_relation(report.soft_failed, "1600 = 1100 + 1200") == []
|
||||
|
||||
|
||||
def test_tolerance_exceeded_by_one_unit_is_violation() -> None:
|
||||
# допуск 1: 902 при 300 + 600 — расхождение 2 > 1, число мало, округление (0,001 %) не спасает
|
||||
lines = _lines(("1100", {0: "300"}), ("1200", {0: "600"}), ("1600", {0: "902"}))
|
||||
report = run_checks("BALANCE", lines, [0], RULES)
|
||||
assert _by_relation(report.failed, "1600 = 1100 + 1200") == [
|
||||
FailedCheck(0, "1600 = 1100 + 1200", D("902"), D("900"), ("1600", "1100", "1200"))
|
||||
]
|
||||
|
||||
|
||||
def test_tolerance_boundary_is_inclusive() -> None:
|
||||
lines = _lines(("1100", {0: "300"}), ("1200", {0: "600"}), ("1600", {0: "901"}))
|
||||
report = run_checks("BALANCE", lines, [0], RULES)
|
||||
assert _by_relation(report.failed, "1600 = 1100 + 1200") == []
|
||||
assert _by_relation(report.soft_failed, "1600 = 1100 + 1200") == []
|
||||
|
||||
|
||||
def test_abs_terms_positive_expense_holds_and_mismatch_fails() -> None:
|
||||
# 66н пишет расход в скобках, 1С — со знаком минус; здесь положительный 2120: 100 − 60 = 40 выполнено по модулю
|
||||
holds = run_checks("INCOME", _lines(("2110", {0: "100"}), ("2120", {0: "60"}), ("2100", {0: "40"})), [0], RULES)
|
||||
assert _by_relation(holds.failed, "2100 = 2110 - 2120") == []
|
||||
broken = run_checks("INCOME", _lines(("2110", {0: "100"}), ("2120", {0: "60"}), ("2100", {0: "50"})), [0], RULES)
|
||||
failed = _by_relation(broken.failed, "2100 = 2110 - 2120")
|
||||
assert len(failed) == 1
|
||||
assert (failed[0].column_index, failed[0].expected, failed[0].actual) == (0, D("50"), D("40"))
|
||||
|
||||
|
||||
def test_subline_attached_to_last_parent_by_row_order() -> None:
|
||||
# M-9: 12310 и 12320 идут под строкой 1230, хотя префикса 1231/1232 среди четырёхзначных кодов нет
|
||||
lines = _lines(("1230", {0: "10"}), ("12310", {0: "6"}), ("12320", {0: "2"}))
|
||||
report = run_checks("BALANCE", lines, [0], RULES)
|
||||
assert [item.relation for item in _by_relation(report.soft_failed, "1230 = 12310 + 12320")] == [
|
||||
"1230 = 12310 + 12320"
|
||||
]
|
||||
|
||||
|
||||
def test_subline_prefers_last_parent_over_prefix() -> None:
|
||||
# 12310 стоит под 1230 (ближайшая строка выше с теми же тремя цифрами), а не под 1231, хотя префикс 1231 есть
|
||||
lines = _lines(("1231", {0: "5"}), ("1230", {0: "10"}), ("12310", {0: "6"}), ("12320", {0: "2"}))
|
||||
report = run_checks("BALANCE", lines, [0], RULES)
|
||||
relations = {item.relation for item in report.soft_failed}
|
||||
assert "1230 = 12310 + 12320" in relations
|
||||
assert not any(relation.startswith("1231 =") for relation in relations)
|
||||
|
||||
|
||||
def test_rounding_threshold_boundary() -> None:
|
||||
# порог = 0,001 % наибольшего по модулю числа соотношения; при итоге 10 000 000 он ровно 100
|
||||
assert D("10000000") * RULES.rounding_tolerance_percent / D("100") == D("100")
|
||||
halves = {0: "4999950"}
|
||||
at_threshold = run_checks(
|
||||
"BALANCE", _lines(("1100", halves), ("1200", halves), ("1600", {0: "10000000"})), [0], RULES
|
||||
)
|
||||
assert _by_relation(at_threshold.failed, "1600 = 1100 + 1200") == []
|
||||
assert [item.expected - item.actual for item in _by_relation(at_threshold.soft_failed, "1600 = 1100 + 1200")] == [
|
||||
D("100")
|
||||
]
|
||||
above = run_checks("BALANCE", _lines(("1100", halves), ("1200", halves), ("1600", {0: "10000001"})), [0], RULES)
|
||||
assert [item.expected - item.actual for item in _by_relation(above.failed, "1600 = 1100 + 1200")] == [D("101")]
|
||||
assert _by_relation(above.soft_failed, "1600 = 1100 + 1200") == []
|
||||
def test_no_core_check_rules() -> None:
|
||||
"""Копии словаря §7 и загрузчика/исполнителя правил в ядре нет (B6)."""
|
||||
assert not (API_DIR / "rules" / "statement_checks.yaml").exists()
|
||||
forbidden = ("load_check_rules", "run_checks", "statement_checks.yaml")
|
||||
for source in (API_DIR / "src").rglob("*.py"):
|
||||
text = source.read_text(encoding="utf-8")
|
||||
for name in forbidden:
|
||||
assert name not in text, f"{source.relative_to(API_DIR)}: {name}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -191,26 +43,6 @@ def test_cross_form(
|
||||
assert cross_form_check(b1370, i2400, D("1")) == expected
|
||||
|
||||
|
||||
def test_broken_rules_file(tmp_path) -> None:
|
||||
bad = tmp_path / "statement_checks.yaml"
|
||||
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_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"}))
|
||||
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")),
|
||||
]
|
||||
|
||||
|
||||
def test_subline_parents_nearest_parent_above_with_same_group() -> None:
|
||||
# 12310 и 12320 относятся к 1230 (ближайшая выше с теми же тремя цифрами), а не к 1231
|
||||
codes = ["1231", "1230", "12310", "12320"]
|
||||
@ -232,42 +64,3 @@ 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 =")] != []
|
||||
|
||||
@ -5,10 +5,24 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from src.domain.statement_lines import TemplateLine, load_line_catalog
|
||||
from src.services.statements_engine import DocumentType, FormVariant, default_engine
|
||||
|
||||
CATALOG = load_line_catalog()
|
||||
|
||||
|
||||
def test_name_dictionary_codes_in_code_orders() -> None:
|
||||
"""Редакции справочника имён — ровно редакции пакета, каждый код имени есть в перечне этой редакции копии пакета
|
||||
(любой вариант формы, B6; перечни — `Rules.code_orders`)."""
|
||||
engine = default_engine()
|
||||
editions = engine.editions()
|
||||
assert tuple(CATALOG.editions) == tuple(edition.value for edition in editions)
|
||||
for edition in editions:
|
||||
for form, document_type in (("BALANCE", DocumentType.BALANCE), ("INCOME", DocumentType.INCOME)):
|
||||
in_orders = {code for variant in FormVariant for code in engine.code_order(edition, document_type, variant)}
|
||||
missing = CATALOG.edition_codes(edition.value, form) - in_orders
|
||||
assert not missing, f"{edition.value}/{form}: коды справочника имён вне перечня пакета: {sorted(missing)}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("edition", "form", "count", "first", "last"),
|
||||
[
|
||||
|
||||
@ -58,3 +58,6 @@ Goals и Non-goals — см. `proposal.md`.
|
||||
| 2026-10-18 | **P3-47 (B5, T5.2 части А).** Нечитаемый JSON (`JSONDecodeError`, не UTF-8) в `_load_result` — тоже `MappingFatal` с текстом «результат не соответствует схеме recognition-result: файл не является JSON» (текст исключения парсера пользователю не показывается; M-1); не-объект (`[]`, `null`) отсекается моделью как нарушение схемы | иначе `ValueError` из `parse_result` уходил бы в повторяемые ошибки и файл `ERROR` получал бы только после `MAPPING_MAX_ATTEMPTS` попыток | ошибка JSON не отличается в тексте от прочих ошибок схемы |
|
||||
| 2026-10-18 | **P3-48 (B5).** Статус ОСВ `2.0` — `osv_status_v2(osv_done = sheet.status == "done", checks_ok, file_error_warning, counterparty, sheet_mapping_warnings)`; «предупреждения маппинга листа» = повтор счёта + сироты + «нарастающая по данным Q1» (вход `document_status` `1.x` тот же); у `1.x` статус как прежде (§9.3, статус задания). Предупреждение уровня `error` ищется в `metrics.warnings` результата (`file_warnings`), а не в `recognition_job.metrics` | Р-28; единый источник с формами | ОСВ с `error` в метриках файла — `NEEDS_CHECK` |
|
||||
| 2026-10-18 | **P3-49 (B5).** `osvDocument.warnings` пишутся в `document_issue` как `warning` / `service` с `severity` из предупреждения (по контракту `info`, `error` не отбрасывается); `line_refs` у ОСВ не сохраняются (строк формы нет), `column_index` и `field` — как пришли. Записи сервиса идут раньше записей маппинга ядра. Дубль листа (повтор счёта) своих предупреждений/статуса не вносит — документом остаётся первый лист | у ОСВ нет `line_ids`/колонок формы; повтор — D-7 | `error`-предупреждение листа ОСВ в `issues` не влияет на статус само по себе (статус — по `status` листа) |
|
||||
| 2026-10-18 | **P3-50 (B6).** `FailedCheck.codes` в ядре остаётся (вопреки плану, который удалял поле): оно нужно межформенной проверке 1370↔2400 (`codes = ("1370", "2400")`) для `codes` / `line_ids` в `checks.cross_form` ответа; нарушения §7 по колонкам читаются из `document_issue`, где коды хранятся сами | межформенную проверку пакет не считает (она по двум документам), без `codes` подсветка строк в ответе пропала бы | у `cross_form` не было бы `codes`/`line_ids`, фронт не подсвечивал бы строки 1370 и 2400 |
|
||||
| 2026-10-18 | **P3-51 (B6).** `checks_report` уже брал допуск у `engine.tolerance()` (с B2), поэтому правки сервиса в B6 нет; константа допуска ядра удалена вместе с `load_check_rules`. Состав и порядок редакций справочника имён `statement_lines.yaml` не переписывается на `engine.editions()` в коде: согласованность проверяет тест `test_name_dictionary_codes_in_code_orders` (редакции справочника = редакции пакета в том же порядке; каждый код справочника есть в перечне редакции пакета, любой вариант формы) | справочник остаётся данными имён и состава экрана; перечни кодов — единственный источник в пакете, расхождение ловит тест, а не молчаливая подмена в рантайме | при добавлении редакции в пакет без справочника имён тест красный; в рантайме новая редакция дала бы форму без шаблонных строк |
|
||||
| 2026-10-18 | **P3-52 (B6).** `edition_for` (граница 66n/fsbu4 по дате для шаблона экрана) в `domain/statement_mapping.py` не тронут: переход на `engine.edition(...)` требует кодов формы и варианта и относится к ответу формы (B7/доработка), а не к удалению копии словаря | не расширять B6 за рамки брифа | ответ формы и приём могут расходиться в редакции на формах без даты; существующее поведение сохранено |
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user