This repository has been archived on 2026-10-26. You can view files and clone it, but cannot push or open issues or pull requests.
Andrey Epifancev 547f9efcdb feat(#5): шаг 2 — числа правее последней подписанной колонки не колонки сумм
Столбец без подписи правее последней колонки с подписью периода колонкой сумм
не считается; вместо COLUMN_WITHOUT_DATE — info NUMBERS_OUTSIDE_TABLE с адресами
ячеек. Решение владельца 18.10.2026 (arm-ka-docs#116, Р-31).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 21:18:07 +04:00

169 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import re
from decimal import Decimal
from pathlib import Path
import pytest
import yaml
from arm_statements.issues import WarningCode, make_warning
from arm_statements.loader import LABEL_KEYS, Rules, RulesError, load_rules
from arm_statements.model import DocumentType, Edition, FormVariant, Severity
RULES_DIR = Path(__file__).parent.parent / "rules"
SPEC_WARNINGS = {
"JSON_FAILED": ("error", "file"),
"TABLE_PARSE_ERROR": ("error", "file"),
"TABLE_LOST": ("error", "file"),
"TABLE_SKIPPED": ("info", "file"),
"CODE_COLUMN_NOT_FOUND": ("error", "document"),
"SEGMENT_WITHOUT_CODES": ("error", "document"),
"SHEET_NOT_JOINED": ("error", "document"),
"CONTINUATION_TYPE_CONFLICT": ("error", "document"),
"SEGMENT_COLUMNS_MISMATCH": ("error", "document"),
"COLUMN_WITHOUT_DATE": ("error", "document"),
"COLUMN_DATE_UNCERTAIN": ("error", "document"),
"HEADER_ROW_NOT_FOUND": ("error", "document"),
"TYPE_CONFLICT": ("error", "document"),
"NO_ORGANIZATION_NO_INN": ("error", "document"),
"PERIOD_UNKNOWN": ("error", "document"),
"REPORT_DATE_UNCERTAIN": ("error", "document"),
"XLSB_SCALE_SUSPECT": ("error", "document"),
"INN_NOT_FOUND": ("info", "document"),
"INN_UNCERTAIN": ("info", "document"),
"ORGANIZATION_NOT_FOUND": ("info", "document"),
"ORGANIZATION_UNCERTAIN": ("info", "document"),
"REPORT_DATE_UNPARSED": ("info", "document"),
"UNITS_NOT_FOUND": ("info", "document"),
"FILE_TYPE_MISMATCH": ("info", "file"),
"COLUMN_SKIPPED": ("info", "document"),
"FRACTIONAL_IN_THOUSANDS": ("info", "document"),
"XLSB_NO_FORMATS": ("info", "document"),
"NUMBER_FORMAT_UNPARSED": ("info", "document"),
"VALUE_OUT_OF_RANGE": ("info", "document"),
"SHEET_OTHER": ("info", "file"),
"ROW_SPLIT": ("info", "document"),
"NUMBERS_OUTSIDE_TABLE": ("info", "document"),
"XLS_NO_CODEPAGE": ("info", "file"),
}
def test_warning_table_matches_spec(rules: Rules) -> None:
assert {c: (w.severity.value, w.scope) for c, w in rules.warnings.items()} == SPEC_WARNINGS
def test_simplified_income_relation_is_soft(rules: Rules) -> None:
(rule,) = rules.checks.relations_for(DocumentType.INCOME, FormVariant.SIMPLIFIED)
assert rule.target == "2400" and not rule.strict
assert rule.abs_terms == frozenset({"2120", "2330", "2350", "2410"})
assert rules.checks.abs_codes(DocumentType.INCOME, FormVariant.FULL) == frozenset(
{"2120", "2210", "2220", "2330", "2350", "2410"}
)
def test_code_orders_with_repeats(rules: Rules) -> None:
fsbu = rules.code_orders.sequence(Edition.FSBU4, DocumentType.BALANCE, FormVariant.FULL)
assert fsbu.count("1220") == len(("1220", "1220")) and "1120" not in fsbu
ras = rules.code_orders.sequence(Edition.RAS_66N, DocumentType.BALANCE, FormVariant.FULL)
assert "1120" in ras and ras.index("1110") < ras.index("1100")
assert rules.code_orders.editions == (Edition.RAS_66N, Edition.FSBU4)
assert rules.code_orders.sequence(Edition.FSBU4, DocumentType.DISCLOSURE, FormVariant.FULL) == ()
def test_public_rule_types(rules: Rules) -> None:
assert isinstance(rules.checks.tolerance, Decimal) and rules.checks.tolerance == Decimal("1")
assert all(isinstance(e, Edition) for e in rules.code_orders.editions)
fsbu = rules.code_orders.sequence(Edition.FSBU4, DocumentType.INCOME, FormVariant.SIMPLIFIED)
assert isinstance(fsbu, tuple) and all(isinstance(c, str) for c in fsbu)
assert (rules.code_ranges.min_codes_continuation, rules.code_ranges.min_codes) == (1, 3)
assert tuple(rules.headers.labels) == LABEL_KEYS
def test_warning_code_enum_matches_dictionary(rules: Rules) -> None:
assert {c.value for c in WarningCode} == set(rules.warnings) == set(SPEC_WARNINGS)
assert WarningCode.XLS_NO_CODEPAGE == "XLS_NO_CODEPAGE"
def test_no_code_to_name_mapping_anywhere() -> None:
code = re.compile(r"^\d{4,5}$")
def walk(node: object) -> None:
if isinstance(node, dict):
for key, value in node.items():
assert not (code.match(str(key)) and isinstance(value, str) and re.search(r"[А-Яа-яЁё]", value))
walk(value)
elif isinstance(node, list):
for item in node:
walk(item)
for path in RULES_DIR.glob("*.yaml"):
walk(yaml.safe_load(path.read_text(encoding="utf-8")))
def test_broken_repeat_count_rejected(tmp_path: Path) -> None:
for path in RULES_DIR.glob("*.yaml"):
(tmp_path / path.name).write_bytes(path.read_bytes())
orders = tmp_path / "statement_code_orders.yaml"
orders.write_text(
orders.read_text(encoding="utf-8").replace('- "1520" # «Обязательство', '# "1520"'), encoding="utf-8"
)
with pytest.raises(RulesError, match="statement_code_orders"):
load_rules(tmp_path)
def test_unknown_severity_rejected(tmp_path: Path) -> None:
for path in RULES_DIR.glob("*.yaml"):
(tmp_path / path.name).write_bytes(path.read_bytes())
target = tmp_path / "statement_warnings.yaml"
target.write_text(
target.read_text(encoding="utf-8").replace("severity: info", "severity: warn", 1), encoding="utf-8"
)
with pytest.raises(RulesError, match="statement_warnings"):
load_rules(tmp_path)
def test_make_warning_takes_severity_from_dictionary(rules: Rules) -> None:
warning = make_warning(WarningCode.COLUMN_SKIPPED, "колонка «Изменение, %» пропущена", rules, column_index=5)
assert (warning.code, warning.severity, warning.column_index) == ("COLUMN_SKIPPED", Severity.INFO, 5)
assert make_warning("JSON_FAILED", "x", rules).severity is Severity.ERROR
def test_make_warning_unknown_code_raises(rules: Rules) -> None:
with pytest.raises(KeyError, match="NOT_A_CODE"):
make_warning("NOT_A_CODE", "x", rules)
def test_label_keys_are_checked(tmp_path: Path) -> None:
for path in RULES_DIR.glob("*.yaml"):
(tmp_path / path.name).write_bytes(path.read_bytes())
target = tmp_path / "statement_headers.yaml"
target.write_text(target.read_text(encoding="utf-8").replace(" okpo:", " okpo_x:", 1), encoding="utf-8")
with pytest.raises(RulesError, match="statement_headers"):
load_rules(tmp_path)
def test_absolute_period_kinds_come_from_dictionary(rules: Rules) -> None:
expected = [
"range_end",
"day_month_year",
"quarter",
"half",
"months",
"month_range",
"year",
]
data = yaml.safe_load((RULES_DIR / "statement_periods.yaml").read_text(encoding="utf-8"))
assert [p["kind"] for p in data["absolute"]] == expected
assert [p.kind for p in rules.periods.absolute] == expected
def test_repeats_for_unknown_form_rejected(tmp_path: Path) -> None:
for path in RULES_DIR.glob("*.yaml"):
(tmp_path / path.name).write_bytes(path.read_bytes())
orders = tmp_path / "statement_code_orders.yaml"
orders.write_text(
orders.read_text(encoding="utf-8").replace(" INCOME: []", " DISCLOSURE: []", 1), encoding="utf-8"
)
with pytest.raises(RulesError, match="repeats"):
load_rules(tmp_path)