"""Заголовок таблицы формы, колонка кодов, наименований и периодов (design D-4; spec «Заголовок таблицы, колонка кодов и колонки-периоды определяются по тексту»).""" import datetime from decimal import Decimal import pytest from gridbuilder import at, sheet_from_rows from src.parser.grid import Cell from src.parser.rules import Rules, load_rules from src.parser.statements.columns import ( LayoutNotFoundError, find_code_column, find_layout, line_code, ) @pytest.fixture(scope="module") def rules() -> Rules: return load_rules() # Test constants CODE_COL_ONE_C = 4 NAME_COL_ONE_C = 2 CODE_COL_TDSHEET = 2 NAME_COL_TDSHEET = 1 HEADER_ROW_ONE_C = 4 FIRST_CODE_ROW_ONE_C = 5 PERIOD_COL_ONE_C_1 = 5 PERIOD_COL_ONE_C_2 = 6 HEADER_ROW_TDSHEET = 2 HEADER_LAST_ROW_TDSHEET = 3 FIRST_CODE_ROW_TDSHEET = 4 HEADER_ROW_TWO_ROW = 1 HEADER_LAST_ROW_TWO_ROW = 2 FIRST_CODE_ROW_TWO_ROW = 3 HEADER_ROW_NO_CAPTION = 1 CODE_COL_NO_CAPTION = 2 NAME_COL_NO_CAPTION = 1 HEADER_ROW_CONTINUATION = None FIRST_DATA_ROW_CONTINUATION = 1 CODE_COL_CONTINUATION = 6 PERIOD_COL_CONT_1 = 7 PERIOD_COL_CONT_2 = 8 PERIOD_COL_CONT_3 = 9 HEADER_ROW_BARE_YEAR = 2 PERIOD_COL_BARE_YEAR_1 = 15 PERIOD_COL_BARE_YEAR_2 = 20 def test_line_code_from_text_and_numeric_cells(rules: Rules) -> None: assert line_code(Cell("1110", "text"), rules) == "1110" assert line_code(Cell(Decimal("1110"), "number"), rules) == "1110" assert line_code(Cell(Decimal("1110.0"), "number"), rules) == "1110" # Review Focus 1: xlrd даёт float assert line_code(Cell("11101", "text"), rules) == "11101" assert line_code(Cell("2110", "text"), rules) == "2110" assert line_code(Cell("5200", "text"), rules) == "5200" assert line_code(Cell("2023", "text"), rules) is None # год, не код assert line_code(Cell("0710001", "text"), rules) is None assert line_code(Cell("", "empty"), rules) is None # Test data: ONE_C_BALANCE sheet _HEADER_ROW_ONE_C = [ "Пояснения", "Наименование показателя", None, "Код", "На 31 декабря 2024 г.", "На 31 декабря 2023 г.", "Изменение, %", ] ONE_C_BALANCE = [ ["Бухгалтерский баланс"], ["на 31 декабря 2024 г."], [], _HEADER_ROW_ONE_C, [None, "АКТИВ"], [None, "Нематериальные активы", None, "1110", 5971264, 27405634, 12], [None, "Нематериальные активы в организации", None, "11101", 5852464, 27214614, 0], [None, None, None, None, 0, 0, 0], ["1", "Основные средства", None, "1150", 2039077663, 1809528568, 7], [None, "Итого по разделу I", None, "1100", 2045048927, 1836934202, 7], ] def test_code_column_by_share(rules: Rules) -> None: sheet = sheet_from_rows(ONE_C_BALANCE) assert find_code_column(sheet, rules, 1, sheet.n_rows) == CODE_COL_ONE_C def test_code_column_needs_three_codes_and_sixty_percent(rules: Rules) -> None: sheet = sheet_from_rows([["x", "1110"], ["y", "abc"], ["z", "def"], ["w", "ghi"], ["v", "1150"]]) assert find_code_column(sheet, rules, 1, 5) is None def test_layout_one_c(rules: Rules) -> None: sheet = sheet_from_rows(ONE_C_BALANCE) layout = find_layout(sheet, rules, 1, sheet.n_rows) assert (layout.header_row, layout.header_last_row, layout.first_data_row) == ( HEADER_ROW_ONE_C, HEADER_ROW_ONE_C, FIRST_CODE_ROW_ONE_C, ) assert layout.code_col == CODE_COL_ONE_C and layout.name_col == NAME_COL_ONE_C expected_periods = [ (PERIOD_COL_ONE_C_1, "На 31 декабря 2024 г.", datetime.date(2024, 12, 31)), (PERIOD_COL_ONE_C_2, "На 31 декабря 2023 г.", datetime.date(2023, 12, 31)), ] assert [(column.col, column.label, column.period_date) for column in layout.periods] == expected_periods assert layout.skipped_labels == ("Изменение, %",) def test_layout_tdsheet_with_numbering_row_and_line_breaks(rules: Rules) -> None: sheet = sheet_from_rows( [ ["БУХГАЛТЕРСКИЙ БАЛАНС"], ["Наименование показателя", "Коды", "За 9 месяцев\n2025 года", "На 31 декабря \n2024 года"], ["1", "2", "3", "4"], ["Нематериальные активы", "1110", 0, 0], ["Основные средства", "1150", Decimal("898852.21795"), 966270], ["ИТОГО по разделу I", "1100", Decimal("898852.21795"), 966270], ] ) layout = find_layout(sheet, rules, 1, sheet.n_rows) assert (layout.header_row, layout.header_last_row, layout.first_data_row) == ( HEADER_ROW_TDSHEET, HEADER_LAST_ROW_TDSHEET, FIRST_CODE_ROW_TDSHEET, ) assert layout.name_col == NAME_COL_TDSHEET and layout.code_col == CODE_COL_TDSHEET expected_labels = ["За 9 месяцев 2025 года", "На 31 декабря 2024 года"] assert [column.label for column in layout.periods] == expected_labels assert layout.periods[0].period_date == datetime.date(2025, 9, 30) def test_layout_two_row_header(rules: Rules) -> None: sheet = sheet_from_rows( [ ["Наименование показателя", "Код", "На 31 декабря", "На 31 декабря"], [None, None, "2024 г.", "2023 г."], ["Запасы", "1210", 10, 20], ["Дебиторская задолженность", "1230", 30, 40], ["Итого по разделу II", "1200", 40, 60], ] ) layout = find_layout(sheet, rules, 1, sheet.n_rows) assert (layout.header_row, layout.header_last_row, layout.first_data_row) == ( HEADER_ROW_TWO_ROW, HEADER_LAST_ROW_TWO_ROW, FIRST_CODE_ROW_TWO_ROW, ) expected_dates = [datetime.date(2024, 12, 31), datetime.date(2023, 12, 31)] assert [column.period_date for column in layout.periods] == expected_dates def test_layout_without_code_caption(rules: Rules) -> None: sheet = sheet_from_rows( [ ["Показатель", None, "На 31.12.2024", "На 31.12.2023"], ["Основные средства", "1150", 100, 90], ["Запасы", "1210", 50, 40], ["БАЛАНС", "1600", 150, 130], ] ) layout = find_layout(sheet, rules, 1, sheet.n_rows) assert layout.header_row == HEADER_ROW_NO_CAPTION assert layout.code_col == CODE_COL_NO_CAPTION assert layout.name_col == NAME_COL_NO_CAPTION expected_dates = [datetime.date(2024, 12, 31), datetime.date(2023, 12, 31)] assert [column.period_date for column in layout.periods] == expected_dates def test_layout_two_row_header_without_code_caption_keeps_upper_row(rules: Rules) -> None: """T13.I2: без подписи «Код» верхняя строка двухстрочного заголовка не теряется (st02): «Изменение, %» пропущена.""" sheet = sheet_from_rows( [ ["Показатель", None, "На 31 декабря", "На 31 декабря", "Изменение, %"], [None, None, "2024 г.", "2023 г."], ["Основные средства", "1150", 100, 90, 5], ["Итого по разделу I", "1100", 100, 90, 5], ["Запасы", "1210", 50, 40, 5], ] ) layout = find_layout(sheet, rules, 1, sheet.n_rows) assert layout.header_row == 1 assert layout.first_data_row == 3 # noqa: PLR2004 expected_dates = [datetime.date(2024, 12, 31), datetime.date(2023, 12, 31)] assert [column.period_date for column in layout.periods] == expected_dates assert [column.label for column in layout.periods] == ["На 31 декабря 2024 г.", "На 31 декабря 2023 г."] assert layout.skipped_labels == ("Изменение, %",) def test_layout_continuation_without_header(rules: Rules) -> None: sheet = sheet_from_rows( [ [None, None, None, None, "в том числе:"], [None, None, None, None, "Биологические активы", "11901", 0, 0, 0], [None, None, None, None, "Итого по разделу I", "1100", 11853698344, 11570097340, 9391157985], [None, None, None, "II. Оборотные активы"], [None, None, None, "Запасы", None, "1210", 1001705104, 1426756617, 3393013092], ] ) layout = find_layout(sheet, rules, 1, sheet.n_rows) assert layout.header_row is HEADER_ROW_CONTINUATION assert layout.first_data_row == FIRST_DATA_ROW_CONTINUATION assert layout.code_col == CODE_COL_CONTINUATION expected_cols = [ (PERIOD_COL_CONT_1, "", None), (PERIOD_COL_CONT_2, "", None), (PERIOD_COL_CONT_3, "", None), ] assert [(column.col, column.label, column.period_date) for column in layout.periods] == expected_cols def test_explanations_column_is_not_name(rules: Rules) -> None: sheet = sheet_from_rows(ONE_C_BALANCE) assert find_layout(sheet, rules, 1, sheet.n_rows).name_col == NAME_COL_ONE_C def test_no_code_column_raises(rules: Rules) -> None: sheet = sheet_from_rows([["Показатель", "Сумма"], ["Основные средства", 100], ["Запасы", 50]]) with pytest.raises(LayoutNotFoundError, match="колонка кодов не найдена"): find_layout(sheet, rules, 1, 3) def test_bare_year_in_title_block_is_not_a_header(rules: Rules) -> None: """У НПЗ над таблицей стоит «Отчетная дата | 30 | 09 | 2025»: строка с подписью «Код» побеждает.""" sheet = sheet_from_rows( [ [*at({6: "Отчетная дата", 22: "30", 24: "09", 25: "2025"})], [*at({4: "Наименование показателя", 12: "Код", 15: "На 30 сентября 2025 г.", 20: "На 31 декабря 2024 г."})], [*at({4: "Гудвил", 12: "1105", 15: 0, 20: 0})], [*at({4: "Нематериальные активы", 12: "1110", 15: 586260, 20: 905786})], [*at({4: "Итого по разделу I", 12: "1100", 15: 586260, 20: 905786})], ] ) layout = find_layout(sheet, rules, 1, sheet.n_rows) period_cols = [PERIOD_COL_BARE_YEAR_1, PERIOD_COL_BARE_YEAR_2] assert layout.header_row == HEADER_ROW_BARE_YEAR assert [column.col for column in layout.periods] == period_cols