arm-tabular-parser/api/tests/fixtures/test_classify_real.py
Andrey Epifancev da7215b314 fix(parser): буквенные субсчета 76.АВ/ВА/ОТ — счета (T8.1, R-16); тесты T8.2, T8.3, T8.5
ACCOUNT_RE: сегмент субсчёта — 1–2 цифры или 1–3 прописные буквы кириллицы/латиницы
(без IGNORECASE); ранг — число сегментов («76.АВ» — сосед «76.04»). Остаточный
ложноположительный «12.ООО» описан в docstring.
Тесты: буквенные субсчета и их дети (outline и account_pattern), отказы «76.АВГД»/«76.ав»;
допуск «60,Расчеты» назван намеренным (R-18); fixtures-свойство «нет строк за схемой»,
data_type строк = data_type шапки (T8.3); негатив R-13: «Итого» без закрепления
вкладывается под последний счёт (T8.5).

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

83 lines
4.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 (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)