diff --git a/db/migrations/20260216_vat_and_profile.sql b/db/migrations/20260216_vat_and_profile.sql new file mode 100644 index 0000000..65c139e --- /dev/null +++ b/db/migrations/20260216_vat_and_profile.sql @@ -0,0 +1,25 @@ +-- Add VAT to stock items +ALTER TABLE stock_items ADD COLUMN vat_rate DECIMAL(5,2) DEFAULT 0.00; + +-- Add VAT details to invoices +ALTER TABLE invoices ADD COLUMN vat_amount DECIMAL(15,2) DEFAULT 0.00; +ALTER TABLE invoices ADD COLUMN total_with_vat DECIMAL(15,2) DEFAULT 0.00; + +-- Create settings table for company profile +CREATE TABLE IF NOT EXISTS settings ( + `key` VARCHAR(50) PRIMARY KEY, + `value` TEXT, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- Seed initial company profile +INSERT INTO settings (`key`, `value`) VALUES +('company_name', 'My Tech Company'), +('company_logo', ''), +('vat_number', ''), +('cr_number', ''), +('company_address', ''), +('company_phone', ''), +('vat_enabled', '1'), +('default_vat_rate', '15.00') +ON DUPLICATE KEY UPDATE `key`=`key`; diff --git a/index.php b/index.php index b622a5b..3c5aa11 100644 --- a/index.php +++ b/index.php @@ -7,7 +7,7 @@ $message = ''; if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['action']) && $_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 FROM stock_items WHERE name_en LIKE ? OR name_ar LIKE ? OR sku LIKE ? LIMIT 10"); + $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; @@ -82,6 +82,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $s_price = (float)($_POST['sale_price'] ?? 0); $qty = (float)($_POST['stock_quantity'] ?? 0); $min_stock = (float)($_POST['min_stock_level'] ?? 0); + $vat_rate = (float)($_POST['vat_rate'] ?? 0); $expiry = $_POST['expiry_date'] ?: null; $image_path = null; @@ -95,8 +96,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } if ($name_en && $name_ar) { - $stmt = db()->prepare("INSERT INTO stock_items (category_id, unit_id, supplier_id, name_en, name_ar, sku, purchase_price, sale_price, stock_quantity, min_stock_level, expiry_date, image_path) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); - $stmt->execute([$cat_id, $unit_id, $supplier_id, $name_en, $name_ar, $sku, $p_price, $s_price, $qty, $min_stock, $expiry, $image_path]); + $stmt = db()->prepare("INSERT INTO stock_items (category_id, unit_id, supplier_id, name_en, name_ar, sku, purchase_price, sale_price, stock_quantity, min_stock_level, expiry_date, image_path, vat_rate) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + $stmt->execute([$cat_id, $unit_id, $supplier_id, $name_en, $name_ar, $sku, $p_price, $s_price, $qty, $min_stock, $expiry, $image_path, $vat_rate]); $message = "Item added successfully!"; } } @@ -128,6 +129,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $s_price = (float)($_POST['sale_price'] ?? 0); $qty = (float)($_POST['stock_quantity'] ?? 0); $min_stock = (float)($_POST['min_stock_level'] ?? 0); + $vat_rate = (float)($_POST['vat_rate'] ?? 0); $expiry = $_POST['expiry_date'] ?: null; $stmt = db()->prepare("SELECT image_path FROM stock_items WHERE id = ?"); @@ -148,8 +150,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } if ($name_en && $name_ar) { - $stmt = db()->prepare("UPDATE stock_items SET category_id = ?, unit_id = ?, supplier_id = ?, name_en = ?, name_ar = ?, sku = ?, purchase_price = ?, sale_price = ?, stock_quantity = ?, min_stock_level = ?, expiry_date = ?, image_path = ? WHERE id = ?"); - $stmt->execute([$cat_id, $unit_id, $supplier_id, $name_en, $name_ar, $sku, $p_price, $s_price, $qty, $min_stock, $expiry, $image_path, $id]); + $stmt = db()->prepare("UPDATE stock_items SET category_id = ?, unit_id = ?, supplier_id = ?, name_en = ?, name_ar = ?, sku = ?, purchase_price = ?, sale_price = ?, stock_quantity = ?, min_stock_level = ?, expiry_date = ?, image_path = ?, vat_rate = ? WHERE id = ?"); + $stmt->execute([$cat_id, $unit_id, $supplier_id, $name_en, $name_ar, $sku, $p_price, $s_price, $qty, $min_stock, $expiry, $image_path, $vat_rate, $id]); $message = "Item updated successfully!"; } } @@ -276,22 +278,42 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $db = db(); $db->beginTransaction(); try { - $total_amount = 0; - foreach ($item_ids as $index => $item_id) { - $total_amount += (float)$quantities[$index] * (float)$prices[$index]; - } - - $stmt = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, type, payment_type, total_amount) VALUES (?, ?, ?, ?, ?)"); - $stmt->execute([$customer_id, $invoice_date, $type, $payment_type, $total_amount]); - $invoice_id = $db->lastInsertId(); - + $subtotal = 0; + $total_vat = 0; + + $items_data = []; foreach ($item_ids as $index => $item_id) { $qty = (float)$quantities[$index]; $price = (float)$prices[$index]; - $total_price = $qty * $price; + + // Fetch vat_rate for this item + $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?"); + $stmtVat->execute([$item_id]); + $vat_rate = (float)$stmtVat->fetchColumn(); + + $line_total = $qty * $price; + $line_vat = $line_total * ($vat_rate / 100); + + $subtotal += $line_total; + $total_vat += $line_vat; + + $items_data[] = [ + 'id' => $item_id, + 'qty' => $qty, + 'price' => $price, + 'total' => $line_total + ]; + } + + $total_with_vat = $subtotal + $total_vat; + $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]); + $invoice_id = $db->lastInsertId(); + + foreach ($items_data as $item) { $stmt = $db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)"); - $stmt->execute([$invoice_id, $item_id, $qty, $price, $total_price]); + $stmt->execute([$invoice_id, $item['id'], $item['qty'], $item['price'], $item['total']]); // Update stock level if ($type === 'sale') { @@ -299,14 +321,142 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } else { $stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?"); } - $stmt->execute([$qty, $item_id]); + $stmt->execute([$item['qty'], $item['id']]); } $db->commit(); - $message = ucfirst($type) . " invoice created successfully!"; + $message = "Invoice #$invoice_id created successfully!"; } catch (Exception $e) { $db->rollBack(); - $message = "Error creating invoice: " . $e->getMessage(); + $message = "Error: " . $e->getMessage(); + } + } + } + + if (isset($_POST['delete_invoice'])) { + $id = (int)$_POST['id']; + if ($id) { + $db = db(); + $db->beginTransaction(); + try { + // Get invoice details + $stmt = $db->prepare("SELECT type FROM invoices WHERE id = ?"); + $stmt->execute([$id]); + $type = $stmt->fetchColumn(); + + // Get items to restore stock + $stmt = $db->prepare("SELECT item_id, quantity FROM invoice_items WHERE invoice_id = ?"); + $stmt->execute([$id]); + $items = $stmt->fetchAll(); + + foreach ($items as $item) { + if ($type === 'sale') { + $stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?"); + } else { + $stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?"); + } + $stmt->execute([$item['quantity'], $item['item_id']]); + } + + $stmt = $db->prepare("DELETE FROM invoice_items WHERE invoice_id = ?"); + $stmt->execute([$id]); + $stmt = $db->prepare("DELETE FROM invoices WHERE id = ?"); + $stmt->execute([$id]); + + $db->commit(); + $message = "Invoice deleted successfully and stock restored!"; + } catch (Exception $e) { + $db->rollBack(); + $message = "Error: " . $e->getMessage(); + } + } + } + + if (isset($_POST['edit_invoice'])) { + $invoice_id = (int)$_POST['invoice_id']; + $customer_id = $_POST['customer_id'] ?: null; + $invoice_date = $_POST['invoice_date'] ?: date('Y-m-d'); + $payment_type = $_POST['payment_type'] ?? 'cash'; + $item_ids = $_POST['item_ids'] ?? []; + $quantities = $_POST['quantities'] ?? []; + $prices = $_POST['prices'] ?? []; + + if ($invoice_id && !empty($item_ids)) { + $db = db(); + $db->beginTransaction(); + try { + // Get old invoice type and items to revert stock + $stmt = $db->prepare("SELECT type FROM invoices WHERE id = ?"); + $stmt->execute([$invoice_id]); + $type = $stmt->fetchColumn(); + + $stmt = $db->prepare("SELECT item_id, quantity FROM invoice_items WHERE invoice_id = ?"); + $stmt->execute([$invoice_id]); + $old_items = $stmt->fetchAll(); + + foreach ($old_items as $item) { + if ($type === 'sale') { + $stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?"); + } else { + $stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?"); + } + $stmt->execute([$item['quantity'], $item['item_id']]); + } + + // Delete old items + $stmt = $db->prepare("DELETE FROM invoice_items WHERE invoice_id = ?"); + $stmt->execute([$invoice_id]); + + // Calculate new totals + $subtotal = 0; + $total_vat = 0; + $items_data = []; + foreach ($item_ids as $index => $item_id) { + $qty = (float)$quantities[$index]; + $price = (float)$prices[$index]; + + $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?"); + $stmtVat->execute([$item_id]); + $vat_rate = (float)$stmtVat->fetchColumn(); + + $line_total = $qty * $price; + $line_vat = $line_total * ($vat_rate / 100); + + $subtotal += $line_total; + $total_vat += $line_vat; + + $items_data[] = [ + 'id' => $item_id, + 'qty' => $qty, + 'price' => $price, + 'total' => $line_total + ]; + } + + $total_with_vat = $subtotal + $total_vat; + + // 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]); + + // Insert new items and update stock + foreach ($items_data as $item) { + $stmt = $db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$invoice_id, $item['id'], $item['qty'], $item['price'], $item['total']]); + + if ($type === 'sale') { + $stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?"); + } else { + $stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?"); + } + $stmt->execute([$item['qty'], $item['id']]); + } + + $db->commit(); + $message = "Invoice #$invoice_id updated successfully!"; + } catch (Exception $e) { + $db->rollBack(); + $message = "Error: " . $e->getMessage(); } } } @@ -340,6 +490,47 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $message = "Payment method deleted successfully!"; } } + + if (isset($_POST['update_settings'])) { + foreach ($_POST['settings'] as $key => $value) { + $stmt = db()->prepare("INSERT INTO settings (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = ?"); + $stmt->execute([$key, $value, $value]); + } + + if (isset($_FILES['company_logo']) && $_FILES['company_logo']['error'] === UPLOAD_ERR_OK) { + $ext = pathinfo($_FILES['company_logo']['name'], PATHINFO_EXTENSION); + $filename = 'logo.' . $ext; + $target = 'uploads/' . $filename; + if (!is_dir('uploads')) mkdir('uploads', 0775, true); + if (move_uploaded_file($_FILES['company_logo']['tmp_name'], $target)) { + $stmt = db()->prepare("INSERT INTO settings (`key`, `value`) VALUES ('company_logo', ?) ON DUPLICATE KEY UPDATE `value` = ?"); + $stmt->execute([$target, $target]); + } + } + + if (isset($_FILES['favicon']) && $_FILES['favicon']['error'] === UPLOAD_ERR_OK) { + $ext = pathinfo($_FILES['favicon']['name'], PATHINFO_EXTENSION); + $filename = 'favicon.' . $ext; + $target = 'uploads/' . $filename; + if (!is_dir('uploads')) mkdir('uploads', 0775, true); + if (move_uploaded_file($_FILES['favicon']['tmp_name'], $target)) { + $stmt = db()->prepare("INSERT INTO settings (`key`, `value`) VALUES ('favicon', ?) ON DUPLICATE KEY UPDATE `value` = ?"); + $stmt->execute([$target, $target]); + } + } + + if (isset($_FILES['manager_signature']) && $_FILES['manager_signature']['error'] === UPLOAD_ERR_OK) { + $ext = pathinfo($_FILES['manager_signature']['name'], PATHINFO_EXTENSION); + $filename = 'signature.' . $ext; + $target = 'uploads/' . $filename; + if (!is_dir('uploads')) mkdir('uploads', 0775, true); + if (move_uploaded_file($_FILES['manager_signature']['tmp_name'], $target)) { + $stmt = db()->prepare("INSERT INTO settings (`key`, `value`) VALUES ('manager_signature', ?) ON DUPLICATE KEY UPDATE `value` = ?"); + $stmt->execute([$target, $target]); + } + } + $message = "Settings updated successfully!"; + } } // Routing & Data Fetching @@ -351,6 +542,12 @@ $data['categories'] = db()->query("SELECT * FROM stock_categories ORDER BY name_ $data['units'] = db()->query("SELECT * FROM stock_units ORDER BY name_en ASC")->fetchAll(); $data['suppliers'] = db()->query("SELECT * FROM customers WHERE type = 'supplier' ORDER BY name ASC")->fetchAll(); +$settings_raw = db()->query("SELECT * FROM settings")->fetchAll(); +$data['settings'] = []; +foreach ($settings_raw as $s) { + $data['settings'][$s['key']] = $s['value']; +} + switch ($page) { case 'suppliers': $data['customers'] = db()->query("SELECT * FROM customers WHERE type = 'supplier' ORDER BY id DESC")->fetchAll(); @@ -375,6 +572,9 @@ switch ($page) { case 'payment_methods': $data['payment_methods'] = db()->query("SELECT * FROM payment_methods ORDER BY id DESC")->fetchAll(); break; + case 'settings': + // Already fetched globally + break; case 'sales': case 'purchases': $type = ($page === 'sales') ? 'sale' : 'purchase'; @@ -383,7 +583,7 @@ switch ($page) { LEFT JOIN customers c ON v.customer_id = c.id WHERE v.type = '$type' ORDER BY v.id DESC")->fetchAll(); - $data['items_list'] = db()->query("SELECT id, name_en, name_ar, sale_price, purchase_price, stock_quantity FROM stock_items ORDER BY name_en ASC")->fetchAll(); + $data['items_list'] = db()->query("SELECT id, name_en, name_ar, sale_price, purchase_price, stock_quantity, vat_rate FROM stock_items ORDER BY name_en ASC")->fetchAll(); $data['customers_list'] = db()->query("SELECT id, name FROM customers WHERE type = '" . ($type === 'sale' ? 'customer' : 'supplier') . "' ORDER BY name ASC")->fetchAll(); break; default: @@ -405,6 +605,9 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; Accounting Admin + + + @@ -494,6 +697,9 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; Payment Methods + + Company Profile + HR @@ -514,6 +720,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; 'payment_methods' => ['en' => 'Payment Methods', 'ar' => 'طرق الدفع'], 'sales' => ['en' => 'Sales Invoices', 'ar' => 'فواتير المبيعات'], 'purchases' => ['en' => 'Purchase Invoices', 'ar' => 'فواتير المشتريات'], + 'settings' => ['en' => 'Company Profile', 'ar' => 'ملف الشركة'], ]; $currTitle = $titles[$page] ?? $titles['dashboard']; ?> @@ -528,7 +735,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; Admin @@ -807,6 +1014,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; Supplier Stock Level Expiry + VAT Actions @@ -839,6 +1047,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; + %
@@ -922,7 +1131,11 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
-
+
+ + +
+
> @@ -967,6 +1180,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
Purchase Price $
Sale Price $
Stock
+
VAT Rate %
Min Stock
Expiry Date
@@ -1004,7 +1218,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; prepare("SELECT ii.*, i.name_en, i.name_ar + $items = db()->prepare("SELECT ii.*, i.name_en, i.name_ar, i.vat_rate FROM invoice_items ii JOIN stock_items i ON ii.item_id = i.id WHERE ii.invoice_id = ?"); @@ -1017,11 +1231,22 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; $ - +
+ + +
+ + +
+
@@ -1089,7 +1314,17 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; - Grand Total + Subtotal + $0.00 + + + + VAT Amount + $0.00 + + + + Grand Total (Inc. VAT) $0.00 @@ -1106,7 +1341,91 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
- + +