- golden_helpers, scripts/golden_update.py (--check), test_golden на 66 ОСВ и синтетике; make golden-update/golden-check/test-fixtures - тест покрытия словарей (R-22): счётчик срабатываний Pattern; из osv_headers.yaml удалены неиспользуемые service_columns kpp/code/quantity/currency - синтетика s14–s16 в генераторе и таблице ожиданий; skip вместо падения при отсутствии osv/synthetic (T11.1); комментарий R-21 в s07 (T11.2) - линтер: предел адреса 10 вместо 4 (T3.1), обращение cell(3, 4) (T3.8); реальные файлы не фиксируют BU/RUB (T3.9) - README: golden-процесс; отмечены Task 12 и пункты 4.2, 4.3 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
115 lines
5.3 KiB
Python
115 lines
5.3 KiB
Python
"""Шапка и схема группировок на реальных ОСВ из ../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 is not None # T3.9: значение («BU» и др.) не фиксируем — файлы не спецификация
|
||
assert doc.currency is not None
|
||
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" # четыре цифры — не счёт: граница по цифрам
|