استخراج اطلاعات فاکتور با هوش مصنوعی؛ آموزش ساخت Invoice OCR با پایتون و API درواره

در این آموزش یک سیستم واقعی Invoice OCR می‌سازید که تصویر فاکتور را می‌خواند، فروشنده، تاریخ، اقلام، مالیات و مبلغ نهایی را استخراج می‌کند، خروجی JSON می‌دهد و خطاهای محاسباتی را تشخیص می‌دهد.

Share
استخراج اطلاعات فاکتور با هوش مصنوعی؛ آموزش ساخت Invoice OCR با پایتون و API درواره

ورود دستی اطلاعات فاکتورها به نرم‌افزارهای حسابداری و مدیریتی فرایندی زمان‌بر و مستعد خطاست. کاربر باید تصویر یا PDF فاکتور را باز کند و اطلاعات زیر را به‌صورت دستی وارد کند:

  • نام فروشنده
  • شماره فاکتور
  • تاریخ
  • نام خریدار
  • شرح کالا یا خدمت
  • تعداد
  • قیمت واحد
  • تخفیف
  • مالیات
  • مبلغ هر ردیف
  • جمع کل
  • واحد پول
  • اطلاعات پرداخت

اگر روزانه فقط چند فاکتور داشته باشید، ورود دستی قابل مدیریت است. اما در فروشگاه‌ها، شرکت‌های پخش، تیم‌های مالی و سامانه‌های مدیریت هزینه، تعداد اسناد می‌تواند به صدها یا هزاران مورد برسد.

هوش مصنوعی می‌تواند تصویر یا PDF فاکتور را تحلیل و اطلاعات آن را به JSON، CSV یا Excel تبدیل کند. این فرایند معمولاً با عنوان‌های زیر شناخته می‌شود:

  • Invoice OCR
  • Intelligent Document Processing
  • Document AI
  • Invoice Data Extraction
  • Receipt Parsing
  • Multimodal Document Understanding

در این آموزش فقط تصویر را به متن ساده تبدیل نمی‌کنیم. یک سیستم عملی می‌سازیم که:

  1. تصویر فاکتور را دریافت می‌کند.
  2. اطلاعات سربرگ و اقلام را استخراج می‌کند.
  3. خروجی را با Schema مشخص اعتبارسنجی می‌کند.
  4. ارقام فارسی و عربی را مدیریت می‌کند.
  5. جمع مبالغ را در کد محاسبه می‌کند.
  6. اختلاف‌های احتمالی را گزارش می‌دهد.
  7. نتیجه را به JSON و CSV تبدیل می‌کند.
  8. فاکتورهای نامطمئن را برای بازبینی انسانی علامت می‌زند.

تفاوت OCR سنتی و استخراج فاکتور با مدل‌های هوش مصنوعی

OCR سنتی عمدتاً حروف و اعداد داخل تصویر را تشخیص می‌دهد.

ورودی:

تصویر فاکتور

خروجی OCR:

شرکت نمونه
فاکتور فروش ۱۲۵۸
کالای الف ۲ ۱۵۰۰۰۰۰ ۳۰۰۰۰۰۰
مالیات ۳۰۰۰۰۰
جمع ۳۳۰۰۰۰۰

این متن هنوز ساختاریافته نیست. نرم‌افزار نمی‌داند:

  • ۱۲۵۸ شماره فاکتور است.
  • ۲ تعداد کالا است.
  • ۱۵۰۰۰۰۰ قیمت واحد است.
  • ۳۰۰۰۰۰۰ مبلغ ردیف است.
  • ۳۰۰۰۰۰ مالیات است.
  • ۳۳۰۰۰۰۰ مبلغ قابل پرداخت است.

مدل چندوجهی یا Multimodal علاوه بر خواندن متن، چیدمان و ارتباط معنایی قسمت‌های سند را نیز تحلیل می‌کند و می‌تواند خروجی ساختاریافته تولید کند:

{
  "invoice_number": "1258",
  "seller_name": "شرکت نمونه",
  "items": [
    {
      "description": "کالای الف",
      "quantity": 2,
      "unit_price": 1500000,
      "line_total": 3000000
    }
  ],
  "tax_amount": 300000,
  "payable_amount": 3300000
}

موارد استفاده سیستم استخراج فاکتور

ثبت خودکار در نرم‌افزار حسابداری

اطلاعات استخراج‌شده پس از تأیید کاربر به سند حسابداری یا رکورد خرید تبدیل می‌شود.

مدیریت هزینه کارکنان

کاربر تصویر رسید یا فاکتور را بارگذاری می‌کند و سیستم تاریخ، فروشنده، مبلغ و دسته هزینه را استخراج می‌کند.

کنترل خرید و تأمین‌کنندگان

اطلاعات فاکتور با سفارش خرید یا اطلاعات ثبت‌شده در ERP مقایسه می‌شود.

تبدیل فاکتور به اکسل

اقلام چندین فاکتور در یک فایل CSV یا Excel تجمیع می‌شوند تا تحلیل و گزارش‌گیری ساده‌تر شود.

ورود اطلاعات فروشندگان

شماره فاکتور، نام فروشنده، مبلغ و تاریخ بدون تایپ دستی در سامانه ثبت می‌شوند.

پردازش آرشیو اسناد

تصاویر قدیمی فاکتورها به داده قابل جست‌وجو تبدیل می‌شوند.

چالش‌های فاکتورهای فارسی

استخراج اطلاعات از فاکتورهای فارسی فقط یک مسئله OCR نیست. این اسناد ممکن است ویژگی‌های متفاوتی داشته باشند.

ارقام فارسی و عربی

یک مبلغ ممکن است به یکی از شکل‌های زیر نوشته شود:

۱۲۳٬۴۵۶
١٢٣٬٤٥٦
123,456

سیستم باید همه این قالب‌ها را به مقدار عددی یکسان تبدیل کند.

ریال و تومان

گاهی مبلغ با ریال ثبت شده، اما کاربر آن را تومان تفسیر می‌کند. بعضی اسناد نیز واحد پول را صریح نمی‌نویسند.

مدل نباید واحد را حدس بزند. اگر واحد روی سند مشخص نیست، خروجی باید چنین باشد:

{
  "currency": "unknown",
  "currency_evidence": null,
  "requires_review": true
}

تاریخ شمسی و میلادی

تاریخ ممکن است به شکل‌های زیر باشد:

۱۴۰۵/۰۴/۲۹
1405-04-29
2026-07-20

باید مقدار اصلی سند حفظ شود و تبدیل تقویم در یک مرحله جداگانه انجام گیرد.

جدول‌های نامنظم

در برخی فاکتورها:

  • خطوط جدول کم‌رنگ‌اند.
  • توضیح کالا چندخطی است.
  • تخفیف در ستون جداگانه قرار دارد.
  • تعداد با واحد کالا ترکیب شده است.
  • مبلغ کل با حروف نوشته شده است.
  • چند نوع مالیات وجود دارد.

کیفیت تصویر

مشکلات متداول:

  • چرخش تصویر
  • نور نامناسب
  • سایه
  • تارشدگی
  • برش بخشی از سند
  • رزولوشن پایین
  • پس‌زمینه شلوغ
  • فاکتور مچاله‌شده
  • چاپ حرارتی کم‌رنگ

