test(statements): синтетика st01…st05 и сценарии спеки на ней; параметры statement_file
Refs #14 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
This commit is contained in:
parent
6f0f05f7c7
commit
d64ba71098
@ -11,6 +11,8 @@ from fixtures_helpers import (
|
||||
osv_dkb_golden_files,
|
||||
osv_fixture_files,
|
||||
resolve_fixtures_dir,
|
||||
statement_files,
|
||||
statement_synthetic_files,
|
||||
synthetic_files,
|
||||
synthetic_skip_reason,
|
||||
)
|
||||
@ -59,6 +61,16 @@ def minimal_settings(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Callabl
|
||||
return build
|
||||
|
||||
|
||||
def statement_synthetic_skip_reason() -> str | None:
|
||||
"""Причина пропуска: каталог fixtures есть, но `statements-xlsx/synthetic` отсутствует или пуст.
|
||||
`None` — файлы есть или нет самих fixtures (тогда причину даёт `fixtures_skip_reason`)."""
|
||||
root = resolve_fixtures_dir()
|
||||
if not (root / "statements-xlsx").is_dir() or statement_synthetic_files(root):
|
||||
return None
|
||||
synth_path = root / "statements-xlsx" / "synthetic"
|
||||
return f"нет синтетики форм: {synth_path} отсутствует или пуст (сгенерируйте: см. README fixtures)"
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def synthetic_dir(fixtures_dir: Path) -> Path:
|
||||
"""`fixtures/osv/synthetic`; тест пропускается с причиной, если синтетики в каталоге нет."""
|
||||
@ -78,13 +90,19 @@ def fixtures_dir() -> Path:
|
||||
|
||||
|
||||
def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
|
||||
"""Параметры `osv_file` (реальные ОСВ), `dkb_file` (пакет ДКБ), `dkb_golden_file` (ДКБ до порога снимка) и
|
||||
`synthetic_file` (синтетика); без каталога — один параметр со skip."""
|
||||
"""Параметры `osv_file` (реальные ОСВ), `dkb_file` (пакет ДКБ), `dkb_golden_file` (ДКБ до порога снимка),
|
||||
`synthetic_file` (синтетика ОСВ), `statement_file` (реальные формы) и `statement_synthetic_file` (синтетика форм);
|
||||
без каталога — один параметр со skip."""
|
||||
sources = {
|
||||
"osv_file": (osv_fixture_files, fixtures_skip_reason),
|
||||
"dkb_file": (osv_dkb_files, fixtures_skip_reason),
|
||||
"dkb_golden_file": (osv_dkb_golden_files, fixtures_skip_reason),
|
||||
"synthetic_file": (synthetic_files, lambda: fixtures_skip_reason() or synthetic_skip_reason()),
|
||||
"statement_file": (statement_files, fixtures_skip_reason),
|
||||
"statement_synthetic_file": (
|
||||
statement_synthetic_files,
|
||||
lambda: fixtures_skip_reason() or statement_synthetic_skip_reason(),
|
||||
),
|
||||
}
|
||||
for argname, (listing, skip_reason) in sources.items():
|
||||
if argname not in metafunc.fixturenames:
|
||||
|
||||
@ -5,6 +5,7 @@ from pathlib import Path
|
||||
|
||||
FIXTURES_ENV = "ARM_KA_FIXTURES"
|
||||
OSV_EXTENSIONS = frozenset({".xls", ".xlsx", ".xlsb"})
|
||||
STATEMENTS_DIR = "statements-xlsx"
|
||||
_SYNTHETIC = "synthetic"
|
||||
_OSV_DKB = "osv-dkb"
|
||||
# Golden-снимок делается для файла пакета ДКБ не больше 1 МиБ: у больших (Агрокомплекс, 9–14 МБ, ~50 тыс. строк)
|
||||
@ -80,3 +81,22 @@ def synthetic_files(root: Path) -> list[Path]:
|
||||
if not directory.is_dir():
|
||||
return []
|
||||
return sorted(path for path in directory.iterdir() if _is_workbook(path, directory))
|
||||
|
||||
|
||||
def statement_files(root: Path) -> list[Path]:
|
||||
"""Книги форм `root/statements-xlsx/**` (включая `templates/`), кроме `synthetic/`; отсортированы."""
|
||||
directory = root / STATEMENTS_DIR
|
||||
if not directory.is_dir():
|
||||
return []
|
||||
synthetic = directory / _SYNTHETIC
|
||||
return sorted(
|
||||
path for path in directory.rglob("*") if _is_workbook(path, directory) and synthetic not in path.parents
|
||||
)
|
||||
|
||||
|
||||
def statement_synthetic_files(root: Path) -> list[Path]:
|
||||
"""Файлы синтетических форм `root/statements-xlsx/synthetic/*.xls|xlsx|xlsb`; отсортированы."""
|
||||
directory = root / STATEMENTS_DIR / _SYNTHETIC
|
||||
if not directory.is_dir():
|
||||
return []
|
||||
return sorted(path for path in directory.iterdir() if _is_workbook(path, directory))
|
||||
|
||||
@ -28,7 +28,7 @@ from openpyxl.styles import Alignment, Font
|
||||
|
||||
from tests.synthetic.xlsb_writer import write_xlsb
|
||||
|
||||
__all__ = ["FILE_NAMES", "generate_all", "main"]
|
||||
__all__ = ["FILE_NAMES", "generate_all", "main", "ORG", "SheetSpec", "OsvSpec", "build_osv", "base_tree", "to_xlsx"]
|
||||
|
||||
FIXED_TIME = datetime.datetime(2026, 1, 1, 0, 0, 0)
|
||||
ZIP_DATE_TIME = (2026, 1, 1, 0, 0, 0)
|
||||
|
||||
169
api/tests/synthetic/generate_statements.py
Normal file
169
api/tests/synthetic/generate_statements.py
Normal file
@ -0,0 +1,169 @@
|
||||
"""Синтетические формы ББ/ОФР (Срез 2b): `python -m tests.synthetic.generate_statements <каталог>` из `api/`.
|
||||
|
||||
st01 — ББ и ОФР на одном листе; st02 — «csv-подобный» лист: заголовок формы, коды без подписи «Код», заголовок в две
|
||||
строки, колонка «Изменение, %», отрицательные в скобках; st03 — форма без колонки кодов; st04 — книга с листом ОСВ
|
||||
и листом баланса; st05 — одинокая страница 2. Детерминирован (см. `generate.py`).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from collections.abc import Callable, Sequence
|
||||
from pathlib import Path
|
||||
|
||||
from tests.synthetic.generate import ORG, OsvSpec, SheetSpec, base_tree, build_osv, to_xlsx
|
||||
|
||||
__all__ = ["FILE_NAMES", "generate_all", "main"]
|
||||
|
||||
BALANCE_LINES = [
|
||||
("Основные средства", "1150", 100, 90),
|
||||
("Итого по разделу I", "1100", 100, 90),
|
||||
("Запасы", "1210", 50, 40),
|
||||
("Итого по разделу II", "1200", 50, 40),
|
||||
("БАЛАНС", "1600", 150, 130),
|
||||
("Уставный капитал", "1310", 150, 130),
|
||||
("Итого по разделу III", "1300", 150, 130),
|
||||
("БАЛАНС", "1700", 150, 130),
|
||||
]
|
||||
INCOME_LINES = [
|
||||
("Выручка", "2110", 1000, 900),
|
||||
("Себестоимость продаж", "2120", "(600)", "(500)"),
|
||||
("Валовая прибыль (убыток)", "2100", 400, 400),
|
||||
("Коммерческие расходы", "2210", "(50)", "(40)"),
|
||||
("Управленческие расходы", "2220", "(50)", "(60)"),
|
||||
("Прибыль (убыток) от продаж", "2200", 300, 300),
|
||||
("Проценты к уплате", "2330", "(100)", "(100)"),
|
||||
("Прибыль (убыток) до налогообложения", "2300", 200, 200),
|
||||
("Налог на прибыль", "2410", "(40)", "(40)"),
|
||||
("Чистая прибыль (убыток)", "2400", 160, 160),
|
||||
] # полный набор: все жёсткие соотношения ОФР сходятся; скобки — запись 66н
|
||||
|
||||
|
||||
def _head(sheet: SheetSpec, row: int, title: str, subtitle: str, okud: str) -> int:
|
||||
sheet.put(row, 2, title)
|
||||
sheet.put(row + 1, 2, subtitle)
|
||||
sheet.put(row + 2, 6, "Форма по ОКУД")
|
||||
sheet.put(row + 2, 7, okud)
|
||||
sheet.put(row + 3, 1, "Организация")
|
||||
sheet.put(row + 3, 3, ORG)
|
||||
sheet.put(row + 3, 6, "ИНН")
|
||||
sheet.put(row + 3, 7, "7700000000")
|
||||
sheet.put(row + 4, 1, "Единица измерения:")
|
||||
sheet.put(row + 4, 3, "тыс. руб.")
|
||||
return row + 6
|
||||
|
||||
|
||||
def _table(
|
||||
sheet: SheetSpec, row: int, labels: Sequence[str], lines: Sequence[tuple], *, caption: str | None = "Код"
|
||||
) -> int:
|
||||
sheet.put(row, 2, "Наименование показателя")
|
||||
if caption:
|
||||
sheet.put(row, 4, caption)
|
||||
for offset, label in enumerate(labels):
|
||||
sheet.put(row, 5 + offset, label)
|
||||
for index, (name, code, *values) in enumerate(lines, start=row + 1):
|
||||
sheet.put(index, 2, name)
|
||||
sheet.put(index, 4, code)
|
||||
for offset, value in enumerate(values):
|
||||
sheet.put(index, 5 + offset, value)
|
||||
return row + 1 + len(lines)
|
||||
|
||||
|
||||
def _st01() -> list[SheetSpec]:
|
||||
sheet = SheetSpec("Отчетность")
|
||||
row = _head(sheet, 1, "Бухгалтерский баланс", "на 31 декабря 2024 г.", "0710001")
|
||||
row = _table(sheet, row, ["На 31 декабря 2024 г.", "На 31 декабря 2023 г."], BALANCE_LINES)
|
||||
row = _head(sheet, row + 1, "Отчет о финансовых результатах", "за 2024 г.", "0710002")
|
||||
_table(sheet, row, ["За 2024 г.", "За 2023 г."], INCOME_LINES)
|
||||
return [sheet]
|
||||
|
||||
|
||||
def _st02() -> list[SheetSpec]:
|
||||
sheet = SheetSpec("Лист1")
|
||||
sheet.put(1, 1, "Бухгалтерский баланс")
|
||||
sheet.put(2, 1, "Организация")
|
||||
sheet.put(2, 2, 'ООО "Синтетика"')
|
||||
sheet.put(3, 1, "ИНН")
|
||||
sheet.put(3, 2, "7700000001")
|
||||
sheet.put(5, 1, "Показатель")
|
||||
sheet.put(5, 3, "На 31 декабря")
|
||||
sheet.put(5, 4, "На 31 декабря")
|
||||
sheet.put(5, 5, "Изменение, %")
|
||||
sheet.put(6, 3, "2024 г.")
|
||||
sheet.put(6, 4, "2023 г.")
|
||||
for index, (name, code, current, previous) in enumerate(BALANCE_LINES, start=7):
|
||||
sheet.put(index, 1, name)
|
||||
sheet.put(index, 2, code)
|
||||
# Adjust values to ensure check relations pass: 1200 = 1210
|
||||
if code == "1210":
|
||||
sheet.put(index, 3, "(50)")
|
||||
sheet.put(index, 4, "(40)")
|
||||
elif code == "1200":
|
||||
sheet.put(index, 3, "(50)")
|
||||
sheet.put(index, 4, "(40)")
|
||||
else:
|
||||
sheet.put(index, 3, current)
|
||||
sheet.put(index, 4, previous)
|
||||
sheet.put(index, 5, 5)
|
||||
return [sheet]
|
||||
|
||||
|
||||
def _st03() -> list[SheetSpec]:
|
||||
sheet = SheetSpec("ББ")
|
||||
row = _head(sheet, 1, "Бухгалтерский баланс", "на 31 декабря 2024 г.", "0710001")
|
||||
sheet.put(row, 2, "Показатель")
|
||||
sheet.put(row, 5, "На 31 декабря 2024 г.")
|
||||
for index, (name, _, current, _) in enumerate(BALANCE_LINES, start=row + 1):
|
||||
sheet.put(index, 2, name)
|
||||
sheet.put(index, 5, current)
|
||||
return [sheet]
|
||||
|
||||
|
||||
def _st04() -> list[SheetSpec]:
|
||||
balance = SheetSpec("Баланс")
|
||||
row = _head(balance, 1, "Бухгалтерский баланс", "на 31 декабря 2024 г.", "0710001")
|
||||
_table(balance, row, ["На 31 декабря 2024 г."], [(name, code, current) for name, code, current, _ in BALANCE_LINES])
|
||||
return [build_osv(OsvSpec(), base_tree()), balance]
|
||||
|
||||
|
||||
def _st05() -> list[SheetSpec]:
|
||||
sheet = SheetSpec("Лист_1")
|
||||
sheet.put(1, 8, "Форма 0710001 с.2")
|
||||
_table(sheet, 2, ["На 31 декабря 2024 г.", "На 31 декабря 2023 г."], BALANCE_LINES[5:])
|
||||
return [sheet]
|
||||
|
||||
|
||||
FILES: dict[str, tuple[Callable[[], list[SheetSpec]], Callable[[Sequence[SheetSpec]], bytes]]] = {
|
||||
"st01_both_forms_one_sheet.xlsx": (_st01, to_xlsx),
|
||||
"st02_csv_like.xlsx": (_st02, to_xlsx),
|
||||
"st03_no_codes.xlsx": (_st03, to_xlsx),
|
||||
"st04_mixed_book.xlsx": (_st04, to_xlsx),
|
||||
"st05_continuation_only.xlsx": (_st05, to_xlsx),
|
||||
}
|
||||
FILE_NAMES = tuple(FILES)
|
||||
|
||||
|
||||
def generate_all(directory: Path) -> list[Path]:
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
written: list[Path] = []
|
||||
for name, (build, serialize) in FILES.items():
|
||||
path = directory / name
|
||||
path.write_bytes(serialize(build()))
|
||||
written.append(path)
|
||||
return written
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="python -m tests.synthetic.generate_statements", description="Синтетические формы st01…st05."
|
||||
)
|
||||
parser.add_argument(
|
||||
"directory", type=Path, help="каталог назначения, например ../../fixtures/statements-xlsx/synthetic"
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
for path in generate_all(args.directory):
|
||||
print(path)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
18
api/tests/synthetic/test_generate_statements.py
Normal file
18
api/tests/synthetic/test_generate_statements.py
Normal file
@ -0,0 +1,18 @@
|
||||
from pathlib import Path
|
||||
|
||||
from tests.synthetic.generate_statements import FILE_NAMES, generate_all
|
||||
|
||||
EXPECTED = (
|
||||
"st01_both_forms_one_sheet.xlsx",
|
||||
"st02_csv_like.xlsx",
|
||||
"st03_no_codes.xlsx",
|
||||
"st04_mixed_book.xlsx",
|
||||
"st05_continuation_only.xlsx",
|
||||
)
|
||||
|
||||
|
||||
def test_names_and_determinism(tmp_path: Path) -> None:
|
||||
assert FILE_NAMES == EXPECTED
|
||||
first = {path.name: path.read_bytes() for path in generate_all(tmp_path / "a")}
|
||||
second = {path.name: path.read_bytes() for path in generate_all(tmp_path / "b")}
|
||||
assert first == second and set(first) == set(EXPECTED)
|
||||
211
api/tests/synthetic/test_statement_synthetic_files.py
Normal file
211
api/tests/synthetic/test_statement_synthetic_files.py
Normal file
@ -0,0 +1,211 @@
|
||||
"""Сценарии спеки `statement-parsing` на синтетических файлах (Срез 2b, Task 13).
|
||||
|
||||
Ожидания выведены из сценариев спеки и из содержимого генератора, а не из текущего вывода парсера:
|
||||
файл — вход, оракулом он не служит. Без каталога fixtures тесты пропускаются (маркер `fixtures`).
|
||||
"""
|
||||
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
from result_contract import assert_valid_result
|
||||
|
||||
from src.contract import FileType, ResultStatus, StatementDocument
|
||||
from src.contract.result import StatementLine
|
||||
from src.parser import ParseOutcome, get_parser
|
||||
|
||||
pytestmark = pytest.mark.fixtures
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _require_synthetic(fixtures_dir: Path) -> None:
|
||||
"""T13.1: fixtures есть, а `statements-xlsx/synthetic` нет/пуст — skip, а не падение."""
|
||||
|
||||
|
||||
SYNTHETIC_DIR = Path("statements-xlsx") / "synthetic"
|
||||
EXPECTED_NAMES = [
|
||||
"st01_both_forms_one_sheet.xlsx",
|
||||
"st02_csv_like.xlsx",
|
||||
"st03_no_codes.xlsx",
|
||||
"st04_mixed_book.xlsx",
|
||||
"st05_continuation_only.xlsx",
|
||||
]
|
||||
|
||||
ORG = 'ООО "Тестовая организация"'
|
||||
D = Decimal
|
||||
EXPECTED_DOCS_COUNT = 2
|
||||
EXPECTED_STATEMENT_COUNT = 1
|
||||
EXPECTED_LOCATORS_COUNT = 1
|
||||
EXPECTED_LINES_COUNT = 0
|
||||
|
||||
|
||||
def synthetic_path(fixtures_dir: Path, name: str) -> Path:
|
||||
return fixtures_dir / SYNTHETIC_DIR / name
|
||||
|
||||
|
||||
def parse_path(path: Path) -> ParseOutcome:
|
||||
return get_parser().parse(path.read_bytes(), cast(FileType, path.suffix.lstrip(".").upper()))
|
||||
|
||||
|
||||
def parse_named(fixtures_dir: Path, name: str) -> ParseOutcome:
|
||||
path = synthetic_path(fixtures_dir, name)
|
||||
if not path.is_file():
|
||||
pytest.fail(f"нет файла {name}: запустите генератор (см. README fixtures)")
|
||||
return parse_path(path)
|
||||
|
||||
|
||||
def statement_documents(outcome: ParseOutcome) -> list[StatementDocument]:
|
||||
"""Документы форм результата."""
|
||||
assert outcome.error is None, outcome.error
|
||||
return [doc for doc in outcome.documents if isinstance(doc, StatementDocument)]
|
||||
|
||||
|
||||
def one_statement(outcome: ParseOutcome) -> StatementDocument:
|
||||
(doc,) = statement_documents(outcome)
|
||||
return doc
|
||||
|
||||
|
||||
def warnings_of(outcome: ParseOutcome) -> list[str]:
|
||||
assert outcome.metrics is not None
|
||||
assert outcome.metrics.warnings is not None
|
||||
return outcome.metrics.warnings
|
||||
|
||||
|
||||
def by_code(doc: StatementDocument) -> dict[str, StatementLine]:
|
||||
"""Строки по коду показателя."""
|
||||
lines = {line.line_code: line for line in doc.lines}
|
||||
return lines
|
||||
|
||||
|
||||
def get_line_value(line: StatementLine, column_index: int = 0) -> D | None:
|
||||
"""Получить значение из строки по индексу колонки."""
|
||||
for value in line.values:
|
||||
if value.column_index == column_index:
|
||||
return value.amount
|
||||
return None
|
||||
|
||||
|
||||
# --- Состав набора и общие ожидания ---
|
||||
|
||||
|
||||
def test_synthetic_set_is_complete(fixtures_dir: Path) -> None:
|
||||
"""В `fixtures/statements-xlsx/synthetic/` лежат файлы набора st01…st05."""
|
||||
present = sorted(path.name for path in (fixtures_dir / SYNTHETIC_DIR).glob("st*") if path.is_file())
|
||||
assert present == EXPECTED_NAMES
|
||||
|
||||
|
||||
def test_every_synthetic_file_is_listed(statement_synthetic_file: Path) -> None:
|
||||
"""Каждый файл каталога — один из ожидаемых."""
|
||||
assert statement_synthetic_file.name in EXPECTED_NAMES
|
||||
|
||||
|
||||
# --- Сценарии спеки ---
|
||||
|
||||
|
||||
def test_st01_both_forms_one_sheet(fixtures_dir: Path) -> None:
|
||||
"""st01: ББ и ОФР на одном листе; два документа с одним `locator`."""
|
||||
outcome = parse_named(fixtures_dir, "st01_both_forms_one_sheet.xlsx")
|
||||
|
||||
stmt_docs = statement_documents(outcome)
|
||||
assert len(stmt_docs) == EXPECTED_DOCS_COUNT, (
|
||||
f"ожидается {EXPECTED_DOCS_COUNT} документа, получено {len(stmt_docs)}"
|
||||
)
|
||||
|
||||
# Оба документа имеют один locator
|
||||
locators = {doc.locator for doc in stmt_docs}
|
||||
assert len(locators) == EXPECTED_LOCATORS_COUNT, (
|
||||
f"ожидается {EXPECTED_LOCATORS_COUNT} уникальный locator, получено {len(locators)}"
|
||||
)
|
||||
|
||||
# Проверяем типы документов
|
||||
doc_types = {doc.document_type for doc in stmt_docs}
|
||||
assert "BALANCE" in doc_types and "INCOME" in doc_types, f"ожидаются типы BALANCE и INCOME, получены {doc_types}"
|
||||
|
||||
|
||||
def test_st02_csv_like_format(fixtures_dir: Path) -> None:
|
||||
"""st02: CSV-подобный лист; организация и ИНН найдены, units None с warning."""
|
||||
outcome = parse_named(fixtures_dir, "st02_csv_like.xlsx")
|
||||
|
||||
doc = one_statement(outcome)
|
||||
|
||||
# Проверяем организацию
|
||||
assert doc.header.organization_name == 'ООО "Синтетика"'
|
||||
assert doc.header.inn == "7700000001"
|
||||
|
||||
# Проверяем units = None с warning
|
||||
assert doc.header.units is None
|
||||
warnings = warnings_of(outcome)
|
||||
assert any("unit" in w.lower() or "измер" in w.lower() for w in warnings), (
|
||||
f"ожидается warning о единицах, получены: {warnings}"
|
||||
)
|
||||
|
||||
# Проверяем что документ содержит данные
|
||||
assert len(doc.lines) > 0, "ожидаются строки данных"
|
||||
|
||||
|
||||
def test_st03_no_code_column(fixtures_dir: Path) -> None:
|
||||
"""st03: форма без колонки кодов; needs_check, warning, lines = []."""
|
||||
outcome = parse_named(fixtures_dir, "st03_no_codes.xlsx")
|
||||
|
||||
doc = one_statement(outcome)
|
||||
|
||||
# Статус needs_check
|
||||
assert outcome.status is ResultStatus.NEEDS_CHECK
|
||||
|
||||
# Нет найденных строк
|
||||
assert len(doc.lines) == EXPECTED_LINES_COUNT, f"ожидается {EXPECTED_LINES_COUNT} строк, получено {len(doc.lines)}"
|
||||
|
||||
# Есть warning о коде
|
||||
warnings = warnings_of(outcome)
|
||||
assert any("code" in w.lower() or "код" in w.lower() for w in warnings), (
|
||||
f"ожидается warning о коде, получены: {warnings}"
|
||||
)
|
||||
|
||||
|
||||
def test_st04_mixed_book_osv_and_statement(fixtures_dir: Path) -> None:
|
||||
"""st04: книга с листом ОСВ и листом баланса; OsvDocument + StatementDocument, статус done."""
|
||||
outcome = parse_named(fixtures_dir, "st04_mixed_book.xlsx")
|
||||
|
||||
stmt_docs = statement_documents(outcome)
|
||||
assert len(stmt_docs) == EXPECTED_STATEMENT_COUNT, (
|
||||
f"ожидается {EXPECTED_STATEMENT_COUNT} StatementDocument, получено {len(stmt_docs)}"
|
||||
)
|
||||
|
||||
# Проверяем что есть также OsvDocument (проверяя общее количество документов)
|
||||
assert len(outcome.documents) == EXPECTED_DOCS_COUNT, (
|
||||
f"ожидается {EXPECTED_DOCS_COUNT} документа всего (OSV + Statement), получено {len(outcome.documents)}"
|
||||
)
|
||||
|
||||
# Статус done
|
||||
assert outcome.status is ResultStatus.DONE
|
||||
|
||||
|
||||
def test_st05_continuation_page(fixtures_dir: Path) -> None:
|
||||
"""st05: одинокая страница 2; needs_check, warning, содержит коды."""
|
||||
outcome = parse_named(fixtures_dir, "st05_continuation_only.xlsx")
|
||||
|
||||
doc = one_statement(outcome)
|
||||
|
||||
# Статус needs_check
|
||||
assert outcome.status is ResultStatus.NEEDS_CHECK
|
||||
|
||||
# Есть warning о continuation
|
||||
warnings = warnings_of(outcome)
|
||||
assert any(
|
||||
"continuation" in w.lower() or "страниц" in w.lower() or "страница" in w.lower() or "page" in w.lower()
|
||||
for w in warnings
|
||||
), f"ожидается warning о continuation, получены: {warnings}"
|
||||
|
||||
# Проверяем что есть коды
|
||||
codes = {line.line_code for line in doc.lines if line.line_code}
|
||||
assert len(codes) > 0, "ожидаются коды, получены пустое множество"
|
||||
|
||||
|
||||
# --- Валидация контракта ---
|
||||
|
||||
|
||||
def test_all_synthetic_results_are_valid(statement_synthetic_file: Path) -> None:
|
||||
"""Все результаты валидны по контракту (общая проверка для всех синтетических файлов)."""
|
||||
outcome = parse_path(statement_synthetic_file)
|
||||
assert_valid_result(outcome)
|
||||
Loading…
x
Reference in New Issue
Block a user