OCR прочитал сумму: как проверить денежные поля перед загрузкой в учёт

Сервис распознал счёт и вернул JSON. В поле итога стоит число, формат корректный, уверенность OCR высокая. Но загрузка документа в учёт всё ещё может создать ошибочную сумму. Полезная дополнительная проверка — связать денежные поля теми же арифметическими правилами, по которым составлен документ, и отправлять расхождения на ручную проверку.

Сначала зададим смысл каждой суммы

Для примера возьмём один тип документа: строки содержат суммы без налога, subtotal равен сумме строк, tax добавляется сверху, total равен subtotal плюс tax. Это явный контракт конкретного шаблона. В документе с ценами, уже включающими налог, или со скидкой после итогов формулы будут другими. Поэтому первый шаг при загрузке — определить тип формы и применимое правило.

Денежные значения передаются строками с точкой и ровно двумя десятичными знаками. Такое ограничение упрощает договор между извлечением и проверкой. Если OCR вернул запятую, валютный символ или пробелы разрядов, их нормализуют в отдельном этапе с сохранением исходного текста. Нельзя молча выкидывать все нецифровые символы: минус и десятичный разделитель меняют смысл числа.

Проверка использует Decimal из строк. Это позволяет точно представить десятичные суммы и сравнивать копейки без погрешности двоичного float. Мы не будем округлять произвольное число до двух знаков на этапе приёмки: лишняя точность считается нарушением входного контракта. Так ошибка извлечения не превратится в незаметно исправленную сумму.

Отделим ошибку поля от расхождения итогов

В примере три результата извлечения. У первого все связи сходятся, у второго OCR потерял разряд в total, у третьего tax имеет неверный формат. Код возвращает причины отклонения и id документа. Это позволяет сформировать очередь, в которой сотрудник видит, что именно нужно сверить с изображением.

import re
from decimal import Decimal

def money(value):
    if not isinstance(value, str) or not re.fullmatch(r"[0-9]+\.[0-9]{2}", value):
        raise ValueError("money_format")
    return Decimal(value)

def check(doc):
    try:
        lines = [money(v) for v in doc["lines"]]
        subtotal, tax, total = (money(doc[k]) for k in ("subtotal", "tax", "total"))
    except (ValueError, KeyError, TypeError):
        return ["invalid_fields"]
    reasons = []
    if not lines:
        reasons.append("empty_lines")
    if sum(lines, Decimal("0.00")) != subtotal:
        reasons.append("lines_vs_subtotal")
    if subtotal + tax != total:
        reasons.append("subtotal_tax_vs_total")
    return reasons

base = {"lines": ["100.10", "200.20"], "subtotal": "300.30", "tax": "60.06", "total": "360.36"}
docs = [("ok", base), ("bad_total", {**base, "total": "36.36"}),
        ("bad_format", {**base, "tax": "60,06"})]
results = {id_: check(doc) for id_, doc in docs}
for id_, reasons in results.items():
    print(id_, "accept" if not reasons else ",".join(reasons))
assert results == {"ok": [], "bad_total": ["subtotal_tax_vs_total"], "bad_format": ["invalid_fields"]}
assert check({**base, "lines": ["100.10", "200.21"]}) == ["lines_vs_subtotal"]

Документ ok проходит проверку. bad_total получает причину subtotal_tax_vs_total, а bad_format — invalid_fields. Неверный десятичный разделитель не смешивается с арифметическим расхождением. Сумма строк вычисляется из входных значений; ожидаемый итог не зашит в функцию. Дополнительный assert проверяет изменение копейки в отдельной строке.

На такой проверке нельзя автоматически «исправить» total суммой других полей. Если subtotal тоже распознан неправильно, система просто заменит одну ошибку другой. Покажите человеку исходное изображение, распознанные значения и конкретное равенство, которое не сошлось. Если у OCR есть координаты фрагментов, подсветите все участвующие поля, включая строки, а не только итог.

Согласованность полезна, но не доказывает правильность

Несколько неверных чисел могут согласованно удовлетворять формулам. Например, распознавание пропустило одну строку и одновременно уменьшило subtotal и total. Арифметическая проверка этого не обнаружит. На размеченной выборке нужно отдельно считать точность денежных полей и долю ошибочных документов, прошедших автоматическую приёмку. Для проверки полноты строк пригодится сравнение с ручной разметкой таблицы.

Для разных шаблонов заведите версионируемые правила. Если документ содержит построчный налог с округлением, считайте налог по строкам и только затем суммируйте. Расчёт от общей суммы может дать другую копейку. Если присутствуют скидки, авансы или удержания, включайте их как отдельные поля формулы. Допуск на расхождение задавайте из правил документа; произвольный широкий допуск скрывает потерянные значения.

Перед записью в БД сохраняйте статус проверки, причины, версию правил и ссылку на исходный файл. При повторной загрузке различайте новое распознавание и новый документ, чтобы исправление OCR не создало дубликат хозяйственной операции. Статистику причин отказа удобно использовать для выбора следующей работы: корректировать извлечение таблиц, нормализацию валюты или распознавание конкретного шаблона.

Поля валюты и знака документа проверяйте до арифметики. Возврат, корректировочный документ и обычный счёт могут по-разному задавать отрицательные суммы. Пример принимает только неотрицательные значения, поэтому для таких форм нужен отдельный контракт с явным знаком. Если разные строки имеют разные валюты, сложение вообще требует согласованного курса и даты пересчёта. Корректное равенство чисел без этой информации не подтверждает пригодность записи для учёта.

При ручной проверке записывайте исправление структурированно: какое поле изменили, какое значение было в извлечении и что видно в документе. Это позволяет оценить, какие ошибки переживают автоматические правила. Например, частые исправления реквизитов при сходящихся суммах означают, что расширять денежные равенства недостаточно. Для следующей версии потребуется проверка контрагента и номера документа по собственным правилам, а тестовый набор должен сохранять эти случаи отдельно от арифметических расхождений.

В платном курсе «Computer Vision Engineer» есть прикладная линия Doc-AI: чеки и акты проходят путь от распознавания до JSON и БД. Вы также работаете с сервисным слоем и метриками. Это помогает собрать весь процесс вокруг проверки: получить поля, сохранить результат и разобрать ошибку документа. Откройте программу курса.

Продолжить на практике

  • Computer Vision Engineer: от детекции и Doc-AI до продакшна ↗

    Практический курс по Computer Vision. Соберёте продакшн-сервис: датасет → обучение (YOLO/RT-DETR, SAM), трекинг, OCR/Doc-AI, веб-приложение (UI+API) и мониторинг. Экспорт в ONNX/TensorRT/OpenVINO, батчинг/квантование под CPU/GPU. Плюс профессиональные блоки: mAP/IoU/Dice и калибровка доверия, QA разметки и аугментации, p95/стоимость, дрейф данных и алерты, guardrails (NMS/правила), A/B-тесты, Triton/gRPC/FastAPI.