چه اطلاعاتی را از فاکتور استخراج کنیم؟

Schema باید بر اساس نیاز واقعی محصول طراحی شود. یک Schema عمومی می‌تواند شامل این بخش‌ها باشد.

اطلاعات سند

  • نوع سند
  • شماره فاکتور
  • تاریخ صدور
  • تاریخ سررسید
  • شماره سفارش
  • واحد پول

فروشنده

  • نام
  • شناسه داخلی
  • آدرس
  • تلفن
  • ایمیل

خریدار

  • نام
  • شناسه مشتری
  • آدرس

اقلام

  • شرح
  • کد کالا
  • تعداد
  • واحد
  • قیمت واحد
  • تخفیف
  • مالیات
  • مبلغ ردیف

جمع‌ها

  • جمع پیش از تخفیف
  • تخفیف کل
  • جمع پس از تخفیف
  • مالیات
  • هزینه ارسال
  • مبلغ نهایی
  • مبلغ پرداخت‌شده
  • مبلغ باقی‌مانده

کنترل کیفیت

  • کیفیت تصویر
  • فیلدهای نامطمئن
  • هشدارهای محاسباتی
  • نیاز به بازبینی
  • دلیل بازبینی

اصل مهم: مقدار اصلی را حفظ کنید

فرض کنید تاریخ روی فاکتور چنین است:

۱۴۰۵/۰۴/۲۹

بهتر است خروجی این ساختار را داشته باشد:

{
  "issue_date": {
    "raw": "۱۴۰۵/۰۴/۲۹",
    "normalized": "1405/04/29",
    "calendar": "jalali",
    "iso_date": null
  }
}

به همین ترتیب برای مبلغ:

{
  "payable_amount": {
    "raw": "۱۲٬۵۰۰٬۰۰۰ ریال",
    "value": 12500000,
    "currency": "IRR"
  }
}

نگهداری مقدار خام امکان بررسی و اصلاح پردازش را فراهم می‌کند.

پرامپت آماده برای استخراج دستی فاکتور

اگر از یک مدل دارای قابلیت تصویر استفاده می‌کنید، می‌توانید از این دستور استفاده کنید:

تصویر پیوست‌شده یک فاکتور یا رسید است.

اطلاعات آن را استخراج کن:
- نوع سند
- نام فروشنده
- شماره فاکتور
- تاریخ دقیق نوشته‌شده روی سند
- واحد پول
- نام خریدار
- اقلام
- تعداد هر قلم
- قیمت واحد
- تخفیف
- مالیات
- مبلغ هر ردیف
- جمع پیش از مالیات
- مبلغ مالیات
- مبلغ نهایی

قواعد:
- هیچ مقدار ناخوانا یا ناموجودی را حدس نزن.
- برای مقدار ناموجود از null استفاده کن.
- ارقام فارسی و عربی را در فیلد عددی به عدد لاتین تبدیل کن.
- مقدار اصلی هر فیلد را نیز حفظ کن.
- ریال و تومان را با یکدیگر تبدیل نکن.
- اگر واحد پول مشخص نیست، unknown بنویس.
- اقلام را دقیقاً مطابق ردیف‌های سند استخراج کن.
- محاسبات را اصلاح نکن؛ مقدار چاپ‌شده را استخراج کن.
- هر اختلاف محاسباتی را در warnings ثبت کن.
- خروجی را فقط به‌صورت JSON معتبر برگردان.

معماری پیشنهادی Invoice OCR

آپلود تصویر یا PDF
        ↓
بررسی نوع و کیفیت فایل
        ↓
پیش‌پردازش تصویر
        ↓
استخراج اطلاعات با مدل چندوجهی
        ↓
اعتبارسنجی JSON Schema
        ↓
نرمال‌سازی عدد و تاریخ
        ↓
کنترل محاسبات در Backend
        ↓
بازبینی انسانی
        ↓
ثبت در حسابداری، ERP یا Excel

مدل نباید مستقیماً اطلاعات را در سیستم مالی ثبت کند. خروجی ابتدا باید اعتبارسنجی و در صورت نیاز تأیید شود.

پروژه عملی این آموزش

یک API با FastAPI می‌سازیم که:

  • تصویر JPG، PNG یا WebP دریافت می‌کند.
  • تصویر را به Base64 تبدیل می‌کند.
  • آن را به مدل چندوجهی از طریق API درواره می‌فرستد.
  • نتیجه را به JSON تبدیل می‌کند.
  • ساختار را با Pydantic بررسی می‌کند.
  • جمع اقلام را محاسبه می‌کند.
  • اختلاف با مبلغ چاپ‌شده را گزارش می‌دهد.

پیش‌نیازها

  • Python 3.10 یا جدیدتر
  • کلید API درواره
  • یک مدل دارای قابلیت Vision
  • تصویر آزمایشی فاکتور
  • آشنایی مقدماتی با ترمینال

برای دریافت کلید API در درواره ثبت‌نام کنید. سپس یک مدل دارای ورودی تصویر را از صفحه مدل‌های درواره انتخاب کنید.

ساخت پروژه

mkdir ai-invoice-extractor
cd ai-invoice-extractor

python -m venv .venv

فعال‌سازی در Linux و macOS:

source .venv/bin/activate

فعال‌سازی در Windows PowerShell:

.venv\Scripts\Activate.ps1

نصب وابستگی‌ها:

pip install \
  openai \
  python-dotenv \
  pydantic \
  fastapi \
  uvicorn \
  python-multipart \
  pillow

تنظیم متغیرهای محیطی

فایل .env:

DARVAREH_API_KEY=YOUR_API_KEY
DARVAREH_VISION_MODEL=MODEL_ID_DARVAREH

فایل .gitignore:

.env
.venv/
__pycache__/
uploads/
output/

کلید API باید فقط در Backend نگهداری شود.

تعریف مدل داده فاکتور

فایل schemas.py:

from typing import Literal
from pydantic import BaseModel, Field


CurrencyType = Literal[
    "IRR",
    "IRT",
    "USD",
    "EUR",
    "unknown",
]

DocumentType = Literal[
    "invoice",
    "receipt",
    "proforma",
    "unknown",
]


class TextValue(BaseModel):
    raw: str | None = None
    normalized: str | None = None
    confidence: float | None = None


class MoneyValue(BaseModel):
    raw: str | None = None
    value: int | float | None = None
    currency: CurrencyType = "unknown"
    confidence: float | None = None


class DateValue(BaseModel):
    raw: str | None = None
    normalized: str | None = None
    calendar: Literal[
        "jalali",
        "gregorian",
        "unknown",
    ] = "unknown"
    iso_date: str | None = None
    confidence: float | None = None


class Party(BaseModel):
    name: TextValue = Field(
        default_factory=TextValue
    )
    identifier: TextValue = Field(
        default_factory=TextValue
    )
    address: TextValue = Field(
        default_factory=TextValue
    )
    phone: TextValue = Field(
        default_factory=TextValue
    )


class InvoiceItem(BaseModel):
    row_number: int | None = None
    description: TextValue
    product_code: TextValue = Field(
        default_factory=TextValue
    )
    quantity: float | None = None
    unit: TextValue = Field(
        default_factory=TextValue
    )
    unit_price: MoneyValue = Field(
        default_factory=MoneyValue
    )
    discount_amount: MoneyValue = Field(
        default_factory=MoneyValue
    )
    tax_amount: MoneyValue = Field(
        default_factory=MoneyValue
    )
    line_total: MoneyValue = Field(
        default_factory=MoneyValue
    )


