Update: 2026-05-04 16:06:15
This commit is contained in:
@@ -4,6 +4,7 @@ namespace App\Core;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gemini AI Integration for Invoice Extraction
|
* Gemini AI Integration for Invoice Extraction
|
||||||
|
* Optimized for Jordan UBL 2.1 Compliance
|
||||||
*/
|
*/
|
||||||
class AI
|
class AI
|
||||||
{
|
{
|
||||||
@@ -20,54 +21,44 @@ class AI
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$prompt = "أنت نظام متخصص في استخلاص بيانات الفواتير التجارية. مهمتك واحدة فقط: استخراج البيانات من الفاتورة المرفقة بدقة تامة.
|
$prompt = "أنت نظام خبير في استخراج البيانات الضريبية للفواتير في الأردن.
|
||||||
|
يجب أن تلتزم بالقواعد التالية بصرامة حسابية مطلقة.
|
||||||
|
|
||||||
## قواعد صارمة:
|
### 1. القواعد الحسابية الصارمة (إلزامي):
|
||||||
**اللغة:**
|
يجب أن توازن الفاتورة حسابياً قبل إرجاع النتيجة. المعادلة الأساسية هي:
|
||||||
- إذا كانت الفاتورة بالعربية: أبقِ جميع أسماء السلع والعناوين بالعربية بدون ترجمة
|
`Grand Total = Subtotal - Discount Total + Tax Amount`
|
||||||
- إذا كانت بالإنجليزية: أبقِها بالإنجليزية بدون ترجمة
|
|
||||||
- الأرقام دائماً بالأرقام اللاتينية (0-9) بغض النظر عن لغة الفاتورة
|
|
||||||
- المبالغ بـ 3 أرقام عشرية (مثال: 15.000 وليس 15)
|
|
||||||
|
|
||||||
**الدقة:**
|
**مثال للتوضيح:**
|
||||||
- لا تخترع أي بيانات غير موجودة في الفاتورة — أعد null إذا لم تجد المعلومة
|
إذا كانت البنود هي:
|
||||||
- تحقق رياضياً: subtotal = مجموع (quantity × unit_price - discount) لكل سطر
|
1. صنف أ: 12.000 (1 × 12.000)
|
||||||
- تحقق: grand_total = subtotal - discount_total + tax_amount
|
2. صنف ب: 175.000 (35 × 5.000)
|
||||||
- إذا وجدت تناقضاً بين الأرقام في الفاتورة، سجِّله في حقل \"validation_warnings\"
|
فإن المجموع الفرعي (Subtotal) هو 187.000.
|
||||||
|
إذا كانت الضريبة 16% على صنف أ فقط، فإن Tax Amount = 1.920.
|
||||||
|
إذاً الإجمالي (Grand Total) يجب أن يكون 188.920.
|
||||||
|
|
||||||
**الضريبة:**
|
**تنبيه:** إذا وجدت في الفاتورة رقماً مكتوباً كإجمالي (Grand Total) ولكنه لا يطابق مجموع البنود والضريبة، قم بتصحيح البيانات المستخرجة للبنود لتتوافق مع المجموع الصحيح أو اتبع المجموع الرياضي الأدق. لا تخرج إجمالياً (مثلاً 15.000) بينما مجموع البنود (311.000).
|
||||||
- في الأردن: ضريبة المبيعات العامة (GST) = 16% للسلع العامة
|
|
||||||
- سلع معفاة من الضريبة: المواد الغذائية الأساسية، الأدوية، الكتب، بعض المعدات الطبية
|
|
||||||
- سلع بضريبة مخفضة: قد تكون 4% أو 8% — استخرج النسبة الفعلية من الفاتورة
|
|
||||||
- لكل سطر: حدد tax_rate الفعلي (0 للمعفاة، وإلا النسبة المئوية كعدد عشري مثل 0.16)
|
|
||||||
|
|
||||||
## البيانات المطلوبة (JSON فقط، بدون أي نص إضافي):
|
### 2. قواعد استخراج البيانات:
|
||||||
|
- **اللغة:** لا تترجم. إذا كان الوصف 'صنف أول' أبقه 'صنف أول'.
|
||||||
|
- **الأرقام:** استخدم الأرقام اللاتينية (0-9).
|
||||||
|
- **الدقة:** استخدم 3 أرقام عشرية للمبالغ (مثال: 0.500).
|
||||||
|
- **الضريبة:** في الأردن، الضريبة العامة هي 16% (0.160). حدد لكل بند النسبة الفعلية.
|
||||||
|
|
||||||
```json
|
### 3. هيكل البيانات (JSON فقط):
|
||||||
{
|
{
|
||||||
\"invoice_number\": \"string | null\",
|
\"invoice_number\": \"string\",
|
||||||
\"invoice_date\": \"YYYY-MM-DD | null\",
|
\"invoice_date\": \"YYYY-MM-DD\",
|
||||||
\"invoice_type\": \"cash | credit\",
|
\"invoice_type\": \"cash | credit\",
|
||||||
\"payment_method_code\": \"013 | 010 | 001\",
|
\"invoice_category\": \"simplified | standard\",
|
||||||
\"supplier\": {
|
\"supplier\": { \"name\": \"string\", \"tin\": \"string\", \"address\": \"string\" },
|
||||||
\"name\": \"string | null\",
|
\"buyer\": { \"name\": \"string\", \"tin\": \"string\", \"national_id\": \"string\" },
|
||||||
\"tin\": \"string | null\",
|
|
||||||
\"address\": \"string | null\"
|
|
||||||
},
|
|
||||||
\"buyer\": {
|
|
||||||
\"name\": \"string | null\",
|
|
||||||
\"tin\": \"string | null\",
|
|
||||||
\"national_id\": \"string | null\"
|
|
||||||
},
|
|
||||||
\"lines\": [
|
\"lines\": [
|
||||||
{
|
{
|
||||||
\"line_number\": 1,
|
\"line_number\": 1,
|
||||||
\"description\": \"string\",
|
\"description\": \"string\",
|
||||||
\"quantity\": 0.000,
|
\"quantity\": 0.000,
|
||||||
\"unit_price\": 0.000,
|
\"unit_price\": 0.000,
|
||||||
\"discount\": 0.000,
|
\"tax_rate\": 0.160,
|
||||||
\"tax_rate\": 0.16,
|
|
||||||
\"tax_exempt_reason\": \"string | null\",
|
|
||||||
\"line_total\": 0.000
|
\"line_total\": 0.000
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -75,14 +66,10 @@ class AI
|
|||||||
\"discount_total\": 0.000,
|
\"discount_total\": 0.000,
|
||||||
\"tax_amount\": 0.000,
|
\"tax_amount\": 0.000,
|
||||||
\"grand_total\": 0.000,
|
\"grand_total\": 0.000,
|
||||||
\"currency_code\": \"JOD\",
|
\"currency_code\": \"JOD\"
|
||||||
\"math_verified\": true,
|
|
||||||
\"validation_warnings\": [],
|
|
||||||
\"ai_confidence\": 0.95
|
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
أعد JSON فقط بدون أي شرح أو مقدمة أو علامات Markdown.";
|
أعد كود JSON فقط بدون أي علامات Markdown أو نصوص إضافية.";
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
"contents" => [
|
"contents" => [
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ if (preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) {
|
|||||||
|
|
||||||
if (!$token) outputErrorImage('Forbidden: No token');
|
if (!$token) outputErrorImage('Forbidden: No token');
|
||||||
|
|
||||||
$decoded = \App\Core\JWT::decode($token);
|
$decoded = \App\Core\JWT::decode($token, env('JWT_SECRET', ''));
|
||||||
if (!$decoded) outputErrorImage('Forbidden: Invalid token');
|
if (!$decoded) outputErrorImage('Forbidden: Invalid token');
|
||||||
|
|
||||||
$db = Database::getInstance();
|
$db = Database::getInstance();
|
||||||
|
|||||||
@@ -59,17 +59,18 @@ try {
|
|||||||
|
|
||||||
$invoices = $stmt->fetchAll();
|
$invoices = $stmt->fetchAll();
|
||||||
|
|
||||||
// 3. Decrypt sensitive fields for display
|
// 3. Decrypt sensitive fields for display (Robustly)
|
||||||
|
$decrypt = fn($val) => Encryption::decrypt($val ?? '') ?: ($val ?? '-');
|
||||||
foreach ($invoices as &$inv) {
|
foreach ($invoices as &$inv) {
|
||||||
$inv['supplier_name'] = Encryption::decrypt($inv['supplier_name'] ?? '') ?: ($inv['supplier_name'] ?? '-');
|
$inv['supplier_name'] = $decrypt($inv['supplier_name']);
|
||||||
$inv['supplier_tin'] = Encryption::decrypt($inv['supplier_tin'] ?? '') ?: ($inv['supplier_tin'] ?? '-');
|
$inv['supplier_tin'] = $decrypt($inv['supplier_tin']);
|
||||||
$inv['buyer_name'] = Encryption::decrypt($inv['buyer_name'] ?? '') ?: ($inv['buyer_name'] ?? '-');
|
$inv['buyer_name'] = $decrypt($inv['buyer_name']);
|
||||||
|
|
||||||
if (!empty($inv['company_name'])) {
|
if (!empty($inv['company_name'])) {
|
||||||
$inv['company_name'] = Encryption::decrypt($inv['company_name']) ?: $inv['company_name'];
|
$inv['company_name'] = $decrypt($inv['company_name']);
|
||||||
}
|
}
|
||||||
if (!empty($inv['tenant_name'])) {
|
if (!empty($inv['tenant_name'])) {
|
||||||
$inv['tenant_name'] = Encryption::decrypt($inv['tenant_name']) ?: $inv['tenant_name'];
|
$inv['tenant_name'] = $decrypt($inv['tenant_name']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,16 +43,18 @@ try {
|
|||||||
$stmtLines->execute([$id]);
|
$stmtLines->execute([$id]);
|
||||||
$invoice['items'] = $stmtLines->fetchAll();
|
$invoice['items'] = $stmtLines->fetchAll();
|
||||||
|
|
||||||
// 5. Decrypt Fields
|
// 5. Decrypt Fields (Robustly)
|
||||||
$invoice['supplier_tin'] = Encryption::decrypt($invoice['supplier_tin'] ?? '') ?: $invoice['supplier_tin'];
|
$decrypt = fn($val) => Encryption::decrypt($val ?? '') ?: $val;
|
||||||
$invoice['supplier_name'] = Encryption::decrypt($invoice['supplier_name'] ?? '') ?: $invoice['supplier_name'];
|
|
||||||
$invoice['supplier_address'] = Encryption::decrypt($invoice['supplier_address'] ?? '') ?: $invoice['supplier_address'];
|
$invoice['supplier_tin'] = $decrypt($invoice['supplier_tin']);
|
||||||
$invoice['buyer_tin'] = Encryption::decrypt($invoice['buyer_tin'] ?? '') ?: $invoice['buyer_tin'];
|
$invoice['supplier_name'] = $decrypt($invoice['supplier_name']);
|
||||||
$invoice['buyer_name'] = Encryption::decrypt($invoice['buyer_name'] ?? '') ?: $invoice['buyer_name'];
|
$invoice['supplier_address'] = $decrypt($invoice['supplier_address']);
|
||||||
$invoice['buyer_national_id'] = Encryption::decrypt($invoice['buyer_national_id'] ?? '') ?: $invoice['buyer_national_id'];
|
$invoice['buyer_tin'] = $decrypt($invoice['buyer_tin']);
|
||||||
|
$invoice['buyer_name'] = $decrypt($invoice['buyer_name']);
|
||||||
|
$invoice['buyer_national_id'] = $decrypt($invoice['buyer_national_id']);
|
||||||
|
|
||||||
if (!empty($invoice['company_name'])) {
|
if (!empty($invoice['company_name'])) {
|
||||||
$invoice['company_name'] = Encryption::decrypt($invoice['company_name']) ?: $invoice['company_name'];
|
$invoice['company_name'] = $decrypt($invoice['company_name']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Generate Public URL for File (Assuming storage is symlinked or served)
|
// 6. Generate Public URL for File (Assuming storage is symlinked or served)
|
||||||
|
|||||||
@@ -328,8 +328,11 @@
|
|||||||
|
|
||||||
<div class="p-6 bg-gray-950/50 border-t border-gray-800 flex gap-4">
|
<div class="p-6 bg-gray-950/50 border-t border-gray-800 flex gap-4">
|
||||||
<template x-if="currentInvoice?.status === 'extracted'">
|
<template x-if="currentInvoice?.status === 'extracted'">
|
||||||
<button @click="approveInvoice(currentInvoice.id)" class="flex-1 bg-emerald-600 hover:bg-emerald-500 py-3 rounded-xl font-bold transition flex items-center justify-center gap-2">
|
<button @click="approveInvoice(currentInvoice.id)"
|
||||||
<span>✅ اعتماد الفاتورة وتوليد QR</span>
|
class="flex-1 bg-emerald-600 hover:bg-emerald-500 py-3 rounded-xl font-bold transition flex items-center justify-center gap-2 disabled:opacity-50"
|
||||||
|
:disabled="isApproving">
|
||||||
|
<span x-show="!isApproving">✅ اعتماد الفاتورة وتوليد QR</span>
|
||||||
|
<span x-show="isApproving">جارِ الإرسال إلى جوفوترة... ⏳</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="currentInvoice?.status === 'approved'">
|
<template x-if="currentInvoice?.status === 'approved'">
|
||||||
@@ -417,6 +420,7 @@
|
|||||||
showUploadModal: false,
|
showUploadModal: false,
|
||||||
showViewModal: false,
|
showViewModal: false,
|
||||||
isUploading: false,
|
isUploading: false,
|
||||||
|
isApproving: false,
|
||||||
globalError: '',
|
globalError: '',
|
||||||
|
|
||||||
newUser: { name: '', email: '', password: '', role: 'employee', tenant_id: '' },
|
newUser: { name: '', email: '', password: '', role: 'employee', tenant_id: '' },
|
||||||
@@ -470,33 +474,39 @@
|
|||||||
|
|
||||||
async approveInvoice(id) {
|
async approveInvoice(id) {
|
||||||
if (!confirm('هل أنت متأكد من اعتماد الفاتورة وإرسالها إلى جوفوترة؟')) return;
|
if (!confirm('هل أنت متأكد من اعتماد الفاتورة وإرسالها إلى جوفوترة؟')) return;
|
||||||
|
this.isApproving = true;
|
||||||
const res = await fetch('/index.php?route=v1/invoices/approve', {
|
try {
|
||||||
method: 'POST',
|
const res = await fetch('/index.php?route=v1/invoices/approve', {
|
||||||
headers: {
|
method: 'POST',
|
||||||
'Authorization': 'Bearer ' + this.token(),
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Authorization': 'Bearer ' + this.token(),
|
||||||
},
|
'Content-Type': 'application/json'
|
||||||
body: JSON.stringify({ id: id })
|
},
|
||||||
});
|
body: JSON.stringify({ id: id })
|
||||||
const json = await res.json();
|
});
|
||||||
|
const json = await res.json();
|
||||||
if (json.success) {
|
|
||||||
alert('تم الاعتماد بنجاح!');
|
if (json.success) {
|
||||||
this.viewInvoice(id); // Reload to show QR
|
alert('✅ تم الاعتماد بنجاح!');
|
||||||
this.loadInvoices();
|
this.viewInvoice(id); // Reload to show QR
|
||||||
} else {
|
this.loadInvoices();
|
||||||
this.showError(json.message);
|
} else {
|
||||||
|
this.showError(json.message || 'فشل الاتصال بنظام جوفوترة');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.showError('خطأ غير متوقع: ' + e.message);
|
||||||
|
} finally {
|
||||||
|
this.isApproving = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
generateQRPng(base64Tlv) {
|
generateQRPng(base64Tlv) {
|
||||||
|
if (!base64Tlv) return '';
|
||||||
const qr = new QRious({
|
const qr = new QRious({
|
||||||
value: base64Tlv,
|
value: base64Tlv,
|
||||||
size: 200,
|
size: 200,
|
||||||
level: 'M'
|
level: 'M'
|
||||||
});
|
});
|
||||||
// Remove 'data:image/png;base64,' from the return, as the template adds it
|
|
||||||
return qr.toDataURL().split(',')[1];
|
return qr.toDataURL().split(',')[1];
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -519,7 +529,7 @@
|
|||||||
if (json.success) {
|
if (json.success) {
|
||||||
this.showUploadModal = false;
|
this.showUploadModal = false;
|
||||||
this.loadInvoices();
|
this.loadInvoices();
|
||||||
this.viewInvoice(json.data.id); // Open view modal immediately!
|
this.viewInvoice(json.data.id);
|
||||||
} else {
|
} else {
|
||||||
this.showError(json.message);
|
this.showError(json.message);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user