"""Строки-показатели ОСВ (§1.3, AURORA-1613, AURORA-1661): метка вида суммы строки и привязка к строке данных.""" from collections.abc import Sequence from dataclasses import dataclass from src.contract.common import DataType from src.parser.deadline import Deadline from src.parser.detect import TableHeader from src.parser.grid import Sheet from src.parser.hierarchy import ROOT_LEVEL, BodyRow, Levels, resolve_levels from src.parser.osv_warnings import unknown_label_warning from src.parser.rules import Rules, first_match, normalize_text __all__ = ["RowLabel", "column_label", "is_indicator_total", "levels_with_indicators", "read_label", "row_data_type"] @dataclass(frozen=True) class RowLabel: """Вид строки по метке: показатель ли она, `data_type` метки, текст строки взамен исходного, предупреждение.""" indicator: bool data_type: DataType | None text: str | None = None # для метки колонки «Показатели» — сама метка (`row_value`) warning: str | None = None def row_data_type(text: str, rules: Rules, currency_codes: frozenset[str]) -> DataType | None: """CURRENCY/QUANTITY по словарю строк («Вал.», «Кол.») или коду валюты ISO («USD», «usd»); иначе `None`.""" found = first_match(rules.data_types.row, text) if found is not None: return found return "CURRENCY" if normalize_text(text).upper() in currency_codes else None def column_label(sheet: Sheet, row: int, header: TableHeader) -> str: """Метка строки в колонке «Показатели» (пусто — колонки нет или ячейка пуста).""" return "" if header.indicator_col is None else sheet.text_at(row, header.indicator_col) def read_label(sheet: Sheet, row: int, text: str, header: TableHeader, rules: Rules) -> RowLabel: """Метка колонки «Показатели» важнее текста строки: «БУ» — строка данных BU, прочие — показатели; без метки — показатель по тексту («Вал.», «Кол.», «Кол-во», «В валюте», код валюты), иначе обычная строка без своего вида.""" label = column_label(sheet, row, header) if label: data_type = first_match(rules.data_types.indicator_column, label) if data_type == "BU": return RowLabel(indicator=False, data_type="BU") if data_type is not None or any(p.search(label) for p in rules.data_types.indicator_column_untyped): return RowLabel(indicator=True, data_type=data_type, text=label) return RowLabel(indicator=True, data_type=None, text=label, warning=unknown_label_warning(row, label)) data_type = row_data_type(text, rules, frozenset(rules.data_types.currency_codes)) return RowLabel(indicator=data_type is not None, data_type=data_type) def is_indicator_total(sheet: Sheet, row: int, header: TableHeader, rules: Rules) -> bool: """Итоговая строка-показатель за «Итого»: метка колонки «Показатели» не «БУ».""" label = column_label(sheet, row, header) return bool(label) and first_match(rules.data_types.indicator_column, label) != "BU" def levels_with_indicators( rows: Sequence[BodyRow], total_row: int | None, deadline: Deadline, *, header_account: bool = False, nest_by_sums: bool = False, ) -> Levels: """Каскад иерархии без строк-показателей; показатель — ребёнок предыдущей строки данных, на уровень глубже (§1.3): так строки НУ/Кол./«В валюте» на одном outline-уровне со своей строкой не ломают дерево строк БУ. Итоговые строки-показатели и показатель без строки данных выше — уровень 0 без родителя. """ levels = resolve_levels( [item for item in rows if not item.indicator], total_row, deadline, header_account=header_account, nest_by_sums=nest_by_sums, ) by_row: dict[int, int] = {} parent: dict[int, int | None] = {} previous: int | None = None for item in rows: if not item.indicator: by_row[item.row], parent[item.row] = levels.by_row[item.row], levels.parent[item.row] if total_row is None or item.row < total_row: previous = item.row continue if previous is None or (total_row is not None and item.row >= total_row): by_row[item.row], parent[item.row] = ROOT_LEVEL, None else: by_row[item.row], parent[item.row] = by_row[previous] + 1, previous return Levels(by_row=by_row, parent=parent, strategy=levels.strategy)