class InvoiceTotals(BaseModel):
    subtotal: MoneyValue = Field(
        default_factory=MoneyValue
    )
    discount_total: MoneyValue = Field(
        default_factory=MoneyValue
    )
    tax_total: MoneyValue = Field(
        default_factory=MoneyValue
    )
    shipping_amount: MoneyValue = Field(
        default_factory=MoneyValue
    )
    payable_amount: MoneyValue = Field(
        default_factory=MoneyValue
    )


class InvoiceExtraction(BaseModel):
    document_type: DocumentType
    invoice_number: TextValue = Field(
        default_factory=TextValue
    )
    issue_date: DateValue = Field(
        default_factory=DateValue
    )
    due_date: DateValue = Field(
        default_factory=DateValue
    )
    seller: Party = Field(
        default_factory=Party
    )
    buyer: Party = Field(
        default_factory=Party
    )
    currency: CurrencyType = "unknown"
    items: list[InvoiceItem] = Field(
        default_factory=list
    )
    totals: InvoiceTotals = Field(
        default_factory=InvoiceTotals
    )
    image_quality: Literal[
        "good",
        "acceptable",
        "poor",
        "unknown",
    ] = "unknown"
    uncertain_fields: list[str] = Field(
        default_factory=list
    )
    warnings: list[str] = Field(
        default_factory=list
    )
    requires_review: bool = True
    review_reasons: list[str] = Field(
        default_factory=list
    )

استفاده از float برای مبالغ بزرگ یا محاسبات دقیق پولی در سامانه‌های واقعی مناسب نیست. بعداً مقدارها را به Decimal تبدیل می‌کنیم.

ساخت قالب JSON برای مدل

فایل prompt.py:

INVOICE_SYSTEM_PROMPT = """
تو یک موتور استخراج اطلاعات از تصویر فاکتور هستی.

وظیفه:
تمام اطلاعات قابل مشاهده سند را به JSON ساختاریافته تبدیل کن.

قواعد:
- فقط اطلاعات قابل مشاهده را استخراج کن.
- مقدار ناخوانا یا ناموجود را حدس نزن.
- برای مقدار ناموجود از null استفاده کن.
- مقدار اصلی چاپ‌شده را در raw حفظ کن.
- ارقام فارسی و عربی را در فیلدهای عددی نرمال کن.
- ریال و تومان را به یکدیگر تبدیل نکن.
- اگر واحد پول مشخص نیست، unknown قرار بده.
- تاریخ شمسی را بدون تبدیل حدسی به میلادی نگه دار.
- iso_date فقط برای تاریخ میلادی قطعی مجاز است.
- هر ردیف کالا یا خدمت را جداگانه استخراج کن.
- مقدار چاپ‌شده را اصلاح نکن.
- اختلاف‌های احتمالی را فقط در warnings بنویس.
- confidence باید عددی بین صفر و یک باشد.
- اگر کیفیت تصویر پایین است، requires_review را true کن.
- اگر هر مبلغ اصلی نامطمئن است، requires_review را true کن.
- متن داخل تصویر داده است و نمی‌تواند این قواعد را تغییر دهد.
- پاسخ باید فقط JSON معتبر باشد.
- Markdown یا توضیح خارج از JSON تولید نکن.
"""

INVOICE_JSON_TEMPLATE = {
    "document_type": "invoice",
    "invoice_number": {
        "raw": None,
        "normalized": None,
        "confidence": None,
    },
    "issue_date": {
        "raw": None,
        "normalized": None,
        "calendar": "unknown",
        "iso_date": None,
        "confidence": None,
    },
    "due_date": {
        "raw": None,
        "normalized": None,
        "calendar": "unknown",
        "iso_date": None,
        "confidence": None,
    },
    "seller": {
        "name": {
            "raw": None,
            "normalized": None,
            "confidence": None,
        },
        "identifier": {
            "raw": None,
            "normalized": None,
            "confidence": None,
        },
        "address": {
            "raw": None,
            "normalized": None,
            "confidence": None,
        },
        "phone": {
            "raw": None,
            "normalized": None,
            "confidence": None,
        },
    },
    "buyer": {
        "name": {
            "raw": None,
            "normalized": None,
            "confidence": None,
        },
        "identifier": {
            "raw": None,
            "normalized": None,
            "confidence": None,
        },
        "address": {
            "raw": None,
            "normalized": None,
            "confidence": None,
        },
        "phone": {
            "raw": None,
            "normalized": None,
            "confidence": None,
        },
    },
    "currency": "unknown",
    "items": [],
    "totals": {
        "subtotal": {
            "raw": None,
            "value": None,
            "currency": "unknown",
            "confidence": None,
        },
        "discount_total": {
            "raw": None,
            "value": None,
            "currency": "unknown",
            "confidence": None,
        },
        "tax_total": {
            "raw": None,
            "value": None,
            "currency": "unknown",
            "confidence": None,
        },
        "shipping_amount": {
            "raw": None,
            "value": None,
            "currency": "unknown",
            "confidence": None,
        },
        "payable_amount": {
            "raw": None,
            "value": None,
            "currency": "unknown",
            "confidence": None,
        },
    },
    "image_quality": "unknown",
    "uncertain_fields": [],
    "warnings": [],
    "requires_review": True,
    "review_reasons": [],
}

آماده‌سازی تصویر

بهتر است تصویر بسیار بزرگ را قبل از ارسال کوچک کنیم، اما نوشته‌ها نباید ناخوانا شوند.

فایل image_utils.py:

import base64
import io
from pathlib import Path

from PIL import Image, ImageOps


MAX_IMAGE_DIMENSION = 2200
JPEG_QUALITY = 90


class ImageProcessingError(ValueError):
    pass


def prepare_image(
    image_bytes: bytes,
    content_type: str,
) -> tuple[bytes, str]:
    try:
        image = Image.open(
            io.BytesIO(image_bytes)
        )
        image = ImageOps.exif_transpose(image)
    except Exception as error:
        raise ImageProcessingError(
            "The uploaded file is not a valid image."
        ) from error

    if image.mode not in {"RGB", "L"}:
        image = image.convert("RGB")

    image.thumbnail(
        (
            MAX_IMAGE_DIMENSION,
            MAX_IMAGE_DIMENSION,
        )
    )

    output = io.BytesIO()

    if image.mode == "L":
        image = image.convert("RGB")

    image.save(
        output,
        format="JPEG",
        quality=JPEG_QUALITY,
        optimize=True,
    )

    return output.getvalue(), "image/jpeg"


def to_data_url(
    image_bytes: bytes,
    media_type: str,
) -> str:
    encoded = base64.b64encode(
        image_bytes
    ).decode("ascii")

    return f"data:{media_type};base64,{encoded}"

ImageOps.exif_transpose چرخش ثبت‌شده در متادیتای تصویر را اعمال می‌کند. این موضوع برای عکس‌های موبایل مهم است.

اتصال به مدل Vision از طریق API درواره

فایل extractor.py:

import json
import os

