From dbea931b5926eb2a2932e97f2b56f61e07b78ec4 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 17 Feb 2026 16:53:00 +0000 Subject: [PATCH] add purchase return --- db/migrations/20260217_purchase_returns.sql | 23 ++ index.php | 388 ++++++++++++++++++-- 2 files changed, 384 insertions(+), 27 deletions(-) create mode 100644 db/migrations/20260217_purchase_returns.sql diff --git a/db/migrations/20260217_purchase_returns.sql b/db/migrations/20260217_purchase_returns.sql new file mode 100644 index 0000000..2a6b9c9 --- /dev/null +++ b/db/migrations/20260217_purchase_returns.sql @@ -0,0 +1,23 @@ +-- Migration: Add Purchase Returns tables +CREATE TABLE IF NOT EXISTS purchase_returns ( + id INT AUTO_INCREMENT PRIMARY KEY, + invoice_id INT NOT NULL, + supplier_id INT NULL, + return_date DATE NOT NULL, + total_amount DECIMAL(15, 3) NOT NULL DEFAULT 0, + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE, + FOREIGN KEY (supplier_id) REFERENCES customers(id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS purchase_return_items ( + id INT AUTO_INCREMENT PRIMARY KEY, + return_id INT NOT NULL, + item_id INT NOT NULL, + quantity DECIMAL(15, 2) NOT NULL, + unit_price DECIMAL(15, 3) NOT NULL, + total_price DECIMAL(15, 3) NOT NULL, + FOREIGN KEY (return_id) REFERENCES purchase_returns(id) ON DELETE CASCADE, + FOREIGN KEY (item_id) REFERENCES stock_items(id) ON DELETE CASCADE +); diff --git a/index.php b/index.php index 9a68a77..d6f2da0 100644 --- a/index.php +++ b/index.php @@ -115,6 +115,34 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['action'])) { echo json_encode($stmt->fetchAll()); exit; } + if ($_GET['action'] === 'get_return_details') { + header('Content-Type: application/json'); + $return_id = (int)$_GET['return_id']; + $type = $_GET['type'] ?? 'sale'; // 'sale' or 'purchase' + + $table = ($type === 'purchase') ? 'purchase_returns' : 'sales_returns'; + $item_table = ($type === 'purchase') ? 'purchase_return_items' : 'sales_return_items'; + + $stmt = db()->prepare("SELECT r.*, c.name as party_name, i.invoice_date as original_invoice_date + FROM $table r + LEFT JOIN customers c ON " . ($type === 'purchase' ? 'r.supplier_id' : 'r.customer_id') . " = c.id + LEFT JOIN invoices i ON r.invoice_id = i.id + WHERE r.id = ?"); + $stmt->execute([$return_id]); + $return = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($return) { + $stmt = db()->prepare("SELECT ri.*, si.name_en, si.name_ar, si.sku + FROM $item_table ri + JOIN stock_items si ON ri.item_id = si.id + WHERE ri.return_id = ?"); + $stmt->execute([$return_id]); + $return['items'] = $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + echo json_encode($return); + exit; + } } if ($_SERVER['REQUEST_METHOD'] === 'POST') { @@ -1123,6 +1151,72 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } } } + + if (isset($_POST['add_purchase_return'])) { + $invoice_id = (int)$_POST['invoice_id']; + $return_date = $_POST['return_date'] ?: date('Y-m-d'); + $notes = $_POST['notes'] ?? ''; + $item_ids = $_POST['item_ids'] ?? []; + $quantities = $_POST['quantities'] ?? []; + $prices = $_POST['prices'] ?? []; + + if ($invoice_id && !empty($item_ids)) { + $db = db(); + $db->beginTransaction(); + try { + // Get supplier ID from invoice + $stmt = $db->prepare("SELECT customer_id FROM invoices WHERE id = ?"); + $stmt->execute([$invoice_id]); + $supplier_id = $stmt->fetchColumn(); + + $total_return_amount = 0; + $items_data = []; + foreach ($item_ids as $index => $item_id) { + $qty = (float)$quantities[$index]; + $price = (float)$prices[$index]; + if ($qty <= 0) continue; + + $line_total = $qty * $price; + $total_return_amount += $line_total; + + $items_data[] = [ + 'id' => $item_id, + 'qty' => $qty, + 'price' => $price, + 'total' => $line_total + ]; + } + + if (empty($items_data)) throw new Exception("No items to return"); + + // Create Purchase Return record + $stmt = $db->prepare("INSERT INTO purchase_returns (invoice_id, supplier_id, return_date, total_amount, notes) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$invoice_id, $supplier_id, $return_date, $total_return_amount, $notes]); + $return_id = $db->lastInsertId(); + + foreach ($items_data as $item) { + $stmt = $db->prepare("INSERT INTO purchase_return_items (return_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$return_id, $item['id'], $item['qty'], $item['price'], $item['total']]); + + // Update stock (decrease) + $stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?"); + $stmt->execute([$item['qty'], $item['id']]); + } + + // Reduce debt to supplier + if ($supplier_id) { + $stmt = $db->prepare("UPDATE customers SET balance = balance + ? WHERE id = ?"); + $stmt->execute([$total_return_amount, $supplier_id]); + } + + $db->commit(); + $message = "Purchase Return #$return_id processed successfully!"; + } catch (Exception $e) { + if (isset($db)) $db->rollBack(); + $message = "Error: " . $e->getMessage(); + } + } + } } @@ -1350,6 +1444,27 @@ switch ($page) { $data['sales_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM invoices WHERE type = 'sale' ORDER BY id DESC")->fetchAll(); break; + case 'purchase_returns': + $where = ["1=1"]; + $params = []; + if (!empty($_GET['search'])) { + $where[] = "(pr.id LIKE ? OR c.name LIKE ? OR pr.invoice_id LIKE ?)"; + $params[] = "%{$_GET['search']}%"; + $params[] = "%{$_GET['search']}%"; + $params[] = "%{$_GET['search']}%"; + } + $whereSql = implode(" AND ", $where); + $stmt = db()->prepare("SELECT pr.*, c.name as supplier_name, i.total_with_vat as invoice_total + FROM purchase_returns pr + LEFT JOIN customers c ON pr.supplier_id = c.id + LEFT JOIN invoices i ON pr.invoice_id = i.id + WHERE $whereSql + ORDER BY pr.id DESC"); + $stmt->execute($params); + $data['returns'] = $stmt->fetchAll(); + $data['purchase_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM invoices WHERE type = 'purchase' ORDER BY id DESC")->fetchAll(); + break; + case 'customer_statement': case 'supplier_statement': $type = ($page === 'customer_statement') ? 'customer' : 'supplier'; @@ -1533,6 +1648,9 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; Purchase Tax Invoices + + Purchase Returns + Quotations @@ -3322,7 +3440,8 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; OMR
- + + @@ -3889,6 +4008,62 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
+ +
+
+
Purchase Returns
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Return #DateInvoice #SupplierTotal AmountActions
PRET-INV-OMR +
+ +
+
No returns found
+
+
+
Company Profile
@@ -4907,13 +5082,14 @@ document.addEventListener('DOMContentLoaded', function() { } }); - // Sales Return Logic - const returnInvoiceSelect = document.getElementById('return_invoice_select'); - - if (returnInvoiceSelect) { - const calculateReturnTotal = function() { + // Return Logic (General for Sales and Purchase) + const setupReturnLogic = (selectId, containerId, tbodyId, totalDisplayId, submitBtnId) => { + const select = document.getElementById(selectId); + if (!select) return; + + const calculateTotal = function() { let total = 0; - document.querySelectorAll('#return_items_tbody tr').forEach(row => { + document.querySelectorAll('#' + tbodyId + ' tr').forEach(row => { const qtyInput = row.querySelector('.return-qty-input'); if (!qtyInput) return; const qty = parseFloat(qtyInput.value) || 0; @@ -4925,21 +5101,21 @@ document.addEventListener('DOMContentLoaded', function() { } total += lineTotal; }); - const totalDisplay = document.getElementById('return_total_display'); + const totalDisplay = document.getElementById(totalDisplayId); if (totalDisplay) { totalDisplay.innerText = 'OMR ' + total.toFixed(3); } - const submitBtn = document.getElementById('submit_return_btn'); + const submitBtn = document.getElementById(submitBtnId); if (submitBtn) { submitBtn.disabled = total <= 0; } }; const handleInvoiceChange = async function() { - const invoiceId = returnInvoiceSelect.value; - const container = document.getElementById('return_items_container'); - const tbody = document.getElementById('return_items_tbody'); - const submitBtn = document.getElementById('submit_return_btn'); + const invoiceId = select.value; + const container = document.getElementById(containerId); + const tbody = document.getElementById(tbodyId); + const submitBtn = document.getElementById(submitBtnId); if (!invoiceId) { if (container) container.style.display = 'none'; @@ -4985,41 +5161,199 @@ document.addEventListener('DOMContentLoaded', function() { if (submitBtn) submitBtn.disabled = true; - // Add event listeners for qty changes const qtyInputs = tbody.querySelectorAll('.return-qty-input'); qtyInputs.forEach(input => { - input.addEventListener('input', calculateReturnTotal); - input.addEventListener('change', calculateReturnTotal); - input.addEventListener('keyup', calculateReturnTotal); + ['input', 'change', 'keyup'].forEach(evt => input.addEventListener(evt, calculateTotal)); }); - calculateReturnTotal(); - + calculateTotal(); } catch (e) { console.error(e); if (window.Swal) Swal.fire('Error', 'Failed to fetch invoice items', 'error'); } }; - returnInvoiceSelect.addEventListener('change', handleInvoiceChange); - // Also support Select2 + select.addEventListener('change', handleInvoiceChange); if (window.jQuery && jQuery.fn.select2) { - $(returnInvoiceSelect).on('select2:select', handleInvoiceChange); - $(returnInvoiceSelect).on('change', handleInvoiceChange); + $(select).on('select2:select change', handleInvoiceChange); } - } + }; + + setupReturnLogic('return_invoice_select', 'return_items_container', 'return_items_tbody', 'return_total_display', 'submit_return_btn'); + setupReturnLogic('purchase_return_invoice_select', 'purchase_return_items_container', 'purchase_return_items_tbody', 'purchase_return_total_display', 'purchase_submit_return_btn'); + + // Return Invoice Button from Sales/Purchases list + document.querySelectorAll('.return-invoice-btn').forEach(btn => { + btn.addEventListener('click', function() { + const invoiceId = this.dataset.id; + const targetModal = this.dataset.bsTarget; + const selectId = targetModal === '#addSalesReturnModal' ? 'return_invoice_select' : 'purchase_return_invoice_select'; + const select = document.getElementById(selectId); + if (select) { + $(select).val(invoiceId).trigger('change'); + } + }); + }); // View Return Logic document.querySelectorAll('.view-return-btn').forEach(btn => { - btn.addEventListener('click', function() { - Swal.fire('Info', 'View Return Details functionality is coming soon!', 'info'); + btn.addEventListener('click', async function() { + const returnId = this.dataset.id; + const type = ''; + const modal = new bootstrap.Modal(document.getElementById('viewReturnDetailsModal')); + + try { + const resp = await fetch(`index.php?action=get_return_details&return_id=${returnId}&type=${type}`); + const data = await resp.json(); + + if (data) { + document.getElementById('view_return_no').innerText = (type === 'purchase' ? 'PRET-' : 'SRET-') + String(data.id).padStart(5, '0'); + document.getElementById('view_return_party').innerText = data.party_name; + document.getElementById('view_return_date').innerText = data.return_date; + document.getElementById('view_return_invoice').innerText = 'INV-' + String(data.invoice_id).padStart(5, '0'); + document.getElementById('view_return_total').innerText = 'OMR ' + parseFloat(data.total_amount).toFixed(3); + document.getElementById('view_return_notes').innerText = data.notes || 'No notes'; + + let itemsHtml = ''; + data.items.forEach(item => { + itemsHtml += ` + + ${item.name_en}
${item.sku} + ${parseFloat(item.quantity).toFixed(2)} + ${parseFloat(item.unit_price).toFixed(3)} + ${parseFloat(item.total_price).toFixed(3)} + + `; + }); + document.getElementById('view_return_items_tbody').innerHTML = itemsHtml; + modal.show(); + } + } catch (e) { + console.error(e); + Swal.fire('Error', 'Failed to fetch return details', 'error'); + } }); }); } catch (e) { console.error("JS Error in DOMContentLoaded:", e); } }); - + + + + +