test: манифест реальных форм и Review Focus под общий код и v2.0 (#54)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
This commit is contained in:
parent
f5d071f602
commit
0f12c0eb3d
46
api/tests/fixtures/test_statements_property.py
vendored
46
api/tests/fixtures/test_statements_property.py
vendored
@ -17,29 +17,20 @@ EXPECTED_FILES = 18
|
||||
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 видна в результате: нарушенное соотношение, дубликаты кодов, нет колонок или шапки."""
|
||||
"""Причина needs_check видна в результате: предупреждение error, нарушенное соотношение, пометка проверки
|
||||
у строки, значения или шапки, нечитаемая или пропущенная строка."""
|
||||
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)
|
||||
any(warning.severity == "error" for warning in document.warnings)
|
||||
or (checks is not None and checks.relations_failed)
|
||||
or document.header.needs_review_fields
|
||||
or any(
|
||||
line.needs_review
|
||||
or line.line_status in ("unreadable", "missing")
|
||||
or any(value.needs_review for value in line.values)
|
||||
for line in document.lines
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@ -61,23 +52,26 @@ def test_statement_file_matches_manifest(fixtures_dir: Path, statement_file: Pat
|
||||
all_done = all(item.status == "done" for item in expectation.documents)
|
||||
assert outcome.status is (ResultStatus.DONE if all_done else ResultStatus.NEEDS_CHECK)
|
||||
for document, item in zip(documents, expectation.documents, strict=True):
|
||||
codes = [line.line_code for line in document.lines]
|
||||
assert len(codes) == len(set(codes)), "коды уникальны"
|
||||
assert all(value.column_index < len(document.columns) for line in document.lines for value in line.values)
|
||||
# повторы кодов строк законны (v2.0); ссылки на строки уникальны, значения ссылаются на колонки документа
|
||||
refs = [line.ref for line in document.lines]
|
||||
assert len(refs) == len(set(refs)), "ref уникальны"
|
||||
column_indexes = {column.column_index for column in document.columns}
|
||||
assert all(value.column_index in column_indexes for line in document.lines for value in line.values)
|
||||
if item.sheets:
|
||||
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.error_warnings is not None:
|
||||
errors = {warning.code for warning in document.warnings if warning.severity == "error"}
|
||||
assert errors == set(item.error_warnings)
|
||||
if item.complete:
|
||||
assert document.checks is not None
|
||||
assert document.checks.relations_failed == [], document.checks
|
||||
assert document.columns
|
||||
assert all(column.period_date is not None for column in document.columns)
|
||||
# статус документа — по критерию парсера (statement.py::_is_ok), независимо от манифеста
|
||||
assert document_ok(document) == (item.status == "done"), (item, document.header)
|
||||
assert document.status == item.status, (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
|
||||
|
||||
66
api/tests/fixtures/test_statements_real.py
vendored
Normal file
66
api/tests/fixtures/test_statements_real.py
vendored
Normal file
@ -0,0 +1,66 @@
|
||||
"""Review Focus плана 4 на реальных книгах `../fixtures/statements-xlsx/**` (спека «Манифест ожиданий...»)."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
|
||||
from src.contract import FileType, StatementDocument
|
||||
from src.parser import get_parser
|
||||
|
||||
pytestmark = pytest.mark.fixtures
|
||||
STATEMENTS = "statements-xlsx"
|
||||
TITLE_BLOCK_FILES = [
|
||||
"unagrande-co/2024-FY/Б.Б Лист 1.xls",
|
||||
"unagrande-co/2024-FY/ОФР. Лист 1.xls",
|
||||
"unagrande-group/2024-FY/ББ. Лист 1 .xls",
|
||||
"unagrande-group/2025-Q3/ББ. Лист 1 3кв.25.xls",
|
||||
"unagrande-group/2025-Q3/ОФР. Лист 1 3кв.25.xls",
|
||||
]
|
||||
NPZ = "npz/2025-Q3/Бухгалтерская отчетность за Январь - Сентябрь 2025 г. (НОВОРОССИЙСКИЙ ПРОКАТНЫЙ ЗАВОД).xlsx"
|
||||
MEZHENINOVSKAYA = [
|
||||
"mezheninovskaya/2025-Q3/1_Форма1_бух_09.xlsx",
|
||||
"mezheninovskaya/2025-Q3/1_Форма2_бух_09.xlsx",
|
||||
]
|
||||
|
||||
|
||||
def statement_docs(fixtures_dir: Path, key: str) -> list[StatementDocument]:
|
||||
path = fixtures_dir / STATEMENTS / key
|
||||
outcome = get_parser().parse(path.read_bytes(), cast(FileType, path.suffix.lstrip(".").upper()))
|
||||
assert outcome.error is None, outcome.error
|
||||
return [document for document in outcome.documents if isinstance(document, StatementDocument)]
|
||||
|
||||
|
||||
def test_unagrande_title_codes_block(fixtures_dir: Path) -> None:
|
||||
"""Review Focus 1: ОКОПФ из блока кодов шапки не становится строкой формы."""
|
||||
for key in TITLE_BLOCK_FILES:
|
||||
for doc in statement_docs(fixtures_dir, key):
|
||||
assert doc.header.okopf == "12267", key
|
||||
assert not any("12267" in (line.line_code_text or "") for line in doc.lines), key
|
||||
|
||||
|
||||
def test_npz_documents(fixtures_dir: Path) -> None:
|
||||
"""Review Focus 2; спека «НПЗ «Реализация иного имущества»»."""
|
||||
balance, income = statement_docs(fixtures_dir, NPZ)
|
||||
assert (balance.status, income.status) == ("done", "done")
|
||||
bad = {"SHEET_NOT_JOINED", "SEGMENT_COLUMNS_MISMATCH", "SEGMENT_WITHOUT_CODES", "HEADER_ROW_NOT_FOUND"}
|
||||
assert not bad & {w.code for d in (balance, income) for w in d.warnings}
|
||||
cols = {c.column_index for c in balance.columns}
|
||||
assert all(v.column_index in cols for line in balance.lines for v in line.values)
|
||||
income_cols = {c.column_index for c in income.columns}
|
||||
assert all(v.column_index in income_cols for line in income.lines for v in line.values)
|
||||
# Строки F34/F44 «Реализация иного имущества…» состоят только из прочерков (0 с форматом `[=0]"-"`, value None):
|
||||
# по спеке строка без кода и без содержимого — не строка данных, пакет её не выдаёт (R-10; вопрос владельцу O-8.2:
|
||||
# сценарий спеки «с суммами» не соответствует файлу, где суммы — прочерки).
|
||||
assert any(
|
||||
line.line_code == "2400" and line.ref.startswith("'Отчет о финансовых результата_2'") for line in income.lines
|
||||
)
|
||||
|
||||
|
||||
def test_mezheninovskaya_numbering_and_signatures(fixtures_dir: Path) -> None:
|
||||
"""Review Focus 3: строка нумерации колонок «2» и подписи не становятся строками формы."""
|
||||
for key in MEZHENINOVSKAYA:
|
||||
(doc,) = statement_docs(fixtures_dir, key)
|
||||
assert doc.status == "done" and doc.header.inn == "7016006635", key
|
||||
assert not any(line.line_code_text == "2" for line in doc.lines), key
|
||||
assert not any(line.line_status in ("unreadable", "missing") for line in doc.lines), key
|
||||
@ -20,6 +20,7 @@ class DocumentExpectation(BaseModel):
|
||||
reason: str | None = None
|
||||
sheets: int | None = None
|
||||
failed_relations: list[str] | None = None # None — не проверяется; список — точное множество relations_failed
|
||||
error_warnings: list[str] | None = None # None — не проверяется; список — точное множество кодов уровня error
|
||||
|
||||
|
||||
class Expectation(BaseModel):
|
||||
|
||||
@ -1,66 +1,69 @@
|
||||
# Ожидания по реальным формам fixtures/statements-xlsx/** (README там же). Ключ — путь относительно statements-xlsx.
|
||||
# documents — по порядку документов результата: type, status (done | needs_check), complete (вся форма: §7 сходится).
|
||||
# error_warnings — точное множество кодов предупреждений документа уровня error (P4-18); отсутствие поля — не проверяется.
|
||||
# Причина needs_check — данные файла (страница без шапки, шаблон без сумм), не дефект разбора.
|
||||
# Значения сверены прогоном парсера на файлах (R-13): расхождения с первоначальным планом помечены «по факту».
|
||||
# Значения ББ/ОФР — как их показывает числовой формат ячейки (#42): рубли с копейками из 1С приходят в тысячах;
|
||||
# мягкие нарушения (в пределах допуска округления, OQ-138) статус не меняют; набор — по факту разбора, см. golden.
|
||||
# Страница 2 без шапки: организации и ИНН нет (NO_ORGANIZATION_NO_INN), а 1600 = 1700 не сходится, потому что актив
|
||||
# на странице 1, а пассив на странице 2 (v2.0: проверка идёт по самой странице).
|
||||
files:
|
||||
"unagrande-co/2024-FY/Б.Б Лист 1.xls":
|
||||
organization: 'Акционерное общество "Унагранде Компани"'
|
||||
inn: "3249004697"
|
||||
documents: [{type: BALANCE, status: needs_check, complete: false, reason: "только актив: 1600 = 1700 не сходится", failed_relations: ["1600 = 1700"]}]
|
||||
documents: [{type: BALANCE, status: needs_check, complete: false, reason: "только актив: 1600 = 1700 не сходится", failed_relations: ["1600 = 1700"], error_warnings: []}]
|
||||
"unagrande-co/2024-FY/Б.Б. Лист 2.xls":
|
||||
documents: [{type: BALANCE, status: needs_check, complete: false, reason: "страница 2 без шапки", failed_relations: []}]
|
||||
documents: [{type: BALANCE, status: needs_check, complete: false, reason: "страница 2 без шапки", failed_relations: ["1600 = 1700"], error_warnings: [NO_ORGANIZATION_NO_INN]}]
|
||||
"unagrande-co/2024-FY/ОФР. Лист 1.xls":
|
||||
organization: 'Акционерное общество "Унагранде Компани"'
|
||||
inn: "3249004697"
|
||||
documents:
|
||||
- {type: INCOME, status: done, complete: true, failed_relations: []} # значения по формату в тыс. руб.; мягко расходится 2400 = 2300 - 2410 + 2420 (в пределах допуска, OQ-138)
|
||||
- {type: INCOME, status: done, complete: true, failed_relations: [], error_warnings: []} # значения по формату в тыс. руб.; мягко расходится 2400 = 2300 - 2410 + 2420 (в пределах допуска, OQ-138)
|
||||
"unagrande-co/2024-FY/ОФР. Лист 2 .xls":
|
||||
documents: [{type: INCOME, status: needs_check, complete: false, reason: "страница 2 без шапки", failed_relations: []}]
|
||||
documents: [{type: INCOME, status: needs_check, complete: false, reason: "страница 2 без шапки", failed_relations: [], error_warnings: [NO_ORGANIZATION_NO_INN]}]
|
||||
"unagrande-group/2024-FY/ББ. Лист 1 .xls":
|
||||
organization: 'Акционерное общество "Унагранде Групп"'
|
||||
inn: "7728757141"
|
||||
documents: [{type: BALANCE, status: needs_check, complete: false, reason: "только актив", failed_relations: ["1600 = 1700"]}]
|
||||
documents: [{type: BALANCE, status: needs_check, complete: false, reason: "только актив", failed_relations: ["1600 = 1700"], error_warnings: []}]
|
||||
"unagrande-group/2024-FY/ББ. Лист 2 (2).xls":
|
||||
documents: [{type: BALANCE, status: needs_check, complete: false, reason: "страница 2 без шапки", failed_relations: []}]
|
||||
documents: [{type: BALANCE, status: needs_check, complete: false, reason: "страница 2 без шапки", failed_relations: ["1600 = 1700"], error_warnings: [NO_ORGANIZATION_NO_INN]}]
|
||||
"unagrande-group/2024-FY/ОФР. Лист 2.xls":
|
||||
documents: [{type: INCOME, status: needs_check, complete: false, reason: "страница 2 без шапки, стр. 1 в пакете нет", failed_relations: []}]
|
||||
documents: [{type: INCOME, status: needs_check, complete: false, reason: "страница 2 без шапки, стр. 1 в пакете нет", failed_relations: [], error_warnings: [NO_ORGANIZATION_NO_INN]}]
|
||||
"unagrande-group/2025-Q3/ББ. Лист 1 3кв.25.xls":
|
||||
organization: 'Акционерное общество "Унагранде Групп"'
|
||||
inn: "7728757141"
|
||||
documents: [{type: BALANCE, status: needs_check, complete: false, reason: "только актив", failed_relations: ["1600 = 1700"]}]
|
||||
documents: [{type: BALANCE, status: needs_check, complete: false, reason: "только актив", failed_relations: ["1600 = 1700"], error_warnings: []}]
|
||||
"unagrande-group/2025-Q3/ББ. Лист 2 3кв.25.xls":
|
||||
documents: [{type: BALANCE, status: needs_check, complete: false, reason: "страница 2 без шапки", failed_relations: []}]
|
||||
documents: [{type: BALANCE, status: needs_check, complete: false, reason: "страница 2 без шапки", failed_relations: ["1600 = 1700"], error_warnings: [NO_ORGANIZATION_NO_INN]}]
|
||||
"unagrande-group/2025-Q3/ОФР. Лист 1 3кв.25.xls":
|
||||
organization: 'Акционерное общество "Унагранде Групп"'
|
||||
inn: "7728757141"
|
||||
documents:
|
||||
- {type: INCOME, status: done, complete: true, failed_relations: []} # мягко расходятся 2400 = 2300 - 2410 + 2420 и 2410 = 2411 + 2412 (в пределах допуска, OQ-138)
|
||||
- {type: INCOME, status: done, complete: true, failed_relations: [], error_warnings: []} # мягко расходятся 2400 = 2300 - 2410 + 2420 и 2410 = 2411 + 2412 (в пределах допуска, OQ-138)
|
||||
"unagrande-group/2025-Q3/ОФР. Лист 2 3кв.25.xls":
|
||||
documents: [{type: INCOME, status: needs_check, complete: false, reason: "страница 2 без шапки", failed_relations: []}]
|
||||
documents: [{type: INCOME, status: needs_check, complete: false, reason: "страница 2 без шапки", failed_relations: [], error_warnings: [NO_ORGANIZATION_NO_INN]}]
|
||||
"npz/2025-Q3/Бухгалтерская отчетность за Январь - Сентябрь 2025 г. (НОВОРОССИЙСКИЙ ПРОКАТНЫЙ ЗАВОД).xlsx":
|
||||
organization: 'ООО "НОВОРОССИЙСКИЙ ПРОКАТНЫЙ ЗАВОД"'
|
||||
inn: "2315995868"
|
||||
inn_optional_for: [INCOME]
|
||||
documents:
|
||||
- {type: BALANCE, status: done, complete: true, sheets: 5, failed_relations: []} # мягко расходятся детализации 1150, 1250, 1410, 1500, 1510 (сверка с итогом в самой выгрузке, OQ-138)
|
||||
- {type: INCOME, status: done, complete: true, sheets: 3, failed_relations: []} # мягко расходятся 2400 и 2410 (OQ-138); лист с единственной строкой 2400 присоединён (O-9)
|
||||
- {type: BALANCE, status: done, complete: true, sheets: 5, failed_relations: [], error_warnings: []} # мягко расходятся детализации 1150, 1250, 1410, 1500, 1510 (сверка с итогом в самой выгрузке, OQ-138)
|
||||
- {type: INCOME, status: done, complete: true, sheets: 3, failed_relations: [], error_warnings: []} # мягко расходятся 2400 и 2410 (OQ-138); лист с единственной строкой 2400 присоединён (O-9)
|
||||
"mezheninovskaya/2025-Q3/1_Форма1_бух_09.xlsx":
|
||||
organization: 'ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "АГРОФИРМА "МЕЖЕНИНОВСКАЯ"'
|
||||
inn: "7016006635"
|
||||
documents: [{type: BALANCE, status: done, complete: true, failed_relations: []}]
|
||||
documents: [{type: BALANCE, status: done, complete: true, failed_relations: [], error_warnings: []}] # колонки сумм M/Q/S; числа правее таблицы формы — NUMBERS_OUTSIDE_TABLE (info, Р-44)
|
||||
"mezheninovskaya/2025-Q3/1_Форма2_бух_09.xlsx":
|
||||
organization: 'ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "АГРОФИРМА "МЕЖЕНИНОВСКАЯ"'
|
||||
inn: "7016006635"
|
||||
documents: [{type: INCOME, status: done, complete: true, failed_relations: []}]
|
||||
documents: [{type: INCOME, status: done, complete: true, failed_relations: [], error_warnings: []}]
|
||||
"templates/ББ до 01.01.2025.xlsx":
|
||||
documents:
|
||||
- {type: BALANCE, status: needs_check, complete: false, reason: "шаблон без сумм и шапки", failed_relations: []}
|
||||
- {type: DISCLOSURE, status: done, complete: false, reason: "по факту: «Примечания» шаблона образуют пустой документ DISCLOSURE"}
|
||||
- {type: BALANCE, status: needs_check, complete: false, reason: "шаблон без сумм и шапки", failed_relations: [], error_warnings: [NO_ORGANIZATION_NO_INN, PERIOD_UNKNOWN, SEGMENT_COLUMNS_MISMATCH]}
|
||||
- {type: DISCLOSURE, status: done, complete: false, reason: "по факту: «Примечания» шаблона образуют пустой документ DISCLOSURE", error_warnings: []}
|
||||
"templates/ББ после 01.01.2025.xlsx":
|
||||
documents: [{type: BALANCE, status: needs_check, complete: false, reason: "шаблон", failed_relations: []}]
|
||||
documents: [{type: BALANCE, status: needs_check, complete: false, reason: "шаблон", failed_relations: [], error_warnings: [PERIOD_UNKNOWN]}]
|
||||
"templates/ОФР_до 01.01.2025.xlsx":
|
||||
documents: [{type: INCOME, status: needs_check, complete: false, reason: "шаблон", failed_relations: []}]
|
||||
documents: [{type: INCOME, status: needs_check, complete: false, reason: "шаблон", failed_relations: [], error_warnings: [NO_ORGANIZATION_NO_INN, PERIOD_UNKNOWN]}]
|
||||
"templates/ОФР_после 01.01.2025.xlsx":
|
||||
documents: [{type: INCOME, status: needs_check, complete: false, reason: "шаблон", failed_relations: []}]
|
||||
documents: [{type: INCOME, status: needs_check, complete: false, reason: "шаблон", failed_relations: [], error_warnings: [PERIOD_UNKNOWN]}]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user