"""Детекторы на реальных ОСВ из ../fixtures: общие свойства на всех 66 файлах и две диагностики из спеки. Фикстуры — регрессия, не оракул: проверяются свойства, верные для любой ОСВ (лист ОСВ найден, шесть сумм, текст левее сумм, ИНН между текстом и суммами), и сценарий спеки «Плавающие колонки двух организаций». Буквы колонок сравниваются только здесь, в тестах, через `ColumnMap.as_diagnostics()` / `column_letter`. """ import time from pathlib import Path from typing import cast import pytest from src.contract import FileType from src.parser.detect import SheetKind, classify_sheet, find_table_header, first_data_row, is_header_repeat from src.parser.grid import Sheet, column_letter from src.parser.readers import read_book from src.parser.rules import Rules, load_rules DETECT_SECONDS_LIMIT = 5.0 RB_60 = "osv/rb/2025-Q1/ОСВ сч 60 за 1 кв 2025 год.xlsx" SCHASTL_62 = "osv/schastl/2024-Q3/Оборотносальдовая ведомость по счету 62 за 3 квартал 2024 г..xls" SCHASTL_62_LABEL_ROWS = 3 # Счет / Подразделение / Контрагенты (+ ИНН) SUM_COLUMNS = 6 HEADER_MAX_ROWS = 4 @pytest.fixture(scope="module") def rules() -> Rules: return load_rules() def _sheets(path: Path) -> tuple[Sheet, ...]: sheets, _ = read_book(path.read_bytes(), cast(FileType, path.suffix.lstrip(".").upper())) return sheets @pytest.mark.fixtures def test_every_real_osv_has_a_full_table_header(osv_file: Path, rules: Rules) -> None: sheets = _sheets(osv_file) started = time.perf_counter() osv_sheets = [sheet for sheet in sheets if classify_sheet(sheet, rules) is SheetKind.OSV] assert len(osv_sheets) == 1 sheet = osv_sheets[0] header = find_table_header(sheet, rules) data_start = first_data_row(sheet, header) repeats = [row for row in range(data_start, sheet.n_rows + 1) if is_header_repeat(sheet, row, header, rules)] assert time.perf_counter() - started < DETECT_SECONDS_LIMIT sums = header.columns.indexes() assert len(sums) == SUM_COLUMNS, header.columns.as_diagnostics() assert len(set(sums.values())) == SUM_COLUMNS assert header.columns.collapsed == frozenset() columns = header.columns for debit, credit in ( (columns.opening_debit, columns.opening_credit), (columns.turnover_debit, columns.turnover_credit), (columns.closing_debit, columns.closing_credit), ): assert debit is not None and credit is not None and debit < credit assert header.text_col < min(sums.values()) assert header.right_edge >= max(sums.values()) if header.inn_col is not None: assert header.text_col < header.inn_col < min(sums.values()) assert all(header.text_col < col <= header.right_edge for col in header.extra_cols) assert header.first_row <= header.last_row < header.first_row + HEADER_MAX_ROWS assert header.group_label_rows and header.group_label_rows[0] == header.first_row assert data_start > header.last_row # Первая строка данных — не метка заголовка: у неё есть сумма, либо это «Итого» пустой ведомости first_values = {col for col, _ in sheet.row_cells(data_start)} assert first_values & set(sums.values()) or any( pattern.search(sheet.text_at(data_start, header.text_col)) for pattern in rules.headers.total_row ), sheet.text_at(data_start, header.text_col) assert repeats == [] @pytest.mark.fixtures def test_rb_60_floating_columns(fixtures_dir: Path, rules: Rules) -> None: sheet = _sheets(fixtures_dir / RB_60)[0] header = find_table_header(sheet, rules) diagnostics = header.columns.as_diagnostics() assert [diagnostics[name] for name in header.columns.indexes()] == ["E", "H", "J", "L", "O", "Q"] assert header.inn_col is None assert column_letter(header.text_col) == "A" @pytest.mark.fixtures def test_schastl_62_floating_columns_inn_and_three_label_rows(fixtures_dir: Path, rules: Rules) -> None: sheet = _sheets(fixtures_dir / SCHASTL_62)[0] header = find_table_header(sheet, rules) diagnostics = header.columns.as_diagnostics() assert [diagnostics[name] for name in header.columns.indexes()] == ["G", "J", "K", "N", "Q", "S"] assert header.inn_col is not None assert column_letter(header.inn_col) == "E" assert len(header.group_label_rows) == SCHASTL_62_LABEL_ROWS # Мусор в T/U (spec «Мусор правее таблицы») — правее границы таблицы assert column_letter(header.right_edge) == "S" assert header.extra_cols == {}