accounting workflows
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require_role($user_role, ['owner', 'admin', 'staff']);
|
||||
require '../../../assets/utils/classes/SupplierReturnManager.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
|
||||
$return_id = (int)($data['id'] ?? 0);
|
||||
|
||||
if (!$return_id) {
|
||||
$answer['message'] = 'Invalid ID.';
|
||||
http_response_code(400);
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
try {
|
||||
dbTransaction($pdo2, function($pdo) use ($return_id, $company_id, $logging) {
|
||||
$mgr = new SupplierReturnManager($pdo, $company_id);
|
||||
$whMgmt = new WarehouseManager($pdo, $company_id);
|
||||
$mgr->cancelReturn($return_id, $logging, $whMgmt);
|
||||
});
|
||||
|
||||
$answer['success'] = 1;
|
||||
$answer['message'] = 'Supplier return cancelled.';
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer['message'] = 'Database error, please try again.';
|
||||
http_response_code(500);
|
||||
} catch (Exception $e) {
|
||||
$answer['message'] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require_role($user_role, ['owner', 'admin', 'staff']);
|
||||
require '../../../assets/utils/classes/PurchaseOrderManager.php';
|
||||
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
|
||||
if (!$id) {
|
||||
$answer['message'] = 'Invalid PO ID.';
|
||||
http_response_code(400);
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
try {
|
||||
dbTransaction($pdo2, function($pdo) use ($id, $company_id, $logging) {
|
||||
$po = new PurchaseOrderManager($pdo, $company_id);
|
||||
$po->closePo($id, $logging);
|
||||
});
|
||||
|
||||
$answer['success'] = 1;
|
||||
$answer['message'] = 'Purchase order closed. No further receipts will be accepted.';
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer['message'] = 'Database error, please try again.';
|
||||
http_response_code(500);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$answer['message'] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require_role($user_role, ['owner', 'admin', 'staff']);
|
||||
require '../../../assets/utils/classes/SupplierReturnManager.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
require '../../../assets/utils/classes/InvoiceManager.php';
|
||||
require '../../../assets/utils/classes/CompanySettingManager.php';
|
||||
|
||||
$return_id = (int)($data['id'] ?? 0);
|
||||
|
||||
if (!$return_id) {
|
||||
$answer['message'] = 'Invalid ID.';
|
||||
http_response_code(400);
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$csm = new CompanySettingManager($pdo1, $company_id);
|
||||
$auto_approve = (int)$csm->get('default_stock_status') === 1;
|
||||
$auto_dn = (int)$csm->get('auto_invoice_and_credit_note') === 1;
|
||||
|
||||
try {
|
||||
dbTransaction($pdo2, function($pdo) use ($return_id, $company_id, $logging, $uuid, $auto_approve, $auto_dn) {
|
||||
$mgr = new SupplierReturnManager($pdo, $company_id);
|
||||
$whMgmt = new WarehouseManager($pdo, $company_id);
|
||||
$invMgmt = new InvoiceManager($pdo, $company_id);
|
||||
$mgr->confirmReturn($return_id, $uuid, $logging, $whMgmt, $auto_approve, $invMgmt, $auto_dn);
|
||||
});
|
||||
|
||||
$answer['success'] = 1;
|
||||
$answer['message'] = 'Supplier return confirmed.';
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer['message'] = 'Database error, please try again.';
|
||||
http_response_code(500);
|
||||
} catch (Exception $e) {
|
||||
$answer['message'] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require_role($user_role, ['owner', 'admin', 'staff']);
|
||||
require '../../../assets/utils/classes/SupplierReturnManager.php';
|
||||
|
||||
$mgr = new SupplierReturnManager($pdo2, $company_id);
|
||||
|
||||
try {
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $action, &$answer) {
|
||||
$mgr = new SupplierReturnManager($pdo, $company_id);
|
||||
|
||||
// Tracking-only update for confirmed returns
|
||||
if (($data['_action'] ?? '') === 'update_tracking') {
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
$mgr->updateTracking($id, $data['shipping_tracking_number'] ?? '');
|
||||
$answer['success'] = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
$raw_items = $data['items'] ?? null;
|
||||
$items = is_array($raw_items)
|
||||
? $raw_items
|
||||
: (json_decode($raw_items ?? '[]', true) ?: []);
|
||||
|
||||
$payload = array_merge($data, ['items' => $items]);
|
||||
$new_id = $mgr->saveReturn($payload, $logging);
|
||||
|
||||
$answer['success'] = 1;
|
||||
if ($new_id) $answer['new_id'] = $new_id;
|
||||
});
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer['message'] = 'Database error, please try again.';
|
||||
http_response_code(500);
|
||||
} catch (Exception $e) {
|
||||
$answer['message'] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require_role($user_role, ['owner', 'admin', 'staff']);
|
||||
require '../../../assets/utils/classes/InvoiceManager.php';
|
||||
|
||||
$po_id = (int)($data['po_id'] ?? 0);
|
||||
|
||||
if (!$po_id) {
|
||||
$answer['message'] = 'Invalid PO ID.';
|
||||
http_response_code(400);
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
try {
|
||||
$new_id = null;
|
||||
|
||||
dbTransaction($pdo2, function($pdo) use ($po_id, $company_id, $logging, &$new_id) {
|
||||
$inv = new InvoiceManager($pdo, $company_id);
|
||||
$new_id = $inv->createFromPo($po_id, $logging);
|
||||
});
|
||||
|
||||
$answer['success'] = 1;
|
||||
$answer['new_id'] = $new_id;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer['message'] = 'Database error, please try again.';
|
||||
http_response_code(500);
|
||||
} catch (Exception $e) {
|
||||
$answer['message'] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require_role($user_role, ['owner', 'admin', 'staff']);
|
||||
require '../../../assets/utils/classes/InvoiceManager.php';
|
||||
|
||||
$return_id = (int)($data['return_id'] ?? 0);
|
||||
|
||||
if (!$return_id) {
|
||||
$answer['message'] = 'Invalid return ID.';
|
||||
http_response_code(400);
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
try {
|
||||
$new_id = null;
|
||||
|
||||
dbTransaction($pdo2, function($pdo) use ($return_id, $company_id, $logging, &$new_id) {
|
||||
$inv = new InvoiceManager($pdo, $company_id);
|
||||
$new_id = $inv->createFromSupplierReturn($return_id, $logging);
|
||||
});
|
||||
|
||||
$answer['success'] = 1;
|
||||
$answer['new_id'] = $new_id;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer['message'] = 'Database error, please try again.';
|
||||
http_response_code(500);
|
||||
} catch (Exception $e) {
|
||||
$answer['message'] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -16,23 +16,13 @@
|
||||
}
|
||||
$answer['output'] = $result;
|
||||
|
||||
// Calculate receivable items — PO items minus already received qty
|
||||
$po_items = $result['items'] ?? [];
|
||||
$receivable = [];
|
||||
foreach ($po_items as $i => $item) {
|
||||
$ordered = (float)($item['quantity'] ?? 0);
|
||||
$received = (float)($item['received_qty'] ?? 0);
|
||||
$remaining = $ordered - $received;
|
||||
if ($remaining > 0) {
|
||||
$receivable[] = array_merge($item, [
|
||||
'item_id' => (int)($item['item_id'] ?? $i),
|
||||
'ordered_qty' => $ordered,
|
||||
'received_qty' => $received,
|
||||
'remaining_qty' => $remaining,
|
||||
]);
|
||||
}
|
||||
}
|
||||
$answer['receivable_items'] = $receivable;
|
||||
$answer['receivable_items'] = $po->getReceivableItems($result);
|
||||
$answer['returnable_items'] = $po->getReturnableItems($result);
|
||||
|
||||
// Check for active purchase invoice linked to this PO
|
||||
$active_pi_id = $po->getActivePurchaseInvoiceId($id);
|
||||
$answer['has_active_pi'] = $active_pi_id > 0 ? 1 : 0;
|
||||
$answer['active_pi_id'] = $active_pi_id;
|
||||
|
||||
} else {
|
||||
$answer['output'] = $po->getPoList();
|
||||
@@ -40,4 +30,4 @@
|
||||
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/SupplierReturnManager.php';
|
||||
|
||||
$mgr = new SupplierReturnManager($pdo2, $company_id);
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
|
||||
if ($id > 0) {
|
||||
$result = $mgr->getReturnById($id);
|
||||
if (!$result) {
|
||||
http_response_code(404);
|
||||
$answer['message'] = 'Supplier return not found.';
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$active_dn_id = $mgr->getActiveDebitNoteId($id);
|
||||
$result['has_active_dn'] = $active_dn_id > 0 ? 1 : 0;
|
||||
$result['active_dn_id'] = $active_dn_id;
|
||||
|
||||
$answer['output'] = $result;
|
||||
} else {
|
||||
$po_id = (int)($data['po_id'] ?? 0);
|
||||
$answer['output'] = $mgr->getReturnList($po_id);
|
||||
}
|
||||
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -0,0 +1,279 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../config.php';
|
||||
require '../include_header.php';
|
||||
?>
|
||||
|
||||
<body>
|
||||
<?php require '../include_topbar.php'; ?>
|
||||
<?php require '../include_sidebar.php'; ?>
|
||||
|
||||
<main id="content" class="content py-15">
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row mb-6">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="fs-3 mb-1">Purchase Invoices</h1>
|
||||
<p class="mb-0 text-muted">Supplier invoices and supplier credit notes</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-5 mb-5">
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="card p-4">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="icon-shape icon-md bg-primary bg-opacity-10 text-primary rounded-2">
|
||||
<i class="ti ti-file-invoice fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 text-muted small">Total</p>
|
||||
<h3 class="fw-bold mb-0" id="stat_total">—</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="card p-4">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="icon-shape icon-md bg-warning bg-opacity-10 text-warning rounded-2">
|
||||
<i class="ti ti-pencil fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 text-muted small">Draft</p>
|
||||
<h3 class="fw-bold mb-0" id="stat_draft">—</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="card p-4">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="icon-shape icon-md bg-info bg-opacity-10 text-info rounded-2">
|
||||
<i class="ti ti-send fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 text-muted small">Issued</p>
|
||||
<h3 class="fw-bold mb-0" id="stat_issued">—</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="card p-4">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="icon-shape icon-md bg-danger bg-opacity-10 text-danger rounded-2">
|
||||
<i class="ti ti-clock fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 text-muted small">Overdue</p>
|
||||
<h3 class="fw-bold mb-0" id="stat_overdue">—</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-5">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-lg-4">
|
||||
<div class="position-relative">
|
||||
<input type="text" id="search_input" class="form-control ps-9"
|
||||
placeholder="Search invoice number, PO, or supplier...">
|
||||
<span class="position-absolute top-25 ms-4">
|
||||
<i class="ti ti-search text-muted"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select id="filter_type" class="form-select">
|
||||
<option value="">All Types</option>
|
||||
<option value="purchase_invoice">Purchase Invoice</option>
|
||||
<option value="supplier_credit_note">Supplier Credit Note</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select id="filter_status" class="form-select">
|
||||
<option value="">All Status</option>
|
||||
<option value="0">Draft</option>
|
||||
<option value="1">Issued</option>
|
||||
<option value="2">Paid</option>
|
||||
<option value="4">Void</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select id="filter_dept" class="form-select">
|
||||
<option value="">All Departments</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<button class="btn btn-secondary w-100" onclick="retrieve_invoices()">
|
||||
<i class="ti ti-search me-1"></i>Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-5">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 table-centered" id="invoice_list">
|
||||
<thead class="table-primary border-light">
|
||||
<tr>
|
||||
<th>Invoice #</th>
|
||||
<th>Type</th>
|
||||
<th>Source</th>
|
||||
<th>Date</th>
|
||||
<th>Supplier</th>
|
||||
<th>Department</th>
|
||||
<th>Grand Total</th>
|
||||
<th>Due Date</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
<tfoot></tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php require '../include_ending.php'; ?>
|
||||
|
||||
<script>
|
||||
function doc_type_badge(doc_type) {
|
||||
const map = {
|
||||
'purchase_invoice': '<span class="badge bg-primary">Purchase Invoice</span>',
|
||||
'supplier_credit_note': '<span class="badge bg-info text-white">Supplier Credit Note</span>',
|
||||
};
|
||||
return map[doc_type] ?? '—';
|
||||
}
|
||||
|
||||
function invoice_status_badge(status, due_date) {
|
||||
if (String(status) === '1' && due_date) {
|
||||
var today = new Date().toISOString().slice(0, 10);
|
||||
if (due_date < today) return '<span class="badge bg-danger">Overdue</span>';
|
||||
}
|
||||
const map = {
|
||||
'0': '<span class="badge bg-secondary">Draft</span>',
|
||||
'1': '<span class="badge bg-info text-white">Issued</span>',
|
||||
'2': '<span class="badge bg-success">Paid</span>',
|
||||
'4': '<span class="badge bg-light text-dark">Void</span>',
|
||||
};
|
||||
return map[String(status)] ?? '—';
|
||||
}
|
||||
|
||||
function retrieve_invoices() {
|
||||
return ajax_request({
|
||||
url: '<?php echo $server_url?>order/api/engine/retrieve_invoice.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
data: { doc_type_group: 'purchase' },
|
||||
onSuccess: function(res) {
|
||||
var all = (res.output || []).filter(function(inv) {
|
||||
return inv.doc_type === 'purchase_invoice' || inv.doc_type === 'supplier_credit_note';
|
||||
});
|
||||
|
||||
var keyword = $('#search_input').val().toLowerCase();
|
||||
var fil_type = $('#filter_type').val();
|
||||
var fil_status = $('#filter_status').val();
|
||||
var fil_dept = $('#filter_dept').val();
|
||||
var today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
var data = all.filter(function(inv) {
|
||||
var match_kw = !keyword ||
|
||||
(inv.invoice_number || '').toLowerCase().includes(keyword) ||
|
||||
(inv.contact_name || '').toLowerCase().includes(keyword);
|
||||
var match_type = !fil_type || inv.doc_type === fil_type;
|
||||
var match_st = !fil_status || String(inv.status) === fil_status;
|
||||
var match_dept = !fil_dept || String(inv.department_id) === fil_dept;
|
||||
return match_kw && match_type && match_st && match_dept;
|
||||
});
|
||||
|
||||
$('#stat_total').text(format_number(all.length));
|
||||
$('#stat_draft').text(format_number(all.filter(i => String(i.status) === '0').length));
|
||||
$('#stat_issued').text(format_number(all.filter(i => String(i.status) === '1').length));
|
||||
$('#stat_overdue').text(format_number(all.filter(i =>
|
||||
String(i.status) === '1' && i.due_date && i.due_date < today
|
||||
).length));
|
||||
|
||||
alasql('CREATE TABLE IF NOT EXISTS invoice_list');
|
||||
alasql.tables.invoice_list.data = data;
|
||||
$('table#invoice_list tfoot').html(generate_pagination('invoice_list', data.length));
|
||||
change_page_invoice_list(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function change_page_invoice_list(page_num) {
|
||||
var offset = (page_num - 1) * prop_limit;
|
||||
var rows = alasql(`SELECT * FROM invoice_list LIMIT ${prop_limit} OFFSET ${offset}`);
|
||||
var body = '';
|
||||
|
||||
if (!rows.length) {
|
||||
$('table#invoice_list tbody').html(
|
||||
'<tr><td colspan="9" class="text-center py-5 text-muted">No purchase invoices found.</td></tr>'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$.each(rows, function(i, inv) {
|
||||
var source_label = '';
|
||||
if (inv.source === 'po' && inv.source_id) {
|
||||
source_label = `<a href="<?php echo $server_url?>po/manage_po.php?id=${inv.source_id}">PO #${inv.source_id}</a>`;
|
||||
} else if (inv.source === 'supplier_return' && inv.source_id) {
|
||||
source_label = `<a href="<?php echo $server_url?>po/manage_supplier_return.php?id=${inv.source_id}">SRN #${inv.source_id}</a>`;
|
||||
} else {
|
||||
source_label = '<span class="text-muted">—</span>';
|
||||
}
|
||||
|
||||
body += `<tr>
|
||||
<td class="py-3 fw-semibold">${escape_html(inv.invoice_number || '')}</td>
|
||||
<td class="py-3">${doc_type_badge(inv.doc_type)}</td>
|
||||
<td class="py-3">${source_label}</td>
|
||||
<td class="py-3">${format_date(inv.issued_date)}</td>
|
||||
<td class="py-3">${escape_html(inv.contact_name || '—')}</td>
|
||||
<td class="py-3">${get_dept_label(inv.department_id)}</td>
|
||||
<td class="py-3 fw-semibold">${format_number(inv.grand_total, 2)}</td>
|
||||
<td class="py-3">${inv.due_date ? format_date(inv.due_date) : '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${invoice_status_badge(inv.status, inv.due_date)}</td>
|
||||
<td class="py-3">
|
||||
<a href="<?php echo $server_url?>po/manage_purchase_invoice.php?id=${inv.id}" title="View">
|
||||
<i class="ti ti-eye fs-5"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
$('table#invoice_list tbody').html(body);
|
||||
}
|
||||
|
||||
$(async function() {
|
||||
await load_dept_cache();
|
||||
populate_dept_filter('filter_dept');
|
||||
retrieve_invoices();
|
||||
$('#search_input').on('keyup', function(e) {
|
||||
if (e.key === 'Enter') retrieve_invoices();
|
||||
});
|
||||
$('#filter_type, #filter_status, #filter_dept').on('change', retrieve_invoices);
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+181
-218
@@ -71,6 +71,13 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-lg-6">
|
||||
<label class="form-label">Department</label>
|
||||
<select id="department_id" class="form-select">
|
||||
<option value="0">— No Department —</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-lg-12">
|
||||
<label class="form-label">Notes</label>
|
||||
<textarea id="notes" class="form-control" rows="2"
|
||||
@@ -97,8 +104,9 @@
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th style="width:120px;">Qty</th>
|
||||
<th style="width:130px;">Unit Price</th>
|
||||
<th style="width:90px;">Qty</th>
|
||||
<th style="width:120px;">Unit Price</th>
|
||||
<th style="width:80px;">Tax%</th>
|
||||
<th style="width:130px;">Total</th>
|
||||
<th style="width:40px;"></th>
|
||||
</tr>
|
||||
@@ -137,7 +145,6 @@
|
||||
<h2 class="fs-5 mb-3">Status</h2>
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<span id="badge_status"></span>
|
||||
<span id="badge_payment"></span>
|
||||
</div>
|
||||
<p class="mb-0 small text-muted" id="po_number_display"></p>
|
||||
<p class="mb-0 small text-muted d-none" id="expected_date_display"></p>
|
||||
@@ -159,8 +166,12 @@
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="text-muted">Tax</span>
|
||||
<input type="number" id="tax" class="form-control form-control-sm text-end"
|
||||
style="width:110px;" value="0" min="0" oninput="recalc_totals()">
|
||||
<span id="summary_tax" class="fw-semibold">0.00</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="text-muted small">Tax Adj.</span>
|
||||
<input type="number" id="tax_adjustment" class="form-control form-control-sm text-end"
|
||||
style="width:90px;" value="0" min="-0.3" max="0.3" step="0.01" oninput="recalc_totals()">
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<span class="text-muted">Shipping</span>
|
||||
@@ -189,20 +200,27 @@
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<div id="payment_status_group" class="d-none">
|
||||
<label class="form-label small text-muted mb-1">Payment Status</label>
|
||||
<div class="d-flex gap-2">
|
||||
<select id="payment_status_select" class="form-select form-select-sm">
|
||||
<option value="0">Unpaid</option>
|
||||
<option value="1">Paid</option>
|
||||
<option value="2">Partial</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-outline-primary text-nowrap"
|
||||
onclick="update_payment_status()">
|
||||
<i class="ti ti-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-1">
|
||||
|
||||
<button class="btn btn-outline-primary w-100 d-none" id="btn_proceed_pi"
|
||||
onclick="proceed_to_purchase_invoice()">
|
||||
<i class="ti ti-file-invoice me-1"></i>Proceed to Purchase Invoice
|
||||
</button>
|
||||
|
||||
<a href="#" class="btn btn-outline-info w-100 d-none" id="btn_view_pi">
|
||||
<i class="ti ti-eye me-1"></i>View Purchase Invoice
|
||||
</a>
|
||||
|
||||
<a href="<?php echo $server_url?>po/manage_supplier_return.php?po_id=<?php echo $po_id; ?>"
|
||||
class="btn btn-outline-secondary w-100 d-none" id="btn_new_return">
|
||||
<i class="ti ti-arrow-back-up me-1"></i>New Supplier Return
|
||||
</a>
|
||||
|
||||
<hr class="my-1">
|
||||
|
||||
<button class="btn btn-outline-warning w-100 d-none" id="btn_close_po" onclick="close_po()">
|
||||
<i class="ti ti-lock me-1"></i>Close PO
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline-danger w-100" id="btn_cancel" onclick="cancel_po()">
|
||||
<i class="ti ti-x me-1"></i>Cancel PO
|
||||
@@ -220,6 +238,7 @@
|
||||
<?php require '../include_ending.php'; ?>
|
||||
|
||||
<script>
|
||||
// custom.js globals: remove_item(), load_location_config(), init_product_search_inputs(), init_lot_search_inputs(), escape_html(), format_number()
|
||||
|
||||
var po_id = <?php echo $po_id; ?>;
|
||||
var po_data = null;
|
||||
@@ -238,40 +257,44 @@
|
||||
// ── Status helpers ────────────────────────────────────────────────────────
|
||||
function po_status_badge(status) {
|
||||
const map = {
|
||||
'-2': '<span class="badge bg-danger">Pending Warehouse</span>',
|
||||
'-1': '<span class="badge bg-secondary">Cancelled</span>',
|
||||
'0': '<span class="badge bg-warning text-dark">Draft</span>',
|
||||
'1': '<span class="badge bg-info text-white">Confirmed</span>',
|
||||
'2': '<span class="badge bg-primary">Partial</span>',
|
||||
'3': '<span class="badge bg-success">Completed</span>',
|
||||
};
|
||||
return map[String(status)] ?? '—';
|
||||
}
|
||||
|
||||
function po_payment_badge(status) {
|
||||
function receipt_status_badge(status) {
|
||||
const map = {
|
||||
'0': '<span class="badge bg-warning text-dark">Unpaid</span>',
|
||||
'1': '<span class="badge bg-success">Paid</span>',
|
||||
'2': '<span class="badge bg-info text-white">Partial</span>',
|
||||
'2': '<span class="badge bg-primary">Partial</span>',
|
||||
'3': '<span class="badge bg-success">Received</span>',
|
||||
};
|
||||
return map[String(status)] ?? '—';
|
||||
return map[String(status)] ?? '';
|
||||
}
|
||||
|
||||
|
||||
// ── Recalculate totals ────────────────────────────────────────────────────
|
||||
function recalc_totals() {
|
||||
var subtotal = 0;
|
||||
var tax_sum = 0;
|
||||
$('#item_tbody tr').each(function() {
|
||||
var qty = parseFloat($(this).find('.item_qty').val()) || 0;
|
||||
var price = parseFloat($(this).find('.item_price').val()) || 0;
|
||||
var total = qty * price;
|
||||
var qty = parseFloat($(this).find('.item_qty').val()) || 0;
|
||||
var price = parseFloat($(this).find('.item_price').val()) || 0;
|
||||
var tax_rate = parseFloat($(this).find('.item_tax_rate').val()) || 0;
|
||||
var total = qty * price;
|
||||
var tax_amt = round_dp(total * tax_rate / 100, 4);
|
||||
$(this).find('.item_total').text(format_number(total, 2));
|
||||
$(this).find('.item_tax_amount').val(tax_amt);
|
||||
subtotal += total;
|
||||
tax_sum += tax_amt;
|
||||
});
|
||||
var discount = parseFloat($('#discount').val()) || 0;
|
||||
var tax = parseFloat($('#tax').val()) || 0;
|
||||
var shipping_fee = parseFloat($('#shipping_fee').val()) || 0;
|
||||
var discount = parseFloat($('#discount').val()) || 0;
|
||||
var tax_adjustment = parseFloat($('#tax_adjustment').val()) || 0;
|
||||
var shipping_fee = parseFloat($('#shipping_fee').val()) || 0;
|
||||
var derived_tax = round_dp(tax_sum, 2);
|
||||
$('#summary_subtotal').text(format_number(subtotal, 2));
|
||||
$('#summary_grand_total').text(format_number(subtotal - discount + tax + shipping_fee, 2));
|
||||
$('#summary_tax').text(format_number(derived_tax, 2));
|
||||
$('#summary_grand_total').text(format_number(subtotal - discount + derived_tax + tax_adjustment + shipping_fee, 2));
|
||||
}
|
||||
|
||||
|
||||
@@ -282,7 +305,7 @@
|
||||
var row = `<tr id="item_row_${idx}">
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm item_sku product_search"
|
||||
placeholder="SKU..." value="${item.product_sku || ''}" autocomplete="off" data-idx="${idx}">
|
||||
placeholder="SKU..." value="${item.product_sku || ''}" autocomplete="off" data-idx="${idx}" data-product-name-target=".item_name">
|
||||
<input type="hidden" class="item_name" value="${item.product_name || ''}">
|
||||
<input type="hidden" class="item_id" value="${item.item_id ?? idx}">
|
||||
<small class="text-muted item_name_display">${item.product_name || ''}</small>
|
||||
@@ -295,6 +318,11 @@
|
||||
<input type="number" class="form-control form-control-sm item_price"
|
||||
value="${item.unit_price || 0}" min="0" step="any" oninput="recalc_totals()">
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm item_tax_rate"
|
||||
value="${parseFloat(item.tax_rate || 0).toFixed(2)}" min="0" max="100" step="0.01" oninput="recalc_totals()">
|
||||
<input type="hidden" class="item_tax_amount" value="${item.tax_amount || 0}">
|
||||
</td>
|
||||
<td class="item_total text-end">${format_number((item.quantity || 1) * (item.unit_price || 0), 2)}</td>
|
||||
<td>
|
||||
<a href="javascript:void(0);" class="link-danger remove_item_btn" onclick="remove_item(${idx})">
|
||||
@@ -303,13 +331,12 @@
|
||||
</td>
|
||||
</tr>`;
|
||||
$('#item_tbody').append(row);
|
||||
if (window.init_product_search_inputs) {
|
||||
window.init_product_search_inputs(`#item_row_${idx}`);
|
||||
}
|
||||
recalc_totals();
|
||||
}
|
||||
|
||||
function remove_item(idx) {
|
||||
$(`#item_row_${idx}`).remove();
|
||||
recalc_totals();
|
||||
}
|
||||
|
||||
|
||||
// ── Collect items from DOM ────────────────────────────────────────────────
|
||||
@@ -317,10 +344,12 @@
|
||||
var items = [];
|
||||
var idx = 0;
|
||||
$('#item_tbody tr').each(function() {
|
||||
var sku = $(this).find('.item_sku').val().trim();
|
||||
var name = $(this).find('.item_name').val().trim();
|
||||
var qty = parseFloat($(this).find('.item_qty').val()) || 0;
|
||||
var price = parseFloat($(this).find('.item_price').val()) || 0;
|
||||
var sku = $(this).find('.item_sku').val().trim();
|
||||
var name = $(this).find('.item_name').val().trim();
|
||||
var qty = parseFloat($(this).find('.item_qty').val()) || 0;
|
||||
var price = parseFloat($(this).find('.item_price').val()) || 0;
|
||||
var tax_rate = parseFloat($(this).find('.item_tax_rate').val()) || 0;
|
||||
var tax_amt = parseFloat($(this).find('.item_tax_amount').val()) || 0;
|
||||
if (!sku || qty <= 0) { idx++; return; }
|
||||
items.push({
|
||||
item_id: idx,
|
||||
@@ -328,7 +357,9 @@
|
||||
product_name: name,
|
||||
quantity: qty,
|
||||
unit_price: price,
|
||||
total_price: qty * price,
|
||||
total_price: round_dp(qty * price, 4),
|
||||
tax_rate: tax_rate,
|
||||
tax_amount: tax_amt,
|
||||
received_qty: 0,
|
||||
stock_in_id: 0,
|
||||
});
|
||||
@@ -338,21 +369,6 @@
|
||||
}
|
||||
|
||||
|
||||
// ── Location config loader ────────────────────────────────────────────────
|
||||
function load_location_config() {
|
||||
return ajax_request({
|
||||
url: '<?php echo $server_url?>setting/api/engine/company_setting.php',
|
||||
autoPrepare: true, checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var s = res.output;
|
||||
advanced = s.advanced_location == 1;
|
||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||
label_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Cascade helpers (scoped per receive row by rowId) ─────────────────────
|
||||
@@ -439,83 +455,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
function init_receive_lot_search(rowId) {
|
||||
var $lot = $(`#recv_lot_${rowId}`);
|
||||
$(document).on('lot:selected', '.lot_search', function(e, lot) {
|
||||
var rowId = $(this).closest('[data-row]').data('row');
|
||||
if (rowId) lock_recv_expiry(rowId, lot.expiry_date || '');
|
||||
});
|
||||
|
||||
$lot.autocomplete({
|
||||
source: function(request, response) {
|
||||
if ($lot.data('xhr')) {
|
||||
$lot.data('xhr').abort();
|
||||
}
|
||||
|
||||
var $card = $lot.closest('[data-row]');
|
||||
var q = prepare_form_data(0, true);
|
||||
q.keyword = request.term;
|
||||
q.product_sku = $card.data('sku') || '';
|
||||
|
||||
var xhr = $.ajax({
|
||||
url: server_url + 'ics/api/engine/retrieve_lot.php',
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: { json: JSON.stringify(q) },
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
|
||||
},
|
||||
success: function(res) {
|
||||
response((res.result || []).map(function(item) {
|
||||
var label = `
|
||||
<div class="lot-result-item" style="font-family:'Poppins',sans-serif;padding:4px 0;">
|
||||
<div style="font-size:10px;text-transform:uppercase;color:#E66239;font-weight:600;line-height:1.2;">
|
||||
Lot: ${item.lot_number}
|
||||
</div>
|
||||
<div style="font-size:13px;font-weight:500;color:#171717;margin-top:2px;">
|
||||
Expiry: ${item.expiry_date || 'N/A'}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
return {
|
||||
label: label,
|
||||
value: item.lot_number,
|
||||
data: item
|
||||
};
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
$lot.data('xhr', xhr);
|
||||
},
|
||||
minLength: 0,
|
||||
delay: 300,
|
||||
select: function(e, ui) {
|
||||
if (!ui.item || !ui.item.data) return;
|
||||
|
||||
var lot = ui.item.data;
|
||||
$lot
|
||||
.val(lot.lot_number)
|
||||
.attr('secondary', lot.lot_number)
|
||||
.attr('data-existing', '1');
|
||||
|
||||
lock_recv_expiry(rowId, lot.expiry_date || '');
|
||||
return false;
|
||||
}
|
||||
}).data('ui-autocomplete')._renderItem = function(ul, item) {
|
||||
return $('<li>').append($('<div>').html(item.label)).appendTo(ul);
|
||||
};
|
||||
|
||||
$lot.on('blur', function() {
|
||||
var secondary = $lot.attr('secondary');
|
||||
if (secondary) $lot.val(secondary);
|
||||
});
|
||||
|
||||
$lot.on('focus', function() {
|
||||
$lot.autocomplete('search', $lot.val());
|
||||
});
|
||||
|
||||
$lot.on('input', function() {
|
||||
$lot.removeAttr('data-existing secondary');
|
||||
unlock_recv_expiry(rowId);
|
||||
});
|
||||
}
|
||||
$(document).on('lot:cleared', '.lot_search', function() {
|
||||
var rowId = $(this).closest('[data-row]').data('row');
|
||||
if (rowId) unlock_recv_expiry(rowId);
|
||||
});
|
||||
|
||||
// Called when warehouse select changes on a receive row
|
||||
function on_recv_wh_change(rowId) {
|
||||
@@ -597,7 +545,7 @@
|
||||
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label small text-muted">Lot Number</label>
|
||||
<input type="text" id="recv_lot_${rowId}" class="form-control form-control-sm recv_lot_search"
|
||||
<input type="text" id="recv_lot_${rowId}" class="form-control form-control-sm lot_search"
|
||||
placeholder="Type to search or enter new lot..." autocomplete="off">
|
||||
</div>
|
||||
|
||||
@@ -640,7 +588,9 @@
|
||||
fp.altInput.classList.add('form-control', 'form-control-sm');
|
||||
}
|
||||
});
|
||||
init_receive_lot_search(rowId);
|
||||
if (window.init_lot_search_inputs) {
|
||||
window.init_lot_search_inputs(`#recv_lot_${rowId}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Pre-select default warehouse and trigger cascade for each row
|
||||
@@ -655,27 +605,50 @@
|
||||
|
||||
|
||||
// ── Set view/edit mode ────────────────────────────────────────────────────
|
||||
function set_mode(status, receivable_count) {
|
||||
function set_mode(status, receivable_count, po_res, closed_at) {
|
||||
status = parseInt(status);
|
||||
receivable_count = receivable_count ?? 0;
|
||||
po_res = po_res || {};
|
||||
var is_closed = !!closed_at;
|
||||
var is_draft = status === 0;
|
||||
var is_pending = status === -2;
|
||||
var is_editable = is_pending || is_draft;
|
||||
var can_confirm = status === 0;
|
||||
var can_cancel = status >= 0 && status <= 1;
|
||||
var can_receive = (status === 1 || status === 2) && receivable_count > 0;
|
||||
var can_cancel = status >= -2 && status <= 1;
|
||||
var can_receive = status === 1 && receivable_count > 0 && !is_closed;
|
||||
var can_close = status === 1 && !is_closed;
|
||||
var is_cancelled = status === -1;
|
||||
var has_pi = !!po_res.has_active_pi;
|
||||
var pi_id = po_res.active_pi_id || 0;
|
||||
var has_received = (po_res.returnable_items || []).length > 0;
|
||||
|
||||
$('#btn_add_item, #contact, #po_date, #expected_date, #warehouse_id, #notes, #discount, #tax, #shipping_fee')
|
||||
.prop('disabled', !is_draft);
|
||||
$('.item_sku, .item_qty, .item_price').prop('disabled', !is_draft);
|
||||
$('.remove_item_btn').toggleClass('d-none', !is_draft);
|
||||
$('#btn_add_item, #contact, #po_date, #expected_date, #warehouse_id, #department_id, #notes, #discount, #tax_adjustment, #shipping_fee')
|
||||
.prop('disabled', !is_editable);
|
||||
$('.item_sku, .item_qty, .item_price, .item_tax_rate').prop('disabled', !is_editable);
|
||||
$('.remove_item_btn').toggleClass('d-none', !is_editable);
|
||||
|
||||
set_btn_state('#btn_save', is_draft, 'Only draft POs can be edited');
|
||||
set_btn_state('#btn_confirm', can_confirm, 'PO must be in draft to confirm');
|
||||
set_btn_state('#btn_save', is_editable, 'Only pending or draft POs can be edited');
|
||||
$('#btn_save').html(is_pending
|
||||
? '<i class="ti ti-device-floppy me-1"></i>Save Warehouse'
|
||||
: '<i class="ti ti-device-floppy me-1"></i>Save Draft');
|
||||
set_btn_state('#btn_confirm', can_confirm,
|
||||
is_pending ? 'Assign a default warehouse and save first. The PO will move to Draft.' : 'PO must be in draft to confirm');
|
||||
set_btn_state('#btn_cancel', can_cancel && !is_cancelled,
|
||||
is_cancelled ? 'PO is already cancelled' : 'Only draft or confirmed POs can be cancelled');
|
||||
|
||||
$('#payment_status_group').toggleClass('d-none', status < 1 || is_cancelled);
|
||||
$('#receive_card').toggleClass('d-none', !can_receive);
|
||||
$('#btn_close_po').toggleClass('d-none', !can_close);
|
||||
|
||||
// Purchase Invoice button
|
||||
$('#btn_proceed_pi').toggleClass('d-none', has_pi || status < 1 || is_cancelled);
|
||||
if (has_pi && pi_id) {
|
||||
$('#btn_view_pi')
|
||||
.attr('href', '<?php echo $server_url?>po/manage_purchase_invoice.php?id=' + pi_id);
|
||||
}
|
||||
$('#btn_view_pi').toggleClass('d-none', !has_pi);
|
||||
|
||||
// New Supplier Return — only when goods have been received (status 2 or 3)
|
||||
$('#btn_new_return').toggleClass('d-none', !has_received || is_cancelled);
|
||||
}
|
||||
|
||||
|
||||
@@ -695,7 +668,6 @@
|
||||
$('#page_subtitle').text('');
|
||||
$('#status_card').removeClass('d-none');
|
||||
$('#badge_status').html(po_status_badge(o.status));
|
||||
$('#badge_payment').html(po_payment_badge(o.payment_status));
|
||||
$('#po_number_display').text(o.po_number);
|
||||
$('#expected_date_display')
|
||||
.toggleClass('d-none', !o.expected_date)
|
||||
@@ -706,18 +678,17 @@
|
||||
$('#po_date').val(o.po_date ? format_date(o.po_date) : '');
|
||||
$('#expected_date').val(o.expected_date ? format_date(o.expected_date) : '');
|
||||
$('#warehouse_id').val(o.warehouse_id || '');
|
||||
$('#department_id').val(o.department_id || 0);
|
||||
$('#notes').val(o.notes || '');
|
||||
$('#discount').val(o.discount || 0);
|
||||
$('#tax').val(o.tax || 0);
|
||||
$('#tax_adjustment').val(o.tax_adjustment || 0);
|
||||
$('#shipping_fee').val(o.shipping_fee || 0);
|
||||
$('#payment_status_select').val(o.payment_status ?? 0);
|
||||
|
||||
$('#item_tbody').empty();
|
||||
(o.items || []).forEach(function(item) { add_item_row(item); });
|
||||
recalc_totals();
|
||||
|
||||
var receivable = res.receivable_items || [];
|
||||
set_mode(o.status, receivable.length);
|
||||
set_mode(o.status, receivable.length, res, o.closed_at);
|
||||
if (receivable.length > 0) {
|
||||
build_receive_rows(receivable);
|
||||
}
|
||||
@@ -737,14 +708,15 @@
|
||||
action: po_id ? 'update' : 'create',
|
||||
data: {
|
||||
id: po_id,
|
||||
contact_id: $('#contact_id').val() || 0,
|
||||
contact_id: $('#contact_id').val() || 0,
|
||||
department_id: $('#department_id').val() || 0,
|
||||
po_date: to_iso_date($('#po_date').val()),
|
||||
expected_date: to_iso_date($('#expected_date').val()) || '',
|
||||
warehouse_id: $('#warehouse_id').val() || 0,
|
||||
warehouse_id: $('#warehouse_id').val() || 0,
|
||||
items: JSON.stringify(items),
|
||||
discount: $('#discount').val() || 0,
|
||||
tax: $('#tax').val() || 0,
|
||||
shipping_fee: $('#shipping_fee').val() || 0,
|
||||
discount: $('#discount').val() || 0,
|
||||
tax_adjustment: $('#tax_adjustment').val() || 0,
|
||||
shipping_fee: $('#shipping_fee').val() || 0,
|
||||
notes: $('#notes').val(),
|
||||
},
|
||||
onSuccess: function(res) {
|
||||
@@ -853,10 +825,7 @@
|
||||
receive_items: JSON.stringify(receive_items),
|
||||
},
|
||||
onSuccess: function(res) {
|
||||
var msg = res.auto_approved
|
||||
? 'Goods received and stock approved automatically.'
|
||||
: 'Goods received. Stock-in rows are pending warehouse approval.';
|
||||
bootbox.alert(msg, function() { retrieve_po(); });
|
||||
bootbox.alert('Goods received successfully.', function() { retrieve_po(); });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -864,19 +833,51 @@
|
||||
}
|
||||
|
||||
|
||||
// ── Update payment status ─────────────────────────────────────────────────
|
||||
function update_payment_status() {
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>po/api/engine/update_po_payment.php',
|
||||
autoPrepare: true, checkRequired: 0,
|
||||
action: 'update',
|
||||
data: { id: po_id, payment_status: $('#payment_status_select').val() },
|
||||
onSuccess: function() { retrieve_po(); }
|
||||
// ── Proceed to Purchase Invoice ───────────────────────────────────────────
|
||||
function proceed_to_purchase_invoice() {
|
||||
bootbox.confirm({
|
||||
message: 'Create a purchase invoice from this PO?',
|
||||
buttons: {
|
||||
confirm: { label: 'Create Purchase Invoice', className: 'btn-primary' },
|
||||
cancel: { label: 'Back', className: 'btn-secondary' }
|
||||
},
|
||||
callback: function(result) {
|
||||
if (!result) return;
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>po/api/engine/proceed_to_purchase_invoice.php',
|
||||
autoPrepare: true, checkRequired: 0,
|
||||
action: 'create',
|
||||
data: { po_id: po_id },
|
||||
onSuccess: function(res) {
|
||||
window.location.href = '<?php echo $server_url?>po/manage_purchase_invoice.php?id=' + res.new_id;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Cancel PO ────────────────────────────────────────────────────────────
|
||||
function close_po() {
|
||||
bootbox.confirm({
|
||||
message: 'Close this PO? No further goods receipts will be accepted. This cannot be undone.',
|
||||
buttons: {
|
||||
confirm: { label: 'Yes, Close PO', className: 'btn-warning' },
|
||||
cancel: { label: 'Back', className: 'btn-secondary' }
|
||||
},
|
||||
callback: function(result) {
|
||||
if (!result) return;
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>po/api/engine/close_po.php',
|
||||
autoPrepare: true, checkRequired: 0,
|
||||
action: 'update',
|
||||
data: { id: po_id },
|
||||
onSuccess: function() { retrieve_po(); }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cancel_po() {
|
||||
bootbox.confirm({
|
||||
message: 'Cancel this purchase order? Any pending (unapproved) stock-in rows will be cancelled.',
|
||||
@@ -930,59 +931,24 @@
|
||||
}
|
||||
|
||||
|
||||
// ── Contact autocomplete ──────────────────────────────────────────────────
|
||||
function init_contact_search() {
|
||||
$('#contact').autocomplete({
|
||||
source: function(req, res) {
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>contact/api/engine/contact.php',
|
||||
autoPrepare: true, checkRequired: 0,
|
||||
action: 'read',
|
||||
data: { keyword: req.term },
|
||||
onSuccess: function(data) {
|
||||
res(data.output.map(c => ({ label: c.contact_name, value: c.contact_name, id: c.id })));
|
||||
}
|
||||
});
|
||||
},
|
||||
select: function(e, ui) { $('#contact_id').val(ui.item.id); }
|
||||
});
|
||||
}
|
||||
$(document).on('contact:selected', '#contact', function(e, contact) {
|
||||
$('#contact_id').val(contact.id);
|
||||
});
|
||||
|
||||
|
||||
// ── Product autocomplete ──────────────────────────────────────────────────
|
||||
function init_product_search() {
|
||||
$(document).on('input', '.item_sku', function() {
|
||||
var $input = $(this);
|
||||
var $row = $input.closest('tr');
|
||||
$input.autocomplete({
|
||||
source: function(req, res) {
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>inventory/api/engine/product.php',
|
||||
autoPrepare: true, checkRequired: 0,
|
||||
action: 'read',
|
||||
data: { keyword: req.term },
|
||||
onSuccess: function(data) {
|
||||
res((data.output || []).map(p => ({
|
||||
label: p.sku + ' — ' + p.product_name,
|
||||
value: p.sku,
|
||||
name: p.product_name,
|
||||
})));
|
||||
}
|
||||
});
|
||||
},
|
||||
select: function(e, ui) {
|
||||
$row.find('.item_name').val(ui.item.name);
|
||||
$row.find('.item_name_display').text(ui.item.name);
|
||||
recalc_totals();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
$(document).on('product:selected', '.item_sku', function(e, product) {
|
||||
var $row = $(this).closest('tr');
|
||||
$row.find('.item_name_display').text(product.product_name);
|
||||
$row.find('.item_name').val(product.product_name || '');
|
||||
$row.find('.item_tax_rate').val(parseFloat(product.tax_rate || 0).toFixed(2));
|
||||
recalc_totals();
|
||||
});
|
||||
|
||||
|
||||
// ── Boot ─────────────────────────────────────────────────────────────────
|
||||
$(async function() {
|
||||
try {
|
||||
load_departments('department_id');
|
||||
await load_location_config();
|
||||
await retrieve_warehouses();
|
||||
|
||||
@@ -997,9 +963,6 @@
|
||||
flatpickr('#po_date', { dateFormat: 'd/m/Y', allowInput: true });
|
||||
flatpickr('#expected_date', { dateFormat: 'd/m/Y', allowInput: true });
|
||||
|
||||
init_contact_search();
|
||||
init_product_search();
|
||||
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../config.php';
|
||||
require '../include_header.php';
|
||||
|
||||
$invoice_id = (int)($_GET['id'] ?? 0);
|
||||
?>
|
||||
|
||||
<body>
|
||||
<?php require '../include_topbar.php'; ?>
|
||||
<?php require '../include_sidebar.php'; ?>
|
||||
|
||||
<main id="content" class="content py-15">
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="mb-6 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="fs-3 mb-1" id="page_title">Purchase Invoice</h1>
|
||||
<p class="mb-0 text-muted" id="page_subtitle"></p>
|
||||
</div>
|
||||
<a href="<?php echo $server_url?>po/invoice.php" class="btn btn-light">
|
||||
<i class="ti ti-arrow-left me-1"></i>Back
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-5">
|
||||
|
||||
<!-- Left: Items -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card mb-5">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="fs-5 mb-4">Items</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th class="text-end">Qty</th>
|
||||
<th class="text-end">Unit Price</th>
|
||||
<th class="text-end" style="width:60px;">Tax%</th>
|
||||
<th class="text-end">Tax Amt</th>
|
||||
<th class="text-end">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="item_tbody"></tbody>
|
||||
<tfoot id="item_tfoot"></tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Linked supplier credit notes (for purchase invoices) -->
|
||||
<div class="card d-none" id="dn_card">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="fs-5 mb-4">Supplier Credit Notes</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0 table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>DN #</th>
|
||||
<th>Date</th>
|
||||
<th class="text-end">Amount</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dn_tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Info + actions -->
|
||||
<div class="col-lg-4">
|
||||
|
||||
<div class="card mb-5">
|
||||
<div class="card-body p-4">
|
||||
<h2 class="fs-5 mb-3">Document Info</h2>
|
||||
<div class="mb-2 d-flex gap-2">
|
||||
<span id="badge_doc_type"></span>
|
||||
<span id="badge_status"></span>
|
||||
</div>
|
||||
<div class="mb-2 small">
|
||||
<span class="text-muted">Source:</span>
|
||||
<a href="#" id="link_source" class="ms-1 fw-semibold"></a>
|
||||
</div>
|
||||
<div class="mb-2 small">
|
||||
<span class="text-muted">Supplier:</span>
|
||||
<span class="ms-1 fw-semibold" id="display_contact"></span>
|
||||
</div>
|
||||
<div class="mb-2 small">
|
||||
<span class="text-muted">Department:</span>
|
||||
<span class="ms-1" id="display_department">—</span>
|
||||
</div>
|
||||
<div class="mb-2 small">
|
||||
<span class="text-muted">Date:</span>
|
||||
<span class="ms-1" id="display_issued"></span>
|
||||
</div>
|
||||
<div class="mb-2 small" id="due_date_row">
|
||||
<span class="text-muted">Due:</span>
|
||||
<input type="text" id="due_date" class="form-control form-control-sm ms-1 d-inline-block"
|
||||
style="width:140px;" placeholder="DD/MM/YYYY">
|
||||
</div>
|
||||
<div class="mt-3 small">
|
||||
<span class="text-muted">Notes:</span>
|
||||
<textarea id="notes" class="form-control form-control-sm mt-1" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="mt-3 small">
|
||||
<span class="text-muted">GL Formula:</span>
|
||||
<div class="d-flex gap-1 mt-1">
|
||||
<select id="formula_id" class="form-select form-select-sm">
|
||||
<option value="">— None —</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-secondary btn-sm px-2" id="btn_save_formula"
|
||||
onclick="save_formula()" title="Save formula">
|
||||
<i class="ti ti-device-floppy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body p-4 d-flex flex-column gap-2">
|
||||
|
||||
<button class="btn btn-primary w-100" id="btn_save" onclick="save_invoice()">
|
||||
<i class="ti ti-device-floppy me-1"></i>Save
|
||||
</button>
|
||||
|
||||
<button class="btn btn-success w-100" id="btn_issue" onclick="issue_invoice()">
|
||||
<i class="ti ti-send me-1"></i>Issue
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline-danger w-100" id="btn_void" onclick="void_invoice()">
|
||||
<i class="ti ti-ban me-1"></i>Void
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php require '../include_ending.php'; ?>
|
||||
|
||||
<script>
|
||||
var invoice_id = <?php echo $invoice_id; ?>;
|
||||
var invoice_data = null;
|
||||
|
||||
function doc_type_badge(doc_type) {
|
||||
const map = {
|
||||
'purchase_invoice': '<span class="badge bg-primary">Purchase Invoice</span>',
|
||||
'supplier_credit_note': '<span class="badge bg-info text-white">Supplier Credit Note</span>',
|
||||
};
|
||||
return map[doc_type] ?? '—';
|
||||
}
|
||||
|
||||
function invoice_status_badge(status, due_date) {
|
||||
if (String(status) === '1' && due_date) {
|
||||
var today = new Date().toISOString().slice(0, 10);
|
||||
if (due_date < today) return '<span class="badge bg-danger">Overdue</span>';
|
||||
}
|
||||
const map = {
|
||||
'0': '<span class="badge bg-secondary">Draft</span>',
|
||||
'1': '<span class="badge bg-info text-white">Issued</span>',
|
||||
'2': '<span class="badge bg-success">Paid</span>',
|
||||
'4': '<span class="badge bg-light text-dark">Void</span>',
|
||||
};
|
||||
return map[String(status)] ?? '—';
|
||||
}
|
||||
|
||||
function set_mode(inv) {
|
||||
var status = parseInt(inv.status);
|
||||
var doc_type = inv.doc_type;
|
||||
var is_pi = doc_type === 'purchase_invoice';
|
||||
var is_dn = doc_type === 'supplier_credit_note';
|
||||
var is_draft = status === 0;
|
||||
var is_editable = is_draft && (is_pi || is_dn);
|
||||
|
||||
$('#due_date_row').toggleClass('d-none', is_dn);
|
||||
$('#due_date, #notes').prop('disabled', !is_editable);
|
||||
|
||||
$('#btn_issue').html(is_dn
|
||||
? '<i class="ti ti-send me-1"></i>Issue Supplier Credit Note'
|
||||
: '<i class="ti ti-send me-1"></i>Issue Purchase Invoice');
|
||||
|
||||
var not_editable_hint = (!is_pi && !is_dn) ? 'Unsupported document type' : '';
|
||||
set_btn_state('#btn_save', is_editable, not_editable_hint || 'Only draft documents can be edited');
|
||||
set_btn_state('#btn_issue', is_editable, not_editable_hint || (status > 0 ? 'Already issued' : 'Only drafts can be issued'));
|
||||
|
||||
set_btn_state('#btn_void', is_pi && status >= 1 && status !== 4,
|
||||
!is_pi ? 'Only purchase invoices can be voided'
|
||||
: status === 0 ? 'Issue the document before voiding'
|
||||
: status === 4 ? 'Already void' : '');
|
||||
}
|
||||
|
||||
function retrieve_invoice() {
|
||||
return ajax_request({
|
||||
url: '<?php echo $server_url?>order/api/engine/retrieve_invoice.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
data: { invoice_id: invoice_id },
|
||||
onSuccess: function(res) {
|
||||
invoice_data = res.output;
|
||||
var inv = invoice_data;
|
||||
|
||||
$('#page_title').text(inv.invoice_number);
|
||||
$('#badge_doc_type').html(doc_type_badge(inv.doc_type));
|
||||
$('#badge_status').html(invoice_status_badge(inv.status, inv.due_date));
|
||||
$('#display_contact').text(inv.contact_name || '—');
|
||||
$('#display_department').text(get_dept_label(inv.department_id));
|
||||
$('#display_issued').text(format_date(inv.issued_date) || '—');
|
||||
$('#due_date').val(inv.due_date ? format_date(inv.due_date) : '');
|
||||
$('#notes').val(inv.notes || '');
|
||||
|
||||
// Source link
|
||||
if (inv.source === 'po' && inv.source_id) {
|
||||
$('#link_source')
|
||||
.attr('href', '<?php echo $server_url?>po/manage_po.php?id=' + inv.source_id)
|
||||
.text('PO #' + inv.source_id);
|
||||
} else if (inv.source === 'supplier_return' && inv.source_id) {
|
||||
$('#link_source')
|
||||
.attr('href', '<?php echo $server_url?>po/manage_supplier_return.php?id=' + inv.source_id)
|
||||
.text('Return #' + inv.source_id);
|
||||
}
|
||||
|
||||
// Items
|
||||
var tbody = '';
|
||||
$.each(inv.items || [], function(i, item) {
|
||||
tbody += `<tr>
|
||||
<td>${escape_html(item.product_name || item.product_sku)}</td>
|
||||
<td class="text-end">${format_number(item.quantity, 2)}</td>
|
||||
<td class="text-end">${format_number(item.unit_price, 2)}</td>
|
||||
<td class="text-end">${format_number(item.tax_rate, 2)}</td>
|
||||
<td class="text-end">${format_number(item.tax_amount, 4)}</td>
|
||||
<td class="text-end">${format_number(item.total_price, 2)}</td>
|
||||
</tr>`;
|
||||
});
|
||||
$('#item_tbody').html(tbody);
|
||||
|
||||
var is_draft = parseInt(inv.status) === 0;
|
||||
var is_editable_doc = inv.doc_type === 'purchase_invoice' || inv.doc_type === 'supplier_credit_note';
|
||||
var tax_adj_val = parseFloat(inv.tax_adjustment) || 0;
|
||||
var tax_adj_cell = (is_draft && is_editable_doc)
|
||||
? `<input type="number" id="tax_adjustment" class="form-control form-control-sm text-end d-inline-block" style="width:90px;" min="-0.3" max="0.3" step="0.01" value="${tax_adj_val.toFixed(2)}">`
|
||||
: `${format_number(tax_adj_val, 2)}`;
|
||||
|
||||
var tfoot = `
|
||||
<tr><td colspan="5" class="text-muted border-bottom-0">Subtotal</td><td class="text-end border-bottom-0">${format_number(inv.subtotal, 2)}</td></tr>
|
||||
<tr><td colspan="5" class="text-muted border-bottom-0">Discount</td><td class="text-end border-bottom-0">${format_number(inv.discount, 2)}</td></tr>
|
||||
<tr><td colspan="5" class="text-muted border-bottom-0">Tax</td><td class="text-end border-bottom-0">${format_number(inv.tax, 2)}</td></tr>
|
||||
<tr><td colspan="5" class="text-muted border-bottom-0">Tax Adj.</td><td class="text-end border-bottom-0">${tax_adj_cell}</td></tr>
|
||||
<tr><td colspan="5" class="text-muted">Shipping</td><td class="text-end">${format_number(inv.shipping_fee, 2)}</td></tr>
|
||||
<tr><td colspan="5" class="fw-bold">Grand Total</td><td class="text-end fw-bold">${format_number(inv.grand_total, 2)}</td></tr>`;
|
||||
$('#item_tfoot').html(tfoot);
|
||||
|
||||
// Linked debit notes (for purchase invoices)
|
||||
if (res.supplier_credit_notes && res.supplier_credit_notes.length) {
|
||||
var dn_rows = '';
|
||||
$.each(res.supplier_credit_notes, function(i, dn) {
|
||||
dn_rows += `<tr>
|
||||
<td>${escape_html(dn.invoice_number)}</td>
|
||||
<td>${format_date(dn.issued_date)}</td>
|
||||
<td class="text-end">${format_number(dn.grand_total, 2)}</td>
|
||||
<td>${invoice_status_badge(dn.status, dn.due_date)}</td>
|
||||
<td><a href="<?php echo $server_url?>po/manage_purchase_invoice.php?id=${dn.id}"><i class="ti ti-eye fs-5"></i></a></td>
|
||||
</tr>`;
|
||||
});
|
||||
$('#dn_tbody').html(dn_rows);
|
||||
$('#dn_card').removeClass('d-none');
|
||||
}
|
||||
|
||||
var gl_type = inv.doc_type === 'supplier_credit_note' ? 'supplier_credit_note' : 'purchase_invoice';
|
||||
var selected_formula = inv.formula_id || '';
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>accounting/api/engine/account_formula.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'get_by_type',
|
||||
data: { document_type: gl_type },
|
||||
onSuccess: function(fres) {
|
||||
var opts = '<option value="">— None —</option>';
|
||||
$.each(fres.output || [], function(i, f) {
|
||||
var sel = String(f.id) === String(selected_formula) ? ' selected' : '';
|
||||
opts += '<option value="' + f.id + '"' + sel + '>' + escape_html(f.formula_name) + '</option>';
|
||||
});
|
||||
$('#formula_id').html(opts);
|
||||
}
|
||||
});
|
||||
$('#btn_save_formula').prop('disabled', parseInt(inv.status) === 4);
|
||||
|
||||
set_mode(inv);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function save_invoice() {
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>order/api/engine/manage_invoice.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'update',
|
||||
data: {
|
||||
id: invoice_id,
|
||||
due_date: $('#due_date').val().split('/').reverse().join('-'),
|
||||
notes: $('#notes').val(),
|
||||
tax_adjustment: parseFloat($('#tax_adjustment').val()) || 0,
|
||||
},
|
||||
onSuccess: function() { retrieve_invoice(); }
|
||||
});
|
||||
}
|
||||
|
||||
function save_formula() {
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>order/api/engine/manage_invoice.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'save_formula',
|
||||
data: { id: invoice_id, formula_id: $('#formula_id').val() || null },
|
||||
onSuccess: function() {
|
||||
bootbox.alert({ message: 'GL formula saved.', size: 'small' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function issue_invoice() {
|
||||
var is_dn = invoice_data && invoice_data.doc_type === 'supplier_credit_note';
|
||||
var due_date = $('#due_date').val().trim();
|
||||
if (!is_dn && !due_date) {
|
||||
bootbox.alert('Please enter a due date before issuing this purchase invoice.');
|
||||
return;
|
||||
}
|
||||
var label = is_dn ? 'Issue Supplier Credit Note' : 'Issue Purchase Invoice';
|
||||
bootbox.confirm({
|
||||
message: is_dn ? 'Issue this supplier credit note?' : 'Issue this purchase invoice?',
|
||||
buttons: {
|
||||
confirm: { label: label, className: 'btn-success' },
|
||||
cancel: { label: 'Back', className: 'btn-secondary' }
|
||||
},
|
||||
callback: function(result) {
|
||||
if (!result) return;
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>order/api/engine/issue_invoice.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'update',
|
||||
data: {
|
||||
invoice_id: invoice_id,
|
||||
due_date: is_dn ? '' : due_date.split('/').reverse().join('-')
|
||||
},
|
||||
onSuccess: function() { retrieve_invoice(); }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function void_invoice() {
|
||||
bootbox.confirm({
|
||||
message: '<strong>Void this purchase invoice?</strong><br>This action cannot be undone.',
|
||||
buttons: {
|
||||
confirm: { label: 'Void', className: 'btn-danger' },
|
||||
cancel: { label: 'Back', className: 'btn-secondary' }
|
||||
},
|
||||
callback: function(result) {
|
||||
if (!result) return;
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>order/api/engine/update_invoice_status.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'update',
|
||||
data: { invoice_id: invoice_id, status: 4 },
|
||||
onSuccess: function() { retrieve_invoice(); }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(function() {
|
||||
load_dept_cache();
|
||||
if (invoice_id) retrieve_invoice();
|
||||
flatpickr('#due_date', { dateFormat: 'd/m/Y', allowInput: true });
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,631 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../config.php';
|
||||
require '../include_header.php';
|
||||
|
||||
$return_id = (int)($_GET['id'] ?? 0);
|
||||
$po_id = (int)($_GET['po_id'] ?? 0);
|
||||
?>
|
||||
|
||||
<body>
|
||||
<?php require '../include_topbar.php'; ?>
|
||||
<?php require '../include_sidebar.php'; ?>
|
||||
|
||||
<main id="content" class="content py-15">
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="mb-6 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="fs-3 mb-1" id="page_title">
|
||||
<?php echo $return_id ? 'Supplier Return Detail' : 'New Supplier Return'; ?>
|
||||
</h1>
|
||||
<p class="mb-0 text-muted" id="page_subtitle"></p>
|
||||
</div>
|
||||
<a href="<?php echo $server_url?>po/supplier_returns.php" class="btn btn-light">
|
||||
<i class="ti ti-arrow-left me-1"></i>Back
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-5">
|
||||
|
||||
<!-- Left: Items -->
|
||||
<div class="col-lg-8">
|
||||
|
||||
<div class="card mb-5">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="fs-5 mb-4">Return Information</h2>
|
||||
<div class="row gx-5">
|
||||
|
||||
<div class="mb-3 col-lg-6">
|
||||
<label class="form-label">Purchase Order</label>
|
||||
<input type="text" id="po_display" class="form-control" readonly
|
||||
placeholder="Loading...">
|
||||
<input type="hidden" id="po_id" value="<?php echo $po_id; ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-lg-6">
|
||||
<label class="form-label">Invoice <span class="text-muted small">(optional)</span></label>
|
||||
<select id="invoice_id" class="form-select">
|
||||
<option value="0">— no invoice —</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-lg-6">
|
||||
<label class="form-label">Supplier</label>
|
||||
<input type="text" id="contact" class="form-control" readonly
|
||||
placeholder="Auto-filled from PO">
|
||||
<input type="hidden" id="contact_id">
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-lg-6">
|
||||
<label class="form-label">Return Date</label>
|
||||
<input type="text" id="return_date" class="form-control"
|
||||
placeholder="DD/MM/YYYY" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-lg-6">
|
||||
<label class="form-label">Department</label>
|
||||
<p class="form-control-plaintext" id="display_department">—</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-lg-12">
|
||||
<label class="form-label">Reason</label>
|
||||
<textarea id="reason" class="form-control" rows="2"
|
||||
placeholder="Reason for returning goods..."></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="fs-5 mb-3">Select Items to Return</h2>
|
||||
<p class="text-muted small mb-4">Only received items with remaining returnable quantity are shown.</p>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0" id="item_table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:40px;"></th>
|
||||
<th>Product</th>
|
||||
<th class="text-end" style="width:110px;">PO Qty</th>
|
||||
<th class="text-end" style="width:110px;">Received</th>
|
||||
<th class="text-end" style="width:110px;">Returnable</th>
|
||||
<th style="width:100px;">Return Qty</th>
|
||||
<th class="text-end" style="width:110px;">Unit Price</th>
|
||||
<th style="width:70px;">Tax%</th>
|
||||
<th class="text-end" style="width:110px;">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="item_tbody">
|
||||
<tr id="loading_row">
|
||||
<td colspan="9" class="text-center text-muted py-4">
|
||||
<i class="ti ti-loader me-1"></i>Loading items...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Right: Summary + actions -->
|
||||
<div class="col-lg-4">
|
||||
|
||||
<div class="card mb-5 d-none" id="status_card">
|
||||
<div class="card-body p-4">
|
||||
<h2 class="fs-5 mb-3">Status</h2>
|
||||
<span id="badge_status"></span>
|
||||
<span id="badge_fulfillment" class="ms-1"></span>
|
||||
<p class="mb-0 small text-muted mt-2" id="return_number_display"></p>
|
||||
<p class="mb-0 small text-muted d-none mt-1" id="tracking_number_display"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-5">
|
||||
<div class="card-body p-4">
|
||||
<h2 class="fs-5 mb-3">Refund Summary</h2>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="text-muted">Subtotal</span>
|
||||
<span id="summary_subtotal">0.00</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="text-muted">Tax</span>
|
||||
<span id="summary_tax">0.00</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="text-muted small">Tax Adj.</span>
|
||||
<input type="number" id="tax_adjustment" class="form-control form-control-sm text-end"
|
||||
style="width:90px;" min="-0.3" max="0.3" step="0.01" value="0" oninput="recalc_totals()">
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex justify-content-between fw-bold fs-5">
|
||||
<span>Total Refund</span>
|
||||
<span id="summary_refund">0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body p-4 d-flex flex-column gap-2">
|
||||
|
||||
<button class="btn btn-primary w-100" id="btn_save" onclick="save_return()">
|
||||
<i class="ti ti-device-floppy me-1"></i>Save Draft
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline-primary w-100" id="btn_save_tracking"
|
||||
onclick="save_tracking()">
|
||||
<i class="ti ti-truck-delivery me-1"></i>Save Tracking
|
||||
</button>
|
||||
|
||||
<button class="btn btn-success w-100" id="btn_confirm" onclick="confirm_return()">
|
||||
<i class="ti ti-check me-1"></i>Confirm Return
|
||||
</button>
|
||||
|
||||
<button class="btn btn-warning w-100" id="btn_create_dn"
|
||||
onclick="proceed_to_supplier_credit_note()">
|
||||
<i class="ti ti-file-plus me-1"></i>Create Supplier Credit Note
|
||||
</button>
|
||||
|
||||
<a href="#" class="btn btn-outline-warning w-100 d-none" id="btn_view_dn">
|
||||
<i class="ti ti-file-invoice me-1"></i>View Supplier Credit Note
|
||||
</a>
|
||||
|
||||
<button class="btn btn-outline-danger w-100" id="btn_cancel" onclick="cancel_return()">
|
||||
<i class="ti ti-x me-1"></i>Cancel Return
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php require '../include_ending.php'; ?>
|
||||
|
||||
<script>
|
||||
var return_id = <?php echo $return_id; ?>;
|
||||
var return_data = null;
|
||||
var po_data = null;
|
||||
|
||||
function return_status_badge(status) {
|
||||
const map = {
|
||||
'-1': '<span class="badge bg-secondary">Cancelled</span>',
|
||||
'0': '<span class="badge bg-warning text-dark">Draft</span>',
|
||||
'1': '<span class="badge bg-success">Confirmed</span>',
|
||||
};
|
||||
return map[String(status)] ?? '—';
|
||||
}
|
||||
|
||||
function fulfillment_status_badge(status) {
|
||||
const map = {
|
||||
'1': '<span class="badge bg-warning text-dark"><i class="ti ti-list-check me-1"></i>Picking</span>',
|
||||
'2': '<span class="badge bg-info text-white"><i class="ti ti-package me-1"></i>Packed</span>',
|
||||
'3': '<span class="badge bg-success"><i class="ti ti-truck me-1"></i>Shipped</span>',
|
||||
};
|
||||
return map[String(status)] ?? '';
|
||||
}
|
||||
|
||||
function recalc_totals() {
|
||||
var subtotal = 0, tax_sum = 0;
|
||||
$('#item_tbody tr.return-item-row').each(function() {
|
||||
var $row = $(this);
|
||||
if (!$row.find('.item_check').is(':checked')) return;
|
||||
var qty = parseFloat($row.find('.item_qty').val()) || 0;
|
||||
var price = parseFloat($row.find('.item_price').val()) || 0;
|
||||
var tax_rate = parseFloat($row.find('.item_tax_rate').val()) || 0;
|
||||
var t = qty * price;
|
||||
$row.find('.item_total').text(format_number(t, 2));
|
||||
subtotal += t;
|
||||
tax_sum += round_dp(t * tax_rate / 100, 4);
|
||||
});
|
||||
var tax_adj = parseFloat($('#tax_adjustment').val()) || 0;
|
||||
var tax = round_dp(tax_sum, 2) + tax_adj;
|
||||
$('#summary_subtotal').text(format_number(subtotal, 2));
|
||||
$('#summary_tax').text(format_number(tax, 2));
|
||||
$('#summary_refund').text(format_number(subtotal + tax, 2));
|
||||
}
|
||||
|
||||
function render_returnable_items(items, selected_items) {
|
||||
var selected_map = {};
|
||||
if (selected_items) {
|
||||
selected_items.forEach(function(si) { selected_map[si.item_id] = si; });
|
||||
}
|
||||
|
||||
if (!items.length) {
|
||||
$('#item_tbody').html(
|
||||
'<tr><td colspan="9" class="text-center text-muted py-4">No returnable items for this PO.</td></tr>'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var rows = '';
|
||||
items.forEach(function(item) {
|
||||
var si = selected_map[item.item_id] || null;
|
||||
var checked = si ? 'checked' : '';
|
||||
var confirmed = return_data && parseInt(return_data.status) === 1 && si;
|
||||
var qty = si ? si.quantity : item.returnable_qty;
|
||||
var price = item.unit_price || item.price || 0;
|
||||
var tax_rate = si ? (parseFloat(si.tax_rate) || 0) : (parseFloat(item.tax_rate) || 0);
|
||||
|
||||
rows += `<tr class="return-item-row" id="ret_item_${item.item_id}">
|
||||
<td>
|
||||
${confirmed
|
||||
? `<span class="text-success" title="Returned"><i class="ti ti-circle-check-filled"></i></span>
|
||||
<input type="checkbox" class="d-none item_check" checked>`
|
||||
: `<input type="checkbox" class="form-check-input item_check"
|
||||
onchange="toggle_item_row(${item.item_id}); recalc_totals();" ${checked}>`}
|
||||
<input type="hidden" class="item_id" value="${item.item_id}">
|
||||
<input type="hidden" class="item_sku" value="${item.product_sku}">
|
||||
<input type="hidden" class="item_name" value="${escape_html(item.product_name || '')}">
|
||||
<input type="hidden" class="item_warehouse" value="${item.warehouse_id || 0}">
|
||||
<input type="hidden" class="item_zone" value="${item.zone || ''}">
|
||||
<input type="hidden" class="item_aisle" value="${item.aisle || ''}">
|
||||
<input type="hidden" class="item_rack" value="${item.rack || ''}">
|
||||
<input type="hidden" class="item_stock_in_id" value="${item.stock_in_id || 0}">
|
||||
<input type="hidden" class="item_lot" value="${item.lot_number || ''}">
|
||||
<input type="hidden" class="item_serial" value="${item.serial_number || ''}">
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold">${escape_html(item.product_sku)}</div>
|
||||
<small class="text-muted">${escape_html(item.product_name || '')}</small>
|
||||
</td>
|
||||
<td class="text-end">${format_number(item.ordered_qty, 2)}</td>
|
||||
<td class="text-end">${format_number(item.received_qty, 2)}</td>
|
||||
<td class="text-end">${format_number(item.returnable_qty, 2)}</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm item_qty"
|
||||
value="${format_number(qty, 2)}"
|
||||
min="0.0001" max="${item.returnable_qty}" step="any"
|
||||
${!checked ? 'disabled' : ''}
|
||||
oninput="recalc_totals()">
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm item_price text-end"
|
||||
value="${price}"
|
||||
min="0" step="any"
|
||||
${!checked ? 'disabled' : ''}
|
||||
oninput="recalc_totals()">
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm item_tax_rate"
|
||||
value="${tax_rate.toFixed(2)}"
|
||||
min="0" max="100" step="0.01"
|
||||
${!checked ? 'disabled' : ''}
|
||||
oninput="recalc_totals()">
|
||||
</td>
|
||||
<td class="text-end item_total">${checked ? format_number(qty * price, 2) : '—'}</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
$('#item_tbody').html(rows);
|
||||
recalc_totals();
|
||||
}
|
||||
|
||||
function toggle_item_row(item_id) {
|
||||
var $row = $(`#ret_item_${item_id}`);
|
||||
var checked = $row.find('.item_check').is(':checked');
|
||||
$row.find('.item_qty, .item_price, .item_tax_rate').prop('disabled', !checked);
|
||||
if (!checked) $row.find('.item_total').text('—');
|
||||
else recalc_totals();
|
||||
}
|
||||
|
||||
|
||||
// ── Load PO ───────────────────────────────────────────────────────────────
|
||||
function load_po(pid, selected_items) {
|
||||
if (!pid) return Promise.resolve();
|
||||
return ajax_request({
|
||||
url: '<?php echo $server_url?>po/api/engine/retrieve_po.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
data: { id: pid },
|
||||
onSuccess: function(res) {
|
||||
po_data = res.output;
|
||||
$('#po_display').val(po_data.po_number || '');
|
||||
$('#contact').val(po_data.contact_name || '');
|
||||
$('#contact_id').val(po_data.contact_id || 0);
|
||||
|
||||
load_invoices_for_po(pid);
|
||||
|
||||
var returnable = res.returnable_items || [];
|
||||
render_returnable_items(returnable, selected_items || null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function load_invoices_for_po(pid) {
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>order/api/engine/retrieve_invoice.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
queueLock: false,
|
||||
action: 'read',
|
||||
data: { source: 'po', source_id: pid, doc_type: 'purchase_invoice' },
|
||||
onSuccess: function(res) {
|
||||
var opts = '<option value="0">— no invoice —</option>';
|
||||
$.each(res.output || [], function(i, inv) {
|
||||
opts += `<option value="${inv.id}">${escape_html(inv.invoice_number)}</option>`;
|
||||
});
|
||||
$('#invoice_id').html(opts);
|
||||
if (return_data && return_data.invoice_id) {
|
||||
$('#invoice_id').val(return_data.invoice_id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Mode ──────────────────────────────────────────────────────────────────
|
||||
function set_mode(status) {
|
||||
status = parseInt(status);
|
||||
var is_draft = status === 0;
|
||||
var is_confirmed = status === 1;
|
||||
var has_dn = return_data && return_data.has_active_dn;
|
||||
var dn_id = return_data && return_data.active_dn_id;
|
||||
|
||||
$('#return_date, #reason, #invoice_id').prop('disabled', !is_draft);
|
||||
$('#tax_adjustment').prop('disabled', !is_draft);
|
||||
// Tracking number stays editable for draft and confirmed (added after ship)
|
||||
$('.item_check').prop('disabled', !is_draft);
|
||||
$('#item_tbody tr.return-item-row').each(function() {
|
||||
var checked = $(this).find('.item_check').is(':checked');
|
||||
$(this).find('.item_qty, .item_price, .item_tax_rate').prop('disabled', !is_draft || !checked);
|
||||
});
|
||||
|
||||
set_btn_state('#btn_save', is_draft,
|
||||
status === -1 ? 'Return is cancelled' : 'Return has already been confirmed');
|
||||
set_btn_state('#btn_save_tracking', is_confirmed,
|
||||
!return_id ? 'Save the return before adding a tracking number'
|
||||
: (status < 1 ? 'Confirm the return before adding a tracking number' : ''));
|
||||
set_btn_state('#btn_confirm', is_draft,
|
||||
!return_id ? 'Save the return before confirming'
|
||||
: (status === -1 ? 'Return is cancelled' : 'Return is already confirmed'));
|
||||
if (!return_id) $('#btn_confirm').prop('disabled', true);
|
||||
|
||||
set_btn_state('#btn_create_dn', is_confirmed && !has_dn,
|
||||
!return_id ? 'Save the return first'
|
||||
: !is_confirmed ? 'Confirm the return before creating a supplier credit note'
|
||||
: 'A supplier credit note already exists for this return');
|
||||
if (has_dn && dn_id) {
|
||||
$('#btn_view_dn').attr('href', '<?php echo $server_url?>po/manage_purchase_invoice.php?id=' + dn_id);
|
||||
}
|
||||
$('#btn_view_dn').toggleClass('d-none', !(is_confirmed && has_dn));
|
||||
|
||||
set_btn_state('#btn_cancel', is_draft || status === 1,
|
||||
status === -1 ? 'Return is already cancelled' : 'Cannot cancel');
|
||||
}
|
||||
|
||||
|
||||
// ── Retrieve existing return ───────────────────────────────────────────────
|
||||
function retrieve_return() {
|
||||
if (!return_id) return Promise.resolve();
|
||||
return ajax_request({
|
||||
url: '<?php echo $server_url?>po/api/engine/retrieve_supplier_return.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
data: { id: return_id },
|
||||
onSuccess: async function(res) {
|
||||
return_data = res.output;
|
||||
var r = return_data;
|
||||
|
||||
$('#page_title').text(r.return_number);
|
||||
$('#status_card').removeClass('d-none');
|
||||
$('#badge_status').html(return_status_badge(r.status));
|
||||
$('#badge_fulfillment').html(fulfillment_status_badge(r.fulfillment_status));
|
||||
$('#return_number_display').text(r.return_number);
|
||||
$('#return_date').val(r.return_date ? format_date(r.return_date) : '');
|
||||
$('#reason').val(r.reason || '');
|
||||
$('#tax_adjustment').val(parseFloat(r.tax_adjustment || 0).toFixed(2));
|
||||
$('#display_department').text(get_dept_label(r.department_id));
|
||||
var tracking = r.shipping_tracking_number || '';
|
||||
$('#tracking_number_display')
|
||||
.toggleClass('d-none', !tracking)
|
||||
.html(tracking ? '<i class="ti ti-truck-delivery me-1"></i>' + escape_html(tracking) : '');
|
||||
|
||||
await load_po(r.po_id, r.items || []);
|
||||
set_mode(r.status);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Collect items ─────────────────────────────────────────────────────────
|
||||
function collect_items() {
|
||||
var items = [];
|
||||
$('#item_tbody tr.return-item-row').each(function() {
|
||||
var $row = $(this);
|
||||
if (!$row.find('.item_check').is(':checked')) return;
|
||||
var qty = parseFloat($row.find('.item_qty').val()) || 0;
|
||||
var price = parseFloat($row.find('.item_price').val()) || 0;
|
||||
var tax_rate = parseFloat($row.find('.item_tax_rate').val()) || 0;
|
||||
if (qty <= 0) return;
|
||||
items.push({
|
||||
item_id: parseInt($row.find('.item_id').val()),
|
||||
product_sku: $row.find('.item_sku').val(),
|
||||
product_name: $row.find('.item_name').val(),
|
||||
warehouse_id: parseInt($row.find('.item_warehouse').val()) || 0,
|
||||
zone: $row.find('.item_zone').val(),
|
||||
aisle: $row.find('.item_aisle').val(),
|
||||
rack: $row.find('.item_rack').val(),
|
||||
stock_in_id: parseInt($row.find('.item_stock_in_id').val()) || 0,
|
||||
lot_number: $row.find('.item_lot').val(),
|
||||
serial_number: $row.find('.item_serial').val(),
|
||||
quantity: qty,
|
||||
price: price,
|
||||
tax_rate: tax_rate,
|
||||
tax_amount: round_dp(qty * price * tax_rate / 100, 4),
|
||||
total_price: round_dp(qty * price, 4),
|
||||
});
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
function build_payload() {
|
||||
var items = collect_items();
|
||||
if (!items.length) {
|
||||
bootbox.alert('Please select at least one item to return.');
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: return_id,
|
||||
po_id: $('#po_id').val() || 0,
|
||||
invoice_id: $('#invoice_id').val() || 0,
|
||||
contact_id: $('#contact_id').val() || 0,
|
||||
return_date: $('#return_date').val().split('/').reverse().join('-'),
|
||||
reason: $('#reason').val(),
|
||||
items: JSON.stringify(items),
|
||||
notes: '',
|
||||
tax_adjustment: parseFloat($('#tax_adjustment').val()) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
function persist_return(onSuccess) {
|
||||
var payload = build_payload();
|
||||
if (!payload) return false;
|
||||
return ajax_request({
|
||||
url: '<?php echo $server_url?>po/api/engine/manage_supplier_return.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: return_id ? 'update' : 'create',
|
||||
data: payload,
|
||||
onSuccess: onSuccess
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Actions ───────────────────────────────────────────────────────────────
|
||||
function save_tracking() {
|
||||
var current = (return_data && return_data.shipping_tracking_number) ? return_data.shipping_tracking_number : '';
|
||||
bootbox.prompt({
|
||||
title: '<i class="ti ti-truck-delivery me-2"></i>Shipping Tracking Number',
|
||||
value: current,
|
||||
inputType: 'text',
|
||||
placeholder: 'Enter carrier tracking number...',
|
||||
callback: function(result) {
|
||||
if (result === null) return;
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>po/api/engine/manage_supplier_return.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'update',
|
||||
data: {
|
||||
id: return_id,
|
||||
_action: 'update_tracking',
|
||||
shipping_tracking_number: result.trim(),
|
||||
},
|
||||
onSuccess: function() { retrieve_return(); }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function save_return() {
|
||||
persist_return(function(res) {
|
||||
if (!return_id && res.new_id) {
|
||||
window.location.href = '<?php echo $server_url?>po/manage_supplier_return.php?id=' + res.new_id;
|
||||
} else {
|
||||
retrieve_return();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function confirm_return() {
|
||||
bootbox.confirm({
|
||||
message: 'Confirm this supplier return? Items will be removed from stock.',
|
||||
buttons: {
|
||||
confirm: { label: 'Confirm Return', className: 'btn-success' },
|
||||
cancel: { label: 'Back', className: 'btn-secondary' }
|
||||
},
|
||||
callback: function(result) {
|
||||
if (!result) return;
|
||||
persist_return(function() {
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>po/api/engine/confirm_supplier_return.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'update',
|
||||
data: { id: return_id },
|
||||
onSuccess: function() { retrieve_return(); }
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function proceed_to_supplier_credit_note() {
|
||||
bootbox.confirm({
|
||||
message: 'Create a supplier credit note for this supplier return?',
|
||||
buttons: {
|
||||
confirm: { label: 'Create Supplier Credit Note', className: 'btn-warning' },
|
||||
cancel: { label: 'Back', className: 'btn-secondary' }
|
||||
},
|
||||
callback: function(result) {
|
||||
if (!result) return;
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>po/api/engine/proceed_to_supplier_credit_note.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'create',
|
||||
data: { return_id: return_id },
|
||||
onSuccess: function(res) {
|
||||
window.location.href = '<?php echo $server_url?>po/manage_purchase_invoice.php?id=' + res.new_id;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cancel_return() {
|
||||
bootbox.confirm({
|
||||
message: 'Cancel this supplier return?',
|
||||
buttons: {
|
||||
confirm: { label: 'Yes, Cancel', className: 'btn-danger' },
|
||||
cancel: { label: 'Back', className: 'btn-secondary' }
|
||||
},
|
||||
callback: function(result) {
|
||||
if (!result) return;
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>po/api/engine/cancel_supplier_return.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'update',
|
||||
data: { id: return_id },
|
||||
onSuccess: function() { retrieve_return(); }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Boot ─────────────────────────────────────────────────────────────────
|
||||
$(async function() {
|
||||
try {
|
||||
flatpickr('#return_date', { dateFormat: 'd/m/Y', allowInput: true });
|
||||
await load_dept_cache();
|
||||
|
||||
if (return_id) {
|
||||
await retrieve_return();
|
||||
} else {
|
||||
var pre_po = <?php echo $po_id; ?>;
|
||||
if (pre_po) await load_po(pre_po, null);
|
||||
$('#return_date').val(flatpickr.formatDate(new Date(), 'd/m/Y'));
|
||||
set_mode(0);
|
||||
}
|
||||
} catch(e) { console.error(e); }
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+34
-33
@@ -75,8 +75,8 @@
|
||||
<i class="ti ti-circle-check fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 text-muted small">Completed</p>
|
||||
<h3 class="fw-bold mb-0" id="stat_completed">—</h3>
|
||||
<p class="mb-0 text-muted small">Received</p>
|
||||
<h3 class="fw-bold mb-0" id="stat_received">—</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,7 +90,7 @@
|
||||
<div class="card">
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-lg-5">
|
||||
<div class="col-lg-4">
|
||||
<div class="position-relative">
|
||||
<input type="text" id="search_input" class="form-control ps-9"
|
||||
placeholder="Search PO number or supplier...">
|
||||
@@ -99,22 +99,18 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<div class="col-lg-2">
|
||||
<select id="filter_status" class="form-select">
|
||||
<option value="">All Status</option>
|
||||
<option value="-2">Pending Warehouse</option>
|
||||
<option value="-1">Cancelled</option>
|
||||
<option value="0">Draft</option>
|
||||
<option value="1">Confirmed</option>
|
||||
<option value="2">Partial</option>
|
||||
<option value="3">Completed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select id="filter_payment" class="form-select">
|
||||
<option value="">All Payment</option>
|
||||
<option value="0">Unpaid</option>
|
||||
<option value="1">Paid</option>
|
||||
<option value="2">Partial</option>
|
||||
<select id="filter_dept" class="form-select">
|
||||
<option value="">All Departments</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
@@ -137,10 +133,11 @@
|
||||
<th class="py-3">PO #</th>
|
||||
<th class="py-3">Date</th>
|
||||
<th class="py-3">Supplier</th>
|
||||
<th class="py-3">Department</th>
|
||||
<th class="py-3">Items</th>
|
||||
<th class="py-3">Grand Total</th>
|
||||
<th class="py-3">Status</th>
|
||||
<th class="py-3">Payment</th>
|
||||
<th class="py-3">Stock</th>
|
||||
<th class="py-3">Expected</th>
|
||||
<th class="py-3">Action</th>
|
||||
</tr>
|
||||
@@ -164,26 +161,22 @@
|
||||
function po_status_badge(status) {
|
||||
status = parseInt(status);
|
||||
const map = {
|
||||
'-2': '<span class="badge bg-danger">Pending Warehouse</span>',
|
||||
'-1': '<span class="badge bg-secondary">Cancelled</span>',
|
||||
'0': '<span class="badge bg-warning text-dark">Draft</span>',
|
||||
'1': '<span class="badge bg-info text-white">Confirmed</span>',
|
||||
'2': '<span class="badge bg-primary">Partial</span>',
|
||||
'3': '<span class="badge bg-success">Completed</span>',
|
||||
};
|
||||
return map[String(status)] ?? '<span class="badge bg-light text-dark">—</span>';
|
||||
}
|
||||
|
||||
function po_payment_badge(status) {
|
||||
status = parseInt(status);
|
||||
function receipt_status_badge(status) {
|
||||
const map = {
|
||||
'0': '<span class="badge bg-warning text-dark">Unpaid</span>',
|
||||
'1': '<span class="badge bg-success">Paid</span>',
|
||||
'2': '<span class="badge bg-info text-white">Partial</span>',
|
||||
'2': '<span class="badge bg-primary">Partial</span>',
|
||||
'3': '<span class="badge bg-success">Received</span>',
|
||||
};
|
||||
return map[String(status)] ?? '<span class="badge bg-light text-dark">—</span>';
|
||||
return map[String(status)] ?? '<span class="text-muted">—</span>';
|
||||
}
|
||||
|
||||
|
||||
function retrieve_pos() {
|
||||
return ajax_request({
|
||||
url: '<?php echo $server_url?>po/api/engine/retrieve_po.php',
|
||||
@@ -193,23 +186,23 @@
|
||||
onSuccess: function(res) {
|
||||
var all = res.output || [];
|
||||
|
||||
var keyword = $('#search_input').val().toLowerCase();
|
||||
var fil_status = $('#filter_status').val();
|
||||
var fil_payment = $('#filter_payment').val();
|
||||
var keyword = $('#search_input').val().toLowerCase();
|
||||
var fil_status = $('#filter_status').val();
|
||||
var fil_dept = $('#filter_dept').val();
|
||||
|
||||
var data = all.filter(function(o) {
|
||||
var match_kw = !keyword ||
|
||||
var match_kw = !keyword ||
|
||||
(o.po_number || '').toLowerCase().includes(keyword) ||
|
||||
(o.contact_name || '').toLowerCase().includes(keyword);
|
||||
var match_st = fil_status === '' || String(o.status) === fil_status;
|
||||
var match_pm = fil_payment === '' || String(o.payment_status) === fil_payment;
|
||||
return match_kw && match_st && match_pm;
|
||||
var match_st = fil_status === '' || String(o.status) === fil_status;
|
||||
var match_dept = !fil_dept || String(o.department_id) === fil_dept;
|
||||
return match_kw && match_st && match_dept;
|
||||
});
|
||||
|
||||
$('#stat_total').text(format_number(all.length));
|
||||
$('#stat_draft').text(format_number(all.filter(o => parseInt(o.status) === 0).length));
|
||||
$('#stat_confirmed').text(format_number(all.filter(o => parseInt(o.status) === 1).length));
|
||||
$('#stat_completed').text(format_number(all.filter(o => parseInt(o.status) === 3).length));
|
||||
$('#stat_received').text(format_number(all.filter(o => parseInt(o.receipt_status) === 3).length));
|
||||
|
||||
alasql('CREATE TABLE IF NOT EXISTS po_list');
|
||||
alasql.tables.po_list.data = data;
|
||||
@@ -233,21 +226,26 @@
|
||||
$.each(page_data, function(i, o) {
|
||||
var items = JSON.parse(o.items || '[]');
|
||||
var item_count = items.length;
|
||||
var can_cancel = parseInt(o.status) >= 0 && parseInt(o.status) <= 1;
|
||||
var can_cancel = parseInt(o.status) >= -2 && parseInt(o.status) <= 1;
|
||||
|
||||
body += `<tr>
|
||||
<td class="py-3 fw-semibold">${o.po_number}</td>
|
||||
<td class="py-3">${format_date(o.po_date)}</td>
|
||||
<td class="py-3">${o.contact_name || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${get_dept_label(o.department_id)}</td>
|
||||
<td class="py-3">${item_count} item${item_count !== 1 ? 's' : ''}</td>
|
||||
<td class="py-3 fw-semibold">${format_number(o.grand_total, 2)}</td>
|
||||
<td class="py-3">${po_status_badge(o.status)}</td>
|
||||
<td class="py-3">${po_payment_badge(o.payment_status)}</td>
|
||||
<td class="py-3">${receipt_status_badge(o.receipt_status)}</td>
|
||||
<td class="py-3">${o.expected_date ? format_date(o.expected_date) : '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">
|
||||
<a href="<?php echo $server_url?>po/manage_po.php?id=${o.id}" title="View">
|
||||
<i class="ti ti-eye fs-5"></i>
|
||||
</a>
|
||||
<a href="javascript:void(0);" class="ms-2 text-secondary" title="Stock"
|
||||
onclick="show_stock_rows('po', ${o.id}, '${escape_html(o.po_number || '')}')">
|
||||
<i class="ti ti-packages fs-5"></i>
|
||||
</a>
|
||||
${can_cancel ? `
|
||||
<a href="javascript:void(0);" class="link-danger ms-2" title="Cancel"
|
||||
onclick="cancel_po(${o.id})">
|
||||
@@ -285,14 +283,17 @@
|
||||
}
|
||||
|
||||
|
||||
$(function() {
|
||||
$(async function() {
|
||||
await load_dept_cache();
|
||||
populate_dept_filter('filter_dept');
|
||||
retrieve_pos();
|
||||
$('#search_input').on('keyup', function(e) {
|
||||
if (e.key === 'Enter') retrieve_pos();
|
||||
});
|
||||
$('#filter_status, #filter_dept').on('change', retrieve_pos);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../config.php';
|
||||
require '../include_header.php';
|
||||
?>
|
||||
|
||||
<body>
|
||||
<?php require '../include_topbar.php'; ?>
|
||||
<?php require '../include_sidebar.php'; ?>
|
||||
|
||||
<main id="content" class="content py-15">
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row mb-6">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="fs-3 mb-1">Supplier Returns</h1>
|
||||
<p class="mb-0 text-muted">Goods returned to suppliers</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-5 mb-5">
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="card p-4">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="icon-shape icon-md bg-primary bg-opacity-10 text-primary rounded-2">
|
||||
<i class="ti ti-arrow-back-up fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 text-muted small">Total</p>
|
||||
<h3 class="fw-bold mb-0" id="stat_total">—</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="card p-4">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="icon-shape icon-md bg-warning bg-opacity-10 text-warning rounded-2">
|
||||
<i class="ti ti-pencil fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 text-muted small">Draft</p>
|
||||
<h3 class="fw-bold mb-0" id="stat_draft">—</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="card p-4">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="icon-shape icon-md bg-success bg-opacity-10 text-success rounded-2">
|
||||
<i class="ti ti-circle-check fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 text-muted small">Confirmed</p>
|
||||
<h3 class="fw-bold mb-0" id="stat_confirmed">—</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="card p-4">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="icon-shape icon-md bg-secondary bg-opacity-10 text-secondary rounded-2">
|
||||
<i class="ti ti-x fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 text-muted small">Cancelled</p>
|
||||
<h3 class="fw-bold mb-0" id="stat_cancelled">—</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-5">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-lg-4">
|
||||
<div class="position-relative">
|
||||
<input type="text" id="search_input" class="form-control ps-9"
|
||||
placeholder="Search return number, PO, or supplier...">
|
||||
<span class="position-absolute top-25 ms-4">
|
||||
<i class="ti ti-search text-muted"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select id="filter_status" class="form-select">
|
||||
<option value="">All Status</option>
|
||||
<option value="-1">Cancelled</option>
|
||||
<option value="0">Draft</option>
|
||||
<option value="1">Confirmed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select id="filter_dept" class="form-select">
|
||||
<option value="">All Departments</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<button class="btn btn-secondary w-100" onclick="retrieve_returns()">
|
||||
<i class="ti ti-search me-1"></i>Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-5">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 table-centered" id="return_list">
|
||||
<thead class="table-primary border-light">
|
||||
<tr>
|
||||
<th>Return #</th>
|
||||
<th>PO #</th>
|
||||
<th>Date</th>
|
||||
<th>Supplier</th>
|
||||
<th>Department</th>
|
||||
<th>Items</th>
|
||||
<th>Refund Amount</th>
|
||||
<th>Status</th>
|
||||
<th>Fulfillment</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
<tfoot></tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php require '../include_ending.php'; ?>
|
||||
|
||||
<script>
|
||||
function return_status_badge(status) {
|
||||
const map = {
|
||||
'-1': '<span class="badge bg-secondary">Cancelled</span>',
|
||||
'0': '<span class="badge bg-warning text-dark">Draft</span>',
|
||||
'1': '<span class="badge bg-success">Confirmed</span>',
|
||||
};
|
||||
return map[String(status)] ?? '—';
|
||||
}
|
||||
|
||||
function fulfillment_status_badge(status) {
|
||||
const map = {
|
||||
'1': '<span class="badge bg-warning text-dark"><i class="ti ti-list-check me-1"></i>Picking</span>',
|
||||
'2': '<span class="badge bg-info text-white"><i class="ti ti-package me-1"></i>Packed</span>',
|
||||
'3': '<span class="badge bg-success"><i class="ti ti-truck me-1"></i>Shipped</span>',
|
||||
};
|
||||
return map[String(status)] ?? '<span class="text-muted">—</span>';
|
||||
}
|
||||
|
||||
function retrieve_returns() {
|
||||
return ajax_request({
|
||||
url: '<?php echo $server_url?>po/api/engine/retrieve_supplier_return.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var all = res.output || [];
|
||||
|
||||
var keyword = $('#search_input').val().toLowerCase();
|
||||
var fil_status = $('#filter_status').val();
|
||||
var fil_dept = $('#filter_dept').val();
|
||||
|
||||
var data = all.filter(function(r) {
|
||||
var match_kw = !keyword ||
|
||||
(r.return_number || '').toLowerCase().includes(keyword) ||
|
||||
(r.po_number || '').toLowerCase().includes(keyword) ||
|
||||
(r.contact_name || '').toLowerCase().includes(keyword);
|
||||
var match_st = fil_status === '' || String(r.status) === fil_status;
|
||||
var match_dept = !fil_dept || String(r.department_id) === fil_dept;
|
||||
return match_kw && match_st && match_dept;
|
||||
});
|
||||
|
||||
$('#stat_total').text(format_number(all.length));
|
||||
$('#stat_draft').text(format_number(all.filter(r => String(r.status) === '0').length));
|
||||
$('#stat_confirmed').text(format_number(all.filter(r => String(r.status) === '1').length));
|
||||
$('#stat_cancelled').text(format_number(all.filter(r => String(r.status) === '-1').length));
|
||||
|
||||
alasql('CREATE TABLE IF NOT EXISTS return_list');
|
||||
alasql.tables.return_list.data = data;
|
||||
$('table#return_list tfoot').html(generate_pagination('return_list', data.length));
|
||||
change_page_return_list(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function change_page_return_list(page_num) {
|
||||
var offset = (page_num - 1) * prop_limit;
|
||||
var rows = alasql(`SELECT * FROM return_list LIMIT ${prop_limit} OFFSET ${offset}`);
|
||||
var body = '';
|
||||
|
||||
if (!rows.length) {
|
||||
$('table#return_list tbody').html(
|
||||
'<tr><td colspan="9" class="text-center py-5 text-muted">No supplier returns found.</td></tr>'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$.each(rows, function(i, r) {
|
||||
var items = [];
|
||||
try { items = JSON.parse(r.items || '[]'); } catch(e) {}
|
||||
var item_count = items.length;
|
||||
body += `<tr>
|
||||
<td class="py-3 fw-semibold">${escape_html(r.return_number || '')}</td>
|
||||
<td class="py-3">
|
||||
${r.po_id ? `<a href="<?php echo $server_url?>po/manage_po.php?id=${r.po_id}">${escape_html(r.po_number || 'PO #' + r.po_id)}</a>` : '<span class="text-muted">—</span>'}
|
||||
</td>
|
||||
<td class="py-3">${format_date(r.return_date)}</td>
|
||||
<td class="py-3">${escape_html(r.contact_name || '—')}</td>
|
||||
<td class="py-3">${get_dept_label(r.department_id)}</td>
|
||||
<td class="py-3">${item_count} item${item_count !== 1 ? 's' : ''}</td>
|
||||
<td class="py-3 fw-semibold">${format_number(r.refund_amount, 2)}</td>
|
||||
<td class="py-3">${return_status_badge(r.status)}</td>
|
||||
<td class="py-3">${fulfillment_status_badge(r.fulfillment_status)}</td>
|
||||
<td class="py-3">
|
||||
<a href="<?php echo $server_url?>po/manage_supplier_return.php?id=${r.id}" title="View">
|
||||
<i class="ti ti-eye fs-5"></i>
|
||||
</a>
|
||||
<a href="javascript:void(0);" class="ms-2 text-secondary" title="Stock"
|
||||
onclick="show_stock_rows('supplier_return', ${r.id}, '${escape_html(r.return_number || '')}')">
|
||||
<i class="ti ti-packages fs-5"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
$('table#return_list tbody').html(body);
|
||||
}
|
||||
|
||||
$(async function() {
|
||||
await load_dept_cache();
|
||||
populate_dept_filter('filter_dept');
|
||||
retrieve_returns();
|
||||
$('#search_input').on('keyup', function(e) {
|
||||
if (e.key === 'Enter') retrieve_returns();
|
||||
});
|
||||
$('#filter_status, #filter_dept').on('change', retrieve_returns);
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user