From 4907935e46bb7ce1d63e763b8d3be4e83590911d Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 6 Oct 2026 19:29:08 +0400 Subject: [PATCH] =?UTF-8?q?test(statements):=20st02=20=D1=81=D0=BE=D0=B3?= =?UTF-8?q?=D0=BB=D0=B0=D1=81=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5,=20=D0=B1=D0=B5=D0=B7?= =?UTF-8?q?=D1=83=D1=81=D0=BB=D0=BE=D0=B2=D0=BD=D1=8B=D0=B5=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8,=20=D1=88=D0=B0=D0=BF?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=20=D0=BD=D0=B5=20=D0=B2=D0=B5=D1=80=D1=85=D0=BD=D1=8F?= =?UTF-8?q?=D1=8F=20=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B3=D0=BE=D0=BB=D0=BE=D0=B2=D0=BA=D0=B0=20(T13.I1,=20T13.I2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Генератор st02: значения ST02_VALUES сходятся по всем жёстким соотношениям. _upper_header_row: верхняя строка берётся, только если подпись склеивается с датой в период. Refs #14 Co-Authored-By: Claude Sonnet Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b --- api/src/parser/statements/columns.py | 7 +++++ api/tests/synthetic/generate_statements.py | 31 ++++++++++--------- .../test_statement_synthetic_files.py | 16 +++++----- .../unit/parser/statements/test_columns.py | 20 ++++++++++++ api/uv.lock | 3 ++ 5 files changed, 54 insertions(+), 23 deletions(-) create mode 100644 api/uv.lock diff --git a/api/src/parser/statements/columns.py b/api/src/parser/statements/columns.py index c53044e..e370668 100644 --- a/api/src/parser/statements/columns.py +++ b/api/src/parser/statements/columns.py @@ -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 diff --git a/api/tests/synthetic/generate_statements.py b/api/tests/synthetic/generate_statements.py index 26a13b8..b5ae46c 100644 --- a/api/tests/synthetic/generate_statements.py +++ b/api/tests/synthetic/generate_statements.py @@ -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] diff --git a/api/tests/synthetic/test_statement_synthetic_files.py b/api/tests/synthetic/test_statement_synthetic_files.py index dcbc76d..cb8fdb5 100644 --- a/api/tests/synthetic/test_statement_synthetic_files.py +++ b/api/tests/synthetic/test_statement_synthetic_files.py @@ -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") diff --git a/api/tests/unit/parser/statements/test_columns.py b/api/tests/unit/parser/statements/test_columns.py index 9307229..8b476be 100644 --- a/api/tests/unit/parser/statements/test_columns.py +++ b/api/tests/unit/parser/statements/test_columns.py @@ -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( [ diff --git a/api/uv.lock b/api/uv.lock new file mode 100644 index 0000000..9431a63 --- /dev/null +++ b/api/uv.lock @@ -0,0 +1,3 @@ +version = 1 +revision = 3 +requires-python = ">=3.11"