Spaces:
Running
Running
File size: 5,566 Bytes
71b0992 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | """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
|