test(statements): failed_relations в манифесте, статус документа по критерию парсера, строгая шапка (T14.I2)

Refs #14

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
This commit is contained in:
Andrey Epifancev 2026-10-06 21:04:50 +04:00
parent d82940a8ac
commit 6cd2ea8a7a
3 changed files with 60 additions and 22 deletions

View File

@ -17,6 +17,32 @@ EXPECTED_FILES = 18
MAX_PARSE_SECONDS = 5 MAX_PARSE_SECONDS = 5
def document_ok(document: StatementDocument) -> bool:
"""Документ пригоден (done): критерий `StatementParser._is_ok`, выраженный полями результата."""
if document.document_type == "DISCLOSURE":
return True
checks = document.checks
return (
checks is not None
and bool(document.columns)
and bool(document.header.organization_name or document.header.inn)
and not checks.relations_failed
and not checks.duplicate_line_codes
)
def explained_needs_check(document: StatementDocument) -> bool:
"""Причина needs_check видна в результате: нарушенное соотношение, дубликаты кодов, нет колонок или шапки."""
checks = document.checks
return bool(
checks is None
or checks.relations_failed
or checks.duplicate_line_codes
or not document.columns
or not (document.header.organization_name or document.header.inn)
)
def test_manifest_covers_every_file(fixtures_dir: Path) -> None: def test_manifest_covers_every_file(fixtures_dir: Path) -> None:
files = statement_files(fixtures_dir) files = statement_files(fixtures_dir)
assert len(files) == EXPECTED_FILES assert len(files) == EXPECTED_FILES
@ -40,13 +66,24 @@ def test_statement_file_matches_manifest(fixtures_dir: Path, statement_file: Pat
assert all(value.column_index < len(document.columns) for line in document.lines for value in line.values) assert all(value.column_index < len(document.columns) for line in document.lines for value in line.values)
if item.sheets: if item.sheets:
assert document.locator.count(",") == item.sheets - 1 assert document.locator.count(",") == item.sheets - 1
failed = {relation.relation for relation in document.checks.relations_failed} if document.checks else set()
if item.failed_relations is not None:
assert failed == set(item.failed_relations)
if item.complete: if item.complete:
assert document.checks is not None assert document.checks is not None
assert document.checks.relations_failed == [], document.checks assert document.checks.relations_failed == [], document.checks
assert document.columns assert document.columns
assert all(column.period_date is not None for column in document.columns) assert all(column.period_date is not None for column in document.columns)
if expectation.organization and document.header.organization_name is not None: # статус документа — по критерию парсера (statement.py::_is_ok), независимо от манифеста
assert document_ok(document) == (item.status == "done"), (item, document.header)
if item.status == "needs_check":
assert not document_ok(document)
assert explained_needs_check(document), "needs_check должен объясняться полями результата"
if expectation.organization:
assert document.header.organization_name == expectation.organization assert document.header.organization_name == expectation.organization
if expectation.inn and document.document_type not in expectation.inn_optional_for and document.header.inn: if expectation.inn:
assert document.header.inn == expectation.inn if document.document_type in expectation.inn_optional_for:
assert document.header.inn in {None, expectation.inn}
else:
assert document.header.inn == expectation.inn
assert_valid_result(outcome) assert_valid_result(outcome)

View File

@ -19,6 +19,7 @@ class DocumentExpectation(BaseModel):
complete: bool complete: bool
reason: str | None = None reason: str | None = None
sheets: int | None = None sheets: int | None = None
failed_relations: list[str] | None = None # None — не проверяется; список — точное множество relations_failed
class Expectation(BaseModel): class Expectation(BaseModel):

View File

@ -6,59 +6,59 @@ files:
"unagrande-co/2024-FY/Б.Б Лист 1.xls": "unagrande-co/2024-FY/Б.Б Лист 1.xls":
organization: 'Акционерное общество "Унагранде Компани"' organization: 'Акционерное общество "Унагранде Компани"'
inn: "3249004697" inn: "3249004697"
documents: [{type: BALANCE, status: needs_check, complete: false, reason: "только актив: 1600 = 1700 не сходится"}] documents: [{type: BALANCE, status: needs_check, complete: false, reason: "только актив: 1600 = 1700 не сходится", failed_relations: ["1600 = 1700"]}]
"unagrande-co/2024-FY/Б.Б. Лист 2.xls": "unagrande-co/2024-FY/Б.Б. Лист 2.xls":
documents: [{type: BALANCE, status: needs_check, complete: false, reason: "страница 2 без шапки"}] documents: [{type: BALANCE, status: needs_check, complete: false, reason: "страница 2 без шапки", failed_relations: []}]
"unagrande-co/2024-FY/ОФР. Лист 1.xls": "unagrande-co/2024-FY/ОФР. Лист 1.xls":
organization: 'Акционерное общество "Унагранде Компани"' organization: 'Акционерное общество "Унагранде Компани"'
inn: "3249004697" inn: "3249004697"
documents: documents:
- {type: INCOME, status: needs_check, complete: false, reason: "по факту: значения в рублях с копейками при шапке тыс. руб., итоги 2100/2200/2300 округлены до тысяч и расходятся с суммой частей (OQ-137)"} - {type: INCOME, status: needs_check, complete: false, reason: "по факту: значения в рублях с копейками при шапке тыс. руб., итоги 2100/2200/2300 округлены до тысяч и расходятся с суммой частей (OQ-137)", failed_relations: ["2100 = 2110 - 2120", "2200 = 2100 - 2210 - 2220", "2300 = 2200 + 2310 + 2320 - 2330 + 2340 - 2350"]}
"unagrande-co/2024-FY/ОФР. Лист 2 .xls": "unagrande-co/2024-FY/ОФР. Лист 2 .xls":
documents: [{type: INCOME, status: needs_check, complete: false, reason: "страница 2 без шапки"}] documents: [{type: INCOME, status: needs_check, complete: false, reason: "страница 2 без шапки", failed_relations: []}]
"unagrande-group/2024-FY/ББ. Лист 1 .xls": "unagrande-group/2024-FY/ББ. Лист 1 .xls":
organization: 'Акционерное общество "Унагранде Групп"' organization: 'Акционерное общество "Унагранде Групп"'
inn: "7728757141" inn: "7728757141"
documents: [{type: BALANCE, status: needs_check, complete: false, reason: "только актив"}] documents: [{type: BALANCE, status: needs_check, complete: false, reason: "только актив", failed_relations: ["1600 = 1700"]}]
"unagrande-group/2024-FY/ББ. Лист 2 (2).xls": "unagrande-group/2024-FY/ББ. Лист 2 (2).xls":
documents: [{type: BALANCE, status: needs_check, complete: false, reason: "страница 2 без шапки"}] documents: [{type: BALANCE, status: needs_check, complete: false, reason: "страница 2 без шапки", failed_relations: []}]
"unagrande-group/2024-FY/ОФР. Лист 2.xls": "unagrande-group/2024-FY/ОФР. Лист 2.xls":
documents: [{type: INCOME, status: needs_check, complete: false, reason: "страница 2 без шапки, стр. 1 в пакете нет"}] documents: [{type: INCOME, status: needs_check, complete: false, reason: "страница 2 без шапки, стр. 1 в пакете нет", failed_relations: []}]
"unagrande-group/2025-Q3/ББ. Лист 1 3кв.25.xls": "unagrande-group/2025-Q3/ББ. Лист 1 3кв.25.xls":
organization: 'Акционерное общество "Унагранде Групп"' organization: 'Акционерное общество "Унагранде Групп"'
inn: "7728757141" inn: "7728757141"
documents: [{type: BALANCE, status: needs_check, complete: false, reason: "только актив"}] documents: [{type: BALANCE, status: needs_check, complete: false, reason: "только актив", failed_relations: ["1600 = 1700"]}]
"unagrande-group/2025-Q3/ББ. Лист 2 3кв.25.xls": "unagrande-group/2025-Q3/ББ. Лист 2 3кв.25.xls":
documents: [{type: BALANCE, status: needs_check, complete: false, reason: "страница 2 без шапки"}] documents: [{type: BALANCE, status: needs_check, complete: false, reason: "страница 2 без шапки", failed_relations: []}]
"unagrande-group/2025-Q3/ОФР. Лист 1 3кв.25.xls": "unagrande-group/2025-Q3/ОФР. Лист 1 3кв.25.xls":
organization: 'Акционерное общество "Унагранде Групп"' organization: 'Акционерное общество "Унагранде Групп"'
inn: "7728757141" inn: "7728757141"
documents: documents:
- {type: INCOME, status: needs_check, complete: false, reason: "по факту: значения в рублях с копейками при шапке тыс. руб., 2100 расходится на 17.50/9.43 руб., 2200/2300 — округление итогов до тысяч (OQ-137)"} - {type: INCOME, status: needs_check, complete: false, reason: "по факту: значения в рублях с копейками при шапке тыс. руб., 2100 расходится на 17.50/9.43 руб., 2200/2300 — округление итогов до тысяч (OQ-137)", failed_relations: ["2100 = 2110 - 2120", "2200 = 2100 - 2210 - 2220", "2300 = 2200 + 2310 + 2320 - 2330 + 2340 - 2350"]}
"unagrande-group/2025-Q3/ОФР. Лист 2 3кв.25.xls": "unagrande-group/2025-Q3/ОФР. Лист 2 3кв.25.xls":
documents: [{type: INCOME, status: needs_check, complete: false, reason: "страница 2 без шапки"}] documents: [{type: INCOME, status: needs_check, complete: false, reason: "страница 2 без шапки", failed_relations: []}]
"npz/2025-Q3/Бухгалтерская отчетность за Январь - Сентябрь 2025 г. (НОВОРОССИЙСКИЙ ПРОКАТНЫЙ ЗАВОД).xlsx": "npz/2025-Q3/Бухгалтерская отчетность за Январь - Сентябрь 2025 г. (НОВОРОССИЙСКИЙ ПРОКАТНЫЙ ЗАВОД).xlsx":
organization: 'ООО "НОВОРОССИЙСКИЙ ПРОКАТНЫЙ ЗАВОД"' organization: 'ООО "НОВОРОССИЙСКИЙ ПРОКАТНЫЙ ЗАВОД"'
inn: "2315995868" inn: "2315995868"
inn_optional_for: [INCOME] inn_optional_for: [INCOME]
documents: documents:
- {type: BALANCE, status: needs_check, complete: false, sheets: 5, reason: "1600 = 1700 расходится на 1.94/9.02/2.48 руб.: значения в рублях при шапке тыс. руб. (OQ-137)"} - {type: BALANCE, status: needs_check, complete: false, sheets: 5, reason: "1600 = 1700 расходится на 1.94/9.02/2.48 руб.: значения в рублях при шапке тыс. руб. (OQ-137)", failed_relations: ["1600 = 1700"]}
- {type: INCOME, status: needs_check, complete: false, sheets: 2, reason: "по факту: рубли с копейками при шапке тыс. руб., 2100/2200/2300 расходятся с округлёнными итогами (OQ-137); одностроечный лист продолжения с 2400 пропущен: меньше min_codes"} - {type: INCOME, status: needs_check, complete: false, sheets: 2, reason: "по факту: рубли с копейками при шапке тыс. руб., 2100/2200/2300 расходятся с округлёнными итогами (OQ-137); одностроечный лист продолжения с 2400 пропущен: меньше min_codes", failed_relations: ["2100 = 2110 - 2120", "2200 = 2100 - 2210 - 2220", "2300 = 2200 + 2310 + 2320 - 2330 + 2340 - 2350"]}
"mezheninovskaya/2025-Q3/1_Форма1_бух_09.xlsx": "mezheninovskaya/2025-Q3/1_Форма1_бух_09.xlsx":
organization: 'ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "АГРОФИРМА "МЕЖЕНИНОВСКАЯ"' organization: 'ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "АГРОФИРМА "МЕЖЕНИНОВСКАЯ"'
inn: "7016006635" inn: "7016006635"
documents: [{type: BALANCE, status: done, complete: true}] documents: [{type: BALANCE, status: done, complete: true, failed_relations: []}]
"mezheninovskaya/2025-Q3/1_Форма2_бух_09.xlsx": "mezheninovskaya/2025-Q3/1_Форма2_бух_09.xlsx":
organization: 'ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "АГРОФИРМА "МЕЖЕНИНОВСКАЯ"' organization: 'ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "АГРОФИРМА "МЕЖЕНИНОВСКАЯ"'
inn: "7016006635" inn: "7016006635"
documents: [{type: INCOME, status: done, complete: true}] documents: [{type: INCOME, status: done, complete: true, failed_relations: []}]
"templates/ББ до 01.01.2025.xlsx": "templates/ББ до 01.01.2025.xlsx":
documents: documents:
- {type: BALANCE, status: needs_check, complete: false, reason: "шаблон без сумм и шапки"} - {type: BALANCE, status: needs_check, complete: false, reason: "шаблон без сумм и шапки", failed_relations: []}
- {type: DISCLOSURE, status: done, complete: false, reason: "по факту: «Примечания» шаблона образуют пустой документ DISCLOSURE"} - {type: DISCLOSURE, status: done, complete: false, reason: "по факту: «Примечания» шаблона образуют пустой документ DISCLOSURE"}
"templates/ББ после 01.01.2025.xlsx": "templates/ББ после 01.01.2025.xlsx":
documents: [{type: BALANCE, status: needs_check, complete: false, reason: "шаблон"}] documents: [{type: BALANCE, status: needs_check, complete: false, reason: "шаблон", failed_relations: []}]
"templates/ОФР_до 01.01.2025.xlsx": "templates/ОФР_до 01.01.2025.xlsx":
documents: [{type: INCOME, status: needs_check, complete: false, reason: "шаблон"}] documents: [{type: INCOME, status: needs_check, complete: false, reason: "шаблон", failed_relations: []}]
"templates/ОФР_после 01.01.2025.xlsx": "templates/ОФР_после 01.01.2025.xlsx":
documents: [{type: INCOME, status: needs_check, complete: false, reason: "шаблон"}] documents: [{type: INCOME, status: needs_check, complete: false, reason: "шаблон", failed_relations: []}]