Заголовок ОСВ: колонка «Показатели» (#58)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-19 11:49:28 +04:00
parent af70c4c466
commit 43653f1b6f
2 changed files with 45 additions and 4 deletions

View File

@ -122,6 +122,7 @@ class TableHeader:
right_edge: int # последняя колонка таблицы (правый край последней колонки сумм); правее — не таблица
extra_cols: dict[int, str] # колонка → заголовок в блоке (или буква колонки, если заголовка нет)
group_label_rows: list[int] # строки блока, где в text_col начинается текст группировки («Счет», «Контрагенты»)
indicator_col: int | None = None # колонка «Показатели» — метка вида суммы строки, §1.3
# --- Текст ячеек ---
@ -381,13 +382,20 @@ def inn_from_cell(cell: Cell) -> str | None:
return digits if digits.isdigit() and len(digits) in INN_LENGTHS else None
def _inn_col(sheet: Sheet, block: range, text_col: int, first_sum: int, rules: Rules) -> int | None:
"""Колонка между текстом и суммами: по заголовку «ИНН» в блоке, иначе по доле значений из 10/12 цифр."""
inn_patterns = rules.headers.service_columns["inn"]
def _service_col(sheet: Sheet, block: range, text_col: int, first_sum: int, patterns: Iterable[Pattern]) -> int | None:
"""Первая колонка между текстом и суммами, чей нормализованный заголовок в блоке матчит `patterns`."""
for row in block:
for col, text in _texts(sheet, row):
if text_col < col < first_sum and _search(inn_patterns, text):
if text_col < col < first_sum and _search(patterns, text):
return col
return None
def _inn_col(sheet: Sheet, block: range, text_col: int, first_sum: int, rules: Rules) -> int | None:
"""Колонка между текстом и суммами: по заголовку «ИНН» в блоке, иначе по доле значений из 10/12 цифр."""
by_title = _service_col(sheet, block, text_col, first_sum, rules.headers.service_columns["inn"])
if by_title is not None:
return by_title
values: dict[int, list[Cell]] = {}
for row in _data_rows(sheet, block[-1], INN_SAMPLE_ROWS):
for col, cell in sheet.row_cells(row):
@ -465,6 +473,8 @@ def find_table_header(sheet: Sheet, rules: Rules, deadline: Deadline | None = No
last_row = _block_last_row(sheet, group_row, sum_cells, rules)
text_col = _text_col(sheet, group_row, last_row, first_sum)
inn_col = _inn_col(sheet, range(group_row, last_row + 1), text_col, first_sum, rules)
indicator_patterns = rules.headers.service_columns.get("indicator", [])
indicator_col = _service_col(sheet, range(group_row, last_row + 1), text_col, first_sum, indicator_patterns)
partial = TableHeader(group_row, last_row, columns, text_col, inn_col, right_edge, {}, [])
occupied = {*sum_cols, text_col} | ({inn_col} if inn_col is not None else set())
@ -485,6 +495,7 @@ def find_table_header(sheet: Sheet, rules: Rules, deadline: Deadline | None = No
right_edge=right_edge,
extra_cols=dict(sorted(extra_cols.items())),
group_label_rows=_group_label_rows(sheet, group_row, last_row, text_col, rules),
indicator_col=indicator_col,
)

View File

@ -438,6 +438,36 @@ def test_header_without_text_column_left_of_sums_is_invalid_format(rules: Rules)
# --- ИНН и extra ---
def test_indicator_column_found_by_hyphenated_title(rules: Rules) -> None:
sheet = sheet_from_rows(
[
[TITLE],
["Счет", None, M(OPENING, 2), M(TURNOVER, 2), M(CLOSING, 2)],
["Счет", "Показа-\nтели", "Дебет", "Кредит", "Дебет", "Кредит", "Дебет", "Кредит"],
[L(0), *at({1: "60", 2: "Дебет", 3: 10, 8: 10})],
]
)
header = find_table_header(sheet, rules)
assert [header.indicator_col] == [2]
assert [header.text_col] == [1]
assert list(header.extra_cols) == [2]
def test_no_indicator_column_when_title_absent(rules: Rules) -> None:
sheet = sheet_from_rows(
[
[TITLE],
["Счет", M(OPENING, 2), M(TURNOVER, 2), M(CLOSING, 2)],
["Контрагенты", "Дебет", "Кредит", "Дебет", "Кредит", "Дебет", "Кредит"],
[L(0), *at({1: "60", 2: 10, 7: 10})],
]
)
assert find_table_header(sheet, rules).indicator_col is None
def test_kpp_between_inn_and_sums_goes_to_extra(rules: Rules) -> None:
sheet = sheet_from_rows(
[