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
83 lines
4.2 KiB
Python
83 lines
4.2 KiB
Python
"""Классификация строк на реальных ОСВ из ../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)
|