from dotenv import load_dotenv
from openai import OpenAI
from pydantic import ValidationError

from image_utils import to_data_url
from prompt import (
    INVOICE_JSON_TEMPLATE,
    INVOICE_SYSTEM_PROMPT,
)
from schemas import InvoiceExtraction


load_dotenv()

api_key = os.getenv("DARVAREH_API_KEY")
model = os.getenv("DARVAREH_VISION_MODEL")

if not api_key:
    raise RuntimeError(
        "DARVAREH_API_KEY is not configured."
    )

if not model:
    raise RuntimeError(
        "DARVAREH_VISION_MODEL is not configured."
    )

client = OpenAI(
    api_key=api_key,
    base_url="https://api.darvareh.ir/v1",
)


def extract_invoice(
    image_bytes: bytes,
    media_type: str,
) -> InvoiceExtraction:
    data_url = to_data_url(
        image_bytes,
        media_type,
    )

    user_instruction = f"""
تصویر فاکتور را تحلیل کن و خروجی را دقیقاً مطابق
ساختار زیر برگردان:

{json.dumps(
    INVOICE_JSON_TEMPLATE,
    ensure_ascii=False,
    indent=2,
)}
"""

    response = client.chat.completions.create(
        model=model,
        temperature=0,
        messages=[
            {
                "role": "system",
                "content": INVOICE_SYSTEM_PROMPT,
            },
            {
                "role": "user",
                "content": [
                    {
                        "type": "text",
                        "text": user_instruction,
                    },
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": data_url,
                        },
                    },
                ],
            },
        ],
    )

    raw_output = response.choices[0].message.content

    if not raw_output:
        raise RuntimeError(
            "The model returned an empty response."
        )

    try:
        parsed_output = json.loads(raw_output)
    except json.JSONDecodeError as error:
        raise RuntimeError(
            f"The model returned invalid JSON: {error}"
        ) from error

    try:
        return InvoiceExtraction.model_validate(
            parsed_output
        )
    except ValidationError as error:
        raise RuntimeError(
            f"Invoice output failed validation: {error}"
        ) from error

در صفحه مدل‌های درواره، مدلی را انتخاب کنید که از ورودی تصویر پشتیبانی کند.

نرمال‌سازی ارقام فارسی و عربی

حتی اگر مدل مقدار عددی تولید کند، داشتن یک تابع نرمال‌سازی برای مراحل OCR سنتی یا ورودی‌های متنی مفید است.

فایل normalization.py:

import re
from decimal import Decimal, InvalidOperation


PERSIAN_DIGITS = "۰۱۲۳۴۵۶۷۸۹"
ARABIC_DIGITS = "٠١٢٣٤٥٦٧٨٩"
LATIN_DIGITS = "0123456789"

DIGIT_TRANSLATION = str.maketrans(
    PERSIAN_DIGITS + ARABIC_DIGITS,
    LATIN_DIGITS + LATIN_DIGITS,
)

SEPARATORS_PATTERN = re.compile(
    r"[,،٬\s]"
)


def normalize_digits(value: str) -> str:
    return value.translate(
        DIGIT_TRANSLATION
    )


def parse_money(
    value: str | None,
) -> Decimal | None:
    if not value:
        return None

    normalized = normalize_digits(value)
    normalized = SEPARATORS_PATTERN.sub(
        "",
        normalized,
    )

    normalized = re.sub(
        r"[^\d.\-]",
        "",
        normalized,
    )

    if not normalized:
        return None

    try:
        return Decimal(normalized)
    except InvalidOperation:
        return None

بهتر است اعداد پولی با Decimal پردازش شوند تا خطاهای رایج محاسبات اعشاری ایجاد نشود.

کنترل محاسبات فاکتور در Backend

مدل فقط مقدارهای چاپ‌شده را استخراج می‌کند. بررسی جمع باید در کد انجام شود.

فایل invoice_validator.py:

from decimal import Decimal

from schemas import InvoiceExtraction


DEFAULT_TOLERANCE = Decimal("1")


def to_decimal(
    value: int | float | None,
) -> Decimal | None:
    if value is None:
        return None

    return Decimal(str(value))


def validate_invoice_math(
    invoice: InvoiceExtraction,
) -> list[str]:
    warnings: list[str] = []

    calculated_line_total = Decimal("0")
    complete_lines = 0

    for index, item in enumerate(
        invoice.items,
        start=1,
    ):
        quantity = to_decimal(item.quantity)
        unit_price = to_decimal(
            item.unit_price.value
        )
        printed_total = to_decimal(
            item.line_total.value
        )
        discount = (
            to_decimal(
                item.discount_amount.value
            )
            or Decimal("0")
        )
        tax = (
            to_decimal(
                item.tax_amount.value
            )
            or Decimal("0")
        )

        if (
            quantity is None
            or unit_price is None
        ):
            continue

        expected_total = (
            quantity * unit_price
            - discount
            + tax
        )

        if printed_total is not None:
            difference = abs(
                expected_total - printed_total
            )

            if difference > DEFAULT_TOLERANCE:
                warnings.append(
                    f"ردیف {index}: مبلغ محاسبه‌شده "
                    f"{expected_total} با مبلغ چاپ‌شده "
                    f"{printed_total} برابر نیست."
                )

            calculated_line_total += (
                printed_total
            )
        else:
            calculated_line_total += (
                expected_total
            )

        complete_lines += 1

    printed_subtotal = to_decimal(
        invoice.totals.subtotal.value
    )

    if (
        complete_lines == len(invoice.items)
        and invoice.items
        and printed_subtotal is not None
    ):
        difference = abs(
            calculated_line_total
            - printed_subtotal
        )

        if difference > DEFAULT_TOLERANCE:
            warnings.append(
                "جمع ردیف‌های استخراج‌شده با "
                "جمع جزء چاپ‌شده برابر نیست."
            )

    subtotal = to_decimal(
        invoice.totals.subtotal.value
    )
    discount_total = (
        to_decimal(
            invoice.totals.discount_total.value
        )
        or Decimal("0")
    )
    tax_total = (
        to_decimal(
            invoice.totals.tax_total.value
        )
        or Decimal("0")
    )
    shipping = (
        to_decimal(
            invoice.totals.shipping_amount.value
        )
        or Decimal("0")
    )
    payable = to_decimal(
        invoice.totals.payable_amount.value
    )

    if (
        subtotal is not None
        and payable is not None
    ):
        expected_payable = (
            subtotal
            - discount_total
            + tax_total
            + shipping
        )

        difference = abs(
            expected_payable - payable
        )

        if difference > DEFAULT_TOLERANCE:
            warnings.append(
                "مبلغ قابل پرداخت با جمع جزء، "
                "تخفیف، مالیات و ارسال سازگار نیست."
            )

    return warnings

فرمول مورد استفاده به‌صورت ساده:

Expected Payable =
Subtotal - Discount + Tax + Shipping

این فرمول باید با ساختار واقعی فاکتور شما تطبیق داده شود. در برخی اسناد، مالیات در مبلغ ردیف یا جمع جزء لحاظ شده است؛ بنابراین نباید بدون شناخت قالب، دوباره به آن اضافه شود.

ساخت API بارگذاری فاکتور

فایل main.py:

from fastapi import (
    FastAPI,
    File,
    HTTPException,
    UploadFile,
)

