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';
| Total | +Subtotal | ++ | ||||
| VAT Amount | ++ | |||||
| Total (Inc. VAT) | ||||||
Thank you for your business!
+ += nl2br(htmlspecialchars($data['settings']['invoice_footer'] ?? 'Thank you for your business!')) ?>