Andrey Epifancev 387e92e014 pylib_ai_arm_statements 0.1.5: разрез по смене формы в конвейере форм (#87)
- Пин pylib_ai_arm_statements==0.1.5 (0.1.4 — группа альтернатив 2400, AURORA-1864;
  0.1.5 — split_by_form и редкий столбец, AURORA-1734/1735).
- StatementPipeline режет кандидата split_by_form перед normalize_candidate: ББ и ОФР
  одним листом без заголовка ОФР — два документа (§11, спека statement-normalization
  «Шаг 2 — разрез кандидата по смене формы»). Тест на такой лист.
- Golden трёх ОФР — fixtures #36 (уходит ложное 2400).
- CLAUDE.md: версия библиотеки — по пину, конвейер с split_by_form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 13:31:26 +04:00

120 lines
5.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.

"""Конвейер форм книги: кандидаты адаптера → пакет → документы и предупреждения файла контракта 2.0."""
import pytest
from arm_statements import Rules as StatementRules
from arm_statements import default_rules
from gridbuilder import at, sheet_from_rows
from result_contract import assert_valid_result
from src.contract import FileWarning, Metrics, ResultStatus
from src.parser import ParseOutcome
from src.parser.deadline import Deadline
from src.parser.detect import SheetKind
from src.parser.grid import Sheet
from src.parser.statements.pipeline import StatementPipeline
NO_DEADLINE = Deadline(10_000.0)
BALANCE_LINES = (
("Основные средства", 1150, 100),
("Итого по разделу I", 1100, 100),
("Запасы", 1210, 50),
("Итого по разделу II", 1200, 50),
("БАЛАНС", 1600, 150),
("Нераспределенная прибыль", 1370, 150),
("Итого по разделу III", 1300, 150),
("БАЛАНС", 1700, 150),
)
@pytest.fixture(scope="module")
def statement_rules() -> StatementRules:
return default_rules()
def _balance_rows() -> list[list[object]]:
"""Шапка из восьми строк, подпись столбцов в 9-й, первая строка с кодом — 10-я (код в столбце C)."""
return [
at({1: "Бухгалтерский баланс"}),
at({1: "на 31 марта 2025 г."}),
at({1: "Организация", 2: "ООО Ромашка"}),
at({1: "ИНН", 2: "7700000000"}),
at({1: "Единица измерения", 2: "тыс. руб."}),
[],
[],
[],
at({1: "Наименование показателя", 3: "Код", 4: "На 31 марта 2025 г."}),
*[at({1: title, 3: code, 4: amount}) for title, code, amount in BALANCE_LINES],
]
def _balance(name: str = "Баланс") -> Sheet:
return sheet_from_rows(_balance_rows(), name=name)
def test_document_converts_to_contract(statement_rules: StatementRules) -> None:
outcome = StatementPipeline(statement_rules).parse([(_balance(), SheetKind.STATEMENT)], NO_DEADLINE, xlsb=False)
assert outcome.file_warnings == ()
document = outcome.documents[0]
assert document.header.form_name == "Бухгалтерский баланс"
assert (document.header.inn, document.header.inn_text, document.header.inn_confidence) == (
"7700000000",
"7700000000",
100,
)
assert document.lines[0].ref == "Баланс!C10"
assert document.status == "done"
assert_valid_result(ParseOutcome(status=ResultStatus.DONE, documents=outcome.documents, metrics=Metrics(rows=8)))
def test_file_warnings_are_contract_models(statement_rules: StatementRules) -> None:
"""Лист без формы (нет заголовка и кодов) → документа нет, `SHEET_OTHER` как `FileWarning` с locator."""
sheet = sheet_from_rows([at({1: "Примечания"}), at({1: "Текст без кодов"})], name="Примечания")
outcome = StatementPipeline(statement_rules).parse(
[(sheet, SheetKind.STATEMENT_CONTINUATION)], NO_DEADLINE, xlsb=False
)
assert outcome.documents == ()
assert all(isinstance(w, FileWarning) for w in outcome.file_warnings)
assert [(w.code, w.severity, w.locator) for w in outcome.file_warnings] == [
("SHEET_OTHER", "info", "sheet:Примечания")
]
def test_xlsb_flag_adds_document_warning(statement_rules: StatementRules) -> None:
outcome = StatementPipeline(statement_rules).parse([(_balance(), SheetKind.STATEMENT)], NO_DEADLINE, xlsb=True)
assert "XLSB_NO_FORMATS" in {w.code for w in outcome.documents[0].warnings}
INCOME_LINES = (
("Выручка", 2110, 300),
("Себестоимость продаж", 2120, -200),
("Валовая прибыль (убыток)", 2100, 100),
("Прибыль (убыток) от продаж", 2200, 100),
("Чистая прибыль (убыток)", 2400, 100),
)
def test_balance_and_income_on_one_sheet_split_into_two_documents(statement_rules: StatementRules) -> None:
"""ББ и ОФР одним листом без заголовка ОФР: шапка таблицы ОФР режет кандидата (`split_by_form`, AURORA-1734)."""
rows = [
*_balance_rows(),
at({1: "Наименование показателя", 3: "Код", 4: "За январь - март 2025 г."}),
*[at({1: title, 3: code, 4: amount}) for title, code, amount in INCOME_LINES],
]
outcome = StatementPipeline(statement_rules).parse(
[(sheet_from_rows(rows, name="Баланс"), SheetKind.STATEMENT)], NO_DEADLINE, xlsb=False
)
assert [(d.document_type, d.locator) for d in outcome.documents] == [
("BALANCE", "sheet:Баланс"),
("INCOME", "sheet:Баланс;form:1"),
]
balance, income = outcome.documents
assert {line.line_code for line in balance.lines} == {str(code) for _, code, _ in BALANCE_LINES}
assert {line.line_code for line in income.lines} == {str(code) for _, code, _ in INCOME_LINES}
assert [str(c.period_date) for c in income.columns] == ["2025-03-31"]