diff --git a/fingerprint.php b/fingerprint.php new file mode 100644 index 0000000..7bd2870 --- /dev/null +++ b/fingerprint.php @@ -0,0 +1,3 @@ += 3 && $thousands <= 10) $tStr = numberToWordsArabic($thousands) . " آلاف"; + else $tStr = numberToWordsArabic($thousands) . " ألف"; + + return $tStr . ($rem ? " و " . numberToWordsArabic($rem) : ""); + } + return (string)$num; +} + // Login Logic $login_error = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) { @@ -289,6 +313,46 @@ if (isset($_GET['action']) || isset($_POST['action'])) { echo json_encode(['success' => true]); exit; } + + if ($action === 'get_invoice_items') { + header('Content-Type: application/json'); + $invoice_id = (int)$_GET['invoice_id']; + $stmt = db()->prepare("SELECT ii.*, i.name_en, i.name_ar, i.sku + FROM invoice_items ii + JOIN stock_items i ON ii.item_id = i.id + WHERE ii.invoice_id = ?"); + $stmt->execute([$invoice_id]); + echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC)); + exit; + } + + if ($action === 'get_return_details') { + header('Content-Type: application/json'); + $return_id = (int)$_GET['return_id']; + $type = $_GET['type'] ?? 'sale'; + + if ($type === 'purchase') { + $stmt = db()->prepare("SELECT pr.*, c.name as party_name FROM purchase_returns pr LEFT JOIN customers c ON pr.supplier_id = c.id WHERE pr.id = ?"); + $stmt->execute([$return_id]); + $return = $stmt->fetch(PDO::FETCH_ASSOC); + if ($return) { + $stmtItems = db()->prepare("SELECT pri.*, i.name_en, i.name_ar, i.sku FROM purchase_return_items pri JOIN stock_items i ON pri.item_id = i.id WHERE pri.return_id = ?"); + $stmtItems->execute([$return_id]); + $return['items'] = $stmtItems->fetchAll(PDO::FETCH_ASSOC); + } + } else { + $stmt = db()->prepare("SELECT sr.*, c.name as party_name FROM sales_returns sr LEFT JOIN customers c ON sr.customer_id = c.id WHERE sr.id = ?"); + $stmt->execute([$return_id]); + $return = $stmt->fetch(PDO::FETCH_ASSOC); + if ($return) { + $stmtItems = db()->prepare("SELECT sri.*, i.name_en, i.name_ar, i.sku FROM sales_return_items sri JOIN stock_items i ON sri.item_id = i.id WHERE sri.return_id = ?"); + $stmtItems->execute([$return_id]); + $return['items'] = $stmtItems->fetchAll(PDO::FETCH_ASSOC); + } + } + echo json_encode($return); + exit; + } } // Redirect to login if not authenticated @@ -355,14 +419,25 @@ function numberToWordsOMR($number) { $number = number_format((float)$number, 3, '.', ''); list($rials, $baisas) = explode('.', $number); - $rialsWords = numberToWords((int)$rials); - $baisasWords = numberToWords((int)$baisas); + $rialsWordsEn = numberToWords((int)$rials); + $baisasWordsEn = numberToWords((int)$baisas); - $result = $rialsWords . " Omani Rials"; + $enResult = $rialsWordsEn . " Omani Rials"; if ((int)$baisas > 0) { - $result .= " and " . $baisasWords . " Baisas"; + $enResult .= " and " . $baisasWordsEn . " Baisas"; } - return $result . " Only"; + $enResult .= " Only"; + + $rialsWordsAr = numberToWordsArabic((int)$rials); + $baisasWordsAr = numberToWordsArabic((int)$baisas); + + $arResult = $rialsWordsAr . " ريال عماني"; + if ((int)$baisas > 0) { + $arResult .= " و " . $baisasWordsAr . " بيسة"; + } + $arResult .= " فقط"; + + return $enResult . " / " . $arResult; } function getPromotionalPrice($item) { @@ -519,6 +594,108 @@ if (isset($_POST['add_hr_department'])) { } } + if (isset($_POST['add_sales_return'])) { + $invoice_id = (int)$_POST['invoice_id']; + $return_date = $_POST['return_date'] ?: date('Y-m-d'); + $notes = $_POST['notes'] ?? ''; + $item_ids = $_POST['item_ids'] ?? []; + $quantities = $_POST['quantities'] ?? []; + $prices = $_POST['prices'] ?? []; + + if ($invoice_id && !empty($item_ids)) { + $db = db(); + try { + $db->beginTransaction(); + + // Get customer_id from invoice + $stmtInv = $db->prepare("SELECT customer_id FROM invoices WHERE id = ?"); + $stmtInv->execute([$invoice_id]); + $customer_id = $stmtInv->fetchColumn(); + + $total_return = 0; + foreach ($quantities as $i => $qty) { + $total_return += (float)$qty * (float)$prices[$i]; + } + + // Insert Sales Return + $stmt = $db->prepare("INSERT INTO sales_returns (invoice_id, customer_id, return_date, total_amount, notes) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$invoice_id, $customer_id, $return_date, $total_return, $notes]); + $return_id = $db->lastInsertId(); + + // Insert Return Items and Update Stock + $stmtItem = $db->prepare("INSERT INTO sales_return_items (return_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)"); + $stmtStock = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?"); + + foreach ($item_ids as $i => $item_id) { + $qty = (float)$quantities[$i]; + if ($qty > 0) { + $price = (float)$prices[$i]; + $line_total = $qty * $price; + $stmtItem->execute([$return_id, $item_id, $qty, $price, $line_total]); + $stmtStock->execute([$qty, $item_id]); + } + } + + $db->commit(); + $message = "Sales Return processed successfully!"; + } catch (Exception $e) { + $db->rollBack(); + $message = "Error processing return: " . $e->getMessage(); + } + } + } + + if (isset($_POST['add_purchase_return'])) { + $invoice_id = (int)$_POST['invoice_id']; + $return_date = $_POST['return_date'] ?: date('Y-m-d'); + $notes = $_POST['notes'] ?? ''; + $item_ids = $_POST['item_ids'] ?? []; + $quantities = $_POST['quantities'] ?? []; + $prices = $_POST['prices'] ?? []; + + if ($invoice_id && !empty($item_ids)) { + $db = db(); + try { + $db->beginTransaction(); + + // Get supplier_id (customer_id column) from invoice + $stmtInv = $db->prepare("SELECT customer_id FROM invoices WHERE id = ?"); + $stmtInv->execute([$invoice_id]); + $supplier_id = $stmtInv->fetchColumn(); + + $total_return = 0; + foreach ($quantities as $i => $qty) { + $total_return += (float)$qty * (float)$prices[$i]; + } + + // Insert Purchase Return + $stmt = $db->prepare("INSERT INTO purchase_returns (invoice_id, supplier_id, return_date, total_amount, notes) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$invoice_id, $supplier_id, $return_date, $total_return, $notes]); + $return_id = $db->lastInsertId(); + + // Insert Return Items and Update Stock + $stmtItem = $db->prepare("INSERT INTO purchase_return_items (return_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)"); + $stmtStock = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?"); + + foreach ($item_ids as $i => $item_id) { + $qty = (float)$quantities[$i]; + if ($qty > 0) { + $price = (float)$prices[$i]; + $line_total = $qty * $price; + $stmtItem->execute([$return_id, $item_id, $qty, $price, $line_total]); + $stmtStock->execute([$qty, $item_id]); + } + } + + $db->commit(); + $message = "Purchase Return processed successfully!"; + } catch (Exception $e) { + $db->rollBack(); + $message = "Error processing return: " . $e->getMessage(); + } + } + } + // --- Biometric Devices Handlers --- if (isset($_POST['add_biometric_device'])) { $name = $_POST['device_name'] ?? ''; @@ -1193,6 +1370,12 @@ switch ($page) { } $data['items_list'] = $items_list_raw; $data['customers_list'] = db()->query("SELECT id, name FROM customers WHERE type = '" . ($type === 'sale' ? 'customer' : 'supplier') . "' ORDER BY name ASC")->fetchAll(); + + if ($type === 'sale') { + $data['sales_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM invoices WHERE type = 'sale' ORDER BY id DESC")->fetchAll(); + } else { + $data['purchase_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM invoices WHERE type = 'purchase' ORDER BY id DESC")->fetchAll(); + } break; case 'sales_returns': @@ -7899,12 +8082,12 @@ document.addEventListener('DOMContentLoaded', function() { ${companyPhone}
-

