87 lines
6.2 KiB
Python
87 lines
6.2 KiB
Python
"""Атрибуция правки «сервис / наш разбор» (§14.2 `05-domain-rules.md`; design `quality-metrics` §5).
|
||
|
||
Правка — отличие подтверждённого аналитиком значения от того, что пришло на вход (снимок `quality_intake_fact`).
|
||
Правило одно: верный ответ есть в сыром тексте, который распознал сервис (ИС Сканирование / `tabular-parser`), —
|
||
ошибка наша (мы неверно разобрали то, что сервис прочитал правильно); верного ответа в сыром тексте нет — ошибка
|
||
сервиса. Для Excel сервис разбора — наш же `tabular-parser`, поэтому там любая ошибка распознавания — «наш разбор»
|
||
(design §5); это решает вызывающий, не передавая Excel-документы сюда, а не отдельная ветка в этих функциях.
|
||
|
||
Разбор сырого текста — тем же кодом, что разбирал сервис: публичным API `arm_statements`
|
||
(`fix_lookalikes`, `parse_amount`, `parse_code`, `default_rules`). Правила читаются один раз на вызов
|
||
(`default_rules()` — чистая и дешёвая фабрика пакета, копия правил per-call не кэшируется: это чистый домен без
|
||
состояния на процесс, кэш — не его забота).
|
||
"""
|
||
|
||
from collections.abc import Sequence
|
||
from typing import Literal
|
||
|
||
from arm_statements import Source, default_rules, fix_lookalikes, parse_amount, parse_code
|
||
|
||
__all__ = [
|
||
"attribute_added_row",
|
||
"attribute_code_fix",
|
||
"attribute_header_fix",
|
||
"attribute_value_fix",
|
||
]
|
||
|
||
Attribution = Literal["service", "ours"]
|
||
|
||
|
||
def _digits_only(text: str) -> str:
|
||
return "".join(char for char in text if char.isdigit())
|
||
|
||
|
||
def attribute_value_fix(confirmed: str, raw_text: str) -> Attribution:
|
||
"""§14.2 «Сумма»: «наш разбор», если цифры подтверждённой суммы (без знака) совпадают с цифрами, которые
|
||
извлекает из сырого текста `parse_amount` после замены двойников §11.3 — сервис прочитал верно, а мы
|
||
неверно разобрали (знак, скобки, разряды, мусор по краям); иначе — «сервис» (сервис сам прочитал не те цифры).
|
||
|
||
`parse_amount` требует `Source`; здесь он не определяет исход («О»→«0» и выделение числового ядра в тексте
|
||
работают одинаково для PDF и Excel), поэтому берём `Source.OCR` — распознавание различий между источниками
|
||
в `parse_amount` касается только неоднозначных разделителей тысяч, а их чтение в эту цифровую проверку не
|
||
входит (решение задачи 3, см. отчёт).
|
||
"""
|
||
rules = default_rules()
|
||
parsed = parse_amount(raw_text, Source.OCR, rules)
|
||
if parsed.amount is None:
|
||
return "service"
|
||
return "ours" if _digits_only(str(abs(parsed.amount))) == _digits_only(confirmed) else "service"
|
||
|
||
|
||
def attribute_code_fix(confirmed_code: str, raw_text: str) -> Attribution:
|
||
"""§14.2 «Код строки»: «наш разбор», если подтверждённый код есть среди кодов, которые `parse_code` извлекает
|
||
из строк сырого `line_code_text` (в т. ч. из одной из частей склейки `1250\\n1260` — каждая строка разбирается
|
||
отдельно, как это делает сам пакет для многострочных ячеек); иначе — «сервис».
|
||
"""
|
||
rules = default_rules()
|
||
codes: set[str] = set()
|
||
for line in raw_text.splitlines():
|
||
if not line.strip():
|
||
continue
|
||
parsed = parse_code(line, rules)
|
||
if parsed.code is not None:
|
||
codes.add(parsed.code)
|
||
return "ours" if confirmed_code in codes else "service"
|
||
|
||
|
||
def attribute_added_row(code: str, raw_line_codes: Sequence[str], *, is_excel: bool) -> Attribution:
|
||
"""§14.2 «Добавленная строка» (`row_index IS NULL`): для Excel всегда «наш разбор» (сервис разбора Excel — наш
|
||
`tabular-parser`, `raw_line_codes` не смотрим вовсе); для PDF — «наш разбор», если код добавленной строки есть
|
||
среди четырёхзначных токенов сырого ответа ИС Сканирование (`raw_line_codes` извлекаются раньше, при приёме, —
|
||
не задача этого модуля); иначе — «сервис» (сервис вообще не увидел такой код).
|
||
"""
|
||
if is_excel:
|
||
return "ours"
|
||
return "ours" if code in raw_line_codes else "service"
|
||
|
||
|
||
def attribute_header_fix(confirmed: str, header_source_text: str) -> Attribution:
|
||
"""§14.2 «Шапка» (период, ИНН, единицы, наименование) и «Дата колонки»: «наш разбор», если подтверждённое
|
||
значение можно найти в сыром тексте источника после замены двойников §11.3 — `header_source[*].text` для полей
|
||
шапки или подпись колонки (`label`) для даты колонки; какой именно текст передать — решает вызывающий, функция
|
||
одна и та же для обоих случаев (design §5). Иначе — «сервис».
|
||
"""
|
||
rules = default_rules()
|
||
normalized_source = fix_lookalikes(header_source_text, rules.lookalikes)
|
||
return "ours" if confirmed in normalized_source else "service"
|