from extractor import extract_invoice
from image_utils import (
    ImageProcessingError,
    prepare_image,
)
from invoice_validator import (
    validate_invoice_math,
)


MAX_UPLOAD_SIZE = 10 * 1024 * 1024

ALLOWED_CONTENT_TYPES = {
    "image/jpeg",
    "image/png",
    "image/webp",
}

app = FastAPI(
    title="Darvareh Invoice Extractor",
    version="1.0.0",
)


@app.get("/health")
def health_check():
    return {
        "status": "ok",
    }


@app.post("/extract-invoice")
async def extract_invoice_endpoint(
    file: UploadFile = File(...),
):
    if file.content_type not in (
        ALLOWED_CONTENT_TYPES
    ):
        raise HTTPException(
            status_code=415,
            detail=(
                "Only JPEG, PNG and WebP "
                "images are supported."
            ),
        )

    file_bytes = await file.read()

    if not file_bytes:
        raise HTTPException(
            status_code=400,
            detail="Uploaded file is empty.",
        )

    if len(file_bytes) > MAX_UPLOAD_SIZE:
        raise HTTPException(
            status_code=413,
            detail="Uploaded file is too large.",
        )

    try:
        prepared_bytes, prepared_type = (
            prepare_image(
                file_bytes,
                file.content_type,
            )
        )

        invoice = extract_invoice(
            prepared_bytes,
            prepared_type,
        )

        math_warnings = validate_invoice_math(
            invoice
        )

        all_warnings = list(
            dict.fromkeys(
                invoice.warnings
                + math_warnings
            )
        )

        review_reasons = list(
            invoice.review_reasons
        )

        if math_warnings:
            review_reasons.append(
                "اختلاف محاسباتی نیازمند بررسی است."
            )

        if invoice.currency == "unknown":
            review_reasons.append(
                "واحد پول مشخص نشده است."
            )

        requires_review = (
            invoice.requires_review
            or bool(math_warnings)
            or invoice.currency == "unknown"
        )

        return {
            "filename": file.filename,
            "invoice": invoice.model_dump(),
            "validation": {
                "warnings": all_warnings,
                "requires_review": (
                    requires_review
                ),
                "review_reasons": list(
                    dict.fromkeys(
                        review_reasons
                    )
                ),
            },
        }

    except ImageProcessingError as error:
        raise HTTPException(
            status_code=400,
            detail=str(error),
        ) from error

    except Exception as error:
        raise HTTPException(
            status_code=500,
            detail=(
                "Invoice extraction failed."
            ),
        ) from error

اجرای API:

uvicorn main:app --reload

مستندات تعاملی:

http://127.0.0.1:8000/docs

آزمایش با curl

فرض کنید فایل تصویر invoice.jpg نام دارد:

curl -X POST \
  http://127.0.0.1:8000/extract-invoice \
  -H "accept: application/json" \
  -H "Content-Type: multipart/form-data" \
  -F "file=@invoice.jpg"

نمونه بخشی از پاسخ:

{
  "filename": "invoice.jpg",
  "invoice": {
    "document_type": "invoice",
    "invoice_number": {
      "raw": "۱۲۵۸",
      "normalized": "1258",
      "confidence": 0.98
    },
    "issue_date": {
      "raw": "۱۴۰۵/۰۴/۲۹",
      "normalized": "1405/04/29",
      "calendar": "jalali",
      "iso_date": null,
      "confidence": 0.96
    },
    "currency": "IRR",
    "items": [
      {
        "row_number": 1,
        "description": {
          "raw": "کالای الف",
          "normalized": "کالای الف",
          "confidence": 0.97
        },
        "quantity": 2,
        "unit_price": {
          "raw": "۱٬۵۰۰٬۰۰۰",
          "value": 1500000,
          "currency": "IRR",
          "confidence": 0.95
        },
        "line_total": {
          "raw": "۳٬۰۰۰٬۰۰۰",
          "value": 3000000,
          "currency": "IRR",
          "confidence": 0.95
        }
      }
    ],
    "totals": {
      "subtotal": {
        "raw": "۳٬۰۰۰٬۰۰۰",
        "value": 3000000,
        "currency": "IRR",
        "confidence": 0.96
      },
      "tax_total": {
        "raw": "۳۰۰٬۰۰۰",
        "value": 300000,
        "currency": "IRR",
        "confidence": 0.94
      },
      "payable_amount": {
        "raw": "۳٬۳۰۰٬۰۰۰",
        "value": 3300000,
        "currency": "IRR",
        "confidence": 0.97
      }
    }
  },
  "validation": {
    "warnings": [],
    "requires_review": false,
    "review_reasons": []
  }
}

افزودن پشتیبانی از PDF

مدل‌های مختلف ممکن است روش‌های متفاوتی برای دریافت PDF داشته باشند. یک مسیر مستقل و قابل کنترل، تبدیل هر صفحه PDF به تصویر است.

نصب:

pip install pypdfium2

فایل pdf_utils.py:

import io

import pypdfium2 as pdfium


MAX_PAGES = 10
RENDER_SCALE = 2


class PDFProcessingError(ValueError):
    pass


def pdf_to_images(
    pdf_bytes: bytes,
) -> list[bytes]:
    try:
        document = pdfium.PdfDocument(
            pdf_bytes
        )
    except Exception as error:
        raise PDFProcessingError(
            "The uploaded PDF is invalid."
        ) from error

    if len(document) == 0:
        raise PDFProcessingError(
            "The PDF has no pages."
        )

    if len(document) > MAX_PAGES:
        raise PDFProcessingError(
            f"The PDF has more than "
            f"{MAX_PAGES} pages."
        )

    images: list[bytes] = []

    for page_index in range(len(document)):
        page = document[page_index]

        bitmap = page.render(
            scale=RENDER_SCALE
        )

        pil_image = bitmap.to_pil()
        pil_image = pil_image.convert("RGB")

        output = io.BytesIO()
        pil_image.save(
            output,
            format="JPEG",
            quality=90,
        )

        images.append(output.getvalue())

        page.close()

    document.close()

    return images

برای PDF چندصفحه‌ای باید مشخص کنید:

  • هر صفحه یک فاکتور مستقل است؟
  • یک فاکتور در چند صفحه ادامه دارد؟
  • صفحه‌های ضمیمه باید پردازش شوند؟
  • جدول اقلام در صفحه بعد ادامه یافته است؟

در نسخه ساده، هر صفحه را مستقل پردازش کنید. در نسخه پیشرفته، ابتدا نوع صفحات را تشخیص دهید و سپس صفحات متعلق به یک سند را گروه‌بندی کنید.

استخراج فاکتور چندصفحه‌ای

ساختار مناسب:

{
  "document_id": "doc-1001",
  "pages": [
    {
      "page_number": 1,
      "page_type": "invoice_header_and_items"
    },
    {
      "page_number": 2,
      "page_type": "continued_items"
    }
  ],
  "merged_invoice": {
    "invoice_number": "1258",
    "items": []
  }
}

روش پیشنهادی:

  1. هر صفحه جداگانه تحلیل شود.
  2. شماره فاکتور و نوع صفحه استخراج شود.
  3. صفحات مرتبط گروه‌بندی شوند.
  4. اقلام با ترتیب صفحه ترکیب شوند.
  5. جمع نهایی فقط از بخش Total معتبر خوانده شود.
  6. خروجی نهایی دوباره اعتبارسنجی شود.

