- Пин 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>
120 lines
5.2 KiB
Python
120 lines
5.2 KiB
Python
"""Конвейер форм книги: кандидаты адаптера → пакет → документы и предупреждения файла контракта 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"]
|