test(statements): st02 согласованные данные, безусловные проверки, шапка документа не верхняя строка заголовка (T13.I1, T13.I2)

Генератор st02: значения ST02_VALUES сходятся по всем жёстким соотношениям. _upper_header_row: верхняя строка берётся,
только если подпись склеивается с датой в период. Refs #14

Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
This commit is contained in:
Andrey Epifancev 2026-10-06 19:29:08 +04:00
parent e7552dcdb0
commit 4907935e46
5 changed files with 54 additions and 23 deletions

View File

@ -147,6 +147,13 @@ def _upper_header_row(sheet: Sheet, rules: Rules, row: int, code_col: int, first
cells = sheet.row_cells(above)
if any(cell.kind == "number" for _, cell in cells) or not any(col > code_col for col, _ in cells):
return row
for col, _ in sheet.row_cells(row):
upper, lower = cell_label(sheet, above, col), cell_label(sheet, row, col)
# подпись над датой должна склеиваться с ней в период («На 31 декабря» + «2024 г.»); иначе это шапка документа
# («Единица измерения: тыс. руб.»), а не заголовок колонки
joins_into_period = rules.statement_periods.period_end(f"{upper} {lower}") is not None
if upper and col > code_col and not joins_into_period:
return row
return above

View File

@ -77,6 +77,20 @@ def _st01() -> list[SheetSpec]:
return [sheet]
# Значения st02, согласованные со всеми жёсткими соотношениями ББ (statement_checks.yaml) в обеих колонках:
# 1100 = 1150, 1200 = 1210 (скобки — запись 66н), 1600 = 1100 + 1200 = 1700, 1300 = 1310, 1700 = 1300.
ST02_VALUES: dict[str, tuple[int | str, int | str]] = {
"1150": (100, 120),
"1100": (100, 120),
"1210": ("(50)", "(40)"),
"1200": ("(50)", "(40)"),
"1600": (50, 80),
"1310": (50, 80),
"1300": (50, 80),
"1700": (50, 80),
}
def _st02() -> list[SheetSpec]:
sheet = SheetSpec("Лист1")
sheet.put(1, 1, "Бухгалтерский баланс")
@ -93,20 +107,9 @@ def _st02() -> list[SheetSpec]:
for index, (name, code, current, previous) in enumerate(BALANCE_LINES, start=7):
sheet.put(index, 1, name)
sheet.put(index, 2, code)
# Приспособить значения чтобы все проверки соотношений прошли: 1210 = -50, 1200 = 1210 = -50,
# 1100 = 200, 1600 = 1100 + 1200 = 150, 1700 = 1300 = 150
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)")
elif code == "1100":
sheet.put(index, 3, 200)
sheet.put(index, 4, 200)
else:
sheet.put(index, 3, current)
sheet.put(index, 4, previous)
value_now, value_before = ST02_VALUES.get(code, (current, previous))
sheet.put(index, 3, value_now)
sheet.put(index, 4, value_before)
sheet.put(index, 5, 5)
return [sheet]

View File

@ -24,6 +24,7 @@ pytestmark = pytest.mark.fixtures
SYNTHETIC_DIR = Path("statements-xlsx") / "synthetic"
EXPECTED_STATEMENT_COUNT_TWO = 2
EXPECTED_COLUMN_COUNT = 2
EXPECTED_LINES_COUNT = 0
@pytest.fixture(autouse=True)
@ -156,10 +157,12 @@ def test_st02_csv_like_format(fixtures_dir: Path) -> None:
# Проверяем код 1210 = -50
lines_by_code = by_code(doc)
if "1210" in lines_by_code:
line = lines_by_code["1210"]
col_0_value = get_line_value(line, 0)
assert col_0_value == Decimal("-50"), f"1210 в колонке 0 должна быть -50, получено {col_0_value}"
assert get_line_value(lines_by_code["1210"], 0) == Decimal("-50")
assert get_line_value(lines_by_code["1210"], 1) == Decimal("-40")
# T13.I1: данные согласованы со всеми жёсткими соотношениями — статус done, нарушений нет
assert outcome.status is ResultStatus.DONE
assert doc.checks.relations_failed == []
# Проверяем warning о единицах
warnings = warnings_of(outcome)
@ -167,8 +170,6 @@ def test_st02_csv_like_format(fixtures_dir: Path) -> None:
f"ожидается warning о единицах, получены: {warnings}"
)
# Примечание: warning о "Изменение, %" может быть не выдан из-за особенности двухстрочного заголовка (см. отчёт)
def test_st03_no_code_column(fixtures_dir: Path) -> None:
"""st03: форма без колонки кодов; needs_check, warning о коде, lines = []."""
@ -184,9 +185,6 @@ def test_st03_no_code_column(fixtures_dir: Path) -> None:
)
EXPECTED_LINES_COUNT = 0
def test_st04_mixed_book_osv_and_statement(fixtures_dir: Path) -> None:
"""st04: книга с листом ОСВ и листом баланса; OsvDocument + StatementDocument, порядок правильный."""
outcome = parse_named(fixtures_dir, "st04_mixed_book.xlsx")

View File

@ -207,6 +207,26 @@ def test_layout_two_row_header_without_code_caption_keeps_upper_row(rules: Rules
assert layout.skipped_labels == ("Изменение, %",)
def test_layout_document_header_above_dates_is_not_upper_header_row(rules: Rules) -> None:
"""Строка шапки документа над датами («Единица измерения…» правее кодов, без чисел) — не часть заголовка."""
sheet = sheet_from_rows(
[
["Бухгалтерский баланс"],
[None, None, "Единица измерения: тыс. руб."],
[None, None, "2024 г.", "2023 г."],
["Основные средства", "1150", 100, 90],
["Итого по разделу I", "1100", 100, 90],
["Запасы", "1210", 50, 40],
]
)
layout = find_layout(sheet, rules, 1, sheet.n_rows)
assert layout.header_row == 3 # noqa: PLR2004
assert layout.skipped_labels == ()
assert len(layout.periods) == 2 # noqa: PLR2004
def test_layout_continuation_without_header(rules: Rules) -> None:
sheet = sheet_from_rows(
[

3
api/uv.lock generated Normal file
View File

@ -0,0 +1,3 @@
version = 1
revision = 3
requires-python = ">=3.11"