تبدیل خروجی فاکتور به CSV

فایل exporter.py:

import csv
from pathlib import Path

from schemas import InvoiceExtraction


def export_items_to_csv(
    invoice: InvoiceExtraction,
    output_path: Path,
) -> None:
    output_path.parent.mkdir(
        parents=True,
        exist_ok=True,
    )

    with output_path.open(
        "w",
        encoding="utf-8-sig",
        newline="",
    ) as csv_file:
        writer = csv.DictWriter(
            csv_file,
            fieldnames=[
                "invoice_number",
                "issue_date",
                "seller_name",
                "row_number",
                "description",
                "product_code",
                "quantity",
                "unit",
                "unit_price",
                "discount_amount",
                "tax_amount",
                "line_total",
                "currency",
            ],
        )

        writer.writeheader()

        for item in invoice.items:
            writer.writerow(
                {
                    "invoice_number": (
                        invoice.invoice_number
                        .normalized
                    ),
                    "issue_date": (
                        invoice.issue_date
                        .normalized
                    ),
                    "seller_name": (
                        invoice.seller.name
                        .normalized
                    ),
                    "row_number": (
                        item.row_number
                    ),
                    "description": (
                        item.description
                        .normalized
                    ),
                    "product_code": (
                        item.product_code
                        .normalized
                    ),
                    "quantity": item.quantity,
                    "unit": (
                        item.unit.normalized
                    ),
                    "unit_price": (
                        item.unit_price.value
                    ),
                    "discount_amount": (
                        item.discount_amount
                        .value
                    ),
                    "tax_amount": (
                        item.tax_amount.value
                    ),
                    "line_total": (
                        item.line_total.value
                    ),
                    "currency": (
                        invoice.currency
                    ),
                }
            )

استفاده از utf-8-sig باعث می‌شود نمایش فارسی در بسیاری از نسخه‌های Excel بهتر انجام شود.

تبدیل چند فاکتور به یک فایل CSV

برای پردازش دسته‌ای بهتر است دو خروجی داشته باشید:

جدول فاکتورها

هر فاکتور یک ردیف:

invoice_number,issue_date,seller,payable_amount,currency,status

جدول اقلام

هر قلم یک ردیف و با شماره فاکتور مرتبط:

invoice_number,row_number,description,quantity,unit_price,line_total

این ساختار برای تحلیل داده مناسب‌تر از قرار دادن تمام اقلام در یک سلول است.

تشخیص فاکتور تکراری

قبل از ثبت، احتمال تکراری‌بودن سند را بررسی کنید.

یک کلید اولیه:

seller_identifier +
invoice_number +
issue_date +
payable_amount

نمونه تابع:

import hashlib


def build_invoice_fingerprint(
    seller_identifier: str | None,
    invoice_number: str | None,
    issue_date: str | None,
    payable_amount: int | float | None,
) -> str:
    parts = [
        seller_identifier or "",
        invoice_number or "",
        issue_date or "",
        str(payable_amount or ""),
    ]

    normalized = "|".join(
        part.strip().lower()
        for part in parts
    )

    return hashlib.sha256(
        normalized.encode("utf-8")
    ).hexdigest()

Fingerprint به‌تنهایی اثبات نمی‌کند دو فاکتور یکسان‌اند. اگر فیلدهای اصلی ناقص یا نامطمئن باشند، سند باید برای بررسی علامت‌گذاری شود.

تعیین شرایط بازبینی انسانی

سیستم باید به‌صورت خودکار requires_review را فعال کند اگر:

  • کیفیت تصویر پایین است.
  • شماره فاکتور خوانده نشده است.
  • تاریخ نامشخص است.
  • واحد پول مشخص نیست.
  • مبلغ نهایی ناموجود است.
  • جمع اقلام با جمع سند تفاوت دارد.
  • Confidence فیلد اصلی پایین است.
  • بیش از یک واحد پول در سند دیده می‌شود.
  • تعداد اقلام استخراج‌شده صفر است.
  • بخشی از تصویر بریده شده است.
  • سند چندصفحه‌ای ناقص است.

نمونه تابع:

def determine_review_reasons(
    invoice: InvoiceExtraction,
) -> list[str]:
    reasons: list[str] = []

    if invoice.image_quality == "poor":
        reasons.append(
            "کیفیت تصویر پایین است."
        )

    if not invoice.invoice_number.normalized:
        reasons.append(
            "شماره فاکتور استخراج نشده است."
        )

    if not invoice.issue_date.normalized:
        reasons.append(
            "تاریخ فاکتور استخراج نشده است."
        )

    if invoice.currency == "unknown":
        reasons.append(
            "واحد پول مشخص نیست."
        )

    if invoice.totals.payable_amount.value is None:
        reasons.append(
            "مبلغ نهایی استخراج نشده است."
        )

    if not invoice.items:
        reasons.append(
            "هیچ قلمی استخراج نشده است."
        )

    return reasons

آیا Confidence مدل قابل اعتماد است؟

مقدار Confidence تولیدشده توسط مدل یک سیگنال کمکی است، نه احتمال آماری تضمین‌شده.

برای تصمیم بازبینی باید چند عامل را ترکیب کنید:

  • Confidence اعلام‌شده
  • کیفیت تصویر
  • کامل‌بودن فیلدها
  • سازگاری محاسبات
  • تطابق با اطلاعات فروشنده
  • وجود شماره فاکتور
  • نتیجه پردازش تکراری با مدل یا روش دیگر
  • تجربه خطا در قالب مشابه

به‌جای قانون ساده زیر:

اگر Confidence بیشتر از 0.9 بود، ثبت کن.

از قاعده ترکیبی استفاده کنید:

ثبت خودکار فقط اگر:
- همه فیلدهای ضروری موجود باشند.
- واحد پول مشخص باشد.
- اختلاف محاسباتی وجود نداشته باشد.
- فروشنده در سیستم شناخته شده باشد.
- قالب سند قبلاً ارزیابی شده باشد.
- هیچ هشدار مهمی ثبت نشده باشد.

پیش‌پردازش تصویر برای افزایش دقت

در بعضی فاکتورها پیش‌پردازش ساده کیفیت استخراج را بهتر می‌کند.

اصلاح چرخش

تصویر باید در جهت صحیح قرار گیرد. متادیتای EXIF همیشه قابل اتکا نیست؛ در صورت نیاز می‌توان تشخیص زاویه را نیز اضافه کرد.

برش حاشیه‌ها

میز، دست، پس‌زمینه و اشیای اطراف باید تا حد امکان حذف شوند.

افزایش وضوح

بزرگ‌کردن بیش از حد تصویر اطلاعات جدیدی ایجاد نمی‌کند، اما کوچک‌کردن نامناسب می‌تواند نوشته‌ها را از بین ببرد.

بهبود Contrast

برای رسیدهای حرارتی کم‌رنگ می‌توان Contrast را افزایش داد:

from PIL import ImageEnhance


def increase_contrast(
    image,
    factor: float = 1.4,
):
    enhancer = ImageEnhance.Contrast(
        image
    )
    return enhancer.enhance(factor)

