"""Классификация строк на реальных ОСВ из ../fixtures (spec «Классификация строк», R-13, T4.5). Свойства на всех файлах: ровно одна строка TOTAL — последняя, уровень 0 без родителя; строки без предупреждений о числах; подвал после «Итого» не попадает ни в строки, ни в `extra`. Конкретные строки — по сценариям спеки. """ from pathlib import Path import pytest from real_osv import parse_real from src.parser.detect import INN_LENGTHS SCHASTL_62_Q3 = Path("osv/schastl/2024-Q3/Оборотносальдовая ведомость по счету 62 за 3 квартал 2024 г..xls") RB_66_FY = Path("osv/rb/2024-FY/ОСВ_66 за 2024.xls") RB_60_Q1_2024 = Path("osv/rb/2024-Q1/сч 60_ 1 кв 2024 г.xlsx") # после «Итого» — числа правее таблицы @pytest.mark.fixtures def test_real_osv_has_exactly_one_total_as_last_root_row(osv_file: Path) -> None: parsed = parse_real(osv_file) totals = [row for row in parsed.rows if row.group_type == "TOTAL"] assert len(totals) == 1 total = totals[0] assert total is parsed.rows[-1] assert (total.level, total.parent, total.body.row) == (0, None, parsed.total_row) assert parsed.warnings == [] @pytest.mark.fixtures def test_real_osv_rows_are_classified_into_schema(osv_file: Path) -> None: parsed = parse_real(osv_file) for row in parsed.data_rows: assert row.data_type == parsed.doc.data_type, row # в фикстурах нет строк «Вал.»/«Кол.» # Ни одной строки за схемой группировок: OTHER без имени группы допустим только у строк-показателей if row.data_type not in ("CURRENCY", "QUANTITY"): assert not (row.group_type == "OTHER" and row.group_name is None), row if row.counterparty_inn is not None: assert row.counterparty_inn.isdigit() assert len(row.counterparty_inn) in INN_LENGTHS @pytest.mark.fixtures def test_schastl_62_department_counterparty_with_and_without_inn(fixtures_dir: Path) -> None: """Spec «Подразделение, контрагент с ИНН, физлицо».""" parsed = parse_real(fixtures_dir / SCHASTL_62_Q3) assert {row.group_type for row in parsed.by_text("БП Аренда")} == {"DEPARTMENT"} poultry = parsed.by_text("МАЛЕЧКИНСКАЯ ПТИЦЕФАБРИКА ООО") assert poultry for row in poultry: assert row.group_type == "COUNTERPARTY" assert row.counterparty_name == "МАЛЕЧКИНСКАЯ ПТИЦЕФАБРИКА ООО" assert row.counterparty_inn is not None assert row.counterparty_inn.isdigit() assert len(row.counterparty_inn) == min(INN_LENGTHS) (payroll,) = parsed.by_text("Ведомость в счет з/платы") assert (payroll.group_type, payroll.counterparty_inn) == ("COUNTERPARTY", None) @pytest.mark.fixtures def test_rb_66_counterparty_inherits_subaccount(fixtures_dir: Path) -> None: """Одноимённый контрагент под 66.03 и 66.04 различается счётом ветки (Review Focus 3).""" parsed = parse_real(fixtures_dir / RB_66_FY) yug = parsed.by_text("Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ") assert [row.account_code_raw for row in yug] == ["66.03", "66.04"] assert {row.group_type for row in yug} == {"COUNTERPARTY"} assert yug[0].parent != yug[1].parent @pytest.mark.fixtures def test_rb_60_footer_after_total_is_not_data(fixtures_dir: Path) -> None: """T4.5: числа правее таблицы после «Итого» не становятся строками и не попадают в `extra`.""" parsed = parse_real(fixtures_dir / RB_60_Q1_2024) assert parsed.rows[-1].group_type == "TOTAL" assert parsed.total_row is not None assert parsed.sheet.n_rows > parsed.total_row # подвал есть assert all(row.extra is None for row in parsed.rows)