diff --git a/db/migrations/20260216_add_quotations.sql b/db/migrations/20260216_add_quotations.sql new file mode 100644 index 0000000..bae92d1 --- /dev/null +++ b/db/migrations/20260216_add_quotations.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS quotations ( + id INT AUTO_INCREMENT PRIMARY KEY, + customer_id INT, + quotation_date DATE NOT NULL, + valid_until DATE, + status ENUM('pending', 'converted', 'expired', 'cancelled') DEFAULT 'pending', + total_amount DECIMAL(15,3) DEFAULT 0.000, + vat_amount DECIMAL(15,3) DEFAULT 0.000, + total_with_vat DECIMAL(15,3) DEFAULT 0.000, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS quotation_items ( + id INT AUTO_INCREMENT PRIMARY KEY, + quotation_id INT NOT NULL, + item_id INT NOT NULL, + quantity DECIMAL(15,2) NOT NULL, + unit_price DECIMAL(15,3) DEFAULT 0.000, + total_price DECIMAL(15,3) DEFAULT 0.000, + FOREIGN KEY (quotation_id) REFERENCES quotations(id) ON DELETE CASCADE, + FOREIGN KEY (item_id) REFERENCES stock_items(id) +); diff --git a/index.php b/index.php index 70f9f01..500bba7 100644 --- a/index.php +++ b/index.php @@ -721,6 +721,149 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } } + if (isset($_POST['add_quotation'])) { + $customer_id = $_POST['customer_id'] ?: null; + $quotation_date = $_POST['quotation_date'] ?: date('Y-m-d'); + $valid_until = $_POST['valid_until'] ?: null; + $item_ids = $_POST['item_ids'] ?? []; + $quantities = $_POST['quantities'] ?? []; + $prices = $_POST['prices'] ?? []; + + if (!empty($item_ids)) { + $db = db(); + $db->beginTransaction(); + try { + $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; + $stmt = $db->prepare("INSERT INTO quotations (customer_id, quotation_date, valid_until, total_amount, vat_amount, total_with_vat) VALUES (?, ?, ?, ?, ?, ?)"); + $stmt->execute([$customer_id, $quotation_date, $valid_until, $subtotal, $total_vat, $total_with_vat]); + $quotation_id = $db->lastInsertId(); + foreach ($items_data as $item) { + $stmt = $db->prepare("INSERT INTO quotation_items (quotation_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$quotation_id, $item['id'], $item['qty'], $item['price'], $item['total']]); + } + $db->commit(); + $message = "Quotation #$quotation_id created successfully!"; + } catch (Exception $e) { + $db->rollBack(); + $message = "Error: " . $e->getMessage(); + } + } + } + + if (isset($_POST['edit_quotation'])) { + $quotation_id = (int)$_POST['quotation_id']; + $customer_id = $_POST['customer_id'] ?: null; + $quotation_date = $_POST['quotation_date'] ?: date('Y-m-d'); + $valid_until = $_POST['valid_until'] ?: null; + $status = $_POST['status'] ?? 'pending'; + $item_ids = $_POST['item_ids'] ?? []; + $quantities = $_POST['quantities'] ?? []; + $prices = $_POST['prices'] ?? []; + + if ($quotation_id && !empty($item_ids)) { + $db = db(); + $db->beginTransaction(); + try { + $stmt = $db->prepare("DELETE FROM quotation_items WHERE quotation_id = ?"); + $stmt->execute([$quotation_id]); + $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; + $stmt = $db->prepare("UPDATE quotations SET customer_id = ?, quotation_date = ?, valid_until = ?, status = ?, total_amount = ?, vat_amount = ?, total_with_vat = ? WHERE id = ?"); + $stmt->execute([$customer_id, $quotation_date, $valid_until, $status, $subtotal, $total_vat, $total_with_vat, $quotation_id]); + foreach ($items_data as $item) { + $stmt = $db->prepare("INSERT INTO quotation_items (quotation_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$quotation_id, $item['id'], $item['qty'], $item['price'], $item['total']]); + } + $db->commit(); + $message = "Quotation #$quotation_id updated successfully!"; + } catch (Exception $e) { + $db->rollBack(); + $message = "Error: " . $e->getMessage(); + } + } + } + + if (isset($_POST['delete_quotation'])) { + $id = (int)$_POST['id']; + if ($id) { + $stmt = db()->prepare("DELETE FROM quotations WHERE id = ?"); + $stmt->execute([$id]); + $message = "Quotation deleted successfully!"; + } + } + + if (isset($_POST['convert_to_invoice'])) { + $quotation_id = (int)$_POST['quotation_id']; + if ($quotation_id) { + $db = db(); + $db->beginTransaction(); + try { + $stmt = $db->prepare("SELECT * FROM quotations WHERE id = ?"); + $stmt->execute([$quotation_id]); + $q = $stmt->fetch(); + if (!$q) throw new Exception("Quotation not found"); + if ($q['status'] === 'converted') throw new Exception("Quotation already converted"); + + $stmt = $db->prepare("SELECT * FROM quotation_items WHERE quotation_id = ?"); + $stmt->execute([$quotation_id]); + $items = $stmt->fetchAll(); + + // Create Invoice + $stmt = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, type, status, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, CURDATE(), 'sale', 'unpaid', ?, ?, ?, 0)"); + $stmt->execute([$q['customer_id'], $q['total_amount'], $q['vat_amount'], $q['total_with_vat']]); + $invoice_id = $db->lastInsertId(); + + foreach ($items as $item) { + $stmt = $db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$invoice_id, $item['item_id'], $item['quantity'], $item['unit_price'], $item['total_price']]); + + // Update stock + $stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?"); + $stmt->execute([$item['quantity'], $item['item_id']]); + } + + // Update quotation status + $stmt = $db->prepare("UPDATE quotations SET status = 'converted' WHERE id = ?"); + $stmt->execute([$quotation_id]); + + $db->commit(); + $message = "Quotation converted to Invoice #$invoice_id successfully!"; + } catch (Exception $e) { + $db->rollBack(); + $message = "Error: " . $e->getMessage(); + } + } + } + if (isset($_POST['add_payment_method'])) { $name_en = $_POST['name_en'] ?? ''; $name_ar = $_POST['name_ar'] ?? ''; @@ -961,6 +1104,37 @@ switch ($page) { $stmt->execute($params); $data['items'] = $stmt->fetchAll(); break; + case 'quotations': + $where = ["1=1"]; + $params = []; + if (!empty($_GET['search'])) { + $where[] = "(q.id LIKE ? OR c.name LIKE ?)"; + $params[] = "%{$_GET['search']}%"; + $params[] = "%{$_GET['search']}%"; + } + if (!empty($_GET['customer_id'])) { + $where[] = "q.customer_id = ?"; + $params[] = $_GET['customer_id']; + } + if (!empty($_GET['start_date'])) { + $where[] = "q.quotation_date >= ?"; + $params[] = $_GET['start_date']; + } + if (!empty($_GET['end_date'])) { + $where[] = "q.quotation_date <= ?"; + $params[] = $_GET['end_date']; + } + $whereSql = implode(" AND ", $where); + $stmt = db()->prepare("SELECT q.*, c.name as customer_name + FROM quotations q + LEFT JOIN customers c ON q.customer_id = c.id + WHERE $whereSql + ORDER BY q.id DESC"); + $stmt->execute($params); + $data['quotations'] = $stmt->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 = 'customer' ORDER BY name ASC")->fetchAll(); + break; case 'payment_methods': $data['payment_methods'] = db()->query("SELECT * FROM payment_methods ORDER BY id DESC")->fetchAll(); break; @@ -1110,7 +1284,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; Operations -
| Quotation # | +Date | +Valid Until | +Customer | +Status | +Total | +Actions | +
|---|---|---|---|---|---|---|
| QUO-= str_pad((string)$q['id'], 5, '0', STR_PAD_LEFT) ?> | += $q['quotation_date'] ?> | += $q['valid_until'] ?: '---' ?> | += htmlspecialchars($q['customer_name'] ?? '---') ?> | ++ + = htmlspecialchars($q['status']) ?> + | +OMR = number_format((float)$q['total_with_vat'], 3) ?> | +
+
+
+
+
+
+
+
+
+
+ |
+
| No quotations found | ||||||
No: QUO-${data.id.toString().padStart(5, '0')}
+Date: ${data.quotation_date}
+Valid Until: ${data.valid_until || 'N/A'}
+Status: ${data.status.toUpperCase()}
+| # | +Item Description | +Qty | +Unit Price | +VAT | +Total | +
|---|---|---|---|---|---|
| Subtotal | +${parseFloat(data.total_amount).toFixed(3)} | +||||
| VAT Amount | +${parseFloat(data.vat_amount).toFixed(3)} | +||||
| Grand Total (OMR) | +${parseFloat(data.total_with_vat).toFixed(3)} | +||||
Terms & Conditions:
+