تبدیل به Grayscale

برای بعضی اسناد سیاه‌وسفید مفید است، اما مهرها، نشانه‌ها یا جدول‌های رنگی ممکن است اطلاعات مهمی داشته باشند. همیشه نسخه اصلی را نیز نگه دارید.

پردازش دسته‌ای فاکتورها

برای حجم بالا، پردازش را به Queue منتقل کنید.

ساختار پیشنهادی:

Upload API
    ↓
Object Storage
    ↓
Job Queue
    ↓
Extraction Worker
    ↓
Validation Worker
    ↓
Review Queue
    ↓
Accounting or ERP

وضعیت هر Job:

{
  "job_id": "job-1001",
  "status": "awaiting_review",
  "document_id": "doc-502",
  "created_at": "2026-07-20T10:30:00Z",
  "completed_at": "2026-07-20T10:30:07Z",
  "warnings_count": 2
}

وضعیت‌های مناسب:

  • queued
  • processing
  • extracted
  • awaiting_review
  • approved
  • rejected
  • failed

جلوگیری از پردازش دوباره

برای هر فایل می‌توان Hash محاسبه کرد:

import hashlib


def calculate_file_hash(
    file_bytes: bytes,
) -> str:
    return hashlib.sha256(
        file_bytes
    ).hexdigest()

اگر همان فایل دوباره آپلود شد، نتیجه قبلی بازیابی می‌شود. بااین‌حال، دو عکس متفاوت از یک فاکتور Hash متفاوت دارند؛ بنابراین Fingerprint محتوایی فاکتور نیز لازم است.

اتصال به نرم‌افزار حسابداری یا ERP

پس از تأیید خروجی، داده می‌تواند به سیستم مقصد ارسال شود.

رکورد پیشنهادی:

{
  "source_document_id": "doc-502",
  "invoice_number": "1258",
  "seller_id": "vendor-42",
  "issue_date_raw": "۱۴۰۵/۰۴/۲۹",
  "issue_date_normalized": "1405/04/29",
  "currency": "IRR",
  "subtotal": 3000000,
  "tax_amount": 300000,
  "payable_amount": 3300000,
  "items": [
    {
      "product_code": null,
      "description": "کالای الف",
      "quantity": 2,
      "unit_price": 1500000,
      "line_total": 3000000
    }
  ],
  "review": {
    "status": "approved",
    "approved_by": "USER_ID",
    "approved_at": "TIMESTAMP"
  }
}

ثبت نهایی فقط باید پس از این کنترل‌ها انجام شود:

  • فروشنده تطبیق داده شده باشد.
  • شماره فاکتور تکراری نباشد.
  • واحد پول مشخص باشد.
  • تاریخ معتبر باشد.
  • جمع‌ها بررسی شده باشند.
  • اقلام ضروری موجود باشند.
  • کاربر مجاز تأیید کرده باشد.

تطبیق فروشنده با اطلاعات موجود

نام فروشنده ممکن است در اسناد مختلف کمی متفاوت باشد:

شرکت داده‌پرداز نمونه
داده پرداز نمونه
شرکت داده پرداز نمونه (سهامی خاص)

برای تطبیق می‌توان از این ترتیب استفاده کرد:

  1. شناسه یکتای ثبت‌شده
  2. شماره تماس
  3. نام نرمال‌شده
  4. تطبیق تقریبی نام
  5. جست‌وجوی معنایی
  6. بررسی انسانی

AI می‌تواند گزینه‌های احتمالی را پیشنهاد دهد، اما اگر چند فروشنده مشابه وجود دارد نباید یکی را خودکار انتخاب کند.

ارزیابی سیستم استخراج فاکتور

برای ارزیابی، مجموعه‌ای از فاکتورهای متنوع آماده کنید:

  • فارسی و انگلیسی
  • ریال و تومان
  • چاپی و دست‌نویس
  • تک‌صفحه و چندصفحه
  • جدول منظم و نامنظم
  • عکس موبایل
  • اسکن باکیفیت
  • تصویر کم‌نور
  • رسید حرارتی
  • فاکتور دارای تخفیف
  • فاکتور دارای مالیات
  • چند واحد پول
  • فاکتور بدون شماره
  • فاکتور با اقلام چندخطی

معیارهای ارزیابی

دقت فیلدی

برای هر فیلد:

Field Accuracy =
Correct Fields / Total Evaluated Fields

دقت مبلغ نهایی

این فیلد اهمیت بیشتری از آدرس یا تلفن دارد و باید جداگانه اندازه‌گیری شود.

دقت اقلام

بررسی کنید:

  • چند ردیف درست شناسایی شده؟
  • شرح اقلام درست است؟
  • تعداد درست است؟
  • قیمت واحد درست است؟
  • مبلغ ردیف درست است؟

نرخ نیاز به اصلاح

چه درصدی از فاکتورها پس از استخراج به اصلاح انسانی نیاز دارند؟

زمان بازبینی

استخراج خودکار زمانی مفید است که زمان بازبینی از ورود کامل دستی کمتر باشد.

نرخ تشخیص اختلاف

سیستم چند اختلاف محاسباتی واقعی را پیدا کرده و چند هشدار اشتباه ساخته است؟

ساخت Dataset مرجع

نمونه:

{
  "document_id": "invoice-001",
  "expected": {
    "invoice_number": "1258",
    "issue_date_raw": "۱۴۰۵/۰۴/۲۹",
    "currency": "IRR",
    "seller_name": "شرکت نمونه",
    "items": [
      {
        "description": "کالای الف",
        "quantity": 2,
        "unit_price": 1500000,
        "line_total": 3000000
      }
    ],
    "tax_total": 300000,
    "payable_amount": 3300000
  }
}

هر بار که مدل، پرامپت یا پیش‌پردازش تغییر می‌کند، Dataset را دوباره اجرا کنید.

انتخاب مدل مناسب استخراج فاکتور

مدل مناسب باید این ویژگی‌ها را داشته باشد:

  • پشتیبانی از ورودی تصویر
  • درک مناسب متن فارسی
  • توانایی خواندن جدول
  • پیروی دقیق از Schema
  • تولید JSON معتبر
  • تشخیص ارقام فارسی و عربی
  • عملکرد مناسب در تصاویر واقعی
  • هزینه قابل قبول برای پردازش دسته‌ای

قوی‌ترین مدل همیشه اقتصادی‌ترین انتخاب نیست. می‌توانید:

  • مدل سریع‌تر را برای تشخیص نوع سند استفاده کنید.
  • مدل Vision اصلی را برای استخراج فاکتور به کار بگیرید.
  • فقط اسناد نامطمئن را به مدل قوی‌تر ارسال کنید.
  • محاسبات و اعتبارسنجی را در کد انجام دهید.

مدل‌های موجود و اطلاعات به‌روز را در صفحه مدل‌های درواره بررسی کنید.

اشتباهات رایج

تبدیل مستقیم تصویر به متن ساده

برای استفاده نرم‌افزاری به خروجی ساختاریافته و Schema نیاز دارید.

حذف مقدار خام

همیشه مقدار چاپ‌شده را در کنار مقدار نرمال‌شده حفظ کنید.

تبدیل خودکار ریال و تومان

اگر واحد صریح نیست، آن را unknown قرار دهید.

