Update: 2026-05-08 01:15:44
This commit is contained in:
133
app/modules_app/invoices/export.php
Normal file
133
app/modules_app/invoices/export.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
/**
|
||||
* Export Invoices as CSV (Excel-compatible)
|
||||
* GET /v1/invoices/export
|
||||
* Downloads a CSV file with invoice data + line items
|
||||
*/
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Core\Encryption;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
|
||||
$decoded = AuthMiddleware::check();
|
||||
$db = Database::getInstance();
|
||||
|
||||
$tenantId = $decoded['tenant_id'];
|
||||
$role = $decoded['role'];
|
||||
$companyId = $_GET['company_id'] ?? null;
|
||||
$dateFrom = $_GET['date_from'] ?? null;
|
||||
$dateTo = $_GET['date_to'] ?? null;
|
||||
$status = $_GET['status'] ?? null;
|
||||
|
||||
// Build query with filters
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($role !== 'super_admin') {
|
||||
$where[] = 'i.tenant_id = ?';
|
||||
$params[] = $tenantId;
|
||||
}
|
||||
|
||||
if ($companyId) {
|
||||
$where[] = 'i.company_id = ?';
|
||||
$params[] = $companyId;
|
||||
}
|
||||
|
||||
if ($dateFrom) {
|
||||
$where[] = 'i.invoice_date >= ?';
|
||||
$params[] = $dateFrom;
|
||||
}
|
||||
|
||||
if ($dateTo) {
|
||||
$where[] = 'i.invoice_date <= ?';
|
||||
$params[] = $dateTo;
|
||||
}
|
||||
|
||||
if ($status) {
|
||||
$where[] = 'i.status = ?';
|
||||
$params[] = $status;
|
||||
}
|
||||
|
||||
$whereClause = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
|
||||
$stmt = $db->prepare("
|
||||
SELECT i.*, c.name as company_name_raw
|
||||
FROM invoices i
|
||||
JOIN companies c ON i.company_id = c.id
|
||||
$whereClause
|
||||
ORDER BY i.invoice_date DESC
|
||||
LIMIT 5000
|
||||
");
|
||||
$stmt->execute($params);
|
||||
$invoices = $stmt->fetchAll();
|
||||
|
||||
// Decrypt helper
|
||||
$dec = function($val) {
|
||||
if (empty($val)) return '';
|
||||
$result = Encryption::decrypt((string)$val);
|
||||
return ($result !== false && $result !== null) ? $result : (string)$val;
|
||||
};
|
||||
|
||||
// UTF-8 BOM for Excel compatibility
|
||||
$output = "\xEF\xBB\xBF";
|
||||
|
||||
// CSV headers
|
||||
$output .= implode(',', [
|
||||
'رقم الفاتورة',
|
||||
'تاريخ الفاتورة',
|
||||
'الشركة',
|
||||
'اسم المورّد',
|
||||
'الرقم الضريبي للمورّد',
|
||||
'عنوان المورّد',
|
||||
'اسم العميل',
|
||||
'الرقم الضريبي للعميل',
|
||||
'نوع الفاتورة',
|
||||
'المبلغ قبل الضريبة',
|
||||
'قيمة الخصم',
|
||||
'قيمة الضريبة',
|
||||
'الإجمالي',
|
||||
'العملة',
|
||||
'الحالة',
|
||||
'JoFotara UUID',
|
||||
'تاريخ الإنشاء',
|
||||
]) . "\n";
|
||||
|
||||
foreach ($invoices as $inv) {
|
||||
$statusAr = match($inv['status']) {
|
||||
'extracted' => 'مستخرجة',
|
||||
'approved' => 'معتمدة',
|
||||
'submitted' => 'مقدمة لجوفتورة',
|
||||
'rejected' => 'مرفوضة',
|
||||
default => $inv['status']
|
||||
};
|
||||
|
||||
$row = [
|
||||
'"' . str_replace('"', '""', $inv['invoice_number'] ?? '') . '"',
|
||||
$inv['invoice_date'] ?? '',
|
||||
'"' . str_replace('"', '""', $dec($inv['company_name_raw'] ?? '')) . '"',
|
||||
'"' . str_replace('"', '""', $dec($inv['supplier_name'])) . '"',
|
||||
'"' . $dec($inv['supplier_tin']) . '"',
|
||||
'"' . str_replace('"', '""', $dec($inv['supplier_address'])) . '"',
|
||||
'"' . str_replace('"', '""', $dec($inv['buyer_name'])) . '"',
|
||||
'"' . $dec($inv['buyer_tin']) . '"',
|
||||
$inv['invoice_type'] ?? 'cash',
|
||||
$inv['subtotal'] ?? '0',
|
||||
$inv['discount_total'] ?? '0',
|
||||
$inv['tax_amount'] ?? '0',
|
||||
$inv['grand_total'] ?? '0',
|
||||
$inv['currency_code'] ?? 'JOD',
|
||||
$statusAr,
|
||||
$inv['jofotara_uuid'] ?? '',
|
||||
$inv['created_at'] ?? '',
|
||||
];
|
||||
|
||||
$output .= implode(',', $row) . "\n";
|
||||
}
|
||||
|
||||
// Send as download
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="musadaq_invoices_' . date('Y-m-d') . '.csv"');
|
||||
header('Cache-Control: no-cache');
|
||||
|
||||
echo $output;
|
||||
exit;
|
||||
116
app/modules_app/invoices/update.php
Normal file
116
app/modules_app/invoices/update.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
/**
|
||||
* Update Invoice (Before Approval Only)
|
||||
* POST /v1/invoices/update
|
||||
* Allows editing extracted data before final approval.
|
||||
*/
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Core\Encryption;
|
||||
use App\Core\AuditLogger;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
|
||||
$decoded = AuthMiddleware::check();
|
||||
$data = input();
|
||||
|
||||
$id = $data['id'] ?? null;
|
||||
if (!$id) json_error('معرّف الفاتورة مطلوب', 422);
|
||||
|
||||
$db = Database::getInstance();
|
||||
$tenantId = $decoded['tenant_id'];
|
||||
$role = $decoded['role'];
|
||||
|
||||
// 1. Fetch & verify access
|
||||
$query = $role === 'super_admin'
|
||||
? "SELECT * FROM invoices WHERE id = ?"
|
||||
: "SELECT * FROM invoices WHERE id = ? AND tenant_id = ?";
|
||||
$params = $role === 'super_admin' ? [$id] : [$id, $tenantId];
|
||||
$stmt = $db->prepare($query);
|
||||
$stmt->execute($params);
|
||||
$invoice = $stmt->fetch();
|
||||
|
||||
if (!$invoice) json_error('الفاتورة غير موجودة', 404);
|
||||
|
||||
// 2. Only allow editing extracted (not yet approved) invoices
|
||||
if (!in_array($invoice['status'], ['extracted', 'pending'])) {
|
||||
json_error('لا يمكن تعديل الفاتورة بعد اعتمادها', 403);
|
||||
}
|
||||
|
||||
$db->beginTransaction();
|
||||
try {
|
||||
// 3. Update main invoice fields
|
||||
$fields = [];
|
||||
$values = [];
|
||||
|
||||
$plainFields = ['invoice_number', 'invoice_date', 'invoice_type', 'invoice_category',
|
||||
'subtotal', 'tax_amount', 'discount_total', 'grand_total', 'currency_code'];
|
||||
|
||||
foreach ($plainFields as $f) {
|
||||
if (isset($data[$f])) {
|
||||
$fields[] = "$f = ?";
|
||||
$values[] = $data[$f];
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypted fields
|
||||
$encryptedFields = [
|
||||
'supplier_name' => 'supplier_name',
|
||||
'supplier_tin' => 'supplier_tin',
|
||||
'supplier_address' => 'supplier_address',
|
||||
'buyer_name' => 'buyer_name',
|
||||
'buyer_tin' => 'buyer_tin',
|
||||
'buyer_national_id' => 'buyer_national_id',
|
||||
];
|
||||
|
||||
foreach ($encryptedFields as $key => $column) {
|
||||
if (isset($data[$key])) {
|
||||
$fields[] = "$column = ?";
|
||||
$values[] = !empty($data[$key]) ? Encryption::encrypt($data[$key]) : '';
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($fields)) {
|
||||
$fields[] = 'updated_at = NOW()';
|
||||
$values[] = $id;
|
||||
$sql = "UPDATE invoices SET " . implode(', ', $fields) . " WHERE id = ?";
|
||||
$db->prepare($sql)->execute($values);
|
||||
}
|
||||
|
||||
// 4. Update line items (if provided)
|
||||
if (isset($data['items']) && is_array($data['items'])) {
|
||||
// Delete old lines
|
||||
$db->prepare("DELETE FROM invoice_lines WHERE invoice_id = ?")->execute([$id]);
|
||||
|
||||
// Insert new lines
|
||||
$lineStmt = $db->prepare(
|
||||
"INSERT INTO invoice_lines (id, invoice_id, line_number, description, quantity, unit_price, tax_rate, line_total)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
|
||||
foreach ($data['items'] as $idx => $item) {
|
||||
$lineStmt->execute([
|
||||
Database::generateUuid(),
|
||||
$id,
|
||||
$item['line_number'] ?? ($idx + 1),
|
||||
$item['description'] ?? '',
|
||||
$item['quantity'] ?? 1,
|
||||
$item['unit_price'] ?? 0,
|
||||
$item['tax_rate'] ?? 0,
|
||||
$item['line_total'] ?? 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
|
||||
AuditLogger::log('invoice.updated', 'invoice', $id, null, [
|
||||
'fields_updated' => array_keys($data),
|
||||
], $decoded);
|
||||
|
||||
json_success(null, 'تم تحديث بيانات الفاتورة بنجاح');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$db->rollBack();
|
||||
error_log("Invoice Update Error: " . $e->getMessage());
|
||||
json_error('فشل تحديث الفاتورة: ' . $e->getMessage(), 500);
|
||||
}
|
||||
154
app/modules_app/reports/tax_summary.php
Normal file
154
app/modules_app/reports/tax_summary.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
/**
|
||||
* Monthly Tax Report API
|
||||
* GET /v1/reports/tax-summary
|
||||
* Returns monthly summary of tax, revenue, and invoice statistics
|
||||
*/
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
|
||||
$decoded = AuthMiddleware::check();
|
||||
$db = Database::getInstance();
|
||||
|
||||
$tenantId = $decoded['tenant_id'];
|
||||
$role = $decoded['role'];
|
||||
$companyId = $_GET['company_id'] ?? null;
|
||||
$month = $_GET['month'] ?? date('m');
|
||||
$year = $_GET['year'] ?? date('Y');
|
||||
|
||||
$where = ["MONTH(i.invoice_date) = ? AND YEAR(i.invoice_date) = ?"];
|
||||
$params = [$month, $year];
|
||||
|
||||
if ($role !== 'super_admin') {
|
||||
$where[] = 'i.tenant_id = ?';
|
||||
$params[] = $tenantId;
|
||||
}
|
||||
|
||||
if ($companyId) {
|
||||
$where[] = 'i.company_id = ?';
|
||||
$params[] = $companyId;
|
||||
}
|
||||
|
||||
$whereClause = 'WHERE ' . implode(' AND ', $where);
|
||||
|
||||
// 1. Main aggregation
|
||||
$stmt = $db->prepare("
|
||||
SELECT
|
||||
COUNT(*) as total_invoices,
|
||||
SUM(CASE WHEN status = 'approved' OR status = 'submitted' THEN 1 ELSE 0 END) as approved_count,
|
||||
SUM(CASE WHEN status = 'extracted' THEN 1 ELSE 0 END) as pending_count,
|
||||
SUM(CASE WHEN status = 'submitted' THEN 1 ELSE 0 END) as submitted_count,
|
||||
COALESCE(SUM(subtotal), 0) as total_subtotal,
|
||||
COALESCE(SUM(tax_amount), 0) as total_tax,
|
||||
COALESCE(SUM(discount_total), 0) as total_discount,
|
||||
COALESCE(SUM(grand_total), 0) as total_grand,
|
||||
COALESCE(AVG(grand_total), 0) as avg_invoice_amount,
|
||||
COALESCE(MAX(grand_total), 0) as max_invoice_amount,
|
||||
COALESCE(MIN(grand_total), 0) as min_invoice_amount
|
||||
FROM invoices i
|
||||
$whereClause
|
||||
");
|
||||
$stmt->execute($params);
|
||||
$summary = $stmt->fetch();
|
||||
|
||||
// 2. Daily breakdown for chart
|
||||
$stmtDaily = $db->prepare("
|
||||
SELECT
|
||||
DAY(i.invoice_date) as day_num,
|
||||
COUNT(*) as count,
|
||||
COALESCE(SUM(grand_total), 0) as daily_total,
|
||||
COALESCE(SUM(tax_amount), 0) as daily_tax
|
||||
FROM invoices i
|
||||
$whereClause
|
||||
GROUP BY DAY(i.invoice_date)
|
||||
ORDER BY day_num
|
||||
");
|
||||
$stmtDaily->execute($params);
|
||||
$dailyBreakdown = $stmtDaily->fetchAll();
|
||||
|
||||
// 3. Invoice type breakdown
|
||||
$stmtType = $db->prepare("
|
||||
SELECT
|
||||
invoice_type,
|
||||
COUNT(*) as count,
|
||||
COALESCE(SUM(grand_total), 0) as total
|
||||
FROM invoices i
|
||||
$whereClause
|
||||
GROUP BY invoice_type
|
||||
");
|
||||
$stmtType->execute($params);
|
||||
$typeBreakdown = $stmtType->fetchAll();
|
||||
|
||||
// 4. Top 5 suppliers
|
||||
$stmtSuppliers = $db->prepare("
|
||||
SELECT
|
||||
supplier_name,
|
||||
COUNT(*) as invoice_count,
|
||||
COALESCE(SUM(grand_total), 0) as total_amount
|
||||
FROM invoices i
|
||||
$whereClause
|
||||
GROUP BY supplier_name
|
||||
ORDER BY total_amount DESC
|
||||
LIMIT 5
|
||||
");
|
||||
$stmtSuppliers->execute($params);
|
||||
$topSuppliers = $stmtSuppliers->fetchAll();
|
||||
|
||||
// Decrypt supplier names
|
||||
foreach ($topSuppliers as &$s) {
|
||||
$decrypted = \App\Core\Encryption::decrypt($s['supplier_name']);
|
||||
$s['supplier_name'] = ($decrypted !== false && $decrypted !== null) ? $decrypted : $s['supplier_name'];
|
||||
}
|
||||
unset($s);
|
||||
|
||||
// 5. Comparison with previous month
|
||||
$prevMonth = $month == 1 ? 12 : $month - 1;
|
||||
$prevYear = $month == 1 ? $year - 1 : $year;
|
||||
|
||||
$prevWhere = str_replace(
|
||||
"MONTH(i.invoice_date) = ? AND YEAR(i.invoice_date) = ?",
|
||||
"MONTH(i.invoice_date) = ? AND YEAR(i.invoice_date) = ?",
|
||||
implode(' AND ', $where)
|
||||
);
|
||||
|
||||
$prevParams = [$prevMonth, $prevYear];
|
||||
if ($role !== 'super_admin') $prevParams[] = $tenantId;
|
||||
if ($companyId) $prevParams[] = $companyId;
|
||||
|
||||
$stmtPrev = $db->prepare("
|
||||
SELECT
|
||||
COUNT(*) as total_invoices,
|
||||
COALESCE(SUM(grand_total), 0) as total_grand,
|
||||
COALESCE(SUM(tax_amount), 0) as total_tax
|
||||
FROM invoices i
|
||||
WHERE MONTH(i.invoice_date) = ? AND YEAR(i.invoice_date) = ?
|
||||
" . ($role !== 'super_admin' ? " AND i.tenant_id = ?" : "")
|
||||
. ($companyId ? " AND i.company_id = ?" : "")
|
||||
);
|
||||
$stmtPrev->execute($prevParams);
|
||||
$previous = $stmtPrev->fetch();
|
||||
|
||||
// Calculate growth
|
||||
$growth = [
|
||||
'invoices' => $previous['total_invoices'] > 0
|
||||
? round((($summary['total_invoices'] - $previous['total_invoices']) / $previous['total_invoices']) * 100, 1)
|
||||
: 0,
|
||||
'revenue' => $previous['total_grand'] > 0
|
||||
? round((($summary['total_grand'] - $previous['total_grand']) / $previous['total_grand']) * 100, 1)
|
||||
: 0,
|
||||
'tax' => $previous['total_tax'] > 0
|
||||
? round((($summary['total_tax'] - $previous['total_tax']) / $previous['total_tax']) * 100, 1)
|
||||
: 0,
|
||||
];
|
||||
|
||||
json_success([
|
||||
'month' => (int)$month,
|
||||
'year' => (int)$year,
|
||||
'summary' => $summary,
|
||||
'daily_breakdown' => $dailyBreakdown,
|
||||
'type_breakdown' => $typeBreakdown,
|
||||
'top_suppliers' => $topSuppliers,
|
||||
'previous_month' => $previous,
|
||||
'growth' => $growth,
|
||||
], 'تقرير ضريبة المبيعات الشهري');
|
||||
Reference in New Issue
Block a user