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 -
+
Point of Sale @@ -1120,6 +1294,9 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; Purchase Tax Invoices + + Quotations + Expenses @@ -1196,6 +1373,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; $titles = [ 'dashboard' => ['en' => 'Dashboard', 'ar' => 'لوحة القيادة'], 'pos' => ['en' => 'Point of Sale', 'ar' => 'نقطة البيع'], + 'quotations' => ['en' => 'Quotations', 'ar' => 'العروض'], 'customers' => ['en' => 'Customers', 'ar' => 'العملاء'], 'suppliers' => ['en' => 'Suppliers', 'ar' => 'الموردون'], 'categories' => ['en' => 'Stock Categories', 'ar' => 'فئات المخزون'], @@ -2528,6 +2706,109 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; }); + +
+
+
Quotations
+ +
+ + +
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + Clear + +
+
+
+
+ + + + + + + + + + + + + + prepare("SELECT qi.*, i.name_en, i.name_ar, i.vat_rate + FROM quotation_items qi + JOIN stock_items i ON qi.item_id = i.id + WHERE qi.quotation_id = ?"); + $items->execute([$q['id']]); + $q['items'] = $items->fetchAll(PDO::FETCH_ASSOC); + ?> + + + + + + + + + + + + + + +
Quotation #DateValid UntilCustomerStatusTotalActions
QUO- + + + OMR +
+ + + + + + + +
+
No quotations found
+
+
+
@@ -3470,10 +3751,175 @@ document.addEventListener('DOMContentLoaded', function() { }); } - const invoiceType = ''; + const invoiceType = ''; initInvoiceForm('productSearchInput', 'searchSuggestions', 'invoiceItemsTableBody', 'grandTotal', 'subtotal', 'totalVat'); initInvoiceForm('editProductSearchInput', 'editSearchSuggestions', 'editInvoiceItemsTableBody', 'edit_grandTotal', 'edit_subtotal', 'edit_totalVat'); + // Quotation Form Logic + initInvoiceForm('quotProductSearchInput', 'quotSearchSuggestions', 'quotItemsTableBody', 'quot_grand_display', 'quot_subtotal_display', 'quot_vat_display'); + initInvoiceForm('editQuotProductSearchInput', 'editQuotSearchSuggestions', 'editQuotItemsTableBody', 'edit_quot_grand_display', 'edit_quot_subtotal_display', 'edit_quot_vat_display'); + + document.querySelectorAll('.edit-quotation-btn').forEach(btn => { + btn.addEventListener('click', function() { + const data = JSON.parse(this.dataset.json); + document.getElementById('edit_quotation_id').value = data.id; + document.getElementById('edit_quot_customer_id').value = data.customer_id; + document.getElementById('edit_quot_date').value = data.quotation_date; + document.getElementById('edit_quot_valid').value = data.valid_until || ''; + document.getElementById('edit_quot_status').value = data.status || 'pending'; + + const tableBody = document.getElementById('editQuotItemsTableBody'); + tableBody.innerHTML = ''; + + data.items.forEach(item => { + addItemToTable({ + id: item.item_id, + name_en: item.name_en, + name_ar: item.name_ar, + sku: '', + vat_rate: item.vat_rate || 0 + }, tableBody, null, null, + document.getElementById('edit_quot_grand_display'), + document.getElementById('edit_quot_subtotal_display'), + document.getElementById('edit_quot_vat_display'), + { quantity: item.quantity, unit_price: item.unit_price }); + }); + }); + }); + + document.querySelectorAll('.convert-quotation-btn').forEach(btn => { + btn.addEventListener('click', function() { + if (confirm('Convert this quotation to an invoice? This will reduce stock.')) { + const f = document.createElement('form'); + f.method = 'POST'; + f.innerHTML = ``; + document.body.appendChild(f); + f.submit(); + } + }); + }); + + // View Quotation Logic + window.viewAndPrintQuotation = function(data, autoPrint = false) { + const modal = new bootstrap.Modal(document.getElementById('viewQuotationModal')); + const content = document.getElementById('quotationPrintableArea'); + + let itemsHtml = ''; + data.items.forEach((item, index) => { + itemsHtml += ` + + ${index + 1} + ${item.name_en}
${item.name_ar} + ${item.quantity} + ${parseFloat(item.unit_price).toFixed(3)} + ${item.vat_rate}% + ${parseFloat(item.total_price).toFixed(3)} + + `; + }); + + content.innerHTML = ` +
+
+
+

QUOTATION

+

No: QUO-${data.id.toString().padStart(5, '0')}

+

Date: ${data.quotation_date}

+

Valid Until: ${data.valid_until || 'N/A'}

+
+
+

${data.customer_name || 'Walk-in Customer'}

+

Status: ${data.status.toUpperCase()}

+
+
+ + + + + + + + + + + + ${itemsHtml} + + + + + + + + + + + + + + +
#Item DescriptionQtyUnit PriceVATTotal
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:

+
    +
  • Quotation is valid until the date mentioned above.
  • +
  • Prices are inclusive of VAT where applicable.
  • +
+
+
+
Authorized Signature
+
+
+
+
+ `; + + const actionButtons = document.getElementById('quotationActionButtons'); + actionButtons.innerHTML = ''; + if (data.status === 'pending') { + const convertBtn = document.createElement('button'); + convertBtn.className = 'btn btn-success me-2'; + convertBtn.innerHTML = ' Convert to Invoice'; + convertBtn.onclick = function() { + if (confirm('Convert this quotation to an invoice?')) { + const f = document.createElement('form'); + f.method = 'POST'; + f.innerHTML = ``; + document.body.appendChild(f); + f.submit(); + } + }; + actionButtons.appendChild(convertBtn); + + const editBtn = document.createElement('button'); + editBtn.className = 'btn btn-primary'; + editBtn.innerHTML = ' Edit'; + editBtn.onclick = function() { + const editModal = new bootstrap.Modal(document.getElementById('editQuotationModal')); + modal.hide(); + // Trigger the existing edit button click logic or manually populate + const originalEditBtn = document.querySelector(`.edit-quotation-btn[data-json*='"id":${data.id},']`) || + document.querySelector(`.edit-quotation-btn[data-json*='"id":${data.id}']`); + if (originalEditBtn) originalEditBtn.click(); + }; + actionButtons.appendChild(editBtn); + } + + modal.show(); + if (autoPrint) { + setTimeout(() => { window.print(); }, 500); + } + }; + + document.querySelectorAll('.view-quotation-btn').forEach(btn => { + btn.addEventListener('click', function() { + const data = JSON.parse(this.dataset.json); + window.viewAndPrintQuotation(data, false); + }); + }); + // Edit Invoice Logic document.querySelectorAll('.edit-invoice-btn').forEach(btn => { btn.addEventListener('click', function() { @@ -3747,6 +4193,194 @@ document.addEventListener('DOMContentLoaded', function() {
+ + + + + + + + +