diff --git a/db/migrations/20260216_add_invoice_status.sql b/db/migrations/20260216_add_invoice_status.sql new file mode 100644 index 0000000..8736de1 --- /dev/null +++ b/db/migrations/20260216_add_invoice_status.sql @@ -0,0 +1,2 @@ +-- Add status column to invoices table +ALTER TABLE invoices ADD COLUMN status ENUM('paid', 'unpaid', 'partially_paid') DEFAULT 'unpaid'; diff --git a/db/migrations/20260216_add_payments_table.sql b/db/migrations/20260216_add_payments_table.sql new file mode 100644 index 0000000..029f22f --- /dev/null +++ b/db/migrations/20260216_add_payments_table.sql @@ -0,0 +1,14 @@ +-- Add paid_amount to invoices +ALTER TABLE invoices ADD COLUMN paid_amount DECIMAL(15,2) DEFAULT 0.00 AFTER total_with_vat; + +-- Create payments table +CREATE TABLE IF NOT EXISTS payments ( + id INT AUTO_INCREMENT PRIMARY KEY, + invoice_id INT NOT NULL, + payment_date DATE NOT NULL, + amount DECIMAL(15,2) NOT NULL, + payment_method VARCHAR(50) DEFAULT 'Cash', + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE +); diff --git a/db/migrations/20260216_update_precision_3_decimal.sql b/db/migrations/20260216_update_precision_3_decimal.sql new file mode 100644 index 0000000..81ae70d --- /dev/null +++ b/db/migrations/20260216_update_precision_3_decimal.sql @@ -0,0 +1,11 @@ +-- Update amount columns to 3 decimal places +ALTER TABLE customers MODIFY COLUMN balance DECIMAL(15,3) DEFAULT 0.000; +ALTER TABLE stock_items MODIFY COLUMN purchase_price DECIMAL(15,3) DEFAULT 0.000; +ALTER TABLE stock_items MODIFY COLUMN sale_price DECIMAL(15,3) DEFAULT 0.000; +ALTER TABLE invoices MODIFY COLUMN total_amount DECIMAL(15,3) DEFAULT 0.000; +ALTER TABLE invoices MODIFY COLUMN vat_amount DECIMAL(15,3) DEFAULT 0.000; +ALTER TABLE invoices MODIFY COLUMN total_with_vat DECIMAL(15,3) DEFAULT 0.000; +ALTER TABLE invoices MODIFY COLUMN paid_amount DECIMAL(15,3) DEFAULT 0.000; +ALTER TABLE invoice_items MODIFY COLUMN unit_price DECIMAL(15,3) DEFAULT 0.000; +ALTER TABLE invoice_items MODIFY COLUMN total_price DECIMAL(15,3) DEFAULT 0.000; +ALTER TABLE payments MODIFY COLUMN amount DECIMAL(15,3) DEFAULT 0.000; diff --git a/index.php b/index.php index 518217d..f783867 100644 --- a/index.php +++ b/index.php @@ -1,16 +1,78 @@ prepare("SELECT id, name_en, name_ar, sku, sale_price, purchase_price, stock_quantity, vat_rate FROM stock_items WHERE name_en LIKE ? OR name_ar LIKE ? OR sku LIKE ? LIMIT 10"); - $stmt->execute(["%$q%", "%$q%", "%$q%"]); - echo json_encode($stmt->fetchAll()); - exit; + +function numberToWordsOMR($number) { + $number = number_format((float)$number, 3, '.', ''); + list($rials, $baisas) = explode('.', $number); + + $rialsWords = numberToWords((int)$rials); + $baisasWords = numberToWords((int)$baisas); + + $result = $rialsWords . " Omani Rials"; + if ((int)$baisas > 0) { + $result .= " and " . $baisasWords . " Baisas"; + } + return $result . " Only"; +} + +function numberToWords($num) { + $num = (int)$num; + if ($num === 0) return "Zero"; + $ones = ["", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"]; + $tens = ["", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"]; + if ($num < 20) return $ones[$num]; + if ($num < 100) return $tens[(int)($num / 10)] . ($num % 10 ? " " . $ones[$num % 10] : ""); + if ($num < 1000) return $ones[(int)($num / 100)] . " Hundred" . ($num % 100 ? " and " . numberToWords($num % 100) : ""); + if ($num < 1000000) return numberToWords((int)($num / 1000)) . " Thousand" . ($num % 1000 ? " " . numberToWords($num % 1000) : ""); + return (string)$num; +} + +if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['action'])) { + if ($_GET['action'] === 'search_items') { + header('Content-Type: application/json'); + $q = $_GET['q'] ?? ''; + $stmt = db()->prepare("SELECT id, name_en, name_ar, sku, sale_price, purchase_price, stock_quantity, vat_rate FROM stock_items WHERE name_en LIKE ? OR name_ar LIKE ? OR sku LIKE ? LIMIT 10"); + $stmt->execute(["%$q%", "%$q%", "%$q%"]); + echo json_encode($stmt->fetchAll()); + exit; + } + if ($_GET['action'] === 'get_payments') { + header('Content-Type: application/json'); + $invoice_id = (int)$_GET['invoice_id']; + $stmt = db()->prepare("SELECT p.*, i.id as inv_id, c.name as customer_name + FROM payments p + JOIN invoices i ON p.invoice_id = i.id + LEFT JOIN customers c ON i.customer_id = c.id + WHERE p.invoice_id = ? ORDER BY p.payment_date DESC, p.id DESC"); + $stmt->execute([$invoice_id]); + $payments = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($payments as &$p) { + $p['amount_words'] = numberToWordsOMR($p['amount']); + } + echo json_encode($payments); + exit; + } + if ($_GET['action'] === 'get_payment_details') { + header('Content-Type: application/json'); + $payment_id = (int)$_GET['payment_id']; + $stmt = db()->prepare("SELECT p.*, i.id as inv_id, c.name as customer_name + FROM payments p + JOIN invoices i ON p.invoice_id = i.id + LEFT JOIN customers c ON i.customer_id = c.id + WHERE p.id = ?"); + $stmt->execute([$payment_id]); + $payment = $stmt->fetch(PDO::FETCH_ASSOC); + if ($payment) { + $payment['amount_words'] = numberToWordsOMR($payment['amount']); + } + echo json_encode($payment); + exit; + } } if ($_SERVER['REQUEST_METHOD'] === 'POST') { @@ -18,11 +80,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $name = $_POST['name'] ?? ''; $email = $_POST['email'] ?? ''; $phone = $_POST['phone'] ?? ''; + $tax_id = $_POST['tax_id'] ?? ''; $balance = (float)($_POST['balance'] ?? 0); $type = $_POST['type'] ?? 'customer'; if ($name) { - $stmt = db()->prepare("INSERT INTO customers (name, email, phone, balance, type) VALUES (?, ?, ?, ?, ?)"); - $stmt->execute([$name, $email, $phone, $balance, $type]); + $stmt = db()->prepare("INSERT INTO customers (name, email, phone, tax_id, balance, type) VALUES (?, ?, ?, ?, ?, ?)"); + $stmt->execute([$name, $email, $phone, $tax_id, $balance, $type]); $message = ucfirst($type) . " added successfully!"; } } @@ -32,10 +95,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $name = $_POST['name'] ?? ''; $email = $_POST['email'] ?? ''; $phone = $_POST['phone'] ?? ''; + $tax_id = $_POST['tax_id'] ?? ''; $balance = (float)($_POST['balance'] ?? 0); if ($id && $name) { - $stmt = db()->prepare("UPDATE customers SET name = ?, email = ?, phone = ?, balance = ? WHERE id = ?"); - $stmt->execute([$name, $email, $phone, $balance, $id]); + $stmt = db()->prepare("UPDATE customers SET name = ?, email = ?, phone = ?, tax_id = ?, balance = ? WHERE id = ?"); + $stmt->execute([$name, $email, $phone, $tax_id, $balance, $id]); $message = "Record updated successfully!"; } } @@ -209,10 +273,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $name = trim($row[0]); $email = trim($row[1]); $phone = trim($row[2]); - $balance = (float)trim($row[3]); + $tax_id = isset($row[3]) ? trim($row[3]) : ''; + $balance = isset($row[4]) ? (float)trim($row[4]) : (float)trim($row[3] ?? 0); if ($name) { - $stmt = db()->prepare("INSERT INTO customers (name, email, phone, balance, type) VALUES (?, ?, ?, ?, ?)"); - $stmt->execute([$name, $email, $phone, $balance, $type]); + $stmt = db()->prepare("INSERT INTO customers (name, email, phone, tax_id, balance, type) VALUES (?, ?, ?, ?, ?, ?)"); + $stmt->execute([$name, $email, $phone, $tax_id, $balance, $type]); $count++; } } @@ -270,6 +335,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $invoice_date = $_POST['invoice_date'] ?: date('Y-m-d'); $type = $_POST['type'] ?? 'sale'; // 'sale' or 'purchase' $payment_type = $_POST['payment_type'] ?? 'cash'; + $status = $_POST['status'] ?? 'unpaid'; $item_ids = $_POST['item_ids'] ?? []; $quantities = $_POST['quantities'] ?? []; $prices = $_POST['prices'] ?? []; @@ -306,9 +372,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } $total_with_vat = $subtotal + $total_vat; + $paid_amount = ($status === 'paid') ? $total_with_vat : 0; - $stmt = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, type, payment_type, total_amount, vat_amount, total_with_vat) VALUES (?, ?, ?, ?, ?, ?, ?)"); - $stmt->execute([$customer_id, $invoice_date, $type, $payment_type, $subtotal, $total_vat, $total_with_vat]); + $stmt = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, type, payment_type, status, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"); + $stmt->execute([$customer_id, $invoice_date, $type, $payment_type, $status, $subtotal, $total_vat, $total_with_vat, $paid_amount]); $invoice_id = $db->lastInsertId(); foreach ($items_data as $item) { @@ -377,6 +444,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $customer_id = $_POST['customer_id'] ?: null; $invoice_date = $_POST['invoice_date'] ?: date('Y-m-d'); $payment_type = $_POST['payment_type'] ?? 'cash'; + $status = $_POST['status'] ?? 'unpaid'; $item_ids = $_POST['item_ids'] ?? []; $quantities = $_POST['quantities'] ?? []; $prices = $_POST['prices'] ?? []; @@ -434,10 +502,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } $total_with_vat = $subtotal + $total_vat; + $paid_amount = ($status === 'paid') ? $total_with_vat : (($status === 'unpaid') ? 0 : $_POST['paid_amount'] ?? 0); // Update invoice - $stmt = $db->prepare("UPDATE invoices SET customer_id = ?, invoice_date = ?, payment_type = ?, total_amount = ?, vat_amount = ?, total_with_vat = ? WHERE id = ?"); - $stmt->execute([$customer_id, $invoice_date, $payment_type, $subtotal, $total_vat, $total_with_vat, $invoice_id]); + $stmt = $db->prepare("UPDATE invoices SET customer_id = ?, invoice_date = ?, payment_type = ?, status = ?, total_amount = ?, vat_amount = ?, total_with_vat = ?, paid_amount = ? WHERE id = ?"); + $stmt->execute([$customer_id, $invoice_date, $payment_type, $status, $subtotal, $total_vat, $total_with_vat, $paid_amount, $invoice_id]); // Insert new items and update stock foreach ($items_data as $item) { @@ -530,8 +599,55 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } } $message = "Settings updated successfully!"; + } } -} + + if (isset($_POST['record_payment'])) { + $invoice_id = (int)$_POST['invoice_id']; + $amount = (float)$_POST['amount']; + $payment_date = $_POST['payment_date'] ?: date('Y-m-d'); + $payment_method = $_POST['payment_method'] ?: 'Cash'; + $notes = $_POST['notes'] ?? ''; + + if ($invoice_id && $amount > 0) { + $db = db(); + $db->beginTransaction(); + try { + // Record the payment + $stmt = $db->prepare("INSERT INTO payments (invoice_id, payment_date, amount, payment_method, notes) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$invoice_id, $payment_date, $amount, $payment_method, $notes]); + $payment_id = $db->lastInsertId(); + + // Update invoice paid_amount and status + $stmt = $db->prepare("SELECT total_with_vat, paid_amount FROM invoices WHERE id = ?"); + $stmt->execute([$invoice_id]); + $inv = $stmt->fetch(); + + $new_paid_amount = (float)$inv['paid_amount'] + $amount; + $total = (float)$inv['total_with_vat']; + + $new_status = 'partially_paid'; + if ($new_paid_amount >= $total) { + $new_status = 'paid'; + } + + $stmt = $db->prepare("UPDATE invoices SET paid_amount = ?, status = ? WHERE id = ?"); + $stmt->execute([$new_paid_amount, $new_status, $invoice_id]); + + $db->commit(); + $message = "Payment of OMR " . number_format($amount, 3) . " recorded successfully! Receipt ID: $payment_id"; + + // For showing receipt after redirect/refresh, we can use a session or just a message. + // The user wants to "issue a receipt". I'll add a trigger to show the receipt modal. + $_SESSION['show_receipt_id'] = $payment_id; + $_SESSION['trigger_receipt_modal'] = true; + } catch (Exception $e) { + $db->rollBack(); + $message = "Error: " . $e->getMessage(); + } + } + } + // Routing & Data Fetching $page = $_GET['page'] ?? 'dashboard'; @@ -557,23 +673,23 @@ if ($page === 'export') { if (!empty($_GET['start_date'])) { $where[] = "v.invoice_date >= ?"; $params[] = $_GET['start_date']; } if (!empty($_GET['end_date'])) { $where[] = "v.invoice_date <= ?"; $params[] = $_GET['end_date']; } $whereSql = implode(" AND ", $where); - $stmt = db()->prepare("SELECT v.id, c.name as customer_name, v.invoice_date, v.payment_type, v.total_amount, v.vat_amount, v.total_with_vat + $stmt = db()->prepare("SELECT v.id, c.name as customer_name, v.invoice_date, v.payment_type, v.status, v.total_amount, v.vat_amount, v.total_with_vat FROM invoices v LEFT JOIN customers c ON v.customer_id = c.id WHERE $whereSql ORDER BY v.id DESC"); $stmt->execute($params); - fputcsv($output, ['Invoice ID', 'Customer/Supplier', 'Date', 'Payment', 'Subtotal', 'VAT', 'Total']); + fputcsv($output, ['Invoice ID', 'Customer/Supplier', 'Date', 'Payment', 'Status', 'Subtotal', 'VAT', 'Total']); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) fputcsv($output, $row); } elseif ($type === 'customers' || $type === 'suppliers') { $custType = ($type === 'suppliers') ? 'supplier' : 'customer'; $where = ["type = ?"]; $params = [$custType]; - if (!empty($_GET['search'])) { $where[] = "(name LIKE ? OR email LIKE ? OR phone LIKE ?)"; $params[] = "%{$_GET['search']}%"; $params[] = "%{$_GET['search']}%"; $params[] = "%{$_GET['search']}%"; } + if (!empty($_GET['search'])) { $where[] = "(name LIKE ? OR email LIKE ? OR phone LIKE ? OR tax_id LIKE ?)"; $params[] = "%{$_GET['search']}%"; $params[] = "%{$_GET['search']}%"; $params[] = "%{$_GET['search']}%"; $params[] = "%{$_GET['search']}%"; } if (!empty($_GET['start_date'])) { $where[] = "DATE(created_at) >= ?"; $params[] = $_GET['start_date']; } if (!empty($_GET['end_date'])) { $where[] = "DATE(created_at) <= ?"; $params[] = $_GET['end_date']; } $whereSql = implode(" AND ", $where); - $stmt = db()->prepare("SELECT id, name, email, phone, balance, created_at FROM customers WHERE $whereSql ORDER BY id DESC"); + $stmt = db()->prepare("SELECT id, name, email, phone, tax_id, balance, created_at FROM customers WHERE $whereSql ORDER BY id DESC"); $stmt->execute($params); - fputcsv($output, ['ID', 'Name', 'Email', 'Phone', 'Balance', 'Created At']); + fputcsv($output, ['ID', 'Name', 'Email', 'Phone', 'Tax ID', 'Balance', 'Created At']); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) fputcsv($output, $row); } elseif ($type === 'items') { $where = ["1=1"]; @@ -609,7 +725,8 @@ switch ($page) { $where = ["type = ?"]; $params = [$type]; if (!empty($_GET['search'])) { - $where[] = "(name LIKE ? OR email LIKE ? OR phone LIKE ?)"; + $where[] = "(name LIKE ? OR email LIKE ? OR phone LIKE ? OR tax_id LIKE ?)"; + $params[] = "%{$_GET['search']}%"; $params[] = "%{$_GET['search']}%"; $params[] = "%{$_GET['search']}%"; $params[] = "%{$_GET['search']}%"; @@ -688,7 +805,7 @@ switch ($page) { } $whereSql = implode(" AND ", $where); - $stmt = db()->prepare("SELECT v.*, c.name as customer_name + $stmt = db()->prepare("SELECT v.*, c.name as customer_name, c.tax_id as customer_tax_id FROM invoices v LEFT JOIN customers c ON v.customer_id = c.id WHERE $whereSql @@ -775,10 +892,10 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; - Sales Invoices + Sales Tax Invoices - Purchase Invoices + Purchase Tax Invoices Expenses @@ -831,8 +948,8 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; 'units' => ['en' => 'Stock Units', 'ar' => 'وحدات المخزون'], 'items' => ['en' => 'Stock Items', 'ar' => 'أصناف المخزون'], 'payment_methods' => ['en' => 'Payment Methods', 'ar' => 'طرق الدفع'], - 'sales' => ['en' => 'Sales Invoices', 'ar' => 'فواتير المبيعات'], - 'purchases' => ['en' => 'Purchase Invoices', 'ar' => 'فواتير المشتريات'], + 'sales' => ['en' => 'Sales Tax Invoices', 'ar' => 'فواتير المبيعات الضريبية'], + 'purchases' => ['en' => 'Purchase Tax Invoices', 'ar' => 'فواتير المشتريات الضريبية'], 'settings' => ['en' => 'Company Profile', 'ar' => 'ملف الشركة'], ]; $currTitle = $titles[$page] ?? $titles['dashboard']; @@ -868,7 +985,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
Total Sales
-
$24,500
+
OMR 24,500.000
@@ -886,7 +1003,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
Net Profit
-
$20,300
+
OMR 20,300.000
@@ -914,7 +1031,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; - $ + OMR @@ -936,10 +1053,10 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; Import Items
- Sales Invoices + Sales Tax Invoices - Purchase Invoices + Purchase Tax Invoices @@ -995,6 +1112,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; Name + Tax ID Email Phone Balance @@ -1005,9 +1123,10 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; + - $ + OMR
@@ -1042,9 +1161,13 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
+
+ + +
- +
-
+
+
+ + +
@@ -1524,17 +1695,17 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; Subtotal - $0.00 + OMR 0.000 VAT Amount - $0.00 + OMR 0.000 Grand Total (Inc. VAT) - $0.00 + OMR 0.000 @@ -1555,7 +1726,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; + + + +
Company Settings
@@ -1839,7 +2203,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
- +
@@ -1983,9 +2347,13 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
+
+ + +
- +
- +
- +
- +
- +
@@ -2072,7 +2440,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
- +
@@ -2135,8 +2503,8 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';