C-1: nest_by_sums включается из osv.py, если схема шапки не называет группы после счёта; строка с ИНН не вкладывается и не становится родителем; прямые дети родителя должны быть на одном отступе. M-1: сдвиги складываются. M-2: код без разделителя сегментов не вкладывается. M-3: строка закрытия — только с ненулевыми оборотами. M-4: общий жадный перебор _greedy_ends для strategy_sums и вложенности, граница NEST_SCAN_LIMIT под тестом. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
95 lines
5.0 KiB
Python
95 lines
5.0 KiB
Python
"""Строки-показатели ОСВ (§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)
|