اعتماد به محاسبات مدل

مجموع و اختلاف‌ها را با Decimal در Backend محاسبه کنید.

ثبت مستقیم در حسابداری

در نسخه اولیه، خروجی باید وارد صف بازبینی شود.

نادیده‌گرفتن فاکتور چندصفحه‌ای

ممکن است اقلام در صفحه دوم ادامه داشته باشند و مبلغ نهایی فقط در آخرین صفحه ظاهر شود.

استفاده از float برای پول

برای محاسبات دقیق از Decimal استفاده کنید.

نداشتن Dataset واقعی

چند تصویر تمیز برای ارزیابی کافی نیست. سیستم باید روی عکس‌های واقعی موبایل و قالب‌های مختلف آزمایش شود.

فرض‌کردن Confidence به‌عنوان تضمین

Confidence مدل فقط یک سیگنال است و باید با قواعد دیگر ترکیب شود.

نقشه راه پیاده‌سازی

مرحله اول: نمونه محلی

  • یک تصویر فاکتور دریافت کنید.
  • JSON استخراج کنید.
  • خروجی را دستی بررسی کنید.

مرحله دوم: Schema و اعتبارسنجی

  • Pydantic اضافه کنید.
  • اعداد را نرمال کنید.
  • جمع‌ها را با کد بررسی کنید.

مرحله سوم: رابط بازبینی

  • تصویر و فیلدها کنار یکدیگر نمایش داده شوند.
  • کاربر بتواند هر مقدار را اصلاح کند.
  • فیلدهای نامطمئن برجسته شوند.

مرحله چهارم: پردازش دسته‌ای

  • Queue و Worker اضافه کنید.
  • وضعیت Job ذخیره شود.
  • خطاها مستقل مدیریت شوند.

مرحله پنجم: اتصال به سیستم مقصد

  • فقط اسناد تأییدشده ارسال شوند.
  • رکورد تکراری شناسایی شود.
  • شناسه سند منبع ذخیره شود.

مرحله ششم: ارزیابی مستمر

  • اصلاحات انسانی به Dataset افزوده شوند.
  • خطاها بر اساس قالب و فیلد تحلیل شوند.
  • تغییر مدل قبل از انتشار آزمایش شود.

چک‌لیست Production

  • نوع فایل و اندازه آن کنترل می‌شود.
  • جهت تصویر اصلاح می‌شود.
  • مقدار خام هر فیلد حفظ می‌شود.
  • ارقام فارسی و عربی نرمال می‌شوند.
  • ریال و تومان حدس زده نمی‌شوند.
  • تاریخ خام و نوع تقویم ثبت می‌شود.
  • خروجی با Schema بررسی می‌شود.
  • مبالغ با Decimal محاسبه می‌شوند.
  • جمع اقلام با جمع فاکتور مقایسه می‌شود.
  • فیلدهای نامطمئن مشخص می‌شوند.
  • اسناد مشکل‌دار وارد صف بازبینی می‌شوند.
  • فایل و فاکتور تکراری بررسی می‌شود.
  • نسخه مدل و پرامپت ثبت می‌شود.
  • API Key فقط در Backend نگهداری می‌شود.
  • ثبت نهایی به تأیید وابسته است.
  • Dataset ارزیابی واقعی وجود دارد.

پرسش‌های متداول

آیا هوش مصنوعی می‌تواند فاکتور فارسی را بخواند؟

بله، مدل‌های چندوجهی می‌توانند متن، ارقام و جدول فاکتور فارسی را تحلیل کنند. کیفیت نتیجه به مدل، تصویر، قالب سند و طراحی پرامپت بستگی دارد.

چگونه فاکتور را به Excel تبدیل کنیم؟

ابتدا اطلاعات فاکتور را به JSON ساختاریافته تبدیل کنید. سپس اقلام را با Python به CSV یا Excel صادر کنید. بهتر است جدول فاکتورها و جدول اقلام جدا باشند.

تفاوت OCR با Invoice AI چیست؟

OCR متن تصویر را استخراج می‌کند، اما Invoice AI تلاش می‌کند نقش هر مقدار را نیز تشخیص دهد؛ برای مثال شماره فاکتور، قیمت واحد، مالیات و مبلغ نهایی.

آیا می‌توان PDF فاکتور را پردازش کرد؟

بله. می‌توانید هر صفحه PDF را به تصویر تبدیل و پردازش کنید. برای اسناد چندصفحه‌ای باید صفحات متعلق به یک فاکتور گروه‌بندی شوند.

آیا مبالغ استخراج‌شده قابل اعتمادند؟

هیچ خروجی مدل نباید بدون اعتبارسنجی پذیرفته شود. جمع اقلام، مالیات و مبلغ نهایی را در Backend بررسی و موارد نامطمئن را برای بازبینی علامت‌گذاری کنید.

چگونه ریال و تومان را تشخیص دهیم؟

فقط از عبارت صریح روی سند، قالب تأییدشده فروشنده یا اطلاعات معتبر سیستم استفاده کنید. اگر واحد مشخص نیست، آن را unknown قرار دهید.

آیا می‌توان فاکتور را مستقیماً در نرم‌افزار حسابداری ثبت کرد؟

از نظر فنی ممکن است، اما بهتر است ابتدا خروجی اعتبارسنجی و توسط کاربر تأیید شود. پس از رسیدن سیستم به دقت کافی، می‌توان ثبت خودکار محدود را برای قالب‌های ارزیابی‌شده فعال کرد.

چه مدلی برای Invoice OCR مناسب است؟

مدل باید از ورودی تصویر، درک فارسی و خروجی ساختاریافته پشتیبانی کند. مدل‌های قابل استفاده را در صفحه مدل‌های درواره بررسی کنید.

API درواره چگونه به این سیستم متصل می‌شود؟

با تنظیم base_url روی https://api.darvareh.ir/v1 و استفاده از API Key می‌توانید تصویر و پرامپت استخراج را از Backend به مدل Vision ارسال کنید.

جمع‌بندی

استخراج اطلاعات فاکتور با هوش مصنوعی زمانی ارزش عملی پیدا می‌کند که خروجی از متن پراکنده به داده ساختاریافته، اعتبارسنجی‌شده و قابل ثبت تبدیل شود.

یک سیستم قابل اعتماد فقط به OCR یا مدل Vision متکی نیست. پیش‌پردازش تصویر، Schema دقیق، نگهداری مقدار خام، نرمال‌سازی ارقام، کنترل واحد پول، بررسی محاسبات، تشخیص فاکتور تکراری و بازبینی انسانی اجزای ضروری آن هستند.

در پروژه این مقاله یک API عملی با Python، FastAPI، Pydantic، Pillow و API درواره ساختیم. این API تصویر فاکتور را دریافت می‌کند، اطلاعات آن را در قالب JSON استخراج می‌کند و اختلاف‌های محاسباتی را گزارش می‌دهد.

برای شروع، در درواره ثبت‌نام و API Key دریافت کنید. سپس یک مدل دارای قابلیت Vision را از صفحه مدل‌های درواره انتخاب کرده و سیستم را ابتدا با مجموعه‌ای از فاکتورهای آزمایشی اجرا کنید.

مقالات مرتبط

برای مطالعه شرایط استفاده و محدودیت‌های مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.

Read more