Quotation

+

Quotation / عرض سعر

${statusBadge}
-

No: ${quotNo}

-

Date: ${quotDate}

-

Valid Until: ${quotValid}

+

No / رقم: ${quotNo}

+

Date / التاريخ: ${quotDate}

+

Valid Until / صالح لغاية: ${quotValid}

@@ -7913,7 +8096,7 @@ document.addEventListener('DOMContentLoaded', function() {
-

To

+

To / إلى

${customerName}
@@ -7923,25 +8106,25 @@ document.addEventListener('DOMContentLoaded', function() { # - Item Description - Qty - Unit Price - VAT - Total + Item Description / وصف الصنف + Qty / الكمية + Unit Price / سعر الوحدة + VAT / الضريبة + Total / الإجمالي ${itemsHtml} - Subtotal + Subtotal / المجموع الفرعي ${parseFloat(data.total_amount).toFixed(3)} - VAT Amount + VAT Amount / مبلغ الضريبة ${parseFloat(data.vat_amount).toFixed(3)} - Grand Total (OMR) + Grand Total (OMR) / المجموع الكلي (رع) ${parseFloat(data.total_with_vat).toFixed(3)} @@ -7950,19 +8133,19 @@ document.addEventListener('DOMContentLoaded', function() {
-

Terms & Conditions:

+

Terms & Conditions / الشروط والأحكام:

    -
  • Quotation is valid until the date mentioned above.
  • -
  • Prices are inclusive of VAT where applicable.
  • +
  • Quotation is valid until the date mentioned above. / عرض السعر صالح لغاية التاريخ المذكور أعلاه.
  • +
  • Prices are inclusive of VAT where applicable. / الأسعار تشمل ضريبة القيمة المضافة حيثما ينطبق ذلك.
-
Authorized Signature
+
Authorized Signature / التوقيع المعتمد
-

Generated by ${companyName}

+

Generated by / تم إنشاؤه بواسطة ${companyName}

`; @@ -8286,7 +8469,7 @@ document.addEventListener('DOMContentLoaded', function() { - +
-

Receipt No

+

Receipt No / رقم السند

-

Date

+

Date / التاريخ

-
Received From
+
Received From / استلمنا من
-
Against Invoice
+
Against Invoice / مقابل فاتورة
-
Payment Method
+
Payment Method / طريقة الدفع
-

Amount Paid

+

Amount Paid / المبلغ المدفوع

-
Receiver's Signature
+
Receiver's Signature / توقيع المستلم
-
Authorized Signatory
+
Authorized Signatory / التوقيع المعتمد
@@ -9885,7 +10068,7 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('invAmountInWords').textContent = data.total_in_words || ''; - document.getElementById('invPartyLabel').textContent = data.type === 'sale' ? 'Bill To' : 'Bill From'; + document.getElementById('invPartyLabel').textContent = data.type === 'sale' ? 'Bill To / فاتورة إلى' : 'Bill From / فاتورة من'; document.getElementById('invPartyLabel').setAttribute('data-en', data.type === 'sale' ? 'Bill To' : 'Bill From'); document.getElementById('invPartyLabel').setAttribute('data-ar', data.type === 'sale' ? 'فاتورة إلى' : 'فاتورة من'); document.getElementById('invoiceTypeLabel').textContent = data.type; @@ -9969,7 +10152,7 @@ document.addEventListener('DOMContentLoaded', function() { const container = document.getElementById('posReceiptContent'); const itemsHtml = inv.items.map(item => ` - ${item.name_en}
${item.quantity} x ${parseFloat(item.unit_price).toFixed(3)} + ${item.name_en} / ${item.name_ar}
${item.quantity} x ${parseFloat(item.unit_price).toFixed(3)} ${(item.unit_price * item.quantity).toFixed(3)} `).join(''); @@ -9985,20 +10168,20 @@ document.addEventListener('DOMContentLoaded', function() { ${companyPhone ? `
Tel: ${companyPhone}
` : ''} ${companyVat ? `
VAT: ${companyVat}
` : ''}
-
TAX INVOICE
-
Inv: INV-${inv.id.toString().padStart(5, '0')}
-
Date: ${inv.invoice_date}
+
TAX INVOICE / فاتورة ضريبية
+
Inv / رقم: INV-${inv.id.toString().padStart(5, '0')}
+
Date / التاريخ: ${inv.invoice_date}
- Customer: ${inv.customer_name || 'Walk-in'} + Customer / العميل: ${inv.customer_name || 'Walk-in / عميل عابر'}
- - + + @@ -10007,20 +10190,20 @@ document.addEventListener('DOMContentLoaded', function() {
ITEMTOTALITEM / الصنفTOTAL / الإجمالي
- TOTAL + TOTAL / الإجمالي OMR ${parseFloat(inv.total_with_vat).toFixed(3)}
- PAID + PAID / المدفوع OMR ${parseFloat(inv.paid_amount).toFixed(3)}
- BALANCE + BALANCE / الرصيد OMR ${(inv.total_with_vat - inv.paid_amount).toFixed(3)}
-

Thank You for your business!

+

Thank You for your business! / شكراً لتعاملكم معنا!

`; diff --git a/installation/index.php b/installation/index.php index 60d3f68..5b7a54f 100644 --- a/installation/index.php +++ b/installation/index.php @@ -71,26 +71,53 @@ if ($step === 3 && $_SERVER['REQUEST_METHOD'] === 'POST') { try { $pdo = db(); - // Create tables if they don't exist - $pdo->exec("CREATE TABLE IF NOT EXISTS role_groups ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(50) NOT NULL, - permissions TEXT - )"); + // Import full schema if available + $schemaFile = __DIR__ . '/../complete_schema.sql'; + if (file_exists($schemaFile)) { + $host = DB_HOST; + $name = DB_NAME; + $user = DB_USER; + $pass = DB_PASS; + + // Use mysql CLI for reliable import + $cmd = sprintf( + 'mysql -h %s -u %s %s %s < %s', + escapeshellarg($host), + escapeshellarg($user), + ($pass ? '-p' . escapeshellarg($pass) : ''), + escapeshellarg($name), + escapeshellarg($schemaFile) + ); + + exec($cmd, $output, $returnVar); + + if ($returnVar !== 0) { + // Fallback to manual execution if CLI fails + $sql = file_get_contents($schemaFile); + $pdo->exec($sql); + } + } else { + // Fallback to base tables if complete_schema.sql is missing + $pdo->exec("CREATE TABLE IF NOT EXISTS role_groups ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(50) NOT NULL, + permissions TEXT + )"); - $pdo->exec("CREATE TABLE IF NOT EXISTS users ( - id INT AUTO_INCREMENT PRIMARY KEY, - username VARCHAR(50) NOT NULL UNIQUE, - password VARCHAR(255) NOT NULL, - email VARCHAR(100), - phone VARCHAR(20), - group_id INT, - status ENUM('active', 'inactive') DEFAULT 'active', - profile_pic VARCHAR(255), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )"); + $pdo->exec("CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + email VARCHAR(100), + phone VARCHAR(20), + group_id INT, + status ENUM('active', 'inactive') DEFAULT 'active', + profile_pic VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )"); + } - // Insert Default Admin Role if missing + // Ensure Admin Role exists (might be in schema but just in case) $stmt = $pdo->prepare("SELECT id FROM role_groups WHERE name = 'Administrator'"); $stmt->execute(); $role = $stmt->fetch(); @@ -101,9 +128,15 @@ if ($step === 3 && $_SERVER['REQUEST_METHOD'] === 'POST') { $roleId = $role['id']; } - // Insert Admin User + // Insert Admin User (Use ON DUPLICATE KEY UPDATE to handle existing user from schema) $hashedPass = password_hash($adminPass, PASSWORD_DEFAULT); - $stmt = $pdo->prepare("INSERT INTO users (username, password, email, group_id, status) VALUES (?, ?, ?, ?, 'active')"); + $stmt = $pdo->prepare("INSERT INTO users (username, password, email, group_id, status) + VALUES (?, ?, ?, ?, 'active') + ON DUPLICATE KEY UPDATE + password = VALUES(password), + email = VALUES(email), + group_id = VALUES(group_id), + status = 'active'"); $stmt->execute([$adminUser, $hashedPass, $adminEmail, $roleId]); header("Location: index.php?step=4"); diff --git a/post_debug.log b/post_debug.log index b5c6c51..afd936d 100644 --- a/post_debug.log +++ b/post_debug.log @@ -41,3 +41,7 @@ 2026-02-18 18:09:44 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":0.595}]","total_amount":"0.595","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":2,\"qty\":1,\"price\":0.2125},{\"id\":1,\"qty\":1,\"price\":0.3825}]"} 2026-02-18 18:09:48 - POST: {"open_register":"1","register_id":"1","opening_balance":"0"} 2026-02-18 18:10:17 - POST: {"close_register":"1","session_id":"2","cash_in_hand":"5","notes":""} +2026-02-19 05:17:29 - POST: {"license_key":"FLAT-5FDB-C2BB","activate":""} +2026-02-19 05:18:29 - POST: {"open_register":"1","register_id":"1","opening_balance":"0"} +2026-02-19 05:18:39 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":0.595}]","total_amount":"0.595","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":1,\"qty\":1,\"price\":0.3825},{\"id\":2,\"qty\":1,\"price\":0.2125}]"} +2026-02-19 05:18:51 - POST: {"open_register":"1","register_id":"1","opening_balance":"0"}