arm-tabular-parser/api/tests/fixtures/test_header_real.py
Andrey Epifancev 8e627e80c5 fix(parser): шапка ОСВ — «за»/«валюта» в названии организации, диапазоны дат с «г.» (T5.1–T5.5, T5.7)
- T5.1: организация исключается только по структурным меткам (лист, счёт, тип данных, параметры, субконто),
  строка периода выбирается по опознанному словарём периоду, метка валюты — только с двоеточием
- T5.2: конец периода — самое короткое начало, которое узнаёт словарь; диапазон «01.01.2025 г. - 31.03.2025 г.» цел
- T5.3: граница по цифрам в правиле счёта из имени файла
- T5.4: тест has_inn для варианта 2 (правило оставлено); T5.5: период только датами -> "" + предупреждение
- T5.7: тест обязательного ключа periods.end

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 13:19:18 +04:00

115 lines
5.2 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.

"""Шапка и схема группировок на реальных ОСВ из ../fixtures: общие свойства на всех 66 файлах.
Фикстуры — регрессия, не оракул: ожидаемые значения выводятся из имени файла и каталога одним общим правилом (без
таблицы исключений), а парсер читает только содержимое. Правило имени файла — `expected_account`.
"""
import re
from pathlib import Path
from typing import cast
import pytest
from fixtures_helpers import osv_fixture_files
from src.contract import FileType
from src.parser.detect import SheetKind, classify_sheet, find_table_header
from src.parser.grid import Sheet
from src.parser.header import build_group_schema, parse_doc_header
from src.parser.readers import read_book
from src.parser.rules import Rules, load_rules
# Первое число имени файла — счёт: «ОСВ_58 за 2024», «сч 01», «по счету 62 за 3 квартал»; «583» — три цифры —
# счёт и субсчёт без точки («58», субсчёт «3» -> «58.03»)
ACCOUNT_IN_NAME = re.compile(r"(?<!\d)\d{2,3}(?:\.\d{1,2})?(?!\d)")
SUBACCOUNT_WITHOUT_DOT = 3 # цифр в коде «счёт + субсчёт» без точки
ACCOUNT_DIGITS = 2 # цифр в корне счёта
SUBACCOUNT_DIGITS = 2 # цифр в записи субсчёта: «58.03»
PERIOD_DIR = re.compile(r"^(?P<year>\d{4})-(?P<kind>FY|Q[1-4])$")
EXPECTED_FILES = 66
@pytest.fixture(scope="module")
def rules() -> Rules:
return load_rules()
def expected_account(path: Path) -> str:
"""Код счёта из имени файла: первое число; три цифры без точки — счёт и субсчёт («583» -> «58.03»)."""
found = ACCOUNT_IN_NAME.search(path.stem)
assert found is not None, path.name
code = found.group(0)
if "." not in code and len(code) == SUBACCOUNT_WITHOUT_DOT:
return f"{code[:ACCOUNT_DIGITS]}.{code[ACCOUNT_DIGITS:].zfill(SUBACCOUNT_DIGITS)}"
return code
def _osv_sheet(path: Path, rules: Rules) -> Sheet:
sheets, _ = read_book(path.read_bytes(), cast(FileType, path.suffix.lstrip(".").upper()))
osv = [sheet for sheet in sheets if classify_sheet(sheet, rules) is SheetKind.OSV]
assert len(osv) == 1
return osv[0]
@pytest.mark.fixtures
def test_every_real_osv_header_and_group_schema(osv_file: Path, rules: Rules) -> None:
sheet = _osv_sheet(osv_file, rules)
table = find_table_header(sheet, rules)
doc = parse_doc_header(sheet, table, rules)
groups, variant = build_group_schema(sheet, table, rules)
assert doc.organization_name
assert doc.account_code_raw == expected_account(osv_file)
assert doc.period_text
assert doc.period_quarter is not None
assert doc.period_year is not None
assert doc.is_cumulative is not None
assert doc.data_type == "BU"
assert doc.currency == "RUB"
assert doc.header_variant == variant == 1
assert doc.warnings == ()
assert groups[0].type == "ACCOUNT"
assert len(groups) == len(table.group_label_rows)
@pytest.mark.fixtures
def test_real_osv_period_matches_its_directory(osv_file: Path, rules: Rules) -> None:
sheet = _osv_sheet(osv_file, rules)
doc = parse_doc_header(sheet, find_table_header(sheet, rules), rules)
directory = PERIOD_DIR.fullmatch(osv_file.parent.name)
assert directory is not None, osv_file.parent.name
year, kind = directory.group("year"), directory.group("kind")
assert doc.period_year == int(year)
if kind == "FY":
assert (doc.period_quarter, doc.is_cumulative) == ("Q4", True)
else:
assert doc.period_quarter == kind
if kind == "Q3":
assert doc.is_cumulative is False # ASSUMPTION(OQ-02): квартальная ведомость, не нарастающая
@pytest.mark.fixtures
def test_group_schemas_of_known_files(fixtures_dir: Path, rules: Rules) -> None:
schastl_62 = fixtures_dir / "osv/schastl/2024-Q3/Оборотносальдовая ведомость по счету 62 за 3 квартал 2024 г..xls"
sheet = _osv_sheet(schastl_62, rules)
groups, variant = build_group_schema(sheet, find_table_header(sheet, rules), rules)
assert variant == 1
assert [(g.type, g.has_inn) for g in groups] == [("ACCOUNT", None), ("DEPARTMENT", None), ("COUNTERPARTY", True)]
doc = parse_doc_header(sheet, find_table_header(sheet, rules), rules)
assert doc.period_text == "3 квартал 2024 г."
@pytest.mark.fixtures
def test_all_66_real_files_are_covered(fixtures_dir: Path) -> None:
assert len(osv_fixture_files(fixtures_dir)) == EXPECTED_FILES
def test_expected_account_rule() -> None:
assert expected_account(Path("сч 583_3 кв 2024.xlsx")) == "58.03"
assert expected_account(Path("ОСВ_01_ОС по субсчетам.xlsx")) == "01"
assert expected_account(Path("ОСВ_58 за 2024.xls")) == "58"
assert expected_account(Path("сч 66_3 кв 2024.xlsx")) == "66"
assert expected_account(Path("отчет 2024 сч 60.xlsx")) == "60" # четыре цифры — не счёт: граница по цифрам