arm-tabular-parser/api/tests/fixtures/test_classify_real.py
Andrey Epifancev 195720c97b Итог ищется по точному «Итого» раньше «Итого …»; хвост — только числа в колонках таблицы (#47, AURORA-1664)
I-1: _find_total сначала берёт первую строку корневого уровня с текстом ровно «Итого»
(новый ключ total_row_exact в osv_headers.yaml) и лишь без неё — первую по total_row.
Контрагент «Итого-Сервис» на плоском листе без outline и отступов больше не обрезает таблицу.
O-3: предупреждение о хвосте считает числа только в колонках таблицы (text_col…right_edge
из TableHeader); контрольные суммы правее таблицы (rb/2024-Q1) его не дают, ослабление
в test_classify_real откачено. Синтетика s32: итог «Итого по счету 60» без точного «Итого»
(нужна тесту покрытия для запасного паттерна total_row).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 12:04:48 +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)