add purchase return

This commit is contained in:
Flatlogic Bot 2026-02-17 16:53:00 +00:00
parent bad7441af2
commit dbea931b59
2 changed files with 384 additions and 27 deletions

View File

@ -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
);

388
index.php
View File

@ -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';
<a href="index.php?page=purchases" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'purchases' ? 'active' : '' ?>">
<i class="bi bi-bag"></i> <span data-en="Purchase Tax Invoices" data-ar="فواتير المشتريات الضريبية">Purchase Tax Invoices</span>
</a>
<a href="index.php?page=purchase_returns" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'purchase_returns' ? 'active' : '' ?>">
<i class="bi bi-arrow-return-right"></i> <span data-en="Purchase Returns" data-ar="مرتجع المشتريات">Purchase Returns</span>
</a>
<a href="index.php?page=quotations" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'quotations' ? 'active' : '' ?>">
<i class="bi bi-file-earmark-text"></i> <span data-en="Quotations" data-ar="العروض">Quotations</span>
</a>
@ -3322,7 +3440,8 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
<td class="text-end text-danger fw-bold">OMR <?= number_format((float)($inv['total_with_vat'] - $inv['paid_amount']), 3) ?></td>
<td class="text-end">
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-info view-invoice-btn" data-json="<?= htmlspecialchars(json_encode($inv)) ?>" title="View"><i class="bi bi-eye"></i></button>
<button class="btn btn-outline-info view-invoice-btn" data-json="<?= htmlspecialchars(json_encode($inv)) ?>" title="View"><i class="bi bi-eye"></i></button>
<button class="btn btn-outline-warning return-invoice-btn" data-id="<?= $inv['id'] ?>" data-bs-toggle="modal" data-bs-target="<?= $page === 'sales' ? '#addSalesReturnModal' : '#addPurchaseReturnModal' ?>" title="Return"><i class="bi bi-arrow-return-left"></i></button>
<button class="btn btn-outline-primary edit-invoice-btn" data-json="<?= htmlspecialchars(json_encode($inv)) ?>" data-bs-toggle="modal" data-bs-target="#editInvoiceModal" title="Edit"><i class="bi bi-pencil"></i></button>
<?php if ($inv['status'] !== 'paid'): ?>
<button class="btn btn-outline-success pay-invoice-btn" data-id="<?= $inv['id'] ?>" data-total="<?= $inv['total_with_vat'] ?>" data-paid="<?= $inv['paid_amount'] ?>" data-bs-toggle="modal" data-bs-target="#payInvoiceModal" title="Payment"><i class="bi bi-cash-coin"></i></button>
@ -3889,6 +4008,62 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
</div>
</div>
<?php elseif ($page === 'purchase_returns'): ?>
<div class="card p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="m-0" data-en="Purchase Returns" data-ar="مرتجع المشتريات">Purchase Returns</h5>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addPurchaseReturnModal">
<i class="bi bi-plus-lg"></i> <span data-en="Create New Return" data-ar="إنشاء مرتجع جديد">Create New Return</span>
</button>
</div>
<div class="bg-light p-3 rounded mb-4">
<form method="GET" class="row g-3">
<input type="hidden" name="page" value="purchase_returns">
<div class="col-md-9">
<input type="text" name="search" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['search'] ?? '') ?>" placeholder="Search by Return ID, Supplier or Invoice ID...">
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-primary btn-sm w-100">Filter</button>
</div>
</form>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th data-en="Return #" data-ar="رقم المرتجع">Return #</th>
<th data-en="Date" data-ar="التاريخ">Date</th>
<th data-en="Invoice #" data-ar="رقم الفاتورة">Invoice #</th>
<th data-en="Supplier" data-ar="المورد">Supplier</th>
<th data-en="Total Amount" data-ar="إجمالي المرتجع" class="text-end">Total Amount</th>
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($data['returns'] as $ret): ?>
<tr>
<td>PRET-<?= str_pad((string)$ret['id'], 5, '0', STR_PAD_LEFT) ?></td>
<td><?= $ret['return_date'] ?></td>
<td>INV-<?= str_pad((string)$ret['invoice_id'], 5, '0', STR_PAD_LEFT) ?></td>
<td><?= htmlspecialchars($ret['supplier_name'] ?? 'Unknown') ?></td>
<td class="text-end fw-bold text-danger">OMR <?= number_format((float)$ret['total_amount'], 3) ?></td>
<td class="text-end">
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-info view-return-btn" data-id="<?= $ret['id'] ?>"><i class="bi bi-eye"></i></button>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($data['returns'])): ?>
<tr><td colspan="6" class="text-center py-4 text-muted">No returns found</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php elseif ($page === 'settings'): ?>
<div class="card p-4">
<h5 class="mb-4" data-en="Company Profile" data-ar="ملف الشركة">Company Profile</h5>
@ -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 = '<?= $page === "purchase_returns" ? "purchase" : "sale" ?>';
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 += `
<tr>
<td>${item.name_en}<br><small class="text-muted">${item.sku}</small></td>
<td class="text-center">${parseFloat(item.quantity).toFixed(2)}</td>
<td class="text-end">${parseFloat(item.unit_price).toFixed(3)}</td>
<td class="text-end">${parseFloat(item.total_price).toFixed(3)}</td>
</tr>
`;
});
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); }
});
</script>
<?php if ($page === 'sales_returns'): ?>
<!-- View Return Details Modal -->
<div class="modal fade" id="viewReturnDetailsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content text-start border-0 shadow">
<div class="modal-header">
<h5 class="modal-title">Return Details <span id="view_return_no"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row mb-4">
<div class="col-6">
<label class="text-muted small d-block">Party Name</label>
<div id="view_return_party" class="fw-bold"></div>
</div>
<div class="col-3">
<label class="text-muted small d-block">Return Date</label>
<div id="view_return_date"></div>
</div>
<div class="col-3">
<label class="text-muted small d-block">Against Invoice</label>
<div id="view_return_invoice"></div>
</div>
</div>
<h6>Returned Items</h6>
<div class="table-responsive">
<table class="table table-sm table-bordered">
<thead class="bg-light">
<tr>
<th>Item</th>
<th class="text-center">Returned Qty</th>
<th class="text-end">Unit Price</th>
<th class="text-end">Total Price</th>
</tr>
</thead>
<tbody id="view_return_items_tbody"></tbody>
<tfoot>
<tr>
<th colspan="3" class="text-end">Total Amount:</th>
<th class="text-end fw-bold" id="view_return_total"></th>
</tr>
</tfoot>
</table>
</div>
<div class="mt-3">
<label class="text-muted small d-block">Notes</label>
<div id="view_return_notes" class="p-2 bg-light rounded"></div>
</div>
</div>
</div>
</div>
</div>
<?php if ($page === 'sales_returns' || $page === 'purchase_returns'): ?>
<div class="modal fade" id="addPurchaseReturnModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content text-start border-0 shadow">
<div class="modal-header">
<h5 class="modal-title">Process Purchase Return</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<div class="modal-body">
<div class="row g-3 mb-4 text-start">
<div class="col-md-6">
<label class="form-label" data-en="Select Invoice" data-ar="اختر الفاتورة">Select Invoice</label>
<select name="invoice_id" id="purchase_return_invoice_select" class="form-select select2" required>
<option value="">Choose Invoice...</option>
<?php if (!empty($data['purchase_invoices'])): ?>
<?php foreach ($data['purchase_invoices'] as $inv): ?>
<option value="<?= $inv['id'] ?>">INV-<?= str_pad((string)$inv['id'], 5, '0', STR_PAD_LEFT) ?> (<?= $inv['invoice_date'] ?>) - OMR <?= number_format((float)$inv['total_with_vat'], 3) ?></option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Return Date</label>
<input type="date" name="return_date" class="form-control" value="<?= date('Y-m-d') ?>" required>
</div>
</div>
<div id="purchase_return_items_container" style="display:none;" class="text-start">
<h6>Items for Return</h6>
<div class="table-responsive">
<table class="table table-sm table-bordered">
<thead class="bg-light">
<tr>
<th>Item</th>
<th>Purchased Qty</th>
<th>Return Qty</th>
<th>Price</th>
<th>Total</th>
</tr>
</thead>
<tbody id="purchase_return_items_tbody"></tbody>
<tfoot>
<tr>
<th colspan="4" class="text-end">Total Return Amount:</th>
<th class="text-end" id="purchase_return_total_display">OMR 0.000</th>
</tr>
</tfoot>
</table>
</div>
<div class="mb-3">
<label class="form-label">Notes / Reason for Return</label>
<textarea name="notes" class="form-control" rows="2"></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
<button type="submit" name="add_purchase_return" class="btn btn-danger" id="purchase_submit_return_btn" disabled>Process Return</button>
</div>
</form>
</div>
</div>
</div>
<!-- Add Sales Return Modal -->
<div class="modal fade" id="addSalesReturnModal" tabindex="-1">
<div class="modal-dialog modal-lg">