"""Шапка формы по меткам словаря (design D-5): значение — правее метки в той же строке, иначе в следующей строке. Особые правила: ИНН ниже метки «аудитор» не берётся; организация — после «Организация», а при пустом значении — ближайшая строка выше с признаком ОПФ (НПЗ); дата — из подзаголовка формы («на <дата>», «за <период>») либо из тройки «число | месяц | год»; единицы — текст как в файле. Найденные поля получают `*_confidence = 100`. """ import datetime import re from collections.abc import Iterator from src.contract.result import StatementHeader from src.parser.grid import Sheet from src.parser.numbers import inn_digits from src.parser.rules import Rules, StatementLabel from src.parser.statements.columns import cell_label from src.parser.statements.detect import FormBlock __all__ = ["parse_header"] _FULL_CONFIDENCE = 100 _DIGITS = re.compile(r"\d+") NO_ORGANIZATION_WARNING = "в шапке не найдена организация" NO_UNITS_WARNING = "в шапке не найдена единица измерения" def _cells(sheet: Sheet, first_row: int, last_row: int) -> Iterator[tuple[int, int, str]]: for row in range(first_row, last_row + 1): for col, _ in sheet.row_cells(row): yield row, col, cell_label(sheet, row, col) def _right(sheet: Sheet, row: int, col: int) -> list[tuple[int, str]]: return [(c, cell_label(sheet, row, c)) for c, _ in sheet.row_cells(row) if c > col] def _value_right_or_below(sheet: Sheet, row: int, col: int, last_row: int) -> str | None: right = _right(sheet, row, col) if right: return right[0][1] if row + 1 <= last_row: below = [(c, cell_label(sheet, row + 1, c)) for c, _ in sheet.row_cells(row + 1)] if below: return below[0][1] return None def _label_rows( sheet: Sheet, rules: Rules, key: StatementLabel, first_row: int, last_row: int ) -> list[tuple[int, int, str]]: return [ (row, col, text) for row, col, text in _cells(sheet, first_row, last_row) if rules.statement_headers.label(key, text) ] def _auditor_from(sheet: Sheet, rules: Rules, first_row: int, last_row: int) -> int: """Первая строка блока аудитора; ниже неё ИНН/названия — не организации.""" rows = [ row for row, _, text in _cells(sheet, first_row, last_row) if rules.statement_headers.is_auditor_label(text) ] return min(rows) if rows else last_row + 1 def _organization(sheet: Sheet, rules: Rules, first_row: int, last_row: int, auditor_from: int) -> str | None: for row, col, _ in _label_rows(sheet, rules, "organization", first_row, min(last_row, auditor_from - 1)): value = _value_right_or_below(sheet, row, col, last_row) if value and not rules.statement_headers.label("okpo", value) and not value.isdigit(): return value for above in range(row - 1, first_row - 1, -1): texts = [text for _, _, text in _cells(sheet, above, above)] if any(rules.statement_headers.has_legal_form(text) for text in texts): return next(text for text in texts if rules.statement_headers.has_legal_form(text)) return None def _inn(sheet: Sheet, rules: Rules, first_row: int, last_row: int, auditor_from: int) -> str | None: for row, col, _ in _label_rows(sheet, rules, "inn", first_row, min(last_row, auditor_from - 1)): for c, _ in _right(sheet, row, col): digits = inn_digits(sheet.cell(row, c)) if digits is not None: return digits return None def _labelled(sheet: Sheet, rules: Rules, key: StatementLabel, first_row: int, last_row: int) -> str | None: for row, col, _ in _label_rows(sheet, rules, key, first_row, last_row): value = _value_right_or_below(sheet, row, col, last_row) if value: return value return None def _okopf_okfs(sheet: Sheet, rules: Rules, first_row: int, last_row: int) -> tuple[str | None, str | None]: """«по ОКОПФ / ОКФС» с двумя числами правее (в той же или предыдущей строке — у 1С коды стоят строкой выше).""" for row, col, _ in _label_rows(sheet, rules, "okopf_okfs", first_row, last_row): for candidate in (row, row - 1): numbers = [text for _, text in _right(sheet, candidate, col) if _DIGITS.fullmatch(text)] if len(numbers) >= 2: # noqa: PLR2004 - пара кодов return numbers[0], numbers[1] if len(numbers) == 1: return numbers[0], None return _labelled(sheet, rules, "okopf", first_row, last_row), _labelled(sheet, rules, "okfs", first_row, last_row) def _units(sheet: Sheet, rules: Rules, first_row: int, last_row: int) -> str | None: for row, col, _ in _label_rows(sheet, rules, "units", first_row, last_row): for _, text in _right(sheet, row, col): if not rules.statement_headers.label("okei", text) and not text.isdigit(): return text return None def _report_date( sheet: Sheet, rules: Rules, block: FormBlock, first_row: int, last_row: int ) -> tuple[str | None, datetime.date | None]: periods = rules.statement_periods if block.title_row is not None and block.title_row + 1 <= last_row: # Подзаголовок — только следующая строка за заголовком формы # («на 31 декабря 2024 г.», «за 9 месяцев 2025 г.»); # глубже лежат «Форма по ОКУД» и тройка даты, где голый «2025» принялся бы за период for _, _, text in _cells(sheet, block.title_row + 1, block.title_row + 1): end = periods.period_end(text) if end is not None: return text, end for row, col, _ in _label_rows(sheet, rules, "date", first_row, last_row): parts = [text for _, text in _right(sheet, row, col)] if len(parts) >= 3: # noqa: PLR2004 - число, месяц, год found = periods.date_triple(parts[0], parts[1], parts[2]) if found is not None: return None, found return None, None def parse_header( sheet: Sheet, rules: Rules, first_row: int, last_row: int, block: FormBlock | None ) -> tuple[StatementHeader, list[str]]: if block is None: marker = next( ( text for _, _, text in _cells(sheet, first_row, last_row) if rules.statement_headers.is_continuation(text) ), None, ) return StatementHeader(form_name=marker), [] auditor_from = _auditor_from(sheet, rules, first_row, last_row) organization = _organization(sheet, rules, first_row, last_row, auditor_from) inn = _inn(sheet, rules, first_row, last_row, auditor_from) okopf, okfs = _okopf_okfs(sheet, rules, first_row, last_row) units = _units(sheet, rules, first_row, last_row) date_text, report_date = _report_date(sheet, rules, block, first_row, last_row) warnings: list[str] = [] if organization is None: warnings.append(NO_ORGANIZATION_WARNING) if units is None: warnings.append(NO_UNITS_WARNING) header = StatementHeader( form_name=block.form_name, organization_name=organization, organization_name_confidence=_FULL_CONFIDENCE if organization else None, inn=inn, inn_confidence=_FULL_CONFIDENCE if inn else None, report_date_text=date_text, report_date=report_date, units=units, okud=block.okud, okpo=_labelled(sheet, rules, "okpo", first_row, last_row), okopf=okopf, okfs=okfs, address=_labelled(sheet, rules, "address", first_row, min(last_row, auditor_from - 1)), ) return header, warnings