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:
parent
e7552dcdb0
commit
4907935e46
@ -147,6 +147,13 @@ def _upper_header_row(sheet: Sheet, rules: Rules, row: int, code_col: int, first
|
|||||||
cells = sheet.row_cells(above)
|
cells = sheet.row_cells(above)
|
||||||
if any(cell.kind == "number" for _, cell in cells) or not any(col > code_col for col, _ in cells):
|
if any(cell.kind == "number" for _, cell in cells) or not any(col > code_col for col, _ in cells):
|
||||||
return row
|
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
|
return above
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -77,6 +77,20 @@ def _st01() -> list[SheetSpec]:
|
|||||||
return [sheet]
|
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]:
|
def _st02() -> list[SheetSpec]:
|
||||||
sheet = SheetSpec("Лист1")
|
sheet = SheetSpec("Лист1")
|
||||||
sheet.put(1, 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):
|
for index, (name, code, current, previous) in enumerate(BALANCE_LINES, start=7):
|
||||||
sheet.put(index, 1, name)
|
sheet.put(index, 1, name)
|
||||||
sheet.put(index, 2, code)
|
sheet.put(index, 2, code)
|
||||||
# Приспособить значения чтобы все проверки соотношений прошли: 1210 = -50, 1200 = 1210 = -50,
|
value_now, value_before = ST02_VALUES.get(code, (current, previous))
|
||||||
# 1100 = 200, 1600 = 1100 + 1200 = 150, 1700 = 1300 = 150
|
sheet.put(index, 3, value_now)
|
||||||
if code == "1210":
|
sheet.put(index, 4, value_before)
|
||||||
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)
|
|
||||||
sheet.put(index, 5, 5)
|
sheet.put(index, 5, 5)
|
||||||
return [sheet]
|
return [sheet]
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,7 @@ pytestmark = pytest.mark.fixtures
|
|||||||
SYNTHETIC_DIR = Path("statements-xlsx") / "synthetic"
|
SYNTHETIC_DIR = Path("statements-xlsx") / "synthetic"
|
||||||
EXPECTED_STATEMENT_COUNT_TWO = 2
|
EXPECTED_STATEMENT_COUNT_TWO = 2
|
||||||
EXPECTED_COLUMN_COUNT = 2
|
EXPECTED_COLUMN_COUNT = 2
|
||||||
|
EXPECTED_LINES_COUNT = 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
@ -156,10 +157,12 @@ def test_st02_csv_like_format(fixtures_dir: Path) -> None:
|
|||||||
|
|
||||||
# Проверяем код 1210 = -50
|
# Проверяем код 1210 = -50
|
||||||
lines_by_code = by_code(doc)
|
lines_by_code = by_code(doc)
|
||||||
if "1210" in lines_by_code:
|
assert get_line_value(lines_by_code["1210"], 0) == Decimal("-50")
|
||||||
line = lines_by_code["1210"]
|
assert get_line_value(lines_by_code["1210"], 1) == Decimal("-40")
|
||||||
col_0_value = get_line_value(line, 0)
|
|
||||||
assert col_0_value == Decimal("-50"), f"1210 в колонке 0 должна быть -50, получено {col_0_value}"
|
# T13.I1: данные согласованы со всеми жёсткими соотношениями — статус done, нарушений нет
|
||||||
|
assert outcome.status is ResultStatus.DONE
|
||||||
|
assert doc.checks.relations_failed == []
|
||||||
|
|
||||||
# Проверяем warning о единицах
|
# Проверяем warning о единицах
|
||||||
warnings = warnings_of(outcome)
|
warnings = warnings_of(outcome)
|
||||||
@ -167,8 +170,6 @@ def test_st02_csv_like_format(fixtures_dir: Path) -> None:
|
|||||||
f"ожидается warning о единицах, получены: {warnings}"
|
f"ожидается warning о единицах, получены: {warnings}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Примечание: warning о "Изменение, %" может быть не выдан из-за особенности двухстрочного заголовка (см. отчёт)
|
|
||||||
|
|
||||||
|
|
||||||
def test_st03_no_code_column(fixtures_dir: Path) -> None:
|
def test_st03_no_code_column(fixtures_dir: Path) -> None:
|
||||||
"""st03: форма без колонки кодов; needs_check, warning о коде, lines = []."""
|
"""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:
|
def test_st04_mixed_book_osv_and_statement(fixtures_dir: Path) -> None:
|
||||||
"""st04: книга с листом ОСВ и листом баланса; OsvDocument + StatementDocument, порядок правильный."""
|
"""st04: книга с листом ОСВ и листом баланса; OsvDocument + StatementDocument, порядок правильный."""
|
||||||
outcome = parse_named(fixtures_dir, "st04_mixed_book.xlsx")
|
outcome = parse_named(fixtures_dir, "st04_mixed_book.xlsx")
|
||||||
|
|||||||
@ -207,6 +207,26 @@ def test_layout_two_row_header_without_code_caption_keeps_upper_row(rules: Rules
|
|||||||
assert layout.skipped_labels == ("Изменение, %",)
|
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:
|
def test_layout_continuation_without_header(rules: Rules) -> None:
|
||||||
sheet = sheet_from_rows(
|
sheet = sheet_from_rows(
|
||||||
[
|
[
|
||||||
|
|||||||
3
api/uv.lock
generated
Normal file
3
api/uv.lock
generated
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.11"
|
||||||
Loading…
x
Reference in New Issue
Block a user