arm-tabular-parser/api/tests/fixtures/test_classify_real.py
Andrey Epifancev 4a16f193af feat(parser): классификация строк по group_schema (§1.3)
classify.py: collect_body_rows — строки таблицы до «Итого» (пустые, из пробелов,
повторы заголовка и строки правее таблицы пропускаются; подвал после «Итого» не данные),
«Итого» по тексту и корневому оформлению до уровней, без требования сумм (R-13, T3.3),
шесть сумм через to_money с номером строки в предупреждениях, свёрнутое сальдо по знаку
(R-1); classify_rows — ACCOUNT по ACCOUNT_RE, позиция в схеме, OTHER за схемой, TOTAL,
COUNTERPARTY с ИНН (inn_digits), Вал./Кол./код валюты -> data_type, extra без
дубликатов ключей и без колонок подвала (T4.5).

Fixtures-тесты иерархии (R-2) и классификации на всех 66 реальных ОСВ.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 14:07:33 +04:00

82 lines
4.0 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 (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)
schema_types = {group.type for group in parsed.schema}
for row in parsed.data_rows:
assert row.group_type == "OTHER" or row.group_type in schema_types, row
assert row.data_type == "BU" # в фикстурах нет строк «Вал.»/«Кол.»
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)