"""Шапка кандидата формы и тип-подсказка по ОКУД и заголовку (§12).""" import re from datetime import date from arm_statements.dates import parse_date from arm_statements.loader import Rules from arm_statements.model import Cell, DateField, DocumentType, HeaderCells, HeaderField, StatementHeader from arm_statements.text import fix_lookalikes, normalize_caption, normalize_label _INN_LENGTHS = (10, 12) _NOT_DIGIT = re.compile(r"\D") def _field(cell: Cell | None) -> HeaderField: if cell is None: return HeaderField(None, None, None) return HeaderField(cell.text, cell.confidence, None) def _stripped(cell: Cell | None) -> HeaderField: if cell is None: return HeaderField(None, None, None) return HeaderField(cell.text, cell.confidence, cell.text.strip() or None) def _digits(cell: Cell | None) -> HeaderField: """Только цифры текста (пусто — None); двойники не заменяются.""" if cell is None: return HeaderField(None, None, None) return HeaderField(cell.text, cell.confidence, _NOT_DIGIT.sub("", cell.text) or None) def _inn(cell: Cell | None) -> HeaderField: base = _digits(cell) if base.value is not None and len(base.value) not in _INN_LENGTHS: return HeaderField(base.text, base.confidence, None) return base def _report_date(cell: Cell | None, rules: Rules) -> DateField: if cell is None: return DateField(None, None, None) value: date | None if isinstance(cell.value, date): value = cell.value else: value = parse_date(fix_lookalikes(cell.text, rules.lookalikes), rules) return DateField(cell.text, cell.confidence, value) def _okud(cell: Cell | None, rules: Rules) -> HeaderField: if cell is None: return HeaderField(None, None, None) match = rules.headers.okud_code.search(normalize_label(cell.text)) return HeaderField(cell.text, cell.confidence, match.group(0) if match else None) def _units(cell: Cell | None, okei: HeaderField, rules: Rules) -> HeaderField: base = _field(cell) if cell is not None: caption = normalize_caption(cell.text) for pattern, canonical in rules.units.spellings: if pattern.search(caption): return HeaderField(base.text, base.confidence, canonical) if okei.value is not None: return HeaderField(base.text, base.confidence, rules.units.okei.get(okei.value)) return base def parse_header(cells: HeaderCells, rules: Rules) -> StatementHeader: """Шапка кандидата: текст и уверенность — из ячейки, значение — по правилам поля.""" okei = _digits(cells.okei) return StatementHeader( organization_name=_stripped(cells.organization_name), inn=_inn(cells.inn), report_date=_report_date(cells.report_date, rules), units=_units(cells.units, okei, rules), okud=_okud(cells.okud, rules), okpo=_digits(cells.okpo), okopf=_digits(cells.okopf), okfs=_digits(cells.okfs), okei=okei, address=_stripped(cells.address), needs_review_fields=(), ) def _type_by_okud(text: str | None, rules: Rules) -> DocumentType | None: if not text: return None label = normalize_label(text) for doc_type, patterns in rules.headers.okud.items(): if any(pattern.search(label) for pattern in patterns): return doc_type return None def type_hint(okud_text: str | None, title_text: str | None, rules: Rules) -> DocumentType | None: """Тип по ОКУД, иначе по заголовку формы; заголовки колонок не участвуют.""" by_okud = _type_by_okud(okud_text, rules) if by_okud is not None: return by_okud if not title_text: return None label = normalize_label(title_text) for doc_type, patterns in rules.headers.form_titles.items(): if any(pattern.search(label) for pattern in patterns): return doc_type return None def is_continuation(text: str | None, rules: Rules) -> bool: """Маркер страницы-продолжения «Форма 0710001 с.2» (ОКУД из словаря не обязателен).""" if not text: return False label = normalize_label(text) return any(pattern.search(label) for pattern in rules.headers.continuation) def continuation_type(text: str | None, rules: Rules) -> DocumentType | None: """Тип по ОКУД из маркера продолжения; не маркер или неизвестный ОКУД — None.""" if not is_continuation(text, rules): return None return _type_by_okud(text, rules)