Refs #14 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
111 lines
5.9 KiB
Python
111 lines
5.9 KiB
Python
"""Словари форм (design D-10): загрузка, обязательные ключи, разбор соотношений, периоды форм."""
|
||
|
||
import datetime
|
||
from decimal import Decimal
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
import yaml
|
||
|
||
from src.parser.rules import DEFAULT_RULES_DIR, STATEMENT_CHECKS_FILE, Rules, load_rules
|
||
|
||
_CODE_MIN_SHARE = 0.6
|
||
_MIN_CODES = 3
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def rules() -> Rules:
|
||
return load_rules()
|
||
|
||
|
||
def test_form_type_by_title_and_okud(rules: Rules) -> None:
|
||
headers = rules.statement_headers
|
||
assert headers.form_type("Бухгалтерский баланс") == "BALANCE"
|
||
assert headers.form_type("ОТЧЕТ О ФИНАНСОВЫХ РЕЗУЛЬТАТАХ") == "INCOME"
|
||
assert headers.form_type("Отчет о прибылях и убытках") == "INCOME"
|
||
assert headers.form_type("Пояснения к бухгалтерскому балансу") == "DISCLOSURE"
|
||
assert headers.form_type("Бухгалтерская отчетность") is None # титул — не тип
|
||
assert headers.okud_type("Форма по ОКУД 0710001") == "BALANCE"
|
||
assert headers.okud_type("0710002") == "INCOME"
|
||
assert headers.okud_type("0710004") == "DISCLOSURE"
|
||
assert headers.okud_type("0710001") == "BALANCE"
|
||
|
||
|
||
def test_continuation_marker_and_captions(rules: Rules) -> None:
|
||
headers = rules.statement_headers
|
||
assert headers.is_continuation("Форма 0710001 с.2")
|
||
assert headers.is_continuation("Форма 0710002 с. 2")
|
||
assert not headers.is_continuation("Бухгалтерский баланс")
|
||
assert headers.is_code_caption("Код") and headers.is_code_caption("Коды")
|
||
assert not headers.is_code_caption("Код строки по ОКУД 0710001")
|
||
assert headers.is_name_caption("Наименование показателя")
|
||
assert headers.is_explanations_caption("Пояснения")
|
||
assert headers.code_ranges == {"BALANCE": (1100, 1799), "INCOME": (2100, 2999), "DISCLOSURE": (5000, 5999)}
|
||
assert headers.code_min_share == _CODE_MIN_SHARE and headers.min_codes == _MIN_CODES
|
||
|
||
|
||
def test_header_labels(rules: Rules) -> None:
|
||
headers = rules.statement_headers
|
||
assert headers.label("organization", "Организация:") is not None
|
||
assert headers.label("organization", "Организация (орган исполнительной власти)") is not None
|
||
assert headers.label("inn", "Идентификационный номер налогоплательщика") is not None
|
||
assert headers.label("inn", "ИНН") is not None
|
||
assert headers.label("units", "Единица измерения:") is not None
|
||
assert headers.label("date", "Дата (число, месяц, год)") is not None
|
||
assert headers.label("date", "Отчетная дата\n(число, месяц, год)") is not None
|
||
assert headers.label("address", "Местонахождение (адрес)") is not None
|
||
assert headers.label("address", "Адрес в пределах места нахождения") is not None
|
||
assert headers.label("okopf_okfs", "по ОКОПФ / ОКФС") is not None
|
||
assert headers.is_auditor_label("Наименование аудиторской организации")
|
||
assert headers.has_legal_form('ООО "НОВОРОССИЙСКИЙ ПРОКАТНЫЙ ЗАВОД"') and not headers.has_legal_form("Коды")
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("text", "expected"),
|
||
[
|
||
("На 31 декабря 2024 г.", datetime.date(2024, 12, 31)),
|
||
("на 30 сентября 2025 г.", datetime.date(2025, 9, 30)),
|
||
("На 31 декабря \n2023 года", datetime.date(2023, 12, 31)),
|
||
("31.12.2024", datetime.date(2024, 12, 31)),
|
||
("За 9 месяцев 2025 г.", datetime.date(2025, 9, 30)),
|
||
("За 9 месяцев\n2025 года", datetime.date(2025, 9, 30)),
|
||
("За Январь - Декабрь 2024 г.", datetime.date(2024, 12, 31)),
|
||
("за январь - сентябрь 2025 г.", datetime.date(2025, 9, 30)),
|
||
("За 2024 г.", datetime.date(2024, 12, 31)),
|
||
("2024", datetime.date(2024, 12, 31)),
|
||
("Изменение, %", None),
|
||
("Пояснения", None),
|
||
],
|
||
)
|
||
def test_period_end(rules: Rules, text: str, expected: datetime.date | None) -> None:
|
||
assert rules.statement_periods.period_end(text) == expected
|
||
|
||
|
||
def test_date_triple(rules: Rules) -> None:
|
||
assert rules.statement_periods.date_triple("31", "12", "2024") == datetime.date(2024, 12, 31)
|
||
assert rules.statement_periods.date_triple("30", "9", "2025") == datetime.date(2025, 9, 30)
|
||
assert rules.statement_periods.date_triple("", "12", "2024") is None
|
||
|
||
|
||
def test_relations_parsed_into_terms(rules: Rules) -> None:
|
||
checks = rules.statement_checks
|
||
balance = {rule.relation: rule for rule in checks.relations_for("BALANCE")}
|
||
rule = balance["1600 = 1100 + 1200"]
|
||
assert rule.strict and rule.target == "1600" and rule.terms == ((1, "1100"), (1, "1200"))
|
||
income = {rule.relation: rule for rule in checks.relations_for("INCOME")}
|
||
gross = income["2100 = 2110 - 2120"]
|
||
assert gross.terms == ((1, "2110"), (-1, "2120")) and gross.abs_terms == ("2120",)
|
||
assert not income["2400 = 2300 - 2410 + 2420"].strict
|
||
assert checks.tolerance == Decimal("1")
|
||
assert checks.relations_for("DISCLOSURE") == []
|
||
|
||
|
||
def test_bad_relation_rejected(tmp_path: Path) -> None:
|
||
for name in DEFAULT_RULES_DIR.glob("*.yaml"):
|
||
(tmp_path / name.name).write_bytes(name.read_bytes())
|
||
broken = yaml.safe_load((tmp_path / STATEMENT_CHECKS_FILE).read_text(encoding="utf-8"))
|
||
broken["relations"].append({"form": "BALANCE", "relation": "1600 = 1100 +", "strict": True})
|
||
(tmp_path / STATEMENT_CHECKS_FILE).write_text(yaml.safe_dump(broken, allow_unicode=True), encoding="utf-8")
|
||
with pytest.raises(RuntimeError, match="statement_checks.yaml"):
|
||
load_rules(tmp_path)
|