doc-verify / src /doc_verify /validation.py
gaurabdas's picture
Deploy DocVerify fictional document-review demo
71b0992 verified
Raw History Blame Contribute Delete
5.57 kB
"""Checks facts supplied to it; makes no model calls or tax-policy decisions."""
import re
from decimal import ROUND_HALF_UP, Decimal
from .models import Document
def verify(invoice, purchase):
inv = Document.model_validate(invoice)
po = Document.model_validate(purchase) if purchase else None
findings = []
def flag(code, field, message):
findings.append({"code": code, "field": field, "message": message})
for name, doc in (("invoice", inv), ("purchase", po)):
if doc is None:
continue
for field in ("vendor", "number", "date", "currency", "subtotal", "tax", "total"):
if getattr(doc, field) is None:
flag(
"missing_field",
f"{name}.{field}",
"Not extracted; review the original document.",
)
quantum = Decimal("1") if doc.currency == "JPY" else Decimal("0.01")
if doc.currency not in {"INR", "USD", "EUR", "GBP", "JPY"}:
flag(
"unsupported_currency",
f"{name}.currency",
"Currency precision is not supported; review monetary calculations.",
)
arithmetic = False
else:
arithmetic = True
if not doc.lines:
flag("missing_lines", name, "No line items extracted.")
for index, line in enumerate(doc.lines):
if any(getattr(line, key) is None for key in ("quantity", "unit_price", "amount")):
flag(
"missing_field",
f"{name}.lines.{index}",
"Line amount, quantity, or price is unknown.",
)
elif arithmetic and (line.quantity * line.unit_price).quantize(
quantum, rounding=ROUND_HALF_UP
) != line.amount.quantize(quantum, rounding=ROUND_HALF_UP):
flag(
"line_total_mismatch",
f"{name}.lines.{index}",
"Quantity × unit price does not equal line amount.",
)
if (
arithmetic
and doc.lines
and all(line.amount is not None for line in doc.lines)
and doc.subtotal is not None
):
expected = sum((line.amount for line in doc.lines), Decimal(0)).quantize(
quantum, rounding=ROUND_HALF_UP
)
if expected != doc.subtotal.quantize(quantum, rounding=ROUND_HALF_UP):
flag(
"subtotal_mismatch",
f"{name}.subtotal",
"Sum of line amounts differs from subtotal.",
)
if arithmetic and all(
getattr(doc, field) is not None for field in ("subtotal", "tax", "total")
):
if (doc.subtotal + doc.tax).quantize(
quantum, rounding=ROUND_HALF_UP
) != doc.total.quantize(quantum, rounding=ROUND_HALF_UP):
flag(
"total_mismatch",
f"{name}.total",
"Subtotal + stated tax differs from total. Check discounts or other charges manually.",
)
if po:
currencies_match = bool(inv.currency and po.currency and inv.currency == po.currency)
if not currencies_match:
flag(
"currency_mismatch",
"currency",
"Currencies differ or are unknown; monetary comparison skipped.",
)
used = set()
for index, line in enumerate(inv.lines):
def normalize(text):
return re.sub(r"\s+", " ", (text or "").strip().casefold())
matches = [
(j, other)
for j, other in enumerate(po.lines)
if (line.sku and other.sku == line.sku)
or (
not line.sku
and line.description
and normalize(other.description) == normalize(line.description)
)
]
if len(matches) != 1 or matches[0][0] in used:
flag(
"ambiguous_match" if matches else "unmatched_line",
f"invoice.lines.{index}",
"Cannot establish a unique purchase-order line; review manually.",
)
continue
matched, other = matches[0]
used.add(matched)
if (
line.quantity is not None
and other.quantity is not None
and line.quantity != other.quantity
):
flag(
"quantity_mismatch",
f"invoice.lines.{index}",
"Invoice quantity differs from purchase order.",
)
if (
currencies_match
and line.unit_price is not None
and other.unit_price is not None
and line.unit_price != other.unit_price
):
flag(
"price_mismatch",
f"invoice.lines.{index}",
"Invoice unit price differs from purchase order.",
)
for index in set(range(len(po.lines))) - used:
flag(
"unmatched_purchase_line",
f"purchase.lines.{index}",
"Purchase-order line was not uniquely matched; partial fulfillment may be intentional.",
)
return findings